Motion radar: วาดทิศการเคลื่อนไหวแบบ polar
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”- แปลงค่า accelerometer และ gyroscope เป็นมุมและขนาด แล้ววาดเป็นกราฟ polar
- ใช้ baseline และ dead-band ตัดการสั่นเล็ก ๆ ก่อนวาด และคำนวณว่าถ้าใช้ moving average แทน จะเพิ่มความหน่วงเท่าไรที่คาบเวลาอ่าน 50 ms
- ออกแบบการแสดงผลที่ผู้ใช้อ่านทิศทางได้ในหนึ่งวินาที
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” ไม่ใช่
“ทิศที่ความเร่งรวมชี้อยู่ ณ ขณะนี้”
dead-band สองเกณฑ์ก่อนยอมให้เข็มขยับเลย
หัวข้อที่มีชื่อว่า “dead-band สองเกณฑ์ก่อนยอมให้เข็มขยับเลย”ก่อนจะคำนวณมุมด้วยซ้ำ โค้ดเช็คก่อนว่า 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_scalewidget สำเร็จรูปกับเข็มเดียว ไม่มี ประวัติการเคลื่อนไหวเก็บไว้เลย
build และ flash
หัวข้อที่มีชื่อว่า “build และ flash”# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/make buildmake program # flash ผ่าน KitProg3หรือเปิด ตัวอย่างนี้บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูป
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”
ก่อนอ่านโค้ด ให้ทายว่าหน้าจอนี้มี object อะไรบ้าง และอะไรเปลี่ยนเมื่อผู้ใช้แตะหรือเมื่อค่าเซนเซอร์เปลี่ยน
- ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
- แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
- ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”- atan2 ใช้ทำอะไรในการหาทิศ
- dead-band ต่างจาก moving average อย่างไรในแง่ความหน่วงของจุดบน radar
- สีหรือขนาดของจุดควรสื่อข้อมูลอะไร
คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- README ของ episode · โฟลเดอร์โค้ด · commit
9a8e3ed - เปิดตัวอย่างนี้บน Developer Hub
- โค้ดเป็นของ Developer Hub และอ้างอิงด้วยลิงก์ ไม่ได้คัดลอกเข้าคลังนี้
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
มุมของเข็มบน radar ในตัวอย่างนี้หมายถึงอะไร (เป้าหมายข้อ 1)
- ทิศของการเปลี่ยนแปลงความเร่งในระนาบ X/Y เทียบกับ baseline ตอนเริ่ม คำนวณด้วย atan2(dy, dx) และความยาวเข็มตามขนาดของการเปลี่ยนแปลง
- ทิศเหนือแม่เหล็ก
- มุมที่บอร์ดหมุนสะสมจาก gyroscope
- มุมเอียงสัมบูรณ์เทียบกับแรงโน้มถ่วง
ดูเฉลย
คำตอบ: 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
-
เปิดเครื่องขณะถือบอร์ดเอียงไว้ แล้วค่อยวางราบบนโต๊ะนิ่ง ๆ radar จะแสดงอะไร (เป้าหมายข้อ 1)
- STILL เพราะบอร์ดไม่ขยับ
- เข็มค้างชี้ทิศหนึ่งแม้บอร์ดนิ่ง เพราะ baseline จาก 30 ตัวอย่างแรก (ราว 1.5 วินาที) ถูกจับตอนเอียง ท่าราบจึงต่างจาก baseline เกินเกณฑ์ STILL
- radar ตั้ง baseline ใหม่เองเมื่อบอร์ดนิ่ง
- จอขึ้น error เพราะ baseline ไม่ถูกต้อง
ดูเฉลย
คำตอบ: B. เข็มค้างชี้ทิศหนึ่งแม้บอร์ดนิ่ง เพราะ baseline จาก 30 ตัวอย่างแรก (ราว 1.5 วินาที) ถูกจับตอนเอียง ท่าราบจึงต่างจาก baseline เกินเกณฑ์ STILL
radar_update_baseline() เฉลี่ย RADAR_BASELINE_SAMPLES = 30 ตัวอย่างแรกที่ 50 ms ต่อตัวอย่าง แล้วไม่คำนวณใหม่อีก การเปลี่ยนท่าทางถาวรจึงดูเหมือนการเคลื่อนไหวตลอดเวลา ควรวางบอร์ดนิ่งในท่าใช้งานตอนเปิดเครื่อง หรือเพิ่มปุ่มตั้ง baseline ใหม่
-
ทำไมโค้ดถือว่า STILL จนกว่าค่าต่างจาก baseline จะเกิน 0.06 g หรือ 12 °/s (เป้าหมายข้อ 2)
- เพื่อให้วาดจอได้เร็วขึ้น
- เพราะ BMI270 วัดค่าที่ต่ำกว่านั้นไม่ได้
- เพื่อประหยัดพลังงานของเซนเซอร์
- เป็น 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 จึงไม่เพิ่มความหน่วง
-
ถ้าเพิ่ม moving average 8 ตัวอย่างให้ acc_dx และ acc_dy ที่ poll ทุก 50 ms ภาพจะนิ่งขึ้น แต่ความหน่วงเฉลี่ยจะเพิ่มขึ้นราวเท่าไร (เป้าหมายข้อ 2)
- ไม่เพิ่ม เพราะการเฉลี่ยคำนวณเสร็จในรอบเดียว
- ราว 25 ms
- ราว 175 ms
- ราว 400 ms
ดูเฉลย
คำตอบ: C. ราว 175 ms
ค่าเฉลี่ยเคลื่อนที่ N ตัวอย่างหน่วงราว (N − 1) / 2 ตัวอย่าง = 3.5 × 50 ms ≈ 175 ms และต้องรอเต็มหน้าต่าง 400 ms กว่าจะตามการเปลี่ยนแบบขั้นบันไดได้ครบ ยิ่งกรองแรง จุดยิ่งนิ่งแต่ตามการเคลื่อนไหวช้าลง ความหน่วงนี้มาจากการที่ผลลัพธ์รวมตัวอย่างในอดีต ไม่ได้มาจากเวลาคำนวณ
-
ตัวอย่างหนึ่งได้ acc_xy_delta = 0.20 g และ gyr_z_delta = 30 °/s ระดับและสีที่แสดงคืออะไร (เป้าหมายข้อ 3)
- LOW สีเขียว
- MEDIUM สีเหลืองอำพัน
- HIGH สีแดง
- 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://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 Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA