Пришло время рассказать об одной потрясающей штуке под названием DMA – то есть прямой доступ к памяти (Direct Memory Access). Поясню что же это такое.
В двух словах – прямой доступ к памяти позволяет перемещать данные без (!) участия центрального процессора. То есть процессор работает себе спокойно над своими задачами, не отвлекается ни на что, а DMA в этот момент может пересылать огромные массивы данных, например, в USART или SPI. Либо, при использовании АЦП можно настроить DMA на автоматическое помещение результатов измерений в некий буфер. Думаю сразу понятно насколько это полезно, ведь процессору теперь не надо отвлекаться от основной полезной работы.
Время традиционной вставки: поскольку компания STMicroelectronics прекратила поддержку библиотеки SPL, которая использовалась в этом курсе, я создал новый, посвященный работе уже с новыми инструментами, так что буду рад видеть вас там - STM32CubeMx. Кроме того, вот глобальная рубрика по STM32, а также небольшая подборка на смежную тему из нового курса:
- STM32 UART. Прием и передача данных по UART в STM32CubeMx.
- STM32 ADC (АЦП) и DMA. Обзор, настройка и пример проекта.
- STM32 SPI и DMA. Конфигурация и пример использования.
А в контроллерах STM32F10x даже не один, а два контроллера прямого доступа к памяти. И, соответственно, у каждого несколько каналов (у DMA1 – 7, а у DMA2 – 5). Вот как выглядит разделение каналов между периферийными устройствами:
Я когда-то упорно пытался подключить USART не к тому каналу, к которому нужно, и совершенно не понимал, почему не работает. С тех пор очень люблю эти картинки )
Давайте-ка теперь рассмотрим процесс настройки в DMA в STM32. Открываем файлы stm32f10x_dma.h и stm32f10x_dma.c из SPL. В первом находим структуру:
typedef struct { uint32_t DMA_PeripheralBaseAddr; uint32_t DMA_MemoryBaseAddr; uint32_t DMA_DIR; uint32_t DMA_BufferSize; uint32_t DMA_PeripheralInc; uint32_t DMA_MemoryInc; uint32_t DMA_PeripheralDataSize; uint32_t DMA_MemoryDataSize; uint32_t DMA_Mode; uint32_t DMA_Priority; uint32_t DMA_M2M; }DMA_InitTypeDef;
Итак:
- uint32_t DMA_PeripheralBaseAddr – сюда мы должны записать адрес периферийного устройства, которое будет участвовать в обмене информацией
- uint32_t DMA_MemoryBaseAddr – записываем адрес области памяти, где лежат данные (ну или куда их надо положить, в зависимости от направления обмена)
- uint32_t DMA_DIR – устанавливаем, является ли периферия источником или местом назначения
- uint32_t DMA_BufferSize – размер буфера для хранения данных
- uint32_t DMA_PeripheralInc, uint32_t DMA_MemoryInc – задаем, надо ли инкрементировать указатели на данные в периферии и в памяти соответственно.
Остановимся на этом поподробнее. Допустим, мы забираем данные из регистра данных АЦП и записываем их в массив в памяти. Так как мы хотим записывать результат преобразования в разные элементы массива, то необходимо инкрементировать указатель, следовательно DMA_MemoryInc = Enable. В то же время регистр данных АЦП у нас один и он никуда не перемещается, а что это значит? Правильно, uint32_t DMA_PeripheralInc = Disable.
Идем дальше:
- DMA_PeripheralDataSize – размер единицы данных в периферии, аналогично – DMA_MemoryDataSize.
Возможные значения этих полей:
DMA_PeripheralDataSize_Byte DMA_PeripheralDataSize_HalfWord DMA_PeripheralDataSize_Word DMA_MemoryDataSize_Byte DMA_MemoryDataSize_HalfWord DMA_MemoryDataSize_Word
- DMA_Mode – режим работы канала DMA
- DMA_Priority – приоритет для канала
- DMA_M2M – используем ли передачу память - память.
Заполнив эти поля нужным образом можно быстро и легко настроить DMA так как нужно для конкретной задачи.
Пришло время немного поэскпериментировать, так что напишем небольшой пример. Объявим массив с данными в памяти и перекинем их с помощью прямого доступа к памяти в регистр данных USART, откуда они и отправятся во внешний мир:
/***************************************************************************************/ #define BAUDRATE 9600 #define DMA_BUFFER_SIZE 16 /***************************************************************************************/ GPIO_InitTypeDef port; ADC_InitTypeDef adc; USART_InitTypeDef usart; uint16_t inputData; uint16_t outputData; DMA_InitTypeDef dma; uint8_t dataBuffer[DMA_BUFFER_SIZE] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; /***************************************************************************************/
dataBuffer – массив с данными, которые надо отправить по USART’у. Не самый информативный набор данных в этом примере, но для тестирования подходит ) Инициализация:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_StructInit(&dma); dma.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->DR); dma.DMA_MemoryBaseAddr = (uint32_t)&dataBuffer[0]; dma.DMA_DIR = DMA_DIR_PeripheralDST; dma.DMA_BufferSize = DMA_BUFFER_SIZE; dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_Init(DMA1_Channel4, &dma); GPIO_StructInit(&port); port.GPIO_Mode = GPIO_Mode_AF_PP; port.GPIO_Pin = GPIO_Pin_9; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, &port); port.GPIO_Mode = GPIO_Mode_AF_PP; port.GPIO_Pin = GPIO_Pin_10; port.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, &port); USART_StructInit(&usart); usart.USART_BaudRate = BAUDRATE; USART_Init(USART1, &usart);
Рассматриваем отдельно то, что касается DMA. Адрес регистра данных USART:
dma.DMA_PeripheralBaseAddr = (uint32_t)&(USART1->DR);
Аналогично, адрес нулевого элемента массива:
dma.DMA_MemoryBaseAddr = (uint32_t)&dataBuffer[0];
Шлем в периферию, а не из нее:
dma.DMA_DIR = DMA_DIR_PeripheralDST;
Размер буфера – 16 байт (у нас определено #define DMA_BUFFER_SIZE 16):
dma.DMA_BufferSize = DMA_BUFFER_SIZE;
В периферии не инкрементируем, в памяти – инкрементируем, прямо как в примере про АЦП чуть выше:
dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable;
Пересылаем данные байтами:
dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
Инициализируем:
DMA_Init(DMA1_Channel4, &dma);
Идем в таблицу с описанием каналов DMA – USART1_TX – 4 канал, все верно!
/***************************************************************************************/ int main() { __enable_irq(); initAll(); USART_Cmd(USART1, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); DMA_Cmd(DMA1_Channel4, ENABLE); while(1) { __NOP(); } } /***************************************************************************************/
Тут все понятно:
USART_Cmd(USART1, ENABLE); DMA_Cmd(DMA1_Channel4, ENABLE);
Включаем USART и DMA. И активируем передачу в последовательный порт по запросу DMA:
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
while(1) { __NOP(); }
Тут могла быть ваша программа... Процессор свободен! Запускаем программу в отладчике:
Данные пришли туда, куда и требовалось 👍 На этом, собственно, все, сегодня мы успешно освоили работу с DMA.