惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
D
Docker
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Cloudflare Blog
雷峰网
雷峰网
A
About on SuperTechFans
小众软件
小众软件
博客园 - Franky
博客园 - 聂微东
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
MongoDB | Blog
MongoDB | Blog
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
量子位
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
罗磊的独立博客
Martin Fowler
Martin Fowler
D
DataBreaches.Net
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
Project Zero
Project Zero
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tailwind CSS Blog
S
Schneier on Security
Blog — PlanetScale
Blog — PlanetScale
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
Security Latest
Security Latest
NISL@THU
NISL@THU
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Свет, тьма, VEML7700 и Python
CyberHamster · 2026-05-22 · via Все публикации подряд на Хабре
Стоит. Измеряет. Управляет подсветкой

Стоит. Измеряет. Управляет подсветкой

Уменя на полке стоит NanoPi Fire3 — старинный SBC с Linux на борту. С течением времени он неизбежно обрастает периферией разной степени бесполезности. Среди прочего, конечно же, в нем прописался экранчик LCD2004 (даже два) со светодиодной подсветкой, который освещал ночную квартиру, как прожектор.

С одной стороны — удобно ходить в туалет, не включая свет, с другой — захотелось гламура, как в мобилке, чтобы, чем меньше вокруг света, тем тусклее была бы подсветка. Для этой цели нужно этот свет как‑то измерять, так что ассортимент периферии было решено расширить датчиком освещенности VEML7700. О том, как я приспосабливал его к делу, и пойдет речь далее.

Что ты за зверь, VEML7700?

Это датчик освещенности с интерфейсом I²C от компании Vishay. Для него производитель заявляет широкий диапазон измерения, высочайшее разрешение, низкое энергопотребление, температурную компенсацию, гибкую конфигурируемость и максимальную близость восприятия к человеческому глазу, что бы именно это не значило.

На самом деле, на момент выбора, я, конечно же, не сильно думал обо всех этих высоких материях, скорее, сыграло роль то, что на Aliexpress этих датчиков навалом по цене в евро‑другой, и в комментариях народ особо их не ругает. Частых упоминаний этого датчика, а, тем более, низкоуровневых разборов протокола управления, я на ресурсе не обнаружил. Так что, есть смысл восполнить этот пробел.

Операция «аппаратная адаптация»

Модули, которые продаются на Aliexpress, один‑в-один передраны с Adafruit‑овских, выглядят вот так:

Внешний вид VEML7700

Внешний вид VEML7700

И дизайн этой схемы нам плохо подходит. Давайте разберемся, почему, посмотрев на нее:

Схема. Источник: Adafruit

Схема. Источник: Adafruit

Сам чип самодостаточен и заточен под работу от 3.3 В, обвязка ему не нужна. Все дополнительные элементы на плате — попытка вписать его в 5-вольтовую схему:

  • LDO DC‑DC преобразователь 5 В — 3.3 В

  • Два полевых транзистора, играющих роль конвертера уровней 5 В — 3.3 В для SCL / SDA

  • Блок из четырех pull‑up подтяжек по 10 кОм, две — к 3.3 В со стороны чипа, две — к 5 В со стороны внешнего устройства

Но мой NanoPi оперирует логикой 3.3 В. Это, что же, придется городить преобразователь уровней 3.3 В — 5 В на стороне SBC, чтобы схема с VEML преобразовывала это на входе обратно в 3.3 В, а на выходе — опять в 5 В, усложняя жизнь внешнему устройству? К тому же, меня терзают сомнения, что все эти многократные преобразования будут стабильно работать на практике.

Так что, мы пойдем другим путем — уберем с платы VEML все ненужное, и соединим чип с SBC напрямую. Убираем DC‑DC, и причастный к нему фильтрующий конденсатор со стороны питания 5 В. Фильтрующие конденсаторы на линии 3.3 В оставляем. Туда же отправляем транзисторы и блок подтяжек:

Раздетая плата

Раздетая плата

Запитать VEML мы сможем через контакт 3Vo, который нам заботливо вывел производитель. Он планировался, как вывод стабилизированного напряжения 3.3 В, а станет вводом. Про контакт VIN для ввода 5 В просто забудем.

Теперь настало время поговорить о подтягивающих резисторах. Вообще, благоразумные производители не подтягивают SCL / SDA к питанию, оставляя это на откуп внешнему устройству. Это позволяет не спалить это самое внешнее устройство, если оно оперирует более низковольтной логикой. В теории, и тут все должно было быть так же. Но проблема в том, что подтягивающие резисторы на стороне SBC имеют огромное сопротивление — в десятки, а то и сотни кОм. Из‑за этого полевые транзисторы на линиях SCL / SDA просто не успевают перезаряжаться с требуемой частотой 100 кГц, и шина I²C начинает сыпать ошибками. Производитель в даташите VEML7700 предлагает ставить 2.2–4.7 кОм, но практика показала, что и это — слишком много, хорошо заработал ~1 кОм. Как Adafruit предлагали работать с исходными 10 кОм, я вообще без понятия.

Так что, мы добавим два подтягивающих резистора по 1 кОм на плату VEML. С учетом напряжения 3.3 В, получим ток 3 мА, вроде, и SBC, и VEML должны его пережить. Контакты, которые остались от исходных подтяжек, как будто созданы для размещения там SMD резисторов формфактора 0805, чем мы и воспользуемся. Это позволит убить сразу двух зайцев: нижними ножками резисторов соединим чип с выводами SCL / SDA на плате, а верхними — подтянем эти SCL / SDA к 3.3 В:

Резисторы 0805 вписались, как родные

Резисторы 0805 вписались, как родные

Правда, эти же верхние ножки закоротили нам 3Vo / 3.3 В с VIN / 5 В, но мы не планируем подавать 5 В в VIN, так что — не страшно.

Если бы я заранее обо всем этом подумал, было бы очевидно, что проще купить голый чип. Он и обошелся бы дешевле в 3–4 раза. Только плату под него пришлось бы травить самому.

Но имеем, что имеем. Произведенных манипуляций более чем достаточно, чтобы уже можно было подсоединять плату VEML к шине I²C на стороне SBC, без боязни ее сжечь.

Про диапазон измерения

Как водится, заявленные производителем диапазон измерения 0–140 клк и разрешение 0.0042 лк имеют скрытые нюансы и недостижимы без кроилова и циркового жонглирования. Статический диапазон датчика меньше заявленного в десятки раз. Но есть возможность регулировать выдержку и усиление сигнала, это дает пространство для маневра.

Можно выставить долгую выдержку и мощное усиление, это повысит разрешение и даст возможность регистрировать жалкие проблески в почти полной темноте. Но любое освещение ярче светлячка будет стабильно зашкаливать в 0xFF. Можно, наоборот, выставить короткую выдержку и скрутить усиление на нет, это, ценой сильного понижения разрешения, даст возможность различать градации очень яркого света, не уходя в насыщение. Но все, что потусклее, будет стабильно выдавать ноль.

Чип предлагает шесть уровней выдержки («integration time», «IT» в терминологии даташита), и четыре уровня усиления («gain»). Это дает десять диапазонов измерения. Да, не 24, потому что различные комбинации выдержки и усиления могут давать одинаковые диапазоны, это тоже добавляет геморроя в алгоритм. Сам чип, конечно же, никак самостоятельно эти параметры не меняет, оставляя это на откуп нам, как пользователям. Предполагается, что мы будем переключать параметры, в зависимости от условий освещенности, и, таким образом, покрывать весь диапазон измерения. Ну что же, попробуем.

Усиление 2

Усиление 1

Усиление 1/4

Усиление 1/8

Выдержка (мс)

Разрешение (люкс / отсчет)

800

0.0042

0.0084

0.0336

0.0672

400

0.0084

0.0168

0.0672

