DMA для новичков или то, что вам нужно знать
Всем привет, сегодня мы с вами поговорим о DMA: именно о той технологии, которая помогает вашему компьютеру воспроизводить для вас музыку, выводить изображение на экран, записывать информацию на жесткий диск, и при этом оказывать на центральный процессор просто мизерную нагрузку.
DMA, что это? О чем вы говорите?
DMA, или Direct Memory Access – технология прямого доступа к памяти, минуя центральный процессор. В эпоху 486-ых и первых Pentium во всю царствовала шина ISA, а также метод обмена данными между устройствами – PIO (Programmed Input/Output).
Когда объемы данных, которыми оперирует процессор начали возрастать, стало понятно, что нужно минимизировать участие процессора в цепочке обмена данными, а то прийдется туго. И вот тогда активное применение нашла технология прямого доступа к памяти.
Кстати говоря, DMA используется не только для обмена данными между устройством и ОЗУ, но также между устройствами в системе, возможен DMA трансфер между двумя участками ОЗУ (хотя данный маневр не применим к x86 архитектуре). Также в своем процессоре Cell, IBM использует DMA как основной механизм обмена данными между синергетическими процессорными элементами (SPE) и центральным процессорным элементом (PPE). Также каждый SPE и PPE может обмениватся данными через DMA с оперативной памятью. Данный прием – на самом деле большое преимущество Cell, ибо избавляет от проблем когерентности кешей при мультипроцессорной обработке данных.
И снова теория
Прежде чем мы перейдем к практике, я бы хотел осветить несколько важных аспектов программирования PCI, PCI-E устройств.
Я вскользь упомянул о регистрах устройства, но как же к ним имеет доступ центральный процессор? Как многие из вас знают, есть такая сущность в компьютерных технологиях, как IO порты (Input/Output ports). Они предназначены для обмена информацией между центральным процессором и периферийными устройствами, а доступ к ним возможен с помощью специальных ассемблерных инструкций — in/out. BIOS (или OpenFirmware на PPC based системах) на ранних этапах инициализации PCI устройств, а также некоторых других (Super IO контроллера, контроллера PS/2 устройств, ACPI timer и т.д.), закрепляет за определенным контроллером собственный диапазон IO портов, куда и отображаются регистры устройства.
Итак, существует два метода утилизации DMA: contiguous DMA и scatter/gather DMA.
Contiguous DMA
Scatter/gather DMA
С ростом скорости Ethernet адаптеров, contiguous DMA показал свою несостоятельность. В основном из-за того, что требовались области памяти достаточно большого размера, которые подчас невозможно было выделить, так как в современных системах фрагментация физической памяти достаточно высока. Во всем виноват механизм виртуальной памяти, без которого нынче никуда 🙂
Решение напрашивается само собой: использовать вместо одного большого участка памяти несколько, но в разных регионах этой самой памяти. Возникает вопрос, но как же сообщить контроллеру устройства, как инициировать DMA трансфер и по какому адресу писать данные? И тут нашли решение, использовать дескрипторы, чтобы описывать каждый вот такой участок в оперативной памяти.
На сегодня пожалуй все, иначе информации станет слишком много. В следующей статье я покажу вам, как с этой уличной магией работает IOKit. Жду отзывов и дополнений 😉
DMA: мифы и реальность
Введение
В прошлой статье («Часть 2: Использование блоков UDB контроллеров PSoC фирмы Cypress для уменьшения числа прерываний в 3D-принтере») я отметил один очень интересный факт: если автомат в UDB изымал данные из FIFO слишком быстро, он успевал заметить состояние, что новых данных в FIFO нет, после чего переходил в ложное состояние Idle. Разумеется, меня заинтересовал этот факт. Вскрывшиеся результаты я показал группе знакомых. Один человек ответил, что это всё вполне очевидно, и даже назвал причины. Остальные были удивлены не менее, чем я в начале исследований. Так что некоторые специалисты не найдут здесь ничего нового, но неплохо бы донести эту информацию до широкой общественности, чтобы её имели в виду все программисты для микроконтроллеров.
Не то чтобы это был срыв каких-то покровов. Оказалось, что всё это отлично задокументировано, но беда в том, что не в основных, а в дополнительных документах. И лично я пребывал в счастливом неведении, считая, что DMA — это очень шустрая подсистема, которая позволяет резко повысить эффективность программ, так как там идёт планомерная перекачка данных без отвлечения на те же команды инкремента регистров и организации цикла. Насчёт повышения эффективности – всё верно, но за счёт чуть иных вещей.
Но обо всём по порядку.
Эксперименты с Cypress PSoC
Сделаем простейший автомат. У него будет условно два состояния: состояние покоя и состояние, в которое он будет попадать, когда в FIFO имеется хотя бы один байт данных. Войдя в такое состояние, он просто изымет эти данные, после чего снова провалится в состояние покоя. Слово «условно» я привёл не случайно. У нас два FIFO, поэтому я сделаю два таких состояния, по одному на каждое FIFO, чтобы убедиться в том, что они полностью идентичны по поведению. Граф переходов у автомата получился таким:
Флаги для выхода из состояния Idle определяем так:
Не забываем на входы Datapath подать биты номера состояния:
Наружу мы выводим две группы сигналов: пару сигналов, что в FIFO имеется свободное место (для того чтобы DMA могли начать закачивать в них данные), и пару сигналов, что FIFO пусты (чтобы отображать этот факт на осциллографе).
АЛУ будет просто фиктивно забирать данные из FIFO:
Давайте я покажу детализацию для состояния «0001»:
Ещё я поставил разрядность шины, какая была в проекте, на котором я заметил данный эффект, 16 бит:
Переходим к схеме самого проекта. Наружу я выдаю не только сигналы о том, что FIFO опустошилось, но и тактовые импульсы. Это позволит мне обойтись без курсорных измерений на осциллографе. Я могу просто считать такты пальцем.
Как видно, тактовую частоту я сделал 24 мегагерца. У процессорного ядра частота точно такая же. Чем ниже частота, тем меньше помех на китайском осциллографе (официально у него полоса 250 МГц, но то китайские мегагерцы), а замеры все будут вестись относительно тактовых импульсов. Какая бы частота ни была, система всё равно отработает относительно них. Я бы и один мегагерц поставил, но среда разработки запретила мне вводить значение частоты процессорного ядра менее, чем 24 МГц.
Теперь тестовые вещи. Для записи в FIFO0 я сделал такую функцию:
Слово ROM в имени функции связано с тем, что отправляемый массив хранится в области ПЗУ, а Cortex M3 имеет Гарвардскую архитектуру. Скорость доступа к шине ОЗУ и шине ПЗУ может различаться, хотелось это проверить, поэтому меня есть аналогичная функция для отправки массива из ОЗУ (у массива steps в её теле отсутствует модификатор static const). Ну, и есть такая же пара функций для посылки в FIFO1, там отличается регистр приёмника: не F0, а F1. В остальном все функции идентичны. Так как особой разницы в результатах я не заметил, рассматривать буду результаты вызова именно приведённой выше функции. Жёлтый луч — тактовые импульсы, голубой — выход FIFO0empty.
Сначала проверим правдоподобность, почему FIFO заполнено на протяжении двух тактов. Посмотрим этот участок поподробнее:
По фронту 1 данные попадают в FIFO, флаг FIFO0enmpty падает. По фронту 2 автомат переходит в состояние GetDataFromFifo1. По фронту 3 в этом состоянии происходит копирование данных из FIFO в регистр АЛУ, FIFO опустошается, флаг FIFO0empty вновь взводится. То есть осциллограмма ведёт себя правдоподобно, можно считать на ней такты на цикл. Получаем 9 штук.
Итого, на осмотренном участке, на копирование одного слова данных из ОЗУ в UDB силами DMA требуется 9 тактов.
А теперь то же самое, но силами процессорного ядра. Сначала — идеальный код, слабо достижимый в реальной жизни:
что превратится в ассемблерный код:
Никаких разрывов, никаких лишних тактов. Две пары тактов подряд…
Сделаем код чуть более реальным (с накладными расходами на организацию цикла выборку данных и инкремент указателей):
полученный ассемблерный код:
На осциллограмме видим всего 7 тактов на цикл против девяти в случае DMA:
Немного о мифе
Если честно, для меня первоначально это было шоком. Я как-то привык считать, что механизм DMA позволяет быстро и эффективно переносить данные. 1/9 от частоты шины — это не то, чтобы очень быстро. Но оказалось, что никто этого и не скрывает. В документе TRM для PSoC 5LP даже имеется ряд теоретических выкладок, а документ «AN84810 — PSoC 3 and PSoC 5LP Advanced DMA Topics» детально расписывает процесс обращения к DMA. Виной всему латентность. Цикл обмена с шиной занимает некоторое количество тактов. Собственно, именно эти такты и играют решающую роль в возникновении задержки. В общем, никто ничего не скрывает, но это надо знать.
Если знаменитый GPIF, используемый в FX2LP (другой архитектуре, выпускаемой фирмой Cypress), скорость ничем не ограничивает, то здесь ограничение скорости обусловлено латентностями, возникающими при обращениях к шине.
Проверка DMA на STM32
Я был под таким впечатлением, что решил провести эксперимент на STM32. В качестве подопытного кролика был взят STM32F103, имеющий такое же процессорное ядро Cortex M3. У него нет UDB, из которого можно было бы вывести служебные сигналы, но проверить DMA вполне можно. Что такое GPIO? Это набор регистров в общем адресном пространстве. Вот и прекрасно. Настроим DMA в режим копирования «память-память», указав в качестве источника реальную память (ПЗУ или ОЗУ), а в качестве приёмника — регистр данных GPIO без инкремента адреса. Будем слать туда поочерёдно то 0, то 1, а результат фиксировать осциллографом. Для начала я выбрал порт B, к нему было проще подключиться на макетке.
Мне очень понравилось считать такты пальцем, а не курсорами. Можно ли сделать так же на данном контроллере? Вполне! Опорную тактовую частоту для осциллографа возьмём с ножки MCO, которая у STM32F10C8T6 связана с портом PA8. Выбор источников для этого дешёвого кристалла не велик (тот же STM32F103, но посолиднее, даёт гораздо больше вариантов), подадим на этот выход сигнал SYSCLK. Так как частота на MCO не может быть выше 50 МГц, уменьшим общую тактовую частоту системы до 48 МГц. Будем умножать частоту кварца 8 МГц не на 9, а на 6 (так как 6 * 8 = 48):
MCO запрограммируем средствами библиотеки mcucpp Константина Чижова (дальше я все обращения к аппаратуре буду вести через эту замечательную библиотеку):
Ну, и теперь задаём вывод массива данных в GPIOB:
Полученная осциллограмма очень похожа на ту, что была на PSoC.
В середине большой голубой горб. Это идёт процесс инициализации DMA. Голубые импульсы слева получены чисто программным путём на PB1. Растянем их пошире:
2 такта на импульс. Работа системы соответствует ожидаемой. Но теперь посмотрим покрупнее область, отмеченную на основной осциллограмме тёмно-синим фоном. В этом месте уже работает блок DMA.
10 тактов на одно изменение линии GPIO. Вообще-то, работа идёт с ОЗУ, а программа зациклена в постоянном цикле. Обращений к ОЗУ от процессорного ядра нет. Шина полностью в распоряжении блока DMA, но 10 тактов. Но на самом деле, результаты не сильно отличаются от увиденных на PSoC, поэтому просто начинаем искать Application Notes, относящийся к DMA на STM32. Их оказалось несколько. Есть AN2548 на F0/F1, есть AN3117 на L0/L1/L3, есть AN4031 на F2/F4/F77. Возможно, есть ещё какие-то…
Но, тем не менее, из них мы видим, что и здесь во всём виновата латентность. Причём у F103 пакетные обращения к шине у DMA невозможны. Они возможны для F4, но не более, чем для четырёх слов. Дальше снова возникнет проблема латентности.
Попробуем выполнить те же действия, но при помощи программной записи. Выше мы видели, что прямая запись в порты идёт моментально. Но там была скорее идеальная запись. Строки:
при условии таких настроек оптимизации (обязательно следует указать оптимизацию для времени):
превратились в следующий ассемблерный код:
В реальном копировании будет обращение к источнику, к приёмнику, изменение переменной цикла, ветвление… В общем, масса накладных расходов (от которых, как считается, как раз и избавляет DMA). Какая будет скорость изменений в порту? Итак, пишем:
Этот код на C++ превращается в такой ассемблерный код:
8 тактов в верхнем полупериоде и 6 — в нижнем (я проверил, результат повторяется для всех полупериодов). Разница возникла потому, что оптимизатор сделал 2 копирования на каждую итерацию. Поэтому 2 такта в одном из полупериодов добавляются на операцию ветвления.
Грубо говоря, при программном копировании тратится 14 тактов на копирование двух слов против 20 тактов на то же самое, но силами DMA. Результат вполне документированный, но весьма неожиданный для тех, кто ещё не читал расширенную литературу.
Хорошо. А что будет, если начать писать данные сразу в два потока DMA? Насколько упадёт скорость? Подключим голубой луч к PA0 и перепишем программу следующим образом:
Сначала осмотрим характер импульсов:
Пока идёт настройка второго канала, скорость копирования для первого выше. Затем, когда идёт копирование в паре, скорость падает. Когда первый канал закончил работу, второй начинает работать быстрее. Всё логично, осталось только выяснить, насколько именно падает скорость.
Пока канал один, запись занимает от 10 до 12 тактов (цифры плавают).
Во время совместной работы получаем 16 тактов на одну запись в каждый порт:
То есть, скорость падает не вдвое. А что если начать писать сразу в три потока? Добавляем работу с PC15, так как PC0 не выведен (именно поэтому в массиве выдаётся не 0, 1, 0, 1. а 0x0000,0x8001, 0x0000, 0x8001. ).
Здесь результат настолько неожиданный, что я отключу луч, отображающий тактовую частоту. Нам не до измерений. Смотрим на логику работы.
Пока не закончил работу первый канал, третий не начал работы. Три канала одновременно не работают! Что-то на эту тему можно вывести из AppNote на DMA, там говорится, что у F103 всего две Engine в одном блоке (а мы копирует средствами одного блока DMA, второй сейчас простаивает, и объём статьи уже такой, что его я в ход пускать не стану). Перепишем на пробу программу так, чтобы третий канал запустился раньше всех:
Картинка изменится следующим образом:
Третий канал запустился, он даже работал вместе с первым, но как в дело вступил второй, третьего вытеснили до тех пор, пока не закончил работу первый канал.
Немного о приоритетах
Собственно, предыдущая картинка связана с приоритетами DMA, есть и такие. Если у всех работающих каналов указан один и тот же приоритет, в дело вступают их номера. В пределах одного заданного приоритета, у кого номер меньше, тот и приоритетней. Попробуем третьему каналу указать иной глобальный приоритет, возвысив его над всеми остальными (попутно повысим приоритет и второму каналу):
Теперь ущемлённым станет первый, который раньше был самым крутым.
Итого, мы видим, что даже играя в приоритеты, больше двух потоков на одном блоке DMA у STM32F103 запустить не получится. В принципе, третий поток можно запустить на процессорном ядре. Это нам позволит сравнить производительность.
Сначала общая картинка, на которой видно, что всё работает в параллель и у процессорного ядра скорость копирования выше всех:
А теперь я дам возможность всем желающим посчитать такты в то время, когда все потоки копирования активны:
Процессорное ядро приоритетней всех
Теперь вернёмся к тому факту, что при двухпоточной работе, пока настраивался второй канал, первый выдавал данные за различное число тактов. Этот факт также хорошо документирован в AppNote на DMA. Дело в том, что во время настройки второго канала, периодически шли запросы к ОЗУ, а процессорное ядро имеет при обращении к ОЗУ больший приоритет, чем ядро DMA. Когда процессор запрашивал какие-то данные, у DMA отнимались такты, оно получало данные с задержкой, поэтому производило копирование медленней. Давайте сделаем последний на сегодня эксперимент. Приблизим работу к более реальной. После запуска DMA будем не уходить в пустой цикл (когда обращений к ОЗУ точно нет), а выполнять операцию копирования из ОЗУ в ОЗУ, но эта операция не будет относиться к работе DMA ядер:
Местами цикл растянулся с 16 до 17 тактов. Я боялся, что будет хуже.
Начинаем делать выводы
Собственно, переходим к тому, что я вообще хотел сказать.
Начну издалека. Несколько лет назад, начиная изучать STM32, я изучал существовавшие на тот момент версии MiddleWare для USB и недоумевал, зачем разработчики убрали прокачку данных через DMA. Видно было, что исходно такой вариант имелся на виду, затем был убран на задворки, а под конец остались только рудименты от него. Теперь я начинаю подозревать, что понимаю разработчиков.
В первой статье про UDB я говорил, что хоть UDB и может работать с параллельными данными, заменить собой GPIF он вряд ли сможет, так как у PSoC шина USB работает на скорости Full Speed против High Speed у FX2LP. Оказывается, есть более серьёзный ограничивающий фактор. DMA просто не успеет доставлять данные с той же скоростью, с какой доставляет их GPIF даже в пределах контроллера, не принимая во внимание шину USB.
Как видим, нет единой сущности DMA. Во-первых, каждый производитель делает его по-своему. Мало того, даже один производитель для разных семейств может варьировать подход к построению DMA. Если планируется серьёзная нагрузка на этот блок, следует внимательно проанализировать, будут ли удовлетворены потребности.
Наверное, надо разбавить пессимистический поток одной оптимистической репликой. Я даже выделю ее.
DMA у контроллеров Cortex M позволяют повысить производительность системы по принципу знаменитых Джавелинов: «Запустил и забыл». Да, программное копирование данных идёт чуть быстрее. Но если надо копировать несколько потоков, никакой оптимизатор не сможет сделать так, чтобы процессор всех их гнал без накладных расходов на перезагрузку регистров и закручивание циклов. Кроме того, для медленных портов процессор должен ещё ждать готовности, а DMA делает это на аппаратном уровне.
Но даже тут возможны различные нюансы. Если порт всего лишь условно медленный… Ну, скажем, SPI, работающий на максимально возможной частоте, то теоретически возможны ситуации, когда DMA не успеет забрать данные из буфера, и произойдёт переполнение. Или наоборот — поместить данные в буферный регистр. Когда поток данных один, вряд ли это произойдёт, но когда их много, мы видели, какие удивительные накладки могут возникать. Чтобы бороться с этим, следует разрабатывать задачи не обособленно, а в комплексе. А тестерам стараться спровоцировать подобные проблемы (такая у тестеров деструктивная работа).
Ещё раз повторю, что эти данные никто не скрывает. Но почему-то всё это обычно содержится не в основном документе, а в Application Notes. Так что моя задача была именно обратить внимание программистов на то, что DMA — это не панацея, а всего лишь удобный инструмент.
Но, разумеется, не только программистов, а ещё и разработчиков аппаратуры. Скажем, у нас в организации сейчас разрабатывается большой программно-аппаратный комплекс для удалённой отладки встраиваемых систем. Идея состоит в том, что кто-то разрабатывает некое устройство, а «прошивку» хочет заказать на стороне. И почему-то не может предоставить оборудование на сторону. Оно может быть громоздким, оно может быть дорогим, оно может быть уникальным и быть «нужно самим», с ним могут работать разные группы в разных часовых поясах, обеспечивая этакую многосменную работу, оно может постоянно доводиться до ума… В общем, причин придумать можно много, нашей группе просто спустили эту задачу, как данность.
Соответственно, комплекс для отладки должен уметь имитировать собой как можно большее число внешних устройств, от банальной имитации нажатия кнопок до различных протоколов SPI, I2C, CAN, 4-20 mA и прочего, прочего, прочего, чтобы через них эмуляторы могли воссоздавать различное поведение внешних блоков, подключаемых к разрабатываемому оборудованию (лично я в своё время сделал много имитаторов для наземной отладки навесного оборудования для вертолётов, у нас на сайте соответствующие кейсы ищутся по слову Cassel Aero).
И вот, в ТЗ на разработку спущены определённые требования. Столько-то SPI, столько-то I2C, столько-то GPIO. Они должны работать на таких-то предельных частотах. Кажется, что всё понятно. Ставим STM32F4 и ULPI для работы с USB в режиме HS. Технология отработанная. Но вот наступают длинные выходные с ноябрьскими праздниками, на которых я разобрался с UDB. Увидев неладное, я уже по вечерам получил те практические результаты, что приведены в начале этой статьи. И понял, что всё, конечно, здорово, но не для данного проекта. Как я уже отметил, когда возможная пиковая производительность системы приближается к верхней границе, следует проектировать всё не раздельно, а в комплексе.
А здесь комплексного проектирования задач не может быть в принципе. Сегодня идёт работа с одним сторонним оборудованием, завтра — совсем с другим. Шины будут использоваться программистами под каждый случай эмуляции по их усмотрению. Поэтому вариант был отвергнут, в схему было добавлено некоторое количество различных мостов FTDI. В пределах моста одна-две-четыре функции будут разрулены по жёсткой схеме, а между мостами всё будет разруливать USB хост. Увы. В данной задаче я не могу доверять DMA. Можно, конечно, сказать, что программисты потом выкрутятся, но часы на процесс выкрутасов – это трудозатраты, которых следует избегать.
Но это крайность. Чаще всего следует просто держать ограничения подсистемы DMA в уме (например, вводить поправочный коэффициент 10: если требуется поток 1 миллион транзакций в секунду, учитывать, что это не 1 миллион, а 10 миллионов тактов) и рассматривать производительность в комплексе.
Электроника для всех
Блог о электронике
Контроллер прямого доступа к памяти (DMA) контроллера STM32
Работа с контроллером DMA
▌Что это?
Есть в современных контроллерах такой блок DMA — контроллер прямого доступа к памяти (ПДП). Штука очень простая, несмотря на умное название. Вся ее суть заключается в том, чтобы по команде от периферии или ядра взять и скопировать кусок памяти с одного места на другой.
Что с этим можно делать? Ой да много чего. Можно, задать в качестве источника адрес какой-либо периферии, скажем выходной регистр АЦП, а в качестве приемника адрес массива в ОЗУ, дать приказ DMA по команде завершения оцифровки АЦП хватать результат и пихать в массив. При этом DMA сам скопирует, сам увеличит адрес, а как заполнит буфер, то обнулится и начнет его переписывать по кругу заново.
И вот у нас, автоматически, без работы проца вообще, образуется циклический буфер приема данных с АЦП, которые мы можем тут же прогнать через любую цифровую обработку, да хоть через усреднение, и получить отфильтрованные уже данные. Или то же самое сделать с UART и заставить DMA аккуратно складывать входящие данные в кольцевой буфер.
А можно сделать и наоборот. Сказать DMA что мол вот тебе буфер, там лежит пара сотен байт, возьми и запихай их все в жерло UART и пойти по своим делам, а DMA трудолюбиво отправит в передачу весь буфер.
▌Ближе к теме
Контроллеры
Если рассмотреть конкретно STM32 то там находится один (на малых контроллерах вроде STM32F103C8T6) или два DMA контроллера DMA1 и DMA2 соответственно. На DMA1 есть 7 каналов, на DMA2 всего 5. Оба канала DMA сидят на шине AHB и перед тем как начать с ним работать надо на него подать тактирование, подняв биты DMA1EN и DMA2EN в регистре RCC_AHBENR, как и с любой другой периферией на STM32. В остальном они идентичные и работа с первым и вторым одинакова.
Каналы контроллера
Каждый канал независимый и может работать сам по себе со своими настройками. Но в один момент времени может работать только один канал у каждого контроллера. Чтобы избежать коллизий у каждого канала есть два уровня приоритета. Первый, программный. Мы просто в битах настройки задаем один из четырех уровней. А второй аппаратный, если придут запрос на два канала с одинаковым приоритетом в настройках, то победит тот, чей номер меньше. Сходно с обработкой прерываний. Там такой же двухступенчатый арбитраж.
За выбор приоритета отвечают два бита PL регистра DMA_CCRx Для каждого канала регистр свой. Вариантов там немного, всего четыре:
Каждый канал привязан к конкретной периферии. Т.е. если вам нужно чтобы DMA пинал АЦП, то ищите к какому каналу подключено именно АЦП.

Обратите внимание на то, что помимо аппаратных событий в мультиплексор еще ведет софтверный триггер от бита MEM2MEM. Для каждого канала:
Размер данных
За раз DMA может копировать порцию данных в 1, 2 или 4 байта. В первую очередь это влияет на приращение адресов при вычислении куда класть. Так что это настройка жизненно важна. Сколько и куда класть определяется обычно периферией. Т.е. если UART принимает и выдает по байту, то результат у нас 8 битный. А вот АЦП, может, например, выдать 16 битный результат. Значит размер указывать надо два байта, чтобы сразу за один заход их все забрать. Ну и, очевидно, что размер принимаемых и сохраняемых данных обычно совпадает. Хотя, вам никто не запретить класть однобайтные данные периферии в 32 разрядный массив, выравниваясь по двойному слову. Тогда размер может быть и разный.
За размер данных периферии и памяти отвечают два бита PSIZE и MSIZE регистра DMA_CCRx
00 — 1 байт
01 — 2 байта
10 — 4 байта
11 — не используется.
Откуда куда
Адрес периферии для каждого канала задается в регистре DMA_CPARх этого канала. Просто пишем туда адрес нужного регистра периферии. Но есть два важных момента. Во-первых, нельзя писать в этот регистр при включенном DMA. Т.е. при изменении их бит EN должен быть снят. Второе, адрес зависим от битов PSIZE регистра DMA_CCRx. Т.е. если у нас указан размер данных как 1 байт (PSIZE = 00), то активные все биты регистра DMA_CPARx. Но если данные указаны как слова или как двойное слово, по 16 или 32 бита соответственно, то один или два младших бита этого регистра игнорируются вообще. Т.е. получается, что адрес выравнивается по словам или двойным словам. Т.е. DMA не сможет записать данные словами начиная с нечетного адреса, но адреса все выровнены по словам, так что это пофигу.
Адрес памяти лежит в аналогичном регистре DMA_CMARx и там все то же, что и для DMA_CPARx только за размер отвечают биты MSIZE и его тоже нельзя трогать на включенном канале.
Также надо указать направление копирования. За него отвечает бит DIR регистра DMA_CCRx.
Когда он 0 то мы читаем из адреса DMA_CPARx и пишем по адресу DMA_CMARx. А когда он 1, то наоборот, соответственно. Название у бита идиотское. Не, ну понятно, что направление, но лучше бы назвали его M2P, то есть если 1, то из памяти в периферию. Или как то так. Долго никак не мог запомнить направление, пока не связал, что 0 он такой округлый и похож на такую же округлую букву P — Periph. А 1 угловатая, прям как буква М — Мemory.
Ну и опции инкремента адреса. биты PINC и MINC во все том же DMA_CCRx. Они отвечают за то, чтобы после каждой сработки у нас автоматически увеличивался адрес которые в DMA_CPARx или с DMA_CMARx соответственно.
Адрес периферии прибит намертво и редко когда надо его менять, так что обычно PINC всегда равно нулю. Т.е. никакого инкремента. Вам же не надо, чтобы после чтения из DR того же UART1 на следующем байте было уже из следующего по списку BRR :)))
Хотя… в некоторых случаях таким образом можно сдернуть по DMA содержимое всех регистров настройки какой-нибудь периферийной штуки. Например для отладки, чтобы сделать это предельно быстро, на лету. Правда тут надо быть очень осторожным. Многие биты событий и флаги прерываний снимаются при чтении. И чтение через DMA тоже считается. Так что побыть бесстрастным наблюдателем не выйдет.
Зачем же нужен инкремент DMA_CPARx? А для режима копирования из памяти в память. Тогда мы в DMA_CPARx пишем адрес одного буфера, в DMA_CMARx адрес другого, ставим бит MEM2MEM, даем разрешение и поехали!
А вот бит MINC ставится почти всегда. Мы указываем DMA_CMARx начало буфера и DMA, увеличивая адрес, его последовательно заполняет, или читает из него в периферию.
Впрочем, если нам надо гнать из периферии в периферию, скажем из АЦП сразу в SPI, то бит MINC тоже равен нулю будет.
Ну и есть еще один вариант, когда инкремента нет ни на адресе приемника, ни на адресе источника. Таким образом делаются прямые перегонки, например, из АЦП сразу в USART, минуя процессор. Или на SPI. Так можно в десяток строк кода превратить STM32 в какой нибудь вариант SPI АЦП 🙂
Также есть бит CIRC, который разрешает цикличный режим. Т.е. как только DMA отработает свое количество циклов заданных в регистре DMA_CNDTRx так адрес сбрасывается в исходное положение. Таким образом, можно указать в DMA_CNDTRx размер буфера и получить циклических буфер нахаляву, аппаратно.
Сколько?
За то сколько раз должно отработать DMA отвечает регистр DMA_CNDTRx. На каждую сработку от входного сигнала (или от постоянно стоящего бита MEM2MEM) DMA копирует один обьект и уменьшает число в DMA_CNDTRx и так до нуля. Дойдет до нуля, канал выключится. Бит EN тут уже ничего решать не будет. Но если стоит бит CIRC, то регистр перезагрузится исходным значением и продолжит работу. Значение может быть до 65535, используются только младшие 16 бит, старшие два байта ДОЛЖНЫ БЫТЬ НУЛЕМ ВСЕГДА.
Записать в регистр DMA_CNDTRx можно только при выключенном DMA канале. Читать можно когда угодно, по нему можно определять сколько объектов осталось DMA передать.
И тут есть важный нюанс. Я намеренно выше говорил, что «сколько осталось объектов». DMA_CNDTRx Считает не БАЙТЫ, а сколько раз DMA сработал. А за одну сработку, он зависимости от настроек, может пересунуть 1, 2 или 4 байта.
Т.е. если вы откуда то скопипастите код в котором будет что-то вида:
Разумеется изменив PSIZE и MSIZE на 32 бита, то вы получите веселуху: sizeof(IN_Buffer) от 10 uint32_t даст вам 40 и DMA пропишет вам в оперативку 40 раз по четыре байта, захреначив все до куда дотянется :)))) Так что либо корректируйте результат операции sizeof с учетом разрядности данных, либо напрямую указывайте сколько у вас повторений в подходе.
▌Обратная связь
Жевать байты это замечательно, но должен же этот контроллер как то сообщать фоновой программе, что «он сделяль»? Само собой. И реализовано это совершенно традиционно, через прерывания. Их у него три:
За включение этих прерываний отвечают биты
TEIE: Transfer error interrupt enable
HTIE: Half transfer interrupt enable
TCIE: Transfer complete interrupt enable
Регистра DMA_CCRx. У каждого канала свои. С прерываниями тут тоже все щедро. У каждого канала свой вектор, если заглянете в startup_stm32f103xb.s файл, то там будет что то вида:
Это вектора прерываний и есть. Чуть ниже, если вообще есть, будут и вектора для контроллера номер два. А понять же по какому поводу нас вызвало можно из регистра DMA_ISR — Interrupt status registry. Он на каждый контроллер DMA свой. В нем стоят все флаги какие только можно, оптом для всех каналов сразу. Сюда можно только только смотреть. Регистр read only.
Для сброса флага нужно записать 1 в соответственный ему бит регистра DMA_IFCR — Interrupt flag clear registry. Запись нуля же не означает ничего. Так что пишем сразу маску и не паримся.
▌Инциализация и запуск
Вот теперь самое интересное. Прочитали выше написанное, поняли что нужно сделать… А теперь важно сделать это все в правильном порядке. DMA все критические настройки у DMA требуют, чтобы канал был выключен в момент и изменения. За включение и выключение отвечает бит EN регистра DMA_CCRx. Причем крайне желательно ставить этот бит отдельно от всех и снимать отдельно от всех.
Иначе могут быть приколы. Я тут недавно прикольные вилы словил. Сделал процедурку инициализации, думаю, а чего это я их буду по одному ставить? В одном же слове все, дай их сразу и пропишу как надо? За один заход. Выставил все биты конфигурации, на UART1 — работает. Ну окей, взял ЭТИ ЖЕ функции и накатил их на инициализацию UART2 с ТОЧНО таким же кодом, только про UART2, т.е. поменял только имена регистров и каналов DMA. Запускаю… не работает, хоть убейся. Первый UART через DMA работает, второй нет. И так и эдак… Ничего не понимаю. Ладно я бы что-то не то сделал, так тогда бы оба не работали…
Стал под отладчиком ходить, смотреть по регистрам DMA, а у меня запись в регистр конфигурации втором случае DMA_CCRx не проиходит. Т.е. бит должен записаться, а не записывается. Стал разбираться что за фигня и как это так получается вообще? Оказалось, что это оптимизатор так решил, что ему удобней будет записать в одном случае (который работает) сначала старший байт в порт, а потом младший. При этом бит EN записывается последним, а во втором случае наоборот. Записывает младший, т.е.бит EN, а при записи старшего происходит аппаратный сброс этого бита EN. ИЧСХ в даташите ни разу не сказано, что бит EN может сниматься аппаратно. Нигде. Но это происходит.
Второй прикол связан с тем, что при записи в регистры DMA надо явно указывать тип данных, чтобы компилятор сделал запись обои байт, старшего и младшего. Пусть даже один из них все нули. Т.к. если
указать биты как:
#define DMAEnable (1 CCR; // Копируем биты настройки tmp &= CCR_CLEAR_Mask; // и стираем все кроме битов EN. А он и так будет 0 tmp |= Conf; // Закатываем на результат наши биты настроек. Channel->CNDTR = Size; // Заполняем все нужные поля. Размер передчи Channel->CPAR = Perif; // Адрес периферии Channel->CMAR = Mem; // Адрес в памяти Channel->CCR = tmp; // Записываем настройки в память. >
Эти две фукнции просто включают и выключают определенный канал.
void DMA_Enable(DMA_Channel_TypeDef* Channel) < Channel->CCR |= DMA_CCR1_EN; > void DMA_Disable(DMA_Channel_TypeDef* Channel) < Channel->CCR &= (uint16_t)(
Еще нужна процедурка деинициализации DMA, чтобы вернуть все настройки в изначальное состояние, как после сброса:
void DMA_DeInit(DMA_Channel_TypeDef* Channel) < Channel->CCR &= (uint16_t)(
DMA_CCR1_EN); Channel->CCR = 0; Channel->CNDTR = 0; Channel->CPAR = 0; Channel->CMAR = 0; if (Channel == DMA1_Channel1) < /* Reset interrupt pending bits for DMA1 Channel1 */ DMA1->IFCR |= DMA1_Channel1_IT_Mask; > else if (Channel == DMA1_Channel2) < /* Reset interrupt pending bits for DMA1 Channel2 */ DMA1->IFCR |= DMA1_Channel2_IT_Mask; > else if (Channel == DMA1_Channel3) < /* Reset interrupt pending bits for DMA1 Channel3 */ DMA1->IFCR |= DMA1_Channel3_IT_Mask; > else if (Channel == DMA1_Channel4) < /* Reset interrupt pending bits for DMA1 Channel4 */ DMA1->IFCR |= DMA1_Channel4_IT_Mask; > else if (Channel == DMA1_Channel5) < /* Reset interrupt pending bits for DMA1 Channel5 */ DMA1->IFCR |= DMA1_Channel5_IT_Mask; > else if (Channel == DMA1_Channel6) < /* Reset interrupt pending bits for DMA1 Channel6 */ DMA1->IFCR |= DMA1_Channel6_IT_Mask; > else if (Channel == DMA1_Channel7) < /* Reset interrupt pending bits for DMA1 Channel7 */ DMA1->IFCR |= DMA1_Channel7_IT_Mask; > else if (Channel == DMA2_Channel1) < /* Reset interrupt pending bits for DMA2 Channel1 */ DMA2->IFCR |= DMA2_Channel1_IT_Mask; > else if (Channel == DMA2_Channel2) < /* Reset interrupt pending bits for DMA2 Channel2 */ DMA2->IFCR |= DMA2_Channel2_IT_Mask; > else if (Channel == DMA2_Channel3) < /* Reset interrupt pending bits for DMA2 Channel3 */ DMA2->IFCR |= DMA2_Channel3_IT_Mask; > else if (Channel == DMA2_Channel4) < /* Reset interrupt pending bits for DMA2 Channel4 */ DMA2->IFCR |= DMA2_Channel4_IT_Mask; > else < if (Channel == DMA2_Channel5) < /* Reset interrupt pending bits for DMA2 Channel5 */ DMA2->IFCR |= DMA2_Channel5_IT_Mask; > > >
И, собственно, примеры:
▌Копирование одного массива в другой. Режим MEM2MEM
// Массив который копируем и куда копируем static uint32_t INbuff[10] = <0xFFFFFFF1,0xFFFFFFF2,0xFFFFFFF3,0xFFFFFFF4,0xFFFFFFF5,0xFFFFFFF6,0xFFFFFFF7,0xFFFFFFF8,0xFFFFFFF9,0xFFFFFF10>; static uint32_t OUTbuff[10] = <0>; // Включаем тактирование DMA RCC->AHBENR |= RCC_AHBENR_DMA1EN; // Обнуляем канал который будем использовать. Канал берем от балды. Для этой цели подойдет любой свободный. DMA_DeInit(DMA1_Channel3); // Настраиваем DMA_Init( DMA1_Channel3, // Какой канал работать будет (uint32_t)INbuff, // Откуда (uint32_t)OUTbuff, // Куда 10, // Сколько. 10 двойных слов, не байтов. Массив у нас на 10 элементов TransCompl_Int_Disable + // Прерывание по передаче выключено HalfCompl_Int_Disable + // Прерывание по половине выключено TransError_Int_Disable + // Прерывание по ошибке выключено ReadPerif + // Читаем из «периферии». CircularMode_Disable + // Циклический режим не нужен. Копируем один раз. PeripheralInc_Enable + // Увеличиваем адрес источника MemoryInc_Enable + // Увеличиваем адрес приемника PDataSize_DW + // Размер источника двойной слово MDataSize_DW + // Размер приемника двойное слово DMA_Priority_Low + // Низкий приоритет M2M_Enable ); // Копирование память-память. // Разрешаем копирование. DMA_Enable(DMA1_Channel3); >
Вуаля! Данные будут скопированы.
▌Копирование из периферии в буфер памяти.
В данном случае из выходного регистра USART в кольцевой буфер в ОЗУ. Все что попадет в USART окажется в памяти автоматом.
static volatile char BufferForRecieving1[256]; // Кольцевой приемный буфер. RCC->AHBENR |= RCC_AHBENR_DMA1EN; // Подали тактирование на DMA DMA_Disable(DMA1_Channel5); // Выключили канал. DMA_DeInit(DMA1_Channel5); // Обнулили DMA канал USART1->SR &=
Теперь все что попадает в USART будет DMA утаскивать прямо в буфер, остается только его проверять. Ну или включить прерывания по половине и/или окончании передачи и реагировать на них.
▌Копирование из буфера в периферию
И после подачи разрешения буфер будет загружен в UAR побайтно, по мере отправки байт через последовательный интерфейс.
Ну и сами два файлика которые я использую как библиотечку для DMA
Спасибо. Вы потрясающие! Всего за месяц мы собрали нужную сумму в 500000 на хоккейную коробку для детского дома Аистенок. Из которых 125000+ было от вас, читателей EasyElectronics. Были даже переводы на 25000+ и просто поток платежей на 251 рубль. Это невероятно круто. Сейчас идет заключение договора и подготовка к строительству!
А я встрял на три года, как минимум, ежемесячной пахоты над статьями :)))))))))))) Спасибо вам за такой мощный пинок.
22 thoughts on “Контроллер прямого доступа к памяти (DMA) контроллера STM32”
Я на СТМ8 пользовался как раз для фоновой обработки АЦП в кольцевой буфер. очень удобно оказалось
Там он вроде бы очень похож на тот, что в стм32.
Ещё есть интересные примеры использования DMA+таймеров есть в этой теме: http://kazus.ru/forums/showthread.php?t=107109
сообщение 8 — управление восьмисегментными индикаторами
сообщение 9 — управление дисплеями HD44780/WH1602A
Находил ещё вот такой интересный материал про DMA: https://habr.com/ru/post/437112/. Не пугайтесь, там только вначале про Cypress, а потом уже про STM32. Был несколько удивлен после прочтения и некоторых размышлений.
Хм, а чему тут удивляться? Дма это не про скорость, а про параллельность. В чем ее и прелесть.
насчет параллельности тоже ведь не все просто. Сколько одновременно потоков может обращаться к памяти? судя по статье с хабра — 2 (по крайней мере для описаного МК). при этом не важно кто: ядро или DMA каналы …
но опять же, в наших то применениях этого и не замечаешь особо.
кстати, а может кто знает: если ОЗУ физически разделена на отдельные регионы (SRAM1, SRAM2, …), то глядя на матрицу шин в даташите, возникает вопрос: возможен ли одновременный доступ ядра к одной области и DMA к другой?
Вопрос чисто теоритический, но вдруг кто на практике проверял)
В данном случае главное, что в это время код выполняется тоже.
Доступ вряд ли доступен одновременно. На то и приоритеты даны, ядро будет приоритетней.
Для приема по USART можно использовать IDLE line detected, чтобы не ждать середины/конца заполнения буфера.
А я DMA настроил на вывод картинки на экран. В памяти сделал фреймбуфер, натравил на этот буфер DMA, и теперь просто рисую в этот буфер, и всё это само появляется на экране. Удобно, быстро и не напрягает процессор кучей прерываний по TXE SPI.
Почитайте внимательно мануал раздел 9.13.17. После программного сброса бита DMA_CCR_EN, нужно дождаться его фактического сброса, циклически перечитывая CCR, и тестируя текущее значение CCR_EN. Иначе ваша инициализация на работающем DMA может с некоторой вероятностью обломиться. И ещё неплохо бы раскрыть тему FIFO, которое позволяет многократно сократить нагрузку на системную шину со стороны DMA.
А вы часом семейства не путаете? В F103 я ничего такого не нашел в описании (RM00008).
Глава 9 вообще про GPIO и там всего 5 подглав.
Да, действительно, я перепутал, это для F4 так, поэтому код от F1 нельзя просто без изменений перенести на старшие семейства, несмотря на внешнюю одинаковость регистров, он вроде и будет работать, но на самом деле не совсем:).
А вот это интересное замечание получилось! SPL она же вроде сквозная идет. В том числе и на ф4, но там я не увидел выборки контроллера в библиотеке и не проверяется бит ЕН.
SPL для каждого семейства своя, хотя у них кое-как совместимый внешний API.
>> …оптимизатор так решил, что ему удобней будет записать в одном случае (который работает) сначала старший байт в порт, а потом младший.
>>
>> …при записи в регистры DMA надо явно указывать тип данных, чтобы компилятор сделал запись обои байт, старшего и младшего.
>>
>> #define DMAEnable ((uint16_t)(1<>
>>
Регистры имеют тип uint32_t, константы (1 uint32_t при присвоении не должно порождать кода преобразования вообще. Откуда чудеса?
Я думаю корни растут из Thumb2 который позволяет упаковку данных в памяти побайтно и работу с отдельными байтами. Так что он вполне может упихать константу в байт.
Не в тему, но про надежность кода — плюсовать битовые параметры не есть хорошая идея, лучше использовать битовый OR
Согласен, если какой-нибудь бит по какой-либо причине продублируется, то в случае «+» будет непонятный глюк, а в случае OR всё в порядке.
Привет. Сори, не знаю где спросить, пишу сюда. Почему в руководстве по Си http://easyelectronics.ru/file/yazyk-programmirovaniya-s-spravochnik/124 после пункта 1.8 Not found страницы?
Они просто недооформлены. Можете попробовать по ключевым фразам из текста загуглить и найти эту методичку.















