Предупреждение: В данной статье повсеместно используются грязные хаки. Её можно воспринимать только как пособие «как не надо делать»!
Как только я увидел статью «Маленький 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, 0x80 ; r16 = 0x80
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 приведёт нас к адресу 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 ; Сложение регистровой пары [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, они незадокументированы, но исполняются так же как и 0x0000 (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 значительно преобладает.
Использование меча подразумевает предельную внимательность.
Спасибо за внимание!
Автор: jaiprakash