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

Motion radar: วาดทิศการเคลื่อนไหวแบบ polar

  1. แปลงค่า accelerometer และ gyroscope เป็นมุมและขนาด แล้ววาดเป็นกราฟ polar
  2. ใช้ baseline และ dead-band ตัดการสั่นเล็ก ๆ ก่อนวาด และคำนวณว่าถ้าใช้ moving average แทน จะเพิ่มความหน่วงเท่าไรที่คาบเวลาอ่าน 50 ms
  3. ออกแบบการแสดงผลที่ผู้ใช้อ่านทิศทางได้ในหนึ่งวินาที

baseline คือ “ท่านิ่ง” ที่จับตอน boot ครั้งเดียว ไม่ใช่ high-pass filter แบบต่อเนื่อง

หัวข้อที่มีชื่อว่า “baseline คือ “ท่านิ่ง” ที่จับตอน boot ครั้งเดียว ไม่ใช่ high-pass filter แบบต่อเนื่อง”

radar_update_baseline() เฉลี่ย accel X/Y และ gyro Z ของ 30 ตัวอย่างแรก (ที่คาบอ่าน BMI270_SAMPLE_PERIOD_MS = 50 ก็คือประมาณ 1.5 วินาทีแรกหลัง boot) เก็บไว้เป็น s_baseline_acc_x/y, s_baseline_gyr_z แล้วไม่คำนวณใหม่ อีกเลยตลอด session นี่คือวิธีลบ gravity/offset คงที่แบบง่ายที่สุด (baseline subtraction) ไม่ใช่ moving-average หรือ low-pass filter ที่คำนวณต่อเนื่อง — ข้อดีคือไม่เพิ่มความหน่วง (latency) เลย ข้อเสียคือถ้าท่าทางตอนบูตไม่ใช่ท่า ที่จะใช้งานจริง baseline จะผิดไปตลอด

มุมและขนาดบน radar คำนวณจากส่วนต่างจาก baseline ไม่ใช่ค่าดิบสามแกน

หัวข้อที่มีชื่อว่า “มุมและขนาดบน radar คำนวณจากส่วนต่างจาก baseline ไม่ใช่ค่าดิบสามแกน”

จุดที่ต่างจากสูตร Cartesian→Polar ทั่วไปคือ code คำนวณ acc_dx = sample.acc_g_x - s_baseline_acc_x และ acc_dy แบบเดียวกัน แล้ว acc_xy_delta_g = sqrt(acc_dx² + acc_dy²) — ใช้แค่แกน X/Y ของส่วนต่างเท่านั้น ไม่รวมแกน Z และ ไม่ใช่ magnitude ของเวกเตอร์ดิบ (ที่จะรวม gravity ~1g ติดมาตลอดเวลาแม้บอร์ดไม่ขยับ) มุมทิศคือ atan2f(acc_dy, acc_dx) ของเวกเตอร์ส่วนต่างนี้ — พูดอีกแบบคือ radar ชี้ไปทาง “ทิศที่ความเร่งเพิ่งเปลี่ยนไปจากตอน boot” ไม่ใช่ “ทิศที่ความเร่งรวมชี้อยู่ ณ ขณะนี้”

ก่อนจะคำนวณมุมด้วยซ้ำ โค้ดเช็คก่อนว่า acc_xy_delta_g >= 0.06g หรือ gyr_z_delta_abs_dps >= 12°/s ถ้าไม่ถึง ทั้งคู่จะถือว่า motion_active = false แล้วบังคับมุมเป็น 0 และไม่วาดเข็มเลย เหตุผลคือ atan2() ของเวกเตอร์ที่ เกือบเป็นศูนย์ (สัญญาณรบกวนระดับ mg) จะให้มุมสุ่มไปทุกทิศ ถ้าไม่มี dead-band เข็มจะส่ายไปมาทั้งที่บอร์ดนิ่งสนิท เทคนิคนี้ต่างจาก low-pass filter ตรงที่ ไม่เพิ่มความหน่วง เลย (ไม่มีการเฉลี่ยข้ามเวลา) แค่ตัดสินใจแสดง/ไม่แสดง ต่อตัวอย่างเดียว ๆ

สามระดับความแรง คำนวณจาก score ที่ normalize แล้วเทียบสองแกน

หัวข้อที่มีชื่อว่า “สามระดับความแรง คำนวณจาก score ที่ normalize แล้วเทียบสองแกน”

radar_calc_motion_level() แปลง acc_xy_delta_g และ gyr_z_delta_abs_dps เป็น score โดยหารด้วยค่าคงที่คนละตัว (0.45g และ 140°/s) แล้วเลือก score ที่มากกว่าระหว่างสองค่านั้นมาตัดสินระดับ — < 0.35 = LOW (เขียว 0x22C55E), 0.35–0.80 = MEDIUM (เหลืองอำพัน 0xF59E0B), ≥ 0.80 = HIGH (แดง 0xEF4444) ระบบนี้ไม่ได้อยู่ใน README ต้นทางเลย แต่เป็นสิ่งที่ทำให้ผู้ใช้อ่านทั้ง “แรงแค่ไหน” (จากสี) และ “ไปทางไหน” (จากมุมเข็ม) ได้พร้อมกันใน แวบเดียว

หน้าจอจริงใช้ widget lv_scale + เข็มเดียว ไม่ใช่ canvas วาด ring และ trace history

หัวข้อที่มีชื่อว่า “หน้าจอจริงใช้ widget lv_scale + เข็มเดียว ไม่ใช่ canvas วาด ring และ trace history”

README ต้นทางอธิบายว่าใช้ lv_canvas วาดวงแหวนซ้อน 4 วง เส้นทิศหลัก N/E/S/W และเก็บ trace 64 จุดล่าสุดมาต่อเป็น เส้นแบบจางลงตามอายุ — แต่โค้ดจริงใช้ lv_scale ซึ่งเป็น widget สำเร็จรูปของ LVGL 9 (หน้าปัดวงกลม 0–360° พร้อม tick 41 อัน) แล้ววาดเข็มเดียวด้วย lv_scale_set_line_needle_value(scale, needle, needle_len, angle_i) โดยความยาวเข็มสื่อขนาดของการเคลื่อนไหว มุมสื่อทิศทาง และเข็มถูกซ่อน (LV_OBJ_FLAG_HIDDEN) เมื่อ motion_active เป็น false ไม่มี trace history หรือ ring grid หลายวงเลย ส่วน gyro แสดงผ่าน dual-ring arc gauge อีกตัว (intensity_gyr_arc) ที่ map ค่า delta เป็น 0–100 ไม่ใช่ arc มุมตามแกน Z ตามที่ README ต้นทางอธิบาย

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

app_ui/radar/radar_presenter.c — มุมและ dead-band จาก baseline delta:

acc_dx = sample.acc_g_x - s_baseline_acc_x;
acc_dy = sample.acc_g_y - s_baseline_acc_y;
acc_xy_delta_g = sqrtf((acc_dx * acc_dx) + (acc_dy * acc_dy));
gyr_z_delta_abs_dps = fabsf(sample.gyr_dps_z - s_baseline_gyr_z);
/* Treat signal as STILL until delta crosses threshold from baseline. */
motion_active = s_baseline_ready &&
((acc_xy_delta_g >= RADAR_STILL_ACC_DELTA_G) ||
(gyr_z_delta_abs_dps >= RADAR_STILL_GYR_DELTA_DPS));
angle_deg = motion_active ? (atan2f(acc_dy, acc_dx) * RADAR_DEG_PER_RAD) : 0.0f;
level = motion_active ? radar_calc_motion_level(acc_xy_delta_g, gyr_z_delta_abs_dps) : RADAR_LEVEL_LOW;

สามระดับความแรงจาก score ที่ normalize แล้ว:

static radar_motion_level_t radar_calc_motion_level(float acc_xy_delta_g, float gyr_z_delta_abs_dps)
{
float acc_score = acc_xy_delta_g / 0.45f;
float gyr_score = gyr_z_delta_abs_dps / 140.0f;
float score = (acc_score > gyr_score) ? acc_score : gyr_score;
if (score < 0.35f) { return RADAR_LEVEL_LOW; }
if (score < 0.80f) { return RADAR_LEVEL_MEDIUM; }
return RADAR_LEVEL_HIGH;
}

app_ui/radar/radar_view.c — เข็มเดียวบน lv_scale, ซ่อนเมื่อนิ่ง:

s_view.radar_scale = lv_scale_create(radar_card);
lv_scale_set_mode(s_view.radar_scale, LV_SCALE_MODE_ROUND_INNER);
lv_scale_set_range(s_view.radar_scale, 0, 360);
lv_scale_set_total_tick_count(s_view.radar_scale, 41);
s_view.radar_needle = lv_line_create(s_view.radar_scale);
lv_scale_set_line_needle_value(s_view.radar_scale, s_view.radar_needle, 18, 0);
/* Hide needle in STILL state; presenter shows it only when motion is active. */
lv_obj_add_flag(s_view.radar_needle, LV_OBJ_FLAG_HIDDEN);
  • main_example.c ส่ง I2C handle เข้า radar_presenter_start() ตรงตามที่ README ต้นทางอธิบาย
  • app_sensor/bmi270/bmi270_config.h — BMI270_SAMPLE_PERIOD_MS = 50 (เร็วกว่าบทเรียน 3.2 ที่ 200 ms เพราะ radar ต้องการความไวสูงกว่า)
  • ดูโฟลเดอร์เต็มที่ int_ep05_bmi270_radar_view/
  • บอร์ดไม่ราบตอน boot ทำให้เข็มค้างชี้ทิศเดียวแม้วางนิ่ง — baseline จับจาก 30 ตัวอย่างแรกครั้งเดียว ถ้าตอน boot บอร์ดเอียงอยู่ ท่าราบทีหลังจะต่างจาก baseline เกินเกณฑ์ STILL ทำให้ดูเหมือนเคลื่อนไหวตลอด ต้องวางบอร์ดนิ่ง ในท่าที่จะใช้งานจริงตอนเปิดเครื่อง
  • คิดว่า magnitude มาจากทั้งสามแกน (x,y,z) — โค้ดจริงใช้แค่ส่วนต่างของ X/Y จาก baseline ไม่รวม Z และไม่ใช่ค่า ดิบ
  • ลดหรือเอา dead-band ออกเพื่อให้ “ไวขึ้น” — จะทำให้เข็มส่ายแบบสุ่มตอนบอร์ดนิ่งสนิท เพราะ atan2() ของ เวกเตอร์เกือบศูนย์ไม่มีความหมาย
  • คิดว่ามี canvas วาด ring 4 วงกับ trace 64 จุด — หน้าจอจริงใช้ lv_scale widget สำเร็จรูปกับเข็มเดียว ไม่มี ประวัติการเคลื่อนไหวเก็บไว้เลย
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 เฟิร์มแวร์สำเร็จรูป

หน้าจอของ EP05 — BMI270 Radar View บน TESAIoT Dev Kit

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

  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
  • atan2 ใช้ทำอะไรในการหาทิศ
  • dead-band ต่างจาก moving average อย่างไรในแง่ความหน่วงของจุดบน radar
  • สีหรือขนาดของจุดควรสื่อข้อมูลอะไร

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

