Довольно часто наши программы работают с массивами данных — читают их и записывают. Это может быть чтение из модуля вроде АЦП и постоянная запись измерений в память, или отправка пакетов данных через SPI, где вам снова нужно регулярно читать данные из памяти и записывать их в регистр данных SPI. Конечно, можно делать это процессором, простейшим кодом наподобие
for(i = 0; i < data_len; i++)
SPI1->DR = data[i];
Но при таком подходе процессор оказывается полностью занят копированием данных, и не может сделать больше ничего. А если нам надо параллельно, к примеру, подготавливать новые данные для отправки или обрабатывать ранее принятые? Выходом из этой ситуации было бы использование прерываний, вроде «назначили байт на отправку, и пока модуль SPI его обрабатывает, успеем подготовить новый», но есть более удобный метод — переложить работу по копированию данных на плечи модуля DMA. Это аппаратный модуль, располагающийся на системной шине и имеющий (как и процессор) доступ ко всей памяти и периферийным модулям. Он сам умеет получать данные и копировать их в нужное место без малейшего использования процессора. Можно сказать, он предоставляет периферийным устройствам унифицированный канал прямого, самостоятельного доступа к памяти.
Архитектура DMA:
Запуск передачи
Всё что вам нужно сделать для передачи данных:
- Включить модуль и канал DMA (следующая глава);
- Указать адрес источника и адрес получателя;
- Задать некоторые параметры передачи (о них — чуть дальше)
Теперь вам нужно только отдать каналу 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:
Каналы DMA2:
Настройки
У каждого канала есть 4 регистра настроек: адрес источника, адрес приёмника, количество данных и конфигурация, плюс два регистра прерываний: статус прерываний и регистр сброса флагов прерываний.
Направление передачи
Возможны три направления передачи:
- Периферия → память — для приёма данных и складирования их в буфер (к примеру, оцифровка АЦП)
- Память → периферия — для передачи данных из буфера (к примеру, вывод аналогового сигнала в ЦАП)
- Память → память — простое копирование блока данных в другое место памяти, аппаратный 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.