В каждом разрабатываемом устройстве у меня присутствовал отладочный вывод в UART, как в самый распространённый и простой интерфейс.
И каждый раз, рано или поздно, мне хотелось помимо пассивного вывода сделать ввод команд через тот же UART. Обычно это происходило когда мне хотелось для отладки выводить какой-нибудь очень большой объём информации по запросу (например состояние NANDFLASH, при разработке собственной файловой системы). А иногда хотелось программно управлять ножками GPIO, чтобы отрепетировать работу с какой-нибудь переферией на плате.
Так или иначе мне был необходим CLI, который позволяет обрабатывать разные команды. Если кто-то натыкался на уже готовый инструмент для этих целей — буду благодарен за ссылку в комментариях. А пока я написал собствыенный.
Требования, в порядке уменьшения важности:
Не буду подробно описывать драйвер UART сохраняющий принятые символы в статический буфер, отбрасывающий пробелы в начале строки и ждущий символа перевода строки.
Начнём с более интересного — у нас есть строка, оканичающаяся '\n'. Теперь надо найти соответсвтующую ей команду и выполнить.
Решение в виде
и поиске в множестве зарегистрированных команд команды с искомым именем напрашивается. Только вот загвоздка — как реализовать этот поиск? Или, точнее, как составить это самое множество?
Если бы дело было в C++ самым очевидным решением было бы использование std::map<char*, cmd_callback_ptr> и поиск в нём (неважно уже как). Тогда процесс регистрации команды сводился бы к добавлению в словарь указателя на функцию-обработчик. Но я пишу на C, и переходить на C++ пока не хочу.
Следующая идея — глобальный массив command_definition registered_commands[] = {...}, но этот путь нарушает требование добавления команд из разных файлов.
Заводить массив «побольше» и добавлять команды функцией вроде
Делать всё тоже самое с помощью динамического выделения памяти и увеличения выделенного массива с помощью realloc на каждом добавлении — наверное неплохой выход, но не хотелось связываться с динамической памятью вообще (нигде больше она в проекте не используется, а кода в ROM занимает много, да и RAM не резиновый).
В итоге я пришёл к следующему любопытному, но, к сожалению, не самому портабельному решению:
Подведём итоги, выпишем плюсы и минусы данного решения:
Плюсы:
Минусы:
Последний минус можно побороть ценой последнего плюса — можно разместить команды в RAM, после чего отсортировать. Или даже заранее посчитать hash-функцию какую-нибудь чтобы сравнивать не через strcmp.
И каждый раз, рано или поздно, мне хотелось помимо пассивного вывода сделать ввод команд через тот же 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[] = {...}, но этот путь нарушает требование добавления команд из разных файлов.
Заводить массив «побольше» и добавлять команды функцией вроде
тоже не хочется, т.к. придётся либо постоянно подправлять константу MAX_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++; }
Делать всё тоже самое с помощью динамического выделения памяти и увеличения выделенного массива с помощью realloc на каждом добавлении — наверное неплохой выход, но не хотелось связываться с динамической памятью вообще (нигде больше она в проекте не используется, а кода в ROM занимает много, да и RAM не резиновый).
В итоге я пришёл к следующему любопытному, но, к сожалению, не самому портабельному решению:
Вся магия здесь заключена макросе REGISTER_COMMAND, который создаёт глобальные переменные так, что при исполнении кода они будут идти в памяти строго друг за другом. А опирается эта магия на атрибут section, который указывает линкеру, что эту переменную надо положить в отдельную секцию памяти. Таким образом на выходе мы получаем нечто очень похожее на массив registered_commands из предыдущего примера, но не требующей заранее знать сколько в нём будет элементов. А указатели на начало и конец этого массива нам предоставляет линкер.#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; }
Подведём итоги, выпишем плюсы и минусы данного решения:
Плюсы:
- Возможность плодить команды пока не кончится память.
- Проверка уникальности имён команд на этапу сборки. Неуникальные команды приведут к созданию двух переменных с одним и тем же именем, что будет диагностировано линкером как ошибка.
- Возможность объявлять команды в любой единице трансляции, не меняя остальные.
- Отсутствие зависимостей от каких-либо внешних библиотек.
- Отсутсвие необходимости в специальной run-time инициализации (регистрация команд и т.д.).
- Отсутсвтие накладных расходов по памяти. Весь массив команд может размещаться в ROM.
Минусы:
- Опирается на конкретный toolchain. Для других придётся править создание команды и, возможно, линкерный скрипт.
- Реализуется не на всех архитектурах, т.к. опирается на структуру бинарного формата исполняемого файла. (см. атрибуты переменных в gcc)
- Линейный поиск по зарегистрированным командам, т.к. массив неотсортирован.
Последний минус можно побороть ценой последнего плюса — можно разместить команды в RAM, после чего отсортировать. Или даже заранее посчитать hash-функцию какую-нибудь чтобы сравнивать не через strcmp.
