OpenHarmony HDF 按键中断开发基于小熊派hm micro

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:深蓝海洋 设计师:CSDN官方博客 返回首页
评论

打赏作者

killer-p

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值