Знакомство с Litex на Tang Nano 9K

от автора

Автору всегда нравилась идея Litex, фреймворка для простой сборки SoC на FPGA, но постоянно не хватало времени, чтобы попробовать. Пришло время изменить это и задокументировать процесс! Мы будем использовать плату FPGA Sipeed Tang Nano 9K, которая является относительно недорогим оборудованием, тем не менее большая часть этой статьи применима к любому поддерживаемому Litex FPGA.

Пришлось кое-что подучить, Litex написан на Python, или, точнее, он использует Migen, инструмент на основе Python, который генерирует Verilog. Автор никогда не писал много кода на Python, не говоря уже о Migen. Таким образом, чтобы освоить основы Litex, необходимо было выполнить следующее:

  1. Разобрать минимальный пример SoC

  2. Настроить SoC с некоторыми периферийными устройствами, уже доступными в LiteX

  3. Написать пользовательское приложение и запустить его на созданном SoC

  4. Создать комфортную среду разработки

Прежде чем продолжить давайте сначала установим Litex и создадим пример!

Создание SoC из примера

Это довольно просто в случае использования актуального Linux, если следовать руководству все работает на Debian 12. Чтобы собирать некоторые примеры, необходим стандартный или полный конфиг, а также нужно установить тулчейн RISC-V. К счастью, руководство по быстрому старту хорошо объясняет все это.

Теперь о тулчейне Gowin, он не является открытым исходным кодом, хотя и бесплатен. Для получения лицензии нужно подать заявку. Ее можно скачать здесь после регистрации. Тулчейн с открытым исходным кодом находится в процессе разработки, однако на момент написания статьи (год назад) он еще не готов для использования с Litex.

Исполняемый файл gw_sh от Gowin необходимо добавить в путь, например через .bashrc:

PATH="$PATH:/path/to/gowin/IDE/bin"

После установки следует перейти в директорию “litex/litex-boards/litex_boards/targets” и выполнить:

./sipeed_tang_nano_9k.py --build --flash

Это займет довольно много времени, выполняется компиляция, синтез, размещение и трассировка, а затем прошивка FPGA. Светодиоды будут весело подмигивать, а после подключения последовательного порта на скорости 115200 бод будет отображено приветствие:

В итоге мы собрали пример, хотя и не имеем понятия, что и как он делает. К счастью, в комплекте есть исходник, давайте взглянем на него. Автор потратил некоторое время, чтобы удалить все, что мог из примера sipeed 9K, чтобы тот больше соответствовал simple.py, и в итоге получил следующее:

import os from migen import *  from litex.gen import *  from litex_boards.platforms import sipeed_tang_nano_9k  from litex.build.io import CRG from litex.soc.integration.soc_core import * from litex.soc.integration.soc import SoCRegion from litex.soc.integration.builder import *  kB = 1024 mB = 1024*kB  # BaseSoC ------------------------------------------------------------------------------------------ class BaseSoC(SoCCore):     def __init__(self, **kwargs):         platform = sipeed_tang_nano_9k.Platform()          sys_clk_freq = int(1e9/platform.default_clk_period)          # CRG --------------------------------------------------------------------------------------         self.crg = CRG(platform.request(platform.default_clk_name))          # SoCCore ----------------------------------------------------------------------------------         kwargs["integrated_rom_size"] = 64*kB           kwargs["integrated_sram_size"] = 8*kB         SoCCore.__init__(self, platform, sys_clk_freq, ident="Tiny LiteX SoC on Tang Nano 9K", **kwargs)  # Build -------------------------------------------------------------------------------------------- def main():     from litex.build.parser import LiteXArgumentParser     parser = LiteXArgumentParser(platform=sipeed_tang_nano_9K_platform.Platform, description="Tiny LiteX SoC on Tang Nano 9K.")     parser.add_target_argument("--flash",                action="store_true",      help="Flash Bitstream.")     args = parser.parse_args()      soc = BaseSoC( **parser.soc_argdict)      builder = Builder(soc, **parser.builder_argdict)     if args.build:         builder.build(**parser.toolchain_argdict)      if args.load:         prog = soc.platform.create_programmer("openfpgaloader")         prog.load_bitstream(builder.get_bitstream_filename(mode="sram"))      if args.flash:         prog = soc.platform.create_programmer("openfpgaloader")         prog.flash(0, builder.get_bitstream_filename(mode="flash", ext=".fs"))          prog.flash(0, builder.get_bios_filename(), external=True)  if __name__ == "__main__":     main()

