STM32无线运动心电温度步数监测系统设计与实现
2026/9/19 13:35:37 网站建设 项目流程

1. 从一道赛题说起:运动心电、温度与步数的无线传输系统

2019年底的时候,我正带着几个学弟准备2020年的电赛。那年A题的方向一出来,我们几个人对着题目琢磨了很久——运动心电、温度、步数,三个生理参数,还要无线传输。说实话,这个题目放在今天看依然很有嚼头,因为它把模拟前端采集数字信号处理无线通信协议低功耗设计这几个嵌入式领域最核心的模块全串到了一起。

你如果正在做类似的项目,或者单纯想找一个能把STM32外设用全的练手方向,这个题目非常合适。它不像单纯的温湿度采集那样只考验I2C驱动能力,也不像纯无线通信那样只调模块。它要求你从传感器选型开始,一路走到数据打包、无线发送、上位机显示,中间任何一个环节出问题,整个系统就跑不起来。

我后来把这个项目重新整理了一遍,用STM32F103C8T6做核心,心电部分用AD8232模块,温度用DS18B20,步数用MPU6050做加速度积分,无线传输用NRF24L01。整套方案成本不高,但涉及的知识点非常密集。下面我把整个设计和调试过程拆开来讲,包括当时踩过的坑和后来复盘时想明白的道理。

2. 系统整体设计与核心器件选型

2.1 为什么选STM32F103C8T6做核心

选主控这件事,我的原则一直是:先看外设需求,再看算力,最后看生态。这个项目需要的外设包括:至少两路ADC(心电和加速度计如果走模拟输出)、多路GPIO(DS18B20单总线、NRF24L01的SPI)、至少一个SPI接口、一个UART用于调试。STM32F103C8T6有2个12位ADC、3个USART、2个SPI、37个GPIO,资源刚好够用而且有余量。

更重要的是它的生态。Keil5加上STM32芯片包,标准外设库或者HAL库都能跑,网上能找到的参考代码多如牛毛。对于电赛这种时间紧、任务重的场景,生态比性能重要得多。你不需要花三天时间去移植一个冷门芯片的驱动,而是可以把精力放在算法和系统联调上。

注意:STM32F103C8T6的Flash只有64KB,RAM只有20KB。如果你打算在片内做心电信号的实时滤波,比如FIR或者小波变换,RAM会非常紧张。我的建议是心电部分只做简单的阈值检测和心率计算,复杂滤波放到上位机去做。

2.2 三个生理参数的采集方案对比

心电采集这块,市面上常见的方案有三种:AD8232模块、ADS1292芯片、以及分立仪表放大器加滤波电路。AD8232是单导联心电前端,输出模拟信号,直接接STM32的ADC就能读。ADS1292是双通道24位ADC,SPI接口,精度高但价格也高,而且焊接难度大。分立方案最便宜但调试周期最长。

我最终选了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,心电模拟信号输入
AD8232LO+PA1导联脱落检测,高电平有效
AD8232LO-PA2导联脱落检测,高电平有效
DS18B20DQPA3单总线数据线,需4.7k上拉
MPU6050SCLPB6I2C1_SCL
MPU6050SDAPB7I2C1_SDA
NRF24L01SCKPA5SPI1_SCK
NRF24L01MISOPA6SPI1_MISO
NRF24L01MOSIPA7SPI1_MOSI
NRF24L01CSNPA4片选,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通信,默认地址是0x68(7位地址)。初始化的时候需要配置几个关键寄存器:电源管理寄存器1(0x6B)要写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的采样率留了足够的余量。

心率计算我用的是峰值检测法。具体步骤是:

  1. 维护一个长度为500的滑动窗口(对应0.5秒的数据)
  2. 计算窗口内的最大值和最小值
  3. 设定阈值为最大值的60%
  4. 当信号从下方穿过阈值向上时,记录一个峰值
  5. 计算相邻两个峰值之间的时间差,换算成心率
#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,配置成模式0(CPOL=0,CPHA=0),时钟分频到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位设为1,0x04的EN_RXADDR位设为1,0x05的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, timeout=0.1) hr_data = deque(maxlen=100) temp_data = deque(maxlen=100) step_data = deque(maxlen=100) 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, interval=100) 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的时钟频率是400kHz,DS18B20的时序是微秒级,两者本来不应该互相干扰。但实际测试中发现,I2C通信时DS18B20的读取成功率会下降。后来发现是I2C的上拉电阻和DS18B20的上拉电阻共用了一条电源线,导致电源波动。分开供电之后问题解决。

7.2 低功耗设计的取舍

电赛题目没有明确的低功耗要求,但如果你想把这个问题做成一个可穿戴设备,低功耗就是必须考虑的了。我后来做了一版低功耗优化,主要措施包括:

  • STM32进入Sleep模式,用TIM2中断唤醒
  • MPU6050配置成低功耗模式,采样率降到10Hz
  • DS18B20的转换周期从100ms延长到1s
  • NRF24L01的发送周期从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
步数一直增加阈值太低串口打印原始数据提高阈值到3000
NRF24L01发送失败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 OLED,SSD1306驱动,STM32的I2C1正好可以复用给MPU6050的那条总线。显示刷新率不用太高,1Hz就够了。

加一个SD卡模块,把数据记录到SD卡里。用SPI接口的SD卡模块,FATFS文件系统,每分钟写一次数据。这样即使无线传输中断,数据也不会丢失。

把NRF24L01换成ESP8266,通过WiFi把数据上传到云平台。ESP8266用AT指令控制,STM32通过串口发送AT指令。这样可以在手机上看实时数据,不用守在电脑旁边。

加一个锂电池充电管理电路,用TP4056充电芯片,加上升压电路到3.3V。这样整个设备就可以脱离电源线工作,真正变成一个可穿戴设备。

优化心电算法,把峰值检测换成小波变换或者神经网络。STM32F103的算力有限,但如果你换成STM32F4系列,就可以在片内跑一些简单的机器学习模型,提高心率检测的准确率。

我个人在实际操作中的体会是,这个项目最大的价值不在于最后做出来的东西有多完美,而在于它逼着你去理解每一个环节的细节。从模拟前端的噪声抑制,到数字总线的时序控制,再到无线通信的协议配置,每一个环节都有它自己的门道。你把这些门道都摸清楚了,再去做其他嵌入式项目的时候,就会有一种一通百通的感觉。

最后再分享一个小技巧:如果你在调试的时候卡住了,不妨把问题拆开,单独测试每一个模块。先确保DS18B20能读到温度,再确保MPU6050能读到加速度,再确保NRF24L01能收发数据,最后再把它们整合到一起。这样虽然看起来慢,但实际上比一上来就联调要快得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询