[Из песочницы] Тюнинг toolchain для Arduino для продолжающих
Давным-давно случилось мне поработать над проектом с Arduino, где были довольно специфические требования к предсказуемости генерации кода, а работать с чёрным ящиком местами раздражало. Так родилась идея несколько поднастроить процесс сборки и внедрить некоторые дополнительные шаги при сборке.
Как известно, язык Wiring на самом деле является полноценным C++, из которого убрана поддержка исключений ввиду их тяжеловесности для среды выполнения и спрятаны некоторые тонкости, связанные со структурой программы, такие как реализация функции main, а разработчику отданы функции setup и loop, которые вызываются из main.
А это означает, что можно пользоваться плюшками C++, даже если о них никто не рассказал.
Всё описанное в статье относится к версиям среды Arduino как минимум от 1.6. Я делал всё под Windows, но для для Linux/MacOS подходы остаются теми же, просто меняются технические детали.
Итак, приступаем.
В среде версии 1.6 по умолчанию используется компилятор GCC версии 4.8, в котором по умолчанию не включена поддержка С++11, но нам же хочется auto, varadic templates и прочие плюшки, не правда ли? А в среде версии 1.8 компилятор уже версии 4.9, поддерживающий С++11 по умолчанию, но и его можно заставить использовать С++14 с его rvalue reference, константными выражениями, разделителями разрядов, двоичными литералами и прочими радостями.
Чтобы добавить поддержку более новых стандартов, чем включены по умолчанию, надо в каталоге установки Arduino открыть файл hardware\arduino\avr\platform.txt
, содержащий настройки компиляции, найти там параметр compiler.cpp.flags
и указать желаемый стандарт языка: -std=gnu++11
заменить на -std=gnu++14
. Если параметра нету — добавить, например в конце строки.
После сохранения файла можно попробовать пересобрать скетч — среда подхватит новые настройки сразу же.
Здесь же можно изменить настройки оптимизации, например вместо более компактного кода изменением опции -Os
на -Ofast
включить генерацию более быстрого кода.
Эти настройки глобальные, поэтому применимы в рамках всех проектов.
Дальше я решил получить более детальный контроль над результатом работы компилятора и добавил некоторый аналог postbuild-событий. Для этого в тот же файл надо добавить параметр recipe.hooks.postbuild.0.pattern
, значение которого должно содержать имя файла, который будет запущен после сборки, и аргументы командной строки. Я присвоил ему значение "{compiler.path}stat.bat" "{build.path}/{build.project_name}"
. Поскольку такого файла пока нет, его надо создать в каталоге hardware\tools\avr\bin
(с именем, очевидно, stat.bat
).
Напишем туда следующие строки:
"%~dp0\avr-objdump.exe" -d -C %1.elf > %1.SX
"%~dp0\avr-nm.exe" %1.elf -nCS > %1.names
Первая строка запускает декомпиляцию уже собранной и оптимизированной программы с помощью objdump, создавая выходной файл с именем проекта и дополнительным расширением .SX (выбор произвольный, но будем считать, что S — традиционно ассемблер, а X указывает на факт некоторого преобразования).
Вторая строка извлекает символьные имена областей памяти запуском команды nm и формирует отчёта в файле с дополнительным расширением .names, при этом сортировка выполняется по адресам.
Здесь надо ещё добавить, что сборка ведётся во временном каталоге, и чтобы его посмотреть, нужно зайти в настройки Arduino и включить чекбокс «Показать подробный вывод» → «Компиляция», после чего запустить эту самую компиляцию и посмотреть, где идёт сборка. В моём случае (IDE версии 1.8.5) это был каталог вида %TEMP%\arduino_build_[random]
.
Читать дизассемблированный код в обычной жизни — удовольствие сомнительное, но может кому-то понадобится (мне доводилось).
Файл имён же в целом более полезен. В спойлере — тестовый скетч, с которым выполнялись опыты:
int counter = 1;
void test(auto&& x) {
static unsigned long time;
long t = millis();
Serial.print(F("Counter: "));
Serial.println(counter);
Serial.print(F(" At "));
Serial.print(t);
Serial.print(F(" value is "));
Serial.println(x);
Serial.print(F("Time between iterations: "));
Serial.println(t - time);
Serial.println();
time = t;
}
void setup() {
Serial.begin(9600);
}
void loop() {
delay(100);
test(digitalRead(A0));
}
Получившийся файл имён целиком тоже под спойлером:
w serialEvent()
00000000 W __heap_end
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 W __vector_default
00000000 T __vectors
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
00000068 T __trampolines_end
00000068 T __trampolines_start
00000068 0000001a t test(int)::__c
00000082 0000000b t test(int)::__c
0000008d 00000005 t test(int)::__c
00000092 0000000a t test(int)::__c
0000009c 00000014 T digital_pin_to_timer_PGM
000000b0 00000014 T digital_pin_to_bit_mask_PGM
000000c4 00000014 T digital_pin_to_port_PGM
000000d8 0000000a T port_to_input_PGM
000000e2 T __ctors_start
000000e4 T __ctors_end
000000e4 T __dtors_end
000000e4 T __dtors_start
000000e4 W __init
000000f0 00000016 T __do_copy_data
00000106 00000010 T __do_clear_bss
0000010e t .do_clear_bss_loop
00000110 t .do_clear_bss_start
00000116 00000016 T __do_global_ctors
00000134 T __bad_interrupt
00000134 W __vector_1
00000134 W __vector_10
00000134 W __vector_11
00000134 W __vector_12
00000134 W __vector_13
00000134 W __vector_14
00000134 W __vector_15
00000134 W __vector_17
00000134 W __vector_2
00000134 W __vector_20
00000134 W __vector_21
00000134 W __vector_22
00000134 W __vector_23
00000134 W __vector_24
00000134 W __vector_25
00000134 W __vector_3
00000134 W __vector_4
00000134 W __vector_5
00000134 W __vector_6
00000134 W __vector_7
00000134 W __vector_8
00000134 W __vector_9
00000138 00000012 T setup
0000014a 000000d8 T loop
00000222 00000094 T __vector_16
000002b6 00000018 T millis
000002ce 00000046 T micros
00000314 00000050 T delay
00000364 00000076 T init
000003da 00000052 t turnOffPWM
0000042c 00000052 T digitalRead
0000047e 00000016 T HardwareSerial::available()
00000494 0000001c T HardwareSerial::peek()
000004b0 00000028 T HardwareSerial::read()
000004d8 000000b8 T HardwareSerial::write(unsigned char)
00000590 000000a4 T HardwareSerial::flush()
00000634 0000001c W serialEventRun()
00000650 00000042 T HardwareSerial::_tx_udr_empty_irq()
00000692 000000d0 T HardwareSerial::begin(unsigned long, unsigned char)
00000762 00000064 T __vector_18
000007c6 0000004c T __vector_19
00000812 00000014 T Serial0_available()
00000826 00000086 t _GLOBAL__sub_I___vector_18
000008ac 00000002 W initVariant
000008ae 00000028 T main
000008d6 00000058 T Print::write(unsigned char const*, unsigned int)
000008ff W __stack
0000092e 0000004a T Print::print(__FlashStringHelper const*)
00000978 00000242 T Print::print(long, int)
00000bba 00000016 T Print::println()
00000bd0 0000029a T Print::println(int, int)
00000e6a 0000013a T Print::println(unsigned long, int)
00000fa4 00000002 t __empty
00000fa4 00000002 W yield
00000fa6 00000044 T __udivmodsi4
00000fb2 t __udivmodsi4_loop
00000fcc t __udivmodsi4_ep
00000fea 00000004 T __tablejump2__
00000fee 00000008 T __tablejump__
00000ff6 T _exit
00000ff6 W exit
00000ff8 t __stop_program
00000ffa A __data_load_start
00000ffa T _etext
00001016 A __data_load_end
00800100 D __data_start
00800100 00000002 D counter
00800102 00000010 V vtable for HardwareSerial
0080011c B __bss_start
0080011c D __data_end
0080011c D _edata
0080011c 00000004 b test(int)::time
00800120 00000001 b timer0_fract
00800121 00000004 B timer0_millis
00800125 00000004 B timer0_overflow_count
00800129 0000009d B Serial
008001c6 B __bss_end
008001c6 N _end
00810000 N __eeprom_end
Как это читать? Давайте разбираться.
В контроллерах AVR две независимые области памяти: память программы и память данных. Соотвественно, указатель на код и указатель на данные — это не одно и то же. Про EEPROM пока молчу, там отдельный разговор про адресацию. В дампе память программы расположена по смещению 0 и с него начинается выполнение, и там же располагается таблица векторов прерываний, по 4 байта на элемент. В отличие от x86, каждый вектор прерываний содержит не адрес обработчика, а инструкцию, которую надо выполнить. Обычно (я не видел других вариантов) — jmp, то есть переход в нужный адрес. К векторам прерываний мы ещё потом ненадолго вернёмся, а пока смотрим дальше.
Память данных состоит из двух частей: регистровый файл со смещения 0 и данные в оперативной памяти (SRAM) со смещения 0×100. Когда выводится дамп памяти, то к адресам SRAM добавляется смещение 0×00800000, которое ничего не значит по сути, но просто удобно. Отмечу также, что смещение EEPROM имеет значение 0×00810000, но нам это не нужно.
Давайте начнём с данных, тут всё немного проще и привычнее, а потом вернёмся к коду. Первый столбец — смещение, второй — размер, дальше тип (пока не обращаем внимание), дальше до конца строки — имя.
Идём на смещение 0×00800100 и видим там записи:
00800100 D __data_start
00800100 00000002 D counter
00800102 00000010 V vtable for HardwareSerial
0080011c D __data_end
Это аналог секции .data и содержит инициализированные глобальные и статические переменные, а также таблицы виртуальных методов. Эти переменные будут проинициализированы bootstrap-кодом, что займёт некоторое время.
Дальше идём (пускай вас не смущает, что __bss_start и __data_end идут в произвольном порядке — они имеют общий адрес).
0080011c B __bss_start
0080011c 00000004 b test(int)::time
00800120 00000001 b timer0_fract
00800121 00000004 B timer0_millis
00800125 00000004 B timer0_overflow_count
00800129 0000009d B Serial
008001c6 B __bss_end
Это секция неинициализированных данных (аналог .bss). Она даётся даром, поскольку заполнена нулевыми байтами. Здесь несколько вспомогательных переменных, имена который начинаются с timer0_, глобальный объект Serial и наша статическая локальная переменная.
Маленькое лирическое отступление: обратите внимание, что объект Serial изначально пуст, и он будет проинициализирован, когда на нём будет вызван метод begin. Для этого есть несколько причин. Во-первых, он может вообще не использоваться (тогда он вообще не будет создан и место под него выделяться не будет), а во-вторых, мы же знаем, что порядок инициализации глобальных объектов не определён, и до входа в main строго говоря контроллер не инициализирован корректно.
По более высоким адресам располагается стек. Если объём памяти контроллера 2 килобайта (платы на базе процессора atmega328), то последний адрес будет 0×008008FF. С этого адреса начинается стек и растёт он в сторону уменьшения адресов. Нет никаких барьеров, и в принципе стек может наползти на переменные, испортив их. Или данные могут испортить стек — как повезёт. В этом случае ничего хорошего не произойдёт. Можно это назвать неопределённым поведением, но лучше всегда в памяти оставлять достаточно места для стека. Сколько — сложно сказать. Чтобы хватило на локальные переменные и кадры стека всех вызовов плюс для самого тяжёлого обработчика прерывания со всеми вложенными вызовами. Я, честно говоря, более-менее честного решения относительно того, как диагностировать использование стека, не нашёл.
Давайте вернёмся к коду. Когда я говорил, что там хранится код, я немного лукавил. На самом деле там могут храниться и данные, доступные только для чтения, но извлекаются они оттуда специальной инструкцией процессора LPM. Так вот, константные данные можно размещать там и размещать даром, потому как на их инициализацию не тратится время. Обратите внимание на макросы F (…) в коде. Они как раз объявляют строки размещёнными в программной памяти и дальше делает немножко магии. Не будем вдаваться в подробности магии, а поищем их в памяти. Такие макросы идут в функции test, и в дампе мы их увидим как
00000068 0000001a t test(int)::__c
00000082 0000000b t test(int)::__c
0000008d 00000005 t test(int)::__c
00000092 0000000a t test(int)::__c
Несмотря на то, что они имеют одинаковые имена, они находятся по разным адресам. Потому что макрос F (…) создаёт область видимости в фигурных скобках, и переменные (точнее константы) друг другу не мешают. Если посмотреть дизассемблерный дамп, то это и будут адреса строк в программной памяти.
И напоследок обещанное о прерываниях. Библиотеки используют некоторые ресурсы контроллера для поддержки работы периферии: таймеры, коммуникационные интерфейсы и т.п. Они регистрируют свои обработчики прерывания, хранят в памяти свои переменные, а в программной памяти — константы. Если скомпилировать пустой скетч, то дамп памяти будет такой:
w serialEventRun()
00000000 W __heap_end
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 a __tmp_reg__
00000000 W __vector_default
00000000 T __vectors
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
00000001 a __zero_reg__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003d a __SP_L__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003e a __SP_H__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
0000003f a __SREG__
00000068 T __ctors_end
00000068 T __ctors_start
00000068 T __dtors_end
00000068 T __dtors_start
00000068 W __init
00000068 T __trampolines_end
00000068 T __trampolines_start
00000074 00000010 T __do_clear_bss
0000007c t .do_clear_bss_loop
0000007e t .do_clear_bss_start
0000008c T __bad_interrupt
0000008c W __vector_1
0000008c W __vector_10
0000008c W __vector_11
0000008c W __vector_12
0000008c W __vector_13
0000008c W __vector_14
0000008c W __vector_15
0000008c W __vector_17
0000008c W __vector_18
0000008c W __vector_19
0000008c W __vector_2
0000008c W __vector_20
0000008c W __vector_21
0000008c W __vector_22
0000008c W __vector_23
0000008c W __vector_24
0000008c W __vector_25
0000008c W __vector_3
0000008c W __vector_4
0000008c W __vector_5
0000008c W __vector_6
0000008c W __vector_7
0000008c W __vector_8
0000008c W __vector_9
00000090 00000002 T setup
00000092 00000002 T loop
00000094 00000002 W initVariant
00000096 00000028 T main
000000be 00000094 T __vector_16
00000152 00000076 T init
000001c8 T _exit
000001c8 W exit
000001ca t __stop_program
000001cc A __data_load_end
000001cc a __data_load_start
000001cc T _etext
000008ff W __stack
00800100 B __bss_start
00800100 D _edata
00800100 00000001 b timer0_fract
00800101 00000004 B timer0_millis
00800105 00000004 B timer0_overflow_count
00800109 B __bss_end
00800109 N _end
00810000 N __eeprom_end
Немало, неправда ли?
А вот код:
/empty.cpp.elf: file format elf32-avr
Disassembly of section .text:
00000000 <__vectors>:
0: 0c 94 34 00 jmp 0x68 ; 0x68 <__ctors_end>
4: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
8: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
c: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
10: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
14: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
18: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
1c: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
20: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
24: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
28: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
2c: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
30: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
34: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
38: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
3c: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
40: 0c 94 5f 00 jmp 0xbe ; 0xbe <__vector_16>
44: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
48: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
4c: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
50: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
54: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
58: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
5c: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
60: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
64: 0c 94 46 00 jmp 0x8c ; 0x8c <__bad_interrupt>
00000068 <__ctors_end>:
68: 11 24 eor r1, r1
6a: 1f be out 0x3f, r1 ; 63
6c: cf ef ldi r28, 0xFF ; 255
6e: d8 e0 ldi r29, 0x08 ; 8
70: de bf out 0x3e, r29 ; 62
72: cd bf out 0x3d, r28 ; 61
00000074 <__do_clear_bss>:
74: 21 e0 ldi r18, 0x01 ; 1
76: a0 e0 ldi r26, 0x00 ; 0
78: b1 e0 ldi r27, 0x01 ; 1
7a: 01 c0 rjmp .+2 ; 0x7e <.do_clear_bss_start>
0000007c <.do_clear_bss_loop>:
7c: 1d 92 st X+, r1
0000007e <.do_clear_bss_start>:
7e: a9 30 cpi r26, 0x09 ; 9
80: b2 07 cpc r27, r18
82: e1 f7 brne .-8 ; 0x7c <.do_clear_bss_loop>
84: 0e 94 4b 00 call 0x96 ; 0x96
88: 0c 94 e4 00 jmp 0x1c8 ; 0x1c8 <_exit>
0000008c <__bad_interrupt>:
8c: 0c 94 00 00 jmp 0 ; 0x0 <__vectors>
00000090 :
90: 08 95 ret
00000092 :
92: 08 95 ret
00000094 :
94: 08 95 ret
00000096 :
96: 0e 94 a9 00 call 0x152 ; 0x152
9a: 0e 94 4a 00 call 0x94 ; 0x94
9e: 0e 94 48 00 call 0x90 ; 0x90
a2: 80 e0 ldi r24, 0x00 ; 0
a4: 90 e0 ldi r25, 0x00 ; 0
a6: 89 2b or r24, r25
a8: 29 f0 breq .+10 ; 0xb4
aa: 0e 94 49 00 call 0x92 ; 0x92
ae: 0e 94 00 00 call 0 ; 0x0 <__vectors>
b2: fb cf rjmp .-10 ; 0xaa
b4: 0e 94 49 00 call 0x92 ; 0x92
b8: 0e 94 49 00 call 0x92 ; 0x92
bc: fb cf rjmp .-10 ; 0xb4
000000be <__vector_16>:
be: 1f 92 push r1
c0: 0f 92 push r0
c2: 0f b6 in r0, 0x3f ; 63
c4: 0f 92 push r0
c6: 11 24 eor r1, r1
c8: 2f 93 push r18
ca: 3f 93 push r19
cc: 8f 93 push r24
ce: 9f 93 push r25
d0: af 93 push r26
d2: bf 93 push r27
d4: 80 91 01 01 lds r24, 0x0101
d8: 90 91 02 01 lds r25, 0x0102
dc: a0 91 03 01 lds r26, 0x0103
e0: b0 91 04 01 lds r27, 0x0104
e4: 30 91 00 01 lds r19, 0x0100
e8: 23 e0 ldi r18, 0x03 ; 3
ea: 23 0f add r18, r19
ec: 2d 37 cpi r18, 0x7D ; 125
ee: 20 f4 brcc .+8 ; 0xf8 <__vector_16+0x3a>
f0: 01 96 adiw r24, 0x01 ; 1
f2: a1 1d adc r26, r1
f4: b1 1d adc r27, r1
f6: 05 c0 rjmp .+10 ; 0x102 <__vector_16+0x44>
f8: 26 e8 ldi r18, 0x86 ; 134
fa: 23 0f add r18, r19
fc: 02 96 adiw r24, 0x02 ; 2
fe: a1 1d adc r26, r1
100: b1 1d adc r27, r1
102: 20 93 00 01 sts 0x0100, r18
106: 80 93 01 01 sts 0x0101, r24
10a: 90 93 02 01 sts 0x0102, r25
10e: a0 93 03 01 sts 0x0103, r26
112: b0 93 04 01 sts 0x0104, r27
116: 80 91 05 01 lds r24, 0x0105
11a: 90 91 06 01 lds r25, 0x0106
11e: a0 91 07 01 lds r26, 0x0107
122: b0 91 08 01 lds r27, 0x0108
126: 01 96 adiw r24, 0x01 ; 1
128: a1 1d adc r26, r1
12a: b1 1d adc r27, r1
12c: 80 93 05 01 sts 0x0105, r24
130: 90 93 06 01 sts 0x0106, r25
134: a0 93 07 01 sts 0x0107, r26
138: b0 93 08 01 sts 0x0108, r27
13c: bf 91 pop r27
13e: af 91 pop r26
140: 9f 91 pop r25
142: 8f 91 pop r24
144: 3f 91 pop r19
146: 2f 91 pop r18
148: 0f 90 pop r0
14a: 0f be out 0x3f, r0 ; 63
14c: 0f 90 pop r0
14e: 1f 90 pop r1
150: 18 95 reti
00000152 :
152: 78 94 sei
154: 84 b5 in r24, 0x24 ; 36
156: 82 60 ori r24, 0x02 ; 2
158: 84 bd out 0x24, r24 ; 36
15a: 84 b5 in r24, 0x24 ; 36
15c: 81 60 ori r24, 0x01 ; 1
15e: 84 bd out 0x24, r24 ; 36
160: 85 b5 in r24, 0x25 ; 37
162: 82 60 ori r24, 0x02 ; 2
164: 85 bd out 0x25, r24 ; 37
166: 85 b5 in r24, 0x25 ; 37
168: 81 60 ori r24, 0x01 ; 1
16a: 85 bd out 0x25, r24 ; 37
16c: ee e6 ldi r30, 0x6E ; 110
16e: f0 e0 ldi r31, 0x00 ; 0
170: 80 81 ld r24, Z
172: 81 60 ori r24, 0x01 ; 1
174: 80 83 st Z, r24
176: e1 e8 ldi r30, 0x81 ; 129
178: f0 e0 ldi r31, 0x00 ; 0
17a: 10 82 st Z, r1
17c: 80 81 ld r24, Z
17e: 82 60 ori r24, 0x02 ; 2
180: 80 83 st Z, r24
182: 80 81 ld r24, Z
184: 81 60 ori r24, 0x01 ; 1
186: 80 83 st Z, r24
188: e0 e8 ldi r30, 0x80 ; 128
18a: f0 e0 ldi r31, 0x00 ; 0
18c: 80 81 ld r24, Z
18e: 81 60 ori r24, 0x01 ; 1
190: 80 83 st Z, r24
192: e1 eb ldi r30, 0xB1 ; 177
194: f0 e0 ldi r31, 0x00 ; 0
196: 80 81 ld r24, Z
198: 84 60 ori r24, 0x04 ; 4
19a: 80 83 st Z, r24
19c: e0 eb ldi r30, 0xB0 ; 176
19e: f0 e0 ldi r31, 0x00 ; 0
1a0: 80 81 ld r24, Z
1a2: 81 60 ori r24, 0x01 ; 1
1a4: 80 83 st Z, r24
1a6: ea e7 ldi r30, 0x7A ; 122
1a8: f0 e0 ldi r31, 0x00 ; 0
1aa: 80 81 ld r24, Z
1ac: 84 60 ori r24, 0x04 ; 4
1ae: 80 83 st Z, r24
1b0: 80 81 ld r24, Z
1b2: 82 60 ori r24, 0x02 ; 2
1b4: 80 83 st Z, r24
1b6: 80 81 ld r24, Z
1b8: 81 60 ori r24, 0x01 ; 1
1ba: 80 83 st Z, r24
1bc: 80 81 ld r24, Z
1be: 80 68 ori r24, 0x80 ; 128
1c0: 80 83 st Z, r24
1c2: 10 92 c1 00 sts 0x00C1, r1
1c6: 08 95 ret
000001c8 <_exit>:
1c8: f8 94 cli
000001ca <__stop_program>:
1ca: ff cf rjmp .-2 ; 0x1ca <__stop_program>
Задействовано прерывание таймера, 9 байт на обслуживание этого таймера, и некоторое количество инфраструктурного кода.
На сегодня, пожалуй, хватит. Надеюсь, кому-нибудь это пригодится.