Текстовый терминал, как я уже писал в самой первой статье, является упрощённой и профессиональной альтернативой других интерфейсов. Как и любой интерфейс, терминал обеспечивает взаимодействие пользователя с устройствами. Кратко говоря, в качестве такого терминала может служить компьютер с соответствующей программой и интерфейсом RS-232. К данному интерфейсу тем или иным образом подключается управляемое устройство через свой UART интерфейс. Это может быть как подключение через реальный COM-порт компьютера, так и через виртуальный: USB переходник, Bluetooth и прочее. Терминальных программ также существует различное множество. Одна из них – HyperTerminal, являющаяся стандартной программой, встроенной в ранние версии Windows. Её также можно использовать и в более поздних версиях системы.

Приведу ещё один распространённый пример, где применяется терминальный интерфейс. Когда речь идёт о написании простейшей программы для ПК («Hello world»), то данная программа не подразумевает графического интерфейса – она работает в терминале, то есть в командной строке. При запуске она выводит в командную строку слова «Hello world», или что-то другое, что задаст программист. При этом, усложняя программу, она может запросить у пользователя ввести те или иные слова или числа, чтобы в дальнейшем их определённым образом обработать и вывести результат. Похожие программы и приёмы их написания изучаются новичками на уроках программирования в образовательных учреждениях. Точно такие же программы можно написать не только для ПК, но и для микроконтроллеров (МК). При этом вместо командной строки будет использоваться терминал (компьютер с терминальной программой), подключенный к данному МК. Сам по себе терминальный интерфейс вовсе не подразумевает простоту программы и начальный уровень программирования. Программа без графического интерфейса может быть очень сложной, многофункциональной, имея при этом для взаимодействия с пользователем только командную строку. Так же и с устройствами на МК – они могут быть многофункциональными и непростыми, но при этом не иметь кнопок и дисплея, а взаимодействие с пользователем осуществлять через терминал.

Как правило, терминальный интерфейс устройства подразумевает за собой набор текстовых команд. Перечень этих команд предоставляется пользователю. Та или иная команда соответствует определённой функции устройства. Также, устройство может так или иначе отвечать на команды, или просто выдавать какие-либо сообщения в терминал. Одна из простых программ для МК с применением терминала и светодиодов (часто встречается на форумах ардуинщиков) – включение того или иного светодиода по определённой команде с терминала. Но данные команды в этих примерах для простоты ограничены только одним символом. Например, цифра «1» включает первый светодиод, цифра «2» – второй. А в данной статье речь пойдёт о реализации терминального интерфейса с командами, состоящие из целых слов (несколько символов). Чаще всего признаком завершения (ввода) команды является символ перехода на новую строку. Иначе говоря – клавиша Enter в терминале осуществляет ввод команды. Если говорить точно, переход на новую строку соответствует двум идущим подряд «символам»: символу перехода на строку вниз и символу перехода на начало строки. Эти символы, кстати говоря, называются управляющими.

Работая в командных строках различных операционных систем, я замечал следующую особенность. При нажатии кнопок на клавиатуре «Вверх» и «Вниз» листается история раннее введённых команд, что может быть иногда вполне удобно. А ещё при многократном нажатии кнопки «Вправо» печатается символ за символом последней введённой команды. Здесь речь идёт не о терминале для управления внешними устройствами, а о терминальном интерфейсе какой-либо операционной системы. К примеру, в Windows – Командная строка (cmd.exe).

