[TH] LittleFS Filesystem

บทความกล่าวถึงการใช้ไลบรารี LittleFS และแนะนำไลบรารีที่ถูกพัฒนาเพื่อใช้กับไมโครคอนโทรเลอร์ esp32 ซึ่งมีส่วนเสริม (plugin) ของ Arduino IDE สำหรับอัพโหลดไฟล์ไปเก็บในรอมของไมโครคอนโทรลเลอร์ ทำให้สะดวกต่อการโหลดข้อมูลไปเก็บและเรียกใช้งาน ด้วยเหตุนี้ถ้าผู้เขียนโปรแกรมรู้สึกยุ่งยากกับการแปลงโค้ด HTML/CSS/JavaScript ให้เป็นสตริงด้วยตนเอง และเปลี่ยนมาเป็นอัพโหลดไฟล์ไปเก็บใน esp32 แล้วอ่านไฟล์เว็บมาใช้งานโดยตรงจะเป็นสิ่งที่จะต้องฝึกฝนใช้งานเจ้า LittleFS ไว้เป็นไลบรารีคู่ใจกันเลยทีเดียว


LittleFS

LittleFS เป็นระบบไฟล์ (filesystem) ขนาดเล็กที่ออกแบบมาเพื่อใช้งานกับไมโครคอนโทรลเลอร์ ภายใต้สิทธิการใช้งานแบบ BSD-3-Clauseโดยผู้ใช้งานสามารถเขียน แก้ไข ปิด และลบไฟล์ได้ ซึ่งจุดเด่นของ LittleFS ได้แก่

  • หากไฟดับระบบไฟล์จะถอยกลับไปสู่สถานะใช้งานก่อนหน้า
  • ออกแบบให้รู้จักหลบเลี่ยงบล็อกของแฟลชที่เสียหายได้

ขั้นตอนการใช้งานระบบไฟล์ของ LittleFS เป็นดังนี้

  1. ตั้งค่าระบบไฟล์
  2. ทำการ mount()
  3. ถ้า mount() ไม่ได้แสดงว่ายังไม่มีระบบไฟล์ติดตั้งอยู่ ให้เรียก format()
  4. ใช้งานไดเรกทอรีและไฟล์
  5. ทำการ 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

ภาพที่ 1 เมนู Manage Libraries…
ภาพที่ 2 ไลบรารี LittleFS_esp32

ตัวอย่างโปรแกรมที่มากับไลบรารีมี 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 (ซึ่งอายุยาวกว่า) เป็นต้น

สุดท้ายขอให้มีความสุขกับการเขียนโปรแกรมครับ

แหล่งอ้างอิง

  1. github : LittleFS
  2. lorol/LITTLEFS
  3. lorol/arduino-esp32fs-plugin
  4. Random Nerd Tutorials: Install ESP8266 NodeMCU LittleFS Filesystem Uploader in Arduino IDE

(C) 2020-2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-11-01, 2022-01-02