SoC: поднимаем простой DMA на FPGA

от автора

День добрый! В прошлой статье я описывал, как «поднять» с нуля SoC от Altrera.
Мы остановились на том, что измерили пропускную способность между CPU и FPGA, когда копирование выполняется процессором.

В этом раз мы пойдем немного дальше и реализуем примитивный DMA в FPGA.
Кому интересно — добро пожаловать под кат.

Используемое железо

В прошлый раз мы использовали плату SoCrates от EBV.
В этот раз я буду использовать плату нашей собственной разработки — именно она представлена на фото.

Основное отличие — в нашей плате 2 интерфейса Gigabit Ethernet и они заведены не на CPU, а на FPGA.
Это позволяет выполнять очень гибкую обработку трафика. Плюс на разъемы выведено большое количество пинов.

Но данные отличия станут для нас принципиальными только в следующих статьях.
В одной мы будем реализовывать NIC в FPGA — для этого, естественно, задействуем гигабитные интерфейсы.
В другой напишем поддержку фреймбуфера для дисплея ILI9341, опять же, в FPGA — для этого будет нужна плата расширения.

А для выполнения действий, описанных ниже, подойдет любая плата с SoC Сyclone V

Исходный код

По ходу статьи я буду приводить только важные куски кода с пояснениями.
Весь исходный код можно посмотреть на гитхабе

Детализация

Подробности сборки ядра, получения bootloader и других действий, описанных в прошлой статье, я приводить не буду.

Замечание насчёт ядра — лучше использовать более свежее ядро версии 3.18 отсюда:

git://git.rocketboards.org/linux-socfpga.git  git checkout remotes/origin/socfpga-3.18

Думаем над реализацией

Выбор DMA-контроллера

Итак, наша цель — передать данные от FPGA до процессора и/или обратно с максимальной пропускной способностью и минимальной загрузкой CPU.
Вариант копирования процессором сразу отпадает, нужно использовать DMA. Но кто может выполнять роль DMA-контроллера?
Для нашего SoC есть два варианта — либо FPGA либо встроенный в HPS контроллер DMA-330.

Судя по обсуждениям в сети, DMA-330 не очень производителен, а соответствующий драйвер, возможно, даже не полностью работоспособен.
Возможно, когда-нибудь, мы попробуем «оживить» DMA-330, но сейчас наш выбор — FPGA

Выбор интерфейса

Чтобы выполнять функции DMA-контроллера FPGA должнен быть мастером. Это возможно реализовать на одном из двух интерфейсов:

  • FPGA-to-HPS (fpga2hps)
  • FPGA-to-HPS SDRAM (fpga2sdram)

Структурная схема компонентов HPS и интерфейсов между ними:

Архитектура HPS

Давайте посмотрим, какие преимущества и недостатки у каждого варианта.

fpga2hps позволяет мастерам в FPGA получать доступ почти ко всем слейвам в системе. То есть не только как к памяти, но и к разнообразной периферии. При использовании fpga2sdram доступ ограничен только RAM.

fpga2sdram позволяет получить большую пропускную способность.

При использовании fpga2hps обмен происходит через один интерфейс. Если в FPGA требуется наличие нескольких мастеров, то необходим арбитраж. Значит нужно либо писать свои модули, либо использовать генерированные при помощи Qsys, а они достаточно ресурсоемкие.
С другой стороны в fpga2sdram можно создать до 6 независимых портов, а все вопросы с арбитражем решит DDR-контроллер.

И fpga2hps и fpga2sdram перед использованием должны быть проинициализированы записью в соответствующие регистры. К сожалению, для fpga2sdram это необходимо сделать после прошивки FPGA, но в момент, когда никаких транзакций на интерфейсе не происходит. Фактически, при использовании Linux, это означает, что прошивать FPGA нужно в U-boot’е. Подробности можно прочитать тут.

При работе с fpga2hps мастер в FPGA должен использовать байтовый адрес, при работе с fpga2sdram — адрес слова.

Более подробную информацию можно найти в Cyclone V Device Handbook, Volume 3: Hard Processor System Technical Reference Manual.
Главы 8 HPS-FPGA Bridges и 11 SDRAM Controller Subsystem.

Для нашей задачи нет принципиальной разницы, что использовать. Давайте выберем fpga2sdram в надежде получить большую пропускную способность.

Выбор реализации DMA-контроллера

Мы определились с тем, что будем реализовывать DMA-контроллер в FPGA и с тем, через какой интерфейс он будет работать.
Но как мы будем делать сам контроллер? Можно использовать одну из открытых «корок», например вот эту, которая также доступна через Qsys.

Это неплохой DMA-контроллер, который имеет много полезных фич. Мы ещё вернемся к нему, когда будем реализовывать свой NIC.
Но сейчас для нашей задачи такой контроллер — это ненужная функциональность и излишняя сложность.
Для обучающей задачи гораздо лучше набросать пару счётчиков в FPGA, чтобы осознать, что суть DMA-контроллера очень проста.

Верхний уровень

Со стороны софта всё тоже достаточно просто — нам нужен драйвер, который будет выделять память, получать шинный адрес этой памяти, конфигурировать и запускать DMA-контроллер в FPGA, дожидаться завершения транзакции и получать данные.

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

Для простоты прошивки в FPGA будем передавать данные в направлении FPGA -> CPU.
Передача данных в обратном направлении почти полностью аналогична, за исключением одного нюанса, о котором будет сказано ниже.
С направлением CPU -> FPGA мы будем работать при реализации фреймбуфера для LCD.

Итак, план:

  • Прошивка для FPGA
  • Программа в userspace
  • Драйвер ядра

Реализация прошивки FPGA

Начнём с нашего любимого Qsys. Нам потребуются три IP-корки:

  • Processors and Peripherals -> Hard Processor Systems -> Arria V / Cyclone V Hard Processor System
  • Basic Functions -> Bridges and Adaptors -> Memory Mapped -> Avalon-MM Pipeline Bridge
  • Basic Functions -> Bridges and Adaptors -> Clock -> Clock Bridge

Для HPS оставляем всё почти так же, как в предыдущей статье.
На вкладке FPGA Interfaces нужно добавить FPGA-to-HPS SDRAM интерфейс.
Выбираем тип Avalon-MM Bidirectional, ширину — 128 бит.

Ещё нужно поставить галку напротив Enable FPGA-to-HPS Interrupts.
Это позволит нашему DMA-контроллеру «сообщить» CPU о завершении транзакции посредством прерывания.

Также ширину интерфейса HPS-to-FPGA нужно поставить в 64 бита. Это интерфейс, через который CPU будет конфигурировать DMA-контроллер.
Его ширина может быть любой, 64 бита ставим просто потом, что у меня была выбрана такая ширина, и исходный код, описываемый далее, настроен под это значение.

Вот что должно получиться:

FPGA Interfaces

Переходим к Avalon-MM Bridge.
Это корка будет выполнять роль конвейера. Нам нужно экспортировать HPS-to-FPGA из автогенерённого Qsys модуля наружу.
Но если мы просто это сделаем, то получим интерфейс AXI, который намного сложнее, чем Avalon-MM. И работать с которым нам совсем не хочется. После добавления этого модуля Qsys автоматически конвертирует AXI в Avalon. Это займет часть ресурсов, но работать будет намного удобнее.

Настроить модуль нужно так:

Avalon-MM Bridge

Переходим к последнему модулю. Он нужен, чтобы мы могли экспортировать клок от HPS наружу и синхронизировать DMA-контроллер по этому клоку. Его настройка примитивна — нужно просто указать количество клоков, равное 1.

После этого нужно соединить все наши модули (обратите внимание на имена в колонке Export):

Qsys Connections

Осталось сохранить и сгенерировать файлы.

Пришло время реализации нашего примитивного DMA-контроллера. Как мы будем его настраивать?
Для настройки мы будем использовать так называемые Контрольные и Статусные Регистры (Control and Status Register, CSR)
Это блоки фиксированного размера, которые доступны CPU на чтение/запись (контрольные) или только на чтение (статусные).

Доступ к этим регистрам будет осуществляться через HPS-to-FPGA.
Так как интерфейс имеет ширину 64 бита, то можно либо сделать регистры такой же ширины, либо добавить конвертер.
Делать регистры 64-битными сильно накладно. Ведь очень часто в целом регистре используется всего лишь несколько бит.
Лучше сделать регистры 16-битными, а если возникнет необходимость иметь слово большой разрядности использовать 2 или 4 смежных регистра.

Теоретически можно было использовать конвертер, сгенерированный Qsys, указав для IP-корки Avalon-MM Bridge ширину в 16 бит, но на практике этого сделать не удалось — Qsys сгенерировал нерабочий модуль. Ничего страшного, будем использовать свой 🙂

В качестве конвертера используется модуль avalon_width_adapter.sv, а сами регистры реализованы в модуле regfile_with_be.v

Логика работы модуля регистров чрезвычайно проста — в зависимости от адреса выставляем на шину прочитанных данных содержимое нужного регистра. Если также пришел сигнал записи, то сохраняем в регистр входные данные. Адрес задает номер регистра, а не номер байта. Способ деление на контрольные и статусные регистры задается параметром при сборке — либо старшим битом адреса (адресное пространство в этом случае делится поровну между контрольными и статусными регистрами), либо по указанному параметрами количеству регистров.

Переходим непосредственно к DMA-контроллеру. Для простоты он расположен в топовом модуле.

Всё, из чего будет состоять наш DMA-контроллер — это три счётчика и пара сигналов.

Напомню, что данные наш контроллер выдает на интерфейс Avalon-MM. Подробное описание можно посмотреть тут, но в общем это достаточно простой интерфейс.
Для того, чтобы записать данные, нужно выставить следующие сигналы:

  • sdram0_address — адрес (напомню, что для fpga2sdram это должен быть адрес слова).
  • sdram0_writedata — данные для записи.
  • sdram0_byteenable — сигнал, указывающий на то, какие байты из данных нужно записать. Для простоты ставим его равным 16′FFFF.
  • sdram0_burstcount — сигнал для управления burst’ом. Опять же для простоты ставим его равным 1.
  • sdram0_write — этот сигнал нужно установить в 1 для выполнения транзакции записи

Единственный нюанс, про который нужно помнить — это наличие сигнала sdram0_waitrequest. Если он равен 1, это означает, что слейв не может в данный момент обработать транзакцию и мастер должен оставить все свои сигналы неизменными. Именно то, как часто сигнал sdram0_waitrequest будет выставляться в 1 и определит в итоге пропускную способность нашего DMA.

Итак, опишем используемые счётчики. Первый — это счётчик адреса, addr_cnt. При начале DMA-транзакции он устанавливается в адрес, заданный CPU. После каждой успешной транзакции (когда sdram0_waitrequest не равен 1) этот счётчик увеличивается на 1.

Второй — счётчик для эмуляции данных. Можно записать в данные всё, что хочется. Главное условие — после завершения транзакции софт должен вычитать из памяти точно те же данные, что были записаны. Поэтому записывать простой счётчик не очень правильно — в данных будет много нулей и сложно будет проверить валидность записи. Идеально было бы писать псевдослучайную последовательность, но для простоты хватит счётчика и его инвертированного значения.

Третий счётчик — счётчик тактов, cycle_cnt, будет сбрасываться в 0 при начале DMA-транзакции и дальше увеличиваться на 1 в каждом такте.
Он нужен для того, чтобы мы могли узнать, сколько тактов заняла наша DMA-транзакция, и рассчитать пропускную способность.

Итого, для счётчиков мы получаем следующий код:

Описание счётчиков

// For emulate data logic [63:0] data_cnt;  // Current address on SDRAM iface logic [31:0] addr_cnt;  // Overall cycles count.  logic [31:0] cycle_cnt;  // Form pseudo-data  always_ff @( posedge clk_w )   if( !test_is_running )     data_cnt <= '0;   else     if( !sdram0_waitrequest )       if( data_cnt != ( dma_data_size - 1 ) )         data_cnt <= data_cnt + 1;  // Increase address if no waitrequest always_ff @( posedge clk_w )   if( run_test_stb )     addr_cnt <= dma_addr;   else     if( !sdram0_waitrequest )       addr_cnt <= addr_cnt + 1;  always_ff @( posedge clk_w )   if( test_is_running_stb )     cycle_cnt <= '0;   else     if( test_is_running )       cycle_cnt <= cycle_cnt + 1;  

Вернёмся к сигналам. Нам потребуется только:

  • test_is_running — сигнал, указывающий, в процессе ли сейчас DMA-транзакция.
  • run_test_stb — сигнал-строб, активный в течении 1 такта в момент, когда CPU запускает DMA-контроллер
  • test_finished — сигнал, показывающий, что необходимое количество данных записано. Также заводится на прерывание.

Формирование этих сигналов тривиально.

Что нам нужно для настройки DMA-контроллера (это будут наши контрольные регистры)?

  • Адрес буфера, куда нужно скопировать данные
  • Размер данных для записи
  • Сигнал для запуска транзакции, из которого потом мы выделим фронт

Статусными регистрами будут:

  • Сигнал занятости DMA-контроллера
  • Значение счётчика cycle_cnt

Итого, вот так выглядит наше объявление регистров:

Объявление регистров

// Control registers `define DMA_CTRL_CR        0         `define DMA_CTRL_CR_RUN_STB      0  `define DMA_ADDR_CR0       1 `define DMA_ADDR_CR1       2  `define DMA_SIZE_CR0       3 `define DMA_SIZE_CR1       4  // Status registers `define DMA_STAT_SR        0         `define DMA_STAT_SR_BUSY         0  `define DMA_CYCLE_CNT_SR0  1 `define DMA_CYCLE_CNT_SR1  2 

А вот так выглядит назначение регистров:

Назначение регистров

// Control from CPU -- bit for start, DMA buffer address and transaction size. assign run_test       = cregs_w[`DMA_CTRL_CR][`DMA_CTRL_CR_RUN_STB]; assign dma_addr       = { cregs_w[`DMA_ADDR_CR1], cregs_w[`DMA_ADDR_CR0] }; assign dma_data_size  = { cregs_w[`DMA_SIZE_CR1], cregs_w[`DMA_SIZE_CR0] };  // Status for CPU -- current state and overall cycles count. assign sregs_w[`DMA_STAT_SR][`DMA_STAT_SR_BUSY] = test_is_running; assign { sregs_w[`DMA_CYCLE_CNT_SR1], sregs_w[`DMA_CYCLE_CNT_SR0] } = cycle_cnt; 

Всё, можно компилировать проект. Для начала выполним Analysis & Synthesis.

После этого создадим файл SignalTap — с его помощью мы сможем смотреть значения сигналов внутри FPGA
Для этого заходим File -> New -> SignalTap II Logic Analyzer File и жмём OK.
В появившемся окне нужно добавить необходимые сигналы. Должно получиться что-то вроде:

SignalTap File

Сохраняем файл, добавляем его в проект и выполняем полную сборку.

После окончания сборки нам нужно получить .rbf файл:

quartus_cpf -c etln.sof dma.rbf

Всё, прошивка готова. Переходим к софтовой части.

Внимание: помните, что после изменения настроек в Qsys (в частности после включения fpga2sdram) нужно перегенерировать и пересобрать Preloader.

Реализация userspace программы

Что нам нужно для того, чтобы работать с DMA-контроллером со стороны софта?

Во-первых, нам нужно уметь настраивать и запускать DMA-контроллер. Для этого мы используем программу mem из предыдущей статьи.

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

Тут нужно маленькое отступление. Обычно все процессы в userspace и даже большинство в ядре работают с так называемыми виртуальными адресами. А вот DMA-контроллеру нужно передать физический адрес (более точно, шинный адрес, но для используемых нами платформ он равен физическому)

В ядре для выполнения подобных задач есть набор специальных функций, которые позволяют по виртуальному адресу получить физический (и наоборот) или выделить область памяти и получить сразу два адреса, которые будут указывать на неё.

Что же делать в userspace? Нам поможет замечательный файл /proc/[PID]/pagemap, который содержит информацию о отображении всех виртуальных страниц в физические для любого процесса.

