2. 嵌入式系统基础:MCU选型策略、RTOS选型与硬件抽象层设计
各位同学,咱们直接进入正题。这一章是地基中的地基。你想想看,智能电表这东西,一装就是十年八年不出问题,MCU选错了,后面全白干。RTOS选错了,任务调度能把你逼疯。硬件抽象层没设计好,换个芯片平台等于重新写一遍固件——这种事我见过太多了。
2.1 MCU选型策略:ARM Cortex-M vs RISC-V
先说MCU。智能电表对MCU的要求其实很明确:低功耗、高可靠性、足够的计算能力、丰富的外设接口(SPI、I2C、UART、ADC这些一个不能少)。目前主流就是ARM Cortex-M系列,但RISC-V这两年势头很猛。
ARM Cortex-M:成熟稳重的老大哥
我个人习惯,项目周期紧、团队经验一般的时候,首选Cortex-M。为什么?生态太成熟了。你随便找个Cortex-M0+或者M4,Keil/IAR/STM32CubeIDE全支持,HAL库、LL库随便用,遇到问题Stack Overflow上一搜一大把。
我在项目中遇到过一件事:有个电表项目用了Cortex-M3,量产到第三年,发现有个极端工况下ADC采样会丢数据。查了三天,最后发现是NVIC中断优先级配置有个坑——嗯,这个坑在ARM官方文档里其实写了,但很少有人注意到。好在社区有人踩过,我们照着补丁改了一版就解决了。这就是生态的价值。
- Cortex-M0+:适合超低功耗、成本敏感的电表(比如单相表)
- Cortex-M3/M4:适合需要复杂计算的三相表、关口表
- Cortex-M7:除非你要做边缘计算,否则一般用不上
RISC-V:后起之秀,但别急着冲
RISC-V这两年确实火。开源、免费、可定制——这三个词对硬件工程师来说太诱人了。但说实话,我在智能电表项目里目前还不敢全面铺开RISC-V。
为什么?说白了,工具链还不够成熟。你想想看,电表固件里经常要用到硬件加密、CRC校验、温度补偿这些功能。ARM那边有CMSIS-DSP、mbedTLS直接调。RISC-V这边呢?很多还得自己写,或者等第三方库适配。
不过,如果你做的是定制化SoC,RISC-V就有优势了。我曾经参与过一个项目,客户要求电表里集成一个专用的谐波分析加速器。ARM那边授权费贵,而且改不了指令集。RISC-V就不一样了,我们自己扩展了几条自定义指令,性能直接翻倍。
| 对比项 | ARM Cortex-M | RISC-V |
|---|---|---|
| 生态成熟度 | ★★★★★ | ★★★☆☆ |
| 工具链支持 | Keil/IAR/GCC全支持 | GCC为主,IDE较少 |
| 定制灵活性 | 低(需授权) | 高(开源可扩展) |
| 功耗表现 | 优秀(M0+低至1μA) | 取决于实现 |
| 适合场景 | 通用电表、快速量产 | 定制SoC、高端电表 |
2.2 RTOS选型:FreeRTOS vs RT-Thread
MCU定下来,接下来就是RTOS。智能电表对RTOS的要求:实时性要够(任务响应在毫秒级就行,不用微秒级)、内存占用要小(Flash 64KB、RAM 8KB也能跑)、稳定性要强(不能死机、不能内存泄漏)。
FreeRTOS:轻量级王者
FreeRTOS我用了快十年了。它的优点就一个字:稳。代码量极小,核心只有三个文件,你甚至能把整个RTOS源码看完。这对做电表这种高可靠性产品来说太重要了——你想想看,电表固件里跑着计量、通信、显示、存储好几个任务,任何一个任务出问题都可能导致计量数据出错。
我记得有一次,一个电表项目在高温老化测试时频繁重启。排查到最后,发现是FreeRTOS的堆栈溢出检测没开。其实FreeRTOS自带configCHECK_FOR_STACK_OVERFLOW这个宏,但默认是关闭的。我建议你在项目初期就把它打开,别等到出问题了再查。
// FreeRTOSConfig.h 中的关键配置
#define configUSE_PREEMPTION 1
#define configUSE_IDLE_HOOK 0
#define configUSE_TICK_HOOK 0
#define configCPU_CLOCK_HZ ( ( unsigned long ) 80000000 )
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
#define configMINIMAL_STACK_SIZE ( ( unsigned short ) 128 )
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 16 * 1024 ) )
#define configCHECK_FOR_STACK_OVERFLOW 2 // 建议开启
RT-Thread:国产之光,但别贪多
RT-Thread这几年在国内智能电表领域用得越来越多。它的优势是组件丰富:文件系统、网络协议栈、设备驱动框架全都有。如果你做的是带Wi-Fi/4G通信的智能电表,RT-Thread能省不少事。
但我要提醒你一点:RT-Thread的组件虽然多,但每个组件都会增加Flash和RAM占用。我曾经接手过一个项目,同事把RT-Thread的几乎所有组件都使能了,结果Flash用了200多KB,RAM用了50多KB。对于一颗Cortex-M0来说,这基本把资源吃光了。
| 对比项 | FreeRTOS | RT-Thread |
|---|---|---|
| 内核大小 | 6-12KB | 10-20KB(裁剪后) |
| 组件丰富度 | 低(需自己移植) | 高(内置大量组件) |
| 学习曲线 | 平缓 | 中等 |
| 商业许可 | 免费(需保留版权) | 开源(Apache 2.0) |
| 适合场景 | 资源受限、高可靠 | 功能复杂、通信需求多 |
2.3 硬件抽象层设计:别让硬件绑架你的代码
这一节是重点中的重点。硬件抽象层(HAL)设计得好不好,直接决定了你的固件能活多久。我见过太多项目,MCU一换,代码重写80%。为什么?因为业务逻辑和硬件操作全混在一起了。
HAL的核心思想:分层隔离
说白了,HAL就是在硬件寄存器和业务逻辑之间加一层「翻译官」。业务逻辑只管调用HAL_ADC_Read(),至于这个函数背后是操作STM32的ADC寄存器,还是GD32的ADC寄存器,业务逻辑不关心。
我在项目中习惯这样分层:
- 硬件寄存器层:直接操作MCU寄存器,最底层
- HAL接口层:封装成统一的API,比如
meter_adc_init()、meter_adc_read() - 业务逻辑层:调用HAL接口,实现计量、通信、显示等功能
// 硬件抽象层接口示例(meter_hal.h)
#ifndef __METER_HAL_H__
#define __METER_HAL_H__
#include <stdint.h>
// ADC接口
void meter_adc_init(void);
uint32_t meter_adc_read(uint8_t channel);
// SPI接口
void meter_spi_init(uint32_t baudrate);
int32_t meter_spi_transfer(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len);
// 定时器接口
void meter_timer_init(uint32_t period_us);
void meter_timer_start(void);
void meter_timer_stop(void);
// 看门狗接口
void meter_wdt_init(uint32_t timeout_ms);
void meter_wdt_feed(void);
#endif
一个血的教训:别在HAL里做业务判断
我曾经犯过一个错误:在HAL的ADC读取函数里,直接判断了电压值是否过压,然后置了一个标志位。结果后来换了一颗MCU,ADC精度变了,过压阈值也要改。我不得不去修改HAL层的代码——这完全违背了HAL的设计初衷。
正确的做法是:HAL只负责「读数据」,业务逻辑负责「判断数据」。HAL返回原始值,业务层去做阈值比较、滤波、校准。
- 原则一: HAL接口参数用标准类型(uint32_t、int16_t),不要用MCU特有的类型
- 原则二: HAL函数不要有阻塞等待(除非是初始化),用回调或状态机
- 原则三: HAL层不要包含任何业务逻辑,只做硬件操作和状态返回
移植性验证:换颗MCU试试
HAL设计得好不好,有个简单的验证方法:换一颗不同厂家的MCU,看看要改多少代码。如果只需要重新实现meter_hal.c,而meter_app.c一行不动,那你的HAL就是合格的。
我建议你在项目初期就做一次移植性验证。比如先用STM32开发,然后花两天时间移植到GD32或者AT32上。这个过程能帮你发现HAL接口设计中的问题——比如某个函数参数依赖了特定MCU的位宽,或者某个初始化顺序在不同MCU上不一样。
meter_hal_version()函数,返回HAL版本号和MCU型号。这样调试时一眼就能知道当前跑在哪个平台上,避免搞混。
好了,这一章的内容就到这里。MCU选型、RTOS选型、HAL设计,这三件事做好了,你的智能电表固件架构就稳了一半。下一章我们聊聊任务划分和状态机设计——那才是真正让固件「活起来」的关键。