2. 开发环境搭建

各位同学,今天我们来啃一块硬骨头——GPIO驱动。别紧张,我保证讲完这节课,你们会对GPIO有全新的认识。我自己做嵌入式这些年,GPIO可以说是打交道最多的外设了,没有之一。

先说说这节课要干什么。我们要从STM32的GPIO寄存器底层开始,一路摸到RT-Thread的PIN设备驱动框架,最后用按键中断控制LED来收尾。说白了,就是让你既看得懂芯片手册,又能用RT-Thread的API写出优雅的驱动代码。

GPIO驱动开发知识体系 GPIO外设寄存器 PIN设备驱动框架 按键中断控制LED 寄存器详解 MODER / OTYPER / OSPEEDR PUPDR / IDR / ODR / BSRR AFRL / AFRH / LCKR PIN设备框架 rt_pin_mode() / rt_pin_write() rt_pin_read() / rt_pin_attach_irq() rt_pin_irq_enable() / 回调机制 实战要点 按键消抖处理 中断优先级配置 LED状态切换逻辑 从寄存器到框架 → 高效驱动开发

1. GPIO外设寄存器详解

先看寄存器。STM32的GPIO模块,每个端口(PA、PB、PC...)都有一组控制寄存器。我刚开始学的时候,觉得寄存器太多了记不住。后来发现,其实核心的就那么几个。

寄存器 偏移地址 功能描述 我常用的配置
MODER 0x00 模式选择(输入/输出/复用/模拟) 00=输入,01=输出,10=复用
OTYPER 0x04 输出类型(推挽/开漏) 0=推挽,1=开漏
OSPEEDR 0x08 输出速度 一般用10=50MHz足够
PUPDR 0x0C 上拉/下拉配置 01=上拉,10=下拉
IDR 0x10 输入数据寄存器(只读) 读引脚电平
ODR 0x14 输出数据寄存器 写引脚电平
BSRR 0x18 位设置/复位寄存器 原子操作,推荐使用
AFRL/AFRH 0x20/0x24 复用功能选择 配置外设引脚映射
我的小技巧:配置GPIO时,我习惯用BSRR而不是ODR。为什么?BSRR是原子操作,写ODR可能被中断打断,导致引脚状态异常。这个坑我踩过,后来就再也不用ODR直接写值了。

举个例子,配置PA5为推挽输出、上拉、50MHz速度:

// 直接寄存器操作
GPIOA->MODER   &= ~(0x3 << 10);  // 先清零
GPIOA->MODER   |=  (0x1 << 10);  // 设为输出模式
GPIOA->OTYPER  &= ~(0x1 << 5);   // 推挽输出
GPIOA->OSPEEDR |=  (0x2 << 10);  // 50MHz速度
GPIOA->PUPDR   |=  (0x1 << 10);  // 上拉

// 设置PA5输出高电平
GPIOA->BSRR = (1 << 5);           // 原子操作,设置位
// 设置PA5输出低电平
GPIOA->BSRR = (1 << (5 + 16));   // 原子操作,复位位
注意:配置复用功能时,AFRL控制PIN0~7,AFRH控制PIN8~15。每个引脚占用4位。我曾经见过有人把AFRL和AFRH搞混,结果调试了一整天。

2. RT-Thread PIN设备驱动框架

好了,寄存器看完了。但实际项目中,我们不会每次都去操作寄存器。RT-Thread给我们封装了一套PIN设备驱动框架,用起来非常方便。

这个框架的核心思想是:把GPIO操作抽象成统一的API,不管你用STM32还是GD32,代码都不用改。我参与RT-Thread内核开发时,这个框架的设计原则就是——简单、统一、可移植。

2.1 核心API一览

API函数 功能 参数说明
rt_pin_mode() 设置引脚模式 pin:引脚号,mode:模式
rt_pin_write() 写引脚电平 pin:引脚号,value:0/1
rt_pin_read() 读引脚电平 pin:引脚号,返回0/1
rt_pin_attach_irq() 绑定中断回调 pin, mode, callback, args
rt_pin_irq_enable() 使能/禁能中断 pin, enabled
rt_pin_detach_irq() 解绑中断回调 pin

引脚编号规则:RT-Thread使用统一的引脚编号,公式为:pin = (port_index * 16) + pin_index。比如PA5 = 0*16 + 5 = 5,PB3 = 1*16 + 3 = 19。这个规则在移植时特别有用。

2.2 框架内部是怎么工作的?

说白了,PIN设备框架就是一层软件封装。底层调用BSP提供的寄存器操作函数,上层给用户提供统一的API。我画个图你们就明白了:

用户应用程序
      ↓
PIN设备API层 (rt_pin_mode, rt_pin_write...)
      ↓
PIN设备驱动层 (stm32_pin_write, stm32_pin_mode...)
      ↓
硬件寄存器层 (GPIOA->BSRR, GPIOA->MODER...)
      ↓
STM32 GPIO硬件

每一层各司其职。用户层不用关心底层实现,驱动层负责和硬件打交道。这种分层设计,我在做RT-Thread内核贡献时深有体会——它让代码的复用性和可维护性大大提升。

3. 实战:按键中断控制LED

理论讲完了,来点实际的。我们用按键中断控制LED,这是嵌入式开发最经典的入门实战。但别小看它,里面有不少门道。

3.1 硬件连接

器件 引脚 配置
LED PA5 推挽输出,低电平点亮
按键 PB2 上拉输入,下降沿触发中断

3.2 完整代码实现

#include <rtthread.h>
#include <rtdevice.h>

#define LED_PIN    5    // PA5
#define KEY_PIN    18   // PB2 = 1*16 + 2

/* 按键中断回调函数 */
static void key_irq_callback(void *args)
{
    rt_uint32_t pin = (rt_uint32_t)args;
    
    /* 翻转LED状态 */
    rt_pin_write(LED_PIN, !rt_pin_read(LED_PIN));
    
    rt_kprintf("Key pressed! LED toggled.\n");
}

/* 初始化函数 */
static int key_led_init(void)
{
    /* 配置LED引脚 */
    rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT);
    rt_pin_write(LED_PIN, PIN_HIGH);  // 初始关闭LED
    
    /* 配置按键引脚 */
    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);
    
    rt_kprintf("Key-LED demo initialized.\n");
    return RT_EOK;
}

/* 自动初始化 */
INIT_APP_EXPORT(key_led_init);
避坑指南:我曾经在中断回调函数里直接调用rt_kprintf,结果系统卡死了。为什么?因为rt_kprintf内部可能使用了互斥锁,而中断上下文不允许阻塞。记住:中断回调里不要做耗时操作,最好只设置标志位,把实际处理放到线程里。

3.3 按键消抖处理

说到按键,就绕不开消抖。机械按键按下时会产生抖动,如果不处理,一次按下可能触发多次中断。我常用的方法有两种:

消抖方式 原理 适用场景 缺点
硬件消抖 RC滤波电路 对实时性要求高 增加BOM成本
软件延时消抖 检测到变化后延时10~20ms再确认 大多数场景 阻塞CPU
定时器扫描消抖 定时器周期性扫描,连续多次采样一致才确认 RTOS环境 实现稍复杂

在RT-Thread里,我推荐用定时器扫描的方式。不阻塞,不浪费CPU,而且和RTOS的思维很搭。

/* 定时器扫描消抖示例 */
static void key_scan_timer_callback(void *parameter)
{
    static rt_uint8_t last_state = 1;
    static rt_uint8_t stable_cnt = 0;
    rt_uint8_t cur_state;
    
    cur_state = rt_pin_read(KEY_PIN);
    
    if (cur_state != last_state) {
        stable_cnt++;
        if (stable_cnt >= 3) {  // 连续3次采样一致
            last_state = cur_state;
            if (cur_state == 0) {  // 按键按下
                rt_pin_write(LED_PIN, !rt_pin_read(LED_PIN));
            }
            stable_cnt = 0;
        }
    } else {
        stable_cnt = 0;
    }
}

/* 创建定时器,10ms周期扫描 */
static rt_timer_t key_timer = RT_NULL;

static int key_timer_init(void)
{
    key_timer = rt_timer_create("key_scan", 
                                key_scan_timer_callback,
                                RT_NULL, 
                                10,  // 10ms周期
                                RT_TIMER_FLAG_PERIODIC);
    if (key_timer != RT_NULL) {
        rt_timer_start(key_timer);
    }
    return RT_EOK;
}
INIT_APP_EXPORT(key_timer_init);
我的经验:实际项目中,我更喜欢用定时器扫描+状态机的方式处理按键。这样不仅能消抖,还能区分短按、长按、双击等操作。一个按键可以玩出很多花样。

4. 中断优先级配置

最后提一下中断优先级。很多人忽略这个,结果系统跑起来各种奇怪问题。RT-Thread默认的中断优先级分组是抢占式优先级4位,子优先级0位。也就是说,优先级值越小,优先级越高。

配置中断优先级时,我建议遵循一个原则:实时性要求高的中断优先级高,但不要高于系统心跳中断。否则可能导致系统调度异常。

/* 在board.h或stm32_config.h中配置 */
/* 设置中断优先级分组 */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);

/* 配置EXTI中断优先级 */
NVIC_SetPriority(EXTI2_IRQn, 5);  // 优先级值5,够用

总结一下这节课的核心:

  • GPIO寄存器:MODER、OTYPER、OSPEEDR、PUPDR、BSRR是核心
  • PIN设备框架:统一API,分层设计,方便移植
  • 按键中断:注意消抖,中断回调不要做耗时操作
  • 中断优先级:合理配置,别抢系统心跳的优先级

好了,这节课的内容就到这里。代码我都跑过,没问题。你们自己动手试试,遇到问题欢迎交流。记住,驱动开发没有捷径,多动手、多踩坑,经验就来了。