...

понедельник, 13 октября 2014 г.

[Из песочницы] Маленький 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<<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 на основе этого же контроллера.


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




Проект на гитхабе;

Эдсгер Дейкстра. Избранные статьи;

Даташит на контроллер;

Электроника для всех.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий