浅析MicroPython系统底层回调机制

简介: 浅析MicroPython系统底层回调机制

嵌入式开发中,多数外设接口的事件通知都是通过回调函数实现的,这体现在Timer,UART,GPIO等外设。部分模块的状态通知也是通过回调实现的,比如网络状态。

常规的基于C语音的开发,ISR(中断回调函数)工作在系统进程/线程的上下文,回调通知机制容易控制。但是在MicroPython中,python应用工作在虚拟机进程的上下文,中断回调函数发生在C底层进程的上下文,C进程同python虚拟机进程是相互隔离的,所以直接的调用是不通的。
 



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.    
  5.     //1 检查调度队列是否已满,队列未满的情况下才可以继续注入回调函数
  6.     if (!mp_sched_full()) {
  7.         if ( MP_STATE_VM(sched_state) == MP_SCHED_IDLE) {
  8.            
  9.             //2 设置调度状态,方便后续虚拟机主线程查询执行
  10.             MP_STATE_VM(sched_state) = MP_SCHED_PENDING;
  11.         }
  12.        
  13.         //3 增加调度队列的索引并注入回调函数
  14.         uint8_t iput = IDX_MASK( MP_STATE_VM(sched_idx) + MP_STATE_VM(sched_len)++);
  15.         MP_STATE_VM(sched_queue)[iput].func = function;
  16.         MP_STATE_VM(sched_queue)[iput].arg = arg;
  17.        
  18.          //4 回调注入成功,返回true
  19.         ret = true;
  20.     } else {
  21.        
  22.         //5 调度队列已满,返回false
  23.         ret = false;
  24.     }
  25.     MICROPY_END_ATOMIC_SECTION(atomic_state);
  26.     return ret;
  27. }

 

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);
  14.         }
  15.     }
  16. }

 

在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.     } else {
  12.         MICROPY_END_ATOMIC_SECTION(atomic_state);
  13.     }
  14.     mp_sched_unlock();
  15. }

 

总结

 

至此我们完成了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") ;
  7.         }
  8.     }
  9. }

 

开发者支持

HaaS轻应用(Python)继承了Python易学易用的特点,同时提供了基于嵌入式硬件的基础库封装,让开发者可以很方便的通过交互式的环境,实时进行嵌入式开发,让嵌入式开发也变得简单方便。

如需更多技术支持,可加入钉钉开发者群,享受一对一的技术支持。

 

 

相关文章
|
2月前
|
消息中间件 Linux API
跨进程通信设计:Qt 进程间通讯类全面解析
跨进程通信设计:Qt 进程间通讯类全面解析
83 0
|
5月前
|
存储 API Windows
8.1 Windows驱动开发:内核文件读写系列函数
在应用层下的文件操作只需要调用微软应用层下的`API`函数及`C库`标准函数即可,而如果在内核中读写文件则应用层的API显然是无法被使用的,内核层需要使用内核专有API,某些应用层下的API只需要增加Zw开头即可在内核中使用,例如本章要讲解的文件与目录操作相关函数,多数ARK反内核工具都具有对文件的管理功能,实现对文件或目录的基本操作功能也是非常有必要的。
58 0
8.1 Windows驱动开发:内核文件读写系列函数
|
11月前
|
存储 API
驱动开发:内核文件读写系列函数
在应用层下的文件操作只需要调用微软应用层下的`API`函数及`C库`标准函数即可,而如果在内核中读写文件则应用层的API显然是无法被使用的,内核层需要使用内核专有API,某些应用层下的API只需要增加Zw开头即可在内核中使用,例如本章要讲解的文件与目录操作相关函数,多数ARK反内核工具都具有对文件的管理功能,实现对文件或目录的基本操作功能也是非常有必要的。
243 0
|
Linux
树莓派内核驱动编写——添加与调用
树莓派内核驱动编写——添加与调用
404 0
驱动开发:内核枚举进程与线程ObCall回调
在笔者上一篇文章`《驱动开发:内核枚举Registry注册表回调》`中我们通过特征码定位实现了对注册表回调的枚举,本篇文章`LyShark`将教大家如何枚举系统中的`ProcessObCall`进程回调以及`ThreadObCall`线程回调,之所以放在一起来讲解是因为这两中回调在枚举是都需要使用通用结构体`_OB_CALLBACK`以及`_OBJECT_TYPE`所以放在一起来讲解最好不过。
287 0
驱动开发:内核枚举进程与线程ObCall回调
|
监控
驱动开发:内核监控FileObject文件回调
本篇文章与上一篇文章`《驱动开发:内核注册并监控对象回调》`所使用的方式是一样的都是使用`ObRegisterCallbacks`注册回调事件,只不过上一篇博文中`LyShark`将回调结构体`OB_OPERATION_REGISTRATION`中的`ObjectType`填充为了`PsProcessType`和`PsThreadType`格式从而实现监控进程与线程,本章我们需要将该结构填充为`IoFileObjectType`以此来实现对文件的监控,文件过滤驱动不仅仅可以用来监控文件的打开,还可以用它实现对文件的保护,一旦驱动加载则文件是不可被删除和改动的。
323 0
驱动开发:内核监控FileObject文件回调
|
小程序 前端开发 JavaScript
小程序同步异步
小程序同步异步
154 0