Bare-Metal STM32: uso del bus I2C en modo de transceptor maestro

Como uno de los buses más populares hoy en día para la comunicación a bordo y entre tarjetas en los sistemas, existe una buena posibilidad de que termine usándolo con un sistema integrado. I2C ofrece una variedad de velocidades y solo requiere dos cables (reloj y datos), lo que lo hace significativamente más fácil de manejar que otras alternativas, como SPI. Dentro de la familia de MCU STM32, encontrará al menos una centralita I2C en cada dispositivo.

Como un entorno semidúplex común, I2C utiliza un diseño de llamada y respuesta bastante simple, donde un dispositivo verifica el reloj y otros dispositivos simplemente esperan y escuchan hasta que su dirección fija se envía en el bus I2C. Si bien la configuración de un periférico STM32 I2C implica algunos pasos, es bastante fácil de usar más adelante, como veremos en este artículo.

Pasos básicos

Suponiendo que los dispositivos receptores, como los sensores, estén cableados correctamente con las resistencias de extracción necesarias en su lugar, entonces podemos comenzar a configurar la periferia I2C de la MCU. Usaremos el STM32F042 como el MCU de destino, pero otras familias STM32 son bastante similares desde la perspectiva de I2C. También utilizaremos un periférico estilo CMSIS y registraremos referencias.

Primero, configuramos los pines GPIO que queremos usar para el I2C periférico, habilitando el modo de función alterna (AF) apropiado. Esto está documentado en la hoja de datos de la MCU de destino. Para el MCU STM23F042, el pin estándar SCL (reloj) está en PA11, con AF 5. SDA (datos) está ubicado en PA12, con el mismo AF. Para hacer esto necesitamos configurar los bits apropiados en el registro GPIO_AFRH (función de registro alto):

GPIO_AFRH en STM32F042 con valores AF.

Al seleccionar AF 5 para los pines 11 y 12 (AFSEL11 y AFSEL12), estos pines se conectan internamente a la primera periferia I2C (I2C1). Esto es similar a lo que hicimos en un artículo anterior sobre la UART. También necesitamos habilitar el modo AF para el pin en GPIO_MODER:

Diseño STM32F0x2 GPIO_MODER (RM0091, 8.4.4).

Todo esto se hace con el siguiente código:


uint8_t pin = 11;                // Repeat for pin 12
uint8_t pin2 = pin * 2;
GPIOA->MODER &= ~(0x3 << pin2); 
GPIOA->MODER |= (0x2 << pin2);   // Set AF mode.

// Set AF mode in appropriate (high/low) register.
if (pin < 8) { 
    uint8_t pin4 = pin * 4; 
    GPIOA->AFR[0] &= ~(0xF << pin4); 
    GPIOA->AFR[0] |= (af << pin4); 
} 
else { 
    uint8_t pin4 = (pin - 8) * 4; 
    GPIOA->AFR[1] &= ~(0xF << pin4); 
    GPIOA->AFR[1] |= (af << pin4);
}

Tenga en cuenta que queremos que los pines SCL y SDA estén configurados en los registros GPIO para estar en un estado flotante sin pullup o pulldown, y en una configuración de drenaje abierto. Esto es consistente con las características del bus I2C, que está diseñado para ser un drenaje abierto. Efectivamente, esto significa que los tirones en las líneas del bus mantienen la señal alta a menos que un dispositivo maestro o esclavo la baje.

El reloj para el primer periférico I2C está habilitado RCC_APB1ENR (habilitar registro) con:

RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;

Algunas MCU STM32F0 tienen solo una excéntrica I2C (STM32F03x y F04x), mientras que otras tienen dos. En cualquier caso, si existe la periferia I2C, después de configurar su bit de habilitación de reloj en este registro, ahora podemos proceder a configurar la periferia I2C como maestro.

Configuración del reloj

Antes de hacer cualquier otra cosa con el periférico I2C, debemos asegurarnos de que esté desactivado:

I2C1->CR1 &= ~I2C_CR1_PE;

Los ajustes del reloj están configurados. I2C_ TIMINGR:

Diseño I2C_TIMINGR, según RM0091 (26.7.5)

El manual de referencia enumera una serie de tablas con ajustes de hora, según el reloj I2C, p. para velocidad de reloj I2C de 8 MHz en STM32F0:

Tabla de ejemplo de configuración de IC2_TIMINGR. Fuente: RM0091, 26.4.10.

Esta matriz se puede convertir en una matriz de valores utilizable para configurar la periferia I2C, colocando estos valores en el orden correcto para ingresar en I2C_TIMINGR, p. por STM32F0:


uint32_t i2c_timings_4[4];
uint32_t i2c_timings_8[4];
uint32_t i2c_timings_16[4];
uint32_t i2c_timings_48[4];
uint32_t i2c_timings_54[4];

i2c_timings_4[0] = 0x004091F3;
i2c_timings_4[1] = 0x00400D10;
i2c_timings_4[2] = 0x00100002;
i2c_timings_4[3] = 0x00000001;
i2c_timings_8[0] = 0x1042C3C7;
i2c_timings_8[1] = 0x10420F13;
i2c_timings_8[2] = 0x00310309;
i2c_timings_8[3] = 0x00100306;
i2c_timings_16[0] = 0x3042C3C7;
i2c_timings_16[1] = 0x30420F13;
i2c_timings_16[2] = 0x10320309;
i2c_timings_16[3] = 0x00200204;
i2c_timings_48[0] = 0xB042C3C7;
i2c_timings_48[1] = 0xB0420F13;
i2c_timings_48[2] = 0x50330309;
i2c_timings_48[3] = 0x50100103;
i2c_timings_54[0] = 0xD0417BFF;
i2c_timings_54[1] = 0x40D32A31;
i2c_timings_54[2] = 0x10A60D20;
i2c_timings_54[3] = 0x00900916;

Las otras opciones disponibles aquí son dejar que las herramientas proporcionadas por STMicroelectronic (por ejemplo, CubeMX) calculen los valores por usted, o usar la información del manual de referencia para calcularlo usted mismo. En este momento, la implementación de I2C del marco de Nodate para STM32 utiliza ambos, con los valores predefinidos anteriores para STM32F0, y valores calculados dinámicamente para otras familias.

La ventaja de calcular dinámicamente los valores de tiempo es que no depende de las velocidades de reloj I2C predefinidas. Como desventaja, existe el retraso adicional que implica calcular esos valores, en lugar de leerlos directamente de una tabla. El enfoque que mejor funcione probablemente dependerá de los requisitos del proyecto.

Con el I2C_TIMINGR registro así configurado, podemos habilitar los periféricos:

I2C1->CR1 |= I2C_CR1_PE;

Escritura de datos

Con el periférico I2C listo y esperando, podemos comenzar a enviar datos. Al igual que con USART, esto se hace escribiendo en un registro de transmisión (TX) y esperando a que finalice la transmisión. Los pasos a seguir aquí están cubiertos en el útil diagrama de flujo proporcionado en el manual de referencia:

Diagrama de flujo del transmisor maestro, reproducido de RM0091.

Vale la pena señalar aquí que con algunas comprobaciones, como I2C_ISR_TC (transferencia completa), la idea no es comprobar una vez y terminar, sino esperar con un tiempo de espera.

Para una transferencia simple de 1 byte, configuraríamos I2C_CR2 de la siguiente manera:


I2C1->CR2 |= (slaveID << 1) | I2C_CR2_AUTOEND | (uint32_t) (1 << 16) | I2C_CR2_START;

Esto iniciaría la transferencia por un total de 1 byte (desplazado a la izquierda a la posición NBYTES en el registro I2C_CR2), apuntando a los 7 bits. slaveID, con la condición de parada I2C generada automáticamente. Después de que se realiza la transferencia (NBYTES entregados), se genera el STOP, que coloca una bandera en I2C_ISR llamada STOPF.

Una vez que sabemos que hemos terminado con la transferencia de datos, debemos esperar a que se configure este indicador, luego borrar el indicador en I2C_ICR y borrar el registro I2C_CR2:


instance.regs->ICR |= I2C_ICR_STOPCF;
instance.regs->CR2 = 0x0;

