高精度称重模块设计方案 IC:STM32F103C8T6 供电12-24V 通信接口CAN 通道2 精度0.1g带零点校位 高精度低温漂 含原理图PCBBOM表源工程代码可直接生产最近调试无人售货机的散称模块踩了一堆坑——普通电阻应变片模块要么零点飘上天要么20kg量程误差±5g根本没法用串口通信还老丢包CAN协议的又贵得离谱。翻了翻开源仓库改了三天终于搞出个低成本、0.1g精度、12-24V宽压、双路称重、带源工程的板子分享出来给有需要的朋友避坑。主控选了烂大街的STM32F103C8T664KB 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分频系数6BS1 8tqBS2 7tq波特率 36MHz / (6 × 16) 375kbps哦不对刚才手滑了无人售货机实际用的是500kbps正确的配置应该是CANSJW1tqCANBS15tqCANBS22tqCANPrescaler4这样波特率 36MHz / (4 × 8) 1.125Mbps不对36MHz APB1时钟下500kbps的正确配置是CANSJW1tqCANBS16tqCANBS21tqCANPrescaler9波特率 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带零点校位 高精度低温漂 含原理图PCBBOM表源工程代码可直接生产