SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (MTB & µPython)
Loading...
Searching...
No Matches
J1 — บัสของเซนเซอร์และ lock ของมัน
variant ที่ใช้ได้
mtb-mpy และ mtb-only

ทุกอย่างในบทนี้เป็นภาษา C และทุกบรรทัดส่งมอบเป็นซอร์สในทั้ง 2 แพ็กเกจ ส่วนการเรียกฝั่ง MicroPython ที่แสดงในขั้นที่ 2 ทำงานบน mtb-mpy เท่านั้น ลำดับการทำงานฝั่ง C และพฤติกรรมบนบัสเหมือนกันทั้งสองฝั่ง

เป้าหมายของหัวข้อนี้

เมื่อจบบทนี้จะอธิบายได้ว่าเหตุใดการอ่านเซนเซอร์ทุกครั้งในเฟิร์มแวร์ชุดนี้จึงต้องอยู่ระหว่าง sensor_i2c_lock() กับ sensor_i2c_unlock() — ไม่ใช่เพราะเป็นธรรมเนียมการเขียนของโครงการ แต่เพราะมี master อีกรายใช้ SCB ตัวเดียวกันอยู่ — และจะเพิ่มอุปกรณ์ลงบนบัสได้โดยไม่ทำให้อีก 4 ตัวที่อยู่ก่อนแล้วพัง อีกทั้งจะบอกได้ว่าบอร์ดใดบอร์ดหนึ่งคอมไพล์ backend ของ CapSense ตัวใดใน 2 ตัวเข้ามา และเหตุใดการเลือกผิดตัวจึงได้อุปกรณ์ที่ไม่ตอบอะไรเลย

ลำดับการทำงานจริงของเฟิร์มแวร์

SCB หนึ่งตัว master 2 ราย บัสของเซนเซอร์คือ SCB0 I2C บนขา P8.0 (SCL) และ P8.1 (SDA) ซึ่ง sensor_i2c_init() กำหนดให้อย่างชัดแจ้ง แทนที่จะไว้ใจ BSP เพราะ BSP อาจ init ให้ CM33_NS หรือไม่ก็ได้ (sensor_i2c.c:88 การกำหนดขาที่ :99-100) คอมเมนต์ที่อยู่ถัดจากบล็อกกำหนดขาคือเหตุผลที่บทนี้มีอยู่: SCB0 ใช้ร่วมกัน บน CM33_NS — OPTIGA Trust M PAL ก็เป็น master บนบัสนี้ด้วย และยกบัสขึ้นที่ 100 kHz (sensor_i2c.c:104-110) คอมเมนต์เดียวกันนั้นบันทึกไว้ว่าเกิดอะไรขึ้นเมื่อ init บล็อกนี้ซ้ำขณะที่มันทำงานอยู่: DPS368, SHT40 และ BMI270 อ่านไม่ได้ทั้งหมด ในขณะที่ BMM350 บน I3C ไม่ได้รับผลกระทบ และเพราะ init ทำงานซ้ำทุกครั้งที่บูต การตัดไฟแล้วจ่ายไฟใหม่จึงกู้กลับไม่ได้

การกู้บัสมาก่อนการตั้งค่า sensor_i2c_init() เรียก sensor_i2c_bus_recover() ก่อนที่จะยกขาให้อุปกรณ์ต่อพ่วง เพื่อปลด slave ที่ยังกด SDA ค้างไว้ต่ำจากการโอนข้อมูลที่ถูกขัดจังหวะ (sensor_i2c.c:93-96) เมื่อ SCB เป็นเจ้าของขาแล้ว จะเคาะสัญญาณนาฬิกาไล่ slave ที่ค้างออกด้วยมือไม่ได้อีก ลำดับนี้จึงเป็นสาระ

lock คือข้อกำหนดการเรียกใช้ sensor_i2c_lock(uint32_t timeout_ms) (sensor_i2c.c:168) และ sensor_i2c_unlock() (:175) ครอบการโอนข้อมูลทุกครั้งในโค้ดที่ส่งมอบจริง ฟังก์ชันช่วยระดับรีจิสเตอร์ตั้งใจให้บางที่สุด — sensor_i2c_write_reg (:215), sensor_i2c_read_reg (:240) และรูปแบบระดับไบต์กับแบบดิบที่ :275, :279, :283, :300 — และไม่มีตัวใดจับ lock ให้ นี่เป็นการตัดสินใจเชิงออกแบบ ไม่ใช่การหลงลืม: การอ่านหลายรีจิสเตอร์จะสอดคล้องกันก็ต่อเมื่อมี lock เดียวครอบทั้งลำดับ

binding ฝั่ง MicroPython ทุกตัวใช้ 3 บรรทัดเดียวกันนี้ ตัวอย่างคือ sensors.bmi270.acceleration() บนสาขาของ AI Kit (modsensors.c:167-173):

float ax, ay, az;
sensor_i2c_lock(100);
bool ok = bmi270_read_accel(&ax, &ay, &az);
sensor_i2c_unlock();
if (!ok) {
mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("BMI270 accel read failed"));
}

สังเกตตำแหน่งที่ raise error: หลังการ unlock การ raise ภายในช่วงวิกฤตจะทิ้งบัสให้ค้างอยู่กับ task ที่ longjmp ออกไปแล้ว

ใครตอบบ้าง อุปกรณ์ 5 ตัว 4 ตัวอยู่บน SCB0:

อุปกรณ์ แอดเดรส บัส header
BMI270 IMU 0x68 SCB0 I2C sensor_bmi270.h:16
BMM350 แมกนีโตมิเตอร์ 0x15 I3C, CYBSP_I3C_SCL/SDA (sensor_bmm350.c:237-240) sensor_bmm350.h:20
DPS368 บารอมิเตอร์ 0x77 SCB0 I2C sensor_dps368.h:16
SHT40 ความชื้น 0x44 SCB0 I2C sensor_sht40.h:17
CapSense 4000T 0x08 ดู ทางแยกของ CapSense: หนึ่ง API สอง backend sensor_capsense.h:20

BMM350 เป็นตัวที่ต่างออกไป — มันอยู่บนบล็อก I3C ไม่ใช่ SCB0 — จึงล้มเหลวได้ตามลำพังในขณะที่อีก 3 ตัวยังปกติ และจึงมีคู่ฟังก์ชันวินิจฉัยของตัวเอง (bmm350_diagnose sensor_bmm350.c:863, bmm350_debug_read :559)

ทางแยกของ CapSense: หนึ่ง API สอง backend

sensor_capsense.h ประกาศ API ชุดเดียว มี 2 ไฟล์ที่นำไปใช้จริง และ capsense.mk เลือกระหว่าง 2 ไฟล์นั้นตอน build (capsense.mk:19-28):

  • I2C โดยตรงsensor_capsense.c (:60, :93, :115, :126) คุยกับ 0x08 บนบัสของเซนเซอร์จาก CM33_NS
  • snapshot ผ่าน IPCsensor_capsense_ipc.c (:75, :83, :95, :106) ถาม CM55 แทน ด้วย IPC_CMD_CONTROLS_STATE และ timeout 100 ms โดยอ่านจากแคชที่ cm55_capsense_tick() รีเฟรชทุก 50 ms ส่วน capsense_fetch_snapshot อยู่ที่ :33

กฎการเลือกตามถ้อยคำของ makefile เอง: บนบอร์ดฐาน QWA309 ตัว 4000T อยู่บนบัสจอแสดงผลและการสัมผัสที่ CM55 เป็นเจ้าของ backend แบบตรงจึงจะไปอ่านผิดบัส ดังนั้น BSP_HAS_QWA309_BASEBOARD=1 จึงเลือก backend แบบ IPC

Warning
แพ็กเกจ mtb-only ขาด backend แบบ IPC แพ็กเกจนี้ส่ง sensor_capsense.c มาให้ แต่ไม่มีทั้ง sensor_capsense_ipc.c และ capsense.mk ทั้งที่บอร์ดเป้าหมายเป็นบอร์ด QWA309 ผู้พัฒนาบน mtb-only ที่เรียก capsense_read() ในวันนี้จึงได้ backend แบบ I2C ตรงบนบัสที่อุปกรณ์ไม่ได้อยู่ และอ่านอะไรไม่ได้เลยจนกว่าแพ็กเกจจะมี backend แบบ IPC ให้อ่าน CapSense บน mtb-only ผ่านฝั่ง CM55 — cm55_controls_snapshot() (cm55_sensor_poll.h และข้อกำหนดของ hook ชนิด weak ใน Weak hook (ให้นิยามเอง ห้ามเรียก)) — แทนการเรียก capsense_read() นี่เป็นข้อบกพร่องของการจัดแพ็กเกจ รายงานไว้ที่นี่ ไม่ใช่การบันทึกว่าเป็นพฤติกรรมที่ตั้งใจ

ทีละขั้น

ขั้นที่ 1 — ยกบัสขึ้นแล้วถามว่ามีใครอยู่บ้าง (C ทั้งสอง variant)

จาก task ของ CM33_NS หลัง scheduler เริ่มทำงานแล้ว:

ตัวอย่าง (เขียนขึ้นเอง — ไม่มี call site ในของที่ส่งมอบจริง)
#include "sensor_i2c.h"
uint8_t addrs[16];
if (sensor_i2c_init()) {
if (sensor_i2c_lock(100)) {
int n = sensor_i2c_scan(addrs, (int)(sizeof addrs));
sensor_i2c_unlock();
/* n is the number of 7-bit addresses written into addrs[] */
(void)n;
}
}
สิ่งที่ควรสังเกต
sensor_i2c_init() คืนค่า true และเป็น idempotent — การเรียกครั้งที่สองคืน true ทันทีโดยไม่แตะ SCB (sensor_i2c.c:89-91) ซึ่งเป็นสิ่งที่ทำให้เรียกจาก init ของไดรเวอร์ทุกตัวได้อย่างปลอดภัย การ scan คืนแอดเดรสที่ตอบ ACK บนบอร์ดที่ปกติจะมี 0x68, 0x77 และ 0x44 รวมอยู่ด้วย ไม่มีอะไรพิมพ์ออกมา บัสนี้ไม่มีผลลัพธ์ทางคอนโซลของตัวเอง หาก sensor_i2c_lock() คืนค่า false แสดงว่าไม่ได้บัสภายในเวลาที่กำหนด ให้ถือว่าเป็น "master อีกรายกำลังโอนข้อมูลอยู่" ไม่ใช่ความผิดพลาดของฮาร์ดแวร์

ขั้นที่ 2 — คำถามเดียวกันจาก REPL (mtb-mpy เท่านั้น)

>>> import sensors
>>> sensors.scan()
สิ่งที่ควรสังเกต
รายการจำนวนเต็ม — แอดเดรสแบบ 7 บิตที่ตอบกลับมา ซึ่งเป็นชุดเดียวกับที่การ scan ฝั่ง C คืนมา เพราะ sensors.scan() คือ sensor_i2c_scan() ที่ทำงานใต้ lock ของ binding เอง (modsensors.c:782) นี่เป็นหนึ่งในสองการเรียกด้านอุปกรณ์ต่อพ่วงที่มีตัวอย่างการใช้งานอยู่จริงในแพ็กเกจ อีกตัวคือ sensors.read_all() และทั้งสองแสดงไว้ใน README.md:130-132 ของแพ็กเกจ การเรียกอื่นทุกตัวในหมวดนี้ไม่มี call site ในแพ็กเกจ จึงเป็นเหตุผลที่ตัวอย่างในบทนี้กำกับไว้ว่าเขียนขึ้นเอง

ขั้นที่ 3 — จับ lock เดียวคร่อมการอ่านหลายรีจิสเตอร์

ความผิดพลาดที่ขั้นนี้ป้องกันนั้นสังเกตยาก เพราะรุ่นที่ผิดทำงานได้เกือบตลอดเวลา

ตัวอย่าง (เขียนขึ้นเอง — ไม่มี call site ในของที่ส่งมอบจริง)
/* Coherent: one lock spans both reads, so the two vectors are from the
* same instant on the bus. */
float ax, ay, az, gx, gy, gz;
sensor_i2c_lock(100);
bool a_ok = bmi270_read_accel(&ax, &ay, &az);
bool g_ok = bmi270_read_gyro(&gx, &gy, &gz);
sensor_i2c_unlock();
สิ่งที่ควรสังเกต
การอ่านทั้ง 2 ครั้งสำเร็จ และตัวเลขทั้งหกบรรยายช่วงเวลาเดียวกัน หากแยกเป็นคู่ lock/unlock สองคู่ ทั้ง 2 ครั้งก็ยังสำเร็จ — แต่ task อื่นเข้ามาแย่งบัสระหว่างกลางได้และบนบอร์ดที่กำลังเคลื่อนไหว เวกเตอร์ของ accelerometer กับ gyroscope จะไม่ตรงกันว่า "ตอนนี้" คือเมื่อใด บน mtb-mpy รูปแบบนี้จัดมาให้พร้อมแล้วในชื่อ sensors.bmi270.motion() (modsensors.c:230) ซึ่งเป็นการเรียกเซนเซอร์ตัวเดียวในโมดูลที่คืนเวกเตอร์ 2 ชุดใต้ lock เดียว

ขั้นที่ 4 — เพิ่มอุปกรณ์โดยไม่ทำให้อีก 4 ตัวที่มีอยู่แล้วพัง

ตัวอย่าง (เขียนขึ้นเอง — ไม่มี call site ในของที่ส่งมอบจริง)
#define MY_DEVICE_ADDR (0x1E)
bool my_device_read_id(uint8_t *out)
{
bool ok = false;
if (sensor_i2c_lock(100)) {
ok = sensor_i2c_read_byte(MY_DEVICE_ADDR, 0x00, out);
sensor_i2c_unlock();
}
return ok;
}
สิ่งที่ควรสังเกต
การอ่านสำเร็จ และไดรเวอร์ที่ส่งมอบทั้ง 4 ตัวยังทำงานได้ กฎ 2 ข้อทำให้เป็นเช่นนั้น และทั้ง 2 ข้อเห็นได้จากไดรเวอร์ที่ส่งมอบจริง: จับ lock ภายในฟังก์ชันของตนเอง ไม่ใช่ให้ผู้เรียกจับ และปล่อย lock บน ทุกเส้นทางขาออก รวมถึงเส้นทางที่ล้มเหลวไดรเวอร์ที่คืนค่าออกไปก่อนกำหนดขณะยังถือบัสอยู่จะทำให้เซนเซอร์ตัวอื่นทั้งบอร์ดค้าง และอาการที่เห็นคือ "แดชบอร์ดค้าง" ไม่ใช่ "อุปกรณ์ตัวใหม่ล้มเหลว"

กับดัก

