100г. Лазерный дальномер VL53L1X: до 4 м, бюджет времени и межизмерительный период, режимы дальности, зона интереса (ROI), статус измерения и несколько датчиков на одной шине
В разделе 100б VL53L0X мерил миллиметры временем пролёта: как свет летит
пикосекунды, что такое SPAD, зачем XSHUT и GPIO1, как включить три датчика на
одной шине. В 100в APDS-9960 ловил взмах руки по свету без миллиметров.
VL53L1X — следующий датчик ST того же рода: тот же адрес 0x29, те же XSHUT и
GPIO1, но матрица SPAD побольше, дальность до метров, и у матрицы можно
выбрать часть — зону интереса. Принцип времени пролёта здесь не повторяется:
только то, чем L1X отличается.
Здесь: что изменилось против L0X; 16-битные адреса регистров, слово 0xEACC и
блок настроек из 91 байта — откуда он и что в нём; режимы Short, Medium и
Long, бюджет времени и период по таймеру, их пересчёт в регистры; шум и
дальность в модели; статус измерения и обёртка фазы; зона интереса и грубая
«картинка» из 16 зон; прерывание по окну дальности; стекло перед датчиком и
две калибровки; три датчика на одной шине и сон.
Опоры: API ST для VL53L1X 2.4.5 (STSW-IMG007) — его копия лежит в модуле
hal_st проекта Zephyr (github.com/zephyrproject-rtos/hal_st, коммит 7a79288,
BSD-3-Clause): карта регистров, форматы, пресеты режимов, формулы времени,
правила статусов, калибровки. Драйвер vl53l1x в Zephyr 4.4.2
(github.com/zephyrproject-rtos/zephyr, коммит dccb095, Apache-2.0) и его
привязка — время загрузки, полярность GPIO1, таблица дальностей по режимам,
нумерация ROI. Открытая библиотека vl53l1x в наборе скриптов OpenMV
(github.com/openmv/openmv, MIT) — блок настроек по умолчанию с комментариями и
слово 0xEACC. Документа производителя (datasheet) и драйвера ULD в проекте
нет: поле зрения 27°, пределы бюджета 15…500 мс, таблица дальностей, питание
и токи — по документации производителя, не проверено. Модель датчика на
Python — своя, числа шума и дальности говорят о модели, а не о датчике. Стенд
— наш драйвер tof100e.c по регистрам, собранный gcc и прогнанный против
модели; формулы времени и номера SPAD сверены с функциями API, взятыми
дословно; тот же код и прошивка fw100e.c собраны armclang под STM32F103 и на
плате не запускались. RM0008 (DocID13902 Rev 17): EXTI1…EXTI3 — позиции 7…9 в
таблице векторов 63, с. 203. DocID13587 Rev 17: PB6 — I2C1_SCL, PB7 —
I2C1_SDA, оба FT, с. 32. I²C — раздел 17, прерывания от вывода — 7.5,
подключение модулей — 95.
Как это называется в документации
Оригинал Перевод ULD (ultra lite driver) облегчённый драйвер ST: функции прямо поверх регистров region of interest, ROI centre SPAD зона интереса; номер SPAD в её центре distance mode: Short, Medium, Long режим дальности: пара периодов лазера и окно фазы timing budget (TB) время на одно измерение по ULD и API L1X inter-measurement period (IMP) шаг измерений по таймеру, в тиках медленного генератора timing A / timing B два чередующихся тайминга с разными периодами лазера phase wraparound, wrap target fail обёртка фазы: дальняя цель видна близкой; отказ по обёртке range valid, no wrap check годно, но обёртка не проверена — первое после пуска stream count счётчик измерений с пуска DSS (dynamic SPAD selection) автоматический выбор числа включённых SPAD crosstalk plane offset (kcps) помеха стекла на один SPAD, вычитается датчиком part-to-part range offset поправка смещения дальности конкретного экземпляра distance threshold / window interrupt прерывание по порогу или окну дальности hardware standby аппаратный сон по XSHUT
100г.1 Чем L1X отличается от L0X
То же, что в 100б: время пролёта, лавинные фотодиоды, адрес 0x29 после
включения, вход XSHUT с активным нулём и выход GPIO1 «готово». Отличия —
в таблице.
| VL53L0X (100б) | VL53L1X | |
|---|---|---|
| приёмник | опорные и сигнальные SPAD без выбора | матрица 16×16 SPAD, зона интереса от 4×4 до 16×16 |
| дальность | в модели 100б белая — до 953 мм (обычный профиль) | в таблице из драйвера Zephyr: Long — 360 см в темноте (по документации производителя, не проверено) |
| поле зрения | не названо | 27° на всю матрицу (по документации производителя, не проверено) |
| адрес регистра | 8 бит | 16 бит, старшим байтом вперёд |
| чем пользоваться | только API (библиотека на Си) | API или ULD; здесь — свой драйвер по регистрам |
| бюджет | от 20 мс | 15…500 мс по ULD (не проверено); у нас — те же пределы |
| режимы | профили пределов | Short, Medium, Long — разные периоды лазера |
Главная новинка — фаза. Датчик меряет задержку отражения в периодах PLL и
складывает это в фазу; в API её переводит в миллиметры VL53L1_range_maths:
разница фаз × период PLL × скорость света. Период PLL считается из частоты
быстрого генератора в заводской памяти (регистр 0x0006, формат 4.12). Если
она меньше 0x1000, API подставляет 0xBCCC — 11,8 МГц, ×64 — 755,2 МГц, такт
1,3241 нс. С этим числом единица фазы — 198,4 мм дальности.
Импульсы лазера идут не каждый такт, а раз в период: 16, 24 или 32 такта PLL в зависимости от режима. Фаза поэтому круговая: цель дальше, чем полпериода пути света, даёт ту же фазу, что и близкая. Против этого у датчика два тайминга, A и B, с разными периодами; соседние измерения идут то с одним, то с другим, и датчик сравнивает их фазы (100г.5).
100г.2 Подключение, 16-битные регистры, слово 0xEACC и блок из 91 байта
| Вывод | Что это | Куда в прошивке |
|---|---|---|
| SCL, SDA | I²C, адрес 0x29 после включения |
PB6, PB7 — I2C1, 400 кГц |
| XSHUT | сброс и аппаратный сон, активный ноль | PB12, PB13, PB14 — открытый сток |
| GPIO1 | «готово» или «сработало окно»; полярность задаёт регистр 0x30 |
PA1, PA2, PA3 — EXTI по спаду |
| питание, земля | по описанию модуля | — |
Шина та же, что у L0X, и код I2C1 в прошивке тот же (100б, полные исходники ниже). Разница — в адресе регистра: два байта, старший первым. Данные многобайтных регистров — тоже старшим байтом вперёд.
int l1_wr(l1_t *d, uint16_t reg, const uint8_t *p, uint32_t n)
{
uint8_t b[2u + L1_MAX_WR];
if (n > L1_MAX_WR)
return 1;
b[0] = (uint8_t)(reg >> 8);
b[1] = (uint8_t)reg;
memcpy(b + 2, p, n);
d->xfers++;
d->bytes += n + 2u;
return l1_i2c_write(d->addr, b, n + 2u);
}
int l1_rd(l1_t *d, uint16_t reg, uint8_t *p, uint32_t n)
{
uint8_t a[2] = {(uint8_t)(reg >> 8), (uint8_t)reg};
d->xfers++;
d->bytes += n + 2u;
return l1_i2c_wr_rd(d->addr, a, 2, p, n);
}
Перепутать порядок байт адреса легко: чтение «слова ID» с адресом,
отправленным младшим байтом вперёд, уходит в регистр 0x0F01, и модель
вернула 0x0000 — не ошибку, а нули. Поэтому слово ID — первая проверка
после включения.
Слово 0xEACC. Это два байта подряд: 0xEA по 0x010F — номер модели
(значение по умолчанию в карте регистров API) — и 0xCC по 0x0110 — тип
модуля (в API: «VL53L1 = 0xCC, VL53L3 = 0xAA»). Читаем словом и сравниваем
с 0xEACC, как библиотека OpenMV.
Загрузка. После отпускания XSHUT датчик грузит свою прошивку; в драйвере
Zephyr на это отведено «до 1,2 мс», а API ждёт бита 0 регистра 0xE5. Наш
драйвер опрашивает этот бит каждые 200 мкс; пока датчик грузится, он не
отвечает, и это тоже «ещё не готов».
/* Отпустить XSHUT, дождаться загрузки (бит 0 регистра 0xE5), проверить слово 0xEACC и перевести датчик на свой
* адрес. Пока датчик грузится, он не отвечает — это тоже «ещё не готов». */
int l1_boot(l1_t *d, uint8_t idx, uint8_t addr7)
{
uint8_t st = 0;
uint16_t id = 0;
uint32_t k;
memset(d, 0, sizeof *d);
d->idx = idx;
d->addr = L1_ADDR_DEFAULT;
l1_xshut(idx, 1);
for (k = 0; k < L1_BOOT_TRIES; k++) {
l1_delay_us(L1_BOOT_POLL_US);
if (l1_rd(d, L1_FW_STATUS, &st, 1) == 0 && (st & 1u))
break;
}
if (k == L1_BOOT_TRIES)
return 1;
if (l1_rd16(d, L1_MODEL_ID, &id) || id != L1_ID)
return 2;
if (addr7 != L1_ADDR_DEFAULT) {
if (l1_wr8(d, L1_I2C_ADDR, addr7)) /* семибитный адрес; API делит 8-битный на 2 сам */
return 3;
d->addr = addr7;
id = 0;
if (l1_rd16(d, L1_MODEL_ID, &id) || id != L1_ID)
return 4;
}
return 0;
}
В модели загрузка длится 1,2 мс: первое подтверждение пришло через 1337,5 мкс после XSHUT, на 7-м обращении; всё включение одного датчика — 7 транзакций, 22 байта, 400 мкс шины.
Блок из 91 байта. У VL53L1X нет описания регистров в открытом доступе,
как и у L0X; API ST грузит настройки из пресетов, а облегчённый драйвер ULD —
одной записью готового блока в регистры 0x2D…0x87: 0x87 − 0x2D + 1 = 91
байт. Этот блок есть в библиотеке OpenMV; его комментарии ссылаются на
функции ULD — VL53L1X_init(), SetROI(), StartRanging(), — так что
происхождение у него оттуда (по документации производителя, не проверено).
Что в нём, по комментариям и карте регистров API:
| Регистры | В блоке | Что это | В пресетах API |
|---|---|---|---|
0x30 |
0x01 |
GPIO1: выход прерывания, бит 4 = 0 — активна единица | 0x11 — активен ноль |
0x46 |
0x20 |
прерывание на каждое новое измерение | то же |
0x5E…0x63 |
0x01DB, 15, 0x01F1, 13 |
таймауты и периоды лазера A и B — режим Long | Long: 15 и 13 |
0x64 |
0x0168 |
предел σ, 14.2: 90 мм | 360 → 90 мм |
0x66 |
0x0080 |
предел сигнала, 9.7: 1,0 MCPS | 192 → 1,5 MCPS |
0x69 |
0xB8 |
верх окна фазы, 5.3: 23,0 | Long: 0xB8 |
0x6C |
0x00000F89 |
период по таймеру | ставится отдельно |
0x7F, 0x80 |
0xC7, 0xFF |
центр ROI — SPAD 199, размер 16×16 | то же |
0x81 |
0x9B |
шаги измерения: без MM2 | 0xDB — с MM2 |
0x86, 0x87 |
0x01, 0x40 |
снять прерывание; пуск по таймеру | — |
Последний байт — ловушка: с 0x40 в 0x87 измерения пошли бы сразу при
загрузке блока. У нас там 0x00, пуск — отдельно. При частоте 0xBCCC
таймауты блока — 42 857 и 41 259 мкс, то есть бюджет около 43 мс.
После блока — то, что делают API и OpenMV: смещение из 0x22 × 4 в 0x1E
(шаги MM выключены, работает только оно), затем полярность и частоты
генераторов.
/* Загрузить блок по умолчанию одной записью, перенести заводское смещение, сделать GPIO1 активным нулём,
* прочитать частоты генераторов. После этого — режим Long, бюджет 33 мс, ROI 16×16. */
int l1_init(l1_t *d)
{
uint16_t v = 0;
if (l1_wr(d, L1_CFG_FIRST, L1_DEFAULT_CFG, L1_CFG_LEN))
return 10;
if (l1_rd16(d, L1_OUTER_OFFSET_MM, &v) || l1_wr16(d, L1_OFFSET_MM, (uint16_t)(v * 4u))) /* как в OpenMV */
return 11;
if (l1_wr8(d, L1_GPIO_MUX, 0x11u)) /* в блоке 0x01 — активна единица; пресеты API: 0x11 */
return 12;
if (l1_rd16(d, L1_OSC_FREQ, &d->osc) || d->osc == 0)
return 13;
if (l1_rd16(d, L1_OSC_CAL, &v))
return 14;
d->osc_cal = v & 0x3FFu;
d->budget_us = 33000u;
if (l1_set_mode(d, L1_LONG))
return 15;
return 0;
}
Включение по блоку — 13 транзакций, 138 байт, 3537,5 мкс шины; из них 2120 мкс — одна запись блока.
Три датчика на одной шине включают так же, как L0X в 100б.5: все XSHUT
вниз, затем по одному — отпустить, дождаться загрузки, проверить ID по
0x29, записать новый адрес. Отличие одно: адрес лежит в регистре 0x0001, и
пишется туда семибитный адрес — функция API VL53L1_SetDeviceAddress сама
делит переданный восьмибитный на 2, как у L0X. На стенде три датчика
получили 0x30, 0x31, 0x32 за 27 транзакций, 1912,5 мкс шины и
5512,5 мкс вместе с загрузкой. Отпущенные разом — все три уехали на
0x30.
Сон и питание. Привязка Zephyr описывает XSHUT так: ноль — аппаратный сон,
если питание подано, или полное выключение без питания. После сна адрес снова
0x29 и блок надо грузить заново. Между измерениями по таймеру датчик сам
простаивает; при бюджете 33 мс и периоде 100 мс он меряет треть времени.
Токи в каждом состоянии и питание модуля — по документации производителя,
не проверено; модель их не считает.
100г.3 Режимы Short, Medium, Long; бюджет и период по таймеру
Режим — это пара периодов лазера для таймингов A и B и верх окна фазы. Числа
взяты из пресетов API standard_ranging_short_range, standard_ranging и
standard_ranging_long_range; окно и обёртка в миллиметрах — пересчёт через
198,4 мм на единицу фазы.
| Режим | Период A / B, такты PLL | A / B, нс | Окно фазы | Окно, мм | Обёртка A / B, мм | Таблица Zephyr: темно / ярко |
|---|---|---|---|---|---|---|
| Short | 16 / 12 | 21,19 / 15,89 | 7,0 | 1389 | 3175 / 2381 | 136 / 135 см |
| Medium | 24 / 20 | 31,78 / 26,48 | 15,0 | 2976 | 4762 / 3968 | 290 / 76 см |
| Long | 32 / 28 | 42,37 / 37,08 | 23,0 | 4564 | 6349 / 5556 | 360 / 73 см |
Комментарии API к пресетам называют окна 1,4, 3,0 и 4,6 м — совпадает с пересчётом. Таблица в правом столбце процитирована в драйвере Zephyr из описания датчика (не проверено): в темноте Short и Medium упираются в своё окно, Long — в сигнал.
/* Режимы — как пресеты API standard_ranging_short_range / standard_ranging / _long_range: периоды лазера A и B,
* верх окна фазы, окна и начальные фазы сигма-дельты. После смены периода таймауты пересчитываются. */
int l1_set_mode(l1_t *d, l1_dist_t m)
{
static const uint8_t va[3] = {0x07u, 0x0Bu, 0x0Fu}, vb[3] = {0x05u, 0x09u, 0x0Du};
static const uint8_t ph[3] = {0x38u, 0x78u, 0xB8u}, ip[3] = {6u, 10u, 14u};
uint8_t b[4];
if (m < L1_SHORT || m > L1_LONG)
return 1;
d->vcsel_a = va[m - 1];
d->vcsel_b = vb[m - 1];
if (l1_wr8(d, L1_VCSEL_A, d->vcsel_a) || l1_wr8(d, L1_VCSEL_B, d->vcsel_b) || l1_wr8(d, L1_PHASE_HIGH, ph[m - 1]))
return 2;
b[0] = d->vcsel_a;
b[1] = d->vcsel_b;
b[2] = ip[m - 1];
b[3] = ip[m - 1];
if (l1_wr(d, L1_WOI_SD0, b, 4)) /* 0x78…0x7B одной записью */
return 3;
return put_timeouts(d) ? 4 : 0;
}
Бюджет в регистры. Время датчик считает в макропериодах: 2304 периода лазера (окно фона 256 и окно дальности 2048). В Long макропериод тайминга A — 97,625 мкс, B — 85,421 мкс. Таймаут пишется в формате «(младший байт сдвинуть влево на старший) + 1». API для режима без шагов MM вычитает из бюджета 1000 мкс запаса и пишет остаток в оба тайминга; мы делаем то же.
/* ── время: те же формулы, что VL53L1_calc_macro_period_us и VL53L1_calc_encoded_timeout в API ── */
uint32_t l1_macro_q12(uint16_t osc, uint8_t vcsel_code)
{
uint32_t pll = (1u << 30) / osc; /* период PLL, мкс, 0.24 */
uint32_t m = (2304u * pll) >> 6; /* макропериод — 2304 периода лазера */
m *= (uint32_t)((vcsel_code + 1u) << 1); /* период лазера в тактах PLL */
return m >> 6; /* мкс, 12.12 */
}
uint16_t l1_encode_timeout(uint32_t us, uint32_t macro_q12)
{
uint32_t mclks = ((us << 12) + (macro_q12 >> 1)) / macro_q12;
uint32_t ls;
uint16_t ms = 0;
if (mclks == 0)
return 0;
ls = mclks - 1u; /* формат (LSB << MSB) + 1 */
while (ls & 0xFFFFFF00u) {
ls >>= 1;
ms++;
}
return (uint16_t)((ms << 8) | (ls & 0xFFu));
}
Что получилось в Long (частота 0xBCCC):
| Бюджет, мс | 0x5E (A) |
0x61 (B) |
Таймаут A, мкс | Таймаут B, мкс |
|---|---|---|---|---|
| 15 | 0x008E |
0x00A3 |
13 960 | 14 009 |
| 20 | 0x00C2 |
0x00DD |
19 037 | 18 964 |
| 33 | 0x01A3 |
0x01BB |
31 923 | 32 033 |
| 50 | 0x01FA |
0x028F |
48 910 | 48 946 |
| 100 | 0x02FD |
0x0390 |
98 894 | 98 491 |
| 200 | 0x03FE |
0x0491 |
198 471 | 198 263 |
| 500 | 0x059F |
0x05B6 |
496 811 | 497 580 |
Калибровка фазы — 1000 мкс, как в пресетах: в 0x4B уходит 10
макропериодов в Long, 14 в Medium, 20 в Short. Все 21 пара кодов
(три режима, семь бюджетов) и номера SPAD для всех 256 клеток стенд сверил с
функциями API, взятыми дословно (VL53L1_calc_macro_period_us,
VL53L1_calc_encoded_timeout, VL53L1_encode_row_col): совпало 21 из 21 и
256 из 256. Бюджет 14 и 501 мс наш драйвер не принимает — пределы ULD.
API пустил бы больше: остаток после запаса — до 550 000 мкс.
Подряд и по таймеру. 0x20 в 0x87 — измерения подряд, 0x40 — по
таймеру, 0x10 — одно. В модели измерения подряд шли с шагом
33 498 и 33 608 мкс при бюджете 33 мс (тайминги A и B немного разные), при 20 и
100 мс — 20 539 / 20 612 и 100 066 / 100 469 мкс. Период по таймеру — миллисекунды × число
тиков медленного генератора на миллисекунду из 0xDE (биты 9:0), так делает
VL53L1_set_inter_measurement_period_ms. API требует, чтобы период был не
меньше бюджета + 4 мс; наш драйвер тоже: 36 мс при бюджете 33 — отказ.
/* Период по таймеру: миллисекунды × тики медленного генератора (VL53L1_set_inter_measurement_period_ms). */
int l1_set_imp(l1_t *d, uint32_t ms)
{
if (ms < d->budget_us / 1000u + L1_IMP_GUARD_MS)
return 1;
return l1_wr32(d, L1_IMP, ms * d->osc_cal) ? 2 : 0;
}
В модели генератор даёт 40 тиков на миллисекунду (допущение), и 100 мс
записались как 0x00000FA0; результаты шли через 99 890 и 100 110 мкс.
100г.4 Шум и дальность: что даёт бюджет и режим (модель)
Как датчик считает σ, API не раскрывает: она приходит готовой в регистре
0x92. Поэтому в модели шум — своя формула: σ = c · 8 нс / 2 · √(Ns + W·Na) /
Ns, где Ns и Na — отсчёты сигнала и фона за таймаут, 8 нс — ширина импульса
из настроек API, W — период лазера к 16 нс (ширина окна фона там же).
Белая мишень на 3600 мм даёт на всей матрице ровно 1,0 MCPS — так модель
подогнана к 360 см из таблицы; сигнал падает как 1/d², а при коротком
периоде лазера импульсов больше и сигнала больше. Свыше 20 MCPS датчик
выключает часть SPAD (цель DSS из пресетов). Это допущения, не датчик.
По 100 измерений на точку, разброс годных и средняя σ, которую «датчик»
кладёт в 0x92:
| Бюджет | Long, белая 1 м | Long, серая 0,2 на 1,2 м | Short, серая 0,2 на 1,2 м |
|---|---|---|---|
| 15 мс | 2,65 (2,75) | 8,48 (7,25) | 4,44 (5,00) |
| 20 мс | 2,05 (2,38) | 6,49 (6,25) | 3,98 (4,25) |
| 33 мс | 1,88 (1,75) | 4,68 (4,88) | 3,16 (3,25) |
| 50 мс | 1,28 (1,50) | 3,71 (3,88) | 2,84 (2,62) |
| 100 мс | 1,01 (1,00) | 2,92 (2,75) | 1,95 (1,88) |
| 200 мс | 0,69 (0,75) | 1,81 (2,00) | 1,33 (1,38) |
| 500 мс | 0,53 (0,50) | 1,21 (1,25) | 0,95 (0,88) |
Вчетверо больше времени — примерно вдвое меньше разброс, как у L0X. В каждой сотне годных 99: первое измерение после пуска приходит со статусом 6 (100г.5).
Дальность белой мишени — наибольшая, где из 40 измерений не меньше 90 % годны и не дальше 100 мм от правды (бюджет 33 мс):
| Режим | Модель: темно | Таблица: темно | Модель: яркий свет | Таблица: ярко |
|---|---|---|---|---|
| Short | 1350 мм | 136 см | 1190 мм | 135 см |
| Medium | 2950 мм | 290 см | 870 мм | 76 см |
| Long | 3600 мм | 360 см | 690 мм | 73 см |
В темноте Short и Medium модель останавливает окно фазы — это не подгонка, а пересчёт из пресетов, и он лёг рядом с таблицей. Яркий свет в модели — 0,9 MCPS фона на SPAD, подобран так, чтобы Long кончался рядом с 73 см; Short на том же свету держит 1190 мм, Medium — 870. Порядок тот же, что в таблице, числа для Medium расходятся — модель грубее датчика. Вывод для практики из таблицы, а не из модели: на солнце Long не дальнобойнее Short.
100г.5 Статус измерения и обёртка фазы
Результат — 17 байт с 0x89: код датчика (биты 4:0), отчёт, счётчик
измерений, число SPAD (8.8), сигнал и фон (9.7, MCPS), σ (14.2, мм), фаза
(5.11), дальность в миллиметрах и сигнал после вычета помехи стекла. Код
датчика переводится в статус по правилам API (SetSimpleData и
ConvertStatusLite):
| Код датчика | Статус API | Что значит | Что делать |
|---|---|---|---|
| 9 | 0 | годно | брать |
| 9 при счётчике 0 | 6 | годно, но обёртка не проверена | не брать: сравнить не с чем |
| 6 | 1 | σ выше предела | выбросить |
| 4 | 2 | сигнал слабый, цели нет | «ничего не видно» |
| 5 | 4 | фаза вне окна | цель дальше окна: режим подлиннее |
| 7 | 7 | фазы A и B не сходятся — обёртка | выбросить, сменить режим |
| 8 | 3 | обрезано по минимальной дальности | по задаче |
| 12 | 9 | сигнал не выше помехи стекла | калибровать стекло |
| 1, 2, 3, 17 | 5 | аппаратный сбой | перезапустить |
/* Код датчика → статус API (SetSimpleData и ConvertStatusLite): первое измерение после пуска (счётчик 0)
* обёртку фазы не проверяло — статус 6, а не 0. */
static uint8_t api_status(uint8_t raw, uint8_t stream)
{
switch (raw) {
case 1: case 2: case 3: case 17: return 5; /* аппаратный сбой */
case 13: return 13; /* ROI вне матрицы */
case 18: return 10; /* синхронизация */
case 9: return stream == 0 ? 6 : 0; /* годно; без проверки обёртки — 6 */
case 6: return 1; /* σ выше предела */
case 4: return 2; /* сигнал слабый */
case 8: return 3; /* обрезано по минимальной дальности */
case 5: return 4; /* фаза вне окна */
case 7: return 7; /* фазы A и B не сходятся — обёртка */
case 12: return 9; /* сигнал не выше помехи стекла */
default: return 255;
}
}
API ещё правит отрицательную дальность: до −30 мм — ноль, ниже — статус 14.
Поправочный множитель API (2011/2048, LITE_RANGING_GAIN_FACTOR) мы не
применяем, как и OpenMV, — какой путь верен для живого датчика, не проверено.
Модель, по 50 измерений подряд:
| Сцена | Режим | Статусы | Первые дальности, мм |
|---|---|---|---|
| пусто | Long | 2 — все | 166, 192, 700, 835 |
| белая на 5000 мм | Long | 2 — все | 5014, 5001, 5012, 5009 |
| белая на 2000 мм | Short | 4 — все | 2004, 2009, 2011, 2009 |
| белая на 3300 мм | Short | 6 — первое, 7 — остальные | 134, 928, 139, 927 |
| отражение 3,0 (втрое ярче белой) на 5000 мм | Medium | 6 — первое, 7 — остальные | 238, 1035, 244, 1041 |
Обёртка фазы — главная ловушка L1X. Белая стена на 3300 мм в Short: это больше обёртки тайминга A (3175 мм), и первое измерение показало 134 мм. Сравнить его было не с чем — счётчик 0, и код 9 стал статусом 6, а не 0. Второе измерение (тайминг B, обёртка 2381 мм) дало 928 мм, фазы разошлись — статус 7, и так до конца. Фильтр, который берёт только статус 0, этой ошибки не пропускает; фильтр «всё, кроме явных отказов» — пропустит 134 мм вместо 3,3 м. Светлые дальние цели за окном опасны именно тем, что сигнала у них хватает.
100г.6 Зона интереса, прерывание по окну, стекло и калибровки
ROI. Центр — номер SPAD в 0x7F, размер — в 0x80: старшая тетрада —
высота − 1, младшая — ширина − 1. Номер SPAD нумеруется не по строкам, а
по правилу VL53L1_encode_row_col; по умолчанию центр — SPAD 199, клетка
(8, 8). API не принимает зону уже 4 SPAD (ширина − 1 меньше 3).
/* ── ROI: номер SPAD центра — как VL53L1_encode_row_col (y — строка, x — столбец) ── */
uint8_t l1_spad(uint8_t x, uint8_t y)
{
if (y > 7u)
return (uint8_t)(128u + (x << 3) + (15u - y));
return (uint8_t)(((15u - x) << 3) + y);
}
/* Прямоугольник w×h SPAD с левым нижним углом (x0, y0); центр — как в VL53L1_SetUserROI. */
int l1_set_roi(l1_t *d, uint8_t x0, uint8_t y0, uint8_t w, uint8_t h)
{
uint8_t b[2];
if (w < 4u || h < 4u || x0 + w > 16u || y0 + h > 16u)
return 1;
b[0] = l1_spad((uint8_t)(x0 + w / 2u), (uint8_t)(y0 + h / 2u));
b[1] = (uint8_t)(((h - 1u) << 4) | (w - 1u));
return l1_wr(d, L1_ROI_CENTRE, b, 2) ? 2 : 0; /* 0x7F и 0x80 подряд */
}
Узкая зона — узкий взгляд и меньше сигнала. Модель (поле зрения делится поровну, 27° по описанию — не проверено), стена в 1500 мм и коробка 4×4 SPAD на 600 мм в центре:
| ROI | Угол | SPAD | Сигнал на 1 м, MCPS | Дальность белой | Коробка: показано |
|---|---|---|---|---|---|
| 16×16 | 27° | 256 | 13,89 | 3600 мм | 1245 мм |
| 8×8 | 13,5° | 64 | 3,47 | 1800 мм | 902 мм |
| 4×4 | 6,75° | 16 | 0,87 | 900 мм | 610 мм |
Вся матрица видит коробку и стену разом и показывает среднее с весом сигнала — 1245 мм, которых нет ни там, ни там. Зона 4×4 видит коробку (610 мм с нашим сдвигом модели +9 мм), но белую стену — только до 900 мм: сигнал в 16 раз слабее.
«Сканирование». Шестнадцать зон 4×4 по очереди дают грубую картинку 4×4. ROI следующей зоны пишем, пока датчик ждёт снятия прерывания, — новое измерение начнётся уже с ней.
/* «Сканирование» 16 зонами 4×4 SPAD, построчно сверху вниз: ROI следующей зоны пишется, пока датчик ждёт
* снятия прерывания, — следующее измерение начнётся уже с ней. */
int l1_scan(l1_t *d, int16_t mm[16], uint8_t st[16])
{
l1_meas_t m;
if (l1_set_roi(d, 0, 12, 4, 4) || l1_start(d, 0x20u))
return 1;
for (uint8_t z = 0; z < 16u; z++) {
uint8_t n = (uint8_t)((z + 1u) & 15u);
if (wait_irq(d) || l1_read(d, &m))
return 2;
mm[z] = m.mm;
st[z] = m.status;
if (l1_set_roi(d, (uint8_t)(4u * (n & 3u)), (uint8_t)(12u - 4u * (n >> 2)), 4, 4) || l1_clear(d))
return 3;
}
return l1_stop(d) ? 4 : 0;
}
Стена на 800 мм, коробка на 400 мм в левой верхней четверти, Short, 33 мс (дальность / статус):
| столбец 0 | 1 | 2 | 3 | |
|---|---|---|---|---|
| строка 0 | 412 / 6 | 409 / 0 | 810 / 0 | 813 / 0 |
| строка 1 | 409 / 0 | 408 / 0 | 811 / 0 | 808 / 0 |
| строка 2 | 811 / 0 | 814 / 0 | 802 / 0 | 807 / 0 |
| строка 3 | 808 / 0 | 805 / 0 | 806 / 0 | 809 / 0 |
Кадр — 538,1 мс, транзакций — 51. Первая зона пришла со статусом 6 — первое измерение после пуска. В модели проверка обёртки сравнивает только измерения с одной ROI; как живой датчик сверяет фазы при смене ROI на ходу — не проверено.
Прерывание по окну. Регистр 0x46: биты 1:0 — условие (0 — ниже нижнего
порога, 1 — выше верхнего, 2 — вне окна, 3 — внутри), бит 5 — «на каждое
измерение»; пороги в миллиметрах — 0x72 (верхний) и 0x74 (нижний).
/* Прерывание по окну дальности или на каждое измерение. Пороги — в 0x72 (верхний) и 0x74 (нижний). */
int l1_set_window(l1_t *d, l1_win_t win, uint16_t low_mm, uint16_t high_mm)
{
uint8_t b[4] = {(uint8_t)(high_mm >> 8), (uint8_t)high_mm, (uint8_t)(low_mm >> 8), (uint8_t)low_mm};
if (win != L1_EVERY && l1_wr(d, L1_THRESH_HIGH, b, 4))
return 1;
return l1_wr8(d, L1_INT_CONFIG, (uint8_t)win) ? 2 : 0;
}
Человек подходит от 1500 до 300 мм за 5 с и уходит обратно, измерение по таймеру раз в 100 мс, 10,4 с:
| Прерывание | Прерываний | Транзакций | Шина, мкс |
|---|---|---|---|
| на каждое измерение | 104 | 208 | 59 800 |
| ниже 500 мм | 15 | 30 | 8625 |
| внутри 400…700 мм | 26 | 52 | 14 950 |
Процессор спит, пока ничего не случилось; по окну «ниже 500 мм» шина занята в 6,9 раза меньше. Условие проверяется на каждом измерении: пока человек внутри окна, прерывание приходит каждые 100 мс, а не один раз на входе.
Стекло. Крышка отражает часть импульса в приёмник; датчик смешивает это отражение с целью, и дальность тянет к нулю — тем сильнее, чем слабее цель. В модели стекло — 0,01 MCPS на SPAD. Две калибровки по образцу API:
- Смещение (
VL53L1_PerformOffsetSimpleCalibration): мишень на известной дальности, первое измерение выбросить, из 50 годных — среднее; поправка = дальность − среднее. Пишем её в0x22и, × 4, в0x1E. - Помеха стекла (
SingleTargetXTalkCalibration): мишень на 600 мм (в API по умолчанию), 50 измерений без компенсации; помеха на SPAD = сигнал / число SPAD × (1 − измерено / 600). В0x16— кило-отсчёты в формате 7.9.
/* Калибровка помехи стекла — как SingleTargetXTalkCalibration в API: мишень на известной дальности, n измерений
* без компенсации; помеха на SPAD = сигнал / SPAD · (1 − измерено / мишень). В 0x16 — кило-отсчёты в 7.9. */
int l1_cal_xtalk(l1_t *d, uint16_t target_mm, uint8_t n, uint16_t *kcps)
{
uint32_t sum_mm = 0, sum_sig = 0, sum_sp = 0, got = 0, sig, sp, mm;
l1_meas_t m;
if (l1_wr16(d, L1_XTALK_KCPS, 0) || l1_wr32(d, L1_XTALK_X_GRAD, 0))
return 1;
if (l1_start(d, 0x20u) || wait_irq(d) || l1_clear(d)) /* первое измерение не в счёт */
return 2;
for (uint8_t i = 0; i < n; i++) {
if (wait_irq(d) || l1_read(d, &m) || l1_clear(d))
return 3;
if (m.status == 0 && m.mm > 0) {
sum_mm += (uint32_t)m.mm;
sum_sig += m.signal;
sum_sp += m.spads >> 8;
got++;
}
}
l1_stop(d);
if (!got)
return 4;
sig = sum_sig / got;
sp = (sum_sp + got / 2u) / got;
mm = (sum_mm + got / 2u) / got;
*kcps = 0;
if (sp && mm < target_mm) /* MCPS 9.7 → кило-отсчёты 7.9: · 1000 · 512 / 128 */
*kcps = (uint16_t)((sig * 4000u / sp) * (target_mm - mm) / target_mm);
return l1_wr16(d, L1_XTALK_KCPS, *kcps) ? 5 : 0;
}
Средняя ошибка годных, мм (по 30 измерений, Long, 33 мс):
| 150 мм | 300 мм | 600 мм | 1000 мм | 2000 мм | без цели | |
|---|---|---|---|---|---|---|
| без калибровок | +9 | +4 | −28 | −147 | все — статус 7 | статус 0 |
| после смещения | 0 | −5 | −38 | −156 | все — статус 7 | статус 0 |
| после помехи стекла | 0 | −1 | 0 | −3 | −20 | статус 2 |
Калибровка смещения на 100 мм нашла −9 мм — свой сдвиг модели — за 108 транзакций и 1711,5 мс. Калибровка стекла нашла 0,00987 MCPS на SPAD при заданных 0,01 (код 5051) за 1711,3 мс. Без неё дальняя цель занижена на сотни миллиметров, на 2000 мм тайминги A и B расходятся и все измерения — статус 7, а без цели датчик «видит» стекло в нескольких миллиметрах со статусом 0. После — без цели статус 2, как и должно быть. Порядок — сначала смещение, потом стекло, со стеклом на месте.
100г.7 Грабли
Прерывание пришло один раз, новых данных нет. Не сняли прерывание записью
0x01 в 0x86. В модели следующее измерение ждёт снятия: за секунду — одно
прерывание, счётчик 0, GPIO1 стоит внизу; после снятия счётчик пошёл 1…5.
ID читается как 0x0000. Адрес регистра отправлен младшим байтом вперёд —
ушло в 0x0F01. Два байта адреса, старший первым.
Стена в 3,3 м показана в 134 мм. Обёртка фазы в Short, а фильтр взял статус 6. Годно только 0; для дальних светлых целей — Medium или Long.
На солнце Long видит хуже Short. По таблице из описания — 73 см против 135; в модели — 690 против 1190 мм. На свету — Short.
Через стекло дальность занижена, а без цели датчик «видит» что-то у самого носа. Нет калибровки помехи стекла. Смещение, затем помеха, со стеклом на месте.
GPIO1 не даёт спада. Блок по умолчанию оставляет 0x30 = 0x01 —
активна единица; EXTI по спаду с подтяжкой ждёт активного нуля. Писать 0x11.
Измерения начались сами сразу после загрузки блока. В блоке OpenMV
последний байт — 0x40 в 0x87. У нас — 0x00.
Первое измерение после пуска «не годится». Статус 6: обёртка не проверена. Это нормально; его и калибровки API выбрасывают.
Период по таймеру не ставится. Меньше бюджета + 4 мс API и наш драйвер не принимают.
Узкая ROI «не видит» дальнюю стену. 4×4 SPAD — в 16 раз меньше сигнала: в модели белая стена — до 900 мм вместо 3600.
Второй датчик на чужом адресе. XSHUT отпущены разом — запись в 0x0001
получили все.
100г.8 Итоги
- VL53L1X — тот же адрес
0x29, XSHUT и GPIO1, что у L0X, но 16-битные адреса регистров, матрица 16×16 SPAD с зоной интереса и два тайминга A и B. Слово ID —0xEACCпо0x010F. - Свой драйвер по регистрам вместо API: блок из 91 байта в
0x2D…0x87, включение одного датчика — 13 транзакций; в прошивке на три датчика драйвер — 1714 байт кода, весь образ — 3192 байт кода, 3536 флеша и 84 байта ОЗУ (у L0X с библиотекой — 12 988 байт кода). - Единица фазы — 198,4 мм; окна Short, Medium, Long — 1389, 2976 и 4564 мм, обёртка тайминга A в Short — 3175 мм.
- Бюджет − 1000 мкс уходит в таймауты A и B; коды совпали с функциями API во всех 21 случае. Период по таймеру — не меньше бюджета + 4 мс.
- Разброс в модели: Long, белая на 1 м — 2,65 мм за 15 мс и 0,53 мм за 500 мс.
- Годно только 0. Статус 6 — первое после пуска, 7 — обёртка, 4 — цель за окном, 2 — сигнала нет.
- ROI 4×4 — 6,75° и в 16 раз меньше сигнала; 16 зон дают картинку 4×4 за 538,1 мс.
- Прерывание по окну вместо «каждое измерение»: 15 прерываний вместо 104 за 10,4 с.
- Стекло: калибровка смещения, затем помехи — ошибка на 1000 мм с −147 до −3 мм в модели.
Полные исходники
Драйвер по регистрам и прошивка на три датчика. Стенд host100e.c, модель
model100e.py, сверка с API ref100e.c и выдержка функций API st_ref.c —
в _verify/tof100e/.
tof100e.h — регистры, константы, шина и состояние драйвера.
/* Раздел 100г: VL53L1X прямо по регистрам — без библиотеки ST. Адреса регистров и форматы — из карты регистров
* API ST VL53L1X 2.4.5 (vl53l1_register_map.h, модуль hal_st Zephyr); блок настроек по умолчанию — из открытой
* библиотеки vl53l1x (набор скриптов OpenMV), его комментарии ссылаются на функции ULD. Шину и выводы даёт
* прошивка (fw100e.c — I2C1 STM32F103) или стенд (host100e.c — строки к модели model100e.py). */
#ifndef TOF100E_H
#define TOF100E_H
#include <stdint.h>
/* ── адреса регистров: 16 бит, на шину — старшим байтом вперёд ── */
#define L1_SOFT_RESET 0x0000u
#define L1_I2C_ADDR 0x0001u /* [6:0] — семибитный адрес */
#define L1_OSC_FREQ 0x0006u /* частота быстрого генератора, 4.12, МГц — из заводской памяти */
#define L1_XTALK_KCPS 0x0016u /* помеха стекла на один SPAD, кило-отсчётов/с, 7.9 */
#define L1_XTALK_X_GRAD 0x0018u
#define L1_XTALK_Y_GRAD 0x001Au
#define L1_OFFSET_MM 0x001Eu /* смещение дальности, мм, 11.2 */
#define L1_INNER_OFFSET_MM 0x0020u
#define L1_OUTER_OFFSET_MM 0x0022u
#define L1_GPIO_MUX 0x0030u /* бит 4: 1 — GPIO1 активен нулём (так в пресетах API) */
#define L1_GPIO_STATUS 0x0031u /* бит 0 — уровень GPIO1 */
#define L1_INT_CONFIG 0x0046u /* [1:0] — окно по дальности, бит 5 — «новое измерение» */
#define L1_PHASECAL_TIMEOUT 0x004Bu
#define L1_TIMEOUT_A 0x005Eu /* таймаут дальнометрии, тайминг A: (LSB << MSB) + 1 макропериодов */
#define L1_VCSEL_A 0x0060u /* период лазера A: (код + 1) · 2 такта PLL */
#define L1_TIMEOUT_B 0x0061u
#define L1_VCSEL_B 0x0063u
#define L1_SIGMA_THRESH 0x0064u /* предел σ, мм, 14.2 */
#define L1_MIN_RATE 0x0066u /* предел сигнала, MCPS, 9.7 */
#define L1_PHASE_LOW 0x0068u /* окно фазы, 5.3 */
#define L1_PHASE_HIGH 0x0069u
#define L1_IMP 0x006Cu /* период между измерениями: мс · OSC_CAL, 32 бита */
#define L1_THRESH_HIGH 0x0072u /* пороги прерывания по дальности, мм */
#define L1_THRESH_LOW 0x0074u
#define L1_WOI_SD0 0x0078u
#define L1_WOI_SD1 0x0079u
#define L1_INIT_PHASE_SD0 0x007Au
#define L1_INIT_PHASE_SD1 0x007Bu
#define L1_ROI_CENTRE 0x007Fu /* номер SPAD центра ROI */
#define L1_ROI_SIZE 0x0080u /* старшая тетрада — высота − 1, младшая — ширина − 1 */
#define L1_INT_CLEAR 0x0086u
#define L1_MODE_START 0x0087u /* 0x10 — однократно, 0x20 — подряд, 0x40 — по таймеру, 0 — стоп */
#define L1_RESULT 0x0089u /* 17 байт: статус, отчёт, счётчик, SPAD, сигнал, фон, σ, фаза, дальность… */
#define L1_OSC_CAL 0x00DEu /* [9:0] — тиков медленного генератора на миллисекунду */
#define L1_FW_STATUS 0x00E5u /* бит 0 — прошивка датчика загрузилась */
#define L1_MODEL_ID 0x010Fu /* слово 0xEACC: модель 0xEA и тип модуля 0xCC */
#define L1_ADDR_DEFAULT 0x29u
#define L1_ID 0xEACCu
#define L1_CFG_FIRST 0x002Du /* блок по умолчанию: 0x2D…0x87 */
#define L1_CFG_LEN 91u
#define L1_BOOT_POLL_US 200u /* опрос загрузки — наше значение */
#define L1_BOOT_TRIES 20u
#define L1_GUARD_US 1000u /* запас API для LITE без MM1/MM2: бюджет − 1000 мкс = таймаут дальнометрии */
#define L1_TB_MIN_MS 15u /* пределы бюджета по ULD — по документации производителя, не проверено */
#define L1_TB_MAX_MS 500u
#define L1_IMP_GUARD_MS 4u /* API: период по таймеру не меньше бюджета + 4 мс */
#define L1_PHASECAL_US 1000u /* таймаут калибровки фазы в пресетах API */
/* ── шина и выводы: реализует прошивка или стенд ── */
int l1_i2c_write(uint8_t addr7, const uint8_t *buf, uint32_t n); /* 0 — всё подтверждено */
int l1_i2c_wr_rd(uint8_t addr7, const uint8_t *wr, uint32_t nw, uint8_t *rd, uint32_t nr);
void l1_delay_us(uint32_t us);
void l1_xshut(uint8_t idx, int run); /* run = 1 — отпустить XSHUT, 0 — прижать к земле */
int l1_idle(void); /* ждать прерывания: прошивка — WFI, стенд — до фронта GPIO1; 0 — не дождались */
typedef enum { L1_SHORT = 1, L1_MEDIUM = 2, L1_LONG = 3 } l1_dist_t; /* нумерация как у API и Zephyr */
typedef enum { L1_BELOW = 0, L1_ABOVE = 1, L1_OUTSIDE = 2, L1_INSIDE = 3, L1_EVERY = 0x20 } l1_win_t;
typedef struct {
int16_t mm; /* дальность из 0x96 — без поправочного множителя API */
uint8_t raw; /* код датчика, биты 4:0 байта 0x89 */
uint8_t status; /* статус по правилам API: 0 годно, 1 σ, 2 сигнал, 4 вне окна, 6 без проверки обёртки… */
uint8_t stream; /* счётчик измерений 0x8B */
uint16_t signal; /* MCPS, 9.7 */
uint16_t ambient; /* MCPS, 9.7 */
uint16_t sigma; /* мм, 14.2 */
uint16_t spads; /* число SPAD, 8.8 */
} l1_meas_t;
typedef struct {
uint8_t addr; /* адрес на шине, 7 бит */
uint8_t idx; /* номер вывода XSHUT и GPIO1 */
uint16_t osc; /* 0x0006: быстрый генератор, 4.12 */
uint16_t osc_cal; /* 0x00DE & 0x3FF */
uint8_t vcsel_a, vcsel_b;
uint32_t budget_us;
volatile uint8_t irq; /* GPIO1 сработал — ставит обработчик EXTI */
uint32_t xfers, bytes; /* транзакции и байты на шине */
} l1_t;
extern const uint8_t L1_DEFAULT_CFG[L1_CFG_LEN];
int l1_rd(l1_t *d, uint16_t reg, uint8_t *p, uint32_t n);
int l1_wr(l1_t *d, uint16_t reg, const uint8_t *p, uint32_t n);
int l1_rd16(l1_t *d, uint16_t reg, uint16_t *v);
int l1_wr8(l1_t *d, uint16_t reg, uint8_t v);
int l1_wr16(l1_t *d, uint16_t reg, uint16_t v);
int l1_wr32(l1_t *d, uint16_t reg, uint32_t v);
void l1_sleep_all(uint8_t n);
int l1_boot(l1_t *d, uint8_t idx, uint8_t addr7);
int l1_init(l1_t *d);
uint32_t l1_macro_q12(uint16_t osc, uint8_t vcsel_code);
uint16_t l1_encode_timeout(uint32_t us, uint32_t macro_q12);
int l1_set_mode(l1_t *d, l1_dist_t m);
int l1_set_budget(l1_t *d, uint32_t ms);
int l1_set_imp(l1_t *d, uint32_t ms);
uint8_t l1_spad(uint8_t x, uint8_t y);
int l1_set_roi(l1_t *d, uint8_t w, uint8_t h, uint8_t x, uint8_t y);
int l1_set_window(l1_t *d, l1_win_t win, uint16_t low_mm, uint16_t high_mm);
int l1_start(l1_t *d, uint8_t mode);
int l1_stop(l1_t *d);
int l1_clear(l1_t *d);
int l1_read(l1_t *d, l1_meas_t *m);
int l1_cal_offset(l1_t *d, uint16_t target_mm, uint8_t n, int16_t *ofs);
int l1_cal_xtalk(l1_t *d, uint16_t target_mm, uint8_t n, uint16_t *kcps);
int l1_scan(l1_t *d, int16_t mm[16], uint8_t st[16]);
#endif
tof100e.c — драйвер: блок по умолчанию, включение, режимы и бюджет, ROI, окно, статусы, калибровки, сканирование.
/* Раздел 100г: драйвер VL53L1X по регистрам. Общий для прошивки и стенда. */
#include <string.h>
#include "tof100e.h"
/* Блок по умолчанию для 0x2D…0x87 — как в открытой библиотеке vl53l1x (OpenMV), кроме последнего байта:
* там 0x40 (измерения стартуют сразу после загрузки), у нас 0x00 — стартуем сами. */
const uint8_t L1_DEFAULT_CFG[L1_CFG_LEN] = {
0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x02, 0x08, 0x00, 0x08, 0x10, 0x01, 0x01, 0x00, 0x00, 0x00, /* 0x2D */
0x00, 0xFF, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0B, 0x00, 0x00, 0x02, 0x0A, 0x21, /* 0x3D */
0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x00, 0x38, 0xFF, 0x01, 0x00, 0x08, 0x00, /* 0x4D */
0x00, 0x01, 0xDB, 0x0F, 0x01, 0xF1, 0x0D, 0x01, 0x68, 0x00, 0x80, 0x08, 0xB8, 0x00, 0x00, 0x00, /* 0x5D */
0x00, 0x0F, 0x89, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x0F, 0x0D, 0x0E, 0x0E, 0x00, /* 0x6D */
0x00, 0x02, 0xC7, 0xFF, 0x9B, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, /* 0x7D */
};
/* ── обращение к регистру: два байта адреса старшим вперёд, затем данные ── */
#define L1_MAX_WR 96u
int l1_wr(l1_t *d, uint16_t reg, const uint8_t *p, uint32_t n)
{
uint8_t b[2u + L1_MAX_WR];
if (n > L1_MAX_WR)
return 1;
b[0] = (uint8_t)(reg >> 8);
b[1] = (uint8_t)reg;
memcpy(b + 2, p, n);
d->xfers++;
d->bytes += n + 2u;
return l1_i2c_write(d->addr, b, n + 2u);
}
int l1_rd(l1_t *d, uint16_t reg, uint8_t *p, uint32_t n)
{
uint8_t a[2] = {(uint8_t)(reg >> 8), (uint8_t)reg};
d->xfers++;
d->bytes += n + 2u;
return l1_i2c_wr_rd(d->addr, a, 2, p, n);
}
int l1_rd16(l1_t *d, uint16_t reg, uint16_t *v)
{
uint8_t b[2] = {0, 0};
int e = l1_rd(d, reg, b, 2);
*v = (uint16_t)((b[0] << 8) | b[1]); /* данные тоже старшим байтом вперёд */
return e;
}
int l1_wr8(l1_t *d, uint16_t reg, uint8_t v)
{
return l1_wr(d, reg, &v, 1);
}
int l1_wr16(l1_t *d, uint16_t reg, uint16_t v)
{
uint8_t b[2] = {(uint8_t)(v >> 8), (uint8_t)v};
return l1_wr(d, reg, b, 2);
}
int l1_wr32(l1_t *d, uint16_t reg, uint32_t v)
{
uint8_t b[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v};
return l1_wr(d, reg, b, 4);
}
/* ── включение по одному и смена адреса ── */
void l1_sleep_all(uint8_t n)
{
for (uint8_t i = 0; i < n; i++)
l1_xshut(i, 0);
l1_delay_us(L1_BOOT_POLL_US);
}
/* Отпустить XSHUT, дождаться загрузки (бит 0 регистра 0xE5), проверить слово 0xEACC и перевести датчик на свой
* адрес. Пока датчик грузится, он не отвечает — это тоже «ещё не готов». */
int l1_boot(l1_t *d, uint8_t idx, uint8_t addr7)
{
uint8_t st = 0;
uint16_t id = 0;
uint32_t k;
memset(d, 0, sizeof *d);
d->idx = idx;
d->addr = L1_ADDR_DEFAULT;
l1_xshut(idx, 1);
for (k = 0; k < L1_BOOT_TRIES; k++) {
l1_delay_us(L1_BOOT_POLL_US);
if (l1_rd(d, L1_FW_STATUS, &st, 1) == 0 && (st & 1u))
break;
}
if (k == L1_BOOT_TRIES)
return 1;
if (l1_rd16(d, L1_MODEL_ID, &id) || id != L1_ID)
return 2;
if (addr7 != L1_ADDR_DEFAULT) {
if (l1_wr8(d, L1_I2C_ADDR, addr7)) /* семибитный адрес; API делит 8-битный на 2 сам */
return 3;
d->addr = addr7;
id = 0;
if (l1_rd16(d, L1_MODEL_ID, &id) || id != L1_ID)
return 4;
}
return 0;
}
/* Загрузить блок по умолчанию одной записью, перенести заводское смещение, сделать GPIO1 активным нулём,
* прочитать частоты генераторов. После этого — режим Long, бюджет 33 мс, ROI 16×16. */
int l1_init(l1_t *d)
{
uint16_t v = 0;
if (l1_wr(d, L1_CFG_FIRST, L1_DEFAULT_CFG, L1_CFG_LEN))
return 10;
if (l1_rd16(d, L1_OUTER_OFFSET_MM, &v) || l1_wr16(d, L1_OFFSET_MM, (uint16_t)(v * 4u))) /* как в OpenMV */
return 11;
if (l1_wr8(d, L1_GPIO_MUX, 0x11u)) /* в блоке 0x01 — активна единица; пресеты API: 0x11 */
return 12;
if (l1_rd16(d, L1_OSC_FREQ, &d->osc) || d->osc == 0)
return 13;
if (l1_rd16(d, L1_OSC_CAL, &v))
return 14;
d->osc_cal = v & 0x3FFu;
d->budget_us = 33000u;
if (l1_set_mode(d, L1_LONG))
return 15;
return 0;
}
/* ── время: те же формулы, что VL53L1_calc_macro_period_us и VL53L1_calc_encoded_timeout в API ── */
uint32_t l1_macro_q12(uint16_t osc, uint8_t vcsel_code)
{
uint32_t pll = (1u << 30) / osc; /* период PLL, мкс, 0.24 */
uint32_t m = (2304u * pll) >> 6; /* макропериод — 2304 периода лазера */
m *= (uint32_t)((vcsel_code + 1u) << 1); /* период лазера в тактах PLL */
return m >> 6; /* мкс, 12.12 */
}
uint16_t l1_encode_timeout(uint32_t us, uint32_t macro_q12)
{
uint32_t mclks = ((us << 12) + (macro_q12 >> 1)) / macro_q12;
uint32_t ls;
uint16_t ms = 0;
if (mclks == 0)
return 0;
ls = mclks - 1u; /* формат (LSB << MSB) + 1 */
while (ls & 0xFFFFFF00u) {
ls >>= 1;
ms++;
}
return (uint16_t)((ms << 8) | (ls & 0xFFu));
}
static int put_timeouts(l1_t *d)
{
uint32_t r = d->budget_us - L1_GUARD_US;
uint32_t pc = ((L1_PHASECAL_US << 12) + (l1_macro_q12(d->osc, d->vcsel_a) >> 1)) / l1_macro_q12(d->osc, d->vcsel_a);
if (l1_wr8(d, L1_PHASECAL_TIMEOUT, (uint8_t)(pc > 0xFFu ? 0xFFu : pc)))
return 1;
if (l1_wr16(d, L1_TIMEOUT_A, l1_encode_timeout(r, l1_macro_q12(d->osc, d->vcsel_a))))
return 1;
return l1_wr16(d, L1_TIMEOUT_B, l1_encode_timeout(r, l1_macro_q12(d->osc, d->vcsel_b)));
}
/* Режимы — как пресеты API standard_ranging_short_range / standard_ranging / _long_range: периоды лазера A и B,
* верх окна фазы, окна и начальные фазы сигма-дельты. После смены периода таймауты пересчитываются. */
int l1_set_mode(l1_t *d, l1_dist_t m)
{
static const uint8_t va[3] = {0x07u, 0x0Bu, 0x0Fu}, vb[3] = {0x05u, 0x09u, 0x0Du};
static const uint8_t ph[3] = {0x38u, 0x78u, 0xB8u}, ip[3] = {6u, 10u, 14u};
uint8_t b[4];
if (m < L1_SHORT || m > L1_LONG)
return 1;
d->vcsel_a = va[m - 1];
d->vcsel_b = vb[m - 1];
if (l1_wr8(d, L1_VCSEL_A, d->vcsel_a) || l1_wr8(d, L1_VCSEL_B, d->vcsel_b) || l1_wr8(d, L1_PHASE_HIGH, ph[m - 1]))
return 2;
b[0] = d->vcsel_a;
b[1] = d->vcsel_b;
b[2] = ip[m - 1];
b[3] = ip[m - 1];
if (l1_wr(d, L1_WOI_SD0, b, 4)) /* 0x78…0x7B одной записью */
return 3;
return put_timeouts(d) ? 4 : 0;
}
int l1_set_budget(l1_t *d, uint32_t ms)
{
if (ms < L1_TB_MIN_MS || ms > L1_TB_MAX_MS)
return 1;
d->budget_us = ms * 1000u;
return put_timeouts(d) ? 2 : 0;
}
/* Период по таймеру: миллисекунды × тики медленного генератора (VL53L1_set_inter_measurement_period_ms). */
int l1_set_imp(l1_t *d, uint32_t ms)
{
if (ms < d->budget_us / 1000u + L1_IMP_GUARD_MS)
return 1;
return l1_wr32(d, L1_IMP, ms * d->osc_cal) ? 2 : 0;
}
/* ── ROI: номер SPAD центра — как VL53L1_encode_row_col (y — строка, x — столбец) ── */
uint8_t l1_spad(uint8_t x, uint8_t y)
{
if (y > 7u)
return (uint8_t)(128u + (x << 3) + (15u - y));
return (uint8_t)(((15u - x) << 3) + y);
}
/* Прямоугольник w×h SPAD с левым нижним углом (x0, y0); центр — как в VL53L1_SetUserROI. */
int l1_set_roi(l1_t *d, uint8_t x0, uint8_t y0, uint8_t w, uint8_t h)
{
uint8_t b[2];
if (w < 4u || h < 4u || x0 + w > 16u || y0 + h > 16u)
return 1;
b[0] = l1_spad((uint8_t)(x0 + w / 2u), (uint8_t)(y0 + h / 2u));
b[1] = (uint8_t)(((h - 1u) << 4) | (w - 1u));
return l1_wr(d, L1_ROI_CENTRE, b, 2) ? 2 : 0; /* 0x7F и 0x80 подряд */
}
/* Прерывание по окну дальности или на каждое измерение. Пороги — в 0x72 (верхний) и 0x74 (нижний). */
int l1_set_window(l1_t *d, l1_win_t win, uint16_t low_mm, uint16_t high_mm)
{
uint8_t b[4] = {(uint8_t)(high_mm >> 8), (uint8_t)high_mm, (uint8_t)(low_mm >> 8), (uint8_t)low_mm};
if (win != L1_EVERY && l1_wr(d, L1_THRESH_HIGH, b, 4))
return 1;
return l1_wr8(d, L1_INT_CONFIG, (uint8_t)win) ? 2 : 0;
}
/* ── измерения ── */
static int wait_irq(l1_t *d)
{
while (!d->irq)
if (!l1_idle())
return 1;
d->irq = 0;
return 0;
}
int l1_start(l1_t *d, uint8_t mode)
{
uint8_t b[2] = {0x01u, mode}; /* 0x86 — снять прерывание, 0x87 — пуск, одной записью */
d->irq = 0;
return l1_wr(d, L1_INT_CLEAR, b, 2);
}
int l1_stop(l1_t *d)
{
return l1_wr8(d, L1_MODE_START, 0x80u); /* ABORT — так останавливает API */
}
int l1_clear(l1_t *d)
{
return l1_wr8(d, L1_INT_CLEAR, 0x01u);
}
/* Код датчика → статус API (SetSimpleData и ConvertStatusLite): первое измерение после пуска (счётчик 0)
* обёртку фазы не проверяло — статус 6, а не 0. */
static uint8_t api_status(uint8_t raw, uint8_t stream)
{
switch (raw) {
case 1: case 2: case 3: case 17: return 5; /* аппаратный сбой */
case 13: return 13; /* ROI вне матрицы */
case 18: return 10; /* синхронизация */
case 9: return stream == 0 ? 6 : 0; /* годно; без проверки обёртки — 6 */
case 6: return 1; /* σ выше предела */
case 4: return 2; /* сигнал слабый */
case 8: return 3; /* обрезано по минимальной дальности */
case 5: return 4; /* фаза вне окна */
case 7: return 7; /* фазы A и B не сходятся — обёртка */
case 12: return 9; /* сигнал не выше помехи стекла */
default: return 255;
}
}
int l1_read(l1_t *d, l1_meas_t *m)
{
uint8_t r[17];
if (l1_rd(d, L1_RESULT, r, sizeof r))
return 1;
m->raw = r[0] & 0x1Fu;
m->stream = r[2];
m->status = api_status(m->raw, m->stream);
m->spads = (uint16_t)((r[3] << 8) | r[4]);
m->ambient = (uint16_t)((r[7] << 8) | r[8]);
m->sigma = (uint16_t)((r[9] << 8) | r[10]);
m->mm = (int16_t)((r[13] << 8) | r[14]);
m->signal = (uint16_t)((r[15] << 8) | r[16]); /* сигнал после вычета помехи стекла */
if (m->status == 0 && m->mm < -30) /* как API: ниже −30 мм — недостоверно, до −30 — ноль */
m->status = 14;
else if (m->status == 0 && m->mm < 0)
m->mm = 0;
return 0;
}
/* Калибровка смещения — как VL53L1_PerformOffsetSimpleCalibration: мишень на известной дальности, первое
* измерение выбросить, из годных взять среднее; смещение = дальность мишени − среднее. */
int l1_cal_offset(l1_t *d, uint16_t target_mm, uint8_t n, int16_t *ofs)
{
int32_t sum = 0;
uint32_t got = 0, tries = 0;
l1_meas_t m;
if (l1_wr16(d, L1_OFFSET_MM, 0) || l1_wr16(d, L1_INNER_OFFSET_MM, 0) || l1_wr16(d, L1_OUTER_OFFSET_MM, 0))
return 1;
if (l1_start(d, 0x20u) || wait_irq(d) || l1_clear(d)) /* первое прерывание — мимо */
return 2;
while (got < n && tries < n + n / 2u) {
if (wait_irq(d) || l1_read(d, &m) || l1_clear(d))
return 3;
tries++;
if (m.status == 0) {
sum += m.mm;
got++;
}
}
l1_stop(d);
if (got < 1u + n / 2u)
return 4;
*ofs = (int16_t)((int32_t)target_mm - (sum + (int32_t)got / 2) / (int32_t)got);
if (l1_wr16(d, L1_OUTER_OFFSET_MM, (uint16_t)*ofs) || l1_wr16(d, L1_OFFSET_MM, (uint16_t)(*ofs * 4)))
return 5;
return 0;
}
/* Калибровка помехи стекла — как SingleTargetXTalkCalibration в API: мишень на известной дальности, n измерений
* без компенсации; помеха на SPAD = сигнал / SPAD · (1 − измерено / мишень). В 0x16 — кило-отсчёты в 7.9. */
int l1_cal_xtalk(l1_t *d, uint16_t target_mm, uint8_t n, uint16_t *kcps)
{
uint32_t sum_mm = 0, sum_sig = 0, sum_sp = 0, got = 0, sig, sp, mm;
l1_meas_t m;
if (l1_wr16(d, L1_XTALK_KCPS, 0) || l1_wr32(d, L1_XTALK_X_GRAD, 0))
return 1;
if (l1_start(d, 0x20u) || wait_irq(d) || l1_clear(d)) /* первое измерение не в счёт */
return 2;
for (uint8_t i = 0; i < n; i++) {
if (wait_irq(d) || l1_read(d, &m) || l1_clear(d))
return 3;
if (m.status == 0 && m.mm > 0) {
sum_mm += (uint32_t)m.mm;
sum_sig += m.signal;
sum_sp += m.spads >> 8;
got++;
}
}
l1_stop(d);
if (!got)
return 4;
sig = sum_sig / got;
sp = (sum_sp + got / 2u) / got;
mm = (sum_mm + got / 2u) / got;
*kcps = 0;
if (sp && mm < target_mm) /* MCPS 9.7 → кило-отсчёты 7.9: · 1000 · 512 / 128 */
*kcps = (uint16_t)((sig * 4000u / sp) * (target_mm - mm) / target_mm);
return l1_wr16(d, L1_XTALK_KCPS, *kcps) ? 5 : 0;
}
/* «Сканирование» 16 зонами 4×4 SPAD, построчно сверху вниз: ROI следующей зоны пишется, пока датчик ждёт
* снятия прерывания, — следующее измерение начнётся уже с ней. */
int l1_scan(l1_t *d, int16_t mm[16], uint8_t st[16])
{
l1_meas_t m;
if (l1_set_roi(d, 0, 12, 4, 4) || l1_start(d, 0x20u))
return 1;
for (uint8_t z = 0; z < 16u; z++) {
uint8_t n = (uint8_t)((z + 1u) & 15u);
if (wait_irq(d) || l1_read(d, &m))
return 2;
mm[z] = m.mm;
st[z] = m.status;
if (l1_set_roi(d, (uint8_t)(4u * (n & 3u)), (uint8_t)(12u - 4u * (n >> 2)), 4, 4) || l1_clear(d))
return 3;
}
return l1_stop(d) ? 4 : 0;
}
fw100e.c — прошивка F103: I2C1 на 400 кГц, XSHUT и GPIO1 трёх датчиков, EXTI1…EXTI3, главный цикл.
/* fw100e.c — раздел 100г: три VL53L1X на одной шине I2C1 STM32F103 (BluePill), 72 МГц.
* I2C1: PB6 — SCL, PB7 — SDA, 400 кГц, как в 100б. XSHUT — PB12, PB13, PB14, открытый сток; GPIO1 — PA1, PA2, PA3,
* входы с подтяжкой, EXTI1…EXTI3 по спаду: драйвер ставит GPIO1 активным нулём (0x30 = 0x11).
* Датчики по одному получают адреса 0x30, 0x31, 0x32, режим Long, бюджет 33 мс, измерение по таймеру раз в 100 мс.
* Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); APB1 = 36 МГц. */
#include "stm32f10x.h"
#include "tof100e.h"
#define CPU_HZ 72000000u
#define APB1_MHZ 36u
#define I2C_FAST_CCR (APB1_MHZ * 1000000u / (3u * 400000u)) /* F/S = 1, DUTY = 0: Tlow = 2 Thigh */
#define I2C_FAST_TRISE (APB1_MHZ * 300u / 1000u + 1u) /* фронт до 300 нс */
#define I2C_SPIN 20000u /* предел ожидания флага, витков */
#define BUDGET_MS 33u
#define PERIOD_MS 100u
_Static_assert(I2C_FAST_CCR == 30u, "CCR = 30: 36 МГц / (3 · 30) = 400 кГц");
_Static_assert(I2C_FAST_TRISE == 11u, "TRISE = 11");
_Static_assert(PERIOD_MS >= BUDGET_MS + L1_IMP_GUARD_MS, "период по таймеру — не меньше бюджета + 4 мс");
static l1_t T[3];
static const uint8_t ADDR[3] = {0x30u, 0x31u, 0x32u};
volatile int16_t dist_mm[3]; /* последняя годная дальность; отрицательное — шаг, где встали */
volatile uint8_t last_status[3];
/* ── I2C1, ведущий, опросом; на каждом ожидании — предел ── */
static int wait1(uint32_t mask)
{
for (uint32_t n = 0; n < I2C_SPIN; n++) {
uint32_t sr = I2C1->SR1;
if (sr & I2C_SR1_AF) { /* нет подтверждения */
I2C1->SR1 = (uint16_t)~I2C_SR1_AF;
return 1;
}
if (sr & mask)
return 0;
}
return 2;
}
static void stop(void)
{
I2C1->CR1 |= I2C_CR1_STOP;
while (I2C1->CR1 & I2C_CR1_STOP) {
}
}
static int start(uint8_t a8)
{
I2C1->CR1 |= I2C_CR1_START;
if (wait1(I2C_SR1_SB))
return 1;
I2C1->DR = a8;
if (wait1(I2C_SR1_ADDR)) {
stop();
return 1;
}
return 0;
}
static int send(const uint8_t *buf, uint32_t n)
{
(void)I2C1->SR1;
(void)I2C1->SR2; /* ADDR снят: SR1, затем SR2 (17.4) */
for (uint32_t i = 0; i < n; i++) {
if (wait1(I2C_SR1_TXE))
return 1;
I2C1->DR = buf[i];
}
return n ? wait1(I2C_SR1_BTF) : 0;
}
int l1_i2c_write(uint8_t addr7, const uint8_t *buf, uint32_t n)
{
if (start((uint8_t)(addr7 << 1)))
return 1;
int e = send(buf, n);
stop();
return e;
}
/* Два байта адреса регистра, повторный старт, приём по RM0008: один байт — ACK снять до снятия ADDR; два — POS;
* больше двух — последние три через BTF, ACK снять перед предпоследним. */
int l1_i2c_wr_rd(uint8_t addr7, const uint8_t *wr, uint32_t nw, uint8_t *buf, uint32_t n)
{
if (start((uint8_t)(addr7 << 1)))
return 1;
if (send(wr, nw) || start((uint8_t)((addr7 << 1) | 1u))) {
stop();
return 1;
}
int e = 0;
if (n == 1) {
I2C1->CR1 &= ~I2C_CR1_ACK;
(void)I2C1->SR1;
(void)I2C1->SR2;
I2C1->CR1 |= I2C_CR1_STOP;
e = wait1(I2C_SR1_RXNE);
buf[0] = (uint8_t)I2C1->DR;
} else if (n == 2) {
I2C1->CR1 |= I2C_CR1_POS;
I2C1->CR1 &= ~I2C_CR1_ACK;
(void)I2C1->SR1;
(void)I2C1->SR2;
e = wait1(I2C_SR1_BTF);
I2C1->CR1 |= I2C_CR1_STOP;
buf[0] = (uint8_t)I2C1->DR;
buf[1] = (uint8_t)I2C1->DR;
} else {
(void)I2C1->SR1;
(void)I2C1->SR2;
uint32_t i = 0;
for (; i < n - 3u && !e; i++) {
e = wait1(I2C_SR1_RXNE);
buf[i] = (uint8_t)I2C1->DR;
}
e |= wait1(I2C_SR1_BTF); /* N-2 в DR, N-1 в сдвиговом */
I2C1->CR1 &= ~I2C_CR1_ACK;
buf[i++] = (uint8_t)I2C1->DR;
I2C1->CR1 |= I2C_CR1_STOP;
buf[i++] = (uint8_t)I2C1->DR;
e |= wait1(I2C_SR1_RXNE);
buf[i] = (uint8_t)I2C1->DR;
}
while (I2C1->CR1 & I2C_CR1_STOP) {
}
I2C1->CR1 = (I2C1->CR1 & ~I2C_CR1_POS) | I2C_CR1_ACK;
return e;
}
/* ── время, выводы, сон ── */
void l1_delay_us(uint32_t us)
{
uint32_t t0 = DWT->CYCCNT, n = us * (CPU_HZ / 1000000u);
while (DWT->CYCCNT - t0 < n) {
}
}
void l1_xshut(uint8_t idx, int run)
{
if (run)
GPIOB->BSRR = 1u << (12u + idx); /* открытый сток: 1 — отпустить */
else
GPIOB->BRR = 1u << (12u + idx);
}
static int any_irq(void)
{
return T[0].irq | T[1].irq | T[2].irq;
}
/* Проверить флаги и уснуть атомарно: прерывание между проверкой и WFI всё равно разбудит. */
int l1_idle(void)
{
__disable_irq();
if (!any_irq())
__WFI();
__enable_irq();
return 1;
}
void EXTI1_IRQHandler(void)
{
EXTI->PR = EXTI_PR_PR1;
T[0].irq = 1;
}
void EXTI2_IRQHandler(void)
{
EXTI->PR = EXTI_PR_PR2;
T[1].irq = 1;
}
void EXTI3_IRQHandler(void)
{
EXTI->PR = EXTI_PR_PR3;
T[2].irq = 1;
}
static void board_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
/* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (11 11) */
GPIOB->CRL = (GPIOB->CRL & ~0xFF000000u) | 0xFF000000u;
/* PB12…PB14 — выход с открытым стоком, 2 МГц (01 10); сначала 0 — все датчики в сбросе */
GPIOB->BRR = 7u << 12;
GPIOB->CRH = (GPIOB->CRH & ~0x0FFF0000u) | 0x06660000u;
/* PA1…PA3 — вход с подтяжкой (10 00), ODR = 1 — к питанию */
GPIOA->CRL = (GPIOA->CRL & ~0x0000FFF0u) | 0x00008880u;
GPIOA->BSRR = 7u << 1;
I2C1->CR1 = I2C_CR1_SWRST;
I2C1->CR1 = 0;
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = I2C_CCR_FS | I2C_FAST_CCR;
I2C1->TRISE = I2C_FAST_TRISE;
I2C1->CR1 = I2C_CR1_PE | I2C_CR1_ACK;
/* EXTI1…3 от порта A (AFIO_EXTICR1 = 0 после сброса), по спаду */
AFIO->EXTICR[0] &= ~(AFIO_EXTICR1_EXTI1 | AFIO_EXTICR1_EXTI2 | AFIO_EXTICR1_EXTI3);
EXTI->FTSR |= EXTI_FTSR_TR1 | EXTI_FTSR_TR2 | EXTI_FTSR_TR3;
EXTI->PR = EXTI_PR_PR1 | EXTI_PR_PR2 | EXTI_PR_PR3;
EXTI->IMR |= EXTI_IMR_MR1 | EXTI_IMR_MR2 | EXTI_IMR_MR3;
NVIC_EnableIRQ(EXTI1_IRQn);
NVIC_EnableIRQ(EXTI2_IRQn);
NVIC_EnableIRQ(EXTI3_IRQn);
}
static int bring_up(uint8_t i)
{
int rc = l1_boot(&T[i], i, ADDR[i]);
if (!rc)
rc = l1_init(&T[i]);
if (!rc && l1_set_budget(&T[i], BUDGET_MS))
rc = 20;
if (!rc && l1_set_imp(&T[i], PERIOD_MS))
rc = 21;
if (!rc && l1_start(&T[i], 0x40u))
rc = 22;
return rc;
}
int main(void)
{
board_init();
l1_sleep_all(3);
for (uint8_t i = 0; i < 3; i++) {
int rc = bring_up(i);
if (rc)
l1_xshut(i, 0); /* не ответил — обратно в сброс, чтобы не занял 0x29 */
dist_mm[i] = (int16_t)(rc ? -rc : 0);
}
for (;;) {
l1_idle();
for (uint8_t i = 0; i < 3; i++) {
l1_meas_t m;
if (!T[i].irq)
continue;
T[i].irq = 0;
if (l1_read(&T[i], &m) || l1_clear(&T[i]))
continue;
last_status[i] = m.status;
if (m.status == 0) /* 6 — первое после пуска, без проверки обёртки: мимо */
dist_mm[i] = m.mm;
}
}
}
/* ── вектора и запуск (armclang + microlib): __main заполняет .data и .bss и вызывает main ── */
#define STACK_TOP 0x20005000u /* конец 20 КБ ОЗУ F103C8 */
__asm(".global __initial_sp\n.set __initial_sp, 0x20005000"); /* microlib берёт вершину стека отсюда */
extern void __main(void);
void Reset_Handler(void)
{
__main();
}
static void Default_Handler(void)
{
for (;;) {
}
}
__attribute__((section("RESET"), used)) static void (*const vectors[16 + 43])(void) = {
[0] = (void (*)(void))STACK_TOP, [1] = Reset_Handler,
[2 ... 16 + 6] = Default_Handler,
[16 + 7] = EXTI1_IRQHandler, [16 + 8] = EXTI2_IRQHandler, [16 + 9] = EXTI3_IRQHandler,
[16 + 10 ... 16 + 42] = Default_Handler,
};
_Static_assert(EXTI1_IRQn == 7 && EXTI2_IRQn == 8 && EXTI3_IRQn == 9, "EXTI1…3 — прерывания 7…9");
Дальше: раздел 101 «Ток и напряжение: INA219 и ACS712» — шунт и калибровочный регистр, датчик Холла и его ноль посередине шкалы.