Как надо было написать blink.ino

Меня всегда бесил базовый пример Arduino - blink.ino - своей неправильностью:

1
2
3
4
5
6
7
8
9
10
11
12
int led = 13;
 
void setup() {
	pinMode(led, OUTPUT);
}
 
void loop() {
	digitalWrite(led, HIGH);
	delay(500);
	digitalWrite(led, LOW);
	delay(500);
}

По моему мнению, он должен выглядить так:

1
2
3
4
5
6
7
8
9
10
11
12
#define LED 13
 
void setup() {
	pinMode(LED, OUTPUT);
}
 
void loop() {
	digitalWrite(LED, HIGH);
	delay(500);
	digitalWrite(LED, LOW);
	delay(500);
}

Однако, отбросим субъективные мнения и взглянем на различные написания скетча blink.ino с точки зрения эффективности исполняемого кода без купюр.

Здесь мне положено сказать, что я использую ОС Ubuntu 15.10, ядро 4.2.0-27-generic, avr-gcc версии 4.8.1, avr-objdump версии 2.24, diff версии 3.3.

Возьмем четыре варианта написания blink.ino:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Вариант 1
 
int led = 13;
 
void setup() {
	pinMode(led, OUTPUT);
}
 
void loop() {
	digitalWrite(led, HIGH);
	delay(500);
	digitalWrite(led, LOW);
	delay(500);
}

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Вариант 2
 
const int led = 13;
 
void setup() {
	pinMode(led, OUTPUT);
}
 
void loop() {
	digitalWrite(led, HIGH);
	delay(500);
	digitalWrite(led, LOW);
	delay(500);
}

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Вариант 3
 
#define LED 13
 
void setup() {
	pinMode(LED, OUTPUT);
}
 
void loop() {
	digitalWrite(LED, HIGH);
	delay(500);
	digitalWrite(LED, LOW);
	delay(500);
}

 

1
2
3
4
5
6
7
8
9
10
11
12
//Вариант 4
 
void setup() {
	pinMode(13, OUTPUT);
}
 
void loop() {
	digitalWrite(13, HIGH);
	delay(500);
	digitalWrite(13, LOW);
	delay(500);
}

 Методом пристального взгляда определяем, что Вариант 3 и Вариант 4 идентичны. Это связано с тем, что до компилятора эти скетчи все равно дойдут в одном и том же виде: макроподстановка - замена в тексте комбинации символов LED на число 13 - происходит еще на этапе препроцессора. Таким образом, можно исключить из наших экспериментов Вариант 4, как полностью идентичный Варианту 3. Однако, сохраним его ради чистоты эксперимента.

 Соберем все варианты.

Вариант 1:

1
2
Sketch uses 1,056 bytes (3.4%) of program storage space. Maximum is 30,720 bytes.
Global variables use 11 bytes (0.5%) of dynamic memory, leaving 2,037 bytes for local variables. Maximum is 2,048 bytes.

Вариант 2:

