For the Mini Maker Faire, I put together the pulse monitor board with a 240×320 full-color TFT display, to make a self-contained pulse monitor (no laptop for display or power):

The display showing my pulse (which was a bit higher than my usual resting pulse). The trace is drawn left-to-right taking 5.33 seconds for each pass. The gap in the middle shows where the new trace is currently being drawn. The heart blinks with the pulse.

The block diagram shows the components I assembled for the monitor. I deliberately am not showing the pulse-monitor amplifier board, since that is a design exercise in my applied electronic course.
I need to redesign the finger block to be easier for kids to use—perhaps a more open design with the phototransistor not so deep. I tried doing a back-scattering design (with the LED and phototransistor adjacent on a board), and I got a usable signal, but it seemed to be even touchier and more sensitive to motion artifacts than this block. The motion artifacts mainly come from varying the amount of pressure with which the finger is pressing against the hole in the block for the phototransistors.
Real pulse monitors clip onto a fingertip or earlobe, so that the person does not have to keep their hand relaxed and still, but I’ve not yet come up with an easy-to-make design that works (mechanical design had never been my strength).
Here is the rather crude source code for the pulse monitor:
// code for pulse monitor on Teensy 3./3.2 board
// Using ILI9341 TFT 240x320 TFT display
// Sun 20 March 2016 Kevin Karplus
#define MONITOR_PIN (A0)
#include "SPI.h"
#include "ILI9341_t3.h"
#include "font_GeorgiaBold.h"
// Use pins 9 and 10 for the DC and chip-select inputs of the TFT SPI interface
#define TFT_DC 9
#define TFT_CS 10
// Use hardware SPI (#13=SCK, #12=MISO, #11=MOSI) and the above for CS/DC
ILI9341_t3 tft = ILI9341_t3(TFT_CS, TFT_DC);
static const unsigned char heart_data[] = {
/* Glyph 0: size=25x22, offset=0,-2, delta=25
** **
******** *******
*********** ***********
***********************
*************************
*************************
*************************
*************************
*************************
***********************
***********************
*********************
*******************
*****************
***************
*************
***********
*********
*******
*****
***
*
*/
0x19,0xB0,0xEC,0x80,0xC0,0x18,0x03,0xFC,0x1F,0xC1,
0xFF,0xDF,0xFC,0x7F,0xFF,0xFF,0x5F,0xFF,0xFF,0xFE,
0x1F,0xFF,0xFF,0xC3,0xFF,0xFF,0xE0,0x7F,0xFF,0xF0,
0x0F,0xFF,0xF8,0x01,0xFF,0xFC,0x00,0x3F,0xFE,0x00,
0x07,0xFF,0x00,0x00,0xFF,0x80,0x00,0x1F,0xC0,0x00,
0x03,0xE0,0x00,0x00,0x70,0x00,0x00,0x08,0x00,0x00,
};
static const unsigned char heart_index[] = {
0x00,0x00,
};
/* font index size: 2 bytes */
/*
typedef struct {
const unsigned char *index;
const unsigned char *unicode;
const unsigned char *data;
unsigned char version;
unsigned char reserved;
unsigned char index1_first;
unsigned char index1_last;
unsigned char index2_first;
unsigned char index2_last;
unsigned char bits_index;
unsigned char bits_width;
unsigned char bits_height;
unsigned char bits_xoffset;
unsigned char bits_yoffset;
unsigned char bits_delta;
unsigned char line_space;
unsigned char cap_height;
} ILI9341_t3_font_t;
*/
const ILI9341_t3_font_t heart_font = {
heart_index,
0,
heart_data,
1,
0,
0,
0,
0,
0,
13,
5,
5,
3,
4,
5,
24,
22
};
#define HYSTERESIS_THRESH (1000)
// filtered signal is turned to square wave with
// hysteresis with thresholds +-HYSTERESIS_THRESH
#define DEFAULT_PERIOD ((int32_t) (60e6/70.)) // period for 70bpm
static volatile int32_t x_0, x_1, x_2;
static volatile int32_t y_0, y_1, y_2;
// filter parameters for biquad bandpass filter
// selected for approx 0.66--6Hz with 60Hz sampling
#define SAMPLE_FREQ (60) // sampling frequency in Hz
#define a0 (256)
#define a1 (-388)
#define a2 (141)
#define SAMPLE_PERIOD_USEC (1.e6/SAMPLE_FREQ)
#define gain (1)
// b0= - b2= gain*a0
// b1=0
#define DELAY_XY (x_2=x_1, x_1=x_0, y_2=y_1, y_1=y_0)
#define GENERAL_BANDPASS (y_0 = ((gain*a0)*(x_0-x_2) -a1*y_1 -a2*y_2)/a0, DELAY_XY)
#define NUM_TIMESTAMPS (8)
volatile int32_t time_falling[NUM_TIMESTAMPS];
// time (from micros()) of last NUM_TIMESTAMPS falling edges
// (may want to replace with circular buffer)
volatile int32_t num_edges_since_pulse_found=0;
volatile bool reported_edge=0; // most recent edge has been reported in loop()
IntervalTimer sampler;
volatile uint16_t xloc; // location of trace in x dimension
uint16_t old_xloc; // old value of xloc, to detect change outside interrupt routine
volatile bool squared_pulse=0; // square wave made from pulse signal
bool heart_displayed; // current state of heart display
void one_sample(void)
{
x_0=analogRead(MONITOR_PIN);
GENERAL_BANDPASS;
xloc++;
if (xloc>=320) {xloc=0;}
if (squared_pulse && y_0< -HYSTERESIS_THRESH) { squared_pulse =0; for (int i=NUM_TIMESTAMPS-1; i>0; i--)
{ time_falling[i]= time_falling[i-1];
}
time_falling[0]=micros();
reported_edge=0; // new edge not reported yet
}
else if (!squared_pulse && y_0 > HYSTERESIS_THRESH)
{
squared_pulse =1;
}
}
// convert the timestamps in time_falling
// to periods and report the median of the
// NUM_TIMESTAMPS-1 periods
int32_t median_period(void)
{
int32_t periods[NUM_TIMESTAMPS-1]; // sorted array of periods
// (increasing)
for (int i=0; i<NUM_TIMESTAMPS-1; i++) { int32_t p=time_falling[i]-time_falling[i+1]; int j; // do a crude insertion sort, since list is so short for (j=i; j>0 && periods[j-1]>p; j--)
{ periods[j] = periods[j-1];
}
periods[j] = p;
}
return (NUM_TIMESTAMPS%2)?
(periods[NUM_TIMESTAMPS/2-1] +periods[NUM_TIMESTAMPS/2])/2:
periods[NUM_TIMESTAMPS/2-1] ;
}
void draw_heart(bool red)
{ tft.setFont(heart_font);
tft.setTextColor(red? ILI9341_RED: ILI9341_WHITE);
tft.setCursor(5,5);
tft.drawFontChar(0);
}
void clear_text(uint16_t x_start=40, uint16_t y_start=0,
uint16_t x_stop=319, uint16_t y_stop=60)
{
tft.fillRect(x_start,y_start,x_stop,y_stop,ILI9341_WHITE);
tft.setCursor(x_start,y_start+5);
tft.setFont(Georgia_14_Bold);
tft.setTextColor(ILI9341_BLACK);
}
void setup(void)
{
tft.begin();
tft.fillScreen(ILI9341_WHITE);
tft.setTextSize(2);
tft.setRotation(1); // Header pins are on the right.
pinMode(MONITOR_PIN, INPUT);
analogReadRes(16);
analogReadAveraging(32);
Serial.begin(115200);
squared_pulse=0;
for (int i=NUM_TIMESTAMPS-1; i>0; i--)
{ time_falling[i]= 0;
}
num_edges_since_pulse_found=0;
sampler.begin(one_sample, SAMPLE_PERIOD_USEC);
heart_displayed=0;
draw_heart(heart_displayed);
old_xloc=xloc=0;
}
void loop(void)
{
if (Serial.available())
{ char c= Serial.read();
if (c=='r')
{ setup();
}
}
#define scale (512)
if (xloc!=old_xloc)
{ tft.drawFastVLine(xloc,75,240,ILI9341_WHITE);
int32_t low_y,height;
if (y_0<y_2)
{ low_y=(y_0/scale)+170;
height = (y_2-y_0)/scale;
}
else
{ low_y=(y_2/scale)+170;
height = (y_0-y_2)/scale;
}
if (low_y<80) { height -= (80-low_y); low_y=80; } if (height>=0)
{ tft.fillRect(xloc-1,low_y-1, 3, height+3, ILI9341_BLACK);
}
old_xloc=xloc;
if (squared_pulse==heart_displayed)
{ // update heart display
heart_displayed = !squared_pulse;
draw_heart(heart_displayed);
}
}
if (reported_edge) return; // nothing new to report
reported_edge=1;
int32_t period = time_falling[0]-time_falling[1];
if (period < 250000 || period > 3000000)
{ clear_text();
tft.setTextColor(ILI9341_RED);
tft.println("PULSE LOST");
tft.setCursor(40,25);
tft.print("period=");
tft.println(period);
num_edges_since_pulse_found = 0;
return; // bogus short or long pulse (maybe should adjust hysteresis?)
}
num_edges_since_pulse_found ++;
if (num_edges_since_pulse_found<=0)
num_edges_since_pulse_found=NUM_TIMESTAMPS; // handle rare overflow
if (num_edges_since_pulse_found<NUM_TIMESTAMPS)
{ // report number more pulses needed
clear_text();
tft.setTextColor(ILI9341_RED);
tft.println("PULSE LOST");
tft.setCursor(40,25);
tft.print(NUM_TIMESTAMPS-num_edges_since_pulse_found);
tft.println(" pulses needed");
return;
}
int32_t mid_period = median_period();
clear_text();
tft.print(60e6/period,1); tft.println(" bpm");
tft.setCursor(40,25);
tft.print(60e6/mid_period,1); tft.println(" bpm (median)");
}
Like this:
Like Loading...