十年前接触生物电子让我对电子产生浓厚的兴趣,让我感到电子科技的博大精深无所不能。最近用stm32和C#实现心电监测,分享给大家一起探讨,我也把这些技术资料整理下。
原理图
心电前端采集电路采用仪表放大器,仪表放大器对于共模干扰有很强的抑制力,适合做心电采集前端电路。传输部分采用USB实现虚拟串口和上位机对接,具体电路如下所示
PCB
上位机程序
实物如下
单片机采用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]);
}
}
}
}