4. 外设驱动开发(GPIO与UART):寄存器映射、GPIO输入输出驱动、UART轮询与中断模式驱动
好,咱们今天聊点实在的。外设驱动开发,说白了就是让芯片的“手脚”动起来。GPIO和UART,这是嵌入式开发里最基础、也最常用的两个外设。你想想看,一个芯片如果连灯都点不亮、数据都发不出去,那跟一块石头有什么区别?
我个人习惯,拿到一个新芯片,第一件事就是先把GPIO和UART调通。为什么?因为GPIO能帮你验证芯片是否正常工作,UART则是你调试的“眼睛”和“嘴巴”。今天我就把这两个外设的驱动开发,从寄存器映射到中断模式,掰开了揉碎了讲给你听。
4.1 寄存器映射:让C语言直接操作硬件
寄存器映射,这是驱动开发的基石。说白了,就是把芯片手册里那些物理地址,用C语言的指针或者结构体给“映射”过来。这样你写代码的时候,就能像操作普通变量一样去读写硬件寄存器了。
我在项目中遇到过不少新手,上来就写 *(volatile uint32_t *)0x40020C00 = 0x01; 这种代码。嗯,能跑,但可读性太差了。你过两周再看,自己都看不懂写了啥。
我建议的做法,是用结构体把一组寄存器封装起来。比如GPIO,通常会有MODER、OTYPER、OSPEEDR、PUPDR、IDR、ODR、BSRR、AFRL、AFRH这些寄存器。你可以这样定义:
typedef struct {
volatile uint32_t MODER; // 模式寄存器
volatile uint32_t OTYPER; // 输出类型寄存器
volatile uint32_t OSPEEDR; // 输出速度寄存器
volatile uint32_t PUPDR; // 上下拉寄存器
volatile uint32_t IDR; // 输入数据寄存器
volatile uint32_t ODR; // 输出数据寄存器
volatile uint32_t BSRR; // 位设置/复位寄存器
volatile uint32_t AFRL; // 复用功能低寄存器
volatile uint32_t AFRH; // 复用功能高寄存器
} GPIO_TypeDef;
然后,把基地址映射成指针:
#define GPIOA_BASE 0x40020000
#define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB_BASE 0x40020400
#define GPIOB ((GPIO_TypeDef *)GPIOB_BASE)
这样,你写 GPIOA->MODER = 0x01; 是不是就清晰多了?
volatile 关键字。编译器有时候会自作聪明地优化掉你对寄存器的读写,加了 volatile 就是告诉编译器:“别动我的寄存器,每次都要老老实实地去读去写。”
4.2 GPIO输入输出驱动:从点灯到按键检测
GPIO驱动,最经典的两个场景就是:输出控制LED,输入检测按键。咱们一个一个说。
4.2.1 GPIO输出驱动——点灯
点灯,嵌入式界的“Hello World”。但你别小看它,这里面门道不少。
第一步,使能时钟。很多新手会栽在这里——寄存器配了半天,灯就是不亮。为什么?因为GPIO模块的时钟没开!在STM32这类芯片上,外设的时钟默认是关闭的,你得先打开它:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; // 使能GPIOA时钟
第二步,配置模式。GPIO有输入、输出、复用、模拟四种模式。输出的话,还要选择推挽还是开漏。推挽输出能主动拉高拉低,开漏则需要外部上拉电阻。我一般用推挽输出:
GPIOA->MODER &= ~(0x3 << (5 * 2)); // 先清零
GPIOA->MODER |= (0x1 << (5 * 2)); // 设置为输出模式
GPIOA->OTYPER &= ~(0x1 << 5); // 推挽输出
GPIOA->OSPEEDR |= (0x2 << (5 * 2)); // 高速
第三步,输出电平。你可以直接写ODR寄存器,但我更推荐用BSRR寄存器。BSRR是“原子操作”,写一次就能同时设置和复位多个引脚,不会被打断:
GPIOA->BSRR = (1 << 5); // 拉高PA5,灯亮
// 或者
GPIOA->BSRR = (1 << (5 + 16)); // 拉低PA5,灯灭
4.2.2 GPIO输入驱动——按键检测
按键检测,看起来简单,但要做好不容易。核心问题就是:机械抖动。
你按下一个按键,实际上在几毫秒内会反复弹跳好几次。如果不做处理,一次按键可能会被误判成十几次。我常用的消抖方法有两种:
- 硬件消抖: 加RC滤波电路,简单粗暴,但会增加BOM成本。
- 软件消抖: 检测到电平变化后,延时10-20ms再读一次。如果状态一致,就认为是有效按键。
配置输入模式也很简单:
GPIOA->MODER &= ~(0x3 << (0 * 2)); // 输入模式
GPIOA->PUPDR |= (0x1 << (0 * 2)); // 上拉,默认高电平
读取按键状态:
uint8_t key_state = (GPIOA->IDR >> 0) & 0x01; // 读PA0
嗯,这里要注意,按键按下时,如果配置了上拉,读到的就是低电平。别搞反了。
4.3 UART轮询模式驱动:简单可靠的数据收发
UART,串口,嵌入式调试的“生命线”。轮询模式是最简单的实现方式,CPU不停地检查状态寄存器,看看数据有没有收到、能不能发送。
先看初始化。UART的配置参数不少:波特率、数据位、停止位、校验位、流控。我一般用115200-8-N-1,也就是115200波特率、8位数据、无校验、1位停止位:
void UART_Init(void) {
// 1. 使能时钟
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// 2. 配置GPIO复用功能
GPIOA->MODER &= ~(0x3 << (2*2) | 0x3 << (3*2));
GPIOA->MODER |= (0x2 << (2*2) | 0x2 << (3*2)); // 复用功能
GPIOA->AFRL |= (0x7 << (2*4) | 0x7 << (3*4)); // AF7 = USART2
// 3. 配置UART参数
USART2->BRR = 84000000 / 115200; // 波特率
USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; // 使能发送、接收、UART
}
轮询发送一个字节:
void UART_SendByte(uint8_t data) {
while(!(USART2->SR & USART_SR_TXE)); // 等待发送缓冲区空
USART2->DR = data;
}
轮询接收一个字节:
uint8_t UART_ReceiveByte(void) {
while(!(USART2->SR & USART_SR_RXNE)); // 等待接收数据
return (uint8_t)(USART2->DR & 0xFF);
}
4.4 UART中断模式驱动:高效的数据处理
中断模式,才是真正实用的UART驱动方式。CPU不用傻等,数据来了,硬件会自动触发中断,你只需要在中断服务函数里处理数据就行。
配置中断的步骤:
// 在初始化中增加中断使能
USART2->CR1 |= USART_CR1_RXNEIE; // 使能接收中断
NVIC_EnableIRQ(USART2_IRQn); // 使能NVIC中断
NVIC_SetPriority(USART2_IRQn, 1); // 设置优先级
中断服务函数:
volatile uint8_t rx_buffer[256];
volatile uint16_t rx_index = 0;
void USART2_IRQHandler(void) {
if(USART2->SR & USART_SR_RXNE) {
uint8_t data = (uint8_t)(USART2->DR & 0xFF);
rx_buffer[rx_index++] = data;
if(rx_index >= 256) rx_index = 0; // 环形缓冲区,溢出回绕
}
}
这里有个坑,我踩过好几次。中断服务函数里,一定要先判断中断标志位,再读数据。因为UART可能有多个中断源(接收、发送、错误等),你不判断就一股脑读DR,可能会读到错误数据。
另外,中断服务函数要尽量短。别在里面做复杂的处理,比如解析协议、调用printf。正确的做法是:中断里只收数据,放到缓冲区,然后设置一个标志位。主循环里检测到标志位,再去处理数据。
4.5 总结与避坑清单
好了,GPIO和UART的驱动开发,核心内容就这些。我最后给你列一个避坑清单,都是我曾经血泪教训换来的:
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| GPIO不工作 | 时钟没使能 | 检查RCC寄存器,确保外设时钟已开启 |
| 按键误触发 | 机械抖动 | 软件延时消抖,或硬件RC滤波 |
| UART数据乱码 | 波特率不匹配 | 核对时钟源频率和BRR寄存器计算值 |
| 中断不触发 | NVIC未使能 | 检查NVIC_EnableIRQ和中断优先级设置 |
| 中断服务函数卡死 | 未清除中断标志 | 读DR寄存器会自动清除RXNE标志,但其他标志需手动清除 |
驱动开发,说白了就是“读手册、配寄存器、测功能”这三板斧。但每一板斧都有它的门道。你多写几次,多踩几个坑,自然就熟练了。下一章,咱们聊聊定时器和PWM,那又是另一番天地了。