21 июля 2019 г.

Как помигать светодиодом на Zynq FPGA (ПЛИС) с помощью Linux используя отладочную плату Zybo вместе с sysfs или /dev/mem

В данной статье будет рассмотрено несколько способов мигания светодиодом, в принципе небольшое пособие по использованию портов ввода вывода из Linux.

Теория

Есть 3 основных варианта создания и настройки GPIO выводов из ARM процессора.
Информация по большей части взята из документации ug585-Zynq-7000-TRM.

в сердце Zybo находится XC7Z010-1CLG400C, это SoC (System on Chip), который состоит из ARM процессора и FPGA. Их можно рассматривать как 2 раздельных устройства, у каждого из них имеются свои подключения к ножкам микросхемы, к тому же между ними есть внутренние соединения для обмена данными. Более детально это можно увидеть на следующей схеме:

Рис.1 структурная схема интерфейсов

Помимо специализированных интерфейсов есть GPIO выводы которые могут работать на вход, выход либо быть отключены (Hi-Z состояние). К тому же на них могут быть настроены прерывания. Есть 2 типа таких выводов. Первые носят название MIO (multiplexed I/O), данные выводы соединены напрямую между ножками микросхемы и процессором. Также имеется набор EMIO (extended multiplexed I/O) выводов, они соединяют процессор (PS) и FPGA (PL). Помимо них можно также использовать AXI (Advanced eXtensible Interface) для передачи данных между PS и PL.

Далее будут подробно рассмотрены все 3 способа.

К соединениям MIO и EMIO подключены мультиплексоры которые выбирают режим их работы. Большинство выводов помимо возможности работать как простой GPIO могут быть сконфигурированы под определённые интерфейсы, UART, I2C, и т.д. Большинство изи них могут быть направлены как на выводы MIO так и EMIO. Но могут отличаться параметры интерфейсов в зависимости от выбранного вывода. К примеру Ethernet через MIO будет подключён по RGMII, для уменьшения числа ножек, а через EMIO по GMII.

Рис.2 Мультиплексирование выводов
Чем быстрее интерфейс тем через меньшее число мультиплексоров он соединён с выводом.

Рис.3 схематическое представление соединений
Стоит отметить что выводы EMIO можно подвести к любым доступным ножкам PL, выводы MIO жёстко прописаны к конкретным ножкам PS.

Рис.4 соединения между выводами
Несмотря на рисунок выводы MIO[7:8] доступны только для вывода.


У Zynq-7000 имеется 54 MIO вывода, каждый из которых может работать на вход выход либо быть отключён (Hi-Z). 192 EMIO сигнала соединяющих PS и PL, это можно расценивать как 64 вывода. 192 получается суммированием возможностей. 64 входа, 64 вывода и 64 сигнала включения. Ведь все эти сигналы должны быть переведены в PL часть для того чтобы их можно было соединить с любым PL выводом.

На форумах есть несколько тем с просьбами разъяснить к каким выводам относятся EMIO и как их воспринимать. Их удобнее всего воспринимать как 3 набора регистров по 64 бит каждый.

Данные сигналы разделены на банки, 2 банка для MIO и 2 банка для EMIO:

Рис.5 Соединение сигналов к соответствующим банкам
Банк 0 контролирует выводы MIO[31:0]
Банк 0 контролирует выводы MIO[53:32]
Банк 0 контролирует выводы EMIO[31: 0]
Банк 0 контролирует выводы EMIO[63:32]

Выводы можно настраивать как независимо друг от друга так и в пределах всего банка. Выходное напряжение для банков MIO должно быть одинаково внутри банка, но может отличаться между банками, для EMIO это не имеет значения, т.к. они выводятся через PL.

К каждой ножке может быть программно подключен Pull - UP резистор. На каждый вывод может быть независимо настроено прерывание на изменение (фронт, срез) либо уровень (высокий, низкий).

У каждого банка есть свой набор регистров отвечающих за конфигурацию выводов:

Рис.6 краткое описание конфигурационных регистров
Для работы с MIO и EMIO можно использовать как регистры указанные выше, производя считывание и запись напрямую в адресное пространство процессора, либо с помощью библиотек для работы с GPIO, либо применив sysfs, примеры можно посмотреть здесь. В дальнейшем будут рассмотрены все эти способы.

Насчёт AXI можно сказать, что это интерфейс передачи данных между PS и PL частью. Его можно в принципе использовать где угодно, просто это основное соединение между PS и PL у XILINX.

Для простоты восприятия, в нашем случае, использование AXI интерфейса можно описать следующим образом:
PS выступает как мастер и выдаёт наружу адрес ячейки к которой он хочет обратиться, за частую размер ячейки 32 бита. можно либо записывать, либо считывать значение из ячейки. В PL нужно каким-либо образом реализовать адресуемую память которая будет работать с AXI интерфейсом. Это задача FPGA части корректно обрабатывать принимаемые данные и отправлять обратно запрашиваемые данные.

Настройка

Прежде чем использовать порты, их нужно настроить, для этого в Vivado нужно открыть настройки IP ядра processing system, перейти в раздел MIO configuration, далее убедиться, что стоит галочка рядом с GPIO MIO. Банк 0 (MIO[31:0]) на плате Zybo подключен к 3.3 В, банк 1 (MIO[53:32]) к 1.8 В.

Можно заметить, что не все выводы отображены, это из-за того, что к ним уже подключена другая периферия и их нельзя использовать как GPIO. Для нас главное чтобы был доступен вывод MIO7, к нему подключен светодиод.

Рис.7 настройка MIO
Ниже находится настройка EMIO портов, включим их и выберем 1 вывод:

Рис.8 настройка EMIO
Настройки AXI находятся в разделе PS-PL Configuration, но в данный момент нам хватит настроек по умолчанию, главное чтобы AXI GP0 был включён:

Рис.9 настройка AXI
Тактовая частота для AXI поступает на вход IP ядра, зачастую туда подают сигнал снимаемый с вывода FCLK_CLK0. У меня он настроен на 50 МГц:

Рис.10 Настройка тактовой частоты AXI
Далее можно нажать ОК, в результате окно блок дизайна должно выглядеть примерно так:

Рис.11 готовое к подключению ядро процессора

Подключение


Далее нужно подключить всё необходимое. MIO подключён по умолчанию к соответствующему выводу FPGA. соединим EMIO. Если раскрыть вкладку GPIO_0, на ядре процессора, можно будет увидеть 3 вывода, вход, выход и включение вывода (out enable), для перевода в Hi-Z состояние. Нам нужно вывести наружу выход для управления светодиодом, можно нажать на соответствующий вывод правой кнопкой и выбрать "Make External", в таком случае схема будет иметь следующий вид:

Рис.12 подключённый EMIO вывод
Теперь следует добавить AXI, для этого по пустому месту блок дизайна нужно нажать правой кнопкой и выбрать "Add IP", в появившемся окне найти "AXI GPIO', нам нужен быть только 1 выход, настройки следующие:

Рис.13 настройки AXI GPIO
Далее нажимаем ОК, после чего вверху экрана появится предложение произвести автоматическое подключение необходимых компонентов. Соглашаемся, ставим везде галочки, жмём ОК. После всех манипуляций схема примет следующий вид:

Рис.14 блок дизайн вместе с AXI GPIO
Вывод автоматически будет выведен наружу. Также добавлено 2 дополнительный ядра для корректной работы AXI интерфейса. Теперь следует указать на какие выводы микросхемы соединить созданные нами выводы GPIO, можно вручную прописать constaints, но я буду использовать GUI подключение через synthesis.
Для этого сперва нужно обновить wrapper:

Рис.15 обновление wrapper
После чего синтезировать дизайн "Run synthesis" (F11). Когда закончиться перейти в синтезис "Open synthesized design". По умолчанию, снизу должна открыться вкладка I/O Ports, если её нет, в правом верхнем углу следует выбрать I/O Planning:

