带 Night 系列是我平时记录折腾过程的一个固定栏目,这一期的情况特别典型:让 Codex 帮我写一段单片机程序,目标很简单——点亮五颗 LED,结果编译链接一路零报错,烧录也顺利,按下复位键之后板子上一颗灯都没亮。
如果你也在用 AI 写单片机代码,或者正准备这么做,这一篇建议看完。因为“编译零报错”和“程序真的能跑”之间,隔着的是整个嵌入式开发的真实世界。
1. 项目概述:AI 写单片机程序的真实定位
1.1 核心需求解析
这次项目的核心需求其实特别朴素:做一个小的演示电路,用单片机控制五颗 LED 按顺序点亮,形成流水灯效果。硬件上我手头有一块常用的 STM32 核心板,五颗 LED 接在几个 GPIO 上,每个 LED 串联限流电阻,电源由 USB 口提供。
表面上看,这种级别的程序谈不上复杂,随便找个例程改改就能用。但我想测试的是另外一个问题:AI 编程工具在单片机开发里到底能帮到什么程度?它能自己理解 GPIO 配置、时钟树、寄存器操作这些底层细节吗?还是说只能生成一堆看起来像那么回事、实际上没法用的代码?
所以我给 Codex 的需求描述很简单:“使用 STM32F103C8T6,通过 GPIO 控制五个 LED 流水灯,每个 LED 点亮 200ms,循环执行。请给出完整的初始化代码和主循环代码。”
1.2 方案选型背后的权衡
选 STM32F103C8T6 这颗芯片,主要原因是资料多、工具链成熟、即使出错也容易在网上找到对照。如果选一颗小众芯片,AI 训练语料里信息太少,生成结果大概率会“编”出根本不存在的寄存器名字。
代码生成工具选 Codex,是因为它在多轮对话、上下文理解上的表现比较稳,更适合反复追问和修改。实际执行下来,它确实能把“初始化时钟 → 配置 GPIO → 主循环控制”这套逻辑组织得相当完整,甚至比我预期的还要顺。
但真正关键的问题不在代码能不能生成,而在于:代码通过编译和程序能运行,这是两码事。
2. Codex 生成单片机程序的核心体验拆解
2.1 需求提问方式对生成质量的影响
先用真实经验下个结论:AI 生成嵌入式代码,提问的颗粒度直接决定结果质量。
我第一次的提问就比较笼统,只说“帮我写一个 STM32 的流水灯程序”,Codex 给出的代码用的是标准外设库(SPL),而且是老版本的写法,寄存器名称都对,但跟我手上这块板子的引脚映射完全不匹配。原因很简单——它不知道我的 LED 具体接在哪些引脚上。
第二次我把需求补充完整:“PA0-PA4 接五颗 LED 的正极,负极经过 330Ω 电阻到 GND,请用 HAL 库实现。”Codex 立刻调整了代码,GPIO 初始化部分完全匹配我的硬件,逻辑也清晰很多。
这里有一条非常实用的经验:你需要把硬件连接的细节当成需求的一部分告诉 AI,而不是把“写代码”这件事全丢给它。AI 在嵌入式领域最大的短板不是语法,而是对具体硬件的无知。你跟它说清楚引脚、电平、外设型号,它就能输出接近可用的代码。
// 第一次提问生成的代码(节选) // 问题:没有指定引脚,Codex 用了 PB0-PB4 GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);// 第二次补充硬件信息后生成的代码(节选) // 指定 PA0-PA4,完全匹配实际电路 GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);对比很明显,补充了硬件细节之后,代码的针对性完全不同了。
2.2 AI 生成代码的常见缺陷与审查方法
说完了好的一面,必须讲讲 AI 生成单片机代码的典型缺陷。这一期我踩到的,以及过去反复踩到的,基本可以归纳成四类:
第一类是引脚映射错误。AI 经常凭训练数据中的“常见例程”假设引脚,比如默认用 PB0、PB1,或者干脆编一个不存在的 GPIOB 时钟使能方式。这类错误每次编译都通过,但板子上就是不亮。
第二类是时钟配置缺失或不一致。HAL 库开发时,SystemClock_Config 这个函数如果配置不对,或者跟实际晶振不匹配,整个系统跑起来就是乱的。AI 有时候会默认给你生成 8MHz 外部晶振的配置,而你的板子用的是 25MHz 晶振,程序同样能编译,但跑起来时序全错。
第三类是外设初始化顺序错误。比如先初始化 GPIO 再初始化时钟(正确做法是先时钟后外设),或者忘了使能 GPIO 对应的总线时钟。这类问题在纯逻辑上看不出来,因为编译器根本不会检查时序关系。
第四类是主循环逻辑问题。七拐八绕的延时嵌套、外层循环和内层循环变量互相干扰、延时时间过短导致视觉上看不清闪烁效果等。代码能跑,但表现跟需求不一致。
审查这些问题的有效方法,不是逐行读代码,而是拿着引脚定义去对硬件原理图:每个被初始化的引脚是不是你要用的引脚,每个引脚对应的时钟有没有被使能,外设初始化发生在时钟使能之后还是之前。这三项检查完,AI 代码的八成问题都能被提前拦截。
2.3 为什么“编译零报错”很容易被误读
这一期标题里的“0 报错”三个字,我特意加了引号,因为它是真的很容易骗人。
嵌入式开发里,编译器能验证的只是语法、类型、作用域之类的基础规则。它没办法知道你配置的 GPIO 引脚上到底接了什么,没办法判断你这个时钟配置跟晶振是否匹配,更没办法模拟“按键按下、LED 亮”这种行为级验证。
有个很好的类比:编译通过相当于文章没有错别字、语法通顺,但不代表文章的观点是对的。单片机程序也一样,编译器保证的是“无边无际的猜测之外没有语法错误”,而不是“程序在硬件上如期运行”。这一点,AI 生成的代码跟人类写的代码没有任何区别,只是 AI 的“错别字率”更低而已。
所以当你看到 AI 写了一段代码、编译零报错的时候,正确的下一步是:烧录、运行、观察硬件表现,而不是直接认为程序没问题。
3. 五颗 LED 全不亮的排查全过程
3.1 从一个最可疑的点开始排查
烧录完代码之后,我按下复位键,五颗 LED 纹丝不动。这时候第一反应不是责怪 Codex,而是按顺序过一遍可能出问题的环节。
排查的第一步就是确认程序到底跑没跑起来。我做了这样一件事:在 main 函数入口、SystemClock_Config 之后、GPIO 初始化之后分别添加了一个翻转测试引脚的操作——拿一个空余的 GPIO 接示波器,用一段简单的代码让它在循环里不断翻转电平。如果示波器上有方波,说明程序在正常执行;如果没有,那问题大概率出在更早的阶段。
实测的结果是:程序确实在跑,主循环的翻转操作正常输出方波。这排除了芯片本身没有运行的情况,把问题缩小到了 GPIO 配置和外部电路。
3.2 用万用表和示波器锁定故障点
下一步是量电压。我把万用表打到直流电压档,红表笔接 LED 正极,黑表笔接 GND,按下复位之后测到的电压是 3.3V——GPIO 端口确实输出了高电平。
这里就出现了一个矛盾:代码在跑,GPIO 有电压,LED 却不亮。按常规逻辑,LED 应该亮才对。那问题在哪?
我再量 LED 两端的电压,发现一个是 3.3V,另一个也是 3.3V。LED 左右两端电压一样,意味着中间没有电流流过。限流电阻在电路板上,我沿着铜箔找,找到 LED 正极到 GPIO 引脚之间居然有一段虚焊,看上去连上了,实际上断裂了。
这就是那一次的真正故障点:硬件问题,不是代码问题。
所以我重新焊了一下这个引脚,再按复位键,五颗 LED 依次亮起,流水效果也出来了。
这里分享一个经验,排查硬件问题时,不应该先怀疑 AI 写的那几十行代码,而应该先确认最基本的电气连接。代码再烂,不会导致“LED 两端电压相同”,只有断路才会。
3.3 代码复审时发现的隐藏问题
硬件修好之后 LED 亮了,但我没有就此收手,而是把 Codex 生成的代码又审了一遍,果然发现了几个典型问题:
当时 Codex 生成的 GPIO 初始化代码里面,没有对 LED 对应的 GPIO 时钟使能调用做检查。它调用了__HAL_RCC_GPIOA_CLK_ENABLE(),但如果你仔细看 STM32F103 的 HAL 库文档,这个函数必须出现在 GPIO 初始化之前。Codex 确实把它放在了前面,这点没错。但它在 SystemClock_Config 之前就调用了 GPIO 时钟使能,虽然不影响正常操作,但属于不够严谨的写法。
另一个问题是延时实现。Codex 用了HAL_Delay(200),在主循环里实现 200ms 的间隔,这个没问题。但如果放到中断里就可能引发问题——HAL_Delay 依赖 SysTick 中断,如果中断优先级配置不当,可能导致延时卡死。这一点在这次项目里不致命,但值得记在心里。
// 复盘时发现的细节:GPIO 时钟使能应该紧跟 SystemClock_Config int main(void) { HAL_Init(); SystemClock_Config(); // 先配置系统时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 再使能 GPIO 时钟 MX_GPIO_Init(); // 最后初始化 GPIO while (1) { // 流水灯逻辑 } }这个顺序要求其实不是硬性的——大多数情况下把 GPIO 时钟使能放在 SystemClock_Config 之前也能跑,但正确顺序能避免后续添加外设时出现莫名其妙的怪问题。我把这次的经验总结成一句话:AI 生成的嵌入式代码,语法上往往是合格的,工程习惯上经常是不合格的。
3.4 为什么这类问题特别容易出现在 AI 编程场景里
多做了几个 AI 生成嵌入式代码的实验之后,我慢慢理解了一个现象:AI 的“逻辑正确”常常只存在于它自己生成的那一小段代码里。
嵌入式程序是一个“系统编排”的过程——时钟树、电源管理、GPIO 复用、外设中断、低功耗模式这些东西是互相耦合的。AI 擅长生成一个函数、一个模块,但不太擅长把控一个完整系统里不同模块之间的接线关系。因此,AI 写的代码如果每个模块单独看,往往都说得通,但放在一起跑,偶尔就会出现微妙的时序、资源冲突问题。
这次的“LED 全不亮”能顺利定位到硬件虚焊,多少有点运气成分。更多时候,问题藏在代码跟代码的缝隙里。比如两个外设同时初始化时,AI 生成的代码可能不会注意引脚复用冲突;又比如它在配置一个 GPIO 时,不会自动检查这个引脚是不是已经被别的外设占用了。
所以我的建议是,用 AI 写嵌入式代码的正确姿势不是“让它全写”,而是“让它写模块,你来编排系统”。让 Codex 负责生成 GPIO 初始化的代码块、生成某个外设的读写函数,再由你把它们按正确的初始化顺序组装起来,并检查引脚冲突、时钟配置这些跨模块的问题。这样既能享受 AI 带来的效率提升,又不会把系统可靠性全部押在 AI 的生态位上。
4. 常见问题速查:AI 单片机程序实战避坑手册
这一期真正想分享的干货,是下面这张问题排查表。我做嵌入式这几年,跟 AI 协作开发中遇到的所有有代表性的问题,基本已经沉淀在这一张表上了。
| 问题现象 | 优先怀疑环节 | 排查方法 | 解决思路 |
|---|---|---|---|
| 编译零报错但 LED 不亮 | 硬件电路 / GPIO 引脚映射 | 万用表量 LED 两端电压;示波器观察 GPIO 波形 | 优先排查虚焊、断路、接线错误;再核对代码引脚是否匹配原理图 |
| 程序烧录后无任何反应 | 时钟配置、启动文件 | 示波器量晶振引脚、检查 NRST 电平 | 核对 SystemClock_Config 与晶振频率是否匹配 |
| LED 亮度异常或不稳定 | GPIO 模式、上下拉配置 | 查看 GPIO_InitStruct.Mode 是否设为推挽输出 | 确认使用 GPIO_MODE_OUTPUT_PP 而不是开漏或模拟模式 |
| 上电后 LED 全亮但不闪烁 | 主循环逻辑 / 延时 | 单步调试或在循环里加翻转测试引脚 | 检查 HAL_Delay 是否被中断阻塞,循环逻辑是否有死路 |
| 多个 LED 中只有部分亮 | GPIO 引脚编号错误 | 逐个比对原理图与代码引脚号 | AI 经常猜测引脚,必须手工核对 |
| 程序偶尔跑飞或复位 | 电源滤波、看门狗配置 | 示波器观察 VDD 纹波,检查 IWDG/WWDG 初始化 | 给电源端并联 100nF 电容,确认代码里没有误开看门狗 |
| 下载时报错无法连接 | 烧录器驱动 / BOOT 引脚 | 检查驱动是否安装,确认 BOOT0 电平 | 重新安装驱动,确认 BOOT0 拉低后复位再下载 |
| 代码能编译但运行时卡死 | 中断优先级、SysTick | 暂停调试查看 PC 指针位置 | 检查 SysTick 中断优先级与 HAL_Delay 调用环境是否匹配 |
| AI 生成的寄存器名不存在 | 芯片型号差异 | 编译报错信息定位到具体行 | 要求 AI 遵循当前芯片的 HAL 库接口,而不是通用寄存器写法 |
这些问题的核心规律就一句话:编译通过只能证明代码没有基础语法错误,不能证明硬件配置和时序没有问题。
4.1 最容易踩的五个坑
展开说说其中五个高频坑。
第一个是GPIO 时钟未使能。不使能总线时钟,GPIO 寄存器写什么都没反应,但这在编译器眼里是完全合法的代码。AI 偶尔会漏掉__HAL_RCC_GPIOx_CLK_ENABLE()这行,或者放错了位置。排查方法很简单:注释掉这一行代码,看编译是否会报错——如果不会报错,说明这个时钟使能调用大概率存在问题。
第二个是引脚编号写错但类型匹配。GPIO_PIN_0到GPIO_PIN_4看习惯了,容易把GPIO_PIN_0写成GPIO_PIN_5,或者把GPIOA写成GPIOB。这类错误编译器完全无法识别,只有对照原理图才能发现。应对方式不复杂:拿到 AI 生成的代码后,第一步就是拿原理图逐行核对引脚。
第三个是高低电平逻辑反了。如果你的 LED 是共阳接法(LED 正极接 VCC,负极经电阻接 GPIO),那 GPIO 输出低电平才会亮;如果是共阴接法,GPIO 输出高电平才会亮。AI 在生成代码时往往默认共阴接法,输出高电平点亮。接反之后,代码逻辑“看起来正确”,实际效果完全相反。这个坑几乎只在硬件层面才能发现,因为编译和静态审查都看不出来。
第四个是延时函数选择不当。HAL_Delay 基于 SysTick 实现,在中断服务函数里调用可能会因为优先级问题导致整个系统卡死。如果 AI 在中断里用了 HAL_Delay,那是明显的设计问题,要立刻改掉,换成基于定时器的非阻塞延时。
第五个是烧录工具与芯片型号不匹配。用 STM32CubeProgrammer 下载时,如果选错了芯片型号,程序烧进去了,但运行行为完全不可预测。这跟 AI 生成代码无关,但它是 AI 帮你生成代码之后你最容易踩到的“周边坑”。下载前先确认 Device 型号跟核心板上的芯片丝印一致。
4.2 调试环境推荐的检查顺序
这里分享一套我自己实测下来效率最高的调试检查顺序,你可以直接套用:
- 确认供电:万用表量 VDD 对 GND 电压,3.3V 左右正常。
- 确认时钟:示波器量晶振两脚,有波形说明芯片时钟已经跑起来。
- 确认程序运行:在主循环里加一个翻转空余 GPIO 的语句,测波形。
- 确认 GPIO 输出:量目标 GPIO 引脚对 GND 的电压,高电平或低电平要符合预期。
- 确认外部电路:量 LED 两端电压差,有压差才可能亮。
- 确认代码逻辑:用调试器单步执行,观察寄存器值是否如预期变化。
这个顺序可以帮你把问题快速隔离到“硬件”还是“软件”。实际踩过几次坑之后你会发现,大多数“灯不亮”的问题,排查到第四步就能锁定方向了。
4.3 用调试器单步执行验证 AI 代码
最后聊聊调试器。很多新手用 AI 生成了代码之后,烧录不亮就直接去改代码,改来改去灯还是不亮,陷入死循环。正确做法是接上 ST-Link,用 STM32CubeIDE 的调试模式单步执行。
我一般会在这几个位置打上断点:
HAL_Init()之后,确认芯片基础初始化成功SystemClock_Config()之后,确认时钟树配置没有卡死- GPIO 初始化函数里,逐个查看 GPIO 输出寄存器的值
- 主循环第一次执行到 LED 操作的位置
单步执行时,如果代码停在SystemClock_Config()里出不来,问题几乎都在时钟配置上;如果 GPIO 初始化之后寄存器值正确,但 LED 不亮,那问题多半在硬件电路。这个过程配合调试器窗口的寄存器视图,可以非常直观地看到 AI 生成的代码每一步实际做了什么。
这套方法在 AI 编程场景里尤其重要,因为你并不完全清楚 AI 在每一行代码里的意图,必须靠调试器建立“代码行为”和“实际现象”之间的对应关系。
5. 这一期实验之后,我对 AI 单片机编程的真实体会
写到最后,想用几句真实体会收尾。
第一句:AI 写单片机程序,效率提升是真的,但不能盲目信任也是真的。编译零报错只是及格线,不应该是你验收 AI 代码的唯一标准。真正的验收标准只有一个——硬件行为是否达到预期。
第二句:AI 更适合做“代码生成助手”,而不是“嵌入式系统设计师”。它能帮你快速写出整齐的初始化代码、外设读写函数、逻辑框架,但你仍然需要具备硬件知识来决定引脚怎么映射、时钟怎么配、变量怎么组织。
第三句:跟 AI 协作嵌入式开发的正确姿势,是把它当成一个基础扎实但经验不足的实习生。它写出来的代码你审查之后才能用,而且审查的重点不是语法,而是工程习惯、硬件匹配、初始化顺序这些“软问题”。
最后再分享一个实用的小技巧:如果你在用 Codex 写单片机程序,可以在提问时主动要求它“添加详细的注释,说明每个初始化步骤的硬件意图”。这个技巧比单纯要求“写注释”有效得多,因为它会强迫 AI 从硬件角度思考。实测下来,加了这句要求之后生成的代码,引脚错误和时钟配置错误的概率明显降低。
下一次遇到 LED 全不亮的场景,先别急着怪 AI,拿万用表把接线量一遍,说不定问题就在那 0.1 欧姆的虚焊里。