ECG/EKG: Need Help Understanding Data Generated

Hi All

I have just bought an OLimex and its ECG/EKG Shield. I'm trying to get some heart monitor readings captured using Serial.print. Ultimately, I wish to send them over BluetoothClassic, or Wifi, to a second Arduino where it will be written to an SD card.

[My platform is Linux-Fedora.]

I need help understanding the data structures in this program. Can you help?

My Questions are:

  1. Why is the range of the channel 1 only moving between about 235 and 255?
  2. What does the protocol type mean? Why does it change from 1 to 2?
  3. By printing the individual chars am I destroying the structure of my data? Does the Serial.print(...) print a true value in ascii?

Here's a sample of the serial monitor output:

338,1,252
339,1,252
340,1,250
341,1,250
342,1,252
343,1,251
344,2,0
345,1,250
346,2,2
347,1,252
348,2,0
349,1,253
350,1,254
351,1,248
352,1,253
353,1,250
354,1,254
355,1,249
356,1,253
357,1,251
358,2,0
359,1,252
360,2,0
361,1,251
362,1,255
363,1,245
364,1,252
365,1,244
366,1,245
367,1,249
368,1,249
369,1,252
370,1,248
371,1,254
372,1,249
373,1,254
374,1,250
375,2,0
376,1,252
377,1,255
378,1,254

Notes: Col1 to Col3 are are: continuous counter, protocol-type, channel1-value. Potocol-type 2 (see column 2) randomly appears and has low values. Above values from modification with tag: "// 3) Send Packet, MODIFIED: Serial.print selected fields."

Here is the source sketch:

/**********************************************************/
/* Demo program for: SHIELD-EKG/EMG + Olimexino328                */
/**********************************************************/
/**********************************************************
This [original] programme connects Olimexino328 to ElectricGuru(TM)
***********************************************************
For proper communication packet format given below have to be supported:
// 17-byte packets are transmitted from Olimexino328 at 256Hz,
// using 1 start bit, 8 data bits, 1 stop bit, no parity, 57600 bits per second.
// Minimial transmission speed is 256Hz * sizeof(Olimexino328_packet) * 10 = 43520 bps.

struct Olimexino328_packet
{
  uint8_t	sync0;		// = 0xa5
  uint8_t	sync1;		// = 0x5a
  uint8_t	version;	// = 2 (packet version)
  uint8_t	count;		// packet counter. Increases by 1 each packet.
  uint16_t	data[6];	// 10-bit sample (= 0 - 1023) in big endian (Motorola) format.
  uint8_t	switches;	// State of PD5 to PD2, in bits 3 to 0.
};
*/
/**********************************************************/
#include <compat/deprecated.h>
#include <FlexiTimer2.h>
//http://www.arduino.cc/playground/Main/FlexiTimer2

// All definitions
// #define NUMCHANNELS 6
#define NUMCHANNELS 1                     // EGB
#define HEADERLEN 4
#define PACKETLEN (NUMCHANNELS * 2 + HEADERLEN + 1)
#define SAMPFREQ 256                      // Orig: ADC sampling rate 256
#define TIMER2VAL (1024/(SAMPFREQ))       // Set 256Hz sampling frequency                    
#define LED1  13
#define CAL_SIG 9

// Global constants and variables
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.
volatile unsigned char counter = 0;	      //Additional divider used to generate CAL_SIG
volatile unsigned int ADC_Value = 0;	    //ADC current value

unsigned int continuous_counter = 0;        //Additional divider used to generate CAL_SIG

//~~~~~~~~~~
// Functions
//~~~~~~~~~~

/****************************************************/
/*    Action: Switches-over LED1.                   */
/****************************************************/
void Toggle_LED1(void){

 if((digitalRead(LED1))==HIGH){ digitalWrite(LED1,LOW); }
 else{ digitalWrite(LED1,HIGH); }
 
}

/****************************************************/
/*    Action: Switches-over GAL_SIG.                */
/****************************************************/
void toggle_GAL_SIG(void){
  
 if(digitalRead(CAL_SIG) == HIGH){ digitalWrite(CAL_SIG, LOW); }
 else{ digitalWrite(CAL_SIG, HIGH); }
 
}


