Pull to refresh

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

Reading time 5 min
Views 57K


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

Как только я увидел статью «Маленький 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 и землёй либо шиной питания, и «наслаждаемся» противным писком.
Tags:
Hubs:
+116
Comments 58
Comments Comments 58

Articles