1. 从一个厨房角落里的需求说起前阵子帮朋友改造他家的旧体重秤,拆开一看,里面就是一块廉价的8位单片机加一个称重传感器,显示用的是段码屏,功能单一到只能站上去看个体重数字。朋友抱怨说,家里老人眼神不好,那个小屏幕的数字根本看不清,而且每次称完还得自己拿本子记,时间一长就懒得记了。这个场景让我意识到,一个带数据记录、能语音播报、还能通过蓝牙把数据传到手机的智能体重秤,其实是一个非常适合用STM32来做的小项目。它涉及模拟前端信号采集、ADC采样、数字滤波、OLED显示驱动、蓝牙通信协议、低功耗管理等多个嵌入式开发的核心知识点,麻雀虽小五脏俱全。这个项目我前后做了大概三周,中间踩了不少坑,从传感器选型到滤波算法调参,从电源噪声干扰到蓝牙数据丢包,每一个环节都有值得说道的地方。今天就把整个项目的设计思路、原理图关键部分、代码架构、仿真验证方法以及实际调试中遇到的问题,完整地梳理一遍。不管你是刚学完STM32基础外设想找个综合项目练手的学生,还是想做一个实用小工具放在家里的电子爱好者,这个项目都能给你提供一条清晰的实现路径。我尽量把每个设计决策背后的原因讲清楚,让你不仅知道怎么做,还知道为什么这么做。2. 整体方案设计与核心器件选型2.1 为什么选STM32而不是更便宜的8位机很多人会问,一个体重秤而已,用STC89C52或者STM8这种便宜片子不就行了吗?我一开始也这么想过,但实际算下来,STM32F103C8T6这颗芯片的性价比在这个项目里反而更高。原因有几个:第一,体重秤需要做数字滤波,像滑动平均、中值滤波、甚至卡尔曼滤波,这些算法在8位机上跑起来很吃力,而STM32F103的72MHz主频和硬件乘法器处理起来绰绰有余;第二,我要驱动OLED显示屏,I2C或者SPI接口的OLED刷新需要一定的带宽,8位机模拟I2C速度慢,刷新率上不去,显示会有闪烁感;第三,蓝牙模块的串口通信需要及时响应,STM32的DMA加空闲中断接收方式可以做到不丢包,而8位机的中断响应和缓冲区管理要麻烦得多;第四,后续如果要加Flash存储历史数据、做OTA升级,STM32的资源和生态优势就体现出来了。具体型号我选的是STM32F103C8T6,也就是常说的“蓝板”或者“最小系统板”。这颗芯片有64KB Flash、20KB RAM,对于这个项目来说完全够用。封装是LQFP48,焊接难度适中,自己打板或者用现成的最小系统板都可以。如果你手头有STM32F401或者G030系列,也完全可以替换,代码移植量不大,主要改一下时钟配置和HAL库的型号宏定义就行。2.2 称重传感器的选型与信号调理体重秤的核心传感器是电阻应变式称重传感器,常见的有两种结构:一种是四个半桥应变片贴在秤体金属支架上组成全桥,另一种是直接买一个成品的人体秤传感器模组。我选的是后者,因为成品模组的四角已经校准好了,线性度和重复性都有保证,省去了自己贴片和组桥的麻烦。具体参数是量程150kg,输出灵敏度1mV/V,输入阻抗1kΩ左右。这里有一个关键点:STM32的ADC参考电压是3.3V,12位分辨率下最小分辨电压是3.3V/4096≈0.8mV。而传感器在150kg满载时的输出只有1mV/V×3.3V=3.3mV,也就是说满量程输出才3.3mV,对应ADC只有4个LSB的变化,根本没法用。所以必须加仪表放大器进行信号放大。我选的是HX711这款专用的24位ADC芯片,它内部集成了低噪声可编程增益放大器,增益可选128倍,专门为称重传感器设计。HX711的输出是串行数字信号,直接跟STM32的GPIO相连,读取时序很简单,比用STM32内部ADC加外部运放的方案要稳定得多。注意:HX711的供电电压会影响零点漂移,建议用独立的LDO给它供3.3V,不要跟电机、显示屏共用一路电源,否则称重读数会跳得厉害。2.3 显示与交互方案显示部分我用了0.96寸的OLED屏,SSD1306驱动芯片,I2C接口,分辨率128×64。选它的理由很简单:自发光、对比度高、可视角度大,而且功耗低,适合电池供电的场景。相比LCD屏,OLED不需要背光,在暗光环境下看数字更清晰。交互方面,我加了三个轻触按键:一个用于去皮归零,一个用于切换单位(kg/lb),一个用于查看历史记录。按键电路用10kΩ上拉电阻加100nF电容做硬件消抖,软件里再做一次20ms的延时确认,双保险。蓝牙模块用的是HC-05,虽然它有点老,但胜在资料多、AT指令简单、配对稳定。如果你想要更低功耗,可以换成BLE模块,但那样手机端就需要自己写APP或者用微信小程序来接收数据,复杂度会上升。HC-05走的是SPP协议,安卓手机可以直接用蓝牙串口助手接收数据,调试起来非常方便。2.4 电源管理设计整个系统可以用三节AA电池供电,也可以用USB 5V供电。电池供电时,需要一颗低压差稳压器把电压稳到3.3V。我选的是XC6206P332,静态电流只有1μA,非常适合电池场景。为了进一步省电,我在固件里做了自动休眠:如果连续30秒没有称重动作(通过检测HX711的输出变化率来判断),就关闭OLED显示,把STM32切到Stop模式,RTC定时唤醒检查一次传感器。实测下来,三节2000mAh的AA电池可以用大概两个月左右。3. 原理图关键电路解析与设计考量3.1 HX711与传感器的连接细节先看传感器到HX711的部分。人体秤传感器模组一般有四根线:E+、E-、A+、A-,分别对应激励正、激励负、信号正、信号负。HX711的AVDD和DVDD都接3.3V,VFB悬空不用。传感器E+接HX711的AVDD,E-接GND,A+接INPA+,A-接INPA-。这里要注意,传感器的屏蔽线要单点接地,而且走线尽量短,避免引入工频干扰。我在PCB上把HX711和传感器接口放在板子边缘,远离STM32的晶振和数字信号线,这样称重读数最稳定。HX711的DOUT和PD_SCK分别接STM32的PB0和PB1。DOUT是数据输出,PD_SCK是时钟输入。读取时序是:当DOUT为低电平时,表示数据准备好,然后PD_SCK发送25到27个时钟脉冲,前24个脉冲读出24位数据,后面的脉冲用来选择下一次的增益通道。具体代码后面会详细讲。3.2 OLED与按键电路OLED的I2C接口我接的是STM32的PB6(SCL)和PB7(SDA),这两个引脚是I2C1的默认复用引脚。上拉电阻用4.7kΩ,接3.3V。OLED的VCC和GND之间并一个100nF的退耦电容,放在靠近屏幕接口的位置。按键部分,三个按键分别接PA0、PA1、PA2,配置为上拉输入,按下时接地。每个按键并联一个100nF电容到地,用来滤除机械抖动产生的高频噪声。3.3 电源部分与退耦电源部分,USB 5V输入经过一个SS34肖特基二极管防反接,然后分两路:一路直接给HC-05供电(HC-05工作电压3.6V到6V,5V没问题),另一路经过XC6206稳压到3.3V给STM32、HX711和OLED供电。每个芯片的电源引脚旁边都要放一个100nF的陶瓷电容和一个10μF的钽电容,位置越靠近引脚越好。HX711的AVDD和DVDD之间还要加一个1Ω的电阻加100nF电容做RC滤波,进一步隔离数字噪声。实操心得:我第一版PCB没加这个RC滤波,结果称重读数在OLED刷新的时候会跳变十几克,加了之后跳变降到2克以内。这个细节在HX711的数据手册里其实有提到,但很容易被忽略。4. 固件架构与核心代码实现4.1 工程目录结构与模块划分整个工程我用Keil MDK开发,基于HAL库。目录结构是这样的:Core文件夹放main.c和中断处理,Drivers文件夹放HAL库和STM32的启动文件,Hardware文件夹放各个外设的驱动,包括hx711.c、oled.c、key.c、bluetooth.c,App文件夹放应用逻辑,包括weight.c(称重算法)、menu.c(菜单状态机)、storage.c(Flash存储)。这样分层的目的是让代码可移植,比如你换一个显示屏,只需要改oled.c,其他模块不用动。4.2 HX711驱动与数据读取HX711的驱动核心是一个读取24位数据的函数。先等DOUT变低,然后循环24次,每次拉高PD_SCK,延时1微秒,读取DOUT的值放到最高位,再拉低PD_SCK,延时1微秒。24位读完后,再发一个脉冲,把PD_SCK拉高再拉低,这样下一次转换的增益就是128。读到的24位数据是二进制补码形式,需要转换成有符号的32位整数。如果最高位是1,说明是负数,要取反加一。int32_t HX711_Read(void) { int32_t value = 0; uint8_t i; while(HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET); for(i=0; i<24; i++) { HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); delay_us(1); value = value << 1; if(HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET) value++; HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); delay_us(1); } HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); delay_us(1); if(value & 0x800000) value |= 0xFF000000; return value; }这个函数每调用一次大概需要几十微秒,采样率可以做到10Hz以上,对于体重秤来说完全够用。实际使用的时候,我会连续读10次,去掉最大值和最小值,剩下的8个取平均,这样一次称重结果大概需要1秒左右,但稳定性很好。4.3 称重算法:从原始值到公斤数HX711读出来的原始值是一个24位有符号整数,范围大概是-8388608到8388607。空载的时候,这个值在零点附近波动,大概几千个LSB。放上已知重量的物体后,原始值会线性变化。所以标定过程就是:先记录空载时的原始值offset,再放一个已知重量(比如5kg的哑铃)记录原始值raw,然后计算比例系数scale = (raw - offset) / 5.0。之后每次称重,重量 = (当前原始值 - offset) / scale。float Get_Weight(void) { int32_t raw = HX711_Read_Average(10); float weight = (raw - offset) / scale; if(weight < 0.05) weight = 0; return weight; }这里有个细节:offset和scale需要存在Flash里,掉电不丢失。我用了STM32内部的Flash最后一页来存这两个参数,每次标定后写入,上电时读出来。注意Flash写入前要先擦除整页,而且写入次数有限制(大概1万次),所以不要频繁标定。4.4 OLED显示驱动与菜单状态机OLED驱动我用的是软件I2C,因为硬件I2C在STM32F103上有些坑,比如死锁问题。软件I2C虽然速度慢一点,但稳定可控。显示内容分两行:第一行显示当前重量,字体用16×32的大号字,方便老人看清;第二行显示单位、蓝牙连接状态和电池电量。菜单状态机用switch-case实现,三个按键分别对应“去皮”、“单位切换”、“历史记录”。历史记录最多存30条,存在Flash的一个独立扇区里,每条记录4个字节(float类型的重量),循环覆盖写入。4.5 蓝牙通信协议设计HC-05默认波特率是9600,我改成115200以提高传输速度。数据协议我定义了一个简单的帧格式:帧头0xAA,然后是4字节的float重量数据,最后是1字节的校验和(前面5个字节的异或)。手机端收到后先找帧头,再校验,通过后解析出重量值。发送频率是每2秒一次,避免数据太快手机端处理不过来。void Bluetooth_SendWeight(float weight) { uint8_t buf[6]; buf[0] = 0xAA; memcpy(&buf[1], &weight, 4); buf[5] = buf[0] ^ buf[1] ^ buf[2] ^ buf[3] ^ buf[4]; HAL_UART_Transmit(&huart1, buf, 6, 100); }注意:HC-05在配对成功后,如果手机端没有打开串口接收,模块会缓冲数据,缓冲满了之后新的数据会丢失。所以最好在手机端定时清空缓冲区,或者降低发送频率。5. 仿真验证与实物调试对比5.1 用Proteus做前期验证在打板之前,我用Proteus搭了一个仿真电路,主要是验证HX711的读取时序和OLED的显示逻辑。Proteus里没有HX711的模型,我用一个电位器模拟传感器输出,然后用ADC0832代替HX711,虽然不完全一样,但可以验证STM32的GPIO时序和显示刷新。OLED用Proteus自带的12864液晶模拟,显示效果虽然不如真实的OLED,但足以验证菜单逻辑。仿真里我重点看了两个东西:一是按键消抖的逻辑是否正确,二是称重数据的滑动平均滤波效果。通过Proteus的虚拟示波器,我可以看到滤波前后的波形对比,滤波后的曲线明显平滑很多。这一步大概花了一个下午,但省去了打板后反复飞线的麻烦。5.2 实物调试中的电源噪声问题第一版实物板子焊好之后,发现称重读数一直在跳,空载时波动范围有±50克。用示波器看HX711的AVDD引脚,发现上面有大概20mV的高频纹波,频率跟OLED的I2C时钟一致。原因找到了:OLED刷新的时候,I2C总线上的电流突变通过电源线耦合到了HX711的模拟供电上。解决办法就是在HX711的AVDD引脚前面加一个RC滤波,电阻用10Ω,电容用10μF加100nF并联。改完之后,空载波动降到±3克以内。另一个问题是蓝牙发送数据时,称重读数会突然跳一下。这是因为HC-05在发送瞬间电流增大,导致3.3V电源瞬间跌落。我在HC-05的VCC引脚旁边加了一个220μF的电解电容,问题解决。5.3 标定过程的实操记录标定是体重秤精度的关键。我的标定步骤是这样的:第一步,上电后先预热5分钟,让HX711和传感器的温度稳定下来;第二步,空载状态下连续读取100次原始值,取平均作为offset;第三步,放上5kg的标准砝码(我用的是厨房秤校准过的哑铃),读取100次取平均,计算scale;第四步,把offset和scale写入Flash;第五步,用10kg和20kg的物体验证,误差在±20克以内算合格。这里有个经验:标定时的环境温度要和实际使用温度接近。我试过在空调房里标定,然后拿到没有空调的厨房用,读数会偏大概30克。后来我在固件里加了一个简单的温度补偿,用STM32内部的温度传感器读温度,然后根据温度偏移量微调offset,效果好了很多。6. 常见问题排查与避坑指南6.1 称重读数不稳定怎么办这是最常见的问题,原因通常有三个:电源噪声、传感器接触不良、滤波参数不合适。排查顺序是:先用万用表量HX711的AVDD电压,看是否稳定在3.3V;然后检查传感器四根线的焊接是否牢固,特别是屏蔽线是否单点接地;最后调整滤波参数,把滑动平均的窗口从10增加到20,或者加入中值滤波。如果还是跳,可以在HX711的输入端并一个0.1μF的电容,滤除高频干扰。6.2 OLED不显示或显示乱码先检查I2C地址是否正确,SSD1306的地址通常是0x78或0x7A,取决于SA0引脚的电平。然后检查初始化序列是否完整,特别是对比度和显示模式的配置。如果显示乱码,多半是I2C时序问题,可以降低软件I2C的时钟频率,把延时从1微秒增加到5微秒试试。另外,OLED的VCC和GND一定要接对,接反了屏幕会烧掉。6.3 蓝牙连接不稳定或数据丢包HC-05的默认波特率是9600,如果STM32的串口配置也是9600,一般没问题。但如果改成115200,需要先用AT指令把HC-05的波特率也改过来,否则收到的全是乱码。数据丢包的话,检查STM32的串口发送是否用了DMA,如果用阻塞发送,在发送期间如果被中断打断,可能会导致数据错位。建议用DMA发送,或者发送前关中断,发送完再开。6.4 Flash写入失败或数据丢失STM32F103的Flash写入需要先擦除整页,擦除的最小单位是1KB。如果offset和scale存在同一页,每次标定都要擦除整页,会缩短Flash寿命。我的做法是把它们存在不同的页,或者加一个写入计数器,超过一定次数就换一页。另外,写入前要确保电压稳定,如果电池电量低,Flash写入可能会失败,所以最好在固件里加一个电压检测,低于3.0V就禁止标定操作。6.5 常见问题速查表问题现象可能原因排查方法解决方案称重读数跳动大电源噪声示波器看AVDD纹波加RC滤波,独立LDO供电OLED不亮I2C地址错误用逻辑分析仪抓I2C波形确认SA0电平,改地址蓝牙乱码波特率不匹配检查双方波特率设置统一改为115200Flash数据丢失擦除次数过多读Flash状态寄存器分页存储,加磨损均衡按键无反应上拉电阻未接万用表量按键引脚电压加10kΩ上拉,软件消抖电池续航短未进休眠测休眠电流加自动休眠,关外设时钟7. 项目扩展方向与个人体会这个项目做完之后,我又陆续加了一些功能。比如用STM32的RTC做一个简单的日历,每次称重时把日期和时间一起存到Flash里,这样历史记录就能看到体重变化的趋势。还加了一个蜂鸣器,称重完成后“滴”一声,提示可以下来了。后来觉得蜂鸣器太吵,换成了振动马达,体验更好。蓝牙部分我试过换成BLE模块,配合手机端的一个简单APP,可以画出体重曲线图。不过APP开发不是我的强项,就用了一个开源的蓝牙串口APP,把数据导出成CSV,再用Excel画图。如果你有安卓开发基础,可以自己写一个APP,用MPAndroidChart库画曲线,那样体验会好很多。最后分享一个我在调试过程中发现的小技巧:HX711的读取速率可以通过调整PD_SCK的时钟频率来改变,但不要超过1MHz,否则数据会出错。另外,如果传感器是四线制的,但你的HX711模块是带屏蔽罩的那种,记得把屏蔽罩接地,否则容易引入干扰。这个项目整体成本大概在50元左右,其中STM32最小系统板15元,HX711模块8元,OLED屏12元,HC-05模块10元,传感器模组20元,其他阻容和PCB大概10元。如果你手头有现成的模块,可以直接用杜邦线连接,不用打板也能跑起来。
1. 从一个厨房角落里的需求说起前阵子帮朋友改造他家的旧体重秤,拆开一看,里面就是一块廉价的8位单片机加一个称重传感器,显示用的是段码屏,功能单一到只能站上去看个体重数字。朋友抱怨说,家里老人眼神不好,那个小屏幕的数字根本看不清,而且每次称完还得自己拿本子记,时间一长就懒得记了。这个场景让我意识到,一个带数据记录、能语音播报、还能通过蓝牙把数据传到手机的智能体重秤,其实是一个非常适合用STM32来做的小项目。它涉及模拟前端信号采集、ADC采样、数字滤波、OLED显示驱动、蓝牙通信协议、低功耗管理等多个嵌入式开发的核心知识点,麻雀虽小五脏俱全。这个项目我前后做了大概三周,中间踩了不少坑,从传感器选型到滤波算法调参,从电源噪声干扰到蓝牙数据丢包,每一个环节都有值得说道的地方。今天就把整个项目的设计思路、原理图关键部分、代码架构、仿真验证方法以及实际调试中遇到的问题,完整地梳理一遍。不管你是刚学完STM32基础外设想找个综合项目练手的学生,还是想做一个实用小工具放在家里的电子爱好者,这个项目都能给你提供一条清晰的实现路径。我尽量把每个设计决策背后的原因讲清楚,让你不仅知道怎么做,还知道为什么这么做。2. 整体方案设计与核心器件选型2.1 为什么选STM32而不是更便宜的8位机很多人会问,一个体重秤而已,用STC89C52或者STM8这种便宜片子不就行了吗?我一开始也这么想过,但实际算下来,STM32F103C8T6这颗芯片的性价比在这个项目里反而更高。原因有几个:第一,体重秤需要做数字滤波,像滑动平均、中值滤波、甚至卡尔曼滤波,这些算法在8位机上跑起来很吃力,而STM32F103的72MHz主频和硬件乘法器处理起来绰绰有余;第二,我要驱动OLED显示屏,I2C或者SPI接口的OLED刷新需要一定的带宽,8位机模拟I2C速度慢,刷新率上不去,显示会有闪烁感;第三,蓝牙模块的串口通信需要及时响应,STM32的DMA加空闲中断接收方式可以做到不丢包,而8位机的中断响应和缓冲区管理要麻烦得多;第四,后续如果要加Flash存储历史数据、做OTA升级,STM32的资源和生态优势就体现出来了。具体型号我选的是STM32F103C8T6,也就是常说的“蓝板”或者“最小系统板”。这颗芯片有64KB Flash、20KB RAM,对于这个项目来说完全够用。封装是LQFP48,焊接难度适中,自己打板或者用现成的最小系统板都可以。如果你手头有STM32F401或者G030系列,也完全可以替换,代码移植量不大,主要改一下时钟配置和HAL库的型号宏定义就行。2.2 称重传感器的选型与信号调理体重秤的核心传感器是电阻应变式称重传感器,常见的有两种结构:一种是四个半桥应变片贴在秤体金属支架上组成全桥,另一种是直接买一个成品的人体秤传感器模组。我选的是后者,因为成品模组的四角已经校准好了,线性度和重复性都有保证,省去了自己贴片和组桥的麻烦。具体参数是量程150kg,输出灵敏度1mV/V,输入阻抗1kΩ左右。这里有一个关键点:STM32的ADC参考电压是3.3V,12位分辨率下最小分辨电压是3.3V/4096≈0.8mV。而传感器在150kg满载时的输出只有1mV/V×3.3V=3.3mV,也就是说满量程输出才3.3mV,对应ADC只有4个LSB的变化,根本没法用。所以必须加仪表放大器进行信号放大。我选的是HX711这款专用的24位ADC芯片,它内部集成了低噪声可编程增益放大器,增益可选128倍,专门为称重传感器设计。HX711的输出是串行数字信号,直接跟STM32的GPIO相连,读取时序很简单,比用STM32内部ADC加外部运放的方案要稳定得多。注意:HX711的供电电压会影响零点漂移,建议用独立的LDO给它供3.3V,不要跟电机、显示屏共用一路电源,否则称重读数会跳得厉害。2.3 显示与交互方案显示部分我用了0.96寸的OLED屏,SSD1306驱动芯片,I2C接口,分辨率128×64。选它的理由很简单:自发光、对比度高、可视角度大,而且功耗低,适合电池供电的场景。相比LCD屏,OLED不需要背光,在暗光环境下看数字更清晰。交互方面,我加了三个轻触按键:一个用于去皮归零,一个用于切换单位(kg/lb),一个用于查看历史记录。按键电路用10kΩ上拉电阻加100nF电容做硬件消抖,软件里再做一次20ms的延时确认,双保险。蓝牙模块用的是HC-05,虽然它有点老,但胜在资料多、AT指令简单、配对稳定。如果你想要更低功耗,可以换成BLE模块,但那样手机端就需要自己写APP或者用微信小程序来接收数据,复杂度会上升。HC-05走的是SPP协议,安卓手机可以直接用蓝牙串口助手接收数据,调试起来非常方便。2.4 电源管理设计整个系统可以用三节AA电池供电,也可以用USB 5V供电。电池供电时,需要一颗低压差稳压器把电压稳到3.3V。我选的是XC6206P332,静态电流只有1μA,非常适合电池场景。为了进一步省电,我在固件里做了自动休眠:如果连续30秒没有称重动作(通过检测HX711的输出变化率来判断),就关闭OLED显示,把STM32切到Stop模式,RTC定时唤醒检查一次传感器。实测下来,三节2000mAh的AA电池可以用大概两个月左右。3. 原理图关键电路解析与设计考量3.1 HX711与传感器的连接细节先看传感器到HX711的部分。人体秤传感器模组一般有四根线:E+、E-、A+、A-,分别对应激励正、激励负、信号正、信号负。HX711的AVDD和DVDD都接3.3V,VFB悬空不用。传感器E+接HX711的AVDD,E-接GND,A+接INPA+,A-接INPA-。这里要注意,传感器的屏蔽线要单点接地,而且走线尽量短,避免引入工频干扰。我在PCB上把HX711和传感器接口放在板子边缘,远离STM32的晶振和数字信号线,这样称重读数最稳定。HX711的DOUT和PD_SCK分别接STM32的PB0和PB1。DOUT是数据输出,PD_SCK是时钟输入。读取时序是:当DOUT为低电平时,表示数据准备好,然后PD_SCK发送25到27个时钟脉冲,前24个脉冲读出24位数据,后面的脉冲用来选择下一次的增益通道。具体代码后面会详细讲。3.2 OLED与按键电路OLED的I2C接口我接的是STM32的PB6(SCL)和PB7(SDA),这两个引脚是I2C1的默认复用引脚。上拉电阻用4.7kΩ,接3.3V。OLED的VCC和GND之间并一个100nF的退耦电容,放在靠近屏幕接口的位置。按键部分,三个按键分别接PA0、PA1、PA2,配置为上拉输入,按下时接地。每个按键并联一个100nF电容到地,用来滤除机械抖动产生的高频噪声。3.3 电源部分与退耦电源部分,USB 5V输入经过一个SS34肖特基二极管防反接,然后分两路:一路直接给HC-05供电(HC-05工作电压3.6V到6V,5V没问题),另一路经过XC6206稳压到3.3V给STM32、HX711和OLED供电。每个芯片的电源引脚旁边都要放一个100nF的陶瓷电容和一个10μF的钽电容,位置越靠近引脚越好。HX711的AVDD和DVDD之间还要加一个1Ω的电阻加100nF电容做RC滤波,进一步隔离数字噪声。实操心得:我第一版PCB没加这个RC滤波,结果称重读数在OLED刷新的时候会跳变十几克,加了之后跳变降到2克以内。这个细节在HX711的数据手册里其实有提到,但很容易被忽略。4. 固件架构与核心代码实现4.1 工程目录结构与模块划分整个工程我用Keil MDK开发,基于HAL库。目录结构是这样的:Core文件夹放main.c和中断处理,Drivers文件夹放HAL库和STM32的启动文件,Hardware文件夹放各个外设的驱动,包括hx711.c、oled.c、key.c、bluetooth.c,App文件夹放应用逻辑,包括weight.c(称重算法)、menu.c(菜单状态机)、storage.c(Flash存储)。这样分层的目的是让代码可移植,比如你换一个显示屏,只需要改oled.c,其他模块不用动。4.2 HX711驱动与数据读取HX711的驱动核心是一个读取24位数据的函数。先等DOUT变低,然后循环24次,每次拉高PD_SCK,延时1微秒,读取DOUT的值放到最高位,再拉低PD_SCK,延时1微秒。24位读完后,再发一个脉冲,把PD_SCK拉高再拉低,这样下一次转换的增益就是128。读到的24位数据是二进制补码形式,需要转换成有符号的32位整数。如果最高位是1,说明是负数,要取反加一。int32_t HX711_Read(void) { int32_t value = 0; uint8_t i; while(HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET); for(i=0; i<24; i++) { HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); delay_us(1); value = value << 1; if(HAL_GPIO_ReadPin(HX711_DOUT_GPIO_Port, HX711_DOUT_Pin) == GPIO_PIN_SET) value++; HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); delay_us(1); } HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET); delay_us(1); if(value & 0x800000) value |= 0xFF000000; return value; }这个函数每调用一次大概需要几十微秒,采样率可以做到10Hz以上,对于体重秤来说完全够用。实际使用的时候,我会连续读10次,去掉最大值和最小值,剩下的8个取平均,这样一次称重结果大概需要1秒左右,但稳定性很好。4.3 称重算法:从原始值到公斤数HX711读出来的原始值是一个24位有符号整数,范围大概是-8388608到8388607。空载的时候,这个值在零点附近波动,大概几千个LSB。放上已知重量的物体后,原始值会线性变化。所以标定过程就是:先记录空载时的原始值offset,再放一个已知重量(比如5kg的哑铃)记录原始值raw,然后计算比例系数scale = (raw - offset) / 5.0。之后每次称重,重量 = (当前原始值 - offset) / scale。float Get_Weight(void) { int32_t raw = HX711_Read_Average(10); float weight = (raw - offset) / scale; if(weight < 0.05) weight = 0; return weight; }这里有个细节:offset和scale需要存在Flash里,掉电不丢失。我用了STM32内部的Flash最后一页来存这两个参数,每次标定后写入,上电时读出来。注意Flash写入前要先擦除整页,而且写入次数有限制(大概1万次),所以不要频繁标定。4.4 OLED显示驱动与菜单状态机OLED驱动我用的是软件I2C,因为硬件I2C在STM32F103上有些坑,比如死锁问题。软件I2C虽然速度慢一点,但稳定可控。显示内容分两行:第一行显示当前重量,字体用16×32的大号字,方便老人看清;第二行显示单位、蓝牙连接状态和电池电量。菜单状态机用switch-case实现,三个按键分别对应“去皮”、“单位切换”、“历史记录”。历史记录最多存30条,存在Flash的一个独立扇区里,每条记录4个字节(float类型的重量),循环覆盖写入。4.5 蓝牙通信协议设计HC-05默认波特率是9600,我改成115200以提高传输速度。数据协议我定义了一个简单的帧格式:帧头0xAA,然后是4字节的float重量数据,最后是1字节的校验和(前面5个字节的异或)。手机端收到后先找帧头,再校验,通过后解析出重量值。发送频率是每2秒一次,避免数据太快手机端处理不过来。void Bluetooth_SendWeight(float weight) { uint8_t buf[6]; buf[0] = 0xAA; memcpy(&buf[1], &weight, 4); buf[5] = buf[0] ^ buf[1] ^ buf[2] ^ buf[3] ^ buf[4]; HAL_UART_Transmit(&huart1, buf, 6, 100); }注意:HC-05在配对成功后,如果手机端没有打开串口接收,模块会缓冲数据,缓冲满了之后新的数据会丢失。所以最好在手机端定时清空缓冲区,或者降低发送频率。5. 仿真验证与实物调试对比5.1 用Proteus做前期验证在打板之前,我用Proteus搭了一个仿真电路,主要是验证HX711的读取时序和OLED的显示逻辑。Proteus里没有HX711的模型,我用一个电位器模拟传感器输出,然后用ADC0832代替HX711,虽然不完全一样,但可以验证STM32的GPIO时序和显示刷新。OLED用Proteus自带的12864液晶模拟,显示效果虽然不如真实的OLED,但足以验证菜单逻辑。仿真里我重点看了两个东西:一是按键消抖的逻辑是否正确,二是称重数据的滑动平均滤波效果。通过Proteus的虚拟示波器,我可以看到滤波前后的波形对比,滤波后的曲线明显平滑很多。这一步大概花了一个下午,但省去了打板后反复飞线的麻烦。5.2 实物调试中的电源噪声问题第一版实物板子焊好之后,发现称重读数一直在跳,空载时波动范围有±50克。用示波器看HX711的AVDD引脚,发现上面有大概20mV的高频纹波,频率跟OLED的I2C时钟一致。原因找到了:OLED刷新的时候,I2C总线上的电流突变通过电源线耦合到了HX711的模拟供电上。解决办法就是在HX711的AVDD引脚前面加一个RC滤波,电阻用10Ω,电容用10μF加100nF并联。改完之后,空载波动降到±3克以内。另一个问题是蓝牙发送数据时,称重读数会突然跳一下。这是因为HC-05在发送瞬间电流增大,导致3.3V电源瞬间跌落。我在HC-05的VCC引脚旁边加了一个220μF的电解电容,问题解决。5.3 标定过程的实操记录标定是体重秤精度的关键。我的标定步骤是这样的:第一步,上电后先预热5分钟,让HX711和传感器的温度稳定下来;第二步,空载状态下连续读取100次原始值,取平均作为offset;第三步,放上5kg的标准砝码(我用的是厨房秤校准过的哑铃),读取100次取平均,计算scale;第四步,把offset和scale写入Flash;第五步,用10kg和20kg的物体验证,误差在±20克以内算合格。这里有个经验:标定时的环境温度要和实际使用温度接近。我试过在空调房里标定,然后拿到没有空调的厨房用,读数会偏大概30克。后来我在固件里加了一个简单的温度补偿,用STM32内部的温度传感器读温度,然后根据温度偏移量微调offset,效果好了很多。6. 常见问题排查与避坑指南6.1 称重读数不稳定怎么办这是最常见的问题,原因通常有三个:电源噪声、传感器接触不良、滤波参数不合适。排查顺序是:先用万用表量HX711的AVDD电压,看是否稳定在3.3V;然后检查传感器四根线的焊接是否牢固,特别是屏蔽线是否单点接地;最后调整滤波参数,把滑动平均的窗口从10增加到20,或者加入中值滤波。如果还是跳,可以在HX711的输入端并一个0.1μF的电容,滤除高频干扰。6.2 OLED不显示或显示乱码先检查I2C地址是否正确,SSD1306的地址通常是0x78或0x7A,取决于SA0引脚的电平。然后检查初始化序列是否完整,特别是对比度和显示模式的配置。如果显示乱码,多半是I2C时序问题,可以降低软件I2C的时钟频率,把延时从1微秒增加到5微秒试试。另外,OLED的VCC和GND一定要接对,接反了屏幕会烧掉。6.3 蓝牙连接不稳定或数据丢包HC-05的默认波特率是9600,如果STM32的串口配置也是9600,一般没问题。但如果改成115200,需要先用AT指令把HC-05的波特率也改过来,否则收到的全是乱码。数据丢包的话,检查STM32的串口发送是否用了DMA,如果用阻塞发送,在发送期间如果被中断打断,可能会导致数据错位。建议用DMA发送,或者发送前关中断,发送完再开。6.4 Flash写入失败或数据丢失STM32F103的Flash写入需要先擦除整页,擦除的最小单位是1KB。如果offset和scale存在同一页,每次标定都要擦除整页,会缩短Flash寿命。我的做法是把它们存在不同的页,或者加一个写入计数器,超过一定次数就换一页。另外,写入前要确保电压稳定,如果电池电量低,Flash写入可能会失败,所以最好在固件里加一个电压检测,低于3.0V就禁止标定操作。6.5 常见问题速查表问题现象可能原因排查方法解决方案称重读数跳动大电源噪声示波器看AVDD纹波加RC滤波,独立LDO供电OLED不亮I2C地址错误用逻辑分析仪抓I2C波形确认SA0电平,改地址蓝牙乱码波特率不匹配检查双方波特率设置统一改为115200Flash数据丢失擦除次数过多读Flash状态寄存器分页存储,加磨损均衡按键无反应上拉电阻未接万用表量按键引脚电压加10kΩ上拉,软件消抖电池续航短未进休眠测休眠电流加自动休眠,关外设时钟7. 项目扩展方向与个人体会这个项目做完之后,我又陆续加了一些功能。比如用STM32的RTC做一个简单的日历,每次称重时把日期和时间一起存到Flash里,这样历史记录就能看到体重变化的趋势。还加了一个蜂鸣器,称重完成后“滴”一声,提示可以下来了。后来觉得蜂鸣器太吵,换成了振动马达,体验更好。蓝牙部分我试过换成BLE模块,配合手机端的一个简单APP,可以画出体重曲线图。不过APP开发不是我的强项,就用了一个开源的蓝牙串口APP,把数据导出成CSV,再用Excel画图。如果你有安卓开发基础,可以自己写一个APP,用MPAndroidChart库画曲线,那样体验会好很多。最后分享一个我在调试过程中发现的小技巧:HX711的读取速率可以通过调整PD_SCK的时钟频率来改变,但不要超过1MHz,否则数据会出错。另外,如果传感器是四线制的,但你的HX711模块是带屏蔽罩的那种,记得把屏蔽罩接地,否则容易引入干扰。这个项目整体成本大概在50元左右,其中STM32最小系统板15元,HX711模块8元,OLED屏12元,HC-05模块10元,传感器模组20元,其他阻容和PCB大概10元。如果你手头有现成的模块,可以直接用杜邦线连接,不用打板也能跑起来。