Я заметил, что в сообществе FPGA многие задают вопросы, которые можно решить с помощью DMA. Сделал поиск по Хабру в поисках чистых статей о том, как запустить DMA и не нашел таких. Поэтому решил в этой статье собрать свои знания в кучу и показать, как пользуюсь DMA . Это будут чистые примеры, без лишней информации, также будут сравнительные тесты разного характера.
Введение
Начнем с определения DMA . DMA (Direct Memory Access) — это механизм, который позволяет периферии напрямую читать и записывать данные в память без участия процессора. Проще говоря - DMA берёт на себя грязную работу по перекачке данных, освобождая CPU для полезных задач.
Из этого термина сразу вытекает ответ на вопрос «А когда мне нужен DMA в проекте?»: он нужен тогда, когда стоит вопрос передачи большого объема данных, особенно если эти данные будут поступать постоянно и на большой скорости.
IP Catalog в Vivado
Если открыть IP Catalog, то можно увидеть, что DMA бывают разные:

В рамках этой статьи нас интересует только AXI Direct Memory Access. К тому же работал я только с ним. Данный модуль является достаточно универсальным инструментом, с учетом внутренних настроек.
Настройки IP DMA

Enable Asynchronous Clock - этот параметр всегда серый, и я не знаю когда он включается, а так же не понимаю его смысла, ведь у DMA каналы для чтения, записи и управляющий интерфейс имеют разные клоки, видимо функция включена.
Enable Scatter Gather Engine – это более сложный режим работы DMA , к тому же имеет больше гибкости в работе. И в рамках этой статьи я не буду использовать этот режим. Именно из-за этого у названия статьи есть приписка «Simple Mode».
Enable Micro DMA – это опция для упрощённого режима работы при передачи пакетов до 8 слов. Я им никогда не пользовался и дать оценку насколько этот механизм хорош не могу.
Enable Multi Channel Support – этот параметр доступен только при включенном SG режиме. Отвечает за количество каналов работы DMA. Изменяются не физические интерфейсы, а добавляются такие сигналы как tdest и tid в AXI-Stream интерфейсах.
Enable Control / Status Stream – без SG режима не доступен. Не пользовался и не знаю, что дает.
Width of Buffer Length Register – это разрядность счетчика байт размера передаваемого пакета. Я не понимаю в каком случае может понадобится его уменьшать или увеличивать, но на первых порах всегда спотыкался на том, что забыл изменить значение на нужное. И как итог DMA зависал.
Enable Read Channel/Enable Write Channel – это включение каналов работы DMA. В случае для чтения это будет канал «память-поток», в случае записи это будет «поток-память». При включении и отключении галочек будут включаться нужные интерфейсы.
Number of Channel – количество каналов, так как режим SG выключен то разное количество каналов не доступно. По-умолчанию всегда 1 канал на интерфейс.
Memory Map Data Width – разрядность шины данных интерфейса, которые смотрят в адресное пространство, это M_AXI_MM2S и M_AXI_S2MM.
Stream Data Width – разрядность шины данных потоковых интерфейсов, это S_AXIS_S2MM и M_AXIS_MM2S. Этот параметр как и параметр выше стоит выбирать исходя из здравого смысла. Если во всей системе стоит ширина данных 32 бита, то при увеличении разрядности в самом DMA вы не получите прирост скорости. Тут нужно считать пропускную способность интерфейсов с учетом того, что интерфейсы MemoryMap имеют небольшие просадки в пропускной способности в сравнении со Stream интерфейсами.
Max Burst Size – Максимальный размер передаваемой порции в адресном пространстве. Как правило это размер в словах. Пока этот размер данных не будет записан или прочитан, другие мастер-интерфейсы не смогут обратиться к памяти.
Allow Unalligned Transfers – Разрешает использовать не выровненные адреса. Всегда включаю этот параметр. До конца так и не понял, когда требуется отключать данный режим работы.
Enable Single AXI4 Data Interface – объединяет Мастер интерфейсы для обращения в адресное пространство (M_AXI_MM2S и M_AXI_S2MM), в один интерфейс. В целом это удобно, разгружает интерконнект, но я все равно этим не пользуюсь.
Тактирование интерфейсов
У модуля DMA, в данном режиме, есть три сигнала для тактирования интерфейсов:
s_axi_lite_aclk – используется для тактирования интерфейса S_AXI_LITE. Он предоставляет доступ к внутренним регистрам DMA.
m_axi_mm2s_aclk – используется для тактирования связки интерфейсов «память-поток» (M_AXI_MM2S и M_AXIS_MM2S).
m_axi_s2mm_aclk - используется для тактирования связки интерфейсов «поток-память» (M_AXI_S2MM и S_AXIS_S2MM).
Стоит отметить, что в даташине на модуль упоминается следующее: частота тактирующего сигнала s_axi_lite_aclk должна быть меньше или равна минимального значения между m_axi_mm2s_aclk и m_axi_s2mm_aclk.
Короткий пример: допустим есть частоты m_axi_mm2s_aclk==100МГц и m_axi_s2mm_aclk==125МГц, то s_axi_lite_aclk должен быть ≤ 100МГц.
Из практики скажу, что если уж началось разделение клоков в проекте, то таким управляющим шинам как AXI4_Lite зачастую подаю меньшую частоту, чтобы синтезатор не страдал при сборке. К тому же, общение через этот интерфейс происходит только в начале инициализации и запуска, а затем периодический опрос статуса DMA. В своих проектах часто использую вариант, когда все клоки одинаковые и запитаны от одного источника. Разумеется, речь про три клока DMA, а не весь проект.
Прерывания
DMA умеет генерировать два прерывания, для канала «память-поток» и «поток-память». События для этих каналов настраиваются в регистрах самого DMA.
Много рассказать про прерывания не смогу, сам использовал только прерывание по завершению передачи пакета.
Так как в этом режиме у DMA отсутствует циклический режим работы, то по завершению передачи транзакции, его необходимо перезапустить - и тут есть два пути решения. Либо в процессорной части бесконечно ждать освобождение канала (есть такой флаг), либо взвести прерывание.
В случае если идти вторым путем, то в обработчике прерывания важно сбрасывать флаг прерывания внутри DMA. Для очистки прерывания «завершение передачи» это 12 бит регистров MM2S_DMAS и S2MM_DMAS (зависит от канала), просто записываем число с единицей в 12 бите(0х0001000), чтобы очистить флаг.
Короткая памятка как настроить прерывания в MicroBlaze для DMA
Кратко пробегусь по тому как настроить прерывания. Пример ниже рабочий.
Подключаем в проект AXI Interrupt Controller как на скрине ниже:

