PIO Signaling and Measurement
การสร้างสัญญาณเอาต์พุตโดยใช้ PIO ของ RP2040 และการวัดสัญญาณเอาต์พุตจริงโดยใช้ออสซิลโลสโคปแบบดิจิทัล -- เริ่มต้นเขียนเมื่อวันที่ 15 กุมภาพันธ์ พ.ศ. 2564
มาเริ่มต้นด้วยการอ่านค่าความถี่ของ RP2040 ที่ใช้สำหรับไมโครไพธอน ดังนี้
import machine
freq = machine.freq() # get the CPU frequency in Hz
print( 'CPU frequency: {} MHz'.format(int(freq/1e6)) )
และจะได้ข้อความเอาต์พุตดังนี้ (ซึ่งจะได้ความถี่เท่ากับ 125 MHz)
CPU frequency: 125 MHz
ถ้าลองเขียนโค้ดไมโครไพธอน เพื่อเปิดใช้งานขา GPIO เช่น ขา GP14 ให้เป็นเอาต์พุต และใช้คำสั่งเพื่อเปลี่ยนสถานะลอจิก High และ Low ตามลำดับ และทำซ้ำไปเรื่อย ๆ แบบ
while
จากนั้นลองวัดสัญญาณเอาต์พุตที่ได้import machine
pin = machine.Pin(14, machine.Pin.OUT)
try:
while True:
pin.low()
pin.high()
except KeyboardInterrupt:
pass
print('Done')
จากรูปคลื่นสัญญาณที่ได้จะเห็นว่า สัญญาณมีการเปลี่ยนแปลงสถานะลอจิก แต่มีคาบเวลาที่ไม่คงที่ ค่อนข้างเลื่อนไปมา หรือมี Jitter เกิดขึ้นอย่างเห็นได้ชัด

รูป: คลื่นสัญญาณเอาต์พุตที่วัดได้
ตัวอย่างนี้ลองสร้างสัญญาณเอาต์พุตที่ขา GP14 เป็นเอาต์พุต และเขียนโปรแกรมสำหรับ PIO โดยเลือกใช้ StateMachine หมายเลข 0 (SM0) และใช้คำสั่ง
set()
เพื่อทำให้ขา GP14 เปลี่ยนหรือสลับสถานะลอจิก 1 กับ 0 ตามลำดับ ให้ทำซ้ำไปเรื่อย ๆ เริ่มต้นมีสถานะลอจิกเป็น Low และในการทำงานของ SM0 ได้เลือกใช้ความถี่ เช่น 2 MHzการทำคำสั่ง
set()
จะใช้เพียงหนึ่งไซเคิลเท่านั้น ดังนั้นถ้าทำสองคำสั่งที่อยู่ระหว่าง wrap_target()
กับ wrap()
ก็จะใช้ 2 ไซเคิล ความถี่เอาต์พุตที่ควรจะได้คือ 2 MHz /2 = 1 MHzข้อสังเกต: การใช้พารามิเตอร์
set_base
เป็นการกำหนดหมายเลขของขาเริ่มต้น (ใช้ได้สูงสุดถึง 5 ขา ที่เรียงติดกัน) และจะนำไปใช้กับ SM ในตัวอย่างนี้ได้ระบุอาร์กิวเนนต์เป็น Pin(14)
มีเพียงขาเดียวเท่านั้น และในส่วนของ Python decorator @asm_pio(
) มีการใช้พารามิเตอร์ set_init
เพื่อกำหนดทิศทางเป็นเอาต์พุตและกำหนดค่าเริ่มต้น โดยระบุอาร์กิวเมนต์เป็น (PIO.OUT_LOW)
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( set_init=PIO.OUT_LOW )
def pio_test():
wrap_target()
set(pins,1) # 1-cycle: drive output pin to 1
set(pins,0) # 1-cycle: drive output pin to 0
wrap() # 0-cycle: unconditional jump to wrap target
# create an instance of StateMachine 0 (SM0)
sm = StateMachine(0, pio_test, freq=2_000_000, set_base=Pin(14) )
sm.active(1) # run the SM0
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
sm.active(0) # stop SM0
หรือจะเปลี่ยนไปใช้วิธี Side Setting แทนก็ได้
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( sideset_init=PIO.OUT_LOW )
def test():
wrap_target()
nop() .side(1) # drive side pin high
nop() .side(0) # drive side pin low
wrap()
sm = StateMachine(0, test, freq=2_000_000, sideset_base=Pin(14) )
sm.active(1) # run the SM0
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
sm.active(0) # stop SM0

