วันหนึ่งเฟิร์มแวร์ที่เคยอัปเดตผ่าน OTA ได้ดีมาตลอด จู่ ๆ ก็ขึ้น fail begin ทุกครั้ง ทั้งที่ตอนคอมไพล์ Arduino IDE บอกชัดเจนว่า:

1
2
Sketch uses 1368848 bytes (69%) of program storage space.
Maximum is 1966080 bytes.

ใช้ไปแค่ 69% เอง แล้วทำไมถึงเขียนไม่ลง?

คำตอบ: ตัวเลขนั้นไม่ใช่ของบอร์ดคุณ

เปอร์เซ็นต์ที่ IDE แสดงคือขนาดของตารางพาร์ทิชันที่คุณเลือกไว้ในเมนู Tools ตอนนั้น ไม่ใช่ตารางที่อยู่ในชิปจริง ๆ

และนี่คือหัวใจของปัญหา — ตารางพาร์ทิชันเปลี่ยนผ่าน OTA ไม่ได้

ตารางพาร์ทิชันอยู่ที่แอดเดรส 0x8000 ในแฟลช ส่วน OTA เขียนได้เฉพาะช่อง app เท่านั้น เวลาอัปเดตผ่าน OTA ตารางจึงยังเป็นของเดิมที่ถูกเขียนไว้ตอน flash ผ่านสาย USB ครั้งแรก ตลอดไป

เพราะฉะนั้นถ้าตอนแรกคุณ flash ด้วย USB โดยเลือก partition แบบ default แล้ววันหลังเปลี่ยนเมนูเป็น min_spiffs เพื่อให้โค้ดใหญ่ขึ้นได้ — บอร์ดจะยังใช้ตาราง default อยู่ IDE บอกว่าพอ แต่บอร์ดบอกว่าไม่พอ

ตัวเลขที่ต้องจำ

Partition Schemeช่อง app (OTA เขียนได้)
Default 4MB with spiffs1,310,720 ไบต์
Minimal SPIFFS (Large APPS)1,966,080 ไบต์

ถ้าไฟล์เฟิร์มแวร์ของคุณอยู่ระหว่างสองค่านี้แล้ว OTA ล้ม แปลว่าบอร์ดถูก flash ครั้งแรกด้วย default เกือบแน่นอน

เคสที่ผมเจอ: เฟิร์มแวร์เดิม 1,267,168 ไบต์ อัปเดต OTA ได้ปกติ (ต่ำกว่า 1,310,720) พอเพิ่มการเชื่อมต่อแบบเข้ารหัสเข้าไป โตเป็น 1,368,848 ไบต์ ซึ่งเกินเพดานเดิมไป 58 KB — จบเห่ทันที

ทางแก้

ต่อสาย USB flash หนึ่งครั้ง โดยเลือก partition scheme ที่ต้องการ การ flash ผ่าน USB จะเขียน bootloader + ตารางพาร์ทิชัน + แอปพร้อมกัน หลังจากนั้น OTA จะใช้พื้นที่ได้เต็มขนาดใหม่

ข่าวดีคือค่าที่ตั้งไว้ไม่หาย — ช่อง NVS อยู่ที่ 0x9000 และมีขนาดเท่ากันทั้งสอง scheme ค่า WiFi, รหัสอุปกรณ์, ค่าตั้งต่าง ๆ ที่เก็บด้วย Preferences.h ยังอยู่ครบหลัง flash

ทำให้ครั้งหน้าไม่ต้องเดา

ปัญหาจริง ๆ ของเรื่องนี้คือคำว่า fail begin มันไม่ได้บอกอะไรเลย ให้แก้ตัวรับ OTA ฝั่งบอร์ดให้รายงานเหตุผลกลับมาด้วย:

1
2
3
4
5
6
7
8
if (!Update.begin(sizeOfNewFirmware)) {
  // ส่งกลับไปให้เห็นว่าเกิดอะไรขึ้นจริง ๆ
  Serial.printf("OTA เริ่มไม่ได้: %s | พื้นที่ที่เขียนได้จริง: %u ไบต์ | ไฟล์ใหม่: %u ไบต์\n",
                Update.errorString(),
                ESP.getFreeSketchSpace(),
                sizeOfNewFirmware);
  return;
}

ESP.getFreeSketchSpace() คือตัวเลขที่บอกความจริง — มันอ่านจากตารางพาร์ทิชันที่อยู่ในบอร์ดจริง ไม่ใช่จากเมนูตอนคอมไพล์ เอาสองตัวเลขนี้มาเทียบกันก็รู้ทันทีว่าปัญหาคืออะไร

ถ้าระบบ OTA ของคุณส่งผลกลับผ่าน MQTT หรือ HTTP อยู่แล้ว ส่งข้อความนี้กลับไปด้วยจะประหยัดเวลาไล่หาสาเหตุได้มาก

เกร็ดเพิ่ม: ระบบกันอัปเดตถอยหลังที่อาจล็อกตัวเอง

ถ้าระบบ OTA ของคุณมีกลไกกันอัปเดตย้อนเวอร์ชัน (เทียบ timestamp หรือเลขเวอร์ชันก่อนยอมเขียน) ให้ระวังกรณีนี้:

พอ flash บอร์ดผ่าน USB ด้วยไฟล์ที่ไม่เคยอัปขึ้นเซิร์ฟเวอร์ บอร์ดจะรันเฟิร์มแวร์ที่เซิร์ฟเวอร์ไม่รู้จัก กลไกกันถอยหลังจะมองว่าทุกอย่างบนเซิร์ฟเวอร์ “เก่ากว่าตัวเอง” แล้ว ปฏิเสธ OTA ทุกเวอร์ชันถาวร — ปลดล็อกจากฝั่งเซิร์ฟเวอร์ไม่ได้เลย ต้องเดินไปต่อสาย USB อย่างเดียว

วิธีกัน: อัปไฟล์ .bin ขึ้นเซิร์ฟเวอร์ก่อน แล้วค่อย flash USB ด้วยไฟล์เดียวกันนั้น พอบอร์ดบูตขึ้นมามันจะเห็นว่าตรงกับที่เซิร์ฟเวอร์มี แล้วรับค่าฐานเวลาไปใช้ต่อได้ตามปกติ

ทำเป็นนิสัยไว้เลยครับ: อัปขึ้นก่อน แล้วค่อย flash ไม่ว่าจะเร่งแค่ไหน