Programación I2C en S3C2440 sin Sistema Operativo: Estructura de Código

El protocolo I2C (Inter-Integrated Circuit) permite la comunicación entre múltiples dispositivos en un bus compartido. En el contexto de la programación de sistemas embebidos sin un sistema operativo, como en el S3C2440, la implementación del controlador I2C se estructura de manera modular para facilitar la gestión de diferentes dispositivos y del propio hardware.

  1. Capas de Abstracción del Software I2C

Para gestionar la comunicación I2C de forma eficiente y escalable, el código se organiza en distintas capas:

  • Capa de Aplicación (i2c_test): Esta capa se encarga de verificar y probar las funcionalidades del bus I2C, interactuando con las capas inferiores para realizar operaciones de lectura y escritura.
  • Capa de Dispositivo I2C (at24cxx.c): Abstrae la complejidad de interactuar con un dispositivo I2C específico (por ejemplo, una memoria EEPROM como la AT24Cxxx). Define funciones para leer y escribir datos en direcciones concretas de este dispositivo.
  • Capa de Controlador I2C Genérico: Proporciona una interfaz unificada (i2c_transfer) para interactuar con el controlador I2C del hardware, independientemente de la marca o modelo específico del microcontrolador.
  • Capa de Controlador I2C Específico (s3c2440_i2c_controller.c): Implementa las operaciones de bajo nivel para un controlador I2C concreto, en este caso, el del S3C2440. Maneja el acceso a registros de hardware, la configuración de pines y la gestión de interrupciones.

La función central i2c_transfer actúa como punto de entrada para todas las transacciones I2C. Cada transacción de datos se representa mediante una estructura i2c_msg. Para operaciones que requieren tanto enviar una dirección como leer datos (como leer desde una dirección específica en una EEPROM), se utilizan múltiples estructuras i2c_msg para definir las diferentes fases de la transferencia (primero la escritura de la dirección, luego la lectura de los datos).

  1. Estructura de Datos i2c_msg

La estructura i2c_msg define los parámetros para una única transacción en el bus I2C:

typedef struct i2c_msg {
    unsigned int addr;         // Dirección del dispositivo esclavo (7 bits)
    int flags;                 // 0 para escritura, 1 para lectura
    int len;                   // Número de bytes a transferir
    int cnt_transferred;       // Contador de bytes transferidos
    unsigned char *buf;        // Puntero al buffer de datos
    int err;                   // Código de error
} i2c_msg, *p_i2c_msg;

  1. Implementación Específica para S3C2440

3.1. Inicialización del Controlador S3C2440

La inicialización del controlador I2C del S3C2440 implica la configuración de los pines GPIO y del registro de control IICCON:

void s3c2440_i2c_con_init(void) {
    // Configurar pines GPE [14:15] como I2C SCL y SDA
    GPECON &= ~((3 << 28) | (3 << 30)); // Limpiar bits
    GPECON |= ((2 << 28) | (2 << 30));  // Configurar como función alternativa 2

    // Configurar IICCON:
    // Bit 7: ACK Enable (en modo recepción)
    // Bit 6: Selección de reloj IICCLK (0: fPCLK/16, 1: fPCLK/512)
    // Bit 5: Habilitar interrupciones I2C
    // Bit 4: Flag de interrupción (lectura 1, escritura 0 para limpiar)
    // Bits 3:0: Divisor de reloj de transmisión (Tx Clock = IICCLK / (IICCON[3:0] + 1))
    // Ejemplo: Para 100kHz con fPCLK=50MHz y IICCLK=fPCLK/16
    // Tx Clock = 50MHz / 16 / (30 + 1) ~= 100kHz
    IICCON = (1 << 7) | (0 << 6) | (1 << 5) | (30 << 0);

    // Registrar la rutina de servicio de interrupción para la interrupción I2C (IRQ 27)
    register_irq(27, i2c_interrupt_func);
}

3.2. Transferenica Maestro-Emisor (do_master_tx)

Esta función inicia una operación de escritura desde el S3C2440 (maestro) a un dispositivo esclavo:

int do_master_tx(p_i2c_msg msg) {
    p_cur_msg = msg; // Almacenar mensaje actual para la ISR
    msg->cnt_transferred = -1;
    msg->err = 0;

    // Configurar IICCON para modo Master Tx
    IICCON |= (1 << 7); // Habilitar ACK en modo recepción (importante para el protocolo)
    IICSTAT = (1 << 4); // Habilitar salida de datos IIC

    // Escribir la dirección del esclavo en IICDS (bits 7:1)
    IICDS = (msg->addr << 1); // El bit 0 indica dirección de transmisión (0 para escribir)

    // Iniciar la transmisión (bit 0 de IICSTAT a 1)
    // El envío de la dirección y la espera de ACK/NACK generarán una interrupción.
    IICSTAT = 0xf0; // Habilita la transmisión y marca el inicio

    // Esperar a que la transferencia se complete (controlado por la ISR)
    while (!msg->err && msg->cnt_transferred != msg->len);

    if (msg->err) {
        return -1; // Error en la transferencia
    } else {
        return 0;  // Transferencia exitosa
    }
}

3.3. Transferencia Maestro-Receptor (do_master_rx)

Esta función inicia una operación de lectura desde un dispositivo esclavo al S3C2440 (maestro):

int do_master_rx(p_i2c_msg msg) {
    p_cur_msg = msg; // Almacenar mensaje actual para la ISR
    msg->cnt_transferred = -1;
    msg->err = 0;

    // Configurar IICCON para modo Master Rx
    IICCON |= (1 << 7); // Habilitar ACK en modo recepción
    IICSTAT = (1 << 4); // Habilitar salida de datos IIC

    // Escribir la dirección del esclavo en IICDS
    // El bit 0 indica dirección de transmisión (1 para leer)
    IICDS = (msg->addr << 1) | (1 << 0);

    // Iniciar la transmisión (bit 0 de IICSTAT a 1)
    // El envío de la dirección y la espera de ACK/NACK generarán una interrupción.
    IICSTAT = 0xb0; // Habilita la transmisión y marca el inicio

    // Esperar a que la transferencia se complete (controlado por la ISR)
    while (!msg->err && msg->cnt_transferred != msg->len);

    if (msg->err) {
        return -1; // Error en la transferencia
    } else {
        return 0;  // Transferencia exitosa
    }
}

3.4. Función de Transferencia General del Controlador S3C2440

s3c2440_master_xfer orquesta las operaciones de lectura y escritura según las especificaciones en el array i2c_msg:

int s3c2440_master_xfer(p_i2c_msg msgs, int num) {
    int i;
    int err;
    for (i = 0; i < num; i++) {
        if (msgs[i].flags == 0) { // Escritura
            err = do_master_tx(&msgs[i]);
        } else { // Lectura
            err = do_master_rx(&msgs[i]);
        }
        if (err) {
            return err; // Detener en caso de error
        }
    }
    return 0; // Todas las transferencias completadas
}

3.5. Registro del Controlador

La estructura i2c_controller define la interfaz para un controlador I2C:

typedef struct i2c_controller {
    int (*init)(void);                 // Función de inicialización
    int (*master_xfer)(i2c_msg *msgs, int num); // Función de transferencia maestro
    char *name;                        // Nombre del controlador
} i2c_controller, *p_i2c_controller;

// Definición de la instancia del controlador S3C2440
static i2c_controller s3c2440_i2c_con = {
    .name = "s3c2440",
    .init = s3c2440_i2c_con_init,
    .master_xfer = s3c2440_master_xfer,
};

// Función para registrar el controlador S3C2440 en la lista global
void s3c2440_i2c_con_add(void) {
    register_i2c_controller(&s3c2440_i2c_con);
}

La función register_i2c_controller (definida en la capa genérica) se encarga de añadir esta estructura a un array de controladores disponibles. Posteriormente, select_i2c_controller elige el controlador activo basándose en su nombre.

  1. Ejemplo de Uso: AT24Cxxx

Las funciones para interactuar con una memoria EEPROM AT24Cxxx utilizan la interfaz i2c_transfer:

4.1. Escritura en AT24Cxxx

#define AT24CXX_ADDR 0x50 // Dirección I2C de la AT24Cxxx

int at24cxx_write(unsigned int addr, unsigned char *data, int len) {
    i2c_msg msg;
    int i;
    int err;
    unsigned char buf[2]; // Buffer para dirección + dato

    for (i = 0; i < len; i++) {
        buf[0] = addr + i; // Dirección a escribir
        buf[1] = data[i];  // Dato a escribir

        // Configurar el mensaje I2C para escribir la dirección y el dato
        msg.addr = AT24CXX_ADDR;
        msg.flags = 0; // Escritura
        msg.len = 2;
        msg.buf = buf;
        msg.err = 0;
        msg.cnt_transferred = -1;

        // Realizar la transferencia I2C
        err = i2c_transfer(&msg, 1);
        if (err) {
            return err; // Devolver error si la transferencia falla
        }
    }
    return 0; // Escritura completada
}

4.2. Lectura desde AT24Cxxx

int at24cxx_read(unsigned int addr, unsigned char *data, int len) {
    i2c_msg msg[2]; // Necesitamos dos mensajes: uno para escribir la dirección, otro para leer los datos
    int err;

    // Mensaje 1: Escribir la dirección de inicio de lectura
    msg[0].addr = AT24CXX_ADDR;
    msg[0].flags = 0; // Escritura
    msg[0].len = 1;
    msg[0].buf = &addr; // Puntero a la dirección a leer
    msg[0].err = 0;
    msg[0].cnt_transferred = -1;

    // Mensaje 2: Leer los datos desde la dirección especificada
    msg[1].addr = AT24CXX_ADDR;
    msg[1].flags = 1; // Lectura
    msg[1].len = len;
    msg[1].buf = data; // Buffer para almacenar los datos leídos
    msg[1].err = 0;
    msg[1].cnt_transferred = -1;

    // Realizar la secuencia de transferencia (escribir dirección, luego leer datos)
    err = i2c_transfer(msg, 2);
    if (err) {
        return err; // Devolver error si la transferencia falla
    }
    return 0; // Lectura completada
}

Este diseño en capas permite desacoplar la lógica de aplicación de los detalles de hardware, facilitando la portabilidad y el mantenimiento del código.

Etiquetas: S3C2440 I2C bare-metal programación embebida controlador I2C

Publicado el 7-17 11:21