[Из песочницы] Где предел минимального Hello World на AVR?
Предупреждение: В данной статье повсеместно используются грязные хаки. Её можно воспринимать только как пособие «как не надо делать»!
Как только я увидел статью «Маленький Hello World для маленького микроконтроллера — в 24 байта», то мой внутренний ассемблерщик наполнился негодованием: «Разве можно так разбрасываться драгоценными байтами?!». И хотя я давно перешёл на C, это не мешает в критических местах проверять быдлокод компилятора и, если всё плохо, то иногда можно слегка изменить C-код и получить заметный выигрыш в скорости и/или занимаемом месте. Либо просто переписать этот кусок на ассемблере.
Итак, условия нашей задачи:
AVR микроконтроллер, у меня больше всего в закромах оказалось ATMega48, пусть будет он; Тактирование от внутреннего источника. Дело в том, что внешне можно тактировать AVR со сколь угодно малой частотой, и это сразу переводит нашу задачу в разряд неспортивных; Мигаем светодиодом с различимой глазом частотой; Размер программы должен быть минимальным; Вся недюженная мощь микроконтроллера бросается на выполнение задачи. Для индикации подключим светодиод с резистором между шиной питания 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, 0×80; r16 = 0×80 start: adiw x, 1; Сложение регистровой пары [r24: r25] с 1 brcc start; Переход, если нет переноса
in r26, DDRB; r26 = DDRB eor r26, r16; r26 ^= r16 out DDRB, r26; DDRB = r26
rjmp start; goto start За неиспользованием прерываний расположим код прямо на месте таблицы оных, т. к. Reset приведёт нас к адресу 0×0000. При переходе x от значения 0xFFFF к 0×0000 взводятся флаги переноса (переполнения) C и флаг нулевого результата Z, можно отлавливать любой с помощью brne или brcc.У нас получилось 14 байт машинного кода и время выполнения цикла счётчика = 4 такта. Т. к. x у нас двухбайтная, полупериод мигания светодиода 65536×4 = 262144 тактов. Выберем внутренний таймер помедленнее, а именно RC-осциллятор 128 кГц. Тогда наш полупериод 262144 / 128000 = 2,048 с. Условия задачи выполнены, но размер прошивки явно можно уменьшить.
Во-первых, пожертвуем чтением состояния направления порта DDRB, зачем оно нам, мы и так знаем что всегда либо 0×00, либо 0×80. Да, так делать нехорошо, но здесь же у нас всё под контролем! А во-вторых, остальные выводы порта B ведь не используются, ничего страшного, если туда будет записываться мусор!
Обратим внимание на старший разряд переменной x: он меняется строго через 65536 / 2×4 = 131072 тактов. Ну так и выведем его старший полубайт xh, избавившись от внутреннего цикла и переменной r16:
start: adiw x, 1; Сложение регистровой пары [r24: r25] с 1 out DDRB, xh; DDRB = r25 rjmp start; goto start Прекрасно! Мы уложились в 6 байт! Подсчитаем тайминги: (2 + 1 + 2) * 65536 / 2 = 163840, значит светодиод будет мигать с полупериодом 163840 / 128000 = 1,28 с. Остальные ноги порта B будут дёргаться гораздо быстрее, на это мы просто закроем глаза.И на этом можно бы успокоиться, однако, настоящий ассемблерщик имеет в рукаве ещё более грязный трюк, чем все предыдущие вместе взятые! Почему бы нам не выбросить этот rjmp, занимающий (подумать только) треть программы?! Обратимся к глубинам. После стирания flash-памяти микроконтроллера все ячейки принимают значение 0xFF, т. е. после того, как процессор выходит за пределы программы ему попадаются исключительно инструкции 0xFFFF, они незадокументированы, но исполняются так же как и 0×0000 (nop), а именно, процессор не делает ничего, кроме увеличения регистра-указателя исполняемой инструкции (Program counter). После достижения оным предельного значения, в нашем случае это размер памяти программ 4096 — 1 = 4097, он переполняется и вновь становится равным 0, указывая на начало программы, куда и переходит исполнение! Теперь задержка будет определяться проходом по всей памяти программ, это 2048 двухбайтных инструкций, выполняющихся по одному такту. Поэтому возьмём однобайтную переменную-счётчик:
add r16, 1; 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 значительно преобладает.
Использование меча подразумевает предельную внимательность.
Спасибо за внимание!