做嵌入式久了,会发现一个规律:驱动代码,最难的不是"怎么操作硬件",而是"怎么把一堆交互逻辑理顺"。
比如写一个带 MQTT 上报的温控器,按键要响应、传感器要采样、断网要重连、告警要推送、Flash 要存参数......这些模块互相调用、互相通知、互相等待。如果全用全局变量 + if-else ,半年后这代码自己都不敢动。
GoF 的 行为型模式(11 种) 就是前人总结的"对象交互套路"。它不关心怎么造对象(那是创建型的活),也不关心怎么搭结构(结构型的活),它只解决一件事:对象之间怎么通信、职责怎么分、算法怎么换,才能不耦合、好扩展、好维护。
C 语言没有类、没有继承,但"行为型模式"的核心思想------把变化的部分封装、把交互的路径理顺------在 C 里照样能用,而且用对了效果惊人。FreeRTOS、RT-Thread、lwIP 这些项目里到处都是。
行为型模式总览
| 模式 | 一句话 | 嵌入式典型场景 |
|---|---|---|
| 责任链 | 请求沿链传递,谁处理谁停 | AT 指令解析、日志分级、按键事件 |
| 命令 | 把动作封装成对象 | 串口命令分发、按键队列、撤销重做 |
| 解释器 | 定义文法并解析 | AT 指令解析器、简单表达式计算 |
| 迭代器 | 统一遍历方式 | 链表、环形缓冲区、消息队列遍历 |
| 中介者 | 多对象交互集中到中介 | 事件中心、消息总线 |
| 备忘录 | 存档与恢复状态 | 参数掉电保存、配置回滚 |
| 观察者 | 一对多通知 | 传感器订阅、事件总线、UI 刷新 |
| 状态 | 状态封装成类,消除条件分支 | 协议状态机、按键消抖、充电流程 |
| 策略 | 算法可互换 | 滤波算法、编解码切换 |
| 模板方法 | 父类定骨架,子类填步骤 | 驱动初始化流程、外设抽象 |
| 访问者 | 不改元素加操作 | 设备树多维度遍历、协议统计 |
新手重点掌握:状态、观察者、策略、责任链、模板方法。这五个在嵌入式里出现频率最高。
配图:行为型模式分类树

1. 责任链模式(Chain of Responsibility)
是什么
把多个处理对象串成一条链,请求沿链传递,每个处理者决定"我处理"还是"传给下一个"。发送者不需要知道最终是谁处理的,实现发送者与接收者解耦。
用在哪
- AT 指令解析:先判断是不是 OK,再判断 ERROR,再判断 +URC,最后默认处理
- 日志系统:DEBUG 信息走串口,WARN 走文件,ERROR 触发告警
- 按键事件:短按、长按、双击,沿链判断
配图:责任链结构与时序

C 代码:AT 响应解析链
c
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
/* 处理结果 */
typedef enum {
CHAIN_PASS = 0, /* 本节点不处理,传给下一个 */
CHAIN_HANDLED /* 已处理,停止传递 */
} ChainResult;
/* 责任链节点:函数指针 + next 指针 */
typedef struct ChainNode {
ChainResult (*handle)(const char *line, void *ctx);
struct ChainNode *next;
} ChainNode;
/* 责任链头部,注册后统一调用 */
void Chain_Dispatch(ChainNode *head, const char *line, void *ctx)
{
ChainNode *node = head;
while (node != NULL) {
if (node->handle(line, ctx) == CHAIN_HANDLED) {
break; /* 已处理,停止传递 */
}
node = node->next;
}
}
/* ---------- 具体处理者:解析 OK ---------- */
static ChainResult HandleOk(const char *line, void *ctx)
{
(void)ctx;
if (strcmp(line, "OK") == 0) {
/* TODO: 唤醒等待 AT 响应的任务 */
return CHAIN_HANDLED;
}
return CHAIN_PASS;
}
/* ---------- 具体处理者:解析 ERROR ---------- */
static ChainResult HandleError(const char *line, void *ctx)
{
(void)ctx;
if (strncmp(line, "ERROR", 5) == 0) {
/* TODO: 设置错误标志,通知等待任务 */
return CHAIN_HANDLED;
}
return CHAIN_PASS;
}
/* ---------- 具体处理者:解析 URC(主动上报) ---------- */
static ChainResult HandleUrc(const char *line, void *ctx)
{
(void)ctx;
if (line[0] == '+') {
/* TODO: 分发到 URC 处理表 */
return CHAIN_HANDLED;
}
return CHAIN_PASS;
}
/* ---------- 组装责任链 ---------- */
void AtParser_Init(void)
{
static ChainNode nodeUrc = { HandleUrc, NULL };
static ChainNode nodeErr = { HandleError, &nodeUrc };
static ChainNode nodeOk = { HandleOk, &nodeErr };
/* 使用时:Chain_Dispatch(&nodeOk, line, ctx); */
(void)nodeOk;
}
容易踩的坑
- 链别太长。超过 5 个节点就该考虑用哈希表或分发表了,链式遍历有性能开销。
- 顺序敏感 。把高频/快判断的放前面,比如 URC 判断
line[0] == '+'代价极低,应靠前。 - 避免循环链 。组装时小心
next指针别指回头部。
2. 命令模式(Command)
是什么
把"一个请求/动作"封装成独立的对象(C 里就是结构体 + 函数指针),让请求可以排队、记录、撤销、重做。请求的发起者不需要知道执行者是谁。
用在哪
- 按键事件队列:按键扫描任务把"按键 X 被按下"封装成命令丢队列,业务任务取出执行
- 串口协议命令分发:每条协议指令封装成一个命令对象
- 撤销/重做:参数配置工具的 Undo 栈
配图:命令模式类图

