Tag: asm

  • AVR: Учимся писать компактный код

    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С”);

  • Очешуеваем.

    Не, господа… это трындец полный. мало мне работ, научки, учебы и поделок для себя, так еще и это…
    В общем спустил зарплату на 900 грамм хлорного железа, стеклотекстолит, пачку attiny2313 и atmega16 + кучу обвязки к ним. Об этом не жалею… теперь остается поднять свои наработки для at91rm9200, и нарисовать разводку экстеншн борды для управления двигателями…
    Хорошо хоть герметичный аккум от бесперебойника и схема заряда оного у меня есть.
    Кстати, никто не подскажет дешевый и сердитый аудио кодек?