<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://www.lowlevel.dev/</id>
    <title>Low Level Learning</title>
    <updated>2026-02-02T14:24:41.850Z</updated>
    <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
    <author>
        <name>Rob Wells</name>
        <uri>https://www.lowlevel.dev/</uri>
    </author>
    <link rel="alternate" href="https://www.lowlevel.dev/"/>
    <link rel="self" href="https://www.lowlevel.dev/atom.xml"/>
    <subtitle>Where a seasoned Engineering leader becomes an embedded newbie, sharing leadership lessons while frantically googling 'what is a watchdog timer?'</subtitle>
    <rights>Copyright © 2026 Rob Wells</rights>
    <entry>
        <title type="html"><![CDATA[Part 1: Why VS Code + Wokwi for Embedded Learning]]></title>
        <id>https://www.lowlevel.dev/posts/embedded-games-1/part-1-why-vs-code--wokwi-for-embedded-learning/</id>
        <link href="https://www.lowlevel.dev/posts/embedded-games-1/part-1-why-vs-code--wokwi-for-embedded-learning/"/>
        <updated>2026-01-27T15:12:48.177Z</updated>
        <summary type="html"><![CDATA[Most people use technology. Very few actually control it. I want to change that. You're going to learn embedded programming by building some...]]></summary>
        <content type="html"><![CDATA[<h1>Post 1: VS Code + Wokwi for Embedded Learning</h1>
<p>Most people use technology. Very few actually control it.</p>
<p>I want to change that. You're going to learn embedded programming by building something fun.  Together, we'll build 'Light Chaser', a classic arcade game where a light bounces back and forth. Time your button press right to hit the centre. Miss and you lose.</p>
<p>Seems easy enough, right? But making it work means nailing the 'Big Three' of electronics: timing, inputs, outputs.</p>
<p>Before we wire up our first virtual LED though, we need to sort out your workshop. Most beginners grab basic tools they'll outgrow in a month. We're skipping that. Going straight to a professional setup that actual developers use: VS Code and Wokwi. Like learning to drive in a simulator before hitting actual roads. Safer, faster, way more fun when nothing can break.</p>
<h2>The Magic Smoke Problem</h2>
<p>Here's how it usually goes when you're starting out.</p>
<p>Order a microcontroller board online. Wait a week for shipping. Wire something wrong and components let out this thing hobbyists call 'magic smoke'. They stop working after that. Order replacements, wait another week.</p>
<p>Hardware doesn't forgive mistakes. Connect power backwards and you've got a dead board. Too much current kills the LED. Short two pins and everything dies. I've done all of these.</p>
<p>This isn't about embedded development being hard. It's about <em>learning</em> it being hard. Professionals screw these things up too, difference is they know why it failed and how to figure it out. Beginners just see smoke and confusion.</p>
<p>Simulators fix this. Mistakes are free. Wire something wrong and you just reset, try again. Want to see what happens when you push 500mA through a pin rated for 20mA? Go for it. The simulated chip won't die. You learn what <em>would</em> happen without spending £15 on replacements or waiting a week for shipping.</p>
<h2>Why Not Just Use the Browser Simulator?</h2>
<p>If you've looked for Arduino tutorials, you've probably seen Wokwi already. Great browser simulator. Build circuits, write code, run everything in your browser. Perfect for quick experiments.</p>
<p>We're taking it further using VS Code. Not just here to blink LEDs. We're learning professional embedded development from day one.</p>
<p>The browser's fine for messing about. VS Code's where real work happens.</p>
<p>Here's what you get with VS Code that the browser can't match:</p>
<p><strong>A real IDE.</strong> The browser editor's fine for basics. VS Code's different. Autocomplete that actually understands your code. Jump to function definitions with one click. Refactoring tools rename variables across your whole project. Errors show up before you even compile. These aren't luxuries, this is how people actually write code. Learning with proper tools now means no bad habits to unlearn later.</p>
<p><strong>Version control.</strong> Browser projects live in browser storage. Clear your cache, switch computers, crash at the wrong moment and it's gone. VS Code stores your projects as files on your computer. Stick them in Git repositories, track every change, experiment on branches. How actual software gets developed.</p>
<p><strong>Offline work.</strong> Once you've installed everything and grabbed dependencies, internet becomes optional. On a train, in a cabin somewhere, wherever. The browser simulator needs connectivity. Your local setup doesn't.</p>
<p><strong>PlatformIO integration.</strong> The Arduino IDE's alright for getting started but hides too much. PlatformIO's widely used in the industry for embedded toolchains. Runs inside VS Code, manages libraries, handles different board configs. Scales from hobby projects to professional firmware. You'll probably face PlatformIO or something like it when you work on real embedded stuff later.</p>
<p><strong>Debugging.</strong> This is the big one. Browser simulator runs your code. VS Code extension lets you stop time. Set a breakpoint on any line and when execution hits it, everything freezes. Every variable's there to inspect. Step through code one line at a time, watch values change. You see exactly what's happening inside your microcontroller at that moment.</p>
<p>I'll cover debugging properly later. Here's what matters now. It changes how you think about code. Problems that'd take hours of print statements take minutes with a debugger.</p>
<h2>The Goal of This Series</h2>
<p>By the end you'll have a complete, playable Light Chaser game in simulation. Clean, modular code organised like professional stuff. You'll understand GPIO, timing, state machines, I2C. Enough to build this on real hardware if you want.</p>
<p>What actually matters though is understanding <em>why</em> things work, not just <em>that</em> they work.</p>
<p>When you see <code>pinMode(13, OUTPUT)</code>, you'll know what's happening at the register level. When you use <code>millis()</code> for timing, you'll understand the overflow behaviour and why it doesn't matter. Read a button and you'll understand pull-up resistors, active-low logic, switch bounce.</p>
<h2>What You'll Need</h2>
<p>A computer before the next post. Windows, Mac, Linux, doesn't matter. Internet for initial setup and downloading packages. About 2GB of disk space for VS Code, PlatformIO, and Wokwi.</p>
<p>No prior programming experience needed. Knowing variables and loops helps, but I'll explain as we go. Never programmed before? Just learn C alongside embedded concepts. Plenty of people do it that way.</p>
<p>No hardware needed either. That's the whole point of Wokwi. Build and test everything in simulation first. Want to build the physical version later? I'll put a shopping list at the end. Optional though.</p>
<h2>Why This Approach Works</h2>
<p>Learning embedded programming's tricky for one reason: too many things at once.</p>
<p>A programming language. How microcontrollers work. Electronics. Reading datasheets. Debugging techniques nothing like web or desktop stuff.</p>
<p>Most tutorials throw all this at you with real hardware. Overwhelming. Something breaks and you don't know if it's your code, your wiring, a fried component, or plain not understanding how the chip works.</p>
<p>Simulation strips out the hardware variables. Something doesn't work and it's your code or your understanding. Much smaller problem. The debugger shows you exactly what your code's doing. Makes finding bugs way easier.</p>
<p>Once you've got the software side down, adding real hardware's straightforward. Concepts transfer directly, code's the same. Just pick up a few practical bits like soldering and reading resistor colours.</p>
<h2>A Note on Arduino vs 'Real Embedded'</h2>
<p>Some people think Arduino's just a toy. They'll tell you to learn 'real' embedded programming with bare-metal register stuff from the start.</p>
<p>They're not wrong. Arduino hides a lot. That <code>digitalWrite()</code> function you'll use compiles into code that's roughly 18-50 times slower than direct register access. For professional work, that matters.</p>
<p>But they're missing the point about learning.</p>
<p>Arduino's abstractions let you focus on concepts without drowning in processor-specific details. Once you know <em>what</em> <code>digitalWrite()</code> does, learning <em>how</em> to do it faster with registers becomes straightforward. The other way round doesn't work. Start with registers and you're fighting syntax whilst trying to grasp fundamental concepts at the same time. Recipe for giving up.</p>
<p>We'll peek behind Arduino's curtain later anyway. You'll see what those friendly functions actually do. By then you'll have context for it.</p>
<h2>What's Next</h2>
<p>Next post we're installing VS Code, PlatformIO, and the Wokwi extension. I'll walk through every step. By the end you'll have a working setup ready for building embedded stuff.</p>
<p>Post after that? You'll write your first embedded program.</p>
<p>Then we build the game. See you next time!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-27T15:12:48.177Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Little Endian vs Big Endian]]></title>
        <id>https://www.lowlevel.dev/posts/little-big-endian/</id>
        <link href="https://www.lowlevel.dev/posts/little-big-endian/"/>
        <updated>2026-01-17T12:52:13.072Z</updated>
        <summary type="html"><![CDATA[You'd think after decades of computing we'd have settled on which end of a number goes first in memory. We haven't. Some processors put the...]]></summary>
        <content type="html"><![CDATA[<p>You'd think after decades of computing we'd have settled on which end of a number goes first in memory. We haven't. Some processors put the big byte first, some put the little byte first, and both camps have been stubbornly coexisting since the 1970s. If you're writing embedded code, this becomes your problem the moment two systems need to exchange data.</p>
<h2>What's a Byte Order?</h2>
<p>Consider the 32-bit integer <code>0x12345678</code>. It has four bytes: <code>0x12</code>, <code>0x34</code>, <code>0x56</code>, and <code>0x78</code>. When stored in memory, these bytes occupy four consecutive addresses. The question is: in what order?</p>
<p>Big endian stores the most significant byte first, at the lowest memory address. The most significant byte is the one that affects the value the most, just like the leftmost digit in a decimal number. In <code>0x12345678</code>, that's <code>0x12</code>. Changing it from <code>0x12</code> to <code>0x13</code> adds 16 million to the value. Changing the least significant byte (<code>0x78</code>) by the same amount only adds 1.</p>
<p>If our integer starts at address 0x1000, memory looks like this:</p>
<pre><code>Address:  0x1000  0x1001  0x1002  0x1003
Value:      12      34      56      78
</code></pre>
<p>The "big end", the byte with the most weight, comes first. This matches how we write numbers: left to right, most significant digit first.</p>
<p>Little endian stores the least significant byte first:</p>
<pre><code>Address:  0x1000  0x1001  0x1002  0x1003
Value:      78      56      34      12
</code></pre>
<p>The "little end" comes first. If you're staring at a memory dump trying to find <code>0x12345678</code>, you'll see <code>78 56 34 12</code> and wonder if something has gone horribly wrong (it hasn't).</p>
<p>The terms themselves come from Gulliver's Travels, where two factions go to war over whether to crack eggs at the big end or the little end. The computer science usage is a deliberate joke about arbitrary, religious disputes. Fitting, given how much trouble byte order has caused over the decades.</p>
<h2>Which Processors Use Which?</h2>
<p>Most processors you'll encounter in embedded work are little endian. The entire x86 and x86-64 family is little endian. ARM processors are predominantly little endian, though many ARM chips can operate in either mode (they're "bi-endian"). Most microcontrollers, including AVR, ESP32, STM32, and others based on ARM Cortex-M cores, are little endian.</p>
<p>Big endian is less common but far from extinct. Network protocols standardised on big endian decades ago, which is why it's often called "network byte order." This was arguably the right call since big endian matches how humans write numbers, but it means that virtually every little-endian machine (which is most of them) has to byte-swap data at the network boundary. Every TCP/IP stack in the world is doing this constantly.</p>
<p>Some PowerPC systems are big endian. Certain Motorola processors are big endian. And you'll encounter it in many file formats and communication protocols regardless of what processor you're running on.</p>
<h2>Why This Matters in Embedded Programming</h2>
<p>In high-level application programming, you can often ignore endianness entirely. The compiler handles it, and your integers just work. Embedded programming is different. You're frequently dealing with situations where byte order becomes explicit.</p>
<p>When you read data from a sensor over I2C or SPI, the sensor sends bytes in a specific order defined by its datasheet. If the sensor is big endian and your microcontroller is little endian, you can't just cast the received bytes to an integer and expect the right answer.</p>
<p>When you're parsing a binary protocol, say, reading packets from a GPS module or a CAN bus, the protocol specification defines the byte order. You need to decode the bytes correctly regardless of your processor's native order.</p>
<p>When you're writing to hardware registers that span multiple bytes, the peripheral's documentation will tell you the expected byte order. Get it wrong and you'll write garbage.</p>
<p>When you're storing data to flash or EEPROM that might be read by a different system, or sending data over a network, you need to agree on a byte order with the other end.</p>
<h2>Detecting Endianness</h2>
<p>You can determine your system's endianness at runtime:</p>
<pre><code>int is_little_endian(void) {
    uint16_t value = 0x0001;
    uint8_t *byte = (uint8_t *)&amp;value;
    return byte[0] == 0x01;  // If the first byte is 0x01, we're little endian
}
</code></pre>
<p>This works by storing a known value and then examining the first byte. In little endian, the least significant byte (0x01) is at the lowest address. In big endian, the most significant byte (0x00) would be there.</p>
<p>For compile-time detection, most compilers provide predefined macros. GCC and Clang offer <code>__BYTE_ORDER__</code>, and many embedded toolchains have their own equivalents.</p>
<h2>Converting Between Byte Orders</h2>
<p>The standard approach is to convert to a known byte order when data crosses a boundary, then convert back on the other side. Network programming uses <code>htons</code>, <code>htonl</code>, <code>ntohs</code>, and <code>ntohl</code> (host-to-network and network-to-host for short and long integers), but these aren't always available in embedded environments. You'll often write your own.</p>
<p>Here's a simple byte swap for a 16-bit value:</p>
<pre><code>uint16_t swap16(uint16_t value) {
    return (value &lt;&lt; 8) | (value &gt;&gt; 8);
}
</code></pre>
<p>And for 32-bit:</p>
<pre><code>uint32_t swap32(uint32_t value) {
    return ((value &gt;&gt; 24) &amp; 0x000000FF) |
           ((value &gt;&gt; 8)  &amp; 0x0000FF00) |
           ((value &lt;&lt; 8)  &amp; 0x00FF0000) |
           ((value &lt;&lt; 24) &amp; 0xFF000000);
}
</code></pre>
<p>Many compilers provide built-in functions for this. GCC has <code>__builtin_bswap16</code>, <code>__builtin_bswap32</code>, and <code>__builtin_bswap64</code>. What looks like a mess of shifts and masks becomes a single <code>REV</code> instruction on ARM, or <code>BSWAP</code> on x86.</p>
<h2>Building Values from Bytes: The Portable Way</h2>
<p>Rather than relying on casts and hoping the byte order works out, you can construct multi-byte values explicitly from their component bytes. This approach is portable and makes the byte order obvious in the code.</p>
<p>Reading a big-endian 16-bit value from a byte buffer:</p>
<pre><code>uint16_t read_be16(const uint8_t *buffer) {
    return ((uint16_t)buffer[0] &lt;&lt; 8) | buffer[1];
}
</code></pre>
<p>Reading a little-endian 16-bit value:</p>
<pre><code>uint16_t read_le16(const uint8_t *buffer) {
    return ((uint16_t)buffer[1] &lt;&lt; 8) | buffer[0];
}
</code></pre>
<p>The same pattern extends to larger values:</p>
<pre><code>uint32_t read_be32(const uint8_t *buffer) {
    return ((uint32_t)buffer[0] &lt;&lt; 24) |
           ((uint32_t)buffer[1] &lt;&lt; 16) |
           ((uint32_t)buffer[2] &lt;&lt; 8)  |
           buffer[3];
}

uint32_t read_le32(const uint8_t *buffer) {
    return ((uint32_t)buffer[3] &lt;&lt; 24) |
           ((uint32_t)buffer[2] &lt;&lt; 16) |
           ((uint32_t)buffer[1] &lt;&lt; 8)  |
           buffer[0];
}
</code></pre>
<p>And for writing:</p>
<pre><code>void write_be16(uint8_t *buffer, uint16_t value) {
    buffer[0] = (value &gt;&gt; 8) &amp; 0xFF;
    buffer[1] = value &amp; 0xFF;
}

void write_le16(uint8_t *buffer, uint16_t value) {
    buffer[0] = value &amp; 0xFF;
    buffer[1] = (value &gt;&gt; 8) &amp; 0xFF;
}
</code></pre>
<p>This might look like extra work, but it's actually cleaner than scattering byte-swap calls throughout your code, and it makes the expected byte order explicit at the point where data is read or written.</p>
<h2>A Practical Example: Parsing a Sensor Reading</h2>
<p>Say you're reading from a temperature sensor that sends a 16-bit signed value in big-endian format over I2C. The raw bytes arrive in a buffer:</p>
<pre><code>uint8_t buffer[2];
i2c_read(SENSOR_ADDR, buffer, 2);  // Read two bytes from the sensor

// Wrong approach on a little-endian system:
int16_t temp_wrong = *(int16_t *)buffer;  // Byte order is reversed!

// Correct approach:
int16_t temp_correct = (int16_t)((buffer[0] &lt;&lt; 8) | buffer[1]);
</code></pre>
<p>The cast approach treats the bytes in the processor's native order, which gives you a garbage value if that order doesn't match what the sensor sent. The explicit approach constructs the value correctly regardless of the processor's endianness.</p>
<p>For signed values, you might need to handle sign extension explicitly if you're building up from unsigned bytes:</p>
<pre><code>int16_t read_be_int16(const uint8_t *buffer) {
    uint16_t raw = ((uint16_t)buffer[0] &lt;&lt; 8) | buffer[1];
    return (int16_t)raw;  // Cast handles sign correctly
}
</code></pre>
<h2>Keeping It Sane</h2>
<p>Endianness bugs are nasty because they're silent. The code compiles. It runs. It doesn't crash. It just produces garbage, and sometimes that garbage looks almost plausible: off by a factor of 256 here, sign-flipped there. You can stare at the code for hours without seeing it.</p>
<p>A few practices help keep things manageable. Be explicit about byte order in code that crosses system boundaries. Use functions like <code>read_be32</code> instead of pointer casts. Document the expected byte order in comments or function names. Use fixed-width types (<code>uint16_t</code>, <code>int32_t</code>) rather than <code>int</code> or <code>short</code> when dealing with binary data, so you know exactly how many bytes you're working with. And when parsing a protocol or file format, keep the specification handy and double-check your byte order assumptions.</p>
<p>Byte order is one of those things that feels like it shouldn't matter in 2025. We've abstracted away so much complexity in computing. Surely we could have standardised this by now. But we haven't, and in embedded work, you're close enough to the metal that the abstraction isn't there to save you. Know your byte order. Be explicit about it in code. And when something's giving you impossible values, check endianness before you tear apart your hardware.</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-17T12:52:13.072Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[RAII in C]]></title>
        <id>https://www.lowlevel.dev/posts/raii-c/</id>
        <link href="https://www.lowlevel.dev/posts/raii-c/"/>
        <updated>2026-01-09T14:37:53.428Z</updated>
        <summary type="html"><![CDATA[You're debugging a production issue at 2am. Memory usage is climbing steadily, the system's getting slower, and somewhere in a 500-line func...]]></summary>
        <content type="html"><![CDATA[<p>You're debugging a production issue at 2am. Memory usage is climbing steadily, the system's getting slower, and somewhere in a 500-line function there's a <code>malloc</code> without a matching <code>free</code>. You've checked every early return. You've traced every error path. And you're thinking: there has to be a better way to do this.</p>
<p>There is. It's called RAII—"Resource Acquisition Is Initialisation." The pattern originated in C++, where it's built into the language. C doesn't have the language features that make RAII automatic, but the underlying concept is still valuable and you can apply it with a bit of discipline.</p>
<p>The core idea is simple: tie the lifetime of a resource to the lifetime of an object. When the object is created, it acquires the resource. When the object is destroyed, it releases the resource. No separate cleanup step to forget, no resources left dangling.</p>
<h2>The Problem RAII Solves</h2>
<p>Consider a function that opens a file, does some work, and closes it:</p>
<pre><code>int process_config(const char *filename) {
    FILE *f = fopen(filename, "r");
    if (!f) return -1;

    // Do some work...
    if (parse_failed) {
        return -1;  // Oops! We forgot to close the file.
    }

    // Do more work...
    if (validation_failed) {
        return -1;  // Forgot again.
    }

    fclose(f);
    return 0;
}
</code></pre>
<p>Every early return is a potential resource leak. You have to remember to call <code>fclose()</code> on every exit path. As functions grow more complex, with more error conditions and nested resources, this becomes a maintenance nightmare. You either end up with deeply nested code trying to avoid early returns, or a tangle of <code>goto</code> statements jumping to cleanup labels.</p>
<p>RAII addresses this by making cleanup automatic. The resource is released when execution leaves the scope, regardless of how it leaves—normal return, early return, or error.</p>
<h2>RAII in C++: The Automatic Version</h2>
<p>In C++, destructors make RAII straightforward. When an object goes out of scope, its destructor runs automatically:</p>
<pre><code>// C++ version - destructor handles cleanup
void process_config(const char *filename) {
    std::ifstream file(filename);  // Constructor opens the file

    if (parse_failed) return;      // Destructor closes file automatically
    if (validation_failed) return; // Destructor closes file automatically

    // Normal exit - destructor still closes file automatically
}
</code></pre>
<p>You literally cannot forget to close the file. The language handles it.</p>
<h2>C Doesn't Have Destructors</h2>
<p>C doesn't have destructors, so we can't get truly automatic cleanup. But we can still apply the RAII principle by being disciplined about how we structure code. The goal is to make resource lifetimes obvious and cleanup hard to forget.</p>
<p>There are a few approaches, each with trade-offs.</p>
<h2>Approach 1: The Cleanup Label Pattern</h2>
<p>This isn't pure RAII, but it's a common C idiom that achieves similar goals. You use a single cleanup section at the end of the function and <code>goto</code> to reach it from any error path:</p>
<pre><code>int process_config(const char *filename) {
    int result = -1;          // Assume failure
    FILE *f = NULL;           // Initialise to NULL so cleanup is safe
    char *buffer = NULL;

    // Acquire resources
    f = fopen(filename, "r");
    if (!f) goto cleanup;

    buffer = malloc(1024);
    if (!buffer) goto cleanup;

    // Do work...
    if (parse_failed) goto cleanup;

    // More work...
    if (validation_failed) goto cleanup;

    // Success
    result = 0;

cleanup:
    // Release resources in reverse order of acquisition
    free(buffer);       // free(NULL) is safe, does nothing
    if (f) fclose(f);   // fclose(NULL) is not safe, so we check

    return result;
}
</code></pre>
<p>This centralises cleanup in one place. You can't accidentally skip it because every exit path goes through the same label. The resources are initialised to safe values (NULL) at the top, so cleanup code can run safely even if acquisition failed partway through.</p>
<p>If you've ever looked at kernel code and wondered why Linus Torvalds, a man with strong opinions about code quality, tolerates <code>goto</code> statements everywhere, this is why. It's a deliberate pattern that's survived decades of battle-testing in one of the most critical codebases on Earth.</p>
<h2>Approach 2: Wrapper Structs with Init/Destroy Functions</h2>
<p>A more structured approach is to create types that bundle a resource with functions to acquire and release it. This is closer to the spirit of RAII:</p>
<pre><code>// file_handle.h
typedef struct {
    FILE *fp;
    int valid;
} FileHandle;

// Initialise the handle and open the file (acquisition)
int file_handle_init(FileHandle *handle, const char *filename, const char *mode) {
    handle-&gt;fp = fopen(filename, mode);
    handle-&gt;valid = (handle-&gt;fp != NULL);
    return handle-&gt;valid ? 0 : -1;
}

// Close the file and clean up (release)
void file_handle_destroy(FileHandle *handle) {
    if (handle-&gt;valid &amp;&amp; handle-&gt;fp) {
        fclose(handle-&gt;fp);
        handle-&gt;fp = NULL;
        handle-&gt;valid = 0;
    }
}

// Helper to check if the handle is usable
int file_handle_is_valid(const FileHandle *handle) {
    return handle-&gt;valid;
}
</code></pre>
<p>Now your function looks like this:</p>
<pre><code>int process_config(const char *filename) {
    FileHandle handle;

    if (file_handle_init(&amp;handle, filename, "r") != 0) {
        return -1;
    }

    // Do work with handle.fp...

    file_handle_destroy(&amp;handle);  // You still have to remember this
    return 0;
}
</code></pre>
<p>This is cleaner, but you still have to remember to call the destroy function. The benefit is that the pairing is obvious—<code>init</code> and <code>destroy</code> go together, and code review can easily spot a missing <code>destroy</code>.</p>
<h2>Approach 3: Scope-Bound Resource Management with GCC/Clang</h2>
<p>If you're using GCC or Clang, there's a feature that almost nobody talks about, buried in the compiler documentation, that gives you proper RAII in C. It's been there for years. I'm genuinely not sure why it isn't more widely known.</p>
<p>It's the <code>cleanup</code> attribute. It lets you specify a function to call automatically when a variable goes out of scope:</p>
<pre><code>// Define a cleanup function
static void auto_close_file(FILE **fp) {
    if (*fp) {
        fclose(*fp);
    }
}

// Macro to make it easy to use
#define AUTO_CLOSE __attribute__((cleanup(auto_close_file)))

int process_config(const char *filename) {
    AUTO_CLOSE FILE *f = fopen(filename, "r");
    if (!f) return -1;

    if (parse_failed) {
        return -1;  // f is automatically closed!
    }

    if (validation_failed) {
        return -1;  // f is automatically closed!
    }

    // Normal return - f is automatically closed!
    return 0;
}
</code></pre>
<p>This is proper RAII in C. The file is closed automatically when <code>f</code> goes out of scope, no matter how that happens.</p>
<p>Notice the cleanup function takes <code>FILE **</code>, not <code>FILE *</code>. That trips people up the first time. The cleanup function receives a pointer to the variable itself, so it can access (and modify) the variable's value.</p>
<p>You can create a family of these for different resource types:</p>
<pre><code>// For malloc'd memory
static void auto_free(void **ptr) {
    free(*ptr);
}
#define AUTO_FREE __attribute__((cleanup(auto_free)))

// For file descriptors
static void auto_close_fd(int *fd) {
    if (*fd &gt;= 0) close(*fd);
}
#define AUTO_CLOSE_FD __attribute__((cleanup(auto_close_fd)))

// For mutexes
static void auto_unlock(pthread_mutex_t **mutex) {
    pthread_mutex_unlock(*mutex);
}
#define AUTO_UNLOCK __attribute__((cleanup(auto_unlock)))
</code></pre>
<p>Usage becomes very clean:</p>
<pre><code>int do_something(void) {
    AUTO_FREE char *buffer = malloc(1024);
    AUTO_CLOSE FILE *f = fopen("data.txt", "r");
    AUTO_CLOSE_FD int sock = socket(AF_INET, SOCK_STREAM, 0);

    if (!buffer || !f || sock &lt; 0) {
        return -1;  // Everything cleaned up automatically
    }

    // Work with resources...

    return 0;  // Everything cleaned up automatically
}
</code></pre>
<p>The catch? It's a compiler extension, not standard C. If your code needs to compile on some obscure embedded compiler from 2003, you're out of luck. But if you're targeting GCC or Clang—which covers Linux, macOS, BSDs, and most ARM toolchains—you've had this superpower available the whole time.</p>
<h2>Approach 4: Scope Guards with Macros</h2>
<p>This next technique is either brilliant or cursed, depending on your tolerance for macro wizardry. I'll show it to you, but I'm not sure I'd recommend it.</p>
<p>You can use macros to create a scope that handles cleanup:</p>
<pre><code>#define SCOPED_FILE(var, filename, mode) \
    for (FILE *var = fopen(filename, mode), *_once = (FILE*)1; \
         _once &amp;&amp; var; \
         _once = NULL, fclose(var))

void process_config(const char *filename) {
    SCOPED_FILE(f, filename, "r") {
        // Use f here...
        // File is closed when this block exits
    }
    // f no longer exists here
}
</code></pre>
<p>The <code>for</code> loop runs exactly once (controlled by the <code>_once</code> variable), and the cleanup happens in the loop's increment expression. It works, but I'd reach for the cleanup attribute if it's available.</p>
<h2>The Underlying Principle</h2>
<p>Whichever approach you use, the principle remains the same: make resource lifetimes explicit, tie acquisition and release together, and structure your code so that cleanup is hard to skip. In C++, the language enforces this through destructors. In C, you enforce it through convention, discipline, and sometimes compiler extensions.</p>
<p>The cleanup attribute gives you the closest thing to automatic RAII in C. The goto-cleanup pattern is portable and battle-tested. Wrapper structs with init/destroy functions make the resource lifecycle explicit even if the calls aren't automatic. Pick the approach that fits your constraints and stick with it consistently.</p>
<p>C doesn't hold your hand. It never has. But that doesn't mean you have to juggle resources manually and hope you never drop one. RAII is a principle, not a language feature, and with a bit of structure, you can make cleanup something you rarely think about because it's already handled.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-09T14:37:53.428Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[How Your Code Talks to Hardware (Memory-Mapped I/O)]]></title>
        <id>https://www.lowlevel.dev/posts/memory-mapped-io/</id>
        <link href="https://www.lowlevel.dev/posts/memory-mapped-io/"/>
        <updated>2026-01-08T16:26:32.114Z</updated>
        <summary type="html"><![CDATA[There's something almost magical about the moment you realise that GPIOA->ODR |= (1 << 5) isn't just moving bits around in memory—it's physi...]]></summary>
        <content type="html"><![CDATA[<p>There's something almost magical about the moment you realise that <code>GPIOA-&gt;ODR |= (1 &lt;&lt; 5)</code> isn't just moving bits around in memory—it's physically changing voltage on a wire, photons leaving an LED, the real world responding to your code.</p>
<p>But how does writing to an address actually <em>do</em> something?</p>
<h2>The Core Idea</h2>
<p>Your microcontroller has an address space. On a 32-bit chip, this runs from 0x00000000 to 0xFFFFFFFF. Some addresses point to RAM, where your variables live. Some point to flash, where your program code lives.</p>
<p>And some addresses don't point to memory at all. They point to <em>hardware</em>. Read that again if you're coming from application development (as I am), because this probably sounds strange.</p>
<p>When you read from or write to one of these special addresses, you're communicating with a peripheral. Writing a value might turn on an LED, change a timer's speed, or send a byte out of a serial port. Reading a value might tell you whether a button is pressed or whether a transmission has completed.</p>
<p>This is memory-mapped I/O. Hardware peripherals are "mapped" into the memory address space, and you interact with them using ordinary reads and writes.</p>
<h2>A Concrete Example: Turning on an LED</h2>
<p>Say you have an LED connected to pin 5 of GPIO port A on an STM32 microcontroller. The GPIO peripheral has several registers, and the datasheet tells you that GPIO port A's registers start at address 0x40020000. A register is just a small piece of memory inside the peripheral that controls or reflects some aspect of its behaviour. The Output Data Register (ODR), which controls the pin states, is at offset 0x14 from that base.</p>
<p>In C:</p>
<pre><code>// The ODR register for GPIOA is at address 0x40020014.
// We cast this address to a pointer, then dereference it so we can
// read and write it like a variable.
#define GPIOA_ODR (*(volatile uint32_t *)0x40020014)

// Turn on the LED by setting bit 5
GPIOA_ODR |= (1 &lt;&lt; 5);

// Turn off the LED by clearing bit 5
GPIOA_ODR &amp;= ~(1 &lt;&lt; 5);
</code></pre>
<p>When you write <code>GPIOA_ODR |= (1 &lt;&lt; 5)</code>, the compiler generates a read from address 0x40020014, an OR operation, and a write back to that address. That write physically changes the voltage on pin 5, which lights up your LED.</p>
<h2>Why "Volatile" Matters</h2>
<p>Notice the <code>volatile</code> keyword. This is critical for MMIO, and leaving it out will cause bugs.</p>
<p>The compiler tries to optimise your code. One optimisation is caching values in CPU registers rather than reading from memory repeatedly. Without <code>volatile</code>, the compiler becomes an overly helpful optimiser that's about to ruin your day.</p>
<p>Consider this loop that waits for a button press:</p>
<pre><code>// Wait until bit 0 of the input register goes high
while ((GPIOA_IDR &amp; (1 &lt;&lt; 0)) == 0) {
    // Waiting...
}
</code></pre>
<p>The compiler looks at your loop, sees you reading the same address repeatedly, and thinks: "Why read this a thousand times? I'll just cache it." Reasonable! Except the hardware doesn't care what the compiler thinks. Your code effectively becomes:</p>
<pre><code>// BAD: Compiler "optimises" by caching the value
uint32_t cached_value = GPIOA_IDR;  // Read once
while ((cached_value &amp; (1 &lt;&lt; 0)) == 0) {
    // This loops forever! cached_value never updates,
    // even when the button is pressed.
}
</code></pre>
<p>But GPIOA_IDR is a hardware register. Its value changes when someone presses the button. Your code would never see that change because it's looking at a stale copy.</p>
<p>The <code>volatile</code> keyword tells the compiler: "This value can change outside the program's control. Read it fresh every single time." With <code>volatile</code>, the compiler generates an actual memory read on every loop iteration.</p>
<h2>Structuring Register Access</h2>
<p>Writing raw addresses everywhere gets messy. A cleaner approach is to define a struct that matches the peripheral's register layout:</p>
<pre><code>// This struct must match the hardware register layout exactly.
// Each field corresponds to a register at a specific offset.
// The order matters: the compiler lays out fields sequentially,
// so MODER is at offset 0, OTYPER at offset 4, and so on.
typedef struct {
    volatile uint32_t MODER;    // Offset 0x00: Mode register
    volatile uint32_t OTYPER;   // Offset 0x04: Output type register
    volatile uint32_t OSPEEDR;  // Offset 0x08: Output speed register
    volatile uint32_t PUPDR;    // Offset 0x0C: Pull-up/pull-down register
    volatile uint32_t IDR;      // Offset 0x10: Input data register
    volatile uint32_t ODR;      // Offset 0x14: Output data register
    volatile uint32_t BSRR;     // Offset 0x18: Bit set/reset register
    volatile uint32_t LCKR;     // Offset 0x1C: Lock register
    volatile uint32_t AFRL;     // Offset 0x20: Alternate function low
    volatile uint32_t AFRH;     // Offset 0x24: Alternate function high
} GPIO_TypeDef;

// Create pointers to each GPIO port at their base addresses
#define GPIOA ((GPIO_TypeDef *)0x40020000)
#define GPIOB ((GPIO_TypeDef *)0x40020400)
#define GPIOC ((GPIO_TypeDef *)0x40020800)
</code></pre>
<p>Now you can write:</p>
<pre><code>GPIOA-&gt;ODR |= (1 &lt;&lt; 5);   // Set pin 5 high

if (GPIOB-&gt;IDR &amp; (1 &lt;&lt; 0)) {
    // Pin 0 on port B is high
}
</code></pre>
<p>This is exactly what vendor-provided header files like <code>stm32f4xx.h</code> give you. The manufacturer has already turned the datasheet into C structures—which, when you think about it, is a pretty wild artefact. Thousands of pages of hardware documentation, distilled into structs and macros.</p>
<h2>Read-Modify-Write: A Subtle Trap</h2>
<p>When you write <code>GPIOA-&gt;ODR |= (1 &lt;&lt; 5)</code>, three things happen: read the current value, OR in your bit, write the result back. This is called read-modify-write, and it can cause problems.</p>
<p>If an interrupt fires between the read and the write, and the interrupt handler modifies the same register, your write will overwrite the handler's changes.</p>
<p>The read-modify-write trap catches everyone eventually. My first encounter was a motor controller that would occasionally ignore commands, but only when the system was under load. Took me ages to realise an interrupt was racing against my main loop.</p>
<p>Many peripherals provide registers that avoid this. On STM32, the GPIO has a Bit Set/Reset Register (BSRR) that lets you set or clear individual pins with a single write:</p>
<pre><code>// Set pin 5 high. This is a single write, no read required.
// Only pin 5 is affected; other pins are unchanged.
GPIOA-&gt;BSRR = (1 &lt;&lt; 5);

// Set pin 5 low. The lower 16 bits of BSRR set pins,
// the upper 16 bits clear pins. Pin 5's clear bit is bit 21 (5 + 16).
GPIOA-&gt;BSRR = (1 &lt;&lt; 21);
</code></pre>
<p>This is faster and avoids race conditions with interrupt handlers. There's something deeply satisfying when hardware designers give you atomic operations like this. They've been bitten by the same bugs and decided to solve them in silicon.</p>
<h2>A Complete Example: Blinking an LED</h2>
<p>Here's a standalone example showing what it takes to blink an LED on PA5 (port A, pin 5) on an STM32F4:</p>
<pre><code>#include &lt;stdint.h&gt;

// Register structures (simplified from the full definitions)
typedef struct {
    volatile uint32_t CR;
    volatile uint32_t PLLCFGR;
    volatile uint32_t CFGR;
    volatile uint32_t CIR;
    volatile uint32_t AHB1RSTR;
    volatile uint32_t AHB2RSTR;
    volatile uint32_t AHB3RSTR;
    volatile uint32_t RESERVED0;
    volatile uint32_t APB1RSTR;
    volatile uint32_t APB2RSTR;
    volatile uint32_t RESERVED1[2];
    volatile uint32_t AHB1ENR;   // Offset 0x30: enables clocks to peripherals
} RCC_TypeDef;

typedef struct {
    volatile uint32_t MODER;
    volatile uint32_t OTYPER;
    volatile uint32_t OSPEEDR;
    volatile uint32_t PUPDR;
    volatile uint32_t IDR;
    volatile uint32_t ODR;
    volatile uint32_t BSRR;
} GPIO_TypeDef;

#define RCC   ((RCC_TypeDef *)0x40023800)
#define GPIOA ((GPIO_TypeDef *)0x40020000)

// Simple delay loop. The 'volatile' prevents the compiler from
// optimising away the entire loop.
void delay(volatile uint32_t count) {
    while (count--);
}

int main(void) {
    // Step 1: Enable the clock to GPIO port A.
    // Peripherals are powered down by default to save energy.
    // Bit 0 of AHB1ENR controls the GPIOA clock.
    // Note: This is a read-modify-write, but there's no atomic alternative
    // for clock enables. Safe here since interrupts aren't enabled yet.
    RCC-&gt;AHB1ENR |= (1 &lt;&lt; 0);

    // Step 2: Configure PA5 as a general-purpose output.
    // The MODER register uses 2 bits per pin:
    //   00 = input
    //   01 = general-purpose output
    //   10 = alternate function
    //   11 = analog
    // Pin 5 is controlled by bits 10 and 11 (pin number * 2).
    GPIOA-&gt;MODER &amp;= ~(0x3 &lt;&lt; 10);  // Clear both bits first
    GPIOA-&gt;MODER |= (0x1 &lt;&lt; 10);   // Set to output mode

    // Step 3: Blink forever
    while (1) {
        // Set PA5 high (LED on).
        // Writing to the lower 16 bits of BSRR sets pins.
        GPIOA-&gt;BSRR = (1 &lt;&lt; 5);
        delay(500000);

        // Set PA5 low (LED off).
        // Writing to the upper 16 bits of BSRR clears pins.
        // Bit 21 = bit 5 + 16.
        GPIOA-&gt;BSRR = (1 &lt;&lt; 21);
        delay(500000);
    }

    return 0;
}
</code></pre>
<p>Every step here is a write to a memory-mapped register. Enabling the clock, configuring the pin mode, toggling the output. It's all done by writing specific values to specific addresses.</p>
<h2>Why This Matters</h2>
<p>Understanding MMIO is foundational. When you use a HAL function like <code>HAL_GPIO_WritePin()</code>, it's doing exactly this underneath. When you configure a timer, a DMA controller, or an ADC, you're manipulating registers through MMIO.</p>
<p>Knowing what's really happening lets you configure peripherals that your HAL doesn't support, debug problems when abstractions behave unexpectedly, optimise critical code paths, and understand why details like <code>volatile</code> and register access order matter.</p>
<p>The address space is your interface to the hardware. Every peripheral you'll ever configure, every sensor you'll read, it all comes down to reading and writing specific addresses. Memory-mapped I/O isn't just a technique. It's the bridge between your code and the physical world.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-08T16:26:32.114Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Headers and Implementation Files in C]]></title>
        <id>https://www.lowlevel.dev/posts/c-headers/</id>
        <link href="https://www.lowlevel.dev/posts/c-headers/"/>
        <updated>2026-01-07T10:11:14.065Z</updated>
        <summary type="html"><![CDATA[As C programs grow beyond a single file, you need a way to share code between files without duplicating it everywhere. This is where the spl...]]></summary>
        <content type="html"><![CDATA[<p>As C programs grow beyond a single file, you need a way to share code between files without duplicating it everywhere. This is where the split between header files and implementation files comes in. It's a simple convention but one that may feel strange when coming from languages that don't have header files (I'm looking at you JS, C#, Python).</p>
<h2>The Problem We're Solving</h2>
<p>Imagine you've written a useful function for calculating distances:</p>
<pre><code>// In geometry.c
float distance(float x1, float y1, float x2, float y2) {
    float dx = x2 - x1;
    float dy = y2 - y1;
    return sqrt(dx * dx + dy * dy);
}
</code></pre>
<p>Now you want to use this function in another file, <code>main.c</code>. The C compiler processes each file independently before the linker combines them, so when it compiles <code>main.c</code>, it has no idea that <code>distance</code> exists over in <code>geometry.c</code>. You need to tell the compiler about the function before you can call it.</p>
<p>You could just copy the declaration into <code>main.c</code>:</p>
<pre><code>// In main.c
float distance(float x1, float y1, float x2, float y2);  // Declaration

int main(void) {
    float d = distance(0, 0, 3, 4);  // Now the compiler knows about it
    return 0;
}
</code></pre>
<p>This works, but it doesn't scale. If you use <code>distance</code> in ten different files, you've got ten copies of the declaration. If you later change the function signature — say, adding a third dimension — you have to update all ten files. Miss one and you've got a bug that might not show up until runtime.</p>
<p>Header files solve this by giving you one place to put declarations that multiple files can share.</p>
<h2>Headers: The Public Interface</h2>
<p>A header file (<code>.h</code>) contains declarations: function prototypes, type definitions, macros, and extern variable declarations. It describes what exists without providing the actual implementation.</p>
<pre><code>// geometry.h
#ifndef GEOMETRY_H
#define GEOMETRY_H

// Function declarations - the public interface
float distance(float x1, float y1, float x2, float y2);
float triangle_area(float base, float height);
float circle_area(float radius);

#endif
</code></pre>
<p>This file says "these functions exist and here's how to call them." It doesn't say how they work. That's not the header's job.</p>
<h2>Implementation Files: The Actual Code</h2>
<p>An implementation file (<code>.c</code>) contains definitions: the actual function bodies, variable initialisations, and any private helper functions. It includes its own header to ensure the declarations and definitions stay in sync.</p>
<pre><code>// geometry.c
#include &lt;math.h&gt;
#include "geometry.h"  // Include our own header

// Private helper function - only visible in this file
static float square(float x) {
    return x * x;
}

// Public function definitions
float distance(float x1, float y1, float x2, float y2) {
    return sqrt(square(x2 - x1) + square(y2 - y1));
}

float triangle_area(float base, float height) {
    return 0.5f * base * height;
}

float circle_area(float radius) {
    return 3.14159f * square(radius);
}
</code></pre>
<p>Notice that <code>geometry.c</code> includes <code>geometry.h</code>. This is good practice because the compiler will catch any mismatch between your declarations and definitions. If you declare <code>float distance(float, float, float, float)</code> in the header but define <code>double distance(...)</code> in the source file, the compiler will complain.</p>
<h2>Using the Header</h2>
<p>Any file that wants to use your geometry functions just includes the header:</p>
<pre><code>// main.c
#include &lt;stdio.h&gt;
#include "geometry.h"  // Now we know about distance, triangle_area, etc.

int main(void) {
    float d = distance(0, 0, 3, 4);
    printf("Distance: %.2f\n", d);  // Prints 5.00

    float area = circle_area(10);
    printf("Circle area: %.2f\n", area);

    return 0;
}
</code></pre>
<p>The <code>#include "geometry.h"</code> directive tells the preprocessor to paste the contents of that file right here. After preprocessing, the compiler sees the function declarations and knows how to handle the calls. Later, the linker connects these calls to the actual function definitions in <code>geometry.o</code>.</p>
<h2>Header Guards: Preventing Double Inclusion</h2>
<p>You might have noticed these lines wrapping the header content:</p>
<pre><code>#ifndef GEOMETRY_H
#define GEOMETRY_H
// ... header contents ...
#endif
</code></pre>
<p>These are header guards, and they solve a subtle but important problem: what happens if a header gets included twice?</p>
<p>This can happen more easily than you might think. Say <code>main.c</code> includes both <code>geometry.h</code> and <code>physics.h</code>, and <code>physics.h</code> itself includes <code>geometry.h</code> because it uses some geometry functions internally:</p>
<pre><code>// main.c
#include "geometry.h"
#include "physics.h"  // This also includes geometry.h internally
</code></pre>
<p>Without header guards, the contents of <code>geometry.h</code> would be pasted twice. For function declarations, this might just cause a warning. But if the header defines a struct or a typedef, you'd get a "redefinition" error — the compiler sees the same type being defined twice. Header guards prevent this.</p>
<p>The first time the preprocessor encounters <code>#include "geometry.h"</code>, it processes these lines:</p>
<pre><code>#ifndef GEOMETRY_H      // Is GEOMETRY_H undefined? Yes, so continue.
#define GEOMETRY_H      // Define GEOMETRY_H (now it exists).
// ... contents ...     // Process the header contents.
#endif                  // End of the conditional block.
</code></pre>
<p>If the same header gets included again later in the same translation unit:</p>
<pre><code>#ifndef GEOMETRY_H      // Is GEOMETRY_H undefined? No, it's defined now.
                        // Skip everything until #endif.
#endif
</code></pre>
<p>The second inclusion is effectively empty. The header contents only appear once, no matter how many times the header is included.</p>
<p>The name you use (like <code>GEOMETRY_H</code>) should be unique. The convention is to use the filename in uppercase with underscores replacing dots and any path separators. Some projects add a prefix or suffix to further reduce collision risk.</p>
<h2>Angle Brackets vs Quotes</h2>
<p>You'll notice two styles of include:</p>
<pre><code>#include &lt;stdio.h&gt;      // Angle brackets
#include "geometry.h"   // Quotes
</code></pre>
<p>Angle brackets tell the preprocessor to look in the system include directories — places where the standard library headers live. Quotes tell it to look in the current directory first (or directories you specify), then fall back to system directories.</p>
<p>The rule of thumb: use angle brackets for standard library and system headers, quotes for your own headers and third-party libraries you've added to your project.</p>
<h2>What Goes Where?</h2>
<p>Here's a quick guide for deciding what belongs in headers versus implementation files.</p>
<p>Headers should contain function declarations (prototypes), struct and union definitions, typedef declarations, enum definitions, macro definitions, extern variable declarations, and static inline function definitions (small functions meant to be inlined).</p>
<p>Implementation files should contain function definitions (the actual code), static (file-private) functions and variables, and the definitions of extern variables declared in headers.</p>
<p>The principle is that headers describe the interface while implementation files provide the behaviour. Anything that other files need to know about goes in the header. Anything that's an internal implementation detail stays in the <code>.c</code> file.</p>
<h2>A Complete Example</h2>
<p>Let's put it all together with a small string utilities module.</p>
<pre><code>// string_utils.h
#ifndef STRING_UTILS_H
#define STRING_UTILS_H

#include &lt;stddef.h&gt;  // For size_t

// Check if a string is empty or NULL
int string_is_empty(const char *str);

// Count occurrences of a character in a string
size_t string_count_char(const char *str, char c);

// Check if string starts with a given prefix
int string_starts_with(const char *str, const char *prefix);

#endif
</code></pre>
<pre><code>// string_utils.c
#include &lt;string.h&gt;
#include "string_utils.h"

int string_is_empty(const char *str) {
    return str == NULL || str[0] == '\0';
}

size_t string_count_char(const char *str, char c) {
    if (str == NULL) return 0;

    size_t count = 0;
    while (*str) {
        if (*str == c) count++;
        str++;
    }
    return count;
}

int string_starts_with(const char *str, const char *prefix) {
    if (str == NULL || prefix == NULL) return 0;
    return strncmp(str, prefix, strlen(prefix)) == 0;
}
</code></pre>
<pre><code>// main.c
#include &lt;stdio.h&gt;
#include "string_utils.h"

int main(void) {
    const char *text = "hello, world";

    if (!string_is_empty(text)) {
        printf("The string has %zu 'l' characters\n",
               string_count_char(text, 'l'));  // Prints 3
    }

    if (string_starts_with(text, "hello")) {
        printf("It starts with 'hello'\n");
    }

    return 0;
}
</code></pre>
<p>To compile this, you'd typically run:</p>
<pre><code>gcc -c string_utils.c -o string_utils.o
gcc -c main.c -o main.o
gcc string_utils.o main.o -o program
</code></pre>
<p>Or more simply:</p>
<pre><code>gcc string_utils.c main.c -o program
</code></pre>
<p>The compiler compiles each <code>.c</code> file into an object file, then the linker combines them. The header file is never compiled directly — it's just pasted into whichever <code>.c</code> files include it.</p>
<h2>Why This Matters</h2>
<p>The header/implementation split does several things for you. It keeps your public interface separate from implementation details, so users of your code only see what they need. It enables separate compilation, so changing one <code>.c</code> file doesn't require recompiling everything. It provides a natural place for documentation — the header shows what functions are available and what they do. And it scales cleanly as projects grow, because dependencies are explicit and manageable.</p>
<p>This pattern is so fundamental to C that you'll see it everywhere: in the standard library, in operating system APIs, in every library you'll ever use. Get comfortable with it, and structuring C projects becomes straightforward.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-07T10:11:14.065Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Functions in C]]></title>
        <id>https://www.lowlevel.dev/posts/c-functions/</id>
        <link href="https://www.lowlevel.dev/posts/c-functions/"/>
        <updated>2026-01-05T10:06:34.942Z</updated>
        <summary type="html"><![CDATA[Functions are the building blocks of any C program. They let you create a chunk of code, give it a name, and call it whenever you need that...]]></summary>
        <content type="html"><![CDATA[<p>Functions are the building blocks of any C program. They let you create a chunk of code, give it a name, and call it whenever you need that behaviour. Understanding how functions work is fundamental to writing C.</p>
<h2>The Basic Structure of a Function</h2>
<p>A function in C has four parts: a return type, a name, a parameter list, and a body.</p>
<pre><code>int add(int a, int b) {
    int result = a + b;
    return result;
}
</code></pre>
<ul>
<li>
<p>The <strong>return type</strong> comes first. In this case it's <code>int</code>, meaning the function will produce an integer value that gets sent back to whoever called it. If a function doesn't need to return anything, you use <code>void</code> as the return type.</p>
</li>
<li>
<p>The <strong>name</strong> comes next. This is how you'll refer to the function when you want to call it. Function names follow the same rules as variable names: they can contain letters, numbers, and underscores, but can't start with a number.</p>
</li>
<li>
<p>The <strong>parameter list</strong> sits inside the parentheses. These are the inputs to the function. There are the values that get passed in when you call it. Each parameter has a type and a name. If a function takes no parameters, you can write <code>void</code> inside the parentheses (or leave them empty, though <code>void</code> is more explicit).</p>
</li>
<li>
<p>The <strong>body</strong> is everything between the curly braces. This is the code that runs when the function is called. If the function has a non-void return type, the body must include a <code>return</code> statement that provides a value of the correct type.</p>
</li>
</ul>
<h2>Calling a Function</h2>
<p>Once you've defined a function, you call it by using its name followed by arguments in parentheses:</p>
<pre><code>int sum = add(5, 3);  // sum is now 8
</code></pre>
<p>The arguments <code>5</code> and <code>3</code> get copied into the parameters <code>a</code> and <code>b</code>. The function runs, computes the result, and returns <code>8</code>. That returned value gets assigned to <code>sum</code>.</p>
<p>This copying is important. C passes arguments by value, meaning the function receives copies of the values, not the original variables. If you modify a parameter inside the function, the original variable outside the function is unaffected:</p>
<pre><code>void try_to_modify(int x) {
    x = 100;  // This only changes the local copy
}

int main(void) {
    int num = 5;
    try_to_modify(num);
    // num is still 5 here, not 100
    return 0;
}
</code></pre>
<p>If you need a function to modify something outside itself, you pass a pointer to it. But that's a topic for another post.</p>
<h2>Return Types and void</h2>
<p>The return type specifies what kind of value the function produces. It can be any type: <code>int</code>, <code>float</code>, <code>char</code>, a pointer, a struct, and so on.</p>
<pre><code>// Returns an integer
int square(int x) {
    return x * x;
}

// Returns a floating-point number
float divide(float a, float b) {
    return a / b;
}

// Returns a pointer to a character (a string)
char *get_greeting(void) {
    return "Hello, world!";
}
</code></pre>
<p>When a function doesn't need to return anything, you declare it with <code>void</code>:</p>
<pre><code>void print_hello(void) {
    printf("Hello!\n");
    // No return statement needed, or you can write: return;
}
</code></pre>
<p>You can include a bare <code>return;</code> statement in a void function to exit early, but you don't have to. The function will return automatically when it reaches the closing brace.</p>
<p>One thing to watch out for: if you declare a function with a non-void return type, you must return a value on every code path. If you forget, the compiler might warn you, but the behaviour is undefined — the caller will receive garbage.</p>
<pre><code>int risky(int x) {
    if (x &gt; 0) {
        return x * 2;
    }
    // Oops! What happens if x &lt;= 0? No return statement.
    // This is undefined behaviour.
}
</code></pre>
<h2>Parameters: Inputs to Your Function</h2>
<p>Parameters let you pass data into a function. You can have as many as you need, separated by commas:</p>
<pre><code>void print_rectangle_info(int width, int height, const char *label) {
    int area = width * height;
    printf("%s: %d x %d = %d\n", label, width, height, area);
}
</code></pre>
<p>Each parameter is essentially a local variable that gets initialised with the value you pass when calling the function.</p>
<p>If a function takes no parameters, you should write <code>void</code> in the parameter list:</p>
<pre><code>int get_answer(void) {
    return 42;
}
</code></pre>
<p>There's a subtle difference between <code>int get_answer(void)</code> and <code>int get_answer()</code>. The first explicitly says "this function takes no arguments." The second, in C (unlike C++), means "this function takes an unspecified number of arguments" — it's an old-style declaration that doesn't prototype the parameters. For clarity and safety, always use <code>void</code> when you mean no parameters.</p>
<h2>Declaration vs Definition</h2>
<p>This is a distinction that trips up newcomers, but it's important for understanding how C programs are structured.</p>
<p>A function definition is the complete function: return type, name, parameters, and body. It's the actual implementation.</p>
<pre><code>// This is a definition - it includes the body
int multiply(int a, int b) {
    return a * b;
}
</code></pre>
<p>A function declaration (also called a prototype) just tells the compiler that a function exists and what its signature looks like. It doesn't include the body:</p>
<pre><code>// This is a declaration - no body, ends with a semicolon
int multiply(int a, int b);
</code></pre>
<p>Why would you need both? Because the C compiler processes your code from top to bottom. If you try to call a function before the compiler has seen it, it won't know what parameters it takes or what it returns.</p>
<pre><code>#include &lt;stdio.h&gt;

int main(void) {
    int result = multiply(3, 4);  // Error! Compiler hasn't seen multiply yet.
    printf("%d\n", result);
    return 0;
}

int multiply(int a, int b) {
    return a * b;
}
</code></pre>
<p>You have two options. Either move the function definition above main so the compiler sees it first, or provide a declaration before main and put the definition wherever you like:</p>
<pre><code>#include &lt;stdio.h&gt;

// Declaration: tells the compiler what multiply looks like
int multiply(int a, int b);

int main(void) {
    int result = multiply(3, 4);  // Now this works
    printf("%d\n", result);
    return 0;
}

// Definition: the actual implementation
int multiply(int a, int b) {
    return a * b;
}
</code></pre>
<p>For small programs, you might just order your functions so each is defined before it's called. For larger programs, the declaration/definition split becomes essential — you put declarations in header files and definitions in source files. That way, any source file that includes the header knows about the function and can call it, even though the actual code lives elsewhere.</p>
<h2>The main Function: Where It All Begins</h2>
<p>Every C program must have a function called <code>main</code>. This is the entry point — when you run your program, execution starts at the first line of main.</p>
<p>The <code>main</code> function is special in several ways. It's called by the operating system, not by your code. Its return type is always <code>int</code>, and the value you return becomes the program's exit status: <code>0</code> conventionally means success, and non-zero values indicate some kind of error.</p>
<p>There are two valid signatures for main:</p>
<pre><code>// Version 1: takes no arguments
int main(void) {
    // Your program here
    return 0;
}

// Version 2: takes command-line arguments
int main(int argc, char *argv[]) {
    // argc is the number of arguments
    // argv is an array of strings containing the arguments
    return 0;
}
</code></pre>
<p>The second version gives you access to command-line arguments. If the user runs <code>./myprogram hello world</code>, then <code>argc</code> would be 3 (the program name plus two arguments), and <code>argv</code> would contain <code>{"./myprogram", "hello", "world"}</code>.</p>
<p>You might occasionally see <code>void main()</code> in old code or tutorials. This is non-standard. The correct return type for main is <code>int</code>. Some compilers accept <code>void main()</code>, but it's not portable and you shouldn't use it.</p>
<p>One more quirk: main is the only function in C where you can omit the return statement. If you don't explicitly return a value, main implicitly returns <code>0</code>. This was added in the C99 standard. I'd still recommend including <code>return 0;</code> for clarity, but it's not strictly required.</p>
<h2>Why Use Functions?</h2>
<p>Functions exist to solve several problems at once.</p>
<p>Code reuse is the obvious benefit. If you need to perform the same operation in multiple places, you write it once as a function and call it wherever needed. If you later need to fix a bug or change the behaviour, you update one place rather than hunting through your code for every copy.</p>
<pre><code>// Without a function: repeated code
float area1 = 3.14159 * radius1 * radius1;
float area2 = 3.14159 * radius2 * radius2;
float area3 = 3.14159 * radius3 * radius3;

// With a function: write once, use anywhere
float circle_area(float radius) {
    return 3.14159f * radius * radius;
}

float area1 = circle_area(radius1);
float area2 = circle_area(radius2);
float area3 = circle_area(radius3);
</code></pre>
<p>Abstraction is equally important. Functions let you hide complexity behind a simple interface. When you call <code>printf</code>, you don't need to know how it formats strings or talks to the terminal. You just pass it arguments and it does its job. You can apply the same principle in your own code: write a function that handles the messy details, give it a clear name, and then use it without thinking about what's inside.</p>
<p>Organisation follows naturally. Functions let you break a large program into smaller, manageable pieces. Each function handles one task. This makes code easier to read, easier to test, and easier to maintain. Instead of one long main function with hundreds of lines, you have a collection of focused functions, each doing one thing well.</p>
<pre><code>int main(void) {
    initialise_system();
    load_configuration();

    while (running) {
        process_input();
        update_state();
        render_output();
    }

    cleanup();
    return 0;
}
</code></pre>
<p>You can understand the structure of this program without reading the implementation of each function. The names tell you what happens at each step.</p>
<h2>The Costs of Functions</h2>
<p>Functions aren't free, though the costs are usually small.</p>
<p>There's a calling overhead. When you call a function, the CPU has to save some state, jump to the function's code, pass the arguments, execute the function, and then return. For tiny operations called millions of times in a tight loop, this overhead can matter. In such cases, you might inline the code manually, use the <code>inline</code> keyword as a hint to the compiler, or (more commonly) trust the compiler to make sensible optimisations.</p>
<p>Arguments are copied. Because C passes by value, each argument is copied into the function's parameter. For small types like <code>int</code> or <code>float</code>, this is trivial. For large structs, it can be expensive. That's why you'll often see functions take pointers to large structs rather than the structs themselves:</p>
<pre><code>// Expensive: copies the entire struct
void process_data(struct LargeData data);

// Cheaper: copies only a pointer (8 bytes on 64-bit systems)
void process_data(const struct LargeData *data);
</code></pre>
<p>Debugging can sometimes be slightly harder when code is spread across many functions, because you have to trace through multiple call sites. But this is usually outweighed by the clarity that good function decomposition provides.</p>
<p>In practice, you should almost always prefer using functions. The benefits of clarity, reuse, and maintainability far outweigh the minor performance costs. Modern compilers are very good at optimising function calls, and you should only avoid them when profiling shows a genuine bottleneck.</p>
<h2>Putting It All Together</h2>
<p>Here's a complete example that demonstrates declarations, definitions, multiple functions, and main:</p>
<pre><code>#include &lt;stdio.h&gt;

// Function declarations (prototypes)
// These tell the compiler what functions exist and their signatures
float celsius_to_fahrenheit(float celsius);
float fahrenheit_to_celsius(float fahrenheit);
void print_conversion(float temp, char from_unit);

int main(void) {
    float temp = 100.0f;

    // Convert boiling point of water both ways
    print_conversion(temp, 'C');
    print_conversion(212.0f, 'F');

    return 0;
}

// Function definitions (implementations)

float celsius_to_fahrenheit(float celsius) {
    return (celsius * 9.0f / 5.0f) + 32.0f;
}

float fahrenheit_to_celsius(float fahrenheit) {
    return (fahrenheit - 32.0f) * 5.0f / 9.0f;
}

void print_conversion(float temp, char from_unit) {
    if (from_unit == 'C') {
        float result = celsius_to_fahrenheit(temp);
        printf("%.1f°C = %.1f°F\n", temp, result);
    } else if (from_unit == 'F') {
        float result = fahrenheit_to_celsius(temp);
        printf("%.1f°F = %.1f°C\n", temp, result);
    } else {
        printf("Unknown unit: %c\n", from_unit);
    }
}
</code></pre>
<p>The declarations at the top let us define main before the other functions. Each function has a single, clear purpose. The main function reads almost like prose — you can see what the program does without diving into implementation details. This is what good use of functions looks like.</p>
<p>Functions are simple in concept but foundational in practice. Master them, and you have the tools to write clear, organised, maintainable C code.</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-05T10:06:34.942Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Design Patterns in C: Adapter Pattern]]></title>
        <id>https://www.lowlevel.dev/posts/adapter-pattern/</id>
        <link href="https://www.lowlevel.dev/posts/adapter-pattern/"/>
        <updated>2026-01-04T17:37:24.739Z</updated>
        <summary type="html"><![CDATA[The adapter pattern solves a common problem: you have some code that expects one interface, and some other code that provides a different in...]]></summary>
        <content type="html"><![CDATA[<p>The adapter pattern solves a common problem: you have some code that expects one interface, and some other code that provides a different interface, and you need them to work together without modifying either. The adapter sits in the middle, translating between the two.</p>
<p>In object-oriented languages, this usually involves classes and inheritance. In C, we achieve the same thing with function pointers and structs. The principle is identical — only the implementation differs.</p>
<h2>A Practical Scenario</h2>
<p>Let's say you're building an audio application. You've defined a clean interface for audio sources:</p>
<pre><code>// audio_source.h
#ifndef AUDIO_SOURCE_H
#define AUDIO_SOURCE_H

#include &lt;stddef.h&gt;

// Your application's audio source interface
typedef struct AudioSource {
    // Read samples into buffer, return number of samples read
    size_t (*read_samples)(struct AudioSource *self, float *buffer, size_t count);

    // Get the sample rate
    int (*get_sample_rate)(struct AudioSource *self);

    // Clean up resources
    void (*destroy)(struct AudioSource *self);

    // Pointer to implementation-specific data
    void *impl_data;
} AudioSource;

#endif
</code></pre>
<p>Your audio pipeline expects <code>AudioSource</code> objects. It calls <code>read_samples()</code> to pull audio data, and everything works nicely.</p>
<p>:::note
In C, a function's name is actually a pointer to where that function's code lives in memory. When you write printf("hello"), you're calling the function via its address. Function pointers let you store that address in a variable, pass it around, and call whatever function it happens to point to. The struct in the above example stores three function pointers: read_samples, get_sample_rate, and destroy; which means each AudioSource instance can have its own set of functions wired up. When you create an MP3 adapter, you point these at your MP3-specific functions. When you create a WAV adapter, you point them at WAV-specific functions. The calling code doesn't know or care which functions are actually being called; it just calls source-&gt;read_samples(source, buffer, count) and the right thing happens.</p>
<p>The impl_data field is the other half of the puzzle. It's a void *, which means it can point to anything. Each adapter uses this to store whatever internal state it needs — the MP3 adapter stores a pointer to its MP3Decoder, the WAV adapter might store file handle and format information, and so on. When one of the function pointers is called, it casts impl_data back to the appropriate type and uses it.</p>
<p>This is how you get polymorphism in C: a generic struct that presents a uniform interface, combined with a void pointer that lets each implementation carry its own specific data. The consumer of the interface just sees AudioSource and calls methods on it; the implementation details are hidden behind those function pointers and that void pointer.
:::</p>
<p>Now you want to add support for a third-party MP3 decoding library. The problem is that this library has its own interface that looks nothing like yours:</p>
<pre><code>// Hypothetical third-party MP3 library
typedef struct {
    void *internal_state;
    int channels;
    int sample_rate;
} MP3Decoder;

// Their functions
MP3Decoder *mp3_open(const char *filename);
int mp3_decode_frame(MP3Decoder *dec, short *pcm_out, int max_samples);
void mp3_close(MP3Decoder *dec);
</code></pre>
<p>The library uses <code>short</code> samples instead of <code>float</code>. It has different function names. The structure is completely different. You can't pass an <code>MP3Decoder</code> to code expecting an <code>AudioSource</code>.</p>
<p>You could modify your audio pipeline to handle MP3Decoder as a special case, but that's messy. You could modify the third-party library, but you don't control it. The adapter pattern gives you a third option: wrap the incompatible interface to make it compatible.</p>
<h2>Building the Adapter</h2>
<p>The adapter needs to present the <code>AudioSource</code> interface while internally using the <code>MP3Decoder</code>. Here's how:</p>
<pre><code>// mp3_adapter.c
#include &lt;stdlib.h&gt;
#include "audio_source.h"
#include "mp3_library.h"  // The third-party library

// Internal structure holding the adapted decoder
typedef struct {
    MP3Decoder *decoder;
} MP3AdapterData;

// Adapter implementation of read_samples
static size_t mp3_adapter_read(AudioSource *self, float *buffer, size_t count) {
    MP3AdapterData *data = (MP3AdapterData *)self-&gt;impl_data;

    // Temporary buffer for the library's short samples
    short *temp = malloc(count * sizeof(short));
    if (!temp) return 0;

    // Call the third-party library
    int samples_read = mp3_decode_frame(data-&gt;decoder, temp, (int)count);

    // Convert from short (-32768 to 32767) to float (-1.0 to 1.0)
    for (int i = 0; i &lt; samples_read; i++) {
        buffer[i] = temp[i] / 32768.0f;
    }

    free(temp);
    return (size_t)samples_read;
}

// Adapter implementation of get_sample_rate
static int mp3_adapter_get_sample_rate(AudioSource *self) {
    MP3AdapterData *data = (MP3AdapterData *)self-&gt;impl_data;
    return data-&gt;decoder-&gt;sample_rate;
}

// Adapter implementation of destroy
static void mp3_adapter_destroy(AudioSource *self) {
    MP3AdapterData *data = (MP3AdapterData *)self-&gt;impl_data;
    mp3_close(data-&gt;decoder);  // Clean up the third-party decoder
    free(data);                 // Clean up our adapter data
    free(self);                 // Clean up the AudioSource struct
}

// Factory function: create an AudioSource from an MP3 file
AudioSource *audio_source_from_mp3(const char *filename) {
    // Open the MP3 using the third-party library
    MP3Decoder *decoder = mp3_open(filename);
    if (!decoder) return NULL;

    // Allocate our adapter data
    MP3AdapterData *data = malloc(sizeof(MP3AdapterData));
    if (!data) {
        mp3_close(decoder);
        return NULL;
    }
    data-&gt;decoder = decoder;

    // Allocate and populate the AudioSource interface
    AudioSource *source = malloc(sizeof(AudioSource));
    if (!source) {
        mp3_close(decoder);
        free(data);
        return NULL;
    }

    // Wire up the function pointers
    source-&gt;read_samples = mp3_adapter_read;
    source-&gt;get_sample_rate = mp3_adapter_get_sample_rate;
    source-&gt;destroy = mp3_adapter_destroy;
    source-&gt;impl_data = data;

    return source;
}
</code></pre>
<p>Now your audio pipeline can use MP3 files without knowing anything about the MP3 library:</p>
<pre><code>// main.c
#include "audio_source.h"

// Declared in mp3_adapter.c
AudioSource *audio_source_from_mp3(const char *filename);

void process_audio(AudioSource *source) {
    float buffer[1024];
    size_t samples;

    while ((samples = source-&gt;read_samples(source, buffer, 1024)) &gt; 0) {
        // Process the audio...
    }
}

int main(void) {
    AudioSource *source = audio_source_from_mp3("song.mp3");
    if (source) {
        process_audio(source);  // Works with any AudioSource
        source-&gt;destroy(source);
    }
    return 0;
}
</code></pre>
<h2>What Makes This an Adapter</h2>
<p>The key elements are clear if you look at what each piece is doing:</p>
<ul>
<li>
<p>The target interface is <code>AudioSource</code>. This is what your existing code expects. You don't want to change it.</p>
</li>
<li>
<p>The adaptee is <code>MP3Decoder</code> from the third-party library. This is what you want to use, but it doesn't match your interface.</p>
</li>
<li>
<p>The adapter is the combination of <code>MP3AdapterData</code> and the <code>mp3_adapter_*</code> functions. It implements the target interface while internally delegating to the adaptee. The translation logic (converting <code>short</code> to <code>float</code>, mapping function calls etc.) lives here.</p>
</li>
<li>
<p>The factory function <code>audio_source_from_mp3()</code> constructs the adapter and wires everything together. Client code just calls this and gets back an <code>AudioSource</code> it can use normally.</p>
</li>
</ul>
<h2>Adding More Adapters</h2>
<p>The real power shows when you add more formats. Each one gets its own adapter:</p>
<pre><code>// Different factory functions, same return type
AudioSource *audio_source_from_mp3(const char *filename);
AudioSource *audio_source_from_wav(const char *filename);
AudioSource *audio_source_from_flac(const char *filename);
AudioSource *audio_source_from_ogg(const char *filename);
</code></pre>
<p>Each adapter wraps a different underlying library, handling whatever quirks that library has. Your audio processing code doesn't care, it just sees <code>AudioSource</code> objects and calls <code>read_samples()</code>.</p>
<pre><code>void play_file(const char *filename) {
    AudioSource *source = NULL;

    // Pick the right adapter based on file extension
    if (ends_with(filename, ".mp3")) {
        source = audio_source_from_mp3(filename);
    } else if (ends_with(filename, ".wav")) {
        source = audio_source_from_wav(filename);
    } else if (ends_with(filename, ".flac")) {
        source = audio_source_from_flac(filename);
    }

    if (source) {
        process_audio(source);  // Same code handles all formats
        source-&gt;destroy(source);
    }
}
</code></pre>
<h2>When to Use This Pattern</h2>
<p>The adapter pattern is useful when you're integrating third-party libraries that have their own conventions, maintaining backward compatibility with old interfaces while using new implementations, writing plugins or modular systems where components need a uniform interface, or testing code by swapping in mock implementations that conform to the expected interface.</p>
<p>There's also a maintenance benefit: the adapter isolates your code from changes in the underlying system. If the MP3 library releases a new version with a different API, maybe mp3_decode_frame becomes mp3_read_pcm with different parameters, you only need to update the adapter. Every part of your codebase that uses AudioSource remains untouched, because it never knew about the MP3 library in the first place. The adapter acts as a buffer between your code and external dependencies, containing the blast radius of changes.</p>
<h2>A Simpler Alternative</h2>
<p>This pattern does add a layer of indirection, which has a small cost. Don't use it when a simple wrapper function would suffice, or when you control both sides of the interface and can just make them match directly.</p>
<pre><code>// Simple wrapper, no function pointers
size_t read_mp3_samples(MP3Decoder *dec, float *buffer, size_t count) {
    short temp[count];
    int read = mp3_decode_frame(dec, temp, count);
    for (int i = 0; i &lt; read; i++) {
        buffer[i] = temp[i] / 32768.0f;
    }
    return read;
}
</code></pre>
<p>This translates between interfaces without the struct-and-function-pointer machinery. Use the full adapter pattern when you need to treat different implementations uniformly through a common interface; use simple wrappers when you just need to bridge a single gap.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-04T17:37:24.739Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[What Happens When You Call malloc and free in C]]></title>
        <id>https://www.lowlevel.dev/posts/malloc-free/</id>
        <link href="https://www.lowlevel.dev/posts/malloc-free/"/>
        <updated>2026-01-04T17:05:31.373Z</updated>
        <summary type="html"><![CDATA[When you write malloc(100) in C, you're asking for 100 bytes of memory. A pointer comes back, you use it, and eventually you call free() to...]]></summary>
        <content type="html"><![CDATA[<p>When you write <code>malloc(100)</code> in C, you're asking for 100 bytes of memory. A pointer comes back, you use it, and eventually you call <code>free()</code> to give it back. But what's actually happening behind that simple interface?</p>
<h2>Where Does Dynamic Memory Live?</h2>
<p>Your program's memory is divided into several regions. The stack holds local variables and function call information, growing and shrinking automatically as functions are called and return. The heap is different: it's a pool of memory that your program can draw from and return to at will, and it persists until you explicitly free it.</p>
<p>The heap isn't managed by the compiler. It's managed by the allocator, which is a chunk of code (usually part of your C library) that sits between your <code>malloc</code> calls and the operating system.</p>
<h2>What malloc Actually Does</h2>
<p>When you call <code>malloc</code>, the allocator needs to find a contiguous block of memory large enough for your request. This is trickier than it sounds.</p>
<p>The allocator maintains data structures to track which parts of the heap are in use and which are free. A simple implementation might keep a linked list of free blocks. When you request memory, it walks the list looking for a block that's big enough. Different allocators use different strategies here: first-fit (take the first block that's large enough), best-fit (find the smallest block that's large enough), or more sophisticated approaches.</p>
<p>When the allocator finds a suitable block, it might be larger than you need. If you ask for 100 bytes and there's a free block of 500 bytes, the allocator will often split that block — give you 100 bytes and keep the remaining 400 bytes on the free list for future requests.</p>
<p>Here's the important bit: the allocator typically stores metadata alongside your allocation. If you ask for 100 bytes, the allocator might actually use 108 or 116 bytes internally — the extra space holds information like the size of the allocation, pointers for the free list, or flags. This metadata usually sits just before the pointer you receive. When you call <code>free(ptr)</code>, the allocator looks at the bytes just before <code>ptr</code> to figure out how big the block is.</p>
<pre><code>What you think you got:      [---- 100 bytes of usable memory ----]

What's actually there:       [header][---- 100 bytes of usable memory ----]
                                 ^
                                 |
                             size, flags, etc.
</code></pre>
<p>This is why freeing an invalid pointer or overwriting memory before your allocation can corrupt the heap — you're trashing the allocator's bookkeeping.</p>
<h2>Where Does the Memory Come From?</h2>
<p>The allocator itself needs to get memory from somewhere. On Unix-like systems, it typically uses system calls like <code>sbrk</code> or <code>mmap</code>. <code>sbrk</code> extends the heap by moving the "program break", which is the boundary between the heap and unused address space. <code>mmap</code> asks the kernel to map a chunk of memory into your process's address space.</p>
<p>Calling into the kernel is expensive, so allocators don't do it for every <code>malloc</code>. Instead, they request large chunks from the operating system and then carve those chunks up to satisfy individual allocations. When you <code>malloc(100)</code>, the allocator might have already obtained a 128KB block from the OS, and it just hands you a piece of that.</p>
<h2>What free Actually Does</h2>
<p>When you call <code>free(ptr)</code>, the allocator marks that block as available for reuse. It reads the metadata to determine the block's size, then adds it back to its free list.</p>
<p>Crucially, <code>free</code> usually doesn't return memory to the operating system. The memory stays in your process's address space, available for future <code>malloc</code> calls. This is why a program's memory usage (as reported by the OS) often doesn't decrease after freeing memory — the allocator is holding onto it in case you need it again soon.</p>
<p>Some allocators will return large blocks to the OS, particularly those allocated with <code>mmap</code>, but smaller allocations typically just go back into the pool.</p>
<h2>Fragmentation</h2>
<p>Over time, as you allocate and free memory of varying sizes, the heap can become fragmented. You might have plenty of free memory in total, but it's scattered across many small non-contiguous blocks. When you ask for a large allocation, there's no single free block big enough to satisfy it, even though the total free memory exceeds your request.</p>
<pre><code>[used][free 50][used][free 30][used][free 80][used][free 40]

Total free: 200 bytes
Largest contiguous block: 80 bytes
</code></pre>
<p>If you request 100 bytes here, you're out of luck as there's no contiguous block large enough. The allocator would need to request more memory from the OS, even though 200 bytes are technically free.</p>
<p>Good allocators use various strategies to combat fragmentation: coalescing adjacent free blocks, using different pools for different allocation sizes, or organising memory in ways that reduce fragmentation in the first place.</p>
<h2>Coalescing: Merging Free Blocks</h2>
<p>When you free a block, a good allocator checks whether the adjacent blocks are also free. If they are, it merges them into a single larger block. This is called coalescing.</p>
<pre><code>Before free(B):   [A: free][B: used][C: free]
After free(B):    [-- one big free block ---]
</code></pre>
<p>Without coalescing, you'd accumulate lots of small free blocks that individually aren't useful for larger allocations. Coalescing helps keep the free list healthy.</p>
<h2>Why This Matters</h2>
<p>Memory overhead: every allocation costs more than you asked for, because of metadata. Allocating millions of tiny objects is wasteful as you'll pay the overhead on each one.</p>
<p>Performance: allocation isn't free. The allocator has to search for suitable blocks, possibly split them, update its data structures. For performance-critical code, you might want to allocate in bulk or use a custom allocator.</p>
<p>Memory doesn't go down: your program's resident memory can grow but rarely shrinks, because <code>free</code> doesn't return memory to the OS. This isn't a leak, the memory is available for reuse within your process, but it can be surprising.</p>
<p>Corruption is catastrophic: writing past the end of an allocation or freeing the same pointer twice corrupts the allocator's data structures. The program might not crash immediately, but the heap is now in an inconsistent state. Future allocations might return overlapping memory, or the allocator might crash much later, far from the original bug.</p>
<h2>A Note on Modern Allocators</h2>
<p>Real-world allocators like glibc's ptmalloc, jemalloc, or tcmalloc are significantly more sophisticated than the simple model I've described. They use multiple arenas to reduce contention in multithreaded programs, maintain separate free lists for different size classes, use thread-local caches to avoid locking on every allocation, and employ various clever tricks to improve performance and reduce fragmentation.</p>
<p>But the fundamentals remain the same: carve up memory into blocks, track what's free, satisfy requests from the pool, and try not to fragment things too badly in the process.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-04T17:05:31.373Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Understanding the Extern Keyword in C]]></title>
        <id>https://www.lowlevel.dev/posts/extern-keyword-c/</id>
        <link href="https://www.lowlevel.dev/posts/extern-keyword-c/"/>
        <updated>2026-01-03T19:40:21.854Z</updated>
        <summary type="html"><![CDATA[If static is about hiding things within a file, extern is about sharing things across files. It's the keyword that lets you say "this variab...]]></summary>
        <content type="html"><![CDATA[<p>If <code>static</code> is about hiding things within a file, <code>extern</code> is about sharing things across files. It's the keyword that lets you say "this variable or function exists, but it's defined somewhere else."</p>
<p>Once you understand the difference between a declaration and a definition, <code>extern</code> becomes much clearer.</p>
<h2>Declarations vs Definitions</h2>
<p>In C, a declaration tells the compiler "this thing exists and has this type." A definition actually creates the thing and allocates storage for it.</p>
<p>For functions, the distinction is obvious:</p>
<pre><code>// Declaration: tells the compiler the function exists
int add(int a, int b);

// Definition: actually implements the function
int add(int a, int b) {
    return a + b;
}
</code></pre>
<p>For variables, it's a bit more subtle, and that's where <code>extern</code> comes in.</p>
<h2>Extern Variables</h2>
<p>Let's say you have a global variable in one file that you want to use in another file.</p>
<pre><code>// config.c
int max_connections = 100;  // This is a definition
</code></pre>
<p>You've defined <code>max_connections</code> here. Memory is allocated for it, and it's initialised to 100.</p>
<p>Now, in another file, you want to use this variable:</p>
<pre><code>// server.c
extern int max_connections;  // This is a declaration

void setup_server(void) {
    printf("Max connections: %d\n", max_connections);
}
</code></pre>
<p>The <code>extern</code> keyword tells the compiler "there's an <code>int</code> called <code>max_connections</code> somewhere, but not here. Don't allocate storage for it. Just trust me that it exists, and the linker will sort it out."</p>
<p>Without <code>extern</code>, you'd get a problem. If you wrote:</p>
<pre><code>// server.c
int max_connections;  // This is another definition!
</code></pre>
<p>Now you've got two definitions of the same variable in two different files. The linker will complain about multiple definitions (or worse, with some linkers and settings, it might silently merge them in ways you don't expect).</p>
<h2>The Header File Pattern</h2>
<p>In practice, you almost always see <code>extern</code> declarations in header files. This is the standard pattern:</p>
<pre><code>// config.h
#ifndef CONFIG_H
#define CONFIG_H

extern int max_connections;      // Declaration
extern const char *app_name;     // Declaration

#endif
</code></pre>
<pre><code>// config.c
#include "config.h"

int max_connections = 100;       // Definition
const char *app_name = "MyApp";  // Definition
</code></pre>
<pre><code>// server.c
#include "config.h"

void setup_server(void) {
    // We can use max_connections and app_name here
    printf("%s allows %d connections\n", app_name, max_connections);
}
</code></pre>
<p>Any file that includes <code>config.h</code> can use these variables. The actual storage lives in <code>config.c</code>, and everyone else just references it through the <code>extern</code> declarations.</p>
<p>:::note
The reason this pattern works is down to how C handles compilation and linking as separate stages. When the compiler processes <code>server.c</code>, it sees <code>#include "config.h"</code>, which pastes in the <code>extern int max_connections;</code> declaration. This tells the compiler "there's an integer called <code>max_connections</code> somewhere, and I need to reference it." The compiler doesn't know or care where it actually lives — it just generates code that refers to a symbol called <code>max_connections</code> and trusts that the linker will resolve it later. Meanwhile, when the compiler processes <code>config.c</code>, it sees <code>int max_connections = 100;</code>, which is a definition. This actually allocates four bytes of storage, initialises them to 100, and exports the symbol <code>max_connections</code> so the linker can find it. When you link the object files together, the linker sees that <code>server.o</code> needs a symbol called <code>max_connections</code>, and <code>config.o</code> provides one, so it wires them up. Every reference to <code>max_connections</code> in your entire program now points to that single four-byte location defined in <code>config.c</code>. The header file is just a convenient way to share the declaration across multiple source files — without it, you'd have to manually write <code>extern int max_connections;</code> at the top of every file that needs access to it, which would be tedious and error-prone.
:::</p>
<p>This is the C way of having shared global state. Whether you should have shared global state is a different question, but when you need it, this is how you do it.</p>
<h2>Extern With Functions (Usually Unnecessary)</h2>
<p>Here's something that catches people out: function declarations are implicitly <code>extern</code>.</p>
<pre><code>// These two lines mean exactly the same thing
int add(int a, int b);
extern int add(int a, int b);
</code></pre>
<p>The <code>extern</code> keyword on a function declaration is redundant. You'll see it occasionally in older code or in code written by people coming from other languages, but it's not doing anything. Most style guides recommend leaving it off.</p>
<p>This is why function prototypes in header files don't need <code>extern</code>:</p>
<pre><code>// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);        // extern is implied
int multiply(int a, int b);   // extern is implied

#endif
</code></pre>
<p>Variables are different. Without <code>extern</code>, a variable declaration at file scope is a definition (or at least a tentative definition, but let's not go down that rabbit hole just yet).</p>
<h2>Extern With Initialisers = That's a Definition</h2>
<p>If you try to initialise an <code>extern</code> variable, you're actually defining it:</p>
<pre><code>extern int max_connections = 100;  // This is a definition, despite extern!
</code></pre>
<p>Some compilers will warn about this. The <code>extern</code> keyword is essentially being ignored because you're providing an initial value, which means you're defining the variable right here.</p>
<p>:::tip
Whether the compiler warns you or not, this is a source of confusion. One you can do without by being explicit: use <code>extern</code> for declarations (no initialiser), and ensure you leave it off for definitions (with or without an initialiser).
:::</p>
<h2>A Practical Example: Building a Simple Module</h2>
<p>Let's put this together with a small example. Say you're writing a logging module.</p>
<pre><code>// logger.h
#ifndef LOGGER_H
#define LOGGER_H

// External variable: current log level
extern int log_level;

// Log level constants
#define LOG_ERROR 0
#define LOG_WARN  1
#define LOG_INFO  2
#define LOG_DEBUG 3

// Function declarations (extern is implied, so we don't write it)
void log_message(int level, const char *message);
void set_log_level(int level);

#endif
</code></pre>
<pre><code>// logger.c
#include &lt;stdio.h&gt;
#include "logger.h"

// Definition of the log level variable
int log_level = LOG_INFO;  // Default to INFO

void log_message(int level, const char *message) {
    if (level &lt;= log_level) {
        const char *level_names[] = {"ERROR", "WARN", "INFO", "DEBUG"};
        printf("[%s] %s\n", level_names[level], message);
    }
}

void set_log_level(int level) {
    log_level = level;
}
</code></pre>
<pre><code>// main.c
#include "logger.h"

int main(void) {
    log_message(LOG_INFO, "Starting up");

    // We can access log_level directly because of the extern declaration
    if (log_level &gt;= LOG_DEBUG) {
        log_message(LOG_DEBUG, "Debug mode is on");
    }

    set_log_level(LOG_DEBUG);
    log_message(LOG_DEBUG, "Now this will print");

    return 0;
}
</code></pre>
<p>The <code>extern int log_level;</code> in the header lets any file that includes <code>logger.h</code> read (or modify) the log level. The actual variable lives in <code>logger.c</code>.</p>
<p>:::note
Did you spot any issues with this code example?</p>
<p>This example is fine for demonstrating the extern pattern, but there are a few things you'd want to improve in production code:</p>
<ol>
<li>Exposing log_level directly via extern means any file can modify it without going through set_log_level(), which undermines encapsulation, if you later need to add validation or trigger behaviour when the level changes, you'd have to track down every place that touches the variable directly. Better to make log_level static in logger.c and add a get_log_level() function if external code needs to read it.</li>
<li>The #define constants would be cleaner as an enum, which gives you better type safety and more useful information in a debugger.</li>
<li>There's no bounds checking when indexing into level_names[], if someone passes an invalid level, you're into undefined behaviour territory.</li>
<li>The level_names array is declared inside the function, which means it's technically recreated on each call (though the compiler will likely optimise this away). Declaring it as static const at file scope makes the intent explicit and guarantees single initialisation.</li>
</ol>
<p>None of these are critical for an example on the extern keyword, but they're worth being aware of as you move toward real-world code.
:::</p>
<h2>Extern and Static</h2>
<p>It's worth noting that <code>extern</code> and <code>static</code> are opposites when applied to global variables and functions.</p>
<p><code>static</code> means internal linkage: visible only within this file.</p>
<p><code>extern</code> means external linkage: visible across files, defined elsewhere.</p>
<p>You can't use both on the same declaration. If you write <code>extern static int x;</code>, the compiler will rightly tell you that makes no sense.</p>
<h2>Wrapping Up</h2>
<p>The <code>extern</code> keyword is fundamentally about separation: declaring something in one place and defining it in another. It's the glue that lets multiple C files share variables.</p>
<p>The key points to remember are that <code>extern</code> on a variable means "this is a declaration, not a definition." On a function, it's redundant since functions are implicitly <code>extern</code> already. And somewhere in your program, every <code>extern</code> variable needs an actual definition without <code>extern</code>.</p>
<p>If you're structuring a C project with multiple files, you'll use this pattern constantly. Declarations with <code>extern</code> go in headers, definitions go in source files, and the linker ties it all together.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-03T19:40:21.854Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Three Jobs of the Static Keyword in C]]></title>
        <id>https://www.lowlevel.dev/posts/static-keyword-c/</id>
        <link href="https://www.lowlevel.dev/posts/static-keyword-c/"/>
        <updated>2026-01-03T09:38:19.644Z</updated>
        <summary type="html"><![CDATA[The static keyword in C is one of those things that trips people up because it does different things depending on where you use it. Same key...]]></summary>
        <content type="html"><![CDATA[<p>The <code>static</code> keyword in C is one of those things that trips people up because it does different things depending on where you use it. Same keyword, different behaviour. Once you know the three contexts, though, it becomes much more straightforward.</p>
<h2>1. Static Local Variables</h2>
<p>Normally, when a function finishes executing, its local variables disappear. The stack frame gets cleaned up, and those variables cease to exist. Next time you call the function, you start fresh.</p>
<p>A <code>static</code> local variable changes that. It persists between function calls.</p>
<pre><code>#include &lt;stdio.h&gt;

void count_calls(void) {
    static int call_count = 0;  // Initialised once, persists forever
    call_count++;
    printf("This function has been called %d times\n", call_count);
}

int main(void) {
    count_calls();  // Prints: 1
    count_calls();  // Prints: 2
    count_calls();  // Prints: 3
    return 0;
}
</code></pre>
<p>The key thing here is that <code>call_count</code> is initialised to zero only once, when the program starts. After that, it just keeps its value between calls. It's not recreated each time the function runs.</p>
<p>Where does it live in memory? Not on the stack. Static local variables are stored in the data segment (or BSS segment if uninitialised), alongside global variables. They have the lifetime of the entire program, but the scope of the function they're declared in.</p>
<p>I find this useful for things like generating unique IDs, caching expensive calculations, or tracking state in a function that needs to remember what happened last time.</p>
<p>One thing to watch out for: this can make your functions harder to reason about and test, because they're no longer pure functions. The output depends on how many times you've called them before. Use it when you need it, but be aware of the trade-off.</p>
<p>:::note
A <strong>pure function</strong> is one that always produces the same output for the same input, and has no side effects. If you call add(2, 3), you get 5 every single time, regardless of when you call it, how many times you've called it before, or what else is happening in your program. The function doesn't have static variables, doesn't read from global variables, doesn't modify anything outside its own scope, and doesn't do I/O. It just takes its arguments, computes a result, and returns it. This makes pure functions easy to test (no setup required, just check input against output), easy to reason about (you can understand them in isolation), and safe to call from anywhere without worrying about hidden consequences.</p>
<p>The moment a function reads from or writes to a static variable, accesses global state, prints to the console, or modifies something via a pointer that was passed in, it stops being pure. That's not always a bad thing, programs need side effects to do anything useful, but knowing which of your functions are pure helps you understand where complexity and unpredictability live in your codebase.
:::</p>
<h2>2. Static Global Variables</h2>
<p>When you declare a global variable (outside any function) as <code>static</code>, you're saying "this variable should only be visible within this file."</p>
<p>Without <code>static</code>:</p>
<pre><code>// file1.c
int shared_counter = 0;  // Visible to other files via extern
</code></pre>
<pre><code>// file2.c
extern int shared_counter;  // We can access it from here
</code></pre>
<p>With <code>static</code>:</p>
<pre><code>// file1.c
static int private_counter = 0;  // Only visible in file1.c
</code></pre>
<pre><code>// file2.c
extern int private_counter;  // Linker error! Can't see it.
</code></pre>
<p>This is about controlling visibility across translation units. The <code>static</code> keyword gives the variable internal linkage, meaning it's private to that file.</p>
<p>:::note
A <strong>translation unit</strong> is what the C compiler actually sees after the preprocessor has finished its work. When you compile a .c file, the preprocessor first handles all the #include directives, macro expansions, and conditional compilation blocks, producing a single blob of source code. That blob, your original .c file with all the headers inlined and macros expanded, is one translation unit.</p>
<p>The compiler processes each translation unit independently, turning it into an object file (.o or .obj), and then the linker combines all the object files into your final executable. This is why static functions and variables are "file-private," they're actually private to the translation unit, which in practice means the .c file they're defined in (plus whatever headers that file includes). It's also why you can have the same static function name in two different .c files without a conflict: each lives in its own translation unit, and the linker never sees them because they have internal linkage. When people say "file scope" in C, they really mean "translation unit scope," though in most cases the distinction doesn't matter since each .c file typically becomes one translation unit.
:::</p>
<p>Why would you want this? Encapsulation. If you're writing a module and you have some state that only that module should touch, making it <code>static</code> prevents other parts of the codebase from accidentally (or deliberately) messing with it. It's a bit like making a member private in an object-oriented language.</p>
<h2>3. Static Functions</h2>
<p>The same logic applies to functions. A <code>static</code> function is only visible within the file where it's defined.</p>
<pre><code>// utils.c

// This helper is only for internal use within utils.c
static int calculate_checksum(const char *data, size_t len) {
    int sum = 0;
    for (size_t i = 0; i &lt; len; i++) {
        sum += data[i];
    }
    return sum;
}

// This is the public function other files can call
int validate_data(const char *data, size_t len) {
    int checksum = calculate_checksum(data, len);
    return checksum == expected_checksum;
}
</code></pre>
<p>Other files can call <code>validate_data</code>, but they can't call <code>calculate_checksum</code> directly. It's an implementation detail.</p>
<p>This is genuinely useful for keeping your API clean. When someone includes your header file, they shouldn't see every little helper function you've written. They should see the functions you've designed for them to use. The <code>static</code> keyword lets you hide the rest.</p>
<p>It also means you can have <code>static</code> functions with the same name in different files without causing linker conflicts. Each one is private to its own file.</p>
<h2>A Quick Summary</h2>
<p>Here's how I think about it:</p>
<ul>
<li><strong>Static local variable:</strong> "Keep your value between calls, but stay scoped to this function."</li>
<li><strong>Static global variable:</strong> "Exist for the whole program, but only be visible in this file."</li>
<li><strong>Static function:</strong> "Only be callable from within this file."</li>
</ul>
<p>The common thread is persistence or restriction of visibility. For local variables, it's about persistence. For globals and functions, it's about restricting visibility to the current translation unit.</p>
<h2>One More Thing: Static in Headers</h2>
<p>You might occasionally see <code>static</code> used in header files, often with <code>inline</code> functions.</p>
<pre><code>// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// static inline: asking the compiler to insert this code directly
// at each call site, avoiding function call overhead
static inline int max(int a, int b) {
    return (a &gt; b) ? a : b;
}

static inline int min(int a, int b) {
    return (a &lt; b) ? a : b;
}

static inline int clamp(int value, int lower, int upper) {
    // Constrain a value to a range
    if (value &lt; lower) return lower;
    if (value &gt; upper) return upper;
    return value;
}

#endif
</code></pre>
<p>This creates a separate copy of the function in each translation unit that includes the header. Sometimes that's what you want (for small inline functions), but it can bloat your binary if you're not careful.</p>
<p>:::tip
If you see <code>static inline</code> in a header, that's usually deliberate. If you see just <code>static</code> for a regular function in a header, someone might have made a mistake. With static inline, the intent is clear: you want the function inlined, and the static is there as a fallback to prevent linker errors if inlining doesn't happen. But with just static on a substantial function, you're not asking for inlining at all, you're just creating a separate copy of the function in every translation unit that includes the header, and that's almost never what someone actually wants.
:::</p>
<h2>Wrapping Up</h2>
<p>The <code>static</code> keyword is doing a lot of work in C. Same word, different contexts, different effects. Once you've got the three uses clear in your head, you'll find it's a handy tool for managing state and keeping your code organised.</p>
<p>If you're coming from a language like Java or C#, note that C's <code>static</code> has nothing to do with class members (C doesn't have classes). It's all about storage duration and linkage. Don't let the shared terminology confuse you.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-03T09:38:19.644Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Working with Memory in C/C++]]></title>
        <id>https://www.lowlevel.dev/posts/memory-management/</id>
        <link href="https://www.lowlevel.dev/posts/memory-management/"/>
        <updated>2026-01-02T16:28:07.813Z</updated>
        <summary type="html"><![CDATA[If you've come from languages like Python, JavaScript, or Java, memory management in C and C++ will feel unfamiliar (and maybe even a little...]]></summary>
        <content type="html"><![CDATA[<p>If you've come from languages like Python, JavaScript, or Java, memory management in C and C++ will feel unfamiliar (and maybe even a little bit scary). Those languages handle memory for you. In C and C++, you're the one in charge.</p>
<p>This direct control over memory is why these languages are still used for operating systems, game engines, and embedded devices. It's also why they have a reputation for being difficult. But once you understand where your data lives and who's responsible for cleaning it up, most of the difficulty disappears.</p>
<h2>The Two Places Your Data Can Live</h2>
<p>In C and C++, your data essentially lives in one of two places: the stack or the heap. Understanding the difference is fundamental to everything else.</p>
<h3>The Stack</h3>
<p>The stack is where local variables live. When you declare a variable inside a function, it goes on the stack. When that function returns, the variable is automatically destroyed. You don't have to think about it.</p>
<pre><code>void greet() {
    int count = 5;           // Lives on the stack
    char message[20];        // Also on the stack

    // Do things with count and message...

}   // count and message are automatically destroyed here
</code></pre>
<p>The stack is fast, predictable, and self-cleaning. It's called a "stack" because it works like a stack of plates: the last thing you put on is the first thing that comes off. Each function call pushes a new "frame" onto the stack containing its local variables, and returning from that function pops the frame off.</p>
<p>The catch? Stack space is limited (typically a few megabytes), and the size of stack allocations must be known at compile time. You can't decide at runtime that you need an array of a million integers and put it on the stack. Well, you can try, but you'll get a stack overflow.</p>
<p>:::tip
You might hear the terms <strong>FIFO</strong> and <strong>LIFO</strong> when people talk about data structures. FIFO stands for "first in, first out." Like a queue at a shop, where the first person to arrive is the first person served. LIFO stands for "last in, first out", and that's how a stack works. The most recent item you added is the first one you remove. When you call a function, its stack frame goes on top. When that function returns, its frame comes off first, before any of the frames that were already there. This is why local variables from a calling function are still intact when a called function returns — they were lower in the stack and never got touched.
:::</p>
<h3>The Heap</h3>
<p>The heap is where dynamic memory lives. It's much larger than the stack, and you can allocate whatever size you need at runtime. The trade-off is that you're responsible for cleaning up after yourself.</p>
<p>In C, you use <code>malloc</code> to allocate and <code>free</code> to deallocate:</p>
<pre><code>#include &lt;stdlib.h&gt;

void process_data(int num_items) {
    // Allocate an array of integers on the heap
    int* data = malloc(num_items * sizeof(int));

    if (data == NULL) {
        // malloc returns NULL if allocation fails
        return;
    }

    // Use the data...
    for (int i = 0; i &lt; num_items; i++) {
        data[i] = i * 2;
    }

    // We're done, so we must free the memory
    free(data);
}
</code></pre>
<p>In C++, you use <code>new</code> and <code>delete</code>:</p>
<pre><code>void process_data(int num_items) {
    // Allocate an array of integers on the heap
    int* data = new int[num_items];

    // Use the data...
    for (int i = 0; i &lt; num_items; i++) {
        data[i] = i * 2;
    }

    // We're done, so we must delete the array
    delete[] data;  // Note the [] for arrays
}
</code></pre>
<p>The heap gives you flexibility, but it demands discipline. Forget to free your memory, and you've got a memory leak. Free it twice, and you've got undefined behaviour. Use it after freeing, and you've got a use-after-free bug. These are the classic memory errors that have caused countless security vulnerabilities over the decades.</p>
<h2>Pointers: Addresses in Memory</h2>
<p>You can't talk about memory in C/C++ without talking about pointers. A pointer is simply a variable that holds a memory address. Instead of containing a value directly, it contains the location where a value is stored.</p>
<pre><code>int value = 42;
int* ptr = &amp;value;  // ptr now holds the address of value

printf("Value: %d\n", value);      // Prints 42
printf("Address: %p\n", ptr);      // Prints something like 0x7ffd5e8c3a4c
printf("Via pointer: %d\n", *ptr); // Prints 42 (dereferencing)
</code></pre>
<p>The <code>&amp;</code> operator gives you the address of something. The <code>*</code> operator (when used on a pointer) gives you what's at that address. This is called dereferencing.</p>
<p>Pointers are how we work with heap memory. When you call <code>malloc</code> or <code>new</code>, you get back a pointer to the allocated memory. You use that pointer to access and manipulate the data, and eventually you pass that same pointer to <code>free</code> or <code>delete</code> to release the memory.</p>
<h2>Pointers vs Arrays in C</h2>
<p>This equivalence between arrays and pointers is one of C's more confusing aspects for newcomers. An array name is essentially a pointer to its first element, and array indexing is just pointer arithmetic with nicer syntax.</p>
<pre><code>int* numbers = malloc(3 * sizeof(int));

// These three lines do exactly the same thing
numbers[0] = 10;
*(numbers + 0) = 10;
*numbers = 10;

// Array indexing is just pointer arithmetic in disguise
numbers[2] = 30;      // Same as *(numbers + 2) = 30
</code></pre>
<p>When you write numbers[2], you're asking for "the element at index 2". But what the compiler actually does is take the address stored in numbers, add 2 * sizeof(int) bytes to it, and then read the value at that resulting address. That's pointer arithmetic: starting from a base address and moving forward by a certain number of elements.</p>
<p>The syntax *(numbers + 2) makes this explicit. numbers + 2 means "the address two integers past where numbers points", and the * dereferences that address to get the value. The square bracket notation numbers[2] is just a more readable way of writing exactly the same thing.</p>
<p>This also explains why numbers[0], *(numbers + 0), and *numbers are all identical. Index zero means no offset from the base address, so you're just dereferencing the pointer directly.</p>
<p>Here's where it gets interesting: because numbers + 2 and 2 + numbers are mathematically equivalent, C actually allows you to write 2[numbers] instead of numbers[2]. They compile to the same thing. You'd never want to write code like that, but it demonstrates just how literally array indexing maps to pointer arithmetic.</p>
<p>The key insight is that C doesn't really have arrays in the way higher-level languages do. What it has are contiguous blocks of memory and pointers to the start of those blocks. The array syntax is a convenience layer on top of that.</p>
<h2>The Dangers: What Can Go Wrong</h2>
<p>Let me walk you through the classic memory errors. Knowing what they look like is the first step to avoiding them.</p>
<h3>Memory Leaks</h3>
<p>A memory leak occurs when you allocate memory but never free it. The memory remains allocated until your program exits, even though you've lost all references to it.</p>
<pre><code>void leaky_function() {
    int* data = malloc(1000 * sizeof(int));

    // Do some work...

    if (some_error_condition) {
        return;  // Oops! We forgot to free data
    }

    free(data);
}
</code></pre>
<p>In a short-running program, leaks might not matter much. In a long-running server or an embedded system that runs for months, leaks accumulate and eventually exhaust available memory.</p>
<h3>Use After Free</h3>
<p>This is when you access memory after you've already freed it. The memory might have been reallocated for something else, so you could be reading or writing garbage, or worse, corrupting unrelated data.</p>
<pre><code>int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);

// ptr still holds the old address, but the memory is no longer ours
*ptr = 100;  // Undefined behaviour! Anything could happen
</code></pre>
<h3>Double Free</h3>
<p>Freeing the same memory twice corrupts the memory allocator's internal data structures. This can cause crashes, or worse, exploitable security vulnerabilities.</p>
<pre><code>int* ptr = malloc(sizeof(int));
free(ptr);
free(ptr);  // Undefined behaviour!
</code></pre>
<h3>Buffer Overflow</h3>
<p>Writing beyond the bounds of an allocated buffer. This is the source of countless security exploits throughout computing history.</p>
<pre><code>char buffer[10];
strcpy(buffer, "This string is way too long for the buffer");
// We've just written past the end of buffer, corrupting whatever was next to it
</code></pre>
<h2>Modern C++: Smart Pointers</h2>
<p>If you're writing C++, you have access to smart pointers, which automate memory management while still giving you control when you need it. They're part of the standard library and should be your default choice for heap allocation in modern C++.</p>
<h3>unique_ptr: Single Ownership</h3>
<p>A <code>unique_ptr</code> owns its memory exclusively. When the <code>unique_ptr</code> goes out of scope, it automatically deletes the memory. You can't copy a <code>unique_ptr</code>, but you can move it to transfer ownership.</p>
<pre><code>#include &lt;memory&gt;

void process() {
    // Memory is automatically managed
    std::unique_ptr&lt;int[]&gt; data = std::make_unique&lt;int[]&gt;(100);

    data[0] = 42;

    // When data goes out of scope, the memory is automatically freed
    // No explicit delete needed, no leaks possible
}
</code></pre>
<p>This handles most use cases well. The memory is tied to a specific scope, and cleanup happens automatically even if exceptions are thrown.</p>
<h3>shared_ptr: Shared Ownership</h3>
<p>Sometimes multiple parts of your code need to hold references to the same data, and you want the memory freed only when everyone's done with it. That's what <code>shared_ptr</code> is for. It keeps a reference count and deletes the memory when the count reaches zero.</p>
<pre><code>#include &lt;memory&gt;

std::shared_ptr&lt;Widget&gt; create_widget() {
    return std::make_shared&lt;Widget&gt;();
}

void use_widgets() {
    std::shared_ptr&lt;Widget&gt; w1 = create_widget();
    std::shared_ptr&lt;Widget&gt; w2 = w1;  // Both now point to the same Widget

    // Reference count is 2

}   // Both w1 and w2 go out of scope, ref count hits 0, Widget is deleted
</code></pre>
<p>Shared pointers have slightly more overhead than unique pointers due to the reference counting, but they solve real problems when ownership genuinely needs to be shared.</p>
<h3>When to Use Raw Pointers</h3>
<p>Even in modern C++, raw pointers aren't obsolete. They're still the right choice for non-owning references, where you need to point to something but aren't responsible for its lifetime. Function parameters that observe but don't own are a common example.</p>
<pre><code>// This function doesn't own the data, it just uses it
void print_value(const int* value) {
    if (value != nullptr) {
        std::cout &lt;&lt; *value &lt;&lt; std::endl;
    }
}

void example() {
    auto data = std::make_unique&lt;int&gt;(42);
    print_value(data.get());  // Pass raw pointer, ownership stays with unique_ptr
}
</code></pre>
<p>The rule of thumb: use smart pointers for ownership, raw pointers for observation.</p>
<h2>RAII: The Pattern Behind the Magic</h2>
<p>Smart pointers are an example of a broader C++ pattern called RAII: Resource Acquisition Is Initialisation. The idea is that resource management (memory, file handles, network connections, mutexes) should be tied to object lifetimes.</p>
<p>You acquire the resource in the constructor and release it in the destructor. Since destructors are called automatically when objects go out of scope, cleanup happens automatically and reliably, even when exceptions are thrown.</p>
<pre><code>class FileHandle {
public:
    FileHandle(const char* path) {
        file = fopen(path, "r");
    }

    ~FileHandle() {
        if (file) {
            fclose(file);  // Automatically closed when FileHandle is destroyed
        }
    }

    // Prevent copying (or implement it properly)
    FileHandle(const FileHandle&amp;) = delete;
    FileHandle&amp; operator=(const FileHandle&amp;) = delete;

private:
    FILE* file;
};

void read_config() {
    FileHandle config("settings.txt");
    // Use config...

}   // config goes out of scope, destructor runs, file is closed
</code></pre>
<p>Once you understand RAII, you'll start seeing opportunities to apply it everywhere. It's one of C++'s most useful patterns.</p>
<h2>Practical Tips</h2>
<p>After working with C and C++ for a while, here are the habits that have helped me avoid the most problems:</p>
<p>Initialise your pointers. An uninitialised pointer contains garbage, which is worse than a null pointer because it might look valid. In C, initialise to <code>NULL</code>. In C++, use <code>nullptr</code>. A null pointer will crash predictably when dereferenced; a garbage pointer might corrupt memory silently.</p>
<pre><code>int* ptr = NULL;  // Safe default
</code></pre>
<p>Set pointers to null after freeing. This prevents use-after-free bugs from silently corrupting memory. If you accidentally use the pointer, you'll get a crash instead of subtle corruption.</p>
<pre><code>free(ptr);
ptr = NULL;
</code></pre>
<p>Match your allocations and deallocations. <code>malloc</code> pairs with <code>free</code>. <code>new</code> pairs with <code>delete</code>. <code>new[]</code> pairs with <code>delete[]</code>. Mixing them is undefined behaviour.</p>
<p>Use tools to catch errors. Valgrind on Linux will detect memory leaks, use-after-free, and buffer overflows. Address Sanitizer (ASan) is built into modern compilers and catches similar issues with less overhead. Use them during development.</p>
<p>In C++, prefer stack allocation when possible. If the object's lifetime matches a scope and it's not too large, put it on the stack. It's faster and there's nothing to clean up.</p>
<pre><code>// Prefer this when possible
void process() {
    std::vector&lt;int&gt; data(100);  // vector manages its own heap memory internally
    // ...
}   // data is destroyed, its internal memory is freed

// Over this
void process() {
    std::vector&lt;int&gt;* data = new std::vector&lt;int&gt;(100);
    // ...
    delete data;
}
</code></pre>
<h2>Wrapping Up</h2>
<p>Memory management in C and C++ comes down to understanding two things: where your data lives (stack vs heap) and who's responsible for cleaning it up (automatic vs manual).</p>
<p>In C, you're always working manually. You call <code>malloc</code>, you call <code>free</code>, and you're responsible for getting it right.</p>
<p>In C++, you have choices. Smart pointers and RAII let you automate cleanup while retaining control. Modern C++ code should rarely contain explicit <code>new</code> and <code>delete</code> calls outside of low-level infrastructure code.</p>
<p>The key is building good habits: initialise your pointers, use the right tools to catch errors, and in C++, let smart pointers do the work for you. Get these fundamentals right, and memory management becomes straightforward.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2026-01-02T16:28:07.813Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Bitwise Operations]]></title>
        <id>https://www.lowlevel.dev/posts/bitwise-operations/</id>
        <link href="https://www.lowlevel.dev/posts/bitwise-operations/"/>
        <updated>2025-12-30T13:35:24.324Z</updated>
        <summary type="html"><![CDATA[I remember the first time I encountered bitwise operations in code. It was many years ago as a fledgling games developer. I was staring at s...]]></summary>
        <content type="html"><![CDATA[<p>I remember the first time I encountered bitwise operations in code. It was <em>many</em> years ago as a fledgling games developer. I was staring at something like <code>flags &amp; 0x04</code> and thinking, "What is this dark magic?" It looked like the kind of thing only compiler writers and embedded systems wizards needed to worry about.</p>
<p>Turns out, bitwise operations aren't magic at all. They're actually quite simple. And once you understand them, you'll have a new tool to use. One that you'll find yourself using fairly often, from compact data storage to quicker calculations.</p>
<h2>First, Let's Talk About Bits</h2>
<p>Before we dive into operations, let's make sure we're on the same page about what we're operating on.</p>
<p>Every number in your computer is stored as a sequence of bits, where each bit is either a 0 or a 1. When I write the number 5 in code, the computer stores it as <code>00000101</code> (assuming an 8-bit number for simplicity). That's just 5 in binary: one 4, zero 2s, and one 1.</p>
<p>Here's a quick reference for the first few numbers:</p>
<pre><code>0  = 00000000
1  = 00000001
2  = 00000010
3  = 00000011
4  = 00000100
5  = 00000101
6  = 00000110
7  = 00000111
8  = 00001000
</code></pre>
<p>Each position represents a power of 2, reading right to left: 1, 2, 4, 8, 16, 32, 64, 128. When a bit is "set" (equals 1), you add that power of 2 to the total. So <code>00000101</code> means 4 + 1 = 5.</p>
<p>Right, with that foundation in place, let's look at what we can actually do with these bits.</p>
<h2>AND (&amp;): Both Must Be True</h2>
<p>The AND operation compares two numbers bit by bit. For each position, the result is 1 only if both input bits are 1. Otherwise, it's 0.</p>
<pre><code>    00001101  (13)
AND 00000111  (7)
  = 00000101  (5)
</code></pre>
<p>This is incredibly useful for checking if specific bits are set. Say you have a byte where each bit represents a different permission, and bit 2 (counting from 0) means "can write files". You can check if that bit is set like this:</p>
<pre><code>// Check if bit 2 is set in the permissions byte
if (permissions &amp; 0b00000100) {
    // User can write files
}
</code></pre>
<p>The number <code>0b00000100</code> (which equals 4) is called a "mask" because it masks out all the bits we don't care about. If bit 2 is set in <code>permissions</code>, we get a non-zero result. If it's not set, we get zero.</p>
<h2>OR (|): Either Will Do</h2>
<p>The OR operation is more generous. For each bit position, the result is 1 if either (or both) input bits are 1.</p>
<pre><code>   00001100  (12)
OR 00000011  (3)
 = 00001111  (15)
</code></pre>
<p>This is perfect for setting bits without disturbing the others. Want to grant that write permission we just checked for?</p>
<pre><code>// Set bit 2, leave everything else alone
permissions = permissions | 0b00000100;

// Or more concisely:
permissions |= 0b00000100;
</code></pre>
<p>Whatever <code>permissions</code> was before, bit 2 is now definitely set, and all the other bits remain exactly as they were.</p>
<h2>XOR (^): One or the Other, But Not Both</h2>
<p>XOR (exclusive or) is a bit peculiar. It returns 1 when the bits are different, and 0 when they're the same.</p>
<pre><code>    00001111  (15)
XOR 00000101  (5)
  = 00001010  (10)
</code></pre>
<p>Here's where it gets interesting: XOR has this lovely property where applying it twice gets you back to where you started. If you XOR a number with the same value twice, you end up with the original number.</p>
<pre><code>int secret = 42;
int key = 123;

int encrypted = secret ^ key;  // Some scrambled value
int decrypted = encrypted ^ key;  // Back to 42!
</code></pre>
<p>This makes XOR useful for simple encryption, toggling bits, and all sorts of clever tricks. Want to toggle bit 2? Just XOR with the mask:</p>
<pre><code>permissions ^= 0b00000100;  // Flip bit 2: if it was 0, it's now 1; if it was 1, it's now 0
</code></pre>
<h2>NOT (~): Flip Everything</h2>
<p>The NOT operation is the simplest: it flips every single bit. Ones become zeros, zeros become ones.</p>
<pre><code>NOT 00001111
  = 11110000
</code></pre>
<p>This is often used together with AND to clear specific bits. If you want to clear bit 2 (set it to 0), you can AND with the inverse of the bit's mask:</p>
<pre><code>// Clear bit 2
permissions = permissions &amp; ~0b00000100;

// What's happening here:
// ~0b00000100 gives us 0b11111011
// ANDing with this clears bit 2 and preserves everything else
</code></pre>
<h2>Bit Shifting: Sliding Things Around</h2>
<p>We also have operations that move bits left or right. Left shift (<code>&lt;&lt;</code>) moves all bits towards the more significant end, filling in zeros on the right. Right shift (<code>&gt;&gt;</code>) does the opposite.</p>
<pre><code>int x = 1;       // 00000001
x = x &lt;&lt; 3;      // 00001000 (now equals 8)
x = x &gt;&gt; 2;      // 00000010 (now equals 2)
</code></pre>
<p>Left shifting by n positions is equivalent to multiplying by 2^n. Right shifting is equivalent to dividing by 2^n (discarding any remainder). This is why you'll sometimes see shifts used for fast multiplication or division by powers of 2, though modern compilers are clever enough to make this optimisation for you.</p>
<p>Shifting is also handy for creating bit masks. Instead of writing out <code>0b00000100</code> to represent bit 2, you can write <code>1 &lt;&lt; 2</code>. This makes your intent clearer and works regardless of how many bits you're dealing with:</p>
<pre><code>#define WRITE_PERMISSION (1 &lt;&lt; 2)
#define READ_PERMISSION  (1 &lt;&lt; 1)
#define EXEC_PERMISSION  (1 &lt;&lt; 0)

// Now your permission checks become quite readable:
if (permissions &amp; WRITE_PERMISSION) {
    // Can write
}
</code></pre>
<h2>A Practical Example: Packing Data</h2>
<p>Let me show you something I find genuinely satisfying. Say you're working on a game, and you need to store a colour. Each colour has red, green, blue, and alpha components, each ranging from 0 to 255. You could use four separate bytes, or you could pack them all into a single 32-bit integer:</p>
<pre><code>// Pack four 8-bit values into one 32-bit integer
uint32_t pack_colour(uint8_t r, uint8_t g, uint8_t b, uint8_t a) {
    // Shift each component to its position and combine with OR
    return (r &lt;&lt; 24) | (g &lt;&lt; 16) | (b &lt;&lt; 8) | a;
}

// Extract the components back out
uint8_t get_red(uint32_t colour) {
    // Shift right to bring red to the bottom, then mask off everything else
    return (colour &gt;&gt; 24) &amp; 0xFF;
}

uint8_t get_green(uint32_t colour) {
    return (colour &gt;&gt; 16) &amp; 0xFF;
}

uint8_t get_blue(uint32_t colour) {
    return (colour &gt;&gt; 8) &amp; 0xFF;
}

uint8_t get_alpha(uint32_t colour) {
    return colour &amp; 0xFF;  // Already at the bottom, just mask
}
</code></pre>
<p>This pattern appears everywhere in graphics programming, network protocols, file formats, and embedded systems where memory is precious and every byte counts.</p>
<h2>Why Bother?</h2>
<p>You might be wondering why any of this matters when you could just use booleans and normal arithmetic. Fair question.</p>
<p>In high-level application code, you might rarely need bitwise operations. But they become essential when you're working close to the hardware, parsing binary file formats, implementing network protocols, or optimising performance-critical code. A single integer holding 32 boolean flags takes up the same space as 32 separate boolean variables, but you can copy, compare, and manipulate all 32 at once with a single operation.</p>
<p>In embedded systems, where I've been spending more time lately, you're often directly manipulating hardware registers where each bit controls something different. Even if you never write low-level code, understanding bits helps you appreciate what's actually happening inside the machine. And occasionally, you'll encounter someone else's bitwise code and be glad you know what <code>status &amp; (1 &lt;&lt; 7)</code> means.</p>
<h2>A Quick Reference</h2>
<p>To wrap up, here's a summary of the common operations:</p>
<p>The <strong>AND</strong> operation (<code>a &amp; b</code>) returns 1 only where both bits are 1, which is useful for checking or clearing bits.</p>
<p>The <strong>OR</strong> operation (<code>a | b</code>) returns 1 where either bit is 1, which is perfect for setting bits.</p>
<p>The <strong>XOR</strong> operation (<code>a ^ b</code>) returns 1 where bits differ, making it ideal for toggling.</p>
<p>The <strong>NOT</strong> operation (<code>~a</code>) inverts every bit.</p>
<p><strong>Left shift</strong> (<code>a &lt;&lt; n</code>) moves bits left by n positions, effectively multiplying by 2^n.</p>
<p><strong>Right shift</strong> (<code>a &gt;&gt; n</code>) moves bits right, effectively dividing by 2^n.</p>
<p>And here are the most common patterns you'll encounter:</p>
<ul>
<li>Use <code>value &amp; mask</code> to check if a bit is set</li>
<li><code>value | mask</code> to set a bit</li>
<li><code>value &amp; ~mask</code> to clear a bit</li>
<li><code>value ^ mask</code> to toggle a bit.</li>
<li>For creating masks, <code>1 &lt;&lt; n</code> gives you a mask for bit n.</li>
</ul>
<p>Once these patterns click, you'll find bitwise operations are less about memorising rules and more about developing an intuition for how bits flow and combine. They're a direct conversation with the machine, and there's something rather satisfying about that.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2025-12-30T13:35:24.324Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Inspecting Code That's Running Somewhere Else]]></title>
        <id>https://www.lowlevel.dev/posts/cross-debug/</id>
        <link href="https://www.lowlevel.dev/posts/cross-debug/"/>
        <updated>2025-12-29T09:31:15.895Z</updated>
        <summary type="html"><![CDATA[If you've read my earlier post on cross compilation, you'll know that embedded development involves a separation that mobile development abs...]]></summary>
        <content type="html"><![CDATA[<p>If you've read my earlier post on cross compilation, you'll know that embedded development involves a separation that mobile development abstracts away: your development machine and your target device are fundamentally different systems. This separation doesn't just affect how you build your code. It also changes how you debug it.</p>
<h2>The Debugging Experience I Was Used To</h2>
<p>Coming from mobile development, I had a particular mental model of debugging. I'd set a breakpoint in Xcode or Android Studio, run my app, and when execution hit that breakpoint, I could inspect variables, step through code, and examine the call stack. The debugger and the code being debugged felt like they existed in the same world, even when the app was running on a connected phone rather than a simulator.</p>
<p>What I didn't fully appreciate was how much infrastructure was working behind the scenes to create that experience. Mobile platforms provide debugging services built into the operating system. The IDE communicates with these services over USB, and it all just works.</p>
<p>Embedded systems, particularly bare-metal microcontrollers with no operating system, don't have any of this infrastructure. There's no debug service waiting for your IDE to connect. The microcontroller is just executing instructions, with no awareness that you might want to pause and inspect what it's doing.</p>
<h2>How Cross Debugging Works</h2>
<p>Cross debugging solves this problem by splitting the debugger into two parts that communicate across the boundary between your host machine and the target device.</p>
<p>On your development machine, you run a debugger frontend. This is typically <a href="https://www.sourceware.org/gdb/">GDB, the GNU Debugger</a>, or a graphical interface built on top of it. The frontend is where you set breakpoints, issue commands, and view your source code. It runs natively on your laptop and understands your keyboard, your display, and your project files.</p>
<p>On the target side, you need some way to actually halt the processor, read its registers, and inspect memory. This is where debug probes come in. Devices like the <a href="https://www.st.com/en/development-tools/st-link-v2.html">ST-Link</a> or <a href="https://www.segger.com/products/debug-probes/j-link/">SEGGER J-Link</a> connect to your microcontroller through special debug interfaces, most commonly JTAG or SWD. These interfaces are built into the silicon itself, providing hardware-level access to the processor's internal state.</p>
<p>The debug probe runs a small piece of software called a GDB server. This server translates between the GDB protocol that your frontend speaks and the low-level debug commands that control the target hardware. When you click "pause" in your IDE, that request travels from the frontend, across USB to the debug probe, and down into the chip's debug hardware, which halts the processor mid-instruction.</p>
<h2>Nothing Comes for Free</h2>
<p>Something I hadn't initially considered is that this debug capability has costs. The microcontroller has to dedicate some of its limited resources to supporting these debug interfaces.</p>
<p>At the silicon level, the debug hardware takes up physical space on the chip. There are dedicated pins for JTAG or SWD that can't be used for anything else while debugging. Some microcontrollers let you reclaim these pins for GPIO in production builds, but during development they're unavailable. On a chip with only twenty or thirty pins, losing two or four to debugging is a real trade-off.</p>
<p>There's also internal circuitry dedicated to the debug interface: logic for halting the core, reading registers, and managing breakpoints. This circuitry consumes a small amount of power even when you're not actively debugging. On some devices, particularly those designed for low-power applications, you can disable the debug interface entirely in production to save power, but then you lose the ability to diagnose issues in the field.</p>
<p>Some debug features consume RAM too. If you're using features like real-time tracing or certain advanced breakpoint modes, the chip may need to buffer data internally before sending it to the debug probe. On a microcontroller with only a few kilobytes of RAM, even a small debug buffer can represent a notable percentage of your available memory.</p>
<h2>The Printf Alternative</h2>
<p>Given these costs and the complexity of setting up proper debug hardware, some developers reach for a more familiar approach: printf debugging. The idea is simple. You write diagnostic messages to an unused serial port, connect a USB-to-serial adapter to your development machine, and watch the output in a terminal. It feels comfortable, especially coming from higher-level development where console logging is standard practice.</p>
<p>The problem is that embedded systems often operate under timing constraints that make printf debugging problematic.</p>
<p>Sending characters over a serial port takes time. At 115200 baud, transmitting a single character takes roughly 87 microseconds. A modest debug message of 50 characters might take over 4 milliseconds to transmit. If your system is running a control loop that needs to execute every millisecond, inserting printf statements can break your timing.</p>
<p>This leads to a common frustration in embedded debugging: bugs that disappear when you try to observe them. Adding printf statements changes the timing of your system enough that race conditions, buffer overflows, or interrupt-related bugs no longer manifest. You add logging, the problem vanishes, you remove logging, the problem returns. You might also introduce new bugs that only exist because of the timing changes from your debug output.</p>
<p>There are ways to mitigate this. You can buffer your debug output and transmit it during idle time, or use a faster interface like SWO (Serial Wire Output) that's designed for debug tracing with less overhead. But these approaches add complexity and still don't fully eliminate the observer effect.</p>
<h2>Halting Hardware</h2>
<p>What took me a while to get used to is how literal the cross debugging process is. When you hit a breakpoint during cross debugging, the microcontroller's processor actually stops. The clock is still ticking, but the CPU isn't fetching or executing instructions. You're halting the hardware in place.</p>
<p>This has consequences that don't arise in mobile debugging. If your embedded system is controlling something in the real world, pausing execution might cause problems. A motor might keep spinning, a heater might stay on, or communication with another device might time out.</p>
<p>Similarly, inspecting variables works differently than you might expect. There's no runtime providing nice abstractions. When you ask to see a variable's value, the debugger is reading raw bytes from specific memory addresses and interpreting them based on your source code's type information. If that memory happens to be a hardware register that changes when read, simply inspecting it might alter your system's behaviour.</p>
<h2>What Mobile Development Hides</h2>
<p>Once again, mobile platforms abstract away this complexity. When you debug an iOS or Android app, you're debugging a process running under an operating system that mediates everything. The OS provides memory protection, process isolation, and debugging APIs. You're never directly touching hardware registers or halting a physical processor. And console logging on a modern smartphone has negligible timing impact.</p>
<p>Embedded cross debugging puts you closer to the hardware. You're not debugging through an operating system's abstraction layer; you're interacting with the silicon directly.</p>
<h2>The Practical Reality</h2>
<p>If you're coming from mobile development and setting up cross debugging for the first time, the good news is that the day-to-day experience isn't dramatically different. You'll still set breakpoints, step through code, and inspect variables. The tools have matured to the point where the underlying complexity is largely invisible. Modern embedded IDEs like STM32CubeIDE, <a href="https://platformio.org/">PlatformIO</a>, or VS Code with the right extensions hide much of this complexity behind familiar interfaces. You click a button, your code uploads, and breakpoints work roughly as you'd expect. But underneath, the machinery is different from what you're used to.</p>
<p>But when something goes wrong: when breakpoints don't trigger, when your timing-sensitive code behaves differently under the debugger, when you can't figure out why your debug session won't connect, understanding the architecture helps. Knowing there's a GDB server running on your probe, that your chip has dedicated debug hardware with its own limitations, and that halting the processor means genuinely halting it gives you a mental model for troubleshooting.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2025-12-29T09:31:15.895Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Why embedded systems can't compile their own code]]></title>
        <id>https://www.lowlevel.dev/posts/cross-compile/</id>
        <link href="https://www.lowlevel.dev/posts/cross-compile/"/>
        <updated>2025-12-28T11:21:22.147Z</updated>
        <summary type="html"><![CDATA[When I first started learning embedded systems development, coming from a mobile background, I kept coming across the term 'cross compilatio...]]></summary>
        <content type="html"><![CDATA[<p>When I first started learning embedded systems development, coming from a mobile background, I kept coming across the term 'cross compilation'. Understanding this concept felt important, not only because it's mentioned in the first chapter of nearly ever book/course about embedded development, but also because it seemed to underpin almost everything I was doing when writing software for microcontrollers. What surprised me was realising that mobile development had been shielding me from this complexity for years.</p>
<h2>Your Computer and Your Target Are Very Different</h2>
<p>Think about your laptop or desktop computer. It runs on a powerful processor, likely an Intel or AMD chip using the x86-64 architecture, or if you're on a newer Mac, an ARM-based M-series chip. It has gigabytes of RAM, an operating system many generations deep, and plenty of storage. Now think about an Arduino Uno or a Raspberry Pi Pico. These devices run on completely different processors with different architectures, far less memory, and often no operating system at all.</p>
<p>When you write code and compile it on your laptop, the compiler produces machine code, the actual binary instructions that a processor can execute. But machine code isn't universal. The binary instructions that an Intel processor understands are meaningless to an ARM Cortex-M0 microcontroller. They speak different languages at the hardware level.</p>
<p><img src="https://www.lowlevel.dev/_astro/cross-compiler.DG0Cg__X_Z1Otj4a.webp" alt="Cross Compilation" title="https://www.linuxbaya.in/2020/09/cross-platform-development-toolchain.html" /></p>
<h2>The Solution</h2>
<p>Cross compilation is the process of building executable code on one type of machine (the host) for a different type of machine (the target). Your laptop acts as the host, providing all the computational power and development tools you need. The embedded device is the target, the machine that will eventually run your code.</p>
<p>When you install an embedded development toolchain, you're getting a special compiler designed for exactly this purpose. Instead of producing machine code for your laptop's processor, it produces machine code for your target's processor. The <a href="https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain">GNU ARM Embedded Toolchain</a>, for instance, runs on your development machine but outputs binaries that ARM microcontrollers understand.</p>
<h2>What Mobile Development Hides From You</h2>
<p>Coming from mobile, I initially assumed I'd been doing something similar all along. But the reality is more nuanced: mobile development abstracts away the cross compilation problem rather than exposing you to it directly.</p>
<p>With Android development, when you write Java or Kotlin code, you're not compiling to native machine code at all. The build process produces DEX bytecode, which is architecture-independent. This bytecode runs on the Android Runtime, which handles the translation to native instructions on the device itself. The architecture mismatch between your development machine and the target phone simply doesn't matter because you're shipping portable bytecode rather than native binaries.</p>
<p>iOS development on modern Macs sidesteps the issue differently. Apple's M-series chips are ARM-based, just like iPhone processors. When you build an iOS app on an M1 or M2 Mac, the host and target share the same underlying architecture. The toolchain still does important work adapting for the different operating systems, but you're not dealing with a fundamental architecture mismatch.</p>
<p>Embedded development offers no such abstractions. When I write C for an STM32 microcontroller, the compiler produces raw ARM machine code. There's no runtime to translate bytecode, no shared architecture to lean on.</p>
<h2>Why Can't We Compile Directly on the Target?</h2>
<p>You might wonder why we don't just compile code directly on the embedded device. For some platforms like the Raspberry Pi, which runs a full Linux operating system, you actually can do this, and it's called native compilation. However, for most embedded targets, this isn't practical.</p>
<p>Microcontrollers simply don't have the resources. A compiler is a complex piece of software that requires significant memory and processing power. An ATmega328P with 2KB of RAM and 32KB of flash couldn't possibly host a compiler. Even when resources aren't the limiting factor, development is far more comfortable on your main computer with your favourite editor, debugging tools, and version control.</p>
<h2>The Toolchain</h2>
<p>When people talk about cross compilation, they're usually referring to an entire toolchain rather than just the compiler itself. A typical embedded toolchain includes the compiler that translates your C, C++, Rust code into assembly, the assembler that converts assembly into machine code, the linker that combines various object files and libraries into a final executable, and various utilities for inspecting and manipulating binaries.</p>
<p>Popular toolchains include GCC-based options like the ARM GNU Toolchain and vendor-specific tools like those provided in development environments such as <a href="https://www.st.com/en/development-tools/stm32cubeide.html">STM32CubeIDE</a> or the <a href="https://www.arduino.cc/en/software">Arduino IDE</a>. Each toolchain is configured to understand the specific capabilities and constraints of its target architecture.</p>
<p>The key insight for me as a newcomer was recognising that my development machine and my target device are completely separate execution environments. I'm using one computer as a tool to prepare software for another, much simpler computer.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2025-12-28T11:21:22.147Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Understanding the volatile keyword]]></title>
        <id>https://www.lowlevel.dev/posts/volatile/</id>
        <link href="https://www.lowlevel.dev/posts/volatile/"/>
        <updated>2025-12-27T09:03:21.669Z</updated>
        <summary type="html"><![CDATA[If you've spent any time in embedded development in C, you've likely encountered the volatile keyword. And if you're like me, you've been bi...]]></summary>
        <content type="html"><![CDATA[<h1>Understanding the 'volatile' keyword</h1>
<p>If you've spent any time in embedded development in C, you've likely encountered the <code>volatile</code> keyword. And if you're like me, you've been bitten by infuriating bugs when you forgot to use it (like why does my code break when I turn optimisation on?!).</p>
<p>Despite being one of the most important qualifiers in embedded C, it's frequently misunderstood. This post will explain what <code>volatile</code> actually does, why it matters in embedded contexts, and just as importantly what it doesn't do.</p>
<h2>The Problem: Compilers Are Too Clever but They Don't See Everything</h2>
<p>Modern C compilers are extraordinarily sophisticated optimisers. They analyse your code, track how variables are used, and make transformations to generate faster, smaller machine code. Most of the time, this is exactly what you want.</p>
<p>Consider this innocent-looking code:</p>
<pre><code>int ready = 0;

void wait_for_ready(void) {
    while (ready == 0) {
        // Wait...
    }
}
</code></pre>
<p>A clever compiler might reason: The variable <code>ready</code> is never modified inside this loop. Therefore, if it's zero when we enter, it will always be zero. I can optimise this to:"</p>
<pre><code>void wait_for_ready(void) {
    if (ready == 0) {
        while (1) { }  // Infinite loop
    }
}
</code></pre>
<p>From the compiler's perspective within the abstract machine model of C, this transformation is perfectly valid. It doesn't know, and has no way of knowing, that an interrupt handler running asynchronously might set <code>ready</code> to 1. So while compilers are very sophisticated, they are not omnipotent.</p>
<p>This is the fundamental problem <code>volatile</code> solves.</p>
<h2>What <code>volatile</code> Actually Means</h2>
<p>And this is where the 'volatile' qualifier comes in. It tells the compiler: "This variable's value may change at any time, through means you cannot see or analyse. Do not make any assumptions about its value. Every time the code reads this variable, generate an actual load instruction. Every time the code writes to it, generate an actual store instruction."</p>
<p>Let's fix our earlier example:</p>
<pre><code>volatile int ready = 0;

void wait_for_ready(void) {
    while (ready == 0) {
        // This actually works now
    }
}
</code></pre>
<p>Now the compiler must generate code that genuinely reads from the memory location of <code>ready</code> on every iteration of the loop. If an interrupt handler sets it to 1, our loop will see the change and exit.</p>
<h2>The Three Core Use Cases in Embedded Systems</h2>
<h3>1. Memory-Mapped Hardware Registers</h3>
<p>In embedded systems, hardware peripherals are typically controlled through memory-mapped registers. Reading or writing to a specific memory address communicates with the hardware, not with regular RAM.</p>
<pre><code>// UART status register at address 0x40001000
#define UART_STATUS (*(volatile uint32_t *)0x40001000)

// UART data register at address 0x40001004
#define UART_DATA   (*(volatile uint32_t *)0x40001004)

void uart_send(char c) {
    // Wait until transmit buffer is empty
    while ((UART_STATUS &amp; TX_READY) == 0) {
        // The hardware updates UART_STATUS independently
    }
    UART_DATA = c;
}
</code></pre>
<p>Without <code>volatile</code>, the compiler might:</p>
<ul>
<li>Read <code>UART_STATUS</code> once and cache the value, never seeing when the hardware clears the busy flag</li>
<li>Decide that writing to <code>UART_DATA</code> has no observable effect (since it's never read) and eliminate the write entirely</li>
<li>Reorder the status check and data write, corrupting the transmission</li>
</ul>
<p>Hardware registers have another important property: reading them often has side effects. Reading a UART receive register might clear an interrupt flag or advance a FIFO pointer. The compiler cannot be allowed to eliminate or combine these reads.</p>
<pre><code>// Reading this register clears pending interrupts
volatile uint32_t *interrupt_clear = (uint32_t *)0x40002000;

void clear_pending_interrupts(void) {
    (void)*interrupt_clear;  // The read itself is the operation
}
</code></pre>
<p>Without <code>volatile</code>, the compiler would remove this "useless" read.</p>
<h3>2. Variables Modified by Interrupt Service Routines</h3>
<p>This is perhaps the most common source of <code>volatile</code>-related bugs. Variables shared between your main code and interrupt handlers must be marked <code>volatile</code>:</p>
<pre><code>volatile uint32_t system_ticks = 0;

// Called by timer interrupt
void SysTick_Handler(void) {
    system_ticks++;
}

void delay_ms(uint32_t ms) {
    uint32_t start = system_ticks;
    while ((system_ticks - start) &lt; ms) {
        // Busy wait
    }
}
</code></pre>
<p>The <code>system_ticks</code> variable is modified by code that the compiler cannot see when compiling <code>delay_ms()</code>. The interrupt can fire at any point, completely asynchronously. Without <code>volatile</code>, the compiler has every right to assume <code>system_ticks</code> never changes during the loop.</p>
<h3>3. DMA Buffers and Multi-Core Shared Memory</h3>
<p>Direct Memory Access (DMA) controllers transfer data between peripherals and memory without CPU involvement. The CPU sets up the transfer and then continues executing other code while the DMA engine works in the background.</p>
<pre><code>volatile uint8_t rx_buffer[256];
volatile int rx_complete = 0;

void start_dma_receive(void) {
    // Configure DMA to write to rx_buffer
    // When complete, DMA sets rx_complete = 1
    DMA1-&gt;CMAR = (uint32_t)rx_buffer;
    DMA1-&gt;CCR |= DMA_CCR_EN;
}

void process_received_data(void) {
    while (!rx_complete) { }

    // Process data in rx_buffer
    for (int i = 0; i &lt; 256; i++) {
        handle_byte(rx_buffer[i]);
    }
}
</code></pre>
<p>The CPU didn't write to <code>rx_buffer</code>, so the compiler might assume it contains whatever was there before (possibly uninitialised garbage). The <code>volatile</code> qualifier forces it to actually read the DMA-filled contents.</p>
<h2>A Deeper Look at the Optimisations <code>volatile</code> Prevents</h2>
<p>Let's examine the specific optimisations that <code>volatile</code> inhibits:</p>
<h3>Register Caching</h3>
<p>Normally, compilers keep frequently-accessed variables in CPU registers for speed:</p>
<pre><code>int counter = 0;

void count_events(void) {
    while (check_sensor()) {
        counter++;
    }
}
</code></pre>
<p>The compiler might load <code>counter</code> into a register at the start, increment the register in the loop, and write the final value back only when exiting. With <code>volatile counter</code>, every increment involves a load from memory, an increment, and a store back to memory.</p>
<h3>Dead Store Elimination</h3>
<p>If a variable is written multiple times before being read, the compiler may keep only the final write:</p>
<pre><code>int status = 0;
status = 1;  // Compiler might remove this
status = 2;  // Keep only this
</code></pre>
<p>For hardware registers, each write might trigger a different hardware behaviour. <code>volatile</code> ensures all writes occur.</p>
<h3>Dead Read Elimination</h3>
<p>Similarly, if a value is read but never used, the compiler may remove the read:</p>
<pre><code>int dummy = *status_register;  // Compiler might remove this
</code></pre>
<p>Reading hardware registers often clears flags or advances state machines. <code>volatile</code> prevents this elimination.</p>
<h3>Loop-Invariant Code Motion</h3>
<p>Compilers can move computations that don't change during a loop to outside the loop:</p>
<pre><code>while (running) {
    if (*config_flag) {  // Might be hoisted outside the loop
        do_something();
    }
}
</code></pre>
<p>With <code>volatile</code>, the flag must be checked on every iteration.</p>
<h2>What <code>volatile</code> Does NOT Provide</h2>
<p>This is where many developers get tripped up. The <code>volatile</code> keyword is more limited than people often assume.</p>
<h3>No Atomicity</h3>
<p><code>volatile</code> does not make operations atomic. Consider:</p>
<pre><code>volatile int counter = 0;

// In main code
counter++;

// In interrupt handler
counter++;
</code></pre>
<p>The <code>counter++</code> operation typically compiles to three instructions: load, increment, store. If an interrupt fires between the load and store in the main code, you'll lose an increment.</p>
<p>For atomicity, you need:</p>
<pre><code>// Option 1: Disable interrupts
disable_interrupts();
counter++;
enable_interrupts();

// Option 2: Use atomic types (C11)
#include &lt;stdatomic.h&gt;
atomic_int counter = 0;
atomic_fetch_add(&amp;counter, 1);

// Option 3: Use platform-specific atomic intrinsics
__sync_fetch_and_add(&amp;counter, 1);
</code></pre>
<h3>No Memory Barriers</h3>
<p>On modern processors with out-of-order execution and caching hierarchies, <code>volatile</code> doesn't guarantee other CPUs or cores see your writes when you expect:</p>
<pre><code>volatile int data_ready = 0;
int data[100];  // Non-volatile

void producer(void) {
    // Fill the data array
    for (int i = 0; i &lt; 100; i++) {
        data[i] = compute(i);
    }
    data_ready = 1;  // Signal completion
}

void consumer(void) {
    while (!data_ready) { }
    // BUG: data[] writes might not be visible yet!
    process(data);
}
</code></pre>
<p>Even though <code>data_ready</code> is volatile, the writes to <code>data[]</code> might still be sitting in a write buffer or cache, invisible to the consumer. You need explicit memory barriers:</p>
<pre><code>void producer(void) {
    for (int i = 0; i &lt; 100; i++) {
        data[i] = compute(i);
    }
    __sync_synchronize();  // Memory barrier
    data_ready = 1;
}
</code></pre>
<p>On single-core microcontrollers without write buffers (like many ARM Cortex-M devices), this is less of a concern. But on multi-core systems or processors with complex memory systems, memory barriers are essential.</p>
<h3>No Prevention of Compiler Reordering Around Non-Volatile Accesses</h3>
<p>The compiler can still reorder non-volatile operations with respect to each other, even if they're adjacent to volatile accesses:</p>
<pre><code>volatile int flag = 0;
int a = 0, b = 0;

void setup(void) {
    a = 1;
    b = 2;
    flag = 1;  // Compiler must put this access here...
    // ...but a=1 and b=2 can still be reordered
}
</code></pre>
<p>If you need ordering guarantees for the non-volatile variables, you need compiler barriers:</p>
<pre><code>void setup(void) {
    a = 1;
    b = 2;
    __asm__ __volatile__ ("" ::: "memory");  // Compiler barrier
    flag = 1;
}
</code></pre>
<h2>Best Practices</h2>
<h3>1. Always Use <code>volatile</code> for Hardware Registers</h3>
<p>Create a clear convention in your codebase:</p>
<pre><code>// Hardware abstraction layer
typedef volatile uint32_t reg32_t;

typedef struct {
    reg32_t CR;     // Control register
    reg32_t SR;     // Status register
    reg32_t DR;     // Data register
} UART_TypeDef;

#define UART1 ((UART_TypeDef *)0x40011000)
</code></pre>
<h3>2. Mark All ISR-Shared Variables as <code>volatile</code></h3>
<p>Even if you think you've analysed the code and it's safe, use <code>volatile</code>. Future optimisation levels or compiler versions might break your assumptions:</p>
<pre><code>// ISR communication variables
static volatile bool tx_complete = false;
static volatile uint32_t error_count = 0;
static volatile uint8_t rx_buffer[64];
</code></pre>
<h3>3. Don't Overuse <code>volatile</code></h3>
<p>Every <code>volatile</code> access has a performance cost. Don't mark variables as <code>volatile</code> unless they genuinely can change unexpectedly:</p>
<pre><code>// BAD: Unnecessary volatile
volatile int loop_counter;  // Only modified by this function

for (loop_counter = 0; loop_counter &lt; 100; loop_counter++) {
    // Performance penalty on every access
}

// GOOD: Copy volatile to local for intensive computation
volatile uint32_t raw_adc_value;

void process_adc(void) {
    uint32_t local_value = raw_adc_value;  // Single volatile read
    // Perform many operations on local_value
    // ...
}
</code></pre>
<h3>4. Remember: <code>volatile</code> is Necessary but Not Sufficient</h3>
<p>For correct concurrent code, you typically need <code>volatile</code> plus one or more of:</p>
<ul>
<li>Critical sections (disabled interrupts)</li>
<li>Atomic operations</li>
<li>Memory barriers</li>
<li>Proper mutex/semaphore primitives</li>
</ul>
<p>The <code>volatile</code> keyword is a fundamental tool in embedded C programming. It prevents the compiler from optimising away critical accesses to hardware registers, ISR-shared variables, and DMA buffers. However, it's important to understand its limitations, it provides neither atomicity nor memory ordering guarantees. When working with hardware or interrupt handlers, ask yourself: "Can this variable change in ways the compiler can't see?" If the answer is yes, you need <code>volatile</code>. Then ask: "Do I also need atomicity or memory barriers?" The answer to that question depends on your specific hardware and use case.</p>
<p>Understanding these nuances is what separates embedded code that "seems to work" from code that's genuinely correct. Code that won't mysteriously break when you change compiler versions, enable optimisation, or port to a new platform.</p>
<p>As always, thanks for joining me on this journey into embedded development!</p>
]]></content>
        <author>
            <name>Rob Wells</name>
            <uri>https://www.lowlevel.dev/</uri>
        </author>
        <published>2025-12-27T09:03:21.669Z</published>
    </entry>
</feed>