嵌入式软件这行有个特别拧巴的地方:代码跑在资源受限的板子上,调试靠串口打印和示波器,编译一次动辄几分钟,而AI编程工具默认假设你在写Web应用或者Python脚本。我第一次尝试让AI帮我写STM32的驱动代码时,它给我生成了一个用动态内存分配的环形缓冲区——在只有20KB RAM的MCU上,这基本等于埋雷。所以当我开始做第一个AI协同开发项目时,核心问题不是“AI能不能写嵌入式代码”,而是“怎么让AI理解嵌入式开发的约束条件,并且产出能直接编译进固件的代码”。
这个项目是我用AI协同方式完成的一个温湿度采集节点的固件开发,主控是STM32F103C8T6,外挂SHT30传感器,通过UART上报数据。整个开发过程我刻意让AI参与了从寄存器配置到状态机设计的多个环节,踩了不少坑,也总结出一套比较顺手的协作流程。如果你也在做嵌入式开发,想搞清楚AI编程到底能帮上什么忙、哪些环节千万别让它碰,这篇内容应该能省你不少时间。
1. 项目整体设计与AI协作思路拆解
1.1 为什么选这个项目作为AI协同开发的起点
选温湿度采集节点作为第一个AI协同项目,不是随手挑的。它足够小,整个固件编译出来不到30KB,Flash占用清晰可控;同时它又足够完整,涵盖了嵌入式开发的典型环节:时钟树配置、GPIO初始化、外设驱动、中断处理、状态机逻辑、通信协议封装。这意味着我可以在一个项目里测试AI在各个环节的表现,而不是分散到多个项目里重复踩坑。
另一个考虑是调试成本。STM32F103C8T6这块芯片太熟了,手头有现成的开发板和SHT30模块,接线简单,供电用USB就够了。如果AI生成的代码有问题,我能快速定位是AI的锅还是硬件的锅。这一点很关键——如果你用一个自己不熟悉的芯片做AI协同开发,出了问题你连排查方向都没有。
项目的基本需求是这样的:系统上电后初始化时钟和外设,每隔2秒采集一次SHT30的温湿度数据,通过UART以9600波特率上报,数据格式为“T:25.3,H:60.2\n”。LED指示灯在采集时闪烁一次,采集失败时常亮报警。没有RTOS,裸机跑主循环加定时器中断。
1.2 AI在嵌入式开发中的能力边界判断
在动手之前,我先花了一个下午测试AI对嵌入式代码的理解程度。我给它出了几道题:写一个STM32的GPIO初始化函数、解释I2C时序中重复起始条件的用途、分析一段中断服务函数里调用printf的问题。结果很有意思——GPIO初始化写得有模有样但寄存器版本和库函数版本混着来,I2C时序解释得基本正确,中断里调printf的问题它也能指出来。
这让我对AI的能力边界有了初步判断:它能理解嵌入式开发的概念和约束,但生成的代码需要人工审查和适配。具体来说,AI擅长的是生成结构化的代码框架、解释外设工作原理、提供调试思路;不擅长的是精确的寄存器配置、特定芯片的时序参数、资源占用的精确评估。
基于这个判断,我制定了协作策略:让AI负责代码框架和逻辑实现,我负责硬件相关的配置和最终审查。具体分工在后续章节会详细展开。
1.3 工具链选型与AI编程环境搭建
工具链方面,我用的组合是VSCode + PlatformIO + STM32CubeMX + Claude。选PlatformIO而不是Keil或IAR,主要是因为它的配置文件是纯文本的platformio.ini,AI可以直接读写和修改,而Keil的工程文件是二进制格式,AI没法直接操作。VSCode的Copilot插件负责行内补全,Claude负责对话式的代码生成和审查。
这里有个细节值得说一下:AI编程工具对嵌入式项目的上下文理解,很大程度上取决于你给它提供的项目结构信息。我一开始只把代码文件丢给AI,它生成的代码经常引用不存在的头文件或者用错库函数。后来我养成了一个习惯,在对话开始时先把platformio.ini的内容、主要头文件的包含关系、以及芯片型号和主频信息一起发给AI,生成的代码质量明显提升。
提示:如果你用Keil或IAR,可以把工程配置导出为文本描述发给AI,虽然麻烦一点,但比让AI瞎猜强得多。
2. 核心细节解析与实操要点
2.1 时钟树配置:AI最容易出错的地方
STM32F103C8T6的时钟树配置是AI翻车的高发区。我让AI生成时钟初始化代码时,它给了一个看起来合理但实际有问题的方案:外部晶振8MHz,PLL倍频到72MHz,但AHB和APB的分频系数设置错了,导致APB1上的外设实际运行在36MHz而不是预期的36MHz——等等,这里其实是对的,但它把APB2的分频设成了2,导致APB2外设跑在36MHz而不是72MHz。
这个问题很隐蔽,因为代码能编译能运行,只是UART的波特率会不对。我是在串口输出乱码时才发现的。排查过程是这样的:先确认波特率设置,9600对应72MHz下的分频值应该是468.75,取整469;然后检查时钟配置,发现APB2实际是36MHz,分频值应该是234.375,取整234。改过来之后串口就正常了。
所以我的做法是:时钟配置这部分不让AI生成完整代码,而是让AI解释每个寄存器的含义,我自己根据参考手册来配。具体来说,我会问AI“RCC_CFGR寄存器的PPRE2位域在不同取值下APB2的分频系数是多少”,然后自己对照手册确认。这样既利用了AI的解释能力,又避免了它记错参数的风险。
2.2 SHT30驱动:I2C通信的AI协作方式
SHT30是I2C接口的温湿度传感器,地址0x44。AI在生成I2C驱动时表现不错,它知道要发送测量命令0x2C06,知道要等待测量完成,知道要读取6个字节的数据并做CRC校验。但它犯了一个典型错误:没有处理I2C的时钟拉伸。SHT30在测量期间会拉低SCL线,如果主机不支持时钟拉伸,通信就会失败。
我用的硬件I2C外设本身支持时钟拉伸,但AI生成的代码里超时设置是100ms,而SHT30在最高精度模式下的测量时间典型值是15ms,最大可能到30ms。100ms的超时理论上够用,但AI没有考虑到I2C总线被其他设备占用的情况。我把超时改成了500ms,并且在等待标志位时加了重试机制。
CRC校验部分AI写得很好,它用了查表法而不是逐位计算,这在嵌入式环境里更高效。但查表法的表数据它给的是错的——我对比了SHT30数据手册里的CRC多项式0x31,AI生成的表有几个值不对。这个错误很危险,因为大部分情况下CRC都能通过,只有在特定数据下才会失败。我是用已知的测试数据验证时发现的。
注意:AI生成的查表数据、CRC多项式、寄存器地址这类“硬编码”信息,必须逐一对照数据手册验证,不能直接使用。
2.3 状态机设计:AI真正发挥价值的地方
如果说时钟配置和驱动代码是AI的弱项,那状态机设计就是它的强项。我让AI设计采集节点的状态机时,它给出了一个很清晰的三状态方案:IDLE(空闲)、MEASURING(测量中)、REPORTING(上报中)。状态转换条件也考虑得很周全,包括测量超时、上报失败重试等情况。
我在此基础上做了调整:增加了ERROR状态用于处理传感器故障,并且把LED指示逻辑和状态机绑定。AI生成的代码框架是这样的:
typedef enum { STATE_IDLE, STATE_MEASURING, STATE_REPORTING, STATE_ERROR } SystemState; SystemState current_state = STATE_IDLE; uint32_t state_timer = 0; void state_machine_tick(void) { switch (current_state) { case STATE_IDLE: if (state_timer >= 2000) { current_state = STATE_MEASURING; state_timer = 0; sht30_start_measurement(); } break; case STATE_MEASURING: if (sht30_data_ready()) { if (sht30_read_data(&temp, &humi) == 0) { current_state = STATE_REPORTING; } else { current_state = STATE_ERROR; } state_timer = 0; } else if (state_timer > 100) { current_state = STATE_ERROR; state_timer = 0; } break; // ... 其他状态 } state_timer += TICK_INTERVAL; }这个框架我基本没改就用了,只是把TICK_INTERVAL从AI建议的1ms改成了10ms,因为主循环里还有其他任务,1ms的tick太频繁了。AI在设计状态机时展现出的逻辑严密性确实超出我的预期,它甚至考虑了状态转换时的资源清理问题。
2.4 UART通信协议:让AI理解“嵌入式约束”
UART上报这部分,AI一开始生成的代码用了sprintf来格式化字符串。这在嵌入式环境里是个隐患——sprintf会链接进一大坨标准库代码,Flash占用可能增加好几KB。我让AI改成不用sprintf的实现,它给出了一个手动转换浮点数的方案,虽然代码长一点,但Flash占用少了2KB多。
具体做法是把浮点数拆成整数部分和小数部分分别转换:
void format_float(char *buf, float value) { int int_part = (int)value; int dec_part = (int)((value - int_part) * 10); if (dec_part < 0) dec_part = -dec_part; // 手动转换int_part和dec_part为字符串 // ... }这个方案不是AI主动想到的,是我明确告诉它“不能用sprintf,Flash空间紧张”之后它才给出的。这说明AI需要你明确告知约束条件,它不会主动考虑嵌入式环境的资源限制。
3. 实操过程与核心环节实现
3.1 项目初始化:从CubeMX到PlatformIO
项目初始化我走的是CubeMX生成基础代码、然后导入PlatformIO的流程。CubeMX负责时钟树和引脚配置的可视化设置,生成初始化代码;PlatformIO负责编译和烧录。AI在这个环节的角色是帮我理解CubeMX生成的代码,以及把HAL库的初始化代码适配到PlatformIO的工程结构里。
具体步骤是这样的:先在CubeMX里配置好时钟(HSE 8MHz,PLL到72MHz)、GPIO(LED用PC13,UART用PA9/PA10)、I2C(用PB6/PB7的硬件I2C1)。然后生成MDK-ARM工程,把Core/Src和Core/Inc目录下的文件复制到PlatformIO工程的src目录。platformio.ini的配置如下:
[env:bluepill_f103c8] platform = ststm32 board = bluepill_f103c8 framework = stm32cube upload_protocol = stlink monitor_speed = 9600 build_flags = -D HAL_I2C_MODULE_ENABLED -D HAL_UART_MODULE_ENABLED这里有个坑:PlatformIO的stm32cube框架默认不启用I2C和UART的HAL模块,需要在build_flags里手动定义。这个信息AI不知道,是我查PlatformIO文档找到的。导入完成后编译,第一次编译报错说找不到stm32f1xx_hal_i2c.h,加上build_flags后就通过了。
3.2 SHT30驱动实现:从AI生成到人工修正
SHT30的驱动实现是整个项目里AI参与度最高的部分。我让AI生成完整的驱动代码,包括初始化、启动测量、读取数据、CRC校验四个函数。AI生成的代码结构很清晰,但有三处需要修正。
第一处是I2C地址。SHT30的7位地址是0x44,但HAL库的I2C函数需要的是8位地址,即0x44<<1=0x88。AI生成的是0x44,直接编译能过但通信失败。这个错误很典型,AI知道7位地址但不知道HAL库的约定。
第二处是测量命令。SHT30的高重复性测量命令是0x2C06,AI生成的是0x2C10,这是低重复性命令。虽然也能工作,但测量精度会下降。我对照数据手册改成了0x2C06。
第三处是CRC校验的初始值。SHT30的CRC计算初始值是0xFF,AI生成的是0x00。这个错误导致所有CRC校验都失败。修正后的CRC函数是这样的:
uint8_t sht30_crc8(const uint8_t *data, int len) { uint8_t crc = 0xFF; for (int i = 0; i < len; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if (crc & 0x80) crc = (crc << 1) ^ 0x31; else crc <<= 1; } } return crc; }修正完这三处之后,SHT30就能正常读取数据了。温度精度0.1度,湿度精度0.1%,满足项目需求。
3.3 主循环与中断的协作:AI给出的方案与调整
主循环的结构AI建议用“定时器中断置标志位、主循环轮询标志位”的方式。具体来说,TIM2配置为1ms中断,在中断里维护一个tick计数器,主循环根据tick计数来判断是否到了采集时间。这个方案比我原来想的“主循环里delay”要好,因为delay会阻塞其他任务。
AI生成的定时器中断代码:
volatile uint32_t g_tick = 0; void TIM2_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE); g_tick++; } }主循环里这样用:
uint32_t last_collect = 0; while (1) { if (g_tick - last_collect >= 2000) { last_collect = g_tick; start_collection(); } state_machine_tick(); }这里有个细节:g_tick是volatile的,因为它在中断里被修改、在主循环里被读取。AI主动加了volatile修饰符,这一点做得对。但AI没有考虑到g_tick溢出问题——uint32_t的tick在1ms间隔下大约49天溢出一次。对于这个项目来说49天足够长了,但如果要做长期运行的设备,需要用减法比较而不是直接比较大小。我用的是g_tick - last_collect >= 2000这种写法,即使溢出也能正确工作。
3.4 低功耗优化:AI没想到但很重要的环节
项目要求用USB供电,所以低功耗不是硬性需求。但我在测试时发现整个系统运行电流大约30mA,其中LED指示灯占了将近10mA。我让AI帮忙分析功耗构成,它给出了一个很详细的分解:MCU运行电流约15mA,SHT30测量时约1mA,LED约10mA,LDO静态电流约2mA。
基于这个分析,我做了两个优化:把LED改成PWM驱动,亮度降低到原来的30%,电流降到3mA;在两次采集之间让MCU进入Sleep模式,通过定时器中断唤醒。优化后平均电流降到了8mA左右。
AI在低功耗优化上给的建议比较泛泛,比如“关闭不用的外设时钟”、“降低主频”这些。具体到STM32F103的Sleep模式配置,它给出的代码基本正确,但漏掉了进入Sleep前需要配置WFI指令和中断优先级的部分。这部分我是参考参考手册补上的。
4. 常见问题与排查技巧实录
4.1 AI生成代码的典型问题速查表
在项目开发过程中,我记录了AI生成代码时最常出现的几类问题,整理成下面这个速查表,方便你在遇到类似情况时快速定位。
| 问题类型 | 具体表现 | 排查方法 | 修正方式 |
|---|---|---|---|
| 寄存器地址错误 | 外设完全不工作 | 对照参考手册检查寄存器偏移 | 手动修正地址 |
| 时钟配置错误 | 通信波特率不对、外设超时 | 用示波器测实际时钟频率 | 重新计算分频系数 |
| 库函数参数错误 | 编译通过但运行异常 | 查HAL库源码确认参数含义 | 按库函数约定修正 |
| 硬编码数据错误 | 特定条件下功能异常 | 用已知测试数据验证 | 对照数据手册修正 |
| 资源占用过大 | Flash/RAM溢出 | 查看map文件分析占用 | 替换为轻量实现 |
| 中断安全问题 | 偶发性死机或数据错乱 | 检查共享变量的原子性 | 加volatile或关中断 |
这个表里的每一类问题我都在项目中实际遇到过。其中“硬编码数据错误”是最难排查的,因为代码逻辑看起来完全正确,只有在特定输入下才会暴露问题。我的经验是:AI生成的任何常量、表格、地址,都要用数据手册或已知测试数据验证一遍。
4.2 编译通过但运行异常:三个真实案例
第一个案例是UART输出乱码。编译烧录后串口助手收到的是乱码,但偶尔能收到一两个正确字符。排查过程:先确认波特率设置,代码里是9600;然后用示波器测PA9的波形,发现实际波特率大约是4800。问题出在时钟配置上,APB2实际频率是36MHz而不是72MHz,导致UART分频值算错。修正时钟配置后问题解决。
第二个案例是SHT30读取数据全为0。I2C通信能收到ACK,但读回来的6个字节全是0。排查过程:用逻辑分析仪抓I2C波形,发现发送测量命令后没有等待测量完成就直接读取了。SHT30在收到测量命令后需要等待至少15ms才能读取结果,AI生成的代码里等待时间只有1ms。把等待时间改成20ms后数据正常。
第三个案例是系统运行几分钟后死机。这个最难排查,因为死机时间不固定。后来在中断里加了一个GPIO翻转来监测中断频率,发现TIM2中断偶尔会连续触发多次。原因是中断标志位清除不完整,AI生成的代码只清了UPDATE标志,但实际还有别的标志位需要清除。改成__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_UPDATE)之后问题消失。
4.3 与AI协作的实用技巧
经过这个项目,我总结了几个和AI协作开发嵌入式的实用技巧,都是踩坑换来的。
第一个技巧是分而治之。不要让AI一次性生成整个模块的代码,而是拆成小函数逐个生成和验证。比如SHT30驱动,我先让AI生成初始化函数,验证I2C通信正常;再生成测量函数,验证能启动测量;最后生成读取函数,验证数据正确。这样出问题时排查范围小很多。
第二个技巧是提供上下文。每次让AI生成代码前,把相关的头文件、宏定义、全局变量声明一起发给它。我试过只发函数需求不发上下文,AI生成的代码引用了不存在的变量,改起来比自己写还费劲。
第三个技巧是要求AI解释代码。对于AI生成的每一段代码,我都会让它解释关键行的作用。这不仅能帮我发现错误,还能学到一些新的实现思路。比如AI在状态机里用了函数指针数组来实现状态处理,这个技巧我之前没想到。
第四个技巧是保留人工审查环节。AI生成的代码我从来不会直接烧录,一定会先过一遍。审查的重点是:寄存器配置、中断处理、资源占用、边界条件。这四类问题AI最容易出错,也最难通过测试发现。
4.4 项目复盘:AI协同开发的实际效率提升
最后说一下效率。这个项目如果完全手写,我估计需要3到4天。实际用AI协同,从开始到稳定运行用了大约1.5天。效率提升主要来自三个方面:代码框架生成省了大约半天,状态机设计省了大约半天,调试思路的提供省了大约半天。但在修正AI错误上多花了大约半天,净节省约2天。
不过效率提升不是线性的。项目越复杂,AI出错的概率越高,修正成本也越大。对于这个规模的项目,AI协同的收益很明显;但如果是一个包含RTOS、文件系统、网络协议栈的复杂项目,AI生成的代码可能需要大量修改,效率提升就没这么显著了。
我个人的体会是:AI在嵌入式开发中最适合的角色是“高级代码补全”和“设计思路提供者”,而不是“代码生成器”。把它当成一个随时可以讨论方案的同事,而不是一个自动写代码的工具,协作效果会好很多。你给它越多的上下文和约束,它给出的方案就越靠谱。反过来,如果你只是丢一个需求过去等它生成完整代码,大概率要花更多时间在调试上。