C 代码:按键命令队列
c
#include <stdint.h>
#include <stdbool.h>
/* 命令接口:每个命令实现 execute */
typedef struct Command {
void (*execute)(struct Command *self, void *arg);
void (*undo)(struct Command *self); /* 可选:撤销 */
uint8_t cmd_id; /* 命令标识 */
} Command;
/* 简单环形队列 */
#define CMD_QUEUE_SIZE 16
static Command *s_queue[CMD_QUEUE_SIZE];
static volatile uint8_t s_head = 0;
static volatile uint8_t s_tail = 0;
bool CmdQueue_Push(Command *cmd)
{
uint8_t next = (s_tail + 1u) % CMD_QUEUE_SIZE;
if (next == s_head) {
return false; /* 队列满 */
}
s_queue[s_tail] = cmd;
s_tail = next;
return true;
}
Command *CmdQueue_Pop(void)
{
if (s_head == s_tail) {
return NULL; /* 队列空 */
}
Command *cmd = s_queue[s_head];
s_head = (s_head + 1u) % CMD_QUEUE_SIZE;
return cmd;
}
/* ---------- 具体命令:切换 LED ---------- */
typedef struct {
Command base;
uint8_t led_id;
} LedCmd;
static void LedCmd_Execute(Command *self, void *arg)
{
LedCmd *c = (LedCmd *)self;
(void)arg;
/* TODO: LED_Toggle(c->led_id); */
(void)c;
}
/* 业务任务:从队列取命令并执行 */
void Task_CommandLoop(void *arg)
{
(void)arg;
for (;;) {
Command *cmd = CmdQueue_Pop();
if (cmd != NULL) {
cmd->execute(cmd, NULL);
} else {
/* vTaskDelay(pdMS_TO_TICKS(10)); */
}
}
}
容易踩的坑
- 命令对象的生命周期。如果命令是栈上临时变量,入队后函数返回就悬空了。要么用静态分配,要么用对象池。
- 别把业务塞进命令。命令只负责"调用谁",具体逻辑在被调用方。命令里写几百行就是反模式。
3. 解释器模式(Interpreter)
是什么
针对一类特定"语言"(可以很简单,比如 SET 12 0xFF),定义文法规则,并实现一个解释器来解析执行。日常开发用得少,但在嵌入式里 AT 指令解析、串口调试命令行、Modbus 寄存器映射 都能看到它的影子。
用在哪
- 串口调试 CLI(
led on、read 0x40021000) - 简单表达式计算(
temp * 0.01 - 40) - AT 指令参数解析
配图:解释器结构

