Style Request for Testbench with Bus Interfaces

M

M. Norton

Hello folks,

I'm posting this in hopes that folks have some suggestions on how to
handle this in a clean way. I find myself (not surprisingly) writing
testbenches for a lot of very similar bus interfaces. At the simplest
I've got an address signal, data out, data in, and a write enable
signal. On a more complex one I might have the full backend interface
to the Actel PCI core or the Altera Avalon interface.

Regardless, I would really like to be able to write my basic
interaction procedures once and just use them after that. However I'm
running into some difficulty with the language and scoping rules. So
for talking purposes, here's a little skeleton

use work.generic_bus_if_pkg.all; -- <procedure declaration area C :
preferred>

entity just_a_testbench is
end entity just_a_testbench;

architecture behavioral of just_a_testbench is
<procedure declaration area B>
signal be_addr : unsigned(15 downto 0);
signal be_rddata : std_logic_vector(31 downto 0);
signal be_wrdata : std_logic_vector(31 downto 0);
signal be_wren : std_logic;
begin
DUT : entity work.foo
port map (
....
be_addr => be_addr,
be_rddata => be_rddata,
be_wrdata => be_wrdata,
be_wren => be_wren,
....
);

BUS_CONTROL : process is
<procedure declaration region A>
begin
.....
be_read32();
be_write32();
.....
end process BUS_CONTROL;

end architecture behavioral;

Alright, I think that's enough to give an idea. So I'd really like my
procedure calls to be something I can abstract preferably into a
package. Also I'd really like to keep the parameter list as minimal
as possible. However I run into scope issues with signals. So if I
use region A for declaring my read/write procedures, I can directly
access all the signals running into the DUT. I don't have to pass
them in. I can simply have an address and data parameters. However
this means every testbench has to declare everything right there in
the process and that's ugly to me.

Now if I use B or C, then I no longer have scope on the signals. If I
want to use a procedure for reading and writing, I've got to pass them
into the procedure. With the example described, it's not really too
big a deal, but consider a PCI interface with all its myriad signals
or another example various PCI core backend interfaces with start,
stop, rdcyc, wrcyc, and various other signals for backpressure. It
seems to me that the parameter list could get unwieldy very quickly.
So I can put the procedures here, but that violates my desire to have
manageable calls.

So, I'm hoping there's some sort of clever way of passing a bus in and
out of procedure declarations to keep code readable and maintainable.
The only way I've thought that might work is to declare a record that
comprises the entire bus interface. Then I could pass in a single
token that represents the entire bus. I haven't really worked out how
that might work with different naming conventions (I'm usually pretty
consistent with signal names, but sometimes there are variations.)
There are very likely other ways to do it, but I haven't had a lot of
exposure to folks who do large scale testbenches and have already
worked out these tricks.

So, in summary does anyone have any technique or best practice for how
to organize bus interfaces for passing in and out of procedure calls?

Thanks for any ideas. I appreciate it.

Best regards,
Mark Norton
 
M

Mike Treseler

So, I'm hoping there's some sort of clever way of passing a bus in and
out of procedure declarations to keep code readable and maintainable.

I haven't seen it, if we include the "readable" part.
I like to use variables and procedures and minimize processes.
Something like:
....
constant reps : natural := 8;
begin -- process main: Top level loop invokes top procedures.
init;
for i in 1 to reps loop
timed_cycle;
end loop;
for i in 1 to reps loop
handshake_cycle;
end loop;
coda;
end process main;

for details, see the testbench here:
http://mysite.ncnetwork.net/reszotzl/

-- Mike Treseler
 
A

Andy

Hello folks,

I'm posting this in hopes that folks have some suggestions on how to
handle this in a clean way.  I find myself (not surprisingly) writing
testbenches for a lot of very similar bus interfaces.  At the simplest
I've got an address signal, data out, data in, and a write enable
signal.  On a more complex one I might have the full backend interface
to the Actel PCI core or the Altera Avalon interface.

Regardless, I would really like to be able to write my basic
interaction procedures once and just use them after that.  However I'm
running into some difficulty with the language and scoping rules.  So
for talking purposes, here's a little skeleton

use work.generic_bus_if_pkg.all; -- <procedure declaration area C :
preferred>

entity just_a_testbench is
end entity just_a_testbench;

architecture behavioral of just_a_testbench is
     <procedure declaration area B>
     signal be_addr : unsigned(15 downto 0);
     signal be_rddata : std_logic_vector(31 downto 0);
     signal be_wrdata : std_logic_vector(31 downto 0);
     signal be_wren : std_logic;
begin
    DUT : entity work.foo
         port map (
              ....
              be_addr => be_addr,
              be_rddata => be_rddata,
              be_wrdata => be_wrdata,
              be_wren => be_wren,
              ....
         );

     BUS_CONTROL : process is
          <procedure declaration region A>
     begin
           .....
           be_read32();
           be_write32();
           .....
     end process BUS_CONTROL;

end architecture behavioral;

Alright, I think that's enough to give an idea.  So I'd really like my
procedure calls to be something I can abstract preferably into a
package.  Also I'd really like to keep the parameter list as minimal
as possible.  However I run into scope issues with signals.  So if I
use region A for declaring my read/write procedures, I can directly
access all the signals running into the DUT.  I don't have to pass
them in.  I can simply have an address and data parameters.  However
this means every testbench has to declare everything right there in
the process and that's ugly to me.

Now if I use B or C, then I no longer have scope on the signals.  If I
want to use a procedure for reading and writing, I've got to pass them
into the procedure.  With the example described, it's not really too
big a deal, but consider a PCI interface with all its myriad signals
or another example various PCI core backend interfaces with start,
stop, rdcyc, wrcyc, and various other signals for backpressure.  It
seems to me that the parameter list could get unwieldy very quickly.
So I can put the procedures here, but that violates my desire to have
manageable calls.

So, I'm hoping there's some sort of clever way of passing a bus in and
out of procedure declarations to keep code readable and maintainable.
The only way I've thought that might work is to declare a record that
comprises the entire bus interface.  Then I could pass in a single
token that represents the entire bus.  I haven't really worked out how
that might work with different naming conventions (I'm usually pretty
consistent with signal names, but sometimes there are variations.)
There are very likely other ways to do it, but I haven't had a lot of
exposure to folks who do large scale testbenches and have already
worked out these tricks.

So, in summary does anyone have any technique or best practice for how
to organize bus interfaces for passing in and out of procedure calls?

Thanks for any ideas.  I appreciate it.

Best regards,
Mark Norton

I've used a single record with an inout port on the procedure(s). All
of the elements of the record must be resolved types. I declare an
"undriven" constant of that record such that the elements that are
never used as outputs by each process are harmlessly driven to 'Z'.

Andy
 
M

M. Norton

I haven't seen it, if we include the "readable" part.
I like to use variables and procedures and minimize processes.
Something like: ....
for details, see the testbench here:http://mysite.ncnetwork.net/reszotzl/

Yeah, this is essentially what I'm doing right now, one process
describing test over time (with loops and whatnot as need requires)
and then a lot of local procedures to handle the transactions. The
only trouble I've got is that I end up repeating myself quite a lot
over a number of testbenches that use identical or more-likely near-
identical protocols. All it takes is a few names changed and my cut
and paste of previously written transaction procedures gets smoked.

I suppose I'm glad to know I'm not too far off in what I've come up
with but it'd be nice to find something that's more reusable. Thanks
for the information!

Mark
 
M

M. Norton

I've used a single record with an inout port on the procedure(s). All
of the elements of the record must be resolved types. I declare an
"undriven" constant of that record such that the elements that are
never used as outputs by each process are harmlessly driven to 'Z'.

I've read and reread this a bit and while I get the theory of what
you're doing with the constant, I'm not sure how it's being applied.
So if we've got a package with a record containing the signal elements
of a bus, including control lines, that would allow harnessing up a
generic procedure to a testbench component. However when do you apply
the constant that's got things set to high impedance? Does that
happen inside the procedure at the beginning of the procedure and then
subsequent assignments override it?

So, possibly something like this?

procedure my_generic_write( ... ; signal my_bus :
T_BUS_RECORD; ... ) is
begin
my_bus <= C_HARMLESSLY_DRIVEN_TO_Z;
wait until rising_edge(some_clk);
my_bus.address <= some_address;
my_bus.wr_cyc <= '1';
wait until rising_edge(some_clk);
my_bus.data <= some_data;
my_bus.wren <= '1';
wait until rising_edge(some_clk);
my_bus_wren <= '0';
wait until rising_edge(some_clk);
my_bus.wr_cyc <= '0';
wait until rising_edge(some_clk);
my_bus <= C_HARMLESSLY_DRIVEN_TO_Z;
end procedure my_generic_write;

