openharmony GPIO 驱动开发
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.hcsgpio_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 = 30static 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//高电平熄灭LEDstatic 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;}//低电平点亮 LEDstatic 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/