ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32+AI协同开发:SHT30温湿度采集与上位机绘图

STM32+AI协同开发:SHT30温湿度采集与上位机绘图 1. 项目开局从“AI能写代码”到“AI能一起做项目”上一篇文章里我们完成了第一个AI协同开发项目的原型——一块STM32板子通过串口输出点灯日志。说实话那一步多数人带着AI走一遍都能跑通毕竟点灯算嵌入式界的“Hello World”。但我的感受是真正让AI编程产生质变的从来不是“让AI吐出一段能编译通过的代码”而是把整个项目当成一个团队工作流来对待让AI在每一个环节都扮演明确角色。这篇是第二个AI协同开发项目的完整复盘。我把目标定在了一个稍微有真实感的任务上用STM32F103读取SHT30温湿度传感器数据通过串口按协议帧上报再用上位机Python脚本解析并绘制曲线。这个项目涉及I2C驱动、传感器时序、数据结构设计、串口通信协议、上位机解析五个模块横跨嵌入式开发和上位机开发复杂度刚好够讲清楚“人机协同”的分工边界。之所以选这个题目还有一个私心。很多朋友问我嵌入式软件到底怎么跟AI配合才不翻车我观察到的常态是两种极端——要么完全自己闷头写把AI当个搜索引擎要么把整个需求一粘贴让AI一次性生成一千行代码然后陷入无休止的“改成什么样”循环。第二个项目我刻意做了中间路线AI负责能标准化的劳动我负责所有需要物理直觉和现场判断的部分。全程跑下来这个分工方式意外地顺也踩到了不少有价值的坑。下面我把整个过程、关键代码、对话思路、排查过程全部摊开来讲。2. 与AI协同时的提示词方法论怎么把工程师脑子里的“潜规则”翻译给AI2.1 核心认知AI不是嵌入式工程师但它可以是你的“高级代码民工”很多人一开始用AI写嵌入式代码最直观的感受是“它能写但写出来的东西总差口气”。差在哪不是语法而是它不知道你的硬件环境里有哪些隐性约束。比如寄存器地址它知道但你的板子外部接了多大的上拉电阻它不知道I2C通信它知道但你的传感器在3.3V供电下需要多少延时它不知道。所以我在这个项目里给自己定了一条规矩凡是涉及硬件行为的内容我自己定边界凡是涉及纯逻辑生成的内容大胆交给AI。更直白一点说AI负责写“脑子里已经有清晰逻辑的代码”我负责提供“它不可能猜到的物理事实”。比如SHT30的I2C从机地址是0x44还是0x45取决于ADDR引脚接法——这种信息AI经常模棱两可但对我这个拿着板子的人来说是板上钉钉的事实。为此我给每个模块的提示词都加入了三段式结构背景正在开发STM32F103上的温湿度采集程序MCU主频72MHzI2C1用于连接SHT30中断优先级分组为4。 任务实现SHT30的初始化函数和单次触发读取函数。 约束I2C通信需带超时处理超时时间不超过10ms函数内部禁止使用阻塞式延时返回值为错误码取值0表示成功。 验收给出调用示例并提供不同错误码的释义。这一段提示词大概花费两分钟但它直接决定了AI输出代码的质量上限。说白了你把边界定义得越清楚AI的“自由发挥空间”就越小产出就越可靠。这一点在后续调试阶段帮了大忙——所有我提前声明过的约束几乎没有在测试中翻车所有我漏声明的地方AI基本都踩了一遍。2.2 嵌入式特有的“潜规则”怎么在提示词里体现嵌入式开发有很多约定俗成但文档不明确的东西。比如中断服务函数要短小精悍。AI不知道你的中断服务里能不能放打印但它需要被你提醒。寄存器操作需要原子性。如果你在多个中断里同时操作一个全局变量AI不会自动帮你加临界区保护。volatile关键字的必要性。AI经常忘记给被中断修改的变量加上volatile。这不是它水平不够而是它没有“变量会被硬件修改”这个物理常识。初始化顺序会直接影响系统行为。时钟、GPIO、外设、中断先后顺序错了表现千奇百怪。我在业务层提示词里单独加了一句硬性要求提示所有由中断修改的全局变量必须声明为volatile所有外设操作函数建议放在单独.c文件启用编译器自带的静态检查选项——-Wall -Wextra。这句话的效果非常直接。AI生成的UART接收缓冲代码里凡是接收计数变量一律带了volatile后来我在代码评审时确认它甚至主动把状态机变量也加了volatile。这让我意识到AI不是不懂这些规则而是需要你把它放在“必须遵守”的位置上。你用工程师对实习生的语气去约束它它就拿出对标的专业度。2.3 一次只让AI做一个模块避免“大爆炸式”生成第一次做AI协同项目的时候我喜欢把一个完整的项目需求扔给AI让它一次生成所有文件。结果是代码能编译结构也算合理但一旦出问题排查范围太大你根本不知道是该怀疑它生成的逻辑还是该怀疑我对硬件的假设。这次我改成“一次一模块、模块间接口先行”的策略。具体流程是我先定义好所有模块之间的接口函数原型数据结构。让AI逐模块实现每个模块只对应一个任务描述。每个模块生成后立即静态检查再和上一个模块做编译联调。所有模块全部联编通过后上硬件调试。比如传感器驱动模块和协议封装模块我先在工程里写了一个protocol.h只声明了帧格式和打包函数然后再让AI去填充.c实现。这样虽然交流次数变多但每轮生成的内容都能快速验证即使出了问题范围的边界也极其清晰。2.4 别忘了让AI自己当“评审员”交叉验证逻辑第二层协同是让AI“审”自己。我经常在一个模块写完后把代码贴回给AI要求它按嵌入式代码规范做一次评审特别关注以下问题- 是否存在未定义行为的C语言写法 - 是否有不必要的动态内存分配 - 是否有中断上下文中的阻塞操作 - 是否有位域、对齐、大小端相关的隐患 - 是否缺少异常分支处理这一轮AI给出的意见通常质量很高因为代码已经摆在桌面上“挑刺”比“创作”更适合大模型发挥。我在SHT30的驱动代码里就靠这一招抓出了两个隐患一个是未初始化局部变量一个是在I2C发送时没有处理NACK信号。这些都是编译器不会警告但实际运行会出问题的点。3. 实操过程底层驱动、状态机与通信协议的一步步实现3.1 硬件接线与工程准备先交代一下硬件环境方便后面复现主控板STM32F103C8T6蓝Pill板传感器SHT30温湿度模块I2C接口默认地址0x44通信接口USART1PA9/PA10波特率115200I2C接口I2C1PB6/PB7外部4.7k上拉软件环境STM32CubeMX Keil MDKAC6编译器附加脚本Python 3.10 pyserial matplotlib工程初始化我用的是STM32CubeMX生成HAL库代码。这是嵌入式AI编程的一个隐形前提尽量让AI处理具体业务逻辑而不要让AI从零搭工程。CubeMX生成的初始化代码具有可预测性AI不需要猜你的时钟树和外设是怎么配置的你只需要告诉它HAL库的版本和主要外设它就能写出匹配风格的代码。3.2 I2C读取SHT30AI生成的驱动代码与我的关键修改SHT30读取温湿度标准流程是三步发送测量命令0x2C 0x06等待约50ms周期测量模式可不等读取6字节数据温度2字节、CRC1字节、湿度2字节、CRC1字节。但实际工程里很少有人直接在应用层打交道基本都要封装成模块。以下是AI生成的初始化与单次触发读取的核心代码我做了少量注释/* sht30.c */ #include sht30.h #include i2c.h #define SHT30_ADDR (0x44 1) #define SHT30_CMD_MEASURE 0x2C06 uint8_t SHT30_Init(void) { HAL_StatusTypeDef status; uint8_t cmd[2] {0x2C, 0x06}; status HAL_I2C_Master_Transmit(hi2c1, SHT30_ADDR, cmd, 2, 10); return (status HAL_OK) ? 0 : 1; } uint8_t SHT30_ReadData(float *temp, float *humi) { uint8_t buf[6]; HAL_StatusTypeDef status; status HAL_I2C_Master_Receive(hi2c1, SHT30_ADDR, buf, 6, 10); if (status ! HAL_OK) { return 2; /* I2C无应答或超时 */ } if (SHT30_CheckCRC(buf, 2) ! buf[2]) { return 3; /* 温度数据CRC校验失败 */ } if (SHT30_CheckCRC(buf 3, 2) ! buf[5]) { return 4; /* 湿度数据CRC校验失败 */ } *temp -45.0f 175.0f * ((buf[0] 8 | buf[1]) / 65535.0f); *humi 100.0f * ((buf[3] 8 | buf[4]) / 65535.0f); return 0; }整体逻辑没问题但我做了三处修改在SHT30_Init中增加软件复位命令发送。有些模组上电后I2C状态不稳定直接发测量命令会石沉大海。先发0x30 0xA2软复位再延时20ms会让传感器进入一个已知状态。温度计算公式后的立即数替换。原代码用了-45.0 175.0 * raw / 65535.0这没错但它把常量都写在表达式里工程上有经验的人会习惯提炼成宏定义方便统一边界。我改成#define SHT30_TEMP_OFFSET (-45.0f)之类的宏以后校准系数只需要动一处。把超时时间从100ms缩短到10ms。AI按自己的“稳妥习惯”给I2C操作安排了大量超时。但在嵌入式实时系统里I2C总线一旦异常你要的是快速报错而不是挂死在那儿。10ms对I2C通信来说已经极其充裕。这里说说为什么我对CRC校验这么坚持。SHT30的6字节数据里每两个数据字节后跟一个CRC字节。别嫌麻烦别图省事直接丢数据——I2C总线长度超过20cm时噪声干扰是看得见的没有CRC保护你解析出的温度可能隔几分钟跳一次离谱值。代码里我用了AI生成的SHT30_CheckCRC函数它实现了标准的CRC8多项式0x31整整一屏代码但一次就通过验证了这就是AI擅长的工作——算法实现没悬念交给它省我半小时。3.3 业务层用状态机替代“延时循环”的思维转变传感器读取本身不复杂复杂的是“主循环里怎么处理测量节奏”。有些新手看到SHT30数据手册里写了“等待时间最长20ms”就直接用HAL_Delay(20)硬扛过去了。这种代码在裸机点灯项目里没毛病但在一个真实系统里属于定时炸弹——Delay会阻塞中断以外的所有任务。我在业务层提示词里给了AI一个硬约束不允许使用HAL_Delay实现周期逻辑必须采用非阻塞状态机。AI输出了一套基于系统节拍HAL_GetTick()的状态机骨架核心逻辑类似这样typedef enum { ST_IDLE, ST_TRIGGER, ST_WAIT, ST_READ, ST_PARSE, ST_REPORT } SensorState_t; void Sensor_Task(void) { static SensorState_t state ST_IDLE; static uint32_t lastTick 0; switch (state) { case ST_IDLE: if (Tick_GetInterval(lastTick, 1000)) { /* 每1秒节奏 */ state ST_TRIGGER; } break; case ST_TRIGGER: if (SHT30_StartMeasure() 0) { state ST_WAIT; } else { state ST_IDLE; /* 出错则下一轮重试 */ } break; case ST_WAIT: if (Tick_GetInterval(waitStartTick, 30)) { /* 非阻塞等待30ms */ state ST_READ; } break; case ST_READ: if (SHT30_ReadData(temp, humi) 0) { state ST_PARSE; } else { state ST_TRIGGER; } break; case ST_PARSE: Protocol_BuildFrame(FRAME_TYPE_TEMP_HUMI, temp, humi); state ST_REPORT; break; case ST_REPORT: USART1_TransmitFrame(txFrame); state ST_IDLE; break; default: state ST_IDLE; break; } }这段代码AI生成后几乎没做改动就通过了评审。为什么这个切换那么顺因为我在提示词里给了两个弹药一是Tick_GetInterval这个工具函数的接口说明二是“状态必须显式迁移、禁止阻塞等待”的约束条件。AI理解起来毫无障碍。说句实在话大部分“AI生成代码不省心”的情况问题都出在提示词没有把这种设计偏好写明白。你告诉它“写一个温湿度采集任务”它能给你写出一个跑得通但难维护的版本你告诉它“用有限状态机实现一个非阻塞采集任务并在每个状态迁移前重置看门狗”那它输出的质量立马不同。3.4 协议层定义帧格式AI写打包和解包两端上位机和下位机之间的通信最忌临时协商格式。所以在动手写任何代码前我先定下了协议帧结构一共6个字段帧头(2字节 0xAA55) 类型(1字节) 长度(1字节) 数据(可变) CRC16低(1字节) CRC16高(1字节)对你没有看错我特意把CRC写成了低字节在前、高字节在后。因为STM32是小端设备串口发出时直接按小端字节序排上位机用Python的struct.unpack(H, ...)就能一次性解出不需要手动调换高低位。这是嵌入式通信里一个微小但让人舒适的设计决策。上下位机的通信模块我分成两半一半下位机C代码一半上位机Python代码。两边的提示词我都给了同一个协议定义并且加了一句话“从机端负责打包主机端负责解包两边遵循相同的字节序和CRC算法。”AI分别完成了这两段代码。我在Python端特意加了一个坏帧槽位用于统计通信误码率——实测下来1500帧数据里没有出现过CRC错误这个协议设计基本够用。Python端解析脚本核心部分如下# parse_serial.py import serial import struct import matplotlib.pyplot as plt START_BYTE 0xAA55 FRAME_TYPE_TEMP_HUMI 0x01 def crc16_modbus(data: bytes) - int: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return crc def parse_frame(frame: bytes): if len(frame) 4: return None start, ftype, flen struct.unpack(HBB, frame[:4]) payload frame[4:4flen] recv_crc struct.unpack(H, frame[4flen:6flen])[0] calc_crc crc16_modbus(frame[:4flen]) if recv_crc ! calc_crc: return None temp struct.unpack(f, payload[:4])[0] humi struct.unpack(f, payload[4:8])[0] return temp, humi这一个小脚本把我整个调试效率拉高了不止一个量级。之前用串口助手盯着十六进制数据肉眼解析那是硬核对眼睛的摧残现在脚本一跑连续500帧的温度曲线直接画出来数据变化趋势一眼就能看出异常。这就是AI协同开发的第二价值下位机忙了半天上位机工具链也要跟上而这个环节AI帮我也节省了至少一天。什么你说写这个Python脚本内行应该很快没错但有了AI之后我甚至可以把这部分时间压缩到10分钟省出来的时间全用来做硬件侧的分析这买卖太值了。4. 踩坑记录与排查技巧实录4.1 中断服务函数里放打印直接“教做人”第一次联调时我图省事直接在UART接收中断服务函数里加了一行printf想看调试信息。当时心里想“我是调试模式跑一下应该没事”结果一上电整个系统直接锁死看门狗疯狂复位。排查了大概10分钟才意识到问题printf走的是阻塞式串口发送波特率115200一个字符约87us一个调试字符串几十个字符就是几毫秒的阻塞而这段时间里再来一个UART中断中断标志没被及时清掉系统就卡死了。这个问题的典型教科书答案人人都知道但单独拎出来速度反应仍然慢。后来我把自己的教训整理成一段话吐给了AI让它告诉我如果有这种需求应该怎么做。AI给出的答案就是标准做法中断里只做FIFO入队主循环中消费队列把打印挪到任务上下文。这个方案不稀奇但帮我清晰梳理了一遍完整的处理思路。更关键的是我从此记住了这条铁律注意中断服务函数里绝对不要出现“可能阻塞”的调用包括printf、HAL_Delay、HAL_I2C阻塞接口、malloc等。宁可多花一点时间封装队列也不要图省事祸害整个系统。4.2 volatile缺失让AI生成的循环“复活”了这是这个项目里最哭笑不得的一个Bug。主函数里的一个状态标志static int measure_done 0;这个变量在一个定时器中断里被置1主循环里判断它并执行后续逻辑。AI生成的代码里没有加volatile。在-O0优化等级下一切正常代码能跑但当我打开AC6编译器的-O2优化后程序直接卡死在循环里while (!measure_done) { /* 空转 */ }原因很基础编译器认为measure_done在循环期间没有被修改于是把它优化到了寄存器里而中断对这个内存地址的写入在寄存器视角中完全不可见。但浪费了我半天时间才想到这个小学二年级就该知道的知识点。后来我在提示词和代码评审清单里都把volatile列成了必检项AI也没有再犯过同类错误。这个项目顺带让我形成了一套代码审查的肌肉记忆看变量与中断的关系时第一个反应就问它是否被中断修改如果是有没有加volatile4.3 初始化顺序问题第一个数据包为什么要等很久才出现第二个比较隐蔽的问题是传感器初始化。AI生成的SHT30初始化里有一个软复位加延时大约20ms传感器数据采集状态机里开机后的第一个采集周期要等1秒才触发。系统启动后串口要等大约1.5秒才出现第一帧数据。这个“慢启动”对功能没什么影响但每次烧录后等待那1.5秒总觉得事情没跑起来。后面我仔细看时间线才想明白问题在CubeMX生成的代码里I2C外设的初始化在主逻辑之前但传感器模块调用初始化时第一个测量命令已经被发出去一次。传感器返回的数据往往刚好是软件上电后的第一帧原始值里面可能含无意义的噪声。解决办法是把SHT30初始化放在主逻辑最前面并且在状态机里忽略前两帧数据。这个坑不算大但它提醒了我一个做嵌入式的基本习惯上电瞬间的传感器数据默认当垃圾数据丢掉。4.4 和AI“对线”实录当它硬要用阻塞延时这个项目里最让我印象深刻的“人机拉锯战”发生在要求AI优化轮询等待的一段逻辑时。原始AI代码里读SHT30数据之前直接用了一个for循环做死等for (volatile int i 0; i 5000; i); /* 忙等 */我让AI改成非阻塞延时它改成了uint32_t start HAL_GetTick(); while ((HAL_GetTick() - start) 30);没看出区别这个写法本质上还是“忙等”只不过从指令周期等待变成了系统节拍等待。它在单任务裸机环境没毛病但放在我那个已经有多个状态机的系统里几毫秒的忙等都会破坏时序敏感的数据采集节奏。我把系统设计解释清楚后AI才改成了状态机里的ST_WAIT状态。这个过程让我体会很深AI偶尔会“偷懒”选择自己最舒服的写法而不是最符合系统架构的写法。这时候工程师的判断力是唯一防线。这不是AI的锅是提示词和需求定义不够细致的锅。4.5 常见问题速查表这个表是我建议你保存到本地笔记里的后面项目复制粘贴也方便。表现可能原因排查方向改法系统上电后死循环中断修改的变量没加volatile查看生成汇编确认变量读自内存还是寄存器给相关变量加volatile或改用原子操作串口输出乱码波特率不匹配或时钟树配置错用逻辑分析仪抓波形测量实际波特率严格对照CubeMX时钟树配置传感器读取偶发错误I2C总线时序不稳定检查上拉电阻、总线长度、干扰源降低I2C速率或加长超时重试逻辑第一个数据包缺失或异常初始化时序问题首帧数据为残留值增加延时或直接丢弃首帧状态机里增加“预热”状态程序能在-O0跑-O2卡死编译器优化改变了数据访问行为排查volatile缺失或未定义行为代码评审阶段就检查优化相关的隐患上位机显示数据不定期跳变协议解析没有过滤坏帧统计CRC错误率确认数据来源在协议层丢弃校验失败的帧4.6 用编译器和静态检查工具兜底这个项目我用了两轮工具链来做安全网Keil自带的静态分析或-Wall -Wextra编译选项和开源工具cppcheck。AI生成的代码质量虽然不低但无法替代工具链检查。实际操作中我的流程是AI生成代码 → 人工过一遍结构和逻辑 → gcc/cppcheck扫描 → Keil编译 → 硬件调试。每一步都能挡掉一类问题。特别是cppcheck这种工具它会报告未初始化变量、内存泄漏、无效逻辑分支等编译器不给严重警告的问题。AI生成的代码里有几个隐藏的“类型转换可能溢出”的提示就是cppcheck扫出来的。对于嵌入式这种“低容错”环境把静态检查前移到代码生成后的第一关是性价比非常高的习惯。5. 复盘与学习方法建议5.1 从AI手里拿到代码后你怎么把它消化成“自己的东西”文章看到这里你可能会问AI把代码都写了我还学什么这个疑问我在带新人时经常遇到。我的回答是AI能替你写代码但替不了你理解系统。真正有价值的嵌入式能力恰恰体现在AI不知道怎么处理的地方。我建议拿到AI生成的代码后至少做三件事。第一读一遍主循环和状态机确认系统的执行流和每一帧数据的生命周期。第二自己画一张数据流图从传感器到串口再到上位机标出每一层的格式转换、错误处理、时序约束——这个图AI不会帮你画画出来才能说你真正理解了系统。第三对每一处错误码和异常分支问自己一个问题“这个错误码真的能被上层正确处理吗”如果答案是“不确定”那说明系统边界没有闭合你还需要补逻辑。5.2 把AI当成“代码评审员”让它在开发周期的每个节点都参与很多人的AI使用习惯是“写代码才找AI”这其实是极大的浪费。我在这个项目里的使用频率排序是设计讨论 代码生成 代码评审 问题排查 文档生成。举例来说我在协议格式设计阶段就抛了两套备选帧格式给AI让它从容错性、解析效率、扩展性三个维度做对比。AI给出的分析里有一条让我眼前一亮它提醒我CRC16的覆盖范围应该包含“长度字节”而不是只覆盖“数据段”这样可以防止长度字段被篡改后导致解析越界。这个点我自己不会想到那么细但AI能指出原因在于它看过大量协议设计案例对这种“隐藏边界”有统计意义上的直觉。这种用法AI就不是“写代码的”而是一个不会累的架构师顾问。5.3 新手怎么通过AI项目学习嵌入式给刚入门嵌入式且想用好AI的朋友一个路线建议不要一开始就拿真实硬件项目练手。先用AI做三个基础项目一是GPIO按键消抖和LED状态机切换二是串口协议收发自解析三是I2C读取传感器随便选一个然后把它们组合成一个完整项目。组合之后再引入中断、定时器、看门狗。AI在这个过程中主要充当“解释器”——当你不理解一个函数的执行过程时直接贴给AI让它逐行解释在硬件上发生什么比翻上百页数据手册效率高得多。但这就要说到一个争议热词“AI下的嵌入式软件怎么学”。我的观点是核心不是“学AI”而是“学工程”。AI不会消除你对时序、中断、存储、功耗的理解需求反而会提高这些知识的杠杆率——你用AI把标准代码生成完之后省下来的时间正好可以去啃最难啃的硬件手册。所以我的学习路线非常老派却有效先用AI快速搭出能跑的系统再把系统拆掉重写一遍第三遍尝试不使用任何AI工具独立完成。三遍下来基本就能形成自己的工程直觉。最后再分享一个我个人的小习惯每次AI帮我解决了问题我都会在工程项目笔记里留下一段“为什么它解决得好”的复盘配一段以后要复制的提示词模板。这个模板库积累到二十条左右的时候你基本不会再被AI的代码质量困扰了。因为你会越来越清楚哪些指令能指挥这台“机器”稳定产出而不会让它临场发挥。
返回列表