บทความกล่าวถึงการใช้ไลบรารี LittleFS และแนะนำไลบรารีที่ถูกพัฒนาเพื่อใช้กับไมโครคอนโทรเลอร์ esp32 ซึ่งมีส่วนเสริม (plugin) ของ Arduino IDE สำหรับอัพโหลดไฟล์ไปเก็บในรอมของไมโครคอนโทรลเลอร์ ทำให้สะดวกต่อการโหลดข้อมูลไปเก็บและเรียกใช้งาน ด้วยเหตุนี้ถ้าผู้เขียนโปรแกรมรู้สึกยุ่งยากกับการแปลงโค้ด HTML/CSS/JavaScript ให้เป็นสตริงด้วยตนเอง และเปลี่ยนมาเป็นอัพโหลดไฟล์ไปเก็บใน esp32 แล้วอ่านไฟล์เว็บมาใช้งานโดยตรงจะเป็นสิ่งที่จะต้องฝึกฝนใช้งานเจ้า LittleFS ไว้เป็นไลบรารีคู่ใจกันเลยทีเดียว
LittleFS
LittleFS เป็นระบบไฟล์ (filesystem) ขนาดเล็กที่ออกแบบมาเพื่อใช้งานกับไมโครคอนโทรลเลอร์ ภายใต้สิทธิการใช้งานแบบ BSD-3-Clauseโดยผู้ใช้งานสามารถเขียน แก้ไข ปิด และลบไฟล์ได้ ซึ่งจุดเด่นของ LittleFS ได้แก่
- หากไฟดับระบบไฟล์จะถอยกลับไปสู่สถานะใช้งานก่อนหน้า
- ออกแบบให้รู้จักหลบเลี่ยงบล็อกของแฟลชที่เสียหายได้
ขั้นตอนการใช้งานระบบไฟล์ของ LittleFS เป็นดังนี้
- ตั้งค่าระบบไฟล์
- ทำการ mount()
- ถ้า mount() ไม่ได้แสดงว่ายังไม่มีระบบไฟล์ติดตั้งอยู่ ให้เรียก format()
- ใช้งานไดเรกทอรีและไฟล์
- ทำการ unmount()
เมื่อศึกษารายละเอียดจากไฟล์ lfs.h (เข้าถึงเมื่อวันที่ 2021-11-01) จะได้รายละเอียดที่สำคัญดังนี้
โครงสร้าง lfs_config
โครงสร้งข้อมูล lfs_config ใช้สำหรับตั้งค่าระบบไฟล์ที่ต้องการใช้งาน โดยมีรายละเอียดค่าเบื้องต้นที่ต้องกำหนดก่อนใช้งานเป็นดังนี้
const struct lfs_config cfg = {
// block device operations
.read = user_provided_block_device_read,
.prog = user_provided_block_device_prog,
.erase = user_provided_block_device_erase,
.sync = user_provided_block_device_sync,
// block device configuration
.read_size = 16,
.prog_size = 16,
.block_size = 4096,
.block_count = 128,
.cache_size = 16,
.lookahead_size = 16,
.block_cycles = 500,
};
จากตัวอย่างด้านบนจะพบว่าได้มีการกำหนดค่าดังนี้
- กำหนดให้สามารถ read, prog, erase, sync ทำให้สามารถสร้าง อ่าน ลบ และซิงค์ข้อมูลได้
- กำหนดให้ขนาดของบล็อก
- การอ่านมีขนาด 16 บล็อก
- การเขียนมีขนาด 16 บล็อก
- ขนาดของบล็อกเป็น 4096 ไบต์
- จำนวนบล็อกที่จะได้จำนวน 128 บล็อก
- ขนาดของแคชเป็น 16 บล็อก
- ขนาดของ lookhead (บัฟเฟอร์ของบล็อกถัดไป) เป็น 16 บล็อก
- ค่าบล็อกไซเคิลหรือค่าของจำนวนครั้งในการลบบล็อกเป็น 500 (โดยปกติกำหนดเป็น 100-1,000)
โครงสร้าง lfs_info
โครงสร้างข้อมูล lfs_info ใช้สำหรับเก็บรายละเอียดของไฟล์ ซึ่งมีรายละเอียดดังนี้
struct lfs_info {
uint8_t type;
lfs_size_t size;
char name[LFS_NAME_MAX+1];
};
โดยที่
- type คือ ประเภทของไฟล์
- LFS_TYPE_REG เป็นไฟล์
- LFS_TYPE_DIR เป็นไดเร็กทอรี
- size คือขนาดของไฟล์ที่เป็น REG ซึ่งมีขนาดไม่เกินตัวเลขจำนวนเต็ม 32 บิต
- name คือ ชื่อเรียกของไฟล์หรือไดเร็กทอรี
โครงสร้าง lfs_config
โครงสร้างข้อมูลแบบ lfs_config ของ LittleFS เป็นดังนี้
struct lfs_file_config {
void *buffer;
struct lfs_attr *attrs;
lfs_size_t attr_count;
};
โดย
- buffer เป็นตัวแปรตัวชี้ไปยังหน่วยความจำสำหรับทำเป็นแคช (cache) ด้วยคำสั่ง lfs_malloc()
- lfs_attr เป็นตัวแปรที่ชี้ไปยังรายการ attribute ทั้งหมด
- attr_count คือ จำนวน attribute ที่เก็บอยู่ใน lfs_config
โครงสร้าง lfs_attr
โครงสร้างข้อมูล lfs_attr มีรูปแบบดังนี้
struct lfs_attr {
uint8_t type;
void *buffer;
lfs_size_t size;
};
โดยที่
- type สำหรับเก็บประเภทของ attribute
- buffer สำหรับเก็บตำแหน่งหน่วยความจำที่เก็บข้อมูลของ attribute
- size คือ ขนาดของ attribute ที่มีค่าไม่เกิน LFS_ATTR_MAX
ประเภทของข้อผิดพลาด
ข้อผิดพลาดที่รายงานจากการทำงานของ LittleFS มีดังนี้
- LFS_ERR_OK หมายถึง ไม่มีข้อผิดพลาด
- LFS_ERR_IO หมายถึง เกิดข้อผิดพลาดระหว่างการทำงานของอุปกรณ์
- LFS_ERR_CORRUP หมายถึง บล็อกความจำเสียหาย
- LFS_ERR_NOENT หมายถึง ไม่มีรายการไดเรกทอรี
- LFS_ERR_EXIST หมายถึง มีรายการของไฟล์หรือไดเรกทอรีอยู่ก่อนแล้ว
- LFS_ERR_NOTDIR หมายถึง รายการไม่ใช่ไดเรกทอรี
- LFS_ERR_ISDIR หมายถึง รายการเป็นไดเรกทอรี
- LFS_ERR_NOTEMPTY หมายถึง ไดเรกทอรีไม่ว่าง
- LFS_ERR_BADF หมายถึง หมายเลขไฟล์ไม่ถูกต้อง
- LFS_ERR_FBIG หมายถึง ขนาดไฟล์ใหญ่เกินไป
- LFS_ERR_INVAL หมายถึง พารามิเตอร์ไม่ถูกต้อง
- LFS_ERR_NOSPC หมายถึง บนอุปกรณ์มีพื้นที่ไม่เพียงพอ
- LFS_ERR_NOMEM หมายถึง หน่วยความจำไม่เพียงพอ
- LFS_ERR_NOATTR หมายถึง attribute ไม่มีข้อมูล
- LFS_ERR_NAMETOOLONG หมายถึง ชื่อไฟล์ยาวเกินไป
lfs_format()
คำสั่งสำหรับฟอร์แม็ตบล็อกเพื่อใช้งานกับ LittleFS มีรูปแบบการใช้งานดังนี้ ซึ่งจะคืนค่าเป็นค่าลบถ้าเกิดข้อผิดพลาดในการทำงาน
int lfs_format(lfs_t *lfs, const struct lfs_config *config);
lfs_mount()
คำสั่งสำหรับการเชื่อมโยงระบบไฟล์ให้เป็นไดรฟ์เพื่อใช้งานมีรูปแบบการใช้คำสั่งดังนี้
int lfs_mount(lfs_t *lfs, const struct lfs_config *config);
lfs_unmount()
คำสั่งสำหรับยกเลิกการเชื่อมโยงระบบไฟล์หลังจากไม่ใช่งานแล้วเป็นดังนี้
int lfs_unmount(lfs_t *lfs);
lfs_remove()
สำหรับสำหรับลบไฟล์หรือไดเรกทอรี ซึ่งการลบไดเรกทอรีจะใช้ได้กับไดเรกทอรีที่ว่างไม่มีไฟล์หรือไดเรกทอรีซ้อนอยู่ภายใน รูปแบบของคำสั่งเป็นดังนี้
int lfs_remove(lfs_t *lfs, const char *path);
lfs_rename()
คำสั่งสำหรับเปลี่ยนชื่อไฟล์หรือไดเรกทอรีเป็นดังนี้
int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath);
lfs_stat()
กรณีที่ต้องการข้อมูลของไฟล์ตามโครงสร้าง lfs_info สามารถใช้คำสั่งดังนี้
int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info);
เปิดไฟล์
คำสั่งและรูปแบบการใช้งานคำสั่งสำหรับเปิดใช้งานไฟล์ คือ
int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags);
ปิดไฟล์
เมื่อต้องการปิดไฟล์ที่เปิดไว้ต้องใช้คำสั่งดังนี้
int lfs_file_close(lfs_t *lfs, lfs_file_t *file);
อ่านข้อมูลจากไฟล์
คำสั่งสำหรับอ่านข้อมูลจากไฟล์ คือ lfs_file_read() มีรูปแบบของคำสั่งเป็นดังต่อไปนี้
lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, void *buffer, lfs_size_t size);
การเขียนข้อมูลลงไฟล์
การนำข้อมูลจากบัฟเฟอร์ buffer ขนาด size เขียนลงไฟล์ file ของระบบไฟล์ lfs มีรูปแบบการใช้งานดังนี้
lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, const void *buffer, lfs_size_t size);
การย้ายตัวชี้ไฟล์
ระบบไฟล์จะมีตัวแปรสำหรับเป็นตัวชี้ตำแหน่งเริ่มต้นสำหรับการเขียนหรืออ่านข้อมูล ซึ่งมักจะเปลี่ยนตำแหน่งโดยอัตโนมัติเมื่อสั่งเปิด ปิด อ่าน และเขียนไฟล์ แต่อย่างไรก็ดี ผู้เขียนโปรแกรมสามารถสั่งเปลี่ยนตำแหน่งของตัวชี้นี้ได้ด้วยคำสั่ง lfs_file_seek() ตามรูปแบบการใช้งานดังนี้
lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, lfs_soff_t off, int whence);
ค่า whence เป็นดังนี้
- LFS_SEEK_SET หมายถึง เริ่มจากตำแหน่งต้นไฟล์
- LFS_SEEK_CUR หมายถึง เริ่มจากตำแหน่งปัจจุบัน
- LFS_SEEK_END หมายถึง เริ้มจากท้ายไฟล์
การตัดไฟล์ทิ้ง
กรณีที่ต้องการตัดไฟล์ตั้งแต่ที่ตัวชี้ของไฟล์ชี้อยู่ทิ้งไปโดยไม่ต้องลบไฟล์ ให้ใช้งานคำสั่ง lfs_truncate() ตามรูปแบบต่อไปนี้
int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size);
ตำแหน่งของตัวชี้ในไฟล์
เมื่อต้องการทราบตำแหน่งตัวชี้ของไฟล์สามารถอ่านได้จากคำสั่งต่อไปนี้
lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file);
lfs_file_rewind()
ถ้าต้องการให้ตัวชี้ของไฟล์กลับไปชี้ที่ต้นไฟล์สามารถใช้คำสั่ง lfs_file_rewind() ตามรูปแบบต่อไปนี้
int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file);
ต้องการทราบขนาดของไฟล์
กรณีที่ผู้เขียนโปรแกรมต้องการทราบขนาดของไฟล์ที่เปิดใช้งานให้เรียกใช้คำสั่งต่อไปนี้
int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file);
การสร้างไดเรกทอรี
คำสั่งสำหรับสร้างไดเรกทอรีคือ
int lfs_mkdir(lfs_t *lfs, const char *path);
คำสั่งสำหรับเปิดไดเรกทอรี
กรณีที่ต้องการย้ายตำแหน่งของไดเรกทอรีเข้าไปยังไดเรกทอรีลูกที่ระบุ หรือตามเส้นทาง (path) ที่ระบุให้ใช้คำสั่งตามรูปแบบต่อไปนี้
int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path);
ปิดการใช้ไดเรกทอรี
สำหรับกรณีที่ต้องการปิดการใช้ไดเรกทอรีเพื่อยกเลิกการใช้งานรายการต่าง ๆ ภายใต้ไดเรกทอรีนั้นสามารถทำได้ด้วยคำสั่งต่อไปนี้
int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir);
รายการในไดเรกทอรี
กรณีที่ต้องการดูรายการภายในไดเรกทอรีที่ระบบหรือเส้นทางที่ระบบใช้คำสั่งต่อไปนี้ โดยรายการจะถูกเก็บในหน่วยความจำบัฟเฟอร์ที่ชื่อ info ซึ่งเทียบได้กับการใช้คำสั่ง ls ในระบบปฏิบัติการ Unix
int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info);
ย้ายตำแหน่งไดเรกทอรี
เมื่อต้องการย้ายตำแหน่งไปยังไดเรกทอรีอื่นใช้คำสั่งต่อไปนี้ ซึ่งเหมือนกับการใช้คำสั่ง cd ในระบบปฏิบัติการ Unix
int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off);
การกลับไปไดเรกทอรีราก
ถ้าต้องการย้ายตำแหน่งการอ้างอิงไดเรกทอรีเป็นไดเรกทอรีราก (root directory) ของระบบไฟล์ให้คำสั่งต่อไปนี้
int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir);
ไดเรกทอรีปัจจุบัน
สำหรับการอ่านค่าตำแหน่งของไดเรกทอรีปัจจุบันที่ใช้งานอยู่ให้เรียกใช้คำสั่ง lfs_dir_tell() ตามรูปแบบต่อไปนี้
lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir);
ขนาดของระบบไฟล์
กรณีที่ต้องการทราบขนาดของระบบไฟล์ให้ใช้คำสั่งตามรูปแบบต่อไปนี้
lfs_ssize_t lfs_fs_size(lfs_t *lfs);
การสั่งให้เข้าถึงไดเรกทอรีทั้งหมด
การเข้าถึงไดเรกทอรีทั้งหมดหรือ traverse เพื่อเข้าถึงทึกบล็อกของไดเรกทอรีสามารถกระทำโดยรูปแบบของคำสั่งต่อไปนี้
int lfs_fs_traverse(lfs_t lfs, int (cb)(void*, lfs_block_t), void *data);
โดยจะต้องสร้างฟังก์ชันสำหรับถูกเรียกใช้งานจากคำสั่ง lfs_fs_traverse() ด้วยรูปแบบของฟังก์ชันต่อไปนี้
int ชื่อฟังห์ชัน( lfs_block_t b ) {
return ข้อมูลส่งคืน;
}
ตัวอย่างโปรแกรมเริ่มต้นที่มากับ LittleFS เป็นการสร้างระบบไฟล์เพื่อนับจำนวนครั้งของการเรียกใช้ระบบไฟล์ ทำให้รู้ว่ามีการเปิดใช้งานระบบไฟล์ไปเป็นจำนวนครั้งเท่าไร (เมื่อก่อนพวกเรานิยมใช้วิธีนี้กับการล็อกจำนวนการใช้งานระบบ เพื่อใช้สร้างระบบงานที่เป็นงานตัวอย่าง)
#include "lfs.h"
// variables used by the filesystem
lfs_t lfs;
lfs_file_t file;
// configuration of the filesystem is provided by this struct
const struct lfs_config cfg = {
// block device operations
.read = user_provided_block_device_read,
.prog = user_provided_block_device_prog,
.erase = user_provided_block_device_erase,
.sync = user_provided_block_device_sync,
// block device configuration
.read_size = 16,
.prog_size = 16,
.block_size = 4096,
.block_count = 128,
.cache_size = 16,
.lookahead_size = 16,
.block_cycles = 500,
};
// entry point
int main(void) {
// mount the filesystem
int err = lfs_mount(&lfs, &cfg);
// reformat if we can't mount the filesystem
// this should only happen on the first boot
if (err) {
lfs_format(&lfs, &cfg);
lfs_mount(&lfs, &cfg);
}
// read current count
uint32_t boot_count = 0;
lfs_file_open(&lfs, &file, "boot_count", LFS_O_RDWR | LFS_O_CREAT);
lfs_file_read(&lfs, &file, &boot_count, sizeof(boot_count));
// update boot count
boot_count += 1;
lfs_file_rewind(&lfs, &file);
lfs_file_write(&lfs, &file, &boot_count, sizeof(boot_count));
// remember the storage is not updated until the file is closed successfully
lfs_file_close(&lfs, &file);
// release any resources we were using
lfs_unmount(&lfs);
// print the boot count
printf("boot_count: %d\n", boot_count);
}
LittleFS สำหรับ ESP32
จากข้อดีของ LittleFS และชุดคำสั่งของ LittleFS ข้างต้นได้มีผู้นำมาพัฒนาเพื่อใช้งานกับ ESP32 บนเฟรมเวิร์กของ Arduino ที่พัฒนาโดย lorol ซึ่งการติดตั้งให้เข้าไปที่เมนู Sketch -> Include Library -> Manage Libraries… ดังภาพที่ 1 แล้วให้เลือกรายการค้นหาเป็น LittleFS จะพบไลบรารีชื่อ ESP32 ดังภาพที่ 2
ตัวอย่างโปรแกรมที่มากับไลบรารีมี 2 ตัวอย่างดังนี้
นอกจากนี้ยังมีเครื่องมือเสริมสำหรับใช้อัพโหลดไฟล์จากเครื่องพัฒนาโปรแกรมไปเก็บในไมโครคอนโทรลเลอร์ผ่านทางพอร์ตสื่อสารอนุกรมที่ชื่อว่า arduino-esp32fs-plugin ส่วนสำหรับ esp8266 สามารถศึกษาได้จาก Random Nerd Tutorial ครับ
สรุป
จากบทความนี้จะพบว่าการใช้งาน LittleFS ทำให้เราสามารถใช้ FlashROM ของไมโครคอนโทรลเลอร์ esp32/esp8266 ได้หลากหลายขึ้น ทำให้สามารถใช้ส่วนที่ไม่ได้ถูกใช้งานได้โดยไม่เกี่ยวกับโปรแกรมที่พัฒนา แต่อย่างไรก็ดี ผู้ควรควรตระหนักว่า flash ROM ที่มากับ esp32 หรือ esp8266 นั้นมีอายุการใช้งาน ปกติมักกำหนดไว้ประมาณ 1,000 รอบการลบ/เขียน (ต้องลบก่อนจึงเขียนทับได้) ดังนั้น ถ้ากังวลว่าการใช้ LittleFS ทำให้ Flash ROM หมดอายุเร็วขึ้น ให้มองกลับกันว่า โดยปกติ ทุกครั้งที่อัพโหลดโปรแกรมลงไมโครคอนโทรลเลอร์นั้นทำให้เกิดการลบและเขียนทับอยู่แล้ว ซึ่งตำแหน่งที่อยู่เกินกว่าโปรแกรมของเรากลับไม่ได้ถูกใช้งาน และถ้ามองว่า แต่ละวันเขียน 1 โปรแกรม ย่อมหมายถึงรอมหมดอายุ 1 ครั้งต่อวัน ทำให้เขียนโปรแกรมได้ถึง 1000 โปรแกรม (โดยประมาณ) หรือต้องใช้เวลาถึง 3 ปีกว่าชิพตัวนั้นจะหมดอายุงาน และต่อให้หมดอายุงานแล้ว นั่นไม่เกี่ยวกับการเสียของชิพ เมื่อโปรแกรมยังอยู่ และชิพยังไม่เสียหาย โปรแกรมในชิพยังคงใช้งานได้อยู่ ถ้าผู้อ่านกังวลว่าถ้าเขียนลบบ่อย ๆ แล้วเสียหายจะทำอย่างไรกับข้อมูลที่เสียหาย อันนี้ต้องขึ้นอยู่กับการออกแบบระบบของผู้อ่านว่า ข้อมูลที่เขียนนั้นคืออะไร เป็นข้อมูลถาวรหรือเป็นค่าที่ถูกใช้ระยะยาว ๆ หรือไม่ ถ้าใช่และทำให้มีอายุเพียงพอต่อการทำงานในระยะรับประกันที่เราให้กับลูกค้าไว้ ก็ให้เก็บลงในรอม (แต่อย่าลืมทำระบบสำรองข้อมูลออกมาด้วย) และถ้าอะไรที่เปลี่ยนบ่อยก็ควรนำไปเก็บในสื่อระบบอื่น ๆ เช่น โยนเข้าคลาวด์ หรือใช้ SD-Card (ซึ่งอายุยาวกว่า) เป็นต้น
สุดท้ายขอให้มีความสุขกับการเขียนโปรแกรมครับ
แหล่งอ้างอิง
- github : LittleFS
- lorol/LITTLEFS
- lorol/arduino-esp32fs-plugin
- Random Nerd Tutorials: Install ESP8266 NodeMCU LittleFS Filesystem Uploader in Arduino IDE
(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-11-01, 2022-01-02