How Many Layers to Blink an LED?
How Arduino drifted from a thin abstraction over bare metal into an embedded software platform… and why I don’t like that.
Published
I was talking to my friend Alex, who works at Red Bull F1, and he was giving me some advice on my placement applications. We briefly discussed what had got us interested in electronics and agreed that Arduino was a major stepping stone for both of us. The article below represents my thoughts on the “old” Arduino, how it compares to the “new” Arduino (post-Qualcomm acquisition) and why I think part of what made it special is now gone.
When I started using Arduino, I was fourteen years old. I was fortunate enough to have been introduced to it through school (thanks, Dr Patterson!). As part of our engineering curriculum, we learnt the basics of “systems and control” using Arduino to do basic things such as reading sensors, controlling servos and blinking LEDs.
Arduino was an effective abstraction. It made embedded programming approachable while leaving a short, discoverable route from the friendly API to the underlying hardware registers. Modern Arduino attempts to present the same programming model on top of a much larger embedded platform: SDKs, HALs, schedulers, background tasks, wireless services and, on some boards, a whole second processor running Linux.
Before I spend the rest of this article whinging about how Arduino has lost its charm, I should clarify that these layers are not inherently bad and often exist for good reasons. I now use FreeRTOS regularly, having learnt enough about concurrency to understand when it is useful. My objection is that the familiar Arduino model can conceal the architecture in which it operates, presenting the programmer with something akin to the tip of an iceberg.
A sufficiently curious beginner might foolishly decide to investigate what happens beneath digitalWrite(). On an old-style Arduino such as the Uno, they would, if persistent enough, eventually find a fairly simple operation on a GPIO register.
On a modern platform, they would instead find an Arduino task running inside an RTOS, followed by platform APIs, drivers and hardware-abstraction layers before finally reaching the register they might have been looking for.
I’m hesitant to blame this trend on “lazy” people wanting features such as Wi-Fi, Bluetooth and graphics without putting in the work to understand them. Perhaps I will instead blame it on microcontrollers becoming too good.
Gone are the days when a beginner had to worry about the limited RAM and clock speed of the ATmega328P. Now they can have a shiny new Uno Q, containing a quad-core application processor running at up to 2 GHz alongside a 160 MHz STM32 microcontroller, compared with the old Uno’s single 16 MHz ATmega328P and 2 KB of SRAM.
With so much power in abundance, a beginner no longer has to choose the right tool for the job quite so carefully. For most beginner projects, the Uno Q can do it all — and if the software needed to make that possible introduces another few layers of abstraction, there is more than enough processing power and memory available to hide them.
I think a nice way of illustrating this is to start with the first Arduino sketch I ever wrote. Unsurprisingly, this is Blinky.
void setup()
{
// Put your setup code here, to run once:
pinMode(LED_BUILTIN, OUTPUT);
}
void loop()
{
// Put your main code here, to run repeatedly:
digitalWrite(LED_BUILTIN, HIGH);
delay(1000);
digitalWrite(LED_BUILTIN, LOW);
delay(1000);
}
Wow, very nostalgic. When we investigate the underlying code that makes this tick, we find the Arduino AVR core, of which I have written a very basic approximation below:
int main()
{
init();
setup();
while (true) {
loop();
}
}
setup(), then call loop() forever.
On the same “layer” as this core is the implementation of digitalWrite(). A minimal implementation probably looks like this:
void digitalWrite(uint8_t pin, uint8_t value)
{
volatile uint8_t *port = portOutputRegister(digitalPinToPort(pin));
uint8_t bit = digitalPinToBitMask(pin);
if (value == HIGH)
*port |= bit;
else
*port &= ~bit;
}
digitalWrite()
Arduino pin mapping followed by direct register access.
Ignoring startup and the bootloader, which I think is fair because they aren’t really involved in GPIO operation, there are only two software layers: the sketch and the Arduino core. Beneath them is the register-level interface to the hardware. Simples.
The Arduino Uno Q, on the other hand, is a completely different story. While the user-written Blink sketch is unchanged, it now runs on an STM32U585 microcontroller under the Zephyr RTOS. The Arduino core is still sort of present, but it becomes a compatibility layer on top of Zephyr rather than owning the hardware directly.
Before the sketch can run, Zephyr initialises the microcontroller, its kernel, scheduling system and drivers. It then loads the compiled sketch and calls setup() and loop().
The route taken by digitalWrite() is therefore much longer. The Arduino core must first translate LED_BUILTIN into the GPIO description supplied for the board through Zephyr’s devicetree, then pass the request to the generic GPIO API, which dispatches it to the STM32-specific GPIO driver. Only then does the bit actually get set in the register.
Arduino sketch
↓
Arduino core (Zephyr pin mapping)
↓
Zephyr GPIO API
↓
STM32 GPIO driver
↓
Register
The other large structural change is, of course, that Zephyr is an RTOS. This means that delay() is handled by Zephyr’s scheduler. This is undoubtedly an improvement, as it allows additional threads to run while the Arduino sketch is sleeping, but it is not apparent from the sketch.
Oh, and on top of all of that, there is a quad-core Qualcomm processor running Debian Linux on the same board. This doesn’t sit right with me, and not because microcontrollers and Linux machines should never share a board. I do not like what the combination does to the educational character of the Uno. The original board’s limitations encouraged the programmer to understand and work with the machine. On the Uno Q, the temptation to escape to the second, vastly more powerful computer is probably too great.
I am therefore grateful to have grown up when I did. The old Arduinos made the first step into electronics relatively frictionless, but the hardware’s limitations eventually forced me to understand the machine underneath. If your goal is to sit down and make things work without learning very much about the computer, the Uno Q is probably the right tool for you. I, for one, am glad that the board I started with gave me no second computer to hand the difficult parts to, and when I wanted to find out what was underneath Arduino, I was lucky that there was not very much underneath Arduino.
There might be a wider lesson here about tools that make it possible to produce increasingly impressive work without understanding how any of it works.