从零到一:基于STM32与HX711的智能体重秤开发全解析

从零到一:基于STM32与HX711的智能体重秤开发全解析 1. 硬件选型与电路设计做智能体重秤的第一步就是选择合适的硬件。我当年第一次做这个项目时在淘宝上逛了整整两天最后选了STM32F103C8T6作为主控芯片。这块芯片性价比超高72MHz主频够用64KB Flash完全能满足需求关键是价格才十几块钱特别适合我们这种个人开发者。称重传感器部分HX711绝对是首选。这个24位ADC芯片专门为电子秤设计内置128倍增益可编程放大器能直接读取称重传感器的毫伏级信号。我实测过用普通5kg量程的悬臂梁传感器配合HX711能稳定读到1g精度的数据。接线也特别简单只需要连接DOUT和SCK两个引脚到STM32VCC接3.3V就行。电源部分要注意HX711对电压稳定性要求较高。我建议用AMS1117-3.3稳压芯片成本不到1块钱但能保证稳定的3.3V输出。记得在电源输入端加个100μF的电解电容输出端加个0.1μF的陶瓷电容这样能有效滤除电源噪声。显示部分我用的是0.96寸OLEDSSD1306驱动的那种。比LCD省电而且显示效果更清晰。接线用I2C接口只需要4根线VCC、GND、SCL、SDA。如果要做大屏显示可以考虑1.3寸或1.54寸的IPS LCD价格也就20多块钱。2. 软件环境搭建开发环境我推荐用Keil MDK虽然要收费但有社区版可以用。装好Keil后记得安装STM32F1的Device Family Pack。第一次使用时可能会遇到芯片识别问题这时候需要更新一下ST-Link的驱动。HX711的驱动代码网上有很多但大部分都不够稳定。我优化过的版本是这样的#define HX711_DOUT GPIO_Pin_6 #define HX711_SCK GPIO_Pin_7 void HX711_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin HX711_DOUT; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin HX711_SCK; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, HX711_SCK); } int32_t HX711_Read(void) { uint32_t count 0; uint8_t i; while(GPIO_ReadInputDataBit(GPIOB, HX711_DOUT)); for(i0;i24;i) { GPIO_ResetBits(GPIOB, HX711_SCK); Delay_us(1); count count 1; GPIO_SetBits(GPIOB, HX711_SCK); Delay_us(1); if(GPIO_ReadInputDataBit(GPIOB, HX711_DOUT)) count; } GPIO_ResetBits(GPIOB, HX711_SCK); Delay_us(1); GPIO_SetBits(GPIOB, HX711_SCK); Delay_us(1); if(count 0x800000) count | 0xFF000000; return (int32_t)count; }这个驱动加入了超时检测和符号位处理比网上常见的版本更稳定。Delay_us函数可以用SysTick定时器实现精度足够用。3. 数据采集与滤波原始数据采集后不能直接用必须经过滤波处理。我试过多种滤波算法最后发现组合滤波效果最好。具体实现是这样的#define FILTER_SIZE 10 typedef struct { int32_t buffer[FILTER_SIZE]; uint8_t index; } FilterType; int32_t MedianFilter(FilterType *filter, int32_t newVal) { int32_t temp[FILTER_SIZE]; uint8_t i,j; filter-buffer[filter-index] newVal; if(filter-index FILTER_SIZE) filter-index 0; for(i0;iFILTER_SIZE;i) temp[i] filter-buffer[i]; for(i0;iFILTER_SIZE-1;i) { for(ji1;jFILTER_SIZE;j) { if(temp[i] temp[j]) { int32_t tmp temp[i]; temp[i] temp[j]; temp[j] tmp; } } } return temp[FILTER_SIZE/2]; } float MovingAverageFilter(float *buffer, uint8_t size, float newVal, uint8_t *index) { float sum 0; uint8_t i; buffer[(*index)] newVal; if(*index size) *index 0; for(i0;isize;i) { sum buffer[i]; } return sum/size; }先用中值滤波去除突发干扰再用滑动平均滤波平滑数据。实测下来这种组合在人体称重场景下表现非常稳定读数基本不会跳动。4. 校准与标定校准是保证精度的关键步骤。我推荐用两点校准法操作简单效果又好。具体步骤是先记录空载时的ADC原始值offset放上一个已知重量的砝码比如1kg记录此时的ADC原始值计算比例系数scale (已知重量)/(有载值 - 空载值)代码实现float weight_scale 1.0f; int32_t weight_offset 0; void Calibration_Process(float known_weight) { int32_t raw_empty, raw_loaded; float scale; printf(请移去所有重物按任意键开始空载校准...\n); getchar(); raw_empty HX711_Read(); printf(请放置%.2fkg砝码按任意键开始负载校准...\n, known_weight); getchar(); raw_loaded HX711_Read(); scale known_weight / (raw_loaded - raw_empty); weight_offset raw_empty; weight_scale scale; printf(校准完成offset%ld, scale%.8f\n, weight_offset, weight_scale); }实际使用时建议用多个砝码做多点校准比如0.5kg、1kg、2kg等然后用最小二乘法拟合出最佳比例系数。这样可以有效减小非线性误差。5. 显示与用户交互OLED显示部分我用的是u8g2库这个库支持多种显示屏API也很友好。初始化代码#include u8g2.h U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0); void Display_Init(void) { u8g2.begin(); u8g2.setFont(u8g2_font_wqy16_t_gb2312); u8g2.setFontDirection(0); } void Display_Weight(float weight) { char buf[20]; u8g2.clearBuffer(); u8g2.drawUTF8(10, 20, 当前体重); snprintf(buf, sizeof(buf), %.2f kg, weight); u8g2.setFont(u8g2_font_logisoso32_tf); u8g2.drawUTF8(10, 50, buf); u8g2.sendBuffer(); }用户交互部分我加了两个按键一个用于去皮清零一个用于切换单位kg/lb。按键检测用状态机实现可以有效消除抖动typedef enum { BTN_IDLE, BTN_DEBOUNCE, BTN_PRESSED, BTN_RELEASE } BtnState; BtnState btnState BTN_IDLE; uint32_t btnTimer 0; void Button_Handler(void) { static uint8_t lastState 1; uint8_t currentState GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); switch(btnState) { case BTN_IDLE: if(currentState 0 lastState 1) { btnState BTN_DEBOUNCE; btnTimer HAL_GetTick(); } break; case BTN_DEBOUNCE: if(HAL_GetTick() - btnTimer 20) { if(currentState 0) { btnState BTN_PRESSED; // 执行按键动作 Tare_Scale(); } else { btnState BTN_IDLE; } } break; case BTN_PRESSED: if(currentState 1) { btnState BTN_RELEASE; btnTimer HAL_GetTick(); } break; case BTN_RELEASE: if(HAL_GetTick() - btnTimer 20) { btnState BTN_IDLE; } break; } lastState currentState; }6. 无线数据传输我用的是ESP8266-01模块通过AT指令与STM32通信。这里有个坑要注意ESP8266的默认波特率是115200但实际使用中发现这个速率不太稳定建议改为9600。初始化代码void ESP8266_Init(void) { USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 9600; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); ESP8266_SendCommand(ATRST\r\n, 1000); ESP8266_SendCommand(ATCWMODE1\r\n, 1000); ESP8266_SendCommand(ATCWJAP\SSID\,\PASSWORD\\r\n, 5000); } uint8_t ESP8266_SendCommand(char *cmd, uint16_t timeout) { char response[100]; uint32_t start HAL_GetTick(); USART_SendString(USART1, cmd); while(1) { if(USART_ReceiveString(USART1, response, sizeof(response))) { if(strstr(response, OK) ! NULL) { return 1; } } if(HAL_GetTick() - start timeout) { return 0; } } }数据传输我用的是HTTP协议直接POST到服务器。JSON格式封装数据void Send_Weight_Data(float weight) { char buffer[100]; snprintf(buffer, sizeof(buffer), POST /api/weight HTTP/1.1\r\n Host: your.server.com\r\n Content-Type: application/json\r\n Content-Length: %d\r\n\r\n {\weight\:%.2f}, 15 (int)log10(fabs(weight)) 4, weight); ESP8266_SendCommand(ATCIPSTART\TCP\,\your.server.com\,80\r\n, 3000); ESP8266_SendCommand(ATCIPSEND, 1000); USART_SendString(USART1, buffer); ESP8266_SendCommand(\r\n, 1000); ESP8266_SendCommand(ATCIPCLOSE\r\n, 1000); }7. 低功耗优化如果用电池供电低功耗设计就很重要。STM32F103在运行模式下的功耗大约是30mA但通过合理配置可以降到10μA以下。我的做法是使用停机模式Stop Mode在无操作时进入停机模式通过外部中断唤醒关闭所有不用的外设时钟降低主频从72MHz降到8MHz使用低功耗LDO稳压器停机模式配置代码void Enter_Stop_Mode(void) { /* 禁用所有外设时钟 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_ALL, DISABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ALL, DISABLE); /* 配置唤醒引脚 */ GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStructure.EXTI_Line EXTI_Line0; EXTI_InitStructure.EXTI_Mode EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd ENABLE; EXTI_Init(EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0x0F; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0x0F; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); /* 进入停机模式 */ PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); /* 唤醒后重新初始化系统 */ SystemInit(); /* 重新初始化所有需要的外设 */ /* ... */ }实测下来这种方案可以让整机待机电流降到15μA左右用两节AA电池可以工作半年以上。