这两年搞嵌入式,我最大的感受就是:AI协同开发STM32程序已经不是“能不能用”的问题,而是“怎么用才能不翻车”的问题。我自己从最早把AI当搜索引擎用,到现在把它当成团队里的半个“实习生”,整个开发节奏变了不少。这篇就聊透一件事:在真实的STM32工程里,AI到底是怎么参与进来的,完整流程长什么样,以及哪些坑我是真踩过的。
先说个结论摆在这:嵌入式软件不像纯网页开发,代码写错了顶多页面报错,单片机程序出了问题是要看示波器、翻寄存器、甚至重新打板的。所以AI协同开发的核心,从来就不是“让AI把代码写完”,而是“让人和AI一起把代码写对”。这篇文章适合已经在用或者准备用AI写嵌入式的朋友,不管是刚入门的新手,还是想优化现有流程的工程师,应该都能捞到点干货。
1. 先想清楚:AI写STM32代码,哪些环节真能帮上忙
1.1 告别“AI只能写Demo”的旧印象
前几年大家普遍觉得AI写嵌入式代码就是玩具,生成个流水灯、点个OLED屏还行,一上正经外设就歇菜。这个印象在最近一年的变化非常明显——代码补全类和对话类AI对STM32生态的理解深度已经上了一个台阶,至少HAL库、标准外设库、甚至寄存器级别的操作,它都能给出能跑的框架代码。
我自己的实际操作体验是,AI现在真正能帮上忙的环节有这么几块:
- 寄存器与外设配置草稿:比如定时器PWM频率要算预分频和重载值,AI几秒钟就能给你算好一组组合,虽然不一定最优,但至少是合理起步点。
- 驱动框架搭建:一个传感器的初始化、读取、去抖、错误处理,把这些逻辑描述清楚,AI能直接生成一个结构完整的驱动文件。
- 代码解释和逆向梳理:拿到一段陌生的老代码,让AI一句一句解释,或者画个调用关系,比自己翻半天省力得多。
- 测试用例和调试代码生成:写个临时的自检函数、往调试串口打印寄存器状态之类的代码,AI几乎是秒出。
真正让我改变看法的是一次实际经历:我需要快速评估一块传感器模组能不能用现有MCU的某个定时器通道驱动,AI在十几分钟内给出了三个可行方案,每个都附了初始化和寄存器配置要点。换作以前,光查参考手册就得耗掉半天。
1.2 协同开发的第一原则:人审代码,AI写初稿
这个原则我反复跟团队里的小伙伴强调:嵌入式环境里,AI生成的每一行代码都应该被当成“面试者的代码”来审查,而不是“工作成果”直接合并。
为什么嵌入式尤其要这样做?因为MCU程序有几个特殊问题:
- 硬件资源不可逆:引脚复用配错了,可能直接拉低了某个外设的电平,导致硬件损坏。
- 时序和实时性:Web程序慢几十毫秒无所谓,定时器中断里多跑了一个阻塞延时,整个控制环就崩了。
- 寄存器操作的隐蔽性:AI生成的代码看起来逻辑没毛病,但漏掉某个时钟使能,现象可能非常诡异——时好时坏,万用表量不到任何规律。
所以“AI写初稿、人做审查”不是保守,而是基于实际后果的必然选择。这也是整套协同流程的基石。
2. 一套可复用的AI协同开发流程,照着搭就行
2.1 需求拆解:先给AI讲清楚“输入、处理、输出”
跟AI协作的第一步,不是急着让它写代码,而是先做需求拆解。我在实践里发现一个很管用的方法:把功能需求翻译成“输入、处理、输出”三个维度喂给AI。
举个例子,你不能直接说“帮我写个电机控制”,而要说:输入是编码器脉冲信号,来自定时器3的通道1和2,需要4倍频计数;处理逻辑是每100毫秒读取一次计数值,计算转速,做一次简单的增量式PID;输出是PWM波形,由定时器2的通道1产生,频率20kHz,占空比由PID结果更新,还要加一个死区保护。
这样拆完,AI给出的代码质量会明显提升,因为它不需要去猜你的硬件连接和项目上下文。而且这么做还有个额外好处:在你自己脑子里,功能边界也清晰了不少,很多隐藏问题在这个阶段就暴露了。
我一般还会顺手让AI列几个“需求确认问题”——比如“电机最大转速是多少”“PID周期是否固定”“堵转时怎么处理”。这些问题看着简单,但确实戳中了不少我最初没细想的点。
2.2 五个阶段的协同节奏
经过多次项目磨合,我现在固定下来的流程是五个阶段,每个阶段人跟AI的分工清清楚楚:
| 阶段 | 人做什么 | AI做什么 | 产出物 |
|---|---|---|---|
| 需求澄清 | 梳理输入、输出、约束条件 | 补充考虑点、生成需求问题清单 | 明确的功能描述文档 |
| 方案与接口 | 确定外设分配、引脚复用、通信协议 | 生成驱动接口建议、评估多种实现方案 | 接口定义与总体框图 |
| 代码生成 | 逐模块描述逻辑,设定边界条件 | 生成初始化与驱动框架代码 | 可编译的初版代码 |
| 静态审查与编译 | 对照参考手册和项目规范审查 | 检查代码风格、补充注释、预判编译错误 | 修改后的代码 |
| 硬件验证 | 烧录、调试、观察波形与寄存器 | 协助分析异常日志、修改调试代码 | 通过实测的固件 |
这个表格看着简单,但每个阶段都有一条红线:任何人都不能用AI直接跨过中间阶段输出最终代码。比如“方案与接口”还没定,就让AI生成整个工程的代码,结果大概率是返工重来、浪费时间。
2.3 为什么流程要卡在“先接口、后实现”
写单片机程序的时候,很多人习惯直接开写,写完再补接口文档。但跟AI协作恰恰相反——接口和数据结构必须先定死。
原因很简单:AI的记忆和上下文是有限的。你让它生成三个文件(主程序、传感器驱动、控制算法),如果每个文件里对“转速”这个变量的类型、单位、更新方式的描述不一致,AI生成的代码就会出现接口不匹配的问题。这种问题在编译阶段不明显,但联调的时候会把你折磨到崩溃。
我自己习惯的做法是:先让AI生成一个“接口声明文件”或者“头文件骨架”,里面定义好主要的typedef、结构体、函数签名、全局变量访问方式。把这个作为后续所有对话的上下文基础。实测下来,代码合并的成功率能提升一大截。
3. 实战:让AI帮你写一个STM32电机控制驱动
3.1 场景描述与外设规划
拿我最近做的一个小项目说——小型直流电机驱动板,需求很简单:用定时器输出PWM调速,用编码器回读实际转速,跑一个简单的闭环控制。
外设规划先定下来:
- 定时器2:PWM输出,通道1,频率20kHz,引脚PA0
- 定时器3:编码器接口模式,通道1和通道2,引脚PA6、PA7
- 调试串口:USART1,用于打印转速和调试信息
这部分我在对话里先发给AI,让它确认这个分配是否合理,顺便生成引脚配置的核对清单。AI给出的反馈里有个很关键的建议——编码器接口模式下要开启输入滤波,否则电机换向时的毛刺会把计数值弄乱。这个点当时确实是我没细想的。
3.2 第一轮对话:生成配置草稿
接下来让AI生成初始化代码。我给的提示大概是这样的:
我使用STM32F103系列MCU,使用HAL库。请生成定时器2的PWM初始化函数,要求:内部时钟72MHz,输出频率20kHz,占空比初始50%,通道1输出,引脚PA0复用推挽输出。请同时给出使能GPIO时钟、定时器时钟、配置PWM通道的全部代码,并注意分频系数和重载值的合理性。AI生成的代码基本骨架是能用的,但有一个细节暴露了它“不懂硬件工程化”的地方——占空比初值直接硬编码成了50%的常量,而不是通过宏定义或者变量传入。这种风格在小工程里没问题,但代码一多就到处是魔法数,维护起来想哭。所以我会在审查阶段统一改成宏定义。
代码的大致形态是这样:
#define PWM_FREQ_HZ 20000 #define PWM_DUTY_INIT 500 // 千分比,500表示50% static TIM_HandleTypeDef htim2; void MX_TIM2_PWM_Init(void) { TIM_OC_InitTypeDef sConfigOC = {0}; __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); htim2.Instance = TIM2; htim2.Init.Prescaler = 72 - 1; // 72MHz / 72 = 1MHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = (1000000 / PWM_FREQ_HZ) - 1; // 1MHz / 20kHz = 50 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(&htim2); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = PWM_DUTY_INIT; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); }这代码初看没啥问题,但注意几个审查点:预分频和重载值算的是对的,72MHz进预分频72得到1MHz计数频率,再分50得到20kHz。可是如果后续要改频率,这里就是两个耦合在一起的参数,所以我一般会让AI顺手生成一个修改PWM频率和占空比的辅助函数:
void set_pwm_freq_duty(uint32_t freq_hz, uint16_t duty_permille) { uint32_t prescaler = 72000000UL / freq_hz; // 这里应该根据实际时钟树调整,此处假设APB1定时器时钟为72MHz __HAL_TIM_SET_PRESCALER(&htim2, prescaler - 1); __HAL_TIM_SET_AUTORELOAD(&htim2, (72000000UL / prescaler / freq_hz) - 1); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, (72000000UL / prescaler / freq_hz) * duty_permille / 1000); }3.3 第二轮对话:生成驱动框架代码
初始化过了,接下来是功能逻辑部分。我先描述清楚需求,让AI生成编码器读取和速度计算的代码。这轮的提示我加了很多边界条件:
定时器3工作在编码器接口模式,计数范围为0~65535,硬件4倍频。请实现一个函数:获取转速(单位RPM)。已知编码器每圈输出100个脉冲,定时器时钟频率为72MHz(已4倍频到计数频率)。采样周期为100ms。函数需要考虑计数器回绕的情况,并返回短时间内的平均转速。这次AI生成的代码包含了回绕判断,方向判断也做了,但有一个逻辑缺陷:采样周期太短导致低速时数值抖动很大。我加了100毫秒的采样窗口后发现,转速低于100RPM时,一个脉冲计数误差就能导致20%的转速波动。AI在生成代码时不会考虑这种工程噪声问题。
这块我给AI补充了要求,让它在返回前做一个简单的软件滤波——连续采三次取平均。AI很快就给出了修改版本。这一步的体会是:嵌入式控制逻辑里的工程判断(滤波、死区、防脉冲干扰)是AI现阶段最大的短板,也是最需要人介入的地方。
3.4 审查AI代码时,我最看重的四个点
代码审查是协同流程里最耗时也最关键的步骤。我总结了自己的四个固定关注点:
- 时钟树使能是否完整:很多AI代码漏了GPIO时钟或者外设时钟,编译不报错但运行无输出。每次我都直接去查
__HAL_RCC_相关的所有调用。 - 中断处理函数里是否有“重活”:AI特别喜欢在中断回调里直接做PID运算或者串口打印,这在低速场景还能忍,在控制周期要求高的场景就是灾难。我的红线是中断里只做置标志位和快速数据更新,其他一律丢到主循环。
- 外设初始化顺序:有些外设要求先配置GPIO再初始化外设、或者先打开时钟再配置中断优先级。AI经常忽略这种隐含的顺序约束。我会在审查时按参考手册的初始化流程模板逐条比对。
- 数值类型和位宽:定时器的重载值寄存器是16位的,AI可能在计算时生成一个超出范围的int数据,编译器不报错,但实际写入时被截断,频率完全不对。
我做过一次小统计,在刚过去的某个项目里,AI生成的初始代码平均有3到5处需要修改,绝大多数集中在这四类。
3.5 烧录验证:用调试器和示波器说话
代码编译通过、烧录进板子,协同开发还没完。硬件验证这一关,AI依然可以参与,但主角已经换成了调试工具。
我的固定流程是先用调试器在关键位置设置断点,观察寄存器实际值是不是跟配置一致。比如PWM初始化完成后,就看TIM2的ARR、CCR1、PSC这三个寄存器。这一步能快速发现前面说的“数值截断”类问题。
然后接逻辑分析仪或者示波器看波形——PWM频率是不是20kHz、占空比是不是50%,编码器信号有没有毛刺。这时候AI的作用是联调排错:你告诉它“PWM频率实测只有40Hz,理论应该是20kHz”,它很快能推断出是不是预分频写错了、或者时钟配置有误。
AI甚至能帮你写一段“自检代码”,在启动时依次检查各个外设寄存器,并通过串口打印检测结果。这套自检代码写起来繁琐且枯燥,AI做这事确实又快又不容易漏项。
4. 我踩过的坑:AI生成代码的“通病”与排查清单
4.1 AI写嵌入式代码的五个典型错误
用了这么久,我总结出AI在STM32开发里最常犯的几类错误,每一条都是拿实际项目踩坑换来的:
| 错误类型 | 现象 | 排查方法 |
|---|---|---|
| 寄存器名拼写或版本差异 | 编译报错,提示结构体成员不存在 | 核对芯片头文件中的实际定义 |
| 外设时钟漏使能 | 代码运行后外设死寂 | 检查全部__HAL_RCC_调用 |
| 位宽截断或类型错误 | 频率/占空比计算乱套 | 给所有中间变量显式标注类型 |
| 阻塞延时误用 | 中断响应变慢,控制周期抖动 | 审查所有延时函数调用位置 |
| 中断优先级配置缺失 | 中断嵌套异常或响应延迟 | 核对NVIC配置和中断抢占关系 |
这五类错误里,最常见的是第一类。不同系列的STM32,甚至同一系列的不同型号,寄存器和结构体定义都有细微差别。AI脑中的“模糊印象”有时候会混合好几个型号的写法,导致生成的代码在特定头文件下编译不过。
4.2 让AI“认错”并修正的正确姿势
面对错误,很多人直接把报错信息原样丢给AI,然后期待它自己改。实话说,这样能改对一些小问题,但效率不高,而且容易在对话里陷入“改一处错一处”的循环。
我的做法是三步走:
- 先自己定位:把编译报错的信息、对应的代码片段、芯片型号和HAL库版本一起发给AI。上下文越具体,AI的修正越准。
- 让它解释原因:先别急着让它改,先问一句“这个为什么会报错”。这一步能过滤掉大量AI的“胡乱猜测式修改”。
- 限定修改范围:如果问题涉及多个地方,记得让它一次只改一处,避免连锁改动引入新问题。
有一次我遇到一个很隐蔽的报错——某个宏定义和头文件里的定义重名了,编译期不报错,链接期才对撞。单独上下文很难看出来,我让AI把整个驱动文件和编译链接日志都读了一遍,它最终定位到问题,并提出了一个改名方案。这个排查过程里,AI的贡献是“搜索范围广、速度快”,但判断和决策还是我来做的。
4.3 用“自己的代码库”对抗AI幻觉
AI最大的风险不是写错,而是“一本正经地写错”——生成的代码逻辑完整、注释清晰,但方案本身有隐患。对抗这个问题,我目前最有效的手段是:建立个人/团队的高质量代码片段库,并把它变成AI协作时的唯一参考基线。
简单说,我把自己多年沉淀的UART驱动、定时器封装、PID控制、Flash参数存储等模块整理成了标准库,每个模块都标注了适用芯片、时钟条件、使用示例。跟AI对话时,我会明确告诉它:“PO类配置参考xxx文件,不要发明新的写法。”
这么做的好处有三个:
- AI生成的代码风格统一,审查工作量大幅降低。
- 不确定项被压到最小,AI只能在自己熟悉的框架内填空。
- 团队多人协作时,代码可维护性大大提升。
我自己算过一笔账,维护代码库的成本是我每周大概抽出半小时整理和写注释,但每次协作节省的审查和返工时间远超这个数。
5. 协同开发的边界:哪些环节先别交给AI
5.1 这些场景,我会把AI关掉
虽然AI在很多方面能帮忙,但我现在也有一些“关掉AI”的场景,这类任务我坚持自己来写:
- 安全关键逻辑:涉及系统保护、硬件互锁、紧急停机这类逻辑,我不会让AI参与。不是信不过它,而是这类代码需要决策者对自己写的每一行负全责,而AI无法感知现场硬件的真实行为。
- 低功耗与时序规划:MCU的睡眠唤醒流程、外设时钟门控顺序、精确到微秒级的时序窗口,这些极度依赖具体芯片的应用手册。AI给的一般只能算“参考思路”,直接当成实现代码跑,很容易出“看起来睡了,其实电流没降”这种怪问题。
- 量产固件的启动与自检流程:量产固件要考虑存储区校验、启动日志、异常恢复,这些环节对错误处理的要求特别高,AI生成的错误处理往往偏“轻量”,当成Demo锻炼一下还行,量产应用我是不会直接用的。
5.2 AI是“实习生”,不是“验收员”
我常在团队里打一个比方:现在的AI就像一个上手很快、但缺乏工程经验的实习生。你给它一个方向明确的任务,它能干得不错;但你要是让它自己去验收代码合规性、判断硬件风险,它就会“用错误的方式做正确的事”。
所以在我的协作定位里,AI负责“产出”,人负责“验收”。验收包括:
- 对照需求清单检查功能完整性
- 对照项目编码规范检查风格和结构
- 对照硬件原理图检查引脚和外设分配
- 实测波形和寄存器来验证配置真实性
这个“AI产出、人验收”的分工模式运行了几个月之后,团队最大的变化是:大家的代码审查能力明显变强了。因为AI每天都能生成大量需要挑毛病的代码,看多了之后,人对自己代码里的隐患也更敏感了。
5.3 团队层面,把AI协作沉淀成规范
如果只是个人用,流程可以随意点。但团队协作时,我强烈建议把AI协同开发的约定固化下来。我目前团队里推的几条规则:
- 每个模块生成前必须先在文档里写清接口定义。
- AI生成的代码必须附“生成对话摘要”,说明需求点和修改点。
- 涉及硬件配置的代码,必须由第二个人复核引脚分配和时钟配置。
- 所有AI生成的关键代码,提交记录里标注“AI协作生成”,方便后期追溯。
这些规范听起来很“行政”,但实际执行下来,真的能防止很多团队级别的混乱。有过一次惨痛教训:两个同事各自用AI生成了一段互相冲突的中断处理代码,合并之后导致系统随机卡死,排查了一整天才发现是中断优先级互相抢占。如果当时都按规范留了接口文档和修改摘要,这个坑半小时就能定位。
最后说一个我自己的小习惯:每次让AI生成初始化代码的时候,我都会让它额外打印一份“预期寄存器状态”表格——比如PWM输出后,TIM2_SR、TIM2_CCR1、TIM2_ARR这几个寄存器理论上应该是什么值。拿到代码烧录后,我先在调试器里逐一核对寄存器,再去看波形。这个习惯帮我至少节省过十几次“明明感觉应该对但就是不对”的排错时间。
AI协同开发STM32程序这个事,说到底是流程改造的问题,不是工具替换的问题。工具随时在变,但“先想清楚、再写清楚、最后验清楚”的思路,什么时候都不过时。