前阵子接手一个活儿:给一套带OLED屏、按键输入、PWM调光和串口日志的STM32设备扩展功能,模块整体逻辑不算复杂,但涉及外设初始化、状态机迁移、中断优先级编排。按我早几年的习惯,先翻参考手册,再对照旧工程改代码,至少得搭进去一整天。这次我换了个流程,把需求拆成小块,让AI代码工具逐个生成初稿,我负责审查、整合、上板验证,结果从接到需求到板子跑通功能,两个多小时就完成了第一版。今天就把这套“AI协同开发STM32程序”的完整流程拆开讲一讲。
先说清楚这篇文章适合谁。如果你已经能独立用STM32CubeMX建工程、能看懂HAL库代码,但总觉得写业务逻辑和外设驱动很耗时;或者你刚入门嵌入式,想找一个“有人带你审代码”的加速方法,这篇文章应该能帮到你。我会从整体工作流讲到具体的提示词写法、编译报错排查、板级验证方法,再到团队协作层面的规范建议,全程以实际项目为例,尽量不落空话。
1. AI协同开发STM32的整体工作流:从“人写人看”到“AI写人审”
很多人一听说AI编程,第一反应是“把整个项目需求扔给AI,让它一口气生成所有代码”。这个思路在纯软件领域偶尔能跑通,但在STM32开发里几乎必翻车。原因很简单:嵌入式工程的代码不是跑在通用操作系统上,而是跑在具体的芯片、具体的引脚、具体的时钟树和外设组合上,AI没有你的原理图、没有你的CubeMX配置、也没有你板子的勘误手册,它只能基于“最常见的写法”去推测。
所以我理解的AI协同开发,核心不是“AI替我把代码写完”,而是“AI替我把初稿写好,我来做集成、审查、验证和兜底”。这套工作流可以分成六个阶段:
| 阶段 | 做的事 | AI角色 | 人的角色 |
|---|---|---|---|
| 需求拆解 | 把功能需求转换成可执行的任务清单 | 帮忙列任务、补边界条件 | 确认任务是否符合硬件约束 |
| 工程初始化 | CubeMX配置、时钟树、引脚分配 | 生成配置建议、工程结构说明 | 核对外设资源是否冲突 |
| 驱动与业务逻辑 | GPIO、定时器、UART、I2C等外设代码 | 按规格生成初始化函数和业务逻辑初稿 | 审查API正确性、补齐防错逻辑 |
| 编译调试 | 处理编译报错、链接错误 | 快速解释报错原因、给出修改建议 | 决定采纳还是重写 |
| 联调验证 | 板级功能测试、时序验证 | 生成自检程序、测试日志框架 | 用示波器、逻辑分析仪确认物理信号 |
| 沉淀归档 | 代码评审、文档整理 | 帮忙写注释、生成改动清单 | 最终确认和提交 |
这个流程走下来,我最深的体会是:AI的价值不在于“零失误”,而在于“把那些重复性的、模式化的编码工作吃掉”。比如给某个外设写初始化模板、把数据手册里的时序翻译成HAL库的延时设置、根据报错信息反查可能的拼写错误,这些活儿AI做起来比人快得多;而硬件调试、信号完整性、中断优先级这类需要结合具体电路和实时行为判断的环节,AI目前还差得很远。
还有一个容易忽略的点:增量式验证。把所有需求一次性扔给AI生成的代码,往往问题成堆,极难排查。正确做法是把功能拆成一个个可独立验证的小块。比如先让AI生成LED闪烁的GPIO控制代码,编译烧录确认灯亮了,再让它生成PWM调速,再接入传感器读取。每加一块功能,就上板验证一次,问题出现时能立刻定位到刚加入的那部分。这套思路无论是否用AI,都是嵌入式开发的基本功,但用了AI之后,验证频率要更高,因为AI生成的代码里,“看起来对但实际不对”的情况比人手写的更多。
2. 需求拆解与工程初始化:把模糊想法变成AI能执行的规格
AI协同开发的第一个卡点,不是写代码,而是描述需求。我见过很多同事用法不对,提示词写“帮我写一个温控风扇的程序”,AI确实能生成一坨代码,但既不确定用哪个型号,也不知道传感器挂在哪条总线上,更不知道风扇控制是PWM还是继电器,最后生成的东西基本没法用。
所以我在实际项目里,会先做一步“规格化描述”,把模糊想法变成AI能够执行的输入。举个例子,假设要做的是一个温控风扇控制器:NTC温度传感器采集温度,按键设置目标温度,OLED显示当前温度和风扇转速,MCU根据温差输出PWM控制风扇。我会把这串描述整理成下面这样的“项目画像”提示词:
项目背景:STM32温控风扇控制器 MCU型号:STM32F103C8T6,外部8MHz晶振,主频设为72MHz 外设资源: - ADC1通道0连接NTC分压电路,采样周期100ms - TIM3_CH1输出PWM驱动风扇,频率25kHz,PA6引脚 - I2C1连接0.3英寸OLED显示屏,地址0x3C - 两个独立按键:KEY1接PB0,KEY2接PB1,均配置为下拉输入 功能需求: 1. ADC采集NTC电压,通过查表或公式换算温度 2. 按键可调整目标温度,长按连续加减 3. OLED显示当前温度、目标温度、PWM占空比 4. 根据温差做分段PWM调速,温差越大转速越高 约束条件: - 使用STM32CubeMX生成的HAL库基础工程 - 业务逻辑写在用户代码区,不修改CubeMX自动生成的初始化段 - 温度换算表需给出采样电阻阻值与NTC B值把这个画像喂给AI,它才能真正开始干活。你会发现,越具体的描述,AI生成的代码越接近能直接编译的状态。如果你自己还没把需求想明白,AI只会把模糊的模糊还给你。
接下来是工程初始化。我的推荐做法是:**物理层配置用STM32CubeMX,业务层代码再交给AI。**为什么?因为引脚分配、时钟树、外设时钟使能这些信息,CubeMX能根据你选的芯片型号自动生成最匹配的初始化代码,这比AI“背记忆”可靠得多。AI对STM32F1的HAL库代码记忆往往混合了F0、F4甚至L0系列的写法,让它直接生成SystemClock_Config这种函数,翻车概率很高。而CubeMX生成的东西,几乎不会在时钟配置上出错。
其实AI在这一步也能帮上忙:把CubeMX生成的.ioc文件内容贴给AI,让它帮忙检查有没有外设资源冲突。我常用的一句提示词是:
我当前CubeMX工程配置如下:(贴入.ioc文件内容) 请检查以下内容: 1. 是否有引脚复用冲突 2. 是否遗漏了某个外设的时钟使能入口 3. 中断优先级分组设置是否合理 4. 给出你认为需要修改的配置项清单实测下来,AI对.ioc文件文本的解析能力不错,能发现一些低级冲突,比如“PA9被同时用作USART1_TX和TIM1_CH2”。但最终确认还是要靠你自己对照芯片引脚图,因为有些冲突AI不一定能意识到,比如某个引脚在板子上已经接了别的功能,而.ioc文件里看不出来。
工程初始化还有一个容易忽视的步骤:版本管理。用AI生成代码,一定要从一开始就用Git管理,哪怕是一个人开发。我会在仓库里建一个ai-generated分支,所有AI生成的初稿都先进这个分支,人工审查并修改完成后再合并到主分支。这样做的理由是:AI偶尔会突然抽风,改着改着代码结构完全变了,有分支做对比,随时能回滚到某个AI版本或某个手工版本。真要等到项目做了一半再建仓库,代码和AI输出混在一起,想分都分不开。
3. 外设驱动生成的实战细节:GPIO、定时器、UART到底怎么写提示词
这一章是全文的实操核心。STM32开发中90%的编码工作,其实是外设驱动的编写和调用。把这些环节拆细了讲,AI协同的效率优势体现得最明显,坑也最多。
先说GPIO控制。假设我要实现两个按键消抖和一个LED翻转,多数人写的提示词是“写一个按键消抖程序”,这个描述太简陋。我给AI的提示词一般长这样:
使用STM32F103C8T6,HAL库,主频72MHz。 两个按键:KEY1接PB0,KEY2接PB1,外部下拉电阻,按下为高电平。 LED接PC13,低电平点亮。 请实现: 1. 按键初始化函数,使用GPIO输入模式,无上下拉(外部已有下拉电阻)? - 注意:如果PB0/PB1默认悬空,请说明是否需要开启内部上下拉。 2. 消抖采用20ms延时消抖,但要求消抖期间不能阻塞主循环超过50ms, 因此给出基于systick的非阻塞消抖状态机伪代码。 3. LED翻转函数。 请给出完整可编译的.c/.h文件内容,并注明需要在CubeMX中配置的时钟和引脚。这个提示词有几个用意:一是明确MCU型号和HAL库,避免AI生成LL库或标准外设库的代码;二是把硬件接线说清楚,让AI知道外部已有下拉电阻;三是指出“非阻塞”和“不能阻塞主循环超过50ms”这类真实约束,AI就会避免给出delay循环的简单做法。实际生成结果通常是一个基于状态机的按键扫描函数,虽然AI写的状态机逻辑不一定完美,但骨架是能用的,我再根据自己项目的调度周期去调整。
接下来是定时器和PWM。PWM是AI比较容易出错的地方,但也是用AI最省时间的部分。我的提示词会写成:
STM32F103C8T6,TIM3_CH1输出PWM,引脚PA6,频率25kHz,ARR自动重装,占空比由变量duty_cycle控制,范围0-100。 请用HAL库生成定时器初始化函数和PWM占空比设置函数。 注意: - 不需要启动定时器中断,只输出PWM - 请说明PWM模式是向上计数模式还是中心对齐模式 - 计算预分频器和自动重装值,假设主频72MHz注意这里我特意写“不需要启动定时器中断”,因为AI有一定概率在初始化函数里加上HAL_TIM_PWM_Start_IT,如果你没有写中断回调函数,编译能过,程序也会死得莫名其妙。AI生成的PWM代码里,最容易翻车的地方是引脚复用映射。STM32F103的PA6确实是TIM3_CH1的默认复用功能,但如果你用的是别的型号,或者引脚映射不同,AI的“记忆”就会出错。我的习惯是每次拿到AI生成的复用代码,都打开CubeMX对比一下Manual Output PWM配置下系统给的那行GPIO_InitStruct.Alternate,两边一致才敢用。
UART相对简单,但有一个容易踩的坑:重映射fputc。AI生成串口发送函数时,往往建议你用printf重定向,然后给出一个简单的fputc实现。这在Keil的MicroLIB下没问题,但如果你用的是GCC工具链或者关掉了MicroLIB,fputc重定向就不生效,串口什么都打不出来。所以我在让AI写串口日志代码时,会明确要求“不使用printf重定向,直接用HAL_UART_Transmit封装一个log函数”。这样虽然输出格式化稍微麻烦一点,但可移植性更好,也省去排查“为什么串口没输出”的时间。
请用HAL_UART_Transmit实现一个串口日志函数,要求: - 串口1,波特率115200 - 提供UART_SendString、UART_SendHex、UART_SendDec三个函数 - 不使用printf重定向,不依赖MicroLIB - 注意HAL_UART_Transmit在发送大字符串时的Timeout参数设置最后说说I2C。STM32的I2C用HAL库时,最大的坑是地址匹配和时序超时。AI生成的I2C读写函数,经常把从设备地址的8位表达和7位表达混在一起。比如某款传感器数据手册写的是7位地址0x3C,但HAL_I2C_Mem_Read需要的是左移一位后的8位地址0x78,AI有时候会用0x3C直接去调用,结果总线上发的地址帧是错的,传感器永远不ACK。
所以涉及I2C时,我总会让AI先生成一段“总线扫描代码”,而不是直接生成某个传感器的读写函数。先在板子上跑扫描,看0x3C这个地址能否被正确响应,然后再让AI生成具体的传感器驱动。这个过程虽然多了一步,但能把I2C类问题从最底层找出来,而不是等整个驱动的代码写完再去debug。还有一个经验:让AI在I2C读写函数里加上超时重试机制,因为分立元件上拉电阻和总线电容的影响,很多传感器第一次通信时会失败,重试一次往往就成功了。
每当AI生成完一段驱动代码,我会做一轮“五件套审查”:时钟使能有没有、引脚复用模式对不对、外设句柄有没有传对、中断和DMA是否多余、返回值有没有被检查。这五条里命中任何一个,都值得停下来打开CubeMX或数据手册核对。AI生成的代码里最坑的不是编译错误,而是“逻辑自洽但对硬件不适用”的错误,这种错误只有靠人审才能拦截。
4. 编译报错与逻辑缺陷:AI诊断和人肉排查如何分工
STM32开发过程中,编译报错几乎每天都会遇到。以前的做法是自己一行行盯代码,或者去搜索引擎翻帖子。现在我会把完整报错信息直接丢给AI,但丢之前会做一个标准的“报错上下文组装”。直接贴一行红字是没有用的,AI再聪明也猜不到你的工程里发生了什么。我的标准格式是这样的:
编译环境:Keil MDK,AC6编译器,使用GCC的strict模式下 芯片:STM32F103C8T6,HAL库版本:STM32Cube_FW_F1_V1.8.5 报错信息(原文复制): ../Core/Src/main.c(45): error: #20: identifier "HAL_GPIO_WritePin" is undefined 相关代码片段: int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); while(1) { HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // 第45行 } } 我尝试过的操作: - 在CubeMX中重新生成GPIO初始化代码,确认MX_GPIO_Init存在 - 检查stm32f1xx_hal_gpio.c是否已添加到工程 请帮我分析可能原因,并给出解决步骤。把这样的上下文给AI,它通常能迅速锁定问题方向——可能是GPIO模块的源文件没参与编译,或者头文件路径没包含到Core/Inc。这种“给足上下文”的用法,比单纯贴一句“No such file or directory”高效得多。
但要说清楚,AI不是万能的调试器。对于“程序不按预期跑”这类逻辑缺陷,AI的判断能力明显下降。我遇到过这样一个例子:AI生成了一段按键控制PWM占空比的代码,逻辑看起来完全正常:按键按下,变量加10,超过100回0,然后调用占空比设置函数。但烧到板子上按键按了没反应。AI给出的建议是检查按键电平、检查GPIO配置、检查上拉电阻——这些都对,但都帮不上忙。最后我拿示波器量按键引脚,发现按下去电压只有0.7V,原来是板子的按键接到了另一个电源域,IO口需要配置为模拟输入才能正确采样。这种问题把整个对话记录给AI看,它也很难猜出来,因为根源在硬件设计层面。
所以我的分工原则是:**编译期问题,AI是主力;运行期问题,人是主力,AI只做知识补充。**比如HardFault,我让AI列出排查步骤可以,但实际要读懂寄存器里的LR、PC值,必须结合自己工程里编译出的符号表和反汇编结果。这里有个实操技巧:运行期出问题时,把Registers窗口的PC值、LR值、以及编译生成的.map文件或.axf反汇编列表喂给AI,让它对照分析这些地址落在哪个函数区间。AI虽然读不了你的调试器,但给它足够的静态信息,它还是能帮你缩小怀疑范围。
在排查逻辑缺陷时,我还推荐一种“二分注释法”,而且这个方法和AI协同并不冲突。如果一套功能由五个模块组成,运行结果不对,先注释掉后三个模块跑一次,如果正常,问题大概率在后面三个里;如果不正常,再注释掉前两个里的一个。每做一轮二分,可以把注释结果和现象喂给AI,看它能否根据“运行正常/不正常”的反馈推断问题点。这个方法比让AI一次性分析全部代码可靠得多。
还有一种更隐蔽的坑,我管它叫“僵尸代码”。AI在生成代码时,偶尔会输出一些看似合理的死代码段,比如一个根本不会被调用的中断回调函数、一个写入了某个不存在的寄存器的操作、或者定义了变量却从没使用过。这些代码编译时往往不报错,但会误导你后期的调试方向。我在一次项目中,AI在定时器初始化函数旁边生成了一段注释掉的“备用DMA配置”,我审查时没删,结果后来另一个同事接手,看到DMA相关代码以为DMA已启用,花了不少时间排查一个其实和DMA无关的功能。所以用AI生成的代码,审查时一定要留意“多余代码”,拿不准就删掉,尽量保持工程干净。
5. 从仿真到板级验证:AI写自检固件,人看示波器和逻辑分析仪
代码编译通过、下载到板子能运行,对嵌入式开发来说只算完成了一半。AI协同开发最大的隐患,就是编译和烧录都很顺利,但功能在硬件层面不正确。这时候需要一套系统性的板级验证方法。
我的做法是:让AI帮我生成“板级自检固件”,铺成一个完整的硬件自检框架。这个固件不实现最终业务逻辑,只做最基础的外设验证。最常见的是LED跑马灯遍历所有GPIO引脚,确保引脚电平输出正常;接着是串口回环测试,用一个跳线把TX和RX短接,然后让程序自发自收,验证USART硬件通路;再往后是I2C总线扫描,列出所有能响应的设备地址。这些自检程序逻辑简单、重复性高,非常适合AI生成,生成后我只需要核对引脚编号即可。
自检固件跑通之后,才开始接入真正的业务逻辑,但每接入一块,就要回到板子上去做对应验证。比如前面说的PWM,我先让AI生成一段“让PWM占空比从10%到90%每两秒循环”的测试程序,然后用示波器量电机驱动芯片输入的PWM波形,看频率是否真的是25kHz、占空比是否按预期跳变、上升沿有没有明显的振铃。这个环节AI替代不了,因为AI无法知道你的电机驱动芯片输入电流要求、电机本身电感特性、驱动的死区时间设置。但在AI生成测试程序之前,我会要求它给出“PWM频率设置的计算过程”,并解释为什么选择这个频率,这样至少能在我忘记算的时候帮我复核一遍。
逻辑分析仪在I2C和SPI验证中尤其重要。用AI生成I2C自检代码的同时,我会要求它把预期的总线时序描述一遍:起始位、设备地址、寄存器地址、数据字节、ACK信号出现的位置。然后把逻辑分析仪抓到的波形和AI的文字描述对照。看起来麻烦,但实际做下来效率反而高,因为AI的文字描述已经是结构化的,我只需要在波形上逐段找对应关系。如果发现第二个字节的ACK缺失,基本可以确认是地址匹配错误或者设备启动时间不够,这时候再带着波形数据的十六进制导出喂给AI分析,它能帮你快速定位是寄存器地址写错还是时序参数不匹配。
还有一种很实用的验证手段是“日志驱动调试”。让AI在业务逻辑的关键路径上插入带上下文的日志输出,格式尽量统一。比如:
[TIME] 12.345 | STATE_TEMP_SENSE | adc_raw=1024 temp=26.5C delta=-0.5C [TIME] 12.445 | STATE_PWM_UPDATE | duty=34 fan_rpm=3560这种日志一方面可用于确认状态机是否按预期流转,另一方面也能在上板验证时快速把故障定位到具体模块。AI极其擅长生成这种格式化日志代码,只要你在提示词里把“日志格式”和“输出位置”写清楚,几乎不用改。但有一点要注意:日志输出本身会占用串口时间和主循环时间,不能在高实时性要求的函数里每行都打日志,否则会影响时序。我的习惯是让AI把日志函数做成宏,通过一个开关在正式版本里彻底关闭。
在板级验证阶段,我自己会专门准备一张“验证记录表”,每一行记录一个验证项、对应的测试方法、预期结果和实测结果。这个方法不值钱但极其有效,因为AI协同开发时,代码改动频率高,有时候你改了这段代码,回头验证另一段功能时发现受影响。没有记录表,你很难判断“这个问题是我刚才改产生的,还是原本就存在”。我一般用Markdown表格记录,验证完一项勾一项,发现回退问题就能快速找到哪次改动引入了回归。
6. 把AI协同流程固化到团队:提示词模板、审查清单与避坑记录
AI协同开发如果只是个人偶尔用用,价值很难最大化。真正有效的方式是把这套流程沉淀成团队的公共资产。我自己在实践里做了三样东西:提示词模板库、AI生成代码审查清单、避坑记录文档。这三样东西听起来简单,却实实在在提高了整个小组的交付速度。
提示词模板库不需要很复杂,就是把项目里反复用到的几个“提示词场景”整理成固定格式。比如“外设驱动生成模板”“报错定位模板”“代码审查模板”“测试程序生成模板”。每个模板里固定好必须填写的字段:MCU型号、HAL库版本、引脚分配、时钟频率、功能描述、约束条件。这样团队里每个人拿到的AI输出质量下限都有保障,不会因为某个人提示词写得太简单而浪费大量返工时间。
审查清单比提示词模板更重要。AI生成的代码,无论看起来多规范,都应该按这个清单逐项过一遍。我和我同事在实践后,最终收敛出了一张只有八项但条条见血的清单:
| 序号 | 检查项 | 说明 |
|---|---|---|
| 1 | 时钟使能完整性 | 对应外设HAL_RCC_xxx_CLK_ENABLE是否出现 |
| 2 | 引脚复用配置 | PINMUX_AF映射是否与CubeMX一致 |
| 3 | 外设句柄传参 | 初始化后句柄指针是否正确传入 |
| 4 | 中断与回调 | 不需要的中断是否被错误开启,回调函数是否已注册 |
| 5 | 阻塞控制 | 是否存在长延时导致主循环卡顿 |
| 6 | 死代码与注释残留 | 多余的函数、未启用的配置块是否清理干净 |
| 7 | 返回值与错误处理 | HAL函数返回值是否检查,Error_Handler是否被正确调用 |
| 8 | 资源和栈占用 | 新增大数组、递归调用、动态内存是否超出MCU资源承受力 |
这张清单的由来很有意思。某次项目里,AI生成了一段看似完美的PWM初始化代码,我和同事两个人都审查过,编译也通过,但电机就是不动。顺着检查清单往回查,才发现AI在初始化函数中把TIM3的通道配置成了TIM_CHANNEL_3,而实际硬件连接的是TIM3的CH1,PA6引脚。代码编译完全正常,因为HAL库的Channel参数只是一个枚举值,编译期不会检查它是否和外设实际通道匹配。打那以后,“引脚复用配置”和“外设句柄传参”这两项就升级成了必查项。AI代码生成的“逻辑自洽但物理错误”必须靠人的审查清单来兜底。
避坑记录文档是一个动态维护的文件,专门记录那些“当时花了很多时间才发现,回头看看其实有规律可循”的问题。每解决一个疑难问题,就往文档里加一条,格式是“现象—根因—解决—如何防再犯”。这些记录是团队极宝贵的资产,因为它们在传统文档里根本搜不到,全都来自现场调试的真实经验。AI协同开发的背景下,避坑记录还有一个新用途:把历史踩坑案例反哺给提示词模板,让AI在生成初稿时就避开已知的坑。比如我们团队之前在I2C上吃过地址位宽的大亏,我在提示词模板里就会固定加一句“I2C设备地址务必换算成8位总线地址,通信前先进行I2C总线扫描验证”,AI生成代码时就会主动带上这层保护逻辑。
关于团队协作,还有一个实际遇到过的问题:同一段功能,两个成员分别用AI工具生成出不同版本的代码,结构差异很大,合并时冲突特别难看。后来我们定了规矩:AI生成代码的同时,必须说明它用的是什么提示词和上下文,并且生成结果按功能模块分开提交,不要一次提交一大坨。这样冲突集中控制在小范围内,双方还能互相审查提示词表达,反而越来越会写提示词。
最后分享一个个人心得。用AI协同开发STM32几个月之后,我发现一个反直觉的现象:我并没有因为AI而变得不会写代码,反而代码审查能力明显增强。以前自己写代码的时候,很多问题是“写的时候就没意识到”,AI生成代码后,我习惯了逐行审查它写的东西,渐渐地,这种审查眼光也回到自己身上,自己写代码时会更早发现问题。所以别怕AI会让工程师退化,只要你始终坚持“AI写初稿、人做终审”的原则,它实际是在把你推向更严谨的工程设计流程。
如果要给刚起步的人一个可执行的建议:从今天开始,挑一个你最近手头的STM32小功能,按文章里写的那些提示词模板,生成一段代码,然后打开CubeMX和参考手册,逐行走一遍审查清单。跑通一个功能之后,再把流程推广到更大一点的项目。AI协同开发的手感是练出来的,光看不练永远只能停留在“好像会了”的阶段。