基于STM32F4的心电监护仪
基于STM32F4的心电监护仪一、硬件设计二、GUI的设计三、导联体系的选择四、心电电极选择五、心电信号时域和频域特征六、软件设计6.1、系统总体设计6.2、系统总体设计6.3、心电信号滤波6.4、心率和QRS宽度检测七、实机演示八、总结与展望从题目中可以看出该课题来源于2020年省电赛A题的无线运动传感器节点的设计该作品得过湖北省电赛二等奖同时也是我本科毕业设计这里我把自己做的关于心电部分的工作进行一次总结也对我的大学四年进行一次总结。一、硬件设计处理器板子的选择本研究的处理器模块选择正点原子公司的STM32F4最小系统板子如图1所示该最小系统板子搭载STM32F407ZGT6芯片并具有 192KB的SRAM、1024KB的FLASH、丰富的定时器资源12个16位定时器2个32位定时器、112个通用I/O口、2个DMA控制器以及1个FSMC接口其中通过FSMC接口可以使得刷屏的速度可达3300W像素/秒另外该板子还外扩了1M字节的SRAM芯片更加有利于该处理器驱动4.3寸的LCD这样极大加快心电监测仪的刷屏速度而且STM32F407ZGT6这款芯片还集成FPU和DSP指令可以加快数字滤波器的处理速度而且该最小系统板子还将FSMC接口和其他IO口一并引出。心电采集板—ADS1292R模块的介绍本研究最重要的地方便是心电采集板关于心电信号的采集板的芯片选择TI公司的ADS1292R外围电路参考TI公司所给的原理图和建议绘制如图所示。关于ADS1292R的外围电路的介绍和使用这里推荐这篇博文ADS1292R的使用温度模块----LMT70温度检测模块采用LMT70温度传感器。其优点是超小型、高精度、低功耗的模拟温度传感器。而缺点是接触式的温度传感器测体表温度存在一定误差。但是考虑到温度测量的精度以及测量方便最终选择LMT70作为测温传感器同时选择采用ADS1118具有PGA、电压基准、16位的高精度ADC对LMT70数据的温度模拟量进行采集。系统不带屏幕的外观二、GUI的设计本系统为了更好的人机交互采用4.3寸触摸屏搭载开源图形库LVGL一方面将显示波形和数据与心电信号的采集和处理隔离开另一方面是为了交互的方便和美观系统运行界面如下图所示。整体界面主要有菜单、返回、图表、数据栏、导联状态灯以及开启心电采集按钮。系统的菜单是通过LVGL的roller控件绘制的roller里面选项的事件则是通过回调函数的形式调用。由于选中roller的选项LVGL就会返回选项的值因此我自己设计了函数指针数组来注册回调函数并且将这个选中的序号通过数组方式调用函数代码如下所示void(*oper_fuc[4])();//函数指针数组voidMenuitem_Init(void){oper_fuc[0]send_type_server;oper_fuc[1]Set_chart_div_line;oper_fuc[2]clear_step;oper_fuc[3]smooth_filter;}staticvoidroller_event_handler(lv_obj_t*obj,lv_event_t event){staticunsigned char count0;if(eventLV_EVENT_VALUE_CHANGED){countlv_roller_get_selected(obj);}if(eventLV_EVENT_CLICKED){oper_fuc[count]();}}roller_event_handler是选中roller中的事件函数在事件函数里面来回调选项的处理函数。roller中总共写了4个选项分别为send_type选择发送类型支持发送到本地显示或者串口发送给上位机、set_div_line是否设置图表的等分线、clear_step清除界面上的数据、smooth_filter是否进行平滑滤波本系统还设计导联状态指示灯前面讨论过ADS1292R可以检测电联的脱落状态因而这里用LVGL的led控件作为导联的状态指示当检测到导联接入人体led控件就会点亮。设计了红心周期性跳动当检测到导联接入人体后红心就会周期性跳动当心电数据采样开始后红心随着心率值的改变而跳动着。同时还设计了采样开始/停止按钮可以随时暂停和开始采样心电信号。除了以上看得见的设计之外还创建了四个周期性的任务任务优先级从高到低分别为更新数据栏里的数据、更新导联状态、检查心电信号的纵轴范围、系统状态的检查。三、导联体系的选择心电信号本质上是测量人体体表的电信号将电极通过一定的导联体系就可以记录到心电图因而选择合适的电极是观察心电图至关重要的选择。在医学上常见的导联体系分别为标准12导联体系、Lewis导联、Fontaine导联、Cabrera导联、Nehb导联、frank导联、Mason-Likar导联等。标准12导联体系是医院所使用的它由3个双极肢体导联、6个单极胸前导联、3个单极加压肢体导联所组成。该系统的主要目的是实时检测心率和QRS宽度因此选择的导联应该基于能观察心电中R波较大的原则。因而选择标准12导联中标准肢体导联I见图左或者Mason-Likar导联见图右。四、心电电极选择人体的内阻很高因而心电信号是一个高内阻且幅度很低的信号如果处理不好就会造成心电信号的衰减因此就需要从两方面解决1降低与电极的接触阻抗2提高采集电路的输入阻抗。目前市面上有三种电极分别为湿电极、干电极和非接触式电极这三种电极中湿电极的接触电阻最小因而对于模拟前端的输入电阻不需要太大。湿电极主要由电极片、Ag/AgCl 涂层、导电胶等物质组成。医学电极贴片与身体接触的是水凝胶亲水化合物“黑色”部分为Ag/AgCl使用导电金属和导线与仪器连接实物如图所示。五、心电信号时域和频域特征人体的心电信号是一种非平稳、非线性、随机性比较强的微弱生理信号,幅值约为毫伏(mV)级,频率在0.05-100Hz之间。心电信号的每一个心跳循环由一系列有规律的波形组成它们分别是P波、QRS复合波和T波而这些波形的起点、终点、波峰、波谷、以及间期分别记录着心脏活动状态的详细信息心电信号各个波段的详细说明如下心电各个波段的功率谱如下心电信号的噪声分析如下读者想对心电信号进一步了解可以参考如下链接http://www.mythbird.com/ecgxin-hao-te-zheng/。六、软件设计6.1、系统总体设计系统先从硬件初始化开始其中包括串口初始化、触摸屏初始化、外部SRAM初始化、ADS1292R初始化、LMT70初始化、LVGL心跳定时器初始化。其次就是LVGL初始化主要是一些主题和变量的初始化。然后创建系统的UI界面和一些定时的任务。最后初始化心电数据缓存、 数字低通滤波器初始化、心率数据缓存初始化。完成以上的初始化系统便进入主循环等待心电数据输入缓存中出现数据随后开始滤波将滤波之后的数据写入心电输出缓存中然后轮询LVGL的任务和触摸屏扫描。就这样不停地循环。其中心电输入缓存中的数据是通过中断从ADS1292R的输出引脚中读取而心电输出缓存则是原始数据经过低通处理后的数据等待LVGL显示任务的到来并显示在触摸屏上。系统总体框图和软件框图如下所示6.2、系统总体设计在前面讨论过心电信号频谱和噪声因而要对心电信号进行滤波为了同时实现心电信号的实时滤波和心电波形实时显示所以有必要设计一个缓存区来解决这个难题。这里我打算用我自己设计的两个循环队列解决这个难题。为了使得在滤波的时候心电数据依然能够采集设计两个循环队列如上图所示其中IN_Buffer和OUT_Buffer的每个矩形框表示25x4个字节的空间这取决一次需要多少字节的数据滤波。这里一次滤波需要25个int型的数据因而每个缓存需要25x4字节。图中的蓝色填充表示缓存区中填满了数据每次读完数据之后都需要切换缓存区且IN_Buffer和OUT_Buffer的读写操作相反即IN_Buffer的读操作是OUT_Buffer的写操作程序框图如下图所示。图上所示的三个程序均是并行处理的程序1是通过外部中断的服务函数调用的程序2则是在UI画图程序里面通过定时器周期性的调用程序3则是在主程序中的滤波函数里面调用程序1代码如下ADS1292R采用中断方式读取数据voidEXTI9_5_IRQHandler(void){if(EXTI-IMREXTI_Line5ADS_DRDY0)//数据接收中断{ADS1292_Read_Data(ads1292_Cache);//数据存到9字节缓冲区Update_ECG_Data(ads1292_Cache);Cheack_lead_stata(ads1292_Cache);if(state_pcb.SampleStartFlagtrue)WriteAdsInBuffer(ecg_info.ecg_data);//数据写入缓存区}EXTI_ClearITPendingBit(EXTI_Line5);}程序2代码如下LVGL的心跳在定时器中周期调用同时程序2也在其中运行主要从滤波后的数据缓存中取出数据进行波形显示voidWave_show(void){int value0;if(ReadEcgOutBuffer(value)!0){if(ecg_graph.send_typeGRAPH){ecg_graph.y_poseTransf_EcgData_To_Vert(value,ecg_graph.sacle);chart_add_data(ecg_graph.y_pose);set_data_into_heart_buff(ecg_graph.y_pose);}elseif(ecg_graph.send_typeUSART){//EcgSendByUart(value);printf(%d\r\n,(int)alg(value/200));}}}//定时器3中断服务程序voidTIM3_IRQHandler(void){staticu8 show_cnt0;if(TIM3-SRTIM_IT_Update)//溢出中断{show_cnt;lv_tick_inc(1);//lvgl的1ms心跳if(show_cnt3){show_cnt0;Wave_show();}}TIM3-SR(uint16_t)~TIM_IT_Update;}程序3代码如下在滤波函数中调用用于承上启下即从IN缓存中取出数据滤波之后写入OUT缓存中voidarm_fir_f32_lp(void){float32_t*inputf32,*outputf32;if(ReadAdsInBuffer()WriterEcgOutBuffer()){//指针定位成功/* 初始化输入输出缓存指针 */inputf32(float32_t*)InFifoDev.rp;outputf32(float32_t*)OutFifoDev.wp;/* 实现FIR滤波 */arm_fir_f32(S,inputf32,outputf32,BLOCK_SIZE);//my_memcpy(OutFifoDev.wp,InFifoDev.rp,BLOCK_SIZE*4);InFifoDev.state[InFifoDev.read_front]Empty;InFifoDev.read_front(InFifoDev.read_front1)%PACK_NUM;//切换读缓存块OutFifoDev.state[OutFifoDev.writer_rear]Full;OutFifoDev.writer_rear(OutFifoDev.writer_rear1)%PACK_NUM;//切换写缓存块}}关于缓存切换代码如下staticvoidWriteAdsInBuffer(int date){staticu8 cnt0;if(InFifoDev.state[InFifoDev.writer_rear]Empty){//缓存块可写InFifoDev.wpAdsInBuffer[InFifoDev.writer_rear*(BLOCK_SIZE)];//将写指针定位写缓存块InFifoDev.wp[cnt]date;if(cntBLOCK_SIZE){cnt0;InFifoDev.state[InFifoDev.writer_rear]Full;InFifoDev.writer_rear(InFifoDev.writer_rear1)%PACK_NUM;//切换写缓存块}}}//定位读指针//成功则返回1不成功则返回0u8ReadAdsInBuffer(void){if(InFifoDev.state[InFifoDev.read_front]Full){//缓存块可读InFifoDev.rpAdsInBuffer[InFifoDev.read_front*(BLOCK_SIZE)];//将读指针定位读缓存块return1;}return0;}//定位读指针u8WriterEcgOutBuffer(void){if(OutFifoDev.state[OutFifoDev.writer_rear]Empty){//缓存块可写OutFifoDev.wpEcgOutBuffer[OutFifoDev.writer_rear*(BLOCK_SIZE)];//将读指针定位读缓存块return1;}return0;}//成功则返回1不成功则返回0u8ReadEcgOutBuffer(int32_t*p){staticu8 cnt0;if(OutFifoDev.state[OutFifoDev.read_front]Full){//缓存块可读OutFifoDev.rpEcgOutBuffer[OutFifoDev.read_front*(BLOCK_SIZE)];//将写指针定位读缓存块*pOutFifoDev.rp[cnt];if(cntBLOCK_SIZE){cnt0;OutFifoDev.state[OutFifoDev.read_front]Empty;OutFifoDev.read_front(OutFifoDev.read_front1)%PACK_NUM;//切换写读缓存块}return1;}return0;}6.3、心电信号滤波工频噪声滤除滤除工频噪声的数字滤波算法主要有经典滤波器、小波变换、自适应滤波。小波变换能将心电信号进行多层分解可以使得心电信号与工频噪声分离但是计算量大所占用的中间变量也比较多对于单片机来说处理的速度也不够快因而对于系统的实时性这一指标很难实现。自适应滤波能够自动跟踪工频噪声的改变但是需要增加一个输入信号作为参考因而增加了系统的复杂性。在前面也讨论过心电信号95%的能量都是集中在0~40Hz,而工频噪声则在50Hz左右过渡带比较宽因而可以选择截止频率为40Hz的低通滤波器。该低通滤波器利用MATLAB的FDATOOL生成只需要选择低通滤波器是FIR结构选择Blackman-Harris窗函数滤波器的阶数定为50选择采样频率为250Hz,截止频率为40Hz,参数如下图所示然后利用FDATOOL生成的冲激响应的数组选择ARM官方的DSP库调用arm_fir_f32函数既可以完成一次滤波。但是在这之前需要调用arm_fir_init_f32进行初始化。滤波器系数如下constfloat32_t fir32LP[NUM_TAPS]{-7.484454468902e-22,-3.269336712398e-06,-1.365915864079e-05,-5.014073980636e-06,6.804735231975e-05,0.0001662336497003,7.965197426322e-05,-0.0003784662837741,-0.0008928563387901,-0.0005280588787408,0.001284875839485,0.003225662215767,0.0022425431358,-0.003157084585057,-0.009028737319977,-0.007219934929014,0.006057868257093,0.02144319498633,0.01971312591228,-0.009448071870685,-0.04806332586811,-0.05291973061693,0.01224382260678,0.1388254178822,0.2663085232723,0.3199984843521,0.2663085232723,0.1388254178822,0.01224382260678,-0.05291973061693,-0.04806332586811,-0.009448071870685,0.01971312591228,0.02144319498633,0.006057868257093,-0.007219934929014,-0.009028737319977,-0.003157084585057,0.0022425431358,0.003225662215767,0.001284875839485,-0.0005280588787408,-0.0008928563387901,-0.0003784662837741,7.965197426322e-05,0.0001662336497003,6.804735231975e-05,-5.014073980636e-06,-1.365915864079e-05,-3.269336712399e-06,-7.484454468902e-22};staticfloat32_t firStateF32[BLOCK_SIZENUM_TAPS-1];arm_fir_instance_f32S;voidarm_fir_Init(void){arm_fir_init_f32(S,NUM_TAPS,(float32_t*)fir32LP[0],firStateF32[0],BLOCK_SIZE);}滤波函数见程序3往上找基线漂移基线漂移与工频噪声不同它是由于呼吸和电极滑动变化所异致的其频率一般低于1Hz左右。常见对于基线漂移滤除的数字算法有高通滤波器、中值滤波、小波变换、形态学滤波、曲线拟合等其中高通滤波器可能会对心电信号的ST波段产生影响毕竟基线漂移的频率也在ST波段里面。曲线拟合对较大的基线漂移处理能力较弱处理的效果与处理数据的长度成正相关因而不适用实时处理的系统。小波变换计算量大也不适用实时处理的系统。相比之下形态学滤波对心电信号的基线漂移滤除效果更好计算量也比中值滤波小。但是形态学滤波要求数据长度足够长因而会改变前面的缓存结构并且在本系统中并未太严重的基线漂移系统的任务也比较多多方面权衡之下选择不处理基线漂移。肌电噪声的抑制肌电噪声主要是由于人体肌肉颤抖导致体表的电位发生变化这种噪声通过电极贴传导至心电模拟前端并且这种噪声持续时间较短使得ECG信号波形产生细小的波纹这种噪声频率分布比较广前面已经将心电信号通过截止频率为40Hz的低通滤波器因而需要5点平滑滤波将细小的波纹滤除为了不影响心电信号的实时处理因而改进版的平滑滤波器代码如下/* * 滑动平均值滤波。 * 每调用一次就加入一个新数据并得到当前的滤波值。 */floatalg(float new_val){/* 用一个减法就做了丢弃最旧的数据加入最新的数据这一操作 */sum(new_val-buf[pos]);buf[pos]new_val;pos(pos1)%MAX_COUNT;/* 个数不足时cnt是实际个数个数足够时cnt最多也只是MAX_COUNT */pcnt(pcntMAX_COUNT);returnsum/MAX_COUNT;}6.4、心率和QRS宽度检测心率和QRS宽度检测作为本系统的算法核心有了心率值和QRS宽度值才能进一步判断常见的心律失常。心率基本上都是检测两个R波之间的时隙来计算的常见检测R的算法主要有阈值法、模板法和语句描述法。而本系统的心率和QRS宽度检测算法是在一起检测的所采用的算法是幅度阈值检测和差分检测相结合因为观察心电信号的R波发现R波是具有窄的脉冲且脉冲的幅度是心电信号最高的因而采用幅度和一阶差分共同约束找到R波同时在找R波的同时还可以估计出QRS的宽度算法的框图如图心率检测和QRS宽度检测算法是采用状态机的编程思想通过R波幅度大且从Q到R一直递增并且R波到S波的一阶差分值很大从而将R波定位出来检测两个R波之前的时间然后通过如下公式就可以计算出心率H R ( 60 ∗ S a m p l e R a t e ) / c o u n t HR(60*SampleRate) /countHR(60∗SampleRate)/count而QRS宽度则是由Q R S Q R S c n t ∗ 2.2 ∗ 1000 / ( S a m p l e R a t e ) QRSQRScnt*2.2* 1000/(SampleRate)QRSQRScnt∗2.2∗1000/(SampleRate)上式中的2.2是估计值因为QRS_cnt是在检测到R波之后才开始计数并且未到S波谷停止计数观察QRS波发现Q到R与R到S近似对称因而采用2.2这个估计值这也是实时检测的缺陷检测的样本不多。心率算法和QRS宽度检测代码如下/** * Brief 测量心率 * Call * Param * Note * Retval */voidecg_heart_rate(int data){int Signaldata;if(Signalhr.vmax)hr.vmaxSignal;if(Signalhr.vmin)hr.vminSignal;threshhr.vmax-(hr.vmax-hr.vmin)/5;for(uint16_t i0;iDATA_NUM_CAL_HR-2;i){DataArrayCalHR[i]DataArrayCalHR[i1];}DataArrayCalHR[DATA_NUM_CAL_HR-1]Signal;Diff_Arrray(DiffDataArrayCalHR,DataArrayCalHR,DATA_NUM_CAL_HR);//差分if(hr.flagStartDetected){uint8_t FlagAllDiffRisetrue;for(uint16_t i0;iDATA_NUM_CAL_HR-2;i)//判断波形是否一直上升{if(DiffDataArrayCalHR[i]0){FlagAllDiffRisefalse;break;}}if(FlagAllDiffRisetrue){hr.flagQWave;}}elseif(hr.flagQWave)//已经找Q波{if(DataArrayCalHR[DATA_NUM_CAL_HR-1]thresh){if(hr.count125){if(hr.firstBeattrue)//如果已经找到 过R波{hr.rate(float)60*SAMPLE_RATE/(hr.count);hr.count0;//清除计数hr.flagRWave;QRScntflagtrue;}elseif(hr.firstBeatfalse){hr.firstBeattrue;hr.count0;//清除计数hr.flagRWave;QRScntflagtrue;}}}}elseif(hr.flagRWave){if(DiffDataArrayCalHR[0]-(hr.vmax-hr.vmin)/5){hr.flagSWave;}}elseif(hr.flagSWave){if(hr.QRS_cnt15){hr.flagStartDetected;hr.QRShr.QRS_cnt*22*100/SAMPLE_RATE;hr.QRS_cnt0;QRScntflagfalse;}else{hr.flagStartDetected;hr.QRS0;hr.QRS_cnt0;QRScntflagfalse;}}if(hr.count420){hr.firstBeatfalse;hr.flagStartDetected;hr.vmaxHeart_MIN;hr.vminHeart_MAX;hr.rate0;thresh0;hr.count0;//清除计数}}七、实机演示实机演示八、总结与展望本系统因为没有加入操作系统的管理造成实现的功能较为少并且数据分析功能因为缺乏强大处理器造成数据分析功能所需要的指标太少要想对心电信号实现自动化分析必须对心电信号更多的信息进行提前而且由于处理器的限制使得一些强大的数字算法是用不了而且将采集—滤波—显示集成一体化本身就显得笨重会让每个处理单元相互牵制严重的会影响系统的采样率造成一些不必要的误差。所以日后会有针对选择更为强大的处理器会将采集、滤波、显示分开来。同时为了减少外界噪声应该选择更为干净的电源和屏蔽外壳系统也不能以这种模块化的方式拼接在一起日后会选择画PCB将所有模块集成在PCB上在套上屏蔽壳这样能够最大程度减少外界噪声干扰。并且无线通信模块保留着但是上位机并未实现因而日后需要增加这一项功能。总结以上的不足如下使用更为强大的处理器将采集、滤波、显示分开来。所有模块应该集成在PCB上在增加屏蔽壳。开发上位机软件实现心电数据的无线通信的功能。代码我会放在github上链接如下https://github.com/lvzhe-speed/STM32_ECG******************************************** 更新于2026年8月9日***************************************************gitee上的链接如下如果github上不去就下这个https://gitee.com/keltop/STM32_ECG******************************************** 更新于2026年8月9日***************************************************后面我去考研了希望能够考上以后会每一年至少写一篇技术博客谢谢各位大佬前来斧正欢迎探讨一起技术进步。其实我也想借此说一下不能因为一次失败就否定自己之前的努力100-10这也许是别人对你的评价但自己不能认同这个错误的式子人生不仅有加法也有减法。也希望对别人多一点谅解每个人都不容易也许今日之菜鸟明日之大鹏总会翱翔九天。******************************************** 更新于2026年8月9日***************************************************三年过去了研究生也毕业了干了三年的雷达算法现在在半导体行业又干回老本行了。承诺以后写更多技术博客.******************************************** 更新于2026年8月9日***************************************************