9. C++ ขั้นสูง: interrupt, PROGMEM, บิต
งานที่ฮาร์ดแวร์ทำเองเบื้องหลัง การเก็บข้อมูลใน Flash แทน RAM และการเปิดปิดทีละบิต
เรื่องขั้นสูง สรุปก่อนอ่าน
- Interrupt ฮาร์ดแวร์สะกิดให้หยุดงานไปรับเรื่องด่วนแล้วกลับมา เหมือนกริ่งหน้าบ้านดังตอนกำลังทำอาหาร
- PROGMEM เก็บข้อมูลที่ไม่เปลี่ยนไว้ในที่เก็บโปรแกรม (Flash) เพื่อไม่กิน RAM ที่มีน้อย
- บิต เปิด-ปิดสวิตช์ทีละดวงในเลข 1 ไบต์
- ข้ามได้ ไม่ต้องรู้เรื่องพวกนี้ก็สั่งโดรนได้ กลับมาอ่านตอนติดปัญหา RAM หรือจังหวะเวลา
สามเรื่องนี้ไม่จำเป็นกับการสั่งโดรนวันแรก แต่โค้ดของชมรมใช้อยู่เบื้องหลัง และจะช่วยเมื่อ RAM เต็มหรือจังหวะผิด
1. Interrupt: ให้ฮาร์ดแวร์เรียกเรา
ปกติ loop() ทำทีละบรรทัดตามลำดับ ถ้า FC ส่งปุ่มมาระหว่างที่ loop() กำลัง delay(3000) จะรับทันไหม คำตอบคือรับทัน เพราะรับด้วย interrupt ฮาร์ดแวร์หยุดงานปัจจุบัน ไปรันฟังก์ชันที่ลงทะเบียนไว้ แล้วกลับมาทำต่อจากเดิม
โค้ดชมรมใช้อยู่แล้วโดยไม่รู้ตัว
// protocol.cpp
Wire.onRequest(requestEvent);
Wire.onReceive(receiveEvent);
void Arduino::receiveEvent(int howMany) {
while (1 < Wire.available()) {
char c = Wire.read();
}
int x = Wire.read();
m_remoteKey = x;
}receiveEvent ไม่ได้ถูกเรียกจาก loop() ไหนเลย ฮาร์ดแวร์ I2C เรียกเองเมื่อไบต์มาถึง ผลคือ m_remoteKey เปลี่ยนแม้ loop() กำลังรอ แล้ว keyDo ค่อยอ่านค่านั้นตอนวนมาถึง
ตัวอย่างชีวิตจริง: กริ่งหน้าบ้าน
คุณนั่งทำการบ้าน (loop) ไม่ได้เดินไปเปิดประตูทุก 10 วินาที เมื่อกริ่งดัง (interrupt) คุณหยุด ไปรับพัสดุ (ISR) แล้วกลับมาทำการบ้านต่อ
กฎของ ISR
- สั้นและเร็ว ห้าม
delay()ใน ISR ตั้งธงแล้วออก - ตัวแปรที่ ISR แก้ ต้องประกาศ
volatileเพื่อบอกคอมไพเลอร์ว่าค่าเปลี่ยนได้นอกสายตา
ตัวอย่างปุ่มบนขา 2 (ทดสอบคอมไพล์สำหรับ Nano แล้ว)
volatile bool buttonPressed = false;
void onButtonPress() {
buttonPressed = true;
}
void setup() {
Serial.begin(9600);
pinMode(2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(2), onButtonPress, FALLING);
}
void loop() {
if (buttonPressed) {
buttonPressed = false;
Serial.println("pressed");
}
}Nano มีขา interrupt ภายนอกแค่ขา 2 และ 3 (FALLING = ขาเปลี่ยนจากสูงเป็นต่ำ) หมายเหตุ ขา 3 คือ PORT4 ของโดรนที่ใช้ยิง จึงไม่ควรเอาไปใช้กับปุ่ม
ปิด interrupt ชั่วคราว
พัลส์เซอร์โวต้องการจังหวะแม่น ถ้ามี interrupt แทรกกลางพัลส์ ความกว้างจะยืดและเซอร์โวสั่น ชมรมจึงปิดชั่วขณะ
// shooting.ino
noInterrupts();
digitalWrite(pin, HIGH);
delayMicroseconds(pulse_us);
digitalWrite(pin, LOW);
interrupts();ปิดนานเกินจะพลาดข้อมูล I2C ที่เข้ามา จึงปิดเฉพาะช่วงพัลส์ที่ยาวไม่เกิน 2.4 ms แล้วเปิดคืนก่อนรอช่วงต่ำ
2. PROGMEM: เก็บข้อมูลใน Flash
Nano มี Flash 32 KB ที่เก็บโปรแกรม (ใช้ได้จริง 30,720 ไบต์ เพราะ bootloader กินส่วนหนึ่ง) แต่ RAM แค่ 2 KB ข้อมูลที่ไม่เปลี่ยนค่า เช่น ตารางเลข หรือข้อความยาว ควรเก็บใน Flash เพื่อไม่ให้ RAM เต็ม
#include <avr/pgmspace.h>
const int pulseTable[] PROGMEM = {600, 1150, 1500, 1900, 2400};
int p = pgm_read_word(&pulseTable[2]); // ได้ 1500PROGMEMบอกให้ใส่ตารางใน Flash- ต้องอ่านกลับด้วย
pgm_read_word(สำหรับ 2 ไบต์) ใช้pulseTable[2]ตรงๆ จะได้ค่ามั่ว เพราะ Flash กับ RAM เป็นคนละที่ที่ชิปอ่านด้วยคำสั่งต่างกัน - สำหรับข้อความใช้
F("...")ได้ เช่นSerial.println(F("hello"))ไม่กิน RAM
เมื่อไหร่ควรใช้: ตอนที่ขึ้น Low memory available หรือ Global variables use ... (80%) ตอนนี้โปรเจกต์ชมรมใช้ RAM 389 จาก 2,048 ไบต์ (18%) ยังไม่ต้องใช้ ตารางพัลส์ในโค้ดจริงใช้ const int ธรรมดา
3. บิต: ตัวเลขคือแถวของสวิตช์
ไบต์หนึ่งมี 8 บิต แต่ละบิตเป็น 0 หรือ 1 เหมือนสวิตช์ 8 ตัว ใช้เก็บสถานะหลายอย่างในไบต์เดียว หรือคุมขาของชิปโดยตรง
byte reg = 0b00000000;
reg |= (1 << 3); // เปิดบิต 3 → 00001000
reg &= ~(1 << 3); // ปิดบิต 3 → 00000000
reg ^= (1 << 3); // สลับบิต 3 → 00001000
if (reg & (1 << 3)) { /* บิต 3 เปิดอยู่ */ }1 << 3เลื่อนเลข 1 ไปทางซ้าย 3 ตำแหน่ง ได้00001000(เท่ากับ 8)|=OR: เปิด&= ~AND กับค่ากลับบิต: ปิด^=XOR: สลับ
โค้ดชมรมใช้บิตจริงที่ตำแหน่งนี้ ตอนแตกเลข 16 บิตเป็นสองไบต์ในบัฟเฟอร์ I2C
// protocol.cpp
m_i2cData[4] = (angle >> 8); // ไบต์สูง = เลื่อนขวา 8 บิต
// protocol.cpp
return (uint16_t)m_externData[0] | ((uint16_t)m_externData[1] << 8); // ประกอบกลับถ้าชมรมจะเขียนขาโดยตรงแทน digitalWrite ก็ทำได้ เช่น ขา 13 บน Nano คือบิต 5 ของ PORTB (PORTB |= (1 << 5);) เร็วกว่ามาก แต่ความเร็วระดับนั้นยังไม่จำเป็นกับเซอร์โว
ลองทำ
- ใส่
0b00001010ลงตัวแปรregแล้วเปิดบิต 0 ด้วย|=ได้ค่าอะไร (คำตอบ:00001011) - คำนวณว่า
1500 >> 8ได้เท่าไร และ1500 & 0xFFได้เท่าไร (คำตอบ: 5 และ 220, เพราะ 1500 = 5×256 + 220) - ใน
loop()เพิ่มSerial.println(Arduino::m_remoteKey);ชั่วคราว (ตัวแปรนี้เป็น public static) กดปุ่มรีโมทแล้วดูเลขปุ่มที่ไหลมาจาก interrupt
พังตรงไหนบ่อย
- ISR มี
delay()หรือSerial.printยาว ค้างหรือพลาดเหตุการณ์ ตั้งธงแล้วออก - ลืม
volatileloop()ไม่เห็นว่าค่าเปลี่ยน เพราะคอมไพเลอร์เก็บค่าเก่าไว้ - อ่าน PROGMEM ตรงๆ ได้ค่ามั่ว ต้อง
pgm_read_* - ปิด interrupt แล้วลืมเปิด
millis()หยุดเดิน โปรแกรมค้าง - สลับ
|กับ|||ทำกับบิต||ทำกับเงื่อนไข ผลต่างกันมาก