Handling critical section in FreeRTOS on RP2040/Pico?

I have just started with both the RP2040, and with FreeRTOS, so I’m hardly an expert in either.

However, I noticed something which I’d like to confirm - I guess you could call it a theoretical question.

Let’s say I have a UART receive interrupt service routine, which runs byte by byte; I would like to protect the UART receive buffer filling as a critical section, and would also like to protect the reading of the same buffer in “normal” code.

In RP2040, there is raspberrypi. github. io/pico-sdk-doxygen/group__critical__section.html) API; and I’ve written tests for RP2040, that in general look like:

// ...
critical_section_t myLock;
uint8_t myRxBuffer[];
// ...

void on_uart_rx_ISR() {
  // ...
  uint8_t ch = uart_getc(UART_ID);
  critical_section_enter_blocking(&myLock);
  myRxBuffer[buf_write_ptr++] = ch;
  critical_section_exit(&myLock);
  // ...
}

int main() {
  // ...
  critical_section_init(&myLock);
  // ...
  irq_set_exclusive_handler(UART_IRQ, on_uart_rx_ISR);
  irq_set_enabled(UART_IRQ, true);
  // ...
  while(true) {
    // ...
    // time to copy buffer
    critical_section_enter_blocking(&myLock);
    memcpy(final_buffer, myRxBuffer, buffer_size);
    critical_section_exit(&myLock);
    // ...
  }
}

I have done some very simple tests with the above structure, and I did not notice anything out of the ordinary - so I guess it works as intended.

What I like about this api, is that I can define different critical_section_t “lock” variables, and protect different sections of the code: for instance, I could have one lock for UART0 receive buffer, and another lock for UART1 receive buffer, and they shouldn’t interfere with one another (e.g. if main has locked UART0 for copying, and UART1 RX ISR fires, UART1 will proceed without issue).


Now I would like to do something similar, again on RP2040, but with FreeRTOS (if relevant, I’ve tried the smp branch of FreeRTOS). That would also mean, that “normal” code is not main() anymore as in above example, but a FreeRTOS task.

I assume that nothing will change in terms of defining the interrupts, because it is stated in forums. freertos. org/t/attaching-interrupt-to-pin-platform-specific-code/15107 :

Am I right in assuming that there are no FreeRTOS wrapper routines which handle such device-specific hardware-to-ISR mappings, and consequently any such code will likely be non-portable from device to device?

Yes, FreeRTOS doesn’t try to wrap all the possible hardware configurations for things like I/O to make a portable application, just a mostly portable task structure.

However, the only thing I’ve found comparable to the critical_section API of RP2040, is the www. freertos. org/taskENTER_CRITICAL_taskEXIT_CRITICAL.html , and www. freertos. org/taskENTER_CRITICAL_FROM_ISR_taskEXIT_CRITICAL_FROM_ISR.html

So, I imagine, the above example in FreeRTOS would look like:

// ...
uint8_t myRxBuffer[];
// ...

void on_uart_rx_ISR() {
  // ...
  uint8_t ch = uart_getc(UART_ID);
  UBaseType_t saved_intr_status = taskENTER_CRITICAL_FROM_ISR();
  myRxBuffer[buf_write_ptr++] = ch;
  taskEXIT_CRITICAL_FROM_ISR(saved_intr_status);
  // ...
}

void my_task( void* pvParameters ) {
  while(true) {
    // ...
    // time to copy buffer
    taskENTER_CRITICAL(); 
    memcpy(final_buffer, myRxBuffer, buffer_size);
    taskEXIT_CRITICAL();
    // ...
  }
}

int main() {
  // ...
  irq_set_exclusive_handler(UART_IRQ, on_uart_rx_ISR);
  irq_set_enabled(UART_IRQ, true);
  // ...
  xTaskCreate(my_task, "MY_Task", 256, NULL, 1, NULL);
  // ...
  vTaskStartScheduler();

  // should never reach here (FreeRTOS-SMP-Demos/FreeRTOS/Demo/CORTEX_M0+_RP2040/OnEitherCore/main.c)
  panic_unsupported();
}

Now, here are my doubts/problems:

  1. www. freertos. org/taskENTER_CRITICAL_taskEXIT_CRITICAL.html notes:

