Build your Command Line Interface: a Guide to ChibiOS/Shell

Introduction

In this article, we explore the ChibiOS/Shell, a fully configurable Command Line Interface (CLI) provided by ChibiOS. This tool allows users to interact with the operating system, execute tasks, and access system features through available serial drivers, utilizing either USB or UART connections.

Embedded systems face a unique challenge because they are closed systems operating on an integrated chip without the interactive interfaces like a mouse or keyboard found on PCs. Programming on these microcontrollers typically creates a system that responds to specific events by generating outputs. These events usually come from external hardware and result in actions on other hardware components. For example, consider a smart miniature greenhouse with a temperature sensor. Positioned to receive sunlight, the greenhouse includes a small window operated by a servo. When the microcontroller reads the temperature sensor and detects that the temperature has exceeded a predetermined threshold, it opens the window to cool down the environment.

Smart greenhouse control system with temperature-triggered window mechanism.

The system described operates in a closed loop, meaning we cannot directly influence its programmed behavior. Now, imagine if the microcontroller could accept commands with parameters through one of its serial ports. Upon receiving these commands, it would perform specific internal operations and provide feedback via the serial output. For instance, it might allow for the adjustment of the temperature threshold that triggers the window to open. Alternatively, it could enable manual control over the window, allowing it to be opened or closed on command.

Enhanced smart greenhouse system with Shell integration for command input and control.

Furthermore, this command line can be used to display sensor data upon request, transforming it into a powerful tool. This capability is precisely what the Shell offers. At its core, a Shell is a Command Line Interface that operates on one of the microcontroller’s COM ports. It is engineered to accept commands and return responses. A command consists of a sequence of human-readable characters, potentially accompanied by optional parameters. Consider the command below, followed by two parameters:

set threshold 35

In this example, the command is set, with threshold and 35 serving as the parameters. When a command is executed, it is linked to a specific function that receives these parameters as arguments. The action taken by the function then depends on how it is programmed by the user.

Reflecting on our smart greenhouse scenario, such a command could adjust the temperature level that triggers the greenhouse door to open. The command might carry out verifications before setting a global variable, and if the command is processed successfully, the response might be:

set threshold OK

This capability illustrates how commands can dynamically interact with the system’s settings, enabling users to tailor the system’s behavior according to specific needs or conditions.

Setting up and running the Shell

The best way to really get to know what the Shell can do is by starting a test project that uses it. This gives us a hands-on chance to see how it works. A good starting point is using The simplest project ever with ChibiOS, chosen for an evaluation kit we like. From there we can expand the project using one of the many demo based on ChibiOS/Shell that ChibiOS offers.

This method helps us get familiar with the Shell quickly and shows us how we can adjust it for our own projects. By doing this, we can learn about the Shell’s capabilities and how to make the most of them in our embedded systems.

Preliminary information

The best reference about how to use the Shell is the demo USB-CDC demo available under [ChibiOS Root]\testhal\STM32\multi\USB_CDC. This demo shows the application of the Shell using the Serial Driver over USB. The SoUSB is a Complex Device Driver that programs a microcontroller native USB peripheral into into a virtual COM port, which the PC can recognize as such. However, doing so requires to deal with USB descriptors and some other complexities related to the USB standard: diving into this while still trying to understand the Shell’s workings might be overly complex because it involves managing several advanced features at once.

Luckily, the Shell is modeled around an abstract interface called BaseSequentialStream. The underlying principle of adopting an interface such as BaseSequentialStream is that it acts as a contract between the application layer and the driver, defining four common methods: write, read, put, and get. Both SerialDriver and SerialDriver over USB adhere to this contract, extending it, which enables their use in any context requiring a BaseSequentialStream. Now, as the Shell relies on these four common methods, it can be interoperable with both drivers.

Diagram illustrating the Shell interaction with UART and USB-specific implementations of BaseSequentialStream, highlighting the common API and device-specific extensions.

For those looking for more details, Appendic A of the article about the chprintf in the article is a good reference. But right now, we are focusing on running the Shell with a Serial Driver to make our first steps with this library easier. If you are not familiar with the Serial Driver, it might be helpful to check out ChibiOS/HAL’s Serial Driver Explained.

