I'm selected for the STM32H7B3I-DK - DISCOVERY KIT road test.
In this part of the review, I try to integrate a UART listener in freeRTOS.
- listen to UART characters arriving from the USB port
- interrupt driven, no constant polling
- use HAL and freeRTOS
- to prevent polling tasks, use task notification to wake up the UART task at interrupt
- use the latest released versions of CubeIDE, HAL layer and CMSIS freeRTOS
Create a Project
In CubeIDE, create a new STM32 project
Select the board, STM32H7B3I-DK. I have added it to the favourites, so that each time I create a project, this one is in a shortlist.
Give it a name, select C , Executable, STMCude, then Finish
Configure Drivers and RTOS
With the MX configurator open, let's start defining the blocks.
First, in Middleware, enable freeRTOS, version CMSIS_V2.
In Tasks and Queues, add a task that will manage our UART:
The task is reviewed later in this post, when we write the content.
Then we configure timer 6 as the RTOS timer.
Last is USART1. I left the parameters: 115200, 8, N, 1.
Enabled the global interrupt in the NVIC tab,
In the SYS -> NVIC settings, I then enabled the USART global interrupt in the NVIC tab, Preemption priority 15.
In the Code Generation tab, I checked Generate IRQ handler and Call HAL handler
Then I saved and generated code.
Write the freeRTOS task, Task Notification and IRQ Handler
The mechanism that I'll use is:
- a non-blocking read from the UART driver of 1 character.
- this will prime the interrupt, that will fire later when a character arrives.
- a blocking "wait for notification". This is freeRTOS lightweight method to make a task wait for an event. Similar to binary semaphores, but lighter.
- the IRQ handler, that's called when the interrupt fires, will unblock that wait.
First the UART task:
TaskHandle_t xTaskToNotifyatUARTRx;
void TaskUARTListen(void *argument) { /* USER CODE BEGIN TaskUARTListen */ configASSERT( xTaskToNotifyatUARTRx == NULL ); xTaskToNotifyatUARTRx = xTaskGetCurrentTaskHandle(); uint8_t in[1]; uint16_t buffersize = 1; /* Infinite loop */ for(;;) { HAL_UART_Receive_IT(&huart1, in, buffersize); uint32_t ulNotificationValue = ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); if( ulNotificationValue == 1 ) { /* The transmission ended as expected. */ // todo here we handle the received character. } else { /* The call to ulTaskNotifyTake() timed out. */ } } /* USER CODE END TaskUARTListen */ }
At the start of the task, we initialise the notification hook.
Then the task loops forever (it actually doesn't loop most of the time, hang on ...)
The UART Rx interrupt is primed.
Then we make the task go sleep until we receive a notification.
As indicated above, that notification will come from the interrupt handler.
Right after the wait is released, the character that arrived is available in our buffer.
Then we repeat to arm the interrupt and set the wait. The wait takes virtually no processor time. Much efficienter than a UART poll loop.
Then the Interrupt Handler
This one, just like the task, gets automatically invoked because we did the MX configuration.
The code is generated in stm32h7xx_it.c. We need to write the activities to release the hold of the UART task.
#include "cmsis_os.h" extern TaskHandle_t xTaskToNotifyatUARTRx;
void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); /* USER CODE BEGIN USART1_IRQn 1 */ BaseType_t xHigherPriorityTaskWoken = pdFALSE; configASSERT( xTaskToNotifyatUARTRx != NULL ); vTaskNotifyGiveFromISR( xTaskToNotifyatUARTRx, &xHigherPriorityTaskWoken ); // xTaskToNotifyatUARTRx = NULL; portYIELD_FROM_ISR( xHigherPriorityTaskWoken ); /* USER CODE END USART1_IRQn 1 */ }
The first line is the HAL internal handler. It's called because we ask that in the configurator.
Then our code. Almost literally the same as the link to the freeRTOS instructions above.
You can clearly see that we notify the task, then yield back to freeRTOS.
In the background, the for() loop in TaskUARTListen() will run one turn, deal with the character that arrived over UART, then puts itself to sleep again.
That is it. To test it, check the COM port that's assigned to the USB of the development kit, and configure a serial terminal such as PuTTY or the Eclipse Terminal to 115200, 8, 1 N.
Set a breakpoint on this line
uint32_t ulNotificationValue = ulTaskNotifyTake( pdTRUE, portMAX_DELAY );
When the code reaches that line the first time, it'll stop.
Then use the Step Over function to execute that line only.
The debugger will not stop, because this line only returns if it receives a notification from the interrupt.
Type a character in the terminal, then see the debugger stop.
inspect the value of in[0]. It 'll be the character you typed in the terminal.
Resume the project and type another character, ...