嵌入式系统基础:ARM Cortex-M系列与RISC-V架构对比、RTOS任务调度原理、内存管理

各位同学,欢迎来到第二章。这一章咱们聊聊嵌入式系统的基础。说实话,这部分内容看着有点枯燥,但它是你后面做算法部署的根基。我见过不少同学,模型在PC上跑得飞起,一上嵌入式板子就各种翻车,十有八九是这里没搞透。

咱们今天重点聊三个事:ARM Cortex-M和RISC-V到底怎么选、FreeRTOS这个实时系统是怎么调度任务的、以及内存管理那些坑。嗯,开始吧。

2.1 ARM Cortex-M系列与RISC-V架构对比

先说说这两个架构。ARM Cortex-M系列,说白了就是嵌入式领域的“老大哥”。你随便拆一个医疗贴片,里面大概率是Cortex-M0、M3或者M4。我最早做心电贴片时,用的就是STM32F4,Cortex-M4内核,带FPU,做FFT和滤波非常顺手。

RISC-V呢?它是后起之秀,开源、免费、可定制。这几年在IoT和AI边缘端越来越火。我个人习惯是:如果项目对成本敏感、需要定制指令集,我会优先考虑RISC-V;如果追求生态成熟、开发效率高,ARM还是首选。

核心区别一句话:ARM是“买授权,用生态”;RISC-V是“开源架构,自己玩”。

咱们用表格对比一下,更直观:

对比项 ARM Cortex-M系列 RISC-V
指令集架构 专有架构,需授权 开源架构,免费
生态成熟度 非常成熟,工具链完善 快速发展中,部分工具尚不完善
功耗 低功耗设计成熟 可定制,理论上更低
AI加速 M4/M7带DSP+FPU,M55有Helium 可扩展向量指令,灵活性高
典型芯片 STM32、NXP LPC、TI Tiva GD32V、SiFive、Kendryte K210
医疗应用场景 心电、血氧、血糖贴片 AI推理、定制化传感器处理

我在项目中遇到过一件事:有个同事用RISC-V的K210做图像预处理,因为可以自定义指令,把卷积运算加速了3倍。但后来换了个供应商,工具链不兼容,折腾了两周。所以我的建议是:如果团队小、工期紧,选ARM;如果你们有底层能力、想极致优化,RISC-V值得一试。

2.2 RTOS(FreeRTOS)任务调度原理

好,接下来聊RTOS。你想想看,一个医疗贴片要同时做数据采集、滤波、算法推理、蓝牙传输,如果没有操作系统,你写个超级循环,迟早会出问题。FreeRTOS就是来解决这个的。

它的核心是任务调度。说白了,就是CPU在多个任务之间快速切换,让你感觉它们在“同时运行”。FreeRTOS支持三种调度方式:

  • 抢占式调度:高优先级任务随时可以打断低优先级任务。这是最常用的方式。
  • 协作式调度:任务主动让出CPU,适合简单系统。
  • 时间片轮转:同优先级任务轮流执行,每个任务分到固定时间片。

我个人习惯在医疗贴片里用抢占式调度。为什么?因为数据采集任务必须实时响应,不能等。比如心电信号的采样率是250Hz,每4ms就要读一次ADC,这个任务优先级必须最高。

小技巧:在FreeRTOS中,任务优先级不要设太多,3-5级就够了。优先级太多反而容易出问题,我曾经在一个项目里设了8级,结果调试时自己都搞混了。

来看一个简单的任务创建示例:

// 创建数据采集任务,优先级最高
xTaskCreate(
    vDataAcquisitionTask,   // 任务函数
    "DataAcq",              // 任务名称
    256,                    // 栈深度(单位:字)
    NULL,                   // 参数
    3,                      // 优先级(0最低,3最高)
    NULL                    // 任务句柄
);

// 创建算法处理任务,优先级中等
xTaskCreate(
    vAlgorithmTask,
    "Algo",
    512,                    // 算法任务需要更多栈空间
    NULL,
    2,
    NULL
);

// 创建通信任务,优先级最低
xTaskCreate(
    vCommTask,
    "Comm",
    256,
    NULL,
    1,
    NULL
);

// 启动调度器
vTaskStartScheduler();

这里要注意:栈深度一定要算好。我曾经在血氧贴片项目里,算法任务栈给少了,结果跑着跑着就栈溢出,系统随机重启。排查了三天才发现是栈不够。后来我习惯在调试阶段把栈深度设大一点,稳定后再优化。

2.3 内存管理:堆栈分配

内存管理,嗯,这是嵌入式开发里最容易翻车的地方。咱们先搞清楚两个概念:堆和栈。

  • 栈(Stack):由编译器自动管理,存放局部变量、函数调用信息。每个任务有自己的栈。
  • 堆(Heap):由程序员手动管理,用于动态内存分配(malloc/free)。

在FreeRTOS中,内存管理有几种方案。我常用的是heap_4.c,它支持动态分配和释放,而且不会产生碎片。为什么?因为它用了“首次适应算法”加“合并空闲块”。

警告:在医疗贴片这种实时性要求高的场景,尽量不要在中断服务函数里做动态内存分配。我曾经踩过这个坑——中断里malloc,结果分配时间不确定,导致采样周期抖动,心电波形都变形了。

来看一个内存分配的示例:

// 在FreeRTOS中,建议使用pvPortMalloc代替标准malloc
uint8_t *buffer = (uint8_t *)pvPortMalloc(1024);
if (buffer != NULL) {
    // 使用buffer
    // ...
    // 使用完后释放
    vPortFree(buffer);
} else {
    // 内存分配失败处理
    // 我习惯在这里打一个日志,或者点亮一个LED告警
}

关于栈分配,我有个经验:每个任务的栈大小,先按实际需求的2倍来设。比如你算出来需要200字节,先设400。等系统稳定后,通过uxTaskGetStackHighWaterMark()查看实际使用峰值,再逐步优化。

避坑指南:我曾经在血糖贴片项目里,为了省内存,把每个任务的栈压到极限。结果产品在低温环境下,栈溢出导致系统死机。后来发现低温时局部变量会多占一些栈空间。从那以后,我至少留30%的余量。

最后,给一个内存管理的检查清单:

  • ✅ 每个任务栈大小是否留有裕量?
  • ✅ 中断服务函数里有没有动态内存分配?
  • ✅ 堆大小是否满足所有任务的最大需求?
  • ✅ 有没有使用内存池来管理固定大小的数据块?
  • ✅ 调试阶段是否开启了栈溢出检测?

好了,这一章的内容就到这里。总结一下:ARM和RISC-V各有千秋,选型要看项目需求;FreeRTOS的抢占式调度适合医疗贴片这种实时场景;内存管理要留余量,别为了省那几百字节给自己挖坑。

下一章咱们开始讲算法部署,到时候会用到这些基础知识。嗯,先消化消化,有问题随时问。