Table of Contents

Introducing ATtiny824 to microZig

Written by: xq

For a project of mine, i needed a small and affordable microcontroller. As i've been proficient with the AVR series, i've selected the ATtiny824.

My first goals were:

  1. Compile some code
  2. Figure out how to flash the chip
  3. Flash a blinky example

Compiling the code

First of all, we have to get microZig ready for our brand new chip. For this, we have to provide a new *chip* component we can pass to microZig as a backing for our embedded executable:

const std = @import("std");
const zpm = @import("zpm.zig");

pub fn build(b: *std.build.Builder) void {
    const mode = b.standardReleaseOptions();

    var exe = zpm.sdks.microzig.addEmbeddedExecutable(
        b,
        "firmware",
        "software/main.zig",
        .{ .chip = attiny824 },
        .{},
    );
    exe.setBuildMode(mode);
    exe.install();
}

Okay, so far nothing special, but we need to define the attiny824 variable. Let's do that real quick!

pub const attiny824 = zpm.sdks.microzig.Chip{
    .name = "ATtiny824",
    .path = "software/attiny824.zig",
    .cpu = tinyAVR,
    .memory_regions = &.{
        .{ .offset = 0x000000, .length = 0x2000, .kind = .flash },
        .{ .offset = 0x803400, .length = 0x0400, .kind = .ram },
    },
};

The memory regions can be fetched from the manual, which tells us that the chip has 8K flash and 1K of RAM (The offsets are documented in the manual as well). Also note the address for the RAM is offset by 0x800000 bytes to tell LLVM and the linker that this is actually RAM and not flash, so the correct instructions will be selected later on.

After that, we have to define a new CPU. microZig doesn't support the tinyAVR 2 instruction set yet, so we'll just define that as well:

pub const tinyAVR = zpm.sdks.microzig.Cpu{
    .name = "tinyAVR",
    .path = "software/tinyavr.zig",
    .target = std.zig.CrossTarget{
        .cpu_arch = .avr,
        .cpu_model = .{ .explicit = &std.Target.avr.cpu.avrxmega4 },
        .os_tag = .freestanding,
        .abi = .eabi,
    },
};

I've selected the avrxmega cpu for LLVM. It's not 100% correct, but it isn't really wrong either. The tinyAVR instruction set is basically a union of the original and the Xmega instruction set, so we're fine by chosing Xmega.

We're also missing two files now. First, software/tinyavr.zig, which is for now just a copy of the avr5.zig. We're going to tackle this later on. attiny824.zig in contrast looks like this:

const std = @import("std");
const micro = @import("microzig");

pub usingnamespace @import("registers.zig");

pub const cpu = micro.cpu;

pub const clock = struct {
    pub const Domain = enum {
        cpu,
    };
};

Here we just forward some fields and export the registers from our file. That's enough to get to main, and this is all we care about right now.

Hitting compile now gives us an error that registers.zig could not be found. This can be resolved by going to to Microchip Packs Repository and download the *Microchip ATtiny Series Device Support* package. This is basically just a zip file. Extract it, go into the atdf folder and grab the right file. But well, it's still XML and not Zig.

Luckily, we have a nice tool to generate code from ATDF files: regz

With regz, we can generate ourselves the registers.zig file, put it into the project folder and compile.

NOTE: *At the time of writing (2022-11-09) this process does not work perfectly and needs some hand adjustment. This will be fixed over time.*

