
Уменя на полке стоит NanoPi Fire3 — старинный SBC с Linux на борту. С течением времени он неизбежно обрастает периферией разной степени бесполезности. Среди прочего, конечно же, в нем прописался экранчик LCD2004 (даже два) со светодиодной подсветкой, который освещал ночную квартиру, как прожектор.
С одной стороны — удобно ходить в туалет, не включая свет, с другой — захотелось гламура, как в мобилке, чтобы, чем меньше вокруг света, тем тусклее была бы подсветка. Для этой цели нужно этот свет как‑то измерять, так что ассортимент периферии было решено расширить датчиком освещенности VEML7700. О том, как я приспосабливал его к делу, и пойдет речь далее.
Что ты за зверь, VEML7700?
Это датчик освещенности с интерфейсом I²C от компании Vishay. Для него производитель заявляет широкий диапазон измерения, высочайшее разрешение, низкое энергопотребление, температурную компенсацию, гибкую конфигурируемость и максимальную близость восприятия к человеческому глазу, что бы именно это не значило.
На самом деле, на момент выбора, я, конечно же, не сильно думал обо всех этих высоких материях, скорее, сыграло роль то, что на Aliexpress этих датчиков навалом по цене в евро‑другой, и в комментариях народ особо их не ругает. Частых упоминаний этого датчика, а, тем более, низкоуровневых разборов протокола управления, я на ресурсе не обнаружил. Так что, есть смысл восполнить этот пробел.
Операция «аппаратная адаптация»
Модули, которые продаются на Aliexpress, один‑в-один передраны с 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 В:

Правда, эти же верхние ножки закоротили нам 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 оно полагается нормальным, и не требует перенастройки датчика. В противном же случае, мы начинаем приседать и подстраиваться.

Создадим, наконец, класс нашего устройства! По традиции, начнем с фиксации магических чисел — номеров регистров, шин и адресов, чтобы понимать, кто на ком стоял. Мой датчик висит на первой шине, а адрес у них захардкожен из коробки. И поменять его не представляется возможным:
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 у меня (и, похоже, в природе) существуют только в SMD варианте, в корпусе SOT-23. Так что приходится использовать вот такую платку-переходник, чтобы паять к нему слоновьи компоненты (обратите внимание на нумерацию выводов, слабые духом и здесь не пройдут):

Строим логику регулирования PWM, исходя из измеренных люксов. Гениальный код этого решения я приводить не буду, он очевиден (если не вспоминать про гамма-коррекцию), а кода в статье и так уже навалом. Хвалим себя за приручение прожектора:
Про то, что быстрее и эффективнее было снять джампер с питания подсветки, и впаять в разрыв фоторезистор, я видел, слышал и понимаю, как это работает. Но программно получить показания фоторезистора и повлиять на результаты регулирования вряд ли получилось бы. Да и о богатом внутреннем мире VEML7700 я бы так ничего и не узнал.
Cliffhanger
Если богатый внутренний мир VEML7700 оказался вам интересен, не стесняйтесь, напишите об этом. Такой интерес мог бы вылиться в следующую статью, с написанием на C быстрого и нетребовательного Industrial IO драйвера для ядра Linux. Было бы много интересного — куда более оптимальное переключение режимов, асинхронное снятие показаний и чтение из драйвера, и мужественное преодоление невозможности использовать FPU.
Что почитать
Исходник на GitHub
Vishay VEML7700 datasheet
Vishay VEML7700 designing manual
