Embedded

DMA для эффективной передачи данных без нагрузки на CPU

Введение

Все предыдущие примеры цикла (UART, ADC) использовали блокирующий или прерывание-управляемый подход, требующий участия процессора в каждой отдельной операции передачи данных. DMA (Direct Memory Access) — аппаратный механизм, позволяющий периферии обмениваться данными напрямую с памятью без участия CPU вовсе, кроме самой первоначальной настройки — в статье разберём, когда и как это применять.

Концепция

DMA-контроллер STM32 может быть настроен на автоматический перенос данных между периферией (UART, ADC) и памятью (массивом в RAM) — после однократной настройки источника, назначения и количества передаваемых элементов, сама передача происходит полностью аппаратно, освобождая процессор для выполнения другого кода параллельно с передачей данных, и генерируя прерывание только по завершению всей передачи (а не на каждый отдельный байт/значение, как в случае прерывания UART из статьи 354).

Пример кода: DMA для эффективной

// Настройка DMA для непрерывного считывания ADC в массив без участия CPU
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

#define ADC_BUFFER_SIZE 100
uint16_t adcBuffer[ADC_BUFFER_SIZE];

void DMA_ADC_Init(void)
{
    __HAL_RCC_DMA2_CLK_ENABLE();

    hdma_adc1.Instance = DMA2_Stream0;
    hdma_adc1.Init.Channel = DMA_CHANNEL_0;
    hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;  // данные текут ОТ периферии (ADC) В память
    hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;        // адрес периферии (регистр ADC) не меняется
    hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;             // адрес в памяти увеличивается с каждым значением
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode = DMA_CIRCULAR;                  // циклический режим — после заполнения буфера
                                                            // начинает заново с начала, без остановки
    hdma_adc1.Init.Priority = DMA_PRIORITY_MEDIUM;
    HAL_DMA_Init(&hdma_adc1);

    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

    // Конфигурация ADC1 на непрерывный режим (ContinuousConvMode = ENABLE) — не показана здесь повторно,
    // аналогична статье 356, но с ContinuousConvMode = ENABLE вместо DISABLE
}

void startAdcDmaCapture(void)
{
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuffer, ADC_BUFFER_SIZE);
    // После этого вызова adcBuffer автоматически и непрерывно заполняется значениями ADC,
    // CPU полностью свободен для другой работы, пока DMA работает в фоне
}

// Прерывание по завершению заполнения половины/всего буфера — позволяет обрабатывать
// уже готовую половину данных, пока DMA продолжает заполнять вторую половину (циклический режим)
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    processAdcSamples(adcBuffer, ADC_BUFFER_SIZE / 2); // первая половина буфера готова
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    processAdcSamples(adcBuffer + ADC_BUFFER_SIZE / 2, ADC_BUFFER_SIZE / 2); // вторая половина готова
}

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

DMA_CIRCULAR режим, объединённый с непрерывным режимом ADC (ContinuousConvMode = ENABLE), создаёт полностью автономный конвейер — ADC непрерывно выполняет преобразования, DMA непрерывно копирует результаты в adcBuffer, циклически возвращаясь к началу буфера после его заполнения, и весь этот процесс происходит без единого вмешательства CPU после первоначального запуска через HAL_ADC_Start_DMA(). Паттерн «половина буфера» (HAL_ADC_ConvHalfCpltCallback/HAL_ADC_ConvCpltCallback) — классический приём для непрерывной потоковой обработки данных без пропусков и без необходимости останавливать DMA для обработки: пока DMA аппаратно заполняет вторую половину буфера новыми данными, процессор успевает обработать уже готовую первую половину, и наоборот, что обеспечивает непрерывный, бесшовный поток данных для последующей обработки (цифровая фильтрация, анализ сигнала) без потери выборок.

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

  • Обработка данных в буфере DMA слишком медленно, не успевая завершить обработку одной половины буфера до того, как DMA начнёт перезаписывать её новыми данными при заполнении второй половины (в циклическом режиме) — это приводит к незаметной на первый взгляд потере или искажению части данных, обрабатываемых «впопыхах» уже частично перезаписанными значениями; при использовании паттерна «половина буфера» критично важно, чтобы обработка одной половины надёжно укладывалась по времени в период заполнения DMA второй половины.
  • Неверная настройка PeriphDataAlignment/MemDataAlignment (например, указание BYTE вместо HALFWORD для 12-битных значений ADC, требующих минимум 16 бит на значение) — несовпадение размера элементов данных между периферией и памятью приведёт либо к ошибке конфигурации DMA, либо, что хуже, к неверно интерпретированным, искажённым значениям в буфере без явной ошибки.
  • Игнорирование настройки приоритета DMA-канала (DMA_PRIORITY_MEDIUM в примере) при использовании нескольких одновременно активных DMA-каналов, конкурирующих за один и тот же физический DMA-контроллер — при недостатке «пропускной способности» DMA-контроллера для обслуживания всех настроенных каналов одновременно, каналы с более низким приоритетом могут получать задержки или даже терять данные (overrun), и для системы с несколькими активными DMA-потоками стоит явно продумать приоритеты в соответствии с реальной критичностью каждого из них.
  • Доступ к буферу DMA из основного кода без понимания, что DMA модифицирует его содержимое полностью независимо и асинхронно относительно остального кода — в отличие от обычных переменных, изменяемых только явным кодом программы, буфер, связанный с активным DMA в циклическом режиме, может измениться в любой момент, без какого-либо явного вызова функции в коде на C, что особенно важно учитывать при попытке прочитать значения из буфера вне специально предусмотренных для этого моментов (callback-функции завершения половины/полного цикла), способных гарантировать, что читаемая часть буфера в этот момент не модифицируется DMA параллельно.