Then during that procedure call, all the my_bus.rd_cyc, my_bus.rd_stb,
etc would remain Z. I will have to try that out and see how it goes.
Seems like it might do what I want (assuming I have your intent
divined correctly).

Thanks for the information!
Mark
 
J

Jonathan Bromley

I'm posting this in hopes that folks have some suggestions on how to
handle this in a clean way. I find myself (not surprisingly) writing
testbenches for a lot of very similar bus interfaces.[...]
I would really like to be able to write my basic
interaction procedures once and just use them after that. However I'm
running into some difficulty with the language and scoping rules.
Yup.

Alright, I think that's enough to give an idea. So I'd really like my
procedure calls to be something I can abstract preferably into a
package. Also I'd really like to keep the parameter list as minimal
as possible.

But, as you've found, a procedure in a package can
only hit signals through its arguments (parameters).
Which may be numerous, for any reasonably complex
model.

There are other issues too. Your bus model probably
needs to keep track of some persistent state, which
you can't comfortably do with a package either.

So, here's what I would regard as the preferred
way to start. Others will doubtless have different
opinions, of course.

First off, consider encapsulating your bus model
not as a package but as an entity. That way it
can have persistent PER-INSTANCE state, i.e.
you can use the same model for numerous different
buses in the same testbench with no difficulty.
And you can provide ports on the entity that
will connect to the physical pins of your
interface. As a super-simple example we can
model an asynchronous serial data (UART)
protocol where the DUT is a receiver, and
your testbench is the transmitter. So the
physical interface is just two wires: TxD
from TB to DUT, and CTS (Clear to Send) from
DUT to TB. Something like this (yes I know it's
no use like this: bear with me as I build up
the example.) Lots of details and declarations
missing, but I'm sure you can fill it all in
yourself.

entity UART_TX_incomplete is
port (
TXD: out std_logic; -- to DUT
CTS: in std_logic ); -- from DUT
end;
architecture incomplete of UART_TX_incomplete is
begin
Transmitter: process
procedure Tx(bit_to_send: std_logic);
begin
TXD <= bit_to_send;
wait for BIT_TIME;
end;
procedure Tx(byte_to_send: unsigned(7 downto 0));
begin
wait until CTS = '1'; -- handshake
Tx('0'); -- start bit
for i in byte_to_send'reverse_range loop
Tx(byte_to_send(i)); -- LSB first
end loop;
Tx('1'); -- stop bit
end;
begin
end process; -- eh??? nothing in this process!!!
end;

OK, this is cool; we just create an instance of this
thing, hook its ports to our DUT interface, and, errm,
call the procedure... oh dear, we can't because the
procedure is hidden away inside a process, inside
the instance. So, how do we call that procedure
from OUTSIDE the UART_TX entity? Answer: provide
a port on said entity that allows your testbench
to command it to do things. This port won't connect
to DUT wires; it will be hooked to a very abstract
signal in the TB, conveying commands. So we can
usefully create a record type that represents a
command. In our case that's kinda simple (just
a byte) but you get the idea. While we're writing
a package that defines this record, we can also
write a procedure to encapsulate the whole business
of getting the UART_TX to do something for us.

package UART_TB_CONTROL_pkg is
type UART_TB_CONTROL_RECORD is record
info: unsigned(7 downto 0);
end record;
procedure send_message(
data_to_send: unsigned(7 downto 0);
signal request: out UART_TB_CONTROL_RECORD;
signal response: in boolean );
begin
-- issue command to UART_TX
request.info <= data_to_send;
-- wait for response signal to toggle
wait on response;
end
endpackage

Now, of course, we must modify our UART_TX so that
it can cope with this request/response protocol.
That costs a couple more ports, but thanks to the
record type, there will ONLY be two such ports
no matter how complex the protocol.

entity UART_TX is
port (
TXD: out std_logic; -- signals to DUT
CTS: in std_logic; -- signals from DUT
REQ: in UART_TB_CONTROL_RECORD;
RSP: out boolean);
end;
architecture OK of UART_TX is
signal response_toggle: boolean;
begin
Transmitter: process
procedure Tx(bit_to_send: std_logic);
begin
TXD <= bit_to_send;
wait for BIT_TIME;
end;
procedure Tx(byte_to_send: unsigned(7 downto 0));
begin
wait until CTS = '1'; -- handshake
Tx('0'); -- start bit
for i in byte_to_send'reverse_range loop
Tx(byte_to_send(i)); -- LSB first
end loop;
Tx('1'); -- stop bit
end;
begin -- here's the management process
-- Wait for the TB to request a new action
wait on REQ'transaction;
-- Implement the requested action
Tx(REQ.info);
-- Indicate completion
response_toggle <= not response_toggle;
-- then loop back to wait for next command
end process;
-- Echo out the response indication
RSP <= response_toggle;
end;

