Implementierung eines I2C-Puffers in C

Ich implementiere einen schreibgeschützten I 2 C-Slave auf einem PIC18F4620 . Ich habe einen funktionierenden ISR-Handler für das MSSP-Modul erstellt:

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;
    }
}

Dies ist nur eine Portierung nach C von einem Teil des ASM-Codes in Anhang B von AN734 .

In meiner Hauptschleife überprüfe ich, ob es neue Daten gibt, wie folgt:

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

Dies führt zu einem Problem, wenn der Master Bytes sehr schnell sendet und neue Daten eingehen, bevor die Hauptschleife ankommt doSomething. Ich möchte daher einen Puffer implementieren, in dem Daten vom Master gespeichert werden. Ich brauche ein 16-Zeichen-Array mit Nullterminierung (das nullnicht als Befehl für den Slave verwendet wird). Die ISR muss neue Daten in dieses Array schreiben, und die Hauptschleife sollte sie in der Reihenfolge, in der sie empfangen wurden, aus dem Array lesen und das Array löschen.

Ich habe keine Ahnung, wie ich das umsetzen soll. Tust du?

Ja, das tue ich, und habe es routinemäßig getan.
@OlinLathrop Wenn Sie der vorhandenen Antwort etwas hinzuzufügen haben, tun Sie dies bitte!

Antworten (4)

Ich habe keine Erfahrung mit PIC, aber das Problem scheint allgemein genug zu sein. Ich würde ein einfaches Array mit zwei unabhängigen Zeigern in das Array erstellen: einen Lesezeiger und einen Schreibzeiger. Immer wenn Sie ein Byte empfangen, erhöhen Sie den Schreibzeiger und schreiben an der neuen Position; In Ihrer Hauptschleife könnten Sie dann überprüfen, ob der Lesezeiger und der Schreibzeiger gleich sind. Wenn nicht, lesen und verarbeiten Sie einfach aus dem Puffer und erhöhen den Lesezeiger für jedes Byte, bis dies der Fall ist.

Sie können dann entweder die Zeiger auf den Anfang des Arrays zurücksetzen oder sie zum Anfang "überfließen" lassen, wodurch im Wesentlichen ein Ringpuffer entsteht. Dies ist am einfachsten, wenn die Größe des Arrays ein Faktor von 2 ist, da Sie dann einfach beide Zeiger nach ihren Inkrementen bitmaskieren können.

Ein Beispiel (Pseudo-)Code:

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
}
Ich habe auch über Zeiger nachgedacht, habe aber keine Ahnung, wie der Code aussehen würde. Können Sie mir ein grundlegendes Beispiel geben? C ist in der Tat ziemlich generisch.
Etwas Code hinzugefügt. Ich denke, es ist technisch gesehen kein echter "Zeiger", sondern eher ein Zähler.
Danke! Das war wirklich hilfreich. Als Referenz ReadI2C()wird im Slave-Modus nicht mehr getan, als darauf zu warten, dass das Buffer Full-Flag gesetzt wird, und dann den Puffer zurückzugeben. (Ich habe diese Funktion nur verwendet, anstatt nur das Register für die Lesbarkeit zu lesen.)

Wenn Sie dies richtig machen möchten, ist die beste Lösung, eine Art Ringpuffer zu implementieren .
Beachten Sie jedoch, dass die Implementierung "unterbrechungssicher" sein muss. Dies ist erforderlich, da während der Pufferinhalte in der Hauptschleife verarbeitet werden, jederzeit weitere Daten in Ihrem SPI ISR eintreffen können!
Daher müssen Sie sich möglicherweise mit den Operationen using und "ATOMIC" befassen, volatilewenn Sie damit nicht vertraut sind.

Ich sehe nicht, wie atomare Operationen hier relevant sein könnten. AFAIK, sie sind in erster Linie wichtig für Lese-, Änderungs- und Schreibvorgänge, wenn versucht wird, einzelne Bits eines Bytes zu ändern, und ein ISR möglicherweise einige der anderen Bits ändern könnte. All das soll hier nicht passieren.
@fm_andreas: Die Verwendung des Ringpuffers in der Hauptschleife erfordert möglicherweise das Deaktivieren und Aktivieren von Interrupts, um diesen Code gegenüber dem Interrupt-Handler-Code atomar zu machen. Ich habe mir die anderen Antworten angesehen und Nick bezieht sich mehr oder weniger auf das, was ich in seinen Codekommentaren meine: "Deaktivieren Sie die ReceiveISR. Wenn die ISR in diesem Block auftritt, kann sie den Puffer beschädigen".
Ja, in seiner Burst-Buffer-Implementierung mag das sehr wohl zutreffen, aber das Schöne an einem Ringbuffer ist, dass neue Daten in den Buffer geschrieben werden können (mit einem ISR), sogar während die Hauptschleife die Daten verarbeitet (solange die Puffergröße ist groß genug). Auch dies ist das Schöne an einem Ringpuffer: Sie können mit der Verarbeitung eingehender Daten mit dem ersten Byte beginnen und trotzdem neue Daten empfangen.
@fm_andreas: Der Ringpuffer muss noch mindestens zwei Multibyte-Zeiger (wahrscheinlich 16 Bit) verwalten. Der erwähnte PIC ist ein 8-Bit-Controller, daher sind mehrere Schreibvorgänge erforderlich, um jeden Zeiger zu aktualisieren. Soweit ich weiß, ist dies nicht "threadsicher".

Aus dem Pseudocode der Antwort von fm_andreas habe ich einen funktionierenden C18-Code erstellt:

#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
    }
}

Das Schöne an diesem Code ist, dass es sich um einen Ringpuffer handelt :

Geben Sie hier die Bildbeschreibung ein

Es ist daher weniger wahrscheinlich, dass es überläuft, wenn große Datenmengen auf einmal gesendet werden. Dies wird in den Kommentaren zu Nick Alexeevs Antwort diskutiert .

Sie können eine Entscheidung ausschließen, indem Sie anstelle des Vergleichs ein Modulo verwenden, und wenn Ihr Puffer eine Zweierpotenz groß ist, ist er sehr effizient.
ZB Lesezeiger = (Lesezeiger + 1) % Puffergröße
macht den Code irgendwie süß aussehend, huh? Wenn eine Anweisung ohne Verzweigung verwendet werden kann, macht dies im Allgemeinen den Code lesbarer und kann häufig Takte verkürzen. Wenn es den Code weniger lesbar macht, müssen Sie entscheiden, ob es sich lohnt. Wenn ich programmiere, verbringe ich Zeit damit, es für zwei Personen verständlich zu machen: Ich und ich 6 Wochen nachdem ich fertig bin!
if (buffer[writePointer] == 0x00)Ich denke nicht, dass das eine gute Bedingung ist, um zu überprüfen, ob keine Daten vorhanden sind. 0x00 könnte ein gültiges Datum sein.

@fm-andreas ist mir da zuvorgekommen. Ich wollte dasselbe vorschlagen: Burst-Puffer mit Lese- und Schreibpositionen. (Dies ist kein Ringpuffer. Es ist einfacher. Es läuft nicht herum.) Ein Burst-Puffer kann einen Burst von Daten speichern. Das System sollte so ausgelegt sein, dass zwischen den Bursts genügend Zeit bleibt, um die Daten zu verarbeiten.

Hier ist meine Version (Pseudocode):

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. */
    }
}

PS
Sehen Sie sich in einem ähnlichen Zusammenhang die Ping-Pong-Puffer an. Dies ist praktisch, wenn Sie Multi-Byte-Pakete (oder Befehle) haben und das gesamte Paket empfangen müssen, bevor Sie mit der Verarbeitung beginnen können.

2x identische Puffer (oder mehr als 2x). ISR füllt einen Puffer, bis es das Ende des Befehls erkennt. Währenddessen verarbeitet die main()-Schleife den anderen Puffer. Wenn die ISR den nächsten vollständigen Befehl erhalten hat und main() mit der Verarbeitung des vorherigen Befehls fertig ist, werden die Pufferzeiger vertauscht.

Wäre die Implementierung eines Ringpuffers nicht besser? Zum Beispiel, wenn der Puffer 32 Zeichen lang ist und 31 Zeichen eingefügt und auch ausgelesen wurden. Wenn sehr schnell zwei Zeichen eingehen würden, würde das bei diesem Code einen Überlauf erzeugen, aber nicht bei einem Ringpuffer. Habe ich Recht, oder übersehe ich etwas?
@CamilStaps Aus Sicht der Informatik haben Sie Recht. Aber betrachten wir das Problem aus der Systemperspektive. Verarbeitet der Slave die Daten langsamer als der Master sie sendet, läuft der Puffer irgendwann über. Ein 32-Byte-Ringpuffer kann 100 ms länger zum Überlaufen benötigen als ein 32-Byte-Plain-Puffer.
Ich verstehe. Ich sage das, weil die Endanwendung die Daten wahrscheinlich in großen Mengen senden und dann lange warten wird. Wenn die Bulks eine andere Größe (aber kleiner) als der Puffer haben, würde dies Probleme mit dem einfachen Puffer verursachen, während dies mit dem Ringpuffer nicht der Fall wäre - wenn ich das richtig verstehe. Trotzdem +1 für einen klaren Code und eine Erklärung!