3. STM32基础外设回顾:GPIO、UART、I2C、SPI、定时器的基础配置与调试方法

做嵌入式AI边缘计算,说白了就是把AI模型塞进MCU里跑。但不管上层算法多花哨,最终都得跟物理世界打交道——点个灯、收个传感器数据、跟外设聊聊天。这些全靠基础外设。

我见过不少新手,一上来就追AI框架,结果连GPIO都翻车。LED死活不亮,查半天发现是时钟没开。嗯,这章咱们就把STM32的五个看门外设捋一遍。不追求大而全,只讲你真正用得上的配置和调试技巧。

本章核心: 不是背寄存器,而是建立一套「配置→验证→调试」的闭环思维。每个外设我都会给出一套我自己的调试检查清单。
STM32 基础外设知识体系 STM32 外设总线 GPIO UART I2C SPI 定时器 通用调试三板斧 ① 逻辑分析仪抓波形(看时序对不对) ② 串口打印调试信息(看数据对不对) ③ 示波器测电平(看硬件通不通)

3.1 GPIO —— 最基础也最容易翻车的外设

GPIO,通用输入输出。说白了就是让芯片引脚变成你的手——可以往外推电平(输出),也可以感知外部电平(输入)。

配置GPIO其实就三步:开时钟、配模式、设电平。但我见过太多人在第一步就跪了。STM32的GPIO挂在AHB总线上,你不开时钟,写再多寄存器也没用。

我的习惯: 每次配GPIO前,先看一眼RCC寄存器。确保对应GPIO组的时钟使能位已经置1。这是排查"引脚没反应"的第一站。

3.1.1 输出模式配置

// 以STM32F4为例,配置PA5为推挽输出
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;   // 开时钟
GPIOA->MODER |= GPIO_MODER_MODER5_0;    // 设为输出模式
GPIOA->OTYPER &= ~GPIO_OTYPER_OT_5;     // 推挽输出
GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR5; // 高速
GPIOA->PUPDR &= ~GPIO_PUPDR_PUPDR5;    // 无上下拉

// 输出高电平
GPIOA->BSRR = GPIO_BSRR_BS_5;

这里有个坑——输出速度。我做过一个项目,用PA5输出1MHz的方波驱动LED,结果波形严重失真。查了半天才发现OSPEEDR配的是低速。你想想看,GPIO的输出驱动能力跟速度等级直接相关,配低了高频就上不去。

3.1.2 输入模式配置

// 配置PC13为输入,内部上拉
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
GPIOC->MODER &= ~GPIO_MODER_MODER13_0; // 输入模式
GPIOC->PUPDR |= GPIO_PUPDR_PUPDR13_0;  // 上拉

// 读取电平
uint8_t level = (GPIOC->IDR & GPIO_IDR_ID13) ? 1 : 0;
我曾经踩过的坑: 读取按键输入时没做消抖。结果一次按键触发了几十次中断。后来老老实实加了10ms延时消抖,或者用定时器做采样滤波。别偷懒,机械开关的抖动是真实存在的。

3.2 UART —— 调试的命脉

做嵌入式开发,UART就是你的眼睛。没有串口打印,你连程序跑到哪了都不知道。我个人习惯,任何新板子到手,第一件事就是调通UART。

3.2.1 基础配置

// USART2配置:PA2-TX, PA3-RX, 115200-8N1
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

// 配置TX为复用功能
GPIOA->MODER |= GPIO_MODER_MODER2_1;   // AF模式
GPIOA->AFR[0] |= 7 << 8;              // AF7 = USART2

// 配置RX为复用功能
GPIOA->MODER |= GPIO_MODER_MODER3_1;
GPIOA->AFR[0] |= 7 << 12;

// 设置波特率
USART2->BRR = 84000000 / 115200;       // 假设APB1=42MHz, 过采样16
USART2->CR1 = USART_CR1_TE | USART_CR1_RE | USART_CR1_UE;

波特率计算是个容易出错的地方。公式很简单:BRR = 时钟频率 / (16 × 目标波特率)。但时钟源是哪个?APB1还是APB2?查清楚再算。

3.2.2 调试技巧

  • 打印函数: 重定向printf到UART,用半主机模式或直接写ITM。我推荐用ITM,不占用UART硬件资源。
  • 环形缓冲区: 中断接收时一定要用环形缓冲区。否则丢数据是必然的。
  • 波特率验证: 用逻辑分析仪抓TX引脚的波形,量一下位宽。1/位宽 = 实际波特率。
避坑指南: 我曾经在调试一个AI推理结果时,发现打印出来的浮点数全是0。查了半天,原来是printf默认不支持%f。需要在编译器选项里加上-u_printf_float。这种坑,遇到了才知道。

3.3 I2C —— 双线制下的优雅与烦恼

I2C只有两根线:SCL和SDA。但正因为简单,反而容易出问题。我调试I2C设备时,最常用的工具就是逻辑分析仪。看波形比看代码快多了。

3.3.1 主机模式配置

// I2C1配置:PB6-SCL, PB7-SDA
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;

GPIOB->MODER |= GPIO_MODER_MODER6_1 | GPIO_MODER_MODER7_1; // AF
GPIOB->AFR[0] |= 4 << 24 | 4 << 28;  // AF4 = I2C1
GPIOB->OTYPER |= GPIO_OTYPER_OT_6 | GPIO_OTYPER_OT_7; // 开漏
GPIOB->PUPDR |= GPIO_PUPDR_PUPDR6_0 | GPIO_PUPDR_PUPDR7_0; // 上拉

// 配置速率:100kHz标准模式
I2C1->CR2 = 42;  // APB1时钟42MHz
I2C1->CCR = 210; // 42MHz / (2*210) = 100kHz
I2C1->TRISE = 43; // 1000ns / (1/42MHz) + 1
注意: I2C的GPIO必须配成开漏输出!推挽输出会导致总线冲突。我见过有人用推挽输出也能工作,但那是因为上拉电阻够强。一旦挂多个设备,必死无疑。

3.3.2 常见故障排查

现象 可能原因 排查方法
SCL无时钟 时钟未使能/GPIO配置错 检查RCC和MODER
SDA一直低 从机拉死总线/上拉电阻缺失 测SDA电平,加外部上拉
ACK丢失 地址错误/从机未就绪 用逻辑分析仪看地址字节
数据错位 速率不匹配/噪声干扰 降低速率,加滤波电容

3.4 SPI —— 高速但挑剔的通信

SPI比I2C快得多,但线也多——至少4根。而且SPI的模式(CPOL/CPHA)配置错了,数据就全乱。我调试SPI时,一定会先看示波器,确认时钟极性和相位对不对。

3.4.1 全双工主机配置

// SPI1配置:PA5-SCK, PA6-MISO, PA7-MOSI, PA4-NSS
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

GPIOA->MODER |= GPIO_MODER_MODER5_1 | GPIO_MODER_MODER6_1 | GPIO_MODER_MODER7_1;
GPIOA->AFR[0] |= 5 << 20 | 5 << 24 | 5 << 28; // AF5 = SPI1

// 软件NSS
GPIOA->MODER |= GPIO_MODER_MODER4_0; // 推挽输出
GPIOA->BSRR = GPIO_BSRR_BS_4;        // 拉高

// SPI配置:模式0, 8位数据, 1MHz
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSI | SPI_CR1_SSM | (3 << 3);
// 波特率 = 42MHz / 16 = 2.625MHz
SPI1->CR1 |= SPI_CR1_SPE;

3.4.2 收发数据

// 发送一个字节
SPI1->DR = data;
while(!(SPI1->SR & SPI_SR_TXE));
while(!(SPI1->SR & SPI_SR_RXNE));
uint8_t recv = SPI1->DR;
我的经验: SPI的NSS(片选)最好用软件控制。硬件NSS有时候会在多字节传输时自动翻转,导致从机复位。我习惯用普通GPIO手动拉高拉低,可控性更强。

3.5 定时器 —— 嵌入式世界的节拍器

定时器是STM32里最强大的外设之一。从简单的延时到复杂的PWM、输入捕获、编码器接口,全靠它。我个人觉得,用好定时器,你的嵌入式水平就上了一个台阶。

3.5.1 基础定时中断

// TIM3配置:1ms中断
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;

TIM3->PSC = 41999;  // 42MHz / 42000 = 1kHz
TIM3->ARR = 999;    // 1kHz / 1000 = 1Hz
TIM3->DIER |= TIM_DIER_UIE;
TIM3->CR1 |= TIM_CR1_CEN;

NVIC_EnableIRQ(TIM3_IRQn);

这里有个细节:预分频器PSC和自动重装载ARR的关系。PSC决定计数时钟频率,ARR决定计数周期。两者配合,就能得到任意想要的定时时间。

3.5.2 PWM输出

// TIM3通道1 PWM输出:PA6
TIM3->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // PWM模式1
TIM3->CCER |= TIM_CCER_CC1E;  // 使能输出
TIM3->CCR1 = 500;  // 50%占空比
我曾经犯过的错: 配置PWM时忘了把GPIO设为复用功能。结果PA6一直输出高电平,电机全速转。还好只是测试,不然电机就烧了。记住:PWM输出必须配AF模式,不是推挽输出。

3.6 调试方法总结

说了这么多,其实调试外设就三板斧:

  1. 看时钟: 外设时钟开了吗?GPIO时钟开了吗?这是90%问题的根源。
  2. 看配置: 模式对不对?复用功能选对了吗?上下拉、速度、类型都配了吗?
  3. 看波形: 逻辑分析仪或示波器接上去,看实际信号跟预期是否一致。
我的调试口诀: 时钟先开,模式后配;波形验证,问题自退。

嗯,这章内容不少。但别急,这些外设后面每个都会反复用到。尤其是做AI边缘计算时,传感器数据采集(I2C/SPI)、推理结果输出(UART/GPIO)、任务调度(定时器),一个都跑不掉。先把基础打牢,后面才能飞得起来。