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!