Could I realistically be expecting some 25% - 50% code size reduction if I abandoned Arduino IDE's framework and moved to something more rudimentary such as AVR Studio ? I am using the EEPROM, Timer1 and Wire libraries whereas my own code is about 700 lines of code.
4 Answers
You don't need to abandon the Arduino IDE.
Let me quote from my forum post about sketch sizes.
Introduction
Every now and again this subject pops up on the Arduino forum. Why does it take 1000 bytes to blink an LED? Why, oh why? It is obviously very bloated, eh?
Example: blink
Let's check that claim first.
The "blink" sketch, as shipped with the IDE (and stripped of comments) is:
int led = 13;
void setup()
{
pinMode(led, OUTPUT);
}
void loop()
{
digitalWrite(led, HIGH);
delay(1000);
digitalWrite(led, LOW);
delay(1000);
}
Compiled:
Binary sketch size: 1,084 bytes (of a 32,256 byte maximum)
OK, over 1000 bytes to "blink an LED".
Well first, as someone on the Arduino forum humorously pointed out, it doesn't take 2000 bytes to blink two LEDs!
int led1 = 13;
int led2 = 12;
void setup()
{
pinMode(led1, OUTPUT);
pinMode(led2, OUTPUT);
}
void loop()
{
digitalWrite(led1, HIGH);
delay(1000);
digitalWrite(led1, LOW);
delay(1000);
digitalWrite(led2, HIGH);
delay(1000);
digitalWrite(led2, LOW);
delay(1000);
}
That takes:
Binary sketch size: 1,140 bytes (of a 32,256 byte maximum)
So the overhead for the second LED is only:
1140 - 1084 = 56
So you could argue that it only takes 56 bytes to blink an LED (providing you are already blinking another one).
It does more than blink
The first relevant rejoinder here is that the example sketch does more than blink an LED. Rather importantly, it also waits for a second between turning the LED on and off (the "delay" function call).
If we remove that, the sketch is somewhat smaller:
int led = 13;
void setup()
{
pinMode(led, OUTPUT);
}
void loop()
{
digitalWrite(led, HIGH);
digitalWrite(led, LOW);
}
Code size:
Binary sketch size: 882 bytes (of a 32,256 byte maximum)
So we saved 202 bytes by not doing a delay. This isn't totally unreasonable, as doing delays requires the library to configure a timer, and have code to check that timer.
The pins numbers are variables
The code in the Blink example uses (wrongly in my opinion) a variable to hold the pin number (13) when a constant would do. And if you use a constant there is a library called digitalWriteFast that generates more efficient code.
#include <digitalWriteFast.h>
const int led = 13;
void setup()
{
pinModeFast(led, OUTPUT);
}
void loop()
{
digitalWriteFast(led, HIGH);
digitalWriteFast(led, LOW);
}
Code size:
Binary sketch size: 470 bytes (of a 32,256 byte maximum)
Now we are down to 470 bytes to "blink an LED".
We can use port manipulation
If all we want to do is blink the LED, we can omit using setup and loop, and go straight into using the processor registers. For example:
int main ()
{
DDRB = bit (5);
while (true)
PINB = bit (5);
}
That blinks pin 13, believe it or not*. And now it only takes 178 bytes:
Binary sketch size: 178 bytes (of a 32,256 byte maximum)
* very rapidly indeed
Interrupt vectors
Well, you may be thinking "Pfft! Still 178 bytes. Why not 10 bytes?"
The answer to that lies in the way the hardware works. The Atmega328P has 26 interrupt vectors, at the start of program memory. Each one is 4 bytes (it jumps to a handler for each interrupt). So 26 * 4 = 104 bytes.
Thus there is an unavoidable overhead of 104 bytes in any piece of code. (Unless you know in advance you aren't going to use all those interrupt vectors, but this is rather specialized).
You can see these vectors if you add handlers for them:
EMPTY_INTERRUPT (INT0_vect)
EMPTY_INTERRUPT (INT1_vect)
EMPTY_INTERRUPT (PCINT0_vect)
EMPTY_INTERRUPT (PCINT1_vect)
EMPTY_INTERRUPT (PCINT2_vect)
EMPTY_INTERRUPT (WDT_vect)
EMPTY_INTERRUPT (TIMER2_COMPA_vect)
EMPTY_INTERRUPT (TIMER2_COMPB_vect)
EMPTY_INTERRUPT (TIMER2_OVF_vect)
EMPTY_INTERRUPT (TIMER1_CAPT_vect)
EMPTY_INTERRUPT (TIMER1_COMPA_vect)
EMPTY_INTERRUPT (TIMER1_COMPB_vect)
EMPTY_INTERRUPT (TIMER1_OVF_vect)
EMPTY_INTERRUPT (TIMER0_COMPA_vect)
EMPTY_INTERRUPT (TIMER0_COMPB_vect)
EMPTY_INTERRUPT (TIMER0_OVF_vect)
EMPTY_INTERRUPT (SPI_STC_vect)
EMPTY_INTERRUPT (USART_RX_vect)
EMPTY_INTERRUPT (USART_UDRE_vect)
EMPTY_INTERRUPT (USART_TX_vect)
EMPTY_INTERRUPT (ADC_vect)
EMPTY_INTERRUPT (EE_READY_vect)
EMPTY_INTERRUPT (ANALOG_COMP_vect)
EMPTY_INTERRUPT (TWI_vect)
EMPTY_INTERRUPT (SPM_READY_vect)
Now the start of the object code is:
00000000 <__vectors>:
0: 0c 94 34 00 jmp 0x68 ; 0x68 <__ctors_end>
4: 0c 94 51 00 jmp 0xa2 ; 0xa2 <__vector_1>
8: 0c 94 52 00 jmp 0xa4 ; 0xa4 <__vector_2>
c: 0c 94 53 00 jmp 0xa6 ; 0xa6 <__vector_3>
10: 0c 94 54 00 jmp 0xa8 ; 0xa8 <__vector_4>
14: 0c 94 55 00 jmp 0xaa ; 0xaa <__vector_5>
18: 0c 94 56 00 jmp 0xac ; 0xac <__vector_6>
1c: 0c 94 57 00 jmp 0xae ; 0xae <__vector_7>
20: 0c 94 58 00 jmp 0xb0 ; 0xb0 <__vector_8>
24: 0c 94 59 00 jmp 0xb2 ; 0xb2 <__vector_9>
28: 0c 94 5a 00 jmp 0xb4 ; 0xb4 <__vector_10>
2c: 0c 94 5b 00 jmp 0xb6 ; 0xb6 <__vector_11>
30: 0c 94 5c 00 jmp 0xb8 ; 0xb8 <__vector_12>
34: 0c 94 5d 00 jmp 0xba ; 0xba <__vector_13>
38: 0c 94 5e 00 jmp 0xbc ; 0xbc <__vector_14>
3c: 0c 94 5f 00 jmp 0xbe ; 0xbe <__vector_15>
40: 0c 94 60 00 jmp 0xc0 ; 0xc0 <__vector_16>
44: 0c 94 61 00 jmp 0xc2 ; 0xc2 <__vector_17>
48: 0c 94 62 00 jmp 0xc4 ; 0xc4 <__vector_18>
4c: 0c 94 63 00 jmp 0xc6 ; 0xc6 <__vector_19>
50: 0c 94 64 00 jmp 0xc8 ; 0xc8 <__vector_20>
54: 0c 94 65 00 jmp 0xca ; 0xca <__vector_21>
58: 0c 94 66 00 jmp 0xcc ; 0xcc <__vector_22>
5c: 0c 94 67 00 jmp 0xce ; 0xce <__vector_23>
60: 0c 94 68 00 jmp 0xd0 ; 0xd0 <__vector_24>
64: 0c 94 69 00 jmp 0xd2 ; 0xd2 <__vector_25>
The "real" code has to start at 0x68 (decimal 104) which is where the reset vector takes us (the first vector).
So now, our code is really 178 - 104 bytes of useful code. That's 74 bytes to blink an LED.
However even some (most) of that is overhead, generated by the C compiler to initialize global variables, call class constructors, etc. Let's test that by blinking two LEDs.
int main ()
{
DDRB = bit (5); // pin 13
DDRB |= bit (4); // pin 12
while (true)
{
PINB = bit (5); // pin 13
PINB = bit (4); // pin 12
}
}
Code size:
Binary sketch size: 186 bytes (of a 32,256 byte maximum)
Eight bytes difference. So really, you can blink an LED in 8 bytes. That is the bottom line. And you can do it in the Arduino IDE.
The libraries are designed to make things easy
The fact is that the default behaviour of the Arduino libraries is to simplify things for beginners. Thus they do some things which take up a bit of memory;
- You can pass variables to pinMode and digitalWrite. Not just constants. Thus, at runtime, code has to convert the variable value to an actual processor port.
- You get an automatic set-up of Timer 0, so you can use millis(), micros(), and delay() function calls.
- You get Timer 1 and Timer 2 set-up, so you can do PWM output easily.
So you are trading off program space, to an extent, for ease of use. But if you want to code around it, as I did above, you can trim that program space right down.
Tests on an Arduino Uno board type, IDE 1.0.5.
-
I think I'll give that a go, sounds promising. I have alrrady tried commenting out all the digtal-Read/Write calls from my own code with little gain though. So I shall have to brush off those libs a bit.– kellogsCommented Nov 8, 2017 at 11:08
-
Note that using just assembly (no interrupts, no C runtime) you can blink an LED in 12 bytes, including the delay. Commented Nov 8, 2017 at 11:10
-
No doubt, but I was talking about what could be achieved with the existing IDE. I suspect the OP isn't interested in coding all his stuff in assembly. :)– Nick Gammon ♦Commented Nov 8, 2017 at 20:08
If you like the Arduino IDE, you also can do the following:
- Do not use the library functions, but use registers directly. The compiler is smart enough that it does not link functions you do not use with your executable/binary.
- Optimize your code by
- Checking if you can simplify algorithms
- Remove unnecessary statements
- Remove global variables (maybe move them to locals if you can)
- Reduce or remove text strings.
- For globals (but also to reduce SRAM) minimize memory sizes of variables, e.g. use uint8_t instead of integers, especially when using arrays of them.
- If you use library functions, optimize them:
- Simplify library functions if you use them (by 'paying' less portability for example)
- By removing functionality that you do not need
Other tips (not necessarily answering your answer but things to keep in mind);
- Use another Arduino which has more memory (Mega/Due)
- Switch to another type of microControllers with more memory/flash
I have managed some 33% downsizing, hurrah!
Adding to the current answers: the biggest reduction came by ridding the floating points. I have come across the answer here and put avr-objdump
to good use. That's how I have had the shock of my life (no hardware FPU built into these $1 MCUs :-o) and also that is how you can see other offenders sneaking into your binary image.
HTH
-
Very good idea to do that. Regardless of IDE, doing floating-point maths when you don't need to will lead to a lot of bloat. Also don't be tempted to use long long (64-bit integers) which the hardware certainly doesn't support. The compiler and linker do optimize rather aggressively, but they can't optimize away something that you are using (eg. floats). They may optimize away parts of a library that you don't use.– Nick Gammon ♦Commented Nov 10, 2017 at 10:23
-
Also avoid the string class and use F for printing strings.– jantjeCommented Nov 10, 2017 at 12:11
-
@jantje Right, but my code comes with no strings attached :)– kellogsCommented Nov 10, 2017 at 13:47
Could I realistically be expecting some 25% - 50% code size reduction ...
code size has nothing to do with the ide, but compiler, compilers used, and libraries / routines used.
in general (which means that there are lots of exceptions) you will see a size increase when porting to an non-arduino environment.
with that said, many of the arduino functionalities are not that well implemented so you can save code size if you roll your own functionally equivalent code.