3. 开发环境搭建
各位同学,这一章咱们来啃一块硬骨头——GPIO驱动移植。说实话,GPIO是嵌入式开发里最基础、最常用的外设,但恰恰是这种基础模块,移植起来反而容易出各种小毛病。我在做第一个RT-Thread项目时,就因为在GPIO中断配置上少写了一行代码,排查了整整一个下午。
这一章的目标很明确:把STM32的GPIO操作,封装成RT-Thread标准的PIN设备驱动。咱们分四步走——先分析STM32的GPIO库,再看RT-Thread的PIN框架,然后动手写驱动,最后用按键中断来验证。
3.1 STM32 GPIO标准库与HAL库分析
STM32的GPIO操作,说白了就是三件事:配置模式、读写电平、管理中断。标准库和HAL库的实现思路不太一样,但底层寄存器操作是相通的。
3.1.1 标准库核心流程
标准库的GPIO初始化,核心就两个结构体:GPIO_InitTypeDef 和 GPIO_TypeDef。前者描述引脚配置,后者映射到硬件寄存器。
// 标准库的典型GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
这里有个细节:标准库的 GPIO_Mode 是枚举值,直接对应CRL/CRH寄存器的配置位。HAL库则多了一层抽象,用 GPIO_InitTypeDef.Mode 组合了方向、上下拉、输出类型。
| 对比项 | 标准库 | HAL库 |
|---|---|---|
| 初始化结构体 | GPIO_InitTypeDef | GPIO_InitTypeDef(同名但字段不同) |
| 模式定义 | GPIO_Mode_IPU/GPIO_Mode_Out_PP | GPIO_MODE_INPUT/GPIO_MODE_OUTPUT_PP |
| 时钟使能 | RCC_APB2PeriphClockCmd() | __HAL_RCC_GPIOA_CLK_ENABLE() |
| 中断配置 | 需手动配置EXTI和NVIC | HAL_GPIO_EXTI_Init()封装 |
我个人习惯:在RT-Thread驱动层,我更倾向于用HAL库。因为HAL库的抽象层次更接近RT-Thread的设备模型,而且HAL库的延时函数、中断回调机制,跟RT-Thread的线程调度能更好地配合。
3.1.2 HAL库的GPIO中断机制
HAL库的GPIO中断,走的是EXTI(外部中断/事件控制器)路线。每个GPIO引脚都可以映射到16个EXTI线之一。这里有个坑:同一EXTI线不能同时映射多个引脚。比如PA0和PB0不能同时使用EXTI0。
// HAL库的GPIO中断配置示例
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
中断响应流程:引脚电平变化 → EXTI检测 → NVIC仲裁 → 执行中断服务函数 → 调用HAL库回调。嗯,这里要注意,HAL库的回调函数 HAL_GPIO_EXTI_Callback() 是弱定义,你需要自己重写。
3.2 RT-Thread PIN设备驱动框架
RT-Thread的PIN设备框架,说白了就是一套标准化的GPIO操作接口。它把不同芯片的GPIO差异,封装在 struct rt_pin_ops 这个结构体里。
3.2.1 框架核心结构
PIN设备框架分为三层:
- 应用层:用户调用
rt_pin_mode()、rt_pin_write()、rt_pin_read()、rt_pin_attach_irq()等API。 - 框架层:维护引脚号到硬件引脚的映射表,统一中断回调注册机制。
- 驱动层:实现
rt_pin_ops中的函数指针,对接具体芯片的寄存器操作。
// rt_pin_ops 结构体定义(简化版)
struct rt_pin_ops
{
void (*pin_mode)(rt_base_t pin, rt_base_t mode);
void (*pin_write)(rt_base_t pin, rt_base_t value);
rt_int32_t (*pin_read)(rt_base_t pin);
rt_err_t (*pin_attach_irq)(rt_base_t pin, rt_uint32_t mode,
void (*hdr)(void *args), void *args);
rt_err_t (*pin_detach_irq)(rt_base_t pin);
rt_err_t (*pin_irq_enable)(rt_base_t pin, rt_uint32_t enabled);
};
你想想看,这个设计妙在哪里?它把硬件相关的操作全部隔离在驱动层。上层应用写一次代码,换芯片时只需要换底层驱动。我在做产品迭代时,从STM32F1换到F4,应用层代码一行没改。
3.2.2 引脚号映射规则
RT-Thread使用统一的引脚编号:PIN_NUM(port, pin)。比如PA0就是 PIN_NUM(0, 0),PB5就是 PIN_NUM(1, 5)。这个宏定义通常在 pin_map.h 里。
// 引脚号映射示例
#define PIN_NUM(port, pin) ((port) * 16 + (pin))
// 使用示例
rt_pin_mode(PIN_NUM(0, 0), PIN_MODE_INPUT_PULLUP); // PA0上拉输入
rt_pin_write(PIN_NUM(1, 5), PIN_HIGH); // PB5输出高电平
pin_map.h 里给每个实际使用的引脚起个语义化的宏名,比如 #define LED_PIN PIN_NUM(0, 1)。这样代码可读性会好很多。
3.3 编写GPIO驱动
好了,理论说完了,咱们动手写代码。这一节我会带着你,一步步把STM32的GPIO封装成RT-Thread的PIN设备。
3.3.1 驱动初始化
驱动初始化要做两件事:注册 rt_pin_ops 到框架,以及初始化必要的硬件资源。
// drv_gpio.c 核心初始化代码
static struct rt_pin_ops stm32_pin_ops =
{
.pin_mode = stm32_pin_mode,
.pin_write = stm32_pin_write,
.pin_read = stm32_pin_read,
.pin_attach_irq = stm32_pin_attach_irq,
.pin_detach_irq = stm32_pin_detach_irq,
.pin_irq_enable = stm32_pin_irq_enable,
};
int rt_hw_pin_init(void)
{
// 注册PIN设备
rt_device_pin_register("pin", &stm32_pin_ops, RT_NULL);
return RT_EOK;
}
INIT_BOARD_EXPORT(rt_hw_pin_init);
这里用了 INIT_BOARD_EXPORT 宏,确保驱动在板级初始化阶段就被加载。我建议所有外设驱动都遵循这个初始化顺序,避免出现依赖问题。
3.3.2 核心函数实现
咱们挑两个最典型的函数来讲解:pin_mode 和 pin_attach_irq。
// pin_mode 实现:将RT-Thread模式映射到HAL库模式
static void stm32_pin_mode(rt_base_t pin, rt_base_t mode)
{
uint16_t gpio_pin = 1 << (pin % 16);
GPIO_TypeDef *gpio_port = get_gpio_port(pin / 16);
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = gpio_pin;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
switch (mode)
{
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_OUTPUT:
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
break;
// ... 其他模式
}
HAL_GPIO_Init(gpio_port, &GPIO_InitStruct);
}
// pin_attach_irq 实现:注册中断回调
static rt_err_t stm32_pin_attach_irq(rt_base_t pin, rt_uint32_t mode,
void (*hdr)(void *args), void *args)
{
uint8_t irq_line = pin % 16;
// 保存回调函数到全局表
irq_callback_table[irq_line].callback = hdr;
irq_callback_table[irq_line].args = args;
// 配置EXTI触发模式
// ... 这里省略HAL库的EXTI配置代码
return RT_EOK;
}
3.4 测试按键中断
驱动写完了,得验证一下。咱们用按键中断来测试——按下按键,LED翻转。这是嵌入式开发的"Hello World"。
3.4.1 硬件连接
| 信号 | 引脚 | 配置 |
|---|---|---|
| 按键 | PA0 | 上拉输入,下降沿触发 |
| LED | PB1 | 推挽输出 |
3.4.2 应用层代码
// main.c 测试代码
#include <rtthread.h>
#include <rtdevice.h>
#define KEY_PIN PIN_NUM(0, 0) // PA0
#define LED_PIN PIN_NUM(1, 1) // PB1
static void key_irq_callback(void *args)
{
rt_base_t pin = (rt_base_t)args;
rt_pin_write(LED_PIN, !rt_pin_read(LED_PIN));
}
int key_irq_test(void)
{
// 配置LED引脚
rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT);
rt_pin_write(LED_PIN, PIN_LOW);
// 配置按键引脚
rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP);
// 注册中断回调
rt_pin_attach_irq(KEY_PIN, PIN_IRQ_MODE_FALLING,
key_irq_callback, (void *)KEY_PIN);
// 使能中断
rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE);
return RT_EOK;
}
MSH_CMD_EXPORT(key_irq_test, test key interrupt);
这段代码在MSH命令行输入 key_irq_test 就能运行。按下按键,LED状态翻转。如果没反应,先检查中断是否使能,再检查回调函数是否注册成功。
rt_pin_write,但这个函数内部可能有关键区保护。如果在中断上下文里调用会导致死锁。解决办法是:在回调函数里只发送一个信号量或事件,真正的处理放在线程里做。
3.5 本章知识体系
下面这张图,把GPIO驱动移植的完整脉络画出来了。从硬件寄存器到应用API,每一层都有明确的职责。
从这张图可以看得很清楚:应用层只调用RT-Thread的标准API,完全不关心底层是STM32还是GD32。驱动层负责把标准API翻译成HAL库调用,而HAL库再操作寄存器。这就是分层设计的威力。
好了,这一章的内容就到这里。GPIO驱动移植是RT-Thread外设驱动的基础,把这个搞懂了,后面UART、I2C、SPI的移植思路都是类似的。记住:先分析硬件库,再理解框架,最后动手写代码——这个顺序别搞反了。