Add VM hook after every opcode (MICROPY_VM_HOOK_OPCODE)
My OS is cooperative multitasking i.e. the context switch must be explicitly initiated via RTOS.yield() which means that in order for me to be able to integrate uPython into my RTOS, I have to find a spot to place RTOS.yield() call and I've found it.
Original code, vm.c
https://github.com/micropython/micropython/blob/cb9da2279b021ea7a916cbf16dd0e05c009c22bb/py/vm.c#L136
#define DISPATCH() do { \
TRACE(ip); \
MARK_EXC_IP_GLOBAL(); \
goto *entry_table[*ip++]; \
} while (0)
Modified code:
#define DISPATCH() do { \
TRACE(ip); \
MARK_EXC_IP_GLOBAL(); \
RTOS.yield();\
goto *entry_table[*ip++]; \
} while (0)
But this is not efficient because it yields after every-single opcode and I don't need it to break after every single opcode.
The next optimization step is to say only yield every X opcodes
#define DISPATCH() do { \
TRACE(ip); \
MARK_EXC_IP_GLOBAL(); \
if(counter++ % NUM_OPS_EXC == 0) RTOS.yield();\
goto *entry_table[*ip++]; \
} while (0)
But I don't really care how many opcodes are executed, I care how much time is taken i.e. 5us, 50us, etc so what I'd really like to do is to have a downcounter that is decremented by an ISR and saturates (doesn't wrap around).
#define DISPATCH() do { \
TRACE(ip); \
MARK_EXC_IP_GLOBAL(); \
if(DownCounter == 0) RTOS.yield();\
goto *entry_table[*ip++]; \
} while (0)
In this config, total execution time is DownCounter timeout + longest opcode execution time
Since #define DISPATCH() already exists, why even create this issue? In a future refactoring of uPython, #define DISPATCH() may be removed and I'll have to find another point to "hack-in" my coop-multitasking fix and it may not be as clean and simple as today's.
I request a placeholder (construct) - macro, function callback, etc - where user-defined RTOS.yield() can be inserted.
Given today's layout, the following makes sense to me:
#define DISPATCH() do { \
TRACE(ip); \
MARK_EXC_IP_GLOBAL(); \
RTOS_YIELD();\
goto *entry_table[*ip++]; \
} while (0)
and if MICROPY_OPT_COMPUTED_GOTO is not defined, you'd have
#define DISPATCH() RTOS_YIELD(); break
In mpconfigport.h
#define RTOS_YIELD()\
if(counter++ % NUM_OPS_EXC == 0) RTOS.yield();\
and in mpconfig.h, you'd add
#ifndef RTOS_YIELD()
#define RTOS_YIELD()
#endif
py/vm: Add macros to hook into various points in the VM.
These can be used to insert arbitrary checks, polling, etc into the VM.
They are left general because the VM is a highly tuned loop and it should
be up to a given port how that port wants to modify the VM internals.
One common use would be to insert a polling check, but only done after
a certain number of opcodes were executed, so as not to slow down the VM
too much. For example:
#define MICROPY_VM_HOOK_COUNT (30)
#define MICROPY_VM_HOOK_INIT static uint vm_hook_divisor = MICROPY_VM_HOOK_COUNT
#define MICROPY_VM_HOOK_POLL if (--vm_hook_divisor == 0) { \
vm_hook_divisor = MICROPY_VM_HOOK_COUNT;
extern void vm_hook_function(void);
vm_hook_function();
}
#define MICROPY_VM_HOOK_LOOP MICROPY_VM_HOOK_POLL
#define MICROPY_VM_HOOK_RETURN MICROPY_VM_HOOK_POLL
Note that the vm_hook_divisor variable is static so that it doesn't take up any stack (and thereby increase the stack usage of all Python calls). This is acceptable for the case where there is only 1 VM/runtime (usually the case). Note that in this case nested calls will use the same divisor variable, but that's actually desirable because if the divisor is large then you want the hook function to be eventually called, even if the dynamic execution of the script is lots of nested calls. Certain ports may want this divisor to be a local variable, and they can easily configure it that way (but would need to be careful because the divisor is reset on each entry to the VM).
This way of hooking is polling based. There is a more efficient way, interrupt based, where an external interrupt of some kind can set mp_pending_exception, for which there is already a check. I did not add any extra hooks for the pending exception check in this PR, but eventually it would be a good idea (especially for soft interrupts).