Юрий Ревич - Занимательная электроника
* * *
Подробности
Если сравнить таблицы прерываний «классического» AT90S2313 и ATtiny2313, то окажется, что первые 11 векторов у них полностью совпадают. Отсутствующие в «классическом» аналоге остальные 8 векторов можно просто проигнорировать: если соответствующие прерывания не задействованы, то к ним никогда не произойдет обращения. По этой причине программы, написанные для AT90S2313, почти без оговорок совместимы с ATtiny2313 (не требуется даже заменять файл определений констант 2313def.inc). В дальнейшем мы будем без пояснений употреблять программы для обеих версий этого МК.
* * *
Здесь 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 меткой, и тогда текст процедуры стал бы неотличим от первого варианта, но так будет корректнее.