Esto completa una transmisión de datos básica. Para transmitir más de un byte, simplemente repite el mismo procedimiento, escribiendo un solo byte dentro I2C_TXDR cada ciclo y esperando I2C_ISR_TXIS sintonización (con el tiempo de espera requerido). Para transferir más de 255 bytes, configure I2C_CR2_RELOAD en cambio I2C_CR2_AUTOEND en I2C_CR2 permitirá transferir un nuevo conjunto de 255 bytes o menos.

Lectura de datos

Cuando lea los datos del dispositivo, asegúrese de que las interrupciones estén desactivadas (usando NVIC_DisableIRQ). Generalmente, el microcontrolador envía una solicitud de lectura al dispositivo, donde el dispositivo responde enviando el contenido del registro solicitado como respuesta. Por ejemplo, si se envía un sensor MEMS BME280 0xd0 como solo una carga útil, responderá devolviendo su ID (fijo) según lo programado en ese registro en la fábrica.

El diagrama de flujo básico para recibir desde un dispositivo se ve así:

Diagrama de flujo del receptor maestro para STM32F0. Fuente: RM0091.

La idea básica aquí es la misma que con la transmisión de datos. Configuramos I2C_CR2 de la misma manera que antes. Las principales diferencias aquí son que estamos esperando que se apague el indicador I2C_ISR_RXNE, después de lo cual podemos leer el contenido de un byte de I2C_RXDR en nuestro búfer.

Al igual que con la escritura de datos, después de leer NBYTES, debemos esperar a que se establezca el indicador I2C_ISR_STOPF, luego lo borramos con el registro I2C_ICR y borramos el registro I2C_CR2.

Interpretaciones basadas en interrupciones

Configurar interrupciones con I2C requiere que habilitemos las interrupciones para el periférico I2C en cuestión. Esto debe hacerse con el periférico en un estado deshabilitado. Después de eso podemos habilitar la interrupción:


NVIC_SetPriority(I2C1_IRQn, 0);
NVIC_EnableIRQ(I2C1_IRQn);

Luego, las interrupciones se habilitan en la periferia configurando el bit de configuración:


I2C1->CR1 |= I2C_CR1_RXIE;

Asegúrese de que el controlador de interrupciones (ISR) con el nombre correcto se implemente con el nombre especificado en el inicio:


volatile uint8_t i2c_rxb = 0;

void I2C1_IRQHandler(void) {
    // Verify interrupt status.
    if ((I2C->ISR & I2C_ISR_RXNE) == I2C_ISR_RXNE) {
        // Read byte (which clears RXNE flag).
        i2c_rxb = instance.regs->RXDR;
    }
}

No olvides agregar el extern "C" { } bloquee alrededor del identificador si usa un lenguaje que no sea C para evitar daños en el nombre de la función.

Con este código en su lugar, cada vez que el búfer de lectura obtenga un byte, se llamará al ISR y podemos copiarlo en un búfer o en otro lugar.

Uso de múltiples dispositivos

Como se puede suponer en este punto, el uso de múltiples dispositivos de un solo receptor de radio con microcontrolador solo requiere que se envíe el identificador de dispositivo correcto antes de cualquier carga útil. Aquí también es esencial borrar el registro I2C_CR2 y configurarlo correctamente para el próximo ciclo de transmisión o recepción, para evitar cualquier confusión en las ID de los dispositivos.

Dependiendo de la implementación del código (p. ej., con RTOS de subprocesos múltiples), es posible que se produzcan conflictos de lectura y escritura. En este caso, es esencial que las escrituras y lecturas de I2C estén coordinadas para que no se pierdan datos o comandos, o se envíen al dispositivo incorrecto.

cuando está envuelto

Usar I2C en STM32 no es muy complicado una vez que se supera el obstáculo de configurar el reloj. Este es un tema que puede merecer su propio artículo, junto con temas de alto perfil relacionados con I2C, como el tictac del reloj y el filtrado de ruido. De manera predeterminada, los periféricos I2C en las MCU STM32 tienen un filtro de ruido habilitado en sus entradas I2C, pero también se pueden configurar.

A pesar de lo fácil que es leer y escribir con I2C, aún queda mucho por explorar, incluso cuando se trata de implementar su propio dispositivo I2C en STM32. Estén atentos para más artículos sobre estos temas.

Nora Prieto
Nora Prieto

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *