How do we organize the code that runs on the chip? There are roughly four common patterns, and the right choice depends on the system's complexity and timing demands.
6.1 Super-loop (round-robin)
int main(void) {
init_all();
while (1) {
read_sensors();
update_state();
update_outputs();
handle_serial();
kick_watchdog();
}
}Simple, compact, no OS overhead. Works great for small projects: a thermostat, an LED controller, a remote control. The downside: any one of those calls blocks the loop. If handle_serial() waits 5 ms for a byte, the rest of the loop is delayed 5 ms. Adding new tasks requires reasoning about cumulative latency.
6.2 Time-triggered
A timer interrupts at fixed intervals (say 1 ms). The ISR sets a flag; the main loop wakes up and runs the next slot of work in a schedule:
void TIMER_ISR(void) { tick++; }
while (1) {
if (tick % 1 == 0) sample_imu();
if (tick % 10 == 0) update_pid();
if (tick % 100 == 0) update_telemetry();
while (!new_tick) sleep();
}Cleaner than free-running super-loop, easier to reason about. Used in many simple closed-loop systems. Limitation: long tasks still block the slot.
6.3 Foreground/background
ISRs do the time-critical bits in the foreground (capture an ADC sample, push to a queue, set a flag). The background loop processes queued data when not interrupted. Most production firmware below RTOS complexity uses this pattern.
Discipline: ISRs do as little as possible, only producing data; the main loop consumes and processes. Anticipate the temptation to "just print this in the ISR" and resist it. We will see why in section 8.
6.4 RTOS-based
When you have many concurrent activities with different priorities and deadlines, an RTOS is the right abstraction. Tasks are scheduled by priority; an ISR can wake a high-priority task that immediately preempts whatever was running. We dedicate a whole section to RTOS below.
The progression up that ladder buys flexibility at the cost of code size, RAM, and reasoning effort.
6.5 Languages
C. Dominates by a huge margin (~80 % of new embedded code). Predictable memory, raw register access, every chip vendor ships a C SDK, two generations of engineers know it. Will compile in 2050. Downside: easy to shoot your feet (buffer overflow, wild pointer, race condition).
C++. Increasingly used in larger embedded systems (Cortex-M4 and up). RAII for resource management, templates for type safety, classes for state machines. Avoid heavy features (exceptions, RTTI, dynamic_cast) on small targets.
Rust. Memory-safe systems language gaining ground. embedded-hal ecosystem, Tock OS, drogue, Embassy. Espressif officially supports Rust on ESP32. The promise: prevent whole classes of memory bugs at compile time, without runtime cost.
Python (MicroPython, CircuitPython). REPL on a chip. Wonderful for prototyping, education, slow-control tasks. Not for hard real-time.
Arduino C++. The friendliest entry point: digitalWrite, delay, Serial.begin. Hides the registers. Great for learning and rapid prototyping. Many serious products started as Arduino sketches.
Assembly. Used for tight inner loops (SIMD, crypto, vector math), boot code, and educational reasons. Otherwise rare.