###### tags: `Python` `CircuitPython` `Raspberry Pi` `Pico` # CircuitPython 入門--使用 Raspberry Pi Pico 可參考官方的[教學文件](https://learn.adafruit.com/circuitpython-essentials), 以及正式的[參考文件](https://circuitpython.readthedocs.io/en/latest/docs/index.html)。 ## 腳位配置 ![腳位圖](https://cdn-shop.adafruit.com/1200x900/4864-04.png) CircuitPython 使用 [micropython.Pin 類別](https://circuitpython.readthedocs.io/en/latest/shared-bindings/microcontroller/index.html#microcontroller.Pin) 的物件來操控個別的腳位, 並用 [microcontroller.pin](https://circuitpython.readthedocs.io/en/latest/shared-bindings/microcontroller/index.html#microcontroller.Pin) 記錄晶片上個別腳位對應的 Pin 物件, 而 [board](https://circuitpython.readthedocs.io/en/latest/shared-bindings/board/index.html) 則是記錄開發板上腳位名稱對應的 Pin 物件。晶片腳位名稱是唯一的, 個別是 GPIO0~GPIO29, 每一個腳位名稱只會對應到一個 Pin 物件, 但是開發板的同一腳位可能會因為用途不同而有多個名稱, 例如 GP28 和 A2 就是同一腳位, 所以會有多個名稱對應到同一個 Pin 物件的狀況。 以下可觀察這些腳位物件在個別模組中的名稱: ```python >>> import microcontroller >>> import board >>> dir(microcontroller.pin) ['__class__', 'GPIO0', 'GPIO1', 'GPIO10', 'GPIO11', 'GPIO12', 'GPIO13', 'GPIO14', 'GPIO15', 'GPIO16', 'GPIO17', 'GPIO18', 'GPIO19', 'GPIO2', 'GPIO20', 'GPIO21', 'GPIO22', 'GPIO23', 'GPIO24', 'GPIO25', 'GPIO26', 'GPIO27', 'GPIO28', 'GPIO29', 'GPIO3', 'GPIO4', 'GPIO5', 'GPIO6', 'GPIO7', 'GPIO8', 'GPIO9'] >>> dir(board) ['__class__', 'A0', 'A1', 'A2', 'A3', 'GP0', 'GP1', 'GP10', 'GP11', 'GP12', 'GP13', 'GP14', 'GP15', 'GP16', 'GP17', 'GP18', 'GP19', 'GP2', 'GP20', 'GP21', 'GP22', 'GP25', 'GP26', 'GP26_A0', 'GP27', 'GP27_A1', 'GP28', 'GP28_A2', 'GP3', 'GP4', 'GP5', 'GP6', 'GP7', 'GP8', 'GP9', 'LED', 'SMPS_MODE', 'VOLTAGE_MONITOR'] >>> ``` 像是 Pico 板子上的內建 LED 其實就是 GPIO25, 因此以下 3 個腳位物件都是指向同一個物件: ```python >>> microcontroller.pin.GPIO25 == board.LED True >>> microcontroller.pin.GPIO25 == board.GP25 True >>> board.LED == board.GP25 True >>> ``` 要用到這個腳位時選用任何一個都可以。如果想知道哪些腳位是同個腳位, 可以執行以下程式: ```python= import microcontroller import board # 晶片上的腳位 chip_pin_names = dir(microcontroller.pin) # 開發板上標示的名稱 (同一腳位可能有多個名稱) board_pin_names = dir(board) for chip_pin_name in chip_pin_names: # 取得對應腳位的 Pin 物件 chip_pin = getattr(microcontroller.pin, chip_pin_name) if isinstance(chip_pin, microcontroller.Pin): for board_pin_name in board_pin_names: # 取得對應名稱的 Pin 物件 board_pin = getattr(board, board_pin_name) if chip_pin == board_pin: # 若是同一物件, 表示是同一腳位 # 顯示開發板上的腳位名稱 print(board_pin_name, end=" ") # 換下一個腳位, 換行顯示 print("") ``` 在 Pico 上執行的結果如下: ``` GP0 GP1 GP10 GP11 GP12 GP13 GP14 GP15 GP16 GP17 GP18 GP19 GP2 GP20 GP21 GP22 SMPS_MODE GP25 LED A0 GP26 GP26_A0 A1 GP27 GP27_A1 A2 GP28 GP28_A2 A3 VOLTAGE_MONITOR GP3 GP4 GP5 GP6 GP7 GP8 GP9 ``` ## 數位輸出 數位輸出/輸入需要用到 [digitalio](https://circuitpython.readthedocs.io/en/latest/shared-bindings/displayio/index.html) 模組, 建立 DigitalInOut 類別的物件, 例如以下是閃爍內建 LED 的例子: ```python= import board import digitalio import time led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT while True: led.value = 1 time.sleep(0.5) led.value = 0 time.sleep(0.5) ``` 腳位不使用時要取消: ```python >>> led.deinit() ``` 數位輸入的話除了設定方向以外, 還可以設定啟用內部的上拉或是下拉電路: ```python >>> import digitalio >>> btn = digitalio.DigitalInOut(board.GP18) >>> btn.direction = digitalio.Direction.INPUT >>> btn.pull = digitalio.Pull.UP >>> btn.value True >>> btn.value False >>> btn.pull = digitalio.Pull.DOWN >>> btn.value False >>> btn.value True ``` :::info CircuitPython 在變更狀態時使用設定屬性的方式, 而不是像 MicroPython 使用物件的方法。你也可以直接讀取屬性來取得當前狀態。 ::: ## 類比輸入 類比輸入使用 [analogio](https://circuitpython.readthedocs.io/en/latest/shared-bindings/analogio/index.html) 模組的 AnalogIn 類別建立物件, 讀值在 0~65535 之間的 16bits 值: ```python= import board import analogio import time a2 = analogio.AnalogIn(board.A2) while True: print(a2.value) time.sleep(0.20) ``` 輸出如下 (實際讀值不會有極值): ``` 65376 65360 65392 64848 57296 49360 49520 20784 432 336 256 288 ``` ## 模擬類比輸出 PWM PWM 使用 [pwmio](https://circuitpython.readthedocs.io/en/latest/shared-bindings/pwmio/index.html) 模組的 PWMOut 類別建立物件, 其中頻率為 32bits、工作週期為 16bits, 例如: ```python= >>> import pwmio >>> led = pwmio.PWMOut(board.GP18, variable_frequency=True) >>> led.frequency=1000 >>> led.duty_cycle=32767 ``` 你可以在建立 PWMOut 物件時就以具名參數 frequency 和 duty_cycle 指定頻率和工作週期, 或者是指定 variable_frequency 為 True, 並在之後隨時變動頻率。 腳位圖中標示 PWM 號碼相同的腳位是共用同一個計時器 (timer) 來提供 PWM 功能, 所以不能同時用來當 PWM: ![腳位圖](https://cdn-shop.adafruit.com/1200x900/4864-04.png) ## 取得系統時間 ```python >>> import time >>> time.monotonic() 8242.5 >>> time.monotonic_ns() 8248821289062 ``` :::info 在 Thonny 3.3.5 執行 time.monotonic() 會出錯, 請更新至 3.3.6 以上即可。 ::: ## 讀取溫度 ```python >>> import microcontroller >>> microcontroller.cpu.temperature 16.8394 >>> microcontroller.cpu.temperature 20.1164 ``` ## 檔案讀寫 CircuitPython 預設會在電腦建立一個隨身碟, 這個隨身碟在 CircuitPython 中是唯讀的檔案系統, 如果嘗試寫入, 會看到錯誤訊息: ```python >>> f = open("/temp.txt", "a") Traceback (most recent call last): File "<stdin>", line 1, in <module> OSError: [Errno 30] Read-only filesystem ``` 你可以在系統開幾時重新將檔案系統掛載為可讀寫, 例如將以下程式儲存到控制板上的 boot.py, 並在重新接上電源前將 GP0 用跳線接到 GND: ```python import board import digitalio import storage gp0 = digitalio.DigitalInOut(board.GP0) gp0.pull = digitalio.Pull.UP if gp0.value == False: storage.remount("/", readonly = False) ``` 控制板重新開機後, 就可以寫入資料了: ```python Adafruit CircuitPython 6.2.0-beta.3 on 2021-03-04; Raspberry Pi Pico with rp2040 >>> f = open("/temp.txt", "a") >>> ``` :::info boot.py 只會在重新接上電源時執行, 軟開機時並不會執行。 ::: ## 程式庫 CircuitPython 的程式庫要將檔案複製到 lib 資料夾下。 ## 用 Pico 當 USB 鍵盤 以下示範使用 [adafruit_hid 程式庫](https://circuitpython.readthedocs.io/projects/hid/en/latest/index.html) 讓 Pico 扮演鍵盤發送特定組合鍵的程式。 [usb_hid](https://circuitpython.readthedocs.io/en/latest/shared-bindings/usb_hid/index.html) 是 CircuitPython 內建的模組, 屬於低階函式庫, 而 adafruit_hid 是要單獨安裝的外部模組, 可以讓我們用滑鼠、鍵盤等高階的方式扮演 hid 裝置。 要安裝外部的程式庫, 只要將下載回來的程式庫檔或是資料夾複製到 CircuitPython 建立的隨身碟中 lib 資料夾下即可: ![](https://i.imgur.com/Zpte5Dt.png) adafruit_hid 程式庫使用的方式很直覺, 以下就是讓 Pico 在按鈕時發送 Ctrl+Shift+A 的快捷鍵, 這是我自己的截圖軟體擷取作用中視窗的快捷鍵: ```python= import time import usb_hid from digitalio import DigitalInOut, Direction, Pull import board from adafruit_hid.keyboard import Keyboard from adafruit_hid.keycode import Keycode btn = DigitalInOut(board.GP18) btn.direction = Direction.INPUT btn.pull = Pull.DOWN kbd = Keyboard(usb_hid.devices) prev = False while True: if prev == False and btn.value == True: time.sleep(0.02) if btn.value == True: # 我的截圖軟體設定快捷鍵是 CTRL+SHIFT+A kbd.send( Keycode.LEFT_CONTROL, Keycode.LEFT_SHIFT, Keycode.A) prev = btn.value ``` ## PIO(Programmable I/O) :::info 網路資源 - [CircuitPython PIO 教學](https://learn.adafruit.com/intro-to-rp2040-pio-with-circuitpython) - [PIO 簡介](https://www.cnx-software.com/2021/01/27/a-closer-look-at-raspberry-pi-rp2040-programmable-ios-pio/) - [datasheet](https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf) ::: PIO 的使用時機有兩個:一是在你需要精確的控制時間與輸出, 以往透過 sleep() 或是時間差的方式控制輸出入時, 有可能因為中間執行的其他程式敘述、或是被中斷而延誤了時間, 造成實際控制的不精確。PIO 是獨立的小處理器, 可以自行運作, 因此不會受到主處理器的運算影響時間精確度。 另一個時機就是當硬體支援不夠用時, 比如說所有的 UART 都用掉了, 或者是某個感測器使用了晶片沒有原生支援的傳輸協定, 這時就可以用 PIO 自行建立所需的傳輸協定。 ### PIO 狀態機 (state machine) 與組合語言 (assembly language) 在 Pico 中有兩個 PIO 單元, 每個 PIO 單元中可以有 4 個單獨運作的處理器, 稱之為**狀態機**, 這些狀態機可以執行使用組合語言撰寫的程式, 看到組合語言不需要害怕, PIO 狀態機是很簡單的處理器, 他的組合語言只有 9 個指令, 以下我們就利用實際的範例來說明如何撰寫 PIO 狀態機的組合語言程式, 以及如何執行。 ### 在 CircuitPython 中使用 PIO 的前置工作 要在 CircuitPython 中使用 PIO, 需要: - 額外安裝 [adafruit_pioasm 模組](https://circuitpython.readthedocs.io/projects/pioasm/en/latest/api.html), 它可以幫我們解譯組合語言程式, 成為可以再狀態機內執行的機器碼。這個模組可以在[這裡](https://circuitpython.org/libraries) 下載, 只要將對應的 mpy 檔複製到 CircuitPython 隨身碟中的 lib 資料夾下即可。 - 內建的[rp2pio 模組](https://circuitpython.readthedocs.io/en/latest/shared-bindings/rp2pio/index.html), 它可以幫我們建立狀態機, 並執行交付給它的機器碼。 ### 用 set 指令直接輸出訊號到指定的腳位 接著就用最簡單的程式來說明使用 PIO 時 CircuitPython 程式的基本架構, 並介紹 PIO 狀態機組合語言中的第一個指令。 使用 PIO 的流程如下: 1. 將狀態機要執行的組合語言程式寫在多行字串中。 2. 利用 adafruit_pioasm 解譯前一步驟寫好的組合語言。 3. 利用 rp2pio 以前一步驟解譯好的程式碼建立狀態機, 建立時可以指定狀態機執行的時脈、要使用的腳位等等。 4. 建立好狀態機後就會自動執行。 接著我們就來撰寫一個只會點亮控制板上內建 LED 的簡易程式: ```python= import rp2pio import adafruit_pioasm import board pgm = """ .program pgm set pins, 1 """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=2000, first_set_pin=board.GP25, ) ``` 整個程式的架構如下: 1. 前 3 行是匯入所需的模組。 2. 5~8 行是 Python 的多行字串, 字串內容就是狀態機要執行的組合語言程式, 稍後再解說。 3. 第 10 行是利用 adafruit_pioasm 模組解譯剛剛寫好的組合語言, 並將組譯結果命名為 assembled。 4. 第 11 行就是利用解譯完的成果建立狀態機, 其中: - 第 1 個參數是組譯結果。 - frequency 具名參數指定的是狀態機的時脈, 表示每秒狀態機可以執行的指令個數, 透過 frequency 就可以精確控制狀態機內的執行時間。 - first_set_pin 具名參數用來指定要操控的腳位, 這裡指定的是連接內建 LED 的 GP25。 現在就可以回頭看看狀態機要執行的組合語言了: 1. 第 6 行是虛擬指令, 用來幫這個組合語言命名, 實際上不會執行任何動作 2. 第 7 行使用 set 指令把 1 設定給 pins, pins 就是指要操控的腳位, 這等於是把腳位設定圍高電位, 內建的 LED 就會亮起來。 如果你執行這個程式, 就只會把內建 LED 點亮。你可以試試看將第 7 行的 set 指令後面的資料改為 0, 就可以把 LED 熄掉。 ### 使用 set 指令控制多個腳位 first_set_pin 指定的其實是要控制的第一個腳位, 還可以透過 set_pin_count 指定連續的腳位數, 例如若 first_set_pin 為 GP16, set_pin_count 為 3, 就表示要控制 GP16~18 共 3 個腳位, 這時 set 指令的值最低位元就對應到 first_set_pin。像是以下程式就會點亮 GP16 和 GP17 兩個 LED: ```python= import rp2pio import adafruit_pioasm import board pgm = """ .program pgm set pins, 3 """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=2000, first_set_pin=board.GP16, set_pin_count=3, ) ``` ### 使用 FIFO 與 out 指令傳送資料給狀態機 剛剛的範例要點亮哪個 LED 已經寫死在狀態機的組合語言中, 我們也可以傳送資料給狀態機, 讓狀態機根據給定的資料點亮對應的 LED。系統與狀態機是透過 FIFO 來傳遞資料, 架構如下: ``` +------+ pull out +----+ | |----> FIFO --------> OSR ---------> |x | |system| |y | | |<---- FIFO <-------- ISR <--------- |pins| +------+ push in +----+ ``` 所謂的 FIFO 就是資料依照順序先進先出的佇列 (queue), 我們的程式可以將資料送入 FIFO, 而狀態機可以藉由 pull 指令從 FIFO 中將資料取出放入 OSR 暫存器, 再經由 out 指令從 OSR 中將資料移至其他地方。狀態機也可藉由反向的 FIFO 將資料送出, 這在之後再說明。PIO 的每一個 FIFO 可以推入 4 個 32 位元的資料。以下的範例就可以每 1 秒輪流點亮接在 GP16、GP17、GP18 的 LED: ```python= import time import rp2pio import adafruit_pioasm import board pgm = """ .program pgm pull out pins, 3 """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=2000, first_out_pin=board.GP16, out_pin_count=3, ) sm.write(b"0x01") time.sleep(1) sm.write(b"0x02") time.sleep(1) sm.write(b"0x04") time.sleep(1) sm.write(b"0x00") ``` 第 8 行的 pull 指令會從 FIFO 中取出第一項資料後放入 OSR 暫存器中, 如果 FIFO 中沒有資料, 這個指令會讓狀態機等候, 不會往下一個指令執行。 第 9 行的 out 指令可以將 OSR 暫存器中的資料移出, 這裡的 pins 表示要將資料對應到控制的腳位;而接著的 3 表示只從 OSR 暫存器中移除 3 個位元, 預設的情況下會從最低位元移出, 也就是說, 如果 OSR 的內容是 0b00000011, 那移出的 3 個位元就是 011, 若要控制的腳位是 GP16~18 , 那麼 GP16 就會是 1、GP17 也是 1、而 GP18 是 0。 要注意的是, set 指令和 out 指令控制的腳位是獨立的, 所以這裡要改用 first_**out**_pin 和 **out**_pin_count 指定腳位。 第 20 行的 sm.write() 就是實際送資料進去給狀態機的程式, 傳入的參數必須是可走訪 (iterable) 的物件, 它會循序取出資料堆入 FIFO 中, 本例傳入的是 bytes 物件。由於我們分別傳入 1、2、4, 在狀態機的程式中就會對應到 GP16、17、18 輪流點亮 LED 了。 ### 使用 jmp 指令與延遲控制流程 利用狀態機精確控制時間的特性, 我們可以很方便的設計出 PWM 輸出, 例如以下的程式可以達成 duty cycle 為 20% 的 PWM 輸出效果: ```python= import time import rp2pio import adafruit_pioasm import board pgm = """ .program pgm loop: set pins, 1 [1] set pins, 0 [6] jmp loop """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=2000, first_set_pin=board.GP16, ) ``` 第 8 行是設定標籤, 這樣可以在第 11 行利用 jmp 指令跳到指定的標籤處回頭重複執行同樣的一段程式。 第 9 和 10 行尾端的中括號, 可以在指令執行後指定延遲 0~31 個時脈週期。在狀態機中, 每個指令執行耗時一個時脈週期, 加上延遲, 就可以控制信號持續時間。 在上面的例子中: - 設定腳位為 1 後延遲 1 個時脈週期, 所以腳位為 1 的時間佔 1+1=2 個時脈週期。 - 設定腳位為 0 後延遲 6 個時脈週期, 加上隨後的 jmp 指令, 所以腳位為 0 的時間佔 1+6+1=8 個時脈週期。 因此, 高電位佔的比例就是 2/(2+8) = 20%。只要重複這段程式, 就可以造成 duty cycle 為 20% 的 PWM 輸出。 ### 使用 wrap 自動重複程式 上一個範例中為了重複執行程式, 額外使用了 jmp 指令, 不過 PIO 的組合語言提供有一個指引命令 (directive) 可以讓狀態機自動重複程式, 同樣功能的程式可以改寫成這樣: ```python= import time import rp2pio import adafruit_pioasm import board pgm = """ .program pgm .wrap_target set pins, 1 [0] set pins, 0 [7] .wrap """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=2000, first_set_pin=board.GP16, ) ``` 第 8 行使用 .wrap_target 標示要重複執行的起點, 而 .wrap 就是表示要回到 .wrap_target 處開始重複執行。 使用 .wrap_target 的好處就是程式會自動重複, 不需要額外耗費時間執行 jmp 指令。實際上, 如果沒有在程式中加上 .wrap_target, 預設就會從程式開頭處開始重複執行;如果沒有加上 .wrap, 預設就是執行到程式結尾處就重複執行。換句話說, 剛剛的例子其實可以刪除第 8 和 11 行, 程式還是會自動重複執行。如果你的程式需要從中間重複執行, 就要明確標示 .wrap_target 與 .wrap 了。 ### 有條件的 jmp 指令 前面的範例中高低電位的時間佔比已經寫死在程式中, 無法改變, 如果可以利用 FIFO 傳入佔比變化, 就可以變換 LED 亮度了。以下就是修改過的程式, 它會從 FIFO 讀取計數, 代表低電位所要佔的時脈週期數, 然後點亮 LED, 再依據指定的週期數熄掉 LED, 因此只要變換傳入 FIFO 的週期數, 就可以調整高電位的時間佔比: ```python= import time import rp2pio import adafruit_pioasm import board import array pgm = """ .program pgm .side_set 3 start: pull noblock out x, 32 mov y, x jmp !y start side 7 loop: jmp y-- loop side 0 jmp start """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=10000, first_sideset_pin=board.GP16, sideset_pin_count=3 ) while True: for l in reversed(range(128)): sm.write(array.array("I", [l])) time.sleep(0.005) time.sleep(0.3) for l in range(128): sm.write(array.array("I", [l])) time.sleep(0.005) ``` 第 11 行幫 pull 指令加上了 noblock, 表示當 FIFO 內沒有資料時, 不等候直接往下執行, 遇到這種狀況時, 狀態機會把 x 的內容複製到 OSR 中。 第 12 行把週期計數從 OSR 移出到 x 中, 並利用 mov 將計數從 x 複製到 y 中。 第 14 行的 jmp 指令加上了 !y 的條件, 表示當 y 不是 0 的時候才會跳躍。這裡我們用來判斷當等待計數是 0 的時候, 不熄滅 LED。 要注意的是第 14 行結尾有一個 side 7, 這是 PIO 的特殊功能, 可以在執行指令的同時輸出到指定的腳位, 腳位要再建立狀態機時利用 first_sideset_pin 具名參數以及 sideset_pin_count 指定, 類似 first_set_pin 的作法。除此之外, 也必須在組合語言程式中像是第 9 行那樣使用 .side_set 指定程式中實際要使用的腳位數, 如果沒有加這一行, side 就不會發揮作用。本例我們指定要使用從 GP16 開始的 3 個腳位, 所以這一行的 side 7 就會從 3 個腳位送出高電位。 第 16 行是另一個使用條件式跳躍的範例, 這裡的條件式 y--, 它會先檢查 y 的值, 再將 y 減去 1, 如果檢查時 y 的值不是 0 就跳躍到指定的標籤處, 否則繼續往下執行。我們同樣在這個指令加上 side 0 將 3 個腳位的輸出都改為 0。這個 jmp 指令可以讓狀態機依據指定的時脈週期數維持 3 個腳位輸出 0。 傳送資料進入 FIFO 時, 本例使用了 Python 內建的 [array 模組](https://docs.python.org/3.9/library/array.html)建立陣列, 建立時的第 1 個參數表示資料類型, "I" 表示無號整數。利用傳入連續的低電位時脈週期數, 就可以製造出類似呼吸燈的效果了。 ### 使用 wait 等待外部訊號 如果狀態機需要等待外部訊號變化, 例如按下按鈕, 就可以使用 wait 指令, 例如: ```python= import time import rp2pio import adafruit_pioasm import board pgm = """ .program pgm wait 1 pin 0 set pins, 1 """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=2000, first_set_pin=board.GP16, first_in_pin=board.GP28, pull_in_pin_down=0x01 ) ``` 第 8 行就是等待第 1 個 (0~31) 輸入腳位變成高電位, 這個指令要能運作, 必須在建立狀態機時如同第 17 行使用 first_in_pin 設定要控制的第一個腳位, 並可再用 in_pin_count 設定腳位總數。為了讓輸入腳位在狀態機執行有明確的電位, 還可以用 pull_in_pin_down 或是 pull_in_pin_up 設定個別腳位要不要啟用內部的 pull down 或是 pull up 電路, 指定的方法是傳入位元遮罩, 最低位元對應到第 1 個輸入腳位。 此程式執行後就會等待按下接在 GP28 的按鈕, 按下按鈕接在 GP16 的 LED 就會亮起。 第 8 行也可以寫成: ```python=8 wait 1 gpio 28 ``` 直接用腳位編號來指定。 ### 使用 irq 同不多個狀態機 如果建立了多個狀態機, 希望狀態機間可以協同運作, 就必須仰賴 IRQ 機制, PIO 的狀態機之間並不能相互傳送資料, 但可以透過設立 IRQ 旗號 (共有 0~7 號), 讓等待該旗號的狀態機接獲通知繼續執行。以下就是一個簡單的例子, 狀態機 sm1 會等待 5 號 IRQ 旗號設立, 然後點亮接在 GP16 的 LED;而狀態機 sm2 會等待使用者按下接在 GP28 的按鈕後設立 5 號 IRQ 旗號: ```python= import time import rp2pio import adafruit_pioasm import board pgm1 = """ .program pgm wait 1 irq 5 set pins, 1 """ pgm2 = """ .program pgm wait 1 pin 0 irq 5 """ assembled1 = adafruit_pioasm.assemble(pgm1) assembled2 = adafruit_pioasm.assemble(pgm2) sm1 = rp2pio.StateMachine( assembled1, frequency=2000, first_set_pin=board.GP16, ) sm2 = rp2pio.StateMachine( assembled2, frequency=2000, first_in_pin=board.GP28, pull_in_pin_down=0x01 ) ``` 第 8 行就是利用 wait 指令等待 5 號 IRQ 旗號被設立 (狀態被設為 1), 如果要等待 IRQ 旗號被清除, 就要將 1 改為 0。 而在第 15 行則是使用 irq 指令設立 5 號旗號。設立旗標後, 若要等待其他狀態機的 wait 執行才接續, 可以將第 15 改寫成這樣: ```python=15 irq 5 ``` 另外, wait irq 會將等待的旗標清除, 如果有多個狀態機再等待同一個編號的旗號, 就必須發出多次 irq 指令, 否則只有一個狀態機會成功等到設立旗號。 你也可以將上面的範例改寫成先設立旗標, 再等待旗標被清除: ```python= import time import rp2pio import adafruit_pioasm import board pgm1 = """ .program pgm irq 5 wait 0 irq 5 set pins, 1 """ pgm2 = """ .program pgm wait 1 pin 0 irq clear 5 """ assembled1 = adafruit_pioasm.assemble(pgm1) assembled2 = adafruit_pioasm.assemble(pgm2) sm1 = rp2pio.StateMachine( assembled1, frequency=2000, first_set_pin=board.GP16, ) sm2 = rp2pio.StateMachine( assembled2, frequency=2000, first_in_pin=board.GP28, pull_in_pin_down=0x01, ) ``` ### 使用 in 和 push 指令送出資料給系統 底下我們以一個簡單的例子來說明和 out 及 pull 相反方向的 in 與 push 指令: ``` +------+ pull out +----+ | |----> FIFO --------> OSR ---------> |x | |system| |y | | |<---- FIFO <-------- ISR <--------- |pins| +------+ push in +----+ ``` 這裡我們使用 GP26~GP28 接 3 個按鈕為例, 預設都啟用 pull down 電路, 每當 GP28 按鈕按下, 就將 GP26 與 GP 27 的按鈕狀態送出給系統: ```python= import time import rp2pio import adafruit_pioasm import board import array pgm = """ .program pgm wait 1 pin 2 in pins 2 push wait 0 pin 2 """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency=2000, first_in_pin=board.GP26, in_pin_count=3, pull_in_pin_down=0x07, ) buf_r = array.array("I", [0]) while True: sm.readinto(buf_r) print("get:{:032b}".format(buf_r[0])) ``` 第 9 行是等待 GP28 的按鈕按下, 第 12 行是等待按鈕放開再繼續, 避免按一次鈕被當成多次按鈕。 第 10 行是將 2 個輸入腳位, 也就是 GP26 與 GP27 的狀態移入 ISR 中, 然後使用 push 指令將 ISR 的內容推入輸出到系統的 FIFO 中。 在建立狀態機時, 就透過第 19~21 行設定輸入腳位是 GP26~28 這 3 個腳位, 並且啟用 pull down 電路。 第 24~27 行先建立一個只有單一元素的陣列, 然後進入無窮迴圈, 嘗試從 FIFO 中讀取資料, 然後以 2 進位格式顯示出來方便觀察個別位元變化。要注意的是, readinto() 在 FiFO 是空的情況下會等待資料到來才會返回。 以下就是實際執行的結果, 一開始若按下 GP28 的按鈕: ``` get:00000000000000000000000000000000 ``` 因為其他按鈕都沒有按下,. 所以讀到的都是 0。讓我們試試看按 GP26 和 GP28: ``` get:01000000000000000000000000000000 ``` 最左邊的 01 就是 GP27 和 GP26 的狀態。我們可以是看看按下 GP27 和 GP28: ``` get:10000000000000000000000000000000 ``` 你會發現變成左邊的位元是 1 了。 你可能會想說為什麼是出現在最左邊 (最高) 位元?這是因為 in 指令預設會將資料從左往右移入 ISR, 也就是從最高位元往右邊移入。以剛剛的例子來說, 若 GP26 有按下但 GP27 沒按下, 讀到的兩個位元是 01(最低位元是啟始腳位 GP), 移入 ISR 的方式如下: ``` 01 --> 00000000000000000000000000000000 ``` 結果就變成 ISR 中最右邊的兩個 0 被移出, 而 01 移入到最左邊變成: ``` 01000000000000000000000000000000 ``` 如果希望 01 出現在最右邊, 可以在建立狀態機時指定 in_shift_right 具名參數為 False: ```python=16 sm = rp2pio.StateMachine( assembled, frequency=2000, first_in_pin=board.GP26, in_pin_count=3, pull_in_pin_down=0x07, in_shift_right=False ) ``` 執行後同樣按下 GP26 與 GP28, 就會發現 01 跑到右邊了: ``` get:00000000000000000000000000000001 ``` ### 不足長度的資料寫入 FIFO 會自動重複填滿 4 bytes 使用 write() 寫入資料到 FIFO 時, 由於 FIFO 的資料單位是 32bits, 如果資料長度不足, 會自動重複填滿。舉例來說, 如果寫入的資料僅有 1 個 byte: ```python= import time import rp2pio import adafruit_pioasm import array pgm = """ .program msb loop: pull ;wait for FIFO out x, 32 in x, 32 push jmp loop ;loop forever """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency = 10000, ) buf_w = array.array("B", [0b01010011]) print("set:{:08b}".format(buf_w[0])) sm.write(buf_w) time.sleep(1) buf_r = array.array("I", [0]) sm.readinto(buf_r) print("get:{:032b}".format(buf_r[0])) ``` 實際送入 FIFO 的就是重複 4 次相同的資料: ``` set:01010011 get:01010011010100110101001101010011 ``` 如果把 23、24 改成這樣: ```python=23 buf_w = array.array("H", [0b0101001100110100]) print("set:{:016b}".format(buf_w[0])) ``` 輸出結果就會變成: ``` set:0101001100110100 get:01010011001101000101001100110100 ``` 你可以看到原始資料的 2 個 bytes 會重複一次變成 4 個 bytes。 ### 使用 out_shift_right 控制 out 從 OSR 移出資料的方向 前面的範例是從 OSR 移出完整的 32 個位元到 x, 所以位元移動的方向並不重要, 因為不管從左邊還是右邊移出資料, 最後 32 個位元都會填滿 x, 結果都一樣。但是如果我們只從 OSR 移出 5 個位元到 x, 從 OSR 的哪一端移出資料就會影響結果, 例如: ```python= import time import rp2pio import adafruit_pioasm import array pgm = """ .program msb loop: pull ;wait for FIFO out x, 5 in x, 32 push jmp loop ;loop forever """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency = 10000, # freq of state machine out_shift_right = True, ) buf_w = array.array("H", [0b0101001100110100]) print("set:{:016b}".format(buf_w[0])) sm.write(buf_w) time.sleep(1) buf_r = array.array("I", [0]) sm.readinto(buf_r) print("get:{:032b}".format(buf_r[0])) ``` 注意第 20 行建立狀態機時指定了 out_shift_right 為 True(這是預設值), 表示從 OSR 右端往右移出 5 個位元到 x 最右端, 這可以看成: 1. 先把 x 的 32 位元都填 0: ``` x: 00000000 00000000 00000000 00000000 ``` 3. 把 OSR 的最右邊 5 個位元移到 x 的最右邊: ``` OSR:01010011 00110100 01010011 001->10100 x :00000000 00000000 00000000 000 10100 ``` 所以最後的結果如下: ``` set:0101001100110100 get:00000000000000000000000000010100 ``` 如果把 out_shift_right 設為 False, 表示從 OSR 的最左端往左移出 5 個位元到 x 的最右端: ``` OSR:01010<-011 00110100 01010011 00110100 x :00000000 00000000 00000000 000 01010 ``` 結果就變成: ``` set:0101001100110100 get:00000000000000000000000000001010 ``` ### 利用 in_shift_right 控制 in 將資料移入 ISR 的方向 同樣的, 將資料移入 ISR 時, 也可以控制要從左邊還是右邊移入, 例如: ```python= import time import rp2pio import adafruit_pioasm import array pgm = """ .program msb loop: pull ;wait for FIFO out x, 32 in x, 5 push jmp loop ;loop forever """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency = 10000, # freq of state machine in_shift_right = True, ) buf_w = array.array("H", [0b0101001100110100]) print("set:{:016b}".format(buf_w[0])) sm.write(buf_w) time.sleep(1) buf_r = array.array("I", [0]) sm.readinto(buf_r) print("get:{:032b}".format(buf_r[0])) ``` 這裡將 in_shift_right 設為 True(這是預設值), 表示要從 x 取出最右端的 5 個位元後從 ISR 最左端往右移入: ``` x :01010011 00110100 01010011 001 10100 ISR:10100->00000000 00000000 00000000 000 ``` 所以結果為: ``` set:0101001100110100 get:10100000000000000000000000000000 ``` 但如果將 in_shift_right 設為 False, 表示一樣從 x 取出最右端的 5 個位元, 但是從 ISR 最右往左端移入: ``` x :01010011 00110100 01010011 001 10100 ISR:00000000 00000000 00000000 000<-10100 ``` 所以結果為: ``` set:0101001100110100 get:00000000000000000000000000010100 ``` 只要記得, 這裡控制的都是從 OSR 移出或是移入 ISR 的方向, 就不容易搞混了。綜合練習, 以下程式: ```python= import time import rp2pio import adafruit_pioasm import array pgm = """ .program msb loop: pull ;wait for FIFO out x, 5 in x, 3 push jmp loop ;loop forever """ assembled = adafruit_pioasm.assemble(pgm) sm = rp2pio.StateMachine( assembled, frequency = 10000, # freq of state machine out_shift_right = False, in_shift_right = False ) buf_w = array.array("H", [0b0101001100110100]) print("set:{:016b}".format(buf_w[0])) sm.write(buf_w) time.sleep(1) buf_r = array.array("I", [0]) sm.readinto(buf_r) print("get:{:032b}".format(buf_r[0])) ``` 就會從 OSR 左端取出 5 個位元 01010 放入 x 最右邊;再從 x 最右邊取出 3 個位元, 也就是 010 從 ISR 右端移入, 所以最後結果就是: ``` set:0101001100110100 get:00000000000000000000000000000010 ```