Why embedded systems can't compile their own code

4 min

When I first started learning embedded systems development, coming from a mobile background, I kept coming across the term ‘cross compilation’. Understanding this concept felt important, not only because it’s mentioned in the first chapter of nearly ever book/course about embedded development, but also because it seemed to underpin almost everything I was doing when writing software for microcontrollers. What surprised me was realising that mobile development had been shielding me from this complexity for years.

Your Computer and Your Target Are Very Different

Think about your laptop or desktop computer. It runs on a powerful processor, likely an Intel or AMD chip using the x86-64 architecture, or if you’re on a newer Mac, an ARM-based M-series chip. It has gigabytes of RAM, an operating system many generations deep, and plenty of storage. Now think about an Arduino Uno or a Raspberry Pi Pico. These devices run on completely different processors with different architectures, far less memory, and often no operating system at all.

When you write code and compile it on your laptop, the compiler produces machine code, the actual binary instructions that a processor can execute. But machine code isn’t universal. The binary instructions that an Intel processor understands are meaningless to an ARM Cortex-M0 microcontroller. They speak different languages at the hardware level.

Cross Compilation
Cross Compilation

The Solution

Cross compilation is the process of building executable code on one type of machine (the host) for a different type of machine (the target). Your laptop acts as the host, providing all the computational power and development tools you need. The embedded device is the target, the machine that will eventually run your code.

When you install an embedded development toolchain, you’re getting a special compiler designed for exactly this purpose. Instead of producing machine code for your laptop’s processor, it produces machine code for your target’s processor. The GNU ARM Embedded Toolchain, for instance, runs on your development machine but outputs binaries that ARM microcontrollers understand.

What Mobile Development Hides From You

Coming from mobile, I initially assumed I’d been doing something similar all along. But the reality is more nuanced: mobile development abstracts away the cross compilation problem rather than exposing you to it directly.

With Android development, when you write Java or Kotlin code, you’re not compiling to native machine code at all. The build process produces DEX bytecode, which is architecture-independent. This bytecode runs on the Android Runtime, which handles the translation to native instructions on the device itself. The architecture mismatch between your development machine and the target phone simply doesn’t matter because you’re shipping portable bytecode rather than native binaries.

iOS development on modern Macs sidesteps the issue differently. Apple’s M-series chips are ARM-based, just like iPhone processors. When you build an iOS app on an M1 or M2 Mac, the host and target share the same underlying architecture. The toolchain still does important work adapting for the different operating systems, but you’re not dealing with a fundamental architecture mismatch.

Embedded development offers no such abstractions. When I write C for an STM32 microcontroller, the compiler produces raw ARM machine code. There’s no runtime to translate bytecode, no shared architecture to lean on.

Why Can’t We Compile Directly on the Target?

You might wonder why we don’t just compile code directly on the embedded device. For some platforms like the Raspberry Pi, which runs a full Linux operating system, you actually can do this, and it’s called native compilation. However, for most embedded targets, this isn’t practical.

Microcontrollers simply don’t have the resources. A compiler is a complex piece of software that requires significant memory and processing power. An ATmega328P with 2KB of RAM and 32KB of flash couldn’t possibly host a compiler. Even when resources aren’t the limiting factor, development is far more comfortable on your main computer with your favourite editor, debugging tools, and version control.

The Toolchain

When people talk about cross compilation, they’re usually referring to an entire toolchain rather than just the compiler itself. A typical embedded toolchain includes the compiler that translates your C, C++, Rust code into assembly, the assembler that converts assembly into machine code, the linker that combines various object files and libraries into a final executable, and various utilities for inspecting and manipulating binaries.

Popular toolchains include GCC-based options like the ARM GNU Toolchain and vendor-specific tools like those provided in development environments such as STM32CubeIDE or the Arduino IDE. Each toolchain is configured to understand the specific capabilities and constraints of its target architecture.

The key insight for me as a newcomer was recognising that my development machine and my target device are completely separate execution environments. I’m using one computer as a tool to prepare software for another, much simpler computer.

As always, thanks for joining me on this journey into embedded development!