0.1344

200

0.0168

0.0336

0.1344

0.2688

100

0.0336

0.0672

0.2688

0.5376

50

0.0672

0.1344

0.5376

1.0752

25

0.1344

0.2688

1.0752

2.1504

Усиление 2

Усиление 1

Усиление 1/4

Усиление 1/8

Выдержка (мс)

Максимальная освещенность, возможная к измерению (люкс)

800

275

550

2202

4404

400

550

1101

4404

8808

200

1101

2202

8808

17 616

100

2202

4404

17 616

35 232

50

4404

8808

35 232

70 463

25

8808

17 616

70 463

140 926

Таблица разрешений и диапазонов из официального даташита

Протокол управления VEML7700

Датчик управляется по протоколу I²C. Это значит, что он абсолютно пассивный, инициатива при записи и чтении данных полностью принадлежит внешнему устройству. Датчик предоставляет некий набор 16-битных регистров, в которые мы можем писать, или читать из них. Давайте их кратко разберем:

  • 0 — регистр конфигурации. Один из двух регистров, с которыми мы будем работать постоянно. Сюда мы будем записывать значения выдержки и усиления, а так же, первоначальную конфигурацию датчика.

  • 1 — в этот регистр можно записать значение освещенности, превышение которого вызывает прерывание. Прерывание здесь, это не асинхронное притягивание к земле специально выделенной ножки, а значение в регистре «6», которое нужно постоянно и преднамеренно вычитывать (помним, что у нас I²C). Мы и так собираемся постоянно и преднамеренно вычитывать значение освещенности из регистра «4», и можем в любой момент оценить хоть превышение, хоть занижение чего угодно. В свете этого, пользоваться прерыванием не вижу смысла.

  • 2 — то же самое, что и регистр «1», только для значения занижения.

  • 3 — режим энергосбережения. Кратно усложняет расчет времени обновления. Мы им пользоваться не будем.

  • 4 — значение освещенности в сыром виде. Это, т. н. канал «ALS», на нем стоят фильтры ИК и УФ излучения, а кривая восприятия приближена к человеческому глазу. Именно отсюда мы и будем его постоянно читать.

  • 5 — значение освещенности т. н. канала «WHITE». На нем нет фильтров и компенсации кривой восприятия. Нужен для всяких специфических задач. А нам — не нужен.

  • 6 — регистр, содержащий флаги того самого прерывания при превышении / занижении значений из регистров «1» и «2». Т. е. прерывания, которым мы не будем пользоваться.

  • 7 — регистр, из которого можно прочитать «Device ID». Это некий фиксированный байт, плюс I²C-адрес чипа. Чтобы что-то прочитать из этого регистра, надо заранее знать I²C-адрес чипа, так что полезного применения я так и не смог придумать.

Для полноты картины, стоит разобрать битовую карту регистра конфигурации «0»:

Поле

Количество бит

Описание

shut_down

1

Программно включаем / выключаем датчик. В ряде случаев, без этого конфигурация не применяется. Либо в целях снижения энергопотребления.

interrupt_enable

1

Включает бесполезные «прерывания».

reserved1

2

Не используется.

persistence_protect_number

2

Значение фильтра дребезга перед выставлением «прерывания», которое мы не используем.

integration_time

4

Длительность выдержки. Задается не напрямую, а предопределенными кодами.

reserved2

1

Не используется.

gain

2

Коэффициент усиления. Задается не напрямую, а предопределенными кодами.

reserved3

3

Не используется.

Чтобы держать нас в тонусе, выдержка и усиление не задаются непосредственными значениями, а кодируются неочевидными кодами вот из такой таблички:

Выдержка (мс)

Усиление

25

1100

1 / 8

10

50

1000

1 / 4

11

100

0000

1

00

200

0001

2

01

400

0010

800

0011

Этих данных нам должно быть достаточно, чтобы приступить к реализации алгоритма.

Реализация алгоритма, вспомогательные конструкции

Пока все эти таблицы и регистры свежи в памяти, давайте запротоколируем эти знания на python.

Начнем со структуры, раскладывающей поля конфигурационного регистра по битам. Для этих целей модуль ctypes содержит удобный импорт Structure:

import sys, time, smbus2
from ctypes import c_uint16, Structure, Union

class ConfigurationRegisterBits(Structure):
    fields = [
        (“shut_down”, c_uint16, 1),
        (“interrupt_enable”, c_uint16, 1),
        (“reserved1”, c_uint16, 2),
        (“persistence_protect_number”, c_uint16, 2),
        (“integration_time”, c_uint16, 4),
        (“reserved2”, c_uint16, 1),
        (“gain”, c_uint16, 2), (“reserved3”, c_uint16, 3)
    ]

Теперь реализуем сам класс, позволяющий удобно управлять конфигурационным регистром. Дадим имена нескольким магическим числам, чтобы по коду было понятно, что происходит. Так же наложим нашу структуру с битовыми полями на беззнаковый 16-битный тип. В этом нам на помощь придет импорт Union из все того же модуля ctypes:

 class ConfigurationRegister(Union):
    REGISTER_ADDRESS = 0

    POWER_ON = 0
    SHUT_DOWN = 1

    RESOLUTION_800MS_GAIN2 = 0.0042

    fields = [("b", ConfigurationRegisterBits), ("asint", c_uint16)]

Дадим имена магическим кодам, нужным для записи значения выдержки в регистр. А, также, составим словарик, где эти коды соответствуют реальным значениям в миллисекундах. Он пригодится нам позже, при вычислении времени ожидания и текущего разрешения:

    INTEGRATION_TIME_25_MS = 0b1100
    INTEGRATION_TIME_50_MS = 0b1000
    INTEGRATION_TIME_100_MS = 0b0000
    INTEGRATION_TIME_200_MS = 0b0001
    INTEGRATION_TIME_400_MS = 0b0010
    INTEGRATION_TIME_800_MS = 0b0011
    
    integration_times = {
        INTEGRATION_TIME_25_MS : 25.0,
        INTEGRATION_TIME_50_MS : 50.0,
        INTEGRATION_TIME_100_MS : 100.0,
        INTEGRATION_TIME_200_MS : 200.0,
        INTEGRATION_TIME_400_MS : 400.0,
        INTEGRATION_TIME_800_MS : 800.0
    }

Аналогичным образом проделаем все то же самое для усиления – пропишем имена для магических кодов, и создадим словарик с реальными значениями:

    GAIN_1_8 = 0b10
    GAIN_1_4 = 0b11
    GAIN_1 = 0b00
    GAIN_2 = 0b01
    
    gains = {
        GAIN_1_8 : 1.0 / 8.0,
        GAIN_1_4 : 1.0 / 4.0,
        GAIN_1 : 1.0,
        GAIN_2 : 2.0
    }

В конструктор будем передавать объект I²C-шины и адрес датчика на ней. Там же обнулим все битовые поля разом, через наложенный поверх 16-битный тип:

    def __init__(self, i2c_bus, device_address):
        self._i2c_bus = i2c_bus
        self._device_address = device_address
        self.asint = 0

Добавим немного удобства. Маленький хелпер, который позволит нам перечислять поля и значения в произвольном порядке, и записывать их все разом в регистр, пользуясь тем самым 16-битный типом. Выглядеть будет как-то так:
register.commit(shut_down = 1, integration_time = 2, gain = 3).

    def commit(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self.b, key, value)

        self._i2c_bus.write_i2c_block_data(self._device_address,
                                           self.REGISTER_ADDRESS,
                                           self.asint.to_bytes(2, "little"))

Для расчета конечного значения освещенности потребуется узнавать текущее разрешение измерения. К счастью, зависимость разрешения от выдержки и усиления — абсолютно линейна, как можно убедиться, посмотрев на табличку в позапрошлой главе. Зная разрешение для какой-то пары выдержка-усиление, можно посчитать разрешение для текущей пары через простые пропорции. Именно для этого мы и прикопали чуть выше константу RESOLUTION_800MS_GAIN2, это самое высокое разрешение, оно будет нашим опорным.

    def resolution(self):
        return self.RESOLUTION_800MS_GAIN2 *
               self.gains[self.GAIN_2] *
               self.integration_times[self.INTEGRATION_TIME_800_MS] /
               ( self.gains[self.b.gain] *
                 self.integration_times[self.b.integration_time] )

Последним штрихом добавим геттер для текущего значения выдержки, оно нам понадобится:

    def integration_time(self):
        return self.integration_times[self.b.integration_time]

Реализация алгоритма, основная логика

В даташите прописан достаточно дубовый алгоритм, как мерить освещенность, задействуя всю доступную мощь датчика, и ничего не упустив.

Предлагается, перед началом измерения, плясать от выдержки в 100 мс, и от усиления в 1/8. Такая выдержка выбрана, я полагаю, потому что в нее укладывается кратное число периодов мерцания света, как в европейской осветительной сети — 5 штук при 50 Гц, так и в американской — 6 штук при 60 Гц. А такое усиление, потому что — самое маленькое и вносит меньше искажений, усиливая шум.

Далее анализируется количество сырых отсчетов, поступившее с датчика. В диапазоне 100-10 000 cts оно полагается нормальным, и не требует перенастройки датчика. В противном же случае, мы начинаем приседать и подстраиваться.

Блок-схема из даташита. Источник: Vishay

Блок-схема из даташита. Источник: Vishay

Создадим, наконец, класс нашего устройства! По традиции, начнем с фиксации магических чисел — номеров регистров, шин и адресов, чтобы понимать, кто на ком стоял. Мой датчик висит на первой шине, а адрес у них захардкожен из коробки. И поменять его не представляется возможным:

class VEML7700:
    HIGHT_THRESHOLD_WINDOW_REGISTER_ADDRESS = 1
    LOW_THRESHOLD_WINDOW_REGISTER_ADDRESS = 2
    POWER_SAVING_REGISTER_ADDRESS = 3
    AMBIENT_LIGHT_REGISTER_ADDRESS = 4
    
    DEFAULT_VEML7700_I2C_BUS = 1
    DEFAULT_VEML7700_I2C_ADDRESS = 0x10

В конструкторе создадим I²C-шину и конфигурационный регистр, заботливо заготовленный заранее. А, заодно, выключим пороговые значения прерывания и режим энергосбережения, ими мы пользоваться не будем:

    def __init__(self,
                 i2c_bus_number = DEFAULT_VEML7700_I2C_BUS,
                 device_address = DEFAULT_VEML7700_I2C_ADDRESS):
        self._device_address = device_address
        self._i2c_bus = smbus2.SMBus(i2c_bus_number)
        self._configuration_register = ConfigurationRegister(self._i2c_bus,
                                                             self._device_address)
        
        self._i2c_bus.write_word_data(self._device_address,
                                      self.HIGHT_THRESHOLD_WINDOW_REGISTER_ADDRESS,
                                      0)

        self._i2c_bus.write_word_data(self._device_address,
                                      self.LOW_THRESHOLD_WINDOW_REGISTER_ADDRESS,
                                      0)
        
        self._i2c_bus.write_word_data(self._device_address,
                                      self.POWER_SAVING_REGISTER_ADDRESS,
                                      0)

Добавим лаконичный геттер, который будет читать из регистра то самое значение освещенности в сырых отсчетах. Обратите внимание на то, что, перед чтением, мы ждем время, равное полутора (для надежности) выдержкам, иначе эта самая выдержка не успеет выдержаться:

    def read_counts(self):
        time.sleep(self._configuration_register.integration_time() * 1.5 / 1000.0)
        return self._i2c_bus.read_word_data(self._device_address,
                                            self.AMBIENT_LIGHT_REGISTER_ADDRESS)

Разобьем ветки блок-схемы на несколько методов. Это позволит избежать простыни кода в основном методе.

Если у нас недосвет — количество сырых отсчетов меньше, или равно 100 cts, то мы начинаем последовательно повышать усиление от 1/8, по одной ступени, пока отсчетов не станет больше 100, или пока мы не упремся в самое большое усиление. Выдержку пока не трогаем, считается приоритетным попытаться удержать ее равной 100 мс, из-за той самой кратности пульсациям электросети.

Обратите внимание, что, для того, чтобы конфигурация, записанная в регистр, применилась в реальной жизни, датчику нужно сделать выкл-вкл. Все для нашего удобства. Хорошо, что разводным ключом постучать не нужно.

    def increase_gain_adjusting(self):
        gain_list = (ConfigurationRegister.GAIN_1_4,
                     ConfigurationRegister.GAIN_1,
                     ConfigurationRegister.GAIN_2)

        for gain_value in gain_list:
            self._configuration_register.commit(
                shut_down = ConfigurationRegister.SHUT_DOWN,
                gain = gain_value
            )
            self._configuration_register.commit(
                shut_down = ConfigurationRegister.POWER_ON
            )
    
            counts = self.read_counts()

            if counts > 100:
                break

        return counts

Если усиление уперлось в двоечку, а недосвет, по прежнему, на месте, то усилять дальше уже некуда. Придется пожертвовать драгоценными 100 мс, и начать увеличивать выдержку от 100 мс, по одной ступени, пока отсчетов не станет больше 100, или пока выдержку не станет некуда повышать:

    def increase_integration_time_adjusting(self):
        integration_time_list = (ConfigurationRegister.INTEGRATION_TIME_200_MS,
                                 ConfigurationRegister.INTEGRATION_TIME_400_MS,
                                 ConfigurationRegister.INTEGRATION_TIME_800_MS)

        for integration_time_value in integration_time_list:
            self._configuration_register.commit(
                shut_down = ConfigurationRegister.SHUT_DOWN,
                integration_time = integration_time_value
            )
            self._configuration_register.commit(
                shut_down = ConfigurationRegister.POWER_ON
            )

            counts = self.read_counts()

            if counts > 100:
                break

        return counts

Если у нас пересвет — количество сырых отсчетов больше 10 000 cts, то мы начинаем последовательно уменьшать выдержку ниже 100 мс, по одной ступени, пока отсчетов не станет меньше 10 000, или пока мы не упремся в самую маленькую выдержку. Уменьшать усиление — дальше некуда, оно уже и так самое маленькое — 1/8.

    def decrease_integration_time_adjusting(self):
        integration_time_list = (ConfigurationRegister.INTEGRATION_TIME_50_MS,
                                 ConfigurationRegister.INTEGRATION_TIME_25_MS)

        for integration_time_value in integration_time_list:
            self._configuration_register.commit(
                shut_down = ConfigurationRegister.SHUT_DOWN,
                integration_time = integration_time_value
            )
            self._configuration_register.commit(
                shut_down = ConfigurationRegister.POWER_ON
            )

            counts = self.read_counts()

            if counts <= 10000:
                break

        return counts

Если вдруг вам показалось, что приседаний было слишком мало, то производитель припрятал для вас последний сюрприз. Когда освещенность превышает 1000 люкс, датчик теряет линейность в измерениях, и кривая отсчетов к реальной освещенности начинает потихоньку загибаться вниз. Понятное дело, что ее разгибание обратно производитель возложил на наши плечи — кому мешает, тот и разгибает. С помощью корректирующего полинома четвертого порядка:

    def correct_non_linear_distortion(self, lux):
        if lux <= 1000:
            return lux

        return 6.0135e-13 * (lux ** 4) -
               9.3924e-9 * (lux ** 3) +
               8.1488e-5 * (lux ** 2) +
               1.0023 * lux

Теперь, наконец, есть все кирпичики, нужные, чтобы написать основной алгоритм. Начинаем с базовой выдержки в 100 мс, и усиления в 1/8, затем, при надобности, подкручиваем чувствительность до победного конца, и корректируем нелинейные искажения в этом самом победном конце:

    def read_lux(self):
        self._configuration_register.commit(
            shut_down = ConfigurationRegister.SHUT_DOWN,
            integration_time = ConfigurationRegister.INTEGRATION_TIME_100_MS,
            gain = ConfigurationRegister.GAIN_1_8
        )
        self._configuration_register.commit(
            shut_down = ConfigurationRegister.POWER_ON
        )

        counts = self.read_counts()

        if counts <= 100:
            counts = self.increase_gain_adjusting()
            if counts <= 100:
                counts = self.increase_integration_time_adjusting()
        elif counts > 10000:
            counts = self.decrease_integration_time_adjusting()

        self._configuration_register.commit(
            shut_down = ConfigurationRegister.SHUT_DOWN
        )
  
        return self.correct_non_linear_distortion(
                   counts * self._configuration_register.resolution()
               )

А как станет не нужен, не забываем накрыть кружевной салфеткой, чтобы датчик отдохнул. Заодно, сэкономим драгоценные 50 мкА:

    def close(self):
        self._configuration_register.commit(
            shut_down = ConfigurationRegister.SHUT_DOWN
        )
        self._i2c_bus.close()

Наслаждаемся результатом

Использование результата трудов незамысловатое, импортируем, читаем:

from veml7700 import VEML7700

v77 = VEML7700()
try:
    while True:
        print("Light:", v77.read_lux(), "lx")
finally:
    v77.close()

Воодушевившись успехом, заводим PWM GPIO управлять подсветкой дисплея через полевой транзистор. N-канальный AO3400, как раз, прекрасно дружит с PWM в 3.3 В. Не забываем поставить между фиолетовым PWM GPIO и затвором токоограничительный резистор, чтобы не спалить транзистор. Еще стоит подтянуть затвор к оранжевой земле, чтобы не было чудес в отсутствие сигнала. И пусть вас не смущает, что оранжевый сток соединен с "A", это потому, что обозначение катода и анода на плате китайцы поменяли местами, чтобы отсечь слабых духом:

Один AO3400, чтобы править катодом

Один AO3400, чтобы править катодом

Если вы не разглядели, где там транзистор, не расстраивайтесь. Он прячется с обратной стороны. Эти AO3400 у меня (и, похоже, в природе) существуют только в SMD варианте, в корпусе SOT-23. Так что приходится использовать вот такую платку-переходник, чтобы паять к нему слоновьи компоненты (обратите внимание на нумерацию выводов, слабые духом и здесь не пройдут):

Лилипут в стране Гулливеров

Лилипут в стране Гулливеров

Строим логику регулирования PWM, исходя из измеренных люксов. Гениальный код этого решения я приводить не буду, он очевиден (если не вспоминать про гамма-коррекцию), а кода в статье и так уже навалом. Хвалим себя за приручение прожектора:

Про то, что быстрее и эффективнее было снять джампер с питания подсветки, и впаять в разрыв фоторезистор, я видел, слышал и понимаю, как это работает. Но программно получить показания фоторезистора и повлиять на результаты регулирования вряд ли получилось бы. Да и о богатом внутреннем мире VEML7700 я бы так ничего и не узнал.

Cliffhanger

Если богатый внутренний мир VEML7700 оказался вам интересен, не стесняйтесь, напишите об этом. Такой интерес мог бы вылиться в следующую статью, с написанием на C быстрого и нетребовательного Industrial IO драйвера для ядра Linux. Было бы много интересного — куда более оптимальное переключение режимов, асинхронное снятие показаний и чтение из драйвера, и мужественное преодоление невозможности использовать FPU.

Что почитать

Исходник на GitHub
Vishay VEML7700 datasheet
Vishay VEML7700 designing manual