PIO Programming
เนื้อหาในส่วนนี้แนะนำการใช้งานวงจรภายในชิปไมโครคอนโทรลเลอร์ RP2040 ที่มีชื่อว่า Programmable I/O (PIO) และเขียนโปรแกรมควบคุมด้วยภาษาไมโครไพธอน -- เริ่มต้นเขียนเมื่อวันที่ 12 กุมภาพันธ์ พ.ศ. 2564
ภายในชิป RP2040 มีวงจรที่เรียกว่า PIO อยู่ 2 ชุด (เรียกว่า PIO0 & PIO1) แต่ละชุดประกอบไปด้วยหน่วยย่อยที่เรียกว่า State Machines (SMs) 4 ชุด (เรียกชื่อเป็น SM0..SM3 สำหรับ PIO0 และ SM4..SM7 สำหรับ PIO1) รวมทั้งหมดเป็น 2 x 4 = 8 ชุด

รูป: องค์ประกอบภายใน PIO
การทำงานของ 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 มายัง SMSM สามารถเข้าถึงขา 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

รูป: องค์ประกอบของ State Machine และการไหลของข้อมูล
ชุดคำสั่ง (Instruction Set) ของ PIO มีเพียง 9 คำสั่งเท่านั้น คือ {
JMP
, WAIT
, IN
, OUT
, PUSH
, PULL
, MOV
, IRQ
, SET
} ทุกคำสั่งมีขนาด 16 บิต และทำงานโดยใช้เพียงหนึ่งไซเคิลเท่านั้น (Single-Cycle Instruction Execution)
ตาราง: ชุดคำสั่งของ PIO
จากรูปแบบของคำสั่งในตาราง จะสังเกตเห็นได้ว่า แต่ละคำสั่งมีขนาด 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)
รูป: Instruction Formats IN, OUT

รูป: Instruction Formats PUSH, PULL

รูป: Instruction Formats MOV, IRQ, SET, WAIT, JMP
การทำงานของ 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()
ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อใช้งาน 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')
ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อใช้งาน
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 Hzimport 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')
ตัวอย่างนี้สาธิตการอ่านค่าอินพุต 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')
โดยปรกติแล้วเราสามารถสร้างสัญญาณ 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')


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

รูป: ตัวอย่างต่อวงจรโดยใช้ไอซี 74HC595 และโมดูล 8-bit LED Bar
เนื้อหาในส่วนนี้ได้แนะนำหลักการทำงานของ PIO ซึ่งเป็นวงจรใน RP2040 และตัวอย่างการเขียนโค้ดไมโครไพธอนในเบื้องต้น อย่างไรก็ตาม ถ้าต้องการจะเข้าใจหลักการทำงานและเขียนโปรแกรมสำหรับ PIO ของ RP2040 แนะนำให้ศึกษาจากไฟล์ RP2040 Datasheet (เช่น ในเนื้อหาบทที่ 3 PIO)
Last modified 2yr ago