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

100б. Лазерный дальномер VL53L0X: время пролёта по I2C, режимы точности и скорости, несколько датчиков на одной шине

В разделе 100 расстояние мерил звук: HC-SR04 отвечает импульсом, и метр даёт эхо 5824 мкс. В разделе 100а радар делил комнату на ворота по 0,7 м и искал в них человека. VL53L0X от ST меряет светом: крошечный лазер шлёт импульсы, матрица счётчиков фотонов ловит отражение, и датчик сам переводит время пролёта в миллиметры. Наружу — шина I2C, вывод XSHUT для сброса и вывод GPIO1 «измерение готово».

Здесь: почему время пролёта света — это пикосекунды и как датчик их получает; выводы и адрес модуля; почему ST даёт не описание регистров, а библиотеку на Си, и что она делает при включении; однократное и непрерывное измерение, бюджет времени и шум; готовность по GPIO1 и прерыванию против опроса; три датчика на одной шине — включение по одному и смена адреса; фильтр, трудные поверхности, солнце и стекло; сравнение с HC-SR04 и радаром.

Опоры: библиотека ST для VL53L0X (API версии 1.0.2, STSW-IMG005) — её копия лежит в модуле hal_st проекта Zephyr под лицензией BSD-3-Clause, — и драйвер датчика в самом Zephyr (Apache-2.0). Оттуда адреса регистров, адрес 0x29, время загрузки, порядок инициализации и профили. Описание самого датчика (datasheet) при подготовке раздела не использовалось: питание модуля, предельная дальность и поле зрения здесь не названы. Стенд — библиотека ST и наш слой, собранные gcc и прогнанные против модели датчика на Python; тот же код собран armclang под STM32F103 и на плате не запускался. I²C — раздел 17, прерывания от вывода — 7.5, подключение модулей — 95.

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

Оригинал Перевод
time of flight (ToF) время пролёта
VCSEL (vertical-cavity surface-emitting laser) лазер с вертикальным резонатором — источник импульсов
SPAD (single photon avalanche diode) лавинный фотодиод, считающий отдельные фотоны
reference SPAD опорные SPAD: их число подбирают под опорный сигнал
ranging, single / continuous / timed ranging измерение; однократное / подряд / по таймеру
measurement timing budget бюджет времени на одно измерение
inter-measurement period период между измерениями в режиме по таймеру
range status статус измерения: годно или почему нет
sigma estimate оценка σ — ожидаемый разброс дальности
signal rate, ambient rate (MCPS) отсчёты отражения и фона, миллионов в секунду
crosstalk (XTalk) перекрёстная помеха: свет, отражённый стеклом крышки
VHV, phase calibration калибровки VHV и фазы
XSHUT (xshutdown) вход сброса, активный ноль
new sample ready (GPIO1) выход «измерение готово»

100б.1 Время пролёта: почему пикосекунды

Свет летит 299 792 458 м/с. Цель в метре — это 2 м пути и 6,671 нс; один миллиметр дальности — 6,671 пс. В библиотеке ST эта величина записана константой cTOF_per_mm_ps — 6,6 пс на миллиметр. Звуку HC-SR04 на тот же миллиметр нужно 5,824 мкс: в 873 000 раз дольше. Отсюда главное: таймер STM32 свет не поймает. Такт на 72 МГц — 13,89 нс, и за него свет проходит путь, равный дальности 2082 мм.

У датчика своя тактовая сетка, но и она грубая: в библиотеке период его PLL — 1655 пс, свет за это время пролетает 496,2 мм, то есть 248,1 мм дальности. Миллиметры получаются статистикой: SPAD считают отдельные фотоны, и датчик усредняет их время прихода. Как именно, ST не раскрывает, но формула оценки σ в библиотеке говорит главное — разброс падает с числом пойманных фотонов. Поэтому точность покупается временем, и это видно в 100б.4.

Время внутри датчика считается в макропериодах: 2304 такта PLL, умноженные на период лазерного импульса в тактах. При периоде 10 тактов макропериод — 38 131 нс, при 14 — 53 384 нс, при 18 — 68 636 нс. В макропериодах задаются тайм-ауты шагов измерения, и через них библиотека переводит бюджет времени в регистры.


100б.2 Модуль VL53L0X: выводы, питание, адрес

Три VL53L0X на STM32F103: слева микроконтроллер, PB6 и PB7 — общая шина I2C1 на 400 кГц, PB12, PB13, PB14 идут на XSHUT датчиков, PA1, PA2, PA3 принимают их GPIO1; справа три модуля, у каждого адрес 0x29 после включения и 0x30, 0x31, 0x32 после смены; посередине — импульс света до цели и обратно: метр за 6,67 нс, миллиметр за 6,67 пс, в библиотеке 6,6 пс, такт STM32 13,89 нс равен 2082 мм, такт PLL датчика 1655 пс — 248 мм, звук в 873 000 раз медленнее; внизу — включение по одному: XSHUT вверх, ожидание 1,2 мс, проверка 0xEEAA по 0x29, запись нового адреса в 0x8A; все три — 9 транзакций и 937,5 мкс на шине, 4,54 мс с ожиданиями

Вывод Что это Куда в прошивке
SCL, SDA I²C, адрес 0x29 — байты линии 0x52 и 0x53 (17.2) PB6, PB7 — I2C1, 400 кГц
XSHUT сброс, активный ноль: 0 — датчик спит и молчит PB12, PB13, PB14 — открытый сток
GPIO1 «измерение готово», после инициализации активный ноль PA1, PA2, PA3 — EXTI по спаду
питание, земля по описанию модуля —

Адрес по умолчанию 0x29 и время загрузки до 1,2 мс после отпускания XSHUT взяты из драйвера Zephyr (VL53L0X_INITIAL_ADDR, T_BOOT), там же проверка «это VL53L0X»: слово по регистру 0xC0 равно 0xEEAA. В описании привязки Zephyr XSHUT назван входом с активным нулём. Выход GPIO1 настраивает сама библиотека: при StaticInit она пишет в регистр 0x0A код 4 — «новое измерение» — и снимает бит 4 регистра 0x84, то есть линия при готовности уходит в ноль.

С питанием честно сказать можно немного. Библиотека по умолчанию считает, что выводы I²C датчика работают от 1,8 В, а для 2,8 В надо собрать её с USE_I2C_2V8 — тогда она поднимает бит 0 регистра 0x89. Драйвер Zephyr этот ключ не задаёт, наша сборка тоже. Есть ли на модуле свой стабилизатор, подтяжки и согласование уровней, зависит от платы модуля; схемы у нас нет, поэтому перед подключением к 3,3 В это надо проверить по описанию модуля. XSHUT в прошивке — выход с открытым стоком: единица лишь отпускает линию, и поднять её должна подтяжка (своя или на модуле). Подключение модулей к STM32 вообще — раздел 95.


100б.3 Почему «API», а не карта регистров: что делает включение

У большинства датчиков из этой части есть таблица регистров: записал режим, прочитал результат. У VL53L0X её нет — ST вместо описания регистров даёт библиотеку на Си, так прямо сказано в README модуля hal_st. Регистры при этом никуда не делись: библиотека обращается к ним по I²C, и по её исходникам видно, куда и что. Наш код делает то же, что драйвер Zephyr: регистры трогает только библиотека, а мы даём ей платформу — функции чтения и записи и паузу опроса.

VL53L0X_Error VL53L0X_WriteMulti(VL53L0X_DEV Dev, uint8_t index, uint8_t *pdata, uint32_t count)
{
    uint8_t b[1u + TOF_MULTI_MAX];
    if (count > TOF_MULTI_MAX)
        return VL53L0X_ERROR_INVALID_PARAMS;
    b[0] = index;
    memcpy(b + 1, pdata, count);
    return tof_i2c_write(Dev->I2cDevAddr, b, count + 1u) ? VL53L0X_ERROR_CONTROL_INTERFACE : VL53L0X_ERROR_NONE;
}

Каждое обращение библиотеки — одна транзакция I²C, многобайтные значения старшим байтом вперёд. Включение датчика — в том порядке, что у Zephyr:

/* Инициализация — в порядке драйвера Zephyr (vl53l0x_start и vl53l0x_setup_single_shot). */
int tof_init(tof_t *t, const tof_profile_t *p)
{
    uint8_t vhv, phase;
    STEP(10, VL53L0X_DataInit(&t->st));
    STEP(11, VL53L0X_StaticInit(&t->st));
    STEP(12, VL53L0X_PerformRefCalibration(&t->st, &vhv, &phase));
    STEP(13, VL53L0X_PerformRefSpadManagement(&t->st, &t->spads, &t->aperture));
    STEP(14, VL53L0X_SetDeviceMode(&t->st, VL53L0X_DEVICEMODE_SINGLE_RANGING));
    STEP(15, VL53L0X_SetLimitCheckEnable(&t->st, VL53L0X_CHECKENABLE_SIGMA_FINAL_RANGE, 1));
    STEP(16, VL53L0X_SetLimitCheckEnable(&t->st, VL53L0X_CHECKENABLE_SIGNAL_RATE_FINAL_RANGE, 1));
    STEP(17, VL53L0X_SetLimitCheckValue(&t->st, VL53L0X_CHECKENABLE_SIGNAL_RATE_FINAL_RANGE, p->signal_min));
    STEP(18, VL53L0X_SetLimitCheckValue(&t->st, VL53L0X_CHECKENABLE_SIGMA_FINAL_RANGE, p->sigma_max));
    STEP(19, VL53L0X_SetMeasurementTimingBudgetMicroSeconds(&t->st, p->budget_us));
    STEP(20, VL53L0X_SetVcselPulsePeriod(&t->st, VL53L0X_VCSEL_PERIOD_PRE_RANGE, p->vcsel_pre));
    STEP(21, VL53L0X_SetVcselPulsePeriod(&t->st, VL53L0X_VCSEL_PERIOD_FINAL_RANGE, p->vcsel_final));
    return 0;
}

Что происходит внутри, по исходникам библиотеки:

На стенде одно такое включение — 684 транзакции и 2621 байт: 63,3 мс работы шины на 400 кГц, 13 измерений внутри и 264 мс вместе с ними. Сколько опорных SPAD выберет датчик, зависит от его опорного сигнала; в модели он задан как 2,3 MCPS на SPAD, и калибровка остановилась на 9 SPAD без апертуры — это число модели, не датчика.

Цена библиотеки видна по прошивке на три датчика (armclang 6.24, -Os, Cortex-M3). Во всём образе 12 988 байт кода, из них 10 332 — библиотека ST, 1124 — наш слой tof100c.c, 1316 — прошивка fw100c.c; компоновщик выбросил из библиотеки всё неиспользуемое (в vl53l0x_api.o было 5096 байт кода, в образ вошло 3144). Флеш — 13 972 байта, ОЗУ — 1860 байт без стека. Из ОЗУ 681 байт — две таблицы библиотеки, таблица настроек (243 байта) и таблица порогов прерываний (438), объявленные без const.


100б.4 Однократно и непрерывно: бюджет времени, шум и готовность

Режим задаёт запись в регистр 0x00: 0x01 — одно измерение, 0x02 — подряд без пауз, 0x04 — по таймеру, с периодом в регистре 0x04 (период в миллисекундах библиотека умножает на калибровку генератора из регистра 0xF8). Запись 0x00 останавливает.

Бюджет времени. Измерение состоит из шагов, и бюджет — их сумма с накладными: 1910 мкс на старт, 960 на конец, 660 на предварительную дальнометрию, 550 на основную. Остаток отдаётся основной дальнометрии. Меньше 20 000 мкс библиотека не принимает — на 19 000 вернула ошибку −4. Что получилось после округления до макропериодов:

Просили, мкс Получили, мкс Предварительная, мкс Основная, мкс
20 000 19 981 8061 2402
33 000 32 908 8061 15 329
66 000 65 853 8061 48 274
100 000 99 713 8061 82 134
200 000 199 769 8061 182 190

Шум. Разброс дальности библиотека оценивает сама — формулой из числа пойманных фотонов отражения и фона. В модели шум задан ровно этой формулой (перенесена из библиотеки), поэтому числа ниже показывают её поведение, а не поведение живого датчика. Мишень с отражением 1 — та, для которой в библиотеке записан опорный сигнал 1,42 MCPS на 400 мм; «серая» — 0,3 от неё, «чёрная» — 0,05; сигнал падает как 1/d². По 100 измерений на точку, обычный профиль:

Бюджет белая, 600 мм серая, 300 мм чёрная, 150 мм
20 мс 8,94 мм 9,47 мм 11,64 мм
33 мс 6,68 мм 5,58 мм 6,98 мм
66 мс 3,29 мм 3,05 мм 4,01 мм
200 мс 1,74 мм 1,90 мм 2,17 мм

Вдесятеро больше времени — примерно впятеро меньше разброс. Оценка самой библиотеки на белой мишени — 9,50, 5,90, 3,69 и 1,97 мм.

Профили. Обычный — то, что ставит библиотека: предел сигнала 0,25 MCPS, σ до 18 мм, периоды лазера 14 и 10 тактов (12…18 и 8…14 — что допускает библиотека). Дальний — как в драйвере Zephyr: 0,1 MCPS, 60 мм, периоды 18 и 14. Предел сигнала уходит в регистр 0x44 в формате 9.7: 0x0020 — ровно 0,25, 0x000C — 0,094. Если сигнал падает как 1/d², то белая мишень пропадает за 953 мм в обычном профиле и за 1557 мм в дальнем, серая — за 522 и 853 мм, чёрная — за 213 и 348 мм. Прогон с бюджетом 33 мс это подтвердил: белая на 1200 мм — 0 годных из 100 и 100 из 100, на 1500 мм в дальнем — 93; серая на 600 мм — 0 и 100; чёрная на 300 мм — 0 и 100. Плата — разброс: белая на 600 мм — 6,68 мм в обычном и 8,15 мм в дальнем. Энергию импульса модель не считает, поэтому дальний профиль в ней — это только мягкие пределы.

Готовность. Библиотека ждёт конца измерения опросом: читает регистр 0x13 (готово, когда младшие три бита равны 4), между чтениями зовёт паузу платформы — у нас 1000 мкс — и сдаётся после 2000 попыток. Одно чтение регистра — 97,5 мкс шины на 400 кГц. Можно иначе: запустить измерение и ждать спада GPIO1 на EXTI, а шину не трогать. Бюджет 33 мс, по 20 измерений:

Транзакций на измерение Байт Шина занята
однократно, опрос 44 177 12,5 %
однократно, GPIO1 13 53 3,7 %
непрерывно, GPIO1 4 25 —

В непрерывном режиме на результат уходят четыре транзакции: 12 байт с 0x14 (дальность, статус, сигнал и фон) и снятие прерывания. Подряд результаты шли через 32 908 мкс — ровно бюджет, по таймеру на 100 мс — через 100 000 мкс.

/* Забрать результат: 12 байт с 0x14 разбирает API, оценку σ он же считает при этом; затем снять прерывание —
 * пока его не снять, GPIO1 стоит внизу. */
int tof_fetch(tof_t *t, tof_meas_t *m)
{
    VL53L0X_RangingMeasurementData_t r;
    FixPoint1616_t sigma = 0;
    memset(&r, 0, sizeof r);                       /* API смотрит в RangeStatus прошлого замера — обнуляем */
    STEP(30, VL53L0X_GetRangingMeasurementData(&t->st, &r));
    STEP(31, VL53L0X_GetLimitCheckCurrent(&t->st, VL53L0X_CHECKENABLE_SIGMA_FINAL_RANGE, &sigma));
    STEP(32, VL53L0X_ClearInterruptMask(&t->st, 0));
    m->mm = r.RangeMilliMeter;
    m->status = r.RangeStatus;
    m->signal = r.SignalRateRtnMegaCps;
    m->ambient = r.AmbientRateRtnMegaCps;
    m->sigma = sigma;
    return 0;
}

Снять прерывание обязательно. GPIO1 стоит внизу, пока в регистр 0x0B не запишут единицу — нового спада не будет, и EXTI больше не сработает. В модели датчик в непрерывном режиме 200 мс не читали: один спад, затем пять результатов, затёртых следующими, а линия так и осталась внизу.

void EXTI1_IRQHandler(void)
{
    EXTI->PR = EXTI_PR_PR1;
    tof_on_gpio1(&T[0]);
}
/* Проверить флаги и уснуть атомарно: прерывание между проверкой и WFI всё равно разбудит. */
void tof_idle(void)
{
    __disable_irq();
    if (!any_irq())
        __WFI();
    __enable_irq();
}

Обработчик только ставит флаг, а читает основной цикл: обращение к шине из прерывания столкнулось бы с обменом, который идёт в этот момент. Сон — внутри запрета прерываний: если спад придёт между проверкой флагов и WFI, ожидающее прерывание всё равно разбудит процессор.


100б.5 Несколько датчиков на одной шине: XSHUT и смена адреса

Все VL53L0X после включения отвечают по 0x29. Адрес меняется записью в регистр 0x8A, но только тому, кто сейчас слушает, — поэтому датчики включают по одному: все XSHUT вниз, затем каждый отпускают, ждут загрузки, проверяют, что по 0x29 отвечает именно VL53L0X, и переводят на свой адрес.

/* Отпустить XSHUT одного датчика, дождаться загрузки, проверить, что по 0x29 отвечает VL53L0X,
 * и перевести его на свой адрес. Остальные датчики в это время держат XSHUT внизу и молчат. */
int tof_wake(tof_t *t, uint8_t idx, uint8_t addr7)
{
    uint16_t id = 0;
    memset(t, 0, sizeof *t);
    t->idx = idx;
    t->st.I2cDevAddr = TOF_ADDR_DEFAULT;
    tof_xshut(idx, 1);
    tof_delay_us(TOF_BOOT_US);
    STEP(1, VL53L0X_RdWord(&t->st, TOF_REG_ID, &id));
    if (id != TOF_CHIP_ID)
        return 2;
    if (addr7 != TOF_ADDR_DEFAULT) {
        STEP(3, VL53L0X_SetDeviceAddress(&t->st, (uint8_t)(addr7 << 1)));   /* API ждёт 8-битный адрес */
        t->st.I2cDevAddr = addr7;
        id = 0;
        STEP(4, VL53L0X_RdWord(&t->st, TOF_REG_ID, &id));
        if (id != TOF_CHIP_ID)
            return 5;
    }
    return 0;
}

Функция библиотеки VL53L0X_SetDeviceAddress ждёт восьмибитный адрес и сама делит его на 2 — путаница из 17.2 в чистом виде: передать ей 0x30 — значит получить 0x18. Драйвер Zephyr передаёт удвоенный адрес, мы тоже.

На стенде три датчика включились за 9 транзакций и 39 байт: 937,5 мкс шины и 4537,5 мкс вместе с ожиданием загрузки; сканирование нашло 0x30, 0x31, 0x32. Модель показала и три ошибки:

В Zephyr смена адреса включается ключом CONFIG_VL53L0X_RECONFIGURE_ADDRESS, и тогда вывод XSHUT обязан быть у каждого датчика. Три датчика, каждый в непрерывном режиме со своим GPIO1, на стенде дали по 30,36 измерения в секунду на датчик при занятости шины 5,4 % и ни одного потерянного результата. Те же три по очереди, однократно и с опросом, — по 9,72 в секунду и 12,5 % шины.


100б.6 Фильтр, трудные поверхности, свет и стекло; сравнение с HC-SR04 и радаром

Каждое измерение приходит со статусом. Библиотека сводит код датчика (биты 6:3 байта по 0x14) и свои проверки в одно число: 0 — годно, 1 — σ больше предела, 2 — слабый сигнал, 3 — слишком близко, 4 — сбой фазы, 5 — сбой устройства. Фильтр начинается с того, что всё ненулевое выбрасывают; из оставшегося берут медиану последних пяти.

/* ── фильтр: недостоверные статусы — мимо, из годных — медиана последних TOF_MED ── */
int tof_filter(tof_filt_t *f, const tof_meas_t *m)
{
    uint16_t s[TOF_MED];
    if (m->status != 0) {
        f->dropped++;
    } else {
        f->taken++;
        f->win[f->pos] = m->mm;
        f->pos = (uint8_t)((f->pos + 1u) % TOF_MED);
        if (f->n < TOF_MED)
            f->n++;
    }
    if (f->n == 0)
        return -1;
    memcpy(s, f->win, sizeof s);
    for (uint8_t i = 1; i < f->n; i++)             /* вставками: окно маленькое */
        for (uint8_t j = i; j > 0 && s[j - 1] > s[j]; j--) {
            uint16_t x = s[j];
            s[j] = s[j - 1];
            s[j - 1] = x;
        }
    return s[f->n / 2];
}

Непрерывный режим, бюджет 33 мс, по 200 измерений:

Сцена (модель) Годных Разброс годных После медианы Худшая ошибка: все / медиана
белая, 600 мм 200 6,08 мм 3,88 мм 16 / 10 мм
белая, 600 мм, фон 2,5 MCPS 52 (148 — статус 1) 18,57 мм 12,96 мм 46 / 29 мм
белая, 300 мм, фон 2,5 MCPS 200 3,67 мм 2,00 мм 11 / 6 мм
белая, 950 мм — у предела 126 (74 — статус 2) 9,51 мм 5,24 мм 32 / 12 мм
пусто 0 (200 — статус 2) — — 1606 мм

Солнце — это фон: фотоны, не связанные с импульсом. Формула σ растёт с отношением фона к сигналу, поэтому фон бьёт по дальним и тёмным целям: на 600 мм из 200 измерений годных осталось 52, на 300 мм — все. Пустая сцена — главный довод проверять статус: в модели дальность без отражения разбросана от 0 до 2606 мм. Что кладёт в регистр живой датчик, когда целиться некуда, мы не проверяли. У самого края дальности недостоверные значения в модели близки к правде (от 933 до 976 мм), но датчик за них не ручается.

Чёрное. Сигнал пропорционален отражению: чёрная мишень в 20 раз темнее белой, и в обычном профиле она пропадает уже за 213 мм (100б.4). Блестящее и наклонное. Зеркальная поверхность отражает импульс по закону отражения, а не во все стороны: под углом свет к датчику не вернётся, прямо — вернётся слишком ярким. Стекло. Крышка перед датчиком отражает часть импульса в приёмник сама — это перекрёстная помеха. На этот случай в библиотеке есть калибровка смещения и компенсация помехи (PerformOffsetCalibration, PerformXTalkCalibration, SetXTalkCompensationEnable); по умолчанию компенсация выключена. Чисел для этих трёх случаев модель не даёт — это качественная картина.

HC-SR04 (раздел 100) HLK-LD2420 (раздел 100а) VL53L0X
чем меряет звук, эхо 5824 мкс на метр радар 24 ГГц свет, 6,671 нс на метр
что видит ближайшую поверхность человека, и неподвижного поверхность, на которую направлен
результат длительность импульса — меряет таймер ворота по 0,7 м, «есть / нет» миллиметры по I²C
время на ответ до таймаута 25 мс 10 раз в секунду бюджет от 20 мс
от чего зависит температура: +3,7 % на морозе пороги ворот отражение, фон, бюджет
несколько на объекте мешают друг другу — по очереди — один адрес — XSHUT и смена адреса

Для «есть ли кто в комнате» нужен радар; для «сколько до стены» на метры — ультразвук; для миллиметров на десятках сантиметров, для нескольких датчиков на одной плате и там, где звук гаснет на мягком, — VL53L0X.


100б.7 Грабли

Второй датчик не находится, а первый отвечает по чужому адресу. Все XSHUT отпущены разом: запись в 0x8A получили все. Включать по одному.

Адрес после смены — 0x18 вместо 0x30. VL53L0X_SetDeviceAddress ждёт восьмибитный адрес и делит его на 2. Передавать удвоенный.

Первое обращение после XSHUT — без подтверждения. Не дождались загрузки: до 1,2 мс.

После провала питания датчик «пропал». Он вернулся на 0x29. Повторить включение по одному и инициализацию.

Прерывание пришло один раз и больше не приходит. Не сняли прерывание записью в 0x0B: GPIO1 стоит внизу, нового спада нет.

Дальность скачет на сотни миллиметров. Не смотрят статус: недостоверные измерения идут в дело. Всё с ненулевым статусом — мимо.

На солнце пропадают дальние цели, ближние в порядке. Фон раздувает σ; дальний профиль, бюджет побольше или тень.

Шина занята, а датчики ещё меряют. Опрос готовности: 44 транзакции на измерение вместо 13. Ждать GPIO1.

Бюджет 19 мс не ставится. Меньше 20 000 мкс библиотека не принимает и возвращает ошибку −4 — проверять код возврата.

ОЗУ ушло больше, чем ждали. Две таблицы библиотеки без const — 681 байт, плюс состояние библиотеки на каждый датчик.

Через стекло дальность занижена или стоит на месте. Свет отражается от крышки. Калибровка смещения и компенсация помехи — или датчик без стекла.


100б.8 Итоги


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

Слой над библиотекой ST и прошивка для трёх датчиков. Сама библиотека — копия из hal_st без изменений, в _verify/tof100c/st/; заголовки платформы — свои, стенд host100c.c и модель model100c.py — в _verify/tof100c/.

vl53l0x_platform.h — платформа библиотеки: устройство и функции доступа к регистрам.

/* Раздел 100б: платформа API ST VL53L0X — какое устройство и как до него добраться.
 * API ST зовёт эти функции для каждого обращения к регистрам; реализация — в tof100c.c,
 * поверх шины из tof100c.h (прошивка — I2C1 F103, стенд — строки к модели). */
#ifndef VL53L0X_PLATFORM_H
#define VL53L0X_PLATFORM_H
#include "vl53l0x_def.h"
#include "vl53l0x_platform_log.h"

typedef struct {
    VL53L0X_DevData_t Data;     /* состояние API — его поле, по имени Data */
    uint8_t I2cDevAddr;         /* адрес на шине, 7 бит (у API ST в SetDeviceAddress — 8 бит) */
} VL53L0X_Dev_t;
typedef VL53L0X_Dev_t *VL53L0X_DEV;

#define PALDevDataGet(Dev, field) (Dev->Data.field)
#define PALDevDataSet(Dev, field, data) ((Dev->Data.field) = (data))

VL53L0X_Error VL53L0X_WriteMulti(VL53L0X_DEV Dev, uint8_t index, uint8_t *pdata, uint32_t count);
VL53L0X_Error VL53L0X_ReadMulti(VL53L0X_DEV Dev, uint8_t index, uint8_t *pdata, uint32_t count);
VL53L0X_Error VL53L0X_WrByte(VL53L0X_DEV Dev, uint8_t index, uint8_t data);
VL53L0X_Error VL53L0X_WrWord(VL53L0X_DEV Dev, uint8_t index, uint16_t data);
VL53L0X_Error VL53L0X_WrDWord(VL53L0X_DEV Dev, uint8_t index, uint32_t data);
VL53L0X_Error VL53L0X_RdByte(VL53L0X_DEV Dev, uint8_t index, uint8_t *data);
VL53L0X_Error VL53L0X_RdWord(VL53L0X_DEV Dev, uint8_t index, uint16_t *data);
VL53L0X_Error VL53L0X_RdDWord(VL53L0X_DEV Dev, uint8_t index, uint32_t *data);
VL53L0X_Error VL53L0X_UpdateByte(VL53L0X_DEV Dev, uint8_t index, uint8_t AndData, uint8_t OrData);
VL53L0X_Error VL53L0X_PollingDelay(VL53L0X_DEV Dev);
#endif

tof100c.h — наш слой: шина, профили, состояние датчика.

/* Раздел 100б: несколько VL53L0X на одной шине I2C поверх API ST (STSW-IMG005 1.0.2, копия из hal_st Zephyr).
 * Регистры трогает только API ST; наш слой — платформа API (функции VL53L0X_WrByte и др.), включение датчиков
 * по одному через XSHUT со сменой адреса, инициализация в порядке драйвера Zephyr, режимы, готовность по GPIO1
 * и фильтр. Шину и выводы даёт прошивка (fw100c.c — I2C1 STM32F103) или стенд (host100c.c — строки к модели). */
#ifndef TOF100C_H
#define TOF100C_H
#include <stdint.h>
#include "vl53l0x_api.h"

#define TOF_ADDR_DEFAULT 0x29u      /* адрес после включения — VL53L0X_INITIAL_ADDR в драйвере Zephyr */
#define TOF_REG_ID       0xC0u      /* слово 0xEEAA: VL53L0X_REG_WHO_AM_I и VL53L0X_CHIP_ID там же */
#define TOF_CHIP_ID      0xEEAAu
#define TOF_BOOT_US      1200u      /* tBOOT: загрузка до 1,2 мс после отпускания XSHUT (там же) */
#define TOF_POLL_US      1000u      /* пауза между опросами готовности — наше значение */
#define TOF_MED          5u         /* окно медианы */

/* ── шина и выводы: реализует прошивка или стенд ── */
int  tof_i2c_write(uint8_t addr7, const uint8_t *buf, uint32_t n);    /* 0 — ведомый подтвердил всё */
int  tof_i2c_read(uint8_t addr7, uint8_t reg, uint8_t *buf, uint32_t n);
void tof_delay_us(uint32_t us);
void tof_xshut(uint8_t idx, int run);   /* run = 1 — отпустить XSHUT (датчик работает), 0 — прижать к земле */
void tof_idle(void);                    /* ждать прерывания: прошивка — WFI, стенд — до спада GPIO1 в модели */

typedef struct {
    uint16_t mm;            /* RangeMilliMeter */
    uint8_t status;         /* RangeStatus API: 0 — годен, 1 сигма, 2 сигнал, 3 мин. дальность, 4 фаза, 5 сбой */
    uint32_t signal, ambient;   /* MCPS, 16.16 */
    uint32_t sigma;         /* оценка σ, мм, 16.16 — считает API */
} tof_meas_t;

typedef struct {
    uint16_t win[TOF_MED];
    uint8_t n, pos;
    uint32_t taken, dropped;
} tof_filt_t;

typedef struct {
    uint32_t budget_us;             /* время на одно измерение */
    uint8_t vcsel_pre, vcsel_final; /* периоды импульса лазера, такты PLL */
    FixPoint1616_t signal_min;      /* предел сигнала, MCPS */
    FixPoint1616_t sigma_max;       /* предел σ, мм */
} tof_profile_t;

typedef struct {
    VL53L0X_Dev_t st;               /* состояние API ST */
    uint8_t idx;                    /* номер вывода XSHUT и GPIO1 */
    volatile uint8_t irq;           /* GPIO1 упал — ставит обработчик EXTI */
    VL53L0X_Error err;              /* код API на шаге, где встали */
    uint32_t spads;                 /* итог калибровки опорных SPAD */
    uint8_t aperture;
    tof_filt_t f;
} tof_t;

extern const tof_profile_t TOF_DEFAULT, TOF_LONG;

int  tof_probe(uint8_t addr7);
int  tof_wake(tof_t *t, uint8_t idx, uint8_t addr7);
void tof_sleep_all(uint8_t n);
int  tof_init(tof_t *t, const tof_profile_t *p);
int  tof_single(tof_t *t, tof_meas_t *m, int use_irq);
int  tof_start(tof_t *t, uint32_t period_ms);
int  tof_stop(tof_t *t);
int  tof_fetch(tof_t *t, tof_meas_t *m);
void tof_on_gpio1(tof_t *t);
int  tof_filter(tof_filt_t *f, const tof_meas_t *m);
#endif

tof100c.c — платформа, включение по одному, инициализация, измерения, фильтр.

/* Раздел 100б: платформа API ST и слой «несколько датчиков на одной шине». Общий для прошивки и стенда. */
#include <string.h>
#include "tof100c.h"

/* Профили. Обычный — то, что API ставит сам: пределы 0,25 MCPS и 18 мм (DataInit), периоды 14 и 10 (таблица
 * настроек). Дальний — как в драйвере Zephyr: 0,1 MCPS, 60 мм, периоды 18 и 14. Бюджет у обоих 33 мс (Zephyr). */
const tof_profile_t TOF_DEFAULT = {33000u, 14u, 10u, (FixPoint1616_t)(65536u / 4u), (FixPoint1616_t)(18u * 65536u)};
const tof_profile_t TOF_LONG = {33000u, 18u, 14u, (FixPoint1616_t)(65536u / 10u), (FixPoint1616_t)(60u * 65536u)};

/* ── платформа API ST: каждое обращение к регистру — одна транзакция I2C ── */
#define TOF_MULTI_MAX 16u

VL53L0X_Error VL53L0X_WriteMulti(VL53L0X_DEV Dev, uint8_t index, uint8_t *pdata, uint32_t count)
{
    uint8_t b[1u + TOF_MULTI_MAX];
    if (count > TOF_MULTI_MAX)
        return VL53L0X_ERROR_INVALID_PARAMS;
    b[0] = index;
    memcpy(b + 1, pdata, count);
    return tof_i2c_write(Dev->I2cDevAddr, b, count + 1u) ? VL53L0X_ERROR_CONTROL_INTERFACE : VL53L0X_ERROR_NONE;
}

VL53L0X_Error VL53L0X_ReadMulti(VL53L0X_DEV Dev, uint8_t index, uint8_t *pdata, uint32_t count)
{
    return tof_i2c_read(Dev->I2cDevAddr, index, pdata, count) ? VL53L0X_ERROR_CONTROL_INTERFACE : VL53L0X_ERROR_NONE;
}

static VL53L0X_Error wr_be(VL53L0X_DEV Dev, uint8_t index, uint32_t v, uint32_t n)
{
    uint8_t b[4];
    for (uint32_t i = 0; i < n; i++)               /* старший байт первым */
        b[i] = (uint8_t)(v >> (8u * (n - 1u - i)));
    return VL53L0X_WriteMulti(Dev, index, b, n);
}

static VL53L0X_Error rd_be(VL53L0X_DEV Dev, uint8_t index, uint32_t *v, uint32_t n)
{
    uint8_t b[4];
    VL53L0X_Error e = VL53L0X_ReadMulti(Dev, index, b, n);
    *v = 0;
    for (uint32_t i = 0; i < n; i++)
        *v = (*v << 8) | b[i];
    return e;
}

VL53L0X_Error VL53L0X_WrByte(VL53L0X_DEV Dev, uint8_t index, uint8_t data) { return wr_be(Dev, index, data, 1); }
VL53L0X_Error VL53L0X_WrWord(VL53L0X_DEV Dev, uint8_t index, uint16_t data) { return wr_be(Dev, index, data, 2); }
VL53L0X_Error VL53L0X_WrDWord(VL53L0X_DEV Dev, uint8_t index, uint32_t data) { return wr_be(Dev, index, data, 4); }

VL53L0X_Error VL53L0X_RdByte(VL53L0X_DEV Dev, uint8_t index, uint8_t *data)
{
    uint32_t v;
    VL53L0X_Error e = rd_be(Dev, index, &v, 1);
    *data = (uint8_t)v;
    return e;
}

VL53L0X_Error VL53L0X_RdWord(VL53L0X_DEV Dev, uint8_t index, uint16_t *data)
{
    uint32_t v;
    VL53L0X_Error e = rd_be(Dev, index, &v, 2);
    *data = (uint16_t)v;
    return e;
}

VL53L0X_Error VL53L0X_RdDWord(VL53L0X_DEV Dev, uint8_t index, uint32_t *data) { return rd_be(Dev, index, data, 4); }

VL53L0X_Error VL53L0X_UpdateByte(VL53L0X_DEV Dev, uint8_t index, uint8_t AndData, uint8_t OrData)
{
    uint8_t v;
    VL53L0X_Error e = VL53L0X_RdByte(Dev, index, &v);
    return e ? e : VL53L0X_WrByte(Dev, index, (uint8_t)((v & AndData) | OrData));
}

VL53L0X_Error VL53L0X_PollingDelay(VL53L0X_DEV Dev)
{
    (void)Dev;
    tof_delay_us(TOF_POLL_US);
    return VL53L0X_ERROR_NONE;
}

/* ── включение по одному и смена адреса ── */
#define STEP(n, call) do { VL53L0X_Error e_ = (call); if (e_ != VL53L0X_ERROR_NONE) { t->err = e_; return (n); } } while (0)

int tof_probe(uint8_t addr7)
{
    return tof_i2c_write(addr7, 0, 0) == 0;        /* только адрес: ответил ли кто-нибудь ACK */
}

void tof_sleep_all(uint8_t n)
{
    for (uint8_t i = 0; i < n; i++)
        tof_xshut(i, 0);                           /* все в сброс: после него адрес снова 0x29 */
    tof_delay_us(TOF_BOOT_US);
}

/* Отпустить XSHUT одного датчика, дождаться загрузки, проверить, что по 0x29 отвечает VL53L0X,
 * и перевести его на свой адрес. Остальные датчики в это время держат XSHUT внизу и молчат. */
int tof_wake(tof_t *t, uint8_t idx, uint8_t addr7)
{
    uint16_t id = 0;
    memset(t, 0, sizeof *t);
    t->idx = idx;
    t->st.I2cDevAddr = TOF_ADDR_DEFAULT;
    tof_xshut(idx, 1);
    tof_delay_us(TOF_BOOT_US);
    STEP(1, VL53L0X_RdWord(&t->st, TOF_REG_ID, &id));
    if (id != TOF_CHIP_ID)
        return 2;
    if (addr7 != TOF_ADDR_DEFAULT) {
        STEP(3, VL53L0X_SetDeviceAddress(&t->st, (uint8_t)(addr7 << 1)));   /* API ждёт 8-битный адрес */
        t->st.I2cDevAddr = addr7;
        id = 0;
        STEP(4, VL53L0X_RdWord(&t->st, TOF_REG_ID, &id));
        if (id != TOF_CHIP_ID)
            return 5;
    }
    return 0;
}

/* Инициализация — в порядке драйвера Zephyr (vl53l0x_start и vl53l0x_setup_single_shot). */
int tof_init(tof_t *t, const tof_profile_t *p)
{
    uint8_t vhv, phase;
    STEP(10, VL53L0X_DataInit(&t->st));
    STEP(11, VL53L0X_StaticInit(&t->st));
    STEP(12, VL53L0X_PerformRefCalibration(&t->st, &vhv, &phase));
    STEP(13, VL53L0X_PerformRefSpadManagement(&t->st, &t->spads, &t->aperture));
    STEP(14, VL53L0X_SetDeviceMode(&t->st, VL53L0X_DEVICEMODE_SINGLE_RANGING));
    STEP(15, VL53L0X_SetLimitCheckEnable(&t->st, VL53L0X_CHECKENABLE_SIGMA_FINAL_RANGE, 1));
    STEP(16, VL53L0X_SetLimitCheckEnable(&t->st, VL53L0X_CHECKENABLE_SIGNAL_RATE_FINAL_RANGE, 1));
    STEP(17, VL53L0X_SetLimitCheckValue(&t->st, VL53L0X_CHECKENABLE_SIGNAL_RATE_FINAL_RANGE, p->signal_min));
    STEP(18, VL53L0X_SetLimitCheckValue(&t->st, VL53L0X_CHECKENABLE_SIGMA_FINAL_RANGE, p->sigma_max));
    STEP(19, VL53L0X_SetMeasurementTimingBudgetMicroSeconds(&t->st, p->budget_us));
    STEP(20, VL53L0X_SetVcselPulsePeriod(&t->st, VL53L0X_VCSEL_PERIOD_PRE_RANGE, p->vcsel_pre));
    STEP(21, VL53L0X_SetVcselPulsePeriod(&t->st, VL53L0X_VCSEL_PERIOD_FINAL_RANGE, p->vcsel_final));
    return 0;
}

/* ── измерения ── */
void tof_on_gpio1(tof_t *t)
{
    t->irq = 1;
}

/* Забрать результат: 12 байт с 0x14 разбирает API, оценку σ он же считает при этом; затем снять прерывание —
 * пока его не снять, GPIO1 стоит внизу. */
int tof_fetch(tof_t *t, tof_meas_t *m)
{
    VL53L0X_RangingMeasurementData_t r;
    FixPoint1616_t sigma = 0;
    memset(&r, 0, sizeof r);                       /* API смотрит в RangeStatus прошлого замера — обнуляем */
    STEP(30, VL53L0X_GetRangingMeasurementData(&t->st, &r));
    STEP(31, VL53L0X_GetLimitCheckCurrent(&t->st, VL53L0X_CHECKENABLE_SIGMA_FINAL_RANGE, &sigma));
    STEP(32, VL53L0X_ClearInterruptMask(&t->st, 0));
    m->mm = r.RangeMilliMeter;
    m->status = r.RangeStatus;
    m->signal = r.SignalRateRtnMegaCps;
    m->ambient = r.AmbientRateRtnMegaCps;
    m->sigma = sigma;
    return 0;
}

/* Однократное измерение. use_irq = 0 — опрос регистра готовности через паузы TOF_POLL_US (так делает
 * PerformSingleMeasurement); use_irq = 1 — ждать спада GPIO1, шина в это время свободна. */
int tof_single(tof_t *t, tof_meas_t *m, int use_irq)
{
    STEP(40, VL53L0X_SetDeviceMode(&t->st, VL53L0X_DEVICEMODE_SINGLE_RANGING));
    if (!use_irq) {
        STEP(41, VL53L0X_PerformSingleMeasurement(&t->st));
    } else {
        t->irq = 0;
        STEP(42, VL53L0X_StartMeasurement(&t->st));
        while (!t->irq)
            tof_idle();
    }
    return tof_fetch(t, m);
}

/* Непрерывный режим: period_ms = 0 — измерения подряд, иначе одно на period_ms. */
int tof_start(tof_t *t, uint32_t period_ms)
{
    if (period_ms == 0) {
        STEP(50, VL53L0X_SetDeviceMode(&t->st, VL53L0X_DEVICEMODE_CONTINUOUS_RANGING));
    } else {
        STEP(51, VL53L0X_SetDeviceMode(&t->st, VL53L0X_DEVICEMODE_CONTINUOUS_TIMED_RANGING));
        STEP(52, VL53L0X_SetInterMeasurementPeriodMilliSeconds(&t->st, period_ms));
    }
    t->irq = 0;
    STEP(53, VL53L0X_StartMeasurement(&t->st));
    return 0;
}

int tof_stop(tof_t *t)
{
    STEP(60, VL53L0X_StopMeasurement(&t->st));
    return 0;
}

/* ── фильтр: недостоверные статусы — мимо, из годных — медиана последних TOF_MED ── */
int tof_filter(tof_filt_t *f, const tof_meas_t *m)
{
    uint16_t s[TOF_MED];
    if (m->status != 0) {
        f->dropped++;
    } else {
        f->taken++;
        f->win[f->pos] = m->mm;
        f->pos = (uint8_t)((f->pos + 1u) % TOF_MED);
        if (f->n < TOF_MED)
            f->n++;
    }
    if (f->n == 0)
        return -1;
    memcpy(s, f->win, sizeof s);
    for (uint8_t i = 1; i < f->n; i++)             /* вставками: окно маленькое */
        for (uint8_t j = i; j > 0 && s[j - 1] > s[j]; j--) {
            uint16_t x = s[j];
            s[j] = s[j - 1];
            s[j - 1] = x;
        }
    return s[f->n / 2];
}

fw100c.c — прошивка STM32F103: I2C1, XSHUT, EXTI, три датчика.

/* fw100c.c — раздел 100б: три VL53L0X на одной шине I2C1 STM32F103 (BluePill), 72 МГц.
 * I2C1: PB6 — SCL, PB7 — SDA, 400 кГц (быстрый режим, как в 17.3). XSHUT датчиков — PB12, PB13, PB14,
 * выходы с открытым стоком: 0 — датчик в сбросе, 1 — отпущен (поднимает подтяжка). GPIO1 датчиков — PA1, PA2,
 * PA3, входы с подтяжкой, EXTI1…EXTI3 по спаду: API ставит GPIO1 «новое измерение, активный ноль».
 * После включения датчики по одному получают адреса 0x30, 0x31, 0x32 и меряют каждые 50 мс.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); APB1 = 36 МГц. */
#include "stm32f10x.h"
#include "tof100c.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 PERIOD_MS 50u

_Static_assert(I2C_FAST_CCR == 30u, "CCR = 30: 36 МГц / (3 · 30) = 400 кГц");
_Static_assert(I2C_FAST_TRISE == 11u, "TRISE = 11");

static tof_t T[3];
static const uint8_t ADDR[3] = {0x30u, 0x31u, 0x32u};
volatile int16_t dist_mm[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;
}

int tof_i2c_write(uint8_t addr7, const uint8_t *buf, uint32_t n)
{
    if (start((uint8_t)(addr7 << 1)))
        return 1;
    (void)I2C1->SR1;
    (void)I2C1->SR2;                           /* ADDR снят: SR1, затем SR2 (17.4) */
    for (uint32_t i = 0; i < n; i++) {
        if (wait1(I2C_SR1_TXE)) {
            stop();
            return 1;
        }
        I2C1->DR = buf[i];
    }
    int e = n ? wait1(I2C_SR1_BTF) : 0;
    stop();
    return e;
}

/* Приём по RM0008 (ведущий приёмник): один байт — ACK снять до снятия ADDR; два — POS; больше двух — последние
 * три байта через BTF, ACK снять перед предпоследним. */
int tof_i2c_read(uint8_t addr7, uint8_t reg, uint8_t *buf, uint32_t n)
{
    if (start((uint8_t)(addr7 << 1)))
        return 1;
    (void)I2C1->SR1;
    (void)I2C1->SR2;
    if (wait1(I2C_SR1_TXE)) {
        stop();
        return 1;
    }
    I2C1->DR = reg;
    if (wait1(I2C_SR1_BTF) || 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 tof_delay_us(uint32_t us)
{
    uint32_t t0 = DWT->CYCCNT, n = us * (CPU_HZ / 1000000u);
    while (DWT->CYCCNT - t0 < n) {
    }
}

void tof_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 всё равно разбудит. */
void tof_idle(void)
{
    __disable_irq();
    if (!any_irq())
        __WFI();
    __enable_irq();
}

void EXTI1_IRQHandler(void)
{
    EXTI->PR = EXTI_PR_PR1;
    tof_on_gpio1(&T[0]);
}

void EXTI2_IRQHandler(void)
{
    EXTI->PR = EXTI_PR_PR2;
    tof_on_gpio1(&T[1]);
}

void EXTI3_IRQHandler(void)
{
    EXTI->PR = EXTI_PR_PR3;
    tof_on_gpio1(&T[2]);
}

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);
}

int main(void)
{
    board_init();
    tof_sleep_all(3);
    for (uint8_t i = 0; i < 3; i++) {
        int rc = tof_wake(&T[i], i, ADDR[i]);
        if (!rc)
            rc = tof_init(&T[i], &TOF_DEFAULT);
        if (!rc)
            rc = tof_start(&T[i], PERIOD_MS);
        if (rc)
            tof_xshut(i, 0);                   /* не ответил — обратно в сброс, чтобы не занял 0x29 */
        dist_mm[i] = (int16_t)(rc ? -rc : 0);
    }
    for (;;) {
        tof_idle();
        for (uint8_t i = 0; i < 3; i++) {
            tof_meas_t m;
            if (!T[i].irq)
                continue;
            T[i].irq = 0;
            if (tof_fetch(&T[i], &m) == 0) {
                int f = tof_filter(&T[i].f, &m);
                if (f >= 0)
                    dist_mm[i] = (int16_t)f;
            }
        }
    }
}

/* ── вектора и запуск (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» — шунт и калибровочный регистр, датчик Холла и его ноль посередине шкалы.

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