Рис.16 переход в корректный режим отображения вкладок
Подключим выводы к LED3 и LED0, схема доступна в открытом доступе. Напряжение банка 3.3 В, обозначения выводов D18 и M14 соответственно.

Рис.17 настройка выходных ножек
Последним осталось сохранить constraints и скомпилировать проект.

SYSFS

Проще всего использовать GPIO через драйвер sysfs, он по умолчанию включён и готов к работе. Примеры работы с ним можно найти здесь. Подробное описание работы здесь.

Сперва перейдём в папку /sys/class/gpio.
cd sys/class/gpio
Выведем её содержимое.
ls  
там должно быть файлы "export", "unexport" и папка "gpiochip906"

Рис.18 содержимое папки gpio
все драйверы в Linux предоставляют доступ к объектам посредством файлов. Для того чтобы работать с GPIO, используя этот интерфейс нужно записывать и считывать данные из файлов. Папка "gpiochip906" содержит файлы для работы с MIO[0], все остальные выводы расположены по возрастанию адреса. После того как закончится последний MIO вывод MIO[53], который в sysfs отображается как папка "gpio959" начинаются EMIO выводы.

Для того чтобы подключить необходимую ножку, в нашем случае MIO[7], необходимо выполнить команду:
echo 913 > export
906 + 7 = 913. Таким образом мы запишем в файл export значение 913 благодаря чему появится дополнительная папка "gpio913":

Рис.19 новая папка gpio913
перейдём в неё и рассмотрим её содержимое:
cd gpio913
ls
Рис.20 содержимое папки gpio913
нас интересуют файлы "direction" и "value". Первый выставляет направление вывода "in" или "out". Второй выходное значение "1" или "0". Настроим ножку на вывод и подадим туда единицу:

Рис.21 зажигание светодиода
Светодиод должен загореться. для того чтобы его потушить, нужно подать "0". Для проверки EMIO, нужно выполнить ту же последовательность команд. Мы работаем с EMIO[0]. Поэтому адрес вывода будет: 906 + 54 + 0 = 960 (54 вывода MIO).

С помощью данных команд можно написать свою программу или скрипт который будет обрабатывать GPIO выводы. Примеры можно посмотреть здесь и здесь.

Скрипт можно написать либо сразу в Linux используя какой-нибудь текстовый редактор, к примеру vim, который установлен по умолчанию. Либо переместив его с компьютера, как это сделать было описано здесь.

Я использовал VIM и создал файл blink_bash.sh в корневой директории, .sh - расширение для скриптового файла. После создания файла, необходимо разрешить его запуск, для этого используется команда:
chmod +x blink_bash.sh
Для запуска скрипта:
./blink_bash.sh
Код скрипта:
# blink MIO LED from Bash script

echo exporting pins
echo 913 > sys/class/gpio/export # MIO[7]
echo pins exported

echo configuring direction to out
echo out > sys/class/gpio/gpio913/direction
echo direction configured to out

echo blink LED 10 times
for i in $(seq 1 10);
do
   echo 1 > sys/class/gpio/gpio913/value
   sleep 1 # delay 1 second
   echo 0 > sys/class/gpio/gpio913/value
   sleep 1 # delay 1 second
   echo LED blinked  $i times
done

# clean stuff, just because we can
echo unexport pins
echo 913 > sys/class/gpio/unexport # MIO[7]
echo pins unexported

echo script finished
Также скрипт можно запускать в фоновом режиме, для этого в конце дописывается $:
./blink_bash.sh $
Также можно создать программу которая будет мигать светодиодом. В той файловой системе (rootfs) которую мы используем нет компилятора, для того чтобы скомпилировать код, нужно либо установить его туда, либо произвести кросс-компиляцию на другом устройстве. Я пошёл по второму пути. Как скомпилировать я нашёл здесь. Сперва нужно установить необходимый компилятор "arm-linux-gnueabihf-gcc", в принципе он используется при сборке Linux проекта. Также он поставляется вместе с Vivado. Для получения доступа к нему через Vivado, нужно использовать команду source и указать адрес к настройкам которые хранятся в папке с Vivado:
source tools/Xilinx/Vivado/2019.1/settings64.sh

Создадим исходный файл blin_bin.c, вставим в него код взятый отсюда.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main ()
{
 int exportfd;
 
 exportfd = open( "/sys/class/gpio/export" , O_WRONLY );
 if( exportfd < 0)
 {
  printf( "Cannot open GPIO to export it \n");
  exit (1);
 }
 write( exportfd ,"913",4);
 close( exportfd );
 printf(" GPIO exported successfully \n " ); 

 int directionfd = open("/sys/class/gpio/gpio913/direction", O_RDWR);
 if( directionfd < 0){
  printf("cannot open the file");
  exit(1); 
 }
 write( directionfd , "out" , 4);
 close (directionfd);
 
 /* Get the GPIO value ready to be toggled */
 int valuefd = open( "/sys/class/gpio/gpio913/value" , O_RDWR );

 if ( valuefd < 0){
  printf(" Cannot open GPIO value \n " );
  exit (1);
 }
 printf(" GPIO value opened , now toggling ...\n " );
 /* toggle the GPIO forever , press control + c to stop it */
 while (1)
 {
  int i ;
  for(i=0;i<0xFF;i++){}
   printf(" GPI0  -off\n");
   write( valuefd , "1" , 2);   
   usleep(500000);
 
   printf(" GPI0  -on\n");
   write( valuefd , "0" , 2);
   usleep(500000);
 }
printf("blink program finished");
}
Для его компиляции используется команда:
arm-linux-gnueabihf-gcc blink_bin.c -o blink_bin
После этого появится бинарный файл blink_bin. Его нужно перенести в Zybo, как это сделать было описано здесь.

Затем нужно выставить ему права на запуск и выполнить его:
chmod +x blink_bin
./blink_bin
Скорей всего появится ошибка:
-/bin/ash: ./blink_bin: not found

Рис.22 файл не найден
Как оказалось, благодаря следующим источникам: 1234. Скомпилированный файл ссылается на динамическую библиотеку которой нет в нашей системе на Zybo. Было обновление после версии Vivado 2015.4 (скорей всего это связано с Linux, ARM Linaro а не Xilinx), после которого используемой динамической библиотекой для скомпилированных программ стала /lib/ld-linux-armhf.so.3, её и не может найти наша программа. Раньше использовалась /lib/ld-linux.so.3, которая находится в нашей системе на Zybo. Для того чтобы узнать какую динамическую библиотеку использует программа нужно выполнить 
readelf -l blink_bin
которая выдаст:
Elf file type is DYN (Shared object file)
Entry point 0x505
There are 9 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x0008c8 0x000008c8 0x000008c8 0x00008 0x00008 R   0x4
  PHDR           0x000034 0x00000034 0x00000034 0x00120 0x00120 R   0x4
  INTERP         0x000154 0x00000154 0x00000154 0x00019 0x00019 R   0x1
      [Requesting program interpreter: /lib/ld-linux-armhf.so.3]
  LOAD           0x000000 0x00000000 0x00000000 0x008d4 0x008d4 R E 0x10000
  LOAD           0x000eac 0x00010eac 0x00010eac 0x0015c 0x00160 RW  0x10000
  DYNAMIC        0x000eb4 0x00010eb4 0x00010eb4 0x000f8 0x000f8 RW  0x4
  NOTE           0x000170 0x00000170 0x00000170 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x000eac 0x00010eac 0x00010eac 0x00154 0x00154 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .ARM.exidx 
   01     
   02     .interp 
   03     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .ARM.exidx .eh_frame 
   04     .init_array .fini_array .dynamic .got .data .bss 
   05     .dynamic 
   06     .note.ABI-tag .note.gnu.build-id 
   07     
   08     .init_array .fini_array .dynamic .got 
либо
objdump -j .interp -s blink_bin
blink_bin:     file format elf32-little

