Наводим красоту в коде для ПЛИС Lattice, построенном на базе пакета LiteX

от автора

В прошлых двух статьях мы сделали и испытали проект, в основе которого лежит система на базе LiteX, а наши модули были написаны на языке Verilog. На протяжении всего повествования я неустанно повторял: «У нас очень много нового материала, не будем отвлекаться на рюшечки, потом разберёмся». Как правило, нет ничего более постоянного, чем временное, но раз тема оказалась интересная, то в этот раз давайте мы наведём красоту в нашем проекте.

Сегодня мы поменяем принцип описания ножек, чтобы не пришлось прыгать по трём справочникам сразу, разместим несколько полей в одном регистре CSR, добавим автодокументирование к регистрам CSR (Command-Status Register) и, наконец, добавим к этим регистрам статус, а то до сих пор мы пробовали играть только в командные регистры. Приступаем.

Важное замечание

Данная статья содержит сведения, украшающие код, написанный в двух предыдущих: первая и вторая.

Если не прочитать предыдущие статьи, рука сама потянется поставить минус с формулировкой «Ничего не понял после прочтения». Желательно сначала ознакомиться с базовым материалом, описанным ранее. Если совсем точно, то ознакомиться надо с мелкими проблемами, которые там были оставлены на потом.

Заменяем список ножек на словарь

В прошлый раз, чтобы понять, на какие ножки были переданы сигналы, описанные таким способом:

    touch_pins = [            soc.platform.request("gpio", 0),            soc.platform.request("gpio", 1),            soc.platform.request("gpio", 2),            soc.platform.request("gpio", 3)        ]

Нам пришлось идти в класс и разглядывать, как же ножки там используются:

То же самое текстом.

        self.specials += Instance(             'gpu',             i_clk=clk,             i_x0=self.x0.storage,             i_x1=self.x1.storage,             i_y0=self.y0.storage,             i_y1=self.y1.storage,             o_hsync=pins[2],             o_vsync=pins[3],             o_color=pins[0]         ) 

А если проект большой и разбросан по нескольким файлам? А если он написан год назад? А если другим человеком, который сейчас недоступен для расспросов? Надо уменьшить количество прыжков при поиске. К счастью, язык Питон даёт нам средства для этого! Передадим перечень ножек не в виде списка, а в виде словаря. Вот так:

То же самое текстом.

    touch_pins = {            'Color' : soc.platform.request("gpio", 0),            'Zero' : soc.platform.request("gpio", 1),            'HSync' : soc.platform.request("gpio", 2),            'VSync' : soc.platform.request("gpio", 3)        } 

А возьмём – так:

То же самое текстом.

class GPU(Module, AutoCSR):     def __init__(self, pins, clk):         self.x0 = CSRStorage(16, reset=100)         self.x1 = CSRStorage(16, reset=150)         self.y0 = CSRStorage(16, reset=100)         self.y1 = CSRStorage(16, reset=200)         self.comb += [             pins['Zero'].eq(0),         ]         self.specials += Instance(             'gpu',             i_clk=clk,             i_x0=self.x0.storage,             i_x1=self.x1.storage,             i_y0=self.y0.storage,             i_y1=self.y1.storage,             o_hsync=pins['HSync'],             o_vsync=pins['VSync'],             o_color=pins['Color']         ) 

Ну вот. С точки зрения компилятора, всё то же самое, но читаемость резко возросла. У нас есть точный справочник, не надо каждый раз возить пальцем по коду и выписывать всё на бумажку.

Хотя, даже лучше, что мы не сразу взялись за такой вариант. Дело в том, что сначала я нашёл пример именно в таком формате… И запутался. Где HSync – это просто ключевое слово для поиска в Питоновском словаре, а где – имя сигнала. Пока мы работали через индексы в списке одноимённых сущностей было меньше, а сейчас мы уже знаем теорию, так что уже ничего не боимся. Теперь нам нужна красота и отсутствие путаницы при подключении нашего устройства к периферии.

Поля в регистрах команд

Следующая тема, требовавшая улучшения – это размерность полей в регистрах команд. Мы добавляли новые 16-битные поля, под каждый регистр нам создавали своё 32-битное слово. Вот так это выглядело на выходе скрипта из прошлой статьи:

csr_register,gpu_x0,0x00000000,1,rw csr_register,gpu_x1,0x00000004,1,rw csr_register,gpu_y0,0x00000008,1,rw csr_register,gpu_y1,0x0000000c,1,rw 

Регистры имели адреса 0, 4, 8 и 0x0c. Хорошо, что мы добавляли шестнадцатибитные поля. А если бы по битику? Должно же быть какое-то средство для решения проблемы. И оно есть!

Давайте я сначала расскажу, как нашёл его. Дело в том, что я не могу найти никакого путного учебника, который бы помог мне систематизировать знания. На форумах общаются явно специалисты, но все они пишут какими-то обрывками фраз. Эти обрывки понятны только им. Поэтому в конце 2021 года найти хорошую литературу по Litex вряд ли удастся. Надеюсь, в будущем это исправится. Но нам некогда ждать будущего! Поэтому я сделал просто. Вот есть у нас в коде строка:

        self.x0 = CSRStorage(16, reset=100) 

Наводим на неё курсор в надежде на удачу… И удача нас не обманула!

Какая хорошая подсказка! Из неё уже можно выдернуть какую-то информацию по использованию класса CSRStorage… Но сейчас нас интересует не это. Нас интересуют классы, описанные где-то рядом. Наверняка рядом есть класс, который нам поможет! Выбираем:

И осматриваемся. Ура! Чуть выше мы видим вот такое дело:

class CSRField(Signal):     """CSR Field.      Parameters / Attributes     -----------------------     name : string         Name of the CSR field.      size : int         Size of the CSR field in bits.      offset : int (optional)         Offset of the CSR field on the CSR register in bits. … 

Очень похоже на то, что нам нужно! Зная это, ищем примеры, содержащие слово CSRField… Вот очень показательный пример с кучей разных способов объявления полей:

        self.iv_2 = CSRStorage(fields=[             CSRField("iv_2", size=32, description="iv")         ])         self.iv_3 = CSRStorage(fields=[             CSRField("iv_3", size=32, description="iv")         ])          self.ctrl = CSRStorage(fields=[             CSRField("mode", size=3, description="set cipher mode. Illegal values mapped to `AES_ECB`", values=[                 ("001", "AES_ECB"),                 ("010", "AES_CBC"),                 ("100", "AES_CTR"),             ]),             CSRField("key_len", size=3, description="length of the aes block. Illegal values mapped to `AES128`", values=[                     ("001", "AES128"),                     ("010", "AES192"),                     ("100", "AES256"),             ]),             CSRField("manual_operation", size=1, description="If `1`, operation starts when `trigger` bit `start` is written, otherwise automatically on data and IV ready"),             CSRField("operation", size=1, description="Sets encrypt/decrypt operation. `0` = encrypt, `1` = decrypt"),         ])         self.status = CSRStatus(fields=[             CSRField("idle", size=1, description="Core idle", reset=1),             CSRField("stall", size=1, description="Core stall"),             CSRField("output_valid", size=1, description="Data output valid"),             CSRField("input_ready", size=1, description="Input value has been latched and it is OK to update to a new value", reset=1),             CSRField("operation_rbk", size=1, description="Operation readback"),             CSRField("mode_rbk", size=3, description="Actual mode selected by hardware readback"),             CSRField("key_len_rbk", size=3, description="Actual key length selected by the hardware readback"),             CSRField("manual_operation_rbk", size=1, description="Manual operation readback")         ]) 

По образу и подобию переписываем свой класс GPU так:

То же самое текстом.

from litex.soc.interconnect.csr import AutoCSR, CSRStatus, CSRStorage, CSRField class GPU(Module, AutoCSR):     def __init__(self, pins, clk):         self.x = CSRStorage(fields=[             CSRField("x0", size=16, reset=100),             CSRField("x1", size=16, reset=150),             ])         self.y = CSRStorage(fields=[             CSRField("y0", size=16, reset=100),             CSRField("y1", size=16, reset=200),             ])         self.comb += [             pins['Zero'].eq(0),         ]         self.specials += Instance(             'gpu',             i_clk=clk,             i_x0=self.x.fields.x0,             i_x1=self.x.fields.x1,             i_y0=self.y.fields.y0,             i_y1=self.y.fields.y1,             o_hsync=pins['HSync'],             o_vsync=pins['VSync'],             o_color=pins['Color']         ) 

Прогоняем получившийся скрипт, осматриваем результирующий Verilog код. Вот так в нём выглядит место включения нашего Верилоговского модуля:

gpu gpu( .clk(basesoc_crg_clkin), .x0(x0), .x1(x1), .y0(y0), .y1(y1), .color(gpio0), .hsync(gpio2), .vsync(gpio3) ); 

Ага, есть какие-то поля x0, x1, y0, y1. Хорошо. А куда они ведут? Давайте отследим иксы.

wire [15:0] x0; wire [15:0] x1; 

assign x0 = x_storage[15:0]; assign x1 = x_storage[31:16]; 

Вроде, всё верно. А что со значениями по умолчанию? Тут целый детектив. Вот строка:

reg  [31:0] x_storage = 32'd9830500; 

В шестнадцатеричном виде это 0x00960064. Раскладываем на шестнадцатибитные слова – получаем 0x0096 для X1 и 0x0064 для X0. Снова переводим в десятичный вид – получаем 150 и 100. Всё совпадает с тем, что мы попросили.

Прекрасно! Код нам сформировали верный! А что насчёт справочника? Смотрим файл csr.csv. Напомню, в материалах для прошлой статьи, там были такие строки:

csr_register,gpu_x0,0x00000000,1,rw csr_register,gpu_x1,0x00000004,1,rw csr_register,gpu_y0,0x00000008,1,rw csr_register,gpu_y1,0x0000000c,1,rw 

Теперь соответствующий участок выглядит так:

csr_register,gpu_x,0x00000000,1,rw csr_register,gpu_y,0x00000004,1,rw 

Мы добились того, чего хотели с точки зрения экономии адресного пространства, у нас шестнадцатибитные поля плотно упакованы в тридцатидвухбитные регистры, но через несколько месяцев нам будет очень трудно вспомнить, где в них поля x0, y0, x1 и y1! Некие намётки на них мы можем найти в файле \build\colorlight_5a_75b\software\include\generated\csr.h.

Смотреть код.

#define CSR_GPU_Y_ADDR (CSR_BASE + 0x4L) #define CSR_GPU_Y_SIZE 1 static inline uint32_t gpu_y_read(void) { return csr_read_simple(CSR_BASE + 0x4L); } static inline void gpu_y_write(uint32_t v) { csr_write_simple(v, CSR_BASE + 0x4L); } #define CSR_GPU_Y_Y0_OFFSET 0 #define CSR_GPU_Y_Y0_SIZE 16 static inline uint32_t gpu_y_y0_extract(uint32_t oldword) { uint32_t mask = ((1 << 16)-1); return ( (oldword >> 0) & mask ); } static inline uint32_t gpu_y_y0_read(void) { uint32_t word = gpu_y_read(); return gpu_y_y0_extract(word); } static inline uint32_t gpu_y_y0_replace(uint32_t oldword, uint32_t plain_value) { uint32_t mask = ((1 << 16)-1); return (oldword & (~(mask << 0))) | (mask & plain_value)<< 0 ; } static inline void gpu_y_y0_write(uint32_t plain_value) { uint32_t oldword = gpu_y_read(); uint32_t newword = gpu_y_y0_replace(oldword, plain_value); gpu_y_write(newword); } #define CSR_GPU_Y_Y1_OFFSET 16 #define CSR_GPU_Y_Y1_SIZE 16 static inline uint32_t gpu_y_y1_extract(uint32_t oldword) { uint32_t mask = ((1 << 16)-1); return ( (oldword >> 16) & mask ); } static inline uint32_t gpu_y_y1_read(void) { uint32_t word = gpu_y_read(); return gpu_y_y1_extract(word); } static inline uint32_t gpu_y_y1_replace(uint32_t oldword, uint32_t plain_value) { uint32_t mask = ((1 << 16)-1); return (oldword & (~(mask << 16))) | (mask & plain_value)<< 16 ; } static inline void gpu_y_y1_write(uint32_t plain_value) { uint32_t oldword = gpu_y_read(); uint32_t newword = gpu_y_y1_replace(oldword, plain_value); gpu_y_write(newword); } 

Тут проглядывают нужные нам константы в чистом виде… Всё можно даже вывести из кода… Но я специально не стал раскрашивать код, потому что это сейчас я тут с красками сижу, а при реальной работе, рыться в нём придётся слишком долго. А когда регистров много, а времени с момента разработки прошло ещё больше, нам придётся рыться долго и вдумчиво. Поэтому давайте потренируемся делать самодокументирующийся код.

Делаем самодокументирующийся код

Подготовка

Вдохновение мы будем черпать тут (ну, хоть что-то хорошо описано):
SoC Documentation · enjoy-digital/litex Wiki (github.com).

Первое, что там требуют сделать – это установить специальный пакет:
pip3 install sphinxcontrib-wavedrom sphinx

Правда, у меня под Windows он не заработал… Но может, под Линуксом будет лучше…

Теперь к основному коду нашего скрипта добавляем в начало:

from litex.soc.doc import generate_docs, generate_svd 

а уже когда система построена, просим сгенерить нам документацию. Я специально добавлю пару реперных строк в начало, чтобы было видно, куда добавлены новые строки:

    builder = Builder(soc, **builder_argdict(args))     builder.build(**trellis_argdict(args), run=args.build)      generate_docs(soc, "build/documentation")     generate_svd(soc, "build") 

Всё! Но чтобы эту документацию создавать, нужным справочные материалы. Чтобы их добавить, идём в многострадальный класс GPU.

Доработка класса, чтобы он стал самодокументирующимся

Перво-наперво добавляем зависимостей:

from litex.soc.integration.doc import AutoDoc, ModuleDoc 

Наш класс GPU уже унаследован от классов Module и AutoCSR. Добавим ему ещё предка AutoDoc:

И вот, всем сущностям CSR (как регистрам, так и их полям) мы теперь можем добавить свойство description. Получаем такую красоту:

То же самое текстом.

class GPU(Module, AutoCSR, AutoDoc):     def __init__(self, pins, clk):         self.x = CSRStorage(             description="X Coordinates",             fields=[                 CSRField("x0", size=16, reset=100,description="Left"),                 CSRField("x1", size=16, reset=150,description="Right"),             ]             )         self.y = CSRStorage(             description="Y Coordinates",             fields=[             CSRField("y0", size=16, reset=100,description="Top"),             CSRField("y1", size=16, reset=200,description="Bottom"),             ])         self.comb += [             pins['Zero'].eq(0),         ]         self.specials += Instance(             'gpu',             i_clk=clk,             i_x0=self.x.fields.x0,             i_x1=self.x.fields.x1,             i_y0=self.y.fields.y0,             i_y1=self.y.fields.y1,             o_hsync=pins['HSync'],             o_vsync=pins['VSync'],             o_color=pins['Color']         ) 

Анализируем автоматически сформированную документацию

Запускаем скрипт, смотрим на сформированные вещи. Первое – это файл soc.svd. Я не буду его показывать. Там скучный XML. Но этот XML – какой надо XML! Именно его надо подсовывать отладчикам (хоть Кейлу, хоть Эклипсе, хоть ещё кому-то) для того, чтобы они начали декодировать всю системную информацию. Было дело, я для своей ARM-системы на базе Cyclone V SoC такое ручками собирал. Было грустно. А тут – полностью автоматическое формирование! Правда, для ручного разбора это не так интересно, поэтому сам факт наличия файла я упомянул, а показывать его содержимое даже не стану.

Лучше осмотрим содержимое каталога documentation:

По ссылке выше рассказывается, как собрать из этих материалов настоящий html-файл! Но, к сожалению, под Windows это приведёт к такому результату:

Судя по результатам, выданным Гуглем, у пользователей MAC OS ситуация будет не лучше. Возможно, в комментариях кто-то подскажет путь решения, так как в Гугле я ничего путного не нашёл. Но в целом, если посмотреть содержимое файлов обычным текстовым редактором, можно найти всё, что нужно и так. Заглянем в файл gpu.rst.

Вот общее описание регистров:

Вот поля первого из них:

В общем, разобраться можно. Отлично! Теперь у нас есть справочники, которые сами будут актуализироваться на протяжении эволюции проекта!

Обратите внимание также на базовый класс ModuleDoc. В статье он не рассматривается, но с его помощью можно добавлять в систему описание не только регистров и их полей, но и целых модулей. Детальное описание – по ссылке выше.

Регистры статуса

Ну, и чтобы закрыть большую тему регистров команд и статуса, нам надо рассмотреть, собственно, те самые регистры статуса. Какой бы статус нам добывать? У нас VGA-выход… Давайте будем возвращать шестнадцатибитный номер текущего кадра. При частоте 60 кадров в секунду он будет переполняться раз примерно в 1000 секунд. То есть, его хватит минут на 15.

Такой регистр делается просто, а выглядит эффектно. Доработаем файл gpu.v так (в заголовке новая строка – последняя, плюс показаны новые строки самого модуля, остальное – старое):

module gpu(     input        clk,      output       hsync,      output       vsync,      output       color,      input signed [15:0] x0,      input signed [15:0] x1,      input signed [15:0] y0,      input signed [15:0] y1,     output reg [15:0] curFrame = 0 ); …    reg vsync_d;   always @(posedge clk)   begin      vsync_d <= vsync;      if ((!vsync_d) & (vsync))      begin         curFrame <= curFrame + 1;        end   end 

Как нам считать порт curFrame через шину Wishbone? Мы уже опытные, мы уже сегодня наводились на CSRStorage и переходили в соответствующий класс, чтобы узнать, какие ещё полезные вещи там имеются. Давайте повторим этот фокус ещё разок. Вот то, что нам подойдёт из того файла, который откроется нам для осмотра:

class CSRStatus(_CompoundCSR):     """Status Register.      The ``CSRStatus`` class is meant to be used as a status register that is read-only from the CPU.      The user design is expected to drive its ``status`` signal. … 

Идём в наш класс и, основываясь на накопленном опыте, твёрдой рукой добавляем:

То же самое текстом.

class GPU(Module, AutoCSR, AutoDoc):     def __init__(self, pins, clk):         self.x = CSRStorage(             description="X Coordinates",             fields=[                 CSRField("x0", size=16, reset=100,description="Left"),                 CSRField("x1", size=16, reset=150,description="Right"),             ]             )         self.y = CSRStorage(             description="Y Coordinates",             fields=[             CSRField("y0", size=16, reset=100,description="Top"),             CSRField("y1", size=16, reset=200,description="Bottom"),             ])         self.frame = CSRStatus (             description="Current Video Frame Number",             size=16             )         self.comb += [             pins['Zero'].eq(0),         ]         self.specials += Instance(             'gpu',             i_clk=clk,             i_x0=self.x.fields.x0,             i_x1=self.x.fields.x1,             i_y0=self.y.fields.y0,             i_y1=self.y.fields.y1,             o_curFrame = self.frame.status,             o_hsync=pins['HSync'],             o_vsync=pins['VSync'],             o_color=pins['Color']         ) 

А не так это и страшно, когда информация наваливается не снежным комом, а последовательно, правда? Бегло проверяем, что нам сгенерилось в Верилоге. Вот включение нашего GPU:

