3. เซอร์โวและสัญญาณ PWM
เซอร์โวรู้องศาจากความกว้างพัลส์ 500–2500 µs ที่ส่งทุก 20 ms พร้อมเครื่องคิดเลขแปลงองศา
เซอร์โว (servo) คือมอเตอร์ตัวเล็กที่หมุนไปหยุดตามองศาที่สั่ง ในโดรนของชมรมมันเปิดประตูปล่อยบอล และดีดแขนยิงลูกดอก เซอร์โวมีสายสามเส้น คือ ไฟบวก กราวด์ และ สายสัญญาณ สายสัญญาณนี้แหละที่ Nano ใช้สั่ง
สั่งด้วย "ความกว้างของพัลส์"
เซอร์โวไม่ได้ฟังค่าตัวเลข มันฟัง ระยะเวลาที่สายสัญญาณเป็นไฟสูง ใน 1 รอบ
- ทุก 20 มิลลิวินาที (ms) Nano ส่งพัลส์หนึ่งครั้ง = 50 ครั้งต่อวินาที = 50 Hz
- พัลส์กว้าง 1.5 ms → เซอร์โวไปกลาง (90°)
- พัลส์แคบลง → ไปทางหนึ่ง พัลส์กว้างขึ้น → ไปอีกทางหนึ่ง
ลองเทียบปลายสองข้าง สังเกตว่าคาบ 20 ms เท่าเดิม เปลี่ยนแค่ความกว้างช่วงสูง
ตัวเลขตำราคือ 500 µs ที่ 0°, 1500 µs ที่ 90°, 2500 µs ที่ 180° (1 ms = 1000 µs) ที่ 1.5 ms ช่วงสูงคิดเป็น 1.5/20 = 7.5% ของรอบ ส่วนอีก 92.5% เป็นช่วงต่ำ
ตัวอย่างชีวิตจริง: เคาะประตูเป็นจังหวะ
ลองนึกว่าคุณเคาะประตูทุก 20 วินาที และบอกคนข้างในด้วยความยาวของการเคาะ เคาะสั้นแปลว่า "เลี้ยวซ้าย" เคาะยาวแปลว่า "เลี้ยวขวา" คนข้างในฟังแค่ความยาว ไม่สนว่าคุณพูดอะไร เซอร์โวก็ฟังแบบนั้น ความยาวของพัลส์คือข้อความทั้งหมด
โค้ดจริงของชมรม
ฟังก์ชันที่ใช้ปล่อยบอลอยู่ใน Servo.ino ตั้งใจเขียนให้เห็นการส่งพัลส์ตรงๆ
// Servo.ino
void release_servo_angle(uint8_t port_pin, int pulseWidth) {
uint32_t t = micros();
while (micros() - t < 1500000) {
digitalWrite(port_pin, HIGH);
delayMicroseconds(pulseWidth);
digitalWrite(port_pin, LOW);
delayMicroseconds(20000 - pulseWidth);
}
}อ่านทีละบรรทัด
- จำเวลาเริ่ม แล้ววนนาน 1,500,000 µs = 1.5 วินาที
- ตั้งขาเป็นสูง รอ
pulseWidthµs - ตั้งขาเป็นต่ำ รอเวลาที่เหลือให้ครบ 20,000 µs
หนึ่งรอบใช้ 20 ms ใน 1.5 วินาทีจึงส่งพัลส์ได้ 75 รอบ ใช้เวลานี้เพื่อให้เซอร์โวมีเวลาหมุนไปถึงและล็อกตำแหน่ง
มีอีกเวอร์ชันใน shooting.ino และ DroneEasy.cpp ที่ ปิดอินเตอร์รัปต์ตอนส่งพัลส์ และ จำกัดความกว้าง 600–2400 µs กันค่าเพี้ยน
// shooting.ino
static inline int clampPulse(int us) {
const int MIN_US = 600, MAX_US = 2400;
return us < MIN_US ? MIN_US : (us > MAX_US ? MAX_US : us);
}
static inline void servoPulse50Hz(int pin, int pulse_us) {
pulse_us = clampPulse(pulse_us);
noInterrupts();
digitalWrite(pin, HIGH);
delayMicroseconds(pulse_us);
digitalWrite(pin, LOW);
interrupts();
delayMicroseconds(20000 - pulse_us);
}noInterrupts() ช่วยให้ความกว้างพัลส์ไม่ถูกยืดจากงานเบื้องหลัง (เช่น ตัวนับเวลา millis() หรือ I2C) ยิ่งพัลส์ยืดมาก เซอร์โวยิ่งสั่น
องศา → พัลส์ ในโค้ดชมรม
เซอร์โวแต่ละตัวไม่ได้ตรงตำราเป๊ะ ชมรมวัดเอง แล้วเขียนไว้ใน Servo_Degree.ino
// Servo_Degree.ino
const int SERVO_DEGREE_MIN_PULSE = 490; // pulse ที่ 0°
const int SERVO_DEGREE_MAX_PULSE = 2550; // pulse ที่ 180°
int servo_degree_to_pulse(int angle) {
angle = constrain(angle, SERVO_DEGREE_MIN, SERVO_DEGREE_MAX);
return map(angle, SERVO_DEGREE_MIN, SERVO_DEGREE_MAX,
SERVO_DEGREE_MIN_PULSE, SERVO_DEGREE_MAX_PULSE);
}ลองกรอกดู เปลี่ยนค่าปลายทั้งสองข้างได้เมื่ออยากเทียบเซอร์โวคนละตัว
90° → พัลส์ 1500 µs (1.50 ms)
ตัวอย่างค่าที่ใช้จริง ดึงจาก Drone_Mission.ino และ config.hpp
| ค่าคงที่ | pulse (µs) | หมายถึง |
|---|---|---|
BALL_12_CLOSE_US | 1200 | ประตู PORT3 ปิด |
BALL_CH1_OPEN_US | 450 | เปิดช่อง 1 |
BALL_CH2_OPEN_US | 2000 | เปิดช่อง 2 |
S_CLOSE | 1500 | แขนยิงอยู่กลาง |
S_LEFT | 1200 | แขนยิงปัดซ้าย |
S_RIGHT | 2000 | แขนยิงปัดขวา |
ค่าเหล่านี้ไม่ใช่ค่าสากล มาจากการลองกับกลไกจริง เปลี่ยนเซอร์โวหรือกลไก ต้องวัดใหม่
ค่าต่ำสุด/สูงสุดไม่ทุกฟังก์ชันเท่ากัน
servoPulse50Hz ตัดค่าให้อยู่ใน 600–2400 µs แต่ release_servo_angle ไม่ตัด ส่งค่า 450 หรือ 2600 ผ่านได้ ถ้าเซอร์โวหมุนสุดแล้วยังสั่นครวญคราง แปลว่าสั่งเกินช่วงที่กลไกไปได้ ให้ลดค่าลงทีละ 20–50 µs
สองเซอร์โวพร้อมกัน
ปล่อยสองช่องพร้อมกันได้โดยเปิดสองขาสูงพร้อมกัน แล้วดึงลงทีละขาตามความกว้างของแต่ละอัน
// Servo.ino
void release_dual_servo_angle(uint8_t port_pin1, int pulse1, uint8_t port_pin2, int pulse2) {
uint32_t t = micros();
while (micros() - t < 1500000) {
digitalWrite(port_pin1, HIGH);
digitalWrite(port_pin2, HIGH);
delayMicroseconds(min(pulse1, pulse2));
if (pulse1 < pulse2) digitalWrite(port_pin1, LOW);
else if (pulse2 < pulse1) digitalWrite(port_pin2, LOW);
else {
digitalWrite(port_pin1, LOW);
digitalWrite(port_pin2, LOW);
}
delayMicroseconds(abs(pulse1 - pulse2));
digitalWrite(port_pin1, LOW);
digitalWrite(port_pin2, LOW);
delayMicroseconds(20000 - max(pulse1, pulse2));
}
}ลำดับคือ ตั้งสองขาสูงพร้อมกัน รอให้ครบพัลส์ที่สั้นกว่า ตัวนั้นลงก่อน รอส่วนต่าง แล้วตัวที่ยาวกว่าลงตาม ที่เหลือคือช่วงต่ำจนครบ 20 ms
ลองทำ
- คำนวณด้วยมือว่าเซอร์โวไป 45° ต้องใช้พัลส์เท่าไรตามสูตรชมรม (คำตอบ: 490 + 45/180 × 2060 = 1005 µs)
- ใช้
<ServoCalc>ด้านบน ใส่ 90° แล้วเปรียบเทียบกับค่าตำรา 1500 µs ต่างกันกี่ µs (คำตอบ: 20 µs) - ถ้าไม่ต้องการให้
loop()ถูกบล็อกนาน 1.5 วินาที ลองเขียนฟังก์ชันที่ส่งพัลส์ 1 รอบต่อการเรียกหนึ่งครั้ง แล้วเรียกซ้ำจากloop()(ดูหน้า millis)
พังตรงไหนบ่อย
- เซอร์โวกระตุกตอนเปิดเครื่อง เพราะยังไม่ได้ส่งพัลส์ ค่าที่ขาเป็นสุ่ม ชมรมแก้ด้วยการสั่งปิดประตูทันทีใน
setupDroneMissionMode()ด้วยclose_all_ball_gate() - เซอร์โวไม่ไปครบองศา แรงดันไฟเลี้ยงต่ำหรือส่งพัลส์น้อยไป (ฟังก์ชัน
release_servo_angleค้างไว้ 1.5 วินาที ถ้ากลไกใหญ่ต้องการนานกว่านั้น ให้ส่งซ้ำอีกรอบ) - เซอร์โวสั่น พัลส์ถูกยืดจากอินเตอร์รัปต์ หรือส่งค่าเกินช่วงกลไก
- เซอร์โวไม่ขยับเลย ไฟเลี้ยงเซอร์โวไม่ได้ต่อ หรือไม่ได้ต่อกราวด์ร่วม อ่าน ไฟเลี้ยงและสัญญาณรบกวน