on 2024-07-08 03:59 AM
In this guide, we explore how to integrate and use the FreeRTOS+ command-line interface (CLI) library to enhance your embedded applications. Designed for ease of use and flexibility, the FreeRTOS+ CLI allows developers to create robust command-line interfaces for debugging, configuration, and user interaction. By the end of this tutorial, you will understand how to implement and customize the CLI to meet your specific needs, making your development process more efficient and intuitive.
Familiarity with FreeRTOS™ basics is recommended before following this guide. ST’s YouTube channel offers an excellent MOOC on FreeRTOS™.
You need STM32CubeMX, STM32CubeIDE, or VS Code with the STM32 extension.
To get started, you'll need the FreeRTOS-Plus CLI files. You have two options:
Either option requires you to copy and save the following files into your src and inc folders of your project:
FreeRTOS_CLI.c
FreeRTOS_CLI.h
These files contain the basic functionality of the command parser. Note that we integrate FreeRTOS via STM32CubeMX, so you do not need the entire FreeRTOS repo; only download the needed files.
Download the two files included at the bottom of this article, they include some necessary code explained later on to get the CLI up and running.
app_cli.c
app_cli.h
The provided code is hardware agnostic, so any STM32 Nucleo or discovery board works. Additionally, any custom board capable of outputting text to a display suffices. This guide uses a Nucleo-H503RB.
First, include FreeRTOS™ in your embedded application. If you have not installed the FreeRTOS expansion pack, do so now.
You can read more about FreeRTOS memory management schemes here: FreeRTOS memory management.
Now that the software pack is available, add it to the project from the "Middleware and Software Packs" section of the configurator and enable CMSIS RTOS2. Note that CMSIS RTOS are just wrapper functions for FreeRTOS, and you can still call the native FreeRTOS functions.
To avoid warnings, change the clock that HAL uses for its tick timebase. Choose a basic timer on your MCU; timer 6 is used in this guide.
Configure USART for VCP according to your board's user manual. On the Nucleo-H503RB, this is USART 3. Enable the global USART interrupt to parse the Rx stream and verify it is enabled on the NVIC line. Consult the user manual for your specific board to find what USART is used for VCP.
Configure your project settings in the project manager according to your preferences and application needs. This guide enables the generation of c/h file pairs for each peripheral used. Generate the code and open it in your IDE or editor of choice.
In CubeIDE, add the FreeRTOS_CLI.h
file into the <project_name>/Core/Inc
directory and the FreeRTOS_CLI.c
source file into the <project_name>/Core/Src
directory.
In VS Code add the FreeRTOS_CLI.h
file into the <project_name>/Core/Inc
directory and the FreeRTOS_CLI.c
source file into the <project_name>/Core/Src
directory, add the new source file path to the CMakeLists.txt file under the user section.
# Add sources to executable
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# Add user sources here
${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/FreeRTOS_CLI.c
${CMAKE_CURRENT_SOURCE_DIR}/Core/Src/cli_app.c
)
At the bottom of this article, there exist cli_app.c
and cli_app.h
, which provide the necessary functionality to get the CLI running. This includes creating the CLI task, handling backspace and enter key inputs, retargeting printf
, and a command for clearing the screen. Some of these features/commands may be terminal-specific and may not work for your specific terminal emulator, so adjust them as needed. This code has been tested with the VS Code terminal extension, PuTTY, and Tera Term. Just like the FreeRTOS_CLI.c/h
files add these to your <project_name>/Core/Src
and <project_name>/Core/Inc
directory and make sure they are included in your project.
Note that the _write function to retarget printf is specific to your MCU of choice as it directly interacts with a USART instance.
The explanations that follow detail the code in clip_app.c/h
. The only modifications required to follow along are changes to main.c
, app_freertos.c
, FreeRTOSConfig.h
, and the file where your USART interrupt is handler such as stm32h5xx_it.c
Lets cover the fundamentals of using this parser. Getting the CLI to run your commands requires three simple steps.
You do not need to copy any of the code in the following sections until the Integrating the CLI code section later in this article.
/* The structure that defines command line commands. A command line command
* should be defined by declaring a const structure of this type. */
typedef struct xCOMMAND_LINE_INPUT
{
const char * const pcCommand; // The command string (e.g., "help"). Must be lowercase.
const char * const pcHelpString; // Describes how to use the command, ending with "\r\n".
const pdCOMMAND_LINE_CALLBACK pxCommandInterpreter; // Callback function returning the command's output.
int8_t cExpectedNumberOfParameters; // Number of expected parameters (0 or more).
} CLI_Command_Definition_t;
Each command is of a CLI_Command_Definition_t
type defined in FreeRTOS_CLI.h
consisting of four elements:
pcCommand
: The CLI command string (for example, "myCommand").pcHelpString
: A help string that is printed when "help" is typedpxCommandInterpreter
: Pointer to the command handler function. This handler must have a specific function prototype defined by the function pointer pdCOMMAND_LINE_CALLBACK
in FreeRTOS.H
cExpectedNumberOfParameters
: Number of expected parameters (use -1 for variable parameters).The code below demonstrates how you would make a command on its own.
CLI_Command_Definition_t myCommand = {
.pcCommand = "mycommand", /* The command string to type. */
.pcHelpString = "mycommand:\r\n This is my custom command\r\n\r\n",
.pxCommandInterpreter = myCommandHandler, /* The function to run. */
.cExpectedNumberOfParameters = 0 /* No parameters are expected. */
};
Alternatively, if you have multiple commands to register you can always make an array of commands.
const CLI_Command_Definition_t xCommandList[] = {
{
.pcCommand = "mycommand", /* The command string to type. */
.pcHelpString = "mycommand:\r\n This is my custom command\r\n\r\n",
.pxCommandInterpreter = myCommandHandler, /* The function to run. */
.cExpectedNumberOfParameters = 0 /* No parameters are expected. */
},
{
.pcCommand = "toggleled", /* The command string to type. */
.pcHelpString = "toggleled n:\r\n toggles led n amount of times\r\n\r\n",
.pxCommandInterpreter = cmd_toggle_led, /* The function to run. */
.cExpectedNumberOfParameters = 1 /* No parameters are expected. */
},
{
.pcCommand = NULL /* simply used as delimeter for end of array*/
}
};
Each command has a callback/handler function that gets executed to give that command functionality. Command handlers must have a specific function prototype as seen below.
static BaseType_t cmd_toggle_led(char *pcWriteBuffer, size_t xWriteBufferLen,
const char *pcCommandString)
{
// functionality here
}
FreeRTOS-Plus-CLI allows command handlers to output one line at a time via pcWriteBuffer
at some point in the command handler you must fill that buffer with any output text your command may have.
Use the function's return value to indicate if more lines are needed.
pdFALSE
to indicate the output is complete and no more lines are needed.pdTRUE
to indicate more lines are needed. The function should handle this accordingly. If you need to output more characters than allowed by configCOMMAND_INT_MAX_OUTPUT_SIZE
, your handler must allow for this by keeping static variables aware of its reentrancy. Alternatively, if configCOMMAND_INT_MAX_OUTPUT_SIZE
is large enough, you can output multiple lines at once.
For example, below is a simple command handler that toggles an LED and prints out a string indicating the LED was toggled.
static BaseType_t cmd_toggle_led(char *pcWriteBuffer, size_t xWriteBufferLen,
const char *pcCommandString)
{
(void)pcCommandString; // comntains the command string
(void)xWriteBufferLen; // contains the length of the write buffer
/* Toggle the LED */
//ToggleLED(); // implement your code to toggle led
/* Write the response to the buffer */
uint8_t string[] = "LED toggled\r\n";
strcpy(pcWriteBuffer, (char *)string);
return pdFALSE; // no more string output is needed
}
To extrapolate the parameters passed to your command, you use the library function below and pass the command string received in the handler, the desired parameter you want in the order they were typed and somewhere to store the string length of that parameter
const char * FreeRTOS_CLIGetParameter( const char * pcCommandString,
UBaseType_t uxWantedParameter,
BaseType_t * pxParameterStringLength )
Below is an example of a command that adds two numbers and extrapolates the parameters to return the result. Note that even though parameters are typed as numbers, everything is treated as a string and conversions must be done.
BaseType_t cmd_add(char *pcWriteBuffer, size_t xWriteBufferLen,
const char *pcCommandString)
{
// hold pointer to parameter string
char *pcParameter1, *pcParameter2;
BaseType_t xParameter1StringLength, xParameter2StringLength;
pcParameter1 = FreeRTOS_CLIGetParameter
(
/* The command string itself. */
pcCommandString,
/* Return the first parameter. */
1,
/* Store the parameter string length. */
&xParameter1StringLength
);
pcParameter2 = FreeRTOS_CLIGetParameter
(
/* The command string itself. */
pcCommandString,
/* Return the first parameter. */
2,
/* Store the parameter string length. */
&xParameter2StringLength
);
// convert the string to a number
int32_t xValue1 = strtol(pcParameter1, NULL, 10);
int32_t xValue2 = strtol(pcParameter2, NULL, 10);
// add the two numbers
int32_t xResultValue = xValue1 + xValue2;
// convert the result to a string
char cResultString[10];
itoa(xResultValue, cResultString, 10);
// copy the result to the write buffer
strcpy(pcWriteBuffer, cResultString);
return pdFALSE;
}
Every command must be registered with the CLI interface. Using the array from above we can iterate through our list registering each command, below is a custom function that accomplishes this.
void vRegisterCLICommands(void){
//itterate thourgh the list of commands and register them
for (int i = 0; xCommandList[i].pcCommand != NULL; i++)
{
FreeRTOS_CLIRegisterCommand(&xCommandList[i]);
}
}
The CLI consists of just the command parsing mechanism, command handler execution, and basic parameter handling. That being said there are a few things we need to add to get the CLI up and going.
Before we can run a test build, add a max size for the output buffer in FreeRTOSConfig.h
. This is the maximum number of characters that you allow the CLI to print out at any given time, an output buffer of this size is made internally. Scroll to the bottom and define configCOMMAND_INT_MAX_OUTPUT_SIZE
according to your needs.
/* USER CODE BEGIN Defines */
/* Section where parameter definitions can be added (for instance, to override default ones in FreeRTOS.h) */
#define configCOMMAND_INT_MAX_OUTPUT_SIZE 200
/* USER CODE END Defines */
The USART interrupt routing needs to notify the CLI of the new character that was received and get ready to receive the next character. Below is my USART IRQ handler, the comments make it self-explanatory. Make sure that the file where your USART handler is located has included the proper headers and also include an extern reference to the CLI task handle.
/* USER CODE BEGIN Includes */
#include "app_freertos.h"
#include "task.h"
/* USER CODE END Includes */
/* USER CODE BEGIN PV */
extern osThreadId_t cmdLineTaskHandle;
/* USER CODE END PV */
void USART3_IRQHandler(void)
{
/* USER CODE BEGIN USART3_IRQn 0 */
/* USER CODE END USART3_IRQn 0 */
HAL_UART_IRQHandler(&huart3);
/* USER CODE BEGIN USART3_IRQn 1 */
// grab char from data register
char rxedValue = USART3->RDR & 0xFF;
//get ready to receive another char
HAL_UART_Receive_IT(&huart3, (uint8_t *)&huart3.Instance->RDR, 1);
//send the char to the command line task
xTaskNotifyFromISR(cmdLineTaskHandle, (uint32_t)rxedValue, eSetValueWithOverwrite , NULL);
/* USER CODE END USART3_IRQn 1 */
}
In your main function before the kernel is started you need to set your USART to receive interrupt mode.
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ICACHE_Init();
MX_USART3_UART_Init();
osKernelInitialize();
/* Call init function for freertos objects (in cmsis_os2.c) */
MX_FREERTOS_Init();
// get USART ready to receive
HAL_UART_Receive_IT(&huart3, (uint8_t *)&huart3.Instance->RDR, 1);
/* Start scheduler */
osKernelStart();
while(1){}
}
Now, we need to make a task that runs the FreeRTOS CLI parser. In app_freertos.c
we create a new variable for the task handle and an instance of the task attributes for the command line task as seen here. Note that the actual task is defined in the file app_cli.c
included at the bottom of this article.
Include the necessary header:
/* USER CODE BEGIN Includes */
#include "cli_app.h"
/* USER CODE END Includes */
Make the task handle and attributes:
/* USER CODE BEGIN Variables */
osThreadId_t cmdLineTaskHandle; // new command line task
const osThreadAttr_t cmdLineTask_attributes = {
.name = "cmdLineTask", // defined in cli_app.c
.priority = (osPriority_t) osPriorityLow,
.stack_size = 128 * 4
};
/* USER CODE END Variables */
Still, in app_freertos.c
we scroll to MX_FREERTOS_Init()
and register our command line task:
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* USER CODE BEGIN RTOS_MUTEX */
/* add mutexes, ... */
/* USER CODE END RTOS_MUTEX */
/* USER CODE BEGIN RTOS_SEMAPHORES */
/* add semaphores, ... */
/* USER CODE END RTOS_SEMAPHORES */
/* USER CODE BEGIN RTOS_TIMERS */
/* start timers, add new ones, ... */
/* USER CODE END RTOS_TIMERS */
/* USER CODE BEGIN RTOS_QUEUES */
/* add queues, ... */
/* USER CODE END RTOS_QUEUES */
/* creation of defaultTask */
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
/* USER CODE BEGIN RTOS_THREADS */
/* add threads, ... */
cmdLineTaskHandle = osThreadNew(vCommandConsoleTask, NULL, &cmdLineTask_attributes);
/* USER CODE END RTOS_THREADS */
/* USER CODE BEGIN RTOS_EVENTS */
/* add events, ... */
/* USER CODE END RTOS_EVENTS */
}
At this point, we have included the necessary files in our build
We have registered the new CLI task with FreeRTOS and made our USART interrupt notify the CLI task of a new character. The USART is made ready to receive a character before our kernel starts, optionally this could be done at the start of our CLI task.
A max number of characters for the CLI output buffer was defined in FreeRTOSConfig.h
At this point, the application can be flashed and run on the MCU. Open up a terminal emulator/COM port viewer and interact with your CLI.
Typing help
and pressing enter should list all the registered commands along with their help strings.
cli_app.c showcases how to handle parameters and implement basic command handlers.
I hope this is helpful. The CLI lends itself to be feature-rich or watered down to bare essential functionality. Some enhancements could include pressing tab to show command hints based on current input string, up and down arrows to cycle through command history. Feel free to comment on issues encountered or bugs in the code, keeping in mind that commands like clear screen, or even backspace may be handled differently depending on the standard used by the com port viewer or terminal emulator.
cli_app.h and cli_app.c
#ifndef CLI_APP_H
#define CLI_APP_H
#include "stdint.h"
void processRxedChar(uint8_t rxChar);
void handleNewline(const char *const pcInputString, char *cOutputBuffer, uint8_t *cInputIndex);
void handleCharacterInput(uint8_t *cInputIndex, char *pcInputString);
void vRegisterCLICommands(void);
void vCommandConsoleTask(void *pvParameters);
#endif // CLI_APP_H
#ifndef CLI_COMMANDS_H
#define CLI_COMMANDS_H
#include "main.h"
#include "FreeRTOS.h"
#include "task.h"
#include "FreeRTOS_CLI.h"
#include "stdbool.h"
#include "string.h"
#include "stdio.h"
#include "stdlib.h"
#define MAX_INPUT_LENGTH 50
#define USING_VS_CODE_TERMINAL 0
#define USING_OTHER_TERMINAL 1 // e.g. Putty, TerraTerm
char cOutputBuffer[configCOMMAND_INT_MAX_OUTPUT_SIZE], pcInputString[MAX_INPUT_LENGTH];
extern const CLI_Command_Definition_t xCommandList[];
int8_t cRxedChar;
const char * cli_prompt = "\r\ncli> ";
/* CLI escape sequences*/
uint8_t backspace[] = "\b \b";
uint8_t backspace_tt[] = " \b";
int _write(int file, char *data, int len)
{
UNUSED(file);
// Transmit data using UART2
for (int i = 0; i < len; i++)
{
// Send the character
USART3->TDR = (uint16_t)data[i];
// Wait for the transmit buffer to be empty
while (!(USART3->ISR & USART_ISR_TXE));
}
return len;
}
//*****************************************************************************
BaseType_t cmd_clearScreen(char *pcWriteBuffer, size_t xWriteBufferLen,
const char *pcCommandString)
{
/* Remove compile time warnings about unused parameters, and check the
write buffer is not NULL. NOTE - for simplicity, this example assumes the
write buffer length is adequate, so does not check for buffer overflows. */
(void)pcCommandString;
(void)xWriteBufferLen;
memset(pcWriteBuffer, 0x00, xWriteBufferLen);
printf("\033[2J\033[1;1H");
return pdFALSE;
}
//*****************************************************************************
BaseType_t cmd_toggle_led(char *pcWriteBuffer, size_t xWriteBufferLen,
const char *pcCommandString)
{
(void)pcCommandString; // comntains the command string
(void)xWriteBufferLen; // contains the length of the write buffer
/* Toggle the LED */
//ToggleLED();
/* Write the response to the buffer */
uint8_t string[] = "LED toggled\r\n";
strcpy(pcWriteBuffer, (char *)string);
return pdFALSE;
}
//*****************************************************************************
BaseType_t cmd_add(char *pcWriteBuffer, size_t xWriteBufferLen,
const char *pcCommandString)
{
char *pcParameter1, *pcParameter2;
BaseType_t xParameter1StringLength, xParameter2StringLength;
/* Obtain the name of the source file, and the length of its name, from
the command string. The name of the source file is the first parameter. */
pcParameter1 = FreeRTOS_CLIGetParameter
(
/* The command string itself. */
pcCommandString,
/* Return the first parameter. */
1,
/* Store the parameter string length. */
&xParameter1StringLength
);
pcParameter2 = FreeRTOS_CLIGetParameter
(
/* The command string itself. */
pcCommandString,
/* Return the first parameter. */
2,
/* Store the parameter string length. */
&xParameter2StringLength
);
// convert the string to a number
int32_t xValue1 = strtol(pcParameter1, NULL, 10);
int32_t xValue2 = strtol(pcParameter2, NULL, 10);
// add the two numbers
int32_t xResultValue = xValue1 + xValue2;
// convert the result to a string
char cResultString[10];
itoa(xResultValue, cResultString, 10);
// copy the result to the write buffer
strcpy(pcWriteBuffer, cResultString);
return pdFALSE;
}
const CLI_Command_Definition_t xCommandList[] = {
{
.pcCommand = "cls", /* The command string to type. */
.pcHelpString = "cls:\r\n Clears screen\r\n\r\n",
.pxCommandInterpreter = cmd_clearScreen, /* The function to run. */
.cExpectedNumberOfParameters = 0 /* No parameters are expected. */
},
{
.pcCommand = "toggleled", /* The command string to type. */
.pcHelpString = "toggleled n:\r\n toggles led n amount of times\r\n\r\n",
.pxCommandInterpreter = cmd_toggle_led, /* The function to run. */
.cExpectedNumberOfParameters = 0 /* No parameters are expected. */
},
{
.pcCommand = "add", /* The command string to type. */
.pcHelpString = "add n:\r\n add two numbers\r\n\r\n",
.pxCommandInterpreter = cmd_add, /* The function to run. */
.cExpectedNumberOfParameters = 2 /* 2 parameters are expected. */
},
{
.pcCommand = NULL /* simply used as delimeter for end of array*/
}
};
void vRegisterCLICommands(void){
//itterate thourgh the list of commands and register them
for (int i = 0; xCommandList[i].pcCommand != NULL; i++)
{
FreeRTOS_CLIRegisterCommand(&xCommandList[i]);
}
}
/*************************************************************************************************/
void cliWrite(const char *str)
{
printf("%s", str);
// flush stdout
fflush(stdout);
}
/*************************************************************************************************/
void handleNewline(const char *const pcInputString, char *cOutputBuffer, uint8_t *cInputIndex)
{
cliWrite("\r\n");
BaseType_t xMoreDataToFollow;
do
{
xMoreDataToFollow = FreeRTOS_CLIProcessCommand(pcInputString, cOutputBuffer, configCOMMAND_INT_MAX_OUTPUT_SIZE);
cliWrite(cOutputBuffer);
} while (xMoreDataToFollow != pdFALSE);
cliWrite(cli_prompt);
*cInputIndex = 0;
memset((void*)pcInputString, 0x00, MAX_INPUT_LENGTH);
}
/*************************************************************************************************/
void handleBackspace(uint8_t *cInputIndex, char *pcInputString)
{
if (*cInputIndex > 0)
{
(*cInputIndex)--;
pcInputString[*cInputIndex] = '\0';
#if USING_VS_CODE_TERMINAL
cliWrite((char *)backspace);
#elif USING_OTHER_TERMINAL
cliWrite((char *)backspace_tt);
#endif
}
else
{
#if USING_OTHER_TERMINAL
uint8_t right[] = "\x1b\x5b\x43";
cliWrite((char *)right);
#endif
}
}
/*************************************************************************************************/
void handleCharacterInput(uint8_t *cInputIndex, char *pcInputString)
{
if (cRxedChar == '\r')
{
return;
}
else if (cRxedChar == (uint8_t)0x08 || cRxedChar == (uint8_t)0x7F)
{
handleBackspace(cInputIndex, pcInputString);
}
else
{
if (*cInputIndex < MAX_INPUT_LENGTH)
{
pcInputString[*cInputIndex] = cRxedChar;
(*cInputIndex)++;
}
}
}
/*************************************************************************************************/
void vCommandConsoleTask(void *pvParameters)
{
uint8_t cInputIndex = 0; // simply used to keep track of the index of the input string
uint32_t receivedValue; // used to store the received value from the notification
UNUSED(pvParameters);
vRegisterCLICommands();
for (;;)
{
xTaskNotifyWait(pdFALSE, // Don't clear bits on entry
0, // Clear all bits on exit
&receivedValue, // Receives the notification value
portMAX_DELAY); // Wait indefinitely
//echo recevied char
cRxedChar = receivedValue & 0xFF;
cliWrite((char *)&cRxedChar);
if (cRxedChar == '\r' || cRxedChar == '\n')
{
// user pressed enter, process the command
handleNewline(pcInputString, cOutputBuffer, &cInputIndex);
}
else
{
// user pressed a character add it to the input string
handleCharacterInput(&cInputIndex, pcInputString);
}
}
}
#endif /* CLI_COMMANDS_H */