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