Главная » Статьи » Уроки по программированию stm32fxxx » Уроки по программированию stm32f1xx

Урок 7: DMA
Данный урок будет посвящён более необычной функции МК, чем все рассмотренные ранее. Из-за этого и сама тема урока несколько сложнее предыдущих.
Прежде всего несколько слов о том, что такое DMA.
DMA (Direct Memory Access - прямой доступ к памяти) позволяет передавать блоки данных минуя процессор. Т.е. раньше, если необходимо скопировать блок данных из одного буфера в другой, копирование производилось в цикле вида:

for (i=0;i<size;i++)
{
     A[i] = B[i];
}

Если Size - огромное число, то копирование отнимало много времени. Если же процессор параллельно должен был опрашивать кнопки, мигать светодиодом и т.д., данные процессы останавливались на время копирования. Это неудобно и не оптимально.
При использовании DMA вы просто говорите ему, что, куда и сколько копировать и говорите когда начинать. По окончании выполнения работы DMA оповестит об этом процессор. Который всё время работы DMA мог заниматься любыми другими делами.
Другой жизненный пример - передача данных через UART. Зачастую встаёт задача передачи больших объёмов данных, к примеру информации, снятой с датчика. Но нельзя тратить время на передачу, т.к. тогда процессор не будет снимать данные с датчика (Если это важно).
В этом случае хорошо использовать DMA для передачи уже снятой информации. можно даже попробовать настроить DMA, которое сначала будет снимать данные с датчика, записывая их в определённую область памяти, а по окончанию записи заданного числа данных, указать другому каналу DMA начать копировать данные из этого буфера с UART, пока первый канал параллельно заполняет другой буфер.
Теперь вы понимаете, насколько полезной является данная функция. А в микроконтроллере stm32f103 целых 2 DMA. Причём в первом DMA 7 каналов, а во втором - 5.
К сожалению каждый канал прикреплён к своему периферийному устройству. Причём, как правило, один канал отвечает сразу за несколько периферийных устройств. Разумеется невозможно запрограммировать DMA на параллельную работу с двумя устройствами, принадлежащими одному каналу. Какой канал за какое устройство отвечает можно увидеть на рисунках 1 и 2, представленных ниже.


рис 1

рис 2


Разобравшись с тем, что такое DMA, перейдём непосредственно к настройке. Она производится по стандартной схеме:
1) Включаем тактирование DMA.
2) Настраиваем DMA.
3) Если надо включаем работу DMA у периферии.
3) Когда надо включаем DMA.
4) Ждём срабатывания прерывания от DMA.

Теперь рассмотрим всё это на примере передачи данных по UART. Для этого проделаем следующие шаги: 1) Включаем тактирование DMA.
2) Включаем тактирование UART
3) Настраиваем DMA.
4) Настраиваем UART.
5) Включаем прерывания от DMA.
6) Включаем прерывания от UART.
7) Включаем общие прерывания с DMA и UART. 8) Разрешаем работу UART через DMA.

На этом инициализация заканчивается. Сам алгоритм программы будет такой:
1) Принимаем байт с ПК.
2) Включаем DMA.
3) Ждём окончания работы DMA.
4) Сбрасываем флаг окончания работы DMA.

Начнём с настройки:
1) Тактирование включается следующим образом:

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

Функцию RCC_AHBPeriphClockCmd можно найти в stm32f10x_rcc.c, а определение RCC_AHBPeriph_DMA1 в файле
stm32f10x_rcc.h. Там же можно найти как включать тактовую для других периферийных устройств.

При помощи CMSIS тактовая DMA включается так:
if ((RCC->AHBENR & RCC_AHBENR_DMA1EN) != RCC_AHBENR_DMA1EN)
   RCC->AHBENR |= RCC_AHBENR_DMA1EN;

Т.е. устанавливаем бит DMA1EN в регистре RCC_AHBENR.

2) Настройку UART я пока опущу.
3) Настройка DMA. Очень важная часть нашего урока. Именно при настройке вы указываете что, куда и как надо отправлять.

Прежде всего сбросим предыдущие настройки DMA:

DMA_DeInit(DMA1_Channel4);

Теперь приступим непосредственно к настройке. Она выглядит следующим образом:

DMA_InitTypeDef DMA_InitStructure;

DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)text;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = sizeof(text);
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_Low;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);

Разберём написанное подробнее:

Прежде всего мы должны сказать системе указатель на регистр (именно указатель, а не сам регистр) периферийного устройства с которым хотим что бы работало DMA. В нашем случае это регистр для передачи данных UART1 носящий название DR. Значок & перед названием регистра говорит, что мы передаём адрес регистра в памяти а не значение регистра.
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);

Теперь надо указать DMA область память, которая будет взаимодействовать с нашим UART. Для этого создадим массив и заполним его предложением. Перед название буфера не ставится &, т.к. название и есть указатель на массив.

text[]="Hello World.";

DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)text;

Когда мы определились с буферами с которыми будем работать, нужно описать направление взаимодействия. В нашем случае от памяти к периферии.

DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

В StdPeriph описаны следующие варианты, которые можно использовать при инициализации DMA_DIR:

DMA_DIR_PeripheralDST - направление от памяти к периферии.
DMA_DIR_PeripheralSRC - направление от периферии к памяти.

Далее указываем размер данных, которые мы хотим передать. В нашем случае размер всего буфера:

DMA_InitStructure.DMA_BufferSize = sizeof(text);

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

DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

Теперь надо указать размерность данных. В нашем случае мы передаём данные размером в 1 байт, что и отмечаем в настройках:

DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;

Ещё бывает DMA_PeripheralDataSize_HalfWord и DMA_PeripheralDataSize_Word.

Теперь выбираем режим передачи. В нашем случае обычный.

DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;

Существует 2 режима:
DMA_Mode_Normal - однократное срабатывание DMA.
DMA_Mode_Circular - многократное срабатывание DMA. В этом случае DMA может выдавать прерывание, но продолжит повторять свою работу вне зависимости отреагируете ли вы на него до тех пор пока вы его не выключите.

Теперь определяем приоритет:

DMA_InitStructure.DMA_Priority = DMA_Priority_Low;

И отключаем режим передачи от памяти к памяти для нашего случая.

DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

Записываем всю структуру в функцию. Как можно видеть из таблицы на рис 1, нам надо инициализировать 4-й канал DMA1, т.к. именно он отвечает за выход UART.

DMA_Init(DMA1_Channel4, &DMA_InitStructure);

Разрешаем UART работать в DMA.

USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);

И устанавливаем прерывания от DMA по окончанию передачи:

DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);


Есть 3 типа прерывания по DMA:
DMA_IT_TC - По окончанию передачи.
DMA_IT_HT - По передаче половины данных.
DMA_IT_TE - При передаче возникла ошибка.

7) Включаем общие прерывания в NVIC.

NVIC_EnableIRQ(DMA1_Channel4_IRQn);

Инициализация окончена.

Та же инициализация на CMSISi выглядит так:

DMA1_Channel4->CMAR = (uint32_t) &text[0]; //адрес буфера передатчика
DMA1_Channel4->CPAR = (uint32_t) &USART1->DR; //адрес регистра данных передатчика
DMA1_Channel4->CNDTR = sizeof(text); //для передатчика
DMA1_Channel4->CCR = DMA_CCR4_DIR | DMA_CCR4_MINC; //чтение из памяти, инкремент указателя в памяти
DMA1_Channel4->CCR |= DMA_CCR4_TEIE; //канал 4
USART1->CR3 |= USART_CR3_DMAT; //разрешить передачу USART1 через DMA

Чтобы включить DMA набираем:

DMA1_Channel4->CCR |= DMA_CCR1_EN;

Если же вы хотите перезапустить DMA, то придётся несколько усложнить функцию:
DMA1_Channel4->CCR &= (uint16_t)(~DMA_CCR1_EN);//Выключаем DMA.
DMA1_Channel4->CNDTR = sizeof(text);//Устанавливаем размер передаваемого буфера заново.
DMA1_Channel4->CCR |= DMA_CCR1_EN;//Включаем DMA

После этих строк DMA, будет работать и в конце выдаст прерывание вида:

void DMA1_Channel4_IRQHandler(void)
{
    DMA1->IFCR |= DMA_ISR_TCIF4;//очистить флаг окончания обмена.
}

Флаг надо сбросить, иначе прерывания будут генерироваться бесконечно.
Собственно с работой UART через DMA всё.
Вот пример работы с памятью:

DMA_DeInit(DMA1_Channel4);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)text2;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)text;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = sizeof(text);
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_Low;
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);

В этом примере строка из text переписывается в text2. При этом были внесены изменения:

1) Вместо UART подставлен буфер text2.
2) Разрешено увеличивать номер указателя text2.
3) Разрешено использовать работу память - память.

Теперь программа скопирует данные в text2 из text.

Удачной работы с важной функцией.
Категория: Уроки по программированию stm32f1xx | Добавил: Korvin (27.07.2012)
Просмотров: 27211 | Комментарии: 4 | Рейтинг: 4.5/6
Всего комментариев: 4
4 Onizuka  
0
Здравствуйте, а можно пример использование i2c с dma?

3 Andy  
0
Кроме одной - если отправили быстрее чем считали=) DMA очень шустрый, чтение сектором с карточки на полновесной FatFS гораздо дольше чем отправка, я успевал 3 раза выслать прежде чем дочиталось.

1 Jester  
0
А как реализовать обратную передачу данных? Т.е. мне надо считать данные с флешки, причем не на сам комп, а на радиомодуль, подключенный к плате через RX TX Vcc и землю. ну и передать эти самые данные на другой радиомодуль.

2 Korvin  
0
В зависимости от типа вашей флешки задача может решаться по-разному.
В частности при использовании микросхем типа NAND, можно
воспользоваться следующим за этим уроком (FSMC+NAND). Если же у Вас
просто SD карта, то тут вообще всё достаточно просто.
Есть файловая система FATFS, библиотеки которой доступны по ссылке:
http://elm-chan.org/fsw/ff/00index_e.html
Можно подключить как через родной интерфейс SDIO так и через SPI.

Передавать же лучше так:
Создаёте 2 буфера. Считываете в один из них, заполнив целиком. Затем с
помощью этого урока отправлять данные из этого буфера по DMA. Пока
отправляется заполняете второй буфер. Когда всё из первого буфера
отправится, ставите флаг, что всё закончили и можно отправлять следующие
данные.
Если заполнили раньше чем отправилось, ждёте пока не появится этот флаг. И только тогда отправляете в ДМА второй буфер, а
считывать с памяти начинаете в первый и т.д. поочерёдно меняя буферы.
Сложностей возникнуть не должно.

Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]