2. สาย I2C คุยกับ Flight Controller
แอดเดรส 0x33 กับ 0x34, บัฟเฟอร์ 6 ไบต์ที่ส่งคำสั่ง และไลบรารี Wire
Nano ไม่ได้ต่อสายตรงเข้ามอเตอร์ มันคุยกับ Flight Controller (FC) ผ่านสายสองเส้นที่เรียกว่า I2C (อ่านว่า ไอ-สแควร์-ซี) ทุกคำสั่งที่คุณเขียน เช่น take_off() หรือ set_height(120) สุดท้ายกลายเป็นไบต์ไม่กี่ตัววิ่งบนสายคู่นี้
สองเส้น สองหน้าที่
- SDA สายข้อมูล บิตวิ่งไปทีละตัว
- SCL สายจังหวะ ทุกครั้งที่สายนี้กระดิก ฝั่งรับอ่านบิตหนึ่งตัว
อุปกรณ์หลายตัวใช้สายคู่เดียวกันได้ จึงต้องมี แอดเดรส (address) เหมือนเลขห้อง ในโค้ดของชมรมมีสองเลข
| อุปกรณ์ | แอดเดรส | ดูได้ที่ |
|---|---|---|
| Arduino Nano | 0x33 | Wire.begin(0x33) ใน protocol.cpp |
| Flight Controller | 0x34 | const uint8_t FC_ADDR = 0x34; ใน protocol.cpp |
เลขขึ้นต้น 0x คือเลขฐานสิบหก 0x33 = 51 และ 0x34 = 52
ตัวอย่างชีวิตจริง: ตึกเดียว หลายห้อง
I2C เหมือนโทรศัพท์ภายในของโรงเรียน สายหลักเส้นเดียวต่อทุกห้อง แต่ละห้องมีเลขภายใน ถ้าห้องธุรการต้องการคุยกับห้องการเงิน ก็กดเลขห้องนั้น ห้องอื่นไม่ต้องสนใจ
Nano เป็น "ผู้รับ" ไม่ใช่ "ผู้ถาม"
ใน I2C ฝั่งหนึ่งเป็นผู้คุม (master) อีกฝั่งเป็นผู้ตาม (slave) บนโดรนนี้ FC เป็นผู้คุม Nano ประกาศตัวเป็นผู้ตามที่แอดเดรส 0x33 แล้วรอให้ FC มาเก็บคำสั่ง นี่คือคอนสตรักเตอร์ของคลาส Arduino
// protocol.cpp
Arduino::Arduino() {
//配置IIC读写
Wire.begin(0x33);
Wire.onRequest(requestEvent); // register event
Wire.onReceive(receiveEvent);
}Wire.begin(0x33)เข้าร่วมบัสในฐานะผู้ตามที่มีแอดเดรส 0x33Wire.onRequest(...)ลงทะเบียนฟังก์ชันที่จะถูกเรียกเมื่อ FC ขอข้อมูลWire.onReceive(...)ลงทะเบียนฟังก์ชันที่จะถูกเรียกเมื่อ FC ส่งข้อมูลมา
ฟังก์ชันสองตัวนี้ถูกเรียกเองโดยฮาร์ดแวร์ ไม่ต้องมี loop() ไปเรียก (หลักการนี้คือ interrupt ดูที่ ขั้นสูง)
// protocol.cpp
void Arduino::requestEvent(void) {
Wire.write(m_i2cData);
}
void Arduino::receiveEvent(int howMany) {
while (1 < Wire.available()) { // loop through all but the last
char c = Wire.read();
}
int x = Wire.read(); // ไบต์สุดท้าย
m_remoteKey = x;
}receiveEvent อ่านไบต์ที่ FC ส่งมา เก็บตัวสุดท้ายเป็นเลขปุ่มรีโมท (m_remoteKey) นี่คือทางที่ปุ่ม KEY ไปถึงโค้ดของเรา ส่วน requestEvent ส่งบัฟเฟอร์ m_i2cData ให้ FC ทุกครั้งที่ FC ถาม
บัฟเฟอร์ 6 ไบต์
คำสั่งทุกคำสั่งถูกเขียนลงอาร์เรย์ m_i2cData ยาว 6 ไบต์
// protocol.hpp
static char m_i2cData[6];โครงสร้างเหมือนกันทุกคำสั่ง
| ไบต์ | หน้าที่ |
|---|---|
[0] | ตัวนับ เพิ่มทีละ 1 ทุกครั้งที่สั่งคำสั่งใหม่ |
[1] | รหัสคำสั่ง เช่น F_ARM = 1, F_AUTO_LAND = 3, F_DIRECTION = 15 |
[2]–[5] | ข้อมูลของคำสั่งนั้น |
ตัวอย่างจริงคือ take_off() และ set_pos()
// protocol.cpp
void Arduino::take_off(void) //起飞
{
m_i2cData[0]++;
m_i2cData[1] = Arduino::F_ARM;
}
void Arduino::set_pos(int16_t x, int16_t y) //坐标点设置
{
m_i2cData[0]++;
m_i2cData[1] = Arduino::F_POS;
m_i2cData[2] = (uint16_t)(x & 0xffff);
m_i2cData[3] = (uint16_t)(x & 0xffff) >> 8;
m_i2cData[4] = (uint16_t)(y & 0xffff);
m_i2cData[5] = (uint16_t)(y & 0xffff) >> 8;
}ตัวนับไว้ทำอะไร
คำสั่ง take_off() ไม่ได้ "ส่ง" ทันที มันแค่เขียนลงบัฟเฟอร์ แล้วรอให้ FC มาอ่าน ถ้า FC อ่านบัฟเฟอร์ซ้ำสองรอบ มันจะเห็นรหัส 1 สองครั้ง แล้วไม่รู้ว่าเราสั่งขึ้นบินสองรอบ หรือค่าเก่าค้างอยู่ ตัวนับที่เพิ่มทุกครั้งช่วยแยกเรื่องนี้ ถ้าตัวนับเปลี่ยน แสดงว่าเป็นคำสั่งใหม่ (นี่คือเจตนาของการออกแบบ ซอร์สฝั่ง FC ไม่ได้อยู่ในชุดไฟล์ของชมรม)
ทำไมต้องแบ่งเป็นสองไบต์
ไบต์หนึ่งเก็บได้ 0–255 แต่ระยะ 300 cm เกินนั้น จึงแตก 16 บิตเป็นไบต์ต่ำกับไบต์สูง ตัวอย่าง 300 = 0x012C ไบต์ต่ำ 0x2C (44) ไบต์สูง 0x01 ฝั่งรับประกอบกลับด้วย ต่ำ | (สูง << 8) โค้ดที่อ่านความสูงจาก FC ก็ทำแบบนี้
// protocol.cpp
uint16_t Arduino::get_height_cm() const {
return (uint16_t)m_externData[0] | ((uint16_t)m_externData[1] << 8);
}ลำดับ "ต่ำก่อนสูง" เรียกว่า little-endian (เหมือนเขียนตัวเลขกลับด้าน)
อีกทาง: อ่านข้อมูลจาก FC
นอกจากรับปุ่ม Nano ยังขอข้อมูล 8 ไบต์จาก FC (รวมความสูง) ด้วย Wire.requestFrom
// protocol.cpp
static void iicRecv() {
const uint8_t FC_ADDR = 0x34;
const uint8_t LEN = 8;
Wire.requestFrom(FC_ADDR, LEN);
uint8_t i = 0;
while (Wire.available() && i < LEN) {
pArduino->m_externData[i++] = Wire.read();
}
while (i < LEN) {
pArduino->m_externData[i++] = 0;
}
}ลำดับคือ ขอ 8 ไบต์ → วนอ่านเท่าที่มี → ช่องที่เหลือเติม 0 เรียกใช้ผ่าน pArduino->poll() ดังนั้นก่อนอ่านความสูงต้อง poll() ก่อนเสมอ ไม่งั้นได้ค่าเก่า
// shooting.ino (reach_height_shoot_stable)
pArduino->poll();
uint16_t h = read_height_cm();
if (h == 0 || h > 300) { ... } // 0 = เซนเซอร์ไม่ตอบลองทำ
- คำนวณว่า
set_pos(1000, 0)จะได้ไบต์[2]และ[3]เป็นเลขอะไร (คำตอบ: 1000 = 0x03E8 →[2]=0xE8,[3]=0x03) - เปิด
protocol.hppหาenum _FUNCTION_SENDแล้วหาว่าF_DIRECTIONมีค่าเท่าไร (คำตอบ: นับต่อจากF_POS = 14ได้ 15) - เพิ่ม
Serial.printlnชั่วคราวเพื่อพิมพ์pArduino->get_height_cm()หลังpoll()แล้วยกโดรนขึ้นลงดูว่าค่าเปลี่ยนไหม
สังเกตเล็กน้อยในโค้ด
ฟังก์ชัน set_height() ใช้รหัสคำสั่งเดียวกับ set_pos() คือ F_POS ต่างกันแค่ข้อมูลที่ใส่ลงไป อ่านที่ ขั้นสูง: คำสั่งเดินทางยังไง เพื่อดูภาพรวมของคำสั่งทั้งหมด
พังตรงไหนบ่อย
- ความสูงอ่านได้ 0 ตลอด สาย I2C หลวม หรือลืม
poll()ก่อนอ่าน - Nano ไม่รับปุ่ม ตรวจว่าไม่มีโค้ดอื่นเรียก
Wire.begin()ซ้ำแล้วเปลี่ยนแอดเดรส - ใส่
delay()ยาวในloop()บัฟเฟอร์ถูกเขียนทับหลายรอบก่อน FC มาอ่าน คำสั่งบางตัวหายไป ใส่delayหลังคำสั่งบินพอให้โดรนทำเสร็จ - ใช้ขา A4/A5 ทำอย่างอื่น สองขานี้เป็นขา I2C ของ Nano