Введение
Все предыдущие примеры цикла (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 параллельно.