Dma chain core что это
Программирование ARM-контроллеров STM32 на ядре Cortex-M3. Часть 14. Использование DMA
DMA (Direct Memory Access) — технология прямого доступа к памяти. Эта технология позволяет быстро и без использования центрального процессора пересылать данные из одной области памяти в другую. При этом для такой пересылки вместо ЦП используется свой специальный контроллер, который называется контроллером DMA.
Контроллер DMA работает независимо от ЦП и параллельно с ним. Таким образом использование DMA позволяет экономить ресурсы ЦП, который во время операций пересылки данных с помощью DMA может заниматься какими-то другими полезными делами.
Несмотря на то, что ЦП и DMA работают независимо друг от друга, DMA может приостанавливать доступ ЦП к системной шине на несколько тактов в случаях когда они оба пытаются обратиться к одним и тем же адресам памяти.
В микроконтроллеры stm32 может быть встроено до 2-х контроллеров DMA — DMA1 и DMA2. Они имеют следующие особенности:
Карта запросов модуля DMA1:
Карта запросов модуля DMA2:
Как вообще работает DMA? Всё происходит следующим образом:
Сама передача данных состоит из трех шагов:
В зависимости от настроек, после каждой транзакции модуль 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.
DMA_IFCR — регистр сброса флагов статуса прерываний. Установка битов этого регистра приводит к сбросу флагов прерываний в регистре DMA_ISR. Биты регистра DMA_IFCR доступны только для записи.
DMA_CCRx — регистры настройки каналов (x=1..7 — номер канала).
DMA_CNDTRx — регистры-счётчики (x=1..7 — номер канала). В младшие 16 бит этих регистров прописывается количество данных для передачи (то есть сколько транзакций нужно выполнить с соответствующим каналом). Каждый из этих регистров обладает следующими особенностями:
DMA_CPARx — регистры адреса (x=1..7 — номер канала). Здесь содержатся начальные адреса регистров периферии в которую или из которой нужно передавать данные по запросу от соответствующего канала. Доступ автоматически выравнивается на границу полуслова или слова, в зависимости от установленного для периферии размера порции данных (достигается игнорированием одного или двух младших бит регистра адреса). Регистры не могут быть перезаписаны пока соответствующий канал включен.
DMA_CMARx — регистры адреса (x=1..7 — номер канала). Здесь содержатся начальные адреса областей памяти в которую или из которой нужно передавать данные по запросу от соответствующего канала. Доступ автоматически выравнивается на границу полуслова или слова, в зависимости от установленного для памяти размера порции данных (достигается игнорированием одного или двух младших бит регистра адреса). Регистры не могут быть перезаписаны пока соответствующий канал включен.
Для модуля DMA2 существуют точно такие же регистры, только x в них может принимать значения 1..5, а не 1..7 (поскольку в DMA2, в отличии от DMA1, всего 5 каналов, а не 7).
Работать с DMA достаточно просто. Всё, что от Вас требуется — это настроить соответствующий канал и далее только обрабатывать события (которых как мы помним для канала всего 3) или прерывания от них, а также, возможно, взводить счётчик передаваемых данных.
Как обрабатывать события и прерывания — решать только Вам, а вот порядок настройки канала приведён ниже:
В библиотеке StdPeriph настройку канала можно выполнить одной функцией — DMA_Init, для включения/выключения используется функция DMA_Cmd.
Примеры работы с DMA приводить не буду, их можно посмотреть в примерах работы с другими модулями (например, в примерах работы с UART или ADC), так что на этом всё.
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. Жду отзывов и дополнений 😉
Прямой доступ к памяти
Прямой доступ к памяти (англ. Direct Memory Access, DMA ) — режим обмена данными между устройствами или же между устройством и основной памятью (RAM) без участия Центрального Процессора (ЦП). В результате скорость передачи увеличивается, так как данные не пересылаются в ЦП и обратно.
Кроме того, данные пересылаются сразу для многих слов, расположенных по подряд идущим адресам, что позволяет использование т. н. «пакетного» (burst) режима работы шины — 1 цикл адреса и следующие за ним многочисленные циклы данных. Аналогичная оптимизация работы ЦП с памятью крайне затруднена.
В оригинальной архитектуре IBM PC (шина ISA) был возможен лишь при наличии аппаратного DMA-контроллера (микросхема с индексом Intel 8237).
DMA-контроллер может получать доступ к системной шине независимо от центрального процессора. Контроллер содержит несколько регистров, доступных центральному процессору для чтения и записи. Регистры контроллера задают порт (который должен быть использован), направление переноса данных (чтение/запись), единицу переноса (побайтно/пословно), число байтов, которое следует перенести.
ЦП программирует контроллер DMA, устанавливая его регистры. Затем процессор даёт команду устройству (например, диску) прочитать данные во внутренний буфер. DMA-контроллер начинает работу, посылая устройству запрос чтения (при этом устройство даже не знает, пришёл ли запрос от процессора или от контроллера DMA). Адрес памяти уже находится на адресной шине, так что устройство знает, куда следует переслать следующее слово из своего внутреннего буфера. Когда запись закончена, устройство посылает сигнал подтверждения контроллеру DMA. Затем контроллер увеличивает используемый адрес памяти и уменьшает значение своего счётчика байтов. После чего запрос чтения повторяется, пока значение счётчика не станет равно нулю. По завершении цикла копирования устройство инициирует прерывание процессора, означающее завершение переноса данных. Контроллер может быть многоканальным, способным параллельно выполнять несколько операций.
Содержание
Захват шины (bus mastering)
В шинах MicroChannel, SBus, разработанной под их большим влиянием PCI и её концептуальных производных AGP и PCI-X, используется иная реализация DMA. Эти шины позволяют любому устройству заявить о возникновении потребности к захвату шины, таковая потребность удовлетворяется т. н. арбитром при первой возможности. Устройство, успешно осуществившее захват шины, самостоятельно выставляет на шину сигналы адреса и управления и исполняет в течение какого-то времени ту же ведущую роль на шине, что и ЦП. Доступ ЦП к шине при этом кратковременно блокируется.
В такой реализации DMA не существует DMA-контроллера, а также номера входа DMA-контроллера.
Некоторые старые устройства PCI, а именно, реализации звуковых карт семейства Sound Blaster, использовали тот же DMA-контроллер 8237 из оригинальной архитектуры IBM PC. Такое использование является, безусловно, устаревшим для PCI, но поддерживалось с целью обеспечить полную совместимость по ПО и драйверам с версиями Sound Blaster для шины ISA.
Данная поддержка называется Distributed DMA (D-DMA) и реализована аппаратным образом как в устройстве, так и в логике моста PCI-ISA, в которой на PCI-системах размещена и логика оригинального IBM PC DMA контроллера 8237. Реализация включает в себя 2 запроса: сначала от устройства мосту PCI-ISA, затем от моста основной памяти.
Кроме упомянутых реализаций Sound Blaster, практически никакие устройства PCI не используют понятие «номер входа DMA-контроллера», как и 8237 вообще.
DMA и виртуальная память, IOMMU и AGP GART
В операционных системах со страничной виртуальной памятью, таких, как Windows и семейство UNIX, непрерывный регион виртуальных адресов может быть реализован разрывно расположенными физическими страницами.
Исполнение DMA по такому региону представляет собой довольно сложную задачу. Также сложной задачей является исполнение DMA по отгружаемой памяти.
Решение этой задачи требует выявления физических страниц, реализующих регион, и их блокировку от отгрузки обращением к подсистеме виртуальной памяти. Далее становится возможным нахождение физических адресов страниц региона, которые в общем случае не являются непрерывными и формируют так называемый «список рассеяния/сборки» (англ. scatter-gather list — SGL).
Задача исполнения DMA по таковому списку может быть решена одним из следующих способов.
1. Выделение подряд идущей физической памяти в ядре операционной системы и промежуточное копирование всех данных туда/оттуда (т. н. «буфер отскока» — англ. bounce buffer ).
Недостатки: трата времени процессора на копирование, потребление крайне ограниченного ресурса непрерывной физической памяти, занятие места в ограниченной части памяти, к которой есть доступ у DMA (первый гигабайт на x86).
2. Разбиение операции на подоперации по границам элементов SGL, с прерыванием в конце каждой операции.
Использовалось в старых 8-битных SCSI-контроллерах, поставляемых со сканерами типа HP ScanJet.
Недостатки: большое количество прерываний.
3. Поддержка SGL самим устройством, с требованием копирования SGL, преобразованного в формат, специфичный для устройства, в устройство через многочисленные обращения к регистрам устройства.
Недостатки: крайне высокая сложность устройства, невысокая производительность большого числа записей в регистры.
4. Поддержка SGL самим устройством, с требованием размещения SGL, преобразованного в формат, специфичный для устройства, в физически непрерывном регионе основной памяти.
Устройство читает SGL тем же механизмом DMA с захватом шины, что и собственно данные, тем самым реализуя функциональность некоего процессора, читающего и исполняющего свою собственную «программу», реализованную как список дескрипторов SGL. Данная архитектура называется «цепной DMA» (англ. chain DMA ), реализована в практически всем стандартном оборудовании современного компьютера — Intel IDE (в примитивном виде), UHCI и OHCI USB, OHCI 1394, а также в большинстве PCI-адаптеров, Ethernet и SCSI (даже в устаревшем AIC78xx). Хороший пример реализации данной архитектуры в очень сложном и развитом виде дан в спецификации оборудования OHCI 1394. По некоторым сведениям, данная архитектура под названием «канальные программы» использовалась ещё в IBM 360, известных в СССР как ЕС ЭВМ.
Недостатки: высокая сложность устройства, хотя и ниже в числе транзисторов, чем предыдущий вариант. Например, контроллер UHCI USB (согласно спецификации на сайте Intel) требует около 5000 транзисторов.
5. Поддержка SGL в межшинном оборудовании, при которой представление физически разрывного буфера для стороны устройства выглядит физически непрерывным.
Недостатки: требование сложной логики уже не в устройстве, а в платформе.
DMA и IDE/ATA, Ultra DMA
Первоначальный контроллер жесткого диска IBM PC/AT не поддерживал DMA, и требовал передачи всех данных дискового ввода/вывода инструкциями REP INSW/REP OUTSW через порт 0x1f0.
В начале 90х годов диски MFM/RLL вымерли, сменившись дисками IDE, но регистровый интерфейс ПО к контроллеру не изменился.
Низкая производительность такого контроллера стала серьёзной проблемой, особенно на системах PCI. Помимо требования нескольких циклов PCI на 2 байта переданных данных, это приводило к загрузке процессора дисковым вводом-выводом.
Для решения проблемы ряд компаний, в том числе Intel, разработали контроллеры IDE с поддержкой DMA. Контроллеры были и есть несовместимы по ПО между различными производителями, хотя совместимость всех Intel IDE/ATA/SATA снизу вверх более или менее поддерживается.
Также особенностью этой поддержки является использование новых команд протокола IDE/ATA, а значит, и требование поддержки DMA не только контроллером, но и самим жестким диском.
Около 2000 года поддержка DMA по шине IDE/ATA развилась в сторону увеличения тактовой частоты шины, что потребовало нового типа кабеля от контроллера к диску с удвоенным числом проводников меньшего размера. Эта технология называлась Ultra DMA (UDMA).
Многие операционные системы требовали действий администратора для использования IDE DMA. Так, например, стандартные ядра Linux до примерно 2004 года не имели такой поддержки, требовалось перестроение ядра с отредактированным файлом конфигурации.
В семействе Windows поддержка IDE DMA появилась сначала только для Intel в пакетах обновлений к Windows NT4, и требовала на большинстве систем ручного редактирования реестра для задействования.
В Windows 2000 это требование исчезло, но появилось требование обязательной вписки даже не-загрузочных дисков в BIOS и обязательного выставления режима DMA для них в настройках BIOS. Эти настройки BIOS становились видимы ядру ОС через технологию ACPI, и ОС не позволяла включить DMA для диска, не вписанного в BIOS. Для сравнения: NT4 поддерживала и произвольный размер диска, и DMA без вписки диска в BIOS.
В системах Linux для включения или выключения IDE DMA вручную может применяться команда hdparm (см. ниже). Современные версии ядра автоматически включают DMA режим, что можно наблюдать в сообщениях отладки (строки вида ata1.00: configured for UDMA/133 или hda: UDMA/33 mode selected).
[Воркшоп] DMA-атаки на практике. Эксплоит через прямой доступ к памяти
В эту субботу 1 февраля 2020 г. в нашем Хакспейсе Нейрон в Москве пройдет мастер-класс по практическому использованию DMA-атак. Вместе мы будем взламывать реальный компьютер с зашифрованной файловой системой, имитирующий банкомат или платежный терминал.
Direct Memory Access (DMA) — низкоуровневый режим работы компьютерных устройств, предполагающий прямой доступ к оперативной памяти компьютера. Он требуется для работы PCIe, Thunderbolt и некоторых других устройств. В нормальных условиях DMA используется для более быстрого доступа к памяти, чтобы не занимать процессор.
С помощью специального «злого» устройства атакующий может захватить контроль над шиной PCIe и получить полный доступ на чтение и запись в память работающего компьютера, даже если программно система защищена от проникновений.
DMA-атаки позволяют
Что будет на занятии
Вначале мы «на пальцах» разберемся, как работает шина PCIe и доступ к памяти, почему такие атаки возможны и какие существуют современные средства защиты от подобных атак. Рассмотрим, какие существуют инструменты для проведения DMA-атак и как лучше проектировать защищенные системы.
ValdikSS расскажет о своем опыте применения DMA-атаки для взлома защиты японского игрового автомата.
Часть 2 — практическое занятие
Для проведения атаки мы будем использовать два компьютера: атакующего и жертву. В PCIe-порт жертвы вставляется специальное «злое» устройство, реализующую физический уровень PCIe и пересылающее команды от атакующего. Атакующий компьютер подключается к «злой» плате по USB, и через нее посылает команды на шину PCIe жертвы.
В качестве жертвы будет выступать обычный X86-компьютер, а плата USB3380 — в качестве «злого» устройства. На стороне атакующего будет использоваться фреймворк pcileech.
Разберем, какие устройства поддерживает pcileech в качестве атакующих, и что лучше выбрать. Настроим с нуля стенд атакующего на основе платы USB3380.
Изначально компьютер-жертва будет иметь зашифрованный с помощью bitlocker жесткий диск и заблокированную для входа в операционную систему.
Мы выполним такие атаки:
Для кого это занятие
Занятие будет полезно разработчикам встраиваемых систем, тем, кто проектирует терминалы, банкоматы, станки, игровые и гемблинговые автоматы. Вам потребуются базовые знания работы аппаратных частей компьютера.
Фреймворк pcileech достаточно простой и имеет несколько удобных плагинов для типовых атак, поэтому научиться им пользоваться вполне сможет любой продвинутый пользователь компуктера.
Об авторах
ValdikSS — исследователь безопасности и энтузиаст открытого ПО. Автор программы для обхода систем DPI GoodByeDPI, и сервисов ПростоVPN и Антизапрет. Работал в Digital Security. ________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
Максим Горячий — разработчик встроенных систем и исследователь по безопасности в Positive Technologies. Интересуется криптографией, технологиями виртуализации, обратной разработкой и всем, что связано с аппаратным обеспечением. Выступал на 33C3, 34C3, Black Hat. Вместе с Марком Ермоловым нашел уязвимости в Intel ME, Apple MacOS Firmware. _________________________________________________________________________________________________________________________________________________________________________
Место встречи — Хакспейс Нейрон
Хакспейс Нейрон — сообщество гиков и техноэнтузиастов в центре Москвы. Рабочие места с профессиональным оборудованием и творческая атмосфера.
Все собранные деньги пойдут на оплату аренды и развитие Хакспейса. Если вы хотите поддержать нас дополнительно, вы можете задонатить и предложить свою помощь.
Внимание: вся информация представленная на мастер-классе носит исключительно исследовательский характер. Тестовый стенд для анализа уязвимостей не является реально эксплуатируемой системой и создан специально для тренировочных целей. Автор не призывает использовать полученные знания для совершения противоправных действий.
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 миллионов тактов) и рассматривать производительность в комплексе.