第2章:嵌入式系统基础回顾:MCU选型、RTOS任务调度与中断管理

各位同学,咱们直接进入正题。做除颤仪这种医疗设备,MCU选型可不是随便挑个便宜的完事。我当年刚入行时,就吃过选型太激进的亏——芯片算力够了,但外设资源不够,最后不得不改板,那叫一个痛苦。

2.1 MCU选型:STM32与ARM Cortex的实战考量

除颤仪的核心MCU,我个人习惯首选ARM Cortex-M4或M7内核。为什么?因为除颤仪需要同时处理高压充放电控制、心电信号采集、实时波形显示、通信协议栈,还得跑个RTOS。M3有时候真扛不住。

具体到型号,STM32F4或H7系列是常见选择。我列个表,大家对比一下:

需求 STM32F4 (M4) STM32H7 (M7) 我的建议
主频 168-180 MHz 400-480 MHz H7更适合多任务
SRAM 192 KB - 1 MB 1 MB+ 除颤仪波形缓存至少512KB
ADC精度 12位 16位 心电采集建议16位
定时器 14个 22个 高压时序控制需要多个
关键点:除颤仪的高压充放电控制,必须用硬件定时器产生精确PWM,不能用软件延时。我见过有人用delay_us()控制充电时间,结果高压模块直接炸了——嗯,那场面挺吓人的。

选型时还要注意:通信外设数量。除颤仪通常需要:

  • 2路UART(一路接蓝牙/WiFi模块,一路接PC调试)
  • 1路SPI(接SD卡或外部Flash)
  • 1路I2C(接传感器或EEPROM)
  • 1路CAN(如果接医院中央监护系统)

你想想看,如果选个STM32F103,只有3个UART,用起来就捉襟见肘了。我建议至少选F4系列起步。

2.2 FreeRTOS任务调度:别让任务饿死

除颤仪这种设备,任务优先级设计不好,会出人命。真的,不开玩笑。

我一般把任务分成三个等级:

  1. 紧急任务(最高优先级):高压放电控制、心电R波检测。这些任务必须在微秒级响应。
  2. 实时任务(中等优先级):波形数据采集、通信协议处理。毫秒级响应即可。
  3. 后台任务(低优先级):日志记录、自检、界面刷新。几十毫秒甚至秒级都行。
避坑指南:我曾经把通信协议处理放在最高优先级,结果高压放电任务被阻塞,导致放电延迟。后来改成:高压任务用中断直接触发,通信任务用消息队列异步处理。问题就解决了。

FreeRTOS的任务调度,我习惯用抢占式调度。配置如下:

// FreeRTOSConfig.h 关键配置
#define configUSE_PREEMPTION          1   // 抢占式调度
#define configUSE_TIME_SLICING        1   // 时间片轮转
#define configMAX_PRIORITIES          5   // 5个优先级足够

// 任务创建示例
xTaskCreate(vHighVoltageTask, "HV", 256, NULL, 4, NULL);  // 优先级4
xTaskCreate(vCommTask,       "COM", 512, NULL, 2, NULL);  // 优先级2
xTaskCreate(vLogTask,        "LOG", 128, NULL, 1, NULL);  // 优先级1

这里要注意:任务栈大小。通信协议栈因为要处理数据包,栈需求大。我一般给512字节,如果用到printf调试,得给到1024。否则会栈溢出,系统莫名其妙重启。

2.3 中断管理:通信的命脉

在除颤仪通信中,中断是灵魂。为什么?因为通信数据随时可能来,你不能让CPU轮询等待——那会耽误高压控制。

我常用的中断分配方案:

中断源 优先级 处理内容 中断时间要求
定时器中断(高压PWM) 最高(0) 充放电时序控制 < 1μs
UART接收中断 高(1) 接收通信数据包 < 10μs
ADC转换完成中断 中(2) 读取心电数据 < 50μs
外部中断(按键等) 低(3) 用户交互 < 1ms
警告:中断服务函数(ISR)里绝对不能做耗时操作!比如在UART中断里直接解析协议包,那会导致其他中断丢失。正确做法:ISR只做数据搬运,用消息队列或信号量通知任务处理。

举个例子,UART接收中断的正确写法:

// UART中断服务函数 - 只做数据接收
void USART1_IRQHandler(void)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t data;
    
    if(LL_USART_IsActiveFlag_RXNE(USART1))
    {
        data = LL_USART_ReceiveData8(USART1);
        // 把数据放入队列,通知通信任务处理
        xQueueSendFromISR(xCommQueue, &data, &xHigherPriorityTaskWoken);
    }
    
    // 如果通信任务优先级更高,立即切换
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 通信任务 - 在任务上下文中解析协议
void vCommTask(void *pvParameters)
{
    uint8_t rxData;
    while(1)
    {
        if(xQueueReceive(xCommQueue, &rxData, portMAX_DELAY) == pdTRUE)
        {
            // 在这里解析协议包,可以放心使用延时、打印等操作
            ProcessProtocolData(rxData);
        }
    }
}

这种设计的好处是:中断响应极快,不会阻塞其他紧急中断。通信任务在低优先级运行,不影响高压控制。

2.4 中断与RTOS的协同:别让优先级反转搞死你

嗯,这里有个坑。当RTOS任务和中断共享资源时,会出现优先级反转。比如:

  • 低优先级的日志任务占用了互斥锁
  • 高优先级的通信任务想用同一个锁,被阻塞
  • 中优先级的界面刷新任务趁机运行,导致高优先级任务迟迟得不到锁

我建议的做法:中断和任务之间尽量用队列通信,不要用互斥锁。队列是FreeRTOS里最安全的中断-任务通信方式。

我的经验:在除颤仪项目中,我所有中断都只做三件事:读数据、写队列、清标志。任何协议解析、数据处理都放到任务里。这样即使通信协议栈再复杂,也不会影响高压控制的实时性。

最后总结一下:MCU选型要留余量,RTOS任务要分优先级,中断处理要短平快。这三条做好了,除颤仪的通信协议栈才能稳定运行。下一章我们开始讲具体的协议设计——嗯,那才是真正有意思的部分。