Project Log: Building a heart rate logger

Discussion in 'Electronics & Electrics' started by LINUX, Oct 16, 2010.

  1. LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    During my wanderings of the Internet I came across this project: http://tinkerish.com/blog/?p=130

    It's a heart rate logger based around a Polar heart rate monitor strap reciever module from Spark Fun: http://www.sparkfun.com/commerce/product_info.php?products_id=8660

    Reading through the datasheeet I came across the table on page 5 which lists "Reception Frequency: 5.5kHz". This produced a bit WTF in my mind: who uses a frequency *that* low in a device this small?

    Anyway, it turns out that this was simple to check as 5.5kHz can easily be recieved by a computer sound card. I strapped on the heart rate monitor I've got (PulseTronic brand) and connected the closest antenna at hand (a ~50cm long aligator lead) to the end of a 3.5mm plug lead and took a recording.

    Sure enough there's a very obvious pulse train near 5kHz:


    Click to view full size!


    I plan on building a radio reciever for these pulses, a wire loop + a bunch of op-amp filters should do the trick well enough. This will then be fed into a microcontroller which will log the data to some kind of memory: eeprom or SD, most likely.
     
  2. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    Hmm, interesting. He still uses the reciever module though and I'd rather make my own.

    I've got a bunch of op-amp filters working well now and can get a good pulse train waveform in Audacity. The pickup coil needs a lot of work though. The "antenna" in the commercial watch reciever is an RF choke (inductor in a resistor-like case) in parallel with a capacitor. I guess it's just a resonant magnetic loop antenna type arrangement.

    So, I've done a bunch of calculations to make a similar ciruit and will grab the required parts when I'm next at Jaycar.
     
  3. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    Well I now have a reciever working *reasonably* well. It's got a range of ~60cm but it's highly dependent on angle. Ie: you need to be facing the reciever coil end on (it's a magnetic loop "antenna", physically it's just a 1mH RF choke).

    Anyway, the reciever output is being fed into the comparator on an AVR which is using the timer capture mode to measure the beat period. It's then doing some rough integer calculations to convert the a timer value of ~7000 to a heart rate ~60bpm.

    Here's the plot:


    Click to view full size!


    Currently it's just dumping to the UART. The next step is to log to either SD or EEPROM memory and have it dump the data on command.
     
  4. hlokk

    hlokk Member

    Joined:
    Jul 18, 2003
    Messages:
    4,610
    Location:
    WA
    Whats the x-scale on that plot? Is each peak a beat, or is the y-scale the BPM? Seems a bit of an odd pattern.
     
  5. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    Sorry, I was rather lazy :p.

    x-scale is beat count, y is bpm. The bpm is calculated from the time interval from every beat. The oscillations are rather interesting though, it's indicative of an under-damped biological feedback mechanism. I don't know anything about cardiology so I can't comment on what causes it, or even if it's normal*.

    Here's a zoomed graph (still can't work out how to add axes, my kingdom for a command line "ylabel("bpm");" to hell with GUIs).


    Click to view full size!

    Again, horizontal axis is beat number, vertical is bpm.

    Each dot is a bpm value calculated from the amount of time between that beat and the previous one. Basically I've just got a microcontroller (atmega8) with a timer configured as an event capture timer. Each time a pulse is detected it captures the timer value, resets the timer, then calculates a bpm by scaling the captured value appropriately given the timer clock and prescaler values.

    *FWIW I'm a commuter cyclist, doing >150km/week, ~5000km so far this year.
     
  6. hlokk

    hlokk Member

    Joined:
    Jul 18, 2003
    Messages:
    4,610
    Location:
    WA
    I dunno, just that much periodic variability doesnt look right to me, but perhaps its normal? The frequency seems quite regular to me. Maybe when you see heart rates vs time they use an averaging filter though.

    So if I understand the graphs correctly, the first would be over a span of about 5-6 minutes (roughly) and the second about a minute and a bit?

    The second had 8-9ish peaks, so seems on a roughly 10 beat cycle?

    Considering it looks like you have the data in excel or similar, could you plot a beats/minute vs time plot? For each data point, you could be able to convert it to time as the scaled value of the BPM added to the previous time code, then plot a BPM to time graph using x-y graphing? If that makes sense? Would be interesting to compare both beat and time axis to see how graph changes.

    Maybe its an arrhythmia :p

    Looking at a normal EKG here, the widths look pretty similar over 10 or so cycles.

    Could it be that in the process of getting the BPM that its very sensitive and hence outputs a quite noisy result? Though the zoomed one shows reasonably smooth data points. Could it be an innacuracy in the timer reset? Maybe resets take varying time?

    Intriguing, but what is the answer?
     
  7. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    Yeah, it's certainly very interesting.

    Timer error is highly unlikely. I'm using the internal calibrated RC oscillator which is quoted as accurate to +/-10% but in practice it's very good so long as you keep Vcc at 5V it's perfectly adequate for UART operation, for example.

    Reset error is also very small. The timer is setup with a /1024 prescaler and gets a value of ~7000 between beats (8MHz clock). The reset takes, at most, 10 instruction cycles (jump to interrupt vector, context saving, write to timer register).

    Also, to really nail measurement error's coffin, it's very obvious when just observing it manually.

    I'll try to get more measurements from different people to compare this data.

    That normal ECG plot shows much more reguar heartbeats than my measurements, so maybe it is an arrhythmia? I've been diagnosed with a murmor but never followed up with the EKG because it wasn't serious.

    Anyway, time for Starcraft!
     
  8. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    A crystal will be required for accurate measurements, yes. Right now though the RC is good enough. Basically what I was trying to get at before was that the RC oscillator is several orders of magnitude more stable than the heart rate variation.

    And here's the code so far, it's pretty simple. Hardware wise the detected* pulse signal goes into PD6 (AIN0, comparator +) and a threshold voltage is connected to PD7 (AIN1, 10-turn pot voltage divider).

    *By "detected" I mean that it's gone through a diode then an RC low pass filter.

    (This code is a seriously rough hack. It's was written for proof-of-concept feedback only, so have fun with it).

    I'll work on drawing up schematics for the receiver. It's basically 5 op-amps: x100 "RF" gain stage -> non-inverting band-pass filter -> x10 inverting gain -> diode + RC filter.

    The band pass filter is 1k output->-ve input. Then capacitor and gyrator to ground. The gyrator is a 2 op-amp circuit which simulates an inductor with theoretically zero series resistance. In practice it's about 20 ohms (capacitor ESR + parasitic + other non-idealisms). Basically it allows for an insanely narrow filter. Here's the noise output of it:


    Click to view full size!


    The 50Hz peak is mainly due to using my sound card's front mic connector, it's got a 40cm unshilded cable running to the card.

    Code:
    #include <avr/io.h>
    #include <stdint.h>
    #include <avr/interrupt.h>
    
    void uart_string(unsigned char *s);
    void uart_char(unsigned char a);
    void uart_number(unsigned int num);
    
    const unsigned char newline[] = {13, 0};
    
    ISR(SIG_INPUT_CAPTURE1)
    {
    	//The pulse filtering is still messy so the comparator
    	//triggers ~20 times. The 500 count threshold stops this
    	//noise from being counted
    	if(ICR1>500)
    	{
    		unsigned int bpm;
    		TCNT1=0x00;
    		bpm = 468750/(long)ICR1;
    		uart_number(bpm);
    		uart_char(10);
    	}
    }
    
    int main(void)
    {
    	//Setup UART
    	// Set frame format to 8 data bits, no parity, 1 stop bit
    	UCSRC = (0<<USBS)|(1<<UCSZ1)|(1<<UCSZ0);
    	// Enable transmitter
    	UCSRB = (1<<TXEN);
    	UCSRA = (1<<U2X);
    	UBRRH=0x00;	//115.2kbps @ 8MHz
    	UBRRL=8;
    
    	//Enable capture interrupt
    	TIMSK = (1<<TICIE1);
    	//Configure timer1 with 1024 prescaler
    	TCCR1B = (1<<ICES1)|(1<<CS12)|(1<<CS10);
    
    	//Turn on comparator
    	ACSR=(1<<ACIC);
    	//PORTD all input.
    	DDRD = 0x03;
    	PORTD = 0x00; //write zero to disable internal pull-up resistors.
    	//PORTC all output, for comparator feedback
    	DDRC = 0xFF;
    
    	sei();
    	
    	while(1)
    	{
    		//Quick way of putting the comparator output on PORTC bit 5 (pin 28).
    		//Just for debugging
    		PORTC = ACSR;
    	}
    
    }
    
    //These are just a set of UART routines, basically just for a debug console
    
    void uart_string(unsigned char *s)
    {
    	unsigned char x=0;
    
    	while(s[x])
    	{
    		while ( (UCSRA&(1<<UDRE)) == 0 );
    		UDR=s[x];
    		x++;
    	}
    }
    
    void uart_char(unsigned char a)
    {
    	while ( (UCSRA&(1<<UDRE)) == 0 );
    	UDR=a;
    }
    
    //Converts an int to a string of ASCII
    void uart_number(unsigned int num)
    {
    	unsigned char nonzero = 0;
    	unsigned char a;
    	unsigned long divisor=10000;
    
    	while(divisor)
    	{
    		a = num/divisor;
    		if(a)
    			nonzero=1;
    		if(nonzero)
    			usart_char(a+48);
    		num=num%divisor;
    		divisor/=10;
    	}
    }
    
     
    Last edited: Oct 20, 2010
  9. fergo

    fergo Member

    Joined:
    Apr 29, 2003
    Messages:
    362
    Location:
    Brisbane
    Impossible to diagnose without an ECG but almost certainly this is respiratory sinus arrhythmia - common to most young, healthy people. Normal variation in heart rate with respiration.

    See here for an example of what this looks like and explanation of the physiology.
     
  10. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    Your input has been most valuable :). Wikipedia had this to add:

    I wonder how well it correlates with fitness? For instance can you measure someone's heart rate at rest and correlate a value such as range/variance/standard deviation of heart rate with other fitness indicators such as VO2 max?

    Anyway, that's kind of irrelevant to the project, but it's all been very interesting :).
     
  11. hlokk

    hlokk Member

    Joined:
    Jul 18, 2003
    Messages:
    4,610
    Location:
    WA
    Hmmm, sounds like that would explain it then. Somewhat counterintuitive that fitness could result in less regular heatbeats, but I'm guessing its not really a problem at all.

    Would that mean that the frequency of peaks corresponds to your frequency of breathing? So the graph shows both your heat rate and your breathing rate :p.


    LINUX: if you can, post up a graph of when you're not breathing (holding your breath for half a minute while relaxed should do it?). Perhaps try it on someone else too?


    Surely its part relevant, seeing as you are building a interface/device for measuring stuff, interesting measured results should be part of it :D
    Its part of the fun of building an intrument.

    Its like Gallileo going "ok, done the telescope, it works, now what should i build next" :lol:
     
    Last edited: Oct 21, 2010
  12. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    Thanks for the measurement ideas. I can try breathing fast/slow etc.

    It kind of makes sense that breathing can influence your heart rate. If your cardiobascular system is infinitely efficient it could only beat once corresponding to the point in time when your lungs hold a maximum of oxygen.

    I'm working on increasing the sensitivity of it right now. Basically playing around with multiple pickup coils and trying to find a combination which works better than a single one.
     
  13. @rt

    @rt Member

    Joined:
    Nov 30, 2005
    Messages:
    2,327
    I did one for the Sony PSP about two years ago.
    It has a mic input, and I was able to pick up a spike from a wire coil mounted close to the chest transmitter, and connected directly to the mic input.
    From there it was all just fun with software :)

    [​IMG]

    Ended up having it speak to the user in an audible voice to:
    http://www.youtube.com/watch?v=dhxFqlQqIH8

    Just a tip, it's good to display the mean average of about the last five rates
    to smooth out a bit.

    This represents a possible 80 minutes of workout session time (10 mins per vertical bar).
    Could make it scroll sideways yet for a longer view, but I don't think I need any longer than that.
    This was a brisk walk that became a short jog up a slight incline, and then slowed down to a walk.
    (all in just under 15 minutes).
     
  14. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    Do you mind sharing the rough outline of how the software worked? Was it FFT based? Or did you just look for sound intensity over a threshold?
     
  15. mtma

    mtma Member

    Joined:
    Aug 12, 2009
    Messages:
    4,737
    Certainly the size of your heart needed to achieve this would be interesting to say the least.

    Air gills and rotary pumps would probably work better :D
     
  16. OP
    OP
    LINUX

    LINUX Member

    Joined:
    Sep 18, 2001
    Messages:
    3,062
    Location:
    Dudley, Newcastle
    I'm trained as a physicist. Point masses, frictionless surfaces and infinite hearts are all just part of the daily grind :p.

    Edit: Today I've increased the sensitivity a bit by throwing in 3 reciever coils (1mH RF choke's) connected in parallel to a x100 addition inverting op-amp circuit. The microcontroller code now has a 20 point moving average filter as well. It summs up the bpm value for 20 beats then divides by 10. This gives a (possibly pointless) 1 decimal place fixed point value. For now it just prints 603 for 60.3bpm and this value is divided by 10 in openoffice. It's fairly easy to write code which prints the decimal place automatically: either to the UART or an LCD.

    The line on the left is just the moving average filter "filling up", it's initialised with zero's.

    Click to view full size!


    The timer capture ISR now looks like this:

    Code:
    ISR(SIG_INPUT_CAPTURE1)
    {
    	//The pulse filtering is still messy so the comparator
    	//triggers ~20 times. The 500 count threshold stops 
    	
    	if(ICR1>2000)
    	{
    		unsigned int bpm;
    		unsigned char i;
    		static unsigned char bpmhist[20] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
    		unsigned int total=0;
    
    		//Shift history along
    		for(i=19;i>0;i--)
    			bpmhist[i] = bpmhist[i-1];
    
    
    		TCNT1=0x0000;
    		bpm = 468750/(long)ICR1;
    		bpmhist[0] = bpm;
    
    		//Calculate average
    		for(i=0;i<20;i++)
    			total+=bpmhist[i];
    		total = total>>1; //Divide by 2, total is now *10, ie: 1 decimal place fixed point
    		
    		usart_number(bpm);
    		usart_char(32);
    		usart_number(total);
    		usart_char(10);
    
    	}
    }
    
     
    Last edited: Oct 22, 2010
  17. @rt

    @rt Member

    Joined:
    Nov 30, 2005
    Messages:
    2,327
    I had a routine that looked at the input sample buffer and drew a wave to the screen.
    I wrote code that looked for spikes in the graphic wave.
    Next, I removed the graphics, and only the maths was left.
     
  18. flightcrank

    flightcrank Member

    Joined:
    Aug 6, 2004
    Messages:
    744

Share This Page