Simple shell implementation for bare-metal firmware applications

During the firmware development, most of the time, I needed to have a simple user interface for the microcontroller based verification environment. Under the Linux , we are lucky to use a TTY based command line interface such as Bash, therefore Linux helps a lot us to apply our verification/validation program by providing with a handy CLI based user interface.

On the other hand, what if we need to use a microcontroller rather than a high end application processor. Some cases, high-end application processors and its MMU aware OSes might result delays and lags, due to the context switching overhead, memory caching, virtual/physical memory translation, etc. Also their hardware’s are pretty complex as compare to the microcontroller architectures. In order to reduce these side affects and improve real-time responses of the verification environment, we can use a microcontroller based system. Especially, the response time of the interrupts is crucial (such as reacting interrupts within a couple microseconds.)

In this case, without having an off the shelf OS, you need to have an user interface to implement your commands to test the functionality of the SoC. You may see the basic setup for the test bed, and the device under test (DUT) in Figure.1. I used Texas Instruments tmp102 as a temperature sensor, and Stellaris LM4F120 LaunchPad as a host device.

Figure.1: Basic test environment.

Temperature sensor images has been obtained from here.

As a designer point of view, we need a command parser and a function table which contains associated function entries and their names. Block diagram of the system can be seen in the Figure.2.

Figure.2: System architecture.

Incidentally, using an simple command line interface through a terminal emulator (such as PuTTY, minicom, etc.) also provides portability over different PCs and OSes. Keep in mind, we are pushing the complexity into the microcontroller (LaunchPad) a little bit, but not so much! You may see the boot message from PuTTY in Figure.3.

Figure.3: Boot messages.

Let’s go through the data structures. We have table_entry data structure in the line 14, this is the basic container of the function to be executed by the parser.
name field contains name of the function as C string.
func is a function pointer to the action which is associated with the command string.
max_arg_num is the upper limit for the number of arguments. We do not want to overflow and mess with the system.
default_arg is the index number of the function in the table. It is useful if you want to call same function via different commands.

Another crucial data structure is table, which is a simple wrapper for the table entries, and it contains number of entries.

#define CMDNOERR                0   /* Command no error */
#define CMDNOPARSE              1   /* Nothing to parse */
#define CMDINVAL                2   /* Invalid command */
#define CMDTMPRM                3   /* Too many parameters */
 
#define INVALID_FUNC            0xFF
#define MAX_ARG                 5
 
#define TABLE_CALL(func) void func(uint8_t index, int argc, argv_t argv)
 
typedef char *argv_t[MAX_ARG];
typedef TABLE_CALL(func_t);
 
typedef struct table_entry {
    const char * const name;
    func_t *func;
    uint8_t max_arg_num;
    uint32_t default_arg;
} table_entry_t;
 
typedef struct table {
    const UINT32_t num;
    const table_entry_t *entry;
} table_t;
 
typedef struct argument {
    uint8_t findex;
    int argc;
    argv_t argv;
} argument_t;
 
int cmd_parse(const table_t *tbl, char *stream, const char * delim);

The parser function takes three arguments.

tbl is the action/function table, it also contains length of the table which is calculated during compilation.
stream is a C string passed from pipe (between the UART interrupt handler and the command parser task)
delim is a deliminator string. In this case it is ” \r”.

int cmd_parse(const table_t *tbl, char *stream, const char * delim)
{
    uint8_t t, state;
    argument_t arg;
    int err;
    char *pch;
 
    err = CMDNOERR;
    state = 0;
    arg.findex = INVALID_FUNC;
    pch = strtok(stream, delim); /* read first token */
 
    if (pch == NULL) {
        err = CMDNOPARSE; /* if there is nothing to parse, return error */
        goto invalid;
    }
 
    while (pch != NULL) {
        if (state == 0) {
            for (t = 0; t < tbl->num; t++)
                if (!strcmp(pch, tbl->entry[t].name)) {
                    arg.findex = t;
                    arg.argv[state] = pch;
                    break;
                }
            if (arg.findex == INVALID_FUNC) {
                err = CMDINVAL; /* command not found */
                goto invalid;
            }
        } else {
            arg.argv[state] = pch;
        }
 
        pch = strtok(NULL, delim);
 
        if (pch == NULL) {
            arg.argc = state + 1;
            tbl->entry[arg.findex].func(arg.findex, arg.argc, arg.argv);
        } else if (state++ == tbl->entry[arg.findex].max_arg_num || state == MAX_ARG) {
            err = CMDTMPRM;  /* too many parameters */
            goto invalid;
        }
    }
 
invalid:
    return err;
}

We can fill in the function table now. Also, TABLE_CALL deserves more detailed explanation. It is defined as below to simplify definition of the function entries.

#define TABLE_CALL(func) void func(uint8_t index, int argc, argv_t argv)

In addition, if we need to change the data structure in the future, we will not need to change the definition of each function every time. Last code section below is an example to show how we can add extra commands. Notice that, in the last line, compiler calculates the number of items in the list on behalf of you.

static TABLE_CALL(reset)
{
    void Reset_Handler(void);
    Reset_Handler();
    return;
}
 
static TABLE_CALL(help)
{
    static const char * const menu =
        "\n Command Line Interface \n\n"\
        "   - reset          reset DUT\n"\
        "   - tmp102         read temperature\n"\
        "   - help           print this menu\n";
 
    printf("%s", menu);
    return;
}
 
static TABLE_CALL(tick)
{
    /* I2C specific functions */
}
 
const table_entry_t entry[] = {
    {"reset", reset, 0, 0},                                 /* 0 arg */
    {"tmp102", tmp102, 0, 0},                               /* 0 arg */
    {"help", help, 0, 0},                                   /* 0 arg */
};
 
table_t table = { sizeof(entry)/sizeof(*entry), entry };

A working example can be seen in the Figure.4. USB connection provides virtual comport access to the development PC. This helps us communicate through the generic terminal program –PuTTY.

Figure.4: Terminal output and hardware setup.

I wanted to share my simple command line interface implementation in this article. My intend is to keep the implementation as simple as possible and dynamic memory allocation free. For simplicity, I used linear search algorithm for the command parser. More advance and optimized search algorithms can be used here, but, for at most 100 function entries, these type of enhancements are too expensive, and prone to be buggy. Especially when we would like to implement them into a microcontroller.

Any feedback is welcome!