Информация для каждой страницы в этом файле занимает равно 8 байт. При этом младшие 55 бит содержат так называемый номер физической страницы — Page Frame Number (PFN), а старшие 9 бит — различные флаги (наличие страницы, нахождение в swap и т.д.) Подробное описание можно посмотреть тут или в man proc

Таким образом, зная виртуальный адрес и размер страницы, легко вычислить номер виртуальной страницы. После этого из файла /proc/[PID]/pagemap нужно просто считать 8 байт по нужному смещению и в младших 55 битах будет номер физической страницы. А его уже легко перевести в физический адрес, который мы и запишем в DMA-контроллер.

Если наша область памяти начинается на границе страницы, то всё становится ещё немного проще.
Поэтому вместо функции malloc() лучше использовать функцию posix_memalign(), которая позволяет задавать желаемое смещение.

Также, для того, чтобы предотвратить выгрузку данных из RAM в swap, желательно использовать функцию mlock()

Описанные выше вещи и выполняет программа phys_addr.c

Важное замечание — страницы, смежные в виртуальном адресном пространстве, не обязательно будут смежными в RAM.
Поэтому в данном способе мы не можем записывать DMA-контроллером данные, размером больше, чем размер страницы.
Обойти это ограничение мы сможем, когда напишем драйвер.

Промежуточная проверка

Итак, прошивка и тестовая программа готовы, время немного их потестировать.

Скопируем бинарники на SD-карту, подключим USB-Blaster и запустим нашу плату.

Выше я писал, что включать fpga2sdram интерфейс нужно до загрузки Linux. Это действительно так, но не всегда.
Если включить интерфейс уже в Linux и попытаться из FPGA прочитать данные в памяти, то система полностью зависнет.
А вот записать данные получится. Естественно, этот вариант явно не стоит использовать на боевой системе и ниже я напишу, как правильно инициализировать интерфейс fpga2sdram. Но для промежуточного тестирования нам это вполне подойдет.

Для начала прошьём FPGA:

cat dma.rbf > /dev/fpga0 

Теперь включим интерфейс HPS-to-FPGA:

echo 1 > /sys/class/fpga-bridge/hps2fpga/enable 

Если сейчас мы запустим SignalTap, то увидим, что сигнал sdram0_waitrequest постоянно висит в 1. Это связано с тем, что интерфейс fpga2sdram выключен.

Включим его:

./mem.o 0xFFC25080 0x3fff

Запись единиц в биты регистра 0xFFC25080 включает соответствующие порты интерфейса fpga2sdram. Описание, какие биты за какие порты отвечают, приведено в вышеуказанном Handbook’е. Нам для простоты достаточно включить все порты (всего в регистре используются 14 бит).

Теперь в SignalTap сигнал sdram0_waitrequest стал равен 0.

Запускаем утилиту phys_addr:

./phys_addr 

Она выделит буфер и выведет его физический адрес. У меня это 0x2d593000.
Мы помним, что при использовании интерфейса fpga2sdram нужно адресоваться по словам.
Так как слова у нас 128-битные, то адрес слова рассчитывается так:

0x2d593000 / 16 = 0x2d59300

Запишем этот адрес в регистры FPGA:

./mem.o 0xC0000002 0x2d59300 

Для адреса у нас используются контрольные регистры под номерами 1 и 2. Каждый адрес — это 16 бит, или 2 байта. Так как HPS-to-FPGA начинается с адреса 0xC0000000, то у первого контрольного регистра байтовый адрес будет равен 0xC0000002
Напомню, что утилита mem.c использует именно байтовые адреса.

После этого запишем длину DMA-транзакции в контрольный регистр номер 3. Длина не должны превышать размера страницы, а для нас это 4096 байт. Так как наш fpga2sdram интерфейс имеет ширину в 128 бит, а размер транзакции мы указываем в словах, то в третий регистр мы должны записать число 256:

./mem.o 0xC0000006 256 

Далее настроим SignalTap на захват по отрицательному фронту сигнала test_is_running и запустим DMA-контроллер.
Для этого нужно записать в нулевой бит нулевого регистра вначале 0 (если его там нет), а потом 1. При этому нужно помнить, что утилита mem.o выполняет транзакции по 4 байта, а это 2 наших регистра. Поэтому, если мы не будем осторожны, то затрём данные в соседнем регистре.

