资讯动态

基于STM32的生理健康监测系统设计与信号处理实战解析

发布时间:2026/9/12 10:39:56 来源:尧图企业网站定制
简介基于STM32的生理健康监测系统设计源码是一套面向嵌入式领域高校学生、毕业设计人员及医疗健康硬件开发者的完整工程。系统由传感器采集、ESP8266通信和OLED显示三大模块协同组成实时采集体温、脉搏、心率等生理参数并在OLED端即时展示同时借助OneNet云平台实现远程查看与数据分析形成端到端的监测链路。压缩包共含138个文件以C语言源代码为核心包括44个头文件与39个C源文件另有Keil工程配置、演示图片、说明文档等配套内容整体大小约10.45MB目录按模块划分便于定位与复用。目前已有421人学习下载。读者可获取传感器驱动、ESP8266通信协议、OLED显示移植及云平台对接等关键实现是一套适合课程设计、学科竞赛或健康监测产品原型开发的现成参考模板。1. 先说清楚STM32生理健康监测系统到底在解决什么问题拿到“基于STM32的生理健康监测系统”这个题目很多人第一反应是“又一个毕业设计”但真正把硬件搭起来、把数据跑通才会发现难点不在“采集”二字而在整个信号链路。传感器模拟输出的噪声、采样时序的抖动、滤波参数与峰值检测的配合、任务划分与实时性之间的平衡每一环都会直接影响心率数值的准确性。源码的价值恰恰在这里——不是一段能编译通过的代码而是一套经过参数验证的信号处理流程。这篇文章不打算铺开所有模块而是从硬件选型、采样配置、滤波计算、任务划分到故障排查把一套可复现的工程做法讲清楚适合正在做穿戴设备原型或相关毕业设计的嵌入式开发者。2. 生理信号采集的硬件选型与信号链路设计2.1 传感器选型PPG 与心电方案的成本和信号特征差异生理监测系统的传感器选型在代码还没写之前就决定了后续方案。最常见的做法是选光电体积描记法传感器比如 MAX30102 或 MAX30100它们把红光和红外 LED 与光电二极管封装在一起输出已经过内部放大的模拟信号或数字量。选择 PPG 的理由很实际不需要导电凝胶适合手环类穿戴设备接口简单STM32 用 I2C 或 ADC 引脚就能读数据。但代价是它容易受环境光和肢体运动干扰后级的数字滤波必须跟上。与之相对的是心电方案常用 AD8232 模拟前端。ECG 信号幅度在毫伏级需要 STM32 的 ADC 以 200Hz 以上采样率持续采集而且对模拟地平面和电源纹波更敏感硬件布局要求高。从工程角度来看原型验证阶段选 PPG 是性价比最高的路线如果目标是做有参考价值的医疗级指标ECG 的信号处理复杂度会高出一个量级。代码层面PPG 只要做峰值检测和周期估计ECG 要先做 QRS 波群检测两者的算法复杂度和调试成本差异很大。2.2 STM32 片内外设与传感器输出的匹配要点无论传感器选哪一种信号最终都要进 STM32 的 ADC 或通过 I2C 读取。容易被忽视的问题是传感器的信号带宽与 ADC 采样率必须匹配。心率信号的有效频率成分集中在 0.5Hz 到 5Hz工程上把采样率设在 50Hz 到 100Hz 即可。很多人配置 ADC 时习惯把采样率拉高到 1kHz 以上CPU 开销变大数据量变大滤波反而引入更多高频噪声。信号类型有效带宽建议采样率ADC 采样时间数据位宽PPG 脉搏波0.5–5Hz50–100Hz28.5µs12bitECG 心电0.05–100Hz200–500Hz13.5µs12bit体温NTC0–0.1Hz1Hz 定时采样不敏感12bit采样时间是另一个常被忽略的参数。STM32 ADC 在转换前需要给内部采样电容充电充电时间受采样时间寄存器控制。如果信号源输出阻抗偏高而采样时间设置过短采集到的电压会偏低且不稳定。传感器的输出阻抗一般在几百欧到几十千欧稳妥的做法是把采样时间配置到 28.5µs 或更长档位确保读数稳定。这里有一个判断依据把采样时间从最短档调到最长档如果采样值变化幅度超过满量程的 2%就说明原设置偏短。2.3 信号调理电路增益、截止频率和基线抬升信号到 STM32 ADC 之前是否需要放大和滤波取决于信号幅度。PPG 传感器模拟输出通常在 0.3V 到 1.5V 之间与 STM32 ADC 的 3.3V 满量程匹配可以直接连接。ECG 前端输出是毫伏级必须先放大并抬升基线。三个参数要在原理图阶段确定下来。增益倍数取 50 到 100 倍太小特征不明显太大信号饱和削顶。RC 低通截止频率取 150Hz 左右用f 1 / (2πRC)计算。基线偏置在单电源系统里要抬到 1.65V给 ADC 的正负波形留出余量。#define ECG_GAIN 50.0f /* 放大倍数配合 AD8232 使用 */ #define ECG_LPF_FC 150.0f /* 低通截止频率 Hz */ #define ADC_VREF 3.3f /* STM32 ADC 参考电压 */ #define ADC_BASE 1.65f /* 基线偏置单电源抬升 */ static uint16_t ecg_sample_raw(void) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t raw HAL_ADC_GetValue(hadc1); return raw; }这段代码读取一次 ADC 原始值。返回值是 0 到 4095 的整数换算真实电压用v raw * 3.3f / 4095.0f减去基线的交流分量才是有效信号。如果发现波形削顶说明增益过高如果波形幅度太小说明增益不够。实际调试时我会用示波器同时观察传感器原始输出和放大后输出把增益调在波形不削顶且不低于 1V 峰峰值的范围。2.4 电源架构与低功耗对代码的影响生理监测设备如果是电池供电STM32 的停止模式必须与采样节奏配合。常见做法是 MCU 在两次采样之间进入 Stop 模式由低功耗定时器唤醒完成一次采样再休眠。但 Stop 模式退出后系统时钟会被重置ADC 和定时器的频率配置失效第一次采样通常拿到一个异常值。uint32_t sample_index 0; void enter_stop_mode(void) { HAL_TIM_Base_Stop_IT(htim6); HAL_SuspendTick(); HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); HAL_ResumeTick(); SystemClock_Config(); HAL_TIM_Base_Start_IT(htim6); sample_index 0; }这段代码的关键在唤醒后重新配置时钟。SystemClock_Config 恢复 PLL 和总线时钟然后重启定时器sample_index 清零是为了丢弃唤醒后前几个不稳定的采样点。实际工程里我会在采样回调中判断 sample_index 小于 3 的样本直接不使用只把第 4 个样本以后的数据送入滤波这样可以规避时钟恢复初期的毛刺值。3. STM32 上数据采集与处理的最小可运行代码3.1 用定时器触发 ADC 多通道采样的配置顺序在 STM32 上实现稳定周期采样我不建议在主循环里用 HAL_ADC_PollForConversion 死等。更好的做法是用基本定时器触发 ADC 转换采样间隔完全由硬件确定不受主循环代码执行时间影响。下面是 TIM6 触发 ADC1 通道 0 的最小配置。static void tim6_adc_trig_config(void) { __HAL_RCC_TIM6_CLK_ENABLE(); TIM_MasterConfigTypeDef sMasterConfig {0}; htim6.Instance TIM6; htim6.Init.Prescaler 3200 - 1; /* 80MHz / 3200 25kHz */ htim6.Init.Period 251 - 1; /* 25kHz / 251 ≈ 100Hz */ HAL_TIM_Base_Init(htim6); sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim6, sMasterConfig); } static void adc1_trigger_init(void) { __HAL_RCC_ADC1_CLK_ENABLE(); hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ExternalTrigConv ADC_EXTERNALTRIGCONV_T6_TRGO; hadc1.Init.ExternalTrigConvEdge ADC_EXTERNALTRIGCONVEDGE_RISING; HAL_ADC_Init(hadc1); }TIM6 的更新事件通过 MasterOutputTrigger 引到 ADC 的外部触发通道ADC 检测到上升沿自动转换。Prescaler 和 Period 的乘积决定触发频率80MHz 主频分频 3200 后是 25kHz再计数 250 次溢出一次约 100Hz对应 10ms 一个采样点正好匹配心率信号的带宽。如果后续要增加体温通道只需要把 ADC 配置为扫描模式在 DMA 目标数组里按通道顺序读取。提示定时器触发模式下Adc 的启动不再依赖HAL_ADC_Start而是由定时器硬件事件驱动。只要定时器在跑转换就会持续发生。3.2 滑动窗口滤波去除直流分量和基线漂移传感器原始信号里混着直流分量与低频基线漂移。直流分量用滑动平均估计信号减均值就能去除基线漂移来自呼吸和肢体微动频率低于 0.3Hz滑动窗口长度决定高通截止频率。窗口 50 点、采样率 100Hz 时等效截止频率约 0.3Hz能把呼吸漂移压下去且不影响脉搏波有效成分。#define WINDOW_SIZE 50 /* 滑动窗口长度对应 0.5 秒数据 */ static float buffer[WINDOW_SIZE]; static uint8_t idx 0; static float sum 0.0f; float dc_remove(float input) { sum - buffer[idx]; buffer[idx] input; sum input; idx (idx 1) % WINDOW_SIZE; float mean sum / WINDOW_SIZE; return input - mean; }这段函数每个采样周期调用一次返回去除直流后的交流信号。窗口大小的选择直接决定滤波特性窗口太短会把真实的脉搏波低频分量滤掉心率偏低的人测出来幅度衰减明显窗口太长则压不住呼吸引起的漂移。50 这个值对成人静息心率场景是合理的起点。注意 buffer 是静态数组函数被任务反复调用不会被重新初始化这也是嵌入式代码里常用的写法。3.3 心率计算阈值加最小间隔约束的峰检测干净波形出来后心率计算用简单的峰值检测加最小间隔约束即可。算法逻辑是记录当前窗口内的峰值当信号从峰值跌落超过阈值时认为检测到一个心跳周期同时用最小间隔过滤高频噪声尖刺。uint16_t current_index 0; float peak_value 0.0f; uint8_t waiting_peak 1; float heart_rate_from_ppg(float filtered) { if (filtered peak_value) { peak_value filtered; current_index 0; } current_index; if (waiting_peak filtered peak_value * 0.7f) { if (current_index 40) { uint16_t interval current_index; current_index 0; peak_value 0.0f; return 60.0f * 100.0f / interval; } waiting_peak 0; } if (filtered 0 waiting_peak 0) { waiting_peak 1; } return 0.0f; }函数返回 0 表示当前采样点没有完整心跳周期返回非零值表示刚测完一次峰间间隔。40 点最小间隔对应 150bpm 的心率上限若需要测更高心率要调小这个值但太小容易把噪声尖刺误判成心跳。阈值 0.7f 表示信号从峰值跌落 30% 才算一个有效波峰噪声大的环境下可以调到 0.5f。心率结果是用60 * 采样率 / 间隔换算的这里的采样率 100Hz 是硬编码如果你改了定时器参数这里要同步改。3.4 数据帧打包串口协议格式与解析约定采集和处理后的心率、血氧数据要输出到上位机或屏幕串口是最直接的通道。帧格式必须固定上位机才能可靠解析。字段长度说明帧头1 字节固定 0xAA长度1 字节后续数据字节数数据N 字节心率、血氧等校验1 字节异或校验typedef struct { uint8_t header; /* 0xAA */ uint8_t length; /* 数据字节数 */ uint16_t heart_rate; /* 心率值 */ uint8_t spo2; /* 血氧饱和度 */ uint8_t checksum; /* 异或校验 */ } health_frame_t; void send_health_frame(uint16_t hr, uint8_t spo2) { health_frame_t frame; frame.header 0xAA; frame.length 4; frame.heart_rate hr; frame.spo2 spo2; frame.checksum frame.header ^ frame.length ^ (frame.heart_rate 0xFF) ^ ((frame.heart_rate 8) 0xFF) ^ frame.spo2; uint8_t* p (uint8_t*)frame; for (uint8_t i 0; i sizeof(frame); i) { while (!(USART2-ISR USART_ISR_TXE)); USART2-TDR p[i]; } }异或校验在原型阶段足够用能够检出单字节翻转但不能检出字节顺序错乱。上位机收到帧后先核对帧头和长度再计算校验和不一致直接丢弃整帧。如果后续要做更严谨的协议可以把校验换成 CRC16。帧里没有加时间戳如果上位机要还原波形需要靠接收序号来对齐这是原型阶段可以接受的简化。4. 从裸机到 RTOS任务划分与实时性设计4.1 裸机主循环的时序瓶颈在哪里原型阶段把所有逻辑塞进 while(1) 主循环数据量小的时候问题不明显。但一旦同时要做屏幕刷新、按键扫描和串口发送主循环周期就会出现抖动。比如阻塞式串口发送 20 字节115200 波特率下耗时约 1.7ms看起来不多但加上 LCD 写屏的 SPI 传输和按键消抖的延时等待ADC 采样回调数据可能在队列里堆积几十毫秒。while (1) { process_key(); /* 按键扫描带消抖等待 */ update_lcd(); /* LCD 写屏SPI 阻塞传输 */ send_uart(); /* 串口发送心率数据 */ }这种结构下即使采样由定时器中断完成滤波和计算仍会在主循环中排队。出现的结果是采样时刻与处理时刻之间存在不确定延迟心率数值在输出中整体后移波形显示时看起来像“慢了半拍”。要量化这个延迟可以在滤波入口翻转一个 GPIO用示波器观察中断回调与滤波处理之间的时间差。延迟超过一个采样周期时就说明主循环已经负载过高需要考虑引入 RTOS。4.2 FreeRTOS 任务划分采集、处理、显示三级流水引入 RTOS 后任务划分遵循一个原则中断只做数据搬运算法处理放在任务里。这样既保证采样中断尽可能短又能让滤波和峰检测在低优先级任务中被合理调度。void vTask_AdcCollect(void *arg) { for (;;) { xSemaphoreTake(sem_adc_ready, portMAX_DELAY); copy_from_dma_buffer(); xQueueSend(q_ppg_raw, sample, 0); } } void vTask_HealthCalc(void *arg) { float raw, filtered, hr; for (;;) { xQueueReceive(q_ppg_raw, raw, portMAX_DELAY); filtered dc_remove(raw); hr heart_rate_from_ppg(filtered); if (hr 0) { xQueueSend(q_result, hr, 0); } } }任务优先级按实时性敏感度排列采集任务最高信号处理次之显示和通信最低。因为采集任务被抢占后DMA 缓冲区数据来不及搬走会被新数据覆盖这是最难排查的数据丢失问题。任务优先级栈大小周期或触发方式ADC 数据搬运3128 字100Hz信号量驱动信号处理2256 字队列消息驱动显示刷新1256 字200ms 定时通信协议2512 字事件触发栈大小在实践中经常翻车。任务里用到浮点运算时128 字栈很容易溢出导致 HardFault 或随机复位。浮点滤波任务给到 256 字起步通信任务如果调用了 sprintf 或类似格式化输出函数512 字才稳妥。判断栈是否够用的办法是在 FreeRTOS 里开启栈溢出检测钩子或者周期性调用uxTaskGetStackHighWaterMark查看剩余栈空间。4.3 队列长度与满队列处理策略任务间通信用队列队列长度直接决定数据是否丢失。原始采样队列设置太短处理任务一旦被高优先级中断抢占新样本在 xQueueSend 时就会被直接丢弃。/* 队列长度按 2 秒数据量计算采样率 100Hz */ QueueHandle_t q_ppg_raw xQueueCreate(200, sizeof(float)); if (xQueueSend(q_ppg_raw, sample, pdMS_TO_TICKS(5)) ! pdTRUE) { dropped_samples; }发送超时设 5ms超过这个时间还没发出去就丢弃并计数。dropped_samples 这个统计变量值得保留。上线调试时如果它持续增长说明处理任务的消费能力不足是任务优先级或处理耗时出了问题而不是硬件故障。还有一种情况是生产者和消费者之间的队列类型选错——如果用二进制信号量代替队列数据内容会丢失只剩“有数据”这个信息不适合原始波形传输。5. 波形质量排查从异常数据倒推问题环节5.1 ADC 采样值跳变的三个典型原因如果串口输出的原始波形有大量毛刺或周期性跳变优先排查三个环节。第一是电源去耦VDDA 和 VREF 引脚附近缺少电容会导致 ADC 在内部开关切换瞬间电压跌落表现为采样值随电源噪声波动解决方法是紧贴引脚放 1µF 和 0.1µF 电容组合。第二是采样时间高阻抗信号源搭配过短采样时间会造成读数偏低把采样时间调到最长档观察是否改善。第三是 DMA 缓冲区长度与通道数不匹配多通道扫描时缓存长度必须是通道数的整数倍。#define ADC_CHANNEL_COUNT 2 /* 双通道 DMA 缓冲16 个元素半传输中断时处理前半段 */ static uint16_t adc_buf[ADC_CHANNEL_COUNT * 8];双缓冲模式下DMA 写满前半缓冲区触发回调同时继续往后半缓冲区写。缓冲区长度取 8 个采样周期能保证回调里有足够时间完成数据处理而不会覆盖。现象可能原因检查方法周期性毛刺VDDA 去耦不足示波器看 3.3V 纹波整体偏高或偏低采样时间过短调长采样时间对比数据错位DMA 缓冲长度错误核对通道数乘周期数5.2 心率数值漂移的滤波参数调节经验心率从 70 突然跳到 120 又跳回来最常见的原因是峰值检测把噪声尖刺当成了有效波峰。排查顺序是先看原始波形找到波谷位置是否存在高频抖动。有抖动就把低通滤波的截止频率降下来。float alpha 0.15f; /* 100Hz 采样率下约 5Hz 截止 */ filtered alpha * raw (1.0f - alpha) * prev; prev filtered;alpha 取 0.15 时对 5Hz 以上噪声有明显抑制波形延迟约 30ms不影响心率计算。alpha 调小可以让波形更平滑但峰值幅度会衰减alpha 调大保留更多细节但噪声变大。调参依据是串口打印的实时波形观察波峰形态来决定往哪个方向调。如果波形峰谷都很圆滑说明过滤过度如果峰旁边粘着细小的毛刺说明过滤不够。比较合适的观察信号是波峰只有一个最高点波谷区域有一段平坦的过渡。5.3 用逻辑分析仪验证采样时序滤波和算法调参之前先确认采样时序没有异常。逻辑分析仪接到定时器的观察引脚上抓 2 秒波形测量脉冲间隔的均匀性。分析仪设置建议 采样率 1Mbps 以上 触发条件上升沿 捕获时间2 秒 测量目标相邻脉冲间隔的抖动率正常情况相邻间隔应在 9.8ms 到 10.2ms 之间波动抖动率不超过 2%。如果出现缺失脉冲或间隔翻倍说明定时器时钟源被其他外设共享或中断优先级配置有问题。这类问题用代码审查很难发现逻辑分析仪是最终裁决工具。还有一种隐蔽情况在调试器里设断点时定时器仍在计数恢复执行后触发序列会整体偏移这属于调试手段导致的假象不属于代码问题。6. 让滤波参数可在线调一个提高调试效率的实操方法6.1 把滤波参数做成串口可改的运行期配置滤波和峰检测的参数在调试阶段要反复改。每次改参数都编译烧录太浪费时间更高效的做法是把参数定义成 RAM 中的结构体通过串口命令在线修改。typedef struct { float alpha; /* 低通系数 */ float threshold_peak; /* 峰值比较阈值 */ uint16_t min_interval; /* 最小间隔点数 */ } filter_params_t; static filter_params_t params { .alpha 0.15f, .threshold_peak 0.7f, .min_interval 40 }; void process_uart_command(uint8_t cmd, float value) { switch (cmd) { case a: params.alpha value; break; case t: params.threshold_peak value; break; case i: params.min_interval (uint16_t)value; break; default: break; } }串口调试助手发a 0.1、t 0.5这样的命令参数立即生效。配合波形打印功能把“改代码-编译-烧录-看波形”的周期缩短成“发命令-看波形”的实时调参循环效率提升非常明显。6.2 用 DMA 打印波形调试输出不阻塞采样在线调参依赖波形反馈但波形打印如果阻塞 CPU采样时序会被破坏。解决方法是把串口发送改成 DMA 方式CPU 只负责把数据写入缓冲区发送过程由 DMA 完成。__HAL_UART_ENABLE_DMA(huart2); HAL_UART_Transmit_DMA(huart2, tx_buffer, len);DMA 发送完成回调里只需要重新装载下一段待发送数据。整个打印过程不占用 CPU 等待时间采样回调的时序不会被打印操作干扰。需要注意 DMA 发送缓冲区不能在数据还没发完时就被覆盖所以一般用双缓冲区交替发送一个在传输一个在准备。6.3 一个 30 秒的最终验证方法系统交付前要有一个快速评估数据可靠性的方法录制 30 秒原始波形手动数出完整脉搏波数量并乘以 2 得到心率和系统计算的平均心率对比误差在 ±3bpm 以内可以算通过。这个方法不需要专业仪器是验证整个信号链路最简单有效的手段。如果差值超过 3bpm大概率是峰检测把噪声算成了心跳优先检查这一路滤波参数。做完这一步再核对采样时间和帧格式定义数值就对得上了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价