嗨~各位看官好啊~
昨天咱們已經把一個最基本的 GATT server 完成了~
看官們說不定會有種:「哼~不外如是!閃開!讓專業的來~」
是的~此等微末之技對各位小姐、公子們自然是分分鐘的事~
但說明這種雜事還是讓小的本喵代勞吧~
在 IDS server 裡,咱們在 BLE ISR 內使用了 micropython.schedule(),希望它能在 ISR 結束後,儘快安排函數在主執行緒裡執行。為什麼不讓函數直接在 ISR 內調用,而要推遲呢?至少有以下原因:
雖然咱們昨天開發的 IDS server 似乎已經能正常運行了,但至少以官方開發文件的角度來看的話,咱們在 ISR 內所做的,有幾處都是嚴重違反 MicroPython 的設計的!
根據 Writing Interrupt Handlers 的描述,ISR 的設計有幾項重要準則:
而文件的推薦做法則是:
對於什麼是「避免記憶體分配」這點,頗讓本喵感到困惑,因為在 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() 滿足以下條件:
運行以下程式來觀察 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 與其進行連接與斷線測試。以下是可能的運行結果:
以下是執行順序表:
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 |
由此次的結果可觀察到:
本喵今天提到了 MicroPython 環境下,幾個設計程式時需要注意的點,雖然在咱們使用的開發版上即使不遵守,也似乎不會有問題,但本喵基本上還是會照著文件準則來設計,雖然這些限制讓一些設計需要迂迴達成目的。說不定哪天才驚覺自己誤解了文件的意思!或 MicroPython 的 ESP32 部分早已解決這問題了?!