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后,每个任务独立,代码清爽多了。嗯,真香。
2.3 硬件抽象层(HAL)设计原则
HAL,全称Hardware Abstraction Layer。说白了,就是给硬件穿上一件「通用外衣」。你写上层应用时,不用关心底层用的是STM32还是ESP32,调用同样的API就行。
我见过太多项目,代码和寄存器操作死死绑在一起。换一个MCU,整个项目几乎重写。这就是没有HAL的代价。
好的HAL设计,我总结了三个原则:
- 接口统一:不管底层是什么MCU,上层调用的函数名、参数、返回值保持一致。
- 隔离变化:硬件相关的代码(寄存器操作、中断处理)全部封装在HAL内部,上层不感知。
- 可配置:通过宏定义或配置文件,选择不同的底层实现。
举个例子,红外传感器的驱动,我们可以这样设计:
// 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实现文件,上层代码一行都不用改。
2.4 本章知识体系
下面这张图,是我梳理的本章核心逻辑。你可以把它当作一个决策树,帮助你在实际项目中快速做选择。
这张图把咱们本章讲的三块内容串起来了。从项目需求出发,先选MCU,再定软件架构,最后用HAL把硬件和软件优雅地解耦。每一步都有取舍,没有绝对的对错,只有合不合适。
好了,第二章就到这里。记住:选型是基础,架构是骨架,HAL是灵魂。这三样东西搞明白了,后面写代码就顺了。