Wednesday, August 19, 2026

Understanding the ESP8266 Linker Script


 

Understanding the ESP8266 Linker Script

A Field Guide to Flash Layout, Memory Regions, and the LittleFS / SPIFFS Partition

 

1. Introduction

The ESP8266 Arduino core ships with a linker script that defines where firmware code, RAM variables, and the flash-resident filesystem are located within a 1 MB (1 MiB) flash chip. This guide walks through that script section by section, explains the two address spaces involved, and then covers how the flash layout can be safely customized to reclaim space for a larger application or a bigger filesystem. From here on, sizes are given in MiB/KiB (powers of two) rather than MB/KB, to match the actual byte counts involved.

Two Address Spaces

There are two different address spaces at play in the linker script:

        0x402xxxxx — the ESP8266 SPI flash, memory-mapped into the CPU address space

        0x3FFE... / 0x3FF... — the ESP8266 RAM and peripheral address space

Keeping these two ranges straight is the key to reading the rest of the script correctly — a flash address and a RAM address can look superficially similar but refer to completely different hardware.

Which Layout Is This?

The official ESP8266 Arduino core ships many different 1 MiB linker script variants — eagle.flash.1m64.ld, 1m128.ld, 1m256.ld, 1m512.ld, a no-filesystem 1m.ld, and others — each splitting the same 1 MiB chip differently between sketch and filesystem. None of the current official variants use exactly the 600 KiB sketch / 400 KiB SPIFFS split shown throughout this guide. For example, eagle.flash.1m256.ld allocates roughly 743 KiB to the sketch and 256 KiB to the filesystem (with _FS_start at 0x402BB000), while eagle.flash.1m512.ld allocates roughly 487 KiB to the sketch and 512 KiB to the filesystem (with _FS_start at 0x4027B000). The irom0_0_seg length also varies to match — official scripts use lengths like 0xb9ff0 or 0x79ff0 rather than an exact 0x96000.

The 600/400 split used here is a valid, self-consistent conceptual layout (and matches some older or custom scripts), which makes it a clean teaching example — but it should be treated as illustrative, not as “the” current default. Before touching a real project's flash layout, check the specific .ld file your board/core version actually selects, typically under the core's tools/sdk/ld/ directory, for the exact numbers in use.

2. Flash Layout Comments

The script begins with a set of comments describing the 1 MiB flash partition:

/* Flash Split for 1M chips */

/* sketch @0x40200000 (~600KB) (614400B) */

/* empty  @0x40296000 (~4KB) (4096B) */

/* spiffs @0x40297000 (~400KB) (409600B) */

/* eeprom @0x402FB000 (4KB) */

/* rfcal  @0x402FC000 (4KB) */

/* wifi   @0x402FD000 (12KB) */

 

Translated into physical flash offsets, the partition looks like this:

 

Region

Flash Address

Size

Sketch

0x000000 → 0x096000

600 KiB

Empty

0x096000 → 0x097000

4 KiB

SPIFFS

0x097000 → 0x0FB000

400 KiB

EEPROM emulation

0x0FB000 → 0x0FC000

4 KiB

RF calibration

0x0FC000 → 0x0FD000

4 KiB

System / SDK params

0x0FD000 → 0x100000

12 KiB

 

The addresses written as 0x402xxxxx are memory-mapped CPU addresses, not raw SPI-flash offsets. For example, 0x40297000 corresponds to 0x97000 in physical flash, because:

0x40297000 - 0x40200000 = 0x97000

0x97000 = 618,496 bytes = 604 KiB

 

0x97000 (618,496 bytes, 604 KiB) is not itself the sketch/SPIFFS boundary — it's 4 KiB further into flash than that boundary. The sketch occupies the first 600 KiB, ending at 0x96000 (614,400 bytes); the empty 4 KiB gap then runs from 0x96000 to 0x97000; and SPIFFS begins at 0x97000, immediately after that gap. So SPIFFS starts 4 KiB after the 600 KiB sketch boundary, not exactly at it.

3. The MEMORY Block

The script opens its region definitions with:

MEMORY

{

 

This tells the linker: “these are the memory areas available to my program.” Three regions are defined inside it.

3.1  dport0_0_seg

dport0_0_seg :

    org = 0x3FF00000,

    len = 0x10

 

A tiny 16-byte (0x10) region starting at 0x3FF00000, associated with the ESP8266 DPORT / peripheral area. It is not where ordinary Arduino variables live.

3.2  dram0_0_seg

dram0_0_seg :

    org = 0x3FFE8000,

    len = 0x14000

 

This is the data-RAM region available to the linker: starting at 0x3FFE8000, with a length of 0x14000 (81,920 bytes, roughly 80 KiB). Ordinary variables such as counters, buffers, and floats live here — but the ESP8266 runtime also uses part of this space, so the full 80 KiB is not all available to application code.

3.3  irom0_0_seg

irom0_0_seg :

    org = 0x40201010,

    len = 0x96000

 

IROM means instruction/data stored in flash and accessed through the instruction ROM mapping — this is where most compiled program code lives, including functions such as setup() and loop(). The ESP8266 executes this code directly from memory-mapped flash. The region starts at 0x40201010 and has a length of 0x96000 (614,400 bytes) — exactly the 600 KiB sketch area.

Why 0x40201010 and not 0x40200000?

0x40200000 is the nominal start of the memory-mapped flash image referenced in the partition comments, but it should not be read as simply “the first byte of application code after a 4 KiB bootloader.” 0x40201010 is the IROM region origin used specifically by the ESP8266 Arduino linker configuration, and it reflects the interaction of several pieces — the eboot bootloader/loader, the ESP8266 image header, and the linker's own section layout — rather than a single simple offset. The Arduino ESP8266 project's own discussion of this value treats it as a linker/build-chain detail, not a general-purpose ESP8266 fact. In practice: treat 0x40201010 as the value this specific linker configuration expects, and don't assume it generalizes to every ESP8266 image or bootloader combination without checking the corresponding core version.

 

Not everything that runs actually lives in irom0_0_seg, though. Code marked with ICACHE_RAM_ATTR — interrupt handlers, flash-cache-sensitive routines, and anything that must keep running while the flash cache is temporarily unavailable — is deliberately placed in IRAM (Instruction RAM), mapped around 0x4010xxxx, rather than in IROM. That's a separate, much smaller region carved out of the ~80 KiB dram0_0_seg budget from Section 3.2, which is why liberal use of ICACHE_RAM_ATTR shrinks available RAM rather than flash: the function's code is copied into RAM at boot instead of being executed directly from the memory-mapped flash.

 

}

This closing brace ends the MEMORY block.

4. Filesystem Symbols

After MEMORY, the script defines a set of linker symbols that tell the filesystem code where its partition lives.

4.1  _FS_start and _FS_end

PROVIDE ( _FS_start = 0x40297000 );

PROVIDE ( _FS_end   = 0x402FB000 );

 

These mark where the filesystem begins and ends. Subtracting the two gives the filesystem size:

0x402FB000 - 0x40297000

= 0x64000

= 409,600 bytes

= 400 KiB

4.2  _FS_page and _FS_block

PROVIDE ( _FS_page  = 0x100 );

PROVIDE ( _FS_block = 0x1000 );

 

These two symbols are easy to confuse but have very different jobs — neither one is a starting address.

 

Symbol

Value

Meaning

_FS_start

0x40297000

Where the filesystem begins

_FS_end

0x402FB000

Where the filesystem ends

_FS_page

0x100 (256 B)

Filesystem logical page size

_FS_block

0x1000 (4096 B)

Filesystem block size used by this configuration — matches the flash's 4 KiB erase sector for this particular layout, but is not a fixed ESP8266 constant

 

ESP8266 SPI flash can be written a byte or a page at a time, but it can only be erased in 4 KiB sectors — that is a fixed hardware property. _FS_block, by contrast, is a filesystem configuration parameter, not a universal ESP8266 constant: it tells the filesystem implementation what block size to use for its own bookkeeping, and its valid range and relationship to the hardware erase-sector size depend on the specific filesystem implementation (LittleFS vs. SPIFFS) and core version in use — that relationship should be verified against the filesystem code being targeted, not assumed. In this particular 1 MiB layout, _FS_block is set to 0x1000 (4096 B), which happens to match the flash's 4 KiB erase sector exactly — a sensible and common choice, but not the only valid one. Larger layouts sometimes use a different value; for example, some published 4 MiB ESP8266 linker layouts set _FS_block to 0x2000 (8 KiB) instead.

