# 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 %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://think-embedded.gitbook.io/micropython/micropython-for-rp2040-pico/using-rp2040s-pio-with-micropython.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
