---
# System prepended metadata

title: CircuitPython 入門--使用 Raspberry Pi Pico
tags: [CircuitPython, Pico, Python, Raspberry Pi]

---

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