2
\$\begingroup\$

I've been writing an application for AVR microcontrollers. I intended to burn it on ATtiny13A (because it's cheap, and I wouldn't use complex functions anyway), but at that time I had only ATmega328P available, so I used it to develop code. Knowing that flash size of ATtiny13A is only 1kB, I spent quite a lot of time optimizing code for size, making it 910B. Today, my ATtiny13A's arrived. However, when I recompiled code for this hardware, the code took just 772B - a 15% of difference! If I knew this before, I would wait with those optimizations. I don't use any advanced library functions - the only files included are: avr/io.h and util/delay.h. Thus, I wonder what caused the size difference - were those some additional assumptions the compiler could use knowing the target platform? If yes, what were they? Or maybe the included files themselves contain some boilerplate code varying betweeen platforms?

Note: I'm using avr-gcc 4.8.1.

\$\endgroup\$
  • 1
    \$\begingroup\$ The binaries are presumably small enough that you could feed them to a disassembler and compare them by hand.. \$\endgroup\$ – pjc50 Aug 26 '15 at 21:50
  • \$\begingroup\$ I'm comparing the disassemblies of both versions - I'll write my observations later. \$\endgroup\$ – akrasuski1 Aug 26 '15 at 21:50
5
\$\begingroup\$

Observations from comparing disassemblies:

  1. Interrupt vectors - although I don't use interrupts, the "stubs" are still there:

14: 0c 94 49 00 jmp 0x92 ; 0x92 <__bad_interrupt>.

ATmega328P has much more of those, and ATtiny uses 2-byte rjmp instructions there.

  1. In a couple of places, ATmega code uses call or jmp opcodes, whereas ATtiny uses rcall or rjmp respectively. For example:

8a: 0e 94 15 01 call 0x22a ; 0x22a <main> 8e: 0c 94 c0 01 jmp 0x380 ; 0x380 <_exit>

vs.

32: b5 d0 rcall .+362 ; 0x19e <main> 32: 60 c1 rjmp .+704 ; 0x2f6 <_exit>

Each of such lines saves two bytes.

  1. No idea why, but the compiler occasionally inserts instructions that are effective nops:

b0: 00 c0 rjmp .+0 ; 0xb2 <ds18b20_reset+0x1c> b2: 00 00 nop

The first instructions jumps to the following one (which would happen anyway, as IP increments after executing every instruction), and the second one is a literal nop. This happens on ATmega code, wasting four bytes each time.

  1. There were some 'tricks' compiler used only in ATtiny, for example to shift r24 seven pleces left (x<<=7;):

148: 20 e8 ldi r18, 0x80 14a: 82 9f mul r24, r18 14c: c0 01 movw r24, r0 14e: 11 24 eor r1, r1

vs.

d0: 87 95 ror r24 d2: 88 27 eor r24, r24 d4: 87 95 ror r24

I'm not sure whether this is exactly equivalent, but this is what compiler said.


Overall, those are approximate wastes:

  1. Interrupt vectors: in ATmega they take 0x68 bytes, in ATtiny 0x14. Difference=84B, which is more than half of the total difference.

  2. Approximate number of jmp -> rjmp & call -> rcall changes, not counting interrupt vectors: 22, which multiplied by two bytes of save each time gives 44B of saves.

The remaining causes are hard to count due to fuzziness of definition.

\$\endgroup\$
5
\$\begingroup\$

Because they're different subarchitectures (avr25 for the '13A, and avr5 for the '328P) and have different peripherals. As such, they have both slightly different instruction sets and different numbers of interrupt vectors. The interrupts alone (10 vs. 26, 1 code address vs. 2 per vector) result in a 26*4-10*2=84 byte difference.

The differing speed between the devices can matter as well, since _delay_*() with a slower device can mean less than 256/65536 loops whereas a faster device would need more than 256/65536 loops. This can mean a difference of 4 to 8 bytes each time one of those functions is called.

\$\endgroup\$
  • \$\begingroup\$ Interrupt vectors: correct. Delay loops: technically, possible, but I clocked ATmega328P with internal 8MHz and ATtiny13A with internal 9.6MHz, so unlikely. Still, there's more. \$\endgroup\$ – akrasuski1 Aug 26 '15 at 22:03
0
\$\begingroup\$

Technically it is the compiled size not the "flash" size that you are referring to. You are correct. The compiler optimizes the code for the target platform via the mmcu command. It looks in io.h and gets directed to the specifi ioxxxx.h for your chip. Eg the numbers of ports etc. So that is why your compiled code will vary in size between chips. Google for "avr io.h" and you should get a link to the docs where it describes exactly how it works. Just fyi you can compile for any supported chip. so in the future you can do your dev on one and compile the code for another and, if you have the atmel studio, simulate it for debugging.

\$\endgroup\$
  • \$\begingroup\$ I/O ports are in I/O space and data space, and do not affect the size of the compiled image other than the instructions used to access them. \$\endgroup\$ – Ignacio Vazquez-Abrams Aug 26 '15 at 22:02
  • \$\begingroup\$ @ignacio you are absolutely right. It was the first difference i thought of between chips! \$\endgroup\$ – BenG Aug 26 '15 at 22:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.