Still waiting… It’s Friday afternoon, and I don’t have my RTL
Using SystemVerilog to model RTL behavior in a pinch or anytime
A couple weeks ago, sitting here in California on a rainy Friday afternoon. No the drought is not over, but the hills are green again, and the reservoirs are filling up. Daydreaming about the power and flexibility of SystemVerilog and some of the trouble people can get themselves into with it. Wishing my RTL models would show up, so I could test my testbench…
I start to think about writing some SystemVerilog… SystemVerilog is a powerful language. You can write some powerful code with little or no pain. It’s just software.
In the early days of SystemVerilog, I coded some C code and some SystemVerilog code that did the same thing. Some function calls and loops and if-then-else and some adds and multiplies. Just code. Then I disassembled both sides and compared the generated code. It was basically the same generated code! Certainly I could expect the same performance. (And I got it).
That result made me feel very comfortable writing SystemVerilog code anytime I needed some “code”. I didn’t need any special C or C++ code, I could just write SystemVerilog. There are plenty of good reasons to use C and C++ alongside your SystemVerilog, but you certainly don’t need to go there FIRST.
With this in mind on that foggy Friday afternoon two weeks ago, I decided to use SystemVerilog as a stand-in or early implementation for some simple missing RTL. What I mean is that I was going to implement the functionality using high-level SystemVerilog code – not RTL. This is a case where exact timing is not so important, and complete functionality is not so important. What IS important is getting some tests running before the weekend.
In this example, we just want a block implemented that acts like a switch – two input ports and two output ports. That code is simple enough, we can just write some SystemVerilog code using queues.
Imagine a switch with two ports in and two ports out. The ports each have an 8 bit data payload, an “output address” and a ready signal.
The protocol is simple. If ready is high on the positive edge of the clock, then a transfer takes place. This transfer takes just one clock cycle. (This is just a simple example). The typedef below defines each “port” connection – or each packet that is transferred on the bus.
typedef struct packed {
bit ready; // Valid data ready
bit output_port; // Send to which port? (1 or 2)
bit [7:0] data; // Data payload
} packet_t;
Now we can build a switch, with a simple clock and four ports (two input and two output). This switch is not too smart. It just shuttles “packets” from input port to output port, choosing one of four possible routes: 1→ 1, 1→ 2, 2→ 1 and 2→ 2. The switch ports are defined as the typedef packet – each port has a ready, a data payload and an output destination.
module switch (input clk,
input packet_t in1, packet_t in2,
output packet_t out1, packet_t out2);
The module is implemented using two simple queues. We are NOT doing any reordering of the queues, nor are we supporting prioritized transfers. We could, but it is Friday afternoon, after all.
packet_t in_q[$];
packet_t out_q[$];
When a packet comes into a port, it is put on the input queue. [i.e. On the clock edge, for that port, if the ready signal is high, then capture the connection into the input queue (ready, data payload and output_port)]. Do this for both input ports – in1 and in2.
always @(posedge clk)
if (in1.ready == 1)
in_q.push_front(in1);
always @(posedge clk)
if (in2.ready == 1)
in_q.push_front(in2);
When the size of the input queue is NOT zero, then a packet is popped off and put on the output queue for “other” processing.
always begin: Input_Queue
packet_t p;
wait (in_q.size() != 0);
p = in_q.pop_back();
out_q.push_front(p);
repeat (1) @(posedge clk);
end
When the size of the output queue is NOT zero, then a packet is popped off and sent out of the module – it goes out onto the bus. Check to see if it goes out on out1 or on out2.
always begin: Output_Queue
packet_t p;
wait (out_q.size() != 0);
p = out_q.pop_back();
case(p.output_port) // Which output port?
0: begin
out1 = p;
out1.ready = 1;
@(posedge clk);
out1.ready = 0;
@(negedge clk);
end
1: begin
out2 = p;
out2.ready = 1;
@(posedge clk);
out2.ready = 0;
@(negedge clk);
end
endcase end
The next higher level that instantiates the switches is simple. Just simple connections. The packed struct takes care of the details. A couple of switches connected together using the packed structs is quite easy to write.
packet_t in1, in2, out1, out2;
packet_t n1, n2;
switch ss_A(clk, in1, in2, n1, n2);
switch ss_B(clk, n1, n2, out1, out2);
We now have a switch implementation with certain kinds of behavior. We can implement a priority scheme, or re-order the queues as we wish. And it is still Friday afternoon! We used user defined types (typedef of packed structs), queues and multiple threads. Pretty good bang for the buck. And coded and tested in less than a day.
The moral of the story is this. If you need to implement some missing functionality, SystemVerilog is certainly capable. It’s not as exciting as a presidential race. It’s not as exciting as going away to college for the first time, nor as exciting as racing Tesla’s on the highway, but in a pinch on a rainy Friday afternoon it sure beats going home early. Or does it?
It’s up and running, and I see the queue sizes in the switch modules growing and draining, and I call it a day.
You can find some ideas on the Verification Academy along these same lines using other SystemVerilog high-level constructs like classes, associative arrays, dynamic arrays and processes by clicking here. Look for “No RTL Yet? No Problem UVM Testing a SystemVerilog Fabric Model”. If you *really* want to write RTL instead of high-level SystemVerilog, you can find some great tips and tricks at Sunburst Design, especially the FIFO 1 & 2 papers.
Happy Friday! More rain predicted for this coming Friday.
Comments