DIY line command interpreter on a microcontroller

    In each device under development, I had debug output in UART, as in the most common and simple interface.
    And every time, sooner or later, in addition to the passive output, I wanted to enter commands through the same UART. Usually this happened when I wanted to debug some very large amount of information on request (for example, the status of NANDFLASH when developing my own file system). And sometimes I wanted to programmatically control the legs of the GPIO in order to rehearse the work with some peripherals on the board.
    Somehow, I needed a CLI that allows me to process different commands. If someone came across a ready-made tool for these purposes - I will be grateful for the link in the comments. In the meantime, I wrote my own.

    Requirements, in decreasing order of importance:
    1. Language C . I'm not ready to write software for microcontrollers on anything else, although the situation may change.
    2. Reception and processing of strings from UART. For simplicity, all lines end with '\ n' .
    3. The ability to pass parameters to the command. The set of parameters varies for different teams.
    4. Ease of adding new teams.
    5. Ability to add new commands in different source files. Those. starting to implement the next functionality in the file " new_feature.c " I do not touch the CLI sources, but add new commands in the same file " new_feature.c ".
    6. Minimum resources used (RAM, ROM, CPU).

    I will not describe in detail the UART driver storing the received characters in a static buffer, discarding spaces at the beginning of a line and waiting for a newline character.
    Let's start with the more interesting - we have a line ending in '\ n' . Now you need to find the appropriate command and execute it.
    Solution in the form
    typedef void (*cmd_callback_ptr)(const char*);
    typedef struct
    {
      const char *cmd_name;
      cmd_callback_ptr callback;
    }command_definition;
    

    and a search in the set of registered teams for the team with the desired name begs. But here's the catch - how to implement this search? Or, more precisely, how to compose this set?
    If it were C ++, the most obvious solution would be to use std :: mapand search in it (no matter how). Then the process of registering a command would be reduced to adding a pointer to a handler function to the dictionary. But I'm writing in C , and I don’t want to switch to C ++ yet.
    The next idea is the global array command_definition registered_commands [] = {...} , but this way violates the requirement to add commands from different files.
    Create an array of "bigger" and add commands like function
    #define MAX_COMMANDS 100
    command_definition registered_commands[MAX_COMMANDS];
    void add_command(const char *name, cmd_callback_ptr callback)
    {
      static size_t commands_count = 0;
      if (commands_count == MAX_COMMANDS)
        return;
      registered_command[commands_count].cmd_name = name;
      registered_command[commands_count].callback = callback;
      commands_count++;
    }
    
    I also don’t feel like it, because I’ll either have to constantly correct the MAX_COMMANDS constant , or wasting memory in vain ... It's somehow ugly somehow :-)
    To do the same with dynamic memory allocation and increasing the allocated array with realloc on each addition is probably a good way out, but I didn’t want to mess with dynamic memory in general (nowhere else is it used in the project, but it takes a lot of code in ROM, and RAM is not rubber).

    As a result, I came to the following curious, but, unfortunately, not the most portable solution:
    #define REGISTER_COMMAND(name, func) const command_definition handler_##name __attribute__ ((section ("CONSOLE_COMMANDS"))) = \
    { \
      .cmd_name = name, \
      .callback = func \
    }
    extern const command_definition *start_CONSOLE_COMMANDS; //предоставленный линкером символ начала секции CONSOLE_COMMANDS
    extern const command_definition *stop_CONSOLE_COMMANDS; //предоставленный линкером символ конца секции CONSOLE_COMMANDS
    command_definition *findCommand(const char *name)
    {
      for (command_definition *cur_cmd = start_CONSOLE_COMMANDS; cur_cmd < stop_CONSOLE_COMMANDS; cur_cmd++)
      {
         if (strcmp(name, cur_cmd->cmd_name) == 0)
         {
           return cur_cmd;
         }
      }
      return NULL;
    }
    
    All the magic here is enclosed in the REGISTER_COMMAND macro , which creates global variables so that when the code is executed, they will go in memory strictly one after another. And this magic relies on the section attribute , which tells the linker that this variable should be put in a separate section of memory. Thus, at the output we get something very similar to the registered_commands array from the previous example, but not requiring knowing in advance how many elements will be in it. And the linker provides us with pointers to the beginning and end of this array.
    To summarize, write out the pros and cons of this solution:
    Pros:
    • The ability to produce teams until the memory runs out.
    • Checking the uniqueness of team names at the assembly stage. Non-unique commands will lead to the creation of two variables with the same name, which will be diagnosed by the linker as an error.
    • The ability to declare teams in any broadcast unit without changing the rest.
    • No dependencies on any external libraries.
    • Lack of need for special run-time initialization (registration of commands, etc.).
    • Lack of memory overhead. The entire array of commands can be placed in ROM.

    Minuses:
    • Based on a specific toolchain. For others, you will have to edit the creation of the team and, possibly, the linker script.
    • Not implemented on all architectures, as relies on the structure of the binary format of the executable file. (see variable attributes in gcc )
    • Linear search on registered teams, as the array is unsorted.

    You can overcome the last minus at the cost of the last plus - you can place the commands in RAM, and then sort them. Or even pre-calculate some hash function to compare not through strcmp .

    Also popular now: