十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32无线运动心电温度步数监测系统设计与实现

STM32无线运动心电温度步数监测系统设计与实现 1. 从一道赛题说起运动心电、温度与步数的无线传输系统2019年底的时候我正带着几个学弟准备2020年的电赛。那年A题的方向一出来我们几个人对着题目琢磨了很久——运动心电、温度、步数三个生理参数还要无线传输。说实话这个题目放在今天看依然很有嚼头因为它把模拟前端采集、数字信号处理、无线通信协议和低功耗设计这几个嵌入式领域最核心的模块全串到了一起。你如果正在做类似的项目或者单纯想找一个能把STM32外设用全的练手方向这个题目非常合适。它不像单纯的温湿度采集那样只考验I2C驱动能力也不像纯无线通信那样只调模块。它要求你从传感器选型开始一路走到数据打包、无线发送、上位机显示中间任何一个环节出问题整个系统就跑不起来。我后来把这个项目重新整理了一遍用STM32F103C8T6做核心心电部分用AD8232模块温度用DS18B20步数用MPU6050做加速度积分无线传输用NRF24L01。整套方案成本不高但涉及的知识点非常密集。下面我把整个设计和调试过程拆开来讲包括当时踩过的坑和后来复盘时想明白的道理。2. 系统整体设计与核心器件选型2.1 为什么选STM32F103C8T6做核心选主控这件事我的原则一直是先看外设需求再看算力最后看生态。这个项目需要的外设包括至少两路ADC心电和加速度计如果走模拟输出、多路GPIODS18B20单总线、NRF24L01的SPI、至少一个SPI接口、一个UART用于调试。STM32F103C8T6有2个12位ADC、3个USART、2个SPI、37个GPIO资源刚好够用而且有余量。更重要的是它的生态。Keil5加上STM32芯片包标准外设库或者HAL库都能跑网上能找到的参考代码多如牛毛。对于电赛这种时间紧、任务重的场景生态比性能重要得多。你不需要花三天时间去移植一个冷门芯片的驱动而是可以把精力放在算法和系统联调上。注意STM32F103C8T6的Flash只有64KBRAM只有20KB。如果你打算在片内做心电信号的实时滤波比如FIR或者小波变换RAM会非常紧张。我的建议是心电部分只做简单的阈值检测和心率计算复杂滤波放到上位机去做。2.2 三个生理参数的采集方案对比心电采集这块市面上常见的方案有三种AD8232模块、ADS1292芯片、以及分立仪表放大器加滤波电路。AD8232是单导联心电前端输出模拟信号直接接STM32的ADC就能读。ADS1292是双通道24位ADCSPI接口精度高但价格也高而且焊接难度大。分立方案最便宜但调试周期最长。我最终选了AD8232模块理由很直接它把仪表放大器、右腿驱动、滤波电路全集成好了输出就是干净的心电波形。你只需要接三个电极片配置好STM32的ADC采样率就能拿到可用的信号。对于电赛这种四天三夜的场景时间比成本重要。温度采集用DS18B20。这个选择几乎没有争议——单总线接口只需要一个GPIO精度0.5°C测量范围-55到125°C完全覆盖体温测量需求。而且它出厂就是数字信号不需要额外的ADC抗干扰能力比模拟温度传感器强很多。步数采集用MPU6050。它集成了三轴加速度计和三轴陀螺仪I2C接口DMP库可以直接输出姿态角。步数检测的原理很简单人走路时垂直方向的加速度会呈现周期性的峰值通过峰值检测或者阈值判断就能计步。MPU6050的加速度计量程可以配置到±2g分辨率足够捕捉走路时的加速度变化。2.3 无线传输方案的选择逻辑无线传输这块NRF24L01和ESP8266是两种最常见的方案。NRF24L01是2.4GHz私有协议SPI接口功耗低传输距离在空旷环境下能到100米左右。ESP8266是WiFi方案串口AT指令控制可以直接连路由器上传到服务器。我选NRF24L01的原因在于实时性和确定性。心电信号对传输延迟很敏感NRF24L01的点对点传输延迟可以稳定在几毫秒以内而WiFi方案受路由器调度影响延迟波动可能到几十甚至上百毫秒。另外NRF24L01不需要配网上电就能通信调试起来简单很多。代价是NRF24L01只能点对点或者星型组网不能直接连互联网。但对于这个题目来说接收端用另一块STM32加NRF24L01再通过串口把数据转发给电脑上位机完全够用。3. 硬件连接与关键电路细节3.1 STM32与各模块的引脚分配引脚分配这件事看起来简单但如果你一开始没规划好后面改代码的时候会非常痛苦。我的原则是按外设功能分组同一组外设的引脚尽量连续这样在写代码的时候可以批量初始化也方便用杜邦线连接。下面是我实际使用的引脚分配表模块引脚STM32引脚功能说明AD8232OUTPUTPA0ADC1_IN0心电模拟信号输入AD8232LOPA1导联脱落检测高电平有效AD8232LO-PA2导联脱落检测高电平有效DS18B20DQPA3单总线数据线需4.7k上拉MPU6050SCLPB6I2C1_SCLMPU6050SDAPB7I2C1_SDANRF24L01SCKPA5SPI1_SCKNRF24L01MISOPA6SPI1_MISONRF24L01MOSIPA7SPI1_MOSINRF24L01CSNPA4片选GPIO控制NRF24L01CEPB0使能GPIO控制NRF24L01IRQPB1中断输出可选调试串口TXPA9USART1_TX调试串口RXPA10USART1_RX这个分配方案的好处是ADC和SPI1的引脚都是STM32的默认复用功能不需要重映射初始化代码最简洁。I2C1用的PB6和PB7也是默认引脚。DS18B20和NRF24L01的控制引脚都是普通GPIO随便分配。提示PA0同时是WKUP引脚如果你用到了待机唤醒功能需要注意。不过这个项目里我们用不到低功耗待机所以没关系。3.2 心电前端的信号调理与电极连接AD8232模块的输出信号范围是0到3.3V中心点在1.5V左右可以直接接STM32的ADC。但实际使用中有几个细节需要注意。电极连接是心电测量最容易出问题的地方。标准的三导联连接方式是RA右臂接右上胸LA左臂接左上胸RL右腿接右下腹作为参考。但如果你是在实验室环境下测试没有真人佩戴可以用信号发生器模拟心电信号输入。AD8232的LO和LO-引脚用于检测电极是否脱落。当电极接触不良时这两个引脚会输出高电平。我在代码里把这两个引脚配置成上拉输入然后在主循环里轮询。如果检测到高电平就通过串口打印警告信息同时暂停心电数据的发送。滤波方面AD8232内部已经做了0.5Hz到40Hz的带通滤波这正好覆盖了心电信号的频率范围。但输出信号上仍然会叠加50Hz的工频干扰。我在AD8232输出和STM32 ADC输入之间加了一个简单的RC低通滤波截止频率设在100Hz左右用1k电阻和1uF电容。实测下来波形明显干净了很多。3.3 DS18B20的单总线时序与上拉电阻DS18B20用的是单总线协议所有通信都通过一根数据线完成。这意味着时序非常关键微秒级的偏差都可能导致通信失败。STM32F103C8T6的主频是72MHz一个机器周期大约是13.9ns。DS18B20的时序要求是复位脉冲至少480us存在脉冲60到240us写0时隙至少60us写1时隙至少1us。用延时函数实现的时候需要精确计算循环次数。我一开始用HAL_Delay做延时结果完全读不到数据。后来换成自己写的微秒延时函数基于SysTick定时器才稳定下来。具体的延时函数实现后面代码部分会讲。上拉电阻是另一个关键点。DS18B20的数据线需要接一个4.7kΩ的上拉电阻到3.3V。这个电阻不能太大也不能太小太大则上升沿太慢太小则功耗增加。4.7k是经过验证的经典值。我试过用10k在长导线情况下波形上升沿明显变缓通信成功率下降。3.4 MPU6050的I2C通信与步数检测原理MPU6050通过I2C接口与STM32通信默认地址是0x687位地址。初始化的时候需要配置几个关键寄存器电源管理寄存器10x6B要写0x00解除休眠配置寄存器0x1C设置加速度计量程采样率分频寄存器0x19设置采样率。步数检测我用的是一种简化的峰值检测算法。具体思路是读取加速度计的Z轴垂直方向数据维护一个滑动窗口计算窗口内的最大值和最小值。当最大值和最小值的差值超过阈值并且当前值处于下降沿时判定为一步。阈值的选择很关键。我实测下来人正常走路时Z轴加速度的波动范围大约是±0.3g跑步时能到±1.5g。所以我把阈值设在0.2g左右既能检测到走路又不会把轻微晃动误判为步数。注意MPU6050的原始数据是16位有符号数量程设为±2g时灵敏度是16384 LSB/g。所以0.2g对应的原始值是3277左右。这个数值在代码里要体现出来。4. 软件架构与核心代码实现4.1 系统主循环与任务调度设计这个项目涉及三个传感器的数据采集、一个无线模块的数据发送如果全部塞在一个while(1)里顺序执行心电信号的采样率会非常不稳定。我的做法是用定时器中断做时间基准在主循环里做任务调度。具体来说TIM2配置成1ms中断一次在中断服务函数里维护几个计数器每1ms触发一次ADC采样心电每10ms触发一次MPU6050读取每100ms触发一次DS18B20温度转换每20ms触发一次NRF24L01数据发送。主循环里只做三件事检查各计数器的标志位执行对应的任务然后处理串口调试输出。这样每个任务的执行周期都是确定的心电采样率稳定在1kHz完全满足心率计算的需求。// 任务调度结构体 typedef struct { uint16_t counter; uint16_t period; void (*task)(void); } Task_t; Task_t tasks[] { {0, 1, ADC_SampleTask}, // 1ms周期 {0, 10, MPU6050_ReadTask}, // 10ms周期 {0, 100, DS18B20_ReadTask}, // 100ms周期 {0, 20, NRF24L01_SendTask}, // 20ms周期 };这个调度框架的好处是扩展性强。如果你后面想加一个OLED显示只需要在数组里加一行写一个显示任务函数就行不用改动主循环的逻辑。4.2 心电信号的ADC采集与心率计算心电信号的采集用ADC1的规则通道配置成连续转换模式用DMA搬运数据。这样CPU不需要干预每次转换只需要在DMA传输完成中断里处理数据就行。采样率设置为1kHz也就是每1ms采样一次。这个采样率对于心电信号来说绰绰有余因为心电信号的主要频率成分在0.5到40Hz之间根据奈奎斯特采样定理采样率只要大于80Hz就不会混叠。1kHz的采样率留了足够的余量。心率计算我用的是峰值检测法。具体步骤是维护一个长度为500的滑动窗口对应0.5秒的数据计算窗口内的最大值和最小值设定阈值为最大值的60%当信号从下方穿过阈值向上时记录一个峰值计算相邻两个峰值之间的时间差换算成心率#define WINDOW_SIZE 500 uint16_t ecg_buffer[WINDOW_SIZE]; uint16_t ecg_index 0; uint32_t last_peak_time 0; uint8_t heart_rate 0; void ECG_Process(uint16_t adc_value) { ecg_buffer[ecg_index] adc_value; ecg_index (ecg_index 1) % WINDOW_SIZE; // 找最大值 uint16_t max_val 0; for (int i 0; i WINDOW_SIZE; i) { if (ecg_buffer[i] max_val) max_val ecg_buffer[i]; } uint16_t threshold max_val * 6 / 10; static uint8_t last_state 0; uint8_t current_state (adc_value threshold) ? 1 : 0; if (last_state 0 current_state 1) { uint32_t now HAL_GetTick(); if (last_peak_time ! 0) { uint32_t interval now - last_peak_time; if (interval 300 interval 2000) { // 30-200 BPM heart_rate 60000 / interval; } } last_peak_time now; } last_state current_state; }这段代码里interval的范围限制在300到2000毫秒之间对应心率30到200 BPM。超出这个范围的峰值被认为是噪声或者误检直接忽略。4.3 DS18B20温度读取的时序实现DS18B20的时序实现是很多人卡住的地方。我用的是GPIO模拟单总线需要自己控制引脚方向和电平。关键是要有一个精确的微秒延时函数。// 微秒延时基于SysTick void delay_us(uint32_t us) { uint32_t start SysTick-VAL; uint32_t ticks us * (SystemCoreClock / 1000000); while (1) { uint32_t now SysTick-VAL; uint32_t elapsed; if (now start) { elapsed start - now; } else { elapsed start (SysTick-LOAD - now); } if (elapsed ticks) break; } }有了微秒延时之后DS18B20的复位、写字节、读字节操作就都好实现了。复位时序是拉低总线至少480us然后释放等待60到240us如果存在脉冲则说明器件在线。uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_DQ_OUT(); DS18B20_DQ_LOW(); delay_us(480); DS18B20_DQ_HIGH(); delay_us(30); DS18B20_DQ_IN(); presence DS18B20_DQ_READ(); delay_us(400); return presence; }温度转换的命令是0x44读取暂存器的命令是0xBE。转换时间在12位精度下是750ms所以我的任务调度里DS18B20的周期设的是100ms但实际上每8次才发起一次新的转换中间7次都是读取上一次的结果。4.4 MPU6050的初始化与步数统计算法MPU6050的初始化流程是解除休眠、设置采样率、设置量程、配置中断。我用的加速度计量程是±2g陀螺仪量程是±250度/秒。void MPU6050_Init(void) { MPU6050_WriteReg(0x6B, 0x00); // 解除休眠 MPU6050_WriteReg(0x19, 0x09); // 采样率1kHz MPU6050_WriteReg(0x1A, 0x03); // 低通滤波44Hz MPU6050_WriteReg(0x1C, 0x00); // 加速度±2g MPU6050_WriteReg(0x1B, 0x00); // 陀螺仪±250度/秒 }步数统计我用的是滑动窗口峰值检测。维护一个长度为20的窗口对应200ms的数据采样率100Hz。计算窗口内Z轴加速度的最大值和最小值如果差值超过阈值并且当前值小于窗口平均值就判定为一步。#define STEP_WINDOW 20 int16_t accel_z_buffer[STEP_WINDOW]; uint8_t step_index 0; uint32_t step_count 0; void Step_Detect(int16_t accel_z) { accel_z_buffer[step_index] accel_z; step_index (step_index 1) % STEP_WINDOW; int16_t max_val -32768, min_val 32767; int32_t sum 0; for (int i 0; i STEP_WINDOW; i) { if (accel_z_buffer[i] max_val) max_val accel_z_buffer[i]; if (accel_z_buffer[i] min_val) min_val accel_z_buffer[i]; sum accel_z_buffer[i]; } int16_t avg sum / STEP_WINDOW; int16_t range max_val - min_val; static uint8_t step_state 0; if (range 3000) { // 约0.18g if (accel_z avg step_state 0) { step_count; step_state 1; } else if (accel_z avg) { step_state 0; } } }阈值3000对应大约0.18g这个值是我实测调整出来的。太小了会把走路时的轻微晃动算进去太大了跑步时反而检测不到。4.5 NRF24L01的SPI驱动与数据打包格式NRF24L01的驱动分两部分SPI底层读写和寄存器配置。SPI用STM32的硬件SPI1配置成模式0CPOL0CPHA0时钟分频到8分频也就是9MHz。NRF24L01支持最高10MHz的SPI时钟9MHz是安全值。数据打包格式我定义了一个结构体包含帧头、心率、温度、步数、校验和。帧头用0xAA和0x55两个字节方便接收端做帧同步。typedef struct { uint8_t header1; // 0xAA uint8_t header2; // 0x55 uint8_t heart_rate; // 心率单位BPM int16_t temperature;// 温度单位0.1°C uint32_t step_count;// 步数 uint8_t checksum; // 校验和 } __attribute__((packed)) DataPacket_t;校验和的计算方式是把前面所有字节累加取低8位。接收端收到数据后重新计算校验和如果不匹配就丢弃这一帧。uint8_t CalcChecksum(DataPacket_t *pkt) { uint8_t *p (uint8_t *)pkt; uint8_t sum 0; for (int i 0; i sizeof(DataPacket_t) - 1; i) { sum p[i]; } return sum; }发送的时候用NRF24L01的增强型ShockBurst模式配置成自动应答和自动重发。这样如果接收端没有正确收到数据发送端会自动重发提高可靠性。5. 调试过程中踩过的坑与排查方法5.1 心电信号噪声大、基线漂移怎么办这是我最开始遇到的最头疼的问题。AD8232输出的波形上叠加了很明显的50Hz工频干扰而且基线会缓慢漂移导致峰值检测算法完全失效。排查思路是这样的先确认干扰来源再针对性解决。我把示波器接在AD8232输出上发现干扰频率确实是50Hz。然后我做了几个实验拔掉电极片干扰依然存在用手触摸电路板地线干扰幅度变化把电路板放到金属盒里干扰明显减小。结论是空间耦合的工频干扰。解决办法有三个一是在AD8232输出加RC低通滤波截止频率设在40Hz左右二是用屏蔽线连接电极片三是软件上做数字滤波我用的是滑动平均滤波窗口长度5对50Hz干扰有很好的抑制效果。基线漂移的问题我在软件里加了一个高通滤波具体做法是用当前值减去滑动窗口的平均值。窗口长度设为200对应0.2秒的数据截止频率大约0.8Hz正好滤掉基线漂移。5.2 DS18B20读不到数据的常见原因DS18B20读不到数据90%的情况是时序问题。我总结了一个排查清单现象可能原因解决方法复位无存在脉冲上拉电阻未接或阻值不对检查4.7k上拉电阻复位有脉冲但读数据全0微秒延时不准用示波器测量延时时间偶尔能读偶尔不能中断打断了时序读DS18B20时关中断温度值固定不变转换命令未发送检查0x44命令是否发出温度值明显偏高分辨率配置错误检查配置寄存器0x1F我遇到的是第三种情况偶尔能读偶尔不能。后来发现是TIM2中断打断了DS18B20的时序。解决办法是在DS18B20的读写函数里进入临界区前关中断退出后开中断。__disable_irq(); // DS18B20时序操作 __enable_irq();5.3 NRF24L01通信距离短、丢包严重NRF24L01的通信距离和很多因素有关发射功率、数据传输率、天线质量、环境干扰。我一开始用的是默认配置发射功率0dBm数据传输率2Mbps空旷环境下只能到20米左右。后来我把发射功率调到最大0dBm其实是NRF24L01的最大值但可以通过配置寄存器0x06的RF_PWR位来设置数据传输率降到250kbps通信距离一下子到了80米以上。代价是传输速率降低但对于我们这个应用来说每20ms发一次数据每次32字节250kbps完全够用。丢包严重的问题我通过开启自动重发和自动应答解决了。配置寄存器0x04的EN_AA位设为10x04的EN_RXADDR位设为10x05的ARC和ARD位设置重发次数和重发延迟。我设的是重发10次延迟250us。提示NRF24L01的电源引脚旁边一定要加一个10uF的电解电容和一个0.1uF的陶瓷电容。这个模块在发射瞬间电流能到100mA以上电源不稳会导致通信失败。5.4 步数统计不准确怎么调步数统计不准确通常表现为两种情况多计步和少计步。多计步是因为阈值太低把走路时的轻微晃动也算进去了。少计步是因为阈值太高或者窗口长度不合适。我的调试方法是先把MPU6050的原始数据通过串口打印出来在电脑上画成曲线观察走路时Z轴加速度的实际波动范围。然后根据这个范围来设定阈值。实测下来人正常走路时Z轴加速度的波动范围大约是±0.3g对应原始值±4900。我把阈值设在3000也就是0.18g既能检测到走路又不会把轻微晃动算进去。窗口长度也很关键。窗口太短最大值和最小值的计算不稳定窗口太长步与步之间会互相干扰。我试过10、20、30三个值最后选了20对应200ms的数据正好覆盖一步的周期。6. 上位机数据接收与可视化方案6.1 接收端STM32的串口转发逻辑接收端STM32的作用是接收NRF24L01的数据然后通过串口转发给电脑。逻辑很简单NRF24L01配置成接收模式在中断里读取数据校验通过后通过USART1发送。串口发送的格式我定义成ASCII字符串方便上位机解析。格式是$HR,72,T,36.5,S,1234#。其中$是帧头#是帧尾HR后面是心率T后面是温度S后面是步数。void NRF24L01_RxHandler(void) { DataPacket_t pkt; NRF24L01_ReadPayload((uint8_t *)pkt, sizeof(pkt)); if (pkt.header1 0xAA pkt.header2 0x55) { if (CalcChecksum(pkt) pkt.checksum) { char buf[64]; sprintf(buf, $HR,%d,T,%.1f,S,%lu#\r\n, pkt.heart_rate, pkt.temperature / 10.0, pkt.step_count); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 100); } } }6.2 用串口助手做快速验证在写上位机之前我建议先用串口助手验证数据是否正确。我用的是一款免费的串口调试助手配置好波特率115200数据位8停止位1无校验就能看到接收端发过来的字符串。如果字符串格式正确数值也在合理范围内说明整个链路是通的。这时候再去写上位机就只是解析字符串和画图的问题了。6.3 Python上位机的数据解析与绘图上位机我用Python写的用pyserial读串口用matplotlib画实时曲线。核心代码不复杂主要是串口读取和数据解析。import serial import matplotlib.pyplot as plt import matplotlib.animation as animation from collections import deque ser serial.Serial(COM3, 115200, timeout0.1) hr_data deque(maxlen100) temp_data deque(maxlen100) step_data deque(maxlen100) def parse_line(line): try: line line.decode(ascii).strip() if line.startswith($) and line.endswith(#): parts line[1:-1].split(,) hr int(parts[1]) temp float(parts[3]) step int(parts[5]) return hr, temp, step except: pass return None def update(frame): while ser.in_waiting: line ser.readline() result parse_line(line) if result: hr, temp, step result hr_data.append(hr) temp_data.append(temp) step_data.append(step) plt.cla() plt.subplot(3,1,1) plt.plot(list(hr_data)) plt.ylabel(Heart Rate (BPM)) plt.subplot(3,1,2) plt.plot(list(temp_data)) plt.ylabel(Temperature (C)) plt.subplot(3,1,3) plt.plot(list(step_data)) plt.ylabel(Steps) ani animation.FuncAnimation(plt.gcf(), update, interval100) plt.show()这个上位机虽然简陋但足够验证数据链路。如果你想要更漂亮的界面可以用PyQt或者Tkinter做GUI把实时数值和曲线整合到一个窗口里。7. 系统联调与性能优化经验7.1 各模块之间的干扰排查系统联调的时候我发现了一个很隐蔽的问题NRF24L01发送数据的时候心电信号的噪声会明显增大。用示波器观察发现是电源纹波导致的。NRF24L01在发射瞬间电流突增导致3.3V电源上出现了一个大约100mV的跌落。这个跌落通过电源线耦合到了AD8232的供电上反映到输出就是噪声。解决办法是在NRF24L01的电源引脚旁边加了一个100uF的电解电容和一个0.1uF的陶瓷电容同时在AD8232的电源引脚旁边也加了10uF和0.1uF的电容。加完之后发射时的噪声明显减小。另一个干扰是MPU6050的I2C通信对DS18B20的影响。I2C的时钟频率是400kHzDS18B20的时序是微秒级两者本来不应该互相干扰。但实际测试中发现I2C通信时DS18B20的读取成功率会下降。后来发现是I2C的上拉电阻和DS18B20的上拉电阻共用了一条电源线导致电源波动。分开供电之后问题解决。7.2 低功耗设计的取舍电赛题目没有明确的低功耗要求但如果你想把这个问题做成一个可穿戴设备低功耗就是必须考虑的了。我后来做了一版低功耗优化主要措施包括STM32进入Sleep模式用TIM2中断唤醒MPU6050配置成低功耗模式采样率降到10HzDS18B20的转换周期从100ms延长到1sNRF24L01的发送周期从20ms延长到100ms代价是心电采样率从1kHz降到了200Hz步数检测的实时性也下降了。但对于可穿戴设备来说这些代价是可以接受的。实测下来整机功耗从原来的80mA降到了15mA左右用1000mAh的锂电池可以连续工作60小时以上。注意进入Sleep模式之前一定要确保所有外设都配置好了中断唤醒。否则STM32会一睡不醒只能按复位键。7.3 数据打包效率与传输可靠性数据打包的效率直接影响传输可靠性。我一开始用的结构体是32字节后来优化到了16字节。减少字节数意味着每次传输的时间更短被干扰的概率更低。优化后的结构体是这样的typedef struct { uint8_t header; // 0xAA uint8_t heart_rate; // 心率 int16_t temperature; // 温度 uint16_t step_count; // 步数最大65535 uint8_t checksum; // 校验和 } __attribute__((packed)) DataPacket_t;步数用uint16_t最大65535步对于一天的活动量来说完全够用。温度用int16_t单位0.1°C范围-3276.8到3276.7°C远远覆盖体温范围。整个结构体只有7个字节加上NRF24L01的包头和CRC实际传输的数据量很小。传输可靠性方面除了自动重发和自动应答我还加了一个序列号机制。每个数据包带一个递增的序列号接收端检查序列号是否连续。如果不连续说明有丢包接收端可以通过串口打印警告信息。8. 常见问题速查与独家避坑技巧8.1 硬件调试速查表问题现象最可能的原因快速验证方法解决方案心电波形是一条直线电极未连接或AD8232未供电测量AD8232的3.3V引脚检查电极连接和电源心电波形噪声大工频干扰或电源纹波示波器观察输出加RC滤波和电源电容DS18B20读数为85转换未完成就读取检查转换延时等待750ms后再读DS18B20读数为0时序错误示波器看复位脉冲调整微秒延时MPU6050读数为0I2C地址错误用I2C扫描工具确认地址是0x68步数一直增加阈值太低串口打印原始数据提高阈值到3000NRF24L01发送失败SPI配置错误读STATUS寄存器检查SPI模式和分频NRF24L01接收不到地址不匹配读RX_ADDR寄存器确保收发地址一致串口无输出波特率不匹配示波器测TX引脚确认115200波特率8.2 那些文档里不会写的实操心得第一焊接NRF24L01的排针时先把排针插在面包板上再焊。这样能保证排针垂直于模块插到面包板上不会歪。我一开始直接焊结果排针歪了插不进面包板只能拆了重焊。第二DS18B20的导线长度不要超过1米。单总线协议对导线电容很敏感导线太长会导致上升沿变缓通信失败。如果必须用长导线把上拉电阻减小到2.2k。第三MPU6050要远离电机和电源线。它的陀螺仪对磁场很敏感靠近电机时零漂会明显增大。我在布局的时候把MPU6050放在了板子边缘离电源模块最远的位置。第四心电电极片要用一次性医用的。我试过用铜片自己做电极结果接触阻抗太大信号完全不能用。后来买了医用心电电极片信号质量立刻就好了。第五调试的时候先用信号发生器代替真实信号。心电信号可以用信号发生器产生一个1mV、1Hz的正弦波来模拟温度可以用电阻箱模拟PT100步数可以用手晃动MPU6050来模拟。这样可以把传感器的问题和电路的问题分开排查。8.3 代码调试的实用技巧用串口打印调试信息是最简单有效的方法。我在每个任务函数的开头和结尾都加了串口打印通过打印的时间戳来判断任务的执行时间是否符合预期。用GPIO翻转做时间测量。在任务开始的时候拉高一个GPIO结束的时候拉低用示波器测量高电平的持续时间就能精确知道任务的执行时间。我用这个方法发现DS18B20的读取任务耗时2ms超过了预期的1ms后来优化了延时函数才降下来。用Keil的Logic Analyzer功能。Keil5自带一个逻辑分析仪可以实时观察变量的变化。我把心电数据的缓冲区加到Watch窗口里用Logic Analyzer观察波形不用外接示波器就能看到心电波形。用条件断点排查偶发问题。DS18B20偶尔读不到数据的问题我设了一个条件断点当读取失败时触发。然后让程序跑了一个小时断点触发后查看调用栈发现是TIM2中断打断了时序。9. 这个项目还能怎么扩展如果你已经把这个基础版本跑通了下面几个方向可以让它更有实用价值。加一个OLED显示屏把心率、温度、步数实时显示在设备上。用0.96寸的I2C OLEDSSD1306驱动STM32的I2C1正好可以复用给MPU6050的那条总线。显示刷新率不用太高1Hz就够了。加一个SD卡模块把数据记录到SD卡里。用SPI接口的SD卡模块FATFS文件系统每分钟写一次数据。这样即使无线传输中断数据也不会丢失。把NRF24L01换成ESP8266通过WiFi把数据上传到云平台。ESP8266用AT指令控制STM32通过串口发送AT指令。这样可以在手机上看实时数据不用守在电脑旁边。加一个锂电池充电管理电路用TP4056充电芯片加上升压电路到3.3V。这样整个设备就可以脱离电源线工作真正变成一个可穿戴设备。优化心电算法把峰值检测换成小波变换或者神经网络。STM32F103的算力有限但如果你换成STM32F4系列就可以在片内跑一些简单的机器学习模型提高心率检测的准确率。我个人在实际操作中的体会是这个项目最大的价值不在于最后做出来的东西有多完美而在于它逼着你去理解每一个环节的细节。从模拟前端的噪声抑制到数字总线的时序控制再到无线通信的协议配置每一个环节都有它自己的门道。你把这些门道都摸清楚了再去做其他嵌入式项目的时候就会有一种一通百通的感觉。最后再分享一个小技巧如果你在调试的时候卡住了不妨把问题拆开单独测试每一个模块。先确保DS18B20能读到温度再确保MPU6050能读到加速度再确保NRF24L01能收发数据最后再把它们整合到一起。这样虽然看起来慢但实际上比一上来就联调要快得多。
返回列表