夜雨聆风学习资料网

ARTICLE · 1239773

让 AI 写状态机不出 bug 的操作流程!

让 AI 写状态机不出 bug 的操作流程!

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

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

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

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

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

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

为什么要这一步?

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

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

举个例子。一个低功耗传感器采集的状态机,需求里只说了"待机、采集、上传",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 不会替我们把需求想清楚,但它非常擅长把想清楚的东西机械地翻译成代码和测试。我们要做的,是先把那张表逼出来。

------------ END------------

STM32MP2双核内存共享实现原理!

分享一个闭环步进伺服软硬件开源项目

搭载MRAM单片机和传统Flash单片机有什么区别?

相关学习资料