OpenHarmony HDF 按键中断开发基于小熊派hm micro
文章目录
-
- 一、驱动代码
-
- 1.1、button驱动
- 1.2 gpio驱动
-
- 1.2.1、gpio核心层
- 1.2.2、gpio驱动
- 二、中断处理过程
- 三、小结
本章使用gpio中断来实现按键驱动,重点在于理解HDF gpio框架
一、驱动代码
参考上一章led驱动程序的编写来实现本章的驱动。
可以按上一章led驱动程序的编写步骤重复做一遍。button驱动与led驱动的区别在于GPIO管脚以及初始化代码、中断相关代码等:
1.1、button驱动
在按键驱动程序button.c中添加gpio的头文件:
#include "gpio_if.h"
在初始化函数中,通过读取button_config.hcs来获取按键的gpio号。然后调用gpio_if.h提供的接口,设置中断回调函数 MyCallBackFunc,最后使能中断。
// 驱动自身业务初始的接口int32_t HdfButtonDriverInit(struct HdfDeviceObject *device){ int32_t ret; struct Stm32Mp1Button *btn = &g_Stm32Mp1IButton; //获取gpio引脚号 Stm32ButtonReadDrs(btn,device->property); //这里可以不需要设置为输入,因为在GpioSetIrq中会将IO设为输入 //GpioSetDir(btn->gpioNum, GPIO_DIR_IN); /* 设置GPIO管脚中断回调函数为MyCallBackFunc,入参为gpio对象,中断触发模式为上升沿触发 */ ret = GpioSetIrq(g_Stm32Mp1IButton.gpioNum, OSAL_IRQF_TRIGGER_RISING, MyCallBackFunc, (void *)device); if (ret != 0) { HDF_LOGE("GpioSetIrq: failed, ret %d\n", ret); return HDF_FAILURE; } /* 使能IO管脚中断 */ ret = GpioEnableIrq(btn->gpioNum); if (ret != 0) { HDF_LOGE("GpioEnableIrq: failed, ret %d\n", ret); return HDF_FAILURE; }return HDF_SUCCESS;}
中断回调函数:
在中断回调函数中统计按键按下次数,然后将次数发送到应用层。这里通过HDF的消息管理机制给应用层发送消息,这一部分后面再聊。
当 HdfDeviceSendEvent()成功执行后,应用程序的监听函数就会被执行,在回调函数中翻转led。
/* 中断服务函数*/int32_t MyCallBackFunc(uint16_t gpio, void *data){ global_data++; //全局变量,记录按键按下次数 //获取设备驱动 struct HdfDeviceObject *deviceObject = (struct HdfDeviceObject *)data; //创建buf来给应用层传递数据 struct HdfSBuf *send_buf = HdfSBufObtainDefaultSize(); //将global_data写入buf HdfSbufWriteUint16(send_buf,global_data); //将buf里的数据传递给订阅了button驱动服务的应用 HdfDeviceSendEvent(deviceObject,520,send_buf); //回收buf HdfSBufRecycle(send_buf); return 0;}
1.2 gpio驱动
1.2.1、gpio核心层
以上GPIO的操作函数都是由gpio_if.h提供,而gpio_if.h只是对gpio_core.c的一层封装,屏蔽了gpio控制器
例如下面的函数实际上是调用 gpio_core.c 中的 GpioCntlrSetIrq()。
int32_t GpioSetIrq(uint16_t gpio, uint16_t mode, GpioIrqFunc func, void *arg){ return GpioCntlrSetIrq(GpioGetCntlr(gpio), GpioToLocal(gpio), mode, func, arg);}
而 gpio_core.c 就是对硬件gpio控制器的抽象,通过gpio控制器可以配置具体的gpio电平、输入输出模式、中断等。
struct GpioCntlr { struct IDeviceIoService service; //gpio无服务 struct HdfDeviceObject *device; //gpio设备(hcs) struct GpioMethod *ops; //gpio操作方式,由stm32mp1_gpio.c实现 struct DListHead list; OsalSpinlock spin; uint16_t start; uint16_t count; struct GpioInfo *ginfos; //数组(见下面) void *priv; //回调函数的参数};
gpio_core.c 中的函数就是调用控制器的各种方法:
int32_t GpioCntlrWrite(struct GpioCntlr *cntlr, uint16_t local, uint16_t val){...... return cntlr->ops->write(cntlr, local, val);}int32_t GpioCntlrRead(struct GpioCntlr *cntlr, uint16_t local, uint16_t *val){...... return cntlr->ops->read(cntlr, local, val);}int32_t GpioCntlrSetDir(struct GpioCntlr *cntlr, uint16_t local, uint16_t dir){...... return cntlr->ops->setDir(cntlr, local, dir);}int32_t GpioCntlrGetDir(struct GpioCntlr *cntlr, uint16_t local, uint16_t *dir){...... return cntlr->ops->getDir(cntlr, local, dir);}
需要注意的地方是:每一个中断控制器都有一个ginfos的数组,其定义如下:
//描述所有GPIO中断回调函数、参数,通常是作为一个数组(不知道为什么要这么取名字)struct GpioInfo { GpioIrqFunc irqFunc;//具体gpio管脚的回调函数 void *irqData;//函数参数};
我们调用GpioSetIrq()设置的MyCallBackFunc()函数就保存在这个数组的特定位置。当gpio管脚的中断产生时,中断函数就会根据gpio控制器找到ginfo数组,再从数组中取出MyCallBackFunc()来执行。所以ginfo数组里的函数就叫中断回调函数。
以下函数就是gpio_core.c提供的回调接口,中断函数需要调用该函数以实现回调 MyCallBackFunc(),例如在2.2.2中的 IrqHandleNoShare()
//由gpio的中断函数调用,用于调用用户设置的gpio回调函数void GpioCntlrIrqCallback(struct GpioCntlr *cntlr, uint16_t local){ struct GpioInfo *ginfo = NULL; //检查gpio控制器 以及 ginfos if (cntlr != NULL && local < cntlr->count && cntlr->ginfos != NULL) { ginfo = &cntlr->ginfos[local]; if (ginfo != NULL && ginfo->irqFunc != NULL) { //执行中断回调函数 (void)ginfo->irqFunc(local, ginfo->irqData); } else { HDF_LOGW("GpioCntlrIrqCallback: ginfo or irqFunc is NULL!"); } } else { HDF_LOGW("GpioCntlrIrqCallback: invalid cntlr(ginfos) or loal num:%u!", local); }}
GpioCntlrSetIrq()源码注释如下:其实现的就是两个功能:
- 设置回调函数。
- 调用gpio驱动、初始化中断相关的硬件配置:cntlr->ops->setIrq(cntlr, local, mode, theFunc, theData);
//设置gpio中断回调函数到控制器int32_t GpioCntlrSetIrq(struct GpioCntlr *cntlr, uint16_t local, uint16_t mode, GpioIrqFunc func, void *arg){ int32_t ret; uint32_t flags; GpioIrqFunc theFunc = func; void *theData = arg; struct GpioIrqBridge *bridge = NULL; void *oldFunc = NULL; void *oldData = NULL; //对参数做些检查 if (cntlr == NULL || cntlr->ginfos == NULL) { return HDF_ERR_INVALID_OBJECT; } if (local >= cntlr->count) { return HDF_ERR_INVALID_PARAM; } if (cntlr->ops == NULL || cntlr->ops->setIrq == NULL) { return HDF_ERR_NOT_SUPPORT; } //使用中断线程来处理中断,回调函数会在中断线程中执行 if ((mode & GPIO_IRQ_USING_THREAD) != 0) { bridge = GpioIrqBridgeCreate(cntlr, local, func, arg); //设置中断服务函数为 GpioIrqBridgeFunc 该函数用于在中断中释放信号量 if (bridge != NULL) { theData = bridge; theFunc = GpioIrqBridgeFunc; } if (bridge == NULL) { return HDF_FAILURE; } } (void)OsalSpinLockIrqSave(&cntlr->spin, &flags); //保存旧的中断函数 oldFunc = cntlr->ginfos[local].irqFunc; oldData = cntlr->ginfos[local].irqData; //将新的中断函数设置到控制器的中断数组 cntlr->ginfos[local].irqFunc = theFunc; cntlr->ginfos[local].irqData = theData; //初始化中断:设置寄存器、回调函数 ret = cntlr->ops->setIrq(cntlr, local, mode, theFunc, theData); if (ret == HDF_SUCCESS) { if (oldFunc == GpioIrqBridgeFunc) { //之前的中断使用的是中断线程、所以要删除该线程 GpioIrqBridgeDestroy((struct GpioIrqBridge *)oldData); } } else { cntlr->ginfos[local].irqFunc = oldFunc; cntlr->ginfos[local].irqData = oldData; if (bridge != NULL) { GpioIrqBridgeDestroy(bridge); bridge = NULL; } } (void)OsalSpinUnlockIrqRestore(&cntlr->spin, &flags); return ret;}
1.2.2、gpio驱动
gpio_core.c中只是提供了gpio控制器的一个”模型“,具体的gpio控制器需要由驱动开发者根据芯片平台实现,在此例如stm32mp157,由小熊派官方已经实现。在/device/st/drivers/gpio/目录下:
stm32mp1_gpio.c的实现参考了[openharmony gpio驱动开发指南](zh-cn/device-dev/driver/driver-platform-gpio-develop.md · OpenHarmony/docs - Gitee.com)。读者最好先阅读该指南。
首先认识stm32的gpio控制器结构体:
该结构体是驱动开发者自定义的,必须包含struct GpioCntlr 成员。
//描述一个GPIO控制器,控制所有的GPIO端口struct Stm32GpioCntlr { struct GpioCntlr cntlr; //gpio核心层控制器 volatile unsigned char *regBase; //寄存器映射后得到的地址 EXTI_TypeDef *exitBase; //同上 uint32_t gpioPhyBase; //gpio寄存器物理基地址 uint32_t gpioRegStep; //gpio寄存器偏移步进 uint32_t irqPhyBase; //外部中断寄存器物理基地址 uint32_t iqrRegStep; //外部中断寄存器偏移步进 uint16_t groupNum; //gpio组数量 uint16_t bitNum; //每组gpio的管脚数量 struct GpioGroup *groups; //gpio端口(一个数组)};//描述一个gpio端口 如:GPIOA,GPIOBstruct GpioGroup { volatile unsigned char *regBase; //寄存器映射地址(寄存器地址映射) EXTI_TypeDef *exitBase; //外部中断基地址 unsigned int index; //端口下标 OsalIRQHandle irqFunc; //端口中断处理函数 OsalSpinlock lock;};
由上面的结构体可知,stm32mp1_gpio.c中除了要实现 核心层的struct GpioCntlr cntlr 之外,还需要获得寄存器地址等硬件配置。
这一点在gpio驱动初始化函数中能得到体现:
//gpio驱动初始化//驱动配置文件gpio_config.hcs中定义了寄存器地址等硬件信息,当驱动成功加载后,这些信息通过device->property传递到这static int32_t GpioDriverInit(struct HdfDeviceObject *device){ int32_t ret; struct Stm32GpioCntlr *stm32gpio = &g_Stm32GpioCntlr; dprintf("%s: Enter", __func__); if (device == NULL || device->property == NULL) { HDF_LOGE("%s: device or property NULL!", __func__); return HDF_ERR_INVALID_OBJECT; } //获取hcs配置信息到 stm32gpio ret = Stm32GpioReadDrs(stm32gpio, device->property); if (ret != HDF_SUCCESS) { HDF_LOGE("%s: get gpio device resource fail:%d", __func__, ret); return ret; } //检查配置信息 if (stm32gpio->groupNum > GROUP_MAX || stm32gpio->groupNum <= 0 || stm32gpio->bitNum > BIT_MAX || stm32gpio->bitNum <= 0) { HDF_LOGE("%s: invalid groupNum:%u or bitNum:%u", __func__, stm32gpio->groupNum, stm32gpio->bitNum); return HDF_ERR_INVALID_PARAM; } //将寄存器地址映射、保存,以后操作寄存器就只能通过映射后的地址操作 stm32gpio->regBase = OsalIoRemap(stm32gpio->gpioPhyBase, stm32gpio->groupNum * stm32gpio->gpioRegStep); if (stm32gpio->regBase == NULL) { HDF_LOGE("%s: err remap phy:0x%x", __func__, stm32gpio->gpioPhyBase); return HDF_ERR_IO; } //将exti外部中断寄存器的地址进行映射 stm32gpio->exitBase = OsalIoRemap(stm32gpio->irqPhyBase, stm32gpio->iqrRegStep); if (stm32gpio->exitBase == NULL) { dprintf("%s: OsalIoRemap fail!", __func__); return -1; } //初始化stm32gpio->groups数组 ret = InitGpioCntlrMem(stm32gpio); if (ret != HDF_SUCCESS) { HDF_LOGE("%s: err init cntlr mem:%d", __func__, ret); OsalIoUnmap((void *)stm32gpio->regBase); stm32gpio->regBase = NULL; return ret; } stm32gpio->cntlr.count = stm32gpio->groupNum * stm32gpio->bitNum; //IO数量 stm32gpio->cntlr.priv = (void *)device->property;//私有配置 stm32gpio->cntlr.device = device;//设备对象 stm32gpio->cntlr.ops = &g_GpioMethod; //操作方法(读写配置gpio,重要) ret = GpioCntlrAdd(&stm32gpio->cntlr); //将GPIO控制器添加到核心层 if (ret != HDF_SUCCESS) { HDF_LOGE("%s: err add controller: %d", __func__, ret); return ret; } HDF_LOGE("%s: dev service:%s init success!", __func__, HdfDeviceGetServiceName(device)); return ret;}
在stm32mp1_gpio.c中还有一个重要的结构体:它是所有gpio操作的具体实现,我们以Stm32Mp157GpioWrite和Stm32Mp157GpioSetIrq为例子,驱动是如何操作硬件的。
//gpio操作方法struct GpioMethod g_GpioMethod = { .request = NULL, .release = NULL, .write = Stm32Mp157GpioWrite, .read = Stm32Mp157GpioRead, .setDir = Stm32Mp157GpioSetDir, .getDir = Stm32Mp157GpioGetDir, .toIrq = NULL, .setIrq = Stm32Mp157GpioSetIrq, .unsetIrq = Stm32Mp157GpioUnsetIrq, .enableIrq = Stm32Mp157GpioEnableIrq, .disableIrq = Stm32Mp157GpioDisableIrq,};
Stm32Mp157GpioWrite:
//设置io引脚电平static int32_t Stm32Mp157GpioWrite(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t val){ int32_t ret; uint32_t irqSave; unsigned int valCur; unsigned int bitNum = Stm32ToBitNum(gpio); //bitNum=gpio%16 组内IO号,范围[0-15] volatile unsigned char *addr = NULL; struct GpioGroup *group = NULL; //获取端口对应的分组 ret = Stm32GetGroupByGpioNum(cntlr, gpio, &group); if (ret != HDF_SUCCESS) { return ret; } //保存锁 if (OsalSpinLockIrqSave(&group->lock, &irqSave) != HDF_SUCCESS) { return HDF_ERR_DEVICE_BUSY; } //通过分组的基地址计算得到某个端口的输出寄存器地址 addr = STM32MP15X_GPIO_DATA(group->regBase); //读取addr上的值,即输出寄存器的值 valCur = OSAL_READL(addr); //要设置低电平,需要设置data寄存器的高16+bitNum位为1(该部分原理参考stm32mp1参考手册) if (val == GPIO_VAL_LOW) { valCur &= ~(0x1 << bitNum); valCur |= (0x1 << (bitNum+16)); } else { //设置为高电平、设置bitNum为1 valCur |= (0x1 << bitNum); } //将新值写入输出寄存器,设置管脚电平 OSAL_WRITEL(valCur, addr); //恢复锁 (void)OsalSpinUnlockIrqRestore(&group->lock, &irqSave); return HDF_SUCCESS;}
Stm32Mp157GpioSetIrq:
//初始化gpio中断寄存器、中断服务函数static int32_t Stm32Mp157GpioSetIrq(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t mode, GpioIrqFunc func, void *arg){ int32_t ret = HDF_SUCCESS; uint32_t irqSave; struct GpioGroup *group = NULL; unsigned int bitNum = Stm32ToBitNum(gpio); (void)func; (void)arg; ret = Stm32GetGroupByGpioNum(cntlr, gpio, &group); if (ret != HDF_SUCCESS) { return ret; } if (OsalSpinLockIrqSave(&group->lock, &irqSave) != HDF_SUCCESS) { return HDF_ERR_DEVICE_BUSY; } //stm32hal库函数:在../stm32mp1xx_hal/STM32MP1xx_HAL_Driver中 EXTI_ConfigTypeDef EXTI_ConfigStructure; //外部中断配置 EXTI_HandleTypeDef hexti; //外部中断服务函数 //设置输入模式 Stm32Mp157GpioSetDir(cntlr,gpio,GPIO_DIR_IN); //配置中断线为GPIO 参考stm32mp1xx_hal_exti.h EXTI_ConfigStructure.Line = EXTI_GPIO | EXTI_EVENT | EXTI_REG1 |bitNum; //下降沿触发 EXTI_ConfigStructure.Trigger = EXTI_TRIGGER_FALLING; EXTI_ConfigStructure.GPIOSel = Stm32ToGroupNum(gpio); EXTI_ConfigStructure.Mode = EXTI_MODE_C1_INTERRUPT; //内核1中断模式(非事件模式) //设置外部中断的寄存器 HAL_EXTI_SetConfigLine(&hexti, &EXTI_ConfigStructure); GpioClearIrqUnsafe(group, bitNum); // clear irq on set if (group->irqFunc != NULL) { (void)OsalSpinUnlockIrqRestore(&group->lock, &irqSave); HDF_LOGI("%s: group irq(%p) already registered!", __func__, group->irqFunc); return HDF_SUCCESS; } //设置gpio中断服务函数(见下文) ret = GpioRegisterGroupIrqUnsafe(bitNum, group); (void)OsalSpinUnlockIrqRestore(&group->lock, &irqSave); HDF_LOGI("%s: group irq(%p) registered!", __func__, group->irqFunc); return ret;}
stm32mp1将全部gpio的中断服务函数都统一设置为 IrqHandleNoShare(),再在IrqHandleNoShare()中去执行GpioCntlrIrqCallback,这个函数在2.2.1中已经分析过,GpioCntlrIrqCallback会去区分具体是哪个GPIO管脚的中断回调函数需要执行。
所以stm32mp1是偷了一个懒,把事儿都丢给了gpio核心层去做。:p
//注册gpio管脚的中断函数static int32_t GpioRegisterGroupIrqUnsafe(uint16_t pinNum, struct GpioGroup *group){ int ret; //向liteos_a内核注册中断服务函数IrqHandleNoShare,参数group。由liteos_a管理中断 ret = OsalRegisterIrq(GetGpioIrqNum(pinNum), 0, IrqHandleNoShare, "GPIO", group); if (ret != 0) { (void)OsalUnregisterIrq(GetGpioIrqNum(pinNum), group); ret = OsalRegisterIrq(GetGpioIrqNum(pinNum), 0, IrqHandleNoShare, "GPIO", group); } if (ret != 0) { HDF_LOGE("%s: irq reg fail:%d!", __func__, ret); return HDF_FAILURE; } //osal层使能中断 ret = OsalEnableIrq(GetGpioIrqNum(pinNum)); if (ret != 0) { HDF_LOGE("%s: irq enable fail:%d!", __func__, ret); (void)OsalUnregisterIrq(GetGpioIrqNum(pinNum), group); return HDF_FAILURE; } group->irqFunc = IrqHandleNoShare; return HDF_SUCCESS;}//所有gpio的中断服务函数//irq:gpio中断号//data:GpioGroup 产生中断的gpio所在的分组(端口)static uint32_t IrqHandleNoShare(uint32_t irq, void *data){ unsigned int i; struct GpioGroup *group = (struct GpioGroup *)data; if (data == NULL) { HDF_LOGW("%s: data is NULL!", __func__); return HDF_ERR_INVALID_PARAM; } //遍历检查gpio分组下,16个IO管脚的中断标志 for (i = 0; i < g_Stm32GpioCntlr.bitNum; i++) { if(__HAL_GPIO_EXTI_GET_IT(1<<i,group->exitBase) != 0) { //中断触发,清除标志, __HAL_GPIO_EXTI_CLEAR_IT(1<<i,group->exitBase); //调用中断回调函数 GpioCntlrIrqCallback(&g_Stm32GpioCntlr.cntlr, Stm32ToGpioNum(group->index, i)); } } return HDF_SUCCESS;}
二、中断处理过程
本节介绍中断触发源到中断回调函数MyCallBackFunc()执行的过程,如图所示,中断信息的传递经过以下6个模块:
gpio外设的中断会经过NVIC中断控制器,触发ARM的普通中断,CPU就会去中断向量表的OSIrqHandler执行:
OSIrqHandler的代码在kernel/liteos_a/arch/arm/arm/src/los_dispatch.S,这一部分的代码可以参考以下博客:
鸿蒙研究站 | 每天死磕一点点 | 2022.01.28 更新 (weharmony.github.io)
OsIrqHandler:...... BLX HalIrqHandler ......
HalIrqHandler 的工作就是读取NVIC寄存器,得到中断号,然后调用OsInterrupt()去执行对应的中断服务程序。
VOID HalIrqHandler(VOID){ UINT32 iar = GiccGetIar(); UINT32 vector = iar & 0x3FFU; /* * invalid irq number, mainly the spurious interrupts 0x3ff, * valid irq ranges from 0~1019, we use OS_HWI_MAX_NUM to do * the checking. */ if (vector >= OS_HWI_MAX_NUM) { return; } g_curIrqNum = vector; OsInterrupt(vector); GiccSetEoir(vector);}
OsInterrupt():
//触发内核层的中断VOID OsInterrupt(UINT32 intNum){...... //在全局数组g_hwiForm[]中取出中断服务函数 hwiForm = (&g_hwiForm[intNum]); if (hwiForm->uwParam) { HWI_PROC_FUNC2 func = (HWI_PROC_FUNC2)hwiForm->pfnHook; if (func != NULL) { //调用中断服务函数 UINTPTR *param = (UINTPTR *)(hwiForm->uwParam); func((INT32)(*param), (VOID *)(*(param + 1))); } } else { HWI_PROC_FUNC0 func = (HWI_PROC_FUNC0)hwiForm->pfnHook; if (func != NULL) { func(); } }.......}
IrqHandleNoShare()在上一节已经分析过,最终它会调用MyCalllBackFunc()来执行我们的中断回调函数。
三、小结
ARM普通中断会使cpu执行中断向量表中的特定的函数,在这个函数中就需要判断是具体哪个外设的中断,这是通过读取NVIC的寄存器得知的。
由于内核负责管理中断,所以由内核实现对外设中断的处理。在liteos_a中,使用一个全局数组来保存所有的外设中断服务函数。
以上是外设中断处理的common情况,而具体到gpio中断,则需要gpio core层的介入。gpio core层也通过一个全局数组来保存所有gpio引脚的回调函数。
为什么需要gpio core这一层呢?我想大概是为了更加方便的管理gpio,以中断为例子,不同的gpio有不同的中断处理程序,但他们 都有一些共性,就是需要清除中断标志位,gpio core将这些共同的处理放在统一的NoShare函数,自己创建一个中断向量表来管理所有的中断。
总之gpio core是一个承上启下的作用,驱动开发者按照gpio core的规定编写完成驱动程序(如gpio控制器),上层应用就能通过 gpio_if正确地使用驱动程序,所以作为驱动开发者,就需要熟悉gpio core,以及其他类型外设的核心层,从而开发正确的驱动程序。