A diferencia del SPI, que requiere 4 cables y un CS por cada dispositivo extra, el I2C utiliza solo 2 cables para todo el ecosistema: SCL (Señal de Reloj) y SDA (Señal de Datos). ¿Cómo distingue el STM32 con quién está hablando si todos comparten el mismo cable? Mediante Direcciones Hexadecimales (Addressing). El Maestro grita en el bus: "¡Dispositivo 0x27, escúchame!", y solo el dispositivo con esa dirección responde.
Si dos chips intentaran enviar datos al mismo tiempo en SPI, habría un cortocircuito (Push-Pull). Para evitar esto en I2C con múltiples esclavos, se utiliza la topología Open-Drain. Ningún chip puede inyectar voltaje (3.3V) al cable. Solo pueden "jalar" el cable hacia Tierra (0V) o "soltarlo".
Para que el bus regrese a 3.3V cuando los chips lo sueltan, son obligatorias dos Resistencias de Pull-Up externas (ej. 4.7kΩ). Ellas "tiran" del voltaje hacia arriba. Si el cable está en 3.3V, significa un `1` lógico. Si algún chip conecta su drenador a tierra, el voltaje cae a 0V, lo que significa un `0` lógico.
A diferencia del SPI donde solo divides el reloj mediante un Prescaler, el I2C requiere cálculos precisos para garantizar los tiempos de subida y bajada de la señal eléctrica. Si nuestro bus APB1 corre a $42\text{ MHz}$, el tiempo de un ciclo de reloj interno ($T_{PCLK1}$) es $\approx 23.8\text{ ns}$.
Para configurar una velocidad estándar de I2C ($100\text{ kHz}$), el STM32 utiliza el registro CCR (Clock Control Register). La ecuación indica que el ciclo completo se divide en dos fases iguales ($T_{high} = T_{low}$). Por lo tanto:
$$ CCR = \frac{f_{PCLK1}}{2 \times f_{I2C}} = \frac{42,000,000}{2 \times 100,000} = 210 $$
Ejercicio 1: Inicializa el módulo I2C1 utilizando los pines físicos PB8 (SCL) y PB9 (SDA). Estos pines corresponden a la Función Alternativa 4 (AF4). Configura los pines expresamente como **Open-Drain**, aplica las fórmulas para $100\text{ kHz}$ y enciende el periférico de comunicaciones.
/**
* @file i2c_driver.c
* @brief Inicialización Bare-Metal del bus I2C1 (Standard Mode 100kHz)
*/
#include "stm32f401xe.h"
// Definimos la frecuencia del Bus APB1 (El I2C1 vive en este bus)
#define APB1_FREQ_MHZ 42
void inicializar_i2c1(void) {
// ====================================================
// 1. ACTIVAR RELOJES (GPIOB y I2C1)
// ====================================================
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
// ====================================================
// 2. CONFIGURAR PINES FÍSICOS (PB8 = SCL, PB9 = SDA)
// ====================================================
// Modo Función Alternativa (10 en el registro MODER)
GPIOB->MODER &= ~(GPIO_MODER_MODE8 | GPIO_MODER_MODE9);
GPIOB->MODER |= (GPIO_MODER_MODE8_1 | GPIO_MODER_MODE9_1);
// IMPORTANTE I2C: Configurar pines como OPEN-DRAIN (1 en OTYPER)
// Si los dejas en Push-Pull (0), quemarás los chips esclavos.
GPIOB->OTYPER |= GPIO_OTYPER_OT8 | GPIO_OTYPER_OT9;
// Velocidad Máxima
GPIOB->OSPEEDR |= (3 << (8*2)) | (3 << (9*2));
// Activar Resistencias Pull-Up internas (Aunque las externas de 4.7k son requeridas físicamente)
GPIOB->PUPDR &= ~(GPIO_PUPDR_PUPD8 | GPIO_PUPDR_PUPD9);
GPIOB->PUPDR |= (GPIO_PUPDR_PUPD8_0 | GPIO_PUPDR_PUPD9_0);
// Seleccionar AF4 (I2C1) para los pines PB8 y PB9 en el registro Alternate Function High (AFRH)
// PB8 es el índice 0 en AFRH, PB9 es el índice 1. (0x4 significa AF4)
GPIOB->AFR[1] &= ~(0xFF << 0);
GPIOB->AFR[1] |= (0x44 << 0);
// ====================================================
// 3. CONFIGURAR REGISTROS DE COMUNICACIÓN I2C1
// ====================================================
// Asegurar que el I2C esté apagado antes de configurar
I2C1->CR1 &= ~I2C_CR1_PE;
// Le informamos al I2C la velocidad real del bus APB1 en MegaHertz (42)
I2C1->CR2 &= ~I2C_CR2_FREQ;
I2C1->CR2 |= APB1_FREQ_MHZ;
// Configurar el reloj (CCR) para 100 kHz (Standard Mode).
// Ecuación: CCR = PCLK1 / (2 * I2C_Freq) = 42MHz / (2 * 100kHz) = 210
I2C1->CCR &= ~I2C_CCR_CCR;
I2C1->CCR |= 210;
// Configurar el Rise Time Máximo (TRISE)
// En Standard Mode, el rise time max es 1000 ns.
// TRISE = (1000ns / (1 / 42MHz)) + 1 = 42 + 1 = 43.
I2C1->TRISE = APB1_FREQ_MHZ + 1;
// ====================================================
// 4. ENCENDER EL BUS
// ====================================================
I2C1->CR1 |= I2C_CR1_PE; // Peripheral Enable
}
int main(void) {
// Preparamos los relojes generales y luego el bus
inicializar_i2c1();
while(1) {
// En el Día 124, utilizaremos este bus para enviar
// nuestro primer comando a la pantalla LCD y verla brillar.
}
return 0;
}
Objetivo del día: Interfaz Humano-Máquina (HMI) y Trazabilidad Cronológica.
En el comercio, el cliente necesita ver qué está pagando, y el auditor fiscal necesita saber cuándo se vendió. Implementaremos una pantalla LCD y un Reloj RTC (Real Time Clock). En la electrónica básica de Arduino, usarías 10 o 12 pines del microcontrolador para conectar ambas cosas. En un entorno industrial ruidoso, tener 12 cables actuando como antenas receptoras de ruido electromagnético cerca del motor de la bomba es un suicidio técnico.
Al migrar la HMI a un bus I2C (Open-Drain), has reducido tu exposición a fallas físicas y ruido electromagnético de 12 cables a tan solo 2. Estos dos cables, fuertemente anclados a 3.3V mediante resistencias Pull-Up, son inherentemente más resistentes a las interferencias. Tu Despachador de Gas LP es ahora un sistema expandible: mañana podríamos añadir sensores de temperatura I2C en diferentes puntos de la máquina sin agregar un solo pin extra al diseño del STM32.
CR2), calcular la división del reloj (Registro CCR) y el tiempo máximo de subida eléctrico (Registro TRISE).