Warning
ห้ามเรียกฟังก์ชันช่วยระดับรีจิสเตอร์โดยไม่จับ lock sensor_i2c_read_reg() และพวกเดียวกันไม่จับ lock ให้ ฟังก์ชันเหล่านี้จะดูเหมือนทำงานได้บนโต๊ะทดลองที่เงียบ และจะล้มเหลวเมื่อ OPTIGA จับมือ (handshake) บน SCB ตัวเดียวกัน
ห้าม raise หรือคืนค่าออกไปขณะยังถือบัสอยู่ binding ที่ส่งมอบจริง unlock ก่อนแล้วจึง raise (modsensors.c:167-173) การสลับ 2 บรรทัดนี้ทำให้ mutex รั่วบนเส้นทางที่มีโอกาสได้ทดสอบน้อยที่สุดพอดี
ห้ามรัน Cy_SCB_I2C_Init() บน SCB0 ที่กำลังทำงานอยู่เพื่อ "รีเซ็ต" บัส การเรียก PDL นี้ต้องการให้บล็อกปิดอยู่ก่อน หากบล็อกเปิดอยู่ มันจะเขียนค่าตั้งค่าอุปกรณ์ต่อพ่วงไปได้ครึ่งเดียว — พลิกอัตราข้อมูล เปลี่ยนการจ่าย peri-clock ชี้ ISR ไปที่ใหม่ — และทำให้บัสแบบ open-drain ค้างสำหรับทุกอุปกรณ์บนบัสนั้น ความล้มเหลวนี้อยู่รอดข้ามการตัดไฟแล้วจ่ายไฟใหม่ เพราะ init ทำงานซ้ำทุกครั้งที่บูต (sensor_i2c.c:104-110) sensor_i2c_init() เป็น idempotent และการคืนค่าออกไปแต่เนิ่น ๆ ที่ sensor_i2c.c:89-91 คือสิ่งที่ปกป้องอยู่ ห้ามข้ามผ่านมันไป สำหรับบัสที่ค้างจริง ๆเครื่องมือที่ใช้คือ sensor_i2c_bus_recover() ซึ่งเป็นสิ่งที่ init เรียกเองอยู่แล้ว
อุปกรณ์ที่เงียบไม่ได้แปลว่าอุปกรณ์ตายเสมอไป ก่อนจะสงสัยฮาร์ดแวร์ให้ยืนยันก่อนว่ามันอยู่บนบัสใด BMM350 อยู่บน I3C และ CapSense 4000T อยู่บนบัสจอแสดงผลที่ CM55 เป็นเจ้าของบนบอร์ดนี้ — ทั้งสองตัวจะไม่มีวันปรากฏในการ scan SCB0 และการที่ทั้งสองไม่อยู่ใน sensors.scan() เป็นสิ่งที่คาดไว้ ไม่ใช่ความผิดพลาด
ห้ามใส่การ scan บัสไว้ในลูปที่ทำงานถี่ sensor_i2c_scan() ไล่เรียกทุก slave บนบัสทีละตัวขณะที่ถือ lock อยู่ การรันทุกเฟรมจะทำให้ task ดันข้อมูลอัตโนมัติที่อธิบายไว้ใน J3 — task ดันข้อมูลอัตโนมัติกับ sensor hub อดตาย

กล่อง variant

mtb-mpy mtb-only
ที่มาของบัส sensor_i2c.c ส่งมอบเป็นซอร์ส ไฟล์เดียวกัน พาธในแพ็กเกจเดียวกัน
ใครจับ lock ตัว binding ในทุกการเรียก ผู้เขียนโค้ดเอง ในไดรเวอร์ของตน
การ scan บัส sensors.scan() หรือ sensor_i2c_scan() sensor_i2c_scan()
backend ของ CapSense snapshot ผ่าน IPC เลือกโดย capsense.mk I2C ตรงเท่านั้น — capsense.mk และ sensor_capsense_ipc.c ไม่มีในแพ็กเกจ (ดูคำเตือนใน ทางแยกของ CapSense: หนึ่ง API สอง backend)
คอนโซล ไม่มีผลลัพธ์ของบัสบน variant ใดเลย ความล้มเหลวแสดงออกเป็นการคืนค่า false หรือ OSError มีเพียง [HB] t=lus tasks=u