<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Mehmet Akif Erdem</title>
    <subtitle>Personal site and notebook of Mehmet Akif Erdem.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://mehmetakiferdem.com/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://mehmetakiferdem.com"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-08-07T00:00:00+00:00</updated>
    <id>https://mehmetakiferdem.com/atom.xml</id>
    <entry xml:lang="en">
        <title>What Is a Device Tree?</title>
        <published>2026-08-07T00:00:00+00:00</published>
        <updated>2026-08-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            Mehmet Akif Erdem
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://mehmetakiferdem.com/blog/what-is-device-tree/"/>
        <id>https://mehmetakiferdem.com/blog/what-is-device-tree/</id>
        
        <content type="html" xml:base="https://mehmetakiferdem.com/blog/what-is-device-tree/">&lt;p&gt;If you have ever tried to bring Linux up on an embedded board, sooner or later you meet a file ending in &lt;code&gt;.dts&lt;&#x2F;code&gt;. Usually at the worst possible moment: the board won&#x27;t boot, nothing comes out on the screen, and someone tells you to &quot;check your device tree&quot;. This is about what that file is, why it exists, and what you are actually looking for inside it.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-problem-it-solves&quot;&gt;The Problem It Solves&lt;&#x2F;h2&gt;
&lt;p&gt;On a desktop, the kernel can largely find the hardware by itself. PCI and USB are formally specified, enumerable buses (1). Devices announce themselves with a vendor and product ID; a driver lists the IDs it supports in a table, and the kernel calls that driver&#x27;s &lt;code&gt;probe()&lt;&#x2F;code&gt; for every device matching an entry (2). USB works the same way through its own ID table (3). Nobody has to tell anybody anything in advance.&lt;&#x2F;p&gt;
&lt;p&gt;Embedded boards do not get that luxury. The kernel&#x27;s own documentation draws the line plainly: peripherals integrated into a system-on-chip hang off buses with minimal infrastructure, &quot;as opposed to large formally specified ones like PCI or USB&quot;, and what those devices have in common is direct addressing from a CPU bus (1). An I²C sensor or a display controller hanging off SPI does not announce itself; it just sits there, at that address, wired to that interrupt line. The only way the kernel knows about it is if somebody says so.&lt;&#x2F;p&gt;
&lt;p&gt;For a long time that somebody was the code itself. Every board had a file inside the kernel tree describing in C which device was wired where — the kernel still describes this path, where board-specific setup code registers the devices one by one (1). On ARM these files multiplied so badly that in 2011 Linus Torvalds took the situation apart in an email, saying the ARM code was heading somewhere unsustainable. The way out that got adopted was the device tree.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-idea&quot;&gt;The Idea&lt;&#x2F;h2&gt;
&lt;p&gt;A device tree is a data file describing how the hardware is wired. It is not code — it does not run, and it is not compiled into the kernel. It is a description: there is a controller at this address, it uses this interrupt line, it is fed by this clock, and these devices hang beneath it.&lt;&#x2F;p&gt;
&lt;p&gt;The format itself rests on a formal specification (4). It is called a tree because it genuinely is one. It starts at a root node, buses branch off it, and the devices attached to those buses branch off them — mirroring how the hardware is physically connected:&lt;&#x2F;p&gt;
&lt;p&gt;Rather than an invented example, here is a real one. The node below comes from &lt;code&gt;k3-am62p-main.dtsi&lt;&#x2F;code&gt; in the U-Boot tree for TI&#x27;s J722S&#x2F;AM67A family — the actual description an AM67A board reads when it boots:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;main_i2c0: i2c@20000000 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    compatible = &amp;quot;ti,am64-i2c&amp;quot;, &amp;quot;ti,omap4-i2c&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reg = &amp;lt;0x00 0x20000000 0x00 0x100&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    interrupts = &amp;lt;GIC_SPI 161 IRQ_TYPE_LEVEL_HIGH&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    #address-cells = &amp;lt;1&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    #size-cells = &amp;lt;0&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    power-domains = &amp;lt;&amp;amp;k3_pds 102 TI_SCI_PD_EXCLUSIVE&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    clocks = &amp;lt;&amp;amp;k3_clks 102 2&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    clock-names = &amp;quot;fck&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    status = &amp;quot;disabled&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Every line here means something. &lt;code&gt;reg&lt;&#x2F;code&gt; gives the controller&#x27;s address in the memory map — &lt;code&gt;0x20000000&lt;&#x2F;code&gt;, length &lt;code&gt;0x100&lt;&#x2F;code&gt;. The addresses are written as pairs of numbers because the address space is 64-bit; the first number carries the upper 32 bits. &lt;code&gt;interrupts&lt;&#x2F;code&gt; says which interrupt line is used; names like &lt;code&gt;GIC_SPI&lt;&#x2F;code&gt; and &lt;code&gt;IRQ_TYPE_LEVEL_HIGH&lt;&#x2F;code&gt; come from included header files, so although device tree source is plain data, it still goes through the C preprocessor.&lt;&#x2F;p&gt;
&lt;p&gt;It is worth noticing that &lt;code&gt;compatible&lt;&#x2F;code&gt; carries two values: &lt;code&gt;&quot;ti,am64-i2c&quot;&lt;&#x2F;code&gt; and &lt;code&gt;&quot;ti,omap4-i2c&quot;&lt;&#x2F;code&gt;. The list is read most-specific first. The kernel looks for a driver specific to am64, and failing that falls back to the older, more general omap4 driver (5). That way a new chip can work without anyone writing a new driver for it.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;clocks&lt;&#x2F;code&gt; and &lt;code&gt;clock-names&lt;&#x2F;code&gt; say where the device gets its clock: output 2 of device 102 on &lt;code&gt;&amp;amp;k3_clks&lt;&#x2F;code&gt;, which the driver will find under the name &lt;code&gt;&quot;fck&quot;&lt;&#x2F;code&gt;. &lt;code&gt;power-domains&lt;&#x2F;code&gt; wires up the power domain the same way. These are not frequency &lt;em&gt;settings&lt;&#x2F;em&gt; — they describe the wiring, which clock goes where.&lt;&#x2F;p&gt;
&lt;p&gt;The last line matters: &lt;code&gt;status = &quot;disabled&quot;&lt;&#x2F;code&gt;. The chip vendor describes the peripheral but leaves it off, because it cannot know whether that I²C bus is used on your board. Turning it on is the job of the board&#x27;s own &lt;code&gt;.dts&lt;&#x2F;code&gt;. In the J722S EVM&#x27;s board file it looks like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;amp;main_i2c0 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    status = &amp;quot;okay&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pinctrl-names = &amp;quot;default&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pinctrl-0 = &amp;lt;&amp;amp;main_i2c0_pins_default&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    clock-frequency = &amp;lt;400000&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    exp1: gpio@23 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        compatible = &amp;quot;ti,tca6424&amp;quot;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        reg = &amp;lt;0x23&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        gpio-controller;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        #gpio-cells = &amp;lt;2&amp;gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    };&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The leading &lt;code&gt;&amp;amp;main_i2c0&lt;&#x2F;code&gt; is a reference to the node above — it does not create a new node, it writes over the existing one. &lt;code&gt;status&lt;&#x2F;code&gt; is switched on, pins are assigned, the speed is set to 400 kHz, and a real device is added underneath: a TCA6424 GPIO expander sitting at address &lt;code&gt;0x23&lt;&#x2F;code&gt; on the I²C bus. Because the controller declared &lt;code&gt;#address-cells = &amp;lt;1&amp;gt;&lt;&#x2F;code&gt;, that address is written as a single number, unlike the controller&#x27;s own &lt;code&gt;reg&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;This is how a device tree works in practice: the vendor describes the SoC once, and you say what is wired to your board.&lt;&#x2F;p&gt;
&lt;p&gt;The property that really matters is &lt;code&gt;compatible&lt;&#x2F;code&gt;. This is where the kernel looks to pair a driver with a piece of hardware. A driver says &quot;I am compatible with &lt;code&gt;ti,tca6424&lt;&#x2F;code&gt;&quot;, a node in the device tree carries the same string, and the kernel puts the two together. Which is why typos fail silently: if the string doesn&#x27;t match, there is no error — the device simply never appears.&lt;&#x2F;p&gt;
&lt;p&gt;Worth keeping in mind: a device tree describes hardware, not driver configuration. The kernel&#x27;s guide to writing bindings stresses this boundary in particular (6). The line blurs constantly in practice, and it is the single most argued-about subject on the kernel lists — &quot;is this genuinely a property of the hardware, or is it your preference?&quot;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-files&quot;&gt;The Files&lt;&#x2F;h2&gt;
&lt;p&gt;You will see three extensions, and they are easy to confuse (11).&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.dts&lt;&#x2F;code&gt; is the source you write, the human-readable form. &lt;code&gt;.dtsi&lt;&#x2F;code&gt; is a fragment other files can include — the &quot;i&quot; is for include. A chip vendor typically describes the whole SoC in a &lt;code&gt;.dtsi&lt;&#x2F;code&gt;; you write a short &lt;code&gt;.dts&lt;&#x2F;code&gt; for your own board, include it, and add only what is specific to your board.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;.dtb&lt;&#x2F;code&gt; is the compiled form — the device tree blob. This is what the kernel actually reads. The compiler is &lt;code&gt;dtc&lt;&#x2F;code&gt; (7):&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;dtc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; -I dts -O dtb -o my-board.dtb my-board.dts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It also runs the other way, which is invaluable when hunting a problem. If you have a working &lt;code&gt;.dtb&lt;&#x2F;code&gt;, you can open it up and look inside:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;dtc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; -I dtb -O dts -o decoded.dts my-board.dtb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;On a running system you can also read the device tree straight from the filesystem. The kernel exposes the tree it is actually using here:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;ls&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; &#x2F;proc&#x2F;device-tree&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the fastest answer to &quot;is the file I edited the file that got loaded?&quot; A surprising share of the time, the answer is no.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-happens-at-boot&quot;&gt;What Happens at Boot&lt;&#x2F;h2&gt;
&lt;p&gt;The order goes like this: the bootloader — usually U-Boot (8) — loads the &lt;code&gt;.dtb&lt;&#x2F;code&gt; into memory, loads the kernel, and then hands the kernel the blob&#x27;s memory address in a register as it starts it. On ARM that register and the conditions the bootloader must meet are set out in the kernel&#x27;s booting document (9). Before the kernel brings up any of its drivers, it reads that tree, extracts the devices from it, and matches drivers against the &lt;code&gt;compatible&lt;&#x2F;code&gt; strings.&lt;&#x2F;p&gt;
&lt;p&gt;The practical consequence is that the kernel and the device tree can be updated separately. Adding a new sensor rarely means rebuilding the kernel; updating the &lt;code&gt;.dtb&lt;&#x2F;code&gt; is usually enough. But that cuts both ways — because they are independent, they can drift apart. Boot a new kernel with an old &lt;code&gt;.dtb&lt;&#x2F;code&gt; and the board quietly comes up incomplete, and it takes a while to work out why.&lt;&#x2F;p&gt;
&lt;p&gt;There is also the idea of overlays: fragments applied on top of an existing tree at runtime (10). This is what happens when you plug a HAT into a Raspberry Pi — the main tree stays as it is and a small fragment gets laid over it. Useful when the base board is always the same and only the module plugged into it changes.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-something-doesn-t-work&quot;&gt;When Something Doesn&#x27;t Work&lt;&#x2F;h2&gt;
&lt;p&gt;Device tree faults have a particular kind of unpleasantness, because they usually don&#x27;t produce an error. The device is simply absent. Working through it in this order saves time.&lt;&#x2F;p&gt;
&lt;p&gt;Start by checking whether the device was created at all. Is the node there under &lt;code&gt;&#x2F;proc&#x2F;device-tree&lt;&#x2F;code&gt;? If not, the problem is not in the file you edited — most likely the wrong &lt;code&gt;.dtb&lt;&#x2F;code&gt; is being loaded, or the node is switched off a level up with &lt;code&gt;status = &quot;disabled&quot;&lt;&#x2F;code&gt;. That second one happens constantly: peripherals usually arrive disabled in the vendor&#x27;s &lt;code&gt;.dtsi&lt;&#x2F;code&gt; files, and you are expected to turn them on one by one with &lt;code&gt;status = &quot;okay&quot;&lt;&#x2F;code&gt; in your board&#x27;s &lt;code&gt;.dts&lt;&#x2F;code&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;If the node is there but no driver attached, put your eye on the &lt;code&gt;compatible&lt;&#x2F;code&gt; string. Find the &lt;code&gt;of_device_id&lt;&#x2F;code&gt; table in the driver&#x27;s source and compare the strings character by character. One comma, one hyphen is enough.&lt;&#x2F;p&gt;
&lt;p&gt;There is also this: dtc warnings arrive switched off in most projects, and they genuinely tell you useful things. Address and &lt;code&gt;reg&lt;&#x2F;code&gt; mismatches, a missing &lt;code&gt;#address-cells&lt;&#x2F;code&gt; — exactly the sort of fault that turns into a silent failure at boot. It is worth keeping warnings on when you build.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;closing-thought&quot;&gt;Closing Thought&lt;&#x2F;h2&gt;
&lt;p&gt;What makes a device tree hard to grasp is that nobody says up front what it actually is. It looks like a configuration file, but it isn&#x27;t; it looks like a driver, but it isn&#x27;t that either. The most useful framing is to think of it as the contract between hardware and software: how the board is wired, written down in a language the kernel understands.&lt;&#x2F;p&gt;
&lt;p&gt;Once it sits in that frame, the rest follows. Why &lt;code&gt;compatible&lt;&#x2F;code&gt; is so critical, why the &lt;code&gt;.dtb&lt;&#x2F;code&gt; lives separately from the kernel, why &lt;code&gt;&#x2F;proc&#x2F;device-tree&lt;&#x2F;code&gt; is the first place to look when something is missing — all of it falls out of the same idea.&lt;&#x2F;p&gt;
&lt;!-- related --&gt;
&lt;h2 id=&quot;related-reading&quot;&gt;Related Reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;embedded-linux-boot-chain&#x2F;&quot;&gt;The Embedded Linux Boot Chain&lt;&#x2F;a&gt; — how the bootloader hands control to the kernel&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;what-is-dfu&#x2F;&quot;&gt;What Is DFU?&lt;&#x2F;a&gt; — flashing firmware over USB when there is no other way in&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;driver-api&#x2F;driver-model&#x2F;platform.html&quot;&gt;Platform Devices and Drivers&lt;&#x2F;a&gt; — why SoC peripherals are not enumerable the way PCI and USB are, and how board-specific code registers them.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;PCI&#x2F;pci.html&quot;&gt;How To Write Linux PCI Drivers&lt;&#x2F;a&gt; — the &lt;code&gt;pci_device_id&lt;&#x2F;code&gt; table and the &lt;code&gt;probe()&lt;&#x2F;code&gt; call for matching devices.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;driver-api&#x2F;usb&#x2F;usb.html&quot;&gt;USB Device Drivers&lt;&#x2F;a&gt; — ID-based driver matching on the USB side.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.devicetree.org&#x2F;specifications&#x2F;&quot;&gt;Devicetree Specification&lt;&#x2F;a&gt; — the formal definition of the format.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;devicetree&#x2F;usage-model.html&quot;&gt;Linux and the Devicetree&lt;&#x2F;a&gt; — how the kernel reads the tree and matches drivers.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;devicetree&#x2F;bindings&#x2F;writing-bindings.html&quot;&gt;Writing Devicetree Bindings&lt;&#x2F;a&gt; — the boundary between describing hardware and configuring a driver.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git.kernel.org&#x2F;pub&#x2F;scm&#x2F;utils&#x2F;dtc&#x2F;dtc.git&#x2F;about&#x2F;&quot;&gt;dtc — Device Tree Compiler&lt;&#x2F;a&gt; — the compiler&#x27;s own repository and usage.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.u-boot.org&#x2F;en&#x2F;latest&#x2F;develop&#x2F;devicetree&#x2F;control.html&quot;&gt;U-Boot: Devicetree Control&lt;&#x2F;a&gt; — how U-Boot handles the device tree.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;arch&#x2F;arm&#x2F;booting.html&quot;&gt;Booting ARM Linux&lt;&#x2F;a&gt; — which register the bootloader uses to hand the blob to the kernel.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.kernel.org&#x2F;devicetree&#x2F;overlay-notes.html&quot;&gt;Devicetree Overlay Notes&lt;&#x2F;a&gt; — how overlays are applied at runtime.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;elinux.org&#x2F;Device_Tree_Reference&quot;&gt;Device Tree Reference (eLinux)&lt;&#x2F;a&gt; — community reference with worked examples.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Git: Deleting Branches, Merging, and Undoing a Bad Push</title>
        <published>2026-08-02T00:00:00+00:00</published>
        <updated>2026-08-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            Mehmet Akif Erdem
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://mehmetakiferdem.com/blog/git-branches-merge-undo-push/"/>
        <id>https://mehmetakiferdem.com/blog/git-branches-merge-undo-push/</id>
        
        <content type="html" xml:base="https://mehmetakiferdem.com/blog/git-branches-merge-undo-push/">&lt;p&gt;These three things come up all the time, and somehow the commands never quite stick between one time and the next. So here they are, written down properly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;deleting-a-branch&quot;&gt;Deleting a Branch&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; branch -d feature-x&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the safe version — Git checks first, and refuses if &lt;code&gt;feature-x&lt;&#x2F;code&gt; has commits that never made it into your current branch. If you&#x27;re sure, you can override it: (1)&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; branch -D feature-x&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That only deletes it locally, though. If the branch also exists on the remote (GitHub, say), you need to delete it there separately:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; push origin --delete feature-x&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;One thing worth knowing: deleting a branch doesn&#x27;t actually delete its commits. They just become unreferenced and sit in the repository until Git&#x27;s garbage collector eventually sweeps them up, which can take a while. So &lt;code&gt;git branch -D&lt;&#x2F;code&gt; is more forgiving than it looks — if you delete the wrong branch, there&#x27;s usually still a way back (more on that in the reflog section below). I wouldn&#x27;t rely on that as a plan, but it&#x27;s good to know it&#x27;s there.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;merging-branches&quot;&gt;Merging Branches&lt;&#x2F;h2&gt;
&lt;p&gt;The usual shape is: check out the branch you want to merge &lt;em&gt;into&lt;&#x2F;em&gt;, then merge the other one in.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; checkout main&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; merge feature-x&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;What happens next depends on whether &lt;code&gt;main&lt;&#x2F;code&gt; has moved since you branched off. If it hasn&#x27;t, Git just slides the &lt;code&gt;main&lt;&#x2F;code&gt; pointer forward to match &lt;code&gt;feature-x&lt;&#x2F;code&gt; — a &lt;strong&gt;fast-forward&lt;&#x2F;strong&gt;, no merge commit involved. If &lt;code&gt;main&lt;&#x2F;code&gt; has moved on in the meantime, Git creates an actual merge commit that ties both histories together (2).&lt;&#x2F;p&gt;
&lt;p&gt;Sometimes you want that merge commit even when a fast-forward would&#x27;ve been possible — it leaves a visible marker in history that a feature branch existed and was merged, rather than the change quietly blending in. For that:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; merge --no-ff feature-x&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;If the same lines were touched on both sides, Git stops and leaves the conflict for you to sort out:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That&#x27;ll list which files are conflicted. Open each one, you&#x27;ll see &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;&#x2F;code&gt;, &lt;code&gt;=======&lt;&#x2F;code&gt;, and &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;code&gt; markers around the competing changes — pick what should actually stay, delete the markers, then:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; add resolved-file.txt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; commit&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And if it turns into more of a mess than it&#x27;s worth, you can just walk away from the whole thing:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; merge --abort&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That puts everything back exactly how it was before you started merging.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;undoing-a-bad-push&quot;&gt;Undoing a Bad Push&lt;&#x2F;h2&gt;
&lt;p&gt;This is the one people panic about, and honestly the panic is usually unwarranted — but which fix you reach for actually matters here.&lt;&#x2F;p&gt;
&lt;p&gt;If there&#x27;s any chance someone else has already pulled the branch, don&#x27;t touch history. Add a new commit that undoes the old one instead:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; revert&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt;bad-commit-sh&lt;&#x2F;span&gt;&lt;span&gt;a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; push&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;This is the boring, always-safe option. It doesn&#x27;t rewrite anything — it just adds a commit on top that cancels out the mistake, so everyone&#x27;s history stays in sync without anyone having to do anything special on their end (3).&lt;&#x2F;p&gt;
&lt;p&gt;If the branch is genuinely just yours and nobody&#x27;s pulled it yet, you can rewind and rewrite: (4)&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; reset --hard&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt;last-good-commit-sh&lt;&#x2F;span&gt;&lt;span&gt;a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; push --force-with-lease&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Use &lt;code&gt;--force-with-lease&lt;&#x2F;code&gt;, not plain &lt;code&gt;--force&lt;&#x2F;code&gt;, out of habit if nothing else. The difference matters more than it seems: &lt;code&gt;--force-with-lease&lt;&#x2F;code&gt; checks that nobody has pushed to the branch since you last fetched, and refuses if they have. Plain &lt;code&gt;--force&lt;&#x2F;code&gt; doesn&#x27;t check anything — it&#x27;ll happily overwrite someone else&#x27;s work without so much as a warning (5).&lt;&#x2F;p&gt;
&lt;p&gt;And if you&#x27;re staring at your terminal trying to remember what the last good commit even was — &lt;code&gt;git log&lt;&#x2F;code&gt; won&#x27;t help here, since it only shows commits reachable from where you currently are, which doesn&#x27;t include whatever you already reset away from. What you want is:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; reflog&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;It shows every place your branch has recently pointed to, including the ones that fell out of normal history. Scroll back to before things went wrong, then: (6)&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; reset --hard HEAD@{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FAB387;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;(swap in whatever position or SHA actually matches your case). The reflog is local to your machine and generally hangs onto entries for about 90 days, so it&#x27;s less &quot;emergency undo for bad pushes&quot; and more a general safety net for &quot;I think I just broke something&quot; — worth remembering it exists even outside this specific situation.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;closing-thought&quot;&gt;Closing Thought&lt;&#x2F;h2&gt;
&lt;p&gt;All three of these lean on the same underlying fact: Git almost never throws anything away immediately. Deleted branches, reset commits, old positions on a branch — they all linger around longer than they seem to. That&#x27;s really what makes &lt;code&gt;-D&lt;&#x2F;code&gt;, &lt;code&gt;reset --hard&lt;&#x2F;code&gt;, and force-pushing safe enough to use day to day, as long as you know where to go looking if one of them turns out to be a mistake.&lt;&#x2F;p&gt;
&lt;!-- related --&gt;
&lt;h2 id=&quot;related-reading&quot;&gt;Related Reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;understanding-make&#x2F;&quot;&gt;Understanding Make&lt;&#x2F;a&gt; — what actually triggers a rebuild&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further Reading&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-branch&quot;&gt;git-branch&lt;&#x2F;a&gt; — branch creation, listing, and deletion.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-merge&quot;&gt;git-merge&lt;&#x2F;a&gt; — merge strategies and conflict resolution.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-revert&quot;&gt;git-revert&lt;&#x2F;a&gt; — undoing commits by creating new ones.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-reset&quot;&gt;git-reset&lt;&#x2F;a&gt; — moving a branch pointer, with the difference between &lt;code&gt;--soft&lt;&#x2F;code&gt;, &lt;code&gt;--mixed&lt;&#x2F;code&gt;, and &lt;code&gt;--hard&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-push#Documentation&#x2F;git-push.txt---force-with-leaseltrefnamegt&quot;&gt;git-push: --force-with-lease&lt;&#x2F;a&gt; — why it&#x27;s safer than &lt;code&gt;--force&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;git-scm.com&#x2F;docs&#x2F;git-reflog&quot;&gt;git-reflog&lt;&#x2F;a&gt; — recovering commits that no longer appear in normal history.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Understanding Make</title>
        <published>2026-08-01T00:00:00+00:00</published>
        <updated>2026-08-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            Mehmet Akif Erdem
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://mehmetakiferdem.com/blog/understanding-make/"/>
        <id>https://mehmetakiferdem.com/blog/understanding-make/</id>
        
        <content type="html" xml:base="https://mehmetakiferdem.com/blog/understanding-make/">&lt;p&gt;&lt;code&gt;make&lt;&#x2F;code&gt; shows up in almost every embedded and systems project I&#x27;ve touched — Yocto recipes, kernel builds, U-Boot, small firmware repos. It&#x27;s also one of those tools people use for years by imitation, copying a Makefile from the last project without ever quite pinning down the model underneath it. Here&#x27;s that model, laid out directly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-is-make&quot;&gt;What is Make?&lt;&#x2F;h2&gt;
