Michcioperz

– Atmega8 programming, part 2: I actually want to be writing in Zig

986 words, ~5 minutes

In my previous venture into AVR programming, I went through the process of setting an LED alight with my Atmega8. I even promised to write and release part 2 the same day, and as you may imagine, I failed to deliver on that promise.

I can’t tell for sure what dragged me away — perhaps I just got tired of writing for the time being — but I think a not unimportant factor in this was my attempt to get rid of C in the project and use Zig instead.

If you used to hang out with me at uni between classes, you might be already familiar with Zig and with my sometimes unrequited crush on that programming language. I don’t really want to get too deep into why it’s cool (there are better resources, like Zig’s official website, or community-run learning sites like ziglearn.org), but in simple terms, I think Zig is the C-with-classes that we deserve rather than the one we already had (C++). To others, a better explanation would be that Zig is a thinner wrapper over LLVM IR than C is. And I think both these properties can help us in the somewhat surprising environment that is embedded programming.

There is one subtle difficulty: if we wanna exorcise the spirit of C, we’re gonna need to dig our heels into the documentation of the microcontroller and reinvent some wheels for ourselves. Where we’re going, we don’t want libc.

Let’s get that LED blinking — I mean, glowing

First, let’s remind ourselves what our code was doing back in the C version:

1
2
3
4
5
6
7
#include <avr/io.h>

int main() {
  DDRB |= (1 << PB2); // direction of pin B2 is output
  PORTB |= (1 << PB2); // set output of pin B2 to 1
  for (;;) {}
}

Our Zig code will have a similar structure, so we could write something pseudocode-like:

1
2
3
4
5
export fn main() noreturn {
  pin_b2.direction(.Out);
  pin_b2.output(true);
  while (true) {}
}

Like I said, Zig has this C-with-classes quality to it that allows us to think in terms of operations on structs, and from my experiments, it could take us far. But first, we’re gonna have to define what exactly our pin_b2 is. Something that can prove very useful for that is the datasheet of Atmega8

If this is your first visit to a datasheet of a microcontroller, you might notice that with the market price of printing services students use in Warsaw, which if I remember correctly is 0.07 PLN per page (wow, I haven’t seen a flyer for a photocopy service in months, it’s terrifying), and the ~2 GBP unit price of an Atmega8, a printed datasheet at 331 pages is something like twice as expensive as the microcontroller itself. Do not fear it. The PDF is free, and probably easier to search in. And anyway, it still could’ve been so much worse.

So if you tried to search for DDRB in this document, you might end up on page 65, which will helpfully tell you that yes, DDRB is The Port B Data Direction Register, and from there you could find out that the register is 8 read/write bits with initial value 0. If you wanted to find out instead how the pins work electrically, you could go back to page 51 and read up. Actually there’s a pretty useful clue on that page, that

Three I/O memory address locations are allocated for each port, one each for the Data Register — PORTx, Data Direction Register — DDRx, and the Port Input Pins — PINx.

So, there’s a memory address to write to? Cool! Unfortunately, this section does not speak of what exactly those addresses are. What we actually need is the Register Summary table on page 309, where we’ll find that register DDRB is at memory address 0x17 (0x37), whatever that means, and that PORTB and PINB are one byte off in each direction. But what does the parenthesis mean?

I think the best explanation handy is on page 18, Figure 8. Data Memory Map, and generally in the chapter that contains it. Turns out that CPU registers are mapped to the first 32 bytes of memory, and I/O registers are mapped right after that. 32 bytes is 0x20, which is the difference between the number in parentheses and the number outside them, so I think we have a winner. That would mean, in possibly simple terms, that DDRB is the 0x17th I/O register, but its location in memory is off by 0x20 from that 0x17, so 0x37. Seems to make sense for now.

It’s getting late, so maybe let’s just write what we need in the most C way possible and come up with a better way afterwards. Let’s define a struct for our pin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const Direction = packed enum(u1) {
    In = 0,
    Out = 1,
};

const DDRB = @intToPtr(*volatile u8, 0x37);
const PORTB = @intToPtr(*volatile u8, 0x38);
const PB2 = 2; // duh

const pin_b2 = struct {
    fn direction(dir: Direction) void {
        switch (dir) {
            .In => {
                DDRB.* &= ~(@as(u8, 1 << PB2));
            },
            .Out => {
                DDRB.* |= 1 << PB2;
            },
        }
    }

    fn output(value: bool) void {
        switch (value) {
            false => {
                PORTB.* &= ~(@as(u8, 1 << PB2));
            },
            true => {
                PORTB.* |= 1 << PB2;
            },
        }
    }
};

So what we did here is, we made a volatile variable out of the I/O registers that we needed, and we wrote some helper functions so our actual code in main function looked understandable at first glance. Does it compile?

1
2
 △ content/post/atmega8-zig zig build-obj -target avr-freestanding-none -O ReleaseSmall -mcpu avr4 glow.zig
 ▲ content/post/atmega8-zig

It does! Let’s take a look at the generated assembly code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
 ▲ content/post/atmega8-zig avr-objdump -d glow.o

glow.o:     file format elf32-avr


Disassembly of section .text:

00000000 <main>:
   0:   ba 9a           sbi     0x17, 2 ; 23
   2:   c2 9a           sbi     0x18, 2 ; 24
   4:   00 c0           rjmp    .+0             ; 0x6 <main+0x6>

Wow, this looks really nice. There are just two instructions to set one bit each on DDRB and PORTB registers, and then it just starts to loop in place. What would avr-gcc do with that C code we had earlier?