Теория
Есть 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[53:32]
Банк 0 контролирует выводы EMIO[31: 0]
Банк 0 контролирует выводы EMIO[63:32]
Выводы можно настраивать как независимо друг от друга так и в пределах всего банка. Выходное напряжение для банков MIO должно быть одинаково внутри банка, но может отличаться между банками, для EMIO это не имеет значения, т.к. они выводятся через PL.
К каждой ножке может быть программно подключен Pull - UP резистор. На каждый вывод может быть независимо настроено прерывание на изменение (фронт, срез) либо уровень (высокий, низкий).
У каждого банка есть свой набор регистров отвечающих за конфигурацию выводов:
Рис.6 краткое описание конфигурационных регистров |
Насчёт 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 |
Рис.8 настройка EMIO |
Рис.9 настройка AXI |
Рис.10 Настройка тактовой частоты AXI |
Рис.11 готовое к подключению ядро процессора |
Подключение
Далее нужно подключить всё необходимое. MIO подключён по умолчанию к соответствующему выводу FPGA. соединим EMIO. Если раскрыть вкладку GPIO_0, на ядре процессора, можно будет увидеть 3 вывода, вход, выход и включение вывода (out enable), для перевода в Hi-Z состояние. Нам нужно вывести наружу выход для управления светодиодом, можно нажать на соответствующий вывод правой кнопкой и выбрать "Make External", в таком случае схема будет иметь следующий вид:
Рис.12 подключённый EMIO вывод |
Рис.13 настройки AXI GPIO |
Рис.14 блок дизайн вместе с AXI GPIO |
Для этого сперва нужно обновить wrapper:
Рис.15 обновление wrapper |
Рис.16 переход в корректный режим отображения вкладок |
Рис.17 настройка выходных ножек |
SYSFS
Сперва перейдём в папку /sys/class/gpio.
cd sys/class/gpioВыведем её содержимое.
lsтам должно быть файлы "export", "unexport" и папка "gpiochip906"
Рис.18 содержимое папки gpio |
Для того чтобы подключить необходимую ножку, в нашем случае MIO[7], необходимо выполнить команду:
echo 913 > export906 + 7 = 913. Таким образом мы запишем в файл export значение 913 благодаря чему появится дополнительная папка "gpio913":
Рис.19 новая папка gpio913 |
cd gpio913 ls
Рис.20 содержимое папки gpio913 |
Рис.21 зажигание светодиода |
С помощью данных команд можно написать свою программу или скрипт который будет обрабатывать 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 файл не найден |
Как оказалось, благодаря следующим источникам: 1, 2, 3, 4. Скомпилированный файл ссылается на динамическую библиотеку которой нет в нашей системе на 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, о том как это сделать было описано ранее.
Стоит отметить, что после выключения питания все файлы пропадут, т.к. всё находится в RAM памяти, для того чтобы избежать этого необходимо изменять rootfs в загрузочном файле BOOT.bin, о том как это сделать было описано ранее.
dev/mem
К выводам можно также обращаться напрямую через регистры. Для этого используется команда devmem. Адреса регистров можно найти в документации ug585 стр. 1345.
Адрес для AXI можно посмотреть в Vivado открыв блок дизайн и нажав на вкладку "Address Editor":
Рис.24 адрес AXI ядра |
Рис.25 команда devmem |
devmem 0x41200000 32 1
Чтобы потушить
devmem 0x41200000 32 0
Для работы с AXI доступно несколько различных регистров, можно работать также как с GPIO, настраивать на ввод, вывод, HI-Z и использовать прерывания. Подробнее в документации.
С выводами MIO[7] и EMIO[0] немного посложней, из-за того что их сперва нужно настроить используя несколько регистров. По умолчанию они настроены на вход.
С выводами MIO[7] и EMIO[0] немного посложней, из-за того что их сперва нужно настроить используя несколько регистров. По умолчанию они настроены на вход.
devmem 0xE000A204 32 0x80 devmem 0xE000A208 32 0x80 devmem 0xE000A000 32 0xFF7F0080
Рис.26 зажигание светодиода на MIO[7] |
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 интерфейсом нужны либо права администратора, либо подходящий драйвер.
Целую ваши ноги) спасибо огромное
ОтветитьУдалить#!/bin/bash не момешает в начале скипта... и / перед sys
ОтветитьУдалитьХорошая статья. Спасибо. Только не понял как вы определили адрес MIO[7] (913). У меня другая плата и адрес оказался 1023, хотя регистры совпали. Как и где его увидеть (адрес MIO), что бы не подбирать? Может подскажите.
ОтветитьУдалитьНа "Рис.18 содержимое папки gpio"
Удалитьможно увидеть папку "gpiochip906" которая содержит файлы для работы с MIO[0].
Она была там изначально.
Т.к. мне нужен MIO[7], мне нужна папка с номером 906 + 7 = 913. т.е. gpiochip913.
Возможно в вашем случае, изначально была папка "gpiochip1016".
Тогда 1016 + 7 = 1023.
Но я в этом не уверен. Таким образом я нашёл адрес для моей платы.