&lt;p&gt;Make is a build automation tool: it reads a file (a &lt;code&gt;Makefile&lt;&#x2F;code&gt;) describing a set of &lt;strong&gt;targets&lt;&#x2F;strong&gt;, the &lt;strong&gt;prerequisites&lt;&#x2F;strong&gt; each target depends on, and the &lt;strong&gt;recipe&lt;&#x2F;strong&gt; (shell commands) needed to produce that target from its prerequisites. Given that description, &lt;code&gt;make&lt;&#x2F;code&gt; figures out which targets are out of date and runs only the recipes needed to bring them up to date — nothing more.&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;make&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;main&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; main.o utils.o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	gcc -o main main.o utils.o&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;main.o&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; main.c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	gcc -c main.c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Run &lt;code&gt;make&lt;&#x2F;code&gt;, and it builds &lt;code&gt;main.o&lt;&#x2F;code&gt; only if &lt;code&gt;main.c&lt;&#x2F;code&gt; is newer than &lt;code&gt;main.o&lt;&#x2F;code&gt; (or &lt;code&gt;main.o&lt;&#x2F;code&gt; doesn&#x27;t exist yet), then links &lt;code&gt;main&lt;&#x2F;code&gt; only if &lt;code&gt;main.o&lt;&#x2F;code&gt; or &lt;code&gt;utils.o&lt;&#x2F;code&gt; changed. Nothing is rebuilt unnecessarily.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;who-is-it-for&quot;&gt;Who is it for?&lt;&#x2F;h2&gt;
&lt;p&gt;Make was written by Stuart Feldman at Bell Labs in April 1976. By his own account, the trigger was a colleague — Steve Johnson, author of &lt;code&gt;yacc&lt;&#x2F;code&gt; — losing a morning debugging a program that was, in fact, correct; the binary simply hadn&#x27;t been relinked after a source change. Feldman received the 2003 ACM Software System Award for it. The tool that came out of that afternoon — track dependencies once, let the machine figure out what needs redoing — became one of the most widely copied ideas in software tooling. GNU Make, the version almost everyone uses today, is a free reimplementation from the GNU Project (1) (5) (6).&lt;&#x2F;p&gt;
&lt;p&gt;In practice, it&#x27;s for anyone assembling a non-trivial artifact — a compiled binary, a document, a set of generated files — from smaller pieces, where rebuilding everything from scratch on every change would be wasteful or slow. That covers C&#x2F;C++ projects, kernel and bootloader builds, LaTeX documents, and plenty of ad-hoc automation that has nothing to do with compiling code at all.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;when-should-you-reach-for-it&quot;&gt;When should you reach for it?&lt;&#x2F;h2&gt;
&lt;p&gt;Make earns its keep when three things are true at once:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;There&#x27;s a real &lt;strong&gt;dependency graph&lt;&#x2F;strong&gt; — some outputs depend on others, and rebuilding is not simply &quot;regenerate everything.&quot;&lt;&#x2F;li&gt;
&lt;li&gt;Rebuilds are &lt;strong&gt;expensive enough&lt;&#x2F;strong&gt; that skipping unnecessary ones matters (a two-second compile doesn&#x27;t need this; a twenty-minute kernel build does).&lt;&#x2F;li&gt;
&lt;li&gt;The rules are reasonably &lt;strong&gt;stable&lt;&#x2F;strong&gt; — Make&#x27;s file-timestamp model doesn&#x27;t handle wildly dynamic pipelines as gracefully as a task runner with explicit invalidation might.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For a handful of files with no interdependencies, a shell script is often simpler and more honest. Make starts paying off once &quot;did this actually need rebuilding?&quot; becomes a question worth asking automatically instead of by hand.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;where-does-it-sit-in-the-toolchain&quot;&gt;Where does it sit in the toolchain?&lt;&#x2F;h2&gt;
&lt;p&gt;Make usually sits one layer above the compiler and one layer below whatever a human runs. In a typical C project: source files → (compiler, driven by Make) → object files → (linker, driven by Make) → final binary. Make itself doesn&#x27;t compile or link anything — it invokes the tools that do, as shell commands in each recipe.&lt;&#x2F;p&gt;
&lt;p&gt;It also tends to sit &lt;em&gt;underneath&lt;&#x2F;em&gt; higher-level build-configuration tools. CMake, Autotools, and Yocto&#x27;s BitBake, for instance, don&#x27;t replace Make so much as generate Makefiles (or drive an equivalent) as their actual execution backend. So even projects that never show you a Makefile directly are often running one somewhere in the pipeline.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-does-it-work-the-way-it-does&quot;&gt;Why does it work the way it does?&lt;&#x2F;h2&gt;
&lt;p&gt;The core design choice — comparing file modification times to decide what&#x27;s stale — is what makes Make fast on large trees: checking a timestamp is cheap, so a rebuild of a huge project after a one-line change can skip almost everything and still be correct, &lt;em&gt;provided the dependency graph given to it is accurate&lt;&#x2F;em&gt;.&lt;&#x2F;p&gt;
&lt;p&gt;That last clause is the source of almost every &quot;Make is broken&quot; complaint people run into, and it&#x27;s worth sitting with: Make is only ever as correct as the graph it&#x27;s told about. If a &lt;code&gt;.c&lt;&#x2F;code&gt; file &lt;code&gt;#include&lt;&#x2F;code&gt;s a header that isn&#x27;t listed as a prerequisite of the corresponding &lt;code&gt;.o&lt;&#x2F;code&gt; file, editing that header won&#x27;t trigger a rebuild — not because Make is confused, but because nothing ever told it that dependency existed. This is why real-world Makefiles almost always pair with a compiler-generated dependency step (&lt;code&gt;-MMD -MP&lt;&#x2F;code&gt; with GCC&#x2F;Clang) rather than hand-written header lists — the compiler already knows the true include graph, so it&#x27;s the only reliable source for it (4).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;how-does-it-actually-work&quot;&gt;How does it actually work?&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Targets, prerequisites, recipes.&lt;&#x2F;strong&gt; The basic unit is:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;make&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;target&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; prerequisite1 prerequisite2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	recipe line 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	recipe line 2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Recipe lines must be indented with a &lt;strong&gt;tab&lt;&#x2F;strong&gt;, not spaces — a famous historical wart that still trips people up today.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Automatic variables&lt;&#x2F;strong&gt; cut down on repetition:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;make&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;%.o&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; %.c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	gcc -c $&amp;lt; -o $@&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;$@&lt;&#x2F;code&gt; is the target, &lt;code&gt;$&amp;lt;&lt;&#x2F;code&gt; is the first prerequisite — this pattern rule alone can build every &lt;code&gt;.o&lt;&#x2F;code&gt; from its matching &lt;code&gt;.c&lt;&#x2F;code&gt; file without one line per file.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;.PHONY&lt;&#x2F;code&gt; targets&lt;&#x2F;strong&gt; mark names that don&#x27;t correspond to real files — &lt;code&gt;clean&lt;&#x2F;code&gt;, &lt;code&gt;all&lt;&#x2F;code&gt;, &lt;code&gt;test&lt;&#x2F;code&gt; — so Make doesn&#x27;t get confused if a file with that name happens to exist, and always runs the recipe rather than checking timestamps: (2)&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;make&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;.PHONY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; clean&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;clean&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	rm -f *.o main&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Variables&lt;&#x2F;strong&gt; avoid repeating flags everywhere and make cross-project reuse easier:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;make&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CC&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; gcc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CFLAGS&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; -Wall -O2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;main.o&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; main.c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	$(CC) $(CFLAGS) -c main.c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;strong&gt;Automatic dependency generation&lt;&#x2F;strong&gt; closes the gap described above:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;make&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CFLAGS&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; -Wall -O2 -MMD -MP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #CBA6F7;&quot;&gt;-include&lt;&#x2F;span&gt;&lt;span&gt; $(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;wildcard&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; *.d&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;%.o&lt;&#x2F;span&gt;&lt;span style=&quot;color: #94E2D5;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span&gt; %.c&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	$(CC) $(CFLAGS) -c $&amp;lt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;-MMD&lt;&#x2F;code&gt; makes the compiler emit a &lt;code&gt;.d&lt;&#x2F;code&gt; file per object, listing every header actually pulled in during that compile. &lt;code&gt;-include&lt;&#x2F;code&gt; folds those files back into the Makefile so the dependency graph stays accurate as headers change — without anyone maintaining it by hand (3).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;closing-thought&quot;&gt;Closing thought&lt;&#x2F;h2&gt;
&lt;p&gt;Most of what looks like Make&#x27;s quirks — the timestamp comparisons, the insistence on tabs, the way a missing dependency fails silently instead of loudly — falls out of one design decision made in 1976: keep the model simple and the check cheap, and trust the Makefile to describe reality accurately. Once that&#x27;s the lens, most &quot;Make is being weird&quot; moments turn out to be &quot;the graph I gave it was incomplete&quot; — which is a much easier thing to fix.&lt;&#x2F;p&gt;
&lt;!-- related --&gt;
&lt;h2 id=&quot;related-reading&quot;&gt;Related Reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;git-branches-merge-undo-push&#x2F;&quot;&gt;Git: Deleting Branches, Merging, and Undoing a Bad Push&lt;&#x2F;a&gt; — the everyday branch and push commands&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;references&quot;&gt;References&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;Feldman, S. I. (1979). &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;onlinelibrary.wiley.com&#x2F;doi&#x2F;abs&#x2F;10.1002&#x2F;spe.4380090402&quot;&gt;Make — A Program for Maintaining Computer Programs&lt;&#x2F;a&gt;. &lt;em&gt;Software: Practice and Experience&lt;&#x2F;em&gt;, 9(4), 255–265. — Feldman&#x27;s own paper introducing Make, including the original motivation.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;make&#x2F;manual&#x2F;html_node&#x2F;index.html&quot;&gt;GNU Make Manual&lt;&#x2F;a&gt; — the complete, authoritative reference for GNU Make&#x27;s syntax and semantics.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.gnu.org&#x2F;software&#x2F;make&#x2F;manual&#x2F;html_node&#x2F;Automatic-Prerequisites.html&quot;&gt;GNU Make Manual: Automatic Prerequisites&lt;&#x2F;a&gt; — the &lt;code&gt;-MMD&lt;&#x2F;code&gt;&#x2F;&lt;code&gt;-include&lt;&#x2F;code&gt; pattern in the maintainers&#x27; own words.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;gcc.gnu.org&#x2F;onlinedocs&#x2F;gcc&#x2F;Preprocessor-Options.html&quot;&gt;GCC: Preprocessor Options (&lt;code&gt;-M&lt;&#x2F;code&gt; family)&lt;&#x2F;a&gt; — what &lt;code&gt;-MMD&lt;&#x2F;code&gt; and &lt;code&gt;-MP&lt;&#x2F;code&gt; do under the hood.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Make_(software)&quot;&gt;Make (software) — Wikipedia&lt;&#x2F;a&gt; — general history and lineage of Make implementations (BSD Make, GNU Make, NMAKE, etc.).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Stuart_Feldman&quot;&gt;Stuart Feldman — Wikipedia&lt;&#x2F;a&gt; — background on Feldman&#x27;s work at Bell Labs and the 2003 ACM Software System Award.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The Embedded Linux Boot Chain</title>
        <published>2026-07-30T00:00:00+00:00</published>
        <updated>2026-07-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            Mehmet Akif Erdem
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://mehmetakiferdem.com/blog/embedded-linux-boot-chain/"/>
        <id>https://mehmetakiferdem.com/blog/embedded-linux-boot-chain/</id>
        
        <content type="html" xml:base="https://mehmetakiferdem.com/blog/embedded-linux-boot-chain/">&lt;p&gt;Last time I wrote about &lt;a href=&quot;&#x2F;blog&#x2F;what-is-dfu&#x2F;&quot;&gt;DFU&lt;&#x2F;a&gt; as a way to flash firmware over USB. That post skipped a question worth answering on its own: what actually happens between pressing the power button and a shell prompt showing up? DFU is just one branch off this path, so it&#x27;s worth walking the whole thing once.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-stages&quot;&gt;The stages&lt;&#x2F;h2&gt;
&lt;p&gt;Drawn as a Mermaid flowchart, the chain (with DFU&#x27;s place in it) looks like this:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;mermaid&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;flowchart TD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    A[Power-on &#x2F; Reset] --&amp;gt; B[Boot ROM&amp;lt;br&#x2F;&amp;gt;immutable, on-chip]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    B --&amp;gt; C{Valid image on&amp;lt;br&#x2F;&amp;gt;boot media?}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    C --&amp;gt;|No &#x2F; forced| D[ROM recovery mode&amp;lt;br&#x2F;&amp;gt;UART &#x2F; USB]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    C --&amp;gt;|Yes| E[SPL&amp;lt;br&#x2F;&amp;gt;first-stage bootloader]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    E --&amp;gt; F[U-Boot&amp;lt;br&#x2F;&amp;gt;second-stage bootloader]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    F --&amp;gt; G{Enter DFU?}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    G --&amp;gt;|Yes| H[DFU mode&amp;lt;br&#x2F;&amp;gt;waiting for firmware over USB]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    G --&amp;gt;|No| I[Load kernel + device tree]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    I --&amp;gt; J[Linux kernel]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    J --&amp;gt; K[Mount root filesystem]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    K --&amp;gt; L[init &#x2F; systemd]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;And rendered:&lt;&#x2F;p&gt;
&lt;img src=&quot;&#x2F;bootloader-flow.svg&quot; alt=&quot;Flowchart of the embedded Linux boot chain: power-on resets into the boot ROM, which checks the boot media for a valid image. If none is found (or recovery is forced), it drops into a UART&#x2F;USB recovery mode. Otherwise it loads the SPL, which loads U-Boot proper. U-Boot can either enter DFU mode to wait for firmware over USB, or load the kernel and device tree, boot Linux, mount the root filesystem, and hand off to init&#x2F;systemd.&quot; &#x2F;&gt;
&lt;h3 id=&quot;boot-rom&quot;&gt;Boot ROM&lt;&#x2F;h3&gt;
&lt;p&gt;Baked into the silicon at manufacturing time — nothing about it can be changed after the fact. Its only job is to find &lt;em&gt;something&lt;&#x2F;em&gt; bootable on one of a small set of boot media (eMMC, SD, QSPI flash, sometimes USB) according to boot-mode pins or eFuses, and jump to it. If it can&#x27;t find anything valid, most SoCs fall back to a minimal UART or USB recovery mode at this stage — the very first place you can talk to the chip at all.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;spl&quot;&gt;SPL&lt;&#x2F;h3&gt;
&lt;p&gt;Short for Secondary Program Loader (U-Boot&#x27;s term for it; other vendors call it FSBL or similar). It runs from a small amount of on-chip SRAM, before DRAM is even initialized, so it has to be tiny. Its main job is exactly that: bring up DRAM, then load the next stage — full U-Boot — into it.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;u-boot-proper&quot;&gt;U-Boot proper&lt;&#x2F;h3&gt;
&lt;p&gt;This is the bootloader most people mean when they say &quot;bootloader.&quot; It has drivers, a shell, environment variables, network support, and a command interpreter. It&#x27;s also where DFU actually lives — running &lt;code&gt;dfu 0 mmc 0&lt;&#x2F;code&gt; (or similar) drops U-Boot into DFU mode instead of continuing the normal boot, which is the mode &lt;code&gt;dfu-util&lt;&#x2F;code&gt; talks to from the host side. Otherwise, it loads the kernel image and device tree blob into memory and jumps to the kernel.&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kernel-and-userspace&quot;&gt;Kernel and userspace&lt;&#x2F;h3&gt;
&lt;p&gt;The kernel decompresses itself, brings up the console, probes hardware using the device tree it was handed, and mounts a root filesystem — often the same eMMC or SD card the bootloader booted from, sometimes NFS during development. Once the root filesystem is mounted, the kernel hands off to PID 1 — usually &lt;code&gt;systemd&lt;&#x2F;code&gt; on a Yocto-built image — and that&#x27;s where &quot;userspace&quot; actually starts.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-this-matters-for-dfu&quot;&gt;Why this matters for DFU&lt;&#x2F;h2&gt;
&lt;p&gt;DFU only makes sense once you see where it sits: it&#x27;s a detour U-Boot can take &lt;em&gt;instead of&lt;&#x2F;em&gt; loading the kernel, not a separate thing bolted on top. That&#x27;s also why DFU can&#x27;t rescue every failure mode — if the boot ROM itself can&#x27;t find a valid SPL, you never reach U-Boot at all, and you&#x27;re relying on whatever recovery mode the boot ROM supports instead (which varies a lot by vendor and is a good topic for another post).&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.u-boot.org&#x2F;en&#x2F;latest&#x2F;develop&#x2F;spl.html&quot;&gt;U-Boot: Generic SPL framework&lt;&#x2F;a&gt; — how SPL is built and what it&#x27;s responsible for.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;trustedfirmware-a.readthedocs.io&#x2F;en&#x2F;latest&#x2F;design&#x2F;firmware-design.html&quot;&gt;Trusted Firmware-A: Firmware Design&lt;&#x2F;a&gt; — the more elaborate multi-stage boot flow (BL1&#x2F;BL2&#x2F;BL31&#x2F;BL32&#x2F;BL33) used on many modern Arm SoCs with a secure boot chain, which sits underneath U-Boot on those platforms.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;!-- related --&gt;
&lt;h2 id=&quot;related-reading&quot;&gt;Related Reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;what-is-device-tree&#x2F;&quot;&gt;What Is a Device Tree?&lt;&#x2F;a&gt; — how hardware is described to the kernel&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;what-is-dfu&#x2F;&quot;&gt;What Is DFU?&lt;&#x2F;a&gt; — flashing firmware over USB when there is no other way in&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>What Is DFU?</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            Mehmet Akif Erdem
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://mehmetakiferdem.com/blog/what-is-dfu/"/>
        <id>https://mehmetakiferdem.com/blog/what-is-dfu/</id>
        
        <content type="html" xml:base="https://mehmetakiferdem.com/blog/what-is-dfu/">&lt;p&gt;If you&#x27;ve ever plugged an embedded board into your computer and watched it enumerate as a strange, driver-less USB device, there&#x27;s a good chance it was sitting in &lt;strong&gt;DFU mode&lt;&#x2F;strong&gt; — Device Firmware Update.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-dfu-actually-is&quot;&gt;What DFU actually is&lt;&#x2F;h2&gt;
&lt;p&gt;DFU is a USB device class defined by the USB Implementers Forum, originally meant for updating firmware on USB peripherals without any product-specific tooling. A device that supports it exposes a small, standardized interface: a host-side tool can query it, send a new firmware image, and trigger a reboot into the new code — all over the same USB cable used for everything else.&lt;&#x2F;p&gt;
&lt;p&gt;On embedded boards, &quot;DFU mode&quot; usually means the bootloader itself (U-Boot, for example) is running a minimal DFU stack instead of booting Linux. The board shows up on the host as a DFU-class USB device, and a tool like &lt;code&gt;dfu-util&lt;&#x2F;code&gt; can list the flashable targets (partitions, memory regions, whatever the bootloader chooses to expose) and write to them directly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-it-matters&quot;&gt;Why it matters&lt;&#x2F;h2&gt;
&lt;p&gt;A few things make DFU genuinely useful in embedded development and production:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No extra hardware.&lt;&#x2F;strong&gt; No JTAG probe, no UART adapter — just the USB port the board already has.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Recoverability.&lt;&#x2F;strong&gt; If a board is bricked or has no valid OS on flash, DFU mode (often triggered by a strap pin or a boot ROM fallback) is frequently the last line of defense to get it flashable again.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Standardization.&lt;&#x2F;strong&gt; Because it&#x27;s a real USB class with real drivers, it works the same way across operating systems without a vendor-specific kernel driver.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;It&#x27;s worth contrasting DFU with adjacent mechanisms you&#x27;ll bump into on the same boards: &lt;code&gt;fastboot&lt;&#x2F;code&gt; (Android&#x27;s protocol, similar goal, different wire format), and full system-image tools like &lt;code&gt;swupdate&lt;&#x2F;code&gt; or &lt;code&gt;RAUC&lt;&#x2F;code&gt;, which handle &lt;em&gt;update delivery and rollback&lt;&#x2F;em&gt; once Linux is already running — DFU typically operates one layer below that, closer to the bootloader.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;a-rough-workflow&quot;&gt;A rough workflow&lt;&#x2F;h2&gt;
&lt;p&gt;A typical DFU session from the host side looks like:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #CDD6F4; background-color: #1E1E2E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9399B2;font-style: italic;&quot;&gt;# see what the board exposes in DFU mode&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;dfu-util&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; -l&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9399B2;font-style: italic;&quot;&gt;# flash an image to a specific alt-setting&#x2F;partition&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #89B4FA;font-style: italic;&quot;&gt;dfu-util&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FAB387;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #A6E3A1;&quot;&gt; -D firmware.bin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;The board resets into the bootloader&#x27;s DFU handler, the host tool negotiates the transfer, and the image lands wherever the bootloader mapped that alt-setting — often a specific eMMC&#x2F;NAND partition or a staging area the bootloader flashes on next boot.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;closing-thought&quot;&gt;Closing thought&lt;&#x2F;h2&gt;
&lt;p&gt;DFU is one of those pieces of infrastructure that&#x27;s invisible when it works: you plug in a cable, run one command, and a board that couldn&#x27;t boot a minute ago is back up. Most of the interesting engineering is in getting the bootloader side right — what gets exposed, how it maps to real storage, and how it fails safely when the transfer gets interrupted halfway through.&lt;&#x2F;p&gt;
&lt;p&gt;This was a short one — I&#x27;ll go deeper into specific implementations (and where they get hairy) in a future post.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;further-reading&quot;&gt;Further reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.usb.org&#x2F;sites&#x2F;default&#x2F;files&#x2F;DFU_1.1.pdf&quot;&gt;Universal Serial Bus Device Class Specification for Device Firmware Upgrade, Revision 1.1&lt;&#x2F;a&gt; — the official USB-IF spec DFU is based on.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dfu-util.sourceforge.net&#x2F;&quot;&gt;dfu-util&lt;&#x2F;a&gt; — the host-side tool used in the example above.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.u-boot.org&#x2F;en&#x2F;latest&#x2F;usage&#x2F;dfu.html&quot;&gt;U-Boot: Device Firmware Upgrade (DFU)&lt;&#x2F;a&gt; — how DFU is implemented in the bootloader most embedded Linux boards use.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;!-- related --&gt;
&lt;h2 id=&quot;related-reading&quot;&gt;Related Reading&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;embedded-linux-boot-chain&#x2F;&quot;&gt;The Embedded Linux Boot Chain&lt;&#x2F;a&gt; — how the bootloader hands control to the kernel&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;&#x2F;blog&#x2F;what-is-device-tree&#x2F;&quot;&gt;What Is a Device Tree?&lt;&#x2F;a&gt; — how hardware is described to the kernel&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
</feed>
