十年前接触生物电子让我对电子产生浓厚的兴趣,让我感到电子科技的博大精深无所不能。最近用stm32和C#实现心电监测,分享给大家一起探讨,我也把这些技术资料整理下。

原理图

心电前端采集电路采用仪表放大器,仪表放大器对于共模干扰有很强的抑制力,适合做心电采集前端电路。传输部分采用USB实现虚拟串口和上位机对接,具体电路如下所示

tsfresh 心电 心电st-t_tsfresh 心电

PCB

tsfresh 心电 心电st-t_C#_02

上位机程序

tsfresh 心电 心电st-t_ECG_03

 

 

实物如下

tsfresh 心电 心电st-t_C#_04

单片机采用stm32内部AD采集,关键程序如下

#include "sys.h"
#include "delay.h"
#include "led.h" 		 	 
#include "usb_lib.h"
#include "hw_config.h"
#include "usb_pwr.h"	 
#include "usb_prop.h"
#include "bsp_usart.h"
#include "bsp_adc.h"

extern char USB_TX_data[512],USB_RX_data[512];
extern u8 USB_Tx_Counter,USB_Rx_Counter,USB_TX_flag,USB_RX_flag;
extern char U1_TX_data[512],U1_RX_data[512];
extern u8 U1_Tx_Counter,U1_Rx_Counter,U1_TX_flag,U1_RX_flag;

extern short	AD_BUF[1024];
extern unsigned int TIM3_count;
extern char TIM3_flag;

void RCC_HSI_Configuration(void)
{
    RCC_DeInit();//??? RCC?????????

    RCC_HSICmd(ENABLE);//??HSI  
    while(RCC_GetFlagStatus(RCC_FLAG_HSIRDY) == RESET)//??HSI????
    {
    }

    if(1)
    {
        FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
        FLASH_SetLatency(FLASH_Latency_2);
        
        RCC_HCLKConfig(RCC_SYSCLK_Div1);   
        RCC_PCLK1Config(RCC_HCLK_Div2);
        RCC_PCLK2Config(RCC_HCLK_Div1);
        RCC_PLLConfig(RCC_PLLSource_HSI_Div2, RCC_PLLMul_12); 
        RCC_PLLCmd(ENABLE);//??PLL???????,????????
        while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET)
        {
        }
        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);  
        while(RCC_GetSYSCLKSource() != 0x08)
        {
        }
    }
}
 int main(void)
 {	 
 	u16 i;
	u16 temp;
	u8 usbstatus=0;	
	RCC_HSI_Configuration();
	delay_init();	    	 //ÑÓʱº¯Êý³õʼ»¯	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	 //ÉèÖÃNVICÖжϷÖ×é2:2λÇÀÕ¼ÓÅÏȼ¶£¬2λÏìÓ¦ÓÅÏȼ¶
	LED_Init();
	USART1_Config();
	ADC_Config();
	
	delay_ms(10000);
	USB_Port_Set(0); 	
	delay_ms(50);
	USB_Port_Set(1);	
 	Set_USBClock();   
 	USB_Interrupts_Config();    
 	USB_Init();	
	
	while(1)
	{
		if(TIM3_flag==1)
		{
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
			TIM3_flag=0;
			temp=ADC_GetConversionValue(ADC1);
			USB_USART_SendData(0x55);//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB
			USB_USART_SendData(0xaa);//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB
			USB_USART_SendData(temp>>8);//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB
			USB_USART_SendData(temp);//ÒÔ×Ö½Ú·½Ê½,·¢Ë͸øUSB 
			USB_USART_RX_STA=0;
			GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
		}
		if(USB_RX_flag==1)
		{
			USB_RX_flag=0;
		}
		if(usbstatus!=bDeviceState)//USBÁ¬½Ó״̬·¢ÉúÁ˸ıä.
		{
			usbstatus=bDeviceState;//¼Ç¼ÐµÄ״̬
		}
	}
}
void ADC_Config(void)
{
	GPIO_InitTypeDef  GPIO_InitStructure;
	NVIC_InitTypeDef   NVIC_InitStructure;
	ADC_InitTypeDef   ADC_InitStructure;
	TIM_TimeBaseInitTypeDef			TIM_TimeBaseInitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1  , ENABLE );
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	RCC_APB1PeriphClockCmd( RCC_APB1Periph_TIM3 , ENABLE);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//Ä£ÄâÊäÈë
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;  
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;  
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; 
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;  
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; 
	ADC_InitStructure.ADC_NbrOfChannel = 1;  
	ADC_Init(ADC1, &ADC_InitStructure);  
	
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_239Cycles5);
	
	ADC_Cmd(ADC1, ENABLE);  
	
	ADC_ResetCalibration(ADC1);  
	while(ADC_GetResetCalibrationStatus(ADC1)); 
	ADC_StartCalibration(ADC1);  
	while(ADC_GetCalibrationStatus(ADC1)); 
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE); 

	TIM_DeInit(TIM3);        //½«ÍâÉèTIM3¼Ä´æÆ÷ÖØÉèΪȱʡֵ  
	TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1 ;    //ÉèÖÃÁËʱÖÓ·Ö¸î(Tck_tim) 
	TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up ;   //Ñ¡ÔñÁ˼ÆÊýÆ÷ģʽ(TIMÏòÉϼÆÊýģʽ)  
	TIM_TimeBaseInitStruct.TIM_Period = 999 ;       //É趨¼ÆÊýÆ÷×Ô¶¯ÖØ×°Öµ,È¡Öµ·¶Î§0x0000~0xFFFF   
	TIM_TimeBaseInitStruct.TIM_Prescaler = 47 ;    //ÉèÖÃÓÃÀ´×÷ΪTIM3ʱÖÓƵÂʳýÊýµÄÔ¤·ÖƵֵΪ(7199+1),È¡Öµ·¶Î§0x0000~0xFFFF 
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct ) ;       

	TIM_ClearFlag(TIM3, TIM_FLAG_Update);         //Çå³ýTIM3µÄ´ý´¦Àí±ê־λ    
	TIM_ITConfig(TIM3, TIM_IT_Update,ENABLE);     //ʹÄÜTIM3ÖÐ¶Ï  
	TIM_Cmd(TIM3, ENABLE);         //ʹÄÜTIM3ÍâÉè  
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //NVIC_Group:ÏÈÕ¼ÓÅÏȼ¶2룬´ÓÓÅÏȼ¶2λ  
  NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;    //ÅäÖÃΪTIM3ÖÐ¶Ï  
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  //ÏÈÕ¼ÓÅÏȼ¶Îª1  
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;   //´ÓÓÅÏȼ¶Îª2  
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;      //ʹÄÜÖжÏͨµÀ  
  NVIC_Init(&NVIC_InitStructure); 
}

