Working with STM32 and I2C: Using DMA Mode

In the previous guides, we took a look how to scan the i2c bus for peripherals (here), read single byte (here), write single byte (here), read multiple-bytes (here) and multiple-bytes(here). In this guide, we shall use DMA to transfer the data from/to peripheral using DMA.

In this guide, we shall cover the following:

  • Configure I2C in DMA
  • I2C write using DMA
  • I2C read using DMA
  • Connection diagram
  • Code
  • Demo

1. Configure the I2C for DMA

We start off by some defines states that will help us later

/**
DMA1_Stream5_channel 1 is I2C1_RX
DMA1_Stream6_channel 1 is I2C1_TX
*/

#define PB8_ALT (1<<17)
#define PB9_ALT (1<<19)
#define I2C_AF4 (0x04)

#define ch1 (1<<25)

We also need to check which DMA, stream and channel that has the I2C1_TX and I2C1_RX

From the DMA section of F411 reference manual,

We can see that DMA1 Stream5 channel 1 is for I2C1_RX and DMA1 Stream 6 Channel 1 is for I2C1_TX

Now we can configure the I2C1

void i2c1_init(void)
		{
		RCC->AHB1ENR|=RCC_AHB1ENR_GPIOBEN; //enable gpiob clock
		RCC->APB1ENR|=RCC_APB1ENR_I2C1EN; //enable i2c1 clock
		GPIOB->MODER|=PB8_ALT|PB9_ALT; //set PB8 and PB9 to alternative function
		GPIOB->AFR[1]|=(I2C_AF4<<0)|(I2C_AF4<<4);
		GPIOB->OTYPER|=GPIO_OTYPER_OT8|GPIO_OTYPER_OT9;
		I2C1->CR1=I2C_CR1_SWRST;//reset i2c
		I2C1->CR1&=~I2C_CR1_SWRST;// release reset i2c	
		I2C1->CR1 &=~ I2C_CR1_NOSTRETCH;//disable clock strech
		I2C1->CR1 &= ~I2C_CR1_ENGC;//diable generaral callback
		I2C1->CR2 |= I2C_CR2_LAST;//set next DMA EOT is last transfer
		I2C1->CR2 |= I2C_CR2_DMAEN; //enable DMA 
		I2C1->CR2|=16;//set clock source to 16MHz
		I2C1->CCR=80;  //based on calculation
		I2C1->TRISE=17; //output max rise 
		I2C1->CR1 |=I2C_CR1_PE;
		}

Initialize the DMA1 for both stream

/**
 * @brief   Initialize DMA1_Stream5
 * @note    CH3 for I2C1
 * @param   None
 * @retval  None
 */	
	void i2c_rx_dma_init(void)
		{
		RCC->AHB1ENR|=RCC_AHB1ENR_DMA1EN;
		DMA1_Stream5->CR=0x00;//reset everything
		while((DMA1_Stream5->CR)&DMA_SxCR_EN){;}
		DMA1_Stream5->CR|=ch1|DMA_SxCR_MINC|DMA_SxCR_TCIE|DMA_SxCR_HTIE|DMA_SxCR_TEIE;
		NVIC_EnableIRQ(DMA1_Stream5_IRQn);
		}
		
/**
 * @brief   Initialize DMA1_Stream6 
 * @note    CH3 for I2C1
 * @param   None
 * @retval  None
 */	
		void i2c_tx_dma_init(void)
		{
		RCC->AHB1ENR|=RCC_AHB1ENR_DMA1EN;
		DMA1_Stream6->CR=0x00;//reset everything
		while((DMA1_Stream6->CR)&DMA_SxCR_EN){;}
		DMA1_Stream6->CR|=ch1|DMA_SxCR_MINC|DMA_SxCR_DIR_0|DMA_SxCR_TCIE|DMA_SxCR_HTIE|DMA_SxCR_TEIE;
		
		NVIC_EnableIRQ(DMA1_Stream6_IRQn);	
		}
		

2. I2C write using DMA:

For the write function, we need two functions, first one is for setting address and start transfer which similar to writing a single byte. After the address has set, we can start with DMA transfer.

void I2C_write(uint8_t SensorAddr,
    uint8_t * pWriteBuffer, uint16_t NumByteToWrite)
{
	/*Wait until the bus is free*/
		while(I2C1->SR2&I2C_SR2_BUSY){;}
	
		/* Generate START */
		I2C1->CR1 |= I2C_CR1_START;

  /* Wait SB flag is set */
		while(!(I2C1->SR1&I2C_SR1_SB)){;}
  /* Read SR1 */
		(void)I2C1->SR1;

  /* Send slave address with write */
			I2C1->DR = (SensorAddr<<1);

  /* Wait ADDR flag is set */
			while(((I2C1->SR1)&I2C_SR1_ADDR)==0){;}
  

  /* Start DMA */
		DMA_Transmit(pWriteBuffer, NumByteToWrite);

  /* Read SR1 */
			(void)I2C1->SR1;

  /* Read SR2 */
			(void)I2C1->SR2;
}		

Now for DMA transfer from memory to peripheral

static void DMA_Transmit(const uint8_t * pBuffer, uint8_t size)
{
	
  /* Check null pointers */
  if(NULL != pBuffer)
  {
    DMA1_Stream6->CR&=~DMA_SxCR_EN;
	while((DMA1_Stream6->CR)&DMA_SxCR_EN){;}

    /* Set memory address */
    DMA1_Stream6->M0AR = (uint32_t)pBuffer;
		DMA1_Stream6->PAR=(uint32_t)&I2C1->DR;
    /* Set number of data items */
    DMA1_Stream6->NDTR = size;

    /* Clear all interrupt flags */
    DMA1->HIFCR = (DMA_HIFCR_CDMEIF6 | DMA_HIFCR_CTEIF6
        | DMA_HIFCR_CHTIF6 | DMA_HIFCR_CTCIF6);

    /* Enable DMA1_Stream4 */
    DMA1_Stream6->CR |= DMA_SxCR_EN;
  }
  else
  {
    /* Null pointers, do nothing */
  }
}

3. I2C Read using DMA:

For reading, we start off normally similar to reading a single byte guide:

void I2C_Read(uint8_t SensorAddr, uint8_t ReadAddr,
				uint8_t * pReadBuffer, uint16_t NumByteToRead)
		{
			//wait until the bus is free
			while(I2C1->SR2&I2C_SR2_BUSY){;}
			
			/* Generate START */
			I2C1->CR1 |= I2C_CR1_START;
			/* Wait SB flag is set */
			while(!(I2C1->SR1&I2C_SR1_SB)){;}

			/* Read SR1 */
			(void)I2C1->SR1;

			/* Send slave address with write */
			I2C1->DR=(SensorAddr<<1|0);

			/* Wait ADDR flag is set */
			while(((I2C1->SR1)&I2C_SR1_ADDR)==0){;}
			/* Read SR1 */
			(void)I2C1->SR1;

			/* Read SR2 */
			(void)I2C1->SR2;

			/* Wait TXE flag is set */
			while(I2C_SR1_TXE != (I2C_SR1_TXE & I2C1->SR1))
			{
				/* Do nothing */
			}

			if(2 <= NumByteToRead)
			{
				/* Acknowledge enable */
				I2C1->CR1 |= I2C_CR1_ACK;

				/* Send register address to read with increment */
				I2C1->DR =  (ReadAddr);
			}
			else
			{
				/* Acknowledge disable */
				I2C1->CR1 &= ~I2C_CR1_ACK;

				/* Send register address to read (single) */
				I2C1->DR =  ReadAddr;
				
			}



			/* Wait BTF flag is set */
			while(!(I2C_SR1_BTF & I2C1->SR1))
			{
				/* Do nothing */
			}

			/* Generate ReSTART */
			I2C1->CR1 |= I2C_CR1_START;

			/* Wait SB flag is set */
			while(I2C_SR1_SB != (I2C_SR1_SB & I2C1->SR1))
			{
				/* Do nothing */
			}

			/* Read SR1 */
			(void)I2C1->SR1;

			/* Send slave address with read */
			I2C1->DR =  (SensorAddr<<1 | (uint8_t)0x01);

			/* Wait ADDR flag is set */
			while(((I2C1->SR1)&I2C_SR1_ADDR)==0){;}
			

			/* Start DMA */
			DMA_Receive(pReadBuffer, NumByteToRead);

			/* Read SR1 */
			(void)I2C1->SR1;

			/* Read SR2 */
			(void)I2C1->SR2;
		}
	

Then rather using while loop, we can use DMA to receive the data as following:

static void DMA_Receive(const uint8_t * pBuffer, uint8_t size)
{
	
  /* Check null pointers */
  if(NULL != pBuffer)
  {
    DMA1_Stream5->CR&=~DMA_SxCR_EN;
	while((DMA1_Stream5->CR)&DMA_SxCR_EN){;}

    /* Set memory address */
    DMA1_Stream5->M0AR = (uint32_t)pBuffer;
		DMA1_Stream5->PAR=(uint32_t)&I2C1->DR;
    /* Set number of data items */
    DMA1_Stream5->NDTR = size;

    /* Clear all interrupt flags */
    DMA1->HIFCR = ( DMA_HIFCR_CDMEIF5 | DMA_HIFCR_CTEIF5
        | DMA_HIFCR_CHTIF5 | DMA_HIFCR_CTCIF5);

    /* Enable DMA1_Stream2 */
    DMA1_Stream5->CR |= DMA_SxCR_EN;
  }
  else
  {
    /* Null pointers, do nothing */
  }
}	

In our header file,

#ifndef __wire__h
#define __wire__h
#include "stm32f4xx.h"                  // Device header
#include <stddef.h>
void i2c1_init(void);
void i2c_rx_dma_init(void);
void i2c_tx_dma_init(void);
void I2C_write(uint8_t SensorAddr,uint8_t * pWriteBuffer, uint16_t NumByteToWrite);
void I2C_Read(uint8_t SensorAddr, uint8_t ReadAddr,uint8_t * pReadBuffer, uint16_t NumByteToRead);


#endif

for testing purposes, we shall randomize the minutes and hours every 5 seconds as following:

#include "debug.h"
#include "wire.h"
#include "stdlib.h"
volatile int finished=0;
int read_finish();
void reset_finish();
uint8_t data[3], data_r[3];
uint8_t data_s[4];
//note: Index zero shall always contains the starting memory address
uint8_t data_write[4]={0x00,0x10,0x12,0x00};


int bcd_to_decimal(unsigned char x) {
    return x - 6 * (x >> 4);
}





int main(void)
	{
	i2c1_init();
	i2c_rx_dma_init();
	i2c_tx_dma_init();
	while(1)
		{
			I2C_Read(0x68,0x00,data,3);
			while(read_finish()==0){;}
			reset_finish();
		//for(volatile int i=0;i<100000;i++);
				for (int i=0;i<3;i++)
						{
						data_r[i]=bcd_to_decimal(data[i]);
						}
				
				if(data_r[0]==5)
						{
				
						data_s[0]=0x00;
						data_s[1]=0;
						data_s[2]=rand()%20;
						data_s[3]=rand()%10;
						I2C_write(0x068,data_s,4);
						
						
						}
		
			
				
				
				
		}
		
	
	}
	
	
int read_finish()
	{
	return finished;
	}
	
void reset_finish()
	{
	finished=0;
	}

void DMA1_Stream5_IRQHandler(void)
			{
			
			if((DMA1->HISR)&DMA_HISR_TCIF5)
					{
					finished=1;
					log_debug("I2C finished receiving using DMA1_Stream5");
					I2C1->CR1 |= I2C_CR1_STOP;
					DMA1->HIFCR=DMA_HIFCR_CTCIF5;
					}
			if((DMA1->HISR)&DMA_HISR_HTIF5)
					{
					log_debug("DMA1 stream5 half transfer interrupt");
					DMA1->HIFCR=DMA_HIFCR_CHTIF5;
					}
					
			if((DMA1->HISR)&DMA_HISR_TEIF5)
					{
					log_debug("DMA1 stream5 error");
					DMA1->HIFCR=DMA_HIFCR_CTEIF5;
					}
			}
			
void DMA1_Stream6_IRQHandler(void)
			{
			
			if((DMA1->HISR)&DMA_HISR_TCIF6)
					{
			
					log_debug("I2C finished transmiting using DMA1_Stream6");
					finished=1;
					I2C1->CR1 |= I2C_CR1_STOP;
					DMA1->HIFCR=DMA_HIFCR_CTCIF6;
						
					}
			if((DMA1->HISR)&DMA_HISR_HTIF6)
					{
					log_debug("DMA1 stream6 half transfer interrupt");
					DMA1->HIFCR=DMA_HIFCR_CHTIF6;
					}
					
			if((DMA1->HISR)&DMA_HISR_TEIF6)
					{
					log_debug("DMA1 stream6 error");
					DMA1->HIFCR=DMA_HIFCR_CTEIF6;
					}
			
			}

4. Connection diagram:

In this guide, we shall use DS3231 for I2C experiment and connected to our STM32F411RE Nucleo as following:

5. Code:

You can download the code from here:

6. Demo

Happy coding 😀

Add Comment

Your email address will not be published. Required fields are marked *