О Lua
Что касается программирования на Lua, написанного Р. Иерусалимским [1], Lua - это небольшой язык сценариев, написанный на C, который совместим со стандартом C89. Lua намного проще в изучении, чем C, и он гораздо проще поддерживает более высокий уровень абстракций, таких как объектная ориентация.
Кроме того, есть некоторые характеристики Lua, как показано ниже, которые делают Lua хорошим языком сценариев для встраивания.
1. Lua изначально предназначен для встраивания в другое приложение; поэтому он предлагает большой набор библиотек, предназначенных для простой интеграции в C.
2. Используя Lua C API, разработчики могут легко использовать код C в качестве библиотеки, которую можно отнести к коду Lua. Кроме того, создавая дополнительные библиотеки на C, разработчики могут расширить возможности сценария Lua.
3. Lua предлагает сборку мусора. Разработчикам не нужно беспокоиться об утечках памяти или критических ошибках, которые могут возникнуть при ручном управлении динамической памятью.
4. Исходный код Lua разработан таким образом, чтобы его можно было легко настраивать. Ядро Lua-движка может быть всего около 100 килобайт за счет исключения не упомянутых библиотек, что полезно для его переноса на встроенные системы или небольшие микроконтроллеры с ограничениями памяти.
5. Кроме того, Lua предоставляется бесплатно по лицензии с открытым исходным кодом, которая позволяет использовать его в коммерческих приложениях. Это также позволяет модифицировать исходный код, чтобы настроить его.
Запуск Lua во встроенных системах без операционных систем
Почему?
Встраивание Lua дает возможность реализовать некоторые функции в Lua, а не в C. Другими словами, разработчики смогут использовать Lua всякий раз, когда некоторые функции могут быть легче разработаны с помощью Lua. Более того, Lua предлагает API-интерфейсы C, которые позволяют очень легко связываться с C, как показано в этом проекте. Используя эту привязку, функции, написанные на Lua, могут взаимодействовать с библиотекой C-wrapper, которая предоставляет аппаратный уровень. Это еще больше расширит возможности приложения Lua.
Подход на основе суперциклов без операционной системы
В зависимости от требований к системе, конструкции встроенных систем можно разделить в основном на два подхода.
Первый подход - запуск системы поверх операционных систем (ОС). В частности, операционная система реального времени должна быть перенесена, если система критична по времени, требуя высокой сложности с большим количеством задач и прерываний. Вытеснение означает возможность приостановить текущую запущенную задачу, когда необходимо выполнить задачу с более высоким приоритетом. Обычно за управление прерываниями отвечают ядра ОС.
Более того, ОС предоставляет промежуточное ПО, которое представляет собой программное обеспечение между ядром и такими приложениями, как файловая система. ОС следует адаптировать, если для системы требуется промежуточное ПО.
Если желаемое приложение имеет небольшое количество задач для управления, широко используется подход на основе суперцикла. В этой системе все задачи выполняются последовательно в бесконечном цикле. Это означает, что задачи во встроенных приложениях будут повторяться бесконечно, пока работает система.
Эта статья призвана показать возможность встраивания Lua во встроенные системы на основе суперциклов.
Моя целевая плата: микроконтроллер STM32F415
STM32F415 - 32-битный микроконтроллер производства STMicroelectronics. Этот микроконтроллер был тщательно выбран для этого проекта, поскольку он поддерживает стандартную библиотеку newlib с ядром ARM Cortex M4.
Работая со стандартной библиотекой, исходный код Lua может быть скомпилирован на целевом компьютере без сложных модификаций. Кроме того, результаты этого проекта могут быть легко применены к различным проектам, поскольку многие из них поддерживают стандартную библиотечную поддержку.
Кроме того, этот микроконтроллер предлагает 1 мегабайт флэш-памяти и 192 килобайта оперативной памяти. В этом проекте скомпилированный двоичный код исходного кода Lua должен находиться во флэш-памяти, и это увеличивает использование памяти. 1 мегабайт флэш-памяти достаточно, чтобы понять влияние на память после встраивания Lua.
Компиляция исходного кода Lua
Исходный код Lua написан на C, и его необходимо скомпилировать на целевой машине, чтобы можно было портировать виртуальную машину Lua.
Исходный код Lua совместим со стандартом C89, что означает, что, пока целевой объект поддерживает стандартную библиотеку C ANSI, исходный код Lua может быть скомпилирован.
Я использовал микроконтроллер STM32, и он предлагает библиотеку под названием Newlib-nano. Newlib - это облегченная версия стандартной библиотеки ANSI C / C ++, предназначенная для встраиваемых систем, а Newlib-nano - это вариант Newlib для расширения поддержки дополнительных микроконтроллеров, таких как микроконтроллеры на базе ARM Cortex-M.
В таблице ниже показан размер объектов исходного кода Lua, скомпилированных с помощью Newlib-nano. Обратите внимание, что версия Lua, которую я использовал в этой работе, была 5.1.

Общий размер всех исходных объектов Lua 5.1 составлял 122.020 килобайт. Учитывая, что микроконтроллер STM32 предлагает 1 мегабайт флэш-памяти, это количество увеличивается после добавления Lua.
Необходимо было исключить два исходных файла: «lua.c» и «luac.c». Эти файлы содержат основную программу для интерпретатора командной строки и автономный компилятор байт-кода. В них не было необходимости, пока Lua нужно было портировать без командной оболочки.
Нужно хранить скрипты Lua для выполнения на целевом объекте, но где?
Чтобы выполнить сценарии и библиотеки Lua на целевом объекте, фактические сценарии Lua должны были храниться во флэш-памяти, чтобы их можно было выполнить на целевом объекте.
Файловая система
Файловая система в ядре ОС облегчает манипулирование файлами и каталогами. Чтобы быть конкретным, файловая система управляет тем, как и где данные хранятся в памяти, чтобы клиенты, такие как пользователи или приложения, могли легко получить доступ к этим данным. Кроме того, файловая система управляет структурой файлов, каталогов и метаданных, а также любыми операциями, такими как чтение или запись. На приведенном ниже рисунке концептуально показано, что файловая система управляет доступом клиента к данным, хранящимся в памяти, и операциями по чтению и записи.

В общем, ОС, такие как Linux или Windows, содержат файловые системы, поэтому клиенты не думают, где хранить / загружать файлы и как ими управлять.
Очевидно, что для встроенных систем без ОС система не может использовать файловую систему, поскольку она предлагается ядром ОС. Это означает, что клиенты должны вручную управлять адресами памяти, где хранятся файлы.
Без файловой системы
Независимо от того, какая файловая система, встраивание файловой системы потенциально потребует дополнительной памяти и затрат времени на обработку. По этой причине вместо использования файловой системы каждый сценарий Lua сохранялся непосредственно в памяти для этой работы. На рисунке ниже показано, что изображение, содержащее все сценарии Lua, хранится по определенному адресу памяти. Изображение также содержит информацию о начальном адресе каждого скрипта.

Информация о начальном адресе была необходима, когда каждый скрипт загружался в приложение C. Например, «library_1.lua», которая хранилась по адресу 0x08010000, была доступна в C, как показано ниже;

