@@ -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```
804878Hardware Timer Overflow (counter wraps from period→0)
@@ -810,94 +884,151 @@ TIM2_IRQHandler() [stm32_it.c:705]
810884 │ IRQ_ENTER(TIM2_IRQn)
811885 ▼
812886timer_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 ▼
815889timer_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 ▼
8921002Return to timer_handle_irq_channel()
893- │ Check result, disable if error
1003+ │ If result < 0: disable callback, set to None
8941004 ▼
8951005Return to TIM2_IRQHandler()
8961006 │ IRQ_EXIT(TIM2_IRQn)
8971007 ▼
8981008Hardware 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
9031034Timer configured at 10MHz (100ns per tick). Warm-state averages after initial cache fills.
0 commit comments