Наводим красоту в коде для ПЛИС 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. Как и в случае с этим блоком, там сам механизм прост, больше сил уйдёт на организацию проверки работоспособности.

Читайте так же:

  • Facebook начал ограничивать охват аккаунтов, распространяющих дезинформациюFacebook начал ограничивать охват аккаунтов, распространяющих дезинформацию Facebook заявил, что будет ограничивать распространение всех постов от тех аккаунтов. Которые регулярно публикуют дезинформацию. Обновление затронет те учетные записи, которые были помечены одним из партнеров по фактчекингу. С которыми сотрудничает соцсеть. Хотя Facebook уже понижает […]
  • Написать контент для сайтаНаписать контент для сайта Как написать контент для веб-сайта-один из самых больших вопросов, с которыми сталкиваются новые маркетологи контента. Вы можете смотреть на свои заметки и гадать, как вы собираетесь превратить все это в эффективную, красивую веб-копию. Что ж, вы пришли в нужное место, чтобы научиться […]
  • Microsoft добавит в PowerToys ещё одну утилиту для мышиMicrosoft добавит в PowerToys ещё одну утилиту для мыши Ведущий специалист PowerToys Клинт Руткас (Clint Rutkas) рассказал в своем Twitter-аккаунте про новую утилиту для мыши. Инструмент поможет людям с ограниченными возможностями находить курсор на экране и войдет в предстоящую сборку PowerToys. Выход «свежей» версии назначен на конец января […]
  • На восстановление доступа в Интернет в Королевстве Тонга уйдут неделиНа восстановление доступа в Интернет в Королевстве Тонга уйдут недели Королевство Тонга в южной части Тихого океана оказалось отрезанным от Интернета после извержения вулкана. Государство соединял с миром единственный подводный интернет-кабель.Извержение вулкана на ТонгеКак и многие островные государства, Тонга подключается к Интернету при помощи одного […]