资讯动态

STM32电子秤实战:HX711高精度称重从硬件到标定

发布时间:2026/10/15 14:06:22 来源:尧图企业网站定制
简介这是一份面向嵌入式初学者与电子设计爱好者的STM32电子秤完整工程源码基于STM32微控制器实现称重采集、数据处理与12864液晶显示适合用于课程设计、毕业设计或DIY项目练手。压缩包共146个文件约1.64MB以36个h头文件与33个c源文件为核心涵盖传感器驱动、ADC采样、显示驱动与主逻辑另有uvprojx、uvoptx等Keil工程文件、hex与axf可执行输出、map与lst编译映射文件以及若干bak备份和bat脚本方便直接编译下载与二次修改。资源围绕应变片传感器信号采集、STM32内置ADC转换、12864屏幕显示驱动及滤波标定算法展开作者在描述中提示可通过调整代码或硬件方案提升测量精度为读者留出优化空间。目前已有3235人学习下载适合希望掌握STM32外设驱动、A/D转换与显示交互的开发者参考借鉴。1. 从一颗应变片到稳定读数基于 STM32 单片机的电子秤到底难在哪很多人第一次做基于 STM32 单片机的电子秤都会觉得这事简单得不像一个项目称重传感器输出差分电压接个 24 位 ADC 读出来再乘个系数换算成克最后显示到屏幕上完事。真上手才发现读数像喝醉了酒一样上下跳放上 500 克砝码显示 498过一会儿又变成 503空载时还会缓慢漂移。问题不在 STM32而在模拟前端、采样时序和标定方法这三件事上。这篇笔记面向的是想真正把电子秤做稳的从业者你可能在做厨房秤、快递秤、配料秤或者只是想拿 STM32 练一套完整的模拟采集链路。我会按「硬件怎么选、ADC 怎么读、算法怎么滤、参数怎么标、坑在哪」的顺序讲所有代码都是能直接编译的最小实现参数给的是我实际调过的范围不是抄手册的典型值。看完你应该能自己搭出一台分辨率 1 克、重复性误差小于 2 克的秤。2. 称重传感器与 STM32 的硬件链路怎么搭2.1 为什么是惠斯通电桥加 24 位 ADC而不是直接接单片机 ADC称重传感器load cell本质是一个惠斯通电桥四片应变片贴在弹性体上受力后电桥失衡输出一个差分电压。常见的 1kg 量程传感器灵敏度是 1mV/V意思是激励 5V 时满量程输出只有 5mV。你要分辨 1 克就是满量程的 1/1000对应 5μV 的电压变化。STM32 自带的 12 位 ADC参考电压 3.3V 时一个 LSB 是 0.8mV连满量程信号都只有 6 个码根本谈不上分辨 1 克。所以行业里的标准做法是外挂一颗专用 24 位 Σ-Δ ADC常见的是 HX711 这类集成方案或者 CS5530、ADS1232 这类独立芯片。HX711 把 PGA、ADC、时钟都集成好了两线串行输出对 STM32 来说只需要两个 GPIO 就能读是入门和量产都常用的选择。选型上我一般这样判断量程 5kg 以内、精度要求 1 克、成本敏感用 HX711量程 30kg 以上、要求 0.1% 精度、有电磁干扰环境用 ADS1232 配外部基准需要多路同步采样比如多传感器配料秤用 CS5530 走 SPI。别一上来就追求 24 位先算清楚你的有效位数需求。2.2 激励电压、参考电压与走线三个决定底噪的细节传感器输出是比例于激励电压的所以激励源的稳定性直接决定读数稳定性。如果用 STM32 的 3.3V 给传感器供电而 ADC 参考又用内部基准两者不相关电源一波动读数就漂。正确做法是让 ADC 的参考电压和传感器激励同源这样电源波动在比例上被抵消这叫比例式测量。具体到 HX711它的 AVDD 既是内部模拟电源也常被用来给传感器激励供电参考电压默认就是 AVDD天然比例式。如果你用外部 LDO 单独给传感器供电那 ADC 参考也必须接同一个 LDO 输出否则白做。走线方面传感器到 ADC 的差分线要等长、靠近、包地远离电机和继电器。我见过一个配料秤项目传感器线跟变频器输出线捆在一起走读数噪声直接大了 20 倍分开走线后立刻恢复。激励线和信号线不要共用一根地线回流用星型接地模拟地和数字地在 ADC 下方单点连接。2.3 最小系统接线与上电时序以 HX711 为例接线是固定的传感器的 E、E- 接 HX711 的 AVDD 和 AGNDA、A- 接通道 A 的差分输入B、B- 接通道 B如果用双通道。HX711 的 DOUT 接 STM32 任意 GPIO 做输入PD_SCK 接另一个 GPIO 做输出。VCC 接 3.3V注意 HX711 模块有的标 5V 供电实际芯片 AVDD 范围是 2.7V 到 5.5V3.3V 完全可用而且和 STM32 电平匹配不用电平转换。上电时序有个容易忽略的点HX711 上电后需要等内部稳压和振荡器稳定官方建议上电后先给 PD_SCK 至少 60μs 的高电平复位再开始读。我一般在上电初始化里先拉高 PD_SCK 100μs 再拉低然后延时 400ms 等第一次转换完成再去读。少了这个延时前几次读数全是垃圾。3. 用 GPIO 模拟时序把 HX711 读数读稳3.1 HX711 的 24 位补码时序到底怎么读HX711 的通信协议很简单但时序敏感DOUT 变低表示数据准备好然后来 25 到 27 个 PD_SCK 脉冲前 24 个上升沿依次移出 24 位数据MSB 在前第 25 个脉冲决定下一次的通道和增益。数据是二进制补码最高位是符号位。第 25 个脉冲数不同选择的通道不同25 个脉冲选通道 A 增益 12826 个选通道 B 增益 3227 个选通道 A 增益 64。下面是 STM32 HAL 库下的读取实现用微秒级延时控制时序// hx711.c #include hx711.h // 微秒延时基于 DWT 或 TIM这里用简单的循环示意 static void delay_us(uint32_t us) { // 实际项目用 DWT-CYCCNT 或硬件定时器这里给出占位 uint32_t count us * (SystemCoreClock / 1000000) / 4; while (count--) { __NOP(); } } // 读一次原始值gain 传 128/64/32 int32_t HX711_Read(uint8_t gain) { uint32_t data 0; uint8_t i; uint8_t pulse; // 等待 DOUT 变低超时返回错误 uint32_t timeout 100000; while (HAL_GPIO_ReadPin(HX_DOUT_PORT, HX_DOUT_PIN) GPIO_PIN_SET) { if (--timeout 0) return 0x80000000; // 超时标志 delay_us(1); } // 读 24 位 for (i 0; i 24; i) { HAL_GPIO_WritePin(HX_SCK_PORT, HX_SCK_PIN, GPIO_PIN_SET); delay_us(1); data data 1; if (HAL_GPIO_ReadPin(HX_DOUT_PORT, HX_DOUT_PIN) GPIO_PIN_SET) { data; } HAL_GPIO_WritePin(HX_SCK_PORT, HX_SCK_PIN, GPIO_PIN_RESET); delay_us(1); } // 第 25 个脉冲选择下一次的增益 pulse (gain 128) ? 1 : (gain 64 ? 3 : 2); for (i 0; i pulse; i) { HAL_GPIO_WritePin(HX_SCK_PORT, HX_SCK_PIN, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(HX_SCK_PORT, HX_SCK_PIN, GPIO_PIN_RESET); delay_us(1); } // 补码转有符号 if (data 0x800000) { data | 0xFF000000; } return (int32_t)data; }逻辑说明先等 DOUT 拉低这是 HX711 告诉 MCU「数据好了」的信号。然后循环 24 次每个 PD_SCK 高电平期间读 DOUT注意数据是在 PD_SCK 上升沿之后稳定的所以先拉高再读。第 25 个脉冲的数量决定增益这个必须在读完 24 位之后立刻发否则下一次转换的通道就错了。补码转换那一步很多人漏掉导致负值读出来是个巨大的正数。参数说明delay_us 的精度不用太苛刻HX711 的 PD_SCK 高电平最小 0.2μs低电平最小 0.2μs1μs 足够。但如果你用 HAL_Delay 那种毫秒级延时会慢到每秒读不了几次而且可能错过 DOUT 的有效窗口。timeout 设 100000 次循环对应大约 100ms正常情况几毫秒内 DOUT 就会变低超时说明接线或供电有问题。3.2 采样率、增益与通道选择别把 10Hz 当 80Hz 用HX711 默认采样率是 10Hz也就是每秒 10 次转换。它内部有个引脚可以选 80Hz但代价是噪声变大。很多人看手册写着 80Hz 就接上去结果读数跳得没法用。我的经验是静态称重厨房秤、台秤用 10Hz配合软件滤波稳定性和响应都够动态称重皮带秤、分选秤才考虑 80Hz而且必须加更重的滤波。增益选择上通道 A 的 128 增益对应输入差分范围 ±20mV适合 1mV/V 灵敏度的传感器配 5V 激励满量程 5mV。如果你的传感器灵敏度是 2mV/V满量程 10mV128 增益也够。但如果传感器输出超过 20mV比如用 10V 激励就得换 64 增益否则 ADC 饱和读数卡在最大值不动。通道 B 的 32 增益一般用来接第二路传感器或者温度补偿单传感器项目用不上。3.3 连续采样与数据就绪判断的两种写法读 HX711 有两种常见写法阻塞式和中断式。阻塞式就是上面那样死等 DOUT简单但占 CPU。中断式把 DOUT 接到外部中断引脚下降沿触发在中断里启动读取。我一般用阻塞式加一个状态机在主循环里判断 DOUT 是否为低低才去读不低就干别的。这样既不占中断资源也不会死等。// main.c 主循环片段 int32_t raw; uint32_t last_sample 0; while (1) { // 每 100ms 尝试读一次对应 10Hz if (HAL_GetTick() - last_sample 100) { if (HAL_GPIO_ReadPin(HX_DOUT_PORT, HX_DOUT_PIN) GPIO_PIN_RESET) { raw HX711_Read(128); if (raw ! 0x80000000) { weight_filter_update(raw); // 送入滤波 } last_sample HAL_GetTick(); } } // 其他任务显示刷新、按键扫描 }这里用 HAL_GetTick 做 100ms 节拍而不是死等是为了给显示和按键留出时间。注意判断 DOUT 为低才读如果没低说明这次转换还没完成跳过下一个节拍再试。这样即使采样率有波动也不会读到重复数据。4. 从原始码值到克数标定与滤波的实操参数4.1 两点标定法的公式推导与代码实现原始码值本身没有物理意义要变成克数需要标定。最常用的是两点标定空载读一个值放已知砝码读一个值两点确定一条直线。公式是重量 (raw - raw_zero) * (known_weight / (raw_known - raw_zero))其中 raw_zero 是空载码值raw_known 是放砝码后的码值known_weight 是砝码重量。这个斜率就是「每克对应多少个码值」。实际代码里我会把斜率的倒数存成 scale 因子避免每次除法。// calibration.c typedef struct { int32_t raw_zero; // 空载码值 float scale; // 克/码值 int32_t raw_known; // 标定砝码码值 float known_weight; // 砝码重量克 } Calib_t; Calib_t calib {0}; // 采集 N 次取平均去掉最大最小各若干 int32_t sample_average(uint8_t n) { int32_t sum 0; int32_t buf[32]; if (n 32) n 32; for (uint8_t i 0; i n; i) { buf[i] HX711_Read(128); HAL_Delay(100); // 等下一次转换 } // 简单冒泡排序后去掉两端 for (uint8_t i 0; i n - 1; i) for (uint8_t j 0; j n - 1 - i; j) if (buf[j] buf[j1]) { int32_t t buf[j]; buf[j] buf[j1]; buf[j1] t; } uint8_t cut n / 8; // 去掉 12.5% 的两端 for (uint8_t i cut; i n - cut; i) sum buf[i]; return sum / (n - 2 * cut); } // 空载标定 void calib_zero(void) { calib.raw_zero sample_average(16); } // 放砝码标定 void calib_span(float known_g) { calib.raw_known sample_average(16); calib.known_weight known_g; if (calib.raw_known ! calib.raw_zero) { calib.scale known_g / (float)(calib.raw_known - calib.raw_zero); } } // 换算重量 float get_weight(int32_t raw) { return (raw - calib.raw_zero) * calib.scale; }逻辑说明sample_average 采 16 次排序后去掉两端各 2 个n/8剩下的取平均。这一步是为了去掉突发干扰比如有人碰了一下秤台。标定时先空载采一次存 raw_zero再放砝码采一次算 scale。get_weight 就是线性换算。参数说明采样次数 16 是我常用的值对应 1.6 秒标定时不差这点时间。去掉两端 12.5% 是经验值如果环境干扰大可以加到 25%。砝码重量建议选满量程的 50% 到 80%比如 5kg 秤用 2kg 或 3kg 砝码太小的话斜率误差被放大。4.2 滑动平均、中值滤波与一阶低通怎么选原始码值即使经过硬件优化仍然会有噪声。软件滤波有三种常用方案滑动平均、中值滤波、一阶低通IIR。滑动平均对周期性噪声好但会引入延迟中值滤波对脉冲干扰好但计算量大一阶低通实现简单参数调好了兼顾稳定和响应。我一般用「中值 一阶低通」组合先取最近 5 个原始值的中值去掉脉冲再送一阶低通平滑。一阶低通的公式是y[n] α * x[n] (1 - α) * y[n-1]α 越小越平滑但响应越慢。10Hz 采样下α 取 0.1 到 0.2 比较合适对应时间常数 0.5 到 1 秒。// filter.c #define MEDIAN_N 5 static int32_t median_buf[MEDIAN_N] {0}; static uint8_t median_idx 0; static float lpf_out 0; static uint8_t lpf_init 0; float filter_update(int32_t raw) { // 中值滤波 median_buf[median_idx] raw; median_idx (median_idx 1) % MEDIAN_N; int32_t tmp[MEDIAN_N]; for (uint8_t i 0; i MEDIAN_N; i) tmp[i] median_buf[i]; for (uint8_t i 0; i MEDIAN_N - 1; i) for (uint8_t j 0; j MEDIAN_N - 1 - i; j) if (tmp[j] tmp[j1]) { int32_t t tmp[j]; tmp[j] tmp[j1]; tmp[j1] t; } int32_t med tmp[MEDIAN_N / 2]; // 一阶低通 if (!lpf_init) { lpf_out (float)med; lpf_init 1; } else { lpf_out 0.15f * (float)med 0.85f * lpf_out; } return lpf_out; }逻辑说明中值滤波维护一个 5 元素环形缓冲每次排序取中间值能有效去掉单次脉冲。一阶低通用 0.15 的系数相当于新数据占 15% 权重。lpf_init 保证第一次直接赋值避免从 0 开始爬升。参数说明MEDIAN_N 取 5 是平衡计算量和效果取 3 去脉冲能力弱取 7 延迟明显。α0.15 对应时间常数约 0.6 秒放上物体后大约 1.5 秒读数稳定。如果你做的是动态称重α 要加大到 0.3 以上但噪声会明显。4.3 零点跟踪与去皮让秤「记住」空载电子秤用久了会有温漂空载码值慢慢变化。零点跟踪就是当读数接近零且稳定一段时间后自动把当前值当作新的零点。这个功能在商用秤里是标配自己做也简单判断当前重量绝对值小于阈值比如 5 克且持续 3 秒就把 raw_zero 更新为当前原始值。去皮是另一个常用功能按一下去皮键把当前重量记下来之后显示的都是减去这个值的结果。实现上就是存一个 tare_offsetget_weight 里减掉。// 零点跟踪与去皮 static int32_t zero_track_counter 0; static float tare_offset 0; void zero_tracking(float weight, int32_t raw) { if (weight -5.0f weight 5.0f) { zero_track_counter; if (zero_track_counter 30) { // 3 秒 calib.raw_zero raw; zero_track_counter 0; } } else { zero_track_counter 0; } } void tare(void) { tare_offset get_weight(HX711_Read(128)); } float get_weight_final(int32_t raw) { return get_weight(raw) - tare_offset; }逻辑说明zero_tracking 每 100ms 调用一次连续 30 次读数在 ±5 克内就更新零点。tare 把当前重量存下来显示时减掉。注意零点跟踪要在去皮之前判断否则去皮后重量接近零会误触发。参数说明±5 克阈值适合 5kg 量程小量程秤要按比例缩小。30 次对应 3 秒太短会误跟踪太长响应慢。去皮范围一般限制在满量程以内超出要提示错误。5. 避坑与排查读数跳、漂移、不准的五个真实原因5.1 读数一直跳换了滤波也没用现象空载时读数在 ±10 克范围内随机跳加滤波后跳幅减小但依然存在。原因九成是电源或地线问题。HX711 的 AVDD 如果和 STM32 的数字电源共用数字开关噪声会串进模拟部分。或者传感器线太长且没屏蔽拾取了环境干扰。解决给 HX711 的 AVDD 单独加 LC 滤波10μH 电感加 10μF 电容模拟地和数字地在 HX711 下方单点连接。传感器线用屏蔽线屏蔽层接模拟地。我遇到过一个案例把传感器线从 30cm 缩短到 10cm跳幅直接从 8 克降到 1 克。5.2 空载读数缓慢单向漂移现象上电后空载读数每分钟漂几克方向固定几十分钟后稳定。原因温漂。应变片和 HX711 内部基准都有温度系数刚上电时芯片温度在爬升。另外如果传感器安装有预紧力弹性体应力释放也会导致漂移。解决上电后预热 5 到 10 分钟再标定。开启零点跟踪让软件自动补偿。如果漂移方向固定且量大检查传感器安装是否过紧弹性体有没有被外力卡住。5.3 放上砝码读数对但线性不好现象500 克砝码显示 5001000 克显示 9952000 克显示 1980。原因传感器本身非线性或者标定点选得太靠近零点斜率外推误差大。也可能是 ADC 在高码值区饱和。解决用多点标定代替两点比如 0、50%、100% 三个点做分段线性。检查 HX711 原始码值在满量程时是否接近 0x7FFFFF如果接近说明增益太大换 64 增益。传感器非线性一般小于 0.05%如果超过这个值换传感器。5.4 读数偶尔跳一个很大的值然后恢复现象稳定读数中突然出现一个偏离几百克的单次值下一次又正常。原因时序干扰。HX711 的 PD_SCK 如果被其他中断打断第 25 个脉冲发错下一次转换的通道就变了读出来是另一个通道的值。或者 DOUT 读取时被高优先级中断抢占读到的位错位。解决读 HX711 的整个过程关中断或者至少保证 25 个脉冲连续发完。把 HX711 读取放在主循环不要放在会被打断的定时器中断里。如果必须用中断读之前 __disable_irq()读完 __enable_irq()。5.5 标定后过几天又不准了现象刚标定完很准放几天再开机同样的砝码显示差了好几克。原因零点漂移没被跟踪或者标定时的环境温度和现在差太多。也可能是传感器蠕变长时间受力后输出变化。解决每次开机先空载等稳定后自动零点校准。标定周期建议一周一次商用场合每天开机校准。如果传感器蠕变明显加载后读数持续变化超过 0.02%说明传感器质量不行换好一点的。6. 把秤做稳之后几个让精度再上一层的技巧做到这里一台能用的电子秤已经成型了。但如果你想把重复性从 2 克压到 0.5 克还有几件事可以做。第一是给 HX711 的模拟电源加一个低温漂基准。HX711 内部基准的温漂典型值是 10ppm/°C如果环境温度变化 20°C基准就变了 200ppm对应 5kg 量程就是 1 克。换成外部 2.5V 基准比如 REF5025温漂 3ppm/°C这一项误差能压到 0.3 克以内。代价是电路复杂一点成本增加几块钱。第二是采样同步。如果你发现读数里有一种周期性的波动频率和市电 50Hz 接近那是工频干扰。HX711 的 10Hz 采样率正好是 50Hz 的 1/5如果采样时刻不固定就会采到工频的不同相位。解决办法是让采样节拍严格同步到工频周期或者用 80Hz 采样率加数字陷波。我一般用定时器触发采样周期设成 100ms 的整数倍实测能把工频干扰压下去一半。第三是温度补偿。如果产品要在户外或者温差大的环境用光靠零点跟踪不够。可以在传感器附近贴一颗 NTC 或者数字温度传感器标定时记录不同温度下的零点偏移做一张温度-偏移查找表运行时线性插值补偿。这个做法在工业秤里很常见代码量不大但效果立竿见影。第四是机械安装。再好的电路也救不了糟糕的机械结构。传感器要安装在刚性足够的底座上受力方向要和传感器设计方向一致避免侧向力和扭矩。秤台和传感器之间用球头或者柔性连接消除偏载。我见过一个案例秤台四个角用螺丝硬连接偏载误差到了 5 克换成柔性支撑后降到 0.5 克。最后说一个我自己的习惯每次改完硬件或者滤波参数不要只看空载稳不稳一定要用砝码在秤台的四个角和中心各测一遍记录最大偏差。偏载误差是最容易被忽略的指标但用户一用就能感觉到。我一般要求自己做的秤偏载误差不超过满量程的 0.05%5kg 秤就是 2.5 克以内。达不到就回去查机械安装和传感器一致性。这套方案我从最早的 HX711 加 STM32F103 开始用后来换到 STM32G0 和 STM32L4核心逻辑没变过。关键不是芯片多高级而是把模拟前端、时序和标定这三件事做扎实。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