Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Сейчас я интервьирую кандидатов которые приходят на позиции в RTL design / проектировщики микросхем на уровне регистровых передач. Но 5 лет назад я интервьировал студентов и других инженеров на позиции в DV / Design Verification / верификаторы блоков микросхем.
Моим стандартным вопросом было написать маркером на доске псевдокод для упрощенного драйвера модели шины (Bus Functional Model - BFM) для протокола AXI. На этом вопросе у ~80% кандидатов наступала агония - они как ужи на сковородке пытались натянуть сову на глобус - приспособить решение для последовательной шины а-ля APB, которое они прочитали в каком-нибудь тьюториале - к шине AXI, которая во-первых конвейерная, а во-вторых, допускает внеочередные ответы на запросы чтения с разными идентификаторами.
Аналогия из другой области: представьте, что кто-то пытается обходить дерево или решить "ханойские башни" - не зная концепций рекурсии и стека. Или написать GUI интерфейс, не зная концепции cобытийно-ориентированной архитектуры.
Причина не в том, что студенты глупые, а в том, что: 1) в вузах верификации не учат (даже в Беркли и Стенфорде) и 2) цель авторов тьюториалов - не научить методам верификации, а научить синтаксису языков (SystemVerilog) и возможностям библиотек (UVM). Поэтому при попытке решить проблему студенты начинают танцевать кругами, заводят fork-и для потоков, семафоры, получают частные случаи, которые глючат. Или начинают накручивать объектно-ориентированное программирование, код внутри интерфейсов, ожидая что время интервью пройдет, хотя проблема в сердце этого кодового гарнира остается.
При этом у задачи существует просто и надежное, но несколько контр-итуитивное решение. Базовую идею я впервые увидел еще в 1990-е годы, когда я работал в Mentor Graphics (сейчас Siemens EDA) в проекте с Nokia и Ericsson, которые тогда проектировали ранние массовые сотовые телефоны. Потом я эксплуатировал тот же подход при работе в Denali (сейчас Cadence), причем клиентами моих BFM-ов были Apple, Broadcom, Xilinx. Затем делал то же самое для клиентов MIPS (PMC Sierra). У некоего японского инженера в Камакуре, которые использовал мою BFM в проекте "зеленого суперкомпьютера", следы этого решения даже просочились в BFM для open-source RISC-V проекта.
Но начнем с начала. Модель интерфейса шины (Bus Functional Model, BFM) - это программа, которая переводит транзакции в последовательности изменений сигналов, а также последовательности из менений сигналов - в транзакции. Транзакция - это высокоуровневый объект, который может передаваться по интерфейсу один или несколько тактов, возможно пересекаясь по времени с другими транзакциями (pipelining, interleave).
Транзакция может быть структурой или объектом класса в SystemVerilog или С++. Например, простая транзакция для минимального подмножества AXI (без burst, mask, locked итд) выглядит так:
class axi_transaction;
rand op_t op; // чтение или запись
rand addr_t addr; // адрес
rand data_t data; // данные
rand id_t id; // идентификатор чтобы понять к какому
// адресу на шине относятся данные
rand delay_t addr_delay; // задержка в тактах для адреса
rand delay_t data_delay; // задержка в тактах для данных
bit data_is_set; // Флаги которые устанавливаются
bit addr_is_sent; // при обработке транзакции
bit data_is_sent;
Транзакции удобны для такого моделирования поведения аппаратных объектов, при котором важен только порядок событий, а не конкретный такт, в который оно произошло. BFM собственно и привязывает транзакции к тактам.
Здесь и далее я буду ссылаться на пример упрощенного Verification IP, который я написал для образовательных семинаров и которое будет в частности использоваться для занятия Школы Синтеза Цифровых Схем в субботу 1 апреля, с полудня по московскому времени (занятия проходят на 12 площадках от Питера до Томска). Вот сайт Школы и телеграм‑канал.
Так вот. Уже лет 20 писатели тьюториалов по SystemVerilog и BFM переписывают друг у друга один и тот же код примеров, который условно говоря выглядит так:
task run ()
while (не конец)
begin
получить следующую транзакцию из теста
выставить на шину ее адрес
если запись, то выставить данные для записи
@ (posedge clock) перейти на следущий такт
while (не готово)
@ (posedge clock) перейти на следущий такт
если чтение, получить данные чтения с шины
закончить транзакцию
end
endtask
Такого рода код вполне адекватно описыват поток последовательных транзакций, когда следущая транзакция начинается после окончания предыдущей. Например:
такт 1 : адрес чтения 1
такт 2 : данные 1
такт 3: адрес чтения 2
такт 4: данные 2
такт 5: адрес чтения 3
такт 6: данные 3:
Проблема в том, что в AXI (и не только AXI, но и в AHB, OCP и других конвейерных шинах, применяемых внутри систем на кристалле) следущая транзакция может начаться, когда еще не прочитано подтверждение за предыдущую.
Например:
такт 1 : адрес и данные записи 1
такт 2 : адрес и данные записи 2 начинаются еще до считывания мастером подтверждения (response valid) записи 1 от слейва
такт 3: адрес и данные записи 3 начинаются еще до считывания мастером подтверждения (response valid) записи 2 от слейва:
Хуже того: мастер может выдать сначала такт за тактом адрес1-адрес2-адрес3, а потом данные для этих трех адресов данные1-данные2-данные3:
Тут студент на собеседовании говорит: "нет проблем, давайте заведем три треда" и пишет что-нибудь типа:
fork
// Поток 1
begin
получить следующую транзакцию-1 из теста
выставить на шину ее адрес-1
repeat (n) @ (posedge clock) пропустить n тактов
выставить на шину ее данные-1
end
// Поток 2
begin
получить следующую транзакцию-2 из теста
выставить на шину ее адрес-2
repeat (n) @ (posedge clock) пропустить n тактов
выставить на шину ее данные-2
end
....
Тут сразу следует от меня вопрос: "а как гарантировать, что данные от транзакции 1 на окажутся на шине в том же такте, что и данные от транзакции 2"? Студент предлагает использовать семафоры.
Тогда я говорю: "а вы в курсе, что данные в AXI могут прийти перед адресом?" Например такт за тактом данные1-данные2-данные3, а потом адрес1-адрес2-адрес3:
Студент начинает еще усложнять. Тогда я говорю: "и вообще на шине может быть одновременно 16 транзакций, причем ответы от запросов на чтение могут приходить не в том порядке, в котором посылались запросы. Вы так и будете 16 тредов писать и семафорами их синхронизировать?"
Одновременно на это накладывается проблема с valid-ready, но опустим ее в этом посте, так как я про не уже писал в Что делать, когда выпускник топ-10 мирового вуза не может спроектировать блок сложения A+B.
Мой подход выглядит так: не пытаемся последовательно проходить одну транзакцию в одном треде который работает несколько тактов. Вместо этого заводим кучу очередей, между которыми перебрасываем транзакции. И работаем с этими очередями внутри одного такта, причем задом наперед: сначала обрабатываем ответы слейва, потом незавершенные передачи мастера, только в конце устанавливаем на шины адреса и данных новые транзакции если шины не заняты. Никаких семафоров, тредов (треды используются в интерфейсе к драйверу, но не в драйвере). Это вам не RTOS, а хардверный симулятор, не надо путать!
В одной компании я работал с юным инженером из юго-западной Европы, который долго сопротивлялся, не верил что так может работать, потом его пробило "ой, а что, так можно?"
Итак, заводим очереди транзакций:
// очередь транзакций, полученных от пользователя
axi_transaction send_queue [$];
// очередь транзакций, передаваемых пользователю
axi_transaction receive_queue [$];
// очередь транзакций, ждущих подтверждения ready
// для valid адреса чтения
axi_transaction rd_addr_queue [$];
// очередь транзакций, ждущих подтверждения ready
// для valid адреса записи
axi_transaction wr_addr_queue [$];
// очередь транзакций, ждущих подтверждения ready
// для valid данных для записи
axi_transaction wr_data_queue [$];
// Массив (индексируемый id) очередей
// ждущих прибытия данных чтения от слейва
axi_transaction rd_data_array [n_ids][$];
// Очередь транзакций ждущих подтверждения записи от слейва
axi_transaction wr_resp_queue [$];
Далее создаем обработчик ровно одного такта ("always @ (posedge clock)", в котором делаем следущее:
Проверяем, принят ли мастером ответ от слейва по поводу чтения ("if (rvalid & rready)"). Если да, перебрасываем головную транзакцию очереди rd_data_array [read id] в хвост очереди ответов пользователю. При этом записывам в транзакцию полученные данные.
Проверяем, принят ли мастером ответ от слейва по поводу записи ("if (bvalid & bready)"). Если да, перебрасываем головную транзакцию очереди wr_resp_queue в хвост очереди ответов пользователю.
Проверяем, подтвердил ли слейв получение адреса чтения ("if (arvalid & arready)"). Если да, перебрасываем головную транзакцию очереди адресов чтения rd_addr_queue в хвост очереди rd_data_array [read id].
Проверяем, подтвердил ли слейв получение адреса записи ("if (awvalid & awready)"). Если да, помечаем головную транзакцию очереди адресов записи wr_addr_queue флагом address_is_sent и выкидываем ее из очереди. При этом, если она еще не помечена флаом data_is_sent, помещаем ее в хвост очереди wr_resp_queue.
Проверяем, подтвердил ли слейв получение данных записи ("if (wvalid & wready)"). Если да, помечаем головную транзакцию очереди данных для записи wr_data_queue флагом data_is_sent и выкидываем ее из очереди. При этом, если она еще не помечена флаом address_is_sent, помещаем ее в хвост очереди wr_resp_queue.
Получаем новые транзакции из очереди transmit_queue и заносим их, в зависимости от типа, в хвосты очередей rd_addr_queue, wr_addr_queue и wr_data_queue.
Начинаем выставлять транзакции на шины (наконец-то!) Если очередь адресов чтения rd_addr_queue непуста и при этом в головной транзакции нет задержки в количестве тактов, выставляем адрес на шину адресов чтения. Иначе ставим arvalid=0. Если очередь непуста и есть задержка в количестве тактов, уменьшаем задержку.
Для адресов записи аналогично (7).
Для данных для записи аналогично (7).
Вот и все. Выглядит незамысловато, но такими моделями шин торговала дюжина компаний, а компания по Verification IP под названием Denali была продана Cadence-у в 2010 году ровно за столько же ($315 миллионов долларов), за сколько Cadence-у же в 2005 году была продана компания Verisity, которая сделала язык "e".
Писание BFM-ов - это хороший хлеб, так как у кучи людей такое программирование вызывает взрыв мозга, при этом если его понять, то все очень просто. Их можно даже писать из Долгопрудного на американский рынок, хотя один такой случай расследовало ФБР.
И еще раз напомню, что остаток примера будет разбираться преподавателем заленоградского МИЭТ Сергеем Чусовым в эту субботу 1 апреля на Школе Синтеза Цифровых Схем, подробности в телеграм‑канале.