SPI Interface with AFE 4490 for Heart Rate Measurement

This project is to examine how to use SPI to interface with AFE4490 Integrated Analog Front-End Pulse Oximeter [1] that I used to measure my heart pulse rate. The interface was developed on iMX RT1060, an NXP ARM cortex M7 MCU, using this NXP development board: iMX RT1060 evk.

Before, we look at how the SPI driver is developed, let’s have a look at the serial timing diagram for read and write. More info about how an SPI protocol works can be found at my previous post at this link.

SPI Serial Timings for Read and Write

The timings below, taken from the IC datasheet [1], describes how to read any of the oximeter registers. In order to read a register, the SPI master sends 4 byte of data. The first byte is what we care about, and the other three bytes can be set to 0  as they are just dummy bytes. In the first byte,  we set the MSB bit to 1, and the rest of bits are used for the register address. The slave always reply with a 24 bits data packet (Three Bytes.)

AFE4490Figure1. Serial interface for SPI Reading from AFE4490 [1]

When we need to write to any of the writable registers of the AFE, we need to set the MSB bit of the first byte to 0, followed by the register address, and the next three bytes are used for the data we need to write. Also, remember here that each transaction for read/write is 32 bits. Thus, the chip select needs to be low for  a data frame of 32 bits.

AFE4490WrtiteFigure2. Serial interface for SPI Writing to AFE4490 [1]

SPI Configuration

The first step is to configure the SPI signals pins for SPICLK, MISO, MOSI, and CS. The function below shows you how to do that. I just want to mention that iMXRT1060 has 4 SPI controllers, and I am using SPI1 in this project.

void BOARD_InitPins(void)
{
CLOCK_EnableClock(kCLOCK_Iomuxc);     /* iomuxc clock (iomuxc_clk_enable): 0x03U */
CLOCK_EnableClock(kCLOCK_IomuxcSnvs); /* iomuxc_snvs clock (iomuxc_snvs_clk_enable): 0x03U */

IOMUXC_SetPinMux(IOMUXC_GPIO_SD_B0_00_LPSPI1_SCK, 0U); /* GPIO_SD_B0_00 is configured as LPSPI1_SCK */
                                                       /* Software Input On Field: Input Path is determined by functionality */

IOMUXC_SetPinMux(IOMUXC_GPIO_SD_B0_01_LPSPI1_PCS0,0U); /* GPIO_SD_B0_01 is configured as LPSPI1_PCS0 */
                                                       /* Software Input On Field: Input Path is determined by functionality */

IOMUXC_SetPinMux(IOMUXC_GPIO_SD_B0_02_LPSPI1_SDO, 0U);  /* GPIO_SD_B0_02 is configured as LPSPI1_SDO */
                                                        /* Software Input On Field: Input Path is determined by functionality */

IOMUXC_SetPinMux( IOMUXC_GPIO_SD_B0_03_LPSPI1_SDI, 0U); /* GPIO_SD_B0_03 is configured as LPSPI1_SDI */
                                                        /* Software Input On Field: Input Path is determined by functionality */

IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_00_LPSPI1_SCK, 0x10B0u);  /* GPIO_SD_B0_00 PAD functional properties : */
	                                                        /* Slew Rate Field: Slow Slew Rate
	                                                        Drive Strength Field: R0/6
	                                                        Speed Field: medium(100MHz)
	                                                        Open Drain Enable Field: Open Drain Disabled
	                                                        Pull / Keep Enable Field: Pull/Keeper Enabled
	                                                        Pull / Keep Select Field: Keeper
	                                                        Pull Up / Down Config. Field: 100K Ohm Pull Down
	                                                        Hyst. Enable Field: Hysteresis Disabled */

IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_01_LPSPI1_PCS0,x10B0u);  /* GPIO_SD_B0_01 PAD functional properties : */
	                                                       /* Slew Rate Field: Slow Slew Rate
	                                                       Drive Strength Field: R0/6
	                                                       Speed Field: medium(100MHz)
	                                                       Open Drain Enable Field: Open Drain Disabled
	                                                       Pull / Keep Enable Field: Pull/Keeper Enabled
	                                                       Pull / Keep Select Field: Keeper
	                                                       Pull Up / Down Config. Field: 100K Ohm Pull Down
	                                                       Hyst. Enable Field: Hysteresis Disabled */
	 
IOMUXC_SetPinConfig( IOMUXC_GPIO_SD_B0_02_LPSPI1_SDO, 0x10B0u); /* GPIO_SD_B0_02 PAD functional properties : */	                                                          
	                                                        Speed Field: medium(100MHz)
	                                                        Open Drain Enable Field: Open Drain Disabled
	                                                        Pull / Keep Enable Field: Pull/Keeper Enabled
	                                                        Pull / Keep Select Field: Keeper
	                                                        Pull Up / Down Config. Field: 100K Ohm Pull Down
	                                                        Hyst. Enable Field: Hysteresis Disabled */
	 
IOMUXC_SetPinConfig(IOMUXC_GPIO_SD_B0_03_LPSPI1_SDI,0x10B0u);   /* GPIO_SD_B0_03 PAD functional properties : */
	                                                        /* Slew Rate Field: Slow Slew Rate
	                                                        Drive Strength Field: R0/6
	                                                        Speed Field: medium(100MHz)
	                                                        Open Drain Enable Field: Open Drain Disabled
	                                                        Pull / Keep Enable Field: Pull/Keeper Enabled
	                                                        Pull / Keep Select Field: Keeper
	                                                        Pull Up / Down Config. Field: 100K Ohm Pull Down
	                                                        Hyst. Enable Field: Hysteresis Disabled */
}

Next we configure the SPI clock frequency, clock polartiy COPL, clock phase CPHA, the order we are sending data: MSB bit first or LSB first, how many bit we are sending per frame,  time between chip select falling edge and first SPI clock rising edge (STE low to SCLKrisingedge), and last SPI clock to chip select delay (SCLKtransitionto SPI STEhigh or low in datasheet.)

All the parameters mentioned above are found and taken from the datasheet[1]. In the case of AFE4490, the COPL=0 (which means clock is low when idle);  and CPHA =0 (which means is sampled on the rising edge of the clock.).

Just to mention that iMXRT1060 has 4 SPI controllers, and I am using SPI1. The function below is used to initialize the master SPI.

#define LPSPI_1_PERIPHERAL LPSPI1
#define LPSPI_1_CLOCK_FREQ 105600000UL

void LPSPI_1_init(void) {
LPSPI_MasterInit(LPSPI_1_PERIPHERAL, &LPSPI_1_config, LPSPI_1_CLOCK_FREQ);
}

The SPI configuration structure is defined as below:

const lpspi_master_config_t LPSPI_1_config = {
 .baudRate = 1000000,   //1MHz SPI clock frequency
 .bitsPerFrame = 32,    //sending 32 bits per frame
 .cpol = kLPSPI_ClockPolarityActiveLow, //when idle clock is low
 .cpha = kLPSPI_ClockPhaseFirstEdge,    //data is sampled on first clock edge.
 .direction = kLPSPI_MsbFirst,          // Msb bits are sent first
 .pcsToSckDelayInNanoSec = 1000,        //STE low to SCLKrisingedge,setuptime
 .lastSckToPcsDelayInNanoSec = 1000,    //SCLK transition to SPI STE high or low
 .betweenTransferDelayInNanoSec = 1000,
 .whichPcs =kLPSPI_Pcs0,                       //chip select CS0 (SPI1)
 .pcsActiveHighOrLow = kLPSPI_PcsActiveLow,    // CS is active low
 .pinCfg = kLPSPI_SdiInSdoOut,
 .dataOutConfig = kLpspiDataOutRetained
}

SPI Interface with ATE4490

The function below is used to write to all writable Oximeter IC registers. I used to change the default registers values.

The first four bytes to transmit are saved in xBuffer[0] .Only the MSB byte here has the useful info ( which is in bit 7), and the three bytes are don’t care. Remember that bit 7 of MSB byte needs to be 0 when writing to a register and 1 when reading from a register.

The MSB byte of xBuffer[1] holds the  the register we need to write to address, and the other three bytes hold the data we need to write to the register. The data is parsed like this in xBuffer[1]: xBuffer[1]= ((address& 0xFF) << 24) | (((data >>16) & 0xFF) << 16) | (((data >> 8) & 0xFF) << 8 ) | ((data & 0xFF)<< 0 )

 

#define LPSPI1_MASTER_BASEADDR (LPSPI1)

/*
* This function writes to AFE4490 Registers.
* Returns: Nothing
*/
void AFE4490Write (uint8_t address, uint32_t data)
{
/* last element in the structure is set to 0 when using LPSPI1. */ 
lpspi_transfer_t Data2TX= {0,0,0,0};   
status_t status;
uint8_t BuffSize=8;                    /*transmitting 8 bytes */
uint32_t TxBuffer[2]={0x0000,0x0000};  /*Tx buffer for 8 bytes */

/*The first byte to transmit is xBuffer[0], and its MSB bit is 0 , while the
other bit are don't care */
TxBuffer[1]= ((address& 0xFF) << 24) | (((data >>16) & 0xFF) << 16) | (((data >> 8) & 0xFF) << 8 ) | ((data & 0xFF)<< 0 );

Data2TX.txData=(uint8_t*)&TxBuffer;
Data2TX.dataSize=BuffSize;
status= LPSPI_MasterTransferBlocking(LPSPI1_MASTER_BASEADDR,&Data2TX);
}

In order to read from a register, we need to send two packets of data, each of which is 32 bits. The first byte of the first 32 bits is a write command which is activated by setting the first received bit in MOSI to 1. In the case, TxBuffer[0]=0x0001.  The second packet of data holds the register  address, which is the MSB byte of TxBuffer[1].

/*!
* @brief AFE4490 Registers read.
* Returns: Nothing
*/
void AFE4490Read (uint8_t address, uint32_t *RxData)
{

lpspi_transfer_t Data2TX= {0,0,0,0};
status_t status;
uint8_t BuffSize=4;  /* transmitting 4 bytes at a time */
uint8_t i=0;

uint32_t TxBuffer[2]={0x0001,0x0000};
uint32_t RxBuffer=0;

/* First 4 bytes to transmit are for read command, where MSB bit
 on MOSI is set to 1 */
Data2TX.txData=(uint8_t*)&TxBuffer; /* First 4 bytes to transmit are for read command, where MSB bit
                                     on MOSI is set to 1 */
Data2TX.rxData=NULL;                /*rxData is NUll since we are not expecting any rely yet */

Data2TX.dataSize=BuffSize;
status= LPSPI_MasterTransferBlocking(LPSPI1_MASTER_BASEADDR,&Data2TX);

/* now we transmit the next 4 bytes that contains the register address */
TxBuffer[1]=address<<24;          /* MSB byte of TxBuffer[1] contains register
                                  address we need to write to */
Data2TX.txData=(uint8_t*)&TxBuffer[1];
Data2TX.rxData=(uint8_t*)&RxBuffer;
Data2TX.dataSize=BuffSize;
status= LPSPI_MasterTransferBlocking(LPSPI1_MASTER_BASEADDR,&Data2TX);

RxBuffer=RxBuffer&(0x00FFFFFF);   /*Mask MSB Byte as data received is only 24 bits wide */

*RxData=RxBuffer;
}

The function below is used to read the LED1 and LED2 pulses amplitudes.

/*!
* @brief Read and Process the data for AFE4490
* Returns: Nothing
*/
void ReadnProcessAFE4490Data(void)
{
static uint16_t i=0;

uint32_t RxData1=0;
uint32_t RxData2=0;
uint32_t RxData3=0;
uint32_t RxData4=0;

AFE4490Read(LED1VAL,&RxData1);     /*digital value of LED1 pulse */
AFE4490Read(LED2VAL,&RxData2);     /*digital value of LED2 pulse */
AFE4490Read(LED1ABSVAL,&RxData3);  /*digital value of LED1 ambient pulse */
AFE4490Read(LED2ABSVAL,&RxData4);  /*digital value of LED2 ambient pulse */
}

AFE4490 has an interrupt pin that I used an input to the MCU, and triggers as soon as digital LEDs data is available. This pin is labeled ADC_RDY (pins 28), and it is defined in the datasheet as an output signal that indicates ADC conversion completion.

In my configuration, I set up  this interrupt to trigger every 5ms, and I did this by writing a value of 2000 to  PRPCOUNT register, Pulse Repetition Period Count Register.  As per [1], The contents of this register can be used to set the pulse repetition period(in number of clock cyclesof the 4-MHzclock). The PRPCOUNT value must be set in the range of 800 to 64000.

Thus, every time an interrupt is triggered, the function above is called and a new set of data is read. It is also possible to read the sensor data by polling instead of an interrupt. You can let a task  that execute the function above runs every 10 ms, and have it execute the function above.

 

References

[1] AFE4490IntegratedAnalogFront-Endfor Pulse Oximeters, TI datasheet.