In the example I will show next, I am using the SDP-K1 evaluation kit, specifically the Serial Driver 5. This driver is selected because UART5 on this evaluation kit can be directly connected to the PC through the Debugger. Thus, when we connect our kit to the PC, we immediately gain access to a COM port that is linked to UART5 on the microcontroller, making it ready to use right away.

The debugger COM port bridge of the SDP-K1

Integrating the Shell in our project

The Shell is an optional library that must be explicitly integrated into your project to be utilized. This process typically involves adding specific files and configuring the makefile, which aids in minimizing the compiled code size by including only what is necessary. This strategy ensures that if the Shell is not needed, the codebase remains streamlined without unnecessary additions.

To activate the Shell, we need to incorporate certain dependencies into our makefile under “Project, target, sources and paths“. There are primarily three groups of dependencies to be added:

  • The Shell itself, being a separate module, must be integrated into the project for usage. It can be located under [ChibiOS root]/os/various/shell/shell.mk
  • The chprintf, which is extensively utilized internally by the Shell, can be included via [ChibiOS root]/os/hal/lib/streams/streams.mk
  • The ChibiOS Test Suite, necessitating multiple inclusions, as shown in the makefile of any standard demo.

Examining my makefile, the subsequent additions would be necessary:

##############################################################################
# Project, target, sources and paths
#
...
# Other files (optional).
include $(CHIBIOS)/os/test/test.mk
include $(CHIBIOS)/test/rt/rt_test.mk
include $(CHIBIOS)/test/oslib/oslib_test.mk
include $(CHIBIOS)/os/hal/lib/streams/streams.mk
include $(CHIBIOS)/os/various/shell/shell.mk

Before we proceed, it is important to mention why the test suite is included. The Shell includes several default commands, among which is one that executes the test suite. Omitting this suite from our project would result in build errors. Later on, we will explore how to disable this specific command through the makefile, effectively removing the test suite if it is unnecessary for our purposes. For the time being, however, we will adhere to the standard demonstration setup.

A minimalistic application with the Shell

The main function below illustrates a minimal application that uses the Shell. This code has been inspired by the demo at [ChibiOS Root]/testhal/STM32/multi/USB_CDC and brings together all necessary elements to run the Shell on the debugger’s serial connection.

Let us examine the entire main function to gain a full understanding before going into the specifics.

/*
    PLAY Embedded demos - Copyright (C) 2014...2024 Rocco Marco Guglielmi
    This file is part of PLAY Embedded demos.
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
        http://www.apache.org/licenses/LICENSE-2.0
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
*/
#include "ch.h"
#include "hal.h"
#include "chprintf.h"
#include "shell.h"
/*===========================================================================*/
/* Command line related.                                                     */
/*===========================================================================*/
/*
 * Working area of the Shell Thread.
 */
static THD_WORKING_AREA(waShell, 2048);
/* Can be measured using dd if=/dev/xxxx of=/dev/null bs=512 count=10000.*/
static void cmd_write(BaseSequentialStream *chp, int argc, char *argv[]) {
  static uint8_t buf[] =
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
  (void)argv;
  if (argc > 0) {
    chprintf(chp, "Usage: write\r\n");
    return;
  }
  while (chnGetTimeout((BaseChannel *)&SD5, TIME_IMMEDIATE) == MSG_TIMEOUT) {
    streamWrite(chp, buf, sizeof buf - 1);
  }
  chprintf(chp, "\r\n\nstopped\r\n");
}
static const ShellCommand commands[] = {
  {"write", cmd_write},
  {NULL, NULL}
};
static const ShellConfig shell_cfg = {
  .sc_channel  = (BaseSequentialStream *)&SD5,
  .sc_commands = commands
};
/*===========================================================================*/
/* Generic code.                                                             */
/*===========================================================================*/
/*
 * This is a periodic thread that does absolutely nothing except flashing
 * a LED.
 */
static THD_WORKING_AREA(waThread1, 128);
static THD_FUNCTION(Thread1, arg) {
  (void)arg;
  chRegSetThreadName("blinker");
  while (true) {
    palToggleLine(LINE_LED_GREEN);
    chThdSleepMilliseconds(250);
  }
}
/*
 * Application entry point.
 */
int main(void) {
  /*
   * System initializations.
   * - HAL initialization, this also initializes the configured device drivers
   *   and performs the board-specific initializations.
   * - Kernel initialization, the main() function becomes a thread and the
   *   RTOS is active.
   */
  halInit();
  chSysInit();
  /* Creates the LED blinker thread. */
  chThdCreateStatic(waThread1, sizeof(waThread1), NORMALPRIO + 1, Thread1, NULL);
  /* Activating the debugger COM port with default configuration (8-N-1, 38400bps). */
  sdStart(&SD5, NULL);
  /* Initializing the Shell. */
  shellInit();
  /* The main spawn the Shell and if terminates it respawns it. */
  while (true) {
    thread_t *shelltp = chThdCreateStatic(waShell, sizeof(waShell),
                                          NORMALPRIO + 1, shellThread,
                                          (void *)&shell_cfg);
    chThdWait(shelltp);
    chThdSleepMilliseconds(1000);
  }
}

The analysis of the code reveals two primary components:

  1. The declaration of all functions and structures necessary for initializing the Shell, positioned outside the main function.
  2. The instantiation of the Shell itself, executed within the main function.

Essentially, the Shell is a parametric thread predefined within the library as shellThread. To activate the Shell, one must initiate this thread by providing it with a suitable working area and its configuration as parameters. The choice between using a static working area or allocating memory from the heap depends on specific project requirements or preferences.

For allocating memory from the heap:

define SHELL_WA_SIZE   THD_WORKING_AREA_SIZE(2048)
thread_t *shelltp = chThdCreateFromHeap(NULL, SHELL_WA_SIZE,
                                        "shell", NORMALPRIO + 1,
                                        shellThread, (void *)&shell_cfg);

For using a static working area:

static THD_WORKING_AREA(waShell, 2048);
thread_t *shelltp = chThdCreateStatic(waShell, sizeof(waShell),
                                      NORMALPRIO + 1, shellThread,
                                      (void *)&shell_cfg);

This arrangement allows for flexibility in how the Shell’s thread is deployed, catering to diverse application scenarios and system resource management strategies.

/* Thread instantiation allocating memory from the heap. */
#define SHELL_WA_SIZE   THD_WORKING_AREA_SIZE(2048)
thread_t *shelltp = chThdCreateFromHeap(NULL, SHELL_WA_SIZE,
                                        "shell", NORMALPRIO + 1,
                                        shellThread, (void *)&shell_cfg);
/* Thread instantiation using a static working area. */
static THD_WORKING_AREA(waShell, 2048);
thread_t *shelltp = chThdCreateStatic(waShell, sizeof(waShell),
                                      NORMALPRIO + 1, shellThread,
                                      (void *)&shell_cfg);

In our scenario, I opted for a static thread. However, there’s a crucial aspect to consider: the Shell thread can terminate upon a certain keystroke (CTRL+D) or if the ‘exit’ command is executed. To accommodate this, we prepare by storing the Shell thread’s pointer in a variable named shelltp. Subsequently, invoking chThdWait puts the main thread on hold indefinitely, awaiting the Shell thread’s termination. Upon such an event, the main thread pauses for an additional 1000 milliseconds before respawning the Shell.

/* The main thread spawns the Shell and respawns it upon termination. */
while (true) {
  thread_t *shelltp = chThdCreateStatic(waShell, sizeof(waShell),
                                        NORMALPRIO + 1, shellThread,
                                        (void *)&shell_cfg);
  chThdWait(shelltp);
  chThdSleepMilliseconds(1000);
}

This serves as a demonstration of the flexibility available, yet it is also feasible to configure the Shell so that its thread does not terminate. It is crucial to initialize the communication channel the Shell will use (in this instance, Serial Driver 5) before starting the Shell thread. Additionally, invoking shellInit() is necessary to set up certain internal aspects of the Shell. This preparation occurs just before we enter the main loop:

/* Activating the debugger COM port with default configuration (8-N-1, 38400bps). */
sdStart(&SD5, NULL);
/* Initializing the Shell. */
shellInit();

With this setup complete, we turn our attention to the configuration structure of the Shell. This structure typically has two key fields:

  • sc_channel, which points to the BaseSequentialStream utilized for spawning the Shell.
  • sc_commands, a pointer to an array defining additional, user-specified commands that can be executed via the Shell.

The sc_channel field connects the Shell to our serial driver, ensuring communication flows through the intended channel. Meanwhile, the sc_commands field facilitates the extension of the Shell’s functionality, allowing for the integration of custom commands tailored to our project’s needs. The configuration for our project is specified as follows:

