1. 从一块“哑巴”开发板说起:这个项目到底在解决什么问题
很多人手里都有一块STM32开发板,点过灯、串口打印过“Hello World”,但真到要做一个能拿得出手、能说清楚“它有什么用”的小系统时,反而不知道从哪下手。温度、电压、电流这三个量,几乎是所有电子设备最基础的“生命体征”——电源模块要看电压电流,电机驱动要看电流,电池管理要看温度,充电器要看温升。把这几个量测准、显示出来、超限了能报警,这就是一个完整的闭环监测系统。
这个项目的核心,就是拿STM32当大脑,用传感器采集温度、电压、电流三路模拟量,经过ADC转换和必要的换算,把结果显示在OLED屏幕上,同时设定阈值,一旦越界就通过蜂鸣器和LED报警。它适合谁?适合刚学完STM32 GPIO和ADC、想找一个综合性项目练手的人;适合要做课程设计、毕业设计但不想做烂大街“温湿度显示”的人;也适合工作中需要快速搭一个设备状态监测原型的工程师。
我见过太多人做这类项目,卡住的地方往往不是代码写不出来,而是几个具体问题:电流怎么测才准?OLED为什么点不亮?ADC读数为什么一直跳?报警逻辑怎么写才不误报?这些细节,教科书上不会讲,但实际做的时候一个都绕不开。下面我就按实际动手的顺序,把整个系统的设计思路、关键选型、代码逻辑和踩坑经验完整拆一遍。
2. 三路信号的采集方案:为什么这样选传感器和电路
2.1 温度采集:DS18B20和NTC热敏电阻的取舍
温度测量最常见的两个方案,一个是DS18B20数字温度传感器,一个是NTC热敏电阻加分压电路。很多人一上来就选DS18B20,理由是“数字输出、精度高、单总线省引脚”。这话没错,DS18B20在-10℃到+85℃范围内精度能到±0.5℃,而且一条总线可以挂多个,确实省事。
但我要说的是,如果你做的是板级温度监测,比如监测电源芯片或者MOS管附近的温升,NTC热敏电阻反而更合适。原因很简单:NTC是贴片封装,可以紧贴发热源,热传导路径短,响应快;DS18B20通常是TO-92直插或者防水探头,体积大,热惯性也大,测的是“环境温度”而不是“器件温度”。我实测过,同样放在一个发热电阻旁边,NTC从25℃升到60℃大概需要8秒,DS18B20要20秒以上。
NTC的缺点是输出是非线性的,需要查表或者用Steinhart-Hart公式换算。对于STM32来说,这不算负担,一个查表加线性插值就能解决。具体做法是:NTC和10kΩ精密电阻串联分压,NTC接VCC,电阻接地,中点接STM32的ADC引脚。25℃时NTC阻值10kΩ,分压得到VCC的一半,ADC读数大约2048(12位)。温度升高,NTC阻值下降,中点电压下降,ADC读数减小。
换算的时候,我建议直接用查表法。提前用Excel或者Python算一张表,从-20℃到100℃,每5℃一个点,存成数组。ADC读到一个值,先查表找到区间,再做线性插值。这样比实时算对数公式快得多,精度也够用。如果你非要用公式,Steinhart-Hart是:
1/T = A + B*ln(R) + C*(ln(R))^3其中T是开尔文温度,R是NTC阻值,A、B、C是厂家给的系数。这个公式算一次要两次对数运算和一次立方,在STM32F103上大概要几十微秒,不算慢,但查表法更直观,调试的时候也更容易看出问题。
2.2 电压采集:分压电阻的精度决定一切
电压采集看起来最简单,一个分压电路就完事。但这里有个坑:很多人用两个10kΩ的1%电阻分压,觉得“1%精度够了”。实际上,分压比是R2/(R1+R2),两个电阻的误差会叠加。如果R1偏+1%,R2偏-1%,分压比误差可能到2%。对于12位ADC来说,2%就是80个LSB,换算成电压就是几十毫伏的误差。
我的做法是:分压电阻用0.1%精度的金属膜电阻,而且阻值不要选太大。有人为了降低功耗,用100kΩ+100kΩ,结果ADC采样时输入阻抗不够低,采样保持电容充不满,读数偏低。STM32的ADC输入阻抗要求是小于50kΩ,所以分压电阻的并联值最好控制在10kΩ以内。比如测12V电压,可以用10kΩ+3.3kΩ,分压比是3.3/(10+3.3)=0.248,12V分压后是2.98V,在3.3V量程内。并联值大约是2.5kΩ,完全满足要求。
还有一个细节:ADC引脚前面要加一个0.1μF的电容到地,滤掉高频噪声。这个电容要尽量靠近引脚,否则走线电感会让滤波效果大打折扣。我试过不加电容,读数跳动大概±5个LSB;加了之后,跳动降到±1个LSB以内。
2.3 电流采集:分流电阻、霍尔和专用芯片的三条路
电流测量是这三路里最麻烦的。常见方案有三种:分流电阻加运放、霍尔电流传感器、专用电流监测芯片。每种都有适用场景,选错了要么测不准,要么成本翻倍。
分流电阻方案最简单:在电流回路里串一个小电阻,测电阻两端电压,用欧姆定律算电流。比如串一个0.1Ω电阻,流过1A电流时压降0.1V,ADC能直接测。但问题在于,0.1Ω电阻在1A时功耗是0.1W,发热会导致阻值漂移;而且如果被测电路和STM32不共地,还需要差分放大。所以这个方案适合低压、小电流、共地的场景,比如测STM32自己系统的总电流。
霍尔电流传感器是非接触式的,比如ACS712,串联在回路里,输出一个和电流成比例的电压。优点是隔离、无插入损耗,缺点是零点漂移大,温漂也大。ACS712的零点在VCC/2,但实际可能在VCC/2±0.1V范围内波动,对应几十个LSB的误差。如果你测的电流范围是0到5A,灵敏度是185mV/A,零点漂移0.1V就相当于0.54A的误差,这在小电流段是致命的。
专用电流监测芯片,比如INA219或者INA226,内部集成了ADC和乘法器,直接通过I2C输出电流、电压、功率。INA219的精度是0.5%,而且内置校准寄存器,可以软件校准。缺点是价格比ACS712贵,而且需要I2C通信,占用两个引脚。但如果你要测的是直流电流,而且对精度有要求,INA219是最省心的选择。
我个人的建议是:如果只是做课程设计或者演示,用ACS712就够了,接线简单,代码也简单;如果要实际做产品原型,或者需要测小电流,用INA219。分流电阻方案适合对成本极度敏感、而且电流范围固定的场景。
3. OLED显示驱动:从点不亮到稳定刷新的完整过程
3.1 I2C和SPI接口的选择:引脚数和刷新率的权衡
OLED屏常见的有I2C和SPI两种接口。I2C只需要两根线(SCL、SDA),SPI需要四根线(SCK、MOSI、CS、DC),如果再加复位就是五根。对于STM32来说,引脚资源通常不是问题,所以很多人会纠结选哪个。
我的经验是:如果你要显示的内容是静态的,或者刷新率要求不高(比如每秒更新几次),I2C完全够用。0.96寸128x64的OLED,I2C速率400kHz,刷一屏大概需要20ms,也就是每秒50帧,对于显示温度电压电流这种慢变量,绰绰有余。而且I2C接线少,PCB布局简单,不容易出错。
但如果你要做波形显示,或者需要快速刷新(比如每秒30帧以上),SPI的优势就出来了。SPI可以跑到10MHz甚至更高,刷一屏只要1ms左右。而且SPI是全双工,虽然OLED只用到了单向,但时序更简单,不容易出现I2C的时钟拉伸问题。
我踩过的一个坑是:用I2C驱动OLED时,如果STM32的I2C时钟配置不对,比如用了标准模式100kHz但上拉电阻太大(10kΩ),波形上升沿会变缓,导致通信失败。正确的做法是上拉电阻用4.7kΩ,时钟配400kHz快速模式。如果还是不稳定,可以在SCL和SDA上各加一个100pF电容到地,滤掉毛刺。
3.2 显存管理和刷新策略:为什么你的OLED会闪
OLED驱动最核心的概念是“显存”(GDDRAM)。屏幕上的每个像素对应显存里的一个bit,128x64的屏幕需要1024字节的显存。你写显存的时候,屏幕不会立刻变化,要等驱动芯片把显存内容扫描到屏幕上。这个扫描是持续进行的,所以如果你在扫描到某一行的时候修改了那一行的显存,就会出现撕裂或者闪烁。
正确的做法是:在内存里开一个1024字节的缓冲区,所有绘图操作都在缓冲区里完成,然后一次性把缓冲区写到OLED的显存里。这个过程叫“刷屏”。刷屏的时候,OLED的扫描还在继续,但因为你写的是整个缓冲区,而且写入速度远快于扫描速度,所以人眼看不到撕裂。
但这里有个细节:I2C的写入速度有限,1024字节在400kHz下大概需要20ms,这20ms内屏幕还在扫描,所以还是有可能看到轻微闪烁。解决办法是:只刷新变化的部分。比如温度值从25变成26,只有几个字符变了,你只需要重写那几个字符对应的显存区域,而不是全屏刷新。这样每次刷新的数据量可能只有几十字节,时间缩短到1ms以内,完全看不到闪烁。
我通常的做法是:定义一个“脏矩形”区域,每次更新数据时,记录哪些区域需要重绘,然后只刷这些区域。对于这个项目,温度、电压、电流三个数值分别显示在屏幕的不同位置,每个数值占一行,每行16个字符。更新的时候,只重写对应行的显存,其他行不动。
3.3 字库和显示格式:让读数一目了然
OLED显示数字,最怕的是“跳来跳去”。比如电压从9.99V变成10.00V,如果显示格式没对齐,小数点位置会变,看起来很不舒服。我的做法是:固定显示宽度,比如电压显示“XX.XXV”,温度显示“XX.XC”,电流显示“X.XXXA”。不足的位补空格,这样小数点永远对齐。
字库方面,如果只是显示数字和几个字母,不需要完整的ASCII字库。可以自己取模,只做0-9、小数点、V、A、C这几个字符。取模软件用PCtoLCD2002,设置成“阴码、逐列式、顺向”,生成的字模数组直接放到代码里。这样Flash占用小,刷新也快。
如果你要显示中文,比如“温度”“电压”“电流”,那就需要中文字库。一个16x16的汉字需要32字节,十几个汉字就是几百字节,对于STM32F103C8T6的64KB Flash来说不算什么。但取模的时候要注意,中文字库通常是“逐行式”的,和英文的“逐列式”不一样,写驱动的时候要分开处理。
4. 报警逻辑的设计:阈值、回差和延时一个都不能少
4.1 阈值设定:为什么不能只设一个点
最简单的报警逻辑是:温度超过50℃就报警,低于50℃就取消。但实际用的时候你会发现,如果温度在50℃附近波动,报警器会“滴滴滴”响个不停,继电器也会频繁切换,这叫“抖动”。
解决办法是加“回差”(也叫迟滞)。比如温度超过50℃报警,但要降到48℃以下才取消报警。这样即使温度在49℃到51℃之间波动,报警状态也不会频繁切换。回差的大小要根据实际系统的热惯性来定。对于小功率电阻加热的系统,回差1到2℃就够了;对于大功率电机,回差可能要5℃以上。
电压和电流的回差也是同样的道理。比如电压超过12.5V报警,降到12.3V以下才取消。电流超过2A报警,降到1.8A以下才取消。回差太小,起不到防抖作用;回差太大,又会导致该报警的时候不报。我的经验是:回差取阈值的5%到10%比较合适。
4.2 软件滤波:ADC读数跳动的处理
ADC读数跳动是必然的,电源纹波、地线噪声、传感器本身的噪声都会导致读数波动。如果不处理,报警阈值附近就会频繁触发。除了硬件上加RC滤波,软件上也要做处理。
最简单的软件滤波是“多次采样取平均”。比如连续采16次,去掉最大值和最小值,剩下的14次取平均。这样能滤掉大部分随机噪声,但会降低响应速度。16次采样在12位ADC、1μs转换时间下,加上软件开销,大概需要100μs,对于温度电压电流这种慢变量,完全够用。
进阶一点的是“滑动平均滤波”。维护一个长度为N的队列,每次新采样进来,踢掉最老的一个,重新算平均。这样每个采样周期都能输出一个滤波后的值,响应速度比“采16次取平均”快。N取8或者16都可以,取决于你对响应速度和稳定性的要求。
还有一种“中值滤波”,连续采3次或5次,取中间值。这种方法对脉冲噪声特别有效,比如电机启动时的尖峰干扰。我通常会把中值滤波和滑动平均结合起来:先中值滤波去掉尖峰,再滑动平均平滑波动。
4.3 报警输出:蜂鸣器和LED的驱动电路
蜂鸣器分有源和无源两种。有源蜂鸣器内部有振荡电路,给电就响,驱动简单,但频率固定;无源蜂鸣器需要外部给方波,频率可调,能发出不同音调。对于报警器来说,有源蜂鸣器就够了,代码里只需要控制GPIO高低电平。
但要注意,蜂鸣器的工作电流通常比较大,5V有源蜂鸣器大概20到30mA,STM32的GPIO最大输出电流是20mA(单个引脚),所以不能直接驱动,要用三极管或者MOS管。我一般用S8050三极管,基极串一个1kΩ电阻到GPIO,集电极接蜂鸣器负极,发射极接地,蜂鸣器正极接5V。这样GPIO输出高电平时,三极管导通,蜂鸣器响。
LED的驱动就简单多了,串一个1kΩ限流电阻直接接GPIO就行。但要注意,如果LED和蜂鸣器共用同一个GPIO,蜂鸣器的电流可能会把GPIO电压拉低,导致LED变暗。所以最好分开两个引脚,或者用ULN2003这种达林顿管阵列来驱动。
5. 仿真与实物调试:那些只有动手才知道的坑
5.1 Proteus仿真:能跑通不代表实物能跑通
很多人做这个项目,第一步是在Proteus里画原理图、跑仿真。Proteus确实方便,不用焊板子,改电路也快。但我要提醒一句:Proteus里的ADC模型是理想的,没有噪声,没有温漂,没有输入阻抗问题。你在仿真里看到的读数,和实物可能差很远。
比如你在Proteus里用两个10kΩ电阻分压测12V,ADC读数很稳,换算出来正好是12.00V。但实物上,电阻的1%误差、ADC的偏移误差、电源的纹波,都会让读数偏离。所以仿真只能验证逻辑,不能验证精度。
还有一个坑:Proteus里的OLED模型刷新速度是固定的,你代码里刷屏再快,仿真里看起来也是那个速度。但实物上,I2C的速率、OLED的响应时间,都会影响刷新效果。所以仿真跑通了,实物上还是要重新调。
5.2 实物调试:从电源开始查起
实物调试的第一步,永远是查电源。我见过太多人,代码写了半天,OLED不亮,最后发现是3.3V稳压芯片没焊好,输出电压只有1.2V。所以拿到板子,先别急着下载程序,用万用表量一下VCC和GND之间的电压,确认是3.3V。
第二步,查晶振。STM32F103通常用8MHz外部晶振,经过PLL倍频到72MHz。如果晶振没起振,系统会默认用内部8MHz RC振荡器,主频只有8MHz,虽然程序能跑,但ADC采样率和I2C速率都会变慢。用示波器量一下晶振引脚,应该有8MHz的正弦波,幅度大概1Vpp。
第三步,查I2C通信。如果OLED不亮,先用逻辑分析仪或者示波器看SCL和SDA有没有波形。如果没有波形,说明I2C初始化有问题;如果有波形但OLED不亮,可能是从机地址不对。0.96寸OLED的I2C地址通常是0x78(写)或0x79(读),但有些模块是0x7A。可以写一个I2C扫描程序,遍历所有地址,看哪个地址有应答。
5.3 ADC校准:一个容易被忽略的步骤
STM32的ADC有一个内部校准功能,但很多人不用。校准的目的是消除ADC内部的偏移误差。具体做法是:在ADC初始化之后,先使能ADC,然后设置ADC_CR2的CAL位,等待校准完成。校准完成后,ADC会得到一个校准系数,后续的转换结果会自动减去这个偏移。
我实测过,不做校准的话,ADC读数在0V输入时可能是3到5个LSB,也就是2.4到4mV的偏移。对于3.3V量程来说,这相当于0.1%的误差。虽然不大,但如果你测的是小电压,比如0.1V,这个偏移就是4%的误差,不能忽略。
校准的代码很简单:
ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1));这几行代码放在ADC初始化之后、开始转换之前。注意,校准期间不能有外部信号输入,否则校准不准。
6. 从能跑到好用:几个提升系统稳定性的细节
6.1 看门狗:防止程序跑飞
STM32内置独立看门狗(IWDG)和窗口看门狗(WWDG)。对于这种监测系统,独立看门狗就够了。IWDG的时钟源是内部40kHz RC振荡器,不受主时钟影响,即使主时钟挂了,看门狗也能复位。
配置IWDG的步骤:先使能IWDG,设置预分频和重装载值,然后定期“喂狗”。比如设置溢出时间为1秒,主循环里每500ms喂一次狗。如果程序跑飞,超过1秒没喂狗,IWDG就会复位系统。
但要注意,喂狗不能在中断里做,否则如果主循环挂了但中断还在跑,看门狗就失效了。正确的做法是在主循环里喂狗,而且喂狗之前要检查各个任务是否正常执行。比如如果ADC采样任务卡住了,就不喂狗,让系统复位。
6.2 电源监控:BOR和PVD
STM32有一个内部电源监控功能,叫PVD(可编程电压检测器)。可以设置一个阈值,比如2.9V,当VCC降到2.9V以下时,PVD会产生中断。在中断里可以保存关键数据,或者直接报警。
这个功能对于电池供电的系统特别有用。如果电池电压降到2.9V,STM32可能还在跑,但ADC的参考电压已经不稳了,读数会偏大。这时候PVD中断可以提醒用户“电量低”,或者直接进入低功耗模式。
配置PVD的代码:
PWR_PVDLevelConfig(PWR_PVDLevel_2V9); PWR_PVDCmd(ENABLE);然后在中断服务函数里处理。注意,PVD中断是边沿触发的,电压恢复后不会自动清除中断标志,需要手动清除。
6.3 数据存储:掉电保存阈值
如果系统需要掉电保存报警阈值,可以用STM32内部的Flash模拟EEPROM。STM32F103的Flash擦写寿命是1万次,如果每天改一次阈值,可以用27年。但要注意,Flash擦除是按页擦除的,一页是1KB或2KB,所以不能只擦一个字节,要整页擦除再写回。
我的做法是:在Flash的最后一页开辟一个区域,存阈值和校准系数。每次修改阈值时,先把整页读到RAM,修改对应字节,擦除整页,再写回。这个过程大概需要几十毫秒,期间不能断电,否则数据会丢失。如果对可靠性要求高,可以用双页备份,一页写坏了还有另一页。
7. 代码结构:模块化让调试事半功倍
7.1 分层设计:驱动层、业务层、应用层
这个项目的代码,我建议分成三层:驱动层、业务层、应用层。驱动层负责和硬件打交道,比如ADC读取、OLED写命令、蜂鸣器控制;业务层负责数据处理,比如温度换算、滤波、报警判断;应用层负责调度,比如主循环里依次调用各个任务。
这样分的好处是:调试的时候可以单独测试每一层。比如OLED不亮,只需要检查驱动层;温度读数不对,只需要检查业务层。而且换硬件的时候,只需要改驱动层,业务层和应用层不用动。
具体文件结构:
Project/ ├── Drivers/ │ ├── adc.c/h │ ├── oled.c/h │ ├── buzzer.c/h │ └── led.c/h ├── Business/ │ ├── temperature.c/h │ ├── voltage.c/h │ ├── current.c/h │ └── alarm.c/h ├── App/ │ └── main.c └── Utils/ └── filter.c/h7.2 定时器调度:告别delay
很多人写这种项目,喜欢用delay_ms()来控节奏。比如读一次ADC,delay 100ms,再读一次。这样写简单,但效率低,而且delay期间CPU什么都做不了。如果还要处理按键、刷新OLED,就会卡顿。
更好的做法是用定时器中断来调度任务。比如配置一个1ms的定时器中断,在中断里设置标志位。主循环里检查标志位,该读ADC就读ADC,该刷OLED就刷OLED。这样各个任务的节奏独立,互不干扰。
具体做法:配置TIM2为1ms中断,在中断服务函数里:
void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); tick_1ms++; if (tick_1ms % 100 == 0) flag_read_adc = 1; if (tick_1ms % 200 == 0) flag_update_oled = 1; if (tick_1ms % 500 == 0) flag_check_alarm = 1; } }主循环里:
while (1) { if (flag_read_adc) { flag_read_adc = 0; read_adc(); } if (flag_update_oled) { flag_update_oled = 0; update_oled(); } if (flag_check_alarm) { flag_check_alarm = 0; check_alarm(); } feed_dog(); }这样每个任务按自己的节奏执行,ADC每100ms读一次,OLED每200ms刷一次,报警每500ms检查一次,互不阻塞。
7.3 调试输出:串口打印不能少
即使OLED能显示,串口打印也不能省。调试的时候,把ADC原始值、换算后的温度电压电流、报警状态都打印到串口,用串口助手看。这样比盯着OLED屏幕方便多了,而且能看到历史数据。
串口配置很简单:波特率115200,8位数据,1位停止,无校验。用printf重定向到串口,注意要勾选“Use MicroLIB”,否则printf会占用大量Flash。
int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); return ch; }然后在代码里直接printf,格式自己定。我通常每100ms打印一行:
T=25.3C V=12.01V I=0.523A ALARM=0这样一眼就能看出数据是否正常,报警逻辑是否正确。
8. 实测数据与经验总结
8.1 精度实测:和万用表对比
我用这个系统测了一组数据,和Fluke 15B+万用表对比。温度方面,NTC方案在25℃到80℃范围内,误差在±0.5℃以内;DS18B20在同样范围内误差在±0.3℃以内。电压方面,用0.1%分压电阻,12V量程内误差在±0.02V以内。电流方面,用INA219测0到2A,误差在±5mA以内;用ACS712测同样范围,误差在±30mA以内。
这些数据说明:如果你对精度有要求,传感器和电阻的选型比代码优化更重要。用1%电阻,代码写得再好,误差也下不来;用0.1%电阻,代码随便写写,精度也能达标。
8.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| OLED不亮 | 电源没接好 | 量VCC和GND之间电压 |
| OLED不亮 | I2C地址不对 | 用I2C扫描程序找地址 |
| OLED花屏 | 显存没初始化 | 上电先清屏 |
| ADC读数跳动大 | 没加滤波电容 | 引脚加0.1μF到地 |
| ADC读数偏大 | 没做校准 | 加ADC校准代码 |
| 温度读数不准 | NTC查表不对 | 检查分压电阻和查表范围 |
| 电流读数偏小 | 分流电阻太大 | 减小分流电阻或换INA219 |
| 报警频繁触发 | 没加回差 | 加5%到10%的回差 |
| 蜂鸣器不响 | 驱动电流不够 | 加三极管驱动 |
| 程序跑飞 | 没开看门狗 | 配置IWDG并定期喂狗 |
8.3 几个让我印象深刻的坑
第一个坑:I2C上拉电阻。我一开始用10kΩ,OLED时亮时不亮,换了4.7kΩ之后稳定了。后来查资料才知道,I2C总线的上升时间由上拉电阻和总线电容决定,10kΩ在400kHz下上升时间太长,导致时序不满足。
第二个坑:ADC参考电压。STM32的ADC参考电压默认是VDDA,如果VDDA和VDD之间有压差,或者VDDA纹波大,ADC读数就会偏。我后来在VDDA引脚加了一个10μF钽电容和一个0.1μF陶瓷电容,读数稳定性明显提升。
第三个坑:蜂鸣器干扰。蜂鸣器响的时候,ADC读数会跳。原因是蜂鸣器的电流脉冲通过地线耦合到了ADC。解决办法是蜂鸣器的地线单独走,不要和模拟地混在一起。如果板子已经做好了,可以在蜂鸣器电源脚加一个100μF电容,吸收电流脉冲。
第四个坑:OLED刷新和ADC采样的时序冲突。如果ADC采样和OLED刷新同时进行,I2C的时钟可能会干扰ADC。我的做法是:ADC采样期间关闭I2C中断,采样完成后再开。或者用DMA搬运ADC数据,CPU不参与,就不会冲突。
这些坑,每一个都花了我至少半天时间排查。写出来是希望后来的人能少走弯路。这个项目本身不难,但细节很多,把细节都处理好,它就是一个能实际用起来的小系统,而不是一个只能演示的玩具。