PIO Programming

เนื้อหาในส่วนนี้แนะนำการใช้งานวงจรภายในชิปไมโครคอนโทรลเลอร์ RP2040 ที่มีชื่อว่า Programmable I/O (PIO) และเขียนโปรแกรมควบคุมด้วยภาษาไมโครไพธอน -- เริ่มต้นเขียนเมื่อวันที่ 12 กุมภาพันธ์ พ.ศ. 2564

Programmable I/O (PIO)

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

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

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

ซีพียูสามารถเขียนหรืออ่านข้อมูล โดยใช้วงจร 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 Register) ทั้งขาเข้าและขาออก (แยกกัน 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)

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

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

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

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

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

PIO Instruction Set

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

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

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

การทำงานของ 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

ในการเขียนโค้ดไมโครไพธอนเพื่อใช้งาน PIO จะต้องใช้โมดูล rp2 และถ้าจะเขียนโค้ดเพื่อนำไปใช้งานกับ 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() กำกับไว้ด้านบนด้วย

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

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

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 ดังกล่าว

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

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

เนื่องจากในกรณีนี้ 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()

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

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

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)

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

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

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

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

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

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

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

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

การทำงานของ 74HC595 จะใช้ขาสัญญาณดังนี้ (ศึกษาเพิ่มเติมได้จาก Datasheet [1][2])

  • 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

การทำงานของโค้ดแบ่งเป็น 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)

การกำหนดขาใช้งานสำหรับ 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)

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 บิต

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

กล่าวสรุป

เนื้อหาในส่วนนี้ได้แนะนำหลักการทำงานของ PIO ซึ่งเป็นวงจรใน RP2040 และตัวอย่างการเขียนโค้ดไมโครไพธอนในเบื้องต้น อย่างไรก็ตาม ถ้าต้องการจะเข้าใจหลักการทำงานและเขียนโปรแกรมสำหรับ PIO ของ RP2040 แนะนำให้ศึกษาจากไฟล์ RP2040 Datasheet (เช่น ในเนื้อหาบทที่ 3 PIO)

เผยแพร่ภายใต้ลิขสิทธิ์ Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)

Last updated