Поднимаем SOC: ARM + FPGA

в 6:02, , рубрики: Altera, embedded, fpga, linux, SoC, Железо, ПЛИС, метки: , , , , ,

Поднимаем SOC: ARM + FPGA

На днях ко мне в руки попала EBV SoCrates Evaluation Board. В двух словах — это плата с SoC от фирмы Altera, на борту которой есть двухъядерный ARM и FPGA Cyclone V.

ARM и FPGA на одном чипе — это должно быть очень интересно! Но для начала всё это добро нужно «поднять».
Об этом процессе я и поведаю в данной статье.

Если вам в руки попала такая или подобная плата и вы не до конца уверены, что же с ней нужно делать. Если вы всегда думали, что FPGA — это что-то сложное и непонятно, как к этому подступиться. Или вы просто любопытный инженер. Тогда заходите. Мы всем рады.

А в качестве маленького бонуса измерим пропускную способность между CPU и FPGA.

План работ

Наш план состоит из следующих пунктов:

  • Получение прошивки FPGA
  • Сборка ядра
  • Сборка U-Boot и Preloader
  • Сборка rootfs
  • Написание тестовых программ
  • Создание SD-карты
  • Запуск платы и измерение пропускной способности

Поехали!

Создание прошивки FPGA

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

Создание проекта

Запускаем Quartus, идём в File -> New Project Wizard, жмём Next, заполняем директорию и название проекта:

Название проекта

Поднимаем SOC: ARM + FPGA

Следующую страницу пропускаем, потом идёт выбор семейства и типа ПЛИС.

Выбор ПЛИС

Поднимаем SOC: ARM + FPGA

Остальные настройки для нас не важны, жмём Finish.

Проект Qsys

Qsys — отличный инструмент для начинающих. Позволяет получить прошивку, не написав ни строчки кода. Вместо этого разработчик собирает конструктор из заранее заданных кубиков (IP-корок). Требуется только правильно настроить каждую корку и соединить их должным образом.

Итак, Tools -> Qsys, в левом окне (IP Catalog) нам потребуются две IP-корки:

  • Processors and Peripherals -> Hard Processor Systems -> Arria V / Cyclone V Hard Processor System
  • Basic Functions -> On Chip Memory -> On Chip Memory (RAM or ROM)

Hard Processor System (HPS) — это наш ARM. С его настроек и начнем.

На первой вкладке нас интересует HPS-to-FPGA interface width, чтобы мы имели доступ из CPU ко внутренней памяти FPGA:

FPGA Interfaces

Поднимаем SOC: ARM + FPGA

Дальше идёт куча настроек для различных интерфейсов — в каких режимах работают, какие пины используются:

Peripheral Pins

Поднимаем SOC: ARM + FPGA

Следующая вкладка — настройка клоков. В Inputs Clocks оставляем всё без изменений:

Input Clocks

Поднимаем SOC: ARM + FPGA

В Output Clocks ставим галку на Enable HPS-to-FPGA user 0 clock:

Output clocks

Поднимаем SOC: ARM + FPGA

Потом идёт большой подраздел с различными настройками для DDR3 памяти.

DDR3 PHY Setting

Поднимаем SOC: ARM + FPGA

DDR3 Memory Parameters

Поднимаем SOC: ARM + FPGA

DDR3 Memory Timing

Поднимаем SOC: ARM + FPGA

DDR3 Board Settings

Поднимаем SOC: ARM + FPGA

С HPS мы разобрались, переходим к настройке On-Chip памяти. Это память, которая расположена непосредственно внутри ПЛИС.
Настроек тут значительно меньше:

On-Chip Memory

Поднимаем SOC: ARM + FPGA

Теперь нужно соединить блоки между собой. Всё достаточно интуитивно (обратите внимание на значение базового адреса напротив s1):

Qsys Connections

Поднимаем SOC: ARM + FPGA

Готово. Сохраняем (File -> Save) под именем soc.

Осталось сгенерировать файлы. Кнопка Generate HDL, в появившемся окне опять жмём Generate, ждём, Finish.

Компиляция проекта

Теперь нужно добавить сгенерённые файлы в проект:
Assignments -> Settings вкладка Files, добавляем файл soc/synthesis/soc.qip

Нужно применить настройки для DDR пинов. Но перед этим нужно выполнить первую стадию компиляции:
Processing -> Start -> Start Analysis & Synthesis

Запускаем скрипт для настройки пинов:
Tools -> Tcl Scripts. В появившемся окне выбираем Project -> soc -> synthesis -> submodules -> hps_sdram_p0_pin_assignments.tcl, Run.

Финальная компиляция проекта:
Processing -> Start Compilation

Мы получили файл soc.sof c прошивкой FPGA. Но мы хотим прошивать ПЛИС прямо из CPU, поэтому нам понадобится другой формат. Выполним конвертацию. Это можно делать и из GUI, но в консоле проще. Да и вообще, пора уже отвыкать от GUI :).

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

quartus_cpf -c soc.sof soc.rbf 

Ура! Мы получили прошивку FPGA.

Сборка ядра

Теперь соберём ядро для нашего ARM.
Из инструментов потребуется Altera SoC EDS. Отсюда мы будет брать компилятор arm-linux-gnueabihf- для кросс-компиляции.

Выкачиваем ядро:

git clone https://github.com/coliby/terasic_MTL.git 

Запускаем скрипт, который добавит в PATH директории с компилятором и запустит bash:

/opt/altera/quartus14.0/embedded/embedded_command_shell.sh 

Устанавливаем переменные окружения:

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
export LOADADDR=0x8000

Переходим в директорию с ядром и выполняем конфигурацию:

cd terasic_MTL/
make socfpga_defconfig

Cобираем образ ядра для U-Boot:

make -j 4 uImage

Теперь нам нужно получить так называемый .dtb (Device Tree Blob) файл. Это бинарный файл, содержащий информацию о платформе — интерфейсы, пины, тактовые сигналы, адресное пространство и т.д. Ядро читает этот файл во время инициализации и вносит в неё изменения. Это позволяет использовать одно собранное ядро на нескольких аппаратных платформах.
Итак, получаем .dtb файл:

make socfpga_cyclone5.dtb

Но этот файл не для нашей платформы, поэтому нам придётся внести в него небольшие изменения. Для этого конвертируем файл в текстовый формат .dts (Device Tree Source):

./scripts/dtc/dtc -I dtb -O dts -o soc.dts arch/arm/boot/dts/socfpga_cyclone5.dtb

Теперь в soc.dts нужно удалить блок bridge@0xff200000. Это можно сделать либо руками, либо наложив патч:

patch soc.dts dts.patch

dts.patch

942,966d941
<               bridge@0xff200000 {
<                       compatible = "altr,h2f_lw_bridge-1.0", "simple-bus";
<                       reg = <0xff200000 0x200000>;
<                       #address-cells = <0x1>;
<                       #size-cells = <0x1>;
<                       ranges = <0x200 0xff200200 0x80 0x100 0xff200100 0x80>;
< 
<                       tsc@0x200 {
<                               compatible = "terasic,mlt_touch_screen";
<                               reg = <0x200 0x80>;
<                               width_pixel = <0x320>;
<                               height_pixel = <0x1e0>;
<                               interrupts = <0x0 0x28 0x4>;
<                       };
< 
<                       vip2@0x100 {
<                               compatible = "ALTR,vip-frame-reader-13.0", "ALTR,vip-frame-reader-9.1";
<                               reg = <0x100 0x80>;
<                               max-width = <0x320>;
<                               max-height = <0x1e0>;
<                               mem-word-width = <0x100>;
<                               bits-per-color = <0x8>;
<                       };
<               };
< 

Теперь конвертируем файл обратно в .dtb:


./scripts/dtc/dtc -I dts -O dtb -o soc.dtb soc.dts

Итого, нас интересует два файла:

  • arch/arm/boot/uImage
  • soc.dtb

Сборка U-Boot и Preloader

Процесс запуска SoC выглядит следующим образом:

  1. Boot ROM
  2. Preloader
  3. Bootloader
  4. OS

Boot ROM — это первая стадия загрузки, которая выполняется сразу после поднятия питания. Её основная функция — определить и выполнить вторую стадию, Preloader.

Функциями Preloader чаще всего являются инициализация SDRAM интерфейса и конфигурация пинов HPS. Инициализация SDRAM позволяет выполнить загрузку следующей стадии из внешней памяти, так как её код может не поместиться в 60 КБ доступной встроенной памяти.

Bootloader может участвовать в дальнейшей инициализации HPS. Также эта стадия выполняет загрузку операционной системы либо пользовательского приложения. Обычно (и в нашем случае) в качестве Bootloader выступает U-Boot.

OS — тут всё просто. Это наш любимый Linux. Ядро для него у нас уже есть, корневую файловую систему получим чуть позже.
А в сейчас мы займемся Preloader и U-Boot

Открываем терминал, запускаем уже знакомый нам скрипт:

/opt/altera/quartus14.0/embedded/embedded_command_shell.sh 

Заходим в директорию с нашим проектом:

cd ~/src/soc_test/

После компиляции там должна появиться директория hps_isw_handoff, переходим в неё:

cd hps_isw_handoff

Запускаем генерацию необходимых файлов:

bsp-create-settings --type spl --bsp-dir build --preloader-settings-dir soc_hps_0 --settings build/settings.bsp --set spl.boot.WATCHDOG_ENABLE false

После этого дожна появиться директория build.
Собираем Preloader:

make -C build 

Собираем U-boot:

make -C build uboot

Теперь нам нужно настроить переменные для U-Boot. Вначале создаем текстовый файл u-boot-env.txt.

u-boot-env.txt

console=ttyS0
baudrate=115200
bootfile=uImage
bootdir=boot
bootcmd=run mmcboot
bootdelay=3
fdt_file=soc.dtb
fdt_addr_r=0xf00000
ethaddr=00:01:02:03:04:05
kernel_addr_r=0x10000000
mmcroot=/dev/mmcblk0p2
mmcpart=2
con_args=setenv bootargs ${bootargs} console=${console},${baudrate}
misc_args=setenv bootargs ${bootargs} uio_pdrv_genirq.of_id=generic-uio
mmc_args=setenv bootargs ${bootargs} root=${mmcroot} rw rootwait
mmcboot=mmc rescan; ext2load mmc 0:${mmcpart} ${kernel_addr_r} ${bootdir}/${bootfile}; ext2load mmc 0:${mmcpart} ${fdt_addr_r} ${bootdir}/${fdt_file}; run mmc_args con_args misc_args; bootm ${kernel_addr_r} - ${fdt_addr_r}
verify=n

Затем конвертируем его в бинарный формат, не забыв указать размер области, содержащей переменные — 4096 байт нам вполне хватит. Даже если реальный размер превысит заданный, mkenvimage сообщит об этом.

./build/uboot-socfpga/tools/mkenvimage -s 4096 -o u-boot-env.img u-boot-env.txt

Нас интересуют три файла:

  • build/uboot-socfpga/u-boot.img
  • u-boot-env.img
  • build/preloader-mkpimage.bin

Сборка rootfs

Это раздел написан для тех, кто использует Debian (или в если Вашем дистрибутиве тоже есть debootstrap). Если Вы не среди них — можете воспользоваться Yocto или любым другим удобным для Вас методом.

Устанавливаем необходимые пакеты:

sudo apt-get install debootstrap qemu-user-static binfmt-support

Создаем директорию и выкачивает туда необходимые файлы:

mkdir rootfs
sudo debootstrap --arch armel --foreign wheezy rootfs http://ftp.debian.org/debian

Чтобы запускать приложения, собранные под ARM-архитектуру, будем использовать qemu static. Для этого скопируем файл в нашу rootfs:

sudo cp /usr/bin/qemu-arm-static rootfs/usr/bin/

Переходим в нашу новую файловую систему:

sudo chroot rootfs /bin/bash

Если приглашение интерпретатора изменилось на «I have no name!@hostname:/#», значит всё прошло успешно.
Заканчиваем процесс установки:

/debootstrap/debootstrap --second-stage

В /etc/inittab оставляем следующие строки:

/etc/inittab

id:5:initdefault:

si::sysinit:/etc/init.d/rcS

~~:S:wait:/sbin/sulogin

l0:0:wait:/etc/init.d/rc 0
l1:1:wait:/etc/init.d/rc 1
l2:2:wait:/etc/init.d/rc 2
l3:3:wait:/etc/init.d/rc 3
l4:4:wait:/etc/init.d/rc 4
l5:5:wait:/etc/init.d/rc 5
l6:6:wait:/etc/init.d/rc 6

z6:6:respawn:/sbin/sulogin
S:2345:respawn:/sbin/getty 115200 console

Устанавливаем пароль:

passwd

Создаём архив:

tar -cpzf rootfs.tar.gz --exclude=rootfs.tar.gz  /

Написание тестовых программ

Если говорить в двух словах, то почти всё взаимодействие между компонентами SoC происходит при помощи отображения адресного пространства одного компонента в адресное пространство другого.
Рассмотрим на примере. В нашем проекте при помощи Qsys мы указали, что на интерфейсе HPS-to-FPGA начиная с адреса 0 расположен блок On-Chip памяти размером 262144 байт. Сам интерфейс HPS-to-FPGA отображается в адресное пространство CPU по адресу 0xC0000000 (см. документацию на Cyclone V). В итоге обращение CPU по адресам от (0xC0000000 + 0) до (0xC0000000 + 262143) будет приводить к обращению ко внутренней памяти FPGA.

Поэтому для работы нам потребуется утилита, с помощью которой можно читать/писать про произвольным адресам памяти. Вот её исходный код:

mem.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>
  
#define MAP_SIZE           (4096)
#define MAP_MASK           (MAP_SIZE-1)


int main( int argc, char *argv[] ) 
{
  int fd;

  if( argc < 2 ) {
    printf( "Usage:n" );
    printf( "%s byte_addr [write_data]n", argv[ 0 ] );
    exit( -1 );
  }

  // /dev/mem это файл символьного устройства, являющийся образом физической памяти.
  fd = open( "/dev/mem", O_RDWR | O_SYNC );
  if( fd < 0 ) {
    perror( "open" );
    exit( -1 ); 
  }

  void *map_page_addr, *map_byte_addr; 
  off_t byte_addr;
  
  byte_addr = strtoul( argv[ 1 ], NULL, 0 );

  // Выполняем отображение файла /dev/mem в адресное пространство нашего процесса. Получаем адрес страницы.
  map_page_addr = mmap( 0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, byte_addr & ~MAP_MASK );
  if( map_page_addr == MAP_FAILED ) {
    perror( "mmap" );
    exit( -1 ); 
  }

  // Вычисляем адрес требуемого слова (адрес при этом байтовый) 
  map_byte_addr = map_page_addr + (byte_addr & MAP_MASK);

  uint32_t data;

  // Если аргументов три, значит записываем данные, иначе -- читаем и выводим на экран.
  if( argc > 2 ) {
    data = strtoul( argv[ 2 ], NULL, 0 );
    *( ( uint32_t *) map_byte_addr ) = data;
  } else {
    data = *( ( uint32_t *) map_byte_addr );
    printf( "data = 0x%08xn", data );
  }

  // Убираем отображение.
  if( munmap( map_page_addr, MAP_SIZE ) ) {
    perror( "munmap" );
    exit( -1 ); 
  }

  close( fd );
  return 0;
}

Теперь нужно собрать её с использованием кросс-компилятора. Для этого запускаем скрипт:

/opt/altera/quartus14.0/embedded/embedded_command_shell.sh 

И выполняем компиляцию:

arm-linux-gnueabihf-gcc -o mem.o mem.c

Также нам нужна утилита для измерения пропускной способности:

memblock.c

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <unistd.h>
#include <errno.h>

// Валидные коды операций
#define COP_WRITE     (0)
#define COP_READ      (1)
#define COP_CHECK     (2)

