5 сентября 2014 в 10:02

Поднимаем SOC: ARM + FPGA из песочницы tutorial



На днях ко мне в руки попала 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, заполняем директорию и название проекта:
Название проекта

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

Остальные настройки для нас не важны, жмём 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

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

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

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

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

DDR3 Memory Parameters

DDR3 Memory Timing

DDR3 Board Settings

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

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

Готово. Сохраняем (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%08x\n", 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%08x\n", 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%x\n", word, data );
            exit( -1 );
          }
        }  
      }
      break;

    default:
      printf( "Error! Unknown COP\n" );
      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
НТЦ Метротек
рейтинг 56,26
Компания прекратила активность на сайте
Похожие публикации

Комментарии (29)

  • –2
    Что-то кроме восьми LEDов ничего дебажного на плате нет. Почему ее тогда не сделали на шине PCIe? И что подразумевается под Evaluation в данном случае?
    • +2
      Ну там вообще много всего, судя по списку:
      1Gbit Ethernet
      USB 2.0 OTG
      CAN
      SPI
      I²C
      UART-USB Converter
      3.3V GPIOs (62)
      User LEDs (8+3)
      DIP-Switches (8)
      Navigationkey
      LVDS connector for e.g. CMOS-Sensor
      LCD TFT interface
      Embedded USB Blaster II
      μSD Card Slot
      Temperature Sensor

      PCIe на этом чипе вроде как не сделать. Собственно это плата «начального» уровня.
    • +4
      Что разработчики вкладывали в название платы, точно сказать не могу — тут я только пользователь :)
      Думаю, что это обозначает возможность оценить все плюсы/минусы работы с SoC такого формата.

      По поводу отладки — nerudo ниже указал список того, что есть на плате.
      Хочу добавить, что в таких SoC достаточно развитая система дебага и трасировки.
      Кроме обычно «защелкивания» внутренностей FPGA при помощи SignalTap, в SoC одна из фич — Cross-trigger,
      когда события/breakpoint'ы одного компонента могут использоваться для трассировки другого.

      Сам я с этим на практике пока не работал. Но думаю, что поработаю, когда буду «разгонять» интерфейс.
      Соответственно, в следующей статье упомяну.
  • +8
    Автор, у вас есть борода?)) Ловите плюс. На хабре очень нужны такие статьи. Спасибо.
    • +3
      Спасибо за тёплый прием!
      Борода периодически отрастает, я её периодически сбриваю ))
  • +2
    Огромное спасибо. Это наверно первая статья на русском с нормальными примерами. В «Компонентах и Технологиях» был сериал ни о чем в трех сериях на эту тему,
  • +2
    И еще 1 плюс.
    Кстати а борода тут при чем? Для тех кто в танке :).
    • +8
      • 0
        А как же Slackware…
        • 0
          Им женщины пользуются
          • 0
            то есть борода ниже пояса, а выше пояса её нет?
  • +3
    Спасибо большое :))
  • +2
    Очень круто. Спасибо! Пишите еще.
    А что означает -O3?
    • +3
      Флаг уровня оптимизации. Код собирается дольше, но более лучше.
      • +2
        > но более лучше

        Не всегда. Можно поймать странные глюки.
  • 0
    Возможно, повысить скорость обмена можно путем отключения вывода в консоль из memblock.o
    • +3
      Измерение скорости проводились с параметром COP, равным 0 (при записи) и 2 (при чтении). В этих режимах вывод в консоль не производится.
  • +3
    Хочу пояснить, что доступ к памяти через отображение /dev/mem выбран для простоты примера.
    В реальной жизни такая практика достаточно опасна и чревата ошибками.

    Правильнее использовать техники типа Userspace I/O:


    Спасибо nymitr за замечание.
  • +1
    349€ за такую платку, нет, спасибо. Моя ZYBO — умеет так-же, и стоит 189$
    • +2
      Так я же нисколько не против :)
      Просто так исторически сложилось, что мы «сидим» на Altera, поэтому мне в руки попала эта платка.

    • +3
      Для каких задач используете эту плату?
      Какая скорость на запись/чтение в FPGA у Xillinx?
      • 0
        Xilinx предлагает несколько разных шин соединения процессора и FPGA внутри Zynq'а. Для них будут существенно разные скорости. Подозреваю, что и с Alter'ой аналогичная ситуация.
        • +1
          Я тоже так думаю, просто интересно сравнение производительности чипов, если встал вопрос о деньгах. Тот Cyclone, который стоит в этой плате жирнее (больше логики и памяти) чем Z-7010, и не является лоукостом)

          От жирности чипа и спидгрейда (если сравнивать серию Cyclone с одинаковым числом ядер) производительность скорее всего не зависит: эти же числа можно получить при самом дешёвом чипе, который около 50-70$ стоит. На плате за 189$ тоже стоит лоукост чип.

          Поэтому я предлагаю сравнивать не борды, а чипы, т.к. не все используют готовые борды в своих задачах, кто-то и сам может развести плату и чип поставить) Плюс интересно кто что делает на FPGA)
        • +1
          Вряд ли, шина там физически одна. Те измерения, которые приведены — ни о чем, автор совершенно верно отметил, что для измерения пропускной способности надо использовать dma. А так вышло измерение связки процессор+программа, главным образом. Пропусная способность памяти и внутреннего интерконнекта значительно выше.
          • 0
            Не одна, а 2 (с половиной). Одна — высокоскоростная и подключена напрямую к памяти и умеет DMA, другая — для всякой небыстрой периферии. Да и сами шины бывают 3-х видов:
            * AXI, шириной 32 или 64 бит.
            * AXI-Lite (32бит)
            * AXI-Stream — самая интересная из всех, для ЦОС — самое то.

            Да и вообще, Xilinx в плане шины ничего особенного не выдумывали, а взяли то, что сделано ARM'ом для своих процов.

            • 0
              Да, согласен, погорячился. Я подразумевал стандартную AXI, AXI-lite несерьезно совсем. А вот на счет AXI-Stream у меня сомнения — разве между HPS и FPGA имеется поддержка такого интерфейса? В доках быстро не нашлось…
              • 0
                Грубо говоря — это надстройка над стандартной AXI, когда мастером выступает не CPU, а блок внутри FPGA, который записывает/читает память в обход процессора. Работает она только с первой шиной (AXI-HP, на рисунке эта шина справа).

                Хотя я вот поигрался с AXI-Stream FIFO, которая наоборот — является слейвом, и управляется процессором. Очень простая в освоении (пишешь туда ворды из процессора, а на шине их получаешь). Но уж очень не любит когда процессор начинает загружаться другими процессами. Так что таки надо переходить на DMA, не зря-ж его придумали.
  • 0
    У нас на этой ПЛИСине тек и не удалось подружить АРМы и сеть. Причём компилили альтеровские примеры, не работают. Звонили в «ЭФО», они отправили запрос в «Альтеру», т.е. ответили «ну да, типа, есть такая бага в некоторых микрухах». И всё. Так что будьте осторожны!
    • 0
      У них багов хватает. Часть багов фиксится переходом на новые версии EDS/Quartus (ага, при этом добавляются новые). А по хорошему — надо заводить аккаунт на техподдержке альтеры (увы, тут надо быть юрлицом), тогда на вопросы отвечают со скоростью пулемёта. Из приятных приколов:
      1) В 13.0/13.0 sp1 нельзя было использовать половину LVDS пар — при компиляции трассировщик ругался, что «низя юзать диффпару 1 поскольку оно конфликтует с дифф2», причем даже если там никакого дифф сигнала не было замаплено.
      2) 13.1 — есть грабли с F2H мостом — по нему низя подняться по скорости выше 150 мегабайт в секунду.
      3) 13.х EDS — тупо не дает нормально работать под baremetal — надо всё делать руками (^#@$@!!! сколько времени я угробил, пока родил условно-рабочий скрипт отладки).
      4) С версией 14.0 мне понравился тот фикус, что «64 bit only». Блин, у меня половина дров на системе времен царя гороха, а тут надо на 64битную винду переходить.
      5) Классная вешь есть ещё — плис спокойно вешает ARM обращениями к памяти. т.е. перезагрузить ARM отдельно от ПЛИС — плохая идея.
      Если кому интересна разработка на этой платформе в baremetal — велкам на семинары, в москве ебвшники оный будут в октябре-ноябре делать. Там будет интересно =)

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

Самое читаемое Разработка