AVR: Учимся писать компактный код.
В свое время, когда я только осваивал AVR, великое зло по имени ардуйно меня обошло стороной, за что я очень благодарен судьбе. Но время идет, и волею судьбы мне довелось познакомиться не столько с ардуйной, сколько с ее фанатами, которые после прочтения этой статьи меня, должно быть, возненавидят.
Не останусь в долгу и я, резонно заметив, что большинство проектов ардуинщиков где используются atmega1280 или 2560 (кто больше?) реализуемы если не на 555 таймере, то уж на аттайни2313.
Скажу более, это не единственная беда, частенько люди настолько загоняют себя в рамки той же ардуйны, что выходит весьма идиотский подход: не хватает ардуйны для решения задачи? Возьми две ардуйны. Не хватает шести? Возьми десять!
Если сказанное вызывает уже ненависть ко мне, я попал в точку, дальнейшее можно не читать.
Что не так с ардуйно?
Не так крайне многое. Своя нумерация пинов, кривое апи, и многое другое. Будучи средой для чайников ардуйна легко загоняет новичка в свои рамки и вызыввает священный ужас при одной мысли о выходе куда-то за пределы, где нет привычных скетчей и есть страшный СИ.
При том, что реально код они уже пишут на С/С++ многие ардуинщики и не знают и честно говорят, что пишут на ардуйно. Это вызывает иногда улыбку, примерно как когда человек говорит, что пишет на ассеблере masm. Они здорово напоминают мне приверженцев одного такого язычка с решеткой в названии. ( С#) Оный тоже до железок добрался, так что ждем новой волны зеленых «погромистов».
Ну и, наконец, это С++. Его ниша никак не системное программирование. Здесь царство чистого СИ. Впрочем недостатки языка я не обсуждаю, скажу только, что при использовании С++ размер прошивки той же аврки пухнет как на дрожжах.
Но мы сейчас не ругаем ардуйно, а ищем конкретные выходы из положения, так ведь? Хочется и писать компактно, и чтобы удобно было.
Производители микроконтроллеров идут навстречу и в некоторых мк AT91RM9200, Stellaris Есть ROM с готовым набором костылей. В атмелевском варианте полезные штуки типа реализации xmodem’a, а у стеллариса вообще API, которое позволяет забыть что в регистры микроконтроллера надо что-то писать.
Что остается делать вымирающим фанатам красивого и компактного кода?
— Писать на ассемблере (круто, оптимально, позволяет познать дзен и научиться вселенскому терпению), но долго и не особо переносимо.
— Пользовать библиотеки типа avrlib, да таскать с собой по проектам вагон разных костылей, которые допиливать по мере необходимости.
— Или применять изыски для автогенерации кусков кода.
Про автогенерацию кода я скажу пару слов потом, когда закончу причесывать свою библиотеку для этих целей.
Сейчас же я рассмотрю парочку трюков, которые я использую для уменьшения размера кода. Начнем с простого.
Все ардуинщики знают штуку, по имени attachInterrupt(). Суть сего костыля проста. По вызову аттач интеррапт мы приклеиваем нашу собственную функцию-обработчик к тому или иному прерыванию. Причем, можем сделать это из кода. С одной стороны это хорошо, красиво и универсально, а с другой стороны…
Сколько раз в маленькой прошивке мк нам надо изменять функцию-обработчик того или иного прерывания? Можете не напрягаться, не часто. А теперь давайте оценим оверхед такой универсальности. Так как таблица векторов прерываний в AVR зашивается в Flash, то изменять ее из кода крайне нежелательно. (Можно, конечно, извратиться и перезаписывать флеш, но это скоро убьет его, так как количество циклов перезаписи ограничено).
А ардуине это реализовано так. Отдельный массив указателей в ОЗУ, куда attachInterrupt кидает указатель на функцию, которая будет вызываться. То есть как бы еще одна таблица векторов прерываний.
Иными словами, происходит нечто вроде этого:
поступило внешнее прерывание -> контроллер скакнул по адресу указанному в зашитой во флеше таблице векторов прерываний -> ардуиновская функция-обработчик загрузила в регистр адрес обработчика выставленного через attachInterrupt -> прыжок к обработчику -> мы обработали прерывание.
Давайте оценим потраченное на это место. Хотя бы для вызова функции по указателю. Для удобства сравнения перед вызовом функции ставим nop’ы.
Для теста набросаем вот такой файлик.
void (*fptr)(void)=0;
int main()
{
asm("nop");
fptr();
asm("nop");
}
Соберем его под архитектуру авр.
avr-gcc test.c -o out.elf
И отдизассемблируем полученную эльфятину.
avr-objdump -S out.elf
Это ассемблерный код, который нам сварганил gcc. Внимание на секцию мейн.
00000054 
:
54:   df 93           push    r29
56:   cf 93           push    r28
58:   cd b7           in      r28, 0x3d       ; 61
5a:   de b7           in      r29, 0x3e       ; 62
5c:   00 00           nop
5e:   80 91 60 00     lds     r24, 0x0060
62:   90 91 61 00     lds     r25, 0x0061
66:   e8 2f           mov     r30, r24
68:   f9 2f           mov     r31, r25
6a:   09 95           icall
6c:   00 00           nop
6e:   cf 91           pop     r28
70:   df 91           pop     r29
72:   08 95           ret
Аккурат между двумя нопами инструкции, которые вызывают функцию по указателю. Запомним, и сварганим другой пример.
int callme()
{
}
int main()
{
asm("nop");
callme();
asm("nop");
}
Собираем elf, дизассемблируем, получаем:
00000072 
:
72:   df 93           push    r29
74:   cf 93           push    r28
76:   cd b7           in      r28, 0x3d       ; 61
78:   de b7           in      r29, 0x3e       ; 62
7a:   00 00           nop
7c:   eb df           rcall   .-42            ; 0x54 
7e:   00 00           nop
80:   cf 91           pop     r28
82:   df 91           pop     r29
84:   08 95           ret
Как видно, разница одна инструкция против пяти. Четыре уходят на загрузку адреса. то есть 8 байт против двух.
Реально, конечно, оверхед не слишком большой, если функцию по указателю дергаем не так часто. Но не забываем, что каждая объявленная функция — это затраты на то, чтобы отправить в стек содержимое регистров и потом их оттуда достать. именно это и делает код в том же самом мейне, не относящийся к nop’ам.
Но не все же мы будем пихать в мейн, так? Тут опять есть выход.
Инлайны. Инлайн фунция, не заставляет компилятор загнать все регистры в стек, прыгнуть к коду, а потом вновь достать регистры из стека и вернуться при помощи ret. Она просто подставляет ее текст в место, где это надо.
В каких случаях это оптимально?
Смотрим дизасм  функции callme состоящей из одного единственного nop и офигеваем.
00000054 
54:   df 93           push    r29
56:   cf 93           push    r28
58:   cd b7           in      r28, 0x3d       ; 61
5a:   de b7           in      r29, 0x3e       ; 62
5c:   00 00           nop
5e:   cf 91           pop     r28
60:   df 91           pop     r29
62:   08 95           ret
На таком тесте видно, что это уже куча 14 байт оверхеда. Мелочь, если у нас сотни килобайт памяти, и в тоже время это ценное место, если у нас памяти кот наплакал — 1кб или 512 байт, на самых маленьких мк.
Но этого маловато, чтобы писать оптимально. Даже если выкинуть ардуйну, что лучше делать как можно раньше, код надо будет пытаться ужимать. Ниже привожу еще один жутко полезный трюк.
Инициализация переменных.
Допустим у нас есть массив чисел. Все числа надо инициализировать нулем, допустим в функции сброса, если они там что-то считают.
Любой программист, увидев, текст вроде:
char a[3];
a[0]=0;
a[1]=0;
a[2]=0;
Скажет китайский код, надо делать в цикле. А будет ли это реально меньше занимать памяти во флеше?
Давайте проверим.
Создаем массивчик:
char a[COUNT];
В мейн пихаем примерно такое:
int main()
{
reset();
}
А в виде reset’a делаем два разных варианта.
reset()
{
a[0]=0;
a[1]=0;
a[2]=0;
}
и
reset()
{
int i;
for (i=0; i<3; i++)
	{
	a[i]=0;
	}
}
И что имеем на выходе?
Для начала дизассемблируем эльфятину с «китайским кодом» и видим вот такое:
00000054 
54:   df 93           push    r29
56:   cf 93           push    r28
58:   cd b7           in      r28, 0x3d       ; 61
5a:   de b7           in      r29, 0x3e       ; 62
5c:   10 92 60 00     sts     0x0060, r1
60:   10 92 61 00     sts     0x0061, r1
64:   10 92 62 00     sts     0x0062, r1
68:   cf 91           pop     r28
6a:   df 91           pop     r29
6c:   08 95           ret
То есть 3 инициализации нулем превратились в 3 sts инструкции.
А теперь вариант с for
00000054 
54:   df 93           push    r29
56:   cf 93           push    r28
58:   00 d0           rcall   .+0             ; 0x5a 
5a:   cd b7           in      r28, 0x3d       ; 61
5c:   de b7           in      r29, 0x3e       ; 62
5e:   1a 82           std     Y+2, r1 ; 0x02
60:   19 82           std     Y+1, r1 ; 0x01
62:   0c c0           rjmp    .+24            ; 0x7c 
64:   89 81           ldd     r24, Y+1        ; 0x01
66:   9a 81           ldd     r25, Y+2        ; 0x02
68:   80 5a           subi    r24, 0xA0       ; 160
6a:   9f 4f           sbci    r25, 0xFF       ; 255
6c:   e8 2f           mov     r30, r24
6e:   f9 2f           mov     r31, r25
70:   10 82           st      Z, r1
72:   89 81           ldd     r24, Y+1        ; 0x01
74:   9a 81           ldd     r25, Y+2        ; 0x02
76:   01 96           adiw    r24, 0x01       ; 1
78:   9a 83           std     Y+2, r25        ; 0x02
7a:   89 83           std     Y+1, r24        ; 0x01
7c:   89 81           ldd     r24, Y+1        ; 0x01
7e:   9a 81           ldd     r25, Y+2        ; 0x02
80:   83 30           cpi     r24, 0x03       ; 3
82:   91 05           cpc     r25, r1
84:   7c f3           brlt    .-34            ; 0x64 
86:   0f 90           pop     r0
88:   0f 90           pop     r0
8a:   cf 91           pop     r28
8c:   df 91           pop     r29
8e:   08 95           ret
На этом месте тихонечко офигеваем: цикл нам обошелся в 40 байт или 20 инструкций. То есть до 20 элементов массива выгоднее инициализировать китайским кодом.
Смотрим сильно ли изменится если элементами будут 16ти битные числа:
00000054 
54:   df 93           push    r29
56:   cf 93           push    r28
58:   cd b7           in      r28, 0x3d       ; 61
5a:   de b7           in      r29, 0x3e       ; 62
5c:   10 92 61 00     sts     0x0061, r1
60:   10 92 60 00     sts     0x0060, r1
64:   10 92 63 00     sts     0x0063, r1
68:   10 92 62 00     sts     0x0062, r1
6c:   10 92 65 00     sts     0x0065, r1
70:   10 92 64 00     sts     0x0064, r1
74:   cf 91           pop     r28
76:   df 91           pop     r29
78:   08 95           ret
В два раза больше sts при китайском коде. А теперь если воткнем цикл.
00000054 
54:   df 93           push    r29
56:   cf 93           push    r28
58:   00 d0           rcall   .+0             ; 0x5a 
5a:   cd b7           in      r28, 0x3d       ; 61
5c:   de b7           in      r29, 0x3e       ; 62
5e:   1a 82           std     Y+2, r1 ; 0x02
60:   19 82           std     Y+1, r1 ; 0x01
62:   0f c0           rjmp    .+30            ; 0x82 
64:   89 81           ldd     r24, Y+1        ; 0x01
66:   9a 81           ldd     r25, Y+2        ; 0x02
68:   88 0f           add     r24, r24
6a:   99 1f           adc     r25, r25
6c:   80 5a           subi    r24, 0xA0       ; 160
6e:   9f 4f           sbci    r25, 0xFF       ; 255
70:   e8 2f           mov     r30, r24
72:   f9 2f           mov     r31, r25
74:   11 82           std     Z+1, r1 ; 0x01
76:   10 82           st      Z, r1
78:   89 81           ldd     r24, Y+1        ; 0x01
7a:   9a 81           ldd     r25, Y+2        ; 0x02
7c:   01 96           adiw    r24, 0x01       ; 1
7e:   9a 83           std     Y+2, r25        ; 0x02
80:   89 83           std     Y+1, r24        ; 0x01
82:   89 81           ldd     r24, Y+1        ; 0x01
84:   9a 81           ldd     r25, Y+2        ; 0x02
86:   83 30           cpi     r24, 0x03       ; 3
88:   91 05           cpc     r25, r1
8a:   64 f3           brlt    .-40            ; 0x64 
8c:   0f 90           pop     r0
8e:   0f 90           pop     r0
90:   cf 91           pop     r28
92:   df 91           pop     r29
94:   08 95           ret
Вариант с for тоже распух прилично. Таким образом можно сделать простой вывод: Если надо инициализировать что-то нулем, а памяти мало, то выгоднее набросать себе маленький костыль на ассемблере.
Пока это все примеры, которые я хотел раписать тут. Дальше мне заниматься этим было лениво.
Уменьшить размер прошивки и занимаемой оперативной памяти можно так же выгружая лишние данные в eeprom и progmem, но расписывать это мне, честно говоря, сейчас лениво.
Заключение.
Как видно из примеров, писать код занимающий мало места, требует знаний конкретной архитектуры, да и вообще штука это нетривиальная, но жутко интересная. Это вам не ПокажиСообщение(«Невосстановимая ошибка базы данных 1С»);