Now comes the part where you Frankenstein my code and yours into something that’ll make lowly mortals scream “science has gone too far !”
This is a fairly complex procedure. Not because the code is complicated but because you’ll need to make small modifications in several different places and none of them is optional. So let’s start with a checklist. You will :
- #include “shell.h”
- Override two STM Shell low-level functions
- Override two ST HAL callback functions
- Call the STM Shell service routine
Pay close attention, as you will need to adapt the code I show you to fit your own target. No mindless copy-pasting past this point !
Also, quick reminder : STM32CubeIDE can generate code for you, which means you need to write your code where the code generator won’t overwrite it. Make sure to only add code between the USER CODE comments provided by the code generator.
And on with the show…
1. #include “shell.h”
Open your project’s “main.h” file, which you’ll find under “Core/Inc”.
In the section starting with the comment /* USER CODE BEGIN Includes */ add the following directive :
#include "shell.h" // STM Shell
The reason you want this there and not in “main.c” is that STM Shell gives you (among other things) a logging macro that sends strings to the shell’s terminal. This #include directive, in “main.h”, allows you to use that macro from anywhere in your project’s code.
2. Portability Layer
There’s more than a thousand different STM32 chips out there, and many more ways to use them. This makes it challenging to write a user interface library that works on all of them without modification. The most efficient way to meet this challenge is to design a portability layer, essentially middleware. It’s a set of functions that your library needs and that must be implemented specifically for your target. Doing this right means limiting this layer to the smallest possible number of functions, and limiting each function to just the code that is target-specific.
I’ve done just that. You’ll see, I promise it won’t hurt. Much.
Open your project’s “main.c”, under “Core/Src”.
You need to override two STM Shell functions:
- shell_out : the function that sends a character string out the UART using DMA
- shell_get_byte : the function that reads a single byte from the UART using interrupts
My library defines these as empty functions with the weak attribute, allowing you to compile your project even without overriding them, but since they are empty your shell will not work.
“main.c” is a good place to define your overrides. Why ? These functions will need to be specific to your microcontroller’s configuration, in particular the UART you’re using, and the CubeIDE code generator places the configuration functions for your microcontroller in “main.c”. Moreover, as you’ll see, this keeps all the code you need to add in “main.c” only. Always a good idea to keep related code in one place.
In order to protect your overrides from the code generator, you’ll be writing them between the comments /* USER CODE BEGIN 0 */ and /* USER CODE END 0 */
Your version of shell_out must look like this:
void shell_out (char *buff, int length) { HAL_UART_Transmit_DMA(&huart3, (uint8_t *) buff, length); shell_state.busy = 1; // DMA transfer in progress. }
It uses the HAL to transmit “length” bytes of data by DMA starting from “buff*”. You may need to replace huart3 to match the UART you’re using.
shell_state is the internal state of STM Shell. Setting its busy flag makes sure the library won’t try to start an overlapping transfer. You’re going to clear that flag once the DMA transfer completes. To do that, you need to override the HAL callback for that event by also writing this function:
void HAL_UART_TxCpltCallback (UART_HandleTypeDef *huart) { // Verify which USART we're dealing with: if (huart->Instance == USART3) // UART used by the shell shell_state.busy = 0; // Transfer complete : clear the flag }
ST HAL callback functions are similar to mine : the ST HAL already defines them, but as weak functions that do nothing. You can override the ones you need, and that’s what you’re doing now.
ST HAL callbacks are always shared across all peripherals of the same type. This means you need to test which UART you’re dealing with. This only matters if your project uses other UART besides the one for STM Shell. Obviously, if you need to test, make sure to replace USART3 with whatever you’re using. I don’t believe you need it spelled out, but if you’ve already overridden this callback for your own application’s purposes, all you need to do is add the “if” statement to your callback.
Thus you can see the lifecycle for the busy flag : any time the shell sends some output to your terminal, it raises its busy flag to prevent overlapping transfers from mangling the output. Whenever the latest output is done being output, it clears the busy flag so the next output can be output.
On to your version of shell_get_byte. It should look like this:
void shell_get_byte (char *c) { HAL_UART_Receive_IT(&huart3, (uint8_t *) c, 1); }
It uses the HAL to read one byte from the UART using interrupts. This returns immediately so that your application won’t block, same as with DMA. When the user hits a key on their terminal, you’ll get an interrupt. As with DMA, that interrupt needs to be handled and that is done by overriding one of the ST HAL callbacks, like so:
void HAL_UART_RxCpltCallback (UART_HandleTypeDef *huart) { // Verify which USART we're dealing with : if (huart->Instance == USART3) // UART used by the shell shell_in (shell_state.c); // Feed the byte to the shell }
Here’s what’s happening : STM Shell continuously calls shell_get_byte to grab user input one keystroke at a time. It stores each incoming keystroke in shell_state and whenever that happens you get a UART interrupt. At that point, you must call the library’s shell_in function to process that keystroke. This will eventually lead to the next call to shell_get_byte in what can best be described as :
And that’s it for the portability layer. See ? Not as bad as you feared, I’m certain.
You may be wondering why I’m using a single-byte interrupt-based read for input instead of DMA as I do for the output. Simply put, I’m expecting this shell to be deployed as a user interface mechanism. The shell needs to output to your terminal as fast as possible, but you will be typing-in your commands one character at a time and (from the microcontroller’s perspective) at the speed of crippled glacier. Furthermore, the shell needs to be able to react to special characters the moment they arrive. For example, the ENTER key (carriage return).
3. Shell Service
It may seem that reacting to terminal input and DMA transfer completion should be all that’s required to keep STM Shell going. Not quite. The shell has its own internal state machine that exists for one vital reason alone : maintaining the performance of your application code. This essentially works like a crude multi-tasking operating system.
Executing the shell state machine is up to you. This is an important feature, as it puts you in control of when, and how often, the shell runs. You can use that to make sure the shell doesn’t interfere with critical operations and to limit how much CPU cycles it uses.
I’m afraid this requires an explanation of how STM Shell works under the hood. Bear with me.
One of my key concerns when designing this shell was to minimize its impact on the execution of the applications it’s integrated in. My solution was to implement it as a state machine, with each state function as short and quick as possible. Thus, the shell has five native states :
- init : this is the initialization state, which is also the default state and runs only once.
- input : this is the state that calls shell_get_byte, that you just overrode.
- parser : whenever input sees a carriage return, this state parses the user’s command.
- output : the state that calls shell_out, that you just overrode.
- idle : between two keystrokes, the shell has nothing to do. This state does nothing.
In addition to those five native states, every shell command (including the ones you’ll write for your application) is also a shell state, meaning the shell stays in that state until the command completes.
The current state is stored as a function pointer named shell_fp that always points to the current state’s function.
For the shell to run, you need to call shell_fp as often as possible. It is, for all intents and purposes, the STM Shell “service routine” or “process”. The more often you call shell_fp, the more responsive STM Shell is.
A good place to call shell_fp is in the body of your main function’s infinite loop. This lets your firmware complete a full cycle of whatever it’s meant to do and then run the shell for a short interval. Rinse and repeat. If your main loop’s body is unusually long, you may also call shell_fp several times throughout, if it improves responsiveness.
If you’re unfamiliar with function pointer syntax, here’s how to call it :
shell_fp();
Yup, that’s it.
And with that, congratulations are in order : you’ve completed the integration of STM Shell into your project.
At this time, you should build your project. There should be no error or warning. Otherwise, you probably made a typo or skipped a step somewhere. Check your work and try again.
Once you’ve succeeded and flashed your STM32, connect your terminal emulator to your UART, make sure the bit rate and miscellaneous settings match, and then reset your board or hit ENTER. You will see this prompt :
Or perhaps you won’t. Don’t despair, there are a couple of well-known situations that will prevent STM Shell from working even though everything compiled correctly and the rest of your code works. I’ll go over how to fix that in the bonus stage Shell Integration : Oops.