Contents of section .interp:
 0154 2f6c6962 2f6c642d 6c696e75 782d6172  /lib/ld-linux-ar
 0164 6d68662e 736f2e33 00                 mhf.so.3.  
Для того чтобы избежать проблем с динамическими библиотеками, для компиляции можно воспользоваться командой:
 arm-linux-gnueabihf-gcc blink_bin.c -o blink_bin -static
Это вставит весь код из библиотеки в бинарный файл, этот файл запуститься, но он весит в 100 раз больше.
Как оказалось файлы которые указаны как динамические библиотеки, на который ссылается файл, на самом деле ссылки на настоящие библиотеки. Я скачал оба файла и оказалось, что "ld-linux-armhf.so.3" и "ld-linux.so.3" отличаются только названием. Они оба ссылаются на библиотеку "ld-2.29.so".

Таким образом для запуска нашего файла, нужно либо добавить необходимый файл "ld-linux-armhf.so.3". Либо создать его самому. В нашем случае файл "ld-linux.so.3" ссылается на файл "ld-2.13.so". это можно проверить командой:
ls -l lib
Она покажет содержимое папки lib в которой находятся библиотеки:

Рис.23 содержимое папки lib
Но всё же, сделаем ссылку на файл ld-linux.so.3, если он вдруг измениться (к примеру новая библиотека добавится), нам не нужно будет ничего менять. Для этого нужно выполнить команду:
ln -s ld-linux.so.3 /lib/ld-linux-armhf.so.3
Тогда в папке lib появится файл ld-linux-armhf.so, который будет ссылаться на файл ld-linux.so.3, который находится в той же папке. После этого файл можно запустить.

Стоит отметить, что после выключения питания все файлы пропадут, т.к. всё находится в RAM памяти, для того чтобы избежать этого необходимо изменять rootfs в загрузочном файле BOOT.bin, о том как это сделать было описано ранее.

dev/mem

К выводам можно также обращаться напрямую через регистры. Для этого используется команда devmem. Адреса регистров можно найти в документации ug585 стр. 1345. Адрес для AXI можно посмотреть в Vivado открыв блок дизайн и нажав на вкладку "Address Editor":
Рис.24 адрес AXI ядра
Синтаксис команды devmem:
Рис.25 команда devmem
Нужно указать адрес и размер возвращаемой ячейки (обычно 32 бита) если необходимо только считать данные, если же нужно записать информацию, то в конце нужно указать и её. светодиод который подключен через AXI находится по адресу 0x41200000. Для того чтобы его зажечь достаточно команды:
devmem 0x41200000 32 1
Чтобы потушить
devmem 0x41200000 32 0
Для работы с AXI доступно несколько различных регистров, можно работать также как с GPIO, настраивать на ввод, вывод, HI-Z и использовать прерывания. Подробнее в документации.

С выводами MIO[7] и EMIO[0] немного посложней, из-за того что их сперва нужно настроить используя несколько регистров. По умолчанию они настроены на вход.
Регистры относятся к банкам, MIO[7] находится в банке 0, EMIO[0] в банке 3. Рассмотрим регистры банка 0, для других банков они идентичны. XGPIOPS_DATA_LSW_OFFSET - установка выходного значения на порт для младших 16 битов с маскированием. 32 битный регистр, старшие 16 бит являются маской, по умолчанию в маске все 0, 0 в маске обозначает что данный бит будет записан, 1 в маске означает что он останется без изменений. Младшие 16 бит являются данными которые будут записаны если в том же месте в старшем бите находится 0. XGPIOPS_DATA_MSW_OFFSET - то же самое для старших 16 бит. XGPIOPS_DATA_OFFSET - значение на выходе порта. DATA_0_RO - чтение данных на порте. XGPIOPS_DIRM_OFFSET - направление данных на порте 0 - вход, 1 - выход. XGPIOPS_OUTEN_OFFSET - включение или отключение выхода. 0 - переход в Hi-Z, 1 - нормальный режим. Также есть дополнительные регистры для прерываний, но они не рассматриваются в данном руководстве. Для того чтобы зажечь светодиод необходимо настроить порт на вывод, подключить его и подать 1. Необходимая последовательность команд:
devmem 0xE000A204 32 0x80
devmem 0xE000A208 32 0x80
devmem 0xE000A000 32 0xFF7F0080
Рис.26 зажигание светодиода на MIO[7] 
Использовалась маскированая передача для того чтобы не помешать работе других устройств использующих MIO выводы. Для EMIO[0] последовательность следующая:
devmem 0xE000A284 32 1
devmem 0xE000A288 32 1
devmem 0xE000A048 32 1
Здесь использовалась прямая запись в регистр вывода, т.к. у нас ничего больше не подключено.

