PIO Programming
เนื้อหาในส่วนนี้แนะนำการใช้งานวงจรภายในชิปไมโครคอนโทรลเลอร์ RP2040 ที่มีชื่อว่า Programmable I/O (PIO) และเขียนโปรแกรมควบคุมด้วยภาษาไมโครไพธอน -- เริ่มต้นเขียนเมื่อวันที่ 12 กุมภาพันธ์ พ.ศ. 2564
Last updated
เนื้อหาในส่วนนี้แนะนำการใช้งานวงจรภายในชิปไมโครคอนโทรลเลอร์ RP2040 ที่มีชื่อว่า Programmable I/O (PIO) และเขียนโปรแกรมควบคุมด้วยภาษาไมโครไพธอน -- เริ่มต้นเขียนเมื่อวันที่ 12 กุมภาพันธ์ พ.ศ. 2564
Last updated
ภายในชิป 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
ชุดคำสั่ง (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()
ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อใช้งาน 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) เพื่อให้ตอบสนองต่อเหตุการณ์ดังกล่าว และรอจนกว่าจะมีการเคลียร์อินเทอร์รัพท์ดังกล่าว
เราสามารถเขียนโค้ดใหม่ โดยใช้เพียง StateMachine เดียวแทนได้ (เช่น SM หมายเลข 0) และกำหนดเหตุการณ์ขอบขาขึ้น (กดปุ่ม) และขาลง (ปล่อยปุ่ม) ให้ตรงกับการสร้างอินเทอร์รัพท์ IRQ index 0 และ 1 ตามลำดับ เพื่อนำไปใช้ตรวจสอบโดย IRQ Handler ของ PIO หมายเลข 0 โดยฟังก์ชัน wait_pin_change()
สำหรับ SM ดังกล่าว
ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อใช้งาน 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()
โค้ดตัวอย่างนี้คล้ายกับตัวอย่างที่แล้ว แต่มีการใช้คำสั่ง set()
ของ PIO ที่เกี่ยวข้องกับ PINS เพื่อเปลี่ยนสถานะลอจิกของ LED ที่ขา GP25 ทุก ๆ 200 msec หรือ 5 Hz โดยตั้งความถี่สำหรับการทำงานของ StateMachine (SM) ให้เท่ากับ 5000 Hz และการทำงานในแต่ละรอบ จะใช้เวลา 2 x 500 = 1000 ไซเคิล ดังนั้น LED จะกระพริบที่อัตรา 5000 /1000 = 5 Hz
ตัวอย่างนี้สาธิตการอ่านค่าอินพุต 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)
โดยปรกติแล้วเราสามารถสร้างสัญญาณ PWM เป็นเอาต์พุตที่ขา GPIO ของ RP2040 ได้หลายช่องสัญญาณ แต่ตัวอย่างนี้สาธิตการใช้ StateMachine ของ PIO เพื่อสร้างสัญญาณ PWM (มองว่า เป็น Soft PWM) และนำไปใช้กับขา GP25 ที่ต่อกับวงจร LED บนบอร์ด
ถ้าทดสอบการทำงานของโค้ดโดยใช้บอร์ด Pico จะเห็นว่า LED จะค่อย ๆ สว่างขึ้นและดับลง แล้วเกิดซ้ำไปเรื่อย ๆ
ตัวอย่างนี้สาธิตการเลื่อนข้อมูลบิตขนาด 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)
โค้ดต่อไปนี้มีการดัดแปลงแก้ไขเล็กน้อย เพื่อทำให้เลื่อนบิตออกทางซ้ายหรือ MSB First และมีข้อสังเกตว่า ข้อมูลหนึ่งไบต์ที่ส่งไปยัง SM นั้นเป็นข้อมูลขนาด 32 บิต ดังนั้นจะต้องเลื่อนบิตไปทางซ้าย 24 บิต
เนื้อหาในส่วนนี้ได้แนะนำหลักการทำงานของ PIO ซึ่งเป็นวงจรใน RP2040 และตัวอย่างการเขียนโค้ดไมโครไพธอนในเบื้องต้น อย่างไรก็ตาม ถ้าต้องการจะเข้าใจหลักการทำงานและเขียนโปรแกรมสำหรับ PIO ของ RP2040 แนะนำให้ศึกษาจากไฟล์ RP2040 Datasheet (เช่น ในเนื้อหาบทที่ 3 PIO)
เผยแพร่ภายใต้ลิขสิทธิ์ Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)