Ух ты, это около 50 строк, неплохо. Оказывается в Litex происходит много волшебства, чтобы сохранить код компактным, давайте попробуем разобрать его!

Сначала несколько директив импорта и определений,

from litex_boards.platforms import sipeed_tang_nano_9k

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

Остальные директивы подключают migen, язык HDL, используемый в Litex, и некоторые базовые блоки для создания SoC.

import os from migen import *   from litex.gen import *  from litex_boards.platforms import sipeed_tang_nano_9k  from litex.build.io import CRG from litex.soc.integration.soc_core import * from litex.soc.integration.soc import SoCRegion from litex.soc.integration.builder import *  kB = 1024 mB = 1024*kB

Теперь пора перейти к концу кода и взглянуть на основную функцию:

def main():     from litex.build.parser import LiteXArgumentParser     parser = LiteXArgumentParser(platform=sipeed_tang_nano_9K_platform.Platform, description="Tiny LiteX SoC on Tang Nano 9K.")     parser.add_target_argument("--flash",                action="store_true",      help="Flash Bitstream.")     args = parser.parse_args()      soc = BaseSoC( **parser.soc_argdict)      builder = Builder(soc, **parser.builder_argdict)     if args.build:         builder.build(**parser.toolchain_argdict)      if args.load:         prog = soc.platform.create_programmer("openfpgaloader")         prog.load_bitstream(builder.get_bitstream_filename(mode="sram"))      if args.flash:         prog = soc.platform.create_programmer("openfpgaloader")         prog.flash(0, builder.get_bitstream_filename(mode="flash", ext=".fs")) # FIXME         prog.flash(0, builder.get_bios_filename(), external=True)

Прежде всего импортируется LitexArgumentParser и создается его экземпляр. Это очень удобная функция в Litex, которая упрощает настройку SoC с помощью аргументов командной строки. Выполнив:

./sipeed_tang_nano_9k.py --help

мы получим полный перечень параметров, вот лишь некоторые из них:

Да, тип процессора — это всего лишь аргумент командной строки, потрясающе!

Затем вызывается функция BaseSoc, которая используется для настройки SoC. Рассмотрим это немного позже. После этого вызывается Litex Builder с SoC в качестве аргумента для построения окончательной SoC.

Наконец, обрабатываются аргументы -load и -flash. Они оба вызывают инструмент OpenFPGALoader, чтобы либо загрузить битстрим в ОЗУ, либо прошить его в SPI-флеш на плате FPGA. OpenFPGALoader устанавливается с помощью скрипта Litex_setup.

А вот и сама SoC!

# BaseSoC ------------------------------------------------------------------------------------------ class BaseSoC(SoCCore):     def __init__(self, **kwargs):         platform = sipeed_tang_nano_9k.Platform()          sys_clk_freq = int(1e9/platform.default_clk_period)          # CRG --------------------------------------------------------------------------------------         self.crg = CRG(platform.request(platform.default_clk_name))          # SoCCore ----------------------------------------------------------------------------------         kwargs["integrated_rom_size"] = 64*kB           kwargs["integrated_sram_size"] = 8*kB         SoCCore.__init__(self, platform, sys_clk_freq, ident="Tiny LiteX SoC on Tang Nano 9K", **kwargs)

Класс BaseSoC создает SoC, который будет передан в Litex Builder немного позже. Исходный SoC в Litex содержит процессор Vexriscv, шину wishbone, немного ОЗУ, ПЗУ, таймер и UART. Все это базовые настраиваемые параметры. Здесь мы задаем тактовых частоту и создаем CRG, формирователь сброса и тактирования, который должен содержать все сигналы сброса и тактирующие сигналы. Пока что есть только один тактовый сигнал, мы рассмотрим это более подробно позже.

