> 文档中心 > 基于HDF的LED驱动程序开发(2)

基于HDF的LED驱动程序开发(2)


引言

本文以小熊派BearPi-HM_Micro_small开发板上的一个LED灯为例,介绍如何基于HDF框架开发一个外设的驱动程序。

在阅读本文之前,建议大家先阅读:《OpenHarmony驱动子系统概述》,对HDF框架有一个基本的了解。

另外,在编写LED灯的驱动程序时,我们会用到很多由HDF框架提供的API。为了便于查阅这些API的用法,建议大家在阅读本文的同时,打开文章《HDF驱动框架的API》(1)、(2)、(3)。在这几篇文章中汇集了本文所有用到的API。当然,你也可以直接去查阅这些API的源码和官方说明文档。


基于HDF框架进行设备驱动开发主要包括两部分工作:驱动配置驱动实现

(1)驱动配置 :主要就是按照HDF框架所定义的设备驱动模型,以某种方式将设备驱动的配置信息(包括:所属设备集合及其属性、所属设备、设备节点及其属性)描述出来,提供给HDF驱动框架使用。只有这样,HDF驱动框架才能对设备驱动进行管理、加载和部署,应用程序才能通过驱动框架找到并使用设备驱动。

(2)驱动实现 :主要就是编写驱动的业务代码,实现驱动的具体功能(注:驱动的功能大多都是以驱动程序对外提供 服务 的形式呈现出来的)。

上一篇: ​ 基于HDF的LED驱动程序开发(1)

二、驱动实现

驱动实现包括编写设备驱动的 业务代码编译脚本 两个部分。

2.1 业务代码

驱动的业务代码包括三部分:驱动服务程序驱动入口函数注册驱动

新建一个文件:device/st/drivers/led/led.c ,用于存放LED的驱动程序。

2.1.1 驱动服务程序

驱动的功能大多都是以对外提供 服务 的形式呈现出来的。驱动服务程序主要由两个部分组成:驱动服务入口结构驱动服务函数

1、驱动服务入口结构体

HDF提供了一个基本的驱动服务入口结构体类型IDeviceIoService,开发者可以用它直接定义一个IDeviceIoService类型的结构体作为驱动服务的入口。

当然,开发者也可以先自定义一个驱动服务入口结构体类型,然后再创建一个这种自定义的结构体作为驱动服务的入口,但是开发者自定义的驱动服务入口结构体类型中的第一个成员必须是IDeviceIoService类型结构体。所以,无论是直接使用结构体类型IDeviceIoService,还是自定义驱动服务入口结构体类型,驱动服务的入口都是IDeviceIoService类型的结构体。

以下是在LED驱动程序中自定义的一个驱动服务入口结构体类型:

//自定义驱动服务入口结构体struct LedDriverService {    struct IDeviceIoService ioService;// 首个成员必须是IDeviceIoService结构体    int32_t (*ServiceA)(void); // 驱动的服务函数    int32_t (*ServiceB)(uint32_t inputCode); // 驱动的服务函数,可以添加多个驱动服务函数};

可以看到:在HDF提供的结构体类型IDeviceIoService中,只有一个驱动服务函数(Dispatch函数)入口/指针,而且这个驱动服务函数的参数和调用机制都是HDF已经规定好的,开发者仅仅是在这个函数中实现驱动服务;在自定义驱动服务入口结构体中,除了可以使用IDeviceIoService结构体中的Dispatch函数以外,还可以再添加多个驱动服务函数指针,这些指针指向的驱动服务函数完全是由驱动开发者自己定义和实现的。

2、驱动服务函数

由上可知,驱动服务函数可以分成两种:Dispatch函数非Dispatch函数

(1)Dispatch函数

Dispatch函数就是HDF定义的结构体IDeviceIoService中的函数指针Dispatch所指向的函数。用户态的应用程序与内核态的驱动程序之间的交互必须使用Dispatch函数。

int32_t (*Dispatch)(struct HdfDeviceIoClient *client, int cmdId, struct HdfSBuf *data, struct HdfSBuf *reply);

这个函数中的代码由驱动开发者负责编写,在应用程序使用驱动服务的时候被HDF调用。HDF调用这个函数的时候,会传进来4个参数:

参数 类型 描述
client HdfDeviceIoClient结构体指针 这个结构体用于存放HDF设备IO服务客户端的信息。
cmdId 整数 来自应用程序的命令字。
data HdfSBuf结构体指针 指向一个缓冲区,缓冲区用于存放从应用程序接收的数据。
reply HdfSBuf结构体指针 指向一个缓冲区,缓冲区用于存放向应用程序回送的数据。

以下是LED驱动程序中的Dispatch函数的代码:

#define LED_WRITE_READ  1   //应用程序下发给LED驱动的命令字。1:写;0:读。
uint8_t status = 0;  //LED的状态。0:灭;1:亮。
//LED的控制数据enum LedOps {    LED_OFF,    //点亮    LED_ON,     //熄灭    LED_TOGGLE, //翻转};
//定义一个结构体,用来存放驱动LED的GPIO端口的编号。struct Stm32Mp1ILed {    uint32_t gpioNum;};static struct Stm32Mp1ILed g_Stm32Mp1ILed;
//LED驱动的Dispatch函数int32_t LedDriverDispatch(struct HdfDeviceIoClient *client, int cmdCode, struct HdfSBuf *data, struct HdfSBuf *reply){    int ret = HDF_SUCCESS;    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里读取一个8-bit无符号整数,赋值给变量contrl */     HdfSbufReadUint8(data, &contrl);     /* 控制LED */     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 (!HdfSbufWriteUint8(reply, status)){  HDF_LOGE("replay is fail");  return HDF_FAILURE;     }  break;     default:     break;    } return ret;}

在这段代码中,使用了HDF提供的以下这些接口函数:

bool HdfSbufReadUint8 (struct HdfSBuf *sbuf, uint8_t *value )bool HdfSbufWriteUint8(struct HdfSBuf *sbuf, uint8_t value)

在这段代码中,使用了HDF提供的宏HDF_LOGE,用于向终端打印输出信息。

在这段代码中,还使用了平台设备GPIO的驱动接口,通过改变GPIO端口的输出电平来控制LED灯的亮/灭。

int32_t GpioWrite(uint16_t gpio, uint16_t val)

用户态的应用程序与内核态的驱动程序之间进行交互的时候,常常会用到HDF提供的两种 消息机制

(1)第一种消息机制:应用先发送消息给驱动,驱动收到消息后再回应消息给应用。上面Dispatch函数中的代码使用的就是这种消息机制。

(2)第二种消息机制:驱动主动发消息(上报事件)给应用,应用监听到驱动上报事件后,自动执行回调函数对收到的消息进行处理。注:我将另外写一篇文章讨论并编程测试这种消息机制。

(2)非Dispatch函数

非Dispatch函数,指的是在自定义的驱动服务入口结构体中、在IDeviceIoService结构体后面添加的那些函数指针所指向的函数。

以下是LED驱动的两个非Dispatch服务函数:

//切换LED的状态int32_t LedDriverServiceA(void){    if(status == 0)    { GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_LOW); status = 1;    }    else    { GpioWrite(g_Stm32Mp1ILed.gpioNum, GPIO_VAL_HIGH); status = 0;    }  return 0;}
//打开或关闭LEDint32_t LedDriverServiceB(uint32_t inputCode){    switch (inputCode){ //开灯  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; // default:     break;    }    return 0;}

本文中并没有使用这两个函数。关于如何使用这些非Dispatch服务函数,我将另外写一篇文章进行讨论和编程测试。

2.1.2 驱动入口函数

下面编写LED驱动的三个入口函数:Bind函数、Init函数、Release函数。

1、Bind函数

Bind函数的主要任务就是把驱动服务的入口告知HDF驱动框架,或者说把设备的驱动服务入口与代表设备的HdfDeviceObject结构体绑定在一起。以下是LED驱动的Bind函数:

// 驱动入口:Bind函数int32_t HdfLedDriverBind(struct HdfDeviceObject *deviceObject){    //检查HDF设备对象结构体是否存在    if (deviceObject == NULL)    { HDF_LOGE("Led driver bind failed!"); return HDF_ERR_INVALID_OBJECT;    } //用自定义的驱动服务入口结构体类型创建驱动服务入口    static struct LedDriverService ledDriver = {    .ioService.Dispatch = LedDriverDispatch, .ServiceA = LedDriverServiceA, .ServiceB = LedDriverServiceB,    } //也可以直接用IDeviceIoService结构体类型创建驱动服务入口    /*    static struct IDeviceIoService ledDriver = { .Dispatch = LedDriverDispatch,    };    */    //注册驱动服务入口    deviceObject->service = (struct IDeviceIoService *)(&ledDriver);    HDF_LOGD("Led driver bind success");    return HDF_SUCCESS;}

2、Init函数

Init函数主要是用来完成一些驱动的初始化动作,比如:通过HDF提供的接口获取设备的配置信息、初始化设备、订阅驱动提供的服务等等。

//定义一个结构体,用来存放驱动LED的GPIO端口的编号。struct Stm32Mp1ILed {    uint32_t gpioNum;};static struct Stm32Mp1ILed g_Stm32Mp1ILed;

以下是 LED驱动的Init函数:

// 驱动入口:Init函数int32_t HdfLedDriverInit(struct HdfDeviceObject *device){    struct Stm32Mp1ILed *led = &g_Stm32Mp1ILed;    int32_t ret;    /* 检查HDF设备对象结构体里的property字段是否有效 */    if (device == NULL || device->property == NULL) { HDF_LOGE("%s: device or property NULL!", __func__); return HDF_ERR_INVALID_OBJECT;    }    /* 读取LED的配置信息 */    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;}

在Init函数中调用了一个自定义函数Stm32LedReadDrs,用于读取LED的配置信息(驱动LED的GPIO端口的编号):

static int32_t Stm32LedReadDrs(struct Stm32Mp1ILed *led, const struct DeviceResourceNode *property){    int32_t ret;    struct DeviceResourceIface *drsOps = NULL;    /* 根据设备配置文件的类型HDF_CONFIG_SOURCE,获取DeviceResourceIface结构体的地址。*/    drsOps = DeviceResourceGetIfaceInstance(HDF_CONFIG_SOURCE);    if (drsOps == NULL || drsOps->GetUint32 == NULL) { HDF_LOGE("%s: invalid drs ops!", __func__); return HDF_FAILURE;    }    /* 读取LED配置里面的led_gpio_num属性 */    ret = drsOps->GetUint32(property, "led_gpio_num", &led->gpioNum, 0);     if (ret != HDF_SUCCESS) { HDF_LOGE("%s: read led gpio num fail!", __func__); return ret;    }    return HDF_SUCCESS;}

在函数Stm32LedReadDrs中,使用到了接口函数DeviceResourceGetIfaceInstance、结构体DeviceResourceIface,以及结构体DeviceResourceIface中函数指针GetUint32所指向的函数。

在Init函数中,调用了更底层的GPIO驱动接口函数GpioSetDir,将驱动LED的GPIO设置成输出。

3、Release函数

Release函数用来释放驱动所占用资源。以下是LED驱动的Release函数:

// 驱动入口:Release函数void HdfLedDriverRelease(struct HdfDeviceObject *deviceObject){    if (deviceObject == NULL)    { HDF_LOGE("Led driver release failed!"); return;    }    HDF_LOGD("Led driver release success");    return;}

2.1.3 注册驱动

注册驱动包括以下两个步骤:

1、创建驱动入口

定义一个HdfDriverEntry结构体类型的全局变量。代码如下所示:

// 驱动入口,HdfDriverEntry结构体类型的全局变量struct HdfDriverEntry g_ledDriverEntry = {    .moduleVersion = 1,   //驱动的版本号    .moduleName = "HDF_LED",     //与device_info.hcs中的驱动名称一致    .Bind    = HdfLedDriverBind,    .Init    = HdfLedDriverInit,    .Release = HdfLedDriverRelease,};

2、注册驱动入口

宏HDF_INIT 将驱动入口注册到HDF驱动框架中,代码如下所示:

// 使用宏HDF_INIT将驱动入口注册到HDF框架中HDF_INIT(g_ledDriverEntry);

2.2 编译脚本

1、为驱动程序led.c新建一个编译脚本:device/st/drivers/led/BUILD.gn,在文件中写入:

import("//drivers/adapter/khdf/liteos/hdf.gni")hdf_driver("hdf_led") {    sources = [ "led.c",    ]}

这个编译脚本指示鸿蒙的编译构建子系统去完成一个类型为hdf_driver、名为hdf_led的编译构建目标。sources列表中是完成这个编译构建目标所需要的源文件。

2、打开上一层编译脚本:device/st/drivers/BUILD.gn,在deps列表中加入刚才那个编译构建目标hdf_led

在这里插入图片描述

上图中的led是一个相对路径,后面省略了:hdf_led


至此,LED灯的驱动程序就开发完了。接下来,在文章《LED的C语言应用程序》中,我们将开发一个LED灯的应用程序来测试一下这个驱动。