Implementando un búfer I2C en C

Estoy implementando un esclavo I 2 C de solo lectura en un PIC18F4620 . He creado un controlador ISR -funcional- para el módulo MSSP:

unsigned char dataFromMaster;

unsigned char SSPISR(void) {
    unsigned char temp = SSPSTAT & 0x2d;
    if ((temp ^ 0x09) == 0x00) {
        // State 1: write operation, last byte was address
        ReadI2C();
        return 1;
    } else if ((temp ^ 0x29) == 0x00) { 
        // State 2: write operation, last byte was data
        dataFromMaster = ReadI2C();
        return 2;
    } else if (((temp & 0x2c) ^ 0x0c) == 0x00) {
        // State 3: read operation, last byte was address
        WriteI2C(0x00);
        return 3;
    } else if (!SSPCON1bits.CKP) {
        // State 4: read operation, last byte was data
        WriteI2C(0x00);
        return 4;
    } else {                                        
        // State 5: slave logic reset by NACK from master
        return 5;
    }
}

Esto es solo un puerto a C de una parte del código ASM en el apéndice B de AN734 .

En mi ciclo principal, estoy comprobando si hay nuevos datos, como este:

void main(void) {
    if (dataFromMaster != 0x00) {
        doSomething(dataFromMaster);
        dataFromMaster = 0x00;
    }
}

Esto genera un problema cuando el maestro envía bytes muy rápido y los nuevos datos ingresan antes de que el ciclo principal llegue a doSomething. Por lo tanto, quiero implementar un búfer donde se almacenen los datos del maestro. Necesito una matriz terminada en nulo de 16 caracteres ( nullno se usará como un comando para el esclavo). El ISR tiene que escribir nuevos datos en esa matriz, y el ciclo principal debe leerlos de la matriz en el orden en que se recibieron y borrar la matriz.

No tengo idea de cómo implementar esto. ¿Tú?

Sí, lo hago, y lo he hecho de forma rutinaria.
@OlinLathrop si tiene algo que agregar a la respuesta existente, ¡hágalo!

Respuestas (4)

No tengo experiencia con PIC, pero el problema parece bastante genérico. Crearía una matriz simple con dos punteros independientes en la matriz: un puntero de lectura y un puntero de escritura. Cada vez que recibe un byte, incrementa el puntero de escritura y escribe en la nueva posición; en su ciclo principal, puede verificar si el puntero de lectura y el puntero de escritura son iguales. De lo contrario, simplemente lea y procese desde el búfer y aumente el puntero de lectura para cada byte hasta que lo estén.

Luego, puede restablecer los punteros al comienzo de la matriz o dejar que "fluyan" hasta el comienzo, creando esencialmente un búfer circular. Esto es más fácil si el tamaño de la matriz es un factor de 2, ya que simplemente puede enmascarar ambos punteros después de sus incrementos.

Algunos ejemplos de (pseudo)código:

volatile unsigned int readPointer= 0;
volatile unsigned int writePointer=0;
volatile char theBuffer[32];
...
//in your ISR
writePointer = (writePointer+1) & 0x1F;
theBuffer[writePointer] = ReadI2C(); // assuming this is the easiest way to do it
                                     // I would probably just read the register directly
...
//in main
while (readPointer != writePointer) {
  readPointer = (readPointer+1) & 0x1F;
  nextByte = theBuffer[readPointer];
  // do whatever necessary with nextByte
}
También he pensado en los punteros, pero no tengo idea de cómo se vería el código. ¿Podría darme un ejemplo básico? C es de hecho bastante genérico.
Se agregó algo de código. Supongo que técnicamente no es un "indicador" real, sino más bien un contador.
¡Gracias! Esto fue realmente útil. Como referencia, ReadI2C()en el modo esclavo no hace más que esperar a que se establezca el indicador Buffer Full y luego devolver el buffer. (Solo usé esta función en lugar de solo leer el registro para facilitar la lectura).

Si quieres hacer esto bien, la mejor solución es implementar algún tipo de búfer de anillo .
Pero tenga en cuenta que la implementación debe ser "a prueba de interrupciones". Esto es necesario, porque mientras los contenidos del búfer se procesan en el bucle principal, ¡pueden llegar datos adicionales en cualquier momento a su SPI ISR!
Por lo tanto, es posible que deba echar un vistazo al uso volatilede las operaciones "ATOMIC" si no está familiarizado con ellas.

No veo cómo las operaciones atómicas podrían ser relevantes aquí. AFAIK, son principalmente importantes para las operaciones de lectura, modificación y escritura, si uno está tratando de modificar bits individuales de un byte, y un ISR podría potencialmente alterar algunos de los otros bits. Nada de esto debería pasar aquí.
@fm_andreas: El uso del búfer de anillo en el bucle principal puede requerir deshabilitar y habilitar las interrupciones para hacer que ese código sea atómico contra el código del controlador de interrupciones. Eché un vistazo a las otras respuestas y Nick se está refiriendo más o menos a lo que quiero decir en sus comentarios de código "deshabilitar el ISR de recepción. Si el ISR ocurre en este bloque, puede corromper el búfer".
Sí, en su implementación de búfer de ráfaga, esto puede muy bien ser cierto, pero la belleza de un búfer de anillo es que se pueden escribir nuevos datos en el búfer (con un ISR) incluso mientras el bucle principal está procesando los datos (siempre que el el tamaño del búfer es lo suficientemente grande). Nuevamente, esta es la belleza de un búfer de anillo: puede comenzar a procesar los datos entrantes con el primer byte y aún recibir datos nuevos.
@fm_andreas: el búfer de anillo todavía tiene que administrar al menos dos punteros de varios bytes (probablemente de 16 bits). El PIC mencionado es un controlador de 8 bits, por lo que se requieren múltiples escrituras para actualizar cualquier puntero. Por lo que entiendo, esto no es "seguro para subprocesos".

A partir del pseudocódigo de la respuesta de fm_andreas , hice un código C18 funcional:

#define bufferSize 0x20
static volatile unsigned char buffer[bufferSize] = {0}; // This is the actual buffer
static volatile unsigned char readPointer = 0;          // The pointer to read data
static volatile unsigned char writePointer = 0;         // The pointer to write data
static volatile unsigned bufferOverflow = 0;            // Indicates a buffer overflow

// In the ISR...
if (buffer[writePointer] == 0x00) {                     // If there is no data at the pointer
    buffer[writePointer] = SSPBUF;                      // Put the data in the buffer
    writePointer = (writePointer+1)%bufferSize;         // Increase the pointer, reset if >32
} else {                                                // If there is data...
    bufferOverflow = 1;                                 // Set the overflow flag
}

// In the main loop...
while (1) {
    // Do some other stuff
    if (readPointer != writePointer) {                  // If there is a new byte
        putc(buffer[readPointer], stdout);              // Do something with the data
        buffer[readPointer] = 0x00;                     // Reset the data
        readPointer = (readPointer+1)%bufferSize;       // Increase the pointer, reset if >32
    }
}

La belleza de este código es que es un búfer circular :

ingrese la descripción de la imagen aquí

Por lo tanto, es menos probable que se desborde cuando se envían grandes cantidades de datos a la vez. Esto se discute en los comentarios sobre la respuesta de Nick Alexeev .

Puede eliminar una decisión utilizando un módulo en lugar de la comparación, y si su búfer tiene un tamaño de potencia de dos, es muy eficiente.
Por ejemplo, puntero de lectura = (puntero de lectura + 1) % tamaño de búfer
hace que el código se vea dulce, ¿eh? En general, si se puede usar una declaración que no sea de rama, generalmente hace que el código sea más legible y, a menudo, puede reducir los tictacs del reloj. Si hace que el código sea menos legible, entonces debe decidir si vale la pena. Cuando programo, paso tiempo tratando de hacerlo comprensible para dos personas: ¡Yo y Yo 6 semanas después de haber terminado!
if (buffer[writePointer] == 0x00)No creo que sea una buena condición para verificar si no hay datos. 0x00 podría ser un dato válido..

