文章目录
-
- 摘要
- [一、前言:几毫伏的信号,卡住了 12 位 ADC](#一、前言:几毫伏的信号,卡住了 12 位 ADC)
- [二、核心原理:应变桥、ΣΔ 调制与"脉冲数即配置"](#二、核心原理:应变桥、ΣΔ 调制与"脉冲数即配置")
-
- [2.1 惠斯通电桥:为什么满量程只有 3.3mV](#2.1 惠斯通电桥:为什么满量程只有 3.3mV)
- [2.2 方案对比:为什么是 HX711 而不是别的](#2.2 方案对比:为什么是 HX711 而不是别的)
- [2.3 数据就绪与增益脉冲:HX711 的全部"寄存器"就是时序](#2.3 数据就绪与增益脉冲:HX711 的全部"寄存器"就是时序)
- [2.4 配置级决策:通道 A ×128 与 RATE 接地](#2.4 配置级决策:通道 A ×128 与 RATE 接地)
- 三、硬件选型与接线
-
- [3.1 选型清单](#3.1 选型清单)
- [3.2 接线表与注意事项](#3.2 接线表与注意事项)
- 四、软件设计
-
- [4.1 CubeMX 配置与寄存器对照](#4.1 CubeMX 配置与寄存器对照)
- [4.2 容易遗漏的步骤:上电 400ms 稳定期与 SCK 初始电平](#4.2 容易遗漏的步骤:上电 400ms 稳定期与 SCK 初始电平)
- [4.3 驱动实现:EXTI 就绪中断 + 临界区时序读取](#4.3 驱动实现:EXTI 就绪中断 + 临界区时序读取)
- [4.4 数据处理:中值滤波、滑动平均与两点标定](#4.4 数据处理:中值滤波、滑动平均与两点标定)
- [4.5 失败路径复盘:一次"增益切换不生效"的排查](#4.5 失败路径复盘:一次"增益切换不生效"的排查)
- 五、测试验证
-
- [5.1 标定与线性度实测](#5.1 标定与线性度实测)
- [5.2 噪声与有效分辨率:理论 vs 实测](#5.2 噪声与有效分辨率:理论 vs 实测)
- [5.3 滤波窗口参数扫描](#5.3 滤波窗口参数扫描)
- [5.4 温漂与蠕变:手册只管芯片,不管传感器](#5.4 温漂与蠕变:手册只管芯片,不管传感器)
- [六、故障排查:6 类高频问题的完整排查链](#六、故障排查:6 类高频问题的完整排查链)
- 七、总结
- 版本备注
- 参考资料
摘要
称重是嵌入式最常见的测量需求之一,但应变桥传感器满量程输出只有几毫伏,F103 内置 12 位 ADC 直采等于自废武功。本文基于 STM32F103C8T6 + HX711 专用 ΣΔ 转换芯片 + 5kg 悬臂梁称重传感器,讲透惠斯通电桥的毫伏级信号链,逐位拆解 24 位补码数据帧与增益脉冲协议,给出 EXTI 就绪中断 + 临界区保护的完整驱动及两点标定实现。实测:10SPS 原始噪声 σ=0.15g(理论 0.13g,偏差 15%),滤波窗口 16 时稳定分辨率 ±0.1g,5kg 量程内线性 R²=0.99997、最大误差 1.6g,帧间隔实测 100.4ms 与手册标称仅差 0.4%。文末附 6 类故障的完整排查链。
一、前言:几毫伏的信号,卡住了 12 位 ADC
之前写过 NTC 测温和热电偶测温,都是"电压 → 温度"的换算链路。最近帮朋友做一个烘焙咖啡豆的失重记录仪,需求反过来:测"重量 → 变化趋势"。本来以为拿 F103 内置 ADC 分个压就能凑合,真上手算了笔账才发现完全不是那么回事。
常见的悬臂梁称重传感器,本质上是一个惠斯通电桥,灵敏度一般标 1.0mV/V------意思是供电 1V、挂满量程砝码时,输出才 1mV。我这块 5kg 传感器用 3.3V 供电,满量程输出 3.3mV,摊到每 1g 只有 0.66μV。F103 的 12 位 ADC,1 个 LSB 是 0.8mV,也就是说 1g 的信号比 1 个 LSB 还小一千多倍,直采连"克"都分不出来,更别提亚克级。
所以称重系统的核心不在 MCU,在前端:一颗带 PGA 的专用 ΣΔ ADC。这个领域 HX711 几乎是默认答案------十几块钱的模块、两根线就能驱动的脉冲接口、24 位分辨率,电子秤、配料机、料位计里到处都是它。网上大量例程能"读出数",但能把时序为什么这么敲、增益切换有什么坑、标定怎么做才准讲清楚的并不多。本文就把这条链路完整走一遍。
本文目标:读完你能独立完成 HX711 的选型接线、寄存器级配置和工程级驱动编写,并理解每一处配置"为什么这样定"。前置条件是会用 CubeMX 建工程、会用 HAL 库操作 GPIO 和外部中断;硬件上需要一块 F103 最小系统板、一个 HX711 模块、一个 5kg 悬臂梁传感器和几个标准砝码。本文完整工程代码(CubeMX 工程 + 驱动 + 标定脚本)可在 CSDN 下载频道 获取(VIP 免费)。
二、核心原理:应变桥、ΣΔ 调制与"脉冲数即配置"
2.1 惠斯通电桥:为什么满量程只有 3.3mV
悬臂梁传感器的弹性体上贴了 4 片应变片,接成惠斯通电桥。受力时两臂阻值增、两臂减,电桥失衡输出差分电压。对全桥结构,输出近似为:
text
Vo = E × S × (F / FS)
其中 E 是桥路供电(激励电压),S 是灵敏度(本文传感器 1.0mV/V),F 是载荷,FS 是满量程 5kg。代入 E=3.3V:满载时 Vo = 3.3mV,每 1g 对应 0.66μV。这个信号比任何 MCU 板级噪声都小,必须先放大再转换。
HX711 的思路是"PGA + ΣΔ ADC + 比例式测量"三合一:
- PGA 可编程增益:通道 A 支持 ×128 / ×64,通道 B 固定 ×32。增益 128 时 3.3mV 被放大到 422mV,落在芯片 ±20mV 差分输入线性范围(增益 128 档)之内且余量充足;
- 比例式设计:ADC 基准直接取自模拟电源 AVDD,也就是桥路激励。激励电压波动时,信号和基准同比例缩放,输出码值不变------这是它能用廉价电源还保持精度的关键;
- 24 位 ΣΔ 转换 :换算下来 1 个 LSB 对应的重量 = 3.3V / 128 / 2²³ × (5000g / 3.3mV) ≈ 4.66mg。理论分辨率远超需求,实际下限由噪声决定,第五节用数据说话。
整体信号链如下:
#mermaid-svg-Irwkd5Ljds7AD98n{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Irwkd5Ljds7AD98n .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Irwkd5Ljds7AD98n .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Irwkd5Ljds7AD98n .error-icon{fill:#552222;}#mermaid-svg-Irwkd5Ljds7AD98n .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Irwkd5Ljds7AD98n .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Irwkd5Ljds7AD98n .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Irwkd5Ljds7AD98n .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Irwkd5Ljds7AD98n .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Irwkd5Ljds7AD98n .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Irwkd5Ljds7AD98n .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Irwkd5Ljds7AD98n .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Irwkd5Ljds7AD98n .marker.cross{stroke:#333333;}#mermaid-svg-Irwkd5Ljds7AD98n svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Irwkd5Ljds7AD98n p{margin:0;}#mermaid-svg-Irwkd5Ljds7AD98n .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Irwkd5Ljds7AD98n .cluster-label text{fill:#333;}#mermaid-svg-Irwkd5Ljds7AD98n .cluster-label span{color:#333;}#mermaid-svg-Irwkd5Ljds7AD98n .cluster-label span p{background-color:transparent;}#mermaid-svg-Irwkd5Ljds7AD98n .label text,#mermaid-svg-Irwkd5Ljds7AD98n span{fill:#333;color:#333;}#mermaid-svg-Irwkd5Ljds7AD98n .node rect,#mermaid-svg-Irwkd5Ljds7AD98n .node circle,#mermaid-svg-Irwkd5Ljds7AD98n .node ellipse,#mermaid-svg-Irwkd5Ljds7AD98n .node polygon,#mermaid-svg-Irwkd5Ljds7AD98n .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Irwkd5Ljds7AD98n .rough-node .label text,#mermaid-svg-Irwkd5Ljds7AD98n .node .label text,#mermaid-svg-Irwkd5Ljds7AD98n .image-shape .label,#mermaid-svg-Irwkd5Ljds7AD98n .icon-shape .label{text-anchor:middle;}#mermaid-svg-Irwkd5Ljds7AD98n .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Irwkd5Ljds7AD98n .rough-node .label,#mermaid-svg-Irwkd5Ljds7AD98n .node .label,#mermaid-svg-Irwkd5Ljds7AD98n .image-shape .label,#mermaid-svg-Irwkd5Ljds7AD98n .icon-shape .label{text-align:center;}#mermaid-svg-Irwkd5Ljds7AD98n .node.clickable{cursor:pointer;}#mermaid-svg-Irwkd5Ljds7AD98n .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Irwkd5Ljds7AD98n .arrowheadPath{fill:#333333;}#mermaid-svg-Irwkd5Ljds7AD98n .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Irwkd5Ljds7AD98n .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Irwkd5Ljds7AD98n .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Irwkd5Ljds7AD98n .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Irwkd5Ljds7AD98n .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Irwkd5Ljds7AD98n .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Irwkd5Ljds7AD98n .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Irwkd5Ljds7AD98n .cluster text{fill:#333;}#mermaid-svg-Irwkd5Ljds7AD98n .cluster span{color:#333;}#mermaid-svg-Irwkd5Ljds7AD98n div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Irwkd5Ljds7AD98n .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Irwkd5Ljds7AD98n rect.text{fill:none;stroke-width:0;}#mermaid-svg-Irwkd5Ljds7AD98n .icon-shape,#mermaid-svg-Irwkd5Ljds7AD98n .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Irwkd5Ljds7AD98n .icon-shape p,#mermaid-svg-Irwkd5Ljds7AD98n .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Irwkd5Ljds7AD98n .icon-shape .label rect,#mermaid-svg-Irwkd5Ljds7AD98n .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Irwkd5Ljds7AD98n .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Irwkd5Ljds7AD98n .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Irwkd5Ljds7AD98n :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 输出
MCU层
信号转换层
机械与传感层
满量程约 3.3mV
PD_SCK 时钟 + 增益脉冲
24bit 补码帧
DOUT 下降沿 = 就绪
5kg 悬臂梁称重传感器
应变桥 灵敏度 1.0mV/V
HXA711 模块
PGA ×128 + 24bit ΣΔ ADC
10SPS / 脉冲接口
EXTI1 下降沿
DOUT 就绪检测
GPIO 位敲时序
PB0=SCK PB1=DOUT
中值滤波 + 滑动平均
两点标定 / 去皮
USART1 上位机
原始码值 / 重量
2.2 方案对比:为什么是 HX711 而不是别的
做选型时我把能想到的路线都过了一遍:
| 方案 | 有效分辨率 | PGA | 成本 | 接口 | 适用场景 |
|---|---|---|---|---|---|
| F103 内置 ADC 直采 | 12 位,1g 信号 ≈ 0.0008 LSB | 无 | 0 | --- | 直接排除 |
| 内置 ADC + 外部仪表放大器 | 取决于放大器与布线 | 自搭 | 中 | ADC 采样 | 需要自定义量程时 |
| ADS1232(TI) | 24 位,集成度更高 | ×1~×128 | 高(约 10 倍) | SPI | 工业仪表 |
| HX711 | 24 位(噪声受限约 19~21 位) | ×32~×128 | 极低 | 自定义脉冲 | 民用/轻工业称重 |
选 HX711 的理由很直接:比例式设计对电源不挑剔、脉冲接口不占用硬件外设、价格只有专业方案零头。代价也要认:内部 RC 振荡器精度一般、无寄存器可查(一切状态都靠时序行为推断)、通道 B 增益固定 32 不能调。对"够准、够便宜、够简单"的需求,它就是最优解;要做 0.01% 级工业衡器,再考虑 ADS1232 这类方案。信号链电路层面的更多设计细节,可以对照这篇《STM32驱动高精度称重模块:HX711 24位ADC的电路设计与代码实战》阅读。
2.3 数据就绪与增益脉冲:HX711 的全部"寄存器"就是时序
HX711 没有任何可读写的寄存器,它的"配置"全部藏在读时序里,这也是它最容易踩坑的地方。工作循环是:
- 内部 ΣΔ 转换进行中,DOUT 输出高电平;
- 转换完成,DOUT 拉低------这个下降沿就是"数据就绪"信号;
- MCU 在 PD_SCK 上敲出 24 个时钟脉冲,HX711 在每个脉冲后把数据位(MSB first)放到 DOUT 上;
- 第 25~27 个脉冲是"增益脉冲":脉冲个数决定下一次转换用哪个通道和增益------25 个 = 通道 A ×128,26 个 = 通道 B ×32,27 个 = 通道 A ×64;
- DOUT 重新拉高,进入下一轮转换(10SPS 档约 100ms 后再次就绪)。
EXTI1 中断 STM32 主循环 HX711 EXTI1 中断 STM32 主循环 HX711 #mermaid-svg-Sk1rsopH1VPzNZt4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sk1rsopH1VPzNZt4 .error-icon{fill:#552222;}#mermaid-svg-Sk1rsopH1VPzNZt4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sk1rsopH1VPzNZt4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sk1rsopH1VPzNZt4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sk1rsopH1VPzNZt4 .marker.cross{stroke:#333333;}#mermaid-svg-Sk1rsopH1VPzNZt4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sk1rsopH1VPzNZt4 p{margin:0;}#mermaid-svg-Sk1rsopH1VPzNZt4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sk1rsopH1VPzNZt4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Sk1rsopH1VPzNZt4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Sk1rsopH1VPzNZt4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Sk1rsopH1VPzNZt4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Sk1rsopH1VPzNZt4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Sk1rsopH1VPzNZt4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Sk1rsopH1VPzNZt4 .sequenceNumber{fill:white;}#mermaid-svg-Sk1rsopH1VPzNZt4 #sequencenumber{fill:#333;}#mermaid-svg-Sk1rsopH1VPzNZt4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Sk1rsopH1VPzNZt4 .messageText{fill:#333;stroke:none;}#mermaid-svg-Sk1rsopH1VPzNZt4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sk1rsopH1VPzNZt4 .labelText,#mermaid-svg-Sk1rsopH1VPzNZt4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Sk1rsopH1VPzNZt4 .loopText,#mermaid-svg-Sk1rsopH1VPzNZt4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Sk1rsopH1VPzNZt4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Sk1rsopH1VPzNZt4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Sk1rsopH1VPzNZt4 .noteText,#mermaid-svg-Sk1rsopH1VPzNZt4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Sk1rsopH1VPzNZt4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sk1rsopH1VPzNZt4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sk1rsopH1VPzNZt4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sk1rsopH1VPzNZt4 .actorPopupMenu{position:absolute;}#mermaid-svg-Sk1rsopH1VPzNZt4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Sk1rsopH1VPzNZt4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sk1rsopH1VPzNZt4 .actor-man circle,#mermaid-svg-Sk1rsopH1VPzNZt4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Sk1rsopH1VPzNZt4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 转换进行中,DOUT = 高 loop 24 次 补码符号扩展 清 EXTI 挂起位,重新开中断 下一帧转换开始 10SPS:约 100ms 后再次就绪 DOUT 下降沿(数据就绪) 回调里置 s_ready 标志(1 行) 检查标志,关 EXTI 中断 PD_SCK 高 2μs → 低 2μs DOUT 移出 1 位(MSB first) 第 25 个脉冲(通道A ×128) 中值 + 滑动平均 → 两点标定
两条硬约束必须刻在脑子里:PD_SCK 高电平持续超过 60μs,芯片进入掉电态 (电流降到 1μA 级,退出后复位为通道 A ×128);数据在 PD_SCK 下降沿之后更新,采样窗口是 SCK 低电平期间。这两个约束直接决定了驱动的写法,4.3 节的临界区保护就是为第一条服务的。
2.4 配置级决策:通道 A ×128 与 RATE 接地
通道选择上没有悬念:桥式传感器输出是毫伏级正负小信号,通道 A 的 ×128 增益能把满量程 3.3mV 放大到 422mV,充分利用 24 位动态范围;通道 B ×32 是给热电偶这类更大幅度信号准备的,用在应变桥上会浪费近三分之二的码值空间。RATE 引脚(速率选择)我接了 GND 定在 10SPS 而不是 80SPS,理由有两条:一是失重记录仪是准静态测量,10Hz 采样绰绰有余;二是手册明确 10SPS 档有效分辨率更高,第五节的实测数据也验证了这一点(噪声低约 2 倍)。如果你做的是动态配料,再切 80SPS 不迟。
三、硬件选型与接线
3.1 选型清单
| 器件 | 型号/规格 | 关键参数 | 参考价 |
|---|---|---|---|
| 主控 | STM32F103C8T6 最小系统板 | 72MHz,足够 | ~10 元 |
| ADC 模块 | HX711 模块(紫板/红板均可) | 24 位,PGA ×128 | ~5 元 |
| 传感器 | 5kg 悬臂梁称重传感器 | 灵敏度 1.0±0.1mV/V,全桥 4 线 | ~8 元 |
| 秤体 | 亚克力支架 + 秤盘 | 悬臂梁需要正确的受力支点 | ~10 元 |
| 砝码 | 100g/500g/1kg×2(标定与校验用) | M1 级更好,M2 够用 | ~30 元 |
| 调试 | USB 转 TTL + 逻辑分析仪 | 逻辑分析仪强烈建议备一把 | ~30 元 |
特别提醒两件事。第一,悬臂梁传感器必须配合正确的机械结构:一端固定、称重点悬空,受力方向垂直于梁面,直接用导线吊着测的数值没有意义。第二,砝码是标定的"基准源",淘宝上误差 ±5% 的廉价砝码会把标定一起带偏,宁可少买几个买好点。
3.2 接线表与注意事项
| 传感器线色* | HX711 端子 | HX711 引脚 | STM32 引脚 |
|---|---|---|---|
| 红(E+) | E+ | --- | --- |
| 黑(E-) | E- | --- | --- |
| 绿(A+) | A+ | --- | --- |
| 白(A-) | A- | --- | --- |
| --- | --- | VCC | 3.3V |
| --- | --- | GND | GND |
| --- | --- | SCK(PD_SCK) | PB0 |
| --- | --- | DT(DOUT) | PB1 |
| --- | --- | RATE | 接 GND(10SPS) |
*线色以模块丝印为准,不同厂家红黑绿白顺序一致但个别用蓝代白,接错最多方向反,第五节有排查办法。
接线只有三条纪律:共地 (模块与 MCU 必须共地,否则时序全乱);传感器信号线尽量短 (最好 10cm 内,太长会引入可观测的噪声,5.2 节有对比);DOUT 走线远离继电器、电机驱动等噪声源。供电我直接用了开发板 3.3V,模块工作电流约 1.5mA,桥路激励电流约 1mA,都在 LDO 裕量内;如果系统里有大电流负载(电机、灯带),建议给 HX711 单独一路 LDO。
四、软件设计
4.1 CubeMX 配置与寄存器对照
HX711 的接口不是标准 SPI------它有"脉冲尾巴定增益"和"高电平 60μs 掉电"这两个私货,硬件外设做不了,只能 GPIO 位敲。这也是本节的配置级决策:不用 SPI 外设,PB0/PB1 纯 GPIO + EXTI。CubeMX 配置如下,每项都标了对应寄存器位,方便脱离 CubeMX 直接操作寄存器的读者:
| 配置项 | CubeMX 设置 | 对应寄存器位 | 说明 |
|---|---|---|---|
| RCC 时钟 | 默认开启 | RCC_APB2ENR:IOPBEN=1、AFIOEN=1 | GPIOB 与 AFIO 时钟必须先开 |
| PB0 | GPIO_Output / Push-Pull / Low / High | GPIOB_CRL:MODE0=0011(50MHz)、CNF0=00(推挽)、ODR0=0 | SCK,初始电平必须是低 |
| PB1 | GPIO_EXTI1 / Input with pull-up | GPIOB_CRL:MODE1=0000、CNF1=10(上拉输入)、ODR1=1 | DOUT,上拉做兜底 |
| EXTI1 | External Interrupt Mode with Falling edge trigger | AFIO_EXTICR1:EXTI13:0=0001(选 PB1);EXTI_FTSR:TR1=1;EXTI_IMR:MR1=1 | 下降沿 = 数据就绪 |
| NVIC | EXTI line1,抢占 1 / 子优先级 1 | NVIC_ISER0:SETENA 第 7 位(EXTI1_IRQn=7) | 优先级给中等即可 |
| SysTick | 默认 1ms | --- | HAL_GetTick 做超时 |
这里有个容易忽略的硬件映射约束:F1 系列的 EXTI 线号与 GPIO 引脚号是绑死的------EXTI1 只能来自各端口的 Pin1,通过 AFIO_EXTICR1 在 PA1/PB1/PC1 之间选一个,同一时刻只能映射一个端口。这也是我把 DOUT 放在 PB1 而不是随便找个脚的原因:如果 PA1 已经被别的中断源占了,就得整条换线号。关于 F1 的 EXTI 映射规则,参考手册 RM0008 的"外部中断/事件控制器"章节有完整表格。
4.2 容易遗漏的步骤:上电 400ms 稳定期与 SCK 初始电平
工具和例程都不会替你做的三件事,遗漏后的症状各不相同:
- 上电后等 400ms 再开始读 。手册规定芯片复位到输出有效数据最长需要 400ms。遗漏症状是开机头几秒读数漂移、甚至前几帧是乱码。这不是软件 bug,是模拟前端建立过程,CubeMX 和任何模板都不会生成这行
HAL_Delay(400)。 - SCK 初始电平必须为低。芯片上电瞬间如果 PD_SCK 悬空或为高,会直接进入掉电态,DOUT 永远不拉低,程序死等。CubeMX 里把 PB0 初始电平设为 Low,硬件上这个脚不要悬空。
- 上电首帧固定是通道 A ×128。复位后无论你"打算"用什么增益,第一帧都是 ×128 转换的。如果驱动里做了增益状态缓存,注意上电后不要拿缓存值当首帧的实际增益。
4.3 驱动实现:EXTI 就绪中断 + 临界区时序读取
驱动分三层:就绪检测(EXTI 中断置标志)、帧读取(临界区位敲时序)、数据处理(滤波 + 标定)。先看头文件:
c
#ifndef __HX711_H
#define __HX711_H
#include "stm32f1xx_hal.h"
#define HX711_SCK_PORT GPIOB
#define HX711_SCK_PIN GPIO_PIN_0
#define HX711_DOUT_PORT GPIOB
#define HX711_DOUT_PIN GPIO_PIN_1
#define HX711_EXTI_IRQn EXTI1_IRQn
#define HX711_SCK_HIGH() HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_SET)
#define HX711_SCK_LOW() HAL_GPIO_WritePin(HX711_SCK_PORT, HX711_SCK_PIN, GPIO_PIN_RESET)
#define HX711_DOUT_READ() HAL_GPIO_ReadPin(HX711_DOUT_PORT, HX711_DOUT_PIN)
/* 增益脉冲数:25 = 通道A ×128,26 = 通道B ×32,27 = 通道A ×64 */
#define HX711_GAIN_128_PULSES 25U
#define HX711_READY_TIMEOUT_MS 500U /* 上电首帧最长等 400ms,留余量 */
#define HX711_READ_ERR INT32_MIN /* 超时错误码,调用方必须检查 */
void HX711_Init(void);
HAL_StatusTypeDef HX711_WaitReady(uint32_t timeout_ms);
int32_t HX711_ReadRaw(void); /* 24 位原始值,已做符号扩展 */
void HX711_Tare(void); /* 去皮 */
float HX711_GetWeightG(int32_t raw);
#endif
就绪检测用 EXTI 下降沿中断,回调里只置标志,不做任何耗时操作------这是轮询和中断的方案级选择:轮询 while(DOUT) 会把主循环整个卡死,中断置标志的方式让 CPU 在转换的 100ms 空窗期可以干别的事:
c
static volatile uint8_t s_ready = 0; /* 中断与主循环共享,必须 volatile */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == HX711_DOUT_PIN) {
s_ready = 1; /* DOUT 下降沿 = 转换完成 */
}
}
HAL_StatusTypeDef HX711_WaitReady(uint32_t timeout_ms)
{
uint32_t tickstart = HAL_GetTick();
while (!s_ready) {
if ((HAL_GetTick() - tickstart) > timeout_ms) {
return HAL_TIMEOUT; /* 超时必须返回明确状态,不静默阻塞 */
}
}
return HAL_OK;
}
帧读取是整个驱动的核心。整段时序必须放在临界区里,原因在 2.3 节说过:SCK 高电平一旦被中断拉伸超过 60μs,芯片会误入掉电态。压缩到 2μs 高电平后,即使碰上中断也远够不到阈值,但"读 24 位"这个动作本身只有 500μs 左右,干脆整体关中断最干净:
c
/* DWT 微秒延时:72MHz 下精度约 0.1μs,比 HAL_Delay(毫秒级)高两个量级 */
static void hx711_delay_us(uint32_t us)
{
uint32_t start = DWT->CYCCNT;
uint32_t ticks = us * (SystemCoreClock / 1000000U);
while ((DWT->CYCCNT - start) < ticks) { /* spin */ }
}
static uint32_t hx711_read_frame(uint8_t gain_pulses)
{
uint32_t raw = 0;
uint8_t i;
/* 顺序要求:必须先关中断,再动 SCK------反过来会在开头留下高电平窗口 */
__disable_irq();
for (i = 0; i < 24; i++) {
HX711_SCK_HIGH(); /* 高电平仅约 2μs,远离 60μs 掉电阈值 */
hx711_delay_us(2);
HX711_SCK_LOW(); /* DOUT 在下降沿后更新,低电平期间采样 */
hx711_delay_us(2);
raw <<= 1;
if (HX711_DOUT_READ()) raw |= 1U;
}
/* 25~27 个增益脉冲:决定下一次转换的通道与增益(对本次读数无效) */
for (i = 24; i < gain_pulses; i++) {
HX711_SCK_HIGH();
hx711_delay_us(2);
HX711_SCK_LOW();
hx711_delay_us(2);
}
__enable_irq();
return raw;
}
int32_t HX711_ReadRaw(void)
{
uint32_t raw;
uint32_t tickstart = HAL_GetTick();
while (!s_ready) {
if ((HAL_GetTick() - tickstart) > HX711_READY_TIMEOUT_MS) {
return HX711_READ_ERR; /* 超时返回错误码,不阻塞死等 */
}
}
s_ready = 0;
HAL_NVIC_DisableIRQ(HX711_EXTI_IRQn); /* 读时序期间 DOUT 会反复翻转,先禁本中断 */
raw = hx711_read_frame(HX711_GAIN_128_PULSES);
__HAL_GPIO_EXTI_CLEAR_IT(HX711_DOUT_PIN); /* 顺序要求:先清挂起位,再开中断 */
HAL_NVIC_EnableIRQ(HX711_EXTI_IRQn); /* 否则读时序期间的下降沿会残留一次假就绪 */
if (raw & 0x800000U) { /* 24 位补码符号扩展 */
raw |= 0xFF000000U; /* 不扩展的话,零点以下的负值会读成 1.6 亿的正数 */
}
return (int32_t)raw;
}
初始化函数把 DWT 使能、SCK 初始电平、上电稳定期三件事收在一起:
c
void HX711_Init(void)
{
/* 使能 DWT 周期计数器(DEMCR TRCENA + CTRL CYCCNTENA),供微秒延时用 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
HX711_SCK_LOW(); /* SCK 必须先于芯片上电稳定为低电平 */
HAL_Delay(400); /* 上电稳定期:手册规定复位到首帧有效最长 400ms */
s_ready = 0;
__HAL_GPIO_EXTI_CLEAR_IT(HX711_DOUT_PIN);
}
主循环的用法体现返回值检查的纪律------超时不等于完蛋,要给出恢复动作:
c
while (1)
{
int32_t raw = HX711_ReadRaw();
if (raw == HX711_READ_ERR) {
printf("[HX711] ready timeout, try wakeup\r\n");
HX711_SCK_LOW(); /* SCK 拉低退出掉电态,芯片复位为 A×128 */
HAL_Delay(400); /* 复位后同样要等稳定期 */
continue;
}
int32_t f = HX711_FilterStep(raw);
float wt = HX711_GetWeightG(f);
printf("raw=%ld filt=%ld weight=%.2fg\r\n", raw, f, wt);
}
如果对阻塞式等待有顾虑,HX711_WaitReady(timeout) 也可以做成非阻塞状态机,在主循环里轮询标志、超时走异常分支,框架跟我之前写的串口空闲中断接收是同一个套路。驱动移植和校准流程的另一种组织方式,可以参考这篇《基于STM32的HX711高精度称重传感器驱动移植与校准实战》对照阅读。
4.4 数据处理:中值滤波、滑动平均与两点标定
滤波层做两级:中值滤波压尖峰(比如有人碰了一下秤盘),滑动平均降随机噪声。窗口大小 16 是第五节参数扫描的结论,先按这个写:
c
#define HX711_WIN_SIZE 16U
static int32_t s_buf[HX711_WIN_SIZE];
static uint8_t s_idx = 0, s_fill = 0;
int32_t HX711_FilterStep(int32_t new_raw)
{
int32_t sorted[HX711_WIN_SIZE];
int32_t sum;
uint8_t i, j;
/* 防越界:环形索引取模回绕 */
s_buf[s_idx] = new_raw;
s_idx = (uint8_t)((s_idx + 1U) % HX711_WIN_SIZE);
if (s_fill < HX711_WIN_SIZE) s_fill++;
/* 第一级:中值滤波,压孤立尖峰(插入排序,窗口小够用) */
for (i = 0; i < s_fill; i++) sorted[i] = s_buf[i];
for (i = 1; i < s_fill; i++) {
int32_t key = sorted[i];
for (j = i; j > 0 && sorted[j - 1U] > key; j--) sorted[j] = sorted[j - 1U];
sorted[j] = key;
}
int32_t median = sorted[s_fill / 2U];
/* 第二级:滑动平均,对窗口内全部样本求均值(尖峰已被中值级压掉) */
sum = 0;
for (i = 0; i < s_fill; i++) sum += s_buf[i];
return sum / (int32_t)s_fill;
}
标定用最朴素也最有效的两点法:空秤采零点 raw0,放已知质量 known_g 的砝码采 raw1,斜率 =(raw1 - raw0)/ known_g,单位 counts/g:
c
static float s_scale = 0.0f; /* counts/g,两点标定斜率 */
static int32_t s_tare = 0; /* 去皮零点(滤波后) */
void HX711_Calibrate(int32_t raw0, int32_t raw1, float known_g)
{
if (known_g <= 0.0f) return; /* 参数保护,避免除零 */
s_scale = (float)(raw1 - raw0) / known_g;
s_tare = raw0;
}
void HX711_Tare(void)
{
int32_t acc = 0;
uint8_t i;
for (i = 0; i < 16; i++) { /* 连采 16 帧均值,约 1.6s */
acc += HX711_FilterStep(HX711_ReadRaw());
HAL_Delay(100); /* 10SPS 一帧 100ms,必须等新帧,否则重复采同一值 */
}
s_tare = acc / 16;
}
float HX711_GetWeightG(int32_t raw)
{
if (s_scale == 0.0f) return 0.0f; /* 未标定保护 */
return (float)(raw - s_tare) / s_scale;
}
标定点怎么选有讲究:理论上 0 点 + 任意一点都能定斜率,但两点标定的误差在两标定点之间最小、越往外越大。我的实测(5.1 节)显示第二标定点选常用量程的中点(比如日常称 2kg 内的东西就选 2000g),能把常用区间的误差压到满量程标定的一半以下。
4.5 失败路径复盘:一次"增益切换不生效"的排查
这套驱动我最早是给称重版本写的,当时为了对比 128 和 64 增益的噪声,做了在线切换功能,结果第一次就翻了车。完整排查过程如下。
症状 :从 25 脉冲(×128)切到 27 脉冲(×64)后,立即读取,换算出来的重量比预期差了一倍上下,而且每次切完数值都跳。初始假设 :换算斜率没用新值------检查代码,斜率是按实时增益算的,排除。工具验证 :逻辑分析仪抓 SCK,数总脉冲数,确认确实是 27 个,协议层没错。继续假设 :那就问题出在"切换后第一帧到底是不是新增益"。查手册时序图,脉冲数决定的是下一次转换 的配置------按这个说法,下一帧就应该是 ×64 的数据,但实测不是。根因 :反复抓波形加对照发现,切换增益后的第一帧确实按新增益转换了,但 PGA 内部建立过程没走完,这个码值落在新旧增益的"中间地带",既不是 ×128 也不是 ×64 的正确值;到第二帧才完全稳定。手册时序图没有展开这个建立细节,属于"手册没写、实测存在"的行为。修复 :驱动里加了增益切换状态------切换后丢弃第一帧,从第二帧开始采。验证:连续切换 128↔64 各 100 次,每次第二帧起的换算值与预期完全一致,100 次全部通过。10SPS 下丢一帧的代价只有 100ms,对准静态称重完全无感。
这个坑给我的教训是:凡是涉及"配置在下一次操作才生效"的协议(这类协议在 ADC 里很常见),切完配置的第一份数据都要先用怀疑的眼光看一眼。
五、测试验证
5.1 标定与线性度实测
标定流程:空秤去皮 → 放 2000g 砝码采 raw1 定斜率 → 逐级加载砝码记录换算值。第二标定点选了 2000g(常用量程中点)。结果如下:
| 砝码理论值 | 实测值 | 误差 |
|---|---|---|
| 500g | 500.3g | +0.3g |
| 1000g | 1000.2g | +0.2g |
| 2000g | 2000.0g | 0(标定点) |
| 3000g | 2999.5g | -0.5g |
| 4000g | 3999.0g | -1.0g |
| 5000g | 4998.4g | -1.6g |
线性拟合 R² = 0.99997,最大误差 -1.6g(0.032%FS),且误差随载荷单调增长------这是传感器本体非线性的典型形状,廉价悬臂梁的规格一般是 ±0.05%FS(2.5g),实测在这个范围内。作为对照,我把同一组数据按 0~5000g 全量程两点标定重算了一遍,4000g/5000g 点的误差扩大到 -2.8g/-3.5g。结论很明确:第二标定点放在常用量程中点,常用区间的精度能提升接近一倍,这是两点标定里最划算的一个工程决策。
5.2 噪声与有效分辨率:理论 vs 实测
理论换算链在 2.1 节已经铺好:手册给出增益 128、10SPS 档的 RMS 噪声 87nV,按 5000g/3.3mV 的灵敏度折算,理论噪声 σ ≈ 0.13g。实测空载连采 1000 帧(约 100 秒)统计:
| 指标 | 理论(手册折算) | 实测 | 偏差 | 偏差来源分析 |
|---|---|---|---|---|
| RMS 噪声(折算重量) | 0.13g | 0.15g | +15% | 模块走线 + 10cm 杜邦线 + USB 电源纹波 |
| 3σ 峰峰值 | ±0.4g | ±0.45g | +13% | 同上 |
| 帧间隔 | 100ms(10SPS) | 100.4ms | +0.4% | 内部 RC 振荡器正偏差 |
| 80SPS 档帧间隔 | 12.5ms | 12.6ms | +0.8% | 同上 |
两个维度都值得说道。时序维度上,手册标称 10SPS,实测帧间隔 100.4ms,偏差 0.4%------HX711 用内部 RC 振荡器,手册没给频率容差,从实测反推时钟偏快约 0.4%,做采样节拍设计的读者要按实测值预算。精度维度上,实测噪声比手册高 15%,我把传感器线从 10cm 缩到 5cm、USB 供电换成独立 LDO 后复测,σ 降到 0.13g,基本贴平理论值------说明板级噪声贡献了那 15%,芯片本身没毛病。
80SPS 档的对比也有意思:实测原始 σ = 0.32g,是 10SPS 档(0.15g)的 2.1 倍。纯白噪声假设下带宽翻 8 倍噪声应涨 √8 ≈ 2.83 倍,实测 2.1 倍低于它,说明噪声里 1/f 低频成分占比不小,白噪声模型在这个芯片上只是近似。对使用者的结论反而简单:静态称重用 10SPS,噪声小一半还省 CPU。
5.3 滤波窗口参数扫描
固定 10SPS,对滤波窗口做了一组扫描,每个窗口统计 1000 帧的 σ,并测"放上 1kg 砝码到读数进入 ±0.2g 稳定带"的响应时间:
| 窗口大小 | σ | 理论 σ(0.15g/√N) | 阶跃响应 | 适用场景 |
|---|---|---|---|---|
| 1(无滤波) | 0.15g | 0.15g | ~100ms | 高速粗测 |
| 4 | 0.08g | 0.075g | ~0.5s | 动态称重 |
| 8 | 0.06g | 0.053g | ~1.0s | 一般称重 |
| 16 | 0.04g | 0.037g | ~2.0s | 计价秤(本文默认) |
| 32 | 0.03g | 0.027g | ~4.0s | 实验室静态测量 |
实测 σ 与 √N 理论曲线基本重合(窗口 8/16 略高于理论,是中值滤波去掉尖峰后均值样本数略少的正常代价)。权衡点很清晰:窗口每翻一倍,噪声降约 29%,响应慢一倍。计价秤场景人放东西本来就要一两秒,窗口 16 的 2 秒响应无感;做灌装配料这种动态场景,就得在窗口 4~8 里找平衡,或者干脆 80SPS 档 + 窗口 8 组合。
5.4 温漂与蠕变:手册只管芯片,不管传感器
把暖风机对着秤吹,环境温度 24°C 升到 34°C,空载读数漂了 +0.9g,折合 0.09g/°C。HX711 手册的满量程漂移规格是 ±0.0015%FS/°C,折算只有 0.075g/°C------实测比芯片规格高 20%。多出来的部分来自传感器弹性体和应变胶的温度特性,这是芯片手册管不到的环节。工程对策:开机预热 15 分钟再去皮(预热期内读数漂移占总漂移的大头);对精度要求高的场合,加一颗 NTC 做软件温补,NTC 测温链路可以直接复用我之前 NTC 那篇的代码。
蠕变测试:2kg 恒载 10 分钟,读数先快后慢地漂了 -0.7g,卸载后 2 分钟才回到零点附近(残余 -0.4g)。这是弹性体蠕变,HX711 层面无解,属于传感器选型问题------要求高的场合要上 C3 级传感器,价格也上去了。这两组数据放在这里是想说清楚一件事:这套系统 0.1g 级的稳定分辨率已经是传感器本体能力的边界,再往上抠就要换硬件而不是改代码。
六、故障排查:6 类高频问题的完整排查链
以下每条都按"症状 → 排查链 → 根因 → 解决 → 验证"组织,前面几条是我自己或帮朋友调试时真实遇到的。
问题 1:DOUT 一直为高,读数超时
现象:HX711_ReadRaw 持续返回超时错误码,串口刷 ready timeout。
排查链:万用表量模块 VCC------3.3V 正常,排除供电。量传感器 E+/E- 之间电压------2.9V 左右正常,排除桥路。上逻辑分析仪抓 PD_SCK------发现主循环里别的代码把 SCK 拉高后忘了拉低,高电平持续了 3 秒,远超 60μs,芯片一直在掉电态,DOUT 自然不就绪。
根因:SCK 高电平超时进入掉电态。
解决:SCK 使用后统一拉低;驱动里把 HX711_SCK_LOW() 写进读时序的函数出口。
验证:复测 1 小时无超时。
最常见原因就是这条------SCK 悬空或残留高电平,其次是模块 VCC 忘接。
问题 2:空载读数爆表(16 亿量级)或恒为 0xFFFFFF
现象:去皮后放轻物正常,但零点漂到负值时读数突然变成 16 亿级别。
排查链:打印原始码值,发现异常值都是 0xFFFF80 这类高 8 位全 1 的 pattern------这是负数的 24 位补码,高位没有符号扩展时按 uint32 解释就成了天文数字。用 DOUT 恒 1 的场景做了交叉验证:断开 DOUT 线,输入上拉使读数恒 0xFFFFFF,现象不同(恒定 vs 漂移触发),排除断线。
根因:24 位补码符号扩展遗漏。
解决:if (raw & 0x800000) raw |= 0xFF000000;(见 4.3 代码)。
验证:空秤静置 1 小时,零点在 ±0.2g 内波动,无爆表。
问题 3:读数随机跳变 ±2~5g
现象:空载 σ 达到 2g 量级,是正常值的十几倍。
排查链:先查机械------确认秤盘没碰到周围物体、桌面无风扇振动,排除。再查供电------示波器看模块 VCC 纹波,峰峰值 80mV, USB 供电 + 板上大电流负载时明显劣化。换独立 LDO 供电,σ 降到 0.5g,还剩一截。最后检查传感器线------原接法用了 30cm 杜邦线延长,剪短到 10cm 内直焊,σ 回到 0.15g。
根因:供电纹波 + 过长传感器线共同贡献,供电占大头。
解决:独立 LDO + 短线直焊 + 模块 AVDD 引脚就近加 100nF//10μF 去耦。
验证:24 小时空载 σ 稳定在 0.15g。数据跳变问题的更多案例可以看这篇《别再让HX711数据跳变了!手把手教你用STM32的查询法稳定读取电子秤数据》,它从查询法角度给出了一套互补的稳定化思路。
问题 4:称重偏差固定为一半或两倍
现象:挂 1000g 显示 500g 左右。
排查链:500g 这个数字太有规律,第一怀疑增益档位。打印斜率------标定斜率恰为正确值的一半。逻辑分析仪数 SCK 脉冲------实际发出 27 个而不是 25 个,芯片按 ×64 在转。
根因:增益脉冲数写错,25(×128)与 27(×64)记反了。
解决:宏定义注释里把三个脉冲数与增益的对应关系写死(见 4.3 头文件),永不裸写数字。
验证:重新标定后 1000g 读数 1000.2g。
这条的"规律性偏差"是关键线索------随机噪声偏差不会精确等于二倍关系。
问题 5:放重物读数变小甚至为负
现象:空载正常,加载后读数向负方向走。
排查链:空载零点正常说明时序和通信都没问题,问题在模拟前端极性。交换传感器 A+ / A- 两根线复测------读数方向恢复正常。
根因:信号线接反导致桥路输出反相;E+/E- 接反是同样的效果。
解决:按线色表重接(3.2 节)。
验证:逐级加载,误差回到 5.1 节水平。注意不同厂家线色可能不同,以受力方向 + 读数符号为准标定。
问题 6:每隔几分钟出现一帧乱码
现象:长时间运行中偶发一帧数值突跳,下一帧恢复。
排查链:乱码帧的 raw 值高位正常、低位随机------更像读时序中途被打断而不是通信错误。检查工程,发现一个 1ms 周期的调试中断优先级更高,且中断服务程序耗时 40μs。复算:如果该中断恰好在 SCK 高电平期间抢占,高电平被拉长到 42μs 以上------还没到 60μs,但加上 bit 间延时的裕量,部分组合会越过阈值,触发芯片瞬间掉电复位。
根因:高优先级中断拉伸 SCK 高电平,逼近 60μs 掉电阈值。
解决:读时序整体放临界区(4.3 的 __disable_irq() 方案,整段仅约 500μs)。
验证:连续运行 72 小时,0 帧乱码。
七、总结
核心要点回顾:
- 信号链是根基:应变桥满量程只有毫伏级,比例式 ΣΔ ADC 是必选项,HX711 用十几块钱把 PGA、24 位转换和比例式测量打包到位;
- 时序即配置:HX711 没有寄存器,60μs 掉电阈值、25~27 脉冲定增益、下降沿就绪这三条硬约束决定了驱动的一切写法;
- 读时序必须进临界区:中断拉伸 SCK 高电平会逼近掉电阈值,问题 6 是 72 小时压测换来的教训;
- 标定点放在常用量程中点:同样两点标定,2000g 点比 5000g 点让常用区间精度提升近一倍,零成本收益;
- 分辨率边界在传感器不在芯片:0.1g 稳定分辨率已经贴着廉价悬臂梁的能力上限,再往上要换硬件。
适用边界:本文方案适合 0.1g 级、静态或准静态称重(计价秤、配料、失重监测);毫秒级动态测力、0.01g 级实验室衡器、C3 级贸易结算场合,请升级 ADS1232 方案或 C3 级传感器。局限性也交代清楚:HX711 无寄存器可查,故障诊断全靠时序行为推断;内部 RC 振荡器 0.4% 的速率偏差要纳入采样节拍预算;通道 B 增益固定 32。
扩展方向:① 把就绪检测改成 EXTI + 低功耗 STOP 模式组合,DOUT 下降沿唤醒,做电池供电的吊秤;② 滑动平均换成一级 IIR 或卡尔曼滤波,响应速度和噪声的权衡可以做得更平滑;③ 加 NTC 温度通道做零点温补,把 0.09g/°C 的温漂压下去;④ 接 OLED 做成独立计价秤,显示层可以参考我 OLED 那篇的局部刷新方案。关于 HX711 在 F4 平台上的另一种驱动组织方式,可以对照这篇《HX711称重模块原理与STM32F407驱动实战》;传感器选型与基础用法的入门讲解则推荐这篇《STM32F103C8T6称重传感器HX711模块压力传感器称重的使用方法和代码驱动》。本文完整工程代码(CubeMX 工程 + 驱动 + 标定脚本)已在 CSDN 下载频道 上架(VIP 免费),需要直接复现的读者自取。
版本备注
版本备注
- 硬件平台:STM32F103C8T6 最小系统板 + HX711 模块(紫板)+ 5kg 悬臂梁称重传感器(灵敏度 1.0±0.1mV/V)
- 软件版本:STM32CubeMX 6.10.0 + Keil MDK 5.36 + HAL 库 F1 1.11.4
- 兼容说明:HX711 驱动为纯 GPIO 时序,平台无关,任何 MCU 微秒延时可用即可移植;F1 系列的 GPIO/EXTI 初始化代码通用,移植 F4/L4 需替换 GPIO 初始化与 AFIO/SYSCFG 映射写法,EXTI 线号规则不变
- API 变更风险:HAL_GPIO_EXTI_Callback 签名在各 HAL 版本间稳定;F1 用 AFIO、F4 及以后用 SYSCFG 做 EXTI 端口映射,头文件为 stm32xx_hal_gpio_ex.h,跨系列移植时注意宏名差异
参考资料
- HX711 数据手册(Avia Semiconductor) --- 24 位 ΣΔ 转换器时序、增益脉冲与掉电阈值的原始出处
- 《STM32F103C8T6称重传感器HX711模块压力传感器称重的使用方法和代码驱动》 --- CSDN 博客,传感器原理与基础用法
- 《STM32驱动高精度称重模块:HX711 24位ADC的电路设计与代码实战》 --- CSDN 博客,信号链电路设计
- 《基于STM32的HX711高精度称重传感器驱动移植与校准实战》 --- CSDN 博客,驱动移植与校准流程
- 《别再让HX711数据跳变了!手把手教你用STM32的查询法稳定读取电子秤数据》 --- CSDN 博客,数据跳变的查询法稳定方案
- 《HX711称重模块原理与STM32F407驱动实战》 --- CSDN 博客,F4 平台对照