其实我真没想到,这个“第一个AI协同开发项目”能让我把系列写到第18篇。上一篇文章我们停在了一个挺微妙的节点上:硬件平台选好了,开发环境跑通了,通信协议也定成了Modbus RTU,甚至整个项目在文档里已经有了像模像样的架构图。但说白了,代码还是零,传感器还躺在快递盒里,一切只停留在规划和原型里。
这一篇就是这个项目的下半场:真真正正把代码写出来,把设备跑起来。和AI协作开发,前期的兴奋感很强烈——生成代码又快又热闹,可一旦进入嵌入式这条赛道,情况就完全不一样了。AI能给你写出一段看起来无懈可击的C语言,但交叉编译会教它“做人”,真实硬件上的时序和电气特性,更会让它的“自信”原形毕露。
如果你也是嵌入式开发者,刚开始把AI编程工具引入到日常工作流中,那么这篇文章里踩过的坑、总结的提示词方法、以及最后那套“AI生成—行为验证—回归修复”的节奏,应该会对你有直接帮助。项目整体还是那个环境监测终端:一块ARM Linux开发板,通过RS485总线接温湿度和PM2.5传感器,数据在本地用Qt5界面展示。
1. 第一部分结束时的位置:方案很完美,代码还是零
1.1 重新回顾上一篇的交付状态
上一次项目结束时,核心交付有三样东西:
- 技术选型报告:确定用全志T113-i这颗Cortex-A7内核的工业级SoC,原因是BSP相对完善,Qt 5.12的适配资料多,社区里踩坑记录能搜到不少,对我们这种“既要快速出活,又要稳定可靠”的侧写很合适;
- 一条通信链路的设计:主机通过RS485总线下挂三个Modbus RTU从站设备,分别是温湿度传感器、PM2.5传感器和风速传感器,波特率9600,8N1;
- 一套AI协同开发的约定流程:提示词模板、代码审查清单,以及“AI输出必须经过本地编译验证再上板”的铁律。
说实话,这个阶段最大的收获不是方案本身,而是彻底认清了一件事:AI在嵌入式开发里的角色,不是“替代工程师思考”,而是“把工程师从重复劳动里解放出来”。方案设计、引脚冲突排查、BSP裁剪这类的活儿,AI能做得又快又好;但如果你让它直接输出“整个项目的完整代码”,结果几乎一定是灾难。
1.2 为什么第二次迭代要把任务拆得更碎
我第一次真正用AI写嵌入式代码时,提示词是这样的:“帮我写一个基于Linux的Modbus RTU主站程序,读取三个传感器的数据。”结果它给我生成了两千多行代码,看着很完整,其实就是把网上常见的modbus库缝合了一遍,还带了几个莫名其妙的依赖。交叉编译一跑,差点把整个构建链都带崩。
这次我换了一种策略:把项目拆成5个可独立交付的小模块,每个模块单独和AI对话。模块清单如下:
| 模块 | 内容 | 交付标准 |
|---|---|---|
| uart_bsp | 串口初始化、打开、读写、关闭 | 可在板卡上正常收发测试帧 |
| modbus_crc | CRC16-Modbus计算 | 与Modbus协议规范样例一致 |
| modbus_master | 生成完整请求帧、解析响应帧 | 解析结果与抓包数据一致 |
| sensor_data | 三个传感器的数据结构与处理 | 原始寄存器值可正确换算为物理值 |
| main_task | 定时轮询与节点管理 | 稳定跑24小时无内存泄漏 |
每一个模块的提示词都要带上上下文信息:芯片型号、编译器版本、内核版本、交叉编译工具链路径、目标板上的运行方式。AI不知道你那块板子上的串口是ttyS3还是ttymxc1,你不告诉它,它就只能瞎猜。
1.3 真正的“AI协同”从建立约束开始
这里要强调一个很多人忽略的点:AI生成的代码看起来“有模有样”,但嵌入式领域最致命的往往不是逻辑错误,而是跑不到目标环境。举个例子,AI经常默认Linux开发板的串口设备名是/dev/ttyS0,但很多工业级板卡上实际是/dev/ttymxc0或/dev/ttyS3。这一行弄错了,后面所有东西都不用谈。
所以我在每个模块提示词的开头,都固定粘贴了一段环境信息模板:
【目标环境】 SoC: 全志T113-i (ARM Cortex-A7) 内核: Linux 4.9.170 交叉编译: arm-linux-gnueabihf-gcc 8.4 目标系统: 嵌入式Linux (Buildroot) 串口设备: /dev/ttyS3 (RS485) 编译选项: -Wall -O2 -march=armv7-a -mfpu=neon 限制: 不依赖libmodbus外部库,全部代码自包含,线程安全这段话之后跟着一句:“请基于上述环境生成代码,如果没有把握,请直接在代码里标注需要适配的部分。”AI在收到明确约束后,生成的代码质量完全不一样,至少设备名、编译选项、系统调用这些“硬约束”不会再出错了。后面我还会详细演示一次带完整上下文的提示词长什么样。
2. 第一次真正实战:先从最底层的Modbus采集模块下手
2.1 带完整约束的提示词到底怎么写
在写提示词这块,很多人觉得“就是需求说得详细一点”,其实不对。在嵌入式场景下,提示词的本质是“把隐性知识显性化”。这里的隐性知识包括三块:
- 目标板的外设资源和驱动状态,比如串口是哪个、RS485方向控制引脚是哪个、GPIO是否已经export;
- 代码要遵守的工程约束,比如是否有动态内存限制、是否要求线程安全、是否禁用第三方库;
- 硬件的电气特性,比如RS485是半双工,收发必须切换方向,切换后要留出稳定时间。
拿modbus_master模块举例,我给AI的提示词是这样的:
请用C语言实现一个Linux平台上的Modbus RTU主站函数,满足以下条件: 1. 使用串口设备/dev/ttyS3,波特率9600,数据位8,停止位1,无校验; 2. 使用GPIO114作为RS485方向控制,发送时置高,接收时置低,收发函数中封装该操作; 3. CRC校验使用Modbus标准CRC16,多项式0xA001,初始值0xFFFF,输出低字节在前; 4. 函数接口为 int modbus_read_registers(int slave_addr, int start_addr, int reg_count, uint8_t* recv_buf, int timeout_ms),timeout_ms使用poll机制实现; 5. 不依赖任何第三方库,纯C加POSIX接口,线程安全; 6. 关键位置写注释,标注Modbus协议帧格式。2.2 AI的初版回复与第一轮审查
AI给出的初版核心结构是这个样子的(我简化了关键片段):
static int rs485_set_dir(int high) { // GPIO export 和 direction 初始化在 uart_bsp 模块里已完成 int fd = open("/sys/class/gpio/gpio114/value", O_WRONLY); if (fd < 0) return -1; char buf[2] = {'0' + high, '\0'}; write(fd, buf, 1); close(fd); return 0; } int modbus_read_registers(int slave_addr, int start_addr, int reg_count, uint8_t* recv_buf, int timeout_ms) { uint8_t tx_buf[64]; int tx_len = 0; tx_buf[tx_len++] = slave_addr; tx_buf[tx_len++] = 0x03; // 功能码:读保持寄存器 tx_buf[tx_len++] = (start_addr >> 8) & 0xFF; tx_buf[tx_len++] = start_addr & 0xFF; tx_buf[tx_len++] = (reg_count >> 8) & 0xFF; tx_buf[tx_len++] = reg_count & 0xFF; uint16_t crc = modbus_crc16(tx_buf, tx_len); tx_buf[tx_len++] = crc & 0xFF; // 低字节在前 tx_buf[tx_len++] = (crc >> 8) & 0xFF; // 高字节在后 rs485_set_dir(1); int n = write(modbus_fd, tx_buf, tx_len); tcdrain(modbus_fd); // 发送后稍延时再切换为接收,否则最后几个字节可能被截断 usleep(500); rs485_set_dir(0); // 接收部分:用poll等待数据,超时返回-2 // 收到后先做帧头CRC预校验,再交给解析层 return parse_modbus_response(slave_addr, recv_len, recv_buf); }先不说功能,单看结构这版已经算及格了。为什么?因为它满足了几个关键约束:协议帧格式正确,RS485方向切换有延时处理,接收用poll而不是阻塞读取,线程安全的设计由上层串口锁保证。这些都是我在提示词里明确要求的,说明AI在给定约束下确实能给出“可用”的代码。
但注意我说的是“可用”,不是“正确”。接下来进入交叉编译环节,第一个坑就冒出来了。
2.3 交叉编译的第一课:AI不知道你的编译环境
编译这一步,我们把上面这个文件扔进工程,用CMake管理,工具链指向/home/toolchain/arm-linux-gnueabihf/。结果编译器报了一堆warning,仔细看,最核心的一个问题是:
warning: implicit declaration of function ‘tcdrain’AI的写法默认了很多GNU扩展函数,比如tcdrain、cfmakeraw这些,严格讲它们不在POSIX标准里,而是glibc提供的扩展。如果你的CMake里刚好开了-Werror,那这一步就直接挂了。解决方式不复杂:要么在代码里加上#define _GNU_SOURCE,要么把编译标准从-std=c11改成-std=gnu11。但我当时更介意的是,AI没有告诉我它用了这些非标准函数。
这件事给了我一个重要的教训:在嵌入式项目里,AI生成的代码,第一轮永远不要直接过编译。你要把目标平台的头文件路径、编译选项、链接库在脑子里先过一遍,再决定是改代码还是改编译配置。不要看编译过了就觉得万事大吉,编译通过只是最低门槛。
3. 真机联调:AI代码的三次“翻车”和对应补救
3.1 第一翻:CRC值对不上,从站根本不响应
代码过了编译,烧进板子,串口接上RS485转USB模块,打开串口调试助手。结果主机发出去的帧,传感器从站一个都不回。
第一个想到的就是CRC。Modbus RTU的CRC16用的是0xA001多项式,但这里面有个巨大的坑:多项式右移还是左移、初始值、异或顺序,一个版本不对,结果就不一样。我拿逻辑分析仪抓了主机发出的原始字节流,和AI代码拼出来的帧比对,发现CRC的低字节和高字节顺序反了。
Modbus规定CRC在帧中是低字节在前,高字节在后。AI的modbus_crc16函数输出结果本身是对的,但发送时先发了高字节:
tx_buf[tx_len++] = (crc >> 8) & 0xFF; // 高字节 tx_buf[tx_len++] = crc & 0xFF; // 低字节就这两行写反了,整个链路全断。怎么发现的?逻辑分析仪抓包,拿十六进制流跟Modbus协议规范里的标准帧案例比对,一帧一帧地看,很快就定位到了。这里要吐槽一句:这种“高低字节顺序”问题,AI在生成时很容易跟内存里的字节序搞混。它知道协议要低字节在前,但它自己写代码时又按“高字节先发”的习惯来了。所以这类校验相关代码,我后来一定要求AI输出时附带测试向量。
我给AI回了一个测试向量要求:CRC("01 03 00 00 00 01") = 0x840A,帧字节应为"01 03 00 00 00 01 0A 84"。AI对照测试向量发现自己错在哪,改完就对了。这个方法比人肉看代码高效得多。
3.2 第二翻:字节序和数据类型,AI默认按“最简单的那套”来
第二个问题出现在数据解析。Modbus读出来的寄存器数据是16位的,但传感器厂商的数据手册里写的是“寄存器内先高字节,后低字节”。我让AI写解析函数时,直接给了它数据手册的简化版,结果它用memcpy把两个字节拷进uint16_t,没做任何转换。
在x86开发机上跑,小端存储,从内存里直接读出来的数值经常碰巧是对的。但目标板是ARM Cortex-A7,同样是little-endian,设备寄存器字节序也是little-endian,有时候也能碰巧跑通。真正恶心的是:传感器模块对寄存器内容的定义是“字内字节序为big-endian”,这意味着即使所有硬件都是little-endian,你依然需要手动交换高低字节。
uint16_t raw = ((uint8_t)recv_buf[0]) | ((uint8_t)recv_buf[1] << 8);改成这个写法,温湿度数据才算正常。但我也理解AI为什么会错:它的训练语料里,“正确”答案大多是直接memcpy的简单case,对“在一个小端传输协议里嵌套一个器件大端定义”这种魔鬼细节,没有足够上下文很难判断。这个问题的解法只有一个:手写一版测试用例,跑通了再让AI去改动。测试用例就是某固定寄存器地址,已知物理值,反推原始字节,必须给出确定性预期。
3.3 第三翻:串口裸奔读数据,漏帧到你怀疑人生
传感器回复了,数据能读了,但系统跑起来之后发现一个更隐蔽的问题:主站程序轮询三个从站,每个周期大约400ms,但采集到的PM2.5值偶尔会突变,前一次还是23μg/m³,下一次直接变成98。反复定位后发现,是接收函数没有做“帧完整性”判断。
AI的初版接收逻辑是:poll到数据就recv到缓冲区,然后直接按固定偏移解析。但Modbus RTU的帧和帧之间、主机轮询命令和从站回复之间,线路上可能残留上一帧的字节。如果上层不按“地址+功能码+长度+数据+CRC”的结构校验,很容易把残留字节当成下一帧的头部。
那段时间正好在做嵌入式面试复习,刷到了不少“嵌入式八股文”,里面反复强调“串口通信必须做超时和帧完整性校验”。这真的不是八股,是血泪教训。后来我在代码里加了一帧解析状态机,状态包括IDLE、3.5T间隔、解析头部、解析数据、CRC校验。每收一个字节都跑一遍状态机,只有CRC校验通过的帧才交给上层,数据突变问题彻底消失。
3.4 调试工具链:靠“让AI猜”不如靠逻辑分析仪
这里顺便聊聊调试工具。我的组合是:逻辑分析仪接在RS485芯片的RO端和DI端,一边抓原始波形,一边跑程序打印解析结果。哪个环节出错,一眼就能看出来是主机没发对、从站没回,还是解析层出了问题。
AI调试有个天然劣势:你把报错信息发给它,它能给你列出五十条可能原因,但真正的原因往往藏在硬件状态里。比如有一回,板子上的RS485方向控制引脚刚好和某个外设复用了,GPIO电平不对,发送时方向切换失败。AI再聪明也猜不到这个,因为它在纯软件层面看不到“引脚被硬件复用”这个事实。这种时候,AI的用途是“缩小排查范围”,比如告诉它现象后,它会建议先抓波形、检查引脚复用、确认驱动加载状态,这些建议能节省不少时间,但最终结论还是得靠硬件工具来拍板。
4. 让AI从“写码工”升级成“结对工程师”
4.1 第二轮的代码评审:不再逐行读,而是“跑测试看行为”
第一次项目我们用AI生成的代码,评审方式是拿过来人肉读,一行一行对照协议规范,累得半死还漏了一堆问题。第二轮我换了一个思路:每个模块交付之后,先不读代码,直接写一个针对该模块的“硬件验证用例”,让它在板子上跑起来,再回头看代码。
这么做的好处是,你关注的是AI代码的“行为”而不是“字面”。Modbus轮询模块,我就写了一个用例:连续跑10000次读操作,统计成功率、平均耗时、最大耗时。如果行为指标都通过,再去看代码的性能热点。这个流程,比任何“代码审查清单”都靠谱。
具体来说,这个验证用例长这样:
# 在板卡上执行,连续读取从站1的3个保持寄存器,共10000次 $ ./stress_test -s 1 -a 0x00 -c 3 -n 10000 -i 20 结果样例: total : 10000 ok : 9998 timeout : 2 crc_error : 0 parse_error: 0 avg_latency: 12.3 ms max_latency: 47.8 ms那次测试跑了大约2分钟,结果里居然有2次timeout和0次CRC错误。timeout虽然只有2次,但在工业环境里就是事故。跑到这个用例之后,我回头去看AI代码,才发现它在poll事件处理里漏了POLLHUP这一类异常事件的判断,个别情况下会把串口当作挂断直接退出等待。这就是“行为测试驱动代码审查”的价值——它逼着你去挖掘真正影响系统的问题,而不是在代码里大海捞针。
4.2 给AI下修改指令的“验收条件式”写法
项目推进到第二轮修改时,我已经总结出了一种比较高效的提示词写法。直接报错给AI是低效的,比如“我这边数据读取不对,帮我看看”,它大概率会给你列出十几条废话。高效的做法是给出三个信息:现象、已排查项、验收标准。
现象:从站地址3的温湿度数据在0x01寄存器返回异常,偶尔读到0xFFFF; 已排查:信号波形正常,CRC校验通过,问题定位在解析层; 请修复解析函数,并保证以下验收标准通过: 1. 连续读取1000次,0xFFFF出现次数必须为0; 2. 目标板CPU占用不超过5%; 3. 不改变现有函数接口。实测下来,这种“验收条件式”的修改指令,让AI代码的一次性通过率高了很多。因为它没有机会漫无边际地改,只能针对性地修,修完还要接受你的行为测试检验。甚至有时候,AI还会主动告诉你“这个改法影响到了另一个分支,建议一并调整”,因为它知道自己必须通过你的验收标准,所以思考得更全面。
4.3 沉淀出的AI协同开发完整流程
经过这轮迭代,我最终沉淀出一套流程,写在这里以备参考:
- 任务拆分(人工):按硬件边界拆成可独立交付的模块,每个模块不超过300行;
- 约束注入(人工):每个模块的最小提示词里包含环境、协议、约束;
- 代码生成(AI):AI基于以上约束提交初版代码;
- 编译验证(人工):过交叉编译,修正平台相关的GNU扩展、头文件等;
- 行为测试(人工):跑硬件验证用例,统计关键指标;
- 缺陷反馈(AI+人工):把现象、已排查项、验收标准回喂给AI;
- 回归测试(人工):验证修复无副作用,更新测试基线。
这套流程里,AI负责第3步和第6步的“生成”,人工负责其余所有“决策”。最后复盘了一下,效率有提升吗?绝对有。纯手写这些底层模块,我估计需要4到5个工作日,加上AI辅助后,实际用了2个工作日,而且包括了调试时间。但如果不走第5步的行为测试,这个时间会缩短到1天,然后后面用10天返工。所以,“AI生成代码省下的时间,必须投入在验证上”这条铁律,我算是彻底践行了。
5. 项目回顾与下一轮的改进方向
5.1 时间都花在了哪里
简单记录一下整个项目的时间分布,给你们一个直观参考:
| 任务 | 耗时 | 说明 |
|---|---|---|
| 环境搭建(含内核编译) | 0.5天 | 大部分时间耗在交叉编译链配置上 |
| Modbus通信模块 | 1天 | AI生成代码0.5天,调试校验0.5天 |
| 三路传感器适配 | 0.5天 | 主要是数据手册理解和字节序修正 |
| Qt界面集成 | 0.5天 | 这部分AI帮助很大 |
| 稳定性测试与修复 | 1天 | 主要是帧完整性状态机优化 |
| 合计 | 约4天 | 完成一个小型环境监测终端闭环 |
这个速度,放在两年前我是不敢想的。以前这类项目,光底层协议栈就要写两三天,还要留一周做稳定性测试。现在AI把重复劳动压缩掉了,人可以做更有价值的事情,比如系统架构、时序优化、异常处理。
5.2 AI在嵌入式领域的可靠边界
通过这次项目,我想给“AI写嵌入式代码”下一个更谨慎的结论:对于传感器驱动、协议栈、状态机这类“有明确规范和协议”的代码,AI完全能胜任,只要你能把所有约束写清楚;但对于“需要和具体硬件行为强耦合”的代码,比如DMA、中断上下文、电源管理、时序临界区,AI的可靠性会急剧下降。这时候它更适合做“参考实现”,你必须亲手review之后再用。
举一个实际的例子:我们在给main_task模块设计轮询调度时,AI一开始建议用sleep(100)做定时循环。这在应用层没问题,但加上RS485方向切换延时、从站响应时间、Qt事件循环之后,实际周期会漂移。漂移本身可能不重要,但如果哪天你要在同一个串口上挂更多从站,这个调度就崩了。后来我改成了timerfd加事件循环,把周期控制的主动权拿回主线程。这种“系统级时序”的决策,AI给不出好的答案,它只会给你一个看起来能跑的“教科书方案”。
5.3 下一轮打算做什么
这个项目如果继续做下去,我大概率会从几个方向扩展:一是把采集任务从单线程轮询改成多线程加环形缓冲区,加入看门狗;二是给数据加上MQTT上报,让数据到达云平台,支持远程查询;三是研究一下在AI生成代码的时候,怎么把“屏蔽硬件细节”这件事做得更好,比如设计一层HAL抽象,让AI生成的业务代码不直接碰硬件。
另外一个让我印象深刻的点是:这轮项目之后,我养成了“给AI喂测试向量”的习惯。不管是CRC、协议解析还是字节序处理,只要AI的代码里出现这类逻辑,我都会顺手写三五个已知输入输出对,让AI先自测再提交。一套用例定义下来,不仅能验证“对不对”,还能逼着AI在生成代码时考虑边界条件,比如空指针、超时、帧长度异常这些。整个过程里,AI不是主角,但你用好它,确实能把项目的周期压缩一半以上。这个经验,我觉得比项目本身更值得带进下一个循环。