1. 课程导入
嵌入式系统开发正从“寄存器直写 + 全局变量 + 大循环”快速走向“可复用组件 + 清晰接口 + 可测试架构”。在资源受限、实时性敏感、硬件差异巨大的嵌入式环境里,C++ 并不是“把 PC 端那套搬过来”,而是要用它更精确地表达硬件抽象、资源所有权、接口契约与组件边界。本课程围绕一个核心问题展开:如何用 C++ 设计出既贴近硬件、又能长期维护和复用的接口与组件。
一、什么是嵌入式C++接口与组件设计
接口是调用者与实现者之间的契约,它规定“能做什么”,隐藏“怎么做”。在嵌入式 C++ 中,接口通常表现为抽象基类、纯虚函数、模板概念、回调签名或编译期约束。组件则是围绕某一职责封装起来的一组接口、数据与实现,例如 GPIO 驱动组件、串口通信组件、传感器采集组件、日志组件、状态机组件。
与通用软件不同,嵌入式接口设计必须同时面对四类约束:
- 资源约束:RAM/ROM 有限,避免不必要的虚函数表、动态分配和异常开销。
- 实时约束:中断上下文、任务优先级、阻塞时间与响应延迟必须可预期。
- 硬件约束:寄存器、DMA、时钟、引脚复用、外设差异需要被抽象但不能被“抽象掉”。
- 生命周期约束:对象何时构造、何时初始化、何时销毁,必须与硬件上电时序一致。
接口设计的目标不是“看起来优雅”,而是让硬件差异被隔离、让上层逻辑可测试、让替换实现不破坏调用方。
二、学习目标:从会写 C++ 到会设计可复用组件
完成本课程后,学习者应能达成以下目标:
- 能够区分“接口”“实现”“组件”“模块”在嵌入式语境下的边界与职责。
- 能够使用抽象基类、模板、策略模式、依赖注入等手段设计低耦合接口。
- 能够为 GPIO、UART、SPI、I2C、ADC、定时器等常见外设设计统一抽象层。
- 能够控制 C++ 特性开销,理解虚函数、RTTI、异常、动态内存在 MCU 上的代价。
- 能够编写可在宿主机上单元测试的硬件无关组件,并通过 mock/fake 替换硬件实现。
- 能够识别并重构“上帝类”“全局状态”“中断与任务共享数据”等典型坏味道。
三、整体知识框架
本课程的知识框架可以理解为一条从“语言机制”到“硬件抽象”再到“系统集成”的链路。下图展示了接口与组件设计在嵌入式 C++ 工程中的位置:底层是语言与资源约束,中层是接口抽象与组件封装,上层是应用逻辑与测试验证。
四、典型应用场景
接口与组件设计并非“架构师才需要”,它直接决定日常开发效率。以下场景在嵌入式项目中非常常见:
| 场景 | 问题 | 接口/组件设计的作用 |
|---|---|---|
| 多型号 MCU 共用业务代码 | 换芯片就要改上层逻辑 | 用统一外设接口隔离寄存器差异 |
| 传感器驱动复用 | 不同传感器读写时序不同 | 抽象出 Sensor 接口,实现可替换 |
| 宿主机单元测试 | 硬件不在身边无法验证逻辑 | 依赖注入 + Fake 实现,脱离硬件运行 |
| 中断与任务共享数据 | 竞态、丢数据、难复现 | 用组件封装队列/双缓冲与临界区 |
| 产品线功能裁剪 | 条件编译遍布代码 | 用接口组合与策略模式替代宏开关 |
五、一个最小示例:接口如何隔离硬件
下面用 GPIO 抽象说明“接口—实现—调用方”的关系。上层只依赖 IGpio,不关心是 STM32 还是 ESP32。
class IGpio {
public:
virtual ~IGpio() = default;
virtual void write(bool level) = 0;
virtual bool read() const = 0;
};
class Stm32Gpio final : public IGpio {
public:
explicit Stm32Gpio(GPIO_TypeDef* port, uint16_t pin)
: port_(port), pin_(pin) {}
void write(bool level) override { /* HAL_GPIO_WritePin */ }
bool read() const override { /* HAL_GPIO_ReadPin */ }
private:
GPIO_TypeDef* port_;
uint16_t pin_;
};
class LedController {
public:
explicit LedController(IGpio& gpio) : gpio_(gpio) {}
void on() { gpio_.write(true); }
void off() { gpio_.write(false); }
private:
IGpio& gpio_;
};
注意:
LedController 不包含任何寄存器头文件,因此可以在 PC 上用 FakeGpio 直接测试。
六、常见误区与复盘
- 误区一:接口越抽象越好。过度抽象会带来虚函数开销和调试困难,嵌入式应优先“够用且稳定”的抽象。
- 误区二:把所有东西都做成单例。全局单例破坏可测试性,也容易在中断与任务间产生隐藏依赖。
- 误区三:在构造函数里初始化硬件。构造顺序不可控,硬件初始化应显式调用
init()并检查返回值。 - 误区四:忽略对象生命周期。静态对象、栈对象、堆对象在嵌入式中的代价完全不同,需明确所有权。
- 误区五:接口没有错误处理。嵌入式接口应明确返回错误码或状态,而不是默默失败。
复盘建议:每设计一个组件,先问三个问题——它依赖什么硬件?谁拥有它的生命周期?如果换一块板子,哪些代码必须改?如果答案超过一处,说明接口边界还需要调整。
本课程后续将沿着“语言机制 → 接口抽象 → 组件封装 → 硬件适配 → 测试验证 → 系统集成”的路径逐步展开。掌握这套方法后,你写的将不再是一堆只能在一款板子上运行的代码,而是可以跨项目、跨芯片、跨团队复用的嵌入式 C++ 组件。