Pull to refresh

Маленький Hello World для маленького микроконтроллера — в 24 байта (и чужое решение в 12 байт)

Reading time 7 min
Views 35K
Классической тестовой программой для большинства программистов на системах, имеющих хоть какой-то дисплей, является Hello World. Такая традиция была введена Керниганом и Ритчи в 1978 году.

Для микроконтроллеров аналогичным примером уже давно стала программа, которая мигает светодиодом. В этой статье я покажу результат эксперимента по максимальному сокращению такой программы на примере контроллера ATTiny15 фирмы Атмел.

image

UPD: В комментариях привели ссылку на рекордное решение в 12 байт. Браво!
UPD2: Путем насилия над контролером, удалось выиграть еще 2 байта.
UPD3: И еще одно решение, с еще большим насилием над контроллером.
UPD4: Еще один вариант — в одну инструкцию (но исполняется при этом вся память программ), как и в вариантах 2 и 3.
UPD5: Вариант с использованием возможности выдать тактовый генератор на один из пинов контроллера, при помощи FUSE-бита


О контроллере


Возможности контроллера невелики. Тактовая частота — 1.6МГц от внутреннего генератора. Свободных выводов, которые можно использовать без отказа от возможности аппаратного сброса и перепрошивки по SPI — всего пять. Имеется два таймера и АЦП. Память — 64 байта EEPROM. ОЗУ нет, только 32 регистра общего назначения и стек, глубина которого не может быть больше трех. AVR-GCC отказывается работать с таким контроллером — предлагает использовать ассемблер.

image

Инструментарий


Операционная система — Open Suse Linux 13.1. Среда разработки — AVR Studio 4.12, выполняется под Wine. Программатор — USBASP под управлением AVRDUDE. Программатор непосредственно соединен с контроллером, давая ему питание и во время прошивки, и во время экспериментов.

Проблема — программатор держит RESET


Сигнал сброса у контроллера инвертирован — высокий уровень означает нормальную работу, низкий — сброс. Сигнал RESET у практически всех AVR подключается через первую ножку контроллера. Кроме возможности сбросить контроллер, этот сигнал играет определяющую роль в процессе внутрисхемного программирования, поэтому заводится на программатор. Путем перенастройки FUSE битов, имеется возможность превратить этот вывод контроллера в вывод общего назначения — контроллер более не будет сбрасываться по сигналу с него, но и не будет программироваться без так называемого высоковольтного программатора.

USBASP все время держит на этом выводе низкий уровень, не давая контроллеру работать, пока подключен программатор. Для удобства отладки нужно либо программно (разобравшись в API USBASP), или аппаратно иметь возможность поднимать контроллеру RESET. Я выбрал аппаратный вариант в виде переключателя, как самый легко достижимый.

image

Мигаем светодиодом


Даже такая простая вещь, как выдача прямоугольных импульсов на определенную ножку контроллера при работе с ассемблером превращается в увлекательный поиск оптимального решения. Несомненным плюсом будет то, что программист не зависит от прихотей операционной системы и компилятора, поэтому может управлять оборудованием так, как считает нужным.

Банальная реализация задержки состоит в организации цикла из пустых инструкций, в котором будет вертеться контроллер между переключениями пина. Это классический delay_ms, столь любимый ардуинщиками. Минусов тут два как минимум: короткий минус — контроллер не получится усыпить, длинный — контроллер не сможет выполнять другие задачи. Действительно, любое прерывание приведет к тому, что тщательно высчитанное для задержки количество тактов ее уже не обеспечит: время, проведенное контроллером в обработчике добавится ко времени задержки. Можно, конечно, в обработчике предусмотреть коррекцию, а потом коррекцию коррекции (если в обработчике есть ветвление с ветвями разной длины) — в итоге получится весьма непростой код, о доказательстве корректности которого говорить достаточно тяжело.

Именно поэтому любые задержки стоит реализовывать на таймерах. У контроллера ATTiny15 таких таймеров два. Оба таймера могут считать до 255, после чего выдавать запрос на прерывание. Источником тактов для таймеров может служить внешний сигнал или делитель внутреннего тактового генератора. Делитель позволяет получать дробные частоты от тактовой — F/1, F/8, F/64, F/256, F/1024. По умолчанию контроллер работает на частоте 1.6МГц, если использовать делитель на 1024, получим частоту приращения таймера 1562,5Гц. Так как прерывание таймер будет выдавать на каждый 256-ой инкремент, мы получим дополнительное деление на 256 и итоговую частоту около 6Гц. Вполне приемлемо для мигания светодиодом.

Первая версия прошивки — по всем правилам хорошего тона


В начале прошивки у всех AVR должны находиться инструкции перехода к обработчикам прерываний. Это очень похоже на состояние дел у x86, но у них в начале памяти хранятся не инструкции, а адреса обработчиков.

У ATTiny15 таких инструкций должно быть девять. Первый обработчик, например — обработчик сброса, фактически — точка входа в прошивку.

rjmp reset
reti
reti
reti
reti
rjmp timer0
reti
reti
reti


На месте отсутствующих обработчиков стоят инструкции возврата из прерывания. Но они никогда вызваны не будут — соответствующие прерывания запрещены. То есть эти четыре штуки reti после rjmp reset нужны только для того, чтобы rjmp timer0 оказалась на своем месте.

Работа программы состоит в настройке предделителя для таймера,
	ldi r31,(1<<cs00 ) | (1<<cs02)
	out tccr0,r31


разрешении прерывания от таймера,
ldi r31,1<<toie0
out timsk,r31


разрешении обработки прерываний процессором,
sei


переключении вывода 7 контроллера в режим push-pull,
ldi r31,0b100
out ddrb,r31


и зависания в бесконечном цикле
	lp:
		rjmp lp


Обработка прерывания состоит в инверсии регистра и выводе этого значения на ножку контроллера
timer0:
	com r31
	out portb,r31
	reti


После сборки получили 40 байт машинного кода:

image

Сокращаем программу


Идея лежит на поверхности — так как из прерываний работают только сброс и таймер, контроллер никогда не окажется на других ячейках таблицы прерываний — займем эти ячейки кодом:

.include "tn15def.inc"

//вместо переходов на обработку прерываний сразу помещаем 4 инструкции
	ldi r31,(1<<cs00 ) | (1<<cs02)
	out tccr0,r31
	ldi r31,1<<toie0
	out timsk,r31
//но обработчик таймера надо оставить на своем месте
rjmp reset
rjmp timer0

//обработчик сброса (продолжение)
reset:
	
//разрешение обработки прерываний
	 sei
	//запись в регистр битовой маски, которая при работе программы будет инвертироваться
	//при переполнении таймера и выдаваться в порт B
	ldi r31,0b100
	//переключение режима порта B, пин 2 (счет с 0) на вывод push-pull
	//даташит, страница 51
	out ddrb,r31

	//бесконечный цикл
	lp:
		rjmp lp
//обработчик прерывания  по переполнению таймера 0, вызывается с частотой около 6Гц
timer0:	
	com r31 			//инверсия битов r31
	out portb,r31		//выдача значения в порт - либо высокий уровень, либо низкий
	reti				//возврат из прерывания	


image

Получили 26 байт, можем еще?

Можем!

Разместим обработчик прерывания от таймера сразу на своем месте, избавившись от перехода и сэкономив целых два байта:

ldi r31,(1<<cs00 ) | (1<<cs02)
out tccr0,r31
ldi r31,1<<toie0
sbr timsk,toie0
//перепрыгиваем обработчик прерывания при переполнении таймера 0
rjmp reset
//обработчик прерывания  по переполнению таймера 0, вызывается с частотой около 6Гц
com r31 //инверсия битов r31
out portb,r31	//выдача значения в порт - либо высокий уровень, либо низкий
reti	//возврат из прерывания
//обработчик сброса (продолжение)
reset:
//разрешение обработки прерываний
sei
//запись в регистр битовой маски, которая при работе программы будет инвертироваться
//при переполнении таймера и выдаваться в порт B
ldi r31,0b100
//переключение режима порта B, пин 2 (счет с 0) на вывод push-pull
//даташит, страница 51
out ddrb,r31
//бесконечный цикл
lp:
rjmp lp	


После сборки получилось 24 байта:

image

Сбережем немножко энергии ценой всего шести байт


Так как контроллер у нас кроме обработки прерывания ничем не занимается, стоит усыпить ядро процессора, оставив при этом таймер работающим. Это делает инструкция SLEEP, которая управляется флагами SE, SM1, SM0 регистра MCUCR. Режимов сна у контроллера несколько, начиная от самого глубокого Power Down, при котором контроллер может разбудить только сторожевой таймер, сброс или изменение состояния вывода, и заканчивая ожиданием, при котором ядро остановлено, но таймеры, АЦП и некоторая другая переферия — работают. Это тот режим, который нам подходит, он задается, если стоит только флаг SE.

Важно помнить, что после пробуждения и обработки прерывания контроллер попытается исполнить следующую после SLEEP инструкцию, значит усыпление необходимо зациклить:

.include "tn15def.inc"

//подготовка таймера 0 - выбор источника тактирования
	//источник - тактовый генератор 1.6 МГц с делителем на 1024
	//дает инкремент таймера каждый 1024 такт
	//выполняется путем записи в регистр TCCR0 комбинации флагов CS00 и CS02
	//даташит, страница 27, таблица 9
	ldi r31,(1<<cs00 ) | (1<<cs02)
	out tccr0,r31
	
	//Разрешение обработки прерывания по переполнению таймера 0
	//выполняется путем записи в регистр TIMSK флага TOIE0
	//даташит, страница 20
	ldi r31,1<<toie0
	out timsk,r31
	//перепрыгиваем обработчик прерывания при переполнении таймера 0
	rjmp reset


////////////////////////////////////////////////////////
//обработчик прерывания по переполнению таймера 0, вызывается с частотой около 6Гц
	com r31 			//инверсия битов r31
	out portb,r31		//выдача значения в порт - либо высокий уровень, либо низкий
	reti				//возврат из прерывания
////////////////////////////////////////////////////////


//обработчик сброса (продолжение)
reset:
	//запись в MCUCR флага разрешения перехода в спящий режим
	ldi r31,(1<<SE)
	out mcucr,r31

	//разрешение обработки прерываний
	sei

	//запись в регистр битовой маски, которая при работе программы будет инвертироваться
	//при переполнении таймера и выдаваться в порт B
	ldi r31,0b100
	//переключение режима порта B, пин 2 (счет с 0) на вывод push-pull
	//даташит, страница 51
	out ddrb,r31

	//бесконечный цикл сна
	lp:		
		sleep	
		rjmp lp	
	



image

Программирование на ассемблере является наиболее трудоемким способом получения программ, оптимизированных по размеру и скорости выполнения. Наиболее интересная задача здесь — разработка такого компилятора и ЯВУ, который бы выдавал оптимизированный код (по какому-то критерию) и доказательство того, что на данном устройстве получить лучший код — невозможно.

В следующей статье — конвертер USART <-> 1wire на основе этого же контроллера.

Интересно почитать


Проект на гитхабе;
Эдсгер Дейкстра. Избранные статьи;
Даташит на контроллер;
Электроника для всех.

UPD: В комментариях привели ссылку на рекордное решение в 12 байт.
Коротко — идея состоит в использовании сторожевого таймера и того факта, что РОН при сбросе от него не обнуляются.
LDI R16,(1<<WDE) | (1<<WDP2) | (1<<WDP1)
OUT WDTCR,R16
OUT DDRB,R16
COM R17
OUT PORTB,R17
L:  RJMP L


Здесь можно конечно же прицепиться к тому, что частота не та и импульсы идут на три других вывода, но по размеру это несомненный рекорд. Поздравляю!

В соединении с усыплением контроллера сторожевой таймер — мощное средство для реализации энергосбережения. Причем вместе с ним можно использовать самый глубокий сон — Power Down. Ценой дополнительных четырех байт сэкономим пару микроватт:

  LDI R16,(1<<WDE) | (1<<WDP2) | (1<<WDP1)
  OUT WDTCR,R16
  ldi r16,(1<<SM1) | (1<<SE) 
  out mcucr,r16
  OUT DDRB,R16
  COM R17
  OUT PORTB,R17
  SLEEP


И да, можно поступить совсем по-свински — убрать из рекорда финальное зацикливание, получив решение в 10 байт.

UPD: В комментариях привели ссылку на рекордное решение в 12 байт. Браво!
UPD2: Путем насилия над контролером, удалось выиграть еще 2 байта.
UPD3: И еще одно решение, с еще большим насилием над контроллером.
UPD4: Еще один вариант — в одну инструкцию (но исполняется при этом вся память программ), как и в вариантах 2 и 3.
Tags:
Hubs:
+56
Comments 41
Comments Comments 41

Articles