ข้ามไปยังเนื้อหา

อ่านความดันและอุณหภูมิจาก DPS368 ผ่าน I2C

  1. อ่านค่าความดันบรรยากาศและอุณหภูมิจาก DPS368 ผ่าน I2C แล้วแสดงบนจอ
  2. อธิบายการแบ่งชั้น driver → reader → presenter → view ของ episode
  3. ตรวจค่าที่อ่านได้กับค่าความดันอ้างอิงของพื้นที่ และอธิบายส่วนต่าง

DPS368 เป็นเซนเซอร์ความดันบรรยากาศ (barometer) แบบ capacitive MEMS ของ Infineon วัดความดันได้ในช่วง 300–1200 hPa ด้วยความละเอียดระดับ ±0.002 hPa (ประมาณการเปลี่ยนระดับความสูง 2 ซม.) และวัดอุณหภูมิร่วมด้วยในตัวเดียวกัน เพราะค่า ความดันต้องชดเชยด้วยอุณหภูมิภายในเซนเซอร์เอง (การคำนวณนี้ทำอยู่ในไลบรารีของ Infineon ให้แล้ว ไม่ต้องเขียนสูตรเอง)

โค้ดแบ่งงานเป็นสี่ชั้นตามชื่อโฟลเดอร์ app_sensor/dps368/ มี dps368_driver (คุยกับเซนเซอร์ผ่านไลบรารี xensiv_dps3xx ของ Infineon โดยตรง) และ dps368_reader (ห่อ driver ให้เป็น dps368_reader_poll() ฟังก์ชันเดียว คืน bool บอกว่ามี sample ใหม่หรือไม่) ส่วน app_ui/dps368/ มี dps368_presenter (ผูก timer + ตัดสินใจว่าจะ อัปเดตจอเมื่อไร) และ dps368_view (สร้างและอัปเดต label ล้วน ๆ ไม่มี gauge หรือ arc ตามที่บางครั้งอาจถูกเข้าใจผิด) แต่ละชั้นรู้จักแค่ชั้นที่อยู่ติดกัน — dps368_view ไม่รู้จัก I2C เลย และ dps368_driver ไม่รู้จัก LVGL เลย

สัญญาณเข้าเซนเซอร์ผ่าน bus ที่ master เตรียมไว้แล้ว พร้อม fallback ที่อยู่ I2C

หัวข้อที่มีชื่อว่า “สัญญาณเข้าเซนเซอร์ผ่าน bus ที่ master เตรียมไว้แล้ว พร้อม fallback ที่อยู่ I2C”

main_example.c ส่ง &sensor_i2c_controller_hal_obj (HAL handle ที่ master template เปิดไว้แล้วตั้งแต่ก่อน example_main() ถูกเรียก — ดูบทเรียน 1.1) เข้าไปให้ dps368_presenter_start() โดยตรง ไม่ต้อง cyhal_i2c_init() ซ้ำ ภายใน app_dps368_service_init() ลองต่อที่ address เริ่มต้น (XENSIV_DPS3XX_I2C_ADDR_DEFAULT = 0x77) ก่อน ถ้าไม่ตอบจะลองที่อยู่สำรอง (XENSIV_DPS3XX_I2C_ADDR_ALT = 0x76 ซึ่งกำหนดโดยขา SDO ของเซนเซอร์) — โค้ดจึงใช้งานได้ กับบอร์ดที่ strap ขา SDO ไว้คนละแบบโดยไม่ต้องแก้โค้ด

dps368_presenter_start() เรียก lv_timer_create(dps368_poll_sensor_cb, DPS368_SAMPLE_PERIOD_MS, NULL) โดย DPS368_SAMPLE_PERIOD_MS = 1000 (วินาทีละครั้ง) — dps368_poll_sensor_cb() รันอยู่บน LVGL thread เดียวกัน กับส่วนที่วาดจอ ไม่ใช่ FreeRTOS task แยกต่างหากที่ส่งข้อมูลผ่านคิวแล้วกลับเข้า LVGL ด้วย lv_async_call() ตามที่บางครั้งอาจเข้าใจผิด การอ่าน I2C (dps368_driver_read_hpa_c()) จึงเป็นการเรียกแบบ blocking ตรง ๆ ภายใน callback ของ timer เพราะ transaction I2C หนึ่งครั้งเร็วพอที่จะไม่ทำให้จอกระตุก ที่ 1 ครั้ง/วินาที episode นี้จึง เลือกความเรียบง่ายเหนือการแยก thread เพราะเซนเซอร์ตัวเดียวไม่คุ้มความซับซ้อนของ task+queue

dps368_reader_poll() เช็ค return code ของ driver พิเศษกรณีเดียวคือ XENSIV_DPS3XX_RSLT_ERR_DATA_NOT_READY — ถ้า เจอค่านี้จะคืน false แต่ ไม่ถือเป็น error (แค่ยังไม่ถึงรอบ conversion ถัดไปของเซนเซอร์) ส่วน error code อื่น ถือเป็นปัญหาจริงที่ presenter จะเอาไปแสดงบน status label เป็นข้อความ "Sensor: read error (0x........)" การแยก สองกรณีนี้ออกจากกันสำคัญ เพราะถ้าถือว่า “ยังไม่พร้อม” เป็น error ทุกครั้ง จอจะกระพริบข้อความ error ทุกวินาทีที่ยัง ไม่ถึงรอบอ่านจริง

โค้ดของ episode นี้อยู่ใน Developer Hub (อ้างอิงที่ commit 9a8e3ed) — อ่าน Why ของ README ต้นทาง เพื่อเข้าใจจุดประสงค์ แต่ โค้ดตัวอย่างด้านล่างคัดลอกจากไฟล์จริง (Apache-2.0, tesaiot/developer-hub, commit เดียวกัน) เพราะกลไก poll จริงต่างจากที่ README ต้นทางอธิบาย (ไม่มี FreeRTOS task/queue แยก)

app_ui/dps368/dps368_presenter.c — lv_timer เดียวที่ poll แล้วอัปเดตจอ:

static void dps368_poll_sensor_cb(lv_timer_t *timer)
{
(void)timer;
dps368_sample_t sample;
bool has_new_sample = dps368_reader_poll(&sample);
if (has_new_sample)
{
dps368_view_update_sample(&sample);
/* ... */
return;
}
cy_rslt_t rslt = dps368_reader_get_last_error();
if (CY_RSLT_SUCCESS == rslt)
{
return; /* not-ready is not an error */
}
/* ... real error -> update status label ... */
}

app_sensor/dps368/dps368_reader.c — แยก “ยังไม่พร้อม” ออกจาก error จริง:

if (CY_RSLT_SUCCESS == rslt)
{
s_last_sample.pressure_hpa = pressure_hpa;
s_last_sample.temperature_c = temperature_c;
s_last_sample.sample_count++;
if (NULL != out_sample) { *out_sample = s_last_sample; }
return true;
}
/* At low sample rate, polling can happen before conversion is ready. */
if (rslt == XENSIV_DPS3XX_RSLT_ERR_DATA_NOT_READY)
{
s_last_error = CY_RSLT_SUCCESS;
return false;
}
s_last_error = rslt;
return false;

app_sensor/app_dps368_service.c — ลองที่อยู่ I2C หลักก่อน แล้วค่อย fallback ไปที่อยู่สำรอง:

rslt = mtb_xensiv_dps3xx_init_i2c(&s_dps368, i2c_bus, XENSIV_DPS3XX_I2C_ADDR_DEFAULT);
if (CY_RSLT_SUCCESS != rslt)
{
rslt = mtb_xensiv_dps3xx_init_i2c(&s_dps368, i2c_bus, XENSIV_DPS3XX_I2C_ADDR_ALT);
/* ... */
}
  • main_example.c ส่ง handle I2C ที่ master เตรียมไว้เข้า dps368_presenter_start() ตรงตามที่ README ต้นทางอธิบาย
  • ไฟล์ basic_label_legacy.* และ dps368_monitor_legacy.* เป็นโค้ดเวอร์ชันก่อน refactor ไว้เทียบ ไม่ใช่ส่วนที่ทำงานจริงของ episode
  • ดูโฟลเดอร์เต็มที่ int_ep01_dps368_monitor/
  • คิดว่ามี FreeRTOS task แยกต่างหาก poll เซนเซอร์ทุก 100 ms — โค้ดจริงใช้ lv_timer เดียวบน LVGL thread ที่ 1000 ms และอ่าน I2C แบบ blocking ตรง ๆ ในนั้นเลย ไม่มี queue หรือ lv_async_call() แนวคิดนี้ใช้ได้เพราะมีเซนเซอร์เดียวและ I2C transaction สั้น — ถ้าเพิ่มเซนเซอร์ที่อ่านช้าลงในอนาคต ค่อยพิจารณาแยก task จริง (ดู EP07 — SensorHub Final)
  • ถือว่า XENSIV_DPS3XX_RSLT_ERR_DATA_NOT_READY เป็น error — ต้องแยกจาก error code อื่นเสมอ เพราะเป็นแค่ สัญญาณว่ายังไม่ถึงรอบ conversion ถัดไป ไม่ใช่ความล้มเหลว ถ้าปฏิบัติเหมือน error ทุกครั้งจะได้ log/สถานะ error ที่ไม่ตรงความจริง
  • ลืมว่าเซนเซอร์มีสองที่อยู่ I2C ที่เป็นไปได้ — ถ้า hard-code แค่ 0x77 โค้ดจะใช้ไม่ได้กับบอร์ดที่ strap ขา SDO ต่างไป ต้องลอง fallback เสมอแบบที่ app_dps368_service_init() ทำ
Terminal window
# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)
# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/
# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/
make build
make program # flash ผ่าน KitProg3

หรือเปิด ตัวอย่างนี้บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูป

หน้าจอของ EP01 — DPS368 Monitor บน TESAIoT Dev Kit

ก่อนอ่านโค้ด ให้ทายว่าหน้าจอนี้มี object อะไรบ้าง และอะไรเปลี่ยนเมื่อผู้ใช้แตะหรือเมื่อค่าเซนเซอร์เปลี่ยน

  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
  • ชั้น presenter ทำอะไรที่ view ไม่ทำ
  • ความดันควรเปลี่ยนอย่างไรเมื่อยกบอร์ดขึ้นสูงหนึ่งชั้นตึก
  • ถ้าเซนเซอร์ตอบ I2C ไม่ได้ ข้อความผิดพลาดควรขึ้นที่ชั้นใด

คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. ถ้า DPS368 ไม่ตอบบน I2C ตอนเริ่มต้น จะเห็นอะไร (เป้าหมายข้อ 1)

    1. บอร์ดรีเซ็ตวนไม่หยุด
    2. ค่าความดันเป็น 0.00 hPa แต่สถานะขึ้น ready
    3. driver ลอง address หลักแล้วลอง address สำรอง ถ้าไม่ตอบทั้งคู่จะ log INIT_FAIL จอขึ้น “Sensor: init failed” และไม่สร้าง timer อ่านค่า
    4. presenter ลองใหม่ทุก 1 วินาทีจนกว่าเซนเซอร์จะตอบ
    ดูเฉลย

    คำตอบ: C. driver ลอง address หลักแล้วลอง address สำรอง ถ้าไม่ตอบทั้งคู่จะ log INIT_FAIL จอขึ้น “Sensor: init failed” และไม่สร้าง timer อ่านค่า

    dps368_driver_init() เรียก mtb_xensiv_dps3xx_init_i2c() ด้วย DPS368_I2C_ADDR_PRIMARY ก่อนแล้วจึง ALTERNATE เมื่อยังล้ม dps368_presenter_start() เรียก dps368_view_set_init_failed() แล้ว return ก่อน lv_timer_create() จึงไม่มีการอ่านซ้ำ ต้องแก้สายหรือบอร์ดแล้วรีเซ็ต ส่วน I2C bus เองถูก master เตรียมไว้ให้แล้ว episode แค่รับ handle sensor_i2c_controller_hal_obj

  2. ระหว่างทำงาน การอ่าน I2C ล้มเหลว ข้อความ error บนจอเกิดจากการทำงานร่วมกันของแต่ละชั้นอย่างไร (เป้าหมายข้อ 2)

    1. driver คืนรหัส error, reader เก็บเป็น last error, presenter ตัดสินใจแสดงเมื่อรหัสเปลี่ยน แล้วเรียก view ให้เขียนข้อความ
    2. driver เรียก lv_label_set_text() เองทันทีที่อ่านล้ม
    3. view อ่าน I2C เองเพื่อตรวจว่าเซนเซอร์ยังอยู่
    4. master template ตรวจแล้วแสดงข้อความให้
    ดูเฉลย

    คำตอบ: A. driver คืนรหัส error, reader เก็บเป็น last error, presenter ตัดสินใจแสดงเมื่อรหัสเปลี่ยน แล้วเรียก view ให้เขียนข้อความ

    dps368_reader_poll() เก็บ s_last_error presenter อ่านด้วย dps368_reader_get_last_error() และเรียก dps368_view_set_read_error() เฉพาะเมื่อรหัสต่างจากครั้งก่อน เพื่อไม่ให้จอกระพริบและ log ท่วม view ไม่รู้จัก I2C และ driver ไม่รู้จัก LVGL แต่ละชั้นจึงเปลี่ยนแยกกันได้

  3. บางรอบ xensiv_dps3xx_read() คืน XENSIV_DPS3XX_RSLT_ERR_DATA_NOT_READY โค้ดจัดการอย่างไร และทำไม (เป้าหมายข้อ 2)

    1. ถือเป็น error ร้ายแรงและแสดงบนจอทันที
    2. init เซนเซอร์ใหม่ทุกครั้ง
    3. หน่วงเวลา 100 ms แล้วอ่านซ้ำในรอบเดียวกัน
    4. reader คืน false แต่ไม่ถือเป็น error presenter จึงข้ามรอบนั้นเงียบ ๆ และจอคงค่าเดิม เพราะที่อัตรา 1 ครั้งต่อวินาทีกับ oversampling 8 เท่า การ poll อาจมาถึงก่อนการแปลงค่าเสร็จ ซึ่งเป็นเรื่องปกติ
    ดูเฉลย

    คำตอบ: D. reader คืน false แต่ไม่ถือเป็น error presenter จึงข้ามรอบนั้นเงียบ ๆ และจอคงค่าเดิม เพราะที่อัตรา 1 ครั้งต่อวินาทีกับ oversampling 8 เท่า การ poll อาจมาถึงก่อนการแปลงค่าเสร็จ ซึ่งเป็นเรื่องปกติ

    comment ใน dps368_reader.c เขียนว่า At low sample rate, polling can happen before conversion is ready. จึงตั้ง s_last_error = CY_RSLT_SUCCESS เพื่อไม่ให้ presenter แสดง error ปลอม ตัวอย่างนี้ poll ผ่าน lv_timer ทุก DPS368_SAMPLE_PERIOD_MS (1000 ms) ในขณะที่เซนเซอร์วัดอยู่เบื้องหลัง (background mode)

  4. ยกบอร์ดขึ้นสูงหนึ่งชั้นตึก (ราว 3 เมตร) ค่าความดันที่อ่านได้ควรเปลี่ยนอย่างไร (เป้าหมายข้อ 3)

    1. เพิ่มขึ้นราว 0.35 hPa
    2. ลดลงราว 0.35 hPa
    3. ลดลงราว 3.5 hPa
    4. ไม่เปลี่ยน เพราะเซนเซอร์ละเอียดไม่พอ
    ดูเฉลย

    คำตอบ: B. ลดลงราว 0.35 hPa

    README ของ episode ให้ตัวเลขว่ายกขึ้น 1 เมตร ความดันลดราว 0.12 hPa (ใกล้ระดับน้ำทะเล) 3 เมตรจึงลดราว 0.36 hPa ความละเอียดของ DPS368 ราว ±0.002 hPa จึงเห็นได้ชัด แต่ควรดูค่าเฉลี่ยหลายวินาที เพราะลมและแอร์ในห้องทำให้ค่าแกว่งได้

  5. บอร์ดอ่านได้ 1005 hPa ขณะที่สถานีอุตุนิยมวิทยาใกล้เคียงรายงาน 1012 hPa ข้อใดอธิบายส่วนต่างได้ดีที่สุด (เป้าหมายข้อ 3)

    1. เซนเซอร์เสีย ต้องเปลี่ยนตัวใหม่
    2. ค่าจากบอร์ดเป็นหน่วย kPa
    3. ค่าที่สถานีรายงานมักปรับเป็นระดับน้ำทะเลแล้ว ส่วนบอร์ดวัดความดันจริงตรงความสูงที่วางอยู่ ส่วนต่างราว 7 hPa ตรงกับความสูงราว 60 เมตร จึงต้องเทียบในเกณฑ์เดียวกันก่อน
    4. ความดันในอาคารสูงกว่าภายนอกเสมอ
    ดูเฉลย

    คำตอบ: C. ค่าที่สถานีรายงานมักปรับเป็นระดับน้ำทะเลแล้ว ส่วนบอร์ดวัดความดันจริงตรงความสูงที่วางอยู่ ส่วนต่างราว 7 hPa ตรงกับความสูงราว 60 เมตร จึงต้องเทียบในเกณฑ์เดียวกันก่อน

    สูตร barometric ใน README คือ h = 44330 × (1 − (P/P0)^0.1903) แทนค่า 1005 กับ 1012 hPa ได้ราว 58 เมตร ให้ตรวจก่อนว่าค่าอ้างอิงเป็นความดันที่ปรับเป็นระดับน้ำทะเลหรือความดันที่สถานี และจุดที่วัดสูงเท่าไร ก่อนสรุปว่าเซนเซอร์คลาด

อ้างอิงบทเรียนนี้

ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน

"อ่านความดันและอุณหภูมิจาก DPS368 ผ่าน I2C" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ข้อความอ้างอิงภาษาอังกฤษ: "Pressure and temperature from the DPS368 over 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/tesaiot-firmware-stack/m03-interactive-sensors/l01-dps368-monitor/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/9a8e3ed1d813bfd67fabf6b7ac15c6ff9750b465/int_ep01_dps368_monitor · Code stays in the Developer Hub and is linked at pinned commits, never copied: the episodes, practice codes and main-branch examples are Apache-2.0; the master template and the OPTIGA client carry Infineon/Cypress EULAs.

วิธีอ้างอิง TESA ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA