If you’ve ever opened a .dts file and stared at a line like reg = <0x40000000 0x20000000>; and wondered what those two numbers actually mean, this lecture is for you. The device tree reg property is one of the most commonly used — and most commonly misunderstood — properties in the entire device tree. Getting it wrong is a classic beginner mistake in any free linux kernel development course, because a malformed reg property doesn’t always fail loudly; it silently gives your driver the wrong address range.
In this lecture, part of our free embedded linux course on bootloaders and firmware-to-kernel handoff, we’ll build up the reg property from first principles: what it represents, how #address-cells and #size-cells control its shape, and how to read (and write) it correctly for both 32-bit and 64-bit platforms.
What You Will Learn
- What a device tree
regproperty actually encodes - Why
regis a list of “cells” and not a fixed pair of numbers - How
#address-cellsand#size-cellsin a parent node change how children are parsed - How the CPU node’s
regdiffers from a memory-mapped device’sreg - How to read a
regproperty out of a running kernel usingof_nodehelpers - Common mistakes that produce silently-wrong addresses
Prerequisites
You should already be comfortable with basic device tree syntax — nodes, properties, and the compatible string — which we covered in the previous lecture, “Introducing Linux Device Trees.” You’ll also want a Linux machine with dtc (the device tree compiler) installed, and ideally access to a board or QEMU target where you can inspect /proc/device-tree or /sys/firmware/devicetree/base.
The reg Property: A List of Address/Size Pairs
The reg property describes one or more ranges of addresses that belong to a device. For a memory-mapped peripheral, that’s the physical address range the CPU uses to talk to the hardware register block. For the memory node itself, it’s the physical range of a bank of RAM. For a CPU node, it’s not an address at all — it’s an identifier, which we’ll get to shortly.
The critical thing to understand is that reg is not simply “start address, length.” It is a flat array of 32-bit integers called cells, and how those cells are grouped into address/length pairs depends entirely on two other properties: #address-cells and #size-cells. These two properties live in the parent node, not in the node that has the reg property.
#address-cells = <1>; // each address takes 1 cell (32-bit)
#size-cells = <1>; // each size takes 1 cell (32-bit)uart0@44e09000 {
reg = <0x44e09000 0x2000>;
// ^address ^size
// 1 cell 1 cell
};
};
If #address-cells were 2 instead of 1, each address in that node’s children would need two 32-bit cells to represent it — because the address is wider than 32 bits (64-bit addressing). The same logic applies to #size-cells. This is exactly the situation you hit on 64-bit platforms such as ARM64 or RISC-V64.
Why 64-bit Platforms Need Two Cells
A single 32-bit cell can only represent values up to 0xFFFFFFFF (4 GB). Once your memory map extends past that boundary — which is normal on any modern SoC with more than 4 GB of address space — a single cell can’t hold the address. The device tree solves this the same way C would solve representing a 64-bit number on a 32-bit machine: split it into two 32-bit halves, high word first.
#address-cells = <2>;
#size-cells = <2>;memory@100000000 {
device_type = “memory”;
reg = <0x00000001 0x00000000 0x00000000 0x40000000>;
// high addr low addr high size low size
// = 0x1_00000000 start, 0x40000000 (1 GB) long
};
};
Read that carefully: the first two cells together form one 64-bit address (0x1_00000000, i.e. 4 GiB), and the next two cells together form one 64-bit size (0x40000000, i.e. 1 GiB). If you get the cell count wrong anywhere in the ancestor chain, the device tree compiler won’t necessarily complain — but the kernel will compute the wrong physical address, and your driver will either fault or silently read garbage.
Finding the Cell Counts: Walk Up, Not Down
A common beginner mistake is assuming a node’s own properties tell you how to parse its reg. They don’t. You have to walk up the tree to the nearest ancestor that defines #address-cells and #size-cells, because those properties apply to that node’s children, not to the node itself.
| Property | Defined in | Applies to |
|---|---|---|
#address-cells |
Parent node | The address portion of each child’s reg |
#size-cells |
Parent node | The size portion of each child’s reg |
reg |
Child node | Itself, formatted per the parent’s cell counts |
If no ancestor defines these properties at all, the device tree specification falls back to defaults of 1 cell for address and 1 cell for size — but relying on this fallback is considered bad practice, since it makes the tree fragile to future edits. Always declare cell counts explicitly at every level that has children with a reg.
The Special Case: CPU reg Values
CPU nodes are the one place where reg doesn’t describe a memory address at all — it describes a CPU identifier. On a quad-core SoC, the four cores are numbered 0 through 3, and that numbering has no “size” component: a CPU ID is a single point, not a range. That’s why the cpus node conventionally sets #size-cells = <0> — telling the parser that children’s reg values contain zero size cells at all.
#address-cells = <1>;
#size-cells = <0>;cpu@0 { device_type = “cpu”; reg = <0>; };
cpu@1 { device_type = “cpu”; reg = <1>; };
cpu@2 { device_type = “cpu”; reg = <2>; };
cpu@3 { device_type = “cpu”; reg = <3>; };
};
Reading reg From Kernel Code
Once your platform boots, you can confirm the kernel parsed the reg property the way you expect by writing a tiny probe-time diagnostic in a driver. Here’s an original example using the of_address_to_resource() helper against a fictional platform device node ep,demo-block — not copied from any book, and using our own compatible string:
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/platform_device.h>static int ep_regdump_probe(struct platform_device *pdev)
{
struct resource res;
int ret;ret = of_address_to_resource(pdev->dev.of_node, 0, &res);
if (ret) {
dev_err(&pdev->dev, “failed to resolve reg property: %d\n”, ret);
return ret;
}dev_info(&pdev->dev, “reg resolved to 0x%llx – 0x%llx\n”,
(unsigned long long)res.start,
(unsigned long long)res.end);return 0;
}static const struct of_device_id ep_regdump_ids[] = {
{ .compatible = “ep,demo-block” },
{ }
};
MODULE_DEVICE_TABLE(of, ep_regdump_ids);static struct platform_driver ep_regdump_driver = {
.probe = ep_regdump_probe,
.driver = {
.name = “ep_regdump”,
.of_match_table = ep_regdump_ids,
},
};
module_platform_driver(ep_regdump_driver);
MODULE_LICENSE(“GPL”);
MODULE_DESCRIPTION(“EmbeddedPathashala reg property demo”);
Match the compatible string in the device tree node (ep,demo-block), build against your kernel headers, insert the module, and check dmesg:
$ sudo insmod ep_regdump.ko
$ dmesg | tail -n 3
[ 12.884511] ep_regdump 44e09000.demo-block: reg resolved to 0x44e09000 – 0x44e0affe
That single line confirms the whole chain worked: the device tree compiler packed the cells correctly, the kernel’s OF address translation walked the parent bus’s ranges/cell counts correctly, and your driver received the right physical resource.
Common Mistakes and Troubleshooting
- Mismatched cell count vs. actual values given. If a parent declares
#address-cells = <2>but a child’sregonly supplies one address cell before the size cells start, the compiler will misparse the boundary between address and size silently. - Forgetting
#size-cells = <0>on identifier-only buses. This affects CPU nodes and some bus controllers (like certain I2C/SPI parents) where children are numbered, not ranged. - Relying on the 1/1 default. It technically works, but any future refactor that adds a parent node without explicit cell counts can silently break every descendant’s
regparsing. - Copy-pasting a
regfrom a different SoC family. Cell widths and memory maps are platform-specific — always verify against your own SoC’s datasheet or reference manual.
Best Practices
- Always declare
#address-cellsand#size-cellsexplicitly on any node that has children with aregproperty — never depend on the fallback default. - When writing a 64-bit
regvalue by hand, double-check the high/low cell order — it’s high word first, matching big-endian cell ordering used throughout the device tree format. - Use
dtc -O dts your.dtbto decompile a built.dtband sanity-check that yourregvalues round-trip the way you intended. - When in doubt about how many cells a bus expects, check the binding documentation for that bus controller rather than guessing from a sibling node.
Performance and Security Considerations
An incorrect reg property doesn’t just cause a functional bug — on a memory-mapped I/O system, mapping the wrong physical range can mean a driver reads or writes to unrelated hardware, including regions that may be reserved for a secure world or another core. On multi-core SoCs with a trusted execution environment, always confirm your reg ranges don’t stray into memory reserved via a reserved-memory node.
Summary and Key Takeaways
- The
regproperty is a flat list of 32-bit cells, grouped into address/size pairs. #address-cellsand#size-cellslive in the parent node and control how a child’sregis parsed.- 64-bit addresses/sizes need 2 cells each, high word first.
- CPU nodes use
regas an identifier, not an address, with#size-cells = <0>. - Always verify cell counts explicitly rather than relying on the 1/1 fallback.
Conclusion
The reg property looks like a trivial pair of numbers at a glance, but it’s actually a small, well-defined encoding scheme that scales cleanly from 32-bit embedded SoCs to 64-bit server-class ARM and RISC-V platforms. Once you internalize that cell counts come from the parent and that CPU nodes are a special identifier-only case, reading any device tree — no matter how unfamiliar the SoC — becomes far less intimidating. In the next lecture in this free linux kernel development course, we’ll look at how devices reference each other across the tree using phandles, starting with interrupt controllers.
Frequently Asked Questions
What is the reg property in a Linux device tree?
It’s a list of one or more address/size pairs describing the memory-mapped resources (or, for CPU nodes, the identifier) that a device tree node owns, encoded as 32-bit cells.
Why does the reg property sometimes have four numbers instead of two?
Because the parent node declares 2 address cells and/or 2 size cells for 64-bit addressing, so each address or size is split into a high and low 32-bit word.
Where are #address-cells and #size-cells defined?
In the parent node, not the node containing the reg property. They apply to how that parent’s children are parsed.
What happens if #address-cells and #size-cells are missing?
The device tree specification defaults both to 1, but relying on this fallback is discouraged because it makes the tree fragile to future restructuring.
Why does a CPU node’s reg property have only one number?
Because CPU reg values are identifiers, not address ranges, so the cpus node sets #size-cells to 0, meaning children supply zero size cells.
How can I verify my reg property was parsed correctly?
Decompile the built .dtb with dtc -O dts, or write a small platform driver that calls of_address_to_resource() and logs the resolved resource at probe time.
Is the reg property specific to Linux?
No — it’s part of the Devicetree Specification used by multiple operating systems and bootloaders (like U-Boot), not a Linux-only convention.
Can a single node have multiple reg entries?
Yes. A node can list several address/size pairs back to back in its reg property when a device exposes multiple discontiguous register ranges.
Continue the Free Embedded Linux Bootloader Course
Next up: how phandles let device tree nodes reference each other for interrupts, clocks, and regulators.
–>