คำถามทบทวน

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

  1. มุมของเข็มบน radar ในตัวอย่างนี้หมายถึงอะไร (เป้าหมายข้อ 1)

    1. ทิศของการเปลี่ยนแปลงความเร่งในระนาบ X/Y เทียบกับ baseline ตอนเริ่ม คำนวณด้วย atan2(dy, dx) และความยาวเข็มตามขนาดของการเปลี่ยนแปลง
    2. ทิศเหนือแม่เหล็ก
    3. มุมที่บอร์ดหมุนสะสมจาก gyroscope
    4. มุมเอียงสัมบูรณ์เทียบกับแรงโน้มถ่วง
    ดูเฉลย

    คำตอบ: A. ทิศของการเปลี่ยนแปลงความเร่งในระนาบ X/Y เทียบกับ baseline ตอนเริ่ม คำนวณด้วย atan2(dy, dx) และความยาวเข็มตามขนาดของการเปลี่ยนแปลง

    radar_poll_sensor_cb() หา acc_dx = acc_g_x − baseline_x และ acc_dy แบบเดียวกัน แล้วใช้ atan2f(acc_dy, acc_dx) เป็นมุม ส่วน needle_len ใน view คำนวณจาก acc_xy_delta_g จึงเป็นเวกเตอร์ของการเปลี่ยนแปลง ไม่ใช่เข็มทิศและไม่ใช่มุมที่สะสมจาก gyro

  2. เปิดเครื่องขณะถือบอร์ดเอียงไว้ แล้วค่อยวางราบบนโต๊ะนิ่ง ๆ radar จะแสดงอะไร (เป้าหมายข้อ 1)

    1. STILL เพราะบอร์ดไม่ขยับ
    2. เข็มค้างชี้ทิศหนึ่งแม้บอร์ดนิ่ง เพราะ baseline จาก 30 ตัวอย่างแรก (ราว 1.5 วินาที) ถูกจับตอนเอียง ท่าราบจึงต่างจาก baseline เกินเกณฑ์ STILL
    3. radar ตั้ง baseline ใหม่เองเมื่อบอร์ดนิ่ง
    4. จอขึ้น error เพราะ baseline ไม่ถูกต้อง
    ดูเฉลย

    คำตอบ: B. เข็มค้างชี้ทิศหนึ่งแม้บอร์ดนิ่ง เพราะ baseline จาก 30 ตัวอย่างแรก (ราว 1.5 วินาที) ถูกจับตอนเอียง ท่าราบจึงต่างจาก baseline เกินเกณฑ์ STILL

    radar_update_baseline() เฉลี่ย RADAR_BASELINE_SAMPLES = 30 ตัวอย่างแรกที่ 50 ms ต่อตัวอย่าง แล้วไม่คำนวณใหม่อีก การเปลี่ยนท่าทางถาวรจึงดูเหมือนการเคลื่อนไหวตลอดเวลา ควรวางบอร์ดนิ่งในท่าใช้งานตอนเปิดเครื่อง หรือเพิ่มปุ่มตั้ง baseline ใหม่

  3. ทำไมโค้ดถือว่า STILL จนกว่าค่าต่างจาก baseline จะเกิน 0.06 g หรือ 12 °/s (เป้าหมายข้อ 2)

    1. เพื่อให้วาดจอได้เร็วขึ้น
    2. เพราะ BMI270 วัดค่าที่ต่ำกว่านั้นไม่ได้
    3. เพื่อประหยัดพลังงานของเซนเซอร์
    4. เป็น dead-band กันสัญญาณรบกวนเล็ก ๆ รอบ baseline: atan2 ของเวกเตอร์ที่เล็กมากให้มุมสุ่มไปทุกทิศ เข็มจะส่ายทั้งที่บอร์ดนิ่ง แลกกับการไม่เห็นการเคลื่อนไหวที่เล็กกว่าเกณฑ์
    ดูเฉลย

    คำตอบ: D. เป็น dead-band กันสัญญาณรบกวนเล็ก ๆ รอบ baseline: atan2 ของเวกเตอร์ที่เล็กมากให้มุมสุ่มไปทุกทิศ เข็มจะส่ายทั้งที่บอร์ดนิ่ง แลกกับการไม่เห็นการเคลื่อนไหวที่เล็กกว่าเกณฑ์

    motion_active เป็นจริงเมื่อ acc_xy_delta_g ≥ RADAR_STILL_ACC_DELTA_G หรือ gyr_z_delta ≥ RADAR_STILL_GYR_DELTA_DPS ถ้าไม่มีเกณฑ์นี้ noise ระดับ mg จะทำให้มุมกระโดด เพราะทิศของเวกเตอร์ที่เกือบเป็นศูนย์ไม่มีความหมาย ตัวอย่างนี้ใช้ dead-band แทนการกรองแบบ low-pass จึงไม่เพิ่มความหน่วง

  4. ถ้าเพิ่ม moving average 8 ตัวอย่างให้ acc_dx และ acc_dy ที่ poll ทุก 50 ms ภาพจะนิ่งขึ้น แต่ความหน่วงเฉลี่ยจะเพิ่มขึ้นราวเท่าไร (เป้าหมายข้อ 2)

    1. ไม่เพิ่ม เพราะการเฉลี่ยคำนวณเสร็จในรอบเดียว
    2. ราว 25 ms
    3. ราว 175 ms
    4. ราว 400 ms
    ดูเฉลย

    คำตอบ: C. ราว 175 ms

    ค่าเฉลี่ยเคลื่อนที่ N ตัวอย่างหน่วงราว (N − 1) / 2 ตัวอย่าง = 3.5 × 50 ms ≈ 175 ms และต้องรอเต็มหน้าต่าง 400 ms กว่าจะตามการเปลี่ยนแบบขั้นบันไดได้ครบ ยิ่งกรองแรง จุดยิ่งนิ่งแต่ตามการเคลื่อนไหวช้าลง ความหน่วงนี้มาจากการที่ผลลัพธ์รวมตัวอย่างในอดีต ไม่ได้มาจากเวลาคำนวณ

  5. ตัวอย่างหนึ่งได้ acc_xy_delta = 0.20 g และ gyr_z_delta = 30 °/s ระดับและสีที่แสดงคืออะไร (เป้าหมายข้อ 3)

    1. LOW สีเขียว
    2. MEDIUM สีเหลืองอำพัน
    3. HIGH สีแดง
    4. STILL สีเทา
    ดูเฉลย

    คำตอบ: B. MEDIUM สีเหลืองอำพัน

    radar_calc_motion_level() ใช้คะแนนที่มากกว่าระหว่าง 0.20 / 0.45 ≈ 0.44 กับ 30 / 140 ≈ 0.21 ได้ 0.44 ซึ่ง ≥ 0.35 แต่ < 0.80 จึงเป็น MEDIUM (0xF59E0B) การใช้สีบอกว่าแรงแค่ไหน และใช้มุมเข็มบอกว่าไปทางไหน ทำให้ผู้ดูอ่านได้ทั้งสองเรื่องในแวบเดียว

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

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

"Motion radar: วาดทิศการเคลื่อนไหวแบบ polar" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ข้อความอ้างอิงภาษาอังกฤษ: "Motion radar: movement direction in polar form" 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/l05-bmi270-radar-view/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/9a8e3ed1d813bfd67fabf6b7ac15c6ff9750b465/int_ep05_bmi270_radar_view · 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