Кроме того, в программе HyperTerminal я давно замечал особенность: при нажатии кнопок со стрелками на клавиатуре курсор в окне терминала перемещается в соответствующем направлении на одну позицию. Причём, это происходит при наличии «эха» (режима самоконтроля), то есть, в простейшем случае, если замкнуть выводы Tx и Rx COM-порта. Стало быть, терминальная программа при нажатии на эти кнопки посылает какую-то информацию, и если данная информация придёт на терминал, то курсор реагирует соответствующе. На самом деле здесь нет ничего необычного. Просто чаще всего в терминале вводят буквы и цифры, и редко где применяются какие-либо дополнительные фишки. Проанализировав трафик и прочитав матчасть, я пришёл к выводу, что при нажатии на клавиши стрелок в терминале он формирует так называемые управляющие последовательности (в дальнейшем – УП). В данном случае они состоят из трёх байтов. Первый байт – ключевой – 0x1B. Он специальный, и не соответствует никакому символу ASCII. У него есть название – «ESC». Забегая вперёд, данный байт терминал на своём выходе выдаёт и при нажатии одноимённой кнопки Esc на клавиатуре. Второй байт соответствует символу «[» (0x5B). Комбинация вышеупомянутых байтов «ESC+[» образует так называемую CSI последовательность, то есть команду. За ней следует третий байт – тот или иной аргумент. Для кнопок со стрелками это байты, соответствующие символам «A», «B», «C» и «D» (по одному байту на каждую кнопку). Вообще, CSI команды являются стандартными и уже определены. Это не только перемещение курсора, но и управление содержимым поля терминала. Например, одна из полезных команд – «CSI+K» – очищает содержимое терминала справа от текущего положения курсора. Кстати, в матчасти сказано, что существует однобайтовый аналог CSI – байт 0x9B, однако он менее распространённый.

Применяя вышеизложенные факты, мне захотелось реализовать в прошивке МК терминальный интерфейс с функцией истории введённых команд, и чтобы она функционировала по аналогии с командной строкой операционных систем. Именно в этом и будет заключаться многофункциональность терминального интерфейса. Сразу отмечу, что далеко не все терминалы поддерживают ввод и вывод управляющих последовательностей. К примеру, встроенный виртуальный терминал в симуляторе Proteus не поддерживает эти функции (по крайней мере, старая 7-ая версия), а мой любимый HyperTerminal – поддерживает. Под него я и буду подстраиваться. Между прочим, последовательность «CSI+K», как я позже заметил, является не только «входящей» (терминал реагирует при приёме, как написано выше), но и «исходящей» – при нажатии на клавишу End. И вообще, экспериментальным методом я обнаружил другие интересные входящие и исходящие последовательности. Они приведены в таблице ниже. Стоит заранее сделать оговорку, что HyperTerminal на клавишу Delete не реагирует. То есть, видимо, не все управляющие последовательности и управляющие символы поддерживает HyperTerminal.

Кнопка

Байты (TX)

Символы

Реакция HyperTherminal на байты (RX)

Новая функция кнопки

Esc

1B

Up

1B 5B 41

□[A

Курсор сдвигается вверх на одну позицию

Прокрутка архива команд вперёд

Down

1B 5B 42

□[B

Курсор сдвигается вниз на одну позицию

Прокрутка архива команд назад

Right

1B 5B 43

□[C

Курсор сдвигается вправо на одну позицию

Ввод символов последней команды или сдвиг курсора вправо на одну позицию

Left

1B 5B 44

□[D

Курсор сдвигается влево на одну позицию

Сдвиг курсора влево на одну позицию

Home

1B 5B 48

□[H

Курсор сдвигается в верхний левый угол

Сдвиг курсора на начало текущей команды

End

1B 5B 4B

□[K

Очищаются символы справа от курсора

Сдвиг курсора на конец текущей команды

F1

1B 4F 50

□OP

Появляется символ «P»

Резерв

F2

1B 4F 51

□OQ

Появляется символ «Q»

Резерв

F3

1B 4F 52

□OR

Появляется символ «R»

Резерв

F4

1B 4F 53

□OS

Появляется символ «S»

Резерв

Терминальный интерфейс можно реализовать в любом МК, где есть UART, и в любой среде программирования. Последнее время набирают популярность STM32 и прочие похожие вещи, но в данной статье будет демонстрироваться вариант с МК AVR. Причём, будет браться не комплекс Arduino, а просто МК (к примеру, Atmega32) и среда разработки CodeVisionAVR с применением CodeWizardAVR для конфигурирования и генерации первоначального кода. Этот вариант мне привычнее всего. Для простоты в проекте я принципиально не буду задействовать входные и выходные порты, то есть, не будет кнопок и светодиодов, ибо основная задача – продемонстрировать работу терминального интерфейса с оговоренными выше функциями.

По ходу написания этой статьи я для демонстрационного примера придумал следующую простую задачу пользовательского уровня, с которой будет работать программа МК через терминал. Пользователь вводит название числа от 0 до 12 на английском языке, а МК в ответ в терминал пишет соответствующее число в числовом виде. Стоит обратить внимание, что в терминале, как правило, используется латиница. Помимо этого будет доступна команда «echo» для включения или отключения режима самоконтроля при вводе команды с терминала. Таким образом, для МК будет реализовано 14 текстовых команд: echo, zero, one, two, three, four, five, six, seven, eight, nine, ten, eleven, twelve. При вводе иной команды МК будет выдавать в терминал слово «Error». Признак завершения команды – нажатие клавиши «Enter. Ещё в программе будет реализована обработка клавиши Backspace для коррекции ввода символа команды.

Функция истории команд будет работать следующим образом. При каждом нажатии клавиши «Вверх» в терминал будут по очереди выводиться ранее введённые команды в хронологическом порядке. По клавише «Вниз» – то же самое, но в обратном порядке. Буфер памяти будет рассчитан на 8 команд, и будет являться цикличным. То есть, старые команды будут перезаписываться новыми. Прокрутка архива команд будет идти не по кругу. Кроме того, при каждом нажатии клавиши «Вправо» будут печататься в терминал символы последней введённой команды. Клавишей Backspace можно стирать символы отображаемой на терминале команды, введённой вручную или выведенной из архива. При всём при этом содержимое в строке терминала будет готово на исполнение по клавише Enter. Можно сделать, чтобы архивировались только корректные команды (из списка), а можно архивировать все вводимые команды подряд. Я выбираю второй вариант. Архивирование повторяющихся команд, введённых друг за другом, будет игнорироваться. При первом включении устройства и попытке нажатия на клавишу «Вверх» ничего не должно происходить, так как ни одной команды ещё не было введено. При первой введённой и исполненной команде она уже будет в архиве и станет доступной повторно по клавише «Вверх». В таком случае нажатие клавиши «Вниз» приведёт к выходу из архива и подготовит строку терминала к вводу новой команды. При вводе второй команды, отличной от первой, она также попадёт в архив и станет доступной по нажатию «Вверх». Повторное нажатие этой клавиши приведёт к вызову первой, более ранней команды. Последующее нажатие «Вниз» приведёт к переключению опять на вторую введённую команду, а ещё одно нажатие – к выходу из архива. По такой же аналогии архив будет функционировать, пока не будут введены 8 команд. При вводе 9-ой команды будет удалена из архива самая первая команда. И вообще, при вводе n-ой команды (n>8) будет удаляться (n-8)-я команда. Иначе говоря, будут доступны 8 последних введённых команд. Также можно будет на текущей отображаемой команде отодвигать курсор влево клавишей «Влево», и внутри строки редактировать команду (дописывать или удалять символы). Такую функцию я не хотел делать до последнего времени, но меня мотивировала на это статья одного из пользователей Хабра. Поэтому работу клавиши «Вправо», выполняющую уже не одну, а две функции, придётся определённым образом продумать.

Можно приступать к созданию проекта. В CodeWizardAVR на вкладке Chip указываем название МК Atmega32, а частоту – 8 МГц. Сразу переходим на вкладку USART. Параметры связи по умолчанию «9600 8N1» можно не менять. Для такой скорости передачи данных и частоты МК, как видно, погрешность 0.2% вполне приемлема. МК будет тактироваться от встроенного RC генератора. Конечно же, на практике так крайне нежелательно делать, но тестирование проекта будет происходить в симуляторе. Далее нужно активировать приёмник, передатчик и разрешить прерывание по приёму. CodeWizardAVR сгенерирует код обработки приёма информации в кольцевой буфер. Размер данного буфера, кратный 8, устанавливается в том же окне. Можно оставить 8 (по умолчанию). На этом конфигурирование можно завершить, нажав соответствующую кнопку.

CodeWizardAVR - Выбор МК и тактовой частоты
CodeWizardAVR - Выбор МК и тактовой частоты
CodeWizardAVR - Настройка USART
CodeWizardAVR - Настройка USART

После успешной генерации кода и сохранения проекта можно приступить дописывать код. Его я уже написал заранее в отдельном файле. Он должен разместиться в тело основного цикла while(1), но я его оставлю в отдельном файле и прикреплю этот файл к коду с помощью функции «include». Не вдаваясь в подробности работы функций приёмника USART и кольцевого буфера, напишу главный принцип его использования. Как только поступает байт на МК, переменная rx_counter увеличивается на единицу. Изначально она равна нулю. При чтении этого принятого байта переменная rx_counter уменьшается на единицу. Чтобы избежать потери данных, нужно читать приходящие байты из буфера не медленнее, чем они приходят в МК. В своей прикладной задаче я также создам буфер (не кольцевой) размером на 32 Байта, в который буду складывать приходящие с терминала символы для формирования командного слова. То есть, максимальное число символов в команде – 31 (учитывая нулевой символ завершения строки). Учитывая, что программа не будет ничем больше загружена, размера первичного буфера 8 Байт вполне хватит: МК будет успевать обрабатывать информацию и не допустит переполнения. Для простоты кода вывод информации по UART буду осуществлять с помощью функции printf. Для ввода информации можно было приделать функцию scanf, но я с ней в CodeVisionAVR пока не работал. Стоит заметить, что эти функции в CodeVisionAVR при программировании МК имеют тот же смысл, что и при написании простейших консольных программ для ПК на том же языке C, о которых говорилось выше. Вообще, данный подход с двумя буферами далеко не оптимален, но мой проект на это не претендует. Если стоит задача оптимизации кода, нужно как-то обходиться одним буфером. А у меня буфера два: первый – сгенерированный с помощью CodeWizardAVR, а второй – прикладной для формирования строки. Перед основным циклом нужно объявить необходимые переменные. Их я также пропишу в отдельном файле и затем также прикреплю его к коду с помощью функции «include». Ещё в программе будут применяться функции работы со строками, поэтому в начале кода необходимо дописать «#include string.h».

Алгоритм программы состоит главным образом из трёх частей: распознавание и обработка УП, обработка и исполнение команд, работа с архивом. Первую часть алгоритма я изобразил в виде упрощённой блок-схемы.

Блок-схема алгоритма распознавания управляющих последовательностей
Блок-схема алгоритма распознавания управляющих последовательностей

Операция чтения байта из UART включает в себя множество вложенных друг в друга операторов switch-case. Вместо использования буфера на 3 байта для приёма возможной управляющей последовательности я использую целочисленный флаг cmd и операции с ним. В зависимости от того или иного принятого байта он принимает то или иное значение. На это значение в дальнейшем ориентируются другие функции при следующем приёме. В итоге, с помощью этого подхода, зная признаки какой-либо УП, её можно без труда распознать. Код этих операций приведён ниже под спойлером.

Реализация алгоритма.
byte=getchar();
switch(byte){
    case 0x08: //Если Backspace;
        //Обработка Backspace;
    break;
    case 0x7F: //Если Ctrl+Backspace (в других терминалах это может быть Delete);
        //Очистка строки;
    break;
    case 0x0D:
    case 0x0A:
        //Завершение ввода команды,
        //сравнение с базовым списком команд,
        //исполнение в случае совпадения,
        //архивирование команды.
        cmd=0;
    break;
    case 0x1B: //ESC;
        cmd=1; //Переход на статус команды в ожидании "[" или "O";
    break;
    case 0x9B: //Однобайтовый аналог CSI (ESC+[);
        cmd=2; //Переход на статус ожидания аргумента;
    break;
    default: //Остальные символы;
        switch(cmd){ //В зависимости от статуса; 
            case 0: //Если не УП;
                //Накопление символов команды;
            break;
            case 1: //Символ после ESC; 
                switch(byte){
                    case 0x5B: //Если второй символ "[";
                        cmd=2; //Подтверждение CSI и переход на статус ожидания аргумента; 
                    break;
                    case 0x4F: //Если второй символ "O";
                        cmd=3; //Подтверждение F1-F4 и переход на статус ожидания аргумента; 
                    break;
                }
            break;
            case 2: //Аргумент CSI;
                switch(byte){
                    case 0x41:
                        //Обработка клавиши "Up":
                        //прокрутка архива команд вперёд,
                        //формирование строки
                        //архивной команды.
                    break;
                    case 0x42:
                        //Обработка клавиши "Down":
                        //прокрутка архива команд назад,
                        //формирование строки
                        //архивной команды.
                    break;
                    case 0x43:
                        //Обработка клавиши "Right":
                        //ввод символов последней команды
                        //или сдвиг курсора вправо
                        //для редактирования.
                    break;
                    case 0x44:
                        //Обработка клавиши "Left":
                        //сдвиг курсора влево
                        //для редактирования.
                    break;
                    case 0x48:
                        //Обработка клавиши "Home":
                        //сдвиг курсора в начало
                        //текущей команды.
                    break;
                    case 0x4B:
                        //Обработка клавиши "End":
                        //сдвиг курсора в конец
                        //текущей команды.
                    break;
                }
                cmd=0; //Выход из статуса команды;
            break; 
            case 3: //Аргумент для F1-F4;
                switch(byte){
                    case 0x50:
                        //Кнопка F1;
                    break;
                    case 0x51:
                        //Кнопка F2;
                    break;
                    case 0x52:
                        //Кнопка F3;
                    break;
                    case 0x53:
                        //Кнопка F4;
                    break;
                }
                cmd=0; //Выход из статуса команды;
            break; 
        }
    break;
}

Во избежание путаницы код приведён в «голом» виде: внутри моих кейсов (распознавателей) приведён только комментарий. Программные коды операций для каждого кейса с развёрнутыми комментариями в виде отдельных кусков я приведу ниже, а полный код – в конце статьи под спойлером. Всего у меня получилось 13 кейсов: накопление и вставка символов команды, обработка нажатия клавиши Enter, Backspace, и десять УП из таблицы. На блок-схеме прямоугольники, соответствующие вышесказанным кейсам, выделены синим цветом. Самый обширный кейс – обработка клавиши Enter. Его я поделю на несколько частей. Пожалуй, с него я и начну. Забыл оговорить, что HyperTerminal с настройками по умолчанию по нажатию клавиши Enter передаёт только символ \r (байт 0x0D), а не \n или комбинацию \r\n. Вообще, можно настроить по-разному. Но в моём случае распознавание Enter осуществляется по любому из этих двух байтов, но не по комбинации.

rx_counter=0;
rx_wr_index=0;
rx_rd_index=0;
rx[b]=0;
b=0;
c=0;
if(echo){
    printf("\r\n");
}
cor=0;

В первых трёх строчках происходит обнуление переменных, обслуживающих кольцевой приёмный буфер UART от CodeWizardAVR, переводя его конфигурацию в первоначальное состояние. Не помню, зачем я это сделал. Видимо – для надёжности. А делал я это ещё давно, когда реализовывал упрощённый терминальный интерфейс без поддержки УП. В четвёртой строке записывается 0 в массив rx (буфер команд) по позиции b (длина введённой команды). Это есть не что иное, как формирование строки, которая в дальнейшем будет распознаваться строковыми функциями. Как известно, символ с кодом 0 в конце массива символов – признак завершения строки, состоящий из этих символов. Буфер rx в моей программе имеет размер 32. Стало быть, максимальное число символов команды – 31. Далее – обнуляется b и c (позиция курсора в терминале). Если бинарная переменная echo, значением которой можно управлять одноимённой командой, является истинным, то на терминал отправляется перевод на новую строку \r\n. Переменная и команда echo отвечает за режим самоконтроля. В дальнейшем все выводы в терминал при оперировании с командами будут проходить через условие «if(echo)». В последней строке подготавливается бинарная переменная cor на 0 (ложное значение). В дальнейшем она станет истиной только в том случае, если введённая в терминал команда совпадёт с допустимой командой из списка.

if(!strcmp(rx,"echo")){
  printf("Echo is ");
  if(!echo){
      echo=1;
      printf("ON");
  }else{
      echo=0;
      printf("OFF");
  }
  cor=1;
}

Функцией strcmp сравнивается строка, введённая в терминале, со строкой «echo». При совпадении эта функция возвращает 0. Это обработка первой из 14 доступных команд в моей программе. На терминал для удобства выводится контрольное словосочетание «Echo is ON» или «Echo is OFF» и меняется значение переменной echo на противоположное. Флаг cor устанавливается в истинное значение.

if(!strcmp(rx,"zero")){output(0); cor=1;}
if(!strcmp(rx,"one")){output(1); cor=1;}
if(!strcmp(rx,"two")){output(2); cor=1;}
if(!strcmp(rx,"three")){output(3); cor=1;}
if(!strcmp(rx,"four")){output(4); cor=1;}
if(!strcmp(rx,"five")){output(5); cor=1;}
if(!strcmp(rx,"six")){output(6); cor=1;}
if(!strcmp(rx,"seven")){output(7); cor=1;}
if(!strcmp(rx,"eight")){output(8); cor=1;}
if(!strcmp(rx,"nine")){output(9); cor=1;}
if(!strcmp(rx,"ten")){output(10); cor=1;}
if(!strcmp(rx,"eleven")){output(11); cor=1;}
if(!strcmp(rx,"twelve")){output(12); cor=1;}

А здесь по аналогии перечислены сравнения буфера rx с остальными командами, согласно моей вышеизложенной задаче. При совпадении вызывается функция output, выводящая в терминал соответствующее команде число по определённому шаблону. Функция представлена ниже.

void output(unsigned char a){
    printf("  is %d",a);
}

Данную функцию я создал как раз для возможности быстрого редактирования шаблона вывода на этапе написания программы.

if(rx[0] && strcmp(rx,archive[(aw-1)&7])){ //Если строка не пустая и не повторяется;
  strcpy(archive[aw&7],rx); //Маска &7 зацикливает счётчик в пределах 8;
  aw+=1;
  if(atop<8){ //Первоначальный набор архива до 8;
      atop+=1;
  }
}
ar=atop; //Иниц. итератора чтения из архива на максимум;
if(!cor){
  printf("Error");
}
printf("\r\n");

Этот фрагмент кода может сразу показаться непонятным, в том числе даже мне, спустя несколько лет. Здесь мы видим новые переменные. Во-первых, archive[32][8] – массив на 8 строк – для архива команд. Итераторы aw и ar – для позиционирования внутри архива при записи и при чтении соответственно. Начинается всё с условия: если строка не пустая (первый байт буфера rx ненулевой) и если команда не повторяется (если строка rx не совпадает со строкой из архива по предыдущей позиции). Предыдущая позиция в архиве соответствует индексу (aw-1), а текущая – aw. Наложение маски на индекс массива строк (на позицию в архиве) &7 необходимо для создания зацикливания в пределах 8 позиций. Тем самым реализуется кольцевой буфер для архива. Поясню, как это работает. Если бесконечно увеличивать на единицу переменную aw, начиная с нуля, то она не уйдёт в бесконечность, как в математике, а вновь обратится в 0 после прохождения значения 255, так как она типа unsigned char. Но если на переменную наложить маску побитового перемножения 7 (в бинарном виде 00000111), то переменная aw будет обращаться в 0 после 7. То есть: 0, 1, 2, 3, 4, 5, 6, 7, 0, 1,…. Аналогично – в обратную сторону. То есть, образуется зацикливание, а массив архива становится «круглым» с восемью секторами. В следующей строке кода происходит архивирование команды функцией копирования строки strcpy по текущей позиции. В двух строках далее встречается новая переменная – atop. Она необходима для того, чтобы первоначально ограничить размер архива команд, пока он ещё не успел заполниться. Значение данной переменной поднимается до 8 только один раз по мере заполнения архива, после чего оно остаётся равной 8, когда архив работает в режиме перезаписи. Далее – итератор чтения ar инициализируется не нулём, а размером архива, так как архив будет читаться с конца в начало. В дальнейшем такое направление чтения я буду называть чтением «вперёд». В завершении этого фрагмента кода – выводится в терминал слово «Error», если ни одного совпадения введённой команды с допустимым списком не было зафиксировано. При этом такая команда всё равно попадёт в архив. В самой последней строке ещё раз в терминал выводится перевод на новую строку.

Следующий кусок – обработка Backspace.

if(!cmd&&c){
  b-=1;
  c-=1;
  memmove(&rx[c],&rx[c+1],b-c); //Сместить влево символы в буфере;
  if(echo){
      printf("%c%c[K",byte,0x1B);
      if(b-c){
          putpart(&rx[c],b-c); //Вывод остаточной части строки;
          printf("%c[%dD",0x1B,b-c);
      }
      //printf("%c[D%c[s\r%c[K%s%c[u",0x1B,0x1B,0x1B,rx,0x1B); //С запоминанием позиции курсора;
  }
}

Его функционал заключён в условие: если терминал не в процессе приёма УП (cmd==0, или же !cmd) и позиция курсора ненулевая (слева есть, что бэкспейсить). Кстати, насчёт процесса приёма УП. Реальная УП из трёх байт при нажатии соответствующей ей клавиши в терминале передаётся мимолётно. Но если изощриться и вводить УП через нажатие трёх клавиш («Esc», «[» и клавиши аргумента), то можно говорить о процессе приёма УП. Он начинается от нажатия первой клавиши и заканчивается нажатием третей. При стирании символа длина слова уменьшается на один символ, но также и сдвигается курсор влево на одну позицию. Для этого переменные b и c уменьшаются на 1. Нужно помнить, что стирать символы слева клавишей Backspace может понадобиться не только в конце строки, но и в середине, предварительно отодвигая курсор влево соответствующей клавишей. И в последнем случае при стирании символа из середины строки необходимо также на 1 позицию сдвигать часть строки, стоящую справа. Этим занимается функция memmove. В частности, если стирание символа происходит не в середине, а в конце (при равенстве b и c), то memmove сама по себе не будет работать из-за третьего нулевого аргумента (и действительно, нам ничего при этом сдвигать не нужно). Затем, в следующих строках кода, в режиме самоконтроля возвращается на терминал символ Backspace из переменной byte, что приводит к сдвигу курсора влево на одну позицию. А вот для того, чтобы по факту убрать в этом месте удаляемый символ, приходится добавочно отправлять УП очистки содержимого справа от курсора (ESC+[K). Здесь есть одно замечание. Если терминал не поддерживает такую УП (как в Proteus, к примеру), то при нажатии Backspace на конце введённой строки мы увидим не удаление, а замену удаляемого символа на сочетание «[K», что сразу может показаться непривычным и неудобным. Если отказаться от этой УП, то в HyperTerminal не будет фактического стирания символа, а будет только передвижение курсора. Я решил не отказываться и оставил первый вариант – в пользу выбора HyperTerminal. Возможно, в дальнейшем, я с помощью своей какой-либо УП (например, «Esc+Backspace») буду переключать режим Backspace, подстраиваясь под тот или иной терминал. Далее по коду следует новое условие: если (b-c) – истина, или же, если backspace происходит не на конце строки. Внутри условия две функции. Первая функция посимвольно выводит часть строки – ту самую часть, что находится справа от стираемого символа. И пользователь на терминале при этом видит, что символ стёрся, как ни в чём не бывало, будто в текстовом редакторе. Функция эта простая, её код будет приведён ниже. Вторая функция в условии отвечает за возвращение курсора на место редактирования. Стоит не забывать, что при выводе символов в терминал курсор продвигается вперёд автоматически. Возвращение курсора на прежнюю позицию достигается с помощью 4-байтовой УП сдвига курсора влево «ESC+[nD», где вместо n – число позиций сдвига. По умолчанию, при отсутствии этого аргумента (исходящая УП при нажатии на кнопку «Влево»), как известно, курсор сдвигается только на одну позицию. А в нашем случае сдвигать надо на длину выводимой перед этим части строки – на число (b-c). И вот эта самая УП некорректно работает (курсор уходит вперёд) при равенстве b и c, когда аргумент принимает нулевое значение. Хотя логически такая УП не должна себя никак проявлять. Эта некорректность будет проявляться в HyperTerminal. Для этого и заключена эта функция в условие, о котором я написал выше. А заодно с ней – putpart. Последняя закомментированная строчка кода – длинный printf – другой способ вывода результата backspace, идею которого я позаимствовал у того же автора. Там применены специальные УП, которые запоминают и восстанавливают позицию курсора. Цепочка операций такая: запомнить позицию курсора, вернуть его в начало строки, очистить строку, вывести получившуюся строку команды целиком (заранее ещё нужно поставить ноль на конец строки, я это убрал из кода), восстановить позицию курсора. По трафику данный способ проигрывает, но немножко выигрывает по объёму памяти МК. Но я всё равно от него отказался, второй способ мне больше нравится с логической стороны.

Функция putpart имеет следующий простой код. Данная функция, как и функция memmove, толерантна к отсутствию нулевого символа в конце строки – работает как с массивом.

void putpart(unsigned char* s, unsigned char n){
    unsigned char i;
    for(i=0;i<n;i++){
        printf("%c",s[i]);
    }
}

Перейдём к следующему кейсу – накопление символов команды. В нём не только накопление, но и функции обработки строки на случай ввода символов в её середине или начале. По аналогии с Backspace при вводе символов не в конце строки необходимо также делать сдвиг правой части строки в буфере, но только не влево, а вправо. Все операции похожи.

if(b<31){ //Ограничение;
  b+=1;
  c+=1;
  memmove(&rx[c],&rx[c-1],b-c); //Сместить вправо символы в буфере;
  rx[c-1]=byte; //Накопление в буфер;
    if(echo){
    printf("%c",byte);
    if(b-c){
      printf("%c[K",0x1B); //Очистка строки справа от курсора;
      putpart(&rx[c],b-c); //Вывод остаточной части строки;
      printf("%c[%dD",0x1B,b-c); //Возвращение курсора назад;
    }
  }
}

Первая строчка кода – условие: если длина команды не превосходит 31. В это условие заключено всё остальное содержимое кейса. Оно нужно для ограничения длины вводимой команды. Переменные b и c увеличиваются на 1, так как при вводе символов длина строки растёт и курсор сдвигается вправо. Функция memmove сдвигает правую часть строки (от курсора) вправо на одну позицию, а в освободившееся место, в следующей строке кода, записывается принятый с терминала символ byte. В режиме самоконтроля выводится вводимый символ на терминал, затем идёт обработка на случай, если символ вводится не в конце строки. В отличие от Backspace, здесь все три функции заключены под условие «if(b-c)», так как очистка строки справа от курсора не требуется, если ввод происходит в конце строки в обычном режиме. На терминале, не поддерживающий УП, эта УП была бы лишней помехой при вводе команды. На последней строке стоял закомментированный длинный printf для второго способа обработки по аналогии с Backspace, я его убрал из кода.

Программа включает в себя функцию распознавания УП, исходящие из HyperTerminal при нажатии кнопок F1-F4, хотя в моей задаче эти кнопки не используются. Вместо этого при их нажатии в терминал будет выдаваться информация с названием нажатой кнопки. На этапе написания программы я их использовал для вывода отладочных сообщений. Ниже приведён код для F1.

printf("\r%c[KF1 key",0x1B);

В терминал выводится символ возврата каретки (курсор в начало), УП очистки строки справа от курсора и сообщение «F1 key». Для кнопок F2, F3, F3 код аналогичен. Зачем вообще нужно посылать УП очистки строки справа от курсора, если строка в терминал и так выводится поверх старого содержимого? Это нужно на тот случай, если выводимая строка по числу символов короче старого содержимого. Поэтому в общем случае я всегда использую УП очистки старой строки.

Осталось разобрать ещё 6 кейсов. Одни из простых операций, почти как для кнопок F1-F4, – обработка УП для кнопок Home и End.

if(c){
  c=0;
  printf("\r");
}

При нажатии на кнопку Home переменная позиции курсора обнуляется, становясь на начало, а на терминал выводится символ возврата каретки, чтобы курсор визуально стал в начальную позицию. Эти две операции заключены в условие: если курсор не в начальной позиции. Это я сделал, чтобы исключить их ненужное выполнение при повторном нажатии кнопки. Аналогично – для кнопки End.

if(b-c){
  printf("%c[%dC",0x1B,b-c);
  c=b;
}

Здесь применяется операция сдвига курсора вправо на число позиций (b-c), которая уже встречалась ранее. После этого переменная позиции курсора принимает значение длины текущей строки. Обе операции заключены в условие: если курсор не в конце строки. Но в данном случае это сделано не столько для исключения их выполнения лишний раз, сколько для предотвращения некорректной работы УП при нулевом аргументе, о чём уже писалось выше.

Обработка кнопки «Вверх» листания архива имеет следующий код.

if(ar>0){
  ar-=1;
  strcpy(rx,archive[(aw+ar-atop)&7]);
  b=strlen(rx);
  c=b;
  if(echo){
    printf("\r%c[K%s",0x1B,rx);
  }
}

На код навешено условие: если ar>0, или же, если не конец архива. Как только архив долистается до своего потолка, кнопка перестанет реагировать. А так, с каждым нажатием переменная ar (индекс чтения из архива) уменьшается на 1. Следующая строка, пожалуй, самая сложная. Это копирование из архива в буфер команд (восстановление из архива). Пришлось хорошо подумать, чтобы составить комбинацию «(aw+ar-atop)&7». Про назначение маски &7 я уже писал. А комбинация из трёх слагаемых позволяет корректно попасть в нужную ячейку архива в зависимости от текущего значения индекса записи aw. А также, на начальных порах формирования архива, – в зависимости от значения переменной atop (про неё я тоже выше писал). Желающие могут проверить на различных примерах, как работает данная арифметика: в уме, на листке бумаги или в Excel. Переменная b формируется, как длина скопированной из архива строки. Позиция курсора остаётся в конце строки. В режиме самоконтроля на терминал выводится следующая информация. Во-первых – возврат курсора в начало, затем – очистка старой строки и вывод строки из архива.

Код обработки кнопки «Вниз» аналогичный, но не совсем.

if(ar<atop){
  ar+=1;
  if(ar!=atop){
    strcpy(rx,archive[(aw+ar-atop)&7]);
  }else{
    rx[0]=0;
  }
  b=strlen(rx);
  c=b;
  if(echo){
    printf("\r%c[K%s",0x1B,rx);
  }
}

Также, весь код заключён в условие: если не начало архива. Это нужно для блокировки кнопки на противоположной границе архива. Но здесь есть ещё одна особенность, которой, например, нет в командной строке Windows – выход их архива. Это значит, что когда мы, листая архив, достигли крайней команды, введённой недавно, очередное нажатие кнопки «Вниз» очистит строку и буфер команд. Это произойдёт, когда переменная ar сравняется с переменной atop. При полностью заполненном архиве atop имеет значение 8, а переменная ar перебирает значения от 0 до 7, ссылаясь на ячейки архива. Переменной b в общем случае присваивается длина строки буфера команд, который только что сформировался. Когда нулевому индексу буфера присваивается ноль, это означает очистку строки, давая строковым функциям знать, что строка пустая (имеет нулевую длину). Далее строки кода аналогичны, как для клавиши «Вверх».

Код обработки кнопки «Влево» довольно простой.

if(c>0){
  c-=1;
  if(echo){
    printf("%c[D",0x1B);
  }
}

Переменная позиции курсора уменьшается на 1. В режиме самоконтроля на терминал выводится та же УП, соответствующая этой кнопке, что визуально продвигает курсор влево на одну позицию. Всё это заключено в условие: если не достигнуто начало строки.

Код обработки кнопки «Вправо», отнюдь, довольно масштабный. Дело в том, что данная кнопка, как уже отмечалось, имеет двойную функцию. Внутри отображаемой строки она возвращает курсор вправо на одну позицию, но выходя за пределы строки – дописывает символы последней обработанной команды, которые соответствуют текущей позиции курсора (если длина этой команды была больше длины текущей строки). Точно так же ведёт себя эта кнопка в командной строке Windows.

if(c<b){
  c+=1;
  if(echo){
    printf("%c[C",0x1B);
  }
}else{
  if(atop){
    if(ar==atop && c<strlen(archive[(aw-1)&7])){
      rx[c]=archive[(aw-1)&7][c];
      if(echo){
        printf("%c",rx[c]);
      }
      c+=1;
      b=c;
    }
    if(ar!=atop && c<strlen(archive[(aw+ar-atop)&7])){
      rx[c]=archive[(aw+ar-atop)&7][c];
      if(echo){
        printf("%c",archive[(aw+ar-atop)&7][c]);
      }
      c+=1;
      b=c;
    }
  }
}

Как раз, первая строка команды – условие, которое разделяет эти две функции кнопки. При истинном значении условия (когда курсор находится внутри строки) код по смыслу аналогичен коду для клавиши «Влево». Рассуждения по сути также аналогичные. А при ложном значении условия (когда курсор выходит за пределы строки) встречаются ещё два сложных условия, заключённые в условие «if(atop)». Последнее условие есть не что иное, как «если в архиве что-то есть». А два сложных условия внутри определяют, находимся лм мы в архиве или вне него. Если мы находимся в архиве, посимвольному (по текущему положению курсора) выводу подвергается строка из текущей ячейки архива (aw+ar-atop)&7. Если вне архива, то – из последней (недавней) позиции архива (aw-1)&7. Также туда навешены ограничения на переменную позиции курсора, чтобы не заходить за длину архивной строки. Внутри условий происходит посимвольное заполнение буфера команды символами из архива, продвижение переменных b и c, а также, в режиме самоконтроля – вывод символов на терминал.

В заключение описания кода обнаружилась ещё одна интересная вещь. Как уже отмечалось выше, нажатие клавиши «Delete» в HyperTerminal, к сожалению, не даёт никакой реакции на выходе. Её наличие было бы очень удобным. Обычно функция данной клавиши – удаление символов справа от курсора, в то время как «Backspace» – слева от курсора. Такой управляющий символ, как я где-то встречал, имеет код 0x7F. Но такой код, как я выяснил экспериментально, HyperTerminal посылает при нажатии комбинации «Ctrl + Backspace». Вообще говоря, HyperTerminal посылает много чего интересного, если удерживать Ctrl и нажимать различные кнопки буквенных символов. В эти подробности я не вдавался. Но на этапе написания статьи я придумал реализовать обработку комбинации «Ctrl + Backspace», которая будет действовать не как Delete, а как она действует в других текстовых редакторах: удаляет часть слова от курсора слева до ближайшего пробела. Или, в частности – всё слово целиком, если курсор стоит в конце слова. Этим я сам часто пользуюсь, когда что-то печатаю и допускаю несколько опечаток в одном слове. Но в терминале такая комбинация будет удалять не слово, а целиком всё содержимое от курсора слева, тем более что пробелы в командах используются редко. Конечно же, функция с такой реализацией в другом терминале, где поддерживается клавиша Delete с кодом 0x7F, будет некорректно работать. Но я всё делаю в пользу HyperTerminal. Возможно, в настройках HyperTerminal можно поменять некие параметры, и клавиша Delete заработает. Но я пока не смотрел эти вещи. На блок-схеме, кто успел заметить, уже нарисован прямоугольник для обработки вышеупомянутой комбинации (подрисовал позже). Также – в листинге кода обработки УП уже имеется соответствующий кейс. Вернее говоря, это не только код обработки УП, но и управляющих символов. Код для обработки комбинации клавиш «Ctrl + Backspace» описания, я думаю, не требует. Рассуждения аналогичны вышеприведённым.

if(!cmd&&c){
  memmove(&rx[0],&rx[c],b-c); //Сместить влево символы в буфере;
  b-=c;
  c=0;
  if(echo){
    printf("\r%c[K",0x1B);
    if(b-c){ //Возвращение курсора назад;
      putpart(&rx[0],b); //Вывод остаточной части строки;
      printf("\r"); //И опять курсор в начало;
    }
  }
}

И ещё одно интересное замечание. Я пока не разбирался, может, кто подскажет в комментариях. В рабочем текстовом поле HyperTerminal есть две зоны: белая (активная) и серая, что расположена выше. В неё уходит содержимое терминала, как в историю, в процессе заполнения рабочего поля, и даже остаётся некая часть содержимого после перезапуска HyperTerminal. Но на этапе тестирования своей программы я обнаружил, что в эту серую зону переносятся строчки, которые подвергаются УП очистки справа от курсора. То есть, содержимое справа от курсора не исчезает бесследно. Вот такая особенность.

Полный код программы приведён ниже.

Основной файл проекта.
/*****************************************************
This program was produced by the
CodeWizardAVR V2.05.3 Standard
Automatic Program Generator
© Copyright 1998-2011 Pavel Haiduc, HP InfoTech s.r.l.
http://www.hpinfotech.com

Project : 
Version : 
Date    : 27.11.2023
Author  : PerTic@n
Company : If You Like This Software,Buy It
Comments: 


Chip type               : ATmega32
Program type            : Application
AVR Core Clock frequency: 8.000000 MHz
Memory model            : Small
External RAM size       : 0
Data Stack size         : 512
*****************************************************/

#include <mega32.h>
#include <string.h>

#ifndef RXB8
#define RXB8 1
#endif

#ifndef TXB8
#define TXB8 0
#endif

#ifndef UPE
#define UPE 2
#endif

#ifndef DOR
#define DOR 3
#endif

#ifndef FE
#define FE 4
#endif

#ifndef UDRE
#define UDRE 5
#endif

#ifndef RXC
#define RXC 7
#endif

#define FRAMING_ERROR (1<<FE)
#define PARITY_ERROR (1<<UPE)
#define DATA_OVERRUN (1<<DOR)
#define DATA_REGISTER_EMPTY (1<<UDRE)
#define RX_COMPLETE (1<<RXC)

// USART Receiver buffer
#define RX_BUFFER_SIZE 8
char rx_buffer[RX_BUFFER_SIZE];

#if RX_BUFFER_SIZE <= 256
unsigned char rx_wr_index,rx_rd_index,rx_counter;
#else
unsigned int rx_wr_index,rx_rd_index,rx_counter;
#endif

// This flag is set on USART Receiver buffer overflow
bit rx_buffer_overflow;

// USART Receiver interrupt service routine
interrupt [USART_RXC] void usart_rx_isr(void)
{
char status,data;
status=UCSRA;
data=UDR;
if ((status & (FRAMING_ERROR | PARITY_ERROR | DATA_OVERRUN))==0)
   {
   rx_buffer[rx_wr_index++]=data;
#if RX_BUFFER_SIZE == 256
   // special case for receiver buffer size=256
   if (++rx_counter == 0) rx_buffer_overflow=1;
#else
   if (rx_wr_index == RX_BUFFER_SIZE) rx_wr_index=0;
   if (++rx_counter == RX_BUFFER_SIZE)
      {
      rx_counter=0;
      rx_buffer_overflow=1;
      }
#endif
   }
}

#ifndef _DEBUG_TERMINAL_IO_
// Get a character from the USART Receiver buffer
#define _ALTERNATE_GETCHAR_
#pragma used+
char getchar(void)
{
char data;
while (rx_counter==0);
data=rx_buffer[rx_rd_index++];
#if RX_BUFFER_SIZE != 256
if (rx_rd_index == RX_BUFFER_SIZE) rx_rd_index=0;
#endif
#asm("cli")
--rx_counter;
#asm("sei")
return data;
}
#pragma used-
#endif

// Standard Input/Output functions
#include <stdio.h>

// Declare your global variables here

void output(unsigned char a){
    printf("  is %d",a);
}

void putpart(unsigned char* s, unsigned char n){
    unsigned char i;
    for(i=0;i<n;i++){
        printf("%c",s[i]);
    }
}

void main(void){
// Declare your local variables here
#include "variables.c"
// Input/Output Ports initialization
// Port A initialization
// Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In 
// State7=T State6=T State5=T State4=T State3=T State2=T State1=T State0=T 
PORTA=0x00;
DDRA=0x00;

// Port B initialization
// Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In 
// State7=T State6=T State5=T State4=T State3=T State2=T State1=T State0=T 
PORTB=0x00;
DDRB=0x00;

// Port C initialization
// Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In 
// State7=T State6=T State5=T State4=T State3=T State2=T State1=T State0=T 
PORTC=0x00;
DDRC=0x00;

// Port D initialization
// Func7=In Func6=In Func5=In Func4=In Func3=In Func2=In Func1=In Func0=In 
// State7=T State6=T State5=T State4=T State3=T State2=T State1=T State0=T 
PORTD=0x00;
DDRD=0x00;

// Timer/Counter 0 initialization
// Clock source: System Clock
// Clock value: Timer 0 Stopped
// Mode: Normal top=0xFF
// OC0 output: Disconnected
TCCR0=0x00;
TCNT0=0x00;
OCR0=0x00;

// Timer/Counter 1 initialization
// Clock source: System Clock
// Clock value: Timer1 Stopped
// Mode: Normal top=0xFFFF
// OC1A output: Discon.
// OC1B output: Discon.
// Noise Canceler: Off
// Input Capture on Falling Edge
// Timer1 Overflow Interrupt: Off
// Input Capture Interrupt: Off
// Compare A Match Interrupt: Off
// Compare B Match Interrupt: Off
TCCR1A=0x00;
TCCR1B=0x00;
TCNT1H=0x00;
TCNT1L=0x00;
ICR1H=0x00;
ICR1L=0x00;
OCR1AH=0x00;
OCR1AL=0x00;
OCR1BH=0x00;
OCR1BL=0x00;

// Timer/Counter 2 initialization
// Clock source: System Clock
// Clock value: Timer2 Stopped
// Mode: Normal top=0xFF
// OC2 output: Disconnected
ASSR=0x00;
TCCR2=0x00;
TCNT2=0x00;
OCR2=0x00;

// External Interrupt(s) initialization
// INT0: Off
// INT1: Off
// INT2: Off
MCUCR=0x00;
MCUCSR=0x00;

// Timer(s)/Counter(s) Interrupt(s) initialization
TIMSK=0x00;

// USART initialization
// Communication Parameters: 8 Data, 1 Stop, No Parity
// USART Receiver: On
// USART Transmitter: On
// USART Mode: Asynchronous
// USART Baud Rate: 9600
UCSRA=0x00;
UCSRB=0x98;
UCSRC=0x86;
UBRRH=0x00;
UBRRL=0x33;

// Analog Comparator initialization
// Analog Comparator: Off
// Analog Comparator Input Capture by Timer/Counter 1: Off
ACSR=0x80;
SFIOR=0x00;

// ADC initialization
// ADC disabled
ADCSRA=0x00;

// SPI initialization
// SPI disabled
SPCR=0x00;

// TWI initialization
// TWI disabled
TWCR=0x00;

// Global enable interrupts
#asm("sei")

while(1){
    if(rx_counter){
        #include "uart.c"
    }
}
}

Файл с объявлением переменных "variables.c".
unsigned char rx[32]; //Буфер UART для операций с терминалом;
unsigned char byte; //Принимаемый байт;
unsigned char b=0; //Номер принимаемого байта по UART;
unsigned char c=0; //Позиция курсора при редактировании;
bit cor=0; //Флаг корректной команды;
unsigned char cmd=0; //Флаг УП;
unsigned char archive[8][32];
unsigned char ar=0,aw=0,atop=0;
bit echo=1; //Эхо вкл./выкл.

Файл обработки UART "uart.c".
byte=getchar();
switch(byte){
    case 0x08: //Если Backspace;
        if(!cmd&&c){
            b-=1;
            c-=1;
            memmove(&rx[c],&rx[c+1],b-c); //Сместить влево символы в буфере;
            //rx[b]=0; //Поставить конец строки;
            if(echo){
                printf("%c%c[K",byte,0x1B);
                //Тут два способа; второй оптимальнее по размеру программы, а первый - по трафику;
                if(b-c){ //Возвращение курсора назад;
                    putpart(&rx[c],b-c); //Вывод остаточной части строки;
                    printf("%c[%dD",0x1B,b-c);
                }
                //printf("%c[D%c[s\r%c[K%s%c[u",0x1B,0x1B,0x1B,rx,0x1B); //С запоминанием позиции курсора;
            }
        }
    break;
    case 0x7F: //Если Ctrl+Backspace (в других терминалах это может быть Delete);
        if(!cmd&&c){
            memmove(&rx[0],&rx[c],b-c); //Сместить влево символы в буфере;
            b-=c;
            c=0;
            if(echo){
                printf("\r%c[K",0x1B);
                if(b-c){ //Возвращение курсора назад;
                    putpart(&rx[0],b); //Вывод остаточной части строки;
                    printf("\r"); //И опять курсор в начало;
                }
            }
        }
    break;
    case 0x0D:
    case 0x0A:
        rx_counter=0;
        rx_wr_index=0;
        rx_rd_index=0;
        rx[b]=0;
        b=0;
        c=0;
        if(echo){
            printf("\r\n");
        }
        cor=0;
        if(!strcmp(rx,"echo")){
            printf("Echo is ");
            if(!echo){
                echo=1;
                printf("ON");
            }else{
                echo=0;
                printf("OFF");
            }
            cor=1;
        }
        if(!strcmp(rx,"zero")){
            output(0);
            cor=1;
        }
        if(!strcmp(rx,"one")){
            output(1);
            cor=1;
        }
        if(!strcmp(rx,"two")){
            output(2);
            cor=1;
        }
        if(!strcmp(rx,"three")){
            output(3);
            cor=1;
        }
        if(!strcmp(rx,"four")){
            output(4);
            cor=1;
        }
        if(!strcmp(rx,"five")){
            output(5);
            cor=1;
        }
        if(!strcmp(rx,"six")){
            output(6);
            cor=1;
        }
        if(!strcmp(rx,"seven")){
            output(7);
            cor=1;
        }
        if(!strcmp(rx,"eight")){
            output(8);
            cor=1;
        }
        if(!strcmp(rx,"nine")){
            output(9);
            cor=1;
        }
        if(!strcmp(rx,"ten")){
            output(10);
            cor=1;
        }
        if(!strcmp(rx,"eleven")){
            output(11);
            cor=1;
        }
        if(!strcmp(rx,"twelve")){
            output(12);
            cor=1;
        }
        if(rx[0] && strcmp(rx,archive[(aw-1)&7])){ //Если строка не пустая и не повторяется;
            strcpy(archive[aw&7],rx); //Маска &7 зацикливает счётчик в пределах 8;
            aw+=1;
            if(atop<8){ //Первоначальный набор архива до 8;
                atop+=1;
            }
        }
        ar=atop; //Иниц. итератора чтения из архива на максимум;
        if(!cor){
            printf("Error");
        }
        printf("\r\n");
        cmd=0;
    break;
    case 0x1B: //ESC;
        cmd=1; //Переход на статус команды в ожидании "[" или "O";
    break;
    case 0x9B: //Однобайтовый аналог CSI (ESC+[);
        cmd=2; //Переход на статус ожидания аргумента;
    break;
    default: //Остальные символы;
        switch(cmd){ //В зависимости от статуса; 
            case 0: //Если не УП;
                if(b<31){ //Ограничение;
                    b+=1;
                    c+=1;
                    memmove(&rx[c],&rx[c-1],b-c); //Сместить вправо символы в буфере;
                    rx[c-1]=byte; //Накопление в буфер;
                    //rx[b]=0; //Поставить конец строки;
                    if(echo){
                        //Вывод куска строки;
                        printf("%c",byte);
                        if(b-c){
                            printf("%c[K",0x1B); //Очистка строки справа от курсора;
                            putpart(&rx[c],b-c); //Вывод остаточной части строки;
                            printf("%c[%dD",0x1B,b-c); //Возвращение курсора назад;
                        }
                    }
                }
            break;
            case 1: //Символ после ESC; 
                switch(byte){
                    case 0x5B: //Если второй символ "[";
                        cmd=2; //Подтверждение CSI и переход на статус ожидания аргумента; 
                    break;
                    case 0x4F: //Если второй символ "O";
                        cmd=3; //Подтверждение F1-F4 и переход на статус ожидания аргумента; 
                    break;
                }
            break;
            case 2: //Аргумент CSI;
                switch(byte){
                    case 0x41: //Кнопка ВВЕРХ;
                        if(ar>0){
                            ar-=1;
                            strcpy(rx,archive[(aw+ar-atop)&7]);
                            b=strlen(rx);
                            c=b;
                            if(echo){
                                printf("\r%c[K%s",0x1B,rx);
                            }
                        }
                    break;
                    case 0x42: //Кнопка ВНИЗ;
                        if(ar<atop){
                            ar+=1;
                            if(ar!=atop){
                                strcpy(rx,archive[(aw+ar-atop)&7]);
                            }else{
                                rx[0]=0;
                            }
                            b=strlen(rx);
                            c=b;
                            if(echo){
                                printf("\r%c[K%s",0x1B,rx);
                            }
                        }
                    break;
                    case 0x43: //Кнопка ВПРАВО;
                        if(c<b){
                            c+=1;
                            if(echo){
                                printf("%c[C",0x1B);
                            }
                        }else{
                            if(atop){
                                if(ar==atop && c<strlen(archive[(aw-1)&7])){
                                    rx[c]=archive[(aw-1)&7][c];
                                    if(echo){
                                        printf("%c",rx[c]);
                                    }
                                    c+=1;
                                    b=c;
                                }
                                if(ar!=atop && c<strlen(archive[(aw+ar-atop)&7])){
                                    rx[c]=archive[(aw+ar-atop)&7][c];
                                    if(echo){
                                        printf("%c",archive[(aw+ar-atop)&7][c]);
                                    }
                                    c+=1;
                                    b=c;
                                }
                            }
                        }
                    break;
                    case 0x44: //Кнопка ВЛЕВО;
                        if(c>0){
                            c-=1;
                            if(echo){
                                printf("%c[D",0x1B);
                            }
                        }
                    break;
                    case 0x48: //Кнопка Home;
                        //printf("Home ");
                        if(c){
                            c=0;
                            printf("\r"); //Курсор в начало;
                        }
                    break;
                    case 0x4B: //Кнопка End;
                        //printf("End ");
                        if(b-c){ //Курсор в конец;
                            printf("%c[%dC",0x1B,b-c);
                            c=b;
                        }
                    break;
                }
                cmd=0; //Выход из статуса команды;
            break; 
            case 3: //Аргумент F1-F4;
                switch(byte){
                    case 0x50:
                        printf("\r%c[KF1 key",0x1B);
                        //putpart(&rx[2],3);
                        //printf("%c[%dD",0x1B,2);
                    break;
                    case 0x51:
                        printf("\r%c[KF2 key",0x1B);
                        //printf("%c[%cG",0x1B,2);
                    break;
                    case 0x52:
                        printf("\r%c[KF3 key",0x1B);
                    break;
                    case 0x53:
                        printf("\r%c[KF4 key",0x1B);
                    break;
                }
                cmd=0; //Выход из статуса команды;
            break; 
        }
    break;
}

Теперь пришла пора, перейти к тестированию программы. Тестирование я буду производить в симуляторе Proteus (ISIS). Электрическая схема довольно простая: МК, переключатель терминалов, встроенный виртуальный терминал и внешний COM-порт. Последний подключен к HyperTerminal через виртуальный COM-COM кабель, в роли которого используется программа N8VBvCOM.

Комплекс программ для симуляции проекта
Комплекс программ для симуляции проекта

Тестирование заключается в демонстрации всевозможных вариантов и приёмов ввода и редактирования команд. Пожалуй, я сделаю отдельное видео с этим процессом.

Комментарии (5)


  1. aabzel
    16.12.2023 00:37

    Замечательно! CLI на микроконтроллере. Это как раз то, что надо!
    https://habr.com/ru/articles/694408/


  1. AndyKorg
    16.12.2023 00:37

    Есть например такой проект https://github.com/antirez/linenoise


  1. VT100
    16.12.2023 00:37

    Также - реализация терминала VT100 в работах @Indemsys.


  1. slog2
    16.12.2023 00:37

    Какой объём памяти это всё занимает?


    1. R3EQ Автор
      16.12.2023 00:37

      Program size: 1874 words (3748 bytes), 11.4% of FLASH