ESP32-S3 สำหรับงาน camera #1 MicroPython/CircuitPython firmware
Python เป็นภาษาคอมพิวเตอร์แบบ Object-Oriented Programming ที่เรียนและเข้าใจง่าย มีฟีเจอร์ของ data structure เช่น list, dict และ set รวมทั้งมีไลบรารีมาตรฐานที่เหมาะกับงานสมัยใหม่ เช่น string processing, URL, JSON ทำให้เหมาะในการพัฒนาซอฟต์แวร์ในแบบ rapid prototyping อย่างไรก็ตาม ข้อจำกัดหลักของ Python เมื่อมาใช้กับไมโครคอนโทรลเลอร์คือ ทรัพยากรของหน่วยประมวลผล (clock speed, Flash, RAM) ไม่เพียงพอ การพัฒนาเฟิร์มแวร์จึงยังต้องอาศัยภาษา C เป็นหลัก โดยงานด้าน rapid prototype มักนิยมไปเขียนด้วย Arduino ที่ได้ abstract ส่วนโค้ดในการเข้าถึงฮาร์ดแวร์
MicroPython และ CircuitPython
MicroPython (และ CircuitPython ที่ทาง Adafruit ได้ fork มาทำเอง) เป็นโครงการโอเพนซอร์ส (เผยแพร่บน GitHub) ที่ปรับภาษา Python ให้มาทำงานบนไมโครคอนโทรลเลอร์สมัยใหม่ เช่น กลุ่มไมโครคอนโทรลเลอร์ที่ใช้ส่วนคอร์ ARM Cortex-M รวมไปถึง ESP32 ที่มีทรัพยากรมากขึ้น (clock > 100MHz, Flash > 1MB, RAM > 50kB) การใช้งาน MicroPython จะใช้การติดตั้งเฟิร์มแวร์ที่ build สำหรับไมโครคอนโทรลเลอร์รุ่นนั้น ซึ่งจะทำงานแบบ REPL (Read-Eval-Print Loop) ผ่านทางช่องสื่อสารแบบอนุกรม ทำให้ผู้ใช้สามารถเขียนคำสั่งหรือสั่งให้ประมวลผลสคริปต์ได้ผ่านโปรแกรมประเภท serial terminal เช่น โปรแกรม Thonny
จุดเด่นหนึ่งของภาษา Python คือการเขียนโค้ดในรูปแบบ C module เพื่อแยกเขียนเฉพาะโค้ดที่ต้องการให้ทำงานเร็วด้วยภาษา C แล้วค่อย import มาทำงานร่วมกับโค้ด Python ส่วนอื่นๆได้ ทั้งนี้ MicroPython จะรองรับการสร้างไฟล์ C module ใน 2 รูปแบบคือ
- การสร้าง external C module ที่รวมมากับเฟิร์มแวร์ เป็นการรวมส่วนโค้ดเข้ากับ MicroPython repo ที่ clone จาก GitHub ซึ่งเมื่อ build แล้วจะกลายเป็นส่วนหนึ่งของ REPL ที่เรียกใช้ได้เลย
- การสร้างไฟล์ .mpy ในแบบ native code เป็นการใช้เครื่องมือที่ใช้ในการ build เฟิร์มแวร์ MicroPython มา cross build โค้ดภาษา C เป็นไฟล์ .mpy ที่สามารถ import เข้ามาทำงานใน RAM ได้
อย่างไรก็ตาม การสร้าง C module ทั้งสองวิธีจะต้องใช้เครื่องมือและส่วนโค้ดของ MicroPython ที่อิงอยู่บน Linux เช่น make จึงจะเป็นปัญหาสำหรับคนที่ใช้ MS Windows ผมได้ทดลองแล้วพบว่าขั้นตอนที่น่าจะสะดวกที่สุดคือ การติดตั้งและใช้งาน Windows Subsystem for Linux (WSL) เพื่อ build ตัวเฟิร์มแวร์ แล้วค่อย copy ไฟล์ (bootloader, partition table, firmware) ข้ามไปส่วนไดร์ฟ MS Windows แล้วจึงใช้เครื่องมือ esp-idf เพื่อติดตั้งไปที่บอร์ด
ESP32-S3
ESP32-S3 เป็นไมโครคอนโทรลเลอร์ในรูปแบบ System-on-Chip ของบริษัท Espressif ที่พัฒนาต่อจาก ESP32 โดยมีจุดเด่นที่ปรับปรุงขึ้นหลายด้าน เช่น
- รองรับขนาดหน่วยความจำแฟลชถึง 16 MB และมี SRAM ใหญ่ขึ้นเป็น 512 kB
- รองรับการเชื่อมต่อหน่วยความจำ Flash แบบ Quad SPI และหน่วยความจำ PSRAM แบบ Octal SPI
- ปรับการออกแบบส่วน on-chip peripheral ให้ทำงานแบบ low-power
- เพิ่มชุดคำสั่งประมวลผล vector เพื่อรองรับงาน Digital Signal Processing และ Neural Networks
- มีส่วน USB serial/JTAG สำหรับการโปรแกรม และส่วน USB OTG สำหรับจำลองทั้ง USB host และ USB device (mass storage และ CDC profile)
บอร์ด ESP32 ที่ออกขายในช่วงหลังจะขยับไปใช้ ESP32-S3 มากขึ้น แต่ข้อควรระวังคือ การที่ ESP32-S3 มีทั้งส่วน USB serial/JTAG และ USB OTG โดย MicroPython เลือกใช้ USB OTG มาทำงานในรูปแบบ USB CDC ที่ทำให้เกิด Virtual COM port บนคอมพิวเตอร์ การโปรแกรมเฟิร์มแวร์ใหม่จึงต้องบังคับให้ ESP32-S3 ไปอยู่ใน serial bootloader ก่อน โดยการกดปุ่ม boot (ขา IO0) แล้วทำการ reset ก่อนจะปล่อยปุ่ม boot
ผมซื้อบอร์ด T-Camera S3 ที่เป็นผลิตภัณฑ์ของ Lilygo มาสักพักแล้ว โดยตัวบอร์ดเองก็มีโค้ดตัวอย่างบน GitHub ที่เป็น Arduino แบบใช้ Platform.io มาให้ด้วย แต่ส่วนโค้ดที่เป็นภาษา C มีความซับซ้อนพอสมควร เลยอยากจะทดลองปรับมาใช้งานบน MicroPython บ้าง จึงเริ่มด้วยการทดลองใช้เฟิร์มแวร์สำเร็จรูปทั้ง MicroPython และ CircuitPython โดยจะประเมินการใช้หน่วยความจำ PSRAM (บอร์ด T-Camera S3 ใช้ ESP32-S3-WROOM-1 แบบมี 8MB PSRAM) ว่าใช้งานภายในสภาพแวดล้อมภาษา Python ได้แค่ไหน
การทดสอบเฟิร์มแวร์สำเร็จรูปของ MicroPython จะเรียกใช้คำสั่ง esptool.py จาก PowerShell ที่ถูกสร้างจากการติดตั้ง esp-idf บน MS Windows คำสั่งติดตั้งเฟิร์มแวร์จะอ้างอิงตามคำอธิบายในเว็บไซต์โดยแบ่งเป็น 2 ขั้นคือ การลบหน่วยความจำแฟลชและการเขียนเฟิร์มแวร์ลงหน่วยความจำแฟลชที่ offset คือ 0x0 ทั้งนี้ ก่อนการเขียนหน่วยความจำแฟลชลงบอร์ด T-Camera S3 จะต้องกดปุ่ม RESET และ boot ก่อนเสียบสาย USB เพื่อบังคับให้ serial bootloader ทำงาน
esptool.py --chip esp32s3 --port COMx erase_flash
esptool.py --chip esp32s3 --port COMx write_flash -z 0 firmware.bin
ผมเตรียมโค้ดทดสอบเป็นการวน 50 รอบสร้าง list ของค่าสุ่ม (เลขทศนิยม) จำนวน 1000 ค่าแล้วคำนวณผลรวมของค่าสุ่ม เพื่อบังคับให้ใช้หน่วยความจำเกินที่ SRAM รองรับได้ (MicroPython จะกันหน่วยความจำ SRAM ของ ESP32-S3 เป็น heap ไว้ประมาณ 150kB ในกรณีที่ไม่มี PSRAM) การทดสอบพบว่า MicroPython จะใช้หน่วยความจำประมาณ 1.8 MB เพื่อรองรับข้อมูลที่เกิดขึ้น โดยหลังจากทำงานเสร็จจะพบว่าส่วน REPL จะหยุดตอบสนองประมาณ 20–30 วินาที ซึ่งคาดว่าจะเป็นกลไก garbage collection ที่ทำงานอยู่ แม้จะขลุกขลักบ้าง แต่ก็แสดงว่า MicroPython มีกลไกการบริหารหน่วยความจำที่สามารถรวมเอา PSRAM มาไว้ในฐานะ heap ซึ่งทำให้เขียนโค้ดจัดการข้อมูลได้ง่ายกว่า
ในระหว่างการทดสอบ ผมพบว่าคำสั่ง micropython.mem_info() จะรายงานหน่วยความจำได้ถูกใช้ไปเรื่อยๆแม้ว่าจะไม่ได้ทำงานอะไร มีการถกกันถึงปัญหานี้ใน forum ว่าเป็นสถานการณ์ memory leak หรือไม่ ผู้ที่จะพัฒนาโค้ดที่ต้องทำงานต่อเนื่องเป็นเวลานาน ควรให้ความสำคัญกับประเด็นของหน่วยความจำที่ถูกใช้ไปเรื่อยๆนี้ เพราะแม้คำสั่ง gc.collect() จะช่วยคืนหน่วยความจำกลับมา แต่คำสั่งนี้จะใช้เวลานานพอสมควรสำหรับหน่วยความจำที่มีทั้ง SRAM และ PSRAM รวมกัน
การทดสอบเฟิร์มแวร์สำเร็จรูปของ CircuitPython จะใช้เว็บ Adafruit ESPTool ที่ทำงานบนเบราเซอร์เพื่อเขียน UF2 bootloader (ไฟล์ combine.bin) ลงหน่วยความแฟลชก่อน จากนั้นจะใช้การลากไฟล์ .uf2 ลงไปใน USB disk จำลองเพื่อเป็นการอัพเดทเฟิร์มแวร์
โค้ดทดสอบจะใช้แนวทางเดียวกับ MicroPython โดยสร้างและคำนวณผลรวมของค่าสุ่มจำนวน 1000 ค่า โดยมีข้อแตกต่างที่จะใช้คำสั่ง gc.mem_alloc() และ gc.mem_free() เพื่อติดตามการใช้หน่วยความจำ
ผมพบว่า CircuitPython จะใช้หน่วยความจำน้อยกว่า MicroPython สำหรับโค้ดทดสอบเดียวกัน โดยใช้ 200kB ในการจัดการ list ขนาด 50 สมาชิกของค่าสุ่ม 1000 ค่า ซึ่งเมื่อคำนวณจากขนาดตัวแปร float สำหรับ CircuitPython คือ 30 บิต (4 ไบต์) พบว่าสอดคล้องมากกว่าในกรณี MicroPython (1.8 MB)
สรุปรายการของเฟิร์มแวร์และผลการทดลองใช้งานมีดังนี้
- MicroPython: Generic ESP32-S3 (SPIRAM) ไม่ทำงาน ตรวจสอบจากไม่มี serial port แสดงบน Device Manager คาดว่าเกิดจากการกำหนดรูปแบบการเชื่อมต่อ PSRAM ไม่ถูกต้อง
- MicroPython: Generic ESP32-S3 (SPIRAM Octal) ทำงานได้ แต่เฟิร์มแวร์มาจาก nightly build (14–04–2023)
- CircuitPython: ESP32-S3-DevKitC-1-N8R8 ทำงานได้
- CircuitPython: ESP32-S3 Box ทำงานผิดพลาดเพราะหน่วยความจำไม่เพียงพอ พบว่าเกิดจากตรวจจับ PSRAM ไม่ได้
- CircuitPython: Maker Feather AIoT S3 ทำงานได้
ข้อสรุปในเบื้องต้นคือ การตั้งค่า PSRAM ที่ไม่ถูกต้องในขั้นตอนการ build น่าจะเป็นปัญหาหลักที่จะทำให้เราต้องเลือกเฟิร์มแวร์สำเร็จรูปให้เหมาะสมกับบอร์ด หรืออาจเลือก build เฟิร์มแวร์เองโดยกำหนด Octal SPI ในการเชื่อมต่อ PSRAM
終わりに (ในตอนท้าย)
บทความนี้แนะนำการทดลองเฟิร์มแวร์สำเร็จรูปของ ESP32-S3 ของ MicroPython และ CircuitPython กับบอร์ด T-Camera S3 ซึ่งเป็นก้าวแรกในการนำภาษา Python มาทดลองกับบอร์ดไมโครคอนโทรลเลอร์ที่ติดตั้งโมดูลกล้อง (บอร์ด T-Camera S3 มีโมดูล OV2640) ดังนั้น การทดลองขั้นแรกจึงเน้นที่ศึกษาว่าหน่วยความจำ PSRAM จะนำมาใช้อย่างไร ซึ่งข้อสรุปในเบื้องต้นคือ สะดวกกว่าการเขียนโค้ดภาษา C ด้วยการเชื่อมโยง heap ให้ทำงานร่วมกันระหว่าง SRAM และ PSRAM แต่ข้อควรระวังคือ การหยุดชะงัดของโค้ดหลังจากโค้ดที่ใช้หน่วยความจำมาก อาจส่งผลกระทบต่อการทำงานของอุปกรณ์ได้ นอกจากนี้ MicroPython สำหรับ ESP32-S3 เหมือนจะมีปัญหา memory leak และใช้หน่วยความจำมากกว่า CircuitPython อย่างมีนัย จึงเป็นประเด็นที่ต้องพิจารณาในการเลือกว่าจะพัฒนาต่อยอดจากเฟิร์มแวร์ไหน