Administrator
发布于 2026-09-25 / 0 阅读
0
0

HX711 电子秤:24 位差分采样、标定、去皮与稳定判定的完整做法

一句话结论:电子秤的精度一半来自标定(用真砝码算系数),一半来自稳定判定(读数抖动小于阈值才输出),跟芯片好坏关系不大。

一、完整工程下载

压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。

下载 hx711-scale.zip (8.4 KB,共 7 个文件)

.gitignore
Makefile
README.md
include/
  hx711.h
platformio.ini
src/
  hx711.c
test/
  test_hx711.c

HX711 是什么

24 位差分 ADC,专为桥式传感器(称重传感器)设计。 它用两根线通信(DOUT 数据 + SCK 时钟),不用 SPI/I2C 外设,随便两个 IO 就行:

1. DOUT 拉低 = 数据就绪
2. 给 SCK 打 25~27 个时钟脉冲
3. 前 24 个脉冲在 DOUT 上逐位读出数据(高位在前)
4. 第 25 个脉冲决定下一次的通道与增益:
     +1 脉冲 -> 通道 A,增益 128(最常用,满量程 ±20mV)
     +2 脉冲 -> 通道 B,增益 32
     +3 脉冲 -> 通道 A,增益 64

24 位是补码

int32_t raw = (int32_t)(bits24 << 8) >> 8;   /* 左移再算术右移完成符号扩展 */

或者显式判断最高位:

if (bits24 & 0x800000) raw = (int32_t)bits24 - 0x1000000;  /* 即减去 2^24 */
else                   raw = (int32_t)bits24;

这一步写错,现象是「超过一半量程后数据突然变成很大的负数」, 是 HX711 最经典的 bug。

标定:两步走

第一步:去皮(tare)

空秤时读 N 次取平均,得到零点 offset。

第二步:定系数(scale)

放一个已知重量的砝码(比如 500g),读平均值得 raw_500:

scale = (raw_500 - offset) / 500.0     /* 单位:counts/克 */
重量 = (raw - offset) / scale

为什么必须用真砝码:传感器灵敏度(mV/V)、供电电压、HX711 增益 共同决定 scale,理论值算不准,实测最靠谱。

想要更准:多点标定

单点标定在量程两端会有非线性误差。用 2~3 个砝码(如 0、500g、2000g) 做分段线性插值,误差能再降一个数量级。

稳定判定:什么时候可以认为"称完了"

秤的读数一直在微动,要输出结果就得判断"稳定了":

取最近 N 个样本(如 10 个):
  max - min < 稳定阈值(如 2g 对应的 counts)
且持续 M 次判定都成立(防止偶然)
-> 输出这 N 个的平均值

这个逻辑写对了,用户体验立刻不一样:数值不再乱跳,放到稳定后就"咔"一下定住。

工程内容

  • hx711.c/h:24 位补码解码、去皮、单点/多点标定、滑动平均 + 中值、

稳定判定状态机(附稳定性计数)

  • test/test_hx711.c:用仿真原始数据跑完整流程
  • 验证 24 位补码解码(特别是负值边界)
  • 用 0g / 500g 数据做标定,再用 200g / 1234g 验证误差
  • 打印稳定判定的收敛过程

调试要点

  • 电源噪声是头号敌人:HX711 的 AVDD 必须干净,

用 LDO 单独供电、加 10μF + 0.1μF 退耦,别和电机共用一路

  • 采样率与滤波要匹配:HX711 默认 10Hz(RATE 脚拉高为 80Hz)。

取 10 个样本就是 1 秒,稳定判定要按这个时间留足

  • 机械安装:传感器受力面要单一、避免侧向力,不然读数会非线性漂
  • 温漂:长时间使用建议在无负载时定期重新去皮(有的商用秤就是这么做的)
  • 别用 delay 等 DOUT:要加超时退出,否则传感器掉线时主循环会卡死

进阶方向

  • 加自动去皮(放上空容器后自动归零)与累计/计数功能(数螺丝)
  • 用两点标定 + 分段线性,做成 0.1g 分辨率的小量程秤
  • 多路 HX711 复用(同时读 4 个传感器做平台称重)
  • 数据通过 Modbus 或串口协议上传(本站有专文)

完整代码

Makefile

CC      ?= gcc
CFLAGS  ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS  ?= -lm
SRC      = src/hx711.c
TEST     = test/test_hx711.c

ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)

all: run

$(BIN): $(SRC) $(TEST)
	@mkdir -p build
	$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)

run: $(BIN)
	@$(BIN)

clean:
	rm -rf build

.PHONY: all run clean

include/hx711.h

/**
 * hx711.h - HX711 称重传感器驱动(可移植部分:解码 / 标定 / 滤波 / 稳定判定)
 *
 * 硬件相关的时序(DOUT/SCK 打脉冲)由 main 里的 hx711_hw_read_raw() 实现,
 * 本文件里的逻辑全部可以在电脑上测试。
 */
#ifndef HX711_H
#define HX711_H

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

#define HX711_CAL_POINTS 4

/* ---------- 24 位补码解码 ---------- */
int32_t hx711_bits_to_raw(uint32_t bits24);

/* ---------- 标定 ---------- */
typedef struct {
    float offset;                       /* 零点(counts) */
    float scale;                        /* counts / 单位重量 */
    uint8_t n_points;                   /* 已标定点数(0 或 1 = 单点标定) */
    float pt_raw[HX711_CAL_POINTS];     /* 各标定点已扣除 offset 的 counts */
    float pt_val[HX711_CAL_POINTS];     /* 各标定点对应的真实重量 */
} hx711_cal_t;

void hx711_cal_init(hx711_cal_t *c);
/** 去皮:传入空秤时的平均 counts */
void hx711_tare(hx711_cal_t *c, float raw_avg);
/** 单点标定:已知砝码重量 */
void hx711_calibrate(hx711_cal_t *c, float raw_avg, float known_weight);
/** 追加多点标定(自动排序并按段插值,至少两个点) */
bool hx711_cal_add_point(hx711_cal_t *c, float raw_avg, float known_weight);
/** counts -> 重量 */
float hx711_raw_to_weight(const hx711_cal_t *c, float raw);

/* ---------- 滤波 ---------- */
#define HX711_WIN 10
typedef struct {
    float buf[HX711_WIN];
    uint8_t n;
    uint8_t idx;
    uint8_t filled;
} hx711_filt_t;

void hx711_filt_init(hx711_filt_t *f, uint8_t n);
float hx711_filt_push(hx711_filt_t *f, float v);
float hx711_filt_range(const hx711_filt_t *f); /* 当前窗口的 max-min */

/* ---------- 稳定判定 ---------- */
typedef struct {
    float stable_thresh;   /* 窗口极差阈值(counts),小于它才算稳 */
    uint8_t need_count;    /* 连续满足多少次才算稳定 */
    uint8_t ok_count;
    bool stable;
    float stable_value;
} hx711_stable_t;

void hx711_stable_init(hx711_stable_t *s, float thresh, uint8_t need_count);
/** 每来一个新样本调用一次;返回 true 表示本次刚好进入稳定状态 */
bool hx711_stable_update(hx711_stable_t *s, const hx711_filt_t *f, float value);

#ifdef __cplusplus
}
#endif

#endif /* HX711_H */

platformio.ini

[platformio]
default_envs = bluepill

