This experimental setup is a combination of the the 3.3V OLED display setup and the EKG/EMG shield with improved code, which does averaging of 4 RR intervals in order to calculate the heart rate. Also an annoying QRS-beep is added 🙂
//Simple Arduino ECG monitor with SSD1306 OLED display //Incorporates a simple QRS detection algorithm and heart rate calculation //the interrupt-based code parts are based on the Olimex approach //Requires the libraries included below! #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> //connect the OLED display in the following way: #define OLED_DC 11 #define OLED_CS 12 #define OLED_CLK 10 #define OLED_MOSI 9 #define OLED_RESET 13 Adafruit_SSD1306 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS); #if (SSD1306_LCDHEIGHT != 64) #error("Height incorrect, please fix Adafruit_SSD1306.h!"); #endif #include <compat/deprecated.h> #include <FlexiTimer2.h> //http://www.arduino.cc/playground/Main/FlexiTimer2 #include <TimerOne.h> //http://arduino.cc/playground/Code/Timer1 /* Erklärung von cbi, sbi, outp und inp Bei solchen Makros sollte man etwas mehr Klammern spendieren: #define sbi(ADDRESS,BIT) ((ADDRESS) |= (1<<(BIT))) #define cbi(ADDRESS,BIT) ((ADDRESS) &= ~(1<<(BIT))) #define outp(VAL,ADRESS) ((ADRESS) = (VAL)) #define inp(VAL) (VAL) The outb( ) function provides a C language interface to the machine instruction that writes a byte to an 8 bit I/O port using the I/O address space instead of the memory address space. */ // All definitions #define NUMCHANNELS 6 #define HEADERLEN 4 #define PACKETLEN (NUMCHANNELS * 2 + HEADERLEN + 1) //6*2+4+1 #define SAMPFREQ 256 // ADC sampling rate 256 #define TIMER2VAL (1024/(SAMPFREQ)) // Set 256Hz sampling frequency #define PWM_OUT 9 // Number of pin used for generating CAL_SIG #define PWMFREQ 10 //10Hz for Calibration signal //#define LED1 13 // Global constants and variables char const channel_order[]= { 0, 1, 2, 3, 4, 5 }; volatile unsigned char TXBuf[PACKETLEN]; //The transmission packet volatile unsigned char TXIndex; //Next byte to write in the transmission packet. volatile unsigned char CurrentCh; //Current channel being sampled. //~~~~~~~~~~ // Functions //~~~~~~~~~~ /****************************************************/ /* Function name: Toggle_LED1 */ /* Parameters */ /* Input : No */ /* Output : No */ /* Action: Switches-over LED1. */ /****************************************************/ //void Toggle_LED1(void){ // // if((digitalRead(LED1))==HIGH){ // digitalWrite(LED1,LOW); // } // else{ // digitalWrite(LED1,HIGH); // } //} /****************************************************/ /* Function name: setup */ /* Parameters */ /* Input : No */ /* Output : No */ /* Action: Initializes all peripherals */ /****************************************************/ void setup() { noInterrupts(); // Disable all interrupts before initialization // LED1 // pinMode(LED1, OUTPUT); //Setup LED1 direction // digitalWrite(LED1,LOW); //Setup LED1 state //Write packet header and footer TXBuf[0] = 0xa5; //Sync 0 TXBuf[1] = 0x5a; //Sync 1 TXBuf[2] = 2; //Protocol version TXBuf[3] = 0; //Packet counter // ADC // Timings for sampling of one 10-bit AD-value: // XTAL = 16000000MHz // prescaler > ((XTAL / 200kHz) = 80 => // prescaler = 128 (ADPS2 = 1, ADPS1 = 1, ADPS0 = 1) // ADCYCLE = XTAL / prescaler = 125000Hz or 8 us/cycle // 14 (single conversion) cycles = 112 us // 26 (1st conversion) cycles = 208 us outb(ADMUX, 0); //Select channel 0 outb(ADCSRA, ((1<<ADPS2) | (1<<ADPS1)| (1<<ADPS0))); //Prescaler = 128, free running mode = off, interrupts off. sbi(ADCSRA, ADIF); //Reset any pending ADC interrupts sbi(ADCSRA, ADEN); //Enable the ADC // Serial Port outb(UBRR0, 16); //Set speed to 57600 bps outb(UCSR0B, (1<<TXEN0)); //Enable USART Transmitter. // Timer1 // It's used for calibration signal generation: CAL_SIG via PWM. // CAL_SIG is used like reference signal when setting-up SHIELD-EKG/EMG's channel gain // During normal operation this signal is not required so it can be disabled with uncommenting te row below! /* pinMode(PWM_OUT, OUTPUT); //Set PWM_OUT direction digitalWrite(PWM_OUT,LOW); //Set PWM_OUT state Timer1.initialize((1000000/(PWMFREQ))); // initialize timer1, and set a 1/10 second period = 10Hz ->freq. of cal signal should be 10-14Hz (schematic) Timer1.pwm(PWM_OUT, 512); // setup pwm on pin 9, 50% duty cycle //Timer1.disablePwm(PWM_OUT); // Uncomment if CAL_SIG is not requiered */ // Timer2 // Timer2 is used for setting ADC sampling frequency. /***************************************************************** Methods of the FlexiTimer2 library: FlexiTimer2::set(unsigned long units, double resolution, void (*f)()) this function sets a time on units time the resolution for the overflow. Each overflow, "f" will be called. "f" has to be declared void with no parameters. E.g. units=1, resolution = 1.0/3000 will call f 3000 times per second, whereas it would be called only 1500 times per second when units=2. FlexiTimer2::set(unsigned long ms, void (*f)()) this function sets a time on ms (1/1000th of a second) for the overflow. Each overflow, "f" will be called. "f" has to be declared void with no parameters. Shorthand for calling the function above with resolution = 0.001. FlexiTimer2::start() enables the interrupt. FlexiTimer2::stop() disables the interrupt. *******************************************************************/ FlexiTimer2::set(TIMER2VAL, Timer2_Overflow_ISR); //TIMER2VAL was (1024/(SAMPFREQ)) in ms =4, SAMPLEFREQ was 256 FlexiTimer2::start(); //enable the Interrupt.... // MCU sleep mode = idle. outb(MCUCR,(inp(MCUCR) | (1<<SE)) & (~(1<<SM0) | ~(1<<SM1) | ~(1<<SM2))); interrupts(); // Enable all interrupts after initialization has been completed // by default, we'll generate the high voltage from the 3.3v line internally! (neat!) display.begin(SSD1306_SWITCHCAPVCC); // init done display.clearDisplay(); // clears the screen and buffer display.setTextSize(1); display.setTextColor(WHITE); } /****************************************************/ /* Function name: Timer2_Overflow_ISR */ /* Parameters */ /* Input : No */ /* Output : No */ /* Action: Determines ADC sampling frequency. */ /****************************************************/ void Timer2_Overflow_ISR() //alle 4ms wird das ausgeführt { // Toggle LED1 with ADC sampling frequency /2 //Toggle_LED1(); CurrentCh = 0; // Write header and footer: // Increase packet counter (fourth byte in header) //Write packet header and footer /**********zur Erinnerung: der Header********** TXBuf[0] = 0xa5; //Sync 0 TXBuf[1] = 0x5a; //Sync 1 TXBuf[2] = 2; //Protocol version TXBuf[3] = 0; //Packet counter ***********************************/ TXBuf[3]++; //the whole packet is /6*2+4+1=17byte //Get state of switches on PD2..5, if any (last byte in packet). TXBuf[2 * NUMCHANNELS + HEADERLEN] = (inp(PIND) >> 2) &0x0F; //2* NUMCHANNELS, weil jeder CHannel 2 byte hat damit 1024 reinpasst cbi(UCSR0B, UDRIE0); //Ensure Data Register Empty Interrupt is disabled. sbi(ADCSRA, ADIF); //Reset any pending ADC interrupts sbi(ADCSRA, ADIE); //Enable ADC interrupts. sbi(ADCSRA, ADSC) ; // Start conversion!!! //Next interrupt will be ISR(ADC_vect) } /****************************************************/ /* Function name: ISR(ADC_vect) */ /* Parameters */ /* Input : No */ /* Output : No */ /* Action: Reads ADC's current selected channel */ /* and stores its value into TXBuf. When */ /* TXBuf is full, it starts sending. */ /****************************************************/ ISR(ADC_vect) { volatile unsigned char i; //volatile?? i = 2 * CurrentCh + HEADERLEN; //also wird i auf 4 gesetzt wenn CurrentCh==0 und unten das 5. byte beschrieben,danach TxBuf[4] ([3] ist das letzte vom Header) TXBuf[i+1] = inp(ADCL); //ADC data register LOW byte TXBuf[i] = inp(ADCH); //ADC data register HIGH byte CurrentCh++; if (CurrentCh < NUMCHANNELS) { outb(ADMUX, (channel_order[CurrentCh])); //Select the next channel. sbi(ADCSRA, ADSC) ; //Start conversion!!! (set ADSC-bit in ADCSRA-Register) } else { //this gets executed first....prior to the stuff above outb(ADMUX, channel_order[0]); //Prepare next conversion, on channel 0. cbi(ADCSRA, ADIE); //Disable ADC interrupts to prevent further calls to ISR(ADC_vect). oben hiess es sbi!!!!!! outb(UDR0, TXBuf[0]); //Send first Packet's byte: Sync 0 sbi(UCSR0B, UDRIE0); //USART Data Register Empty Interrupt Enable TXIndex = 1; //Next interrupt will be ISR(USART_UDRE_vect) } } /****************************************************/ /* Function name: ISR(USART_UDRE_vect) */ /* Parameters */ /* Input : No */ /* Output : No */ /* Action: Sends remaining part of the Packet. */ /****************************************************/ ISR(USART_UDRE_vect){ outb(UDR0, TXBuf[TXIndex]); //Send next byte TXIndex++; /******hier also*** ch0hb = TxBuf[4]; ch0lb = TxBuf[5]; *******************/ if (TXIndex == PACKETLEN) //See if we're done with this packet { cbi(UCSR0B, UDRIE0); //USART Data Register Empty Interrupt Disable //Next interrupt will be Timer2_Overflow_ISR() } } //function for fusion of the ADCL and ADCH byte unsigned int weiterverarbeitung(volatile unsigned char high_byte, volatile unsigned char low_byte) { unsigned int value = ((high_byte&0x0f)*256)+(low_byte); return(value); } /****************************************************/ /* Function name: loop */ /* Parameters */ /* Input : last 2 channel bytes of the packet */ /* Output : to display */ /* Action: Draws ECG, detects QRS, calculates HR */ /****************************************************/ unsigned long Start, Finished = 0; int heart_rate[4]; int heart_rate_avg; float RR_interval = 0.0; unsigned int Delay = 2; unsigned int QRS_counter = 0; int thisdot = 0; int prevdot = 0; void loop() { //"heart rate" display.setCursor(1,52); display.print("heart rate:"); display.display(); // show it //show heart rate once per screen if(heart_rate_avg<220) { display.setCursor(80,52); display.print(heart_rate_avg); display.display(); // show it } //draw the actual graph: (128 = display width) for(int i=0; i<128; i++) { Finished = 0; //get the ADC value and scale it to the higth of the display unsigned int val = weiterverarbeitung(TXBuf[14],TXBuf[15]); //using A5 and extracting the last 2 channel bytes out of the packet unsigned int y = map (val, 0, 1023, 64, 0); //oben=0!! thisdot = y; //calculate the graph slope for QRS detection //slope can be negative so it has to be an SIGNED int int slope = prevdot - thisdot; //QRS complex detected above a certain threshold if (slope >= 8 && Start == 0) { //QRS Beep, use Pin 6 to not interfere with Timer 2!! tone(6, 2000, 50); //start "stop watch" Start = millis(); } else if(slope >= 8 && Start > 0) { //QRS Beep tone(6, 2000, 50); //stop Finished = millis(); //calculate a RR interval RR_interval = Finished - Start; if(RR_interval>=150) //refractory period, RR-intervals should be longer than this (filter method) { RR_interval = RR_interval/1000; //convert to seconds heart_rate[QRS_counter] = 60/RR_interval; //collect 4 intervals QRS_counter ++; //averaging calculation if(QRS_counter >= 3){ for(int j = 0; j<4; j++){ heart_rate_avg += heart_rate[j]; } heart_rate_avg /= 4; QRS_counter = 0; } } //reset Start value for time measurement Start = 0; } //Draw graph display.drawPixel(i, y, WHITE); display.display(); delay(Delay); prevdot = thisdot; thisdot = 0; slope = 0; } display.clearDisplay(); }
good night
really liked this project, I am trying to create a monitoring these a long time but could not yet, can you help me ?
Can you please be more specific? Do you have any experience with the Ardino environment? Already built something?
Hi sir ..
I want to ask
why my project generates noise waves in Serial Monitor?
and not responding to my muscles, nothing has changed from the noise wave.
I am using Arduino Uno V3 and EMG Shield from Olimex.
Make sure the system is battery-powered, the electrodes are good and the electrode wires not too long