Introduction: ECG Simulator

About: Jim Lynch lives in Grand Island, New York and is a software developer for Control Techniques, a subsidiary of Emerson Electric. He has two grown children, Liz and Chris. Jim lives alone with his dog Comet. …

This instructable describes in detail the steps required to create an Arduino-based ECG simulator. An ECG simulator replicates the cardiac waveform that can be measured by attaching three electrodes (RA, LA, RL) to the patient's chest. This ECG signal is only a few millivolts in amplitude. The finished project is shown in the first photograph below.

The project was built using an Adafruit Menta kit plus a few additional parts. The Menta kit includes a Arduino ATMega328P microprocessor with 32K of Flash memory and 2K of RAM memory plus an Altoids-type metal case which has enough room to fit a small numeric display, a potentiometer to adjust the heart rate, and three banana receptacles for the patient leads.

The waveform was created by first doing a screen capture of a suitable waveform image from the Internet. This picture file was then digitized using the open source Engauge program from Sourceforge. The resulting text file was further processed by a custom Python program that used linear interpolation to space the samples 1.0 millisecond apart followed by formatting the digitized table into a C Language array construct that could be pasted into the Arduino sketch.

To output an analog waveform on the Arduino Menta, an inexpensive Microchip 12-bit digital-to-analog converter was soldered to the Menta prototyping area. A simple resistive voltage divider was employed to attenuate the D/A signal to the required millivolt levels.

The resulting signal from the Adafruit Menta was then connected to a Texas Instruments ADS1293EVM Evaluation Module which itself demonstrates the operation of their ADS1293 ECG front-end chip (a single integrated circuit that implements all the signal processing normally found in the front end of an ECG heart monitor).

The TI software that shipped with the Demonstration Kit was used to display the incoming ECG signal from the ECG simulator which agreed closely with the shape and amplitude of the ECG waveform captured from the Internet document.

While this project was directed solely at generating an ECG signal, the methodology could be used to create just about any waveform you can draw or extract from a document!

The project also shows just how useful the Adafruit Menta kit can be when used as a starting point for a custom, one-off embedded micro-controller design. I needed an ECG simulator and was able to build it for about $60 in parts.

If you would like to view the full tutorial (pdf) of the project, which goes into much more detail about the techniques and procedures used to replicate the ECG waveform, click on this link:

https://github.com/lynchzilla/ECG_Simulator_Documents

This will bring up the summary GitHub repository (second figure above) which has only the detailed tutorial (pdf) and the Arduino sketch (ino). Just click on the ZIP button to download these two files (about 136 megabytes).

If you would like to have everything about the project including the Libra Office document and all drawings, click on the following link (about 242 megabytes):

https://github.com/lynchzilla/ecg_simulator



Step 1: Background

Twenty six years ago, I was in charge of of software development for the world's first color heart monitor, the Mennen Medical Horizon 2000. The CRT display for this instrument was the first to make use of color for alarm conditions, alerts, and so forth.

The Horizon 2000 patient monitor had two Motorola 68000 microprocessor circuit boards, one for signal collection and one for the
graphics display and soft-menu system. I wrote the software for the signal collection board and my astute and industrious office mate, Linda, wrote the software for the board supporting the menus and displays. In a design feature rarely seen today, the 68000 boards communicated via a shared, arbitrated dual-port RAM memory.

While developing software for the heart monitor, one indispensable tool I used was a “ECG simulator”. This was a device that created a reasonable facsimile of an ECG signal (that waveform you see on all medical shows). These units were usually battery-powered and the heart monitor's ECG leads were connected to the simulator rather than a patient (or yourself).

These ECG simulators can be bought on Ebay for a couple hundred dollars. An ECG simulator can be very sophisticated, displaying not only the standard “normal sinus rhythm ECG” but the abnormal waveforms as well (arrhythmia,
tachycardia, and so forth).

Step 2: TI ADS1293 Single Chip Heart Monitor

As I near retirement, I'm interested in building a heart monitor for myself, you know all DIY and open source. These days you can get the entire analog front-end for an ECG monitor as an inexpensive integrated circuit from Texas Instruments. The ADS1293 device supports four ECG leads and does all the A/D conversion and signal processing. You can communicate through a simple SPI interface and this should make it straightforward to “isolate” the ADS1293 from the rest of the heart monitor using, for example, Analog Devices transformer-based digital isolators.

To learn all about the TI ADS1293 ECG front-end chip, I purchased a $99 evaluation board from TI. The ECG leads are wired through a 9-pin D-connector and a USB cable connects this board to the PC and TI supplies a sophisticated Windows application that lets you modify all the chip's control registers, view the signals, and so forth. The first step in learning about any sophisticated chip is to get a data sheet and an evaluation board.

Step 3: Need a ECG Simulator

Now we get to the heart of the matter; to use this TI evaluation board there must be an ECG signal.

Why not just hook yourself up with some ECG leads and those conductive pads they use in hospitals? First, Texas Instruments would totally freak out since there are no protection circuits on this evaluation board. By this I mean the isolation circuitry, the Zener diodes, the neon-lamps, current-limiting resistors normally used to protect the patient are not present on this board. Second,
such a scheme would be cumbersome and inconvenient.

Obviously I could buy a used ECG simulator, but that wouldn't be any fun. How about building one from scratch?

I looked at possible solution platforms and a nice starting point is Adafruit's Menta kit. The Menta is a Arduino board with an Atmel
ATMega328P 8-bit microprocessor with 32k of Flash memory and 2k of RAM. Notice that Lady Ada designed it to fit into a metal Altoids-style case.

Unfortunately the ATMega328P does not have a D/A (digital-to-analog) converter which will be needed to generate the analog ECG waveform. The Menta with the case is $35.00, delivered as a kit.

Looking at the Menta and applying a little arm chair engineering, we'll need to fit three banana receptacles on the top to attach the three ECG leads, a pot with a knob to adjust the heart rate, and a small 4-digit 7-segment numeric display to show the dialed-in heart rate.

The Menta has a breadboard area which can be used to fit a small D/A converter chip. A resistor voltage divider network will attenuate the 5 volt ECG signal from the D/A converter down to a couple of millivolts, which is the amplitude of a human ECG signal detected via chest electrodes.

The Menta can be powered via a cheap 9-volt Wall Wart using the power connector on the left.

Programming is accomplished using an inexpensive FTDI Friend board ($14.75 from Adafruit) using your computer's USB port. Becky Stern, an Adafruit employee and New York City artist, has a very nice introductory video about the Menta here:

http://www.youtube.com/watch?v=opuD2h4puSk&feature=player_embedded


Step 4: Block Diagram of Adafruit Menta ECG Simulator

A block diagram showing the basic components of the Menta ECG Simulator is shown here:

Step 5: Acquiring the Parts

The $35.00 Adafruit Menta is a kit that you must assemble. In the spirit of and even better than Heathkit, Adafruit has a detailed tutorial on their web site that shows how to assemble the Menta. In my case, it took less than an hour to solder the kit together.

I actually bought two kits, one for the ECG Simulator and one for bread boarding (where I fitted the headers for the I/O ports).

For the D/A converter, I selected the Microchip MCP4921 single channel D/A chip, which communicates via the SPI interface. The
resolution of the MCP4921 is 12-bits; it takes two sequential SPI 8-bit transmissions to send the 12 bits plus four configuration bits.

The D/A converter will operate at a rate of 1000 updates per second, or in other words 1.0 millisecond per sample. The D/A signal will be updated as part of an on-board Timer2 interrupt handler. The execution time to update the D/A via the SPI interface is only 63 microseconds (author's measurement).

Not long after I acquired the Microchip D/A chip, Adafruit offered a dual channel D/A device on a breakout board for $4.95. I'm sure that this device would be also perfect for this application. The Microchip MCP4921 can be ordered from Digikey for $2.36. The pin layout for the MCP4921 is given below.

For the 4 digit heart rate display, I considered three possible solutions. In the photograph, the top device is the Adafruit 0.56" 4-Digit 7-Segment Display w/I2C Backpack for $9.95. This display's onboard controller makes it easy to communicate via SPI and the numbers are large and bright. However, you can see from the figure that it's a bit too big for the Menta cover.

The middle device is the Adafruit Monochrome 1.3" 128x64 OLED graphic display for $24.50. It's small and thin and the interface is SPI so it looked like a good choice. It proved unsuitable because the interface is write-only so you can't read back the internal graphic RAM. This means that Adafruit's software driver had to maintain a complete copy of the display's graphic RAM on the ATMega328P chip. Worse yet, the driver Adafruit prepared writes the entire graphic RAM for any command, even if you only wanted to change one pixel. I measured the execution time to update the entire graphic RAM and it was longer than the expected D/A sample period of 1.00 msec. Reluctantly I set this one aside for future projects.

The bottom display device is the Sparkfun COM-09764 7-Segment Serial Display – Blue for $12.95. It also has a controller that makes it work with a simple SPI interface. Note that the size looks appropriate for the Menta cover, so the Sparkfun display was best for this project.

For the pot to adjust the heart rate, I found the typical pot available from Radio Shack (and Adafruit) to be too thick for the Menta case. Searching the Jameco catalog, I found a pot intended for circuit board mounting that is somewhat thinner.

The Jameco pot is the bottom one in the picture. This is the Panel Control - 22MM-ST-CP 3 (part number: 1998141) for $3.39. The metal clip can be popped off to give a much thinner profile (this is a 5k pot).

I also purchased a knob from Jameco, this is Knob ¼" Shaft, Metal, Round, Silver for $0.99 (part number: 162481).

The banana receptacles, resistors, and capacitors are stock items at Radio Shack.

Step 6: Bill of Materials

Here is the bill of materials for the ECG Simulator project.

Step 7: ECG Simulator Schematic

Below is the final schematic for the Menta ECG Simulator project. LadyAda's Eagle schematic for the Menta was used as a starting point and I simply added the parts required to complete the project. I did not attempt to make a custom circuit board from this schematic since the intent is to simply solder the D/A converter, etc. directly to the breadboard area of the Menta

Step 8: Menta ECG Simulator Wiring Diagram

The figure below shows the wiring of the Menta ECG Simulator. All wiring is routed on the top (component) side of the Menta circuit board and most soldering is on the bottom side of the board. Solder joints are identified as small black circles. I elected to simply solder the MCP-4921 D/A converter chip directly to the board, but one could easily substitute an 8-pin IC socket instead.

Step 9: Wiring the Menta ECG Simulator Board

The figure below shows the completed ECG Simulator prior to installation into the Altoids case.

I added a couple of stake pins for ground and the output of the D/A converter so operation could be verified with an oscilloscope. Since the banana jacks for the ECG leads have to be bolted onto the case, they can't be soldered to the green outputs from the voltage divider at this point.

The Adafruit Menta is specifically designed for the type of build you see here. The I/O ports and power/ground access points are all doubled up, all holes are plated-through, and all pads are tinned for easy soldering. If you can build a circuit with a breadboard, you can build a one-off prototype with the Adafruit Menta!

Step 10: Drilling the Menta Mounting Holes

Rather than trying to measure and locate the circuit board mounting holes on the bottom of the Altoids metal case, it's a lot easier to drop the Menta board into the case and use a center punch to mark the holes, as shown below. A center punch uses a spring-loaded pin to dimple the metal surface and thus provides a good starting point for a drill bit.

The next photo below shows the dimples created by the center punch's spring-loaded pin driver, ready for the
drill press.

Now the circuit board holes can be drilled with a desk-top drill press. The Adafruit Menta holes measured 7/64” diameter so a 1/8” drill bit seemed suitable. The next photo shows the drill press in action. The resulting holes on the bottom side of the Altoids tin have ragged “burrs” after drilling so these can be ground down using a Dremel MotoTool with a flat grinding wheel.

The final photograph below shows the completed circuit board mounting holes.

Step 11: Design the Front Panel

Phil at Jumper One created an excellent tutorial  concerning making nice front panels at home.
http://jumperone.com/2013/01/how-to-make-diy-front-panel/

Based on Phil's suggestions, the first thing to do is design a front panel layout that locates all the holes, etc. A good drawing tool to use is Inkscape, an open source and free vector-based drawing system. Inkscape can be downloaded from here:
http://www.inkscape.org/download/

I purchased “The Book of Inkscape” by Dmitry Kirsanof (one of the current developers of Inkscape) and spent a couple of days learning just enough to create some pretty fancy drawings. The photo below illustrates an Inkscape-created stick-on label for the front panel.

The second photo below shows a dimensional drilling diagram of the front panel (the top cover of the Altoids tin) which locates the holes for the banana jacks, potentiometer, and the 7-segment display cut-out. The design takes into account the underneath sizes of the banana jacks, display, and potentiometer so that these parts don't interfere with each other (the underneath outlines are shown as dashed lines).

Step 12: Drill the Front Panel

Print out the front panel label on ordinary printing paper. Cut out the outline with scissors, then use a hobbyist razor knife and a straight edge to cut out the rectangular hole for the 7-segment display.

Using masking tape, carefully affix the drilling template to the Altoids case front cover as shown in the first photo below. Using a “magic marker and a straight edge, outline the 7-segment display rectangle as shown in first photo. As illustrated in the second photo, center punch the three banana jack holes, the rate potentiometer hole, and a hole in the center of the 7-segment display rectangle (for an opening to get the nibbling tool started). This will be used to “nibble” the rectangular outline for the display. The third photo shows the Altoids front cover ready for drilling and nibbling.

While these holes can be drilled with a hand-held electric drill, a desk-top drill press is more suitable for this task. The fourth photo shows a banana jack hole being drilled. A large hole (11/32”) should be drilled in the center of the 7-segment display rectangle to serve as an entry point for the Radio Shack nibbling tool (shown in the fifth photo).

Step drills, as shown in the sixth photo, are extremely useful in widening holes and removing burrs. These can be acquired from
Amazon or eBay. Normally they don't need a pilot hole if a spring-loaded center punch is used to locate the hole. The ones the author procured have “flats” on the shaft that eliminate drill chuck slippage.

The Radio Shack nibbler tool, shown earlier, chops a tiny 7/32” x 1/16” rectangular bite out of the metal it is cutting. Basically you insert the cutting tip down into the pilot hole and start nibbling away as shown inin the seventh photo. If you are careful, a fairly decent cut can be achieved. I cleaned up the final cut with a flat file and then ensured that the 7-segment display will fit tightly through the opening. The finished product is shown in the eighth photo below.

Step 13: Cutting the Power Access Port

The last bit of drilling is to provide the access for the power connector. The first photo shows the drilling diagram; a 5/16” hole was drilled and the Radio Shack nibbler was used to form a 1/2” rectangular cutout. If the banana jacks are defined as the top, then this power access port would be located bottom left.

The second and third photos show two different drill bits being used.

After doing some nibbling, the square hole was cleaned up with a hand file (fourth photo). The finished product is shown in the fifth photo.

Step 14: Create and Attach the Front Panel

While I had considered using Press-Type lettering and a clear lacquer top coat to mark the top of the Altoids tin, a much better solution is to print the label on white label stock and then cover that with single-sided clear plastic laminating sheet. These items are available from Amazon as shown below.

"Avery® White Full-Sheet Labels for Inkjet Printers with TrueBlock(TM) Technology, 8-1/2 inches x 11 inches, Pack of 25 (8165)"
Office Product; $9.98

"Scotch® Laminating Sheets LS854SS-10, 9 Inches x 12 Inches, Letter Size, Single Sided" Office Product; $7.97

On the Avery 8165 label stock, print out the front panel label as shown on the left in the first photo below. Using sharp scissors, carefully cut out the label outline. The holes for the banana jacks, potentiometer, and display rectangle can be cut freehand using a sharp hobbyist knife and a straightedge.

Likewise, the label can serve as a template to mark and cut out the clear plastic laminate sheet using the Scotch LS854SS-10 laminating sheets, as shown on the right in in the first photo.

Remove the paper backing from the laminating sheet (right in the first photo below) and apply it to the top of the label (left in same photo). Using a sharp hobbyist knife, cut out the holes and the rectangular cutout for the display again. Now we have a stick-on label with a laminated plastic sheet that will protect the lettering for a long time (and look very nice too).

To actually stick the label on the case, I temporarily installed one banana jack and potentiometer using masking tape to help register the label (you only get one try at this). Remove the bottom paper backing from the label and stick it on the Altoids tin. This procedure is illustrated in the second photo.

The third photo below shows the Altoids tin with the label attached. Any little mismatches or anomalies around the holes will be covered by the hardware when the banana jacks and potentiometer are installed.

Step 15: Insulating the Altoids Tin Bottom

The inside bottom of the Altoids tin has to be insulated prior to mounting the Menta circuit board. While the Scotch single-sided laminating sheets would be satisfactory, I elected to use heavier plastic stock that is used as front and back covers for report binding. Using the label as a template, the resulting plastic label can be marked for the four mounting holes and then these holes can be cut
out using a razor knife. This plastic insulating insert is shown on the right in the photo below. Just remember to install this insulating sheet before dropping the Menta circuit board into the case.

Step 16: Installing the 7-Segment Display

While the Sparkfun display is a very nice product, it appears that it was designed for bread boarding only. There is no obvious way to mount it in the Altoids case. I elected to epoxy “wings” to the sides of the display and then epoxy this assembly to the case (from underneath).

I used some 1.8” x 3.2mm ABS square rod cut to 1 1/2” for the wings. This stock can be procured from http://www.hobbylinc.com/

PLS90351 Square Rod ABS 1/8 (5) $4.59

Before attempting to epoxy this rectangular bar stock to the display, it's wise to roughen two adjacent edges with a file before gluing. This is a two-step process; first two “wings” are epoxied to the sides of the display (as shown in the first photo), and then this assembly is epoxied to the to the Altoids case from underneath (second photo).

Step 17: The Banana Jacks Need Some Surgery

The stock Radio Shack banana jack is too long and would contact the Menta circuit board if installed unmodified. Basically it has to be shortened.

Disassemble the stock banana jack and grip the bottom part (the plastic threaded part that will bolt to the case) with vice grip pliers. Be sure to leave the washer and threaded nut on the bottom assembly because they will be used to “restore” the plastic threads after cutting through. Just backing the threaded nut off after cutting cleans up the threads damaged by the cutting tool.

I used a Dremel MotoTool with a cut-off wheel to cut through the plastic threads. Try to leave two or three threads on the bottom part. The first photo below shows the bottom part of the banana jack being shortened. As you can see, the nut is still installed so you can back it off after cutting and thereby restore the threads.The second photo shows the result of shortening the banana jacks. The stock Radio Shack part is shown for reference.

The banana jack metal post has to be shortened too. Leaving one nut threaded as shown in the third photo, use a Dremel MotoTool cut-off wheel to cut the post, leaving three or four threads.

The last photo shows the modified banana jacks (with the original for comparison). There's just enough threads in the post to back the nut away enough to loop and attach the ECG wires from the circuit board.

Step 18: Install the Menta Circuit Board

For the circuit board mounting hardware, I used M3 x 8mm machine screws, M3 flat washers, and a M3 hex nut. These fit the mounting holes nicely.

To insulate the bottom of the circuit board from the metal case, I cut a rectangular piece of thin plastic using the Menta board as a template. Plastic report covers work well for this purpose. An Exacto knife can be used to make cut-outs for the machine screws if a paper hole punch is not available.

Please, please, please don't forget the plastic insulating sheet placed below the circuit board. If you forget this, the Menta board will short circuit!

Step 19: Install the Banana Jacks and the Potentiometer

The last step in assembly is to mechanically mount the banana jacks and the potentiometer, as shown in the photo below. I simply formed a small loop at the end of the ECG wires and anchored this over the threaded post with the nuts supplied with the banana jack.

You might notice that I used a plug to connect to the Sparkfun 7-segment display. This is a Jameco product:

Jameco 70755 .100” (2.54 mm) female straight header receptacle 8-contacts $0.55

To provide adequate clearance, I bent the header pins soldered to the Sparkfun board 90 degrees. The potentiometer is fastened with a nut and a knob is attached and anchored with a tiny allen wrench.

Step 20: The Completed Hardware

Before closing the Altoids case, make sure that the Adafruit Menta circuit board's PWR_SEL jumper is set for external power (not USB driven). The power jumper is to the left of the Reset button, as shown in the first photo below. To select the external power via the jack, set the jumper to the right as shown.

The second photo shows the completed Adafruit Menta ECG Simulator. The ECG Simulator can be powered by just about any wall wart (DC power supply) you have around (for example, a 12 volt 500 ma supply). Now all we need is some software!

Step 21: Source of the ECG Signal

To get started, we need a suitable ECG waveform to digitize. The obvious thing to do is to find a waveform on the Internet and do a screen capture (convert it into a jpeg image file). A very good screen capture utility is MWSnap; it's free and easy to use. If you are running Windows 7 or 8, there's a nice accessory called the “snipping tool” that can do the same job. You'll find it in the start
menu under “accessories”.

The waveform selected, shown in the first figure below, is from an Army Flight Medical Training Course on Understanding ECG Waveforms. http://www.scribd.com/doc/6072532/ECG-Interpretation

This waveform was chosen for three reasons, it was relatively uncluttered, had both axes labeled numerically, and being a government document there should be no copyright violation in using it. I used the MWSnap screen capture utility to grab this image and create a jpeg file. The jpeg image was then converted to black-and-white and sharpened via the open source Picasa photo editor from Google.

Step 22: Digitize and Convert the Waveform to a C Array Structure

The screen captured ECG waveform was named “SampledECGWaveform.jpg” and stored in my c:\temp\ folder. The typical heart waveform is composed of the P-wave, the QRS complex (you know, the “ba-bump” part of your heart beat), and the T-wave.

On the horizontal time axis, major grid lines are at 0.200 seconds and the smallest grid divisions are 0.040 seconds. On the vertical amplitude axis, the major divisions are 1.0 millivolts and the smallest vertical divisions are 0.200 millivolts. After the last data point of the T-wave, the ECG waveform is quiescent until the next PQRST complex. That quiescent period (holding a constant sample) can be altered to vary the heart rate.

Normal heart rate for most people is 60 beats per minute or, in other words, the R-Wave peaks are one second apart. Now it's true that as the heart rate really speeds up, the QRS waveform compresses somewhat, but we will simplify the simulator by outputting the same QRS part followed by a variable quiescent part.

The other thing to know is that the amplitude of the ECG as measured by attached electrodes on the skin is just a couple of millivolts. The next step is to digitize the waveform with the given time and amplitude axes using the Open Source digitizer program Engauge.

The first figure shows the Enguage digitizer program being used to pick data points off the ECG waveform. Each little blue tick mark is a mouse click. Obviously, the more points one clicks, the better the rendition of the waveform will be.

When digitization is finished, a text file is created showing the (x, y) data points that were entered via Enguage (see the second figure below).

There are a couple of problems with this text file. First, the data points are not evenly spaced at 1.0 millisecond intervals (our target sample rate) and second, it's possible that some data points are out-of-sequence (you accidentally clicked a data point to the left of the previous data point).

To fix these problems and to scale the waveform points to a 12-bit D/A converter (0 .. 4095) and the time interval to 1.000 msec, a Python program was written to do these modifications (linear interpolation, sorting, and a lot of formatting) and to eventually massage the data into a perfectly legal const C array structure with an initializer (see the third figure below). This text file can be cut-and-pasted into the Arduino sketch.

The Python program is too complex for an Instructable, but you can download the Python program from the project repository on GitHub. Details on this repository are given at the beginning of this Instructable.

The C language Array structure with initializer only stores the PQRST part of the ECG waveform. The quiescent part is not stored; rather the D/A converter will "hold" a single value during the quiescent period. Modifying the number of samples emitted during the quiescent period effectively controls the heart rate. The fourth figure gives details about this feature.

Step 23: Arduino Software Development

While the Arduino script required to implement the ECG waveform generation is not difficult, it is a little more involved than the “blink an LED” introductory examples one may encounter. Here are some of the issues one must think about before entering the first line of code.

Waveform as a C-language Array
This project has two important criteria concerning the waveform. The sample rate will be 1000 times a second (1 millisecond per sample); this was chosen for good waveform fidelity. The scale for this waveform should be 0 to 4095 (to make full-scale use of a 12-bit unipolar D/A converter).

The waveform should reside in EPROM, easily satisfied by using a const C-array with an initializer in the following form:

const short y_data[] = {
939, 940, 941, 942, 944, 945, 946, 947, 951, 956,
962, 967, 973, 978, 983, 989, 994, 1000, 1005, 1015
};

By declaring the waveform array as a const array, it is assembled in the 32k EPROM rather than the more precious 2k of RAM which will be needed for variables, etc.

Waveform Updated as a Timer2 Interrupt
The waveform will be output at an update rate of 1000 samples per second. To do this, Timer2 will be used to count out a one millisecond period and then trigger a Timer2 interrupt. Within the interrupt routine, the next sample will be moved from the stored waveform array and sent to the D/A converter via the SPI interface. The Timer2 is restarted and this continues ad-infinitum (forever).

Heart Rate Displayed every 50 Milliseconds
Every 50th entry to the Timer2 interrupt routine, the heart rate selected by the user via the potentiometer will be sent to the 4-digit numeric display (again using the SPI interface). Both the D/A and the 4-digit display are updated within the Timer2 interrupt routine. Since they are serialized (one after the other), there will be no contention on the SPI interface bus.

Heart Rate Pot Read in the Background Loop
The Arduino background loop is where the analog voltage (0 to 5 volts) set by the pot is read using an analog input. This value will be used to specify the number of samples in the “quiescent period” of the waveform, specifically that flat-line period after the T-wave that continues to the start of the next PQRS complex. This “quiescent period” value will be written to a variable that will be read by the Timer2 interrupt routine.

Step 24: ECG Simulator - Arduino Sketch

// ***************************************************************************************************
//                                    ECG SIMULATOR
//
//  Purpose: simulate the normal sinus rhythm ECG signal (3-leads RA,LA,RL)
//
//  Background:
//
//  In normal electrocardiography (ECG or EKG if you're German), three leads make up the
//  Einthoven's triangle. Two leads are taped to the right and left side of the chest above
//  the heart (RA = right arm, LA = left arm) and one lead is taped to the lower chest, typically
//  on the right hip (RL = right leg).
//                                          
//  It's important to know that these ECG signals are millivolts in amplitude. This can be achieved by         
//  feeding the D/A converter through a voltage divider to get to the millivolt levels.
//
// 
//  The ECG signal:
//
//  I found a suitable ECG waveform from the internet. Here is how I converted a picture from my
//  monitor screen to a C language array of A/D values, each spaced 1.00 msec apart.
//                                          
//       A. Screen shot of waveform using the free screen capture program MWSNAP         
//                http://www.mirekw.com/winfreeware/mwsnap.html
//
//       B. Digitize the jpeg waveform using the free digitizing program ENGAUGE
//                http://digitizer.sourceforge.net/
//
//           
//       C: I wrote a Python program to convert the rather irregular samples from ENGAUGE
//          to an array of values spaced 1.0 milliseconds apart using linear interpolation.                                
//          Then I created a text file where these data points were part of a C language array
//          construct; that is, the data points are C initializers.
//
//       D: Cut-and-paste from the text file the C data array with initializers into the
//          Arduino sketch below.
// 
//
//  Arduino Resources:  
//
//    Digital Output # 9  - chip select the 7-segment display SPI port (low to select)
//    Digital Output # 10 - chip select for D/A converter  (low to select)
//    Digital Output # 11 - SDI data to the D/A converter  (SPI interface)
//    Digital Output # 13 - SCK clock to the D/A converter (SPI interface)
// 
//    Analog Input   # 0  - center wiper pin of 5k ohm pot (heart rate adjust)
//
//    I followed the Timer2 setup as outlined by Sebastian Wallin
//    http://popdevelop.com/2010/04/mastering-timer-interrupts-on-the-arduino/
//
//    I set up the SPI interface according to the excellent instructions of Australian John Boxall,
//    whose wonderful website has many excellent Arduino tutorials:
//    http://tronixstuff.wordpress.com/
//
//  Programmer:  James P Lynch
//                             lynch007@gmail.com
// 
// ***************************************************************************************************

#include "SPI.h"             // supports the SPI interface to the D/A converter and 7-segment display
#include <Wire.h>         // need the Wire library

// various constants used by the waveform generator
#define INIT          0
#define IDLE        1
#define QRS         2
#define FOUR      4
#define THREE    3
#define TWO         2
#define ONE         1



// *******************************************************************************************
//   y_data[543] - digitized ecg waveform, sampled at 1.0 msec
//
//   Waveform is scaled for a 12-bit D/A converter (0 .. 4096)
//
//   A 60 beat/min ECG would require this waveform (543 samples) plus 457 samples
//   of the first y_data[0] value of 939.
//
// *********************************************************************************************
const short  y_data[] = {
939, 940, 941, 942, 944, 945, 946, 947, 951, 956,
962, 967, 973, 978, 983, 989, 994, 1000, 1005, 1015,
1024, 1034, 1043, 1053, 1062, 1075, 1087, 1100, 1112, 1121,
1126, 1131, 1136, 1141, 1146, 1151, 1156, 1164, 1172, 1179,
1187, 1194, 1202, 1209, 1216, 1222, 1229, 1235, 1241, 1248,
1254, 1260, 1264, 1268, 1271, 1275, 1279, 1283, 1287, 1286,
1284, 1281, 1279, 1276, 1274, 1271, 1268, 1266, 1263, 1261,
1258, 1256, 1253, 1251, 1246, 1242, 1237, 1232, 1227, 1222,
1218, 1215, 1211, 1207, 1203, 1199, 1195, 1191, 1184, 1178,
1171, 1165, 1159, 1152, 1146, 1141, 1136, 1130, 1125, 1120,
1115, 1110, 1103, 1096, 1088, 1080, 1073, 1065, 1057, 1049,
1040, 1030, 1021, 1012, 1004, 995, 987, 982, 978, 974,
970, 966, 963, 959, 955, 952, 949, 945, 942, 939,
938, 939, 940, 941, 943, 944, 945, 946, 946, 946,
946, 946, 946, 946, 946, 947, 950, 952, 954, 956,
958, 960, 962, 964, 965, 965, 965, 965, 965, 965,
963, 960, 957, 954, 951, 947, 944, 941, 938, 932,
926, 920, 913, 907, 901, 894, 885, 865, 820, 733,
606, 555, 507, 632, 697, 752, 807, 896, 977, 1023,
1069, 1127, 1237, 1347, 1457, 2085, 2246, 2474, 2549, 2595,
2641, 2695, 3083, 3135, 3187, 3217, 3315, 3403, 3492, 3581,
3804, 3847, 3890, 3798, 3443, 3453, 3297, 3053, 2819, 2810,
2225, 2258, 1892, 1734, 1625, 998, 903, 355, 376, 203,
30, 33, 61, 90, 119, 160, 238, 275, 292, 309,
325, 343, 371, 399, 429, 484, 542, 602, 652, 703,
758, 802, 838, 856, 875, 895, 917, 938, 967, 1016,
1035, 1041, 1047, 1054, 1060, 1066, 1066, 1064, 1061, 1058,
1056, 1053, 1051, 1048, 1046, 1043, 1041, 1038, 1035, 1033,
1030, 1028, 1025, 1022, 1019, 1017, 1014, 1011, 1008, 1006,
1003, 1001, 999, 998, 996, 994, 993, 991, 990, 988,
986, 985, 983, 981, 978, 976, 973, 971, 968, 966,
963, 963, 963, 963, 963, 963, 963, 963, 963, 963,
963, 963, 963, 963, 963, 963, 963, 963, 963, 963,
964, 965, 966, 967, 968, 969, 970, 971, 972, 974,
976, 978, 980, 983, 985, 987, 989, 991, 993, 995,
997, 999, 1002, 1006, 1011, 1015, 1019, 1023, 1028, 1032,
1036, 1040, 1045, 1050, 1055, 1059, 1064, 1069, 1076, 1082,
1088, 1095, 1101, 1107, 1114, 1120, 1126, 1132, 1141, 1149,
1158, 1166, 1173, 1178, 1183, 1188, 1193, 1198, 1203, 1208,
1214, 1221, 1227, 1233, 1240, 1246, 1250, 1254, 1259, 1263,
1269, 1278, 1286, 1294, 1303, 1309, 1315, 1322, 1328, 1334,
1341, 1343, 1345, 1347, 1349, 1351, 1353, 1355, 1357, 1359,
1359, 1359, 1359, 1359, 1358, 1356, 1354, 1352, 1350, 1347,
1345, 1343, 1341, 1339, 1336, 1334, 1332, 1329, 1327, 1324,
1322, 1320, 1317, 1315, 1312, 1307, 1301, 1294, 1288, 1281,
1275, 1270, 1265, 1260, 1256, 1251, 1246, 1240, 1233, 1227,
1221, 1214, 1208, 1201, 1194, 1186, 1178, 1170, 1162, 1154,
1148, 1144, 1140, 1136, 1131, 1127, 1123, 1118, 1114, 1107,
1099, 1090, 1082, 1074, 1069, 1064, 1058, 1053, 1048, 1043,
1038, 1034, 1029, 1025, 1021, 1017, 1013, 1009, 1005, 1001,
997, 994, 990, 991, 992, 994, 996, 997, 999, 998,
997, 996, 995, 994, 993, 991, 990, 989, 989, 989,
989, 989, 989, 989, 988, 986, 984, 983, 981, 980,
982, 984, 986, 988, 990, 993, 995, 997, 999, 1002,
1005, 1008, 1012};


// global variables used by the program
unsigned int  NumSamples = sizeof(y_data) / 2;              // number of elements in y_data[] above
unsigned int  QRSCount = 0;                                                // running QRS period msec count
unsigned int  IdleCount = 0;                                                  // running Idle period msec count
unsigned long IdlePeriod = 0;                                              // idle period is adjusted by pot to set heart rate
unsigned int  State = INIT;                                                     // states are INIT, QRS, and IDLE
unsigned int  DisplayCount = 0;                                           // counts 50 msec to update the 7-segment display
unsigned int  tcnt2;                                                                 // Timer2 reload value, globally available
float         BeatsPerMinute;                                                     // floating point representation of the heart rate
unsigned int  Bpm;                                                                 // integer version of heart rate (times 10)
unsigned int  BpmLow;                                                          // lowest heart rate allowed (x10)
unsigned int  BpmHigh;                                                         // highest heart rate allowed (x10)
int           Value;                                                                         // place holder for analog input 0
unsigned long BpmValues[32] = {0, 0, 0, 0, 0, 0, 0, 0,     // holds 32 last analog pot readings
                                                             0, 0, 0, 0, 0, 0, 0, 0,     // for use in filtering out display jitter
                                                             0, 0, 0, 0, 0, 0, 0, 0,     // for use in filtering out display jitter
                                                             0, 0, 0, 0, 0, 0, 0, 0};    // for use in filtering out display jitter
unsigned long BpmAverage = 0;                                          // used in a simple averaging filter
unsigned char Index = 0;                                                       // used in a simple averaging filter 
unsigned int  DisplayValue = 0;                                           // filtered Beats Per Minute sent to display


void setup()   {

  // Configure the output ports (1 msec intrerrupt indicator and D/A SPI support)
  pinMode(9, OUTPUT);                     // 7-segment display chip select   (low to select chip)
  pinMode(10, OUTPUT);                    // D/A converter chip select       (low to select chip)
  pinMode(11, OUTPUT);                    // SDI data
  pinMode(13, OUTPUT);                    // SCK clock

  // initial state of SPI interface
  SPI.begin();                                                          // wake up the SPI bus.
  SPI.setDataMode(0);                                          // mode: CPHA=0, data captured on clock's rising edge (low-to-high)
  SPI.setClockDivider(SPI_CLOCK_DIV64);    // system clock / 64
  SPI.setBitOrder(MSBFIRST);                            // bit 7 clocks out first

  // establish the heart rate range allowed
  // BpmLow  = 300 (30 bpm x 10)
  // BpmHigh = (60.0 / (NumSamples * 0.001)) * 10  = (60.0 / .543) * 10 = 1104  (110.49 x 10)
  BpmLow = 300;
  BpmHigh = (60.0 / ((float)NumSamples * 0.001)) * 10;

  // First disable the timer overflow interrupt while we're configuring
  TIMSK2 &= ~(1<<TOIE2);

  // Configure timer2 in normal mode (pure counting, no PWM etc.)
  TCCR2A &= ~((1<<WGM21) | (1<<WGM20));
  TCCR2B &= ~(1<<WGM22);

  // Select clock source: internal I/O clock  
  ASSR &= ~(1<<AS2);

  // Disable Compare Match A interrupt enable (only want overflow)  
  TIMSK2 &= ~(1<<OCIE2A);

  // Now configure the prescaler to CPU clock divided by 128  
  TCCR2B |= (1<<CS22)  | (1<<CS20); // Set bits
  TCCR2B &= ~(1<<CS21);             // Clear bit

  // We need to calculate a proper value to load the timer counter.
  // The following loads the value 131 into the Timer 2 counter register
  // The math behind this is:
  // (CPU frequency) / (prescaler value) = 125000 Hz = 8us.
  // (desired period) / 8us = 125.
  // MAX(uint8) + 1 - 125 = 131;
  //
  // Save value globally for later reload in ISR  /
  tcnt2 = 131;

  // Finally load end enable the timer  
  TCNT2 = tcnt2;
  TIMSK2 |= (1<<TOIE2);
}


void loop() {

  // read from the heart rate pot (Analog Input 0) 
  Value = analogRead(0);

  // map the Analog Input 0 range (0 .. 1023) to the Bpm range (300 .. 1104)
  Bpm = map(Value, 0, 1023, BpmLow, BpmHigh);

  // To lessen the jitter or bounce in the display's least significant digit,
  // a moving average filter (32 values) will smooth it out.
  BpmValues[Index++] = Bpm;                       // add latest sample to eight element array
  if (Index == 32) {                                              // handle wrap-around
    Index = 0;
  }
  BpmAverage = 0;
  for (int  i = 0;  i < 32; i++) {                             // summation of all values in the array
    BpmAverage += BpmValues[i];
  }
  BpmAverage >>= 5;                                      // Divide by 32 to get average

  // now update the 4-digit display - format: XXX.X
  // since update is a multi-byte transfer, disable interrupts until it's done
  noInterrupts();
  DisplayValue = BpmAverage;
  interrupts(); 

  // given the pot value (beats per minute) read in, calculate the IdlePeriod (msec)
  // this value is used by the Timer2 1.0 msec interrupt service routine
  BeatsPerMinute = (float)Bpm / 10.0;
  noInterrupts();
  IdlePeriod = (unsigned int)((float)60000.0 / BeatsPerMinute) - (float)NumSamples;
  interrupts();

  delay(20);
}






// ********************************************************************************
//                Timer2 Interrupt Service Routine
//
// Interrupt Service Routine (ISR) for Timer2 overflow at 1.000 msec.
//
//
// The Timer2 interrupt function is used to send the 16-bit waveform point
// to the Microchip MCP4921 D/A converter using the SPI interface.
//
// The Timer2 interrupt function is also used to send the current heart rate
// as read from the potentiometer every 50 Timer2 interrupts to the 7-segment display.
//
// The pot is read and the heart rate is calculated in the background loop.
// By running both SPI peripherals at interrupt level, we "serialize" them and avoid
// corruption by one SPI transmission being interrupted by the other.
//
// A state machime is implemented to accomplish this. It's states are:
//
//       INIT -    basically clears the counters and sets the state to QRS.
//
//       QRS  -    outputs the next ECG waveform data point every 1.0 msec
//                       there are 543 of these QRS complex data points.
//
//       IDLE -    variable period after the QRS part.
//                      D/A holds first ECG value (939) for all of the IDLE period.
//                      Idle period varies to allow adjustment of the basic heart rate;
//                      a value of zero msec for the idle period gives 110.4 beats per min
//                      while the maximum idle period of 457 msec gives 30.0 bpm.
//
//                      Note that the IDLE period is calculated in the main background
//                      loop by reading a pot and converting its range to one suitable
//                      for the background period. The interrupt routine reads this
//                      value to determine when to stop the IDLE period.
//
// The transmission of the next data point to the D/A converter via SPI takes
// about 63 microseconds (that includes two SPI byte transmissions).
//
// The transmission of the heart rate digits to the Sparkfun 7-segment display
// takes about 350 usec (it is only transmitted every 50 Timer2 interrupts)
//
// ********************************************************************************
ISR(TIMER2_OVF_vect) {

    // Reload the timer  
    TCNT2 = tcnt2;

    // state machine
    switch (State) {

    case INIT:

          // zero the QRS and IDLE counters
          QRSCount = 0;
          IdleCount = 0;
          DisplayCount = 0;

          // set next state to QRS 
          State = QRS;
    break;

    case QRS:

          // output the next sample in the QRS waveform to the D/A converter
          DTOA_Send(y_data[QRSCount]);

          // advance sample counter and check for end
          QRSCount++;
          if (QRSCount >= NumSamples) {
              // start IDLE period and output first sample to DTOA
              QRSCount = 0;
              DTOA_Send(y_data[0]);
              State = IDLE;
          }
    break;

    case IDLE:

          // since D/A converter will hold the previous value written, all we have
          // to do is determine how long the IDLE period should be.

          // advance idle counter and check for end
          IdleCount++;

          // the IdlePeriod is calculated in the main loop (from a pot)
          if (IdleCount >= IdlePeriod) {
              IdleCount = 0;
              State = QRS;
          } 
    break;

    default:
    break;
    }

    // output to the 7-segment display every 50 msec
    DisplayCount++;
    if (DisplayCount >= 50) {
        DisplayCount = 0;
        Display7Seg_Send(DisplayValue);
     }





// ***************************************************************************************************
//                      void  DTOA_Send(unsigned short)
//
//  Purpose: send 12-bit D/A value to Microchip MCP4921 D/A converter ( 0 .. 4096 )
//
//
//  Input:    DtoAValue - 12-bit D/A value ( 0 .. 4096 )
//
//
//            The DtoAValue is prepended with the A/B, BUF, GA, and SHDN bits before transmission.
//
//                                           WRITE COMMAND
//            |-----------|-----------|-----------|-------------|--------------------------------------------------------------------------------|
//            |  A/B       |   BUF   |  GA        | SHDN     | D11 D10 D09 D08 D07 D06 D05 D04 D03 D02 D01 D00  |
//            |               |              |               |                  |                                                                                                          |
//            |setting:  |setting :|setting: |Setting:    |                                    DtoAValue   (12 bits)                                 |
//            |   0          |   0          |   1        |   1            |                                                                                                           |
//            | DAC-A  |unbuffer |   1x      |power-on|              ( 0 .. 4096  will output as 0 volts .. 5 volts )                  |
//            |-----------|------------|----------|-------------|--------------------------------------------------------------------------------|
//                15             14            13            12       11                                                                                                    0
//  To D/A    <======================================================================================
//
//            Note:  WriteCommand is clocked out with bit 15 first!
//
//
//  Returns:  nothing
//
//
//  I/O Resources:  Digital Pin 9  = chip select (low to select chip)
//                               Digital Pin 13 = SPI Clock
//                               Digital Pin 11 = SPI Data
//
//  Note: by grounding the LDAC* pin in the hardware hook-up, the SPI data will be clocked into the
//             D/A converter latches when the chip select rises at the end-of-transfer.
//
//             This routine takes 63 usec using an Adafruit Menta
// ***************************************************************************************************
void  DTOA_Send(unsigned short DtoAValue) {

  byte            Data = 0;

  // select the D/A chip (low)
  digitalWrite(10, 0);    // chip select low

  // send the high byte first 0011xxxx
  Data = highByte(DtoAValue);
  Data = 0b00001111 & Data;
  Data = 0b00110000 | Data;
  SPI.transfer(Data);

  // send the low byte next xxxxxxxx
  Data = lowByte(DtoAValue);
  SPI.transfer(Data);

  // all done, de-select the chip (this updates the D/A with the new value)
  digitalWrite(10, 1);    // chip select high
}





// ***************************************************************************************************
//                      void  Display7Seg_Send(char *)
//
//  Purpose: send 4 digits to SparkFun Serial 7-Segment Display (requires 4 SPI writes)
//
//  Input:    value -  unsigned int version of BeatsPerMinute
//
//  Returns:  nothing
//
//  I/O Resources:  Digital Pin 10 = chip select (low to select chip)
//                               Digital Pin 13 = SPI Clock
//                               Digital Pin 11 = SPI Data
//
//  Note:  this routine takes 350 usec using an Adafruit Menta
// ***************************************************************************************************
void  Display7Seg_Send(unsigned int  HeartRate) {
  uint8_t       digit1, digit2, digit3, digit4;
  unsigned int  value;

  // convert to four digits (set leading zeros to blanks; 0x78 is the blank character)
  value = HeartRate;
  digit1 = value / 1000;
  value -= digit1 * 1000;
  if (digit1 == 0) digit1 = 0x78;

  digit2 = value / 100;
  value -= digit2 * 100;
  if ((digit1 == 0x78) && (digit2 == 0)) digit2 = 0x78;

  digit3 = value / 10;
  value -= digit3 * 10;
  if ((digit1 == 0x78) && (digit2 == 0x78) && (digit3 == 0)) digit3 = 0x78;

  digit4 = value;

  digitalWrite(9, LOW);    // select the Sparkfun 7-seg display
  SPI.transfer(0x76);      // reset display
  SPI.transfer(0x7A);      // brightness command
  SPI.transfer(0x00);      // 0 = bright,  255 = dim
  SPI.transfer(digit1);    // Thousands Digit
  SPI.transfer(digit2);    // Hundreds Digit
  SPI.transfer(digit3);    // Tens Digit
  SPI.transfer(digit4);    // Ones Digit
  SPI.transfer(0x77);      // set decimal points command
  SPI.transfer(0x04);      // turn on dec pt between digits 3 and 4
  digitalWrite(9, HIGH);   // release Sparkfun 7-seg display
}

Step 25: Test Results

To test the ECG simulator, the Texas Instruments ADS1293EVM evaluation board was connected to the Menta ECG Simulator via the banana jacks and the supplied TI Windows software was used to look at the received signal on my desktop computer (I'm running Windows 8). The test setup is shown in the first figure below.

In the interest of honest reporting, heart monitors employ a lot of filtering to clean up the ECG signal. I set up the TI software digital filters to do the same thing. The second figure below shows the Texas Instruments software displaying the ECG signal from the simulator. The second channel in the TI software was not connected with this test setup.

The third figure shows the original ECG signal screen-captured from the Army document. As you can see, the generated ECG signal is a close match to the original.

Note that the signal is just a couple of millivolts. With the Rate Adjustment pot, you can vary the rate from 30 bpm to 110 bpm.

Step 26: Conclusions

My goal with this project was to design and fabricate a 3-lead ECG simulator that would allow me to study the Texas Instruments ADS1293 ECG Front-end chip. Using the Adafruit Menta kit as a starting point, the simulator was built for just over $60 in parts. There was no intention to manufacture this device, it was essentially a “one-off” design.

The software effort was a bit more complicated since I decided to start from a page in an Army medical document on the Internet. The sample waveform was screen-captured using MWSnap, digitized by the Sourceforge Enguage tool, and then manipulated by a custom Python program to create a C language array structure with initializer that can be “pasted” into the Arduino sketch.

The Arduino sketch is modestly complicated, using a Timer2 interrupt to precisely time the waveform samples at one millisecond intervals and some SPI driver code to update the D/A converter and the 4-digit display.

There are two conclusions: the 8-bit Arduino micro-controller is a perfect fit for this application and the Adafruit Menta is a wonderful platform for building things.

It bears repeating that the strategies I outlined in this tutorial to generate the ECG waveform can be utilized to create any waveform that you might see in a book or on the Internet.

Step 27: About the Author

Jim Lynch lives in Grand Island, New York and is a software developer for Control Techniques, a subsidiary of Emerson Electric. 
He develops embedded software for the company’s industrial drives (high power motor controllers) which are sold all over the world.

Mr. Lynch has previously worked for Mennen Medical, Calspan Corporation, and the Boeing Company. He has a BSEE from Ohio
University and a MSEE from State University of New York at Buffalo.

Jim is a single father and has two grown children and four grandchildren who now live in Florida and Nevada.

He has two brothers, one is a Viet Nam veteran in Hollywood, Florida and the other is the Bishop of St. Petersburg, also in Florida. Jim enjoys playing the guitar, woodworking, and going to the movies.

Lynch can be reached via e-mail at:    lynch007@gmail.com