5. Шины и тестирование последовательностной логики
Дана задача:
Пусть есть точки на отрезке. Каждая точка характеризуется двумя параметрами: 8 бит координат и 3 бита цвет. В систему каждый из параметров поступает последовательно по каналу шириной 1 бит.
Модуль обработки обрабатывает 11 бит за раз. Необходимо написать конвертер двух последовательных входов в один параллельный.
На примере данной задачи и рассмотрим темы данного занятия.
Шины
Редкая программа состоит из одной функции. Мы привыкли к тому, что программа состоит множества функций, где одна вызывает другую. Так и хороший дизайн состоит из нескольких модулей. Однако все модули существуют одновременно, поэтому появляется необходимость синхронизировать модули друг с другом.
Для решения данной проблемы используются шинные протоколы, которые достаточно хорошо стандартизованы. Одни из наиболее известных протоколов: AMBA AXI[1], WishBone, Avalon Bus. Протоколы часто рассчитаны на различные топологии подключения: master-slave, конвейер данных, общая шина и другие.
Мы будем рассматривать топологию конвейер данных.
Каждый модуль одновременно является и master (префикс m_
), и slave (префикс s_
).
Когда один модуль готов отправить данные он записывает данные в m_data
и поднимает сигнал m_valid
, а затем ждёт готовности следующего модуля в цепочке.
Как только поднимается сигнал m_ready
, происходит обмен данными.
Важно, что m_valid
не может зависеть от m_ready
.
Конвертер последовательного сигнала в параллельный
Самым простым решением нашей задачи является использования трех блоков: один будет выдавать 8-битный параллельный сигнал, второй 3-битный параллельный сигнал, а третий будет их объединять.
Начнём решать нашу задачу с создания конвертера последовательного сигнала в параллельный.
module serial_to_parallel #(
parameter int OUT_WIDTH = 8 (1)
) (
input logic clk,
input logic aresetn, (2)
// Interface inspired by AXI Stream
// Input from master
input logic s_valid,
output logic s_ready,
input logic s_data,
// Output to slave
output logic m_valid,
input logic m_ready,
output logic [OUT_WIDTH-1:0] m_data
);
logic [OUT_WIDTH-1:0] data_ff;
logic [$clog2(OUT_WIDTH)-1:0] counter; (3)
logic valid_ff;
always_ff @(posedge clk or negedge aresetn) begin (4)
if (~aresetn) begin
data_ff <= '0;
end else if (s_ready & s_valid) begin (5)
data_ff <= {s_data, data_ff[OUT_WIDTH-1:1]}; (6)
end
end
always_ff @(posedge clk or negedge aresetn) begin (7)
if (~aresetn) begin
counter <= '0;
valid_ff <= 1'b0;
end else if (s_ready & s_valid) begin
counter <= counter + 1;
if (counter == (OUT_WIDTH - 1)) begin
valid_ff <= 1'b1;
counter <= '0;
end else begin
valid_ff <= 1'b0;
end
end
end
always_comb begin (8)
m_valid = valid_ff;
s_ready = ~(m_valid & ~m_ready); (9)
m_data = data_ff;
end
endmodule
1 | Параметризуем наш модуль шириной последовательного порта. |
2 | Здесь будет использовать асинхронный (префикс a ) сигнал сброса с активным нижним уровнем (суффикс n ).
Асинхронность означает, что мы обязаны реагировать на сигнал сброса в любое время, а не только по фронту clk .
Активный нижний уровень означает, что обычное значение сигнала — 1, а для сброса его необходимо сделать 0. |
3 | Используем встроенную функцию $clog2 , которая вычисляет логарифм по основанию 2, а затем округляет число вверх, чтобы вычислить сколько бит необходимо для того, чтобы досчитать до числа OUT_WIDTH -1. |
4 | Данный блок отвечает за сохранение валидных битов.
Здесь в списке чувствительности указываем не только clk , но и aresetn . |
5 | Проверяем стоит ли нам забирать входные данные. |
6 | Такая конструкция часто называется сдвиговый регистр. |
7 | Данный блок отвечает за учёт сохраненных битов. |
8 | Данный блок отвечает за вывод сигналов. |
9 | Единственный случай, когда мы не готовы принимать данные — мы сформировали вывод, а следующее устройство в цепочке не готово его принять. |
Тестирование кода
Теперь наконец поговорим о более сложных тестбенчах. На самом деле SystemVerilog предоставляет богатые возможности по написанию тестбенчей. Здесь показан наиболее простой код, демонстрирующий основные концепции. Дальше данный код можно расширять используя функции, задачи (аналог процедур), а также классы[2].
`timescale 1ns / 1ps (1)
module serial_to_parallel_tb;
localparam int OutWidth = 11; (2)
logic clk;
logic aresetn;
logic s_valid;
logic s_ready;
logic s_data;
logic m_valid;
logic m_ready;
logic [OutWidth-1:0] m_data;
serial_to_parallel #(
.OUT_WIDTH(OutWidth)
) DUT (
.clk(clk),
.aresetn(aresetn),
.s_valid(s_valid),
.s_ready(s_ready),
.s_data(s_data),
.m_valid(m_valid),
.m_ready(m_ready),
.m_data(m_data)
);
localparam int ClkPeriod = 10;
initial begin
clk <= '0;
forever begin
#(ClkPeriod / 2) clk <= ~clk;
end
end
initial begin
aresetn <= '0;
#(ClkPeriod);
aresetn <= '1;
end
localparam int NOfIterations = 1000;
initial begin (2)
wait (~aresetn);
s_valid <= '0;
s_data <= '0;
wait (aresetn);
repeat (NOfIterations) begin (3)
@(posedge clk);
s_valid <= '1;
s_data <= $urandom(); (4)
do begin
@(posedge clk);
end while (~s_ready); (5)
s_valid <= '0; (6)
end
$finish(); (7)
end
initial begin (8)
wait (~aresetn);
m_ready <= $urandom();
wait (aresetn);
forever begin
@(posedge clk);
m_ready <= $urandom();
end
end
logic [OutWidth-1:0] parallel_out = '0;
int counter = 0;
initial begin (9)
wait (aresetn);
forever begin
@(posedge clk);
if (s_valid & s_ready) begin
parallel_out <= {s_data, parallel_out[OutWidth-1:1]};
if (counter == OutWidth) begin
counter <= 1;
end else begin
counter <= counter + 1;
end
end
end
end
initial begin (10)
wait (aresetn);
forever begin
@(posedge clk);
if (counter == OutWidth) begin
if (~(m_valid)) begin
$error("%0t Incorrect m_valid. Expected: 1, actual: %d", $time(), m_valid);
end
end
if (m_data !== parallel_out) begin
$error("%0t Incorrect m_data. Expected: %d, actual: %d", $time(), parallel_out, m_data);
end
end
end
// initial begin (11)
// $monitor("%0t\t", $time(), "aresetn=%d\t", aresetn, "s_valid=%d\t", s_valid, "s_ready=%d\t",
// s_ready, "s_data=%d\t", s_data, "m_valid=%d\t", m_valid, "m_ready=%d\t", m_ready,
// "m_data=%b\t", m_data, "parallel_out=%b", parallel_out);
// end
initial begin (12)
repeat (100000) @(posedge clk);
$stop();
end
`ifdef __ICARUS__ (13)
initial begin
$dumpfile("serial_to_parallel_tb.vcd");
$dumpvars(0, serial_to_parallel_tb);
end
`endif
endmodule
1 | Укажем время и временную точность симуляции[3]. |
2 | Конструкции выше уже должны быть знакомы с прошлой темы. Этот блок отвечает за генерацию входных сигналов. |
3 | Цикл, который повторится заданное количество раз. |
4 | Генерируем случайный вход. |
5 | Держим данные пока принимающее устройство не готово. |
6 | "Облегчаем" процесс коммуникации.
На самом деле обнулять сигнал s_valid нет необходимости. |
7 | Именно этот блок контролирует момент конца симуляции. |
8 | В данном блоке симулируем следующее устройство в цепи. |
9 | Реализуем функциональность нашего блока в тестах, чтобы проверить корректность. |
10 | Блок, в котором происходит проверка корректности работы. |
11 | Данный блок закомментирован, но может быть удобен для отладки. Обратите внимание на непривычный способ форматирования строки. |
12 | Данный блок закончит симуляцию по таймауту. Может быть очень полезен, если ваша симуляция зависла. |
13 | Выгружаем сигналы для GTKWave. В данном случае при помощи препроцессора запускаем данное действие только при использовании Icarus Verilog. |
Итого, для тестирования модуля стоит использовать более близкие к обычным языкам программирования возможности SystemVerilog, а так же для рандомизированного тестирования стоит иметь модель модуля для сравнения результатов. Однако создание модели на SystemVerilog бывает крайне неудобно, об альтернативных способах тестирования мы расскажем в следующих блоках.
Объединение двух параллельных сигналов
Теперь нам нужно объединить два параллельных сигнала в один. Посмотрим на код модуля, который реализует данное действие.
module merge_parallel #(
parameter int IN_WIDTH_1 = 3,
parameter int IN_WIDTH_2 = 8,
parameter int OUT_WIDTH = IN_WIDTH_1 + IN_WIDTH_2 (1)
) (
input logic clk,
input logic aresetn,
// Input from master 1
input logic s_valid_1,
output logic s_ready_1,
input logic [IN_WIDTH_1-1:0] s_data_1,
// Input from master 2
input logic s_valid_2,
output logic s_ready_2,
input logic [IN_WIDTH_2-1:0] s_data_2,
// Output to slave
output logic m_valid,
input logic m_ready,
output logic [OUT_WIDTH-1:0] m_data
);
logic [OUT_WIDTH-1:0] data_ff;
logic valid_1_ff, valid_2_ff;
always_ff @(posedge clk or negedge aresetn) begin (2)
if (~aresetn) begin
data_ff <= '0;
end else begin
if (s_ready_1 & s_valid_1) begin
data_ff[IN_WIDTH_1-1:0] <= s_data_1;
end
if (s_ready_2 & s_valid_2) begin
data_ff[OUT_WIDTH-1:IN_WIDTH_1] <= s_data_2;
end
end
end
always_ff @(posedge clk or negedge aresetn) begin (3)
if (~aresetn) begin
valid_1_ff <= '0;
valid_2_ff <= '0;
end else begin
if (s_ready_1 & s_valid_1) begin
valid_1_ff <= '1;
end
if (s_ready_2 & s_valid_2) begin
valid_2_ff <= '1;
end
// This condition isn't perfect,
// as it requires excess cycle to reset flags
if (m_valid & m_ready) begin
valid_1_ff <= '0;
valid_2_ff <= '0;
end
end
end
always_comb begin
m_valid = valid_1_ff & valid_2_ff;
s_ready_1 = ~valid_1_ff;
s_ready_2 = ~valid_2_ff;
m_data = data_ff;
end
endmodule
1 | Параметры тоже могут зависеть от параметров. К сожалению у такого подхода есть один минус: этот параметр всё так же можно менять при инстанциации. |
2 | Правило хорошего тона: разбиваем логику работы с данными и с управляющими сигналами на два always_ff блока с одинаковым списком чувствительности.
В этом блоке работает с данными. |
3 | А в этом с управляющими сигналами. |
Однако теперь мы видим проблему: как только мы преобразовали все три бита в одном конвертере, он будет простаивать и дожидаться когда отработает второй конвертер. Обычно такая проблема как раз возникает при использовании внешней периферии и для того, чтобы её смягчить используют более быстрый или более медленный управляющий сигнал. К сожалению, перемещение сигнала между различными управляющими сигналами — непростая задача, которая не будет освещена в данном курсе[4].
Итоговый модуль может выглядеть примерно так:
module merge_serial_to_parallel #(
parameter int WIDTH_1 = 3,
parameter int WIDTH_2 = 8,
parameter int OUT_WIDTH = WIDTH_1 + WIDTH_2
) (
input logic clk,
input logic aresetn,
// Input from master 1
input logic s_valid_1,
output logic s_ready_1,
input logic s_data_1,
// Input from master 2
input logic s_valid_2,
output logic s_ready_2,
input logic s_data_2,
// Output to slave
output logic m_valid,
input logic m_ready,
output logic [OUT_WIDTH-1:0] m_data
);
logic s2p_1_valid, s2p_1_ready, s2p_2_valid, s2p_2_ready;
logic [WIDTH_1-1:0] s2p_1_data;
logic [WIDTH_2-1:0] s2p_2_data;
serial_to_parallel #(
.OUT_WIDTH(WIDTH_1)
) s2p_1 (
.clk,
.aresetn,
.s_valid(s_valid_1),
.s_ready(s_ready_1),
.s_data (s_data_1),
.m_valid(s2p_1_valid),
.m_ready(s2p_1_ready),
.m_data (s2p_1_data)
);
serial_to_parallel #(
.OUT_WIDTH(WIDTH_2)
) s2p_2 (
.clk,
.aresetn,
.s_valid(s_valid_2),
.s_ready(s_ready_2),
.s_data (s_data_2),
.m_valid(s2p_2_valid),
.m_ready(s2p_2_ready),
.m_data (s2p_2_data)
);
merge_parallel #(
.IN_WIDTH_1(WIDTH_1),
.IN_WIDTH_2(WIDTH_2)
) merge (
.clk,
.aresetn,
.s_valid_1(s2p_1_valid),
.s_ready_1(s2p_1_ready),
.s_data_1 (s2p_1_data),
.s_valid_2(s2p_2_valid),
.s_ready_2(s2p_2_ready),
.s_data_2 (s2p_2_data),
.m_valid,
.m_ready,
.m_data
);
endmodule
Обязательно порешайте упражнения. |