I2C
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”เมื่อจบบทเรียนนี้ คุณจะ
- สแกนบัส I2C และระบุอุปกรณ์ที่พบจาก address ได้
- อ่านและเขียนรีจิสเตอร์ของอุปกรณ์ โดยถือ lock ของบัสตลอดหนึ่งธุรกรรมและคืนทุกครั้ง
- ถอดรหัสภาพสัญญาณ I2C ได้ครบ start, address, read/write, ACK/NACK และ stop
ใช้เวลาประมาณ 70 นาที (แนวคิด 15 · ฝึก 25 · แล็บ 25 · เช็ก 5) แล็บใช้ logic analyzer สองช่องที่รับ 3.3 V ได้
ก่อนเริ่ม
หัวข้อที่มีชื่อว่า “ก่อนเริ่ม”ทวนจากบทก่อนหน้าสองข้อ
- UART ไม่มีสายสัญญาณนาฬิกา ผู้รับรู้จังหวะของบิตได้อย่างไร แล้วบัสที่มีสาย clock แยก (อย่าง I2C) ต่างกันอย่างไร
- ตัวอย่าง
06_raw_register_access.cที่อ่านในบทเรียน 1.1 ทำ read-modify-write ของPWR_CTRLภายในอะไร และทำไมต้องครอบทั้งขั้น
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”เปิด examples/12_i2c_transaction.c โปรแกรมนี้จำลองบัสเซนเซอร์ของบอร์ดแล้วพิมพ์ธุรกรรมแบบที่ decoder ของ logic analyzer แสดง
ทายก่อนรัน ว่าไบต์แรกบนสายเมื่อจะอ่านจาก BMI270 (address 0x68) คืออะไร
gcc -std=c11 -Wall -Wextra -o i2c_transaction examples/12_i2c_transaction.c./i2c_transactionคำตอบคือ D0 ไม่ใช่ 68 เพราะ address 7 บิตถูกเลื่อนซ้ายหนึ่งบิตแล้วต่อด้วยบิต R/W บรรทัด probe แสดงสิ่งที่การสแกนทำ ส่ง address แล้วดูว่ามีใครตอบ ACK
ส่วนบรรทัด read แสดงว่าการอ่านรีจิสเตอร์หนึ่งครั้งประกอบด้วยการเขียนหมายเลขรีจิสเตอร์ repeated START แล้วจึงอ่าน ทั้งหมดในธุรกรรมเดียว
1. หนึ่งธุรกรรมบนบัส I2C
หัวข้อที่มีชื่อว่า “1. หนึ่งธุรกรรมบนบัส I2C”I2C ใช้สองสาย SCL (clock ที่ master เป็นคนขับ) และ SDA (ข้อมูล) ทั้งสองเป็นแบบ open-drain มีตัวต้านทานดึงขึ้น อุปกรณ์หลายตัวต่อร่วมสายเดียวกันและแยกกันด้วย address
| เหตุการณ์ | บนสาย | ความหมาย |
|---|---|---|
| START | SDA ลงขณะ SCL สูง | เริ่มธุรกรรม |
| address + R/W | 8 บิต MSB ก่อน: address 7 บิตแล้วบิต R/W (0 เขียน 1 อ่าน) | เรียกอุปกรณ์หนึ่งตัว |
| ACK / NACK | บิตที่เก้า ผู้รับดึง SDA ลง = ACK ปล่อยไว้สูง = NACK | “ได้รับแล้ว” หรือ “ไม่มีใครอยู่” / “พอแล้ว” |
| ข้อมูล | 8 บิต MSB ก่อน ตามด้วย ACK/NACK ทุกไบต์ | ทีละไบต์ |
| repeated START | START ใหม่โดยไม่ STOP ก่อน | เปลี่ยนจากเขียนเป็นอ่านโดยไม่ปล่อยบัส |
| STOP | SDA ขึ้นขณะ SCL สูง | จบธุรกรรม |
อุปกรณ์ส่วนใหญ่บนบัสเป็น “แฟ้มรีจิสเตอร์” ตัวอย่าง 06_raw_register_access.c
ของ SDK อธิบายว่า sensor_i2c_read_reg() เขียนหมายเลขรีจิสเตอร์ “with NO stop” แล้วตามด้วย repeated START กับการอ่าน
ซึ่ง “is what the parts expect and what you would get wrong writing it yourself” และบางอุปกรณ์ เช่น SHT40 ไม่มีรีจิสเตอร์เลย ใช้การส่งคำสั่งแล้วอ่านคำตอบ
ไฟล์เดียวกันจึงมี sensor_i2c_write_raw() กับ sensor_i2c_read_raw() ไว้สำหรับกรณีนั้น
2. สแกนแล้วพิสูจน์สายก่อนเชื่อไดรเวอร์
หัวข้อที่มีชื่อว่า “2. สแกนแล้วพิสูจน์สายก่อนเชื่อไดรเวอร์”การสแกนคือการ probe ทุก address ในช่วง 0x08 ถึง 0x77 แล้วจดตัวที่ตอบ ACK บัสเซนเซอร์ของบอร์ดคือ SCB0 ที่ P8.0 (SCL) และ P8.1 (SDA)
ทำงานที่ 1.8 V และ 400 kHz และ address ที่คาดว่าจะพบบนบอร์ดนี้อยู่ในตาราง bus_device_name() ของ
01_i2c_bus_scan.c
| address | อุปกรณ์ |
|---|---|
| 0x08 | CapSense PSoC 4000T (บอร์ดฐาน) |
| 0x18 | TLV320DAC3100 audio codec |
| 0x44 | SHT40 ความชื้นและอุณหภูมิ |
| 0x68 | BMI270 IMU |
| 0x77 | DPS368 ความดันและอุณหภูมิ |
สองเรื่องที่ตัวอย่างเตือนไว้: BMM350 (0x15) อยู่บน I3C ซึ่งเป็นอุปกรณ์คนละตัว “and never appears here” และ address ที่ไม่อยู่ในตาราง “is not an error — it is a board you have added something to”
การพบ address ยังไม่ใช่การพิสูจน์ว่าเป็นชิปที่คิด 02_read_imu.c
สอนว่า “PROVE THE WIRE FIRST” อ่านรีจิสเตอร์ 0x00 ก่อน ได้ 0x24 คือ BMI270 จริง ได้ 0xFF หรืออ่านไม่ได้คือบัสหรือชิปมีปัญหา ได้ค่าอื่นคือชิปคนละตัวที่ address เดียวกัน
3. lock ของบัส: หนึ่งธุรกรรมหนึ่ง lock และคืนทุกทาง
หัวข้อที่มีชื่อว่า “3. lock ของบัส: หนึ่งธุรกรรมหนึ่ง lock และคืนทุกทาง”SCB0 มีหลายเจ้าของ บท J1 ของเอกสาร SDK ระบุว่า PAL ของ OPTIGA Trust M ก็เป็น master บน SCB0 ด้วย และ task เบื้องหลังอ่านเซนเซอร์ทุก 100 ms
ฟังก์ชันอ่านเขียนของ SDK ไม่ถือ lock ให้ ผู้เรียกต้องครอบเองด้วย sensor_i2c_lock() กับ sensor_i2c_unlock() คอมเมนต์ใน 01_i2c_bus_scan.c
อธิบายว่าถ้าไม่ถือ “It works, it keeps working, and then one day a repeated START from this task lands between the address phase and the data phase
of the auto task’s read and both come back wrong.”
/* Step 2 — take the bus. Never scan without this. */if (!sensor_i2c_lock(SCAN_LOCK_TIMEOUT_MS)) { /* Someone still holds it after a full second. The likeliest cause is * the hazard in the header block: the auto task was suspended between * its own lock and unlock. Resuming it lets it finish and release. */ printf(" sensor_i2c_lock(%u ms) TIMED OUT — someone holds the bus\r\n", (unsigned)SCAN_LOCK_TIMEOUT_MS); if (auto_was_running) { printf(" resuming the auto task so it can release the mutex\r\n"); sensor_auto_start(); } return SDK_EX_BUSY;}ที่มา: 01_i2c_bus_scan.c บรรทัด 143-155 (Apache-2.0, tesaiot-pse84-devkit-sdk)
กติกาที่ได้จากตัวอย่างเหล่านี้
- lock มี timeout เสมอ และเมื่อหมดเวลาให้คืนผลว่า busy ไม่ใช่สรุปว่าบัสเสีย
- หนึ่ง lock ต่อหนึ่งชุดข้อมูล ไม่ใช่ต่อหนึ่งรีจิสเตอร์ 02_read_imu อ่านความเร่ง gyro และอุณหภูมิภายใน lock เดียว เพื่อให้ทั้งสามมาจากจังหวะเดียวกัน และอุปกรณ์แบบสั่งแล้วรอคำตอบต้องถือ lock ข้าม “the command, the wait AND the response” (06_raw_register_access)
- คืน lock ทุกทางออก ในตัวอย่างสแกนมีบรรทัด “There is no path out of here that skips this.” และบท J1 ชี้ว่าแม้การแจ้ง error ก็ต้องทำหลัง unlock
- ไม่ถือ lock ข้ามการหน่วงเวลายาว ๆ มันขวางทุกงานที่รอบัสอยู่
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”examples/12_i2c_transaction.c ทำงานเป็นสามท่า
- ท่าที่ 1
address_byte()เลื่อน address 7 บิตแล้วใส่บิต R/W - ท่าที่ 2
probe()จำลองการสแกนสี่ address รอบ 0x68 ตัวที่มีอุปกรณ์ตอบ ACK ที่เหลือ NACK - ท่าที่ 3
read_register()พิมพ์การอ่าน chip id หนึ่งไบต์ และการอ่านความเร่งหกไบต์ในธุรกรรมเดียว (burst read)
ลองแก้แล้วทายก่อนรัน
- เปลี่ยนไปอ่าน DPS368 (0x77) ไบต์ address ตอนเขียนและตอนอ่านเป็นอะไร
- ถ้าแยกการอ่านหกไบต์เป็นหกธุรกรรม ธุรกรรมละหนึ่งไบต์ จำนวนไบต์บนสายเพิ่มเป็นเท่าไร และทำไมตัวอย่างของ SDK บอกว่าแบบ burst “both faster and ATOMIC”
- ใส่
0x10(DFR0522 RGB matrix ที่ต่อกับบัส 3.3 V ของ header) ลงในรายการอุปกรณ์ แล้ว probe ดู
ฝึกเติม
หัวข้อที่มีชื่อว่า “ฝึกเติม”เปิด practice/12_i2c_trace.c มีช่องให้เติม 4 จุด
addr_byte()ประกอบไบต์ addressparse_addr_byte()แยกกลับ และปฏิเสธ address ที่อยู่นอกช่วง 0x08 ถึง 0x77encode_read_reg()สร้างลำดับเหตุการณ์ของการอ่านรีจิสเตอร์ รวม NACK ที่ไบต์สุดท้ายscan()สแกนแบบเดียวกับsensor_i2c_scan()และตัดผลเมื่อที่เก็บเต็ม
gcc -std=c11 -Wall -Wextra -o i2c_trace practice/12_i2c_trace.c && ./i2c_traceลองเองก่อนอย่างน้อย 15 นาที แล้วเปิด solution/12_i2c_trace.c คอมเมนต์ในเฉลยอธิบายสองจุดที่มักถูกข้าม ทำไมไม่มี STOP ระหว่างเลือกรีจิสเตอร์กับการอ่าน และทำไม master ต้องตอบ NACK ที่ไบต์สุดท้าย
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”ตอบคำถาม 5 ข้อใน quiz.yaml (บนเว็บไซต์อยู่ท้ายหน้านี้) ครอบคลุมเป้าหมายทั้งสามข้อ ตอบถูกตั้งแต่ 4 ข้อขึ้นไปถือว่าจบบทเรียน
งาน: สแกนบัสเซนเซอร์ของบอร์ดด้วยตัวอย่างของ SDK แล้วจับธุรกรรม I2C จริงบนบัส 3.3 V ของ header ด้วย logic analyzer
ส่วนที่ 1 สแกนด้วย SDK
- build ด้วย
make build -j ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/sensors/01_i2c_bus_scanแล้ว flash ถอดสายเสียบใหม่ - จาก serial console จด address ทุกตัวที่ตอบ และชื่อที่ตัวอย่างพิมพ์ เทียบกับตารางในแนวคิดข้อ 2 ตัวไหนไม่พบ ตัวไหนเกินมา
- build อีกครั้งด้วย
SDK_EXAMPLE_CM33=cm33/sensors/02_read_imuจดบรรทัด chip id และตอบว่านี่คือหลักฐานแบบไหนที่การสแกนให้ไม่ได้
ส่วนที่ 2 จับสัญญาณ (บัสเซนเซอร์ทำงานที่ 1.8 V และขาไม่ได้ออกมาที่ header ส่วนนี้จึงใช้บัส I2C 3.3 V ของ header แทน)
- flash ตัวอย่าง QWA309 — Header I/O Test จาก Developer Hub ต่อ logic analyzer ช่องหนึ่งที่ SCL อีกช่องที่ SDA ของ I2C บน Arduino header และต่อกราวด์ ตั้ง decoder เป็น I2C
- จับขณะกดปุ่ม Scan บนจอ ตัวอย่างสแกน 0x08 ถึง 0x77 บนบัสเดียวกับจอสัมผัส หาใน decoder หนึ่ง address ที่ได้ NACK และหนึ่ง address ที่ได้ ACK แล้วถอดรหัสด้วยตาทีละบิต: START, 7 บิตของ address, บิต R/W, บิตที่เก้า, STOP
- ถ้ามี DFR0522 RGB matrix ให้ต่อแล้วสแกนซ้ำ ควรเห็น 0x10 ตอบ ACK (ตัวอย่าง DFR0522 RGB Dot Matrix ใช้ address นี้)
- วัดความถี่ของ SCL จากรูปคลื่น
หลักฐานที่เก็บไว้ใน portfolio: log ของการสแกนและ chip id จาก serial console ภาพรูปคลื่นของธุรกรรม ACK หนึ่งภาพและ NACK หนึ่งภาพที่ขีดป้ายด้วยมือ และความถี่ SCL ที่วัดได้ (ให้บอกว่าวัดจากส่วนไหนของรูปคลื่น เพราะ I2C ความเร็วต่างกันได้ตามการตั้งค่าของแต่ละบัส)
- อ่านบท J1 — The sensor bus and its lock ของเอกสาร SDK
หัวข้อที่อธิบายว่าทำไม
sensor_i2c_init()เรียกการกู้บัส (ปลดอุปกรณ์ที่ค้าง SDA ไว้) ก่อนตั้งขา - อ่าน Understanding the I2C Bus (Texas Instruments SLVA704) เรื่อง pull-up และ clock stretching แล้วโยงกับ Appendix X #13 ของเอกสาร SDK ที่เล่าว่าอุปกรณ์ที่ยืด clock ค้างบนบัสของจอทำให้ task อื่นของ CM55 อดอาหาร
บทถัดไป: บทเรียน 5.3 SPI
สะท้อนคิด
หัวข้อที่มีชื่อว่า “สะท้อนคิด”- ถ้าคุณจะเพิ่มเซนเซอร์ตัวใหม่บนบัส SCB0 คุณต้องตรวจอะไรบ้างก่อน เพื่อไม่ให้เซนเซอร์สี่ตัวที่มีอยู่แล้วพัง
- เคยเจอโค้ดที่ “ส่วนใหญ่ทำงาน แต่บางทีค่าเพี้ยน” ไหม ลองคิดว่าเป็นอาการของการแย่งบัสได้หรือไม่
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- SDK: cm33/sensors/01_i2c_bus_scan.c
- SDK: cm33/sensors/06_raw_register_access.c
- SDK: cm33/sensors/02_read_imu.c (พิสูจน์สายก่อนเชื่อ driver)
- J1 — The sensor bus and its lock (เอกสาร SDK สร้างจาก commit ef72c1b)
- Texas Instruments: Understanding the I2C Bus (SLVA704)
ตัวอย่างบน TESAIoT Developer Hub
หัวข้อที่มีชื่อว่า “ตัวอย่างบน TESAIoT Developer Hub”ลองของจริงบน TESAIoT Dev Kit: เปิดตัวอย่างบน Developer Hub เพื่ออ่านโค้ด ดาวน์โหลด หรือ flash เฟิร์มแวร์สำเร็จรูป
- QWA309 — DFR0522 RGB Dot Matrix — ควบคุม DFRobot DFR0522 RGB matrix 8x16 (I2C 0x10) บน bus 3.3V ร่วมกับ display แสดง clear/fill/pixel/pattern ผ่าน LVGL UI
- EP01 — DPS368 Monitor — อ่านค่าความดันบรรยากาศและอุณหภูมิจากเซนเซอร์ Infineon DPS368 ผ่าน I2C แล้วแสดงผลบนจอ LVGL
- QWA309 — Header I/O Test — diagnostic: สแกนบัส I2C 3.3 V ของ header และทดสอบ UART, SPI, GPIO, PWM พร้อม console UI
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
สแกนบัสเซนเซอร์ของบอร์ดแล้วพบ 0x44, 0x68, 0x77 แต่ไม่พบ 0x15 ข้อใดสรุปได้ถูกต้อง (เป้าหมายข้อ 1)
- แมกนีโตมิเตอร์ BMM350 เสีย
- พบ SHT40, BMI270 และ DPS368 ส่วน BMM350 (0x15) อยู่บน I3C ซึ่งเป็นอุปกรณ์คนละตัว การสแกนนี้จึงไม่มีวันเห็นมัน
- ต้องสแกนซ้ำด้วยความเร็วต่ำกว่านี้
- บัส SDA ค้าง
ดูเฉลย
คำตอบ: B. พบ SHT40, BMI270 และ DPS368 ส่วน BMM350 (0x15) อยู่บน I3C ซึ่งเป็นอุปกรณ์คนละตัว การสแกนนี้จึงไม่มีวันเห็นมัน
ตัวอย่าง 01_i2c_bus_scan ของ SDK เขียนไว้ว่า BMM350 lives on I3C P3[0]/P3[1] and never appears here การสแกนตอบได้เฉพาะอุปกรณ์บนบัสที่สแกน
-
พบอุปกรณ์ตอบ ACK ที่ 0x68 ขั้นต่อไปที่ดีที่สุดก่อนเรียก bmi270_init() คืออะไร (เป้าหมายข้อ 1)
- เรียก init เลย ถ้าผ่านแปลว่าถูกชิป
- อ่านรีจิสเตอร์ chip id (0x00) ถ้าได้ 0x24 คือ BMI270 จริง ค่าอื่นคือชิปคนละตัวที่ address เดียวกัน
- เขียนค่าทดสอบลงทุกรีจิสเตอร์
- สแกนซ้ำสิบครั้ง
ดูเฉลย
คำตอบ: B. อ่านรีจิสเตอร์ chip id (0x00) ถ้าได้ 0x24 คือ BMI270 จริง ค่าอื่นคือชิปคนละตัวที่ address เดียวกัน
02_read_imu ของ SDK สอนว่า PROVE THE WIRE FIRST การอ่านหนึ่งไบต์เปลี่ยนคำว่า init failed ให้กลายเป็นการวินิจฉัย และการเขียนมั่วอาจเปลี่ยนสถานะของชิปที่ไดรเวอร์พึ่งอยู่
-
ต้องอ่าน accelerometer, gyro และอุณหภูมิของ BMI270 เป็นชุดเดียวกัน วิธีถือ lock ใดถูกต้อง (เป้าหมายข้อ 2)
- lock และ unlock รอบการอ่านแต่ละค่า สามครั้ง
- lock ครั้งเดียว อ่านทั้งสาม unlock แล้วจึงหน่วงเวลาก่อนรอบถัดไป
- lock ครั้งเดียวตอนบูต แล้วไม่ต้อง unlock
- ไม่ต้อง lock เพราะไดรเวอร์ถือให้แล้ว
ดูเฉลย
คำตอบ: B. lock ครั้งเดียว อ่านทั้งสาม unlock แล้วจึงหน่วงเวลาก่อนรอบถัดไป
หนึ่ง lock ต่อหนึ่งชุดข้อมูล ทำให้สามค่ามาจากจังหวะเดียวกัน และห้ามถือ lock ข้ามการหน่วงเวลา ส่วนฟังก์ชันของ SDK ไม่ถือ lock ให้ ผู้เรียกต้องครอบเอง
-
เรียงขั้นของการสแกนบัสอย่างปลอดภัยตามตัวอย่าง 01_i2c_bus_scan ของ SDK (เป้าหมายข้อ 2)
- sensor_i2c_unlock()
- หยุด task เบื้องหลังที่อ่านเซนเซอร์ (ถ้ากำลังทำงาน)
- sensor_i2c_scan(addrs, max)
- sensor_i2c_lock(timeout) และคืน BUSY ถ้าหมดเวลา
- เปิด task เบื้องหลังกลับเหมือนเดิม
ดูเฉลย
ลำดับที่ถูก: B. หยุด task เบื้องหลังที่อ่านเซนเซอร์ (ถ้ากำลังทำงาน) → D. sensor_i2c_lock(timeout) และคืน BUSY ถ้าหมดเวลา → C. sensor_i2c_scan(addrs, max) → A. sensor_i2c_unlock() → E. เปิด task เบื้องหลังกลับเหมือนเดิม
เอาอีกเจ้าของออกจากสายก่อน ถือ lock สแกน คืน lock ทุกทาง แล้วคืนสภาพระบบเหมือนที่พบ การสแกนถือบัสราว 30 ถึง 60 ms ตัวอย่างจึงหยุด task เบื้องหลังแทนการแย่ง lock กับมัน
-
decoder แสดงลำดับ S, D1, ACK, 24, NACK, P ข้อใดอธิบายถูก (เป้าหมายข้อ 3)
- master เขียนค่า 0x24 ไปที่อุปกรณ์ 0xD1
- master อ่านหนึ่งไบต์จากอุปกรณ์ 0x68 ได้ 0x24 แล้วตอบ NACK เพราะเป็นไบต์สุดท้าย ก่อน STOP
- อุปกรณ์ไม่มีอยู่บนบัส
- เกิด error เพราะมี NACK
ดูเฉลย
คำตอบ: B. master อ่านหนึ่งไบต์จากอุปกรณ์ 0x68 ได้ 0x24 แล้วตอบ NACK เพราะเป็นไบต์สุดท้าย ก่อน STOP
0xD1 คือ address 0x68 เลื่อนซ้ายแล้วบิต R/W = 1 (อ่าน) อุปกรณ์ตอบ ACK แล้วส่ง 0x24 master ตอบ NACK เพื่อบอกว่าพอแล้ว NACK ตรงนี้คือสิ่งที่ถูกต้อง ไม่ใช่ความผิดพลาด
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"I2C" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "I2C" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/embedded-c-foundations/m05-serial-buses/l02-i2c/
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA