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

103б. Датчик цвета TCS34725 по I2C: каналы RGBC, время интегрирования и усиление, баланс белого, цветовая температура и освещённость

В разделе 103 BH1750 мерил освещённость одним числом, в 103а MAX30102 светил в палец и мерил, сколько вернулось. TCS34725 видит цвет: у него четыре канала — за красным, зелёным и синим фильтрами и без фильтра (Clear). Из этих четырёх чисел получают цвет предмета, освещённость и цветовую температуру света. Здесь — что внутри и как с ним говорить по I2C, как выбрать время интегрирования и усиление, как сделать баланс белого и узнать цвет карточки, как по формулам аппнотки DN40 посчитать люксы и кельвины и почему лампа накаливания и солнце сбивают датчик, как работает прерывание по порогу.

Опоры: RM0008 (DocID13902 Rev 17): таблица режимов выводов, с. 160; AFIO_EXTICR3, с. 191; EXTI_FTSR, с. 211; приём больше двух байт ведущим, способ 2, с. 762–763; приём одного байта, с. 764; I2C_CCR, с. 783–784; I2C_TRISE, с. 784. DocID13587 Rev 17 (STM32F103x8/xB): PB6 — I2C1_SCL, PB7 — I2C1_SDA, оба FT, с. 32; PB8 и PB9 — FT, с. 33. Документа на TCS34725 и аппнотки DN40 в проекте нет: адрес, командный байт, регистры, биты, формула времени интегрирования, коды усиления, persistence и формулы DN40 с коэффициентами взяты из документации производителя и заложены в модель — по документации производителя, не проверено. Номера в командном байте совпадают с прямыми номерами регистров родственного TCS3400 в драйвере Zephyr 4.4.2 на диске, но это другой датчик и не документ. Все числа — прогон драйвера _verify/rgb103c/tcs103c.c и обработки col103c.c на компьютере (gcc 12.2.0) со своей моделью TCS34725 как ведомого I2C, источников света и цветных карточек на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3. Кривые чувствительности, спектры и карточки в модели упрощённые: числа ниже говорят о модели, а не о датчике.

Как это называется в документации

Оригинал Перевод
RGBC (red, green, blue, clear) четыре канала: красный, зелёный, синий и без цветного фильтра
IR blocking filter ИК-фильтр поверх фотодиодов
command register, CMD bit командный байт, его бит 7
auto-increment / repeated byte номер регистра растёт / не растёт
integration time (ATIME) время интегрирования: сколько копится свет
analog gain (AGAIN) усиление
AVALID измерение закончено, данные свежие
interrupt threshold (AILT, AIHT) нижний и верхний пороги Clear
persistence (APERS) сколько измерений подряд за порогом до прерывания
white balance баланс белого: белая карточка — одинаково во всех каналах
hue, saturation (HSV) оттенок и насыщенность
correlated color temperature (CCT) цветовая температура, К
counts per lux (CPL) отсчётов на люкс
device factor (DF), glass attenuation (GA) множитель устройства и ослабление стекла

103б.1 Датчик: четыре канала, ИК-фильтр и командный байт

По документации производителя в TCS34725 — матрица фотодиодов: часть под красным, зелёным и синим фильтрами, часть без цветного фильтра (Clear). Поверх всего — фильтр, который режет ближний ИК. На каждый канал свой интегрирующий АЦП: свет копится заданное время, результат — 16 бит. Модули добавляют стабилизатор, подтяжки и белый светодиод подсветки — им освещают предмет, цвет которого хотят узнать.

Адрес на шине — 0x29 (байт в линии 0x52 для записи и 0x53 для чтения, 17.2). Первый байт каждой записи — командный. Бит 7 (0x80) обязателен, биты 6:5 — тип: 00 — номер регистра не растёт, 01 — растёт после каждого байта, 11 — специальная функция; 0xE6 снимает прерывание по Clear. Регистры (по документации производителя):

Регистр Номер В командном байте Что в нём
ENABLE 0x00 0x80 AIEN [4], WEN [3], AEN [1], PON [0]
ATIME 0x01 0x81 время интегрирования
WTIME 0x03 0x83 пауза между измерениями
AILTL…AIHTH 0x04…0x07 0x84…0x87 пороги Clear, младший байт первым
PERS 0x0C 0x8C APERS [3:0]
CONFIG 0x0D 0x8D WLONG [1]: пауза × 12
CONTROL 0x0F 0x8F AGAIN [1:0]: 1×, 4×, 16×, 60×
ID 0x12 0x92 0x44 — TCS34721 и TCS34725, 0x4D — TCS34723 и TCS34727
STATUS 0x13 0x93 AINT [4], AVALID [0]
CDATAL…BDATAH 0x14…0x1B 0x94…0x9B C, R, G, B по два байта, младший первым

Сверка с Zephyr 4.4.2. Готового драйвера TCS34725 там нет, есть родственный TCS3400. У него номера регистров пишутся прямо, без командного бита, и совпадают с колонкой «В командном байте»: ENABLE 0x80 с PON, AEN и AIEN в битах 0, 1 и 4, ATIME 0x81, PERS 0x8C, CONTROL 0x8F, ID 0x92, STATUS 0x93 с AVALID в бите 0, CDATAL…BDATAH 0x94…0x9B в порядке C, R, G, B младшим байтом вперёд. Время там задаётся так же: записывают 256 минус число тактов. Вывод INT в привязке — открытый сток, активный ноль. Снятие прерывания у TCS3400 своё (регистр 0xE7), а ID — 0x90 или 0x93.

Всё, что сделано из этого в модели: в командном байте без бита 7 модель байт не понимает, обмен ничего не меняет, и чтение идёт с прежнего номера — это допущение модели, документ здесь не сверялся.


103б.2 Подключение к F103 и инициализация

Вывод модуля STM32F103 Назначение
VIN 3,3 В питание модуля
GND GND земля
SCL PB6 такт I2C1
SDA PB7 данные I2C1
INT PB8 прерывание по порогу, активный ноль, открытый сток
LED PB9 светодиод подсветки: единица — горит (так у распространённых модулей)

I2C1 — как в 15б.2: 400 кГц, CCR = F/S | 30, TRISE = 11. PB8 — вход с подтяжкой, EXTI8 по спаду: в AFIO_EXTICR3 (с. 191) код 0001 — порт B, EXTI_FTSR (с. 211) — спад, как в 103а.3. PB9 — двухтактный выход на 2 МГц (CNF = 00, MODE = 10; RM0008, с. 160). PB6…PB9 — FT (с. 32–33): у модулей, которые питаются от 5 В, подтяжки бывают к 5 В — смотрите схему своего модуля (не проверено).

Инициализация — проверить ID, включить генератор (PON), выждать 2,4 мс, настроить время, усиление, persistence и пороги, снять прерывание и только потом включить измерения (AEN):

/* проверить ID, включить генератор, выждать 2,4 мс, настроить, запустить измерения */
int tcs_init(tcs_t *d, const tcs_cfg_t *c)
{
    int rc;
    d->xfers = d->errors = 0;
    if ((rc = tcs_get(d, TCS_ID, &d->id)) != TCS_OK)
        return rc;
    if (d->id != TCS_ID_25 && d->id != TCS_ID_27)
        return TCS_E_ID;                                /* по адресу 0x29 не TCS3472x */
    if ((rc = tcs_reg(d, TCS_ENABLE, TCS_EN_PON)) != TCS_OK)
        return rc;
    tcs_port_delay_us(TCS_PON_US);
    if ((rc = tcs_reg(d, TCS_ATIME, c->atime)) != TCS_OK || (rc = tcs_reg(d, TCS_CONTROL, c->again & 3u)) != TCS_OK
        || (rc = tcs_reg(d, TCS_PERS, c->pers & 0x0Fu)) != TCS_OK || (rc = tcs_thresholds(d, c->lo, c->hi)) != TCS_OK
        || (rc = tcs_clear_int(d)) != TCS_OK)
        return rc;
    d->atime = c->atime;
    d->again = c->again & 3u;
    d->enable = (uint8_t)(TCS_EN_PON | TCS_EN_AEN | (c->aien ? TCS_EN_AIEN : 0u));
    return tcs_reg(d, TCS_ENABLE, d->enable);
}

В модели tcs_init — 8 обменов: чтение ID (92), 80 01 (PON), 81 C0, 8F 02, 8C 00, пороги одним обменом A4 00 00 FF FF, E6 и 80 03 (PON + AEN). На шине 650 мкс, вместе с паузой после PON — 3050 мкс. ID 0x4D тоже принят, чужой (0x11) — отказ TCS_E_ID, −3. После включения в модели ATIME = 0xFF, CONTROL = 0.

Данные читаются одним обменом с STATUS: командный байт 0xB3 (0x80 | 0x20 | 0x13), дальше 9 байт — статус и C, R, G, B. Один номер регистра на всё, и младший байт каждого канала первым:

/* STATUS и восемь байт C, R, G, B одним чтением: командный байт 0xB3, номер растёт */
int tcs_read(tcs_t *d, tcs_rgbc_t *v)
{
    uint8_t c = (uint8_t)(TCS_CMD | TCS_AUTO | TCS_STATUS), b[9];
    int rc = xfer_done(d, tcs_port_xfer(d->addr, &c, 1, b, 9));
    if (rc != TCS_OK)
        return rc;
    d->status = b[0];
    v->c = (uint16_t)(b[1] | b[2] << 8);                /* младший байт первым */
    v->r = (uint16_t)(b[3] | b[4] << 8);
    v->g = (uint16_t)(b[5] | b[6] << 8);
    v->b = (uint16_t)(b[7] | b[8] << 8);
    return TCS_OK;
}

Это 111 тактов SCL, 277,5 мкс на 400 кГц. Измерение «по запросу» — tcs_measure: снять и снова поставить AEN (новый запуск снимает AVALID), выждать время интегрирования и 2,4 мс подготовки, прочитать. В модели при ATIME 0xC0 — 3 обмена, 422,5 мкс шины и 156,4 мс всего.

Вверху — подключение модуля TCS34725 к STM32F103: PB6 — SCL, PB7 — SDA на I2C1 на 400 кГц, адрес 0x29, INT модуля — на PB8, EXTI8 по спаду, светодиод подсветки — на PB9; светодиод освещает карточку, отражённый свет попадает на фотодиоды; командный байт с битом 0x80, 0xA0 для чтения подряд, 0xE6 — снять прерывание; время интегрирования 2,4 мс × (256 − ATIME), счёт до 1024 на такт и не больше 65535. Внизу — упрощённые кривые чувствительности каналов C, R, G и B в модели от 400 до 1100 нм и спектр лампы 2856 К: за ИК-фильтром на 680 нм остаётся 4 %, и в ИК цветные фильтры прозрачны, поэтому ИК добавляется во все каналы почти поровну


103б.3 Время интегрирования, усиление и автоподбор

Время интегрирования — 2,4 мс × (256 − ATIME). Счёт растёт на 1024 за такт, но регистр 16-битный, поэтому верх шкалы — 1024 × (256 − ATIME), но не больше 65535. В модели канал C под очень ярким светом при 1×:

ATIME Тактов Время Верх шкалы в модели Измерение Предмет (2 измерения)
0xFF 1 2,4 мс 1024 5,2 мс 10,4 мс
0xF6 10 24 мс 10 240 26,8 мс 53,6 мс
0xEB 21 50,4 мс 21 504 53,2 мс 106,4 мс
0xE7 25 60 мс 25 600 62,8 мс 125,6 мс
0xD5 43 103,2 мс 44 032 106 мс 212 мс
0xC0 64 153,6 мс 65 535 156,4 мс 312,8 мс
0x00 256 614,4 мс 65 535 617,2 мс 1234,4 мс

С 64 тактов шкала больше не растёт: дольше копить — только медленнее (103б.7). Насыщение драйвер ловит по верху шкалы:

/* 1024 на такт, но не больше 16 бит */
uint32_t tcs_max_count(uint8_t atime)
{
    uint32_t m = 1024u * tcs_cycles(atime);
    return m > 65535u ? 65535u : m;
}

Усиление — 1×, 4×, 16×, 60× (по документации производителя; настоящие множители у экземпляров разные, не проверено). В модели свет 100 лк, ATIME 0xC0, 20 измерений:

Код Усиление Clear, среднее Отношение к 1× Разброс
0 1× 119,9 1,00 1,37 %
1 4× 471 3,93 1,23 %
2 16× 1876,3 15,65 1,20 %
3 60× 7028,6 58,62 1,20 %

Отношения не ровно 4, 16 и 60 из-за темнового отсчёта: в модели у Clear он 3 единицы при любом усилении. Разброс от усиления почти не зависит: шум в модели — от числа пойманных фотонов, усиление его тоже умножает.

Мерцание. Лампы от сети мигают с частотой 100 Гц. Короткое время интегрирования ловит то вершину, то провал. В модели светодиодная лампа 300 лк мерцает на 30 %, 60×, 40 измерений в случайные моменты:

ATIME Время Периодов 100 Гц Разброс Clear Без мерцания
0xFF 2,4 мс 0,24 67,44 % 25,15 %
0xF6 24 мс 2,4 15,32 % 8,47 %
0xEB 50,4 мс 5,04 6,09 % 5,85 %
0xE7 60 мс 6 5,36 % 5,36 %
0xD5 103,2 мс 10,32 5,32 % 4,09 %

При 60 мс — ровно 6 периодов, и мерцание вычитается целиком: разброс тот же, что без него. При 50 Гц в сети берут время, кратное 10 мс: 2,4 мс × 25 = 60 мс.

Автоподбор. Семь ступеней — от 1 такта при 1× до 256 тактов при 60× (чувствительность 1, 10, 64, 256, 1024, 3840, 15360). Упёрлись в шкалу — ступенью ниже; иначе — самая чувствительная ступень, где Clear ожидается ниже 70 % шкалы:

/* автоподбор: упёрлись в шкалу — ступенью ниже; иначе самая чувствительная ступень, где Clear ожидается
 * ниже 70 % шкалы; совпала с текущей — готово */
int col_auto(tcs_t *d, uint32_t *step, tcs_rgbc_t *v, uint32_t *tries)
{
    uint32_t k = *step < COL_STEPS ? *step : 0u;
    int rc;
    for (*tries = 1;; (*tries)++) {
        if ((rc = tcs_set(d, col_steps[k].atime, col_steps[k].again)) != TCS_OK || (rc = tcs_measure(d, v)) != TCS_OK)
            return rc;
        uint32_t j = k;
        if (col_saturated(v, col_steps[k].atime)) {
            if (k == 0u)
                break;                                  /* ярче шкалы даже на самой грубой ступени */
            j = k - 1u;
        } else {
            for (j = COL_STEPS - 1u; j > 0u; j--) {
                uint64_t want = (uint64_t)v->c * sens(j) / sens(k);
                if (want * 100u < (uint64_t)tcs_max_count(col_steps[j].atime) * COL_FILL_PCT)
                    break;
            }
        }
        if (j == k || *tries >= COL_AUTO_TRIES)
            break;
        k = j;
    }
    *step = k;
    return TCS_OK;
}

В модели, начиная со ступени 2 (64 такта, 1×; ступени считаются с нуля):

Свет Ступень Попыток ATIME, усиление Clear Заполнение Время Люксы по DN40
лампа, 1 лк 6 2 0x00, 60× 294 0,4 % 774,2 мс 0,7
лампа, 10 лк 6 2 0x00, 60× 2897 4,4 % 774,2 мс 7,9
лампа, 300 лк 5 2 0xC0, 60× 21 676 33,1 % 313,4 мс 239,2
лампа, 3000 лк 3 2 0xC0, 4× 14 443 22 % 313,4 мс 2395,8
солнце, 30 000 лк 0 2 0xFF, 1× 733 71,6 % 162,2 мс 25 666,4
солнце, 100 000 лк 0 3 0xFF, 1× 1024 100 % 189,3 мс насыщение

Лампа здесь — светодиодная, солнце — чёрное тело 5800 К. При 100 000 лк не хватает и самой грубой ступени: 1024 за 2,4 мс. В модели канал Clear насыщается при 1× и 2,4 мс где-то между 30 000 и 100 000 лк; на улице датчик ставят за ослабляющее стекло — его и учитывает множитель GA в DN40 (103б.5). На 1 лк Clear — 294 отсчёта, и люксы по DN40 — 0,7: темновой отсчёт и шум здесь уже заметны.


103б.4 Баланс белого и цвет предмета

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

/* отсчёт предмета: со светодиодом минус без него — внешний свет вычитается */
int col_object(tcs_t *d, col_v_t *o, int *sat)
{
    tcs_rgbc_t on, off;
    int rc;
    tcs_port_led(1);
    rc = tcs_measure(d, &on);
    tcs_port_led(0);
    if (rc != TCS_OK || (rc = tcs_measure(d, &off)) != TCS_OK)
        return rc;
    *sat = col_saturated(&on, d->atime);
    o->c = (int32_t)on.c - off.c;
    o->r = (int32_t)on.r - off.r;
    o->g = (int32_t)on.g - off.g;
    o->b = (int32_t)on.b - off.b;
    return TCS_OK;
}

Баланс белого. Каналы разные по чувствительности, и белый свет светодиода не плоский: сырой отсчёт белой карточки в модели — C 14 938, R 4337, G 5510, B 3619. Поэтому снимают два опорных отсчёта: чёрную карточку (тёмный — что остаётся без предмета: засветка внутри модуля и шум) и белую. Дальше каждый канал — доля пути от тёмного к белому, в тысячных:

/* баланс белого: (x − тёмный) / (белый − тёмный), белая карточка — 1000 в каждом канале */
void col_balance(const col_wb_t *wb, const col_v_t *o, int32_t out[3])
{
    const int32_t x[3] = {o->r, o->g, o->b}, dk[3] = {wb->dark.r, wb->dark.g, wb->dark.b},
                  wh[3] = {wb->white.r, wb->white.g, wb->white.b};
    for (int i = 0; i < 3; i++) {
        int32_t span = wh[i] - dk[i];
        out[i] = span > 0 ? (int32_t)((int64_t)(x[i] - dk[i]) * 1000 / span) : 0;
    }
}

Белая карточка после баланса — 996, 1001 и 991 (это повторное измерение, а не та же цифра). Ступень для предметов подбирают по белой карточке со светодиодом: в модели на 10 мм это ATIME 0xC0 и 16×, Clear 15 708 — четверть шкалы, чтобы яркие предметы не упирались.

Цветность и оттенок. Из баланса получают доли r, g, b (сумма — 1000): они не зависят от яркости. Оттенок HSV — угол на цветовом круге: 0° — красный, 120° — зелёный, 240° — синий. В модели 12 карточек на 10 мм:

Карточка R, G, B после баланса r, g, b Оттенок Насыщенность
белая 991, 999, 994 332, 334, 333 — 0,008
серая 314, 307, 314 335, 328, 335 — 0,022
чёрная −2, −3, 4 0, 0, 1000 — 1
красная 544, 58, 17 878, 93, 27 4,6° 0,968
оранжевая 790, 177, 24 797, 178, 24 11,9° 0,969
жёлтая 947, 695, 103 542, 398, 59 42° 0,891
зелёная 114, 424, 178 159, 592, 248 132,3° 0,731
голубая 147, 613, 766 96, 401, 501 194,9° 0,808
синяя 14, 84, 435 26, 157, 816 230,1° 0,967
фиолетовая 219, 52, 247 422, 100, 476 291,3° 0,789
розовая 821, 487, 437 470, 279, 250 7,8° 0,467
коричневая 227, 52, 19 761, 174, 63 9,5° 0,916

У белой, серой и чёрной оттенка нет: у первых двух насыщенность почти 0, у чёрной все каналы около нуля, и угол — шум. Красная, розовая и коричневая по оттенку почти одно (4,6°, 7,8° и 9,5°): различает их яркость и насыщенность. Поэтому узнают не по одному углу, а по ближайшему эталону — карточке, снятой заранее. Расстояние — сумма квадратов разностей:

/* ближайший эталон по сумме квадратов разностей */
uint32_t col_nearest(const int32_t (*ref)[3], uint32_t n, const int32_t x[3])
{
    uint32_t best = 0;
    int64_t bd = -1;
    for (uint32_t i = 0; i < n; i++) {
        int64_t s = 0;
        for (int k = 0; k < 3; k++) {
            int64_t e = (int64_t)x[k] - ref[i][k];
            s += e * e;
        }
        if (bd < 0 || s < bd) {
            bd = s;
            best = i;
        }
    }
    return best;
}

Точность в модели. 240 случайных карточек на 10 ± 0,5 мм, эталоны сняты при светодиодной лампе 300 лк. Два способа сравнения: по R, G, B после баланса (отражение) и по долям r, g, b (цветность):

Свет при измерении Со светодиодом минус без, отражение / цветность Только со светодиодом, отражение / цветность
лампа, 300 лк 100 / 90 % 100 / 90,4 %
темно 100 / 89,2 % 99,6 / 90 %
лампа накаливания, 500 лк 100 / 88,8 % 92,9 / 75,4 %
солнце, 20 000 лк 100 / 82,9 % (насыщение в 20) 8,3 / 54,6 % (насыщение в 20)

Вычитание отсчёта без светодиода делает узнавание почти независимым от света в комнате. Без него эталоны верны только при том свете, при котором их снимали. Цветность теряет около 10 %: белая, серая и чёрная у неё одинаковые. Под солнцем 20 отсчётов из 240 упёрлись в шкалу, и обработка поставила флаг насыщения: верить им нельзя, даже если угадано.


103б.5 Цветовая температура, освещённость и ИК

Для освещённости и цветовой температуры датчик смотрит на свет, а не на предмет, и светодиод погашен. Формулы — из аппнотки производителя DN40 (по аппнотке, не проверено):

IR = (R + G + B − C) / 2; R′ = R − IR, G′ = G − IR, B′ = B − IR

люксы = (0,136 · R′ + 1,000 · G′ − 0,444 · B′) / CPL, CPL = (2,4 мс × тактов) × усиление / (GA × DF)

CCT = 3810 · B′ / R′ + 1391

Здесь DF = 310 — множитель устройства, GA — ослабление стекла (1 без стекла). Все шесть чисел — параметры: они сняты производителем для своих кривых и своего корпуса, под стекло и модуль их снимают заново. В коде они в одной структуре, люксы считаются в десятых долях целыми:

/* DN40: IR = (R + G + B − C) / 2; X' = X − IR; люксы = (r·R' + g·G' + b·B') / CPL,
 * CPL = (2,4 мс × такты) × усиление / (GA × DF); CCT = ct_coef × B'/R' + ct_off */
void col_light(const tcs_rgbc_t *v, uint8_t atime, uint8_t again, const col_dn40_t *p, col_light_t *out)
{
    int32_t ir = p->ir_sub ? ((int32_t)v->r + v->g + v->b - v->c) / 2 : 0;
    if (ir < 0)
        ir = 0;
    int32_t r = v->r - ir, g = v->g - ir, b = v->b - ir;
    int64_t s = (int64_t)p->r * r + (int64_t)p->g * g + (int64_t)p->b * b;
    int64_t den = (int64_t)240000 * tcs_cycles(atime) * tcs_gain_x(again);
    out->ir = ir;
    out->lux10 = (int32_t)(s * p->ga * p->df / den);
    out->cct = r > 0 ? (int32_t)((int64_t)p->ct_coef * b / r + p->ct_off) : 0;
    out->sat = col_saturated(v, atime);
}

Почему ИК. Цветные фильтры в ближнем ИК прозрачны, ИК-фильтр пропускает немного — и всё, что прошло, добавляется во все каналы почти поровну. У светодиодной лампы ИК почти нет, у лампы накаливания и солнца — много. Добавка поровну размывает цвет и завышает люксы. Оценка DN40 (R + G + B − C) / 2 опирается на то, что в видимой части R + G + B ≈ C, а в ИК каждый канал видит то же, что Clear. В модели, 500 лк, без калибровки, кроме масштаба по лампе 2856 К:

