This instructable is intended as a guid to create an Embeddded ECG data acquisition board, providing some background knowledge on digital signal processing and system identification in Matlab, FPGA programming in Vivado, High level synthesis of files,printed circuit design, embedding a small Linux in the ZYNQ ARM processor, and an Android program for signal visualization.

My objective is to create a portable, affordable,
and cheap ECG data acquisition device for the family medicine, which could be supervised by specialized personnel (if the case arises).

Step 1: State of Art

In order to formulate the
device requirements first I researched companies and their products in the field of mobile ECG devices. Not surprisingly the first result was on eBay:

  • Heal Force Portable Handheld Color ECG EKG Heart
    Monitor: The device has 3 electrodes, High Resolution LCD, it is battery powered, measures a single lead of ECG signal, and it’s capable of recording 30 seconds of ECG data, while the price is 126$

  • Omron HeartScan Portable ECG Machine: This device has incorporated electrodes, monochromatic LCD, can measure a single ECG lead, and store 300 x 30 s of data with rhythm strips. The price is 500$.

  • Nasiff CardioResting™ ECG System: This device is also from Medical Device Depot, but differs from the upper mentioned model. The device connects to a PC with USB connection. Other technical details are the same as the details of the upper mentioned model. The price is 1895$.

  • Welch Allyn CP 300 CardioLaptop ECG Machine: While this device from Medical Device Depot is not the most portable one could find, it is an FAA class I ECG device, with full color display and noninterpretive, 12-lead measurement capabilities, 40 GB of data storage, frequency domain of 0.05 to 150 Hz, and contains a printer for the measured data. The price of the device is 8300$.

Step 2: Material Gathering

  • The main board is a ZYBO dev board from Digilent, which contains a ZYNQ7010 SoC with Dual core ARM processor
  • A fast SD card for storing Linux and temporarily saving ECG data files
  • A precision data acquision board for biopotential measurements
  • RTC circuit for accurate time labling of files even without internet
  • Pmod Cables to connect misc. boards to the ZYBO dev board.
  • Battery (and charger) for a supply coupled from mains (noise and safety reasons) - For testing any small battery will be good, but the final product needs a good battery based on Power consumption calculations, and customized charger circuit.
  • ECG cables or other shielded cables to connect the electrodes to the device ( ECG cables are preffered due to the specific header and quality shielding)
  • ECG electrodes ( a lot of them) If someone uses Ag/AgCl electrodes, one needs a bunch of them since they are for single use ( 3 -4 short usage when testing). They have the annoying properties of degradationthe of adhesives, and the drying of the electrolyte. If one uses solid state electrodes (gold or gold plated) with proper skin preparation they can be used indefinitely

Step 3: The Design of the Analog DAQ and Supply Board

Since I had the ZYBO dev board, I had two options for DAQ:

  1. I could've used the on-board XADC with an external analog signal conditioning board. The advantages: with the instrumentation amplifier and opamps I could've integrated analog filters in the circuit filtering out the noise before amplification. The disadvantage was the resolution of the ADC: 12bits, which is a minimum in biopotential measurements.
  2. I could've used an external Analog Frontend chip which is designed for biopotential measurements. This is the path I took. I've chosen the ADS1299 Analog Frontend IC from Texas Instruments, which has circuit protection, 8 programmable gain amplifiers (x1 - x24), 8 delta-sigma modulated ADC-s with 24bit data resolution, and sampling frequency from 250SPS to 16kSPS.

The board design is the minimal setup based on the recommandation in the datasheet.

Since the ZYBO board is supplied with 5V DC, the ADS needs +2.5V, -2.5V and 3.3V, but my batterys output voltage was 4.2V when fully charged, power converter circuit was necessary for the supply. The power efficiency is a key factor in battery operated circuits, so the use of DCDC converters was inevitable.

The first step in a power supply design was the polarity protection. This could be done in a series diode, which would let current flow only if the polarity is rigth, but the diode has a small amount of voltage drop. Another possibility would be with an anti-parallel diode, and fuse combination, but this requires 2 components. Yet another possibility is using a transistor as low- or high-side switch. I've chosen the MOS-FET based high-side switch with low on resistance transistor. The high-side switch was used because when off, there is no voltage in the circuit.

