1. 当我把嵌入式框架的键盘交给 AI 之后
嵌入式开发有个老生常谈的尴尬:代码量不大,但每一行都长在硬件上,牵一发动全身。一个跑了三年的老框架,里面塞满了各种历史补丁、寄存器魔数、状态机分支,想重构又不敢动,因为一动就可能炸出一堆时序问题。我这次做的事情,就是把这个"不敢动"的框架,交给 AI 去重构,自己几乎没手写一行代码。
先说清楚这个框架是什么。它是一个典型的裸机嵌入式框架,跑在一颗 Cortex-M 系列的 MCU 上,核心模块包括:一个基于时间片的任务调度器、一套环形缓冲区驱动、一个状态机引擎、以及若干外设的抽象层。代码总量大概在八千行左右,C 语言写的,风格是那种"能跑就行"的祖传风格——全局变量满天飞,函数职责不清,注释和代码对不上。这套东西稳定运行了很久,但每次加新功能都像在雷区里跳舞。
我为什么要让 AI 来重构它?原因很直接:我自己重构过两次,两次都中途放弃。第一次重构到一半,发现调度器和缓冲区之间有隐式耦合,改了一处崩了三处;第二次重构引入了新的抽象层,结果中断响应时间从 2 微秒涨到了 8 微秒,实时性直接不达标。手工重构的成本太高,而且人的思维容易被现有代码结构绑架,很难跳出原来的框。
这次我换了个思路:把重构拆成"AI 能理解的任务",让 AI 去执行,我只做架构决策和验收。整个过程下来,我真正手写的代码不超过五十行,其余全部由 AI 生成,我再逐块审查和微调。这篇文章就把这套方法完整拆开讲,包括我怎么给 AI 描述需求、怎么切分任务、怎么验证 AI 的输出、以及踩过的那些坑。
适合读这篇的人:手上有历史遗留嵌入式代码、想重构又不敢动的开发者;对 AI 辅助编程感兴趣但不知道怎么用在底层开发上的人;以及任何想了解"人机协作重构"这套工作流的人。不需要你是 AI 专家,但需要你对 C 语言和嵌入式基础有基本概念。
2. 为什么嵌入式框架的重构比普通业务代码更难
2.1 硬件耦合让"等价重构"变成伪命题
普通业务代码重构,核心目标是行为等价——输入一样,输出一样,内部结构随便改。但嵌入式框架不一样,它的"行为"不只是逻辑输出,还包括时序、内存占用、中断延迟、功耗表现。你把一个函数拆成两个,逻辑上完全等价,但多了一次函数调用,栈深度变了,中断里的执行时间就可能超标。
我在这次重构里遇到的最典型问题,就是调度器的 tick 处理函数。原来的写法是一个大函数,里面顺序处理所有任务的时间片递减和就绪判断。AI 第一次重构时,很自然地把它拆成了"遍历任务列表"和"判断任务就绪"两个函数,逻辑上完全正确。但实测下来,tick 中断的处理时间从 1.8 微秒涨到了 3.2 微秒,因为多了一层函数调用和参数传递。对于 72MHz 的主频来说,这多出来的 1.4 微秒意味着每个 tick 多消耗约 100 个时钟周期,在高频 tick 场景下会明显影响其他中断的响应。
所以嵌入式重构的第一条铁律是:逻辑等价不等于系统等价。你必须在重构前就明确哪些指标是不能退化的,然后把这些指标作为验收标准交给 AI。我当时的做法是列了一张"不可退化清单":
| 指标 | 原值 | 允许偏差 | 测量方式 |
|---|---|---|---|
| tick 中断处理时间 | 1.8us | +10% | 示波器翻转 IO |
| 最大栈深度 | 512B | 不增加 | 栈填充法测量 |
| 静态 RAM 占用 | 4.2KB | 不增加 | map 文件 |
| 任务切换延迟 | 3.5us | +15% | 逻辑分析仪 |
这张表后来成了我审查 AI 输出的核心依据。每次 AI 给出一版重构代码,我都先跑一遍这几个指标,不达标就直接打回,根本不看逻辑对不对。这个顺序很重要——先过性能关,再过逻辑关,能省掉大量无效审查。
2.2 祖传代码里的"隐式契约"AI 看不见
老框架里最坑的东西,是那些没有写在任何地方的隐式契约。比如某个全局变量g_sys_tick,表面上只是个计数器,但实际上中断里会读它、主循环会写它、某个外设驱动会在特定条件下清零它。这种跨模块的隐式依赖,代码里没有任何注释说明,AI 读代码时也看不出来,因为它是"运行时才成立"的约束。
我踩的第一个大坑就在这里。AI 重构缓冲区模块时,把g_sys_tick的读写全部收敛到了一个 getter 函数里,看起来更规范了。但那个外设驱动里的清零操作被 AI 判定为"可疑的写操作",直接删掉了。结果就是系统跑几个小时后,tick 计数器溢出,任务调度彻底乱套。这个 bug 极其隐蔽,因为前几个小时完全正常。
后来我总结出一个方法:在让 AI 重构之前,先把所有隐式契约显式化。具体做法是,我自己花时间把代码里所有全局变量的读写点全部列出来,标注每个读写点的意图,形成一份"契约清单",然后把这份清单作为上下文一起交给 AI。这份清单不需要很正式,用注释形式贴在代码旁边就行,但它是 AI 理解代码真实意图的关键输入。
提示:隐式契约的挖掘没有捷径,只能靠人。AI 能帮你分析代码结构,但"这个变量为什么在这里被清零"这种问题,只有写过这段代码的人或者深入调试过的人才知道。重构前花在契约梳理上的时间,会在重构后成倍省回来。
2.3 没有测试框架,AI 的输出无法自动验证
业务代码重构有单元测试兜底,跑一遍测试就知道有没有改坏。但嵌入式框架通常没有测试框架,验证全靠上板实测。这就带来一个问题:AI 生成代码很快,但你验证的速度跟不上,重构效率反而下降。
我这次的做法是,在重构开始之前,先花半天时间搭了一个最小化的验证环境。不是完整的单元测试框架,而是几个针对性的验证手段:
- PC 端仿真验证:把不依赖硬件的纯逻辑模块(比如状态机、环形缓冲区)抽出来,在 PC 上用 gcc 编译,写几个测试用例跑一遍。这部分能覆盖大约 40% 的代码。
- 板上 IO 翻转验证:在关键路径的首尾加 IO 翻转,用示波器测时间。这部分覆盖时序相关的模块。
- 内存填充验证:在栈和堆区域填充特定模式,运行一段时间后检查填充被破坏的程度,测出实际栈深度。
- 长时间运行验证:让系统连续跑 24 小时,观察是否有计数器溢出、内存泄漏等问题。
这套验证环境搭起来花了半天,但后面每次审查 AI 输出时,我都能在几分钟内得到反馈,整体效率提升非常明显。没有这套东西,AI 重构就是"生成快、验证慢",最后变成瓶颈。
3. 把重构任务拆成 AI 能接住的粒度
3.1 任务拆分的三个层次:模块级、函数级、语句级
直接跟 AI 说"帮我重构这个框架",结果一定是灾难。AI 会给你一版看起来很美但完全跑不起来的代码。正确的做法是把重构拆成不同粒度的任务,不同粒度用不同的交互方式。
我的拆分是这样的:
模块级任务:比如"把环形缓冲区模块重写为无锁版本"。这种任务 AI 一次性完成的质量很差,因为涉及太多设计决策。我的做法是模块级任务只让 AI 做"方案设计",不写代码。我会让 AI 输出三套方案,每套方案说明数据结构、关键操作、优缺点,然后我来选。选定方案后,再进入函数级。
函数级任务:比如"实现环形缓冲区的写入函数,要求无锁、支持单生产者单消费者"。这种粒度 AI 完成得最好,因为输入输出明确,约束清晰。我会把函数签名、参数含义、返回值约定、性能要求全部写清楚,AI 生成的代码通常一次就能用。
语句级任务:比如"把这个 if-else 链改成查表实现"。这种任务其实不太需要 AI,因为改动太小,自己写更快。我一般只在遇到特别绕的逻辑时才用 AI,比如复杂的位运算或者状态机跳转表。
实际执行下来,函数级任务是性价比最高的。模块级太粗,AI 容易跑偏;语句级太细,沟通成本高于收益。函数级刚好卡在"AI 能理解完整意图"和"人能把控质量"的平衡点上。
3.2 给 AI 写"重构需求说明书"的模板
我发现一个规律:AI 重构的质量,八成取决于你怎么描述需求。描述得越像一份正式的接口文档,输出质量越高。我后来固定用一套模板来写需求,效果很稳定。模板包含六个部分:
- 背景:这个模块是干什么的,在系统里的位置,被谁调用。
- 现状问题:为什么要重构,现在有什么具体问题(越具体越好,比如"当前实现在中断里调用了 malloc,有重入风险")。
- 目标:重构后要达到什么状态,包括功能目标和性能目标。
- 约束:不能改变什么,比如"函数签名不能变,因为有三个调用方"、"不能引入动态内存分配"。
- 接口约定:输入输出的格式、边界条件、错误处理方式。
- 验收标准:怎么判断重构成功,包括功能验证和性能验证。
举个例子,我重构状态机引擎时的需求描述是这样的:
背景:状态机引擎负责管理设备的工作模式切换,当前支持 8 个状态、20 条转移。 现状问题:转移表用嵌套 switch 实现,新增状态需要改三处代码,容易漏改。 目标:改为表驱动实现,新增状态只需改一处。 约束:状态转移的执行时间不能超过 5us;不能增加静态 RAM 占用超过 64B。 接口约定:保持原有的 state_machine_run() 签名不变。 验收标准:8 个状态的所有转移路径实测正确;转移执行时间实测 < 5us。这份描述大概两百字,但信息密度很高。AI 拿到之后,生成的表驱动实现基本一次到位,我只改了两个边界条件的判断。
3.3 上下文怎么给:全量代码还是按需投喂
这是很多人纠结的问题。我的经验是:不要一次性把整个框架丢给 AI。原因有两个:一是上下文太长,AI 的注意力会被稀释,容易忽略关键约束;二是全量代码里有很多与当前任务无关的部分,会干扰 AI 的判断。
我的做法是"按需投喂 + 依赖闭包"。具体来说,重构某个函数时,我给它这几样东西:
- 这个函数的完整代码
- 这个函数直接调用的其他函数的签名和简要说明
- 这个函数被谁调用(调用点的代码片段)
- 相关的全局变量定义和契约说明
- 上面说的需求说明书
这样一份上下文通常在几百行以内,AI 能完整消化。如果某个函数依赖特别复杂,我会先把它依赖的部分也重构掉,形成"自底向上"的重构顺序。这样每一步的上下文都是干净的。
注意:按需投喂的前提是你自己对代码的依赖关系很清楚。如果你自己都不清楚某个函数被谁调用,那说明这个框架的耦合已经到了必须重构的程度,但同时也意味着你需要先花时间做依赖分析,而不是急着让 AI 动手。
4. 实战:调度器模块的 AI 重构全过程
4.1 重构前的调度器长什么样
原来的调度器是一个基于时间片的设计,核心结构是一个任务数组,每个任务有周期、剩余时间片、执行函数三个字段。tick 中断里遍历数组,递减剩余时间片,减到零就标记任务就绪。主循环里检查就绪标志,执行对应函数。
代码大概长这样(简化版):
typedef struct { uint16_t period; uint16_t remain; void (*func)(void); uint8_t ready; } task_t; task_t g_tasks[MAX_TASKS]; uint8_t g_task_count = 0; void scheduler_tick(void) { for (uint8_t i = 0; i < g_task_count; i++) { if (g_tasks[i].remain > 0) { g_tasks[i].remain--; if (g_tasks[i].remain == 0) { g_tasks[i].ready = 1; g_tasks[i].remain = g_tasks[i].period; } } } } void scheduler_run(void) { for (uint8_t i = 0; i < g_task_count; i++) { if (g_tasks[i].ready) { g_tasks[i].ready = 0; g_tasks[i].func(); } } }这段代码的问题很明显:任务数量固定、没有优先级、tick 里遍历所有任务效率低、ready 标志的读写没有保护。但它跑了很久没出问题,因为任务数量少、周期长,实际负载很轻。
4.2 我给 AI 的重构需求与约束
我的重构目标不是推倒重来,而是解决三个具体问题:支持动态添加任务、支持优先级、降低 tick 开销。需求说明书如下:
背景:调度器是系统的核心,负责按周期调度所有任务。当前实现见附件代码。 现状问题: 1. 任务数量编译期固定,新增任务要改宏定义并重新编译。 2. 无优先级,所有任务平等,高实时任务可能被低实时任务阻塞。 3. tick 里遍历全部任务,任务多时开销线性增长。 目标: 1. 支持运行时动态添加任务,最多 16 个。 2. 支持 4 级优先级,高优先级任务优先执行。 3. tick 开销与就绪任务数相关,而非总任务数。 约束: 1. tick 中断处理时间不超过 2us(原 1.8us)。 2. 不引入动态内存分配,任务数组静态分配。 3. 保持 scheduler_tick 和 scheduler_run 的调用方式不变。 接口约定: 新增 task_add(period, priority, func) 接口,返回任务 ID 或 -1。 验收标准: 1. 动态添加 16 个任务后系统正常运行。 2. 高优先级任务在低优先级任务之前执行。 3. tick 时间实测 < 2us。这份需求里,最关键的是"tick 开销与就绪任务数相关"这一条。它直接决定了数据结构的选择——不能用简单的数组遍历,得用就绪链表或者位图。AI 拿到这个约束后,选择了位图方案,我觉得合理。
4.3 AI 给出的三版方案与我的取舍
AI 第一轮给了三套方案,我简单说一下和我的判断:
方案 A:就绪位图 + 优先级位图。用一个 16 位的就绪位图标记哪些任务就绪,再用 4 个位标记哪些优先级有就绪任务。tick 里只更新位图,不遍历任务。主循环里用位扫描指令(CLZ)快速找到最高优先级就绪任务。优点是 tick 开销恒定,与任务数无关;缺点是需要位操作,代码可读性稍差。
方案 B:多级就绪链表。每个优先级一个链表,任务就绪时挂到对应链表。tick 里只操作链表头。优点是逻辑直观;缺点是链表操作在中断里做,有指针操作,风险稍高。
方案 C:时间轮。用时间轮管理周期任务,tick 只推进轮指针。优点是 tick 开销极低;缺点是实现复杂,且对非周期任务支持不好。
我选了方案 A。理由是:位图方案在中断里只做位设置,没有指针操作,最安全;位扫描指令在 Cortex-M 上是单周期指令,效率最高;代码虽然用了位操作,但逻辑其实很清晰,加好注释就行。方案 B 的链表操作在中断里有风险,方案 C 对现有任务模型改动太大。
这个取舍过程我没有让 AI 参与,因为这是架构决策,需要人对系统有整体判断。AI 适合给方案,不适合做决策。
4.4 代码生成后的逐块审查与微调
AI 按方案 A 生成的代码大概两百行,我逐块审查了一遍。审查的重点不是逻辑对不对(逻辑 AI 基本不会错),而是这几个方面:
第一,中断安全。tick 里对位图的写操作,和主循环里对位图的读操作,会不会有竞争?AI 的代码里,tick 只做g_ready_bitmap |= (1 << id),主循环读的时候先关中断再读。这个处理是对的,但我发现主循环里关中断的范围太大,把任务执行也包进去了。我改成只关中断读位图,读完立刻开中断,任务执行在中断开启状态下进行。
第二,边界条件。任务 ID 是 0 到 15,AI 的代码里位操作都用1 << id,当 id 是 15 时没问题,但如果将来扩展到 32 个任务,1 << 31在 int 上是未定义行为。我加了显式的(uint32_t)1 << id强制转换,避免隐患。
第三,性能实测。改完之后上板测 tick 时间,实测 1.6us,比原来的 1.8us 还快了一点。原因是原来遍历数组有循环开销,现在只做位操作。这个结果符合预期。
第四,可读性。AI 生成的位操作代码没有注释,我加了注释说明每个位图的含义。这部分我手写了几行注释,算是整个重构里我唯一手写的代码。
整个调度器重构,从写需求到验证通过,大概花了三个小时。如果手工重构,我估计至少要两天,而且大概率会引入新 bug。
5. 那些 AI 重构翻车的瞬间和我的补救
5.1 缓冲区模块:AI 把"无锁"理解成了"不用锁"
环形缓冲区模块的重构是我踩的最大的坑。原实现是单生产者单消费者,靠读写指针的原子性天然无锁。我在需求里写了"保持无锁特性",AI 理解成了"不需要任何同步机制",于是它把原来用于内存屏障的几行代码删掉了。
在 x86 上,这代码可能还能跑,因为 x86 的内存模型比较强。但在 Cortex-M 上,编译器的指令重排和 CPU 的乱序执行(虽然 M 系列乱序有限,但编译器重排是真实存在的)会导致读写指针更新顺序错乱,缓冲区数据出现撕裂。这个 bug 在低负载下几乎不出现,高负载下才偶发,极其难查。
我的补救方式是:在需求里明确写出"需要内存屏障"这个约束,并且把屏障的位置和原因写清楚。AI 第二次生成的代码就正确了。这件事给我的教训是:AI 不懂硬件内存模型,所有和硬件相关的约束都必须显式写出来,不能指望 AI 自己知道。
5.2 状态机模块:AI 自作主张加了"优化"
状态机模块重构时,AI 在生成表驱动代码的同时,自作主张加了一个"优化":把连续的同目标状态转移合并了。逻辑上这没错,但问题是,原状态机里有些转移虽然目标状态相同,但转移时的副作用不同(比如一个会打印日志,一个不会)。AI 合并之后,副作用丢了。
这个问题的根源是 AI 只看了状态转移表,没看转移动作函数。我在需求里只描述了状态和转移关系,没描述动作。补救方式是重新提交需求,把每个转移的动作函数也列出来,AI 就正确生成了。
这件事说明:AI 会做它认为合理的优化,但这些优化可能破坏你的隐式语义。防御方法是,在需求里明确写"不要做任何未要求的优化",把 AI 的自由度压到最低。
5.3 外设抽象层:AI 生成的寄存器操作"看起来对但时序不对"
外设抽象层的重构最惊险。AI 把一个外设初始化序列重写了,寄存器配置值都对,但顺序变了。原来先配时钟再配引脚,AI 改成了先配引脚再配时钟。在大多数情况下这没问题,但这颗 MCU 的某个引脚在时钟未使能时配置会失败,导致外设完全不工作。
这个 bug 上板一测就发现了,因为外设直接没反应。但如果是个更隐蔽的时序问题,可能就要查很久。补救方式是,在需求里明确写"保持原有寄存器操作顺序",并且把顺序的原因也写上。AI 之后就没再动顺序。
提示:外设初始化序列的顺序,很多是芯片手册里明确要求的,但也有一些是经验性的。让 AI 重构这部分时,最安全的做法是"只改结构不改顺序",把顺序相关的代码原样保留,只重构周边的逻辑。
5.4 从翻车中提炼的 AI 重构检查清单
踩了这几个坑之后,我总结了一份检查清单,每次审查 AI 输出时都过一遍:
| 检查项 | 检查方法 | 常见问题 |
|---|---|---|
| 中断安全 | 看中断和主循环共享的变量 | 缺临界区保护、关中断范围过大 |
| 内存屏障 | 看多核或中断共享数据的读写顺序 | 屏障被删、顺序被改 |
| 隐式语义 | 对照契约清单逐条核对 | 副作用丢失、隐式依赖被删 |
| 寄存器顺序 | 对照原代码逐行比对 | 顺序被"优化" |
| 边界条件 | 看所有数组下标和位操作 | 溢出、未定义行为 |
| 性能指标 | 上板实测 | 函数调用增多、栈深度增加 |
这份清单后来成了我审查 AI 输出的标准流程,每次过一遍大概十分钟,能拦下大部分问题。
6. 人机协作重构的边界:哪些该交给 AI,哪些必须自己来
6.1 适合交给 AI 的重构任务
经过这次完整的重构,我对"什么任务适合 AI"有了比较清晰的认识。适合 AI 的任务有几个共同特征:
逻辑密集但硬件无关。比如状态机的表驱动改造、缓冲区的索引计算、协议解析的状态跳转。这些任务逻辑复杂但纯软件,AI 处理得很好。
有明确输入输出的函数级重写。给清楚签名、约束、验收标准,AI 生成的代码质量很高,通常一次就能用。
重复性的模式改造。比如把十个类似的 if-else 改成查表、把散落的魔数提取成宏定义。AI 做这种批量改造又快又准。
代码注释和文档生成。AI 读代码生成注释的能力很强,而且比人写得快。我这次重构后,所有模块的注释都是 AI 生成的,我只做了少量修正。
6.2 必须自己把控的关键决策
反过来,有些事 AI 做不了,或者做不好,必须自己来:
架构决策。比如调度器用位图还是链表,这种决策需要对系统整体有判断,AI 给的方案只是参考,拍板必须是人。
性能取舍。当功能和性能冲突时,砍哪个保哪个,这是价值判断,AI 没有这个能力。
隐式契约的梳理。前面说过,AI 看不见运行时才成立的约束,这部分只能靠人。
硬件相关的时序和顺序。AI 不懂硬件,所有和硬件时序相关的部分,必须人工审查。
最终验收。AI 可以帮你验证逻辑,但系统级的验收——长时间运行、边界场景、异常恢复——必须人来设计和执行。
6.3 我的时间分配:AI 干了多少,我干了多少
最后说一下实际的时间分配,给想尝试这套方法的人一个参考。整个重构项目,我记录的时间大概是:
- 需求梳理和契约清单:4 小时(纯人工)
- 验证环境搭建:3 小时(纯人工)
- 与 AI 交互生成代码:约 6 小时(含多轮修改)
- 代码审查和微调:5 小时(人工为主)
- 上板验证和调试:4 小时(人工为主)
总计约 22 小时,其中纯人工部分约 16 小时,AI 生成部分约 6 小时。对比手工重构,我估计纯手工需要 40 小时以上,而且质量不一定更好。AI 的价值不在于"替你写代码",而在于"把写代码的时间压缩,让你把精力集中在架构和验证上"。
我手写的代码确实不超过五十行,主要是注释、边界条件修正、以及几处性能关键的微调。但"没手写代码"不等于"没干活"——需求梳理、契约挖掘、审查验证,这些工作量一点没少,只是从"写代码"变成了"审代码"和"想清楚"。
如果你也想让 AI 重构你的嵌入式框架,我的建议是:先从一个小模块开始,把上面这套流程走一遍,感受一下 AI 的能力边界。不要一上来就重构整个框架,那样大概率会翻车。等你在小模块上跑通了流程,再逐步扩大范围。这套方法的核心不是 AI 有多强,而是你能不能把任务拆到 AI 能接住的粒度,以及你能不能守住那些 AI 看不见的边界。