Вдохновившись серией статей на сайте проекта Марсоход, в которых автор пытается запустить на FPGA-плате Марсоход 2 открытую систему на кристалле Amber SoC и Linux, я решил попробовать повторить этот опыт на своей плате Terasic DE2-115. Но, вместо древней как говно мамонта устаревшей версии Linux 2.4.27, я буду запускать последнюю версию Linux на данный момент — 4.8.0-rc5.
Система на кристалле Amber
Процессорное ядро Amber представляет собой 32-битный RISC процессор, полностью совместимый с архитектурой и системой команд ARM v2a, что позволяет компилировать программы для него с использованием GCC. Помимо, собственно, процессора, проект Amber предоставляет в составе системы на кристалле несколько периферийных устройств, включая UART, таймер и Ethernet MAC. Процессорное ядро предоставляется в двух вариантах:
Amber 23 | Amber 25 | |
Конвейер | трехуровневый | пятиуровневый |
Кэш | общий (код + данные) | раздельный |
Разрядность шины Wishbone | 32 бита | 128 бит |
Производительность | 0.75 DMIPS/MHz | 1.05 DMIPS/MHz |
Как видно, производительность процессорного ядра сравнима с производительностью ядер, основанных на более поздних версиях архитектуры ARM, таких как ARMv4 и ARMv5. Архитектура ARMv2a реализована в процессоре Amber по той причине, что она не покрывается патентами и ее реализации могут свободно распространяться. С этим, однако, связаны и некоторые проблемы — данная архитектура считается устаревшей в GCC, откуда ее поддержка постепенно «выпиливается», а из ядра Linux поддержка этой архитектуры была удалена уже давно.
Важной особенностью архитектуры является то что, в отличие от более новых версий архитектуры ARM, процессор не поддерживает THUMB-режим, в нем отсутствуют регистры CPSR/SPSR и поддержка инструкций MSR/MRS, а флаги процессора содержатся в части битов регистра PC:
Из-за этого процессор может адресовать в регистре PC максимум 64 МБ памяти (26 бит), два младших из которых всегда равны 0, т.к. инструкции всегда выровнены по границе слова, поэтому два младших бита регистра используются под флаги, определяющие режим работы процессора (пользовательский/привилегированный, обработчик прерывания). В других регистрах процессор может адресовать до 4 ГБ памяти. Более подробно с архитектурой процессорного ядра и реализованным в нем набором команд можно ознакомиться тут и тут.
Установка кросс-компилятора ARM
К сожалению, компилятор Sourcery CodeBench Lite, которым пользовался автор статей о портировании проекта на плату Марсоход, более недоступны для скачивания, но это не очень большая проблема. Для установки компилятора можно воспользоваться crosstool-NG или crossdev
в Gentoo Linux.
Для установки с помощью crosstool-NG достаточно воспользоваться конфигурацией arm-unknown-eabi
, доступной «из коробки»:
$ ct-ng arm-unknown-eabi
$ ct-ng build
Этот компилятор будет использоваться для сборки ядра Linux и bare-metal программ, таких как первоначальный загрузчик, и простое приложение, печатающее Hello, World в последовательный порт.
Компилируем Hello World и запускаем в Verilog-симуляторе Verilator
Скачиваем дистрибутив проекта с GitHub и заглядываем внутрь: проект поделен на 2 части — в папке hw
содержатся исходники «аппаратной» части на языке Verilog, а в папке sw
— исходный код программ, которые будут выполняться на процессоре, и некоторых вспомогательных утилит, использующихся при сборке и преобразующих форматы файлов ELF и BIN в формат, поддерживаемый инструментами Xilinx и скриптами тестирования (test bench) проекта Amber.
Переходим в папку sw/hello
и компилируем программу hello-world.c
:
$ cd sw/hello-world
$ export AMBER_CROSSTOOL=arm-unknown-eabi
$ make
В результате, помимо всего прочего, будет сгенерирован файл hello-world.mem
— текстовый файл с содержимым скомпилированной программы, пригодный для загрузки в симулятор и в Boot ROM нашего процессора.
Автор оригинальных статей, которыми я руководствовался, использовал для симуляции проекта Icarus Verilog — бесплатный и весьма популярный симулятор, но проблема в том, что он работает ужасно медленно — на моей машине с процессором 2.6 ГГц тактовая частота ядра Amber при симуляции в Icarus Verilog составляет около 16 кГц, и каждый символ строки «Hello, World» из примера выше выводится на экран около полсекунды. Такая скорость достаточна, если нужно отладить выполнение небольшой программы, вроде начального загрузчика или того же hello-world, но неприемлема, если требуется отладить загрузку целого ядра Linux — приходится ждать целую вечность.
Поэтому будем использовать симулятор Verilator, который компилирует Verilog в С++ и работает очень быстро — Hello World печатается мгновенно без какой-либо видимой задержки, а тактовая частота на моей машине составляет около 1.5 МГц, что в 100 раз быстрее, чем Icarus Verilog! Кстати, процесс отладки запуска ядра Linux занял у меня около недели, и в этом очень помогала симуляция, поскольку в режиме симуляции код test bench'а пишет в текстовый лог-файл ассемблерный листинг всех инструкций, выполняемых процессором, включая переходы по адресам, асинхронные и программные прерывания и т.д. Этакий дизассемблер, реализованный на языке Verilog.
Устанавливаем Verilator по инструкции с официального сайта, переходим в папку hw/de2_115/tb
, в которой находится модифицированный тестбенч, и делаем make
. Не взирая на поток предупреждений компилятора Verilog, в результате появится папка obj_dir
, а в ней — исполняемый файл Vtb
, который мы и будем запускать, чтобы симулировать работу системы.
Далее выполняем следующие команды:
$ cp ../../../sw/hello-world/hello-world.mem ./boot-loader.mem
$ ./obj_dir/Vtb
В результате будет запущена симуляция и мы увидим долгожданный Hello, World:
Load boot memory from boot-loader.mem
Read in 961 lines
Hello, World!
Это значит, что процессор успешно прочитал и выполнил нашу программу, скомпилированную GCC под ARM!
При желании, можно в Makefile
добавить в список ключей запуска verilator
ключ --trace
, тогда в процессе работы тестбенча будет генерироваться еще один файл — out.vcd
, который затем можно открыть программой GTKWave, и воочию лицезреть осциллограммы различных сигналов внутри процессора и других блоков:
Сборка initramfs с помощью Builtroot
Перед тем, как собирать ядро Linux, создадим окружение для компиляции пользовательских программ под нашу систему (на базе uClibc-ng) и сгенерируем файл, который будет добавлен в ядро в качестве initramfs в процессе сборки. Для этого воспользуемся Buildroot, который можно скачать отсюда.
$ make amber_defconfig
$ make
В результате у нас будет собран тулчейн arm-buildroot-uclinux-uclibcgnueabi
и образ файловой системы в ./output/images/rootfs.cpio
. Путь к этому образу нужно будет указать в конфигурационном файле ядра, параметр CONFIG_INITRAMFS_SOURCE
. В образ файловой системы включен BusyBox, однако он пока у меня до конца не запускается, и в рамках этой статьи ограничимся простым «Hello, World» в качестве процесса /sbin/init
. Для этого в директории, в которой собирался BuildRoot создаем файл hello.c
с известным каждому программисту содержимым, и выполняем следующие команды:
$ ./output/host/usr/bin/arm-buildroot-uclinux-uclibcgnueabi-gcc -o hello hello.c
$ mv hello output/target/sbin/init
$ rm hello.gdb
$ make
После успешного выполнения этих команд ./output/images/rootfs.cpio
будет пересобран с нашим приложением вместо BusyBox. Такой способ подмены файлов подходит, чтобы быстро что-то проверить, для полноценного же добавления и замены файлов в rootfs
в процессе сборки существует конфигурационная опция BR2_ROOTFS_OVERLAY
.
В отличие от того примера, который мы запускали в симуляторе Verilator, этот новый «Hello, World» уже работает не как bare-metal приложение, а как пользовательское приложение под Linux — текст будет выводиться в последовательный порт с помощью стандартной библиотеки uClibc, которая сделает системный вызов write
и передаст управление ядру через программное прерывание, ядро передаст управление драйверу tty
, потом драйверу последовательного порта и, наконец, сообщение будет выведено.
Сборка ядра Linux и начального загрузчика
Естественно, чтобы запустить самое свежее ядро, в него пришлось вносить некоторые изменения. По большей части эти изменения связаны с кодом обработки прерываний и переключения режимов процессора, так как этот код является зависимым от архитектуры. Далее я адаптировал код поддержки платформы Integrator (mach-integrator), т.к. в оригинальном патче автора проекта Amber для ядра 2.4 есть намеки, что именно эта платформа является прообразом архитектуры SoC Amber (в частности, было обнаружено, что периферийные устройства, такие как контроллер прерываний, таймер и последовательный порт, реализованы совместимыми с драйверами устройств, используемых на этой платформе) и создал на его основе новую платформу Amber.
К счастью, часы отладки позади, и теперь сборка рабочего ядра делается легким движением руки. Желающие повторить его могут склонировать исходники и выполнить следующие команды:
$ make ARCH=arm CROSS_BUILD=arm-none-eabi- amber_defconfig
$ make -j8 ARCH=arm CROSS_BUILD=arm-none-eabi- Image
$ make ARCH=arm CROSS_BUILD=arm-none-eabi- arch/arm/boot/dts/amber-de2115.dts
После сборки ядра будет созданы файлы arch/arm/boot/Image
и arch/arm/boot/dts/amber-de2115.dtb
, готовые к загрузке в плату с помощью начального загрузчика через последовательный порт по протоколу XMODEM.
Для сборки начального загрузчика переходим в папку sw/boot-loader-serial
, делаем make
(не забываем про переменную окружения AMBER_CROSSTOOL
) и получаем файл boot-loader-serial.mem
, который с помощью утилиты mem2mif
можно преобразовать в формат MIF, который принимает Altera Quartus II в качестве файла инициализации памяти.
Собираем все воедино
Тем, у кого есть плата Terasic DE2-115 самое время открыть проект de2_115.qpf
, синтезировать его (обратите внимание, что у меня в проекте последовательный порт выведен на разъем EXT_IO вместо имеющегося на плате RS232, так как на моей материнке отсутствуют COM-порты), указать в качестве файла инициализации памяти de2_115_sram_2048_32_byte_en
полученный на предыдущем шаге boot-loader-serial.mif
и загрузить битстрим в плату. Поскольку в процессоре Amber по одному разработчику известным причинам не была реализована логика сброса, то сбросить процессор в начальное состояние можно только перезаливкой битстрима. При этом, если в процессе удерживать в нажатом состоянии кнопку KEY0, то процессор не начнет выполнение программы до тех пор, пока ее не отпустить. Я пользовался этой кнопкой для отладки Verilog-кода с помощью SignalTap. Но уж если вы ее отпустили, то только перезагрузка битстрима поможет начать все сначала.
После загрузки битстрима в терминале, настроенном на 460800 бод, немедленно появится приглашение загрузчика Amber. Далее нужно набрать команду b 80000
и отправить с помощью XMODEM файл ядра Linux (arch/arm/boot/Image
), сформированный ранее, а затем снова набрать команду b 78000
и отправить файл DTB, в котором описано, какие устройства по каким адресам искать, какие драйверы для них загружать, сколько в системе оперативной памяти, командная строка с параметрами ядра и другая информация. Я пропатчил начальный загрузчик таким образом, что он передает ядру адрес 0x78000
в качестве адреса, где следует искать DTB, поэтому мы его загружаем именно по этому адресу.
Наконец, когда оба файла загружены в оперативную память (SDRAM), можно ввести команду j 80000
в консоли загрузчика. Начнется загрузка Linux, и, если все сделано правильно, то результатом будет примерно такой вывод:
Наш «Hello, World» запустился в качестве первого пользовательского процесса (/sbin/init
) и вывел заветную фразу на экран через стандартную библиотеку и ядро. Здорово, не правда ли?
Если у вас нет платы Terasic DE2-115 или какой-либо другой платы с ПЛИС достаточного объема, то все равно можно запустить Linux в симуляторе Verilator. Для этого в hw/de2_115/tb/Makefile
нужно добавить ключи -DAMBER_LOAD_MAIN_MEM=1
и -DAMBER_LOAD_DTB_MEM=1
, и пересобрать исполняемый файл Vtb
. Затем, с помощью утилиты amber-bin2mem
создаем файлы ядра и DTB для симулятора:
$ amber-bin2mem arch/arm/boot/Image 80000 > vmlinux.mem
$ amber-bin2mem arch/arm/boot/dts/amber-de2115.dtb 78000 > dtb.mem
Кроме того, потребуется немного поправить код начального загрузчика для симуляции, закомментировав вызов функции main
так как в обычном режиме он запрашивает команды от пользователя. Тогда загрузчик сразу передаст управление ядру Linux. Копируем файлы *.mem
в папку с тестбенчем, запускаем: ./obj_dir/Vtb
и наблюдаем за загрузкой Linux.
Ограничения, практическая польза
Конечно, запустившийся в итоге Linux не совсем похож на тот, каким мы его привыкли видеть на серверах и рабочих станциях, в силу того, что процессорное ядро Amber не имеет MMU (Memory Management Unit) и, как следствие, поддержки виртуальной памяти (вся память является физической), защиты памяти (любое приложение может испортить память ядра или общаться с устройствами минуя его, через шину Wishbone), copy-on-write и др. В NOMMU Linux на данный момент не поддерживаются в привычном виде исполняемые файлы формата ELF (хотя есть наработки по поддержке формата FDPIC ELF) и динамические библиотеки — вместо этого используется формат bFLT (Binary Flat) — простой формат, основанный на a.out
. И если вы запустите на такой системе, скажем, N экземпляров какого-то приложения, то ровно столько же его копий будет находиться в памяти.
Практическая польза от проделанной работы все же есть, даже такие «урезанные» версии Linux работают во многих устройствах, основанных на микроконтроллерах с ограниченными ресурсами. Надеюсь, что увлекающиеся программированием FPGA читатели смогут почерпнуть для себя что-то полезное, экспериментируя с полноценным Linux на синтезированном в FPGA процессоре (который кстати, на DE2-115 занимает всего 8% емкости или около 10,000 LE). Если у вас другая плата на базе Altera или Xilinx, то портирование на нее не составит труда, т.к. основная часть работы уже проделана. Конечно, сейчас уже существуют более интересные с практической точки зрения решения, такие как Xilinx Zynq, Altera Cyclone V SoC, которые содержат полноценный ARM-SoC на одном кристалле с FPGA, но представленное в данной статье решение позволяет запустить Linux даже обладателям простых плат с не очень мощными ПЛИС на борту. Оставшуюся свободной логику можно использовать для реализации новых кастомных периферийных устройств, которые «вешать» на шину Wishbone и делать доступными из ОС с помощью драйверов.
Планы
Плата Terasic DE2-115 — это поистине одна из самых мощных отладочных плат, на базе которой уже были сделаны интереснейшие проекты (вот ярчайший пример). Она имеет на борту широкий набор периферийных устройств:
- 128 MB SDRAM
- 8 MB SPI Flash
- Светодиоды и семисегментные индикаторы
- 16х2 жидкокристаллический дисплей
- 24-битный аудио-кодек
- Слот для SD-карты памяти
- 2 гигабитных Ethernet-порта
- VGA-выход на монитор, PS/2 для клавиатуры
- Порты USB
Из всего этого богатства я в данном проекте пока использовал только оперативную память. В будущем, если будет время, я хочу скомпилировать U-Boot и разместить его во встроенной Flash-памяти, в коде начального загрузчика в ПЛИС загружать U-Boot, который бы затем загружал ядро Linux и корневую файловую систему с SD-карты памяти. Кроме того, хотелось бы попробовать реализовать поддержку периферийных устройств, имеющихся на плате — Ethernet, например. Но для начала, надо разобраться, почему «падает» при запуске BusyBox.
Автор: madprogrammer