基于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口一并引出。
本研究最重要的地方便是心電采集板,關于心電信號的采集板的芯片選擇TI公司的ADS1292R,外圍電路參考TI公司所給的原理圖和建議繪制,如圖所示。
關于ADS1292R的外圍電路的介紹和使用,這里推薦這篇博文,ADS1292R的使用
溫度檢測模塊采用LMT70溫度傳感器。其優點是:超小型、高精度、低功耗的模擬溫度傳感器。而缺點是:接觸式的溫度傳感器,測體表溫度存在一定誤差。但是考慮到溫度測量的精度以及測量方便,最終選擇LMT70作為測溫傳感器,同時選擇采用ADS1118具有PGA、電壓基準、16位的高精度ADC對LMT70數據的溫度模擬量進行采集。
二、GUI的設計
本系統為了更好的人機交互,采用4.3寸觸摸屏搭載開源圖形庫LVGL,一方面將顯示波形和數據與心電信號的采集和處理隔離開,另一方面是為了交互的方便和美觀,系統運行界面如下圖所示。整體界面主要有菜單、返回、圖表、數據欄、導聯狀態燈以及開啟心電采集按鈕。
系統的菜單是通過LVGL的roller控件繪制的,roller里面選項的事件則是通過回調函數的形式調用。由于選中roller的選項,LVGL就會返回選項的值,因此我自己設計了函數指針數組來注冊回調函數,并且將這個選中的序號通過數組方式調用函數,代碼如下所示:
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采用中斷方式讀取數據):
程序2代碼如下(LVGL的心跳在定時器中周期調用,同時程序2也在其中運行,主要從濾波后的數據緩存中取出數據進行波形顯示):
void Wave_show(void) {int value=0;if(ReadEcgOutBuffer(&value)!=0) {if(ecg_graph.send_type==GRAPH) {ecg_graph.y_pose=Transf_EcgData_To_Vert(value,ecg_graph.sacle);chart_add_data(ecg_graph.y_pose);set_data_into_heart_buff(ecg_graph.y_pose);} else if(ecg_graph.send_type==USART) {//EcgSendByUart(value);printf("%d\r\n",(int)alg(value/200));}} } //定時器3中斷服務程序 void TIM3_IRQHandler(void) { static u8 show_cnt=0; if(TIM3->SR&TIM_IT_Update)//溢出中斷{ show_cnt++;lv_tick_inc(1);//lvgl的1ms心跳if(show_cnt==3){show_cnt=0;Wave_show();}} TIM3->SR = (uint16_t)~TIM_IT_Update; }程序3代碼如下(在濾波函數中調用,用于承上啟下,即從IN緩存中取出數據,濾波之后寫入OUT緩存中):
void arm_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_front+1)%PACK_NUM;//切換讀緩存塊OutFifoDev.state[OutFifoDev.writer_rear]=Full;OutFifoDev.writer_rear=(OutFifoDev.writer_rear+1)%PACK_NUM;//切換寫緩存塊}}關于緩存切換代碼如下:
static void WriteAdsInBuffer(int date) {static u8 cnt=0;if(InFifoDev.state[InFifoDev.writer_rear]==Empty){//緩存塊可寫InFifoDev.wp=&AdsInBuffer[InFifoDev.writer_rear*(BLOCK_SIZE)];//將寫指針定位寫緩存塊InFifoDev.wp[cnt++]=date;if(cnt==BLOCK_SIZE){cnt=0;InFifoDev.state[InFifoDev.writer_rear]=Full;InFifoDev.writer_rear=(InFifoDev.writer_rear+1)%PACK_NUM;//切換寫緩存塊}} }//定位讀指針 //成功則返回1,不成功則返回0 u8 ReadAdsInBuffer(void) {if(InFifoDev.state[InFifoDev.read_front]==Full){//緩存塊可讀InFifoDev.rp=&AdsInBuffer[InFifoDev.read_front*(BLOCK_SIZE)];//將讀指針定位讀緩存塊return 1;}return 0; }//定位讀指針 u8 WriterEcgOutBuffer(void) {if(OutFifoDev.state[OutFifoDev.writer_rear]==Empty){//緩存塊可寫OutFifoDev.wp=&EcgOutBuffer[OutFifoDev.writer_rear*(BLOCK_SIZE)];//將讀指針定位讀緩存塊return 1;}return 0; }//成功則返回1,不成功則返回0 u8 ReadEcgOutBuffer(int32_t *p) {static u8 cnt=0;if(OutFifoDev.state[OutFifoDev.read_front]==Full){//緩存塊可讀OutFifoDev.rp=&EcgOutBuffer[OutFifoDev.read_front*(BLOCK_SIZE)];//將寫指針定位讀緩存塊*p=OutFifoDev.rp[cnt++];if(cnt==BLOCK_SIZE){cnt=0;OutFifoDev.state[OutFifoDev.read_front]=Empty;OutFifoDev.read_front=(OutFifoDev.read_front+1)%PACK_NUM;//切換寫讀緩存塊}return 1;}return 0; }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進行初始化。
濾波器系數如下:
濾波函數見程序3(往上找)
基線漂移與工頻噪聲不同,它是由于呼吸和電極滑動變化所異致的,其頻率一般低于1Hz左右。常見對于基線漂移濾除的數字算法有高通濾波器、中值濾波、小波變換、形態學濾波、曲線擬合等,其中高通濾波器可能會對心電信號的ST波段產生影響,畢竟基線漂移的頻率也在ST波段里面。曲線擬合對較大的基線漂移處理能力較弱,處理的效果與處理數據的長度成正相關,因而不適用實時處理的系統。小波變換計算量大,也不適用實時處理的系統。相比之下,形態學濾波對心電信號的基線漂移濾除效果更好,計算量也比中值濾波小。但是形態學濾波要求數據長度足夠長,因而會改變前面的緩存結構,并且在本系統中并未太嚴重的基線漂移,系統的任務也比較多,多方面權衡之下,選擇不處理基線漂移。
肌電噪聲主要是由于人體肌肉顫抖導致體表的電位發生變化,這種噪聲通過電極貼傳導至心電模擬前端,并且這種噪聲持續時間較短,使得ECG信號波形產生細小的波紋,這種噪聲頻率分布比較廣,前面已經將心電信號通過截止頻率為40Hz的低通濾波器,因而需要5點平滑濾波將細小的波紋濾除,為了不影響心電信號的實時處理,因而改進版的平滑濾波器代碼如下:
/** 滑動平均值濾波。* 每調用一次,就加入一個新數據,并得到當前的濾波值。*/ float alg(float new_val) {/* 用一個減法,就做了"丟棄最舊的數據,加入最新的數據"這一操作 */sum += (new_val - buf[pos]);buf[pos] = new_val;pos = (pos + 1) % MAX_COUNT;/* 個數不足時,cnt是實際個數,個數足夠時,cnt最多也只是MAX_COUNT */pcnt += (pcnt < MAX_COUNT);return sum / MAX_COUNT; }6.4、心率和QRS寬度檢測
心率和QRS寬度檢測作為本系統的算法核心,有了心率值和QRS寬度值才能進一步判斷常見的心律失常。心率基本上都是檢測兩個R波之間的時隙來計算的,常見檢測R的算法主要有閾值法、模板法和語句描述法。
而本系統的心率和QRS寬度檢測算法是在一起檢測的,所采用的算法是幅度閾值檢測和差分檢測相結合,因為觀察心電信號的R波,發現R波是具有窄的脈沖,且脈沖的幅度是心電信號最高的,因而采用幅度和一階差分共同約束找到R波,同時在找R波的同時還可以估計出QRS的寬度,算法的框圖如圖
心率檢測和QRS寬度檢測算法是采用狀態機的編程思想,通過R波幅度大且從Q到R一直遞增,并且R波到S波的一階差分值很大,從而將R波定位出來,檢測兩個R波之前的時間,然后通過如下公式就可以計算出心率:
HR=(60?SampleRate)/countHR=(60*SampleRate) /count HR=(60?SampleRate)/count
而QRS寬度則是由
QRS=QRScnt?2.2?1000/(SampleRate)QRS=QRScnt*2.2* 1000/(SampleRate) QRS=QRScnt?2.2?1000/(SampleRate)
上式中的2.2是估計值,因為QRS_cnt是在檢測到R波之后才開始計數,并且未到S波谷停止計數,觀察QRS波,發現Q到R與R到S近似對稱,因而采用2.2這個估計值,這也是實時檢測的缺陷,檢測的樣本不多。
心率算法和QRS寬度檢測代碼如下:
七、實機演示
實機演示
八、總結與展望
本系統因為沒有加入操作系統的管理,造成實現的功能較為少,并且數據分析功能因為缺乏強大處理器造成數據分析功能所需要的指標太少,要想對心電信號實現自動化分析,必須對心電信號更多的信息進行提前,而且由于處理器的限制使得一些強大的數字算法是用不了,而且將采集—濾波—顯示集成一體化本身就顯得笨重,會讓每個處理單元相互牽制,嚴重的會影響系統的采樣率,造成一些不必要的誤差。所以日后會有針對選擇更為強大的處理器,會將采集、濾波、顯示分開來。同時為了減少外界噪聲,應該選擇更為干凈的電源和屏蔽外殼,系統也不能以這種模塊化的方式拼接在一起,日后會選擇畫PCB,將所有模塊集成在PCB上,在套上屏蔽殼,這樣能夠最大程度減少外界噪聲干擾。并且無線通信模塊保留著,但是上位機并未實現,因而日后需要增加這一項功能。總結以上的不足如下:
代碼我會放在github上,鏈接如下:
https://github.com/lvzhe-speed/STM32_ECG
后面我去考研了,希望能夠考上,以后會每一年至少寫一篇技術博客,謝謝各位大佬前來斧正,歡迎探討,一起技術進步。其實我也想借此說一下,不能因為一次失敗就否定自己之前的努力,100-1=0這也許是別人對你的評價,但自己不能認同這個錯誤的式子,人生不僅有加法,也有減法。也希望對別人多一點諒解,每個人都不容易,也許今日之菜鳥,明日之大鵬,總會翱翔九天。
總結
以上是生活随笔為你收集整理的基于STM32F4的心电监护仪的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 心电电路算法滤波_一种新型心电信号滤波电
- 下一篇: 《设计你的人生》的部分经典语录