Занимательная электроника — страница 105 из 128

* * *

Здесь rjmp — знакомая нам команда безусловного перехода, a RESET, EXT_INTO и т. п. — метки в тексте программы, откуда начинается текст процедуры (подпрограммы) обработки прерывания. Метки могут быть обозначены и по-иному, тут полный произвол. Сам переход осуществляется автоматически, если в процессе выполнения программы возникнут условия для возникновения соответствующего прерывания, для чего и его отдельно, и прерывания вообще надо еще разрешить.

Все прерывания и особенности их вызова мы рассматривать, конечно, не будем, а некоторые из них рассмотрим далее на практике.

Сначала заметим, что подобным образом должна выглядеть программа, если вы используете все 19 прерываний, чего на самом деле, конечно, не бывает. Но каждой записи rjmp <метка> должно соответствовать наличие метки далее в тексте, иначе ассемблер укажет на ошибку (посылать по неизвестному адресу нехорошо!). Поэтому если некоторые прерывания не используются, то, вообще говоря, можно вместо безусловных переходов наставить в соответствующих строках команд nор (nо operation — ее код равен просто нулям во всех разрядах), однако на практике вместо них чаще ставят команду reti, которая означает возврат из процедуры прерывания к выполнению основной программы, если вдруг оно возникнет.

* * *

Заметки на полях

На самом деле в общем случае не имеет значения, какую именно команду в этих строках ставить — выполняться они никогда не будут, если соответствующие прерывания не активированы, нам только нужно занять память, чтобы команда rjmp используемого нами прерывания оказалась по нужному адресу, — например, можно поставить команду rjmp $0000. Есть и другие, более корректные способы размещения команд по конкретным адресам памяти (с помощью директивы .org), но не будем усложнять. Кстати, надо учесть, что данный способ относится, строго говоря, лишь к МК с памятью не более 8 Кбайт, уже для ATmega16 команды будут другими: вместо 2-байтовой rjmp там применяется 4-байтовая jmp (а вместо команды вызова подпрограммы rcall — call).

* * *

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



Если указатель стека не установить, то прерывания не заработают. Установка указателя стека для ряда моделейTiny (в которых отсутствует SRAM) не требуется, а вот для других, в том числе и всех Mega, где количество SRAM превышает 256 байтов, эта загрузка будет протекать иначе, т. к. константа RAMEND там размером больше байта:



* * *

Подробности

Как мы уже отмечали в главе 18, стек — область памяти, куда будут записываться адреса точек возврата при вызове подпрограмм по команде rcall или перехода к обработчикам прерываний. По окончании процедуры обработки прерывания (по команде reti) или обычной подпрограммы (по команде ret) этот адрес считывается в программный счетчик, и выполнение основной программы продолжается. Если во время выполнения обработчика данного прерывания происходит другое прерывание с большим приоритетом (это нужно специально разрешать, по умолчанию в AVR любое прерывание будет ожидать окончания обработки предыдущего), то в стек записывается также и этот текущий адрес команды, таким образом ошибиться при последовательном возврате невозможно. Стек устроен по принципу «последним вошел — первым вышел», т. е. извлекается всегда последнее записанное туда значение. Программист может использовать стек и для своих целей (командами push и pop — например, для сохранения текущего значения рабочих регистров), но неопытным программистам лучше активно этим способом не пользоваться — вероятность допустить трудно обнаруживаемую ошибку значительно возрастает. Тем более что в AVR и без того достаточно РОН (в отличие, например, от х51, где без стека обойтись практически невозможно), а при необходимости можно еще и хранить текущие значения в SRAM.


Прерывание таймера по переполнению

С учетом всего сказанного напишем программу, переключающую светодиод. В данном случае она будет это делать по событию переполнения таймера-счетчика Timer 1 (вектор у нас обозначен: TIM1_OVF). Так как счетчик 16-разрядный, то событие переполнения будет возникать при каждом 65 536-м импульсе входной частоты. Если мы зададим коэффициент деления тактовой частоты на входе Timer 1 равным 64, то при 4 МГц частоты генератора мы получим примерно 1 Гц: 4 000 000/64/65 536 = 0,953674 Гц.

Это не совсем то, что нам требуется, и к тому же частота неточно равна одному герцу. Для того чтобы светодиод переключался точно раз в полсекунды (т. е. период его был равен секунде), программу придется немного усложнить, загружая каждый раз в счетные регистры определенное значение, которое рассчитывается просто: если период одного тактового импульса таймера равен 16 мкс (частота 4 000 000/64), то для получения 500 000 микросекунд надо отсчитать таких импульсов 31 250. Так как счетчик суммирующий, а прерывание возникает при достижении числа 65 536, то нужно предварительно загружать в него необходимое число 65 536 — 31250 = 34 286.

Это не единственный способ, но наиболее универсальный, годящийся для всех таймеров. Кстати, именно таким способом реализован отсчет времени в Arduino (см. главу 21). Иной способ — использовать прерывание по достижению определенного числа, загруженного в регистр сравнения А или В. Как это делается, мы увидим далее в этой главе. Для того чтобы осуществить само переключение из красного в зеленый, нам придется поступить как раньше, т. е. по каждому событию переполнения перебрасывать два бита в регистре PortD.

Полностью программа тогда будет выглядеть так:





Я не буду комментировать подробно каждый оператор, т. к. это заняло бы слишком много места. После выполнения всех команд начальной установки МК зацикливается, но бесконечный цикл будет прерываться возникновением прерывания — здесь все аналогично операционной системе Windows, которая также представляет собой бесконечный цикл ожидания событий. Как вы узнаете из последующих глав, в Arduino такой цикл — одна из главных составляющих любой программы, как раз потому что прерывания там почти не используются. Внутрь бесконечного цикла здесь можно поставить знакомую команду sleep, без дополнительных настроек режима энергопотребления она будет экономить около 30 % питания. А вот сэкономить еще больше просто так не получится, поскольку придется останавливать процессорное ядро, и таймер перестанет работать.

* * *

Заметки на полях

Кстати, а как остановить запущенный таймер, если это потребуется? Очень просто: если обнулить регистр TCCR1B (тот, в котором задается коэффициент деления тактовой частоты), то таймер остановится. Чтобы запустить его опять с коэффициентом 1/64, нужно снова записать в этот регистр значение 0b00000011.

* * *

Обратите внимание, что оператор reti (окончание обработки прерывания) при обработке прерывания таймера встречается дважды — это вполне нормальный прием, когда подпрограмма разветвляется. Можно, конечно, и пометить последний оператор reti меткой, и тогда текст процедуры стал бы неотличим от первого варианта, но так будет корректнее.

Обратите также внимание на форму записи ldi temp, (1 << TOIE1). Поскольку бит, обозначаемый как TOIE1, в регистре TIMSK имеет номер 7, то эта запись эквивалентна записи ldi temp,0b10000000 — можно писать и так, и так, и еще кучей разных способов. Например, для запуска таймера с коэффициентом 1/64 требуется, как видно из текста программы, установить младшие два бита регистра TCCR1B. Здесь мы устанавливаем их в temp напрямую, но поскольку эти биты называются CS11 и CS10, то можно записать так:

ldi temp, (1 << CS11) I (1 << CS10)

или даже так:

ldi temp, (3 << CS10)

Подробно этот способ записи приведен в описании AVR-ассемблера на сайте Atmel[35].

* * *

Подробности

В этой программе есть один тонкий момент, связанный с загрузкой счетных регистров таймера. При чтении и записи 16-разрядных регистров Timer 1 их содержимое может измениться в промежутке между чтением или записью отдельных 8-разрядных «половинок» (ведь, например, в данном случае таймер продолжает считать, пока идет обработка прерывания). Потому в 16-разрядных таймерах AVR предусмотрен специальный механизм чтения и записи таких регистров. При записи первым загружается значение старшего байта, которое автоматически помещается в некий (недоступный для программиста) буферный регистр. Затем, когда поступает команда на запись младшего байта, оба значения объединяются, и запись производится одновременно в обе «половинки» 16-разрядного регистра. Наоборот, при чтении первым должен быть прочитан младший байт, при этом значение старшего автоматически фиксируется помещением в тот же буферный регистр, и при следующей операции чтения старшего байта его значение извлекается оттуда. Таким образом и при чтении значения оба байта соответствуют одному и тому же моменту времени.