news 2026/7/2 19:50:09

嵌入式设计模式:从状态机到观察者,代码结构怎么搭才清爽

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式设计模式:从状态机到观察者,代码结构怎么搭才清爽

写嵌入式程序的人,很少会一开始就考虑"设计模式"这回事。毕竟MCU上资源有限,RAM按KB算,Flash按MB算,哪有PC上那些花里胡哨的抽象继承多态。但干过三五个项目之后,你会发现一个规律——代码写到后面最难维护的,从来不是某个算法写不出来,而是各个模块之间怎么组织、怎么通信、怎么扩展。

不妨从最常用的一个模式说起。

状态机:不只是switch-case

大部分人接触状态机都是从按键消抖开始的。但状态机真正的价值不在消抖,而在——它把一个随时间变化的行为,拆解成了"当前状态 + 输入事件 → 下一个状态"的确定性映射。

来看一个实际的例子。假设我们要管理一个IoT设备的网络连接生命周期,有这几个阶段:未初始化、WiFi扫描中、正在连接、已连接、断连重试。

typedef enum { NET_IDLE, NET_SCANNING, NET_CONNECTING, NET_CONNECTED, NET_RETRYING } net_state_t; typedef struct { net_state_t state; uint8_t retry_count; uint32_t last_event_time; void (*on_enter[NET_RETRYING + 1])(void); } net_fsm_t;

注意这里每个状态都挂了一个on_enter回调。这个设计的用意是——状态切换时自动触发进入动作,而不是在事件处理的switch里到处塞代码。我们来看状态转移怎么组织:

void net_fsm_run(net_fsm_t *fsm, net_event_t evt) { net_state_t next = fsm->state; switch (fsm->state) { case NET_IDLE: if (evt == EVT_START) next = NET_SCANNING; break; case NET_SCANNING: if (evt == EVT_AP_FOUND) next = NET_CONNECTING; else if (evt == EVT_TIMEOUT) next = NET_IDLE; break; case NET_CONNECTING: if (evt == EVT_CONNECTED) next = NET_CONNECTED; else if (evt == EVT_FAILED) next = NET_RETRYING; break; case NET_CONNECTED: if (evt == EVT_DISCONNECTED) next = NET_RETRYING; break; case NET_RETRYING: if (evt == EVT_RETRY_OK) next = NET_CONNECTING; else if (evt == EVT_GIVE_UP) next = NET_IDLE; break; } if (next != fsm->state) { fsm->state = next; if (fsm->on_enter[next]) fsm->on_enter[next](); } }

这段代码的精髓在于——状态转移表是显式写在switch里的,每个状态能响应哪些事件一目了然。后来改需求要加一个"获取IP地址"的中间状态,直接在NET_CONNECTINGNET_CONNECTED之间插一个NET_DHCPING,补上对应的事件处理和进入回调就行,不会波及别的逻辑。这就是状态机的核心价值——把分支逻辑收敛到一处,改一个状态不影响另外六个

有意思的是,很多开发者一开始图省事,直接把网络状态写成一堆if嵌套。三个状态以内还能撑住,到五个以上,那个嵌套的深度和分支的组合数,看一眼就想重构。

观察者模式:松耦合的关键

嵌入式里经常遇到这种场景:一个传感器采集到了新数据,好几个模块都要知道——LCD要刷新显示,云端要上传,本地日志要记录,阈值判断要触发报警。

最直接的做法是在采集完成的地方依次调用:

void sensor_data_ready(sensor_data_t *data) { lcd_update(data); cloud_upload(data); log_save(data); threshold_check(data); }

这个写法的问题在于——采集模块和显示、上传、日志、阈值判断全都耦合在一起了。哪天要加一个数据保存到SD卡的功能,得回来改采集模块的代码。哪天要关掉云上传,又得回来改。

观察者模式思路不同。采集模块不关心数据被谁用、怎么用,它只管"发布"一个事件:

typedef struct { void (*on_data)(sensor_data_t *); struct subscriber_t *next; } subscriber_t; static subscriber_t *head = NULL; void sensor_subscribe(subscriber_t *sub) { sub->next = head; head = sub; } void sensor_notify(sensor_data_t *data) { for (subscriber_t *p = head; p; p = p->next) { if (p->on_data) p->on_data(data); } }

之后每个关心数据的模块,各自实现一个回调函数,然后在初始化阶段注册进来:

static void on_lcd_update(sensor_data_t *d) { /* ... */ } static void on_cloud_upload(sensor_data_t *d) { /* ... */ } subscriber_t lcd_sub = { .on_data = on_lcd_update }; subscriber_t cloud_sub = { .on_data = on_cloud_upload }; void app_init(void) { sensor_subscribe(&lcd_sub); sensor_subscribe(&cloud_sub); }

这里写死了静态注册,因为MCU上裸机代码动态分配内存总让人不放心。实际上观察者模式在嵌入式里最常用的变体就是这种"链表+静态变量"的组合,够用,不引入malloc,零碎开销可控。

命令模式:把"操作"变成"数据"

回想一下你写过的串口命令解析。如果这样写:

void handle_uart_command(uint8_t cmd, uint8_t *args) { switch (cmd) { case CMD_SET_TEMP: set_temperature(args[0]); break; case CMD_SET_MODE: set_mode(args[0]); break; case CMD_GET_STATUS: send_status(); break; case CMD_REBOOT: system_reboot(); break; } }

功能和状态机的switch看起来类似,但这里有一个更本质的抽象——每个命令其实是一个"可延迟执行的操作"。命令模式的思想就是把一个操作封装成一个对象(在C里就是一个结构体),让它可以被排队、被记录、被撤销。

typedef struct { uint8_t id; uint8_t args[8]; uint8_t arg_len; uint8_t priority; } command_t; #define CMD_QUEUE_SIZE 16 static command_t cmd_queue[CMD_QUEUE_SIZE]; static uint8_t head = 0, tail = 0; int cmd_enqueue(command_t *cmd) { uint8_t next = (tail + 1) % CMD_QUEUE_SIZE; if (next == head) return -1; // 队列满 cmd_queue[tail] = *cmd; tail = next; return 0; } int cmd_dequeue(command_t *cmd) { if (head == tail) return -1; *cmd = cmd_queue[head]; head = (head + 1) % CMD_QUEUE_SIZE; return 0; }

这样一来,串口中断里只做一件事——把收到的命令塞进队列。主循环里再一条一条取出来执行。中断服务程序的时间降到了微秒级,不会再因为执行某个耗时的命令导致丢帧。而命令表本身可以做成一张静态表格,由不同模块各自注册,互不干扰。

这三个模式——状态机、观察者、命令队列——几乎是嵌入式项目里最通用的三种组织思路。状态机管行为,观察者管通信,命令队列管时序。三个组合起来,一个中等复杂度的嵌入式系统,代码结构就能理得相当清爽。

当然,设计模式不是银弹。一个几百行的传感器驱动硬套抽象层,反而本末倒置。但在模块数量超过五六个、交互关系开始让人头疼的时候,想一想"这个状态转移能不能用状态机收拢""这个通知关系能不能用观察者解耦""这个时序问题能不能用命令队列缓冲"——往往比硬撑着往下堆代码要省心得多。

你在项目里用过哪些设计模式?欢迎聊聊实际踩过的坑和摸索出的经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/2 19:49:14

EmbodiedClaw:对话式工作流如何革新具身AI开发范式

1. EmbodiedClaw:当具身AI学会“听”和“做”最近和几个做机器人和AI的朋友聊天,大家不约而同地提到了一个词:“具身智能”。这词儿听起来挺学术,但说白了,就是让AI不再只是屏幕里的代码,而是能通过物理身体…

作者头像 李华
网站建设 2026/7/2 19:47:57

GPT-4的1.8万亿参数与2%稀疏激活真相:MoE工程实践全解析

1. 项目概述:参数规模与稀疏激活的真相拆解“GPT-4 Has 1.8 Trillion Parameters. It Uses 2% of Them Per Token.”——这句话过去两年在技术社区反复刷屏,常被当作“大模型已突破人脑算力”的佐证,也被当成“AI即将失控”的警世恒言。但作为…

作者头像 李华
网站建设 2026/7/2 19:46:21

Mythos与Gated Release:大模型长程推理的可编程能力架构

1. 项目概述:一次被刻意“锁住”的能力跃迁 如果你最近关注大模型前沿动态,大概率在技术社区、AI从业者群或邮件列表里见过“TAI #200”这个编号——它不是某篇论文的DOI,也不是某个开源项目的Release Tag,而是The AI Alignment N…

作者头像 李华
网站建设 2026/7/2 19:45:58

大语言模型几何推理能力评估:表示形式敏感性与转换求解策略

1. 项目概述:当大模型遇上几何题最近在折腾本地部署的大语言模型时,我突发奇想,想看看这些动辄千亿参数的“智能大脑”,在面对我们中学时代最熟悉的几何证明题时,到底表现如何。这可不是简单的“看图说话”&#xff0c…

作者头像 李华