Команда devmem всего лишь автоматизирует запись в файл dev/mem который отображает реальную память процессора. Всё, что находится в RAM памяти доступно через файл dev/mem. Для работы с ним нужно скомпилировать программу для чтения и записи в этот файл. Вот пример для работы с AXI, взято отсюда.
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <string.h> 
#include <errno.h> 
#include <signal.h> 
#include <fcntl.h> 
#include <ctype.h> 
#include <termios.h> 
#include <sys/types.h> 
#include <sys/mman.h> 

#define LEDBASE 0x41200000 
#define MAP_SIZE 4096UL 
#define LED_BIT 1

int main(int argc, char **argv) { 
  int fd; 
  void *map_base;
  unsigned char *led_val;
  off_t offset; 
 
   offset = LEDBASE; 

   if ((fd = open ("/dev/mem", O_RDWR | O_SYNC)) == -1) {
     
    printf ("/dev/mem not opened.\n");
    return -1;
  }

   map_base = mmap (0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, offset); 
   if (map_base == (void *) -1) {
    close(fd);
    printf ("/dev/mem mapping failed.\n");
  }
  led_val = map_base;
  while(1)
  {
    printf ("LED is %s \n", (*led_val & 1) ? "On": "Off");
    fflush (stdout); 
    sleep(1);
    printf ("Changing LED Status\n");
    fflush (stdout); 
    *led_val ^= LED_BIT;
    sleep(1);
    
  }

  if (munmap(map_base, MAP_SIZE) == -1) {
    printf("unmap failed\n");
  }
  close (fd); 
  return 0; 
}
В заключении стоит добавить, что работа с памятью напрямую через команду devmem, либо с помощью файла /dev/mem, по умолчанию возможна только с правами администратора. Если обычный пользователь попробует воспользоваться данными методами, ему будет отказано в доступе, это сделано ради обеспечения безопасности. Для того чтобы работать с периферией, определённые отделы памяти объявляют доступными пользователю, либо создают файлы которые драйвер сам обрабатывает и определяет, что следует поместить в регистры. Это делается посредством драйверов, пример этого sysfs, который был использован выше. Для указания направления мы прописывали "out" драйвер сам определял, что нужно записать в регистр.

Получается, что для работы с AXI интерфейсом нужны либо права администратора, либо подходящий драйвер.

4 комментария :

  1. Целую ваши ноги) спасибо огромное

    ОтветитьУдалить
  2. #!/bin/bash не момешает в начале скипта... и / перед sys

    ОтветитьУдалить
  3. Хорошая статья. Спасибо. Только не понял как вы определили адрес MIO[7] (913). У меня другая плата и адрес оказался 1023, хотя регистры совпали. Как и где его увидеть (адрес MIO), что бы не подбирать? Может подскажите.

    ОтветитьУдалить
    Ответы
    1. На "Рис.18 содержимое папки gpio"
      можно увидеть папку "gpiochip906" которая содержит файлы для работы с MIO[0].
      Она была там изначально.

      Т.к. мне нужен MIO[7], мне нужна папка с номером 906 + 7 = 913. т.е. gpiochip913.

      Возможно в вашем случае, изначально была папка "gpiochip1016".
      Тогда 1016 + 7 = 1023.

      Но я в этом не уверен. Таким образом я нашёл адрес для моей платы.

      Удалить