1
\$\begingroup\$

I have a sensor connected over I2C, where I read out data at a constant address. The uC is an STM32F410CB with 100MHz. A control loop runs at around 16kHz without reading out the sensor and drops to 5 kHz when I always read out the sensor. I2C is configured in the fast mode (400kHz).

So I decided to use the DMA, where a function is available in the HAL libraries that look pretty promising:

HAL_I2C_Mem_Read_DMA(i2c1,addr,addr_mem,addr_mem_size,data,data_size);

So I implemented this function, which is also called every cycle the control loop runs. The frequency of the control loop increased to 7kHz now, but that's still not enough for my application. Only 3 Interrupts are generated for the read out, I don't know where this huge overhead comes from. When I only call the function every third time, the control loop runs twice at 16kHz, then once at 7kHz. I need a more stable result. Is there a way to fully autonomous read out a sensor over I2C at a constant register address, such that nearly no overhead is generated?

/**
  * @brief  Reads an amount of data in non-blocking mode with DMA from a specific memory address.
  * @param  hi2c Pointer to a I2C_HandleTypeDef structure that contains
  *                the configuration information for the specified I2C.
  * @param  DevAddress Target device address
  * @param  MemAddress Internal memory address
  * @param  MemAddSize Size of internal memory address
  * @param  pData Pointer to data buffer
  * @param  Size Amount of data to be read
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_I2C_Mem_Read_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size)
{
  uint32_t tickstart = 0x00U;
  __IO uint32_t count = 0U;

  /* Init tickstart for timeout management*/
  tickstart = HAL_GetTick();

  /* Check the parameters */
  assert_param(IS_I2C_MEMADD_SIZE(MemAddSize));

  if(hi2c->State == HAL_I2C_STATE_READY)
  {
    /* Wait until BUSY flag is reset */
    count = I2C_TIMEOUT_BUSY_FLAG * (SystemCoreClock /25U /1000U);
    do
    {
      if(count-- == 0U)
      {
        hi2c->PreviousState = I2C_STATE_NONE;
        hi2c->State= HAL_I2C_STATE_READY;

        /* Process Unlocked */
        __HAL_UNLOCK(hi2c);

        return HAL_TIMEOUT; 
      }
    }
    while(__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BUSY) != RESET);

    /* Process Locked */
    __HAL_LOCK(hi2c);

    /* Check if the I2C is already enabled */
    if((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE)
    {
      /* Enable I2C peripheral */
      __HAL_I2C_ENABLE(hi2c);
    }

    /* Disable Pos */
    hi2c->Instance->CR1 &= ~I2C_CR1_POS;

    hi2c->State     = HAL_I2C_STATE_BUSY_RX;
    hi2c->Mode      = HAL_I2C_MODE_MEM;
    hi2c->ErrorCode = HAL_I2C_ERROR_NONE;

    /* Prepare transfer parameters */
    hi2c->pBuffPtr = pData;
    hi2c->XferCount = Size;
    hi2c->XferOptions = I2C_NO_OPTION_FRAME;
    hi2c->XferSize    = hi2c->XferCount;

    if(hi2c->XferSize > 0U)
    {
      /* Set the I2C DMA transfer complete callback */
      hi2c->hdmarx->XferCpltCallback = I2C_DMAXferCplt;

      /* Set the DMA error callback */
      hi2c->hdmarx->XferErrorCallback = I2C_DMAError;

      /* Set the unused DMA callbacks to NULL */
      hi2c->hdmarx->XferHalfCpltCallback = NULL;
      hi2c->hdmarx->XferM1CpltCallback = NULL;
      hi2c->hdmarx->XferM1HalfCpltCallback = NULL;
      hi2c->hdmarx->XferAbortCallback = NULL;

      /* Enable the DMA Stream */
      HAL_DMA_Start_IT(hi2c->hdmarx, (uint32_t)&hi2c->Instance->DR, (uint32_t)hi2c->pBuffPtr, hi2c->XferSize);

      /* Send Slave Address and Memory Address */
      if(I2C_RequestMemoryRead(hi2c, DevAddress, MemAddress, MemAddSize, I2C_TIMEOUT_FLAG, tickstart) != HAL_OK)
      {
        if(hi2c->ErrorCode == HAL_I2C_ERROR_AF)
        {
          /* Process Unlocked */
          __HAL_UNLOCK(hi2c);
          return HAL_ERROR;
        }
        else
        {
          /* Process Unlocked */
          __HAL_UNLOCK(hi2c);
          return HAL_TIMEOUT;
        }
      }

      if(Size == 1U)
      {
        /* Disable Acknowledge */
        hi2c->Instance->CR1 &= ~I2C_CR1_ACK;
      }
      else
      {
        /* Enable Last DMA bit */
        hi2c->Instance->CR2 |= I2C_CR2_LAST;
      }

      /* Clear ADDR flag */
      __HAL_I2C_CLEAR_ADDRFLAG(hi2c);

      /* Process Unlocked */
      __HAL_UNLOCK(hi2c);

      /* Note : The I2C interrupts must be enabled after unlocking current process
                to avoid the risk of I2C interrupt handle execution before current
                process unlock */
      /* Enable ERR interrupt */
      __HAL_I2C_ENABLE_IT(hi2c, I2C_IT_ERR);

     /* Enable DMA Request */
      hi2c->Instance->CR2 |= I2C_CR2_DMAEN;
    }
    else
    {
      /* Send Slave Address and Memory Address */
      if(I2C_RequestMemoryRead(hi2c, DevAddress, MemAddress, MemAddSize, I2C_TIMEOUT_FLAG, tickstart) != HAL_OK)
      {
        if(hi2c->ErrorCode == HAL_I2C_ERROR_AF)
        {
          /* Process Unlocked */
          __HAL_UNLOCK(hi2c);
          return HAL_ERROR;
        }
        else
        {
          /* Process Unlocked */
          __HAL_UNLOCK(hi2c);
          return HAL_TIMEOUT;
        }
      }

      /* Clear ADDR flag */
      __HAL_I2C_CLEAR_ADDRFLAG(hi2c);

      /* Generate Stop */
      hi2c->Instance->CR1 |= I2C_CR1_STOP;

      hi2c->State = HAL_I2C_STATE_READY;

      /* Process Unlocked */
      __HAL_UNLOCK(hi2c);
    }

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

Edit: Here is the code for the I2C readout:

Low Level:

//DMA I2C
err_t I2C_Driver_TXRX_DMA(uint16_t addr, uint16_t addr_mem, uint8_t* pData, uint16_t size) {
    addr = addr << 1;
    err_t err={.value=HAL_OK};

    err.value=HAL_I2C_Mem_Read_DMA(i2c1,addr,addr_mem,1,pData,size); //Error callback called in error case

    return err;
}

Higher Layer:

uint8_t buf_rx[2]={0};
uint16_t pos=0;

void triggerDMAMeasurementPosition() {
    if(getI2CErrorState()) {
        I2C_Error_Solver(ENC_ADDR);
        i2c_err=0;
    } else {
        I2C_Driver_TXRX_DMA(ENC_ADDR,RAW_ANGLE_ADDR,buf_rx,2);
    }
}

void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c) {
    pos=buf_rx[0]<<8 | buf_rx[1];
}

