Контроллер прямого доступа к памяти (DMA) контроллера STM32

Работа с контроллером DMA

▌Что это?
Есть в современных контроллерах такой блок DMA — контроллер прямого доступа к памяти (ПДП). Штука очень простая, несмотря на умное название. Вся ее суть заключается в том, чтобы по команде от периферии или ядра взять и скопировать кусок памяти с одного места на другой.

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

Что с этим можно делать? Ой да много чего. Можно, задать в качестве источника адрес какой-либо периферии, скажем выходной регистр АЦП, а в качестве приемника адрес массива в ОЗУ, дать приказ DMA по команде завершения оцифровки АЦП хватать результат и пихать в массив. При этом DMA сам скопирует, сам увеличит адрес, а как заполнит буфер, то обнулится и начнет его переписывать по кругу заново.

И вот у нас, автоматически, без работы проца вообще, образуется циклический буфер приема данных с АЦП, которые мы можем тут же прогнать через любую цифровую обработку, да хоть через усреднение, и получить отфильтрованные уже данные. Или то же самое сделать с UART и заставить DMA аккуратно складывать входящие данные в кольцевой буфер.

А можно сделать и наоборот. Сказать DMA что мол вот тебе буфер, там лежит пара сотен байт, возьми и запихай их все в жерло UART и пойти по своим делам, а DMA трудолюбиво отправит в передачу весь буфер.

▌Ближе к теме
Контроллеры
Если рассмотреть конкретно STM32 то там находится один (на малых контроллерах вроде STM32F103C8T6) или два DMA контроллера DMA1 и DMA2 соответственно. На DMA1 есть 7 каналов, на DMA2 всего 5. Оба канала DMA сидят на шине AHB и перед тем как начать с ним работать надо на него подать тактирование, подняв биты DMA1EN и DMA2EN в регистре RCC_AHBENR, как и с любой другой периферией на STM32. В остальном они идентичные и работа с первым и вторым одинакова.

Каналы контроллера
Каждый канал независимый и может работать сам по себе со своими настройками. Но в один момент времени может работать только один канал у каждого контроллера. Чтобы избежать коллизий у каждого канала есть два уровня приоритета. Первый, программный. Мы просто в битах настройки задаем один из четырех уровней. А второй аппаратный, если придут запрос на два канала с одинаковым приоритетом в настройках, то победит тот, чей номер меньше. Сходно с обработкой прерываний. Там такой же двухступенчатый арбитраж.

За выбор приоритета отвечают два бита PL регистра DMA_CCRx Для каждого канала регистр свой. Вариантов там немного, всего четыре:

  • 00 — низкий
  • 01 — средний
  • 10 — высокий
  • 11 — сверхвысокий.

Каждый канал привязан к конкретной периферии. Т.е. если вам нужно чтобы DMA пинал АЦП, то ищите к какому каналу подключено именно АЦП.


Обратите внимание на то, что помимо аппаратных событий в мультиплексор еще ведет софтверный триггер от бита MEM2MEM. Для каждого канала:

Т.е. мы можем гонять данные не только по приказу периферии, но и просто по желанию левой пятки. Просто надо поставить бит MEM2MEM, указать откуда куда и дать приказ в виде EN бита. И DMA исполнит. Это дает некоторую свободу. Например, у нас нет возможности повесить DMA канал на периферию ,т.к. ее родной канал уже занят другой периферией. Бывает такое огорчение. Что делать? Правильно, взять любой другой канал, настроить на обслуживание этой периферии, а пинок давать не через аппаратное событие, в прерывании, вручную дернуть этот канал через Mem2Mem бит. Да будет не совсем красиво, но куда лучше чем в том же прерывании вручную копировать все это. Хотя тут надо смотреть что будет быстрей. Возможно выгоды не будет никакой. Ради одного байта заряжать DMA смысла нет, а вот ради четырех… уже может иметь смысл.

Размер данных
За раз DMA может копировать порцию данных в 1, 2 или 4 байта. В первую очередь это влияет на приращение адресов при вычислении куда класть. Так что это настройка жизненно важна. Сколько и куда класть определяется обычно периферией. Т.е. если UART принимает и выдает по байту, то результат у нас 8 битный. А вот АЦП, может, например, выдать 16 битный результат. Значит размер указывать надо два байта, чтобы сразу за один заход их все забрать. Ну и, очевидно, что размер принимаемых и сохраняемых данных обычно совпадает. Хотя, вам никто не запретить класть однобайтные данные периферии в 32 разрядный массив, выравниваясь по двойному слову. Тогда размер может быть и разный.

За размер данных периферии и памяти отвечают два бита PSIZE и MSIZE регистра DMA_CCRx

00 — 1 байт
01 — 2 байта
10 — 4 байта
11 — не используется.

Откуда куда
Адрес периферии для каждого канала задается в регистре DMA_CPARх этого канала. Просто пишем туда адрес нужного регистра периферии. Но есть два важных момента. Во-первых, нельзя писать в этот регистр при включенном DMA. Т.е. при изменении их бит EN должен быть снят. Второе, адрес зависим от битов PSIZE регистра DMA_CCRx. Т.е. если у нас указан размер данных как 1 байт (PSIZE = 00), то активные все биты регистра DMA_CPARx. Но если данные указаны как слова или как двойное слово, по 16 или 32 бита соответственно, то один или два младших бита этого регистра игнорируются вообще. Т.е. получается, что адрес выравнивается по словам или двойным словам. Т.е. DMA не сможет записать данные словами начиная с нечетного адреса, но адреса все выровнены по словам, так что это пофигу.

Адрес памяти лежит в аналогичном регистре DMA_CMARx и там все то же, что и для DMA_CPARx только за размер отвечают биты MSIZE и его тоже нельзя трогать на включенном канале.

Также надо указать направление копирования. За него отвечает бит DIR регистра DMA_CCRx.

Когда он 0 то мы читаем из адреса DMA_CPARx и пишем по адресу DMA_CMARx. А когда он 1, то наоборот, соответственно. Название у бита идиотское. Не, ну понятно, что направление, но лучше бы назвали его M2P, то есть если 1, то из памяти в периферию. Или как то так. Долго никак не мог запомнить направление, пока не связал, что 0 он такой округлый и похож на такую же округлую букву P — Periph. А 1 угловатая, прям как буква М — Мemory.

Ну и опции инкремента адреса. биты PINC и MINC во все том же DMA_CCRx. Они отвечают за то, чтобы после каждой сработки у нас автоматически увеличивался адрес которые в DMA_CPARx или с DMA_CMARx соответственно.

Адрес периферии прибит намертво и редко когда надо его менять, так что обычно PINC всегда равно нулю. Т.е. никакого инкремента. Вам же не надо, чтобы после чтения из DR того же UART1 на следующем байте было уже из следующего по списку BRR :)))

Хотя… в некоторых случаях таким образом можно сдернуть по DMA содержимое всех регистров настройки какой-нибудь периферийной штуки. Например для отладки, чтобы сделать это предельно быстро, на лету. Правда тут надо быть очень осторожным. Многие биты событий и флаги прерываний снимаются при чтении. И чтение через DMA тоже считается. Так что побыть бесстрастным наблюдателем не выйдет.

Зачем же нужен инкремент DMA_CPARx? А для режима копирования из памяти в память. Тогда мы в DMA_CPARx пишем адрес одного буфера, в DMA_CMARx адрес другого, ставим бит MEM2MEM, даем разрешение и поехали!

А вот бит MINC ставится почти всегда. Мы указываем DMA_CMARx начало буфера и DMA, увеличивая адрес, его последовательно заполняет, или читает из него в периферию.
Впрочем, если нам надо гнать из периферии в периферию, скажем из АЦП сразу в SPI, то бит MINC тоже равен нулю будет.

Ну и есть еще один вариант, когда инкремента нет ни на адресе приемника, ни на адресе источника. Таким образом делаются прямые перегонки, например, из АЦП сразу в USART, минуя процессор. Или на SPI. Так можно в десяток строк кода превратить STM32 в какой нибудь вариант SPI АЦП :)