static const ShellConfig shell_cfg = {
  .sc_channel  = (BaseSequentialStream *)&SD5,
  .sc_commands = commands
};

The commands list is defined within our main function:

static const ShellCommand commands[] = {
  {"write", cmd_write},
  {NULL, NULL}
};

This array links a string to a function, enabling the expansion of the Shell’s command set beyond the default commands. It is important to note that since the array’s size is not predefined, it must conclude with a {NULL, NULL} entry. This serves as a marker to indicate the end of the command list.

In this instance, we have defined a single user-created command, “write”. Typing “write” in the Shell triggers the execution of the cmd_write function, also detailed in our main application:

/* Can be measured using dd if=/dev/xxxx of=/dev/null bs=512 count=10000.*/
static void cmd_write(BaseSequentialStream *chp, int argc, char *argv[]) {
  static uint8_t buf[] =
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
  (void)argv;
  if (argc > 0) {
    chprintf(chp, "Usage: write\r\n");
    return;
  }
  while (chnGetTimeout((BaseChannel *)&SD5, TIME_IMMEDIATE) == MSG_TIMEOUT) {
    streamWrite(chp, buf, sizeof buf - 1);
  }
  chprintf(chp, "\r\n\nstopped\r\n");
} 

This function continuously prints a predefined buffer until any key is pressed by the user, demonstrating a simple, yet practical application of custom Shell commands.

Experiencing the Shell in action

When we open the COM port linked to our evaluation kit after flashing our firmware, we have the opportunity to explore this shell demonstration. Initially, if we press “Enter” in the terminal, the shell responds, indicating it is operational.

Initial response from the Shell: verifying its presence.

The first command we should try is “help,” which displays a list of all available commands:

Exploring Shell commands: output of the ‘help’ command.

This list includes several built-in commands and our custom command, “write.” Executing the “write” command triggers the corresponding function, cmd_write, which outputs a predefined string repeatedly until any key is pressed, halting the process with a “stopped” message.

The custom command ‘write’ in action.

As mentioned, the command continuously outputs the buffer until any key is pressed in the shell. When this happens, the loop is interrupted, and the function displays “stopped” before returning to the main shell loop.

Command Handling

The shell is fully customizable, enabling the addition of new commands, linking them to specific functions, and even disabling built-in commands. Initially, the shell handles the input by separating the command from its parameters. However, once we execute our custom-defined function, managing the input and performing error checks becomes our responsibility.

Creating and managing command handlers

The shell operates by taking input from the user in the form of characters. These characters are not immediately processed. Instead, the shell waits until the user presses the ‘Enter’ key. Upon doing so, the shell interprets the entire input string. It separates the string into multiple parts using spaces as the dividing factor. The first word is recognized as the command, which tells the shell which action should be executed. The subsequent words are considered parameters, which provide additional information or specifics about how the command should be executed.

Illustration of command and parameter separation in the shell.

A command is linked to a function: if the command is either a built-in command or one specified in our custom command list, then the corresponding function is invoked. The handling of parameters is important here.

The function associated with a command is defined as follows:

/**
 * @brief   Command handler function type.
 */
typedef void (*shellcmd_t)(BaseSequentialStream *chp, int argc, char *argv[]);

This function takes the BaseSequentialStream used by the shell, an integer representing the number of arguments entered in the shell following the command, and an array of strings that are the parameters themselves.

To clarify this concept, let’s introduce a new command and add it to our list of user-defined commands:

static void cmd_argcount(BaseSequentialStream *chp, int argc, char *argv[]) {
  chprintf(chp, "Command argcount received with %d arguments\r\n", argc);
  if(argc > 0) {
    for(int i = 0; i < argc; i++) {
      chprintf(chp, "Argument %d: %s\r\n", argc + 1, argv[i]);
    }
  }
}
static const ShellCommand commands[] = {
  {"write", cmd_write},
  {"argcount", cmd_argcount},
  {NULL, NULL}
};

Next, we will test these commands in the shell to observe their responses.

Demonstrating the ‘argcount’ command in action.

Argument checks and validation

From our previous examples, it is clear that the arguments for a command, along with their count, are passed directly to the function as anticipated. This enables us to perform checks on these arguments and tailor the function’s behavior based on the command’s parameters.