C 代码:简易串口 CLI 解释器
c
#include <string.h>
#include <stdlib.h>
/* 命令表条目 */
typedef struct {
const char *name; /* 指令名 */
void (*handler)(int argc, char *argv[]); /* 处理函数 */
const char *help; /* 帮助文本 */
} CliCmd;
/* 简单 tokenizer:按空格切分 */
static int Tokenize(char *line, char *argv[], int max)
{
int argc = 0;
char *p = line;
while (*p != '\0' && argc < max) {
while (*p == ' ') { *p++ = '\0'; } /* 跳过空格 */
if (*p == '\0') { break; }
argv[argc++] = p;
while (*p != '\0' && *p != ' ') { p++; }
}
return argc;
}
/* 命令表 */
static const CliCmd s_cmds[] = {
{ "led", Cmd_Led, "led <on|off> <id>" },
{ "read", Cmd_Read, "read <addr>" },
{ "help", Cmd_Help, "show help" },
};
#define CMD_COUNT (sizeof(s_cmds) / sizeof(s_cmds[0]))
/* 解释器入口 */
void Cli_Interpret(char *line)
{
char *argv[8];
int argc = Tokenize(line, argv, 8);
if (argc == 0) { return; }
for (uint32_t i = 0; i < CMD_COUNT; i++) {
if (strcmp(argv[0], s_cmds[i].name) == 0) {
s_cmds[i].handler(argc, argv);
return;
}
}
/* 未知指令 */
/* TODO: 输出 "unknown command" */
}
/* 示例 handler */
void Cmd_Led(int argc, char *argv[])
{
if (argc < 3) { return; }
bool on = (strcmp(argv[1], "on") == 0);
uint8_t id = (uint8_t)atoi(argv[2]);
/* TODO: 控制 LED */
(void)on; (void)id;
}
容易踩的坑
- 别造复杂语言。如果语法开始出现嵌套括号、运算优先级,赶紧换 Lua 或现成解析器,C 手撸解释器是地狱。
- Modbus 这种成熟协议别自己解释,用现成库(FreeModbus、libmodbus)。
4. 迭代器模式(Iterator)
是什么
提供统一的方式顺序访问一个聚合对象的元素,不暴露集合内部结构 。C 里没有 foreach,但可以用函数指针模拟迭代器接口。
用在哪
- 链表遍历
- 环形缓冲区遍历
- 消息队列消费
- 设备链表枚举
配图:迭代器结构

C 代码:通用链表迭代器
c
#include <stdbool.h>
/* 链表节点 */
typedef struct ListNode {
struct ListNode *next;
void *data; /* 节点持有任意类型数据 */
} ListNode;
/* 迭代器:持有当前节点指针 */
typedef struct {
ListNode *cur;
} ListIter;
void ListIter_Init(ListIter *it, ListNode *head) { it->cur = head; }
bool ListIter_HasNext(ListIter *it) { return it->cur != NULL; }
void *ListIter_Next(ListIter *it)
{
if (it->cur == NULL) { return NULL; }
void *data = it->cur->data;
it->cur = it->cur->next;
return data;
}
/* 使用示例:遍历设备链表,统一调用 Init */
void Bus_InitAllDevices(ListNode *head)
{
ListIter it;
ListIter_Init(&it, head);
while (ListIter_HasNext(&it)) {
Device *dev = (Device *)ListIter_Next(&it);
if (dev != NULL && dev->init != NULL) {
dev->init(dev);
}
}
}
容易踩的坑
- 遍历中删除节点 。标准做法是迭代器返回"待删除标记",遍历结束后统一删除;或用
prev指针谨慎处理。 - 多线程遍历要加锁。一个任务遍历、另一个任务插入节点,必崩。
5. 中介者模式(Mediator)
是什么
用中介对象封装多个对象之间的交互,把"网状依赖"变成"星型依赖"。所有对象只跟中介通信,互不直接引用。
用在哪
- 事件中心 / 消息总线:模块 A 触发事件,模块 B、C 订阅,互不感知
- 任务间协调:采集任务、上传任务、显示任务通过中介者协调
- UI 与业务解耦:UI 不直接调业务,通过中介者转发
配图:中介者模式(网状 → 星型)

C 代码:事件总线中介者
c
#include <stdint.h>
#define MAX_SUBSCRIBERS 8
/* 事件 ID 枚举 */
typedef enum {
EVT_TEMP_UPDATED = 0,
EVT_NET_CONNECTED,
EVT_NET_DISCONNECTED,
EVT_COUNT
} EventId;
/* 订阅者回调 */
typedef void (*EventHandler)(EventId id, void *payload, uint16_t len);
/* 中介者:每个事件维护一个订阅者数组 */
typedef struct {
EventHandler subs[EVT_COUNT][MAX_SUBSCRIBERS];
uint8_t sub_count[EVT_COUNT];
} EventBus;
static EventBus s_bus;
/* 订阅 */
int8_t EventBus_Subscribe(EventId id, EventHandler handler)
{
if (id >= EVT_COUNT) { return -1; }
uint8_t cnt = s_bus.sub_count[id];
if (cnt >= MAX_SUBSCRIBERS) { return -1; }
s_bus.subs[id][cnt] = handler;
s_bus.sub_count[id] = cnt + 1u;
return 0;
}
/* 发布:中介者统一通知所有订阅者 */
void EventBus_Publish(EventId id, void *payload, uint16_t len)
{
if (id >= EVT_COUNT) { return; }
uint8_t cnt = s_bus.sub_count[id];
for (uint8_t i = 0; i < cnt; i++) {
if (s_bus.subs[id][i] != NULL) {
s_bus.subs[id][i](id, payload, len);
}
}
}
/* ---------- 使用示例 ---------- */
/* 传感器任务:采集后发布 */
void Task_Sensor(void *arg)
{
(void)arg;
float temp = 25.6f;
for (;;) {
/* temp = ReadTemp(); */
EventBus_Publish(EVT_TEMP_UPDATED, &temp, sizeof(temp));
/* vTaskDelay(pdMS_TO_TICKS(1000)); */
break;
}
}
/* 显示模块:订阅温度事件 */
static void OnTempUpdated(EventId id, void *payload, uint16_t len)
{
(void)id; (void)len;
float temp = *(float *)payload;
/* TODO: LCD_ShowTemp(temp); */
(void)temp;
}
void App_Init(void)
{
EventBus_Subscribe(EVT_TEMP_UPDATED, OnTempUpdated);
}
容易踩的坑
- 回调里别做重活。中断里发布事件,订阅回调在中断上下文执行会拖垮实时性。推荐"中断发布 → 任务消费"的二段式。
- 订阅者数量要有上限。无限增长会内存泄漏或越界,用静态数组 + 编译期上限。
- 避免循环发布。A 订阅 B 的事件,回调里又发布一个事件触发 B,死循环。
6. 备忘录模式(Memento)
是什么
在不破坏封装的前提下,捕获对象的内部状态,保存到外部,以便日后恢复。嵌入式里最典型的应用就是参数掉电保存与上电恢复。
用在哪
- 系统参数掉电保存(Flash/EEPROM)
- 配置回滚(OTA 失败恢复上一版本参数)
- 表单草稿、断点续传
配图:备忘录保存与恢复时序

