Рассада на подоконнике в феврале ждала от меня автоночник: лампа включается, когда за окном темнеет. Фоторезистор на A1 выдавал числа от 0 до 1023, и я две недели подбирал порог на глаз: сегодня 400 — темнеет, завтра 400 — ещё светло. Никакой таблицы «столько-то отсчётов — столько-то света» в природе не существует: показания плывут с напряжением питания, температурой и партией резистора, а пересобранный вечером макет требовал калибровки заново. Мне нужен был прибор с единицами измерения, а не палка с насечками — и им оказался BH1750, модуль за полтораста рублей, который сам считает люксы и отдаёт их по шине.
BH1750 — шестнадцатибитный датчик освещённости от Rohm: внутри корпуса фотодиод и преобразователь, чип меряет падающий свет и по I2C отдаёт готовое число сразу в люксах — без делителей, подбора коэффициентов и логарифмических кривых. Проводов два: SDA и SCL на A4 и A5, плюс питание и общая земля. Если шина для вас в новинку, её устройство разложил базовый урок про два провода, а чем фоторезисторный ночник отличался от приборного замера — показал урок про ночник на фоторезисторе; здесь идём дальше, к командам и байтам.
План такой. Сначала адреса 0x23 и 0x5C и ножка ADDR, которая между ними решает. Потом команды измерения: однобайтовые опкоды с разными шагами и таймингами, чтение пары байтов и перевод сырого слова в люксы делением на 1,2. В конце — две вещи, на которых я собрал весь свой негативный опыт: потолок 65535 люкс, который на прямом солнце замирает, и щёлкающий туда-сюда авточник без гистерезиса.
Адреса 0x23 и 0x5C: ножка ADDR решает
У BH1750 адрес из одной перемычки. Ножка ADDR прижата к земле — на большинстве готовых модулей она уже подтянута на плате — и чип отвечает на 0x23; посадите ADDR на питание, получите 0x5C. Это удобно, когда датчиков хочется два: один смотрит в комнату, второй — в окно, и оба живут на одной шине, не мешая друг другу. Перед первой сборкой адрес проверяет сканер из урока про поиск устройств на шине — тридцать секунд против вечера гаданий; полная таблица «кто на какой адрес претендует» лежит в уроке про адреса типовых модулей.
Обмен асимметричный: в датчик пишут однобайтовую команду — код режима, из датчика читают два байта результата. Команда уходит через Wire.beginTransmission(0x23), Wire.write с опкодом и endTransmission — никаких регистров и указателей, каждый байт сам говорит чипу, что делать. После неё чипу нужно время на замер, и здесь прячется первая мелкая грабля: читать раньше готовности можно, но придёт результат прошлого прохода или нули. Тайминги по режимам я собрал в таблицу.
| Режим | Опкод | Шаг | Время замера |
|---|---|---|---|
| непрерывно, высокое разрешение | 0x10 | 1 лк | 120 мс, до 180 мс |
| непрерывно, высокое разрешение 2 | 0x11 | 0,5 лк | 120 мс, потолок вдвое ниже |
| непрерывно, низкое разрешение | 0x13 | 4 лк | 16 мс, до 24 мс |
| разовое измерение | 0x20 | 1 лк | 120 мс, потом чип отдыхает |
Опкоды, чтение пары байтов и деление на 1,2
Опкодов четыре, и разница между ними — в цене деления и времени замера. 0x10 — мой рабочий режим: непрерывное измерение с шагом 1 люкс, свежий результат каждые 120–180 мс. 0x11 — тот же ритм, но шаг 0,5 люкса: график плавнее, зато верхняя планка диапазона вдвое ниже. 0x13 — низкое разрешение: шаг 4 люкса, зато готово за 16 мс — для вердиктов «светло или темно» и экономии шины. 0x20 — разовое измерение: чип мерит, отдаёт число и отдыхает, пока не получит новую команду, — режим батарейных метеостанций, которые просыпаются раз в минуту.
Чтение выглядит так: Wire.requestFrom(0x23, 2) и два Wire.read() — старший байт приходит первым. Слово собираю в переменные: hi = Wire.read(), lo = Wire.read(), raw = (hi << 8) | lo; собирать прямо в выражении я перестал после того, как на одной из плат байты поменялись местами — порядок вычисления операндов компилятор не обещает. Перевод в люксы: raw / 1.2, и деление обязано быть дробным — держите переменную float, иначе шаг станет целым и вы потеряете половину разрешения. Механику двухходовки «команда — пауза — чтение» по тактам разобрал урок про requestFrom и регистры.
Пара байтов умеет мстить конкретными числами. Первый лог датчика выглядел у меня так: 4000, 4020, 40975, 4005 — на ровном месте значение прыгнуло на порядок. Вскрытие заняло полчаса: в одном проходе слово собралось прямо в выражении, и после очередной правки пары легли младшим вперёд. Ориентиры для калибровки: 0x12C0 — это 4800 отсчёта и честные 4000 лк; те же байты наоборот, 0xC012, — уже 49170 отсчёта и 40975 лк. Ступенчатый скачок без единой причины снаружи — фирменный почерк перепутанных половин: лечится он переменными hi и lo и быстрой проверкой «в комнате не бывает 20000 лк».
#include <Wire.h> // BH1750 без библиотеки: ADDR на земле -> адрес 0x23.// 0x10 - непрерывное измерение, шаг 1 люкс, ~120 мс на отсчёт.const byte ADDR = 0x23;const byte CMD_HRES = 0x10; void bhSend(byte cmd) { Wire.beginTransmission(ADDR); Wire.write(cmd); Wire.endTransmission();} void setup() { Wire.begin(); Serial.begin(9600); bhSend(CMD_HRES); // запустили непрерывные замеры delay(180); // пауза на первый отсчёт (максимум по даташиту) Serial.println("BH1750 на 0x23, режим 0x10: непрерывно, 1 лк");} void loop() { delay(1000); // чаще нет смысла: результат живёт 120-180 мс if (Wire.requestFrom(ADDR, 2) != 2) { // чип обязан отдать ровно 2 байта Serial.println("Датчик не отвечает: адрес, провода, подтяжки"); bhSend(CMD_HRES); // перезапуск режима на всякий случай delay(200); return; } byte hi = Wire.read(); // старший байт идёт первым byte lo = Wire.read(); unsigned int raw = (hi << 8) | lo; float lux = raw / 1.2; // дробное деление - обязательное условие Serial.print("Сырое слово: "); Serial.print(raw); Serial.print(" -> "); Serial.print(lux, 0); Serial.println(" лк"); if (lux < 50) Serial.println("темнота: пора включать свет"); else if (lux < 300) Serial.println("сумерки или приглушённый свет"); else if (lux < 12000) Serial.println("комнатный или облачный свет"); else Serial.println("яркое дневное освещение");}В скетче две детали, которые экономят нервы. Первая — проверка requestFrom: датчик обязан отдать ровно два байта, и если их меньше, честнее перезапустить режим и пропустить проход, чем рисовать на экране ерунду. Вторая — пауза 180 мс после старта: типовое время замера 120 мс, но даташит допускает до 180, и я однажды словил нули на старте именно потому, что поторопился. Ритм опроса в секунду на delay для одинокого авточника простителен, но если рядом живёт опрос кнопок, переведите его на millis — как именно, показал урок про millis вместо delay.
Зачем я принципиально показываю обмен без библиотеки: она сжала бы скетч до трёх строк, но оставила бы магию. Голые два вызова Wire видны целиком, и когда датчик молчит, подозреваемых ровно три — питание, адрес, подтяжки — вместо четвёртого, спрятанного внутри чужого кода. Вердикты «темнота — сумерки — день» при этом отдельная логика: пороги правятся под ваше окно, механике обмена до них дела нет.
В симуляторе чипа-заглушки нет: движок подтверждает любой адрес и отвечает одинаковыми байтами 0x2A, отсюда столбец 10794 отсчёта и 8995 люксов на каждом проходе. Это каркас, а не показания прибора: сама механика обмена — команда, запрос, сборка слова, деление — работает ровно как на плате. На живой макетке числа пошевелятся: в комнате я вижу 250–450 лк под потолком, у окна в пасмурный день — 2000–5000, в темноте за шторой — единицы. Светодиод D12 в макетке просто повторяет вердикт «темнота» — заготовка авточника.
Применений у люксового датчика два любимых. Авточник: включаю свет ниже 200 лк, выключаю выше 400 — вилка между порогами не даёт реле трещать на границе, когда облако прошло. Контроль засветки: датчик рядом с экраном ругается, когда блики зашкаливают, — так я поймал неудачное место для метеостанции, которое после обеда заливало солнцем из окна. В обоих случаях важна не абсолютная точность, а повторяемость — и с ней у BH1750 порядок.
Мнение, которое вы имеете право не делить: если задача кончается словами «включить в темноте», фоторезистор за десять рублей честнее — он быстрее, дешевле и не занимает шину. BH1750 я ставлю там, где нужны именно единицы: журналы данных, сравнение мест у окна, досветка по паспортной норме растения. Питание тоже без сюрпризов, но с оговоркой: голому чипу нужны 2,4–3,6 В, а модули GY-302 несут на борту стабилизатор, поэтому 5-вольтовая Uno кормит их напрямую; на 3,3-вольтовом ESP32 я беру питание с рейки 3,3 В, и модуль живёт без каких-либо переходников.
Потолок 65535: что солнце делает с шестнадцатью битами
Шестнадцать бит — это 65535 максимум, и в режиме 0x10 это же число люксов. Комнатный свет и облачность до потолка не добираются, а вот прямое солнце выжимает 80000–100000 лк: счётчик упирается в насыщение, число замирает около 65500 и дальше молчит — прибор не предупреждает, просто врёт втихую. Симптом узнаваемый: «днём на улице показания не меняются». Если датчик планирует жить под открытым небом, обработку этого плато закладывайте сразу.
Лечится снижением времени интегрирования регистром MTreg: заводское значение 69, рабочий диапазон 31–254. При 31 один отсчёт равен примерно 1,9 лк, а потолок поднимается до ~120000 лк — солнце уже не задавит. Наоборот, в сумерках MTreg 254 даёт шаг 0,29 лк и потолок около 18900 лк: тонкий замер там, где света мало. Диапазон и разрешение качаются на одних качелях, и MTreg — рычаг между ними; при смене этого регистра меняется и коэффициент пересчёта, деление на 1,2 верно только для заводских 69.
MTreg пишется парой однобайтовых команд: 0x40 | (MTreg >> 5), следом 0x60 | (MTreg & 0x1F). Для заводских 69 это 0x42 и 0x65, для солнечных 31 — 0x40 и 0x7F. Между командами держите паузу хотя бы 5 мс, а после второй дайте 180 мс на первый пересчитанный отсчёт: я однажды снял график из нулей и успел обвинить датчик в поломке, которой не было. И держите делитель рядом с регистром: коэффициент 1,2 привязан к MTreg 69, при любом другом значении пересчёт едет по формуле из даташита, а скетч молча врёт.
- 1. На модуле ножка ADDR подтянута к земле. Какой адрес у датчика?
- 2. Опкод 0x10 отправлен, requestFrom вызван через 10 мс. Что в ответе?
- 3. Старший байт 0x12, младший 0xC0. Сколько люкс в режиме 0x10?
- 4. На солнце показания замерли около 65500 лк. Что делать?
- 5. Зачем авточнику вилка 200/400 лк вместо одного порога?
- ADDR на земле - 0x23, на питании - 0x5C: два датчика живут на одной шине
- Опкод - один байт: 0x10 непрерывно по 1 лк, 0x11 - 0,5 лк, 0x13 - 4 лк за 16 мс, 0x20 - разовое
- Первый отсчёт готов через 120-180 мс; люксы = (hi<<8|lo) / 1.2, деление на float
- Потолок 16 бит - 65535 лк: на солнце счётчик насыщается; MTreg 31 поднимает планку до ~120000 лк
- Авточник держит вилку порогов: ниже 200 лк включить, выше 400 лк - выключить
Частые вопросы по теме урока
- На модуле ножка ADDR подтянута к земле. Какой адрес у датчика?
- ADDR на земле — базовый адрес 0x23; ножка на питании уводит чип на 0x5C. Сканер шины подтвердит за полминуты. 0x23
- Опкод 0x10 отправлен, requestFrom вызван через 10 мс. Что в ответе?
- Первый отсчёт готов только через 120–180 мс после команды; раньше приходит старьё. Пауза после старта обязательна. результат прошлого прохода или нули
- Старший байт 0x12, младший 0xC0. Сколько люкс в режиме 0x10?
- 0x12C0 — это 4800 отсчётов; деление на 1,2 даёт 4000 лк. Деление должно быть дробным, иначе шаг станет целым. 4000
- На солнце показания замерли около 65500 лк. Что делать?
- Счётчик насытился. MTreg 31 поднимает потолок примерно до 120000 лк, а 254 наоборот сузит его до ~18900. снизить MTreg или принять порог «солнце»
- Зачем авточнику вилка 200/400 лк вместо одного порога?
- Гистерезис: между порогами включения и выключения показания могут болтаться сколько угодно, реле трогают только крайние зоны. чтобы свет не щёлкал на границе