
简介基于STM32单片机的心电检测系统毕业设计资源以STM32F103C8T6单片机为主控配合心率传感器、TFT液晶屏、独立按键以及蜂鸣器报警模块构建了一套完整的便携式心电监护方案。系统可实时采集并显示心率数值同时绘制心率变化曲线通过按键可自由设置报警阈值当心率超限时蜂鸣器鸣叫且心率值由蓝色变为红色交互反馈直观。整套设计覆盖硬件原理图、单片机源程序、参考文献等关键交付物压缩包约为41.91MB内容结构清晰十分适合电子、嵌入式、生物医学工程等专业学生用于毕业设计、课程设计或创新项目。目前已有942人浏览学习初学者可借此理解STM32的信号采集与显示控制流程开发者也能获得可二次复用的工程模板整体实用价值较高。1. 一个完整的便携式心电检测系统除了STM32最小系统、ADC采集和显示屏最难的不是算法而是信号链前端的模拟设计。很多人把精力放在心率阈值判断上结果现场测波形全是50Hz工频干扰最后只能对着液晶屏上一条“毛刺”发愁。基于STM32单片机的毕业设计功能可以简化为“采集处理显示存储”但每个环节都有可复现的步骤。我会从电极到心率计算的全链路逐一拆开涉及原理图设计思路、源程序模块划分、调试方法和参考文献整理方式适合已经把STM32基础外设跑通、准备把心电信号真正采出来的同学。2. 心电信号采集前端从电极到ADC的电路设计与原理图要点2.1 三电极导联的选择与信号特征人体心电信号幅度只有0.52mV频率集中在0.05100Hz其中QRS波群的能量大部分在10Hz附近。要在这样小的信号里把心率测准模拟前端会直接决定后续数字信号处理的难度。我一般会选用标准三导联中的I导联即左臂、右臂、右腿驱动相比12导联来说元件数量、电极贴放位置和难度都低很多对毕业设计展示和实物演示都够用。电极和皮肤接触会形成一个约几十到几百kΩ的不稳定阻抗受出汗和运动影响明显。因此第一级放大必须用仪表放大器它的高输入阻抗、高共模抑制比CMRR和低失调电压可以避免普通运放输入偏置电流造成的基线漂移。低频漂移会让ADC输入端电压超出量程这也是很多自搭电路调试中最容易忽略的点。常用电路结构是三电极分别接到输入保护网络、仪表放大器输入端和右腿驱动RLD。右腿驱动从仪表放大器的两个输入中提取共模信号反相后经电阻反馈回人体用来抵消电源线经人体容性耦合形成的50Hz共模电压。如果省掉右腿驱动共模信号会直接在差分输出里残留后级陷波器也很难完全消掉。因此原理图设计里RLD电路并不是可选项对工频抑制来说它是必须有的。2.2 前置放大电路参数计算与增益分配仪表放大器选型上AD620、INA128、INA826都可以用。以AD620为例增益公式是G 49.4 kΩ / Rg 1若希望总增益约500倍Rg 49.4kΩ / 499 ≈ 99Ω取标称值100Ω实际增益约495。后级再放24倍让1mV的QRS波经放大后有0.5V左右幅度ADC输入范围不易饱和同时保留一定余量。总增益不要一次做到1000倍以上前级噪声也会跟着被放大。级间留出滤波和隔直的位置。下面这段Python代码用于快速计算增益电阻和滤波器截止频率画原理图前可以先把参数表定下来import math def ad620_gain(rg_ohm): return 49.4e3 / rg_ohm 1 def sallen_key_fc(r1, r2, c1, c2): return 1 / (2 * math.pi * math.sqrt(r1*r2*c1*c2)) # 设定增益 rg 100 # 单位欧姆 g ad620_gain(rg) print(fRG{rg}Ω - 增益{g:.1f}) # 低通滤波器fc目标100Hz # 取R1R216kΩC1C20.1μF fc sallen_key_fc(16e3, 16e3, 0.1e-6, 0.1e-6) print(f低通截止频率{fc:.1f}Hz)这段计算逻辑很简单AD620增益公式用于选RgSallen-Key公式用于确定二阶有源低通的阻容。实际焊好后因为电容精度和运放带宽限制截止频率会偏移10%左右下来还要用信号发生器扫一遍。放大后的信号要通过一级0.050.3Hz的高通把电极直流极化电压滤掉。直流极化电压如果太大会导致后级饱和可以用隔直电容实现取值110μF配合100kΩ电阻形成大约0.11.6Hz的高通基线漂移基本压在ADC允许范围内。注意这个高通频率不能太高否则ST段会被滤掉测心率只关心R波可能无所谓但要做完整波形分析就有问题。2.3 有源滤波与50Hz陷波器设计前置放大后需要两次滤波先做无源RC粗滤波再由Sallen-Key结构构成二阶低通截止频率100Hz左右Q值取0.707巴特沃斯特性。阻容值选取上R1R216kΩC1C20.1μF计算理论截止频率约99.5Hz。如果希望衰减更陡峭的可级联两级但要注意每级输出阻抗会互相影响实际截止频率要按前后级联重新算。50Hz工频陷波器常用双T有源陷波。选择R1R2R330kΩC1C2C10nFR3R1/2≈165kΩ陷波频率约在50Hz附近Q值由运放反馈深度决定。简单做法是在运放反馈回路中串一个电位器调到50Hz衰减最大。陷波器的问题是会把QRS波群的低频分量也削掉一点所以毕业设计里如果右腿驱动做得足够好陷波器可以不加。很多成品模块不配陷波器而是靠右腿驱动加屏蔽来解决。我倾向于在原理图上预留陷波器位置调试时根据实际波形决定贴0Ω电阻还是贴陷波元件。2.4 原理图绘制与PCB布局落地要点原理图设计时按信号流向分区输入保护部分、差分放大、滤波、ADC和STM32最小系统。对应到stm32f103c8t6原理图这里可以直接使用最小系统板而非自制主控板这样既降低难度又能和心电前端板分清楚。原理图上要标注关键测试点比如TP1前置输出、TP2低通输出、TP3ADC输入方便用示波器排查故障。PCB布局上模拟地和数字地不要大面积铺铜连通而是单点连接。连接点通常放在ADC的地脚附近。模拟部分的电源由线性稳压器或者低噪声LDO供电开关电源噪声容易串进放大级。导联线要保持双绞或屏蔽屏蔽层接模拟地。板上晶振远离放大器STM32的PF0/PF1管脚如果不做外部晶振输入就直接用最小系统板的8M晶振区域不要将放大器的反馈电阻布在晶振下方。模块推荐型号关键参数作用前置仪表放大器AD620 / INA128CMRR100dB输入阻抗10^8Ω差分放大与共模抑制右腿驱动运放LM358低功耗输出电流5mA抑制50Hz共模干扰低通滤波器双运放TL072低噪声带宽4MHz100Hz低通、50Hz陷波ADCSTM32F103C8T6内置12位采样率最高1MSPS数字信号采集隔直电容CBB 1μF等效串联电阻低滤除直流极化电压有些同学直接买现成的ECG前端模块这没问题但毕业设计论文里必须能把前端原理图和参数讲明白。如果时间允许建议自己画一块模拟板调试时从第一级开始逐级测后面第五章会给出常见的故障判断方法。3. STM32 ADC采样与心电信号数字处理算法3.1 用定时器触发ADC控制采样率心电信号不需要高采样率250Hz到500Hz足够。为了减小频谱混叠采样率最好不低于200Hz针对100Hz的低通截止频率500Hz采样率可以获得大约2倍以上的裕度也方便后续计算RR间期和心率。使用STM32F103C8T6的ADC1最大72MHz时钟分频后通常是12MHz或9MHz采样周期设置1.5个周期就能满足。关键在于用定时器来触发ADC不能在主循环里随机采样。常见做法是TIM2产生更新事件或TRGO事件触发ADC1规则组转换再通过DMA把转换结果搬到内存缓冲区。为了获得500Hz采样率把TIM2定时周期设置成2ms即可。下面给出一段基于标准外设库的初始化代码适合直接从STM32F10x标准库移植void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; DMA_InitTypeDef DMA_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_StructInit(GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; // PA1 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructInit(TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 1MHz计数 TIM_TimeBaseStructure.TIM_Period 2000 - 1; // 2ms - 500Hz TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_StructInit(DMA_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize AD_BUF_LEN; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_StructInit(ADC_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_1Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这段代码的核心是TIM2的更新事件作为ADC外部触发源ADC工作在单次转换模式每次触发转换一次DMA循环搬运。采样频率由TIM2决定不随主循环耗时波动。PA1输入信号已经过模拟低通ADC采样时间设1.5个周期在12MHz/9MHz ADC时钟下约0.125μs左右速度足够快又避免了信号源输出阻抗造成的采样误差。如果使用HAL库配置的底层寄存器都一样区别只是用定时器触发中断或HAL_ADC_Start_DMA。标准外设库在F103工程里依然大量使用很多参考代码都是基于这个库。3.2 数字滤波去掉基线漂移的IIR高通模拟前端已经把信号放大到ADC可采范围但电极接触不良和呼吸都会造成低频基线漂移幅度可能远大于QRS波。数字端可以做一个0.5Hz左右的高通滤波器把基线拉回零点附近。最简单是一阶IIR高通公式是y[n] α * (y[n-1] x[n] - x[n-1])α 1 / (1 fs / (2π * fc))当fs500Hzfc0.5Hz时α ≈ 1/(1159) ≈ 0.00625系数很小浮点运算是没有问题的。下面代码给出浮点版本因为心率数据量小每秒500次浮点运算对72MHz的F103不是负担static float alpha 0.00625f; static float y_prev 0.0f; static float x_prev 0.0f; float iir_hp(float x) { float y alpha * (y_prev x - x_prev); y_prev y; x_prev x; return x - y; // 高通输出 }注意高通输出是x - yy内部累积的是低频分量。这种结构本质上是一阶高通在0.5Hz附近有缓慢的幅频过渡会损失部分ST段信息但用来检测R波足够了。如果想要更锐利的截止特性可以用二阶巴特沃斯高通系数表格可以从网上算好写死不必在运行时计算。如果滤波后的波形基线在0附近波动说明滤波器工作正常如果发现整个波形上下缓慢平移多半是α不够大或者滤波器没生效。3.3 R波峰值检测与心率计算心率检测的核心是准确定位QRS波群中的R波。R波特征是幅度较大、上升沿陡峭所以一个有效的算法是先计算信号的绝对值用一个滑动窗口估计峰值包络阈值设为包络的一半。超过阈值并且与上一次R波间隔大于200ms时判定为一次心搏然后计算60秒内的平均RR间隔得到心率。这里代码不需要向量库直接用数组实现#define SAMPLE_RATE 500 #define MIN_RR_INTERVAL 100 // 200ms - 100采样点这里取100 int detect_r_wave(int16_t *filtered, int len) { static int last_r_index 0; static float peak_env 0.0f; int count 0; for (int i 0; i len; i) { int16_t v filtered[i] 0 ? filtered[i] : -filtered[i]; peak_env peak_env * 0.95f v * 0.05f; float threshold peak_env * 0.5f; if (v threshold (i - last_r_index) MIN_RR_INTERVAL) { int max_idx i; for (int j i; j i 10 j len; j) { if (filtered[j] filtered[max_idx]) max_idx j; } if (last_r_index 0) { int rr max_idx - last_r_index; if (rr 500) instant_hr 60.0f * SAMPLE_RATE / rr; } last_r_index max_idx; count; } } return count; }峰值包络更新系数0.95和0.05决定包络跟随速度。太大会造成阈值变化迟钝遇到剧烈运动时假阳性太小则会把T波当成R波需要对不同人的信号做实地测试。R波与T波的区别之一是陡峭程度和高频成分如果检测成功率不高可以在阈值判断前先做一阶差分用信号变化率代替幅值来检测。实际调试时可以在LCD上叠加一条阈值曲线观察阈值与R波的关系调整起来很直观。3.4 将ADC值转换为电压并上传上位机ADC原始值是04095的整数对应03.3V但要反映真实的心电信号需要换算。如果前置放大器的总增益是500倍则ADC电压转回输入电压的公式是ecg_mV (adc_val / 4095.0 * 3300 - vref_offset) / gain其中vref_offset是模拟前端输出的直流偏置一般通过高通滤波已经几乎消失但没有移除直流偏置前你要先记录一个静息基线。下面是串口输出格式printf(HR:%d RR:%d ECG:%04d\r\n, heart_rate, rr_interval, adc_raw);经常有人直接把adc_raw当作显示值倍率不正确导致波形高度看起来没差异。正确做法是用信号发生器输入一个1mV、10Hz的正弦波经过放大滤波后输出口能看到约0.5V峰峰值再用上位机校验。具体链路调试在第五章详细说。4. 源程序组织与STM32工程模块划分4.1 毕业设计源程序的文件分层工程清爽是实验成功的前提。一个常见的STM32工程结构是目录/文件作用main.c任务调度、初始化adc.cADC初始化、DMA设置filter.c高通/低通数字滤波heart_rate.cR波检测与心率平均oled.cOLED显示uart.c串口发送、接收key.c按键扫描如果直接在main.c里堆500行代码调试时会非常痛苦。不建议为了省事把所有函数都放在main.c。文件名和函数名保持对应比如filter.c里主要提供filter_init、iir_hp_filter两个接口其他文件不直接操作滤波器内部变量。4.2 用状态机把ADC数据流串起来裸机下不要用零散的while和delay主循环中放一个简单状态机状态IDLE、DATA_READY、PROCESSING、DISPLAY。ADC DMA中断置位一个标志位当缓冲区半满或全满时更新。下面给出框架代码volatile uint8_t adc_flag 0; uint16_t adc_buf[2][256]; void process_ecg(void) { if (adc_flag) { adc_flag 0; // 从当前缓冲区拷贝数据 // 滤波 - 峰值检测 - 更新心率 // 组织显示数据 } }中断服务函数里尽量只置位标志不要处理数据这样能保证中断服务程序短小。DMA循环搬运ADC数据时要注意缓冲区的双缓冲问题。使用DMA_Circular模式会让数据连续覆盖必须通过比较DMA当前地址和内存地址来确定哪个半区是新的。对心电应用的实时性要求不算苛刻也可以在TIM2更新中断里轮询DMA半满标志。4.3 OLED显示波形与心率数值OLED是毕业设计最常见的显示设备。如果直接在主循环里逐点画会闪烁推荐用显存数组。更新时先把ADC滤波后的数据映射到显示屏高度范围内再循环画点。下面是一个简化的OLED示波显示void oled_draw_ecg(int16_t *data, uint8_t len) { oled_clear(); int16_t min_v -500, max_v 500; for (uint8_t i 1; i len i OLED_W; i) { int16_t y0 OLED_H - 1 - (data[i-1] - min_v) * (OLED_H - 2) / (max_v - min_v); int16_t y1 OLED_H - 1 - (data[i] - min_v) * (OLED_H - 2) / (max_v - min_v); oled_line(i - 1, y0, i, y1, 1); } }注意这个映射假设数据范围±500对应实际±1mV具体值要根据放大增益调整。心电波形在屏幕上宽度只有128像素按500Hz采样率屏幕只能显示约256ms的波形不到一个完整心动周期。若要显示完整心跳可以每34个点取平均或者把刷新率降低到30Hz。这个细节很多人到答辩前才意识到建议一开始就决定屏幕显示时间窗长度。4.4 按键切换显示模式与参数调节写一个简单的按键逻辑按键短按切换显示“波形”和“心率/RR”长按进入阈值参数调整菜单。状态机里使用threshold_ratio变量在调试时直接按键调节峰值检测阈值比例同时用串口打印调整后的参数便于找到最适合环境的数值void key_scan(void) { if (KEY_PRESSED) { delay_ms(10); if (KEY_PRESSED) { if (menu_flag) { threshold_ratio 0.05f; if (threshold_ratio 0.9f) threshold_ratio 0.1f; } menu_flag !menu_flag; } } }这类按键逻辑配合长按短按会有抖动问题如果硬件原理图上没有RC滤波就要在软件里增加消抖延时简单但有效。5. 系统调试与排错从原理图到实物常见问题5.1 波形毛刺太大以我的经验STM32最小系统板本身的数字噪声会通过地耦合进模拟前端。观察到的现象是ADC数据跳动大波形看起来像一层烟雾。优先检查模拟电源和数字电源是否分开不要用同一路LDO同时给STM32和放大器供电。可以用电池给模拟部分供电对比测试。用示波器测量放大级输出如果输出端纹波已经几十mV问题在硬件。如果放大级输出干净而ADC内部跳动问题在ADC参考电压或采样时间。STM32的VREF引脚如果没有外部基准就把VDDA接单独的3.3V并在引脚旁放一个1μF低ESR电容。5.2 50Hz工频干扰的消除顺序排除方法先用短接线将差分输入端短接此时若输出仍有50Hz波形说明放大器和周围布线有问题。如果短接后波形干净说明干扰是从电极导联线进入的。导联线使用屏蔽线屏蔽层接地并减少环路面积。右腿驱动要检查是否真的接在人体上电极必须接触良好。现象可能原因优先排查波形全是毛刺电源噪声检查模拟电源、去耦电容50Hz固定干扰右腿驱动未接/屏蔽不良短接输入端测试心率偏慢峰值检测阈值抬太高调节 threshold_ratio波形截止或平顶隔直高通频率太高检查阻容值5.3 心率检测明显偏慢或漏检阈值不合适是最常见的原因。如果R波被运动伪迹淹没可以用一个二阶低通进一步平滑但会降低QRS的高频分量两者需要权衡。常见做法把ADC原始数据通过一个截止频率30Hz的低通同时计算30Hz和100Hz两路信号用两路的能量比值做QRS检测这个策略实时性略差但准确性高。另外MIN_RR_INTERVAL参数设得太短会把T波误判为R波设得太长会漏掉快速心率对于正常心跳600ms间隔参数取300ms比较合适。5.4 原理图、源程序和参考文献如何组织成设计文档毕业设计除了实物还需要完整文档。原理图可以通过立创EDA或Altium导出PDF标注上模块名称和测试点。源程序压缩包中附上README说明编译环境和操作步骤。参考文献不要随意贴建议找近几年关于心电信号处理、STM32低功耗设计的期刊论文和硕士学位论文。可以给出几条典型的参考文献格式王某某. 基于STM32的便携式心电监护系统设计[D]. 北京: 某大学, 2022.Sun Y, et al. Wearable ECG signal processing. IEEE Reviews, 2021, 14: 5-15.参考文献数量和格式能体现文献检索能力一般需要中文期刊、英文期刊、学位论文和标准文档混合。6. 用信号发生器验证系统精度并给毕业设计加一个功能点验证阶段最有说服力的方法就是用函数信号发生器输出一个1mV、60Hz的正弦波模拟心电信号。经过系统后OLED屏幕上应该能看到与输入同频的正弦波形同时心率数值显示60次/分。注意信号发生器输出阻抗和电极连接方式不能把大信号直接接到电极端子可以在前端加一个1kΩ电阻作为人体阻抗模拟。如果没有信号发生器可以用已知频率的方波分段测试先用示波器确认放大级增益再用STM32的内部DAC生成一个50mV正弦波输入给ADC验证采样和滤波。最终成品的精度以对比测试为准同时用一台标准血氧仪或另一个ECG模块测同一人计算RR间隔误差在±2次/分以内就可以认为系统正常工作。剩下的时间可以挑一个小的功能点深化不用贪多。例如加入SD卡记录24小时HRV数据或通过蓝牙把波形发到手机App。这两个方向都能体现数据分析和低功耗设计能力比过度复杂的外置Linux方案更切合STM32F103的资源。关于源程序建议在文件头部写上每个函数的输入输出、依赖的资源和改动日期这比注释本身更有价值。如果要做低功耗把OLED熄灭、ADC每5分钟唤醒一次采集30秒STM32进入Stop模式可以让一节18650电池运行一两天。具体做法是把主循环改成睡眠-唤醒流程在RTC周期中断中唤醒。这个过程涉及独立看门狗、外部中断和RTC备份寄存器但收益极大——心电测量会因为“低功耗”这个标签在答辩中变成一个亮点。最后把地线连接点、陷波器的Q值、阈值调整公式这三样数据和原理图放在同一页这个细节往往能让你在答辩时少卡壳。本文还有配套的精品资源点击获取