最近一个月,我发现自己写代码时有个明显变化:凡是拿不准的外设配置,第一反应是打开AI工具问一圈,而不是第一时间去翻几千页的参考手册。同事半开玩笑地说,你这是染上“AI病”了。我仔细想了想,这顶帽子还真没扣错。作为一个嵌入式工程师,我已经从“凡事硬刚寄存器”的状态,慢慢变成了“AI给方案、人肉来兜底”的状态。今天想把自己这段经历摊开聊聊,讲讲我从抵触AI到离不开AI的过程,包括我用AI做了哪些正事、踩过哪些坑,以及最终怎么把AI安全地嵌进自己的工作流。这篇文章不是让AI替我写多少代码,而是怎么让AI帮我少走弯路。如果你也是嵌入式开发者,或者正在转嵌入式方向,希望这些经验能让你少踩几个坑。
1. “AI病”初期的典型症状:从抗拒到真香
1.1 最初我有多排斥AI写代码
我做嵌入式开发差不多快十年了,早期那套原则特别顽固:代码必须自己一行行写,寄存器必须对着参考手册一个位一个位抠,调试必须靠示波器和逻辑分析仪说话。那时候觉得AI生成代码就是玩具,应付个LeetCode还凑合,到了单片机这种资源受限、硬件耦合极强的场景,AI肯定分不清GPIO和SPI的区别。
所以一开始我连GitHub Copilot都没装,别人在群里讨论AI写驱动,我还在心里默默吐槽:这种没有硬件环境、没有实时约束的代码,能直接用吗?估计连编译都过不了。这种心态大概维持了很长一段时间,直到某个周五下午,我被一块触摸屏驱动按在地上摩擦了整整四个小时。
1.2 是什么让我“破防”了
当时在调一块I2C接口的触摸屏,现象是中断触发特别频繁,但读出来的坐标数据全是乱的。我对照数据手册检查了寄存器配置,总觉得没问题,又用示波器看了波形,SCL和SDA也都有信号,可数据就是不对。按照老办法,我得把I2C时序图一帧一帧对一遍,再翻IC的勘误表,看是不是有寄存器初始化顺序要求。
折腾到傍晚,我抱着试试看的心态,把问题描述、芯片型号、寄存器配置代码一股脑粘进AI对话框,问它“I2C触摸屏中断频繁且坐标乱,可能是什么原因”。它没有直接给一个“正确”答案,而是列了几条排查方向:先确认I2C地址是否做了8位左移、检查中断引脚是否配置成开漏、看读数据的寄存器是否需要在每次读取前重新写起始地址。我顺着它列的思路,很快发现是I2C读时序里缺少了“重复起始信号”的配置,导致触摸IC老是返回同一份错误数据。
说真的,那一刻我有点破防。不是因为AI比我强,而是它用一分钟时间,把我四小时该做的“知识检索”给做完了。人还是那个人,但工具变了,效率就是上来了。
1.3 摆正心态:AI不是替代,是外挂
“破防”之后我很快冷静下来,给自己立了三条规矩:第一,AI生成的代码默认是“半成品”,必须经过编译、静态检查、硬件实测三重验证;第二,AI只能提供方案,不能替我做架构决策,比如用中断还是DMA、用RTOS还是裸机;第三,涉及安全、认证、可靠性要求极高的代码,人类工程师必须逐行签字负责。
我常跟同事打比方:AI就像一台能出影像的MRI设备,它能帮你快速看到身体内部的问题,但最后下诊断、开药方、动手术的,还得是医生。嵌入式工程师别怕自己变成“只会按按钮的”,因为真正的经验和判断力,恰恰是你用来兜住AI幻觉的保险网。
2. 嵌入式开发里,AI真正能干的几类活
2.1 外设寄存器配置:让AI读手册,而不是你
嵌入式开发最耗时间的事情之一,就是啃芯片参考手册。动辄几千页的PDF,里面寄存器描述、位域说明、时序图表密密麻麻,查一个DMA映射表能翻半天。AI最擅长干这种事:你把芯片型号、时钟频率、外设名、想要的功能告诉它,它能给你整理出寄存器初始化顺序和计算过程。
我用STM32F407做过一个串口波特率配置,人工算要查时钟树,确定APB1总线时钟,再看USART_DIV的小数部分怎么取整。AI直接给出计算过程:84MHz的APB1时钟,目标波特率115200,则USARTDIV = 84,000,000 / (16 * 115200) = 45.5729,整数部分写0x2D,小数部分取0x0B(对应0.5729 * 16 = 9.166,四舍五入后是9)。虽然它给出的最终寄存器值需要再对照参考手册核实,但整体思路和计算过程基本没错,省掉了大量查表时间。
不过,这里有个必须注意的点:不同系列、不同批次芯片的寄存器位定义可能有差异。AI如果用的是通用知识,很可能会把A芯片的位定义套到B芯片上。我现在的做法是,要求AI在回答时必须标注“信息来源”和“假设条件”,比如“假设你使用的是STM32F407VG,参考手册RM0090 Rev8”。然后我再拿官方头文件和手册做交叉验证。
2.2 设备树与启动代码:少掉头发的好帮手
做嵌入式Linux的时候,设备树也是个磨人的环节。有时候一个外设节点少了pinctrl-0,或者interrupt-parent写错,驱动就是起不来。我自己写新板级设备树时,会把SoC的参考设备树片段喂给AI,让它按照我要用的外设型号生成一段模板。
比如给一款I2C接口的温度传感器生成设备树节点,AI会给出:
&i2c1 { clock-frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c1>; status = "okay"; tmp117: tmp117@48 { compatible = "ti,tmp117"; reg = <0x48>; status = "okay"; }; };这段代码初看没问题,但如果我不去确认这颗芯片的I2C地址到底是0x48还是0x49,或者不核对compatible字符串是否与主线内核驱动匹配,后面大概率还是要调试大半天。AI的价值在于把繁琐的模板搭好,真正要命的兼容性问题,还是得靠人去查。
启动代码里的startup.s、链接脚本、Makefile也是一样。AI可以帮你快速生成一份能编译过的骨架,也能解释每一行汇编在干什么,但链接脚本里RAM和Flash区间分配、堆栈大小的设置,必须结合你的MCU型号和实际资源来决定,这些只能由人来定。
2.3 单元测试与代码审查:从“懒得写”到“自动过”
嵌入式项目里,单元测试一直不太受待见,主要原因是硬件耦合强、模拟外设麻烦。但AI可以把写测试桩的工作量打下来。比如我写了一个环形缓冲区的模块,要让AI生成单元测试,只需要给它函数原型和一个模拟的内存池,它就能生成一批边界情况的测试用例,包括空队列读、满队列写、回绕场景等。
代码审查方面,AI更像是一个不会疲劳的“初检员”。我会把一段可能存在指针越界的中断处理函数粘给它,让它专门找空指针解引用、数组下标越界、位运算优先级错误、static变量重入等问题。有些问题它真的能一眼揪出来。比如我写过这样一行:
if (status & 0x08 == 0) { // do something }AI立刻指出这里应该写成(status & 0x08) == 0,因为==的优先级高于&。这种细节以前靠人肉眼review,确实容易漏,AI不会犯困,效率自然高很多。
2.4 调试日志与Bug定位:AI帮你画草图,你负责拍板
还有一种高频用法,是把串口打印的日志、系统异常栈、甚至逻辑分析仪导出的时序描述,粘贴给AI,让AI给出故障假设清单。它能帮你把可能性排序,指出最可疑的方向。
我调一块网卡驱动时遇到过一个问题:以太网链路起来了,但PING不通。抓包软件又没有,只能靠串口打印ICMP请求和应答计数。我把log贴给AI,它给出的假设里有一条很关键:检查ARP缓存是否正常,因为如果对端设备的ARP请求没有正确应答,ICMP请求就永远到不了对端。沿着这个方向,我果然发现是接收路径里的MAC地址过滤配置错了,导致发来的ARP广播帧被硬件丢弃。
但AI也经常把方向带偏,因为日志信息是残缺的,它只能基于你提供的信息做猜测,硬件上的噪声、时序上的毛刺、电源纹波,AI根本看不到。所以它画草图,拍板必须是我自己,结合示波器和实际电路去验证。
3. 一场真实的小项目:用AI辅助完成STM32 ADC采集与DMA搬运
3.1 需求与硬件背景
大概三个月前,我做了块小型数据采集板,主控是STM32F103C8T6,需要采集三路模拟量(两个电位器加一个温度传感器),并以100Hz的采样率连续输出到串口。按照传统写法,我会启动ADC并用DMA把数据搬到内存数组里,然后定时从数组里取平均值。
这个任务本身不复杂,但要注意的细节很多:ADC1的DMA请求要选哪个通道、DMA传输模式是Normal还是Circular、缓冲数组大小和数据类型必须对齐、转换结束后怎么清标志等。以前这活我可能要花一整个下午确认,这次我试着用AI走了一遍完整流程。
3.2 我的“AI工作流”怎么走
我先把需求描述给AI,包括芯片型号、时钟配置、采样通道、采样频率、缓冲区大小,要求它给出两种方案:一种是基于HAL库的CubeMX配置思路,另一种是直接操作寄存器的最小实现。
AI首先提醒我,STM32F103的ADC1对应DMA1通道1(具体要看参考手册的DMA请求映射表),并给出建议:
- ADC选择扫描模式,连续转换,规则组用DMA循环传输
- DMA设置成Circular模式,外设地址是
&ADC1->DR,内存地址指向uint16_t adc_buf[3] - 每次转换完成,DMA会按顺序把三个通道的数据填入数组,无需进中断
这一步让我少走了很多弯路。以前我总纠结要不要开ADC中断,现在AI把这个决策逻辑讲清楚了:如果采样速率不高且不需要每个样本都处理,用DMA循环搬运加一个定时器标志就够了。
3.3 实际效果与代码细节
最终我的初始化代码是这么写的(HAL库版本):
ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; uint16_t adc_buf[3] = {0}; static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode = ENABLE; hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 3; HAL_ADC_Init(&hadc1); ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_REGULAR_RANK_2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_2; sConfig.Rank = ADC_REGULAR_RANK_3; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }DMA部分对应的配置关键代码如下:
hdma_adc1.Instance = DMA1_Channel1; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc1); __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);代码烧进去之后,用串口打印adc_buf[0]、adc_buf[1]、adc_buf[2],数据确实在更新,三路电压值也能对应上。整个过程顺利得让我自己都吃惊。但后面真正体现AI“不可信”的事情,发生在排查阶段。
3.4 排查阶段:AI给出的方案为什么不能直接抄
采集功能做完后,我想加一路PWM输出控制电机,结果发现PWM有输出,但ADC采集到的第一通道数据变成了一条直线。我第一反应是DMA被干扰了,于是去问AI:“为什么加PWM后ADC的DMA数据异常?”
AI给了几个可能:电源噪声、DMA优先级不够、PWM触发了ADC的外部触发。甚至还建议我把DMA优先级提到最高。我照着试了,发现没用。后来我仔细查了芯片手册里的DMA请求映射表才发现,我用的PWM输出通道恰恰占了DMA1的另一个通道,虽然不同外设,但DMA的仲裁器总线冲突确实会影响传输时序。而且ADC用了DMA1_Channel1,PWM用的是TIM2的更新事件,它并没有直接占DMA,但在某些低功耗模式下总线时钟分配会有干扰。
这个案例特别说明问题:AI不知道你板子上的具体外设占用、不知道你是用哪个定时器、也不知道你供电是不是稳定。它给出的是一般性可能,不是针对你的硬件的结论。所以我后来总结出一个原则:AI建议你改优先级、改时钟、改DMA模式,最后一定要回到参考手册和实际波形去验证,不能因为它给了一条看似合理的路,就直接把代码改了。
4. 踩过的坑,比学到的知识还值钱
4.1 幻觉不是玄学:AI一本正经地编寄存器名字
最典型的坑就是AI幻觉。我之前让AI生成一个STM32F407的ADC+DMA配置,它很自信地写了一个DMAMUX1_CHANNEL_9的宏。偏偏STM32F407根本没有DMAMUX外设,更不存在这个通道编号。如果我不熟悉芯片,直接在代码里用了这个宏,编译就会直接报错。这个还算好,更危险的是AI编一个“看起来合法、实际上不存在”的寄存器位,比如某个状态寄存器里根本不存在的溢出标志,代码能编译过,但运行行为完全不对。
应对幻觉我有一个土办法:凡是AI给的关键寄存器名、外设通道号、中断向量号,我都要在官方头文件、参考手册或运行时的调试寄存器里核对一遍。有时候我甚至会让AI“自证”:让它解释为什么这个寄存器存在,或者让它给出它在官方头文件里检索到的代码片段。虽然它还是可能编,但至少会把矛盾暴露得更早。
4.2 上下文遗忘:聊到第20轮,它忘了芯片型号
另一个经常遇到的问题是上下文丢失。跟AI对话时间长了,它会忘记前面设定的“芯片型号是STM32G474,时钟主频170MHz,使用CubeIDE编译”这些关键约束。到了第20轮,它突然给出一个STM32F1系列的解决方案,还振振有词。这种错误比幻觉还难发现,因为前面的回答都很正常,你会下意识信任它。
我的解决办法是分成两类场景:如果是简单问答,我会在每次提问开头都重新粘贴一遍关键环境信息;如果是一个完整的项目,就直接用Claude Code这类能与本地代码库联动的工具,让AI始终读取项目里的芯片头文件、编译配置和代码上下文。这样至少能把“它忘了我在做什么”的概率降下来不少。
4.3 安全与合规:嵌入式工程师的底线
越聊越深入之后,我反而越来越担心一个问题:AI生成代码的合规边界。嵌入式开发经常涉及车规、医疗、军工类项目,代码的可追溯性要求极高,如果上线了一套由AI生成、但没人能完全解释每一行的代码,一旦出了问题,责任怎么划,追责怎么追?
我给自己定了几条硬规矩:
- 企业内部代码、未公开的硬件原理图、私有的通信协议,绝不粘贴到公共AI服务里
- AI生成的代码必须进入公司的代码审查流程,和人类同事写的代码一视同仁
- 对可靠性要求极高的模块(如安全校验、故障注入、看门狗逻辑),坚持人工手写并做额外的同行评审
- 定期把AI生成的成果和项目需求文档对照,避免“实现正确但不是客户想要的”
这不是不信任AI,而是为了在工具效率和安全责任之间找到一个可持续的平衡点。
4.4 常用提示词和“人肉审查”清单
既然AI工具要用,那就要学会怎么用。我总结了一套自己常用的提示词模板,分享给大家参考:
- 初始化外设:“请给我配置STM32U5的LPUART1,时钟来自PLL2的PCLK1,波特率9600,奇偶校验无,使用中断接收,要求给出基于HAL库的配置代码和引脚复用编号。”
- 排查问题:“我的设备现象是A,代码中是B,测量波形是C,请列出可能导致问题的三个方向,按可能性从高到低排序,并说明每个方向对应的验证方法。”
- 代码审查:“请以安全关键系统代码审查者的角色,审查以下C代码,重点检查数组越界、空指针、并发访问和位运算优先级问题,不要输出优化建议,只输出潜在缺陷。”
同时,我整理了一张“AI生成内容人工核对清单”:
| 核对项 | 说明 |
|---|---|
| 寄存器名称 | 对照官方头文件和参考手册,确认不存在于其他型号 |
| 中断通道/优先级 | 确认与NVIC配置一致,没有与外设DMA冲突 |
| 时钟频率 | 结合时钟树计算,确认外设时钟来自预分频后的正确答案 |
| DMA通道 | 查芯片参考手册的DMA请求映射表,防止已被其他外设占用 |
| 设备树compatible | 对比内核源码中的driver匹配表,避免字符串不一致 |
| 内存地址/堆栈大小 | 对照链接脚本和实际RAM大小,防止溢出 |
| 安全逻辑 | 看门狗、错误处理、异常分支必须人工重写并评审 |
5. 一些个人心得和小建议
聊了这么多,最后分享几个我死活不愿意改的操作习惯。第一,我现在做新板子驱动,一定会先用CubeMX这类官方工具把底子搭好,再用AI去解释和优化代码,而不是让AI从零生成一份“看起来完美”的驱动。因为官方工具至少能保证时钟树和外设配置的一致性,AI再多也只是辅助。
第二,AI可以大幅提升学习效率,但不能替代基础训练。很多刚转嵌入式的朋友,上来就追求“AI帮我写驱动”,结果连中断优先级、DMA半传输中断、位带操作这些概念都说不清楚。我建议想入行的朋友,至少先把“嵌入式八股文”吃透,比如C语言指针、结构体对齐、链表、环形队列,再让AI帮你提高,这样才不会变成一个只会复制粘贴的人肉编译器。
第三,如果你对AI编程工作流感兴趣,不妨先从单板机、Docker嵌入式环境、MCU工程模板这些小事开始改造,一点点把AI嵌进流程里。我目前的工作流是:架构和模块划分自己定,AI负责把繁琐的配置代码、测试桩、日志分析做掉,最后我用示波器、逻辑分析仪和代码评审兜底。这套流程跑下来,确实比过去单纯手写高效了很多。
最后再分享一个小技巧:让AI写代码之前,先让它复述一遍你对需求的理解。如果它复述的和你说的都不一样,那后面生成的代码基本不能用。这一两分钟的“额外操作”,能帮你省下好几个小时的改错时间。
现在的我,还是会盯着示波器发呆,还是会为了一个GPIO的上下拉纠结,但至少在“接下来该往哪儿查”这个问题上,AI帮我把灯照亮了一大截。带病生存,也要病得有底气。