int main( int argc, char *argv[ 0 ] ) 
{
  int fd;
  void *map_addr; 
  
  if( argc < 5 ) {
    printf( "Usage:n" );
    printf( "%s <cop> <address> <word_count> <cycles>n", argv[ 0 ] );
    exit( -1 );
  }

  // /dev/mem это файл символьного устройства, являющийся образом физической памяти.
  fd = open( "/dev/mem", O_RDWR | O_SYNC );
  if( fd < 0 ) {
    perror( "open" );
    exit( -1 ); 
  }

  uint8_t  cop;
  off_t    addr;
  uint32_t word_cnt;
  uint32_t cycle_cnt;

  // Код операции 
  cop       = strtoul( argv[ 1 ], NULL, 0 );
  // Начальный адрес
  addr      = strtoul( argv[ 2 ], NULL, 0 );
  // Количество слова для записи/чтения
  word_cnt  = strtoul( argv[ 3 ], NULL, 0 );
  // Количество циклов повторения
  cycle_cnt = strtoul( argv[ 4 ], NULL, 0 );

  // Выполняем отображение файла /dev/mem в адресное пространство нашего процесса. 
  map_addr = mmap( 0, word_cnt * 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, addr );
  if( map_addr == MAP_FAILED ) {
    perror( "map" );
    exit( -1 ); 
  }

  uint32_t cycle;
  uint32_t word;
  uint32_t data;

  // В зависимости от кода операции
  switch( cop ) {

    // Записываем в память "счётчик".
    case( COP_WRITE ):
      for( cycle = 0; cycle < cycle_cnt; cycle++ ) {
        for( word = 0; word < word_cnt; word++ ) {
          *( ( uint32_t *) map_addr + word ) = word;
        }
      }
      break;
   
    // Читаем данные и выводим на экран.
    case( COP_READ ):
      for( cycle = 0; cycle < cycle_cnt; cycle++ ) {
        for( word = 0; word < word_cnt; word++ ) {
          data = *( ( uint32_t *) map_addr + word );
          printf( "idx = 0x%x, data = 0x%08xn", word, data );
        }  
      }
      break;

    // Читаем данные и сравниваем с "гипотетически записанными".
    case( COP_CHECK ):
      for( cycle = 0; cycle < cycle_cnt; cycle++ ) {
        for( word = 0; word < word_cnt; word++ ) {
          data = *( ( uint32_t *) map_addr + word );
          if( data != word ) {
            printf( "Error! write = 0x%x, read = 0x%xn", word, data );
            exit( -1 );
          }
        }  
      }
      break;

    default:
      printf( "Error! Unknown COPn" );
      exit( -1 );
  }
     
  if( munmap( map_addr, word_cnt * 4 ) ) {
    perror( "munmap" );
    exit( -1 ); 
  }

  close( fd );
  return 0;
}    

Компилируем:

arm-linux-gnueabihf-gcc -o memblock.o memclock.c

Соответственно, интересующие нас файлы:

  • mem.o
  • memblock.o

Создание SD-карты

Настало время собрать всё воедино. На текущий момент у нас должны быть следующие файлы:

  • soc.rbf
  • uImage
  • soc.dtb
  • preloader-mkpimage.bin
  • u-boot.img
  • u-boot-env.img
  • rootfs.tar.gz
  • mem.o
  • memblock.o

Если какого-то из них нет — значит Вы что-то пропустили :)

Создадим директорию и скопируем все указанные файлы в неё. Далее нам нужно найти и подключить MicroSD карту.
В последующих командах предполагается, что карта определилась как устройство /dev/sdb. Мы создадим на ней два раздела:

  • /dev/sdb1 — для Preloader и U-Boot
  • /dev/sdb2 — для файловой системы

Если карта определилась под другим именем, внесите соответствующие изменения.

На всякий случай затираем всё нулями.
Внимание! Eще раз проверьте, что /dev/sdb — это карта, а не Ваш второй жёсткий диск.

sudo dd if=/dev/zero of=/dev/sdb bs=10M

Для того, чтобы создать разделы, воспользуемся утилитой fdisk:

sudo fdisk /dev/sdb

Далее нужно ввести следующие команды (пустая строка — ввод Enter):

Команды для fdisk

o
n
p
1
2048
+1M
n
p
2


t
1
a2
t
2
83
w

Можно проверить, что у нас получилось:

sudo fdisk -l /dev/sdb

Должно быть что-то похожее на:

Вывод fdisk -l

Disk /dev/sdb: 1966 MB, 1966080000 bytes
61 heads, 62 sectors/track, 1015 cylinders, total 3840000 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x02be07e5

   Device Boot      Start         End      Blocks   Id  System
/dev/sdb1            2048        4095        1024   a2  Unknown
/dev/sdb2            4096     3839999     1917952   83  Linux

Теперь скопируем на карту образ с переменными U-Boot:

sudo dd if=u-boot-env.img of=/dev/sdb bs=1 seek=512

После этого копируем Preloader:

sudo dd if=preloader-mkpimage.bin of=/dev/sdb1

И сам U-Boot:

sudo dd if=u-boot.img of=/dev/sdb1 bs=64k seek=4

Создаём файловую систему ext3:

sudo mkfs.ext3 /dev/sdb2

Монтируем её:

sudo mount /dev/sdb2 /mnt/

И разворачиваем в неё нашу rootfs:

sudo tar xvf rootfs.tar.gz -C /mnt/

Далее копируем образ ядра, dtb, прошивку FPGA и тестовые программы:

sudo cp uImage /mnt/boot/
sudo cp soc.dtb /mnt/boot/
sudo cp soc.rbf /mnt/boot/
sudo cp mem.o /mnt/root/
sudo cp memblock.o /mnt/root/

Отмонтируем файловую систему:

sudo umount /dev/sdb2

Всё, карта готова!

Запуск платы и измерение пропускной способности

Наконец-то всё готово для работы. Вставляем карту, подключаем USB и питание.
Заходим по консоли:

minicom -D /dev/ttyUSB0 -b 115200 -s

Первым делом прошьём FPGA.
Для это необходимо установить переключатель P18 на плате в положение «On On On On On» (выключатели с 1 по 5).
Смотрим текущее состояние FPGA:

cat /sys/class/fpga/fpga0/status

Мы должны увидеть configuration phase
Заливаем прошивку:

 dd if=/boot/soc.rbf of=/dev/fpga0 bs=4096 

И смотри состояние еще раз:

cat /sys/class/fpga/fpga0/status

Состояние должно смениться на user mode. Это означает, что ПЛИС сконфигурирована и готова к работе.

Теперь проверяем наши утилиты. Но перед этим ещё немного «работы напильником».
У нашего кросс-компилятора и у Debian разные названия динамического линкера. Поэтому для того, чтобы утилиты работали, нам необходимо создать ссылку на правильный линкер:

ln -s /lib/ld-linux.so.3 /lib/ld-linux-armhf.so.3

Итак, запускаем утилиту (пояснение, что это за адрес, будет чуть ниже):

./mem.o 0xFFD0501C

Если в результате Вы видите строку data = 0x00000007, значит всё в порядке.

Как я уже писал выше, внутренняя память ПЛИС у нас будет отображена в адресное пространство начиная с адреса 0xC0000000. Но перед тем, как мы сможем работать с этой памятью, нам нужно сделать еще два действия.

Первое — так как по умолчанию все интерфейсы между CPU и FPGA находятся в ресете, то мы должны его снять. За это отвечает блок Reset Manager (rstmgr), с базовым адресом 0xFFD05000, и конкретно его регистр brgmodrst со смещением 0x1C. Итоговый адрес регистра — 0xFFD0501C. В нём задействованы только три младших бита:

  • 0-й — сброс интерфейса HPS-to-FPGA
  • 1-й — сброс интерфейса LWHPS-to-FPGA
  • 2-й — сброс интерфейса FPGA-to-HPS

Логика работы всех битов одинакова — если там записана единица, значит соответствующий интерфейс находится в ресете. В итоге, значение по умолчанию для этого регистра — это 0x7, что мы и видели, когда читали из него при помощи нашей утилиты. Нам требуется снять ресет с интерфейса HPS-to-FPGA, значит мы должны записать в регистр число 0x6:

./mem.o 0xFFD0501C 0x6

После этого вновь прочитаем регистр, чтобы убедиться, что данные записались корректно:

./mem.o 0xFFD0501C

Второе — мы должны включить отображение интерфейса HPS-to-FPGA в адресное пространство CPU. За это отвечает блок L3 (NIC-301) GPV (l3regs) с базовым адресом 0xFF800000, и конкретно его регистр remap со смещением 0. За HPS-to-FPGA отвечает бит под номером 3. В итоге, нам нужно записать в регистр число 0x8:

./mem.o 0xFF800000 0x8

К сожалению, этот регистр доступен только для записи, поэтому прочитать для проверки данные у нас не получится.

Теперь мы можем читать и писать в память FPGA. Проверим это. Читаем:

./mem.o 0xC0000000

Естественно, там должны быть нули. Теперь запишем туда что-нибудь:

./mem.o 0xC0000000 0x12345678

И снова прочитаем:

./mem.o 0xC0000000

Должно совпасть с записанным.

Ура! Мы наконец-то сделали это! Мы получили работающую SoC с FPGA и организовали доступ к её памяти из CPU.
Но просто читать/писать — это как-то совсем скучно. Давайте хотя бы измерим пропускную способность нашего интерфейса. Тем более это займет совсем немного времени.

Для этого нам потребуется наша вторая утилита memblock:

root@desktop:~# ./memblock.o 
Usage:
./memblock.o <cop> <address> <word_count> <cycles>

Она работает следующим образом: если первый аргумент cop равен 0, то в word_count 32-битных слов, начиная с адреса address, будет записана последовательность чисел от 0 до word_count-1. Вся процедура будет произведена cycles раз (это сделано для более точного измерения пропускной способности).
Если cop равен 1, то эти же слова будут считаны и выведены на экран.
Если cop равен 2, то слова будут считаны, а их значения будут сравниваться с теми, что гипотетически были записаны.

Проверим. Запишем немного данных:

./memblock.o 0 0xC0000000 10 1

Теперь считаем их:

./memblock.o 1 0xC0000000 10 1

Результат должен быть следующим:

Вывод memblock.o

data = 0x00000000
data = 0x00000001
data = 0x00000002
data = 0x00000003
data = 0x00000004
data = 0x00000005
data = 0x00000006
data = 0x00000007
data = 0x00000008
data = 0x00000009

Теперь попробуем сравнить данные, специально задав чуть большее количество слов:

./memblock.o 2 0xC0000000 11 1 

Должны получить такую строку:

Error! write = 0xa, read = 0x0

Теперь запускаем запись по всему объему памяти в количестве 1000-ти повторений и замеряем время записи:

time ./memblock.o 0 0xC0000000 0x10000 1000

Среднее значение по 5 запускам равно 11.17 секунд. Считаем пропускную способность:

1000 раз * 65536 записей * 4 байта * 8 бит/в_байте / ( 11.17 * 10^6 ) = 187.75 Мбит/c

Не очень густо. А что у нас с чтением:

time ./memblock.o 2 0xC0000000 0x10000 1000

Среднее время 10.5 секунд. Что выливается в:

1000 * 65536 * 4 * 8 / ( 10.5 * 10^6 ) = 199.73 Мбит/c

Примерно то же самое. Естественно, на время выполнения любой из этих операций одно из двух ядер загружается на 100%.

Если при компиляции добавить флаг -O3, то пропускная способность на запись и на чтение станет 212 Мбит/c и 228 Мбит/c соответственно. Чуть лучше, но тоже не метеор.

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

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

Полезные ссылки

Официальная документация на Cyclone V
Rocketboards.org — много разных статей про платы с SoC
Информация конкретно по EBV SoCrates Evaluation Board

Автор: Des333

Источник

* - обязательные к заполнению поля