รูป: คลื่นสัญญาณเอาต์พุต ความถี่ 1 MHz
ถ้าเปลี่ยนความถี่ในการทำงานของ SM0 จาก 2 MHz ให้เป็น 50 MHz ความถี่ของสัญญาณเอาต์พุตที่ได้ควรจะเป็น 50 MHz /2 = 25 MHz ตามรูปคลื่นสัญญาณที่วัดได้จะเห็นว่า มีลักษณะไม่เป็นคลื่นสี่เหลี่ยม แต่มีอัตราการเปลี่ยนระดับแรงดันไฟฟ้าสองระดับ เท่ากับ 25 MHz

รูป: คลื่นสัญญาณเอาต์พุต ความถี่ 25 MHz
ตัวอย่างนี้คล้ายกับตัวอย่างที่แล้ว แต่มีการเปลี่ยนจากการใช้คำสั่ง
wrap_target()
และ wrap()
เพื่อกำหนดขอบเขตการทำลำดับของคำสั่งแบบวนซ้ำโดยอัตโนมัติ มาเป็นการใช้คำสั่ง label()
และ jmp()
แทน คำสั่ง
set()
มีการใช้งานอยู่ 2 คำสั่ง ตามลำดับ ใช้สำหรับกำหนดสถานะลอจิกของขาเอาต์พุต (ใช้ขา GP14
เพียงขาเดียวในตัวอย่างนี้) ให้เป็น High และ Low สลับกันไป (เริ่มต้นมีสถานะลอจิกเป็น Low)เมื่อทำคำสั่ง
set()
ทั้งสองคำสั่งแล้ว จึงทำคำสั่ง jmp()
เพื่อให้ย้อนกลับไปเริ่มทำคำสั่งที่อยู่ถัดไปจากบรรทัดที่มีการประกาศ label()
โดยรวมทั้งหมด มี 3 คำสั่ง แต่ละคำสั่งใช้เวลาหนึ่งไซเคิล ดังนั้นจึงใช้ 3 ไซเคิล ต่อหนึ่งรอบในตัวอย่างนี้ได้เลือกใช้ความถี่สำหรับ SM0 ของ PIO ให้เท่ากับ 5 MHz ดังนั้น จะได้ความถี่ของสัญญาณเอาต์พุตเท่ากับ 5 MHz /3 = 1.667 MHz มีช่วงเวลาที่เป็น High : Low เท่ากับ 1 : 2 หรือมีค่า Duty Cycle = 33.33%
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( set_init=(PIO.OUT_LOW) )
def pio_test():
label('loop')
set(pins,1) # 1-cycle: drive output pin to 1
set(pins,0) # 1-cycle: drive output pin to 0
jmp('loop') # 1-cycle: unconditional jump
# 5MHz/3 = 1.667 MHz
sm = StateMachine(0, pio_test, freq=5_000_000, set_base=Pin(14) )
sm.active(1)
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
sm.active(0)

รูป: คลื่นสัญญาณเอาต์พุต ความถี่ 1.667 MHz (Duty Cycle: 33.33%)
จากโค้ดตัวอย่างที่ 3 ลองมาเพิ่มการหน่วงเวลา (delay) หลังจากทำคำสั่ง
set()
แต่ละคำสั่งดูบ้าง โดยเขียนจำนวนไซเคิล (เป็นค่าคงที่เลขจำนวนเต็มบวกขนาดไม่เกิน 5 บิต หรือ 1..31) ในวงเล็บสี่เหลี่ยมต่อท้ายคำสั่ง เช่น [1]
หมายถึง หน่วงเวลาไว้หนึ่งไซเคิลหลังจากทำคำสั่งนั้นเสร็จแล้วจากนั้นมาลองวัดสัญญาณเอาต์พุตดูว่า มีการเปลี่ยนแปลงไปอย่างไร
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( set_init=(PIO.OUT_LOW) )
def pio_test():
label('loop')
set(pins,1) [1] # 2 cycles: drive output pin to 1
set(pins,0) [1] # 2 cycles: drive output pin to 0
jmp('loop') # 1 cycle: unconditional jump
# output frequency: 5MHz/5 = 1MHz
sm = StateMachine(0, pio_test, freq=5_000_000, set_base=Pin(14) )
sm.active(1)
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
sm.active(0)
จากรูปสัญญาณเอาต์พุตที่ได้ จะเห็นว่า มีความถี่ 1 MHz และความกว้างช่วงที่เป็น High และ Low มีอัตราส่วน 2:3 หรือ Duty Cycle = 40%

