eLua interrupt handlers
eLua interrupt handlers
Starting with vesion 0.8 eLua supports interrupt handlers written in Lua. Once an interrupt handler is set in the Lua code, it will be called each time a supported interrupt is generated. A supported interrupt is any interrupt that is handled by the platform C code (see here for more details).
IMPORTANT: before learning how to use interrupt handlers in Lua, please keep in mind that Lua interrupt handlers don’t work the same way as regular (C) interrupt handlers. As Lua doesn’t have direct support for interrupts, they have to be emulated. eLua emulates them using a queue that is populated with interrupt data by the C support code. As long as the queue is not empty, a Lua hook is set to run every 2 Lua bytecode instructions. This hook function is the Lua interrupt handler. After all the interrupts are handled and the queue is emptied, the hook is automatically disabled. Consequently:
-
When the interrupt queue is full (a situation that might appear when interrupts are added to the queue faster than the Lua code can handle them) subsequent interrupts are ignored (not added to the queue) and an error message is printed on the eLua console device. The interrupt queue size can be configured at build time, as explained here. Even if the interrupt queue is large, one most remember that Lua code is significantly slower than C code, thus not all C interrupts make suitable candidates for Lua interrupt handlers. For example, a serial interrupt that is generated each time a char is received at 115200 baud might be too fast for Lua (this is largely dependent on the platform). On the other hand, a GPIO interrupt-on-change on a GPIO line connected with a matrix keyboard is a very good candidate for a Lua handler. Experimenting with different interrupt types is the best way to find the interrupts that work well with Lua.
-
A more subtle point is that the Lua virtual machine must run for the interrupt handlers to work. A simple analogy is that a CPU must have a running clock in order to function properly (and in order to take care of the hardware interrupts). If the clock is stopped the CPU doesn’t run and the interrupt handlers aren’t called anymore, although the occurence of the interrupt might be recorded inside the CPU. This is the exact same situation with Lua: if the virtual machine doesn’t run, the interrupts are still recorded in the interrupt queue, but the Lua handler won’t be called until the virtual machine runs again. In this case though, the "clock" of the Lua VM is a C function that is executed for every VM instruction. If this function blocks for some reason, the VM instructions are not executed anymore. It’s not hard to make this function block; for example, it blocks everytime the Lua code waits for some user input at the console, or when a tmr.delay is executed, or when uart.read is called with an infinite or very large timeout; in general, any function from a Lua library that doesn’t return immediately or after a short ammount of time will block the VM. Care must be taken to avoid such operations as much as possible, otherwise the interrupt support code won’t run properly.
-
There is a single interrupt handler per interrupt type in Lua (the same holds true for C interrupt support), as opposed to the many hardware interrupts handlers usually found on the eLua targets. It is however easy to differentiate between different interrupt sources, as will be explained in the next section.
-
Lua interrupt handlers are not reentrant.
While this might seem restrictive, Lua interrupt handlers work quite well in practical situations. As an added bonus, since they are implemented by C support code, there’s nothing preventing eLua from implementing "custom interrupts" (software generated interrupts that don’t correspond to a hardware interrupt on the CPU), such as serial interrupt on char match (generate an interrupt when a certain char is received on the serial port, for example a newline), timer interrupts for virtual timers, TCP/UDP data packet received interrupt and many others.
An up-to-date list of all interrupts supported by eLua can be found here. |
Using interrupt handlers in Lua
To enable Lua interrupt handler, define BUILD_LUA_INT_HANDLERS and PLTATFORM_INT_QUEUE_LOG_SIZE in platform_conf.h (see here for details). Setting up interrupt handlers is a straightforward process, most of the required functionality is provided by the cpu module:
-
use cpu.set_int_handler( int_id, handler ) to set the interrupt handler function for the specified interrupt (call with nil to disable the interrupt handler for that interrupt). cpu.set_int_handler returns the previous interrupt handler for int_id (or nil is an interrupt handler was not previously set for the interrupt). In most cases, your interrupt handler should call the previous handler to ensure proper interrupt management.
-
use cpu.sei( int_id, resnum1, [resnum2], …, [resnumn]) and cpu.cli( int_id, resnum1, [resnum2], …, [resnumn]) to enable/disable specific CPU interrupts that will trigger the interrupt handler. You can also use cpu.sei() and cpu.cli (without parameters) to enable/disable global interrupts on the CPU, although this is not recommended.
The interrupt handler receives the resource ID that specifies the resource that fired the interrupt. It can be a timer ID for a timer overflow interrupt, a GPIO port/pin combination for a GPIO interrupt on pin change, a SPI interface ID for a SPI data available interrupt, and so on.
An example that uses the above concepts and knows how to handle two different interrupt types is presented below:
local vtmrid = tmr.VIRT0 local to = 1500000 local prev_tmr, new_prev_tmr, prev_gpio -- This is the timer interrupt handler local function tmr_handler( resnum ) print( string.format( "Timer interrupt for id %d", resnum ) ) if prev_tmr then prev_tmr( resnum ) end end -- This is the timer interrupt handler that gets set after tmr_handler local function new_tmr_handler( resnum ) print( string.format( "NEW HANDLER: timer interrupt for id %d", resnum ) ) -- This will chain to the previous interrupt handler (tmr_handler above) if new_prev_tmr then new_prev_tmr( resnum ) end end -- This is the GPIO interrupt on change (falling edge) interrupt local function gpio_negedge_handler( resnum ) local port, pin = pio.decode( resnum ) print( string.format( "GPIO NEGEDGE interrupt on port %d, pin %d", port, pin ) ) if prev_gpio then prev_gpio( resnum ) end end -- Set timer interrupt handler prev_tmr = cpu.set_int_handler( cpu.INT_TMR_MATCH, tmr_handler ) -- Set GPIO interrupt on change (negative edge) interrupt handler prev_gpio = cpu.set_int_handler( cpu.INT_GPIO_NEGEDGE, gpio_negedge_handler ) -- Setup periodic timer interrupt for virtual timer 0 tmr.set_match_int( vtmrid, to, tmr.INT_CYCLIC ) -- Enable GPIO interrupt on change (negative edge) for pin 0 of port 0 cpu.sei( cpu.INT_GPIO_NEGEDGE, pio.P0_0 ) -- Enable timer match interrupt on virtual timer 0 cpu.sei( cpu.INT_TMR_MATCH, vtmrid ) local tmrid, count = 0, 0 while true do print "Outside interrupt" for i = 1, 1000 do tmr.delay( tmrid, 1000 ) end if uart.getchar( uartid, 0 ) ~= "" then break end count = count + 1 if count == 5 then print "Changing timer interrupt handler" new_prev_tmr = cpu.set_int_handler( cpu.INT_TMR_MATCH, new_tmr_handler ) end end -- Cleanup -- Stop the timer from generating periodic interrupts tmr.set_match_int( vtmrid, 0, tmr.INT_CYCLIC ); -- Disable the GPIO interrupt on change (negative edge) interrupt cpu.cli( cpu.INT_GPIO_NEGEDGE, pio.P0_0 ) -- Disable the timer interrupt on match interrupt cpu.cli( cpu.INT_TMR_MATCH, vtmrid ) -- Clear the timer interrupt handler cpu.set_int_handler( cpu.INT_TMR_MATCH, nil ); -- Clear the GPIO interrupt handler cpu.set_int_handler( cpu.INT_GPIO_NEGEDGE, nil );
This is the most common use case for Lua interrupts, but it’s not the only one. Another way to use interrupts from eLua uses polling instead of interrupt handlers: directly check the interrupt flags and execute a certain action when one of them becomes set. For this, use the cpu.get_int_flag( id, resnum, [clear] ) function from the cpu module, which returns the specified interrupt’s status for resource resnum. clear is an optional boolean parameter, specifying if the interrupt flag should be cleared if it is set. It defaults to true, and in most cases it shouldn’t be changed. Using this feature, it becomes easy to wait for one or more interrupt flag(s) to be set. To use interrupt polling:
-
Disable the interrrupt(s) to be polled with cpu.cli.
-
(optional) disable the interrupt handler for the interrupt with cpu.set_interrupt_handler( id, nil )
-
Use cpu.get_int_flag to get the interrupt flag.
The int_select function below is a possible implementation of a function that gets an array of interrupts and returns the first one that gets active:
function int_select( int_table ) while true do for i = 1, #int_table do local t = int_table[ i ] if cpu.get_int_flag[ t[ 1 ], t[ 2 ] ) then return t[ 1 ], t[ 2 ] end end end end cpu.cli( cpu.INT_GPIO_NEGEDGE, pio.P0_0 ) cpu.cli( cpu.INT_TMR_MATCH, tmr.VIRT0 ) local ints = { { cpu.INT_GPIO_NEGEDGE, pio.P0_0 }, { cpu.INT_TMR_MATCH, tmr.VIRT0 } } -- int_select will wait for either INT_GPIO_NEGEDGE or INT_TMR_MATCH to become active print( int_select( ints ) )
Note that the two mechanisms (interrupt handlers and interrupt polling) can be used at the same time as long as they are applied to different interrupt id/resource number pairs. This is why it makes sense to write the int_select function above in Lua instead of C: it keeps the Lua VM running, so Lua interrupt handlers can be executed.
Interrupt handlers in C
The interrupt subsystem has also a basic C API that can be used to implement portable eLua components and modules. It is enabled by defining BUILD_C_INT_HANDLERS in platform_conf.h. It is defined in inc/elua_int.h and has 2 functions:
- elua_int_c_handler elua_int_set_c_handler( elua_int_id inttype, elua_int_c_handler phandler )
-
Sets the interrupt handler for interrupt inttype to phandler and returns the previous interrupt handler for interrupt inttype.
- elua_int_c_handler elua_int_get_c_handler( elua_int_id inttype )
-
Returns the interrupt handler for interrupt inttype
elua_int_c_handler is a function that doesn’t return anything and receives a single parameter of type elua_int_resnum to differentiate between the sources (GPIO pin, UART id, timer id and so on) that can trigger the interrupt inttype. This is similar in functionality with the Lua handlers.
To work with interrupts from C code use these functions defined by the CPU platform interface:
- int platform_cpu_set_interrupt( elua_int_id id, elua_int_resnum resnum, int status )
-
Enable (status = PLATFORM_CPU_ENABLE) or disable (status = PLATFORM_CPU_DISABLE) interrupt id for resource resnum.
- int platform_cpu_get_interrupt( elua_int_id id, elua_int_resnum resnum )
-
Returns 1 if interrupt id is enabled for resource resnum, 0 otherwise.
- int platform_cpu_get_interrupt_flag( elua_int_id id, elua_int_resnum resnum, int clear )
-
Get interrupt flag for interrupt id and resource resnum, clear interrupt flag if it is set and clear is 1, leave it untouched otherwise.
Since elua_int_set_c_handler returns the previous handler, it is easy to chain the interrupt handlers from different system components. To ensure correct operation, every C module that needs access to interrupt handlers should use this sequence:
#include "elua_int.h" static elua_int_c_handler prev_handler; static void int_handler( elua_int_resnum resnum ); void module_init() { int id = SOME_INT_ID; platform_cpu_set_interrupt( id, some_resnum, PLATFORM_CPU_ENABLE ); prev_handler = elua_int_set_c_handler( id, int_handler ); } static void int_handler( elua_int_resnum resnum ) { // Note: prev_handler can also be called at the end of int_handler if( prev_handler ) prev_handler( resnum ); // (Optional) Check resnum and return if the interrupt was fired by a different resource if( resnum != some_resnum ) return; // Actual interrupt handler code comes here }
Sharing an interrupt between Lua and C
As seen above the eLua interrupt subsystem consists of two parts: the C side interrupt support and the Lua side interrupt support. When an interrupt handler is enabled on both sides (C and Lua) the interrupt subsystem knows to enqueue the interrupt for the Lua handler and to call the C interrupt handler directly. When the same interrupt (with the same ID) is handled by both the C and Lua code it becomes a shared interrupt. Shared interrupts might be problematic in some scenarios. To avoid potential issues one should should try to remember a few simple rules:
- Always check your resource number.
-
The Lua side might enable an interrupt for a resource number while the C side might enable the same interrupt for another resource number. The effect of this is that both interrupt handlers (in Lua and C) will be called with both resource numbers (since there is a single interrupt handler per interrupt ID). Thus each side should check for the resource number that it needs before acting on the interrupt.
- Disable your handlers when you don’t need them anymore.
-
Starting from the same scenario as above (a shared interrupt with a resource number on the C side and another resource number on the Lua side) let’s assume that at some point you don’t need to receive interrupts on the Lua side anymore. You might think that calling cpu.cli with the Lua resource number is enough to acomplish this but this is not the case. Since the C side still has a resource number active your Lua handler will still be called with the resource number from the C side. If you followed the first rule (above) your code will behave as expected but it will run slower because of the interrupt support overhead. To completely inhibit the interrupt handler on the Lua side call cpu.set_int_handler with nil for the 'handler' argument. After this the Lua interrupt subsystem will not queue interrupts for the given interrupt ID and thus the handler won’t be called anymore. You can re-enable the handler by calling cpu.set_int_handler with an actual function for the 'handler' argument.
The same holds true for the C side. To disable the handler on the C side call elua_int_set_c_handler with NULL for the 'handler' argument. To re-enable the C handler call elua_int_set_c_handler with an actual function for the 'handler' argument. - Use cpu.cli and cpu.sei carefully.
-
The cpu.cli and cpu.sei functions in the CPU module are wrappers for the platform_cpu_set_interrupt function described above. This means that eventually they manipulate hardware interrupts directly. If you disable an interrupt ID/resource number pair (with cpu.cli) that is used by the C side the C interrupt handler won’t be called anymore and eLua will most likely malfuction. Most of the time this isn’t an issue since the Lua code is not supposed to enable/disable interrupts used by the C code. It might become a real issue when Lua and C share the same interrupt ID/resource number pair. In this case it is recommended that the Lua code doesn’t call cpu.cli at all, instead it should simply set an interrupt handler and service the interrupt. By contrast, cpu.sei can be called even if the interrupt is already enabled.