Now we're really ready to go. In your TB, create an
instance of UART_TX with its TXD and CTS signals
connected up to the DUT in the obvious way. Of course,
in your real world there will be many more signals than
two - but the same principles apply. Then, in the TB,
provide signals for the REQ and RSP ports of your UART_TX
instance. And then a process in the TB can generate
stimulus like this:

send_many_characters: process
variable L: line; -- to get stimulus data from a file?
variable Ch: character;
variable V: unsigned(7 downto 0);
begin
-- Read L from a file using usual textio stuff.
-- Or anything else to get some interesting data.
-- Then, send the characters stored in L to the UART:
for i in L'range loop
Ch := L(i);
V := to_unsigned(character'pos(Ch), V'length);
send_message(V, REQ_signal, RSP_signal);
end loop;
end process;

Now the send_message procedure (defined in the package)
has only a small number of arguments, regardless of the
complexity of your physical interface - the complexity
is abstracted-away in the transaction record type.

If you find yourself calling the procedure send_message()
many times in the same process, with the same signal
arguments every time, you can tidy that up too:

process
....
-- Simplified local version of the package procedure
procedure send_message(V: unsigned(7 downto 0);
begin
-- Call the package procedure, with appropriate
-- signals provided as arguments. These signals
-- must be declared in the architecture, of course.
send_message(V, REQ_signal, RSP_signal);
end;
begin -- main body of process
-- calls the local procedure, which fills in the signals
send_message("00001111"); -- it's that simple
send_message("10101010");
...

I'm aware that this example has been quite sketchy,
and uses some "advanced" (whatever that means)
tricks like 'transaction, but I hope at least
it points you in some interesting directions.
Many details yet to fill in - initialization,
declarations, you name it. Over to you.

You can make this work the other way, too, for
models that monitor (rather than drive) a DUT
interface. Same idea applies: convert the messy
signal-wiggles into a transaction record, and work
with that in the TB. Capture the signal-to-record
converter block as an entity, instantiate it with
ports connected to DUT signals and just a couple
of ports to expose the collected transaction
record. Hook up those latter ports to TB-only
signals so that the rest of the TB can see them.

There are several interesting variants on this
theme: for example, the blocks can be coded as
procedures and "instanced" as concurrent
procedure calls. That's convenient, but it
causes some trouble with the 'transaction
trickery, so personally I prefer to use entities.

Enjoy, and thanks for asking all the right questions.
 
M

M. Norton

There are other issues too.  Your bus model probably
needs to keep track of some persistent state, which
you can't comfortably do with a package either.

Glad you mentioned this. I hadn't even gotten to the point where I
was concerned about persistent state. My initial examples were pretty
straightforward and simple, but I do have in mind trying to create a
PCI bus model to try to test this core we seem to be using and reusing
and that absolutely would be a far more complex driver and would
require persistent state for some transactions.
I'm aware that this example has been quite sketchy,
and uses some "advanced" (whatever that means)
tricks like 'transaction, but I hope at least
it points you in some interesting directions.
Many details yet to fill in - initialization,
declarations, you name it.  Over to you.

Absolutely, sketchy is fine. I'm looking for theory mainly and this
has given me a lot to chew on. In the past I have used entities as
DUT drivers, usually for complex packet data, but I hadn't really
thought of abstracting it a level further and giving myself command
hooks into it. This also neatly dodges the issue of clocks. I was
thinking about that when I wrote the little snippet asking about what
Andy suggested, and was realizing I would need to pass in the clock as
well as everything else, which seems a little on the messy side.
Ideally the procedure call would be transaction and message related
information only.

And having a variety of tools in the toolbox is never a bad thing.
Seems to me there's times when Mike's strategy is simplest, and then
Andy's and then this feels like the sledgehammer.
Enjoy, and thanks for asking all the right questions.

I appreciate it.

Best regards,
Mark
 
M

Mike Treseler

Yeah, this is essentially what I'm doing right now, one process
describing test over time (with loops and whatnot as need requires)
and then a lot of local procedures to handle the transactions. The
only trouble I've got is that I end up repeating myself quite a lot
over a number of testbenches that use identical or more-likely near-
identical protocols. All it takes is a few names changed and my cut
and paste of previously written transaction procedures gets smoked.

I try to do the heavy lifting in functions, which are easily packaged
with required and default parameters.
I use simple procedures with no parameters when possible for readable
code.
I suppose I'm glad to know I'm not too far off in what I've come up
with but it'd be nice to find something that's more reusable. Thanks
for the information!

Functions are easily packaged and reused.
Procedures can at least eliminate the local
cut and paste.

-- Mike Treseler
 
K

KJ

So, in summary does anyone have any technique or best practice for how
to organize bus interfaces for passing in and out of procedure calls?

1. Similar to what Jonathon suggested, but a bit easier (I think) to
draw good boundary lines for the testing entity is to model real
parts. FPGAs in real applications do not exist untethered to the rest
of the world, they are connected to real parts on a circuit board so
your 'FPGA testbench' could (I think 'should') be a model of that PCBA
(and surrounding systems)...which means that if you model the parts
that go on that PCBA and connect them per the netlist then you're
modelling your real system. In your case, you had mentioned in later
posts about modelling a PCI bus. I'm guessing that there is a
processor on the board, so model that processor and you'll have your
PCI bus. Maybe for starters, the PCI bus is about all that you will
model, the rest can come later when needed. In any case, by doing
this, you'll be refining your cheezy processor model over time and
making it closer to the real thing which will make that model better
and better over time.

What I've also found is that as I model more of the real system, there
comes less of a need for that 'magic communications interface' between
two parts that Jonathon mentioned in his post. If needed, I simply
put that interface into a 'magic comms' package which is project
specific (but can be named the same for each project). For the most
part, simulations become watching the system perform whatever it is
that you set it up to do without too much 'magic' communications
between components.

2. Standardize on a comm interface. For the most part, interfaces
simply need to read and write. Use Avalon as a guide, at the low
level you can write once and use over and over that handshake for any
interface. Then the task becomes to create an 'Avalon to PCI' widget
(as an example). If that same type of PCI interface is needed in a
different application that is a totally different processor, then
guess what? You can use that Avalon to PCI widget that you created in
your new processor. Both processors could use the same Avalon
protocol on the one side with PCI on the other.

3. As has been pointed out and you've discovered, removing the
signals from procedures means putting them into a process which can be
a pain. However, if you implement #2, then this becomes a
straightforward wrapper to portion of your your part model from #1.
That description is likely unique to that model so you wouldn't really
have a need to 'reuse it' somewhere else other than to plop down your
part model from #1. However, there can still be cutting and pasting
within that part model. Consider a processor model where you
implement the equivalent of several 'threads' that wake up and run at
appropriate times. Each thread would be a process, therefore each
'thread' would have to have the shorthand procedures cut and pasted.
A pain, but again those procedures are only of use to that part model
so you're likely editing a single file.

Kevin Jennings
 
A

Andy

I've read and reread this a bit and while I get the theory of what
you're doing with the constant, I'm not sure how it's being applied.
So if we've got a package with a record containing the signal elements
of a bus, including control lines, that would allow harnessing up a
generic procedure to a testbench component.  However when do you apply
the constant that's got things set to high impedance?  Does that
happen inside the procedure at the beginning of the procedure and then
subsequent assignments override it?

So, possibly something like this?

   procedure my_generic_write( ... ; signal my_bus :
T_BUS_RECORD; ... ) is
   begin
        my_bus <= C_HARMLESSLY_DRIVEN_TO_Z;
        wait until rising_edge(some_clk);
        my_bus.address <= some_address;
        my_bus.wr_cyc <= '1';
        wait until rising_edge(some_clk);
        my_bus.data <= some_data;
        my_bus.wren <= '1';
        wait until rising_edge(some_clk);
        my_bus_wren <= '0';
        wait until rising_edge(some_clk);
        my_bus.wr_cyc <= '0';
        wait until rising_edge(some_clk);
        my_bus <= C_HARMLESSLY_DRIVEN_TO_Z;
   end procedure my_generic_write;

Then during that procedure call, all the my_bus.rd_cyc, my_bus.rd_stb,
etc would remain Z.  I will have to try that out and see how it goes.
Seems like it might do what I want (assuming I have your intent
divined correctly).

Thanks for the information!
Mark

Yes, you'd use it like that in the procedure(s), but you also need to
make sure that processes in the DUT that interact with the record also
drive the constant onto the signal, so that the DUT does not end up
driving 'U' on input elements of the record.

Andy
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,904
Latest member
HealthyVisionsCBDPrice

Latest Threads

Top