The battery voltage was Boosted to 5V with a Boost DCDC converter. This supply was used to power the Soc board, and for generating the positive analog suply. The analog supply was generated with a LDO linear power supply. The negative supply was created with an inverting Buck-Boost converter. Both DCDC converters were designed based on the MC33083 driver ICs datasheets recommendation.

While using switching mode power supplies one must be cautious of the generated noise, especially in a signal acquisition circuit. I made sure the noise was on an acceptable level by designing the converters to work on 50kHz switching frequency, while the sampling frequency of my ADC will be less or equal to 2kHz. Even though we can be certain the circuit will work correctly based on the Nyquist criteria, I measured the AC to DC ratio, and it was under 0.02%.

The 3.3V supply was used from the ZYBOs PMOD supplys.

With these considerations in mind the DAQ board was designed in EAGLE cad,

Step 4: Designing the Real-time Clock

In order to have an accurate date on every data attached to the database, the date must be constantly updated. This can be done in 3 different ways:

  • Constantly running the system after an initial date and time setting – This is the easiest way, no external hardware or algorithm is needed, but greatly reduces the measurement time due to power consumption.
  • · Using external RTC circuit – A simple low power consumption circuit with its own battery. On each power-up the system checks the date and time on the RTC circuit, and updates the system time.
  • · Using the internet to poll the actual time – Since the ZYBO board already has Ethernet port, this method wouldn’t require additional hardware, but a complex algorithm or OS should be used for time polling.

From the upper mentioned methods, I used the second one, since it requires only minimal hardware and software tweaking. The chosen RTC circuit was MCP7940N from Microchip.

Step 5: Signal Processing Theory

The signals recorded from the human body are contaminated by noise. The noise contains low frequency components like muscle movement noise (EMG), baseline wandering (from the slow changing of the skin and electrode properties), but high frequency noises too, like the 50Hz mains noise, RF picked up by the cables etc.

To remove these noises, filters must be used in the circuit. The filters can be realized with hardware – has the advantage of filtering out the noise before the amplification stage, or in software – FIR filters can be applied with linear phase response, to cancel distortion.

In ECG measurement specification a 0.05 Hz to 200 or 250Hz band is advised with mains frequency removed or damped. I didn’t implement the high-pass filter, because the baseline wandering and other low frequency noises can be removed with an on-line polynomial fitting algorithm.

The used low-pass filter was a 64thorder Taylor windowed FIR filter with 1kHz sampling frequency.

For the band stop filter I chose an IIR filter, because at lower order it can achieve the same results as a high order FIR filter.

Both of these filters were designed in Matlab, with FDA tool.

Step 6: Internal Hardware Design

In this step I focused on the hardware design in the Zynq SoC.

  1. A new design is created with the hardware present on the board ( XC7Z010CLG400-1).
  2. A ZYNQ ARM processing system is used as the main device ( Alternatively MicroBlaze can be used in case of FPGAs - but this will be covered in another instructable). The base configuration can be downloaded from the Resource Center
  3. Some customization must be done to my device: The BL2 module will be connected to a PMOD connector so an UART peripheral must be mapped to EMIO pins. The ADS1299 communicates on SPI protocol, so the SPI is outrouted also. An I2C peripheral is needed for the serial EEPROM with MAC address. We need to enable the Timers and the watchDog Timer in the ARM core. Last but not least the Global Interrupt Controller must be edited by enabling The shared Interrupt ports of the PL-PS interrupt fabric.
  4. The SD card peripheral must be enabled, and the write protect bit tied to 0.
  5. Now the EMIO pins must be given some port numbers. This can be done by adding a constraint file. With the Edit Constraints Set and Add Constraint options I could create the required XDC file. The ZYBO pin deffinitions can be found in the reference manual.

The XDC file has the following syntax:

set_property PACKAGE_PIN <external> [get_ports {<internal>}]<br>set_property IOSTANDARD LVCMOS33 [get_ports {<internal>}]

The first row maps an internal pin (like UART_txd) to an external pin (like W14),while the second row sets the standards of that pin.

For now I added only a FIR compiler IP for testing purposes, but the filter will be replaced later on with IIR + FIR versions made in HLS.

In order to work with the FIR Compiler IP, an AXI Stream block must be added to the system with matching or larger data width ports. The FIR coefficients were saved from the FDA tool to a .coe file in hexadecimal, 16bit signed integers.

With this we made a hardware ready for the inicial test of the bare-bone Core containing the acquisition software, and for the Core with OS on it. The only step to make is to generate a bitstream and import the design to SDK.

Step 7: Creating the Bare-bone Data Acquisition Software - Part1: the SPI

The data acquisition and signal digital signal conditioning both are time critical tasks, so their implementation must be done in a real-time operating system or in a fast automaton/micro controler. The first approach would be the usage of Linux with Xenomai distribution, while the second approach would be writing a firmware to the micro controler. Since I had more experience with hardwares/firmwares , I've chosen the second method. Since the embedded ARM processor has two physical cores, I wanted to create a fast firmware to one of the cores, and an operating system to the other core.

The first thing to do is the configuration of the SPI peripheral, and the ADS1299 analog front-end. The SPI pripheral was configured as the EEPROM example suggested in SDK, with some minor modifications:

XSpiPs_SetClkPrescaler(SpiInstancePtr, XSPIPS_CLK_PRESCALE_256);

The upper mentioned snipet was moddified to slow down the SPI clock below 1MHz. For data transmission the polled transfer function was used.

The configuration sequence is as follows: RESET, STOP_DATA_TRANSMISSION, WRITE_DATA_REGISTERS, ENABLE_DATA_TRANSMISSION, START_ACQUISITION. This sequence is a must when configuring the ADS1299, since after reset, the IC automatically starts to transmit measured data, and won't save register changes. In the setup I disabled 5 chanels ( I intend to use only 3 + ground), changed the reference to common, the sampling frequency to 1kHz, and gain to 24.

With these settings made, the only thing to do is to test the communication, except there could be some small problems: if the battery is low, but the digital part is powerered from USB, the commucation can be made, but the acquisition won't work. Another problem could be setting the SPI parameters correctly. Both of these problems can be solved with the help of an oscilloscope and logic analyzer.

Step 8: Creating the Bare-bone Data Acquisition Software - Part2: Data Interrupt and Transmission

If the previsios step was successful, on the nDRDY pin of the ADS1299 a pulse with 1kHz frequency should be present - this is the acquired data ready signal.

So a GPIO with interrupt must be used. Fortunately the SDK has an example to this too. The only moddification is in the ISR handler function.

if(!XGpio_DiscreteRead(&DRDYinstance, 1)){
        XSpiPs_SetSlaveSelect(&SpiInstance, ADS_SPI_SELECT);
        XSpiPs_PolledTransfer(&SpiInstance, NULL, inputData, 27);



The upper mentioned code snipet shows the small change. In the interrupt handler we check the state of the GPIO. If it's LOW, we have a valid data, so we read it. The data is in the following order: OP CH1 CH2 CH3 ... CH8. The OP contains some options like lead-off detection, while CHn is the data on the n chanel ( 24b of data).

For transmission, the UART peripheral is used Since the Start condition occures when the user starts the acquisition from the phone, an interrupt must be used on the receiver end. My setting for the Bluetooth UART communication was 230400 Baud/s speed with the following protocol:

  • 250 p p ... p 255 is the patient protocol, here we start sending information like name, birth date etc.

  • 251 ... 255 is the end of the patient data protocol.

  • 252 p p ... p 255 is the start protocol, with this the data acquisition is started - here the p bits will mean sampling settings like, one shot with defined interval, one shot indefinitely, sampling frequency etc.
  • 253 p p ... p 255 is the stop protocol
  • 254 d d .... d 254 is the ECG data protocol

So far a packet of 16B length is used : START CH_NUM(2) CHi(3) CHj(3) CHk(3) CHl(3) STOP, where 2 bytes are used for data identification, and 4 24b separate data is sent to the android device.

After some observations the conclusion was to reduce the packet size, the display on these android devices is so small, a change of 8b in data is tolerable, so with 16b of sampled data, the final packet size is 11B, since the Chanel number can also be resized. This CH_NUM byte will be helpful when using all 8 chanels of ADC, but only transmiting 4.

Step 9: Analog Filter Stage

After doing some tests on the system, I came to the conclusion that the noise level is greater than the signal level. Using only the amplification and the software filter would only cut out a small part of the noise, but it would also cut out some of the useful data.

The solution to this problem is to use an analog filter before the PGA. As we saw in the previous steps, two filters must be implemented, a 50Hz bandstop filter for the mains noise, and a 250Hz lowpass filter for the high frequency noise (and anti-aliasing).

    The Bandstop filter design:

    A second order bandstop filter was designed based on the filter transfer function Hbs = H0(s^2 + w^2)/(s^2 + (w/Q)s + w^2), w = 2*pi*fc, and Q = fc/(fb), where fc is the center frequency, fb is the stopband width, H0 is the gain.

    Since the purpose of the filter is to cancel out the noise, and not the amplification, H0 = 1, fc = 50Hz, fb = 2Hz. For the implementation a modified Bainter-topology was used as shown in the second picture. The Bainter-topology has the advantage of matching independence ( the Q factor of the filter only depends on the gain, not the resistor matching opposed to the Sallen-key).

    The Lowpass filter design:

    A forth order lowpass filter was designed with two second order filter stages: Hlp = H0*w^2/(s^2 + 2*e*w*s + w^2), w = 2*pi*fc, where fc is the cut-off frequency and e is the damping ratio. I've chosen a filter with H0 = 1, fc = 250Hz, e = 1 parameters. The chosen filter type was the multiple feedback topology (MFT). The MFT LP filter is prefered, because it has high Q factor, and narrow pass-band relative to the Sallen-key topology. The only drawback would be the inverted output signal, but I used 2 stages, so this wasn't an issue.

    A swept sine wave input was used to test the overall behavior of the filter (last picture).

    Step 10: Creating an Android Bluetooth Data Plotter

    To create the data plotter program you can either use Eclipse or the new Android Studio ( I went with the later option). After creating a new project, we need to add bluetooth handling permissions, this can be done in the Android manifest permissions, where we need the BLUETOOTH and the BLUETOOTH_ADMIN permissions (figure 1).

    Now we need to create an Xml file in layout to contain the discovered bluetooth devices (as shown in figure 2), incorporating a textView for the title and a listView for the device list.

    Next we need to add a class to use the Bluetooth module. This class needs to extend activity and implement OnItemClickListener. This is used to choose our data acquisition device by clicking on the displayed text.

    For the Bluetooth connection thread we need to copy the example code from this link (ConnectThread and ConnectedThread). I added sleep inside ConnectThreads run method, to ease the resource usage.

    The write method must be altered to receive the bytes from our device as shown in figure 3.

    Now it's time to import the used libs (if the libs weren't imported previsiously), and to clean the project.

    A Broadcast receiver method must be implemented to filter and display devices (figure 4).

    OnPause, OnActivityResult, OnItemClick listener must be implemented to choose the device. A disconnect method must be implemented to disconnect the bluetooth device.

    Step 11: Using the Filters From HLS And/or the FIR Compiler

    Since I had prior experience with filter construction in C/C++, I chose the Vivado HLS program to implement the filters.

    The filter function should have a header as follows:

     void function (<data_type>* input, <data_type> *output, <data_type> *coeffs)

    First let's have a look at the function arguments. What data type should we choose? If this would be part of a PC program, then data types wouldn't be much of a concern (we could choose double or float), but we want to synthesize to hardware, so data width IS a concern. The ADS1299 datasheet specifies the exact data type: signed 24b data (2s complement), so it would be a waste to use 64 or 32b floating point data.

    Fortunately HLS has a header just for this job - "ap_int.h" contains custom width integer and fixed point data types.

    Now to the last part, the coefficients. The coefficients were generated in Matlab, and will be used in HLS. If the coefficients are specified only once, we can specify them as constants at the start of the function. If we want to change the coefficients we can leave in the arguments, but the storage must be a dual port RAM/ROM type since the optimisation requires it.

    The first digital filter will be a 65 point FIR filter.

    const int firlen = 65;
    const int dWidth = 24;
    typedef ap_fixed<dWidth + 4, 4> fir_t;<br>

    The fir_t type will be used for fixed point data type. We need a storage register for the input data, a temporal register, and a storage register for the sum of products.

    fir_t xStore[firlen];
    ap_int<dWidth + 4>tempX;
    ap_fixed<dWidth*2,dWidth-8> sum;<br>

    The sum register must have a size that can handle 65 24b*24b values sumed.

    We need to define the filter coefficients as ( the values can be copied from Matlab):

    fir_t coeffB[firlen] = {...};

    In an infinite loop we store the input values, convert them, and proceed to the MAC operation.

    	for(int i = 0; i < firlen - 1; ++i) 
    		xStore[i] = xStore[i+1];
            temp = (ap_int<dwidth +="" 4="">) *(x++);<br></dwidth>	xStore[firlen-1](dWidth + 3,0) = temp(dWidth + 3,0);
    	sum =  0; 
    	for(int i=0; i < firlen; ++i)
                sum += coeffB[i]*xStore[(firlen - 1) -i];

    Last but not least we need to convert the fixed point value to signed integer in the loop.

    temp = fir_t(sum).range(dWidth -1,0);
    *(y++) = (ap_int<dwidth>) temp;</dwidth>

    Now it's time for C synthesis. We can see (Figure 1) that just a small amount was used from the resources, and the estimated timing is 15.33ns. We can optimise the filter by pipelining the storage and MAC loops with

    #pragma HLS PIPELINE II=1

    Similarly, the IIR filter can be made.

    This time the process is as follows:

    1. Shift the input registers and store the next value.
    2. Write the MAC operations ( for the input and output registers).
    3. shift the output register, and store the output.
    4. return the output.

    The last picture shows the synthesiyed structure after pipelining the filter.

    Step 12: Building Linux for the ZYBO Dev Board

    On a Linux OS some files are needed for the embedded operating system: a device tree folder

    u-boot Linux kernel .

    The unzipped folder needs to be copied to the SDK directory. In Xilinx Tools/Device Repositories add the location of the device tree folder. Next step is to create the device tree files. Under File/New create nem Board Support Package and choose the device-tree option. Note the core name. It doesn’t matter which core you use, but use this core for all the projects (the ARM in ZYNQ is a dual core MPU). From
    the generated files copy the pl.dtsi, skeleton.dtsi, zynq-7000.dtsi and system.dts files to the Linux platform. Open up the system.dts file and replace the &gem part with:

    &gem0 { 
    	local-mac-address = [00 0a 35 00 00 00]; 
    	phy-handle = <&phy0>; 
    	phy-mode = "rgmii-id";  
    	status = "okay";
    	xlnx,ptp-enet-clock = <0x6750918>;
    	ps7_ethernet_0_mdio: mdio { #address-cells = <1>; 
    	#size-cells= <0>;
      	phy0: phy@0 { compatible = "realtek,rtl8211e"; 
    	device_type = "ethernet-phy"; 
    	reg = <0>;};};};

    This modification configures the Ethernet PHY of the device. On the Linux device I used ./scripts/dtc/dtc -I dts -O dtb -o .dtb .dts command which created the system.dtb file. The FSBL for the device tree: I created a new Application project from File/New. Select Zynq FSBL as the template and build the project.

    In order to create the boot Image file, I need to build the Xilinx uBoot file.

    Running make zynq_zybo_config will create the .config file, and running make will build the output in the src
    folder. In SDK go to Xilinx Tools/Create Zynq Boot Image. The FSBL.elf must be added as bootloader file, the design_wrapper.bit as data. uBoot file should be renamed to uBoot.elf and added to the files. The order of the files is importan!

    To build Linux, the folowing code sequence must be executed:

    make ARCH=arm mrproper 
    make ARCH=arm xilinx_zynq_defconfig 
    make ARCH=arm

    Now I made a zipped image of the kernel - zImage, but the unzipped version is needed, so I needed to run the following command:

    make ARCH=arm UIMAGE_LOADADDR=0x8000 uImage.

    In Linux I need to build a ram disk image by executing the command:

    mkimage –A arm –T
    ramdisk –c gzip –g ./ramdisk8M.image.gz uramdisk.image.gz

    The last step is to copy all files to an SD card: the files needed are: BOOT.bin generated from the FSBL process, devicetree.dtb, uImage – without the elf extension, and the uramdisk file, all of these files will be copied to a small FAT32 partition on the SD card, an ext4 partition is needed for the file storage system ( if this partition is not used, the Linux will work only on the RAM, so for this partition I used an ARCH Linux file system).

    Step 13: Combining the OS and Bare-bone Firmware


    ** Since the bootloader only loads one firmware to the Core, I need to modify the ELF file, to have Linux and bare-bone Core at the same time **

    Step 14: Applications Under OS


    ** Here I will write a simple program which uploads the saved .edb file to a cloud server **

    So far a video was made to demonstrate the state of the project on Youtube, the next steps are : developing the combined OS/firmware architecture, and complete the documentation

    <p>Great project idea.</p>

    About This Instructable




    More by ÁronF1:Embedded ECG data acquisition system 
    Add instructable to: