【FFH】HDF驱动开发之编写驱动代码 原创 精华
HDF驱动开发之编写驱动代码
::: hljs-center
:::
HDF的基本概念
HDF(Hardware Driver Foundation)是OpenHarmony的驱动程序框架,为驱动开发者提供驱动框架能力,包括驱动加载、驱动服务管理和驱动消息机制。旨在构建统一的驱动架构平台,为驱动开发者提供更精准、更高效的开发环境,力求做到一次开发,多系统部署。
<br>
HDF的整体架构如图:
::: hljs-center
图源《深入浅出OpenHarmony》
:::
自下而上看,其中:
- OSAL层:是操作系统的抽象层,封装了驱动相关的系统调用。
- Adapter层:适配层,厂家需要具体实现的IO操作,与Core一起构成了驱动程序。
- Core 层:平台驱动层,是随OpenHarmony一起发布的平台类驱动程序框架。
- Device层:具体的设备对象。
- Host 层:各种Device按照一定的关系关联在一起,形成Host的概念。
- Manager:OpenHarmony设备管理服务。
- HDI:OpenHarmony驱动接口,供上层框架层调用,作用类似于安卓的HAL。
- 能力库,驱动模型,HCS 等:驱动框架相关的工具。
<br>
本文以点亮一个LED程序为例,编写相关驱动代码。
编写驱动代码
一. 在./device/st/drivers下新建led文件夹(存放驱动源码文件)
1. 在led下新建一个led.c文件——存放驱动源码文件
#include "hdf_device_desc.h" //HDF框架对驱动开发相关能力接口的头文件
#include "hdf_log.h" //HDF框架提供的日志接口头文件
#include "device_resource_if.h"
#include "osal_io.h"
#include "osal.h"
#include "osal_mem.h"
#include "gpio_if.h"
#define HDF_LOG_TAG led_driver // 打印日志所包含的标签,如果不定义则用默认定义的HDF_TAG标签
#define LED_WRITE_READ 1 // 读写操作码1
//枚举灯的可操作选项
enum LedOps {
LED_OFF,
LED_ON,
LED_TOGGLE,
};
struct Stm32Mp1ILed {
uint32_t gpioNum;
};
static struct Stm32Mp1ILed g_Stm32Mp1ILed;
uint8_t status = 0;
// Dispatch是用来处理用户态发下来的消息
int32_t LedDriverDispatch(struct HdfDeviceIoClient *client, int cmdCode, struct HdfSBuf *data, struct HdfSBuf *reply)
{
uint8_t contrl;
HDF_LOGE("Led driver dispatch");
if (client == NULL || client->device == NULL)
{
HDF_LOGE("Led driver device is NULL");
return HDF_ERR_INVALID_OBJECT;
}
switch (cmdCode)
{
/* 接收到用户态发来的LED_WRITE_READ命令 */
case LED_WRITE_READ:
/* 读取data里的数据,赋值给contrl */
HdfSbufReadUint8(data,&contrl);
switch (contrl)
{
/* 开灯 */
case LED_ON:
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_LOW);
status = 1;
break;
/* 关灯 */
case LED_OFF:
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_HIGH);
status = 0;
break;
/* 状态翻转 */
case LED_TOGGLE:
if(status == 0)
{
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_LOW);
status = 1;
}
else
{
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_HIGH);
status = 0;
}
break;
default:
break;
}
/* 把LED的状态值写入reply, 可被带至用户程序 */
if (!HdfSbufWriteInt32(reply, status))
{
HDF_LOGE("replay is fail");
return HDF_FAILURE;
}
break;
default:
break;
}
return HDF_SUCCESS;
}
// 读取驱动私有配置
static int32_t Stm32LedReadDrs(struct Stm32Mp1ILed *led, const struct DeviceResourceNode *node)
{
int32_t ret;
struct DeviceResourceIface *drsOps = NULL;
drsOps = DeviceResourceGetIfaceInstance(HDF_CONFIG_SOURCE);
if (drsOps == NULL || drsOps->GetUint32 == NULL) {
HDF_LOGE("%s: invalid drs ops!", __func__);
return HDF_FAILURE;
}
/* 读取led.hcs里面led_gpio_num的值 */
ret = drsOps->GetUint32(node, "led_gpio_num", &led->gpioNum, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read led gpio num fail!", __func__);
return ret;
}
return HDF_SUCCESS;
}
//驱动对外提供的服务能力,将相关的服务接口绑定到HDF框架
int32_t HdfLedDriverBind(struct HdfDeviceObject *deviceObject)
{
if (deviceObject == NULL)
{
HDF_LOGE("Led driver bind failed!");
return HDF_ERR_INVALID_OBJECT;
}
static struct IDeviceIoService ledDriver = {
.Dispatch = LedDriverDispatch,
};
deviceObject->service = (struct IDeviceIoService *)(&ledDriver);
HDF_LOGD("Led driver bind success");
return HDF_SUCCESS;
}
// 驱动自身业务初始的接口
int32_t HdfLedDriverInit(struct HdfDeviceObject *device)
{
struct Stm32Mp1ILed *led = &g_Stm32Mp1ILed;
int32_t ret;
if (device == NULL || device->property == NULL) {
HDF_LOGE("%s: device or property NULL!", __func__);
return HDF_ERR_INVALID_OBJECT;
}
/* 读取hcs私有属性值 */
ret = Stm32LedReadDrs(led, device->property);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: get led device resource fail:%d", __func__, ret);
return ret;
}
/* 将GPIO管脚配置为输出 */
ret = GpioSetDir(led->gpioNum, GPIO_DIR_OUT);
if (ret != 0)
{
HDF_LOGE("GpioSerDir: failed, ret %d\n", ret);
return ret;
}
HDF_LOGD("Led driver Init success");
return HDF_SUCCESS;
}
// 驱动资源释放的接口
void HdfLedDriverRelease(struct HdfDeviceObject *deviceObject)
{
if (deviceObject == NULL)
{
HDF_LOGE("Led driver release failed!");
return;
}
HDF_LOGD("Led driver release success");
return;
}
// 定义驱动入口的对象,必须为HdfDriverEntry(在hdf_device_desc.h中定义)类型的全局变量
struct HdfDriverEntry g_ledDriverEntry = {
.moduleVersion = 1,
.moduleName = "HDF_LED",
.Bind = HdfLedDriverBind,
.Init = HdfLedDriverInit,
.Release = HdfLedDriverRelease,
};
// 调用HDF_INIT将驱动入口注册到HDF框架中
HDF_INIT(g_ledDriverEntry);
led.c的主要作用是定义驱动程序入口函数,在HDF加载时读取配置信息并初始化GPIO,定义dispatch函数。
接下来,让我们在小熊派官方已有注释的基础上,深入剖析上述代码,理解其实现内容。
<br>
首先我们先来了解一下驱动消息机制。
- 驱动消息机制:
HDF框架提供统一的驱动消息机制,支持用户态应用向内核态驱动发送消息,也支持内核态驱动向用户态应用发送消息。
接口说明
- 用户态应用发送消息到驱动
- 用户台应用接受驱动主动上报事件
以下代码对应业务代码my_led_app.c中“通过Dispatch发送驱动”步骤:
其中object为订阅者的私有数据,service为被订阅的服务对象
ret = serv->dispatcher->Dispatch(&serv->object, LED_WRITE_READ, data, reply);
if (ret != HDF_SUCCESS)
{
printf("fail to send service call!\r\n");
goto out;
}
接下来,实现服务基类成员IDeviceIoService中的Dispatch方法
Dispatch函数,是用来处理用户态发下来的信息的函数。
驱动作为内核态的内容能够处理用户态发下来的消息,同时返回数据,达到处理与用户层的交互逻辑的目的。
此处的LedDriverDispatch是最重要的消息分发处理函数,在这个函数中我们针对预先定义的不同的命令码进行不同的处理。
int32_t LedDriverDispatch(struct HdfDeviceIoClient *client, int cmdCode, struct HdfSBuf *data, struct HdfSBuf *reply)
/*
上述参数可对应业务代码my_led_app.c中“通过Dispatch发送驱动”步骤
*/
{
uint8_t contrl;
HDF_LOGE("Led driver dispatch");
if (client == NULL || client->device == NULL)
{
HDF_LOGE("Led driver device is NULL");
return HDF_ERR_INVALID_OBJECT;
//HDF_ERR_INVALID_OBJECT被定义为-4,表示订阅失败
}
//命令处理
switch (cmdCode)
{
/* 接收到用户态发来的LED_WRITE_READ命令 */
case LED_WRITE_READ://设置模块工作模式
/* 驱动读取业务代码的data数据,并赋值给contrl */
HdfSbufReadUint8(data,&contrl);
//根据contrl选择动作
switch (contrl)
{
/* 开灯 */
case LED_ON:
//对应管脚输出低电平,并确定状态
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_LOW);
status = 1;
break;
/* 关灯 */
case LED_OFF:
//对应管脚输出高电平,并确定状态
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_HIGH);
status = 0;
break;
/* 状态翻转 */
case LED_TOGGLE:
if(status == 0)
{
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_LOW);
status = 1;//转变状态
}
else
{
GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_HIGH);
status = 0;//转变状态
}
break;
default:
break;
}
/* 把LED的状态值,即status,写入reply, 可被带至用户程序 (读取模式)*/
if (!HdfSbufWriteInt32(reply, status))
{
HDF_LOGE("replay is fail");
return HDF_FAILURE;
}
break;
default:
break;
}
return HDF_SUCCESS;
}
第一步:发送LED_WRITE_READ命令到驱动,此处只编写了一个命令,后续可以增加更多命令选项。
第二步:发送data的值到驱动。
第三步:对led灯进行操作后,读取IO口电平状态并写入reply,然后通过reply携带到用户程序。
<br>
- 读取驱动私有配置
static int32_t Stm32LedReadDrs(struct Stm32Mp1ILed *led, const struct DeviceResourceNode *node)
{
int32_t ret;
struct DeviceResourceIface *drsOps = NULL;
drsOps = DeviceResourceGetIfaceInstance(HDF_CONFIG_SOURCE);
if (drsOps == NULL || drsOps->GetUint32 == NULL) {
HDF_LOGE("%s: invalid drs ops!", __func__);
return HDF_FAILURE;//表示,读取失败,无法调用操作系统基础函数
}
/* 读取led.hcs里面led_gpio_num的值 */
ret = drsOps->GetUint32(node, "led_gpio_num", &led->gpioNum, 0);
//封装回传数据
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read led gpio num fail!", __func__);
return ret;
}
return HDF_SUCCESS;
}
如果驱动有私有配置,则可以添加一个驱动的配置文件,用来填写一些驱动的默认配置信息,HDF框架在加载驱动的时候,会将对应的配置信息获取并保存在HdfDeviceObject 中的property里面,通过Bind和Init传递给驱动。而上述代码的作用就是读取驱动私有配置。
我们可以看看device\st\bearpi_hm_micro\liteos_a\hdf_config\led\led_config.hcs下的LED私有配置描述。
root {
LedDriverConfig {
led_gpio_num = 13;
match_attr = "st_stm32mp157_led"; //该字段的值必须和device_info.hcs中的deviceMatchAttr值一致
}
}
上述代码采用了HCS文件格式。简单来说,HCS文件就是将设备相关的资源等配置信息加入到系统镜像中的一种方式。
root是每个HCS文件的缺省根节点,且每个HCS文件有且只有一个,但可以在根节点下面定义子节点,子节点可以嵌套包含子节点。所有的节点都称为node。
HCS定义了一组保留字,用来实现各种语法。其中,上述代码中的match_attr就是保留字之一,用于查找节点的主键。
<br>
- 驱动对外提供的服务能力,将相关服务接口绑定到HDF框架
Bind(绑定)函数:
此函数把一个驱动函数的外部接口绑定到HDF上。在这个函数中,驱动需要定义自己的IDeviceloService 接口的实现,并赋值给 deviceObject->service 。
我们先来初步了解一下IDeviceIoService:
IDeviceIoService是所有驱动服务的接口基类,其继承自HdfObject。IDeviceIoService对外提供了三个标准的接口方法————Open、Dispatch、Release。
其类的定义如下:
struct IDeviceloService {
/** 基类 */
struct HdfObject object;
/** 启动服务函数,在客户端请求驱动服务时被调用 */
Int32_t(Open)(struct HdfDeviceIoClient *client);
/** 分发函数,在客户端调用驱动服务分发消息时被调用*/
int32_t(Dispatch)(struct HdfDeviceIoClient *client,int cmdId,struct HdfSBuf *data,struct HdfSBuf *reply);
/** 释放服务函数,在客户端不再需要驱动服务时被调用*/
void (*Release)(struct HdfDeviceIoClient *client);
本案例代码应用如下:
int32_t HdfLedDriverBind(struct HdfDeviceObject *deviceObject)
{
// deviceObject为HDF框架给每一个驱动创建的设备对象,用来保存设备相关的私有数据和服务接口
if (deviceObject == NULL)
{
HDF_LOGE("Led driver bind failed!");
return HDF_ERR_INVALID_OBJECT;
}
static struct IDeviceIoService ledDriver = {
.Dispatch = LedDriverDispatch,
};
deviceObject->service = (struct IDeviceIoService *)(&ledDriver);
HDF_LOGD("Led driver bind success");
return HDF_SUCCESS;
}
此HdfLedDriverBind函数名在之后需要添加进HdfDriverEntry。
上述代码仅在第9行至第11行使用了Dispatch方法构造了自身的IDeviceloService的接口实现,并在第12行将其赋值给了 deviceObject->service,返回IDeviceIoService实例。
在第10行,采用了之前定义好的LedDriverDispatch函数,此时再结合IDeviceIoService类中dispatch函数参数定义就能理解之前为什么那样给LedDriverDispatch函数传参了。
<br>
- 驱动自身业务初始的接口
Init(初始化)函数:初始化一个驱动程序。一般是对驱动程序的特有数据进行初始化,如果需要分配新的内存,也需要在这个函数中完成。
int32_t HdfLedDriverInit(struct HdfDeviceObject *device)
{
struct Stm32Mp1ILed *led = &g_Stm32Mp1ILed;
int32_t ret;
if (device == NULL || device->property == NULL) {
HDF_LOGE("%s: device or property NULL!", __func__);
return HDF_ERR_INVALID_OBJECT;
}
/* 读取hcs私有属性值 */
ret = Stm32LedReadDrs(led, device->property);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: get led device resource fail:%d", __func__, ret);
return ret;
}
/* 将GPIO管脚配置为输出 */
ret = GpioSetDir(led->gpioNum, GPIO_DIR_OUT);
if (ret != 0)
{
HDF_LOGE("GpioSerDir: failed, ret %d\n", ret);
return ret;
}
HDF_LOGD("Led driver Init success");
return HDF_SUCCESS;
}
第11行即利用了之前定义好的用来读取驱动私有配置的Stm32LedReadDrs函数。
<br>
- 驱动资源释放的接口
Release(释放)函数:销毁一个驱动程序。如果有,需要释放在Init当中申请的内存。
void HdfLedDriverRelease(struct HdfDeviceObject *deviceObject)
{
if (deviceObject == NULL)
{
HDF_LOGE("Led driver release failed!");
return;
}
HDF_LOGD("Led driver release success");
return;
}
<br>
- 定义驱动入口的对象
必须为HdfDriverEntry(hdf_device_desc.h中定义)类型的全局变量
struct HdfDriverEntry g_ledDriverEntry = {
.moduleVersion = 1, //驱动版本号为1
.moduleName = "HDF_LED", //驱动模块名称,需要与HCS当中的名称保持一致
.Bind = HdfLedDriverBind,//驱动对外提供的服务能力,将相关的服务接口绑定到HDF框架
.Init = HdfLedDriverInit, //驱动自身业务初始的接口
.Release = HdfLedDriverRelease, //驱动资源释放的接口
};
HdfDriverEntry是所有驱动程序入口的HDF虚基类,编写驱动程序必须实现此接口。且驱动开发者所实现的子类必须实现Bind、Init、Release这三个虚函数,这些函数都会被HDF自动调用。
该类的定义如下:
struct HdfDriverEntry {
/** 驱动版本号 */
int32_t module Version;
/** 驱动模块名称,需要与HCS当中的名称保持一致*/
const char *moduleName;
/** 把对外提供服务的接口绑定到驱动程序上,deviceObject指向为驱动服务的Node节点*/
int32_t(*Bind)(struct HdfDeviceObject *deviceObject);
/** 初始化驱动程序,会被驱动加载过程自动调用*/
int32_t(*Init)(struct HdfDeviceObject *deviceObject);
/** 释放驱动程序,在加载驱动出错或者驱动卸载的过程中自动调用*/
void(*Release)(struct HdfDeviceObject *deviceObject);
};
device_info.hcs文件中的moduleName必须要和以上驱动文件led.c中的moduleName字段匹配,这样驱动才会加载起来
<br>
- 调用HDF_INIT将驱动入口注册到HDF框架中
每个驱动程序在编写完成后,必须通过HDF_INIT这个宏注册驱动程序,即调用HDF_INIT将驱动入口注册到HDF框架中。获得驱动的入口地址后,在加载驱动时HDF框架会按照定义先调用Bind函数,再调用Init函数加载该驱动,当Init调用异常时,HDF框架会调用Release释放驱动资源并退出。(此说法来源于OpenHarmony文档https://docs.openharmony.cn/pages/v3.1/zh-cn/device-dev/driver/driver-hdf-development.md/,但在《深入浅出OpenHarmony》一书中P145却写的是依次调用驱动Init函数和Bind函数,所以…)
HDF_INIT(g_ledDriverEntry);
//g_ledDriverEntry为驱动入口的对象
<br>
2.在led下新建一个BUILD.gn文件——创建驱动源码编译脚本
在创建完成led.c后,还要把它加入OpenHarmony的构建系统,因而需要在led.c同目录下创建BUILD.gn。
BUILD.gn文件是一种编译构建文件,类似于Cmake。相较于Cmake,当工程规模增大到难以想象的量级时,编译速度和工程模块的划分变得尤为重要,而gn(Gernerate ninja的缩写,用于产生ninja文件,ninja是一个专注于速度的小型构建系统)便很好解决了这两个问题。
具体操作为:
在led/BUILD.gn文件中添加以下代码
● 导入hdf.gni组件
● 定义hdf驱动:将驱动的源文件led.c编译成hdf_led
import("//drivers/adapter/khdf/liteos/hdf.gni") //首先导入.gni组件
hdf_driver("hdf_led") {
sources = [
"led.c", //此处填写模块要编译的源码文件
]
}
<br>
3. 修改drivers的编译脚本使之编译进内核
在/device/st/drivers/BUILD.gn文件中的deps中添加"led"目录,将hdf_led编译进内核
import("//drivers/adapter/khdf/liteos/hdf.gni") //首先导入.gni组件
group("drivers") {
deps = [
"adc",
"pwm",
"uart",
"iwdg",
"led", //此行是新增模块的BUILD.gn所在的目录
"i2c",
"gpio",
"e53_driver",
"stm32mp1xx_hal",
]
}
config("public") { //定义依赖的头文件配置
lib_dirs = [ "//device/st/drivers/libs/ohos/llvm/stm32mp157" ]
ldflags = [ "-Wl,--push-state,--whole-archive" ]
if (defined(LOSCFG_DRIVERS_HDF_PLATFORM_LTDC)) {
ldflags += [ "-lltdc" ]
}
if (defined(LOSCFG_DRIVERS_MMC)) {
ldflags += [ "-lmmc" ]
}
if (defined(LOSCFG_DRIVERS_HDF_WIFI)) {
ldflags += [ "-lhdf_vendor_wifi" ]
}
if (defined(LOSCFG_DRIVERS_HDF_WIFI) && defined(LOSCFG_DRIVERS_HI3881)) {
ldflags += [ "-lhi3881" ]
}
}
后记
参考文献:
1.《深入浅出OpenHarmony》
2.https://gitee.com/bearpi/bearpi-hm_micro_small/blob/master/applications/BearPi/BearPi-HM_Micro/docs/device-dev/编写一个点亮LED灯程序.md
3.https://docs.openharmony.cn/pages/v3.1/zh-cn/device-dev/driver/driver-hdf-overview.md/
作为一名南向设备开发小白,在上手学习BearPi-HM_Micro开发板的前期实在是非常痛苦。接触鸿蒙的这几个月以来,环境配置失误重装导致的一系列问题,读不懂代码,理解不了复杂的编译架构…种种困难让我轻易就中断了OpenHarmony的学习。我曾将这归咎于小熊派已有的教程不全面,让我的学习没有继续的头绪。但其实,最大的问题在于自己的功底不够扎实,对OpenHarmony系统没有理解透彻,没有打好C语言面向对象编程的基础,也没有去广泛的搜索学习资料。当你下定决心要把某个东西搞懂的时候,其实是很容易从外界获得帮助的。
这两天我收到了李传钊老师的《深入浅出OpenHarmony》这本书,一拿到手就赶紧去读了自己之前学习过的HDF驱动程序框架这章,然后在一些疑惑消除后给自己之前写的学习笔记补充了很多内容。
在此,十分感谢李老师给我们学校带来的讲座以及这本书带给我的帮助。
所以,就把这作为我发的第一篇51CTO博客吧。本代码解析可能还有不完善的地方,仅供参考。如有问题,欢迎指正~
感谢楼主解读,很详细的讲解