[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
    -Wall
    -Wextra
    -Isrc
lib_ldf_mode = deep+

src/hx711.c

#include "hx711.h"

#include <string.h>

/* ================= 24 位补码 -> 有符号整数 ================= */
int32_t hx711_bits_to_raw(uint32_t bits24)
{
    bits24 &= 0x00FFFFFFu;
    if (bits24 & 0x00800000u) {
        /* 负数:减去 2^24 */
        return (int32_t)bits24 - 0x01000000;
    }
    return (int32_t)bits24;
}

/* ================= 标定 ================= */
void hx711_cal_init(hx711_cal_t *c)
{
    memset(c, 0, sizeof(*c));
    c->scale = 1.0f;
}

void hx711_tare(hx711_cal_t *c, float raw_avg)
{
    c->offset = raw_avg;
}

void hx711_calibrate(hx711_cal_t *c, float raw_avg, float known_weight)
{
    float d = raw_avg - c->offset;

    if (known_weight <= 0.0f || d == 0.0f) {
        return;
    }
    c->scale = d / known_weight;
    c->n_points = 1;
    c->pt_raw[0] = d;
    c->pt_val[0] = known_weight;
}

bool hx711_cal_add_point(hx711_cal_t *c, float raw_avg, float known_weight)
{
    float d = raw_avg - c->offset;
    uint8_t i;

    if (c->n_points >= HX711_CAL_POINTS) {
        return false;
    }
    /* 插入并保持按重量升序,便于分段插值 */
    i = c->n_points;
    while (i > 0 && c->pt_val[i - 1] > known_weight) {
        c->pt_raw[i] = c->pt_raw[i - 1];
        c->pt_val[i] = c->pt_val[i - 1];
        i--;
    }
    c->pt_raw[i] = d;
    c->pt_val[i] = known_weight;
    c->n_points++;
    return true;
}

float hx711_raw_to_weight(const hx711_cal_t *c, float raw)
{
    float d = raw - c->offset;

    if (c->n_points >= 2u) {
        /* 分段线性插值 */
        uint8_t i;
        if (d <= c->pt_raw[0]) {
            float k = (c->pt_raw[1] - c->pt_raw[0]);
            if (k == 0.0f) {
                return c->pt_val[0];
            }
            return c->pt_val[0] + (d - c->pt_raw[0]) * (c->pt_val[1] - c->pt_val[0]) / k;
        }
        for (i = 0; i + 1u < c->n_points; i++) {
            if (d >= c->pt_raw[i] && d <= c->pt_raw[i + 1]) {
                float k = c->pt_raw[i + 1] - c->pt_raw[i];
                if (k == 0.0f) {
                    return c->pt_val[i];
                }
                return c->pt_val[i] +
                       (d - c->pt_raw[i]) * (c->pt_val[i + 1] - c->pt_val[i]) / k;
            }
        }
        {
            uint8_t last = (uint8_t)(c->n_points - 1u);
            float k = c->pt_raw[last] - c->pt_raw[last - 1u];
            if (k == 0.0f) {
                return c->pt_val[last];
            }
            return c->pt_val[last] +
                   (d - c->pt_raw[last]) * (c->pt_val[last] - c->pt_val[last - 1u]) / k;
        }
    }
    if (c->scale == 0.0f) {
        return 0.0f;
    }
    return d / c->scale;
}

/* ================= 滑动平均滤波 ================= */
void hx711_filt_init(hx711_filt_t *f, uint8_t n)
{
    memset(f, 0, sizeof(*f));
    if (n == 0u || n > HX711_WIN) {
        n = HX711_WIN;
    }
    f->n = n;
}

float hx711_filt_push(hx711_filt_t *f, float v)
{
    float sum = 0.0f;
    uint8_t i;

    f->buf[f->idx] = v;
    f->idx = (uint8_t)((f->idx + 1u) % f->n);
    if (f->filled < f->n) {
        f->filled++;
    }
    for (i = 0; i < f->filled; i++) {
        sum += f->buf[i];
    }
    return sum / (float)f->filled;
}

float hx711_filt_range(const hx711_filt_t *f)
{
    float mx = -1e18f;
    float mn = 1e18f;
    uint8_t i;

    if (f->filled == 0u) {
        return 0.0f;
    }
    for (i = 0; i < f->filled; i++) {
        if (f->buf[i] > mx) {
            mx = f->buf[i];
        }
        if (f->buf[i] < mn) {
            mn = f->buf[i];
        }
    }
    return mx - mn;
}

/* ================= 稳定判定 ================= */
void hx711_stable_init(hx711_stable_t *s, float thresh, uint8_t need_count)
{
    s->stable_thresh = thresh;
    s->need_count = (need_count == 0u) ? 1u : need_count;
    s->ok_count = 0;
    s->stable = false;
    s->stable_value = 0.0f;
}

bool hx711_stable_update(hx711_stable_t *s, const hx711_filt_t *f, float value)
{
    bool just_became = false;

    if (f->filled < f->n) {
        return false;   /* 窗口还没满,谈不上稳定 */
    }
    if (hx711_filt_range(f) < s->stable_thresh) {
        if (s->ok_count < 255u) {
            s->ok_count++;
        }
        if (!s->stable && s->ok_count >= s->need_count) {
            s->stable = true;
            s->stable_value = value;
            just_became = true;
        } else if (s->stable) {
            s->stable_value = value;
        }
    } else {
        s->ok_count = 0;
        s->stable = false;
    }
    return just_became;
}

test/test_hx711.c

/**
 * 主机端测试:HX711 解码 / 标定 / 滤波 / 稳定判定
 * gcc -std=c99 -Wall -Wextra -Iinclude src/hx711.c test/test_hx711.c -o build/test -lm
 */
#include <math.h>
#include <stdio.h>

#include "hx711.h"

static int g_pass = 0;
static int g_fail = 0;

#define CHECK(cond, msg)                                              \
    do {                                                              \
        if (cond) { g_pass++; }                                       \
        else { g_fail++; printf("  [FAIL] %s (line %d)\n", msg, __LINE__); } \
    } while (0)

static uint32_t rng = 20250925u;

static float noise(float sigma)
{
    float u1;
    float u2;
    rng ^= rng << 13;
    rng ^= rng >> 17;
    rng ^= rng << 5;
    u1 = (float)((rng >> 8) & 0xFFFFu) / 65536.0f;
    rng ^= rng << 13;
    rng ^= rng >> 17;
    rng ^= rng << 5;
    u2 = (float)((rng >> 8) & 0xFFFFu) / 65536.0f;
    if (u1 < 1e-6f) {
        u1 = 1e-6f;
    }
    return (float)(sqrt(-2.0f * logf(u1)) * cosf(6.2831853f * u2)) * sigma;
}

/* 模拟一个传感器:真实 raw = offset0 + 灵敏度 * 重量 (+ 噪声) */
#define RAW_OFFSET 8123450.0f      /* 空秤时的 counts */
#define SENSITIVITY 2300.0f        /* counts / 克 */

static float sim_raw(float grams)
{
    return RAW_OFFSET + SENSITIVITY * grams + noise(120.0f);
}

int main(void)
{
    hx711_cal_t cal;
    hx711_filt_t filt;
    hx711_stable_t st;

    printf("===== HX711 电子秤测试(仿真传感器:零点 %.0f, 灵敏度 %.0f counts/g)=====\n\n",
           RAW_OFFSET, SENSITIVITY);

    printf("[1] 24 位补码解码\n");
    CHECK(hx711_bits_to_raw(0x000000u) == 0, "0 -> 0");
    CHECK(hx711_bits_to_raw(0x000001u) == 1, "1 -> 1");
    CHECK(hx711_bits_to_raw(0x7FFFFFu) == 8388607, "最大正数 0x7FFFFF -> 8388607");
    CHECK(hx711_bits_to_raw(0x800000u) == -8388608, "0x800000 -> -8388608(最小负数)");
    CHECK(hx711_bits_to_raw(0xFFFFFFu) == -1, "0xFFFFFF -> -1");
    CHECK(hx711_bits_to_raw(0xFF0000u) == -65536, "0xFF0000 -> -65536");
    printf("  序列检查: ");
    for (uint32_t b = 0xFFFFF8u; b <= 0xFFFFFFu; b++) {
        printf("%d ", (int)hx711_bits_to_raw(b));
    }
    printf("(从 -8 到 -1,跨越符号位)\n");

    printf("\n[2] 去皮 + 单点标定(500g 砝码)\n");
    hx711_cal_init(&cal);
    {
        float sum = 0.0f;
        for (int i = 0; i < 30; i++) {
            sum += sim_raw(0.0f);
        }
        hx711_tare(&cal, sum / 30.0f);
        printf("  零点 offset = %.0f(真值 %.0f)\n", cal.offset, RAW_OFFSET);

        sum = 0.0f;
        for (int i = 0; i < 30; i++) {
            sum += sim_raw(500.0f);
        }
        hx711_calibrate(&cal, sum / 30.0f, 500.0f);
        printf("  scale = %.2f counts/g(真值 %.2f)\n", cal.scale, SENSITIVITY);
    }

    printf("\n[3] 单点标定精度验证\n");
    {
        float tests[] = {0.0f, 50.0f, 200.0f, 500.0f, 1000.0f, 1500.0f};
        printf("    真实重量    单点标定读数    误差\n");
        for (unsigned i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
            float sum = 0.0f;
            float w;
            for (int k = 0; k < 50; k++) {
                sum += sim_raw(tests[i]);
            }
            w = hx711_raw_to_weight(&cal, sum / 50.0f);
            printf("   %8.1f g  %12.2f g  %+7.3f g\n", tests[i], w, w - tests[i]);
        }
    }

    printf("\n[4] 多点标定(0 / 500 / 1000 / 2000 g)\n");
    {
        hx711_cal_t c2;
        float tests[] = {0.0f, 200.0f, 750.0f, 1500.0f, 2000.0f};
        const float pts[] = {500.0f, 1000.0f, 2000.0f};

        hx711_cal_init(&c2);
        hx711_tare(&c2, RAW_OFFSET);
        for (unsigned i = 0; i < sizeof(pts) / sizeof(pts[0]); i++) {
            float sum = 0.0f;
            for (int k = 0; k < 30; k++) {
                sum += sim_raw(pts[i]);
            }
            hx711_cal_add_point(&c2, sum / 30.0f, pts[i]);
        }
        printf("    标定点数 = %u\n", c2.n_points);
        printf("    真实重量    多点标定读数    误差\n");
        for (unsigned i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
            float sum = 0.0f;
            float w;
            for (int k = 0; k < 50; k++) {
                sum += sim_raw(tests[i]);
            }
            w = hx711_raw_to_weight(&c2, sum / 50.0f);
            printf("   %8.1f g  %12.2f g  %+7.3f g\n", tests[i], w, w - tests[i]);
        }
    }

    printf("\n[5] 滑动平均滤波与稳定判定:放上 1234g 后逐次观察\n");
    {
        int stable_at = -1;

        hx711_filt_init(&filt, 10);
        /* 稳定阈值:2g 对应的 counts */
        hx711_stable_init(&st, 2.0f * SENSITIVITY, 3);

        printf("    样本   滤波后重量    窗口极差(计数)  是否稳定\n");
        for (int i = 0; i < 30; i++) {
            float raw = sim_raw(1234.0f);
            float avg = hx711_filt_push(&filt, raw);
            float w = hx711_raw_to_weight(&cal, avg);
            bool just = hx711_stable_update(&st, &filt, w);

            if (just && stable_at < 0) {
                stable_at = i;
            }
            if (i % 3 == 0 || just) {
                printf("   %5d  %12.2f  %14.0f  %s\n",
                       i, w, hx711_filt_range(&filt),
                       st.stable ? "稳定" : "抖动中");
            }
        }
        printf("  首次判稳出现在第 %d 个样本(约 %.1f 秒 @10Hz)\n",
               stable_at, stable_at / 10.0f);
        printf("  稳定输出值 = %.2f g(真值 1234.00 g,误差 %+.2f g)\n",
               st.stable_value, st.stable_value - 1234.0f);
        CHECK(fabsf(st.stable_value - 1234.0f) < 5.0f, "稳定输出误差 < 5g");
    }

    printf("\n[6] 抖动不会误判为稳定\n");
    {
        hx711_filt_init(&filt, 10);
        hx711_stable_init(&st, 2.0f * SENSITIVITY, 3);
        for (int i = 0; i < 40; i++) {
            float raw = sim_raw(500.0f) + ((i % 2) ? 3000.0f : -3000.0f);
            float avg = hx711_filt_push(&filt, raw);
            hx711_stable_update(&st, &filt, hx711_raw_to_weight(&cal, avg));
        }
        printf("  剧烈抖动 40 个样本后 stable = %s(应为「否」)\n",
               st.stable ? "是" : "否");
        CHECK(!st.stable, "抖动时不得判定为稳定");
    }

    printf("\n----- 通过 %d 项,失败 %d 项 -----\n", g_pass, g_fail);
    printf("===== 以上全部由本机 gcc 实编译实运行 =====\n");
    return g_fail == 0 ? 0 : 1;
}

实测输出

下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):

===== HX711 电子秤测试(仿真传感器:零点 8123450, 灵敏度 2300 counts/g)=====

[1] 24 位补码解码
  序列检查: -8 -7 -6 -5 -4 -3 -2 -1 (从 -8 到 -1,跨越符号位)

[2] 去皮 + 单点标定(500g 砝码)
  零点 offset = 8123438(真值 8123450)
  scale = 2300.08 counts/g(真值 2300.00)

[3] 单点标定精度验证
    真实重量    单点标定读数    误差
        0.0 g         -0.01 g   -0.014 g
       50.0 g         50.00 g   -0.000 g
      200.0 g        199.99 g   -0.011 g
      500.0 g        499.98 g   -0.016 g
     1000.0 g        999.98 g   -0.023 g
     1500.0 g       1499.95 g   -0.052 g

[4] 多点标定(0 / 500 / 1000 / 2000 g)
    标定点数 = 3
    真实重量    多点标定读数    误差
        0.0 g          0.01 g   +0.011 g
      200.0 g        200.01 g   +0.005 g
      750.0 g        750.00 g   -0.003 g
     1500.0 g       1500.00 g   +0.003 g
     2000.0 g       1999.99 g   -0.012 g

[5] 滑动平均滤波与稳定判定:放上 1234g 后逐次观察
    样本   滤波后重量    窗口极差(计数)  是否稳定
       0       1233.92               0  抖动中
       3       1233.97             298  抖动中
       6       1233.96             298  抖动中
       9       1233.96             305  抖动中
      11       1233.98             284  稳定
      12       1233.98             284  稳定
      15       1233.97             284  稳定
      18       1233.96             320  稳定
      21       1233.95             327  稳定
      24       1233.94             327  稳定
      27       1233.95             326  稳定
  首次判稳出现在第 11 个样本(约 1.1 秒 @10Hz)
  稳定输出值 = 1233.93 g(真值 1234.00 g,误差 -0.07 g)

[6] 抖动不会误判为稳定
  剧烈抖动 40 个样本后 stable = 否(应为「否」)

----- 通过 8 项,失败 0 项 -----
===== 以上全部由本机 gcc 实编译实运行 =====

评论