STM32 - DMA часть вторая





Здравствуйте.

Это продолжение про DMA, начало там.


Предыдущая часть закончилась на режиме Timer DMA-burst. Я намеренно пишу такую длинную фразу потому что существует ещё и просто DMA-burst, и эти два понятия нельзя смешивать. DMA-burst — это пакетная передача данных, которая есть не у всех stm32, однако об этом позже, сейчас поработаем с GPIO.


GPIO

С помощью DMA можно за одну транзакцию включать/отключать GPIO-порт целиком или отдельные ножки. Собственно это можно делать и без DMA, но с DMA можно «прикрутив» таймер в качестве триггера (источника запросов к DMA) получить ШИМ сразу на всём порту. Или не на всём, можно на одной, двух, трёх ножках, или со сдвигом по фазе, в общем как заблагорассудится.

порт
На всякий случай поясню что такое порт GPIO. Ножки микроконтроллера разделены на несколько портов обозначенных буквами — GPIOA, GPIOB, GPIOC и т.д. в зависимости от общего количества ножек у микроконтроллера.

К каждому порту привязано не больше 16 ножек, может быть меньше. Названия ножек соответствуют букве порта, например, к GPIOA привязаны ножки РА0-РА15, к GPIOB РB0-РB15, и т.д. Сами буквы как-бы намекают — РB15 — Port B 15.

У каждого порта есть несколько 32-х битных регистров, отвечающих за настройки и функции порта. У регистров которые соединены с ножкой (если выражаться предельно упрощённо), например регистр ODR, используется только половина бит — с 0 по 15, поэтому то и номера ножек с 0 до 15, остальные биты (с 16 по 31) зарезервированы.

Чтобы подать «плюс» на ножку РА0 настроенную как GPIO Output нужно в нулевой бит регистра ODR (output data register) записать единичку…


Если нужно узнать в каком состоянии сейчас ножка, тогда нужно прочитать этот бит.

Есть ещё регистры BRR и BSRR для атомарного управления портом, но статья не об этом.


Если ножка РА0 настроена как GPIO Input, тогда чтоб узнать её состояние нужно прочитать нулевой бит в регистре IDR (input data register)




Чтоб продемонстрировать работу этого механизма настроим пару ножек и таймер. Настраиваем РВ14 и РВ15 как GPIO_Output…




Таймер понадобится для того чтобы «толкать» DMA. То есть при каждом переполнении таймер будет делать запрос к DMA, и DMA будет копировать (записывать) 16-ти битное значение в регистр ODR.

Например, если записать в регистр ODR порта GPIOB число 0x4000, которое в двоичном виде выглядит так 0100000000000000, то в четырнадцатый бит (счёт ведётся от нуля, справа налево) запишется единичка и светик подключённый к РВ14 загорится. Разумеется это действие повлияет только на те ножки порта, которые настроены как GPIO Output.

Если сейчас сгенерировать проект и добавить в бесконечный цикл вот такую конструкцию…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	GPIOB->ODR = 0b0100000000000000; // оно же в hex 0x4000, оно же в dec 16384, оно же сдвиг (1 << 14)
	HAL_Delay(200);

	GPIOB->ODR = 0b1000000000000000; // оно же в hex 0x8000, оно же в dec 32768, оно же сдвиг (1 << 15)
	HAL_Delay(200);
  }

… то светодиоды подключённые к РВ14 и РВ15 начнут весело перемигиваться. Это просто демонстрация записи в регистр GPIO, для работы не понадобится.

Приставка «0b» сообщает компилятору что число представлено в бинарном формате.


Теперь настроим таймер как измеритель интервалов времени…


Включаем тактирование таймера и делаем так чтоб он переполнялся каждые 200мс.


DMA…



Запрос к блоку DMA будет происходить во время переполнения — TIM1_UP. Направление передачи из массива в регистр ODR. Размерность данных Half Word. Регистр ODR 32-ух битный, но поскольку используется в нём только первые 16 бит, мы будем передавать полслова.

В программе создаём массив из двух значений, тех же самых, которые использовались в примере выше, только в hex формате…

/* USER CODE BEGIN 2 */
  uint16_t buff[2] = {0x4000, 0x8000};


Прописываем основную функцию…

HAL_DMA_Start_IT(htim1.hdma[TIM_DMA_ID_UPDATE], (uint32_t)buff, (uint32_t)&GPIOB->ODR, 2);

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

Оставшиеся аргументы это указатель на буфер, указатель на регистр ODR, размер буфера.

заметка
Если бы мы хотели чтоб запрос к DMA происходил во время другого события, например, сравнения или какого-то триггера, тогда нужно указать что-то из этого списка...


Файл stm32f1xx_hal_tim.h.

В Кубе, в разделе DMA Request тоже нужно указать соответствующий пункт.

И добавляем два макроса: инициирующий работу таймера с DMA и запускающий его…

__HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_UPDATE);
__HAL_TIM_ENABLE(&htim1);


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


Если изменить буфер вот так…

uint16_t buff[2] = {0x0000, 0xFFFF};

Тогда регистр ODR будет попеременно полностью заполнятся единичками или нолями, то есть будет мигать весь порт. Это легко проверить если настроить другие ножки порта 'B' как GPIO Output и подключить лампочки.

Если у таймера уменьшить предделитель, например до 71, а переполнение сделать 99, то получим аппаратный ШИМ на всём порту с частотой 5КГц и коэффициентом заполнения 50%.

Если сделаем такой буфер…

uint16_t buff[4] = {0x0000, 0x0000, 0x0000, 0xFFFF};

… то получим ШИМ с частотой 2.5КГц и коэффициентом заполнения 25%. Не забудьте прописать размер буфера в функции.


В общем изменяя настройки таймера и создавая хитроумные буферы, можно делать ШИМы со сдвигом фронтов или ещё что-нибудь. Более того, сюда ещё можно добавить режим Timer DMA-burst и менять с помощью DMA значение переполнения таймера, чтоб внести ещё больше разнообразия в сигналы.

Сделать это очень просто, нужно добавить ещё один канал DMA…


Источником запросов к DMA будет сравнение на первом канале.


В код добавляем запуск этого хозяйства…

uint16_t buff2[2] = {5000, 30000};
HAL_TIM_DMABurst_MultiWriteStart(&htim1, TIM_DMABASE_ARR, TIM_DMA_CC1, (uint32_t*)buff2, TIM_DMABURSTLENGTH_1TRANSFER, 2);

Изменяем только один регистр — ARR. Источник запросов сравнение — TIM_DMA_CC1.

Всё, можно прошивать.


Сейчас функция HAL_DMA_Start_IT(...) генерит прерывание, и если нужны колбеки, то их нужно зарегистрировать как это описано в первой части. Если прерывания не нужны, то надо использовать функцию без прерывания — HAL_DMA_Start(...).



Про GPIO и DMA хочется сказать ещё несколько слов. Записывать данные в регистры GPIO можно и без всякого таймера, правда смысла в этом особого нет, но тем не менее это весьма полезная информация в целом.

Удалите в Кубе все настройки связанные с DMA и настройте его на копирование из памяти в память…



Направление передачи из памяти в память, другого варианта нет, однако регистр GPIO это по сути периферия, и в прошлом примере мы записывали данные в GPIO как в периферию, а сейчас у нас включён режим Memory To Memory. Возникает вопрос — как же так, а ответ очень прост, настройки Memory To Memory, Memory To Peripheral, Peripheral To Peripheral, это просто формальность. Как я уже писал в первой части — что бы мы не делали в микроконтроллере, это всего лишь запись единиц и ноликов в определённые ячейки памяти.

Mode — по идее нам нужно включить циклический режим, но Куб не даёт этого сделать в режиме Memory To Memory, поэтому сделаем это вручную в коде.

Increment Address — инкрементируется только источник (массив со значениями которые будут записываться в GPIO).


В функцию инициализации DMA прописываем циклический режим — DMA_CIRCULAR…



И запускаем процесс…

/* USER CODE BEGIN 2 */
  uint16_t buff[] = {0x4000, 0x0000};
  HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)buff, (uint32_t)&GPIOB->ODR, 2);

Отличие от предыдущего примера только в первом аргументе.

Лампочка будет светить в полнакала. DMA меняет бит в регистре со всей возможной скоростью. До этого у нас таймер рулил скоростью, а сейчас оно жарит «на всю катушку».

Если замерить частоту на пине то получим около 3.6 МГц, что в общем то не так уж и много.


Если делать то же самое только без DMA…

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  GPIOB->ODR = 0x4000; // 0b0100000000000000
	  GPIOB->ODR = 0x0000; // 0b0000000000000000
  }

… тогда получим частоту около 12 МГц.

Если нечем замерить частоту, тогда воспользуйтесь этой статьёй чтоб превратить вашу плату в частотомер — будет сама себя измерять. Нужно сделать всё до строчки «Вот и вся программа».

Не забывайте включать DMA_CIRCULAR после перегенерации проекта.


С DMA…



Без DMA…



Выводы таковы: мнение о том, что с bluetooth всё становится лучше (Шелдон Купер) с DMA всё работает быстрее, на самом деле не всегда верно, DMA тратит время на свои внутренние делишки — обработка request, «договор» с шиной, а так же влияние оказывает то, с какими шинами идёт работа и их занятостью.

Для пущей убедительности прочтите эту статью, не пробегитесь глазами, а прочитайте внимательно, там человек проводил исследование DMA (там есть глава про stm32) и сделал очень полезные выводы.

Однако не всё так однозначно. Например если копировать большой объём данных конкретно из памяти в память, тогда DMA существенно выигрывает в скорости. Собственно у меня получилось, что при копировании свыше 100 байт и больше, DMA копирует в два раза быстрее. Эксперимент проводился в идеальных условиях, то есть ничего больше программа не делала.

Для измерения использовался DWT

/* USER CODE BEGIN 2 */
  #define BUFFSIZE 14 // размер буфера
  volatile uint8_t src_buff[BUFFSIZE] = {0,}; // два буфера
  volatile uint8_t dst_buff[BUFFSIZE] = {0,};
  char trans_str[32] = {0,};
  uint32_t count_tic = 0;

  //////////////////////// копируем вручную ////////////////////////////////
  CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;// разрешаем использовать DWT
  DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // включаем счётчик
  DWT->CYCCNT = 0;// обнуляем значение

  for(uint16_t i = 0; i < BUFFSIZE; i++)
  {
	  dst_buff[i] = src_buff[i];
  }

  count_tic = DWT->CYCCNT; // сколько натикало
  snprintf(trans_str, 63, "MAN %lu tiks\n", count_tic);
  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

  //////////////////////// копируем через DMA ////////////////////////////////
  count_tic = 0;
  DWT->CYCCNT = 0;

  HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)dst_buff, BUFFSIZE); 

  while(!(DMA1->ISR & DMA_FLAG_TC1)){} // дожидаемся окончания копирования

  count_tic = DWT->CYCCNT; // сколько натикало
  snprintf(trans_str, 63, "DMA %lu tiks\n", count_tic);
  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
  /* USER CODE END 2 */


Копируется 14 байт, сначала вручную, потом через DMA…




20 байт…




100 байт…


В два раза быстрее.


Если в функции DMA получателем сделать GPIO…

HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)src_buff, (uint32_t)&GPIOB->ODR, BUFFSIZE);


Тогда получим вот такой результат…


Так как GPIO это уже другая шина, то скорость упала с 692-х тиков до 1091.



про BSRR
Я намеренно не использовал регистры BSRR и BRR так как для примеров было достаточно ODR, однако ничто не мешает работать таким же образом с BSRR. Для этого нужно в настройках DMA указать длину слова WORD (32 бита), и изменить размер элементов буфера…

uint32_t buff[2] = {0,};

Элементы буфера надо подобрать такие, чтоб каждый из них мог включать какие-то биты, а какие-то отключать. Например число 0x90000101 (10010000000000000000000100000001) одним махом включит 0-ой и 8-ой биты, и отключит 12-й и 15-й. Таким образом с помощью одного элемента буфера можно будет не только включать и отключать определённые биты одновременно, но ещё и не затрагивать все остальные биты в регистре ODR.

В функции нужно изменить только регистр…

HAL_DMA_Start(&hdma_memtomem_dma1_channel1, (uint32_t)buff, (uint32_t)&GPIOB->BSRR, 2);

Это применительно ко всем выше описанным примерам.




ADC

Вначале я хотел здесь описать работу АЦП c DMA, но потом решил что не стоит дублировать информацию, которая рассказывает об этом в статье про АЦП. Тем более, что это трудно сделать без описания нюансов работы АЦП, поэтому добро пожаловать сюда.




Всего написанного выше вполне достаточно для полноценного использования DMA, однако надо рассказать про устройство DMA у некоторых камней из серии F2, F4 и F7. Оно несколько сложнее, и есть дополнительный функционал, но в целом суть работы остаётся прежняя.


FIFO


Вот так выглядит схема двух контроллеров DMA у F4…


Рис. 1.


А вот так выглядит один из этих контроллеров крупным планом…


Рис. 2.

Восемь REQ_STREAMx (идущие от мультиплексоров Channel selection) подключённые к арбитру (по центру), это запросы к потокам, поступающие от периферии с «просьбой» на передачу данных. Когда приходит один или несколько запросов, арбитр на основании приоритетов решает какой из потоков (STREAMх) запустить. Потоки служат для передачи данных.

Например, если пришёл запрос REQ_STREAM1, то соответственно запускается поток STREAM 1 (в правой части рисунка), и происходит передача данных (данные проходят через FIFO, о котором чуть позже) между памятью (Memory port) и периферией (Peripheral port).

На рис. 1 видно что у DMA контроллера №2, порты Memory и Peripheral, соединяются через Bus matrix (тоненькие чёрные и синии линии) со всем чем только можно, что даёт возможность копирования периферия-память (и наоборот), периферия-периферия, и память-память. У контроллера №1 с Bus matrix соединён только порт Memory (серые линии), а порт Peripheral соединён только с AHB-APB bridge1 и APB1, то есть доступа к памяти нет, поэтому контроллер №1 не может копировать из памяти в память.


AHB slave programming interface (с права, с низу, рис. 2) — интерфейс для настройки контроллера DMA. На сколько я понимаю он для нас не представляет интереса.


Channel selection на рис. 2 слева, это восемь мультиплексоров к которым подключены по восемь каналов (REQ_STRx_CHх) для отправки запросов от различной периферии. Один мультиплексор выглядит так…


Только один канал может быть активирован на одном мультиплексоре, и соответственно соединён с REQ_STREAMx.

Для сопоставления каналов и потоков, в мануалах на конкретный камень есть таблицы. Например…



Таким образом, например, получение данных от ADC1 на канале №0 первого мультиплексора, через поток №0 (STREAM0), будет выглядеть так…


По красной линии, от периферийного устройства придёт запрос к арбитру на разрешение передачи данных. По жёлтым линиям арбитр указывает какой поток нужно соединить с AHB master. Ну, а по зелёной будут передаваться данные.

на общей схеме


Так же мы могли бы настроить ADC1 на передачу данных по четвёртому потоку (STREAM4), тогда запрос бы приходил через нулевой канал четвёртого мультиплексора — REQ_STR4_CH0 ⇨ REQ_STREAM4.

Если бы при ADC1 работающем через нулевой поток, нам понадобилось бы запустить SPI1_RX, тогда пришлось бы воспользоваться STREAM2, так как два устройства не могут работать на одном потоке. Думаю суть понятна, ну а если настраиваете в Кубе, то и вовсе не о чём парится, он всё сделает сам.



Сначала про устройство FIFO, а потом про его применение.

У каждого потока есть свой буфер FIFO, то есть у каждого контроллера DMA есть восемь независимых FIFO. Размер каждого FIFO равен 4х32 бита, то есть 16 байт. Выглядит буфер так…



В буфер можно помещать данные либо целыми словами (32 бита), либо полусловами (16 бит), либо байтами (8 бит). Заполнять буфер можно (вертикальные колонки) на четверть, на половину, на три четверти, или полностью. На картинке буфер заполнен полностью байтами. Обратите внимание, что на вход подаются байты, и выходят тоже байты, однако можно делать по разному, заполнять байтами, а забирать байты, полуслова или целые слова, можно заполнять полусловами, а забирать байты, полуслова, или целые слова. Можно заполнять целыми словами, а забирать байты, полуслова, или целые слова, в общем можно делать как угодно исходя из задачи. Смотрите…

картинку из мануала



Суть буфера заключается в том, чтобы разгрузить шину AHB. Объяснить это лучше на примере поэтому вернёмся к ADC1. Когда буфер FIFO отключён…


… тогда данные из АЦП передаются в память напрямую, как будто буфера нет. Это называется прямой режим.

Предположим что АЦП занимается преобразованием непрерывно, и соответственно получается, что шина занята и чтением из АЦП, и перекладыванием полученных данных в память практически одновременно.

Если же включить FIFO…


… тогда данные поступающие от АЦП будут заполнять буфер, и пока буфер не заполнится, эти данные не будут передаваться в память. Таким образом шина Bus matrix частично разгрузится на это время. Как только буфер заполнится, всё данные из буфера тут же будут перемещены в память.

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


Теперь, помните я в самом начале говорил про DMA-burst (пакетная передача данных), так вот, пункт Burst size, это как раз про это.

Если мы сейчас запустим процесс, то будет происходить следующее: АЦП после каждого преобразования будет посылать запрос к DMA чтоб тот забрал очередной результат. DMA будет забирать результаты и складывать их в FIFO до тех пор пока FIFO не заполнится (в память результаты не будут пересылаться). Как только FIFO заполнится, так сразу же эти результаты начнут перекладываться в память. Так вот, поскольку у нас сейчас в настройках указано Single, то данные из FIFO будут передаваться в память по одному, то есть перед каждой очередной передачей, DMA будет «согласовывать» свои действия с шиной AHB, то есть тратить на это такты.

Но, дело не только, и не сколько в тактах, а в том, что между передачами могут происходить задержки из-за того, что ресурсы Bus matrix и других шин будут передаваться в распоряжение ЦПУ. В данный момент эти задержки не страшны, так как DMA работает в нормальном режиме, а вот если бы был включён циклический режим (Mode ⇨ Circular), и АЦП опрашивалось бы непрерывно, тогда могло случится так, что из-за задержек FiFO бы не успевал вычитываться, что привело бы к ошибкам.

Так вот, чтоб такого не происходило, был придуман пакетный режим, суть которого заключается в том, что после заполнения FIFO, перед началом передачи из FIFO в память, DMA говорит, — «я занимаю шину на такое-то количество транзакций, прошу никого не мешать мне работать». И всё, после этого DMA быстренько перекладывает данные из FIFO в память безраздельно властвуя над шиной.

Настраивается это так…


В разделе Memory указываем 8 Increment. В FIFO у нас лежит восемь полуслов, поэтому мы говорим, что нам нужно переслать пакет из восьми значений. Можно указать 4 Increment, тогда передача выполнится за два захода.


Если мы оперируем целыми словами…


… тогда размер пакета не может быть больше 4 значений, так как FIFO не может содержать больше 4 слов.


Если бы мы оперировали байтами…


… тогда размер пакета может быть 4, 8 или 16 значений, и соответственно передачи смогут осуществляться за четыре, за два, и за один заход.


Если мы сейчас захотим уменьшить FIFO до половины (Threshold ⇨ Half Full), то Куб не даст нам это сделать пока мы не уменьшим размер пакета до восьми или четырёх Increment, так как размер пакета не может быть больше чем количество данных в FIFO.

Как вы уже поняли, при работе с FIFO всё взаимосвязано, и размер FIFO (Threshold), и длина данных (Data Width), и размер пакета (Burst size), поэтому нужно быть внимательным.

Burst size можно настраивать не только для Memory, но и для Peripheral, а так же и для обоих, в обоих направлениях. То же самое касается режима память-память. При этом нужно учитывать скорости шин, на которых весит периферия. В общем при работе с пакетами нужно хорошо понимать что вы делаете.


Помимо этого FIFO умеет запаковывать/распаковывать данные. Допустим нам надо забирать откуда-то байты, а пересылать их куда-то уже в виде полуслов…


FIFO позаботится о том, чтоб полученные байты вычитывались из буфера как полуслова, с соответствующим инкрементом памяти. Это называется запаковка. Так же можно запаковывать байты в слова или полуслова в слова. То же самое можно делать и наоборот, это будет называться распаковкой. Пакетную передачу тоже можно использовать. Так же нужно понимать, что в режиме запаковки/распаковки необходимо соблюдать кратность данных, то же самое касается работы с FIFO в целом.

К сожалению я не проверял работу с FIFO так как у меня нету платы с таким функционалом (если кому не жалко, киньте немного доната), всё выше написанное это выводы из прочитанного в мануалах. Собственно поэтому и объяснения могут показаться несколько расплывчатыми, а где-то могут оказаться и не совсем точными. Впрочем здесь не только моя вина, сами мануалы написаны так, что порой не заешь как трактовать те или иные вещи, а важным деталям не уделяется должного внимания.




Double buffer

Режим двойного буфера:

В режиме периферия-память (например из USART'а в память) данные из периферии передаются в два массива в памяти. Как только заполниться первый массив, начинает заполняться второй массив. После заполнения второго массива, снова начинает заполняться первый. Таким образом можно обрабатывать данные в одном массиве, пока заполняется другой.

В режиме память-периферия (например из памяти в USART), в памяти создаются два массива, данные из которых попеременно передаются в периферию. Сначала отдаются все данные из первого массива, потом из второго, и т.д. В данном случае можно пока выводится один массив, другой заполнять новыми данными.



Режим двойного буфера работает только в циклическом режиме DMA (Mode ⇨ Circular). Массивы должны быть одинаковых размеров.


Функция для запуска выглядит так…

HAL_DMAEx_MultiBufferStart(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength)


SrcAddress — это источник.

DstAddress — это адрес первого массива.

SecondMemAddress — это адрес второго массива.


Самое интересное в этой функции, это то, что совершенно не понятно как её использовать

Единственное что приходит в голову, на примере отправки данных по USART'у через DMA, это в функции HAL_UART_Transmit_DMA(...) заменить функцию HAL_DMA_Start_IT(...) на эту, с некоторыми доработками. Тогда по идее должно заработать.

Не знаю, может я конечно где-то что-то упустил, но с другой стороны, можно предположить, что эту функцию не стали доводить до ума так как по сути тот же функционал выполняет прерывание по половине DMA.

Тем не менее, эта непонятка относится только к работе с библиотекой HAL, если же делать на CMSIS, то оно работает. Во всяком случае в сети есть пример, где человек это проделывал.


Ещё есть функция (дополняющая выше описанную) для смены адреса памяти…

HAL_DMAEx_ChangeMemory(DMA_HandleTypeDef *hdma, uint32_t Address, HAL_DMA_MemoryTypeDef memory)


Address — новый адрес.

memory — какой именно адрес менять, первый (MEMORY0) или второй (MEMORY1).

Менять адреса можно на лету, но с условием:

Первый адрес можно менять когда бит CT в регистре DMA_SxCR равен 1.
Второй адрес можно менять когда бит CT в регистре DMA_SxCR равен 0.

Этого я тоже не проверял, по тем же причинам.

Инфа:

AN4031 стр. 6.

AN4031 — перевод через онлайн переводчик.

RM0090 стр. 304.

Частичный перевод на русский.




DMAMUX

Этот функционал есть у STM32H7, STM32G0, STM32L4+ и STM32WB.

DMAMUX — это мультиплексор позволяющий выбирать какую периферию к какому каналу подключать. Так же умеет запускать передачу данных по триггеру от таймера №12 или по внешнему прерыванию. Так же запрос на одном канале может запускать другой канал. В общем вносит больше гибкости в работе с DMA.

Описывать это я не буду так как там вроде ничего сложного нет, и нужно пробовать на железе, которого у меня опять таки нет.

Вся необходимая инфа тут — AN5224.




Ещё у микроконтроллеров старших серий есть штуковина по названием DMA2D, это графический ускоритель Chrom-ART для TFT-LCD-дисплеев работающий по технологии DMA. Но это уже совсем другая история.




В довершение статьи хочется сказать, что DMA это не волшебник, он не делает всё быстрее по мановению палочки. Каждая транзакция требует дополнительных тактов на запрос и арбитраж для доступа к шине, на вычисление адреса, на одиночную передачу, и наконец на подтверждение передачи. DMA и CPU выступают в качестве хозяев Bus matrix, однако когда им обоим потребуется одновременный доступ к одному и тому же ресурсу, Bus matrix отдает приоритет CPU. Это не значит что DMA будет «выкинуто на мороз», просто оно запустится чуть позже. Максимально возможная задержка зависит от количества мастеров, осуществляющих доступ к ресурсу, а также от количества каналов, настроенных на конкретном DMA. Подробнее про всё это лучше почитать в мануале, так как там много нюансов связанных с работой шин.


Основное достоинство DMA не в том, что оно может работать с данными быстрее, а в том, что оно позволяет распараллелить программу и тем самым освободить ЦПУ для других задач. То есть увеличить скорость работы микроконтроллера в целом.


Это всё, всем спасибо   Собираю донаты на хорошенькую F7, чтоб можно было экспериментировать в статьях со всем чем только возможно.


Да, вот ещё пара любопытных ссылок — раз и два.


Телеграм-чат istarik

Телеграм-чат STM32


  • 0
  • 39086
Поддержать автора


Telegram-чат istarik

Задать вопрос по статье
Telegram-канал istarik

Известит Вас о новых публикациях






Комментарии (0)

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.