1. 电子秤设计概述:从零开始打造高精度称重系统

大家好,今天我想分享一个非常实用的项目——用STM32和HX711制作高精度电子秤。这个项目不仅适合电子爱好者练手,对于需要精确测量重量的各种应用场景也非常实用。我自己在实际项目中多次使用这种方案,从厨房秤到工业称重设备都能胜任。

STM32作为主控芯片的选择非常明智,它拥有丰富的外设接口和强大的处理能力,而HX711则是专为电子秤设计的24位模数转换器,能够提供极高的测量精度。两者结合,可以构建一个成本低廉但性能出色的称重系统。

在实际制作过程中,你会发现这个项目的硬件连接相对简单,主要难点在于软件的校准和滤波处理。不过别担心,我会带你一步步解决这些问题。我记得第一次做电子秤时,最头疼的就是数据跳动问题,后来通过多种滤波算法组合,终于实现了稳定的测量效果。

这个项目需要的硬件成本很低,STM32开发板几十块钱就能买到,HX711模块更是只要几元钱,称重传感器根据量程不同价格从十几元到上百元不等。如果你正在学习嵌入式开发,或者需要为一个项目添加称重功能,这个方案绝对值得尝试。

2. 硬件选型与电路设计

2.1 核心器件选择

选择合适的硬件是项目成功的关键。对于主控芯片,我推荐使用STM32F103系列,比如STM32F103C8T6(蓝色pill开发板),这款芯片价格便宜且资源丰富,完全能够满足电子秤的需求。如果你需要更多的IO口或者更强的处理能力,STM32F407系列也是不错的选择。

HX711模块的选择很重要,市面上有很多版本,建议选择带有金属屏蔽罩的版本,抗干扰能力更强。称重传感器(也称应变片)的选择要根据你的测量范围来决定,常见的量程有1kg、5kg、10kg、50kg等。对于一般用途,5kg的量程比较合适,既能测量小物件,也能应对稍重一些的物品。

电源部分需要特别注意,称重传感器对电源噪声很敏感。建议使用线性稳压电源(如LM7805)而不是开关电源,虽然效率低一些,但噪声小得多。我在实际测试中发现,使用开关电源时读数跳动明显,换成线性电源后立即稳定了很多。

2.2 电路连接详解

硬件连接其实很简单,但有几个关键点需要注意。HX711模块通常有4个主要引脚:VCC、GND、DT和SCK。VCC接5V电源,GND接地,DT接STM32的输入引脚,SCK接STM32的输出引脚。

称重传感器一般是4线制:红、黑、绿、白。红色接激励正(E+),黑色接激励负(E-),绿色接信号正(A+),白色接信号负(A-)。有些传感器线色可能不同,最好查阅具体型号的数据手册。

这里有个实用技巧:在DT引脚和地之间并联一个0.1μF的电容,可以显著减少高频干扰。同样,在电源输入端也加上滤波电容,我用的是100μF电解电容并联0.1μF瓷片电容,效果很好。

如果你想要更稳定的读数,可以考虑为基准电压源添加额外的滤波电路。虽然HX711内部已经有稳压电路,但外部再加一级RC滤波会让读数更加稳定。我在一个工业项目中就这样做过,效果相当明显。

3. STM32的GPIO与SPI配置

3.1 GPIO初始化设置

STM32的GPIO配置是整个系统的基础,需要正确设置才能确保与HX711的正常通信。首先我们要启用GPIO的时钟,然后配置数据线DT为输入模式,时钟线SCK为输出模式。

我习惯使用STM32CubeMX来生成初始化代码,这样既快速又不容易出错。对于DT引脚,配置为上拉输入模式,这样在空闲时能保持高电平。SCK引脚配置为推挽输出模式,初始状态设为低电平,因为HX711在时钟下降沿输出数据。

在实际编程中,我会为这些引脚定义清晰的宏,这样代码可读性更好:

#define HX711_DT_GPIO_Port GPIOA
#define HX711_DT_Pin GPIO_PIN_0
#define HX711_SCK_GPIO_Port GPIOA
#define HX711_SCK_Pin GPIO_PIN_1

初始化函数这样写:

void HX711_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 启用GPIOA时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    // 配置SCK引脚为输出
    GPIO_InitStruct.Pin = HX711_SCK_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(HX711_SCK_GPIO_Port, &GPIO_InitStruct);
    
    // 配置DT引脚为输入
    GPIO_InitStruct.Pin = HX711_DT_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(HX711_DT_GPIO_Port, &GPIO_InitStruct);
    
    // 初始状态:SCK为低
    HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET);
}