Настройки оставляем базовыми, меня ни чего не нужно:

В коде для MicroBlaze пишем следующее:
void Init_Interrupt_system()
{
//включение прерывания внутри самого ДМА
//enable interrupt inside DMA
uint32_t tmp_reg_dma = Xil_In32(XPAR_AXI_DMA_0_BASEADDR+0x00);
tmp_reg_dma = tmp_reg_dma | 0x00001000;
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR+0x00, tmp_reg_dma);
int Status;
// Инициализация контроллера прерываний
Status = XIntc_Initialize(&InterruptController, XPAR_AXI_INTC_0_DEVICE_ID);
// Подключение обработчика прерывания к контроллеру. отдельно написаная функция ReLaunch_DmaMm2sIsr в которой обрабатывается прерывание
Status = XIntc_Connect(&InterruptController, XPAR_INTC_0_AXIDMA_0_VEC_ID,(XInterruptHandler)ReLaunch_DmaMm2sIsr, NULL);
// Запуск контроллера прерываний в реальном режиме
Status = XIntc_Start(&InterruptController, XIN_REAL_MODE); // XIN_REAL_MODE //XIN_INTC_NOCASCADE
// Включение прерывания для вашего устройства
XIntc_Enable(&InterruptController, XPAR_INTC_0_AXIDMA_0_VEC_ID);
// Инициализация системы исключений
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XIntc_InterruptHandler, &InterruptController);
Xil_ExceptionEnable();
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR+0x04, 0x00001000); //очистка бита прерывания в дма.
}
//функция обработчик которая была подключена в контроллер прерываний
void ReLaunch_DmaMm2sIsr(void *CallbackRef) {
//очистка бита прерывания внутри ДМА
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR+0x04, 0x00001000);//MM2S_DMASR
//дальше пишем польовательскую часть, например перезапуск ДМА для новой транзакции.
}
Особенности работы DMA
Хочется отметить некоторые особенности работы DMA, с которыми я столкнулся.
У входного AXI-Stream интерфейса имеется буфер в 4 слова. В него можно записать независимо от того нестроен DMA или нет. Поэтому нужно учитывать, что в буфере могут быть мусорные данные. А также это может создать иллюзорное мнение, что запуск DMA произошел раньше, чем прошла его инициализация.
DMA может принимать пакеты меньшей длины, чем поставлено в задаче. Длинна пакета определяется сигналом tlast и tkeep; второй сигнал можно игнорировать, если все данные у вас кратны размеру слова в шине. Все остальные варианты передачи пакетов, которые не соответствуют задаче DMA приведут к зависанию: если пакет пришел больше, чем ожидалось, если DMA стал обращаться в несуществующую область памяти. Поэтому тщательно проверяйте что, куда и сколько пишется. В случае, когда у вас принимается пакет меньше, чем ожидается, важно узнать сколько было принято данных. Для этого есть регистр S2MM_LENGTH. Просто читаем значение данного регистра после завершения передачи, получаем число принятых байт.
Обязательно отключаем работу кэш. Я не разобрался какой именно кэш нужно отключить (инструкций или данных), но я всегда отключаю оба. В противном случае команды до DMA не доходят.
Каналы DMA работают независимо.
Запуск передачи данных
Итак, мы подошли к самой лучшей части статьи – это запуск DMA. Понятно, что все параметры можно и самому прочитать, но как его запустить это уже ключевой вопрос в работе с DMA.
Настройка и запуск происходят через интерфейс S_AXI_LITE. К этому интерфейсу можно достучаться разными путями, например самостоятельно реализовать Мастер интерфейс AXI4-Lite или воспользоваться готовым мастером в лице MicroBlaze (также в этой роли подойдет и ZYNQ). В этой статье я покажу два варианта.
Вариант 1. Запуск с использование процессора
В моем случае я буду использовать MicroBlaze для инициализации DMA. Для этого открываем Vivado и создаем проект, далее создаем блок дизайн и накидываем типовой проект для данного теста.

