衡山派Luban-Lite GPIO驱动架构详解:HAL与Driver层设计及RT-Thread Pin设备驱动集成
衡山派Luban-Lite GPIO驱动架构详解:HAL与Driver层设计及RT-Thread Pin设备驱动集成
最近在衡山派开发板上做项目,发现它的GPIO驱动设计得挺有意思,采用了清晰的分层架构。很多刚开始接触Luban-Lite操作系统的朋友可能会好奇,这套驱动到底是怎么工作的,又是怎么和RT-Thread的Pin设备驱动框架结合起来的。今天,我就结合源码,带大家一层层剥开它的设计,让你不仅能用,还能懂。
这篇文章适合正在或打算在衡山派平台上进行嵌入式开发的工程师,特别是那些想深入了解驱动底层,或者需要移植、调试GPIO功能的朋友。咱们会从源码结构讲起,再到模块架构、初始化流程,最后深入到中断处理和具体接口的使用,手把手带你搞懂这套驱动。
1. 源码结构:从哪开始看
拿到一套新的驱动代码,第一件事就是找到文件在哪。衡山派Luban-Lite的GPIO驱动代码组织得非常清晰,主要分为HAL(硬件抽象层)和Driver(驱动层)两部分。
HAL层的源码位于:
- 头文件:
luban-lite\bsp\artinchip\include\hal\ - 源文件:
luban-lite\bsp\artinchip\hal\gpio\
Driver层的源码位于:
- 头文件:
luban-lite\bsp\artinchip\include\drv\ - 源文件:
luban-lite\bsp\artinchip\drv\gpio\
具体到每个文件,它们的分工如下:
| 文件 | 说明 |
|---|---|
aic_hal_gpio.h | GPIO HAL层的API声明头文件,定义了上层可调用的函数接口。 |
aic_hal_gpio.c | GPIO HAL层API的具体实现,直接操作寄存器。 |
aic_drv_gpio.h | GPIO Driver层的API声明头文件。 |
aic_drv_gpio.c | GPIO驱动注册以及Driver层API的实现,负责对接RT-Thread框架。 |
aic_hal_gpio_def_v10.c | 针对GPIO硬件版本V1.0的特定配置。 |
aic_hal_gpio_def_v11.c | 针对GPIO硬件版本V1.1的特定配置。 |
aic_hal_gpio_def_v12.c | 针对GPIO硬件版本V1.2的特定配置。 |
提示:
aic_hal_gpio_def_v1x.c这类文件的存在,说明驱动考虑到了芯片不同版本间GPIO硬件的差异,通过条件编译来适配,这是驱动可移植性的一个体现。
2. 模块架构:分层设计的妙处
衡山派Luban-Lite的GPIO驱动采用了典型的分层设计,好处是隔离了硬件细节和操作系统框架,让代码更清晰,也更容易维护和移植。
整个架构可以这样理解:
1. GPIO HAL层 (硬件抽象层)这一层是离硬件最近的“翻译官”。它把操作具体GPIO寄存器(比如设置引脚功能、方向、上下拉、中断模式等)的复杂动作,封装成一个个简单的函数接口,比如hal_gpio_set_func、hal_gpio_direction_output。开发者调用这些函数,就不用去关心底层寄存器地址和位域了。这一层也实现了PinMux(引脚复用)的功能。
2. GPIO Driver层 (驱动层)这一层是“框架适配器”。它基于HAL层提供的功能,去适配不同的软件运行环境。
- 对于RT-Thread:Driver层将通用的GPIO功能(输入、输出、中断)注册到RT-Thread的Pin设备驱动框架中。这样,应用程序就可以使用RT-Thread标准、统一的PIN设备API(如
rt_pin_write)来操作GPIO,非常方便。而PinMux功能则直接以API形式提供(通常通过HAL层函数)。 - 对于Baremetal (裸机):如果你不用RT-Thread,只想在裸机环境下跑,那么可以绕过Driver层,直接调用HAL层的API来操作GPIO。这给了开发者很大的灵活性。
简单说,HAL层搞定硬件,Driver层搞定操作系统框架。你想用RT-Thread的标准方式,就走Driver层;你想自己玩裸机,就直接调HAL。
2.1 理解RT-Thread Pin设备驱动
在RT-Thread环境下,Driver层做的最核心的一件事,就是调用rt_device_pin_register()函数,把GPIO功能注册成一个Pin设备。
这个Pin设备驱动提供了我们最常用的几类功能:
- 模式配置:设置引脚为输入、输出,以及配置输入时的上拉/下拉电阻。
- 读写引脚状态:读取输入引脚的电平,或者设置输出引脚的电平。
- GPIO中断配置:给引脚注册中断处理函数、设置触发模式、使能/关闭中断。
这里有个关键点:一个RT-Thread系统里只允许注册一个Pin设备。所以,你在使用时有两种方式:
- 先通过
rt_device_find(“pin”)找到这个设备,再用设备操作接口去调用功能。 - 更常用的方式:直接使用RT-Thread提供的一组便捷函数,如
rt_pin_mode(),rt_pin_write(),rt_pin_read()等。这些函数内部会自动去操作那个唯一的Pin设备。
3. 关键流程:驱动如何启动与响应中断
3.1 初始化流程:驱动是怎么挂载上的
很多朋友可能没细究过,drv_pin_init()这个函数是在什么时候被调用的。其实它是随着RT-Thread系统启动自动完成的。整个调用链非常清晰:
Reset_Handler → entry() → rtthread_startup() → rt_hw_board_init() → rt_components_board_init() → drv_pin_init()在drv_pin_init()函数里,完成了Pin设备的注册。我们来看看源码:
const static struct rt_pin_ops _drv_pin_ops = { drv_pin_mode, drv_pin_write, drv_pin_read, #ifdef AIC_GPIO_IRQ_DRV_EN drv_pin_attach_irq, drv_pin_detach_irq, drv_pin_irq_enable, #else RT_NULL, RT_NULL, RT_NULL, #endif drv_pin_get, }; int drv_pin_init(void) { int ret = RT_EOK; // 关键的一步:注册Pin设备,将操作集_drv_pin_ops与设备名“pin”绑定 ret = rt_device_pin_register("pin", &_drv_pin_ops, RT_NULL); return ret; } // 通过宏声明,让系统自动初始化这个函数 INIT_BOARD_EXPORT(drv_pin_init);这段代码干了啥?
- 定义了一个
rt_pin_ops结构体_drv_pin_ops,里面填满了Driver层实现的各个功能函数指针。这就是驱动能力的“清单”。 - 在
drv_pin_init()中,调用rt_device_pin_register,把这份“清单”和名为“pin”的设备注册到RT-Thread内核。 INIT_BOARD_EXPORT宏将这个初始化函数放入特定的段,系统启动时会自动执行它。
注意:中断相关的三个函数 (
drv_pin_attach_irq等) 被条件编译AIC_GPIO_IRQ_DRV_EN包裹着。这意味着你可以在配置文件中决定是否编译中断支持,对于不需要中断的应用可以节省代码空间。
3.2 GPIO中断(IRQ)流程:二级中断封装
中断处理是驱动里比较精妙的部分。衡山派的GPIO中断采用了一种“二级中断”的封装模型,主要是为了高效管理多个GPIO引脚。
它是这样工作的:
- 一级中断 (物理中断):硬件上,每一组(GROUP)GPIO(最多包含32个引脚)共享一个物理中断号。比如,PA0~PA31这32个引脚都对应同一个CPU中断线。
- 二级中断 (虚拟中断):当PA组的中断发生时,进入GPIO Group ISR (中断服务程序)。这个ISR会检查是组内哪个具体的引脚触发了中断,然后去调用事先为该引脚注册好的用户中断处理函数。
你可以把这个过程想象成:
- 一级中断是小区总门铃(GPIO Group),响了说明有快递。
- 二级中断是你家的门铃(具体GPIO Pin)。物业(Group ISR)听到总门铃响后,查看快递单,再按响对应你家的门铃。
这种设计的好处是减少了CPU需要处理的中断源数量,简化了中断控制器配置,同时又能精确地响应每个引脚的中断。
4. 接口详解:怎么用这些驱动
4.1 Driver层接口 (对接RT-Thread)
Driver层需要实现RT-Thread Pin设备框架定义的标准接口集rt_pin_ops:
struct rt_pin_ops { void (*pin_mode)(struct rt_device *device, rt_base_t pin, rt_base_t mode); void (*pin_write)(struct rt_device *device, rt_base_t pin, rt_base_t value); int (*pin_read)(struct rt_device *device, rt_base_t pin); rt_err_t (*pin_attach_irq)(struct rt_device *device, rt_int32_t pin, rt_uint32_t mode, void (*hdr)(void *args), void *args); rt_err_t (*pin_detach_irq)(struct rt_device *device, rt_int32_t pin); rt_err_t (*pin_irq_enable)(struct rt_device *device, rt_base_t pin, rt_uint32_t enabled); rt_base_t (*pin_get)(const char *name); };衡山派驱动中对应的实现函数是drv_pin_xxx。作为应用开发者,你通常不直接调用这些drv_pin_xxx,而是调用RT-Thread提供的rt_pin_xxx封装函数。但了解它们的功能有助于调试:
| 函数原型 | 功能说明 | 关键参数与返回值 |
|---|---|---|
void drv_pin_mode(..., rt_base_t pin, rt_base_t mode) | 设置引脚模式(输入/输出/上下拉)。 | mode: 使用PIN_MODE_INPUT,PIN_MODE_OUTPUT,PIN_MODE_INPUT_PULLUP等宏。 |
void drv_pin_write(..., rt_base_t pin, rt_base_t value) | 设置输出引脚的电平状态。 | value: 0 (低电平) 或 1 (高电平)。 |
int drv_pin_read(..., rt_base_t pin) | 读取输入引脚的电平状态。 | 返回值: 0 (低电平) 或 1 (高电平)。 |
rt_err_t drv_pin_attach_irq(..., rt_int32_t pin, rt_uint32_t mode, void (*hdr)(void *args), void *args) | 给指定引脚注册中断处理函数。 | mode: 中断模式,如PIN_IRQ_MODE_RISING(上升沿)。hdr: 你的中断处理函数。 |
rt_err_t drv_pin_detach_irq(..., rt_int32_t pin) | 注销指定引脚的中断。 | |
rt_err_t drv_pin_irq_enable(..., rt_base_t pin, rt_uint32_t enabled) | 使能或关闭指定引脚的中断。 | enabled: 1 (使能) 或 0 (关闭)。 |
rt_base_t drv_pin_get(const char *name) | 根据引脚名(如”PA.1”)获取引脚编号(Pin ID)。 | 返回值: 对应的Pin ID。 |
4.2 HAL层接口 (底层硬件操作)
HAL层的函数是Driver层和裸机应用的基石。它们直接与硬件寄存器打交道,参数通常包含group(组号)和pin(组内偏移)。
几个核心的HAL函数:
| 函数原型 | 功能说明 |
|---|---|
int hal_gpio_set_func(unsigned int group, unsigned int pin, unsigned int func) | 设置引脚功能模式。func: 0=关闭,1=通用GPIO,2~8=专用功能(如UART、I2C)。 |
int hal_gpio_direction_input(unsigned int group, unsigned int pin) | 设置引脚为输入模式。 |
int hal_gpio_direction_output(unsigned int group, unsigned int pin) | 设置引脚为输出模式。 |
int hal_gpio_set_bias_pull(unsigned int group, unsigned int pin, unsigned int pull) | 设置引脚的上下拉。pull: 0=关闭,2=下拉,3=上拉。 |
int hal_gpio_set_irq_mode(unsigned int group, unsigned int pin, unsigned int irq_mode) | 设置引脚的中断触发模式。irq_mode: 0=下降沿,1=上升沿,4=双沿等。 |
注意:在RT-Thread环境下,我们一般通过
rt_pin_mode()等函数来间接配置这些属性,无需直接调用HAL函数。但在裸机开发或深度定制时,HAL接口就派上用场了。
5. 实战Demo:代码是怎么写的
理论说了这么多,来看一段实际的代码。这是衡山派SDK中test_gpio命令的部分源码,展示了如何使用RT-Thread的Pin设备API,特别是二级中断的配置方法:
static u32 test_gpio_input_pin_cfg(char *arg_pin) { // 1. 根据传入的引脚名称字符串(如"PA.1"),获取对应的引脚编号(Pin ID) static u32 pin = 0; pin = test_gpio_pin_check(arg_pin, GPIO_SET_MODE_FLAG); // 2. 配置引脚模式为上拉输入模式 // 这里调用的是RT-Thread的标准API rt_pin_mode rt_pin_mode(pin, PIN_MODE_INPUT_PULLUP); // 3. 注册中断处理函数 // 将自定义的中断处理程序 test_gpio_input_irq_handler 绑定到该引脚 // 中断触发模式设置为上升沿和下降沿都触发 (PIN_IRQ_MODE_RISING_FALLING) rt_pin_attach_irq(pin, PIN_IRQ_MODE_RISING_FALLING, test_gpio_input_irq_handler, &pin); // 最后一个参数是传递给中断处理函数的参数 // 4. 使能该引脚的中断功能 rt_pin_irq_enable(pin, PIN_IRQ_ENABLE); return pin; }这段代码是一个标准的GPIO输入中断配置流程:
- 引脚定位:通过名称获取Pin ID。
- 模式设置:设置为上拉输入模式,这样引脚在悬空时会有一个确定的高电平。
- 中断绑定:调用
rt_pin_attach_irq,告诉系统当这个引脚出现上升沿或下降沿变化时,去调用test_gpio_input_irq_handler这个函数。 - 中断使能:最后一步,打开中断开关。这一步很重要,很多初学者忘了使能中断,然后发现中断不触发。
当你需要在你的应用中使用GPIO中断时,参照这个流程来写就对了。你的中断处理函数test_gpio_input_irq_handler里,就可以处理按键按下、信号边沿等事件了。
