tmerr.com

Look ma, no compiler: blinking an LED

I am not sure how to explain the motivation for this project, so let's just get into it: for one reason or another I decided I wanted to use a microcontroller to blink an LED. The twist is that I would use only machine code.

Going into this, I had some idea how to use Arduino IDE to upload and run programs, but was not sure what it was doing under the hood. Since this would be the first time I tried programming a real device at such a low level, I expected to run into some you don't know what you don't know type surprises.

Materials used

This wasn't an electronics project but I did have to plug in an LED. I avoided using the RGB LED built into my dev board since it seemed complicated to control. Instead I found an LED along with a resistor (to limit the current) in a pile of electronics junk and wired GPIO pin 18 to LED to resistor to GND.

Understanding the build process

I started out by setting up the Arduino IDE to work with my microcontroller, following this guide. I figured I knew how to express the program in the Arduino IDE, and maybe could work backward to how it works.

void setup() {
  pinMode(18, OUTPUT);
}

void loop() {
  digitalWrite(18, HIGH);
  delay(500);
  digitalWrite(18, LOW);
  delay(500);
}

Arduino IDE does a great job of abstracting away the build process. To see under the hood, I found a useful option to output build outputs to the sketch directory: "Sketch > Export compiled binary".

These build outputs included

I never figured out what all of these do. But among these, the ELF file caught my attention. I recognized the ELF format as the usual format for Linux executable. But the microcontroller wasn't running Linux, so I was a little surprised to see it here. I investigated with a 32-bit RISC-V disassembler

$ riscv32-unknown-linux-gnu-objdump -d my-sketch.ino.elf

The output was about 65,000 lines of assembly instructions. Most of it didn't seem especially relevant to blinking an LED. For example there was a routine "modem_clock_wifi_mac_configure". I was not sure how to quickly filter this assembly down to the relevant lines, so decided to try another angle.

The second file that caught my interest was the 8MB binary. It seemed unlikely to be coincidence that it matched the ESP32's flash memory size. My suspicion that this was the firmware image flashed to the ESP32, and this was confirmed by Espressif's image format docs. Apparently the 8MB firmware image is built from the ELF file by esptool. This special firmware image format specifies some parameters and maps parts of the image into virtual memory (yes, this microcontroller has virtual memory!). I was now fully expecting to have to deal with this image file format in addition to the machine code.

Luckily while reading about the details in the ESP32 Technical reference, I stumbled upon a way to bypass the image format entirely. This is through a feature called "direct boot".

Direct boot: ... programs run directly from flash. To enable this mode, make sure that the first two words of the bin file downloaded to flash are 0xaedb041d.

Since the CPU architecture is little endian, the image file begins:

[1d, 04, db, ae, 1d, 04, db, ae]

Then the machine code instructions are supposed to come after.

Generating machine code

With the image format out of the way, the next step was to get things working end to end with some minimally viable machine code. Given no obvious way to debug, I would create two variants of my program. One would set the LED on and then infinite loop, and the other would set the LED off and then infinite loop. If each program did what I expected, then I could move on to the next step of making the LED blink.

I wrote some Python that would output the machine code instructions. This quick reference was handy for instruction encoding. I also decided to output assembly instructions since I could use them for debugging. I found the site venus.krakvil.me useful for simulating the execution, and the GNU assembler (from the 32-bit version of riscv-gnu-toolchain) useful as a reference for correct machine code output.

The hard part turned out not to be instruction encoding, but figuring out what the program was supposed to do. In theory all of the relevant details were in the well-written ESP32-C6 technical reference manual. But which of the 1300 pages to pay attention to?

What the program should do

The big idea I eventually understood is that many hardware features in the ESP32-C6 are controlled by reading from and writing to memory locations. Within a 32-bit location, a few bits might represent an integer, and another bit might represent a feature toggle. There are different bits that control

To control these, I would first use the lw (load word) instruction to read the memory into a register, modify the relevant bits, and then write it back out using sw (store word). This pattern repeated again and again.

The actual memory locations are scattered across the technical manual, where often they are given as relative offsets from some base address.

At the end of the program is:

jal zero, 0

Jump to instruction at relative offset 0: an infinite loop

Uploading to the microcontroller

To upload the program to the microcontroller, I plugged it in over USB and used the esptool utility. It turned out, this software was already somewhere on my filesystem as an exe for use by the Arduino IDE. So I ran

python blink.py # my script that creates blink.bin
.\esptool.exe --port COM3 --baud 921600 write_flash 0x0 .\blink.bin

Since the program size was less than 1KB the upload completed almost instantly.

This worked! I could upload one program that held the LED on, and another that held it off.

Making it blink

To make the LED properly blink, I would also need a delay mechanism. While the efficient thing to do would probably involve hardware timers, I chose to count to 20 million. If the microcontroller can run one addition instruction per clock cycle (a guess), and the clock is running at 40MHz, that should introduce a half second delay. So the full program turns on the LED, delays, turns off the LED, delays, then jumps to the beginning.

The code

Download the Python script that generates the machine code here.

Resources