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 мс всего.
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 Итоги
- TCS34725 — каналы C, R, G, B под ИК-фильтром, 16 бит, I2C, адрес 0x29; первый байт — командный с битом 0x80, 0xA0 | номер — чтение подряд, 0xE6 — снять прерывание; всё это — по документации производителя.
- Время интегрирования — 2,4 мс × (256 −
ATIME), верх шкалы — 1024 × (256 −ATIME), но не больше 65535; с 64 тактов (153,6 мс) он не растёт. Усиление — 1×, 4×, 16×, 60×. - Время, кратное 10 мс (60 мс — 25 тактов), вычитает мерцание 100 Гц: в модели разброс 5,36 % против 67,44 % при 2,4 мс.
- Цвет предмета — со светодиодом минус без него, баланс по чёрной и белой карточкам, ближайший эталон: в модели 100 % на 10 мм при любом свете, кроме насыщения; расстояние надо фиксировать.
- Люксы и цветовая температура — DN40 с вычетом ИК; коэффициенты — параметры: в модели CCT по аппнотке занижена в 1,4–1,7 раза, после калибровки по 2856 и 6500 К — 2674…6499 К у чёрных тел.
- ИК лампы накаливания без вычета завышает люксы на четверть (630,1 вместо 500 в модели).
- Прерывание по порогу Clear — с persistence: при 2,4 мс и
APERS1 — 517 ложных за 3 с, при 60 мс — ни одного.
Полные исходники
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.