Embedded

Итоговый мини-проект: STM32-контроллер с Qt-приложением мониторинга по UART

Введение

В заключительной статье цикла собираем итоговый проект — STM32-плата, считывающая показания датчика через ADC (статья 356) с использованием DMA (статья 357) для эффективности, организующая параллельную обработку через FreeRTOS-задачи (статья 358), и передающая данные через UART (статья 354) в Qt-приложение мониторинга на ПК, продолжающее визуальный стиль приложений мониторинга из Arduino-цикла (статьи 280-290).

Концепция

Архитектура проекта: STM32 непрерывно собирает данные ADC через DMA в циклическом режиме, FreeRTOS-задача фильтрует и усредняет накопленные значения, отдельная задача периодически формирует и отправляет UART-сообщение с усреднённым значением, и Qt-приложение на ПК через QSerialPort принимает данные и отображает их на графике, аналогично подходу из основного Arduino-цикла, но с STM32-специфичной внутренней архитектурой обработки данных, недоступной на простом Arduino без аналогичных по сложности усилий.

Пример кода: Итоговый мини-проект

// stm32 main.c — итоговая архитектура, объединяющая DMA, FreeRTOS и UART
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

#define ADC_BUFFER_SIZE 100
uint16_t adcBuffer[ADC_BUFFER_SIZE];
QueueHandle_t averagedValueQueue;

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    // Минимальная работа в прерывании DMA-завершения — простое усреднение всего буфера
    uint32_t sum = 0;
    for (int i = 0; i < ADC_BUFFER_SIZE; i++) {
        sum += adcBuffer[i];
    }
    uint16_t average = sum / ADC_BUFFER_SIZE;

    // Отправка в очередь из контекста прерывания — специальная ISR-безопасная версия функции
    BaseType_t higherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(averagedValueQueue, &average, &higherPriorityTaskWoken);
    portYIELD_FROM_ISR(higherPriorityTaskWoken);
}

void UartReportTask(void *parameter)
{
    uint16_t averagedAdc;
    while (1) {
        if (xQueueReceive(averagedValueQueue, &averagedAdc, portMAX_DELAY) == pdTRUE) {
            float voltage = (averagedAdc / 4095.0f) * 3.3f;
            char message[64];
            int length = snprintf(message, sizeof(message), "VOLTAGE:%.3f\n", voltage);
            HAL_UART_Transmit(&huart2, (uint8_t *)message, length, HAL_MAX_DELAY);
        }
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    UART2_Init();
    ADC1_Init();
    DMA_ADC_Init();

    averagedValueQueue = xQueueCreate(5, sizeof(uint16_t));

    xTaskCreate(UartReportTask, "UartReport", 256, NULL, 1, NULL);

    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuffer, ADC_BUFFER_SIZE);

    vTaskStartScheduler();
    while (1) {}
}
// Qt-приложение мониторинга — приём и отображение, аналогично подходу из Arduino-цикла
#include <QSerialPort>
#include <QtCharts/QChart>
#include <QtCharts/QLineSeries>

class Stm32Monitor : public QObject
{
    Q_OBJECT
public:
    Stm32Monitor()
    {
        m_port.setPortName("COM5");
        m_port.setBaudRate(115200);
        m_port.open(QIODevice::ReadOnly);

        connect(&m_port, &QSerialPort::readyRead, this, &Stm32Monitor::onDataReady);
    }

private slots:
    void onDataReady()
    {
        m_buffer += m_port.readAll();
        while (m_buffer.contains('\n')) {
            int index = m_buffer.indexOf('\n');
            QString line = QString::fromUtf8(m_buffer.left(index));
            m_buffer.remove(0, index + 1);

            if (line.startsWith("VOLTAGE:")) {
                double voltage = line.mid(8).toDouble();
                m_series->append(m_series->count(), voltage);
                emit voltageUpdated(voltage);
            }
        }
    }

signals:
    void voltageUpdated(double voltage);

private:
    QSerialPort m_port;
    QByteArray m_buffer;
    QLineSeries *m_series = nullptr;
};

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

xQueueSendFromISR() — специальная, ISR-безопасная (Interrupt Service Routine — обработчик прерывания) версия обычной xQueueSend(), обязательная для вызова из контекста прерывания (а не из обычного кода задачи), что демонстрирует общий принцип FreeRTOS — функции для использования внутри прерываний и снаружи них различаются, и использование неправильной версии функции в неправильном контексте может привести к неопределённому поведению или краху системы. Архитектура целиком показывает практическое объединение трёх независимых тем цикла — DMA освобождает CPU от непосредственного участия в сборе данных ADC, FreeRTOS-очередь безопасно передаёт усреднённое значение из контекста прерывания в обычную задачу, а эта задача уже без спешки и ограничений контекста прерывания формирует и отправляет UART-сообщение, и на стороне Qt-приложения накопление данных в m_buffer с разбором по символу \n — тот же надёжный паттерн обработки потокового, разделённого на строки протокола, что последовательно использовался во всём основном Arduino-цикле статей.

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

  • Усреднение всего буфера ADC непосредственно в прерывании завершения DMA, как показано в примере — хотя цикл усреднения 100 значений достаточно быстр, для буферов значительно большего размера или более сложной обработки (не простое усреднение, а, например, цифровая фильтрация с более сложными вычислениями) выполнение этой работы непосредственно в прерывании может задержать обработку других прерываний системы; для более тяжёлой обработки стоит рассмотреть перенос вычислений в отдельную FreeRTOS-задачу, разблокируемую через семафор/очередь из более лёгкого обработчика прерывания, ограничивающегося лишь сигналом «данные готовы».
  • Отсутствие проверки переполнения очереди averagedValueQueue (созданной с ограниченной ёмкостью — 5 элементов в примере) если UartReportTask по какой-то причине не успевает обрабатывать данные с той же скоростью, с которой DMA генерирует новые усреднённые значения — xQueueSendFromISR() при заполненной очереди не блокируется (в отличие от обычного xQueueSend() из задачи), а просто завершается с кодом неудачи, который в примере явно не проверяется; для production-кода стоит явно обрабатывать этот случай, фиксируя ситуацию переполнения хотя бы в виде счётчика потерянных значений для последующей диагностики.
  • Использование одного, единственного значения опорного напряжения (3.3В) для всего проекта без учёта возможных индивидуальных отклонений конкретной платы, упомянутых как подводный камень ещё в статье 356 — для проекта мониторинга, претендующего на отображение реальных значений напряжения (а не просто относительных изменений), стоит предусмотреть процедуру калибровки конкретного экземпляра устройства против известного эталонного источника напряжения, если точность представляет реальную ценность для конечного применения.
  • Отсутствие в Qt-приложении обработки потери соединения с STM32, что может произойти при физическом отключении USB-кабеля или сбое самого микроконтроллера — аналогично подводным камням из основного Arduino-цикла, приложение мониторинга должно явно отслеживать сигнал QSerialPort::errorOccurred и информировать пользователя о потере связи, а не молча продолжать отображать график, как если бы данные продолжали исправно поступать.

Цикл «STM32 и bare-metal разработка» завершён — план дополнен до 360 статей; финальный цикл этой партии — CAN-шина и автомобильная диагностика (361-370).