Wie kann man den Interrupt-Code auf ein Minimum reduzieren?

Ich habe einen Interrupt, sagen wir von UART, um ein echtes Beispiel zu geben:

void USART2_IRQHandler(void)
{
    int i = 0;
    if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET)
    {
        static uint8_t cnt = 0;
        char t = USART_ReceiveData(USART2);
        if((t!='!')&&(cnt < MAX_STRLEN))
        {
            received_string[cnt] = t;
            cnt++;
        }
        else
        {
            cnt = 0;
            if(strncmp(received_string,"connection",10) == 0)
            {
                USART2_SendText("connection ok");
            }
            else if(strncmp(received_string,"sine",4) == 0)
            {
                DAC_DeInit();
                DAC_Ch2SineWaveConfig();
                USART2_SendText("generating sine");
            }
            else
            {
                USART2_SendText("unknown commmand: ");
                USART2_SendText(received_string);
            }
            for (i = 0; i <= MAX_STRLEN+1; i++)         // flush buffer
                received_string[i] = '\0'; 
        }
    }
}

Der Interrupt-Code sollte jedoch so schnell wie möglich ausgeführt werden. Und hier haben wir einige zeitraubende Funktionen drin.

Die Frage ist: Wie werden Interrupts richtig implementiert, die zeitaufwändige Funktionen aufrufen?

Eine meiner Ideen ist es, Flags Buffer und Flags in Interrupt zu erstellen. Und verarbeiten Sie den Flag-Puffer in der Hauptschleife, indem Sie die entsprechenden Funktionen aufrufen. Ist es richtig?

Die grundlegende Antwort ist, das System von Anfang an richtig zu gestalten. Erklären Sie, was Ihr System leisten muss, nicht wie es Ihrer Meinung nach erreicht werden sollte, und wir können Ihnen möglicherweise eine geeignete Architektur vorschlagen. Ohne irgendeine Art von Spezifikation ist dies überhaupt keine Frage oder viel zu offen.
Erledigen Sie die zeitkritischen Dinge in der Interrupt-Routine und verwenden Sie Flags, die hauptsächlich für alle anderen Dinge behandelt werden. Wo zeitkritisch ist, liegt die Genauigkeit in der Größenordnung von einigen Befehlszyklen.
@Olin Lathrop: Dieses Beispiel stammt von einem Signalgenerator, der von einer PC-Software gesteuert wird. Ich sende Befehle über UART und sie sollten das generierte Signal, Parameter usw. ändern. Aber ich wollte diese Frage allgemein stellen, um zu wissen, was gute Stile und Designmuster bei der Implementierung von Interrupts sind.

Antworten (3)

UART ist in der Tat ein ziemlich typischer Fall, da viele Anwendungen erfordern, dass eine Verarbeitung als Reaktion auf Befehle/Datum erfolgt, die über die serielle Schnittstelle empfangen werden. Wenn die Anwendung um eine unendliche Verarbeitungsschleife herum aufgebaut ist, was oft der Fall ist, besteht eine gute Möglichkeit darin, empfangene Bytes mit DMA in einen kleinen Puffer zu übertragen und diesen Puffer bei jedem Schleifendurchlauf zu verarbeiten. Der folgende Beispielcode veranschaulicht dies:

#define BUFFER_SIZE 1000
uint8_t inputBuffer[BUFFER_SIZE];
uint16_t inputBufferPosition = 0;    

// setup DMA reception USART2 RX => DMA1, Stream 6, Channel 4
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE);
DMA_InitTypeDef dmaInit;
DMA_StructInit(&dmaInit);
dmaInit.DMA_Channel = DMA_Channel_4;
dmaInit.DMA_PeripheralBaseAddr = ((uint32_t) USART2 + 0x04);
dmaInit.DMA_Memory0BaseAddr = (uint32_t) inputBuffer;
dmaInit.DMA_DIR = DMA_DIR_PeripheralToMemory;
dmaInit.DMA_BufferSize = BUFFER_SIZE;
dmaInit.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
dmaInit.DMA_MemoryInc = DMA_MemoryInc_Enable;
dmaInit.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
dmaInit.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
dmaInit.DMA_Mode = DMA_Mode_Circular;
dmaInit.DMA_Priority = DMA_Priority_Medium;
dmaInit.DMA_FIFOMode = DMA_FIFOMode_Disable;
dmaInit.DMA_MemoryBurst = DMA_MemoryBurst_Single;
dmaInit.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
DMA_Init(DMA1_Stream5, &dmaInit);
USART_DMACmd(port, USART_DMAReq_Rx, ENABLE);

// loop infinitely
while(true)
{
    // read out from the DMA buffer
    uint16_t dataCounter = DMA_GetCurrDataCounter(DMA1_Stream5);
    uint16_t bufferPos = BUFFER_SIZE - dataCounter;

    // if we wrapped, we consume everything to the end of the buffer
    if (bufferPos < inputBufferPosition)
    {
        while (inputBufferPosition < BUFFER_SIZE)
            processByte(inputBuffer[inputBufferPosition++]);
        inputBufferPosition = 0;
    }

    // consume the beginning of the buffer
    while (inputBufferPosition < bufferPos)
        processByte(inputBuffer[inputBufferPosition++]);

    // do other things...
}

Was dieser Code tut, um zuerst einen DMA-Kanal einzurichten, um von USART2 zu lesen. Der richtige DMA-Controller, Stream und Kanal hängt davon ab, welchen USART Sie verwenden (sehen Sie im STM32-Referenzhandbuch nach, um herauszufinden, welche Kombination für einen bestimmten USART-Port erforderlich ist). Dann tritt der Code in die Hauptendlosschleife ein. Bei jeder Schleife prüft der Code, ob etwas (über DMA) in geschrieben wurde inputBuffer. Wenn dies der Fall ist, werden diese Daten von verarbeitet processByte, was Sie ähnlich wie Ihren ursprünglichen IRQ-Handler implementieren sollten.

Das Schöne an diesem Setup ist, dass es keinen Interrupt-Code gibt – alles läuft synchron. Dank DMA erscheinen empfangene Daten einfach "magisch" in inputBuffer. Die Größe inputBuffersollte jedoch sorgfältig bestimmt werden. Es sollte groß genug sein, um alle Daten zu enthalten, die Sie möglicherweise während einer Schleifeniteration erhalten können. Beispielsweise sollte bei einer Baudrate von 115200 (ca. 11 KB/s) und einer maximalen Schleifenzeit von 50 ms die Puffergröße mindestens 11 KB/s * 50 ms = 550 Bytes betragen.

Es wäre schön, wenn ein DMA-Kanal so konfiguriert werden könnte, dass er als kontinuierlicher FIFO fungiert, insbesondere wenn der DMA-Controller das Handshaking auf vernünftige Weise handhaben könnte. Konzeptionell sollte es nicht zu schwer sein. Leider bietet keiner der mir bekannten DMA-Controller eine solche Möglichkeit; funktioniert das auf STM32?
@supercat: Es wäre in der Tat schön, aber ich glaube nicht, dass es das kann.
Ich frage mich, wie viel es kosten würde, eine Handshaking-Fähigkeit hinzuzufügen? Ich weiß, dass ich DMA-Puffer mit „Wrap“-Registern gesehen habe. Grundsätzlich wäre alles, was für einen vollen FIFO benötigt werden sollte, ein "Stopadressen"-Register; der DMA-Controller sollte jedes Mal pausieren, wenn die nächste Anforderung an dieser Adresse wäre. Wenn die CPU entweder Daten zum Puffer hinzufügt oder Daten liest (Raum für neu ankommende Daten verfügbar macht), aktualisiert sie das Stoppadressenregister entsprechend - eine Operation, die sicher durchgeführt werden könnte, selbst während Operationen im Gange wären.

Es kommt wirklich darauf an. Wenn es wichtig ist, dass der Code in Ihrem Handler "sofort" verarbeitet wird, dann gibt es kaum eine Möglichkeit, dies zu umgehen, außer kostspielige externe Funktionsaufrufe zu vermeiden (dh die Funktionalität der aufgerufenen Funktion innerhalb des Handlers zu implementieren). Wenn Sie sich nur Sorgen machen, die eingehenden Daten von Ihrem USART zu lesen, aber die Daten selbst später "bearbeitet" werden können, verwenden Sie besser einen sehr einfachen ISR oder noch besser den DMA und einen externen Puffer das die eingehenden Daten vorübergehend halten kann. ST hat einen netten Anwendungshinweis AN3109 , der zeigt, wie das geht.

Inlining vermeidet nicht nur den Aufruf-Overhead, sondern ermöglicht auch die Code-Spezialisierung (strncmp ist eine eher allgemeine Funktion) und die Befehlsplanung über den Code hinweg, der in separaten Aufrufen enthalten gewesen wäre. Wenn Sie strncmp nicht verwenden, können Sie möglicherweise auch vermeiden, den Puffer zu leeren, da eine Nullterminierung möglicherweise nicht mehr erforderlich ist. Wenn USART2_SendText() langsam ist, kann die Verkettung der Zeichenfolgen anstelle der Verwendung von zwei Aufrufen die WCET verbessern; Das Platzieren von "unbekannter Befehl: " unmittelbar vor Received_string könnte dies frei machen.
Übrigens ist der Compiler möglicherweise nicht schlau genug, um die bedingten Aufrufe zu extrahieren, indem er einen String-Zeiger bedingt setzt. Dies würde die Verwendung von bedingten Bewegungen anstelle von zwei der Verzweigungen ermöglichen (was möglicherweise WCET hilft) und die Codegröße reduzieren.

Meiner Erfahrung nach ist die allgemeinste Methode, die Embedded-Entwicklern zur Verfügung steht, Nachrichtenwarteschlangen, die von den meisten RTOS da draußen bereitgestellt werden. Der Interrupt-Handler platziert empfangene Daten in der Warteschlange und die Handler-Task (die mit der „Thread“-Priorität läuft, um einen Cortex-M-Term zu verwenden) empfängt und verarbeitet die Daten in einer Schleife. Dadurch werden Flags, Locks, Semaphoren etc. vermieden, die eine ständige Fehlerquelle darstellen. Diese Methode hat natürlich ihre Nachteile, zum Beispiel eine ziemlich hohe RAM-Nutzung und die Notwendigkeit eines RTOS. Dennoch finde ich es durchaus gerechtfertigt, wenn die zu implementierende Logik komplex genug ist (und der verfügbare Arbeitsspeicher nicht zu begrenzt ist).

Auf diese Weise können Sie ein ziemlich generisches Ereignisbehandlungssystem erstellen. Es folgt ein FreeRTOS / Cortex-M-Skelettbeispiel.

#define EVENT_QUEUE_SIZE 32 // could be tricky to get right
xQueueHandle event_queue;

void Some_IRQHandler(void)
{
    // reset the interrupt pending bit

    event_t event;
    event.type = FOO; // if event_t is a tagged union
    event.foo = ...; // fill the structure with data from the peripheral

    // place into t he queue
    portBASE_TYPE task_woken = pdFALSE;
    xQueueSendFromISR(event_queue, &event, &task_woken);
}

void Other_IRQHandler(void)
{
    // same except
    event.type = BAR;
}


void handler_task(void *pvParameters)
{
    while(true) {
        event_t event;
        if(!xQueueReceive(event_queue, &event, portMAX_DELAY))
            continue;

        // process the event
        switch(event.type) {
            case FOO:
               ...
            break;

            ...
        }
    }
}

int main()
{
    // create the queue
    event_queue = xQueueCreate(EVENT_QUEUE_SIZE, sizeof(event_t));

    // create handler task
    xTaskCreate(handler_task, ...);

    // enable interrupts, start the scheduler
}