Также мы задаем размер ПЗУ и ОЗУ, что, строго говоря, не является обязательным, в случае если подходят стандартные значения. Вся эта информация передается в функцию SoCCore.init, которая возвращает наш SoC.

Вот и все, минимальный SoC готов, потрясающе. Полный пример можно посмотреть на GitHub.

Теперь давайте постепенно добавим к нему новые функции!

Добавляем CRG

В настоящее время CRG очень ограничен по сравнению с приведенным в примере, нет даже кнопки сброса! Давайте изменим это и добавим PLL и сброс.

class _CRG(LiteXModule):     def __init__(self, platform, sys_clk_freq):         self.rst    = Signal()         self.cd_sys = ClockDomain()          # Clk / Rst         clk27 = platform.request("clk27")         rst_n = platform.request("user_btn", 0)          # PLL         self.pll = pll = GW1NPLL(devicename=platform.devicename, device=platform.device)         self.comb += pll.reset.eq(~rst_n)         pll.register_clkin(clk27, 27e6)         pll.create_clkout(self.cd_sys, sys_clk_freq)

По сравнению с предыдущим вариантом, CRG теперь использует одну из пользовательских кнопок в качестве входа сброса. Генерируется PLL, пока с одинаковой частотой на входе и выходе, но это можно изменить, передав в качестве параметра запрашиваемую частоту тактового сигнала, здорово! Сигнал сброса сбрасывает PLL, что, в свою очередь, сбрасывает процессор.

Пришло время периферии

В Litex уже доступно довольно много периферийных устройств: таймеры, UART, I2C, SPI и прочее. К сожалению, документация оставляет желать лучшего, однако после некоторых изысканий автор смог заставить большинство из них работать.

Давайте добавим несколько устройств в файл sipeed_tang_nano_9k.py!

from litex.soc.cores.timer import * from litex.soc.cores.gpio import * from litex.soc.cores.bitbang import I2CMaster from litex.soc.cores.spi import SPIMaster from litex.soc.cores import uart

Готово, это решает проблему с наиболее распространенными периферийными устройствами. К счастью, инициализация тоже не вызывает затруднений!

        self.timer1 = Timer()         self.timer2 = Timer()                  self.leds = GPIOOut(pads = platform.request_all("user_led"))                  # Serial stuff          self.i2c0 = I2CMaster(pads = platform.request("i2c0"))                  self.add_uart("serial0", "uart0")                  self.gpio = GPIOIn(platform.request("user_btn", 1))

Два дополнительных таймера, несколько светодиодов, I2C, UART и вход GPIO в десятке строк кода. Это намного проще, чем VHDL или Verilog. Теперь файл платформы необходимо дополнить, чтобы Litex знал, что размещать на каких входах-выходах:

    ("gpio", 0, Pins("25"), IOStandard("LVCMOS33")),     ("gpio", 1, Pins("26"), IOStandard("LVCMOS33")),     ("gpio", 2, Pins("27"), IOStandard("LVCMOS33")),     ("gpio", 3, Pins("28"), IOStandard("LVCMOS33")),     ("gpio", 4, Pins("29"), IOStandard("LVCMOS33")),     ("gpio", 5, Pins("30"), IOStandard("LVCMOS33")),     ("gpio", 6, Pins("33"), IOStandard("LVCMOS33")),     ("gpio", 7, Pins("34"), IOStandard("LVCMOS33")),          ("i2c0", 0,         Subsignal("sda", Pins("40")),         Subsignal("scl", Pins("35")),         IOStandard("LVCMOS33"),     ),          ("uart0", 0,         Subsignal("rx", Pins("41")),         Subsignal("tx", Pins("42")),         IOStandard("LVCMOS33")     ),

Отлично! Но все же есть небольшая проблема, редактировать все это в репозитории litex-boards не совсем правильно.

Пора создать отдельную директорию для всего этого, а еще лучше — использовать Docker.

Контейнеризация

При обсуждении с другом запуска всего этого на MacBook (IDE Gowin недоступна для Mac OS), для развертывания он создал небольшой контейнер Docker, достаточно указать расположение файла лицензии, и все готово! Автор внес несколько небольших изменений, в основном чтобы установить рабочую директорию и добавить vim. Так что загляните в этот репозиторий и попробуйте!

Это позволит надежно запускать Litex с инструментами Gowin на любом компьютере, независимо от операционной системы и дистрибутива.

Одна проблема решена, теперь нужно навести порядок, автор остановился на следующей структуре директорий:

Директория «platform» содержит файл платформы, а «software» — исходный код на C для программы SoC, который можно найти на моем GitHub.

Команда запуска контейнера в Docker выглядит следующим образом:

docker run --rm \                                     --platform linux/amd64 \     --mac-address xx:xx:xx:xx:xx:xx \     -v "${HOME}/gowin_E_xxxxxxxxxx.lic:/data/license.lic" \     -v ${HOME}/Documents/Git/LitexTang9KExperiments:/data/work \     -it gowin-docker:latest

Файл лицензии привязывается к MAC-адресу сетевой карты, поэтому убедитесь, что вы установили свой MAC-адрес в Docker, чтобы он соответствовал тому, который указан в вашей лицензии. Рекомендуется использовать генератор MAC-адресов, предварительно убедившись в отсутствии потенциальных коллизий.

После запуска контейнера мы сразу оказываемся в нужной папке,

./sipeed_tang_nano_9k.py --build

в шаге от сборки.

Передряги с ПО

Для начала автор посмотрел на демонстрационное приложение в Litex и скомпилировал его. Его можно прошить, интегрировав в внутреннее ПЗУ SoC, но это означает пересборку всей SoC при каждом изменении кода. Это довольно неудобно, если вы хотите быстро вносить изменения в код.

К счастью, у Litex есть отличная программа под названием litex_term, которая может использоваться для загрузки бинарных файлов и подключения терминала к SoC.

Стандартный BIOS в Litex поддерживает загрузку и выполнение бинарных файлов, похоже на загрузчик в Arduino. Использовать его довольно просто:

litex_term /dev/TTYhere --kernel=yourapp.bin

чтобы повторно загрузить бинарный файл после внесения изменений достаточно просто перезагрузить плату!

На SoC должно быть доступно немного ОЗУ, которое не используется BIOS. Логично, что нельзя загружать новый код в область ОЗУ BIOS. Мой выбор пал на использование внутренней HyperRAM FPGA. В примере также используется данный подход, и он, похоже, работает довольно хорошо. Код для добавления этого в SoC выглядит следующим образом:

        # HyperRAM ---------------------------------------------------------------------------------         if not self.integrated_main_ram_size:             # TODO: Use second 32Mbit PSRAM chip.             dq      = platform.request("IO_psram_dq")             rwds    = platform.request("IO_psram_rwds")             reset_n = platform.request("O_psram_reset_n")             cs_n    = platform.request("O_psram_cs_n")             ck      = platform.request("O_psram_ck")             ck_n    = platform.request("O_psram_ck_n")             class HyperRAMPads:                 def __init__(self, n):                     self.clk   = Signal()                     self.rst_n = reset_n[n]                     self.dq    = dq[8*n:8*(n+1)]                     self.cs_n  = cs_n[n]                     self.rwds  = rwds[n]             # FIXME: Issue with upstream HyperRAM core, so the old one is checked in in the repo for now             hyperram_pads = HyperRAMPads(0)             self.comb += ck[0].eq(hyperram_pads.clk)             self.comb += ck_n[0].eq(~hyperram_pads.clk)             self.hyperram = HyperRAM(hyperram_pads)             self.bus.add_slave("main_ram", slave=self.hyperram.bus, region=SoCRegion(origin=self.mem_map["main_ram"], size=4*mB))                      self.add_constant("CONFIG_MAIN_RAM_INIT") # This disables the memory test on the hyperram and saves some boottime

Одной проблемой меньше, однако хотелось бы иметь возможность компилировать свой собственный код, отдельно от репозитория Litex, и при этом использовать их готовые драйверы и прочее. После некоторых экспериментов получился следующий makefile.

Вся магия заключается в директориях сборки и заголовков вверху:

BUILD_DIR=../../build/sipeed_tang_nano_9k SOC_DIR=/usr/local/share/litex/litex/litex/litex/soc/ include $(BUILD_DIR)/software/include/generated/variables.mak include $(SOC_DIR)/software/common.mak

Код в значительной степени основан на демонстрационном приложении: сначала автор его упростил, а затем дополнил новыми периферийными устройствами.

Драйверы периферии

После построения SoC с набором входов/выходов, I2C и прочего, возникает желание подключить периферию! Большинство из них довольно просто использовать, но на самом деле нет никакой документации о том, как это сделать. Лучший способ — посмотреть на код migen и позволить Litex сгенерировать файл со всеми регистрами. Это можно сделать, добавив опцию «-soc-csv». Например:

./sipeed_tang_nano_9k.py --build --soc-csv=soc.csv

сгенерирует файл soc.csv со всеми регистрами внутри. Также доступны опции -soc-json и -soc-svd для генерации файлов в формате JSON и SVD соответственно.

Некоторые файлы заголовков C также генерируются при сборке. В частности файл csr.h, расположенный в директории build/sipeed_tang_nano_9k/software/include/generated/ очень полезен. Для небольших периферийных устройств использование функций из этого файла вполне реализуемо.

Например, функция «gpio_in_read» для чтения состояния GPIO, работает как ожидалось.

Для некоторых периферийных устройств в Litex доступны драйверы. Например, для I2C есть отличный драйвер, способный обрабатывать более одного созданного I2C устройства, потрясающе!

Пора разобраться с использованием прерываний.

Передряги с прерываниями

Задействование прерываний на стороне Litex/FPGA реализовано довольно просто, функция irq.add позаботится обо всем! Например:

        self.gpio = GPIOIn(platform.request("user_btn", 1), with_irq=True)         self.timer1 = Timer()         self.timer2 = Timer()                  # And add the interrupts!         self.irq.add("gpio", use_loc_if_exists=True)         self.irq.add("timer1",  use_loc_if_exists=True)         self.irq.add("timer2",  use_loc_if_exists=True)

Однако как использовать их в программном обеспечении? После просмотра существующего кода, вот здесь был найден обработчик прерываний. Но есть крошечная проблема:

void isr(void) {     __attribute__((unused)) unsigned int irqs;     irqs = irq_pending() & irq_getmask();     if(irqs & (1 << UART_INTERRUPT))         uart_isr(); }

Автор удалил некоторые #define для ясности, таким образом код будет обрабатывать только прерывание UART для стандартного UART! Так что нужно либо изменить этот файл в Litex, либо не использовать библиотеки Litex. Либо сделать небольшое изменение:

// Weak function that can be overriden in own software for any IRQ that is not the uart. // Return true (not zero) if an IRQ was handled, or 0 if not. unsigned int __attribute__((weak)) isr_handler(int irqs);  // Override by default with return 0 unsigned int isr_handler(int irqs) {     return 0; }  ...  void isr(void) {     __attribute__((unused)) unsigned int irqs;     irqs = irq_pending() & irq_getmask();     if(irqs & (1 << UART_INTERRUPT))         uart_isr();     else         if(!isr_handler(irqs))             printf("Unhandled irq!\n"); }

Таким образом, простая функция с атрибутом weak определена вверху. Это означает, что если такая же функция существует где-либо еще, она переопределит weak функцию. Если же ее нет, будет вызвана weak функция.

Это означает, что если произойдет прерывание, которое не является прерыванием UART, будет вызвана функция isr_handler(). Если вы реализуете ее в своем коде, отлично, она будет вызвана и выполнится. В противном случае ничего страшного, будет вызвана функция из этого файла.

В собственном main.c можно просто сделать следующее:

unsigned int isr_handler(int irqs) {         unsigned int irqHandled = 0;     if(irqs & (1 << GPIO_INTERRUPT))     {         GpioInClearPendingInterrupt();         irqHandled = 1;     }         return irqHandled; }

