Using STM32 DMA and I2C to read data from MPU6050 - Old library

Using STM32 DMA and I2C to read data from MPU6050 - Old library

Update 11/2021: this was posted in 2014 using the old STM32 stdlib, which might be tedious to setup and get it working. The newer STM32CubeMX and STM32CubeIDE support DMA setup much quicker and easier. Nevertheless, the explanation in this post is still valid and useful to briefly layout how DMA in STM32 actually works. The code, however, is outdated and for reference only.

In the previous post, an example of using STM32 DMA to perform a simple data copy between 2 arrays was introduced. Now, I will show another example with DMA and I2C to read raw data from MPU6050 acceleration and gyroscope sensor directly. Besides, a comparison to show the timing difference between using and not using DMA is also mentioned.

stm32 mpu6050

stm32 mpu6050

MPU6050 is a very popular MEMS acceleration and gyroscope sensor and other devices can connect and get data from it through an I2C connection. There are a lot of libraries for Arduino that are available on the internet for connecting with MPU6050 and a few libraries for STM32. Harinadha has done the porting job from MPU6050 Arduino library of Jeff Rowberg to STM32 here as well: http://harinadha.wordpress.com/2012/05/23/mpu6050lib/ without using INT pin (interrupt pin) of MPU6050. I also used this library for the first time and found it was quite difficult to get the most updated gyro data for calculation. So, I got the wrong gyro angle all the time. Moreover, I noticed that the code took lots of time to read 14 bytes of data (including 6 bytes acceleration, 2 bytes of temperature, and 6 bytes of the gyro), nearly 2ms, so there is no chance to get the sample rate at 1ms.

MPU6050 STM32 connection

MPU6050 STM32 connection

Then I tried to look back at the code for Arduino and they actually used the INT pin of MPU6050 to trigger the reading routine. So, I tried to edit the code of Harinadha to implement both INT triggering and DMA reading from I2C with some fine tunes to give the best processing time. First, let’s have a look at the initialized routine for MPU6050. Again, Stdperiph driver V3.5.0 of ST was used here for basic I2C peripheral functions:

void MPU6050_Initialize(void)  
{
MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_PWR_MGMT_1, 1<<7);//reset the whole module first

delay(50);    //wait for 50ms for the gyro to stable

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_PWR_MGMT_1, MPU6050_CLOCK_PLL_ZGYRO);//PLL with Z axis gyroscope reference

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_CONFIG, 0x01);        //DLPF_CFG = 1: Fs=1khz; bandwidth=42hz 

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_SMPLRT_DIV, 0x01);    //500Hz sample rate ~ 2ms

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_GYRO_CONFIG, MPU6050_GYRO_FS_2000);    //Gyro full scale setting

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_ACCEL_CONFIG, MPU6050_ACCEL_FS_16);    //Accel full scale setting

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_INT_PIN_CFG, 1<<4);        //interrupt status bits are cleared on any read operation

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_INT_ENABLE, 1<<0);        //interupt occurs when data is ready. The interupt routine is in the receiver.c file.   

MPU6050_Write(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_SIGNAL_PATH_RESET, 0x07);//reset gyro and accel sensor  
}

With the MPU6050_Write function and I2C routine as follow:

void MPU6050_Write(uint8_t slaveAddr, uint8_t regAddr, uint8_t data)  
{
    uint8_t tmp;  
    tmp = data;
    MPU6050_I2C_ByteWrite(slaveAddr,&tmp,regAddr);   
}
//------------------------------------------------------------------
void MPU6050_I2C_ByteWrite(u8 slaveAddr, u8* pBuffer, u8 writeAddr)  
{

/* Send START condition */
I2C_GenerateSTART(MPU6050_I2C, ENABLE);  
/* Test on EV5 and clear it */
while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_MODE_SELECT));  
/* Send MPU6050 address for write */
I2C_Send7bitAddress(MPU6050_I2C, slaveAddr, I2C_Direction_Transmitter);  
/* Test on EV6 and clear it */
while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));  
/* Send the MPU6050's internal address to write to */
I2C_SendData(MPU6050_I2C, writeAddr);  
/* Test on EV8 and clear it */
//while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTING));
/* Send the byte to be written */
if (pBuffer!=0) I2C_SendData(MPU6050_I2C, pBuffer);  
/* Test on EV8_2 and clear it */
while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED));  
/* Send STOP condition */
I2C_GenerateSTOP(MPU6050_I2C, ENABLE);

}

