- PVSM.RU - https://www.pvsm.ru -

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

Поднимаем 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, скачать его можно на официальном сайте [1]
Описывать установку не буду — там всё достаточно очевидно.

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

Запускаем 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 [2]. Отсюда мы будет брать компилятор 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 [3] или любым другим удобным для Вас методом.

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

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, чтобы разгрузить процессор.
Но это уже в следующей статье, если, конечно, кому-то это будет интересно.

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

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

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

Автор: Des333

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/linux/68990

Ссылки в тексте:

[1] на официальном сайте: https://www.altera.com/download

[2] Altera SoC EDS: http://www.altera.com/devices/processor/arm/cortex-a9/software/proc-soc-embedded-design-suite.html

[3] Yocto: http://www.rocketboards.org/foswiki/Documentation/GSRD131GettingStartedYocto#Building_U_45Boot_47Kernel_47Rootfs

[4] Официальная документация: http://www.altera.com/literature/lit-cyclone-v.jsp

[5] Rocketboards.org: http://www.rocketboards.org/foswiki/Documentation/WebHome

[6] Информация: https://www.rocketboards.org/foswiki/Documentation/EBVSoCratesEvaluationBoard

[7] Источник: http://habrahabr.ru/post/235707/