Расходомер воды выдаёт 450 импульсов на литр — при потоке 10 л/мин это 75 импульсов в секунду, и обычный attachInterrupt справляется на ура, так я считал в частотомере с окном оборотов. Но соседний проект — счётчик мотков на перемоточном станке — выдаёт до 12 000 импульсов в секунду с оптического датчика, а в loop() у меня ещё крутится протокол и подсветка. Каждое прерывание рвёт основной цикл на куски: вход в обработчик, выход, сохранение регистров — чем плотнее поток, тем больше процессор живёт в чужом коде, а не в своём.
У таймера 1 в ATmega328P есть дверь, о которой вводные курсы молчат: вывод T1 (он же D5) можно назначить тактовым входом самого счётчика. Три бита CS10..CS12 в TCCR1B, выставленные в 111, переключают Timer1 с кварца на внешний пин: каждый фронт на D5 сам прибавляет единицу к регистру TCNT1 — без единого прерывания и без строчки кода в обработчике. Аппаратура считает сама, процессор занимается своими делами, а я раз в секунду забираю готовое число.
План урока: биты CS10..CS12 и чем внешний такт отличается от внутреннего, жизнь счётчика от нуля до 65535 и переполнение, как правильно читать 16-битный TCNT1 и где проходит граница «прерывания ещё тянут / уже нет» — с числами на руках. Обзор таймеров ATmega328 держи рядом как карту: понимать, где сидит T1 и кто его сосед, полезно до того, как крутишь регистры.
CS10..CS12 = 111: внешний такт с пина T1
За источник такта Timer1 отвечают три младших бита регистра TCCR1B — CS10, CS11, CS12. Комбинации знакомые: 001 — кварц без делителя, 010 — /8, 011 — /64, 100 — /256, 101 — /1024, 000 — стоп. А вот 110 и 111 — особые: такт берётся с пина T1, по спаду и по фронту соответственно. Ставишь 111 — таймер перестаёт тикать от 16 МГц и ждёт фронтов снаружи; каждый фронт на D5 — плюс один к TCNT1. Остальные биты держим в нуле: WGM-биты нулевые — нормальный режим, без CTC и PWM, форма счёта тут не нужна, период задаёт не мы, а входящий поток.
Два следствия из этой настройки. Первое: счёт идёт до потолка 65535, следующий фронт перекатывает регистр в ноль и ставит флаг TOV1 — переполнение. Если между опросами приходит меньше 65536 импульсов, флаг можно игнорировать; если больше — считай переполнения и добавляй по 65536 к итогу за каждое. Второе: предельная частота внешнего такта ограничена выборкой пина — по datasheet не выше F_CPU/2,5, на 16 МГц это примерно 6,4 МГц. Никакой attachInterrupt близко не подбирается: у него бюджет процессора кончается на десятках килогерц.
Пин у входа один: T1 сидит на D5, переназначить нельзя — заводская разводка мультиплексора. Если D5 занят, варианты честные: перетащить другой сигнал, либо взять таймер со свободным входом (у Timer0 вход T0 на D4, но на Timer0 стоит millis() — трогать его не хочу никогда). Планируй разводку платы под это заранее, а не после того, как дорожки разведены.
// Счёт импульсов с пина T1 (D5) регистрами: ноль прерываний.// Регистры TCCR1B/TCNT1 движок симулятора не исполняет - скетч для Arduino IDE.const int PIN_LED = 8; // маячок переполнений unsigned int perep = 0; // переполнения TCNT1 с последнего срезаunsigned long vsego = 0; // общий итогunsigned long bylo = 0; // итог на прошлом срезе - для счёта за секундуunsigned long tSrez = 0; void setup() { Serial.begin(9600); pinMode(PIN_LED, OUTPUT); pinMode(5, INPUT); // T1: вход внешнего такта TCCR1A = 0; // нормальный режим: WGM в нуле, без PWM TCCR1B = 0b00000111; // CS12..CS10 = 111: такт с пина T1 по фронту TCNT1 = 0; // счёт с нуля} void loop() { if (TIFR1 & _BV(TOV1)) { // флаг переполнения TCNT1 TIFR1 = _BV(TOV1); // флаг снимается записью единицы perep++; digitalWrite(PIN_LED, HIGH); // вспыхнул - было переполнение } if (millis() - tSrez >= 1000) { // раз в секунду забираем готовое число tSrez += 1000; unsigned int snimok = TCNT1; // компилятор читает младший байт первым vsego += (unsigned long)perep * 65536UL + snimok; perep = 0; TCNT1 = 0; digitalWrite(PIN_LED, LOW); Serial.print("Всего импульсов: "); Serial.print(vsego); Serial.print(" | за последнюю секунду: "); Serial.println(vsego - bylo); bylo = vsego; }}Разбор по строкам. TCCR1A = 0 оставляет нормальный режим — там, где от того же Timer1 нужен точный период, живёт CTC с точным периодом, а здесь задача обратная: ритм задаёт поток. TCCR1B = 0b00000111: три младших бита — CS, единицы в них означают внешний такт по фронту, хочешь по спаду — 110. Чтение: unsigned int snimok = TCNT1 одним присваиванием — компилятор подставит чтение младшего байта с фиксацией старшего. Ручное чтение TCNT1H до TCNT1L как раз и ловит рваное значение, если фронт вклинился между байтами: старшая половина от одного числа, младшая от другого.
Теперь бюджет прерываний — те самые числа на руках. Вход в обработчик и выход на AVR стоят около 4-5 мкс чистыми даже с пустым телом; живой счётчик с volatile-переменной и защитой от гонок — 10-15 мкс. При 10 000 имп/с прерывания съедают 10-15% цикла, при 20 000 — четверть-треть, и всё это время loop() стоит. А каждая строка Serial внутри обработчика умножает цену в разы — копи в прерывании, печатай в loop(), иначе и точные замеры micros() вокруг начнут плыть. Расклад простой: редкий поток — геркон, дверная кнопка — attachInterrupt честнее и проще; тысячи импульсов в секунду — аппаратный счётчик вне конкуренции.
Переполнения при плотном потоке: 65 536 импульсов в секунду — флаг TOV1 горит раз в секунду, ловить его в loop() хватает с запасом; для сравнения подходов пригодится прерывание по совпадению CTC, где событие привязано к таймеру. Мой рабочий расклад для станка: 12 000 имп/с дают одно переполнение в 5,5 секунды — ноль прерываний за смену, процессор занят протоколом, а не чужим кодом. LED-маячок из скетча вспыхивает на каждом переполнении — дешёвая проверка живости счёта на собранном железе.
| CS12..CS10 | Источник такта | Когда брать |
|---|---|---|
| 000 | стоп: счётчик заморожен | перезарядка счёта, экономия |
| 001 | кварц 16 МГц без делителя | обычный тайминг, точный период |
| 011 | кварц /64 | милисекундный ритм соседних уроков |
| 110 | внешний пин T1 (D5), по спаду | импульсы активного низкого уровня |
| 111 | внешний пин T1 (D5), по фронту | счёт импульсов: расходомер, энкодер, датчик мотков |
- 1. Какая настройка TCCR1B назначает Timer1 такт с пина T1 (D5)?
- 2. Поток до 12 000 импульсов в секунду. Почему аппаратный счётчик здесь сильнее attachInterrupt?
- 3. Счётчик дошёл до 65535. Что случится на следующем фронте?
- CS12..CS10 = 111 в TCCR1B: Timer1 тикает фронтами с пина T1 (D5), прерываний ноль, процессор свободен
- потолок счёта 65535: следующий фронт кладёт регистр в 0 и ставит TOV1 — флаги складывай в итог по 65536
- TCNT1 читай одним присваиванием unsigned int: младший байт первым, иначе поймаешь рваное значение
- граница выбора: редкие импульсы — attachInterrupt, тысячи в секунду — аппаратный счёт; внешний такт не выше F_CPU/2,5 (~6,4 МГц на 16 МГц)
Частые вопросы по теме урока
- Какая настройка TCCR1B назначает Timer1 такт с пина T1 (D5)?
- За источник такта отвечают три младших бита: 111 — фронт с пина T1, 110 — спад. WGM задаёт форму счёта и к внешнему входу отношения не имеет. CS12..CS10 = 111 — внешний такт по фронту
- Поток до 12 000 импульсов в секунду. Почему аппаратный счётчик здесь сильнее attachInterrupt?
- Каждое прерывание рвёт loop(): вход-выход плюс тело обработчика — 10-15 мкс. При 12 000 имп/с это уже пятая часть цикла. TCNT1 наращивается железом, процессор тратит на счёт ноль. вход в обработчик стоит 10-15 мкс — до 15% цикла уходит на прерывания, а T1 считает сам без единой строчки кода
- Счётчик дошёл до 65535. Что случится на следующем фронте?
- Нормальный режим: после 65535 следующий фронт кладёт TCNT1 в ноль и ставит флаг TOV1. Собирай флаги — за каждое переполнение плюс 65536 к итогу. регистр перекатится в 0 и поднимется флаг TOV1 — переполнение