简介:基于STM32F103C8T6单片机的智能手环功能实现方案,面向嵌入式初学者、高校毕业设计或课程设计学生,以及可穿戴设备开发爱好者,覆盖运动计步与健康监测场景。硬件集成ADXL345三轴加速度传感器、心率检测模块和数字温度传感器,软件实现步数统计、步行距离换算、平均速度计算,并同步完成心率与体表温度连续监测,所有测量结果由LCD1602字符液晶屏分类动态显示。压缩包共92个文件,整体仅454KB,包含38个头文件与37个C源码文件,形成Keil MDK标准分层工程(CORE/SYSTEM/HARDWARE);另有完整原理图SchDoc、PCB版图PDF、设计说明文档、功能清单TXT、可烧录HEX、demo.py辅助脚本及keilkilll.bat一键清理脚本,组织结构清晰、轻量易读。已有64人学习下载。代码基于标准外设库,传感器驱动与显示逻辑分离,便于逐模块调试与二次开发;资料配套完整,既可直接烧录验证,也可作为STM32外设驱动、低功耗手环逻辑和项目文档撰写的参考底稿。
1. STM32F103C8T6手环主控工程:ADXL345计步、心率、体温加上LCD1602,一条传感链路到底值不值得做
在可穿戴设备的开发检索里,STM32F103C8T6手环主控工程被搜到的频率一直很高,原因很直接:它把嵌入式里最值得练的几件事——传感器驱动、信号滤波、数据融合、屏幕显示——收进了一个能戴在手上的小系统。ADXL345负责计步,光电式心率传感器测脉搏,NTC热敏电阻测体温,LCD1602把这三组数据落屏。对刚学完定时器和串口的开发者来说,这是从流水灯迈向完整产品的第一道门槛;对做方案预研的工程师来说,它又是一套验证器件选型和算法可行性的低成本底座。这篇文章按我实际做可穿戴原型机的路子,把原理图、驱动、计步算法和显示调度一起讲透,连最容易翻车的细节也标出来。
2. 原理图设计三处关键选型:供电、I2C总线和NTC分压
2.1 器件选型:为什么是F103C8T6、ADXL345、光电心率模块和LCD1602
STM32F103C8T6是Cortex-M3核,72MHz,64KB Flash,20KB RAM,没有FPU。这个配置对手环来说不算宽裕,但恰好卡在“够用又需要动脑子”的位置:计步和心率滤波要尽量用定点运算或快速近似,这能帮你提前建立对嵌入式算力边界的感知。外设上它有2个I2C、3个SPI、1个ADC,挂三个传感器加一块屏幕,不需要刻意省资源。
ADXL345是学习级三轴加速度计里资料最全的一颗:I2C和SPI双接口,量程±2g到±16g可编程,输出数据率可设。选它而不是MPU6050,关键原因是它没有陀螺仪,功耗更低,而计步只需要加速度,不需要做姿态融合;MPU6050也不是不行,但代码复杂度会明显上一个台阶。
心率模块我建议用光电式PPG方案,常见模块是红光加红外双LED加光电二极管,通过I2C输出数字信号。它的好处是和ADXL345共用一条I2C总线,地址不冲突就能挂上去。体温部分直接用NTC热敏电阻,不要用DS18B20这类数字温度传感器:NTC可以贴着皮肤用PCB铜箔感温,响应更快,成本也更低,而且通过电阻分压接到STM32的ADC引脚即可,不占用I2C地址。
LCD1602看似老,实则在调试阶段比OLED更实用,因为它不依赖全屏刷新,显示信息密度高,把步数、心率和体温一屏放下,成本只有OLED的一半。搭配PCF8574 I2C转接板后只占SCL、SDA两根线,整机走线很清爽。下表是我常用的引脚分配:
| 信号 | 器件 | 引脚 | 说明 |
|---|---|---|---|
| SCL | ADXL345、心率模块、LCD转接板 | PB6 | I2C1时钟线 |
| SDA | ADXL345、心率模块、LCD转接板 | PB7 | I2C1数据线 |
| ADC输入 | NTC分压节点 | PA0 | ADC1通道0 |
| 电源 | 全部器件 | 3.3V轨 | 每颗IC就近0.1uF去耦 |
2.2 供电与去耦:3.3V电源轨、BOOT0和I2C上拉电阻的规范接法
整个系统统一用3.3V。USB供电时先降压,常用AMS1117-3.3或RT9193,后者压差更小、静态功耗更低。原理图里最容易埋雷的是三个地方。
第一,I2C总线上挂了三个从机,上拉电阻放一组就好,不要每个模块板都自带一组上拉,否则等效并联阻抗可能低于2kΩ,低电平拉不下去,通信会随机失败。我一般取4.7kΩ,总线较长时降到2.2kΩ。
第二,BOOT0引脚必须用10kΩ电阻下拉到GND。很多复刻工程把这个引脚悬空,上电后程序不跑,还以为是晶振问题。F103的BOOT0悬空时电平不确定,这是玄学问题里最常见的一个。
第三,48pin封装的F103把VREF+和VDDA内部连在一起,所以VDDA要单独滤波,用10Ω电阻或磁珠从3.3V引过来,再并一个1uF和一个0.1uF电容。这个细节直接影响ADC采样稳定性和心率模拟信号质量。
每颗芯片的电源引脚都放0.1uF去耦电容,并尽可能靠近引脚,这不是可选项,是可靠性的底线。
2.3 NTC体温采样电路:分压比、阻值范围与ADC分辨率核算
NTC我用最通用的10kΩ、B值3950。分压结构取“固定电阻在上、NTC在下”:3.3V先经过10k固定电阻,中间节点接PA0,再经过NTC到地。温度升高时NTC阻值下降,节点电压也随之下降,换算关系更直观。
人体体温范围35℃到42℃内,这只10k NTC的阻值大约在6.5k到4.9k之间,分压节点电压约1.30V到1.08V。12位ADC在3.3V参考下每LSB约0.8mV,算下来这个区间约有275个LSB,温度分辨率可以做到0.1℃以内,完全够用手环显示。
需要注意的是ADC引脚的采样稳定时间。分压节点阻抗约在3k到4k,ADC内部采样电容充电需要一点时间,我习惯在每次切换通道后延时50us再启动转换,同时在PA0与地之间放一个0.1uF电容,构成简单低通滤波,抑制手机射频和电源高频干扰。
3. ADXL345计步落地:I2C驱动、加速度合成与峰值检测算法
3.1 初始化与三轴读取:寄存器配置与I2C连续读
ADXL345的I2C地址由SDO引脚决定,SDO接GND时是0x53,接VCC时是0x1D。初始化第一步读DEVID寄存器,固定值0xE5,读不到就说明链路不通,先去查地址和上拉电阻。
#include "stm32f1xx_hal.h" #include <math.h> #define ADXL345_ADDR 0x53 // SDO接地时7bit地址 #define ADXL345_DEVID 0x00 #define ADXL345_BW_RATE 0x2C #define ADXL345_POWER_CTL 0x2D #define ADXL345_DATAX0 0x32 static uint8_t adxl_read_reg(uint8_t reg) { uint8_t val = 0; HAL_I2C_Mem_Read(&hi2c1, ADXL345_ADDR << 1, reg, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); return val; } static void adxl_write_reg(uint8_t reg, uint8_t val) { HAL_I2C_Mem_Write(&hi2c1, ADXL345_ADDR << 1, reg, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); } int adxl345_init(void) { if (adxl_read_reg(ADXL345_DEVID) != 0xE5) { return -1; // I2C通信失败,检查接线或地址 } // DATA_FORMAT=0x01:±4g量程,10位分辨率 adxl_write_reg(0x31, 0x01); // 输出率100Hz,带宽50Hz adxl_write_reg(ADXL345_BW_RATE, 0x0A); // 退出待机,进入测量模式 adxl_write_reg(ADXL345_POWER_CTL, 0x08); return 0; }逻辑说明:DEVID是只读辨识寄存器,能读回0xE5说明I2C物理层通了。DATA_FORMAT写0x01选择±4g量程,相比默认±2g,给手腕摆动留出更大动态余量,避免削顶。BW_RATE写0x0A对应100Hz输出率,这个采样率对1.5Hz到2.2Hz的步频有足够余量,芯片内部还能做抗混叠滤波。POWER_CTL写0x08是测量模式,如果写0则上电后停留在待机模式,读出来的三个轴全是0。
读取三轴数据时用连续读6字节,从DATAX0开始,一次I2C事务拿回全部三轴,避免反复访问:
typedef struct { int16_t x; int16_t y; int16_t z; } acc_t; acc_t adxl345_read(void) { uint8_t buf[6]; acc_t a; HAL_I2C_Mem_Read(&hi2c1, ADXL345_ADDR << 1, ADXL345_DATAX0, I2C_MEMADD_SIZE_8BIT, buf, 6, 100); a.x = (buf[1] << 8) | buf[0]; a.y = (buf[3] << 8) | buf[2]; a.z = (buf[5] << 8) | buf[4]; return a; }逻辑说明:0x32是DATAX0寄存器,连续读会自动地址自增。10位模式下数据以补码存进16位,高6位是符号扩展。±4g量程下每LSB对应7.8mg,后面计算合成加速度时要按这个比例换算成g值。
3.2 计步算法:合成加速度、高通滤波与上下沿确认
计步不能只看某一个轴,因为手环在手腕上的姿态会变,单轴信号随朝向不同会失效。标准做法是取三轴合成加速度:
a_mag = sqrt(x^2 + y^2 + z^2)
合成值包含约1g的重力分量和叠加的摆动分量。先做高通滤波去掉重力,再对滤波后的信号做峰值检测:
#define ALPHA_HP 0.85f float hp_filter(float input, float *prev) { *prev = ALPHA_HP * (*prev + input - *prev); return input - *prev; }逻辑说明:这个一阶高通是先做低通再与原信号相减,工程上直观且稳定。ALPHA_HP取值0.85对应100Hz采样率下的截止频率约2Hz,能把静止时的低频漂移滤掉,同时保留1.5Hz到2.2Hz的步频。如果采样率改成50Hz,这个系数要调到0.75附近,否则截止频率偏移,低幅摆手会被放大成伪步。
峰值检测我用“波峰—波谷成对确认”,而不是单点过阈值,这样能滤掉大部分毛刺:
#define PEAK_THRESHOLD 0.9f // 合成加速度峰值门限,单位g #define STEP_MIN_MS 250 // 步间隔下限,对应240步/分钟 #define STEP_MAX_MS 1200 // 步间隔上限,对应50步/分钟 static float acc_magnitude(acc_t a) { float xg = a.x / 128.0f; // ±4g量程,每g对应128 LSB float yg = a.y / 128.0f; float zg = a.z / 128.0f; return sqrtf(xg * xg + yg * yg + zg * zg); } uint32_t step_count = 0; static uint8_t detect_state = 0; void step_detect(float mag, uint32_t now_ms) { static float last_peak = 0, last_valley = 0; static uint32_t last_step_ms = 0; if (detect_state == 0) { if (mag > PEAK_THRESHOLD && mag > last_peak) { last_peak = mag; } else if (mag < last_peak - 0.15f) { detect_state = 1; last_valley = mag; } } else { if (mag < last_peak - 0.30f && mag < last_valley) { last_valley = mag; } else if (mag > last_valley + 0.15f) { uint32_t gap = now_ms - last_step_ms; if (gap >= STEP_MIN_MS && gap <= STEP_MAX_MS) { step_count++; last_step_ms = now_ms; } detect_state = 0; last_peak = 0; } } }逻辑说明:进入找峰状态后,持续更新最高点,直到信号从峰值回落0.15g,认为峰已经过去,转入找谷。找谷同样要求信号比峰值低0.3g,再从谷底回升0.15g,这才确认一个完整摆动周期。两个0.15g的回跳阈值过滤掉了幅度小于0.3g的抖动。步距窗口用毫秒而不是样本数,避免采样率改变后参数失效。
参数说明:PEAK_THRESHOLD取0.9g对应正常走路,慢走时摆动小可以降到0.7g,跑步时摆动大可以升到1.2g。回落阈值0.15g偏灵敏,如果出现多记步,把它改成0.25g让判定更钝。
3.3 三个必调参数:阈值、回落宽度和步距时间窗
计步不准的原因多数不在算法复杂,而在参数和佩戴方式不匹配。
第一个必调参数是合成加速度峰值阈值。它要和量程联动。±4g量程下0.9g合理,如果改回±2g量程,同样的摆动换算成g值会偏大,阈值要相应提高,否则每次摆手都被算成一步。
第二个是回落宽度。走路时波形平滑,0.15g的回落宽度没有问题;但如果手环戴得松,传感器会出现高频抖动,回落宽度要加大到0.25g甚至0.3g。判断依据是看串口打出的原始mag波形,如果波形在峰值附近有连续毛刺,就加宽。
第三个是步距时间窗。我将STEP_MIN_MS设为250,STEP_MAX_MS设为1200,对应每分钟50到240步。跑步时步频可能到180到200,仍在窗口内。如果做的是老人步态检测,建议把下限放宽到300,把上限提高到1500,否则慢步会被过滤掉。
静止漂移也是隐蔽问题。手环戴在手腕上静止时,肌肉微颤会产生约0.3g的小波动,双沿判定已经能挡掉大半。如果在地铁或公交车上,车身振动频率和步频接近,会出现虚步。这种场景原型阶段不用强纠,产品上一般会加入连续两步周期一致性判断。
4. 心率与体温采集:PPG信号链和ADC信号链的完整实现
4.1 心率传感器初始化与PPG读取
心率模块内部原理是红光和红外LED交替照射皮肤,光电二极管接收反射光,血液搏动引起反射光强变化,形成光电容积脉搏波(PPG)。模块通过I2C输出原始PPG采样值,STM32负责从波形中提取心率。市面上常见模块寄存器地址基本相近,但不同批次可能有差异,初始化前先确认模块手册。
常见的初始化流程是复位芯片、配置采样率和LED脉宽:
#define HR_SENSOR_ADDR 0x57 int hr_init(void) { uint8_t val; // 复位模块,bit6置1触发软复位 val = 0x40; HAL_I2C_Mem_Write(&hi2c1, HR_SENSOR_ADDR << 1, 0x09, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); HAL_Delay(100); // 配置采样率约50Hz,LED脉宽约411us val = 0x03; HAL_I2C_Mem_Write(&hi2c1, HR_SENSOR_ADDR << 1, 0x08, I2C_MEMADD_SIZE_8BIT, &val, 1, 100); return 0; }逻辑说明:复位寄存器地址0x09写0x40是软复位,等待100ms让内部上电时序稳定。工作配置寄存器地址0x08写0x03,意思是50Hz采样率和中等LED电流。50Hz对心率信号足够,心率基频在1Hz到2Hz左右。
读取PPG时要关注FIFO溢出。模块内部有FIFO缓存多组数据,如果读取不及时,新数据会覆盖旧数据,波形出现断点。我的经验是每20ms读一次,并检查溢出标志,溢出时先清FIFO再继续。
4.2 峰值间隔法计算心率与滑动窗口滤波
完整的心率算法用FFT做频谱估计在F103上能做,但对原型项目来说不划算,一是M3没有FPU,二是窗口数据要反复搬运。工程上更常见的是峰值间隔法:检测PPG波形的波峰,记录相邻两个波峰的时间间隔,再用60000除以间隔得到瞬时心率,做滑动平均。
#define HR_WINDOW_SIZE 10 static uint16_t hr_buf[HR_WINDOW_SIZE] = {0}; static uint8_t hr_buf_idx = 0; static uint32_t last_peak_ms = 0; static uint8_t rising_edge = 0; uint16_t hr_value = 0; void ppg_process(uint32_t ppg_value, uint32_t now_ms) { // 典型模块原始值范围在20000到80000之间 // 实际阈值需要根据静态基线调整,不能写死 if (ppg_value > 55000 && rising_edge == 0) { rising_edge = 1; if (last_peak_ms != 0 && (now_ms - last_peak_ms) > 300) { uint32_t interval = now_ms - last_peak_ms; uint16_t bpm = 60000 / interval; if (bpm > 40 && bpm < 220) { hr_buf[hr_buf_idx] = bpm; hr_buf_idx = (hr_buf_idx + 1) % HR_WINDOW_SIZE; uint32_t sum = 0, cnt = 0; for (uint8_t i = 0; i < HR_WINDOW_SIZE; i++) { if (hr_buf[i] > 0) { sum += hr_buf[i]; cnt++; } } if (cnt >= 5) { hr_value = sum / cnt; } } } last_peak_ms = now_ms; } else if (ppg_value < 45000) { rising_edge = 0; } }逻辑说明:这里用了高阈值和低阈值构成滞回比较,避免信号在阈值附近反复震荡。55000和45000是某类模块的常见初值,实际佩戴时基线因人种、肤色、松紧和光照差异很大,必须先用串口打印一段原始PPG数据,观察波形基线后再设定。
滑动窗口长度10对50Hz采样率来说覆盖了2秒的数据。心率更新滞后约1秒,正好抵消呼吸性心率波动的影响,显示值不会来回跳。
4.3 NTC体温换算:B值公式、滑动平均与电压极性反转
体温采样用ADC1通道0,NTC分压节点接PA0。分压结构是固定电阻在上、NTC在下,温度升高时NTC阻值下降,节点电压下降,这是反极性信号,换算时要注意方向。
#define NTC_SERIES_RES 10000.0f // 分压固定电阻 #define NTC_NOMINAL 10000.0f // 25℃时NTC阻值 #define NTC_B_VALUE 3950.0f float read_ntc_temperature(void) { uint16_t adc_val = 0; HAL_ADC_Start(&hadc1); if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) { adc_val = HAL_ADC_GetValue(&hadc1); } float vout = adc_val / 4095.0f * 3.3f; // 固定电阻在上,NTC在下: // Rntc = Rfixed * Vout / (3.3 - Vout) float rntc = NTC_SERIES_RES * vout / (3.3f - vout); // B值公式:1/T = 1/T0 + (1/B) * ln(R/R0) float ln_r = logf(rntc / NTC_NOMINAL); float t_kelvin = 1.0f / (1.0f / 298.15f + 1.0f / NTC_B_VALUE * ln_r); return t_kelvin - 273.15f; }逻辑说明:B值公式是厂商给出的标准方程,比Steinhart-Hart三系数方程更适合原型阶段,因为不需要额外标定。注意分子位置的vout不能为0,所以NTC分压节点不能直接短路到地。
参数说明:如果买到的是100k NTC,NTC_NOMINAL要改成100000,NTC_SERIES_RES也要配套换成100k。100k方案在人体温区节点阻抗更高,对走线噪声更敏感,原型阶段我还是首推10k。
体温数据做一个简单的滑动平均,连续采5次去掉最大最小再平均,能滤掉接触不良造成的尖峰。如果体温显示持续漂移,优先检查是不是手指按压导致局部温度累积,而不是怀疑算法。
5. LCD1602显示整合与排查避坑:5个高发问题和根因
5.1 LCD1602 I2C驱动:PCF8574转接板的底层时序
LCD1602原生并行接口需要6个IO,加上传感器占用的I2C和ADC,IO会紧张。带PCF8574转接板的LCD1602模块把8位并行转成I2C,只占SCL和SDA两条线,而且能和ADXL345、心率模块共用同一条I2C总线。
PCF8574的输出映射是:P7到P4接LCD的D7到D4,P3接背光,P2接EN,P1接RW,P0接RS。写数据时要先发高4位脉冲,再发低4位脉冲,每个脉冲要有足够高电平时间:
#define LCD_ADDR 0x27 // 常见默认地址,A0A1A2全接地时 static void lcd_byte(uint8_t data, uint8_t rs) { uint8_t buf[4]; uint8_t hi = data & 0xF0; uint8_t lo = (data << 4) & 0xF0; uint8_t ctrl = rs ? 0x01 : 0x00; // RS位 // 依次输出:高4位EN=1,高4位EN=0,低4位EN=1,低4位EN=0 buf[0] = hi | 0x04 | ctrl | 0x08; buf[1] = hi | 0x00 | ctrl | 0x08; buf[2] = lo | 0x04 | ctrl | 0x08; buf[3] = lo | 0x00 | ctrl | 0x08; HAL_I2C_Master_Transmit(&hi2c1, LCD_ADDR << 1, buf, 4, 100); HAL_Delay(2); } void lcd_init(void) { HAL_Delay(200); // 等待LCD自身上电稳定 lcd_byte(0x30, 0); // 8位模式指令 lcd_byte(0x20, 0); // 切到4位模式 lcd_byte(0x28, 0); // 4位模式双行 lcd_byte(0x0C, 0); // 显示开、光标关 lcd_byte(0x01, 0); // 清屏 HAL_Delay(20); }逻辑说明:PCF8574转接板虽然只用了I2C,但LCD内部还是HD44780控制器,初始化时序必须完整走一遍4位模式流程。lcd_byte里的0x04对应EN引脚,0x08对应背光使能,发送时先拉高EN再拉低,产生一个下降沿,LCD在下降沿锁存数据。每个字节之间的2ms延时是为了等LCD内部处理完,如果延时太短会丢字符。
注意这个地址和引脚映射是针对最常见的转接板,不同批次可能有差异,拿到模块先扫描I2C地址,确认是0x27还是0x3F。
5.2 多传感器调度:时间片轮询与屏幕刷新策略
三个传感器加一块屏幕,调度不用上RTOS,一个while循环加时间戳差值就够了。ADXL345按100Hz轮询,PPG按50Hz读,体温和屏幕每秒刷新一次。
uint32_t t_now, t_last_ppg, t_last_lcd; t_last_ppg = 0; t_last_lcd = 0; while (1) { t_now = HAL_GetTick(); acc_t acc = adxl345_read(); float mag = acc_magnitude(acc); step_detect(mag, t_now); if (t_now - t_last_ppg >= 20) { // 50Hz t_last_ppg = t_now; uint32_t ppg = hr_read_ppg(); ppg_process(ppg, t_now); } if (t_now - t_last_lcd >= 1000) { // 1Hz t_last_lcd = t_now; float temp = read_ntc_temperature(); refresh_lcd(step_count, hr_value, temp); } HAL_Delay(10); }逻辑说明:用时间戳差值而不是HAL_GetTick取模,避免毫秒翻转时发生瞬时重复执行。LCD刷新只有1Hz,因为LCD1602本身响应慢,刷快了人眼也感知不到增益。I2C总线上三个从机地址不同,HAL库的Mem_Read和Master_Transmit每次都会发起完整START到STOP事务,天然是互斥的,不需要加锁。
显示布局上,第一行放“STEP”和“HR”,第二行放“TEMP”和状态。LCD1602一行只有16个字符,数据之间要留分隔符,避免数字位数变化时黏在一起。
5.3 5类典型故障的现象、原因与解决办法
现象一:I2C扫描不到ADXL345。原因通常是SDO引脚悬空导致地址不确定,或者上拉电阻没生效。解决:先确认SDO是接地还是接VCC,再分别扫描0x53和0x1D两个地址;量一下SCL和SDA对地电压,3.3V下空闲电平应该在3.0V以上。
现象二:计步数明显偏多。原因一般是PEAK_THRESHOLD太低或回落宽度太小,手部小动作也被当成一步。解决:把阈值提到1.1g及以上,把回落宽度从0.15g改到0.25g,然后按正常步速走100步,统计误差。
现象三:心率读数忽高忽低。原因通常是佩戴不紧、环境光漏进传感器,或者LED电流太大导致信号饱和。解决:用遮光胶带或黑色软胶包裹模块边缘,调低LED电流,再把滑动窗口从10加大到20,可以让显示值稳定下来。
现象四:NTC体温读数漂移。原因多半是电源纹波直接进了ADC,或者分压节点阻抗过高。解决:在PA0和地之间加0.1uF电容,软件里做5次采样去极值平均,还可以加一阶滞后滤波,让温度变化速率更符合物理规律。
现象五:LCD1602复位后乱码。原因是初始化时序不对或LCD上电比MCU晚,STM32已经跑完初始化序列LCD还没准备好。解决:在lcd_init开头加200ms延时,确保LCD内部上电复位完成,再按HD44780手册的时序逐条发送初始化命令。若只在部分字符随机异常,重点检查PCF8574转接板的EN脉冲宽度,把lcd_byte里的2ms延时加长到5ms。
6. 进阶验证:用串口日志把计步阈值从拍脑袋变成工程标定
手环工程最容易被忽略的是最后一步:代码跑起来了,但没人验证数据对不对。我习惯把串口当成示波器,把合成加速度、PPG原始值、时间戳按CSV格式打出来,存到电脑里画曲线。格式类似:
time_ms,acc_mag_g,ppg_raw,step_flag 0,1.03,53210,0 10,1.05,53289,0 20,1.47,53312,1step_flag置1表示算法认为这一步成立。把这份数据和实际迈步脚数对齐后,才能判断阈值是否合理,而不是看LCD数字“好像差不多”。具体做法是走100步,步速刻意分成慢、中、快三段,每段走完按一下板载按键打一个标记行,离线统计每段误差。
标定流程我固定分三步:第一步记录三种场景的原始mag峰值,静止时看基线噪声幅度,慢走看峰值,快走看峰值上限;第二步把阈值定在慢走峰值的70%到80%,确保慢走能触发、静止抖动不能触发;第三步验证步距窗口是否覆盖三段步频,必要时动态统计相邻峰间隔的中位数来做自适应。
我见过不少开发者在这类工程上反复下载数十次固件,就为了调一个阈值,本质上是把算法当成了黑匣子在猜。把原始数据打出来,看一次曲线,比盲改代码高效得多。这个习惯也是我从这个手环工程里收获最大的部分:传感器数据必须先被看见,才能被信任。
做完这个工程之后,计步、心率、体温三路数据已经形成了完整的闭环。换一块OLED替换LCD1602,或者接上蓝牙模块把数据传到手机,都是在现有驱动上的增量改造,工程骨架不需要推翻。希望帮到你。
本文还有配套的精品资源,点击获取