То же самое текстом.

gpu gpu( .clk(basesoc_crg_clkin), .x0(x0), .x1(x1), .y0(y0), .y1(y1), .color(gpio0), .curFrame(frame_status), .hsync(gpio2), .vsync(gpio3) ); 

Неплохо. И куда это уходит?

То же самое текстом.

assign builder_basesoc_csrbank2_frame_w = frame_status[15:0]; … if (builder_basesoc_csrbank2_sel) begin case (builder_basesoc_interface2_adr[8:0]) 1'd0: begin builder_basesoc_interface2_dat_r <= builder_basesoc_csrbank2_x0_w; end 1'd1: begin builder_basesoc_interface2_dat_r <= builder_basesoc_csrbank2_y0_w; end 2'd2: begin builder_basesoc_interface2_dat_r <= builder_basesoc_csrbank2_frame_w; end endcase end 

Ну, что-то такое, правдоподобное. Какие-то мультиплексоры и какая-то шина данных. Значит, можно проверять на практике.

Давайте напишем скрипт, который постоянно принимает это значение. Тут-то вся правда и откроется. Запуск скрипта – не самое тривиальное дело, но в прошлой статье мы это уже делали. Всегда можно открыть её и освежить методику в памяти. Итак, делаем такой скрипт:

#!/usr/bin/env python3  import time  from litex import RemoteClient  wb = RemoteClient() wb.open()  # # #  for i in range(1000):     print (wb.regs.gpu_frame.read())  wb.close() 

Вот результат его работы:

Что-то тикает, но то ли? Всё в порядке. В первой версии «прошивки» я нечаянно считал не кадровые, а строчные импульсы, там было веселей:

По скорости переполнения 16-битного поля я и догадался, что что-то идёт не так. Так что всё верно. Это мы читаем тот счётчик, который передаётся.

Заключение

Мы познакомились с методиками улучшения читаемости кода, сделанного на базе LiteX. Благодаря этому, код, переданный другому разработчику (да и просто написанный год назад) не потеряет своей понятности. Мы освоили работу не только с регистрами управления блока CSR (с ними мы уже две статьи, как знакомы), но и с регистрами статуса. Кроме того, мы теперь знаем, где можно осмотреться на предмет более серьёзного использования механизма CSR.

Код, разработанный для данной статьи, можно найти тут.

Но CSR – это только одна из вещей, которые мы можем вывести из системы, построенной на базе LiteX в свои Верилоговские модули. Следующий (но не последний) уровень для вывода наружу – целая шина. Например, Wishbone. Если интерес к теме ещё не потерян (рейтинг покажет), то в следующей статье мы рассмотрим, как подключить Verilog код с шиной Wishbone в режиме Slave. Ну а дальше – уже заняться Wishbone в режиме Master. Как и в случае с этим блоком, там сам механизм прост, больше сил уйдёт на организацию проверки работоспособности.


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


Комментарии

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

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