> For the complete documentation index, see [llms.txt](https://think-embedded.gitbook.io/micropython/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://think-embedded.gitbook.io/micropython/micropython-for-rp2040-pico/using-rp2040s-pio-with-micropython.md).

# PIO Programming

## Programmable I/O (PIO)

ภายในชิป **RP2040** มีวงจรที่เรียกว่า **PIO** อยู่ 2 ชุด (เรียกว่า **PIO0 & PIO1**) แต่ละชุดประกอบไปด้วยหน่วยย่อยที่เรียกว่า **State Machines** (**SMs**) 4 ชุด **(**&#xE40;รียกชื่อเป็น **SM0..SM3** สำหรับ **PIO0** และ **SM4..SM7** สำหรับ **PIO1)** รวมทั้งหมดเป็น **2 x 4 = 8** ชุด&#x20;

![รูป: องค์ประกอบภายใน PIO ](/files/-MTJLaQ_ExZg1FzAj5XQ)

การทำงานของ **SM** แต่ละตัว จะต้องอาศัยคำสั่งที่ได้มีการโปรแกรมเก็บไว้ในหน่วยความจำของ **PIO** (เรียกว่า **Instruction Memory** หรือ **PIO Register File**) ซึ่งใช้ร่วมกันสำหรับ **SMs** ทั้ง 4 ชุด สามารถเก็บคำสั่งได้สูงสุด 32 คำสั่งสำหรับ **PIO** แต่ละชุด

ในการเขียนคำสั่งจากซีพียูไปยังหน่วยความจำนี้  มีพอร์ตสำหรับการเขียน (**Single Write Port**) เพียงพอร์ตเดียว แต่มีพอร์ตสำหรับอ่านคำสั่ง (**Multiple Read Ports**) แยกกันระหว่าง **SM** แต่ละตัว กล่าวคือ สามารถดึงคำสั่ง (**Instruction Fetch**) ไปยัง **SM** ทั้ง 4 ชุด ได้พร้อมกัน&#x20;

ซีพียูสามารถเขียนหรืออ่านข้อมูล โดยใช้วงจร **FIFO (First-In-First-Out)** ที่มีขนาด **4 x 32** บิต (**4 Words**) แบ่งเป็น **RX FIFO** และ **TX FIFO** สำหรับ **SM** แต่ละตัว และใช้คำสั่ง `PUSH` เพื่อส่งข้อมูลจาก **SM** ไปยัง **RX FIFO** และคำสั่ง `PULL` ดึงข้อมูลมาจาก **TX FIFO** มายัง **SM**

**SM** สามารถเข้าถึงขา **GPIO** ได้เช่นกัน นอกจากนั้นแล้ว ยังสามารถสร้างอินเทอร์รัพท์ **(IRQ0 & IRQ1**) แจ้งเตือนไปยังซีพียูได้โดยใช้คำสั่ง `IRQ` รวมถึงการเซ็ตหรือเคลียร์บิตสำหรับอินเทอร์รัพท์ (**IRQ Flags**)

ภายใน **SM** แต่ละตัว มีวงจรเลื่อนบิตข้อมูลขนาด 32 บิต (**32-bit** **Shift Registe**r) ทั้งขาเข้าและขาออก (แยกกัน 2 ชุด) ใช้ในการเลื่อนบิตออกไปยังขา **GPIO** ได้ หรือรับข้อมูลเข้ามาก็ได้ ดังนั้นจึงสามารถทำฟังก์ชันได้เหมือนวงจร **I2C**, **I2S**, **SDIO**, **SPI**, **UART** เป็นต้น

คำสั่ง  `OUT` ใช้สำหรับเลื่อนบิตออกจาก **Output Shift Register (OSR)** เช่น ไปยังขา **GPIO** หรือคำสั่ง `IN` ใช้เพื่อเลื่อนบิตเข้าไปใส่ไว้ใน **Input Shift Register** **(ISR)** เป็นต้น

ในการทำงานของตัวเลื่อนบิตขาออก (หรือ **OSR)** โดยใช้คำสั่ง `OUT`  เมื่อได้เลื่อนบิตออกไปจำนวนหนึ่งแล้ว (โดยทั่วไปก็ เช่น 8, 16, 24 เป็นต้น) เราสามารถตั้งค่า **Pull Threshold** ให้โหลดข้อมูลถัดไป (**32-bit Word**) จาก **TX FIFO** มาใส่ลงใน **OSR** ได้โดยอัตโนมัติ (**Auto-Push**) &#x20;

ถ้ารับข้อมูลโดยการเลื่อนบิตเข้ามาโดยใช้คำสั่ง `IN` ก็สามารถตั้งค่า **Push Threshold** ให้นำข้อมูลที่ได้รับนั้นจาก **ISR** ไปใส่ลงใน **RX FIFO** ได้โดยอัตโนมัติ&#x20;

นอกจากรีจิสเตอร์สำหรับเลื่อนบิตข้อมูลแล้ว **SM** แต่ละตัวยังมีรีจิสเตอร์อีกสองตัว เรียกว่า **Scratch Registers (X & Y)** สำหรับเก็บข้อมูลชั่วคราว สามารถรับข้อมูลบิตมาจากรีจิสเตอร์เลื่อนบิต หรือขา **GPIO** ได้

การทำงานของ **SM** ต้องใช้สัญญาณ **Clock** โดยมีตัวหารความถี่ทีเรียกว่า **Fractional Clock Divider** (**16.8 fixed-point number**) ที่โปรแกรมค่าได้ และรับสัญญาณมาจาก **System Clock**  ดังนั้นจึงปรับความเร็วในการทำงานได้

เนื่องจากความเร็วในการทำงานของซีพียูสำหรับไมโครไพธอน `rp2` คือ 1**25 MHz** ความถี่ตำสุดที่เลือกใช้ได้คือ

$$
f\_{PIO,min} =\frac{125; MHz}{ 65,536 }  \approx 1,907.45; Hz
$$

และมีความละเอียด **1/256 Hz** หรือ **0.00390625 Hz**

![รูป: องค์ประกอบของ State Machine และการไหลของข้อมูล](/files/-MTJRQbp80Km2M6gpsbQ)

## PIO Instruction Set&#x20;

ชุดคำสั่ง (**Instruction Set**) ของ **PIO** มีเพียง 9 คำสั่งเท่านั้น คือ { `JMP`, `WAIT`, `IN`, `OUT`, `PUSH`, `PULL`, `MOV`, `IRQ`, `SET` } ทุกคำสั่งมีขนาด 16 บิต และทำงานโดยใช้เพียงหนึ่งไซเคิลเท่านั้น (**Single-Cycle Instruction Execution**)

![ตาราง: ชุดคำสั่งของ PIO ](/files/-MTJdg228bU5oTv6YPL5)

จากรูปแบบของคำสั่งในตาราง จะสังเกตเห็นได้ว่า แต่ละคำสั่งมีขนาด **Opcode** เท่ากับ 3 บิต (บิตที่ 15-12) และถัดไปมี 5 บิต (บิตที่ 12-8) ที่แบ่งใช้ร่วมกันสำหรับ **side-set** กับ **delay**&#x20;

* **Side-setting** ใช้กับขา **GPIO pins** และเป็นการเซตหรือเคลียร์บิตสำหรับสถานะลอจิกที่ขาเหล่านั้น และเกิดขึ้นเมื่อทำคำสั่งของ **SM** ในไซเคิลเดียวกัน
* **Delaying** เป็นการหน่วงเวลาหรือรอเวลาไว้ หลังจากทำคำสั่งของ **SM** ตามจำนวนไซเคิลที่ต้องการ สูงสุดไม่เกิน 31 ไซเคิล ในการทำคำสั่งหนึ่งครั้ง

ยกตัวอย่างเช่น คำสั่ง `IN` และ `OUT` ใช้สำหรับเลื่อนข้อมูลบิตเข้าหรือออกสำหรับ **ISR** หรือ **OSR** ตามลำดับ โดยจะต้องระบุแหล่งที่มา (**Source**) หรือเป้าหมาย (**Destination**) เช่น สำหรับขา **GPIO** (**PINS**, **PINDIRS**), **Shift Registers** (**ISR, OSR**), **Scratch Registers** (**X, Y**), **NULL** (**0's**) และเลือกได้ว่า ต้องการจะเลื่อนข้อมูลจำนวนกี่บิต (**Bit Count**)

คำสั่ง `PUSH` ใช้ในการเลื่อนข้อมูลจากรีจิสเตอร์ **ISR** ไปยัง **RX FIFO** และคำสั่ง `PULL` ใช้เพื่อเลื่อนข้อมูลจาก **TX FIFO** ไปยัง **OSR** ทีละ 32 บิต และสามารถตั้งเงื่อนไขให้หยุดการทำงานของ **FIFO** ชั่วคราวได้ (**FIFO Stall**) เช่น ในกรณีที่ข้อมูลเต็มความจุ (**FIFO Full**) หรือไม่มีข้อมูลเหลืออยู่ (**FIFO Empty**)

![รูป: Instruction Formats IN, OUT](/files/-MTJoshSvRRbpSeBSkC3)

![รูป: Instruction Formats PUSH,  PULL](/files/-MTJqgDjp9WLg8sZXaC2)

![รูป: Instruction Formats MOV, IRQ, SET, WAIT, JMP](/files/-MTJt91hYTHLmlLBW95V)

การทำงานของ **SM** ที่เกี่ยวข้องกับขา **GPIO** แบ่งได้เป็น 5 กรณีคือ

* **Input Pins**: เป็นกลุ่มขาอินพุต และกำหนดขาเริ่มต้นด้วย `in_base`
* **Output Pins**: เป็นกลุ่มขาอินพุต และกำหนดขาเริ่มต้นด้วย `out_base`
* **Set Pins:** เป็นกลุ่มขาอินพุตหรือเอาต์พุต และกำหนดขาเริ่มต้นด้วย **`set_base`**
* **Side-Set Pins**: เป็นกลุ่มขาเอาต์พุต ใช้สำหรับ **Side-setting** และกำหนดขาเริ่มต้นด้วย `sideset_base`
* **Jump Pin:** เป็นขาอินพุตที่ใช้กำหนดเงื่อนไขสำหรับคำสั่ง **JMP**&#x20;

ในการเขียนโค้ดไมโครไพธอนเพื่อใช้งาน **PIO** จะต้องใช้โมดูล [`rp2`](https://github.com/micropython/micropython/blob/master/ports/rp2/modules/rp2.py) และถ้าจะเขียนโค้ดเพื่อนำไปใช้งานกับ **SM** ของ **PIO** จะต้องสร้างเป็นฟังก์ชันที่ระบุไว้ด้วย **Python Decorator** `@asm_pio()`

## โค้ดตัวอย่างที่ 1: PIO-based Pin Change Interrupt

ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อใช้งาน **StateMachine (SM)** ของ **PIO** จำนวน 2 ชุด โดยเลือกใช้ **SM0** และ **SM1** และตรวจสอบการเปลี่ยนแปลงของสถานะที่ขาอินพุต (ขา **GP18**) ซึ่งต่อกับวงจรปุ่มกดภายนอก (ทำงานแบบ **Active-Low Push Button**) แบ่งออกเป็น 2 กรณีคือ ขอบขาลง (**Falling Edge**) และขอบขาขึ้น (**Rising Edge**)

ในโค้ดได้มีการสร้างฟังก์ชัน `wait_falling_edge()` และ `wait_rising_edge()` ใช้สำหรับทำคำสั่งของ **SM0** และ **SM1** ตามลำดับ แต่จะต้องเขียนคำสั่ง `@asm_pio()` กำกับไว้ด้านบนด้วย&#x20;

การทำคำสั่งของ **SM0** และ **SM1** จะเป็นการทำคำสั่งแบบวนซ้ำโดยอัตโนมัติ ดังนั้นจึงมีการทำคำสั่งแรกเป็น `wrap_target()` และคำสั่งสุดท้ายเป็น `wrap()`

คำสั่งที่รอให้เกิดเหตุการณ์ที่ขาอินพุตคือ `wait()` เช่น รอให้ขาอินพุตเปลี่ยนเป็น 0 หรือ เปลี่ยนเป็น 1 จากนั้นให้สร้างอินเทอร์รัพท์ **IRQ** ไปยังซีพียู (เลือก **IRQ number** ได้ **0..3**) เพื่อให้ตอบสนองต่อเหตุการณ์ดังกล่าว และรอจนกว่าจะมีการเคลียร์อินเทอร์รัพท์ดังกล่าว

```python
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin

@asm_pio()
def wait_falling_edge():
    wrap_target()
    wait(0, pin, 0)  # wait for logic 0 on pin index 0
    irq(block, 0)    # set IRQ index 0 and wait for IRQ ack
    wait(1, pin, 0)  # wait for logic 1 on pin index 0
    wrap()

@asm_pio()
def wait_rising_edge():
    wait(0, pin, 0)  # wait for logic 0 on pin index 0
    wrap_target()
    wait(1, pin, 0)  # wait for logic 1 on pin index 0
    irq(block, 1)    # set IRQ index 1 and wait for IRQ ack
    wait(0, pin, 0)  # wait for logic 0 on pin index 0
    wrap()

events_counter = 0

def falling_handler(sm):
    global events_counter
    events_counter += 1
    print('Button  pressed: {}'.format(events_counter) )

def rising_handler(sm):
    global events_counter
    events_counter += 1
    print('Button released: {}'.format(events_counter) )
        
# use GP18 pin for push button
button = Pin(18, Pin.IN, Pin.PULL_UP)

# use StateMachine SM0 and SM1
sm0 = StateMachine(0, wait_falling_edge, in_base=button, freq=10000)
sm1 = StateMachine(1, wait_rising_edge,  in_base=button, freq=10000)
sm0.irq( falling_handler )
sm1.irq( rising_handler  )

sm0.active(1) # run SM0
sm1.active(1) # run SM1

try:
    while events_counter < 10:
        time.sleep_ms(10)
except KeyboardInterrupt:
    pass
finally:
    sm0.active(0) # stop SM0
    sm1.active(0) # stop SM1
    del sm0, sm1
time.sleep(0.1)
print('Done')
```

เราสามารถเขียนโค้ดใหม่ โดยใช้เพียง **StateMachine** เดียวแทนได้  (เช่น **SM** หมายเลข **0**) และกำหนดเหตุการณ์ขอบขาขึ้น (กดปุ่ม) และขาลง (ปล่อยปุ่ม) ให้ตรงกับการสร้างอินเทอร์รัพท์ **IRQ index 0** และ **1** ตามลำดับ เพื่อนำไปใช้ตรวจสอบโดย **IRQ Handler**  ของ **PIO** หมายเลข **0** โดยฟังก์ชัน `wait_pin_change()` สำหรับ **SM** ดังกล่าว

```python
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin

@asm_pio()
def wait_pin_change():
    irq(clear,0)     # clear IRQ index 0 
    irq(clear,1)     # clear IRQ index 1
    wrap_target()
    wait(0, pin, 0)  # wait for logic 0 on pin index 0
    irq(block,0)     # set IRQ index 0 and wait for IRQ ack
    wait(1, pin, 0)  # wait for logic 1 on pin index 0
    irq(block,1)     # set IRQ index 0 and wait for IRQ ack
    wrap()

events_counter = 0

def pin_change_handler( pio ):
    global events_counter
    flags = pio.irq().flags()
    action = ['DOWN','UP'][int(flags & 0x100 == 0x100)]
    events_counter += 1
    print('Events: {}'.format(events_counter), action)

# use GP18 pin for push button
button = Pin(18, Pin.IN, Pin.PULL_UP)
time.sleep(0.1)

# use StateMachine SM0
sm = StateMachine(0, wait_pin_change, in_base=button, freq=10000)
PIO(0).irq( pin_change_handler )
sm.active(1) # run SM

try:
    while events_counter < 10:
        time.sleep_ms(10)
except KeyboardInterrupt:
    pass
finally:
    PIO(0).irq(None)
    if sm.active():
        sm.active(0) # stop SM
    del sm
time.sleep(0.1)
print('Done')
```

## โค้ดตัวอย่างที่ 2: PIO-based IRQ Interrupt

ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อใช้งาน `StateMachine` (**SM**) หมายเลข **0** ของ **PIO** เพื่อให้สร้าง **IRQ** จาก **SM** ดังกล่าว โดยให้เกิดทุก ๆ **200 msec** หรือ **5 Hz** และตั้งค่าความถี่สำหรับการทำงานของ **SM** ไว้ที่ **2500 Hz**&#x20;

การทำให้เกิดอินเทอร์รัพท์จาก **SM** เราจะใช้คำสั่ง `irq(rel(0))` โดยเลือกใช้ **IRQ 0** และในการทำคำสั่งต่าง ๆ ไปตามลำดับของ **SM** เราจะเปิดและปิดด้วย `wrap_target()` และ `wrap()` เพื่อเป็นการกำหนดบล็อกคำสั่งที่จะต้องทำซ้ำไปเรื่อย ๆ โดยอัตโนมัติ&#x20;

เนื่องจากในกรณีนี้ **SM** ใช้ความถี่เท่ากับ **2500 Hz** ดังนั้นถ้าจะทำให้เกิดอินเทอร์รัพท์ที่อัตราคงที่ **5 Hz** ได้ จะต้องมีการทำคำสั่งของ **SM** เพื่อหน่วงเวลาก่อนทำคำสั่ง `irq()` เพื่อให้รวมเวลาทั้งหมดต่อรอบ ให้ได้เท่ากับ **2500/5 = 500** ไซเคิล

ในการทำงานแต่ละรอบ ที่กำหนดจุดเริ่มต้นโดยใช้คำสั่ง `wrap_target()` เราจะใช้ **Scratch Register X** เป็นตัวนับถอยหลังจนถึง **0** ตั้งค่าเริ่มต้นให้เท่ากับ **19** เมื่อทำคำสั่งนี้แล้ว ให้หน่วงเวลาไว้อีก **18** ไซเคิล ถ้าทำเสร็จแล้ว เวลาจะผ่านไป **(1+18) = 19** ไซเคิล

ค่าของรีจิสเตอร์ **X** นี้ จะถูกนำไปใช้เป็นตัวแปร **Loop Counter** ร่วมกับคำสั่ง `jmp()` ถ้าค่าของรีจิสเตอร์ **X** ลดลงทีละหนึ่งแล้วยังไม่เท่ากับ 0 ให้ทำซ้ำ ดังนั้นจะมีการวนซ้ำ **(19+1) = 20** ครั้ง

ในการทำซ้ำแต่ละครั้ง จะมีการทำคำสั่ง `nop()` และหน่วงเวลา (**Delay**) อีก **22** ไซเคิล ดังนั้นเมื่อนับรวมสำหรับการทำงานของลูป จะได้ **(19+1) \* (1+22+1) = 480** ไซเคิล และถ้าไปนับรวมกับคำสั่งก่อนหน้านี้ จะได้เป็น **19 + 480 = 499** ไซเคิล  จากนั้นจึงทำคำสั่ง `irq()` อีก **1** ไซเคิล เป็นคำสั่งสุดท้าย (รวมเป็น **500** ไซเคิล) ก่อนย้อนไปเริ่มให้ทั้งหมดด้วยคำสั่ง `wrap()`&#x20;

```python
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin

@asm_pio()
def pio_code():
    # (1+18) + (19+1)*(1+22+1) + 1 = 19 + 20*24 + 1 = 500 cycles
    wrap_target()       # [ 0]: set the wrap target
    #------------------------------------------------
    set(x, 19)    [18]  # [ 1]: set scratch register x to 19
                        # [18]: add a delay of 18 cycles
    label("loop")
    nop()         [22]  # [ 1]: no operation
                        # [22]: add a delay of 22 cycles
    jmp(x_dec, "loop")  # [ 1]: decrement x by 1
                        #       if x not zero jump to 'loop'
    irq( rel(0) )       # [ 1]: raise IRQ 0
    #------------------------------------------------
    wrap()              # [ 0]: go back to the wrap target

led = Pin( 25, Pin.OUT ) # use the onboard LED / GP25 pin
countdown = 20
saved_ts  = time.ticks_ms()

def irq_callback( sm ):
    global saved_ts, countdown, led
    now = time.ticks_ms()
    delta = time.ticks_diff(now, saved_ts)
    saved_ts = now
    countdown -= 1 # countdown
    print( '+{:03d} ms'.format(delta) )
    led.toggle() # toggle the onboard LED
    if countdown == 0:
        sm.active(0) # stop the state machine
        
# 2500 Hz / 500 = 5 Hz or 200 msec
sm = StateMachine(0, pio_code, freq=2500)
# set the function callback for StateMachine IRQ
sm.irq( irq_callback )
sm.active(1) # run the StateMachine 0

try:
    while countdown > 0:
        time.sleep_ms(10)
except KeyboardInterrupt:
    pass
finally:
    sm.active(0) # stop the StateMachine 0
print('Done')
```

## โค้ดตัวอย่างที่ 3: PIO-based LED Blink (Periodic Event)

โค้ดตัวอย่างนี้คล้ายกับตัวอย่างที่แล้ว แต่มีการใช้คำสั่ง `set()` ของ **PIO** ที่เกี่ยวข้องกับ **PINS** เพื่อเปลี่ยนสถานะลอจิกของ **LED** ที่ขา **GP25** ทุก ๆ **200 msec** หรือ **5 Hz** โดยตั้งความถี่สำหรับการทำงานของ **StateMachine (SM)** ให้เท่ากับ **5000 Hz** และการทำงานในแต่ละรอบ จะใช้เวลา **2 x 500 = 1000** ไซเคิล ดังนั้น **LED** จะกระพริบที่อัตรา **5000 /1000 = 5 Hz**

```python
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin

@asm_pio( set_init=(PIO.OUT_LOW) )
def pio_code():
    wrap_target()          # [ 0]: set the wrap target
    # 1 + (1+18) + (19+1)*(1+22+1) = 20 + 20*24 = 500 cycles
    #------------------------------------------------
    set( pins, 1 )         # [ 1]: set pin output to 1
    set(x, 19)    [18]     # [ 1]: set scratch register x to 19
                           # [18]: add a delay of 18 cycles
    label("loop_high")
    nop()         [22]     # [ 1]: no operation
                           # [22]: add a delay of 22 cycles
    jmp(x_dec,"loop_high") # [ 1]: decrement x by 1
                           #       if x not zero jump to 'loop'
    #------------------------------------------------
    set( pins, 0 )         # [ 1]: set pin output to 0
    set(y, 19)    [18]     # [ 1]: set scratch register x to 19
                           # [18]: add a delay of 18 cycles
    label("loop_low")
    nop()         [22]     # [ 1]: no operation
                           # [22]: add a delay of 22 cycles
    jmp(y_dec,"loop_low")  # [ 1]: decrement y by 1
                           #       if y not zero jump to 'loop'
    #------------------------------------------------
    wrap()                 # [ 0]: go back to the wrap target

button = Pin( 18, Pin.IN, Pin.PULL_UP )
led = Pin( 25, Pin.OUT ) # use the onboard LED / GP25 pin

sm = StateMachine(0, pio_code, freq=5000, set_base=led)

try:
    time.sleep(1.0)
    sm.active(1) # run the StateMachine 0
    while button.value():
        time.sleep_ms(10)
except KeyboardInterrupt:
    pass
finally:
    sm.active(0) # stop the StateMachine 0
    time.sleep(0.1)
    del sm
print('Done')
```

## โค้ดตัวอย่างที่ 4: PIO-based Reading of Rotary Encoder

ตัวอย่างนี้สาธิตการอ่านค่าอินพุต **A** และ **B** จากโมดูล **Rotary Encoder** โดยใช้ **StateMachine** (**SM**) ของ **PIO** (เลือกใช้ **SM** หมายเลข **0** ) เพื่อคอยตรวจสอบการเปลี่ยนสถานะอินพุต

ในตัวอย่างนี้ได้เลือกใช้ขา **GP16** และ **GP17** ของบอร์ด **Pico** สำหรับสัญญาณอินพุต **A** และ **B** ตามลำดับ และกำหนดให้ `in_base` ตรงกับขา **GP16** เป็น **PINS** หมายเลข **0** และ **GP17** หมายเลข **1**

**เ**ริ่มต้นการทำงานของ **SM** จะต้องรอให้ขา **GP17** เปลี่ยนจาก **High** เป็น **Low** หรือเกิดขอบขาลง  โดยใช้คำสั่ง `wait()` จากนั้นจะมีการอ่านค่าอินพุตที่ขาทั้งสอง โดยการเลื่อนบิต จำนวน 2 บิต จากขาอินพุตทั้งสอง โดยใช้คำสั่ง `in_()` เพื่อนำมาเก็บไว้ใน **ISR (Input Shift Register)**&#x20;

ถัดไปจึงนำค่าในรีจิสเตอร์ **ISR** ไปใส่ลงใน **RX FIFO** ด้วยคำสั่ง `push()` แล้วสร้างสัญญาณอินเทอร์รัพท์ ด้วยคำสั่ง `irq()` หลังจากนั้น ก็รอให้ขา **GP17** เปลี่ยนจาก **Low** ไปเป็น **High** แล้วทำขั้นตอนทั้งหมดซ้ำอีกรอบ

ฟังก์ชัน `irq_callback()` จะถูกเรียกเมื่อเกิดสัญญาณอินเทอร์รัพท์จาก **SM** ของ **PIO** และอ่านข้อมูลออกจาก **FIFO** แล้วนำมาตรวจสอบว่า เป็น 0 หรือ 1 เพื่อใช้ในการจำแนกว่า จะต้องเพิ่มหรือลดค่าของตัวนับ (**Increment=0 / Decrement=1**)

```python
import time
from machine import Pin
from rp2 import asm_pio, StateMachine, PIO

@asm_pio( set_init=(PIO.IN_HIGH,PIO.IN_HIGH) )
def rotary_encoder():
    irq( clear, 0 )    # clear IRQ 0
    wrap_target()
    wait( 0, pin, 1 )  # wait input_pins(1) goes LOW
    in_( pins, 2 )     # read input_pins into ISR
    push(block)        # push ISR to RX FIFO
    irq(0)             # raise IRQ 0
    wait( 1, pin, 1 )  # wait input_pins(1) goes HIGH
    wrap()

counter = 0

def irq_callback(sm):
    global counter
    if sm.irq().flags() > 0:
        direction = sm.get()
        if direction == 0: # up
            counter += 1
        else: # down
            counter -= 1
        action = ['INC','DEC'][direction]
        print( action, counter )

# use GP16 and GP17 pins for Rotary Encoder Inputs: A and B
sm = StateMachine(0, rotary_encoder, in_base=Pin(16), freq=10000)
sm.irq( irq_callback )
sm.active(1) # run the SM

try:
    while True: # press Ctrl+C to stop
        time.sleep_ms(10)
except KeyboardInterrupt:
    pass
finally:
    sm.active(0) # stop the SAM
    sm.irq( None )
    del sm
print('Done')
```

## โค้ดตัวอย่างที่ 5: PIO-based PWM Output for LED Dimming&#x20;

โดยปรกติแล้วเราสามารถสร้างสัญญาณ **PWM** เป็นเอาต์พุตที่ขา **GPIO** ของ **RP2040** ได้หลายช่องสัญญาณ แต่ตัวอย่างนี้สาธิตการใช้ **StateMachine** ของ **PIO** เพื่อสร้างสัญญาณ **PWM** (มองว่า เป็น **Soft PWM**) และนำไปใช้กับขา **GP25** ที่ต่อกับวงจร **LED** บนบอร์ด

ถ้าทดสอบการทำงานของโค้ดโดยใช้บอร์ด **Pico** จะเห็นว่า **LED** จะค่อย ๆ สว่างขึ้นและดับลง แล้วเกิดซ้ำไปเรื่อย ๆ

```python
import utime as time
from machine import Pin
from rp2 import PIO, StateMachine, asm_pio

@asm_pio( sideset_init=PIO.OUT_LOW )
def pwm_output():
    wrap_target()
    # 3 + (99*(2) + 1*(3)) = 204 cycles per loop -> 100_000/204 = ~490 Hz
    pull(noblock) .side(0) # if TX-FIFO is empty, copy X to OSR
                           # if not empty, get data and write to OSR
                           # side-setting: drive output low
    mov(x,osr)             # copy OSR to X
    mov(y,isr)             # copy ISR to Y
    label('pwmloop')
    jmp(x_not_y, 'skip')   # if x not equal y goto 'skip'
    nop()         .side(1) # no operation
                           # side-setting: drive output high
    label("skip")
    jmp(y_dec, "pwmloop")  # if Y is not zero, goto 'pwmloop' and decrement Y
    wrap()

sm = StateMachine(0, pwm_output, freq=100000, sideset_base=Pin(25))
sm.put(99)  # push a word (PWM max count) to TX-FIFO of SM0
sm.put(0)   # push a word (PWM level) to TX-FIFO of SM0

sm.exec('pull(block)')  # get a word from TX-FIFO 
sm.exec('mov(y,osr)' )  # copy OSR to Y (saved as PWM max count)
sm.exec('mov(isr,y)' )  # copy Y to ISR
sm.exec('pull(block)')  # get a word from TX-FIFO
sm.exec('mov(x,osr)' )  # copy OSR to X (saved as PWM level)

sm.active(1)  # run the SM0

try:
    while True: # press Ctrl+C to stop
        for i in range(201):
            if i <= 100:
                sm.put(i)
            else:
                sm.put(200-i)
            time.sleep_ms(10)
            
except KeyboardInterrupt:
    pass
finally:
    sm.active(0) # stop the SM0
    del sm
print('Done')
```

![](/files/-MT_-ivX5_UjHmQGkd-J)

![รูป: คลื่นสัญญาณเอาต์พุตที่วัดได้จริงโดยใช้เครื่องมือวัดออสซิลโลสโคป](/files/-MT_-oMMmkcQ_CdhfXxl)

## โค้ดตัวอย่างที่ 6: PIO-based 74HC595 Bit Shifting

ตัวอย่างนี้สาธิตการเลื่อนข้อมูลบิตขนาด 8 บิต ออกไปทีละบิต โดยการใช้ **StateMachine** ของ **PIO** เพื่อส่งข้อมูลไปยังไอซี **74HC595 8-bit Shift Register** และขาเอาต์พุตของไอซีดังกล่าว ก็นำไปควบคุมสถานะลอจิกของโมดูล **8-bit LED Bar (Active-Low)**

การทำงานของ **74HC595** จะใช้ขาสัญญาณดังนี้ (ศึกษาเพิ่มเติมได้จาก **Datasheet** \[[**1**](https://www.ti.com/lit/ds/scls041i/scls041i.pdf)]\[[**2**](https://assets.nexperia.com/documents/data-sheet/74HC_HCT595.pdf)])

* **Serial Clock (SCLK)** เป็นขาอินพุต รับสัญญาณ **Clock** กำหนดจังหวะการเลื่อนบิต
* **Serial Data (SDA)** เป็นขาอินพุต รับสัญญาณข้อมูลบิตเข้ามา
* **Load (LOAD)** เป็นขาสัญญาณอินพุต รับสัญญาณพัลส์ เพื่อทำให้วงจรภายในไอซี นำข้อมูลที่ได้รับ ไปอัปเดตสถานะบิตที่ขาเอาต์พุต **Q0..Q7** หลังจากที่ได้เลื่อนข้อมูลเข้าไปครบแล้ว
* **Q0..Q7** เป็นขาสัญญาณเอาต์พุต สำหรับข้อมูล 8 บิตแบบขนาน
* **Q7S** เป็นขาสัญญาณเอาต์พุตสำหรับข้อมูลบิตที่ถูกเลื่อนออกมา (ใช้สำหรับการต่อไอซีแบบ **Cascade** หรือ **Daisy Chain**)
* **Master Clear (/MCLR**) -- Active-low (ต่อตัวต้านทาน Pullup) ใช้สำหรับรีเซตข้อมูลภายใน
* **Output Enable (/OE)** -- Active-low (ต่อตัวต้านทาน Pullup) เปิดหรือปิดขาเอาต์พุต **Q0.. Q7**&#x20;

การทำงานของโค้ดแบ่งเป็น 2 กรณี จำแนกตามทิศทางการเลื่อนบิตดังนี้

* เลื่อนบิตออกไปทางขวา `PIO.SHIFT_RIGHT` หรือ **LSB First**
* เลื่อนบิตออกไปทางซ้าย `PIO.SHIFT_LEFT` หรือ **MSB First**

ในตัวอย่างนี้มีการใช้งาน **Auto-Pull** สำหรับ **TX-FIFO** และกำหนดค่า **Pull Threshold** ไว้เท่ากับ 8 ซึ่งหมายถึง การส่งข้อมูลไปยัง **74HC595** จะใช้กับข้อมูล 8 บิต เท่านั้น เมื่อเลื่อนข้อมูลออกไปครบ 8 บิต ให้โหลดข้อมูลใหม่โดยอัตโนมัติ มาใส่ลงใน **OSR** ยกเว้นถ้า **TX-FIFO** ไม่มีข้อมูล ให้หยุดรอชั่วคราว (**FIFO Stall**)&#x20;

การกำหนดขาใช้งานสำหรับ **RP2040** และใช้งานกับ **PIO** ที่นำไปต่อกับไอซี **74HC595** มี 3 ขา ดังนี้

* **SDA = GP11 (PIO output pins: number 0)**
* **SCLK = GP10 (PIO side-set pins: number 1)**
* **LOAD = GP9 (PIO side-set pins: number 0)**

โค้ดต่อไปนี้ใช้สำหรับการเลื่อนบิตออกไปทางขวา **LSB First** การส่งข้อมูลหนึ่งไบต์จากซีพียูไปยัง **SM** ของ **PIO** จะส่งผ่าน **TX FIFO** ซึ่งจะได้เป็นข้อมูลขนาด **32 บิต** และใช้เพียง **8 บิตล่าง** (**8 บิต** นับจากทางขวา คือ บิตที่ **0..7)**

```python
import time
from machine import Pin
from rp2 import asm_pio, StateMachine, PIO

# SDA=11, SCLK=10, LOAD=9

@asm_pio( out_shiftdir=PIO.SHIFT_RIGHT,
          autopull=True, pull_thresh=8,
          out_init=(PIO.OUT_LOW),
          sideset_init=(PIO.OUT_LOW,PIO.OUT_LOW) )
def send_out():
    wrap_target()
    set(x,7)          .side(0b00) # set X to 7 (use X as bit counter)
    # shift 8 bits out to the SCLK pin, LSB first
    label('loop')
    out(pins,1)       .side(0b00) # shift 1 bit from OSR to the first out pin
                                  # drive SCLK pin low
    jmp(x_dec,'loop') .side(0b10) # branch to loop if X is not zero, decrement X
                                  # drive SCLK pin high
    pull(ifempty)     .side(0b01) # get a word from TX-FIFO into OSR, stall if empty
                                  # drive LOAD pin high
    wrap()

sm = StateMachine(0, send_out, out_base=Pin( 11 ),
                     sideset_base=Pin( 9 ), freq=1_000_000)
sm.active(1) # run the SM
try:
    while True:
        for data in [0x00,0x01,0x81,0x42,0x24,0x18,0x55,0xaa,0xff]:
            sm.put( (data ^ 0xff) ) # use inverted bits, LSB first
            time.sleep_ms(500)
except KeyboardInterrupt:
    pass
finally:
    sm.put(0xff)
    sm.active(0)
```

โค้ดต่อไปนี้มีการดัดแปลงแก้ไขเล็กน้อย เพื่อทำให้เลื่อนบิตออกทางซ้ายหรือ **MSB First** และมีข้อสังเกตว่า ข้อมูลหนึ่งไบต์ที่ส่งไปยัง **SM** นั้นเป็นข้อมูลขนาด **32 บิต** ดังนั้นจะต้องเลื่อนบิตไปทางซ้าย **24 บิต**&#x20;

```python
import time
from machine import Pin
from rp2 import asm_pio, StateMachine, PIO

# SDA=11, SCLK=10, LOAD=9

@asm_pio( out_shiftdir=PIO.SHIFT_LEFT,
          autopull=True, pull_thresh=8,
          out_init=(PIO.OUT_LOW),
          sideset_init=(PIO.OUT_LOW,PIO.OUT_LOW) )
def send_out():
    wrap_target()
    set(x,7)          .side(0b00) # set X to 7 (use X as bit counter)
    # shift 8 bits out to the SCLK pin, LSB first
    label('loop')
    out(pins,1)       .side(0b00) # shift 1 bit from OSR to the first out pin
                                  # drive SCLK pin low
    jmp(x_dec,'loop') .side(0b10) # branch to loop if X is not zero, decrement X
                                  # drive SCLK pin high
    pull(ifempty)     .side(0b01) # get a word from TX-FIFO into OSR, stall if empty
                                  # drive LOAD pin high
    wrap()

sm = StateMachine(0, send_out, out_base=Pin( 11 ),
                     sideset_base=Pin( 9 ), freq=2000000)
sm.active(1) # run the SM0
try:
    while True:
        for data in [0x00,0x01,0x81,0x42,0x24,0x18,0x55,0xaa,0xff]:
            sm.put( (data ^ 0xff) << 24 ) # use inverted bits, MSB first
            time.sleep_ms(500)
except KeyboardInterrupt:
    pass
finally:
    sm.put(0xff)
    sm.active(0) # stop the SM0
```

![รูป: ตัวอย่างต่อวงจรโดยใช้ไอซี 74HC595 และโมดูล 8-bit LED Bar](/files/-MTTcRzmNtbeCMYp2DEY)

## กล่าวสรุป

เนื้อหาในส่วนนี้ได้แนะนำหลักการทำงานของ **PIO** ซึ่งเป็นวงจรใน **RP2040** และตัวอย่างการเขียนโค้ดไมโครไพธอนในเบื้องต้น  อย่างไรก็ตาม ถ้าต้องการจะเข้าใจหลักการทำงานและเขียนโปรแกรมสำหรับ **PIO** ของ **RP2040** แนะนำให้ศึกษาจากไฟล์ [**RP2040 Datasheet** ](https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf) (เช่น ในเนื้อหาบทที่ **3 PIO**)

{% hint style="info" %}
**เผยแพร่ภายใต้ลิขสิทธิ์**\
**Attribution-ShareAlike 4.0 International (**[**CC BY-SA 4.0**](https://creativecommons.org/licenses/by-sa/4.0/)**)**
{% endhint %}
