PIO Signaling and Measurement

การสร้างสัญญาณเอาต์พุตโดยใช้ PIO ของ RP2040 และการวัดสัญญาณเอาต์พุตจริงโดยใช้ออสซิลโลสโคปแบบดิจิทัล -- เริ่มต้นเขียนเมื่อวันที่ 15 กุมภาพันธ์ พ.ศ. 2564

โค้ดตัวอย่างที่ 1

มาเริ่มต้นด้วยการอ่านค่าความถี่ของ 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 เกิดขึ้นอย่างเห็นได้ชัด

โค้ดตัวอย่างที่ 2

ตัวอย่างนี้ลองสร้างสัญญาณเอาต์พุตที่ขา 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

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

โค้ดตัวอย่างที่ 3

ตัวอย่างนี้คล้ายกับตัวอย่างที่แล้ว แต่มีการเปลี่ยนจากการใช้คำสั่ง 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)

โค้ดตัวอย่างที่ 4

จากโค้ดตัวอย่างที่ 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%

โค้ดตัวอย่างที่ 5

ตัวอย่างนี้สาธิตการใช้งานขา 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)

โค้ดตัวอย่างที่ 6

ตัวอย่างนี้สาธิตการใช้ 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)

โค้ดตัวอย่างที่ 7

ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อให้ 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)

โค้ดตัวอย่างที่ 8

ตัวอย่างนี้คล้ายตัวอย่างที่ 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

กล่าวสรุป

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

Last updated