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