2、嵌入式系统基础回顾:MCU架构与选型、RTOS基础(FreeRTOS/uCOS)、硬件抽象层(HAL)概念

各位同学,咱们今天聊点实在的。做工业通信协议栈移植,说白了就是让协议栈在MCU上跑起来、跑得稳。那MCU选不对、RTOS用不好、HAL层没搭好,后面全是坑。我这些年踩过的坑,今天一次性给你们讲透。

2.1 MCU架构与选型:别光看主频

很多人选MCU,上来就看主频。嗯,这其实是个误区。我见过一个项目,主频200MHz的芯片,跑Modbus TCP死活不稳定,后来发现是DMA和中断优先级没配好。你想想看,主频再高,外设响应慢,有啥用?

我个人习惯,选型时重点看三样东西:

  • 外设资源:你要跑什么协议?CAN、EtherCAT、Profinet?对应的外设得有。比如做EtherCAT,MCU得有EtherCAT从站控制器(ESC),不然全靠软件模拟,延迟高得吓人。
  • 中断响应能力:工业通信对实时性要求极高。我建议选那些中断延迟可预测的MCU,比如Cortex-M系列,它的NVIC(嵌套向量中断控制器)做得不错。
  • 内存与DMA:协议栈要缓存数据,DMA能帮你把CPU解放出来。我曾经在一个项目里,因为没配DMA,CPU被数据搬运占用了30%的性能,后来加上DMA,瞬间流畅了。

重要提醒:别只看数据手册上的理论值。实际跑起来,外设的响应时间、总线仲裁的延迟,都会影响性能。我建议你拿示波器实测一下GPIO翻转时间,心里才有底。

说到架构,Cortex-M系列是主流。M0适合简单控制,M4/M7适合复杂协议栈。我个人偏好M7,它的双精度浮点单元(FPU)在做一些算法时很有用。但注意,M7的功耗也高,电池供电的项目要慎重。

2.2 RTOS基础:FreeRTOS vs uCOS

RTOS,说白了就是帮你在MCU上做任务调度。没有RTOS,你只能写超级循环(super loop),那玩意儿在复杂协议栈面前就是个灾难。

我最早用uCOS,后来转FreeRTOS。为什么?因为FreeRTOS开源、免费,而且生态好。uCOS虽然稳定,但商业授权贵,小公司扛不住。不过,uCOS的文档确实写得详细,适合新手入门。

咱们对比一下:

特性 FreeRTOS uCOS-III
开源协议 MIT(免费) 商业授权(收费)
任务数量 无限制 无限制
抢占式调度 支持 支持
内存占用 小(约4KB) 中等(约8KB)
实时性 优秀 优秀
社区支持 非常活跃 一般

我个人建议,新项目优先选FreeRTOS。但如果你做的是医疗、航空等高可靠性产品,uCOS的认证(如DO-178C)可能更合适。

小技巧:用FreeRTOS时,记得把configUSE_TICKLESS_IDLE打开。这能省电,尤其适合电池供电的设备。我有个项目,开了这个选项后,功耗降了40%。

RTOS的核心概念,你得搞懂:

  • 任务(Task):每个任务就是一个无限循环。注意,任务栈大小要算好,我见过有人栈溢出导致系统崩溃,查了三天才发现。
  • 队列(Queue):任务间通信用的。我习惯用队列传递数据,而不是全局变量。全局变量容易引发竞态条件,你想想看,两个任务同时写一个变量,结果会怎样?
  • 信号量(Semaphore):用于同步。二值信号量适合事件通知,计数信号量适合资源管理。
  • 互斥量(Mutex):防止资源被多个任务同时访问。注意,互斥量有优先级继承机制,能避免优先级反转。我曾经在一个项目里没注意这个,结果低优先级任务占着资源,高优先级任务一直等,系统卡死了。

代码示例,咱们看看FreeRTOS怎么创建一个任务:

void vTaskFunction(void *pvParameters) {
    while(1) {
        // 处理协议栈数据
        vTaskDelay(pdMS_TO_TICKS(10)); // 10ms周期
    }
}

int main(void) {
    xTaskCreate(vTaskFunction, "ProtocolTask", 256, NULL, 2, NULL);
    vTaskStartScheduler();
    while(1); // 不会执行到这里
}

注意,任务栈大小256字节够不够?得看你任务里用了多少局部变量。我建议先设大一点,比如512,然后通过FreeRTOS的栈溢出检测功能(configCHECK_FOR_STACK_OVERFLOW)来优化。

2.3 硬件抽象层(HAL)概念:别让代码绑死在芯片上

HAL,说白了就是一层中间件。它把MCU的硬件操作封装起来,让你上层代码不用关心底层是STM32还是GD32。我见过太多人,代码里直接写寄存器操作,结果换芯片时,整个协议栈都得重写。那叫一个痛苦。

HAL的好处:

  • 可移植性:换MCU时,只需要改HAL层,上层代码不动。
  • 可维护性:硬件相关的代码集中管理,出问题好定位。
  • 可测试性:你可以模拟HAL接口,在PC上测试协议栈逻辑。

我建议,HAL层至少封装这些接口:

  • GPIO:初始化、读写、中断配置
  • UART/SPI/I2C:收发数据、DMA配置
  • 定时器:延时、PWM、捕获
  • 中断:使能、禁止、优先级设置
  • 内存操作:memcpy、memset等(有些MCU的DMA能加速)

举个例子,咱们封装一个UART发送接口:

// hal_uart.h
typedef struct {
    void (*init)(uint32_t baudrate);
    void (*send)(uint8_t *data, uint16_t len);
    void (*register_callback)(void (*rx_cb)(uint8_t));
} HAL_UART_t;

// hal_uart_stm32.c
static void stm32_uart_init(uint32_t baudrate) {
    // STM32的HAL库初始化
    HAL_UART_Init(&huart1);
}

static void stm32_uart_send(uint8_t *data, uint16_t len) {
    HAL_UART_Transmit(&huart1, data, len, 1000);
}

HAL_UART_t hal_uart = {
    .init = stm32_uart_init,
    .send = stm32_uart_send,
    .register_callback = stm32_uart_register_callback
};

你看,上层代码只需要调用hal_uart.send(),不用管底层是哪个MCU。换芯片时,只要重新实现hal_uart_xxx.c就行。

避坑指南:我曾经在一个项目里,HAL层封装得太厚,导致每次中断都要经过三层函数调用,延迟增加了5微秒。对于高速协议(如EtherCAT),这5微秒就是灾难。所以,HAL层要适度封装,关键路径上可以直接操作寄存器。

最后,我建议你在HAL层加一个断言(assert)机制。比如,参数检查、硬件状态检查。这样出问题时能快速定位。我习惯用:

#define HAL_ASSERT(expr) \
    if (!(expr)) { \
        /* 打印错误信息,进入死循环 */ \
        while(1); \
    }

嗯,今天就聊到这儿。下一章咱们开始讲协议栈移植的具体步骤。记住,基础不牢,地动山摇。MCU选型、RTOS配置、HAL设计,这三样搞定了,后面就顺了。