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

У кожному розроблювальному пристрої у мене був присутній вивід зневадження в 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.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.