I feel the best way to teach you the best practices in writing shell commands is to teach by example.
Earlier, you built your project with “shell_pfs.c”, which means you now have a demo PFS with demo commands in it. We’ll have a look and them. But first, test-drive your brand-new shell.
The first command you should try is “ls”. It should return this :
Let’s unpack.
The prompt always shows your current path in the PFS. “/STM32” is the root. You can change that string, it’s just like a directory name. My preference is to use the name of your project or board. This can be useful if you’re working on multiple products at the same time.
The output of the “ls” command starts with the name of the directory you’re in, bracketed by double-equals for readability.
Then each line shows one entry in the current directory. The starting character is either a “C” for a command or a “>” for a subdirectory (you can also call them menus, submenus, blocks, pages, etc…).
In both cases, the command is left of the dash. Everything after the dash is commentary, a brief description of what the command does (or what the submenu contains)
All you need to do to launch a command is type the first word (in this case, led, cnt, flash…). If that happens to be the name of a submenu, the shell will navigate to it :
As you can see, a directory’s name can be different than the command you type to access it. This design choice was born of UX considerations. Suppose your product will require extensive testing, maybe for each unit, performed at the factory : think about the people who’ll have to type commands all day. They won’t appreciate 20-character case-sensitive commands when three letters would have done the job. But when they read the prompt, however, they will appreciate that 20-character case-sensitive name.
In fact, I went a step further : STM Shell recognizes incomplete commands. In the example above, “s” and “sm” would also match “sm1”. If there is ambiguity (for example, two commands started with “s” in the same directory) then the shell takes the first match, so you may want to place the most used commands first.
Notice two more things in this latest screenshot ?
The “load” command appears twice. It was also present in the root directory. This is allowed. You can create a command and then place it in multiple locations. Again, think UX : you could, say, put a “print report” command in one directory and the commands to launch some tests in different directories. But then that would force the user to “cd..” all the time. This doesn’t bring any value, it’s only annoying.
Also, there is a nested submenu entry. I’ll let you explore it for yourself.
Use the “cd..” command to get back up one level towards the root.
It’s now time to find out how commands are made.
1. The “led” Command
Let’s start with that one because it’s the simplest of all, and a good way to introduce a fundamental aspect of command functions.
Here we have a command that just toggles a pin. Pretty much the shortest viable command. Use it a few times to check that everything works as intended.
Now let’s look at its code :
void command_led_toggle () { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // Transition back to the prompt shell_fp = shell_state_output; shell_state.command_fp = 0; }
The first statement is rather self-evident : you’re calling the ST GPIO HAL to toggle a pin. The arguments are constants generated by STM32CubeIDE : their definitions reside in your project’s “main.h” which is why “shell_pfs.c” includes it.
What makes command_led_toggle a command function is what comes next : the function resets shell state to return the user to the prompt.
As you may remember, shell_fp is the function pointer that your application calls regularly to service the shell. I’ve already covered how it may point to one of five STM Shell internal state functions. Well, it is also used to store the pointer to the command function that is currently executing (in this case, command_led_toggle). Therefore, once your command has completed, you must set that pointer to the shell’s output state.
The second shell variable you’re clearing, shell_state.command_fp, contains the same thing shell_fp did. It might seem strange, but you’ll easily understand why this exists :
shell_state_output is not only an internal shell state function; it’s the function that sends a line of output to the user’s terminal. As such, it’s the function you will use to send the output of your commands to the terminal. That means the shell needs to know which function was the current state before shell_state_output, so it can return to the right state. Think of command_fp as a stack with just one level.
When shell_state_output is ready to transition, it only knows two possibilities :
- if command_fp is null, then there’s currently no user command executing. Perhaps the last user command has completed, or maybe there hasn’t been one yet. The shell must display the prompt and then go idle until the user enters a command.
- If command_fp isn’t null, then it points to a command function (it could be an internal one, like “ls”). Since the shell only outputs one line at a time (so that it doesn’t hog the CPU) and the current command might produce multiple lines of output, shell_state_output will transition back to command_fp so it may continue its work.
That is an important concept to grasp and always keep in mind : when the user enters a command, STM Shell will simply call the relevant command function repeatedly, until that function transitions the shell back to the prompt.
If you want your shell to behave politely and interfere as little as possible with your application, you should take advantage of this design. If your commands take a long time to execute, think of ways you can split the work over multiple calls to the same function. In the rest of this page, I’ll show you a few simple approaches to do just that.
Also, always remember to return to the prompt. If your command function does not, then the shell will be stuck in that command (a problem you’ll notice very easily). STM Shell is a very lightweight program, it does not support a break (CTRL-C)
2. The “cnt” Command
The “led” command doesn’t do much. It will execute once and return you to the prompt. It doesn’t even output anything to the terminal. Let’s see how to do that.
The “cnt” command counts the number of times it’s been called and displays it on your terminal.
Try it a few times. Now let’s look at the code :
void command_cnt () { static int cnt = 0; // counter static int state = 0; // the command's own state machine's state switch (state) { case 0: sprintf (shell_state.output, "\r\nCalled %i times", cnt); shell_fp = shell_state_output; // Print to terminal state = 1; break; // Yield the CPU case 1: cnt++; state = 0; shell_state.command_fp = 0; command's execution shell_fp = shell_state_output; break; } }
State persistence is achieved by using static variables, think of it as an ersatz of “process memory” or “mass storage” since our filesystem is pseudo. In this function, there are two static variables :
- “cnt” will store the number of times the “cnt” command is launched.
- “state” keeps track of the current state of this command’s state machine.
That’s right. We’re using nested state machines out here. Nothing scary, but feel free to show off to your colleagues and potential romantic partners.
Moments ago, I reminded you that STM Shell execution is under your application’s control, in the form of regular calls to the shell_fp function pointer. This mechanism was initially intended to avoid blocking the execution of application code. It has other uses, as you’re about to learn.
Since a command function will be called repeatedly until it completes its job, it must have a mechanism to keep track of what it’s already done. A state machine, with the current state kept in a static variable, is one obvious approach.
command_cnt has two states :
- First (in state 0) it prepares its output. Here I’ve used sprintf to build a string that contains the value of the cnt variable. That string is written to shell_state.output : that’s the buffer STM Shell uses for its DMA transfers to the terminal. It then sets shell_fp to the shell’s internal transfer function.
- Later (in state 1) it updates cnt by incrementing it, and finishes the command as we’ve seen before.
Each state sets the state variable to implement state transitions that progress the command and “rearm” it for the next time the user enters it.
If you’re finding it a bit hard to follow what happens when the user enters the “cnt” command, here’s how it plays out. Each bullet point is a call to shell_fp that is made from your main loop :
- shell_state_parser sets shell_fp to command_cnt
- command_cnt writes to the shell’s output buffer and sets shell_fp to shell_state_output
- shell_state_output outputs to the terminal and sets shell_fp back to command_cnt
- command_cnt updates the “cnt” counter and sets shell_fp to shell_state_output
- shell_state_output notices command_fp is null and outputs the shell prompt
This might seem inefficient, but keep in mind that you’re servicing a human user, who thinks much slower than an STM32 can execute five functions. Meanwhile, splitting the command into five short calls to shell_fp gives your application code four intervals to execute in between, giving the user the illusion of parallel execution, pretty much the same way your computer’s OS does.
3. Interlude : Helper Macros
I didn’t choose my demo commands randomly. Many of your own shell commands are likely to boil down to something like “command_cnt” with extra states and perhaps some branching or looping. Because I’m terminally lazy, the prospect of having to type those switch / case statements which, for the most part, don’t do “useful work”, led me to create a set of macros to help you write your command functions faster and make them easier to read.
Let’s look at “command_cnt” again, this time two versions side by side : the one I’ve just walked you through, and the exact same written using macros :
I think you’ll agree the macros not only shorten the code, they also make it easier to read.
You can find the definition of those macros in “shell_pfs.c”, they are pretty easy to follow :
- STATE_MACHINE declares and initializes the static state variable, then starts the switch statement.
- STATE breaks out of the previous case statement and starts the next one.
- RETURN handles the function pointer gymnastics that ends the execution of the command.
- STATE_MACHINE_END completes the switch statement.
You don’t have to use those macros, you can even make your own if they feel more convenient.
4. The “flash” Command
This example shows you to implement a command that takes arguments from the command line.
This command will blink an LED “N” times, where “N” is the command’s argument. For example, to blink the LED 10 times you’d enter the command “flash 10”.
For the sake of clarity, I’ll show you the macro-based implementation. “shell_pfs.c” also contains the macro-free version.
Pay close attention to state zero, this is where the magic happens.
STM Shell stores the last command line entered by the user in its state variable, specifically shell_state.input. Your command functions can use it, it’s a null-terminated C string. In this scenario, I’ve used the standard sscanf function to retrieve the command line argument, assuming there was one.
void command_flash () { static int arg = 0; static int delay; // Controls flashing frequency const int max_delay = 100000; int rv = 0; STATE_MACHINE STATE 0: // parse the command line rv = sscanf (shell_state.input, "flash %i", &arg); state = ((rv == 1) && (arg > 0)) ? 1 : 6; STATE 1: // turn on the LED LED(1); delay = max_delay; state++; // Move on to next state : delay loop STATE 2: // delay loop delay--; state = (delay > 0) ? 2 : 3; STATE 3: // turn off the LED LED(0); delay = max_delay; state++; // Move on to next state : delay loop STATE 4: // delay loop delay--; state = (delay > 0) ? 4 : 5; STATE 5: // decrement arg and test for command completion arg--; state = (arg == 0) ? 6 : 1; STATE 6: // command complete, return to prompt RETURN STATE_MACHINE_END }
Let’s look at the transitions and see how to implement argument validation and loops.
In state 0, there is an attempt to extract an integer argument from the command line and store it in the arg variable. That’s a static variable because of course this value affects the execution of the entire command. By comparison, rv (for “return value”) is only used in state 0, which only runs once, so it doesn’t need to be static.
If rv and arg show that a valid argument was present and parsed, the command function transitions to state 1, otherwise it goes straight to state 6.
state 1 starts the LED blinking by turning the LED on. It also initializes a delay counter (which is obviously a static variable) and transitions to state 2.
state 2 is a delay loop : because we need to wait for inferior human eyes to notice the LED blinking, it’s necessary to call “command_flash” 100,000 times for nothing. When the counter eventually reaches zero, we can finally transition to state 3.
state 3 turns off the LED, and then state 4 implements the same delay as state 2.
state 5 counts down one full blink of the LED. If all the blinks requested by the user have been performed, it transitions to state 6, otherwise it’s back to state 1.
state 6 ends the execution of the command, resulting in a fresh prompt.
This example is, of course, about more than command line arguments, conditional branching and delays. I encourage you to use it to explore the impact of STM Shell on the performance of your application. To that end, you’ll also find the “load” command to be very useful. It’s an example of a command that does a lot of calculations with little respect for the application. It’s another tool you can use to verify the robustness (and fitness for purpose) of the combination of your application and STM Shell.
This concludes the tutorial on writing command functions. Let’s end with a…
5. Quick Recap
STM Shell is designed as a state machine so as to use as little CPU time as possible and give priority to your application. Because your application is what matters. The shell is just a tool.
The state of the shell’s state machine is two variables :
- shell_fp : pointer to the function that implements the current state. That can be an internal function of the shell, or one of your command functions.
- shell_state : structure that contains the rest of the shell state, such as the latest command line entered by the user. You can use that to process command line arguments.
shell_fp must be called repeatedly by your application. Think of it as the shell’s process. Your application controls how often it executes. If you don’t call it often enough, the shell will feel unresponsive. Balance this against the needs of your application.
When the user enters a valid command, the shell assigns shell_fp the address of the relevant command function, and thus that function will be called repeatedly by the application. If your command function takes a long time to execute, you can take advantage of that mechanism to split the work into smaller chunks, so you don’t block the execution of the application for too long.
Because of that design, your command functions must always end with shell_fp pointing to the shell’s shell_state_output function. This is how the shell displays a new prompt and goes back to waiting for the next command from the user.
You can also transition to shell_state_output to print your own output strings to the user’s terminal. The shell will then transition back to your command function.
STM Shell does not contain critical zones or disable any interrupt, and may be given the lowest interrupt and DMA priorities. It does not have demanding timing requirements. Try to maintain that low-impact philosophy in your own command functions. This shell is meant as a tool to help your developments, not as an additional source of bugs.
Up next : we’ll see how command functions are presented to the users in the form of a pseudo filesystem.