3.2 SPI通信替代方案

虽然HX711使用简单的两线制通信,但有些人喜欢用STM32的SPI接口来模拟时序。我个人更推荐直接使用GPIO操作,因为这样时序控制更精确,而且不占用SPI外设资源。

不过了解SPI方式也有其价值,特别是在需要与其他SPI设备共享总线时。SPI配置需要注意设置正确的时钟极性和相位,HX711要求时钟空闲时为低电平,在上升沿采样数据。

如果你选择SPI方式,记得设置数据大小为8位,波特率预分频器要设置得足够大,因为HX711的时钟频率不能太高,通常建议在100kHz以下。我一般使用SPI_BAUDRATEPRESCALER_256,这样时钟频率大约在280kHz左右,略高于推荐值但实际使用中很稳定。

无论哪种方式,关键是要保证时序正确。HX711对时序要求不是很严格,但至少要保证每个时钟脉冲的宽度大于1μs。我通常使用滴答定时器来实现微秒级延时,这样比简单的循环延时更准确。

4. HX711数据采集与处理算法

4.1 数据读取原理

HX711的数据读取过程很有讲究。这个芯片内部有一个24位的ADC,输出数据采用二进制补码格式。读取数据前,需要先检查DT引脚的状态,当DT为低电平时表示数据已经准备好。

读取过程是这样的:先拉高SCK然后迅速拉低,产生一个时钟脉冲,然后在每个脉冲的下降沿读取DT引脚的状态。重复24次,就能得到完整的24位数据。最后还要再发送一个脉冲来选择增益和通道,为下一次转换做准备。

这里有个重要细节:HX711输出的数据是补码形式,需要转换成原码才能使用。最高位是符号位,如果为1表示负数。在实际应用中,我们通常使用无符号数,所以需要进行转换。

我写的读取函数是这样的:

int32_t HX711_Read(void)
{
    int32_t data = 0;
    
    // 等待数据就绪
    while (HAL_GPIO_ReadPin(HX711_DT_GPIO_Port, HX711_DT_Pin) == GPIO_PIN_SET);
    
    // 读取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);
        data <<= 1;
        if (HAL_GPIO_ReadPin(HX711_DT_GPIO_Port, HX711_DT_Pin) == GPIO_PIN_SET)
        {
            data++;
        }
        HAL_GPIO_WritePin(HX711_SCK_GPIO_Port, HX711_SCK_Pin, GPIO_PIN_RESET);
        delay_us(1);
    }
    
    // 第25个脉冲,选择通道A,增益128
    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);
    
    // 转换补码为原码
    if (data & 0x00800000)
    {
        data |= 0xFF000000;
    }
    
    return data;
}

4.2 数据处理与滤波

原始数据往往带有噪声,直接使用会导致读数跳动。因此需要采用合适的滤波算法。我最常用的是移动平均滤波,简单但效果很好。

移动平均滤波的实现很简单:维护一个数据缓冲区,每次读取新数据时替换最旧的数据,然后计算平均值。缓冲区大小根据需要选择,我一般用8-16个数据。

#define FILTER_SIZE 8

int32_t HX711_ReadAverage(void)
{
    static int32_t buffer[FILTER_SIZE] = {0};
    static uint8_t index = 0;
    static int64_t sum = 0;
    
    // 读取新数据
    int32_t new_data = HX711_Read();
    
    // 减去最旧的数据
    sum -= buffer[index];
    
    // 加上新数据
    sum += new_data;
    buffer[index] = new_data;
    
    // 更新索引
    index = (index + 1) % FILTER_SIZE;
    
    // 返回平均值
    return (int32_t)(sum / FILTER_SIZE);
}

除了移动平均,还可以使用中值滤波来去除突发干扰。我通常先进行中值滤波去除异常值,再进行移动平均平滑数据。这种组合滤波效果很好,但计算量稍大。

对于要求更高的场合,可以考虑卡尔曼滤波。虽然算法复杂一些,但滤波效果非常好。我在一个工业称重项目中用过卡尔曼滤波,读数非常稳定,几乎看不到跳动。

5. 校准与误差补偿技术

5.1 线性校准方法

校准是电子秤制作中最关键的环节。即使硬件完全相同,每个传感器的特性也有细微差别,必须通过校准来保证测量精度。

校准需要准备标准重量,最好是经过计量的砝码。如果没有专业砝码,可以用已知重量的物品代替,比如未开封的饮料、包装食品等,这些物品的重量通常标得很准确。

校准过程分为两步:零点校准和斜率校准。零点校准就是在空载时读取ADC值,这个值就是零点偏移。斜率校准则需要放置已知重量的物品,读取此时的ADC值。

计算公式很简单: 实际重量 = (当前读数 - 零点读数) × 比例系数

比例系数 = 标准重量 / (有载读数 - 零点读数)

我通常这样实现校准函数:

float scale_factor = 1.0f;
int32_t zero_offset = 0;

void HX711_Calibrate(float known_weight)
{
    // 零点校准
    zero_offset = HX711_ReadAverage();
    
    // 提示放置重物
    printf("请放置%.2fg重物后按任意键继续...\n", known_weight);
    getchar();
    
    // 有载读数
    int32_t loaded_value = HX711_ReadAverage();
    
    // 计算比例系数
    scale_factor = known_weight / (loaded_value - zero_offset);
    
    printf("校准完成!比例系数:%f\n", scale_factor);
}

5.2 温度补偿与非线性校正

在实际使用中,你会发现读数会随着温度变化而漂移,这是由传感器的温度特性决定的。对于精度要求高的场合,需要进行温度补偿。

最简单的温度补偿方法是测量环境温度,然后根据温度修正读数。可以在电路中添加一个温度传感器如DS18B20,实时监测温度变化。

非线性校正更复杂一些。应变式传感器通常有一定的非线性,特别是在量程的两端。可以通过多点校准来建立更精确的数学模型,比如在25%、50%、75%、100%量程处分别校准。

我做过一个项目,采用了二次多项式拟合:

float weight = a * reading * reading + b * reading + c;

系数a、b、c通过最小二乘法拟合多个校准点得到。这种方法精度很高,但校准过程比较麻烦。

还有一种方法是分段线性插值,将量程分成若干段,每段使用不同的比例系数。这种方法实现简单,效果也不错。

6. 实际应用与性能优化

6.1 显示与用户界面

一个好的电子秤需要有清晰的显示和友好的用户界面。最简单的显示可以用数码管,但现在更常用的是LCD或OLED显示屏。

我偏好使用0.96寸OLED显示屏,分辨率128x64,通过I2C或SPI接口与STM32连接。这种显示屏功耗低、显示效果好,而且价格便宜。

用户界面至少应该包括当前重量显示、单位切换(g/kg/oz/lb)、去皮功能。高级一些的还可以增加计时器、计数模式、数据记录等功能。

去皮功能的实现很简单:在需要去皮时记录当前读数,以后的所有读数都减去这个值。记得提供一个清零按钮来取消去皮。

int32_t tare_value = 0;

void Tare(void)
{
    tare_value = HX711_ReadAverage();
}

float GetWeight(void)
{
    int32_t raw_data = HX711_ReadAverage();
    return (raw_data - tare_value - zero_offset) * scale_factor;
}

6.2 功耗优化与稳定性提升

对于电池供电的电子秤,功耗优化很重要。HX711本身功耗不高,但STM32的功耗可以通过优化程序进一步降低。

我最常用的方法是使用停机模式:当没有称重操作时,让STM32进入低功耗模式,通过外部中断唤醒。HX711的数据就绪信号可以连接到STM32的外部中断引脚,当有数据准备好时唤醒MCU。

稳定性方面,除了前面提到的滤波算法,硬件布局也很重要。模拟部分和数字部分要分开布局,地线要合理布置,避免数字噪声干扰模拟信号。

电源稳定性也不容忽视。我在一个项目中遇到过读数随时间缓慢漂移的问题,最后发现是基准电压源温漂导致的。更换为低温漂的基准源后问题解决。

如果你发现读数偶尔有跳变,可以尝试在软件中添加异常值检测。当读数变化超过一定阈值时,认为是干扰或错误操作,保持上次的有效读数。

float last_valid_weight = 0.0f;

float GetStableWeight(void)
{
    float current_weight = GetWeight();
    
    // 如果变化太大,认为是干扰
    if (fabs(current_weight - last_valid_weight) > MAX_JUMP)
    {
        return last_valid_weight;
    }
    
    last_valid_weight = current_weight;
    return current_weight;
}

这些优化措施看似简单,但对提升用户体验很有帮助。一个反应灵敏、读数稳定的电子秤会让使用者感到愉悦,而这些正是通过细节优化实现的。

注意:实际项目中,建议定期保存校准参数到STM32的Flash中,这样断电后不需要重新校准。同时添加硬件看门狗,防止程序跑飞。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