3. STM32硬件基础回顾
这一章,我们来啃一块硬骨头——GPIO。别觉得它简单,GPIO是嵌入式世界里最基础也最容易被忽视的外设。我见过不少工程师,GPIO点灯溜得很,但一遇到中断回调、设备模型集成就翻车。说白了,寄存器操作是基本功,框架理解才是分水岭。
3.1 STM32 GPIO寄存器,你真的看懂了吗?
先看寄存器。STM32的GPIO,每个端口有16个引脚,控制寄存器就那么几个。我习惯把它们分成三类:模式配置、数据读写、复用与中断。
| 寄存器 | 功能 | 我常用的位域 |
|---|---|---|
| MODER | 模式选择(输入/输出/复用/模拟) | 每引脚2位,00=输入,01=输出,10=复用,11=模拟 |
| OTYPER | 输出类型(推挽/开漏) | 每引脚1位,0=推挽,1=开漏 |
| OSPEEDR | 输出速度 | 每引脚2位,建议外设用high,GPIO用medium |
| PUPDR | 上下拉配置 | 每引脚2位,01=上拉,10=下拉 |
| IDR / ODR | 输入/输出数据 | 读IDR,写ODR |
| BSRR | 位设置/复位 | 低16位置位,高16位复位,原子操作 |
| AFRL / AFRH | 复用功能选择 | 每引脚4位,查数据手册对应AF编号 |
这里有个坑,我刚开始做项目时踩过——BSRR寄存器。很多人写ODR来操作引脚,但ODR不是原子操作,在多任务环境下可能被中断打断。BSRR是原子操作,写1置位,写1复位,不会受其他操作影响。嗯,这点在RT-Thread里尤其重要。
核心原则:多任务环境下操作GPIO输出,优先用BSRR而不是ODR。BSRR的原子性可以避免“读-改-写”带来的竞态问题。
3.2 RT-Thread PIN设备驱动框架
RT-Thread把GPIO抽象成了PIN设备。说白了,就是不管你用STM32还是GD32,上层API都一样。框架分三层:
- 设备驱动层:实现rt_pin_ops结构体,包含mode、write、read、attach_irq等函数指针
- 设备管理层:rt_device_pin_register注册设备,提供统一接口
- 应用层:rt_pin_mode、rt_pin_write、rt_pin_read、rt_pin_attach_irq
我个人的习惯是,先看驱动层的实现,再理解上层API。驱动层核心就是下面这个结构体:
static const 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,
stm32_pin_get,
};
每个函数对应一个寄存器操作。比如stm32_pin_write,最终调用的就是BSRR或ODR。你想想看,框架把底层差异全封装了,应用层根本不用关心寄存器地址。
小技巧:RT-Thread的PIN编号是线性映射的。比如PA0=0,PA1=1,...,PB0=16。这个映射关系在board.h里定义,移植时一定要核对。
3.3 编写GPIO按键驱动
好,理论讲完了,咱们动手写一个按键驱动。这个驱动要支持:
- 按键初始化(配置为输入、上拉)
- 读取按键状态
- 注册中断回调
- 消抖处理
先看初始化代码:
#include <rtthread.h>
#include <rtdevice.h>
#define KEY_PIN GET_PIN(C, 13) // PC13,板载按键
#define KEY_IRQ_MODE PIN_IRQ_MODE_FALLING
static struct rt_semaphore key_sem;
static void key_irq_callback(void *args)
{
rt_sem_release(&key_sem);
}
static void key_thread_entry(void *parameter)
{
while (1)
{
if (rt_sem_take(&key_sem, RT_WAITING_FOREVER) == RT_EOK)
{
// 消抖处理:延时20ms再读一次
rt_thread_mdelay(20);
if (rt_pin_read(KEY_PIN) == PIN_LOW)
{
rt_kprintf("Key pressed!\n");
// 这里可以触发你的业务逻辑
}
}
}
}
int key_driver_init(void)
{
rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP);
rt_pin_attach_irq(KEY_PIN, KEY_IRQ_MODE, key_irq_callback, RT_NULL);
rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE);
rt_sem_init(&key_sem, "key_sem", 0, RT_IPC_FLAG_FIFO);
rt_thread_t tid = rt_thread_create("key", key_thread_entry, RT_NULL, 1024, 5, 10);
if (tid != RT_NULL)
rt_thread_startup(tid);
return RT_EOK;
}
INIT_APP_EXPORT(key_driver_init);
这段代码有几个关键点:
- 中断回调里只做信号量释放,不做耗时操作。这是RT-Thread的硬性要求,中断上下文不能阻塞。
- 消抖放在线程里做,用rt_thread_mdelay延时,不占用中断时间。
- 使用INIT_APP_EXPORT自动初始化,省去手动调用的麻烦。
我曾经踩过的坑:中断回调里直接调用了rt_kprintf,结果系统卡死。原因是rt_kprintf可能触发调度,而中断里不允许调度。记住:中断回调只做最轻量的事,比如发信号量、发消息、置标志位。
3.4 中断处理与回调机制
RT-Thread的中断处理,说白了就是三层跳转:
- 硬件中断:CPU响应中断,跳转到中断向量表
- 内核中断处理:rt_hw_interrupt_handler,保存上下文,调用注册的中断服务函数
- 驱动回调:stm32_pin_irq_handler遍历引脚中断状态,调用用户注册的回调函数
我画了一张图,帮你理清这个流程:
回调机制的核心,是函数指针。驱动层维护一个回调函数表,每个引脚可以注册一个回调。当硬件中断发生时,驱动遍历所有引脚,找到触发中断的引脚,调用对应的回调函数。
// 驱动内部实现(简化版)
static void (*pin_irq_handler[16])(void *args);
void stm32_pin_irq_handler(int pin)
{
if (pin_irq_handler[pin] != RT_NULL)
{
pin_irq_handler[pin](RT_NULL); // 调用用户回调
}
}
这里有个设计细节:回调函数的参数是void *args,你可以传入任意结构体指针。我习惯传入一个结构体,包含引脚号、事件类型、用户数据,这样回调里能拿到完整的上下文信息。
最佳实践:回调函数里只做“通知”操作,不做“处理”操作。通知线程去处理,线程里做消抖、滤波、业务逻辑。这样中断响应快,系统稳定。
3.5 本章小结
这一章我们走了三个层次:寄存器层、框架层、应用层。寄存器是基础,框架是工具,应用是目标。我个人觉得,理解这三层的关系,比背下每个寄存器的地址更重要。你想想看,不管芯片怎么换,只要框架在,应用代码几乎不用改——这就是设备模型的价值。