Making FreeRTOS CLI more CLI-ish
I am making the assumption that you have a mechanism to transmit a byte across the UART interface, by mechanism I mean a microcontroller of course, but some people also use simulators or perhaps a software UART or some exotic thing I have never heard of, nevertheless, you should be able to transmit and receive through UART. Using interrupts or DMA or polling is beside the point and you can handle those details however you want.
Hardware / Software
I will be using an STM32F446RE Nucleo since its onboard ST link will provide the virtual comport which I will use to communicate to minicom on my Linux machine. I am running Ubuntu 22, everything here will also work on Windows using something like Putty as your com port monitor.
I am not using Cube IDE to write code. I am however using Cube MX to generate a Makefile project and VS code as my editor. None of that makes any difference, that I am aware of, so get in loosers we are going coding.
Project and file setup
I like to start my projects as mentioned in my previous blog post so you can read that if you’d like , the gist of it is that I am going to make a separate application c/h file pair. I do not like seeing all those comment sections and having to tip toe around them to type my code in designated areas.
Make an STM32 project: USART 2 with interrupt + LED pin
Configure a project however you like at setup the UART interface, since I am using an F4 Nucleo I will setup UART 2 which is connected to the virtual com port on the ST Link , I will enable its global interrupt and also configure an LED pin
I like to generate my projects with seprate c and h files for all the peripherals used. I makes it easier to go find things instead of scrolling through a long monolithic main.c file.
Import FreeRTOS and FreeRTOS CLI
With my project created I import freeRTOS, if you do not know how to do this you can watch my video which will walk you through the process here
. After freeRTOS lives in our project then we can go fetch freeRTOS CLI extension which lives in this github page:
https://github.com/FreeRTOS/FreeRTOS/tree/main/FreeRTOS-Plus/Source/FreeRTOS-Plus-CLI
You simply need to add the FreeRTOS_CLI.c and h files and make sure your IDE or Makefile know where you put them.
I will show you my Makefile in a bit but first lets make all the files we need.
Make a main_app.c / h pair
main_app.c/h are where all of my code will live.
Make a commands.c
This file is where we will write and configure all the commands the CLI will support
And that is pretty much it as far as new file creation is concerned. Obviously you need to make your FreeRTOSConfig.h file but that is usually copy pasted from some other project or their demos repo, this is not a tutorial on FreeRTOS but rather the CLI extension. You can watch the previously mentioned video for a thorough FreeRTOS project setup.
Configure Makefile
This only applies if you are using a Makefile, if you are using an IDE the video I keep mentioning shows you how to add all these files so that CUBEIDE can compile them. As for me I simply add all the files I will need to my Makefile like pictured below. You can see the full test in the github repo linked at the end.
Ok that was all housekeeping things that I quickly went through because I assume this is not your first rodeo.
Retarget printf
For the sake of simplicity I will simply reroute the printf function to output on the UART instead of implementing my usual printf alternative.
You can do this in your main.c like this:
extern UART_HandleTypeDef huart2;
int _write(int file, char *ptr, int len) {
HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, HAL_MAX_DELAY);
return len;
}
Do not forget to include stdio.h in main for this to work. These are the additional includes I have added to main aside from the generated ones
#include "main_app.h"
#include "stdio.h"
I include everything my main_app.c will need to operate properly into my main_app.h. Also notice the main_app_init function which will add into the main_app.c file
#ifndef MAIN_APP_H_
#define MAIN_APP_H_
#include "FreeRTOS.h"
#include "FreeRTOSConfig.h"
#include "list.h"
#include "main.h"
#include "semphr.h"
#include "stdarg.h"
#include "stdint.h"
#include "stdio.h"
#include "stm32f4xx_hal.h"
#include "stream_buffer.h"
#include "string.h"
#include "task.h"
#include "timers.h"
void main_app_init(void);
#endif /* MAIN_APP_H_ */
main_app.c
Now here I will include my main_app.h and for now just make an empty init function
1 2 3 4 5 6 | #include "main_app.h" void main_app_init(void) { } |
main.c
Now lets go back to the generated files and in main.c we have to call this main_app_init function just as pictured on line 98
usart.c
Since I generated my project with seprate c/h files for the peripherals I have a usart.c file where the USART generated code live. In there I go into MSP init function and I enable the Not Empty Interrupt as shown below on line 89
Getting the CLI to work
Before I attempt to improve the CLI application I have to first get it to a working state.
Add a config in FreeRTOSConfig.h
I added this line the bottom of the FreertosConfig.h file
1 2 | /* FreeRTOS+CLI requires this size to be defined, but we do not use it */ #define configCOMMAND_INT_MAX_OUTPUT_SIZE 1 |
Global buffer
I made a global varialbe at the top of my main_app.c to hold each byte I receive through UART like so:
1 | uint8_t cRxedChar = 0x00; |
UART interrupt : stm32f4xx_it.c
Next I go to my USART interrupt file and I copy the contents of the data register into this variable. I also make sure to include the variable as an extern
I inserted this somewhere in the comments where it says I can insert user code at the top of the file
1 | extern uint8_t cRxedChar; |
1 2 3 4 5 6 7 8 9 10 | void USART2_IRQHandler(void) { /* USER CODE BEGIN USART2_IRQn 0 */ cRxedChar = USART2->DR; /* USER CODE END USART2_IRQn 0 */ HAL_UART_IRQHandler(&huart2); /* USER CODE BEGIN USART2_IRQn 1 */ /* USER CODE END USART2_IRQn 1 */ } |
The command line task
I went to the freeRTOS website and found a section about the CLI lnked below
I found their CLI documentation and on that page the have a sample CLI task which I copy and pasted it all into my main_app.c file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | #define MAX_INPUT_LENGTH 50 #define MAX_OUTPUT_LENGTH 100 static const int8_t * const pcWelcomeMessage = "FreeRTOS command server.rnType Help to view a list of registered commands.rn"; void vCommandConsoleTask( void *pvParameters ) { Peripheral_Descriptor_t xConsole; int8_t cRxedChar, cInputIndex = 0; BaseType_t xMoreDataToFollow; /* The input and output buffers are declared static to keep them off the stack. */ static int8_t pcOutputString[ MAX_OUTPUT_LENGTH ], pcInputString[ MAX_INPUT_LENGTH ]; /* This code assumes the peripheral being used as the console has already been opened and configured, and is passed into the task as the task parameter. Cast the task parameter to the correct type. */ xConsole = ( Peripheral_Descriptor_t ) pvParameters; /* Send a welcome message to the user knows they are connected. */ FreeRTOS_write( xConsole, pcWelcomeMessage, strlen( pcWelcomeMessage ) ); for( ;; ) { /* This implementation reads a single character at a time. Wait in the Blocked state until a character is received. */ FreeRTOS_read( xConsole, &cRxedChar, sizeof( cRxedChar ) ); if( cRxedChar == '\n' ) { /* A newline character was received, so the input command string is complete and can be processed. Transmit a line separator, just to make the output easier to read. */ FreeRTOS_write( xConsole, "\r\n", strlen( "\r\n" ); /* The command interpreter is called repeatedly until it returns pdFALSE. See the "Implementing a command" documentation for an exaplanation of why this is. */ do { /* Send the command string to the command interpreter. Any output generated by the command interpreter will be placed in the pcOutputString buffer. */ xMoreDataToFollow = FreeRTOS_CLIProcessCommand ( pcInputString, /* The command string.*/ pcOutputString, /* The output buffer. */ MAX_OUTPUT_LENGTH/* The size of the output buffer. */ ); /* Write the output generated by the command interpreter to the console. */ FreeRTOS_write( xConsole, pcOutputString, strlen( pcOutputString ) ); } while( xMoreDataToFollow != pdFALSE ); /* All the strings generated by the input command have been sent. Processing of the command is complete. Clear the input string ready to receive the next command. */ cInputIndex = 0; memset( pcInputString, 0x00, MAX_INPUT_LENGTH ); } else { /* The if() clause performs the processing after a newline character is received. This else clause performs the processing if any other character is received. */ if( cRxedChar == '\r' ) { /* Ignore carriage returns. */ } else if( cRxedChar == '\b' ) { /* Backspace was pressed. Erase the last character in the input buffer - if there are any. */ if( cInputIndex > 0 ) { cInputIndex--; pcInputString[ cInputIndex ] = ''; } } else { /* A character was entered. It was not a new line, backspace or carriage return, so it is accepted as part of the input and placed into the input buffer. When a n is entered the complete string will be passed to the command interpreter. */ if( cInputIndex < MAX_INPUT_LENGTH ) { pcInputString[ cInputIndex ] = cRxedChar; cInputIndex++; } } } } } |
As it is above it is pretty much unusable right now… Just look at the welcome message, what the hell is “.rn” ? I know it means return carriage and newline but not like that sir.
Lets clean things up a bit. First of all you will notice that cRxedChar is used all over the place here and it is defined in the top of this task. I will delete it from there since I have defined it globally.
I will also get ride of the Peripheral_Descriptor_t type which they name xConsole, and the writing of the welcome message because I dont like it. The start of my command line task now looks like this:
1 2 3 4 5 6 7 | void vCommandConsoleTask(void *pvParameters) { int8_t cInputIndex = 0; BaseType_t xMoreDataToFollow; /* The input and output buffers are declared static to keep them off the * stack. */ static int8_t pcOutputString[MAX_OUTPUT_LENGTH], pcInputString[MAX_INPUT_LENGTH]; |
The FreeRTOS_read right after the for loop also has to go, its job is to pause the task and wait for a character, I will simply put and if statement here though we could use a FreeRTOS notification and it would work the same.
1 2 3 4 5 | for (;;) { if (cRxedChar != 0x00) { |
The next thing to go is the call to freeRTOS_write, we will replace this with our printf function. This call simply prints a newline with the \n character is recevied, but this does not seem to work for me and my microcontroller so I will change the if statement to also check for \r.
1 2 3 4 5 6 7 | if (cRxedChar == '\r' || cRxedChar == '\n') { printf("\r\n"); fflush(stdout); /* A newline character was received, so the input command string is complete and can be processed. Transmit a line separator, just to make the output easier to read. */ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /* The command interpreter is called repeatedly until it returns pdFALSE. See the "Implementing a command" documentation for an exaplanation of why this is. */ do { /* Send the command string to the command interpreter. Any output generated by the command interpreter will be placed in the pcOutputString buffer. */ xMoreDataToFollow = FreeRTOS_CLIProcessCommand(pcInputString, /* The command string.*/ pcOutputString, /* The output buffer. */ MAX_OUTPUT_LENGTH /* The size of the output buffer. */ ); /* Write the output generated by the command interpreter to the console. */ for (int x = 0; x < (xMoreDataToFollow == pdTRUE ? MAX_OUTPUT_LENGTH : strlen(pcOutputString)); x++) { printf("%c", *(pcOutputString + x)); fflush(stdout); } } while (xMoreDataToFollow != pdFALSE); |
1 2 3 4 5 6 7 | /* All the strings generated by the input command have been sent. Processing of the command is complete. Clear the input string ready to receive the next command. */ cInputIndex = 0; memset(pcInputString, 0x00, MAX_INPUT_LENGTH); memset(pcOutputString, 0x00, MAX_INPUT_LENGTH); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | else { /* The if() clause performs the processing after a newline character is received. This else clause performs the processing if any other character is received. */ if (cRxedChar == '\b') { /* Backspace was pressed. Erase the last character in the input buffer - if there are any. */ if (cInputIndex > 0) { cInputIndex--; pcInputString[cInputIndex] = ""; } } else { /* A character was entered. It was not a new line, backspace or carriage return, so it is accepted as part of the input and placed into the input buffer. When a n is entered the complete string will be passed to the command interpreter. */ if (cInputIndex < MAX_INPUT_LENGTH) { pcInputString[cInputIndex] = cRxedChar; cInputIndex++; printf("%c", cRxedChar); } } } cRxedChar = 0x00; fflush(stdout); } } } |
The new main_app.c with revised command line task
The entirety of the new command line task looks like the main_app.c file below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | #include "main_app.h" #define MAX_INPUT_LENGTH 50 #define MAX_OUTPUT_LENGTH 100 uint8_t cRxedChar = 0x00; void vCommandConsoleTask(void *pvParameters) { int8_t cInputIndex = 0; BaseType_t xMoreDataToFollow; /* The input and output buffers are declared static to keep them off the * stack. */ static int8_t pcOutputString[MAX_OUTPUT_LENGTH], pcInputString[MAX_INPUT_LENGTH]; for (;;) { if (cRxedChar != 0x00) { if (cRxedChar == '\r' || cRxedChar == '\n') { printf("\r\n"); fflush(stdout); /* A newline character was received, so the input command string is complete and can be processed. Transmit a line separator, just to make the output easier to read. */ /* The command interpreter is called repeatedly until it returns pdFALSE. See the "Implementing a command" documentation for an exaplanation of why this is. */ do { /* Send the command string to the command interpreter. Any output generated by the command interpreter will be placed in the pcOutputString buffer. */ xMoreDataToFollow = FreeRTOS_CLIProcessCommand(pcInputString, /* The command string.*/ pcOutputString, /* The output buffer. */ MAX_OUTPUT_LENGTH /* The size of the output buffer. */ ); /* Write the output generated by the command interpreter to the console. */ for (int x = 0; x < (xMoreDataToFollow == pdTRUE ? MAX_OUTPUT_LENGTH : strlen(pcOutputString)); x++) { printf("%c", *(pcOutputString + x)); fflush(stdout); } } while (xMoreDataToFollow != pdFALSE); /* All the strings generated by the input command have been sent. Processing of the command is complete. Clear the input string ready to receive the next command. */ cInputIndex = 0; memset(pcInputString, 0x00, MAX_INPUT_LENGTH); memset(pcOutputString, 0x00, MAX_INPUT_LENGTH); } else { /* The if() clause performs the processing after a newline character is received. This else clause performs the processing if any other character is received. */ if (cRxedChar == '\b') { /* Backspace was pressed. Erase the last character in the input buffer - if there are any. */ if (cInputIndex > 0) { cInputIndex--; pcInputString[cInputIndex] = ""; } } else { /* A character was entered. It was not a new line, backspace or carriage return, so it is accepted as part of the input and placed into the input buffer. When a n is entered the complete string will be passed to the command interpreter. */ if (cInputIndex < MAX_INPUT_LENGTH) { pcInputString[cInputIndex] = cRxedChar; cInputIndex++; printf("%c", cRxedChar); } } } cRxedChar = 0x00; fflush(stdout); } } } void main_app_init(void) { } |
Lets now create this command line task and start the schedule to test our basic CLI. Make a task handle variable to hold the handle of this task. I made it right under the cRxedChar we made earlier
1 2 | uint8_t cRxedChar = 0x00; TaskHandle_t consoleTaskID; |
1 2 3 4 5 6 7 8 9 10 | void main_app_init(void) { printf("freeRTOS CLI\r\n"); int status = xTaskCreate(vCommandConsoleTask, (const char *)"Console", 4000, NULL, tskIDLE_PRIORITY + 2, &consoleTaskID); if (status == -1) printf("error creating Console task task\n"); vTaskStartScheduler(); } |
Now at this point you should be able to run the application and when you type help it will display the default help message from the CLI.
In the next post I will go over the short comings of this basic CLI and how to make commands and then we will begin to really turn this CLI into something the resembles more of a traditional CLI on a desktop.
Comments