Using:
CW 5.9, HCS08QE32, DEMOQE
Polar RMCM-01 heart-rate receiver - outputs 1ms, 3V pulse each beat detected
I am trying to implement a heart-rate logger as part of a bigger project, but am having difficulty with the code. I have got some good advice from these forums, but would be greatful if you could look at my code and help me out.
/*Program to calculate HR from Polar RMCM-01 reciever - outputs 1ms digital pulse when HR detected.Set up TPM input capture to measure period between pulses, calling interrupt. Blink LED each pulse.Use period to calculate HR in bpm, take in 4 readings and calculate average bpm.Store the average HRs with time stamp.*/#define OFFTIME 3000#define NUMREADINGS 4int readings[NUMREADINGS];int index = 0;int total = 0;int average = 0;int period;// Initialise input pin and status LED, clockvoid init(void){ PTCD_PTCD0 = 0; // PTC0 as input for HR PTDD_PTDD0 = 1; // PTD0 as output for LED // Set TPM clock to 1ms??? TPM3C0SC = 0x44; // Set capture rising edge only, enable interrupt // Loop through readings for (int i = 0; i++) { readings[i] = 0; // set readings to 0 }}// Calculate BPMvoid bpm(void) { int frequence; frequence = (60000 / period); //calculate bpm if ((frequence >= 40) && (frequence <= 220)) // limit for bpm { total -= readings[index]; // subtract the last reading readings[index] = frequence; total += readings[index]; // add the reading to the total index = (index + 1); // advance to the next index if (index >= NUMREADINGS) // if at the end of the array { index = 0; // go back to beginning of array average = total / NUMREADINGS; // calculate average bpm } } // Set boundries for period if (period > OFFTIME) { average = 0; index = 0; }}// Calculate period with ISRvoid interupt VectorNumber_Vtpm3ch0 period_isr(void){ static unsigned int previous_beat; { previous_beat = TPM3C0V; // store current capture period = TPM3C0V - previous_beat; // Calculate period in ms } PTDD_PTDD0 = ~PTDD_PTDD0 // Blink LED on pulses}//Store heart rate (average) with timestamp??
Other than not knowing how to set my TPM clock frequency, I don't think I have set up my hardware interrupt properly, and I'm sure there are other mistakes...any help appreciated!
Regards,
Iain
Hello Iain,
Firstly, to achieve a TPM clock rate in the region of 1000 Hz (1ms period increment), you will need to use a low frequency external crystal or oscillator so that the fixed system clock (XCLK) can be utilised as the TPM clock source. Using the bus clock as the source would would result in a much higher TPM clock rate, and wouldrequire substantial additional complication, to allow for multiple TPM overflows during each period measurement - and not the simple input capture process.
Using a standard 32.768 kHz crystal, the actual frequency achieved would be 1024 Hz, with a prescale setting of 32.
I see a number of issues with your ISR code -
Some other observations may result in simplification of the code, or improved operation -
Since you are totalising four HR periods (for a reading update interval of 1-6 seconds), and you appear to have no ultimate interest in the individual periods, the totalising might be done very simply within the ISR code. In fact the TPMC0V value need only be processed every fourth interrupt, involving a single period calculation.
Perhaps the range limit test should be done in terms of the equivalent period, rather than frequency, and done within the ISR to quickly determine whether the current reading should be aborted, and a new sequence started. If the lowest allowable HR is 40 bpm, surely anything below this should be considered a "no signal" case, rather than using a separate OFFTIME parameter.
There may still be an issue with the no signal case, where the period may exceed the TPM overflow period, and no capture interrupt will occur anyway. A possible solution to this problem is to use a second TPM channel in output compare interrupt mode. The idea is to set the compare value, during each input capture interrupt, to the current capture value plus an amount corresponding to the period for the 40 bpm limit. During normal operation, the OC interupt should never occur, but when it does, this will positively indicate a no signal or intermittent condition.
With the present code the LED will remain on or off for a full HR period, i.e. does not give a flash for each pulse. Again, a TPM output compare channel might be used to provide a short flash of perhaps 200 ms. The LED would be turned on within the input capture processing, and turned off by the output compare event. It may be possible to use the same OC channel for LED timing and no signal detection.
Regards,
Mac
Thanks very much for your input Mac. I think I have fixed the main issues, as well as changing the program a bit, so that it takes the average of the periods and then calculates bpm.
bigmac wrote: ...In fact the TPMC0V value need only be processed every fourth interrupt, involving a single period calculation.
I am still calculating the period every pulse, as i still want the visual indication of blinking LED every heart beat, I've tried to do this without using a second TPM channel... to keep it simple.
I have set it to use TPM clock, but am not sure how to actually connect a crystal to the TPMCLK pin. I understand how to connect and external clock for the uC (connecting crystal + Cs + R across XTAL and EXTAL pins), what is required for TPMCLK, or should I just use my external clock?
Below is my revised code, please let me know if it's an improvement or not on the previous attempt, and if I'm still doing anything obviously stupid.
/* Program to calculate HR from Polar RMCM-01 reciever - outputs 1ms digital pulse when HR detected. Set up TPM input capture to measure period between pulses, take in 4 readings and calculate average. Blink LED each pulse. Use period to calculate HR in bpm. Store the average HRs with time stamp. */ #define NUMREADINGS 4 volatile int once; volatile long period; // Initialise input pin and status LED, clock void init(void) { PTCD_PTCD0 = 0; // PTC0 as input for HR PTDD_PTDD0 = 1; // PTD0 as output for LED TPM3SC = 0b00011101;// Use TPM clock, scale by 32 to give ~1000Hz TPM3C0SC = 0b01000100; // enable interrupt, rising edge only, } // Calculate BPM void bpm(void) { long hr; if (once != 1) { hr = (60000 / period); //calculate bpm } once = 1; } // Calculate period with ISR void interupt VectorNumber_Vtpm3ch0 period_isr(void) { static unsigned long previous_beat; int readings[NUMREADINGS]; int index = 0; long total = 0; long average = 0; TPM3C0_CH3F = 0; //clear interrupt flag // Loop through readings for (int i = 0; i++) { readings[i] = 0; // set readings to 0 period = TPM3C0V - previous_beat; // Calculate period in ms previous_beat = TPM3C0V; // store current capture PTDD_PTDD0 = !PTDD_PTDD0 // Blink LED on pulses } if ((period >= 1500) && (period <= 270)) // limit for 220>bpm>40 { total -= readings[index]; // subtract the last reading readings[index] = frequence; total += readings[index]; // add the reading to the total index = (index + 1); // advance to the next index } else { average = 0; index = 0; } if (index >= NUMREADINGS) // if at the end of the array { index = 0; // go back to beginning of array average = total / NUMREADINGS; // calculate average period } once = 0; } //Store heart rate (average) with timestamp??
Regards,
Iain
Hello Iain,
I previously referred to the use of the fixed system clock (XCLK), which makes use of the external reference clock. The TPMCLK input is a different animal.
I can still see a number of issues with your code. A major one seems to be the scope of some of your variables. How does the bpm() function return the bpm value, since hr is a local variable that will not be visible when the function exits? I would suggest that the function directly return the heart rate (byte) value. I also notice that the function still makes use of the period value, even though this does not seem now appropriate to the ISR code.
Personally, I would not calculate average within the ISR, but would make direct use of total value, perhaps similar to the following example. I have also taken into account a specific no signal case.
// Global variables:volatile int total;volatile byte once = 1, index = 0, cycle = 0, phase;#define NUMREADINGS 4#define MAXLIM 1500#define MINLIM 270byte calc_bpm( void){ dword hr; if (once == 0) { // Next reading is ready hr = 60000L * NUMREADINGS / total; once = 1; // Enable next reading to start } else if (once == 0xFF) { // No signal once = 1; // Enable next reading to start cyle = 0; // Commence new reading cycle index = 0; return 0; // Indicate invalid result } return (byte)hr; // Return heart rate value}
Within the ISR code your intent is a little unclear. The presence of the for loop suggests that you expect to remain within the ISR while four readings are processed, but with no monitoring of the presence of the channel flag. While this might be made to work, it would be a poor coding practice. The ISR should process a single input capture event only, and then exit. Additionallly, the average variable does not have visibility outside of the ISR, yet this seems to be the value from which the heart rate is calculated. The flag clearing is also suspect, with apparent confusion between TPM number and channel number.
You suggest that you will stay with the use of a single TPM channel "for simplicity". However, this will result in some negative effects, some of which I alluded to in a previous post. With the LED remaining on or off for a whole hr period, and then toggling, when the HR signal disappears there would be 50 percent probability that the LED would remain in a continuously on state - probably not desireable. Additionally, all input capture interrupts would cease, leaving no way of indicating the loss of signal to the main function. Therefore, an additional timing function would be required, hence the use of a second TPM channel in output compare mode.
The following untested ISR code is my take on providing a "rolling total" for multiple HR periods.
void interrupt VectorNumber_Vtpm3ch0 ISR_TPM3C0( void){ static word prev, period; static word array[NUMREADINGS]; TPM3C0SC_CH0F = 0; // Clear flag // LED flash control: PTDD_PTDD0 = 1; // Turn LED on TPM3C1V = TPM3C0V + 200 // Update Ch1 OC for 200ms flash phase = 0; // Process input capture value: if (once) { // Test for previous reading processed period = TPM3C0V - prev; prev = TPM3C0V; // Test for valid period range: if ((period <= MAXLIM) && (period >= MINLIM)) { // Ramge is valid if (++index == NUMREADINGS) { index = 0; cycle = 1; // Indicate cycle completed } if (cycle) { total -= array[index]; // Subtract oldest period once = 0; // Flag new reading is ready } array[index] = period; // Update array total += period; // New totalised value else { // Period range is invalid total = 0; cycle = 0; // Commence new reading cycle index = 0; } }}
The following code is the output compare ISR for handling both LED turn off and no signal detection.
void interrupt VectorNumber_Vtpm3ch1 ISR_TPM3C1( void){ TPM3C1SC_CH1F = 0; // Clear flag if (phase == 0) { PTDD_PTDD0 = 0; // Turn LED off TPM3C1V += (MAXLIM - 200); // Ready for no signal test phase = 1; } else if (phase == 1) // No signal state caused interrupt once = 0xFF; // Flag no signal}
Regards,
Mac
Thanks Mac, you've been a big help!
I realised the mistake with the TPM and channel number in my flag clear, and I have changed the TPM3SC register, so that it uses the XCLK.
Think I've now got my head around whats happening in your provided code, just a couple questions:
1) in the hr calculation, what is the purpose of the 'L' in 6000L, or is it simply a typo?
2) I assume the HR chip needs to be connected to TPM3CH0 pin on micro, is it correct that the LED should be connected just to a GP output pin, in this case PTD0, rather than the TPM3CH1 pin?
Again, thanks for your help.
Regards,
Iain
Hello Iain,
The 'L' is intentional, to explicitly indicate to the compiler that 60000 is to be treated as a long value. Whether it is strictly necessary in this case, I don't know. It may also potentially avoid some compiler warnings.
You are correct about the connection of the HR chip. Yes, it is intended that the LED connect to PTD0 pin, per your original code. I forgot to mention earlier that TPM3 channel 1 should be setup as output compare interrupt only. The TPM3CH1 pin would be available as GPIO.
I have just noticed that there is a missing closing brace within my posted ISR_TPM3CH0() code, immediately prior to the else block. I presume that you have already discovered the error if you attempted to compile the code. There is also another error - the block if (++index == NUMREADINGS) { ... } is in the wrong position. It should be placed just before the missing closing brace, so that four readings will occur before cycle becomes set to 1.
Regards,
Mac
Hi Mac,
L meaning long value - handy thing to know!
I thought TPMC1SC should be set up that way, glad you confirmed that.
I currently have TPM3 channel 0 pin set as input, but I read that this is probably unnecessary, as it is automatically configured as an input when operating in capture mode.
Also, picked up on missing brace when I compiled, and will move that code block.
Regards,
Iain
Sorry forgot to ask...
As I will be using the external clock source for my TPM (currently the built in 32.768kHz crystal of my DEMOQE board), I realise I will need to initialise this, but have not made use of it before.
I have been looking at the clock control registers, but am not sure what I need to set. Here is what I think I need:
ICSC1 = 0b11000000 // sets external clock for FLL circuitryICSC2 = 0b00000100 // for crystal connected to EXTAL and XTAL pins// no need to set anything for ICSSC register?
...is this right? or even close?
Regards,
Iain