Here's how to get started with a new project based on MicroLua:
-
Create a file
main.luacontaining amain()function.function main() print("Hello, world!") end
-
Create a top-level
CMakeLists.txtfor the project.# Define the project and initialize MicroLua. cmake_minimum_required(VERSION 3.24) include(mlua_import.cmake) project(my_project C CXX ASM) set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 17) mlua_init() # Add a library for the main module. mlua_add_lua_modules(my_project_main main.lua) target_link_libraries(my_project_main INTERFACE mlua_mod_mlua.stdio ) # Add the executable. mlua_add_executable(my_project_hello) target_link_libraries(my_project_hello PRIVATE my_project_main pico_stdlib )
-
Copy the file
mlua_import.cmakefrom the MicroLua repository to the same directory as the top-levelCMakeLists.txtof the project. -
Build the project.
# Adjust the paths for your setup. $ export MLUA_PATH="${HOME}/MicroLua" # Build the executable. $ cmake -S . -B build -DPICO_BOARD=pico $ make -j9 -C build # This generates the executable files build/my_project_hello.elf and # build/my_project_hello.uf2, which can be flashed onto a target.
.lua files are imported as modules. That is, they are executed on the first
require() call, with _ENV set to a new, empty module table that is
registered in package.loaded with the name given to require().
Within .lua files:
- Local variables and functions are private to the module.
- Non-local variables and functions are exported.
- Lookups of undefined symbols are forwarded to the global environment
_G. - Lookups of undefined symbols in the global environment raise an error.
.lua files must be registered as a libraries in CMakeFile.txt, and their
dependencies must be declared:
mlua_add_lua_modules(mod_example example.lua)
target_link_libraries(mod_example INTERFACE
mlua_mod_hardware.adc
mlua_mod_hardware.gpio
)MicroLua supports writing modules in Fennel, by
transpiling Fennel sources to Lua. Simply use mlua_add_fnl_modules() instead
of mlua_add_lua_modules(). There are a few examples in the
MicroLua-examples
repository.
The MicroLua runtime sets a metatable on the lightuserdata type, to make it
operate like C pointers (that's effectively what light userdata is). The
metatable has the following items:
-
__name = 'pointer'
The name to use intostring(). -
__add(ptr, offset) -> pointer
Add an offset to the pointer. -
__sub(ptr, offset) -> pointer
__sub(ptr1, ptr2) -> integer
Subtract an offset from a pointer, or compute the difference between two pointers. -
__lt(ptr1, ptr2) -> boolean
__le(ptr1, ptr2) -> boolean
Return the result of the respective comparison operation on the pointers. -
__buffer(ptr) -> ptr
Implement the buffer protocol. No size is returned, so the buffer is considered infinite.
The MicroLua runtime sets a metatable on the function type with a __close
metamethod that calls the function. This makes it easy to define deferreds, e.g.
for reliable resource management.
do
local save = sync.save_and_disable_interrupts()
local done<close> = function() sync.restore_interrupts(save) end
flash.range_program(offs, data)
end -- Interrupts are restored here, even if range_program() raises an errorA buffer is an object that provides low-level access to mutable memory. To be
recognized as a buffer, an object must implement the __buffer metamethod.
Object:__buffer() -> (ptr, [size], [vtable])
Return a pointer to the buffer, its size, and aMLuaBufferVt*.ptrandvtableare light userdata, andsizeis an unsigned integer. Ifsizeis missing ornil, the buffer is considered infinite, which disables bounds checks. Ifvtableis missing ornil, the buffer is raw andptrpoints at a contiguous block of memory.
Read-only tables reduce the RAM usage of tables where the keys are known at
compile-time and mostly immutable, for example module or class tables. They are
implemented using
minimal perfect hashing
with omitted key values. Table definitions (MLUA_SYMBOLS()) are parsed out of
C modules at compile-time to generate the hash functions. The hash function and
the table values are stored in flash, while the table keys aren't stored at
all. At runtime, lookups are performed in the __index metamethod by computing
the hash of the key and looking up the corresponding value.
The perfect hash functions are computed using the CHM92 algorithm, as described in:
An optimal algorithm for generating minimal perfect hash functions
Z. J. Czech, G. Havas and B. S. Majewski
Information Processing Letters, 43(5):257-264, 1992
Not storing table keys massively reduces flash usage, and makes it possible to
include huge modules like hardware.regs.io_bank0,
which wouldn't even fit in flash otherwise. There is one downside, however:
Important
Lookups of string keys in read-only tables always succeed. When looking up a key that wasn't present in the original table, an arbitrary, other value from the table is returned.
This is usually not an issue for module and class tables, but it means that it's
not possible to check for the presence of a key, and typos will cause undefined
behavior, making debugging more difficult. To alleviate the latter during
development, the MLUA_SYMBOL_HASH_DEBUG compile definition can be set to 1
for a build target, which causes table keys to be stored in flash and checked at
runtime. This significantly increases flash usage and degrades lookup
performance, so it shouldn't be left enabled in a release build.
target_compile_definitions(example_target PRIVATE
MLUA_SYMBOL_HASH_DEBUG=1
)There is a fairly obvious mapping from the C library name to the corresponding
Lua module. For example, the hardware_i2c library is wrapped by the
hardware.i2c module.
The constants and functions exported by C libraries are exposed as module- or class-level symbols with the same names, with obvious prefixes stripped when they would cause stuttering. For example:
PICO_UART_ENABLE_CRLF_SUPPORT=>hardware.uart.ENABLE_CRLF_SUPPORTUART_PARITY_NONE=>hardware.uart.PARITY_NONEuart_set_baudrate()=>hardware.uart.UART.set_baudrate()
Function arguments are kept in the same order as their C counterpart, except for out arguments, which map to multiple return values. Function argument types are mapped as follows:
| C type | Lua type |
|---|---|
bool |
boolean |
| integer (<=32 bits) | integer |
int64_t, uint64_t |
Int64 or integer |
float, double |
number |
char* |
string |
| pointer + size | string |
| pointer to struct | userdata |
absolute_time_t |
Int64 or integer |
As a convenience, bool function arguments accept 0 and 0.0 as false.
Events are a mechanism to bridge low-level IRQ handlers and callbacks with high-level Lua code. Each event has an associated set of watching threads. When an event is triggered, the threads watching the event are moved to the active queue at the next context switch. This allows threads to register interest in an event and suspend themselves, and to be resumed when the event happens.
IRQ enabler functions set up an IRQ handler and one or more events. Their
enable argument can take the following values:
- Missing,
true,nil, negativeinteger: Set up an exclusive IRQ handler and enable the IRQ. - Non-negative
integer: Set up a shared IRQ handler with the given priority and enable the IRQ. The priority must be in the range [hardware.irq.SHARED_IRQ_HANDLER_LOWEST_ORDER_PRIORITY;hardware.irq.SHARED_IRQ_HANDLER_HIGHEST_ORDER_PRIORITY], i.e.[0; 0xff]. false: Disable the IRQ and remove the IRQ handler.
Pico SDK functions that accept a callback are implemented using threads. A C callback is set up to trigger an event, and the thread watches the event and calls the Lua callback when the event triggers.