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就不一样了,我们自己扩展了几条自定义指令,性能直接翻倍。

我的建议: 如果团队有硬件能力,且产品量级在百万级以上,可以考虑RISC-V定制。否则,老老实实用Cortex-M,省心。
对比项 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来说,这基本把资源吃光了。

避坑指南: 用RT-Thread时,一定要做组件裁剪。只保留你真正需要的:内核、FinSH控制台(调试用)、设备驱动框架。其他像POSIX接口、C++支持这些,能关就关。
对比项 FreeRTOS RT-Thread
内核大小 6-12KB 10-20KB(裁剪后)
组件丰富度 低(需自己移植) 高(内置大量组件)
学习曲线 平缓 中等
商业许可 免费(需保留版权) 开源(Apache 2.0)
适合场景 资源受限、高可靠 功能复杂、通信需求多

2.3 硬件抽象层设计:别让硬件绑架你的代码

这一节是重点中的重点。硬件抽象层(HAL)设计得好不好,直接决定了你的固件能活多久。我见过太多项目,MCU一换,代码重写80%。为什么?因为业务逻辑和硬件操作全混在一起了。

HAL的核心思想:分层隔离

说白了,HAL就是在硬件寄存器和业务逻辑之间加一层「翻译官」。业务逻辑只管调用HAL_ADC_Read(),至于这个函数背后是操作STM32的ADC寄存器,还是GD32的ADC寄存器,业务逻辑不关心。

我在项目中习惯这样分层:

  1. 硬件寄存器层:直接操作MCU寄存器,最底层
  2. HAL接口层:封装成统一的API,比如meter_adc_init()meter_adc_read()
  3. 业务逻辑层:调用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设计三原则:
  • 原则一: HAL接口参数用标准类型(uint32_t、int16_t),不要用MCU特有的类型
  • 原则二: HAL函数不要有阻塞等待(除非是初始化),用回调或状态机
  • 原则三: HAL层不要包含任何业务逻辑,只做硬件操作和状态返回

移植性验证:换颗MCU试试

HAL设计得好不好,有个简单的验证方法:换一颗不同厂家的MCU,看看要改多少代码。如果只需要重新实现meter_hal.c,而meter_app.c一行不动,那你的HAL就是合格的。

我建议你在项目初期就做一次移植性验证。比如先用STM32开发,然后花两天时间移植到GD32或者AT32上。这个过程能帮你发现HAL接口设计中的问题——比如某个函数参数依赖了特定MCU的位宽,或者某个初始化顺序在不同MCU上不一样。

小技巧: 在HAL接口里加一个meter_hal_version()函数,返回HAL版本号和MCU型号。这样调试时一眼就能知道当前跑在哪个平台上,避免搞混。

好了,这一章的内容就到这里。MCU选型、RTOS选型、HAL设计,这三件事做好了,你的智能电表固件架构就稳了一半。下一章我们聊聊任务划分和状态机设计——那才是真正让固件「活起来」的关键。