STM32 → DMA

Довольно часто наши программы работают с массивами данных — читают их и записывают. Это может быть чтение из модуля вроде АЦП и постоянная запись измерений в память, или отправка пакетов данных через SPI, где вам снова нужно регулярно читать данные из памяти и записывать их в регистр данных SPI. Конечно, можно делать это процессором, простейшим кодом наподобие

for(i = 0; i < data_len; i++)
SPI1->DR = data[i];

Но при таком подходе процессор оказывается полностью занят копированием данных, и не может сделать больше ничего. А если нам надо параллельно, к примеру, подготавливать новые данные для отправки или обрабатывать ранее принятые? Выходом из этой ситуации было бы использование прерываний, вроде «назначили байт на отправку, и пока модуль SPI его обрабатывает, успеем подготовить новый», но есть более удобный метод — переложить работу по копированию данных на плечи модуля DMA. Это аппаратный модуль, располагающийся на системной шине и имеющий (как и процессор) доступ ко всей памяти и периферийным модулям. Он сам умеет получать данные и копировать их в нужное место без малейшего использования процессора. Можно сказать, он предоставляет периферийным устройствам унифицированный канал прямого, самостоятельного доступа к памяти.

Архитектура DMA:

dma_arch

Запуск передачи

Всё что вам нужно сделать для передачи данных:

  1. Включить модуль и канал DMA (следующая глава);
  2. Указать адрес источника и адрес получателя;
  3. Задать некоторые параметры передачи (о них — чуть дальше)

Теперь вам нужно только отдать каналу DMA команду «старт», и он начнёт копировать данные — но не все сразу, а слушая сигнал Event от периферийного модуля. Как только модуль принял или готов передать очередные данные, он отправляет Event каналу DMA, и тот совершает очередную транзакцию. Независимо от объёма данных этот процесс не требует участия процессора, а по завершению передачи канал DMA выдаст прерывание, в котором можно сделать какие-то действия вроде запуска новой передачи или отключения DMA.

По соглашениям модуль DMA не может занимать более 50% пропускной способности системной шины, чтобы не помешать работе процессора с периферией. Однако, само выполнение процессором программы не может нарушиться, потому что он связан с внутренней Flash-памятью программ отдельной шиной I-bus.

Каналы DMA

У STM32 есть два модуля DMA, каждый модуль разделён на несколько каналов, к которым подключены разные периферийные устройства. Количество каналов:

  • в МК малой и средней плотности у первого модуля их 7, у второго — 5
  • в МК высокой плотности — по 12 каналов у обоих модулей
  • только в STM32F100 есть только один 7-канальный модуль DMA.

Все каналы одного модуля DMA имеют приоритет, который равен номеру канала: 0 — самый старший, последний (5, 7 или 12) — самый младший. Если к одному модулю DMA одновременно пришли запросы на старт нескольких каналов — он отдаст предпочтение более приоритетному; если же транзакция уже идёт, запрос от младшего канала также будет отклонён. Как только старший канал освободится — сразу же можно запускать младший канал. Понятно, что два модуля DMA могут работать одновременно.

Раскладка периферии по каналам DMA1:

dma_chan

Каналы DMA2:

dma2_chan

Настройки

У каждого канала есть 4 регистра настроек: адрес источника, адрес приёмника, количество данных и конфигурация, плюс два регистра прерываний: статус прерываний и регистр сброса флагов прерываний.

Направление передачи

Возможны три направления передачи:

  1. Периферия → память — для приёма данных и складирования их в буфер (к примеру, оцифровка АЦП)
  2. Память → периферия — для передачи данных из буфера (к примеру, вывод аналогового сигнала в ЦАП)
  3. Память → память — простое копирование блока данных в другое место памяти, аппаратный memcpy.

Периферийному модулю всегда требуется какое-то время на обработку данных, поэтому он тактирует передачу с помощью сигнала Event. В случае же передачи «память → память» копирование происходит с максимальной возможной скоростью, которая стремится к скорости работы DMA, но не превышает 50% загрузки системной шины. К сожалению, из-за этого ограничения копирование памяти через DMA всё-таки происходит на 30% медленнее, чем оптимизированная версия процессорной функции memcpy, зато оно не отвлекает процессор от другой работы.

Ширина данных

Можно передавать байт (8 бит), пол-слова (16 бит) или слово (32 бит) за один раз. К примеру, для передачи по UART, SPI и I2C обычно достаточно 8 бит, но для 9-битного режима UART потребуется уже передача по 16 бит. Копировать массив в памяти так и вообще выгоднее по 32 бита — по 4 байта за раз.

Приёмнику и передатчику можно задавать разные размеры посылок, но при этом необходимо учитывать происходящее преобразование данных по таблице:

Разрядность Биты данных источника
(выделены используемые)
Биты данных приёмника
(выделены использованные)
8 → 8 0123 0123
8 → 16 0123 123
8 → 32 0123 000000100020003
16 → 8 1234567 0246
16 → 16 01234567 01234567
16 → 32 01234567 0001002300450067
32 → 8 123456789ABCDEF 048C
32 → 16 0123456789ABCDEF 014589CD
32 → 32 0123456789ABCDEF 0123456789ABCDEF

Как видно, недостающие данные заполняются нулями.

Инкрементирование адреса

Как адрес источника, так и адрес приёмника можно инкрементировать при каждой транзакции, они будут увеличены на ширину данных. Именно так делается передача массива в выходной регистр — «память» настраивается на инкремент, а «периферия» — на постоянный адрес, и каждая новая транзакция будет брать данные из следующего адреса в памяти.

Разрядность данных

Количество единиц данных, которые нужно передать, обычно количество байт (если ширина настроена на 8 бит). После передачи половины от этого количества канал DMA вызывает прерывание «передана половина», а в самом конце — передача заканчивается и вызывается прерывание «транзакция закончена».

Кольцевой режим

Особый режим работы, в котором по достижению последнего адреса копирование продолжается с нулевого адреса, и таким образом никогда не кончается самостоятельно. Этот режим очень удобен для организации кольцевого FIFO буфера в памяти — остаётся только в нужные моменты вынимать накопленные данные и обрабатывать их. Старые данные затираются, поэтому нужно следить чтобы буфер никогда не переполнился, вовремя забирать данные.

Фичи

Благодаря DMA становится возможным приём, складирование и передача данных на очень высокой скорости, а также реализация особых режимов вроде оцифровки аналоговых сигналов на утроенной скорости: 3 АЦП включаются с задержкой t_преобр/3 друг относительно друга, на один и тот же входной канал. Результаты своей работы они складывают в память со смещением в 3 слова — таким образом, при t_преобр = 1мкс становится возможным объединить силы трёх АЦП так, чтобы вести преобразование на скорости 3MSPS.

Порт ODR модуля GPIO также можно настроить как приёмник DMA, но он не отправляет Event — поэтому передача опять же будет идти со скоростью системной шины. Так можно устроить по-настоящему быстрый bit-banding для реализации каких-либо нестандартных интерфейсов, или сделать R-2R ЦАП 16 бит на скорости 1MSPS — Covox на стероидах :)

Пример: UART через DMA

В качестве простого примера использования DMA передадим с его помощью пакет данных через UART. Для начала сформируем этот пакет в памяти по определённому адресу; передадим этот адрес в канал DMA в качестве источника данных, и проведём настройки передачи; в конце просто запустим канал на передачу.

uint16_t Buffer[10] = {0x54, 0xF8, 0x26, 0x10, 0x3B, 0x48, 0x92, 0xDC, 0xA9, 0x65};

void init_DMA_UART()
{
RCC->APB2ENR |= RCC_APB2ENR_IOPDEN | RCC_APB2ENR_AFIOEN;

RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
AFIO->MAPR |= AFIO_MAPR_USART2_REMAP;
GPIOD->CRL &= !GPIO_CRL_CNF5;
GPIOD->CRL |=  GPIO_CRL_CNF5_1 | GPIO_CRL_MODE5_0 | GPIO_CRL_CNF6_0;

USART2->BRR = USART_BRR;
USART2->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitTypeDef DMA_InitStruct;
DMA_StructInit(&DMA_InitStruct);
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t) &(USART2->DR);
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t) Buffer;
DMA_InitStruct.DMA_BufferSize = 10;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_Init(DMA1_Channel6, &DMA_InitStruct);

USART_DMACmd(USART2, USART_DMAReq_Tx, ENABLE);
DMA_Cmd(DMA1_Channel6, ENABLE);

DMA_ITConfig(DMA1_Channel6, DMA_IT_TC, ENABLE);
NVIC_EnableIRQ(DMA1_Channel6_IRQn);
}

void main()
{
init_DMA_UART();
while(1);
}

void DMA1_Channel6_IRQHandler(void)
{
DMA_ClearITPendingBit(DMA1_IT_TC6);
DMA_Cmd(DMA1_Channel6, DISABLE);
}

По окончании передачи произойдёт прерывание «Transfer complete», и вызовется обработчик DMA1_Channel6_IRQHandler. В нём мы сбросим бит прерывания и отключим 6 канал DMA1.

Ссылка на основную публикацию