1
2
Sketch uses 1,018 bytes (3.3%) of program storage space. Maximum is 30,720 bytes.
Global variables use 9 bytes (0.4%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.

Вариант 3:

1
2
Sketch uses 1,018 bytes (3.3%) of program storage space. Maximum is 30,720 bytes.
Global variables use 9 bytes (0.4%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.

Вариант 4:

1
2
Sketch uses 1,018 bytes (3.3%) of program storage space. Maximum is 30,720 bytes.
Global variables use 9 bytes (0.4%) of dynamic memory, leaving 2,039 bytes for local variables. Maximum is 2,048 bytes.

 Видно, что исполняемый код от Варианта 1 больше других, а все остальные одинаковые. Проверим, что они совсем одинаковые:

1
2
/tmp/Stino_build$ diff -s blink2/blink2.hex blink3/blink3.hex 
Files blink2/blink2.hex and blink3/blink3.hex are identical

 

1
2
/tmp/Stino_build$ diff -s blink3/blink3.hex blink4/blink4.hex 
Files blink3/blink3.hex and blink4/blink4.hex are identical

Давайте дизассемблируем скетчи 1 и 2 и посмотрим, чем они отличаются. Сделаем вот так:

1
2
/tmp/Stino_build$ avr-objdump -d blink1/blink1.ino.cpp.o > blink1ino.txt
/tmp/Stino_build$ avr-objdump -d blink2/blink2.ino.cpp.o > blink2ino.txt 

Теперь посмотрим н исполнаемый код скетчей 1 и 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
blink1/blink1.ino.cpp.o:     file format elf32-avr
 
 
Disassembly of section .text.setup:
 
00000000 <setup>:
   0:	61 e0       	ldi	r22, 0x01	; 1
   2:	80 91 00 00 	lds	r24, 0x0000
   6:	0c 94 00 00 	jmp	0	; 0x0 <setup>
 
Disassembly of section .text.loop:
 
00000000 <loop>:
   0:	cf 93       	push	r28
   2:	df 93       	push	r29
   4:	c0 e0       	ldi	r28, 0x00	; 0
   6:	d0 e0       	ldi	r29, 0x00	; 0
   8:	61 e0       	ldi	r22, 0x01	; 1
   a:	88 81       	ld	r24, Y
   c:	0e 94 00 00 	call	0	; 0x0 <loop>
  10:	64 ef       	ldi	r22, 0xF4	; 244
  12:	71 e0       	ldi	r23, 0x01	; 1
  14:	80 e0       	ldi	r24, 0x00	; 0
  16:	90 e0       	ldi	r25, 0x00	; 0
  18:	0e 94 00 00 	call	0	; 0x0 <loop>
  1c:	60 e0       	ldi	r22, 0x00	; 0
  1e:	88 81       	ld	r24, Y
  20:	0e 94 00 00 	call	0	; 0x0 <loop>
  24:	64 ef       	ldi	r22, 0xF4	; 244
  26:	71 e0       	ldi	r23, 0x01	; 1
  28:	80 e0       	ldi	r24, 0x00	; 0
  2a:	90 e0       	ldi	r25, 0x00	; 0
  2c:	df 91       	pop	r29
  2e:	cf 91       	pop	r28
  30:	0c 94 00 00 	jmp	0	; 0x0 <loop>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
blink2/blink2.ino.cpp.o:     file format elf32-avr
 
 
Disassembly of section .text.setup:
 
00000000 <setup>:
   0:	61 e0       	ldi	r22, 0x01	; 1
   2:	8d e0       	ldi	r24, 0x0D	; 13
   4:	0c 94 00 00 	jmp	0	; 0x0 <setup>
 
Disassembly of section .text.loop:
 
00000000 <loop>:
   0:	61 e0       	ldi	r22, 0x01	; 1
   2:	8d e0       	ldi	r24, 0x0D	; 13
   4:	0e 94 00 00 	call	0	; 0x0 <loop>
   8:	64 ef       	ldi	r22, 0xF4	; 244
   a:	71 e0       	ldi	r23, 0x01	; 1
   c:	80 e0       	ldi	r24, 0x00	; 0
   e:	90 e0       	ldi	r25, 0x00	; 0
  10:	0e 94 00 00 	call	0	; 0x0 <loop>
  14:	60 e0       	ldi	r22, 0x00	; 0
  16:	8d e0       	ldi	r24, 0x0D	; 13
  18:	0e 94 00 00 	call	0	; 0x0 <loop>
  1c:	64 ef       	ldi	r22, 0xF4	; 244
  1e:	71 e0       	ldi	r23, 0x01	; 1
  20:	80 e0       	ldi	r24, 0x00	; 0
  22:	90 e0       	ldi	r25, 0x00	; 0
  24:	0c 94 00 00 	jmp	0	; 0x0 <loop>

Приколы начинаются на строке 8.  В варианте 2 в регистр 24 напрямую загружается значение 13, в то время как в варианте 1 значение загружается из памяти SRAM, это добавляет 2 байта. Пусть вас не смущает, что значение загружается как будто из адреса 0x0000, этот адрес здесь потому, что я разобрал не линкованный объект. Если разобрать blink1.elf, то адрес будет правильный - 0x0100.

Далее заглянем в loop и увидим, что в его начале и конце командами push и pop теребонькается стек. Кроме этого, происходит загрузка регистров 28 и 29 (строки 16-17). Нули здесь опять из-за того, что я разобрал не линкованный объект. Эти приколы добавляют еще 12 байт в наш многострадальный код. 

Однако код варианта 1 больше всех остальных на 1056-1018=38 байт, а мы нашли пока только 14. Придется дизассемблировать линкованные файлы, чтобы разобраться. Сделаем так:

1
2
/tmp/Stino_build$ avr-objdump -d blink1/blink1.elf > blink1.txt
/tmp/Stino_build$ avr-objdump -d blink2/blink2.elf > blink2.txt

Не буду приводить всю портянку кода, органичусь лишь тем куском, в котором разница

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
blink1/blink1.elf:     file format elf32-avr
 
 
000000b8 <__ctors_end>:
  b8:	11 24       	eor	r1, r1
  ba:	1f be       	out	0x3f, r1	; 63
  bc:	cf ef       	ldi	r28, 0xFF	; 255
  be:	d8 e0       	ldi	r29, 0x08	; 8
  c0:	de bf       	out	0x3e, r29	; 62
  c2:	cd bf       	out	0x3d, r28	; 61
 
000000c4 <__do_copy_data>:
  c4:	11 e0       	ldi	r17, 0x01	; 1
  c6:	a0 e0       	ldi	r26, 0x00	; 0
  c8:	b1 e0       	ldi	r27, 0x01	; 1
  ca:	ee e1       	ldi	r30, 0x1E	; 30
  cc:	f4 e0       	ldi	r31, 0x04	; 4
  ce:	02 c0       	rjmp	.+4      	; 0xd4 <__do_copy_data+0x10>
  d0:	05 90       	lpm	r0, Z+
  d2:	0d 92       	st	X+, r0
  d4:	a2 30       	cpi	r26, 0x02	; 2
  d6:	b1 07       	cpc	r27, r17
  d8:	d9 f7       	brne	.-10     	; 0xd0 <__do_copy_data+0xc>
 
000000da <__do_clear_bss>:
  da:	21 e0       	ldi	r18, 0x01	; 1
  dc:	a2 e0       	ldi	r26, 0x02	; 2
  de:	b1 e0       	ldi	r27, 0x01	; 1
  e0:	01 c0       	rjmp	.+2      	; 0xe4 <.do_clear_bss_start>
 
000000e2 <.do_clear_bss_loop>:
  e2:	1d 92       	st	X+, r1
 
000000e4 <.do_clear_bss_start>:
  e4:	ab 30       	cpi	r26, 0x0B	; 11
  e6:	b2 07       	cpc	r27, r18
  e8:	e1 f7       	brne	.-8      	; 0xe2 <.do_clear_bss_loop>
  ea:	0e 94 00 02 	call	0x400	; 0x400 <main>
  ee:	0c 94 0d 02 	jmp	0x41a	; 0x41a <_exit>

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
blink2/blink2.elf:     file format elf32-avr
 
000000b8 <__ctors_end>:
  b8:	11 24       	eor	r1, r1
  ba:	1f be       	out	0x3f, r1	; 63
  bc:	cf ef       	ldi	r28, 0xFF	; 255
  be:	d8 e0       	ldi	r29, 0x08	; 8
  c0:	de bf       	out	0x3e, r29	; 62
  c2:	cd bf       	out	0x3d, r28	; 61
 
000000c4 <__do_clear_bss>:
  c4:	21 e0       	ldi	r18, 0x01	; 1
  c6:	a0 e0       	ldi	r26, 0x00	; 0
  c8:	b1 e0       	ldi	r27, 0x01	; 1
  ca:	01 c0       	rjmp	.+2      	; 0xce <.do_clear_bss_start>
 
000000cc <.do_clear_bss_loop>:
  cc:	1d 92       	st	X+, r1
 
000000ce <.do_clear_bss_start>:
  ce:	a9 30       	cpi	r26, 0x09	; 9
  d0:	b2 07       	cpc	r27, r18
  d2:	e1 f7       	brne	.-8      	; 0xcc <.do_clear_bss_loop>
  d4:	0e 94 ee 01 	call	0x3dc	; 0x3dc <main>
  d8:	0c 94 fb 01 	jmp	0x3f6	; 0x3f6 <_exit>

Видно, что в варианте 1 добавилась целая секция __do_copy_data. Этот код копирует данные из памяти Flash в память SRAM. Эта секция увеличила код еще на 22 байта. Осталось найти еще два. Ну, собственно, прежде чем попасть в SRAM значение нашей переменной led должно храниться где-то во Flash. Дизассемблируем все секции blink1.elf, а не только исполняемые:

1
/tmp/Stino_build$ avr-objdump -D blink1/blink1.elf > blink1D.txt

И сразу находим то, что нужно:

1
2
3
4
Disassembly of section .data:
 
00800100 <__data_start>:
  800100:	0d 00       	.word	0x000d	; ????

  В секции .data хранятся глобальные переменные, которые инициализируются не нулями. У нас было написано

1
int led = 13;

Переведем 13 в шестнадцатеричный вид, получим 0x000d. Это как раз то, что мы нашли в секции .data.

 

Резюмируем.

Базовый пример blink.ino отстойный. Он использует глобальную переменную там, где это не нужно, занимая лишнее место в SRAM и добавляя постоянные операции со стеком. Мы выяснили, что прямое написание значения агрументов pinMode и digitalWrite, макроопределение и определение константы равнозначно с точки зрения скомпилированного бинарника.  С точки зрения написания и чтения скетча, конечно, лучше использовать макроопределение.

Теперь ответим на вопрос из топика: как надо было написать blink.ino? Хоть как, кроме того, как это сделано в примере :D

 

 

Метки: программирование, Arduino, код, хардкор, asm

Добавить комментарий


Защитный код
Обновить