[Из песочницы] Маленький Hello World для маленького микроконтроллера — в 24 байта

Классической тестовой программой для большинства программистов на системах, имеющих хоть какой-то дисплей, является Hello World. Такая традиция была введена Керниганом и Ритчи в 1978 году.Для микроконтроллеров аналогичным примером уже давно стала программа, которая мигает светодиодом. В этой статье я покажу результат эксперимента по максимальному сокращению такой программы на примере контроллера ATTiny15 фирмы Атмел.

image

О контроллереВозможности контроллера невелики. Тактовая частота — 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<

ldi r31,1<

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<

//обработчик сброса (продолжение) 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<

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<

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

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

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

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

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

image

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

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

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

© Habrahabr.ru