☰
AI协同开发STM32程序实用流程:任务拆解、代码审查与测试兜底
2026/10/12 23:00:53 网站建设 项目流程

现在做嵌入式开发的,很难绕开一个话题:要不要用AI写STM32程序。我最初是拒绝的——寄存器位看错了谁负责?中断时序乱了我怎么查?后来被一个紧急项目逼着试了几次,发现真正的问题不在AI,而在使用AI的方式。大多数人拿了需求就整个工程甩给AI,让它一口气生成全部代码,然后被错误代码折磨一整天,最后得出“AI不适合嵌入式”的结论。我自己也踩过一模一样的坑。

真正让AI发挥价值的做法,不是让它替你想,而是把整个开发流程拆开,让AI在每一段流程里做它擅长的事:代码骨架、驱动模板、状态机逻辑、单元测试、错误处理。这篇就聊聊我怎么把“AI协同开发STM32程序的流程”跑成一个可复现的步骤,踩过哪些坑,以及每一步该给AI喂什么、审什么、测什么。适合正在用AI做单片机开发、但总觉得效率上不去的朋友参考。

1. 先用一句话说清:AI在STM32开发里到底站在哪个位置

1.1 我的真实经历:你以为AI在写代码,其实它在帮你省体力活

我第一次用AI写MCU代码时的场景很典型:想做一个多路传感器采集,我直接把需求描述发给AI,让它“写完整工程”。它还真给了我一份像模像样的代码,结构完整,注释整齐,甚至带了FreeRTOS任务。结果一编译,报错几十个,多半是芯片型号对应的外设地址不对、中断服务函数名拼错、时钟树配置和启动文件不匹配。我当时差点把键盘拍碎。

后来我换了个思路。不要求AI一步到位交付整个系统,而是把系统中的单个环节拆出来:先让它帮我写某个外设的初始化骨架,再让它补一个具体功能函数,最后让它生成对应的单元测试。同样一个AI,效果天差地别。因为小任务出错的概率低,出错了我也能快速定位甚至手改。之前那种大而全的生成,错误是交织的,排查成本极高。

这段经历让我意识到一个关键结论:AI协同开发的目的不是“让AI全自动交付软件”,而是把重复性、模式化、文档化的劳动交给AI,把判断、验证、时序相关的决策留给自己。它更像一个实习工程师,写东西快,但不清楚你的硬件约束,你必须给它边界。

1.2 AI的强项与雷区:哪些能交给它,哪些必须自己把关

我刚接触AI写代码时,总想让AI做“最聪明”的事,比如设计整个系统的架构。但AI在架构层面往往过于泛化,不能结合你的芯片、外设、中断优先级、功耗限制做深度权衡,容易给出理想但不可落地的方案。反之,像寄存器初始化的“固定套路”、驱动函数的模板、注释补全这类事,AI做得又快又好。

下面这张表是我个人在实际项目中总结的,分享给各位参考:

适合交给AI的任务不适合完全交给AI的任务
GPIO/UART/I2C/SPI等外设的初始化代码骨架中断延迟评估与实时性分析
驱动函数的常规编写与参数校验低功耗策略的制定
状态机框架搭建与状态转换逻辑硬件时序问题的定位
数据格式转换、校验算法、CRC工具寄存器级配置的最终确认
单元测试与测试代码生成硬件bug的排查与根因分析
注释补全、文档生成、代码格式化芯片具体型号的选型与硬件设计沟通

这表格不是绝对的,但有参考意义。把表格里“适合”的交给AI,能省下至少三分之一的工作量;“不适合”的部分,AI最多给你一个初稿或建议,最终拍板必须靠自己。举个例子,AI写出一个定时器初始化函数,时基参数对不对,你要结合时钟树实际配置来判断,而不是直接信任它的注释“默认时钟80MHz”。

2. 开工前的磨刀:为AI准备一套可运行的嵌入式工作台

2.1 模型选择:云端通用大模型还是本地开源小模型

想做好嵌入式AI编程,第一步不是写提示词,而是选工具。我试过几种路线,逐个说下感受。

第一类是云端通用大模型。这类模型对STM32、HAL库、寄存器操作的理解比较扎实,能处理很长的上下文,尤其擅长看代码片段后做修改建议。缺点是代码可能涉及保密项目时,上传代码有合规顾虑,以及依赖网络。第二类是本地部署的开源小模型。好处是代码不出设备,离线可用,适合军工、医疗器械这类对保密要求极高的场景。坏处是参数量小,对嵌入式专用知识的掌握不如云端模型,需要更多的提示词细节去弥补。第三类是代码专用模型,生成代码的语法正确率相对高,但对“芯片手册”“寄存器描述”这类自然语言夹杂技术资料的内容理解有限。

我的建议是:如果没有保密要求,优先用云端大模型做日常开发;如果必须离线,就选本地小模型,但一定要有“喂资料”的意识——把芯片手册、工程现有代码、风格规范一并喂进去,否则本地模型就是半个瞎子在写代码。

2.2 给AI立规矩:用规范文件约束命名、接口和输出格式

很多开发者的工程代码风格各异,有人的函数名用大写驼峰,有人的变量名加前缀,有人把外设句柄全局化,有人坚持传参。AI第一次生成的代码不一定合你的风格,直接收编会污染整个工程。所以我现在的做法是:在工程根目录放一个“编码规则说明”文件,例如此类文件,专门描述命名规则、文件头注释格式、错误处理约定、代码块边界要求。

这个文件就是给AI的“上岗培训”。每次和AI对话时,不需要重复发一大堆规则,只要说“请先阅读工程目录下的编码规则说明,再按照里面的规范生成代码”。实测下来,AI输出风格能和原有工程保持九成以上的一致性。连不同项目的“性格”都能模拟——比如项目A的代码必须是纯寄存器操作不许用HAL,项目B恰恰相反,靠一个规则文件就能轻松切换。

规则文件里我通常会写这些要点:

  • 命名规范:函数名、变量名、宏定义的前缀规则。
  • 接口约定:错误码统一返回类型,参数必须加合法性检查。
  • 注释要求:文件头写明作者、功能、修改历史;函数注释说明参数、返回值和硬件依赖。
  • 禁止事项:严禁使用动态内存、严禁在中断里做耗时循环、严禁直接操作未映射地址。
  • 输出要求:产出完整可编译的代码块,不省略任何实现细节,不出现伪代码。

2.3 上下文投喂法:把芯片手册和工程结构变成AI的“记忆”

AI没有“记忆库”,它的所有知识都来自预训练和当前对话内容。你如果不告诉它你用的是哪款芯片、哪个开发环境、哪个HAL库版本,它可能会按照训练数据里最常见的某系列芯片来生成代码,结果某些寄存器和中断向量根本对不上。这就像让一个工程师开发代码但不告诉他硬件是什么,他能写,但很可能全错。

我的上下文投喂格式如下,基本每次都复用:

  • 工程目录结构:把关键文件列表、路径结构粘贴出来,让AI知道现有代码的组织方式。
  • 芯片与工具链信息:具体芯片型号、工作主频、开发环境、编译器版本。
  • 现有代码依赖:本次修改涉及的文件,粘贴全部内容或关键片段,并标明行号范围。
  • 数据手册内容:如果涉及外设寄存器操作,把对应的寄存器描述章节贴一段进去,尤其注意寄存器位的说明。
  • 任务目标:用短句描述这次要完成的事,越具体越好。

这些信息一次性给足,比挤牙膏式的一问一答要高效得多。我刚开始喜欢开对话时只写“帮我配一下定时器”,然后来回补信息,结果AI每次理解都不一样,生成的代码版本飘来飘去。后来学乖了,干脆写一封“任务邮件”,把所有相关信息放在开头,AI收到的第一版回复往往就是高质量的。

3. 任务拆解的艺术:把一个大功能切成AI能消化的颗粒度

3.1 为什么拆解失败率极高:上下文长度与注意力的物理限制

这部分是我的经验教训,曾让我浪费了整个周末。当时我让AI“写一个完整的温湿度传感器采集模块,包含初始化、读取、超时重试、数据转换、历史记录”,结果它写出来一个500行的文件,逻辑本身是完整的,但第一步初始化就没有处理芯片的I2C超时标志,导致后面所有读取函数的错误处理都建立在错的前提上。我去问AI,它却坚持说代码没问题,直到我贴出芯片手册里I2C状态寄存器的位定义,它才承认错误。

AI的问题在于它其实没有执行能力。它处理长任务时,注意力容易在中间位置衰减,后面写的东西可能忘记前面定下的约束。你让它一口气写10个函数,后5个往往在风格和逻辑上偏离前5个。这种“注意力漂移”是模型架构决定的,不是靠提示能完全解决的。

所以我现在严格执行一条规则:一次对话只让AI做一件事。一件事的标准是“单一函数或单一模块”,比如:

  • 写一个I2C读函数,不要顺带把DMA也配了。
  • 写一个按键消抖的状态机,不要顺带把指示灯逻辑也做了。
  • 写一个Modbus CRC校验函数,不要顺带把整个协议栈写了。

3.2 一次对话只做一件事:从“写个温湿度采集系统”到“初始化一下I2C”

举例说明。假设我需要开发一个温湿度采集系统,传统写法是把需求整体扔给AI:

这样写的提示词效果很差。AI会把一大堆功能搅在一起,看似全面实则处处有漏洞。

我的拆解流程是这样的:

  1. 先列功能清单:I2C初始化、温湿度传感器读时序、数据格式转换、超时重试、日志输出。
  2. 每次选其中一项作为一次对话的任务。
  3. 每一项都带上“前置条件”和“验收标准”,告诉AI“现有初始化代码已经完成,不要改动它,你只需要补充读取函数”。

比如“I2C初始化”这个任务的提示词是:

这样拆完以后,每个任务都比较小,AI生成时间短、出错率低、容易审查。而且单个任务失败时,不会影响其他已完成的任务,我可以只重做那一个模块。

3.3 提示词里的嵌入式要素:芯片后缀、时钟频率、编译器和约束条件

嵌入式领域写提示词和互联网后端完全不一样。你写一个Web API,AI用通用知识就能应付;但嵌入式代码和芯片绑定极深,芯片型号不同、工具链不同,代码就是两回事。我在提示词里几乎每个任务都会带上这几个要素,尽量写得像一份硬件设计需求:

  • 芯片具体型号:比如某系列某具体型号,以及它的Flash/RAM容量,避免AI在启动文件配置上犯错。
  • 时钟配置:系统时钟频率、外设总线频率,AI在算定时器重载值、波特率分频时要靠这些。
  • 编译器与优化选项:不同编译器对位域、volatile处理有差异,AI需要知道用哪个语法。
  • 硬性约束:不允许用动态内存、不允许阻塞延时超过100ms、中断必须短小快速等。
  • 验收标准:代码建议能通过-Wall -Wextra编译,函数需要返回错误码,变量命名需符合规则文件。

这些信息拼接在提示词里,形如:

AI收到这种提示词,写出来的代码风格就像是在这个工程里干了半年的人。对比一下,什么都不给直接让它写,绝大多数时候出来的代码都带着“通用教材”味道,根本不敢直接放进产品里。

4. 实际走一遍:AI协同开发一个外部中断按键状态机

4.1 第一轮对话:生成GPIO外部中断的初始化与回调骨架

我拿一个颇具代表性的需求举例:按键检测。直接轮询按键电平很简单,但实际产品里经常要求用外部中断唤醒、软件消抖,还需要根据按键状态切换系统模式。在真实项目里,实现这个功能的代码通常分散在几个模块里。

第一轮我让AI只做“GPIO外部中断的初始化和回调骨架”。我的提示词如下:

AI生成的结果大概是这样的形状(简化后):

static void BUTTON_EXTI_Config(void) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin = GPIO_PIN_0; gpio.Mode = GPIO_MODE_IT_FALLING; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &gpio); HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn); } void EXTI0_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0); } void HAL_GPIO_EXTI_Falling_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == GPIO_PIN_0) { Buttons_SetEvent(BUTTON_EVENT_SHORT_PRESS); } }

结构上第一眼没什么问题,但我不急着信任。看这个代码,我第二眼要核查的是引脚号、时钟门控、中断优先级这三个关键点是否和硬件设计保持一致。把硬件原理图拿过来一对比,AI写的GPIOA引脚0和实际按键接的PE2完全对不上。这种情况非常普遍,AI不负责硬件原理,所以提示词里最好直接写明“按键接在GPEx的某些引脚上,低电平有效”,让AI从源头就开始按你的硬件来写。

4.2 第二轮对话:加入按键防抖逻辑与状态切换

第一轮生成的代码只是检测到下降沿就上报事件,实际产品要防抖。防抖有几种方案:延时消抖、定时器扫描、外部RTC唤醒扫描。做低功耗产品一般都不用阻塞延时,而是用定时器判断电平持续状态。我把这个思路告诉AI,让它接着第一轮的接口,而不改变原来外设初始化部分。

提示词:

AI给出的典型方案是用一个软件定时器,在按键触发外部中断后启动计时,定时器中断到来时读取当前电平,连续两次都稳定才认为按键有效。这个方案思路正确,但AI很容易在代码里引入“全局变量满天飞”的问题。比如它可能写出一个模块级全局变量保存按键状态,还可能在中断回调里直接调用耗时的防抖函数。碰到这种情况,我会要求它把所有状态封装在结构体里,并保证中断回调只置位标志,不做耗时操作。

输出骨架:

typedef struct { uint8_t debounce_state; uint16_t stable_count; uint8_t key_event; } KeyDebounce_t; void KeyDebounce_OnExti(void) { KeyDebounce_StartTimer(10u); } void KeyDebounce_OnTimerTick(void) { if (HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET) { if (++KeyDebounce_Inst.stable_count >= 3u) { KeyDebounce_Inst.key_event = KEY_EVENT_PRESSED; KeyDebounce_StopTimer(); } } else { KeyDebounce_Inst.stable_count = 0u; KeyDebounce_StopTimer(); } }

这里有个我常踩的坑:AI写计时器任务时,默认会期待你在主循环里调用“KeyDebounce_OnTimerTick”,但它不会主动标注清楚调用频率和上下文。所以第二轮对话结束时,我习惯让AI在代码注释里写清楚:“该函数需在1ms定时中断中周期调用,或在RTOS的1ms tick任务中调用”,否则后来者很容易接错位置。

4.3 第三轮对话:把状态机抽成独立模块并补全错误处理

按键事件有了之后,下一个自然需求是根据按键切换系统工作模式:待机、运行、休眠。这是典型的状态机,AI写状态机框架得心应手。这轮对话我会要求它独立出这样一个模块,头文件里只暴露初始化、事件注入、周期运行三个接口。

提示词内容:

AI生成的状态机文件会包含:

typedef enum { SYS_MODE_STANDBY, SYS_MODE_RUNNING, SYS_MODE_SLEEPING, SYS_MODE_COUNT } SysMode_t; typedef struct { SysMode_t current_mode; uint32_t mode_entry_tick; uint8_t transition_error; } SysStateMachine_t; void SysStateMachine_Init(SysStateMachine_t *sm); void SysStateMachine_HandleEvent(SysStateMachine_t *sm, KeyEvent_t event); void SysStateMachine_Tick(SysStateMachine_t *sm);

到这里,AI的作用基本就是按照我们约定的状态转移表生成代码,逻辑本身没难度,但它能把繁琐的switch-case、边界条件、非法转移处理都补齐。真正需要我做的,是把状态转移表明确地画出来给AI,比如“待机态收到短按进入运行态,运行态收到长按进入休眠态,休眠态收到任意按键回到运行态”,越完整越不会错。

这个过程也验证了一个经验:AI更适合处理“明确规则的执行”,而不是“模糊规则的定义”。规则你来定,执行交给它,配合才是正确的AI协同开发节奏。

5. 审代码比写代码更重要:AI生成结果的反诈与调试

5.1 编译告警一条不留:那些“看起来能过”的隐患

AI代码最大的问题是“看起来对,实际有隐患”,而编译告警是第一个过滤网。很多人觉得代码能编译就万事大吉,但嵌入式开发里很多编译告警恰恰指向严重问题。比如“变量可能未初始化”,在单片机里就是随机值跑飞的前奏;“函数声明与定义不匹配”,在C语言里可能造成栈被破坏。

我建议在工程里把编译告警级别拉满:-Wall -Wextra -Wpedantic,对GCC系工具链还可以加-Wshadow、-Wconversion。AI生成代码后,第一步就是先过编译,不是随便过,而是零告警。如果AI对告警无动于衷,直接把它返回的告警信息贴回去,让它解释原因并修复,在多数场景下,它给出的修复方案质量也相当不错。

有一段经历可以分享。之前AI生成过一笔DMA传输函数,编译无错,但程序跑起来后数据总是不稳定。后来打开编译器生成的map文件才发现,DMA描述符被放在了不该放的内存段,AI完全没有考虑内存对齐和DMA访问规则。编译通过只是底线,不代表正确。

5.2 寄存器配置逐一核对:AI最爱在这里“一本正经胡说”

我看AI写的代码,最警惕的就是寄存器操作部分。原因很好理解:AI在预训练时吸收太多芯片的资料,很容易把不同系列甚至不同厂商的寄存器配置混在一起。你在聊天里看不出它记混了,因为位名称和偏移看着都很像,但编译的时候寄存器宏根本不存在,或者存在但语义完全不同。

举例来说,让它写某个外设的中断使能,它可能把对应系列A的中断寄存器直接套用,导致中断行为完全错乱。我处理这个问题的办法是“强制性来源”:在提示词里明确要求AI“每个关键寄存器配置后面,用注释标注这一位的功能和依据,例如来自数据手册的哪一章节”。这个要求听起来啰嗦,但效果很好。AI在需要解释理由时,明显会更谨慎,减少了胡编的概率。

人工审查时也不能偷懒,我的核对流程是:

  1. 查芯片型号,确认AI用的是不是当前工程指定芯片的寄存器名称。
  2. 查时钟树,确认外设时钟门控的宏与实际使用的总线一致。
  3. 查中断向量表,确认中断服务函数名在启动文件里真实存在。
  4. 查外设基地址,确认寄存器偏移量正确、位域定义正确。

这些操作看起来费时间,但比在硬件上跑飞后再排查快得多。AI负责快,你负责准,这才是搭档。

5.3 在线调试三板斧:断点、外设寄存器视图、时序波形

代码审完、编译零告警,也过不了最后一关:上板子真跑。AI代码在语法层面对并不意味着行为正确,尤其涉及外部中断和边沿触发时,必须在线调试观察实际情况。我的调试三板斧是固定的:

第一斧,断点法。在回调函数、中断服务函数入口打断点,确认事件真的触发并走到了这个函数。第二步,查看调用栈,确认是硬件中断进来了,还是某个定时器轮询调进来的,这两种路径允许的操作完全不同。

第二斧,外设寄存器视图。在线调试器基本都有外设寄存器窗口,可以实时看GPIO的输入数据寄存器、中断标志位、中断屏蔽寄存器的状态。我之前遇到过一次按键偶尔失灵,AI代码逻辑没问题,但查看寄存器后发现中断标志根本没被清除。原因是AI漏掉了一句“清中断标志”的API调用,单看代码根本发现不了。

第三斧,时序波形。外设行为层面的问题,例如I2C时序不满足传感器要求、SPI时钟极性不对,用逻辑分析仪抓波形比看寄存器视图直观得多。我通常让AI先生成一份“期望时序说明”,再对照逻辑分析仪抓到的实际波形逐段核对。AI在这件事上给的帮助是快速解读波形参数,但抓波形、判断采样点还是要靠自己的硬件调试功底。

6. 测试你的AI代码:用模拟器和硬件清单把风险兜住

6.1 没有硬件也能跑逻辑:模拟器的编译与单元测试

很多人觉得嵌入式代码必须上板才能测,其实不然。纯逻辑部分,比如状态机、CRC计算、数据格式转换、协议解析,完全可以抽出来在本地PC上编译成可执行文件跑单元测试。我用过几种方案,最简单的是把源代码文件加上一个测试驱动main函数,用普通GCC在本地直接编译运行,断言验证结果。

