====== 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 [[https://www.microchip.com/en-us/product/ATtiny824|ATtiny824]]. My first goals were: - Compile some code - Figure out how to flash the chip - 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 [[https://packs.download.microchip.com/|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: [[https://github.com/ZigEmbeddedGroup/regz|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 : 0: 0c 94 0e 00 jmp 0x1c ; 0x1c 00000004 : 4: f8 94 cli 6: fe cf rjmp .-4 ; 0x4 00000008 : 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 12: 80 93 07 04 sts 0x0407, r24 ; 0x800407 16: fd cf rjmp .-6 ; 0x12 <.app.main+0x6> 00000018 : 18: 0e 94 02 00 call 0x4 ; 0x4 0000001c : 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 2e: 2d 91 ld r18, X+ 30: 21 93 st Z+, r18 32: fa cf rjmp .-12 ; 0x28 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 42: 0e 94 04 00 call 0x8 ; 0x8 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: * Parallel programming (PP) * High voltage serial programming (HVSP) * In system programming (ISP) 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: * Unified Program and Debug Interface (UPDI) 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: [[https://github.com/SpenceKonde/megaTinyCore|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!**