2. 嵌入式系统概述:MCU选型、RTOS与裸机对比、HAL设计原则

好,咱们进入第二章。这一章我打算聊聊嵌入式系统里几个最基础、也最绕不开的话题。说白了,就是三个核心问题:芯片怎么选?要不要上系统?代码怎么写才不烂?

这三个问题,我当年刚入行时也纠结了很久。记得有一次项目赶工期,我拍脑袋选了个STM32F103,结果发现外设不够用,硬生生加了块CPLD来兜底。嗯,从那以后,我选型再也不敢只看主频和Flash了。

2.1 MCU选型:STM32 vs ESP32

选MCU就像挑工具。你不能拿螺丝刀去钉钉子,对吧?

我个人习惯把选型拆成三个维度:性能、外设、生态。咱们拿STM32和ESP32这两个明星选手来对比一下。

对比项 STM32(以F4/H7为例) ESP32
内核 Cortex-M4/M7,主频可达480MHz Xtensa LX6双核,主频240MHz
存储 Flash最大2MB,SRAM最大1MB Flash最大16MB(外挂),SRAM 520KB
无线能力 无(需外挂WiFi/BLE芯片) 内置WiFi + BLE 5.0
外设丰富度 极其丰富:多路ADC/DAC、CAN、USB、以太网 够用:I2C、SPI、UART、I2S、触摸传感器
功耗 低功耗模式优秀(uA级) 中等,深度睡眠可到10uA左右
开发工具 Keil、IAR、STM32CubeIDE ESP-IDF、Arduino、MicroPython
成本 中高(F4系列约20-50元) 低(约10-20元)

你想想看,如果你的红外传感器项目需要联网上报数据,ESP32几乎是首选。内置WiFi,省掉一个外挂模块,BOM成本直接砍半。但如果你需要高精度ADC采集模拟信号,或者需要CAN总线与车载系统通信,那STM32的丰富外设就是你的不二之选。

我的小建议: 别只看芯片本身的价格。算上开发周期、调试难度、供应链稳定性,有时候贵一点的芯片反而更省钱。我在一个量产项目里吃过亏,选了冷门芯片,结果供货不稳定,被迫换方案,损失惨重。

2.2 RTOS vs 裸机:什么时候该上系统?

这个问题,几乎每个嵌入式工程师都会遇到。我的答案是:看你的任务复杂度

裸机,说白了就是一个超级循环(Super Loop)。代码从上往下跑,跑完一遍再从头来。简单、直接、没有额外开销。

// 裸机典型结构
void main(void) {
    while(1) {
        read_sensor();      // 读传感器
        process_data();     // 处理数据
        send_output();      // 输出结果
        delay_ms(100);      // 延时
    }
}

这种模式,适合什么场景?

  • 任务单一,比如只读一个传感器,然后控制一个LED
  • 实时性要求不高,几十毫秒的抖动无所谓
  • 代码量小,逻辑简单

但一旦任务多起来,裸机就捉襟见肘了。比如你要同时处理红外传感器数据、响应按键、刷新OLED屏幕、还要通过UART上报日志。这时候,一个任务卡住,其他任务全得等着。

RTOS(实时操作系统)就是来解决这个问题的。它把任务切分成多个线程,由调度器决定谁先跑、跑多久。

// RTOS典型结构(以FreeRTOS为例)
void task_sensor(void *pvParameters) {
    while(1) {
        read_sensor();
        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

void task_display(void *pvParameters) {
    while(1) {
        update_display();
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

void main(void) {
    xTaskCreate(task_sensor, "Sensor", 256, NULL, 1, NULL);
    xTaskCreate(task_display, "Display", 256, NULL, 2, NULL);
    vTaskStartScheduler();
}
核心对比:
  • 裸机:简单、低开销、适合单任务。但扩展性差,任务间耦合高。
  • RTOS:任务隔离、优先级管理、适合多任务。但有额外RAM/ROM开销,调试复杂度增加。

我曾经在一个红外测温枪项目里,一开始用裸机,后来发现按键消抖、ADC采样、LCD刷新、蜂鸣器控制搅在一起,改一个地方就崩一片。换成FreeRTOS后,每个任务独立,代码清爽多了。嗯,真香。

注意: RTOS不是万能药。如果你的任务之间共享大量数据,或者对时序要求极其苛刻(比如微秒级),裸机+状态机可能更合适。别为了用RTOS而用RTOS。

2.3 硬件抽象层(HAL)设计原则

HAL,全称Hardware Abstraction Layer。说白了,就是给硬件穿上一件「通用外衣」。你写上层应用时,不用关心底层用的是STM32还是ESP32,调用同样的API就行。

我见过太多项目,代码和寄存器操作死死绑在一起。换一个MCU,整个项目几乎重写。这就是没有HAL的代价。

好的HAL设计,我总结了三个原则:

  1. 接口统一:不管底层是什么MCU,上层调用的函数名、参数、返回值保持一致。
  2. 隔离变化:硬件相关的代码(寄存器操作、中断处理)全部封装在HAL内部,上层不感知。
  3. 可配置:通过宏定义或配置文件,选择不同的底层实现。

举个例子,红外传感器的驱动,我们可以这样设计:

// hal_ir_sensor.h - 统一的HAL接口
#ifndef HAL_IR_SENSOR_H
#define HAL_IR_SENSOR_H

#include <stdint.h>

// 初始化红外传感器
void HAL_IR_Init(void);

// 读取原始数据
uint16_t HAL_IR_ReadRaw(void);

// 读取温度值(单位:摄氏度)
float HAL_IR_ReadTemperature(void);

#endif

然后,针对不同MCU,提供不同的实现文件:

// hal_ir_sensor_stm32.c - STM32实现
#include "hal_ir_sensor.h"
#include "stm32f4xx_hal.h"

void HAL_IR_Init(void) {
    // 初始化STM32的ADC和GPIO
    HAL_ADC_Init(&hadc1);
    HAL_GPIO_WritePin(IR_POWER_GPIO_Port, IR_POWER_Pin, GPIO_PIN_SET);
}

uint16_t HAL_IR_ReadRaw(void) {
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 100);
    return HAL_ADC_GetValue(&hadc1);
}
// hal_ir_sensor_esp32.c - ESP32实现
#include "hal_ir_sensor.h"
#include "driver/adc.h"

void HAL_IR_Init(void) {
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_11);
}

uint16_t HAL_IR_ReadRaw(void) {
    return adc1_get_raw(ADC1_CHANNEL_0);
}

你看,上层应用只需要调用 HAL_IR_Init()HAL_IR_ReadRaw(),完全不用管底层是ADC还是I2C。换MCU时,只需要换一个HAL实现文件,上层代码一行都不用改。

避坑指南: 我曾经在一个项目里,把HAL层设计得过于「抽象」,结果每个函数都包了三层指针回调,调试时根本不知道实际调了哪个函数。记住:抽象是为了简化,不是为了炫技。保持接口简单、直接,必要时可以牺牲一点点通用性。

2.4 本章知识体系

下面这张图,是我梳理的本章核心逻辑。你可以把它当作一个决策树,帮助你在实际项目中快速做选择。

嵌入式系统核心决策树 项目需求分析 MCU选型 软件架构选择 STM32(外设丰富) ESP32(无线集成) 裸机(简单任务) RTOS(多任务) 硬件抽象层(HAL)设计 接口统一 · 隔离变化 · 可配置

这张图把咱们本章讲的三块内容串起来了。从项目需求出发,先选MCU,再定软件架构,最后用HAL把硬件和软件优雅地解耦。每一步都有取舍,没有绝对的对错,只有合不合适。

好了,第二章就到这里。记住:选型是基础,架构是骨架,HAL是灵魂。这三样东西搞明白了,后面写代码就顺了。


专注资料整理