I2C Init:

/* I2C1 init function */
static void MX_I2C1_Init(void)
{

  hi2c1.Instance = I2C1;
  hi2c1.Init.ClockSpeed = 400000;
  hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
  hi2c1.Init.OwnAddress1 = 0;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

}

Edit 2: BTW, the slow down happens only when HAL_DMA_Start_IT(..) is called inside HAL_I2C_Mem_Read_DMA(..). So it is probably somehow related to the interrupts and what happens inside there.

\$\endgroup\$
10
  • \$\begingroup\$ It would help to have your code too - it's unlikely to be a HAL issue. Also, what's the sensor? \$\endgroup\$
    – awjlogan
    Oct 17, 2018 at 9:19
  • \$\begingroup\$ Can you change the sensor to a SPI version ? Are the I2C lines only used for this particular IC ? \$\endgroup\$
    – Damien
    Oct 17, 2018 at 9:20
  • \$\begingroup\$ SPI is no option, I know that would be pretty handy. The sensor is an AS5601 magnetic encoder, there are no cheap SPI versions available. The I2C is only used for this sensor. \$\endgroup\$ Oct 17, 2018 at 9:30
  • \$\begingroup\$ "So I implemented this function, which is also called every cycle the control loop runs...." This sound like your problem, depending on how much data you are sending and receiving it could easily prevent you to operate at 15 kHz. \$\endgroup\$
    – Rokta
    Oct 17, 2018 at 9:36
  • \$\begingroup\$ Well, when I call this function and the DMA is ongoing, it will just return a busy and continue, or at least it should? But somehow it always slows down to 7kHz when this function is called. \$\endgroup\$ Oct 17, 2018 at 9:40

6 Answers 6

2
\$\begingroup\$

No, the STM32F4 I2C implementation does not allow a fully autonomous operation.

There is a state machine which has to be implemented in software for the peripheral to do its work. This can either be done with interrupts or with polling. From my experience handling it with polling results in a more robust operation. Polling of course has the problem that your controller isn't doing useful work, but if your system is just waiting on new input values, it wouldn't matter.

You can implement that yourself to try and fix your problem. If you go down that road, be prepared for some hardship as the I2C peripheral is not a masterpiece in terms of usability.

Going down that route also allows you to get the I2C clock higher than 400 kHz - it is outside of the normal specification but there is a note that you can get details from ST on how to run it at 1 MHz. I asked them and never got feedback, so I fiddled around until I ended up with 800 kHz running from 8 MHz. I guess with 100 MHz you should get 1 MHz quite easily.

\$\endgroup\$
2
\$\begingroup\$

First of all - 16 bits + 2x address (16 bits) + memory read address 8 bits + ACK / NACK * 4 I think + 2xSTART + 1xSTOP (if I did not miss anything) which makes about 40 clocks. 400kHz / 40 ~= 10k sequences / second.

