MicroPython中如何写中断处理程序(一)

社区整理的参考资料、设计资源
回复
eeemaker小王子
帖子: 3
注册时间: 2021年 1月 11日 21:30

MicroPython中如何写中断处理程序(一)

#1

帖子 eeemaker小王子 »

MicroPython提供了在适当的硬件平台上用Python语言写中断处理函数的能力。中断处理函数,即中断服务例程(ISR),被定义为回调函数,其被作为类似于定时器触发或某引脚上电压改变等事件的响应函数而执行。这些事件在常规程序代码执行中的任何时间均可能发生,也就带来了一些值得注意的问题。其中,有些问题是特定于MicroPython而存在的,有些问题则对于所有实时系统是共性存在的。该篇文档首先关注MicroPython所特有的问题,接着针对新手简要介绍了对于实时系统编程时需要注意的问题。
该篇文档中使用了一些相对模糊的词语,比如"慢"或者"尽可能地快",这是有意为之,因为执行速度具体需依赖于实际应用程序设计而言。而ISR的可接受持续运行时间长短则依赖于中断发生频率,主程序本身和有无其它并发事件的产生而言。
建议及最佳实践
这里详细总结了写中断处理程序时所推荐遵守的原则如下:
  • 保持代码尽可能地简短。
  • 避免内存分配:不要向列表追加元素或者向字典插入元素,尽量不使用浮点数。
  • 考虑使用micropython.schedule以绕过上述限制。
  • 当ISR返回多个字节数据时,使用预先分配的bytearray类型数据。如果需要在主程序和ISR之间共享多个整数值,考虑使用数组(array.array)。
  • 当需要在主程序和ISR之间共享数据时,考虑首先禁用优先级中断而在主程序中存取数据,之后及时重新使能中断(详细查看“临界区”章节)。
  • 分配一段紧急异常缓冲区(详细查看下面说明)。
MicroPython需要注意问题
1> 紧急异常缓冲区
如果在ISR中发生了错误,MicroPython并不能报告其错误信息,除非已经预先为其创建了特定的内存缓冲区。在带有中断处理的主程序中加入如下代码,可简化程序调试过程。

代码: 全选

import micropython
micropython.alloc_emergency_exception_buf(100)
紧急异常缓冲区仅能保留一段异常追踪信息,这意味着在异常处理期间,堆空间被锁定时,如果有第二个异常被抛出,则其异常追踪信息会覆盖掉原来信息,即使第二个异常被干净地处理过了也会这样。这可能会导致在随后打印出缓冲区信息后,使得异常信息看似难以理解。
2> 简洁性
有许多原因可以说明保持ISR尽可能简洁的重要性。在某事件发生后的ISR中应该立即做且仅做那些必需要做的事情,而能够被延迟的操作应该交给主程序循环去做。典型场景下,ISR中会处理引发中断的硬件设备,使其准备就绪以处理下次中断,并更新某共享数据来指示中断已经发生,最后返回主程序。其以这种方式来和主程序完成通信。ISR应尽快返回主程序循环,该点也可以说并不特定于MicroPython,其会在下面章节进行更详细解说。
3> 在ISR和主程序之间通信
通常,在ISR和主程序之间确实需要进行适当通信。最简单的方式便是通过一个或者多个共享的数据对象,其可以为全局数据对象或者通过一个类进行共享。该方式有各种使用限制和需注意的危险,将在后面章节详细介绍。整数对象,字节串和字节数组对象常被用于该场景,还有来自于array模块的数组对象,其能够存储各种数据类型,也经常被使用。
4> 使用对象方法作为回调函数
MicroPython支持该项有力的技术特性,使得ISR能够使用底层代码共享实例变量,同时也使实现设备驱动的类能够同时支持多个设备实例。下面的示例代码使得两个LED以不同的频率闪烁。

代码: 全选

import pyb, micropython
micropython.alloc_emergency_exception_buf(100)

class Foo(object):
def __init__(self, timer, led):
self.led = led
timer.callback(self.cb)
def cb(self, tim):
self.led.toggle()

red = Foo(pyb.Timer(4, freq=1), pyb.LED(1))
green = Foo(pyb.Timer(2, freq=0.8), pyb.LED(2))
该示例中,red实例将定时器4和LED1关联起来:定时器4中断发生时,red.cb()被调用,导致LED1改变状态。green实例以相似方式工作:定时器2的中断会导致green.cb()执行和LED2状态翻转。实例方法的使用有两个好处。第一,一个单独的类能够在多个硬件实例对象之间共享代码。第二,作为绑定方法,回调函数的第一个参数是self,这便使得回调函数能够访问实例数据并在连续调用之间保存状态。例如,如果上面的类在构造函数中将变量self.count置为零,cb()可以增加该计数器,然后,red和green实例将保持对各自LED状态翻转次数的独立计数。
5> 创建python对象
不能在ISR中创建python对象实例。这是因为对象实例的创建需要MicroPython从被称为堆的空闲空间中分配内存,但内存分配操作是不可重入的,所以这种操作在中断处理程序中是不被允许的。换句话说,当主程序正在执行内存分配时,可能产生中断,为了保持堆的完整性,python解释器不允许同时在ISR中进行内存分配。这样做的后果是,ISR中也不允许使用浮点运算,因为浮点数在python中均是数据对象。同样,ISR中也不能对列表追加元素。但实际使用过程中,往往很难准确地确定哪些代码执行了对象构造从而引起内存分配并最终引发错误消息:这也是倡导尽量保持ISR代码简短的另一个原因。
避免此问题的一种方法是在ISR中仅使用预分配的缓冲区。例如,在类构造函数中创建一个bytearray实例和一个布尔标志,然后ISR中仅将数据设置到缓冲区中适当位置,并置标志位。这样,当类对象被实例化时,内存分配就发生在主程序代码中,而不是在ISR中。
MicroPython库输入/输出函数通常提供使用预分配缓冲区的参数选项。例如,pyb.i2c.recv()可以接受一个可变缓冲区作为它的一个参数:这使得该函数可以在ISR中被使用。
不使用类或全局对象而创建新对象的另一种方法如下:

代码: 全选

def set_volume(t, buf=bytearray(3)):
buf[0] = 0xa5
buf[1] = t >> 4
buf[2] = 0x5a
return buf
这样,在第一次加载函数时(通常是在导入其所在模块时),编译器会实例化默认的buf参数。
当创建对绑定方法的引用时,也会发生对象实例的创建,这意味着ISR中不能将绑定方法传递给函数。一种解决方案是在类的构造函数中创建对绑定方法的引用,并在ISR中传递该引用。比如

代码: 全选

class Foo():
def __init__(self):
self.bar_ref = self.bar # 在此分配内存
self.x = 0.1
tim = pyb.Timer(4)
tim.init(freq=2)
tim.callback(self.cb)

def bar(self, _):
self.x *= 1.2
print(self.x)

def cb(self, t):
# 直接传递self.bar会引起内存分配
micropython.schedule(self.bar_ref, 0)
其它的技术还包括在构造函数中定义和实例化方法等等。
6> python对象的使用
源于python本身的工作方式,其对数据对象的进一步限制也需要考虑在内。当执行import语句时,python代码被编译成字节码,并且一行代码通常可映射到多个字节码。当代码运行时,解释器读取每个字节码,并将其作为一系列机器码指令来执行。假设中断可以在机器码指令执行时的任何时间发生,python的原本代码行可能在中断发生时只被部分执行。因此,在主循环中被修改的python对象(如集合,列表或字典)可能会在中断发生时不能保证其内部一致性。
这样导致的典型后果如下:在极少情况下,ISR可能在某数据对象被部分修改过程中的某时刻开始运行,这样当ISR尝试读取数据对象时,可能导致崩溃发生。因为这种问题通常发生在罕见的随机情况下,所以很难被诊断出来。当然,也有一些方法可以避免该问题,将在下面“临界区”章节进行描述。
清楚地明白到底是什么原因造成了对数据对象的修改十分重要。对内置类型(如字典)数据的修改容易导致问题的发生,但对bytes或bytearray类型数据的修改则一般不会导致这种问题,这是因为字节或字类型数据在内部会被转化为单独的机器码,不可被中断执行:在实时编程的术语中,其被称为原子操作。这样,用户定义的类对象中便可以尽量实例化整数,数组或字节数组,其内容对于主循环和ISR来说均相对可靠有效。
MicroPython可支持任意精度的整数,2**30-1到-2**30之间的任何数值均可被存储在一个字存储空间中,但更大的数值则会以python对象形式被存储。因此,对长整数的修改不能被视作原子性的。在ISR中使用长整数是不安全的,因为当该变量值被修改时,可能会尝试内存分配动作。
7> 克服浮点数的限制
一般来说,最好避免在ISR中使用浮点数:硬件设备通常处理整数,然后在主循环中再进行浮点数转换。然而,有一些DSP算法确实需要进行浮点运算。在有硬件浮点支持的平台上(比如pyboard),其内嵌的ARM Thumb汇编器可以解决该限制。由于在这些平台上,处理器可将浮点值存储在一个字存储空间中,这样便可以通过浮点数数组在ISR和主程序代码之间共享数据。
8> 使用micropython.schedule
该函数使得ISR能够“很快”安排回调函数,使其得以执行。通过该调用该函数,回调函数会排队等待,在堆空间未被锁定的时候再开始执行,因此,其便又能够创建python对象并使用浮点值了。并且,这也会保证回调函数会在主程序完成对python对象的任何修改之后再开始运行,那么,回调函数执行时也就不会遇到被部分修改的数据对象了。
其典型用途是处理传感器原始数据:ISR从传感器硬件获取原始数据,并重新使能中断,之后使用该函数来安排回调函数具体处理数据。
使用micropython.schedule函数安排的回调函数在编写时应遵循下面章节所述通用中断处理程序设计原则。因为这些原则能够避免由于输入/输出活动和修改共享数据所引起的问题,这些问题可能出现在任何先于主程序循环的代码中。
ISR持续时间需要综合考虑中断发生频率来进行调整。如果在前一个回调执行过程中又发生了中断,则另一个回调函数实例将排队等待执行,其将在当前回调函数执行完毕之后再开始运行。因此,持续高频率的中断会带来队列无限增长的风险,最终导致产生RuntimeError异常。
如果要传递给micropython.schedule()的回调是绑定方法,可参考"创建python对象"章节中所述建议。


注,基本翻译自官网手册,请各位大佬指正,更多可关注 eeemaker小王子公众号 哦
 

回复

  • 随机主题
    回复总数
    阅读次数
    最新文章