Как надо было написать 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:
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:
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:
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:
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 больше других, а все остальные одинаковые. Проверим, что они совсем одинаковые:
/tmp/Stino_build$ diff -s blink2/blink2.hex blink3/blink3.hex
Files blink2/blink2.hex and blink3/blink3.hex are identical
/tmp/Stino_build$ diff -s blink3/blink3.hex blink4/blink4.hex
Files blink3/blink3.hex and blink4/blink4.hex are identical
Давайте дизассемблируем скетчи 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. Придется дизассемблировать линкованные файлы, чтобы разобраться. Сделаем так:
/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, а не только исполняемые:
/tmp/Stino_build$ avr-objdump -D blink1/blink1.elf > blink1D.txt
И сразу находим то, что нужно:
Disassembly of section .data:
00800100 <__data_start>:
800100: 0d 00 .word 0x000d ; ????
В секции .data хранятся глобальные переменные, которые инициализируются не нулями. У нас было написано
Переведем 13 в шестнадцатеричный вид, получим 0x000d. Это как раз то, что мы нашли в секции .data.
Резюмируем.
Базовый пример blink.ino отстойный. Он использует глобальную переменную там, где это не нужно, занимая лишнее место в SRAM и добавляя постоянные операции со стеком. Мы выяснили, что прямое написание значения агрументов pinMode и digitalWrite, макроопределение и определение константы равнозначно с точки зрения скомпилированного бинарника. С точки зрения написания и чтения скетча, конечно, лучше использовать макроопределение.
Теперь ответим на вопрос из топика: как надо было написать blink.ino? Хоть как, кроме того, как это сделано в примере :D
Метки: программирование, Arduino, код, хардкор, asm