Comportamiento extraño de la bandera ocupada de I2C

He estado usando STMCUBE combinado con Keil desde hace algún tiempo. En general, me gusta la biblioteca HAL y la documentación para los controladores STM32f1xx es bastante buena.

Estoy haciendo un proyecto en el que estoy usando la tarjeta STM32f103rb Nucleo combinada con un giroscopio/acelerómetro MPU6050. Utilizo la herramienta de generación de código STM32CubeMX para generar la función de inicio. Sin embargo, cuando quiero implementar I2C, tengo un problema extraño. STM32Cube genera todos los pasos de iniciación necesarios, el controlador se configura, luego los pines GPIO se configuran como OD, luego, finalmente, el reloj se habilita usando la macro __HAL_RCC_I2C1_CLK_ENABLE(), sin embargo, cuando esta macro se ejecuta dentro del HAL_I2C_MspInit, el indicador I2C ocupado parece estar configurado y no se borra , por lo tanto, no puedo comunicarme con el dispositivo MPU6050.

Me di cuenta de que si coloco algo (por ejemplo, una sonda de medición) en la línea SDA mientras __HAL_RCC_I2C1_CLK_ENABLE()se ejecuta la macro, el indicador de ocupado no se configura y mi comunicación I2C funciona hasta que reinicio el microcontrolador.

Otra forma (¿mejor que poner una sonda física?) que parece funcionar es que después __HAL_RCC_I2C1_CLK_ENABLE()de ejecutar la macro, uso macros __HAL_RCC_I2C1_FORCE_RESET()y __HAL_RCC_I2C1_RELEASE_RESET(). De esta manera mi comunicación I2C funciona bien.

Creo que es extraño y realmente no puedo explicar el comportamiento. Pero desde que agregué las macros de reinicio forzado y reinicio de liberación, no he tenido ningún problema de I2C, funciona perfectamente.

Avíseme si necesito compartir más código.

¿Las resistencias pull up I2C están bien?
Sí, estoy usando pullups de 2.7k, funciona bien
¿Cuál es el estado de la línea SDA inicialmente y después de tocarla con la sonda? ¿Qué cambio se produce?
Inicialmente es alto, hay un "pico" corto que hace que sea bajo cuando tengo la sonda conectada durante CLK_ENABLE, luego vuelve a ser alto
Estoy pensando si la sonda logra simular una condición de parada, lo que borra el indicador de ocupado i2c. Pero luego me pregunto por qué el bus está ocupado en primer lugar, ya que no se ha ejecutado nada más que la función init. Y si tengo razón, ¿cómo es que las macros FORCE_RESET y RELEASE_RESET logran hacerlo y es una solución estable?

Respuestas (5)

ST ha publicado una hoja de erratas llamada:

Limitaciones del dispositivo de línea de valor de alta densidad STM32F100xC, STM32F10 0xD y STM32F100xE.

El punto interesante aquí es:

2.9.7 El filtro analógico I2C puede proporcionar un valor incorrecto, bloqueando el indicador OCUPADO e impidiendo la entrada al modo maestro

Hay una solución alternativa detallada de 15 pasos que funcionó para mí, sorprendentemente para un STM32F446, por lo que los periféricos I2C de cada serie STM32 CORTEX-M podrían verse afectados.

Durante esta operación, los miembros del autobús no deben subir o bajar activamente las líneas. Entonces, si conecta dos interfaces I2C de la misma MCU al bus, primero configure los pines de ambas en Función alternativa/Drenaje abierto, luego llame a la rutina, ya que se requiere una transición de niveles lógicos.

Aquí hay un ejemplo con las bibliotecas HAL que uso después de la primera inicialización y durante el tiempo de ejecución, si ocurre un error. Como se dijo anteriormente, esto es para STM32F4, las bibliotecas para SMT32F1 pueden diferir un poco.

struct I2C_Module
{
  I2C_HandleTypeDef   instance;
  uint16_t            sdaPin;
  GPIO_TypeDef*       sdaPort;
  uint16_t            sclPin;
  GPIO_TypeDef*       sclPort;
};

