Implementación no bloqueante de un driver LCD 
A la hora de controlador un display LCD mediante el conocido adaptador I2C la gran mayoría de ejemplos disponibles por ahí implementan los estados de espera necesarios mediante retardos explícitos ("delays"). Dichas implementaciones están bien como prueba de concepto, pero no son deseables en entornos multitarea donde no podemos desperdiciar ciclos sólo esperando. En entornos reales se precisa de implementaciones no bloqueantes que hagan uso de timers e interrupciones.

El circuito

La interfaz de un display LCD estándar de caracteres es una interfaz paralelo de 8 bits, con 3 líneas de control adicionales (RS, EN y RW). Del bus paralelo de 8 bits pueden usarse sólo los 4 bits más significativos enviando de forma adecuada los comandos. Los circuitos de conversión a I2C que se venden habitualmente por AliExpress, Ebay y demás están basados en el conversor I2C/paralelo de 8 bits PCF8574 de Texas Instruments: del bus paralelo de dicho conversor se sacan los 4 bits más significativos para el bus paralelo del LCD y las tres señales de control para RS, EN y RW.



La configuración habitual en este tipo de módulos es esta:

PCF8574bit 7bit 6bit 5bit 4bit 3bit 2bit 1bit 0
LCDD7D6D5D4BLENRWRS

En la tabla se puede apreciar una cuarta señal de control etiquetada como BL (backlight) que controla el encendido del led de la luz trasera. Dicho led no forma parte de la circuitería estándar del display y ha sido introducido en versiones más recientes.

El problema

Los displays baratos de caracteres LCD que se encuentran en el mercado están basados en en un chip de Hitachi que no se caracteriza precisamente por su velocidad (probablemente debe ser uno de los chips más rentabilizados de toda la historia de Hitachi) y normalmente cada acceso debe estar seguido por una espera de uno a varios microsegundos, dependiendo del acceso realizado. A continuación puede verse la tabla de comandos de referencia del display, nótese la columna de la derecha ("Execution Time"):


