<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ memory-management - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ memory-management - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 24 Jun 2026 22:47:04 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/memory-management/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ ITCM vs DTCM vs DDR: Embedded Memory Types Explained [Full Handbook] ]]>
                </title>
                <description>
                    <![CDATA[ Most embedded engineers hit this problem early on: the same code on the same processor runs fast in one scenario and surprisingly slow in another. The culprit is almost always where the code and data  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/itcm-vs-dtcm-vs-ddr-embedded-memory-types-explained-handbook/</link>
                <guid isPermaLink="false">69fb8bbc50ecad4533638e41</guid>
                
                    <category>
                        <![CDATA[ embedded systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory-management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikheel Vishwas Savant ]]>
                </dc:creator>
                <pubDate>Wed, 06 May 2026 18:43:08 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/66013473-45d1-4f6f-87f4-727bf75e0c5e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most embedded engineers hit this problem early on: the same code on the same processor runs fast in one scenario and surprisingly slow in another. The culprit is almost always <em>where</em> the code and data are stored in memory.</p>
<p>Desktop and server processors hide memory latency behind multi-level caches. Many embedded processors, especially ARM Cortex-M and Cortex-R based chips, take a different approach. They give you direct control over multiple memory regions, each with very different performance characteristics.</p>
<p>This handbook covers what ITCM, DTCM, and DDR memory are, how they differ, how to place code and data in the right region, and how to profile and monitor firmware memory usage over time.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-embedded-memory-architecture-matters">Why Embedded Memory Architecture Matters</a></p>
</li>
<li><p><a href="#heading-what-is-itcm-instruction-tightly-coupled-memory">What is ITCM (Instruction Tightly-Coupled Memory)?</a></p>
</li>
<li><p><a href="#heading-what-is-dtcm-data-tightly-coupled-memory">What is DTCM (Data Tightly-Coupled Memory)?</a></p>
</li>
<li><p><a href="#heading-what-is-ddr-double-data-rate-memory">What is DDR (Double Data Rate) Memory?</a></p>
</li>
<li><p><a href="#heading-how-they-compare-a-side-by-side-overview">How They Compare: A Side-by-Side Overview</a></p>
</li>
<li><p><a href="#heading-how-to-decide-where-to-place-code-and-data">How to Decide Where to Place Code and Data</a></p>
</li>
<li><p><a href="#heading-how-the-linker-script-controls-memory-placement">How the Linker Script Controls Memory Placement</a></p>
</li>
<li><p><a href="#heading-common-mistakes-to-avoid">Common Mistakes to Avoid</a></p>
</li>
<li><p><a href="#heading-performance-comparison-with-real-numbers">Performance Comparison With Real Numbers</a></p>
</li>
<li><p><a href="#heading-how-tcm-affects-power-consumption">How TCM Affects Power Consumption</a></p>
</li>
<li><p><a href="#heading-how-to-profile-memory-usage">How to Profile Memory Usage</a></p>
</li>
<li><p><a href="#heading-summary">Summary</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most from this guide, you should have a basic understanding of C programming, including pointers, structs, and the difference between static and local variables.</p>
<p>Some familiarity with embedded development concepts like compiling, linking, and flashing firmware to a target board will also help.</p>
<p>Finally, a general sense of how a CPU fetches and executes instructions will make the performance discussions easier to follow.</p>
<p>You don't need to be an expert in any of these. The article explains each concept as it comes up.</p>
<h2 id="heading-why-embedded-memory-architecture-matters">Why Embedded Memory Architecture Matters</h2>
<p>A modern embedded processor might be clocked at 400 MHz or higher. It can execute an instruction every few nanoseconds.</p>
<p>But when it needs to fetch that instruction from memory, or read a variable, the memory might not keep up. The processor ends up stalling, waiting for the memory subsystem to deliver the data it asked for. Those stall cycles add up fast.</p>
<p>On a desktop computer, hardware caches (L1, L2, L3) sit between the CPU and main memory, automatically keeping recently-used data nearby. The cache hardware decides what to keep and what to evict, and it does this transparently. The programmer rarely needs to think about it, and performance is generally good enough without manual intervention.</p>
<p>On many embedded processors, the situation is different. Instead of hardware caches, you get <strong>three distinct memory regions</strong>, each attached to the CPU in a different way.</p>
<table>
<thead>
<tr>
<th>Memory Type</th>
<th>What It Stores</th>
<th>Access Speed</th>
<th>Typical Size</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ITCM</strong></td>
<td>Instructions (executable code)</td>
<td>Single-cycle (deterministic)</td>
<td>512 KB to 2 MB</td>
</tr>
<tr>
<td><strong>DTCM</strong></td>
<td>Data (variables, stacks, buffers)</td>
<td>Single-cycle (deterministic)</td>
<td>512 KB to 1.5 MB</td>
</tr>
<tr>
<td><strong>DDR</strong></td>
<td>Everything else</td>
<td>Multi-cycle (variable)</td>
<td>4 MB to several GB</td>
</tr>
</tbody></table>
<p>The table above shows the three memory types you'll encounter on a typical ARM Cortex-M or Cortex-R-based embedded system. ITCM and DTCM are fast but small. DDR is slow but large.</p>
<p>The "deterministic" label on TCM means that the access time is always the same, every single time, regardless of what accessed that memory before or what else is happening on the chip. The "variable" label on DDR means the access time can change depending on the internal state of the DDR chip and its controller.</p>
<p>You, the developer, control which region each piece of your firmware lives in. The compiler and linker don't make these decisions automatically. You specify them through section attributes in your source code and placement rules in your linker script. Getting this right is often the difference between firmware that meets its real-time deadlines and firmware that misses them.</p>
<h2 id="heading-what-is-itcm-instruction-tightly-coupled-memory">What is ITCM (Instruction Tightly-Coupled Memory)?</h2>
<p>ITCM stands for <strong>Instruction Tightly-Coupled Memory</strong>.</p>
<p>The "Instruction" part means this memory is used for storing executable machine code, the compiled instructions your CPU fetches and runs.</p>
<p>The "Tightly-Coupled" part means the memory is physically located on the same silicon die as the CPU core, connected through a dedicated bus with no arbitration or contention. There's no shared bus to compete with. There's no cache hierarchy to traverse. The CPU asks for an instruction, and ITCM delivers it directly, through a private path that nothing else on the chip can interfere with.</p>
<p>The CPU can fetch an instruction from ITCM in a <strong>single clock cycle, every time</strong>. This access time is both fast and deterministic. It doesn't vary based on access patterns, recent history, or what else is happening on the bus.</p>
<p>This determinism is just as important as the raw speed, because it makes worst-case execution time analysis possible. In safety-critical systems, you need to be able to <em>prove</em> that a function will always complete within a certain number of cycles. ITCM makes that proof much simpler.</p>
<h3 id="heading-why-single-cycle-fetch-matters">Why Single-Cycle Fetch Matters</h3>
<p>Every line of C code compiles down to one or more machine instructions. Each of those instructions must be fetched from memory before the CPU can decode and execute it. This fetch step happens for every single instruction, so even small per-instruction delays compound rapidly in loops and frequently-called functions.</p>
<p>Consider a loop that runs 1,000,000 iterations, where each iteration involves 10 instruction fetches. That's 10 million fetches total.</p>
<pre><code class="language-shell">ITCM:  10,000,000 fetches x 1 cycle  = 10,000,000 cycles
DDR:   10,000,000 fetches x 8 cycles = 80,000,000 cycles

Difference: 70,000,000 cycles
At 400 MHz: 70,000,000 / 400,000,000 = 0.175 seconds = 175 ms
</code></pre>
<p>This calculation compares the total cycle count when the same loop runs from ITCM versus DDR. With ITCM, each fetch takes 1 cycle, so 10 million fetches cost 10 million cycles.</p>
<p>With DDR, each fetch takes 8 cycles (a conservative average), so the same 10 million fetches cost 80 million cycles. The difference is 70 million cycles, which at 400 MHz translates to 175 milliseconds.</p>
<p>In a real-time system running a control loop at 1 kHz (one iteration every 1 ms), 175 ms of extra latency spread across your processing isn't a minor inconvenience. It can cause the system to miss deadlines, drop sensor readings, or produce incorrect outputs. In motor control applications, a missed deadline can mean physical damage to the hardware. In audio processing, it means audible glitches. The cost of slow instruction fetch isn't abstract.</p>
<h3 id="heading-what-should-go-in-itcm">What Should Go in ITCM?</h3>
<p>Because ITCM is small (typically 512 KB to 2 MB), you can't fit your entire firmware in it. You need to be selective about what earns a spot.</p>
<p><strong>Interrupt Service Routines (ISRs)</strong> are the highest-priority candidates. ISRs run in response to hardware events like a timer tick, an ADC conversion completing, or a communication peripheral receiving data. They need to execute and return as quickly as possible.</p>
<p>A slow ISR delays all lower-priority interrupts and can cause missed events. If your ISR fetches its instructions from DDR, each fetch takes multiple cycles, and the total ISR execution time increases by a factor that could push it past its deadline.</p>
<p>Placing ISRs in ITCM ensures they run at maximum speed with completely predictable timing.</p>
<p><strong>Real-time processing functions</strong> are the next priority. These include signal processing routines, motor control loops, audio processing pipelines, and any function that runs at a fixed rate and must complete within a strict time budget.</p>
<p>If your audio codec callback needs to process a buffer of samples every 5 ms, every instruction fetch cycle counts. Placing these functions in ITCM gives you the maximum amount of CPU time for actual computation rather than waiting on memory.</p>
<p><strong>Inner loops of your main processing pipeline</strong> also benefit significantly from ITCM placement. If your firmware spends 80% of its time in a handful of functions, those functions should be in ITCM. Profiling tools and the linker map file (covered later in this article) can help you identify which functions are the hottest.</p>
<p><strong>Functions that require deterministic timing</strong> belong in ITCM even if they aren't the fastest path. ITCM access time doesn't vary, which makes timing analysis predictable. This matters for safety-critical systems (automotive, medical, aerospace) where you need to prove worst-case execution times to a certification authority.</p>
<h3 id="heading-how-to-place-a-function-in-itcm">How to Place a Function in ITCM</h3>
<p>You use a GCC section attribute to tell the compiler that a function belongs in a specific memory section. Then, in your linker script, you map that section to the ITCM memory region.</p>
<pre><code class="language-c">__attribute__((section(".itcm_text")))
void my_critical_isr(void) {
    volatile uint32_t *sensor_reg = (volatile uint32_t *)0x40001000;
    uint32_t reading = *sensor_reg;
    process_sample(reading);
}
</code></pre>
<p>In this code, the <code>__attribute__((section(".itcm_text")))</code> directive tells the compiler to emit this function's compiled machine code into a section called <code>.itcm_text</code> instead of the default <code>.text</code> section. The function itself reads a sensor register at the memory-mapped address <code>0x40001000</code>, stores the result in a local variable, and passes it to <code>process_sample()</code> for further processing. The <code>volatile</code> keyword tells the compiler that this memory address can change at any time (because it is a hardware register), so the compiler must not optimize away the read.</p>
<p>On its own, the section attribute doesn't determine where the function ends up in physical memory. It just tells the compiler to label the function's code with a specific section name.</p>
<p>The actual memory placement is the linker script's job, which maps <code>.itcm_text</code> to the ITCM address range. We'll cover the linker script in detail in a later section.</p>
<h3 id="heading-how-much-itcm-is-typical">How Much ITCM is Typical?</h3>
<p>A real-world memory profile from an embedded project, to give you a sense of scale:</p>
<pre><code class="language-shell">Memory region         Used Size  Region Size  %age Used
            ITCM:      570936 B         2 MB     27.22%
            DTCM:      727240 B    1572608 B     46.24%
             DDR:      622915 B         4 MB     14.85%
</code></pre>
<p>This output comes from the linker map file's summary section. It shows three memory regions and how much of each one is used by the compiled firmware.</p>
<p>ITCM has 2 MB available and the firmware is using about 557 KB (27.22%). DTCM has about 1.5 MB available and is using 727 KB (46.24%). DDR has 4 MB available and is using about 609 KB (14.85%).</p>
<p>This project uses about 557 KB of the available 2 MB of ITCM, roughly 27%. That leaves good headroom for growth.</p>
<p>In practice, you want to keep ITCM utilization below 80-85% to leave room for future features and library updates. If utilization climbs above 90%, you're one feature addition away from a build failure, and you should proactively move less-critical code to DDR.</p>
<h2 id="heading-what-is-dtcm-data-tightly-coupled-memory">What is DTCM (Data Tightly-Coupled Memory)?</h2>
<p>DTCM stands for <strong>Data Tightly-Coupled Memory</strong>. It works on the same principle as ITCM (physically close to the CPU core, connected via a dedicated bus, single-cycle access) but it stores <strong>data</strong> instead of instructions.</p>
<p>If ITCM is where your code lives, DTCM is where your code <em>works</em>. It's the fast scratch space that the CPU reads from and writes to while executing your performance-critical functions. Every variable read, every array access, every stack push and pop in your hot code paths goes through data memory. Making that data memory as fast as possible eliminates one of the biggest sources of stall cycles.</p>
<h3 id="heading-what-kind-of-data-belongs-in-dtcm">What Kind of Data Belongs in DTCM?</h3>
<p><strong>Stack frames</strong> are the most important thing in DTCM. Every function call pushes a stack frame containing local variables, the return address, and saved registers. Every function return pops that frame. I</p>
<p>f your stack is in DTCM, the memory-access portion of function calls and returns happens in a single cycle. If your stack were in DDR, every function call and return would incur multiple cycles of memory latency just for the stack operations alone, before the function even begins doing useful work.</p>
<p>On most Cortex-M and Cortex-R configurations, the startup code initializes the stack pointer to point into DTCM by default, so you get this benefit without any extra configuration.</p>
<p><strong>Frequently accessed global variables</strong> are another strong candidate. State machine variables, control flags, sensor readings that are updated and read in every loop iteration, counters that are incremented in ISRs and read in the main loop: all of these benefit from single-cycle access.</p>
<p>If a variable is read or written thousands of times per second, the cumulative latency difference between DTCM and DDR adds up.</p>
<p><strong>Small lookup tables used in hot paths</strong> belong in DTCM when they're small enough to fit. Sine/cosine tables for motor control, filter coefficients for audio processing, and CRC tables for communication protocols are common examples.</p>
<p>These tables are typically a few hundred bytes to a few kilobytes, and they get accessed on every iteration of a processing loop. The key word is "small." A 512-byte sine table is a good fit for DTCM. A 64 KB calibration table is not, and should go in DDR instead.</p>
<p><strong>DMA buffers</strong> can sometimes go in DTCM, but this depends on your chip's bus architecture. On some chips, the DMA controller has a direct path to DTCM through the bus matrix. On others, the DMA controller can only reach DDR and possibly other SRAM regions. If you place a DMA buffer in DTCM on a chip where the DMA controller can't reach it, the transfer will silently fail or write to a completely wrong address.</p>
<p>Always check your chip's bus matrix diagram in the reference manual before putting DMA buffers in DTCM.</p>
<h3 id="heading-how-to-place-data-in-dtcm">How to Place Data in DTCM</h3>
<p>Placing data in DTCM uses the same section attribute mechanism as ITCM, but with a section name that your linker script maps to the DTCM address range.</p>
<pre><code class="language-c">__attribute__((section(".dtcm_data")))
static int16_t audio_buffer[256];

__attribute__((section(".dtcm_data")))
static volatile uint32_t sensor_state = 0;
</code></pre>
<p>In this code, <code>audio_buffer</code> is an array of 256 signed 16-bit integers (512 bytes total) that will be placed in DTCM. This could be a buffer for audio samples that gets filled by a DMA transfer and processed by an ISR. The <code>static</code> keyword means the buffer has file scope and persists for the lifetime of the program (it's not allocated on the stack).</p>
<p>The <code>sensor_state</code> variable is a 32-bit unsigned integer marked as <code>volatile</code>, meaning the compiler must read it from memory every time it's accessed rather than caching it in a register.</p>
<p>This is important for variables that are written in an ISR and read in the main loop, since the compiler needs to know the value can change at any time. Placing it in DTCM ensures that both the ISR write and the main loop read happen in a single cycle.</p>
<h3 id="heading-dtcm-fills-up-faster-than-itcm">DTCM Fills Up Faster Than ITCM</h3>
<p>Looking at the memory profile again:</p>
<pre><code class="language-shell">            DTCM:      727240 B    1572608 B     46.24%
</code></pre>
<p>This single line from the linker map file summary shows that DTCM has 1,572,608 bytes (about 1.5 MB) available, and the firmware is using 727,240 bytes (about 710 KB), which is 46.24% of the total capacity.</p>
<p>DTCM fills up faster than ITCM because many things compete for it: your stack, your heap (if you have one), your global variables, and data sections from every library you link against. Every C library function that uses static data, every RTOS data structure, every middleware component brings its own data footprint. This creates a constant sizing exercise.</p>
<p>For every data structure, you need to ask: does this really need single-cycle access, or can it work from DDR?</p>
<h3 id="heading-a-concrete-example-of-the-performance-impact">A Concrete Example of the Performance Impact</h3>
<p>Say your processor runs at 400 MHz. DTCM gives you 1-cycle access. DDR gives you 8-cycle access. You have a lookup table that gets accessed 100,000 times per second.</p>
<pre><code class="language-shell">DTCM: 100,000 accesses x 1 cycle  = 100,000 cycles/sec
DDR:  100,000 accesses x 8 cycles = 800,000 cycles/sec

Difference: 700,000 cycles/sec
At 400 MHz: 700,000 / 400,000,000 = 0.00175 seconds = 1.75 ms
</code></pre>
<p>This calculation shows the cycle cost of 100,000 memory accesses per second in both memory types. In DTCM, each access is 1 cycle, totaling 100,000 cycles. In DDR, each access is 8 cycles, totaling 800,000 cycles. The difference of 700,000 cycles per second, at a 400 MHz clock rate, translates to 1.75 milliseconds of additional CPU time spent waiting on memory.</p>
<p>If you're running a real-time control loop at 1 kHz (1 ms period), 1.75 ms of additional memory latency per second means that some individual iterations are running longer than their 1 ms budget. Whether this causes actual deadline misses depends on how the accesses are distributed across iterations and how much slack you have in your time budget, but it shows why memory placement decisions have real consequences in embedded systems.</p>
<h2 id="heading-what-is-ddr-double-data-rate-memory">What is DDR (Double Data Rate) Memory?</h2>
<p>DDR is external memory. It sits on the circuit board outside the processor die, connected through a memory controller. It's much larger than TCM (typically 4 MB to several GB), but significantly slower to access.</p>
<p>The name "Double Data Rate" refers to how data is transferred between the DDR chip and the memory controller: data is sent on both the rising edge and the falling edge of the clock signal, effectively doubling the transfer rate compared to a single-data-rate design. But this doesn't eliminate the latency of activating rows and columns inside the DDR chip, which is where the slowness comes from.</p>
<h3 id="heading-how-ddr-access-works">How DDR Access Works</h3>
<p>When your CPU reads from DDR, a multi-step process occurs inside the memory controller and DDR chip.</p>
<p>First, the CPU sends an address request to the memory controller. The memory controller is a hardware block inside the processor that translates CPU addresses into the specific row and column addresses that the DDR chip understands.</p>
<p>Second, the memory controller activates the correct row inside the DDR chip. This step is called the RAS (Row Address Strobe) phase. The DDR chip is organized as a grid of tiny capacitors, and "activating a row" means reading all the capacitors in that row into a row buffer inside the DDR chip. This takes several clock cycles.</p>
<p>Third, the memory controller selects the correct column within the activated row. This is called the CAS (Column Address Strobe) phase. The DDR chip uses the column address to pick the right bits out of the row buffer. This also takes several clock cycles.</p>
<p>Fourth, the data is transferred back to the memory controller, and from there to the CPU. The data transfer happens on both clock edges (the "double data rate" part), which helps with throughput but doesn't reduce the initial latency of the RAS and CAS phases.</p>
<p>The total latency depends on what state the memory is in when the request arrives. If the correct row is already activated from a previous access (a "row hit"), the RAS phase can be skipped, and the access is faster. If a different row is active and needs to be closed (precharged) before the new row can be opened (a "row miss"), the access takes longer. If the DDR chip happens to be performing a refresh cycle at that moment, the access is delayed further.</p>
<p>In practice, DDR access latency ranges from about 5 to 20+ CPU clock cycles, depending on the access pattern and timing.</p>
<h3 id="heading-why-ddr-is-necessary">Why DDR is Necessary</h3>
<p>Because firmware often doesn't fit in TCM alone. Real embedded projects include protocol stacks, connectivity libraries, file system drivers, debug interfaces, and more. TCM is typically 2 to 3.5 MB total (ITCM + DTCM combined), and a full-featured firmware image can easily exceed that.</p>
<p>A real example showing memory usage before and after adding a wireless connectivity stack:</p>
<pre><code class="language-shell">Without connectivity stack:
    ITCM:      506,996 B     (24.18%)
    DTCM:      628,408 B     (39.96%)
    DDR:       558,779 B     (13.32%)

With connectivity stack:
    ITCM:      570,936 B     (27.22%)
    DTCM:      727,240 B     (46.24%)
    DDR:       622,915 B     (14.85%)

Delta:
    ITCM: +63,940 B   (~62 KB of additional code)
    DTCM: +98,832 B   (~96 KB of additional data)
    DDR:  +64,136 B   (~62 KB of additional data/code)
</code></pre>
<p>This comparison shows memory usage from the same project built with and without a wireless connectivity stack.</p>
<p>The "Without" rows show the baseline. The "With" rows show the usage after adding the connectivity feature. The "Delta" rows show the difference.</p>
<p>Adding this single feature consumed an extra ~220 KB across all three memory regions. The time-critical parts of the stack (interrupt handlers, buffer management) went into ITCM and DTCM. The rest (packet parsers, connection management, configuration logic) went into DDR where it doesn't need single-cycle performance.</p>
<h3 id="heading-what-belongs-in-ddr">What Belongs in DDR?</h3>
<p><strong>Initialization and configuration code</strong> is the easiest category. Functions that run once at boot, like parsing a configuration file, initializing peripherals, or setting up data structures, don't need fast execution. They run once, take a few extra milliseconds because of DDR latency, and then never run again. Nobody notices. Put them in DDR and save TCM space for the code that runs a million times per second.</p>
<p><strong>Large buffers</strong> must go in DDR because they simply can't fit in TCM. An image framebuffer for a 320x240 display at 16 bits per pixel is 150 KB. A network packet pool might be 32 KB or more. A file system cache might be 64 KB. These buffers would consume a significant fraction of DTCM's total capacity, leaving no room for the stack and variables that actually need single-cycle access.</p>
<p><strong>Infrequently accessed data</strong> belongs in DDR as well. Calibration tables that are loaded once at boot and then read occasionally during operation, string tables for debug messages that are only printed during development or error conditions, and error description tables are all fine in DDR. The extra latency per access is irrelevant when the access count is low.</p>
<p><strong>Non-time-critical code</strong> rounds out the DDR category. Protocol stacks (Bluetooth, Wi-Fi, TCP/IP), file system drivers, OTA update handlers, and shell/debug command interpreters all do important work, but none of them need to execute in a single clock cycle per instruction. They can tolerate the higher latency of DDR without affecting system behavior.</p>
<h3 id="heading-how-to-place-code-and-data-in-ddr">How to Place Code and Data in DDR</h3>
<pre><code class="language-c">__attribute__((section(".ddr_text")))
void parse_config_file(const char *path) {
    // Runs from DDR, slower instruction fetch,
    // but config parsing happens once at boot,
    // so the latency does not affect runtime performance.
}

__attribute__((section(".ddr_bss")))
static uint8_t network_packet_pool[32768];

__attribute__((section(".ddr_bss")))
static uint8_t framebuffer[320 * 240 * 2];  // 150 KB, far too large for TCM
</code></pre>
<p>In this code, <code>parse_config_file</code> is placed in the <code>.ddr_text</code> section, which the linker script maps to DDR. Every instruction in this function will be fetched from DDR at multi-cycle latency, but since config parsing happens once at boot, the extra time is negligible.</p>
<p>The <code>network_packet_pool</code> is a 32 KB buffer placed in <code>.ddr_bss</code>. The <code>.bss</code> suffix is a convention indicating that this is zero-initialized data (the linker will ensure the memory is zeroed at startup rather than storing 32 KB of zeros in the firmware image). This buffer is used for network packet storage, which is not time-critical enough to justify DTCM space.</p>
<p>The <code>framebuffer</code> is a 150 KB buffer (320 pixels wide, 240 pixels tall, 2 bytes per pixel) also placed in <code>.ddr_bss</code>. At 150 KB, this single buffer would consume about 10% of DTCM's total capacity, which is far too expensive when the display update isn't a hard real-time operation.</p>
<h2 id="heading-how-they-compare-a-side-by-side-overview">How They Compare: A Side-by-Side Overview</h2>
<table>
<thead>
<tr>
<th>Property</th>
<th>ITCM</th>
<th>DTCM</th>
<th>DDR</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Purpose</strong></td>
<td>Instruction storage</td>
<td>Data storage</td>
<td>General-purpose storage</td>
</tr>
<tr>
<td><strong>Location</strong></td>
<td>On-die, dedicated bus</td>
<td>On-die, dedicated bus</td>
<td>Off-chip, through memory controller</td>
</tr>
<tr>
<td><strong>Access latency</strong></td>
<td>1 cycle (deterministic)</td>
<td>1 cycle (deterministic)</td>
<td>5 to 20+ cycles (variable)</td>
</tr>
<tr>
<td><strong>Typical size</strong></td>
<td>512 KB to 2 MB</td>
<td>512 KB to 1.5 MB</td>
<td>4 MB to several GB</td>
</tr>
<tr>
<td><strong>Technology</strong></td>
<td>SRAM</td>
<td>SRAM</td>
<td>DRAM (requires refresh)</td>
</tr>
<tr>
<td><strong>Power</strong></td>
<td>Low (no refresh needed)</td>
<td>Low (no refresh needed)</td>
<td>Higher (constant refresh)</td>
</tr>
<tr>
<td><strong>Best for</strong></td>
<td>ISRs, real-time loops, DSP</td>
<td>Stack, hot variables, lookup tables</td>
<td>Large buffers, init code, protocol stacks</td>
</tr>
</tbody></table>
<p>This table summarizes the key differences between the three memory types. The most important columns are "Access latency" and "Typical size," because they represent the fundamental tradeoff: TCM is fast but small, DDR is slow but large.</p>
<p>The "Technology" column explains why: TCM uses SRAM (static RAM), which stores each bit using a flip-flop circuit that holds its state as long as power is applied. DDR uses DRAM (dynamic RAM), which stores each bit as charge in a tiny capacitor. Because capacitors leak charge, DRAM must be periodically refreshed, which adds power consumption and introduces occasional access delays when a refresh cycle coincides with a read request.</p>
<h3 id="heading-the-memory-map">The Memory Map</h3>
<pre><code class="language-markdown">Address Space:
  +------------------------------+  0x00000000
  |                              |
  |         ITCM (2 MB)          |  Single-cycle Inst Fetch
  |    ISRs, real-time loops,    |
  |    DSP, critical code        |
  |                              |
  +------------------------------+  0x00200000
  |       (reserved/gap)         |
  +------------------------------+  0x20000000
  |                              |
  |       DTCM (~1.5 MB)         |  Single-cycle Data Access
  |    Stack, hot variables,     |
  |    lookup tables, DMA bufs   |
  |                              |
  +------------------------------+  0x20180000
  |       (reserved/gap)         |
  +------------------------------+  0x80000000
  |                              |
  |         DDR (4 MB)           |  Multi-cycle Access
  |    Large buffers, init code, |
  |    protocol stacks, config   |
  |                              |
  +------------------------------+  0x80400000
</code></pre>
<p>This diagram shows the CPU's address space laid out from low addresses at the top to high addresses at the bottom. ITCM occupies the lowest 2 MB starting at address 0x00000000. After a gap of reserved/unused address space, DTCM sits at 0x20000000 and spans about 1.5 MB. Another gap of reserved space follows, and then DDR starts at 0x80000000 with 4 MB of space.</p>
<p>The gaps between regions are important. They're reserved address ranges that don't map to any physical memory. If your code accidentally reads from or writes to an address in one of these gaps, the result depends on the chip's bus fault configuration: it might trigger a HardFault exception, or it might silently return garbage data.</p>
<p>These addresses are illustrative. Every chip has its own memory map, documented in its Technical Reference Manual (TRM). Always consult your chip's TRM for the exact addresses and sizes.</p>
<h2 id="heading-how-to-decide-where-to-place-code-and-data">How to Decide Where to Place Code and Data</h2>
<pre><code class="language-plaintext">Is it code or data?
|
+-- CODE (instructions):
|   +-- Called from an ISR or runs in a real-time loop?
|   |   +-- YES -&gt; ITCM (deterministic timing is critical)
|   +-- Called frequently in the main processing pipeline?
|   |   +-- YES -&gt; ITCM (if space is available)
|   +-- Called rarely (init, config, debug)?
|       +-- DDR (save ITCM space for critical code)
|
+-- DATA (variables, buffers, tables):
    +-- Accessed in an ISR or real-time context?
    |   +-- YES -&gt; DTCM (single-cycle, deterministic)
    +-- Small and frequently accessed?
    |   +-- YES -&gt; DTCM (if space is available)
    +-- Large buffer (&gt;16 KB)?
    |   +-- Probably DDR (DTCM cannot afford the space)
    +-- Accessed only once at boot or very rarely?
        +-- DDR (do not use DTCM for this)
</code></pre>
<p>This decision tree captures the thought process for placing each piece of firmware into the right memory region.</p>
<p>Start by asking whether you're placing code (instructions) or data (variables, buffers, tables). For code, the primary question is how often it runs and whether it has timing constraints. ISR code and real-time loop code goes in ITCM. Everything else goes in DDR. For data, the primary question is how often it's accessed and how large it is. Small, frequently accessed data goes in DTCM. Large buffers and rarely-accessed data go in DDR.</p>
<p>The general principle: <strong>put the hottest code and data in TCM, and everything else in DDR</strong>. "Hot" means frequently accessed, latency-sensitive, or requiring deterministic timing. When in doubt, start with DDR placement and move things to TCM only when profiling shows it's necessary. It's much easier to promote a function from DDR to ITCM after discovering it's a bottleneck than to cram everything into ITCM from the start and run out of space.</p>
<h2 id="heading-how-the-linker-script-controls-memory-placement">How the Linker Script Controls Memory Placement</h2>
<p>Everything we've discussed so far (section attributes, memory placement, address assignments) comes together in the <strong>linker script</strong>. This is a file (usually with a <code>.ld</code> extension) that tells the linker exactly which sections go into which memory regions. The linker script is the single source of truth for your firmware's memory layout.</p>
<pre><code class="language-plaintext">MEMORY
{
    ITCM    (rx)  : ORIGIN = 0x00000000, LENGTH = 2M
    DTCM    (rw)  : ORIGIN = 0x20000000, LENGTH = 1536K
    DDR     (rwx) : ORIGIN = 0x80000000, LENGTH = 4M
}

SECTIONS
{
    /* === ITCM: Critical code === */
    .itcm_text :
    {
        KEEP(*(.isr_vector))          /* Interrupt vector table */
        *(.itcm_text)                 /* Functions with __attribute__((section(".itcm_text"))) */
        *audio_processing.o(.text)    /* All code from audio_processing.c */
        *motor_control.o(.text)       /* All code from motor_control.c */
    } &gt; ITCM

    /* === DDR: Non-critical code === */
    .ddr_text :
    {
        *(.text)                      /* Default catch-all for remaining code */
        *(.text*)
        *(.rodata)                    /* Read-only data (string literals, constants) */
        *(.rodata*)
    } &gt; DDR

    /* === DTCM: Critical data === */
    .dtcm_data :
    {
        *(.dtcm_data)                 /* Data with __attribute__((section(".dtcm_data"))) */
        *audio_processing.o(.data)    /* All initialized data from audio_processing.c */
        *audio_processing.o(.bss)     /* All zero-initialized data from audio_processing.c */
    } &gt; DTCM

    /* === DTCM: Stack === */
    .stack (NOLOAD) :
    {
        . = ALIGN(8);
        __stack_start = .;
        . = . + 8K;                  /* 8 KB stack */
        __stack_end = .;
    } &gt; DTCM

    /* === DDR: Everything else === */
    .ddr_data :
    {
        *(.data)                      /* Default catch-all for remaining initialized data */
        *(.bss)                       /* Default catch-all for remaining zero-initialized data */
        *(COMMON)
    } &gt; DDR
}
</code></pre>
<p>This linker script has two main blocks: <code>MEMORY</code> and <code>SECTIONS</code>.</p>
<p>The <code>MEMORY</code> block defines the physical memory regions available on the chip. Each line declares a region name, its permissions (<code>rx</code> for read-execute, <code>rw</code> for read-write, <code>rwx</code> for read-write-execute), its starting address (<code>ORIGIN</code>), and its size (<code>LENGTH</code>). These values must match your chip's actual memory map as documented in its reference manual.</p>
<p>The <code>SECTIONS</code> block defines how the linker should distribute compiled code and data across those memory regions. Each section rule consists of a section name (like <code>.itcm_text</code>), a list of input patterns that specify which object file sections to include, and a <code>&gt; REGION</code> directive that tells the linker which memory region to place the output section in.</p>
<p>The <code>.itcm_text</code> section collects the interrupt vector table (<code>KEEP(*(.isr_vector))</code>), any functions explicitly marked with <code>__attribute__((section(".itcm_text")))</code>, and all code from <code>audio_processing.o</code> and <code>motor_control.o</code>. The <code>KEEP</code> directive prevents the linker from discarding the interrupt vector table during garbage collection, even if no code appears to reference it directly. All of this goes into ITCM.</p>
<p>The <code>.ddr_text</code> section uses catch-all patterns <code>*(.text)</code> and <code>*(.text*)</code> to collect all remaining code that wasn't claimed by the ITCM section above. It also collects read-only data (<code>.rodata</code>), which includes string literals and <code>const</code> variables. All of this goes into DDR.</p>
<p>The <code>.dtcm_data</code> section collects explicitly-placed data and all data from <code>audio_processing.o</code>. The <code>.stack</code> section reserves 8 KB for the stack with 8-byte alignment, and exports the <code>__stack_start</code> and <code>__stack_end</code> symbols that your startup code and stack profiling code can reference. Both go into DTCM.</p>
<p>The <code>.ddr_data</code> section collects all remaining data with catch-all patterns, and goes into DDR.</p>
<h3 id="heading-how-section-matching-works">How Section Matching Works</h3>
<p>The linker processes sections from top to bottom. When it encounters a wildcard pattern like <code>*(.text)</code>, it matches all <code>.text</code> sections that haven't already been claimed by a more specific rule earlier in the script.</p>
<p>So in the example above, <code>*audio_processing.o(.text)</code> in the ITCM section claims all code from <code>audio_processing.c</code> first. Then, when the linker reaches <code>*(.text)</code> in the DDR section, <code>audio_processing.o</code>'s <code>.text</code> section has already been placed, so it's skipped. Only unclaimed <code>.text</code> sections from other object files match the DDR catch-all.</p>
<p>This means the <strong>order of sections in your linker script matters</strong>. Place your specific rules (individual object files, named sections) before the generic catch-all rules. If you put the <code>*(.text)</code> catch-all before the <code>*audio_processing.o(.text)</code> rule, the catch-all would claim everything first, and the specific rule would match nothing.</p>
<h2 id="heading-common-mistakes-to-avoid">Common Mistakes to Avoid</h2>
<h3 id="heading-1-stack-overflow-in-dtcm">1. Stack Overflow in DTCM</h3>
<p>Your stack lives in DTCM. DTCM is small. If you declare a large local array inside a function, it goes on the stack:</p>
<pre><code class="language-c">void problematic_function(void) {
    uint8_t huge_local_buffer[65536];  // 64 KB allocated on the stack
    // This consumes 64 KB of DTCM immediately
}
</code></pre>
<p>This code declares a 64 KB local array. Because it's a local variable (not <code>static</code>), it is allocated on the stack when the function is called. If your total stack size is 8 KB (as in the linker script example above), this single declaration overflows the stack by 56 KB, writing into whatever memory is adjacent to the stack in DTCM.</p>
<p>On a desktop OS, a stack overflow triggers a segmentation fault because the OS uses virtual memory and guard pages to detect it.</p>
<p>In an embedded system without memory protection, the stack silently grows into adjacent memory regions, corrupting whatever data is stored there. The resulting bugs are extremely difficult to diagnose because the symptoms (corrupted variables, erratic behavior, intermittent crashes) appear unrelated to the actual cause. You might spend days debugging a seemingly random data corruption issue before realizing the root cause is a stack overflow from a function three call levels deep.</p>
<p><strong>The fix</strong>: Use <code>static</code> allocation or heap allocation for large buffers, and place them in DDR:</p>
<pre><code class="language-c">void fixed_function(void) {
    __attribute__((section(".ddr_bss")))
    static uint8_t huge_buffer[65536];  // In DDR, not on the stack

    // Stack is safe, DTCM is not wasted
}
</code></pre>
<p>By making the buffer <code>static</code>, it's no longer allocated on the stack. Instead, the linker allocates it once in the <code>.ddr_bss</code> section, which maps to DDR. The buffer persists for the entire lifetime of the program (like a global variable), but its name is scoped to this function. The stack only holds a pointer to the buffer, which is a few bytes instead of 64 KB.</p>
<h3 id="heading-2-overfilling-itcm">2. Overfilling ITCM</h3>
<p>If you exceed ITCM's capacity, the linker will produce an error along the lines of "region ITCM overflowed by N bytes." But if you're <em>close</em> to the limit, you're one library update or feature addition away from a build failure. A minor version bump of your RTOS or connectivity stack could add enough code to push ITCM over the edge.</p>
<p>Keep headroom. The 27% utilization shown earlier is healthy. If you're above 85%, you should actively work on moving less-critical code to DDR. If you're above 95%, you have no room for growth and need to make immediate changes. Setting up automated memory budget checks in your CI pipeline (covered later in this article) prevents surprises.</p>
<h3 id="heading-3-ignoring-alignment-requirements">3. Ignoring Alignment Requirements</h3>
<p>TCM memories often have alignment requirements. On Cortex-M processors with strict alignment enforcement, accessing a 32-bit value at an unaligned address causes a HardFault exception.</p>
<pre><code class="language-c">/* Problematic: packed struct can create unaligned fields */
__attribute__((section(".dtcm_data"), packed))
struct badly_aligned {
    uint8_t  flag;
    uint32_t counter;  // May be at byte offset 1, unaligned
};

/* Correct: natural alignment, with minor padding */
__attribute__((section(".dtcm_data")))
struct properly_aligned {
    uint32_t counter;  // At offset 0, 4-byte aligned
    uint8_t  flag;     // At offset 4
    // 3 bytes of padding follow, a small cost for correctness
};
</code></pre>
<p>In the first struct, the <code>packed</code> attribute tells the compiler to use no padding between fields. This means <code>counter</code> starts at byte offset 1 (right after the 1-byte <code>flag</code>), which isn't a multiple of 4. When the CPU tries to read a 32-bit value from a non-4-byte-aligned address in TCM, it triggers a HardFault on processors with strict alignment (which includes most Cortex-M cores).</p>
<p>In the second struct, the fields are ordered so that <code>counter</code> (4 bytes) comes first at offset 0, which is naturally 4-byte aligned. The <code>flag</code> (1 byte) follows at offset 4. The compiler inserts 3 bytes of padding after <code>flag</code> to bring the struct size to 8 bytes (a multiple of 4), but this is a small price for correct, crash-free operation.</p>
<h3 id="heading-4-dma-transfers-to-tcm-on-incompatible-bus-architectures">4. DMA Transfers to TCM on Incompatible Bus Architectures</h3>
<p>Some DMA controllers can't access TCM memory. Whether DMA can reach TCM depends entirely on your chip's internal bus architecture (the bus matrix).</p>
<p>If you set up a DMA transfer from a peripheral to a DTCM buffer, but the DMA controller doesn't have a bus path to DTCM, the transfer will either silently fail or write to an incorrect address.</p>
<p>Neither produces an obvious error. The DMA controller thinks it completed successfully, your code reads the buffer expecting fresh data, and you get stale or garbage values instead. This is one of the most confusing bugs in embedded development because everything <em>looks</em> correct in the code.</p>
<p><strong>Always check your chip's bus matrix diagram</strong> in the reference manual before using DMA with TCM buffers. The bus matrix diagram shows which masters (CPU, DMA, USB, and so on) can access which slaves (ITCM, DTCM, SRAM, DDR, peripherals). Look for whether the DMA controller's master port has a connection line to the TCM slave port. If it doesn't, your DMA transfers to TCM will not work.</p>
<h2 id="heading-performance-comparison-with-real-numbers">Performance Comparison With Real Numbers</h2>
<p>The following table compares access latencies across memory types, assuming a Cortex-R class processor at 400 MHz:</p>
<pre><code class="language-markdown">+---------------------+----------+----------+----------+
| Operation           | ITCM/    |   DDR    | Slowdown |
|                     | DTCM     |          | Factor   |
+---------------------+----------+----------+----------+
| Instruction fetch   | 1 cycle  | 5-20 cyc |   5-20x  |
| Data read (32-bit)  | 1 cycle  | 5-20 cyc |   5-20x  |
| Data write (32-bit) | 1 cycle  | 5-20 cyc |   5-20x  |
| Sequential burst    | 1 cyc/wd | 2-4 cy/wd|    2-4x  |
| Random access       | 1 cycle  | 10-20 cyc|  10-20x  |
+---------------------+----------+----------+----------+
</code></pre>
<p>This table shows the latency for five different types of memory operations. The first three rows (instruction fetch, data read, data write) show that individual accesses to TCM are always 1 cycle, while individual accesses to DDR range from 5 to 20 cycles depending on the memory's internal state. The slowdown factor is the ratio between the two.</p>
<p>The "Sequential burst" row shows what happens when you read or write consecutive addresses. DDR performs much better in burst mode (2-4 cycles per word instead of 5-20) because once a row is activated, subsequent reads from the same row skip the RAS phase. TCM is still 1 cycle per word because it doesn't have the row/column structure of DDR.</p>
<p>The "Random access" row shows the worst case for DDR. When each access hits a different row, the memory controller must precharge the old row and activate the new one every time. This is the 10-20 cycle range, and it's common in workloads that jump around in memory (traversing linked lists, hash table lookups, and indirect function calls through function pointer arrays).</p>
<p>The practical takeaway: if your code accesses DDR data, try to access it sequentially. Iterating through an array in order is much faster than jumping to random positions. Your memory controller and the DDR chip's internal prefetch logic work in your favor during sequential access patterns.</p>
<h2 id="heading-how-tcm-affects-power-consumption">How TCM Affects Power Consumption</h2>
<p>Memory placement has a direct impact on power consumption, something that becomes critical for battery-powered products.</p>
<p><strong>DDR requires constant refresh cycles.</strong> DRAM stores each bit as charge in a tiny capacitor, and that charge leaks over time.</p>
<p>To prevent data loss, the memory controller must read and rewrite every row in the DDR chip approximately every 64 ms. This refresh process consumes power even when the processor is sleeping and no code is running. On some systems, DDR refresh can account for a significant portion of the total sleep-mode power budget.</p>
<p><strong>TCM is SRAM-based and doesn't require refresh.</strong> SRAM stores data using flip-flop circuits that hold their state as long as power is applied. There is some leakage current (no transistor is perfect), but it is orders of magnitude lower than DDR refresh power.</p>
<p>For battery-powered devices (wearables, IoT sensors, medical devices), this means you should keep data that must survive sleep modes in DTCM when possible.</p>
<p>If your hardware supports it, power-gate the DDR chip during deep sleep to eliminate its refresh power entirely. The less DDR your firmware uses at runtime, the more aggressively you can manage DDR power states, which directly extends battery life.</p>
<h2 id="heading-how-to-profile-memory-usage">How to Profile Memory Usage</h2>
<p>After placing code and data into ITCM, DTCM, and DDR, you need to verify that everything fits, monitor usage over time, and catch regressions before they become build failures. There are several techniques for this, ranging from simple command-line tools to automated CI checks.</p>
<h3 id="heading-method-1-the-linker-map-file">Method 1: The Linker Map File</h3>
<p>Every time you build your firmware, the linker can produce a <strong>map file</strong>, a detailed text file that records where every symbol (function, variable, constant) ended up and how large it is. This is the most useful single artifact in embedded development for understanding memory usage.</p>
<p>To generate one, add <code>-Wl,-Map=output.map</code> to your linker flags:</p>
<pre><code class="language-shell">arm-none-eabi-gcc \
    -T linker_script.ld \
    -Wl,-Map=firmware.map \
    -o firmware.elf \
    main.o audio.o bluetooth.o
</code></pre>
<p>This command invokes the ARM GCC toolchain to link three object files (<code>main.o</code>, <code>audio.o</code>, <code>bluetooth.o</code>) using the linker script <code>linker_script.ld</code>. The <code>-Wl,-Map=firmware.map</code> flag tells GCC to pass the <code>-Map=firmware.map</code> option to the linker, which causes it to write a detailed map file alongside the output ELF binary. The map file can be thousands of lines long, but the most useful part is the summary at the end.</p>
<p>The summary at the end of the map file shows overall utilization per memory region:</p>
<pre><code class="language-shell">Memory region         Used Size  Region Size  %age Used
            ITCM:      570936 B         2 MB     27.22%
            DTCM:      727240 B    1572608 B     46.24%
             DDR:      622915 B         4 MB     14.85%
</code></pre>
<p>This summary shows three columns: how many bytes are used, the total size of the region, and the percentage used. It gives you the health of your firmware at a glance. As a rule of thumb, below 80% is healthy with room for growth. Between 80% and 90% is getting tight, and you should plan for how you will accommodate the next feature. Above 90% requires action: start moving things to a cheaper memory region or optimizing existing placement.</p>
<h3 id="heading-method-2-parsing-the-map-file-for-per-module-breakdown">Method 2: Parsing the Map File for Per-Module Breakdown</h3>
<p>The summary tells you <em>how much</em> memory is used, but not <em>who</em> is using it. The map file contains per-symbol details, but they're difficult to read manually because the file can be thousands of lines long with a format that isn't designed for human consumption.</p>
<p>The following Python script parses the map file and produces a per-module report showing which object files are consuming memory in which regions.</p>
<pre><code class="language-python">#!/usr/bin/env python3
"""Parse a linker map file and report memory usage per object file."""

import re
import sys
from collections import defaultdict

def parse_map_file(map_path):
    """Extract symbol placements from a GCC linker map file."""
    usage = defaultdict(lambda: defaultdict(int))

    regions = {
        'ITCM': (0x00000000, 0x00200000),
        'DTCM': (0x20000000, 0x20180000),
        'DDR':  (0x80000000, 0x80400000),
    }

    def addr_to_region(addr):
        for name, (start, end) in regions.items():
            if start &lt;= addr &lt; end:
                return name
        return 'UNKNOWN'

    symbol_re = re.compile(
        r'^\s+\S+\s+(0x[0-9a-fA-F]+)\s+(0x[0-9a-fA-F]+)\s+(\S+\.o)'
    )

    with open(map_path) as f:
        for line in f:
            m = symbol_re.match(line)
            if m:
                addr = int(m.group(1), 16)
                size = int(m.group(2), 16)
                obj = m.group(3).split('/')[-1]
                region = addr_to_region(addr)
                usage[obj][region] += size

    return usage

def print_report(usage):
    """Print a sorted memory usage report."""
    print(f"{'Object File':&lt;35} {'ITCM':&gt;10} {'DTCM':&gt;10} {'DDR':&gt;10} {'Total':&gt;10}")
    print("-" * 80)

    totals = defaultdict(int)
    rows = []

    for obj, regions in usage.items():
        total = sum(regions.values())
        rows.append((obj, regions, total))
        for r, s in regions.items():
            totals[r] += s

    rows.sort(key=lambda x: x[2], reverse=True)

    for obj, regions, total in rows[:20]:
        print(f"{obj:&lt;35} "
              f"{regions.get('ITCM', 0):&gt;10,} "
              f"{regions.get('DTCM', 0):&gt;10,} "
              f"{regions.get('DDR', 0):&gt;10,} "
              f"{total:&gt;10,}")

    print("-" * 80)
    grand = sum(totals.values())
    print(f"{'TOTAL':&lt;35} "
          f"{totals.get('ITCM', 0):&gt;10,} "
          f"{totals.get('DTCM', 0):&gt;10,} "
          f"{totals.get('DDR', 0):&gt;10,} "
          f"{grand:&gt;10,}")

if __name__ == '__main__':
    usage = parse_map_file(sys.argv[1])
    print_report(usage)
</code></pre>
<p>This script does three things. First, <code>parse_map_file</code> reads the map file line by line, looking for lines that match the format of a symbol placement entry (a section name, an address, a size, and an object file name). For each match, it converts the hex address to an integer, determines which memory region it falls in using the <code>addr_to_region</code> helper, and accumulates the size into a nested dictionary keyed by object file and region.</p>
<p>Second, <code>print_report</code> sorts the object files by total memory usage (largest first), prints the top 20, and shows how much each one uses in each region.</p>
<p>Third, the <code>if __name__ == '__main__'</code> block makes the script runnable from the command line.</p>
<p>You'll need to adjust the address ranges in the <code>regions</code> dictionary to match your chip's memory map.</p>
<p>Run it with:</p>
<pre><code class="language-shell">python3 parse_map.py firmware.map
</code></pre>
<p>Sample output:</p>
<pre><code class="language-shell">Object File                              ITCM       DTCM        DDR      Total
--------------------------------------------------------------------------------
bluetooth_stack.o                      42,380     65,200     38,400    146,080
audio_processing.o                     89,200     32,000          0    121,200
wifi_driver.o                          21,560     33,632     25,736     80,928
sensor_hub.o                           45,000     18,400          0     63,400
libc.a(memcpy.o)                       12,340          0          0     12,340
...
--------------------------------------------------------------------------------
TOTAL                                 570,936    727,240    622,915  1,921,091
</code></pre>
<p>This output shows the top memory consumers in the firmware, sorted by total usage. Each row shows an object file and how many bytes it contributes to each memory region.</p>
<p>The <code>bluetooth_stack.o</code> file is the largest consumer at 146 KB total, spread across all three regions. The <code>audio_processing.o</code> file uses 121 KB, all in ITCM and DTCM (0 bytes in DDR), which makes sense because audio processing is time-critical and was placed entirely in TCM. The <code>libc.a(memcpy.o)</code> entry shows a C library function that was placed in ITCM, likely because it is called from performance-critical code paths.</p>
<h3 id="heading-method-3-the-size-command">Method 3: The <code>size</code> Command</h3>
<p>For a quick check without parsing the map file, use <code>arm-none-eabi-size</code>:</p>
<pre><code class="language-shell">arm-none-eabi-size -A firmware.elf
</code></pre>
<p>Output:</p>
<pre><code class="language-shell">firmware.elf  :
section               size        addr
.itcm_text          570936           0
.dtcm_data          530240   536870912
.dtcm_bss           196000   537401152
.stack                8192   537600000
.ddr_text           422915  2147483648
.ddr_data           120000  2147906563
.ddr_bss             80000  2148026563
Total              1928283
</code></pre>
<p>This output lists every section in the ELF binary, its size in bytes, and its starting address (shown in decimal).</p>
<p>You can map sections to memory regions by looking at the address: addresses near 0 are ITCM, addresses near 536 million (0x20000000) are DTCM, and addresses near 2.1 billion (0x80000000) are DDR.</p>
<p>Alternatively, the section names themselves indicate the region (<code>.itcm_text</code> is in ITCM, <code>.dtcm_data</code> and <code>.dtcm_bss</code> are in DTCM, <code>.ddr_text</code> and <code>.ddr_data</code> and <code>.ddr_bss</code> are in DDR).</p>
<p>The <code>-A</code> flag gives per-section sizes instead of the default BSD-format output. It's less detailed than the map file approach, but it runs instantly and gives you the big picture.</p>
<h3 id="heading-method-4-runtime-stack-profiling">Method 4: Runtime Stack Profiling</h3>
<p>Static analysis (map files, <code>size</code> output) tells you about compile-time placement. But some memory usage is dynamic, particularly the stack, which grows and shrinks at runtime based on call depth and local variable sizes. A function that allocates a 2 KB local buffer only uses that stack space while it is executing, so static analysis can't tell you the peak stack usage.</p>
<p>A common technique is <strong>stack watermarking</strong>: fill the entire stack region with a known pattern at boot, then periodically check how much of the pattern has been overwritten.</p>
<pre><code class="language-c">#define STACK_FILL_PATTERN 0xDEADBEEF

void stack_watermark_init(void) {
    extern uint32_t __stack_start;
    extern uint32_t __stack_end;
    uint32_t *p = &amp;__stack_start;

    register uint32_t sp asm("sp");
    while (p &lt; (uint32_t *)(sp - 64)) {
        *p++ = STACK_FILL_PATTERN;
    }
}

uint32_t stack_usage_bytes(void) {
    extern uint32_t __stack_start;
    extern uint32_t __stack_end;
    uint32_t *p = &amp;__stack_start;

    while (p &lt; &amp;__stack_end &amp;&amp; *p == STACK_FILL_PATTERN) {
        p++;
    }

    return (uint32_t)(&amp;__stack_end) - (uint32_t)p;
}

void check_stack_health(void) {
    uint32_t used = stack_usage_bytes();
    uint32_t total = 8192;
    uint32_t percent = (used * 100) / total;

    if (percent &gt; 80) {
        log_warning("Stack usage: %lu / %lu bytes (%lu%%)",
                    used, total, percent);
    }
}
</code></pre>
<p>The <code>stack_watermark_init</code> function fills the stack memory (from <code>__stack_start</code> to just below the current stack pointer) with the pattern <code>0xDEADBEEF</code>. The <code>extern</code> declarations reference the linker symbols defined in the linker script's <code>.stack</code> section. The <code>register uint32_t sp asm("sp")</code> line reads the current stack pointer value so the function knows where to stop filling (you do not want to overwrite your own stack frame). The 64-byte safety margin ensures the fill loop doesn't get too close to the active stack.</p>
<p>The <code>stack_usage_bytes</code> function scans from the bottom of the stack upward, counting how many words still contain the fill pattern. The first word that does <em>not</em> match the pattern indicates the deepest point the stack has reached (the high-water mark). The function returns the number of bytes from that point to the top of the stack.</p>
<p>The <code>check_stack_health</code> function computes the percentage of stack used and logs a warning if it exceeds 80%. Call this function periodically during normal operation to monitor stack usage.</p>
<p>Call <code>stack_watermark_init()</code> as early as possible in your startup code (before <code>main()</code> if you can), then call <code>check_stack_health()</code> periodically during normal operation. This tells you the high-water mark, the maximum stack depth your firmware has reached so far.</p>
<h3 id="heading-method-5-tracking-memory-across-builds">Method 5: Tracking Memory Across Builds</h3>
<p>Every time you add a feature or merge a change, run the memory profile before and after:</p>
<pre><code class="language-shell">arm-none-eabi-size -A firmware_before.elf &gt; mem_before.txt
arm-none-eabi-size -A firmware_after.elf &gt; mem_after.txt
diff mem_before.txt mem_after.txt
</code></pre>
<p>These three commands capture the section sizes of two firmware builds (before and after a change) into text files, then diff them to see what changed. This is useful but the raw diff output can be hard to read. The following script provides a cleaner view by computing the delta per memory region:</p>
<pre><code class="language-shell">#!/bin/bash
# memory_diff.sh - Compare memory usage between two builds

echo "Memory Impact of Change:"
echo "========================"

parse_size() {
    arm-none-eabi-size -A "$1" | awk '
    /\.itcm/  { itcm += $2 }
    /\.dtcm/  { dtcm += $2 }
    /\.ddr/   { ddr += $2 }
    /\.stack/ { dtcm += $2 }
    END { printf "%d %d %d", itcm, dtcm, ddr }
    '
}

read itcm_before dtcm_before ddr_before &lt;&lt;&lt; \((parse_size "\)1")
read itcm_after  dtcm_after  ddr_after  &lt;&lt;&lt; \((parse_size "\)2")

printf "ITCM: %+d bytes (%d -&gt; %d)\n" \
    \(((itcm_after - itcm_before)) \)itcm_before $itcm_after
printf "DTCM: %+d bytes (%d -&gt; %d)\n" \
    \(((dtcm_after - dtcm_before)) \)dtcm_before $dtcm_after
printf "DDR:  %+d bytes (%d -&gt; %d)\n" \
    \(((ddr_after - ddr_before)) \)ddr_before $ddr_after
</code></pre>
<p>This script takes two ELF files as arguments (the "before" and "after" builds). The <code>parse_size</code> function runs <code>arm-none-eabi-size -A</code> on the given ELF file and uses <code>awk</code> to sum up section sizes by memory region. Sections whose names contain <code>.itcm</code> are counted toward ITCM, sections containing <code>.dtcm</code> or <code>.stack</code> toward DTCM, and sections containing <code>.ddr</code> toward DDR. The main body reads the before and after values, then prints the delta for each region with a <code>+</code> or <code>-</code> sign.</p>
<p>Usage and output:</p>
<pre><code class="language-shell">$ ./memory_diff.sh firmware_without_bt.elf firmware_with_bt.elf

Memory Impact of Change:
========================
ITCM: +63940 bytes (506996 -&gt; 570936)
DTCM: +98832 bytes (628408 -&gt; 727240)
DDR:  +64136 bytes (558779 -&gt; 622915)
</code></pre>
<p>This output shows that adding the Bluetooth feature increased ITCM by about 62 KB, DTCM by about 96 KB, and DDR by about 62 KB. You can put this in your CI/CD pipeline so that every pull request shows exactly how much memory it costs.</p>
<h3 id="heading-method-6-automated-memory-budget-checks-in-ci">Method 6: Automated Memory Budget Checks in CI</h3>
<p>You can integrate memory profiling into your CI/CD pipeline to catch overflows before they land in your main branch.</p>
<pre><code class="language-shell">#!/bin/bash
# memory_check.sh - Fail CI if memory usage exceeds thresholds

ITCM_LIMIT=85   # percent
DTCM_LIMIT=80
DDR_LIMIT=90

check_region() {
    local name=\(1 used=\)2 total=\(3 limit=\)4
    local percent=$((used * 100 / total))

    if [ \(percent -ge \)limit ]; then
        echo "FAIL: \(name usage is \){percent}% (limit: ${limit}%)"
        echo "      Used: \(used / \)total bytes"
        return 1
    else
        echo "OK:   \(name usage is \){percent}% (limit: ${limit}%)"
        return 0
    fi
}

ITCM_USED=\((grep "ITCM:" firmware.map | awk '{print \)2}')
ITCM_TOTAL=$((2 * 1024 * 1024))

DTCM_USED=\((grep "DTCM:" firmware.map | awk '{print \)2}')
DTCM_TOTAL=1572608

DDR_USED=\((grep "DDR:" firmware.map | awk '{print \)2}')
DDR_TOTAL=$((4 * 1024 * 1024))

FAILED=0
check_region "ITCM" \(ITCM_USED \)ITCM_TOTAL $ITCM_LIMIT || FAILED=1
check_region "DTCM" \(DTCM_USED \)DTCM_TOTAL $DTCM_LIMIT || FAILED=1
check_region "DDR"  \(DDR_USED  \)DDR_TOTAL  $DDR_LIMIT  || FAILED=1

exit $FAILED
</code></pre>
<p>This script reads memory usage numbers from the linker map file and compares them against configurable percentage thresholds. The <code>check_region</code> function takes a region name, the number of bytes used, the total bytes available, and the percentage limit. It computes the actual percentage and prints either "OK" or "FAIL" along with the numbers. If any region exceeds its limit, the script exits with a non-zero status, which causes the CI build to fail.</p>
<p>The thresholds at the top (85% for ITCM, 80% for DTCM, 90% for DDR) should be adjusted based on your project's growth rate and how much headroom you want to maintain. DTCM has a lower limit because it fills up faster and is harder to free up.</p>
<p>Add this script to your build pipeline so every pull request shows its memory cost. If a change pushes any region past its threshold, the build fails and the developer knows immediately.</p>
<h3 id="heading-method-7-heap-tracking-at-runtime">Method 7: Heap Tracking at Runtime</h3>
<p>If your embedded project uses dynamic memory allocation (<code>malloc</code>/<code>free</code>), you can wrap the allocator to track usage.</p>
<pre><code class="language-c">static size_t heap_used = 0;
static size_t heap_peak = 0;

void *tracked_malloc(size_t size) {
    size_t *block = (size_t *)malloc(size + sizeof(size_t));
    if (!block) return NULL;

    *block = size;
    heap_used += size;
    if (heap_used &gt; heap_peak) {
        heap_peak = heap_used;
    }

    return (void *)(block + 1);
}

void tracked_free(void *ptr) {
    if (!ptr) return;
    size_t *block = ((size_t *)ptr) - 1;
    heap_used -= *block;
    free(block);
}

void print_heap_stats(void) {
    printf("Heap: current=%zu bytes, peak=%zu bytes\n",
           heap_used, heap_peak);
}
</code></pre>
<p>This code wraps <code>malloc</code> and <code>free</code> with tracking logic. The <code>tracked_malloc</code> function allocates slightly more memory than requested (an extra <code>sizeof(size_t)</code> bytes) and stores the requested size in the first word of the allocation. It then updates the <code>heap_used</code> counter and, if the new total exceeds the previous peak, updates <code>heap_peak</code>. It returns a pointer that's offset past the size header, so the caller sees a normal pointer to their data.</p>
<p>The <code>tracked_free</code> function reverses the process: it subtracts one <code>size_t</code> from the pointer to find the hidden size header, subtracts that size from <code>heap_used</code>, and calls the real <code>free</code> on the original block.</p>
<p>The <code>print_heap_stats</code> function prints the current and peak heap usage. Call it periodically or on demand through a debug interface (UART console, debug CLI) to monitor how much heap your firmware is using.</p>
<p>This approach has a small overhead (one extra word per allocation), but it gives you visibility into dynamic memory usage that's otherwise completely invisible. It's especially useful for tracking down memory leaks: if <code>heap_used</code> keeps growing over time without ever decreasing, something is allocating without freeing.</p>
<h2 id="heading-summary">Summary</h2>
<p>Embedded processors based on ARM Cortex-M and Cortex-R architectures give you direct control over three memory regions with very different performance characteristics.</p>
<p><strong>ITCM (Instruction Tightly-Coupled Memory)</strong> stores your most performance-critical code. It provides single-cycle, deterministic instruction fetch. It's small (typically 512 KB to 2 MB), so reserve it for ISRs, real-time processing functions, and hot loops.</p>
<p><strong>DTCM (Data Tightly-Coupled Memory)</strong> stores your most performance-critical data. It also provides single-cycle, deterministic access. Your stack lives here by default. It's even smaller than ITCM and fills up quickly, so be deliberate about what you place in it.</p>
<p><strong>DDR (Double Data Rate) memory</strong> stores everything else. It's much larger but slower (5 to 20+ cycles per access, with variable latency). Use it for initialization code, large buffers, protocol stacks, and anything that doesn't need deterministic timing.</p>
<p>You control placement through <code>__attribute__((section(...)))</code> in your C code and section-to-region mappings in your linker script. You verify placement through map files, the <code>size</code> command, and runtime profiling techniques like stack watermarking. The core skill is knowing which region each piece of your firmware belongs in, and having the tooling to catch mistakes early.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Understanding Escape Analysis in Go – Explained with Example Code ]]>
                </title>
                <description>
                    <![CDATA[ In most languages, the stack and heap are two ways a program stores data in memory, managed by the language runtime. Each is optimized for different use cases, such as fast access or flexible lifetimes. Go follows the same model, but you usually don’... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/understanding-escape-analysis-in-go/</link>
                <guid isPermaLink="false">698e1c7090ca92017618cb24</guid>
                
                    <category>
                        <![CDATA[ Go Language ]]>
                    </category>
                
                    <category>
                        <![CDATA[ golang ]]>
                    </category>
                
                    <category>
                        <![CDATA[ optimization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ stack ]]>
                    </category>
                
                    <category>
                        <![CDATA[ heap ]]>
                    </category>
                
                    <category>
                        <![CDATA[ escape analysis ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory-management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Eti Ijeoma ]]>
                </dc:creator>
                <pubDate>Thu, 12 Feb 2026 18:31:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770921059389/c45b42cb-8cff-4de5-b3d1-7c7adad402c5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In most languages, the stack and heap are two ways a program stores data in memory, managed by the language runtime. Each is optimized for different use cases, such as fast access or flexible lifetimes.</p>
<p>Go follows the same model, but you usually don’t decide between the stack and the heap directly. Instead, the Go compiler decides where values live. If the compiler can prove a value is only needed within the current function call, it can keep it on the stack. If it cannot prove that, the value “escapes” and is placed on the heap. This technique is called <strong>escape analysis</strong>.</p>
<p>This matters because heap allocations increase garbage collector work. In code that runs often, that extra work can show up as more CPU spent in GC, more allocations, and less predictable performance.</p>
<p>In this article, I’ll explain what escape analysis is, the common patterns that trigger heap allocation, and how to confirm and reduce avoidable allocations.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-do-you-really-need-to-care-about-escape-analysis">Do You Really Need to Care About Escape Analysis?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-memory-layout-and-lifecycle">Memory Layout and Lifecycle</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-sharing-down-and-sharing-up">Sharing Down and Sharing Up</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-escape-analysis-in-practice">Escape Analysis in Practice</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-escape-analysis-to-guide-performance">How to Use Escape Analysis to Guide Performance</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-further-reading">Further Reading</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<ul>
<li><p>Familiarity with Go fundamentals (functions, variables, structs, slices, maps)</p>
</li>
<li><p>Basic understanding of pointers in Go (<code>&amp;</code> and <code>*</code>)</p>
</li>
<li><p>A general idea of how goroutines work</p>
</li>
</ul>
<h2 id="heading-do-you-really-need-to-care-about-escape-analysis">Do You Really Need to Care About Escape Analysis?</h2>
<p>Before we go deeper, I want to call this out clearly. For the correctness of your program, it doesn’t matter whether a variable lives on the stack or on the heap, or whether you know that detail. The Go compiler is smart enough to place values where they need to be so that your program behaves correctly.</p>
<p>Most of the time, you don’t need to think about this at all. It only starts to matter when performance becomes a problem. If your program is already fast enough, you’re done, and there’s no point trying to squeeze out extra speed.</p>
<p>You should only start caring about stack vs heap when you have benchmarks that show your program is too slow, and those same benchmarks point to heavy heap allocation and garbage collection as part of the problem.</p>
<h2 id="heading-memory-layout-and-lifecycle"><strong>Memory Layout and Lifecycle</strong></h2>
<p>To get a better understanding of what escape analysis is, you first need a simple picture of how Go lays out memory while your program runs. At this level, it comes down to the stack each goroutine uses, how stack frames are carved out of that stack, and when values move to the heap where the garbage collector can see them.</p>
<h3 id="heading-goroutine-stacks-and-stack-frames">Goroutine Stacks and Stack Frames</h3>
<p>When a Go program starts, the runtime creates the <code>main</code> goroutine, and every <code>go</code> statement creates a new goroutine, each with its own stack.</p>
<p>There’s not a single global stack for the whole process. As of writing this article, with Go v1.25.7, each goroutine gets an initial contiguous block of 2,048 bytes of memory, which acts as its stack. The stack is where Go stores data that belongs to function calls. When a goroutine calls a function, Go reserves a chunk of that goroutine’s stack for the function’s local data. That chunk is called a <strong>stack frame</strong>.</p>
<p>It holds the function’s local variables and the call state needed to return and continue execution. If that function calls another function, a new frame is added on top. When the inner function returns, its frame becomes invalid, and the goroutine continues in the caller’s frame.</p>
<p>A stack frame only lives for as long as the function is active. Once the function returns, anything stored in its frame is considered invalid, even if the raw bytes are still in memory and will be reused later. Code must not rely on those values after the return</p>
<p>Go stacks can grow. A goroutine starts with a small stack and the runtime grows it when needed, but the lifetime rule stays the same. A value is safe in a stack frame only if nothing can still reference it after the function returns. If it might be referenced later, it can’t stay in that frame and must be placed somewhere safer.</p>
<h3 id="heading-pointers-and-lifetime">Pointers and Lifetime</h3>
<p>In Go, taking an address like <code>p := &amp;x</code> means you now have a pointer in one stack frame that refers to a value which may have been created in another frame. When you pass that pointer into a function, Go still passes by value. The callee gets its own pointer variable on its own stack frame, but the address inside still points to the same underlying value. So pointers are how you share access to one value across several frames without copying the value itself.</p>
<p>Lifetime becomes important when a pointer can outlive the frame where the pointed value was created. As long as both the pointer and the value live inside frames that are still active in the current call stack, everything is safe.</p>
<p>Once a pointer might still exist after the original frame has returned, the value can no longer stay in that frame, because that frame will become invalid. At that point, the value has to be placed in a safer location so that no pointer ever points into dead stack memory.</p>
<h2 id="heading-sharing-down-and-sharing-up">Sharing Down and Sharing Up</h2>
<p>Now that you have a picture of stacks, frames, and pointers, we can look at two common ways pointers move through your code. I’ll call them sharing down and sharing up. The names aren’t special Go terms. They’re just a simple way to describe how a pointer moves along the call stack.</p>
<h3 id="heading-sharing-down">Sharing Down</h3>
<p>Sharing down means a function passes a pointer or reference to functions it calls. The pointer moves deeper into the call stack, but the value it points to still belongs to a frame that is active.</p>
<p>Example code:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span>

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    n := <span class="hljs-number">10</span>
    multiply(&amp;n) 
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">multiply</span><span class="hljs-params">(v *<span class="hljs-keyword">int</span>)</span></span> {
   *v = *v * <span class="hljs-number">2</span>
}
</code></pre>
<p>In <code>main</code>, you take the address of <code>n</code> and pass it into <code>multiply</code>. While <code>multiply</code> runs, both the <code>main</code> frame and the <code>multiply</code> frames are active. The pointer in <code>multiply</code> points to a value that still lives in an active frame, so this situation is safe from a lifetime point of view.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770153560595/0681b535-3668-406d-acaf-6e27679d52e1.png" alt="Diagram showing two stack frames on one goroutine, with the upper frame pointing to a value in the lower frame to illustrate sharing down on the stack" class="image--center mx-auto" width="1252" height="1102" loading="lazy"></p>
<p>In the diagram below, after the <code>multiply</code> function runs and returns, the <code>multiply</code> frame becomes invalid, and we don’t need to do anything because the stack pointer is simply popped back to the previous frame's address. This action automatically reclaims all the memory used by that function in one step, so the garbage collector is not involved in cleaning up stack memory</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770153730555/9093b49a-a8bc-497d-be06-77738a89a6c4.png" alt="Diagram showing two stack frames with a value in the upper frame updated through a pointer stored in the lower frame, again illustrating sharing down entirely on the stack" class="image--center mx-auto" width="1346" height="1130" loading="lazy"></p>
<h3 id="heading-sharing-up">Sharing Up</h3>
<p>Sharing up means a function returns a pointer, or stores it somewhere that will still be around after the function returns. The pointer moves back up the call stack or into some longer-lived state while the frame that created the value is about to end, so that value can no longer be tied to that one frame.</p>
<p>The same idea shows up when you share a value with another goroutine, because Go doesn’t let one goroutine hold pointers into another goroutine’s stack, so shared data needs a lifetime that is not tied to a single stack.</p>
<h4 id="heading-heap-garbage-collection-and-lifetime">Heap, garbage collection, and lifetime</h4>
<p>Values that might outlive a single stack frame can’t stay in that frame. The compiler places them on the heap instead. The heap is a separate region of memory that isn’t tied to one function call. Any goroutine can hold pointers to heap values, and those values stay valid as long as something in the program can still reach them. You can think of the heap as storage for “<em>might live longer than this call</em>”.</p>
<p>The garbage collector is what keeps this safe. Periodically, the runtime starts from a set of roots (global variables, active stack frames, some internal state) and follows all the pointers it can see. Any heap value that is still reachable is kept. Any heap value that is no longer reachable is treated as garbage and its memory is reclaimed.</p>
<p>This means a pointer in <code>main</code> will never legally point into dead stack memory. Either the value stayed in an active frame, or it was placed on the heap where the GC can track its lifetime. The tradeoff is that more heap allocations and longer-lived objects require the GC to do more work.</p>
<p>Here’s an example:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> <span class="hljs-string">"fmt"</span>

<span class="hljs-keyword">type</span> Car <span class="hljs-keyword">struct</span> {
    Brand <span class="hljs-keyword">string</span>
    Model <span class="hljs-keyword">string</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    <span class="hljs-comment">// main receives a pointer from a function it called and this is sharing up</span>
    carPtr := makeCar(<span class="hljs-string">"Volkswagen"</span>, <span class="hljs-string">"Golf"</span>) 

    fmt.Printf(<span class="hljs-string">"I received a car: %s %s\n"</span>, carPtr.Brand, carPtr.Model)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">makeCar</span><span class="hljs-params">(b, m <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">Car</span></span> {
    myCar := Car{
        Brand: b,
        Model: m,
    }
    <span class="hljs-keyword">return</span> &amp;myCar
}
</code></pre>
<p>In the above code:</p>
<ol>
<li><p>In <code>makeCar</code> (the callee frame), Go creates a local variable <code>myCar</code>. Because you return <code>&amp;myCar</code>, the compiler allocates the <code>Car</code> value on the heap, and let’s <code>myCar</code> hold the heap address <code>0xc00029fa0</code>.</p>
</li>
<li><p>When <code>makeCar</code> returns, that address is copied into <code>carPtr</code> in <code>main</code> (the top frame). <code>carPtr</code> is just another stack variable, but its value is still <code>0xc00029fa0</code>, so now <code>main</code> also points to the same heap <code>Car</code>.</p>
</li>
<li><p>On the right, the heap bubble shows the actual <code>Car</code> value at <code>0xc00029fa0</code>. Both <code>car</code> (while <code>makeCar</code> is running) and <code>carPtr</code> (after it returns) reach that same value through their pointers.</p>
</li>
<li><p>Once <code>makeCar</code> is done, its frame drops into the “invalid memory” region, but the <code>Car</code> stays alive on the heap because <code>main</code> still holds <code>carPtr</code>. That’s the escape: the value stops being tied to the callee frame and gets heap lifetime instead.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770255008545/5ef80c9b-3203-4ca3-a26e-f1e2462912cf.png" alt="Diagram showing a caller and callee stack frame both holding a pointer to the same value in heap memory, illustrating a value being shared up and escaping the stack" class="image--center mx-auto" width="1850" height="1190" loading="lazy"></p>
<h2 id="heading-escape-analysis-in-practice">Escape Analysis in Practice</h2>
<p>Escape analysis is how the Go compiler decides whether a value lives on the stack or on the heap. It’s not only about returning pointers – it follows how addresses move through your code. If a value might outlive the current function, the compiler can’t keep it in that stack frame and moves it to the heap. Since only the compiler sees the full picture, the useful thing is to ask it to show these decisions and then link them back to your code.</p>
<p>To do that, we can pass compiler flags using <code>-gcflags</code> when running <code>go build</code> or <code>go run</code>. If you want to see the available options, you can check <code>go tool compile -h</code>. In that list, <code>-m</code> prints the compiler’s optimisation decisions, including escape analysis output. If you want more details, you can use <code>-m=2</code> or <code>-m=3</code> for a more verbose output. The <code>-l</code> flag disables inlining, so the report is easier to read because the compiler is not merging small functions into their callers.</p>
<p>So, the command will look like this:</p>
<pre><code class="lang-bash">go run -gcflags=<span class="hljs-string">'all=-m -l'</span> .
</code></pre>
<p>Or for a build:</p>
<pre><code class="lang-bash">go build -gcflags=<span class="hljs-string">'all=-m -l'</span> .
</code></pre>
<h2 id="heading-how-to-use-escape-analysis-to-guide-performance">How to Use Escape Analysis to Guide Performance</h2>
<p>You can think of escape analysis as the thing that turns your code choices into GC work. When a value escapes, it gets heap lifetime, and the garbage collector has to visit it. In hot paths, lots of small escaping values show up as extra GC time and jitter in latency. When a value stays in a stack frame, it becomes invalid and dies with the frame and the GC does not care about it.</p>
<p>Here are five simple practices that help performance without making</p>
<ol>
<li><p><strong>Prefer values for small data:</strong> If the function doesn’t need to mutate the caller’s data, use value types for small structs and basic types when passing arguments and returning results. It’s cheap to copy an <code>int</code> or a small struct, and it often keeps lifetimes local to a single call.</p>
</li>
<li><p><strong>Use pointers when sharing or mutation is part of the design:</strong> opt for pointers when you genuinely need shared mutable state or want to avoid copying large structs.</p>
</li>
<li><p><strong>Avoid creating long-lived references by accident</strong>: Be careful when returning pointers to locals, capturing variables in closures, or storing addresses in long-lived structs, maps, or interfaces. These patterns are the ones most likely to push values out of a stack frame.</p>
</li>
<li><p><strong>Pass in reusable buffers on hot paths</strong>: On code paths that run very often, the problem is usually not one big allocation, but many small ones happening in a loop. A common cause is functions that always create a new buffer inside, even when the caller could have passed one in.</p>
<p> A simple way to cut those extra allocations is to let the caller own the buffer. The caller allocates a <code>[]byte</code> once, then passes it into the function each time. The function only fills the buffer instead of creating a new one.</p>
<p> Here’s an example of how a bad function allocates a new buffer every call:</p>
<pre><code class="lang-go"> <span class="hljs-keyword">package</span> main

 <span class="hljs-comment">// Bad: helper allocates every call.</span>
 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">fillBad</span><span class="hljs-params">()</span> []<span class="hljs-title">byte</span></span> {
     buf := <span class="hljs-built_in">make</span>([]<span class="hljs-keyword">byte</span>, <span class="hljs-number">4096</span>)
     <span class="hljs-comment">// pretend we read into it</span>
     buf[<span class="hljs-number">0</span>] = <span class="hljs-number">1</span>
     <span class="hljs-keyword">return</span> buf
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">hotPathBad</span><span class="hljs-params">()</span></span> {
     <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">1</span>_000_000; i++ {
         b := fillBad() <span class="hljs-comment">// allocates 1,000,000 times</span>
         _ = b
     }
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     hotPathBad()
 }
</code></pre>
<p> When we run escape analysis with this:</p>
<pre><code class="lang-bash"> go run -gcflags=<span class="hljs-string">'-m -l'</span> .
</code></pre>
<p> We see the following:</p>
<pre><code class="lang-plaintext"> ./main.go:5:13: make([]byte, 4096) escapes to heap
</code></pre>
<p> If we were only allocating a few times, we could choose not to worry – but the real problem is how this looks inside the loop. <code>hotPathBad</code> calls <code>fillBad</code> on every iteration, so each call allocates a new 4 KB slice on the heap. If this loop runs many times, you end up creating a lot of short-lived heap objects. The garbage collector then has to find and clean up all those buffers, which adds extra work that you could have avoided by reusing a single buffer.  </p>
<p> Here’s an example of a better version where the caller allocates once and reuses:</p>
<pre><code class="lang-go"> <span class="hljs-keyword">package</span> main

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">fill</span><span class="hljs-params">(buf []<span class="hljs-keyword">byte</span>)</span> <span class="hljs-title">int</span></span> {
     <span class="hljs-comment">// pretend we read into it</span>
     buf[<span class="hljs-number">0</span>] = <span class="hljs-number">1</span>
     <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">hotPath</span><span class="hljs-params">()</span></span> {
     buf := <span class="hljs-built_in">make</span>([]<span class="hljs-keyword">byte</span>, <span class="hljs-number">4096</span>) 

     <span class="hljs-keyword">for</span> i := <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">1</span>_000_000; i++ {
         n := fill(buf) 
         _ = buf[:n]
     }
 }

 <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
     hotPath()
 }
</code></pre>
<p> In this version, <code>hotPath</code> controls the buffer. It allocates <code>buf</code> once, then passes it into <code>fill</code> on every loop. You still read the same data, but you avoid creating a new slice on each call. That reduces avoidable allocations in the hot path.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In Go, where a value ends up is not decided by how you create it. It’s decided by how long that value must remain valid and how it is referenced as your code runs.</p>
<p>The practical takeaway is not to avoid pointers. It’s to be deliberate about lifetime. Value semantics can keep lifetimes tight and reduce GC work, while pointers can be the right choice when you need shared state or in-place updates. The balance is to write the clear version first, then look at your benchmarks and profiles to see if anything actually really needs to change.</p>
<h2 id="heading-further-reading">Further Reading</h2>
<p>Language Mechanics On Stacks And Pointers - William Kennedy</p>
<p><a target="_blank" href="https://go.dev/doc/gc-guide">Go Compiler: Escape Analysis Flaws</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Embedded Swift: A Modern Approach to Low-Level Programming ]]>
                </title>
                <description>
                    <![CDATA[ Embedded programming has long been dominated by C and C++, powering everything from microcontrollers to real-time systems. While these languages offer unmatched low-level control, they also introduce persistent challenges, manual memory management, u... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/embedded-swift-a-modern-approach-to-low-level-programming/</link>
                <guid isPermaLink="false">688d5fc7d30be1cecdacf767</guid>
                
                    <category>
                        <![CDATA[ embedded systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Firmware Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Swift ]]>
                    </category>
                
                    <category>
                        <![CDATA[ C ]]>
                    </category>
                
                    <category>
                        <![CDATA[ C++ ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory-management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ programming languages ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Soham Banerjee ]]>
                </dc:creator>
                <pubDate>Sat, 02 Aug 2025 00:45:59 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754090186842/80a42dca-f2c4-49de-b704-2e90134c6397.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Embedded programming has long been dominated by C and C++, powering everything from microcontrollers to real-time systems. While these languages offer unmatched low-level control, they also introduce persistent challenges, manual memory management, unsafe pointer operations, and subtle logic bugs stemming from weak type systems and undefined behavior.</p>
<p>With the release of Swift 6 and its new Embedded Swift compilation mode, developers now have access to a modern, memory-safe, and performant alternative that’s tailored specifically for resource-constrained systems.</p>
<p>While languages like Rust have also emerged to address these issues, Embedded Swift brings the clarity and safety of Swift to microcontroller environments, without giving up on determinism, binary size, or hardware access.</p>
<p>This article introduces Embedded Swift and explores how it compares to traditional C/C++ development. We’ll cover its key features, programming and memory models, how to set up the toolchain for STM32 microcontrollers, and how to link Swift with existing C drivers.</p>
<p>Along the way, we’ll examine performance trade-offs, growing ecosystem support, and the broader industry movement toward memory-safe languages. As I hope you’ll see, Swift is a serious contender in the future of embedded development.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most out of this article, you should have a basic understanding of programming in Swift and C. Familiarity with embedded hardware platforms and firmware development concepts will also be helpful.</p>
<p>If you're new to embedded systems, consider reviewing this <a target="_blank" href="https://www.freecodecamp.org/news/learn-embedded-systems-firmware-basics-handbook-for-devs/">introductory guide to embedded firmware</a> to build foundational knowledge before diving into Embedded Swift.</p>
<h2 id="heading-scope">Scope</h2>
<p>This article is intended as a practical introduction to Embedded Swift. It covers:</p>
<ul>
<li><p>An overview of Embedded Swift and its key language features</p>
</li>
<li><p>Swift’s programming and memory model in an embedded context</p>
</li>
<li><p>Setting up the Embedded Swift toolchain on macOS for STM32 microcontrollers</p>
</li>
<li><p>Interoperability with C code and linking to existing low-level drivers</p>
</li>
<li><p>A look at memory and instruction-level performance</p>
</li>
<li><p>Future directions and use cases for Embedded Swift</p>
</li>
</ul>
<p>Note that this article does not provide a full tutorial on the Swift language itself. While the primary focus is on STM32, similar principles apply to other supported platforms such as ESP32, Raspberry Pi Pico, and nRF52.</p>
<h2 id="heading-table-of-contents">Table of Contents:</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-swift-what-is-embedded-swift">What is Swift? What is Embedded Swift?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-swift-programming-model">Swift Programming Model</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-swift-memory-management">Swift Memory Management</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-memory-and-instruction-cycle-comparison">Memory and Instruction Cycle Comparison</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-setup-embedded-swift">How to Setup Embedded Swift</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-c-swift-linkages">C-Swift Linkages:</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-future-work">Future Work</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-swift-what-is-embedded-swift">What is Swift? What is Embedded Swift?</h2>
<p>Swift is a modern programming language developed by Apple that combines the performance of compiled languages with the expressiveness and safety of modern language design. While Swift was originally created for iOS and macOS development, it has evolved into a powerful general-purpose language used in server-side development, systems programming, and increasingly, embedded systems.</p>
<p>Embedded Swift is a special compilation mode introduced in Swift 6 that brings the benefits of Swift to resource-constrained platforms like microcontrollers. It lets developers use a safe, high-level language while still producing compact, deterministic, and performant binaries suitable for embedded applications.</p>
<h3 id="heading-key-features-of-swift">Key Features of Swift</h3>
<p>Embedded Swift retains many of the powerful language features that make Swift an attractive alternative to C/C++ in embedded development:</p>
<p><strong>Type Safety</strong>: Swift uses a strong static type system, which prevents many programming errors at compile time. Unlike C, where type mismatches can result in undefined behavior, Swift ensures all types are used correctly before code even runs.</p>
<p><strong>Strict Type Checking</strong>: Swift doesn't allow implicit type conversions that could lose data or cause unexpected behavior. For example:</p>
<pre><code class="lang-swift"><span class="hljs-comment">// This won't compile in Swift</span>
<span class="hljs-keyword">let</span> integer: <span class="hljs-type">Int</span> = <span class="hljs-number">42</span>
<span class="hljs-keyword">let</span> decimal: <span class="hljs-type">Double</span> = <span class="hljs-number">3.14</span>
<span class="hljs-keyword">let</span> result = integer + decimal  <span class="hljs-comment">// Error: Cannot convert value of type 'Int' to expected argument type 'Double'</span>

<span class="hljs-comment">// You must be explicit about conversions</span>
<span class="hljs-keyword">let</span> result = <span class="hljs-type">Double</span>(integer) + decimal  <span class="hljs-comment">// Correct</span>
</code></pre>
<p><strong>Non-nullable Types by Default</strong>: In C, pointers can be null by default, which introduces risk. In Swift, variables cannot be nil unless explicitly marked as optionals:</p>
<pre><code class="lang-swift"><span class="hljs-keyword">var</span> name: <span class="hljs-type">String</span> = <span class="hljs-string">"John"</span>
name = <span class="hljs-literal">nil</span>  <span class="hljs-comment">// Compile error - String cannot be nil</span>

<span class="hljs-keyword">var</span> optionalName: <span class="hljs-type">String?</span> = <span class="hljs-string">"John"</span>
optionalName = <span class="hljs-literal">nil</span>  <span class="hljs-comment">// This is allowed</span>
</code></pre>
<h4 id="heading-memory-safety-via-arc-covered-in-detail-later">Memory Safety via ARC (Covered in detail later):</h4>
<p>Swift manages memory automatically using Automatic Reference Counting (ARC). Unlike manual memory management in C/C++, ARC handles object lifecycles efficiently without unpredictable garbage collection pauses. We'll cover ARC and its impact in embedded contexts in a dedicated section later.</p>
<p><strong>Modern Syntax</strong>:<br>Swift's syntax is clean, consistent, and designed for readability. It supports modern paradigms including:</p>
<ul>
<li><p>Functional programming (map, filter, reduce)</p>
</li>
<li><p>Generics (type-safe abstractions)</p>
</li>
<li><p>Protocol-Oriented Programming (discussed in the next section)</p>
</li>
</ul>
<p>These features allow you to write more expressive and maintainable code compared to procedural C or inheritance-heavy C++.</p>
<p><strong>Performance</strong>:<br>Swift is designed to perform on par with C++ in many scenarios. Optimizations such as inlining, dead code elimination, and static dispatch help ensure that high-level abstractions don’t compromise performance. In embedded mode, Swift disables features like runtime reflection and dynamic dispatch to further reduce overhead.</p>
<p>To fully leverage Swift for embedded development, it's important to understand its programming model. Unlike C’s procedural approach or C++’s class-heavy design, Swift promotes protocol-oriented programming and composition, which offers both flexibility and safety in embedded system design.</p>
<h2 id="heading-swift-programming-model">Swift Programming Model</h2>
<p>Swift embraces a multi-paradigm programming model that blends object-oriented, functional, and protocol-oriented programming, all underpinned by strong type safety and memory safety.</p>
<p>For embedded developers coming from C or C++, this model may feel different at first. But it provides a more modular and testable way to build complex systems, something especially valuable in embedded applications where hardware abstraction and strict reliability are critical.</p>
<h3 id="heading-protocol-oriented-programming-pop">Protocol-Oriented Programming (POP)</h3>
<p>Swift emphasizes protocols over inheritance, encouraging developers to define behaviors through protocols and implement them using value types like <code>struct</code> and <code>enum</code>, rather than relying heavily on classes.</p>
<p>This philosophy favors composition over inheritance, allowing you to build complex functionality by combining smaller, well-defined components.</p>
<p>Key Concepts<strong>:</strong></p>
<ul>
<li><p><code>protocol</code> defines required behavior.</p>
</li>
<li><p>Protocol extensions provide default behavior.</p>
</li>
<li><p>Prefer value semantics using <code>struct</code>.</p>
</li>
</ul>
<p>Example<strong>:</strong></p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">protocol</span> <span class="hljs-title">Speakable</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">speak</span><span class="hljs-params">()</span></span>
}

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">Speakable</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">speak</span><span class="hljs-params">()</span></span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Default sound"</span>)
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Dog</span>: <span class="hljs-title">Speakable</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">speak</span><span class="hljs-params">()</span></span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Woof!"</span>)
    }
}
</code></pre>
<p>Embedded Swift uses protocols with static dispatch. With static dispatch, the compiler knows the exact memory address of the function to call and can generate a direct jump instruction. There's no runtime lookup, no indirection, and no uncertainty.</p>
<h4 id="heading-why-pop-matters-for-embedded-systems">Why POP Matters for Embedded Systems</h4>
<p>First, you get flexible hardware extraction. Protocols make it easy to define interfaces for hardware components, allowing for mock implementations during testing or platform-specific variations.</p>
<p>Second, you have nice low overhead. Embedded Swift uses static dispatch for protocols, meaning there’s no runtime lookup, and calls are resolved at compile time for maximum performance.</p>
<p>Also, <code>struct</code> and <code>enum</code> types avoid heap allocations, making code more efficient and predictable in low-memory environments.</p>
<p>Now that we’ve explored how Swift’s programming model enables safer and more modular embedded code, let’s turn to another critical piece of the puzzle: memory management. Swift’s use of Automatic Reference Counting (ARC) replaces manual memory handling and offers important benefits, and tradeoffs, for embedded systems.</p>
<h2 id="heading-swift-memory-management">Swift Memory Management</h2>
<p>One of Swift’s most impactful features, especially in the context of embedded systems, is its use of Automatic Reference Counting (ARC) for memory management. Unlike C/C++, where memory must be manually allocated and freed using <code>malloc</code> and <code>free</code>, Swift automates this process while maintaining deterministic performance.</p>
<p>This automation significantly reduces the risk of common memory-related bugs like leaks, dangling pointers, or use-after-free errors, all of which are notorious in low-level C code.</p>
<h3 id="heading-how-arc-works">How ARC works</h3>
<p>Swift supports ARC not only for the Cocoa Touch API's but for all APIs, providing a streamlined approach to memory management. Unlike garbage collection systems that can cause unpredictable pauses, ARC works deterministically at compile time and runtime to manage memory.</p>
<p>ARC automatically tracks and manages the lifetime of objects in memory based on how many references point to them.</p>
<ul>
<li><p>Reference Counting: Every object has a counter that tracks how many strong references point to it.</p>
</li>
<li><p>Retain / Release: The compiler inserts <code>retain</code> and <code>release</code> calls automatically during assignment and deinitialization.</p>
</li>
<li><p>Immediate Deallocation: When the reference count reaches zero, the object is deallocated immediately.</p>
</li>
<li><p>Deterministic: Unlike garbage collectors, ARC doesn’t introduce unpredictable pauses or runtime scanning.</p>
</li>
</ul>
<p>Swift offers multiple reference types to give you precise control over memory behavior and prevent cycles:</p>
<p><strong>Strong References</strong> (default)</p>
<ul>
<li><p>Keeps the referenced object alive.</p>
</li>
<li><p>Used in most cases.</p>
</li>
</ul>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MotorController</span> </span>{
    <span class="hljs-keyword">var</span> sensor: <span class="hljs-type">SensorData?</span>  <span class="hljs-comment">// Strong reference</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">updateReading</span><span class="hljs-params">(newData: SensorData)</span></span> {
        <span class="hljs-keyword">self</span>.sensor = newData  <span class="hljs-comment">// Previous sensor data automatically deallocated</span>
    }
}
</code></pre>
<p><strong>Weak References</strong></p>
<ul>
<li><p>Used to break reference cycles (especially in two-way object relationships).</p>
</li>
<li><p>Automatically becomes <code>nil</code> when the referenced object is deallocated.</p>
</li>
</ul>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Device</span> </span>{
    <span class="hljs-keyword">var</span> controller: <span class="hljs-type">MotorController?</span>

    <span class="hljs-keyword">deinit</span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Device deallocated"</span>)
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MotorController</span> </span>{
    <span class="hljs-keyword">weak</span> <span class="hljs-keyword">var</span> device: <span class="hljs-type">Device?</span>  <span class="hljs-comment">// ← Weak reference breaks the cycle</span>

    <span class="hljs-keyword">deinit</span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"MotorController deallocated"</span>)
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">breakCycle</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">let</span> device = <span class="hljs-type">Device</span>()
    <span class="hljs-keyword">let</span> controller = <span class="hljs-type">MotorController</span>()

    device.controller = controller
    controller.device = device  <span class="hljs-comment">// ← This is now a weak reference</span>

    <span class="hljs-comment">// When this function ends, both objects are properly deallocated</span>
}

breakCycle()
<span class="hljs-comment">// Output:</span>
<span class="hljs-comment">// Device deallocated</span>
<span class="hljs-comment">// MotorController deallocated</span>
</code></pre>
<p><strong>Unowned References</strong></p>
<ul>
<li><p>Non-optional version of <code>weak</code>.</p>
</li>
<li><p>Assumes the object will never be deallocated while still in use.</p>
</li>
<li><p>More lightweight than <code>weak</code>, but unsafe if misused.</p>
</li>
</ul>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SensorSystem</span> </span>{
    <span class="hljs-keyword">unowned</span> <span class="hljs-keyword">let</span> controller: <span class="hljs-type">MotorController</span>  <span class="hljs-comment">// unowned reference</span>

    <span class="hljs-keyword">init</span>(controller: <span class="hljs-type">MotorController</span>) {
        <span class="hljs-keyword">self</span>.controller = controller
    }
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MotorController</span> </span>{
    <span class="hljs-keyword">var</span> sensorSystem: <span class="hljs-type">SensorSystem?</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">setupSensors</span><span class="hljs-params">()</span></span> {
        sensorSystem = <span class="hljs-type">SensorSystem</span>(controller: <span class="hljs-keyword">self</span>)
    }

    <span class="hljs-keyword">deinit</span> {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"MotorController deallocated"</span>)
    }
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">testUnowned</span><span class="hljs-params">()</span></span> {
    <span class="hljs-keyword">let</span> controller = <span class="hljs-type">MotorController</span>()
    controller.setupSensors()
    <span class="hljs-comment">// sensorSystem deallocates before controller ends</span>
}

testUnowned()
<span class="hljs-comment">// Output: MotorController deallocated</span>
</code></pre>
<h3 id="heading-arc-overhead-in-embedded-systems">ARC Overhead in Embedded Systems</h3>
<p>While ARC provides safety benefits, it does introduce some overhead compared to manual memory management:</p>
<h4 id="heading-memory-overhead">Memory Overhead:</h4>
<p>ARC-managed class instances in Swift typically include an additional 4 or 8 bytes to store reference count metadata, depending on the system architecture, 4 bytes on 32-bit systems and 8 bytes on 64-bit systems. This metadata allows the runtime to track how many active references exist to a given object and deallocate it when no references remain. When developers use weak or unowned references, the memory footprint increases further. These references require additional data structures, such as side tables or tracking mechanisms, to manage object liveness and cleanup. In the case of weak references specifically, Swift maintains zeroing weak reference tables that automatically null out pointers once the referenced object is deallocated, ensuring memory safety.</p>
<h4 id="heading-cpu-overhead">CPU Overhead:</h4>
<p>ARC introduces some runtime overhead due to retain and release operations, which are inserted automatically during reference assignments. These operations involve incrementing or decrementing the reference count and are especially common in code that passes objects between functions or stores them in collections. To ensure thread safety, these updates are typically implemented using atomic operations, which add further instruction cycles. In complex object graphs, ARC may also engage in cycle detection and cleanup through the use of weak references to prevent memory leaks caused by strong reference cycles. While Swift's ARC provides deterministic and efficient memory management, it does so with both memory and CPU costs that developers should consider carefully, especially in performance-critical embedded systems.</p>
<h3 id="heading-type-safety-and-error-prevention">Type Safety and Error Prevention</h3>
<p>Swift's type system prevents many common errors that plague C/C++ programs:</p>
<ul>
<li><p><strong>Buffer Overflows</strong>: Swift arrays are bounds-checked, preventing buffer overflow vulnerabilities that are common in C.</p>
</li>
<li><p><strong>Null Pointer Dereferences</strong>: Swift's optional types make null pointer dereferences impossible at compile time.</p>
</li>
<li><p><strong>Use After Free</strong>: Swift's ownership model prevents use-after-free errors that can cause crashes or security vulnerabilities.</p>
</li>
</ul>
<p>Now that we’ve covered Swift's memory model and ARC behavior, let’s explore how it compares to C in terms of memory usage and instruction cycles, a crucial aspect when evaluating Embedded Swift for real-world deployment.</p>
<h2 id="heading-memory-and-instruction-cycle-comparison">Memory and Instruction Cycle Comparison</h2>
<p>Understanding the performance characteristics of Swift versus C is essential for embedded systems, where every instruction cycle and byte of memory matters. While Swift brings advantages like safety and expressiveness, these benefits come with certain trade-offs in terms of memory usage and runtime behavior that embedded developers must evaluate carefully.</p>
<h3 id="heading-memory-management">Memory Management:</h3>
<p>Swift uses Automatic Reference Counting (ARC) to manage memory. ARC tracks the number of references to each object and deallocates it when no references remain. This eliminates the need for explicit <code>free()</code> calls but introduces overhead.</p>
<p>C, in contrast, uses manual memory management. Developers allocate memory using <code>malloc</code> and release it using <code>free</code>, or rely on the stack for most short-lived data.</p>
<p>The table below provides the memory management comparison between Swift and C:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Feature</strong></td><td><strong>Swift (ARC)</strong></td><td><strong>C (Manual)</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Memory strategy</td><td>Automatic reference counting</td><td>Manual with <code>malloc</code>/<code>free</code></td></tr>
<tr>
<td>Overhead per object</td><td>4–8 bytes (for ref count)</td><td>None for stack; variable for heap</td></tr>
<tr>
<td>Deallocation</td><td>Deterministic, triggered by ARC</td><td>Developer-controlled</td></tr>
<tr>
<td>Weak reference support</td><td>Requires additional metadata</td><td>Not built-in</td></tr>
<tr>
<td>Thread safety</td><td>Atomic operations in ARC</td><td>Not guaranteed</td></tr>
<tr>
<td>Layout control</td><td>Limited, compiler-managed</td><td>Full control (via structs/pointers)</td></tr>
</tbody>
</table>
</div><p>Swift ensures safety through deterministic cleanup and predictable memory usage. But this comes at the cost of added memory and CPU overhead.</p>
<p>C’s approach offers complete control over memory layout and minimal runtime cost, but increases the risk of memory leaks and fragmentation without disciplined practices.</p>
<h3 id="heading-instruction-cycle-analysis">Instruction Cycle Analysis</h3>
<p>The safety features in Swift, such as bounds checking, optional unwrapping, and ARC updates, translate into additional CPU instructions. While this can impact performance, the Swift compiler is aggressive about optimization in release builds. For example, inlining and ARC elision can remove much of the overhead in performance-critical paths.</p>
<p>C has no built-in safety checks, allowing it to generate highly efficient, predictable code. Developers can even use inline assembly for tight control over performance.</p>
<p>The table below provides the instruction cycle comparison between Swift and C:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Instruction-Level Feature</strong></td><td><strong>Swift</strong></td><td><strong>C</strong></td></tr>
</thead>
<tbody>
<tr>
<td>Reference count updates</td><td>2–4 instructions per assignment</td><td>N/A</td></tr>
<tr>
<td>Bounds checking</td><td>1–3 instructions per array access</td><td>None</td></tr>
<tr>
<td>Optional unwrapping</td><td>1–2 instructions per check</td><td>N/A</td></tr>
<tr>
<td>Method dispatch</td><td>Protocols introduce indirection</td><td>Direct calls or function pointers</td></tr>
<tr>
<td>Optimization potential</td><td>ARC elision, inlining, dead code removal</td><td>Full manual control, inline assembly</td></tr>
<tr>
<td>Predictability</td><td>High in optimized builds, with some abstraction overhead</td><td>Very high, minimal abstraction</td></tr>
</tbody>
</table>
</div><p>Although Swift inserts extra instructions for safety, much of this cost can be mitigated through compiler optimization.</p>
<p>C has no such features by default, making it ideal for applications where performance must be tightly controlled and the developer is willing to take full responsibility for safety.</p>
<h3 id="heading-instruction-count-comparison-swift-vs-c-loop-performance">Instruction Count Comparison: Swift vs C Loop Performance</h3>
<p>When evaluating Swift and C for embedded use, it's helpful to analyze instruction-level performance on basic operations, such as a loop that processes an array of floating-point numbers. This gives us a concrete sense of the computational cost of each language's safety and abstraction features.</p>
<p>Let’s consider a simple example: summing an array of <code>Float</code> values and returning the average. In Swift, the code uses a high-level <code>for-in</code> loop over an array:</p>
<p>Simple loop performance:</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Swift loop with safety checks</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">processData</span><span class="hljs-params">(<span class="hljs-number">_</span> data: [Float])</span></span> -&gt; <span class="hljs-type">Float</span> {
    <span class="hljs-keyword">var</span> sum: <span class="hljs-type">Float</span> = <span class="hljs-number">0.0</span>
    <span class="hljs-keyword">for</span> value <span class="hljs-keyword">in</span> data {  <span class="hljs-comment">// Iterator with bounds checking</span>
        sum += value     <span class="hljs-comment">// Safe arithmetic</span>
    }
    <span class="hljs-keyword">return</span> sum / <span class="hljs-type">Float</span>(data.<span class="hljs-built_in">count</span>)  <span class="hljs-comment">// Safe division</span>
}
<span class="hljs-comment">// Estimated: ~8-10 instructions per iteration</span>
</code></pre>
<p>Although elegant and safe, this loop includes several safety mechanisms:</p>
<ol>
<li><p>Bounds checking on every array access</p>
</li>
<li><p>Reference counting if <code>data</code> is passed as a reference type</p>
</li>
<li><p>Overflow protection in debug mode</p>
</li>
<li><p>Optional handling or runtime checks if <code>data</code> might be empty</p>
</li>
</ol>
<p>These checks introduce runtime overhead, resulting in an estimated 8–10 instructions per iteration on most platforms (depending on optimization level and target architecture). In release builds, Swift aggressively inlines and strips redundant checks, but some level of abstraction cost remains, especially compared to raw memory access in C.</p>
<p>Now, compare that to its equivalent in C:</p>
<pre><code class="lang-c"><span class="hljs-comment">// C loop without safety checks</span>
<span class="hljs-function"><span class="hljs-keyword">float</span> <span class="hljs-title">process_data</span><span class="hljs-params">(<span class="hljs-keyword">float</span>* data, <span class="hljs-keyword">int</span> count)</span> </span>{
    <span class="hljs-keyword">float</span> sum = <span class="hljs-number">0.0f</span>;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; count; i++) {  <span class="hljs-comment">// Direct pointer arithmetic</span>
        sum += data[i];                <span class="hljs-comment">// Direct memory access</span>
    }
    <span class="hljs-keyword">return</span> sum / count;  <span class="hljs-comment">// Direct division (no safety check)</span>
}
<span class="hljs-comment">// Estimated: ~4-5 instructions per iteration</span>
</code></pre>
<p>This version performs direct memory access with pointer arithmetic, no bounds checks, and no type safety. The C code is lower-level, with fewer runtime checks, and compiles down to just 4–5 instructions per iteration, depending on the target CPU and compiler flags. It is lean and fast, ideal for cycles-per-instruction-critical scenarios.</p>
<p>The table below shows the comparison of single loop performance between Swift and C:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Aspect</td><td>Swift</td><td>C</td></tr>
</thead>
<tbody>
<tr>
<td>Array access</td><td>Bounds-checked</td><td>Direct pointer access</td></tr>
<tr>
<td>Loop iteration</td><td>High-level iterator abstraction</td><td>Raw loop with pointer increment</td></tr>
<tr>
<td>Instruction count (per loop)</td><td>~8–10 (in debug), ~6–8 (in release)</td><td>~4–5</td></tr>
<tr>
<td>Division</td><td>Safe (avoids divide-by-zero in dev)</td><td>Direct</td></tr>
<tr>
<td>Overflow behavior</td><td>Checked in debug, unchecked in release</td><td>Unchecked</td></tr>
<tr>
<td>Readability and safety</td><td>High</td><td>Low</td></tr>
<tr>
<td>Performance</td><td>Lower (but optimizable)</td><td>Higher (manual)</td></tr>
</tbody>
</table>
</div><p>Now that we’ve compared Swift and C in terms of memory and cycle costs, let’s move into the practical side: how to set up Embedded Swift on an STM32 platform and get started with real-world development.</p>
<h2 id="heading-how-to-setup-embedded-swift">How to Setup Embedded Swift</h2>
<p>In this section, we'll walk through how to configure and use Embedded Swift for development on STM32 microcontrollers. STM32 is a popular family of ARM Cortex-M–based microcontrollers, commonly used in industrial, consumer, and IoT applications.</p>
<h3 id="heading-prerequisites-1">Prerequisites</h3>
<p><strong>Required Software:</strong></p>
<ul>
<li><p>Swift Development Snapshot (includes the Embedded Swift toolchain)</p>
</li>
<li><p>Swiftly - Easiest way to manage and install swift toolchains</p>
</li>
<li><p>Swiftc - Swift Compiler command-line tool</p>
</li>
<li><p>Python3 - Required to run scripts to convert Mach-O to binary files</p>
</li>
<li><p>Git (to clone sample repositories) like <a target="_blank" href="https://github.com/swiftlang/swift-embedded-examples">https://github.com/swiftlang/swift-embedded-examples</a></p>
</li>
<li><p>A Unix-like development environment (macOS is currently best supported)</p>
</li>
</ul>
<p><strong>Target Hardware:</strong> This guide focuses on STM32 microcontrollers, which are widely used in embedded applications and have excellent community support.</p>
<p>This guide walks you through the full setup process, from installing the required Swift toolchain to flashing the final binary onto your board. We’ll begin by installing the Swift Development Snapshot using Swiftly, a simple command-line utility for managing Swift toolchains. From there, we’ll configure the build system, set up the correct board variant, customize the build script, and compile the Swift and C source code into a binary. Finally, we’ll flash the firmware onto the STM32 using standard tools</p>
<h3 id="heading-install-swift-development-snapshot">Install Swift Development Snapshot</h3>
<p>The easiest way to install and manage Embedded Swift toolchains is by using the swiftly tool, which simplifies downloading and using Swift snapshots.</p>
<h4 id="heading-macos-installation">macOS Installation:</h4>
<p>The below steps will help install the Swift embedded toolchain:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Using Swiftly (Recommended)</span>
curl -O https://download.swift.org/swiftly/darwin/swiftly.pkg
installer -pkg swiftly.pkg -target CurrentUserHomeDirectory
~/.swiftly/bin/swiftly init --quiet-shell-followup
<span class="hljs-built_in">source</span> <span class="hljs-string">"<span class="hljs-variable">${SWIFTLY_HOME_DIR:-<span class="hljs-variable">$HOME</span>/.swiftly}</span>/env.sh"</span>

<span class="hljs-comment"># Install and use development snapshot</span>
swiftly install main-snapshot
swiftly use main-snapshot

<span class="hljs-comment"># Verify installation</span>
swift --version
</code></pre>
<p>You can clone this Github example repository:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/swiftlang/swift-embedded-examples.git 
<span class="hljs-built_in">cd</span> swift-embedded-examples/projects/stm32-blink
</code></pre>
<p>The stm32-blink contains:</p>
<ul>
<li><p>Swift code that toggles GPIOs</p>
</li>
<li><p>A C startup file with vector table</p>
</li>
<li><p>A build.sh script that uses swiftc, clang, and a custom linker setup</p>
</li>
</ul>
<h3 id="heading-setup-the-stm32-board">Setup the STM32 Board</h3>
<p>Tell the build script which STM32 board is being used:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> STM_BOARD=STM32F746G_DISCOVERY
</code></pre>
<p>You can add your own board variant by defining the appropriate memory map and compiler flags in the script.</p>
<h3 id="heading-modify-buildsh-optional">Modify build.sh (Optional)</h3>
<p>Ensure the script correctly locates the following:</p>
<ul>
<li><p>swiftc: should point to the toolchain you installed with Swiftly</p>
</li>
<li><p>clang: can be macOS’s default Clang</p>
</li>
<li><p>libBuiltin.a, crt0.s, and macho2bin.py: used to provide minimal runtime support and convert output to flashable binaries</p>
</li>
</ul>
<p>If needed, update these paths:</p>
<pre><code class="lang-bash">SWIFT_EXEC=<span class="hljs-variable">${SWIFT_EXEC:-$(swiftly which swiftc)}</span>
CLANG_EXEC=<span class="hljs-variable">${CLANG_EXEC:-$(xcrun -f clang)}</span>
PYTHON_EXEC=<span class="hljs-variable">${PYTHON_EXEC:-$(which python3)}</span>
</code></pre>
<p>Ensure the linker flags match your target’s flash and RAM sizes.</p>
<h3 id="heading-build-and-flash-the-project">Build and Flash the Project:</h3>
<p>Run:</p>
<pre><code class="lang-bash">./build.sh
</code></pre>
<p>This compiles Swift and C code, links them, and produces a blink.bin file.</p>
<p>If successful, you’ll see:</p>
<pre><code class="lang-bash">.build/blink.bin  <span class="hljs-comment"># ready to flash Step 6: Flash the Firmware to STM32</span>
</code></pre>
<p>Use ST-Link tools or openocd to flash your board. Example using st-flash:</p>
<pre><code class="lang-bash">brew install stlink
st-flash write .build/blink.bin 0x8000000
</code></pre>
<p>You should now see an LED blinking.</p>
<p><a target="_blank" href="https://docs.swift.org/embedded/documentation/embedded/stm32baremetalguide">Here’s</a> a more detailed step by step approach to writing a bare metal code on STM32. For comprehensive installation guides covering other platforms (Raspberry Pi Pico, ESP32, nRF52), detailed IDE configuration, troubleshooting, and advanced examples, you can check out the official documentation:</p>
<ul>
<li><p>Complete Setup Guide: <a target="_blank" href="https://docs.swift.org/embedded/documentation/embedded/installembeddedswift/">Install Embedded Swift</a></p>
</li>
<li><p>Platform Examples: <a target="_blank" href="https://github.com/apple/swift-embedded-examples">Swift Embedded Examples Repository</a></p>
</li>
<li><p>Getting Started Tutorial: <a target="_blank" href="https://docs.swift.org/embedded/documentation/embedded">Embedded Swift on Microcontrollers</a></p>
</li>
</ul>
<p>Now that we’ve set up Embedded Swift and explored how to build and run an example project, let’s look at a critical real-world scenario: interfacing Swift with low-level C drivers.</p>
<h2 id="heading-c-swift-linkages">C-Swift Linkages</h2>
<p>In many embedded projects, low-level hardware drivers are written in C because of its close-to-metal control and widespread ecosystem support. Embedded Swift supports seamless interoperability with C, which lets you reuse existing C libraries and drivers, write hardware control logic in C, and implement higher-level application logic in Swift.</p>
<p>This hybrid model lets you combine Swift’s safety and productivity with C’s hardware-level control, with no runtime overhead or object translation.</p>
<p>Let’s walk through an example where a low-level sensor driver is implemented in C and the application logic is written in Swift.</p>
<h3 id="heading-c-header-file-sensordriverh">C Header File (sensor_driver.h):</h3>
<p>This C header file defines the public interface for a low-level sensor driver. It includes standard fixed-width integer types and declares four functions:</p>
<ul>
<li><p>sensor_init(): Initializes the hardware sensor</p>
</li>
<li><p>sensor_read_temperature() and sensor_read_humidity(): Read raw sensor values</p>
</li>
<li><p>sensor_delay_ms(): Delays execution for a given number of milliseconds</p>
</li>
</ul>
<p>This interface acts as a bridge between Swift and C. Swift will link to these functions by name, no wrappers or bindings required.</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">ifndef</span> SENSOR_DRIVER_H</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SENSOR_DRIVER_H</span>

<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>

<span class="hljs-comment">// Low-level sensor driver functions</span>
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_init</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">uint32_t</span> <span class="hljs-title">sensor_read_temperature</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">uint32_t</span> <span class="hljs-title">sensor_read_humidity</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span></span>;
<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_delay_ms</span><span class="hljs-params">(<span class="hljs-keyword">uint32_t</span> milliseconds)</span></span>;

<span class="hljs-meta">#<span class="hljs-meta-keyword">endif</span></span>
</code></pre>
<h3 id="heading-c-implementation-sensordriverc">C Implementation (sensor_driver.c):</h3>
<p>This implementation assumes the sensor is memory-mapped at a fixed address (<code>0x40001000</code>). Each register, temperature, humidity, and control, is accessed by offset from that base address.</p>
<p>The <code>sensor_init</code>() function writes <code>0x01</code> to the control register, presumably enabling or starting the sensor hardware.</p>
<p>The <code>sensor_read_temperature()</code> method and <code>sensor_read_humidity()</code> method reads from memory-mapped registers and return the raw ADC values from the sensor.</p>
<p>The <code>sensor_delay_ms()</code> method performs a simple busy-wait loop using nop (no-operation) instructions to approximate a delay. This is suitable for short, coarse-grained delays in bare-metal contexts.</p>
<pre><code class="lang-c"><span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">"sensor_driver.h"</span></span>

<span class="hljs-comment">// Hardware register addresses</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> SENSOR_BASE_ADDR    0x40001000</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> TEMP_REG_OFFSET     0x00</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> HUMIDITY_REG_OFFSET 0x04</span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> CONTROL_REG_OFFSET  0x08</span>

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_init</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
    <span class="hljs-comment">// Initialize sensor hardware</span>
    <span class="hljs-keyword">volatile</span> <span class="hljs-keyword">uint32_t</span>* control_reg = (<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">uint32_t</span>*)(SENSOR_BASE_ADDR + CONTROL_REG_OFFSET);
    *control_reg = <span class="hljs-number">0x01</span>; <span class="hljs-comment">// Enable sensor</span>
}

<span class="hljs-function"><span class="hljs-keyword">uint32_t</span> <span class="hljs-title">sensor_read_temperature</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
    <span class="hljs-keyword">volatile</span> <span class="hljs-keyword">uint32_t</span>* temp_reg = (<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">uint32_t</span>*)(SENSOR_BASE_ADDR + TEMP_REG_OFFSET);
    <span class="hljs-keyword">return</span> *temp_reg;
}

<span class="hljs-function"><span class="hljs-keyword">uint32_t</span> <span class="hljs-title">sensor_read_humidity</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span> </span>{
    <span class="hljs-keyword">volatile</span> <span class="hljs-keyword">uint32_t</span>* humidity_reg = (<span class="hljs-keyword">volatile</span> <span class="hljs-keyword">uint32_t</span>*)(SENSOR_BASE_ADDR + HUMIDITY_REG_OFFSET);
    <span class="hljs-keyword">return</span> *humidity_reg;
}

<span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">sensor_delay_ms</span><span class="hljs-params">(<span class="hljs-keyword">uint32_t</span> milliseconds)</span> </span>{
    <span class="hljs-comment">// Simple delay implementation</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint32_t</span> i = <span class="hljs-number">0</span>; i &lt; milliseconds * <span class="hljs-number">1000</span>; i++) {
        __asm__(<span class="hljs-string">"nop"</span>);
    }
}
</code></pre>
<h3 id="heading-swift-code-using-c-driver">Swift Code Using C Driver:</h3>
<p>To use these C functions from Swift, you declare them using <code>@_silgen_name</code>, which tells the Swift compiler to link directly to these symbol names at runtime.</p>
<p>The <code>SensorController</code> class encapsulates sensor-related logic. In its <code>init()</code> method, it calls the <code>sensor_init()</code> function defined in C to initialize the sensor hardware.</p>
<p>The <code>readSensors()</code> method reads the raw values from the C driver, converts them into human-readable units using helper functions, stores them internally, and returns the processed values.</p>
<p>The <code>convertTemperature()</code> and <code>convertHumidity()</code> conversion methods apply a basic linear formula to turn raw ADC values into temperature in Celsius and humidity in percentage, respectively. These formulas would be based on the specific sensor’s datasheet.</p>
<p>The <code>checkThresholds()</code> method applies simple threshold logic, a good example of where Swift’s readability and type safety shine. You could easily expand this logic to include error bounds, state machines, or alerts.</p>
<pre><code class="lang-swift"><span class="hljs-comment">// Import C driver functions</span>

<span class="hljs-comment">/*
These declarations match the C function signatures exactly. 
They allow Swift to invoke the C functions as if they were native Swift functions 
— with zero overhead.
*/</span>
@_silgen_name(<span class="hljs-string">"sensor_init"</span>)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sensor_init</span><span class="hljs-params">()</span></span>

@_silgen_name(<span class="hljs-string">"sensor_read_temperature"</span>)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sensor_read_temperature</span><span class="hljs-params">()</span></span> -&gt; <span class="hljs-type">UInt32</span>

@_silgen_name(<span class="hljs-string">"sensor_read_humidity"</span>)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sensor_read_humidity</span><span class="hljs-params">()</span></span> -&gt; <span class="hljs-type">UInt32</span>

@_silgen_name(<span class="hljs-string">"sensor_delay_ms"</span>)
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sensor_delay_ms</span><span class="hljs-params">(<span class="hljs-number">_</span> ms: UInt32)</span></span>

<span class="hljs-comment">// Swift sensor controller using C driver</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SensorController</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> lastTemperature: <span class="hljs-type">Float</span> = <span class="hljs-number">0.0</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> lastHumidity: <span class="hljs-type">Float</span> = <span class="hljs-number">0.0</span>

    <span class="hljs-keyword">init</span>() {
        <span class="hljs-comment">// Initialize the C driver</span>
        sensor_init()
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">readSensors</span><span class="hljs-params">()</span></span> -&gt; (temperature: <span class="hljs-type">Float</span>, humidity: <span class="hljs-type">Float</span>) {
        <span class="hljs-comment">// Read raw values from C driver</span>
        <span class="hljs-keyword">let</span> rawTemp = sensor_read_temperature()
        <span class="hljs-keyword">let</span> rawHumidity = sensor_read_humidity()

        <span class="hljs-comment">// Convert raw values to meaningful units in Swift</span>
        <span class="hljs-keyword">let</span> temperature = convertTemperature(rawValue: rawTemp)
        <span class="hljs-keyword">let</span> humidity = convertHumidity(rawValue: rawHumidity)

        <span class="hljs-comment">// Store for comparison</span>
        lastTemperature = temperature
        lastHumidity = humidity

        <span class="hljs-keyword">return</span> (temperature: temperature, humidity: humidity)
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">convertTemperature</span><span class="hljs-params">(rawValue: UInt32)</span></span> -&gt; <span class="hljs-type">Float</span> {
        <span class="hljs-comment">// Convert raw ADC value to Celsius</span>
        <span class="hljs-keyword">return</span> (<span class="hljs-type">Float</span>(rawValue) * <span class="hljs-number">3.3</span> / <span class="hljs-number">4095.0</span> - <span class="hljs-number">0.5</span>) * <span class="hljs-number">100.0</span>
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">convertHumidity</span><span class="hljs-params">(rawValue: UInt32)</span></span> -&gt; <span class="hljs-type">Float</span> {
        <span class="hljs-comment">// Convert raw ADC value to percentage</span>
        <span class="hljs-keyword">return</span> <span class="hljs-type">Float</span>(rawValue) * <span class="hljs-number">100.0</span> / <span class="hljs-number">4095.0</span>
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">checkThresholds</span><span class="hljs-params">()</span></span> -&gt; <span class="hljs-type">Bool</span> {
        <span class="hljs-comment">// Swift logic for threshold checking</span>
        <span class="hljs-keyword">let</span> tempThreshold: <span class="hljs-type">Float</span> = <span class="hljs-number">25.0</span>
        <span class="hljs-keyword">let</span> humidityThreshold: <span class="hljs-type">Float</span> = <span class="hljs-number">60.0</span>

        <span class="hljs-keyword">return</span> lastTemperature &gt; tempThreshold || lastHumidity &gt; humidityThreshold
    }
}

<span class="hljs-comment">// Main application loop</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> -&gt; <span class="hljs-type">Never</span> {
    <span class="hljs-keyword">let</span> sensorController = <span class="hljs-type">SensorController</span>()

    <span class="hljs-keyword">while</span> <span class="hljs-literal">true</span> {
        <span class="hljs-comment">// Read sensors using Swift controller with C driver</span>
        <span class="hljs-keyword">let</span> readings = sensorController.readSensors()

        <span class="hljs-comment">// Process data with Swift's type safety and expressiveness</span>
        <span class="hljs-keyword">if</span> sensorController.checkThresholds() {
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"Warning: Temperature: \(readings.temperature)°C, Humidity: \(readings.humidity)%"</span>)
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-built_in">print</span>(<span class="hljs-string">"Normal: Temperature: \(readings.temperature)°C, Humidity: \(readings.humidity)%"</span>)
        }

        <span class="hljs-comment">// Delay using C driver function</span>
        sensor_delay_ms(<span class="hljs-number">1000</span>) <span class="hljs-comment">// 1 second delay</span>
    }
}
</code></pre>
<p>The <code>func main()</code> is the main event loop standard for embedded systems. It creates the sensor controller, reads sensor data in a loop, checks thresholds, and prints results accordingly. The loop includes a delay (via the C driver) to avoid hammering the sensor continuously.</p>
<p>In an actual embedded context, instead of using <code>print()</code>, you might blink an LED, send UART messages, or log data to memory.</p>
<p>With Embedded Swift and C now working together, let’s explore what lies ahead. The next section outlines ongoing improvements, emerging use cases, and research directions that are shaping the future of Embedded Swift.</p>
<h2 id="heading-future-work">Future Work</h2>
<p>Embedded Swift is still a young but rapidly evolving technology. Its modern language features, type safety, and performance make it an attractive option for embedded development, and ongoing work is expanding its capabilities, reach, and ecosystem.</p>
<h3 id="heading-ongoing-improvements">Ongoing Improvements</h3>
<p><strong>Compiler Optimizations</strong>: The Swift compiler team is actively improving code generation for embedded targets, including:</p>
<ul>
<li><p>Reducing binary size</p>
</li>
<li><p>Minimizing ARC overhead</p>
</li>
<li><p>Improving static dispatch performance</p>
</li>
</ul>
<p><strong>Hardware Support</strong>: Embedded Swift can target a wide variety of ARM and RISC-V microcontrollers, which are popular for building industrial applications. Support for additional architectures is being developed.</p>
<p><strong>Tooling Enhancements</strong>: Tooling support for Embedded Swift is still evolving, but several community-driven and open-source efforts are making development more accessible:</p>
<ul>
<li><p><strong>Build Systems</strong>: The Swift Embedded Working Group provides example projects that adapt Swift Package Manager (SwiftPM) for cross-compilation. Custom linker scripts and build helpers are available for platforms like STM32 and nRF52.</p>
</li>
<li><p><strong>Debugging Support</strong>: Developers can debug Embedded Swift programs using existing tools like GDB or OpenOCD, provided the build includes appropriate debug symbols. While not yet officially streamlined, this approach enables step-through debugging on real hardware.</p>
</li>
<li><p><strong>IDE Integration</strong>: There is no official IDE support yet, but some developers use VSCode with Swift syntax highlighting and external build tasks. These setups are still manual but serve as early prototypes for embedded workflows.</p>
</li>
</ul>
<h3 id="heading-emerging-use-cases">Emerging Use Cases</h3>
<p>There are a number of emerging use cases for embedded Swift. For example, Swift’s memory safety, type guarantees, and protocol-oriented design make it ideal for secure and scalable IoT devices, especially where firmware bugs could affect user safety or privacy.</p>
<p>The automotive sector is also exploring Swift for infotainment systems, driver assistance features, and safety-critical logic (where deterministic execution and safety matter).</p>
<p>Swift’s expressive syntax and compile-time safety make it suitable for industrial automation – think real-time control loops, sensor fusion systems, and edge devices in smart manufacturing.</p>
<p>It’s also useful for medical devices, as it aligns well with strict medical regulations around memory safety, type guarantees, and predictable resource usage.</p>
<h3 id="heading-community-and-ecosystem">Community and Ecosystem</h3>
<h4 id="heading-open-source-projects">Open Source Projects</h4>
<p>The Swift Embedded working group maintains <a target="_blank" href="https://github.com/swiftlang/swift-embedded-examples">example repositories</a> showcasing how to use Embedded Swift on microcontrollers such as STM32, nRF52, and ESP32. Early-stage libraries for UART, GPIO, and basic peripherals are emerging, though the ecosystem is still young compared to C or Rust.</p>
<h4 id="heading-learning-resources">Learning Resources</h4>
<p>While <a target="_blank" href="https://docs.swift.org/embedded/documentation/embedded">Embedded Swift</a> is not yet widely taught in formal curricula, community tutorials and exploratory projects (for example, Swift for Arduino) are lowering the barrier for hobbyists and independent learners. As tooling matures, educational adoption is likely to follow.</p>
<h4 id="heading-industry-interest">Industry Interest</h4>
<p>Embedded Swift is beginning to draw attention from developers and companies looking for safer, more maintainable alternatives to C. Although large-scale adoption remains limited, use cases like rapid prototyping, IoT development, and internal experimentation are gaining traction.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Embedded Swift represents a major step forward in embedded programming. By combining the power and safety of Swift with the low-level control needed for microcontrollers, it offers an exciting alternative to traditional C and C++ development.</p>
<p>While C will remain essential for hardware-level programming and performance-critical paths, Swift brings compelling advantages to many embedded scenarios:</p>
<ul>
<li><p><strong>Memory safety</strong>: Swift eliminates entire categories of bugs such as buffer overflows, use-after-free, and null pointer dereferencing.</p>
</li>
<li><p><strong>Type safety</strong>: Many logic errors are caught at compile time, long before they can cause runtime failures.</p>
</li>
<li><p><strong>Modern language features</strong>: Developers can use functional paradigms, generics, and protocol-oriented design even in embedded code.</p>
</li>
<li><p><strong>C interoperability</strong>: Swift works seamlessly with existing C libraries, allowing gradual adoption without rewriting low-level drivers.</p>
</li>
<li><p><strong>Developer productivity</strong>: Clear syntax, automatic memory management, and strong tooling lead to faster development and easier maintenance.</p>
</li>
</ul>
<p>Government and regulatory bodies are increasingly encouraging or mandating the use of memory-safe programming languages to reduce vulnerabilities in critical software systems. For example:</p>
<ul>
<li><p>In 2022, the <a target="_blank" href="https://media.defense.gov/2025/Jun/23/2003742198/-1/-1/0/CSI_MEMORY_SAFE_LANGUAGES_REDUCING_VULNERABILITIES_IN_MODERN_SOFTWARE_DEVELOPMENT.PDF"><strong>U.S. National Security Agency (NSA)</strong></a> recommended moving away from unsafe languages like C/C++ for new software projects, promoting memory-safe alternatives.</p>
</li>
<li><p>In June 2025, the NSA and CISA released a joint Cybersecurity Information Sheet titled “<a target="_blank" href="https://www.nsa.gov/Press-Room/Press-Releases-Statements/Press-Release-View/Article/4223298/nsa-and-cisa-release-csi-highlighting-importance-of-memory-safe-languages-in-so/">Memory Safe Languages: Reducing Vulnerabilities in Modern Software Development</a>”, which emphasized that memory safety flaws remain a persistent risk, and organizations should develop strategies to adopt memory-safe programming languages in new systems.</p>
</li>
<li><p>The <a target="_blank" href="https://www.trust-in-soft.com/resources/blogs/memory-safety-is-key-the-shift-in-u.s.-cyber-standards"><strong>U.S. Cybersecurity and Infrastructure Security Agency (CISA)</strong></a> and <a target="_blank" href="https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-218.pdf"><strong>NIST</strong></a> have echoed similar guidance in the context of national cybersecurity.</p>
</li>
</ul>
<p>While these documents do not mention Swift explicitly, Swift's strong type system, ARC-based memory model, and compile-time safety guarantees align closely with the goals outlined in these recommendations. As such, it offers a practical, developer-friendly path toward safer embedded development.</p>
<p>Swift may not be the right fit for every embedded system. In applications where every byte of memory or instruction cycle is critical, real-time guarantees are hard requirements, or toolchain maturity is essential (for example, RTOS integration, static analyzers), C or Rust may still be preferred.</p>
<p>But in many modern embedded applications, especially those involving rapid prototyping, fast product iteration, safety-critical or maintainable firmware, and interoperability with existing C codebases, Swift offers a highly productive and safe development experience.</p>
<p>Embedded Swift is still maturing, but its momentum is undeniable. With ongoing compiler work, community-driven examples, and growing interest from developers, it’s poised to play a major role in the future of embedded systems.</p>
<p>Whether you're building an IoT device, a piece of industrial equipment, or a proof-of-concept wearable, Swift can help you write safer, more expressive firmware, without giving up performance or control.</p>
<p>Swift can be especially powerful during the prototyping phase, when the primary goal is to validate functionality quickly and safely. And with its increasing support for multiple hardware platforms, it offers a strong foundation for bringing modern software development practices to the embedded world.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How AI Agents Remember Things: The Role of Vector Stores in LLM Memory ]]>
                </title>
                <description>
                    <![CDATA[ When you talk to an AI assistant, it can feel like it remembers what you said before. But large language models (LLMs) don’t actually have memory on their own. They don’t remember conversations unless that information is given to them again. So, how ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-ai-agents-remember-things-vector-stores-in-llm-memory/</link>
                <guid isPermaLink="false">687903d3ccb68881821f3a68</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory-management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Thu, 17 Jul 2025 14:08:19 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1747753163050/26574896-6da9-4a30-af4f-1d0b7f38f43b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you talk to an AI assistant, it can feel like it remembers what you said before.</p>
<p>But large language models (LLMs) don’t actually have memory on their own. They don’t remember conversations unless that information is given to them again.</p>
<p>So, how do they seem to recall things?</p>
<p>The answer lies in something called a vector store – and that’s what you’ll learn about in this article.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-vector-store">What Is a Vector Store?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-embeddings-work">How Embeddings Work</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-vector-stores-are-crucial-for-memory">Why Vector Stores Are Crucial for Memory</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-popular-vector-stores">Popular Vector Stores</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-faiss-facebook-ai-similarity-searchhttpsaimetacomtoolsfaiss">FAISS (Facebook AI Similarity Search)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-pineconehttpswwwpineconeio">Pinecone</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-making-ai-seem-smart-with-retrieval-augmented-generation">Making AI Seem Smart with Retrieval-Augmented Generation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-limits-of-vector-based-memory">The Limits of Vector-Based Memory</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-a-vector-store"><strong>What Is a Vector Store?</strong></h2>
<p>A vector store is a special type of database. Instead of storing text or numbers like a regular database, it stores vectors.</p>
<p>A vector is a list of numbers that represents the meaning of a piece of text. You get these vectors using a process called embedding.</p>
<p>The model takes a sentence and turns it into a high-dimensional point in space. In that space, similar meanings are close together.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746419405154/214a0566-8dc6-4402-a0f1-e30f8d81003c.png" alt="214a0566-8dc6-4402-a0f1-e30f8d81003c" class="image--center mx-auto" width="1200" height="1121" loading="lazy"></p>
<p>For example, if I embed “I love sushi,” it might be close to “Sushi is my favourite food” in vector space. These embeddings help an AI agent find related thoughts even if the exact words differ.</p>
<h2 id="heading-how-embeddings-work"><strong>How Embeddings Work</strong></h2>
<p>Let’s say a user tells an assistant:</p>
<pre><code class="lang-plaintext">“I live in Austin, Texas.”
</code></pre>
<p>The model turns this sentence into a vector:</p>
<pre><code class="lang-plaintext">[0.23, -0.41, 0.77, ..., 0.08]
</code></pre>
<p>This vector doesn’t mean much to us, but to the AI, it’s a way to capture the sentence’s meaning. That vector gets stored in a vector database, along with some extra info – maybe a timestamp or a note that it came from this user.</p>
<p>Later, if the user says:</p>
<pre><code class="lang-plaintext">“Book a flight to my hometown.”
</code></pre>
<p>The model turns this new sentence into a new vector. It then searches the vector database to find the most similar stored vectors.</p>
<p>The closest match might be “I live in Austin, Texas.” Now the AI knows what you probably meant by “my hometown.”</p>
<p>This ability to look up related past inputs based on meaning – not just matching keywords – is what gives LLMs a form of memory.</p>
<h2 id="heading-why-vector-stores-are-crucial-for-memory"><strong>Why Vector Stores Are Crucial for Memory</strong></h2>
<p>LLMs process language using a context window. That’s the amount of text they can “see” at once.</p>
<p>For GPT-4-turbo, the window can handle up to 128,000 tokens, which sounds huge – but even that gets filled fast. You can’t keep the whole conversation there forever.</p>
<p>Instead, you use a vector store as long-term memory. You embed and save useful info.</p>
<p>Then, when needed, you query the vector store, retrieve the top relevant pieces, and feed them back into the LLM. This way, the model remembers just enough to act smart – without holding everything in its short-term memory.</p>
<h2 id="heading-popular-vector-stores"><strong>Popular Vector Stores</strong></h2>
<p>There are several popular vector databases in use. Each one has its strengths.</p>
<h3 id="heading-faiss-facebook-ai-similarity-searchhttpsaimetacomtoolsfaiss"><a target="_blank" href="https://ai.meta.com/tools/faiss">FAISS (Facebook AI Similarity Search)</a></h3>
<p>FAISS is an open-source library developed by Meta. It’s fast and works well for local or on-premise applications.</p>
<p>FAISS is great if you want full control and don’t need cloud hosting. It supports millions of vectors and provides tools for indexing and searching with high performance.</p>
<p>Here’s how you can use FAISS:</p>
<pre><code class="lang-plaintext">from sentence_transformers import SentenceTransformer
import faiss
import numpy as np
</code></pre>
<pre><code class="lang-plaintext"># Load a pre-trained sentence transformer model that converts sentences to numerical vectors (embeddings)
model = SentenceTransformer('all-MiniLM-L6-v2')

# Define the input sentence we want to store in memory
sentence = "User lives in Austin, Texas"

# Convert the sentence into a dense vector (embedding)
embedding = model.encode(sentence)

# Get the dimensionality of the embedding vector (needed to create the FAISS index)
dimension = embedding.shape[0]

# Create a FAISS index for L2 (Euclidean) similarity search using the embedding dimension
index = faiss.IndexFlatL2(dimension)

# Add the sentence embedding to the FAISS index (this is our "memory")
index.add(np.array([embedding]))

# Encode a new query sentence that we want to match against the stored memory
query = model.encode("Where is the user from?")

# Search the FAISS index for the top-1 most similar vector to the query
D, I = index.search(np.array([query]), k=1)

# Print the index of the most relevant memory (in this case, only one item in the index)
print("Most relevant memory index:", I[0][0])
</code></pre>
<p>This code uses a pre-trained model to turn a sentence like “User lives in Austin, Texas” into an embedding.</p>
<p>It stores this embedding in a FAISS index. When you ask a question like “Where is the user from?”, the code converts that question into another embedding and searches the index to find the stored sentence that’s most similar in meaning.</p>
<p>Finally, it prints the position (index) of the most relevant sentence in the memory.</p>
<p>FAISS is efficient, but it’s not hosted. That means you need to manage your own infrastructure.</p>
<h3 id="heading-pineconehttpswwwpineconeio"><a target="_blank" href="https://www.pinecone.io">Pinecone</a></h3>
<p>Pinecone is a cloud-native vector database. It’s managed for you, which makes it great for production systems.</p>
<p>You don’t need to worry about scaling or maintaining servers. Pinecone handles billions of vectors and offers filtering, metadata support, and fast queries. It integrates well with tools like LangChain and OpenAI.</p>
<p>Here’s how a basic Pinecone setup works:</p>
<pre><code class="lang-plaintext">import pinecone
from sentence_transformers import SentenceTransformer
</code></pre>
<pre><code class="lang-plaintext"># Initialize Pinecone with your API key and environment
pinecone.init(api_key="your-api-key", environment="us-west1-gcp")

# Connect to or create a Pinecone index named "memory-store"
index = pinecone.Index("memory-store")

# Load a pre-trained sentence transformer model to convert text into embeddings
model = SentenceTransformer('all-MiniLM-L6-v2')

# Convert a fact/sentence into a numerical embedding (vector)
embedding = model.encode("User prefers vegetarian food")

# Store (upsert) the embedding into Pinecone with a unique ID
index.upsert([("user-pref-001", embedding.tolist())])

# Encode the query sentence into an embedding
query = model.encode("What kind of food does the user like?")

# Search Pinecone to find the most relevant stored embedding for the query
results = index.query(queries=[query.tolist()], top_k=1)

# Print the ID of the top matching memory
print("Top match ID:", results['matches'][0]['id'])
</code></pre>
<p>Pinecone is ideal if you want scalability and ease of use without managing hardware.</p>
<p>Other popular vector stores include:</p>
<ul>
<li><p><a target="_blank" href="https://weaviate.io"><strong>Weaviate</strong></a> – Combines vector search with knowledge graphs. Offers strong semantic search with hybrid keyword support.</p>
</li>
<li><p><a target="_blank" href="https://www.trychroma.com"><strong>Chroma</strong></a> – Simple to use and good for prototyping. Often used in personal apps or demos.</p>
</li>
<li><p><a target="_blank" href="https://qdrant.tech"><strong>Qdrant</strong></a> – Open-source and built for high-performance vector search with filtering.</p>
</li>
</ul>
<p>Each of these has its place depending on whether you need speed, scale, simplicity, or special features.</p>
<h2 id="heading-making-ai-seem-smart-with-retrieval-augmented-generation"><strong>Making AI Seem Smart with Retrieval-Augmented Generation</strong></h2>
<p>This whole system – embedding user inputs, storing them in a vector database, and retrieving them later – is called <a target="_blank" href="https://www.freecodecamp.org/news/retrieval-augmented-generation-rag-handbook/">retrieval-augmented generation (RAG)</a>.</p>
<p>The AI still doesn’t have a brain, but it can act like it does. You choose what to remember, when to recall it, and how to feed it back into the conversation.</p>
<p>If the AI helps a user track project updates, you can store each project detail as a vector. When the user later asks, “What’s the status of the design phase?” you search your memory database, pull the most relevant notes, and let the LLM stitch them into a helpful answer.</p>
<h2 id="heading-the-limits-of-vector-based-memory"><strong>The Limits of Vector-Based Memory</strong></h2>
<p>While vector stores give AI agents a powerful way to simulate memory, this approach comes with some important limitations.</p>
<p>Vector search is based on similarity, not true understanding. That means the most similar stored embedding may not always be the most relevant or helpful in context. For instance, two sentences might be mathematically close in vector space but carry very different meanings. As a result, the AI can sometimes surface confusing or off-topic results, especially when nuance or emotional tone is involved.</p>
<p>Another challenge is that embeddings are static snapshots. Once stored, they don’t evolve or adapt unless explicitly updated. If a user changes their mind or provides new information, the system won’t "learn" unless the original vector is removed or replaced. Unlike human memory, which adapts and refines itself over time, vector-based memory is frozen unless developers actively manage it.</p>
<p>There are a few ways you can mitigate these challenges.</p>
<p>One is to include more context in the retrieval process, such as filtering results by metadata like timestamps, topics, or user intent. This helps narrow down results to what’s truly relevant at the moment.</p>
<p>Another approach is to reprocess or re-embed older memories periodically, ensuring that the information reflects the most current understanding of the user’s needs or preferences.</p>
<p>Beyond technical limitations, vector stores also raise privacy and ethical concerns. Key questions are: Who decides what gets saved? How long should that memory persist? And does the user have control over what is remembered or forgotten?</p>
<p>Ideally, these decisions should not be made solely by the developer or system. A more thoughtful approach is to make memory explicit. Let users choose what gets remembered. For example, by marking certain inputs as “important”, it adds a layer of consent and transparency. Similarly, memory retention should be time-bound where appropriate, with expiration policies based on how long the information remains useful.</p>
<p>Equally important is the ability for users to view, manage, or delete their stored data. Whether through a simple interface or a programmatic API, memory management tools are essential for trust. As the use of vector stores expands, so does the expectation that AI systems will respect user agency and privacy.</p>
<p>The broader AI community is still shaping best practices around these issues. But one thing is clear: simulated memory should be designed not just for accuracy and performance, but for accountability. By combining strong defaults with user control, developers can ensure vector-based memory systems are both smart and responsible.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Vector stores give AI agents a way to fake memory – and they do it well. By embedding text into vectors and using tools like FAISS or Pinecone, we give models the power to recall what matters. It’s not real memory. But it makes AI systems feel more personal, more helpful, and more human.</p>
<p>As these tools grow more advanced, so does the illusion. But behind every smart AI is a simple system of vectors and similarity. If you can master that, you can build assistants that remember, learn, and improve with time.</p>
<p>Hope you enjoyed this article. <a target="_blank" href="https://www.linkedin.com/in/manishmshiva/"><strong>Connect with me on Linkedin</strong>.</a></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Debug and Prevent Buffer Overflows in Embedded Systems ]]>
                </title>
                <description>
                    <![CDATA[ Buffer overflows are one of the most serious software bugs, especially in embedded systems, where hardware limitations and real-time execution make them hard to detect and fix. A buffer overflow happens when a program writes more data into a buffer t... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-debug-and-prevent-buffer-overflows-in-embedded-systems/</link>
                <guid isPermaLink="false">67d84f228d156200bc7d3d8c</guid>
                
                    <category>
                        <![CDATA[ embedded systems ]]>
                    </category>
                
                    <category>
                        <![CDATA[ embedded ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory-management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Buffer Overfow ]]>
                    </category>
                
                    <category>
                        <![CDATA[ debugging ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Firmware Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Software Engineering ]]>
                    </category>
                
                    <category>
                        <![CDATA[ learn to code ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming basics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ C ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Coding Best Practices ]]>
                    </category>
                
                    <category>
                        <![CDATA[ clean code ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Soham Banerjee ]]>
                </dc:creator>
                <pubDate>Mon, 17 Mar 2025 16:34:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742229245130/858b21cc-443e-43ee-82ce-091438f6c5c0.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Buffer overflows are one of the most serious software bugs, especially in embedded systems, where hardware limitations and real-time execution make them hard to detect and fix.</p>
<p>A buffer overflow happens when a program writes more data into a buffer than it was allocated, leading to memory corruption, crashes, or even security vulnerabilities. A buffer corruption occurs when unintended modifications overwrite unread data or modify memory in unexpected ways.</p>
<p>In safety-critical systems like cars, medical devices, and spacecraft, buffer overflows can cause life-threatening failures. Unlike simple software bugs, buffer overflows are unpredictable and depend on the state of the system, making them difficult to diagnose and debug.</p>
<p>To prevent these issues, it's important to understand how buffer overflows and corruptions occur, and how to detect and fix them.</p>
<h2 id="heading-article-scope">Article Scope</h2>
<p>In this article, you will learn:</p>
<ol>
<li><p>What buffers, buffer overflows, and corruptions are. I’ll give you a beginner-friendly explanation with real-world examples.</p>
</li>
<li><p>How to debug buffer overflows. You’ll learn how to use tools like GDB, LLDB, and memory maps to find memory corruption.</p>
</li>
<li><p>How to prevent buffer overflows. We’ll cover some best practices like input validation, safe memory handling, and defensive programming.</p>
</li>
</ol>
<p>I’ll also show you some hands-on code examples – simple C programs that demonstrate buffer overflow issues and how to fix them.</p>
<p>What this article doesn’t cover:</p>
<ol>
<li><p>Security exploits and hacking techniques. We’ll focus on preventing accidental overflows, not hacking-related buffer overflows.</p>
</li>
<li><p>Operating system-specific issues. This guide is for embedded systems, not general-purpose computers or servers.</p>
</li>
<li><p>Advanced RTOS memory management. While we discuss interrupt-driven overflows, we won’t dive deep into real-time operating system (RTOS) concepts.</p>
</li>
</ol>
<p>Now that you know what this article covers (and what it doesn’t), let’s go over the skills that will help you get the most out of it.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This article is designed for developers who have some experience with C programming and want to understand how to debug and prevent buffer overflows in embedded systems. Still, beginners can follow along, as I’ll explain key concepts in a clear and structured way.</p>
<p>Before reading, it helps if you know:</p>
<ol>
<li><p>Basic C programming.</p>
</li>
<li><p>How memory works – the difference between stack, heap, and global variables.</p>
</li>
<li><p>Basic debugging concepts – if you’ve used a debugger like GDB or LLDB, that’s a plus, but not required.</p>
</li>
<li><p>What embedded systems are – a basic idea of how microcontrollers store and manage memory.</p>
</li>
</ol>
<p>Even if you’re not familiar with these topics, this guide will walk you through them in an easy-to-understand way.</p>
<p>Before you dive into buffer overflows, debugging, and prevention, let’s take a step back and understand what a buffer is and why it’s important in embedded systems. Buffers play a crucial role in managing data flow between hardware and software but when handled incorrectly, they can lead to serious software failures.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-buffer-and-how-does-it-work">What is a Buffer, and How Does it Work?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-a-buffer-overflow">What is a Buffer Overflow?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-common-causes-of-buffer-overflows-and-corruption">Common Causes of Buffer Overflows and Corruption</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-consequences-of-buffer-overflows">Consequences of Buffer Overflows</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-debug-buffer-overflows">How to Debug Buffer Overflows</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-prevent-buffer-overflows">How to Prevent Buffer Overflows</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-a-buffer-and-how-does-it-work">What is a Buffer, and How Does it Work?</h2>
<p>A buffer is a contiguous block of memory used to temporarily store data before it is processed. Buffers are commonly used in two scenarios:</p>
<ol>
<li><p>Data accumulation: When the system needs to collect a certain amount of data before processing.</p>
</li>
<li><p>Rate matching: When the data producer generates data faster than the data consumer can process it.</p>
</li>
</ol>
<p>Buffers are typically implemented as arrays in C, where elements are indexed from 0 to N-1 (where N is the buffer size).</p>
<p>Let’s look at an example of a buffer in a sensor system.</p>
<p>Consider a system with a sensor task that generates data at 400 Hz (400 samples per second or 1 sample every 2.5 ms). But the data processor (consumer) operates at only 100 Hz (100 samples per second or 1 sample every 10 ms). Since the consumer task is slower than the producer, we need a buffer to store incoming data until it is processed.</p>
<p>To determine the buffer size, we calculate:</p>
<p>Buffer Size = Time to consume 1 sample / Time to generate 1 sample = 10 ms/ 2.5 ms = 4</p>
<p>This means the buffer must hold at least 4 samples at a time to avoid data loss.</p>
<p>Once the buffer reaches capacity, there are several strategies to decide which data gets passed to the consumer task:</p>
<ol>
<li><p>Max/min sampling: Use the maximum or minimum value in the buffer.</p>
</li>
<li><p>Averaging: Compute the average of all values in the buffer.</p>
</li>
<li><p>Random access: Pick a sample from a specific location (for example, the most recent or the first).</p>
</li>
</ol>
<p>In real-world applications, it’s beneficial to use circular buffers or double buffering to prevent data corruption.</p>
<ul>
<li><p>Circular buffer approach: A circular buffer (also called a ring buffer) continuously wraps around when it reaches the end, ensuring old data is overwritten safely without exceeding memory boundaries. The buffer size should be multiplied by 2 (4 × 2 = 8) to hold 8 samples. This allows the consumer task to process 4 samples while the next 4 samples are being filled, preventing data overwrites.</p>
</li>
<li><p>Double buffer approach: Double buffering is useful when data loss is unacceptable. It allows continuous data capture while the processor is busy handling previous data. A second buffer of the same size is added. When the first buffer is full, the write pointer switches to the second buffer, allowing the consumer task to process data from the first buffer while the second buffer is being filled. This prevents data overwrites and ensures a continuous data flow.</p>
</li>
</ul>
<p>Buffers help manage data efficiently, but what happens when they are mismanaged? This is where buffer overflows and corruptions come into play.</p>
<h2 id="heading-what-is-a-buffer-overflow">What is a Buffer Overflow?</h2>
<p>A buffer overflow occurs when a program writes more data into a buffer than it was allocated, causing unintended memory corruption. This can lead to unpredictable behavior, ranging from minor bugs to critical system failures.</p>
<p>To understand buffer overflow, let's use a simple analogy. Imagine a jug with a tap near the bottom. The jug represents a buffer, while the tap controls how much liquid (data) is consumed.</p>
<p>The jug is designed to hold a fixed amount of liquid. As long as water flows into the jug at the same rate or slower than it flows out, everything works fine. But if water flows in faster than it flows out, the jug will eventually overflow.</p>
<p>Similarly, in software, if data enters a buffer faster than it is processed, it exceeds the allocated memory space, causing a buffer overflow. In the case of a circular buffer, this can cause the write pointer to wrap around and overwrite unread data, leading to buffer corruption.</p>
<h3 id="heading-buffer-overflows-in-software">Buffer Overflows in Software</h3>
<p>Unlike the jug, where water simply spills over, a buffer overflow in software overwrites adjacent memory locations. This can cause a variety of hard-to-diagnose issues, including:</p>
<ol>
<li><p>Corrupting other data stored nearby.</p>
</li>
<li><p>Altering program execution, leading to crashes.</p>
</li>
<li><p>Security vulnerabilities, where attackers exploit overflows to inject malicious code.</p>
</li>
</ol>
<p>When a buffer overflow occurs, data can overwrite variables, function pointers, or even return addresses, depending on where the buffer is allocated.</p>
<p>Buffer overflows can occur in different memory regions:</p>
<ol>
<li><p>Buffer overflows in global/static memory (.bss / .data sections)</p>
<ul>
<li><p>These occur when global or static variables exceed their allocated size.</p>
</li>
<li><p>The overflow can corrupt adjacent variables, leading to unexpected behavior in other modules.</p>
</li>
<li><p>Debugging is easier because memory addresses are fixed at compile time unless the compiler optimizes them. Map files provide a memory layout of variables during the compilation and linking.</p>
</li>
</ul>
</li>
<li><p>Stack-based buffer overflow (more predictable, easier to debug):</p>
<ul>
<li><p>Happens when a buffer is allocated in the stack (for example, local variables inside functions).</p>
</li>
<li><p>Overflowing the stack can affect adjacent local variables or return addresses, potentially crashing the program.</p>
</li>
<li><p>In embedded systems with small stack sizes, this often leads to a crash or execution of unintended code.</p>
</li>
</ul>
</li>
<li><p>Heap-based buffer overflow (harder to debug):</p>
<ul>
<li><p>Happens when a buffer is dynamically allocated in the heap (for example, using malloc() in C).</p>
</li>
<li><p>Overflowing a heap buffer can corrupt adjacent dynamically allocated objects or heap management structures.</p>
</li>
<li><p>Debugging is harder because heap memory is allocated dynamically at runtime, causing memory locations to vary.</p>
</li>
</ul>
</li>
</ol>
<h4 id="heading-buffer-overflow-vs-buffer-corruption">Buffer Overflow vs Buffer Corruption</h4>
<p>Buffer overflow and buffer corruption are of course related, but refer to different situations.</p>
<p>A buffer overflow happens when data is written beyond the allocated buffer size, leading to memory corruption, unpredictable behavior, or system crashes.</p>
<p>A buffer corruption happens when unintended data modifications result in unexpected software failures, even if the write remains within buffer boundaries.</p>
<p>Both issues typically result from poor write pointer management, lack of boundary checks, and unexpected system behavior.</p>
<p>Now that we've covered what a buffer overflow is and how it can overwrite memory, let’s take a closer look at how these issues affect embedded systems.</p>
<p>In the next section, we’ll explore how buffer overflows and corruption happen in real-world embedded systems and break down common causes, including pointer mismanagement and boundary violations.</p>
<h2 id="heading-common-causes-of-buffer-overflows-and-corruption">Common Causes of Buffer Overflows and Corruption</h2>
<p>Embedded systems use buffers to store data from sensors, communication interfaces (like UART (Universal Asynchronous Receiver-Transmitter), SPI (Serial Peripheral Interface), I2C (Inter-integrated Circuit), and real-time tasks. These buffers are often statically allocated to avoid memory fragmentation, and many implementations use circular (ring) buffers to efficiently handle continuous data streams.</p>
<p>Here are three common scenarios where buffer overflows or corruptions occur in embedded systems:</p>
<h3 id="heading-writing-data-larger-than-the-available-space">Writing Data Larger Than the Available Space</h3>
<p><strong>Issue</strong>: The software writes incoming data to the buffer without checking if there is enough space.</p>
<p><strong>Example</strong>: Imagine a 100-byte buffer to store sensor data. The buffer receives variable-sized packets. If an incoming packet is larger than the remaining space, it will overwrite adjacent memory, leading to corruption.</p>
<p>So why does this happen?</p>
<ul>
<li><p>Some embedded designs increment the write pointer after copying data, making it too late to prevent overflow.</p>
</li>
<li><p>Many low-level memory functions (memcpy, strcpy, etc.) do not check buffer boundaries, leading to unintended writes.</p>
</li>
<li><p>Without proper bound checking, a large write can exceed the buffer size and corrupt nearby memory.</p>
</li>
</ul>
<p>Here’s a code sample to demonstrate buffer overflow in a .bss / .data section:</p>
<pre><code class="lang-c">  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
  <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;string.h&gt;</span></span>

  <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> BUFFER_SIZE 300</span>

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint16_t</span> sample_count = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint8_t</span> buffer[BUFFER_SIZE] = {<span class="hljs-number">0</span>};

  <span class="hljs-comment">// Function to simulate a buffer overflow scenario</span>
  <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">updateBufferWithData</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> *data, <span class="hljs-keyword">uint16_t</span> size)</span>
  </span>{
      <span class="hljs-comment">// Simulating a buffer overflow: No boundary check!</span>
      <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Attempting to write %d bytes at position %d...\n"</span>, size, sample_count);

      <span class="hljs-comment">// Deliberate buffer overflow for demonstration</span>
      <span class="hljs-keyword">if</span> (sample_count + size &gt; BUFFER_SIZE)
      {
          <span class="hljs-built_in">printf</span>(<span class="hljs-string">"WARNING: Buffer Overflow Occurred! Writing beyond allocated memory!\n"</span>);
      }

      <span class="hljs-comment">// Copy data (unsafe, can cause overflow)</span>
      <span class="hljs-built_in">memcpy</span>(&amp;buffer[sample_count], data, size);

      <span class="hljs-comment">// Increment sample count (incorrectly, leading to wraparound issues)</span>
      sample_count += size;
  }

  <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span>
  </span>{   
      <span class="hljs-comment">// Save 1 byte to buffer</span>
      <span class="hljs-keyword">uint8_t</span> data_to_buffer = <span class="hljs-number">10</span>;
      updateBufferWithData(&amp;data_to_buffer, <span class="hljs-number">1</span>);

      <span class="hljs-comment">// Save an array of 20 bytes to buffer</span>
      <span class="hljs-keyword">uint8_t</span> data_to_buffer_1[<span class="hljs-number">20</span>] = {<span class="hljs-number">5</span>};
      updateBufferWithData(data_to_buffer_1, <span class="hljs-keyword">sizeof</span>(data_to_buffer_1));

      <span class="hljs-comment">// Intentional buffer overflow: Save an array of 50 x 8 bytes (400 bytes)</span>
      <span class="hljs-keyword">uint64_t</span> data_to_buffer_2[<span class="hljs-number">50</span>] = {<span class="hljs-number">7</span>};
      updateBufferWithData((<span class="hljs-keyword">uint8_t</span>*)data_to_buffer_2, <span class="hljs-keyword">sizeof</span>(data_to_buffer_2));

      <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
  }
</code></pre>
<h3 id="heading-interrupt-driven-overflows-real-time-systems">Interrupt-Driven Overflows (Real-time Systems)</h3>
<p><strong>Issue</strong>: The interrupt service routine (ISR) may write data faster than the main task can process, leading to buffer corruption or buffer overflow if the write pointer is not properly managed.</p>
<p><strong>Example</strong>: Imagine a sensor ISR that writes incoming data into a buffer every time a new reading arrives. Meanwhile, a low-priority processing task reads and processes the data.</p>
<p>What can go wrong?</p>
<ul>
<li><p>If the ISR triggers too frequently (due to a misbehaving sensor or high interrupt priority), the buffer may fill up faster than the processing task can keep up.</p>
</li>
<li><p>This can result in one of two failures:</p>
<ol>
<li><p>Buffer Corruption: The ISR overwrites unread data, leading to loss of information.</p>
</li>
<li><p>Buffer Overflow: The ISR exceeds buffer boundaries, causing memory corruption or system crashes.</p>
</li>
</ol>
</li>
</ul>
<p>So why does this happen?</p>
<ul>
<li><p>In real-time embedded systems, ISR execution preempts lower-priority tasks.</p>
</li>
<li><p>If the processing task doesn't not get enough CPU time, the buffer may become overwritten or overflow beyond its allocated scope.</p>
</li>
</ul>
<h3 id="heading-system-state-changes-amp-buffer-corruption">System State Changes &amp; Buffer Corruption</h3>
<p><strong>Issue</strong>: The system may unexpectedly reset, enter low-power mode, or changes operating state, leaving the buffer write pointers in an inconsistent state. This can result in buffer corruption (stale or incorrect data) or buffer overflow (writing past the buffer’s limits.</p>
<p><strong>Example Scenarios</strong>:</p>
<ol>
<li><p>Low-power wake-up issue (Buffer Overflow risk): Some embedded systems enter deep sleep to conserve energy. Upon waking up, if the buffer write pointer is not correctly reinitialized, it may point outside buffer boundaries, leading to buffer overflow and unintended memory corruption.</p>
</li>
<li><p>Unexpected mode transitions: If a sensor task is writing data and the system suddenly switches modes, the buffer states and pointers may not be cleaned up. The next time the sensor task runs, it may continue writing without clearing previous data. This can cause undefined behavior due to presence of stale data.</p>
</li>
</ol>
<p>Now that you understand how buffer overflows and corruptions happen, let’s examine their consequences in embedded systems ranging from incorrect sensor readings to complete system failures, making debugging and prevention critical.</p>
<h2 id="heading-consequences-of-buffer-overflows">Consequences of Buffer Overflows</h2>
<p>Buffer overflows can be catastrophic in embedded systems, leading to system crashes, data corruption, and unpredictable behavior. Unlike general-purpose computers, many embedded devices lack memory protection, making them particularly vulnerable to buffer overflows.</p>
<p>A buffer overflow can corrupt two critical types of memory:</p>
<h3 id="heading-1-data-variables-corruption">1. Data Variables Corruption</h3>
<p>A buffer overflow can overwrite data variables, corrupting the inputs for other software modules. This can cause unexpected behavior or even system crashes if critical parameters are modified.</p>
<p>For example, a buffer overflow could accidentally overwrite a sensor calibration value stored in memory. As a result, the system would start using incorrect sensor readings, leading to faulty operation and potentially unsafe conditions.</p>
<h3 id="heading-2-function-pointer-corruption">2. Function Pointer Corruption</h3>
<p>In embedded systems, function pointers are often used for interrupt handlers, callback functions, and RTOS task scheduling. If a buffer overflow corrupts a function pointer, the system may execute unintended instructions, leading to a crash or unexpected behavior.</p>
<p>As an example, a function pointer controlling motor speed regulation could be overwritten. Instead of executing the correct function, the system would jump to a random memory address, causing a system fault or erratic motor behavior.</p>
<p>Buffer overflows are among the hardest bugs to identify and fix because their effects depend on which data is corrupted and the values it contains. A buffer overflow can affect memory in different ways:</p>
<ul>
<li><p>If a buffer overflow corrupts unused memory, the system may seem fine during testing, making the issue harder to detect.</p>
</li>
<li><p>if a buffer overflow alters critical data variables, it can cause hidden logic errors that cause unpredictable behavior.</p>
</li>
<li><p>If a buffer overflow corrupts function pointers, it may crash immediately, making the problem easier to identify.</p>
</li>
</ul>
<p>During development, if tests focus only on detecting crashes, they may overlook silent memory corruption caused by a buffer overflow. In real-world deployments, new use cases not covered in testing can trigger previously undetected buffer overflow issues, leading to unpredictable failures.</p>
<p>Buffer overflows can cause a chain reaction, where one overflow leads to another overflow or buffer corruption, resulting in widespread system failures. So how does this happen?</p>
<ol>
<li><p>A buffer overflow corrupts a critical variable (for example, a timer interval).</p>
</li>
<li><p>The corrupted variable disrupts another module (for example, triggers the timer interrupt too frequently, causing it to push more data into a buffer than intended.).</p>
</li>
<li><p>This increased interrupt frequency forces a sensor task to write data faster than intended, eventually causing another buffer overflow or corruption by overwriting unread data.</p>
</li>
</ol>
<p>This chain reaction can spread across multiple software modules, making debugging nearly impossible. In real-word applications, buffer overflows in embedded systems can be life-threatening:</p>
<ul>
<li><p>In cars: A buffer overflow in an ECU (Electronic Control Unit) could cause brake failure or unintended acceleration.</p>
</li>
<li><p>In a spacecraft: A memory corruption issue could disable navigation systems, leading to mission failure.</p>
</li>
</ul>
<p>Now that we’ve seen how buffer overflows can corrupt memory, disrupt system behavior, and even cause critical failures, the next step is understanding how to detect and fix them before they lead to serious issues.</p>
<h2 id="heading-how-to-debug-buffer-overflows">How to Debug Buffer Overflows</h2>
<p>Debugging buffer overflows in embedded systems can be complex, as their effects range from immediate crashes to silent data corruption, making them difficult to trace. A buffer overflow can cause either:</p>
<ol>
<li><p>A system crash, which is easier to detect since it halts execution or forces a system reboot.</p>
</li>
<li><p>Unexpected behavior, which is much harder to debug as it requires tracing how corrupted data affects different modules.</p>
</li>
</ol>
<p>This section focuses on embedded system debugging techniques using memory map files, debuggers (GDB/LLDB), and a structured debugging approach. Let’s look into the debuggers and memory map files.</p>
<h3 id="heading-memory-map-file-map-file">Memory Map File (.map file)</h3>
<p>A memory map file is generated during the linking process. It provides a memory layout of global/static variables, function addresses, and heap/stack locations. It provides a memory layout of Flash and RAM, including:</p>
<ul>
<li><p>Text section (.text): Stores executable code.</p>
</li>
<li><p>Read-only section (.rodata): Stores constants and string literals.</p>
</li>
<li><p>BSS section (.bss): Stores uninitialized global and static variables.</p>
</li>
<li><p>Data section (.data): Stores initialized global and static variables.</p>
</li>
<li><p>Heap and stack locations, depending on the linker script.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739064875727/1e01992d-4d9d-42fb-b971-6f4e92452c22.png" alt="Figure 1: A visual of the memory layout" class="image--center mx-auto" width="1256" height="425" loading="lazy"></p>
<p>If a buffer overflow corrupts a global variable, the .map file can identify nearby variables that may also be affected, provided the compiler has not optimized the memory allocation. Similarly, if a function pointer is corrupted, the .map file can reveal where it was stored in memory.</p>
<h3 id="heading-debuggers-gdb-amp-lldb">Debuggers (GDB &amp; LLDB)</h3>
<p>Debugging tools like GDB (GNU Debugger) and LLDB (LLVM Debugger) allow:</p>
<ul>
<li><p>Controlling execution (breakpoints, stepping through code).</p>
</li>
<li><p>Inspecting variable values and memory addresses.</p>
</li>
<li><p>Getting backtraces (viewing function calls before a crash).</p>
</li>
<li><p>Extracting core dumps from microcontrollers for post-mortem analysis.</p>
</li>
</ul>
<p>If the system halts on a crash, a backtrace (bt command in GDB) can reveal which function was executing before failure. If the overflow affects a heap-allocated variable, GDB can inspect heap memory usage to detect corruption.</p>
<h3 id="heading-the-debugging-process">The Debugging Process</h3>
<p>Now, let’s go through a step-by-step debugging process to identify and fix buffer overflows. Once a crash or unexpected behavior occurs, follow these techniques to trace the root cause:</p>
<h4 id="heading-step-1-identify-the-misbehaving-module">Step 1: Identify the misbehaving module</h4>
<p>If the system crashes, use GDB or LLDB backtrace (bt command) to locate the last executed function. If the system behaves unexpectedly, determine which software module controls the affected functionality.</p>
<h4 id="heading-step-2-analyze-inputs-and-outputs-of-the-module">Step 2: Analyze inputs and outputs of the module</h4>
<p>Every function or module has inputs and outputs. Create a truth table listing expected outputs for all possible inputs. Check if the unexpected behavior matches any undefined input combination, which may indicate corruption.</p>
<h4 id="heading-step-3-locate-memory-corruption-using-address-analysis">Step 3: Locate memory corruption using address analysis</h4>
<p>If a variable shows incorrect values, determine its physical memory location. Depending on where the variable is stored:</p>
<ol>
<li><p>Global/static variables (.bss / .data): Look up the memory map file for nearby buffers.</p>
</li>
<li><p>Heap variables: Snapshot heap allocations using GDB.  </p>
<p> Here’s an example of using GDB to find corrupted variables:</p>
<pre><code class="lang-c"> (gdb) print &amp;my_variable  # Get memory address of the variable
 $<span class="hljs-number">1</span> = (<span class="hljs-keyword">int</span> *) <span class="hljs-number">0x20001000</span>
 (gdb) x/<span class="hljs-number">10</span>x <span class="hljs-number">0x20001000</span>   # Examine memory near <span class="hljs-keyword">this</span> address, Display <span class="hljs-number">10</span> memory words in hexadecimal format starting from <span class="hljs-number">0x20001000</span>
</code></pre>
</li>
</ol>
<h4 id="heading-step-4-identify-the-overflowing-buffer">Step 4: Identify the overflowing buffer</h4>
<p>If a buffer is located just before the corrupted variable, inspect its usage in the code. Review all possible code paths that write to the buffer. Check if any design limitations could cause an overflow under a specific use cases.</p>
<h4 id="heading-step-5-fix-the-root-cause">Step 5: Fix the root cause</h4>
<p>If the buffer overflow happened due to missing bounds checks, add proper input validation to prevent it. Buffer design should enforce strict memory limits. The module should implement strict boundary checks for all inputs and maintain a consistent state.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739065828677/74322607-5997-4275-87d0-b3d0acf54373.png" alt="Figure 2: Steps to debug a buffer overflow" class="image--center mx-auto" width="1105" height="325" loading="lazy"></p>
<p>In addition to GDB/LLDB, you can also use techniques like hardware tracing and fault injection to simulate buffer overflows and observe system behavior in real-time.</p>
<p>While debugging helps identify and fix buffer overflows, prevention is always the best approach. Let’s explore techniques that can help avoid buffer overflows altogether.</p>
<h2 id="heading-how-to-prevent-buffer-overflows">How to Prevent Buffer Overflows</h2>
<p>You can often prevent buffer overflows through good software design, defensive programming, hardware protections, and rigorous testing. Embedded systems, unlike general-purpose computers, often lack memory protection mechanisms, which means that buffer overflow prevention critical for system reliability and security.</p>
<p>Here are some key techniques to help prevent buffer overflows:</p>
<h3 id="heading-defensive-programming">Defensive Programming</h3>
<p>Defensive programming helps minimize buffer overflow risks by ensuring all inputs are validated and unexpected conditions are handled safely.</p>
<p>First, it’s crucial to validate input size before writing to a buffer. Always check the write index by adding the size of data to be written prior to writing data to make sure more data is not written than the available buffer space.</p>
<p>Then you’ll want to make sure you have proper error handling and fail-safe mechanisms in place. If an input is invalid, halt execution, log the error, or switch to a safe state. Also, functions should indicate success/failure with helpful error codes to prevent misuse.</p>
<p>Sample Code:</p>
<pre><code class="lang-c">   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdint.h&gt;</span></span>
   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;string.h&gt;</span></span>
   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdbool.h&gt;</span></span>
   <span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>

   <span class="hljs-meta">#<span class="hljs-meta-keyword">define</span> BUFFER_SIZE 300</span>

   <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint16_t</span> sample_count = <span class="hljs-number">0</span>;
   <span class="hljs-keyword">static</span> <span class="hljs-keyword">uint8_t</span> buffer[BUFFER_SIZE] = {<span class="hljs-number">0</span>};

   <span class="hljs-keyword">typedef</span> <span class="hljs-keyword">enum</span>
   {
       SUCCESS = <span class="hljs-number">0</span>,
       NOT_ENOUGH_SPACE = <span class="hljs-number">1</span>,
       DATA_IS_INVALID = <span class="hljs-number">2</span>,
   } buffer_err_code_e;


   <span class="hljs-function">buffer_err_code_e <span class="hljs-title">updateBufferWithData</span><span class="hljs-params">(<span class="hljs-keyword">uint8_t</span> *data, <span class="hljs-keyword">uint16_t</span> size)</span>
   </span>{
       <span class="hljs-keyword">if</span> (data == <span class="hljs-literal">NULL</span> || size == <span class="hljs-number">0</span> || size &gt; BUFFER_SIZE)  
       {
           <span class="hljs-keyword">return</span> DATA_IS_INVALID; <span class="hljs-comment">// Invalid input size</span>
       }

       <span class="hljs-keyword">uint16_t</span> available_space = BUFFER_SIZE - sample_count;
       <span class="hljs-keyword">bool</span> can_write = (available_space &gt;= size) ? <span class="hljs-literal">true</span> : <span class="hljs-literal">false</span>;

       <span class="hljs-keyword">if</span> (!can_write)  
       {
           <span class="hljs-keyword">return</span> NOT_ENOUGH_SPACE;
       }

       <span class="hljs-comment">// Copy data safely</span>
       <span class="hljs-built_in">memcpy</span>(&amp;buffer[sample_count], data, size);
       sample_count += size;

       <span class="hljs-keyword">return</span> SUCCESS;
   }

   <span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span>
   </span>{   
       buffer_err_code_e ret;

       <span class="hljs-comment">// Save 1 byte to buffer</span>
       <span class="hljs-keyword">uint8_t</span> data_to_buffer = <span class="hljs-number">10</span>;
       ret = updateBufferWithData(&amp;data_to_buffer, <span class="hljs-keyword">sizeof</span>(data_to_buffer));
       <span class="hljs-keyword">if</span> (ret)  
       {
           <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Buffer update didn't succeed, Err:%d\n"</span>, ret);
       }

       <span class="hljs-comment">// Save an array of 20 bytes to buffer</span>
       <span class="hljs-keyword">uint8_t</span> data_to_buffer_1[<span class="hljs-number">20</span>] = {<span class="hljs-number">5</span>};
       ret = updateBufferWithData(data_to_buffer_1, <span class="hljs-keyword">sizeof</span>(data_to_buffer_1));
       <span class="hljs-keyword">if</span> (ret)  
       {
           <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Buffer update didn't succeed, Err:%d\n"</span>, ret);
       }

       <span class="hljs-comment">// Save an array of 50 x 8 bytes, Intentional buffer overflow</span>
       <span class="hljs-keyword">uint64_t</span> data_to_buffer_2[<span class="hljs-number">50</span>] = {<span class="hljs-number">7</span>};
       ret = updateBufferWithData((<span class="hljs-keyword">uint8_t</span>*)data_to_buffer_2, <span class="hljs-keyword">sizeof</span>(data_to_buffer_2));  
       <span class="hljs-keyword">if</span> (ret)  
       {
           <span class="hljs-built_in">printf</span>(<span class="hljs-string">"Buffer update didn't succeed, Err:%d\n"</span>, ret);
       }

       <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
   }
</code></pre>
<h3 id="heading-choosing-the-right-buffer-design-and-size">Choosing the Right Buffer Design And Size</h3>
<p>Some buffer designs handle overflow better than others. Choosing the correct buffer type and size for the application reduces the risk of corruption.</p>
<ul>
<li><p>Circular Buffers (Ring Buffers) prevent out-of-bounds writes by wrapping around. They overwrite the oldest data instead of corrupting memory. These are useful for real-time streaming data (for example, UART, sensor readings). This approach is ideal for applications where data loss is unacceptable.</p>
</li>
<li><p>Ping-Pong Buffers (Double Buffers) use two buffers. One buffer fills up with data. Then, once it’s full, it switches to the second buffer while the first one is processed. This approach is beneficial for application that have strict requirements on no data loss. The buffer design should be based on the speed of write and read tasks.</p>
</li>
</ul>
<h3 id="heading-hardware-protection">Hardware Protection</h3>
<h4 id="heading-memory-protection-unit-mpu">Memory Protection Unit (MPU)</h4>
<p>An MPU (Memory Protection Unit) helps detect unauthorized memory accesses, including buffer overflows, by restricting which regions of memory can be written to. It prevents buffer overflows from modifying critical memory regions and triggers a MemManage Fault if a process attemps to write outside an allowed region.</p>
<p>But keep in mind that, an MPU does not prevent buffer overflows – it only detects and stops execution when they occur. Not all microcontrollers have an MPU, and some low-end MCUs lack hardware protection, making software-based safeguards even more critical.</p>
<p>Modern C compilers provide several flags to identify memory errors at compile-time:</p>
<ol>
<li><p>-Wall -Wextra: Enables useful warnings</p>
</li>
<li><p>-Warray-bounds: Detects out-of-bounds array access when the array size is known at compile-time</p>
</li>
<li><p>-Wstringop-overflow: Warns about possible overflows in string functions like memcpy and strcpy.</p>
</li>
</ol>
<h3 id="heading-testing-and-validation">Testing and Validation</h3>
<p>Testing helps detect buffer overflows before deployment, reducing the risk of field failures. Unit testing each function independently with valid inputs, boundary cases, and invalid inputs helps detect buffer-related issues early. Automated testing involves feeding random and invalid inputs into the system to uncover crashes and unexpected behavior. Static Analysis Tools like Coverity, Clang Static Analyzer help detect buffer overflows before runtime. Run real-world inputs on embedded hardware to detect issues.</p>
<p>Now that we've explored how to identify, debug, and prevent buffer overflows, it’s clear that these vulnerabilities pose a significant threat to embedded systems. From silent data corruption to catastrophic system failures, the consequences can be severe.</p>
<p>But with the right debugging tools, systematic analysis, and preventive techniques, you can effectively either prevent or mitigate buffer overflows in your systems.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Buffer overflows and corruption are major challenges in embedded systems, leading to crashes, unpredictable behavior, and security risks. Debugging these issues is difficult because their symptoms vary based on system state, requiring systematic analysis using memory map files, GDB/LLDB, and structured debugging approaches.</p>
<p>In this article, we explored:</p>
<ul>
<li><p>The causes and consequences of buffer overflows and corruptions</p>
</li>
<li><p>How to debug buffer overflows using memory analysis and debugging tools</p>
</li>
<li><p>Best practices for prevention</p>
</li>
</ul>
<p>Buffer overflow prevention requires a multi-layered approach:</p>
<ol>
<li><p>Follow a structured software design process to identify risks early.</p>
</li>
<li><p>Apply defensive programming principles to validate inputs and handle errors gracefully.</p>
</li>
<li><p>Use hardware-based protections like MPUs where available.</p>
</li>
<li><p>Enable compiler flags that help identify memory errors.</p>
</li>
<li><p>Test extensively, unit testing, automated testing, and code reviews help catch vulnerabilities early.</p>
</li>
</ol>
<p>By implementing these best practices, you can minimize the risk of buffer overflows in embedded systems, improving reliability and security.</p>
<p>In embedded systems, where reliability and safety are critical, preventing buffer overflows is not just a best practice, it is a necessity. A single buffer overflow can compromise an entire system. Defensive programming, rigorous testing, and hardware protections are essential for building secure and robust embedded applications.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Get a Memory Map of Your System using BIOS Interrupts ]]>
                </title>
                <description>
                    <![CDATA[ When you are developing a kernel, one of the most important things is memory. The kernel must know how much memory is available and where it's located to avoid overwriting crucial system resources. But not all memory is freely available for use. Some... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-get-a-memory-map-of-your-system-using-bios-interrupts/</link>
                <guid isPermaLink="false">66f177c1aa7c0509267cf26e</guid>
                
                    <category>
                        <![CDATA[ Kernel ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory-management ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Linux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikolaos Panagopoulos ]]>
                </dc:creator>
                <pubDate>Mon, 23 Sep 2024 14:14:25 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/iar-afB0QQw/upload/7b7f724f7260216b7427408112d5f8c4.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you are developing a kernel, one of the most important things is memory. The kernel must know how much memory is available and where it's located to avoid overwriting crucial system resources.</p>
<p>But not all memory is freely available for use. Some memory sections are reserved for system functions and others may be occupied by hardware devices. That’s why it is very important to get the system’s memory map.</p>
<h3 id="heading-what-is-a-memory-map">What is a Memory Map?</h3>
<p>But what is a memory map? A memory map is a representation (think about it like a table) that shows how physical memory is organized in your system. It shows the address of each memory region, it’s length and it’s type.</p>
<p>Type 1 means that the region is available for you to use freely and type 2 means that it is reserved by your system. Type 3 means that the region is reserved for the Advanced configuration and power interface (ACPI 3.x). While a type 3 region might not be used by the system, it can be reclaimed later.</p>
<p>Using a memory map will allow you to manage memory resources successfully without any issues such as crashes or system instability.</p>
<p>There are some ways you can detect your system’s available memory. One is by using the BIOS and interrupt 15h. Another one is by doing memory probing.</p>
<p>In this article you will learn which tools are available to help you get a memory map of your system, which ones you should use, and which ones you should avoid and why. Then finally, you will see some assembly code that you can use in your own bootloader / kernel.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>if you want to follow along with the code shown in this article, you’ll need:</p>
<ul>
<li><p>A Linux operating system</p>
</li>
<li><p>Some knowledge of assembly language</p>
</li>
<li><p>A text editor of your choice</p>
</li>
<li><p>An emulator installed. For this example I use QEMU.</p>
</li>
<li><p>FASM assembler installed</p>
</li>
<li><p>Git to be able to clone the repository (<a target="_blank" href="https://github.com/nikolaospanagopoulos/memoryMapBoot">https://github.com/nikolaospanagopoulos/memoryMapBoot</a>)</p>
</li>
</ul>
<h3 id="heading-a-few-words-about-bios-int-15h">A Few Words about BIOS int 15h</h3>
<p>In Real mode, the BIOS offers many interrupts that interact with the hardware and can give you information.</p>
<p>There are some interrupts that can help with getting a memory map, but the most powerful one is int15h with E820h function (hexadecimal numbers! very important to remember. Decimal numbers will not work). This method offers a detailed memory map that you can use to safely determine which areas of memory can be used for vital tasks like setting up paging, memory allocation, and more.</p>
<p>In this article you will see how you can use this interrupt to get a detailed memory map of your system.</p>
<p>Now, before we go deeper, I would like to add a few things about memory probing and why you should avoid it.</p>
<h3 id="heading-memory-probing-and-why-you-should-avoid-it">Memory probing and why you should avoid it</h3>
<p>Memory probing is the process of manually accessing physical memory and determining whether it is available or not. The issue is that not all memory is designed to be accessed directly.</p>
<p>Accessing parts of memory that you shouldn’t can cause unpredictable behavior like:</p>
<ul>
<li><p><strong>System Crashes:</strong> some memory is reserved for BIOS structures, hardware devices etc. Accessing those areas can lead to system crashes or system instability.</p>
</li>
<li><p><strong>Memory Corruption:</strong> accessing reserved memory areas can lead to corruption of those areas. This can cause again crashes, instability, malfunctions etc</p>
</li>
</ul>
<p>So, you should avoid memory probing because it’s an unnecessary risk to your kernel development process.</p>
<h2 id="heading-the-code">The Code</h2>
<h3 id="heading-step-1-prepare-to-call-int-15h">Step 1: Prepare to Call int 15h</h3>
<p>In this part, you will basically setup the environment needed to invoke int 15h. The general purpose registers need to be stored so that no important data on them is lost during the interrupt invocation. Then the registers <code>bp</code>, <code>ebx</code> are cleared so that they can be set to their initial values.</p>
<p>The “SMAP” value is stored in the <code>edx</code> register to ensure the correct format that the BIOS will return. Finally, we setup the <code>0xe820</code> function and request memory map data.</p>
<pre><code class="lang-plaintext">pusha
mov di, 0x0504        ; Set DI register for memory storage
xor ebx, ebx          ; EBX must be 0
xor bp, bp            ; BP must be 0 (to keep an entry count)
mov edx, 0x534D4150   ; Place "SMAP" into edx | The "SMAP" signature ensures that the BIOS provides the correct memory map format
mov eax, 0xe820       ; Function 0xE820 to get memory map
mov dword [es:di + 20], 1 ; force a valid ACPI 3.X entry | allows us to get additional information (extended attributes)
mov ecx, 24           ; Request 24 bytes of data
</code></pre>
<ul>
<li><p>The <code>pusha</code> command pushed all general purpose registers to the stack to save their values during the interrupt call. They can be restored after the interrupt call to avoid corruption of other areas.</p>
</li>
<li><p>The <code>mov di, 0x0504</code> instruction sets the di register to 0×0504 (where the memory map entries will be stored).</p>
</li>
<li><p><code>xor ebx, ebx</code> the xor instruction uses the xor operator to clear the ebx register. It must be set to 0 to start retrieving entries.</p>
</li>
<li><p><code>xor bp, bp</code> use of the same xor operator here to set bp to 0. This will keep track of your memory entries.</p>
</li>
<li><p><code>mov edx, 0x534D4150</code> this instruction will store <code>0x534D4150</code> (ASCII string “SMAP”) into the edx register. It makes certain that the BIOS will return the correct format for your memory map.</p>
</li>
<li><p><code>mov eax, 0xe820</code> this instruction sets the function 0xe280 which will get the memory map along with int15h.</p>
</li>
<li><p><code>mov dword [es:di + 20], 1</code> this instruction forces a valid ACPI (Advanced Configuration and Power Interface) 3.x entry. This way the BIOS provides extra information in the form of extra attributes.</p>
</li>
<li><p><code>mov ecx, 24</code> this instruction asks the BIOS for 24 bytes of memory data. This is the size that ACPI 3.x entries need to include extra information.</p>
</li>
</ul>
<h3 id="heading-step-2-call-int15h">Step 2: Call int15h</h3>
<p>Here, you can finally invoke the interrupt to fetch the memory map. You need to check that the function is supported by the BIOS and that valid data is being fetched. You also need to ensure that the correct format is being fetched by setting again the “SMAP” into the <code>edx</code> register.</p>
<pre><code class="lang-plaintext">    int 0x15                 ; using interrupt
    jc short .failed         ; carry set on first call means "unsupported function"
    mov edx, 0x534D4150      ; Some BIOSes apparently trash this register? lets set it again
    cmp eax, edx             ; on success, eax must have been reset to "SMAP"
    jne short .failed
    test ebx, ebx            ; ebx = 0 implies list is only 1 entry long (worthless)
    je short .failed
</code></pre>
<ul>
<li><p><code>int 0x15</code> this instruction invokes the interrupt 0×15.</p>
</li>
<li><p><code>jc short .failed</code> is the carry flag that is set. It means the function is unsupported and the call has failed. It jumps to our error handler.</p>
</li>
<li><p><code>mov edx, 0x534D4150</code> set again the “SMAP” because some BIOSes corrupt this register after the call.</p>
</li>
<li><p><code>cmp eax, edx</code> if the call is successfull, on success the BIOS will return the “SMAP” value in eax.</p>
</li>
<li><p><code>jne short .failed</code> if it doesn’t, it means the call has failed and it jumps to our error handling label.</p>
</li>
<li><p><code>test ebx, ebx</code> this instruction checks if ebx is 0 after the first call. This means that the memory map only contains one entry. This entry is probably invalid, so it jumps to the error handling label.</p>
</li>
</ul>
<h3 id="heading-step-3-loop-through-memory-entries">Step 3: Loop Through Memory Entries</h3>
<p>After a successful first invocation, you need to loop through each entry of the memory map.</p>
<p>In the loop, you will invoke again int 15h to get all subsequent memory entries while checking each entry’s length and other attributes. If it meets the criteria, you increment the counter and you store the entry. This continues until there are no entries left to process.</p>
<pre><code class="lang-plaintext">    jmp short .jmpin
.e820lp:
    mov eax, 0xe820          ; eax, ecx get trashed on every int 0x15 call
    mov dword [es:di + 20], 1 ; force a valid ACPI 3.X entry
    mov ecx, 24              ; ask for 24 bytes again
    int 0x15
    jc short .e820f          ; carry set means "end of list already reached"
    mov edx, 0x534D4150      ; repair potentially trashed register
.jmpin:
    jcxz .skipent            ; skip any 0 length entries (If ecx is zero, skip this entry (indicates an invalid entry length))
    cmp cl, 20               ; got a 24 byte ACPI 3.X response?
    jbe short .notext
    test byte [es:di + 20], 1 ;if bit 0 is clear, the entry should be ignored
    je short .skipent         ; jump if bit 0 is clear 
.notext:
    mov eax, [es:di + 8]     ; get lower uint32_t of memory region length
    or eax, [es:di + 12]     ; "or" it with upper uint32_t to test for zero and form 64 bits (little endian)
    jz .skipent              ; if length uint64_t is 0, skip entry
    inc bp                   ; got a good entry: ++count, move to next storage spot
    add di, 24               ; move next entry into buffer
.skipent:
    test ebx, ebx            ; if ebx resets to 0, list is complete
    jne short .e820lp
</code></pre>
<ul>
<li><code>.e820lp</code> is a label for looping through each memory map entry.</li>
</ul>
<p>The next lines are used to call int15h to get the next memory entry:</p>
<ul>
<li><p><code>jc short .e820f</code> if the carry flag is set, it means that we have reached the end of the list.</p>
</li>
<li><p><code>jcxz .skipent</code> if ecx register is 0, it means the length of the memory entry is invalid. So the code skips it.</p>
</li>
<li><p><code>cmp cl, 20</code> checks if the memory entry is a valid ACPI 3.x entry. (It would be 24 bytes long). If it is not, the code jumps to <code>.notext</code>.</p>
</li>
<li><p><code>test byte [es:di + 20], 1</code> checks if bit 0 is set in the memory entry's extended attributes, indicating a valid entry. If it's clear, the entry is skipped.</p>
</li>
<li><p><code>mov eax, [es:di + 8]</code> gets the lower 32 bits of the memory region length and then we combine it using the or operator, with the upper 32 bits. If the total length is 0, then the entry is skipped.</p>
</li>
<li><p><code>inc bp</code> increments entry count.</p>
</li>
<li><p><code>add di, 24</code> moves the pointer di forward to the next memory entry. Each entry is 24 bytes long.</p>
</li>
</ul>
<h3 id="heading-step-4-end-of-memory-entries-handling">Step 4: End of Memory Entries Handling</h3>
<p>Finally, you can store the entry count. And by using the <code>popa</code> instruction, you will restore all general purpose registers to their previous values. If an error occurs during the process, the code jumps to <code>.failed</code> label which is our error handling function.</p>
<pre><code class="lang-plaintext">.e820f:
    mov [mmap_ent], bp       ; store the entry count
    clc                      ; there is "jc" on end of list to this point, so the carry must be cleared

    popa
    ret
.failed:
    stc                      ; "function unsupported" error exit
    ret
</code></pre>
<ul>
<li><p><code>mov [mmap_ent], bp</code> stores the entry count.</p>
</li>
<li><p><code>clc</code> clears the carry flag because it is already set.</p>
</li>
<li><p><code>popa</code> pops all general purpose registers back from the stack.</p>
</li>
<li><p><code>.failed</code> we use this label for error handling.</p>
</li>
</ul>
<p>Here is a video from my YouTube account where I implement and explain the above code:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/WW3pduHMWkc" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h3 id="heading-epilogue">Epilogue</h3>
<p>In kernel development, one of the most important tasks is managing memory. The above is a reliable way to detect your system’s memory layout information. This means that you can make safe decisions when allocating resources, implementing paging, and so on.</p>
<p>It might appear to be complex and it maybe is, but if you follow the code line by line you will be able to understand it. These techniques will allow you to build a robust kernel capable of running on different hardware configurations.</p>
<p>Keep Coding!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