«LuaL_loadbuffer» загружает буфер как блок Lua, не выполняя его на самом деле, и этот блок Lua может выполняться всякий раз, когда он вызывается в приложении C. Однако это решение оказалось крайне неэффективным, когда нужно было хранить большое количество сценариев Lua, потому что нужно было отслеживать и управлять начальными адресами каждого сценария Lua. Кроме того, буфер нужно было загружать столько раз, сколько скриптов Lua.
Объединение Lua
Чтобы улучшить способ хранения и загрузки сценариев Lua, все сценарии Lua могут быть объединены и объединены в один файл. Тогда количество операций загрузки буфера может быть уменьшено до одного раза, потому что все сценарии объединены в один файл.
Перед объединением нам нужно понять, как Lua VM ищет модуль.
Lua VM, также известная как интерпретатор, представляет собой движок для выполнения байт-кода Lua. Когда виртуальная машина Lua выполняет сценарий Lua, она ищет все необходимые модули Lua из таблицы Lua под названием «package.preload» для загрузки модулей.
Предполагая, что виртуальная машина Lua ищет модуль с именем «lua_to_c», процесс будет выполняться в следующей последовательности.
Прежде всего, виртуальная машина Lua ищет этот модуль в таблице package.preload, чтобы узнать, загружен ли модуль предварительно. Стандартные библиотеки Lua обычно предварительно загружаются, поэтому стандартная функция, такая как print (), может использоваться по умолчанию.
Если модуль «lua_to_c» отсутствует в предварительно загруженной таблице, виртуальная машина Lua начинает искать папку общей библиотеки, а также локальную папку проекта.
Инструмент Lua Amalg
Amalg - это инструмент для упаковки сценариев Lua и зависимых модулей в один файл путем добавления зависимых модулей в эту таблицу package.preload. Оно разработано П. Янда и является бесплатным программным обеспечением под лицензией MIT [2].
Этот инструмент принимает исходный файл и список необходимых модулей в качестве аргументов, а затем создает один выходной файл.
На рисунке ниже показано, как выполняется объединение. «Lua_application.lua» требует двух библиотечных модулей. Затем эти библиотечные модули были добавлены в таблицу package.preload. Виртуальной машине Lua не нужно было заглядывать в каталоги, поскольку в этом случае виртуальная машина Lua смогла найти необходимые модули в таблице предварительной загрузки.

После процесса объединения все объединенные файлы были объединены вместе для создания одного файла Lua. Загрузка этого объединенного файла Lua в приложение C позволила загрузить все необходимые модули Lua сразу. Кроме того, можно было без проблем сделать так, чтобы для одного модуля требовались другие модули.

Примечание: файл Lua был преобразован в формат SREC для прошивки цели. SREC означает S-Record, который обычно используется для программирования флэш-памяти.
На этом этапе мы загрузили сценарии и библиотеки Lua, а также исходные коды интерпретатора, все загружены в цель. Нам нужно понять, как запускать код из наших приложений C.
Запуск скриптов Lua
Инициализация виртуальной машины Lua
Виртуальная машина Lua выполняет байт-код Lua на целевом объекте, байт-код - это промежуточная форма кода, которая может быть создана путем компиляции исходного кода Lua. Виртуальная машина Lua должна быть инициализирована в целевом приложении. На рисунке ниже представлен обзор процесса инициализации Lua VM.


Прежде всего, новое состояние интерпретатора Lua создается путем выделения памяти с использованием Lua C API. Затем регистрируются стандартные библиотеки, показанные слева. После регистрации стандартных библиотек строки Lua необходимо будет загрузить и преобразовать в байт-код после компиляции, выполненной компилятором Lua. Наконец, этот байт-код будет выполнен виртуальной машиной Lua. Этот процесс необходимо реализовать в приложении C, чтобы инициализировать виртуальную машину Lua во время запуска приложения.
Lua C API?
Lua предлагает простой, но очень сильный C API. Этот C API расширяет возможности модуля C, поскольку он позволяет разработчикам не только запускать код Lua, но также получать доступ к объектам Lua из C. Аналогичным образом разработчики могут получать доступ к библиотекам C и вызывать функции C из Lua.
Lua и C используют общий стек для связи. В частности, виртуальная машина Lua будет инициализирована вместе с распределителем памяти, затем виртуальная машина также выделяет стек в куче для совместного использования данных между Lua и C. В C эти данные были извлечены из общего стека.
Экономия памяти
Для многих встроенных систем размер памяти ограничен. Когда исходный код Lua загружается и компилируется в байт-код, он проходит через некоторые процессы, такие как загрузка в память и компиляция, что требует огромного потребления памяти.
Фактически, процесс компиляции в моем случае не был успешным при тестировании на STM32. Чтобы быть более наглядным, при попытке загрузить сценарии Lua в целевой объект произошла ошибка, вызвавшая исключение сбоя из-за нехватки памяти. Ошибка была замечена при запуске парсера Lua, читающего входные тексты. Это произошло потому, что виртуальная машина Lua потребляет оперативную память при синтаксическом анализе исходного кода, чтобы преобразовать его в байт-код. Поскольку у цели были ограниченные ресурсы памяти, размер ОЗУ, требуемый парсером Lua, был больше, чем было доступно.
Предварительная компиляция
Эта проблема была решена путем предварительной компиляции входных сценариев в байт-код перед загрузкой. Это произошло потому, что загрузка байт-кода вместо входных текстов пропускает процесс компиляции.
Виртуальная машина Lua способна определять, является ли входной двоичный код исходным кодом или байт-кодом, читая блок заголовка. Блок заголовка - это первые 12 байтов предварительно скомпилированного фрагмента. Блок заголовка содержит не только подпись байт-кода, но и полезные конфигурации Lua, перечисленные ниже [3];

Следующие шестнадцатеричные значения на рисунке ниже были первыми 12 байтами байт-кода Lua, созданного в моей работе. Первые 4 байта были «0x1B4C7561», что должно было показать, что этот входной блок был байт-кодом Lua. «51» означало, что этот фрагмент был скомпилирован на Lua версии 5.1. Этот фрагмент байт-кода был кросс-скомпилирован для микроконтроллера STM32 на основе 32-разрядного процессора с использованием формата little-endian. Байты с 7 по 11 должны были показать эту конфигурацию. Последний байт был 1, показывая, что этот фрагмент был предварительно скомпилирован для конфигурации только с целыми числами.

Кросс-компилятор
Для выполнения процесса предварительной компиляции необходим кросс-компилятор Lua. Требуется скомпилировать двоичный образ на ПК для другой аппаратной платформы и сделать скомпилированный байт-код исполняемым на целевых устройствах ARM. Lua 5.1 содержит компилятор под названием «luac», но этот компилятор не поддерживает кросс-компиляцию. Одно из решений этого - скомпилировать исходный код и выгрузить скомпилированный байт-код на микроконтроллер с той же архитектурой, что и STM32, с гораздо большим объемом оперативной памяти, чтобы можно было скомпилировать код. Затем этот скомпилированный байт-код должен быть выполнен на устройстве Superbean; но это неэффективно, поскольку требует дополнительного оборудования, а также требует много времени.
В качестве альтернативы eLua предлагает настроенную версию luac с поддержкой кросс-компиляции [4]. Этот кросс-компилятор принимает несколько параметров для работы с различными конфигурациями целевой платы и выводит на их основе байт-код. Флаг «-ccn» предназначен для настройки типа «lua_Number» и размера, который является целым числом и 32 байтами, соответственно. Параметр «-cce» может использоваться для настройки порядка байтов, который в этом случае установлен на малое значение.

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

Кроме того, предварительно скомпилированный фрагмент Lua оказался меньше соответствующего исходного файла на 3,451 килобайта, как показано в левой таблице в моем случае.
Общий
Ниже показан общий процесс сборки и выполнения кода Lua после включения процесса предварительной компиляции.

Ссылки
[1] Р. Иерусалимский, Программирование на Lua, 2014 г., 3-е изд. глава 4, с. 293–365.
[2] П. Джанда, Объединение модулей / скриптов Lua [Онлайн]. Доступно: https://github.com/siffiejoe/lua-amalg [Проверено в июле. 3, 2018]
[3] К., Х. Ман, «Простое введение в инструкции Lua 5.1 VM», март 2006 г., версия 0.1.
[4] eLua Doc, «Общая информация» [Интернет]. Доступно: http://www.eluaproject.net/doc/v0.8/en_using.html [дата обращения: июнь. 29, 2018]