STM32驱动HX711电子秤程序实战:从时序、滤波到标定的完整指南 简介基于STM32与HX711的电子称程序完整工程面向嵌入式开发者与电子称相关项目学习者解决高精度称重数据采集、滤波及标定问题。工程基于STM32F407平台涵盖HX711驱动、限幅平均滤波、去皮及比例调整等核心功能。HX711作为专用高精度A/D转换器通过SCK与DOUT引脚与STM32的GPIO连接实现数字信号读取限幅平均滤波可有效抑制噪声去皮功能支持容器毛重扣除比例调整则完成传感器原始值到实际重量的标定代码结构清晰便于直接二次开发。压缩包共九十八个文件包含四十四份C源码、四十四份头文件及Keil工程配置等大小约四百九十八KB。源码覆盖底层驱动、应用逻辑与工程配置可导入Keil直接编译调试。已有二千九百八十五人学习。资源提供了完整的HX711驱动示例包含数字滤波、去皮和比例系数调整的具体实现可帮助开发者快速理解STM32与HX711的GPIO时序交互节省传感器调试时间。工程中硬件模块与系统库分离目录结构清晰适合入门学习也可作为商用电子称开发的基础模板。1. 用 STM32 驱动 HX711 做电子秤程序先解决三个绕不开的问题想用 STM32 做一台能稳定显示的电子秤核心工作其实不是“读传感器”而是把 HX711 输出的 24 位原始 ADC 码值处理成一个不抖、不漂、有意义的重量数字。HX711 是一颗专为称重应用设计的 24 位高精度 ADC 芯片它内部集成了低噪声放大器、稳压和时钟电路与应变式称重传感器配合能从微伏级的桥路信号里读出稳定的数字量。但芯片只是“给了你一个数字”能不能变成一台好用的秤取决于你如何处理时序、供电、滤波和标定这几块。这篇文章面向的是手里已经有 STM32 开发板、想快速把电子秤程序跑通并做到可用的开发者。我会从 HX711 的信号链和接线讲起再给出一套完整的最小驱动代码然后重点说明标定公式怎么推导、滤波参数怎么设置、以及我在这类项目里反复踩过的几个坑——尤其是那些让读数漂移、标定失效、归零困难的硬件层面问题。整篇内容不依赖某个特定开发板的库所有代码都基于标准 GPIO 模拟时序移植到 F1、F4 或 G0 系列都只需要改引脚定义。2. HX711 的信号链从应变片到 ADC 码值中间发生了什么2.1 半桥与全桥称重传感器的 mV 级输出为什么必须经过 HX711常见的称重传感器内部是电阻应变片组成的惠斯通电桥。供电电压通常为 5V 或 10V满量程输出只有 2mV/V 左右——也就是说 5V 供电下满量程也才 10mV。这么微弱的差分信号直接进 STM32 的 ADC几乎什么都读不到。HX711 的角色就是把传感器的差分电压先放大再数字化。HX711 的 A 通道内置了可编程增益典型值为 128 或 64。选择增益 128 时满量程差分输入约 ±20mV配合 24 位 ADC理论分辨率可以做到微伏级。但要注意HX711 的有效位数通常在 19 到 21 位之间取决于供电和 PCB 布局。这意味着你看到的“24 位”只代表数字输出格式不代表每克都能可靠分辨。实际项目中1kg 量程做到 0.1g 分辨率是合理的追求但做到 0.01g 就需要在电源和屏蔽上花很多功夫。2.2 接线方案四线制全桥传感器的标准接法与两个重点典型的四线制传感器有红、黑、绿、白四根线分别对应 VCC、GND、SIG、SIG-。HX711 的 E、E- 接传感器的供电线A、A- 接信号线。这里有一个新手常犯的错误把 E 与 AVDD 混淆。E 是传感器供电输出引脚它输出的电压由 HX711 内部的稳压电路决定通常是 2.6V 到 5V取决于 AVDD 和稳压配置不是直接接外部电源。我一般会把 RATE 引脚接 GND让数据输出速率固定在 10Hz。这个速率对电子秤足够快而且抗干扰能力比 80Hz 好很多。如果做动态称重或高速分选再把 RATE 拉高也不迟。接线时还要注意 DOUT 和 PD_SCK 的上拉或下拉——HX711 的 DOUT 是推挽输出一般不需要外加电阻PD_SCK 输入需要防抖但 STM32 的 GPIO 输出足够驱动它也不建议在 SCK 上加过长走线。2.3 供电与时钟决定称重稳定性的三个硬件细节第一HX711 的数字电源 VDD 和模拟电源 AVDD 需要分开退耦。我通常会在 AVDD 引脚旁边放一个 10μF 钽电容加 0.1μF 陶瓷电容的组合VDD 旁放 1μF 就够。不要为了省元件把两个电容并在一起模拟部分的纹波会直接影响读数。第二PD_SCK 的时钟频率。HX711 数据手册允许最高约几兆赫兹的时钟但实际使用时我建议把脉冲宽度控制在 1μs 左右即频率约 500kHz。更快的时钟虽然对读取没影响但会在 DOUT 数据线上引入额外的开关噪声。而且 STM32 的 GPIO 翻转速度不是问题真正的问题是延时精度——用HAL_Delay(1)这种毫秒级延时去做微秒级时序完全不靠谱必须用 DWT 或 SysTick 做微秒延时或者直接用几个 NOP 指令凑出脉冲宽度。第三晶振。HX711 内部自带时钟不需要外部晶振但它的时钟精度直接决定数据输出速率是否准确。不是所有 HX711 模块上的电容都一致如果你发现同一块板子在两台设备上读数速率明显不同多半是模块上的振荡电容批次差异不影响称重结果但会影响滤波时间常数的实际表现。3. 最小驱动代码用 GPIO 模拟时序读取 HX711 的完整流程3.1 初始化与复位上电后必须先等 DOUT 拉低HX711 上电后需要一段时间完成内部自校准这段时间通常为几十毫秒。如果上电后立刻读 DOUT它可能还维持在高电平。稳妥的做法是初始化 GPIO 后先给 PD_SCK 输出 30 个以上的脉冲让芯片进入一个已知的复位状态再等待 DOUT 变低。下面是标准初始化函数适配任意 STM32 系列的 HAL 库#define HX711_SCK_PORT GPIOB #define HX711_SCK_PIN GPIO_PIN_0 #define HX711_DOUT_PORT GPIOB #define HX711_DOUT_PIN GPIO_PIN_1 void HX711_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio.Pin HX711_SCK_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(HX711_SCK_PORT, gpio); gpio.Pin HX711_DOUT_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(HX711_DOUT_PORT, gpio); /* 复位至少30个脉冲 */ HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET); for (int i 0; i 30; i) { HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET); delay_us(1); HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET); delay_us(1); } HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET); /* 等待第一次转换完成DOUT 会被拉低 */ uint32_t timeout 100000; while (HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN) GPIO_PIN_SET timeout-- 0); }这里delay_us是微秒延时函数可以用 DWT 实现。注意 PD_SCK 在空闲时保持低电平这与很多其他 SPI 外设相反。输出引脚速度设为 HIGH是为了保证上升沿足够陡避免在信号线上形成缓慢的边沿导致误判。3.2 读 24 位数据完整的一次转换时序读取 HX711 数据的关键是等待 DOUT 变低表示转换完成然后产生 24 个时钟脉冲在脉冲的上升沿前后采样 DOUT 电平最后再补一个第 25 个脉冲为下一次转换做准备。数据是二进制补码格式最高位是符号位所以需要手动做符号扩展。int32_t HX711_Read(void) { uint32_t value 0; uint32_t timeout 100000; /* 等待 DOUT 拉低表示转换完成。超时保护必须有 */ while (HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN) GPIO_PIN_SET timeout-- 0); if (timeout 0) { return 0x80000000; /* 返回一个异常值调用方判断 */ } /* 读取24位数据MSB先行 */ for (int i 0; i 24; i) { HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET); delay_us(1); value (value 1) | (HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN) GPIO_PIN_SET); HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET); delay_us(1); } /* 第25个脉冲为下一次转换启动做准备 */ HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET); delay_us(1); /* 符号扩展如果第24位是1说明是负数 */ if (value 0x800000) { value | 0xFF000000; } return (int32_t)value; }延时参数为什么是 1μs因为 PD_SCK 高电平持续时间至少要满足 HX711 的内部采样要求数据手册里 t4 之类的参数实际用 1μs 已经留了很大余量。延时太短比如只有几十纳秒在极端情况下可能采到不确定的数据延时太长除了浪费 CPU 时间还可能让读取周期意外拉长到和 HX711 的转换周期重叠导致下一次等待时间变长。3.3 连续读数的周期控制为什么不能无脑循环HX711 在 10Hz 数据速率下每 100ms 完成一次转换。如果你的主循环每 10ms 就调用一次HX711_Read()你会发现函数内部等待 DOUT 拉低的时间很长。因为 DOUT 在半数的周期内都是高电平你在高电平期间调用读函数程序就会阻塞在 while 循环里浪费大量 CPU 时间。更严重的问题是如果你读得比转换快可能会连续读到同一组数据两次或者漏掉中间一次转换。因为第 25 个脉冲已经触发了下一次转换但如果下一次转换还没完成DOUT 还是低电平——你调用读取函数时它会立刻通过等待然后读到上一次转换的结果。我一般会把调用频率固定为 10Hz或者略低于 10Hz比如每 110ms 读一次。这样能保证每一次HX711_Read()拿到的都是一次新转换的结果。如果读太频繁也没有坏结果但数据里会混入重复值影响后续滤波效果。void main_loop(void) { int32_t raw; while (1) { delay_ms(110); /* 略大于10Hz周期保证每次读到的都是新数据 */ raw HX711_Read(); if (raw 0x80000000) { /* 读超时处理异常 */ continue; } process_weight(raw); } }这里的 110ms 是我调试时常用的值。如果你想用 80Hz 数据速率就需要把调用间隔改成 12.5ms 左右但要注意80Hz 模式下 HX711 的噪声明显增大通常需要更强的滤波才能得到和平稳的 10Hz 模式接近的读数。4. 从原始码值到克数滤波、去皮与两点标定4.1 去毛刺为什么中值滤波比滑动平均先做直接从 HX711 读回的序列里偶尔会出现一个明显偏离的毛刺值——可能是电磁干扰、电源瞬间跌落或者时钟线上耦合的噪声。如果先做滑动平均这个毛刺会被“摊”到后面好几个点里表现为一个持续几百毫秒的小凸起。更稳妥的顺序是先做一个 3 到 5 点的中值滤波把毛刺直接剔除再对中值滤波后的结果做滑动平均抑制高频抖动。以下是我在实际项目里用的二阶段滤波函数#define FILTER_MEDIAN_N 5 #define FILTER_AVG_N 8 static int32_t median_buf[FILTER_MEDIAN_N]; static int32_t avg_buf[FILTER_AVG_N]; static uint8_t median_idx, avg_idx; int32_t HX711_Filter(int32_t raw) { int32_t tmp[FILTER_MEDIAN_N]; int32_t sum 0; /* 第一层中值滤波剔除毛刺 */ median_buf[median_idx] raw; median_idx (median_idx 1) % FILTER_MEDIAN_N; for (int i 0; i FILTER_MEDIAN_N; i) { tmp[i] median_buf[i]; } /* 简单的冒泡排序N只有5效率无所谓 */ for (int i 0; i FILTER_MEDIAN_N - 1; i) { for (int j i 1; j FILTER_MEDIAN_N; j) { if (tmp[i] tmp[j]) { int32_t t tmp[i]; tmp[i] tmp[j]; tmp[j] t; } } } int32_t median_val tmp[FILTER_MEDIAN_N / 2]; /* 第二层滑动平均进一步平滑 */ avg_buf[avg_idx] median_val; avg_idx (avg_idx 1) % FILTER_AVG_N; for (int i 0; i FILTER_AVG_N; i) { sum avg_buf[i]; } return sum / FILTER_AVG_N; }中值窗口取 5、平均窗口取 8这两个数字是经验值。5 点中值能容忍连续两个毛刺——3 点中值遇到双毛刺时会直接穿透。8 点滑动平均在 10Hz 采样下产生约 0.8 秒的响应延迟对静态称重完全够用对动态称重可能偏慢。如果你做的是快速分选把平均窗口缩短到 4中值保留 5效果会好很多。4.2 去皮重上电时取零点运行中动态清零去皮有两个层次一个是上电时的零点采集——此时秤盘上没有任何东西读到的值是传感器的零点偏移需要在标定前扣除另一个是运行中的“去皮”操作——把当前重量设为零点让后续新增的重量直接从零开始计。我的做法是上电时连续读 20 次滤波后的值取平均作为tare_offset运行时如果需要去皮就把当前滤波值重新赋给tare_offset。注意去皮操作要触发在滤波收敛之后如果你在主循环里每 100ms 调用一次滤波那应该在滤波输出稳定后再记录零点。static int32_t tare_offset; void Tare_On_PowerUp(void) { int64_t sum 0; for (int i 0; i 20; i) { sum HX711_Filter(HX711_Read()); delay_ms(110); } tare_offset (int32_t)(sum / 20); } void Tare_Reset(void) { tare_offset HX711_Filter(HX711_Read()); }去皮的时机有个隐蔽问题如果秤盘上还有东西时按了去皮那这个重量会被当成零点。这在产品设计上不是 bug但需要上层逻辑去判断当前是否“允许去皮”。更常见的坑是去皮后传感器还在蠕变读数继续缓慢上升导致去皮基准失效。这种情况一般不是代码问题而是传感器本身的蠕变特性需要在静置数秒后再操作。4.3 标定的核心公式从两组已知重量推导 scale 和 offset这是整篇文章里最关键的一个公式。设去皮后读到的原始滤波值为raw实际重量为w假设二者呈线性关系w (raw - offset) / scale其中offset是零负载时的原始值scale是每克对应的 ADC 码值。两点标定法就是找两个已知重量测出两个 raw然后解出两个参数。void Calibrate(int32_t raw_zero, int32_t raw_known, int16_t weight_known_gram) { /* 第一个点空秤去掉皮重后 offset 直接取 raw_zero */ /* 实际使用时 offset 应等于 tare_offset所以下面的计算只推 scale */ int32_t diff_raw raw_known - raw_zero; int32_t scale diff_raw / weight_known_gram; /* 保存 scale 到 Flash 或 EEPROM */ /* 注意这里用的 weight_known_gram 必须是标准的砝码重量而不是显示值 */ }细想一下这个公式里的scale单位是“ADC 码值/克”。如果你用 500g 砝码标定读到的 raw 差值是 500000那scale就是 1000。之后任意重量就是(raw - tare_offset) / 1000克。标定过程中有几个容易出问题的地方。第一必须等读数稳定后再记录 raw我通常在放置砝码后等待 2 秒以上再采样。第二砝码要放在传感器的中心位置偏载会产生力矩误差这在硬件上避免不了只能靠标定时保持一致。第三标定完成后要立刻验证放一个中间重量的砝码看显示偏差是否在允许范围内。如果偏差大于 0.5% 满量程说明传感器线性度不好或者装配有松动不是调参数能解决的。完整的重量换算函数static int32_t scale_cali; /* 码值/克标定得到 */ static int32_t tare_offset; /* 零点偏移去皮得到 */ float Get_Weight_Gram(void) { int32_t raw HX711_Filter(HX711_Read()); float weight (float)(raw - tare_offset) / (float)scale_cali; /* 负值截断为0避免空秤时出现负重量 */ if (weight 0.0f) { weight 0.0f; } return weight; }截断为 0 这个操作要谨慎。如果传感器温漂导致零点偏高负值截断后你永远看不到“零点变负”会掩盖硬件问题。我建议把负值保留在上位机或屏幕上显示为 0但在串口调试时输出真实负值方便排查。5. STM32 HX711 电子秤避坑5 个实操中反复出现的翻车场景5.1 读数漂移放十分钟还在跳现象空秤情况下滤波后的重量从 0.0g 慢慢漂到 0.3g然后跳回 0.0g周期大概几十秒到几分钟。原因多数情况是电源纹波或基准漂移其次是传感器自发热。HX711 内部虽有稳压但它对 AVDD 上的纹波抑制有限纹波会直接叠加到传感器的激励电压上表现为读数的周期性漂移。解决检查模拟电源给 HX711 的 AVDD 增加一级 LC 滤波或者改用更干净的电源。自发热问题则是传感器长时间工作后内部温度升高导致的热漂移解决思路是上电后预热 5 分钟再让用户标定或者代码里做温度补偿后者复杂度高一般不用。5.2 标定后前几次测量不准越用越准现象刚标定完放 100g 砝码显示 95g等几分钟再放变成 99.5g再过会儿就准了。原因标定时传感器还未完全进入热平衡。标定过程越快传感器内部的应变片温度变化越大弹性模量随之变化导致初始阶段灵敏度偏低。解决标定前先让设备预热至少 10 分钟。如果时间不允许可以做一个两次标定流程——先快速标定一次等 5 分钟后再重标一次第二次的标定值往往会稳定得多。5.3 大力压过后读数归不了零现象用力压秤盘超过额定负载后空秤显示 1.5g怎么去皮都回不到 0.0g。原因应变式传感器的弹性体发生了塑性变形零点和灵敏度都永久偏了。这不是 HX711 的问题也不是程序能补偿的。解决检查传感器是否超载更换传感器。程序层面只能做“超载保护”——当 raw 值超过量程对应值的 120% 时在屏幕上提示超载并停止进一步累计。5.4 同一个传感器换一块 STM32标定失效现象在开发板上标定好的参数烧录到量产板上后同样的传感器读数差了几克。原因HX711 的参考电压来自 AVDD不同板子的 AVDD 电压不同可能因为稳压芯片批次差异、PCB 走线阻抗差异导致同样重量的码值变化。虽然 HX711 内部 Vref 与传感器激励相关但实际上每个模块的供电环境不同标定参数不能跨板通用。解决每块板子都必须独立标定。如果批量生产可以做一个自动标定流程——用上位机发送标定指令设备自动采集两个已知重量并写入 Flash。5.5 读出的数据偶尔出现一个明显坏点现象正常读数在 100000 到 100100 之间波动偶尔跳出一个 95000 或 105000 的孤立值。原因大概率是 DOUT 采样时刻刚好在数据线翻转过程中或者 PD_SCK 的边沿触发了 HX711 内部的时序竞争。另一个可能是时钟线上有毛刺导致多读或少读了一个 bit。解决先把 1μs 延时适当加长到 2μs看是否改善。如果仍然有坏点检查 DOUT 线是否靠近任何高频开关信号比如 PWM 输出线。把 DOUT 和 PD_SCK 两根线用短跳线直接连到 MCU远离排线上的其他信号能解决大部分问题。另外在HX711_Read()里加超时保护是必须的否则 DOUT 卡在高电平时程序会死循环。6. 把称重程序往前再推一步三点标定、动态滤波与上电自检三点标定能明显改善非线性。用 1/3 量程和 2/3 量程两个砝码加上零点算出两段不同的 scale 值float Get_Weight_Improved(int32_t raw) { if (raw mid_raw) { return (float)(raw - tare_offset) / scale_low; } else { return (float)(raw - mid_raw) / scale_high weight_mid; } }scale_low和scale_high分别是零点到 1/3 点、1/3 点到 2/3 点之间的斜率。这么做以后非线性误差通常能从 0.5% 压到 0.2% 以内代价只是标定时多放一次砝码。动态滤波方面我习惯做成“静止即平滑运动即跟手”的切换当连续 5 次滤波值的最大差小于某个阈值时判定为静止此时把滑动平均窗口拉长到 16一旦差值超过阈值立即把窗口缩短到 4让读数快速跟踪重量变化。这样可以兼顾稳定性与动态响应适合需要同时做慢速称重和快速加料的场景。上电自检是产品化不可少的一步把已标定的秤断电后放置确认空秤读回零再放置一个已知重量的砝码看偏差是否在容差内。这是低成本但很有效的质量保障手段。最后分享一个我早期的教训总想靠软件滤波把硬件问题掩盖掉结果读数是稳了但响应也迟钝了用户放东西 2 秒才显示最终重量体验很差。后来才明白HX711 电源干净、布局合理、接地可靠比任何滤波算法都吃得开。先花时间把硬件折腾到位再决定滤波强度这才是正确的顺序。希望这篇笔记能帮你在 STM32 电子秤项目上少走几条弯路。本文还有配套的精品资源点击获取