> 文档中心 > OpenHarmony HDF 按键中断开发基于小熊派hm micro

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,以及其他类型外设的核心层,从而开发正确的驱动程序。