/****************************************************/
/*    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
 pinMode(CAL_SIG, OUTPUT);
 
 //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
 TXBuf[4] = 0x02;    //CH1 High Byte
 TXBuf[5] = 0x00;    //CH1 Low Byte
 TXBuf[6] = 0x02;    //CH2 High Byte
 TXBuf[7] = 0x00;    //CH2 Low Byte
 TXBuf[8] = 0x02;    //CH3 High Byte
 TXBuf[9] = 0x00;    //CH3 Low Byte
 TXBuf[10] = 0x02;   //CH4 High Byte
 TXBuf[11] = 0x00;   //CH4 Low Byte
 TXBuf[12] = 0x02;   //CH5 High Byte
 TXBuf[13] = 0x00;   //CH5 Low Byte
 TXBuf[14] = 0x02;   //CH6 High Byte
 TXBuf[15] = 0x00;   //CH6 Low Byte 
 TXBuf[2 * NUMCHANNELS + HEADERLEN] =  0x01;	// Switches state

 // Timer2
 // Timer2 is used to setup the analag channels sampling frequency and packet update.
 // Whenever interrupt occures, the current read packet is sent to the PC
 // In addition the CAL_SIG is generated as well, so Timer1 is not required in this case!
 FlexiTimer2::set(TIMER2VAL, Timer2_Overflow_ISR);
 FlexiTimer2::start();
 
 // Serial Port
 Serial.begin(57600); // Original
 
 // 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
}

/****************************************************/
/*  Function name: Timer2_Overflow_ISR              */
/*    Action: Determines ADC sampling frequency.    */
/****************************************************/
void Timer2_Overflow_ISR() {
  // Toggle LED1 with ADC sampling frequency /2
  Toggle_LED1();
  
  //Read the 6 ADC inputs and store current values in Packet
  // for(CurrentCh=0;CurrentCh<6;CurrentCh++){ // Original
  for( CurrentCh=0; CurrentCh<NUMCHANNELS ;CurrentCh++ ){ // EGB
    ADC_Value = analogRead(CurrentCh);
    TXBuf[((2*CurrentCh) + HEADERLEN)] = ((unsigned char)((ADC_Value & 0xFF00) >> 8));	// Write High Byte
    TXBuf[((2*CurrentCh) + HEADERLEN + 1)] = ((unsigned char)(ADC_Value & 0x00FF));	// Write Low Byte
  }
	 
  // 1) Send Packet: ORIGINAL
  //for(TXIndex=0;TXIndex<17;TXIndex++){
  //  Serial.write(TXBuf[TXIndex]); // Orig
  //  if ( TXIndex < (17 - 1)) Serial.print(","); // EGB: added to separate the char prints
  //}

  // 2) Send Packet, MODIFIED: Serial.print with char separator
  //for(TXIndex=0;TXIndex<17;TXIndex++){
  //  Serial.print(TXBuf[TXIndex]); // EGB: to convert Hex to Ascii
  //  if ( TXIndex < (17 - 1)) Serial.print(","); // EGB: added to separate the char prints
  //}
  //Serial.println();       // EGB: added for \n

  // 3) Send Packet, MODIFIED: Serial.print selected fields
  Serial.print(continuous_counter); // EGB:
  Serial.print(",");                // EGB:
  Serial.print(TXBuf[4]);           // EGB:
  Serial.print(",");                // EGB:
  Serial.print(TXBuf[5]);           // EGB:
  Serial.println();                 // EGB: added for \n
  continuous_counter++;             // increment 
  
  // Increment the packet counter
  TXBuf[3]++;			
  
  // Generate the CAL_SIGnal
  counter++;		// increment the devider counter
  if(counter == 12){	// 250/12/2 = 10.4Hz ->Toggle frequency
    counter = 0;
    toggle_GAL_SIG();	// Generate CAL signal with frequ ~10Hz
  }
}


/****************************************************/
/*    Action: Puts MCU into sleep mode.             */
/****************************************************/
void loop() {

 __asm__ __volatile__ ("sleep");

}

Notes: Modifications are marked with EGB, and for the serial printouts:
// 1) Send Packet: ORIGINAL
// 2) Send Packet, MODIFIED: Serial.print with char separator
// 3) Send Packet, MODIFIED: Serial.print selected fields

Re: the ElectricGuru stuff I cannot use it. I need data written to file.]

Well, I look forward to your help! Any instructions/ideas/recommendations will be welcome. I will reply to them all!

Thanks in advance
EGB

On quick inspection it appears that the columns that you have shown are

  1. A sequential number (continuous_counter)
  2. Channel 1 - High byte
  3. Channel 1 - Low byte

So you should multiply column 2 by 256 and add it to column 3 to get the value for channel 1.

Serial.print() should not really be called from an interrupt service routine, in this case Timer2_Overflow_ISR().

Is the code that you have posted original or did you modify it ?

Hi @6v6gt. Thanks for your quick reply.

6v6gt:
.
A] On quick inspection it appears that the columns that you have shown are

  1. A sequential number (continuous_counter)
  2. Channel 1 - High byte
  3. Channel 1 - Low byte
    So you should multiply column 2 by 256 and add it to column 3 to get the value for channel 1.

B] Serial.print() should not really be called from an interrupt service routine, in this case Timer2_Overflow_ISR().

C] Is the code that you have posted original or did you modify it ?

Re: A] Yes, I suspected that. What I did was actually take the ADC_Value (a volatile unsigned integer), which reads pin 9. Doing that avoids reconverting the value from High and Low Hex values into integer.

Following your idea, here are better looking results. Attached is an image:

4096.00,528.00
4097.00,484.00
4098.00,487.00
4099.00,529.00
4100.00,470.00
4101.00,531.00
4102.00,482.00
4103.00,516.00
4104.00,517.00
4105.00,472.00
4106.00,535.00
4107.00,476.00
4108.00,526.00
4109.00,485.00
4110.00,483.00
4111.00,528.00
4112.00,469.00
4113.00,530.00
4114.00,483.00
4115.00,510.00
4116.00,521.00
4117.00,469.00
4118.00,532.00
4119.00,474.00

Col1 is a record sequence number; Col2 is the analogue to digital value from pin 9.

Re: B] Yea. I get that idea, too, but I don't know what to do about it!

  • How do I move the volatile values to, say the void loop()?
  • Do I simply move my Serial.print statements to the void loop()?

Re: C] The code is very slightly modified - only to direct output to the serial monitor. The original is from Olimex, written by Penko Todorov Bozhkov. The structure of the output is designed for input to the ElectricGuru displays.

Outstanding questions are:

  • Why is the range of the ECG values zig-zag between a range of value, between about 470 and 535? See image attached.
  • Why isn't there a heart pulse profile?

Any ideas??

Super progress, thanks
EGB

demo_EKG_1minai.png

You are reading pin 9 ? What Arduino are you using ? Looking at the program it appears that channel 1 is connected analog pin 0 (A0) and pin 9 is a digital pin switching at 10.4 Hz.

The program is very complex, but what it is doing is very simple as it is configured for one channel.
It is reading analog pin A0 every 4 mS (250Hz), writing its value and writing the value of a sequential counter to serial output. It is also flashing a LED and toggling pin 9 every 12 cycles.

You could probably do all that with a few lines of code and do it all in the loop, based on the famous "blink without delay" or similar sketch.

I don't know why you are not getting the results you expect, but check first you are reading the correct pin.

6v6gt:
...
The program is very complex, but what it is doing is very simple as it is configured for one channel.
...

Hi there @6v6gt.

Yes, it is complex - but doing only a simple task! As I get closer to code, and understand more I know what you mean.

Here is a cut down version appearing to achieve the same thing:

/**********************************************************/
/*  Originally from:  Penko Todorov Bozhkov               */
/**********************************************************/
// <---- Headers ------------->
#include <compat/deprecated.h>
#include <FlexiTimer2.h> // http://www.arduino.cc/playground/Main/FlexiTimer2

// <---- Definitions ------------->
int NumChanells = 4;                                    // EGB; Orginal NumChanells = 6
int SamplingFreq = 256;                                 // Orig: ADC sampling rate 256
int Timer2_Interrupt_Interval = (1024/(SamplingFreq));  // Set 256Hz sampling frequency                    
int HeartSignalPin = 9;                                 // Pin 9, the analogRead pin

// <---- Global constants and variables ------------->
volatile unsigned int CurrentChannel;                   //Channel being sampled.
unsigned long int continuous_counter = 0;               //EGB: added as print record counter
unsigned int ADC_reading[6];                            //EGB: for serial-printing
unsigned long int ADC_total_3_channels = 0;             //EGB: for average of 3 channels
int i;

// <---- Function: Switches-over HeartSignalPin ------------->
void toggle_HeartSignalPin(void){
  if(digitalRead(HeartSignalPin) == HIGH){ 
    digitalWrite(HeartSignalPin, LOW); 
  }else{ 
    digitalWrite(HeartSignalPin, HIGH); 
  }
}

// <---- Function: Timer2_ISR, INTERRUPT; Determines ADC sampling frequency ------------->
void Timer2_ISR() {
  for( CurrentChannel=0; CurrentChannel<NumChanells; CurrentChannel++ ){ // EGB
    ADC_reading[CurrentChannel] = analogRead(CurrentChannel); // EGB Modified #4
  }
  toggle_HeartSignalPin(); //EGB: toggle_HeartSignalPin at same freq as the sampling rate
}

// <---- Action: Initializes all peripherals ------------->
void setup() {
  noInterrupts();  // All interrupts OFF before initialization
  // <----------------->
  pinMode(HeartSignalPin, OUTPUT); // Pin 9
  // When interrupt occures, integer array is populated and HeartSignalPin is generated
  FlexiTimer2::set(Timer2_Interrupt_Interval, Timer2_ISR);
  FlexiTimer2::start();
  Serial.begin(57600); // Original
  // <----------------->
  interrupts();  // All interrupts set ON after initialization has been completed
}

// <---- Action: Formats and sends output ------------->
void output_write() {
    Serial.print(continuous_counter);                      // EGB:
    Serial.print(".00,");                                  // EGB:
    for( i=0; i<NumChanells; i++ ){                        // EGB
      if ( i < 3 ) ADC_total_3_channels = ADC_total_3_channels + ADC_reading[i]; 
      Serial.print(ADC_reading[i]);                        // EGB:
      // if ( i < (NumChanells - 1)) Serial.print(".00,"); // EGB:
      Serial.print(".00,"); // EGB:
    }
    Serial.print(ADC_total_3_channels/3);                  // EGB:
    Serial.println(".00");                                   // EGB: added for \n
    continuous_counter++;                                  // increment 
    ADC_total_3_channels = 0;
}

// <---- Loop(): System sleeps until next interrupt ------------->
void loop() {
  output_write();
  __asm__ __volatile__ ("sleep");
}

In the function "output_write()", where I do my serial.prints, I get a bit heavy: trying to average the 1st three columns.

A sample image is attached, using the average readings. The ideal is also attached. It is clear which is the ideal output. They are both with readings from my own heart.

Why is the profile so different? So bad? What could be improved?

Look forward to your reply,
Thanks
EGB

PS: I will look again at the Digital Pin 9 versus Analog Pin 0.

demo_EKG_10aq.png

0000000000_gb_aa.png

I'd have programmed the original sketch something like this to eliminate the timer and the Serial.print calls (indirectly) from an ISR. It doesn't take account of your latest modifications but should give an idea:

// All definitions

#define SAMPFREQ 256                      // Orig: ADC sampling rate 256
#define TIMER2VAL (1024/(SAMPFREQ))       // Set 256Hz sampling frequency
#define LED1  13
#define CAL_SIG 9

// Global constants and variables
unsigned char counter = 0;	      //Additional divider used to generate CAL_SIG
unsigned int continuous_counter = 0;        //Additional divider used to generate CAL_SIG  ?
uint32_t lastLoopAtMs  = 0 ;

//~~~~~~~~~~
// Functions
//~~~~~~~~~~

/****************************************************/
/*    Action: Switches-over LED1.                   */
/****************************************************/
void Toggle_LED1(void){

	if((digitalRead(LED1))==HIGH){ digitalWrite(LED1,LOW); }
	else{ digitalWrite(LED1,HIGH); }

}

/****************************************************/
/*    Action: Switches-over GAL_SIG.                */
/****************************************************/
void toggle_GAL_SIG(void){

	if(digitalRead(CAL_SIG) == HIGH){ digitalWrite(CAL_SIG, LOW); }
	else{ digitalWrite(CAL_SIG, HIGH); }

}


/****************************************************/
/*    Action: Initializes all peripherals           */
/****************************************************/
void setup() {
	// LED1
	pinMode(LED1, OUTPUT);  //Setup LED1 direction
	digitalWrite(LED1,LOW); //Setup LED1 state
	pinMode(CAL_SIG, OUTPUT);

	// Serial Port
	Serial.begin(57600); // Original
}



/****************************************************/
/*                                                   */
/****************************************************/
void loop() {
	if ( millis() - lastLoopAtMs > TIMER2VAL ) {

		Serial.print(continuous_counter); // EGB:
		Serial.print(",");                // EGB:
		Serial.print( analogRead( A0) );  // EGB:  (assume A0)
                Serial.print(",");                // EGB:
		Serial.print( analogRead( A1) );  // EGB:  (assume A1)   // comment out if not required
                Serial.print(",");                // EGB:
		Serial.print( analogRead( A2) );  // EGB:  (assume A2)   // comment out if not required
		Serial.println();                 // EGB: added for \n


		// Toggle LED1 with ADC sampling frequency /2
		Toggle_LED1();

		// Generate the CAL_SIGnal
		counter++;		// increment the devider counter
		if(counter == 12){	// 250/12/2 = 10.4Hz ->Toggle frequency
			counter = 0;
			toggle_GAL_SIG();	// Generate CAL signal with frequ ~10Hz
		}

		lastLoopAtMs = millis() ;
	}

}

Your current code appears to print at loop speed, i.e. much higher than 250 Hz.
Can you provide a link to the major components you are using ?
Your pictures are very difficult to interpret because they are more or less black on black.