• 正文
  • 相关推荐
申请入驻 产业图谱

让AI写状态机不出 bug:先要表格,再要表驱动,最后要穷举测试

09/21 08:20
186
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

嵌入式开发中,让 AI 辅助时,只要需求里出现状态两个字——通信协议解析、传感器采集流程、低功耗模式切换、BLE 连接管理、bootloader 流程——AI 就开始表演自由发挥。

生成的代码可能不太符合我们的预期:状态漏了、转移漏了、该报错的地方默默忽略。它跑起来像那么回事,出事在三个月后。

这次分享我们摸出来的四步做法:先表后码 → 穷举事件 → 表驱动 → 从表生成测试。文末有一段可以直接复制的提示词模板。

1. 先表后码:让 AI 先整理思路

不要一上来就让 AI 写代码。第一步永远是:

先不要写代码。把这个状态机的所有状态、所有事件、所有转移列成一张表,包括错误转移和异常处理。我确认之后你再实现。

为什么要这一步?

因为表格比代码好审 10 倍。审代码时,我们的注意力被语法、寄存器操作、边界条件分走了大半;审一张状态转移表时,我们只需要确认四个问题:

    有没有漏掉的状态?有没有漏掉的事件?有没有漏掉的转移?每个转移的目标状态和动作,对不对?

举个例子。一个低功耗传感器采集的状态机,需求里只说了"待机、采集、上传",AI 交上来的表大概长这样( 表示转移, 表示显式忽略,! 表示非法事件,应记录并断言):

状态\事件 启动采集 采集完成 上传完成 总线错误 超时
IDLE → SAMP !
SAMP → UPLD → ERROR → ERROR
UPLD → IDLE → ERROR → ERROR
ERROR → SAMP !

表格确认了,代码只是表格的机械翻译。翻译这一步,AI 很擅长。

但先别急着确认。 这张表至少少了三样东西:

动作→ SAMP 的时候,要不要给传感器上电、清 FIFO?表驱动里,动作才是业务逻辑的落点。

低功耗态和终态:掉电唤醒、低电量休眠走哪条路?错误一直重试,有没有尽头?

错误恢复策略ERROR 收到"启动采集"就直接回 SAMP,是自动重试还是等外部命令?重试几次放弃?

补齐之后就顺眼了:

状态\事件 启动采集 采集完成 上传完成 总线错误 超时 低电量
IDLE → SAMP ! → SLEEP
SAMP → UPLD → ERROR → ERROR → SLEEP
UPLD → IDLE → ERROR → ERROR → SLEEP
ERROR → SAMP * ! → SLEEP
SLEEP → IDLE

2. 穷举,不要举例

给 AI 举例子,它可能只会处理我们举的例子。

"采集流程有待机、采集、上传这几个状态"

如果我们这样描述,AI 大概率不会主动问我们:

掉电唤醒算不算一个事件?

I2C NACK 之后是重试还是进错误态?

低电量时是直接休眠还是先把手头的数据存进 Flash?

上一节那张表,最后就是被"低电量 → SLEEP"这种谁都没提过的事件补完整的。嵌入式场景下,要特别把这几类容易被 AI 遗忘的硬件事件写进提示词:

列出所有状态和所有事件。特别注意,事件来源包括但不限于:

命令类:上位机命令、按键(含机械抖动、长按短按)、通信协议包;

硬件类:总线错误(NACK、超时、CRC 错)、传感器异常、掉电/欠压检测(PVD)、复位(含看门狗复位);

时间类:软件定时器超时、协议要求的应答超时、重试间隔;

异步类:中断到达(数据就绪、DMA 完成)、其他任务/线程发来的消息。 每个状态遇到每个事件时的行为(转移 / 显式忽略 / 报错)必须定义,不允许留默认。

输出表格后自查一遍,把你拿不准、需要我确认的格子单独列出来。