(imagen extraida de https://learningmsp430.wordpress.com/2013/11/13/16x2-lcd-interfacing-in-8bit-mode/)

Cuando uno realiza una búsqueda en internet sobre códigos de ejemplo para control de displays LCD, la gran mayoría de los mismos (no digo todos porque considero que no los he visto todos, pero al menos todos los que yo he visto), implementan las esperas mediante retardos utilizando funciones "delay" o similares. Esta forma de implementación, aunque resulta simple, supone un desperdicio de ciclos e impide que el microcontrolador realice otras tareas de forma concurrente.

La solución no bloqueante

La solución ideal pasaría por una implementación basada en colas y en interrupciones. En este caso se ha implementado una máquina de estados que controla el flujo de datos I2C, el troceado de los bytes en dos nibbles y las esperas que hay que realizar entre un envío y el siguiente. Grosso modo, la solución sería la siguiente:

- Cada vez que se quiere escribir en el display, lo que se hace es escribir lo que se quiere mandar al display en una cola de datos, por lo que la función encargada de escribir regresa inmediatamente (no es bloqueante).

- El systick del microcontrolador cuando detecta que hay algún dato en la cola de datos inicia una máquina de estados que se encarga de trocear en byte en dos nibbles y enviarlos en tiempos diferentes, así hasta que la cola de datos quede vacía, en cuyo momento la máquina de estados pasa a modo "IDLE" y queda a la espera que de haya más datos en la cola.

- La capa I2C también está implementada como una cola de bytes de tal manera que si la capa LCD quiere escribir N bytes seguidos por I2C, los escribe de forma no bloqueante en la cola I2C (la función de escritura I2C también regresa inmediatamente) y se va vaciando a medida que la interrupción de callback de transmisión es llamada por el microcontrolador.

A continuación puede verse cómo ha quedado la máquina de estados del controlador LCD:



El código no queda tan sencillo a simple vista pero se trata, sin duda, de una implementación más eficiente.

#include "LCD.H"

using namespace avelino;
using namespace std;

void LCD::init(uint8_t address) {
    this->address = address;
    this->timerCounter = 5;
    this->status = LCD::Status::WAIT_AFTER_INIT;
    this->queue.push(LCD::QueueItem(0x33, LCD::IsCommand::YES));
    this->queue.push(LCD::QueueItem(0x32, LCD::IsCommand::YES));
    this->queue.push(LCD::QueueItem(0x28, LCD::IsCommand::YES));
    this->queue.push(LCD::QueueItem(0x08, LCD::IsCommand::YES));
    this->queue.push(LCD::QueueItem(0x01, LCD::IsCommand::YES));
    this->queue.push(LCD::QueueItem(0x06, LCD::IsCommand::YES));
    this->queue.push(LCD::QueueItem(0x0C, LCD::IsCommand::YES));
}

void LCD::tick() {
    Status localStatus = this->status;
    do {
        this->status = localStatus;
        if (localStatus == LCD::Status::WAIT_AFTER_INIT) {
            if (this->timerCounter > 0)
                this->timerCounter--;
            else
                localStatus = LCD::Status::IDLE;
        }
        else if (localStatus == LCD::Status::IDLE) {
            if (!this->queue.empty()) {
                I2CManager::deviceAddress = this->address << 1;
                localStatus = LCD::Status::SEND_FIRST_NIBBLE;
            }
        }
        else if (localStatus == LCD::Status::SEND_FIRST_NIBBLE) {
            uint8_t byte = this->queue.head().byte;
            LCD::IsCommand isCommand = this->queue.head().isCommand;
            I2CManager::txQueue.push((byte & 0xF0) | ((isCommand == LCD::IsCommand::YES) ? LCD::RS_0 : LCD::RS_1) | LCD::RW_0 | LCD::EN_1 | LCD::BL_1);
            I2CManager::txQueue.push((byte & 0xF0) | ((isCommand == LCD::IsCommand::YES) ? LCD::RS_0 : LCD::RS_1) | LCD::RW_0 | LCD::EN_0 | LCD::BL_1);
            I2CManager::send();
            localStatus = LCD::Status::WAIT_FIRST_NIBBLE_SENT;
        }
        else if (localStatus == LCD::Status::WAIT_FIRST_NIBBLE_SENT) {
            if (I2CManager::txDone) {
                this->timerCounter = 1;
                localStatus = LCD::Status::WAIT_TICK_AFTER_FIRST_NIBBLE_SENT;
            }
        }
        else if (localStatus == LCD::Status::WAIT_TICK_AFTER_FIRST_NIBBLE_SENT) {
            if (this->timerCounter > 0)
                this->timerCounter--;
            else
                localStatus = LCD::Status::SEND_SECOND_NIBBLE;
        }
        else if (localStatus == LCD::Status::SEND_SECOND_NIBBLE) {
            uint8_t byte = this->queue.head().byte << 4;
            LCD::IsCommand isCommand = this->queue.head().isCommand;
            this->queue.pop();
            I2CManager::txQueue.push((byte & 0xF0) | ((isCommand == LCD::IsCommand::YES) ? LCD::RS_0 : LCD::RS_1) | LCD::RW_0 | LCD::EN_1 | LCD::BL_1);
            I2CManager::txQueue.push((byte & 0xF0) | ((isCommand == LCD::IsCommand::YES) ? LCD::RS_0 : LCD::RS_1) | LCD::RW_0 | LCD::EN_0 | LCD::BL_1);
            I2CManager::send();
            localStatus = LCD::Status::WAIT_SECOND_NIBBLE_SENT;
        }
        else if (localStatus == LCD::Status::WAIT_SECOND_NIBBLE_SENT) {
            if (I2CManager::txDone) {
                this->timerCounter = 1;
                localStatus = LCD::Status::WAIT_TICK_AFTER_SECOND_NIBBLE_SENT;
            }
        }
        else if (localStatus == LCD::Status::WAIT_TICK_AFTER_SECOND_NIBBLE_SENT) {
            if (this->timerCounter > 0)
                this->timerCounter--;
            else
                localStatus = LCD::Status::IDLE;
        }
    } while (localStatus != this->status);
}


void LCD::write(const char *s, int16_t size, LCD::IsCommand isCommand) {
    while ((*s != 0) && ((size < 0) || (size > 0))) {
        this->queue.push(QueueItem(*s, isCommand));
        s++;
        if (size > 0)
            size--;
    }
}

La función miembro "tick" es invocada desde la interrupción systick del microcontrolador en "main.cc":

LCD lcd;

void systick() __attribute__ ((section(".systick")));

void systick() {
    lcd.tick();
}

Nótese que las colas (tanto la cola I2C como la cola LCD) están implementadas usando colas circulares estáticas a través de una plantilla ("StaticQueue.H").

#ifndef  __STATICQUEUE_H__
#define  __STATICQUEUE_H__

#include <stdint.h>

extern "C++" {
    namespace avelino {
        using namespace std;

        template <typename T, int32_t N>
        class StaticQueue {
            public:
                T data[N];
                int32_t headIndex;
                int32_t tailIndex;
                void push(const T &v);
                const T &head() { return this->data[this->headIndex]; };
                void pop();
                bool empty() { return (this->headIndex == this->tailIndex); };
                StaticQueue() : headIndex(0), tailIndex(0) { };
        };

        template <typename T, int32_t N>
        void StaticQueue<T, N>::push(const T &v) {
            this->data[this->tailIndex] = v;
            this->tailIndex++;
            if (this->tailIndex == N)
                this->tailIndex = 0;
        }

        template <typename T, int32_t N>
        void StaticQueue<T, N>::pop() {
            this->headIndex++;
            if (this->headIndex == N)
                this->headIndex = 0;
        }
    }
}

#endif  // __STATICQUEUE_H__

Se ha utilizado en varios sitios el "enum class", que permite trabajar con enumerados fuertemente tipados (introducido en el estándar C++11).

En la sección soft puede descargarse todo el código fuente.



Comentarios 
Lo sentimos. No se permiten nuevos comentarios después de 90 días.