With this layout's 400 KiB filesystem and 4 KiB block size, the partition works out to exactly 100 blocks:

409,600 ÷ 4,096 = 100 filesystem blocks

4.3  EEPROM Start

PROVIDE ( _EEPROM_start = 0x402fb000 );

 

This tells the Arduino EEPROM emulation layer where its 4 KiB sector begins — immediately after the SPIFFS/LittleFS partition ends, so EEPROM gets the next 4 KiB sector in sequence.

5. Deprecated SPIFFS Symbols

The script also defines an older, SPIFFS-specific set of symbol names, kept for backward compatibility with code written before the core adopted the generic _FS_* naming:

PROVIDE ( _SPIFFS_start = 0x40297000 );

PROVIDE ( _SPIFFS_end   = 0x402FB000 );

PROVIDE ( _SPIFFS_page  = 0x100 );

PROVIDE ( _SPIFFS_block = 0x1000 );

 

Each of these mirrors its _FS_* counterpart exactly, so older code that still references _SPIFFS_start, for example, resolves to the same 0x40297000 address as _FS_start.

6. Pulling in the Common Linker Script

INCLUDE "local.eagle.app.v6.common.ld"

 

Rather than defining every ESP8266 linker section itself, this file includes another script that supplies the common rules — sections such as .text, .data, .rodata, .bss, IRAM, IROM, exception handling, constructors, and initialization data. In short: this file defines the partition-specific memory layout, while the included file supplies the actual linking machinery.

7. The Whole Layout, Visually

Putting it all together, the 1 MiB flash chip breaks down as follows:

ESP8266 1 MiB FLASH (physical offsets)

0x000000  +--------------------------+

          |      Arduino Sketch      |

          |         ~600 KiB         |

0x096000  +--------------------------+

          |        Empty  4 KiB      |

0x097000  +--------------------------+

          |         SPIFFS           |

          |         400 KiB          |

0x0FB000  +--------------------------+

          |       EEPROM  4 KiB      |

0x0FC000  +--------------------------+

          |     RF Calibration 4 KiB |

0x0FD000  +--------------------------+

          |   System / SDK params 12KB|

0x100000  +--------------------------+

 

And as the CPU sees it, mapped into its address space:

0x40200000  <- flash mapping begins

     |-- Sketch

0x40296000

     |-- Empty

0x40297000  <- _FS_start

     |-- SPIFFS / LittleFS  (400 KiB)

0x402FB000  <- _FS_end / _EEPROM_start

     |-- EEPROM

0x402FC000

     |-- RF calibration

0x402FD000

     |-- System / SDK params

0x40300000

Why This Matters for Filesystem Tooling

When a filesystem uploader (such as an mklittlefs tool) builds a filesystem image, it needs to know the start (0x97000) and size (0x64000) of the target region so it can create a matching 400 KiB image and write it to the correct flash offset. The firmware side then relies on _FS_start (0x40297000) and _FS_end (0x402FB000) so that calls such as LittleFS.begin() and LittleFS.open() know exactly which part of external SPI flash to use. In short, this linker script is the contract between the ESP8266 firmware, the Arduino core, the bootloader/flash layout, and the filesystem — and if you're modifying a custom Arduino core or filesystem tool, these addresses are exactly the values that must stay consistent.

8. Reclaiming Flash Space: What Can Actually Be Changed

A custom ESP8266 flash layout is possible, and in a typical 1 MiB layout there is genuinely some space to reclaim — but not all of it, and some regions should be left alone.

8.1  The Empty 4 KiB Gap — Reclaimable, With a Caveat

The “empty” 4 KiB area between the sketch and SPIFFS is only a comment in this script; it is not an actual linker-defined region in the MEMORY block, and nothing currently claims it. That makes it reclaimable: _FS_start can be moved back from 0x40297000 to 0x40296000, gaining 4 KiB for the filesystem, as shown below.

It's worth being precise about why this gap exists in the broader Arduino ESP8266 picture, though. The general ESP8266 Arduino flash architecture does reserve a separate area for an Over-The-Air (OTA) update image, and the documented partition order is sketch → OTA → filesystem → EEPROM → WiFi/SDK configuration. But this specific 4 KiB comment in this specific 1 MiB layout should not be read as “the OTA image” itself — a real OTA slot needs to be roughly as large as the sketch, far bigger than 4 KiB. Rather, this gap is simply unallocated padding within a layout that, as written, does not carve out full OTA space at all. Reclaiming it is safe precisely because nothing here currently depends on it, not because it was doing OTA's job and is now being freed from that job.

SKETCH       600 KiB

SPIFFS       404 KiB   (was 400 KiB)

EEPROM         4 KiB

RF_CAL         4 KiB

SYSTEM        12 KiB

----------------------

TOTAL       1024 KiB

 

The math checks out and lands on a clean 4 KiB sector boundary:

404 KiB = 413,696 bytes = 0x65000

0x40296000 + 0x65000 = 0x402FB000

8.2  The 12 KiB System / SDK Parameter Area — Do Not Remove

This region is better described as system/SDK parameter space than “WiFi code.” The official SDK partition definitions identify RF_CAL, PHY_DATA, and SYSTEM_PARAMETER as distinct system partitions here, not application or networking code. The core computes:

rf_cal            = flash_size - 0x4000;

system_parameter  = flash_size - 0x3000;

 

and registers PHY_DATA, RF_CAL, and SYSTEM_PARAMETER (3 × 4 KiB = 12 KiB) at the end of flash based on the physical chip size. Even an application that never calls the WiFi API still runs on an SDK/core that assumes these sectors exist — “WiFi disabled” does not mean “those sectors are unused.” They should be left in place.

8.3  EEPROM — Negotiable, But Not Casually

If a project never calls EEPROM.begin(), EEPROM.read(), EEPROM.write(), or EEPROM.commit(), the dedicated EEPROM sector is more negotiable than RF_CAL or the system parameters. However, this is the ESP8266 Arduino core's own SDK-3-specific behavior, not a general property of the raw Espressif SDK: on cores built against SDK 3.0 and later, the Arduino core explicitly computes phy_data as an offset from _EEPROM_start and overlays PHY_DATA there as part of its own initialization — so the sector participates in that core-specific mechanism whether or not the sketch uses Arduino's EEPROM emulation. Do not simply delete or repurpose the EEPROM sector on an SDK 3.x-based core without accounting for this; verify against the specific core version in use before changing it.

8.4  OTA — A Separate Mechanism, Not This 4 KiB Gap

It bears repeating: a genuine OTA-capable layout reserves an update slot roughly the size of the sketch itself, not 4 KiB. The ESP8266 Arduino core ships distinct flash-map configurations for OTA-enabled, maximum-filesystem, and no-filesystem builds precisely because these are different partitioning strategies, not variations achieved by nudging a single symbol. A project that doesn't need OTA has more freedom to grow the sketch or filesystem into that space — but doing so means deliberately choosing a non-OTA flash map, not merely reclaiming a leftover 4 KiB comment.

9. What Is Actually Mandatory?

Region

Can Change?

Recommendation

Bootloader (first 4 KiB)

No

Keep

Sketch

Yes

Resize as needed

OTA slot

Depends on flash map

Reclaimable only when using a non-OTA layout

LittleFS / SPIFFS

Yes

Can be resized, provided the filesystem image builder, linker symbols, flash layout, and uploader all agree on the same start address and size

EEPROM

Yes, if unused

Can eliminate with a custom core/config

RF_CAL

No

Keep

PHY / system data

No

Keep

Final system parameter sectors

No

Keep

 

A note on “first 4 KiB”: the eboot bootloader itself does fit within that space, but the physical .bin file that esptool.py writes at offset 0x000000 also includes the ESP8266 image header alongside it. Treating the whole first 4 KiB sector as off-limits is the safe, conservative rule to follow — it's simpler than tracking the exact boundary between bootloader and header, and it guarantees esptool.py won't be pointed at a write that clobbers either one.

10. A Conceptual Custom 1 MiB Partition Map

Read This Before Using Any Layout Below

