Перейти к содержанию
Материалы раздела

Язык uLua

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

Цена такой компактности — предсказуемые, но жёсткие ограничения. uLua подходит для GPIO, датчиков, таймеров и коротких протоколов, однако не заменяет полную Lua при работе с таблицами, динамическими модулями или числами с плавающей точкой.

hw.gled(true)
return 1 + 2

Модель выполнения

  • исходный файл компилируется до загрузки на устройство;
  • встроенные и пользовательские функции определяются при компиляции;
  • на устройстве нет require, load, _G, интерактивной командной строки (REPL) и сборщика мусора;
  • программа получает доступ к оборудованию через встроенные модули hw, cmd, mlib и другие модули выбранного устройства.

Что поддерживается

В uLua доступны:

  • целые числа, логические значения, nil и строки;
  • локальные переменные;
  • if, elseif, else;
  • while, repeat ... until, числовой for и break;
  • именованные функции верхнего уровня;
  • до трёх возвращаемых значений;
  • строковая конкатенация;
  • арифметические, битовые, логические операции и сравнения.

Не поддерживаются:

  • таблицы и конструкторы таблиц;
  • pairs, ipairs и next;
  • синтаксис методов object:method();
  • обобщённый цикл for ... in;
  • goto;
  • числа с плавающей точкой;
  • анонимные и вложенные функции;
  • замыкания и захват внешних локальных переменных;
  • косвенный вызов функции через значение, вычисленное во время выполнения;
  • переменное число аргументов ....

Для небольших битовых наборов и множеств целых используйте mlib.

Жёсткие ограничения

Одна программа может содержать:

  • до 16 функций, включая основную;
  • до 144 констант;
  • не более 8 одновременно активных функций, включая основную;
  • до 128 слотов на функцию: параметры, локальные переменные и стек вычислений вместе;
  • до 65 535 байт кода в одной функции и во всей программе;
  • до 65 535 байт в строковом пуле и в одной строковой константе;
  • не более трёх значений в одном return.

Некоторые ограничения конкретного оборудования — например, число таймеров или размер приёмного буфера — перечислены на страницах соответствующих модулей. При вызовах все активные функции также делят общий пул из 128 слотов. Поэтому глубоко вложенная цепочка крупных функций может исчерпать его раньше предела в восемь вызовов.

Фактический размер программы на устройстве

Пределы 65 535 байт относятся к полям формата байт-кода, а не к доступной памяти STM32F042. Для программы действуют следующие ограничения:

  • сохранённый файл байт-кода для автоматического запуска может занимать не более 2032 байт;
  • временный запуск файла до 250 байт использует ОЗУ;
  • временный запуск файла размером 251..2048 байт использует ту же страницу флеш-памяти и поэтому недоступен, пока в ней хранится программа автозапуска.

В размер входит весь файл: заголовок, таблицы функций и констант, строки и код. Если редактор сообщает, что программа не помещается, сокращать нужно итоговый байт-код, а не только исходный текст.

Типы значений

ТипПримерОсобенность
Целое42, -7, 0xFFЗнаковое 28-битное число
Логическоеtrue, falsefalse — ложное значение
nilnilОтсутствие значения
Строка"hello", a .. bКонстанта или временный результат конкатенации
Встроенный дескрипторhw.p1, mlib.bitset_new(8)Значение, которым управляет встроенный модуль

Константы вроде hw.p1 имеют собственный встроенный тип. Обычное число 1 нельзя использовать вместо дескриптора вывода; для преобразования номера служит hw.pin(1).

Числа и арифметика

Все вычисления целочисленные. Значение uLua лежит в диапазоне -2²⁷..2²⁷−1. Виртуальная машина выполняет арифметику в 32 битах, а при сохранении результата оставляет 28 бит.

local a = 10 + 3
local b = 10 - 3
local c = 10 * 3
local d = 10 / 3   -- 3
local e = 10 % 3   -- 1
local f = -10

Оператор / делит целые числа с усечением к нулю. Остаток % рассчитан по тому же правилу: например, -10 % 3 равно -1. Это отличается от округления вниз в полной Lua. Деление на ноль завершает программу ошибкой. Операторы << и >> используют только пять младших битов правого операнда, то есть фактически сдвигают на n & 31.

Битовые операции

Поддерживаются:

  • & — И;
  • | — ИЛИ;
  • бинарный ~ — исключающее ИЛИ;
  • унарный ~ — инверсия;
  • << и >> — сдвиги.
local flags = 0
flags = flags | 1
flags = flags | 4

if (flags & 4) ~= 0 then
    hw.cdc_write("bit 2 set\n")
end

Условия и логические операции

В условиях только false и nil считаются ложью. Ноль, как и в Lua, истинен.

Доступны сравнения ==, ~=, <, <=, > и >=, а также not, and и or. Равенство можно проверять для любых значений; строки сравниваются по содержимому. Сравнения порядка <, <=, > и >= работают только с целыми числами. Операторы and и or вычисляют правую часть только при необходимости.

Строки

uLua поддерживает строковые литералы, передачу строк встроенным функциям и конкатенацию через ... Объединять можно строки и целые числа. Логические значения, nil и встроенные дескрипторы конкатенация не принимает; выводите их отдельным вызовом hw.print(). Общего аналога tostring() нет.

local name = "sensor"
local value = 42
hw.cdc_write(name .. "=" .. value .. "\n")

local n = #"hello"   -- 5

Оператор # работает и со строковым литералом, и с результатом конкатенации. Он возвращает число байтов, а не символов Unicode.

Результат конкатенации помещается в один из двух общих буферов по 128 байт. Следующая конкатенация может перезаписать прежнюю временную строку. Поэтому не сохраняйте такой результат для позднего использования, например в аргументе обработчика таймера. Строковые литералы от этого ограничения не зависят.

В одной операции можно объединить от двух до восьми частей. Более длинную последовательность разбейте на несколько выражений, помня, что для временных строк доступны только два общих буфера.

Встроенной библиотеки string в uLua нет. Доступные операции со строками зависят от модели и версии прошивки.

Переменные и циклы

Поддерживаются обычные локальные переменные:

local pin = hw.p1
local state = 0
state = 1 - state

Условия

local x = 5

if x < 3 then
    hw.print("small\n")
elseif x < 8 then
    hw.print("medium\n")
else
    hw.print("large\n")
end

while и repeat

local i = 0
local sum = 0

while i < 5 do
    sum = sum + i
    i = i + 1
end

repeat
    i = i - 1
until i == 0

Числовой for

local sum = 0

for i = 1, 10 do
    sum = sum + i
end

for i = 10, 1, -2 do
    hw.print(i .. "\n")
end

Функции

Пользовательские функции должны быть именованными и находиться на верхнем уровне программы.

function square(x)
    return x * x
end

function sum_and_diff(a, b)
    return a + b, a - b
end

local x = square(6)
local sum, diff = sum_and_diff(7, 2)
return x + sum + diff

Число результатов определяется местом вызова, как в Lua: лишние отбрасываются, недостающие становятся nil, а отдельный вызов не сохраняет результат. Внешний интерфейс запуска программы показывает только первое значение, возвращённое основной функцией; несколько результатов нужны прежде всего при вызовах пользовательских функций внутри программы.

function values()
    return 1, 2
end

local first = values()      -- 1
local a, b, c = values()    -- 1, 2, nil
values()                    -- оба результата отброшены

Почему внешняя локальная переменная недоступна

Такой код требует замыкания и не скомпилируется:

local OUT_OPEN = 1
local OUT_CLOSED = 0

function pulse(pin, ms)
    hw.gpio_set(pin, OUT_CLOSED)
    hw.delay_ms(ms)
    hw.gpio_set(pin, OUT_OPEN)
end

Передайте значения явно:

function pulse(pin, ms, closed_level, open_level)
    hw.gpio_set(pin, closed_level)
    hw.delay_ms(ms)
    hw.gpio_set(pin, open_level)
end

pulse(hw.pa3, 200, 0, 1)

Или оставьте неизменяемые значения внутри функции:

function pulse_low(pin, ms)
    hw.gpio_set(pin, 0)
    hw.delay_ms(ms)
    hw.gpio_set(pin, 1)
end

Полный пример

hw.gpio_init(hw.p1, hw.out)

function blink_once()
    hw.gpio_set(hw.p1, 1)
    hw.delay_ms(100)
    hw.gpio_set(hw.p1, 0)
end

local i = 0
while i < 5 do
    blink_once()
    hw.print("blink " .. i .. "\n")
    i = i + 1
    hw.delay_ms(400)
end

return i

Дальше можно перейти к работе с оборудованием или к примерам загрузки программ на устройство.