Где предел минимального Hello World на AVR?



Предупреждение: В данной статье повсеместно используются грязные хаки. Её можно воспринимать только как пособие «как не надо делать»!

Как только я увидел статью «Маленький Hello World для маленького микроконтроллера — в 24 байта», то мой внутренний ассемблерщик наполнился негодованием: «Разве можно так разбрасываться драгоценными байтами?!». И хотя я давно перешёл на C, это не мешает в критических местах проверять быдлокод компилятора и, если всё плохо, то иногда можно слегка изменить C-код и получить заметный выигрыш в скорости и/или занимаемом месте. Либо просто переписать этот кусок на ассемблере.

Итак, условия нашей задачи:

  1. AVR микроконтроллер, у меня больше всего в закромах оказалось ATMega48, пусть будет он;
  2. Тактирование от внутреннего источника. Дело в том, что внешне можно тактировать AVR со сколь угодно малой частотой, и это сразу переводит нашу задачу в разряд неспортивных;
  3. Мигаем светодиодом с различимой глазом частотой;
  4. Размер программы должен быть минимальным;
  5. Вся недюженная мощь микроконтроллера бросается на выполнение задачи.


Для индикации подключим светодиод с резистором между шиной питания VCC и выводом B7 нашей маленькой меги.

Писать будем в AVR Studio.

Дабы не бросаться сразу в дебри asm'а, приведём сперва очевидный псевдокод на C:

int main(void)
{
volatile uint16_t x;

	while (1) {					// Бесконечный цикл
		while (++x)				// Задержка
			;
		DDRB ^= (1 << PB7);			// Изменение состояния вывода B7 на противоположное
	}
}

Так как нам не нужно отвлекаться на другие задачи, то использование таймеров явно избыточно. Обычная для GCC функция задержки _delay_us() имеет в основе нечто похожее на приведённый здесь внутренний цикл while. Мы сразу же обошлись с переменной x нехорошо — мы делаем цикл на основе её переполнения, что в реальных задачах недопустимо.

Заглядываем в листинг, ужасаемся расточительности компилятора и создаём проект на основе ассемблера. Выкинем лишнее из наваянного компилятором, остаётся:

	.include "m48def.inc"		; Используем ATMega48

	.CSEG				; Кодовый сегмент

	ldi		r16, 0x80		; r16 = 0x80
start:
	adiw	x, 1			; Сложение регистровой пары [r26:r27] с 1
	brcc	start			; Переход, если нет переноса

	in		r28, DDRB		; r28 = DDRB
	eor		r28, r16		; r28 ^= r16
	out		DDRB, r28		; DDRB = r28

	rjmp	start			; goto start

За неиспользованием прерываний расположим код прямо на месте таблицы оных, т. к. Reset приведёт нас к адресу 0x0000. При переходе x от значения 0xFFFF к 0x0000 взводятся флаги переноса (переполнения) C и флаг нулевого результата Z, можно отлавливать любой с помощью brne или brcc.

У нас получилось 14 байт машинного кода и время выполнения цикла счётчика = 4 такта. Т. к. x у нас двухбайтная, полупериод мигания светодиода 65536 * 4 = 262144 тактов. Выберем внутренний таймер помедленнее, а именно RC-осциллятор 128 кГц. Тогда наш полупериод 262144 / 128000 = 2,048 с. Условия задачи выполнены, но размер прошивки явно можно уменьшить.

Во-первых, пожертвуем чтением состояния направления порта DDRB, зачем оно нам, мы и так знаем что там всегда либо 0x00, либо 0x80. Да, так делать нехорошо, но здесь же у нас всё под контролем! А во-вторых, остальные выводы порта B ведь не используются, ничего страшного, если туда будет записываться мусор!

Обратим внимание на старший разряд переменной x: он меняется строго через 65536 / 2 * 4 = 131072 тактов. Ну так и выведем в порт её старший байт xh, избавившись от внутреннего цикла и переменной r16:

start:
	adiw	x, 1			; Сложение регистровой пары [r26:r27] с 1
	out		DDRB, xh		; DDRB = r27
	rjmp	start			; goto start

Прекрасно! Мы уложились в 6 байт! Подсчитаем тайминги: (2 + 1 + 2) * 65536 / 2 = 163840, значит светодиод будет мигать с полупериодом 163840 / 128000 = 1,28 с. Остальные ноги порта B будут дёргаться гораздо быстрее, на это мы просто закроем глаза.

И на этом можно бы успокоиться, однако, настоящий ассемблерщик имеет в рукаве ещё более грязный трюк, чем все предыдущие вместе взятые! Почему бы нам не выбросить этот rjmp, занимающий (подумать только) треть программы?! Обратимся к глубинам. После стирания flash-памяти микроконтроллера все ячейки принимают значение 0xFF, т. е. после того, как процессор выходит за пределы программы ему попадаются исключительно инструкции 0xFFFF, они незадокументированы, но исполняются так же как и 0x0000 (nop), а именно, процессор не делает ничего, кроме увеличения регистра-указателя исполняемой инструкции (Program counter). После достижения оным предельного значения, в нашем случае это размер памяти программ 4096 − 1 = 4097, он переполняется и вновь становится равным 0, указывая на начало программы, куда и переходит исполнение! Теперь задержка будет определяться проходом по всей памяти программ, это 2048 двухбайтных инструкций, выполняющихся по одному такту. Поэтому возьмём однобайтную переменную-счётчик:

	inc		r16			; r16++
	out		DDRB, r16		; DDRB = r16

Или на C:

uint_8 b

	DDRB = ++b;

Полупериод мигания светодиодом составит 2048 * 256 / 2 = 262144 тактов или 2,048 с (как и в первом примере).

Итого, размер нашей программы 4 байта, она функциональна, однако, эта победа достигнута такой ценой, что нам стыдно смотреть в зеркало. К слову, размер первоначальной программы на C составил 110 байт с опцией компиляции -Os (быстрый и компактный код).

Выводы


Мы рассмотрели несколько способов выстрелить в ногу
Если вам становится тесно в рамках языка — спускайтесь на самый низ, здесь нет ничего сложного. Изучив, как работает процессор, становится гораздо проще и с языками верхнего уровня. Да, сейчас в моде повышение абстракции: фреймворки, линукс в кофеварке, даже встраиваемый x86, однако, ассемблер не собирается сдавать позиции в тех случаях, когда нужен жёсткий realtime, максимальная производительность, ограничены ресурсы и т. п. Несмотря на плохую переносимость (иногда даже внутри семейства), модифицируемость, лёгкость утратить понимание происходящего и сложность написания больших программ, на ассемблере вполне успешно пишутся быстрые и маленькие функции и вставки, и, похоже, из этой ниши его не выбить никогда! Хотя это касается в первую очередь эмбеддеров, а в жизни большинства x86-программеров ассемблер, в основном, встречается при отладке, выскакивая пугающим листингом.

Для меня холивара Asm vs C не существует, я применяю их вместе, при этом C значительно преобладает.

Использование меча подразумевает предельную внимательность.

Спасибо за внимание!

UPD1
Не поленился, залил в железо — да, так и работает!

UPD2
А вот так совсем никогда не делайте!
Ввиду того, что мысль сокращать программу дальше не покидает умы, продолжим.

Я сам не пробовал, но некоторые люди в интернете говорят, что если писать в регистр PINx, то значение PORTx изменится на противоположное (кроме самых старых микроконтроллеров AVR). Это значит, что между VCC и выводом подключается/отключается внутренний подтягивающий резистор.
Возьмём светодиод почувствительнее к малым токам и присоединим его между выводом B0 и землёй.
Запрограммируем фьюз CKDIV8, тактовая частота упадёт ещё в 8 раз — до 16 кГц. (Только теперь перепрограммировать микроконтроллер сможет не всякий программатор, например, оригинальный AVRISP mkII — может, а за его клоны не ручаюсь).
Доведём уже программу до 1 команды (2 байта):
	sbi		PINB, 0		; PINB = 0x01 или PORTB ^= 0x01

Прошиваем, и наблюдаем в темноте слабое мерцание. Частота 16000 / 2049 / 2 ≈ 4 Гц. Для микроконтроллера с бо́льшим объёмом flash-памяти, эта частота будет, соответственно меньше — вплоть до вполне такого мигания.

UPD3
Двигаемся дальше.
Может ли микроконтроллер AVR сигнализировать о своей работе вовсе без программы?
Конечно! Достаточно запрограммировать фьюз CKOUT, и тогда на пин CLKO (снова PB0) будет выдаваться сигнал тактового генератора, в т. ч. внутреннего, и если его частота уменьшена предделителем, то будет выводиться замедленный.
Так что стираем кристалл, не записываем нашу программу в 0 байт, прошиваем фьюзы. Но подавать 16 кГц на светодиод с резистором смысла мало, хотя мы и заметим что он засветился с половинной яркостью.
Однако, кроме визуального низкочастотного Hello World, есть высокочастотный аудиальный! Этот вариант, конечно, не соответствует нашему первоначальному ТЗ, но вполне сигнализирует о работе МК. Цепляем пьзоэлемент между выводом B0 и землёй либо шиной питания, и «наслаждаемся» противным писком.
Поделиться публикацией

Похожие публикации

Комментарии 58

    +15
    Мсье знает толк в извращениях оптимизациях…
      +9
      Дааа!

      Грязные хаки в продакшне — недопустимы. Но читать про них применимо к исследованию чего-либо… Истинное наслаждение. Спасибо.
        +3
        Ну как сказать недопустимы в продакшене… Бывают, знаете ли, ситуации, когда устройства уже готовы и вам нужно кровь из носу запихать прошивку в отведенные ей N килобайт ПЗУ контроллера.
        +11
        Любой продакшен состоит из трёх частей: говна, палок и интеллектуальной собственности. Палки из этих трёх — самые прочные и предсказуемые.
        0
        Так как нам не нужно отвлекаться на другие задачи, то использование таймеров явно избыточно.

        При несколько измененном ТЗ, как раз таймер очень уместен в подобной задаче: Мигаем светодиодом в AVR-бутлодере
          0
          Мигание таймером вполне допустимо по ТЗ, просто мне сразу показалось, что это будет занимать больше места.
          А если у нас какая-никакая многозадачность, и таймер больше нигде не нужен — то предпочтительнее задействовать его, конечно!
          +1
          Энергетически неэффективно, по сравнению с предыдущим автором :-)
            +3
            Есть такое ))) Искусство требует жертв ©.
            +4
            В последнем случае нужно считать, что размер программы — 4096 байт :)
              0
              Да, у нас в конкретном случае объём 4 KiB, взяв другой микроконтроллер с большей памятью программ мы увеличим период, однако остальные ноги порта B осциллируют с бо́льшими частотами, можем подпаяться к ним.
                +1
                А, не понял сразу))
                «Исполняться» будут все 4 KiB, просто AVR Studio выдаст: Размер CSEG = 4
                  0
                  То есть, имея МК с большей памятью, можно обойтись вообще одной командой?
                    0
                    Двумя — в порт можно выводить только РОН, а РОН при включении пусты, поэтому инструкций две:
                    inc r16
                    out ddrb,r16
                    

                    У МК есть еще конфигурационные биты, ими можно понизить или повысить тактовую частоту.
                      +1
                      Верно, запрограммировав фьюз CKDIV8, можно снизить тактовую частоту в 8 раз — до 16 кГц.
                        +1
                        Этот фьюз может здорово попить кровь, к примеру, на ATTiny2313 он выставлен по умолчанию, поэтому генератор вроде как работает на 8МГц, а контроллеру достается только один.
                          +2
                          Делать надо это аккуратно, от тактовой частоты работает схема внутрисхемного программирования — программатор тоже надо будет замедлять чтобы контроллер оставался видимым программатору.
                            0
                            Несомненно, например, AVRISP mkII поддерживает частоты программирования от 50 Гц, AVRDude тоже настраивается, говорят, что 16 кГц — не проблема.
                              0
                              Об этом просто надо помнить, ведь программатору надо сказать что работать нужно на пониженной частоте.
                                0
                                Помнить — хорошо, но не всегда получается )), тогда программатор начинает ругаться и память немедленно освежается )
                              +1
                              Классическая неприятность — запрограммироваться на тактирование от внешнего источника. После этого спасает или высоковольтный программатор, или этот самый внешний источник.

                              Это усугубляется следующим моментом:

                              Исторически, fuse обозначал пережигаемый бит в памяти контроллера и программировался ровно один раз, путем обнуления. Поэтому все даташиты atmel пестрят фразой «0 means programmed»

                              Разные утилиты-прошивальщики интерпретируют запрограммированный фьюз по-разному. Популярная PonyProg рисует галочку, если значение бита 1, то есть фьюз как раз не запрограммирован. От сюда путаница и куча окирпиченных контроллеров.
                      0
                      Нефига, мы ведь не писали все эти 4096 байт, да и прошивка контроллера заливаться будет всего 4 байта. А то что исполнятся будут все 4096 байт… на то он и хак!

                      Можно вообще сделать на 3 команды программу — инкрементируем регистр, выводим в порт и тут же зацикливаемся одной командой RJMP на её же. Устанавливаем бит конфигурации WDTON в активное состояние и наслаждаемся. Наш код будет выполняться каждые 16мс, 5-й бит порта будет мигать с периодом в 1 секунду.
                      Регистры по вачдогу не обнуляются, только состояние периферии поэтому каждое срабатывание вачдога будет изменение внутреннего состояния.

                      Хм. даже зацикливаться не надо, достаточно использовать команду SLEEP — заодно экономичная работа получится. Поскольку контроллер никогда не проснется… то никакого выполнения пустого кода не будет.
                        +2
                        Пардон, вы к посту про программу из двух команд пишете «Можно вообще сделать на 3 команды».
                          0
                          что-то мне показалось что там на 4 команды…
                          0
                          Дело в том, что использовать ту память, которая не занята нашей программой для чего-то другого все равно нельзя. Потому что она таки используется нашей программой, задействована под нашу программу

                          В общем, тут сложный вопрос
                            0
                            Как же нельзя? AVR прекрасно умеет читать/писать свою собственную память программ (а иначе бутлоадер не реализовать). Там можно данные хранить.
                              +1
                              Если туда что-то записать, то программа станет работать не так, как положено. В этом конкретном случае, имеется в виду.
                                +1
                                Ан нет — некоторые AVR можно перепрограммировать на старт не с 0x0000, в таком случае все что впереди — в нашем распоряжении.
                                  0
                                  Так в этом конкретном случае никто же не пишет в память программ.
                                  Или я не понял о чём вы?
                                    0
                                    Тут просто по дороге нашелся еще один косвенный способ управления частотой генерации для четырехбайтной прошивки — передвинуть старт, укоротив прогоняемый контроллером участок кода.
                                      +1
                                      Если записать последнюю прошивку 4 байта в область загрузчика, то это не повлияет на частоту — PC дойдёт до конца, переполнится, т. е. станет равен 0, снова дощёлкает до загрузчика и выполнит итерацию. Только «фаза» сместится, что мы даже не заметим.
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            В пересчете на байт, наверное, была самая дорогая программа в мире
                              +6
                              Нет, не угадали. habrahabr.ru/post/147075/ — бесконечно выгодная программа.
                              Так GO.COM, которая состояла из 0 байтов кода и продавалась по 5 фунтов за копию, оказалась самой прибыльной в мире — любая другая программа принесла куда меньше фунтов на байт!
                                +2
                                Это если цельная программа. А если программное решение, то у меня был случай, когда я в течении дня пялился в неработающий код, который ребята не могли заставить работать три дня, после чего удалил из него 4 буковки и всё заработало. В пересчёте на написанный байт моё исправление имело вообще отрицательную цену.
                                0
                                это был x86? джамп на ребут не может занимать 3 байта однако. адрес длиннее.
                                и опасно это — надо обязательно буфера smartdrv флашнуть на диск — он-то не в курсе
                                можно было через 19-ое прерывание (кажется 19-ое? я уже не помню), но тогда это не 3 байта
                                • НЛО прилетело и опубликовало эту надпись здесь
                                    +1
                                    вызвать прерывание… два байта — команда 1 байт и номер прерывания 1 байт.
                                    Как сейчас помню — байт-код 0xCDxx где XX — номер прерывания.
                                    18 или 19 прерывание… одно из них сброс, а другое зарезервировано за вызовом встроенного интерпретатора бейсика, но даже со времён XT бейсика там и в помине не было, поэтому прерывание выполняло функцию сброса системы…

                                    Но ИМХО сейчас на современных машинах это не прокатит, супервизор перехватит переход на прерывание и даст по рукам программе — «ай-яй-яй так делать нельзя!».
                                      0
                                      а я что сказал? то же самое, что это не три байта.
                                      кроме того, совершенно нет гарантии, что какой-то резидент не перехватывает это прерывание, либо просто вектор указывает куда-то в пустоту после того как кто-то затер системную память.
                                      так что перезагрузка таким образом не совсем надёжна.
                                        +1
                                        На современных системах НИ ОДИН способ программной перезагрузки не может быть абсолютно надёжным.
                                          0
                                          Полностью согласен — помогает только железный костыль в виде сторожевого таймера.
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                              0
                                              Старинный компьютер с LPT, 12 опторазвязок Sharp PC817 — и готова станция перезапуска/включения/наблюдения для четырех серверов.

                                              Такой себе iLO на коленке.
                                • НЛО прилетело и опубликовало эту надпись здесь
                                    0
                                    Любая программа содержит ошибки.
                                    Любую программу можно укоротить хотя бы на байт.

                                    Вывод: в результате оптимизации получаем программу длиной в 1 байт, содержащий ошибку.
                                      0
                                      В AVR команды имеют длину 2 или 4 байта, так что 1 байт не получится, зато можно 2.
                                      Добавил в статью.
                                        +1
                                        Нет, ошибся. Оставляем 2 команды.
                                      +1
                                      Ну, господа, грех не использовать хардварные регистры для битовых переменных, если сам функционал хардварный не используется. Доступ за один такт например, очень пользительно. Главное бит активации не трогать. Так можно использовать spi, twi, или usart.
                                      Даже в продакшене это не не кошерно.
                                      А есть еще кучка хинтов, и отнють не грязных. Просто не используемых…
                                        +1
                                        У AVR доступ ко всем РОН в один такт:

                                        Регистровый файл с битовым доступом содержит 32 восьмибитных РОН с доступом за один тактовый цикл:

                                        «The fast-access Register File concept contains 32 x 8-bit general purpose working registers with a single-clock-cycle access».
                                        //////// Даташит на ATTiny15, страница 5.
                                          +1
                                          Это у Вас переменных в программе мало. У меня вот регистры кончились на одном проекте, а дергать переменные в памяти накладно, пришлось регистры периферии запользовать, что сильно сказалось на размере и быстродействии. На 8й меге проект.
                                        +1
                                        Как автор публикации про 24 байта замечу, что мной и вторым автором — рекордсменом (12 байт) были показаны чистые способы экономии байт, которые таки годятся в продакшн.

                                        В очередной раз благодарю за проявленный интерес и усердие в исследованиях тонкостей ассемблера и AVR в частности.

                                        т. е. после того, как процессор выходит за пределы программы ему попадаются исключительно инструкции 0xFFFF, они незадокументированы


                                        0xFFFF = SBRS R31,7
                                          +1
                                          sbrs r, b
                                          Пропустить команду, если рязряд регистра общего назначения установлен.
                                          Если r.b = 1, то PC = PC + 2 (3), иначе PC = PC + 1
                                          Код операции: 1111 111r rrrr 0bbb

                                          Следовательно, sbrs r31, 7 это 0xFFF7.

                                          Возможно, процессор воспринимает 0xFFFF как эту команду, при r31.7 = 0 он просто переходит дальше.
                                          +2
                                          Во времена 16F84 был такой способ написать HelloWorld: изменялось состояние нужного пина на обратное и выставлялся WatchDog. Через какое-то время WatchDog сбрасывал и перезагружал процессор, то все опять с начала: изменили, заснули.
                                            0
                                              0
                                              Я тут выше хотел так же, только без WatchDog'а. Но оказалось, что поменять состояние пина на противоположное — это уже три команды (прочитать, поменять, записать).
                                                0
                                                Вообще-то… одна. Запись в порт PINx на новых контроллерах приводит к инверсии пина который попал в записанную маску. Это всего две команды. Но с вачдогом это не прокатит, каждый раз порт будет «обнуляться».

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое