Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 14. Использование DMA
DMA (Direct Memory Access) — технология прямого доступа к памяти. Эта технология позволяет быстро и без использования центрального процессора пересылать данные из одной области памяти в другую. При этом для такой пересылки вместо ЦП используется свой специальный контроллер, который называется контроллером DMA.
Контроллер DMA работает независимо от ЦП и параллельно с ним. Таким образом использование DMA позволяет экономить ресурсы ЦП, который во время операций пересылки данных с помощью DMA может заниматься какими-то другими полезными делами.
Несмотря на то, что ЦП и DMA работают независимо друг от друга, DMA может приостанавливать доступ ЦП к системной шине на несколько тактов в случаях когда они оба пытаются обратиться к одним и тем же адресам памяти.
В микроконтроллеры stm32 может быть встроено до 2-х контроллеров DMA — DMA1 и DMA2. Они имеют следующие особенности:
- 12 независимо конфигурируемых каналов: 7 для DMA1 и 5 для DMA2
- Возможность использования в качестве источника или приёмника данных Flash, SRAM и периферийных модулей, подключенных к шинам APB1, APB2 и AHB
- Аппаратный запрос от каждого периферийного модуля подключен к одному из 12 каналов
- Возможность генерации программного запроса по каждому каналу
- Приоритеты между запросами от разных каналов одного контроллера DMA настраиваются программно и могут иметь 4 уровня: very high, high, medium, low. В случае, если для двух каналов установлен одинаковый приоритет, более приоритетным считается канал с меньшим номером
- Независимая установка размера порции данных для источника и приёмника (байт, 16-битное полуслово или 32-битное слово)
- Поддержка кольцевого буфера (когда данные пишутся в буфер по кругу)
- Возможности передачи данных из памяти в память, из памяти в периферию, из периферии в память или из периферии в периферию
- 3 флага событий (Half Transfer, Transfer Complete и Transfer Error) логически объединённых в один запрос прерывания для каждого канала
- Программируемое количество передаваемых данных (количество порций) — до 65535 (то есть если данные передавать 32-битными словами, то максимум можно передать 65535*4 = 262140 байт, — почти 256 кбайт)
Карта запросов модуля DMA1:
Карта запросов модуля DMA2:
Как вообще работает DMA? Всё происходит следующим образом:
- В периферийном модуле происходит событие, по которому через определённый для этого периферийного модуля канал генерируется запрос к DMA (генерация запроса к DMA должна быть разрешена в настройках самого периферийного модуля)
- Контроллер DMA обрабатывает пришедший запрос в соответствии с настройками и установленным приоритетом канала, через который пришёл запрос
- Как только контроллер DMA получает доступ к периферии (выполняет передачу одной единицы информации) — он посылает сгенерировавшему запрос периферийному модулю сигнал подтверждения
- При получении сигнала подтверждения периферийный модуль снимает свой запрос
- Как только периферийный модуль снимает свой запрос — DMA контроллер отменяет сигнал подтверждения
- Если есть ещё запросы от периферии — начинает обрабатываться следующий запрос (опять же в соответствии с установленным приоритетом)
Сама передача данных состоит из трех шагов:
- Загрузка данных из регистра периферии или из памяти по адресу, записанному во внутреннем регистре текущего адреса источника (этот регистр программно недоступен). Начальный адрес, используемый для первой передачи, прописывается в регистр DMA_CPARx или в регистр DMA_CMARx (в зависимости от направления передачи)
- Сохранение загруженных данных в регистре периферии или в памяти по адресу, указанному во внутреннем регистре текущего адреса приёмника (этот регистр программно недоступен). Начальный адрес, используемый для первой передачи, прописывается в регистр DMA_CPARx или в регистр DMA_CMARx (в зависимости от направления передачи)
- уменьшение значения регистра-счётчика DMA_CNDTRx, который содержит количество оставшихся транзакций
В зависимости от настроек, после каждой транзакции модуль DMA может автоматически инкрементировать адреса источника и/или приёмника. Эта возможность настраивается для источника и приёмника независимо друг от друга установкой/сбросом битов PINC и MINC в регистре DMA_CCRx. Причём, в зависимости от установленных для источника и приёмника размеров порции данных адрес автоматически инкрементируется на 1, 2 или 4.
Если канал сконфигурированв нормальном режиме, то после обнуления счётчика транзакций новые запросы к DMA обслуживаться не будут. Чтобы включить обслуживание новых запросов — нужно сначала программно взвести счётчик (записать в регистр DMA_CNDTRx новое значение). Сделать это можно только предварительно выключив соответствующий канал DMA (при выключении канала его настройки не сбрасываются).
В кольцевом режиме (circular mode) значение счётчика автоматически устанавливается к начальному значению после выполнения последней запланированной транзакции (то есть после достижения нуля). Одновременно с этим адреса внутренних регистров текущих адресов сбрасываются к начальным адресам, прописанным в регистрах DMA_CPARx, DMA_CMARx. Кольцевой режим можно включить/выключить установкой/сбросом бита CIRC регистра DMA_CCRx.
Режим пересылки данных из памяти в память выбирается установкой бита MEM2MEM в регистре DMA_CCRx. В этом режиме контроллер DMA начинает выполнять передачу данных сразу после включения канала установкой бита EN регистра DMA_CCRx, то есть не дожидаясь внешнего аппаратного запроса. Не знаю зачем этому режиму дали такое название, я бы лучше назвал его режимом без внешнего триггера, поскольку фактически в этом режиме вы можете как и раньше пересылать данные не только из памяти в память, но и из памяти в регистры периферии, и из регистров периферии в память, и из регистров в регистры, просто теперь каждая транзакция запускается не по событию от периферии, а автоматически, пока не обнулится счётчик (или вообще бесконечно, если для канала выбран кольцевой режим).
DMA_ISR — регистр статуса прерываний. Биты этого регистра содержат флаги событий для каждого канала. Они доступны только для чтения, а сбрасываются записью 1 в соответствующий бит регистра DMA_IFCR.
- GIFx: глобальный флаг прерывания для канала x (x=1..7). Устанавливается в 1 аппаратно в случае возникновения одного из прерываний TE, HT или TC. Сбрасывается программно, записью единицы в соответствующий бит регистра DMA_IFCR.
- TCIFx: флаг окончания передачи. Устанавливается в 1 аппаратно после обнуления счётчика передаваемых данных. Сбрасывается программно, записью единицы в соответствующий бит регистра DMA_IFCR.
- HTIFx: флаг окончания передачи половины данных. Устанавливается в 1 аппаратно после отправки половины запланированных данных (когда счётчик уменьшается наполовину). Сбрасывается программно, записью единицы в соответствующий бит регистра DMA_IFCR.
- TEIFx: флаг ошибки передачи. Устанавливается в 1 аппаратно при возникновении ошибок передачи (при попытках работы с физически нереализованными или зарезервированными адресами памяти). Сбрасывается программно, записью единицы в соответствующий бит регистра DMA_IFCR.
DMA_IFCR — регистр сброса флагов статуса прерываний. Установка битов этого регистра приводит к сбросу флагов прерываний в регистре DMA_ISR. Биты регистра DMA_IFCR доступны только для записи.
- CGIFx: установка этого бита в 1 приводит к сбросу флагов GIFx, TCIFx, HTIFx и TEIFx (x=1..7 — номер канала) в регистре DMA_ISR
- CTCIFx: установка этого бита в 1 приводит к сбросу флага TCIFx (x=1..7 — номер канала) в регистре DMA_ISR
- CHTIFx: установка этого бита в 1 приводит к сбросу флага HTIFx (x=1..7 — номер канала) в регистре DMA_ISR
- CTEIFx: установка этого бита в 1 приводит к сбросу флага TEIFx (x=1..7 — номер канала) в регистре DMA_ISR.
DMA_CCRx — регистры настройки каналов (x=1..7 — номер канала).
- EN: включение/выключение канала
- TCIE: разрешение(1)/запрет(0) прерывания по событию TC (переданы все данные, счётчик обнулился)
- HTIE: разрешение(1)/запрет(0) прерывания по событию HT (передана половина данных, счётчик уменьшился наполовину)
- TEIE: разрешение прерывания по событию TE (ошибка передачи)
- DIR: направление передачи данных. 0 — читать из периферии (и писать в память), 1 — читать из памяти (и писать в периферию)
- CIRC: включение/выключение циклического режима передачи (1 — circular mode, 0 — normal mode)
- PINC: включает(1)/выключает(0) инкрементирование адреса периферии после каждой транзакции
- MINC: включает(1)/выключает(0) инкрементирование адреса памяти после каждой транзакции
- PSIZE[1:0]: определение размера единицы данных для периферии. Может принимать следующие значения:
- 00: 8 бит
- 01: 16 бит
- 10: 32 бита
- 11: зарезервировано
- 00: 8 бит
- 01: 16 бит
- 10: 32 бита
- 11: зарезервировано
- 00: Low (низкий)
- 01: Medium (средний)
- 10: High (высокий)
- 11: Very high (очень высокий)
DMA_CNDTRx — регистры-счётчики (x=1..7 — номер канала). В младшие 16 бит этих регистров прописывается количество данных для передачи (то есть сколько транзакций нужно выполнить с соответствующим каналом). Каждый из этих регистров обладает следующими особенностями:
- Значение в регистр можно прописать только когда соответствующий канал выключен
- После включения канала регистр становится недоступен для записи
- Значение в регистре автоматически декрементируется после каждой транзакции
- После того, как значение регистра станет равным нулю — новые запросы к DMA от соответствующего канала перестают обслуживаться
- Если для соответствующего канала установлен циклический режим передачи, то после обнуления регистра в него автоматически загружается начальное значение
DMA_CPARx — регистры адреса (x=1..7 — номер канала). Здесь содержатся начальные адреса регистров периферии в которую или из которой нужно передавать данные по запросу от соответствующего канала. Доступ автоматически выравнивается на границу полуслова или слова, в зависимости от установленного для периферии размера порции данных (достигается игнорированием одного или двух младших бит регистра адреса). Регистры не могут быть перезаписаны пока соответствующий канал включен.
DMA_CMARx — регистры адреса (x=1..7 — номер канала). Здесь содержатся начальные адреса областей памяти в которую или из которой нужно передавать данные по запросу от соответствующего канала. Доступ автоматически выравнивается на границу полуслова или слова, в зависимости от установленного для памяти размера порции данных (достигается игнорированием одного или двух младших бит регистра адреса). Регистры не могут быть перезаписаны пока соответствующий канал включен.
Для модуля DMA2 существуют точно такие же регистры, только x в них может принимать значения 1..5, а не 1..7 (поскольку в DMA2, в отличии от DMA1, всего 5 каналов, а не 7).
Работать с DMA достаточно просто. Всё, что от Вас требуется — это настроить соответствующий канал и далее только обрабатывать события (которых как мы помним для канала всего 3) или прерывания от них, а также, возможно, взводить счётчик передаваемых данных.
Как обрабатывать события и прерывания — решать только Вам, а вот порядок настройки канала приведён ниже:
- Установить в регистре DMA_CPARx (x — номер канала) начальный адрес регистра периферии в которую/из которой будут передаваться данные
- Установить в регистре DMA_CMARx (x — номер канала) начальный адрес области памяти в которую/из которой будут передаваться данные
- Прописать в регистре DMA_CRDTRx (x — номер канала) количество передаваемых данных (количество транзакций)
- Установить в регистре DMA_CCRx (x — номер канала) приоритет настраиваемого канала (биты PL[0:1]), направление передачи данных, режим (circular/normal), отметить нужно или не нужно инкрементировать адреса периферии и памяти после каждой транзакции, установить размеры порций данных для периферии и для памяти, а также настроить прерывания
- Включить канал, установив в 1 бит EN регситра DMA_CCRx (x — номер канала)
- Ну и конечно нужно не забыть разрешить слать запросы к DMA в настройках самого периферийного модуля.
В библиотеке StdPeriph настройку канала можно выполнить одной функцией — DMA_Init, для включения/выключения используется функция DMA_Cmd.
Примеры работы с DMA приводить не буду, их можно посмотреть в примерах работы с другими модулями (например, в примерах работы с UART или ADC), так что на этом всё.
Урок 33. Прямой доступ к памяти в STM32. Контроллер DMA.
В уроке узнаем о способе передачи данных в режиме прямого доступа к памяти, реализации этого процесса в микроконтроллере STM32. Подробно изучим режимы работы, форматы регистров, особенности контроллера ПДП STM32.
Микроконтроллер обменивается данными с “внешним миром” только через периферийные устройства, а данные хранит в памяти. Поэтому в любой реальной программе существует задача обмена данными между оперативной памятью микроконтроллера и периферийными устройствами. Также бывает необходимость в обмене данными между блоками памяти (копирование блоков) и в передаче информации с одного периферийного устройства на другое.
В предыдущих уроках мы реализовывали подобные задачи программным способом. В цикле считывали данное из памяти или регистра и загружали его в регистр или память.
- В каких-то случаях управление периферийными устройствами с помощью программных операторов выглядело единственным логичным вариантом. Например, когда мы зажигали светодиод, подключенный к порту ввода/вывода. Странно было бы установить ячейку памяти в нужное состояние, а затем с помощью дополнительных аппаратных узлов переслать ее содержимое на порт ввода/вывода.
- В других случаях передача данных из программы нас вполне устраивала, не вызывала проблем в реализации. Например, когда выводили сообщения из массива или текстовой строки в последовательный порт. Но уже тогда было заметно, что мощный процессор использует минимум своих вычислительных возможностей. Он просто ждет освобождения контроллера UART, считывает данное из памяти и записывает его в регистр данных контроллера UART. И так нужное количество циклов.
Использование прерываний позволяло только оперативно перераспределять работу процессора между задачами. Процессор все равно занимался примитивными операциями – пересылкой данных. Программа была простой, и мы могли позволить тратить почти все процессорное время на копирование данных.
- А вот при реализации цифрового осциллографа в уроке 29 программное чтение данных АЦП значительно ограничивало возможности устройства. На время чтения развертки осциллографа программа работала в блокирующем режиме, зависала. Приходилось даже запрещать прерывания. Благодаря высокому быстродействию STM32 удалось считывать данные АЦП с дискретностью 1 мкс, но это был предел. С большей скоростью поток данных уже не обработать.
А ведь задача пересылки данных между памятью и периферийным устройством с аппаратной точки зрения очень простая. Достаточно аппаратного счетчика адреса памяти, счетчика количества данных и промежуточного регистра. Данное считывается, например, из АЦП и записывается в память по адресу из специального регистра-счетчика. Его содержимое увеличивается на 1, и новое данное из АЦП записывается в память по следующему адресу. Процесс продолжается пока счетчик количества данных не остановит работу устройства. При этом процессор продолжает выполнение основной программы.
Подобные аппаратные узлы содержатся во всех современных высокопроизводительных микроконтроллерах. Конечно, их устройство сложнее, чем я описал в предыдущем абзаце, но и функций намного больше.
Процесс обмена данными без участия процессора (за счет аппаратных узлов микроконтроллера)
- между памятью и периферийными устройствами микроконтроллера,
- или между периферийными устройствами,
- или между блоками памяти
называется прямым доступом к памяти (ПДП). Английская аббревиатура DMA — Direct memory access.
Аппаратная часть микроконтроллера, реализующая процесс прямого доступа к памяти называется контроллер прямого доступа к памяти (контроллер DMA).
Использование ПДП (DMA) позволяет:
- освободить процессор для выполнения других задач;
- обеспечить высокие скорости передачи данных, недостижимые при использовании программного способа обмена.

Устройство контроллеров DMA, как правило, простое, очевидное и понятное для разработчиков программ.
Общие принципы работы контроллера прямого доступа к памяти в STM32.
В техническом описании STM32 есть функциональная схема контроллера DMA. Не вижу никакого смысла приводить ее здесь. На схеме показаны шины подключения контроллера к системным шинам, через которые происходит прямой доступ к оперативной памяти.
Но взаимодействие контроллера DMA с памятью, разрешение конфликтов при одновременном обращении процессора и контроллера к памяти или периферийным устройствам отрабатываются автоматически. Повлиять на них мы не можем. Поэтому просто считаем, что контроллер DMA имеет доступ к оперативной памяти.
- В микроконтроллере STM32F103c8 есть только один контроллер DMA c 7 каналами. В микроконтроллерах старших серий (high-density и XL-density) добавлено второе устройство DMA.
- Как я писал выше, прямой доступ к памяти происходит по общей с процессором системной шине. Поэтому при одновременном обращении к памяти или периферийному устройству процессора и контроллера DMA могут возникать конфликты. В результате работа процессора может приостанавливаться, но как минимум половина пропускной способности системной шины отдается процессору. Программа будет выполняться даже при интенсивном обмене через прямой доступ к памяти. Не говоря о том, что не каждая команда программы работает с оперативной памятью или периферийным устройством.
- Инициаторами обмена с помощью контроллера DMA являются аппаратные события микроконтроллера (event). Конечно, при наличии соответствующих разрешений в управляющих регистрах. В уроке 18 я писал о событиях, и чем они отличаются от прерываний.
В общем случае прямой доступ к памяти происходит следующим образом.
- В периферийном устройстве возникает событие, требующее операции DMA. Соответствующий сигнал поступает на контроллер DMA.
- Происходит обмен данными между памятью и периферийным устройством.
- Сразу после обращения к периферийному устройству, контроллер посылает ему сигнал подтверждения, который сбрасывает событие.
- Новое событие вызывает очередной обмен данными.
Сам обмен данными (транзакция) с использованием DMA состоит из трех операций.
- Данное загружается в промежуточный регистр из памяти или из регистра данных периферийного устройства в зависимости от направления обмена. Адреса памяти и периферийного устройства содержатся в специальных регистрах.
- Данное перегружается в память или регистр данных периферийного устройства.
- Регистр количества транзакций (элементов обмена) уменьшается на 1. При достижении 0, обмен завершается.
Все просто и логично.
Каналы DMA.
Повторюсь. В микроконтроллере STM32F103c8 есть только один контроллер DMA c 7 каналами.
С точки зрения программиста каждый канал – это отдельный аппаратный узел микроконтроллера. Он осуществляет DMAпередачу между регистром данных периферийного устройства (расположенным по фиксированному адресу) и памятью.
Каналы работают с общей памятью, через общую шину данных. Поэтому между ними могут возникать конфликты, которые разрешаются следующим способом.
В каждый момент времени обмен данными может происходить только по одному каналу. При нескольких активных каналах контроллер DMAразрешает работу канала с более высоким приоритетом.
Приоритет каждого канала устанавливается программно и может иметь четыре уровня:
- Very high priority (очень высокий);
- High priority (высокий);
- Medium priority (средний);
- Low priority (низкий).
При активных каналах с одинаковым уровнем приоритета преимущество имеют каналы с меньшим номером.
Регистры каналов.
Каждый канал для нас – отдельный аппаратный контроллер передачи данных.
Каждый канал имеет 4 собственных регистров, которые полностью определяют режим обмена данными по нему.
Каналы настолько независимы, что если вести речь обо всех регистрах контроллера DMA, то к регистрам каналов добавятся только регистры управления прерываниями.
Итак 4 регистра, с очевидным назначением.
-
— адрес памяти, по которому выполняется запись или чтение в зависимости от направления передачи данных. — адрес регистра данных периферийного устройства, из которого выполняется чтение или в который производится запись. В режиме обмена данными между блоками памяти он также содержит адрес памяти. — содержит количество элементов для передачи (количество транзакций). В качестве элемента транзакции могут быть выбраны байт, полуслово, слово. — регистр конфигурации канала. Разрешает работу канала, определяет режимы передачи данных.
Полное описание регистров есть в справочнике.
Количество элементов данных для передачи устанавливается программно и может быть от 0 до 65535.
В общем случае обмен DMA происходит между памятью и периферийным устройством. Источником или приемником данных периферийного устройства является регистр данных этого устройства. Он имеет фиксированный адрес, который должен оставаться неизменным в ходе DMA обмена.
Что касается памяти, то как правило, необходимо записывать или считывать данные по последовательно расположенным адресам.
Для этого существует режим инкрементации адреса. Он доступен как для адреса памяти (бит MINC регистра DMA_CCRx), так и для адреса периферийного устройства (бит PINC регистра DMA_CCRx). Хотя в последнем случае не имеет смысла.
Таким образом, после каждого обращения к памяти указатель увеличивается на 1, 2 или 4, в зависимости от заданного размера элемента данных. Начальные адреса передачи данных хранятся в регистрах DMA_CMARx (для памяти) и DMA_CPARx (для периферии). В процессе обмена данными их значения не изменяются, а текущие значения указателей содержатся во внутренних регистрах микроконтроллера и программе недоступны.
Размер элементов данных транзакции может иметь значения:
- 8 бит (байт);
- 16 бит (полуслово);
- 32 бита (слово).
Выбор размера данных осуществляется независимо не только для каждого канала, но и отдельно для приемника и передатчика данных каждого канала. Для этого в регистре DMA_CCRx существуют битовые поля PSIZE и MSIZE.
После выполнения последней передачи запросы по каналу перестают обслуживаться. Регистр количества данных передачи DMA_CNDTRx содержит 0. Чтобы инициировать новый DMA обмен необходимо загрузить в него новое значение. Это возможно только после отключения канала. При этом все остальные регистры канала (DMA_CMARx, DMA_CPARx и DMA_CCRx) сохраняют значения.
Если задан циклический режим (бит CIRC регистра DMA_CCRx), то по окончанию передачи данных в регистр DMA_CNDTRx автоматически загружается его первоначальное значение и процесс DMA обмена возобновляется.
Конфигурация канала DMA.
Для использования канала необходимо конфигурировать его, выполнив следующую последовательность действий. Изначально канал должен быть запрещен (бит EN регистра DMA_CCRx сброшен).
- Загрузить в DMA_CPARx адрес регистра данных периферийного устройства. Данные будут записываться/считываться по этому адресу после возникновения события периферийного устройства, формирующего запрос DMA.
- Загрузить в DMA_CMARx начальный адрес блока памяти. В этот блок будут записываться данные из периферийного устройства, либо из него будут считываться данные для передачи в периферийное устройство.
- Записать в DMA_CNDTRx общее количество элементов обмена. Каждая транзакция будет уменьшать значение этого регистра. При достижении 0, DMA передача будет остановлена.
- Задать конфигурацию DMA передачи в регистре DMA_CCRx: направление передачи, приоритет канала, режим инкрементирования, размер элемента данных и т.д.
- Включить канал битом EN регистра DMA_CCRx.
После этого начинают обрабатываться запросы от периферийных устройств, подключенных к каналу.
Чтением регистра DMA_CNDTRx можно узнать количество оставшихся для передачи элементов.
- После завершения передачи половины от общего количества элементов, будет установлен флаг HTIFx в регистре DMA_ISR. Произойдет прерывание на это событие, если оно разрешено в регистре DMA_CCRx (бит HTIE).
- После завершения передачи всех данных, будет установлен флаг TCIFx в регистре DMA_ISR. Произойдет прерывание на это событие, если оно разрешено в регистре DMA_CCRx (бит TCIE).
- Если не задан циклический режим, то после пересылки заданного количества данных работа канала останавливается.
- В циклическом режиме (бит CIRC регистра DMA_CCRx) после завершения передачи данных, в регистр DMA_CNDTRx автоматически перезагружается его первоначальное значение, и обмен продолжается бесконечно.
Режим “из памяти в память”.
Режим используется для копирования блоков данных в памяти.
- Для его конфигурации необходимо установить бит MEM2MEM в регистре DMA_CCRx.
- После включения канала битом EN регистра DMA_CCRx сразу начнется передача данных. Дополнительные запросы не требуются.
Режим запрещено использовать совместно с циклическим режимом.
Выравнивание данных разной разрядности.
При DMA передаче данных разрядности приемника и источника могут не совпадать. Действует простое и логичное правило. Всегда сохраняются младшие разряды.
- Если разрядность источника превышает разрядность приемника, то лишние старшие разряды источника игнорируются. Например, 32х разрядное число 0x87654321 передаем в периферию с регистром данных 16 разрядов. В него запишется 0x4321.
- Если разрядность источника меньше разрядности приемника, то недостающие старшие разряды в приемнике будут заполнены нулями. Например, 16ти разрядное число 0x4321 источника загружаем в 32х разрядный приемник. Запишется 0x00004321.
В принципе используется правило преобразования (приведения) типов данных языка C.
Специфика доступа к периферийным устройствам через шины AHB/APB.
Периферийные устройства STM32 подключены к процессору и памяти через шины AHB/APB. Это 32х разрядные шины. Они не поддерживают операций передачи меньшей разрядности. В результате в регистры периферийных устройств можно записывать или считывать из них только 32х разрядные слова.
Многие регистры периферии имеют меньшую разрядность, часто 16 разрядов. В оставшиеся биты при передаче данных DMA тоже что-то будет записываться. В большинстве случае это не важно, разряды не значащие. Но все-таки давайте внесем определенность в этот процесс.
При записи в периферийное устройство байта или полуслова через шину AHB данные на неиспользуемых линиях шины дублируются. Например.
- Если мы записываем полуслово 0x4321, в регистр периферии загрузится 0x43214321.
- Если мы записываем байт 0x21, в регистр периферии загрузится 0x21212121.
- Но если мы укажем разрядность источника слово, т.е. запишем слово 0x87654321, а разрядность периферийного регистра укажем полуслово, то произойдет выравнивание разрядности, описанное в предыдущем абзаце. В результате в регистр загрузится 0x00004321.
Ошибки при DMA передаче.
Ошибка может возникнуть только в результате попытки доступа к зарезервированному адресному пространству. В этом случае канал, в котором произошла ошибка, отключается аппаратным сбросом бита EN. Устанавливается флаг ошибки TEIFx в регистре DMA_ISR. И формируется прерывание, если оно разрешено битом TEIE в регистре DMA_CCRx.
Прерывания DMA.
Для каждого канала могут генерироваться прерывания по следующим флагам регистра DMA_ISR.
- HTIFx – завершена передача половины данных. Разрешается прерывание битом HTIE в регистре DMA_CCRx.
- TCIFx — завершена передача всех данных. Разрешается прерывание битом TCIE в регистре DMA_CCRx.
- TEIFx – ошибка передачи. Разрешается прерывание битом TEIE в регистре DMA_CCRx.
Соответствие каналов DMA периферийным устройствам.
Каналов DMA только 7. Периферийных устройств больше. Поэтому на каждый канал поступают сигналы запросов от нескольких периферийных устройств, объединенные логическим элементом ИЛИ. В каждый момент времени только одно устройство может использовать канал.
Остальные периферийные устройства должны отключить свои запросы DMA. Для этого у каждого устройства с поддержкой DMA есть специальный бит активации/деактивации формирования запроса.
Остается привести таблицу соответствия каналов DMA периферийным устройствам.

На мой взгляд, контроллер DMA один из самых простых и понятных узлов STM32.
В следующем уроке будем разрабатывать программы, использующие контроллер DMA.
Программирование STM32. Часть 8: DMA
Direct memory access (DMA), или прямой доступ к памяти (ПДП) используется для быстрой передачи данных между памятью и периферийным устройством, памятью и памятью, или между двумя периферийными устройствами без участия процессора. В микроконтроллере STM32F103c8 доступен один контроллер DMA1 с 7-ю каналами. DMA2 присутствует только в микроконтроллерах high-density и XL-density. Предыдущая статья здесь, все статьи цикла можно посмотреть тут: http://dimoon.ru/category/obuchalka/stm32f1.
Содержание
- Функциональное описание DMA
- Приоритеты каналов DMA
- Каналы DMA
- Выравнивание данных разной разрядности
- Особенности обращения к периферии AHB/APB
- Кольцевой режим DMA (Circular mode)
- Режим «Из памяти в память» (Memory-to-memory mode)
- Прерывания DMA
- Ошибки при передаче данных по DMA
- Каналы DMA и периферия
Функциональное описание DMA
Контроллер DMA использует системную шину совместно с процессорным ядром. Если CPU и DMA обращаются к одной о той же области памяти или одной и той же периферии, то DMA может приостановить доступ CPU к системной шине, но при этом как минимум половина пропускной способности шины резервируется за CPU. Это означает, что даже при интенсивном обмене данными по DMA процессор не зависнет наглухо.
При возникновении определенного события, периферийное устройство отправляет сигнал запроса в контроллер DMA. После этого запускается процесс обмена данными, который состоит из 3-х шагов:
- Загрузка данных из регистра периферийного устройства (если направление передачи из периферии в память) или загрузка данных из памяти (если направление передачи из памяти в периферию);
- Сохранение загруженных данных в память (если из периферии в память) или в периферийное устройство (если из памяти в периферию);
- Уменьшение значения регистра DMA_CNDTRx на единицу. Как только этот регистр станет равен нулю, то передача данных завершится.
Приоритеты каналов DMA
DMA1 в микроконтроллерах STM32F103c8 имеет 7 каналов, причем в конкретный момент времени передача данных может осуществляться только по одному из них. Однако, если активно несколько каналов, то при одновременном возникновении запросов DMA передача будет запущена для того канала, приоритет которого выше. Приведу небольшой пример. Пусть у нас 3-й канал DMA1 настроен на передачу массива данных в SPI1, а 1-й канал на прием от ADC1. Установим приоритет канала 3 больше, чем у канала 1. В этом случае, если запросы от SPI1 и ADC1 возникнут одновременно, то сначала будет обработан запрос от SPI1 (3-й канал), а уже потом от ADC1 (1-й канал). То есть одновременно включать несколько каналов DMA можно, но одновременно вести передачу может только один из них.
Приоритеты можно настраивать программно, всего 4-е градации:
- Very high priority (Очень высокий приоритет)
- High priority (Высокий приоритет)
- Medium priority (Средний приоритет)
- Low priority (Низкий приоритет)
При одинаковом уровне приоритета (например, 1-й и 3-й канал настроили на Very high priority) канал с меньшим номером будет иметь приоритет над каналом с большим номером (канал 1 будет иметь бОльший приоритет)
Каналы DMA
Каждый канал DMA имеет следующие регистры:
- DMA_CCRx — регистр конфигурации канала DMA. В нем содержатся биты конфигурации передачи данных, биты разрешения прерывания от канала и бит включения канала.
- DMA_CNDTRx — сколько кадров данных подлежит передаче (0..65535). Я намеренно употребил слово «кадр», а не «байт». Дело в том, то есть возможность настроить количество байт данных, которые передаются за одну транзакцию (настраивается в регистре DMA_CCRx), об этом поговорим далее.
- DMA_CMARx — адрес памяти. Если направление передачи из периферии в память, то сюда будем записывать данные, если обратно, то читать.
- DMA_CPARx — адрес периферийного устройства. Если направление передачи из периферии в память, то отсюда будем читать данные, если обратно, то записывать. В режиме Memory-to-memory сюда так же записывается адрес памяти.
Так, вроде все понятно: имеем 4-е регистра, с помощью которых можно настроить пересылку данных туда-сюда.
Теперь самое время поговорить о такой вещи как инкремент адреса памяти и периферии. Все примеры буду приводить для SPI. Инкремент адреса периферии чаще всего не имеет смысла, а вот инкремент памяти очень полезен. Пусть в регистр DMA_CMARx занесен адрес нулевой ячейки массива, который мы ходим отправить в SPI (вспоминаем указатели Си). После каждой отправки данных в SPI внутренний указатель памяти канала DMA будет увеличиваться на 1 элемент массива. Тут стоит отметить один важный момент: инкремент производится внутреннего указателя, который недоступен программно для чтения или записи, регистр DMA_CMARx не меняет своего значения в процессе передачи данных.
На примере SPI работать это будет вот так. В регистр DMA_CMARx занесли адрес нулевого элемента массива, который хотим отправить, в DMA_CPARx адрес регистра данных DR модуля SPI. В DMA_CNDTRx записали количество байт для передачи. Включили инкремент адреса памяти, в модуле SPI разрешили запрос к DMA на передачу данных и запустили процесс, установкой бита EN в регистре DMA_CCRx. В начальном состоянии передатчик SPI пуст, флаг пустого передатчика устанавливается в единицу, это провоцирует зарос DMA. DMA получает запрос от SPI, после этого читает байт данных из массива и записывает его в регистр DR интерфейса SPI, увеличивает внутренний указатель памяти на один элемент массива (1 байт) и уменьшает значение регистра DMA_CNDTRx на единицу. После того, как SPI выплюнет байт данных, процесс повторится. Все это будет продолжаться до тех пор, пока значение DMA_CNDTRx не станет равно нулю. После этого канал DMA завершит передачу и больше не будет реагировать на запросы от SPI.
Но это для случая, если нам надо передавать данные в периферию по одному байту. А что делать, если у нас разрядность массива 2 байта, и периферия хочет на вход 2 байта тоже?
Для таких случаев в регистре DMA_CCRx есть конфигурационные биты разрядности периферийного регистра (PSIZE) и разрядности данных в памяти (MSIZE). Они могут принимать следующие значения:
- 00: 8-bits
- 01: 16-bits
- 10: 32-bits
- 11: Reserved (эта комбинация не используется)
То есть, если мы поставим MSIZE=16 бит (2 байта), то за раз мы будем отправлять уже 2 байта, и указатель на адрес памяти будет увеличиваться на 2. А вот регистр DMA_CNDTRx все так же будет уменьшаться на единицу, так как он содержит не количество байт для передачи, а количество самих передач (транзакций). Получается, что MSIZE нужен для того, чтобы сказать DMA, на сколько байт увеличивать внутренний указатель на адрес памяти. Все верно, но MSIZE используется и еще для одной вещи.
Выравнивание данных разной разрядности
Очень часто бывают ситуации, когда разрядность приемника данных не совпадает с разрядностью источника. Например, в модуле SPI разрядность регистра данных DR равна 16 бит (2 байта, или полуслово). Однако, SPI у нас может быть настроен на передачу 8-и бит за раз и мы имеем массив данных для передачи, с разрядностью 8 бит. DMA позволяет настроить независимо разрядность передатчика и приемника данных. Как было сказано выше, с помощью битов MSIZE мы задаем разрядность данных в памяти. Но есть еще биты PSIZE, которыми надо указать разрядность регистра периферийного устройства (8, 16 или 32 бита). Если PSIZE не равен MSIZE, то DMA будет производить автоматическое выравнивание данных по следующим правилам.
Пусть разрядность источника данных 8 бит, а приемника 16. Тогда при пересылке DMA добавит 8 незначащих нулей к данным из источника и запишет их в приемник: из источника прочитали, например, 0x13, а в приемник записали 0x0013. В случае, если разрядность источника больше разрядности приемника, то DMA обрежет лишние старшие биты у данных из источника, и в приемник запишет только младшие биты: если разрядность источника 32 бита, а приемника 8 бит, то DMA прочтет из источника значение, например, 0xABCDEF12, а в приемник попадет 0x12. В принципе, все как при присвоении значений переменным в Си.
В Reference manual на микроконтроллеры STM32F1xxx в разделе про DMA есть вот такая таблица:

Рис. 1. Правила выравнивания данных DMA
Таблица из Reference manual-а может показаться довольно замысловатой (на самом деле это так и есть ).Давайте разберемся в ней на одном из случае. Например, источник данных у нас 32 бита, а приемник 16 бит:

Рис. 2. Преобразование 32-х битных значений к 16-и битным
Пусть мы пересылаем данные из одного массива в памяти в другой посредством DMA. Причем эти массивы разной разрядности. Массив-источник имеет 32 разряда и содержит следующие данные:
- элемент 0: B3B2B1B0, смещение 0x00
- элемент 1: B7B6B5B4, смещение 0x04
- элемент 2: BBBAB9B8, смещение 0x08
- элемент 3: BFBEBDBC, смещение 0x0C
Если непонятно что такое смещение и почему оно принимает именно эти значения, то необходимо почитать про организацию данных в памяти микроконтроллера и указатели языка Си.
В качестве массива это будет выглядеть вот так:
Ну и приемник данных:
А DMA будет выполнять вот такую операцию:
Думаю, что для людей, знакомых с Си, будет понятен результат такой операции:
В принципе, все то же самое справедливо и для пересылки данных из памяти в регистр периферии, только в том случае не используется инкремент адреса периферии.
Особенности обращения к периферии AHB/APB
Тут есть одна очень важная и не очевидная особенность архитектуры микроконтроллеров STM32. CPU в STM32 является 32-х разрядным, и для записи в память 8, 16 или 32-х бит существуют разные команды и разные запросы на запись. Для ОЗУ ни каких проблем не существует: мы можем выполнять 8, 16 и 32-х разрядные запросы к памяти. А вот к периферии AHB/APB можно обращаться только 32-х битными запросами. А если нам надо выполнить запись в регистр, который имеет разрядность меньше, чем 32 бита? Объясню на примере все того же SPI. Регистр данных DR у него имеет разрядность 16 бит, и старшие 16 бит 32-х разрядной шины просто не используются:

Рис. 3. Карта регистров SPI
Если в DMA мы настроим разрядность периферии PSIZE = 16 бит, и разрядность памяти MSIZE = 16 бит, то DMA продублирует младшие 16 бит в старшие и произведет 32-х битный запрос к периферии:
Т.е. из 0xABCD DMA сделает 0xABCDABCD и это значение отправится в периферию. И так как старшие 16 бит регистра DR не используются (зарезервированы), то старшие 16 бит просто проигнорируются. Так же можно настроить PSIZE = 32 бита, и тогда в регистр DR будет занесено значение 0x0000ABCD. А вот если PSIZE установить 8 бит, то DMA сделает следующее преобразование:
Таким образом, в DR будет занесено 0xABAB а не 0x00AB, как можно подумать, если не знать этих особенностей. Вот как раз из-за того, что к периферии можно обращаться только 32-битными запросами, все регистры в периферии выравнены по границе 32 бита (см. рис. 3).
Кольцевой режим DMA (Circular mode)
Думаю, все знакомы с кольцевым буфером. Его очень удобно использовать при непрерывном приеме/передаче данных. В DMA микроконтроллеров STM32 такой режим работы реализован аппаратно, и включается он битом CIRC в регистре управления DMA_CCRx. Если этот режим активирован, то после передачи всех данных по DMA (после того, как DMA_CNDTRx станет равно нулю), регистр DMA_CNDTRx заново перезагружается исходным значением и передача продолжается.
Режим «Из памяти в память» (Memory-to-memory mode)
В «обычном» режиме канал DMA ждет запроса на передачу данных от какого-либо периферийного модуля (SPI, ADC, таймер, и т.д.) Однако, канал DMA может работать и без запроса от периферии, т.е. передача начнется сразу после установки бита EN в регистре DMA_CCRx. Этот режим может использоваться для копирования одной области памяти в другую. Для этого необходимо в регистры DMA_CPARx и DMA_CMARx занести адреса массивов, над которыми необходимо выполнить операцию копирования, и установить бит MEM2MEM в регистре DMA_CCRx. Получается, что и регистру адреса периферии, и регистру адреса памяти присваиваются адреса массивов в памяти. При пересылке MEM2MEM можно использовать любой свободный канал DMA. А как выбирается направление передачи? Точно так же, как и при обмене данными с периферией: битом DIR регистра DMA_CCRx. Пример передачи из памяти в память будет в одной из следующих статей, там, где мы перейдем к практике. Стоит отметить, что нельзя использовать режим MEM2MEM одновременно с Circular mode.
Прерывания DMA
Каждый канал DMA имеет 3 прерывания:
- Half-transfer — DMA передал половину данных, удобно при потоковой передаче данных совместно с кольцевым режимом: пока передаем одну половину массива, заполняем другую.
- Transfer complete — прерывание о завершении передачи данных.
- Transfer error — прерывание об ошибке передачи.
Ошибки при передаче данных по DMA
Ошибка DMA может возникнуть при чтении/записи в зарезервированное адресное пространство микроконтроллера STM32. При возникновении ошибки соответствующий канал DMA отключается (сбрасывается бит EN) и возникает прерывание Transfer error (если разрешено).
Каналы DMA и периферия
DMA1 в микроконтроллерах STM32F103C8 имеет 7 каналов передачи данных, причем на каждом канале висит своя периферия. Приведу таблицу из Reference manual, чтоб было понятнее:

Рис. 4. Каналы DMA и соответствующие им запросы от периферийных устройств
Например, 1-й канал может обслуживать запросы от ADC1, TIM2_CH3 и TIM4_CH1, а 2-й канал от SPI1_RX, USART3_TX, TIM1_CH1, TIM2_UP и TIM3_CH3. Стоит отметить, что сами запросы должны быть разрешены в регистрах периферийных устройств, при этом, если разрешить DMA-запрос от 2-х источников, то обмен данными будет запускаться от 2-х разных запросов. С ходу не смогу привести пример, где это может быть полезно, и скорее всего такая конфигурация не имеет смысла.
На этом пока все, в следующей статье будет описание регистров DMA, а уже потом перейдем к практике. Продолжение следует.
STM32 — DMA часть первая

В первую очередь нужно напомнить — что бы мы не делали в микроконтроллере, будь то работа с таймером, или приём байт по какой-либо шине данных (USART, I2C, SPI и т.д.), или управление GPIO, или любая другая операция, всё это ни что иное как запись единицы или ноля в определённую ячейку (регистр) памяти.
В stm32 память поделена на разные области, а область с регистрами отвечающими за перечисленную выше периферию, обозначена как Peripheral .

То есть, когда мы хотим подать «плюс» или «минус» на какую-то ножку МК, мы просто записываем единицу или ноль в определённый регистр в области Peripheral , и происходит соединение/разъединение этой ножки с электрической шиной. То же самое происходит с таймером — когда мы хотим его запустить, то записываем единичку в нужный регистр, таймер соединяется с тактирующей шиной и начинает тикать. Если хотим узнать сколько он натикал, то лезем в соответствующий регистр и смотрим что там лежит.
Когда отправляем данные по USART’у, то берём данные из какого-то нашего массива, и последовательно перекладываем их в регистр (USART_DR), а из него они улетают по проводкам. При приёме всё происходит в обратном порядке — байты прилетают в приёмный регистр (он так же называется USART_DR, но для приёма и отправки они разные), а мы обязаны быстренько их оттуда забирать и куда-то сохранять (если забрать не успели, то получим ошибку).
Так вот, к чему я это всё тут понаписал — все эти действия, запись в регистр, чтение, перекладывание из массива в регистр и обратно, производятся с помощью ЦПУ…
То есть ЦПУ тратит своё время на эти операции и само собой программа ничего другого в это время делать не может.И тут мы подошли к основному вопросу — что такое DMA (Direct Memory Access) — прямой доступ к памяти (само название как-бы намекает
).DMA это отдельный блок (или несколько блоков, в зависимости от «жирности» камня), подключённый через Bus Matrix, к различной периферии и различной памяти…

… который может самостоятельно, без участия ЦПУ, перегонять данные из регистров периферии в оперативную память и обратно, либо из периферии в периферию, либо из памяти в память. То есть мы можем сказать DMA, — «вот массив байт в ОЗУ, возьми их и отправь по USART’у». Либо наоборот, — «сиди и жди когда придут данные по USART’у, и сложи их в такой-то массив». А теперь самое главное — вся эта работа будет происходить без какого либо участия ЦПУ, всё будет делать само DMA…
То есть, мы даём команду DMA, и можем дальше выполнять код из программы. Таким образом программа распараллелится, DMA будет получать данные из USART’а, а ЦПУ делать что-то другое. Конечно же не всё так просто, а точнее вообще не просто, шины не резиновые
, и надо учитывать пропускную способность, время доступа, и прочее, но об этом в следующей части.Толстые стрелки на схеме, это шины по которым гоняются данные, а тоненькие (Request) служат для того, чтоб периферия могла подавать различные запросы блоку DMA. Подробнее про Request см. ниже.
Основные возможности
У каждого блока DMA имеется несколько каналов, которые связаны с определённым набором периферии, это нужно смотреть в мануале на конкретный камень, но если вы настраиваете через Куб, то он всё сделает за вас. У некоторых камней есть штуковина под названием DMAMUX (см. в мануале на свой МК), с помощью которой можно настраивать любой канал на работу с любой периферией. В общем это некий мультиплексор каналов.
Каждому каналу DMA можно назначить один из четырёх уровней приоритета (можно оставить одинаковыми). За исполнением приоритетов следит встроенный в блок арбитр (Arbiter). Всё это происходит аппаратно, разработчику нужно только назначить приоритет.
С помощью DMA можно обращаться к Flash, SRAM, периферии на шинах APB1, APB2 и AHB. Любое из устройств может быть как источником, так и приёмником данных. Подробнее нужно смотреть в мануале для конкретного камня.
DMA может вызывать три вида прерываний:
Первый — завершена передача половины данных. Второй — завершена передача всех данных. Прерывание по половинке сделано не спроста, это очень удобный механизм для организации некого подобия «кольцевого буфера». Например, вам нужно непрерывно отправлять какой-то большой объём данных по USART’у, тогда можно сделать так: как только первая половина массива будет отправлена и произойдёт прерывание по половинке, то пока отправляется вторая половина массива, можно заполнять первую половину новыми данными. Как только произойдёт прерывание по завершении передачи всех данных, можно заполнять новыми данными вторую половину массива, и т.д.
Третий вид прерывания, это какая-либо ошибка.
У каждого канала свои прерывания.
Функционал
Механизм работы с DMA очень прост: при копировании данных из памяти в память нужно передать в функцию указатель на буфер-источник, указатель на буфер-приёмник, и количество байт, которые нужно скопировать. Копировать можно байтами (8 бит), полусловами (16 бит), и словами (32 бита).
Если ведётся чтение/запись данных из/в периферийное устройство, то опять же, передаётся указатель на данные, указатель на адрес нужного регистра устройства, и количество данных.
Интерфейсы (I2C, SPI, и т.д.)
Теперь давайте для наглядности настроим USART на передачу массива с помощью DMA…
Тут ничего нового, обычная настройка.Переходим на вкладку DMA Settings, нажимаем кнопку «Add», в выпадающем списке «Select» выбираем USART1_TX, и кликаем появившуюся строчку…

DMA Request — здесь указывается какая периферия будет работать с DMA. Нам нужно отправлять данные поэтому указываем USART1_ТX. Настраивается либо приём, либо отправка. Если нужно ещё и прием, тогда нужно ещё раз нажать кнопку «Add» и выбрать USART1_RX.
Запрос (Request), это ключевая деталь в работе DMA c любой периферией. В случае с USART’ом это выглядит примерно так: когда мы запустим процесс, DMA спросит у USART’а, — «ты готов?», USART ответит — «готов». После чего DMA отправит в USART один байт и будет ждать от него сообщения что он готов к отправке следующего байта, и так до тех пор пока не будет отправлен весь массив.
Если происходит приём данных, то USART сообщает блоку DMA, — «я получил очередной байт, забери его из регистра USART_DR и переложи в массив».
Любая периферия работает с DMA через запросы — запрос-операция, запрос-операция, и т.д. Если же происходит копирование память-память, то без запросов, просто дали команду и данные гонятся из одного места в другое.
Один запрос-операция называется транзакция. То есть приём/отправка одного байта — это одна транзакция.
Channel — канал настроился автоматически, тут нам не о чём беспокоится. В некоторых случаях появляется несколько каналов на выбор — указывайте любой.
Direction — здесь указывается откуда и куда нужно передавать данные. В данном случае мы будем пересылать данные из памяти, то есть из массива который создадим в программе, в регистр USART_DR, то есть в периферию. Если бы мы принимали данные, то указали бы наоборот — периферия-память.
Priority — тут указываем приоритет каналов. Сейчас у нас задействован всего один канал, поэтому неважно что там указанно. Когда будете использовать несколько каналов, тогда сами решайте какой важнее. Работа приоритетов заключается в том, что когда одновременно прилетают два или более запросов, арбитр решает какой канал запустить первым. Как я уже говорил, работа с DMA не такая простая как кажется на первый взгляд, это связано с доступом к шинам. Во второй части я попробую рассказать об этом чуть подробнее, а на первых порах это не сильно важно.
Если приоритеты будут одинаковые у всех каналов, то первым будет запущен канал с меньшим порядковым номером. То же самое относится и к блокам DMA — приоритет у блока с меньшим номером.
Mode — об этом чуть позже.
Increment Address — здесь указываем что нужно приращивать. Поскольку мы будем копировать массив из нескольких байт находящийся в памяти, то соответственно нам нужно чтобы указатель двигался по этому массиву после отправки очередного байта. Что же касается Peripheral, то естественно ничего прибавлять не нужно, байты последовательно кладутся в регистр USART_DR и улетают.
Data Width — тут указываем размерность данных. Мы передаём восьмибитные байты поэтому Byte. Этот параметр настраивается исходя из того с чем работаем — гоняем 16-ти битные данные, тогда указываем полслова, если 32-ух битные, тогда целое слово. Если указать источник 16 бит, а приёмник 8 бит, тогда DMA обрежет каждый 16-ти битный байт до 8. Если сделать наоборот, тогда DMA дополнит его нулями. В некоторых случаях можно настроить так, что в 32-ух битное слово будет записываться два разных 16-ти битных значения, в частности так делается в парном режиме АЦП, про это в следующей части.
Все эти пункты очень важные. Когда настраиваете, будьте внимательны.
В программе перед бесконечным циклом пишем и прошиваем…
Здесь мы передаём в функцию только указатель на массив и количество, но в глубинах HAL’а это выглядит так.
Указатель на источник, указатель на приёмник, и кол-во данных.Что же касается последнего аргумента — количество данных, то оно не должно превышать 65535, вне зависимости от размерности данных. Это связано с тем, что счётный регистр DMA (CNDTR) шестнадцатибитный.
Теперь вернёмся к настройке режима DMA — Mode ⇨ Normal . Сейчас у нас строка выводится на печать один раз, а если указать Mode ⇨ Circular , тогда DMA будет работать в циклическом режиме. То есть строчка будет печататься бесконечно. Этот режим очень пригодится когда нужно что-то делать постоянно, не только отправлять/принимать данные по USART’у, но и при работе с другой периферией или памятью — запустили и забыли — DMA занимается своим делом, а ЦПУ в вашем распоряжении. Если необходимо изменять данные, то делать это нужно в прерывании…
Прерывание от DMA включается автоматически и его нельзя отключить в Кубе…

Если всё же хочется отключить прерывание, то это можно сделать с помощью функции…
… но если отключите, то не узнаете закончило ли DMA работу.
Тут есть особенность, если включён режим Mode ⇨ Normal , то сработает только колбек по половинке, а если Mode ⇨ Circular , то будут срабатывать оба. Если нужно чтоб в режиме Mode ⇨ Normal сработали оба колбека, включите глобальное прерывание…

С другими интерфейсами (I2C, SPI, и т.д.) работа ведётся примерно так же.
Память-память
Чтобы копировать из памяти в память, нужно в Кубе в разделе Peripherals (левая колонка) тыкнуть DMA, и на вкладке MemToMem проделать то же самое что и с USART’ом — кнопка «Add» ⇨ Select ⇨ MEMTOMEM…

Канал можно выбрать любой (кроме четвёртого, он занят уартом). Копировать будем из буфера в буфер, поэтому инкремент и у источника, и у получателя. Размерность (Data Width) указываем исходя из того, что копируем. То есть если бы массив состоял из 16-ти битных значений, то указали бы Half Word, если из 32-ух битных, то Word.
В программе будем копировать существующий массив в массив-получатель…
В функцию DMA передаётся номер канала, указатели на буфер-источник и на буфер-получатель, и кол-во байт. Прерывания эта функция не вызывает. Далее выводим на печать буфер-получатель чтоб убедиться что данные скопировались.
Тут стоит обратить внимание на одну важную деталь. Функция DMA работает параллельно, то есть она неблокирующая, то есть получается, что мы запустили копирование и буквально тут же стали выводить на печать. То есть, по идее, у нас ничего не должно было напечататься, так как ещё не успело скопироваться. Но, поскольку для запуска функции вывода нужно какое-то время, то первые символы успевают скопироваться, а пока присходит их отправка (сам по себе вывод в USART довольно таки долгая операция), DMA успевает докопировать всё остальное. Однако такое может прокатить только с USART’ом или ещё где-то, но в других случаях может понадобиться дождаться окончания работы DMA. Для этого есть специальная функция ожидания…
В нашем примере она бы выглядела так…
Первый аргумент это номер канала, который нужно «подождать», второй означает что нужно дождаться копирования всего буфера (не половинки), а последний это таймаут, как в обычных халовских функциях. В циклическом режиме (Mode ⇨ Circular) эта функция не работает.
В целом конечно это абсолютно бессмысленно, так как теряется суть DMA. Правильней использовать прерывание.
Прерывание нужно включить в разделе NVIC…

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

Последний аргумент это имя самого колбека, который нужно добавить в программу…
Колбек регистрируется один раз, до запуска функции…
Если нужен колбек по половинке, тогда и его тоже надо зарегистрировать, и создать саму функцию…
То есть будет так…
Таймер
Далее поработаем с таймером. Однако перед тем как что-либо настраивать, отключите в Кубе все каналы DMA, так чтоб не осталось ничего связанного с DMA, и перегенерируйте проект. Позже я объясню зачем это нужно.
Теперь настроим первый канал таймера №1 в режиме ШИМа…
Предделитель указываем 7200, переполнение 30000, а длину импульса 1000, больше ничего трогать не надо. Таким образом, при условии что камень работает на частоте 72МГц, у нас получится ШИМ очень сильно растянутый во времени. То есть когда мы подключим лампочку к пину РА8, она через каждые три секунды будет загораться на 100мс, в общем моргать будет раз в три секунды. Это нужно чтоб наглядно продемонстрировать наши дальнейшие действия.В программе, перед бесконечным циклом добавьте запуск таймера и прошейтесь…
Если лампочка моргает тогда продолжим. Предположим вам нужно изменять длину импульса, то есть менять значение сравнения (Pulse), например для плавного увеличения свечения. Если бы у нас не было DMA, тогда пришлось бы в прерывании или в цикле приращивать регистр CCR (Capture/Compare Register) , однако у нас есть DMA, а значит всё будет происходить аппаратно. Суть очень простая, мы создаём буфер с несколькими различными значениями, которые DMA будет подсовывать в регистр сравнения после каждого очередного переполнения таймера.
Идём в настройки нашего таймера и добавляем DMA…

DMA Request — выбираем TIM1_CH1, так как работаем с первым каналом таймера. Запрос к блоку DMA будет посылаться в момент захвата/сравнения.
Если бы работали со вторым или третьим каналом, или со всеми вместе, то они были бы там. Четвёртый канал там уже есть, он ещё может работать как триггер (см. ниже). Это пункт есть только у самых «жирнючих» таймеров. TIM1_UP — запрос к DMA посылается в момент переполнения таймера (см. ниже).
Direction — здесь всё понятно — из буфера в регистр захвата/сравнения таймера.
Mode — указываем циклический режим, чтоб значения в регистре таймера менялись постоянно. Если указать Normal, то буфер прокрутится один раз.
Increment Address — инкрементируем только память, то бишь наш буфер.
Data Width — указываем полслова так как CCR регистр у таймера 16-ти битный.
В программе создаём буфер…
Большой буфер нам не нужен, достаточно пары-тройки значений чтоб наглядно убедится в том, что значения меняются, но вы можете создавать буферы с любым количеством значений.
После запуска у нас будет следующее: сначала светик загорится на 100мс, потом на 1 сек, потом на 2 сек, потом всё повторится, и так бесконечно, у нас ведь DMA в циклическом режиме.
Добавляем запуск таймера, прошиваем, и смотрим…
… и может так случиться, что у вас ничего не работает, точнее светик то будет мигать, но только короткими вспышками, то есть таймер запустился, но DMA не подсовывает новые значения.
Дело вот в чём. Выше я намеренно попросил вас удалить всё что связано с DMA и настроить таймер в обычном режиме, сгенерировать проект, а потом добавить DMA. В результате получилось следующее: Куб сначала сгенерировал инициализацию таймера, а потом инициализацию DMA, то есть так…
В результате когда таймер инициализировался, он ничего не знал про DMA и работал в обычном режиме.Неправильной работой Куба это нельзя назвать, просто если настраиваете какую-либо периферию на работу c DMA, то настраивайте DMA сразу же, тогда инициализация будет прописана в правильном порядке. Либо в настройках Куба на вкладке Project Manager, в разделе Advanced Settings укажите правильный порядок инициализации периферии стрелочками…

В результате должно получится так…
Компилите и прошивайте, теперь всё будет окей.Чтобы уж полностью убедится что всё работает, можно добавить колбеки и посмотреть что лежит в CCR…
Первого значения мы не увидим так как прерывания происходят, как вы помните, по половине и по полному буферу. Три на два не делятся по этомувот тебе один золотой Буратино ©половина в большую сторону.И в довершение начатого сделаем плавное включение/выключение лампочки. Создадим буфер на 2Кб и заполним его значениями…
Предделитель таймера уменьшим до 7, можно это сделать прям в коде, чтоб не лазить в Куб…

Можно прошивать — светик будет плавно разгораться и угасать, а колбеки присылать соответствующие значения…

Всё то же самое, можно проделывать не только в режиме PWM, но и Output Compare, вызывая соответствующую функцию — HAL_TIM_OC_Start_DMA(. ).
Теперь сделаем наоборот, будем захватывать сигнал. В Кубе нужно перенастроить канал на захват и изменить направление DMA, из периферии в память…

Будем подавать сигнал на вход канала таймера и записывать полученные значения в наш буфер с помощью DMA.
Буфер оставляем прежний, а функцию меняем на захват (то что в скобках остаётся неизменным)…
В бесконечном цикле делаем импульсы, которые будем захватывать. Нужно настроить какой-нибудь пин как GPIO_Output и соединить его с РА8. Заодно выведем на печать результат…
Не забудьте про очерёдность инициализации.
Всё работает, но это какая-то чепуха. Чтоб результат был внятный нужно после каждого захвата обнулять счётчик таймера (Counter Period). Можно конечно это делать в прерывании по захвату, но тогда смысл DMA просто потеряется, поэтому мы сделаем так чтоб всё было аппаратно, ибо таймеры у stm32 офигительно крутые
Идём в настройки таймера и делаем его подчинённым самому себе, а триггером будет любой фронт сигнала на первом канале…

TI1_ED — триггером служит любой фронт.
Reset Mode — получая триггерный сигнал таймер обнуляет свой счётчик.
Про таймеры у меня есть отдельные статьи.
Прошиваемся и получаем искомое…
Вуаля, у нас получилось полностью аппаратное измерение длины импульса, ЦПУ не принимает вообще никакого участия в этом процессе.Ну и само собой есть колбеки по заполнению половинки и полного буфера…
Да, я начал с каналов таймера и совсем забыл, что c помощью DMA можно менять и значение переполнения.
Настраиваем таймер просто как измеритель интервалов времени…
Включаем тактирование таймера и делаем так чтоб он переполнялся каждые 200мс.
Запрос к блоку DMA будет происходить во время переполнения — TIM1_UP. Направление передачи из памяти в периферию — из массива в регистр переполнения (Counter Period, он же ARR). Размерность данных Half Word, так как регистр 16-ти битный. И включаем циклический режим.
В программе делаем массив из трёх значений, которые будут поочерёдно записываться в регистр переполнения после каждого переполнения, ну и запускаем таймер в соответствующем режиме…
Колбеки по половинке и по полному буферу…
Timer DMA-burst
Есть ещё один режим работы таймера с DMA, он называется Timer DMA-burst. В этом режиме можно с помощью DMA менять значения в нескольких регистрах таймера за один запрос. До этого мы за один запрос меняли одно значение в одном регистре таймера (CCR).
Поскольку каждый запрос требует нескольких лишних тактов, то Timer DMA-burst работает быстрее чем простой режим DMA.
Например, если понадобится с помощью таймера создать какую-то хитровыдуманную синусойду, где необходимо очень быстро менять не только длину импульса, но и значение переполнения, и ещё какие-нибудь регистры, да ещё и у нескольких каналов сразу, да ещё и всё это делать аппаратно, то режим Timer DMA-burst это то что нужно.
Протестируем этот режим на практике. Удалите в Кубе всё что связанно с таймером, перегенерируйте проект, и настройте четыре канала как PWM Generation c DMA…

DMA Request — указываем TIM1_UP, то есть запрос к блоку DMA будет происходить не в момент сравнения как в прошлом примере, а в момент переполнения таймера. Можно было бы выбрать и сравнение, указав любой из каналов, но для разнообразия сделаем так.
Direction — из памяти в периферию.
В разделе Parameter Settings меняем только один пункт — переполнение…
Всё остальное оставим по нулям. Заполнять эти значения будем с помощью DMA. Переполнение тоже будем изменять, но если его оставить равным нулю, то таймер не запустится.Теперь давайте в бесконечном цикле запустим вывод значений регистров таймера…
В результате мы получим что и ожидали, все регистры кроме ARR равны нулю…
PSK — предделитель (Prescaler). ARR — переполнение (Counter Period). RCR — Repetition Counter. CCRx — регистры захвата/сравнения (Pulse).Список регистров, к которым можно обращаться с помощью Timer DMA-burst находится в файле stm32f1xx_hal_tim.c , перед функцией HAL_TIM_DMABurst_WriteStart(. ) — это та самая функция, с помощью которой запускается режим Timer DMA-burst.

Всего 18 регистров. Тут они обозначены в том виде, в котором их нужно прописывать в качестве аргумента функции.
Ниже указаны параметры, которые тоже нужно прописывать в функцию, а означают они то же самое, что мы указывали в Кубе, в качестве DMA Request. То есть это событие, при котором будет происходить запрос к блоку DMA…

Теперь добавьте перед бесконечным циклом следующий код…
В буфере (buff) лежат семь значений, которые будут записываться в соответствующие регистры. 7200 — предделитель, 30000 — переполнение, 0 — Repetition Counter, последние четыре значения запишутся в регистры сравнения каналов.
Далее запускаем таймер в режиме PWM. Для демонстрации работы нам не обязательно запускать все каналы, достаточно одного. Собственно вообще не важно как мы запустим таймер, можно и так — HAL_TIM_Base_Start(), главное чтоб он работал и в момент переполнения «толкал» DMA.
И наконец запускаем Timer DMA-burst.
Второй аргумент этой функции означает с какого регистра начать запись, а последний аргумент говорит о том, что нужно переписать семь регистров. То есть первый элемент буфера будет записан в регистр PSC (TIM_DMABASE_PSC), второй в ARR (TIM_DMABASE_ARR), третий в RCR (TIM_DMABASE_RCR), и т.д., семь регистров…

Третий аргумент указывает что транзакция будет происходить во время переполнения таймера, а четвёртый указатель на буфер.
Прошиваем плату и смотрим на результат…
Все семь регистров перезаписались.Если бы нам нужно было перезаписать только регистр ARR, тогда буфер будет таким…
… а функция такая…
Запись начинается с регистра ARR, а количество перезаписываемых регистров равно одному, то есть перезаписываем только один регистр.

Если нужно переписать регистры начиная с ARR и заканчивая CCR2, тогда делаем буфер из четырёх элементов, и в функции указываем что нужно перезаписывать четыре регистра…

Как вы уже наверно поняли, нельзя исключить перезапись каких-либо регистров находящихся в середине. То есть, если нам нужно перезаписывать регистр ARR и CCRx, то исключить RCR из этой операции не получится. Однако в этом нет ничего страшного так как никаких лишних ресурсов на перезапись «ненужного» регистра не тратится.
Сейчас у нас все значения записываются один раз, и особого смысла в этом нет, так как это можно сделать и вручную. А вот чтобы они изменялись динамически, нужно включить циклический режим DMA — Mode ⇨ Circular (сделайте сейчас), и внести изменения в код.
Воспользуемся последним примером и добавим в буфер ещё значений…
Здесь добавлено ещё два раза по четыре значения: при первом запросе будут отправлены первые четыре элемента, при втором следующие четыре, и при третьем последние четыре, после этого всё повторится.
Функция запуска будет немного другая — HAL_TIM_DMABurst_MultiWriteStart(. ) , у неё добавлен ещё один аргумент (последний), в котором указывается длина буфера. В нашем случае это будет число 12, то есть три раза по четыре значения…
Всё как задумано, меняются значения в регистрах ARR, CCR1 и CCR2. В ARR чередуются значения — 3000, 1000, 1234. В CCR1 — 1111, 5555, 7777. В CCR2 — 2222, 4444, 8888. В RCR постоянно записывается 0.Регистр RCR есть только у таймеров №1 и №8, но не смотря на это, работа с другими таймерами происходит так же, то есть как будто этот регистр есть.
В режиме Timer DMA-burst можно не только записывать в регистры, но читать из них. Это делается с помощью функции HAL_TIM_DMABurst_ReadStart(. ) . Всё то же самое что и запись, но только наоборот. Откровенно говоря не знаю какой практический смысл в этой функции. Разве что во время захвата на всех каналах таймера читать значения всех CCRx.
Чтобы остановить запись/чтение есть соответствующие функции…
На этом первая часть заканчивается, в следующей части описана работа с GPIO, АЦП, и ещё много чего.
Всем спасибо

