GPIO timer interrupt และ PWM
UAT 204 ไมโครคอนโทรลเลอร์และระบบสมองกลฝังตัว
บทเรียน
เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ
- ตั้งค่าขา GPIO เป็นอินพุตหรือเอาต์พุต พร้อม pull-up หรือ pull-down
- คำนวณค่า prescaler และ auto-reload ของ timer เพื่อสร้าง PWM ที่ความถี่ต้องการ
- สร้างความกว้างพัลส์สำหรับ servo และ ESC ตามช่วงค่าเริ่มต้นของ ArduPilot
- เปรียบเทียบ polling กับ interrupt และกรองสัญญาณสั่นของปุ่ม (debounce)
ทำไมต้องรู้
ทุกอย่างที่ MCU ทำกับโลกภายนอกเริ่มจากขาของชิป อ่านปุ่ม อ่านสัญญาณจากเซนเซอร์ สั่ง LED และสั่ง servo หรือ ESC ด้วยพัลส์ที่แม่นยำระดับไมโครวินาที ถ้าใช้ CPU วนนับเวลาเอง จะไม่แม่นยำและกินเวลาที่ควรทำงานอื่น MCU จึงมี timer ที่สร้างพัลส์ได้เองโดยไม่ต้องใช้ CPU และมี interrupt ที่ปลุก CPU เมื่อมีเหตุการณ์
GPIO
ขา GPIO (general-purpose input/output) ตั้งเป็นอินพุตหรือเอาต์พุตได้ ขาอินพุตที่ไม่ได้ต่อกับอะไรจะ “ลอย” และอ่านค่าสุ่ม จึงต้องมี pull-up (ดึงขึ้นหาไฟบวก) หรือ pull-down (ดึงลงกราวด์) ซึ่งมักมีในชิปให้เปิดได้ ปุ่มที่ต่อลงกราวด์กับ pull-up จะอ่านได้ 1 เมื่อไม่กด และ 0 เมื่อกด
Timer และ PWM
Timer นับสัญญาณนาฬิกา โดยหารด้วย prescaler (PSC) ก่อน แล้วนับขึ้นจนถึง auto-reload (ARR) จึงเริ่มใหม่ ตามคู่มือ timer ของ ST (AN4776) ความถี่ของรอบคือ ส่วนค่า CCR กำหนดจุดที่ขาเปลี่ยนจากสูงเป็นต่ำ จึงกำหนดความกว้างพัลส์
Servo และ ESC แบบ PWM อ่าน ความกว้างพัลส์ เป็นคำสั่ง ArduPilot ตั้งค่าเริ่มต้นของเอาต์พุตไว้ที่ SERVOx_MIN 1100, SERVOx_TRIM 1500 และ SERVOx_MAX 1900 µs ส่วนความถี่ของพัลส์ขึ้นกับอุปกรณ์ ต้องดูจากเอกสารของ servo หรือ ESC รุ่นที่ใช้
ตัวอย่างที่ 1 หาค่า PSC และ ARR
สมมติ timer รับนาฬิกา 84 MHz ต้องการให้นับทีละ 1 µs และคาบ 20 ms (ตัวอย่าง)
f_tim = 84_000_000
tick_hz = 1_000_000 # นับทีละ 1 µs
psc = f_tim // tick_hz - 1
period_us = 20_000
arr = period_us - 1
f_pwm = f_tim / ((psc + 1) * (arr + 1))
print(f"PSC = {psc}, ARR = {arr}, f_PWM = {f_pwm:.1f} Hz")
for pulse_us in (1100, 1500, 1900):
print(f"pulse {pulse_us} µs -> CCR = {pulse_us}, duty = {pulse_us / (arr + 1):.2%}")
PSC = 83, ARR = 19999, f_PWM = 50.0 Hz
pulse 1100 µs -> CCR = 1100, duty = 5.50%
pulse 1500 µs -> CCR = 1500, duty = 7.50%
pulse 1900 µs -> CCR = 1900, duty = 9.50%
เมื่อ timer นับทีละ 1 µs ค่า CCR เท่ากับความกว้างพัลส์เป็นไมโครวินาทีพอดี ทำให้อ่านโค้ดง่าย ถ้าชิปรับนาฬิกาต่างจากนี้ ต้องคำนวณ PSC ใหม่ และ ARR ต้องไม่เกินขนาดของตัวนับ (เช่น 65,535 สำหรับ timer 16 บิต)
Polling กับ interrupt
- Polling โปรแกรมหลักถามสถานะซ้ำ ๆ ง่าย แต่เปลือง CPU และอาจพลาดเหตุการณ์สั้น
- Interrupt ฮาร์ดแวร์หยุดโปรแกรมหลักชั่วคราวเพื่อเรียก ISR (interrupt service routine) แล้วกลับมาทำต่อ ISR ต้องสั้นที่สุด ตั้ง flag แล้วให้โปรแกรมหลักทำงานหนักต่อ
ปุ่มกลไกมีหน้าสัมผัสที่ สั่น (bounce) ช่วงสั้น ๆ ตอนกด ทำให้เห็นเป็นหลายครั้ง ต้อง กรอง (debounce) โดยยอมรับค่าใหม่เมื่อคงที่นานพอ
ตัวอย่างที่ 2 กรองการสั่นของปุ่ม
อ่านขาทุก 1 ms (1 = ไม่กด ด้วย pull-up) ยอมรับค่าใหม่เมื่อคงที่ 5 ms
samples = [1] * 5 + [0, 1, 0, 1, 1, 0, 0, 1, 0] + [0] * 20 + [1, 0, 1] + [1] * 15
raw_presses = sum(1 for a, b in zip(samples, samples[1:]) if a == 1 and b == 0)
stable, candidate, count, presses = 1, 1, 0, 0
for s in samples:
count = count + 1 if s == candidate else 1
candidate = s
if count >= 5 and candidate != stable:
stable = candidate
presses += stable == 0
print(f"raw falling edges: {raw_presses}, debounced presses: {presses}")
raw falling edges: 5, debounced presses: 1
การกดจริงหนึ่งครั้งแต่หน้าสัมผัสสั่นทำให้เห็นขอบขาลงหลายครั้ง หลังกรองเหลือหนึ่งครั้ง ค่า 5 ms เป็นค่าตัวอย่าง ต้องเลือกจากลักษณะของปุ่มที่ใช้
ปฏิบัติการประจำโมดูล
ปฏิบัติการ: สั่ง servo และอ่านปุ่มด้วย interrupt
- ต่อปุ่มเข้าขา GPIO เปิด pull-up ภายใน อ่านค่าแบบ polling แล้วนับจำนวนครั้งที่กดโดยไม่กรอง
- เปลี่ยนเป็น interrupt ที่ตั้ง flag แบบ
volatileแล้วกรองการสั่นในโปรแกรมหลัก เทียบจำนวนครั้งกับข้อ 1 - ใช้ PWM ของ Pico 2 สร้างพัลส์ 1100, 1500 และ 1900 µs ที่ความถี่ตามเอกสารของ servo ที่ใช้ วัดด้วยออสซิลโลสโคปหรือ logic analyzer
- ต่อ servo (ใช้แหล่งไฟแยก ต่อกราวด์ร่วม) แล้วสั่งตามพัลส์ทั้งสามค่า
- คำนวณ PSC และ ARR สำหรับนาฬิกาของ Pico 2 ด้วยหลักเดียวกับตัวอย่างที่ 1
ข้อผิดพลาดที่พบบ่อย
ระวัง
- ปล่อยขาอินพุตลอย โดยไม่มี pull-up หรือ pull-down
- ใช้ ARR เกินขนาดของตัวนับ
- ทำงานหนักใน ISR เช่น พิมพ์ข้อความหรือรอ
- ไม่กรองการสั่นของปุ่ม
- จ่ายไฟ servo จากขาไฟของ MCU จนไฟตก
สรุป
- GPIO ต้องมี pull-up หรือ pull-down เมื่อเป็นอินพุต
- ความถี่ PWM คือ และ CCR กำหนดความกว้างพัลส์
- ArduPilot ตั้งค่าเริ่มต้นของพัลส์ไว้ 1100–1900 µs กึ่งกลาง 1500 µs
- Interrupt ตอบสนองเหตุการณ์ได้ทันที ISR ต้องสั้น และปุ่มต้องกรองการสั่น
แบบฝึกตรวจความเข้าใจ
- MHz, PSC = 47, ARR = 999 ความถี่ PWM เท่าใด
- ในข้อ 1 ถ้า CCR = 250 duty cycle เท่าใด
- ต้องการนับทีละ 1 µs จากนาฬิกา 150 MHz ต้องตั้ง PSC เท่าใด
- ArduPilot ตั้งค่าเริ่มต้นกึ่งกลางของพัลส์ (
SERVOx_TRIM) ไว้เท่าใด - ทำไม ISR จึงควรสั้นที่สุด
เฉลย
- Hz
- 1500 µs
- ขณะอยู่ใน ISR งานอื่นและ interrupt ระดับเดียวกันต้องรอ ถ้านานเกินจะทำให้ระบบพลาดเวลา
สรุปสูตรสำคัญ
| ความถี่ PWM ของ timer | |
| duty cycle |
แหล่งอ้างอิงหลัก
- STMicroelectronics. General-purpose timer cookbook for STM32 microcontrollers (AN4776). link
- Raspberry Pi Ltd. RP2350 datasheet. link
- White, E. (2024). Making embedded systems: Design patterns for great software (2nd ed.). O'Reilly Media.
- ArduPilot Dev Team. ESC calibration. ArduPilot Copter documentation. link
อ่านเพิ่มเติม
ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล
ในชั้นเรียน / ภาคสนาม
ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย
หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz