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: выводы, питание, адрес
| Вывод | Что это | Куда в прошивке |
|---|---|---|
| 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;
}
Что происходит внутри, по исходникам библиотеки:
- DataInit переводит интерфейс в стандартный режим (запись
0x00в0x88), читает «стоп-переменную» со страницы 1 (регистр0x91; страницы переключает регистр0xFF) и включает проверки с пределами по умолчанию: σ не больше 18 мм, сигнал не меньше 0,25 MCPS. - StaticInit читает заводские данные — число опорных SPAD и карту годных
— через строб в регистре
0x83и окно0x90, грузит таблицу настроек (243 байта, помечена «update 02/11/2015_v36») и ставит GPIO1 на «новое измерение». - PerformRefCalibration — две короткие калибровки, VHV и фазы: запуск
записью
0x41и0x01в регистр0x00. - PerformRefSpadManagement подбирает опорные SPAD: начинает с трёх без апертуры и добавляет по одному, пока опорный сигнал не подойдёт ближе всего к 20 MCPS. Окно — 44 SPAD начиная с номера 180: двенадцать без апертуры (180…191), остальные с апертурой.
- Пределы, бюджет и периоды лазера. Смена периода сама пересчитывает бюджет и запускает ещё одну калибровку фазы.
На стенде одно такое включение — 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. Модель показала и три ошибки:
- Сразу после XSHUT. Обращение без паузы — нет подтверждения; через 1200 мкс датчик отвечает.
- Все XSHUT разом. Сканирование находит один адрес,
0x29. Запись нового адреса подтверждена — и все три переезжают на0x30. Проверка ID по0x30читает0xEEAA: у всех трёх одно слово, а открытый сток складывает их по «И» без следа. Коллизию так не поймать. - Сброс одного. Датчик №1 опустили и отпустили XSHUT — он снова на
0x29, сканирование показывает0x29,0x30,0x32. Адрес не хранится; после сброса или провала питания датчик включают заново. В модели это допущение; драйвер Zephyr поступает так же — после выхода из сна вызывает всю инициализацию снова.
В 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 Итоги
- Время пролёта света — 6,671 нс на метр и 6,671 пс на миллиметр; такт STM32 на 72 МГц равен 2082 мм. Датчик набирает миллиметры статистикой фотонов.
- Карты регистров нет: регистры трогает библиотека ST, наш код даёт ей чтение, запись и паузу. Включение одного датчика — 684 транзакции, 2621 байт и 13 измерений калибровки внутри; в прошивке библиотека — 10 332 байта кода из 12 988.
- Бюджет — от 20 000 мкс; разброс падает со временем: 8,94 мм за 20 мс и 1,74 мм за 200 мс на белой мишени в 600 мм (модель с формулой σ библиотеки).
- Профили: 0,25 MCPS и 18 мм против 0,1 MCPS и 60 мм. Белая мишень при сигнале как 1/d² — до 953 и до 1557 мм.
- Готовность — по GPIO1 на EXTI: 13 транзакций на однократное измерение
вместо 44, в непрерывном режиме 4. Прерывание снимать записью в
0x0B. - Несколько датчиков: адрес
0x29у всех; включать по одному через XSHUT, ждать 1,2 мс, писать новый адрес в0x8A. Три датчика — 30,36 измерения в секунду каждый при 5,4 % шины. - Статус ненулевой — измерение выбросить, из годных взять медиану: разброс 6,08 мм становится 3,88 мм.
Полные исходники
Слой над библиотекой 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» — шунт и калибровочный регистр, датчик Холла и его ноль посередине шкалы.