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

в 13:36, , рубрики: avr, ненормальное программирование, программирование микроконтроллеров

Где предел минимального 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			; Сложение регистровой пары [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

Источник

Поделиться

* - обязательные к заполнению поля