Итого, нам нужно вначале прочитать данные по адресу 0xC0000000, а потом записать их же, но с установленным нулевым битом.

Читаем:

./mem.o 0xC0000000 

У меня прочиталось 0x93000000

Записываем:

./mem.o 0xC0000000 0x93000001 

После этого мы должны получить в SignalTap примерно такую картинку:

SignalTap Result

Как видите, значение счётчика cycle_cnt на момент окончания транзакции равно 3167.
Давайте посчитаем пропускную способность. Частота тактового сигнала в моем проекте — 150 МГц.
Ширина — 128 бит. За 3167 тактов передано 256 слов. Итого:

128 * 150 / (3167/256) = 1551 Мбит/c 

Осталось убедиться, что данные записались правильно. «Снимаем» утилиту phys_addr с паузы, нажав Enter.
Мы должны увидеть такой текст:

Результат выполнения phys_addr

 0: 0x0 1: 0xffffffffffffffff 2: 0x1 3: 0xfffffffffffffffe ... 507: 0xffffffffffffff02 508: 0xfe 509: 0xffffffffffffff01 510: 0xff 511: 0xffffffffffffff00 

Если увидели, то всё прошло успешно.

Поэкспериментировав с разными параметрами, я увидел, что частота тактового сигнала почти не влияет на пропускную способность.
Она остается примерно одинаковой, что для 25 МГц, что для 150 МГц.
А вот ширина интерфейса fpga2sdram, напротив, даёт почти линейную зависимость — проверено при 64 и 128 битах. Для 256 не проверял.

Естественно, из-за того, что количество записываемых данных мало (всего 4096 байт), погрешность измерения довольно большая.
Увеличить размер DMA-транзакции мы сможем, написав свой примитивный драйвер.

Написание драйвера

Статья вышла чуть больше, чем я предполагал, поэтому про драйвер я расскажу совсем вкратце.
Тем более, что с ним нам ещё предстоит поработать в следующих статьях.
Но код лежит на гитхабе, кому интересно — можете посмотреть подробности.

Основная идея проста — при запуске драйвера мы задаем параметром, какой размер транзакции нам необходим.
Драйвер выделяет память и записывает шинный адрес и размер транзакции в FPGA.

Также драйвер регистрирует обработчик прерывания, которое мы задали в прошивке FPGA.

После этого драйвер создает два char-девайса:

  • /dev/etn-ctrl — для запуска DMA-транзакции
  • /dev/etn-data — для получения данных

При чтении из файла /dev/etn-ctrl, происходит запуск DMA-транзакции.
После этого вызов блокируется до прихода прерывания от FPGA.

Когда прерывание приходит, вызов завершается. Это означает, что данные записаны и их можно считать из файла /dev/etn-data.

Чтобы драйвер заработал в .dts файл нужно добавить следующие строки:

Изменения в .dts

 fpga {     compatible = "mtk,etn";     interrupts = <0x0 0x28 0x1>; }; 

Первая строка задает совместимый драйвер, а вторая — номер и тип прерывания от FPGA.

При использовании транзакции размером 4MB пропускная способность выходит порядка 2000 Мбит/с

Выводы

Был написал примитивный DMA-контроллер в FPGA и измерена его пропускная способность. Она составила около 2 Гбит/c.
Это не очень много, но следует учесть что, во-первых, не использовался интерфейс шириной 256 бит.
Во-вторых, не производилось никакой настройки арбитража в DDR-контроллере.
В-третьих, мы не использовали burst.
В-четвертых, возможно, я ещё что-нибудь забыл 🙂

Дальнейший план статей такой, если, конечно, они кому-нибудь интересны:

  • Реализация фреймбуфера для ILI9341 в FPGA
  • Работа с SGDMA-контроллером
  • Реализация гигабитного 2-х портового NIC в FPGA с использованием SGDMA-контроллера

Спасибо тем, кто добрался до конца! Удачи!

ссылка на оригинал статьи http://habrahabr.ru/post/248145/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *