2022-09-30
The AVR Toolchain
One of the goals I had when I started getting into the digital side of electronics was to get a good grasp of the GNU toolchain and some of the avr tools. The arduino platform is nice but it doesn’t really give you a good grasp of what’s actually happening so here’s a primer from an attiny perspective and some of the common tools used to understand what’s in the final binary.
There’s already a great overview of the main avr toolchain), but this article provides some examples built against a simple binary to wrap your head around it all.
Tools
All of these examples are build with the attiny25 family in mind and uses a binary from a really dumb program in , loop.c
:
int main(void) {
int i = 0;
while(i < 10000) {
i++;
}
}
This was compiled to the loop
binary using avr-gcc -g -mmcu=avr25 -o loop loop.c
-g
to add debugging symbols, gives an easier time linking back to the individual line of code.-mmcu=avr25
specifies the AVR instruction set architecture to use. Change this to whatever chip you’re targeting.
At a high level, avr-libc provides a standard c library that can be compiled with gcc. The tools here are mostly provided through binutils and you would normally use make to stitch it all together.
Objdump
This is the swiss army knife to picking apart object files. Typically used to take a look at the disassembly:
> avr-objdump -m avr25 -S loop
loop: file format elf32-avr
Disassembly of section .text:
00000000 <main>:
int main(void) {
0: cf 93 push r28
2: df 93 push r29
4: 00 d0 rcall .+0 ; 0x6 <L0^A>
00000006 <L0^A>:
6: cd b7 in r28, 0x3d ; 61
8: de b7 in r29, 0x3e ; 62
0000000a <.Loc.1>:
int i = 0;
a: 1a 82 std Y+2, r1 ; 0x02
c: 19 82 std Y+1, r1 ; 0x01
0000000e <.Loc.2>:
while(i < 10000) {
e: 05 c0 rjmp .+10 ; 0x1a <.L2>
00000010 <.L3>:
i++;
10: 89 81 ldd r24, Y+1 ; 0x01
12: 9a 81 ldd r25, Y+2 ; 0x02
14: 01 96 adiw r24, 0x01 ; 1
16: 9a 83 std Y+2, r25 ; 0x02
18: 89 83 std Y+1, r24 ; 0x01
0000001a <.L2>:
while(i < 10000) {
1a: 89 81 ldd r24, Y+1 ; 0x01
1c: 9a 81 ldd r25, Y+2 ; 0x02
1e: 80 31 cpi r24, 0x10 ; 16
20: 97 42 sbci r25, 0x27 ; 39
22: b4 f3 brlt .-20 ; 0x10 <.L3>
24: 80 e0 ldi r24, 0x00 ; 0
26: 90 e0 ldi r25, 0x00 ; 0
00000028 <.Loc.5>:
}
28: 0f 90 pop r0
2a: 0f 90 pop r0
2c: df 91 pop r29
2e: cf 91 pop r28
30: 08 95 ret
The -S
flag attempts to intermix the original source code along with the assembly.
The output of this dump is a little confusing at first but let’s break it down. Looking at the counter increment code:
# This is the address in the elf file and the associated debugging symbol <.L3>
00000010 <.L3>:
# We're looking at the increment instruction here
i++;
# 10 is the address, 89 81 is the actual data.
# objdump translates this to ldd r24, Y+1 for an attiny.
10: 89 81 ldd r24, Y+1 ; 0x01
12: 9a 81 ldd r25, Y+2 ; 0x02
14: 01 96 adiw r24, 0x01 ; 1
16: 9a 83 std Y+2, r25 ; 0x02
18: 89 83 std Y+1, r24 ; 0x01
Hexdump
Probably not that useful but shows a raw view of the file.
1 byte per line dump:
> hexdump -v -e '1/1 "%02x " "\n"' loop
7f
45
4c
46
01
01
01
... a lot more is dumped
Od - Octal Dump
Way simpler to use than hexdump.
No address: -An
and just text: -a
.
> od -An -a loop
del E L F soh soh soh nul nul nul nul nul nul nul nul nul
stx nul S nul soh nul nul nul nul nul nul nul 4 nul nul nul
84 bs nul nul em nul nul nul 4 nul sp nul stx nul ( nul
cr nul ff nul soh nul nul nul t nul nul nul nul nul nul nul
... once again, lots more dumped
Size
Easy way to see the sizes of each section.
> avr-size --format=SysV loop
loop :
section size addr
.text 50 0
.data 0 8388704
.comment 36 0
.debug_aranges 32 0
.debug_info 85 0
.debug_abbrev 78 0
.debug_line 91 0
.debug_frame 52 0
.debug_str 95 0
Total 519
Objcopy
Elf files contain a bunch of unnecessary sections that would be a waste to store in our limited flash space on the target, like all the debugging sections and comments. This command is similar to strip
but also encodes the file from elf to various formats.
Intel hex
Most microcontrollers code uploads use an ihex file format rather than an elf file.
Building ihex from an elf:
> avr-objcopy -j .text -j .data -O ihex loop loop.hex && cat loop.hex
:10000000CF93DF9300D0CDB7DEB71A82198205C037
:1000100089819A8101969A83898389819A81803125
:100020009742B4F380E090E00F900F90DF91CF9172
:02003000089531
:00000001FF
Binary
You can take a look at the raw binary if hex is annoying to look at. Not that this is really useful outside of educational purposes.
# first dump to a binary file
> avr-objcopy -O binary --only-section=.text main test.bin
# Now display the binary for one instruction per line, `xxd` can handle the conversion:
> xxd -b -c 2 test.bin
00000000: 00001110 11000000 ..
00000002: 00010101 11000000 ..
00000004: 00010100 11000000 ..
00000006: 00010011 11000000 ..
00000008: 00010010 11000000 ..
0000000a: 00010001 11000000 ..
0000000c: 00010000 11000000 ..
0000000e: 00001111 11000000 ..
00000010: 00001110 11000000 ..
00000012: 00001101 11000000 ..
00000014: 00001100 11000000 ..
00000016: 00001011 11000000 ..
00000018: 00001010 11000000 ..
0000001a: 00001001 11000000 ..
0000001c: 00001000 11000000 ..
0000001e: 00010001 00100100 .$
00000020: 00011111 10111110 ..
00000022: 11001111 11100101 ..
00000024: 11010010 11100000 ..
00000026: 11011110 10111111 ..
00000028: 11001101 10111111 ..
0000002a: 00000010 11010000 ..
0000002c: 00000100 11000000 ..
0000002e: 11101000 11001111 ..
00000030: 10010000 11100000 ..
00000032: 10000000 11100000 ..
00000034: 00001000 10010101 ..
00000036: 11111000 10010100 ..
00000038: 11111111 11001111 ..
AVR GCC
This is the main compiler I use when working with avr chips.
Standards
C has published new standard for the language over time. New standards bring new features or datatypes. For an example of one I like, c99 introduced boolean datatypes. There are a number of different standards, as of 2022 theres c89, gnu89, c94, c99, gnu99, c11, gnu11, c17, gnu17, c2x, and gnu2x. I’m not sure why each release has a GNU equivalent or the history behind that, but there are differences, mostly a couple keywords and macros.
Note use gnu*
standards for arduino, I’ve seen some cases in the SpencerKonde ATtiny core where the asm
keyword is used.
Linking
The avr-gcc compiler not only has many AVR ISAs compilation support, it also provides the linker with defaults. This means that the architecture flag -m
needs to always be specified.
For AVRs, the interrupt table starts at 0x000. Dumping the table for an attiny85, compiled with -mmcu=attiny85
:
> avr-objdump -S -m avr25 loop
...
00000000 <__vectors>:
0: 0e c0 rjmp .+28 ; 0x1e <__ctors_end>
2: 15 c0 rjmp .+42 ; 0x2e <__bad_interrupt>
4: 14 c0 rjmp .+40 ; 0x2e <__bad_interrupt>
6: 13 c0 rjmp .+38 ; 0x2e <__bad_interrupt>
8: 12 c0 rjmp .+36 ; 0x2e <__bad_interrupt>
a: 11 c0 rjmp .+34 ; 0x2e <__bad_interrupt>
c: 10 c0 rjmp .+32 ; 0x2e <__bad_interrupt>
e: 0f c0 rjmp .+30 ; 0x2e <__bad_interrupt>
10: 0e c0 rjmp .+28 ; 0x2e <__bad_interrupt>
12: 0d c0 rjmp .+26 ; 0x2e <__bad_interrupt>
14: 0c c0 rjmp .+24 ; 0x2e <__bad_interrupt>
16: 0b c0 rjmp .+22 ; 0x2e <__bad_interrupt>
18: 0a c0 rjmp .+20 ; 0x2e <__bad_interrupt>
1a: 09 c0 rjmp .+18 ; 0x2e <__bad_interrupt>
1c: 08 c0 rjmp .+16 ; 0x2e <__bad_interrupt>
Compare this to the Arduino Uno’s ATmega328p, compiled with -mmcu=atmega328p
:
> avr-objdump -S -m avr5 loop
...
00000000 <__vectors>:
0: 0c 94 34 00 jmp 0x68 ; 0x68 <__ctors_end>
4: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
8: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
10: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
14: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
18: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
1c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
20: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
24: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
28: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
2c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
30: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
34: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
38: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
3c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
40: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
44: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
48: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
4c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
50: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
54: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
58: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
5c: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
60: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
64: 0c 94 3e 00 jmp 0x7c ; 0x7c <__bad_interrupt>
Avrdude
Pretty slick tool to read and write memory on your avr chip.
The Arduino platform has a pretty good integration with these tools. I found it to be helpful to turn the ide verbosity up to read the avrdude commands that are run. There’s a config file that comes from whatever core you are using that might be useful.
The following examples are based on an attiny85 connected to a Sparkfun tinyIsp programmer plugged into a usb port.
Upload an ihex file:
# -U [memory(flash/eeprom)]:[w/r]:[input/output file]:[i stands for intel hex]
avrdude -v -c usbtiny -B8 -p attiny85 -U flash:w:hello.hex:i
Dump the flash:
avrdude -v -c usbtiny -B8 -p attiny85 -U flash:r:dump.hex:i
avr-objdump -sSd -m avr25 dump.hex
Terminal mode to dump part of eeprom:
avrdude -v -c usbtiny -B8 -p attiny85 -t
>>> dump eeprom 0 16
Reading | ################################################## | 100% 0.02s
0000 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|
Changing the clock speed from 8Mhz internal to 1Mhz internal. Note that the F_CPU macro in your code needs to match the fuse value. Use a fuse calculator for these. Setting the fuses:
avrdude -v -c usbtiny -B8 -p attiny85 -t
>>> dump lfuse
Reading | ################################################## | 100% 0.00s
0000 e2 |. |
>>> write lfuse 0 0x62
Info: Writing 1 bytes starting from address 0x00
Writing | ################################################## | 100% 0.01s
>>> read lfuse
Reading | ################################################## | 100% 0.00s
0000 62 |b |