2. STM32基础外设回顾:GPIO、UART、定时器、中断系统

说实话,很多做FreeRTOS移植的朋友,最后栽跟头的地方往往不是操作系统本身,而是基础外设没配置明白。我见过太多人花一周调一个UART中断,结果发现是GPIO复用功能没开。所以这一章,咱们把STM32最常用的四个外设模块再过一遍。重点不是讲数据手册,而是讲「移植FreeRTOS时你一定会碰到的那些坑」。

本章核心脉络:GPIO是肌肉,UART是嘴巴,定时器是心跳,中断系统是神经。四者配合,才能让FreeRTOS跑得稳、跑得准。

STM32基础外设知识体系 FreeRTOS移植基础 GPIO UART 定时器 中断系统 推挽/开漏 上下拉 波特率配置 中断接收 PWM输出 输入捕获 NVIC优先级 💡 移植FreeRTOS时,这四个外设的配置顺序和优先级分组直接影响系统稳定性 GPIO→UART→定时器→中断系统,建议按这个顺序逐一验证后再加载RTOS内核

2.1 GPIO — 最基础也最容易翻车

GPIO看起来简单,不就是配置输入输出吗?但我在Bootloader项目里吃过一次亏:升级完成后,GPIO引脚电平没恢复,导致新固件启动时外设误动作。你想想看,一个引脚在Bootloader里是推挽输出高电平,切到App后变成开漏输入,中间那几百微秒会发生什么?

配置GPIO时,我建议你养成三个习惯:

  • 先查复用功能表 — 别想当然。UART的TX/RX、定时器的PWM通道,每个引脚都有固定的AF编号。我见过有人把USART1的TX配到PA9,结果AF号写成了AF7(应该是AF1),调了三天。
  • 输出模式选推挽还是开漏 — 推挽适合驱动LED、普通IO;开漏用于I2C、电平转换场景。选错了,波形就变形。
  • 上下拉电阻别乱设 — 外部有上拉时,内部就别再开了,否则分压后电平可能不达标。
// 我个人习惯的GPIO初始化模板
void GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOA_CLK_ENABLE();   // 先开时钟,这是最容易忘的
    
    GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;         // 复用推挽
    GPIO_InitStruct.Pull = GPIO_PULLUP;             // 内部上拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;   // 高速模式
    GPIO_InitStruct.Alternate = GPIO_AF1_USART1;    // 复用功能号
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

⚠️ 我曾经踩过的坑:在Bootloader里初始化了GPIO,跳转到App前没有恢复默认状态。结果App的GPIO配置和Bootloader冲突,外设直接罢工。解决办法是在跳转前调用HAL_GPIO_DeInit()把所有用过的引脚复位。

2.2 UART — 调试和升级的生命线

UART在FreeRTOS项目里有两个角色:一是打印调试信息,二是承载Ymodem/Xmodem等升级协议。说白了,它就是你和单片机之间的电话线。

配置UART时,我重点关注三个参数:

参数 推荐值 说明
波特率 115200 / 921600 Bootloader升级建议用115200,兼容性好;调试用921600更快
数据位 8 标准设置,别改
停止位 1 够用,2位浪费带宽
校验位 无 升级协议自带CRC校验,UART层不需要重复校验

中断接收是UART的核心。FreeRTOS里,我习惯用中断方式接收不定长数据,配合一个环形缓冲区。为什么不用DMA?因为DMA在不定长接收时需要超时判断,逻辑复杂,而且中断方式在115200波特率下完全够用。

// UART中断接收的典型写法
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        // 把收到的字节放入环形缓冲区
        RingBuf_Write(&uart_rx_buf, rx_byte);
        // 重新启动中断接收
        HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
    }
}

💡 小技巧:在Bootloader里,我建议把UART中断优先级设成比FreeRTOS系统节拍中断低一级。这样既不会丢失数据,又不会影响系统调度。具体怎么配,下一节讲中断系统时会细说。

2.3 定时器 — 系统的心跳源

FreeRTOS需要一个硬件定时器来产生系统节拍(SysTick)。STM32的SysTick是内核自带的24位递减计数器,专门给操作系统用的。但除了SysTick,通用定时器(TIMx)在Bootloader里也有大用处。

我常用的定时器场景:

  • SysTick:给FreeRTOS提供1ms的时基。配置很简单,但要注意中断优先级必须设为最低(FreeRTOS要求)。
  • TIM2/3/4:做PWM输出,驱动蜂鸣器或LED呼吸效果,提示升级进度。
  • TIM5/6:做输入捕获,测量外部脉冲宽度,比如检测升级按键的长按/短按。

配置SysTick时,我见过一个经典错误:在FreeRTOS启动前调用了HAL_Delay()。HAL_Delay依赖SysTick中断,而FreeRTOS接管SysTick后,中断处理函数变了,HAL_Delay就失效了。解决办法是:在vTaskStartScheduler()之前,不要用任何依赖SysTick的HAL函数。

// FreeRTOS配置中SysTick的优先级设置
// 在FreeRTOSConfig.h中
#define configKERNEL_INTERRUPT_PRIORITY      255   // 最低优先级
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 191   // 可安全调用的最高优先级

关键点:定时器的预分频器和自动重装载值决定了中断频率。公式很简单:频率 = 时钟源 / (预分频器+1) / (自动重装载值+1)。我建议你把这个公式贴在显示器上,调定时器时随时看。

2.4 中断系统 — 优先级决定一切

中断系统是STM32的神经中枢,也是FreeRTOS移植中最容易出问题的地方。说白了,中断优先级配不好,系统要么死锁,要么响应慢得像蜗牛。

STM32的中断优先级分为抢占优先级和子优先级,通过NVIC的4位优先级寄存器来配置。这4位可以拆分成不同的组合:

分组 抢占优先级位数 子优先级位数 适用场景
NVIC_PriorityGroup_0 0 4 所有中断同级,按子优先级排队
NVIC_PriorityGroup_2 2 2 常用,4级抢占,每级4个子优先级
NVIC_PriorityGroup_4 4 0 FreeRTOS推荐,16级抢占,无子优先级

我个人习惯用NVIC_PriorityGroup_4,也就是16级抢占优先级。为什么?因为FreeRTOS的临界区保护和中断屏蔽逻辑都是基于抢占优先级工作的。用分组4,每个中断的优先级就是唯一的,不会出现子优先级嵌套的混乱情况。

中断优先级分配原则(我自己的经验):

  1. SysTick中断:优先级最低(15),因为FreeRTOS的调度器不能打断其他关键中断。
  2. UART接收中断:优先级中等(7-10),保证数据不丢失,但又不至于阻塞系统。
  3. 定时器中断:看用途。PWM不需要中断,输入捕获需要高优先级(3-5)。
  4. 外部中断(EXTI):紧急事件用最高优先级(0-2),比如掉电检测、紧急停止。

⚠️ 我曾经踩过的坑:把UART中断优先级设得比SysTick还高。结果UART频繁中断,导致SysTick节拍不准,FreeRTOS的vTaskDelay直接乱套。记住:SysTick永远是优先级最低的那个。

2.5 四个外设的联动配置清单

在开始移植FreeRTOS之前,我建议你按这个顺序逐一验证每个外设:

  1. GPIO:点个LED,确认输出正常。再读个按键,确认输入正常。
  2. UART:用串口助手发一个字节,看中断接收是否触发。再发一串数据,看环形缓冲区是否溢出。
  3. 定时器:配置SysTick产生1ms中断,在中断里翻转一个GPIO,用示波器看波形是否准确。
  4. 中断系统:同时触发UART中断和定时器中断,观察优先级是否按预期执行。

这四个步骤走完,你的STM32基础外设就算调通了。接下来加载FreeRTOS内核时,你才能确定:出问题的是操作系统,而不是外设配置。

💡 最后说一句:别急着上RTOS。先把GPIO、UART、定时器、中断这四个模块单独跑通,每个都写一个测试函数。磨刀不误砍柴工,这个习惯帮我省了至少一半的调试时间。