For instance, if a function is not designed to accept any arguments, we could structure it as follows:

static void cmd_example(BaseSequentialStream *chp, int argc, char *argv[]) {
  
  /* Ignoring argv to avoid unused variable warning */
  (void) argv;
  
  if(argc > 0) {
    chprintf(chp, "Usage: example\r\n",);
  }
  else {
    /* Implement the command's functionality here.. */
  }
}

If a function requires a more complex set of arguments, we can extend our validation accordingly. Revisiting the greenhouse example with the command to set a temperature threshold:

set threshold 35

Imagine this command always requires two parameters: the first must be “threshold,” and the second, a number between 25 and 40. A potential implementation could look like this:

static void cmd_set(BaseSequentialStream *chp, int argc, char *argv[]) {
  
  msg_t msg = MSG_OK;
  
  /* Check if the command received exactly two arguments */
  if(argc != 2) {
    msg = MSG_RESET;
  }
  else {
    /* Check if the first argument is "threshold" */
    if(strcmp(argv[0], "threshold") == 0) {
      /* Convert the second argument to an integer */
      int tmp = atoi(argv[1]);
      
      /* Check if the number is outside the valid range */
      if((tmp < 25) || (tmp > 40)) {
        msg = MSG_RESET;
      }
      else {
        /* Your action here: for example, setting a threshold variable */
        // threshold = tmp;
      }
    }
    else {
      msg = MSG_RESET;
    }
  }
  
  /* Provide usage feedback if command format is incorrect */
  if(msg == MSG_RESET) {
    chprintf(chp, "set threshold ERR\r\n");
    chprintf(chp, "Usage: set threshold [temp] where temp is a number between 25 and 40\r\n");
  }
  else {
    chprintf(chp, "set threshold OK\r\n");
  }
}

This method allows for precise control over the command inputs and provides clear feedback to the user on how to correctly use the command

Implementing breakable loops

A common requirement in shell functionality is creating functions that run in a loop until interrupted by a key press. This allows for tasks, such as printing sensor data to the shell indefinitely, to be executed until the user decides to stop. We illustrated this concept with the “write” command example, showcasing the importance of understanding how to implement such breakable loops effectively.

The core functionality is encapsulated in the following loop:

  while (chnGetTimeout((BaseChannel *)&SD5, TIME_IMMEDIATE) == MSG_TIMEOUT) {
    streamWrite(chp, buf, sizeof buf - 1);
  }
  chprintf(chp, "\r\n\nstopped\r\n");

The key to this functionality is the chnGetTimeout function, a method of the BaseChannel abstract interface, which extends BaseSequentialStream by adding four additional methods with timeouts (gett, putt, writet, and readt). The SerialDriver implements both interfaces, therefore, it is possible to use chnGetTimeout on the SerialDriver.

This function, similarly to sdGetTimeout, is designed for reading a single byte from a SerialDriver within a specified timeout period. When set to TIME_IMMEDIATE, this function returns immediately.

If the function returns a character, it indicates a key press by the user. Otherwise, it returns MSG_TIMEOUT, indicating no key press.

As a result, the loop continues indefinitely as long as chnGetTimeout returns MSG_TIMEOUT. Once a key is pressed, chnGetTimeout will return a character different from MSG_TIMEOUT, breaking the loop.

While this method effectively prints data upon command, it comes with a significant limitation: data printing must be halted each time the user wishes to enter commands. In situations where continuous data streaming is desired alongside command input capability, a viable solution could be to employ a second BaseSequentialStream exclusively for data streaming, while reserving the Shell for command execution.

Disabling native commands

The shell is equipped with several built-in commands, all of which, except for “help,” can be disabled. These commands are specified in the [ChibiOS Root]\os\various\shell\shell_cmd.h file. Disabling them involves setting certain flags to false:

/*===========================================================================*/
/* Module pre-compile time settings.                                         */
/*===========================================================================*/
#if !defined(SHELL_CMD_EXIT_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_EXIT_ENABLED              TRUE
#endif
#if !defined(SHELL_CMD_INFO_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_INFO_ENABLED              TRUE
#endif
#if !defined(SHELL_CMD_ECHO_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_ECHO_ENABLED              TRUE
#endif
#if !defined(SHELL_CMD_SYSTIME_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_SYSTIME_ENABLED           TRUE
#endif
#if !defined(SHELL_CMD_MEM_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_MEM_ENABLED               TRUE
#endif
#if !defined(SHELL_CMD_THREADS_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_THREADS_ENABLED           TRUE
#endif
#if !defined(SHELL_CMD_TEST_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_TEST_ENABLED              TRUE
#endif
#if !defined(SHELL_CMD_FILES_ENABLED) || defined(__DOXYGEN__)
#define SHELL_CMD_FILES_ENABLED             FALSE
#endif

However this configuration is common to all the projects using the shell, and it could be subject to changes if we update our ChibiOS repository. Therefore, instead of modifying these flags directly in the header, a more manageable approach is to modify them through the project’s makefile. This allows for project-specific customization without altering the shared codebase. For instance, to disable the “systime” and “test” commands, you can modify the UDEFS variable in your makefile as shown:

##############################################################################
# Start of user section
#
# List all user C define here, like -D_DEBUG=1
UDEFS = -DSHELL_CMD_SYSTIME_ENABLED=0 \
        -DSHELL_CMD_TEST_ENABLED=0

With this adjustment, executing the “help” command will reflect the absence of the “systime” and “test” commands.

The result of the “help” command after disabling certain commands.

Among the native commands, special attention should be given to “exit” and “test”:

  • The command “exit” terminates the shell session. Its disablement is crucial if you intend for the shell thread to run indefinitely without user termination. This setup requires the shell thread to be initialized prior to entering the main application loop, allowing the main loop to serve other purposes.
  • The command “test” launches the test suite, primarily used to verify ChibiOS functionality on new microcontroller ports or for benchmarking. Although valuable for development, it consumes significant memory, making it desirable to disable for deployed applications. Disabling the “test” command in the makefile and commenting out or removing related includes can free up resources.

The code snippet below demonstrates how to exclude the “test” command and the test suite from our project

##############################################################################
# Project, target, sources and paths
#
...
include $(CHIBIOS)/tools/mk/autobuild.mk
# Other files (optional).
#include $(CHIBIOS)/os/test/test.mk
#include $(CHIBIOS)/test/rt/rt_test.mk
#include $(CHIBIOS)/test/oslib/oslib_test.mk
include $(CHIBIOS)/os/hal/lib/streams/streams.mk
include $(CHIBIOS)/os/various/shell/shell.mk
...
##############################################################################
# Start of user section
#
# List all user C define here, like -D_DEBUG=1
UDEFS = -DSHELL_CMD_TEST_ENABLED=0
...

This approach allows for efficient management and customization of shell commands, tailoring the shell’s functionality to specific project requirements.

Advanced Shell configurations

The shell provides further customization options, detailed in the file [ChibiOS Root]\os\various\shell\shell.h. At this time, we will focus on two significant settings:

/**
 * @brief   Shell maximum input line length.
 */
#if !defined(SHELL_MAX_LINE_LENGTH) || defined(__DOXYGEN__)
#define SHELL_MAX_LINE_LENGTH       64
#endif
/**
 * @brief   Shell maximum arguments per command.
 */
#if !defined(SHELL_MAX_ARGUMENTS) || defined(__DOXYGEN__)
#define SHELL_MAX_ARGUMENTS         4
#endif

These configurations set limits on the maximum characters per input line and the maximum number of arguments a command can accept. Adjusting these limits is possible through the project’s makefile, but caution is advised. Increasing these values directly impacts the shell’s memory requirements. If system crashes occur after adjustments, you may need to allocate more memory to the shell’s working area.

Other configurable features, such as shell history and autocompletion, are also available but less critical for beginners. These advanced topics will be covered in a future article.

Conclusions

In conclusion, this article provides a solid foundation for anyone looking to start building a customized Command Line Interface (CLI) using ChibiOS/Shell. From setting up and running the Shell to managing command handlers and configuring advanced shell properties, we have covered essential aspects to get you underway. Whether it is implementing breakable loops, disabling native commands, or adjusting shell parameters for memory efficiency, these guidelines equip you with the knowledge to tailor the CLI to your project’s needs. With this introduction, you are well on your way to creating a CLI that enhances the interaction with your embedded systems, making development and debugging more efficient and user-friendly.

Be the first to reply at Build your Command Line Interface: a Guide to ChibiOS/Shell

Leave a Reply