- variant ที่ใช้ได้
- mtb-mpy เท่านั้น (ไม่มีการลิงก์ libbento_mpy.a เมื่อ BENTO_HAS_MPY=0)
โมดูล sensors คอมไพล์มาจาก modsensors.c กับไฟล์คู่ modsensors_*.c อีก 5 ไฟล์ซึ่งไม่มีไฟล์ใดอยู่ในแพ็กเกจ mtb-only ส่วนจุดเข้าใช้งานฝั่ง C ที่อยู่ใต้การเรียกทุกตัวในบทนี้ มีอยู่ในทั้งสองแพ็กเกจ และแสดงรายการไว้ที่ จุดเข้าฝั่ง C (ทั้ง 2 แพ็กเกจ) ส่วนเส้นทางไปหาจุดเหล่านั้นบน mtb-only คือ J1 — บัสของเซนเซอร์และ lock ของมัน
เป้าหมายของหัวข้อนี้
เมื่อจบบทนี้จะอ่านเซนเซอร์ทุกตัวบนบอร์ดจาก REPL ได้ คาดเดาได้ว่าการเรียกแต่ละครั้งคืนค่าอะไรและในหน่วยใด และที่มีประโยชน์กว่านั้นคือบอกได้ว่าแต่ละตัวทำอะไรเมื่อฮาร์ดแวร์ไม่มีอยู่ อีกทั้งจะรู้ว่าการเรียกครั้งเดียวแบบใดให้ค่าหกแกนที่สอดคล้องกัน และเหตุใดอีก 5 วิธีที่ได้ตัวเลข 6 ตัวชุดเดียวกันจึงไม่เทียบเท่ากัน
- Note
- หลักฐานบางเบา ระบุไว้ตั้งแต่ต้น แพ็กเกจนี้ไม่มีไฟล์ Python อยู่เลย: find template -name "*.py" คืนเฉพาะเครื่องมือสำหรับ build ตัวอย่างการใช้งานที่อยู่ในแพ็กเกจจริงสำหรับส่วนอุปกรณ์ต่อพ่วงทั้งหมดมีเพียงชิ้นเดียวคือ README.md:130-132 ซึ่งแสดง sensors.scan() และ sensors.read_all() ตัวอย่างอื่นทุกชิ้นในบทนี้ เขียนขึ้นเอง และกำกับไว้เช่นนั้น ไม่มีสิ่งใดในบทนี้ที่ให้รันไฟล์ที่ไม่มีอยู่
ลำดับการทำงานจริงของเฟิร์มแวร์
ทั้ง 6 ตัวไม่สมมาตรกัน โมดูล sensors ตั้งชื่อเซนเซอร์ไว้ 6 ตัว — bmi270, dps368, sht40, bmm350, capsense, pot (modsensors.c:1218-1223) — แต่มีเพียง BMI270 ที่ไม่มีเงื่อนไข อีก 5 ตัวอยู่หลัง flag BSP_HAS_* ในตารางลงทะเบียน (modsensors.c:1341-1362) และบน build ที่ flag เป็น 0 โมดูลย่อยนั้นไม่ได้แค่นิ่งเฉยแต่ ตัว attribute นั้นไม่มีอยู่ sensors.sht40 โยน AttributeError ไม่ใช่ OSError ให้ถาม hasattr(sensors, 'sht40') เมื่อต้องการรู้ว่า "บอร์ดนี้ติดตั้งตัวนั้นมาหรือไม่"
init() ส่งเสียงดังเมื่อล้มเหลวบางส่วน และตรงไปตรงมาเมื่อล้มเหลวทั้งหมด sensors.init() (modsensors.c:523) โยน OSError เฉพาะเมื่อบัสเองยกขึ้นไม่ได้ไดรเวอร์ย่อยแต่ละตัวที่ล้มเหลวจะพิมพ์หนึ่งบรรทัดและล้าง flag ของตน การเรียกจึงคืน False ในขณะที่เซนเซอร์ที่ปกติยังทำงานต่อไป:
if (!bmi270_init()) {
mp_printf(&mp_plat_print, "sensors: BMI270 init failed\n");
ok = false;
}
รูปแบบเดียวกันนี้ซ้ำสำหรับ DPS368, SHT40, BMM350, CapSense และ pot โดยแต่ละตัวอยู่ภายใน #if BSP_HAS_* ของตัวเอง (modsensors.c:538-571)
read_all() จับ lock เดียวตลอดการกวาดทั้งรอบ มันเริ่มด้วย sensor_i2c_lock(200) — เป็นสองเท่าของ timeout ต่อการเรียกหนึ่งครั้ง เพราะมันทำงานของเซนเซอร์ 6 ตัว — และสร้าง dict ขึ้นภายใน lock นั้น (modsensors.c:820) เซนเซอร์ที่อ่านไม่สำเร็จจะ ไม่มี key อยู่เลย ไม่ใช่มี key ที่เก็บค่า None โค้ดที่เขียน d['sht40']['temperature'] โดยไม่ตรวจสมาชิกภาพก่อนจะโยน KeyError บนบอร์ดที่ SHT40 หยุดตอบ และนั่นคือสัญญาณที่ตั้งใจให้เป็น
ทั้ง 6 ตัว ทีละการเรียก
หน่วย ค่าที่คืน และรูปแบบความล้มเหลว ตารางฉบับเต็มพร้อมเลขบรรทัดอยู่ที่ MicroPython — โมดูลย่อยของเซนเซอร์
| เซนเซอร์ | การเรียก | หน่วย | เมื่อล้มเหลว |
| BMI270 | acceleration(), gyroscope(), temperature(), motion(), chip_id() | m/s², deg/s, °C | OSError พร้อมระบุชื่อเซนเซอร์ |
| DPS368 | pressure(), temperature(), altitude(), pressure_temperature() | hPa, °C, m | OSError |
| SHT40 | temperature(), humidity(), temperature_humidity() | °C, RH | OSError |
| BMM350 | magnetic(), heading(), chip_id(), cal_reset(), cal_status() | µT, องศา 0..360 โดย 0 = ทิศเหนือ | OSError |
| CapSense | read(), buttons(), slider() | bool, bool, 0..100 | OSError ที่ระบุคอร์ผู้ตอบ |
| Pot | read(), percent(), voltage() | 0..65535, 0..100, 0..3.3 V | OSError |
หมายเหตุ 3 ข้อที่ตารางบรรจุไม่ได้:
- bmi270.temperature() คืออุณหภูมิของได ไม่ใช่อุณหภูมิห้อง มันอ่านเซนเซอร์ภายในของ IMU และมีค่าสูงกว่าอุณหภูมิแวดล้อมหลายองศา หากต้องการอุณหภูมิห้องให้ใช้ sht40.temperature() หรือ dps368.temperature()
- bmm350.heading() ต้องดู cal_status() ก่อนจึงจะเชื่อถือได้ ค่าทิศทางคำนวณจากเวกเตอร์ X/Y ดิบเทียบกับค่าชดเชยการสอบเทียบที่กำลังทำงานอยู่ (sensor_bmm350.c:791 และ :829) cal_reset() (modsensors_bmm350.c:54) เริ่มการประมาณค่านั้นใหม่ และ cal_status() (:62) คือวิธีตรวจว่าค่าลู่เข้าแล้วหรือยัง
- pot.voltage() ปรับสเกลเทียบกับค่าคงที่ ไม่ใช่เทียบกับค่าที่วัดได้ POTENTIOMETER_VDDA_V มีค่า 3.3f (sensor_potentiometer.c:19) และซอร์สบันทึกไว้ว่าผังวงจรระบุ VDD_1V8 ทั้ง 2 ค่ายังไม่ได้ปรับให้ตรงกัน และคำสั่งที่ยืนอยู่คือห้ามยืนยันค่าใดค่าหนึ่งจนกว่าจะวัด P15[1] จริง ให้ถือว่า voltage() เป็นค่านับที่ปรับสเกลแล้วซึ่งมีคำถามค้างอยู่ตามที่บันทึกไว้ และให้ใช้ read() หรือ percent() แทนสำหรับสิ่งที่สำคัญ
การเรียกเดียวที่ไม่ใช่ wrapper เพื่อความสะดวก
sensors.bmi270.motion() (modsensors.c:230) คืนค่าครบทั้งหกแกนใต้ lock ของบัส เดียว:
sensor_i2c_lock(100);
bool ok = bmi270_read_accel(&ax, &ay, &az) &&
bmi270_read_gyro(&gx, &gy, &gz);
sensor_i2c_unlock();
การเรียก acceleration() แล้วตามด้วย gyroscope() ให้ตัวเลข 6 ตัวชุดเดียวกัน แต่ ไม่ใช่ สิ่งเดียวกัน: นั่นคือคู่ lock/unlock สองคู่ และบัสเป็นของผู้อื่นในช่วงระหว่างกลาง บนบอร์ดที่อยู่นิ่งจะไม่มีวันเห็นความต่าง แต่บนบอร์ดที่กำลังถูกเขย่า — ซึ่งคือทุกบอร์ดที่ใช้ฝึกโมเดลการเคลื่อนไหว — เวกเตอร์ทั้งสองบรรยายคนละช่วงเวลา และผลที่หลอมรวมออกมาก็ผิดในแบบที่ unit test จับไม่ได้ ให้ใช้ motion() ทุกครั้งที่ตัวเลข 6 ตัวนั้นถูกใช้ร่วมกัน
ทีละขั้น
ขั้นที่ 1 — ยกเซนเซอร์ขึ้นแล้วอ่านคอนโซล
>>> import sensors
>>> sensors.init()
- สิ่งที่ควรสังเกต
- บนบอร์ดที่สมบูรณ์ดีทั้งหมดจะได้หนึ่งบรรทัด — sensors: all sensors initialized OK (modsensors.c:573-575) — แล้วตามด้วย True บรรทัดอื่นทุกบรรทัดที่การเรียกนี้พิมพ์ได้คือบรรทัดความล้มเหลว หนึ่งบรรทัดต่อหนึ่งไดรเวอร์ย่อย ในรูปแบบ sensors: BMI270 init failed (modsensors.c:533-536 และรูปแบบเดียวกันสำหรับเซนเซอร์ที่มี flag กำกับทุกตัวจนถึง :571) การคืนค่า False พร้อมบรรทัดแบบนั้นหนึ่งบรรทัดหมายความว่าเซนเซอร์ 5 ตัวปกติและอีกหนึ่งตัวไม่ปกติ ส่วน OSError หมายความว่าบัสเองยกขึ้นไม่ได้ และไม่มีการอ่านค่าใดเลย
ขั้นที่ 2 — การเรียก 2 ตัวที่มีตัวอย่างอยู่จริงในของที่ส่งมอบ
>>> sensors.scan()
>>> sensors.read_all()
- สิ่งที่ควรสังเกต
- scan() คืนแอดเดรสที่ตอบกลับ (J1 — บัสของเซนเซอร์และ lock ของมัน) ส่วน read_all() คืน dict ที่มี key เป็นเซนเซอร์ที่ตอบในการกวาดรอบนั้นพอดี — bmi270 แล้วตามด้วย dps368, sht40, bmm350, capsense, pot เท่าที่บอร์ดและ flag อนุญาต เซนเซอร์ที่อ่านไม่สำเร็จจะ ไม่ปรากฏ นี่คือคู่ที่แสดงไว้ใน README.md:130-132 และเป็นตัวอย่างด้านอุปกรณ์ต่อพ่วงเพียงตัวอย่างเดียวที่ส่งมาในแพ็กเกจ
ขั้นที่ 3 — การอ่านหกแกนที่สอดคล้องกัน
- ตัวอย่าง (เขียนขึ้นเอง — ไม่มี call site ในของที่ส่งมอบจริง)
import sensors
sensors.init()
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
print("accel m/s^2:", ax, ay, az)
print("gyro deg/s :", gx, gy, gz)
- สิ่งที่ควรสังเกต
- ค่า float 6 ตัว ขณะอยู่นิ่ง แกนหนึ่งของ accelerometer อ่านได้ประมาณ 9.8 และอีกสองแกนประมาณ 0 — แกนใดขึ้นกับท่าที่วางบอร์ดไว้ — และค่า gyro ทั้ง 3 ตัวอยู่ใกล้ 0 พร้อมสัญญาณรบกวนเล็กน้อย เมื่อเอียงบอร์ด ค่า 9.8 จะย้ายไปมาระหว่างแกน การเรียกนี้ไม่มี call site ที่ใดเลยในซอร์สที่ส่งมอบ จึงเป็นเหตุผลที่ตัวอย่างข้างบนเขียนขึ้นเองแทนการคัดมา
ขั้นที่ 4 — IMU ยังมีชีวิตอยู่จริงหรือไม่
- ตัวอย่าง (เขียนขึ้นเอง — ไม่มี call site ในของที่ส่งมอบจริง)
import sensors
sensors.init()
print(hex(sensors.bmi270.chip_id()))
- สิ่งที่ควรสังเกต
- ได้ 0x24 บน BMI270 ที่ปกติ (modsensors.c:253 ไดรเวอร์อยู่ที่ sensor_bmi270.c:65) ค่าอื่นใด หรือ OSError แยก "เซนเซอร์ไม่พูด" ออกจาก "เซนเซอร์พูด แต่รายงานค่าไร้สาระ" — ความต่างที่การเรียกอ่านค่าแยกให้ไม่ได้ เพราะทั้งบัสที่ค้างและเซนเซอร์ที่ค้างอยู่ที่ศูนย์ต่างก็ให้ค่าศูนย์ทั้งคู่ เป็นการเรียกที่ไม่มีผู้เรียกตัวอย่างจึงเขียนขึ้นเอง
ขั้นที่ 5 — เมื่อแมกนีโตมิเตอร์คือตัวที่ผิด
- ตัวอย่าง (เขียนขึ้นเอง — ไม่มี call site ในของที่ส่งมอบจริง)
import sensors
sensors.init()
d = sensors.bmm350_diag()
print(d['step'], d['init'], d['id'])
print(d['wr_st'], d['wr_ev'], d['rd_st'], d['rd_ev'])
- สิ่งที่ควรสังเกต
- dict ที่มี 12 key: step, init, att, s_init, s_en, pres, forced, wr_st, wr_ev, rd_st, rd_ev, id (modsensors.c:1269-1315 ฝั่งไดรเวอร์คือ bmm350_diagnose(), sensor_bmm350.c:863) จำนวนฟิลด์จำกัดไว้ที่สิบสองโดยเจตนาเพื่อไม่ให้ REPL ตัดข้อความ ส่วนคู่ของมันคือ sensors.bmm350_debug() (modsensors.c:1315) ซึ่งพิมพ์การดัมป์รีจิสเตอร์ออกมาแทนการคืนค่า ทั้งสองเป็นข้อมูลวินิจฉัยที่ไม่มีผู้เรียก ตัวอย่างในบทนี้จึงเขียนขึ้นเอง
เหตุผลที่เซนเซอร์ตัวนี้มีข้อมูลวินิจฉัยของตัวเองในขณะที่ตัวอื่นไม่มี: BMM350 อยู่บน I3C ไม่ใช่บน SCB0 จึงล้มเหลวได้ทั้งตัวตามลำพัง ในขณะที่บัสและเซนเซอร์ตัวอื่นทุกตัวยังปกติ (J1 — บัสของเซนเซอร์และ lock ของมัน)
กับดัก
- Warning
- key ที่หายไปคือส่วนหนึ่งของ API ไม่ใช่ข้อบกพร่อง read_all() ละเซนเซอร์ที่ล้มเหลวออก และไม่เคยเก็บค่า None ให้ตรวจด้วย in ก่อนทำดัชนี
-
โมดูลย่อยที่ไม่มีอยู่จะโยน AttributeError บน build ที่ BSP_HAS_SHT40=0 ชื่อ sensors.sht40 ไม่มีอยู่ ให้ตรวจความสามารถด้วย hasattr ไม่ใช่ด้วย try: ... except OSError
-
ห้ามประกอบ motion() ขึ้นใหม่จากการเรียก 2 ครั้ง คู่ lock/unlock สองคู่ไม่เท่ากับคู่เดียว ดู การเรียกเดียวที่ไม่ใช่ wrapper เพื่อความสะดวก
-
bmi270.temperature() ไม่ใช่อุณหภูมิของห้อง การรายงานค่านั้นเป็นอุณหภูมิแวดล้อมเป็นข้อผิดพลาดทางเอกสารที่เคยเกิดขึ้นมาแล้ว
-
pot.voltage() มีคำถามเรื่องแรงดันอ้างอิงที่ยังไม่ได้ข้อยุติ โค้ดปรับสเกลที่ 3.3 V ส่วนผังวงจรระบุ VDD_1V8 ห้าม "แก้" ฝั่งใดฝั่งหนึ่งโดยไม่วัด P15[1] ก่อน (sensor_potentiometer.c:6-19)
-
sensors.snapshot() ไม่มีอยู่ในแพ็กเกจนี้ — มันลงทะเบียนเฉพาะภายใต้ #ifdef USE_KIT_PSE84_EVAL_EPC2 (modsensors.c:1330-1332) และไม่มี BSP ของ Eva ส่งมาที่นี่ sensors.light() ก็ไม่มีเช่นกัน ซึ่งปรากฏในชุดตัวอย่างรุ่นเก่า และไม่ปรากฏในตารางโมดูลใดในซอร์สชุดนี้
กล่อง variant
| mtb-mpy | mtb-only |
| โมดูล sensors | มี | ไม่มี — modsensors.c ไม่อยู่ในแพ็กเกจ |
| โมดูลย่อย | 6 ตัว มี flag กำกับ | ไม่มี |
| ไดรเวอร์ที่อยู่ข้างใต้ | ไฟล์ sensor_*.c ชุดเดียวกัน | ไฟล์ sensor_*.c ชุดเดียวกัน เรียกโดยโค้ดที่เขียนเอง |
| การอ่านหลายค่าที่สอดคล้องกัน | sensors.bmi270.motion() | sensor_i2c_lock() เดียวครอบการเรียกไดรเวอร์ทั้ง 2 ครั้ง — ขั้นที่ 3 — จับ lock เดียวคร่อมการอ่านหลายรีจิสเตอร์ |
| ข้อมูลวินิจฉัย | bmm350_diag(), bmm350_debug() | bmm350_diagnose(), bmm350_debug_read() |