Pull to refresh

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

Reading time9 min
Views33K
Представляю вашему вниманию библиотеку 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
Tags:
Hubs:
+89
Comments31

Articles

Change theme settings