Также есть бит CIRC, который разрешает цикличный режим. Т.е. как только DMA отработает свое количество циклов заданных в регистре DMA_CNDTRx так адрес сбрасывается в исходное положение. Таким образом, можно указать в DMA_CNDTRx размер буфера и получить циклических буфер нахаляву, аппаратно.

Сколько?
За то сколько раз должно отработать DMA отвечает регистр DMA_CNDTRx. На каждую сработку от входного сигнала (или от постоянно стоящего бита MEM2MEM) DMA копирует один обьект и уменьшает число в DMA_CNDTRx и так до нуля. Дойдет до нуля, канал выключится. Бит EN тут уже ничего решать не будет. Но если стоит бит CIRC, то регистр перезагрузится исходным значением и продолжит работу. Значение может быть до 65535, используются только младшие 16 бит, старшие два байта ДОЛЖНЫ БЫТЬ НУЛЕМ ВСЕГДА.

Записать в регистр DMA_CNDTRx можно только при выключенном DMA канале. Читать можно когда угодно, по нему можно определять сколько объектов осталось DMA передать.

И тут есть важный нюанс. Я намеренно выше говорил, что «сколько осталось объектов». DMA_CNDTRx Считает не БАЙТЫ, а сколько раз DMA сработал. А за одну сработку, он зависимости от настроек, может пересунуть 1, 2 или 4 байта.

Т.е. если вы откуда то скопипастите код в котором будет что-то вида:

1
2
3
4
5
6
...
uint8_t IN_buffer[10];
 
... 
DMA1_Channel1->CNDTR = sizeof(IN_Buffer);
...

И поменяете на

1
2
3
4
5
6
...
uint32_t IN_buffer[10];
 
... 
DMA1_Channel1->CNDTR = sizeof(IN_Buffer);
...

Разумеется изменив PSIZE и MSIZE на 32 бита, то вы получите веселуху: sizeof(IN_Buffer) от 10 uint32_t даст вам 40 и DMA пропишет вам в оперативку 40 раз по четыре байта, захреначив все до куда дотянется :)))) Так что либо корректируйте результат операции sizeof с учетом разрядности данных, либо напрямую указывайте сколько у вас повторений в подходе.

▌Обратная связь
Жевать байты это замечательно, но должен же этот контроллер как то сообщать фоновой программе, что «он сделяль»? Само собой. И реализовано это совершенно традиционно, через прерывания. Их у него три:

  1. Transfer error interrupt — ошибка передачи. Честно говоря не знаю, что должно произойти, чтобы эта ошибка вылезла. Никаких ограничений вроде бы нет. Выход за адресное пространство разве что?
  2. Half transfer interrupt — передана половина данных. Очень удобное прерывание. Можно организовать передачу по половинка буфера. Т.е. нажираем пол буфера, вызываем это прерывание, оно дает отмашку фоновой проге, что пора разгребать,а пока та разгребает, нажираем вторую половину. И как нажрем произойдет:
  3. Transfer complete interrupt — прерывание по окончании передачи. Ну тут все понятно

За включение этих прерываний отвечают биты

TEIE: Transfer error interrupt enable
HTIE: Half transfer interrupt enable
TCIE: Transfer complete interrupt enable

Регистра DMA_CCRx. У каждого канала свои. С прерываниями тут тоже все щедро. У каждого канала свой вектор, если заглянете в startup_stm32f103xb.s файл, то там будет что то вида:

1
2
3
4
5
6
7
8
9
...
    .long    DMA1_Channel1_IRQHandler   // DMA1 Channel 1
    .long    DMA1_Channel2_IRQHandler   // DMA1 Channel 2
    .long    DMA1_Channel3_IRQHandler   // DMA1 Channel 3
    .long    DMA1_Channel4_IRQHandler   // DMA1 Channel 4
    .long    DMA1_Channel5_IRQHandler   // DMA1 Channel 5
    .long    DMA1_Channel6_IRQHandler   // DMA1 Channel 6
    .long    DMA1_Channel7_IRQHandler   // DMA1 Channel 7
...

Это вектора прерываний и есть. Чуть ниже, если вообще есть, будут и вектора для контроллера номер два. А понять же по какому поводу нас вызвало можно из регистра DMA_ISR — Interrupt status registry. Он на каждый контроллер DMA свой. В нем стоят все флаги какие только можно, оптом для всех каналов сразу. Сюда можно только только смотреть. Регистр read only.

Для сброса флага нужно записать 1 в соответственный ему бит регистра DMA_IFCR — Interrupt flag clear registry. Запись нуля же не означает ничего. Так что пишем сразу маску и не паримся.

▌Инциализация и запуск

Вот теперь самое интересное. Прочитали выше написанное, поняли что нужно сделать… А теперь важно сделать это все в правильном порядке. DMA все критические настройки у DMA требуют, чтобы канал был выключен в момент и изменения. За включение и выключение отвечает бит EN регистра DMA_CCRx. Причем крайне желательно ставить этот бит отдельно от всех и снимать отдельно от всех.

Иначе могут быть приколы. Я тут недавно прикольные вилы словил. Сделал процедурку инициализации, думаю, а чего это я их буду по одному ставить? В одном же слове все, дай их сразу и пропишу как надо? За один заход. Выставил все биты конфигурации, на UART1 — работает. Ну окей, взял ЭТИ ЖЕ функции и накатил их на инициализацию UART2 с ТОЧНО таким же кодом, только про UART2, т.е. поменял только имена регистров и каналов DMA. Запускаю… не работает, хоть убейся. Первый UART через DMA работает, второй нет. И так и эдак… Ничего не понимаю. Ладно я бы что-то не то сделал, так тогда бы оба не работали…

Стал под отладчиком ходить, смотреть по регистрам DMA, а у меня запись в регистр конфигурации втором случае DMA_CCRx не проиходит. Т.е. бит должен записаться, а не записывается. Стал разбираться что за фигня и как это так получается вообще? Оказалось, что это оптимизатор так решил, что ему удобней будет записать в одном случае (который работает) сначала старший байт в порт, а потом младший. При этом бит EN записывается последним, а во втором случае наоборот. Записывает младший, т.е.бит EN, а при записи старшего происходит аппаратный сброс этого бита EN. ИЧСХ в даташите ни разу не сказано, что бит EN может сниматься аппаратно. Нигде. Но это происходит.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#define DMAEnable (1<<0)
#define DMADisable (0)
 
#define TransCompl_Int_Enable (1<<1)
#define TransCompl_Int_Disable (0)
 
#define HalfCompl_Int_Enable (1<<2)
#define HalfCompl_Int_Disable (0)
 
#define TransError_Int_Enable (1<<3)
#define TransError_Int_Disable (0)
 
#define ReadMemory (1<<4)
#define ReadPerif (0)
 
#define CircularMode_Enable (1<<5)
#define CircularMode_Disable (0)
 
 
#define PeripheralInc_Enable (1<<6)
#define PeripheralInc_Disable (0)
 
#define MemoryInc_Enable (1<<7)
#define MemoryInc_Disable (0)
 
#define PDataSize_B (0)
#define PDataSize_W (0x0100)
#define PDataSize_DW (0x0200)
 
#define MDataSize_B (0)
#define MDataSize_W (0x0400)
#define MDataSize_DW (0x0800)
 
#define DMA_Priority_Low (0)
#define DMA_Priority_Med (1<<12)
#define DMA_Priority_Hi  (2<<12)
#define DMA_Priority_VHi (3<<12)
 
#define M2M_Enable (1<<14)
#define M2M_Disable (0)
#define CCR_CLEAR_Mask           (0xFFFF8001)

То работать будет через раз, в зависимости от желания оптимизатора.

А если указать тип явно, то все становится прекрасно.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#define DMAEnable ((uint16_t)(1<<0))
#define DMADisable ((uint16_t)0)
 
#define TransCompl_Int_Enable ((uint16_t)(1<<1))
#define TransCompl_Int_Disable ((uint16_t)0)
 
#define HalfCompl_Int_Enable ((uint16_t)1<<2)
#define HalfCompl_Int_Disable ((uint16_t)0)
 
#define TransError_Int_Enable ((uint16_t)(1<<3))
#define TransError_Int_Disable ((uint16_t)0)
 
#define ReadMemory ((uint16_t)(1<<4))
#define ReadPerif ((uint16_t)0)
 
#define CircularMode_Enable ((uint16_t)1<<5)
#define CircularMode_Disable ((uint16_t)0)
 
 
#define PeripheralInc_Enable ((uint16_t)(1<<6))
#define PeripheralInc_Disable ((uint16_t)0)
 
#define MemoryInc_Enable ((uint16_t)(1<<7))
#define MemoryInc_Disable ((uint16_t)0)
 
#define PDataSize_B ((uint16_t)0)
#define PDataSize_W ((uint16_t)0x0100)
#define PDataSize_DW ((uint16_t)0x0200)
 
#define MDataSize_B ((uint16_t)0)
#define MDataSize_W ((uint16_t)0x0400)
#define MDataSize_DW ((uint16_t)0x0800)
 
#define DMA_Priority_Low ((uint16_t)0)
#define DMA_Priority_Med ((uint16_t)(1<<12))
#define DMA_Priority_Hi  ((uint16_t)(2<<12))
#define DMA_Priority_VHi ((uint16_t)(3<<12))
 
#define M2M_Enable (1<<14)
#define M2M_Disable 0
#define CCR_CLEAR_Mask           ((uint32_t)0xFFFF8001)

Ну и сама инициализация выглядит предельно просто, взят порядок из SPL, в принципе, в SPL коде для DMA тоже нет ничего лишнего, но меня раздражают эти структуры для настройки. Ну если мы один хрен хардкодим параметры, то зачем их держать в ОЗУ то? Поэтому я переписал все по простым битмаскам.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
void DMA_Init(DMA_Channel_TypeDef* Channel, uint32_t Perif, uint32_t Mem, uint16_t Size, uint16_t Conf)
{
uint32_t tmp = 0;
 
    tmp = Channel->CCR;		// Копируем биты настройки 
    tmp &= CCR_CLEAR_Mask;	// и стираем все кроме битов EN. А он и так будет 0
    tmp |= Conf;			// Закатываем на результат наши биты настроек. 
 
    Channel->CNDTR = Size;	// Заполняем все нужные поля. Размер передчи
    Channel->CPAR = Perif;	// Адрес периферии
    Channel->CMAR = Mem;		// Адрес в памяти
    Channel->CCR = tmp;		// Записываем настройки в память.
}

Эти две фукнции просто включают и выключают определенный канал.

1
2
3
4
5
6
7
8
9
void DMA_Enable(DMA_Channel_TypeDef* Channel)
{
    Channel->CCR |= DMA_CCR1_EN;
}
 
void DMA_Disable(DMA_Channel_TypeDef* Channel)
{
    Channel->CCR &= (uint16_t)(~DMA_CCR1_EN);
}

Еще нужна процедурка деинициализации DMA, чтобы вернуть все настройки в изначальное состояние, как после сброса:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void DMA_DeInit(DMA_Channel_TypeDef* Channel)
{
    Channel->CCR &= (uint16_t)(~DMA_CCR1_EN);
    Channel->CCR = 0;
    Channel->CNDTR = 0;
    Channel->CPAR = 0;
    Channel->CMAR = 0;
 
 
    if (Channel == DMA1_Channel1)
      {
        /* Reset interrupt pending bits for DMA1 Channel1 */
        DMA1->IFCR |= DMA1_Channel1_IT_Mask;
      }
      else if (Channel == DMA1_Channel2)
      {
        /* Reset interrupt pending bits for DMA1 Channel2 */
        DMA1->IFCR |= DMA1_Channel2_IT_Mask;
      }
      else if (Channel == DMA1_Channel3)
      {
        /* Reset interrupt pending bits for DMA1 Channel3 */
        DMA1->IFCR |= DMA1_Channel3_IT_Mask;
      }
      else if (Channel == DMA1_Channel4)
      {
        /* Reset interrupt pending bits for DMA1 Channel4 */
        DMA1->IFCR |= DMA1_Channel4_IT_Mask;
      }
      else if (Channel == DMA1_Channel5)
      {
        /* Reset interrupt pending bits for DMA1 Channel5 */
        DMA1->IFCR |= DMA1_Channel5_IT_Mask;
      }
      else if (Channel == DMA1_Channel6)
      {
        /* Reset interrupt pending bits for DMA1 Channel6 */
        DMA1->IFCR |= DMA1_Channel6_IT_Mask;
      }
      else if (Channel == DMA1_Channel7)
      {
        /* Reset interrupt pending bits for DMA1 Channel7 */
        DMA1->IFCR |= DMA1_Channel7_IT_Mask;
      }
      else if (Channel == DMA2_Channel1)
      {
        /* Reset interrupt pending bits for DMA2 Channel1 */
        DMA2->IFCR |= DMA2_Channel1_IT_Mask;
      }
      else if (Channel == DMA2_Channel2)
      {
        /* Reset interrupt pending bits for DMA2 Channel2 */
        DMA2->IFCR |= DMA2_Channel2_IT_Mask;
      }
      else if (Channel == DMA2_Channel3)
      {
        /* Reset interrupt pending bits for DMA2 Channel3 */
        DMA2->IFCR |= DMA2_Channel3_IT_Mask;
      }
      else if (Channel == DMA2_Channel4)
      {
        /* Reset interrupt pending bits for DMA2 Channel4 */
        DMA2->IFCR |= DMA2_Channel4_IT_Mask;
      }
      else
      {
        if (Channel == DMA2_Channel5)
        {
          /* Reset interrupt pending bits for DMA2 Channel5 */
          DMA2->IFCR |= DMA2_Channel5_IT_Mask;
        }
      }
}

И, собственно, примеры:

▌Копирование одного массива в другой. Режим MEM2MEM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Массив который копируем и куда копируем
static uint32_t INbuff[10] = {0xFFFFFFF1,0xFFFFFFF2,0xFFFFFFF3,0xFFFFFFF4,0xFFFFFFF5,0xFFFFFFF6,0xFFFFFFF7,0xFFFFFFF8,0xFFFFFFF9,0xFFFFFF10};
static uint32_t OUTbuff[10] = {0};
 
// Включаем тактирование DMA
RCC->AHBENR	|= RCC_AHBENR_DMA1EN;
 
 
// Обнуляем канал который будем использовать. Канал берем от балды. Для этой цели подойдет любой свободный.
DMA_DeInit(DMA1_Channel3);
 
 
// Настраиваем
DMA_Init( DMA1_Channel3,			// Какой канал работать будет
              (uint32_t)INbuff,			// Откуда
              (uint32_t)OUTbuff,			// Куда
              10,				// Сколько. 10 двойных слов, не байтов!!! Массив у нас на 10 элементов
              TransCompl_Int_Disable      +	// Прерывание по передаче выключено
              HalfCompl_Int_Disable       +	// Прерывание по половине выключено
              TransError_Int_Disable      +	// Прерывание по ошибке выключено
              ReadPerif                   +	// Читаем из "периферии". 
              CircularMode_Disable         +	// Циклический режим не нужен. Копируем один раз.
              PeripheralInc_Enable       +	// Увеличиваем адрес источника
              MemoryInc_Enable            +	// Увеличиваем адрес приемника
              PDataSize_DW                 +	// Размер источника двойной слово
              MDataSize_DW                 +	// Размер приемника двойное слово
              DMA_Priority_Low            +	// Низкий приоритет
              M2M_Enable                 );	// Копирование память-память.
 
// Разрешаем копирование...
    DMA_Enable(DMA1_Channel3);
}

Вуаля! Данные будут скопированы.

▌Копирование из периферии в буфер памяти.
В данном случае из выходного регистра USART в кольцевой буфер в ОЗУ. Все что попадет в USART окажется в памяти автоматом.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static volatile char BufferForRecieving1[256];	// Кольцевой приемный буфер. 
 
 
RCC->AHBENR	|= RCC_AHBENR_DMA1EN;	// Подали тактирование на DMA
 
DMA_Disable(DMA1_Channel5);		// Выключили канал.
DMA_DeInit(DMA1_Channel5);		// Обнулили DMA канал
 
USART1->SR &=~(USART_SR_TC);		// Сбросили флаг передачи
USART1->CR3 |=USART_CR3_DMAR;		// Включили сигнал от приема на DMA. В данном случае за UART1 на RX закреплен DMA1_Channel5)
 
// Настраиваем канал
DMA_Init( DMA1_Channel5,				// Пятый канал 1 контроллера. 
              (uint32_t)&(USART1->DR),		// Адрес откуда брать -- адрес регистра DR  в USART1
              (uint32_t)BufferForRecieving1,	// Адрес куда класть результат
              sizeof(BufferForRecieving1),		// Сколько класть? Так как буфер у нас из char, то sizeof будет равен числу элементов. Но лучше так не делать ;)
              TransCompl_Int_Disable      +	// Прерывание по окончанию выключено
              HalfCompl_Int_Disable       +	// Прерывание по половине выключено
              TransError_Int_Disable      +	// Прерывание по ошибке выключено
              ReadPerif                   +	// Читаем из периферии
              CircularMode_Enable         +	// Цикличный режим включен
              PeripheralInc_Disable       +	// Адрес периферии не увеличиваем
              MemoryInc_Enable            +	// А вот адрес примного буфера увеличиваем, перебирая байт за байтом его
              PDataSize_B                 +	// Размер данных из периферии - байт
              MDataSize_B                 +	// Размер данных в памяти - байт
              DMA_Priority_Low            +	// Низкий приоритет
              M2M_Disable                 );	// Режим копирования память-память выключен. 
 
// Включаем DMA - поехали!
DMA_Enable(DMA1_Channel5);

Теперь все что попадает в USART будет DMA утаскивать прямо в буфер, остается только его проверять. Ну или включить прерывания по половине и/или окончании передачи и реагировать на них.

▌Копирование из буфера в периферию

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static volatile char BufferForSending1[256];
 
 
	DMA_Disable(DMA1_Channel4);	// Выключили канал.
	DMA_DeInit(DMA1_Channel4);	// Сбросили все настройки
 
 
    DMA_Init( DMA1_Channel4,		// Канал будет 4 контроллера 1. Именно этот канал пинает USART1 при передаче
              (uint32_t)&(USART1->DR),		// Указываем адрес регистра данных USART
              (uint32_t)BufferForSending1,		// Указываем адрес буфера для передачи. В него предварительно записаны данные.
              256,				// Указываем размер буфера. 
              TransCompl_Int_Disable      +	// Прерывание по окончанию выключено
              HalfCompl_Int_Disable       +	// Прерывание по половине посылке выключено
              TransError_Int_Disable      +	// Прерывание по ошибке выключено
              ReadMemory                  +	// Читаем из памяти (а пишем в периферию)
              CircularMode_Disable        +	// Циклический режим выключен
              PeripheralInc_Disable       +	// Адрес периферии не меняем
              MemoryInc_Enable            +	// А вот адрес памяти наоборот увеличиваем
              PDataSize_B                 +	// Данные размером в байт в периферии
              MDataSize_B                 +	// Данные размером в байт в памяти
              DMA_Priority_Low            +	// Низкий приоритет
              M2M_Disable                 );	// Не из памяти в память.
 
 
	DMA1->IFCR = DMA_IFCR_CTCIF4;		// Сбрасываем флажок бита пустого регистра. Чтобы не отправить сразу нулевой байт
	USART1->CR3 |=USART_CR3_DMAT;		// Подключаем канал DMA к UART. Установив бит отвечающий за пинание уартом дма канала.
 
	DMA_Enable(DMA1_Channel4);		// Поехали!

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

Ну и сами два файлика которые я использую как библиотечку для DMA

dma.c
dma.h

22 thoughts on “Контроллер прямого доступа к памяти (DMA) контроллера STM32”

  1. Я на СТМ8 пользовался как раз для фоновой обработки АЦП в кольцевой буфер. очень удобно оказалось

  2. Находил ещё вот такой интересный материал про DMA: https://habr.com/ru/post/437112/. Не пугайтесь, там только вначале про Cypress, а потом уже про STM32. Был несколько удивлен после прочтения и некоторых размышлений.

      1. насчет параллельности тоже ведь не все просто. Сколько одновременно потоков может обращаться к памяти? судя по статье с хабра — 2 (по крайней мере для описаного МК). при этом не важно кто: ядро или DMA каналы …

        но опять же, в наших то применениях этого и не замечаешь особо.

        кстати, а может кто знает: если ОЗУ физически разделена на отдельные регионы (SRAM1, SRAM2, …), то глядя на матрицу шин в даташите, возникает вопрос: возможен ли одновременный доступ ядра к одной области и DMA к другой?
        Вопрос чисто теоритический, но вдруг кто на практике проверял)

        1. В данном случае главное, что в это время код выполняется тоже.

          Доступ вряд ли доступен одновременно. На то и приоритеты даны, ядро будет приоритетней.

  3. А я DMA настроил на вывод картинки на экран. В памяти сделал фреймбуфер, натравил на этот буфер DMA, и теперь просто рисую в этот буфер, и всё это само появляется на экране. Удобно, быстро и не напрягает процессор кучей прерываний по TXE SPI.

  4. Почитайте внимательно мануал раздел 9.13.17. После программного сброса бита DMA_CCR_EN, нужно дождаться его фактического сброса, циклически перечитывая CCR, и тестируя текущее значение CCR_EN. Иначе ваша инициализация на работающем DMA может с некоторой вероятностью обломиться. И ещё неплохо бы раскрыть тему FIFO, которое позволяет многократно сократить нагрузку на системную шину со стороны DMA.

    1. А вы часом семейства не путаете? В F103 я ничего такого не нашел в описании (RM00008).

      Глава 9 вообще про GPIO и там всего 5 подглав.

      1. Да, действительно, я перепутал, это для F4 так, поэтому код от F1 нельзя просто без изменений перенести на старшие семейства, несмотря на внешнюю одинаковость регистров, он вроде и будет работать, но на самом деле не совсем:).

        1. А вот это интересное замечание получилось! SPL она же вроде сквозная идет. В том числе и на ф4, но там я не увидел выборки контроллера в библиотеке и не проверяется бит ЕН.

  5. >> …оптимизатор так решил, что ему удобней будет записать в одном случае (который работает) сначала старший байт в порт, а потом младший.
    >>
    >> …при записи в регистры DMA надо явно указывать тип данных, чтобы компилятор сделал запись обои байт, старшего и младшего.
    >>
    >> #define DMAEnable ((uint16_t)(1<>
    >>

    Регистры имеют тип uint32_t, константы (1< uint16_t => uint32_t при присвоении не должно порождать кода преобразования вообще. Откуда чудеса?

    1. Я думаю корни растут из Thumb2 который позволяет упаковку данных в памяти побайтно и работу с отдельными байтами. Так что он вполне может упихать константу в байт.

  6. Не в тему, но про надежность кода — плюсовать битовые параметры не есть хорошая идея, лучше использовать битовый OR

    1. Согласен, если какой-нибудь бит по какой-либо причине продублируется, то в случае «+» будет непонятный глюк, а в случае OR всё в порядке.

    1. Они просто недооформлены. Можете попробовать по ключевым фразам из текста загуглить и найти эту методичку.

Добавить комментарий

Ваш e-mail не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.