STM32F4 I2C lee con la menor sobrecarga

1

Tengo un sensor conectado a través de I2C, donde leo datos en una dirección constante. El uC es un STM32F410CB con 100MHz. Un circuito de control se ejecuta a aproximadamente 16 kHz sin leer el sensor y cae a 5 kHz cuando siempre leo el sensor. I2C está configurado en el modo rápido (400kHz).

Así que decidí usar el DMA, donde hay una función disponible en las bibliotecas HAL que parecen bastante prometedoras:

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

Así que implementé esta función, que también se denomina en todos los ciclos que se ejecuta el ciclo de control. La frecuencia del bucle de control aumentó a 7 kHz ahora, pero aún no es suficiente para mi aplicación. Solo se generan 3 interrupciones para la lectura, no sé de dónde proviene esta gran sobrecarga. Cuando solo llamo a la función cada tres veces, el bucle de control se ejecuta dos veces a 16 kHz, luego una vez a 7 kHz. Necesito un resultado más estable. ¿Hay alguna forma de leer un sensor completamente autónomo a través de I2C en una dirección de registro constante, de manera que casi no se genere una sobrecarga?

/**
  * @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;
  }
}

Editar: Aquí está el código para la lectura I2C:

Nivel bajo:

//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;
}

Capa superior:

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];
}

Inic. I2C:

/* 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__);
  }

}

Edición 2: Por cierto, la desaceleración ocurre solo cuando se llama a HAL_DMA_Start_IT (..) dentro de HAL_I2C_Mem_Read_DMA (..). Por lo tanto, es probable que esté relacionado de alguna manera con las interrupciones y con lo que sucede dentro.

    
pregunta HansPeterLoft

4 respuestas

2

No, la implementación del STM32F4 I2C no permite una operación completamente autónoma.

Hay una máquina de estado que debe implementarse en el software para que el periférico haga su trabajo. Esto se puede hacer con interrupciones o con sondeo. Desde mi experiencia manejándolo con resultados de sondeo en una operación más robusta. Por supuesto, el sondeo tiene el problema de que su controlador no está haciendo un trabajo útil, pero si su sistema solo está esperando nuevos valores de entrada, no importaría.

Puede implementarlo usted mismo para intentar solucionar su problema. Si vas por ese camino, prepárate para algunas dificultades, ya que el periférico I2C no es una obra maestra en términos de facilidad de uso.

Bajar esa ruta también le permite obtener el reloj I2C a más de 400 kHz; está fuera de la especificación normal, pero hay una nota de que puede obtener detalles de ST sobre cómo ejecutarlo a 1 MHz. Les pregunté y nunca obtuve retroalimentación, así que jugué un poco hasta que terminé con 800 kHz a 8 MHz. Supongo que con 100 MHz deberías obtener 1 MHz con bastante facilidad.

    
respondido por el Arsenal
2

Primero de todo: 16 bits + 2x dirección (16 bits) + memoria leída dirección 8 bits + ACK / NACK * 4 Creo que + 2xSTART + 1xSTOP (si no me perdí nada), lo que hace aproximadamente 40 relojes. 400kHz / 40 ~ = 10k secuencias / segundo.

La lectura de dos bytes en el uso de DMA es inútil. Probablemente tomará más tiempo configurarlo usando HAL y procesando las devoluciones de llamada que la transferencia en sí.

Conclusión: no puedes alcanzar esta tasa utilizando I2C. No utilice HAL. Es mucho más fácil y eficiente si lo programa de forma sencilla.

    
respondido por el P__J__
1

La velocidad de datos I2C probablemente no sea lo suficientemente alta como para admitir la frecuencia de muestreo deseada. Al observar su código, parece que está leyendo dos bytes de datos ( ENC_ADDR y RAW_ANGLE_ADDR ). Combine eso con la dirección y el byte R / W da 24 bits de datos. Ahora, 400 kHz / 24 es de 16.7 kHz, por lo que es ajustado incluso sin sobrecarga. Yo sugeriría tratar de muestrear confiablemente en, digamos, 8 kHz. Alternativamente, mirando la hoja de datos del AS560, soporta I2C de 1 MHz; si el microcontrolador lo admite, podría aumentar la velocidad de datos.

    
respondido por el awjlogan
0

No estoy familiarizado con el chip y la biblioteca ST, pero estoy traduciendo de lo que sé de otro sistema.

Según el código, particularmente de las secuencias de bloqueo y desbloqueo, parece que se está utilizando en un RTOS, que probablemente sea el marco de ST para su chip.

Los RTOS son excelentes, pero necesitan cierta comprensión para manejar el código que debe ejecutarse rápidamente. A veces, si el código es bastante simple y necesita velocidad, es mejor evitar el uso del RTOS.

Básicamente, RTOS es un programador de tareas (un mini kernel), y cada vez que necesite cambiar de una tarea a otra, utilizará algo de tiempo.

Lo que sucede en este código, es que solicita el identificador en el puerto I2C, ya que espera, probablemente ejecutará otras tareas. Si ese puerto es usado por otras tareas, debe esperar hasta que se complete esa otra tarea.

Dependiendo de cómo esté cableado su hardware, si la línea I2C solo se usa para este sensor, puede omitir la solicitud de manejo de I2C, abrir el I2C una vez y luego seguir leyendo, probablemente se optimizará un poco. También puede poner este proceso en una tarea específica con alta prioridad.

Si es usado por otro chip al que necesita acceder, también puede guardar todo en una sola tarea y manejarse cuando se llame a ese chip, evitando también tener que desbloquear / bloquear el bus I2C.

EDIT

Ya que no es un RTOS y agrega un código a tu pregunta:

  • Intente volver a escribir el controlador I2C para evitar todos los mutex.
  • No estoy seguro de que el DMA sea una ganancia allí, ya que parece que tienes que manejar el byte por byte del paquete de todos modos, pierde sentido.
  • Intente evitar las interrupciones, el tiempo de procesamiento del procesador en interrupción puede ser bastante largo y siempre es más rápido encuestarlo desde el bucle principal para evitar el cambio de contexto.
respondido por el Damien

Lea otras preguntas en las etiquetas