LPI2C Controller example - interrupt
Resources: ese_driver_examples\lpi2c\controller_interrupt_p3t1755
Goal
To know how to configure the Low-Power I2C module for basic serial communication and use the LPI2C with interrupts.
API
An interrutp driven API provides the advantage of not blocking the main application while waiting for a transfer to complete. The API is based on the LPI2C - polling example, but uses interrupts to handle the transmission and reception of data.
The API consists of the same functions as the LPI2C - polling example, but with the addition of the following two functions. These functions can be used to check if the transmissin has been completed or not.
void lpi2c0_write(const uint8_t dev_address, const uint8_t reg, uint8_t *p,
const uint8_t len);
void lpi2c0_read(const uint8_t dev_address, const uint8_t reg, uint8_t *p,
const uint8_t len);
bool lpi2c0_write_done(void);
bool lpi2c0_read_done(void);
Data validity
Similar to using an interrupt driven UART, transmitted and received data needs to be stored temporarily. Fortunately, the MCXA153A microcontroller features a LPI2C module with a four word hardware FIFO (see LPI2C - polling for more info). However, if more than four words need to be temporary stored, a software solution is required. For that purpose the interrupt driven API uses the following local variables:
static volatile uint8_t *tx_buffer; // Pointer to the buffer for transmit data
static volatile uint8_t *rx_buffer; // Pointer to the buffer for receive data
static volatile uint32_t tx_count = 0; // Number of bytes transmitted
static volatile uint32_t tx_total = 0; // Total number of bytes to transmit
static volatile uint32_t rx_count = 0; // Number of bytes received
static volatile uint32_t rx_total = 0; // Total number of bytes to receive
Notice that it uses pointers to buffers. It is the responsibility of the calling application that the data remains valid until the transmission is completed. The API does not copy the data to a local buffer.
Initialization
When compared to the LPI2C - polling example, the following additional steps are required:
- Stop condition automatically generated. The STOP condition is the last command that must be transmitted, but it cannot be appended to the user provided data buffer. However, the LPI2C module can automatically generate a stop condition when the transmit FIFO is empty and the controller is busy. This is done by setting the
STOPbit in theMCRregister. - Interrupts are enabled, in the LPI2C0 module and the NVIC.
- An interrupt handler is implemented, taking care of the transmission and reception of data.
Writing data
Data can be written as follows:
void lpi2c0_write(const uint8_t dev_address, const uint8_t reg, uint8_t *p,
const uint8_t len)
{
// Wait as long as bus or controller is busy
lpi2c0_wait_busy();
// Clear all status flags
LPI2C0->MSR = LPI2C_MSR_STF_MASK | LPI2C_MSR_DMF_MASK |
LPI2C_MSR_PLTF_MASK | LPI2C_MSR_FEF_MASK | LPI2C_MSR_ALF_MASK |
LPI2C_MSR_NDF_MASK | LPI2C_MSR_SDF_MASK | LPI2C_MSR_EPF_MASK;
// Reset fifos
LPI2C0->MCR |= LPI2C_MCR_RTF(1) | LPI2C_MCR_RRF(1);
// Fill transmit fifo
// Command and transmit FIFO of 4 words (8-bit transmit data + 3-bit command)
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b100) | LPI2C_MTDR_DATA(dev_address << 1);
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b000) | LPI2C_MTDR_DATA(reg);
if(len >= 1)
{
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b000) | LPI2C_MTDR_DATA(p[0]);
}
if(len >= 2)
{
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b000) | LPI2C_MTDR_DATA(p[1]);
}
// The words that will not fit within the transmit fifo, must be transmitted
// through the software buffer.
if(len >= 3)
{
// Set initial value for variables
tx_count = 2;
tx_total = len;
tx_buffer = p;
// Enable transmit interrupts
LPI2C0->MIER |= LPI2C_MIER_TDIE(1);
}
}
The following interrupt handler is used to handle the transmission of data:
void LPI2C0_IRQHandler(void)
{
NVIC_ClearPendingIRQ(LPI2C0_IRQn);
// Transmit interrupt?
if((LPI2C0->MSR & LPI2C_MSR_TDF_MASK) != 0)
{
// Put as many data items in the transmit fifo
while((tx_count < tx_total) && !lpi2c0_txfifo_full())
{
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b000) | LPI2C_MTDR_DATA(tx_buffer[tx_count++]);
}
// All data transmitted?
if(tx_count == tx_total)
{
// Disable transmit interrupt
LPI2C0->MIER &= ~LPI2C_MIER_TDIE(1);
}
}
}
And the following function can be used to check if the transmission is completed:
bool lpi2c0_write_done(void)
{
// Check if the transmission is completed
return (tx_count == tx_total);
}
And, finally, a PT3T1755 specific function to set the configuration register is as follows:
void p3t1755_set_configuration_reg(uint8_t val)
{
// Device address: 0b1001000 (P3T1755)
// Pointer byte: 0b00000001 (Configuration register)
// Send one byte: val
lpi2c0_write(0b1001000, 0b00000001, &val, 1);
// Wait until the writing is finished
while(!lpi2c0_write_done())
{}
}
Note. Calling the function lpi2c0_write_done() will check if the transmission is completed before proceeding. Strictly speaking this is not necessary, because the data is transmitted based on interrupts. It is here, so it exactly resembles the polling implementation.
Reading data
A generic, interrupt driven read function is implemented as follows:
void lpi2c0_read(const uint8_t dev_address, const uint8_t reg, uint8_t *p,
const uint8_t len)
{
// Wait as long as bus or controller is busy
lpi2c0_wait_busy();
// Clear all status flags
LPI2C0->MSR = LPI2C_MSR_STF_MASK | LPI2C_MSR_DMF_MASK |
LPI2C_MSR_PLTF_MASK | LPI2C_MSR_FEF_MASK | LPI2C_MSR_ALF_MASK |
LPI2C_MSR_NDF_MASK | LPI2C_MSR_SDF_MASK | LPI2C_MSR_EPF_MASK;
// Reset fifos
LPI2C0->MCR |= LPI2C_MCR_RTF(1) | LPI2C_MCR_RRF(1);
// Fill transmit fifo
// Command and transmit FIFO of 4 words (8-bit transmit data + 3-bit command)
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b100) | LPI2C_MTDR_DATA(dev_address << 1);
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b000) | LPI2C_MTDR_DATA(reg);
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b100) | LPI2C_MTDR_DATA((dev_address << 1) | 1 );
LPI2C0->MTDR = LPI2C_MTDR_CMD(0b001) | LPI2C_MTDR_DATA(len-1);
// Set initial value for receive variables
rx_count = 0;
rx_total = len;
rx_buffer = p;
// Enable receive interrupts
LPI2C0->MIER |= LPI2C_MIER_RDIE(1);
}
Notice how the four word FIFO is used to transmit the command and data. The first two words are used to send the device address and register address, while the last two words are used to request data from the device. The first word of the last two words contains the device address with the read bit set, and the second word contains the number of bytes to read minus one (as mentioned in the reference manual).
The following interrupt handler is used to handle the transmission of data:
void LPI2C0_IRQHandler(void)
{
NVIC_ClearPendingIRQ(LPI2C0_IRQn);
// Receive interrupt?
if((LPI2C0->MSR & LPI2C_MSR_RDF_MASK) != 0)
{
// Read the data
rx_buffer[rx_count++] = (uint8_t)LPI2C0->MRDR;
// All data received?
if(rx_count == rx_total)
{
// Disable receive interrupt
LPI2C0->MIER &= ~LPI2C_MIER_RDIE(1);
}
}
}
The following function can be used to check if the transmission is completed:
bool lpi2c0_read_done(void)
{
return rx_count == rx_total;
}
And, finally, a PT3T1755 specific function to read the temperature register is as follows:
float p3t1755_get_temperature(void)
{
uint8_t data[2] = {0};
// Get temperature data from the P3T1755
// Device address: 0b1001000 (P3T1755)
// Pointer byte: 0b00000000 (Temperature register)
lpi2c0_read(0b1001000, 0b00000000, data, 2);
// Wait until the reading is finished
while(!lpi2c0_read_done())
{}
// Calculate temperature
uint16_t temp_data = (int16_t)(data[0] << 4) | (data[1] >> 4);
float temperature = 0;
// Positive temperature?
if((temp_data & 0b0000100000000000) == 0)
{
temperature = temp_data * 0.0625f;
}
else
{
temp_data = (~temp_data) & 0x0FFF;
temperature = -((temp_data + 1) * 0.0625f);
}
return temperature;
}
Assignment
Q1 According to the P3T1755 datasheet, what is the default value of the registers T_LOW and T_HIGH?
Implement the following API functions:
uint16_t p3t1755_get_t_low(void);
uint16_t p3t1755_get_t_high(void);
Test these functions by calling them in p3t1755_init() as follows:
void p3t1755_init(void)
{
lpi2c0_controller_init();
#ifdef DEBUG
// Check connectivity by reading the control register. The POR value is
// 0x28 (NXP, 2023).
//
uint8_t reg = p3t1755_get_configuration_reg();
if(reg != 0x28)
{
// Error
while(1)
{}
}
uint16_t t_low = p3t1755_get_t_low();
uint16_t t_high = p3t1755_get_t_high();
if((t_low != 0x4B00) || (t_high != 0x5000))
{
// Error
while(1)
{}
}
#endif
}
Use a logic analyzer to visualize the SDA and SCL signals and verify that the default value of the registers T_LOW and T_HIGH indeed is transmitted.