openharmony GPIO 驱动开发
GPIO 基础知识
GPIO 基础知识——概念
GPIO 基础知识——IO 复用
GPIO 基础知识——GPIO 分组和编号
GPIO 基础知识——用户态测试
HDF 框架下 GPIO 驱动
HDF 框架下的 GPIO 驱动——案例描述(以 HI3516DV300 平台为例,提供代码)
HDF 框架下的 GPIO 驱动——应用绑定服务
HDF 框架下的 GPIO 驱动——用户态 HdfSBuf
HDF 框架下的 GPIO 驱动——应用和驱动通信1
HDF 驱动框架下的 GPIO 驱动——应用和驱动通信2
HDF 框架下的 GPIO 驱动——驱动入口
HDF 框架下的 GPIO 驱动——驱动接收和发送数据
HDF 框架下的 GPIO 驱动——GPIO 配置
HDF 框架下的 GPIO 驱动——GPIO 配置(中断)
HDF 框架下的 GPIO 驱动——防抖和浮空
HDF 框架下的 GPIO 驱动——LED 控制
HDF 框架下的 GPIO 驱动——驱动程序目录和结构
HDF 框架下的 GPIO 驱动——应用程序目录结构
总结
参考链接
GPIO 基础知识
GPIO 基础知识——概念
GPIO:输入或输出高低电平,任意的高低电平的数量和波形组合,无任何协议要求,可以驱动 LED、按键等外设专用 IO:有协议约束的 IO,输入和输出的高低电平的数量、波形组合、波形的持续时间遵循相应的协议,如 I2C、SPI、UART、PWM
- PWM
- i2c
GPIO 基础知识——IO 复用
芯片应提供尽可能多的功能和外部接口,但是芯片的管脚(Pin)数量有限,使用很多 IO 管脚具有多个功能,通过软件配置实现对同一个管脚的分时复用。以 HI3516DV300 为例,共 92 个 GPIO, GPIO3_6 的复用关系如下图:
不是所有 IO 管脚都可以作为 GPIOI,有些只能作为专用 IO,如外接存储芯片,而有些管脚只能作为 GPIO。
GPIO 基础知识——GPIO 分组和编号
数量众多的 GPIO 通过分组管理,因此每个 GPIO 都有一个组号和组内号(组内偏移,offset),不同芯片的 GPIO 分组数量和组内 GPIO 管脚数量定义不同。
例如:比如 RK3399/RK3399Pro 提供 5 组 GPIO(GPIO0~GPIO4)共 122 个,所有的 GPIO 都可以用作中断,GPIO0/GPIO1 可以作为系统唤醒脚,所有 GPIO 都可以软件配置为上拉或者下拉,所有 GPIO 默 认为输入,GPIO 的驱动能力软件可以配置。 关于原理图上的 GPIO 跟 dts 里面的 GPIO 的对应关系,例如GPIO4c0,那么对应的 dts 里面应该是“gpio4 16”。因为 GPIO4A 有 8 个 pin,GPIO4B 也有 8 个 pin,以此计算可得 c0 口就是 16,c1 口就是 17,以此类推; GPIO 的 使用请参考 docs\Kernel\Pin-Ctrl\目录下 《Rockchip Pin-Ctrl 开 发指南 V1.0-20160725.pdf》。
GPIO 基础知识——用户态测试
- 确定 GPIO 管脚编号和电平状态
- 将管脚复用为 GPIO 功能(复位后默认为 GPIO)
- 比如 GPIO3_6 管脚计算得到为 GPIO30,可以执行 echo 30 > export
- 此时,会在 gpio 下新增 gpio30 目录,可以在该目录下执行操作进行 GPIO30 管脚的控制,比如 direction 方向(in、out),电平高低 value(1/0)
HDF 框架下 GPIO 驱动
HDF 框架下的 GPIO 驱动——案例描述(以 HI3516DV300 平台为例,提供代码)
- GPIO0_6 外接 LED,输出低电平点亮 LED、高电平熄灭 LED
- GPIO3_6 外接 KEY,配置为中断,触发方式为双边沿触发
- 用户态程序发送指令到驱动实现点亮和熄灭 LED 操作,驱动程序返回 LED 对应管脚的电平状态到用户态,驱动程序通过形参和事件两种方式实现与应用程序的数据交互
- 按键触发外部中断,中断服务程序可以点亮或熄灭 LED
HDF 框架下的 GPIO 驱动——应用绑定服务
- Linux 系统下应用程序通过 open 系统调用打开 /dev/ 目录下的设备节点,获取设备文件句柄,通过这个文件句柄调用 read/write/ioctl 等系统调用接口,实现对设备的操作
- HDF 框架下用户态应用程序调用特定接口获取驱动程序提供的服务,实现应用和驱动的绑定,应用程序获取到服务后,可基于该服务实现对驱动和设备的操作
//应用程序绑定服务 struct HdfIoService *serv = HdfIoServiceBind("GPIO_TEST_SERVICE"); if(serv == NULL){ HDF_LOGE("fail to get service %s", "GPIO_TEST_SERVICE"); return HDF_FAILURE; } gpio_drv_test_host::host{ hostName = "gpio_drv_test"; priority = 100; device_test_driver::device{ device0::deviceNode{ policy = 2; priority = 100; preload = 0; permission = 0664; moduleName = "GPIO_TEST_DRIVER"; serviceName = "GPIO_TEST_SERVICE"; } } }
HDF 框架下的 GPIO 驱动——用户态 HdfSBuf
- 应用程序获取驱动服务后,就可以利用服务实现和驱动的通信。通信数据的载体是 HdfSBuf,应用程序调用 HdfSBufObtainDefaultSize 可以获取一个默认大小为 256 字节的内存堆空间, HDF 将该内存空间组织为一个环形队列。应用程序会将数据写入该队列,驱动程序可以从队列中读取数据,反之亦然。由于是环形队列,需要保证读取数据的顺序、读取的数据类型与写入数据的一致,遵循先进先出的原则。
//用户态申请空间 struct HdfSBuf *data = HdfSBufObtainDefaultSize(); if(data == NULL){ printf("fail to obtain sbuf data\n"); ret = HDF_DEV_ERR_NO_MEMORY; goto out; } //用户态写数据 if(!HdfSbufWriteString(data, eventData)){ printf("fail to write data\n"); goto HDF_FAILURE; } //用户态读数据 char *replyData = HdfSbufReadString(data); if(replyData == NULL){ printf("fail to read data\n"); goto HDF_FAILURE; }
HDF 框架下的 GPIO 驱动——应用和驱动通信1
- 应用程序申请两个缓存区(环形队列),用于和驱动程序进行数据交互
- 应用程序向 data 缓冲区写入 String 类型数据
- 应用程序通过服务的 Dispatch 函数向驱动程序发送数据,导致驱动的 Dispatch 函数被执行
- 用户程序读取驱动返回的数据:首先获取 String 类型,再获取 uint16 类型
struct HdfSBuf *data = HdfSBufObtainDefaultSize(); struct HdfSBuf *reply = HdfSBufObtainDefaultSize(); if(id == LED_ON){ SbufWriteString(data, eventData); ret = ser->dispatcher->Dispatch(&serv->object, LED_ON, data, reply); string = HdfSbufReadString(data)l; HdfSbufReadUint16(reply, &pin_val); }
HDF 驱动框架下的 GPIO 驱动——应用和驱动通信2
应用程序通过已获取的服务注册一个事件监听器,当驱动程序调用事件发送函数 HdfDeviceSendEvent 后,会触发事件监听器的 callBack 回调函数的执行,在该回调函数中接收驱动发送的数据
static struct HdfDevEventlistener listener = { .callBack = OnDevEventReceived, .priv = "Service0" }; //应用程序注册服务 if(HdfDeviceRegisterEventListener(serv, &listener) != HDF_SUCESS){ HDF_LOGE("fail to register event listener"); return HDF_FAILURE; }
- 驱动程序调用事件发送接口 HdfDeviceSendEvent 向用户态程序发送事件,触发用户态事件监听器执行
- 应用程序按照驱动程序写入缓冲区的顺序读取数据:首先读取 string 类型数据;再读取 uint16 类型数据
static int32_t OnDevEventReceived(void *priv, uint32_t id, struct HdfSBuf *reply) { uint16_t pin_val = 0; const char *string = HdfSbufReadString(reply); if(string == NULL){ printf("fail to read string in event reply\n"); return HDF_FAILURE; } if(!HdfSbufReadUint16(reply, &pin_val){ printf("fail to read uint16 in event reply\n"); return HDF_FAILURE; } printf("%s: event reply received : id %u, string %s, pin val %u\n", (char *)priv, id, string, pin_val); return HDF_SUCCESS; }
HDF 框架下的 GPIO 驱动——驱动入口
- 驱动入口 g_GPIODriverEntry 中定义了三个函数和驱动模块名字 moduleName;
- device_info.hcs 中新增了一个节点 gpio_drv_test_host, 包含一个名为 moduleName 的属性;
- 两个 moduleName 的值相等时表示 hcs 和驱动匹配成功,进而调用驱动入口的 Bind 函数、Init 函数。
该过程类似于 dts 和 Linux 驱动中的 compatible 字段,当两者匹配时调用驱动中的 probe 函数
struct HdfDriverEntry g_GPIODriverEntry = { .moduleVersion = 1, .moduleName = "GPIO_TEST_DRIVER", .Bind = HdfGPIODriverBind, .Init = HdfGPIODriverInit, .Release = HdfGPIODriverRelease, } HDF_INIT(g_GPIODriverEntry); //device_info.hcs gpio_drv_test_host :: host{ hostName = "gpio_drv_test"; priority = 100; device_test_driver :: device{ device0 :: deviceNode{ policy = 2; priority = 100; preload = 0; permission = 0664; moduleName = "GPIO_TEST_DRIVER"; serviceName = "GPIO_TEST_SERVICE"; } } }
int32_t HdfGPIODriverBind(strutc HdfDeviceObject *deviceObject) { if(deviceObject == NULL){ return HDF_FAILURE; } static struct IDeviceIoService gpioTestService = { .Dispatch = HdfGPIODriverDispatch, }; deviceObject->service = &gpioTestService; HDF_LOGE("GPIO driver bind success"); return HDF_SUCCESS; }
* Bind 函数中定义了一个结构体,它是一个名为 gpioTestService 的服务,需要实现 Dispatch 成员函数,该函数用来接收用户态程序发送到内核态的消息,实现用户态和内核态之间的通信。 * 驱动中定义了一个服务,即驱动可以对外提供服务,应用程序可以使用该服务。
HDF 框架下的 GPIO 驱动——驱动接收和发送数据
- 应用程序首先获取服务,调用服务中定义的 Dispatch 接口可触发该函数的执行,通过参数 id 区分来自用户程序的指令
- 读取用户态发送的 string 类型数据,执行点亮或者熄灭 LED 的操作
- 返回一个字符串和 LED 对应管脚的电平状态给用户程序,用户态程序应该首先读取第一个 string 类型数据,再读取uint16 类型数据,遵循 FIFO 原则
- 注意驱动程序向用户程序返回数据的两种方式:
* 返回值和发送事件,针对不同的方式 * 用户态获取驱动数据的方式也不同
int32_tHdfGPIODriverDispatch(struct HdfDeviceIoClient *client, int32_t id, struct HdfSBuf *data, struct HdfSBuf *reply) { //驱动程序通过接口读取来自用户态的数据,并向用户态返回数据 if(id == LED_ON){ const char *readData = HdfSbufReadString(data); if(readData != NULL){ HDF_LOGE("%s: read data is : %s", __func__, readData); led_on(); GpioRead(LED_PIN, &led_pin_val); } HdfSbufWriteString(reply, "ledon"); HdfSbufWriteUint16(reply, led_pin_val); } else if(id == LED_OFF){ const char *readData = HdfSbufReadString(data); if(readData != NULL){ HDF_LOGE("%s: read data is :%s", __func__, readData); led_off(); GpioRead(LED_PIN, &led_pin_val); } HdfSbufWriteString(reply, "ledoff"); HdfSbufWriteUint16(reply, led_pin_val); if(HdfDeviceSend(client->device, id, reply) != HDF_SUCCESS) return HDF_FAILURE; } return HDF_SUCCESS; }
HDF 框架下的 GPIO 驱动——GPIO 配置
功能分类 接口名 描述
GPIO 读写 GpioRead 读管脚电平值
GpioWrite 写管脚电平值
GPIO 配置 GpioSetDir 设置管脚方向
GpioGetDir 获取管脚方向
GPIO 中断设置 GpioSetIrq 设置管脚对应的中断服务函数
GpioUnSetIrq 取消管脚对应的中断服务函数
GpioEnableIrq 使能管脚中断
GpioDisableIrq 禁止管脚中断
#define LED_PIN 6// GPIO0_6, 0*8+6 = 6 #define IRQ_PIN 30//GPIO3_6 3*8+6 = 30 static int32_t GpioSetup() { //驱动程序,配置 GPIO if(GpioSetDir(LED_PIN, GPIO_DIR_OUT) != HDF_SUCCESS){ HDF_LOGE("GPIOsetDir: LED_PIN failed\n"); return HDF_FAILURE; } GpioSetDir(IRQ_PIN, GPIO_DIR_IN); GpioDisableIrq(IRQ_PIN); GpioSetIrq(IRQ_PIN, OSAL_IRQF_IRIGGER_RISING | OSAL_IRQF_TRIGGER_FALLING, gpio_test_irq, NULL); GpioEnableIrq(IRQ_PIN); }
何处调用 GpioSetup, Init() 还是 Dispatch ?
HDF 框架下的 GPIO 驱动——GPIO 配置(中断)
- 中断触发方式
参数 中断触发方式
OSAL_IRQF_TRIGGER_RISING 上升沿触发
OSAL_IRQF_TRIGGER_FALLING 下降沿触发
OSAL_IRQF_TRIGGER_HIGH 高电平触发
OSAL_IRQF_TRIGGER_LOW 低电平触发
int32_t gpio_testr_irq(uint16_t gpio, void *data) { //驱动,中断服务程序 if(GpioDisableIrq(gpio) != HDF_SUCCESS){ HDF_LOGE("%s: disable irq failed", __func__); return HDF_FAILURE; } GpioRead(IRQ_PIN, &irq_pin_val); if(irq_pin_val == 0) led_off(); else led_on(); GpioEnableIrq(gpio); }
HDF 框架下的 GPIO 驱动——防抖和浮空
中断抖动常见于使用按键作为 GPIO 中断触发源,由于按键的机械性质,很难从根本上消除抖动,需要屏蔽抖动带来的影响,这种技术称为防抖:
- 硬件:某平台支持 GPIO 去毛刺、可配置中断触发电平值等技术
- 软件:在中断服务程序中多次读取中断管脚的电平值,直到电平稳定
GPIO 管脚外部既不拉高、也不拉低时的状态称为浮空状态,浮空状态下的 GPIO 是不稳定的,程序读取 GPIO 对应值时,可能会出现高低频繁跳变。若浮空管脚作为外部中断,会频繁触发中断,要避免这种情况的发生:
- GPIO 外部电路明确接 GND 或者 VCC
- 使用上拉或者下拉电阻
HDF 框架下的 GPIO 驱动——LED 控制
#define LED_PIN 6// GPIO0_6, 0*8+6 = 6 #define IRQ_PIN 30//GPIO3_6 3*8+6 = 30 //高电平熄灭LED static int32_t led_off(void) { if(GpioWrite(LED_PIN, 1) != HDF_SUCCESS){ HDF_LOGE("GpioWrite: LED_PIN failed\n"); return HDF_FAILURE; } return HDF_SUCCESS; } //低电平点亮 LED static int32_t led_on(void) { if(GpioWrite(LED_PIN, 0) != HDF_SUCCESS){ HDF_LOGE("GpioWrite: LED_PIN failed\n"); return HDF_FAILURE; } return HDF_SUCCESS; }
HDF 框架下的 GPIO 驱动——驱动程序目录和结构
- 驱动源码目录: drivers/adapter/khdf/linux/gpio_test_drv/
- 在上一层目录的 Makefile 添加编译目标: drivers/adapter/khdf/linux/Makefile
obj-$(CONFIG_DRIVERS_HDF) += gpio_test_drv/
- hcs 配置文件中添加设备节点定义:vendor/hisilicon/Hi3516DV300/hdf_config/khdf/device_info/device_info.hcs
HDF 框架下的 GPIO 驱动——应用程序目录结构
- 在 openharmony 源码根目录下创建子目录 examples/gpio_test_app/,其中 examples 作为一个子系统,gpio_test_app 作为该子系统下的一个组件
- 在上述目录下创建应用程序源文件和构建文件
- 在产品定义文件 productdefine/common/products/Hi3516DV300.json 中添加 examples 子系统和 gpio_test_app 组件,使其被编译
- 清空 out 目录,编译全量代码,驱动编译进内核,测试程序 gpio_test_app 位于 bin 目录下
总结
- GPIO:通用和专用 IO 的区别、不同平台下的 GPIO 的分组和编号、GPIO 常用调试手段
- HDF 驱动:GPIO 接口的配置方式、读写操作、中断,两种方式实现应用和驱动的通信,缓冲区的基本操作,基本覆盖了全部的 GPIO 接口
- 提供一套完整的驱动程序和应用程序,并给出其目录结构
参考链接
openharmony 官方网站:https://www.openharmony.cn/mainPlay
openharmony 官方视频链接:https://www.bilibili.com/video/BV1z34y1t76h/