2. MCU与中间件基础
做嵌入式开发久了,你会发现一个规律:代码的腐烂速度,往往比硬件老化快得多。我刚入行那会儿,接手过一个智能家居项目。LED 控制、按键扫描、串口通信全揉在一个文件里,足足 3000 多行。改一个呼吸灯效果,得翻遍整个文件,生怕动到别的地方。后来我花了整整两周重构,才把项目从崩溃边缘拉回来。
那次经历让我彻底明白——模块化设计不是花架子,是保命符。这一章,我们就来聊聊怎么把模块化思想落地到 MCU 开发中。
为什么需要分层架构?
你想想看,一个典型的 MCU 项目,通常包含这些要素:
- 操作 GPIO 点亮 LED
- 通过定时器产生 PWM
- 用 SPI/I2C 读写传感器
- 处理中断和任务调度
如果所有代码都堆在一起,会是什么后果?
分层架构的核心思路很简单:把不同职责的代码放到不同层级。我习惯分成三层:
| 层级 | 职责 | 典型内容 |
|---|---|---|
| 应用层 | 业务逻辑、状态机、用户交互 | LED 闪烁模式、按键响应逻辑 |
| 中间件层 | 通用功能模块、协议封装 | LED 驱动接口、定时器抽象 |
| 硬件抽象层 (HAL) | MCU 外设操作、寄存器读写 | GPIO 置位/复位、定时器配置 |
这样做的好处很明显:换 MCU 时,只需要改 HAL 层。应用层和中间件层基本不用动。我在 STM32 和 GD32 之间移植过项目,只花了半天就搞定了——因为底层接口长得一模一样。
模块解耦:让每个模块只做一件事
解耦这个词听起来高大上,说白了就是:别让 A 模块直接操作 B 模块的内部变量。
举个例子。一个常见的 LED 驱动,很多人会这样写:
// 不好的写法:直接操作 GPIO 寄存器
void led_on(void) {
GPIOA->BSRR = (1 << 5); // PA5 置高
}
void led_off(void) {
GPIOA->BRR = (1 << 5); // PA5 置低
}
这段代码有什么问题?它把硬件细节暴露给了调用者。如果哪天换了 PCB,LED 从 PA5 改到 PB3,你得改所有调用 led_on/led_off 的地方。
我建议的做法是:引入一个配置层。
// led_cfg.h - 硬件配置
#define LED_PORT GPIOA
#define LED_PIN GPIO_PIN_5
// led_drv.h - 驱动接口
void led_init(void);
void led_set(uint8_t state);
void led_toggle(void);
// led_drv.c - 驱动实现
#include "led_cfg.h"
#include "hal_gpio.h"
void led_init(void) {
hal_gpio_init(LED_PORT, LED_PIN, GPIO_MODE_OUTPUT);
}
void led_set(uint8_t state) {
if (state) {
hal_gpio_write(LED_PORT, LED_PIN, 1);
} else {
hal_gpio_write(LED_PORT, LED_PIN, 0);
}
}
这样改完之后,应用层只调用 led_set(1) 和 led_set(0),根本不知道底层用的是哪个引脚。换硬件?改一下 led_cfg.h 就行。
接口设计原则:稳定比什么都重要
接口是模块之间的契约。一旦定下来,尽量不要改。我在项目中总结了几条原则:
- 参数尽量简单:能用基本类型就别用结构体。比如 led_set(uint8_t state),比 led_set(led_state_t *p_state) 好得多。
- 返回值要明确:成功返回 0,失败返回负值。别搞什么 0 表示成功、1 表示失败——那会让调用者晕头转向。
- 命名要自解释:led_init、led_set、led_toggle,一看就知道干什么。别用 led_func1、led_proc 这种名字。
举个例子,一个设计良好的定时器接口:
// timer_if.h - 定时器抽象接口
typedef void (*timer_callback_t)(void *arg);
int timer_init(uint32_t period_ms, timer_callback_t cb, void *arg);
int timer_start(void);
int timer_stop(void);
int timer_set_period(uint32_t period_ms);
这个接口有什么好处?它不依赖任何具体的定时器硬件。底层可以用 SysTick,也可以用 TIM2,甚至可以用一个软件定时器。只要接口不变,上层代码纹丝不动。
依赖倒置:让高层不依赖底层细节
依赖倒置原则(DIP)听起来很玄乎,其实就一句话:高层模块不应该依赖低层模块,两者都应该依赖抽象。
怎么理解?看个例子。假设你的应用层需要读取温度传感器:
// 不好的写法:应用层直接调用传感器驱动
void app_temperature_task(void) {
int temp = ds18b20_read_temperature();
// 处理温度数据...
}
如果哪天换了传感器,从 DS18B20 换成 SHT30,你得改应用层代码。这违反了开闭原则。
用依赖倒置的思路,应该这样设计:
// sensor_if.h - 传感器抽象接口
typedef struct {
int (*init)(void);
int (*read)(int *value);
int (*deinit)(void);
} sensor_t;
// app_temperature.c - 应用层
void app_temperature_task(sensor_t *sensor) {
int temp;
if (sensor->read(&temp) == 0) {
// 处理温度数据...
}
}
这样,应用层只依赖 sensor_t 这个抽象接口,不依赖具体的传感器型号。换传感器时,只需要提供一个新的 sensor_t 实例。
实战案例:LED 驱动模块化重构
说了这么多理论,我们来动真格的。下面是一个完整的 LED 驱动模块化重构案例。
原始代码(混乱版):
// main.c - 所有代码揉在一起
#include "stm32f1xx.h"
void delay_ms(uint32_t ms) {
for (uint32_t i = 0; i < ms * 1000; i++);
}
int main(void) {
RCC->APB2ENR |= (1 << 2); // 使能 GPIOA 时钟
GPIOA->CRL &= ~(0xF << 20);
GPIOA->CRL |= (0x2 << 20); // PA5 推挽输出
while (1) {
GPIOA->BSRR = (1 << 5); // 亮
delay_ms(500);
GPIOA->BRR = (1 << 5); // 灭
delay_ms(500);
}
}
这段代码的问题很明显:硬件操作、延时逻辑、主循环全混在一起。换 MCU 得重写整个文件。
重构后的模块化版本:
第一步:定义硬件抽象层接口。
// hal_gpio.h
#ifndef HAL_GPIO_H
#define HAL_GPIO_H
#include <stdint.h>
typedef enum {
HAL_GPIO_MODE_INPUT,
HAL_GPIO_MODE_OUTPUT,
HAL_GPIO_MODE_AF,
HAL_GPIO_MODE_ANALOG
} hal_gpio_mode_t;
void hal_gpio_init(uint32_t port, uint32_t pin, hal_gpio_mode_t mode);
void hal_gpio_write(uint32_t port, uint32_t pin, uint8_t value);
uint8_t hal_gpio_read(uint32_t port, uint32_t pin);
#endif
第二步:实现 LED 驱动模块。
// led_drv.h
#ifndef LED_DRV_H
#define LED_DRV_H
#include <stdint.h>
void led_init(void);
void led_on(void);
void led_off(void);
void led_toggle(void);
#endif
// led_drv.c
#include "led_drv.h"
#include "led_cfg.h"
#include "hal_gpio.h"
void led_init(void) {
hal_gpio_init(LED_PORT, LED_PIN, HAL_GPIO_MODE_OUTPUT);
led_off();
}
void led_on(void) {
hal_gpio_write(LED_PORT, LED_PIN, LED_ON_LEVEL);
}
void led_off(void) {
hal_gpio_write(LED_PORT, LED_PIN, LED_OFF_LEVEL);
}
void led_toggle(void) {
uint8_t state = hal_gpio_read(LED_PORT, LED_PIN);
hal_gpio_write(LED_PORT, LED_PIN, !state);
}
第三步:配置信息独立出来。
// led_cfg.h
#ifndef LED_CFG_H
#define LED_CFG_H
#include "hal_gpio.h"
#define LED_PORT GPIOA_BASE
#define LED_PIN 5
#define LED_ON_LEVEL 1 // 高电平点亮
#define LED_OFF_LEVEL 0
#endif
第四步:应用层只调用 LED 驱动接口。
// app_led.c
#include "led_drv.h"
#include "app_timer.h"
static void led_blink_callback(void *arg) {
led_toggle();
}
void app_led_start_blink(uint32_t period_ms) {
led_init();
timer_start(period_ms, led_blink_callback, NULL);
}
重构之后,每个模块的职责非常清晰:
- hal_gpio:封装 GPIO 寄存器操作,与 MCU 型号绑定
- led_drv:提供 LED 控制接口,不关心硬件细节
- led_cfg:存放硬件配置,改板子时只需改这里
- app_led:实现业务逻辑,比如闪烁、呼吸灯效果
换 MCU 时,只需要重写 hal_gpio 层。其他代码完全不用动。我在 STM32F103 和 GD32F303 之间做过移植,只改了 hal_gpio.c 一个文件,总共不到 100 行代码。
本章知识结构
下面这张图总结了模块化设计的核心逻辑:
这张图展示了模块化设计的核心思想:上层依赖下层,但下层不依赖上层。每一层只通过接口与相邻层通信,互不干扰。