รูป: คลื่นสัญญาณเอาต์พุต ความถี่ 1 MHz (Duty Cycle: 40%)
ตัวอย่างนี้สาธิตการใช้งานขา GPIO จำนวน 2 ขา คือ GP14 และ GP15 โดยใช้เป็นเอาต์พุตเหมือนกันและจะถูกกำหนดสถานะลอจิกโดย StateMachine (SM) ของ PIO แต่ใช้งานต่างรูปแบบกัน เช่น ได้เลือกใช้ GP14 ใช้กับคำสั่ง
set()
แ ต่ GP15 ใช้สำหรับ .side()
ในรูปแบบที่เรียกว่า Side Setting เกิดขึ้นพร้อมกับการคำสั่งในบรรทัดเดียวกันความถี่ในการทำงานของ SM ในตัวอย่างนี้คือ 2 MHz ดังนั้น ความถี่ของสัญญาณเอาต์พุตที่ได้จากขา GP14 และ GP15 คือ 2 MHz /2 = 1 MHz แต่สถานะลอจิกของขาทั้งสองจะต่างกัน
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( set_init=(PIO.OUT_LOW), sideset_init=(PIO.OUT_LOW) )
def pio_test():
wrap_target()
set(pins,1) .side(0) # GP14=1, GP15=0
set(pins,0) .side(1) # GP14=0, GP15=1
wrap()
sm = StateMachine(0, pio_test, freq=2_000_000,
set_base=Pin(14), sideset_base=Pin(15) )
sm.active(1)
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
sm.active(0)

รูป: คลื่นสัญญาณเอาต์พุต ความถี่ 1 MHz (Duty Cycle: 50%) 2 ช่อง สถานะลอจิกตรงข้ามกัน
ตัวอย่างนี้สาธิตการใช้ StateMachine พร้อมกัน 4 ชุด (เลือกใช้ SM 0,1,2,3 ของ PIO 0) ให้สร้างสัญญาณเอาต์พุตที่ขา GP13, GP12, GP11, GP10 ตามลำดับ และให้รอสัญญาณอินพุตที่ขา GP14 เปลี่ยนจาก High เป็น Low ก่อน ซึ่งได้จากการต่อวงจรปุ่มกดจากภายนอก จากนั้นจึงเริ่มทำงาน
วงจร SM0 - SM3 จะเริ่มต้นสร้างสัญญาณพัลส์ไม่พร้อมกัน เนื่องจากมีการใช้คำสั่ง
nop() [...]
และหน่วงเวลาไว้แตกต่างกัน ก่อนเริ่มทำลำดับคำสั่งที่วนซ้ำระหว่าง wrap_target()
และ wrap()
การทำงานของ SM0 - SM3 ใช้ความถี่ 2 MHz และในการทำงานแต่ละรอบของกลำดับคำสั่งจะใช้เวลาเท่ากับ 4 ไซเคิล ดังนั้นจะได้ความถี่ 2 MHz /4 = 500 kHz
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( set_init=(PIO.OUT_LOW) )
def pio0():
wait(0, pin, 0) # wait for input button goes low
nop() [0]
wrap_target()
set(pins,1)
set(pins,0) [2]
wrap()
@asm_pio( set_init=(PIO.OUT_LOW) )
def pio1():
wait(0, pin, 0) # wait for input button goes low
nop() [1]
wrap_target()
set(pins,1)
set(pins,0) [2]
wrap()
@asm_pio( set_init=(PIO.OUT_LOW) )
def pio2():
wait(0, pin, 0) # wait for input button goes low
nop() [2]
wrap_target()
set(pins,1)
set(pins,0) [2]
wrap()
@asm_pio( set_init=(PIO.OUT_LOW) )
def pio3():
wait(0, pin, 0) # wait for input button goes low
nop() [3]
wrap_target()
set(pins,1)
set(pins,0) [2]
wrap()
button = Pin(14, Pin.IN, Pin.PULL_UP)
pins = [Pin(13),Pin(12),Pin(11),Pin(10)]
pio_funcs = [pio0, pio1, pio2, pio3]
sm_list = []
for i in range(4):
sm_list.append( StateMachine(i,
pio_funcs[i], freq=2_000_000,
in_base=button, set_base=pins[i] ) )
for sm in sm_list:
sm.active(1)
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
for sm in sm_list:
sm.active(0)

