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 STOP bit in the MCR register.
  • 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.

Solution