В этом случае, если происходит прерывание GPIO_INTERRUPT, то оно будет обработано с возвратом 1, в противном случае будет возвращен 0, и обработчик прерываний сможет выдать предупреждение 🙂

В рамках демонстрации автор создал программу, которая считывает данные с последовательного порта и может выполнять несколько команд для тестирования I2C, GPIO, прерываний таймера и так далее. Полный код можно найти здесь.

Теперь осталась только одна вещь, которую надо опробовать. Создание собственного периферийного устройства!

Создание собственного периферийного устройства

В целях освоения создания периферийного устройства, автор решил реализовать простой периферийный модуль PWM. Что-то простое, что генерирует сигнал PWM с заданной частотой и коэффициентом заполнения. Внутри он должен иметь счетчик, и когда счетчик ниже или выше определенного значения, он будет переключать выход для управления коэффициентом заполнения PWM.

Он должен иметь несколько регистров:

  1. Регистры включения, чтобы включать/выключать периферийное устройство PWM

  2. Регистры делителя, чтобы иметь возможность создавать PWM-сигналы с более низкой частотой

  3. Регистры максимального счета, которые должны считать до этого значения, а затем сбрасывать свой внутренний счетчик

  4. Регистры коэффициента заполнения: если счетчик ниже этого значения, состояние выхода должно быть низким, в противном случае — высоким.

Все это выглядит вполне выполнимо, и хотя Migen реализован иначе, чем Verilog или VHDL, он позволяет писать компактный код благодаря всем возможностям Litex.

Создание регистра и подключение его к процессору осуществляется очень просто:

from migen import *  from litex.soc.interconnect.csr import * from litex.gen import *  class PwmModule(LiteXModule):     def __init__(self, pad, clock_domain="sys"):         self.divider = CSRStorage(size=16, reset=0, description="Clock divider")

Несколько строк, и простое периферийное устройство готово! Это лишь один 16-битный регистр, однако это удивительно! Не нужно беспокоиться о шинах процессора или чем-то подобном. CSRStorage не самый быстрый метод, но для периферийного устройства, такого как PWM, этого вполне достаточно.

Итак, давайте быстро создадим это периферийное устройство!

from migen import *  from litex.soc.interconnect.csr import * from litex.gen import *  class PwmModule(LiteXModule):     def __init__(self, pad, clock_domain="sys"):                  self.enable = CSRStorage(size=1, reset=0, description="Enable the PWM peripheral")         self.divider = CSRStorage(size=16, reset=0, description="Clock divider")         self.maxCount = CSRStorage(size=16, reset=0, description="Max count for the PWM counter")         self.dutycycle = CSRStorage(size=16, reset=0, description="IO dutycycle value")                  divcounter = Signal(16, reset=0)         pwmcounter = Signal(16, reset=0)                  sync = getattr(self.sync, clock_domain)                  sync += [             If(self.enable.storage,                 divcounter.eq(divcounter + 1),                     If(divcounter >= self.divider.storage,                         divcounter.eq(0),                         pwmcounter.eq(pwmcounter + 1),                         If(pwmcounter >= self.maxCount.storage,                             pwmcounter.eq(0),                         ),                     )                 )             ]                              sync += pad.eq(self.enable.storage & (pwmcounter < self.dutycycle.storage))

Несколько дополнительных регистров и несколько внутренних счетчиков для деления тактового сигнала и счетчика PWM. Полное и работоспособное периферийное устройство всего чуть более 30 строк, потрясающе!

А чтобы использовать это периферийное устройство в SoC, нужна всего одна строка:

self.pwm0 = PwmModule(platform.request("pwm0"))

На стороне программного обеспечения нужно инициализировать всего несколько регистров:

    pwm0_divider_write(10);     pwm0_maxCount_write(1000);     pwm0_toggle_write(400);     pwm0_enable_write(1);

Полный код для SoC можно найти здесь.

Заключение

Это было весело! От нуля до FPGA SoC с некоторыми пользовательскими периферийными устройствами — потрясающе. И все это с довольно небольшим количеством строк кода. Определенно Litex произвел впечатление!


ссылка на оригинал статьи https://habr.com/ru/articles/852006/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *