{"id":256,"date":"2014-06-04T21:02:06","date_gmt":"2014-06-04T21:02:06","guid":{"rendered":"http:\/\/www.thinkering.de\/cms\/?p=256"},"modified":"2017-02-23T08:15:09","modified_gmt":"2017-02-23T08:15:09","slug":"arduino-ecg-monitor-2","status":"publish","type":"post","link":"http:\/\/www.thinkering.de\/cms\/?p=256","title":{"rendered":"Arduino ECG Monitor 2"},"content":{"rendered":"<p><iframe width=\"560\" height=\"315\" src=\"\/\/www.youtube.com\/embed\/47Nis8ITqoo?rel=0\" frameborder=\"0\" allowfullscreen><\/iframe><br \/>\n<br style=\u201dclear:both;\u201d \/><br \/>\n<br style=\u201dclear:both;\u201d \/><br \/>\n<a href=\"http:\/\/www.thinkering.de\/cms\/wp-content\/uploads\/2014\/06\/20121220_145149.jpg\"><img loading=\"lazy\" src=\"http:\/\/www.thinkering.de\/cms\/wp-content\/uploads\/2014\/06\/20121220_145149-300x225.jpg\" alt=\"Arduino ECG Monitor 2\" width=\"300\" height=\"225\" class=\"alignnone size-medium wp-image-257\" srcset=\"http:\/\/www.thinkering.de\/cms\/wp-content\/uploads\/2014\/06\/20121220_145149-300x225.jpg 300w, http:\/\/www.thinkering.de\/cms\/wp-content\/uploads\/2014\/06\/20121220_145149-1024x768.jpg 1024w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/a><br \/>\n<br style=\u201dclear:both;\u201d \/><br \/>\n<br style=\u201dclear:both;\u201d \/><br \/>\nThis 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 \ud83d\ude42<br \/>\n<br style=\u201dclear:both;\u201d \/><br \/>\n<br style=\u201dclear:both;\u201d \/><\/p>\n<pre class=\"brush: cpp; collapse: true; light: false; title: ; toolbar: true; notranslate\" title=\"\">\r\n\/\/Simple Arduino ECG monitor with SSD1306 OLED display\r\n\/\/Incorporates a simple QRS detection algorithm and heart rate calculation\r\n\/\/the interrupt-based code parts are based on the Olimex approach\r\n\/\/Requires the libraries included below!\r\n\r\n#include &lt;Wire.h&gt;\r\n#include &lt;Adafruit_GFX.h&gt;\r\n#include &lt;Adafruit_SSD1306.h&gt;\r\n\r\n\/\/connect the OLED display in the following way:\r\n#define OLED_DC 11\r\n#define OLED_CS 12\r\n#define OLED_CLK 10\r\n#define OLED_MOSI 9\r\n#define OLED_RESET 13\r\nAdafruit_SSD1306 display(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);\r\n\r\n#if (SSD1306_LCDHEIGHT != 64)\r\n#error(&quot;Height incorrect, please fix Adafruit_SSD1306.h!&quot;);\r\n#endif\r\n\r\n#include &lt;compat\/deprecated.h&gt;\r\n#include &lt;FlexiTimer2.h&gt;\r\n\/\/http:\/\/www.arduino.cc\/playground\/Main\/FlexiTimer2\r\n#include &lt;TimerOne.h&gt;\r\n\/\/http:\/\/arduino.cc\/playground\/Code\/Timer1\r\n\r\n\/*\r\nErkl\u00c3\u00a4rung von cbi, sbi, outp und inp\r\nBei solchen Makros sollte man etwas mehr Klammern spendieren:\r\n\r\n#define sbi(ADDRESS,BIT) ((ADDRESS) |= (1&lt;&lt;(BIT)))\r\n#define cbi(ADDRESS,BIT) ((ADDRESS) &amp;= ~(1&lt;&lt;(BIT)))\r\n#define outp(VAL,ADRESS) ((ADRESS) = (VAL))\r\n#define inp(VAL) (VAL)\r\nThe 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.\r\n*\/\r\n\r\n\/\/ All definitions\r\n#define NUMCHANNELS 6\r\n#define HEADERLEN 4\r\n#define PACKETLEN (NUMCHANNELS * 2 + HEADERLEN + 1)    \/\/6*2+4+1\r\n#define SAMPFREQ 256                   \/\/ ADC sampling rate 256\r\n#define TIMER2VAL (1024\/(SAMPFREQ))    \/\/ Set 256Hz sampling frequency\r\n#define PWM_OUT 9                      \/\/ Number of pin used for generating CAL_SIG\r\n#define PWMFREQ 10        \/\/10Hz for Calibration signal             \r\n\/\/#define LED1  13\r\n\r\n\/\/ Global constants and variables\r\nchar const channel_order[]= { 0, 1, 2, 3, 4, 5 };\r\nvolatile unsigned char TXBuf[PACKETLEN];  \/\/The transmission packet\r\nvolatile unsigned char TXIndex;           \/\/Next byte to write in the transmission packet.\r\nvolatile unsigned char CurrentCh;         \/\/Current channel being sampled.\r\n\r\n\/\/~~~~~~~~~~\r\n\/\/ Functions\r\n\/\/~~~~~~~~~~\r\n\r\n\/****************************************************\/\r\n\/*  Function name: Toggle_LED1                      *\/\r\n\/*  Parameters                                      *\/\r\n\/*    Input   :  No                                *\/\r\n\/*    Output  :  No                                 *\/\r\n\/*    Action: Switches-over LED1.                   *\/\r\n\/****************************************************\/\r\n\/\/void Toggle_LED1(void){\r\n\/\/\r\n\/\/ if((digitalRead(LED1))==HIGH){\r\n\/\/   digitalWrite(LED1,LOW);\r\n\/\/  }\r\n\/\/  else{\r\n\/\/   digitalWrite(LED1,HIGH);\r\n\/\/  }\r\n\/\/}\r\n\/****************************************************\/\r\n\/*  Function name: setup                            *\/\r\n\/*  Parameters                                      *\/\r\n\/*    Input   :  No                                *\/\r\n\/*    Output  :  No                                 *\/\r\n\/*    Action: Initializes all peripherals           *\/\r\n\/****************************************************\/\r\nvoid setup() {\r\n\r\n noInterrupts();  \/\/ Disable all interrupts before initialization\r\n \r\n \/\/ LED1\r\n\/\/ pinMode(LED1, OUTPUT);  \/\/Setup LED1 direction\r\n\/\/ digitalWrite(LED1,LOW); \/\/Setup LED1 state\r\n \r\n \/\/Write packet header and footer\r\n TXBuf[0] = 0xa5;  \/\/Sync 0\r\n TXBuf[1] = 0x5a;  \/\/Sync 1\r\n TXBuf[2] = 2;     \/\/Protocol version\r\n TXBuf[3] = 0;     \/\/Packet counter\r\n \r\n \/\/ ADC\r\n \/\/ Timings for sampling of one 10-bit AD-value:\r\n \/\/ XTAL = 16000000MHz\r\n \/\/ prescaler &gt; ((XTAL \/ 200kHz) = 80 =&gt;\r\n \/\/ prescaler = 128 (ADPS2 = 1, ADPS1 = 1, ADPS0 = 1)\r\n \/\/ ADCYCLE = XTAL \/ prescaler = 125000Hz or 8 us\/cycle\r\n \/\/ 14 (single conversion) cycles = 112 us\r\n \/\/ 26 (1st conversion) cycles = 208 us\r\n outb(ADMUX, 0);         \/\/Select channel 0\r\n outb(ADCSRA, ((1&lt;&lt;ADPS2) | (1&lt;&lt;ADPS1)| (1&lt;&lt;ADPS0))); \/\/Prescaler = 128, free running mode = off, interrupts off.\r\n sbi(ADCSRA, ADIF);  \/\/Reset any pending ADC interrupts  \r\n sbi(ADCSRA, ADEN);  \/\/Enable the ADC                    \r\n \r\n \/\/ Serial Port\r\n outb(UBRR0, 16);              \/\/Set speed to 57600 bps     \r\n outb(UCSR0B, (1&lt;&lt;TXEN0));     \/\/Enable USART Transmitter.\r\n \r\n \/\/ Timer1\r\n \/\/ It's used for calibration signal generation: CAL_SIG via PWM.\r\n \/\/ CAL_SIG is used like reference signal when setting-up SHIELD-EKG\/EMG's channel gain\r\n \/\/ During normal operation this signal is not required so it can be disabled with uncommenting te row below!\r\n \/*\r\n pinMode(PWM_OUT, OUTPUT);    \/\/Set PWM_OUT direction\r\n digitalWrite(PWM_OUT,LOW);   \/\/Set PWM_OUT state\r\n Timer1.initialize((1000000\/(PWMFREQ))); \/\/ initialize timer1, and set a 1\/10 second period = 10Hz -&gt;freq. of cal signal should be 10-14Hz (schematic)\r\n Timer1.pwm(PWM_OUT, 512);             \/\/ setup pwm on pin 9, 50% duty cycle\r\n \/\/Timer1.disablePwm(PWM_OUT); \/\/ Uncomment if CAL_SIG is not requiered\r\n *\/\r\n\r\n \/\/ Timer2\r\n \/\/ Timer2 is used for setting ADC sampling frequency.\r\n \r\n\/*****************************************************************\r\nMethods of the FlexiTimer2 library:\r\n\r\nFlexiTimer2::set(unsigned long units, double resolution, void (*f)())\r\n    this function sets a time on units time the resolution for the overflow. Each overflow, &quot;f&quot; will be called. &quot;f&quot; has to be declared void with no parameters.\r\n    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.\r\nFlexiTimer2::set(unsigned long ms, void (*f)())\r\n    this function sets a time on ms (1\/1000th of a second) for the overflow. Each overflow, &quot;f&quot; will be called. &quot;f&quot; has to be declared void with no parameters.\r\n    Shorthand for calling the function above with resolution = 0.001.\r\nFlexiTimer2::start()\r\n    enables the interrupt.\r\nFlexiTimer2::stop()\r\n    disables the interrupt.\r\n*******************************************************************\/\r\n FlexiTimer2::set(TIMER2VAL, Timer2_Overflow_ISR); \/\/TIMER2VAL was (1024\/(SAMPFREQ)) in ms =4, SAMPLEFREQ was 256\r\n FlexiTimer2::start();  \/\/enable the Interrupt....\r\n \r\n \/\/ MCU sleep mode = idle.\r\n outb(MCUCR,(inp(MCUCR) | (1&lt;&lt;SE)) &amp; (~(1&lt;&lt;SM0) | ~(1&lt;&lt;SM1) | ~(1&lt;&lt;SM2)));\r\n \r\n interrupts();  \/\/ Enable all interrupts after initialization has been completed\r\n  \/\/ by default, we'll generate the high voltage from the 3.3v line internally! (neat!)\r\n  display.begin(SSD1306_SWITCHCAPVCC);\r\n  \/\/ init done\r\n display.clearDisplay();   \/\/ clears the screen and buffer\r\n  \r\n  display.setTextSize(1);\r\n  display.setTextColor(WHITE);\r\n  \r\n}\r\n\/****************************************************\/\r\n\/*  Function name: Timer2_Overflow_ISR              *\/\r\n\/*  Parameters                                      *\/\r\n\/*    Input   :  No                                *\/\r\n\/*    Output  :  No                                 *\/\r\n\/*    Action: Determines ADC sampling frequency.    *\/\r\n\/****************************************************\/\r\nvoid Timer2_Overflow_ISR()    \/\/alle 4ms wird das ausgef\u00c3\u00bchrt\r\n{\r\n  \/\/ Toggle LED1 with ADC sampling frequency \/2\r\n  \/\/Toggle_LED1();\r\n \r\n  CurrentCh = 0;\r\n  \/\/ Write header and footer:\r\n  \/\/ Increase packet counter (fourth byte in header)\r\n   \/\/Write packet header and footer\r\n \/**********zur Erinnerung: der Header**********\r\n TXBuf[0] = 0xa5;  \/\/Sync 0\r\n TXBuf[1] = 0x5a;  \/\/Sync 1\r\n TXBuf[2] = 2;     \/\/Protocol version\r\n TXBuf[3] = 0;     \/\/Packet counter\r\n ***********************************\/\r\n TXBuf[3]++;\r\n  \/\/the whole packet is \/6*2+4+1=17byte\r\n  \/\/Get state of switches on PD2..5, if any (last byte in packet).\r\n  TXBuf[2 * NUMCHANNELS + HEADERLEN] = (inp(PIND) &gt;&gt; 2) &amp;0x0F;  \/\/2* NUMCHANNELS, weil jeder CHannel 2 byte hat damit 1024 reinpasst\r\n \r\n  cbi(UCSR0B, UDRIE0); \/\/Ensure Data Register Empty Interrupt is disabled.\r\n  sbi(ADCSRA, ADIF);   \/\/Reset any pending ADC interrupts\r\n  sbi(ADCSRA, ADIE);   \/\/Enable ADC interrupts. \r\n  sbi(ADCSRA, ADSC) ;  \/\/ Start conversion!!!\r\n  \/\/Next interrupt will be ISR(ADC_vect)\r\n}\r\n\r\n\/****************************************************\/\r\n\/*  Function name: ISR(ADC_vect)                    *\/\r\n\/*  Parameters                                      *\/\r\n\/*    Input   :  No                                *\/\r\n\/*    Output  :  No                                 *\/\r\n\/*    Action: Reads ADC's current selected channel  *\/\r\n\/*            and stores its value into TXBuf. When *\/\r\n\/*            TXBuf is full, it starts sending.     *\/\r\n\/****************************************************\/\r\nISR(ADC_vect)\r\n{\r\n volatile unsigned char i;    \/\/volatile??\r\n \r\n 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)\r\n TXBuf[i+1] = inp(ADCL);      \/\/ADC data register LOW byte\r\n TXBuf[i] = inp(ADCH);        \/\/ADC data register HIGH byte\r\n CurrentCh++;  \r\n if (CurrentCh &lt; NUMCHANNELS)\r\n {\r\n  outb(ADMUX, (channel_order[CurrentCh])); \/\/Select the next channel.\r\n  sbi(ADCSRA, ADSC) ;                   \/\/Start conversion!!! (set ADSC-bit in ADCSRA-Register)\r\n }\r\n else\r\n {\r\n   \/\/this gets executed first....prior to the stuff above\r\n  outb(ADMUX, channel_order[0]);      \/\/Prepare next conversion, on channel 0.\r\n  cbi(ADCSRA, ADIE);    \/\/Disable ADC interrupts to prevent further calls to ISR(ADC_vect). oben hiess es sbi!!!!!!\r\n  outb(UDR0, TXBuf[0]); \/\/Send first Packet's byte: Sync 0\r\n  sbi(UCSR0B, UDRIE0);  \/\/USART Data Register Empty Interrupt Enable\r\n  TXIndex = 1;          \/\/Next interrupt will be ISR(USART_UDRE_vect)\r\n }\r\n}\r\n\r\n\/****************************************************\/\r\n\/*  Function name: ISR(USART_UDRE_vect)             *\/\r\n\/*  Parameters                                      *\/\r\n\/*    Input   :  No                                *\/\r\n\/*    Output  :  No                                 *\/\r\n\/*    Action: Sends remaining part of the Packet.   *\/\r\n\/****************************************************\/\r\nISR(USART_UDRE_vect){\r\n \r\n outb(UDR0, TXBuf[TXIndex]);  \/\/Send next byte\r\n TXIndex++;\r\n \/******hier also***\r\n ch0hb = TxBuf[4];\r\n ch0lb = TxBuf[5];\r\n *******************\/\r\n\r\n \r\n if (TXIndex == PACKETLEN)    \/\/See if we're done with this packet\r\n {\r\n   cbi(UCSR0B, UDRIE0);    \/\/USART Data Register Empty Interrupt Disable\r\n                              \/\/Next interrupt will be Timer2_Overflow_ISR()\r\n }\r\n}\r\n\r\n\r\n\/\/function for fusion of the ADCL and ADCH byte\r\n\r\nunsigned int weiterverarbeitung(volatile unsigned char high_byte, volatile unsigned char low_byte)\r\n{\r\n unsigned int value = ((high_byte&amp;0x0f)*256)+(low_byte);\r\n return(value);\r\n}\r\n\r\n\/****************************************************\/\r\n\/*  Function name: loop                             *\/\r\n\/*  Parameters                                      *\/\r\n\/*    Input   :  last 2 channel bytes of the packet *\/\r\n\/*    Output  :  to display                         *\/\r\n\/*    Action: Draws ECG, detects QRS, calculates HR *\/\r\n\/****************************************************\/\r\nunsigned long Start, Finished = 0;\r\nint heart_rate[4];\r\nint heart_rate_avg;\r\nfloat RR_interval = 0.0;\r\nunsigned int Delay = 2;\r\nunsigned int QRS_counter = 0;\r\nint thisdot = 0;\r\nint prevdot = 0;\r\n\r\n\r\n\r\nvoid loop() {\r\n \r\n  \/\/&quot;heart rate&quot;\r\n  display.setCursor(1,52);          \r\n  display.print(&quot;heart rate:&quot;);\r\n  display.display(); \/\/ show it\r\n  \r\n  \/\/show heart rate once per screen\r\n  if(heart_rate_avg&lt;220)\r\n      {\r\n          display.setCursor(80,52);          \r\n          display.print(heart_rate_avg);\r\n          display.display(); \/\/ show it\r\n\r\n      }\r\n \r\n  \/\/draw the actual graph: (128 = display width)\r\n  for(int i=0; i&lt;128; i++)\r\n  {\r\n    \r\n    Finished = 0;\r\n    \/\/get the ADC value and scale it to the higth of the display\r\n    unsigned int val = weiterverarbeitung(TXBuf[14],TXBuf[15]);  \/\/using A5 and extracting the last 2 channel bytes out of the packet\r\n    unsigned int y = map (val, 0, 1023, 64, 0);      \/\/oben=0!!\r\n    thisdot = y;\r\n    \r\n    \/\/calculate the graph slope for QRS detection\r\n    \/\/slope can be negative so it has to be an SIGNED int\r\n    int slope = prevdot - thisdot;\r\n    \r\n    \/\/QRS complex detected above a certain threshold\r\n     if (slope &gt;= 8 &amp;&amp; Start == 0)\r\n      {\r\n        \/\/QRS Beep, use Pin 6 to not interfere with Timer 2!!\r\n        tone(6, 2000, 50); \r\n        \/\/start &quot;stop watch&quot;\r\n        Start = millis();   \r\n      }\r\n      else if(slope &gt;= 8 &amp;&amp; Start &gt; 0)\r\n    {\r\n      \/\/QRS Beep\r\n      tone(6, 2000, 50); \r\n      \/\/stop\r\n      Finished = millis();\r\n      \/\/calculate a RR interval\r\n      RR_interval = Finished - Start;\r\n              \r\n      if(RR_interval&gt;=150)  \/\/refractory period, RR-intervals should be longer than this (filter method)\r\n        {\r\n          RR_interval = RR_interval\/1000;  \/\/convert to seconds\r\n          heart_rate[QRS_counter] = 60\/RR_interval; \/\/collect 4 intervals\r\n          QRS_counter ++;   \r\n          \r\n          \/\/averaging calculation\r\n          if(QRS_counter &gt;= 3){\r\n            for(int j = 0; j&lt;4; j++){\r\n              heart_rate_avg += heart_rate[j];\r\n            }\r\n            heart_rate_avg \/= 4;\r\n            QRS_counter = 0;\r\n          }\r\n        }\r\n      \/\/reset Start value for time measurement\r\n      Start = 0;\r\n      \r\n    }\r\n      \r\n    \/\/Draw graph  \r\n    display.drawPixel(i, y, WHITE);\r\n    display.display();\r\n    delay(Delay);\r\n    \r\n    prevdot = thisdot;\r\n    thisdot = 0;\r\n    slope = 0; \r\n  } \r\ndisplay.clearDisplay();\r\n \r\n}\r\n\r\n<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>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 \ud83d\ude42<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"ngg_post_thumbnail":0},"categories":[1,10],"tags":[],"_links":{"self":[{"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=\/wp\/v2\/posts\/256"}],"collection":[{"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=256"}],"version-history":[{"count":6,"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=\/wp\/v2\/posts\/256\/revisions"}],"predecessor-version":[{"id":927,"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=\/wp\/v2\/posts\/256\/revisions\/927"}],"wp:attachment":[{"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=256"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=256"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.thinkering.de\/cms\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=256"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}