У модуля DMA довольно простая задача - копировать данные из одного места в другое с заданными настройками. Запустить копирование можно несколькими способами, как программно, так и аппаратно событием от периферии. Сейчас пойдет речь о запуске DMA по событию от таймера. Для примера сделаем полностью аппаратную "мигалку светодиодом" подключенную к порту PC13.
Код.
#include "stm32f10x_conf.h"
extern uint32_t SystemCoreClock;
volatile uint16_t DmaBuff[2] = {0, 1<<13};
void Config(void)
{
// Настройка GPIO.
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // Вкл. порт GPIOC,
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // и модуль альтернативных функций.
GPIO_SetBits(GPIOC, GPIO_Pin_13);
{
GPIO_InitTypeDef GPIO;
GPIO.GPIO_Pin = GPIO_Pin_13;
GPIO.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO); // Настройка GPIOC.13 в режим выхода.
}
// Настройка DMA.
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // Вкл. модуль DMA1.
{
DMA_InitTypeDef Dma;
Dma.DMA_MemoryBaseAddr = (uint32_t) DmaBuff; // Источник данных массив DmaBuff.
Dma.DMA_PeripheralBaseAddr = (uint32_t) &(GPIOC->ODR); // Приемник - регистр ODR порта GPIOC.
Dma.DMA_DIR = DMA_DIR_PeripheralDST; // Периферия является приемником данных.
Dma.DMA_BufferSize = 2; // Размер данных в "словах".
Dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // Не изменять адрес приемника.
Dma.DMA_MemoryInc = DMA_MemoryInc_Enable; // Увеличивать адрес памяти.
Dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // Размер "слова" - 2 байта.
Dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // Размер "слова" - 2 байта.
Dma.DMA_Mode = DMA_Mode_Circular; // Циклическая работа.
Dma.DMA_Priority = DMA_Priority_Low; // Приоритет канала DMA.
Dma.DMA_M2M = DMA_M2M_Disable; // Не копирование из памяти в память.
DMA_Init(DMA1_Channel2, &Dma);
}
// Настройка таймера.
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // Вкл. таймер TIM2.
{
TIM_TimeBaseInitTypeDef Tim;
TIM_TimeBaseStructInit(&Tim);
Tim.TIM_Prescaler = SystemCoreClock / 10000 - 1; // Настройка предделителя таймера.
Tim.TIM_Period = 10000 - 1;
TIM_TimeBaseInit(TIM2, &Tim);
}
TIM_GenerateEvent(TIM2, TIM_EventSource_Update); // Генерация события при переполнении таймера.
TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE); // Разрешаем запуск DMA по событию переполнения.
DMA_Cmd(DMA1_Channel2, ENABLE); // Разрешаем работу 2 канала модуля DMA1.
TIM_Cmd(TIM2, ENABLE); // Разрешаем работу таймера TIM2.
}
int main(void)
{
SystemCoreClockUpdate();
Config();
while(1)
{
}
}В массиве DmaBuff находятся данные выводимые в порт через DMA. В нулевом элементе массива находится 0, а в первом - установлен 13 бит который соответствует выводу порта PC13. Поочередная запись нулевого и первого элемента массива в порт приведет к изменению логического уровня на выводе PC13.
В функции Config сперва настраивается порт PC13 для работы на выход. Далее второй канал модуля DMA1 конфигурируется таким образом чтобы при запуске каждой транзакции DMA, данные копировались из массива DmaBuff в регистр ODR порта GPIOC. Работа производится циклически и как только будет достигнут конец массива, копирование начнется сначала. После настраивается таймер TIM2 таким образом чтобы он переполнялся раз в секунду и разрешается генерация события от переполнения. Затем разрешается запуск транзакций DMA от события переполнения таймера TIM2 и разрешаются работа второго канала модуля DMA1 и таймера. После всех этих действий, таймер начинает работать и генерировать события каждую секунду, от которого запускается работа DMA и копирование данных из массива в порт.
Вот таким довольно простым методом можно сделать полностью аппаратную "мигалку светодиодом".
Светодиод мигает, а процессор занимается своими делами. 
Но у этого метода есть существенный недостаток - запись в регистр ODR влияет на весь порт, а не на отдельные выходы. Чтобы избавится от этого недостатка, нужно использовать вместо ODR, регистр BSRR.
Старшие 16 бит — для сброса соответствующих пинов в 0. 0 — ничего не делает, 1 — сбрасывает соответствующий бит.
Младшие 16 бит — для установки битов в 1. Точно так же, запись «0» ничего не делает, запись «1» устанавливает соответствующий бит в 1.Регистр только для записи — он сбрасывается в ноль на каждом такте APB2.
Такой метод позволяет изменять состояние одного или нескольких выводов порта не затрагивая остальные.
Для этого в код нужно внести небольшие изменения. Объявление массива.
volatile uint32_t DmaBuff[2] = {1<<13<<16, 1<<13};Настройка DMA.
// Настройка DMA.
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // Вкл. модуль DMA1.
{
DMA_InitTypeDef Dma;
Dma.DMA_MemoryBaseAddr = (uint32_t) DmaBuff; // Источник данных массив DmaBuff.
Dma.DMA_PeripheralBaseAddr = (uint32_t) &(GPIOC->BSRR); // Приемник - регистр BSRR порта GPIOC.
Dma.DMA_DIR = DMA_DIR_PeripheralDST; // Периферия является приемником данных.
Dma.DMA_BufferSize = 2; // Размер данных в "словах".
Dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // Не изменять адрес приемника.
Dma.DMA_MemoryInc = DMA_MemoryInc_Enable; // Увеличивать адрес памяти.
Dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; // Размер "слова" - 4 байта.
Dma.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; // Размер "слова" - 4 байта.
Dma.DMA_Mode = DMA_Mode_Circular; // Циклическая работа.
Dma.DMA_Priority = DMA_Priority_Low; // Приоритет канала DMA.
Dma.DMA_M2M = DMA_M2M_Disable; // Не копирование из памяти в память.
DMA_Init(DMA1_Channel2, &Dma);
}А теперь о том где это может пригодится на практике. Мигать светодиодом не имеет большого смысла. Проще добиться того же результата таймером в ШИМ режиме. Применять подобный метод целесообразно например для аппаратной реализации динамической индикации, т. е. без прерываний.
Подобным методом можно не только выводить данные в порт, но и считывать из него, что позволит аппаратно реализовать работу например с 1-Wire, DHT22 и др. устройствами.