void I2C_ClearBusyFlagErratum(struct I2C_Module* i2c)
{
  GPIO_InitTypeDef GPIO_InitStructure;

  // 1. Clear PE bit.
  i2c->instance.Instance->CR1 &= ~(0x0001);

  //  2. Configure the SCL and SDA I/Os as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
  GPIO_InitStructure.Mode         = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStructure.Alternate    = I2C_PIN_MAP;
  GPIO_InitStructure.Pull         = GPIO_PULLUP;
  GPIO_InitStructure.Speed        = GPIO_SPEED_FREQ_HIGH;

  GPIO_InitStructure.Pin          = i2c->sclPin;
  HAL_GPIO_Init(i2c->sclPort, &GPIO_InitStructure);
  HAL_GPIO_WritePin(i2c->sclPort, i2c->sclPin, GPIO_PIN_SET);

  GPIO_InitStructure.Pin          = i2c->sdaPin;
  HAL_GPIO_Init(i2c->sdaPort, &GPIO_InitStructure);
  HAL_GPIO_WritePin(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_SET);

  // 3. Check SCL and SDA High level in GPIOx_IDR.
  while (GPIO_PIN_SET != HAL_GPIO_ReadPin(i2c->sclPort, i2c->sclPin))
  {
    asm("nop");
  }

  while (GPIO_PIN_SET != HAL_GPIO_ReadPin(i2c->sdaPort, i2c->sdaPin))
  {
    asm("nop");
  }

  // 4. Configure the SDA I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
  HAL_GPIO_WritePin(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_RESET);

  //  5. Check SDA Low level in GPIOx_IDR.
  while (GPIO_PIN_RESET != HAL_GPIO_ReadPin(i2c->sdaPort, i2c->sdaPin))
  {
    asm("nop");
  }

  // 6. Configure the SCL I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
  HAL_GPIO_WritePin(i2c->sclPort, i2c->sclPin, GPIO_PIN_RESET);

  //  7. Check SCL Low level in GPIOx_IDR.
  while (GPIO_PIN_RESET != HAL_GPIO_ReadPin(i2c->sclPort, i2c->sclPin))
  {
    asm("nop");
  }

  // 8. Configure the SCL I/O as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
  HAL_GPIO_WritePin(i2c->sclPort, i2c->sclPin, GPIO_PIN_SET);

  // 9. Check SCL High level in GPIOx_IDR.
  while (GPIO_PIN_SET != HAL_GPIO_ReadPin(i2c->sclPort, i2c->sclPin))
  {
    asm("nop");
  }

  // 10. Configure the SDA I/O as General Purpose Output Open-Drain , High level (Write 1 to GPIOx_ODR).
  HAL_GPIO_WritePin(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_SET);

  // 11. Check SDA High level in GPIOx_IDR.
  while (GPIO_PIN_SET != HAL_GPIO_ReadPin(i2c->sdaPort, i2c->sdaPin))
  {
    asm("nop");
  }

  // 12. Configure the SCL and SDA I/Os as Alternate function Open-Drain.
  GPIO_InitStructure.Mode         = GPIO_MODE_AF_OD;
  GPIO_InitStructure.Alternate    = I2C_PIN_MAP;

  GPIO_InitStructure.Pin          = i2c->sclPin;
  HAL_GPIO_Init(i2c->sclPort, &GPIO_InitStructure);

  GPIO_InitStructure.Pin          = i2c->sdaPin;
  HAL_GPIO_Init(i2c->sdaPort, &GPIO_InitStructure);

  // 13. Set SWRST bit in I2Cx_CR1 register.
  i2c->instance.Instance->CR1 |= 0x8000;

  asm("nop");

  // 14. Clear SWRST bit in I2Cx_CR1 register.
  i2c->instance.Instance->CR1 &= ~0x8000;

  asm("nop");

  // 15. Enable the I2C peripheral by setting the PE bit in I2Cx_CR1 register
  i2c->instance.Instance->CR1 |= 0x0001;

  // Call initialization function.
  HAL_I2C_Init(&(i2c->instance));
}
Mismo problema con F427, la solución parece funcionar.
Como ya no pude reproducir el error (cambié de compañía, el proyecto actual usa solo el software I2C) nunca pude probar la opción de simplemente desactivar los filtros analógicos configurando el bitflag ANOFF en el registro FLTR antes de activar el periférico I2C, pero al menos Quería mencionarlo.

Esto es menos una respuesta y más una advertencia ... Me quedé atascado con el mismo problema y el código anterior me ayudó (junto con estirar el reinicio de I2C e inicializar el reloj antes que el GPIO; vea las respuestas vinculadas).

Sin embargo, dediqué aproximadamente medio día más de lo que necesitaba debido a un error de novato: cuando reinicia el STM32, en mi caso con un programador externo conectado a través de SwD, generalmente no reinicia su periférico I2C.

Si agrega el código anterior, o alguna otra solución, recuerde probar ocasionalmente un ciclo de encendido o un restablecimiento completo de su placa para que los periféricos I2C también se restablezcan.

Basado en el código publicado por @Hugo-Arganda y @AxelBe. Tengo un STM32F051 y parece que tengo el mismo problema. ¡La solución parece resolver esto también!

Para su código: no veo de dónde obtiene la definición de pin/puerto. En mi opinión, este uso de estructura no debería funcionar en absoluto, ¡ya que HAL no le proporciona esa información!

Vea mi código revisado a continuación. Me deshice de su estructura y simplemente uso definiciones de puerto/pin codificadas.

Sugerencia: si usa CubeMX y etiqueta las líneas SDA/SCL manualmente, obtiene esas definiciones automáticamente:

#define I2C1_SCL_Pin GPIO_PIN_6
#define I2C1_SCL_GPIO_Port GPIOB
#define I2C1_SDA_Pin GPIO_PIN_7
#define I2C1_SDA_GPIO_Port GPIOB

Código revisado:

static bool wait_for_gpio_state_timeout(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState state, uint32_t timeout)
 {
    uint32_t Tickstart = HAL_GetTick();
    bool ret = true;
    /* Wait until flag is set */
    for(;(state != HAL_GPIO_ReadPin(port, pin)) && (true == ret);)
    {
        /* Check for the timeout */
        if (timeout != HAL_MAX_DELAY)
        {
            if ((timeout == 0U) || ((HAL_GetTick() - Tickstart) > timeout))
            {
                ret = false;
            }
            else
            {
            }
        }
        asm("nop");
    }
    return ret;
}


static void I2C_ClearBusyFlagErratum(I2C_HandleTypeDef* handle, uint32_t timeout)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    // 1. Clear PE bit.
    CLEAR_BIT(handle->Instance->CR1, I2C_CR1_PE);

    //  2. Configure the SCL and SDA I/Os as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
    HAL_I2C_DeInit(handle);

    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStructure.Pull = GPIO_NOPULL;

    GPIO_InitStructure.Pin = I2C1_SCL_Pin;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);

    GPIO_InitStructure.Pin = I2C1_SDA_Pin;
    HAL_GPIO_Init(I2C1_SDA_GPIO_Port, &GPIO_InitStructure);

    // 3. Check SCL and SDA High level in GPIOx_IDR.
    HAL_GPIO_WritePin(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, GPIO_PIN_SET);

    wait_for_gpio_state_timeout(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, GPIO_PIN_SET, timeout);
    wait_for_gpio_state_timeout(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, GPIO_PIN_SET, timeout);

    // 4. Configure the SDA I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
    HAL_GPIO_WritePin(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, GPIO_PIN_RESET);

    // 5. Check SDA Low level in GPIOx_IDR.
    wait_for_gpio_state_timeout(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, GPIO_PIN_RESET, timeout);

    // 6. Configure the SCL I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
    HAL_GPIO_WritePin(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, GPIO_PIN_RESET);

    // 7. Check SCL Low level in GPIOx_IDR.
    wait_for_gpio_state_timeout(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, GPIO_PIN_RESET, timeout);

    // 8. Configure the SCL I/O as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
    HAL_GPIO_WritePin(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, GPIO_PIN_SET);

    // 9. Check SCL High level in GPIOx_IDR.
    wait_for_gpio_state_timeout(I2C1_SCL_GPIO_Port, I2C1_SCL_Pin, GPIO_PIN_SET, timeout);

    // 10. Configure the SDA I/O as General Purpose Output Open-Drain , High level (Write 1 to GPIOx_ODR).
    HAL_GPIO_WritePin(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, GPIO_PIN_SET);

    // 11. Check SDA High level in GPIOx_IDR.
    wait_for_gpio_state_timeout(I2C1_SDA_GPIO_Port, I2C1_SDA_Pin, GPIO_PIN_SET, timeout);

    // 12. Configure the SCL and SDA I/Os as Alternate function Open-Drain.
    GPIO_InitStructure.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStructure.Alternate = GPIO_AF1_I2C1;

    GPIO_InitStructure.Pin = I2C1_SCL_Pin;
    HAL_GPIO_Init(I2C1_SCL_GPIO_Port, &GPIO_InitStructure);

    GPIO_InitStructure.Pin = I2C1_SDA_Pin;
    HAL_GPIO_Init(I2C1_SDA_GPIO_Port, &GPIO_InitStructure);

    // 13. Set SWRST bit in I2Cx_CR1 register.
    SET_BIT(handle->Instance->CR1, I2C_CR1_SWRST);
    asm("nop");

    /* 14. Clear SWRST bit in I2Cx_CR1 register. */
    CLEAR_BIT(handle->Instance->CR1, I2C_CR1_SWRST);
    asm("nop");

    /* 15. Enable the I2C peripheral by setting the PE bit in I2Cx_CR1 register */
    SET_BIT(handle->Instance->CR1, I2C_CR1_PE);
    asm("nop");

    // Call initialization function.
    HAL_I2C_Init(handle);
}

Basado en la respuesta de AxelBe, creé esta versión de la solución, sin bloquear las operaciones y agregando el DeInit de la instancia I2C. Espero que encuentres esto útil.

typedef struct
 {
    I2C_HandleTypeDef* instance;
    uint16_t sdaPin;
    GPIO_TypeDef* sdaPort;
    uint16_t sclPin;
    GPIO_TypeDef* sclPort;
} I2C_Module_t;

static uint8_t wait_for_gpio_state_timeout(GPIO_TypeDef *port, uint16_t pin, GPIO_PinState state, uint32_t timeout)
 {
    uint32_t Tickstart = HAL_GetTick();
    uint8_t ret = TRUE;
    /* Wait until flag is set */
    for(;(state != HAL_GPIO_ReadPin(port, pin)) && (TRUE == ret);)
    {
        /* Check for the timeout */
        if (timeout != HAL_MAX_DELAY)
        {
            if ((timeout == 0U) || ((HAL_GetTick() - Tickstart) > timeout))
            {
                ret = FALSE;
            }
            else
            {
            }
        }
        asm("nop");
    }
    return ret;
}

static void I2C_ClearBusyFlagErratum(I2C_Module_t* i2c, uint32_t timeout)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    I2C_HandleTypeDef* handler = NULL;

    handler = i2c->instance;

    // 1. Clear PE bit.
    CLEAR_BIT(handler->Instance->CR1, I2C_CR1_PE);

    //  2. Configure the SCL and SDA I/Os as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
    HAL_I2C_DeInit(handler);

    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStructure.Pull = GPIO_NOPULL;

    GPIO_InitStructure.Pin = i2c->sclPin;
    HAL_GPIO_Init(i2c->sclPort, &GPIO_InitStructure);

    GPIO_InitStructure.Pin = i2c->sdaPin;
    HAL_GPIO_Init(i2c->sdaPort, &GPIO_InitStructure);

    // 3. Check SCL and SDA High level in GPIOx_IDR.
    HAL_GPIO_WritePin(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(i2c->sclPort, i2c->sclPin, GPIO_PIN_SET);

    wait_for_gpio_state_timeout(i2c->sclPort, i2c->sclPin, GPIO_PIN_SET, timeout);
    wait_for_gpio_state_timeout(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_SET, timeout);

    // 4. Configure the SDA I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
    HAL_GPIO_WritePin(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_RESET);

    // 5. Check SDA Low level in GPIOx_IDR.
    wait_for_gpio_state_timeout(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_RESET, timeout);

    // 6. Configure the SCL I/O as General Purpose Output Open-Drain, Low level (Write 0 to GPIOx_ODR).
    HAL_GPIO_WritePin(i2c->sclPort, i2c->sclPin, GPIO_PIN_RESET);

    // 7. Check SCL Low level in GPIOx_IDR.
    wait_for_gpio_state_timeout(i2c->sclPort, i2c->sclPin, GPIO_PIN_RESET, timeout);

    // 8. Configure the SCL I/O as General Purpose Output Open-Drain, High level (Write 1 to GPIOx_ODR).
    HAL_GPIO_WritePin(i2c->sclPort, i2c->sclPin, GPIO_PIN_SET);

    // 9. Check SCL High level in GPIOx_IDR.
    wait_for_gpio_state_timeout(i2c->sclPort, i2c->sclPin, GPIO_PIN_SET, timeout);

    // 10. Configure the SDA I/O as General Purpose Output Open-Drain , High level (Write 1 to GPIOx_ODR).
    HAL_GPIO_WritePin(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_SET);

    // 11. Check SDA High level in GPIOx_IDR.
    wait_for_gpio_state_timeout(i2c->sdaPort, i2c->sdaPin, GPIO_PIN_SET, timeout);

    // 12. Configure the SCL and SDA I/Os as Alternate function Open-Drain.
    GPIO_InitStructure.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStructure.Alternate = GPIO_AF4_I2C2;

    GPIO_InitStructure.Pin = i2c->sclPin;
    HAL_GPIO_Init(i2c->sclPort, &GPIO_InitStructure);

    GPIO_InitStructure.Pin = i2c->sdaPin;
    HAL_GPIO_Init(i2c->sdaPort, &GPIO_InitStructure);

    // 13. Set SWRST bit in I2Cx_CR1 register.
    SET_BIT(handler->Instance->CR1, I2C_CR1_SWRST);
    asm("nop");

    /* 14. Clear SWRST bit in I2Cx_CR1 register. */
    CLEAR_BIT(handler->Instance->CR1, I2C_CR1_SWRST);
    asm("nop");

    /* 15. Enable the I2C peripheral by setting the PE bit in I2Cx_CR1 register */
    SET_BIT(handler->Instance->CR1, I2C_CR1_PE);
    asm("nop");

    // Call initialization function.
    HAL_I2C_Init(handler);
}