If the FreeRTOS port being used does not make use of the configMAX_SYSCALL_INTERRUPT_PRIORITY … then calling taskENTER_CRITICAL() will leave interrupts globally disabled. If the FreeRTOS port being used does make use of the configMAX_SYSCALL_INTERRUPT_PRIORITY …, then calling taskENTER_CRITICAL() will leave interrupts at and below the interrupt priority set by configMAX_SYSCALL_INTERRUPT_PRIORITY disabled, and all higher priority interrupt enabled.

So, if taskENTER_CRITICAL disables interrupts, it might lead to missed bytes (bytes not copied) in the UART RX ISR (which I have observed).

  1. taskENTER_CRITICAL/_FROM_ISR does not take any arguments.

That means, I cannot really use these functions to protect, say, two UARTs as I described above - because if I lock UART0 for copying, then that will also lock UART1 operation (interrupt disabling notwithstanding).

  1. As far as I can tell, critical_section_t from the RP2040/Pico SDK is basically a “spinlock”; and that would imply “busy wait” - as per stackoverflow. com/questions/38124337/spinlock-vs-busy-wait :

So a spin-lock is implemented using busy-waiting. Busy-waiting is useful in any situation where a very low latency response is more important than wasting CPU cycles (like in some types of embedded programming).

Which, I guess, means that I shouldn’t use critical_section_t API from RP2040/Pico SDK within FreeRTOS, as it would sort of defeat the purpose of FreeRTOS (primarily managing the timing of tasks).


So, in summary, here are my questions:

  1. Is there a FreeRTOS “native” “critical section” handling, that does not work globally as a single lock - but instead can allow for “multiple” locks (like the RP2040/Pico API allows: by using a spinlock/critical_section_t variable as argument to the relevant functions, which effectively provides multiple locks)?
  2. (even if I already arrived at the opposite conclusion above, maybe best to double-check:) Would it be OK to mix RP2040/Pico SDK “critical section” API, with ISRs and FreeRTOS tasks?

A “Spin-Lock” type critical section is totally unusable in an ISR of a single core processor, as the processor will just spin forever and NEVER get to the code that currently holds the lock to finish and release it. Remember ISRs NEVER get switched away from to go to task level code.

Even in a multi-core processor, if all the cores can be interrupted, you still run the risk of dead-lock if ALL the processors get into spin-locks that is held by a task level entity, as it will never get to run.

ISRs are different then just “High Priority” tasks, because of this sort of limitation, ISRs will NEVER get deferred to run a task.

Note, infinite spin locks are even bad at just task level unless they incorporate priority inheritance, as high priority tasks spinning on them will block the lower priority task that has the lock.

This wouldn’t be as much of a problem for a system that doesn’t have priority levels or doesn’t strictly enforce them.

1 Like

Many thanks, @richard-damon - your answer helped me put things into perspective.

At first, I thought about confirming what I’ve understood: considering that:

Even in a multi-core processor, if all the cores can be interrupted, you still run the risk of dead-lock if ALL the processors get into spin-locks that is held by a task level entity, as it will never get to run.

… would it be reasonable to say: considering this risk of dead-lock, is that why taskENTER_CRITICAL/_FROM_ISR (generally) “protect” a section by disabling interrupts (some or all)?

Then again, I thought I’d check these macros, and I arrived to this (note, .../ are github links):

.../FreeRTOS/FreeRTOS-Kernel/blob/smp/include/task.h:

#define taskENTER_CRITICAL()               portENTER_CRITICAL()
#define taskENTER_CRITICAL_FROM_ISR()      portSET_INTERRUPT_MASK_FROM_ISR()

.../FreeRTOS/FreeRTOS-Kernel/blob/smp/portable/ThirdParty/GCC/RP2040/include/portmacro.h

#define portENTER_CRITICAL()                   vTaskEnterCritical()

.../FreeRTOS/FreeRTOS-Kernel/blob/smp/portable/ThirdParty/GCC/rpi_pico/portmacro.h

#define portENTER_CRITICAL()                      vPortEnterCritical()

.../FreeRTOS/FreeRTOS-Kernel/blob/smp/tasks.c

    void vTaskEnterCritical( void )
    {
        portDISABLE_INTERRUPTS();

        if( xSchedulerRunning != pdFALSE )
        {
            if( pxCurrentTCB->uxCriticalNesting == 0U )
            {
                if( portCHECK_IF_IN_ISR() == pdFALSE )
                {
                    portGET_TASK_LOCK();
                }

                portGET_ISR_LOCK();
            }

            ( pxCurrentTCB->uxCriticalNesting )++;

            /* This should now be interrupt safe. The only time there would be
             * a problem is if this is called before a context switch and
             * vTaskExitCritical() is called after pxCurrentTCB changes. Therefore
             * this should not be used within vTaskSwitchContext(). */

            if( ( uxSchedulerSuspended == 0U ) && ( pxCurrentTCB->uxCriticalNesting == 1U ) )
            {
                prvCheckForRunStateChange();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }

... /FreeRTOS/FreeRTOS-Kernel/blob/smp/portable/ThirdParty/GCC/RP2040/include/portmacro.h

    #define portGET_TASK_LOCK()     vPortRecursiveLock(1, spin_lock_instance(configSMP_SPINLOCK_1), pdTRUE)
    #define portGET_ISR_LOCK()      vPortRecursiveLock(0, spin_lock_instance(configSMP_SPINLOCK_0), pdTRUE)

.../FreeRTOS/FreeRTOS-Kernel/blob/smp/portable/ThirdParty/GCC/rpi_pico/portmacro.h

#define portGET_TASK_LOCK()     do{ while(!portSIE_SPINLOCK1); }while(0)
#define portGET_ISR_LOCK()      do{ while(!portSIE_SPINLOCK0); }while(0)

First, I got confused why is there both a GCC/RP2040 and GCC/rpi_pico folder, considering that the RPi Pico is the demonstation board for the RP2040.

But leaving that aside, I can see that - at least in some circumstances - taskENTER_CRITICAL might resolve to code, that regardless, seems to wait on a spinlock!? I am speculating now, that maybe this was implemented as such, because this is the SMP branch, and there is the a-priori assumption that there are multiple cores …

This also answered my first question - there is apparently “waiting on multiple locks”, via vPortRecursiveLock(1, spin_lock_instance(configSMP_SPINLOCK_1), pdTRUE) (vs vPortRecursiveLock(0, spin_lock_instance(configSMP_SPINLOCK_0), pdTRUE)).

However, if:

  • I am to avoid spinlocks in the kind of example that I posted (due to the reasons in @richard-damon’s post /2); and
  • taskENTER_CRITICAL might still end up using them on the platform that I’m on

… what are my options now, in terms of “protecting” the writing of an array from ISR, and reading it elsewhere? Maybe I should change my approach totally within FreeRTOS, and just put bytes received in the UART recepetion ISR in something like a FreeRTOS queue (and thus, obviate the need to do “critical section” protection there, at the producer side - and indeed, also on the queue consumer side?)

I will begin with the caveat that I haven’t looked at the Multi-core SMP branch, so I am not familiar with what it does for multi-core systems.

The idea of the basic FreeRTOS critical section is that if you have a SHORT interval where you want to be sure no ISR interferes or gets confused by accessing the data mid-update, you just disable the interrupts, and then nothing can get in the way.

This method is simple and low impact (as long as the update is quick so section is small). We add just a handful of instructions to the interrupt response time due to the section, if it occurs during the section.

This method just doesn’t work for multi-core, as it doesn’t block the other cores. Core to Core blocking uses the spin lock, and you also need to disable interrupt for that core to get task to ISR blocking on that core.

Personally, I tend to use the provided methods that someone who has spent time studying the problem has generated (like the FreeRTOS Queue) rather than creating my own just for the sake of doing it. If/When I find a performance issue, then I can work on.a custom version optimized for my particular situation.

1 Like

Just wanted to say again many thanks for the response, @richard-damon - especially the “The idea of the basic FreeRTOS critical section is …” and "This method just doesn’t work for multi-core, … " - that helps me finally start getting a grasp on the specifics of FreeRTOS.

Btw, I was not really trying to work on a custom solution - it’s just that I have a somewhat limited experience, and I’ve only encountered the “spinlock” kind of “critical section” protection in my previous work; so that was my starting point in trying to understand how FreeRTOS would differ.

In all, now I’m pretty sure I should start looking into FreeRTOS Queue as an approach - and thanks again for helping me get here!