Adding I2C reads to your processing bandwidth constrained application

Document created by Chris Brown Employee on Apr 29, 2016Last modified by Chris Brown Employee on Aug 1, 2016
Version 7Show Document
  • View in full screen mode

Introduction

 

With the growth of the Internet of Things (IoT), more and more applications are incorporating the use of sensors while also requiring power efficiency and increased performance.  A popular interface for these sensors is the I2C protocol. The I2C bus is a great protocol that is a true multi-master protocol and allows for each bus to contain many devices.  As the performance demand of the application grows, so will the speed of the I2C bus as it will be necessary to get more data from the sensors and/or at a faster rate.  Many applications may already have a need to operate an I2C bus at 400 kHz or more.  Higher data rates means the MCU core will need to spend more time servicing the I2C transactions.  The DMA module is one good way to free up the core in order to let it tend to other aspects of the application.  This can add much needed or much desired performance to applications.  Especially applications that may be using small, power efficient, single core MCUs.

 

It may seem like an easy, straight-forward task to add I2C reads from a sensor to an application.  However I2C is a time sensitive protocol and consequently, so is the I2C peripherals on MCUs.  It is important to understand the time requirements and how to overcome them. The recommended approach is to use DMA to transfer the received I2C data to the desired buffer in your application software.  This document is going to outline how to setup your DMA and provide an example of how to do this for a KW40 device using the Kinetis SDK version 1.3.  The KW40 is being targeted because this is a small, power efficient MCU that incorporates a radio for your wireless applications and as such, it is likely that your application could need this DMA approach.  The KSDK version 1.3 is being targeted because this version of the SDK does not currently support DMA transactions for the I2C peripheral.

 

Understanding the Kinetis I2C peripheral module

Before getting into the specifics of creating a DMA enabled I2C driver, it is important to understand some basics of the Kinetis I2C peripheral module.  This module handles a lot of the low-level timing.  However the I2C registers must be serviced in a timely manner to operate correctly.  Take the case of a master reading data from a typical I2C sensor as shown in the diagram below.

 

I2C_overview.png

 

In the diagram above, the red lines indicate points in the transaction where software or DMA needs to interact with the I2C peripheral to ensure the transaction happens correctly.  To begin a transaction the core must change the MST bit which puts a start bit on the bus (marked by symbol ST).  Immediately following this, the core should then also write the target slave's address (Device Address) including the read/write bit (R/W).  Once this transaction is complete, the I2C will issue an interrupt and then the core should write the register address to be read from. Upon completion of that being put on the bus, the I2C will issue another interrupt and the master should then put a repeated start (SR) on the bus as well as the slave's address again.  Now the slave will send data to the master (once the master begins the transaction by issuing a dummy read of the I2C data register).  In the standard configuration, the I2C peripheral will automatically send the NAK or AK depending on the configuration of the TXAK bit in the I2C peripheral.  Because of this automation, it is important that this bit be handled properly and is configured one frame in advance. Furthermore, to ensure that the NAK bit is sent at the appropriate time, the TXAK bit must be set when the second to last byte is received.  The timing of this configuration change is very important to ensuring that the transaction happens properly.

 

This document will describe how to use DMA to receive the data.  The DMA will be configured before the transaction begins and will be used to receive the data from the slave.  The document will also discuss options to handle proper NAK'ing of the data to end the transaction.

 

Writing a DMA I2C master receive function

The first step in adding DMA capability to your SDK driver is to create a new receive function with an appropriate name.  For this example, the newly created receive function is named I2C_DRV_MasterReceiveDataDMA.  To create this function, the I2C_DRV_MasterReceive function (which is called for both blocking and non-blocking) was copied and then modified by removing the blocking capability of the function. Then in this function, after the dummy read of the IIC data register that triggers the reception of data, the DMA enable bit of the I2C control register is written.

 

/*FUNCTION**********************************************************************
 *
 * Function Name : I2C_DRV_MasterReceiveDataDMA
 * Description   : Performs a non-blocking receive transaction on the I2C bus
 *                 utilizing DMA to receive the data.
 *
 *END**************************************************************************/
