Arduino

Ультразвуковой датчик расстояния (HC-SR04): измерения в реальном времени на графике

Введение

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

Концепция

HC-SR04 измеряет расстояние по времени прохождения ультразвукового импульса до препятствия и обратно — прошивка подаёт короткий импульс на пин TRIG, затем измеряет длительность ответного импульса на пине ECHO через функцию pulseIn(), и по известной скорости звука вычисляет расстояние. На стороне Qt полученные значения накапливаются в скользящем буфере фиксированного размера и отображаются на линейном графике, обновляемом в реальном времени по мере поступления новых измерений.

Пример кода

// firmware/src/main.cpp — прошивка для HC-SR04
#define TRIG_PIN 9
#define ECHO_PIN 10

void setup()
{
    Serial.begin(9600);
    pinMode(TRIG_PIN, OUTPUT);
    pinMode(ECHO_PIN, INPUT);
    delay(100);
    Serial.println("READY");
}

void loop()
{
    static unsigned long lastMeasureTime = 0;

    if (millis() - lastMeasureTime >= 200) { // 5 измерений в секунду
        digitalWrite(TRIG_PIN, LOW);
        delayMicroseconds(2);
        digitalWrite(TRIG_PIN, HIGH);
        delayMicroseconds(10);
        digitalWrite(TRIG_PIN, LOW);

        // pulseIn с таймаутом — без него функция может зависнуть надолго,
        // если эхо-сигнал не вернулся (препятствие слишком далеко или отсутствует)
        const unsigned long durationMicros = pulseIn(ECHO_PIN, HIGH, 30000);

        if (durationMicros == 0) {
            Serial.println("ERROR:DISTANCE_TIMEOUT");
        } else {
            // Скорость звука ~343 м/с, импульс проходит расстояние туда и обратно
            const float distanceCm = (durationMicros * 0.0343) / 2.0;
            Serial.print("DISTANCE:");
            Serial.println(distanceCm);
        }

        lastMeasureTime = millis();
    }
}
// distancechartwidget.h — отображение измерений расстояния на графике в реальном времени
#pragma once
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QValueAxis>
#include "arduinoconnection.h"

class DistanceChartWidget : public QChartView
{
    Q_OBJECT
public:
    explicit DistanceChartWidget(ArduinoConnection *connection, QWidget *parent = nullptr)
        : QChartView(parent)
    {
        m_series = new QLineSeries();
        auto *chart = new QChart();
        chart->addSeries(m_series);
        chart->setTitle("Расстояние до препятствия (см)");

        m_axisX = new QValueAxis();
        m_axisX->setRange(0, m_maxPoints);
        m_axisX->setLabelFormat("%d");
        chart->addAxis(m_axisX, Qt::AlignBottom);
        m_series->attachAxis(m_axisX);

        m_axisY = new QValueAxis();
        m_axisY->setRange(0, 200); // максимальная дальность HC-SR04 — около 400 см, берём разумный запас
        chart->addAxis(m_axisY, Qt::AlignLeft);
        m_series->attachAxis(m_axisY);

        setChart(chart);
        setRenderHint(QPainter::Antialiasing);

        connect(connection, &ArduinoConnection::lineReceived, this, &DistanceChartWidget::onLineReceived);
    }

private slots:
    void onLineReceived(const QString &line)
    {
        if (!line.startsWith("DISTANCE:")) return;

        bool ok = false;
        const double distance = line.mid(9).toDouble(&ok);
        if (!ok) return;

        // Скользящее окно — старые точки сдвигаются влево, освобождая место для новых
        m_series->append(m_pointIndex, distance);
        if (m_series->count() > m_maxPoints) {
            m_series->remove(0);
            // Пересчёт диапазона оси X для эффекта "плавающего" окна последних измерений
            m_axisX->setRange(m_pointIndex - m_maxPoints, m_pointIndex);
        }
        m_pointIndex++;
    }

private:
    QLineSeries *m_series;
    QValueAxis *m_axisX;
    QValueAxis *m_axisY;
    int m_pointIndex = 0;
    static constexpr int m_maxPoints = 100;
};

Пояснения к коду

Прошивка реализует стандартный для HC-SR04 цикл измерения — короткий импульс на TRIG (с обязательным предварительным LOW для гарантии чистого фронта импульса), затем измерение длительности ответа на ECHO через pulseIn() с явным таймаутом в 30000 микросекунд — без этого таймаута функция могла бы блокироваться неопределённо долго, если эхо не вернулось вовсе (препятствие за пределами дальности датчика). Вычисление расстояния использует известную скорость звука и деление на два, поскольку измеренное время соответствует прохождению сигналом расстояния туда и обратно.

DistanceChartWidget реализует паттерн «скользящего окна» графика — при превышении m_maxPoints самая старая точка удаляется (m_series->remove(0)), а диапазон оси X сдвигается соответственно, создавая визуальный эффект «плывущего» вправо графика, на котором всегда видны только последние m_maxPoints измерений, а не бесконечно растущая история — это стандартный, практичный подход для визуализации потоковых данных датчиков в реальном времени, не дающий накапливаться неограниченному количеству точек на графике.

Подводные камни

  • Отсутствие таймаута в вызове pulseIn() (вызов без третьего аргумента) приводит к тому, что функция может заблокировать выполнение loop() на неопределённо долгое время, если эхо-сигнал не вернулся вовсе — это полностью останавливает обработку входящих команд от Qt на это время, аналогично проблеме блокирующего delay() из статьи 282; явный таймаут, как в примере, обязателен для отзывчивой прошивки.
  • Физические ограничения датчика — минимальная дальность (около 2 см) и максимальная (около 400 см), угол обзора конуса ультразвукового луча, и чувствительность к мягким, поглощающим звук материалам (ткань, мех) — могут давать неточные или вовсе отсутствующие показания в граничных условиях, что не является программной ошибкой, а аппаратным ограничением технологии измерения, которое нужно явно учитывать при выборе датчика для конкретной задачи.
  • Постоянное удаление первой точки серии (m_series->remove(0)) при каждом новом измерении имеет вычислительную сложность, зависящую от внутренней реализации QLineSeries — для очень высокой частоты поступления данных (значительно выше, чем 5 раз в секунду из примера) с очень большим m_maxPoints стоит профилировать эту операцию отдельно, поскольку для некоторых реализаций серий удаление из начала списка может быть менее эффективным, чем удаление с конца.
  • Несинхронизированность частоты опроса датчика на Arduino (200 мс в примере) и частоты обновления графика на экране — если эти величины сильно отличаются (очень частые измерения при низкой частоте обновления экрана), часть данных может визуально «теряться» между кадрами отрисовки, что для большинства практических случаев незаметно, но стоит явно учитывать при проектировании системы с очень высокочастотными датчиками.