¿Cómo reducir el código de interrupción al mínimo?

Tengo alguna interrupción, digamos de UART para hacer un ejemplo real:

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

Pero el código de interrupción debe ejecutarse lo más rápido posible. Y aquí tenemos algunas funciones internas que consumen mucho tiempo.

La pregunta es: ¿Cuál es la forma correcta de implementar interrupciones que llaman a funciones que consumen mucho tiempo?

Una de mis ideas es crear un búfer de banderas y banderas en interrupción. Y procesar el búfer de bandera en el bucle principal llamando a las funciones apropiadas. ¿Es correcto?

La respuesta básica es diseñar el sistema correctamente desde el principio. Explique lo que su sistema necesita lograr, no cómo cree que debería lograrse, y es posible que podamos sugerirle una arquitectura adecuada. Sin algún tipo de especificación, esto no es una pregunta o es demasiado abierto.
Haga las cosas críticas de tiempo en la rutina de interrupción y use banderas que se manejarán en main para todas las demás cosas. Donde el tiempo crítico está en el orden de un par de ciclos de instrucción precisos.
@Olin Lathrop: este ejemplo provino de un generador de señales controlado por software de PC. Envío comandos a través de UART y deberían cambiar la señal generada, los parámetros, etc. Pero mi objetivo era generalizar esta pregunta para saber cuáles son los buenos estilos y patrones de diseño al implementar interrupciones.

Respuestas (3)

UART es, de hecho, un caso bastante típico porque muchas aplicaciones requieren que se realice algún procesamiento en respuesta al comando/fecha recibido a través del puerto serie. Si la arquitectura de la aplicación gira en torno a un ciclo de procesamiento infinito, como suele ser el caso, una buena manera es usar DMA para transferir los bytes recibidos a un búfer pequeño y procesar este búfer en cada iteración del ciclo. El siguiente código de ejemplo ilustra esto:

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

Lo que hace este código es configurar primero un canal DMA para leer desde USART2. El controlador DMA, la transmisión y el canal correctos dependen del USART que utilice (consulte el manual de referencia de STM32 para averiguar qué combinación se necesita para un puerto USART determinado). Luego, el código ingresa al bucle infinito principal. En cada bucle, el código comprueba si se ha escrito algo (a través de DMA) en formato inputBuffer. Si es así, estos datos son procesados ​​por processByte, que debe implementar de una manera similar a su controlador de IRQ original.

Lo bueno de esta configuración es que no hay código de interrupción: todo se ejecuta sincrónicamente. Gracias a DMA, los datos recibidos simplemente aparecen "mágicamente" en archivos inputBuffer. Sin embargo, el tamaño inputBufferdebe determinarse cuidadosamente. Debe ser lo suficientemente grande como para contener todos los datos que posiblemente pueda recibir durante una iteración de bucle. Por ejemplo, con una velocidad de transmisión de 115200 (alrededor de 11 KB/s) y un tiempo de bucle máximo de 50 ms, el tamaño del búfer debe ser de al menos 11 KB/s * 50 ms = 550 bytes.

Sería bueno si un canal DMA pudiera configurarse para actuar como un FIFO continuo, especialmente si el controlador DMA pudiera manejar el protocolo de enlace de una manera razonable. Conceptualmente, no debería ser demasiado difícil. Desafortunadamente, ninguno de los controladores DMA con los que estoy familiarizado proporciona tal facilidad; ¿El de STM32?
@supercat: sería bueno, pero no creo que pueda hacer eso.
Me pregunto cuánto costaría agregar algo de capacidad de apretón de manos. Sé que he visto búferes DMA con registros de "envoltura". Básicamente, todo lo que debería ser necesario para un FIFO completo sería un registro de "dirección de parada"; el controlador de DMA debe hacer una pausa en cualquier momento en que la próxima solicitud sea en esa dirección. Cuando la CPU agrega datos al búfer o lee datos (haciendo espacio disponible para nuevos datos entrantes), actualiza el registro de dirección de parada de manera adecuada, una operación que podría realizarse de manera segura incluso mientras las operaciones estaban en curso.

Realmente depende. Si es importante que el código dentro de su controlador se procese "inmediatamente", entonces no hay forma de evitar esto, aparte de evitar costosas llamadas a funciones externas (es decir, implementar la funcionalidad de la función llamada dentro del controlador). Si todo lo que le preocupa es leer los datos entrantes de su USART, pero los datos en sí pueden "tratarse" más adelante, es mejor que use un ISR muy simple, o mejor aún, el DMA, y algún búfer externo. que puede contener temporalmente los datos entrantes. ST tiene una buena nota de aplicación AN3109 que muestra cómo hacer esto.

La inserción no solo evita la sobrecarga de llamadas, sino que también permite la especialización del código (strncmp es una función bastante general) y la programación de instrucciones en el código que habría estado en llamadas separadas. No usar strncmp también podría permitir evitar vaciar el búfer, ya que la terminación cero ya no sería necesaria. De manera similar, si USART2_SendText() es lento, concatenar las cadenas en lugar de usar dos llamadas podría mejorar WCET; colocando "comando desconocido:" inmediatamente antes de recibir_cadena podría hacer esto gratis.
Por cierto, es posible que el compilador no sea lo suficientemente inteligente como para extraer las llamadas condicionales al establecer condicionalmente un puntero de cadena. Esto permitiría el uso de movimientos condicionales en lugar de dos de las ramas (posiblemente ayudando a WCET) y reduciría el tamaño del código.

En mi experiencia, el método más general disponible para los desarrolladores integrados son las colas de mensajes que proporcionan la mayoría de los RTOS. El controlador de interrupciones colocará los datos recibidos en la cola y la tarea del controlador (que se ejecuta en la prioridad de "subproceso" para usar un término Cortex-M) recibirá y procesará los datos en un bucle. Esto evita banderas, bloqueos, semáforos, etc., que son una fuente constante de errores. Por supuesto, este método tiene sus desventajas, por ejemplo, un uso bastante alto de RAM y la necesidad de un RTOS. Aún así, lo encuentro bastante justificado si la lógica a implementar es lo suficientemente compleja (y la RAM disponible no está demasiado restringida).

Puede crear un sistema de manejo de eventos bastante genérico de esta manera. A continuación se muestra un ejemplo de esqueleto de FreeRTOS/Cortex-M.

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