上位机采用C#,C#开发window系统应用程序非常方便,程序如下

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApplication4
{
    public partial class Form1 : Form
    {
        private StringBuilder sb = new StringBuilder();     //为了避免在接收处理函数中反复调用,依然声明为一个全局变量
        long AD_num = 12;
        long LD_num = 1;
        long MD_num = 11;
        long QP_num = 0;
        int QP_flag = 0;
        long uart_count = 0;
        private const int Unit_length = 32;//单位格大小
        private const int X_End = 1024+512+256+48;//Y轴最大数值
        private const int Y_End = 512+256+128;//Y轴最大数值
        private const int X_Start = 48;//Y轴最大数值
        private const int Y_Start = 128;//Y轴最大数值
        private const int MaxStep = 33;//绘制单位最大值
        private const int MinStep = 1;//绘制单位最小值
        private const int StartPrint = 100;//点坐标偏移量
        private List<int> DataList = new List<int>();//数据结构----线性链表
        private Pen TablePen = new Pen(Color.FromArgb(0x80, 0x00, 0x00));//轴线颜色
        private Pen LinesPen = new Pen(Color.FromArgb(0x00, 0x80, 0x80));//波形颜色
        public Form1()
        {
            this.SetStyle(ControlStyles.DoubleBuffer | ControlStyles.UserPaint |
                           ControlStyles.AllPaintingInWmPaint,
                           true);//开启双缓冲
            this.UpdateStyles();
            InitializeComponent();
            System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
            TablePen.DashStyle = System.Drawing.Drawing2D.DashStyle.DashDotDot;
            SearchAndAddSerialToComboBox(serialPort1, comboBox1);
        }
        private void SearchAndAddSerialToComboBox(SerialPort MyPort, ComboBox MyBox)
        {                                                               //将可用端口号添加到ComboBox
            string Buffer;                                              //缓存
            comboBox1.Items.Clear();                                        //清空ComboBox内容
            //int count = 0;
            for (int i = 1; i < 30; i++)                                //循环
            {
                try                                                     //核心原理是依靠try和catch完成遍历
                {
                    Buffer = "COM" + i.ToString();
                    MyPort.PortName = Buffer;
                    MyPort.Open();                                      //如果失败,后面的代码不会执行
                                                                        // MyString[count] = Buffer;
                    comboBox1.Items.Add(Buffer);                            //打开成功,添加至下俩列表
                    MyPort.Close();                                     //关闭
                }
                catch
                {
                }
            }
        }
        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                //将可能产生异常的代码放置在try块中
                //根据当前串口属性来判断是否打开
                if (serialPort1.IsOpen)
                {
                    //串口已经处于打开状态
                    serialPort1.Close();    //关闭串口
                    button1.Text = "打开串口";
                    button1.BackColor = Color.ForestGreen;
                    comboBox1.Enabled = true;
                    uart_count = 0;
                }
                else
                {
                    //串口已经处于关闭状态,则设置好串口属性后打开
                    comboBox1.Enabled = false;
                    serialPort1.PortName = comboBox1.Text;
                    serialPort1.BaudRate = 115200;
                    serialPort1.DataBits = 8;
                    serialPort1.Parity = System.IO.Ports.Parity.None;
                    serialPort1.StopBits = System.IO.Ports.StopBits.One;
                    serialPort1.Open();     //打开串口
                    button1.Text = "关闭串口";
                    button1.BackColor = Color.Firebrick;
                }
            }
            catch (Exception ex)
            {
                //捕获可能发生的异常并进行处理

                //捕获到异常,创建一个新的对象,之前的不可以再用
                serialPort1 = new System.IO.Ports.SerialPort();
                //刷新COM口选项
                comboBox1.Items.Clear();
                comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
                //响铃并显示异常给用户
                System.Media.SystemSounds.Beep.Play();
                button1.Text = "打开串口";
                button1.BackColor = Color.ForestGreen;
                MessageBox.Show(ex.Message);
            }
        }

        private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            int num = serialPort1.BytesToRead; 
            byte[] received_buf = new byte[num];
            int[] show_buf = new int[num];
            float show_data = 1;
            
            serialPort1.Read(received_buf, 0, num);           
            if (num == 4)
            {
                uart_count = uart_count + 4;

                show_data =  ((long)(received_buf[2] << 8) + (long)(received_buf[3]))*768/4096;

                
                show_buf[0] = (int)show_data;
                DataList.Add(show_buf[0]);//链表尾部添加数据
                Invalidate();            //刷新显示
                sb.Clear();
                try
                {
                    //因为要访问UI资源,所以需要使用invoke方式同步ui
                    this.Invoke((EventHandler)(delegate
                    {
                        textBox1.Clear();
                        textBox1.AppendText(uart_count.ToString("F2"));
                    }
                       )
                    );

                }
                catch (Exception ex)
                {
                    //响铃并显示异常给用户
                    System.Media.SystemSounds.Beep.Play();
                    MessageBox.Show(ex.Message);

                }
            }
        }
        private void Form1_Paint(object sender, PaintEventArgs e)//画
        {
            String Str = "";
            System.Drawing.Drawing2D.GraphicsPath gp = new System.Drawing.Drawing2D.GraphicsPath();
            e.Graphics.FillRectangle(Brushes.White, e.Graphics.ClipBounds);
            //Draw Y 纵向轴绘制
            for (int i = 0; i <= (X_End - X_Start) / Unit_length; i++)
            {
                e.Graphics.DrawLine(TablePen, X_Start + i * Unit_length, Y_Start, X_Start + i * Unit_length, Y_End);//画线
                gp.AddString(i.ToString(), this.Font.FontFamily, (int)FontStyle.Regular, 12, new RectangleF(X_Start + i * Unit_length - 7, Y_End + 4, 400, 50), null);//添加文字
            }
            //Draw X 横向轴绘制
            for (int i = 0; i <= (Y_End - Y_Start) / Unit_length; i++)
            {
                e.Graphics.DrawLine(TablePen, X_Start, Y_Start + i * Unit_length, X_End, Y_Start + i * Unit_length);//画线
               // if (i == 17)           break;
                gp.AddString((((12 - i) * Unit_length).ToString() ), this.Font.FontFamily, (int)FontStyle.Regular, 14, new RectangleF(X_Start - 50, Y_Start + i * Unit_length - 8, 400, 50), null);//添加文字
            }
            e.Graphics.DrawPath(Pens.Black, gp);//写文字
            if (DataList.Count - 1 >= (X_End - X_Start))//如果数据量大于可容纳的数据量,即删除最左数据
            {
                DataList.RemoveRange(0, DataList.Count - (X_End - X_Start) - 1);
            }
            for (int i = 0; i < DataList.Count - 1; i++)//绘制
            {
                e.Graphics.DrawLine(LinesPen, X_Start + i, Y_End - DataList[i], X_Start + (i + 1), Y_End - DataList[i + 1]);
            }

        }
    }
}