Ejecuto esto tan pronto como obtengo la respuesta HAL_BUSY en las funciones I2C HAL, aquí hay un ejemplo:

    /* HAL Write */
    status = HAL_I2C_Mem_Write(eeprom_handler.instance, (uint16_t)device_address, mem_addr_masked, I2C_MEMADD_SIZE_8BIT, src, bytes_to_write, 1000);
    if (HAL_OK == status)
    {
        bytes_written = bytes_to_write;
    }
    else if (HAL_BUSY == status)
    {
        I2C_ClearBusyFlagErratum(&eeprom_handler, 1000);
    }

El filtro analógico I2C puede proporcionar un valor incorrecto, bloqueando el indicador OCUPADO e impidiendo la entrada al modo maestro

Los filtros analógicos I2C integrados en las E/S I2C pueden vincularse a un nivel bajo, mientras que las líneas SCL y SDA se mantienen a un nivel alto. Esto puede ocurrir después de un reinicio de encendido de MCU o durante el estrés de ESD. En consecuencia, se establece el indicador I2C BUSY y el I2C no puede ingresar al modo maestro (no se puede enviar la condición de INICIO). El indicador I2C BUSY no se puede borrar con el bit de control SWRST, ni con un reinicio periférico o del sistema. El bit BUSY se borra durante el reinicio, pero vuelve a establecerse alto tan pronto como se libera el reinicio, porque la salida del filtro analógico todavía está en un nivel bajo. Este problema ocurre aleatoriamente.

Nota : En las mismas condiciones, los filtros analógicos I2C también pueden proporcionar un nivel alto, mientras que las líneas SCL y SDA se mantienen en un nivel bajo. Esto no debería crear problemas ya que la salida de los filtros será correcta después de la próxima transición SCL y SDA.

Solución alternativa La salida del filtro analógico SCL y SDA se actualiza después de que ocurre una transición en la línea SCL y SDA respectivamente. La transición SCL y SDA se puede forzar mediante software que configura las E/S I2C en modo de salida. Luego, una vez que los filtros analógicos se desbloquean y emiten el nivel de las líneas SCL y SDA, el indicador BUSY se puede restablecer con un reinicio de software y el I2C puede ingresar al modo maestro. Por lo tanto, se debe aplicar la siguiente secuencia:

1. Deshabilite el periférico I2C borrando el bit PE en el registro I2Cx_CR1.

2. Configure las E/S SCL y SDA como salida de propósito general, drenaje abierto, nivel alto (Escriba 1 en GPIOx_ODR).

3. Compruebe el nivel alto de SCL y SDA en GPIOx_IDR.

4. Configure la E/S de SDA como salida de uso general, drenaje abierto, nivel bajo (escriba 0 en GPIOx_ODR).

5. Compruebe el nivel bajo de SDA en GPIOx_IDR.

6. Configure SCL I/O como salida de uso general, drenaje abierto, nivel bajo (escriba 0 en GPIOx_ODR).

7. Compruebe el nivel bajo de SCL en GPIOx_IDR.

8. Configure el SCL I/O como salida de uso general, drenaje abierto, nivel alto (Escriba 1 en GPIOx_ODR).

9. Compruebe el nivel alto de SCL en GPIOx_IDR.

10. Configure la E/S SDA como salida de propósito general, drenaje abierto, nivel alto (Escriba 1 en GPIOx_ODR).

11. Compruebe el nivel alto de SDA en GPIOx_IDR.

12. Configure las E/S SCL y SDA como función alternativa Open-Drain.

13. Configure el bit SWRST en el registro I2Cx_CR1.

14. Borre el bit SWRST en el registro I2Cx_CR1.

15. Habilite el periférico I2C configurando el bit PE en el registro I2Cx_CR1.

más

Gracias, pero esto es solo copiar y pegar de un documento de errata STM32 (en realidad no es el que se aplica al STM32F103 mencionado en la pregunta). Es la misma solución ya mencionada (con otro documento de errata vinculado y con un código de muestra) en otra respuesta a esta pregunta de hace 1,5 años aquí .
Sí, esto es copiar y pegar :), pero esta es una respuesta perfecta para esta pregunta. Este fue mi problema hoy.