Embedded HandbookПриложения
Все разделы › XI. Датчики: подключение, схемы, драйверы
STM32F103 · уровень средний · 32 мин чтения
Открыть в интерактивном справочнике

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, ШИМ в модели нет.

Вверху — подключение MLX90614 к STM32F103: PB6 — SCL, PB7 — SDA на I2C1 в режиме SMBus на 100 кГц, адрес 0x5A, подтяжки к 3,3 В; датчик принимает ИК от объекта с излучательной способностью ε. В середине — кадр чтения Tobj1: старт, B4 — адрес с записью, 07 — команда, повторный старт, B5 — адрес с чтением, 24 и 3F — младший и старший байты, 41 — PEC, стоп; слово 0x3F24 — 50,13 °C, старший бит — флаг ошибки. Внизу — поле зрения 90°, 35° и 10° и пятно на 100 мм: 200, 63 и 17 мм; чашка 70 мм при 90° занимает долю 0,123 пятна, при 35° и 10° закрывает всё пятно


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 Итоги

Полные исходники

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-адреса.

Открыть в интерактивном справочнике