microcontroller console with micro readline

    I present to you the microrl library ( on github ), designed to organize the console interface in various kinds of built-in glands on microcontrollers.

    Why do we need a console in MK?


    The text-based console interface has several advantages for embedded systems, with all its power and simplicity (after all, the text, unlike the LED, speaks for itself!):
    • It requires relatively few MK resources, and a minimum of hardware costs - a serial interface such as UART or any other available in the MK, it can be a built-in USB or an external USB-Com adapter or even TCP if your microcontroller is quite serious.
    • It’s convenient to connect - just a terminal supporting Com-port (putty for Windows or minicom for linux) is enough.
    • Convenient to use - color output to the terminal, support for auto-additions, hot keys and input history.

    So that the library currently supports:
    • basic functions of the vt100 terminal (most terminal emulators support it)
    • a configuration file that allows you to turn features on and off to save memory (for MK it is very important);
    • understands HOME, END, cursor keys, backspace;
    • understands the hot keys ^ U ^ K ^ E ^ A ^ N ^ P etc.
    • input history with navigation up-down arrows and hotkeys
    • auto-completion (auto-substitution?)
    I decided to write a library that is analogous to gnu readline for linux, i.e. the part that is responsible for terminal input, processing of the string and control sequences of the terminal, etc. The main goals are compactness, ease of use, a minimum of necessary functionality for comfortable work (we are not talking about large PCs, but small MKs with tens or hundreds of KB of memory).

    Bit of theory


    A small digression into the history and features of the terminal economy were well described in this topic , I will not repeat it, I will describe only in brief the principle of work.
    image
    From the user's point of view, everything starts in the terminal and ends in it, because as Wikipedia says: “A terminal is an input / output device, its main functions are to display and enter data . There are a lot of terminal emulators for all platforms and with different functionality, for example, gnome-terminal, rxvt, putty, minicom, etc.

    The user presses the buttons on the keyboard, the terminal sends them via any channel to the device or system, and it returns the characters for printing on the screen. In addition to simple text characters, ESC sequences are transmitted to both sides for transmitting control codes and service information, for example, special codes are sent from the keyboard. keys (Enter, cursor, ESC, Backspace, etc.). The sequences go back to the screen to control the cursor position, clear the screen, line feed, delete a character, control color, font type, etc.

    An ESC sequence, in general, is a sequence of bytes starting with an ESC character with code 27, followed by sequence codes consisting of a number of printed or non-printed characters. For the vt100 terminal, codes can be viewed, for example, here. Control codes are non-printable characters with codes from 0 to 31 (32 is the code of the first ascii character - a space).

    The program in the device (the part that is responsible for the command line), receiving characters and sequences, forms in its command line buffer what the user enters and displays this line back to the terminal (the local terminal echo must be turned off). The device prints text and ESC sequences on the screen to control the cursor, color, cursor position, as well as commands such as "Delete text from cursor to end of line." In fact, this is the main task of the library - to create a line in the memory and on the terminal screen, to allow the user to edit it (delete arbitrary characters of the line, move along it, etc.) and at the right time to give it to the higher-order interpreter for processing.

    A good console should have a history of input, well, and, perhaps, more auto-add-ons, without which it would not be so comfortable in the terminal.

    Internal organization


    Consider the architecture of the application using the library:


    The figure shows a block diagram of the interaction of microrl and the user application. The blue arrows indicate the callback functions that are called upon the occurrence of events, the green arrow indicates the call to the library function, in which, in fact, all the work takes place.

    Before use, you need to install 3 callbac functions (blue):
    void print (char * str); // вызывается для вывода в терминал
    int execute (int argc, char * argv[]); // вызывается когда пользователь нажал ENTER
    char ** complete (int argc, char * argv[]); // вызывается когда пользователь нажал TAB

    The complete function is optional, to save resources you can disable auto-completion in the configuration file in general.

    Input stream

    The user application receives characters from the terminal (via the serial interface: UART, usb-cdc, tcp-socket, etc.) and transfers them to the library by calling the function (green arrow):
    void microrl_insert_char (char ch);
    ESC sequences and control characters that are responsible for moving the cursor, pressing Tab, Enter, Backspace, etc., are extracted and processed from the input stream. The remaining characters entered from the keyboard are placed in the command line buffer.

    Execute

    When the user presses Enter ( 0x10 or 0x13 code is encountered in the input sequence ), the command line buffer is cut into “tokens” (words separated by spaces) and the execute function is called with the number of these words in argc and an array of argv pointers .

    The words in the argv [] array are NULL-terminated, which means you can use ordinary string functions, so command processing is as simple and convenient as processing the parameters of the main function of the desktop application. Many, if not all, are familiar with this technology, well, or you can easily find information .

    To save RAM, all spaces entered are replaced with the character ' \ 0 ', and when output to the terminal are replaced back with spaces. Thanks to this, you can use one buffer for entering and storing the command line and for processing it, because it is enough to “collect” pointers to the beginning of the tokens, and all of them will automatically be NULL-terminated.



    Processing of commands is done by the library user in the execute function , in fact, this is the shell, but do not be afraid of this phrase: D, the usual if - else if - else is the simplest shell:
    /*пример простого обработчика команд "help", "mem clear" и "mem dump"*/
    int execute (int argc, char * argv[])
    {
        int i = 0;
        if (!strcmp (argv[i], "mem")) {
            i++;
            if ((i < argc) && (!strcmp (argv[i], "dump"))) { 
                mem_dump ();
            } else if ((i < argc) && (!strcmp(argv[i], "clear"))) {
                mem_clear();
            } else {
                printf ("\"mem\" needs argument {dump|clear}\n");
            }
        } else if (!strcmp (argv[i], "help")) {
            print_help ();
        } else {
             printf ("%s: cmd not found\n");
        }
        return 0;
    }


    Auto Add-ons with Complete

    When the user wants auto-add-ons, he presses Tab - this is a persistent habit for everyone who works with the console. Here we do the same when the tabulation code is caught - again we cut the line with the pointers in argv, but not for the entire line, but only for the section from the beginning to the cursor (do we usually complete the word under the cursor?). The same int argc and char * argv [] are passed to callback complete , and there is one trick: if the cursor is blank, then we are starting a new word, i.e. we do not seem to supplement anything concrete, in this case an empty string will lie in the last argv [argc-1] element.
    Why is this needed? In order to make it clear in the callback function of the auto-completion what commands the user has already entered, and whether he is completing something specific, or simply clicks the Tab to see the available commands. As you can see, you have everything in order to make truly “smart” additions, no worse than in adult consoles.

    Important!! The last element of the array must always be NULL!
    If you return NULL in the very first element ([NULL]) - this means that there are no options for addition.
    If there is one element in front of the array before NULL (["help"] [NULL]) - that means there was only one option, it will be completely substituted.
    If there are several elements in the array (["clean"] ["clear"] [NULL]) - then only the general part of the words will be supplemented, if any, in general, everything is familiar as in bash: D!

    Input history

    If you have enough RAM, feel free to include input history support in the config - it improves usability! To save money, a ring buffer is used, so you can’t say how many last command lines we can remember, it depends on their length. Search in the history is carried out as usual, up / down arrows or hotkeys Ctrl + n Ctrl + p (try in bash!). It works simply: messages are copied to the buffer in turn, if there is no space, delete the old ones until it appears, then the line is copied to memory, and the pointer to the last message is shifted after it. When the end of the buffer is reached, we jump over 0 and so on in a circle.

    Resources


    All that is needed to implement the console in the application is a bit of memory and a serial two-way interface, you can use UART (including via a USB-RS232 converter), usb-cdc, wireless bluetooth-serial modules with a Serial com-port profile, tcp sockets, etc. , all that can be connected between a PC and a controller, and through which terminal emulators can work.

    As for the memory, I collected everything with GCC with -0s optimization for the Atmega8 (8-bit) controller (an example is in the source) and for the AT91SAM7S64 (16/32-bit) controller on the ARM7 core. For comparison, I collected in two versions: in the stripped-down one - without auto-additions, without a history of entering and processing cursor arrows and complete, here is the result
                      ARM                AVR
    урезанный       1,5Кб              1,6Кб
    полный          3,1Кб              3,9Кб 


    Notice how the 16/32 bit ARM core does AVR!
    I must say that the measurements were carried out only for the library itself, neither USART processing (USB-CDC for АRM), nor the interpreter were taken into account, because this is a shell.
    I’ll just say that the example in the source for AVR takes about 6 Kb Flash (out of 8), but there is “all-inclusive” from the library’s features, you can narrow down to 4. Obviously, for very small controllers (with 8 Kb) this is already expensive, but the amount of memory in controllers is growing by leaps and bounds, now you will not surprise anyone with a ST or NXP MK with 128, 512KB Flash.

    As for RAM, everything is simple, the library needs ~ 30 bytes for the internal variables, plus a buffer for the command line — you determine its length in the config, plus a buffer for the input history — put on how much it matters (but not more than 256 bytes in this implementation) .

    Use cases:

    Debugging software . You can debug logic and algorithms by emulating events from other devices / systems / protocols, change parameters, and select empirical values.
    # Запрос состояния
    > print status
    state machine: receive
    # Установка значений переменных
    > set low_value 23
    # Отладка протоколов
    > set speed 9200
    > send_msg 23409823
    # Вывод дампов
    > map dump 0 8
    0x40 0x0 0x0 0x0 0x34 0x12 0xFF 0xFF
    # отладка логики и алгоритмов
    > rise_event alarm
    # Вызовы процедур
    > calc_crc16
    0x92A1


    The configuration of the device . Setting parameters via the CLI is easier than writing binary data: it does not require additional utilities, it does not require special interfaces. It is performed on any PC with a terminal and a COM port (including virtual through an adapter). Users (many) themselves are able to use the CLI if necessary.
    # Конфигурируем устройство
    > set phone_0 8952920xxxx
    > set dial_delay 150
    > set alarm_level low
    > set user_name Eugene
    > set temperature 36
    > print config
    0 phone: 8952920xxxx
    dial delay: 150 ms
    alarm level: low
    user name: Eugene
    temperature: 36
    


    Monitoring . Upon request, you can print any data of any subsystem, buffers or counters.
    # Вывод измереных значений с 1 по 4 канал АЦП
    > print ADC_val 1 4
    121, 63, 55, 0
    # Запрос значения счетчика
    > get operation_count
    87
    # Вывод статистики
    > print stat
    statistics: counted 299 pulse, smaller is 11 ms, longer is 119 ms


    Interactive device management . Turn on the relay, turn your head 30 degrees, turn on the camera, take a step to the left. Using bluetooth-serial modules, you can steer a mobile robot through the console! I think the idea is clear.

    Additionally, you can organize authorization using a password or one-time / N-time access.
    And of course, pseudographics in the terminal! The game console in the literal sense of the word "console"! : D

    As you can see, sometimes such an interface can really be very out of place, not necessarily the main one, but debugging, configuration or duplicating.

    Lyrical digression




    The idea to write a library was born when I was making a USB IR receiver IRin , as a replacement for lirc with its complex infrastructure. My USB dongle is defined without special drivers in the system as / dev / ACM0 , which is essentially a virtual com port. When I press the remote control button, the dongle sends Ascii a string of the form " NEC 23442 " - the code of the pressed button to the port. Button processing is very simple, a regular bash script that reads / dev / ACM0 with a large switch by button codes.
    Excellent! what else is needed? Simple, convenient, no complicated configs, no lirc. But somehow I wanted to get the line " VOLUME_UP " instead of " NEC D12C0A " from the port"... But how to set correspondences if there is only one button on the device, and then it is not used yet? Very simple! We open virtual com-port" / dev / ACM0 " through the minicom terminal emulator
    $minicom -D /dev/ACM0 -b 115200 
    and get the console! Next, enter the commands:
    > set_name VOLUME_UP #установить имя для последней нажатой кнопки пульта.

    Button aliases are stored in the AT24C16 2KB EEPROM. In addition, there is such a parameter as the speed of repeated pressing when you clamp the button on the remote control. It can be installed with the command:
    > repeat_speed 500

    You can still do
    > eeprom print 1 60 # вывести из памяти все записи кнопок с 1 по 60

    well, a fun team
    > eeprom format # очистить все записи в памяти


    Conclusion


    In the near future I will make a library for the Arduino SDK.
    See the source for examples, there is an example for unix * and for AVR. For AVR, you can simply download the hex file, flash and try it! The application allows you to enable / disable the input / output lines of the ports PORTB and PORTD.

    I hope it was interesting: D
    Thank you for your attention!

    Link to github github.com/Helius/microrl

    Also popular now: