Try   HackMD
tags: Python CircuitPython Raspberry Pi Pico

CircuitPython 入門使用 Raspberry Pi Pico

可參考官方的教學文件, 以及正式的參考文件

腳位配置

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

CircuitPython 使用 micropython.Pin 類別 的物件來操控個別的腳位, 並用 microcontroller.pin 記錄晶片上個別腳位對應的 Pin 物件, 而 board 則是記錄開發板上腳位名稱對應的 Pin 物件。晶片腳位名稱是唯一的, 個別是 GPIO0~GPIO29, 每一個腳位名稱只會對應到一個 Pin 物件, 但是開發板的同一腳位可能會因為用途不同而有多個名稱, 例如 GP28 和 A2 就是同一腳位, 所以會有多個名稱對應到同一個 Pin 物件的狀況。

以下可觀察這些腳位物件在個別模組中的名稱:

>>> 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 個腳位物件都是指向同一個物件:

>>> microcontroller.pin.GPIO25 == board.LED
True
>>> microcontroller.pin.GPIO25 == board.GP25
True
>>> board.LED == board.GP25
True
>>>

要用到這個腳位時選用任何一個都可以。如果想知道哪些腳位是同個腳位, 可以執行以下程式:

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 模組, 建立 DigitalInOut 類別的物件, 例如以下是閃爍內建 LED 的例子:

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)

腳位不使用時要取消:

>>> led.deinit()

數位輸入的話除了設定方向以外, 還可以設定啟用內部的上拉或是下拉電路:

>>> 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

CircuitPython 在變更狀態時使用設定屬性的方式, 而不是像 MicroPython 使用物件的方法。你也可以直接讀取屬性來取得當前狀態。

類比輸入

類比輸入使用 analogio 模組的 AnalogIn 類別建立物件, 讀值在 0~65535 之間的 16bits 值:

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 模組的 PWMOut 類別建立物件, 其中頻率為 32bits、工作週期為 16bits, 例如:

>>> 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:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

取得系統時間

>>> import time
>>> time.monotonic()
8242.5
>>> time.monotonic_ns()
8248821289062

在 Thonny 3.3.5 執行 time.monotonic() 會出錯, 請更新至 3.3.6 以上即可。

讀取溫度

>>> import microcontroller
>>> microcontroller.cpu.temperature
16.8394
>>> microcontroller.cpu.temperature
20.1164

檔案讀寫

CircuitPython 預設會在電腦建立一個隨身碟, 這個隨身碟在 CircuitPython 中是唯讀的檔案系統, 如果嘗試寫入, 會看到錯誤訊息:

>>> 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:

import board
import digitalio
import storage

gp0 = digitalio.DigitalInOut(board.GP0)
gp0.pull = digitalio.Pull.UP

if gp0.value == False:
    storage.remount("/", readonly = False)

控制板重新開機後, 就可以寫入資料了:

Adafruit CircuitPython 6.2.0-beta.3 on 2021-03-04; Raspberry Pi Pico with rp2040
>>> f = open("/temp.txt", "a")
>>>

boot.py 只會在重新接上電源時執行, 軟開機時並不會執行。

程式庫

CircuitPython 的程式庫要將檔案複製到 lib 資料夾下。

用 Pico 當 USB 鍵盤

以下示範使用 adafruit_hid 程式庫 讓 Pico 扮演鍵盤發送特定組合鍵的程式。

usb_hid 是 CircuitPython 內建的模組, 屬於低階函式庫, 而 adafruit_hid 是要單獨安裝的外部模組, 可以讓我們用滑鼠、鍵盤等高階的方式扮演 hid 裝置。

要安裝外部的程式庫, 只要將下載回來的程式庫檔或是資料夾複製到 CircuitPython 建立的隨身碟中 lib 資料夾下即可:

adafruit_hid 程式庫使用的方式很直覺, 以下就是讓 Pico 在按鈕時發送 Ctrl+Shift+A 的快捷鍵, 這是我自己的截圖軟體擷取作用中視窗的快捷鍵:

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)

PIO 的使用時機有兩個:一是在你需要精確的控制時間與輸出, 以往透過 sleep() 或是時間差的方式控制輸出入時, 有可能因為中間執行的其他程式敘述、或是被中斷而延誤了時間, 造成實際控制的不精確。PIO 是獨立的小處理器, 可以自行運作, 因此不會受到主處理器的運算影響時間精確度。

另一個時機就是當硬體支援不夠用時, 比如說所有的 UART 都用掉了, 或者是某個感測器使用了晶片沒有原生支援的傳輸協定, 這時就可以用 PIO 自行建立所需的傳輸協定。

PIO 狀態機 (state machine) 與組合語言 (assembly language)

在 Pico 中有兩個 PIO 單元, 每個 PIO 單元中可以有 4 個單獨運作的處理器, 稱之為狀態機, 這些狀態機可以執行使用組合語言撰寫的程式, 看到組合語言不需要害怕, PIO 狀態機是很簡單的處理器, 他的組合語言只有 9 個指令, 以下我們就利用實際的範例來說明如何撰寫 PIO 狀態機的組合語言程式, 以及如何執行。

在 CircuitPython 中使用 PIO 的前置工作

要在 CircuitPython 中使用 PIO, 需要:

  • 額外安裝 adafruit_pioasm 模組, 它可以幫我們解譯組合語言程式, 成為可以再狀態機內執行的機器碼。這個模組可以在這裡 下載, 只要將對應的 mpy 檔複製到 CircuitPython 隨身碟中的 lib 資料夾下即可。
  • 內建的rp2pio 模組, 它可以幫我們建立狀態機, 並執行交付給它的機器碼。

用 set 指令直接輸出訊號到指定的腳位

接著就用最簡單的程式來說明使用 PIO 時 CircuitPython 程式的基本架構, 並介紹 PIO 狀態機組合語言中的第一個指令。

使用 PIO 的流程如下:

  1. 將狀態機要執行的組合語言程式寫在多行字串中。
  2. 利用 adafruit_pioasm 解譯前一步驟寫好的組合語言。
  3. 利用 rp2pio 以前一步驟解譯好的程式碼建立狀態機, 建立時可以指定狀態機執行的時脈、要使用的腳位等等。
  4. 建立好狀態機後就會自動執行。

接著我們就來撰寫一個只會點亮控制板上內建 LED 的簡易程式:

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:

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:

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 輸出效果:

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) 可以讓狀態機自動重複程式, 同樣功能的程式可以改寫成這樣:

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 的週期數, 就可以調整高電位的時間佔比:

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 模組建立陣列, 建立時的第 1 個參數表示資料類型, "I" 表示無號整數。利用傳入連續的低電位時脈週期數, 就可以製造出類似呼吸燈的效果了。

使用 wait 等待外部訊號

如果狀態機需要等待外部訊號變化, 例如按下按鈕, 就可以使用 wait 指令, 例如:

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 行也可以寫成:

wait 1 gpio 28

直接用腳位編號來指定。

使用 irq 同不多個狀態機

如果建立了多個狀態機, 希望狀態機間可以協同運作, 就必須仰賴 IRQ 機制, PIO 的狀態機之間並不能相互傳送資料, 但可以透過設立 IRQ 旗號 (共有 0~7 號), 讓等待該旗號的狀態機接獲通知繼續執行。以下就是一個簡單的例子, 狀態機 sm1 會等待 5 號 IRQ 旗號設立, 然後點亮接在 GP16 的 LED;而狀態機 sm2 會等待使用者按下接在 GP28 的按鈕後設立 5 號 IRQ 旗號:

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 改寫成這樣:

irq 5

另外, wait irq 會將等待的旗標清除, 如果有多個狀態機再等待同一個編號的旗號, 就必須發出多次 irq 指令, 否則只有一個狀態機會成功等到設立旗號。

你也可以將上面的範例改寫成先設立旗標, 再等待旗標被清除:

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 的按鈕狀態送出給系統:

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:

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:

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 改成這樣:

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 的哪一端移出資料就會影響結果, 例如:

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  
    
  2. 把 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 時, 也可以控制要從左邊還是右邊移入, 例如:

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 的方向, 就不容易搞混了。綜合練習, 以下程式:

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