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\$
  • \$\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 '18 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 '18 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\$ – HansPeterLoft Oct 17 '18 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 '18 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\$ – HansPeterLoft Oct 17 '18 at 9:40
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\$
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\$
  • \$\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\$ – HansPeterLoft Oct 17 '18 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 '18 at 11:45
1
\$\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\$
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\$
  • \$\begingroup\$ No RTOS is used in my project, but the code from HAL is probably optimized for a FreeRTOS etc. \$\endgroup\$ – HansPeterLoft Oct 17 '18 at 9:38
  • \$\begingroup\$ Either way, try bypassing the mutexes requests \$\endgroup\$ – Damien Oct 17 '18 at 9:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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