ชมรมโดรน

3. เซอร์โวและสัญญาณ PWM

เซอร์โวรู้องศาจากความกว้างพัลส์ 500–2500 µs ที่ส่งทุก 20 ms พร้อมเครื่องคิดเลขแปลงองศา

เซอร์โว (servo) คือมอเตอร์ตัวเล็กที่หมุนไปหยุดตามองศาที่สั่ง ในโดรนของชมรมมันเปิดประตูปล่อยบอล และดีดแขนยิงลูกดอก เซอร์โวมีสายสามเส้น คือ ไฟบวก กราวด์ และ สายสัญญาณ สายสัญญาณนี้แหละที่ Nano ใช้สั่ง

สั่งด้วย "ความกว้างของพัลส์"

เซอร์โวไม่ได้ฟังค่าตัวเลข มันฟัง ระยะเวลาที่สายสัญญาณเป็นไฟสูง ใน 1 รอบ

  • ทุก 20 มิลลิวินาที (ms) Nano ส่งพัลส์หนึ่งครั้ง = 50 ครั้งต่อวินาที = 50 Hz
  • พัลส์กว้าง 1.5 ms → เซอร์โวไปกลาง (90°)
  • พัลส์แคบลง → ไปทางหนึ่ง พัลส์กว้างขึ้น → ไปอีกทางหนึ่ง
สัญญาณ PWM พัลส์ 1.5 มิลลิวินาที ทุก 20 มิลลิวินาทีทุก 20 ms (50 ครั้งต่อวินาที)กว้าง 1.5 ms → กลาง (90°)สูงต่ำ

ลองเทียบปลายสองข้าง สังเกตว่าคาบ 20 ms เท่าเดิม เปลี่ยนแค่ความกว้างช่วงสูง

สัญญาณ PWM พัลส์ 0.5 มิลลิวินาที ทุก 20 มิลลิวินาทีทุก 20 ms (50 ครั้งต่อวินาที)กว้าง 0.5 ms → สุดข้างหนึ่ง (0°)สูงต่ำ สัญญาณ PWM พัลส์ 2.5 มิลลิวินาที ทุก 20 มิลลิวินาทีทุก 20 ms (50 ครั้งต่อวินาที)กว้าง 2.5 ms → สุดอีกข้าง (180°)สูงต่ำ

ตัวเลขตำราคือ 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. จำเวลาเริ่ม แล้ววนนาน 1,500,000 µs = 1.5 วินาที
  2. ตั้งขาเป็นสูง รอ pulseWidth µs
  3. ตั้งขาเป็นต่ำ รอเวลาที่เหลือให้ครบ 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);
}
องศาของเซอร์โวเทียบกับความกว้างพัลส์ตำราใช้ 500 ถึง 2500 ไมโครวินาที โค้ดชมรมใน Servo_Degree.ino ใช้ 490 ถึง 25500°45°90°135°180°ตำรา: 500 µs1500 µs2500 µsชมรม: 490 µs1520 µs2550 µs1520 = 490 + 90/180 × (2550 − 490) ตามสูตร map() ใน Servo_Degree.ino
องศากับความกว้างพัลส์: ตำราใช้ 500–2500 µs ชมรมวัดจริงได้ 490–2550 µs แถวบนสีฟ้าคือตำรา แถวล่างสีแดงคือชมรม ค่าเลื่อนกันเล็กน้อย

ลองกรอกดู เปลี่ยนค่าปลายทั้งสองข้างได้เมื่ออยากเทียบเซอร์โวคนละตัว

90° → พัลส์ 1500 µs (1.50 ms)

ตัวอย่างค่าที่ใช้จริง ดึงจาก Drone_Mission.ino และ config.hpp

ค่าคงที่pulse (µs)หมายถึง
BALL_12_CLOSE_US1200ประตู PORT3 ปิด
BALL_CH1_OPEN_US450เปิดช่อง 1
BALL_CH2_OPEN_US2000เปิดช่อง 2
S_CLOSE1500แขนยิงอยู่กลาง
S_LEFT1200แขนยิงปัดซ้าย
S_RIGHT2000แขนยิงปัดขวา

ค่าเหล่านี้ไม่ใช่ค่าสากล มาจากการลองกับกลไกจริง เปลี่ยนเซอร์โวหรือกลไก ต้องวัดใหม่

ค่าต่ำสุด/สูงสุดไม่ทุกฟังก์ชันเท่ากัน

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

ลองทำ

  1. คำนวณด้วยมือว่าเซอร์โวไป 45° ต้องใช้พัลส์เท่าไรตามสูตรชมรม (คำตอบ: 490 + 45/180 × 2060 = 1005 µs)
  2. ใช้ <ServoCalc> ด้านบน ใส่ 90° แล้วเปรียบเทียบกับค่าตำรา 1500 µs ต่างกันกี่ µs (คำตอบ: 20 µs)
  3. ถ้าไม่ต้องการให้ loop() ถูกบล็อกนาน 1.5 วินาที ลองเขียนฟังก์ชันที่ส่งพัลส์ 1 รอบต่อการเรียกหนึ่งครั้ง แล้วเรียกซ้ำจาก loop() (ดูหน้า millis)

พังตรงไหนบ่อย

  • เซอร์โวกระตุกตอนเปิดเครื่อง เพราะยังไม่ได้ส่งพัลส์ ค่าที่ขาเป็นสุ่ม ชมรมแก้ด้วยการสั่งปิดประตูทันทีใน setupDroneMissionMode() ด้วย close_all_ball_gate()
  • เซอร์โวไม่ไปครบองศา แรงดันไฟเลี้ยงต่ำหรือส่งพัลส์น้อยไป (ฟังก์ชัน release_servo_angle ค้างไว้ 1.5 วินาที ถ้ากลไกใหญ่ต้องการนานกว่านั้น ให้ส่งซ้ำอีกรอบ)
  • เซอร์โวสั่น พัลส์ถูกยืดจากอินเตอร์รัปต์ หรือส่งค่าเกินช่วงกลไก
  • เซอร์โวไม่ขยับเลย ไฟเลี้ยงเซอร์โวไม่ได้ต่อ หรือไม่ได้ต่อกราวด์ร่วม อ่าน ไฟเลี้ยงและสัญญาณรบกวน

On this page