天天看點

淺析MicroPython系統底層回調機制1、建立ISR線程虛拟化環境2、Looper-Handler模式總結 開發者支援

嵌入式開發中,多數外設接口的事件通知都是通過回調函數實作的,這展現在Timer,UART,GPIO等外設。部分子產品的狀态通知也是通過回調實作的,比如網絡狀态。

正常的基于C語音的開發,ISR(中斷回調函數)工作在系統程序/線程的上下文,回調通知機制容易控制。但是在MicroPython中,python應用工作在虛拟機程序的上下文,中斷回調函數發生在C底層程序的上下文,C程序同python虛拟機程序是互相隔離的,是以直接的調用是不通的。

淺析MicroPython系統底層回調機制1、建立ISR線程虛拟化環境2、Looper-Handler模式總結 開發者支援

MicroPython提供了兩種方式實作C底層程序到Python虛拟機程序的通信,實作了底層回調函數到Python應用層的通知。

接下來我們以Timer子產品為例,詳細分析兩種回調機制的原理,以便大家擴充自己的子產品到MicroPython系統中,共同豐富發展python輕應用生态。

1、建立ISR線程虛拟化環境

建立并初始化ISR線程虛拟化環境,以便ISR線程能獲得跟Python虛拟機程序相同的上下文。

  1. //1 擷取并儲存目前虛拟機線程狀态
  2. void *old_state = mp_thread_get_state();
  3. //2 配置設定并設定ISR線程的狀态資訊,後續初始化均作用在該線程狀态上
  4. mp_state_thread_t ts;
  5. mp_thread_set_state(&ts);
  6. //3 初始化ISR新虛拟機線程的堆棧指針, +1表示在跟指針掃描中需要包含ts資訊
  7. mp_stack_set_top(&ts + 1);
  8. //4 根據ISR線程的堆棧大小設定新線程虛拟機堆棧大小,堆棧大小依賴于ISR線程堆棧,在不同的子產品中該值會
  9. //  有所變化。(痛點1)
  10. mp_stack_set_limit(1024);
  11. //5 傳遞目前虛拟機線程本地和全局狀态資訊到新建立線程中
  12. mp_locals_set(mp_locals_get());
  13. mp_globals_set(mp_globals_get());
  14. //6 禁止虛拟機線程排程,防止虛拟機切換到其他MicroPython線程
  15. mp_sched_lock();
  16. //7 屏蔽記憶體配置設定
  17. gc_lock();
  18. //8 執行MicroPython APIs回調,完成C底層到Python應用層回調 (痛點2)
  19. mp_call_function_1_protected(callback, MP_OBJ_FROM_PTR(arg));
  20. //9 使能記憶體配置設定
  21. gc_unlock();
  22. //10 使能虛拟機線程排程
  23. mp_sched_unlock();
  24. //11 恢複虛拟機線程狀态到第一步儲存的狀态
  25. mp_thread_set_state(old_state);

由ISR層調用到Python線程共計需要11步才能完成,并且存在兩處痛點:

  • 第4步需要評估ISR線程的堆棧大小來設定新線程虛拟環境的堆棧,不容易實作。
  • 第8步需要根據ISR的回調參數數目确定函數調用,當有更多的回調參數時,需要把多個參數轉換成字典變量進行回調。目前MicroPython提供兩個回調函數:
  1. mp_obj_t mp_call_function_1_protected(mp_obj_t fun, mp_obj_t arg);
  2. mp_obj_t mp_call_function_2_protected(mp_obj_t fun, mp_obj_t arg1, mp_obj_t arg2);

可以看出,上述方法雖然能夠實作ISR到Python應用層的回調,但是需要11步才能完成且新線程的堆棧大小不容易評估。

是否可以考慮一種新的機制呢?在ISR線程把Python應用層傳過來的回調函數句柄注入到虛拟機環境,ISR線程僅需要通知Python主線程,在Python主線程查詢并實作回調函數,如此則不需要建立新虛拟線程。這就引入第二種回調機制:Looper-Handler模式。

2、Looper-Handler模式

MicroPython提供了mp_sched_schedule函數,允許ISR注冊回調函數到虛拟機環境中。

Python主線程在解析執行py代碼的時候,在不同的狀态下檢查虛拟機排程狀态,進而判決是否需要執行回調函數。

  1. bool MICROPY_WRAP_MP_SCHED_SCHEDULE(mp_sched_schedule)(mp_obj_t function, mp_obj_t arg) {
  2.     mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
  3.     bool ret;
  4.     //1 檢查排程隊列是否已滿,隊列未滿的情況下才可以繼續注入回調函數
  5.     if (!mp_sched_full()) {
  6.         if (MP_STATE_VM(sched_state) == MP_SCHED_IDLE) {
  7.             //2 設定排程狀态,友善後續虛拟機主線程查詢執行
  8.             MP_STATE_VM(sched_state) = MP_SCHED_PENDING;
  9.         }
  10.         //3 增加排程隊列的索引并注入回調函數
  11.         uint8_t iput = IDX_MASK(MP_STATE_VM(sched_idx) + MP_STATE_VM(sched_len)++);
  12.         MP_STATE_VM(sched_queue)[iput].func = function;
  13.         MP_STATE_VM(sched_queue)[iput].arg = arg;
  14.          //4 回調注入成功,傳回true
  15.         ret = true;
  16.     } else {
  17.         //5 排程隊列已滿,傳回false
  18.         ret = false;
  19.     }
  20.     MICROPY_END_ATOMIC_SECTION(atomic_state);
  21.     return ret;
  22. }

MicroPython通過預編譯參數MICROPY_SCHEDULER_DEPTH設定排程隊列的深度,預設情況下為4。

  1. // Maximum number of entries in the scheduler
  2. #ifndef MICROPY_SCHEDULER_DEPTH
  3. #define MICROPY_SCHEDULER_DEPTH (4)
  4. #endif

系統封裝了MICROPY_EVENT_POLL_HOOK宏定義,在REPL(

互動式解釋器

)模式或其他情形下需要立刻執行回調函數的時候調用該宏,以觸發虛拟機線程調用 mp_handle_pending(bool) ,完成對mp_handle_pending_tail函數的調用,最終實作對注入到排程隊列函數的回調。

  1. #define MICROPY_EVENT_POLL_HOOK \
  2.     do { \
  3.         extern void mp_handle_pending(bool); \
  4.         mp_handle_pending(true); \
  5.         MICROPY_PY_USOCKET_EVENTS_HANDLER \
  6.         MP_THREAD_GIL_EXIT(); \
  7.         MP_THREAD_GIL_ENTER(); \
  8.     } while (0);
  1. // A variant of this is inlined in the VM at the pending exception check
  2. void mp_handle_pending(bool raise_exc) {
  3.     if (MP_STATE_VM(sched_state) == MP_SCHED_PENDING) {
  4.         mp_uint_t atomic_state = MICROPY_BEGIN_ATOMIC_SECTION();
  5.         // Re-check state is still pending now that we're in the atomic section.
  6.         if (MP_STATE_VM(sched_state) == MP_SCHED_PENDING) {
  7.             mp_obj_t obj = MP_STATE_VM(mp_pending_exception);
  8.             if (obj != MP_OBJ_NULL) {
  9.                 ...
  10.             }
  11.             mp_handle_pending_tail(atomic_state);
  12.         } else {
  13.             MICROPY_END_ATOMIC_SECTION(atomic_state);

在lexer(詞法分析器)模式下則會直接調用mp_handle_pending_tail函數實作回調觸發,這裡我們不做詳細的分析,感興趣的同學可以參考vm.c檔案中的mp_execute_bytecode函數實作。

  1. // This function should only be called by mp_handle_pending,
  2. // or by the VM's inlined version of that function.
  3. void mp_handle_pending_tail(mp_uint_t atomic_state) {
  4.     MP_STATE_VM(sched_state) = MP_SCHED_LOCKED;
  5.     if (!mp_sched_empty()) {
  6.         mp_sched_item_t item = MP_STATE_VM(sched_queue)[MP_STATE_VM(sched_idx)];
  7.         MP_STATE_VM(sched_idx) = IDX_MASK(MP_STATE_VM(sched_idx) + 1);
  8.         --MP_STATE_VM(sched_len);
  9.         MICROPY_END_ATOMIC_SECTION(atomic_state);
  10.         mp_call_function_1_protected(item.func, item.arg);
  11.     mp_sched_unlock();

總結

至此我們完成了MicroPython中兩種不同的C底層到Python應用層回調機制,可以看出第二種方式僅需要調用一個函數即可實作回調注入,極大地友善了開發者。

最後我們貼出Timer子產品中ISR函數的示例代碼供大家參考。

  1. STATIC void driver_timer_isr(void *self_in) {
  2.     driver_timer_obj_t *self = (driver_timer_obj_t*)self_in;
  3.     if (self->callback != mp_const_none) {
  4.         bool ret = mp_sched_schedule(self->callback, MP_OBJ_FROM_PTR(self));
  5.         if(ret == false) {
  6.             printf("[utility]: schedule queue is full !!!!\r\n");

開發者支援

HaaS輕應用(Python)繼承了Python易學易用的特點,同時提供了基于嵌入式硬體的基礎庫封裝,讓開發者可以很友善的通過互動式的環境,實時進行嵌入式開發,讓嵌入式開發也變得簡單友善。

如需更多技術支援,可加入釘釘開發者群,享受一對一的技術支援。

淺析MicroPython系統底層回調機制1、建立ISR線程虛拟化環境2、Looper-Handler模式總結 開發者支援