February 19, 2014

Shell Implementation for Bare-Metal Firmware Applications

Filed under: Firmware — Tags: , , , , , — admin @ 4:24 pm

During firmware development most of the time, I needed to have a simple user interface for the microcontroller based verification environments. Under the Linux environment, we are lucky to use a tty based command line interface such as Bash. In this case Linux helps a lot us to apply your verification/validation program by providing with a handy tty 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 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, interrupt response time is crucial (such as reacting interrupts within 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 in the Figure.1, and as a device under test (DUT) I used Texas Instruments tmp102 i2c temparature sensor. As a host device, Stellaris LM4F120 LaunchPad has been wired.


Figure.1: Basic test environment.

Temperature sensor images 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 with 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 function as C string.
func is a function pointer to the action which is associated with command string.
max_arg_num is a 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 , this is a simple wrapper for the table entries, and it contains number of entries.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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);

Here is the parser. I needed to describe this first, but I believe data structures are also important. Parser function takes three arguments.
tbl is the action/function table, it also contains length of the table which is calculated during compilation. I am not fan of the passing explicit numbers of these data structures as a separate argument. Let’s compiler do its work.
stream is a C string passed from pipe (between uart interrupt handler and command parser task)
delim is a deliminator string. In this case it is

" \r"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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

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

so as to simplify definition of the function entries. In addition to this, 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, in the last line, compiler calculates the number of items in the list on behalf of you.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 my laptop. This helps me to communicate through my 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. I am not fan of using malloc and its friends in the firmware applications. 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 onto a microcontroller.

Thanks for your patience to be able to read till here, and any kinds of feedback are welcome!

Powered by WordPress