Сигнализация в гараже жила у меня спокойно ровно до того дня, когда я добавил в неё сирену. Часы на сигнализации тикали регистрами из урока про прерывание по таймеру: Timer2 в CTC, тысяча прерываний в секунду, шесть строк с битами WGM21 и CS22 и записью в OCR2A. Сирену я подключил через tone() — и часы встали. Никаких предупреждений при компиляции, никакой реакции в мониторе: просто секунды больше не идут. Пока я полчаса перечитывал свой же код, виновник был у всех на виду: tone() занимает Timer2 целиком и молча перезаписывает мои регистры TCCR2A, OCR2A и TIMSK2 своими. Делить таймер с ним нельзя — карта таймеров это говорит прямо, а я в тот день её не перечитал.
Выход напрашивался сам: перевести тик на Timer1. Но регистры Timer1 в два раза объёмнее — TCCR1A и TCCR1B, 16-битный регистр сравнения, значение в который кладут через временный регистр в строго определённом порядке. Я один раз выписал эту последовательность на бумажку и в тот же вечер нашёл библиотеку TimerOne, которая делает то же самое двумя вызовами: Timer1.initialize(1000) и Timer1.attachInterrupt(naTik). Библиотека сама считает предделитель и границу сравнения из моего числа — именно это я и купил за два килобайта флеша: я задаю период, а биты CS и WGM подбирает она.
В этом уроке — API библиотеки целиком: период в микросекундах и его границы, функция на тик и правила её жизни, пауза и запуск, бонусный ШИМ на пинах 9 и 10. В конце — модель тика в симуляторе: движок TimerOne не знает, поэтому механику собираем честно из переменных — период из потенциометра, пауза кнопкой, гейт без дрейфа.
Две строки вместо шести
API библиотеки умещается в пять строк. Timer1.initialize(микросекунды) — обязательный первый вызов: настраивает Timer1 на период, 1000 означает тысячную долю секунды, 50 — период в пятьдесят микросекунд, то есть двадцать тысяч вызовов в секунду. Timer1.setPeriod(микросекунды) — меняет период на ходу, не останавливая счёт. Timer1.attachInterrupt(функция) — вешает вашу функцию на каждый тик; есть и сдвоенная форма Timer1.attachInterrupt(функция, период) — вешает и сразу задаёт период. Границы по документации: от 1 микросекунды — миллион вызовов в секунду, теоретический предел — до 8 388 480 микросекунд, это около 8,3 секунды на тик. Между этими числами библиотека сама подбирает предделитель из ряда 1, 8, 64, 256, 1024 и границу сравнения — вы число подали, она биты разложила.
Функция на тик — это та же ISR, что и в уроке про внешние прерывания, просто источник события не пин, а счётчик. Правила неизменны: переменные, которые она трогает, — volatile; в теле — только арифметика и взвод флагов; Serial и delay внутри — зависание скетча на реальной плате. Все решения принимает loop(): смотрит на счётчик, мигает, печатает, кормит часы. У библиотеки есть и бонусы: Timer1.pwm(пин, duty) и setPwmDuty() дают ШИМ на пинах 9 и 10 мимо analogWrite, а stop(), start() и restart() управляют ходом счётчика — stop ставит паузу, start продолжает с места остановки, restart начинает период заново.
Цена вопроса — такты. Вход и выход из прерывания на 16 МГц стоят несколько десятков тактов, это порядка двух-четырёх микросекунд на каждый вызов. При тике 1000 раз в секунду процессор тратит на прерывания доли процента — незаметно. А мой рекорд бесполезности: мигалка с периодом 2 микросекунды — светодиод честно горел вполнакала, потому что процессор жил внутри обработчика, а loop() доставались крохи: Serial печатал обрывки строк, и опрос кнопки тоже перестал успевать. Правило, к которому я пришёл: тик честнее держать в диапазоне 1-10 килогерц, а каждый шаг выше — сначала оправдать. Если сомневаетесь — начните с тысячи герц и поднимайте частоту только под конкретную задачу, с числом в руках: сколько тактов уходит на обработчик, столько и стоит каждый тик.
// TimerOne: секундная мигалка на тиках. Библиотека ставится через// менеджер библиотек: TimerOne (автор Paul Stoffregen).#include <TimerOne.h> volatile int tik = 0; // меняется из функции на тик — обязательно volatile void naTik() { // зовётся сама, каждый период tik = tik + 1; // только арифметика: никаких Serial и delay} void setup() { Serial.begin(9600); pinMode(12, OUTPUT); Timer1.initialize(1000); // период в МИКРОсекундах: 1000 мкс = 1 мс Timer1.attachInterrupt(naTik); // функция на каждый тик} void loop() { static int bylo = 0; if (tik - bylo >= 1000) { // прошла секунда тиков bylo = bylo + 1000; if (digitalRead(12) == HIGH) { digitalWrite(12, LOW); } else { digitalWrite(12, HIGH); } Serial.print("sec="); Serial.println(bylo / 1000); }}| Способ получить тик | Занимает | Диапазон периодов | Цена входа |
|---|---|---|---|
| millis-гейт в loop() | ни одного таймера | от 1 мс и выше | тик ждёт свободного loop: при печати строк в 30 мс сбивается |
| Timer2 в CTC регистрами | Timer2 — tone() умирает | 16 мкс — 16,4 мс | шесть строк с битами WGM, CS и OCR2A |
| TimerOne | Timer1 — Servo.h умирает | 1 мкс — 8 388 480 мкс (около 8,3 с) | две строки: initialize и attachInterrupt |
Модель тика в симуляторе
Движок симулятора не знает TimerOne — библиотечные вызовы и регистры за пределами его API, и я не буду притворяться наоборот. Модель ниже собрана из того, что движок исполняет, и повторяет логику библиотеки один к один: переменная period — модель периода (в модели миллисекунды вместо микросекунд платы, чтобы переполнения было видно глазом), потенциометр A0 задаёт период — аналог setPeriod на ходу, кнопка D2 — пауза и запуск, это то, что на плате делают Timer1.stop() и Timer1.start(). Гейт lastTik += period вместо lastTik = millis() — тот же приём, что в CTC из прошлого урока: интервал не накапливает хвосты занятости loop и не дрейфует. Светодиод мигает каждые пять тиков — маячок, по которому видно ритм.
Честная оговорка про код выше: скетч с #include <TimerOne.h> — это код для настоящей платы, в симуляторе он не запустится. А вот привычки, которые тренирует модель, — общие: смотреть на тик как на число, менять его одной переменной и никогда не позволять тику зависеть от занятости loop(). В модели цифры карликовые: на плате тот же скетч живёт с периодом 1000 микросекунд — тысяча вызовов в секунду, — и потенциометр там не нужен: setPeriod зовут один раз в setup или по кнопке, а не каждый оборот loop().
Где тик окупается
Железный тик — это фундамент, на который встаёт планировщик задач: задача срабатывает по счётчику тиков, и занятость loop перестаёт влиять на ритм — задержка между срабатываниями сжимается с «когда-нибудь» до худшего случая в один тик, одну-две миллисекунды. Мои рабочие применения: опрос энкодера каждые 1-2 миллисекунды — декодер таблицей из урока про энкодер на прерываниях прекрасно живёт на тике и перестаёт бояться дребезга; софтверный второй Serial на другой паре пинов — там тик обязан бить в 8-16 раз чаще бода, для 9600 это 8-16 килогерц; шаг конечного автомата раз в 10 мс. Для быстрых замеров до микросекунд — micros() и её пределы, это другой инструмент: тик меряет ритм, micros — длительность.
А где тик не окупается: всё, что медленнее ста раз в секунду, честно закрывается millis-гейтом из урока про millis вместо delay — дороже секунды на прерываниях я не гонял ни разу; для интервалов в часы и сутки есть и watchdog со сном, и обычный счётчик секунд поверх тика. Поглядеть на ритм тика глазами проще всего графиком: выложите tik= в монитор и откройте плоттер — ступеньки с фиксированным шагом и есть ваш период; их наклон подскажет быстрее любого Serial-принта, куда уплыл ритм. И правило из практики: в функции на тик у меня живёт ровно один инкремент — всё остальное это просьба о проблемах, о которых макетка предупредит миганием, а плата — молчанием.
- 1. Какой период задаёт Timer1.initialize(500)?
- 2. В скетч подключили и TimerOne, и Servo.h. Что пойдёт не так?
- 3. Период тика — 2 микросекунды. Сколько времени остаётся loop()?
- 4. Каков диапазон периодов TimerOne по документации?
- 5. Что допустимо в функции, повешенной на тик?
- TimerOne прячет регистры Timer1: initialize и attachInterrupt вместо битов WGM и CS — период задаёте числом в микросекундах, предделитель и границу подбирает библиотека
- Диапазон — от 1 мкс до 8 388 480 мкс; тик разумнее держать в 1-10 кГц: выше — процессор живёт в прерываниях, ниже — хватает millis-гейта
- Функция на тик — та же ISR: volatile, десятки тактов, никакого Serial и delay; решения принимает loop, который следит за счётчиком
- Timer1 забран целиком: Servo.h и TimerOne несовместимы, ШИМ на 9 и 10 у библиотеки свой — Timer1.pwm(пин, duty); tone() при этом свободен
- Пауза и запуск — stop() и start(); смена периода на ходу — setPeriod(): всё то же самое, что модель показывает потенциометром и кнопкой D2
Частые вопросы по теме урока
- Какой период задаёт Timer1.initialize(500)?
- Единицы библиотеки — микросекунды во всех функциях: initialize(500) — период полмиллисекунды, то есть 2000 вызовов функции на тик в секунду. Проверяйте единицы каждый раз: одна привычная миллисекунда меняет частоту в тысячу раз. 500 микросекунд: аргумент initialize всегда в микросекундах
- В скетч подключили и TimerOne, и Servo.h. Что пойдёт не так?
- Timer1 — территория обеих библиотек. Конфликт не ловится на компиляции: обе живут, каждая портит настройку другой. Выбирайте одно из двух или уводите тик на Timer2, а серво оставляйте в покое. обе библиотеки претендуют на Timer1: серво дёргается, тик плывёт
- Период тика — 2 микросекунды. Сколько времени остаётся loop()?
- При 16 МГц период 2 мкс — это 32 такта, а пролог и эпилог прерывания съедают несколько десятков. Процессор живёт в обработчике, loop получает крохи — это и есть мой опыт с мигалкой на 2 мкс. почти ничего: на 2 мкс всего 32 такта, а вход и выход из ISR стоят десятки
- Каков диапазон периодов TimerOne по документации?
- Документация библиотеки называет минимум 1 микросекунду (миллион вызовов в секунду) и максимум 8 388 480 микросекунд. Библиотека сама подбирает предделитель и границу, поэтому весь диапазон покрывается одним вызовом. от 1 мкс до 8 388 480 мкс — около 8,3 секунды
- Что допустимо в функции, повешенной на тик?
- Функция на тик — ISR со всеми её правилами: максимум тактов, только volatile-переменные. Serial, delay и рисование внутри обработчика на реальной плате вешают скетч — печатает и решает всегда loop(). инкремент volatile-счётчика