@fm-andreas se me ha adelantado en esto. Iba a proponer lo mismo: ráfaga de búfer con posiciones de lectura y escritura. (Esto no es un búfer de anillo. Es más simple. No se ajusta). Un búfer de ráfaga puede almacenar una ráfaga de datos. El sistema debe diseñarse de manera que haya suficiente tiempo entre las ráfagas para procesar los datos.

Aquí está mi versión (pseudo-código):

const unsigned char g_BUFF_LEN = 16;
unsigned char   g_dataBuff[BUFF_LEN];       // buffer
unsigned char   g_g_iWriteOffset, g_g_iReadOffset;      // write and read offsets
unsigned char   g_iFlags;


void main()
{
    resetBuffer();      // initialize the burst buffer

    // process the contents buffer
    while (1)
    {
        // other code that lives in the main loop

        while (g_iWriteOffset > g_iReadOffset)      // inner loop for processing the received data
        {
            doSomething(g_dataBuff[g_iReadOffset]);     

            // disable the receiveISR.  If the ISR occurs in this block, it can corrupr the buffer.
            ++g_iReadOffset;        // advance the read offset
            if (g_iReadOffset == g_iWriteOffset)    // is there remaining unprocessed data in the buffer?
            {
                resetBuffer();
            }
            // re-enable the receive ISR
        }
    }
}


void resetBuffer()
{
    g_iWriteOffset = 0;
    g_iReadOffset = 0;
}


void receiveISR()
{
    /*  Receive the byte.
        Specific code for keeping the hardware happy goes here.
        Keelan, you've already posted it in the O.P.  I'll save some time and not repeat it.  */

    g_dataBuff[g_iWriteOffset] = newByte;

    ++g_iWriteOffset;       // advance the write offset
    if (g_iWriteOffset >= g_BUFF_LEN)   // have we got a buffer overflow?
    {
        g_iFlags |= COMM_BUFF_OVERFLOW;
        /*  Handling of errors (such as this overflow) is an interesting topic.
            However, is depends on the nature of the instrument.
            It's somewhat outside the sope of the question. */
    }
}

PD
En una nota relacionada, mire en los búferes de ping-pong. Esto es útil cuando tiene paquetes (o comandos) de varios bytes y necesita recibir el paquete completo antes de poder comenzar a procesarlo.

2x búferes idénticos (o más de 2x). ISR llena un búfer hasta que detecta el final del comando. Mientras tanto, el bucle main() procesa el otro búfer. Si el ISR ha recibido el siguiente comando completo y main() ha terminado de procesar el comando anterior, los punteros del búfer se intercambian.

¿No sería mejor implementar un búfer de anillo? Por ejemplo, para cuando el búfer es de 32 caracteres y se han insertado 31 y también se han leído. Cuando vendrían dos caracteres muy rápido, eso crearía un desbordamiento con este código, pero no con un búfer de anillo. ¿Tengo razón o me estoy perdiendo algo?
@CamilStaps Tienes razón desde la perspectiva de la informática. Pero veamos el problema desde la perspectiva de los sistemas. Si el esclavo procesa los datos más lentamente de lo que el maestro los envía, el búfer finalmente se desbordará. Un búfer de anillo de 32 bytes puede tardar 100 ms más en desbordarse que un búfer simple de 32 bytes.
Entiendo. Digo esto porque la aplicación final probablemente enviará los datos en grandes cantidades y luego esperará mucho tiempo. Cuando los bultos son de un tamaño diferente (pero más pequeños) que el búfer, eso causaría problemas con el búfer simple, mientras que no lo haría con el búfer de anillo, si entiendo correctamente. Aún así, ¡+1 para un código claro y una explicación!