当前位置:   article > 正文

stm32实现心电监测-数据传输、滤波、波形显示_基于stm32的心电监测

基于stm32的心电监测

有一段时间因为一些事导致心脏不太好,偶尔出现砰砰重跳几下,这种情况不容易捕捉,一般需要心电监护,24小时记录心电波,这样可以判断早搏等。一般来说休息好能自己恢复,不用吃药,想想心电监测还是有点用处的,能看出点问题,值得开发研究。心电监护是综合性很强的电子项目,涉及到模拟电路、数字电路、数字信号处理、通信、波形显示等。心电监测前端硬件电路对信号进行放大滤波,单片机AD采集模拟信号转为数字信号,单片机对数字信号进行数字滤波,通过USB虚拟串口与上位机通信。上位机用C#编写,接收单片机的数字信号,进行波形绘制。

实物如下

上位机最终效果如下

 

项目中有如下几项关键技术值得研究

1、心电前端模拟电路

人体的心电信号幅度非常小,大概1mV左右,人体与心电电极的接触电阻比较大,需要前端电路有很高的输入阻抗,否则会因为接触电阻衰减信号。人体处在复杂的电磁环境中,相当于天线把各种信号耦合到心电电路中,其中最常见的是50Hz工频信号。

以前有人问我,示波器上的波形线这么粗呢,这个原因就是信号里叠加了很多噪声。上图是心电放大后的信号,可以看出信号上叠加了50Hz的噪声,幅度是三个心电波幅度的一半。从图中也可以看出这个心电信号的幅度是比较大的,大约几百毫伏,这是前端电路对心电信号放大的结果,电路原理图如下。

图中除了对信号放大以外还有两处关键的模拟滤波器,一处是C20和R22组成的高通滤波器,一处是R24和C22组成的低通滤波器。除此以外,C17、C18、C19、C21也有滤波的作用,总之,滤波的目的是把没有用的信号去除。但是,模拟滤波器的效果是有限的,需要结合数字信号处理,用数字滤波器去除噪声,数字滤波后面讲。

2、单片机采集及传输

单片机的采样和心电信号带宽有关,根据IEC规格,心电图的带宽要求从0.5Hz到150Hz。奈奎斯特抽样定理指若频带宽度有限的,要从抽样信号中无失真地恢复原信号,抽样频率应大于2倍信号最高频率。抽样频率小于2倍频谱最高频率时,信号的频谱有混叠。抽样频率大于2倍频谱最高频率时,信号的频谱无混叠。程序里信号采集使用定时器触发,这样能保证采集间隔的准确或者说采样率。

  1. void ADC_Config(void)
  2. {
  3. GPIO_InitTypeDef GPIO_InitStructure;
  4. ADC_InitTypeDef ADC_InitStructure;
  5. TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
  6. TIM_OCInitTypeDef TIM_OCInitStructure;
  7. DMA_InitTypeDef DMA_InitStructure;
  8. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1 , ENABLE );
  9. RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
  10. RCC_ADCCLKConfig(RCC_PCLK2_Div6);
  11. RCC_APB1PeriphClockCmd( RCC_APB1Periph_TIM2 , ENABLE);
  12. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
  13. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//Ä£ÄâÊäÈë
  14. GPIO_Init(GPIOA, &GPIO_InitStructure);
  15. /* DMA channel1 configuration */
  16. DMA_DeInit(DMA1_Channel1);
  17. DMA_InitStructure.DMA_PeripheralBaseAddr = ADC1_DR_Address; //ADCµØÖ·
  18. DMA_InitStructure.DMA_MemoryBaseAddr = (u32)&AD_BUF;//ÄÚ´æµØÖ·
  19. DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
  20. DMA_InitStructure.DMA_BufferSize = 500; //»º´æµ¥ÔªµÄ¸öÊý
  21. DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//ÍâÉèµØÖ·¹Ì¶¨
  22. DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //ÄÚ´æµØÖ·¹Ì¶¨
  23. DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //°ë×Ö
  24. DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
  25. DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //Ñ­»·´«Êä
  26. DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  27. DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
  28. DMA_Init(DMA1_Channel1, &DMA_InitStructure);
  29. DMA_ITConfig(DMA1_Channel1,DMA_IT_TC,ENABLE);//Æô¶¯ÖжϱêÖ¾
  30. DMA_Cmd(DMA1_Channel1, ENABLE);
  31. ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
  32. ADC_InitStructure.ADC_ScanConvMode = DISABLE;
  33. ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
  34. ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_CC2;
  35. ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  36. ADC_InitStructure.ADC_NbrOfChannel = 1;
  37. ADC_Init(ADC1, &ADC_InitStructure);
  38. ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_1Cycles5);
  39. ADC_DMACmd(ADC1, ENABLE); //ʹÄÜDMA´«Êä
  40. ADC_Cmd(ADC1, ENABLE);
  41. ADC_ResetCalibration(ADC1);
  42. while(ADC_GetResetCalibrationStatus(ADC1));
  43. ADC_StartCalibration(ADC1);
  44. while(ADC_GetCalibrationStatus(ADC1));
  45. ADC_SoftwareStartConvCmd(ADC1, ENABLE);
  46. TIM_TimeBaseInitStruct.TIM_Period = 400; //ÖØÔØʱµÄÖµ ´¥·¢Ê±¼ä2000us£¬ÆµÂÊ200Hz
  47. TIM_TimeBaseInitStruct.TIM_Prescaler = 480 - 1; //·ÖƵϵÊý
  48. TIM_TimeBaseInitStruct.TIM_ClockDivision = 0; //ʱÖÓ·Ö¸î
  49. TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; //ÏòÉϼÆÊý
  50. TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
  51. TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct); //µ÷Óÿ⺯ÊýдÈë¼Ä´æÆ÷
  52. TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //Ñ¡Ôñ¶¨Ê±Æ÷ģʽΪÂö³å¿í¶Èµ÷ÖÆģʽ1
  53. TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
  54. TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable;
  55. TIM_OCInitStructure.TIM_Pulse = 200;
  56. TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
  57. TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_Low;
  58. TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set;
  59. TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCIdleState_Reset;
  60. TIM_OC2Init(TIM2, &TIM_OCInitStructure);
  61. TIM_Cmd(TIM2, ENABLE);
  62. TIM_CtrlPWMOutputs(TIM2, ENABLE); //¿ØÖÆTIM2 PWMÊä³ö
  63. }

程序用DMA自动转换ADC,并且用半中断传输技术,避免传输数据时,数据被DMA改写。关键在这两个量DMA1_FLAG_TC1,DMA1_FLAG_HT1,即DMA缓存到一半时,触发中断,把这一半数据处理并对发送。另一半缓存自动转换,不影响前面的一半。

  1. if(DMA_GetFlagStatus(DMA1_FLAG_TC1)==SET) //»ñÈ¡±ê־룬ÅжÏÊÇ·ñ´«ÊäÍê³É
  2. {
  3. GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
  4. IIR_filter_H(); //Â˲¨Æ÷º¯Êý
  5. USB_USART_SendData('$');//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB
  6. for(i=0;i<250;i++)
  7. {
  8. Send_int_Data[i]=(int)(Send_float_Data[i+250]*1000); //floatת»»Îªint
  9. //Send_int_Data[i]=0x0650;
  10. int_to_char(Send_int_Data[i],Send_char_data); //intתcharÐÍÊý×é
  11. for(j=0;j<4;j++)
  12. {
  13. USB_USART_SendData(Send_char_data[j]);
  14. }
  15. }
  16. USB_USART_SendData('*');//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB
  17. USB_USART_RX_STA=0;
  18. GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
  19. DMA_ClearFlag(DMA1_FLAG_TC1); //´¦ÀíÍêÊý¾Ý,Çå³þ±ê־λ
  20. }
  21. if(DMA_GetFlagStatus(DMA1_FLAG_HT1)==SET) //»ñÈ¡±ê־룬ÅжÏÊÇ·ñ´«ÊäÍê³É
  22. {
  23. GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
  24. IIR_filter_L(); //Â˲¨Æ÷º¯Êý
  25. USB_USART_SendData('$');//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB
  26. for(i=0;i<250;i++)
  27. {
  28. Send_int_Data[i]=(int)(Send_float_Data[i]*1000); //floatת»»Îªint
  29. //Send_int_Data[i]=0x0650;
  30. int_to_char(Send_int_Data[i],Send_char_data); //intתcharÐÍÊý×é
  31. for(j=0;j<4;j++)
  32. {
  33. USB_USART_SendData(Send_char_data[j]);
  34. }
  35. }
  36. USB_USART_SendData('*');//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB
  37. USB_USART_RX_STA=0;
  38. GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
  39. DMA_ClearFlag(DMA1_FLAG_HT1); //´¦ÀíÍêÊý¾Ý,Çå³þ±ê־λ
  40. }

3、单片机信号处理

数字信号处理发展很快,其中数字滤波器解决了模拟滤波器滤波效果差的缺陷,从前面图中可以看出,上位机显示的心电波形比示波器的波形要清晰很多,原因就是在单片机中加入了数字滤波器,滤除50Hz工频信号。程序里用的是IIR 50Hz陷波器,IIR滤波器是单片机数字信号处理中用的比较多的,对于IIR滤波器,冲激响应理论上应会无限持续,其输出不仅取决于当前和过去的输入信号值,也取决于过去的信号输出值。常用的工频陷波器主要有IIR和FIR两种,其中FIR具有良好的线性相位,但是在同等滤波效果的情况下,IIR的阶数要比FIR少很多,一个两阶的IIR滤波器的效果FIR要付出100多阶的代价,阶数大意味着运算量大,对于一个MCU单片机来说这是得不偿失的,所以采用IIR滤波器来实现工频滤波。滤波器设计不得不提到MATLAB,他是很好的软件,能很方便的制作数字滤波器。只需填入几个参数可以实现

单片机程序如下

  1. ADC_ConvertedValueLocal =(float) (AD_BUF[i+250]>>2)/4096*3.3; // ¶Áȡת»»µÄADÖµ
  2. x0=ADC_ConvertedValueLocal; //ÊäÈëÐźÅ
  3. w0[0]=IIR_50Notch_A[0]*x0-IIR_50Notch_A[1]*w0[1]-IIR_50Notch_A[2]*w0[2];
  4. y0=IIR_50Notch_B[0]*w0[0]+IIR_50Notch_B[1]*w0[1]+IIR_50Notch_B[2]*w0[2];
  5. Send_float_Data[i+250]=y0;
  6. w0[2]=w0[1];
  7. w0[1]=w0[0];
  8. w1[2]=w1[1];
  9. w1[1]=w1[0];

4、C#解析单片机上传的数据

C#解析单片机上传的数据用列表的方法解决数据包连包的问题。C#接收一包数据SerialPort事件有事要产生一次或者多次事件,导到一包数据分成两包数据。这里用List型变量,每次通知有数据到来,就把数据加入list,处理完移除list。

  1. private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
  2. {
  3. int num = serialPort1.BytesToRead;
  4. byte[] received_buf = new byte[num];
  5. byte[] nreceived_buf = new byte[1002];
  6. int[] show_buf = new int[1002];
  7. float show_data = 1;
  8. int i;
  9. serialPort1.Read(received_buf, 0, num);
  10. m_buffer.AddRange(received_buf);
  11. if (m_buffer.Count != 0)
  12. {
  13. int HeadIndex = m_buffer.FindIndex(o => o == '$');
  14. if (HeadIndex == -1)
  15. {
  16. m_buffer.Clear();
  17. }
  18. else if (HeadIndex != 0) //不为开头移掉之前的字节
  19. {
  20. if (HeadIndex > 1)
  21. m_buffer.RemoveRange(0, HeadIndex);
  22. }
  23. if ((HeadIndex == 0) &&(m_buffer.Count > 1002))
  24. {
  25. m_buffer.CopyTo(0, nreceived_buf, 0, 1002);
  26. m_buffer.RemoveRange(0, 1002);
  27. uart_count = uart_count + 1000;
  28. for (i = 0; i < 1000; i++)
  29. {
  30. show_data = (((long)(((nreceived_buf[i+1]-0x30)*1000)+ (nreceived_buf[i + 2] - 0x30) * 100) + ((nreceived_buf[i + 3] - 0x30) * 10) + (nreceived_buf[i +4] - 0x30))/1);
  31. show_buf[0] = (int)show_data-300;
  32. DataList.Add(show_buf[0]);//链表尾部添加数据
  33. i++; i++; i++;
  34. }
  35. Invalidate(); //刷新显示
  36. sb.Clear();
  37. try
  38. {
  39. //因为要访问UI资源,所以需要使用invoke方式同步ui
  40. this.Invoke((EventHandler)(delegate
  41. {
  42. textBox1.Clear();
  43. textBox1.AppendText(uart_count.ToString("F2"));
  44. listBox1.Items.Add(DateTime.Now.ToString() +" " +uart_count.ToString() + " " + System.Text.Encoding.Default.GetString(nreceived_buf));
  45. listBox1.SelectedIndex = listBox1.Items.Count - 1;
  46. listBox1.SelectedIndex = -1;
  47. }
  48. )
  49. );
  50. }
  51. catch (Exception ex)
  52. {
  53. //响铃并显示异常给用户
  54. System.Media.SystemSounds.Beep.Play();
  55. MessageBox.Show(ex.Message);
  56. }
  57. }
  58. }
  59. }

5、C#波形绘制

C#波形绘制用到双缓冲技术

  1. public Form1()
  2. {
  3. this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |
  4. ControlStyles.AllPaintingInWmPaint,
  5. true);//开启双缓冲
  6. this.UpdateStyles();
  7. InitializeComponent();
  8. System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
  9. TablePen.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDotDot;
  10. SearchAndAddSerialToComboBox(serialPort1, comboBox1);
  11. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/236665
推荐阅读
相关标签
  

闽ICP备14008679号