
1. 为什么选择 STM32 HX711 这套组合做称重1.1 从需求倒推方案称重项目的核心诉求是什么做称重类项目绕不开三个硬指标精度、稳定性、响应速度。你拿一个厨房秤的方案去做工业配料秤精度差两个数量级反过来用 24 位工业级 ADC 去做一个只需要判断有没有东西放上去的检测装置成本又浪费得离谱。所以在动手之前先想清楚你的量程和精度要求。HX711 这颗芯片本质上是一个专为电子秤设计的 24 位模数转换器内部集成了稳压电源、片内时钟振荡器以及最重要的——两路差分输入通道其中通道 A 支持 128 倍或 64 倍增益通道 B 固定 32 倍增益。这意味着你不需要额外搭仪表放大器电阻应变片输出的毫伏级差分信号直接进 HX711 就能被放大到 ADC 能识别的范围。STM32 在这里扮演的角色是大脑读取 HX711 转换结果、做数字滤波、去皮标定、驱动 OLED 显示、处理按键交互。为什么不用 51 单片机因为称重项目里滤波算法、多任务调度、浮点运算对算力有要求STM32F103C8T6 这类 Cortex-M3 内核跑 72MHz做滑动平均加中值滤波毫无压力而且 HAL 库生态成熟OLED 驱动、定时器、串口这些外设配置起来很快。1.2 硬件选型的关键参数对比先把几个核心器件的选型逻辑理清楚这部分直接决定项目能不能跑起来。器件常见型号关键参数选型理由主控STM32F103C8T672MHz, 64KB Flash, 20KB RAM性价比高资料多够用称重 ADCHX71124位, 增益128/64/32专为称重设计内置放大显示屏SSD1306 0.96寸 OLED128x64, I2C/SPI自发光对比度高驱动简单传感器电阻应变式称重传感器1kg/5kg/20kg 可选输出 1mV/V 左右差分信号这里有个新手最容易踩的坑称重传感器的量程和 HX711 的增益要匹配。传感器输出灵敏度通常是 1mV/V假设激励电压 5V满量程输出就是 5mV。HX711 通道 A 在 128 增益下输入满量程约 ±20mV刚好覆盖。如果你选了 64 增益满量程变成 ±40mV虽然不会溢出但有效位数会浪费。所以默认用通道 A 128 增益除非你的传感器灵敏度特别高。1.3 系统整体架构拆解整个项目的信号链路是这样的称重传感器应变片电桥→ HX711差分放大 24位ADC→ STM32GPIO模拟时序读取→ 数字滤波 → 标定换算 → OLED显示HX711 和 STM32 之间用的是自定义两线串行接口不是标准 I2C 也不是 SPI就两根线PD_SCK时钟和 DOUT数据。STM32 通过拉高拉低 PD_SCK 来产生时钟脉冲HX711 在 DOUT 上逐位输出 24 位数据。这个时序必须严格遵守否则读出来的数据全是乱的。OLED 这边0.96 寸 SSD1306 用 I2C 接口只需要两根线SCL、SDA和 HX711 加起来一共占用 4 个 GPIO资源占用非常小。如果你用的是 SPI 版本 OLED速度会更快但接线多两根看个人取舍。2. HX711 驱动时序与采样率深度解析2.1 HX711 通信时序为什么时序错了数据就废了HX711 的数据读取时序是整个项目最核心也最容易出错的地方。它的工作流程是这样的当 DOUT 为高电平时表示转换数据还没准备好此时不能读当 DOUT 变为低电平表示数据就绪给 PD_SCK 发送 25~27 个时钟脉冲前 24 个脉冲每个上升沿从 DOUT 读出 1 位数据MSB 先行第 25 个脉冲用于选择下一次转换的通道和增益关键点在于第 25 个脉冲的数量决定下一次的通道和增益脉冲总数下次通道增益25A12826B3227A64所以如果你只发 24 个脉冲就拉低 PD_SCKHX711 会一直等待DOUT 保持状态下次读取就会错位。我见过太多人卡在这里读出来的数据要么全是 0xFFFFFF要么跳变剧烈根本原因就是脉冲数不对。// HX711 读取一个 24 位有符号数通道A增益128 long HX711_Read(void) { unsigned long count 0; unsigned char i; HX711_DOUT_READ(); // 配置 DOUT 为输入 while(HX711_DOUT_IN()); // 等待 DOUT 变低数据就绪 delay_us(1); for(i 0; i 24; i) { HX711_SCK_H(); delay_us(1); count count 1; HX711_SCK_L(); delay_us(1); if(HX711_DOUT_IN()) count; } // 第25个脉冲选择下次通道A增益128 HX711_SCK_H(); delay_us(1); HX711_SCK_L(); delay_us(1); // 24位有符号数转32位有符号 if(count 0x800000) count | 0xFF000000; return (long)count; }这段代码里delay_us(1)不能省。HX711 手册要求 PD_SCK 高电平持续时间 0.2~50us低电平同样。虽然 STM32 跑 72MHz 时 GPIO 翻转本身就够慢但为了保险加个 1us 延时最稳。实测下来不加延时在部分批次 HX711 上会偶发读错。2.2 采样率 10Hz 还是 80Hz这个选择影响你的滤波策略HX711 有两个采样率档位10Hz 和 80Hz通过 RATE 引脚控制。RATE 接低电平默认是 10Hz接高电平是 80Hz。很多人一上来就想用 80Hz觉得采样越快越好。但这里有个权衡10Hz 模式内部滤波更强噪声更低适合静态称重比如电子秤、配料秤80Hz 模式响应快但噪声明显增大适合动态称重比如皮带秤、分拣线我实测过同一套硬件10Hz 模式下读数波动在 ±5 个 ADC 码以内80Hz 模式下波动能到 ±30 个码。对于需要显示稳定重量的场景10Hz 是更优解。如果你确实需要快速响应可以在 80Hz 下配合更强的数字滤波但要注意滤波会引入延迟。提示RATE 引脚在模块上通常有焊盘可以改但如果你买的是成品 HX711 模块默认就是 10Hz不用动。2.3 数字滤波滑动平均 中值滤波的组合拳原始 ADC 数据一定带噪声直接显示会跳得没法看。我的做法是先中值滤波去脉冲干扰再滑动平均平滑。中值滤波窗口取 5滑动平均窗口取 10。为什么是这个组合中值滤波能干掉突发的尖峰噪声比如电源波动、机械振动滑动平均则让数据曲线更平滑。窗口太大响应慢太小滤波效果差5 和 10 是我反复试出来的平衡点。#define MEDIAN_WINDOW 5 #define AVERAGE_WINDOW 10 long median_filter(long new_val) { static long buf[MEDIAN_WINDOW] {0}; static int idx 0; long temp[MEDIAN_WINDOW]; int i, j; buf[idx] new_val; if(idx MEDIAN_WINDOW) idx 0; for(i 0; i MEDIAN_WINDOW; i) temp[i] buf[i]; // 冒泡排序 for(i 0; i MEDIAN_WINDOW - 1; i) for(j 0; j MEDIAN_WINDOW - 1 - i; j) if(temp[j] temp[j1]) { long t temp[j]; temp[j] temp[j1]; temp[j1] t; } return temp[MEDIAN_WINDOW / 2]; } long average_filter(long new_val) { static long buf[AVERAGE_WINDOW] {0}; static int idx 0; static long sum 0; long avg; sum - buf[idx]; buf[idx] new_val; sum new_val; if(idx AVERAGE_WINDOW) idx 0; avg sum / AVERAGE_WINDOW; return avg; }这两个函数串起来用average_filter(median_filter(raw))。注意滑动平均的 sum 要用 long 类型10 个 24 位数据加起来可能超过 int 范围。3. 标定与去皮让读数变成真实重量3.1 两点标定法的数学原理HX711 读出来的是 ADC 码值不是重量。要换算成克或千克需要标定。最常用的是两点标定法空载时读一个值raw_zero放上已知重量W_ref的砝码读一个值raw_ref计算比例系数scale (raw_ref - raw_zero) / W_ref实际重量weight (raw_current - raw_zero) / scale这个公式假设传感器线性度良好。实际上电阻应变式传感器在满量程 60% 以内线性度非常好所以标定砝码选满量程的 50%~80%最准。有个细节raw_zero和raw_ref都应该是滤波后的稳定值不能拿单次读数。我的做法是空载时连续读 20 次取平均作为零点放砝码后等读数稳定连续 10 次波动小于阈值再取平均。3.2 去皮功能的实现逻辑去皮Tare本质上是把当前重量当作新的零点。实现很简单float tare_offset 0; void tare(void) { tare_offset current_weight; // 当前重量记为皮重 } float get_weight(void) { return current_weight - tare_offset; }但这里有个坑去皮后如果传感器漂移零点会慢慢偏。我的经验是去皮操作后如果长时间不用最好重新标定零点。另外去皮值不要存到 Flash 里反复擦写STM32F103 的 Flash 擦写寿命只有 1 万次左右频繁存皮重会把扇区写坏。如果确实需要掉电保存用 EEPROM 或者加个铁电存储器。3.3 温漂补偿的实用技巧称重传感器对温度敏感尤其是廉价传感器温漂可能达到满量程的 0.05%/℃。如果你的项目工作在温差大的环境不做补偿的话早上标定好中午读数就偏了。简单的补偿方法在传感器附近加一个 NTC 热敏电阻或 DS18B20测温度然后做线性补偿。具体做法是在 20℃ 和 40℃ 两个温度点分别标定得到两个 scale 值计算温度系数k (scale_40 - scale_20) / 20实际使用时scale_compensated scale_20 k * (T - 20)这个方法虽然粗糙但能把温漂影响降低 70% 以上。如果要求更高就得做多点标定加查表了。4. OLED 显示驱动与界面设计4.1 SSD1306 I2C 驱动要点0.96 寸 OLED 用 SSD1306 驱动芯片I2C 地址通常是 0x78写或 0x7A。注意有些模块地址是 0x3C 或 0x3D这是 7 位地址左移一位后就是 0x78 和 0x7A。如果你用 HAL 库的HAL_I2C_Master_Transmit传的是 8 位地址所以用 0x78。初始化序列比较长但核心就几步关显示、设时钟分频、设多路复用比、设显示偏移、设起始行、设电荷泵、设内存寻址模式、开显示。这些寄存器配置在 SSD1306 手册里都有网上也有现成的初始化数组直接抄就行。// OLED 写命令 void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; // 0x00 表示命令 HAL_I2C_Master_Transmit(hi2c1, 0x78, buf, 2, 100); } // OLED 写数据 void OLED_WriteData(uint8_t data) { uint8_t buf[2] {0x40, data}; // 0x40 表示数据 HAL_I2C_Master_Transmit(hi2c1, 0x78, buf, 2, 100); }这里0x00和0x40是控制字节告诉 SSD1306 接下来的是命令还是数据。很多人第一次写 OLED 不亮就是忘了加这个控制字节。4.2 显示汉字和自定义图形SSD1306 本身不带字库显示英文和数字可以用内置的 6x8 或 8x16 点阵但显示汉字必须自己取模。常用的取模软件是 PCtoLCD2002设置成阴码、逐列式、顺向生成 16x16 的汉字点阵数组。显示汉字的函数逻辑是先设置页地址和列地址然后连续写 32 个字节16x16 点阵占两页每页 16 字节。void OLED_ShowChinese(uint8_t x, uint8_t page, const uint8_t *hz) { uint8_t i; OLED_SetPos(x, page); for(i 0; i 16; i) OLED_WriteData(hz[i]); OLED_SetPos(x, page 1); for(i 0; i 16; i) OLED_WriteData(hz[i 16]); }如果你要显示图片比如开机 Logo也是同样的原理用取模软件生成 128x64 的数组然后逐页写入。注意图片取模时选阴码因为 OLED 是 1 亮 0 灭阴码正好对应。4.3 界面布局与刷新策略称重项目的界面不需要花哨核心信息就三个当前重量、单位、状态指示。我的布局是第一行标题电子秤或项目名中间大字重量数值用 16x32 的大字体右下角单位g或kg左下角状态稳定或波动刷新策略上不要每次循环都全屏刷新I2C 速率有限全屏刷新一次要几十毫秒会拖慢主循环。我的做法是只刷新重量数值区域其他静态内容只在初始化时写一次。另外重量数值变化小于最小显示单位时不刷新避免屏幕闪烁。注意0.9 寸 OLED 和 0.96 寸的 I2C 兼容性有差异部分 0.9 寸模块需要上拉电阻更强4.7k 甚至 2.2k否则通信不稳定。如果你遇到花屏或偶尔不亮先检查上拉电阻。5. 完整实操流程与关键代码实现5.1 硬件接线与供电注意事项先把接线理清楚这是最容易出问题的地方。HX711 侧VCC → STM32 3.3V注意HX711 模块有的标 5V但 3.3V 也能工作且和 STM32 电平匹配GND → STM32 GNDDT (DOUT) → PA0SCK → PA1E / E- → 传感器激励正负A / A- → 传感器信号正负OLED 侧VCC → 3.3VGND → GNDSCL → PB6 (I2C1_SCL)SDA → PB7 (I2C1_SDA)供电上有个大坑HX711 和 OLED 不要共用一根细杜邦线供电。OLED 全亮时电流能到 20mAHX711 工作时也有几毫安共用细线会导致电压跌落HX711 读数跳动。我的做法是电源线单独走粗线或者在 HX711 的 VCC 和 GND 之间并一个 100uF 电解电容加 0.1uF 陶瓷电容。传感器接线更要小心E 和 E- 接反不会烧但 A 和 A- 接反会导致读数反向。如果你发现放上砝码读数变小把 A 和 A- 对调即可。5.2 主循环任务调度主循环不能写成读 HX711 → 显示 → 读 HX711 → 显示这种阻塞式否则按键响应会卡。我的做法是用定时器中断做时间片调度10ms 时间片读 HX71110Hz 采样率100ms 读一次这里做计数分频50ms 时间片更新滤波、计算重量100ms 时间片刷新 OLED20ms 时间片扫描按键volatile uint32_t tick_10ms 0; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim htim2) tick_10ms; } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_TIM2_Init(); OLED_Init(); OLED_Clear(); HX711_Init(); HAL_TIM_Base_Start_IT(htim2); uint32_t last_read 0, last_display 0, last_key 0; while(1) { uint32_t now tick_10ms; if(now - last_read 10) // 100ms 读一次 { last_read now; long raw HX711_Read(); long filtered average_filter(median_filter(raw)); current_weight (filtered - raw_zero) / scale; } if(now - last_display 10) // 100ms 刷新一次 { last_display now; OLED_ShowWeight(current_weight); } if(now - last_key 2) // 20ms 扫描按键 { last_key now; Key_Scan(); } } }这种写法下每个任务都不会互相阻塞按键响应灵敏显示也不闪。5.3 标定流程的代码实现标定我做成一个独立模式通过按键进入。流程是长按按键 3 秒进入标定模式OLED 提示请清空秤台等待 2 秒连续读 20 次 HX711取平均作为raw_zeroOLED 提示请放上砝码等待用户放好并按键确认连续读 20 次取平均作为raw_ref用户通过按键输入砝码重量或者代码里写死 500g计算scale (raw_ref - raw_zero) / 500.0保存到 Flash可选void Calibration(void) { long sum 0; OLED_ShowString(0, 0, Calibrating...); OLED_ShowString(0, 2, Empty the scale); HAL_Delay(2000); for(int i 0; i 20; i) { sum HX711_Read(); HAL_Delay(100); } raw_zero sum / 20; OLED_Clear(); OLED_ShowString(0, 0, Put 500g weight); OLED_ShowString(0, 2, Press key...); while(!Key_Pressed()); sum 0; for(int i 0; i 20; i) { sum HX711_Read(); HAL_Delay(100); } raw_ref sum / 20; scale (float)(raw_ref - raw_zero) / 500.0f; OLED_Clear(); OLED_ShowString(0, 0, Calibration OK); HAL_Delay(1000); }标定完成后scale和raw_zero就是你的核心参数。如果每次上电都要重新标定太麻烦可以把这两个值存到 Flash 的某个扇区上电时读出来。6. 常见问题排查与避坑经验6.1 读数不稳定、跳变剧烈的排查思路这是问得最多的问题。按以下顺序排查现象可能原因解决方法读数在几百个码内跳电源噪声加滤波电容单独供电读数周期性跳变50Hz 工频干扰传感器线远离电源线加屏蔽读数缓慢漂移温漂做温度补偿或等热稳定读数偶尔突变时序错误检查 25 个脉冲加延时读数一直是 0xFFFFFFDOUT 没接或损坏检查接线换模块我遇到过一个案例客户反映读数每隔几秒跳一次大的。查了半天发现是 HX711 的 SCK 线和电机的 PWM 线捆在一起了电机一转就干扰。把线分开走问题消失。所以布线时信号线一定要远离功率线。6.2 OLED 不亮或花屏的排查OLED 不亮先量 VCC 和 GND 之间电压是不是 3.3V。如果电压正常检查 I2C 地址对不对。用逻辑分析仪或者示波器看 SCL 和 SDA 有没有波形。如果没有波形说明 I2C 初始化失败检查 GPIO 配置和时钟使能。花屏通常是初始化序列不完整或者上拉电阻不合适。0.96 寸模块一般自带 4.7k 上拉但有些廉价模块上拉是 10k在 400kHz 速率下波形上升沿太慢导致数据错位。解决办法是把 I2C 速率降到 100kHz或者外接 2.2k 上拉电阻。还有一个隐蔽问题OLED 的复位引脚。有些模块有 RES 引脚必须在上电后给一个低脉冲复位否则不工作。如果你买的模块只有 4 个引脚VCC、GND、SCL、SDA那复位是内部完成的不用管。6.3 HX711 采样率切换的实操细节前面说了 10Hz 和 80Hz 的选择这里补充一个实操细节切换 RATE 引脚后需要等下一次转换完成才生效。也就是说你不能在读取过程中动态切换否则会读到错误数据。正确做法是切换 RATE 后丢弃接下来的一次读数从第二次开始才是新速率的数据。另外80Hz 模式下HX711 的转换时间是 12.5ms但如果你用 10ms 的定时器去读会读到还没转换完的数据DOUT 还是高程序会卡在while(HX711_DOUT_IN())里。所以 80Hz 模式下读取间隔要大于 12.5ms我一般用 15ms。6.4 按键抖动与长按短按识别机械按键抖动是必然的不处理的话按一次会触发多次。硬件上可以并 0.1uF 电容但软件消抖更灵活。我的做法是状态机 计时typedef enum { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED, KEY_LONG } KeyState; KeyState key_state KEY_IDLE; uint32_t key_tick 0; void Key_Scan(void) { uint8_t pin HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_2); switch(key_state) { case KEY_IDLE: if(pin 0) { key_state KEY_DEBOUNCE; key_tick tick_10ms; } break; case KEY_DEBOUNCE: if(tick_10ms - key_tick 2) // 20ms 消抖 { if(pin 0) { key_state KEY_PRESSED; key_tick tick_10ms; } else key_state KEY_IDLE; } break; case KEY_PRESSED: if(pin 1) { key_state KEY_IDLE; Key_ShortPress(); } else if(tick_10ms - key_tick 300) { key_state KEY_LONG; Key_LongPress(); } break; case KEY_LONG: if(pin 1) key_state KEY_IDLE; break; } }这样短按和长按都能识别而且不会重复触发。长按 3 秒进入标定模式短按去皮逻辑清晰。6.5 Flash 保存参数的注意事项STM32F103C8T6 的 Flash 是 64KB分成 64 个 1KB 的页。擦除最小单位是页写入最小单位是半字16 位。保存标定参数时不要每次标定都擦写同一页因为擦写寿命只有 1 万次。我的做法是在 Flash 末尾划一页专门存参数每次保存时先读出整页到 RAM修改对应位置擦除整页写回如果参数更新频繁可以用双页交替的方式A 页写满了写 B 页B 页写满了擦 A 页这样寿命翻倍。#define PARAM_ADDR 0x0800FC00 // 最后一页起始地址 typedef struct { long raw_zero; float scale; uint32_t magic; // 用于判断是否已初始化 } Params; void Save_Params(Params *p) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress PARAM_ADDR; erase.NbPages 1; uint32_t page_err; HAL_FLASHEx_Erase(erase, page_err); uint16_t *src (uint16_t *)p; for(int i 0; i sizeof(Params) / 2; i) HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, PARAM_ADDR i * 2, src[i]); HAL_FLASH_Lock(); }注意magic字段上电时先读这个值如果是特定值比如 0x5A5A说明参数有效否则用默认值。这样避免第一次上电读到全 0xFF 导致除零错误。7. 项目扩展方向与个人实操体会这套 STM32 HX711 OLED 的框架搭好之后扩展性其实很强。比如加一个继电器模块就能做定量控制——重量到了就断开加一个SD 卡模块就能记录称重数据加一个蓝牙模块就能把数据传到手机。我甚至见过有人用这套方案做智能药盒称药片重量来判断有没有按时吃药。如果你要做多路称重比如四个角各一个传感器可以用多个 HX711每个占用两个 GPIOSTM32F103C8T6 的 IO 完全够用。读取时轮流读注意每个 HX711 的 DOUT 要分开接SCK 可以共用。我个人在实际操作中的体会是称重项目 80% 的问题出在硬件和时序上软件算法只占 20%。很多人一上来就研究卡尔曼滤波结果连 HX711 的 25 个脉冲都没搞对。先把基础时序调通读数稳定了再考虑高级滤波。另外标定用的砝码一定要准你拿一个标称 500g 实际 480g 的砝码去标定后面所有读数都是错的。条件允许的话用标准砝码或者去菜市场找公平秤校准一下。最后分享一个小技巧调试阶段可以在 OLED 上同时显示原始 ADC 值和换算后的重量这样一眼就能看出是 ADC 读数问题还是换算问题。等调通了再把原始值隐藏掉。这个习惯帮我省了很多排查时间。