Блок‑схема устройства

Логика на уровне блоков:

Текстовая блок‑диаграмма:

[12V авто сеть]
      |
[DC-DC 12->5V c фильтрацией]
      |
 +----+-----------------------------+
 |                                  |
[LiDAR (TFmini-S/TF03)]        [RP2040 + CircuitPython]
       | UART TX/RX                   |
       +------------------------------+
                         |
              [buzzer + LED/индикаторы]

UART на RP2040 в CircuitPython поднимается через busio.UART, пины укажешь под свою плату (например, GP4/GP5). [youtube](https://www.youtube.com/watch?v=tvqMbFoBlVk)

Протокол LiDAR (TFmini‑подобный)

У TFmini/TFmini‑S базовый протокол: пакеты по 9 байт, заголовок 0x59 0x59, потом distance low/high, strength, температура, checksum. [blog.csdn](https://blog.csdn.net/shanghaiwren/article/details/152655977)
У TF03 похожая идея, но другой формат и длина пакета (смотри актуальный мануал TF03 UART/CAN). [en.benewake](https://en.benewake.com/uploadfiles/2024/04/20240426134845102.pdf)

Ниже — пример под “классический” TFmini‑совместимый поток на 115200 бод, с чтением фреймов 9 байт. [forums.raspberrypi](https://forums.raspberrypi.com/viewtopic.php?t=327477)

Пример CircuitPython‑кода для RP2040 (UART + пороги)

Этот код:

  • Инициализирует UART.
  • Читает поток пакетов TFmini‑формата.
  • Выделяет distance_cm.
  • В зависимости от дистанции управляет буззером и тремя светодиодами.
import time
import board
import busio
import digitalio

# ---------- Пины под свою плату ----------
# Пример для Raspberry Pi Pico:
UART_TX_PIN = board.GP4
UART_RX_PIN = board.GP5

BUZZER_PIN = board.GP10
LED_FAR_PIN = board.GP11      # "далеко" (зелёный)
LED_MID_PIN = board.GP12      # "средне" (жёлтый)
LED_NEAR_PIN = board.GP13     # "близко" (красный)

# ---------- Пороговые дистанции в метрах ----------
FAR_THRESHOLD_M = 30      # >30 м — почти тишина
MID_THRESHOLD_M = 15      # 15–30 м — редкие сигналы
NEAR_THRESHOLD_M = 5      # 5–15 м — частые сигналы, <5 м — постоянный

# ---------- Инициализация UART ----------
# Для TFmini/TFmini-S/многих Benewake: 115200 бод по умолчанию
uart = busio.UART(
    UART_TX_PIN,
    UART_RX_PIN,
    baudrate=115200,
    timeout=0.05,
)

# ---------- Инициализация вывода ----------
buzzer = digitalio.DigitalInOut(BUZZER_PIN)
buzzer.direction = digitalio.Direction.OUTPUT
buzzer.value = False

led_far = digitalio.DigitalInOut(LED_FAR_PIN)
led_far.direction = digitalio.Direction.OUTPUT
led_mid = digitalio.DigitalInOut(LED_MID_PIN)
led_mid.direction = digitalio.Direction.OUTPUT
led_near = digitalio.DigitalInOut(LED_NEAR_PIN)
led_near.direction = digitalio.Direction.OUTPUT

def set_leds_mode(mode: str) -> None:
    # mode: "far", "mid", "near", "alarm", "off"
    if mode == "far":
        led_far.value = True
        led_mid.value = False
        led_near.value = False
    elif mode == "mid":
        led_far.value = False
        led_mid.value = True
        led_near.value = False
    elif mode == "near":
        led_far.value = False
        led_mid.value = False
        led_near.value = True
    elif mode == "alarm":
        # можно мигать всеми сразу, но тут просто все включены
        led_far.value = True
        led_mid.value = True
        led_near.value = True
    else:
        led_far.value = False
        led_mid.value = False
        led_near.value = False

def beep(duration: float) -> None:
    buzzer.value = True
    time.sleep(duration)
    buzzer.value = False

# ---------- Парсер пакетов TFmini‑формата ----------
def read_distance_cm():
    """
    Читает поток UART и возвращает дистанцию в сантиметрах (int) или None.
    Ожидает пакеты длиной 9 байт формата:
    0: 0x59, 1: 0x59, 2: Dist_L, 3: Dist_H, 4: Strength_L, 5: Strength_H,
    6: Temp_L, 7: Temp_H, 8: Checksum.
    """
    # Пытаемся найти заголовок 0x59 0x59 в потоке
    data = uart.read(1)
    if not data:
        return None

    if data[0] != 0x59:
        return None

    data2 = uart.read(1)
    if not data2:
        return None

    if data2[0] != 0x59:
        return None

    # Читаем ещё 7 байт до конца фрейма
    rest = uart.read(7)
    if not rest or len(rest) != 7:
        return None

    frame = bytes([0x59, 0x59]) + rest

    # Контрольная сумма: сумма первых 8 байт & 0xFF == 9-й байт
    checksum_calc = sum(frame[0:8]) & 0xFF
    checksum_frame = frame [youtube](https://www.youtube.com/watch?v=tvqMbFoBlVk)
    if checksum_calc != checksum_frame:
        return None

    dist_cm = frame [en.benewake](https://en.benewake.com/uploadfiles/2024/04/20240426134845102.pdf) + (frame [blog.csdn](https://blog.csdn.net/shanghaiwren/article/details/152655977) << 8)
    # strength = frame [mybotshop](https://www.mybotshop.de/Datasheet/Benewake_TF03_Manual.pdf) + (frame [mouser](https://www.mouser.com/catalog/specsheets/Benewake_10152020_TF03-100.pdf) << 8)
    # temperature = (frame [mybotshop](https://www.mybotshop.de/Datasheet/Benewake_TFMini-S_Datasheet.pdf) + (frame [worldronemarket](https://www.worldronemarket.com/benewake-tf03-rangefinder-100-m-datasheet/) << 8)) / 8 - 256

    return dist_cm

# ---------- Главный цикл ----------
last_beep_time = 0.0

while True:
    dist_cm = read_distance_cm()
    now = time.monotonic()

    if dist_cm is None or dist_cm == 0:
        # нет корректных данных
        set_leds_mode("off")
        buzzer.value = False
        continue

    dist_m = dist_cm / 100.0

    # Простейший фильтр: с ограничением частоты чтения
    # (датчик и так выдаёт ~100–1000 Гц, нам этого выше крыши)
    # Логику порогов можно усложнить, добавив усреднение/гистерезис.

    if dist_m > FAR_THRESHOLD_M:
        # далеко — еле-еле пищим
        set_leds_mode("far")
        # допустим, один короткий писк раз в 3 секунды
        if now - last_beep_time > 3.0:
            beep(0.03)
            last_beep_time = now

    elif MID_THRESHOLD_M < dist_m <= FAR_THRESHOLD_M:
        # середина — редкие сигналы
        set_leds_mode("mid")
        # короткий писк раз в 1 секунду
        if now - last_beep_time > 1.0:
            beep(0.05)
            last_beep_time = now

    elif NEAR_THRESHOLD_M < dist_m <= MID_THRESHOLD_M:
        # близко — частые короткие сигналы
        set_leds_mode("near")
        # пик раз в 0.3 сек
        if now - last_beep_time > 0.3:
            beep(0.08)
            last_beep_time = now

    else:
        # очень близко: < NEAR_THRESHOLD_M
        set_leds_mode("alarm")
        # почти непрерывный тон (можно прерывать для эффекта)
        buzzer.value = True
        # чтобы не забить основной цикл, чуть спим
        time.sleep(0.05)
        continue

    # немного разгружаем цикл
    time.sleep(0.01)

Основу read_distance_cm() я опирал на типичный разбор TFmini‑пакета: заголовок 0x59 0x59, 9‑байтовый фрейм, checksum как сумма первых 8 байт. [docs.circuitpython](https://docs.circuitpython.org/projects/tfmini/en/latest/)

Что подправить под TF03

У TF03 другая длина и формат фрейма (обычно 10 байт с заголовком 0x59 0x59 или 0x57 0x0A, зависящий от режима). Это нужно перепроверить по свежему TF03 UART/CAN User Manual и даташиту. Там подобная логика: [mybotshop](https://www.mybotshop.de/Datasheet/Benewake_TF03_Manual.pdf)

  • ищем подпись,
  • читаем фиксированное количество байт,
  • проверяем checksum,
  • парсим distance/strength.

Тебе останется заменить read_distance_cm() на парсер конкретного формата из мануала TF03 и поправить baudrate, если он другой. [en.benewake](https://en.benewake.com/uploadfiles/2024/04/20240426134845102.pdf)

Железо/питание и авто‑нюансы

  • Запитывать всё через DC‑DC с входом 9–36 В и защитой от бросков, плюс TVS/RC‑фильтр на 12 В после зажигания. [directindustry](https://www.directindustry.com/prod/benewake/product-201153-2420998.html)
  • LiDAR лучше ставить за стеклом/пластиком с известной прозрачностью в ИК по даташиту; многие Benewake это допускают, но с поправкой по дальности. [mouser](https://www.mouser.com/catalog/specsheets/Benewake_10152020_TF03-100.pdf)
  • RP2040 держать подальше от “грязной массы”; общая GND всё равно нужна, но разводка звёздочкой и фильтры сильно помогают.

[file-name 000378_2026-06-22_10-24-46.txt]