В каждом разрабатываемом устройстве у меня присутствовал отладочный вывод в UART, как в самый распространённый и простой интерфейс.
И каждый раз, рано или поздно, мне хотелось помимо пассивного вывода сделать ввод команд через тот же UART. Обычно это происходило когда мне хотелось для отладки выводить какой-нибудь очень большой объём информации по запросу (например состояние NANDFLASH, при разработке собственной файловой системы). А иногда хотелось программно управлять ножками GPIO, чтобы отрепетировать работу с какой-нибудь переферией на плате.
Так или иначе мне был необходим CLI, который позволяет обрабатывать разные команды. Если кто-то натыкался на уже готовый инструмент для этих целей — буду благодарен за ссылку в комментариях. А пока я написал собствыенный.
Требования, в порядке уменьшения важности:
- Язык С. Я пока не готов писать ПО для микроконтроллеров на чём-либо другом, хотя ситуация может и измениться.
- Приём и обработка строк из UART. Для простоты все строки оканчиваются 'n'.
- Возможность передавать в команду параметры. Набор параметров различается для разных команд.
- Легкость добавления новых команд.
- Возможность добавления новых команд в разных исходных файлах. Т.е. начиная реализовывать очередной функционал в файле "new_feature.c" я не трогаю исходники CLI, а добавляю новые команды в том же файле "new_feature.c".
- Минимум используемых ресурсов (RAM, ROM, CPU).
Не буду подробно описывать драйвер UART сохраняющий принятые символы в статический буфер, отбрасывающий пробелы в начале строки и ждущий символа перевода строки.
Начнём с более интересного — у нас есть строка, оканичающаяся 'n'. Теперь надо найти соответсвтующую ей команду и выполнить.
Решение в виде
typedef void (*cmd_callback_ptr)(const char*);
typedef struct
{
const char *cmd_name;
cmd_callback_ptr callback;
}command_definition;
и поиске в множестве зарегистрированных команд команды с искомым именем напрашивается. Только вот загвоздка — как реализовать этот поиск? Или, точнее, как составить это самое множество?
Если бы дело было в C++ самым очевидным решением было бы использование std::map<char*, cmd_callback_ptr> и поиск в нём (неважно уже как). Тогда процесс регистрации команды сводился бы к добавлению в словарь указателя на функцию-обработчик. Но я пишу на C, и переходить на C++ пока не хочу.
Следующая идея — глобальный массив command_definition registered_commands[] = {...}, но этот путь нарушает требование добавления команд из разных файлов.
Заводить массив «побольше» и добавлять команды функцией вроде
#define MAX_COMMANDS 100
command_definition registered_commands[MAX_COMMANDS];
void add_command(const char *name, cmd_callback_ptr callback)
{
static size_t commands_count = 0;
if (commands_count == MAX_COMMANDS)
return;
registered_command[commands_count].cmd_name = name;
registered_command[commands_count].callback = callback;
commands_count++;
}
тоже не хочется, т.к. придётся либо постоянно подправлять константу MAX_COMMANDS, либо зря расходовать память… Вообщем некрасиво как-то :-)
Делать всё тоже самое с помощью динамического выделения памяти и увеличения выделенного массива с помощью realloc на каждом добавлении — наверное неплохой выход, но не хотелось связываться с динамической памятью вообще (нигде больше она в проекте не используется, а кода в ROM занимает много, да и RAM не резиновый).
В итоге я пришёл к следующему любопытному, но, к сожалению, не самому портабельному решению:
#define REGISTER_COMMAND(name, func) const command_definition handler_##name __attribute__ ((section ("CONSOLE_COMMANDS"))) =
{
.cmd_name = name,
.callback = func
}
extern const command_definition *start_CONSOLE_COMMANDS; //предоставленный линкером символ начала секции CONSOLE_COMMANDS
extern const command_definition *stop_CONSOLE_COMMANDS; //предоставленный линкером символ конца секции CONSOLE_COMMANDS
command_definition *findCommand(const char *name)
{
for (command_definition *cur_cmd = start_CONSOLE_COMMANDS; cur_cmd < stop_CONSOLE_COMMANDS; cur_cmd++)
{
if (strcmp(name, cur_cmd->cmd_name) == 0)
{
return cur_cmd;
}
}
return NULL;
}
Вся магия здесь заключена макросе REGISTER_COMMAND, который создаёт глобальные переменные так, что при исполнении кода они будут идти в памяти строго друг за другом. А опирается эта магия на атрибут section, который указывает линкеру, что эту переменную надо положить в отдельную секцию памяти. Таким образом на выходе мы получаем нечто очень похожее на массив registered_commands из предыдущего примера, но не требующей заранее знать сколько в нём будет элементов. А указатели на начало и конец этого массива нам предоставляет линкер.
Подведём итоги, выпишем плюсы и минусы данного решения:
Плюсы:
- Возможность плодить команды пока не кончится память.
- Проверка уникальности имён команд на этапу сборки. Неуникальные команды приведут к созданию двух переменных с одним и тем же именем, что будет диагностировано линкером как ошибка.
- Возможность объявлять команды в любой единице трансляции, не меняя остальные.
- Отсутствие зависимостей от каких-либо внешних библиотек.
- Отсутсвие необходимости в специальной run-time инициализации (регистрация команд и т.д.).
- Отсутсвтие накладных расходов по памяти. Весь массив команд может размещаться в ROM.
Минусы:
- Опирается на конкретный toolchain. Для других придётся править создание команды и, возможно, линкерный скрипт.
- Реализуется не на всех архитектурах, т.к. опирается на структуру бинарного формата исполняемого файла. (см. атрибуты переменных в gcc)
- Линейный поиск по зарегистрированным командам, т.к. массив неотсортирован.
Последний минус можно побороть ценой последнего плюса — можно разместить команды в RAM, после чего отсортировать. Или даже заранее посчитать hash-функцию какую-нибудь чтобы сравнивать не через strcmp.
Автор: Corviniol