尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32电子秤开发实战:HX711高精度称重与标定存储全解析

STM32电子秤开发实战:HX711高精度称重与标定存储全解析 简介基于STM32的电子秤项目压缩包定位为嵌入式入门与综合实践资源适合毕业设计、课程设计或工作项目参考。方案围绕STM32F103C8T6展开完整覆盖称重传感器模拟信号采集、ADC转换、滤波校准、LCD/OLED显示、按键交互与低功耗管理工程中使用了HAL库并保留定时器、I2C、UART等外设驱动便于理解软硬件协同设计。压缩包共2000个文件以715个qml界面文件、189个dll动态库、107个h头文件与59个c源码为主同时包含hex/bin固件、ioc引脚配置、CMake构建脚本及说明文档既方便阅读二次开发也可直接烧录验证整体体积122.73MB。目前已有275人学习下载对于希望快速掌握STM32传感器应用与嵌入式项目流程的学习者较有参考价值。1. 拿到 bin 文件之后拆开这个 STM32 电子秤工程压缩包里躺着一个DianZiChen.bin旁边是stm32f103c8t6_zhsx.bin和一堆 CMake 中间产物第一次打开这份“基于 STM32 的电子秤”工程包的人容易把 bin 直接烧进芯片就以为收工。但电子秤不是点亮一个 LED它要同时处理 HX711 的 24 位数据流、OLED 的 I2C 刷新、按键消抖还要在掉电前把标定系数写进 Flash。F103C8T6 这颗 64KB Flash、20KB RAM 的芯片能把这些事压在一个主循环里跑顺靠的正是每一条总线的时序收敛。这篇记录从信号采集一路写到烧录排错适合正在做基于 stm32 的毕业设计或者第一次把 HAL 库工程整合成实物的同学。2. HX711 数据采集为什么不用片内 ADC 直接读电桥称重传感器的输出是一个桥式差分信号直接进 STM32F103C8T6 的 ADC 并不划算。片内 ADC 虽然支持多通道扫描但 12 位分辨率、参考电压不可调面对满量程 2mV/V 的应变片输出称重范围会被压得很窄。所以绝大多数 stm32 电子秤项目会选择外接 HX711这颗 24 位 Sigma-Delta ADC 自带可控增益项目包里看到stm32f1xx_hal_tim.c就是给读取时序提供微秒级时基用的。2.1 传感器接线与增益选择HX711 和 STM32 之间只需要两根引脚不占用 ADC、SPI 或 I2C 外设。下面是我常用的接法DOUT 接 PB0SCK 接 PB1传感器电桥输出接 HX711 的 A 和 A-供电用数字 5V但注意传感器激励电压不能和 MCU 逻辑电源混在一起否则读数会跟着 USB 供电波动。HX711 引脚方向连接到 STM32F103C8T6说明DOUT输入PB0转换完成信号低电平表示数据可读SCK输出PB1移位时钟第 25 个脉冲选择通道和增益VCC/GND-5V / GND数字电源建议串磁珠隔离E/E--传感器电桥为应变片提供激励电压A/A-差分输入电桥输出差分毫伏信号输入HX711 的 A 通道增益是 128 或 64B 通道固定 64。电子秤常用 A 通道 128 倍增益这样 20kg 传感器在空载到满载之间能给出足够大的码值变化。读取数据时SCK 输出 24 个脉冲读回 24 位原始数据第 25 个脉冲负责切换下一次采样的通道和增益这个细节很容易被忽略。2.2 HAL 库下的 GPIO 模拟时序项目里的显示和通信都走 HAL 库HX711 的驱动也用 HAL 的 GPIO 接口写代码可移植性比较好。下面这段是完整的 24 位读取函数#define HX711_SCK_Pin GPIO_PIN_1 #define HX711_SCK_Port GPIOB #define HX711_DOUT_Pin GPIO_PIN_0 #define HX711_DOUT_Port GPIOB uint32_t HX711_Read(void) { uint32_t raw 0; // DOUT 为低表示一次转换完成等待周期一般在 100us 内 while (HAL_GPIO_ReadPin(HX711_DOUT_Port, HX711_DOUT_Pin) GPIO_PIN_SET) ; for (int i 0; i 24; i) { HAL_GPIO_WritePin(HX711_SCK_Port, HX711_SCK_Pin, GPIO_PIN_SET); delay_us(1); raw 1; if (HAL_GPIO_ReadPin(HX711_DOUT_Port, HX711_DOUT_Pin) GPIO_PIN_SET) { raw; } HAL_GPIO_WritePin(HX711_SCK_Port, HX711_SCK_Pin, GPIO_PIN_RESET); delay_us(1); } // 第 25 个时钟沿选择 A 通道增益 128 HAL_GPIO_WritePin(HX711_SCK_Port, HX711_SCK_Pin, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(HX711_SCK_Port, HX711_SCK_Pin, GPIO_PIN_RESET); return raw ^ 0x800000; }代码里delay_us(1)是通过stm32f1xx_hal_tim.c提供的定时器延时函数不要用HAL_Delay(1)因为那会放大到毫秒级SCK 频率太低反而让 HX711 内部滤波器把读数拉迟钝。raw ^ 0x800000是把二进制的补码结果转成无符号数否则负满量程的时候会出现一个很大的正数后面的标定计算会直接乱掉。读回来的原始值只是 24 位码值不是克。要得到稳定的重量必须先做滤波再和砝码做两点标定。2.3 滤波策略移动平均与中位值平均HX711 的读数在静止状态下也会有 ±5 到 ±20 个码值的跳动直接显示会不停闪。我一般先跑一个移动平均滤波把多次采样做累加取平均。代码量很小MCU 开销也很低#define FILTER_LEN 8 static uint32_t ring[FILTER_LEN]; static uint8_t index; uint32_t MovingAverage(uint32_t sample) { uint32_t sum 0; ring[index] sample; if (index FILTER_LEN) index 0; for (uint8_t i 0; i FILTER_LEN; i) sum ring[i]; return sum / FILTER_LEN; }这个环形缓冲没有清零初始化时为 0 会导致上电前几次读数偏低所以主循环启动时先连续读十几轮原始值填满缓冲再参与显示。下面是不同窗口长度的取舍实际需要根据传感器量程和采样速度调整滤波长度更新周期平滑效果适合场景8 次约 150ms仍有小幅跳动快速去皮、连续称重16 次约 300ms较稳定一般台秤32 次约 600ms很稳定静态称重不允许抖动如果现场有振动干扰比如旁边有变频器或电机移动平均会被尖峰脉冲拉偏。常见做法是改成“中位值平均滤波”先排序去掉最大最小再对剩余值求平均。这个方案在 stm32 项目里很常见因为系统架构简单每个周期也被压缩在几十微秒内不会拖慢主循环。3. OLED 显示、按键消抖与 UART 调试外设协同重量算出来之后要显示还要有按键去皮、切单位这一层最重要的不是代码能跑而是外设之间不能互相抢 CPU。项目包里出现了stm32f1xx_hal_i2c.c和stm32f1xx_hal_uart.c对应的就是 OLED 屏幕和调试串口。STM32 HAL 库在这种多外设场景下的优势很明显每个外设的句柄都是独立结构体初始化代码由 CubeMX 生成我们可以把注意力集中在业务逻辑上。3.1 用 HAL_I2C 驱动 SSD1306 显示屏OLED 屏的 I2C 地址绝大多数是 0x3C写成七位地址后是0x3C 1。SSD1306 的控制字节有两种0x00 表示后面是命令0x40 表示后面是显存数据。HAL 库封装好的HAL_I2C_Mem_Write正好适合做这件事它帮我们把起始信号、设备地址、控制字节和数据包都拼好了uint8_t OLED_WriteCmd(uint8_t cmd) { return HAL_I2C_Mem_Write(hi2c1, 0x3C 1, 0x00, 1, cmd, 1, 100); } uint8_t OLED_WriteData(uint8_t *data, uint16_t len) { return HAL_I2C_Mem_Write(hi2c1, 0x3C 1, 0x40, 1, data, len, 100); }调用时第一个参数是 I2C 句柄hi2c1第二个是器件地址第三个是控制字节第四个表示控制字节长度为 1 字节。最后的 100 是超时毫秒数调试阶段可以设大一些但不要超过主循环周期。注意 OLED 初始化序列必须在屏幕供电稳定后执行有些模块上电复位不够快程序跑到一半屏幕不亮复位引脚拉一下再初始化是最稳妥的。3.2 按键消抖定时器中断 状态机机械按键按下时会产生 5ms 到 20ms 的抖动如果直接用 GPIO 中断判断一次按下可能触发三四次。项目包里的stm32f1xx_hal_tim.c在这里就派上用场我一般让定时器 2 产生 10ms 的中断在中断里读按键电平连续读到低电平超过 50ms 才算稳定按下。这样既消抖又能在长按和短按之间做区分static uint8_t key_state 0; static uint16_t press_cnt 0; static uint8_t key_released 1; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { uint8_t level HAL_GPIO_ReadPin(KEY_Port, KEY_Pin); if (level GPIO_PIN_RESET) { press_cnt; if (press_cnt 50 key_state 0) { key_state 1; // 长按超过 500ms进入校准模式 } } else { if (press_cnt 2 press_cnt 50 key_released 0) { key_state 2; // 短按去皮或切换单位 } press_cnt 0; key_released 1; } } }这里把按键事件放到状态变量key_state里主循环检测到这个变量再执行去皮或校准不在中断里做重活。这样设计的好处是后续如果想接蓝牙或 485 通信按键逻辑不会被串口阻塞整个系统的实时性也更好。3.3 UART 调试输出与串口调参思路裸机开发最怕看不到中间变量。HX711 原始值、滤波后的码值、标定后的克数这些数据如果只靠 OLED 轮询显示调参效率很低。项目包里的stm32f1xx_hal_uart.c对应的是调试串口常见做法是将串口 1 重定向为printf在使用 HAL 库时只需要重写fputcint fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 100); return ch; }之后在代码里直接printf(raw:%lu filter:%lu\r\n, raw, filtered);用 115200 波特率打开串口工具就能实时看到数据。如果你在调重量闭环或电机反馈可以在上位机里画波形这和 stm32 串口调试 PID 的思路完全一致先看原始曲线再调整滤波和标定参数而不是盲目改代码。外设引脚句柄用途I2C1PB6/PB7hi2c1SSD1306 OLEDUSART1PA9/PA10huart1调试数据输出TIM2内部htim210ms 按键扫描基准4. 标定与 Flash 存储从原始码值到克重HX711 读回来的值没有任何物理意义只有先拿到零点和标准砝码点才能建立“码值→克”的线性关系。这个环节做不好后面显示得再漂亮也没用。4.1 两点标定算法标定的数学非常简单空秤时读一个码值zero_raw放上已知重量的砝码再读一个load_raw然后用这两点算出一个线性映射。公式、代码和校验逻辑要放在一起写避免在手工调试时按下一次砝码就覆盖掉之前的标定static float offset 0.0f; static float scale 1.0f; void Calibrate(uint32_t zero_raw, uint32_t load_raw, float load_kg) { offset (float)zero_raw; scale ((float)load_raw - zero_raw) / load_kg; } float Weight_Get(uint32_t filtered_raw) { return ((float)filtered_raw - offset) / scale; }这里的scale不是增益倍数而是“每克重量对应多少码值”。比如 20kg 传感器满量程码值差在 800000 左右那么scale大约是 40也就是说每增加 1 克滤波后的码值增加 40。如果算出来这个数小于 10说明传感器量程和 HX711 增益不匹配或者供电激励电压太低需要回到硬件上去查。4.2 标定参数写入 STM32 内部 Flash标定参数存在 RAM 里掉电就没了。电子秤每次上电都重新标定不现实所以要把系数写进 STM32 内部 Flash。F103C8T6 的 Flash 从0x08000000开始总共 64KB程序一般从 0 地址烧录我把校准参数放在最后一页0x0800FC00这样即使代码扩充到 60KB 也不会覆盖到参数区。#define CAL_ADDR 0x0800FC00 void Save_Calibration(float offset_val, float scale_val) { HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR); FLASH_EraseInitTypeDef erase {0}; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress CAL_ADDR; erase.NbPages 1; uint32_t error 0; HAL_FLASHEx_Erase(erase, error); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, CAL_ADDR, *(uint32_t *)offset_val); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, CAL_ADDR 4, *(uint32_t *)scale_val); HAL_FLASH_Lock(); }写 Flash 之前必须擦除整页F103 的页大小是 1KB。另外注意HAL_FLASH_Program的写入地址必须按 4 字节对齐数据要转成uint32_t指针再取内容。如果直接传float变量编译会报类型警告而且写入后读回来可能是一个错乱值。上电读取时不要只读偏移和斜率还要加一个 Magic Number 作校验#define CAL_MAGIC 0xA5A5A5A5 bool Load_Calibration(void) { uint32_t magic *(volatile uint32_t *)CAL_ADDR; if (magic ! CAL_MAGIC) return false; offset *(float *)(CAL_ADDR 4); scale *(float *)(CAL_ADDR 8); return true; }如果 Magic Number 不匹配说明 Flash 是空的或者标定数据被破坏这时候强制进入标定模式防止用户拿到一台“零位飘走”的秤却不知道哪里出了问题。4.3 温漂与上电自检应变片传感器的零位会随温度缓慢漂移常见做法是在Weight_Get返回前做一次零点跟踪如果当前滤波值在零点附近连续 N 次变化小于阈值就把当前值更新为零点。这个思路很朴素但要注意避免在放置重物时误触发通常需要同时判断“重量小于 2% 满量程”和“连续 10 次变化小于 3 个码”。上电自检则是另一个容易漏掉的环节。启动时先读一次 HX711如果 DOUT 一直为高超过 500ms说明传感器没接好OLED 上直接显示E1。把自检放到主循环开头比烧录后再拿万用表捅引脚直观得多。5. 固件烧录与现场排错bin 文件不是烧进去就能跑工程包里的DianZiChen.bin是编译产物烧录方式有很多种但新手最容易卡在“烧录成功但没反应”。这一节把烧录命令和排查路径都收拢在一起。5.1 用 STM32 工具链烧录 binF103C8T6 支持 SWD 接口用 ST-Link 或 J-Link 都能烧。我习惯用 STM32CubeProgrammer 的命令行工具脚本化之后复位和校验一步到位STM32_Programmer_CLI -c portSWD -w DianZiChen.bin 0x08000000 -v -rst-v是烧录后校验-rst是烧完自动复位运行。如果手边只有 J-Flash也能直接把DianZiChen.bin加载到0x08000000连上 SWD 后执行 program 和 verify。区别不大但要注意.bin文件没有地址信息必须手工指定起始地址写错了固件起不来。stm32f103c8t6_zhsx.bin这个文件如果和主 bin 一样大可能是同一份程序的另一版本不要同时烧两个。5.2 如何判断程序是否真的在跑烧录成功的提示并不能代表业务逻辑正确。上电后先看三处OLED 有没有初始化画面HX711 的 DOUT 有没有周期性拉低串口有没有打印数据。如果屏幕不亮但串口有输出重点检查 I2C 引脚配置和 OLED 地址如果 HX711 的 DOUT 一直高先量传感器激励电压和 A/A- 差分电压如果全都没反应先用调试器在main入口打一个断点。症状可能原因处理方式烧录成功但 SWD 找不到芯片程序里禁用了 JTAG/SWD 引脚拉高 BOOT0 后上电用 ISP 擦除整片OLED 不亮但串口正常I2C 地址错或初始化时序不对用逻辑分析仪抓 SCL/SDA确认 ACKHX711 读数恒定不变传感器接线错误或 HX711 供电异常先短路 A A-读数应接近 0x800000重量数字来回跳滤波长度过短或激励电源不稳加长滤波窗口传感器电源单独滤波5.3 几个容易踩的实际问题HAL 库工程最常见的卡死点是HAL_Delay进不去原因是 SysTick 中断优先级被改了或者某处关中断时间过长。另一个是 STM32 禁用 JTAG 引脚如果程序把 PA13/PA14 配置为普通 GPIOSWD 第二次就无法连接。遇到这种情况不用慌BOOT0 拉高重启芯片进入系统存储器擦掉 Flash 就能恢复。I2C 总线锁死也很典型OLED 拔插时 SDA 被拉低后续所有通信全部挂起。恢复方法是把 SCL 手动翻转 9 个时钟脉冲释放总线再把外设重新初始化。项目包里既然带了stm32f1xx_hal_i2c.c和stm32f1xx_hal_uart.c我建议把 I2C 错误回调里的报错信息通过串口打出来这样调试时少走很多弯路。本文还有配套的精品资源点击获取
返回列表