Skip to content

Commit b4b8842

Browse files
committed
docs: Update IRQ dispatch flow documentation with unified architecture.
Rewrites the IRQ dispatch sequence diagram to document the current unified architecture where all hard IRQ handlers are wrapped as mp_type_gen_instance objects. Adds sentinel value table, registration phase details, and per-type dispatch paths (bytecode function, native function, viper, generators, generic callable). Also updates test_viper_irq.py with cleaner variable names and direct viper callback test. Signed-off-by: Andrew Leech <[email protected]>
1 parent 4419b64 commit b4b8842

File tree

2 files changed

+268
-100
lines changed

2 files changed

+268
-100
lines changed

docs/coroutine-implementation.md

Lines changed: 205 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,81 @@ To understand where time is spent between hardware interrupt and Python callback
798798
| P13 | `timer.c:1425` | `pyb_timer_counter()` entry | Actual C function entry |
799799
| Px | Python code | `t.counter()` return | Counter value captured in Python |
800800

801-
### Complete Sequence Diagram
801+
### Complete IRQ Dispatch Flow (Unified Architecture)
802+
803+
The current implementation uses a unified dispatch architecture where all hard IRQ handlers
804+
are wrapped as `mp_type_gen_instance` objects at registration time. This eliminates type
805+
checks in the hot dispatch path and enables per-type optimizations via sentinel values
806+
in the `exc_sp_idx` field.
807+
808+
#### Phase 1: Registration (tim.callback() call)
809+
810+
```
811+
Python: tim.callback(handler)
812+
813+
814+
pyb_timer_callback() [timer.c:1509]
815+
│ Disable timer interrupt
816+
817+
mp_irq_prepare_handler(callback, parent, ishard) [mpirq.c:240]
818+
819+
├─── Is generator function (mp_type_gen_wrap or native_gen_wrap)?
820+
│ │
821+
│ ▼ YES
822+
│ mp_call_function_1(callback, parent) Instantiate generator
823+
│ │
824+
│ ▼
825+
│ mp_obj_gen_resume(gen, None, NULL) Prime to first yield
826+
│ │
827+
│ └── Ready: gen_instance with valid ip/sp at yield point
828+
829+
├─── Is bytecode function (mp_type_fun_bc)?
830+
│ │
831+
│ ▼ YES: mp_irq_wrap_bytecode_function() [mpirq.c:61]
832+
│ - Decode prelude: n_state, n_pos_args, scope_flags
833+
│ - Reject if scope_flags & GENERATOR (has yield)
834+
│ - Reject if n_pos_args != 1
835+
│ - Allocate gen_instance with state[] + extra data
836+
│ - Call mp_setup_code_state() with dummy arg
837+
│ - Cache bytecode_start in extra data
838+
│ - Set exc_sp_idx = IRQ_FUNC_BC (-2)
839+
840+
├─── Is @native function (mp_type_fun_native)?
841+
│ │
842+
│ ▼ YES: mp_irq_wrap_native_function() [mpirq.c:120]
843+
│ - Decode native prelude for n_state
844+
│ - Reject if n_pos_args != 1
845+
│ - Allocate gen_instance with state[] + extra data
846+
│ - Cache native_entry pointer
847+
│ - Set exc_sp_idx = IRQ_FUNC_NAT (-3)
848+
849+
├─── Is @viper function (mp_type_fun_viper)?
850+
│ │
851+
│ ▼ YES: mp_irq_wrap_viper_function() [mpirq.c:156]
852+
│ - Allocate gen_instance (minimal state)
853+
│ - Entry point at fun_bc->bytecode directly
854+
│ - Set exc_sp_idx = IRQ_VIPER (-4)
855+
856+
└─── Other callable (bound method, closure, etc)?
857+
858+
▼ YES: mp_irq_wrap_callable() [mpirq.c:194]
859+
- Allocate gen_instance with n_state=2
860+
- Store callable in state[0]
861+
- Set exc_sp_idx = IRQ_CALLABLE (-5)
862+
```
863+
864+
**Sentinel Values (defined in py/bc.h:169-178):**
865+
866+
| Sentinel | Value | Handler Type |
867+
|----------|-------|--------------|
868+
| `SENTINEL` | -1 | Native generator (@native def with yield) |
869+
| `IRQ_FUNC_BC` | -2 | Wrapped bytecode function |
870+
| `IRQ_FUNC_NAT` | -3 | Wrapped @native function |
871+
| `IRQ_VIPER` | -4 | Wrapped @viper function |
872+
| `IRQ_CALLABLE` | -5 | Generic callable (bound method, etc) |
873+
| 0+ | n | Bytecode generator (n = exc_stack index) |
874+
875+
#### Phase 2: Hardware IRQ Dispatch
802876

803877
```
804878
Hardware Timer Overflow (counter wraps from period→0)
@@ -810,94 +884,151 @@ TIM2_IRQHandler() [stm32_it.c:705]
810884
│ IRQ_ENTER(TIM2_IRQn)
811885
812886
timer_irq_handler(2) [timer.c:1733]
813-
│ Lookup timer object from MP_STATE_PORT
887+
│ Lookup: tim = MP_STATE_PORT(pyb_timer_obj_all)[1]
814888
815889
timer_handle_irq_channel(tim, 0, callback) [timer.c:1716]
816-
├─▶ P0: IRQ_PROFILE_CAPTURE(0) [timer.c:1724]
817-
│ Check __HAL_TIM_GET_FLAG()
818-
│ Check __HAL_TIM_GET_IT_SOURCE()
819-
│ __HAL_TIM_CLEAR_IT()
890+
│ Check __HAL_TIM_GET_FLAG() & __HAL_TIM_GET_IT_SOURCE()
891+
│ __HAL_TIM_CLEAR_IT()
820892
821-
mp_irq_dispatch(callback, tim, ishard=true) [mpirq.c:97]
822-
├─▶ P1: MP_IRQ_PROFILE_CAPTURE(1) [mpirq.c:98]
823-
│ mp_cstack_init_with_sp_here() [if STACK_CHECK]
824-
│ mp_sched_lock()
825-
│ gc_lock()
826-
├─▶ P2: MP_IRQ_PROFILE_CAPTURE(2) [mpirq.c:116]
827-
│ nlr_push(&nlr)
828-
├─▶ P3: MP_IRQ_PROFILE_CAPTURE(3) [mpirq.c:119]
829-
│ mp_obj_is_type(handler, &mp_type_gen_instance)
893+
mp_irq_dispatch(handler, tim, ishard=true) [mpirq.c:289]
894+
895+
│ [Stack check setup if MICROPY_STACK_CHECK]
896+
│ mp_sched_lock()
897+
│ gc_lock()
898+
│ nlr_push(&nlr)
899+
900+
│ ════════════════════════════════════════════════════════════
901+
│ UNIFIED DISPATCH: All handlers are mp_type_gen_instance
902+
│ ════════════════════════════════════════════════════════════
903+
904+
mp_obj_gen_resume_irq(handler, tim, &ret_val) [objgenerator.c:260]
905+
906+
│ mp_cstack_check()
907+
│ Reentrance check: if (pend_exc == NULL) return ERROR
908+
909+
│ Switch on exc_sp_idx:
910+
911+
├─── IRQ_FUNC_BC (-2): Wrapped bytecode function
912+
│ │ [objgenerator.c:275]
913+
│ │ Get extra data: bytecode_start
914+
│ │ Reset: ip = bytecode_start
915+
│ │ Reset: sp = &state[0] - 1
916+
│ │ Reset: exc_sp_idx = 0 (for VM)
917+
│ │ Place arg: state[n_state-1] = send_value
918+
│ │ pend_exc = NULL (mark running)
919+
│ │ mp_globals_set()
920+
│ ▼
921+
│ mp_execute_bytecode(&code_state, NULL) [vm.c:285]
922+
│ │ FRAME_ENTER/SETUP
923+
│ │ Execute bytecodes
924+
│ │ Return when function ends
925+
│ ▼
926+
│ Restore: exc_sp_idx = IRQ_FUNC_BC
927+
│ pend_exc = mp_const_none (mark idle)
928+
929+
├─── IRQ_FUNC_NAT (-3): Wrapped @native function
930+
│ │ [objgenerator.c:310]
931+
│ │ Get extra data: native_entry
932+
│ │ pend_exc = NULL
933+
│ │ args[0] = send_value
934+
│ ▼
935+
│ native_fun(fun_bc, 1, 0, args) Direct call
936+
│ │ ARM native code executes
937+
│ ▼
938+
│ pend_exc = mp_const_none
939+
940+
├─── IRQ_VIPER (-4): Wrapped @viper function
941+
│ │ [objgenerator.c:322]
942+
│ │ Entry = fun_bc->bytecode (direct)
943+
│ │ pend_exc = NULL
944+
│ │ args[0] = send_value
945+
│ ▼
946+
│ viper_fun(fun_bc, 1, 0, args) Direct call
947+
│ │ ARM native code executes
948+
│ ▼
949+
│ pend_exc = mp_const_none
950+
951+
├─── SENTINEL (-1): Native generator
952+
│ │ [objgenerator.c:334]
953+
│ │ Check: ip == 0? (exhausted)
954+
│ │ *sp = send_value
955+
│ │ pend_exc = NULL
956+
│ │ mp_globals_set()
957+
│ ▼
958+
│ mp_fun_native_gen_t fun(code_state, NULL)
959+
│ │ Native generator resume function
960+
│ ▼
961+
│ mp_globals_restore()
962+
│ pend_exc = mp_const_none
963+
964+
├─── IRQ_CALLABLE (-5): Generic callable wrapper
965+
│ │ [objgenerator.c:355]
966+
│ │ callable = state[0]
967+
│ │ pend_exc = NULL
968+
│ ▼
969+
│ mp_call_function_1(callable, send_value) [runtime.c:674]
970+
│ │ Full type dispatch
971+
│ │ (slowest path - bound method overhead)
972+
│ ▼
973+
│ pend_exc = mp_const_none
974+
975+
└─── Default (0+): Bytecode generator
976+
│ [objgenerator.c:365]
977+
│ Check: ip == 0? (exhausted)
978+
│ *sp = send_value
979+
│ pend_exc = NULL
980+
│ mp_globals_set()
981+
982+
mp_execute_bytecode(&code_state, NULL) [vm.c:285]
983+
│ Resume from saved ip (after yield)
984+
│ Execute until next yield or return
985+
986+
mp_globals_restore()
987+
pend_exc = mp_const_none
830988
831-
├───────────────────────────────────────────────────────────────
832-
│ FUNCTION PATH │ GENERATOR PATH
833-
├───────────────────────────────────┼───────────────────────────
834-
│ │
835-
├─▶ P4: (before call) ├─▶ P4: (before resume)
836-
│ [mpirq.c:138] │ [mpirq.c:122]
837-
▼ ▼
838-
mp_call_function_1(handler, tim) mp_obj_gen_resume(handler, tim, ...)
839-
│ [py/runtime.c:674] │ [py/objgenerator.c:153]
840-
▼ │
841-
mp_call_function_n_kw() ├─▶ P7: (gen_resume entry)
842-
│ [py/runtime.c:671] │ [objgenerator.c:154]
843-
▼ │ mp_cstack_check()
844-
fun_bc_call(handler, 1, 0, &tim) │ Check ip != 0
845-
│ [py/objfun.c:255] │ Check pend_exc != NULL
846-
├─▶ P7: (fun_bc_call entry) │ Place send_value on sp
847-
│ [objfun.c:256] │ Set pend_exc = NULL
848-
│ mp_cstack_check() │
849-
│ DECODE_CODESTATE_SIZE() ├─▶ P8: (after send_value)
850-
│ alloca()/heap for state │ [objgenerator.c:194]
851-
│ INIT_CODESTATE() │ mp_globals_set()
852-
│ - parse prelude │
853-
│ - copy args to locals │
854-
│ - init exception stack │
855-
├─▶ P8: (after INIT_CODESTATE) │
856-
│ [objfun.c:293] │
857-
│ mp_globals_set() │
858-
▼ ▼
859-
mp_execute_bytecode(code_state, ...) mp_execute_bytecode(code_state, ...)
860-
│ [py/vm.c:220] │ [py/vm.c:220]
861-
├─▶ P9: (vm entry) ├─▶ P9: (vm entry)
862-
│ [vm.c:272] │ [vm.c:272]
863-
│ FRAME_ENTER() │ FRAME_ENTER()
864-
│ FRAME_SETUP() │ FRAME_SETUP()
865-
│ Setup fastn, exc_stack │ Setup fastn, exc_stack
866-
│ nlr_push(&nlr) │ nlr_push(&nlr)
867-
│ Setup ip, sp, qstr_table │ Restore ip, sp from state
868-
│ MICROPY_VM_HOOK_INIT │ MICROPY_VM_HOOK_INIT
869-
├─▶ P10: (dispatch ready) ├─▶ P10: (dispatch ready)
870-
│ [vm.c:311] │ [vm.c:311]
871-
│ Enter dispatch_loop │ Enter dispatch_loop
872-
│ DISPATCH() → first opcode │ DISPATCH() → resume opcode
873-
│ ▼ │ ▼
874-
│ ┌────────────────────┐ │ ┌────────────────────┐
875-
│ │ Execute bytecodes │ │ │ Execute bytecodes │
876-
│ │ until t.counter() │ │ │ after yield resume │
877-
│ └────────────────────┘ │ └────────────────────┘
878-
│ ▼ │ ▼
879-
└──▶ Px: t.counter() ◀───────────┴──▶ Px: t.counter()
880-
(Python captures (Python captures
881-
TIM2->CNT here) TIM2->CNT here)
882989
═══════════════════════════════════════════════════════════════
883990
884-
▼ [After handler completes...]
885-
├─▶ P5: MP_IRQ_PROFILE_CAPTURE(5) [mpirq.c:125/140]
886-
│ nlr_pop()
887-
│ gc_unlock()
888-
│ mp_sched_unlock()
889-
├─▶ P6: MP_IRQ_PROFILE_CAPTURE(6) [mpirq.c:150]
890-
│ Restore stack limits [if STACK_CHECK]
991+
▼ [Common return handling]
992+
│ Check ret_kind:
993+
│ NORMAL: For real generators, mark exhausted (ip=0)
994+
│ YIELD: Success, handler remains active
995+
│ EXCEPTION: Print error, return -1
996+
997+
│ nlr_pop()
998+
│ gc_unlock()
999+
│ mp_sched_unlock()
1000+
│ [Restore stack limits if STACK_CHECK]
8911001
8921002
Return to timer_handle_irq_channel()
893-
Check result, disable if error
1003+
If result < 0: disable callback, set to None
8941004
8951005
Return to TIM2_IRQHandler()
8961006
│ IRQ_EXIT(TIM2_IRQn)
8971007
8981008
Hardware resumes interrupted code
8991009
```
9001010

1011+
#### Key Files Reference
1012+
1013+
| Component | File | Key Lines |
1014+
|-----------|------|-----------|
1015+
| Sentinel definitions | `py/bc.h` | 169-178 |
1016+
| Handler wrapping | `shared/runtime/mpirq.c` | 61-210 |
1017+
| Unified prepare | `shared/runtime/mpirq.c` | 240-286 |
1018+
| Unified dispatch | `shared/runtime/mpirq.c` | 289-346 |
1019+
| Resume with sentinels | `py/objgenerator.c` | 260-414 |
1020+
| Timer callback reg | `ports/stm32/timer.c` | 1509-1528 |
1021+
| Timer IRQ handler | `ports/stm32/timer.c` | 1716-1766 |
1022+
| Hardware ISR | `ports/stm32/stm32_it.c` | 705-709 |
1023+
1024+
#### Soft IRQ Path (ishard=false)
1025+
1026+
For soft IRQs, the flow is simpler:
1027+
- Generators are still instantiated and primed at registration
1028+
- Other callables are NOT wrapped (passed directly)
1029+
- At IRQ time: `mp_sched_schedule(handler, parent)` queues the callback
1030+
- Later: VM checks `sched_state`, calls `mp_call_function_1_protected()`
1031+
9011032
### Profiling Results (11 capture points)
9021033

9031034
Timer configured at 10MHz (100ns per tick). Warm-state averages after initial cache fills.

0 commit comments

Comments
 (0)