最后那句"把拿不准的格子列出来"很关键:AI 的沉默不等于它没有疑问,它只是默认替你做了决定。

3. 表驱动

确认完表格,接下来怎么写代码?

很多人的直觉是让 AI 生成一个大 switch (state),里面嵌套 if (event == ...)。AI 写出来大概是这个样子:

能用,但这是状态机腐烂的开始——switch 分支散落在几个函数里,三个月后需求一变,改了东忘了西。更要命的是 AI 生成的 switch 经常只在部分 case 里更新状态,其余路径的状态保持旧值,这种 bug 审代码时极难发现。

正确姿势是表驱动(table-driven):转移表是数据,执行器是代码。

(三张图依次是转移表定义、动作函数、只查表的执行器。)

这套结构对嵌入式的额外好处:

const 转移表落进 .rodata,只要链接脚本把它指向 Flash,就不占 RAM(函数指针数组、状态变量、事件队列仍然占 RAM,别把这句话理解成"整套机制零开销")。

加状态、加事件,只改表,不动执行器

表可以独立审查,甚至可以当作接口文档发给同事。

4. 从表生成测试,再让 AI 反向对表

第一,从表生成穷举测试。

好处是联动:表改了,测试跟着变,漏掉的格子立刻变红。

第二,让 AI 反向输出一张表来对。

这是最便宜、也最有效的一招:

读一遍你刚才实现的执行器和转移表,反向输出一张"状态 × 事件"表,格式与第一步完全一致。然后把它和第一步的表逐格 diff,逐条列出不一致的格子,先不要修改代码。

为什么有效?因为正向生成时,AI 顺着表格机械翻译;反向生成时,它必须真的去读代码

5. 提示词模板

把上面几条串起来,就是这样一个完整的工作流提示词:

环境:____(裸机 / RTOS / 嵌入式 Linux)
我要在 MCU 上实现一个状态机,
请严格按以下步骤来,每一步完成后
停下来等我确认,不要跳步。

【第一步】只输出一张状态转移表,
不写代码。表头:状态 × 事件 →
目标状态 + 动作。要求穷举:
1. 所有状态,含错误态、低功耗态、终态;
2. 所有事件,至少包括:
   - 命令类:上位机命令、按键(含抖动、
     长按短按)、通信协议包;
   - 硬件类:总线错误(NACK / 超时 /
     CRC 错)、传感器异常、掉电欠压(PVD)、
     复位(含看门狗复位);
   - 时间类:软件定时器超时、协议应答
     超时、重试间隔;
   - 异步类:中断就绪(数据就绪 / DMA
     完成)、其他任务或线程发来的消息。
3. 每个 (state, event) 组合的行为:
   转移、显式忽略、或报错,不留默认。
4. 输出后自查一遍,把拿不准、
   需要我确认的格子单独列出来。

【第二步】等我确认表格后,
用 C 表驱动风格实现:
- 转移表用 const 二维数组(进 Flash),
  动作用函数指针;
- 执行器只查表,无业务判断;
- ISR 只往事件队列入队,状态修改
  只发生在唯一执行上下文,共享数据
  用临界区 / 原子操作保护;
- 禁止 malloc、禁止 C++ 异常,
  栈和队列全部静态分配。

【第三步】生成 PC 端可跑的穷举测试
(Unity / CppUTest):
- 每个状态 × 每个事件,
  断言结果与表一致;
- mock 所有 action,验证副作用;
- 覆盖事件队列的满 / 空边界。

【第四步】反向校验:
读一遍你写的实现,反向输出同样
格式的状态 × 事件表,与第一步的
表逐格 diff,列出所有不一致的格子,
先不要改代码。

____ 处换成自己的运行环境;粘贴给 AI 时不用管这里的折行。)

6. 总结

AI 不会替我们把需求想清楚,但它非常擅长把想清楚的东西机械地翻译成代码和测试。我们要做的,是先把那张表逼出来。

相关推荐

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!