C 代码:参数备份与恢复
c
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
/* 原发器:系统参数对象 */
typedef struct {
int16_t temp_threshold; /* 告警阈值 */
uint16_t sample_interval_ms; /* 采样周期 */
uint8_t brightness; /* 屏幕亮度 */
} SysConfig;
/* 备忘录:保存到 Flash 的快照(加魔数和 CRC 校验) */
typedef struct {
uint32_t magic; /* 魔数:用于判断 Flash 是否有效 */
SysConfig cfg;
uint32_t crc; /* CRC32 校验 */
} ConfigMemento;
/* 计算 CRC32(简化版) */
static uint32_t CalcCrc32(const uint8_t *data, uint32_t len)
{
uint32_t crc = 0xFFFFFFFFu;
for (uint32_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
crc = (crc >> 1) ^ (0xEDB88320u & -(crc & 1u));
}
}
return ~crc;
}
#define CFG_MAGIC 0xCFGA5A5u
/* 创建备忘录:把当前配置打包成可持久化的快照 */
void Config_SaveMemento(const SysConfig *cfg, ConfigMemento *out)
{
out->magic = CFG_MAGIC;
memcpy(&out->cfg, cfg, sizeof(SysConfig));
out->crc = CalcCrc32((const uint8_t *)cfg, sizeof(SysConfig));
}
/* 从备忘录恢复:校验失败返回 false */
bool Config_RestoreFromMemento(SysConfig *cfg, const ConfigMemento *m)
{
if (m->magic != CFG_MAGIC) { return false; }
uint32_t crc = CalcCrc32((const uint8_t *)&m->cfg, sizeof(SysConfig));
if (crc != m->crc) { return false; }
memcpy(cfg, &m->cfg, sizeof(SysConfig));
return true;
}
/* 使用:上电时从 Flash 读取,校验失败则用默认值 */
void App_LoadConfig(SysConfig *cfg)
{
ConfigMemento m;
/* Flash_Read(CONFIG_ADDR, &m, sizeof(m)); */
if (!Config_RestoreFromMemento(cfg, &m)) {
/* 校验失败:使用默认值 */
cfg->temp_threshold = 500; /* 50.0℃ */
cfg->sample_interval_ms = 1000;
cfg->brightness = 80;
}
}
容易踩的坑
- 必须有魔数 + CRC。Flash 掉电写一半、首次上电全 0xFF,没有校验就拿到一堆垃圾值。
- 版本兼容 。结构体一改,老设备的 Flash 数据就废了。加
version字段,迁移时按版本升级。 - Flash 写入前先擦除。NOR Flash 只能 1→0,写前必须 Sector Erase。
7. 观察者模式(Observer)
是什么
定义对象间一对多依赖:当一个对象状态变化时,所有依赖它的对象都自动收到通知。也叫"发布-订阅"。其实第 5 节的事件总线就是一种观察者模式的实现------这里再给一个更直接的"主题-观察者"版本。
用在哪
- 传感器数据订阅(一个采集源,多个消费者:显示、上传、告警)
- UI 数据绑定
- 系统状态变化通知
配图:观察者模式类图

C 代码:温度采集主题 + 多观察者
c
#include <stdint.h>
/* 观察者接口 */
typedef struct Observer {
void (*on_update)(struct Observer *self, float value);
struct Observer *next; /* 链式订阅 */
} Observer;
/* 主题:温度采集器 */
typedef struct {
Observer *head; /* 订阅链表头 */
float current_temp;
} TempSubject;
void TempSubject_Attach(TempSubject *s, Observer *o)
{
o->next = s->head;
s->head = o;
}
/* 状态变化时通知所有观察者 */
void TempSubject_SetValue(TempSubject *s, float value)
{
s->current_temp = value;
Observer *o = s->head;
while (o != NULL) {
if (o->on_update != NULL) {
o->on_update(o, value);
}
o = o->next;
}
}
/* ---------- 三个具体观察者 ---------- */
typedef struct { Observer base; } DisplayObserver;
typedef struct { Observer base; } UploadObserver;
typedef struct { Observer base; } AlarmObserver;
static void Display_OnUpdate(Observer *self, float value)
{
(void)self;
/* TODO: LCD_ShowTemp(value); */
(void)value;
}
static void Upload_OnUpdate(Observer *self, float value)
{
(void)self;
/* TODO: MQTT_Publish("temp", value); */
(void)value;
}
static void Alarm_OnUpdate(Observer *self, float value)
{
(void)self;
if (value > 50.0f) {
/* TODO: Buzzer_On(); */
}
}
void App_SetupObservers(TempSubject *s)
{
static DisplayObserver disp = { Display_OnUpdate, NULL };
static UploadObserver up = { Upload_OnUpdate, NULL };
static AlarmObserver alm = { Alarm_OnUpdate, NULL };
TempSubject_Attach(s, &disp.base);
TempSubject_Attach(s, &up.base);
TempSubject_Attach(s, &alm.base);
}
容易踩的坑
- 观察者与中介者的区别:观察者是"一个主题通知多个订阅者";中介者是"任意模块互相通信"。简单一对多用观察者,多对多用中介者。
- 通知顺序不保证。如果观察者之间有依赖(比如告警必须在采集后),别靠链表顺序,用显式优先级或拆成两个事件。
- 取消订阅。如果订阅者是临时对象,记得从链表摘除,否则下次通知调到野指针。
8. 状态模式(State)
是什么
把对象的不同状态封装成独立的状态对象,状态切换时行为自动跟着变,用多态替代巨大的 switch-case。嵌入式里协议状态机、按键状态机、充电状态机到处都是。
用在哪
- TCP/Modbus/自定义协议状态机
- 按键消抖状态机(按下、抖动、长按、释放)
- 电池充电流程(待机、预充、恒流、恒压、满电)
- 电梯、自动门控制系统
配图:按键状态机状态图

C 代码:按键状态机(带长按检测)
c
#include <stdint.h>
#include <stdbool.h>
/* 状态机事件 */
typedef enum {
EVT_KEY_DOWN = 0,
EVT_KEY_UP,
EVT_TICK_10MS
} KeyEvt;
/* 前置声明 */
typedef struct KeyFsm KeyFsm;
/* 状态接口:每个状态实现一组事件处理函数 */
typedef struct {
void (*on_event)(KeyFsm *fsm, KeyEvt evt);
const char *name;
} KeyState;
/* 状态机上下文 */
struct KeyFsm {
const KeyState *cur; /* 当前状态 */
uint16_t hold_ms; /* 已经按下的毫秒数 */
bool long_pressed;
};
/* 切换状态 */
void KeyFsm_Transition(KeyFsm *fsm, const KeyState *next)
{
fsm->cur = next;
fsm->hold_ms = 0u;
}
/* ---------- 状态:空闲 ---------- */
static void StateIdle_OnEvent(KeyFsm *fsm, KeyEvt evt);
static const KeyState StateIdle = { StateIdle_OnEvent, "Idle" };
static void StateIdle_OnEvent(KeyFsm *fsm, KeyEvt evt)
{
if (evt == EVT_KEY_DOWN) {
/* 进入按下状态 */
/* KeyFsm_Transition(fsm, &StatePressed); */
}
}
/* ---------- 状态:按下(判断长按) ---------- */
static void StatePressed_OnEvent(KeyFsm *fsm, KeyEvt evt);
static const KeyState StatePressed = { StatePressed_OnEvent, "Pressed" };
static void StatePressed_OnEvent(KeyFsm *fsm, KeyEvt evt)
{
switch (evt) {
case EVT_TICK_10MS:
fsm->hold_ms += 10u;
if (fsm->hold_ms >= 1000u && !fsm->long_pressed) {
fsm->long_pressed = true;
/* TODO: 触发长按事件 LongPress_Callback(); */
}
break;
case EVT_KEY_UP:
if (!fsm->long_pressed) {
/* TODO: 触发短按事件 ShortPress_Callback(); */
}
fsm->long_pressed = false;
/* KeyFsm_Transition(fsm, &StateIdle); */
break;
default:
break;
}
}
/* 状态机初始化 */
void KeyFsm_Init(KeyFsm *fsm)
{
fsm->cur = &StateIdle;
fsm->hold_ms = 0u;
fsm->long_pressed = false;
}
/* 外部事件入口 */
void KeyFsm_Handle(KeyFsm *fsm, KeyEvt evt)
{
if (fsm->cur != NULL && fsm->cur->on_event != NULL) {
fsm->cur->on_event(fsm, evt);
}
}
容易踩的坑
- 状态爆炸。状态超过 10 个就该考虑分层状态机(HSM),参考 QP/qpnano 框架。
- 状态切换时别忘记清理上下文 。
hold_ms这种临时计数器,切换状态时必须清零,否则下次进来直接命中长按。 - 每个状态都要处理所有事件。漏掉一个事件,状态机就卡死。建议用状态-事件矩阵表先画清楚再写代码。
经验 :复杂状态机强烈推荐画状态图。工具推荐用
PlantUML或draw.io,画完再写代码,bug 少一半。
9. 策略模式(Strategy)
是什么
定义一系列可互换的算法,每个算法独立封装,运行时可灵活切换。算法的变化不影响使用方。
用在哪
- 滤波算法切换(移动平均、中值、卡尔曼)
- 编解码切换(CRC8/CRC16/CRC32)
- 不同传感器的换算公式
- 排序算法选择
配图:策略模式类图

C 代码:温度滤波策略
c
#include <stdint.h>
/* 策略接口 */
typedef struct {
float (*filter)(struct FilterStrategy *self, float sample);
void (*reset)(struct FilterStrategy *self);
} FilterStrategy;
/* ---------- 策略 A:移动平均 ---------- */
#define MAVG_WIN 8
typedef struct {
FilterStrategy base;
float buf[MAAVG_WIN];
uint8_t idx;
uint8_t count;
} MovingAvgFilter;
static float MovingAvg_Filter(FilterStrategy *self, float sample)
{
MovingAvgFilter *f = (MovingAvgFilter *)self;
f->buf[f->idx] = sample;
f->idx = (f->idx + 1u) % MAVG_WIN;
if (f->count < MAVG_WIN) { f->count++; }
float sum = 0.0f;
for (uint8_t i = 0; i < f->count; i++) {
sum += f->buf[i];
}
return sum / (float)f->count;
}
static void MovingAvg_Reset(FilterStrategy *self)
{
MovingAvgFilter *f = (MovingAvgFilter *)self;
f->idx = 0u;
f->count = 0u;
for (uint8_t i = 0; i < MAVG_WIN; i++) { f->buf[i] = 0.0f; }
}
/* ---------- 策略 B:中值滤波 ---------- */
#define MED_WIN 5
typedef struct {
FilterStrategy base;
float buf[MED_WIN];
uint8_t count;
} MedianFilter;
static float Median_Filter(FilterStrategy *self, float sample)
{
MedianFilter *f = (MedianFilter *)self;
/* 滑动窗口:左移,最新值放末尾 */
for (uint8_t i = 0; i < MED_WIN - 1u; i++) {
f->buf[i] = f->buf[i + 1u];
}
f->buf[MED_WIN - 1u] = sample;
if (f->count < MED_WIN) { f->count++; }
/* 简单冒泡排序取中值(窗口小,够用) */
float tmp[MED_WIN];
for (uint8_t i = 0; i < f->count; i++) { tmp[i] = f->buf[i]; }
for (uint8_t i = 0; i < f->count - 1u; i++) {
for (uint8_t j = 0; j < f->count - 1u - i; j++) {
if (tmp[j] > tmp[j + 1u]) {
float t = tmp[j]; tmp[j] = tmp[j + 1u]; tmp[j + 1u] = t;
}
}
}
return tmp[f->count / 2u];
}
/* ---------- 使用方:温度采集器,运行时切换策略 ---------- */
typedef struct {
FilterStrategy *strategy; /* 持有当前策略 */
} TempSensor;
float TempSensor_Read(TempSensor *s)
{
float raw = 0.0f; /* TODO: ADC_Read(); */
return s->strategy->filter(s->strategy, raw);
}
void TempSensor_SetStrategy(TempSensor *s, FilterStrategy *st)
{
s->strategy = st;
}
容易踩的坑
- 策略切换时记得 reset。换滤波器前不清空历史窗口,前一个策略的残留数据会污染新策略的输出。
- 别为只有 2 个算法上策略模式 。一个
if-else就够了,3 个及以上再上。 - 策略对象别频繁创建销毁。用静态实例,运行时只切换指针。
10. 模板方法模式(Template Method)
是什么
在"父类"中定义算法的整体骨架,把可变步骤延迟到"子类"实现。C 没有继承,但可以用 函数指针 + 结构体组合 模拟:骨架函数定义流程顺序,具体步骤通过回调注入。
用在哪
- 驱动初始化流程(开时钟 → 配置 GPIO → 配置外设 → 使能中断)
- 传感器采集流程(复位 → 配置 → 触发转换 → 读取 → 换算)
- 通信协议流程(连接 → 握手 → 收发 → 断开)
配图:模板方法流程时序

C 代码:传感器驱动模板
c
#include <stdint.h>
#include <stdbool.h>
/* 抽象步骤:每个具体传感器实现这组回调 */
typedef struct {
bool (*reset)(void);
void (*config)(uint8_t mode);
void (*trigger)(void);
uint16_t (*read_raw)(void);
float (*convert)(uint16_t raw);
const char *name;
} SensorOps;
/* 模板方法:定义统一的采集流程骨架,子步骤由 ops 决定 */
bool Sensor_TemplateAcquire(const SensorOps *ops, float *out)
{
if (ops == NULL || out == NULL) { return false; }
/* 1. 复位 */
if (ops->reset != NULL && !ops->reset()) {
return false;
}
/* 2. 配置 */
if (ops->config != NULL) {
ops->config(0u);
}
/* 3. 触发转换 */
if (ops->trigger != NULL) {
ops->trigger();
}
/* 4. 读取原始值 */
uint16_t raw = 0u;
if (ops->read_raw != NULL) {
raw = ops->read_raw();
}
/* 5. 换算 */
if (ops->convert != NULL) {
*out = ops->convert(raw);
} else {
*out = (float)raw;
}
return true;
}
/* ---------- 具体传感器 A:DS18B20 ---------- */
static bool Ds_Reset(void) { /* TODO: 1-Wire 复位 */ return true; }
static void Ds_Config(uint8_t m) { (void)m; /* 配置精度 */ }
static void Ds_Trigger(void) { /* 触发温度转换 */ }
static uint16_t Ds_ReadRaw(void) { return 0x01FFu; /* 示例 */ }
static float Ds_Convert(uint16_t r) { return (float)r * 0.0625f; }
static const SensorOps g_ds18b20 = {
Ds_Reset, Ds_Config, Ds_Trigger, Ds_ReadRaw, Ds_Convert, "DS18B20"
};
/* ---------- 具体传感器 B:内部 ADC ---------- */
static bool Adc_Reset(void) { return true; }
static void Adc_Config(uint8_t m) { (void)m; /* ADC 通道配置 */ }
static void Adc_Trigger(void) { /* 启动转换 */ }
static uint16_t Adc_ReadRaw(void) { return 0x0800u; /* 示例 */ }
static float Adc_Convert(uint16_t r) {
return (float)r * 3.3f / 4095.0f; /* 12 位 ADC,3.3V 参考 */
}
static const SensorOps g_adc = {
Adc_Reset, Adc_Config, Adc_Trigger, Adc_ReadRaw, Adc_Convert, "ADC"
};
/* 使用:同一套流程,不同传感器 */
void App_Demo(void)
{
float temp, voltage;
Sensor_TemplateAcquire(&g_ds18b20, &temp);
Sensor_TemplateAcquire(&g_adc, &voltage);
}
容易踩的坑
- 骨架别太长。模板方法超过 30 行,说明流程本身有问题,应该拆分。
- 回调要做 NULL 检查 。某个步骤可选时,必须
if (ops->step != NULL)保护,否则裸指针调用直接 HardFault。 - 流程顺序是约定。模板方法的核心价值就是"流程不可变",使用方不能跳过某步------所以骨架函数不能给使用方重写的机会。
11. 访问者模式(Visitor)
是什么
把"数据结构"和"操作"分离,在不修改元素类的前提下,为元素新增操作。适用于数据结构稳定、但操作频繁扩展的场景。
嵌入式里用得相对少,但 设备树多维度统计、协议字段多格式导出、AST 遍历 这类场景能看到。C 实现比较绕,因为要做"双重分派",举个轻量例子。
用在哪
- 设备树多维度遍历(统计数量、打印信息、生成报告)
- 协议字段导出(JSON、二进制、文本多种格式)
- 调试器多视图展示
配图:访问者双重分派时序

