HX711高精度电子秤的STM32驱动开发避坑指南(0.001g精度实现)

最近在做一个微型精密称重项目,目标是实现0-50克范围内、0.001克(1毫克)的测量精度。听起来像是实验室级别的需求,但用STM32配合常见的HX711模块,其实完全可以在成本可控的情况下做到。不过,从原理图到代码,再到最后的校准,每一步都有不少“坑”等着你。这篇文章,我就把自己从零搭建、调试,最终稳定实现毫克级精度的全过程,以及中间遇到的那些典型问题和解决方案,毫无保留地分享出来。无论你是想优化现有的电子秤设计,还是正被精度不达标、读数跳变等问题困扰,希望这些实战经验能帮你少走弯路。

1. 硬件选型与电路设计的隐形陷阱

实现毫克级精度,硬件是地基。地基不稳,软件再怎么优化都是空中楼阁。很多人以为随便找个HX711模块和称重传感器就能开始写代码,结果往往卡在最初的硬件环节。

1.1 传感器与HX711模块的匹配艺术

称重传感器的选择是第一道坎。市面上常见的“体重秤传感器”或“厨房秤传感器”,其额定载荷(如5kg)和灵敏度(通常2.0mV/V)是针对百克级精度设计的。要实现毫克级精度,我们需要的是小量程、高灵敏度的传感器。

注意:传感器的“精度”和“分辨率”是两个概念。一个100g量程、精度0.05%FS的传感器,其理论误差是±0.05g,这已经超过了我们1mg的目标。因此,必须选择更高精度等级(如0.02%FS或C3级)的微型传感器。

这里有个关键参数常被忽略:传感器输出信号的最小电压。假设我们使用一个50g量程、灵敏度1.5mV/V的传感器,供电电压(激励电压)为5V。那么满量程输出信号为 5V * 1.5mV/V = 7.5mV。HX711的内部PGA(可编程增益放大器)最大增益为128,其输入参考电压Vref通常为内部产生的1.25V。那么,放大后的满量程电压为 7.5mV * 128 = 960mV,仍在ADC的输入范围内。但如果传感器信号更小,或者供电电压波动,这个值就会变化,直接影响有效分辨率。

一个实用的选型对照表如下:

参数推荐规格不推荐规格原因分析
传感器量程50g - 100g1kg 以上量程越大,对1mg变化的信号响应越微弱,信噪比越低。
精度等级C3 或 0.02%FSC2 或 0.05%FS更高的精度等级意味着更小的非线性误差和蠕变。
灵敏度≥ 1.0 mV/V< 1.0 mV/V更高的灵敏度能产生更大的原始信号,有利于提升信噪比。
推荐型号举例BLR-50g, HZC-50g通用型5kg传感器前者为实验室级微型力传感器,后者为商用级。

1.2 电源与接地的“魔鬼细节”

电源噪声是精度杀手,尤其是对HX711这种高增益、高分辨率的ADC。很多开发板直接用USB的5V给整个系统供电,噪声水平可能高达几十毫伏,这足以淹没1mg重量对应的微伏级信号变化。

我的经验是:必须为模拟部分(HX711和传感器)提供独立、干净的线性稳压电源。可以使用一个独立的LDO(如AMS1117-3.3)专门为HX711的AVDD(模拟电源)和传感器电桥供电。数字部分(STM32、LCD1602)的电源可以分开。

接地同样关键。务必采用星型单点接地。具体做法是:将模拟地(AGND)和数字地(DGND)在电源入口处通过一个0欧姆电阻或磁珠连接在一起,之后各自形成独立的接地回路。HX711的GND引脚、传感器的屏蔽线应连接到干净的模拟地平面。

// 一个常见的错误接线示例(会导致严重噪声)
// USB 5V ---> STM32 Vin ---> 3.3V LDO ---> 同时给STM32、HX711、传感器供电
// 结果:数字电路的开关噪声通过电源直接耦合进模拟电路。

// 推荐的电源架构示意图(文字描述):
// USB 5V 输入
//   ├──> [开关稳压器] ---> 5V ---> 数字部分 (STM32核心、LCD背光等)
//   └──> [线性稳压器 LDO1] ---> 3.3V_A ---> HX711.AVDD, 传感器激励+
//       注意:传感器激励- 接 AGND。
//       模拟地(AGND)与数字地(DGND)在电源输入处单点连接。

2. STM32驱动HX711的核心代码与时序玄机

网上能找到的HX711驱动程序很多,但大多只实现了基本读数功能,对于高精度应用,时序、滤波和异常处理都经不起推敲。

2.1 超越“教科书式”的驱动编写

标准的HX711驱动依赖于精准的微秒级延时来控制DATA和SCK引脚。在STM32上,很多人直接用HAL_Delay,这是大忌,因为它会阻塞整个系统。更优解是使用硬件SPI模拟通信,或者精确的定时器延时。

下面是我优化后的驱动核心函数,重点在于增强鲁棒性。它包含了超时判断,防止因为线缆松动或传感器未就绪导致的程序死锁。

/**
  * @brief  从HX711读取一个原始ADC值(24位有符号)
  * @param  hx711_gain: 通道和增益选择,详见宏定义
  * @retval int32_t: 原始ADC值,如果读取失败返回0x800000(一个错误值)
  */
int32_t HX711_ReadValue(HX711_Gain_t gain)
{
    uint32_t count = 0;
    int32_t value = 0;
    
    // 1. 等待HX711就绪 (DATA引脚变高)
    while (HAL_GPIO_ReadPin(HX711_DATA_GPIO_Port, HX711_DATA_Pin) == GPIO_PIN_RESET)
    {
        count++;
        if (count > 400000) // 超时判断,约200ms
        {
            return HX711_READ_TIMEOUT; // 定义超时错误码
        }
    }
    
    // 2. 禁用外部中断等可能影响精度的操作(可选,根据系统设计)
    __disable_irq();
    
    // 3. 开始读取24位数据
    for (uint8_t i = 0; i < 24; i++)
    {
        HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET);
        delay_us(1); // 使用精准的微秒延时,如SysTick或定时器实现
        
        value = value << 1;
        if (HAL_GPIO_ReadPin(HX711_DATA_GPIO_Port, HX711_DATA_Pin) == GPIO_PIN_SET)
        {
            value++;
        }
        
        HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET);
        delay_us(1);
    }
    
    // 4. 发送额外的脉冲设置下一次读取的增益
    for (uint8_t i = 0; i < gain; i++)
    {
        HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_SET);
        delay_us(1);
        HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET);
        delay_us(1);
    }
    
    __enable_irq();
    
    // 5. 扩展符号位(24位有符号数转换为32位有符号数)
    if (value & 0x00800000)
    {
        value |= (int32_t)0xFF000000;
    }
    
    return value;
}

这个驱动里有两个关键点:

  1. 超时机制:防止因传感器断开而卡死在while循环。
  2. 中断屏蔽:在关键的24位数据读取期间屏蔽中断,避免时序被系统中断(如SysTick)打断,造成数据错位。这对于高精度采样至关重要。

2.2 采样策略与数字滤波实战

直接读取一次HX711的值是充满噪声的。必须通过软件滤波来稳定读数。但滤波算法选不好,要么响应迟钝,要么滤波效果差。

  • 移动平均滤波:最简单,但会引入滞后。窗口大小N需要权衡。对于电子秤,N=10~32比较常见。
  • 一阶低通滤波(指数加权平均):计算量小,实时性好。公式为 filtered_val = alpha * raw_val + (1 - alpha) * last_filtered_val。alpha越小,滤波越强,滞后越大。
  • 中值滤波:对脉冲噪声(如用手触碰引起的毛刺)有奇效,但不能平滑高斯噪声。

我最终采用的是复合滤波策略:先连续采样10次,去掉一个最大值和一个最小值(类似体操计分),对剩余的8个值求平均,再将这个平均值送入一阶低通滤波器。这样既能抵抗偶然干扰,又能保证平滑性。

#define SAMPLE_NUM 10
#define ALPHA 0.2f // 低通滤波系数

int32_t Get_Filtered_ADC(void)
{
    int32_t samples[SAMPLE_NUM];
    int32_t sum = 0;
    int32_t temp;
    static int32_t filtered_value = 0;
    
    // 1. 采集一组原始数据
    for (int i = 0; i < SAMPLE_NUM; i++)
    {
        samples[i] = HX711_ReadValue(GAIN_128); // 假设使用通道A,增益128
        // 这里可以加入简单的有效性判断,如数值是否在合理范围内
    }
    
    // 2. 冒泡排序,便于去掉最大最小值(简单示例,实际可用更高效算法)
    for (int i = 0; i < SAMPLE_NUM - 1; i++)
    {
        for (int j = 0; j < SAMPLE_NUM - 1 - i; j++)
        {
            if (samples[j] > samples[j + 1])
            {
                temp = samples[j];
                samples[j] = samples[j + 1];
                samples[j + 1] = temp;
            }
        }
    }
    
    // 3. 去掉首尾(最大最小值),求中间值的和
    sum = 0;
    for (int i = 1; i < SAMPLE_NUM - 1; i++)
    {
        sum += samples[i];
    }
    
    // 4. 计算去极值平均
    int32_t trimmed_mean = sum / (SAMPLE_NUM - 2);
    
    // 5. 一阶低通滤波
    filtered_value = (int32_t)(ALPHA * trimmed_mean + (1 - ALPHA) * filtered_value);
    
    return filtered_value;
}

3. 从ADC原始值到毫克重量的校准哲学

这是将数字信号转化为物理量的魔法步骤,也是最容易出错的地方。常见的两点校准法(零点、满量程)在这里可能不够用。

3.1 非线性校准与多点拟合

传感器的输出与重量关系并非完美的直线,尤其是在接近零点和满量程时。对于毫克级精度,必须考虑非线性。我推荐使用多点线性拟合或二次曲线拟合。

具体操作:

  1. 准备一组已知重量的标准砝码,例如:0g, 5g, 10g, 20g, 30g, 40g, 50g。砝码的精度最好高于你的目标精度一个数量级(即0.1mg级)。
  2. 在无负重状态下,记录ADC值作为零点AD_Z。
  3. 依次放置每个砝码,待读数稳定后记录对应的ADC值AD_i。
  4. 将数据对(Weight_i, AD_i - AD_Z)输入到Excel或MATLAB等工具中,进行线性或多项式回归分析。

你会发现,简单的线性公式 Weight = K * (AD - AD_Z) 在全程范围内可能有几个毫克的偏差。而采用二次公式 Weight = A * (AD - AD_Z)^2 + B * (AD - AD_Z) 可以显著提高拟合精度。

在STM32中实现二次曲线计算:

// 校准系数,通过上位机拟合得到后填入
float calib_A = 1.234e-10; // 二次项系数,通常非常小
float calib_B = 0.0456789; // 一次项系数,即灵敏度
int32_t calib_AD_Z = 8560000; // 零点ADC值

float ADC_To_Weight(int32_t adc_value)
{
    int32_t adc_offset = adc_value - calib_AD_Z;
    float weight = calib_A * adc_offset * adc_offset + calib_B * adc_offset;
    // 注意单位:如果calib_B单位是 g/LSB,那weight单位就是克
    return weight;
}

3.2 温度补偿与长期漂移应对

环境温度变化会影响传感器的灵敏度和零点。对于要求苛刻的应用,需要考虑温度补偿。

  • 硬件方案:在传感器附近放置一个高精度数字温度传感器(如DS18B20)。
  • 软件方案:在不同温度下(如10°C, 25°C, 40°C)重复3.1的校准步骤,得到多组校准系数(A_T, B_T, AD_Z_T)。在实际使用时,根据实时温度T,对系数进行插值。

长期漂移(蠕变)是另一个难题。传感器在长期负载后,卸载回零时读数会偏移。应对策略:

  • 选用蠕变性能好的C3级传感器。
  • 软件上实现“自动清零”功能:在检测到重量长时间稳定在极低值(如<0.01g)时,缓慢地更新零点值AD_Z,但要有阈值限制,防止误触发。

4. 系统集成与显示优化

当重量值已经能够稳定、准确地计算出来后,如何呈现给用户同样影响体验。我们使用LCD1602,如何让0.001g的跳动看起来舒服?

4.1 显示刷新与数据平滑

切忌以原始滤波后的数据直接高频刷新显示。人的眼睛对快速跳动的数字非常敏感,即使跳动只有±0.001g,也会让人觉得“不稳定”。

我的策略是分层显示:

  • 底层:高速采样与滤波(如10Hz),用于内部计算和快速响应。
  • 中层:显示专用滤波。对底层输出的重量值再进行一次时间常数较大的低通滤波(例如alpha=0.05),这个滤波只用于显示刷新。
  • 顶层:控制刷新率。LCD1602的刷新速度不宜过快,每200-500ms更新一次显示数据即可。在更新前,判断当前显示值与新计算值的差值,如果小于一个阈值(如0.0005g),则跳过本次更新,避免无意义的末位数字闪烁。
// 显示层专用滤波器和更新逻辑
static float displayed_weight = 0.0f;
#define DISPLAY_ALPHA 0.05f
#define DISPLAY_THRESHOLD 0.0005f

void Update_Display_Task(void) // 在定时器中断或主循环中周期调用,如200ms一次
{
    float current_calc_weight = Get_Current_Weight(); // 获取经过校准和底层滤波的重量
    
    // 显示层滤波
    float new_display_weight = DISPLAY_ALPHA * current_calc_weight + (1 - DISPLAY_ALPHA) * displayed_weight;
    
    // 变化阈值判断
    if (fabs(new_display_weight - displayed_weight) >= DISPLAY_THRESHOLD)
    {
        displayed_weight = new_display_weight;
        // 调用LCD显示函数,格式化为小数点后3位
        LCD_ShowWeight(displayed_weight);
    }
}

4.2 抗干扰与异常状态处理

一个健壮的系统必须能处理异常。

  • 上电/开机自检:读取HX711的ID(通过特定时序,某些版本支持),检查ADC值是否在合理范围内(不应接近0x000000或0xFFFFFF)。
  • 重量突变检测:如果相邻两次采样值差超过物理可能范围(比如50g传感器,1ms内变化了10g),则判定为干扰或传感器故障,丢弃该数据并触发警告。
  • LCD1602通信异常处理:在发送指令和数据后,增加读取忙标志的等待(如果支持)或插入足够的延时,确保显示稳定。

最后,把所有模块整合起来。主循环的结构可以非常清晰:

int main(void)
{
    // 硬件初始化
    System_Init();
    HX711_Init();
    LCD_Init();
    
    // 执行开机 tare (去皮)
    Perform_Tare();
    
    while (1)
    {
        // 1. 获取滤波后的ADC值
        int32_t adc = Get_Filtered_ADC();
        
        // 2. 转换为重量(含非线性校准)
        float weight_g = ADC_To_Weight(adc);
        
        // 3. 处理去皮、单位换算等
        weight_g = Apply_Tare(weight_g);
        
        // 4. 更新显示(内部会做显示层滤波和节流)
        Update_Display(weight_g);
        
        // 5. 处理按键(去皮、校准、单位切换)
        Key_Process();
        
        // 6. 其他系统任务...
        HAL_Delay(10); // 主循环周期约10ms
    }
}

走到这一步,一个能够稳定显示0.001g精度的电子秤系统就基本搭建完成了。剩下的就是细致的调试:用一组标准砝码,从0g、5g、10g……一直到50g,反复测试,记录误差,微调校准系数,观察长时间工作的稳定性。这个过程需要耐心,但当你看到LCD上稳定地显示着“5.000g”,而秤盘上正好放着5g的标准砝码时,那种成就感是实实在在的。

更多推荐