3. GPIO驱动移植与实战(LED/按键)
各位同学,欢迎来到第三章。这一章我们开始真正动手——把GPIO驱动移植到RT-Thread上。说白了,就是让板子上的LED能亮,按键能响应中断。我刚开始学RT-Thread时,觉得GPIO驱动是最简单的,结果踩了不少坑。今天我把这些经验都揉进这节课里,你跟着走一遍,后面再写其他外设驱动就顺了。
3.1 STM32 HAL库GPIO基础
先回顾一下HAL库的GPIO操作。STM32的GPIO,每个端口有16个引脚,每个引脚可以配置成输入、输出、复用功能或模拟模式。HAL库封装了这些操作,我们常用的就几个函数:
| 函数 | 功能 | 典型用法 |
|---|---|---|
HAL_GPIO_WritePin |
设置引脚输出电平 | 点亮/熄灭LED |
HAL_GPIO_ReadPin |
读取引脚输入电平 | 读取按键状态 |
HAL_GPIO_TogglePin |
翻转引脚输出电平 | LED闪烁 |
HAL_GPIO_EXTI_Callback |
外部中断回调函数 | 按键中断处理 |
嗯,这里要注意:HAL库的GPIO初始化是通过MX_GPIO_Init()完成的,CubeMX会帮我们生成配置代码。但RT-Thread的PIN设备驱动,需要我们自己把HAL库的底层操作封装成统一的接口。
3.2 RT-Thread PIN设备驱动框架解析
RT-Thread的PIN设备驱动,核心是一个结构体:struct rt_pin_ops。它定义了所有GPIO操作的标准接口。你想想看,不管是什么芯片,最终都要实现这组操作函数。
struct rt_pin_ops
{
void (*pin_mode)(rt_device_t dev, rt_base_t pin, rt_base_t mode);
void (*pin_write)(rt_device_t dev, rt_base_t pin, rt_base_t value);
rt_int32_t (*pin_read)(rt_device_t dev, rt_base_t pin);
rt_err_t (*pin_attach_irq)(rt_device_t dev, rt_base_t pin,
rt_uint32_t mode, void (*hdr)(void *args), void *args);
rt_err_t (*pin_detach_irq)(rt_device_t dev, rt_base_t pin);
rt_err_t (*pin_irq_enable)(rt_device_t dev, rt_base_t pin, rt_uint32_t enabled);
};
这个结构体就是驱动框架的“契约”。你只要实现了这6个函数,RT-Thread就能通过统一的API操作你的GPIO。比如上层调用rt_pin_write(),最终会调用到你写的pin_write函数。
我画了一张图,帮你理清整个调用链路:
3.3 编写GPIO驱动:注册PIN设备、实现ops
好,理论讲完了,我们直接上代码。我会一步步带你实现一个完整的GPIO驱动。
3.3.1 定义引脚编号映射
首先,我们需要把STM32的引脚编号映射到RT-Thread的引脚编号。RT-Thread用0~15表示每个端口的引脚,但我们需要区分不同端口。我习惯用(端口号 * 16 + 引脚号)的方式编码。
#define STM32_PIN(port, pin) ((port * 16) + pin)
/* 举例:PA0 = 0*16 + 0 = 0, PB5 = 1*16 + 5 = 21 */
#define LED_PIN STM32_PIN(0, 5) /* PA5 */
#define KEY_PIN STM32_PIN(1, 12) /* PB12 */
3.3.2 实现pin_mode函数
这个函数负责配置引脚模式。RT-Thread定义了PIN_MODE_OUTPUT、PIN_MODE_INPUT、PIN_MODE_INPUT_PULLUP等模式,我们需要映射到HAL库的GPIO初始化结构体。
static void stm32_pin_mode(rt_device_t dev, rt_base_t pin, rt_base_t mode)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
uint16_t hal_pin = 1 << (pin & 0x0F); /* 取低4位作为引脚号 */
GPIO_TypeDef *gpio_port = get_gpio_port(pin >> 4); /* 取高4位作为端口号 */
GPIO_InitStruct.Pin = hal_pin;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
switch (mode)
{
case PIN_MODE_OUTPUT:
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
break;
case PIN_MODE_INPUT:
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
break;
case PIN_MODE_INPUT_PULLUP:
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
break;
case PIN_MODE_INPUT_PULLDOWN:
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
break;
default:
return;
}
HAL_GPIO_Init(gpio_port, &GPIO_InitStruct);
}
3.3.3 实现pin_write和pin_read
这两个函数相对简单,直接调用HAL库的接口就行。
static void stm32_pin_write(rt_device_t dev, rt_base_t pin, rt_base_t value)
{
uint16_t hal_pin = 1 << (pin & 0x0F);
GPIO_TypeDef *gpio_port = get_gpio_port(pin >> 4);
HAL_GPIO_WritePin(gpio_port, hal_pin, (GPIO_PinState)value);
}
static rt_int32_t stm32_pin_read(rt_device_t dev, rt_base_t pin)
{
uint16_t hal_pin = 1 << (pin & 0x0F);
GPIO_TypeDef *gpio_port = get_gpio_port(pin >> 4);
return (rt_int32_t)HAL_GPIO_ReadPin(gpio_port, hal_pin);
}
3.3.4 实现中断相关函数
中断处理是GPIO驱动里最绕的部分。RT-Thread要求我们实现pin_attach_irq、pin_detach_irq和pin_irq_enable。核心思路是:用一个数组保存每个引脚的中断回调函数,然后在HAL库的HAL_GPIO_EXTI_Callback里统一分发。
/* 中断回调函数表 */
static struct rt_pin_irq_hdr pin_irq_hdr_tab[] =
{
{-1, 0, RT_NULL, RT_NULL},
/* ... 共16个,对应EXTI0~EXTI15 */
};
static rt_err_t stm32_pin_attach_irq(rt_device_t dev, rt_base_t pin,
rt_uint32_t mode, void (*hdr)(void *args), void *args)
{
rt_uint8_t irq_index = pin & 0x0F;
pin_irq_hdr_tab[irq_index].pin = pin;
pin_irq_hdr_tab[irq_index].mode = mode;
pin_irq_hdr_tab[irq_index].hdr = hdr;
pin_irq_hdr_tab[irq_index].args = args;
return RT_EOK;
}
/* HAL库中断回调 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
rt_uint8_t irq_index;
/* 根据GPIO_Pin找到对应的irq_index */
for (int i = 0; i < 16; i++)
{
if (GPIO_Pin & (1 << i))
{
irq_index = i;
break;
}
}
if (pin_irq_hdr_tab[irq_index].hdr != RT_NULL)
{
pin_irq_hdr_tab[irq_index].hdr(pin_irq_hdr_tab[irq_index].args);
}
}
3.3.5 注册PIN设备
最后一步,把ops结构体实例化,并注册到RT-Thread设备管理器中。
const static struct rt_pin_ops stm32_pin_ops =
{
stm32_pin_mode,
stm32_pin_write,
stm32_pin_read,
stm32_pin_attach_irq,
stm32_pin_detach_irq,
stm32_pin_irq_enable,
};
int rt_hw_pin_init(void)
{
rt_device_pin_register("pin", &stm32_pin_ops, RT_NULL);
return 0;
}
INIT_BOARD_EXPORT(rt_hw_pin_init); /* 板级初始化时自动调用 */
INIT_BOARD_EXPORT宏,确保PIN设备在系统启动早期就被注册。这样后面的线程和设备才能正常使用GPIO。我见过有人用INIT_APP_EXPORT,结果驱动还没注册好,应用就开始调用了,直接崩溃。
3.4 实战:LED闪烁与按键中断
驱动写好了,我们来验证一下。写两个简单的线程:一个让LED以500ms间隔闪烁,另一个等待按键中断并打印消息。
3.4.1 LED闪烁线程
static void led_thread_entry(void *parameter)
{
rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT);
while (1)
{
rt_pin_write(LED_PIN, PIN_HIGH);
rt_thread_mdelay(500);
rt_pin_write(LED_PIN, PIN_LOW);
rt_thread_mdelay(500);
}
}
3.4.2 按键中断线程
static void key_irq_callback(void *args)
{
/* 中断回调中只做标记,不要做耗时操作 */
rt_kprintf("Key pressed!\n");
}
static void key_thread_entry(void *parameter)
{
rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP);
rt_pin_attach_irq(KEY_PIN, PIN_IRQ_MODE_FALLING, key_irq_callback, RT_NULL);
rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE);
while (1)
{
rt_thread_mdelay(1000); /* 线程主要做其他任务,中断由回调处理 */
}
}
3.5 常见问题与避坑指南
- 引脚编号混乱:确保你的
STM32_PIN宏和HAL库的端口号一致。我建议在头文件里用枚举把所有用到的引脚定义好,避免硬编码。 - 中断优先级配置:RT-Thread需要中断优先级分组为4(即4位抢占优先级,0位子优先级)。如果CubeMX配置不对,会导致系统调度异常。
- 中断回调未注册:记得在
HAL_GPIO_EXTI_Callback里调用RT-Thread的回调分发。我一开始忘了这步,按键按下去一点反应都没有。 - GPIO时钟未使能:在
stm32_pin_mode里,如果端口时钟没开,配置会无效。建议在驱动初始化时统一使能所有可能用到的端口时钟。