104б. ИК-термометр MLX90614 по SMBus: температура объекта без контакта, PEC, излучательная способность и поле зрения
В разделе 96 NTC и DS18B20 мерили температуру того, к чему прижаты, в 104 термопара мерила своим спаем, в 104а термопара сидела прямо в жале паяльника. MLX90614 ничего не касается: он ловит тепловое излучение предмета и по нему считает температуру. Так меряют лоб, кружку, мотор на ходу, плату под нагрузкой. Здесь — как он это делает и почему ему нужно знать излучательную способность, как говорить с ним по SMBus и проверять контрольную сумму PEC, как перевести код в градусы целыми, что такое поле зрения и почему фон рядом с предметом меняет показание, как правильно записать новую излучательную способность в EEPROM.
Опоры: RM0008 (DocID13902 Rev 17): приём больше двух байт ведущим,
способ 2, с. 762–763; SMBus против I2C — 100 кГц, 10 кГц, 35 мс,
с. 767; PEC — полином x⁸ + x² + x + 1 по всем байтам вместе с адресами,
с. 771. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 —
I2C1_SDA, оба FT, с. 32. Устройство SMBus и PEC — раздел 17.9, здесь
не повторяются. Документа на MLX90614 (Melexis) в проекте нет: адрес,
команды, ячейки EEPROM, шаг 0,02 K, флаг ошибки, диапазон, поле зрения
версий, питание и порядок записи EEPROM взяты из документации
производителя и заложены в модель — по документации производителя, не
проверено. Готового драйвера MLX90614 в Zephyr 4.4.2 на диске нет; есть
общий SMBus, и его smbus_pec() считает PEC так же, как драйвер этого
раздела. Все числа — прогон драйвера _verify/ir104c/mlx104c.c на
компьютере (gcc 12.2.0) со своей моделью MLX90614 как ведомого SMBus и
того, на что он смотрит, на Python 3.11; прошивка собрана armclang 6.24
под Cortex-M3. Излучение в модели пропорционально T⁴, поле зрения —
конус с равной чувствительностью, шум и материалы — допущения: числа ниже
говорят о модели, а не о датчике.
Как это называется в документации
Оригинал Перевод thermopile термобатарея: много термопар последовательно object temperature ( Tobj)температура объекта ambient temperature ( Ta)температура корпуса датчика emissivity (ε) излучательная способность: доля излучения чёрного тела field of view (FOV) поле зрения: угол, из которого собирается излучение spot size пятно: что датчик видит на заданном расстоянии packet error code (PEC) контрольная сумма посылки SMBus repeated start повторный старт clock low timeout тайм-аут: SCL в нуле дольше 35 мс thermal shock тепловой удар: корпус датчика прогрет неравномерно
104б.1 Как он меряет: термобатарея, закон Стефана–Больцмана и ε
Всякое тело излучает, и тем сильнее, чем оно горячее: по закону
Стефана–Больцмана мощность растёт как четвёртая степень абсолютной
температуры, P ∝ ε · T⁴. Комнатные предметы светят в дальнем ИК, глазом
этого не видно, а термобатарея видит. Термобатарея — много термопар
последовательно (термопара — 104.1): горячие спаи на тонкой мембране,
которую греет пришедшее излучение, холодные — на корпусе. Напряжение
говорит о разнице между тем, что пришло от предмета, и тем, что излучает
сам корпус. Поэтому рядом стоит второй датчик — температуры корпуса Ta,
и микросхема по обоим считает температуру объекта Tobj. По документации
производителя внутри — усилитель, АЦП и вычислитель, наружу выходят
готовые слова с шагом 0,02 K.
Излучательная способность ε — сколько тело излучает по сравнению с идеальным чёрным телом той же температуры: 1 — чёрное тело, 0 — идеальное зеркало. Остаток 1 − ε тело отражает: в датчик приходит и чужое излучение, отражённое от предмета. Микросхема не знает, на что смотрит, и считает по заданному ε. В модели излучение того, что в поле зрения, — ε · T⁴ объекта плюс (1 − ε) · T⁴ отражённого, а показание — Tobj⁴ = Ta⁴ + (излучение − Ta⁴) / ε в настройке.
Как ошибается показание, если ε задан неверно. В модели предмет 100 °C с ε = 0,95, комната и корпус — 25 °C:
| ε в настройке | 0,90 | 0,92 | 0,94 | 0,95 | 0,96 | 0,98 | 1,00 |
|---|---|---|---|---|---|---|---|
| показание, °C | 103,03 | 101,79 | 100,59 | 100,00 | 99,42 | 98,30 | 97,21 |
Каждая сотая ε — около 0,6 °C при 100 °C. И чем дальше предмет от температуры корпуса, тем больше ошибка: при ε = 1,00 в настройке и настоящем 0,95 —
| Предмет | 0 °C | 25 °C | 40 °C | 60 °C | 100 °C | 200 °C |
|---|---|---|---|---|---|---|
| показание, °C | 1,42 | 25,00 | 39,30 | 58,50 | 97,21 | 194,94 |
При 25 °C ошибки нет вовсе: предмет и корпус одной температуры, и разницы нет, что бы ни отражалось. Ниже корпуса знак ошибки меняется — холодный предмет кажется теплее.
104б.2 Подключение к F103: SMBus поверх I2C1
| Вывод модуля | STM32F103 | Назначение |
|---|---|---|
| VIN | 3,3 В | питание: версия на 3 В — только от 3,3 В |
| GND | GND | земля |
| SCL | PB6 | такт I2C1 |
| SDA | PB7 | данные I2C1; у микросхемы этот же вывод — выход ШИМ |
По документации производителя MLX90614 выпускают на 5 В и на 3 В — это буква в обозначении версии; голую микросхему на 3 В от 5 В не питают (по документации производителя, не проверено). Модули со стабилизатором берут и 5 В, но подтяжки тогда бывают к 5 В: PB6 и PB7 — FT (с. 32), это они выдержат.
SMBus — это I2C с правилами (17.9): не быстрее 100 кГц и не медленнее
10 кГц, SCL в нуле не дольше 35 мс (RM0008, с. 767). Значит, I2C1 — в
стандартном режиме на 100 кГц, как в 17.3: CCR = 180, TRISE = 37:
static void gpio_i2c1_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
/* PB6, PB7 — альтернативная функция, открытый сток, 2 МГц (1110): фронтам SMBus больше не нужно */
GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xEE000000u;
I2C1->CR1 = 0; /* настраивать при PE = 0 */
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = APB1_MHZ * 1000000u / (2u * 100000u); /* стандартный режим: 36 МГц / (2 × 180) = 100 кГц */
I2C1->TRISE = APB1_MHZ + 1u; /* 1000 нс × 36 МГц = 36 → 37 */
I2C1->CR1 = I2C_CR1_PE;
}
Чтение — команда, повторный старт и три байта: младший, старший и PEC.
Три байта ведущий F103 принимает способом 2 (с. 762–763): дождаться
BTF, снять ACK, взять первый байт, снова BTF, STOP, второй, потом
третий:
/* приём трёх байт (младший, старший, PEC) — способ 2, RM0008, с. 762–763 */
static int rx3(uint8_t *rx)
{
int rc;
(void)I2C1->SR2; /* ADDR снят, ACK = 1 */
if ((rc = sr1_wait(I2C_SR1_BTF)) != MLX_OK) /* байт 1 в DR, байт 2 в сдвиговом, SCL прижат */
return rc;
I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
rx[0] = (uint8_t)I2C1->DR; /* пошёл приём PEC с NACK */
if ((rc = sr1_wait(I2C_SR1_BTF)) != MLX_OK)
return rc;
I2C1->CR1 |= I2C_CR1_STOP;
rx[1] = (uint8_t)I2C1->DR;
if ((rc = sr1_wait(I2C_SR1_RXNE)) != MLX_OK)
return rc;
rx[2] = (uint8_t)I2C1->DR;
I2C1->CR1 |= I2C_CR1_ACK;
return MLX_OK;
}
Выход ШИМ здесь не нужен. По документации производителя вывод SDA может выдавать ШИМ с заполнением по температуре, а в SMBus его переводят особым условием на шине — SCL в нуле дольше заданного времени (по документации производителя, не проверено). Драйвер этого раздела работает только по SMBus, ШИМ в модели нет.
104б.3 Чтение слова, PEC и перевод в градусы
Адрес на шине по документации производителя — 0x5A (байт 0xB4 для записи и 0xB5 для чтения, 17.2). Команда — первый байт после адреса: три старших бита выбирают память, пять младших — ячейку:
| Команда | Что | Что в слове |
|---|---|---|
| 0x06 | RAM: Ta |
температура корпуса, шаг 0,02 K |
| 0x07 | RAM: Tobj1 |
температура объекта, шаг 0,02 K; старший бит — флаг ошибки |
| 0x08 | RAM: Tobj2 |
второй объект — только у двухзонных версий |
| 0x24 | EEPROM, ячейка 0x04 | излучательная способность: 65535 × ε |
| 0x2E | EEPROM, ячейка 0x0E | младший байт — адрес на шине |
Всё это — по документации производителя, не проверено. Слово идёт
младшим байтом вперёд, за ним — PEC. PEC — CRC-8 с полиномом 0x07 по
всем байтам посылки, и адреса тоже входят (RM0008, с. 771; код и
проверка на «123456789» — 17.9). В чтении это пять байт: адрес с
записью, команда, адрес с чтением, младший, старший. Блок I2C у F103 умеет считать
PEC сам (бит ENPEC, 17.9), но драйвер считает программно — тот же код
работает и на стенде:
/* CRC-8 SMBus: полином x^8 + x^2 + x + 1 (0x07), старший бит первым; начальное значение — 0 */
uint8_t mlx_crc8(uint8_t crc, const uint8_t *p, uint32_t n)
{
while (n--) {
crc ^= *p++;
for (int i = 0; i < 8; i++)
crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x07u) : (uint8_t)(crc << 1);
}
return crc;
}
/* чтение слова: адрес + запись, команда, повторный старт, адрес + чтение, младший байт, старший, PEC;
* PEC считается по всем пяти байтам до него — адреса тоже входят */
int mlx_read_word(mlx_t *d, uint8_t cmd, uint16_t *w)
{
uint8_t f[5] = {(uint8_t)(d->addr << 1), cmd, (uint8_t)(d->addr << 1 | 1u), 0, 0}, rx[3];
int rc;
for (uint32_t i = 0; i < MLX_TRIES; i++) {
if (i)
d->retries++;
if ((rc = xfer_done(d, mlx_port_xfer(d->addr, &cmd, 1, rx, 3))) != MLX_OK)
return rc;
f[3] = rx[0];
f[4] = rx[1];
if (mlx_crc8(0, f, 5) == rx[2]) {
*w = (uint16_t)(rx[0] | rx[1] << 8); /* младший байт первым */
return MLX_OK;
}
d->pec_errors++; /* помеха на линии: прочитать ещё раз */
}
return MLX_E_PEC;
}
В модели чтение Tobj1 при предмете 50 °C — B4 07 B5 24 3F 41: слово 0x3F24, PEC
0x41. Чтение Ta — B4 06 B5 3B 3A D8. Обмен — 57 тактов SCL, 570 мкс на
100 кГц; запись слова — 47 тактов, 470 мкс. Ошибку PEC драйвер
повторяет до трёх раз: это помеха, и следующий кадр обычно чистый.
Что ловит PEC. В модели каждый десятый кадр портится: в первом случае один бит, во втором — один байт целиком, в третьем — два байта из трёх:
| Порча | Чтений | Кадров с повторами | PEC не сошёлся | Не прочитано за три раза | Неверных принято | Без проверки PEC: испорчено / неверных принято |
|---|---|---|---|---|---|---|
| один бит | 4000 | 4448 | 451 | 3 | 0 | 429 / 291 |
| один байт | 4000 | 4452 | 453 | 1 | 0 | 422 / 282 |
| два байта | 20 000 | 22 227 | 2254 | 31 | 3 | 2012 / 2012 |
Один испорченный бит и даже целый байт CRC-8 ловит всегда: ни одного
неверного значения не принято. Два испорченных байта уже могут
проскочить: в прогоне 3 раза из 2257. Перебор всех 195 075 порч
ровно двух байт из трёх: PEC не замечает 765 — одну из 255. Без
проверки PEC в программу ушли бы все испорченные данные; при порче одного
бита неверных меньше, чем испорченных кадров, — часть ошибок попала в
сам PEC. «Не прочитано за три раза» — три испорченных кадра подряд:
драйвер честно возвращает MLX_E_PEC.
Перевод в градусы. Слово × 0,02 — кельвины, минус 273,15 — градусы. У F103 нет FPU, поэтому всё в сотых долях градуса целыми: слово × 2 − 27315. Старший бит слова — не температура, а флаг ошибки:
/* шаг 0,02 K: сотые градуса = raw × 2 − 27315 */
int32_t mlx_raw_to_c100(uint16_t raw)
{
return (int32_t)raw * 2 - 27315;
}
/* температура в сотых °C; старший бит слова — флаг ошибки, а не температура */
int mlx_read_temp(mlx_t *d, uint8_t cmd, int32_t *c100)
{
uint16_t w;
int rc = mlx_read_word(d, cmd, &w);
if (rc != MLX_OK)
return rc;
if (w & MLX_FLAG)
return MLX_E_FLAG;
*c100 = mlx_raw_to_c100(w);
return MLX_OK;
}
| Температура | −70 °C | −40 °C | 0 °C | 25 °C | 36,6 °C | 100 °C | 380 °C |
|---|---|---|---|---|---|---|---|
| ближайший код | 0x27AE | 0x2D8A | 0x355A | 0x3A3C | 0x3C80 | 0x48E2 | 0x7F92 |
| обратно, °C | −69,99 | −39,99 | 0,01 | 25,01 | 36,61 | 100,01 | 380,01 |
273,15 / 0,02 = 13 657,5 — полшага: ровные градусы попадают между
кодами, и обратно получается на 0,01 °C больше. Ta в модели при 25 °C —
24,99 °C: модель округлила полшага в другую сторону. Наибольшее слово без флага, 0x7FFF, — 382,19 °C;
диапазон объекта по документации производителя — −70…380 °C (не
проверено). В модели предмет 500 °C и −100 °C — вне диапазона: слово
0x8000, драйвер возвращает MLX_E_FLAG, −4. Без проверки флага это было
бы 382,21 °C.
104б.4 Поле зрения и пятно: что попадает в измерение
Датчик собирает излучение из конуса — поля зрения. По документации производителя у разных версий оно разное: 90°, 35°, 10° и уже — узкое дают линза или длинная трубка (не проверено). На расстоянии d пятно — круг диаметром 2 · d · tg(FOV / 2):
| Поле зрения | 10 мм | 50 мм | 100 мм | 300 мм | 1000 мм |
|---|---|---|---|---|---|
| 90° | 20 мм | 100 мм | 200 мм | 600 мм | 2000 мм |
| 35° | 6 мм | 32 мм | 63 мм | 189 мм | 631 мм |
| 10° | 2 мм | 9 мм | 17 мм | 52 мм | 175 мм |
Всё, что попало в пятно, датчик усредняет по излучению. Если предмет меньше пятна, остальное — фон. В модели чашка 70 мм, 60 °C, ε = 0,95 (в настройке тоже 0,95), стена и воздух 25 °C:
| Расстояние | 20 мм | 50 мм | 100 мм | 200 мм | 500 мм |
|---|---|---|---|---|---|
| 90°, показание | 60,00 | 43,60 | 29,98 | 26,26 | 25,20 |
| 90°, с поправкой | 60,01 | 60,01 | 59,91 | 59,47 | 58,85 |
| 35°, показание | 60,00 | 60,00 | 60,00 | 37,08 | 27,03 |
| 35°, с поправкой | 60,01 | 60,01 | 60,01 | 60,01 | 60,01 |
| 10°, показание | 60,00 | 60,00 | 60,00 | 60,00 | 48,69 |
| 10°, с поправкой | 60,01 | 60,01 | 60,01 | 60,01 | 60,01 |
С 90° уже на 100 мм чашка — лишь 0,123 пятна, и показание 29,98 °C вместо 60. С 10° до 200 мм чашка закрывает всё пятно. Отсюда правило: предмет должен быть больше пятна, лучше вдвое, — подвести датчик ближе или взять узкое поле зрения.
Почему меряют фон рядом. Если знать долю пятна w, которую занимает
предмет, и фон, показание можно разделить обратно: T⁴ = (Tm⁴ − (1 − w) ·
Tф⁴) / w. Та же формула при Tф = Ta и w = ε — поправка на излучательную
способность. В коде одна функция, всё в единицах слова и в 64 битах:
/* показание — смесь двух излучений с долями w и 1 − w (в тысячных): T⁴ = (Tm⁴ − (1 − w)·To⁴) / w.
* other = Ta, w = ε — поправка на излучательную способность; other = фон, w = доля пятна — на неполное пятно.
* Всё в единицах слова, 0,02 K; 32767⁴ ≈ 1,15·10¹⁸ — помещается в 64 бита */
uint16_t mlx_unmix(uint16_t meas, uint16_t other, uint32_t w1000)
{
uint64_t m4, o4, t, x, q, r;
if (w1000 < 1u || w1000 > 1000u)
return 0u;
m4 = pow4(meas & 0x7FFFu);
o4 = pow4(other & 0x7FFFu);
t = o4 / 1000u * (1000u - w1000) + o4 % 1000u * (1000u - w1000) / 1000u;
if (m4 <= t)
return 0u;
x = m4 - t;
if (x / w1000 > UINT64_MAX / 1000u)
return 0x7FFFu;
q = x / w1000 * 1000u + x % w1000 * 1000u / w1000;
r = isqrt64(isqrt64(q));
if (r >= 0x7FFFu)
return 0x7FFFu;
if (pow4(r + 1u) - q < q - pow4(r))
r++; /* к ближайшему */
return (uint16_t)r;
}
Фон берут не из головы, а меряют тем же датчиком, повернув его на то, что вокруг предмета. Строки «с поправкой» выше так и получены: фон 25,01 °C, поправка возвращает 60 °C с точностью до шума. Чем меньше доля пятна, тем сильнее шум растёт: при 500 мм и 90° — 58,85 °C. А если за чашкой батарея 45 °C, а не стена? Показание — 47,78 °C, фон рядом — 45,98 °C. Поправка с измеренным фоном даёт 59,83 °C, а с температурой корпуса вместо фона — 142,75 °C. Фон нельзя заменить комнатной температурой.
104б.5 Излучательная способность: материалы и запись в EEPROM
У кожи, воды, краски и дерева ε высокая, у блестящего металла — крошечная: он почти зеркало и показывает то, что в нём отражается. В модели ε материалов — типичные значения, допущение модели, а не таблица из документа. Предмет 60 °C заполняет пятно, вокруг 25 °C, среднее из 16 показаний:
| Материал | ε в модели | При ε = 1,00 | ε записан в EEPROM | Поправка в программе |
|---|---|---|---|---|
| кожа | 0,98 | 59,40 | 60,00 | 60,01 |
| вода | 0,96 | 58,80 | 60,00 | 60,01 |
| матовая чёрная краска | 0,95 | 58,50 | 60,00 | 60,01 |
| дерево | 0,90 | 56,98 | 60,00 | 60,01 |
| окисленная сталь | 0,80 | 53,86 | 60,00 | 60,01 |
| нержавеющая сталь | 0,15 | 31,06 | 60,00 | 60,09 |
| полированный алюминий | 0,05 | 27,06 | 60,00 | 60,27 |
Для кожи и воды ε = 1 почти не врёт — около градуса при 60 °C. Окисленная
сталь уже на шесть градусов ниже, полированный алюминий показывает
почти комнату. Правильный ε в EEPROM возвращает 60 °C у всех, так же как
поправка mlx_unmix в программе; у алюминия она уже заметно шумит,
60,27 °C: делить на 0,05 — умножать шум в двадцать раз. В модели шум
добавлен к готовому показанию, поэтому при записанном ε он не растёт; у
датчика при малом ε растёт и он (не проверено). Но у металла с малым ε в модели
поправка работает, только пока отражается комната: если напротив
алюминиевой детали (ε = 0,05) горячая лампа 80 °C, показание с
правильным ε — 357,02 °C. Такие поверхности перед измерением красят
матовой краской или клеят на них чёрную ленту с известным ε.
Запись ε в EEPROM. Код — round(65535 × ε): 1,00 → 0xFFFF, 0,98 → 0xFAE0, 0,95 → 0xF332, 0,90 → 0xE666, 0,05 → 0x0CCD (по документации производителя, не проверено). Ячейку по документации производителя сначала стирают — пишут 0, — выжидают, потом пишут новое значение и снова выжидают. Ресурс записи EEPROM ограничен, поэтому драйвер сначала читает ячейку и при том же значении ничего не пишет:
/* запись ячейки EEPROM: то же значение — не трогать; иначе стереть (записать 0), выждать, записать, выждать,
* прочитать обратно */
int mlx_ee_write(mlx_t *d, uint8_t cmd, uint16_t w)
{
uint16_t now;
int rc;
if ((rc = mlx_read_word(d, cmd, &now)) != MLX_OK)
return rc;
if (now == w)
return MLX_OK; /* ресурс записи ограничен: лишних записей нет */
if ((rc = mlx_write_word(d, cmd, 0)) != MLX_OK)
return rc;
mlx_port_delay_us(MLX_EE_WAIT_US);
if ((rc = mlx_write_word(d, cmd, w)) != MLX_OK)
return rc;
mlx_port_delay_us(MLX_EE_WAIT_US);
if ((rc = mlx_read_word(d, cmd, &now)) != MLX_OK)
return rc;
return now == w ? MLX_OK : MLX_E_VERIFY;
}
В модели смена ε с 1,00 на 0,95 — 4 обмена: чтение 24, запись 24 00 00 28, запись 24 32 F3 2C, чтение 24; 2080 мкс шины
и 22 080 мкс вместе с паузами по 10 мс. Повторный вызов с тем же ε —
1 обмен, 570 мкс, без записи; после 100 включений, на каждом
из которых прошивка заново задаёт 0,95, записей в ячейку по-прежнему
2. Прошивка поэтому пишет ε только при изменении. В модели новый ε
действует сразу; когда он вступает в силу у микросхемы — по документации
производителя, не проверено. Адрес меняется так же — младший байт ячейки
0x0E, старший не наш, его сохраняют: в модели ячейка стала 0xBE5B,
и старый адрес отвечает до выключения питания, а после — только новый.
104б.6 Тепловой удар и усреднение
Термобатарея мерит разницу между мембраной и холодными спаями на корпусе,
а вычислитель подставляет вместо спаев Ta — температуру корпуса в
другом его месте. Пока корпус прогрет ровно, это одно и то же. Если
датчик внесли с холода, рядом греется стабилизатор или на корпус светит
лампа, корпус прогрет неравномерно, и показание уходит — это и есть
тепловой удар. Подробно это поведение здесь не моделируется. В модели
только перекос: холодные спаи теплее датчика Ta, предмет 60 °C:
| Перекос | 0 K | 0,5 K | 1 K | 2 K |
|---|---|---|---|---|
| показание, °C | 60,00 | 59,64 | 59,28 | 58,54 |
Перекос в 1 K даёт около 0,72 °C ошибки. Средства — механические: датчик подальше от тёплых деталей, после переноса дать прогреться, корпус не трогать рукой.
Усреднение против перекоса не помогает — он одинаков во всех показаниях, — а против шума помогает. В модели шум одного показания 0,1 K; показания разделены паузой 100 мс, потому что у микросхемы результат обновляется не мгновенно (период — по документации производителя, не проверено). 200 средних:
/* среднее n показаний с паузой gap_us между ними, округление к ближайшему */
int mlx_avg(mlx_t *d, uint8_t cmd, uint32_t n, uint32_t gap_us, int32_t *c100)
{
int32_t s = 0, t;
int rc;
if (!n)
return MLX_E_BUS;
for (uint32_t i = 0; i < n; i++) {
if (i)
mlx_port_delay_us(gap_us);
if ((rc = mlx_read_temp(d, cmd, &t)) != MLX_OK)
return rc;
s += t;
}
*c100 = s >= 0 ? (s + (int32_t)(n / 2u)) / (int32_t)n : -((-s + (int32_t)(n / 2u)) / (int32_t)n);
return MLX_OK;
}
| Показаний в среднем | 1 | 4 | 16 |
|---|---|---|---|
| разброс среднего, K | 0,099 | 0,054 | 0,028 |
| время, мс | 0,6 | 302,3 | 1509,1 |
Вчетверо больше показаний — почти вдвое меньше разброс, а время растёт вчетверо.
104б.7 Грабли
Читается мусор или NACK, хотя адрес верный: SCL держали слишком долго. Обмен SMBus нельзя прерывать надолго: SCL в нуле дольше 35 мс (RM0008, с. 767), и ведомый сбрасывает обмен. В модели пауза 20 мс посреди чтения — код 0, 40 мс — −1 (NACK). Прерывание с долгим обработчиком посреди обмена — та же пауза.
PEC никогда не сходится: нет повторного старта. Команду записали и
поставили STOP, потом отдельным обменом прочитали три байта. В модели
команда после STOP забыта, и пришло FF FF FF — PEC 0xFF не
совпал с ожидаемым 0x22 (как поступает микросхема, не проверено).
Чтение — только с повторным стартом (17.2).
PEC не сходится всегда при правильном обмене. PEC считают по всем байтам: адрес с записью, команда, адрес с чтением, данные. Забыли адреса или поставили адрес без бита чтения — сумма другая. Это видно сразу: ни одно чтение не проходит.
После записи ε показания странные. Записали без стирания. В модели запись без нуля даёт «старое ИЛИ новое» (так ведёт себя модель; что получится у микросхемы — по документации производителя нужно стирать, не проверено): с 0xF332 (0,95) на 0xE666 (0,90) в ячейке 0xF776 — ε = 0,9667, и предмет 100 °C показан как 96,13 °C. Со стиранием — 100,00 °C. Без паузы вторая запись получает NACK (код −1), а ячейка остаётся стёртой, 0x0000, — ε = 0. Проверять код возврата и читать ячейку обратно.
Сквозь стекло меряется стекло. Обычное стекло в дальнем ИК непрозрачно (в модели — полностью, с ε = 0,92): предмет 60 °C за окошком из стекла комнатной температуры показан как 25,01 °C. Окно перед датчиком ставят из материала, прозрачного в его диапазоне, или меряют без окна.
Блестящее — холодное. Металл с малым ε при ε = 1 в настройке показывает почти комнату (104б.5), а с правильным ε — то, что в нём отражается. Ленту или краску с известным ε.
Показание ниже, чем на самом деле. Предмет меньше пятна (104б.4), либо датчик смотрит мимо. Проверить пятно на своём расстоянии.
Версия на 3 В от 5 В. По документации производителя версии на 3 и 5 В разные; голую микросхему на 3 В от 5 В можно сжечь (не проверено). На модуле смотрят стабилизатор и подтяжки.
104б.8 Итоги
- MLX90614 считает температуру объекта по излучению: термобатарея мерит
разницу с корпусом,
Ta— корпус; по документации производителя адрес 0x5A,Ta— 0x06,Tobj1— 0x07, ε — 0x24, адрес — 0x2E, шаг 0,02 K. - SMBus — I2C1 на 100 кГц (
CCR= 180,TRISE= 37), чтение с повторным стартом, SCL в нуле не дольше 35 мс. - PEC — CRC-8 с полиномом 0x07 по пяти байтам вместе с адресами; в модели она ловит любой испорченный бит и байт, а из 2257 кадров с двумя испорченными байтами пропустила 3.
- Градусы целыми: слово × 2 − 27315 сотых; старший бит — флаг ошибки.
- Неверный ε — ошибка, растущая с разницей до корпуса: в модели при настоящем ε 0,95 и 1,00 в настройке — −2,79 °C при 100 °C и −5,06 °C при 200 °C.
- Пятно — 2 · d · tg(FOV / 2): при 90° на 100 мм чашка 70 мм показана как 29,98 °C вместо 60; фон меряют рядом с предметом и вычитают.
- EEPROM: прочитать, при другом значении стереть (0), выждать, записать, выждать, прочитать обратно; одно и то же не перезаписывать.
Полные исходники
mlx104c.h — команды, ячейки, коды ошибок, порт и функции драйвера MLX90614.
/* mlx104c.h — раздел 104б: драйвер ИК-термометра MLX90614 по SMBus без регистров микроконтроллера. Драйвер говорит
* с микросхемой только через функции порта (mlx_port_*): в прошивке fw104c.c их делает I2C1 STM32F103, в стенде
* host104c.c — модель model104c.py. Адрес, команды RAM и EEPROM, ячейки, шаг 0,02 K и флаг ошибки в старшем бите —
* по документации производителя (Melexis); документа в проекте нет, с микросхемой не сверено. PEC — CRC-8 SMBus
* с полиномом 0x07 по всем байтам посылки, включая адреса (RM0008, с. 771; smbus_pec() в Zephyr 4.4.2). */
#ifndef MLX104C_H
#define MLX104C_H
#include <stdint.h>
/* ── команды: 000x xxxx — RAM, 001x xxxx — EEPROM ─────────────────────── */
#define MLX_RAM_TA 0x06u /* температура корпуса датчика */
#define MLX_RAM_TOBJ1 0x07u /* температура объекта, зона 1 */
#define MLX_RAM_TOBJ2 0x08u /* зона 2 — только у двухзонных версий */
#define MLX_EE 0x20u /* 0x20 | ячейка EEPROM */
#define MLX_EE_EMIS 0x24u /* ячейка 0x04: излучательная способность, 65535 × ε */
#define MLX_EE_ADDR 0x2Eu /* ячейка 0x0E: младший байт — адрес на шине */
#define MLX_ADDR 0x5Au /* семибитный: 0xB4 запись, 0xB5 чтение */
#define MLX_FLAG 0x8000u /* старший бит слова температуры: ошибка, не температура */
#define MLX_EE_WAIT_US 10000u /* пауза после записи ячейки EEPROM */
#define MLX_TRIES 3u /* повторов чтения, если PEC не сошёлся */
enum { MLX_OK = 0, MLX_E_NACK = -1, MLX_E_BUS = -2, MLX_E_PEC = -3, MLX_E_FLAG = -4, MLX_E_VERIFY = -5 };
typedef struct {
uint8_t addr;
uint32_t xfers, errors, pec_errors, retries, ee_writes;
} mlx_t;
/* ── порт: в прошивке — I2C1, в стенде — строки к модели ──────────────── */
/* ntx байт записи, затем (если nrx) повторный старт и nrx байт чтения; 0, MLX_E_NACK или MLX_E_BUS */
int mlx_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx);
void mlx_port_delay_us(uint32_t us);
/* ── драйвер ──────────────────────────────────────────────────────────── */
uint8_t mlx_crc8(uint8_t crc, const uint8_t *p, uint32_t n);
int mlx_read_word(mlx_t *d, uint8_t cmd, uint16_t *w);
int mlx_write_word(mlx_t *d, uint8_t cmd, uint16_t w);
int32_t mlx_raw_to_c100(uint16_t raw);
uint16_t mlx_c100_to_raw(int32_t c100);
int mlx_read_temp(mlx_t *d, uint8_t cmd, int32_t *c100);
int mlx_avg(mlx_t *d, uint8_t cmd, uint32_t n, uint32_t gap_us, int32_t *c100);
int mlx_ee_write(mlx_t *d, uint8_t cmd, uint16_t w);
uint16_t mlx_emis_code(uint32_t e1000);
int mlx_set_emissivity(mlx_t *d, uint32_t e1000);
int mlx_set_addr(mlx_t *d, uint8_t new7);
uint16_t mlx_unmix(uint16_t meas, uint16_t other, uint32_t w1000);
#endif
mlx104c.c — драйвер: CRC-8, чтение и запись слова с PEC, градусы, EEPROM, поправка на ε и фон.
/* mlx104c.c — раздел 104б: драйвер MLX90614 по SMBus. Один и тот же файл собирается в прошивку (armclang,
* Cortex-M3) и в стенд на компьютере (gcc): микроконтроллера он не знает, только порт mlx_port_*. */
#include "mlx104c.h"
static int xfer_done(mlx_t *d, int rc)
{
d->xfers++;
if (rc)
d->errors++;
return rc;
}
/* CRC-8 SMBus: полином x^8 + x^2 + x + 1 (0x07), старший бит первым; начальное значение — 0 */
uint8_t mlx_crc8(uint8_t crc, const uint8_t *p, uint32_t n)
{
while (n--) {
crc ^= *p++;
for (int i = 0; i < 8; i++)
crc = (crc & 0x80u) ? (uint8_t)((crc << 1) ^ 0x07u) : (uint8_t)(crc << 1);
}
return crc;
}
/* чтение слова: адрес + запись, команда, повторный старт, адрес + чтение, младший байт, старший, PEC;
* PEC считается по всем пяти байтам до него — адреса тоже входят */
int mlx_read_word(mlx_t *d, uint8_t cmd, uint16_t *w)
{
uint8_t f[5] = {(uint8_t)(d->addr << 1), cmd, (uint8_t)(d->addr << 1 | 1u), 0, 0}, rx[3];
int rc;
for (uint32_t i = 0; i < MLX_TRIES; i++) {
if (i)
d->retries++;
if ((rc = xfer_done(d, mlx_port_xfer(d->addr, &cmd, 1, rx, 3))) != MLX_OK)
return rc;
f[3] = rx[0];
f[4] = rx[1];
if (mlx_crc8(0, f, 5) == rx[2]) {
*w = (uint16_t)(rx[0] | rx[1] << 8); /* младший байт первым */
return MLX_OK;
}
d->pec_errors++; /* помеха на линии: прочитать ещё раз */
}
return MLX_E_PEC;
}
/* запись слова: адрес + запись, команда, младший байт, старший, PEC по четырём байтам до него */
int mlx_write_word(mlx_t *d, uint8_t cmd, uint16_t w)
{
uint8_t f[5] = {(uint8_t)(d->addr << 1), cmd, (uint8_t)w, (uint8_t)(w >> 8), 0};
f[4] = mlx_crc8(0, f, 4);
if ((cmd & 0xE0u) == MLX_EE)
d->ee_writes++;
return xfer_done(d, mlx_port_xfer(d->addr, f + 1, 4, 0, 0));
}
/* шаг 0,02 K: сотые градуса = raw × 2 − 27315 */
int32_t mlx_raw_to_c100(uint16_t raw)
{
return (int32_t)raw * 2 - 27315;
}
uint16_t mlx_c100_to_raw(int32_t c100)
{
int32_t k2 = c100 + 27315; /* сотые кельвина */
return k2 <= 0 ? 0u : (uint16_t)((k2 + 1) / 2);
}
/* температура в сотых °C; старший бит слова — флаг ошибки, а не температура */
int mlx_read_temp(mlx_t *d, uint8_t cmd, int32_t *c100)
{
uint16_t w;
int rc = mlx_read_word(d, cmd, &w);
if (rc != MLX_OK)
return rc;
if (w & MLX_FLAG)
return MLX_E_FLAG;
*c100 = mlx_raw_to_c100(w);
return MLX_OK;
}
/* среднее n показаний с паузой gap_us между ними, округление к ближайшему */
int mlx_avg(mlx_t *d, uint8_t cmd, uint32_t n, uint32_t gap_us, int32_t *c100)
{
int32_t s = 0, t;
int rc;
if (!n)
return MLX_E_BUS;
for (uint32_t i = 0; i < n; i++) {
if (i)
mlx_port_delay_us(gap_us);
if ((rc = mlx_read_temp(d, cmd, &t)) != MLX_OK)
return rc;
s += t;
}
*c100 = s >= 0 ? (s + (int32_t)(n / 2u)) / (int32_t)n : -((-s + (int32_t)(n / 2u)) / (int32_t)n);
return MLX_OK;
}
/* запись ячейки EEPROM: то же значение — не трогать; иначе стереть (записать 0), выждать, записать, выждать,
* прочитать обратно */
int mlx_ee_write(mlx_t *d, uint8_t cmd, uint16_t w)
{
uint16_t now;
int rc;
if ((rc = mlx_read_word(d, cmd, &now)) != MLX_OK)
return rc;
if (now == w)
return MLX_OK; /* ресурс записи ограничен: лишних записей нет */
if ((rc = mlx_write_word(d, cmd, 0)) != MLX_OK)
return rc;
mlx_port_delay_us(MLX_EE_WAIT_US);
if ((rc = mlx_write_word(d, cmd, w)) != MLX_OK)
return rc;
mlx_port_delay_us(MLX_EE_WAIT_US);
if ((rc = mlx_read_word(d, cmd, &now)) != MLX_OK)
return rc;
return now == w ? MLX_OK : MLX_E_VERIFY;
}
/* код излучательной способности: round(65535 × ε), ε — в тысячных, 1…1000 */
uint16_t mlx_emis_code(uint32_t e1000)
{
if (e1000 < 1u)
e1000 = 1u;
if (e1000 > 1000u)
e1000 = 1000u;
return (uint16_t)((65535u * e1000 + 500u) / 1000u);
}
int mlx_set_emissivity(mlx_t *d, uint32_t e1000)
{
return mlx_ee_write(d, MLX_EE_EMIS, mlx_emis_code(e1000));
}
/* новый адрес: младший байт ячейки 0x0E, старший — не наш, его сохранить; заработает после выключения питания */
int mlx_set_addr(mlx_t *d, uint8_t new7)
{
uint16_t now;
int rc;
if (new7 < 1u || new7 > 0x7Fu)
return MLX_E_BUS;
if ((rc = mlx_read_word(d, MLX_EE_ADDR, &now)) != MLX_OK)
return rc;
return mlx_ee_write(d, MLX_EE_ADDR, (uint16_t)((now & 0xFF00u) | new7));
}
static uint64_t isqrt64(uint64_t v)
{
uint64_t r = 0, b = 1ull << 62;
while (b > v)
b >>= 2;
while (b) {
if (v >= r + b) {
v -= r + b;
r = (r >> 1) + b;
} else {
r >>= 1;
}
b >>= 2;
}
return r;
}
static uint64_t pow4(uint64_t x)
{
return x * x * x * x;
}
/* показание — смесь двух излучений с долями w и 1 − w (в тысячных): T⁴ = (Tm⁴ − (1 − w)·To⁴) / w.
* other = Ta, w = ε — поправка на излучательную способность; other = фон, w = доля пятна — на неполное пятно.
* Всё в единицах слова, 0,02 K; 32767⁴ ≈ 1,15·10¹⁸ — помещается в 64 бита */
uint16_t mlx_unmix(uint16_t meas, uint16_t other, uint32_t w1000)
{
uint64_t m4, o4, t, x, q, r;
if (w1000 < 1u || w1000 > 1000u)
return 0u;
m4 = pow4(meas & 0x7FFFu);
o4 = pow4(other & 0x7FFFu);
t = o4 / 1000u * (1000u - w1000) + o4 % 1000u * (1000u - w1000) / 1000u;
if (m4 <= t)
return 0u;
x = m4 - t;
if (x / w1000 > UINT64_MAX / 1000u)
return 0x7FFFu;
q = x / w1000 * 1000u + x % w1000 * 1000u / w1000;
r = isqrt64(isqrt64(q));
if (r >= 0x7FFFu)
return 0x7FFFu;
if (pow4(r + 1u) - q < q - pow4(r))
r++; /* к ближайшему */
return (uint16_t)r;
}
fw104c.c — прошивка F103: I2C1 на 100 кГц, приём трёх байт, среднее, ε в EEPROM только при изменении.
/* fw104c.c — раздел 104б: MLX90614 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
* I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 100 кГц — верх SMBus). Цикл: четыре показания объекта с паузой
* 100 мс — среднее, температура корпуса, при заданной доле пятна — поправка на фон. Настройки — через
* отладчик: emis1000 — излучательная способность в тысячных (пишется в EEPROM, только если изменилась),
* fill1000 и bg_c100 — доля пятна, занятая объектом, и температура фона рядом с ним. Тактирование до 72 МГц —
* как в разделе 6. */
#include "stm32f10x.h"
#include "mlx104c.h"
#define CPU_HZ 72000000u
#define APB1_MHZ 36u
#define I2C_TIMEOUT_US 2000u /* байт на 100 кГц — 90 мкс; весь обмен много короче 35 мс */
#define AVG_N 4u
#define AVG_GAP_US 100000u
static mlx_t dev = {.addr = MLX_ADDR};
volatile uint32_t emis1000 = 1000u, fill1000 = 1000u, bus_errors, flag_errors;
volatile int32_t t_obj_c100, t_amb_c100, bg_c100 = 2500, t_fixed_c100;
static uint32_t emis_now; /* 0: при запуске сверить с ячейкой — запись, только если другое */
static int expired(uint32_t t0, uint32_t us)
{
return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}
void mlx_port_delay_us(uint32_t us)
{
uint32_t t0 = DWT->CYCCNT;
while (!expired(t0, us)) {
}
}
static void gpio_i2c1_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
/* PB6, PB7 — альтернативная функция, открытый сток, 2 МГц (1110): фронтам SMBus больше не нужно */
GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xEE000000u;
I2C1->CR1 = 0; /* настраивать при PE = 0 */
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = APB1_MHZ * 1000000u / (2u * 100000u); /* стандартный режим: 36 МГц / (2 × 180) = 100 кГц */
I2C1->TRISE = APB1_MHZ + 1u; /* 1000 нс × 36 МГц = 36 → 37 */
I2C1->CR1 = I2C_CR1_PE;
}
static int sr1_wait(uint16_t mask)
{
uint32_t t0 = DWT->CYCCNT;
while (!(I2C1->SR1 & mask)) {
if (I2C1->SR1 & I2C_SR1_AF)
return MLX_E_NACK; /* адрес или байт не подтверждён */
if (expired(t0, I2C_TIMEOUT_US))
return MLX_E_BUS;
}
return MLX_OK;
}
static int fail(int rc)
{
I2C1->CR1 |= I2C_CR1_STOP;
I2C1->SR1 = (uint16_t)~I2C_SR1_AF; /* AF снимается записью нуля */
I2C1->CR1 |= I2C_CR1_ACK;
return rc;
}
/* приём трёх байт (младший, старший, PEC) — способ 2, RM0008, с. 762–763 */
static int rx3(uint8_t *rx)
{
int rc;
(void)I2C1->SR2; /* ADDR снят, ACK = 1 */
if ((rc = sr1_wait(I2C_SR1_BTF)) != MLX_OK) /* байт 1 в DR, байт 2 в сдвиговом, SCL прижат */
return rc;
I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
rx[0] = (uint8_t)I2C1->DR; /* пошёл приём PEC с NACK */
if ((rc = sr1_wait(I2C_SR1_BTF)) != MLX_OK)
return rc;
I2C1->CR1 |= I2C_CR1_STOP;
rx[1] = (uint8_t)I2C1->DR;
if ((rc = sr1_wait(I2C_SR1_RXNE)) != MLX_OK)
return rc;
rx[2] = (uint8_t)I2C1->DR;
I2C1->CR1 |= I2C_CR1_ACK;
return MLX_OK;
}
int mlx_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx)
{
int rc;
uint32_t t0 = DWT->CYCCNT;
if ((nrx != 0u && nrx != 3u) || ntx == 0u)
return MLX_E_BUS; /* драйвер пишет команду и читает слово с PEC — 3 байта */
while (I2C1->SR2 & I2C_SR2_BUSY) { /* SDA держит ведомый — раздел 17.6 */
if (expired(t0, I2C_TIMEOUT_US))
return MLX_E_BUS;
}
I2C1->CR1 |= I2C_CR1_ACK;
I2C1->CR1 |= I2C_CR1_START;
if ((rc = sr1_wait(I2C_SR1_SB)) != MLX_OK)
return fail(rc);
I2C1->DR = (uint8_t)(addr7 << 1);
if ((rc = sr1_wait(I2C_SR1_ADDR)) != MLX_OK)
return fail(rc);
(void)I2C1->SR2;
for (uint32_t i = 0; i < ntx; i++) {
if ((rc = sr1_wait(I2C_SR1_TXE)) != MLX_OK)
return fail(rc);
I2C1->DR = tx[i];
}
if ((rc = sr1_wait(I2C_SR1_BTF)) != MLX_OK)
return fail(rc);
if (!nrx) {
I2C1->CR1 |= I2C_CR1_STOP;
return MLX_OK;
}
I2C1->CR1 |= I2C_CR1_START; /* повторный старт: без него команда потеряна */
if ((rc = sr1_wait(I2C_SR1_SB)) != MLX_OK)
return fail(rc);
I2C1->DR = (uint8_t)(addr7 << 1 | 1u);
if ((rc = sr1_wait(I2C_SR1_ADDR)) != MLX_OK)
return fail(rc);
if ((rc = rx3(rx)) != MLX_OK)
return fail(rc);
return MLX_OK;
}
static void count(int rc)
{
if (rc == MLX_E_FLAG)
flag_errors++;
else if (rc != MLX_OK)
bus_errors++;
}
int main(void)
{
int32_t t;
int rc;
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
gpio_i2c1_init();
while (mlx_read_temp(&dev, MLX_RAM_TA, &t) != MLX_OK)
mlx_port_delay_us(100000u); /* нет ответа: провод, адрес, питание, режим ШИМ */
for (;;) {
if (emis1000 != emis_now) { /* EEPROM — только при изменении */
count(rc = mlx_set_emissivity(&dev, emis1000));
if (rc == MLX_OK)
emis_now = emis1000;
}
if ((rc = mlx_read_temp(&dev, MLX_RAM_TA, &t)) == MLX_OK)
t_amb_c100 = t;
count(rc);
if ((rc = mlx_avg(&dev, MLX_RAM_TOBJ1, AVG_N, AVG_GAP_US, &t)) != MLX_OK) {
count(rc);
continue;
}
t_obj_c100 = t;
t_fixed_c100 = fill1000 >= 1000u ? t
: mlx_raw_to_c100(mlx_unmix(mlx_c100_to_raw(t), mlx_c100_to_raw(bg_c100), fill1000));
mlx_port_delay_us(AVG_GAP_US);
}
}
Дальше: часть XII «Сети и DevOps для инженера», раздел 105 «Сеть снизу вверх: Ethernet, MAC, IP, подсети, ARP» — что происходит с кадром от разъёма до IP-адреса.