Microcontroller Programming

7 min read Last updated Fri Jun 12 2026 01:43:02 GMT+0000 (Coordinated Universal Time)

To program a microcontroller, a basic development setup is required, which includes:

  • Microcontroller development board
  • Computer with an IDE
  • Programming cable or adapter
  • Power supply
  • Input/output devices

Programs are typically written in high-level languages such as C or C++, compiled into machine code, and uploaded to the microcontroller.

Programming Languages

C and C++

C dominated early MCU firmware and is still used for low-level driver code. For application-layer work, C++ with the Wiring/Arduino framework provides equivalent performance with cleaner abstractions. Scripting languages like Lua and MicroPython enable faster prototyping on capable MCUs.

C++ is the primary language for Arduino development via the Wiring framework.

Java

Java Micro Edition (Java ME) targeted embedded devices but is not used on typical MCUs. The JVM requires RAM and flash well beyond typical MCU budgets. The garbage collector introduces non-deterministic pauses, violating real-time constraints, and JIT compilation adds latency on low-clock processors.

Lua

Designed for embedded systems. Used on ESP chips via the NodeMCU firmware. The interpreter has a ~256 KB flash footprint, uses dynamic typing, and collects garbage via mark-and-sweep with an event-driven callback model.

Compared to C/C++ for ESP, Lua produces shorter code, requires less memory, and provides an interactive REPL for rapid testing. No compile-flash cycle: scripts run directly from the interpreter.

MicroPython

Python subset designed for microcontrollers. Runs on ESP32, RP2040 (Raspberry Pi Pico), STM32, and others. Provides an interactive REPL over serial, dynamic typing, garbage collection, and standard Python syntax with a reduced standard library. Includes modules for GPIO, I²C, SPI, UART, Wi-Fi, and timers.

Execution is slower than C/C++ since code is interpreted rather than compiled to native code. Memory footprint is higher than Lua. Not suitable for hard real-time tasks, but prototyping and iteration are significantly faster.

Cross Compilation

Native compilation produces machine code for the same architecture as the host machine. Cross compilation produces machine code for a different target architecture.

MCU firmware is almost always cross-compiled. The host is a general-purpose PC (x86-64); the target is an 8-bit or 32-bit MCU (AVR, ARM Cortex-M, RISC-V). The host has no ability to run the resulting binary.

Toolchain

A cross-compilation toolchain contains three components:

  • Compiler
    Translates source code to object files for the target ISA. Examples: avr-gcc for AVR, arm-none-eabi-gcc for ARM Cortex-M.
  • Linker
    Combines object files and resolves symbols. Uses a linker script that maps code and data sections to the exact flash and RAM addresses of the target device.
  • Binutils
    Assembler, object dump, and format conversion tools. The objcopy tool converts the linked ELF binary to Intel HEX or raw binary for upload.

Why Cross Compilation is Required

  • MCUs lack the RAM, storage, and OS to run a compiler.
  • Target ISA differs from the host; host-compiled binaries cannot execute on the MCU.
  • The linker script encodes the exact memory map of a specific chip variant, which is only known at build time.

Build Flow

Dedicated Programmer

The earliest and most direct method. A standalone hardware device (named programmer) writes firmware into the MCU’s flash or EEPROM before the chip is placed in the target circuit.

The MCU is extracted from the board and seated in a ZIF (zero insertion force) socket on the programmer. The programmer applies the required supply and programming voltages, then transfers the firmware image over a parallel or serial interface directly into the chip’s memory cells. Once programming is complete, the chip is removed and installed in the target circuit.

No bootloader or resident firmware is required. The method works on completely blank devices and on one-time-programmable (OTP) parts. In high-volume production, gang programmers write identical images to many chips simultaneously to reduce cycle time.

The main limitation is the physical chip-removal requirement. Each firmware revision means extracting the chip, reprogramming it, and reinstalling it which is impractical for field updates or surface-mounted devices that cannot be socketed.

In-Circuit Serial Programming

Aka. ICSP. Writes compiled firmware directly into the microcontroller’s flash while the chip remains soldered in the circuit. Uses a serial connection between the host programmer tool and the target MCU.

No chip removal required. The same physical device used in production can be programmed and reprogrammed without a socket or extraction tool.

How It Works

A hardware programmer (e.g. AVRISP, USBasp, ST-Link) connects to dedicated programming pins on the MCU. The programmer asserts a reset or special entry condition, then streams the firmware image over the serial link. The MCU’s internal bootrom or dedicated programming hardware writes the data to flash.

Programming pins used:

  • AVR (SPI-based ICSP): MOSI, MISO, SCK, RESET
  • PIC (2-wire ICSP): ICSPDAT, ICSPCLK plus MCLR for entry
  • ARM Cortex-M: SWD (SWDIO, SWDCLK) or JTAG

ICSP vs Bootloader

ICSP is hardware-level: the programmer directly writes flash cells. A bootloader is firmware-level: resident code in the MCU accepts the image over USB/UART and writes it itself. ICSP works on a blank chip; a bootloader must already be installed.

Over-the-Air Programming

Aka. OTA. Enables firmware updates delivered through a wireless link rather than a physical programming cable. The device downloads the new firmware image and writes it to flash without any physical access.

Used in deployed IoT devices where physical access is impractical or impossible.

Bootloader Role

OTA requires a resident bootloader that persists across firmware updates. On startup, the bootloader:

  • Checks for a pending update image in a designated flash region
  • Validates the image (CRC or cryptographic signature)
  • Writes the image to the application partition if valid
  • Boots the new application

The bootloader itself is never overwritten by an OTA update.

Delivery Methods

  • Pull
    Device polls a remote server at intervals and downloads a new image when one is available. Simple to implement and works behind NAT without inbound connections. Update latency is bounded by the polling interval, and frequent polling increases power and bandwidth consumption.
  • Push
    Server initiates the transfer; device listens for a notification and fetches or receives the image. Enables immediate delivery and server-controlled rollout, useful for critical security patches. Requires the device to maintain a persistent connection or be directly reachable, which is problematic behind NAT or firewalls.

Dual-Partition Scheme

The most common approach. Flash is divided into two application partitions: active and update.

  1. New image downloads into the update partition while the current firmware keeps running.
  2. On success, the bootloader swaps the active and update labels.
  3. Device reboots into the new firmware.

If the new firmware fails to boot, the bootloader rolls back to the previously active partition.

Security Requirements

OTA is a remote code execution path. Without protection, it is an attack surface.

  • Image signing
    Firmware image is signed with a private key. Bootloader verifies the signature against a public key stored in protected flash before installing.
  • Transport encryption
    Image transferred over TLS or equivalent to prevent interception.
  • Version check
    Bootloader rejects images with a version number lower than the current firmware to prevent downgrade attacks.
  • Secure element storage
    Signing keys stored in a hardware secure element or OTP region, not in general flash.

ESP32 OTA

The ESP-IDF framework provides a built-in OTA API. Flash is partitioned into ota_0 and ota_1 application slots plus an otadata partition that records which slot is active.

The esp_ota_ops library handles:

  • Writing the incoming image chunk by chunk to the inactive slot
  • Marking the new slot as pending-verify
  • Triggering a reboot; on successful first boot the slot is confirmed active
Was this helpful?