Наводим красоту в коде для ПЛИС 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 и 0×0c. Хорошо, что мы добавляли шестнадцатибитные поля. А если бы по битику? Должно же быть какое-то средство для решения проблемы. И оно есть!
Давайте я сначала расскажу, как нашёл его. Дело в том, что я не могу найти никакого путного учебника, который бы помог мне систематизировать знания. На форумах общаются явно специалисты, но все они пишут какими-то обрывками фраз. Эти обрывки понятны только им. Поэтому в конце 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;
В шестнадцатеричном виде это 0×00960064. Раскладываем на шестнадцатибитные слова — получаем 0×0096 для X1 и 0×0064 для 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. Как и в случае с этим блоком, там сам механизм прост, больше сил уйдёт на организацию проверки работоспособности.