What follows is a conceptual flash partition map, not a drop-in linker script. The irom0_0_seg origin, the eboot bootloader, the generated firmware image header, and esptool's flashing behavior are all interdependent — shifting where the sketch region physically begins is not simply a matter of editing one linker symbol. The ESP8266 Arduino core ships separate, tested flash-map configurations for OTA-enabled, maximum-filesystem, and no-filesystem builds precisely because these combinations need to be validated as a whole. Treat the addresses below as a planning aid, and validate any change by building, flashing, and booting successfully — not by arithmetic alone.

With that caveat in place, here is the conceptual partitioning for a device that uses neither OTA nor EEPROM emulation, expressed as flash regions rather than as linker syntax:

 

Region

Approx. Range

Notes

Bootloader / image header

start of flash

Reserved by eboot / the ESP8266 image format — not user-configurable

Sketch (IROM)

≈600 KiB

Origin fixed by irom0_0_seg in the linker script actually used to build the image

LittleFS

≈400–404 KiB

Size/location must match the filesystem image builder and uploader exactly

EEPROM

4 KiB

Also used by PHY_DATA on SDK 3.x — see §8.3

RF_CAL

4 KiB

Do not relocate or remove

System parameters

12 KiB

Do not relocate or remove

 

The one change from this guide that is a straightforward, symbol-level edit — rather than a structural change to where the sketch begins — is reclaiming the 4 KiB empty gap described in §8.1, by moving _FS_start back by one sector:

PROVIDE ( _FS_start = 0x40296000 );

PROVIDE ( _FS_end   = 0x402FB000 );

PROVIDE ( _FS_page  = 0x100 );

PROVIDE ( _FS_block = 0x1000 );

 

PROVIDE ( _EEPROM_start = 0x402FB000 );

 

INCLUDE "local.eagle.app.v6.common.ld"

 

This yields a 404 KiB filesystem instead of 400 KiB, touches no system sectors, and does not move the sketch's own origin — so it carries much less risk than reshaping the sketch/bootloader boundary.

The Sketch ↔ Filesystem Trade-off

Because the flash total is fixed, the sketch and filesystem sizes trade off directly against each other, subject to the reserved system areas at the end of the chip. The figures below are illustrative targets, not verified linker configurations — reaching them requires the sketch's own linker region, the filesystem image builder, and the uploader to all agree on the same boundary:

 

Layout

Sketch

LittleFS

Reserved (EEPROM+RF_CAL+SYSTEM)

Balanced

600 KiB

404 KiB

20 KiB

Filesystem-heavy

500 KiB

504 KiB

20 KiB

Application-heavy

700 KiB

304 KiB

20 KiB

Warning: Reclaiming the Final 12 KiB Is a Deeper Change

Fully reclaiming the final 12 KiB of system/SDK parameter space is a deeper change. It requires coordinated changes across the linker script, the Arduino core, SDK partition registration, the flash-size information, the esptool image header, the filesystem uploader, and the boot process — because the core calculates RF/system sector locations from the physical flash size, not simply from the .ld file. Editing only the linker script is not sufficient to safely reclaim this space.

 

11. Summary

        0x402xxxxx addresses are memory-mapped flash; 0x3FFxxxxx addresses are RAM/peripherals.

        The MEMORY block defines three regions: a tiny DPORT segment, ~80 KiB of data RAM, and ~600 KiB of IROM flash for application code.

        _FS_page and _FS_block are filesystem configuration values (page size and block size), not addresses; _FS_block matches the 4 KiB hardware erase sector in this layout, but is not a fixed ESP8266-wide constant.

        The 4 KiB “empty” gap before the filesystem is unclaimed padding in this specific layout, safe to reclaim on its own terms — it is not itself an OTA image, even though OTA is the general reason ESP8266 layouts reserve extra space between sketch and filesystem.

        EEPROM's 4 KiB sector is more negotiable than RF_CAL or the system parameters, but on SDK 3.x it also carries PHY_DATA, so it should not be repurposed without checking the core version.

        A linker script's addresses, the physical flash offsets, the generated firmware image, and the Arduino flash-partition configuration are related but not interchangeable — any layout change should be validated through a full build/upload/boot cycle, not just checked arithmetically.

No comments:

Post a Comment