基于STM32F103C8T6与HX711的电子秤设计:HAL库驱动与数据校准实战
1. 项目背景与硬件选型
最近在做一个智能厨房项目,需要精确测量食材重量。市面上现成的电子秤模块要么精度不够,要么价格太高,于是决定自己用STM32F103C8T6和HX711搭建一个高精度电子秤系统。这个组合特别适合创客和嵌入式开发者,成本不到50元,精度却能轻松达到0.1%级别。
选择STM32F103C8T6这块"蓝色小药丸"有几个原因:首先是性价比超高,20块钱就能买到正品;其次它自带硬件SPI和定时器,方便与HX711通信;最重要的是HAL库支持完善,开发效率高。HX711则是专为电子秤设计的24位ADC芯片,内部集成放大器,直接连接应变片就能用,省去了复杂的外围电路。
硬件连接非常简单:
- HX711的DT脚接PA1(任何GPIO都可)
- SCK脚接PA0
- VCC接3.3V
- GND共地
- 应变片的E+、E-接HX711的正负输入端
2. 开发环境搭建
我用的是STM32CubeIDE,这个IDE最大的好处是自带HAL库和图形化配置工具。新建工程时选择STM32F103C8T6,时钟配置为72MHz(外部8MHz晶振倍频)。关键是要开启两个外设:
- USART1:用于调试输出重量数据
- TIM2:用于实现微秒级延时
在CubeMX里配置GPIO时,PA0设为输出模式(推挽),PA1设为输入模式(上拉)。时钟树配置要注意APB1总线不要超过36MHz,否则定时器会出问题。生成代码后,记得在main.c里添加重定向printf的代码:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xff); return ch; }3. HX711驱动实现
HX711的通信协议比较特殊,不是标准的SPI或I2C,需要手动控制时序。在HX711.h中定义几个宏简化操作:
#define CLK_1 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET) #define CLK_0 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET) #define Read_PIN HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1)数据读取的核心在Get_number()函数。HX711每次输出24位数据,高位在前。关键是要在时钟下降沿读取数据,每个时钟周期保持至少1us的低电平:
unsigned long Get_number() { val = 0; CLK_0; while(!Read_PIN); // 等待数据就绪 for(int i=0; i<24; i++) { HAL_Delay_us(1); CLK_1; val = val << 1; HAL_Delay_us(1); CLK_0; if(Read_PIN) val++; HAL_Delay_us(1); } // 第25个脉冲设置增益 CLK_1; val = val ^ 0x800000; // 补码转换 HAL_Delay_us(1); CLK_0; return val; }这里有个坑要注意:很多网上的例程没有在读取后清零val变量,会导致后续数据异常。实测发现延时参数也很关键,太快会导致数据错位,太慢会影响采样率。
4. 数据校准与滤波处理
原始ADC值需要转换为实际重量。我的20kg应变片分度系数是103,在Get_Weight()函数中处理:
long Get_Weight(void) { HX711_Buffer = Get_number(); Weight = HX711_Buffer; Weight = (long)((float)Weight / 103); // 校准系数 return Weight; }校准步骤很关键:
- 空载时读取10次取平均值作为零点
- 放置已知重物(如500g砝码)
- 计算系数 = 原始ADC值 / 实际重量
- 重复三次取平均
主循环中加入了简单的滑动滤波:
while(1) { static long weights[5] = {0}; static int index = 0; weights[index] = -Get_Weight() + First_weight; index = (index + 1) % 5; long avg = 0; for(int i=0; i<5; i++) { avg += weights[i]; } printf("%ld\n", avg / 5); HAL_Delay(120); // 约10Hz更新率 }5. 常见问题排查
调试时遇到几个典型问题:
- 数据全为0:检查DT脚是否接触不良,HX711供电是否稳定
- 数据跳动大:尝试在VCC和GND之间加104电容,缩短传感器到HX711的导线
- 读数不稳定:确保应变片牢固粘贴,避免机械振动
- 通信超时:检查while(!Read_PIN)是否有超时退出机制
一个实用的调试技巧:先用示波器看SCK和DT波形,确认时序符合HX711规格书要求。如果没示波器,可以用LED闪烁指示通信状态:
if(Get_number() == 0) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); }6. 性能优化技巧
经过实测,这几个优化很有效:
- 将HAL_Delay_us()改为定时器实现,精度更高
- 在Get_Weight()中加入温度补偿算法
- 使用DMA传输USART数据,降低CPU占用
- 添加按键去皮功能:
if(HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET) { First_weight = Get_Weight(); HAL_Delay(200); // 消抖 }电源管理也很重要,HX711对电源噪声敏感,建议:
- 单独LDO给HX711供电
- 模拟和数字地之间加0欧电阻
- 电源走线尽量粗短
7. 扩展应用方向
这个基础框架可以扩展很多实用功能:
- 蓝牙传输:加个HC-05模块实现手机APP显示
- 数据记录:用SPI Flash存储历史称重数据
- 自动识别:通过重量变化模式识别不同食材
- 流量计算:结合时间戳计算粉末物料流量
比如实现简单的配方功能:
typedef struct { char name[20]; long target_weight; } Ingredient; Ingredient recipe[] = { {"面粉", 500}, {"糖", 200}, {"盐", 5} }; void check_recipe() { for(int i=0; i<3; i++) { while(Get_Weight() < recipe[i].target_weight) { printf("请添加%s...\n", recipe[i].name); HAL_Delay(1000); } printf("%s添加完成\n", recipe[i].name); } }8. 工程代码结构建议
好的代码组织能大幅提高可维护性,我的项目结构如下:
/Drivers /HX711 hx711.c hx711.h /Filters moving_avg.c /Utils delay.c /Application /Recipe recipe.c /Display lcd.c关键设计原则:
- 硬件驱动与业务逻辑分离
- 使用面向接口编程
- 重要参数集中配置
- 版本控制提交注释规范
比如将校准系数改为配置文件:
// config.h #define CALIB_FACTOR 103.0f #define MAX_WEIGHT 20000 // 20kg最后提醒,不同批次的应变片灵敏度可能有差异,建议每批都做单独校准。实际测试发现,温度每变化10℃,读数会漂移0.5%左右,对精度要求高的场合需要做温度补偿。
