консоль в микроконтроллере с micro readline

    Представляю вашему вниманию библиотеку microrl (on github), предназначенную для организации консольного интерфейса в разного рода встраиваемых железках на микроконтроллерах.

    Зачем нам консоль в МК?


    Текстовый консольный интерфейс обладает рядом преимуществ для встраиваемых систем, при всей своей мощи и простоте (ведь текст, в отличие от светодиода, говорит сам за себя!):
    • Требует относительно мало ресурсов МК, и минимум аппаратных затрат — последовательный интерфейс типа UART или любой другой имеющийся в МК, это может быть встроенный USB или внешний USB-Com адаптер или даже TCP если ваше микроконтроллер достаточно серьезный.
    • Удобно подключаться — достаточно терминала поддерживающего Com-port (putty для Windows или minicom для linux).
    • Удобно использовать — цветной вывод в терминал, поддержка авто-дополнений, горячих клавиш и истории ввода.

    Итак, что библиотека поддерживает на данный момент:
    • базовые функции терминала vt100 (его поддерживают большинство эмуляторов терминала)
    • конфигурационный файл, позволяющий включать и выключать фичи для экономии памяти (для МК очень актуально);
    • понимает HOME, END, курсорные клавиши, backspace;
    • понимает горячие клавиши ^U ^K ^E ^A ^N ^P итд;
    • историю ввода с навигацией стрелками вверх-вниз и хоткеями
    • авто-дополнения (авто-подстановка?)
    Я решил написать библиотеку, являющуюся аналогом gnu readline для linux, т.е. той частью, которая отвечает за терминальный ввод, обработку строки и управляющих последовательностей терминала итд. Основные цели — компактность, простота в использовании, минимум необходимого функционала для комфортной работы (речь ведь идет не о больших ПК, а маленьких МК с десятками-сотнями Кб памяти).

    Немного теории


    Небольшой экскурс в историю и особенности терминального хозяйства хорошо были описаны в этом топике, не буду повторяться, опишу только в кратце принцип работы.
    image
    С точки зрения пользователя все начинается в терминале и в нем же заканчивается, потому что как гласит википедия: «Терминал — это устройство ввода-вывода, его основные функции заключаются в отображении и вводе данных». Эмуляторов терминалов существует огромное множество под все платформы и с разным функционалом, например, gnome-terminal, rxvt, putty, minicom итд.

    Пользователь нажимает кнопки на клавиатуре, терминал посылает их по какому-либо каналу в устройство или систему, а оно возвращает символы для печати на экран. Кроме простых текстовых символов в обе стороны передаются ESC-последовательности для передачи управляющих кодов и служебной информации, например, от клавиатуры идут коды спец. клавиш (Enter, курсор, ESC, Backspace итд). Обратно на экран идут последовательности для управления положением курсора, очистки экрана, перевода строки, удаления символа, управление цветом, видом шрифта итд.

    ESC-последовательность, в общем, виде представляет собой последовательность байт начинающихся с ESC символа с кодом 27, затем идут коды последовательности, состоящие из некоторого кол-ва печатных или непечатных символов. Для терминала vt100 коды можно посмотреть например тут. Управляющие коды — это непечатные символы с кодами от 0 до 31 (32 — это код первого ascii-символа — пробела).

    Программа в устройстве (та ее часть, которая отвечает за командную строку), принимая символы и последовательности, формирует в своем буфере командной строки то, что вводит пользователь, и выводит эту строку обратно в терминал (местное echo терминала должно быть выключено). Устройство печатает на экран текст и ESC-последовательности для управления курсором, цветом, положением курсора, а так же команды типа «Удалить текст от курсора до конца строки». По сути, это и есть основная задача библиотеки — формировать строку в памяти и на экране терминала, позволять пользователю ее редактировать (удалять произвольные символы строки, перемещаться по ней итд) и в нужный момент отдавать ее на обработку вышестоящему интерпретатору.

    Хорошая консоль должна иметь историю ввода, ну, и, пожалуй, еще авто-дополнения, без которых в терминале было бы не так комфортно.

    Внутреннее устройство


    Рассмотрим архитектуру приложения, использующего библиотеку:


    На рисунке изображена блок-схема взаимодействия microrl и приложения пользователя. Синими стрелками обозначены callback-функции, вызываемые при наступлении событий, зеленой стрелкой показан вызов библиотечной функции, в которой, собственно, и происходит вся работа.

    Перед использованием нужно установить 3 callbac- функции (синие):
    void print (char * str); // вызывается для вывода в терминал
    int execute (int argc, char * argv[]); // вызывается когда пользователь нажал ENTER
    char ** complete (int argc, char * argv[]); // вызывается когда пользователь нажал TAB

    Функция complete не обязательна, для экономии ресурсов вы можете отключить в конфигурационном файле авто-дополнения вообще.

    Входной поток

    Пользовательское приложение принимает от терминала символы (через последовательный интерфейс: UART, usb-cdc, tcp-socket итд) и передает их в библиотеку вызовом функции (зеленая стрелочка):
    void microrl_insert_char (char ch);
    Из входного потока выделяются и обрабатываются ESC-последовательности и управляющие символы, отвечающие за перемещение курсора, нажатия Тab, Enter, Backspace итд… Остальные символы, введенные с клавиатуры, помещаются в буфер командной строки.

    Execute

    Когда пользователь нажимает Enter (во входной последовательности встречается код 0х10 или 0х13), осуществляется нарезка буфера командной строки на «токены» (слова разделенные пробелами) и вызывается функция execute с кол-вом этих слов в argc и массивом указателей argv.

    Слова в массиве argv[] NULL-терминированы, значит можно использовать обычные строковые функции, таким образом, обработка команд такая же простая и удобная, как обработка параметров функции main десктопного приложения. С этой технологией знакомы многие, если не все, ну или можно запросто найти информацию.

    Для экономии ОЗУ все вводимые пробелы заменяются на символ '\0', а при выводе в терминал заменяются обратно на пробелы. Благодаря этому, можно использовать один буфер для ввода и хранения командной строки и для ее обработки, ведь достаточно «собрать» указатели на начала токенов, и все они автоматически будут NULL-терминированы.



    Обработка команд делается пользователем библиотеки в фукции execute, по сути, это и есть командный интерпретатор, но не пугайтесь этой фразы :D, обычный if — else if — else и есть простейший командный интерпретатор:
    /*пример простого обработчика команд "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;
    }


    Авто-дополнения с Complete

    Когда пользователь хочет авто-дополнений он жмет Tab — это стойкая привычка у всех кто работает с консолью. Тут мы делаем тоже самое, когда отловлен код табуляции — опять нарезаем строку указателями в argv, но уже не для всей строки, а только для участка от начала до курсора (мы ведь обычно дополняем слово под курсором?). Те же int argc и char * argv[] передаются в callback complete, и тут есть одна хитрость: если перед курсором стоит пробел, значит мы начинаем новое слово, т.е. мы как бы ничего не дополняем конкретного, в этом случае в последнем элементе argv[argc-1] будет лежать пустая строка.
    Зачем это нужно? Для того, чтоб в callback-функции авто-дополнения было понятно какие команды пользователь уже ввел, и дополняет ли он что то конкретное, или просто щелкает Tab-ом, что бы посмотреть доступные команды. Как видите, у вас есть все для того, чтобы сделать действительно «умные» дополнения, не хуже чем во взрослых консолях.

    Важно!! Последний элемент массива должен быть всегда NULL!
    Если вы вернете NULL в самом первом элементе ([NULL]) — это означает, что вариантов дополнения нет.
    Если в массиве присутствует один элемент перед NULL ([«help»][NULL]) — значит нашелся только один вариант дополнения, он будет просто подставлен полностью.
    Если в массиве присутствуют несколько элементов ([«clean»][«clear»][NULL]) — тогда дополнится только общая часть слов, если она есть, вобщем все привычно как в bash :D!

    История ввода

    Если у вас достаточно ОЗУ, смело включайте в конфиге поддержку истории ввода — повышает удобство работы! Для экономии используется кольцевой буфер, поэтому нельзя сказать сколько последних командных строк мы можем запомнить, это зависит от их длинны. Поиск в истории осуществляется привычно, стрелками вверх/вниз или хоткеями Ctrl+n Ctrl+p (попробуйте в bash!). Работает просто: сообщения копируются в буфер по очереди, если места нет удаляем старые до тех пор пока оно не появится, затем строка копируется в память, а указатель на последнее сообщение смещается вслед за ней. Когда достигнут конец буфера, мы перепрыгиваем через 0 и так по кругу.

    Ресурсы


    Все что необходимо для реализации консоли в приложении — это немного памяти и последовательный двусторонний интерфейс, можно использовать UART (в том числе через конвертор USB-RS232), usb-cdc, беспроводные bluetooth-serial модули с Serial com-port профилем, tcp сокеты итд, все, чем можно связать ПК и контроллер, и через что умеют работать эмуляторы терминалов.

    Что касается памяти, все собирал с GCC с оптимизацией -0s для контроллера Atmega8 (8-bit) (пример есть в исходниках) и для контроллера AT91SAM7S64 (16/32-bit) на ядре ARM7. Для сравнения собирал в двух вариантах: в урезанном — без авто-дополнений, без истории ввода и обработки курсорных стрелок и полном, вот результат
                      ARM                AVR
    урезанный       1,5Кб              1,6Кб
    полный          3,1Кб              3,9Кб 


    Обратите внимание, как 16/32 битное ARM ядро уделывает AVR!
    Надо сказать, что измерения проводились только для самой библиотеки, ни обработка USART (USB-CDC для АRM), ни интерпретатор не учитывались, т.к. это уже оболочка (shell).
    Скажу только, что пример в исходниках для AVR занимает около 6 Кб Flash (из 8), но там «все включено» из возможностей библиотеки, можно поужаться до 4. Очевидно, что для совсем маленьких контроллеров (с 8 Кб) это уже накладно, но кол-во памяти в контроллерах растет как на дрожжах, сейчас никого не удивишь МК от ST или NXP с 128, 512Кб Flash.

    Что касается ОЗУ, тут все просто, библиотеке под внутренние переменные нужно байт ~30, плюс буфер для командной строки — определяете сами в конфиге его длину, плюс буфер для истории ввода — поставьте сколько не жалко (но не более 256 байт в этой реализации).

    Варианты использования:

    Отладка софта. Можно отлаживать логику и алгоритмы, эмулируя события от других приборов/систем/протоколов, изменять параметры, подбирать эмпирические значения.
    # Запрос состояния
    > 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


    Конфигурация устройства. Установка параметров через CLI проще, чем запись бинарных данных: не требует дополнительных утилит, не требует специальных интерфейсов. Выполняется на любом ПК с терминалом и COM-портом (виртуальным через адаптер в том числе). Пользователи (многие) сами в состоянии воспользоваться CLI при необходимости.
    # Конфигурируем устройство
    > 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
    


    Мониторинг. По запросу можно распечатать любые данные любой подсистемы, буферы или счетчики.
    # Вывод измереных значений с 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


    Интерактивное управление устройством. Включить реле, повернуть голову на 30 градусов, включить камеру, сделать шаг влево. Используя bluetooth-serial модули, можно через консоль рулить мобильным роботом! Идея я думаю понятна.

    Дополнительно можно организовать авторизацию c помощью пароля или одноразовый/N-разовый доступ.
    Ну и конечно же псевдографика в терминале! Игровая консоль в буквальном смысле слова «консоль»! :D

    Как видите, иногда такой интерфейс действительно может быть очень к месту, не обязательно основным, а отладочным, конфигурационным или дублирующим.

    Лирическое отступление




    Идея написать библиотеку родилась, когда я делал USB ИК приемник IRin, как замену lirc с его сложной инфраструктурой. Мой USB-донгл определяется без специальных драйверов в системе как /dev/ACM0, что по сути виртуальный com-порт. Когда я нажимаю кнопку пульта, донгл посылает Ascii строку вида "NEC 23442" — код нажатой кнопки в порт. Обработка кнопок очень простая, обычный bash скрипт, читающий /dev/ACM0 с большим switch-ем по кодам кнопок.
    Отлично! что еще нужно? Просто, удобно, никаких сложных конфигов, никаких lirc. Но захотелось мне как-то, чтобы из порта вместо "NEC D12C0A" приходила строка "VOLUME_UP"… Но как задать соответствия, если на устройстве только одна кнопка, и то пока не используется? Очень просто! Открываем через эмулятор терминала minicom виртуальный com-port "/dev/ACM0"
    $minicom -D /dev/ACM0 -b 115200 
    и получаем консоль! Далее вводим команды:
    > set_name VOLUME_UP #установить имя для последней нажатой кнопки пульта.

    Псевдонимы для кнопок сохраняются в AT24C16 2KB EEPROM. Кроме того, есть такой параметр, как скорость повторного нажатия, когда вы зажимаете кнопку на пульте. Ее можно устанавить командой:
    > repeat_speed 500

    Еще можно сделать
    > eeprom print 1 60 # вывести из памяти все записи кнопок с 1 по 60

    ну и веселая команда
    > eeprom format # очистить все записи в памяти


    Заключение


    В ближайшее время сделаю библиотеку для Arduino SDK.
    Примеры смотрите в исходниках, есть пример для unix* и для AVR. Для AVR можно просто скачать hex файл, прошить и попробовать! Приложение позволяет включать/выключать линии ввода-вывода портов PORTB и PORTD.

    Надеюсь было интересно :D
    Спасибо за внимание!

    Ссылка на гитхаб github.com/Helius/microrl
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 29
    • НЛО прилетело и опубликовало эту надпись здесь
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Сама библиотека к платформе никакой привязки не имеет. Требуется дописать 2 функции — прием и передачу символов (связку между железом и библиотекой).
          • НЛО прилетело и опубликовало эту надпись здесь
            • +2
              Совершенно верно, спасибо theaspin! Действительно, ее можно прикрутить, по идее, куда угодно, я хотел ее сделать как можно более простой и понятной, ведь Embedder-ские компиляторы не всегда такие же умные и удобные как GCC…
              • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Автор, правильно ли я понимаю: в памяти контроллера хранятся некие функции, которые вызываются в зависимости от того, что пришло по UARTу (или другому интерфейсу)? Т.е. можно реализовать следующее — прошить контроллер «реакциями» (функциями). и все. а программу писать на любом языке на компьютере. в свою очередь эта программа посылает команды по УАРТу, например, и контроллер делает то, что мы от него хотим. То есть ОДИН РАЗ ПРОШИЛ, и используй в разных целях НЕ ПРОШИВАЯ. Я правильно понимаю цель?
          • 0
            и еще вопрос. почему бы не написать приложение-терминал самому? например для того, чтобы на стороне компьютера организовать автодополнение и историю команд. чтобы этой информацией не забивать память МК. в бльшинстве случаев память — очень критичный момент для контроллеров)
            • +2
              Мне кажется основная выгода в том, что телнет уже есть готовый для кучи девайсов, и если на плате есть, например, bluetooth, то рулить ей можно будет с одинаковым удобством как с компа под виндой или линуксом, так и с андоидофона.
              • +1
                А как терминал узнает какие команды есть в железке на том конце провода?
                А тем более про аргументы этих команд, где и какую надо дополнять? ;)

                Историю да — по идее можно сделать и в терминале.
              • +1
                Я вас понял, вы хотите некую виртуальную машину, которая выполняет команды, причем передаваемые с компьютера а не зашитые в скрипте, вот тут человек сделал нечто подобное bitlash.net/wiki/start#how_does_it_work
                Это интерпретатор который выполняет функции из EEPROM или с SD карточки (понимает FAT и файлы).

                Я хотел сделать либу для консольного ввода, интерпретатор я намеренно сделал для демонстрации отдельно, можете сделать свой или найти готовый, суть в том, что сам по себе интерпретатор вещь специфическая, всем (и мне) нужные разные системы команд и разный функционал в разных проектах.

                Его мы пишем под необходимую задачу, тем более что это довольно просто и не отнимает много времени. Ведь где то надо больше интер-активности, поддержка скриптов, куча команд, а иногда хватит и 3 команд для всего.
                • 0
                  И Вы поняли меня и я понял Вас. спасибо)
              • 0
                не подумал. действительно, мобильность достигается за счет этого. хотя, этими удобствами можно и пожертвовать (история команд, автодополнение), дабы беречь память мк.
                • +1
                  Отличная библиотека. Давно искал такую, а самому влом было писать. Поэтому пользовался совсем простенькой из avrlib. Там, например, история только одной команды, нет управляющих символов, да и много чего еще.
                  В общем, автор, большое спасибо за открытые исходники!
                  • +1
                    Рад если воспользуетесь, пишите в личку или в IssueTracker на github.com если будут проблемы, баги или предложения!
                    • +1
                      Если хотите получить больше отзывов и пожеланий технического плана, советую опубликовать разработку на тематических сайтах. Например, здесь: forum.electronix.ru
                  • +1
                    Примите и от меня благодарность, я как раз недавно хотел заиметь простенький шелл на своёй платке с STM32, прямо мысли мои прочитали (:
                    • +1
                      Пользуйтесь! )) пишите если будут вопросы!
                    • 0
                      По-идее так можно узнать о наборе команд: из терминала посылается запрос в железку «выдать список команд». а описание команд хранить на стороне терминала. может и бред сейчас говорю, но как вариант годен.

                      знаете, я когда прочел Вашу стать, то сразу появилась такая мысль. Ведь флеш память МК выдерживает около 100000 циклов перезаписи. А такой подход с консолью решает эту проблему. нужно только раз прошить МК набором функций самых элементарных (записать значение в порт, считать и т.д.) и писать скрипт на компьютере, который просит выполнить набор этих команд. То есть к МК можно будет подключать разную периферию без перепрошивки кода внутри МК. стоит только изменить скрипт выполняемый на стороне компьютера. правда в этом случае скорость последовательности выполнения команд ограничивается скоростью передачи данных в МК (UART, USB, BLUETOOTH). очень мощный и интересный подход.ь на стороне терминала. может и бред сейчас говорю, но как вариант годен.

                      знаете, я когда прочел Вашу стать, то сразу появилась такая мысль. Ведь флеш память МК выдерживает около 100000 циклов перезаписи. А такой подход с консолью решает эту проблему. нужно только раз прошить МК набором функций самых элементарных (записать значение в порт, считать и т.д.) и писать скрипт на компьютере, который просит выполнить набор этих команд. То есть к МК можно будет подключать разную периферию без перепрошивки кода внутри МК. стоит только изменить скрипт выполняемый на стороне компьютера. правда в этом случае скорость последовательности выполнения команд ограничивается скоростью передачи данных в МК (UART, USB, BLUETOOTH). очень мощный и интересный подход.
                      • 0
                        Блин, извините за повтор. с телефона писал.
                        • 0
                          Для процесса разработки, такое количество циклов можно считать бесконечным.
                          Плюс есть внутрисистемные отладчики(JTAG, debugWire для AVR) которые позволяют ускорить отладку и снизить количество необходимых перепрошивок.
                          • +1
                            А я вот на днях запилил работу с SD-картами, FAT32 и парсер ini-файлов под STM32. Теперь можно конфиг прямо на SD-карте хранить. Ещё всё хочу Scheme туда впихнуть в виде байткода или в нативном (ещё компилятор найди, ага), тогда можно будет и скрипты с карты прожёвывать. Что-то меня в сторону Linux несёт, чувствую (:
                          • +1
                            Как раз писать такой собирался. Спасибо за труд!
                            • 0
                              > В ближайшее время сделаю библиотеку для Arduino SDK
                              Быть бы в курсе такого события… )
                              • 0
                                Напишу в личку, как зарелизю.
                            • 0
                              и мне, если не сложно)
                              • +1
                                Классная штука! Утащил себе в загашники. Спасибо :)

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.