Embedded HandbookApps
All sections › XI. Sensors: connection, circuits, drivers
STM32F103 · level intermediate · 37 min read
Open in the interactive handbook

100б. VL53L0X laser rangefinder: time of flight over I2C, accuracy and speed modes, several sensors on one bus

In section 100 distance was measured with sound: the HC-SR04 answers with a pulse, and one metre gives an echo of 5824 µs. In section 100а a radar split the room into gates of 0.7 m and looked for a person in them. The VL53L0X from ST measures with light: a tiny laser sends pulses, an array of photon counters catches the reflection, and the sensor itself turns the time of flight into millimetres. Outside there is the I2C bus, the XSHUT pin for reset and the GPIO1 pin "measurement ready".

Here: why the time of flight of light means picoseconds and how the sensor gets them; the pins and address of the module; why ST gives not a register description but a C library, and what it does at power-up; single and continuous measurement, the timing budget and noise; readiness on GPIO1 with an interrupt versus polling; three sensors on one bus — powering them up one by one and changing the address; filtering, difficult surfaces, sunlight and glass; a comparison with the HC-SR04 and the radar.

Prerequisites: the ST library for the VL53L0X (API version 1.0.2, STSW-IMG005) — its copy lives in the hal_st module of the Zephyr project under the BSD-3-Clause license, — and the sensor driver in Zephyr itself (Apache-2.0). Register addresses, the address 0x29, the boot time, the initialization order and the profiles come from there. The description of the sensor itself (datasheet) was not used in preparing this section: the supply of the module, the maximum range and the field of view are not given here. The bench is the ST library and our layer, built with gcc and run against a model of the sensor in Python; the same code was built with armclang for the STM32F103 and has not been run on a board. I²C — section 17, pin interrupts — 7.5, connecting modules — 95.

Terms used in the documentation

Term Meaning
time of flight (ToF) the time light takes to fly to the target and back
VCSEL (vertical-cavity surface-emitting laser) a vertical-cavity laser — the source of the pulses
SPAD (single photon avalanche diode) an avalanche photodiode that counts single photons
reference SPAD reference SPADs: their number is chosen for the reference signal
ranging, single / continuous / timed ranging a measurement; one-shot / back to back / by timer
measurement timing budget the time allowed for one measurement
inter-measurement period the period between measurements in timed mode
range status the measurement status: valid, or why not
sigma estimate the σ estimate — the expected spread of the range
signal rate, ambient rate (MCPS) counts of reflection and background, millions per second
crosstalk (XTalk) crosstalk: light reflected by the cover glass
VHV, phase calibration the VHV and phase calibrations
XSHUT (xshutdown) the reset input, active low
new sample ready (GPIO1) the "measurement ready" output

100б.1 Time of flight: why picoseconds

Light travels at 299 792 458 m/s. A target at one metre means 2 m of path and 6.671 ns; one millimetre of range is 6.671 ps. In the ST library this value is written as the constant cTOF_per_mm_ps — 6.6 ps per millimetre. The sound of the HC-SR04 needs 5.824 µs for the same millimetre: 873 000 times longer. Hence the main point: an STM32 timer cannot catch light. One tick at 72 MHz is 13.89 ns, and in it light travels a path equal to a range of 2082 mm.

The sensor has its own clock, but it is coarse too: in the library its PLL period is 1655 ps, light covers 496.2 mm in that time, that is 248.1 mm of range. The millimetres come from statistics: the SPADs count single photons, and the sensor averages their arrival time. ST does not disclose exactly how, but the σ estimate formula in the library tells the main thing — the spread falls with the number of caught photons. That is why accuracy is bought with time, and this shows in 100б.4.

Time inside the sensor is counted in macro periods: 2304 PLL ticks multiplied by the laser pulse period in ticks. With a period of 10 ticks the macro period is 38 131 ns, with 14 — 53 384 ns, with 18 — 68 636 ns. The timeouts of the measurement steps are set in macro periods, and through them the library turns the timing budget into registers.


100б.2 The VL53L0X module: pins, supply, address

Three VL53L0X on an STM32F103: on the left the microcontroller, PB6 and PB7 are the shared I2C1 bus at 400 kHz, PB12, PB13, PB14 go to the XSHUT pins of the sensors, PA1, PA2, PA3 take their GPIO1; on the right three modules, each with address 0x29 after power-up and 0x30, 0x31, 0x32 after the change; in the middle a light pulse to the target and back: a metre in 6.67 ns, a millimetre in 6.67 ps, in the library 6.6 ps, an STM32 tick of 13.89 ns equals 2082 mm, the sensor PLL tick of 1655 ps is 248 mm, sound is 873 000 times slower; at the bottom powering up one by one: XSHUT up, a wait of 1.2 ms, checking 0xEEAA at 0x29, writing the new address to 0x8A; all three take 9 transactions and 937.5 µs on the bus, 4.54 ms with the waits

Pin What it is Where in the firmware
SCL, SDA I²C, address 0x29 — line bytes 0x52 and 0x53 (17.2) PB6, PB7 — I2C1, 400 kHz
XSHUT reset, active low: 0 — the sensor sleeps and stays silent PB12, PB13, PB14 — open drain
GPIO1 "measurement ready", active low after initialization PA1, PA2, PA3 — EXTI on the falling edge
supply, ground per the module description —

The default address 0x29 and the boot time of up to 1.2 ms after releasing XSHUT are taken from the Zephyr driver (VL53L0X_INITIAL_ADDR, T_BOOT), and so is the check "this is a VL53L0X": the word at register 0xC0 equals 0xEEAA. In the Zephyr binding description XSHUT is called an active-low input. The library configures the GPIO1 output itself: in StaticInit it writes code 4 — "new measurement" — to register 0x0A and clears bit 4 of register 0x84, so the line goes low when a result is ready.

Honestly, little can be said about the supply. By default the library assumes that the I²C pins of the sensor run at 1.8 V, and for 2.8 V it has to be built with USE_I2C_2V8 — then it sets bit 0 of register 0x89. The Zephyr driver does not set this key, and neither does our build. Whether the module has its own regulator, pull-ups and level shifting depends on the module board; we have no schematic, so before connecting to 3.3 V this must be checked in the module description. XSHUT in the firmware is an open-drain output: a one only releases the line, and a pull-up (your own or on the module) has to raise it. Connecting modules to the STM32 in general — section 95.


100б.3 Why an "API" and not a register map: what power-up does

Most sensors in this part have a register table: write the mode, read the result. The VL53L0X has none — instead of describing the registers ST gives a C library, as the README of the hal_st module says plainly. The registers have not gone anywhere: the library accesses them over I²C, and its sources show where and what. Our code does the same as the Zephyr driver: only the library touches the registers, and we give it a platform — read and write functions and the polling pause.

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

Each library access is one I²C transaction, multi-byte values most significant byte first. Powering up the sensor follows the same order as in 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;
}

What happens inside, according to the library sources:

On the bench one such power-up takes 684 transactions and 2621 bytes: 63.3 ms of bus time at 400 kHz, 13 measurements inside and 264 ms together with them. How many reference SPADs the sensor picks depends on its reference signal; in the model it is set to 2.3 MCPS per SPAD, and the calibration stopped at 9 SPADs without aperture — this is a number of the model, not of the sensor.

The price of the library shows in the firmware for three sensors (armclang 6.24, -Os, Cortex-M3). The whole image has 12 988 bytes of code, of which 10 332 are the ST library, 1124 are our layer tof100c.c, 1316 are the firmware fw100c.c; the linker threw out everything unused from the library (vl53l0x_api.o had 5096 bytes of code, 3144 went into the image). Flash — 13 972 bytes, RAM — 1860 bytes without the stack. Of the RAM, 681 bytes are two library tables, the tuning table (243 bytes) and the interrupt threshold table (438), declared without const.


100б.4 Single and continuous: timing budget, noise and readiness

The mode is set by a write to register 0x00: 0x01 — one measurement, 0x02 — back to back without pauses, 0x04 — by timer, with the period in register 0x04 (the library multiplies the period in milliseconds by the oscillator calibration from register 0xF8). Writing 0x00 stops.

Timing budget. A measurement consists of steps, and the budget is their sum with overheads: 1910 µs for the start, 960 for the end, 660 for the pre-range, 550 for the final range. The remainder goes to the final range. The library does not accept less than 20 000 µs — for 19 000 it returned error −4. What came out after rounding to macro periods:

Requested, µs Got, µs Pre-range, µs Final range, µs
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

Noise. The library estimates the range spread itself — with a formula from the number of caught photons of reflection and background. In the model the noise is given by exactly this formula (ported from the library), so the numbers below show the behaviour of the formula, not the behaviour of a real sensor. The target with reflectance 1 is the one for which the library stores a reference signal of 1.42 MCPS at 400 mm; "grey" is 0.3 of it, "black" is 0.05; the signal falls as 1/d². 100 measurements per point, default profile:

Budget white, 600 mm grey, 300 mm black, 150 mm
20 ms 8.94 mm 9.47 mm 11.64 mm
33 ms 6.68 mm 5.58 mm 6.98 mm
66 ms 3.29 mm 3.05 mm 4.01 mm
200 ms 1.74 mm 1.90 mm 2.17 mm

Ten times more time gives roughly five times less spread. The estimate of the library itself on the white target is 9.50, 5.90, 3.69 and 1.97 mm.

Profiles. The default one is what the library sets: a signal limit of 0.25 MCPS, σ up to 18 mm, laser periods of 14 and 10 ticks (12…18 and 8…14 is what the library allows). The long-range one is as in the Zephyr driver: 0.1 MCPS, 60 mm, periods 18 and 14. The signal limit goes into register 0x44 in 9.7 format: 0x0020 is exactly 0.25, 0x000C is 0.094. If the signal falls as 1/d², then the white target is lost beyond 953 mm in the default profile and beyond 1557 mm in the long-range one, the grey one beyond 522 and 853 mm, the black one beyond 213 and 348 mm. A run with a budget of 33 ms confirmed this: white at 1200 mm — 0 valid of 100 and 100 of 100, at 1500 mm in long range — 93; grey at 600 mm — 0 and 100; black at 300 mm — 0 and 100. The price is spread: white at 600 mm — 6.68 mm in the default and 8.15 mm in the long-range profile. The model does not account for the pulse energy, so in it the long-range profile is only softer limits.

Readiness. The library waits for the end of a measurement by polling: it reads register 0x13 (ready when the lowest three bits equal 4), between reads it calls the platform pause — 1000 µs in our case — and gives up after 2000 attempts. One read of the register takes 97.5 µs of bus at 400 kHz. It can be done differently: start a measurement and wait for the falling edge of GPIO1 on EXTI, leaving the bus alone. Budget 33 ms, 20 measurements each:

Transactions per measurement Bytes Bus busy
single, polling 44 177 12.5 %
single, GPIO1 13 53 3.7 %
continuous, GPIO1 4 25 —

In continuous mode one result takes four transactions: 12 bytes from 0x14 (range, status, signal and background) and clearing the interrupt. Back to back the results came every 32 908 µs — exactly the budget, by a 100 ms timer — every 100 000 µs.

/* Забрать результат: 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;
}

Clearing the interrupt is mandatory. GPIO1 stays low until a one is written to register 0x0B — there will be no new falling edge, and EXTI will not fire again. In the model the sensor in continuous mode was not read for 200 ms: one falling edge, then five results overwritten by the next ones, and the line stayed low.

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

The handler only sets a flag, and the main loop does the reading: a bus access from the interrupt would collide with a transfer in progress at that moment. Sleeping happens with interrupts disabled: if the falling edge arrives between checking the flags and WFI, the pending interrupt still wakes the processor.


100б.5 Several sensors on one bus: XSHUT and address change

All VL53L0X answer at 0x29 after power-up. The address is changed by writing to register 0x8A, but only for whoever is listening right now — so the sensors are powered up one by one: all XSHUT low, then each one is released, the boot is waited for, it is checked that a VL53L0X really answers at 0x29, and it is moved to its own address.

/* Отпустить 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;
}

The library function VL53L0X_SetDeviceAddress expects an eight-bit address and itself divides it by 2 — the confusion from 17.2 in its pure form: passing it 0x30 means getting 0x18. The Zephyr driver passes the doubled address, and so do we.

On the bench the three sensors came up in 9 transactions and 39 bytes: 937.5 µs of bus and 4537.5 µs together with waiting for the boot; the scan found 0x30, 0x31, 0x32. The model also showed three mistakes:

In Zephyr the address change is enabled by the CONFIG_VL53L0X_RECONFIGURE_ADDRESS key, and then every sensor must have an XSHUT pin. Three sensors, each in continuous mode with its own GPIO1, gave 30.36 measurements per second per sensor on the bench with the bus 5.4 % busy and not a single lost result. The same three in turn, single-shot and with polling — 9.72 per second and 12.5 % of the bus.


100б.6 Filtering, difficult surfaces, light and glass; comparison with the HC-SR04 and the radar

Each measurement comes with a status. The library combines the sensor code (bits 6:3 of the byte at 0x14) and its own checks into one number: 0 — valid, 1 — σ above the limit, 2 — weak signal, 3 — too close, 4 — phase failure, 5 — device failure. The filter starts by throwing out everything non-zero; of what remains it takes the median of the last five.

/* ── фильтр: недостоверные статусы — мимо, из годных — медиана последних 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];
}

Continuous mode, budget 33 ms, 200 measurements each:

Scene (model) Valid Spread of valid After the median Worst error: all / median
white, 600 mm 200 6.08 mm 3.88 mm 16 / 10 mm
white, 600 mm, background 2.5 MCPS 52 (148 — status 1) 18.57 mm 12.96 mm 46 / 29 mm
white, 300 mm, background 2.5 MCPS 200 3.67 mm 2.00 mm 11 / 6 mm
white, 950 mm — at the limit 126 (74 — status 2) 9.51 mm 5.24 mm 32 / 12 mm
empty 0 (200 — status 2) — — 1606 mm

Sunlight is background: photons not related to the pulse. The σ formula grows with the ratio of background to signal, so background hits distant and dark targets: at 600 mm, of 200 measurements 52 stayed valid, at 300 mm — all of them. The empty scene is the main argument for checking the status: in the model the range without a reflection is scattered from 0 to 2606 mm. What a real sensor puts into the register when there is nothing to aim at, we did not check. At the very edge of the range the invalid values in the model are close to the truth (from 933 to 976 mm), but the sensor does not vouch for them.

Black. The signal is proportional to the reflectance: the black target is 20 times darker than the white one, and in the default profile it is lost already beyond 213 mm (100б.4). Shiny and tilted. A mirror surface reflects the pulse by the law of reflection, not in all directions: at an angle the light will not come back to the sensor, head-on it comes back too bright. Glass. A cover in front of the sensor itself reflects part of the pulse into the receiver — this is crosstalk. For this case the library has an offset calibration and crosstalk compensation (PerformOffsetCalibration, PerformXTalkCalibration, SetXTalkCompensationEnable); by default the compensation is off. The model gives no numbers for these three cases — this is a qualitative picture.

HC-SR04 (section 100) HLK-LD2420 (section 100а) VL53L0X
measures with sound, echo of 5824 µs per metre a 24 GHz radar light, 6.671 ns per metre
sees the nearest surface a person, even a motionless one the surface it is pointed at
result pulse length — measured by a timer gates of 0.7 m, "present / absent" millimetres over I²C
time per answer up to the 25 ms timeout 10 times a second budget from 20 ms
depends on temperature: +3.7 % in frost gate thresholds reflectance, background, budget
several on one device disturb each other — take turns — one address — XSHUT and address change

For "is anyone in the room" you need the radar; for "how far is the wall" over metres — ultrasound; for millimetres over tens of centimetres, for several sensors on one board and where sound dies on soft surfaces — the VL53L0X.


100б.7 Pitfalls

The second sensor is not found, and the first answers at someone else's address. All XSHUT were released at once: every sensor got the write to 0x8A. Power them up one by one.

After the change the address is 0x18 instead of 0x30. VL53L0X_SetDeviceAddress expects an eight-bit address and divides it by 2. Pass the doubled one.

The first access after XSHUT gets no acknowledge. The boot was not waited for: up to 1.2 ms.

After a supply dropout the sensor "disappeared". It went back to 0x29. Repeat the one-by-one power-up and the initialization.

The interrupt came once and never again. The interrupt was not cleared by writing to 0x0B: GPIO1 stays low, there is no new falling edge.

The range jumps by hundreds of millimetres. The status is ignored: invalid measurements are used. Everything with a non-zero status goes out.

In sunlight distant targets disappear, near ones are fine. Background inflates σ; the long-range profile, a larger budget or shade.

The bus is busy while the sensors are still measuring. Readiness polling: 44 transactions per measurement instead of 13. Wait for GPIO1.

A budget of 19 ms cannot be set. The library does not accept less than 20 000 µs and returns error −4 — check the return code.

More RAM is gone than expected. Two library tables without const — 681 bytes, plus the library state for every sensor.

Through glass the range is too short or stuck. Light reflects from the cover. Offset calibration and crosstalk compensation — or a sensor without glass.


100б.8 Summary


Full sources

The layer over the ST library and the firmware for three sensors. The library itself is an unchanged copy from hal_st, in _verify/tof100c/st/; the platform headers are our own, the bench host100c.c and the model model100c.py are in _verify/tof100c/.

vl53l0x_platform.h — the library platform: the device and the register access functions.

/* Раздел 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 — our layer: the bus, the profiles, the sensor state.

/* Раздел 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 — the platform, one-by-one power-up, initialization, measurements, filter.

/* Раздел 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 — the STM32F103 firmware: I2C1, XSHUT, EXTI, three sensors.

/* 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");

Next: section 101 "Current and voltage: INA219 and ACS712" — a shunt and the calibration register, a Hall sensor and its zero in the middle of the scale.

Open in the interactive handbook