Как стать автором
Обновить

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

Время на прочтение12 мин
Количество просмотров2.1K
В прошлых двух статьях мы сделали и испытали проект, в основе которого лежит система на базе 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. Как и в случае с этим блоком, там сам механизм прост, больше сил уйдёт на организацию проверки работоспособности.
Теги:
Хабы:
Всего голосов 14: ↑12 и ↓2+15
Комментарии1

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань