【FFH】BearPi-HM Micro 南向研究——GPIO驱动分析 原创 精华
【FFH】BearPi-HM Micro 南向研究——GPIO驱动分析
参考
概述
BearPi-HM Micro开发板板载高性能的工业级处理器STM32MP157芯片,为了使STM32MP1的GPIO驱动适配到HDF框架,厂家会对芯片GPIO的驱动与HDF框架的GPIO进行适配,也就是适配层到核心层的实现,然后核心层又会从核心层抽象出一系列接口到接口层,供开发者使用。通过这样用户就可以忽略芯片的差异,只需关注核心层和接口层,就可以实现驱动程序的开发。
在HDF框架中,GPIO的接口适配模式采用无服务模式,用于不需要在用户态提供API的设备类型,或者没有用户态和内核区分的OS系统,其关联方式是DevHandle直接指向设备对象内核态地址。
芯片管脚号计算规则
分析之前得先了解BearPi-HM Micro的GPIO引脚编号(gpio_num)规则。后面提到的许多函数都会出现gpio_num,看过led驱动例程,查询引脚图可以知道led是PA13,然后在代码中led的引脚编号是led_gpio_num=13,那么如果是PB13引脚编号应该是多少?
就去研究了一下这个引脚编号是如何得来的,最后得出BearPi-HM_Micro的引脚号的计算方法。其实就是按芯片原理图的引脚顺序一个一个排下去。
查看原理图,可以看到引脚号由P和从A开始的字母,定义为组,以及一个编号组成,即组内偏移,对应的引脚编号为
GPIOA 起始-终止:0 - 15
GPIOB 起始-终止:16 - 31
GPIOC 起始-终止: 32 - 47
GPIO号 = GPIO组索引 * 每组GPIO管脚数(16) + 组内偏移
例如:PG2 = 6*16+2=98 (7是G的字母序,从0开始)
当然具体得看芯片,例如
如图引脚号到PI11就结束了,那么下一个PZ0的引脚号就是接着PI11下一个,也就是140.
总结:引脚号其实是引脚从第一个数下来的编号,我们只需要知道它的命名规则,就如上面PX都有16个引脚,就可以计算PB,PC对应的起始编号,得到我们想要的gpio_num。
驱动开发步骤
GPIO模块适配的三个环节是配置属性文件,实例化驱动入口,以及实例化核心层接口函数。GPIO控制器分组管理所有管脚,相关参数会在属性文件中有所体现;驱动入口和接口函数的实例化环节是厂商驱动接入HDF的核心环节。
1. 配置属性文件:
在device_info.hcs文件中添加GPIO的deviceNode描述。deviceNode信息与驱动入口注册相关,器件属性值与核心层GpioCntlr成员的默认值或限制范围有密切关系。 本例只有一个GPIO控制器,如有多个器件信息,则需要在device_info文件增加deviceNode信息,以及在gpio_config文件中增加对应的器件属性。
device_gpio :: device {
device0 :: deviceNode {
policy = 0;// 等于0,不需要发布服务
priority = 10;// 驱动启动优先级
permission = 0644;// 驱动创建设备节点权限
moduleName = "HDF_PLATFORM_GPIO";//【必要】用于指定驱动名称,需要与期望的驱动Entry中的moduleName一致;
serviceName = "HDF_PLATFORM_GPIO";
deviceMatchAttr = "st_stm32mp157_gpio";//【必要】用于配置控制器私有数据,要与 gpio_config.hcs 中
// 对应控制器保持一致,其他控制器信息也在文件中
}
}
【可选】添加gpio_config.hcs器件属性文件。也就是私有配置属性。私有配置属性如何获取和调用将在后面Stm32GpioReadDrs函数分析。相关配置信息可以在芯片手册寻找。
HDF框架在加载驱动的时候,会将对应的配置信息获取并保存在HdfDeviceObject中的property里面。
root {
platform {
gpio_config {
controller_0x50002000 {
match_attr = "st_stm32mp157_gpio";//【必要】必须和device_info.hcs中的deviceMatchAttr值一致
groupNum = 11;//【必要】GPIO组索引,需要根据设备情况填写
bitNum = 16;//【必要】每组GPIO管脚数
gpioRegBase = 0x50002000;//【必要】物理基地址
gpioRegStep = 0x1000;//【必要】寄存器偏移步进
irqRegBase = 0x5000D000;//【必要】开启中断
irqRegStep = 0x400; //【必要】共享中断
}
}
}
}
2. 实例化驱动入口
驱动开发首先需要实例化驱动入口,驱动入口必须为HdfDriverEntry(在 hdf_device_desc.h 中定义)类型的全局变量,且moduleName要和device_info.hcs中保持一致。HDF框架会将所有加载的驱动的HdfDriverEntry对象首地址汇总,形成一个类似数组的段地址空间,方便上层调用。 一般在加载驱动时HDF会先调用Bind函数,再调用Init函数加载该驱动。当Init调用异常时,HDF框架会调用Release释放驱动资源并退出。
步骤:
- 实例化HdfDriverEntry结构体成员。
- 调用HDF_INIT将HdfDriverEntry实例化对象注册到HDF框架中。
/* HdfDriverEntry definition */
struct HdfDriverEntry g_GpioDriverEntry = {
.moduleVersion = 1,
.moduleName = "HDF_PLATFORM_GPIO",
.Bind = GpioDriverBind,
.Init = GpioDriverInit,
.Release = GpioDriverRelease,
};
/* Init HdfDriverEntry */
HDF_INIT(g_GpioDriverEntry);
实例化Bind,Init,Release函数
因为gpio采用无服务模式,所以并不需要在Bind函数将服务接口绑定到HDF框架,Init函数主要用于初始化自定义结构体对象,初始化GpioCntlr成员,调用核心层GpioCntlrAdd函数。Release函数用于释放内存和删除控制器,该函数需要在驱动入口结构体中赋值给Release 接口,当HDF框架调用Init函数初始化驱动失败时,可以调用Release释放驱动资源。所有强制转换获取相应对象的操作前提是在Init函数中具备对应赋值的操作。
下列为HDF_STATUS相关状态的说明。
/* HdfDriverEntry hook function implementations */
static int32_t GpioDriverBind(struct HdfDeviceObject *device)
{
(void)device;
return HDF_SUCCESS;
}
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;
}
//获取私有属性数据
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;
}
/* OsalIoRemap: remap registers */
stm32gpio->exitBase = OsalIoRemap(stm32gpio->irqPhyBase, stm32gpio->iqrRegStep);
if (stm32gpio->exitBase == NULL) {
dprintf("%s: OsalIoRemap fail!", __func__);
return -1;
}
//为控制器分配内存
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;//计算组起始编号
stm32gpio->cntlr.priv = (void *)device->property;//私有属性
stm32gpio->cntlr.device = device;//绑定设备
stm32gpio->cntlr.ops = &g_GpioMethod;//绑定接口函数
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;
}
static void GpioDriverRelease(struct HdfDeviceObject *device)
{
struct GpioCntlr *gpioCntlr = NULL;
struct Stm32GpioCntlr *stm32gpioGpioCntlr = NULL;
HDF_LOGD("%s: Enter", __func__);
if (device == NULL) {
HDF_LOGE("%s: device is null!", __func__);
return;
}
gpioCntlr = GpioCntlrFromDevice(device);//获取gpio控制器设备句柄
if (gpioCntlr == NULL) {
HDF_LOGE("%s: no service bound!", __func__);
return;
}
GpioCntlrRemove(gpioCntlr);//移除gpio控制器
stm32gpioGpioCntlr = (struct Stm32GpioCntlr *)gpioCntlr;
ReleaseGpioCntlrMem(stm32gpioGpioCntlr);//释放内存
OsalIoUnmap((void *)stm32gpioGpioCntlr->regBase);
stm32gpioGpioCntlr->regBase = NULL;
}
3.实例化GPIO控制器对象:
初始化GpioCntlr成员
GpioCntlr结构体定义如下
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;//私有属性指针
};
在Init函数已经对GpioCntlr进行了初始化,如下图,主要步骤就是先创建一个gpio控制器并分配内存,然后对控制器的成员进行赋值,最后调用核心层GpioCntlrAdd添加控制器。
对于GpioCntlr的实例化,主要提几点:
- cntlr.device初始化用于绑定GPIO控制器设备对象,为HdfDeviceObject类型。
- cntlr.priv,也就是我们的私有属性获取,上面在配置私有属性时已经提到私有属性保存在HdfDeviceObject中的property里面,而这一过程在厂家写的Stm32GpioReadDrs中实现。
/* 获取hcs配置信息到 stm32gpio */
static int32_t Stm32GpioReadDrs(struct Stm32GpioCntlr *stm32gpio, const struct DeviceResourceNode *node)
{
int32_t ret;
struct DeviceResourceIface *drsOps = NULL;
drsOps = DeviceResourceGetIfaceInstance(HDF_CONFIG_SOURCE);//获取有关设备资源配置树的信息
if (drsOps == NULL || drsOps->GetUint32 == NULL) {
HDF_LOGE("%s: invalid drs ops fail!", __func__);
return HDF_FAILURE;
}
ret = drsOps->GetUint32(node, "gpioRegBase", &stm32gpio->gpioPhyBase, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read regBase fail!", __func__);
return ret;
}
ret = drsOps->GetUint32(node, "gpioRegStep", &stm32gpio->gpioRegStep, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read gpioRegStep fail!", __func__);
return ret;
}
ret = drsOps->GetUint16(node, "groupNum", &stm32gpio->groupNum, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read groupNum fail!", __func__);
return ret;
}
ret = drsOps->GetUint16(node, "bitNum", &stm32gpio->bitNum, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read bitNum fail!", __func__);
return ret;
}
ret = drsOps->GetUint32(node, "irqRegBase", &stm32gpio->irqPhyBase, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read regBase fail!", __func__);
return ret;
}
ret = drsOps->GetUint32(node, "irqRegStep", &stm32gpio->iqrRegStep, 0);
if (ret != HDF_SUCCESS) {
HDF_LOGE("%s: read gpioRegStep fail!", __func__);
return ret;
}
return HDF_SUCCESS;
}
结构体DeviceResourceIface提供用于获取有关设备资源配置树的信息的函数。
通过DeviceResourceGetIfaceInstance(HDF_CONFIG_SOURCE)获取结构体句柄,然后就可以通过里面的函数来获取私有配置信息。
例如:ret = drsOps->GetUint32(node, “gpioRegBase”, &stm32gpio->gpioPhyBase, 0);就是将私有配置中的"gpioRegBase"以uint32的数据类型赋值给stm32gpio->gpioPhyBase,node是指示指向配置树节点的指针,通过node就可以定位到设备的属性树的root节点,从而读取到数据。然后最后一个参数是指示在操作失败时要填充到值所指向的内存。
实例化GpioCntlr成员GpioMethod。
上面的初始化还有一个操作stm32gpio->cntlr.ops = &g_GpioMethod;这个就是实现实例化GpioCntlr成员GpioMethod。
GpioMethod结构体在drivers/framework/support/platform/include/gpio_core.h中定义,而我们需要实例化这些接口函数。gpio_core.c 就是对硬件gpio控制器的抽象,通过gpio控制器可以配置具体的gpio电平、输入输出模式、中断等。
struct GpioMethod {
/** request exclusive access to an GPIO pin, optional */
int32_t (*request)(struct GpioCntlr *cntlr, uint16_t local);
/** release exclusive access to an GPIO pin, optional */
int32_t (*release)(struct GpioCntlr *cntlr, uint16_t local);
/** write the level value into a GPIO pin */
int32_t (*write)(struct GpioCntlr *cntlr, uint16_t local, uint16_t val);
/** read the level value of a GPIO pin */
int32_t (*read)(struct GpioCntlr *cntlr, uint16_t local, uint16_t *val);
/** set the direction for a GPIO pin */
int32_t (*setDir)(struct GpioCntlr *cntlr, uint16_t local, uint16_t dir);
/** get the direction of a GPIO pin */
int32_t (*getDir)(struct GpioCntlr *cntlr, uint16_t local, uint16_t *dir);
/** get the irq number of a GPIO pin, optional */
int32_t (*toIrq)(struct GpioCntlr *cntlr, uint16_t local, uint16_t *irq);
/** set the ISR function for a GPIO pin */
int32_t (*setIrq)(struct GpioCntlr *cntlr, uint16_t local, uint16_t mode, GpioIrqFunc func, void *arg);
/** unset the ISR function for a GPIO pin */
int32_t (*unsetIrq)(struct GpioCntlr *cntlr, uint16_t local);
/** enable a GPIO pin interrupt */
int32_t (*enableIrq)(struct GpioCntlr *cntlr, uint16_t local);
/** disable a GPIO pin interrupt */
int32_t (*disableIrq)(struct GpioCntlr *cntlr, uint16_t local);
};
回到stm32mp1_gpio.c文件,厂家写的GpioMethod结构体定义g_GpioMethod如下,对gpio_core中的函数进行实例化。
/* GpioMethod definition */
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,
};
厂家通过实例化上述函数,就可以实现core核心层与适配层的对接,通过调用core里面的函数就可以间接调用到厂家实现的对不同芯片的适配驱动函数。以上函数都可以在stm32mp1_gpio.c文件找到。
例如:
int32_t GpioCntlrRead(struct GpioCntlr *cntlr, uint16_t local, uint16_t *val)
{
if (cntlr == NULL) {
return HDF_ERR_INVALID_OBJECT;
}
if (cntlr->ops == NULL || cntlr->ops->read == NULL) {
return HDF_ERR_NOT_SUPPORT;
}
if (val == NULL) {
return HDF_ERR_INVALID_PARAM;
}
return cntlr->ops->read(cntlr, local, val);//调用控制器的方法
}
可以发现core核心层的函数其实就是调用控制器的GpioMethod中的函数,也就是最终会调用到厂家写的适配驱动函数。
通过适配层绑定后我们就可以调用核心层的函数来操控我们的gpio,而gpio_core的函数在同路径的gpio.if中又被封装了一层,简化了参数,不需要传入GpioCntlr这个控制器,直接传入gpio编号就可以进行控制。故如果我们想要实现gpio的一些功能,可以引用gpio_if.h这个头文件,在把路径依赖加上,就可以实现对gpio的操作。
例如:gpio.if.c中的GpioRead函数,对gpio_core.c中的GpioCntlrRead进行封装,只需要传入gpio编号及val的指针地址,就可以实现对gpio的读取操作。
int32_t GpioRead(uint16_t gpio, uint16_t *val)
{
return GpioCntlrRead(GpioGetCntlr(gpio), GpioToLocal(gpio), val);
}
在LED的例程中也可以发现编写的驱动代码对led的gpio管脚操作也是引用了gpio_if.h头文件,并且调用其中的函数来操作gpio,想了解gpio的使用可以再仔细查看代码看看它是怎么操作的,gpio的操作流程如下:
stm32mp1_gpio适配层驱动代码分析
下面是对厂家写的适配层的代码的分析。
先看看stm32mp1_gpio.h文件厂家自定义的结构体类型
#define GROUP_MAX 13 //最大组数
#define BIT_MAX 16 //一组最大位号
//描述一个gpio端口 如:GPIOA,GPIOB
struct GpioGroup {
volatile unsigned char *regBase; //寄存器映射地址(寄存器地址映射)
EXTI_TypeDef *exitBase; //外部中断基地址
unsigned int index; //端口中断处理函数
OsalIRQHandle irqFunc; //中段句柄?
OsalSpinlock lock; //自旋锁
};
//描述一个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端口(一个数组)
};
static struct Stm32GpioCntlr g_Stm32GpioCntlr = {
.groups = NULL,
.groupNum = GROUP_MAX,
.bitNum = BIT_MAX,
};
stm32mp1_gpio.c驱动函数分析
前面是对组号引脚号处理函数,上面已经分析了gpio_num和组号引脚号的关系,下面这些函数是他们之间的转换关系。
static inline struct Stm32GpioCntlr *ToStm32GpioCntlr(struct GpioCntlr *cntlr)
{
return (struct Stm32GpioCntlr *)cntlr;
}
//获取组号
static inline uint16_t Stm32ToGroupNum(uint16_t gpio)
{
return (uint16_t)(gpio / g_Stm32GpioCntlr.bitNum);//根据GPIO编号计算组号,即除于每组最大引脚号
}
//获取位号
static inline uint16_t Stm32ToBitNum(uint16_t gpio)
{
return (uint16_t)(gpio % g_Stm32GpioCntlr.bitNum);//根据GPIO编号计算位号,余数即为引脚号
}
//根据组号和位号计算GPIO编号
static inline uint16_t Stm32ToGpioNum(uint16_t group, uint16_t bit)
{
return (uint16_t)(group * g_Stm32GpioCntlr.bitNum + bit);
}
//根据GPIO编号获取对应的stm32组号,存在group里
static int32_t Stm32GetGroupByGpioNum(struct GpioCntlr *cntlr, uint16_t gpio, struct GpioGroup **group)
{
struct Stm32GpioCntlr *stm32gpio = NULL;
uint16_t groupIndex = Stm32ToGroupNum(gpio);
if (cntlr == NULL || cntlr->priv == NULL) {
HDF_LOGE("%s: cntlr or priv is NULL", __func__);
return HDF_ERR_INVALID_OBJECT;
}
stm32gpio = ToStm32GpioCntlr(cntlr); //GPIOCntlr转成Stm32GpioCntlr
if (groupIndex >= stm32gpio->groupNum) {
HDF_LOGE("%s: err group index:%u", __func__, groupIndex);
return HDF_ERR_INVALID_PARAM;
}
*group = &stm32gpio->groups[groupIndex];//group的映射(可能)
return HDF_SUCCESS;
}
厂家实现的驱动函数功能说明
/*
功能:设置GPIO口方向
参数:*cntlr :GPIO控制器结构体
gpio:gpio口编号
dir:方向(IN/OUT)
*/
static int32_t Stm32Mp157GpioSetDir(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t dir);
/*
功能:获取GPIO口方向
参数:*cntlr :GPIO控制器结构体
gpio:gpio口编号
*dir:存取获取到的gpio输入输出方向
*/
static int32_t Stm32Mp157GpioGetDir(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t *dir);
/*
功能:设置GPIO电平
参数:*cntlr :GPIO控制器结构体
gpio:gpio口编号
val: 电平高低(GPIO_VAL_LOW/GPIO_VAL_HIGH )
*/
static int32_t Stm32Mp157GpioWrite(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t val);
/*
功能:获取GPIO电平
参数:*cntlr :GPIO控制器结构体
gpio:gpio口编号
*val: 存取获取到的电平值
*/
static int32_t Stm32Mp157GpioRead(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t *val);
/*
功能:所有gpio的中断服务函数
参数:irq:gpio中断号
data:GpioGroup 产生中断的gpio所在的分组(端口)
*/
static uint32_t IrqHandleNoShare(uint32_t irq, void *data);
/*
功能:根据引脚号获取中断号
参数:pinNum:E53引脚编号
*/
static uint32_t GetGpioIrqNum(uint16_t pinNum);
/*
功能:注册gpio管脚的中断函数
参数:pinNum:E53引脚编号
group:GPIO组
*/
static int32_t GpioRegisterGroupIrqUnsafe(uint16_t pinNum, struct GpioGroup *group);
/*
功能:初始化gpio中断寄存器、中断服务函数
参数:*cntlr :GPIO控制器结构体
gpio:gpio引脚编号
func:回调函数参数:*cntlr :GPIO控制器结构体
gpio:gpio引脚编号
arg:可选参数
*/
static int32_t Stm32Mp157GpioSetIrq(struct GpioCntlr *cntlr, uint16_t gpio, uint16_t mode, GpioIrqFunc func, void *arg);
/*
功能:取消设置gpio中断寄存器、中断服务函数
参数:*cntlr :GPIO控制器结构体
gpio:gpio引脚编号
*/
static int32_t Stm32Mp157GpioUnsetIrq(struct GpioCntlr *cntlr, uint16_t gpio);
/*
功能:使能GPIO管脚中断
参数:*cntlr :GPIO控制器结构体
gpio:gpio引脚编号
*/
static int32_t Stm32Mp157GpioEnableIrq(struct GpioCntlr *cntlr, uint16_t gpio);
/*
功能:禁止GPIO管脚中断
参数:*cntlr :GPIO控制器结构体
gpio:gpio引脚编号
*/
static int32_t Stm32Mp157GpioDisableIrq(struct GpioCntlr *cntlr, uint16_t gpio);
总结
GPIO的操作函数都是由gpio_if提供,而gpio_if只是对gpio_core的一层封装,屏蔽了gpio控制器。gpio_core 中的函数就是调用控制器的各种方法,而厂家需要实现适配层的驱动编写stm32mp1_gpio,以此来实例化核心层驱动。
通过分析GPIO驱动对HDF框架有了更深入的理解,也了解了不同芯片是如何适配到HDF框架的,对驱动的开发流程也有了很大帮助。后续会尝试E53串口驱动的研究,厂家给的驱动函数并没有把E53的串口驱动加上,研究了挺久也没驱动起来,希望有成功使用BearPi-HM MicroE53串口的能够交流交流。
以上有些内容属于个人理解,可能有不对的地方,欢迎交流探讨。
期待大佬更多文章
超级详细透彻,自己背地里研究透彻后才发现有这样一篇宝藏文章。
不错不错,很详细