And those defined registers of MPU6050 can be found here:

MPU6050.h

Here, we finish setting up the MPU6050 sensor. From now on, the sensor will run with the following configuration:

  • Sample rate: 2ms
  • Gyro full scale for X, Y, and Z-axis: +- 2000 degree/second. This means for example if the sensor is rotated in X-axis with a maximum angular velocity of 2000 degrees per second, the gyro X data will be a maximum value of 16bit integer variable: 32768. On the other hand, the readout value will be -32768 if the angular velocity is -2000 degrees per second. From here, we can come out with the conversion ratio from raw sensor data to real angular velocity: r = 32768 / full scale value = 32768 / 2000 = 16.384.
  • Accelerometer full scale: +- 16g.
  • Fire interrupt signal when data is available. Clear interrupt flag whenever the data is completely readout.

Next, we need to configure DMA peripheral to connect with I2C as well:

NVIC_InitTypeDef NVIC_InitStructure;  
DMA_InitTypeDef  DMA_InitStructure;

DMA_DeInit(MPU6050_DMA_Channel); //reset DMA1 channe1 to default values;

DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)I2C_DR_Address; //=0x40005410 : address of data reading register of I2C1  
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)I2C_Rx_Buffer; //variable to store data  
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //channel will be used for peripheral to memory transfer  
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;    //setting normal mode (non circular)  
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;    //medium priority  
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;    //Location assigned to peripheral register will be source  
DMA_InitStructure.DMA_BufferSize = 14;    //number of data to be transfered  
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //automatic memory increment disable for peripheral  
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;    //automatic memory increment enable for memory  
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;    //source peripheral data size = 8bit  
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;    //destination memory data size = 8bit  
DMA_Init(MPU6050_DMA_Channel, &DMA_InitStructure);  
DMA_ITConfig(MPU6050_DMA_Channel, DMA_IT_TC, ENABLE);

NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel7_IRQn; //I2C1 connect to channel 7 of DMA1  
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x05;  
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x05;  
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
NVIC_Init(&NVIC_InitStructure);  

The purpose of this configuration is to connect the I2C1 RX with the memory buffer directly using the corresponding DMA channel. We have some important points to notice here in order for you to be able to edit the code yourself in the future if you use another I2C peripheral or different type of sensor with different bytes to transfer through DMA:

  • Which DMA to use?
dma1
dma2

As you can see from the two tables from STM32F1 reference manual, there are 2 DMA blocks connected to different types of peripherals using different channels. Here we connect MPU6050 with I2C1 of STM32 so that the only choice is DMA1. Then, for reading data, we need to consider channel 7 that is connected to the RX register of I2C1 where all incoming data is stored. Later, if you want to use other peripherals with DMA in your own project, this table can be useful.

  • What is the physical peripheral address:

Each peripheral inside the STM32 has a boundary address which can be found in Table 3 of the reference manual.

dma3

And inside that peripheral, there are several registers whose addresses are inside that peripheral boundary. For example, in our case, we need to locate the address of I2C_DR register (Data register) to assign to DMA controller.

dma4a

Notice the “Offset” column in Table 189, it means the physical address of I2C_DR register will be offset from the initial address of I2C1 (0x40005400) by 0x10 -> I2C_DR address is 0x40005410

  • The number of byte to transfer:

As mentioned before, 14 bytes will be read from MPU6050, so DMA_Buffersize here should be 14 bytes.

After finishing the configuration parts, we move to the reading part. MPU6050 uses INT pin to trigger STM32 to read out its data as set before and we can use External Interrupt peripheral to capture it. The interrupt routine is as follow:

void EXTI4_IRQHandler(void)  
  {
  if (EXTI_GetITStatus(MPU6050_INT_Exti))            //MPU6050_INT
    {
    EXTI_ClearITPendingBit(MPU6050_INT_Exti);
    #ifndef USE_I2C_DMA
      Prepare_Gyro_Data();    //Read out the accel and gyro data whenever interrupt occurs.
    #else
      I2C_DMA_Read(MPU6050_DEFAULT_ADDRESS, MPU6050_RA_ACCEL_XOUT_H,MPU6050);
    #endif
}
}

Noted the define “USE_I2C_DMA” I used to choose between regular way and DMA way of reading. Here the INT pin is connected to GPIO_Pin_4 of GPIOB so EXTI4 is activated. Then the I2C_DMA_Read function is presented:

void I2C_DMA_Read(u8 slaveAddr, u8 readAddr, u8 sensor)  
  {
  /* Disable DMA channel*/
  DMA_Cmd(MPU6050_DMA_Channel, DISABLE);
  /* Set current data number again to 14 for MPu6050, only possible after disabling the DMA channel */
  DMA_SetCurrDataCounter(MPU6050_DMA_Channel, 14);

  /* While the bus is busy */
  while(I2C_GetFlagStatus(MPU6050_I2C, I2C_FLAG_BUSY));

  /* Enable DMA NACK automatic generation */
  I2C_DMALastTransferCmd(MPU6050_I2C, ENABLE);                    //Note this one, very important

  /* Send START condition */
I2C_GenerateSTART(MPU6050_I2C, ENABLE);

  /* Test on EV5 and clear it */
  while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_MODE_SELECT));

  /* Send MPU6050 address for write */
  I2C_Send7bitAddress(MPU6050_I2C, slaveAddr, I2C_Direction_Transmitter); 

  /* Test on EV6 and clear it */
  while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

  /* Clear EV6 by setting again the PE bit */
  I2C_Cmd(MPU6050_I2C, ENABLE);

  /* Send the MPU6050's internal address to write to */
  I2C_SendData(MPU6050_I2C, readAddr);

  /* Test on EV8 and clear it */
  while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

  /* Send STRAT condition a second time */
  I2C_GenerateSTART(MPU6050_I2C, ENABLE);

  /* Test on EV5 and clear it */
  while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_MODE_SELECT));

  /* Send MPU6050 address for read */
  I2C_Send7bitAddress(MPU6050_I2C, slaveAddr, I2C_Direction_Receiver);

  /* Test on EV6 and clear it */
  while(!I2C_CheckEvent(MPU6050_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

  /* Start DMA to receive data from I2C */
  DMA_Cmd(MPU6050_DMA_Channel, ENABLE);
  I2C_DMACmd(MPU6050_I2C, ENABLE);

  // When the data transmission is complete, it will automatically jump to DMA interrupt routine to finish the rest.
  //now go back to the main routine
}

And DMA interrupts routine:

void DMA1_Channel7_IRQHandler(void)  
  {
  if (DMA_GetFlagStatus(DMA1_FLAG_TC7))
    {
    /* Clear transmission complete flag */
    DMA_ClearFlag(DMA1_FLAG_TC7);

    I2C_DMACmd(MPU6050_I2C, DISABLE);
    /* Send I2C1 STOP Condition */
    I2C_GenerateSTOP(MPU6050_I2C, ENABLE);
    /* Disable DMA channel*/
    DMA_Cmd(MPU6050_DMA_Channel, DISABLE);

    //Read Accel data from byte 0 to byte 2
    for(i=0; i<3; i++) 
      AccelGyro[i]=((s16)((u16)I2C_Rx_Buffer[2*i] << 8) + I2C_Rx_Buffer[2*i+1]);
      //Skip byte 3 of temperature data
    //Read Gyro data from byte 4 to byte 6
    for(i=4; i<7; i++)
      AccelGyro[i-1]=((s16)((u16)I2C_Rx_Buffer[2*i] << 8) + I2C_Rx_Buffer[2*i+1]);    
    }
  }

Now, the reading sequence will be done automatically and stored into AccelGyro variable with the minimum time needed. I have also done a timing test to check how efficient this method could be. The following figures show the timing consumption of two methods: regular reading and DMA reading.

mpu6050_i2c
Regular I2C reading
mpu6050_i2c_dma
DMA supported reading

Channel 3 in both figures shows the timing period when the CPU is dealing with I2C reading. With the normal way of reading I2C data, the CPU is busy for the whole period and cannot do anything else. This could lead to a delay in reading other sensor data as well. By using DMA, we can free the CPU to do another task as DMA handles all the reading parts from the I2C peripheral.

Hope you can find this article useful and let’s wait for more to come :D