Reading two bytes in using DMA is useless. It will probably take more time setting it using HAL and processing the callbacks than the transfer itself.

Conclusion - you cant reach this rate using I2C. Do not use HAL. It is much easier and efficient if you program it the bare register way.

\$\endgroup\$
0
2
\$\begingroup\$

The HAL_I2C_Mem_Read_DMA function has blocking calls to send the slave address and memory address, and then only uses DMA for the data transfer. At 400 kHz it will block 45 microseconds to send those two bytes, which is quite a problem when your loop time is 62.5 microseconds!

The solution would be to rewrite the I2C driver so it can use interrupts for the addresses.

HAL I2C code: https://github.com/STMicroelectronics/STM32CubeF4/blob/master/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_i2c.c

Look at the function HAL_I2C_Mem_Read_DMA, which calls I2C_RequestMemoryRead, which calls several blocking functions.

\$\endgroup\$
1
\$\begingroup\$

The I2C data rate is probably not high enough to support your desired sampling frequency. Looking at your code, it seems you are reading two bytes of data (ENC_ADDR, and RAW_ANGLE_ADDR). Combine that with the address and R/W byte gives 24 bits of data. Now, 400 kHz / 24 is 16.7 kHz so it's tight even with no overhead. I would suggest trying to sample reliably at, say, 8 kHz. Alternatively, looking at the AS5601's datasheet, it supports 1 MHz I2C; if the microcontroller supports that, you could push the data rate up.

\$\endgroup\$
2
  • \$\begingroup\$ Thx, ye I think I really have to change to an SPI version, this will minimize the overhead. I2C is really not made for this I think ,but I wondered anyway what the best method is to read out a sensor over I2C with least overhead. \$\endgroup\$ Oct 17, 2018 at 11:27
  • \$\begingroup\$ Probably the way you are going is pretty close to minimal overhead - a lot of the HAL functions will be optimised out when not used. You could write a stripped down HAL as Damien suggests, an interesting exercise if nothing else. I2C inherently has quite a high overhead with the START:ADDR:[DATA]:ACK protocol, especially if DATA is short. \$\endgroup\$
    – awjlogan
    Oct 17, 2018 at 11:45
1
\$\begingroup\$

Minimum overhead is possible when using I2C in circular DMA mode with XferHalfCpltCallback XferCpltCallback callbacks.

For stm32f4 with HAL libraries: Configure I2C DMA to use circular mode. After that you can't use HAL I2C driver directly, because I2C_DMAXferCplt will stop DMA transfer. Make custom function based on HAL_I2C_Master_Receive_DMA and two callbacks XferHalfCpltCallback, XferCpltCallback. Then you can use memcpy or dma mem2mem to copy data from circular buffer to safe memory location for further processing in main loop, or process data directly in DMA transfer callback functions.

The sensor you use is AS5601, datasheet states:

These are special registers which suppress the automatic increment of the address pointer on reads, so a re-read of these registers requires no I²C write command to reload the address pointer. This special treatment of the pointer is effective only if the address pointer is set to the higher byte of the register, which holds the least significant bits.

It seems that this sensor is perfectly suited to perform continuous readout of all/single registers using circular DMA buffer.

\$\endgroup\$
0
0
\$\begingroup\$

I'm not familiar ST chip and library, but translating from what I know from other system.

Based on the code, particularly from the lock-unlock sequences, it seems it is being used in a RTOS, which is probably the framework of ST for their chip.

RTOS are great, but it needs some understanding for dealing with code that needs to run fast. Sometimes, if the code is fairly simple and you need speed, it is better to avoid using the RTOS.

RTOS basically is a task scheduler (a mini kernel), and every time it needs to switch from a task to another, it will use some time.

What happens in this code, is that it request the handle on the I2C port, as it wait, it probably will execute other tasks. If that port is used by other tasks, it needs to wait until that other task is done.

Depending on how your hardware is wired, if the I2C line is only used for this sensor, you can bypass the I2C handle request, open the I2C one time and then just keep reading it, this will probably optimize quite a bit. You can also put this process on a specific task with high priority.

If it's used by other chip you need to access, you can also keep everything in a single task, and handle yourself when which chip is called, also avoiding having to unlock / lock the I2C bus.


EDIT

Since it's not an RTOS and added code to your question:

  • Try rewriting the I2C driver to avoid all the mutexes.
  • I'm not sure the DMA is a gain in there, since you seem to have to handle the packet byte by byte anyway, it loose sense.
  • Try avoiding interrupts, processor time switching into interrupt can be quite long and it is always faster to poll it from the main loop to avoid context switch.
\$\endgroup\$
2
  • \$\begingroup\$ No RTOS is used in my project, but the code from HAL is probably optimized for a FreeRTOS etc. \$\endgroup\$ Oct 17, 2018 at 9:38
  • \$\begingroup\$ Either way, try bypassing the mutexes requests \$\endgroup\$
    – Damien
    Oct 17, 2018 at 9:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.