以刚才的按键状态机为例,我在本地建了一个测试工程,把状态机模块的.c和.h文件拷贝进来,写了一个测试驱动:模拟连续按键事件,检查状态是否正确转移。整个测试不需要任何硬件,跑起来只要几毫秒,能发现绝大多数逻辑错误。

测试驱动的大致结构:

#include <assert.h> #include "sys_state_machine.h" int main(void) { SysStateMachine_t sm; SysStateMachine_Init(&sm); assert(sm.current_mode == SYS_MODE_STANDBY); SysStateMachine_HandleEvent(&sm, KEY_EVENT_PRESSED); assert(sm.current_mode == SYS_MODE_RUNNING); SysStateMachine_HandleEvent(&sm, KEY_EVENT_LONG_PRESS); assert(sm.current_mode == SYS_MODE_SLEEPING); return 0; }

这是我现在最推荐的做法:AI写完一个模块,就顺手让它给这个模块写一个可独立运行的主机测试用例,你在PC上先跑通逻辑,再烧进芯片。这一步能把“代码逻辑错误”和“硬件问题”两大变量彻底分开,让后续上板调试的复杂度降低一大半。

6.2 上板验证清单:时钟、外设门控、中断优先级、看门狗

逻辑测试过了,不代表硬件没问题。上板验证我永远按照一张固定清单逐项勾选,每一项都和AI生成的内容有关,而不是来了就跑main函数看现象。

首先是时钟系统,确认系统时钟频率和代码注释里写的一致,特别是PLL配置、分频系数,用示波器看MCU时钟输出引脚或定时器PWM频率即可验证。其次是外设门控,AI经常会漏掉开外设时钟的宏,比如用了某个定时器却不调用定时器时钟使能,上电后读寄存器全是0,代码执行到一半就死掉。

然后是中断优先级,AI生成的NVIC配置可能和系统其他中断冲突,尤其在有RTOS的场景下,中断优先级分组不统一会导致调度异常。这块靠代码review比在线调试更有效,因为是优先级配置问题,不踩到临界点往往看不出来。最后是看门狗——如果AI代码里涉及长阻塞循环,且你没关掉看门狗,那程序会在某个正常运行路径上被反复复位。这类问题在AI生成的代码里尤其常见,因为它不知道你的看门狗配置有多严格。

我建议把这张清单打印出来贴在工位上,每次提交给硬件测试前都过一遍,能减少一半以上的现场调试时间。

6.3 回归工作流:每次AI改动都必须过一遍自动化编译

最后一个关键习惯是“防回退”。AI协同开发的代码改动频率远比纯手工高,因为每次对话调整都可能产生新版本,如果没有自动化手段,很容易出现“上次改好了,这次改坏了还没发现”的情况。

我的做法是在工程里放一个自动化编译脚本,用命令行工具链编译整个固件,并连接单元测试。每次AI做完一轮修改,我会让脚本跑一遍,出错就立刻回滚。这个脚本相当于一道自动门禁,AI可以随便迭代,但只要编译测试不过,改动就不会进入主分支。

脚本核心逻辑很简单:

# 固件编译 build_tool -j8 -Werror -Wall -Wextra if [ $? -ne 0 ]; then echo "build failed" exit 1 fi # 主机单元测试 gcc test/test_key_state_machine.c src/key_state_machine.c -o test_runner ./test_runner

一旦这套机制建好,AI的开发速度优势才能真正释放。因为它每次改完代码,我能快速知道好坏,而不是被一堆隐藏回归问题拖住。这个工作流磨合顺畅后,整体开发效率比纯手工写代码大约提升一倍不止,尤其在做驱动适配、代码迁移这类重复性强的任务时效果显著。

最后再分享一条我自己的使用习惯:AI生成的每个文件,我都会在文件头注释里标注“由AI初稿+人工审查修订”以及日期,这样后期排查问题时能快速定位是否有AI生成成分,决定排查策略。这不是对AI不信任,而是对硬件工程严谨性的基本尊重。嵌入式开发终究要面对物理世界,AI再怎么聪明,最后按下复位键的依然是开发者的判断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询