i2c_status_t I2C_DRV_MasterReceiveDataDMA(uint32_t instance,
                                               const i2c_device_t * device,
                                               const uint8_t * cmdBuff,
                                               uint32_t cmdSize,
                                               uint8_t * rxBuff,
                                               uint32_t rxSize,
                                               uint32_t timeout_ms)
{
    assert(instance < I2C_INSTANCE_COUNT);
    assert(rxBuff);
  
    I2C_Type * base = g_i2cBase[instance];
    i2c_master_state_t * master = (i2c_master_state_t *)g_i2cStatePtr[instance];
        
    /* Return if current instance is used */
    OSA_EnterCritical(kCriticalDisableInt);     
    if (!master->i2cIdle)
    {
        OSA_ExitCritical(kCriticalDisableInt);
        return kStatus_I2C_Busy;
    }
    
    master->rxBuff = rxBuff;
    master->rxSize = rxSize;
    master->txBuff = NULL;
    master->txSize = 0;
    master->status = kStatus_I2C_Success;
    master->i2cIdle = false;
    master->isBlocking = true;
    OSA_ExitCritical(kCriticalDisableInt);
        
    while(I2C_HAL_GetStatusFlag(base, kI2CBusBusy));


    I2C_DRV_MasterSetBaudRate(instance, device);
    
    /* Set direction to send for sending of address. */
    I2C_HAL_SetDirMode(base, kI2CSend);
  
    /* Enable i2c interrupt.*/
    I2C_HAL_ClearInt(base);
    I2C_HAL_SetIntCmd(base, true);
  
    /* Generate start signal. */
    I2C_HAL_SendStart(base);
  
    /* Send out slave address. */
    I2C_DRV_SendAddress(instance, device, cmdBuff, cmdSize, kI2CReceive, timeout_ms);
  
    /* Start to receive data. */
    if (master->status == kStatus_I2C_Success)
    {
        /* Change direction to receive. */
        I2C_HAL_SetDirMode(base, kI2CReceive);
        
        /* Send NAK if only one byte to read. */
        if (rxSize == 0x1U)
        {
        I2C_HAL_SendNak(base);
        }
        else
        {
        I2C_HAL_SendAck(base);
        }
        
        /* Dummy read to trigger receive of next byte in interrupt. */
        I2C_HAL_ReadByte(base);
        
        /* Now set the DMA bit to let the DMA take over the reception. */
        I2C_C1_REG(I2C1) |= I2C_C1_DMAEN_MASK;
        
        /* Don't wait for the transfer to finish. Exit immediately*/
    }
    else if (master->status == kStatus_I2C_Timeout)
    {
        /* Disable interrupt. */
        I2C_HAL_SetIntCmd(base, false);
        
        if (I2C_HAL_GetStatusFlag(base, kI2CBusBusy))
        {
        /* Generate stop signal. */
        I2C_HAL_SendStop(base);
        }
        
        /* Indicate I2C bus is idle. */
        master->i2cIdle = true;
    }
    
    return master->status;
}

 

After writing the DMA driver, a DMA specific transfer complete function must be implemented. This is needed in order for the application software to signal to the driver structures that the transfer has been completed and the bus is now idle. In addition, the DMA enable bit needs to be cleared in order for other driver functions to be able to properly use the IIC peripheral.

 

void I2C_DRV_CompleteTransferDMA(uint32_t instance)
{
    assert(instance < I2C_INSTANCE_COUNT);


    I2C_Type * base = g_i2cBase[instance];
    i2c_master_state_t * master = (i2c_master_state_t *)g_i2cStatePtr[instance];
    
    I2C_C1_REG(base) &= ~(I2C_C1_DMAEN_MASK | I2C_C1_TX_MASK);
    I2C_C1_REG(base) &= ~I2C_C1_MST_MASK;;
   
    /* Indicate I2C bus is idle. */
    master->i2cIdle = true;
}

 

DMA Configuration

