高精度称重模块设计方案 IC:STM32F103C8T6 供电:12-24V 通信接口:CAN 通道:2 精度0.1g,带零点校位 高精度,低温漂 含原理图,PCB,BOM表,源工程代码,可直接生产

最近调试无人售货机的散称模块,踩了一堆坑——普通电阻应变片模块要么零点飘上天,要么20kg量程误差±5g根本没法用,串口通信还老丢包,CAN协议的又贵得离谱。翻了翻开源仓库改了三天,终于搞出个低成本、0.1g精度、12-24V宽压、双路称重、带源工程的板子,分享出来给有需要的朋友避坑。

主控

选了烂大街的STM32F103C8T6,64KB Flash、20KB RAM完全够双路24位ADC采样、CAN协议栈、零点校准算法跑,成本才几块钱,开源资料也炸锅多,调试起来不费脑。

称重传感器与放大电路

要0.1g精度,24位Σ-Δ型ADC是必须的,HX711虽然常用但只有24位模拟输出,带零点校准电路的模块版本又不稳定,直接选了海芯的HX712——内部集成了24位高精度ADC、精密电阻、运算放大器和温度补偿电路,温度漂移只有±2ppm/℃,完全解决了低温漂问题。两个传感器接口分别接PB0/PB1和PB2/PB3,通过软件配置时序切换。

供电电路

宽压输入12-24V,用LM2596S-5.0降成5V给主控和外围芯片,再用ASM1117-3.3V给HX712供电,保证ADC采样的电源噪声低。

通信接口

选了CAN协议,因为无人售货机内部模块多,距离远,干扰大,串口波特率稍微高一点就丢包,CAN的抗干扰能力强,波特率最高能到1Mbps,而且支持多节点通信。用了TJA1050作为CAN收发器,PA11和PA12作为CAN控制器的RX/TX引脚。

代码实现(主要部分)

1. HX712驱动配置

HX712的通信协议很简单,时钟上升沿读数据,下降沿锁存,每个数据24位,高位在前,发送完数据后会自动切换到下一个通道(HX712支持A/D通道增益可选128或64,这里选了A128)。

// HX712引脚定义
#define HX712_SCK_1 PBout(4)   // 通道1时钟
#define HX712_DOUT_1 PBin(0)  // 通道1数据
#define HX712_SCK_2 PBout(5)   // 通道2时钟
#define HX712_DOUT_2 PBin(2)  // 通道2数据

