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

С Таймером 1 и Таймером 2 связаны три вектора прерывания:

Прерывание переполнения таймера - Timer Overflow Interrupt (INT00,2000H);

Прерывание переполнения Таймера 2 - Timer 2 Overflow Interrupt (INT12,2038H);

Прерывание фиксатора Таймера 2 - Timer 2 Capture Interrupt (INT11,2036H).

Регистр IOS1 содержит флажки, которые указывают какое событие вызвало прерывание. Обращение к битам регистра IOS1 по командам JBC или JBS обнуляет биты 0-5. По этой причине, мы рекомендуем чтобы Вы копировали содержимое регистра IOS1 в промежуточный регистр и затем выполняли команды проверки разрядов типа JBC или JBS на промежуточном регистре.

1.3.1. Прерывание переполнения таймера

И Таймер 1 и Таймер 2 могут вызвать прерывание переполнения Таймера (INT00). Установите INT_MASK.0 ,чтобы разрешить это прерывание. Установите или IOC1.2 (Таймер 1) или IOC1.3 (Таймер 2) чтобы выбрать источник прерывания. Когда происходит переполнение, устанавливается флажок состояния в регистре IOS1. Переполнение Таймера 1 устанавливает IOS1.5, а переполнение Таймера 2 устанавливает IOS1.4.

Input/Output Control Register 1

HWindow 0 (Write), HWindow 15 (Read)

Input/Output Status Register 1

Hwindow 0 (Write), HWindow 15 (Read)

1.3.2. Прерывание переполнения Таймера 2

Таймер 2 может генерировать прерывание переполнения Таймера 2 (INT12, адрес вектора - 2038H) вместо стандартного прерывания переполнения Таймера. Это прерывание разрешается установкой INT_MASK1.4. Переполнение Таймера 2 устанавливает IOS1.4. Таймер 2 может генерировать прерывание переполнения Таймера 2 или на границе 7FFFH/8000H или на границе 0FFFFH/0000H. Переполнение может происходить в любом направлении. IOC2.5 выбирает границу переполнения. Когда IOC2.5 установлен, Таймер 2 прерывается на границе 7FFFH/8000H. Иначе, он прерывается на границе 0FFFFH/0000H.

1.3.3. Прерывание фиксатора Таймера 2

Положительный переход на контакте T2CAPTURE (P2.7) заставляет значение Таймера 2 загружаться в регистр T2CAPTURE . Это событие генерирует прерывание фиксатора Таймера 2 (INT11), если установлен INT_MASK1.3 и T2CAPTURE утверждается в течение более двух времен состояний.

Timer 2 Capture Register

Hwindow 15(Read /Write)

1.4. Предосторожности при работе с Таймерами

При использовании таймеров как датчиков времени для HSI или HSO, следующие руководящие принципы помогут Вам избежать потенциальных проблем.

Будьте осторожны при записи в регистры таймера TIMER1 и TIMER2:

Изменение значения TIMER1 после инициализизации HSI модуля может разрушить относительные ссылки между HSI событиями. Также, изменение значения соответствующего таймера (TIMER1 или TIMER2), после инициализизации HSO модуля, может заставить HSO пропускать запрограммированные события или выполнять их в неправильном порядке.

Конфигурируйте Таймер 2 для функционирования в нормальном режиме (не в быстром режиме приращения):

Так как для полного сканирования CAM, HSO требует восьми времен состояния, Таймер 2, когда он используется как датчик времени для HSO, должен функционировать в нормальном режиме приращения (не в быстром режиме приращения) .

Очистите бит FAST_T2_ENA(IOC2.0) для выбора нормального режима работы таймера.

Конфигурируйте Таймер 2 для счета только в одном направлении.

Таймер 2 , когда он используется как датчик времени для HSO, должен считать только в одном направлении, поскольку, если Таймер 2 колеблется вокруг отметки времени выполнения команды, блокировка входов может происходить несколько раз.

Очистите бит T2UD_ENA(IOC2.1), чтобы сконфигурировать Таймер 2 как суммирующий счетчик.

Используйте предостережение при сбросе Таймера 2.

Не сбрасывайте Tаймер 2 до того, как его значение достигнет самого наибольшего времени, запрограммированного в CAM. CAM задерживает ожидание события до соответствующего времени. Если запрограммированное значение Таймера 2 никогда не достигается, событие будет оставаться отложенным, пока устройство не сбросится или CAM не очистится.

Когда Таймер 2 сконфигурирован для сброса внешним контактом, события программы должны происходить когда Таймер 2 равен единице, а не нулю:

Когда Таймер 2 сконфигурирован, чтобы сбрасываться внешним контактом сброса (IOC0.3 =1), программные события не должны происходить, когда Таймер 2 равен нулю. Если HSI.0 или T2RST (P2.3) сбрасывают Таймер 2, событие может не произойти. Внешние контакты сбрасывают Таймер 2 асинхронно, и Таймер 2 может увеличиться до 1 до того, как HSO может сравнить и распознать CAM запись. Программируйте события так, чтобы они происходили, когда Таймер 2 равен 1, это гарантирует,что HSO имеет достаточное время, чтобы распознать команду, записанную в CAM .

ФРАГМЕНТ ПРОГРАММЫ ИЗУЧЕНИЯ ПРОГРАММИРОВАНИЯ ТАЙМЕРОВ

;**Исследование пpoгpаммных задеpжек с использованием Timer1*

ldb wsr,#15 ; Пеpеключиться в HWindow 15

ld timer1,#0c000h ; Загpузить значение счетчика

ldb wsr,#0 ; Пеpеключиться в HWindow 0

jbc ios1, 5, $ ; Ожидание пеpеполнения Timer

;***********************************************************

В этом уроке мы поговорим о прерываниях. Как понятно из названия, прерывание это событие, которое приостанавливает выполнение текущих задач и передает управление обработчику прерывания. Обработчик прерывания — это функция. Например: если вы написали скетч по управлению мотором или просто плавно зажигаете и гасите светодиод в цикле, то нажатие на кнопку может не обрабатываться, так как Arduino в данный момент занята другой частью кода. Если же использовать прерывание, то такой проблемы не возникнет, так как прерывания имеют более высокий приоритет.

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

В этом уроке используется:

Аппаратные прерывания

В Arduino имеется 4 вида аппаратных прерываний. Отличаются они сигналом на контакте прерывания.

  • Контакт прерывания притянут к земле. Ардуино будет выполнять обработчик прерывания пока на пине прерывания будет сигнал LOW.
  • Изменение сигнала на контакте прерывания. Ардуино будет выполнять обработчик прерывания каждый раз когда на пине прерывания будет изменяться сигнал.
  • Изменение сигнала на контакте прерывания от LOW к HIGH. Обработчик прерывания исполняется только при изменении низкого сигнала на высокий.
  • Изменение сигнала на контакте прерывания от HIGH к LOW. Обработчик прерывания исполняется только при изменении высокого сигнала на низкий.

Если прерывание ожидает нажатия кнопки, то это может стать проблемой из-за дребезга контактов. В мы уже говорили о дребезге контактов. Тогда мы использовали функцию но в прерываниях данная функция не доступна. Поэтому нам придется подавить дребезг контактов немного усложнив схему подключения кнопки к пину прерывания. Для этого понадобится резистор на 10 КОм, конденсатор на 10 микрофарад,
и инвертирующий триггер шмитта. Подключается все по следующей схеме:

В Arduino Uno есть два пина, поддерживающие прерывания. Это цифровые пины 2 (int 0) и 3 (int 1). Один из них мы и будем использовать в нашей схеме.

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


Схема кажется сложной и запутанной, но это не так. Мы подключаем кнопку прерывания к пину Arduino D2, используя аппаратное подавление дребезга контактов. К аналоговому пину A0 мы подключаем инфракрасный дальномер. И к пинам D9, D10 и D11 мы подключаем светодиоды через резисторы на 150 Ом. Мы выбрали именно эти контакты для светодиодов, потому что они могут выдавать ШИМ сигнал.Теперь рассмотрим скетч:

// Назначение прерывания int buttonInt = 0; // Переменные с пинами светодиодов int yellowLed = 11; int redLed = 10; int greenLed = 9; int nullLed = 6; volatile int selectedLed = greenLed; // Инфракрасный дальномер int distPin = 0; void setup () { // Устанавливаем режимы пинов pinMode(redLed, OUTPUT); pinMode(greenLed, OUTPUT); pinMode(yellowLed, OUTPUT); pinMode(nullLed, OUTPUT); // Устанавливаем прерывание attachInterrupt(buttonInt, swap, RISING); } // Обработчик прерывания void swap() { if(selectedLed == greenLed) selectedLed = redLed; else if(selectedLed == redLed) selectedLed = yellowLed; else if(selectedLed == yellowLed) selectedLed = nullLed; else selectedLed = greenLed; } void loop () { // Получаем данные с дальномера int dist = analogRead(distPin); int brightness = map(dist, 0, 1023, 0, 255); // Управляем яркостью analogWrite(selectedLed, brightness); }

// Назначение прерывания

int buttonInt = 0 ;

// Переменные с пинами светодиодов

int yellowLed = 11 ;

int redLed = 10 ;

int greenLed = 9 ;

int nullLed = 6 ;

volatile int selectedLed = greenLed ;

// Инфракрасный дальномер

int distPin = 0 ;

void setup () {

// Устанавливаем режимы пинов

pinMode (redLed , OUTPUT ) ;

pinMode (greenLed , OUTPUT ) ;

pinMode (yellowLed , OUTPUT ) ;

pinMode (nullLed , OUTPUT ) ;

// Устанавливаем прерывание

attachInterrupt (buttonInt , swap , RISING ) ;

// Обработчик прерывания

void swap () {

if (selectedLed == greenLed )

selectedLed = redLed ;

else if (selectedLed == redLed )

selectedLed = yellowLed ;

else if (selectedLed == yellowLed )

Урок 10

Таймеры-счетчики. Прерывания

Сегодня мы узнаем, что такое таймеры-счётчики в микроконтроллерах и для чего они нужны, а также что такое прерывания и для чего они тоже нужны.

Таймеры-счётчики — это такие устройства или модули в микроконтроллере, которые, как видно из названия, постоянно что-то считают. Считают они либо до определённой величины, либо до такой величины, сколько они битности. Считают они постоянно с одной скоростью, со скоростью тактовой частоты микроконтроллера, поправленной на делители частоты, которые мы будем конфигурировать в определённых регистрах.

И вот эти таймеры-счётчики постоянно считают, если мы их инициализируем.

Таймеров в МК Atmega8 три.

Два из них — это восьмибитные таймеры, то есть такие, которые могут максимально досчитать только до 255. Данной величины нам будет маловато. Даже если мы применим максимальный делитель частоты, то мы не то что секунду не отсчитаем, мы даже полсекунды не сможем посчитать. А у нас задача именно такая, чтобы досчитывать до 1 секунды, чтобы управлять наращиванием счёта светодиодного индикатора. Можно конечно применить ещё наращивание переменной до определенной величины, но хотелось бы полностью аппаратного счёта.

Но есть ещё один таймер — это полноправный 16-битный таймер. Он не только 16-битный , но есть в нём ещё определённые прелести, которых нет у других таймеров. С данными опциями мы познакомимся позже.

Вот этот 16-битный таймер мы и будем сегодня изучать и использовать. Также, познакомившись с данным таймером, вам ничего не будет стоить самостоятельно изучить работу двух других, так как они значительно проще. Но тем не менее 8-битные таймеры в дальнейшем мы также будем рассматривать, так как для достижения более сложных задач нам одного таймера будет недостаточно.

Теперь коротко о прерываниях.

Прерывания (Interrupts ) — это такие механизмы, которые прерывают код в зависимости от определённых условий или определённой обстановки, которые будут диктовать некоторые устройства, модули и шины, находящиеся в микроконтроллере.

В нашем контроллере Atmega8 существует 19 видов прерываний. Вот они все находятся в таблице в технической документации на контроллер

Какого типа могут быть условия? В нашем случае, например, досчитал таймер до определённой величины, либо например в какую-нибудь шину пришёл байт и другие условия.

На данный момент мы будем обрабатывать прерывание, которое находится в таблице, размещённой выше на 7 позиции — TIMER1 COMPA , вызываемое по адресу 0x006.

Теперь давайте рассмотрим наш 16-битный таймер или TIMER1 .

Вот его структурная схема

Мы видим там регистр TCNTn , в котором постоянно меняется число, то есть оно постоянно наращивается. Практически это и есть счётчик. То есть данный регистр и хранит число, до которого и досчитал таймер.

А в регистры OCRnA и OCRnB (буквы n — это номер таймера, в нашем случае будет 1) — это регистры, в которые мы заносим число, с которым будет сравниваться чило в регистре TCNTn.

Например, занесли мы какое-нибудь число в регистр OCRnA и как только данное число совпало со значением в регистре счёта, то возникнет прерывание и мы его сможем обработать. Таймеры с прерываниями очень похожи на обычную задержку в коде, только когда мы находимся в задержке, то мы в это время не можем выполнять никакой код (ну опять же образно "мы", на самом деле АЛУ). А когда считает таймер, то весь код нашей программы в это время спокойно выполняется. Так что мы выигрываем колоссально, не давая простаивать огромным ресурсам контроллера по секунде или даже по полсекунды. В это время мы можем обрабатывать нажатия кнопок, которые мы также можем обрабатывать в таймере и многое другое.

Есть также регистр TCCR. Данный регистр — это регистр управления. Там настраиваются определенные биты, отвечающие за конфигурацию таймера.

Также у таймера существует несколько режимов, с которыми мы также познакомимся немного позденее.

Он состоит из двух половинок, так как у нас конотроллер 8-битный и в нем не может быть 16-битных регистров. Поэтому в одной половинке регистра (а физически в одном регистре) хранится старшая часть регистра, а в другом — младшая. Можно также назвать это регистровой парой, состоящей из двух отдельных регистров TCCR1A и TCCR1B. Цифра 1 означает то, что регистр принадлежит именно таймеру 1.

Даный регист TCCR отвечает за установку делителя, чтобы таймер не так быстро считал, также он отвечает (вернее его определённые биты) за установку определённого режима.

За установку режима отвечают биты WGM

Мы видим здесь очень много разновидностей режимов.

Normal — это обычный режим, таймер считает до конца.

PWM — это ШИМ только разные разновидности, то есть таймер может играть роль широтно-импульсного модулятора . С данной технологией мы будем знакомиться в более поздних занятиях.

CTC — это сброс по совпадению, как раз то что нам будет нужно. Здесь то и сравнивются регистры TCNT и OCR. Таких режима два, нам нужен первый, второй работает с другим регистром.

Все разновидности режимов мы в данном занятии изучать не будем. Когда нам эти режимы потребуются, тогда и разберёмся.

Ну давайте не будем томить себя документацией и наконец-то попробуем что-то в какие-нибудь регистры занести.

Код, как всегда, был создан из прошлого проекта. Для протеуса также код был скопирован и переименован с прошлого занятия, также в свойствах контроллера был указан путь к новой прошивке. Проекты мы назовем Test07 .

Попробуем как всегда скомпилировать код и запустить его в протеусе. Если всё нормально работает, то начинаем добавлять новый код.

Добавим ещё одну функцию, благо добавлять функции мы на прошлом занятии научились. Код функции разместим после функции segchar и до функции main. После из-за того, что мы будем внутри нашей новой функции вызывать функцию segchar.

Мало того, мы создадим не одну функцию, а целых две. В одну функцию мы разместим весь код инициализации нашего таймеру, а другая функция будет являться обработчиком прерывания от таймера, а такие функции они специфичны и вызывать их не требуется. Когда возникнет необходимость, они вызовутся сами в зависимости от определённых условий, которые были оговорены выше.

Поэтому первую функцию мы назвовём timer_ini

//———————————————

void timer_ini ( void )

{

}

//———————————————

Также давайте наши функции, а также какие-то законченные блоки с объявлением глобальных переменных, с прототипами функций будем отделять друг от друга вот такими чёрточками, которые за счет наличия двух слешей впереди компилятор обрабатывать не будет и примет их за комментарии. За счёт этих отчерчиваний мы будем видеть, где заканчивается одна функция и начинается другая.

Данная функция, как мы видим не имеет ни каких аргументов — ни входных, не возвращаемых. Давайте сразу данную функцию вызовем в функции main()

unsigned char butcount=0, butstate=0;

timer_ini ();

Теперь мы данную функцию начнём потихонечку наполнять кодом.

Начнем с регистра управления таймером, например с TCCR1B. Используя нашу любимую операцию "ИЛИ", мы в определённый бит регистра занесём единичку

void timer_ini ( void )

TCCR1B |= (1<< WGM12 );

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

Также у таймера существует ещё вот такой регистр — TIMSK . Данный регистр отвечает за маски прерываний — Interrupt Mask . Доступен данный регистр для всех таймеров, не только для первого, он общий. В данном регистре мы установим бит OCIE1A , который включит нужный нам тип прерывания TIMER1 COMPA

TCCR1B |= (1<< WGM12 ); // устанавливаем режим СТС (сброс по совпадению)

TIMSK |= (1<< OCIE1A );

Теперь давайте поиграемся с самими регистрами сравнения OCR1A(H и L) . Для этого придётся немного посчитать. Регистр OCR1AH хранит старшую часть числа для сравнения, а регистр OCR1AL — младшую.

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

TIMSK |= (1<< OCIE1A ); //устанавливаем бит разрешения прерывания 1ого счетчика по совпадению с OCR1A(H и L)

OCR1AH = 0b10000000;

OCR1AL = 0b00000000;

TCCR1B |= ( ); //установим делитель.

Пока никакой делитель не устанавливаем, так как мы его ещё не посчитали. Давайте мы этим и займёмся.

Пока у нас в регистре OCR1A находится число 0b1000000000000000, что соответствует десятичному числу 32768.

Микроконтроллер у нас работает, как мы договорились, на частоте 8000000 Гц.

Разделим 8000000 на 32768, получим приблизительно 244,14. Вот с такой частотой в герцах и будет работать наш таймер, если мы не применим делитель. То есть цифры наши будут меняться 244 раза в секунду, поэтому мы их даже не увидим. Поэтому нужно будет применить делитель частоты таймера. Выберем делитель на 256. Он нам как раз подойдёт, а ровно до 1 Гц мы скорректируем затем числом сравнения.

Вот какие существуют делители для 1 таймера

Я выделил в таблице требуемый нам делитель. Мы видим, что нам требуется установить только бит CS12 .

Так как делитель частоты у нас 256, то на этот делитель мы поделим 8000000, получится 31250, вот такое вот мы и должны занести число в TCNT. До такого числа и будет считать наш таймер, чтобы досчитать до 1 секунды. Число 31250 — это в двоичном представлении 0b0111101000010010. Занесём данное число в регистровую пару, и также применим делитель

OCR1AH = 0b01111010 ; //записываем в регистр число для сравнения

OCR1AL = 0b00010010 ;

TCCR1B |= (1<< CS12 ); //установим делитель.

С данной функцией всё.

Теперь следующая функция — обработчик прерывания от таймера по совпадению. Пишется она вот так

ISR ( TIMER1_COMPA_vect )

{

}

И тело этой функции будет выполняться само по факту наступления совпадения чисел.

Нам нужна будет переменная. Объявим её глобально, в начале файла

#include

//———————————————

unsigned char i ;

//———————————————

Соответственно, из кода в функции main() мы такую же переменную уберём

int main ( void )

unsigned char i ;

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

while (1)

{

// for(i=0;i<10;i++)

// {

// while (butstate==0)

// {

// if (!(PINB&0b00000001))

// {

// if(butcount < 5)

// {

// butcount++;

// }

// else

// {

// i=0;

// butstate=1;

// }

// }

// else

// {

// if(butcount > 0)

// {

// butcount—;

// }

// else

// {

// butstate=1;

// }

// }

// }

// segchar(i);

// _delay_ms(500);

// butstate=0;

// }

Теперь, собственно, тело функции-обработчика. Здесь мы будем вызывать функцию segchar. Затем будем наращивать на 1 переменную i . И чтобы она не ушла за пределы однозначного числа, будем её обнулять при данном условии

ISR ( TIMER1_COMPA_vect )

if ( i >9) i =0;

segchar ( i );

i ++;

Теперь немного исправим код вначале функции main(). Порт D , отвечающий за состояние сегментов, забьём единичками, чтобы при включении у нас не светился индикатор, так как он с общим анодом. Затем мы здесь занесём число 0 в глобавльную переменную i, просто для порядка. Вообще, как правило, при старте в неициализированных переменных и так всегда нули. Но мы всё же проинициализируем её. И, самое главное, чтобы прерывание от таймера работало, её недостаточно включить в инициализации таймера. Также вообще для работы всех прерываний необходимо разрешить глобальные прерывания. Для этого существует специальная функция sei() — Set Interrupt .

Теперь код будет вот таким

DDRB = 0x00;

PORTD = 0b11111111 ;

PORTB = 0b00000001;

i =0;

sei ();

while (1)

Также ещё мы обязаны подключить файл библиотеки прерываний вначале файла

#include

#include

#include

Также переменные для кнопки нам пока не потребуются, так как с кнопкой мы сегодня работать не будем. Закомментируем их

int main ( void )

//unsigned char butcount=0, butstate=0;

timer_ini ();

Соберём наш код и проверим его работоспособность сначала в протеусе. Если всё нормально работает, то проверим также в живой схеме

Всё у нас работает. Отлично!

Вот такой вот получился секундомер. Но так как у нас даже нет кварцевого резонатора, то данный секундомер нельзя назвать точным.

Тем не менее сегодня мы с вами много чему научились. Мы узнали о прерываниях, также научились их обрабатывать, Научились работать с таймерами, конфигурировать несколько новых регистров микроконтроллера, до этого мы работали только с регистрами портов. Также за счёт всего этого мы значительно разгрузили арифметическо-логическое устройство нашего микроконтроллера.

Смотреть ВИДЕОУРОК

Post Views: 17 258

/* ISR_Blink Те же фрукты, только в другом ракурсе Мигание светодиодом с использованием механизма прерываний (переполнение таймера/счетчика 2) */ volatile long mks100; volatile long ms10; volatile int cntr; long tmillis,tms10=0; char flip; void setup() { mks100 = 0; // счетчик сотен микросекунд, переполнение счетчика примерно через 5 суток ms10 = 0; // счетчик десятков миллисекунд, переполнение счетчика примерно через 16 месяцев cntr = 0; flip = 0; // мигаем стандартным светодиодом. // На большинстве плат Arduino он подключен к 13-му выводу: pinMode(13, OUTPUT); Serial.begin(9600); // Включаем нужный нам режим таймера/счетчика - нормальный TCCR2A = 0; //нормальный режим (по умолчанию 1 - ШИМ с коррекцией фазы?) // Предделитель таймера/счетчика настраиваем на 16 - // это позволит "тикать" таймером каждую микросекунду // (в предположении, что сердце микроконтроллера стучит с // частотой 16.000.000 ударов в секунду) TCCR2B = 2; // 010 - fclk/8 (по умолчанию 100 - fclk/64) //TCCR2B = 7; // 111 - fclk/1024 (по умолчанию 100 - fclk/64) TCNT2=59;//55; TIMSK2 |= (1 << TOIE2); // разрешаем прерывание таймера/счетчика 2 по переполнению } ISR(TIMER2_OVF_vect) { // прежде всего взводим счетчик TCNT2=59;//55; // прошли очередные 100 мксек - увеличиваем счетчик сотен микросекунд mks100++; // if(mks100%100==0) ms10++; cntr++; // прошли очередные 10 мсек? - увеличиваем счетчик десятков миллисекунд if(cntr>99) { ms10++; cntr = 0; } } void loop() { if (ms10>tms10) { tmillis = millis(); tms10 = ms10; if (tms10%100==0) { if(flip) digitalWrite(13, HIGH); // set the LED on else digitalWrite(13, LOW); // set the LED off flip = !flip; } if (tms10%1000==0) { // выполнение каждые 10 сек Serial.print(tmillis,DEC); Serial.print(" milliseconnds, "); Serial.println(tms10,DEC); } } }

  • Categorized in
  • Tagged with

One comment

Когда у меня возникло желание вести разработку под Arduino, я столкнулся с несколькими проблемами:
  • Выбор модели из списка доступных
  • Попытки понять, чего мне понадобится кроме самой платформы
  • Установка и настройка среды разработки
  • Поиск и разбор тестовых примеров
  • «Разборки» с экраном
  • «Разборки» с процессором

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

Выбор платформы

Перед началом программирования под железяку требуется в начале ее купить. И тут я столкнулся с первой проблемой: оказалось, что разных *дуин довольно много. Тут есть и широкая линейка Arduino и примерно такая же широкая Freeduino и другие аналоги. Как оказалось, большой разницы, что именно брать, нет. То есть одни из этих устройств чуть быстрее, другие чуть медленнее, одни дешевле, другие - дороже, но основные принципы работы практически не отличаются. Отличия появляются практически только при работе с регистрами процессора и то я далее объясню, как по возможности избежать проблем.

Я выбрал платформу Arduino Leonardo как самую доступную по цене и имеющуюся на тот момент в Интернет магазине, в котором я всё и заказывал. Отличается она от остальной линейки тем, что у нее на борту установлен только один контроллер, который занимается и работой с USB-портом и выполнением тех самых задач, которые мы на наше устройство повесим. У этого есть свои плюсы и минусы, но напороться на них при первоначальном изучении не получится, поэтому забудем о них пока. Оказалось, что она подключается к компьютеру через micro-USB, а не USB-B, как вроде бы большинство других. Это меня несколько удивило, но и обрадовало, потому что я, как владелец современного устройства на Android"е без этого кабеля вообще из дома не выхожу.
Да, питается почти любая *дуино-совместимая железяка несколькими способами, в том числе, от того же кабеля, через который программируется. Также один светодиод почти у всех плат размещен прямо на плате контроллера, что позволяет начать работу с устройством сразу после покупки, даже не имея в руках вообще ничего, кроме совместимого кабеля.

Спектр задач

Я думаю, что прежде, чем взяться за как таковое написание чего-то под железяку, интересно понять, что на ней можно реализовать. С Ардуино реализовать получится почти что угодно. Системы автоматизации, идеи для «умного дома», контроллеры управления чем-нибудь полезным, «мозги» роботов… Вариантов просто уйма. И сильно помогает в этом направлении довольно широкий набор модулей расширения, чрезвычайно просто подключаемых к плате контроллера. Список их довольно длинный и многообещающий, и ищутся они в Интернете по слову shield. Из всех этих устройств я для себя посчитал самым полезным LCD экран с базовым набором кнопок, без которого по моему скромному мнению заниматься какими бы то ни было тренировочными проектами совершенно неинтересно. Экран брался отсюда , еще там есть его описание, а также с приведенной страницы ведут ссылки на официальный сайт производителя.

Постановка задачи

Я как-то привык при получении в свои руки нового инструмента сразу ставить себе какую-нибудь в меру сложную и абсолютно ненужную задачу, браво ее решать, после чего откладывать исходник в сторонку и только потом браться за что-нибудь по настоящему сложное и полезное. Сейчас у меня под рукой был очень похожий на составную часть мины из какого-нибудь голливудского кинофильма экран со всеми необходимыми кнопками, с которым надо было научиться работать, а также очень хотелось освоить работу с прерываниями (иначе в чем смысл использования контроллера?) поэтому первым же, что пришло в голову, оказалось написать часы. А поскольку размер экрана позволял, то еще и с датой.

Первые шаги

Вот мне наконец приехали все купленные компоненты и я их собрал. Разъем экрана подключился к плате контроллера как родной, плата была подключена к компьютеру… И тут мне сильно помогла вот эта статья. Повторять то же самое я не буду.

Скрытый текст

Единственное скажу, что вспомнив молодость (а точнее первый «проект», собранный во время изучения радиоэлектроники во Дворце пионеров - мультивибратор с двумя светодиодами), я нашел 2 светодиода и поправил приведенный в статье пример и начал мигать ими:).

«Вторые шаги»

Следующим закономерным вопросом для меня стало «как работать с LCD экраном?». Официальная страница устройства любезно предоставила мне ссылки на архив, в котором оказалось 2 библиотеки с замечательными примерами. Только не сказала, что с этим всем делать. Оказалось, что содержимое нужно просто распаковать в папку libraries среды разработки.

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

Использованного в примере набора команд в принципе достаточно для простой работы с экраном, но если захочется чего-то большего, то можно открыть исходный текст библиотек LCDKeypad и LiquidCrystal и посмотреть что там есть еще.

Архитектура программы

Основная задача часов - считать время. И делать это они должны точно. Естественно, что без использования механизма прерываний никто не сможет гарантировать, что время считается с достаточной точностью. Поэтому вычисление времени точно нужно оставить им. Всё остальное можно вынести в тело основной программы. А этого самого «остального» у нас довольно много - вся работа с интерфейсом. Можно было бы поступить иначе, создать стек событий, создаваемый в том числе и механизмом обработки прерываний, а обрабатываемый внутри основного приложения, это позволило бы например заниматься обновлением экрана не чаще, чем раз в пол секунды (или по нажатию кнопки) но я посчитал это лишним для такой простой задачи, поскольку кроме перерисовки экрана процессору всё равно заняться нечем. Поэтому всё свободное время программа перечитывает состояние кнопок и перерисовывает экран.
Проблемы, связанные с таким подходом
Периодические изменения экрана
Очень хотелось сделать мигающие двоеточия между часами, минутами и секундами, чтобы как в классических часах они пол секунды горели, а пол - нет. Но поскольку экран перерисовывается всё время, надо было как-то определять в какую половину секунды их рисовать, а в какую - нет. Самым простым оказалось сделать 120 секунд в минуте и рисовать двоеточия каждую нечетную секунду.
Мелькания
При постоянной перерисовки экрана становятся заметны мелькания. Чтобы этого не возникало, имеет смысл не очищать экран, а рисовать новый текст поверх старого. Если сам текст при этом не меняется, то мелькания на экране не будет. Тогда функция перерисовки времени будет выглядеть вот так:
LCDKeypad lcd; void showTime(){ lcd.home(); if (hour<10) lcd.print("0"); // Случай разной длины приходится обрабатывать руками lcd.print(hour,DEC); // английские буквы и цифры ОНО пишет само, русские буквы нужно определять программисту if (second %2) lcd.print(" "); else lcd.print(":"); // вот они где используются, мои 120 секунд в минуте if (minute<10) lcd.print("0"); lcd.print(minute,DEC); if (second %2) lcd.print(" "); else lcd.print(":"); if (second<20) lcd.print("0"); lcd.print(second / 2,DEC); lcd.print(" "); lcd.setCursor(0,1); // переходим в координаты x=0, y=1 то есть в начало второй строки lcd.print(" "); lcd.print(day,DEC); lcd.print(months); // месяцы мне было приятнее нумеровать от 1 до 12, а массив с названиями от 0 до 11 lcd.print(year,DEC); }
Работа с кнопками
Похожая ситуация с кнопками. Нажатая кнопка числится нажатой при каждом прогоне программы, поэтому за одно нажатие может обработаться любое количество раз. Приходится заставлять программу ждать «отжимания» отдельно. Начнем основную программу так:
int lb=0; // переменная хранит старое значение кнопки void loop(){ // main program int cb,rb; // определим 2 переменные, для реально нажатой кнопки и для той, которую будет считать нажатой программа cb=rb=lcd.button(); // в начале можно считать, что это одна и та же кнопка if (rb!=KEYPAD_NONE) showval=1; // переменная указывает, что пока нажата кнопка не надо мигать тому, что настраивается if (cb!=lb) lb=cb; // если состояние кнопки изменилось, запоминаем новое, else cb=KEYPAD_NONE; // иначе говорим программе, что все кнопки давно отпущены.

Работа с таймером

Собственно, чтобы вся работа с таймером состоит из двух важных компонентов:
  • Инициализации механизма прерываний от таймера в удобном для нас режиме
  • Собственно, обработки прерывания
Инициализация таймера
Для того, чтобы начать получать нужные нам прерывания, нужно настроить процессор таким образом, чтобы он начал их генерировать. Для этого нужно установить нужные нам регистры в нужные значения. Какие именно регистры и в какие именно значения нужно устанавливать, нужно смотреть в… даташите на процессор:(. Честно говоря, сильно надеялся, что эту информацию можно будет найти в документации на саму Arduino, но нет, это было бы слишком просто. Мало того, для разных процессоров серии номера битов могут отличаться. И я сам лично натолкнулся на то, что попытка установки битов в соответствие с даташитом на соседний процессор привели к плачевным результатам… Но тем не менее, всё не настолько печально, как может показаться, поскольку для этих битов есть еще и имена, они более-менее общие для разных процессоров. Поэтому использовать цифровые значения мы не будем, только имена.

Для начала вспомним, что в микроконтроллерах AVR таймеров несколько. Нулевой используется для вычисления значений delay() и тому подобных вещей, поэтому его мы использовать не будем. Соответственно, используем первый. Поэтому далее в обозначении регистров часто будет проскакивать единичка, для настройки скажем второго таймера нужно там же поставить двойку.

Вся инициализация таймера должна происходить в процедуре setup(). Состоит она из помещения значений в 4 регистра, TCCR1A, TCCR1B, TIMSK1, OCR1A. Первые 2 из них называются «регистрами A и B управления таймера-счетчика 1». Третий - «регистр маски прерываний таймера/счетчика 1», и последний - «регистр сравнения A счетчика 1».

Команды для установки битов принято использовать следующие (понятное дело, что вариантов много, но чаще всего используются именно эти):
BITE |= (1 << POSITION)
то есть вдвигаем «1» на POSITION бит справо налево и проводим логическое «или» между целевым и полученным байтами. При включении контроллера значения всех этих регистров содержат 0, поэтому о нулях мы просто забываем. Таким образом, после выполнения следующего кода

A=0; A |= (1 << 3)

Значение A станет 8.

Вариантов настройки таймера уйма, но нам нужно добиться от таймера следующего:

  • Чтобы таймер перешел в режим работы CTC (то есть в режим счета со сбросом после совпадения, «Clear Timer on Compare match»), судя по даташиту это достигается установкой битов WGM12:0 = 2, что само по себе означает установить биты со второго по нулевой в значение «2», то есть, «010», команда TCCR1B |= (1 << WGM12) ;
  • Поскольку 16МГц (а именно такая частота у кварцевого резонатора на моей плате) это много, выбрать максимально возможный делитель, 1024 (то есть только каждый 1024-ый такт будет доходить до нашего счетчика), CS12:0=5
  • Сделать так, чтобы прерывание приходило при совпадении с регистром A, для данного счетчика TIMSK1 |= (1 << OCIE1A)
  • Указать при достижении какого именно значения вызывать обработку прерывания, это значение помещается в тот самый регистр A счетчика 1 (целиком его название OCR1A), прерывание по совпадении с которым мы включали предыдущим пунктом.

Как посчитать, до скольки нам нужно проводить вычисления? - Легко, если тактовая частота кварцевого резонатора 16МГц, то при достижении счетчиком значения 16000 прошла бы секунда, если бы коэффициент деления был 1. Так как он 1024, то мы получаем 16000000/1024=15625 в секунду. И всё бы хорошо, но нам нужно получать значения каждые пол секунды, а 15625 на 2 не делится. Значит мы до этого ошиблись и придется взять коэффициент деления поменьше. А следующий по уменьшению у нас 256, что дает 62500 тиков в секунду или 31250 за пол секунды. Счетчик у нас 16-тибитный, поэтому может досчитать до 65536. Иными словами, нам его хватает и на пол секунды и на секунду. Лезем в даташит, потом в исходник и исправляем на CS12:0=4 , а после этого OCR1A = 31249; (как я понял, один такт уходит то ли на на сброс, то ли еще куда, поэтому встречаются советы сбросить еще единичку с полученной цифры).

Обработка прерывания
Синтаксис функции обработки прерывания несколько изменился, сейчас он выглядит так, как в примере ниже. Так что не удивляйтесь, если где-нибудь увидите несколько иное описание имени функции.

Собственно, сейчас оно состоит из зарезервированного слова ISR и указания конкретного прерывания, которое эта функция обрабатывает в скобках. А внутри у этой функции как видите нет ничего фантастического. Даже обязательное RETI как видите за нас автоматом вставляет компилятор.

ISR(TIMER1_COMPA_vect) { digitalWrite(LEDPIN, !digitalRead(LEDPIN)); // LEDPIN=13. Эта строка мигает светодиодом на плате. Удобно и прикольно:) second++; if ((second %2) && lastshowval) { // эта и следующие 7 строк нужны только для того, lastshowval = 0; // чтобы можно было добиться этого забавного эффекта, как на аппаратных часах, showval = 0; // когда в режиме настройки скажем минут, значение настраиваемого параметра мигает } if (!(second %2) && !lastshowval){ // только при отпущенных кнопках, а пока кнопки нажаты, оно просто горит. lastshowval = 1; showval = 1; } if (second>=120) { // опять мои 120 секунд в минуте. Ну а кому сейчас легко? second-=120; minute++; if (minute>=60){ minute-=60; hour++; if (hour>=24){ hour-=24; day++; if (daylarge(day,month,year) // возвращает true если значение дня // больше максимально возможного для этого месяца этого года.) { day=1; month++; if (month>12){ month = 1; year++; } } } } } }

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