Pull to refresh

Command line interpreter на микроконтроллере своими руками

Reading time 4 min
Views 30K
В каждом разрабатываемом устройстве у меня присутствовал отладочный вывод в UART, как в самый распространённый и простой интерфейс.
И каждый раз, рано или поздно, мне хотелось помимо пассивного вывода сделать ввод команд через тот же UART. Обычно это происходило когда мне хотелось для отладки выводить какой-нибудь очень большой объём информации по запросу (например состояние NANDFLASH, при разработке собственной файловой системы). А иногда хотелось программно управлять ножками GPIO, чтобы отрепетировать работу с какой-нибудь переферией на плате.
Так или иначе мне был необходим CLI, который позволяет обрабатывать разные команды. Если кто-то натыкался на уже готовый инструмент для этих целей — буду благодарен за ссылку в комментариях. А пока я написал собствыенный.

Требования, в порядке уменьшения важности:
  1. Язык С. Я пока не готов писать ПО для микроконтроллеров на чём-либо другом, хотя ситуация может и измениться.
  2. Приём и обработка строк из UART. Для простоты все строки оканчиваются '\n'.
  3. Возможность передавать в команду параметры. Набор параметров различается для разных команд.
  4. Легкость добавления новых команд.
  5. Возможность добавления новых команд в разных исходных файлах. Т.е. начиная реализовывать очередной функционал в файле "new_feature.c" я не трогаю исходники CLI, а добавляю новые команды в том же файле "new_feature.c".
  6. Минимум используемых ресурсов (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.
Tags:
Hubs:
+7
Comments 38
Comments Comments 38

Articles