Всем привет, меня зовут Артём Кашканов, я ведущий инженер по программно-аппаратному дизайну в компании YADRO. Я занимаюсь разработкой аппаратных блоков, данные с которых будут использоваться в инструментах анализа производительности ПО. В процессе создания мне приходится взаимодействовать с архитекторами, разработчиками RTL, верификаторами, разработчиками драйверов и техническими писателями. И хотя основной документ для коммуникации — это микроархитектурная спецификация, отдельная ее глава под названием «регистровая карта» — самая больная тема.

Упрощенная структурная схема IP блока. Основная логика с щепоткой контрольно-статусных регистров, дополненная внешними интерфейсами.
Упрощенная структурная схема IP блока. Основная логика с щепоткой контрольно-статусных регистров, дополненная внешними интерфейсами.

Как правило, цифровая схема реализует не только саму логику работы с данными, но и предоставляет пользователю возможность этой логикой управлять. Так в блоке появляются контрольные и статусные регистры, к которым нужно обеспечить подключение со стороны системного интерконнекта.

Теперь представим: архитектор описывает эти регистры в каком-нибудь Markdown, инженер, глядя на них, пишет Verilog, а разработчик драйвера вручную создает заголовочные файлы на C. Если повезет, все эти компоненты даже будут соответствовать друг другу. Один раз... Решить проблему согласованности этих артефактов можно с помощью языка описания регистров SystemRDL.

SystemRDL, поддерживаемый консорциумом Accellera, был разработан специально для описания и реализации широкого спектра регистров состояния и управления. Используя SystemRDL, разработчики могут автоматически генерировать и синхронизировать представления регистров для спецификации, проектирования оборудования, разработки программного обеспечения, проверки и документирования.

С его помощью можно описать все регистровые поля, определить аппаратный и программный доступ к ним, добавить различные модификаторы и сигналы. А потом преобразовать их и в SystemVerilog-файл, и в готовую документацию, и в UVM-сценарии, и во многое другое.

Единое описание - множество представлений.
Единое описание - множество представлений.

Это сразу закрывает потребности всех конечных пользователей:

  • архитекторов, которые разрабатывают функционал блока и способы его управления;

  • RTL-инженеров, которым достаточно написать основную логику и подключить ее к готовому блоку регистров;

  • программистов,которые получают готовые C-заголовки, дабы драйвер знал, какие поля существуют в блоке;

  • технических писателей, заскриптующих генерацию готовой документации по этому описанию.

Что особенно важно, с SystemRDL могут работать люди, не знакомые с цифровой разработкой. Это упрощает взаимодействие различных команд.

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

PeakRDL

Однако одного языка нам недостаточно. Нужен инструмент, который превратит описание регистров как минимум в подключаемый модуль на SystemVerilog. Существует ряд инструментов для работы с SystemRDL:

  • коммерческие, типа Agnisys, Semifore's CSR Compiler или Magillem,

  • и open source — например, PeakRDL, ORDT, RgGen и другие.

Инструмент

Лицензия

HDL

C-заголовки

UVM

IP-XACT (IEEE1685)

Doc

Open Register Designer Tool

Apache 2.0

SystemVerilog, Verilog

C, C++, Python

+

JSpec

PeakRDL

GPL3

SystemVerilog

C, C++

+

+

HTML

RgGen

GPL3

SystemVerilog, Verilog, VHDL, Veryl

C

+

Markdown

ORDT - инструмент от компании Juniper Networks, написан на языке java, принимает на вход файлы в форматах SystemRDL или JSpec и генерирует RTL-код на SystemVerilog/Verilog, модели UVM, C++/Python-модели и заголовочные файлы C. Правда в официальном репозитории уже три года как висит плашка "this repo is no longer actively monitored or maintained."

RgGen - проект на Ruby, единственный предлагающий генерацию кода в VHDL и Veryl. Имеет модульную структуру и тот же SystemRDL требует плагина rggen-systemrdl

PeakRDL, на мой взгляд, самый интересный, так как написан на Python, и также позволяет писать собственные плагины, расширяющие базовый функционал компилятора. Еще он единственный, умеющий работать с промышленным стандартом IP-XACT. Устанавливается тул из PyPI-репозитория с помощью команды:

python3 -m pip install peakrdl
Пайплайн работы с PeakRDL
Пайплайн работы с PeakRDL

Синтаксис языка

Описание регистрового поля имеет иерархическую структуру: адресное пространство -> регистровый файл -> регистр -> поле в регистре.

Адресное пространство

Адресное пространство — это набор регистров, регистровых полей или вложенных адресных пространств, имеющий свой адрес и границы. В одном файле может быть несколько адресных пространств, но рекомендую держать строго одно, так как по умолчанию PeakRDL скомпилирует только первое:

addrmap map_name_t{
 name = "какое-то адресное пространство на сколько-то там регистров"
 //пропишем настройки по умолчанию:
 default regwidth = 8;    //ширина регистра
 default accesswidth = 8; //ширина интерфейсной шины
 default sw = rw;         //полный доступ по стороны интерфейса
 default hw = r;         //со стороны устройства - доступ только на чтение
 regfile_t a{};          //инстанс какого-то поля с типом regfile_t
 reg_t b{};              //инстанс какого-то регистра с типом reg_t
} map_name_inst @0x100;  //базовый адрес

При этом стандарт позволяет как сразу описывать новый инстанс пространства/файла/регистра/поля, так и определить его тип и структуру, а потом сделать множественное инстанцирование. Название до фигурных скобок работает как typedef, а после скобок — как имя инстанса.

Регистровый файл

Регистровый файл — это логическая группировка нескольких регистров, которой можно дать свои свойства и определенное место в адресном пространстве. Например, если в устройстве имеется некоторое количество одинаковых подблоков и нужно обеспечить выровненный доступ к ним. Для этого мы один раз определяем регистровый файл, а потом несколько раз его инстанцируем, указывая необходимый адрес или выравнивание:

regfile new_regfile_t{
  	name = "регистровый файл подблока"
  //предположим, что структура этих регистров описана ранее
  	reg1_t a;
    reg2_t b;
  }; 

  new_regfile_t block1 @0x10; //инстанцируем файл по адресу 0x10
  new_regfile_t block2 += 0x20; //еще одну копию - со сдвигом на 0x20 к предыдущему
  new_regfile_t block2[5] + 0x30; //и еще пяток копий со сдвигом на 0x30 к каждому и предыдущему

Регистр и его поля

Поле — самая низкоуровневая структура в языке SystemRDL. К полю осуществляется доступ со стороны устройства. Полем может являться как триггер, так и просто провод. Регистром же называется набор из одного или нескольких полей, которые доступны через интерфейс по определенному адресу.

reg reg1_t{
 name = "какой-то регистр на пару полей"
 field {
  name = "какое-то однобитовое поле"
 } a = 0; //без четкой позиции в регистре, со значением по умолчанию
  
 field {
  name = "какое-то 4-битовое поле"
 } b[4:1] = 0xf; //с заданной позицией в регистре и значением по умолчанию
  
 field {
  singlepulse; //после записи бит будет активен только один такт времени и автоматически сбросится
 }c;
  
 field {
  rclr; //а это поле будет очищено при чтении
 } d;
  
field {  
  swacc; //а это поле предоставит строб о том, что его «потрогали» — прочитали или записали
  swmod; //а это поле явно скажет, что была запись в регистр со стороны софта
 } e;
};

reg reg2_t {
   regwidth = 16; //регистр большой, а шина была объявлена 8 бит.
   buffer_reads = true; //обеспечим к нему атомарный доступ на чтение 
   buffer_writes = true; //и на запись
}reg @0x10;

В документации SystemRDL представлено большое количество различных свойств регистров и полей - можно реализовать функционал счетчика с сигналами инкремента, декремента, сброса и переполнения, можно активировать функционал прерываний, проверку чётности и многое другое. Отельного внимания заслуживают свойства с сайд-эффектами, типа Clear on read, Set on read, Write one to clear, Write one to clear и другие. Из названия понятно что произойдет с полем при той или иной манипуляции со стороны софта.

User-Defined Properties

Buffer_reads и buffer_writes — это по стандарту SystemRDL так называемые User-Defined Properties (UDP). Каждый компилятор предоставляет свой набор свойств, расширяющий возможности генерации: например, у Agnisys более 400 UDP. С их помощью можно активировать переход через клоковые домены (clock-domain crossing), агрегировать блоки, кастомизировать интерфейсы к стандартным шинам, отключать блоки для уменьшения потребления, добавлять функции безопасности — например, коды четности и исправления ошибок (ECC), проверки избыточности (CRC) и многое другое.

У PeakRDL этих параметров всего шесть: buffer_reads, buffer_writes, rbuffer_trigger, wbuffer_trigger, rd_swacc, wr_swacc.

Buffer_reads и buffer_writes используют, если требуемая ширина регистра больше ширины шины данных: например, при 32-битовой APB-шине у нас 64-битовый регистр. Добавление этих UDP обеспечивает атомарное чтение или запись всего регистра разом. При чтении мы должны сначала запросить содержимое младшей части, при этом старшая часть сохранится в теневой буфер. При следующем чтении старшей части мы получим консистентные части единого целого. Запись аналогична: сначала пишем младшую часть — она сохранится в теневой буфер и будет ждать записи старшей части. Как только это произойдет, регистр будет обновлен целиком.

Если вариант с младшей частью большого регистра не подходит, то можно использовать rbuffer_trigger, wbuffer_trigger и сохранять данные из регистра в теневой буфер от какого-то иного внешнего события. Допустим, в дизайне есть несколько разных регистров, но вы хотите получить слепок их состояния. Тогда с опцией reg2->rbuffer_trigger=reg1 обращение к одному определенному регистру приведет к теневому копированию второго регистра:

status_reg status1;
status_reg status2;
status2->buffer_reads = true;
/*чтение регистра status1 со стороны интерфейса вызовет 
сохранение значения регистра status2 в промежуточном буфере*/
status2->rbuffer_trigger = status1;

Особого внимания заслуживают rd_swacc и wr_swacc. Выше в коде упомянут строб swacc, но получив его, мы не поймем, был ли регистр прочитан или записан. А это важно, если поле имело особое свойство — например, Write-To-Clear. rd_swacc и wr_swacc уже разделяют операции чтения и записи. Через систему плагинов можно писать и свои собственные свойства.

Память и другие опции

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

external mem fifo_mem {
  mementries = 1024;
  memwidth = 32;
};

Она создаст область памяти на 1024 слова шириной в 32 бита каждое. Но саму память она не создает — лишь выделяет запрашиваемую область и выводит провода для подключения этой памяти во внешний интерфейс. Где такое может понадобиться? Допустим, у вас в устройстве есть ROM с сырыми данными или что-то подобное. Директива mem позволяет включить ее в общее пространство без костылей типа мультиплексоров. Директива external, к слову, может применяться и для отдельных регистров. Тогда для него не будет создаваться внутренняя логика управления и все будет отдано целиком на откуп разработчику. А вот на документации это никак не отражается — детали реализации остаются скрыты от глаз технического писателя.

Занимаетесь аппаратной разработкой? Обратите внимание на наши вакансии:

Компиляция

После того как регистровый файл написан, необходимо скомпилировать его в исходный код на Verilog и в документацию. PeakRDL со всем этим справляется из коробки. Для изучения этого функционала рассмотрим готовый пример Register description of Atmel XMEGA AU's SPI controller из папки examples в гитхабе SystemRDL.

Генерация SystemVerilog-файла

Сгенерируем готовые файлы в папке regblock и с плоским APB3-интерфейсом. Помимо APB поддерживаются также AXI4-lite, Intel Avalon и Wishbone. Можно сгенерировать доступ как интерфейсом, так и отдельными проводами.

peakrdl regblock atxmega_spi.rdl -o regblock/ --cpuif apb3-flat

В папке regblock найдем два файла:

  • Atxmega_spi_pkg.sv, в котором содержится package, описывающий структуру внутреннего регистрового интерфейса. Его будет использовать наш блок.

  • Atxmega_spi.sv — основной файл с описанием модуля, который мы и будем инстанцировать в наш код. Заголовок его выглядит так:

module atxmega_spi (
	input wire clk,
	input wire rst,

    // плоский APB интерфейс
	input wire s_apb_psel,
	input wire s_apb_penable,
	input wire s_apb_pwrite,
	input wire [2:0] s_apb_paddr,
	input wire [7:0] s_apb_pwdata,
	output logic s_apb_pready,
	output logic [7:0] s_apb_prdata,
	output logic s_apb_pslverr,

    //структуры, подключаемые к нашему IP-блоку
	input atxmega_spi_pkg::atxmega_spi__in_t hwif_in,
	output atxmega_spi_pkg::atxmega_spi__out_t hwif_out
);
Структура сгенерированного регистрового блока
Структура сгенерированного регистрового блока

Провода APB интерфейса подключаются к вышестоящему блоку, а *_pkg — к внутренней логике нашего блока. Делается это, например, так:

//текущее значение нашего регистра
hwif_out.my_reg[0].my_field.value
  
//значение, которое будет записано в регистр в следующем такте
hwif_in.my_reg[0].my_field.next 

Вся синхронная логика работы с регистрами уже есть в этих структурах, поэтому нет смысла в нашем коде защелкивать значение еще раз.

Генерация HTML-документации

Для получения HTML-документации используем следующую команду: 

peakrdl html atxmega_spi.rdl -o html_dir/

После запуска браузера показывается главное окно с описанием адресного пространства — нужно только разрешить запускать локальные файлы или воспользоваться bat-файлом в папке вывода. В нашем примере пространство состоит из четырех отдельных регистров:

Начальная страница документации
Начальная страница документации

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

Описание полей регистра CTRL
Описание полей регистра CTRL

Здесь указаны права доступа к этим полям, их значение после сброса. Есть также поля ввода для значений отдельных полей или регистра целиком. Это позволяет как набрать из отдельных полей итоговое значение регистра, так и разобрать имеющееся значение регистра в состояния его полей. Очень актуально для регистров со множеством символов.

Недостатки и ограничения

При всех достоинствах автоматического создания регистрового файла с реализованным интерфейсом связи с внешним миром не обошлось и без недостатков.

На мой взгляд, главный из них — параметризация, вернее, её отсутствие. Да, параметры как бы есть, с иерархией, и при генерации можно указать, например, ширину регистров, начальные значения полей, размеры массива регистров и т.п. Но в итоговом .sv-файле всё будет прибито гвоздями. Вы не получите параметризованный модуль на выходе — под каждую конфигурацию придется генерировать файл заново.

В документации помимо параметров есть еще и define — как Verilog-подобные, так и perl-вставки. Вот только они тоже применяются на этапе компиляции! То есть в языке есть две структуры для параметризации итогового блока, и обе работают только на этапе компиляции.

Возможно, в коммерческих инструментах есть UDP для генерации параметризированного кода. Также на GitHub есть форк SystemRDL-compiler и PeakRDL с поддержкой параметризации, но они оба немного заброшены, как и issue от их автора. В итоге я запускаю PeakRDL через Python, а затем хитрым поиском и регулярными выражениями заменяю магические числа на параметры и получаю-таки на выходе параметризованный модуль. Но это костыль. 

Еще стоит отметить, что не все возможности SystemRDL поддерживаются в PeakRDL. Например, нет ключевого слова alias, с которым вы можете сослаться на существующий регистр, чтобы у вас в одном адресном пространстве по разным адресам был доступен один и тот же регистр.

Все адреса и отступы будут выровнены по ширине интерфейсной шины. Ширина шины доступа к регистрам определяется наибольшей accesswidth в группе. Исключением являются ситуации, когда регистр сам по себе меньше этой ширины.

//Максимальная ширина доступа accesswidth = 32, поэтому ширина шины данных 32 бит
reg {
	regwidth = 32;
	accesswidth = 32;
} reg_a @ 0x00; // OK. Обычный 32-разрядный регистр
reg {
	regwidth = 64;
	accesswidth = 32;
} reg_b @ 0x08; // OK. Широкий регистр на 64 бит, с доступом по 32 бита.
reg {
	regwidth = 8;
	accesswidth = 8;
} reg_c @ 0x10; // OK. Мелкий регистр, но адрес выровнен к ширине шины
reg {
	regwidth = 32;
	accesswidth = 8;
} bad_reg @ 0x14; // Не OK. регистр влезает в шину целиком, а accesswidth мелкий. 

Побайтового доступа при ширине шины больше 8 бит мы не получим - в самом начале сгенерированного файла младшие биты адреса - обрежутся.

«Безальтернативщина»

SystemRDL не единственный язык описания регистровых блоков. Индустриальным стандартом считается IP-XACT — международный стандарт IEEE-1685, описывающий структуру XML-данных для упаковки, интеграции и повторного использования электронных IP-компонентов в автоматизированных средах проектирования. Он поддерживается всеми крупными EDA-вендорами (Synopsys, Cadence, Siemens), но это XML. Писать IP-XACT вручную — врагу не пожелаешь. Да и использовать его только для регистровых файлов избыточно. Впрочем, PeakRDL — один из немногих open source-тулов, умеющих работать с этим форматом в обе стороны.

Несмотря на ограничения, SystemRDL + PeakRDL — это, пожалуй, золотой стандарт для разработки на чистом SystemVerilog сегодня. Регистровый файл, созданный с помощью SystemRDL, освобождает разработчика от головной боли из-за работы с внешним интерфейсом и документации на этот файл. Все это вкупе дает единую точку работы с регистрами и снижает вероятность сопутствующих ошибок.

Полезные ссылки

Эта статья дополняет мою оригинальную статью о SystemRDL, написанную для журнала FPGA-Systems. Сейчас доступно зеркало оригинального сайта, а также новый сайт сообщества.

Комментарии (0)