高精度称重模块设计方案 IC:STM32F103C8T6 供电:12-24V 通信接口:CAN...
高精度称重模块设计方案 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;
}
代码分析:
- 先初始化HX712的时钟和数据引脚,时钟设为推挽输出,数据设为浮空输入(因为HX712的DOUT是漏极开路输出)
- 等待DOUT引脚变低,表示数据已经准备好可以读取
- 读取24位数据,每个时钟上升沿读取一位
- 发送第25个时钟脉冲,强制切换到A128增益通道,保证每次采样的增益一致
- 因为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);
}
}
}
代码分析:
- 首先开启GPIOA和CAN时钟,配置PA11和PA12为CAN功能引脚
- 配置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,刚才的代码是笔误,源工程里已经改过来了)
- 配置CAN过滤器为掩码模式,绑定到FIFO0,接收所有ID的标准帧
- 开启CAN接收中断,方便处理主控制器发来的命令
- 实现CAN发送函数,发送数据到主控制器
- 实现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;
}
代码分析:
- 首先开启TIM3时钟,配置TIM3为1秒中断一次
- 开启TIM3中断,方便定时检测称重传感器的输出值
- 实现定时器中断函数,自动校准零点
- 实现称重计算函数,减去零点偏移,根据校准系数计算重量,限制重量范围,并保留3位小数
源工程获取
源工程包含原理图、PCB、BOM表、Keil5工程代码,可直接生产,链接在评论区第一条,记得点赞关注收藏不迷路~
调试注意事项
- 传感器校准:每个传感器的灵敏度都不同,需要用标准砝码校准,步骤如下:
- 给传感器上电,空载时校准零点
- 放一个标准砝码(比如100g),读取传感器的输出值
- 计算校准系数:校准系数 = 标准砝码重量 × 1000 / (传感器输出值 - 零点偏移值)
- 把校准系数写入CalibrationFactor数组中 - 干扰抑制:称重传感器的信号线要做好屏蔽,地线要单点接地,LM2596S的输出端要加100μF电解电容和0.1μF陶瓷电容滤波
- CAN通信:TJA1050的RX和TX引脚要接1kΩ的电阻到地,防止静电干扰
- 电源要求:电源纹波要小于100mV,否则会影响ADC采样精度
高精度称重模块设计方案 IC:STM32F103C8T6 供电:12-24V 通信接口:CAN 通道:2 精度0.1g,带零点校位 高精度,低温漂 含原理图,PCB,BOM表,源工程代码,可直接生产



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


所有评论(0)