
其实我真没想到这个“第一个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传感器和风速传感器波特率96008N1一套AI协同开发的约定流程提示词模板、代码审查清单以及“AI输出必须经过本地编译验证再上板”的铁律。说实话这个阶段最大的收获不是方案本身而是彻底认清了一件事AI在嵌入式开发里的角色不是“替代工程师思考”而是“把工程师从重复劳动里解放出来”。方案设计、引脚冲突排查、BSP裁剪这类的活儿AI能做得又快又好但如果你让它直接输出“整个项目的完整代码”结果几乎一定是灾难。1.2 为什么第二次迭代要把任务拆得更碎我第一次真正用AI写嵌入式代码时提示词是这样的“帮我写一个基于Linux的Modbus RTU主站程序读取三个传感器的数据。”结果它给我生成了两千多行代码看着很完整其实就是把网上常见的modbus库缝合了一遍还带了几个莫名其妙的依赖。交叉编译一跑差点把整个构建链都带崩。这次我换了一种策略把项目拆成5个可独立交付的小模块每个模块单独和AI对话。模块清单如下模块内容交付标准uart_bsp串口初始化、打开、读写、关闭可在板卡上正常收发测试帧modbus_crcCRC16-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 -marcharmv7-a -mfpuneon 限制: 不依赖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要么把编译标准从-stdc11改成-stdgnu11。但我当时更介意的是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行约束注入人工每个模块的最小提示词里包含环境、协议、约束代码生成AIAI基于以上约束提交初版代码编译验证人工过交叉编译修正平台相关的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不是主角但你用好它确实能把项目的周期压缩一半以上。这个经验我觉得比项目本身更值得带进下一个循环。