1. 项目整体设计与思路拆解
1.1 为什么选ADC采集作为入门项目
嵌入式开发入门最大的拦路虎不是语法,而是“没有反馈感”。GPIO点灯固然有成就感,但点灯只能告诉你“通”或“不通”,这种数字量反馈玩几天就容易腻。ADC(模数转换)采集则完全不一样——它能让你用一个电位器、一颗光敏电阻或者一个温度传感器,把物理世界里的连续变化变成你屏幕上跳动的数字,这种反馈是连续的、可控的、可观察的。
50块钱做一套ADC采集成品,我认为是嵌入式入门里性价比最高的方向之一。核心板十几块,传感器几块钱,加上一堆杜邦线和一个小屏幕,全套下来不超过50元。这套东西能带你完整走一遍嵌入式开发的闭环:硬件连接、芯片配置、代码编写、调试排查、数据展示。更关键的是,它能让你真正理解“芯片是怎么看世界的”。
AI辅助在这个项目里扮演的角色非常实在:它帮你查手册、写驱动代码、解释寄存器、排查编译错误。但我要提前泼一盆冷水——AI不能替你理解电路,不能替你动手接线,更不能替你做最终判断。它的定位是“一个手里有海量资料、回复速度极快的助教”,而不是“替你做项目的代练”。
1.2 整套流程的闭环设计
我做这套教程时,给读者规划的是一条“从零到成品”的完整链路,每一步都对应一个看得见的结果。
第一步是硬件采购与连接,让你手里有一块能跑的板子。第二步是理解ADC原理,不需要啃大部头,只需要搞懂分辨率、参考电压、量程这三个核心概念。第三步是用AI辅助编写采集代码,把“电位器电压值”实时读出来并通过串口打印到电脑上。第四步是把数据可视化——用上位机看波形、用OLED屏显示实时数值,甚至可以自己在PC上画一个简单的滚动曲线。第五步是加滤波算法、校准偏移,让你的数据从“能看”变成“准”。第六步是扩展场景,把电位器换成光敏电阻、NTC热敏电阻,做一个实际可用的光照计或温度计。
这一套走下来,你对嵌入式开发的基本节奏就会有体感。以后不管是玩传感器、蓝牙、Wi-Fi,还是做物联网项目,底层逻辑都是相通的:外设初始化、数据读取、数据处理、数据交互。
2. 硬件选型与成本拆解
2.1 核心板选择:STM32F103还是ESP32
我做这套项目时,核心板首选是STM32F103C8T6最小系统板,也就是大家常说的“蓝板”。一块板子现在价格大约12到18元,具体看渠道和商家。为什么选它而不是更便宜的51单片机?因为STM32的ADC是12位分辨率的,能采集0到4095的数值,而51单片机的ADC通常只有10位甚至需要外扩。12位的精度对初学者来说既够用又能直观理解“分辨率”这个核心概念。
还有一个理由:STM32的寄存器、HAL库、标准库资料量极大,AI训练语料里关于STM32的内容非常充足。这意味着你拿着AI辅助开发时,它给出的代码正确率会明显高于一些小众芯片。AI辅助开发的效率,很大程度上取决于这个芯片的“语料覆盖率”。
如果你手里有ESP32开发板,也可以跟着做,但要注意两点:ESP32的ADC在默认条件下线性度没有STM32好,尤其是末尾段会有轻微的非线性,做高精度采集时需要开启衰减设置;另外ESP32的ADC引脚有特定限制,不是所有引脚都能复用为ADC,用之前一定要查对应型号的引脚功能表。我建议新手优先用STM32,理由不是ESP32不好,而是STM32的ADC模型更“教科书”,学明白后举一反三更容易。
2.2 传感器与外围器件清单
配套的传感器和外围器件,我列一个实测可用的采购清单:
| 器件 | 型号/规格 | 参考单价 | 用途说明 |
|---|---|---|---|
| 核心板 | STM32F103C8T6最小系统板 | 12~18元 | 主控 |
| USB转TTL | CH340模块或板载USART | 3~8元 | 串口通信下载、调试 |
| 电位器 | 10K单联旋转电位器 | 1~2元 | 可调电压输入 |
| 光敏电阻 | 5516/5528 | 0.5~1元/颗 | 光强采集 |
| NTC热敏电阻 | 10K NTC + 10K普通电阻 | 1~2元 | 温度采集 |
| OLED屏 | 0.96寸 I2C SSD1306 | 8~12元 | 数据显示 |
| 杜邦线 | 公对公、公对母若干 | 2~4元/排 | 连接线 |
| 面包板 | 400孔小型 | 2~3元 | 免焊搭建电路 |
| 电阻 | 10K、1K等常用 | 约1元/包 | 分压、限流 |
如果你手头已经有面包板和杜邦线,总成本可以压到25元左右。OLED屏不是必须的,前期用串口输出一样能完成项目,但加上屏幕会有“成品”的质感,所以教程里我建议分两个版本:基础版纯串口,升级版加OLED。
2.3 硬件连接的前期检查
拿到开发板和器件后,不要急着接线,先花五分钟做三件小事,能省下后面几小时的排查时间。
第一,检查主控板USB口供电是否正常,插上电脑后板载指示灯应该亮起。如果灯不亮,先换USB线——非常多人踩过这个坑,接线板赠送的USB线可能只带电源不带数据,或者本身就是坏的。
第二,测量核心板3.3V引脚对地电压,应该稳定在3.3V左右。如果电压明显偏低,可能是电脑USB口供电不足,换一个接口或者外接供电再测试。
第三,把每根杜邦线两端对应喷一喷金属触点(开玩笑的,不是喷剂),实际是用肉眼检查排针有没有弯曲、杜邦线有没有破损。排针弯曲是焊接工艺问题,是比较常见的,轻轻掰正即可。这些基础检查做完,后面电路搭起来才会稳。
3. ADC采集原理与关键参数
3.1 用“尺子的刻度”理解ADC
把ADC转换想象成拿一把有刻度线的尺子去量一个物体的长度。物体实际的长度就是你要测的模拟电压,尺子的最大量程就是参考电压,尺子上的刻度线数量就是2的N次方,N是ADC的分辨率位数。
STM32F103的ADC是12位分辨率,相当于这把尺子被等分成了4096个刻度。如果参考电压设为3.3V,那么每一个最小刻度代表的电压就是3.3V / 4096 ≈ 0.000805V,也就是大约0.8mV。这个最小刻度电压,就是该配置下的最小分辨率。这个概念一定要刻在脑子里,因为后面做温度采集、电压检测时,你需要根据量程来评估误差范围,决定需不需要做放大电路或者换更高精度的ADC。
3.2 分辨率、参考电压与量程的关系
ADC的原始读数是一个整数,范围是0到4095。要将原始读数转换成实际电压,公式是:
实际电压 = 原始读数 × 参考电压 / 4096如果参考电压是3.3V,读数是2048,那实际电压就是2048 × 3.3 / 4096 ≈ 1.65V,刚好是参考电压的一半。
这里有个非常容易忽略的坑:STM32F103C8T6的VREF引脚在最小系统板中通常是内部连接到3.3V供电轨上的,也就是说参考电压直接取决于USB供电的稳定性。如果你用USB供电,电脑USB口输出一般比较稳定,但如果后面玩电池供电,参考电压会随着电池电压下降而下降,导致ADC读数漂移。这种情况下要么使用外部基准源芯片,要么软件里按照当前供电电压做归一化校准。
另一个概念是采样时间——ADC不是瞬间完成转换的,它需要一定时间去采样保持和逐次逼近。STM32的ADC采样时间可以配置为1.5周期到239.5周期不等。如果你采集的是一个缓慢变化的电压,比如电位器分压值,那么短采样时间完全够用,转换速度快。但如果采集的是快速变化的信号,比如音频波形,就需要仔细权衡采样时间和精度。
3.3 输入阻抗与驱动能力
ADC的输入引脚并不是“无穷大电阻”,它在内部有一个采样电容。每次开始转换前,ADC内部会把这个采样电容接到输入引脚上充电,充电时间取决于输入源的阻抗。
拿10K电位器举例:电位器等效输出阻抗相对较高,如果直接用STM32的ADC引脚测量,内部采样电容充电速度会变慢,导致测量结果出现偏差,尤其是你快速改变电位器阻值时,读数会“跟不上”。实操中的表现就是:你扭动电位器,串口打印出来的数值变化滞后,或者读数明显偏低。解决办法是加大采样时间配置,或者用电容滤波降低源阻抗影响。我在代码里会优先把采样时间调大,方便新手理解。
> 注意:不要用ADC引脚直接量超过参考电压的输入,否则大概率烧毁ADC模块。如果需要测量0到10V的外部电压,必须先做分压电路或者运放衰减。
4. AI辅助开发实战:从提示词到代码
4.1 AI辅助开发的正确姿势
我用AI辅助嵌入式开发的整体工作流是:先自己把需求想清楚,再把需求拆成一个个小问题,让AI针对每个小问题给出可验证的答案,然后人工做逻辑校验,最后上机实测。核心原则是“小步快跑,逐段验证”。
不建议让AI一口气生成一个完整大工程再编译。你很可能得到一个满屏报错的项目,而且代码量越大,AI犯的隐性错误越难排查。正确做法是分步来:先让AI写“初始化ADC引脚并读取一次原始值”的代码,编译烧录跑通之后,再让它加“串口打印”逻辑,再之后加“滤波算法”,一步步升级。
4.2 我实际用过的提示词模板
这里分享几组翻译成实践语言的AI提示词,可以直接复制改参数使用,比问“帮我写个ADC程序”有效得多。
第一种,查配置型:
我现在用STM32F103C8T6,在STM32CubeIDE环境下,HAL库,使用ADC1的通道1,引脚是PA1,参考电压3.3V。请给出初始化ADC的HAL代码,并把采样时间配置为最大值,转换方式配置为单次转换,同时给出读取ADC原始值的函数。
这种提示词把芯片型号、开发环境、库类型、引脚、通道、配置偏好全部交代清楚。AI拿到这些信息后,给出的代码基本可以直接编译。
第二种,排查问题型:
我的STM32 ADC读取值始终是4095,引脚悬空,怀疑是引脚配置错误。请列出ADC引脚配置中导致读数满偏的所有可能原因,并按概率从高到低排序。
这种问题式提示词会让AI输出一个排查清单,而不是直接给代码。嵌入式调试很多时候不是“写代码难”,而是“定位问题难”,AI能把常见的失败模式整理出来,让你按清单挨个排查。
第三种,改进算法型:
我采集到的电位器电压值在1.5V附近跳动,波动幅度大概20mV。请用C语言写一个简单的滑动平均滤波函数,窗口大小8,注意数据类型的溢出问题。
AI会给出滤波函数。但你需要自己思考一个问题:窗口大小为什么是8而不是100?因为滤波会带来延迟。采集电位器这种慢变量时8到16的窗口很合适,但如果后面采集快速变化的信号,窗口太大就会“糊”。这个权衡考量,AI不会替你做决定,你得自己理解。
4.3 AI代码的“人工稽查”清单
AI生成代码替换不了人工审查,以下几类问题是高频雷区:时钟使能遗漏、GPIO复用功能配置错误、DMA中断回调函数名拼写错误、HAL库版本差异导致API不一致、变量类型溢出。每次拿到AI生成的代码,我会按这张表自查一遍:
- 检查所有外设的时钟是否都使能了(ADC时钟、GPIO时钟、DMA时钟)。
- 检查引脚模式有没有设置为模拟输入(注意不是普通的输入模式,模拟输入和数字输入在STM32的GPIO配置里是两个不同的选项)。
- 检查ADC通道号是否和芯片引脚对应关系一致。
- 检查读取ADC结果的数据类型,12位ADC结果最大值是4095,用uint16_t完全够,不要用int8_t。
- 检查采样时间参数是否是合法枚举值。
5. 实操过程与核心代码解析
5.1 硬件接线:三个必须检查的点
电路搭建本身很简单,我只强调三个要点。
电位器接法:电位器有三个引脚,两端分别接3.3V和GND,中间抽头接STM32的PA1。注意两端不要接反了,接反后在代码里读到的数值变化是反的,容易产生“硬件坏了”的错觉。
光敏电阻分压电路:光敏电阻和一个10K电阻串联,一端接3.3V,一端接GND,中间节点接PA1的ADC通道0。光强变化时,中间节点电压会变化。给新手的建议是先不换器件,等电位器版本跑通了再换,这样变量控制最小,遇到问题容易定位。
共地问题:如果你用USB转TTL模块给板子供电,同时还用另一个USB口供电,两个USB口的GND必须连通。最稳妥的做法是只用一条USB线给核心板供电,通过板载USB转串口芯片完成通信,保证电源和通信共地。
5.2 CubeMX初始化配置详解
新建STM32CubeIDE工程,选择芯片STM32F103C8T6,在Pinout视图里按下图思路配置:
- SYS选Debug Serial Wire,方便后面用调试器。
- RCC选Crystal Oscillator。
- ADC1勾选IN1,对应PA1,配置为单次转换模式,扫描模式不勾选。
- 采样时间选择239.5 Cycles,这个数值是可选的最大值,先保证采样稳定。
- USART1开启异步模式,波特率115200,PA9 / PA10是默认引脚。
- 时钟树默认配置即可,ADC时钟可以设为PCLK2的分频值,保持不超过14MHz,这是STM32F103器件的ADC时钟上限。
CubeMX生成代码后,在main函数里按三步写业务逻辑:初始化外设后,开一个while循环,每次循环做“采集→转换→打印→延时”。
一个新手易错的点:CubeMX默认不开启ADC的“连续转换模式”,也就是说每次转换完成后,如果想再采集一次,需要重新启动一次转换。所以代码里每次读取前调用HAL_ADC_Start(),每次读取后用HAL_ADC_Stop()停掉,或者配置为连续转换。我演示用单次转换模式,这样逻辑更清晰。
5.3 核心代码解读
在main.c的while循环中,核心代码注释如下:
uint16_t adc_raw = 0; float adc_voltage = 0.0f; while (1) { // 启动一次ADC转换,等转换结束后从数据寄存器读出原始值 HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 10); adc_raw = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); // 原始值 [0,4095] 映射到 [0.0, 3.3V] adc_voltage = adc_raw / 4096.0f * 3.3f; // 带精确帧格式的串口输出,方便上位机解析 printf("RAW:%u VOLT:%.3f\r\n", adc_raw, adc_voltage); HAL_Delay(50); }printf和HAL库的底层串口输出需要做重定向。在CubeMX工程里,通常需要加上如下代码,把printf的输出重定向到UART1的句柄上:
int _write(int fd, char *ptr, int len) { HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, 0xFFFF); return len; }如果不做这个重定向,printf不会往串口发任何数据。这个细节很多新手会漏掉,编译没报错,但串口就是没输出。
5.4 观察输出的三种方式
调试过程中,看ADC采集结果有以下几种方式,按由简到繁排列:
第一种,串口终端直接看。串口助手设置波特率115200、8位数据、无校验、1位停止位,就能看到“RAW:2048 VOLT:1.650”这样的输出。
第二种,用串口示波器软件看波形。常见的工具如SerialPlot,把RAW值作为纵轴,时间作为横轴,旋转电位器时你能实时看到曲线变化。这个体验非常直观,会让你产生“我好像真的在做数据分析”的感觉。
第三种,OLED屏显示。用I2C方式驱动SSD1306屏幕,把电压值、原始值和百分比显示在屏上。这个需要额外移植驱动库,AI辅助可以快速生成初始化代码,但接线I2C的SDA和SCL不要接反了,SCL接PB6,SDA接PB7,这是STM32F103的I2C1默认引脚。
5.5 滤波与校准:从“能跑”到“能用”
一个原始ADC读数直接打印出来,你细看时会发现数值在几十甚至上百的范围内波动。因为影响因素很多:电源纹波、电磁干扰、参考电压波动。
我推荐入门阶段使用滑动平均滤波。滤波器思路是保留下一次的新数据,与之前的窗口数据先求平均再输出。下面这段是窗口为16的滑动平均代码:
#define FILTER_N 16 static uint16_t history[FILTER_N] = {0}; static uint8_t index = 0; static uint32_t sum = 0; uint16_t adc_filtered(uint16_t new_raw) { sum -= history[index]; sum += new_raw; history[index] = new_raw; index = (index + 1) % FILTER_N; return (uint16_t)(sum / FILTER_N); }使用注意:sum变量用uint32_t,因为16个4095累加最大是65520,uint16_t会溢出,这是非常典型的溢出漏洞,AI有时候也不会主动提醒。
校准更简单。当你把电位器抽头输出接到3.3V时,明显应该读4095,如果读出来是4050,说明存在系统偏移或者参考电压略低。可以把实际读到的值作为满量程值,然后做一次线性修正。公式:
校准后的值 = 原始值 / 实际满量程值 × 4096这种做法不是真正的“高精度计量”,目的是让新手明白校准这个概念。如果你后面做温度计,把0摄氏度和100摄氏度两个点标定掉,数据就会准确得多。
6. 常见问题与排查技巧实录
6.1 读数始终为0或始终为4095
遇到这种“极端读数”先别怀疑ADC坏了,按以下顺序去查。
读数始终为0:检查输入引脚是否真的接到了电位器中间抽头;检查电位器另一端是否接到了GND;用万用表直流电压档直接量PA1脚对地电压,如果引脚确实有电压但读数为0,说明引脚配置有问题,大概率是GPIO没有设置为模拟输入模式。
读数始终为4095:先判断是不是引脚悬空。悬空状态读满偏是常见现象,因为引脚是高阻态的弱上拉或浮空,采样电容充电结果不可控。接上电位器后,如果依然满偏,用万用表量PA1电压是否接近3.3V,如果是,检查是不是杜邦线接到了3.3V引脚而不是电位器抽头。如果电压正常但读数依然是4095,你就要检查ADC通道配置是否和实际引脚对应,比如你接入的是PA1但代码里配的是通道0。
6.2 数值持续跳动
刚开始采集时,你会发现数值不是稳定的,这是新手最容易焦虑的点。多数情况下这不是故障,而是正常噪声。
排查顺序是:先用滑动平均滤波消除软件层面的随机跳动。如果滤波后依然跳,检查电源端有没有加100nF去耦电容,很多最小系统板已经有电容了,但你外接传感器时拉长了导线,引入了噪声。把电位器那组的导线段缩短,或者直接在ADC输入引脚到地之间并联一个0.1uF陶瓷电容,通常能显著降低读数跳动。
另外一个会被忽略的原因:USB转串口的发送过程会对MCU产生电磁干扰,尤其是使用了质量较差的杜邦线时。可以把串口模块挪远一点试试,如果读数明显变稳,说明是串口模块电场耦合进来的噪声。
6.3 串口输出乱码或没有输出
串口乱码的第一反应是波特率不匹配。CubeMX里USART1配置为115200,串口助手也设置115200,如果还乱码,检查时钟树配置是否让USART的时钟频率等于预期值,尤其注意外部晶振是否实际焊接、RCC配置是否选了外部晶振。很多最小系统板上的晶振是8MHz,但如果你配置的是HSI内部时钟,波特率会产生较大误差。
没有输出的情况分两种:一是printf没有重定向到HAL库的UART句柄,二是接线问题。检查USB转TTL模块的RXD是否接到芯片的PA9(TX),TXD是否接到PA10(RX),两边要交叉连接,这是新手最容易犯的接线错误之一。
6.4 AI生成代码编译不过的常见原因
AI给出的代码编译失败,最常见的几类原因:漏掉头文件、结构体成员名和当前HAL库版本不一致、使用了旧标准库的函数但当前工程是HAL库。处理技巧很简单,把报错信息完整复制丢给AI,让它根据报错信息做修正。但要注意:不要让AI反复猜测,把“我当前使用的是STM32CubeIDE + HAL库版本1.8.0”这些编译环境信息一并给它,会大幅提高修正速度。
> 提示:排查问题最重要的习惯是“一次只改一个变量”。不要同时换了引脚配置又换了滤波算法又换了采样时间,否则出了问题根本说不清是哪个改动引入的错误。
7. 写在最后:用经验换效率
做这套教程的过程中,我最深的体会是:AI辅助开发的最大价值不是替代思考,而是把“查资料、抄代码、试编译”的琐碎时间压缩掉,让人能把精力集中在真正需要理解的核心逻辑上。
如果你是完全零基础,我的建议是前四次踩坑不要着急“优化”,就按照默认配置走一遍流程。只有当你亲眼看到电位器从0调到3.3V、串口数据显示出平滑的曲线,那一刻你才会真正建立对MCU内部转换机制的感受。这种“手感”是看多少教程都换不来的。
最后再分享一个扩展方向:把电位器换成NTC热敏电阻分压电路,再把ADC读数通过公式转换成温度值,打印出来,你就拥有了一只“数字温度计”。再把光敏电阻也接上,做一个光照上下限报警器,整机成本仍然控制在50元上下。也就是说,这套ADC采集的“地基”打好了,后面盖什么“房子”都顺手。希望这篇教程能帮你迈出嵌入式实战的第一步。