Embedded Systems CLI

When I develop an embedded system, one of the first things I implement is a command-line interface (CLI) which is invaluable for unit testing. It doesn’t take a lot of code to make a fairly sophisticated interface that allows you to type commands and get responses just like a unix or windows/dos command shell.

When I am developing in EmBitz (my favorite microcontroller IDE), for STM32 microcontrollers (my favorite microcontrollers), I use an STLinkV2 (or clone) to allow me to load software into the target and debug it easily. EmBitz provides a wonderful additional feature called EBMonitor that hooks _write() and allows you to redirect standard input and standard output (e.g. printf) over the STLinkV2 and display it in a console window within the development environment. This means you don’t need a serial connection to your target to access the console. See my previous post for more information on using EBMonitor.

However, you often want the CLI to be available for non-developers (e.g. users) using a serial connection via a USB-to-TTL dongle or a USB-to-TTL serial converter built into your target such as the CH340G or HT42B534 into the target. Creating a serial UART console is easy too; you just need to implement your own _read() and _write() functions that usually look something like this:

#ifndef USE_EBMONITOR

// Function called by stdio to send output to stdout
int _write( int fd, char* buf, int len )
{
    int i;
    for(i=0; i<len; i++) {
        uart_putchar(buf[i]);
    }
    return i ;
}

// Function called by stdio to read data from stdin
int _read(int const fd, char* buf, unsigned buf_size)
{
    int nRead = 0;
    do {
        int ch = uart_get_char();
        if ((ch != EOF) && buf) {
           buf[nRead++] = ch;
        }
    } while ((ch != EOF) && (nRead < buf_size));
    return nRead ? nRead : EOF;

}
#endif

and uart_getchar() and uart_putchar() are functions that read/write a character from/to the UART…trivial for polled output or a little more complicated if you want it interrupt-driven (which you do). Once you’ve written this, then you can just #include <stdio.h> in your other modules and use printf() for formatted I/O.

Notice the use of the #ifndef USE_EBMONITOR to wrap _write(). I do this so I can use EBMonitor for debug builds and UART for release builds. EmBitz supports two targets by default: Debug and Release. For the Debug target, I define USE_EBMONITOR under:
Project -> Build Options -> Compiler Settings -> #defines

For the Release target I don’t define EBMONITOR:

Writing interrupt driven UART code is beyond the scope of this post, but there are loads of examples and tutorials online. When implementing a CLI you’ll probably want to do some processing of characters as they are received in the ISR. Typically, you’ll store them in a command buffer and then set a flag (e.g. cmd_ready) when a carriage return is received to indicate that there is a command ready to be processed (don’t process commands in interrupt time; just poll the flag in your main loop and clear it after processing the command).

I usually have a command interpreter module that creates a linked-list of commands and their associated functions. The structure of a command looks like this:

/// Commands are stored as linked lists
typedef struct cmd_s {
    char  *nameP;        // command name - string to match
    void (*fnP)();       // function to execute if string matched
    struct cmd_s *nextP; // link to next command in this list
} Command;

The command interpreter code then has only a few EBMonitor-specific portions like those below (and most of those are just for efficiency):

void command_init(void) {
#ifdef USE_EBMONITOR
    // UART1 is normally used for console I/O, but
    // EBLink GDB Server supports console I/O via STLink debug interface
    // so we don't have to use the UART for debugging.  printf output
    // is buffered until \r\n or fflush(stdout) and then displayed in EB monitor
    // input is read from stdin (scanf, fgets, etc.)
    void    EBmonitor_buffer(FILE* , char*, uint16_t);
    #define EBM_outLength 128       // EB Monitor is used for debugging
    #define EBM_inLength 64
    static char EBM_out[EBM_outLength];
    static char EBM_in[EBM_inLength];

    // Route console I/O over the STLink debug interface
    EBmonitor_buffer(stdout, EBM_out, EBM_outLength);
    EBmonitor_buffer(stdin,  EBM_in,  EBM_inLength);
#endif
    // Turn off buffers, so I/O occurs immediately
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
}

The rest of the command interpreter is the same between UART and STLinkV2 interfaces. For example:


/// Top-level command list
Command *commandsP;

/// Flag indicating a command is ready to be processed
unsigned char cmd_ready;

/// Buffer for current command being entered
#define MAX_CMD_LEN 80
static char cmd[MAX_CMD_LEN];

/// Prompt user for a command
void command_prompt(void) {
   printf("ready>");
   fflush(stdout);
}

/// @returns true if a command is ready for processing
int command_ready(void)
{
    return !!cmd_ready;
}

/// Add a command to the head of the commands list
void command_add(Command **listP, Command *cmdP) {
    if (cmdP && listP) {
        cmdP->nextP = *listP;
        *listP = cmdP;
    }
}

/// Display commands available in the specified command list
static
void list_commands(Command *listP) {
    printf("Commands: ");
    while (listP) {
       printf("%s ", listP->nameP);
       listP = listP->nextP;
    }
    printf("\r\n");
}

// Call regularly from your main loop
void command_process(void)
{
    static int len;   // length of current command in buffer
    int ch = getchar();
    if (ch != EOF) {
        // drop received characters while waiting to process last command
        if (cmd_ready) return;

        if ((ch == '\r') || (ch== '\n')) {
            putchar('\r');
            putchar('\n');
            if (len) {
                cmd[len] = 0; // null terminate current command
                cmd_ready = 1;
                len      = 0;
            } else {
                command_prompt();
            }
        } else if (ch == '\b') {
            if (len) {
                len--;
                putchar(ch);
                putchar(' ');
                putchar(ch);
            } else {
                putchar('\a'); // sound beep
            }
        } else if ((len+1 < MAX_CMD_LEN)) {
            cmd[len++] = ch;
            putchar(ch);
        } else {
            putchar('\a'); // sound beep
        }
    }

    if (cmd_ready) {
        char *command = strtok(cmd, " \r\n");  // extract first command token
        command_execute(commandsP, command);
        command_prompt();
        cmd_ready = 0;
    }

}


/// Search list of commands for specified command and execute if found
void command_execute(Command *listP, char *command) {
    // search list of commands and execute matching command if found
    Command *cmdP = listP;
    while (command && cmdP) {
        if (strcmp(command, cmdP->nameP) == 0) {
            // command found so execute associated function
             cmdP->fnP();
             return;
        }
        cmdP = cmdP->nextP;
    }
    // command not found, show user the command options
    list_commands(listP);

    cmd_ready = 0;
}

Here’s an example of a command:


static
void fwinfo_fn(void) {
    printf("Built: %s\r\n",__DATE__ " " __TIME__);
}
static Command fwinfo_cmd = {"fwinfo", fwinfo_fn, 0};

....

command_add(&commandsP, &fwinfo_cmd);

Call command_process() from your main loop and voila…CLI!