iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
Software Development

以MicroPython在ESP32上實作Insulin Delivery Service系列 第 3

Day 03 - ISR 與 micropython.schedule 的限制

  • 分享至 

  • xImage
  •  

嗨~各位看官好啊~
昨天咱們已經把一個最基本的 GATT server 完成了~
看官們說不定會有種:「哼~不外如是!閃開!讓專業的來~」
是的~此等微末之技對各位小姐、公子們自然是分分鐘的事~
但說明這種雜事還是讓小的本喵代勞吧~

在 IDS server 裡,咱們在 BLE ISR 內使用了 micropython.schedule(),希望它能在 ISR 結束後,儘快安排函數在主執行緒裡執行。為什麼不讓函數直接在 ISR 內調用,而要推遲呢?至少有以下原因:

  • ISR 是在硬體中斷時被呼叫,若在 ISR 內耗費太多時間,將延後其他 IRQ 的處置。
  • ISR 的實作有著若干限制,而咱們並不清楚呼叫的函數是否會違反這些規定。

雖然咱們昨天開發的 IDS server 似乎已經能正常運行了,但至少以官方開發文件的角度來看的話,咱們在 ISR 內所做的,有幾處都是嚴重違反 MicroPython 的設計的!

根據 Writing Interrupt Handlers 的描述,ISR 的設計有幾項重要準則:

  • 保持 ISR 盡可能簡短明了。
  • ISR 應該只處理在中斷事件發生後,必須立即執行的操作。
  • 可以推遲的操作應委託給主程式循環。
  • ISR 應盡快將控制權回傳給主循環。

而文件的推薦做法則是:

  • 使代碼盡可能簡短。
  • 避免記憶體分配:不附加項目到 list,或插入到 dictionary 中,不使用浮點數(因浮點數在 Python 裡是物件)。
  • 考慮使用 micropython.schedule() 來解決上述限制。
  • 如果 ISR 須和主程式之間共用資料,使用預先分配的 bytearray 或 array.array。
  • 當主程式和 ISR 之間共用資料,在主程式存取資料前停用中斷;等存取完成後,立即重新啟用中斷。
    Interrupt Related Functions
  • 分配緊急異常緩衝區。

對於什麼是「避免記憶體分配」這點,頗讓本喵感到困惑,因為在 IDS server 的 ISR 實作中,咱們確實分配了記憶體,也就是 f-string 的字串拼接,但這行為並沒有在 ESP32-DevKitC (WROOM-32D) 上引發任何可見異常;且即使咱們硬是在 ISR 裡建立 list 等結構,或是新增項目到這些結構裡,server 也依然能正常運行。

依官方文件在 BLE.irq() 的說明:

As an optimisation to prevent unnecessary allocations, the addr, adv_data, char_data, notify_data, and uuid entries in the tuples are read-only memoryview instances pointing to bluetooth’s internal ringbuffer, and are only valid during the invocation of the IRQ handler function. If your program needs to save one of these values to access after the IRQ handler has returned (e.g. by saving it in a class instance or global variable), then it needs to take a copy of the data, either by using bytes() or bluetooth.UUID().

在 BLE ISR 裡會傳進 addr、 adv_data、char_data、notify_data、和 uuid 等 tuple 型態的資料,但為了避免不必要的記憶體分配,這些資料都是指向藍牙內部環形緩衝區的唯讀 memoryview 實例,並且僅在 BLE ISR 期間有效。

而無論是因這些資料處理耗時的考量,所以需要 micropython.schedule() 調度;或因需要保存,以待未來使用,都勢必需要這些資料的副本。咱們當然可以預先配置記憶體來保存,但因為 MicroPython 在 ESP32-DevKitC (WROOM-32D) 上的記憶體有些限制,所以預先配置不一定是合適的方案。MicroPython 建議使用以下方式來保存這些參數:

connected_addr = bytes(addr)  # equivalently: adv_data, char_data, or notify_data
matched_uuid = bluetooth.UUID(uuid)

可是這說明也令本喵加深了疑惑,因為 bytes 與 bluetooth.UUID 應該都會進行記憶體分配的行為,為何這樣的行為是被允許的呢?

因官方文件明確記載:

Avoid memory allocation: no appending to lists or insertion into dictionaries, no floating point.

所以或許會認為只要避開以上三種情境就好,但文件也說明:

ISR’s cannot create instances of Python objects. This is because MicroPython needs to allocate memory for the object from a store of free memory block called the heap. This is not permitted in an interrupt handler because heap allocation is not re-entrant.

若直譯此說明,只要會由 heap 分配記憶體的行為應該都是禁止的。目前並不清楚是什麼原因讓 ESP32-DevKitC (WROOM-32D) 能正常運作,也不清楚是否在其他硬體平台上都沒問題。或許單純因為記憶體還很夠?或咱們很幸運?或 ... 文件已跟不上實作了?!但既然官方文件有明確的推薦做法,往後實作都會儘量依照官方建議來進行,除非真的必要,且經過足夠的測試,能保證此例外可行。

而對於記憶體分配有一點是較不明顯的情況,就是使用綁定方法。如文件提供的程式片段:

class Foo():
    def __init__(self):
        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):
        # Passing self.bar would cause allocation.
        micropython.schedule(self.bar, 0)

self.bar() 就是所謂的綁定方法。當我們執行 micropython.schedule(self.bar, 0),為了讓 schedule() 能呼叫 self.bar,會建立對綁定方法的參考,此時會建立物件實例,這意謂著,在 ISR 中無法將綁定方法傳遞給函數。為了解決這問題,在類別建構函式中建立一個綁定方法的參考,並在 ISR 中傳遞該參考:

class Foo():
    def __init__(self):
        self.bar_ref = self.bar  # Allocation occurs here
        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):
        # Passing self.bar would cause allocation.
        micropython.schedule(self.bar_ref, 0)

但依然,在 ESP32-DevKitC (WROOM-32D) 上,即使在 ISR 直接將綁定方法傳遞給 schedule(),也沒有出現任何問題 ...

在「分配緊急異常緩衝區」建議上,如果在 ISR 中發生錯誤,MicroPython 將無法產生錯誤報告,因此建議在 boot.py 或 main.py 一開頭就建立特殊的緩衝區 micropython.alloc_emergency_exception_buf

import micropython
micropython.alloc_emergency_exception_buf(100)

有幾點要特別注意,micropython.schedule() 滿足以下條件:

  • 已排程的函數永遠不會搶先其他已排程的函數。
    意即被排程的函數是有順序性的,他們被放置在一個有限的 queue 中。
  • 調度函數總是在「操作碼之間」執行,這保證所有基本的 Python 操作都具備原子性,如附加項目到 list。
  • 在臨界區內,如 ISR,被調度的函數不會被執行。
  • 若 queue 已滿,則 schedule() 將引發 RuntimeError。

運行以下程式來觀察 ISR、被調度函數、與主程式循環之間的執行順序:

def f1(arg):
    log("f1")


def f2(arg):
    global _f4_arg
    log("f2 blocked")
    _f4_arg += 1
    asyncio.create_task(f4(_f4_arg))
    time.sleep(1)


def f3(arg):
    log("f3")


async def f4(arg):
    log(f"f4 async: {arg}")


def ble_isr(event, data):
    log(f"event: {event}")
    micropython.schedule(f1, None)
    micropython.schedule(f2, None)
    micropython.schedule(f3, None)


async def main():
    while True:
        log("Main loop")
        await asyncio.sleep_ms(1000)

完整程式請參考 test_schedule.py

使用 nRF Connect app 與其進行連接與斷線測試。以下是可能的運行結果:
https://ithelp.ithome.com.tw/upload/images/20250803/20177799cnVAoT2n7Y.png

以下是執行順序表:

Line Current Queue async
2 event f1, f2, f3
3 f1 f2, f3
4 f2 f3 f4
5 event f3, f1, f2, f3 f4
6 event f3, f1, f2, f3, f1, f2, f3 f4
7 f3 f1, f2, f3, f1, f2, f3 f4
8 f1 f2, f3, f1, f2, f3 f4
9 f2 f3, f1, f2, f3 f4, f4
10 f3 f1, f2, f3 f4, f4
11 f1 f2, f3 f4, f4
12 f4 f2, f3 f4
13 f2 f3 f4, f4
14 f3 f4, f4
15 main f4, f4
16 f4 f4
17 f4
18 main
21 event f1, f2, f3
22 f1 f2, f3
23 f2 f3 f4
24 f3 f4
25 f4

由此次的結果可觀察到:

  • 被 schedule() 調度的函數是以 FIFO 的順序呼叫,而被 async 修飾的函數則不在此限。
  • 被 schedule() 所調度的函數的優先權未必高於 async 修飾的函數。
    比如在 Line 12,即使 Queue 裡還有其他函數,但 MicroPython 是安排 async 函數 f4 執行。
  • async 函數之間未必有一定的順序關係
    比如預期 main() 是每秒被呼叫一次,但實際執行時,main() 在 26~28 秒之間完全沒有執行機會。反而在 28 秒時,f4 還有執行一次。
  • 被 schedule() 調度的函數 f2,它在內部呼叫 sleep 後,會鎖住主執行緒和 schedule() 的調度。
  • 若快速點擊 nRF Connect 上的 CONNECT/DISCONNECT,那麼可以發現,雖然 f2 因呼叫 sleep,鎖住了主執行緒和 schedule,但是並不會影響 BLE ISR 被執行。

本喵今天提到了 MicroPython 環境下,幾個設計程式時需要注意的點,雖然在咱們使用的開發版上即使不遵守,也似乎不會有問題,但本喵基本上還是會照著文件準則來設計,雖然這些限制讓一些設計需要迂迴達成目的。說不定哪天才驚覺自己誤解了文件的意思!或 MicroPython 的 ESP32 部分早已解決這問題了?!


上一篇
Day 02 - 開發基本 GATT 伺服器
下一篇
Day 04 - 整體架構 & logger & 字串拼接效能
系列文
以MicroPython在ESP32上實作Insulin Delivery Service31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言