รูป: คลื่นสัญญาณเอาต์พุตทั้ง 4 ช่อง จากขา GP13-GP10 ตามลำดับ
ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อให้ PIO คอยตรวจสอบอินพุตแล้วให้เอาต์พุตเปลี่ยนสถานะของเอาต์พุตตามสถานะของอินพุตที่อ่านเข้ามา และใช้วงจรปุ่มกดสร้างสัญญาณอินพุต ไม่ กดได้ลอจิก High และแต่ถ้ากดปุ่มค้างไว้จะได้ลอจิก Low
ขา GP14 ถูกเลือกใช้เป็นขาอินพุต และขา GP15 ถ ูกเลือกใช้เป็นขาเอาต์พุต และกำหนดความถี่ในการทำงานเท่ากับ 5 MHz (1 Cycle = 200 ns)
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( set_init=(PIO.OUT_HIGH) )
def pio_test():
wrap_target()
label('start')
jmp(pin,'drive_high') # check jump pin for branch
set(pins,0) # drive output low
jmp('start')
label('drive_high')
set(pins,1) # drive output pin
wrap()
in_pin = Pin(14, Pin.IN, Pin.PULL_UP )
out_pin = Pin(15, Pin.OUT )
sm = StateMachine(0)
sm.init( pio_test, freq=5_000_000,
set_base=out_pin, jmp_pin=in_pin )
sm.active(1) # start the SM
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
sm.active(0)

รูป: คลื่นสัญญาณอินพุต CH1 และเอาต์พุต CH2 เมื่อมีการกดปุ่มแล้วปล่อย

รูป: ขอบขาลงของสัญญาณอินพุตจากนั้นจึงเป็นขอบขาลงของสัญญาณเอาต์พุต
ตัวอย่างนี้คล้ายตัวอย่างที่ 7 แต่ใช้วิธีตรวจสอบสถานะของอินพุตที่ขา GP14 โดยใช้การเลื่อนบิตเข้ามาหนึ่งตำแหน่ง ซึ่งจะเก็บไว้ใน ISR (Input Shift Register) จากนั้นก็สำเนาค่าจาก ISR ไปยัง OSR (Output Shift Register) แล้วเลื่อนบิตออกไปหนึ่งตำแหน่งไปยังขาเอาต์พุตที่ขา GP15
import utime as time
from rp2 import PIO, asm_pio, StateMachine
from machine import Pin
@asm_pio( out_shiftdir=PIO.SHIFT_RIGHT,
in_shiftdir=PIO.SHIFT_LEFT,
out_init=PIO.OUT_LOW )
def pio_test():
wrap_target()
in_(pins,1) # shift input pin into ISR
mov(osr,isr) # copy from ISR to OSR
out(pins,1) # shift data from OSR to output pin
wrap() # jump to wrap target
# create an instance of StateMachine 0 (SM0)
sm = StateMachine(0, pio_test, freq=15_000_000,
out_base=Pin(15), in_base=Pin(14) )
sm.active(1) # run the SM0
try:
while True:
time.sleep_ms(10)
except KeyboardInterrupt:
pass
finally:
sm.active(0) # stop SM0

รูป: คลื่นสัญญาณอินพุต CH1 และเอาต์พุต CH2 เมื่อมีการกดปุ่มแล้วปล่อย

รูป: ขอบขาลงของสัญญาณอินพุตจากนั้นจึงเป็นขอบขาลงของสัญญาณเอาต์พุต
เนื้อหาในส่วนนี้ได้นำเสนอตัวอย่างโค้ดไมโครไพธอน เพื่อนำไปทดลองใช้งานกับบอร์ด Raspberry Pi Pico (RP2040) สาธิตการทำงานของวงจร Programmable I/O (PIO) ที่อยู่ภายใน และใช้เครื่องมือวัดออสซิลโลสโคปในการวัดสัญญาณ I/O เพื่อให้เห็นพฤติกรรมการทำงานของ PIO เช่น การเปลี่ยนแปลงของสัญญาณและการตอบสนองในเชิงเวลาที่เกิดจากการทำงานคำสั่งพื้นฐานของ PIO
Last modified 2yr ago