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

เก็บโปรไฟล์ Wi-Fi ลงหน่วยความจำถาวร

  1. สร้างฟอร์มกรอก SSID และรหัสผ่าน โดยช่องรหัสผ่านอยู่ใน password mode
  2. บันทึก โหลด และล้างโปรไฟล์ผ่าน profile store ใน NVM ได้ และค่ายังอยู่หลังรีเซ็ตบอร์ด
  3. อธิบายความเสี่ยงของการเก็บรหัสผ่านใน 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 แยกต่างหาก

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.");
}
  • คิดว่าใช้ 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 ต้นทางอธิบาย
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 เฟิร์มแวร์สำเร็จรูป

หน้าจอของ EP06 — WiFi Profile NVM บน TESAIoT Dev Kit

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

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

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

คำถามทบทวน

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

  1. lv_textarea_set_password_mode(password_ta, true) ป้องกันอะไรได้ และป้องกันอะไรไม่ได้ (เป้าหมายข้อ 1)

    1. เข้ารหัสรหัสผ่านก่อนบันทึกลง NVM
    2. ซ่อนตัวอักษรบนจอจากคนที่มองอยู่ แต่ lv_textarea_get_text() ยังคืนรหัสจริง และค่าที่บันทึกลง NVM เป็นข้อความจริง
    3. ป้องกันไม่ให้โค้ดส่วนอื่นอ่านค่าจาก textarea
    4. ลบรหัสผ่านออกจาก 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 ตรง ๆ

  2. กด Save ในกรณีใดบ้างที่โปรไฟล์ถูกบันทึกลง NVM จริง (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 1)

    1. SSID “Lab”, security OPEN, รหัสผ่านว่าง
    2. SSID “Lab”, security WPA2-AES-PSK, รหัสผ่านว่าง
    3. SSID ว่าง, security OPEN
    4. 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() ข้อมูลที่ไม่ครบจึงไม่ถูกเขียน

  3. ไฟดับระหว่างเขียนโปรไฟล์ ทำให้ slot ใน NVM มีข้อมูลใหม่ครึ่งหนึ่งเก่าครึ่งหนึ่ง เมื่อเปิดเครื่องใหม่ wifi_profile_store_load() จะทำอย่างไร (เป้าหมายข้อ 2)

    1. โหลดข้อมูลครึ่ง ๆ นั้นมาใช้
    2. ซ่อมข้อมูลให้เองจาก CRC
    3. ตรวจ magic, version, valid, payload_len และ CRC32 ของ payload เมื่อไม่ตรงจะไม่ใช้ slot นั้น (แล้วลอง slot legacy) ถ้าไม่มี slot ใดผ่านจะรายงานว่าไม่มีโปรไฟล์
    4. ลบ 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 ตรวจจับความเสียหายได้ แต่ซ่อมไม่ได้

  4. กด Save ซ้ำด้วยข้อมูลเดิม wifi_profile_store_save() ทำอะไร และทำไม (เป้าหมายข้อ 2)

    1. อ่าน slot ปัจจุบันมาเทียบ ถ้าเหมือนทุกไบต์จะข้ามการเขียน (SAVE_SKIP_SAME) เพื่อลดการสึกของหน่วยความจำ ถ้าต่างจะเขียนแล้วอ่านกลับมาตรวจอีกครั้ง
    2. เขียนทับทุกครั้งเพื่อให้แน่ใจว่าข้อมูลเป็นปัจจุบัน
    3. เขียนลง slot ใหม่ถัดไปเพื่อเก็บประวัติ
    4. ล้าง slot ก่อนแล้วค่อยเขียน
    ดูเฉลย

    คำตอบ: A. อ่าน slot ปัจจุบันมาเทียบ ถ้าเหมือนทุกไบต์จะข้ามการเขียน (SAVE_SKIP_SAME) เพื่อลดการสึกของหน่วยความจำ ถ้าต่างจะเขียนแล้วอ่านกลับมาตรวจอีกครั้ง

    โค้ดเทียบ current_block กับ block ใหม่ด้วย memcmp ก่อนเขียน เพราะหน่วยความจำไม่ลบเลือนมีจำนวนรอบการเขียนจำกัด หลังเขียนยังอ่าน slot กลับมาเทียบ (SAVE_VERIFY_FAIL ถ้าไม่ตรง) เพื่อยืนยันว่าเขียนสำเร็จจริง ไม่ใช่แค่ฟังก์ชันคืนค่าสำเร็จ

  5. record ใน NVM มี CRC32 อยู่แล้ว ข้อใดสรุปความเสี่ยงของการเก็บรหัสผ่านแบบนี้ได้ถูกต้อง (เป้าหมายข้อ 3)

    1. ปลอดภัยแล้ว เพราะ CRC32 ทำให้อ่านรหัสผ่านไม่ออก
    2. ปลอดภัยแล้ว เพราะ password mode เข้ารหัสไว้ก่อนเขียน
    3. ความเสี่ยงอยู่ที่จอเท่านั้น เพราะหน่วยความจำภายในชิปอ่านจากภายนอกไม่ได้
    4. 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 ฉบับเต็ม

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

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