After that, we can zig build again and finally get an executable (assuming we're builing ReleaseSmall). So far, so good, seems like we have successfully tamed the compiler. Let's check the generated code real quick.

This is done by invoking avr-objdump -td zig-out/bin/firmware:

zig-out/bin/sdd-firmware:     file format elf32-avr

SYMBOL TABLE:
00000000 l    df *ABS*	00000000 sdd-firmware
00000000 l       *ABS*	00000000 __tmp_reg__
00000001 l       *ABS*	00000000 __zero_reg__
0000003f l       *ABS*	00000000 __SREG__
0000003e l       *ABS*	00000000 __SP_H__
0000003d l       *ABS*	00000000 __SP_L__
0000003c l       *ABS*	00000000 __EIND__
0000003b l       *ABS*	00000000 __RAMPZ__
00000004 l     F .text	00000004 hang
0000000c l     F .text	0000000c .app.main
00000000 g     F .text	00000004 vector_table
0000001c g     F .text	0000002a microzig_start
00000008 g     F .text	00000004 microzig_main
00000018 g     F .text	00000004 microzig_unhandled_vector
00803400 g       .data	00000000 microzig_data_start
00803400 g       .bss	00000000 microzig_bss_start
00803400 g       .bss	00000000 microzig_bss_end
00803400 g       .data	00000000 microzig_data_end
00000046 g       *ABS*	00000000 microzig_data_load_start

Disassembly of section .text:

00000000 <vector_table>:
   0:	0c 94 0e 00 	jmp	0x1c	; 0x1c <microzig_start>

00000004 <hang>:
   4:	f8 94       	cli
   6:	fe cf       	rjmp	.-4      	; 0x4 <hang>

00000008 <microzig_main>:
   8:	0e 94 06 00 	call	0xc	; 0xc <.app.main>

0000000c <.app.main>:
   c:	80 e1       	ldi	r24, 0x10	; 16
   e:	80 93 01 04 	sts	0x0401, r24	; 0x800401 <microzig_data_load_start+0x8003bb>
  12:	80 93 07 04 	sts	0x0407, r24	; 0x800407 <microzig_data_load_start+0x8003c1>
  16:	fd cf       	rjmp	.-6      	; 0x12 <.app.main+0x6>

00000018 <microzig_unhandled_vector>:
  18:	0e 94 02 00 	call	0x4	; 0x4 <hang>

0000001c <microzig_start>:
  1c:	a0 e0       	ldi	r26, 0x00	; 0
  1e:	b4 eb       	ldi	r27, 0xB4	; 180
  20:	e0 e0       	ldi	r30, 0x00	; 0
  22:	f4 e3       	ldi	r31, 0x34	; 52
  24:	80 e0       	ldi	r24, 0x00	; 0
  26:	94 e3       	ldi	r25, 0x34	; 52
  28:	e8 17       	cp	r30, r24
  2a:	f9 07       	cpc	r31, r25
  2c:	19 f0       	breq	.+6      	; 0x34 <microzig_start+0x18>
  2e:	2d 91       	ld	r18, X+
  30:	21 93       	st	Z+, r18
  32:	fa cf       	rjmp	.-12     	; 0x28 <microzig_start+0xc>
  34:	a0 e0       	ldi	r26, 0x00	; 0
  36:	b4 e3       	ldi	r27, 0x34	; 52
  38:	a8 17       	cp	r26, r24
  3a:	b9 07       	cpc	r27, r25
  3c:	11 f0       	breq	.+4      	; 0x42 <__SREG__+0x3>
  3e:	1d 92       	st	X+, r1
  40:	fb cf       	rjmp	.-10     	; 0x38 <microzig_start+0x1c>
  42:	0e 94 04 00 	call	0x8	; 0x8 <microzig_main>

Not bad. That's not really much code, but .data and .bss initialization is there, basic interrupt handling is there (aka: RESET) and we have a panic handler “hang”.

I adjusted the .bss and .data initialization to utilize the tinyAVR feature that the flash is fully mapped into the RAM segment as well. This allows us to load code without handwritten assembly, which is always nice.

So we now have an executable. But how the heck do we get that into the flash of our AVR?

Flashing a latest-generation AVR

When i started with AVRs in 2005, there were two options to flash an AVR:

The first two options were basically only possible with an STK500, which i then bought, but i mostly used the ISP option. ISP required us to have 6 pins connected to the AVR for flashing. Our ATtiny824 has only 14 pins, so that makes 42% of our available pins. Not a good solution.

Luckily, Microchip/Atmel came up with a new solution for that:

This option is way cooler than the ones before, but will leave our microcontroller with a reset pin. The benefits are brilliant though: We only need two pins connected to our microcontroller: GND and UPDI.

After inspecting the datasheet, i figured that UPDI makes it pretty easy on communications, as it's just a single-wire bi-directional UART. That means, what i need is only a USB-serial adapter, and a 1K resistor.

Sounds easy, let's get a flashing software.

I found several Python scripts that implement or clone a single implementation of the UPDI protocol. I tried several of them, but only the last one was working:

megaTinyCore is the current Arduino backing solution for the tinyAVR family, so it's widespread and tested.

So next, i set up my AVR with power, and the USB-serial adapter, and tried to erase the flash.

Surprisingly, that worked first try!

Let's flash our firmware!

Booting blinky

Oh, i totally forgot the code we were compiling:

const std = @import("std");
const microzig = @import("microzig");
const regz = microzig.chip.registers;

pub fn main() void {
    regz.PORTA.DIRSET.* = (1 << 4); // PA4
    while (true) {
        regz.PORTA.OUTTGL.* = (1 << 4);
    }
}

That code is just setting PA4 to output and then toggling it like a madman.

To flash the AVR, i had to convert the ELF file generated by microZig to Intel HEX. This is done by either using the right call in our build.zig, but i was lazy and just used avr-objcopy.

After that, the final magic invocation was:

[felix@denkplatte-v2 sd-drive]$ python ~/megaTinyCore/megaavr/tools/prog.py   updi-flasher --device attiny824 -u /dev/ttyUSB0 -a write -f zig-out/bin/  sdd-firmware.hex 
SerialUPDI
UPDI programming for Arduino using a serial adapter
Based on pymcuprog, with significant modifications
By Quentin Bolsee and Spence Konde
Version 1.2.3 - Jan 2022
Using serial port /dev/ttyUSB0 at 115200 baud.
Target: attiny824
Action: write
File: zig-out/bin/sdd-firmware.hex
Pinging device...
Ping response: 1E9329
Chip/Bulk erase,
Memory type eeprom is conditionally erased (depending upon EESAVE fuse setting)
Memory type flash is always erased
Memory type lockbits is always erased
...
Erased.
Action took 0.01s
Writing from hex file...
Writing flash...
[==================================================] 2/2
Action took 0.04s
Verifying...
Verify successful. Data in flash matches data in specified hex-file
Action took 0.02s
[felix@denkplatte-v2 sd-drive]$ 

That… was also surprisingly easy. I've fetched the scope and checked if the pin was toggling. And indeed, it was!

This means we've successfully bootstrapped our ATtiny824.

Conclusion

That bring-up story was mostly a lot of reading, and apart from that everything went basically first try. I had to pester Matt a bit about getting regz to compile the ATDF file, but apart from that everything went butter smooth.

This means that we should be able to support tinyAVR and megaAVR series microcontrollers quite easily. This is good news, as we can than cover a part of the embedded world not covered by C yet (at least not officially, avr-libc has code upstream, but it's not in a release).

Hell yeah!