Next, the application layer needs a function to configure the DMA properly, and a DMA callback is needed to properly service the DMA interrupt that will be used as well as post a semaphore. But before diving into the specifics of that, it is important to discuss the overall strategy of using the DMA in this particular application. After every transaction, the data register must be serviced to ensure that all of the necessary data is received.  One DMA channel can easily be assigned to service this activity.  After the reception of the second to last data byte, the TXAK bit must be written with a '1' to ensure that the NAK is put on the bus at the appropriate time. This is a little trickier to do.  There are three options:

 

  1. A second dedicated DMA channel can be linked to write the I2C_C1 register every time the I2C_D register is serviced.  This option will require a second array to hold the appropriate values to be written to the I2C_C1 register.  The following figure illustrates this process.Case_1 (002).png
  2. The second DMA channel can be linked to write the I2C_C1 register after the second to last data byte has been received.  This option would require that the primary DMA channel be set to receive two data bytes less than the total number of desired data bytes.  The primary DMA channel would also need to be re-configured to receive the last two bytes (or the application software would need to handle this).  However this could be a desirable programming path for applications that are memory constrained as it reduces the amount of memory necessary for your application.Case_2 (002).png
  3. The primary DMA channel can be set to receive two data bytes less than the total number of desired data bytes and the core (application software) can handle the reception of the last two bytes.  This would be a desirable option for those looking for a simpler solution but has the drawback that in a system where the core is already handling many other tasks, there may still be issues with writing the TXAK bit on time. Case_3 (002).png

 

This example will focus on option number 1, as this is the simplest, fully automatic solution.  It could also easily be modified to fit the second option as the programmer would simply need to change the number of bytes to receive by the primary DMA and add some reconfiguration information in the interrupt to service the primary DMA channel.

 

DMA Channel #1

The first DMA channel is configured to perform 8-bit  transfers from the I2C data register (I2C_D) to the buffer to hold the desired data.  This channel should transfer the number of desired bytes minus one.  The final byte will be received by the core.  Other DMA configuration bits that are important to set are the cycle steal bit, disable request bit, peripheral request bit (ERQ), interrupt on completion of transfer (EINT), and destination increment (DINC).  It also important to configure the link channel control to perform a link to channel LCH1 after each cycle-steal transfer and LCH1 should be configured for the channel that will transfer from memory to the I2C control register (I2C_C1).  The first DMA channel is configured as shown below.

// Set Source Address (this is the UART0_D register
      DMA_SAR0 = (uint32_t)&I2C_D_REG(base);
      
      // Set BCR to know how many bytes to transfer
      // Need to set to desired size minus 1 because the last will be manually 
      // read.  
      DMA_DSR_BCR0 = DMA_DSR_BCR_BCR(destArraySize - 1);
      
      // Clear Source size and Destination size fields.  
      DMA_DCR0 &= ~(DMA_DCR_SSIZE_MASK 
                    | DMA_DCR_DSIZE_MASK
                    );

      // Set DMA as follows:
      //     Source size is byte size
      //     Destination size is byte size
      //     D_REQ cleared automatically by hardware
      //     Destination address will be incremented after each transfer
      //     Cycle Steal mode
      //     External Requests are enabled
      //     Interrupts are enabled
      //     Asynchronous DMA requests are enabled.
      //     Linking to channel LCH1 after each cycle steal transfer
      //     Set LCH1 to DMA CH 1.  
      DMA_DCR0 |= (DMA_DCR_SSIZE(1)             // 1 = 8-bit transfers
                   | DMA_DCR_DSIZE(1)           // 1 = 8-bit transfers
                   | DMA_DCR_D_REQ_MASK
                   | DMA_DCR_DINC_MASK
                   | DMA_DCR_CS_MASK
                   | DMA_DCR_ERQ_MASK
                   | DMA_DCR_EINT_MASK
                   | DMA_DCR_EADREQ_MASK
                   | DMA_DCR_LINKCC(2)          // Link to LCH1 after each cycle-steal transfer
                   | DMA_DCR_LCH1(1)            // Link to DMA CH1
                   );

      // Set destination address
      DMA_DAR0 = (uint32_t)destArray;

 

DMA Channel #2

The second DMA channel, which is the linked channel, should be configured to perform 8-bit transfers that transfer data from an array in memory (titled ack_nak_array in this example) to the I2C control register (I2C_C1).  This channel should also disables requests upon completion of the entire transfer, and enable the cycle-steal mode.  In this channel, the source should be incremented (as opposed to the destination as in the first channel). This channel is configured as shown below:

 

// Set Source Address (this is the UART0_D register
      DMA_SAR1 = (uint32_t)ack_nak_array;
      
      // Set BCR to know how many bytes to transfer
      // Need to set to desired size minus 1 because the last will be manually 
      // read. 
      DMA_DSR_BCR1 = DMA_DSR_BCR_BCR(destArraySize - 1);
      
      // Clear Source size and Destination size fields.  
      DMA_DCR1 &= ~(DMA_DCR_SSIZE_MASK 
                    | DMA_DCR_DSIZE_MASK
                    );
      
      // Set DMA as follows:
      //     Source size is byte size
      //     Destination size is byte size
      //     D_REQ cleared automatically by hardware
      //     Destination address will be incremented after each transfer
      //     Cycle Steal mode
      //     External Requests are disabled
      //     Asynchronous DMA requests are enabled.
      DMA_DCR1 |= (DMA_DCR_SSIZE(1)             // 1 = 8-bit transfers
                   | DMA_DCR_DSIZE(1)           // 1 = 8-bit transfers
                   | DMA_DCR_D_REQ_MASK
                   | DMA_DCR_SINC_MASK
                   | DMA_DCR_CS_MASK
                   | DMA_DCR_EADREQ_MASK
                   );
      
      // Set destination address
      DMA_DAR1 = (uint32_t)&I2C_C1_REG(base);

 

Once the DMA channels are initialized, the only action left is to configure the interrupts, enable the channel in the DMA MUX, and create the semaphore if it has not already been created.  This is done as shown below.

 

//Need to enable the DMA IRQ
      NVIC_EnableIRQ(DMA0_IRQn);
      //////////////////////////////////////////////////////////////////////////
      // MUX configuration
      // Enables the DMA channel and select the DMA Channel Source  
      DMAMUX0_CHCFG0 = DMAMUX_CHCFG_SOURCE(BOARD_I2C_DMAMUX_CHN); //DMAMUX_CHCFG_ENBL_MASK|DMAMUX_CHCFG_SOURCE(0x31); //0xb1; 
      DMAMUX0_CHCFG0 |= DMAMUX_CHCFG_ENBL_MASK;
      
      /* Create semaphore */
      if(semDmaReady == NULL){
        semDmaReady = OSA_EXT_SemaphoreCreate(0);
      }

 

Finally, the DMA initialization function also initializes the ack_nak_array.  This is only necessary when implementing the first DMA strategy.  The second DMA strategy would only need to write a single value at the correct time.  The array initialization for strategy #1 is shown below.  Note that the values written to the array are 0xA1 plus the appropriate value of the TXAK bit.  By writing 0xA1, it is ensured that the I2C module will be enabled in master mode with the DMA enable bit set.

 

// Initialize Ack/Nak array
      // Need to initialize the Ack/Nak buffer first
      for( j=0; j < destArraySize; j++)
      {
          if(j >= (destArraySize - 2))
          {
              ack_nak_array[j] = 0xA1 | I2C_C1_TXAK_MASK;
          }
          else
          {
              ack_nak_array[j] = 0xA1 & (~I2C_C1_TXAK_MASK);
          }
      }

 

DMA Interrupt Handler

Now a DMA interrupt handler is required.  A minimum of overhead will be required for this example as the interrupt handler simply needs to service the DONE bit and post the semaphore created in the initialization.  The DMA interrupt handler is as follows:

 

void DMA0_IRQHandler(void)
{
    // Clear pending errors or the done bit 
    if (((DMA_DSR_BCR0 & DMA_DSR_BCR_DONE_MASK) == DMA_DSR_BCR_DONE_MASK)
        | ((DMA_DSR_BCR0 & DMA_DSR_BCR_BES_MASK) == DMA_DSR_BCR_BES_MASK)
        | ((DMA_DSR_BCR0 & DMA_DSR_BCR_BED_MASK) == DMA_DSR_BCR_BED_MASK)
        | ((DMA_DSR_BCR0 & DMA_DSR_BCR_CE_MASK) == DMA_DSR_BCR_CE_MASK))
    {
        // Clear the Done MASK and set semaphore, dmaDone
        DMA_DSR_BCR0 |= DMA_DSR_BCR_DONE_MASK;
        //dmaDone = 1;
        OSA_SemaphorePost(semDmaReady);
    }
}

 

Using your newly written driver function

Once all of these items have been taken care of, it is now time for the application to use the functions. It is expected that the DMA will be initialized before calling the DMA receive function.  After the first call, the DMA can be re-initialized every time or could simply be reset with the start address of the arrays and byte counter (this is the minimum of actions that must be performed).  Then the application should ensure that the transaction happened successfully.   Upon a successful call to the I2C_DRV_MasterReceiveDataDMA function, the application should wait for the semaphore to be posted.  Once the semaphore posts, the application software should wait for the Transfer Complete flag to become set.  This ensures that the application does not try to put a STOP signal on the bus before the NAK has been physically put on the bus.  If the STOP signal is attempted out of sequence, the I2C module could be put in an erroneous state and the STOP signal may not be sent.  Next, the I2C_DRV_CompleteTransferDMA function should be called to send the STOP signal and to signal to the driver structures that the bus is idle.  At this point, the I2C transaction is now fully complete and there is still one data byte that hasn't been transferred to the receive buffer.  It is the application's responsibility to perform one last read of the Data register to receive the last data byte of the transaction.

 

/* Now initialize the DMA */
   dma_init(BOARD_I2C_INSTANCE, Buffer, ack_nak_buffer, FXOS8700CQ_READ_LEN); //Init DMAMUX
   
   returnValue = I2C_DRV_MasterReceiveDataDMA(BOARD_I2C_INSTANCE, &slave,
                                                  cmdBuff, 1, Buffer, FXOS8700CQ_READ_LEN, 1000);

if (returnValue != kStatus_I2C_Success)
   {
       return (kStatus_I2C_Fail);
   }

/* Wait for the DMA transaction to complete */
   OSA_SemaphoreWait(semDmaReady, OSA_WAIT_FOREVER);
   
   /* Need to wait for the transfer to complete */
for(temp=0; temp<250; temp++)
    {
        if(I2C_HAL_GetStatusFlag(base, kI2CTransferComplete))
        {
            break;
        }
    }

   
   /* Now complete the transfer; this includes sending the I2C STOP signal and 
      clearing the DMA enable bit */
   I2C_DRV_CompleteTransferDMA(BOARD_I2C_INSTANCE);
   
   // Once the Transfer is complete, there is still one byte sitting in the Data
   // register.   
   Buffer[11] = I2C_D_REG(g_i2cBase[BOARD_I2C_INSTANCE]);

 

Conclusion

To summarize, as consumers demand more and more power efficient technology with more and more functionality, MCU product developers need to cram more functionality in small power efficient MCUs.  Relying on DMA for basic data transfers is one good way to improve performance of smaller power efficient MCUs with a single core. This can be particularly useful in applications where an MCU needs to pull information from and I2C sensor.  To do this, there are three methods of implementing an I2C master receive function in your SDK 1.3 based application.

 

  1. Use two DMA channels.  The first to transfer from the I2C Data register to the destination array.  A second dedicated DMA channel can be linked to write the I2C_C1 register every time the I2C_D register is serviced.
  2. Use two DMA channels.  The first to transfer from the I2C Data register to the destination array. The second DMA channel can be linked to write the I2C_C1 register only after the second to last data byte has been received.
  3. Use a single DMA channel can be set to receive two data bytes less than the total number of desired data bytes and the core (application software) can handle the reception of the last two bytes.

 

The recommendation of this document is to implement the first or second option as these are fully automatic options requiring the least intervention by the core.

2 people found this helpful

Outcomes