The Zephyr Shell is an interactive console to inspect and control running firmware.
So far, when we compile the firmware, it’s a “black box.” We flash it and hope it works. If something fails, we use printk and cross our fingers.
But what if you could “enter” the microcontroller while it is running? What if you could ask it: “How much free memory is left?”, “Which threads are running?” or “Turn on LED 2” by typing commands in a terminal?
This subsystem turns your UART (or USB) connection into a command line very similar to Linux or DOS.
Activating the Shell
The Shell is an optional module. To activate it, we just need to add a couple of lines to our prj.conf.
# Activate the Shell subsystem
CONFIG_SHELL=y
# Choose the backend (output channel). Usually UART.
CONFIG_SHELL_BACKEND_SERIAL=y
# Kernel and device commands used in this article
CONFIG_KERNEL_SHELL=y
CONFIG_DEVICE_SHELL=y
CONFIG_THREAD_MONITOR=y
# Navigable history with keyboard arrows
CONFIG_SHELL_HISTORY=y
# Optional: Increase buffer for long commands
CONFIG_SHELL_CMD_BUFF_SIZE=128After recompiling and flashing (west build -p always ...), open the serial monitor (PuTTY, Minicom, or the VSCode extension) and press Enter. You will see an interactive prompt:
uart:~$We are in!
Built-in Commands
The available commands depend on which Kconfig options are enabled. Type help to see the commands included in your build.
kernel version and kernel uptime
The basics. Tells you the OS version and how long it has been running.
kernel threads
This is the most important command. It displays a real-time table of all system threads, their stack usage, and priority.
uart:~$ kernel threads
Scheduler: 335 since last call
Thread Pri Stack % Of State
*idle 48 128 33 384 running
main 0 420 20 2048 suspend
logging 14 200 19 1024 suspend
shell_uart 14 512 25 2048 readyIf you see that the stack usage percentage (%) of any thread is approaching 90%, you are about to experience a stack overflow. Increase the stack in the code.
device list
Shows registered devices and their status. The necessary options are already included in the prj.conf above: CONFIG_KERNEL_SHELL=y, CONFIG_DEVICE_SHELL=y and CONFIG_THREAD_MONITOR=y.
History
With CONFIG_SHELL_HISTORY=y, you can use the up and down arrow keys to recall previous commands.
Peripheral Shells
In addition to Kernel commands, we can activate specific commands to test hardware without writing C code. This is extremely useful for validating a newly manufactured PCB.
In prj.conf:
CONFIG_GPIO_SHELL=y
CONFIG_I2C_SHELL=yNow you can do things like:
- Read a pin:
gpio get gpio@50000000 13(Reads pin 13 of port 0). - Scan I2C:
i2c scan i2c@40003000(Searches for devices on the bus).
Strange names like gpio@50000000 are the internal device labels. You can see the correct names using device list.
Creating Custom Commands
The best part of the Shell is that you can add your own custom commands.
Let’s create an app status command to print application variables and an app say_hello command that receives an argument.
The necessary code in your main.c is surprisingly simple:
#include <zephyr/kernel.h>
#include <zephyr/shell/shell.h>
/* Function executed with the 'status' command */
static int cmd_status(const struct shell *sh, size_t argc, char **argv)
{
/* Use shell_print instead of printk so it outputs via the shell */
shell_print(sh, "System status: OK. Temperature: 24.5C");
return 0;
}
/* Function executed with the 'say_hello' command */
/* argc is the number of arguments, argv is the argument array */
static int cmd_say_hello(const struct shell *sh, size_t argc, char **argv)
{
/* argv[0] is the command name. argv[1] is the first argument. */
shell_print(sh, "Hello %s!", argv[1]);
return 0;
}
/* Define the subcommands (lowest level) */
SHELL_STATIC_SUBCMD_SET_CREATE(sub_app,
SHELL_CMD(status, NULL, "Shows status", cmd_status),
SHELL_CMD_ARG(say_hello, NULL, "Greets someone. Usage: say_hello <name>", cmd_say_hello, 2, 0),
SHELL_SUBCMD_SET_END /* Marks the end of the list */
);
/* Register the root command 'app' */
SHELL_CMD_REGISTER(app, &sub_app, "My application commands", NULL);
int main(void) {
/* ... rest of code ... */
return 0;
}Now, after compiling and running, you can type:
uart:~$ app status
System status: OK. Temperature: 24.5C
uart:~$ app say_hello Luis
Hello Luis!Colors and Formatting
The Shell supports VT100 escape codes. This means you can print in colors.
shell_info(sh, "This is information (Green)");
shell_warn(sh, "This is a warning (Yellow)");
shell_error(sh, "This is an error (Red)");