Парковочный помощник для полки в гараже был моим третьим проектом с дальномером HC-SR04: один модуль и лесенка из светодиодов — зелёный, можно ехать, красный, стоп. В даташите к модулю было написано: длительность эха в микросекундах поделите на 58, получите сантиметры. Откуда 58 — не объяснялось, и я захотел проверить формулу сам. Упёрся сразу: мой измерительный арсенал был пуст. millis() на импульс эха длиной 11 600 мкс честно отвечала нулями — для неё это меньше одного тика.
И правда: эхо для препятствия в 20 см живёт около 11 600 мкс — это 11,6 мс, меньше полутора тиков millis(). Показания будут прыгать между 11 и 12 мс, и каждый мс ошибки — это почти 2 см расстояния. А всё, что короче миллисекунды — импульсы ИК-пультов, хвосты ШИМ, сигналы датчиков, — для millis() не существует вовсе: секундная стрелка не видит взмаха крыла.
Микроскоп в Arduino уже встроен. Функция micros() возвращает микросекунды с того же запуска, что и millis(), с разрешением в 4 мкс, а pulseIn() — готовый секундомер длительности одного импульса. В этом уроке: откуда берутся те самые 4 мкс, почему micros() доживает до переполнения всего за 71,6 минуты, как pulseIn устроен изнутри и что он делает с loop, пока ждёт. Практика — дальномер с честной формулой us/58 и проверкой на ноль.
Часы с шагом 4 мкс
millis() и micros() растут из одного корня — таймера Timer0. На Uno кварц 16 МГц, предделитель у Timer0 — 64, значит таймер тикает каждые 16 МГц / 64 = каждые 4 мкс и переполняется за 256 тиков — примерно каждые 1024 мкс. micros() читает счётчик таймера и пересчитывает его в микросекунды — отсюда и шаг ровно 4 мкс, мельче не бывает, сколько ни проси. millis() считает те же переполнения: 1024 мкс — это почти, но не ровно 1 мс, поэтому ядро Arduino копит дробную часть, чтобы средняя скорость оставалась честной секундой в секунду.
Цена разрешения — срок службы. micros() возвращает unsigned long: 32 бита микросекунд — это 4295 секунд, 71,6 минуты. Через час с небольшим счётчик склеивается и идёт с нуля. Разность двух меток через беззнаковое вычитание остаётся верной и через склейку — тот же приём, что с millis() и её 49,7 сутками из урока про millis() вместо delay. Ломается другое: метка, сохранённая «на потом». Разница больше 71,6 минуты — враньё: замеры микросекундами живут в масштабе кадров loop, не часов.
Второй сюрприз — внутри прерываний. millis() в обработчике не растёт: её обновляет то самое прерывание Timer0, а вложенные прерывания на AVR запрещены. micros() читает счётчик таймера напрямую и в обработчике живёт. Поэтому длительность короткого импульса можно ловить в прерывании attachInterrupt: зафиксировал micros() на фронте, зафиксировал на спаде, разность — длительность. Импульсы короче десяти микросекунд опросом из loop не поймать вовсе — там прерывание без вариантов.
| Событие | Длительность | Чем мерить |
|---|---|---|
| Один такт 16 МГц | 62,5 нс | только счёт тактов в ассемблере |
| digitalWrite или digitalRead | около 5 мкс | micros() впритык: её шаг — 4 мкс |
| analogRead | около 112 мкс | micros() |
| Символ на 9600 бод | около 1,04 мс | millis() |
| Эхо HC-SR04 для 20 см | около 11 600 мкс | pulseIn или метки micros() |
| Период ШИМ 490 Гц | около 2041 мкс | pulseIn |
| Переполнение micros() | 71,6 минуты | не держать метки дольше |
pulseIn(pin, level, timeout) — готовый секундомер одного импульса. Внутри это цикл «все бросаем и ждём»: функция крутится на digitalRead, ловит нужный уровень, запоминает micros(), ждёт противоположный уровень и отдаёт разность в микросекундах. Пока импульс идёт, loop не крутится: опроса кнопки нет, печати нет, ничего нет. Третий аргумент — таймаут в микросекундах; без него pulseIn ждёт целую секунду. Не дождались — возвращает 0.
Для HC-SR04 всё сходится. Скетч посылает триг 10 мкс, модуль орёт ультразвуком, звук летит до препятствия, отражается, возвращается — и длительность его эха пропорциональна расстоянию. Звук идёт 343 м/с, это 0,0343 см за микросекунду; волне нужно пройти туда и обратно, двойной путь, поэтому один сантиметр расстояния — это 58,3 мкс эха. Отсюда даташитовское «дели на 58». Про ШИМ из урока про analogWrite та же математика: при 490 Гц период 2041 мкс, и длительность высокого уровня при половинном заполнении — примерно 1020 мкс, pulseIn такое видит насквозь.
Точность pulseIn ограничена снизу тем самым digitalRead: каждый шаг опроса стоит около 5 мкс, поэтому длительность измеряется с пятном порядка ±10 мкс — это ±0,17 см расстояния. Для парковки — избыток с запасом. И про аналоговые величины отдельная строка: analogRead занимает около 112 мкс на один замер, так что чаще тысячи опросов в секунду аналоговый вход не имеет смысла — быстрее его физика не разрешает.
const int PIN_TRIG = 12;const int PIN_ECHO = 11;const int PIN_KRAS = 8;unsigned long lastZamer = 0; void setup() { Serial.begin(9600); pinMode(PIN_TRIG, OUTPUT); pinMode(PIN_ECHO, INPUT); pinMode(PIN_KRAS, OUTPUT); Serial.println("Dalnomer: trig 12, echo 11");} void loop() { if (millis() - lastZamer >= 200) { // замер два раза в секунду lastZamer = millis(); // триг 10 мкс: короткий HIGH на TRIG digitalWrite(PIN_TRIG, LOW); delayMicroseconds(2); digitalWrite(PIN_TRIG, HIGH); delayMicroseconds(10); digitalWrite(PIN_TRIG, LOW); // секундомер эха: таймаут 25000 мкс обязателен unsigned long us = pulseIn(PIN_ECHO, HIGH, 25000); if (us == 0) { Serial.println("eho net"); // 0 — не дождались, а не 0 см! digitalWrite(PIN_KRAS, HIGH); } else { unsigned long sm = us / 58; // 58 мкс на сантиметр Serial.print("us="); Serial.print(us); Serial.print(" sm="); Serial.println(sm); if (sm < 20) { digitalWrite(PIN_KRAS, HIGH); // ближе 20 см — стоп } else { digitalWrite(PIN_KRAS, LOW); } } }}pulseIn — не единственный способ. Когда импульс надо поймать в контексте — например, отмерить, сколько длится собственный digitalWrite, — включают ручной секундомер: метка t0 = micros() перед делом, метка t1 = micros() после, разность — и есть замер. Так я однажды честно померил digitalWrite: вышло около 5 мкс на запись — впритык к шагу самих micros(), поэтому честнее говорить «4–6 мкс». Разность двух меток во много раз точнее одиночного показания: оба показания дрожат одинаково, и дрожь уходит в вычитание.
Три правила ручного секундомера, каждое — с грабли. Первое: никакого Serial.print внутри замера — один символ на 9600 бод стоит около миллисекунды, и печать между метками разорвёт измерение на тысячи микросекунд; сначала меряем, потом печатаем. Второе: паузы на время замера — только delayMicroseconds(): обычный delay() даже с единицей занимает миллисекунду, а delayMicroseconds() точен до такта — но живёт до 16383 мкс, для трига дальномера хватает с запасом, для длинных пауз он не предназначен. Третье: одиночный замер ничего не значит — только серия из десяти с минимумом и средним: первый прогон часто попадает на прерывание Timer0 и врёт на 4–8 мкс. Так меряем всё, что короче миллисекунды, и не верим ни одному одиночному числу — включая мои.
Дальномер в симуляторе
- Проведите серию: 2 см, 100 см, 400 см — сверьте us= с правилом 58 мкс на сантиметр и посчитайте руками: 100 см должны дать около 5800 мкс.
- Ужмите таймаут с 25000 до 5000 мкс и найдите ползунком границу, за которой скетч начнёт печатать eho net. Граница должна лечь около 86 см — это 5000, делённые на 58.
- Посчитайте в тетради: если препятствий нет совсем, сколько времени loop отдаст pulseIn за секунду? Пять замеров по 25 мс таймаута — четверть секунды на пустых направлениях.
- Закомментируйте ветку if (us == 0), уведите препятствие за границу таймаута из задания 2 и поймайте ложную тревогу: sm=0 при пустой полке, красный горит.
- Замените деление на 58 на точное 58,3 через us * 10 / 583 и сравните показания на 400 см: разница выйдет около 2 см. Решите для парковки, стоит ли эта копейка усложнения.
- micros() — тот же Timer0: шаг ровно 4 мкс при 16 МГц и предделителе 64; millis() считает его же переполнения и копит дробную часть
- micros() склеивается каждые 71,6 минуты против 49,7 суток у millis(); верна разность двух свежих меток через беззнаковое вычитание — она корректна и через склейку
- pulseIn() ждёт импульс до секунды по умолчанию: таймаут третьим аргументом — всегда; вернул 0 — это «не дождались», а не нулевая длительность
- HC-SR04: триг 10 мкс, эхо 58 мкс на сантиметр (звук 343 м/с, путь туда и обратно); точность ограничена digitalRead — пятно ±10 мкс, это ±0,2 см
- Внутри прерывания millis() не растёт, а micros() живёт: длительность короткого импульса ловят метками micros() на фронте и на спаде
- 1. Почему шаг micros() — ровно 4 мкс?
- 2. Метка t0 сохранена в переменной, а разность посчитали через 80 минут. Что выйдет?
- 3. pulseIn вернул 0. Что это значит?
- 4. Сколько loop простаивает на одном замере, если препятствия нет, а таймаут 25000 мкс?
Частые вопросы по теме урока
- Почему шаг micros() — ровно 4 мкс?
- 16 МГц / 64 = 250 кГц: один тик таймера каждые 4 мкс. micros() читает этот счётчик напрямую, поэтому мельче тика разрешить не может. Кварц 16 МГц поделён предделителем 64: Timer0 тикает каждые 4 мкс, и мельче считать не из чего
- Метка t0 сохранена в переменной, а разность посчитали через 80 минут. Что выйдет?
- Беззнаковое вычитание прощает склейку счётчика, но не прощает интервалы длиннее периода счётчика. 80 минут больше 71,6 — метка протухла. Неверная: разность превысила срок службы счётчика — 71,6 минуты; метки микросекунд живут в масштабе кадров
- pulseIn вернул 0. Что это значит?
- 0 — сигнал «не дождались». Трактовать его как 0 сантиметров — классическая ложная тревога: «стена в упор» при пустой комнате. Ветку if (us == 0) пишем всегда. Таймаут: импульса заданного уровня не дождались
- Сколько loop простаивает на одном замере, если препятствия нет, а таймаут 25000 мкс?
- pulseIn — блокирующий секундомер: без эха он честно ждёт все 25000 мкс. Пять замеров в секунду — четверть секунды простоя, и опрос кнопки всё это время стоит. 25 мс: pulseIn крутится до таймаута, и всё это время loop не крутится