Ключевые узлы:
Сам MicroBlaze, для управления логикой.
К MicroBlaze подключен AXI Interconnect, который создает адресное пространство для всех модулей и связывает их.
Подключаем BRAM память через BRAM Controller. В данной ситуации подключаю с одним портом. Эта память будет отражать область памяти в адресном пространстве, с которой будет работать DMA. Вместо этой памяти мог стоять MIG (DDR).
DMA – гвоздь программы. Его два Мастер интерфейса M_AXI_MM2S и M_AXI_S2MM подключены к общему AXI Interconnect, что немаловажно. Это позволит достучаться до BRAM памяти.
AXI Stream Data FiFО – это FIFO через которое будут подключены потоковые интерфейсы DMA сами на себя. Нужен для гарантии, что отправляемый пакет сможет выйти из DMA. Глубина FIFO небольшая, так как для тестов пакеты будут небольшие.
И последний модуль AXI UART Lite – нужен для вывода/ввода текстовой информации.
Блоки в проекте специально разбросал таким образом, чтобы можно было проследить линии подключения.
После сборки проекта, делаем Export Hardware и запускаем SDK. На этом этапе стоит отметить, что я пользуюсь Vivado 2019.1, и Vitis отсутствует. Но в любом случае различия большого не будет, суть одна: переходим программировать MicroBlaze.
Благодаря тому что был добавлен в проект UART, можно автоматически сгенерировать проект HelloWorld, и сразу проверить что собранный проект работает. А мы идем дальше…
Есть два способа инициализации DMA — это использовать готовую библиотеку либо самостоятельно изучать регистры, у обоих способов есть преимущества. В первом случае готовые, читабельные и отлаженные функции. Во втором случае уменьшение потребления памяти, а там есть что уменьшать! Реализуем две функции которые делают одно и то же, но разными способами.
Способ 1. Через готовую библиотеку. В топовом файле проекта (в моем случае helloworld.c) подключаем библиотеки "xaxidma.h" и "xparameters.h", первая нужна для получения библиотечных функций, а вторая для глобальных параметров проекта. Не всегда в проекте этот файл виден, поэтому я вручную его подключаю. Далее реализуем следующий код, каждый кусок кода я реализую отдельно в функцию и затем вызываю в main().
Функция launch_dma_includes()
void launch_dma_includes()
{
int stat;
// mem_tx и mem_rx – указатели на области памяти, с которой будет работать DMA. Вместо указателей можно создать массивы, но в моем случае мы указываем в каком месте расположена область памяти.
u32 *mem_tx = XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR; //8000 items
u32 *mem_rx = XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR + 1024;
//типовые переменные для работы с DMA
XAxiDma_Config *dma_cfg;
XAxiDma dma_ctrl;
//Инициализация DMA. Сначала создается инициализирующая переменная на основе ID DMA, а затем этой переменной инициализируется сам DMA. Затем в dma_ctrl будет храниться отражение значений регистров DMA.
dma_cfg = XAxiDma_LookupConfig(XPAR_AXI_DMA_0_DEVICE_ID);
stat = XAxiDma_CfgInitialize(&dma_ctrl, dma_cfg);
// Сброс DMA. Зачастую я этого не делаю. После сброса ждем, когда DMA выйдет из состояния сброса.
XAxiDma_Reset(&dma_ctrl);
while(XAxiDma_ResetIsDone(&dma_ctrl) == 0);
//MM2S – Запуск передачи пакета из памяти в поток.
stat = XAxiDma_SimpleTransfer(&dma_ctrl, mem_tx, 32*4, XAXIDMA_DMA_TO_DEVICE); //запуск
//S2MM – Запуск приема данных из потока в память.
stat = XAxiDma_SimpleTransfer(&dma_ctrl, mem_rx, 32*4, XAXIDMA_DEVICE_TO_DMA); //запуск
while(XAxiDma_Busy(&dma_ctrl, XAXIDMA_DMA_TO_DEVICE) != 0);//ждем когда передача завершится «память-поток»
while(XAxiDma_Busy(&dma_ctrl, XAXIDMA_DEVICE_TO_DMA) != 0);//ждем когда передача завершится «поток-память»
}
Отдельно рассмотрим функцию XAxiDma_SimpleTransfer(). Это ключевая функция для запуска передачи данных через DMA.
Первым аргументом передается указатель на переменную, которая отражает целевой DMA, в нашем случае DMA один и переменная одна dma_ctrl. Поэтому передаем ссылку на нее.
Вторым аргументом передаем область памяти, с которой будет работать DMA. Передается указатель; так как mem_tx и mem_rx уже являются указателями, то пишем, как есть.
Третьим параметром передаем размер пакета, который ожидается передать через DMA, размер всегда указывается в байтах. Я привык всегда указывать размер каким-то выражением, зачастую привязанным к типу переменной в памяти. Например, я ожидаю передать 32 слова в Stream интерфейс, в каждом слове по 4 байта. Размер слова определяется в настройках DMA еще в Vivado. Поэтому в качестве длины я указываю 32*4, хотя можно сразу было указать 128. Сами решайте, как проще.
Четвертым параметром передаем направление передачи. Это важный параметр, а важен он тем, что часто его значение путают, и в итоге все зависает и передача не происходит. Нужно внимательно относиться к этому параметру: XAXIDMA_DMA_TO_DEVICE – передача из памяти в поток(READ CHANNEL), XAXIDMA_DEVICE_TO_DMA – передача из потока в память(WRITE CHANNEL).
Для повторного запуска DMA нужно дождаться освобождение каналов от задачи.
Способ 2. Через прямой доступ к регистрам. В топовом файле проекта (в моем случае helloworld.c) подключаем библиотеки "xil_io.h" и "xparameters.h", первая нужна для использования функции прямого доступа к адресу, вторая - для получения глобальных параметров проекта.
Функция launch_dma_registers()
void launch_dma_registers()
{
//Инициализация DMA. Сброс каналов.
// Для MM2S канала
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x00, 0x00000004); // MM2S_DMACR.Reset = 1
while ((Xil_In32(XPAR_AXI_DMA_0_BASEADDR + 0x04) & 0x00000001) == 0); // Ждем завершения сброса
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x00, 0x00000000);//очищаем регистр сброса
// Для S2MM канала
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x30, 0x00000004); // S2MM_DMACR.Reset = 1
while ((Xil_In32(XPAR_AXI_DMA_0_BASEADDR + 0x34) & 0x00000001) == 0); // Ждем
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x30, 0x00000000);
//Старт для MM2S
// 1. Запустить канал
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x00, 0x00000001); // MM2S_DMACR.RS = 1 (Run)
while (Xil_In32(XPAR_AXI_DMA_0_BASEADDR + 0x04) & 0x00000001); // Ждем
// 2. Установить адрес источника (32-битный адрес)
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x18, mem_tx); // MM2S_SA = адрес памяти
// 3. Запустить передачу (указать длину в байтах)
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x28, 32*4); // MM2S_LENGTH = длина
//Старт для S2MM
// 1. Запустить канал
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x30, 0x00000001); // S2MM_DMACR.RS = 1 (Run)
while (Xil_In32(XPAR_AXI_DMA_0_BASEADDR + 0x34) & 0x00000001); // Ждем
// 2. Установить адрес назначения (32-битный адрес)
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x48, mem_rx); // S2MM_DA = адрес памяти
// 3. Запустить прием (указать размер буфера в байтах)
Xil_Out32(XPAR_AXI_DMA_0_BASEADDR + 0x58, 32*4); // S2MM_LENGTH = размер буфера
//Ждем когда каналы освободятся
while (!(Xil_In32(XPAR_AXI_DMA_0_BASEADDR + 0x04) & 0x00000002)); // Ждем пока Idle != 1
while (!(Xil_In32(XPAR_AXI_DMA_0_BASEADDR + 0x34) & 0x00000002)); // Ждем пока Idle != 1
}
Данная функция делает то же самое, что и launch_dma_includes(): запускает передачу 128 байт из памяти, и запускает прием 128 байт для записи в память. В конце просто ждет, когда завершится передача.
Запуск передачи происходит при записи размера пакета в DMA в соответствующем канале, поэтому при циклической работе DMA, в обработчике прерываний достаточно просто перезаписать один регистр предыдущим значением.
Функции Xil_Out32() и Xil_In32() выполняют чтение и запись указанного адреса.
Сравнение этих способов: Оба варианта дают одинаковый результат, но в итоге имеют сопутствующие плюсы и минусы. При использовании готовой библиотеки вам НЕ придется реализовать свой набор функций для запуска DMA. Но зато переменные dma_cfg и dma_ctrl потребляют не мало памяти: dma_cfg 68 байт, dma_ctrl 1872 байт. В случае, когда память ограничена, это может быть критично.
При использовании прямого доступа к регистрам, увеличивается скорость инициализации DMA. Например, для выполнения этих задач функция launch_dma_includes() запросила 6805 тактов системного клока, а launch_dma_registers() справилась за 761 такт. Разумеется, в это время входит еще и передача данных, но при таких показателях за 32 слова данных цепляться не стоит.
Скриншоты предачи данных
Сигнал gpio_io_o поднимается на время выполнения функции, по нему стоит триггер


Реализация своего Master AXI Lite
Разумеется, я не остановился на том, чтобы запустить DMA из MicroBlaze. С ним тоже нужно уметь работать и пользоваться. Поэтому хочется напрямую запустить DMA и больше ничего. Как и в предыдущей части открываем Vivado и создаем проект, также создаем блок дизайн и накидываем типовой проект для данного теста. На рисунке ниже видна блок схема:

Ключевые узлы, как и в прошлом этапе аналогичные за исключеним того, что пропал AXI UART из-за ненадобности, и пропал MicroBlaze так как он не нужен. Вместо него я подключил самописный модуль launch_dma. Модуль написан на Verilog.
Особенность подключения: подключить launch_dma напрямую к DMA можно, но не даст никакого результата. Проблема возникает в базовом адресе. Я не смог понять какой базовый адрес у DMA при таком подключении. Вариант 0х0000000 не сработал. С другой стороны, если в системе будет AXI Interconnect, а он будет, так как у вас будет область памяти и сам DMA выдает 2 AXI интерфейса и их нужно куда-то подключить, поэтому соединить все через AXI Interconnect будет адекватно.
Для работы определяем базовый адрес во вкладке Address Editor, в моем случае это 0x41e10000, прописываем его в самописном модуле и запускаем.
Код самописного модуля:
module launch_dma #
(
// Users to add parameters here
// Parameters of Axi Master Bus Interface M00_AXI
parameter integer C_M00_AXI_ADDR_WIDTH = 32,
parameter integer C_M00_AXI_DATA_WIDTH = 32,
parameter BASE_ADDR_DMA = 32'h0000_0000
)
(
// Users to add ports here
input start_launch_dma,
// User ports ends
// Do not modify the ports beyond this line
// Ports of Axi Master Bus Interface M00_AXI
//input wire m00_axi_init_axi_txn,
//output wire m00_axi_error,
//output wire m00_axi_txn_done,
input wire m00_axi_aclk,
input wire m00_axi_aresetn,
output reg [C_M00_AXI_ADDR_WIDTH-1 : 0] m00_axi_awaddr = 0,
output wire [2 : 0] m00_axi_awprot,
output reg m00_axi_awvalid = 0,
input wire m00_axi_awready,
output reg [C_M00_AXI_DATA_WIDTH-1 : 0] m00_axi_wdata = 0,
output wire [C_M00_AXI_DATA_WIDTH/8-1 : 0] m00_axi_wstrb,
output reg m00_axi_wvalid = 0,
input wire m00_axi_wready,
input wire [1 : 0] m00_axi_bresp,
input wire m00_axi_bvalid,
output reg m00_axi_bready = 0,
output reg [C_M00_AXI_ADDR_WIDTH-1 : 0] m00_axi_araddr = 0,
output wire [2 : 0] m00_axi_arprot,
output reg m00_axi_arvalid = 0,
input wire m00_axi_arready,
input wire [C_M00_AXI_DATA_WIDTH-1 : 0] m00_axi_rdata,
input wire [1 : 0] m00_axi_rresp,
input wire m00_axi_rvalid,
output reg m00_axi_rready = 0
);
localparam [3:0] CMD_wr = 0; //write reg
localparam [3:0] CMD_nop = 1; //pass cmd
localparam [3:0] CMD_wait_1_0 = 2; //read reg, get first bit, wait reset done, while(bit == 0)
localparam [3:0] CMD_wait_1_1 = 3; //read reg, get first bit, wait reset done, while(bit == 1)
localparam [3:0] CMD_wait_2_0 = 4; //read reg, get second bit, wait dma_idle == 1, while(bit == 0)
localparam STATE_wait = 0;
localparam STATE_idle = 1;
localparam STATE_write_wraddr = 2;
localparam STATE_write_val = 3;
localparam STATE_bresp = 4;
localparam STATE_read_wraddr = 5;
localparam STATE_read_value = 6;
localparam SIZE_PROGRAMM_for_DMA = 17;
reg [67:0] run_list_cmd[SIZE_PROGRAMM_for_DMA-1:0]; //{CMD, ADDR, Value}
(* MARK_DEBUG="true" *)
reg [3:0] state_dma;
initial
begin
//reset MM2S
run_list_cmd[0] = {CMD_wr, 32'h00, 32'h00000004};//write reset bit MM2S
run_list_cmd[1] = {CMD_wait_1_0, 32'h04, 32'h00000000}; //wait set reset MM2s
run_list_cmd[2] = {CMD_wr, 32'h00, 32'h00000000};//clear reset
//reset S2MM
run_list_cmd[3] = {CMD_wr, 32'h30, 32'h00000004}; //write reset bit S2MM
run_list_cmd[4] = {CMD_wait_1_0, 32'h34, 32'h00000000}; ////wait set reset S2MM
run_list_cmd[5] = {CMD_wr, 32'h30, 32'h00000000}; // //clear reset
//launch MM2S transmit
run_list_cmd[6] = {CMD_wr, 32'h00, 32'h00000001}; // S2MM_DMACR.RS = 1 (Run)
run_list_cmd[7] = {CMD_wait_1_1, 32'h04, 32'h00000000}; //wait when start
run_list_cmd[8] = {CMD_wr, 32'h18, 32'hC0000000}; //write Base ADDR in MM. !!!
run_list_cmd[9] = {CMD_wr, 32'h28, 32'h00000080}; //write length pkg, 128 byte //START transmit
// run_list_cmd[10] = {CMD_wait_2_0, 32'h04, 32'h00000000}; //128-byte buffer.//wait when writing to fifo buffer //There must be a buffer after DMA, otherwise it will freeze.
run_list_cmd[10] = {CMD_nop, 32'h04, 32'h00000000}; //128-byte buffer. //There must be a buffer after DMA, otherwise it will freeze.
//launch S2MM transmit
run_list_cmd[11] = {CMD_wr, 32'h30, 32'h00000001}; // S2MM_DMACR.RS = 1 (Run)
run_list_cmd[12] = {CMD_wait_1_1, 32'h34, 32'h00000000}; //wait when start
run_list_cmd[13] = {CMD_wr, 32'h48, 32'hC0000000}; //write Base ADDR in MM. !!!
run_list_cmd[14] = {CMD_wr, 32'h58, 32'h00000080}; //write length pkg, 128 byte //START transmit
// run_list_cmd[15] = {CMD_wait_2_0, 32'h34, 32'h00000000}; //wait when reading from fifo buffer
run_list_cmd[15] = {CMD_wait_2_0, 32'h04, 32'h00000000}; //wait when writing to fifo buffer
run_list_cmd[16] = {CMD_wait_2_0, 32'h34, 32'h00000000}; //wait when reading from fifo buffer
end
(* MARK_DEBUG="true" *)
reg start_launch_dma_s = 0;
(* MARK_DEBUG="true" *)
integer pointer_cmd = 0;
(* MARK_DEBUG="true" *)
reg [31:0] addr, value, wait_value_for_edge, mask_reg;
always@(posedge m00_axi_aclk)
begin
if(m00_axi_aresetn == 0) begin
state_dma <= STATE_wait;
start_launch_dma_s <= 0;
addr <= 'd0;
value <= 'd0;
m00_axi_awvalid <= 0;
m00_axi_wvalid <= 0;
m00_axi_bready <= 0;
m00_axi_arvalid <= 0;
m00_axi_rready <= 0;
end else begin
start_launch_dma_s <= start_launch_dma;
case(state_dma)
STATE_wait : begin //wait command start
if({start_launch_dma_s,start_launch_dma} == 2'b01) begin
pointer_cmd <= 'd0;
state_dma <= STATE_idle;
end
end
STATE_idle : begin
if(pointer_cmd < SIZE_PROGRAMM_for_DMA) begin
if(run_list_cmd[pointer_cmd][67:64] == CMD_wr) begin
state_dma <= STATE_write_wraddr;
end if(run_list_cmd[pointer_cmd][67:64] == CMD_wait_1_0) begin
wait_value_for_edge <= 32'd1;
mask_reg <= 32'd1;
state_dma <= STATE_read_wraddr;
end if(run_list_cmd[pointer_cmd][67:64] == CMD_wait_1_1) begin
wait_value_for_edge <= 32'd0;
mask_reg <= 32'd1;
state_dma <= STATE_read_wraddr;
end if(run_list_cmd[pointer_cmd][67:64] == CMD_wait_2_0) begin
wait_value_for_edge <= 32'd2;
mask_reg <= 32'd2;
state_dma <= STATE_read_wraddr;
end else if(run_list_cmd[pointer_cmd][67:64] == CMD_nop) begin
pointer_cmd <= pointer_cmd+1;
end
addr <= run_list_cmd[pointer_cmd][63:32] + BASE_ADDR_DMA;
value <= run_list_cmd[pointer_cmd][31:0];
end else begin
state_dma <= STATE_wait;
end
end
STATE_write_wraddr: begin//send addr
if(m00_axi_awvalid & m00_axi_awready) begin
m00_axi_awvalid <= 1'b0;
state_dma <= STATE_write_val;//goto send value
end else begin
m00_axi_awvalid <= 1'b1;
m00_axi_awaddr <= addr;
end
end
STATE_write_val : begin//send value
if(m00_axi_wvalid & m00_axi_wready) begin
m00_axi_wvalid <= 1'b0;
state_dma <= STATE_bresp;//goto send value
end else begin
m00_axi_wdata <= value;
m00_axi_wvalid <= 1'b1;
end
end
STATE_bresp : begin//get bresp
if(m00_axi_bvalid & m00_axi_bready) begin
m00_axi_bready <= 1'b0;
if(m00_axi_bresp == 2'b00) begin //write OK
state_dma <= STATE_idle;//goto idle
pointer_cmd <= pointer_cmd + 1;//next cmd
end else begin//error
state_dma <= STATE_wait;//goto wait
end
end else begin
m00_axi_bready <= 1'b1;
end
end
STATE_read_wraddr : begin
if(m00_axi_arvalid & m00_axi_arready) begin
m00_axi_arvalid <= 1'b0;
state_dma <= STATE_read_value;//goto send value
end else begin
m00_axi_arvalid <= 1'b1;
m00_axi_araddr <= addr;
end
end
STATE_read_value : begin
if(m00_axi_rvalid & m00_axi_rready) begin
m00_axi_rready <= 1'b0;
if(m00_axi_rresp == 2'b00) begin //read ok
if( (m00_axi_rdata & mask_reg) == wait_value_for_edge) begin// while done, goto next cmd
state_dma <= STATE_idle;//goto idle
pointer_cmd <= pointer_cmd + 1;//next cmd
end else begin
state_dma <= STATE_read_wraddr;
end
end else begin
state_dma <= STATE_wait;//goto send value
end
end else begin
m00_axi_rready <= 1'b1;
end
end
endcase
end
end
assign m00_axi_awprot = 3'b000;
assign m00_axi_wstrb = 8'hFF;
assign m00_axi_arprot = 3'b000;
endmodule По сути работа модуля launch_dma сводится к работе с адресами аналогично функции launch_dma_registers(). Внутри реализован автомат по записи и чтению регистров и их проверки. В переменной run_list_cmd записаны команды работы и автомат по очереди выполняет команды. Разумеется, этот код можно оптимизировать и ускорить.
Самописный модуль позволил ускорить запуск DMA еще больше. Теперь время выполнения всей задачи занимает 329 тактов вместе с передачей 32 слов данных. Скриншот ниже:

Кроме скорости выполнения у самописного модуля есть явное преимущество - это экономия ресурсов.
Если MicroBlaze со своей памятью и минимальной конфигурацией потребляет: LUTs 913, Registers 676, MUX 108, BRAM >0 (зависит от настройки проекта)
То, самописный модуль(launch_dma) использует только: LUTs 196 и Registers 266.
На мой взгляд, вариант с самописным модулем сразу же становится главным фаворитом всей этой статьи. Но тут есть большое «НО», в этом случае теряются все прелести MicroBlaze и его отладки. Уже трудно будет отправлять сообщение в UART, делать сложные условия работы и проверки.
Лично я этим методом стал бы пользоваться только в самом крайнем случае, когда нужна оптимизация кода ради экономии ресурсов ПЛИС.
Ещё не все....
В целом DMA реализует преобразование двух представлений: стрим интерфейс и адресное пространство. И отлично с этим справляется. И как бы это не было очевидно сам DMA тоже потребляет ресурсы ПЛИС, и как раз таки для тех, кто использует MicroBlaze есть небольшое решение, как преобразовать поток данных в запись в память, разумеется, за счет ресурсов процессора.
В настройках MicroBlaze есть пункт Number of Stream Links – этот параметр позволяет включить дополнительный интерфейсы процессору, в данном случае AXI-Stream. И за счет них читать(писать) данные из(в) памяти.
Для этого есть функции putfsl() и getfsl() - которые позволяют читать и писать данные в Stream интерфейс. Запись и чтение происходит по 1 слову. Данные функции являются блокирующими.
Вот скриншот как это выглядит на интерфейсах AXI-Stream:

Вывод в Stream интерфейс (из микроблейза) выводил в цикле по 4 слова, отсюда прослеживается группировка валидных данных по словам. Слова данных выводятся с периодичностью в 13 тактов. Чтение данных из Stream интерфейса (из fifo_2) тоже имеют группировку по 4 слова. Время чтения составляет 11-14 тактов.
Заключение
В целом я показал, как можно запустить DMA для передачи данных, отметил некоторые особенности и нюансы. Надеюсь статья даст достаточно понимания в этой области. В будущем планирую разобрать запуск DMA с SG режимом, там уже поддерживаются несколько каналов, цикличность и многое другое.
Источники: сам даташит на DMA