Счётчик посетителей на фоторезисторе из урока про ИК-барьер прожил у меня в мастерской ровно до первого солнечного утра: луч уехал вместе с порогом, и счётчик наскрёб десяток «посетителей» до кофе. Тот урок честно разобрал порог, гистерезис и засветку — а постоянному прибору в коридоре нужны ещё две вещи, которых там не было: различение входа и выхода и память, пережидающая моргание света. Обе собираются из деталей этого раздела.
План такой. Первое: заменить голый свет модулированным лучом — кадрами NEC на несущей 38 килогерц вместо непрерывного свечения диода. Второе: поставить два луча со сдвигом поперёк прохода и читать направление по порядку пересечений. Третье: сложить счётчики в EEPROM, чтобы прибор помнил посетителей к следующему вторнику, а не только до вечера. По ходу разберём, почему непрерывная несущая ослепляет приёмник и как оптический антидребезг достаётся бесплатно.
Луч из кадров: непрерывный свет против АРУ
Наивная схема «диод горит, приёмник видит» с модулем VS1838B не работает, и причина внутри банки. В приёмнике живёт АРУ — автоматическая регулировка усиления, и непрерывную несущую она давит: через несколько сотен миллисекунд постоянного 38-килогерцного сигнала усиление спадает, и модуль перестаёт видеть собственный луч. Модуль заточен под пачки с паузами — так устроен любой протокол пультов.
Лекарство следует из болезни: подавать свет пачками. Передатчик шлёт кадры NEC каждые девяносто миллисекунд — сам кадр живёт около 68, на паузу остаётся двадцать с лишним, и АРУ между кадрами успевает отдышаться. Приёмник ведёт для каждого луча метку «когда приходил последний кадр» и считает луч живым, пока метка свежее двухсот миллисекунд. Получается забавно: весь раздел учил читать кадры как команды, а здесь кадр стал маячком «я жив» — содержимое не важно, важен сам факт. Пара при этом стандартная: диод на пине 3 и sendNEC уже собирались в уроке про свой пульт из Arduino, а внутренности трёх ног приёмника — урок про TSOP и VS1838.
// Передатчик луча: отдельная плата, диод строго на пине 3#include <IRremote.hpp> const uint16_t ADDR_BEAM = 0x0A01; // у второй платы адрес 0x0A02const uint8_t CMD_BEAM = 0x01; void setup() { IrSender.begin(); // библиотека сама займёт пин 3} void loop() { IrSender.sendNEC(ADDR_BEAM, CMD_BEAM, 0); // кадр живёт около 68 мс delay(22); // пауза: цикл около 90 мс, АРУ дышит}Две оговорки к этой схеме. Первая: передатчик — отдельная плата. Приём и передача делят второй таймер ATmega328, две роли на одной Uno не живут — предупреждение об этом уже было в уроке про свой пульт. Вторая: окно свежести — ручка с ценой промаха. Двухсот миллисекунд хватает для нормального шага; медленный человек задерживается в луче дольше окна, и луч «оживает» в середине прохода. Тогда окно укорачивают, а темп передатчика поднимают коротким протоколом вроде RC5, чей кадр втрое короче NEC. Оба значения подбираются под ширину прохода: узкая дверь прощает ленивые кадры, широкий коридор требует темпа.
Про числа цикла подробнее. Кадр NEC при нулевых повторах живёт около 68 миллисекунд: девять — заголовок, полторы-две — каждый из тридцати двух бит. С добавкой 22 миллисекунд паузы цикл выходит около 90, то есть примерно одиннадцать кадров в секунду. Окно свежести двести миллисекунд перекрывает два пропущенных кадра: одиночный сбой от блика или птицы в кадре луч не гасит, а настоящий проход, который длиннее окна, виден всегда. Шумному коридору окно растят до трёхсот, но вместе с ним растёт и задержка фиксации прохода.
Монтаж в двух словах. Приёмник ставится в тёмную трубку из изоленты — она режет боковой свет и немного подтягивает диаграмму направленности; диоды передатчиков смотрят строго в приёмник, на метре расстояния отклонение на ширину пальца уже роняет кадры. Каждому диоду — свой резистор: общий резистор на два передатчика развязывает кадры по току и даёт фантомные метки свежести. Кадры двух передатчиков специально не синхронизируются: случайные коллизии глотаются, и окно свежести их переживает.
Направление: порядок пересечений
Один луч считает события, а не людей: прошёл — единица, куда шёл — неизвестно. Направление даёт пара лучей со сдвигом 15–20 сантиметров поперёк прохода — те же числа, что в уроке про барьер. У меня два передатчика шлют на разных адресах NEC: 0x0A01 у входа, 0x0A02 дальше по ходу, и один приёмник различает лучи по адресу — второй приёмник не нужен. Человек, идущий внутрь, перекрывает сначала ближний луч, потом оба, потом дальний; идущий наружу — в обратном порядке.
Правило счёта, на котором я остановился после пары переделок: проход засчитывается, только если оба луча перекрывались — человек прошёл целиком, а не замер в луче и не отступил в полшага; направление определяется последним освободившимся лучом. Отступившего счётчик игнорирует — это фича, а не потеря: половина ложных срабатываний однолучевых счётчиков живёт именно в полушагах. Оптический антидребезг здесь бесплатный: рука, дрожащая на границе луча, рвёт кадры на миллисекунды, а окно свежести такие рывки сглаживает — отдельного дебаунса не нужно. Скетч приёмника:
#include <IRremote.hpp>#include <EEPROM.h> const int IR_PIN = 11;const int LED_PIN = 12;const uint16_t ADDR_A = 0x0A01; // луч у входаconst uint16_t ADDR_B = 0x0A02; // луч дальше по ходуconst unsigned long FRESH_MS = 200; // окно живости луча unsigned long msA = 0;unsigned long msB = 0;bool wasMoving = false;bool bothSeen = false; // оба луча перекрывались за проходint lastCleared = 0; // 1 = A освободился последним, 2 = Bbool prevABroken = false;bool prevBBroken = false;int inCount = 0;int outCount = 0;unsigned long flashMs = 0; void setup() { Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); pinMode(LED_PIN, OUTPUT); inCount = EEPROM.read(0); outCount = EEPROM.read(1); if (inCount == 255) inCount = 0; // свежая ячейка читается как 255 if (outCount == 255) outCount = 0; Serial.print("vosstanovleno: vhod "); Serial.print(inCount); Serial.print(", vyhod "); Serial.println(outCount);} void loop() { if (IrReceiver.decode()) { uint16_t addr = IrReceiver.decodedIRData.address; if (addr == ADDR_A) msA = millis(); if (addr == ADDR_B) msB = millis(); IrReceiver.resume(); } bool aBroken = millis() - msA >= FRESH_MS; bool bBroken = millis() - msB >= FRESH_MS; bool moving = aBroken || bBroken; if (aBroken && bBroken) bothSeen = true; if (prevABroken && !aBroken) lastCleared = 1; if (prevBBroken && !bBroken) lastCleared = 2; prevABroken = aBroken; prevBBroken = bBroken; if (!moving && wasMoving && bothSeen) { if (lastCleared == 2) { // последним освободился дальний - вход inCount = inCount + 1; EEPROM.update(0, inCount); Serial.print("VHOD #"); Serial.println(inCount); } else { outCount = outCount + 1; EEPROM.update(1, outCount); Serial.print("VIHOD #"); Serial.println(outCount); } Serial.print("vnutri: "); Serial.println(inCount - outCount); digitalWrite(LED_PIN, HIGH); flashMs = millis(); bothSeen = false; lastCleared = 0; } wasMoving = moving; if (millis() - flashMs > 150) digitalWrite(LED_PIN, LOW);}Восстановление счёта — одна строка в setup: два байта EEPROM читаются на старте, свежая ячейка со значением 255 прижимается к нулю. Пишет только update — он пропускает запись, если байт не изменился, а это две записи на проход человека. Ресурс ячейки — порядка ста тысяч циклов по даташиту; при трёхстах проходах в день пара байт живёт около года непрерывной работы, и дальше вступает неравномерность износа, которую разбирает урок про ресурс ячеек. Отчёт и сброс я вывел в монитор: команда по Serial печатает входы, выходы и разницу «внутри сейчас», а механика чтения чисел из порта разобрана в уроке про parseInt.
Сброс счётчика я делаю двухступенчатым: буква r печатает текущие числа и ждёт вторую r в течение пяти секунд, только после второй обнуляются ячейки и печатается подтверждение. Одноступенчатый сброс стоил мне однажды годового счёта: задел клавишу в мониторной строке — и архив ушёл. Вторая ступень — три строки кода, а защита от собственной руки лучше любой аккуратности.
| Узел | Фоторезисторный барьер | Этот урок |
|---|---|---|
| источник света | непрерывное свечение ИК-диода | кадры NEC 38 кГц каждые ~90 мс |
| приёмник | фоторезистор и порог с гистерезисом | модуль TSOP/VS1838B, фильтр по адресу |
| направление | не различается — одно событие | два луча со сдвигом, порядок пересечений |
| память счёта | переменная в ОЗУ | EEPROM.update и восстановление на старте |
Собранный счётчик я вешал в коридоре и в мастерской, и главный вывод за год такой: два луча окупаются с первого дня. Однолучевой вариант держался честно только на узкой двери, где мимо не разминуться; на проходе шире метра он считал «туда-обратно» за каждое колебание. Пара лучей со сдвигом и правило «оба перекрылись — иначе не считаем» убрали большую часть споров с реальностью. Разницу легко оценить за вечер: положите рядом бумажный журнал с отметками вручную и сверяйте оба счёта наутро.
Куда расти: считать по часам — массив из двадцати четырёх ячеек в ОЗУ за день и одна запись в EEPROM на ночь дают почасовую гистограмму посещений. А если хочется видеть счёт не только в мониторе, счётчик выводится на экран обратной связи — маршрут показан в уроке про ИК-пульт и LCD.
И про честную границу: автомат различает направления на проходах в один человек. Двое плечом к плечу через оба луча пишутся как один — это видно по редким расхождениям с глазами; узкая дверь и шахматный сдвиг лучей лечат, как и в фоторезисторной схеме. Для учётной точки с плотным потоком берите готовые сдвоенные модули, а для мастерской и коридора этого прибора хватает с запасом.
- 1. Почему непрерывную несущую 38 кГц приёмник перестаёт видеть?
- 2. Приёмник один. Как скетч различает два луча?
- 3. Когда проход засчитывается?
- 4. Что делает EEPROM.update при записи того же значения?
- 5. Человек вошёл в луч A, замер и отступил обратно, второй луч не задел. Что покажет счётчик?
- Непрерывная несущая ослепляет АРУ: луч живёт пачками — кадры NEC каждые ~90 мс с паузами
- Живость луча — метка свежести: кадр приходил в последние 200 мс; антидребезг достаётся бесплатно
- Два передатчика на разных адресах NEC и один приёмник; направление — по последнему освободившемуся лучу
- Считаем только полные проходы: оба луча перекрывались, иначе отступивших и замерших пишут в счёт
- Счёт в EEPROM.update: две записи на проход, восстановление на старте, свежая ячейка 255 прижимается к нулю
Частые вопросы по теме урока
- Почему непрерывную несущую 38 кГц приёмник перестаёт видеть?
- Модуль заточен под пачки с паузами: при постоянной несущей автоматическая регулировка усиления спадает, и луч теряется. Поэтому свет подаётся кадрами каждые ~90 мс. внутренняя АРУ давит постоянный сигнал через сотни миллисекунд
- Приёмник один. Как скетч различает два луча?
- Передатчики шлют на разных адресах, например 0x0A01 и 0x0A02, и приёмник ведёт метку свежести отдельно для каждого адреса. Второй приёмник не нужен. по адресу NEC, на который шлёт каждый передатчик
- Когда проход засчитывается?
- Полный проход — это перекрытие обоих лучей. Направление даёт порядок освобождения: последним освободился дальний луч — вход, ближний — выход. Полушаги не считаются. только если оба луча перекрывались; направление — по последнему освободившемуся
- Что делает EEPROM.update при записи того же значения?
- Update сравнивает байт с записываемым и пропускает лишний цикл стирания-записи. При двух записях на проход пара байт счётчика живёт около года при 300 проходах в день. пропускает запись: ячейка не изнашивается
- Человек вошёл в луч A, замер и отступил обратно, второй луч не задел. Что покажет счётчик?
- Правило «оба перекрылись» отсеивает полушаги и замерших в луче: проход без обоих лучей не считается. Это главная защита от ложных срабатываний однолучевых схем. не посчитает: оба луча не перекрывались