2. 嵌入式基础回顾

在进入FreeRTOS多任务控制系统的学习之前,我们必须先夯实嵌入式开发的核心地基。机器人控制本质上是对“时间”和“事件”的精确管理:何时读取传感器、何时驱动电机、何时处理通信数据。STM32与Arduino作为当前机器人教育与应用中最主流的两大平台,分别代表了“寄存器级精细控制”与“快速原型验证”两种开发范式。本章将系统梳理GPIO、定时器、中断、串口通信四大核心外设的工作原理与工程实践方法,并建立它们与RTOS任务调度之间的映射关系——这是后续理解信号量、队列、软件定时器等RTOS机制的关键前提。

STM32 / Arduino MCU Cortex-M3/M4 / AVR NVIC 中断控制器 SysTick / TIMx 定时器 GPIO / USART / I2C / SPI 传感器输入 超声波 / 红外 / IMU 编码器 / 摄像头 GPIO输入 / ADC / 外部中断 执行器输出 直流电机 / 舵机 / 步进 LED / 蜂鸣器 / 电磁阀 PWM / GPIO输出 / 定时器 上位机/外设通信 蓝牙 / Wi-Fi / USB ROS / 手机APP / 调试器 USART / SPI / I2C / CAN 电源与调试 电池 / 稳压 / 保护 SWD / JTAG / 串口打印 电源域 / 时钟 / 复位 FreeRTOS 多任务调度层(任务 / 队列 / 信号量 / 软件定时器) 硬件抽象接口

上图展示了机器人控制系统中MCU与外围设备的典型连接关系。左侧为传感器输入通道,右侧为执行器与通信通道,底部则是FreeRTOS将要承载的多任务调度层。理解这张图的关键在于:所有外设的底层操作最终都归结为对寄存器或库函数的调用,而RTOS的作用是让这些外设操作在时间维度上高效协同,避免CPU空转等待。

2.1 嵌入式平台选型:STM32与Arduino的工程定位

核心原理:STM32基于ARM Cortex-M内核,提供丰富的外设资源(如多路定时器、DMA、ADC、CAN等),适合需要精确时序控制、多传感器融合和复杂算法的机器人项目。Arduino则基于AVR或ARM(如Due、Zero),通过封装好的Wiring库大幅降低开发门槛,适合快速验证控制逻辑和教学演示。两者的本质差异在于:STM32需要开发者关注时钟树配置、引脚复用功能(AFIO)和中断优先级分组,而Arduino将这些细节封装为pinMode()、digitalWrite()等简单API。

对比维度STM32(以F103/F407为例)Arduino(以Uno/Mega为例)
主频与算力72MHz~168MHz,Cortex-M3/M416MHz,AVR 8位
GPIO配置需配置时钟、模式、速度、上下拉pinMode()一行完成
定时器资源8~14个,支持PWM、编码器、输入捕获3~6个,功能相对简单
中断管理NVIC嵌套向量中断,支持抢占优先级有限的外部中断引脚,优先级固定
串口数量3~6路USART,支持DMA1~4路硬件串口
开发效率需配置CubeMX或手写寄存器即插即用,适合原型验证
典型应用平衡车、机械臂、无人机飞控教学小车、简单避障、创客作品
选型建议:如果课程项目需要运行FreeRTOS并管理5个以上任务(如电机控制+传感器融合+通信+UI),建议选择STM32F103或F407系列。Arduino Uno的2KB SRAM在运行RTOS时会非常紧张,仅适合学习RTOS基础概念。

2.2 GPIO控制:从LED闪烁到电机方向切换

核心原理:GPIO(General Purpose Input/Output)是MCU与外部世界交互的最基本通道。每个GPIO引脚内部包含输入缓冲器、输出驱动器和可配置的上下拉电阻。在STM32中,GPIO必须挂载在APB2总线(如GPIOA~GPIOD)上,使用前需开启对应时钟。输出模式包括推挽(Push-Pull)和开漏(Open-Drain),输入模式包括浮空、上拉、下拉和模拟输入。对于机器人控制,推挽输出用于驱动LED、蜂鸣器、电机方向引脚;开漏输出用于I2C总线或电平转换场景。

操作流程(以STM32 HAL库为例):

  1. 使能GPIO端口时钟:__HAL_RCC_GPIOA_CLK_ENABLE();
  2. 初始化GPIO引脚:配置GPIO_InitTypeDef结构体,设置Pin、Mode、Pull、Speed。
  3. 控制输出电平:HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
  4. 读取输入电平:uint8_t val = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);

案例拆解:假设我们要控制一个双路电机驱动模块(如L298N),需要两个GPIO引脚(IN1、IN2)决定电机正转、反转或停止。在Arduino中,代码为digitalWrite(IN1, HIGH); digitalWrite(IN2, LOW);。但在STM32中,我们还需要考虑引脚复用冲突——例如PA9和PA10默认是USART1的TX/RX,如果用作GPIO必须禁用复用功能。实际调试中,常见故障是引脚配置为复用模式却未正确调用HAL_GPIO_Init(),导致电平无法翻转。

常见误区:不要将3.3V的STM32 GPIO直接驱动5V供电的电机驱动芯片逻辑端,除非驱动芯片标注兼容3.3V输入。否则需要电平转换电路(如MOSFET或专用芯片)。另外,GPIO驱动能力有限(STM32约±8mA),不能直接驱动继电器或大功率LED,必须通过三极管或MOSFET放大。

2.3 定时器:PWM调速与时间基准

核心原理:定时器是机器人控制中最重要的外设之一。STM32的定时器分为高级定时器(TIM1/TIM8)、通用定时器(TIM2~TIM5)和基本定时器(TIM6/TIM7)。它们基于16位或32位计数器,可产生PWM信号、测量脉冲宽度(输入捕获)、输出比较中断等。PWM调速的本质是固定频率下改变占空比,从而改变电机端电压的平均值。例如,10kHz的PWM频率对直流电机是合适的,既能避免 audible noise,又能保证电流连续性。

关键参数计算:假设STM32F103系统时钟72MHz,我们希望产生20kHz的PWM。设置定时器预分频器(PSC)为71(即72分频),则计数时钟为1MHz。若自动重装载值(ARR)为49,则PWM频率 = 1MHz / (49+1) = 20kHz。占空比由比较寄存器(CCR)控制,CCR=25时占空比为50%。

// STM32 HAL库 PWM初始化示例(TIM2, Channel1, PA0)
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;         // 72MHz/72 = 1MHz
htim2.Init.Period = 49;            // 20kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);
TIM_OC_InitTypeDef sConfigOC;
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 25;              // 50%占空比
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

实战场景:在轮式机器人中,我们通常使用定时器输出两路PWM分别控制左右电机,同时利用定时器的编码器模式(如TIM4的CH1/CH2)读取正交编码器,实现速度闭环。Arduino的analogWrite()虽然也能输出PWM,但其频率固定约490Hz(D3/D5/D6/D9/D10/D11),对于某些低电感电机可能产生啸叫。因此,在FreeRTOS课程中,我们更推荐使用STM32的定时器资源,因为RTOS的时基(SysTick)与PWM定时器是独立的,不会互相干扰。

经验提醒:在RTOS环境中,PWM占空比的更新应放在定时器更新中断或DMA回调中,而不是在任务中直接修改CCR值。因为任务可能被更高优先级任务抢占,导致PWM波形出现毛刺。正确做法是使用HAL_TIM_PWM_Start_DMA()或利用定时器更新事件触发中断,在中断中更新比较值。

2.4 中断系统:事件驱动的核心机制

核心原理:中断允许MCU在等待外部事件(如按键按下、传感器跳变、串口接收完成)时暂停当前任务,跳转到中断服务函数(ISR)处理事件,然后返回。STM32的NVIC支持多达16级可编程优先级(4位抢占优先级+4位子优先级)。在FreeRTOS中,中断优先级与RTOS的临界区保护密切相关:通常将FreeRTOS管理的最高中断优先级设置为5~15(数值越小优先级越高),低于5的中断不会被RTOS屏蔽,从而保证实时性。

中断设计原则:

  • ISR中只做标记或快速处理,避免耗时操作(如浮点运算、打印)。
  • 使用portYIELD_FROM_ISR()在中断中触发任务切换。
  • 区分外部中断(EXTI)和内部中断(定时器、串口)。
  • 在Arduino中,attachInterrupt()只能用于特定引脚(如Uno的D2/D3),且ISR中不能使用delay()。

案例拆解:假设机器人前方安装了一个光电门传感器,用于检测码盘转速。传感器输出脉冲信号,我们希望每检测到一个脉冲就计数一次。在STM32中,将传感器输出连接到PA0(EXTI0),配置上升沿触发。ISR中仅执行counter++操作。主循环中每100ms读取一次counter值并清零,即可计算转速。若使用FreeRTOS,我们可以创建一个二值信号量,在ISR中释放信号量,而转速计算任务阻塞等待信号量——这样既保证了实时性,又避免了轮询浪费CPU。

风险说明:中断优先级设置不当会导致RTOS任务调度异常。例如,若将定时器中断优先级设为0(最高),则FreeRTOS的临界区无法屏蔽它,可能导致共享数据被破坏。务必在FreeRTOSConfig.h中设置configMAX_SYSCALL_INTERRUPT_PRIORITY为5,并将所有调用FreeRTOS API的中断优先级设为5或更低(数值更大)。

2.5 串口通信:调试与数据交换的生命线

核心原理:串口(USART/UART)是机器人开发中最常用的通信接口,用于与PC调试助手、蓝牙模块、GPS模块等设备交换数据。其本质是异步串行通信,双方需约定相同的波特率(如115200bps)、数据位(8)、停止位(1)和校验位(无)。STM32的USART支持DMA传输和中断接收,这在与RTOS结合时尤为重要——我们可以将串口接收放在中断中,通过队列将数据传递给处理任务,避免任务阻塞在轮询接收上。

操作流程(中断接收模式):

  1. 初始化USART1,配置波特率、字长、停止位,并使能接收中断。
  2. 编写中断回调函数HAL_UART_RxCpltCallback(),在函数中将收到的字节放入FreeRTOS队列。
  3. 创建串口处理任务,阻塞等待队列消息,解析指令(如“M100”表示电机转速100)。
// 串口接收中断回调(HAL库)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        xQueueSendFromISR(xUartQueue, &rxByte, &xHigherPriorityTaskWoken);
        HAL_UART_Receive_IT(&huart1, &rxByte, 1); // 重新使能接收
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

常见误区:初学者常犯的错误是在主循环中使用HAL_UART_Receive()阻塞接收,这会导致CPU空转等待数据,无法执行其他任务。在RTOS环境中,必须使用中断+DMA或中断+队列的方式。另外,串口波特率误差超过2%会导致乱码,尤其是使用内部RC振荡器时,建议使用外部晶振(如8MHz)并通过PLL倍频到72MHz。

通信方式适用场景RTOS中的推荐用法
轮询(Polling)调试阶段、低速数据不推荐,浪费CPU
中断接收不定长数据、指令响应配合队列或信号量
DMA接收大数据量、高速传输配合空闲中断(IDLE)
DMA+空闲中断不定长帧接收最佳实践,CPU零负担
总结复盘:本章回顾的四大外设是FreeRTOS课程的基础。GPIO是“手”,定时器是“心跳”,中断是“神经反射”,串口是“感官和语言”。在后续章节中,我们将看到如何用FreeRTOS的任务管理来替代传统的裸机轮询,用队列来安全传递串口数据,用信号量来同步中断与任务。请务必在开发板上亲手实践本节内容——尤其是用STM32CubeMX生成一个包含GPIO、定时器PWM、外部中断和串口中断的基础工程,这将为后续的RTOS移植和任务设计节省大量时间。