เก็บโปรไฟล์ Wi-Fi ลงหน่วยความจำถาวร
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”- สร้างฟอร์มกรอก SSID และรหัสผ่าน โดยช่องรหัสผ่านอยู่ใน password mode
- บันทึก โหลด และล้างโปรไฟล์ผ่าน profile store ใน NVM ได้ และค่ายังอยู่หลังรีเซ็ตบอร์ด
- อธิบายความเสี่ยงของการเก็บรหัสผ่านใน flash และแนวทางลดความเสี่ยง
หน่วยเก็บที่ใช้จริงคือ RRAM ของ PSoC Edge ไม่ใช่ flash ทั่วไป
หัวข้อที่มีชื่อว่า “หน่วยเก็บที่ใช้จริงคือ RRAM ของ PSoC Edge ไม่ใช่ flash ทั่วไป”README ของ episode พูดกว้าง ๆ ว่า implementation “จะใช้ API ของ PSoC flash เช่น cyhal_flash_* หรือ cy_em_eeprom
หรือ MCUboot NVS แล้วแต่บอร์ด” แต่โค้ดจริงที่ commit 9a8e3ed เรียก Cy_RRAM_TSReadByteArray() และ
Cy_RRAM_NvmWriteByteArray() ตรง ๆ กับ RRAMC0 — RRAM (Resistive RAM) คือหน่วยความจำไม่ลบเลือนบนชิปของ PSoC
Edge E84 เอง ที่อยู่เขียนคือ CYMEM_CM55_0_user_nvm_C_START (พื้นที่ user NVM ของ CM55) ไม่ใช่ EEPROM emulation
หรือ MCUboot NVS แยกต่างหาก
บันทึกเป็น record ขนาดคงที่ 256 ไบต์ พร้อม magic + CRC32
หัวข้อที่มีชื่อว่า “บันทึกเป็น record ขนาดคงที่ 256 ไบต์ พร้อม magic + CRC32”wifi_profile_record_t ที่เขียนลง RRAM จริงมี magic (0x57465031 = “WFP1” ไม่ใช่ “WIFI” ตามที่ README ต้นทาง
บอก), version, payload_len, crc32, valid และข้อมูล ssid/password/security/auto_connect รวมกันพอดีใน
WIFI_PROFILE_SLOT_SIZE = 256 ไบต์ (ตรวจด้วย compile-time assertion wifi_profile_record_size_check) CRC32
คำนวณจาก payload เท่านั้น ไม่รวม header ของ record ใช้ตรวจว่าอ่านค่ากลับมาไม่เพี้ยน ส่วน struct สาธารณะ
wifi_profile_data_t ที่ UI เห็นมีแค่ ssid, password, security, auto_connect — ไม่มีฟิลด์ magic (magic
อยู่ใน record ภายในเท่านั้น ไม่ใช่ struct ที่ UI ส่งเข้าออก ตามที่ README ต้นทางเขียนไว้)
เขียนแล้วอ่านกลับมาตรวจ (verify-after-write) และข้ามการเขียนถ้าค่าไม่เปลี่ยน
หัวข้อที่มีชื่อว่า “เขียนแล้วอ่านกลับมาตรวจ (verify-after-write) และข้ามการเขียนถ้าค่าไม่เปลี่ยน”wifi_profile_store_save() เทียบ block ที่จะเขียนกับ block ปัจจุบันก่อน ถ้าเหมือนกันทุกไบต์จะข้ามการเขียนเลย
(SAVE_SKIP_SAME) เพื่อลดจำนวนรอบเขียนของหน่วยความจำ (wear) หลังเขียนจริงแล้วมันอ่านกลับมาเทียบกับ block ที่ตั้งใจ
เขียนอีกครั้ง (verify_block) ถ้าไม่ตรงจะถือว่าล้มเหลว (SAVE_VERIFY_FAIL) แม้ Cy_RRAM_NvmWriteByteArray() จะ
คืนค่า success ก็ตาม — เป็นการตรวจสองชั้นสำหรับข้อมูลที่สำคัญ
แยก slot “erased” ออกจาก slot “ไม่มีข้อมูล” ด้วยรูปแบบ 0xFF
หัวข้อที่มีชื่อว่า “แยก slot “erased” ออกจาก slot “ไม่มีข้อมูล” ด้วยรูปแบบ 0xFF”RRAM/flash ที่ยังไม่เคยเขียนจะมีค่าไบต์เป็น 0xFF ทั้งบล็อก wifi_profile_is_erased() เช็ค pattern นี้ก่อนพยายาม
parse เป็น record เสมอ ถ้าไม่เช็คแล้วอ่าน 0xFF ทั้งก้อนไปตีความเป็น struct ตรง ๆ อาจได้ magic ที่บังเอิญตรง
(แม้โอกาสน้อยก็ตาม) การ “clear” จึงหมายถึงเขียน 0xFF ทับทั้ง slot ไม่ใช่มี erase API แยกต่างหาก
auto-fill จาก Scan ไปหน้า Profile: ต้องกดปุ่ม “Use” ไม่ใช่แค่แตะแถว
หัวข้อที่มีชื่อว่า “auto-fill จาก Scan ไปหน้า Profile: ต้องกดปุ่ม “Use” ไม่ใช่แค่แตะแถว”README ต้นทางอธิบายว่าแค่แตะแถวใน scan list จะ auto-jump ไปหน้า Profile พร้อม pre-fill SSID ทันที แต่โค้ดจริงแบ่ง
เป็นสองขั้น: แตะแถวก่อนเพื่อ เลือก (s_ctx.selected_idx) แล้วต้องกดปุ่ม “Use this AP” แยกต่างหากเพื่อคัดลอก
AP ที่เลือกไปหน้า Profile จริง (ui_wifi_use_selected_ap_cb() เรียก callback ที่ถูกลงทะเบียนไว้ ซึ่งไปเรียก
ui_wifi_profile_page_apply_ap() ที่ copy SSID + security เข้า form) หน้า scan กับหน้า profile ไม่รู้จักกันโดยตรง
— เชื่อมกันผ่าน callback ที่ registered ไว้ตอนสร้าง shell เท่านั้น ไม่ใช่ผ่าน global state pending_ssid ใน
menu_nav_state_t ตามที่ README ต้นทางอธิบาย (state จริงของ menu_nav_state_t ใน episode นี้ไม่มีฟิลด์ SSID เลย)
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”โค้ดของ episode นี้อยู่ใน Developer Hub (อ้างอิงที่ commit 9a8e3ed) — อ่าน Why ของ README ต้นทาง เพื่อเข้าใจจุดประสงค์ แต่ โค้ดตัวอย่างด้านล่างคัดลอกจากไฟล์จริง (Apache-2.0, tesaiot/developer-hub, commit เดียวกัน) เพราะรายละเอียดของ storage และ cross-page flow ต่างจากที่ README ต้นทางอธิบายไว้
wifi_profile/wifi_profile_store.c — เขียนลง RRAM จริง พร้อม skip-if-same และ verify-after-write:
/* Skip write if content is unchanged to reduce NVM wear. */if(wifi_profile_read_slot(WIFI_PROFILE_PRIMARY_ADDR, current_block)) { if(0 == memcmp(current_block, block, sizeof(block))) { wifi_profile_log("SAVE_SKIP_SAME"); return true; }}
if(!wifi_profile_write_slot(WIFI_PROFILE_PRIMARY_ADDR, block)) { return false;}
if(!wifi_profile_read_slot(WIFI_PROFILE_PRIMARY_ADDR, verify_block)) { return false;}
if(0 != memcmp(verify_block, block, sizeof(block))) { wifi_profile_log("SAVE_VERIFY_FAIL"); return false;}wifi_profile_read_slot()/write_slot() เรียก RRAM API ของ PSoC Edge ตรง ๆ:
static bool wifi_profile_read_slot(uint32_t addr, uint8_t *out){ cy_en_rram_status_t st = Cy_RRAM_TSReadByteArray(RRAMC0, addr, out, WIFI_PROFILE_SLOT_SIZE); return (st == CY_RRAM_SUCCESS);}
static bool wifi_profile_write_slot(uint32_t addr, const uint8_t *in){ cy_en_rram_status_t st = Cy_RRAM_NvmWriteByteArray(RRAMC0, addr, (uint8_t *)in, WIFI_PROFILE_SLOT_SIZE); return (st == CY_RRAM_SUCCESS);}wifi_list/ui_wifi_list_page.c — ต้องเลือกแถวก่อน แล้วกดปุ่ม Use แยก:
static void ui_wifi_use_selected_ap_cb(lv_event_t *e){ uint16_t count; const wifi_scan_ap_t *aps = wifi_scan_service_get_list(&s_ctx.service, &count);
if((aps == NULL) || (count == 0U) || (s_ctx.selected_idx >= count)) { lv_label_set_text(s_ctx.hint_label, "Select an AP row before using it in profile page."); return; }
s_use_ap_cb(&aps[s_ctx.selected_idx], s_use_ap_user_data); lv_label_set_text(s_ctx.hint_label, "AP copied to profile page.");}main_example.cpre-init WiFi scan service แล้ว forward เข้าui_wifi_profile_nvm_create()ตรงตามที่ README ต้นทางอธิบายwifi_profile/wifi_profile_types.h— struct สาธารณะจริง (ไม่มีmagic)- ดูโฟลเดอร์เต็มที่
hmi_ep06_wifi_profile_nvm/
จุดที่มักพลาด
หัวข้อที่มีชื่อว่า “จุดที่มักพลาด”- คิดว่าใช้ generic flash API — README ต้นทางพูดกว้าง ๆ ถึง
cyhal_flash_*/cy_em_eeprom/MCUboot NVS แต่โค้ด จริงเรียก RRAM API (Cy_RRAM_TSReadByteArray,Cy_RRAM_NvmWriteByteArray) ตรง ๆ กับพื้นที่ user NVM ของ CM55 - ไม่เช็คว่า slot ถูก erase หรือมีข้อมูลจริง — ต้องเช็ค pattern
0xFFทั้งบล็อกก่อนเสมอ ไม่งั้นอาจตีความขยะจาก หน่วยความจำที่ยังไม่เคยเขียนเป็นโปรไฟล์ที่ใช้ได้ - เขียนแล้วไม่ตรวจกลับ (verify-after-write) — โค้ดต้นทางทำสองชั้นคือเช็ค return code ของการเขียน และอ่าน กลับมาเทียบไบต์ต่อไบต์อีกที ถ้าข้ามขั้นตอนนี้จะไม่รู้ว่าการเขียนเพี้ยนจริงหรือไม่
- คิดว่าแตะแถวใน scan list จะ auto-jump ไปหน้า profile ทันที — โค้ดจริงต้องกดปุ่ม “Use this AP” แยกอีกขั้น หลังเลือกแถว ไม่ใช่ auto-jump แบบที่ README ต้นทางอธิบาย
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
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”- ข้อมูลใน NVM ต่างจากตัวแปรใน RAM อย่างไรเมื่อบอร์ดรีเซ็ต
- ทำไมช่องรหัสผ่านต้องซ่อนตัวอักษร
- ถ้าต้องการลบโปรไฟล์ทั้งหมด ต้องเรียกฟังก์ชันใดของ profile store
คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- README ของ episode · โฟลเดอร์โค้ด · commit
9a8e3ed - เปิดตัวอย่างนี้บน Developer Hub
- โค้ดเป็นของ Developer Hub และอ้างอิงด้วยลิงก์ ไม่ได้คัดลอกเข้าคลังนี้
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
lv_textarea_set_password_mode(password_ta, true) ป้องกันอะไรได้ และป้องกันอะไรไม่ได้ (เป้าหมายข้อ 1)
- เข้ารหัสรหัสผ่านก่อนบันทึกลง NVM
- ซ่อนตัวอักษรบนจอจากคนที่มองอยู่ แต่ lv_textarea_get_text() ยังคืนรหัสจริง และค่าที่บันทึกลง NVM เป็นข้อความจริง
- ป้องกันไม่ให้โค้ดส่วนอื่นอ่านค่าจาก textarea
- ลบรหัสผ่านออกจาก RAM หลังพิมพ์เสร็จ
ดูเฉลย
คำตอบ: B. ซ่อนตัวอักษรบนจอจากคนที่มองอยู่ แต่ lv_textarea_get_text() ยังคืนรหัสจริง และค่าที่บันทึกลง NVM เป็นข้อความจริง
password mode เปลี่ยนแค่การแสดงผล ui_profile_read_to_data() ยังอ่านค่าด้วย lv_textarea_get_text() แล้ว strncpy ลงโครงสร้าง profile และ wifi_profile_store_save() เขียนข้อความนั้นลง NVM ตรง ๆ
-
กด Save ในกรณีใดบ้างที่โปรไฟล์ถูกบันทึกลง NVM จริง (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 1)
- SSID “Lab”, security OPEN, รหัสผ่านว่าง
- SSID “Lab”, security WPA2-AES-PSK, รหัสผ่านว่าง
- SSID ว่าง, security OPEN
- SSID “Lab”, security WPA3-SAE, รหัสผ่าน “abcd1234”
ดูเฉลย
คำตอบ: A. SSID “Lab”, security OPEN, รหัสผ่านว่าง · D. SSID “Lab”, security WPA3-SAE, รหัสผ่าน “abcd1234”
ui_profile_validate_before_save() ปฏิเสธ SSID ว่าง (Save failed: SSID is empty) และปฏิเสธรหัสผ่านว่างเมื่อ security ไม่ใช่ OPEN (Save failed: Password is required) ด่านนี้อยู่ก่อนเรียก wifi_profile_store_save() ข้อมูลที่ไม่ครบจึงไม่ถูกเขียน
-
ไฟดับระหว่างเขียนโปรไฟล์ ทำให้ slot ใน NVM มีข้อมูลใหม่ครึ่งหนึ่งเก่าครึ่งหนึ่ง เมื่อเปิดเครื่องใหม่ wifi_profile_store_load() จะทำอย่างไร (เป้าหมายข้อ 2)
- โหลดข้อมูลครึ่ง ๆ นั้นมาใช้
- ซ่อมข้อมูลให้เองจาก CRC
- ตรวจ magic, version, valid, payload_len และ CRC32 ของ payload เมื่อไม่ตรงจะไม่ใช้ slot นั้น (แล้วลอง slot legacy) ถ้าไม่มี slot ใดผ่านจะรายงานว่าไม่มีโปรไฟล์
- ลบ NVM ทั้งหมดแล้วรีเซ็ตบอร์ด
ดูเฉลย
คำตอบ: C. ตรวจ magic, version, valid, payload_len และ CRC32 ของ payload เมื่อไม่ตรงจะไม่ใช้ slot นั้น (แล้วลอง slot legacy) ถ้าไม่มี slot ใดผ่านจะรายงานว่าไม่มีโปรไฟล์
wifi_profile_parse_record() ตรวจ header แล้วคำนวณ CRC32 ใหม่เทียบกับค่าที่เก็บ ถ้าไม่ตรงจะ log CRC_FAIL และคืน false หน้า Profile จึงขึ้นว่าไม่มีโปรไฟล์ แทนการนำ SSID หรือรหัสผ่านที่เสียไปใช้ CRC ตรวจจับความเสียหายได้ แต่ซ่อมไม่ได้
-
กด Save ซ้ำด้วยข้อมูลเดิม wifi_profile_store_save() ทำอะไร และทำไม (เป้าหมายข้อ 2)
- อ่าน slot ปัจจุบันมาเทียบ ถ้าเหมือนทุกไบต์จะข้ามการเขียน (SAVE_SKIP_SAME) เพื่อลดการสึกของหน่วยความจำ ถ้าต่างจะเขียนแล้วอ่านกลับมาตรวจอีกครั้ง
- เขียนทับทุกครั้งเพื่อให้แน่ใจว่าข้อมูลเป็นปัจจุบัน
- เขียนลง slot ใหม่ถัดไปเพื่อเก็บประวัติ
- ล้าง slot ก่อนแล้วค่อยเขียน
ดูเฉลย
คำตอบ: A. อ่าน slot ปัจจุบันมาเทียบ ถ้าเหมือนทุกไบต์จะข้ามการเขียน (SAVE_SKIP_SAME) เพื่อลดการสึกของหน่วยความจำ ถ้าต่างจะเขียนแล้วอ่านกลับมาตรวจอีกครั้ง
โค้ดเทียบ current_block กับ block ใหม่ด้วย memcmp ก่อนเขียน เพราะหน่วยความจำไม่ลบเลือนมีจำนวนรอบการเขียนจำกัด หลังเขียนยังอ่าน slot กลับมาเทียบ (SAVE_VERIFY_FAIL ถ้าไม่ตรง) เพื่อยืนยันว่าเขียนสำเร็จจริง ไม่ใช่แค่ฟังก์ชันคืนค่าสำเร็จ
-
record ใน NVM มี CRC32 อยู่แล้ว ข้อใดสรุปความเสี่ยงของการเก็บรหัสผ่านแบบนี้ได้ถูกต้อง (เป้าหมายข้อ 3)
- ปลอดภัยแล้ว เพราะ CRC32 ทำให้อ่านรหัสผ่านไม่ออก
- ปลอดภัยแล้ว เพราะ password mode เข้ารหัสไว้ก่อนเขียน
- ความเสี่ยงอยู่ที่จอเท่านั้น เพราะหน่วยความจำภายในชิปอ่านจากภายนอกไม่ได้
- CRC32 แค่ตรวจว่าข้อมูลเสียหรือไม่ รหัสผ่านยังเป็นข้อความจริงใน NVM ใครอ่านหน่วยความจำได้ (debugger หรือ dump เฟิร์มแวร์) ก็ได้รหัสไป ควรเข้ารหัสด้วยกุญแจที่เก็บในที่ปลอดภัย เช่น secure element และปิดทาง debug ในเครื่องที่ส่งมอบ
ดูเฉลย
คำตอบ: D. CRC32 แค่ตรวจว่าข้อมูลเสียหรือไม่ รหัสผ่านยังเป็นข้อความจริงใน NVM ใครอ่านหน่วยความจำได้ (debugger หรือ dump เฟิร์มแวร์) ก็ได้รหัสไป ควรเข้ารหัสด้วยกุญแจที่เก็บในที่ปลอดภัย เช่น secure element และปิดทาง debug ในเครื่องที่ส่งมอบ
wifi_profile_make_record() ใช้ strncpy ใส่รหัสผ่านลง record ตรง ๆ แล้วคิด CRC32 ทับ CRC ไม่ใช่การเข้ารหัส ใครก็คำนวณใหม่ได้ บทเรียน OPTIGA Trust M ในโมดูล 5 แสดงแนวทางเก็บความลับไว้ในชิปที่อ่านกุญแจออกไม่ได้
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"เก็บโปรไฟล์ Wi-Fi ลงหน่วยความจำถาวร" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "Store the Wi-Fi profile in non-volatile memory" 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/m02-hmi-menu-setting/l06-wifi-profile-nvm/
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/9a8e3ed1d813bfd67fabf6b7ac15c6ff9750b465/hmi_ep06_wifi_profile_nvm · 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