A single flash chip on an embedded board almost never holds just one thing — it holds a bootloader, bootloader environment variables, a kernel image, and a root filesystem, all sharing the same physical NOR or NAND part. This free linux device drivers course lecture covers how Linux carves that one chip into named MTD partitions, the three ways to describe the layout, and how to inspect the result at runtime.
What You Will Learn
- Why flash is partitioned and what a typical embedded partition layout looks like
- The three ways to describe MTD partitions: kernel command line, device tree, and platform data
- Which method is the modern default and why the others still matter
- How to inspect partitions at runtime through
/proc/mtd, sysfs, andmtdinfo - A hands-on demo that partitions a RAM-backed MTD device and reads the layout back
Prerequisites
- The previous lecture in this series on MTD subsystem architecture (chip drivers, core, device nodes)
- Basic device tree familiarity is helpful for the device tree section
mtd-utilsinstalled on a Linux test machine or VM
Why Partition a Flash Chip
Even a modest board typically needs to keep several independent images on one chip: a first-stage loader, the main bootloader, the bootloader’s saved environment, a kernel image, and a root filesystem. Each of these has different requirements — the bootloader region usually needs write protection so a bad flash write can’t brick the board, while the root filesystem region needs to be as large as possible. MTD partitioning solves this by presenting one physical chip as several independent, named, sub-sized MTD devices, each with its own device nodes.
Notice two things every board layout you’ll meet does the same way: the bootloader-related partitions come first and are usually marked read-only, and the last partition is sized to consume whatever space is left rather than a fixed number.
Three Ways to Describe MTD Partitions
Linux has supported three different ways to tell the kernel where each partition starts and ends. All three still exist in the kernel today, but they are not equally recommended for new designs.
| Method | Where it lives | Still recommended for new boards? |
|---|---|---|
Kernel command line (mtdparts=) | Bootloader-passed kernel argument | Useful for quick lab experiments and recovery scenarios; not the primary mechanism today |
| Device tree | .dts / .dtsi files compiled into the DTB | Yes — the standard approach on virtually all current ARM/RISC-V embedded boards |
| Platform data | Board support C code in kernel sources | Legacy — only seen on very old non-device-tree boards; avoid for new work |
Method 1: Kernel Command Line
The bootloader can pass an mtdparts= argument on the kernel command line. Its grammar names the flash chip, then lists a comma-separated set of partitions, each with a size, an optional name, and optional flags:
mtdparts=<mtd-id>:<size>[@offset](name)[ro][lk][,...]
A concrete example for a single 128 MB chip split into five partitions:
mtdparts=spi0.0:512k(spl)ro,780k(uboot)ro,128k(env),4m(kernel),-(rootfs)
Here the ro suffix hardware-protects the bootloader-related partitions from the MTD layer, and the trailing dash means “consume all remaining space” for the root filesystem partition — you never have to compute the exact remainder by hand.
Method 2: Device Tree (the modern default)
On current kernels, the flash controller node in the device tree normally contains a partitions child node using the fixed-partitions compatible string, with one child per partition:
&spi_flash {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
partition@0 {
label = "spl";
reg = <0x000000 0x080000>;
read-only;
};
partition@80000 {
label = "uboot";
reg = <0x080000 0x0c3000>;
read-only;
};
partition@143000 {
label = "env";
reg = <0x143000 0x020000>;
};
partition@163000 {
label = "kernel";
reg = <0x163000 0x400000>;
};
partition@563000 {
label = "rootfs";
reg = <0x563000 0x7a9d000>;
};
};
};
This is the layout you should reach for on any new board today: it lives with the rest of the hardware description, survives kernel upgrades without touching board C code, and is what every mainline NAND/SPI-NOR controller driver expects.
Method 3: Platform Data (legacy)
Before device tree became standard, board files described partitions as a C array of struct mtd_partition, compiled directly into the board support package and passed to the chip driver at probe time. You’ll still encounter this pattern only on old, pre-device-tree board code; new designs should use device tree instead, since platform data requires a kernel rebuild for even a one-line layout change.
Inspecting Partitions At Runtime
Whichever method produced the layout, you inspect it the same three ways once the board is booted.
Quick Summary: /proc/mtd
cat /proc/mtd
Expected output:
dev: size erasesize name
mtd0: 00080000 00010000 "spl"
mtd1: 000c3000 00010000 "uboot"
mtd2: 00020000 00010000 "env"
mtd3: 00400000 00010000 "kernel"
mtd4: 07a9d000 00010000 "rootfs"
Detailed Per-Partition Info: mtdinfo
sudo mtdinfo /dev/mtd3
Expected output:
mtd3
Name: kernel
Type: nand
Eraseblock size: 65536 bytes, 64.0 KiB
Amount of eraseblocks: 64 (4194304 bytes, 4.0 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:6
Bad blocks are allowed: true
Device is writable: true
Raw sysfs Attributes
ls /sys/class/mtd/mtd3/
cat /sys/class/mtd/mtd3/size
cat /sys/class/mtd/mtd3/erasesize
Hands-On: Partitioning a RAM-Backed MTD Device
You can practice the whole workflow without real flash by combining mtdram from the previous lecture with the kernel’s runtime partition-creation ioctl through mtd-utils‘ mtdpart helper — or, more portably across kernel versions, by loading a small original demo module, ep_mtd_part, that registers two fixed partitions over an existing MTD master device using the standard partition-parser API.
// ep_mtd_part.c
#include <linux/module.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/partitions.h>
static struct mtd_partition ep_parts[] = {
{ .name = "ep_boot", .offset = 0, .size = 512 * 1024 },
{ .name = "ep_data", .offset = 512*1024, .size = MTDPART_SIZ_FULL },
};
static struct mtd_info *ep_master;
static int __init ep_mtd_part_init(void)
{
ep_master = get_mtd_device(NULL, 0);
if (IS_ERR(ep_master)) {
pr_err("ep_mtd_part: no mtd0 found, load mtdram first\n");
return PTR_ERR(ep_master);
}
mtd_device_register(ep_master, ep_parts, ARRAY_SIZE(ep_parts));
pr_info("ep_mtd_part: registered %zu partitions over %s\n",
ARRAY_SIZE(ep_parts), ep_master->name);
return 0;
}
static void __exit ep_mtd_part_exit(void)
{
mtd_device_unregister(ep_master);
put_mtd_device(ep_master);
pr_info("ep_mtd_part: unregistered\n");
}
module_init(ep_mtd_part_init);
module_exit(ep_mtd_part_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala demo: register fixed MTD partitions at runtime");
sudo modprobe mtdram total_size=2048 erase_size=64
sudo insmod ep_mtd_part.ko
cat /proc/mtd
Expected output:
dev: size erasesize name
mtd0: 00200000 00010000 "mtdram test device"
mtd1: 00080000 00010000 "ep_boot"
mtd2: 00180000 00010000 "ep_data"
dmesg | tail -3
Expected dmesg output:
[ 241.220981] ep_mtd_part: registered 2 partitions over mtdram test device
[ 260.884512] ep_mtd_part: unregistered
This mirrors exactly what a real NAND or SPI-NOR controller driver does on boot: it registers one master MTD device, then hands a partition table (from device tree, command line, or platform data) to mtd_device_register(), which is what produces the extra mtd1/mtd2 entries you see above.
Common Mistakes and Troubleshooting
- Forgetting
read-onlyon bootloader partitions. Without it, a misbehaving user-space process can erase your bootloader region; always mark SPL/U-Boot-equivalent partitions read-only in device tree. - Hardcoding an exact size for the last partition. Use
MTDPART_SIZ_FULL(platform data) or simply omit an explicit size where the parser supports “remaining space,” so the layout tolerates chips of slightly different total capacity. - Mixing partition methods. If both a command-line
mtdparts=and a device-tree partition table are present, behavior depends on driver and kernel config precedence — pick one method per board and be consistent. - Off-by-one offsets. A partition’s
offsetmust exactly match the previous partition’soffset + size; a gap or overlap either wastes flash or corrupts an adjacent partition.
Best Practices
- Use device tree
fixed-partitionsfor any new board — it’s the mainline-supported, maintainable option. - Keep partition names stable across board revisions so init scripts and bootloader environment variables referencing them by name don’t break.
- Reserve the last partition for whatever needs the most flexible size (usually rootfs) using the “remaining space” convention.
Performance Considerations
Partition boundaries should align to erase-block boundaries. A partition that starts or ends mid-erase-block forces the filesystem or flashing tool to handle a partially usable block, which wastes space and can slow down wear-leveling filesystems like UBIFS.
Security Considerations
The read-only flag on a device-tree partition is enforced by the MTD core, not just a userspace convention — it’s your first line of defense against accidental bootloader corruption from a compromised or buggy userspace process, and it’s cheap to add, so use it on every partition that shouldn’t change after manufacturing.
Summary and Key Takeaways
- Flash chips are split into named MTD partitions so bootloader, environment, kernel, and rootfs can coexist safely on one chip.
- Device tree’s
fixed-partitionsbinding is the modern, recommended way to describe the layout; command-line and platform-data methods still exist but are legacy or special-purpose. /proc/mtd,mtdinfo, and sysfs all let you verify the layout the kernel actually applied at runtime.- You can practice the whole registration flow on a desktop using
mtdramplus a small partition-registering module, no real flash required.
Conclusion
Partitioning is what turns one physical flash chip into a safe, structured home for a bootloader, its environment, a kernel, and a root filesystem. With the architecture from the previous lecture and the partitioning methods from this one, you now have the full picture of how Linux presents raw flash to the rest of the system — the foundation this free linux kernel development course builds on when it covers flash-aware filesystems like JFFS2 and UBIFS next.
FAQ
What is the recommended way to define MTD partitions on a new board?
Device tree, using the fixed-partitions compatible binding under the flash controller node.
Can I still use the mtdparts= kernel command line today?
Yes, it’s still supported and useful for lab/recovery scenarios, but it’s not the primary method for new board designs.
Why mark bootloader partitions read-only?
It’s enforced by the MTD core itself, protecting the bootloader region from accidental erasure or overwrite by userspace tools or buggy code.
How do I let the last partition use all remaining flash space?
Omit a fixed size for it in device tree, or use the dash notation on the command line, or MTDPART_SIZ_FULL in legacy platform data.
How can I check the actual partition layout the kernel applied?
Read /proc/mtd for a quick summary, or run mtdinfo /dev/mtdN for detailed per-partition information including eraseblock size and bad-block support.
What happens if command-line and device-tree partitions are both present?
Behavior depends on the specific driver and kernel configuration precedence, so it’s best practice to use only one partitioning method per board.
Continue the Free Embedded Linux Course
Next up: flash-aware filesystems — JFFS2 and UBIFS — built on top of the MTD partitions from this lecture.
Next Lecture Browse Full Course
2 Comments