MTD Subsystem: Hiểu sâu từ silicon đến kernel
Nếu bạn từng làm việc với embedded Linux, chắc hẳn bạn đã gặp những con số khó hiểu như /dev/mtd0, UBIFS, hay câu lệnh flash_erase đầy bí ẩn. Chúng xuất hiện trong bootloader, trong script flash firmware, trong log kernel, nhưng ít ai giải thích rõ tại sao chúng tồn tại. Đây là hành trình từ tấm bán dẫn (silicon) lên đến kernel Linux, qua con mắt của MTD, Memory Technology Devices subsystem.
1. Flash khác ổ cứng như thế nào?
Cái câu chuyện về công tắc điện
Hãy tưởng tượng một công tắc điện trong nhà bạn. Công tắc có hai trạng thái: bật (1) và tắt (0). Cũng dễ dàng, bạn chỉ cần tay gạt sang trái hay phải. Ổ cứng đĩa từ (HDD) cũng vậy, ghi dữ liệu là thay đổi từ trường trên một vùng vật chất nhỏ (grain) trên mặt đĩa. Ghi lên 1 hay 0 đều như nhau, không có sự khác biệt về thời gian hay năng lượng.
Nhưng Flash memory không dùng từ trường. Nó dùng electron, những hạt vật chất siêu nhỏ mang điện tích âm, để lưu trạng thái.
Floating-Gate Transistor: cái bẫy electron
Mỗi bit trong Flash được lưu trong một floating-gate transistor (FG transistor). Hình dung nó như một con đường ngầm có ba lớp: dây dẫn ở trên (control gate), một lớp cách ly bằng silicon oxide dày ~10nm ở giữa (floating gate, "cái bẫy"), và dây dẫn ở dưới (channel).
Khi program (ghi dữ liệu 1 -> 0), ta đặt điện áp rất cao (~15-20V) lên control gate. Điện áp này khiến electron trong channel năng lượng cao đến mức chúng có thể "xuyên qua" lớp oxide mỏng manh, hiện tượng gọi là Fowler-Nordheim tunneling. Electron bị bẫy vào floating gate và mắc kẹt ở đó vì lớp oxide không dẫn điện. Bây giờ, ngay cả khi ta rút điện, electron vẫn ở trong FG. Nhiều electron trong FG tạo ra điện trường làm "tắt" transistor, đây là bit 0.
Khi erase (xóa, quay về 1), ta đặt điện áp ngược lên substrate (nền). Điện áp này đẩy electron ra khỏi floating gate, quay trở lại channel. Transistor trở lại trạng thái "mở", đây là bit 1.
Tại sao phải erase trước khi write?
Đây là điều làm người mới hay nhầm lẫn: Flash chỉ có thể ghi 1 -> 0, không thể ghi 0 -> 1 mà không erase. Không phải vì người thiết kế phần mềm lựa chọn như vậy, mà vì vật lý không cho phép. Để ghi 0 -> 1, bạn bắt buộc phải đẩy electron ra khỏi FG. Và thao tác này chỉ có thể làm được bằng erase với điện áp cao trên toàn bộ block.
Một cách khác để hiểu: bạn có một tờ giấy và một cây bút chì. Bút chì chỉ viết được lên giấy (ghi 1 -> 0). Để xóa, bạn phải dùng cục tẩy (erase block) để tẩy đi toàn bộ trang giấy, rồi viết lại. Bạn không thể "tẩy đi một chữ cũ" mà không ảnh hưởng đến xung quanh.
Erase chậm, program nhanh: tại sao?
Erase mất 0.5-3.5 mili-giây, trong khi program chỉ mất 20-200 micro-giây, chênh lệch gần 10-100 lần. Tại sao?
- Program: Chỉ cần "bắn" electron vào một floating gate duy nhất. Như bạn rót một giọt nước vào ly. Nhanh, chi tiết.
- Erase: Phải đẩy electron ra khỏi tất cả FG trong một block (128KB - 8MB) đồng thời. Tất cả transistor trong block chia sẻ chung substrate, nên bạn không thể chọn lọc. Như bạn phải đổ sạch nước ra khỏi cả bể bơi thay vì chỉ một ly. Đồng thời, erase còn phải đảm bảo electron được đẩy ra hoàn toàn, nếu còn sót electron, transistor không mở hoàn toàn, bit 1 sẽ không rõ ràng.
Đây là constraint vật lý cơ bản của Flash. Không có công nghệ nào khắc phục được nó, chỉ có thể làm cho nhanh hơn một chút bằng vật liệu mới.
P/E cycles: tuổi thọ có hạn
Mỗi lần erase và program, electron xuyên qua lớp oxide sẽ làm hao mòn lớp này. Oxide mỏng dần, đến một ngày nó không còn cách ly đủ tốt nữa. Số lần erase/program tối đa gọi là P/E cycles.
- SLC (1 bit/cell, 2 mức điện áp): 50,000 - 100,000 P/E. Chỉ cần phân biệt "có electron" hay "không", khoảng cách lớn, dễ chịu lỗi.
- MLC (2 bit/cell, 4 mức điện áp): 3,000 - 10,000 P/E. Phải phân biệt 4 mức điện tích electron khác nhau. Càng nhiều mức, khoảng cách giữa chúng càng gần, càng dễ bị nhiễu.
- TLC (3 bit/cell, 8 mức điện áp): 500 - 3,000 P/E.
- QLC (4 bit/cell, 16 mức điện áp): 100 - 1,000 P/E.
Vì sao nhiều bit/cell lại ít P/E hơn? Tưởng tượng bạn đang đổ nước vào cốc có vạch chia. SLC chỉ có 2 vạch: có nước và không nước, dễ đoán. QLC có 16 vạch, một chút rung lắc, nước lệch một chút là bạn đọc nhầm vạch. Trong Flash, "rung lắc" chính là nhiễu điện tử nhiệt độ, suy giảm oxide, hay interference từ cell lân cận.
2. NOR vs NAND: Chọn loại nào?
Từ cái tên đến cấu trúc silicon
Tên gọi NOR và NAND bắt nguồn từ logic gates. Trong một NOR gate, đầu ra chỉ là 1 khi tất cả đầu vào là 0. Trong mạch Flash, điều này có nghĩa là các cell được nối song song với nhau, mỗi cell có drain riêng nối với bit line. Tương tự, NAND gate chỉ ra 0 khi tất cả đầu vào là 1, nên các cell trong NAND Flash được nối nối tiếp thành chuỗi.
Sự khác biệt về cách nối dây này quyết định mọi thứ khác.
NOR Flash: như thư viện mở khóa
NOR Flash có mỗi cell nối song song với bit line riêng. Điều này cho phép CPU đọc bất kỳ cell nào mà không cần đọc qua các cell khác, gọi là random access. Bạn có thể đọc từng byte như đọc RAM. Điều này cực kỳ quan trọng vì nó cho phép XIP (eXecute-In-Place): CPU có thể chạy code trực tiếp từ NOR Flash mà không cần copy vào RAM.
Tuy nhiên, mỗi cell cần một contact riêng nối với bit line. Trên silicon, mỗi contact chiếm diện tích đáng kể. Kết quả: NOR có mật độ thấp, giá thành $/MB cao gấp 4-8 lần NAND.
NAND Flash: như đường ray đơn
NAND Flash nối các cell thành chuỗi 32-128 cell (NAND string). Cả chuỗi chỉ cần 2 contact ở hai đầu. Mật độ cao, giá rẻ. Nhưng để đọc một cell, bạn phải bật cả chuỗi, không thể truy cập ngẫu nhiên từng byte được. Bạn phải đọc theo page (2KB-16KB) hoặc block (128KB-8MB).
Hơn nữa, vì chuỗi dài, chỉ cần một cell lỗi trong chuỗi là ảnh hưởng đến cả page. Điều này dẫn đến hiện tượng bad blocks, tới 2% block có thể không tốt ngay từ nhà máy. Nhà sản xuất không vứt bỏ chip vì vài block lỗi; họ chỉ đánh dấu bad và dùng block dự phòng (spare blocks).
XIP và bootloader: tại sao NOR vẫn sống?
Khi hệ thống bật lên, CPU cần chạy code bootloader để khởi động. Lúc này, RAM có thể chưa sẵn sàng (cần DRAM controller init), và hệ thống cần ít nhất vài MB để chạy code. NOR Flash cho phép CPU đọc lệnh trực tiếp như đọc từ RAM, không cần copy, không cần driver phức tạp. Đây là lý do NOR vẫn tồn tại trong embedded dù đắt đỏ.
Với NAND, bootloader phải được copy vào RAM trước khi chạy. Điều này yêu cầu một chuỗi init phức tạp hơn: CPU phải có sẵn một NAND controller driver nhỏ trong ROM để đọc bootloader từ NAND vào RAM, rồi mới nhảy đến RAM thực thi.
| NOR Flash | NAND Flash | |
|---|---|---|
| Đọc | Nhanh, ngẫu nhiên | Chậm ngẫu nhiên, nhanh tuần tự |
| Ghi/Xóa | Chậm | Nhanh |
| Giá | Đắt ($/MB cao) | Rẻ ($/MB thấp) |
| Độ tin cậy | 100% good bits | Có bad blocks từ nhà máy |
| XIP | Có - chạy code trực tiếp | Không - phải copy ra RAM |
| ECC | Không cần | Bắt buộc |
| Dùng cho | Bootloader <32MB | Rootfs, storage >128MB |
NAND Flash sản xuất trên wafer silicon với quy trình nanometer. Một chuỗi 128 cell nối tiếp chỉ cần một cell có lỗi (oxide không đều, kim loại bẩn) là cả page trở nên không dùng được. Thay vì vứt bỏ cả chip chỉ vì một cell, nhà sản xuất đánh dấu block đó là "bad" và cung cấp thêm block dự phòng (spare). Điều này khiến yield sản xuất tăng đáng kể và giá thành giảm. Hệ điều hành phải quản lý bad blocks, đó chính là một trong những lý do MTD tồn tại.
3. MTD là gì? Tại sao cần nó?
Vấn đề: Flash không phải block device
Linux kernel có hai loại device chính: character device (đọc/ghi từng byte, không buffering) và block device (đọc/ghi theo block, có buffering, cache). Ổ cứng HDD và SSD đều là block device. Nhưng raw Flash thì không.
Raw Flash có những đặc tính không có ở HDD:
- Erase-before-write: không thể ghi đè lên dữ liệu cũ mà không xóa cả block
- Bad blocks: có thể xuất hiện bất kỳ lúc nào, không được báo trước
- Wear leveling: mỗi block chỉ chịu được số lần erase hữu hạn
- Bit flips: đọc nhiều lần có thể làm thay đổi giá trị bit (đọc ra 0 thay vì 1)
- OOB (Out-Of-Band): mỗi page có thêm một vùng nhớ nhỏ để lưu ECC, bad block marker
Nếu bạn dùng hệ thống file ext4 trên raw Flash, sau một thời gian sẽ hỏng vì ext4 không biết xử lý bad blocks, không wear-leveling, và không xóa block trước khi ghi.
MTD: lớp dịch chuyển giữa VFS và phần cứng
MTD (Memory Technology Devices) là subsystem trong Linux kernel cung cấp abstraction cho các bộ nhớ non-volatile (không bay hơi, giữ dữ liệu khi mất điện) như NOR, NAND, OneNAND, SPI-NOR. MTD nằm giữa hệ thống file (VFS) và driver phần cứng, đóng vai trò "phiên dịch" giữa hai thế giới không tương thích.
Ba lớp trong MTD stack
Lớp 1: MTD Core, trái tim của hệ thống
Core định nghĩa struct mtd_info, đây là cấu trúc dữ liệu mô tả mọi thứ bạn cần biết về một chip Flash:
struct mtd_info {
uint8_t type; // MTD_NORFLASH hay MTD_NANDFLASH
uint64_t size; // Tổng dung lượng (bytes)
uint32_t erasesize; // Kích thước block có thể xóa
uint32_t writesize; // Kích thước page (page size)
uint32_t oobsize; // Kích thước vùng OOB mỗi page
uint16_t ecc_strength; // Sức mạnh ECC (số bit lỗi có thể sửa)
// ... các trường khác
};
Core cũng tạo ra hai loại device node:
/dev/mtdX(major 90): Character device cho raw access, bao gồm cả OOB data. Đây là cách user-space "nói chuyện" trực tiếp với chip./dev/mtdblockX(major 31): Block device nhưng chỉ read-only. Không nên dùng để ghi vì không có wear leveling, bạn sẽ làm hỏng Flash nhanh chóng.
Lớp 2: Chip Drivers, hiểu phần cứng cụ thể
Đây là các driver biết cách "nói chuyện" với từng loại chip:
nand_base.c: Generic NAND controller, xử lý các thao tác cơ bản đọc/ghi/xóa NANDcfi_cmdset_0001.c: Hiểu Intel/Sharp Common Flash Interface, chuẩn giao tiếp cho NORm25p80.c: SPI NOR Flash (Macronix, Winbond, Spansion...) giao tiếp qua SPI bus
Lớp 3: Board Drivers, nối với phần cứng trên board
Board driver kết nối chip driver với phần cứng cụ thể. Nó đọc Device Tree (ví dụ jedec,spi-nor hoặc nand-controller) để biết chip gắn vào bus nào, GPIO nào, và ioremap để ánh xạ vùng nhớ phần cứng vào không gian địa chỉ của kernel.
eMMC và SD card có FTL (Flash Translation Layer) tích hợp sẵn trong chip controller. FTL này biến đổi raw Flash thành một block device "giống HDD" hoàn hảo, tự wear leveling, tự bad block management, tự ECC. Hệ điều hành thấy eMMC như ổ cứng thông thường, dùng ext4 hay FAT32 thoải mái. MTD chỉ xử lý raw Flash không có FTL.
4. Chia vùng (Partitioning)
Flash không có partition table
Ổ cứng HDD dùng partition table (MBR/GPT) để định nghĩa các vùng. Flash raw không có khái niệm này. Thay vào đó, các partition được định nghĩa ngoài chip, trong bootloader hay Device Tree, và kernel chỉ cần biết "offset X đến offset Y là partition Z".
Ba cách định nghĩa partition
Cách 1: RedBoot FIS (Flash Image System)
RedBoot là bootloader phổ biến thời kỳ đầu embedded Linux. Nó lưu một "partition table" đặc biệt ở cuối Flash, gọi là FIS directory. Khi boot, RedBoot đọc directory này để biết các firmware image nằm ở đâu. Đây là cách cổ xưa, ít dùng ngày nay nhưng vẫn gặp trong các hệ thống legacy.
Cách 2: mtdparts command line
Truyền partition map qua kernel command line:
mtdparts=nand0:512k(bootloader),128k(params),4m(kernel),-(rootfs)
Cách này tiện cho việc thử nghiệm nhanh: bạn không cần recompile Device Tree. Tuy nhiên, partition nằm trong bootloader config, không phải trong source tree chuẩn.
Cách 3: Device Tree (phương pháp hiện đại)
Đây là cách chuẩn cho ARM/ARM64 embedded systems:
&nand_controller {
nand@0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "bootloader";
reg = <0x0000000 0x080000>; // Offset 0, 512KB
read-only; // Không cho ghi đè
};
partition@80000 {
label = "kernel";
reg = <0x080000 0x400000>; // Offset 512KB, 4MB
};
partition@480000 {
label = "rootfs";
reg = <0x480000 0x7B80000>; // Còn lại
};
};
};
};
Mỗi partition được định nghĩa bằng cặp (offset, size). Flag read-only ngăn kernel vô tình ghi đè lên bootloader. Khi kernel boot, MTD parser sẽ đọc các node này và tạo ra các /dev/mtd0, /dev/mtd1, ... tương ứng.
MBR/GPT thiết kế cho block device có thể ghi bất kỳ sector nào bất kỳ lúc nào. Flash không thể, bạn phải erase cả block (128KB+) trước khi ghi. Partition table trên Flash sẽ tồn tại ở một vùng cố định, và mỗi lần cập nhật partition table bạn phải erase cả block chứa nó, có thể vô tình xóa dữ liệu quan trọng gần đó. Thay vào đó, Flash dùng "static" partition được định nghĩa compile-time trong Device Tree.
5. Công cụ cần biết
Làm việc với Flash không giống làm việc với file thông thường. Bạn không thể dùng cp hay dd một cách vô tư vì chúng không hiểu bad blocks, OOB, hay ECC.
flash_erase: "dọn dẹp sân khấu"
Trước khi ghi dữ liệu mới, bạn phải xóa (erase) partition. Lệnh cơ bản:
# flash_erase /dev/mtd0 0 4
# | | |
# | | +-- Số block cần xóa (4 block)
# | +---- Offset bắt đầu (0 = từ đầu)
# +---------------- MTD device
Nếu dùng JFFS2, thêm flag -j để ghi JFFS2 cleanmarker vào OOB sau khi erase, giúp JFFS2 nhận ra block đã được "dọn dẹp":
flash_erase -j /dev/mtd2 0 0 # 0 cuối = xóa toàn bộ partition
nandwrite: "thợ xây dựng"
Ghi file image lên NAND Flash. Khác với dd, nandwrite biết:
- Kiểm tra bad blocks và tự động bỏ qua
- Ghi đúng vào OOB (nếu cần)
- Tuân thủ writesize của chip
# nandwrite -p /dev/mtd2 rootfs.ubi
# |
# +-- Pad dữ liệu để đủ page size (không pad sẽ lỗi)
nanddump: "thu thập bằng chứng"
Đọc nội dung NAND ra file. Tùy chọn quan trọng:
# nanddump -o -f backup.bin /dev/mtd2
# |
# +-- Đọc cả OOB data (quan trọng cho debug)
Flag -o đọc cả OOB, cực kỳ hữu ích khi bạn đang debug vấn đề ECC hay tìm hiểu bad block marker.
flashcp: "ghi một chạm"
Kết hợp erase + write trong một lệnh. Tiện cho các file nhỏ:
flashcp firmware.bin /dev/mtd1
# Tự động: erase partition -> ghi file -> verify
mtdinfo: "khám bệnh trước khi chữa"
Luôn chạy trước khi làm bất cứ thao tác ghi nào:
mtdinfo /dev/mtd2
# Hiển thị: type, eraseblock size, amount of eraseblocks
# sub-page size, OOB size, ECC strength
| Công cụ | Chức năng | Cảnh báo |
|---|---|---|
flash_erase | Xóa MTD partition | -j để thêm JFFS2 marker |
nandwrite | Ghi file lên NAND | Dùng thay vì dd |
nanddump | Đọc NAND ra file | -o để đọc cả OOB |
flashcp | Copy file (xóa+ghi 1 lệnh) | Tiện cho file nhỏ |
mtdinfo | Xem thông tin MTD | Kiểm tra trước khi ghi |
Lệnh dd đọc/ghi raw bytes mà không hiểu cấu trúc NAND. Khi gặp bad block, dd vẫn cố ghi lên đó, khiến dữ liệu bị mất. dd không biết về OOB, không kiểm tra ECC, không bỏ qua bad blocks. Chỉ dùng dd với NOR Flash hoặc khi bạn thực sự hiểu rõ mình đang làm gì.
6. Filesystem: JFFS2 vs UBIFS
Tại sao Flash cần filesystem đặc biệt?
Bạn không thể dùng ext4 hay FAT32 trên raw Flash vì ba lý do cơ bản:
- Không có wear leveling: ext4 ghi vào cùng "inode table" và "superblock" mỗi lần cập nhật. Trên Flash, vùng này sẽ bị erase hàng trăm lần mỗi ngày trong khi phần còn lại còn mới tinh.
- Không xử lý bad blocks: ext4 không biết làm gì khi đọc ra dữ liệu lỗi. NAND có thể có bit flip hoặc bad block bất kỳ lúc nào.
- Erase-before-write: ext4 ghi đè lên cùng block. Flash không thể ghi đè mà không erase trước.
Flash filesystem phải tự quản lý: wear leveling, bad block handling, erase-before-write transparently.
JFFS2: người tiên phong
JFFS2 (Journalling Flash File System v2) là filesystem đầu tiên hoạt động tốt trên Flash. Nó sử dụng kiến trúc log-structured: mỗi thao tác (ghi file, xóa file, rename) được ghi như một "node" mới vào cuối log. Không bao giờ ghi đè lên node cũ, chỉ thêm node mới. Điều này tự nhiên tránh vấn đề erase-before-write.
Tuy nhiên, JFFS2 có một vấn đề nghiêm trọng: mount time. Khi mount, JFFS2 phải quét toàn bộ Flash để đọc tất cả nodes và xây dựng lại danh sách inode. Thời gian này tỷ lệ thuận với dung lượng Flash.
Với một NAND 256MB đã dùng 75%, JFFS2 có thể mất ~180 giây (3 phút) để mount. Trên hệ thống embedded cần boot nhanh, điều này không chấp nhận được.
JFFS2 cũng không hỗ trợ compression (nén) hiệu quả và bị phân mảnh (fragmentation) nặng khi Flash gần đầy.
UBI: lớp trung gian thông minh
UBI (Unsorted Block Images) không phải filesystem, nó là một volume management layer nằm giữa MTD và filesystem. UBI tách riêng vấn đề quản lý Flash ra khỏi filesystem, theo nguyên lý decoupling quan trọng trong kỹ thuật phần mềm.
UBI làm những việc này:
-
Wear leveling: UBI theo dõi số lần erase mỗi block (lưu trong EC header). Khi cần ghi, UBI chọn block có erase count thấp nhất. Khi chênh lệch giữa block cao nhất và thấp nhất vượt ngưỡng (thường 4096 cycles), UBI sẽ "đổi chỗ" dữ liệu để cân bằng.
-
Bad block management: UBI tự động đánh dấu và bỏ qua bad blocks. Bạn chỉ thấy một "volume" liên tục, không lo lắng về bad blocks.
-
Atomic updates: UBI đảm bảo ghi hoặc hoàn toàn thành công, hoặc hoàn toàn thất bại, không có trạng thái "giữa chừng".
Mỗi erase block trong UBI có hai header nhỏ:
- EC header (Erase Counter): lưu số lần block đã bị erase
- VID header (Volume ID): lưu thông tin volume mà block thuộc về
UBIFS: filesystem hiện đại
UBIFS là filesystem chạy trên UBI (không chạy trực tiếp trên MTD). UBIFS sử dụng B+ tree để tổ chức dữ liệu:
- Mount O(1): B+ tree đã được lưu sẵn trên Flash. Mount chỉ cần đọc root node, không cần quét toàn bộ chip.
- Write-back cache: UBIFS gom các writes vào buffer, rồi flush xuống Flash một lúc. Giảm số lần ghi, tăng tốc độ.
- Compression: Tự động nén dữ liệu bằng LZO hoặc Zstd trước khi ghi.
- Wandering tree: Mỗi lần commit, UBIFS ghi node mới thay vì ghi đè lên node cũ. Journal nằm ở đầu volume.
So sánh thực tế
| Tiêu chí | JFFS2 | UBIFS | |----------|-------|-------| | Mount time 256MB | ~180 giây | ~0.5 giây | | Wear leveling | Có (đơn giản) | Có (tinh vi) | | Compression | Không | LZO/Zstd | | Write performance | Chậm | Nhanh (write-back) | | Max volume size | ~128MB | Hàng TB | | Cần UBI? | Không | Có |
Nguyên lý thiết kế quan trọng: "separation of concerns." Wear leveling và bad block management là vấn đề của Flash hardware, không phải vấn đề của filesystem. Bằng cách tách UBI thành lớp riêng, nhiều filesystem khác nhau (UBIFS, SquashFS, v.v.) có thể dùng chung một cơ chế quản lý Flash. Điều này giống như LVM (Logical Volume Manager) cho ổ cứng, LVM quản lý địa chỉ vật lý, ext4 chỉ quản lý file.
7. Quy trình end-to-end: từ image đến mount
Đây là 5 bước để tạo và mount một UBIFS volume trên NAND Flash. Mỗi bước đều có ý nghĩa riêng và không thể bỏ qua.
Bước 1: mkfs.ubifs: "tạo filesystem"
Tạo một UBIFS image từ một thư mục trên PC:
mkfs.ubifs -r ./rootfs -o rootfs.ubifs -m 2048 -e 126976 -c 1000
# | | | | | |
# | | | | | +-- Số LEB tối đa (max count)
# | | | | +----------- Erase block size minus 2xUBI header
# | | | +------------------ NAND page size (min I/O unit)
# | | +--------------------------------- Output file
# | +----------------------------------- Source directory
# +---------------------------------------------- Root directory
-r ./rootfs: Thư mục nguồn chứa các file cần đưa vào image-o rootfs.ubifs: File image đầu ra-m 2048: Kích thước page NAND (minimum I/O unit). Giống như kích thước tờ giấy nhỏ nhất bạn có thể viết.-e 126976: Kích thước logical erase block. UBI dùng 2 page đầu mỗi block cho EC và VID header, nên còn lại 128KB - 2*2048 = 126976 bytes cho dữ liệu.-c 1000: Số LEB (logical erase blocks) tối đa. UBIFS cần biết giới hạn này để cấp phát không gian. Tính bằngvolume_size / eraseblock_size.
Kiểm tra:
echo $? # Phải ra 0. Nếu khác 0, image không dùng được.
Bước 2: ubinize: "đóng gói cho UBI"
UBI không đọc UBIFS trực tiếp, nó cần một UBI image có metadata. ubinize đóng gói UBIFS vào định dạng UBI:
ubinize -o rootfs.ubi -p 131072 -m 2048 -s 2048 ubinize.cfg
# | | | |
# | | | +-- Sub-page size (cho partial programming)
# | | +--------- NAND page size
# | +------------------- Physical erase block size (PEB)
# +--------------------------------- Output UBI image
File ubinize.cfg:
[ubifs]
mode=ubi
image=rootfs.ubifs
vol_id=0
vol_size=120MiB
vol_type=dynamic
vol_name=rootfs
vol_flags=autoresize
-p 131072: Physical erase block size (128KB). Đây là block thực sự mà NAND có thể erase.-s 2048: Sub-page size. Cho phép UBI ghi header vào dưới một page (tiết kiệm không gian).vol_type=dynamic: Volume có thể đọc/ghi (không phải read-only).vol_flags=autoresize: Tự động mở rộng volume khi gần đầy.
Kiểm tra:
echo $? # Phải ra 0
Bước 3: ubiformat: "ghi xuống NAND"
Bây giờ ghi UBI image xuống Flash. KHÔNG dùng dd hay nandwrite thông thường, ubiformat đảm bảo UBI headers được ghi đúng:
ubiformat /dev/mtd2 -s 2048 -f rootfs.ubi
# | | |
# | | +-- File image để ghi
# | +--------- Sub-page size
# +--------------------- MTD device (partition rootfs)
ubiformat sẽ tự động:
- Erase tất cả block trên
/dev/mtd2 - Bỏ qua bad blocks
- Ghi EC header vào mỗi block
- Ghi UBI image vào các block còn lại
Kiểm tra:
echo $? # Phải ra 0
Bước 4: ubiattach: "kết nối UBI với kernel"
Gắn MTD partition vào UBI subsystem của kernel:
ubiattach /dev/ubi_ctrl -m 2
# | |
# | +-- MTD device number (mtd2)
# +------------------ UBI control device
Sau lệnh này, kernel sẽ tạo ra /dev/ubi0 và /dev/ubi0_0 (volume đầu tiên). UBI đọc các VID headers để biết có bao nhiêu volume.
Kiểm tra:
echo $? # Phải ra 0
Bước 5: mount: "sử dụng"
mount -t ubifs /dev/ubi0_0 /mnt/rootfs
# | | |
# | | +-- Mount point
# | +------------- UBI volume device
# +------------------------ Filesystem type
Kiểm tra:
echo $? # Phải ra 0
Để tự động mount sau mỗi boot, thêm vào /etc/fstab:
/dev/ubi0_0 /mnt/rootfs ubifs defaults,noatime 0 0
Flag noatime vô hiệu hóa việc ghi timestamp mỗi lần đọc file, giảm đáng kể số lần ghi lên Flash.
Mỗi bước xử lý một lớp abstraction khác nhau: mkfs.ubifs tạo filesystem (như ext4), ubinize đóng gói vào UBI volume format (như LVM), ubiformat ghi xuống raw Flash (như dd nhưng Flash-aware), ubiattach kết nối với kernel, và mount đăng ký với VFS. Sự tách biệt này cho phép linh hoạt: bạn có thể tạo image trên PC, rồi chỉ cần 2 lệnh cuối trên device.
8. 2026 Reality Check: Flash còn dùng được không?
Thị trường năm 2026
Đến năm 2026, landscape lưu trữ đã thay đổi nhiều. Nhưng raw Flash (NOR/NAND không có FTL) không biến mất, nó chỉ chuyển sang các phân khúc chuyên dụng.
| Phân khúc | Giải pháp | Filesystem |
|---|---|---|
| Smartphone/Tablet | eMMC/UFS | ext4/F2FS |
| Automotive ECU | Raw NAND | UBIFS |
| Industrial IoT | Raw NAND | UBIFS |
| Bootloader | NOR/NAND | JFFS2/SquashFS |
Raw NAND còn ở đâu?
Automotive ECU: Xe hơi hiện đại có 50-100 ECU (Electronic Control Units). Mỗi ECU chạy một hệ điều hành nhỏ, cần lưu trữ bền bỉ trong môi trường nhiệt độ cực đoan (-40 độ C đến 125 độ C). eMMC không đủ bền trong những môi trường này, controller eMMC có thể chết trước chip Flash. Raw NAND với UBI cho phép hệ thống tự quản lý và có thể "thô" hơn.
Industrial IoT: Các thiết bị công nghiệp cần vòng đời 10-15 năm. eMMC không có datasheet đầy đủ cho vòng đời dài, controller là "hộp đen." Raw NAND cho phép kiểm soát hoàn toàn wear leveling và dự đoán tuổi thọ.
Chi phí: Với dung lượng <1GB, raw NAND rẻ hơn eMMC vì không cần controller chip. Trong sản xuất hàng triệu đơn vị, mỗi xu tiết kiệm đều quan trọng.
Quy tắc quyết định thực tế
| Câu hỏi | Lựa chọn |
|---------|----------|
| Cần boot trực tiếp không copy RAM? | SPI-NOR + XIP |
| Storage 128MB-8GB, cần bền bỉ? | Raw NAND + UBIFS |
| Storage >8GB, consumer device? | eMMC/UFS |
| Cần FS có thể mount <1 giây? | UBIFS (không phải JFFS2) |
| Firmware update OTA? | UBI volume update (atomic) |
| Giám sát sức khỏe Flash? | mtdinfo + UBI stats |
Lời cuối
MTD subsystem có vẻ cũ kỹ trong thời eMMC và SSD, nhưng nó là một ví dụ tuyệt vời về cách Linux kernel thiết kế abstraction layers: tách biệt concerns, hỗ trợ legacy, và mở rộng cho tương lai. Hiểu MTD không chỉ giúp bạn làm việc với embedded Flash, nó dạy bạn cách nghĩ về hardware abstraction nói chung. Và khi bạn gặp một hệ thống industrial đang chạy UBIFS trên NAND 10 năm tuổi, bạn sẽ biết chính xác chuyện gì đang xảy ra bên dưới lớp vỏ kim loại.