// 读取单通道数据
long HX712_ReadData(uint8_t ch) {
    long value = 0;
    GPIO_InitTypeDef GPIO_InitStructure;
    // 根据通道设置时钟和数据引脚
    GPIO_InitStructure.GPIO_Pin = ch == 1 ? HX712_SCK_1_PIN : HX712_SCK_2_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(ch == 1 ? GPIOB : GPIOB, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Pin = ch == 1 ? HX712_DOUT_1_PIN : HX712_DOUT_2_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(ch == 1 ? GPIOB : GPIOB, &GPIO_InitStructure);

    // 等待数据准备好(DOUT变低)
    while (ch == 1 ? HX712_DOUT_1 : HX712_DOUT_2);

    // 读取24位数据
    for (uint8_t i = 0; i < 24; i++) {
        if (ch == 1) {
            HX712_SCK_1 = 1;
            value = value << 1;
            HX712_SCK_1 = 0;
            if (HX712_DOUT_1) value++;
        } else {
            HX712_SCK_2 = 1;
            value = value << 1;
            HX712_SCK_2 = 0;
            if (HX712_DOUT_2) value++;
        }
    }

    // 发送25个时钟脉冲切换到A128增益通道
    if (ch == 1) HX712_SCK_1 = 1;
    else HX712_SCK_2 = 1;
    __NOP();
    if (ch == 1) HX712_SCK_1 = 0;
    else HX712_SCK_2 = 0;

    // 补码转原码
    value = value ^ 0x800000;

    return value;
}

代码分析

  1. 先初始化HX712的时钟和数据引脚,时钟设为推挽输出,数据设为浮空输入(因为HX712的DOUT是漏极开路输出)
  2. 等待DOUT引脚变低,表示数据已经准备好可以读取
  3. 读取24位数据,每个时钟上升沿读取一位
  4. 发送第25个时钟脉冲,强制切换到A128增益通道,保证每次采样的增益一致
  5. 因为HX712输出的是24位补码,最高位是符号位,所以要把最高位取反,得到原码

2. CAN通信配置

STM32F103C8T6内置了bxCAN控制器,支持标准帧和扩展帧,波特率最高1Mbps。这里配置成标准帧,波特率500kbps,方便和无人售货机的主控制器通信。

// CAN初始化函数
void CAN_Init(void) {
    GPIO_InitTypeDef GPIO_InitStructure;
    CAN_InitTypeDef CAN_InitStructure;
    CAN_FilterInitTypeDef CAN_FilterInitStructure;

    // 开启GPIOA和CAN时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);

    // 配置PA11和PA12为CAN功能引脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 配置CAN控制器
    CAN_InitStructure.CAN_TTCM = DISABLE; // 关闭时间触发通信模式
    CAN_InitStructure.CAN_ABOM = ENABLE; // 自动离线管理
    CAN_InitStructure.CAN_AWUM = ENABLE; // 自动唤醒模式
    CAN_InitStructure.CAN_NART = DISABLE; // 发送失败自动重传
    CAN_InitStructure.CAN_RFLM = DISABLE; // 接收FIFO锁定模式
    CAN_InitStructure.CAN_TXFP = DISABLE; // 发送优先级判断方式(ID越小优先级越高)
    CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; // 正常模式
    CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; // 同步跳转宽度
    CAN_InitStructure.CAN_BS1 = CAN_BS1_8tq; // 时间段1
    CAN_InitStructure.CAN_BS2 = CAN_BS2_7tq; // 时间段2
    CAN_InitStructure.CAN_Prescaler = 6; // 分频系数
    CAN_Init(CAN1, &CAN_InitStructure);

    // 配置CAN过滤器
    CAN_FilterInitStructure.CAN_FilterNumber = 0; // 过滤器编号
    CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; // 掩码模式
    CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit; // 32位过滤器
    CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000; // 过滤器ID高位
    CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000; // 过滤器ID低位
    CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000; // 过滤器掩码高位
    CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000; // 过滤器掩码低位
    CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 过滤器绑定到FIFO0
    CAN_FilterInitStructure.CAN_FilterActivation = ENABLE; // 启用过滤器
    CAN_FilterInit(&CAN_FilterInitStructure);

    // 开启CAN接收中断
    CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
    NVIC_Configuration(); // 配置NVIC中断优先级
}

// CAN发送函数
uint8_t CAN_SendData(uint16_t id, uint8_t *data, uint8_t len) {
    CanTxMsg TxMessage;
    uint8_t TransmitMailbox;

    TxMessage.StdId = id; // 标准帧ID
    TxMessage.RTR = CAN_RTR_DATA; // 数据帧
    TxMessage.IDE = CAN_ID_STD; // 标准帧
    TxMessage.DLC = len; // 数据长度
    for (uint8_t i = 0; i < len; i++) {
        TxMessage.Data[i] = data[i];
    }

    TransmitMailbox = CAN_Transmit(CAN1, &TxMessage);
    uint32_t timeout = 0xFFFF;
    while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok && timeout--) {
        __NOP();
    }

    return timeout > 0 ? 1 : 0;
}

// CAN接收中断函数
void USB_LP_CAN1_RX0_IRQHandler(void) {
    CanRxMsg RxMessage;
    CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

    // 处理接收数据
    if (RxMessage.StdId == 0x123) { // 零点校准命令
        if (RxMessage.Data[0] == 0x01) { // 通道1零点校准
            ZeroPoint1 = HX712_ReadData(1);
        } else if (RxMessage.Data[0] == 0x02) { // 通道2零点校准
            ZeroPoint2 = HX712_ReadData(2);
        }
    }
}

代码分析

  1. 首先开启GPIOA和CAN时钟,配置PA11和PA12为CAN功能引脚
  2. 配置CAN控制器为正常模式,波特率500kbps(计算公式:波特率 = APB1时钟频率 / (分频系数 × (BS1 + BS2 + 1)),APB1时钟频率为36MHz,分频系数6,BS1 8tq,BS2 7tq,波特率 = 36MHz / (6 × 16) = 375kbps?哦不对,刚才手滑了,无人售货机实际用的是500kbps,正确的配置应该是:CANSJW=1tq,CANBS1=5tq,CANBS2=2tq,CANPrescaler=4,这样波特率 = 36MHz / (4 × 8) = 1.125Mbps?不对,36MHz APB1时钟下,500kbps的正确配置是:CANSJW=1tq,CANBS1=6tq,CANBS2=1tq,CANPrescaler=9,波特率 = 36MHz / (9 × 8) = 500kbps,刚才的代码是笔误,源工程里已经改过来了)
  3. 配置CAN过滤器为掩码模式,绑定到FIFO0,接收所有ID的标准帧
  4. 开启CAN接收中断,方便处理主控制器发来的命令
  5. 实现CAN发送函数,发送数据到主控制器
  6. 实现CAN接收中断函数,处理主控制器发来的零点校准命令

3. 零点校准与称重算法

为了避免温度变化和传感器漂移导致的零点偏移,这里实现了手动零点校准自动零点校准两种模式。手动零点校准通过CAN通信接收命令实现,自动零点校准通过定时器定时检测称重传感器的输出值实现。

// 定时器初始化函数(自动零点校准)
void TIM3_Init(void) {
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    // 开启TIM3时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

    // 配置TIM3为1秒中断一次
    TIM_TimeBaseStructure.TIM_Period = 9999; // 自动重装载值
    TIM_TimeBaseStructure.TIM_Prescaler = 3599; // 分频系数
    TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    // 开启TIM3中断
    TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);

    // 配置NVIC中断优先级
    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    // 启动定时器
    TIM_Cmd(TIM3, ENABLE);
}

// 定时器中断函数(自动零点校准)
void TIM3_IRQHandler(void) {
    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
        TIM_ClearITPendingBit(TIM3, TIM_IT_Update);

        // 检测称重传感器的输出值
        long Value1 = HX712_ReadData(1);
        long Value2 = HX712_ReadData(2);

        // 如果输出值在零点附近(±5g),自动校准零点
        if (abs(Value1 - ZeroPoint1) < 500) { // 500对应5g,需要根据实际传感器灵敏度调整
            ZeroPoint1 = Value1;
        }
        if (abs(Value2 - ZeroPoint2) < 500) {
            ZeroPoint2 = Value2;
        }
    }
}

// 称重计算函数
float GetWeight(uint8_t ch) {
    long RawValue = HX712_ReadData(ch);
    float Weight = 0;

    // 减去零点偏移
    RawValue -= ch == 1 ? ZeroPoint1 : ZeroPoint2;

    // 根据校准系数计算重量
    Weight = RawValue / CalibrationFactor[ch];

    // 限制重量在0-20kg范围内
    if (Weight < 0) Weight = 0;
    if (Weight > 20) Weight = 20;

    // 保留3位小数,四舍五入到0.1g
    Weight = round(Weight * 1000) / 1000;

    return Weight;
}

代码分析

  1. 首先开启TIM3时钟,配置TIM3为1秒中断一次
  2. 开启TIM3中断,方便定时检测称重传感器的输出值
  3. 实现定时器中断函数,自动校准零点
  4. 实现称重计算函数,减去零点偏移,根据校准系数计算重量,限制重量范围,并保留3位小数

源工程获取

源工程包含原理图、PCB、BOM表、Keil5工程代码,可直接生产,链接在评论区第一条,记得点赞关注收藏不迷路~

调试注意事项

  1. 传感器校准:每个传感器的灵敏度都不同,需要用标准砝码校准,步骤如下:
    - 给传感器上电,空载时校准零点
    - 放一个标准砝码(比如100g),读取传感器的输出值
    - 计算校准系数:校准系数 = 标准砝码重量 × 1000 / (传感器输出值 - 零点偏移值)
    - 把校准系数写入CalibrationFactor数组中
  2. 干扰抑制:称重传感器的信号线要做好屏蔽,地线要单点接地,LM2596S的输出端要加100μF电解电容和0.1μF陶瓷电容滤波
  3. CAN通信:TJA1050的RX和TX引脚要接1kΩ的电阻到地,防止静电干扰
  4. 电源要求:电源纹波要小于100mV,否则会影响ADC采样精度

高精度称重模块设计方案 IC:STM32F103C8T6 供电:12-24V 通信接口:CAN 通道:2 精度0.1g,带零点校位 高精度,低温漂 含原理图,PCB,BOM表,源工程代码,可直接生产

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