Свет ИК в Clear на деле Оценка DN40 Люксы Люксы без вычета ИК CCT по DN40 CCT на деле
лампа накаливания, 2856 К 17,4 % 10,5 % 496,9 630,1 2007 2856
чёрное тело 2700 К 19,8 % 12,9 % 503,4 674,1 1918 2700
чёрное тело 4000 К 8,5 % 1,2 % 476,3 489 2711 4000
чёрное тело 5000 К 5,7 % 0 % 445,4 445,4 3179 5000
чёрное тело 6500 К 3,8 % 0 % 411,6 411,6 3798 6500
солнце (чёрное тело 5800 К) 4,5 % 0 % 424,8 424,8 3517 5800
светодиодная лампа, тёплая 0,5 % 0 % 393,2 393,2 2025 —
светодиодная лампа, холодная 0,1 % 0 % 343,8 343,8 4520 —

Под лампой накаливания без вычета ИК люксы завышены на четверть (630,1 вместо 500), с вычетом — 496,9. Но оценка грубая: в модели она находит меньше ИК, чем есть, а у холодных источников — ноль, потому что в видимой части R + G + B модели не равно C. Цветовая температура по коэффициентам аппнотки в модели занижена в 1,4–1,7 раза: кривые модели — не кривые датчика. Это и есть урок: коэффициенты DN40 — стартовая точка, не истина.

Калибровка. Цветовую температуру проще всего поправить по двум известным лампам. В модели B′/R′ при 2856 К — 0,1619, при 6500 К — 0,6318, отсюда коэффициент 7755 и сдвиг 1600:

Свет 2856 К 2700 К 4000 К 5000 К 6500 К 5800 К
CCT после калибровки 2855 2674 4288 5239 6499 5928

Для люксов три коэффициента подбирают наименьшими квадратами по нескольким лампам с люксметром. В модели по всем восьми источникам вышло 0,272 со знаком минус, 2,417 и 1,572 со знаком минус, и все восемь показаний легли в 479,1…538,8 лк при 500 настоящих (было 343,8…503,4).


103б.6 Прерывание по порогу Clear и persistence

Датчик сам сравнивает Clear с двумя порогами в конце каждого измерения. Выход за пороги нужное число раз подряд ставит AINT, и при AIEN вывод INT опускается в ноль, пока прерывание не снимут командой 0xE6. Число раз задаёт APERS (по документации производителя): 0 — каждое измерение, независимо от порогов; 1, 2, 3 — столько же; дальше по 5: код 4 — 5, код 5 — 10, …, код 15 — 60. Пороги пишутся одним обменом:

/* пороги Clear одним обменом: 0x04…0x07 подряд, номер растёт (TYPE = 01) */
int tcs_thresholds(tcs_t *d, uint16_t lo, uint16_t hi)
{
    uint8_t b[5] = {(uint8_t)(TCS_CMD | TCS_AUTO | TCS_AILTL), (uint8_t)lo, (uint8_t)(lo >> 8), (uint8_t)hi, (uint8_t)(hi >> 8)};
    return xfer_done(d, tcs_port_xfer(d->addr, b, 5, 0, 0));
}
/* снять AINT: один командный байт специальной функции, без данных */
int tcs_clear_int(tcs_t *d)
{
    uint8_t c = TCS_SF_CLEAR;
    return xfer_done(d, tcs_port_xfer(d->addr, &c, 1, 0, 0));
}

Так прошивка ждёт, пока свет не изменится, не опрашивая датчик. В модели светодиодная лампа 300 лк с мерцанием 30 %, 60×, пороги ±20 % от среднего, через 3 с свет падает до 30 лк. Ложные прерывания — за первые 3 с:

APERS Подряд 2,4 мс: ложных 2,4 мс: задержка 60 мс: ложных 60 мс: задержка
1 1 517 0,7 мс 0 65,5 мс
2 2 54 0,7 мс 0 125,5 мс
3 3 15 3,1 мс 0 185,5 мс
4 5 1 7,9 мс 0 305,5 мс
5 10 0 19,9 мс 0 605,5 мс

При 2,4 мс датчик видит каждый провал мерцания, и порог ±20 % пересекается сотни раз. Persistence их отсекает ценой задержки. При 60 мс мерцание вычитается само (103б.3), ложных нет и при APERS = 1, а задержка — одно измерение. Выбирать — сначала время, кратное 10 мс, потом APERS. В прошивке так устроен «сторож»: 60 мс, 16×, пороги ±25 % от нынешнего Clear, APERS = 4.


103б.7 Грабли

Читаются нули. Забыт командный бит: номер регистра 0x14 записан как есть. В модели такой байт не понят, и восемь байт пришли с прежнего номера — 00 00 00 00 00 00 00 00 вместо C, R, G, B (как ведёт себя микросхема, не проверено).

Все каналы одинаковые. Командный байт 0x94 без типа 01: номер регистра не растёт, и восемь раз читается CDATAL. В модели — B3 B3 B3 B3 B3 B3 B3 B3 при Clear 5811 (0x16B3). Для чтения подряд — 0xA0 | номер.

Светодиод модуля засвечивает. Люксы и цветовую температуру меряют с погашенным светодиодом. В модели светодиод, который светит прямо на фотодиоды, при тёплой лампе 100 лк поднимает показания с 78,4 до 106,2 лк и с 2891 до 3777 К (коэффициенты — после калибровки 103б.5). Вывод LED на модуле — на вывод МК, а не на питание.

На ярком свету всё белое. Каналы упёрлись в шкалу: у Clear верх наступает первым, потом у остальных, и доли перекашиваются. Проверять col_saturated и не верить отсчёту с насыщением; на солнце даже 2,4 мс при 1× мало (103б.3).

Узнаётся не тот цвет, стоит отодвинуть карточку. Свет светодиода на предмете падает как квадрат расстояния. В модели эталоны сняты на 10 мм:

Расстояние 5 мм 8 мм 10 мм 15 мм 20 мм 30 мм
по отражению 8,3 % 74,2 % 100 % 8,8 % 8,8 % 8,8 %
по цветности 72,1 % 82,9 % 90 % 62,9 % 63,3 % 30,8 %

Ближе — всё ярче белого эталона и 6 отсчётов в насыщении, дальше — всё темнее чёрного; цветность держится дольше, но и её съедают засветка внутри модуля и шум. Расстояние фиксируют: трубка, упор, держатель. Угол — родственная беда: глянцевая карточка под прямым углом отражает светодиод как зеркало. В модели блик 8 % снижает насыщенность красной с 0,969 до 0,828 и точность до 88,8 / 62,9 %. Датчик держат под углом к глянцевой поверхности или берут матовую.

Модуль от 5 В. Сама микросхема по документации производителя питается от 3,3 В; модули со стабилизатором берут и 5 В, но подтяжки SDA, SCL и INT могут уйти к 5 В. PB6…PB9 — FT (с. 32–33), обычный вывод этого не терпит. Модуль без стабилизатора от 5 В — сгорит (не проверено).

Медленно: одно показание в секунду. ATIME = 0x00 — 614,4 мс, а предмет — два измерения: в модели 1234,4 мс. Шкала с 64 тактов не растёт (103б.3): длиннее 153,6 мс брать незачем, кроме совсем тёмного.

Прерывание на каждом измерении. APERS = 0 ставит AINT всегда, пороги не при чём: в модели после tcs_init с нулём STATUS = 0x11. Для сторожа — APERS от 1.


103б.8 Итоги

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

tcs103c.h — регистры, биты, порт и функции драйвера TCS34725.

/* tcs103c.h — раздел 103б: драйвер датчика цвета TCS34725 (фотодиоды R, G, B и Clear под ИК-фильтром) по I2C
 * без регистров микроконтроллера. Драйвер говорит с микросхемой только через функции порта (tcs_port_*):
 * в прошивке fw103c.c их делает I2C1 STM32F103, в стенде host103c.c — модель model103c.py.
 * Адрес, командный байт, карта регистров и биты — по документации производителя; документа в проекте нет,
 * с микросхемой не сверено. Номера в командном байте (0x80 | регистр) совпадают с прямыми номерами регистров
 * родственного TCS3400 в драйвере Zephyr 4.4.2. */
#ifndef TCS103C_H
#define TCS103C_H
#include <stdint.h>

/* ── командный байт: первый байт каждой записи ────────────────────────── */
#define TCS_CMD           0x80u         /* бит 7: это команда — без него байт не понят */
#define TCS_AUTO          0x20u         /* TYPE = 01: номер регистра растёт после каждого байта */
#define TCS_SF_CLEAR      0xE6u         /* TYPE = 11, функция 00110: снять прерывание по Clear */

/* ── регистры ─────────────────────────────────────────────────────────── */
#define TCS_ENABLE        0x00u         /* AIEN [4], WEN [3], AEN [1], PON [0] */
#define TCS_ATIME         0x01u         /* время интегрирования: 2,4 мс × (256 − ATIME) */
#define TCS_WTIME         0x03u         /* пауза между измерениями: 2,4 мс × (256 − WTIME) */
#define TCS_AILTL         0x04u         /* 0x04…0x07 — нижний и верхний пороги Clear, младший байт первым */
#define TCS_PERS          0x0Cu         /* APERS [3:0]: сколько измерений подряд за порогом */
#define TCS_CONFIG        0x0Du         /* WLONG [1]: пауза × 12 */
#define TCS_CONTROL       0x0Fu         /* AGAIN [1:0]: 1×, 4×, 16×, 60× */
#define TCS_ID            0x12u
#define TCS_STATUS        0x13u         /* AINT [4], AVALID [0] */
#define TCS_CDATAL        0x14u         /* 0x14…0x1B — C, R, G, B по два байта, младший первым */

/* ── биты ─────────────────────────────────────────────────────────────── */
#define TCS_EN_PON        0x01u         /* генератор включён */
#define TCS_EN_AEN        0x02u         /* измерения идут */
#define TCS_EN_WEN        0x08u         /* пауза WTIME между измерениями */
#define TCS_EN_AIEN       0x10u         /* прерывание по порогу Clear выводит INT в ноль */
#define TCS_ST_AVALID     0x01u         /* измерение закончено */
#define TCS_ST_AINT       0x10u         /* Clear вышел за пороги */

#define TCS_ADDR          0x29u         /* семибитный: 0x52 запись, 0x53 чтение */
#define TCS_ID_25         0x44u         /* TCS34721 и TCS34725 */
#define TCS_ID_27         0x4Du         /* TCS34723 и TCS34727 */
#define TCS_CYCLE_US      2400u         /* один такт интегрирования */
#define TCS_PON_US        2400u         /* после PON — не меньше 2,4 мс до AEN */
#define TCS_WAIT_TRIES    8u

enum { TCS_OK = 0, TCS_E_NACK = -1, TCS_E_BUS = -2, TCS_E_ID = -3, TCS_E_TIMEOUT = -4 };
enum { TCS_GAIN_1 = 0, TCS_GAIN_4 = 1, TCS_GAIN_16 = 2, TCS_GAIN_60 = 3 };

typedef struct {
    uint8_t atime;                      /* 0xFF — 1 такт, 2,4 мс; 0x00 — 256 тактов */
    uint8_t again;                      /* TCS_GAIN_* */
    uint8_t pers;                       /* APERS, 0…15 */
    uint16_t lo, hi;                    /* пороги Clear */
    uint8_t aien;                       /* 1 — прерывание по порогу */
} tcs_cfg_t;

typedef struct {
    uint8_t addr;
    uint8_t id, status, enable;
    uint8_t atime, again;               /* то, что сейчас записано в микросхему */
    uint32_t xfers, errors;
} tcs_t;

typedef struct {
    uint16_t c, r, g, b;
} tcs_rgbc_t;

/* ── порт: в прошивке — I2C1 и вывод светодиода, в стенде — строки к модели ── */
/* ntx байт записи, затем (если nrx) повторный старт и nrx байт чтения; 0, TCS_E_NACK или TCS_E_BUS */
int tcs_port_xfer(uint8_t addr7, const uint8_t *tx, uint32_t ntx, uint8_t *rx, uint32_t nrx);
void tcs_port_delay_us(uint32_t us);
void tcs_port_led(int on);              /* светодиод подсветки модуля */

/* ── драйвер ──────────────────────────────────────────────────────────── */
int tcs_reg(tcs_t *d, uint8_t reg, uint8_t v);
int tcs_get(tcs_t *d, uint8_t reg, uint8_t *v);
int tcs_init(tcs_t *d, const tcs_cfg_t *c);
int tcs_set(tcs_t *d, uint8_t atime, uint8_t again);
int tcs_thresholds(tcs_t *d, uint16_t lo, uint16_t hi);
int tcs_clear_int(tcs_t *d);
int tcs_read(tcs_t *d, tcs_rgbc_t *v);
int tcs_measure(tcs_t *d, tcs_rgbc_t *v);
uint32_t tcs_cycles(uint8_t atime);
uint32_t tcs_atime_us(uint8_t atime);
uint32_t tcs_max_count(uint8_t atime);
uint32_t tcs_gain_x(uint8_t again);

#endif

tcs103c.c — драйвер: командный байт, инициализация, пороги, чтение и измерение.

/* tcs103c.c — раздел 103б: драйвер TCS34725 по I2C. Один и тот же файл собирается в прошивку (armclang,
 * Cortex-M3) и в стенд на компьютере (gcc): микроконтроллера он не знает, только порт tcs_port_*. */
#include "tcs103c.h"

static int xfer_done(tcs_t *d, int rc)
{
    d->xfers++;
    if (rc)
        d->errors++;
    return rc;
}

/* запись одного регистра: командный байт 0x80 | номер, затем значение */
int tcs_reg(tcs_t *d, uint8_t reg, uint8_t v)
{
    uint8_t b[2] = {(uint8_t)(TCS_CMD | reg), v};
    return xfer_done(d, tcs_port_xfer(d->addr, b, 2, 0, 0));
}

int tcs_get(tcs_t *d, uint8_t reg, uint8_t *v)
{
    uint8_t c = (uint8_t)(TCS_CMD | reg);
    return xfer_done(d, tcs_port_xfer(d->addr, &c, 1, v, 1));
}

uint32_t tcs_cycles(uint8_t atime)
{
    return 256u - atime;
}

uint32_t tcs_atime_us(uint8_t atime)
{
    return TCS_CYCLE_US * tcs_cycles(atime);
}

/* 1024 на такт, но не больше 16 бит */
uint32_t tcs_max_count(uint8_t atime)
{
    uint32_t m = 1024u * tcs_cycles(atime);
    return m > 65535u ? 65535u : m;
}

uint32_t tcs_gain_x(uint8_t again)
{
    static const uint8_t x[4] = {1, 4, 16, 60};
    return x[again & 3u];
}

/* пороги Clear одним обменом: 0x04…0x07 подряд, номер растёт (TYPE = 01) */
int tcs_thresholds(tcs_t *d, uint16_t lo, uint16_t hi)
{
    uint8_t b[5] = {(uint8_t)(TCS_CMD | TCS_AUTO | TCS_AILTL), (uint8_t)lo, (uint8_t)(lo >> 8), (uint8_t)hi, (uint8_t)(hi >> 8)};
    return xfer_done(d, tcs_port_xfer(d->addr, b, 5, 0, 0));
}

/* снять AINT: один командный байт специальной функции, без данных */
int tcs_clear_int(tcs_t *d)
{
    uint8_t c = TCS_SF_CLEAR;
    return xfer_done(d, tcs_port_xfer(d->addr, &c, 1, 0, 0));
}

/* проверить ID, включить генератор, выждать 2,4 мс, настроить, запустить измерения */
int tcs_init(tcs_t *d, const tcs_cfg_t *c)
{
    int rc;
    d->xfers = d->errors = 0;
    if ((rc = tcs_get(d, TCS_ID, &d->id)) != TCS_OK)
        return rc;
    if (d->id != TCS_ID_25 && d->id != TCS_ID_27)
        return TCS_E_ID;                                /* по адресу 0x29 не TCS3472x */
    if ((rc = tcs_reg(d, TCS_ENABLE, TCS_EN_PON)) != TCS_OK)
        return rc;
    tcs_port_delay_us(TCS_PON_US);
    if ((rc = tcs_reg(d, TCS_ATIME, c->atime)) != TCS_OK || (rc = tcs_reg(d, TCS_CONTROL, c->again & 3u)) != TCS_OK
        || (rc = tcs_reg(d, TCS_PERS, c->pers & 0x0Fu)) != TCS_OK || (rc = tcs_thresholds(d, c->lo, c->hi)) != TCS_OK
        || (rc = tcs_clear_int(d)) != TCS_OK)
        return rc;
    d->atime = c->atime;
    d->again = c->again & 3u;
    d->enable = (uint8_t)(TCS_EN_PON | TCS_EN_AEN | (c->aien ? TCS_EN_AIEN : 0u));
    return tcs_reg(d, TCS_ENABLE, d->enable);
}

/* новое время и усиление; измерение перезапускается, чтобы следующий отсчёт был целиком с новыми */
int tcs_set(tcs_t *d, uint8_t atime, uint8_t again)
{
    int rc;
    if ((rc = tcs_reg(d, TCS_ENABLE, (uint8_t)(d->enable & ~TCS_EN_AEN))) != TCS_OK)
        return rc;
    if ((rc = tcs_reg(d, TCS_ATIME, atime)) != TCS_OK || (rc = tcs_reg(d, TCS_CONTROL, again & 3u)) != TCS_OK)
        return rc;
    d->atime = atime;
    d->again = again & 3u;
    return tcs_reg(d, TCS_ENABLE, d->enable);
}

/* STATUS и восемь байт C, R, G, B одним чтением: командный байт 0xB3, номер растёт */
int tcs_read(tcs_t *d, tcs_rgbc_t *v)
{
    uint8_t c = (uint8_t)(TCS_CMD | TCS_AUTO | TCS_STATUS), b[9];
    int rc = xfer_done(d, tcs_port_xfer(d->addr, &c, 1, b, 9));
    if (rc != TCS_OK)
        return rc;
    d->status = b[0];
    v->c = (uint16_t)(b[1] | b[2] << 8);                /* младший байт первым */
    v->r = (uint16_t)(b[3] | b[4] << 8);
    v->g = (uint16_t)(b[5] | b[6] << 8);
    v->b = (uint16_t)(b[7] | b[8] << 8);
    return TCS_OK;
}

/* одно свежее измерение: перезапуск AEN (снимает AVALID), ожидание времени интегрирования, опрос AVALID */
int tcs_measure(tcs_t *d, tcs_rgbc_t *v)
{
    int rc;
    if ((rc = tcs_reg(d, TCS_ENABLE, (uint8_t)(d->enable & ~TCS_EN_AEN))) != TCS_OK
        || (rc = tcs_reg(d, TCS_ENABLE, d->enable)) != TCS_OK)
        return rc;
    tcs_port_delay_us(tcs_atime_us(d->atime) + TCS_CYCLE_US);
    for (uint32_t i = 0; i < TCS_WAIT_TRIES; i++) {
        if ((rc = tcs_read(d, v)) != TCS_OK)
            return rc;
        if (d->status & TCS_ST_AVALID)
            return TCS_OK;
        tcs_port_delay_us(TCS_CYCLE_US);
    }
    return TCS_E_TIMEOUT;
}

col103c.h — параметры обработки и коэффициенты DN40.

/* col103c.h — раздел 103б: обработка отсчётов TCS34725 целыми числами (у F103 нет FPU): автоподбор времени и
 * усиления, отсчёт предмета без внешнего света, баланс белого, цветность, оттенок, ближайший эталон,
 * освещённость и цветовая температура по формулам аппнотки DN40 (коэффициенты — параметры, не проверено). */
#ifndef COL103C_H
#define COL103C_H
#include <stdint.h>
#include "tcs103c.h"

#define COL_STEPS      7u               /* ступени автоподбора: от самой грубой к самой чувствительной */
#define COL_FILL_PCT   70u              /* ступень годится, если Clear на ней ожидается ниже 70 % шкалы */
#define COL_AUTO_TRIES 8u

typedef struct {
    uint8_t atime, again;
} col_step_t;

extern const col_step_t col_steps[COL_STEPS];

typedef struct {
    int32_t c, r, g, b;                 /* со знаком: разность двух отсчётов может уйти ниже нуля */
} col_v_t;

typedef struct {
    col_v_t dark, white;                /* тёмный отсчёт и белая карточка */
} col_wb_t;

/* коэффициенты DN40: r, g, b — ×1000; df — множитель устройства; ga — ослабление стекла ×1000 */
typedef struct {
    int32_t r, g, b;
    int32_t ct_coef, ct_off;
    int32_t df, ga;
    int ir_sub;                         /* 1 — вычитать ИК-оценку (R + G + B − C) / 2 */
} col_dn40_t;

extern const col_dn40_t col_dn40_default;

typedef struct {
    int32_t ir;                         /* ИК-оценка, отсчёты */
    int32_t lux10;                      /* освещённость × 10 */
    int32_t cct;                        /* цветовая температура, К; 0 — не посчитать */
    int sat;                            /* 1 — какой-то канал упёрся в шкалу: верить нельзя */
} col_light_t;

int col_saturated(const tcs_rgbc_t *v, uint8_t atime);
int col_auto(tcs_t *d, uint32_t *step, tcs_rgbc_t *v, uint32_t *tries);
int col_object(tcs_t *d, col_v_t *o, int *sat);
void col_balance(const col_wb_t *wb, const col_v_t *o, int32_t out[3]);
void col_chroma(const int32_t x[3], int32_t out[3]);
void col_hsv(const int32_t x[3], int32_t *h10, int32_t *s1000, int32_t *v);
uint32_t col_nearest(const int32_t (*ref)[3], uint32_t n, const int32_t x[3]);
void col_light(const tcs_rgbc_t *v, uint8_t atime, uint8_t again, const col_dn40_t *p, col_light_t *out);

#endif

col103c.c — обработка: автоподбор, предмет, баланс белого, оттенок, эталоны, DN40.

/* col103c.c — раздел 103б: автоподбор, баланс белого, цвет предмета, освещённость и цветовая температура.
 * Тот же файл — в прошивке (armclang, Cortex-M3) и в стенде на компьютере (gcc). */
#include "col103c.h"

/* чувствительность ступени — такты × усиление: 1, 10, 64, 256, 1024, 3840, 15360 */
const col_step_t col_steps[COL_STEPS] = {
    {0xFF, TCS_GAIN_1}, {0xF6, TCS_GAIN_1}, {0xC0, TCS_GAIN_1}, {0xC0, TCS_GAIN_4},
    {0xC0, TCS_GAIN_16}, {0xC0, TCS_GAIN_60}, {0x00, TCS_GAIN_60},
};

/* DN40 для TCS34725 — по аппнотке производителя, не проверено; под своё стекло их снимают заново */
const col_dn40_t col_dn40_default = {.r = 136, .g = 1000, .b = -444, .ct_coef = 3810, .ct_off = 1391, .df = 310, .ga = 1000,
                                     .ir_sub = 1};

static uint32_t sens(uint32_t k)
{
    return tcs_cycles(col_steps[k].atime) * tcs_gain_x(col_steps[k].again);
}

int col_saturated(const tcs_rgbc_t *v, uint8_t atime)
{
    uint32_t m = tcs_max_count(atime);
    return v->c >= m || v->r >= m || v->g >= m || v->b >= m;
}

/* автоподбор: упёрлись в шкалу — ступенью ниже; иначе самая чувствительная ступень, где Clear ожидается
 * ниже 70 % шкалы; совпала с текущей — готово */
int col_auto(tcs_t *d, uint32_t *step, tcs_rgbc_t *v, uint32_t *tries)
{
    uint32_t k = *step < COL_STEPS ? *step : 0u;
    int rc;
    for (*tries = 1;; (*tries)++) {
        if ((rc = tcs_set(d, col_steps[k].atime, col_steps[k].again)) != TCS_OK || (rc = tcs_measure(d, v)) != TCS_OK)
            return rc;
        uint32_t j = k;
        if (col_saturated(v, col_steps[k].atime)) {
            if (k == 0u)
                break;                                  /* ярче шкалы даже на самой грубой ступени */
            j = k - 1u;
        } else {
            for (j = COL_STEPS - 1u; j > 0u; j--) {
                uint64_t want = (uint64_t)v->c * sens(j) / sens(k);
                if (want * 100u < (uint64_t)tcs_max_count(col_steps[j].atime) * COL_FILL_PCT)
                    break;
            }
        }
        if (j == k || *tries >= COL_AUTO_TRIES)
            break;
        k = j;
    }
    *step = k;
    return TCS_OK;
}

/* отсчёт предмета: со светодиодом минус без него — внешний свет вычитается */
int col_object(tcs_t *d, col_v_t *o, int *sat)
{
    tcs_rgbc_t on, off;
    int rc;
    tcs_port_led(1);
    rc = tcs_measure(d, &on);
    tcs_port_led(0);
    if (rc != TCS_OK || (rc = tcs_measure(d, &off)) != TCS_OK)
        return rc;
    *sat = col_saturated(&on, d->atime);
    o->c = (int32_t)on.c - off.c;
    o->r = (int32_t)on.r - off.r;
    o->g = (int32_t)on.g - off.g;
    o->b = (int32_t)on.b - off.b;
    return TCS_OK;
}

/* баланс белого: (x − тёмный) / (белый − тёмный), белая карточка — 1000 в каждом канале */
void col_balance(const col_wb_t *wb, const col_v_t *o, int32_t out[3])
{
    const int32_t x[3] = {o->r, o->g, o->b}, dk[3] = {wb->dark.r, wb->dark.g, wb->dark.b},
                  wh[3] = {wb->white.r, wb->white.g, wb->white.b};
    for (int i = 0; i < 3; i++) {
        int32_t span = wh[i] - dk[i];
        out[i] = span > 0 ? (int32_t)((int64_t)(x[i] - dk[i]) * 1000 / span) : 0;
    }
}

/* цветность: доли каналов ×1000 — яркость (и расстояние) уходит */
void col_chroma(const int32_t x[3], int32_t out[3])
{
    int32_t a[3], s = 0;
    for (int i = 0; i < 3; i++) {
        a[i] = x[i] > 0 ? x[i] : 0;
        s += a[i];
    }
    for (int i = 0; i < 3; i++)
        out[i] = s > 0 ? a[i] * 1000 / s : 333;
}

/* HSV: оттенок в десятых долях градуса, насыщенность ×1000, яркость — как во входе */
void col_hsv(const int32_t x[3], int32_t *h10, int32_t *s1000, int32_t *v)
{
    int32_t r = x[0] > 0 ? x[0] : 0, g = x[1] > 0 ? x[1] : 0, b = x[2] > 0 ? x[2] : 0;
    int32_t mx = r > g ? (r > b ? r : b) : (g > b ? g : b), mn = r < g ? (r < b ? r : b) : (g < b ? g : b), dd = mx - mn, h;
    *v = mx;
    *s1000 = mx > 0 ? dd * 1000 / mx : 0;
    if (dd == 0) {
        *h10 = 0;
        return;
    }
    if (mx == r)
        h = 600 * (g - b) / dd;
    else if (mx == g)
        h = 1200 + 600 * (b - r) / dd;
    else
        h = 2400 + 600 * (r - g) / dd;
    *h10 = h < 0 ? h + 3600 : h;
}

/* ближайший эталон по сумме квадратов разностей */
uint32_t col_nearest(const int32_t (*ref)[3], uint32_t n, const int32_t x[3])
{
    uint32_t best = 0;
    int64_t bd = -1;
    for (uint32_t i = 0; i < n; i++) {
        int64_t s = 0;
        for (int k = 0; k < 3; k++) {
            int64_t e = (int64_t)x[k] - ref[i][k];
            s += e * e;
        }
        if (bd < 0 || s < bd) {
            bd = s;
            best = i;
        }
    }
    return best;
}

/* DN40: IR = (R + G + B − C) / 2; X' = X − IR; люксы = (r·R' + g·G' + b·B') / CPL,
 * CPL = (2,4 мс × такты) × усиление / (GA × DF); CCT = ct_coef × B'/R' + ct_off */
void col_light(const tcs_rgbc_t *v, uint8_t atime, uint8_t again, const col_dn40_t *p, col_light_t *out)
{
    int32_t ir = p->ir_sub ? ((int32_t)v->r + v->g + v->b - v->c) / 2 : 0;
    if (ir < 0)
        ir = 0;
    int32_t r = v->r - ir, g = v->g - ir, b = v->b - ir;
    int64_t s = (int64_t)p->r * r + (int64_t)p->g * g + (int64_t)p->b * b;
    int64_t den = (int64_t)240000 * tcs_cycles(atime) * tcs_gain_x(again);
    out->ir = ir;
    out->lux10 = (int32_t)(s * p->ga * p->df / den);
    out->cct = r > 0 ? (int32_t)((int64_t)p->ct_coef * b / r + p->ct_off) : 0;
    out->sat = col_saturated(v, atime);
}

fw103c.c — прошивка F103: I2C1, INT на PB8, светодиод на PB9, свет, предмет и сторож.

/* fw103c.c — раздел 103б: TCS34725 на STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц). INT модуля — на PB8 (допуск 5 В, вход с подтяжкой,
 * EXTI8 по спаду). LED модуля — на PB9 (выход; единица — светодиод горит: так у распространённых модулей,
 * смотрите схему своего). Цикл: внешний свет (автоподбор, люксы и цветовая температура по DN40), затем
 * предмет перед датчиком (со светодиодом минус без него, баланс белого, ближайший эталон). Калибровка — через
 * отладчик: cal = 1 — чёрная карточка, 2 — белая, 3 — эталон номер ref_n; watch = 1 — сторож: ждать INT по порогу. Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "tcs103c.h"
#include "col103c.h"

#define CPU_HZ          72000000u
#define APB1_MHZ        36u
#define I2C_TIMEOUT_US  1000u           /* дольше любого флага на 400 кГц */
#define NREF            12u
#define OBJ_STEP        4u              /* 153,6 мс и 16×: белая карточка в 10 мм — четверть шкалы (в модели) */

static tcs_t dev = {.addr = TCS_ADDR};
static col_wb_t wb;
static int32_t ref[NREF][3];
static volatile uint8_t irq;
volatile uint32_t cal, ref_n, nref, step = 2u, color, watch, irq_count, bus_errors;
volatile int32_t lux10, cct, hue10, sat1000;

static int expired(uint32_t t0, uint32_t us)
{
    return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}

void tcs_port_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT;
    while (!expired(t0, us)) {
    }
}

void tcs_port_led(int on)
{
    GPIOB->BSRR = on ? 1u << 9 : 1u << (9 + 16);
}

static void gpio_i2c1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    /* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111); PB8 — вход с подтяжкой (1000);
     * PB9 — выход двухтактный, 2 МГц (0010) */
    GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;
    GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x28u;
    GPIOB->BSRR = 1u << 8 | 1u << (9 + 16);             /* подтяжка PB8 вверх, светодиод погашен */
    I2C1->CR1 = 0;                                      /* настраивать при PE = 0 */
    I2C1->CR2 = APB1_MHZ;
    I2C1->CCR = I2C_CCR_FS | 30u;                       /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
    I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u;         /* 300 нс × 36 МГц = 10,8 → 10 + 1 = 11 */
    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 TCS_E_NACK;                          /* адрес или байт не подтверждён */
        if (expired(t0, I2C_TIMEOUT_US))
            return TCS_E_BUS;
    }
    return TCS_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;
}

/* приём nrx байт после повторного старта: 1 байт — RM0008, с. 764; больше двух — способ 2, с. 762–763 */
static int rx_bytes(uint8_t *rx, uint32_t nrx)
{
    int rc;
    uint32_t i = 0;
    if (nrx == 1u) {
        I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;            /* ACK снять до сброса ADDR */
        (void)I2C1->SR2;
        I2C1->CR1 |= I2C_CR1_STOP;
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != TCS_OK)
            return rc;
        rx[0] = (uint8_t)I2C1->DR;
        I2C1->CR1 |= I2C_CR1_ACK;
        return TCS_OK;
    }
    (void)I2C1->SR2;                                    /* ADDR снят, ACK = 1 */
    for (; i + 3u < nrx; i++) {
        if ((rc = sr1_wait(I2C_SR1_RXNE)) != TCS_OK)
            return rc;
        rx[i] = (uint8_t)I2C1->DR;
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != TCS_OK)         /* осталось три: N−2 в DR, N−1 в сдвиговом, SCL прижат */
        return rc;
    I2C1->CR1 &= (uint16_t)~I2C_CR1_ACK;
    rx[i++] = (uint8_t)I2C1->DR;                        /* N−2; пошёл приём N с NACK */
    if ((rc = sr1_wait(I2C_SR1_BTF)) != TCS_OK)
        return rc;
    I2C1->CR1 |= I2C_CR1_STOP;
    rx[i++] = (uint8_t)I2C1->DR;                        /* N−1 */
    if ((rc = sr1_wait(I2C_SR1_RXNE)) != TCS_OK)
        return rc;
    rx[i] = (uint8_t)I2C1->DR;                          /* N */
    I2C1->CR1 |= I2C_CR1_ACK;
    return TCS_OK;
}

int tcs_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 == 2u || ntx == 0u)
        return TCS_E_BUS;                               /* драйвер пишет хотя бы командный байт, читает 1 или 9 */
    while (I2C1->SR2 & I2C_SR2_BUSY) {                  /* SDA держит ведомый — раздел 17.6 */
        if (expired(t0, I2C_TIMEOUT_US))
            return TCS_E_BUS;
    }
    I2C1->CR1 |= I2C_CR1_ACK;
    I2C1->CR1 |= I2C_CR1_START;
    if ((rc = sr1_wait(I2C_SR1_SB)) != TCS_OK)
        return fail(rc);
    I2C1->DR = (uint8_t)(addr7 << 1);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != TCS_OK)
        return fail(rc);
    (void)I2C1->SR2;
    for (uint32_t i = 0; i < ntx; i++) {
        if ((rc = sr1_wait(I2C_SR1_TXE)) != TCS_OK)
            return fail(rc);
        I2C1->DR = tx[i];
    }
    if ((rc = sr1_wait(I2C_SR1_BTF)) != TCS_OK)
        return fail(rc);
    if (!nrx) {
        I2C1->CR1 |= I2C_CR1_STOP;
        return TCS_OK;
    }
    I2C1->CR1 |= I2C_CR1_START;                         /* повторный старт: командный байт уже передан */
    if ((rc = sr1_wait(I2C_SR1_SB)) != TCS_OK)
        return fail(rc);
    I2C1->DR = (uint8_t)(addr7 << 1 | 1u);
    if ((rc = sr1_wait(I2C_SR1_ADDR)) != TCS_OK)
        return fail(rc);
    if ((rc = rx_bytes(rx, nrx)) != TCS_OK)
        return fail(rc);
    return TCS_OK;
}

/* INT: спад на PB8 — Clear вышел за пороги нужное число измерений подряд */
void EXTI9_5_IRQHandler(void)
{
    EXTI->PR = 1u << 8;
    irq = 1;
}

static void irq_init(void)
{
    AFIO->EXTICR[2] = (AFIO->EXTICR[2] & ~0xFu) | 0x1u;     /* EXTI8 ← PB8 */
    EXTI->FTSR |= 1u << 8;
    EXTI->IMR |= 1u << 8;
    NVIC_EnableIRQ(EXTI9_5_IRQn);
}

/* до автоподбора: 60 мс — 6 периодов мерцания 100 Гц, 16×; порог — 5 измерений подряд (APERS = 4) */
static const tcs_cfg_t cfg = {.atime = 0xE7, .again = TCS_GAIN_16, .pers = 4, .lo = 0, .hi = 0xFFFF, .aien = 0};

static void calibrate(const col_v_t *o)
{
    if (cal == 1u) {
        wb.dark = *o;                                   /* чёрная карточка */
        nref = 0;
    } else if (cal == 2u) {
        wb.white = *o;                                  /* белая карточка */
    } else if (cal == 3u && ref_n < NREF) {
        col_balance(&wb, o, ref[ref_n]);
        if (ref_n >= nref)
            nref = ref_n + 1u;
    }
    cal = 0;
}

/* сторож: 60 мс и 16×, пороги ±25 % вокруг нынешнего Clear, INT — когда свет изменился на 5 измерений подряд */
static int watch_once(void)
{
    tcs_rgbc_t v;
    int rc;
    if ((rc = tcs_set(&dev, 0xE7, TCS_GAIN_16)) != TCS_OK || (rc = tcs_measure(&dev, &v)) != TCS_OK)
        return rc;
    uint32_t hi = v.c + v.c / 4u;
    if ((rc = tcs_thresholds(&dev, (uint16_t)(v.c - v.c / 4u), (uint16_t)(hi > 65535u ? 65535u : hi))) != TCS_OK
        || (rc = tcs_clear_int(&dev)) != TCS_OK)
        return rc;
    irq = 0;
    dev.enable |= TCS_EN_AIEN;
    if ((rc = tcs_reg(&dev, TCS_ENABLE, dev.enable)) != TCS_OK)
        return rc;
    while (watch && !irq && (GPIOB->IDR & (1u << 8))) {
    }                                                   /* INT в единице и спада не было — ждать */
    if (watch)
        irq_count++;
    dev.enable &= (uint8_t)~TCS_EN_AIEN;
    if ((rc = tcs_reg(&dev, TCS_ENABLE, dev.enable)) != TCS_OK)
        return rc;
    return tcs_clear_int(&dev);
}

int main(void)
{
    tcs_rgbc_t v;
    col_light_t l;
    col_v_t o;
    int sat;
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    gpio_i2c1_init();
    irq_init();
    while (tcs_init(&dev, &cfg) != TCS_OK)
        tcs_port_delay_us(100000u);                     /* нет ответа: провод, адрес, питание, уровни */
    for (;;) {
        uint32_t s = step, tries;
        if (watch) {
            if (watch_once() != TCS_OK)
                bus_errors++;
            continue;
        }
        /* внешний свет: светодиод погашен, автоподбор, DN40 */
        if (col_auto(&dev, &s, &v, &tries) != TCS_OK) {
            bus_errors++;
            continue;
        }
        step = s;
        col_light(&v, dev.atime, dev.again, &col_dn40_default, &l);
        lux10 = l.lux10;
        cct = l.cct;
        /* предмет: та ступень, на которой снимали белую карточку */
        if (tcs_set(&dev, col_steps[OBJ_STEP].atime, col_steps[OBJ_STEP].again) != TCS_OK
            || col_object(&dev, &o, &sat) != TCS_OK) {
            bus_errors++;
            continue;
        }
        if (cal)
            calibrate(&o);
        if (nref) {
            int32_t x[3], h, sa, val;
            col_balance(&wb, &o, x);
            col_hsv(x, &h, &sa, &val);
            hue10 = h;
            sat1000 = sa;
            color = sat ? 0xFFu : col_nearest((const int32_t(*)[3])ref, nref, x);  /* 0xFF — насыщение, не верить */
        }
    }
}

Дальше: раздел 103в «Датчик пыли PMS5003 по UART: кадр 32 байта, PM1.0/PM2.5/PM10, активный и пассивный режим, сон и индекс качества воздуха» — пыль вместо цвета: кадр PMS5003 по UART через DMA в кольцо, пассивный режим, сон с прогревом и индекс AQI по PM2.5.

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