C 代码:设备列表多操作访问
c
#include <stdint.h>
/* 前置声明 */
typedef struct DeviceNode DeviceNode;
typedef struct DeviceVisitor DeviceVisitor;
/* 设备类型枚举 */
typedef enum {
DEV_SENSOR = 0,
DEV_ACTUATOR,
DEV_COMM,
} DevType;
/* 设备节点(元素) */
struct DeviceNode {
DevType type;
const char *name;
DeviceNode *next;
/* 双重分派:把 visitor 传给自己,再让 visitor 调用自己的具体方法 */
void (*accept)(DeviceNode *self, DeviceVisitor *v);
};
/* 访问者接口:每种设备类型一个访问方法 */
struct DeviceVisitor {
void (*visit_sensor)(DeviceVisitor *v, DeviceNode *d);
void (*visit_actuator)(DeviceVisitor *v, DeviceNode *d);
void (*visit_comm)(DeviceVisitor *v, DeviceNode *d);
void *ctx; /* 访问者私有上下文,比如统计计数器 */
};
/* 各类型 accept 实现 */
static void Sensor_Accept(DeviceNode *self, DeviceVisitor *v)
{
if (v->visit_sensor != NULL) { v->visit_sensor(v, self); }
}
static void Actuator_Accept(DeviceNode *self, DeviceVisitor *v)
{
if (v->visit_actuator != NULL) { v->visit_actuator(v, self); }
}
static void Comm_Accept(DeviceNode *self, DeviceVisitor *v)
{
if (v->visit_comm != NULL) { v->visit_comm(v, self); }
}
/* ---------- 具体访问者 A:统计设备数量 ---------- */
typedef struct {
DeviceVisitor base;
uint16_t sensor_cnt;
uint16_t actuator_cnt;
uint16_t comm_cnt;
} CountVisitor;
static void Count_Sensor(DeviceVisitor *v, DeviceNode *d)
{
(void)d;
CountVisitor *cv = (CountVisitor *)v;
cv->sensor_cnt++;
}
static void Count_Actuator(DeviceVisitor *v, DeviceNode *d)
{
(void)d;
CountVisitor *cv = (CountVisitor *)v;
cv->actuator_cnt++;
}
static void Count_Comm(DeviceVisitor *v, DeviceNode *d)
{
(void)d;
CountVisitor *cv = (CountVisitor *)v;
cv->comm_cnt++;
}
/* ---------- 具体访问者 B:打印设备信息 ---------- */
static void Print_Sensor(DeviceVisitor *v, DeviceNode *d)
{
(void)v;
/* Log_Debug("[Sensor] %s", d->name); */
(void)d;
}
static void Print_Actuator(DeviceVisitor *v, DeviceNode *d)
{
(void)v;
/* Log_Debug("[Actuator] %s", d->name); */
(void)d;
}
static void Print_Comm(DeviceVisitor *v, DeviceNode *d)
{
(void)v;
/* Log_Debug("[Comm] %s", d->name); */
(void)d;
}
/* 遍历设备链表,对每个节点执行 accept */
void DeviceList_Traverse(DeviceNode *head, DeviceVisitor *v)
{
DeviceNode *p = head;
while (p != NULL) {
if (p->accept != NULL) {
p->accept(p, v);
}
p = p->next;
}
}
容易踩的坑
- 数据结构频繁变化时别用 。新增一种设备类型,所有访问者都得改------这是访问者模式最大的代价。结构稳定、操作多变才用,反之用别的方式。
- C 里双重分派写起来啰嗦 。评估收益:如果操作种类不多(≤2 种),直接在每个节点写
print()、count()方法更简单。 - 优先考虑用函数指针表替代 。如果设备类型用枚举,可以用
func_table[DEV_TYPE]直接查表,比双重分派轻量得多。
总结:行为型模式怎么选型?
速查表:
| 问题 | 推荐模式 |
|---|---|
| 一个事件多个模块关心 | 观察者 (一对一多) / 中介者(多对多) |
| 协议解析、AT 响应处理 | 责任链 |
状态多、switch-case 巨长 |
状态模式 |
| 算法需要运行时切换 | 策略 |
| 流程固定,步骤可变 | 模板方法 |
| 按键 / 命令需要排队 | 命令 |
| 集合遍历要统一接口 | 迭代器 |
| 参数需要掉电保存 | 备忘录 |
| 自定义小语言解析 | 解释器(慎用) |
| 数据结构稳定,操作多变 | 访问者(慎用) |
- 状态机和观察者是嵌入式工程师的"看家模式",先吃透这两个,能解决一半以上的交互问题。
- 解释器和访问者在嵌入式里用得少,别强求,遇到再学。
- C 语言实现模式要"神似"而非"形似"。没有类就用结构体 + 函数指针,没有继承就用组合,没有多态就用 vtable。重点是思想,不是语法糖。
- 模式是工具不是 KPI 。一个三层菜单你给它套组合 + 装饰器 + 访问者,那不是工程,是炫技。简单优先,痛了再上模式。
好的代码,不是用满了 23 种模式,而是看起来"平平无奇"------每层各管各的,改一个功能不用满世界找。模式只是达成这个目标的工具,不是目标本身。