Semi-Complex RTL Record Based SPI Interface Use Case

Introduction

The SPI bus having existed since slightly before the wheel, it presents a familiar use case to introduce a slightly more complex block and interface structure to examine the use of the new VHDL record concepts.

Syntax remains extremely preliminary, and is presented only as to provide a concrete idea of the underlying concepts. A compressed tarball with the VHDL files (both 2008 version and according to the proposal) can be found http://www.eda-twiki.org/twiki/pub/P1076/Ballots/SpiExample/../InterfaceAndBundleEnhancements/SPI_Bus.tar.gz.

Model

The case being modeled will be of an RTL FPGA containing a general purpose QSPI master block (quad SPI, transmitting a nibble at a time).

This FPGA is harnessed in a testbench simulating a board level design in which the QSPI interface connects to three devices on the PCB:

  1. a QSPI serial flash (read/write)
  2. a conventional SPI DAC (write-only)
  3. a conventional SPI ADC (read/write)

Each of these will connect to one active-low chip select (SSEL[3:0]) from the FPGA. MOSI[3:0] and SCLK drive from the master to all 3 slaves (with conventional SPI using MOSI[0] only) and MISO[3:0] has weak pullups when not being driven by any of the slaves (with conventional SPI using MISO[0] only).

SPI Bus Package

In order to share our definitions across the design, they are defined within a package:

package spi_bus_pkg is
        generic(
                CHIP_SELECTS : positive
        );

        subtype ssel_rng is natural range CHIP_SELECTS-1 downto 0;
        subtype ssel_vst is std_logic_vector(ssel_rng);
end package spi_bus_pkg;

Main Wiring Bundle

The wiring bundle itself contains all the signals for the SPI bus:

type spi_r is record
        mosi : std_logic_vector(3 downto 0);            -- Data from master to slave
        miso : std_logic_vector(3 downto 0);            -- Data from slave to master
        sclk : std_logic;                               -- Serial clock
        ssel : ssel_vst;                                 -- Chip selects
end record spi_r;

Master View

The master has a view onto the entire signal bundle:

record view qspi_master_view of spi_r is
        element(
                mosi : out;
                miso : in;
                sclk : out;
                ssel : out
        );
end record view qspi_master_view;

Slave View

Whether QSPI or conventional, an SPI needs only one chip select, so one main goal of the view is to pluck the correct one from the bundle:

record view qspi_slave of spi_r is
        generic(
                IDX : ssel_rng
        );
        element(
                mosi : in;
                miso : out;
                sclk : in;
                cs   : in std_logic is ssel(IDX)
        );
end record view qspi_slave;

record view spi_slave of spi_r is
        generic(
                IDX : ssel_rng
        );
        element(
                din  : in  std_logic is mosi(0);
                dout : out std_logic is miso(0);
                sclk : in;
                cs   : in  std_logic is ssel(IDX)
        );
end record view spi_slave;

Question: Generic Selection in a Named Mode View. -- Brent Hayhoe - 2015-09-10

Procedures

Thinking ahead to our simulated SPI slaves; we'll want some simple procedures to enable us to work with the SPI bus in behavioral code:

procedure spi_bits(
        view spi  : spi_slave;
        bits : in  positive;
        xmit : in  std_logic_vector;
        recv : out std_logic_vector;

        Tpd  : in  time;
        Ts   : in  time
) is
        variable xd : std_logic_vector(bits-1 downto 0);
        variable rd : std_logic_vector(bits-1 downto 0);
begin
        xd := xmit;
        for i in range 0 to bits-1 loop
                wait until spi.sclk = '1';
                spi.dout <= xd(bits-i-1) after Tpd;
                rd(i) := spi.din'delayed(Ts);
        end loop;
        recv := rd;
end procedure spi_bits;

A similar procedure qspi_bits will take a qspi_slave.

Note: the SPI parameter has to be declared to be a view in the same syntactic place as it would otherwise be declared a signal, variable, or constant, because it's not any of those things: it's a view.

Question: Mode View Equivalence to Signals, Variables & Constants. -- Brent Hayhoe - 2015-09-10

Note: this points out some of the difficulties we're going to have with incorporating timing delays into the bundle syntax.

Note: there's still the IDX generic on the view to deal with somehow.

Implementing the (RTL) Master

Instantiate the interface package based on the 3 slaves as discussed:

package spi_bus_3slv is new spi_bus_pkg
        generic map (
                CHIP_SELECTS : 3
        );

Create the SPI master complete with the bundle:

entity spi_master is
        port(
                clk             : in  std_logic;
                rst             : in  std_logic;
                ...
                spi             : view spi_bus_3slv.qspi_master_view
        );
end entity spi_master

architecture RTL of spi_master is
        FOOBAR: process(clk, rst)
        begin
                ...
                spi.ssel <= (others => '1');
                if cs_asserted then
                        spi.ssel(active_cs) <= '0';
                end if;

                if shiftdata then
                        spi.mosi <= sr(sr'high downto sr'high-3);
                        sr(3 downto 0) := spi.miso;
                end if;

                if makerisingedge then
                        spi.sclk <= '1';
                elsif makefallingedge then
                        spi.sclk <= '0';
                end if;
                ...
        end process FOOBAR;
end architecture RTL;

And place it inside of an entire design:

entity FPGA is
        port(
                clk             : in  std_logic;
                rst             : in  std_logic;
                ...
                spi             : view spi_bus_3slv.qspi_master_view
        );
end entity FPGA

architecture Structural of FPGA is
        ...
begin
        SPIMST: entity work.spi_master
                port map (
                        clk => clk,
                        rst => rst,
                        ...
                        spi => spi
                );

        ...
end architecture Structural;

Note: do we want to force FPGA to bind to a view on the bundle, or should we allow FPGA to bind to the unqualified bundle?

Implementing the Simulation Slaves

The slaves use the defined slave views:

-- TODO: Get the generic package instantiation correct.
entity qspi_flash
        generic (
                pkg : spi_bus_pkg;
                CS  : pkg.ssel_rng;
                Tpd : time := 1 ns;
                Ts  : time := 1 ns
        );
        port (
                spi  : view pkg.qspi_slave
                                        generic map (IDX => CS);
                wren : in std_logic := 1
        );
end entity qspi_flash;

architecture Behavioral of qspi_flash is
begin

        SIM: process
                variable cmd : std_logic_vector(7 downto 0);
        begin
                spi.dout <= 'Z';
                wait until cs = '0';
                qspi_bits(spi, 8, x"00", cmd, Tpd, Ts);
                case cmd is
                        when x"80" =>
                                ...
        end process SIM;
end architecture Behavioral;

entity spi_adc
        generic (
                CS  : spi_bus_3slv.ssel_rng;
                VREF: real;
                Tpd : time := 5 ns;
                Ts  : time := 5 ns
        );
        port (
                spi  : view spi_bus_3slv.spi_slave
                                        generic map (IDX => CS);
                ain  : in real
        );
end entity spi_adc;

architecture Behavioral of spi_adc is
begin

        SIM: process
                variable adcdata : std_logic_vector(15 downto 0);
                variable cmd : std_logic_vector(15 downto 0);
        begin
                spi.dout <= 'Z'
                wait until cs = '0';
                adcdata := STD_LOGIC_VECTOR(TO_UNSIGNED(INTEGER(ain * 65536.0 / VREF), 16));
                spi_bits(spi, 16, adcdata, cmd, Tpd, Ts);
                case cmd is
                        ...
        end process SIM;
end architecture Behavioral;

entity spi_dac
        generic (
                CS  : spi_bus_3slv.ssel_rng;
                VREF: real;
                Ts  : time := 5 ns
        );
        port (
                spi  : view spi_bus_3slv.spi_slave
                                        generic map (IDX => CS);
                aout : out real
        );
end entity spi_dac;

architecture Behavioral of spi_dac is
begin

        SIM: process
                variable dacdata : std_logic_vector(15 downto 0);
                constant dout : std_logic_vector(15 downto 0) := (others => 'Z');
        begin
                wait until cs = '0';
                spi_bits(spi, 16, dout, dacdata, 0 ns, Ts);
                aout <= REAL(TO_INTEGER(UNSIGNED(data))) * VREF/65536.0;
        end process SIM;
end architecture Behavioral;

Note: avoiding having to create another view for the simulation DAC, complete with an associated procedure that doesn't drive dout, required creation of a dummy output that was always 'Z'. Not horrific, but not particularly elegant either.

Note: as implemented above, the binding of a specific SPI slave to a chip select requires that the selection mechanism be a generic of the slave itself. That doesn't seem right; the selection of which chip select goes to what should be entirely encapsulated in the wiring; the individual slave shouldn't even contain that as a concept. We'll re-examine this in `Proposal 2`.

Comment: Generics Used for Instantiation Identification. -- Brent Hayhoe - 2015-09-10

Top-Level Harness

At the testbench level, we need to connect together the FPGA (which contains the SPI master) with the three slaves:

entity Testbench
end entity Testbench;

architecture TB of Testbench is

        signal clk : std_logic;
        signal rst : std_logic;

        signal spi : spi_bus_3slv.spi_r;

begin
        DUT: entity work.FPGA
                port map (
                        clk => clk,
                        rst => rst,
                        ...
                        -- Because qspi_master is a view onto the spi_r, this
                        -- mapping is allowable.
                        spi => spi,
                );

        MEMORY: entity work.qspi_flash
                generic map (
                        CS  => 0
                ) port map (
                        -- This mapping is where the chip select is selected
                        -- using the CS generic above.
                        spi => spi
                );

        ADC: entity work.spi_adc
                generic map (
                        CS  => 1,
                        VREF => 3.0
                ) port map (
                        spi => spi,
                        adc => monitored_voltage
                );

        DAC: entity work.spi_dac
                generic map (
                        CS  => 2,
                        VREF => 3.0
                ) port map (
                        spi => spi,
                        aout => driven_voltage
                );

        -- And our resistive pullups.
        spi.miso <= (others => 'Z');

end architecture TB;

Note: if the physical bundle is defined using the signal keyword, that may cause issues with trying to incorporate terminals, variables, etc.

Possible Refinements

Promoting view to a declaration

To try to get around some of the problems with the view, we'll upgrade views to be equivalent to signals, variables, or files in the syntax.

This syntax would probably give view definitions looking more like:

view qspi_master_view of spi_r : record is
        element(
                mosi : out;
                miso : in;
                sclk : out;
                ssel : out
        );
end view qspi_master_view;

In declarations, a view becomes something that contrasts a signal:

entity FPGA is
        port(
                clk             : in  std_logic;
                rst             : in  std_logic;
                ...
                view spi : spi_bus_3slv.qspi_master_view
        );
end entity FPGA

And at the hierarchical level containing the bundle, the views would be instantiated at this level:

entity Testbench
end entity Testbench;

architecture TB of Testbench is

        signal clk : std_logic;
        signal rst : std_logic;

        signal spi : spi_bus_3slv.spi_r;

        alias spi_to_flash : qspi_slave is qspi_slave view of spi
                generic map (IDX => 0)

begin
        DUT: entity work.FPGA
                port map (
                        clk => clk,
                        rst => rst,
                        ...
                        -- Because qspi_master is a view onto the spi_r, this
                        -- mapping is allowable.
                        spi => qspi_master view of spi
                );

        MEMORY: entity work.qspi_flash
                port map (
                        -- This mapping is where the chip select is selected
                        -- using the CS generic above.
                        spi => spi_to_flash
                );

        ADC: entity work.spi_adc
                generic map (
                        VREF => 3.0
                ) port map (
                        spi => spi_slave view of spi generic map (IDX=> 1),
                        adc => monitored_voltage
                );

        DAC: entity work.spi_dac
                generic map (
                        VREF => 3.0
                ) port map (
                        spi => spi_slave view of spi generic map (IDX=> 2),
                        aout => driven_voltage
                );

        -- And our resistive pullups.
        spi.miso <= (others => 'Z');

end architecture TB;

This is still wrong because it requires spi to be defined as a signal, but feels closer to right conceptually.

Back to bundles

One possible further refinement would be to make a view both the top-level element and the slice of the element. View is obviously the wrong keyword at that point, so we'll go back to bundle, and try defining the package all over again:

bundle spi_r is
        element (
                -- These are all implicitly signals, but could be explicitly
                -- variables, files, terminals, or other bundles.
                --

                mosi : std_logic_vector(3 downto 0);            -- Data from master to slave
                miso : std_logic_vector(3 downto 0);            -- Data from slave to master
                sclk : std_logic;                               -- Serial clock
                ssel : ssel_vst                                 -- Chip selects
        );
end bundle spi_r;

subtype qspi_master_view is spi_r
        element (
                mosi : out;
                miso : in;
                sclk : out;
                ssel : out
        );
end subtype qspi_master_view;

subtype qspi_slave is spi_r
        generic(
                IDX : ssel_rng
        );
        element(
                mosi : in;
                miso : out;
                sclk : in;
                cs   : in std_logic is ssel(IDX)
        );
end subtype qspi_slave;

subtype spi_slave is spi_r
        generic(
                IDX : ssel_rng
        );
        element(
                din  : in  std_logic is mosi(0);
                dout : out std_logic is miso(0);
                sclk : in;
                cs   : in  std_logic is ssel(IDX)
        );
end subtype spi_slave;

Now an spi_r is a bundle and every view, being a subtype, is a bundle as well, which means that the top level becomes:

architecture TB of Testbench is

        signal clk : std_logic;
        signal rst : std_logic;

        bundle spi : spi_bus_3slv.spi_r;

        alias spi_to_flash : bundle is spi(qspi_slave)
                generic map (IDX => 0);

begin
        DUT: entity work.FPGA
                port map (
                        clk => clk,
                        rst => rst,
                        ...
                        -- Because qspi_master is a view onto the spi_r, this
                        -- mapping is allowable.
                        spi => spi(qspi_master)
                );

        MEMORY: entity work.qspi_flash
                port map (
                        -- This mapping is where the chip select is selected
                        -- using the CS generic above.
                        spi => spi_to_flash
                );

        ADC: entity work.spi_adc
                generic map (
                        VREF => 3.0
                ) port map (
                        spi => spi(spi_slave) generic map (IDX => 1),
                        adc => monitored_voltage
                );

        DAC: entity work.spi_dac
                generic map (
                        VREF => 3.0
                ) port map (
                        spi => spi(spi_slave) generic map (IDX => 2),
                        aout => driven_voltage
                );

        -- And our resistive pullups.  These are not signal assignment
        -- because they go to spi, they are signal assignement because
        -- the miso element is a signal.
        spi.miso <= (others => 'Z');

end architecture TB;

- RobGaddi - 2015-09-03

Comments & Questions

Question: Generic Selection in a Named Mode View

With reference to your slave named view and the generic selection of a single chip select: what happens with the other chip select elements of the 'ssel' vector when that view is applied, i.e. what mode do they default to in the port connection of the composite vector type?

-- Brent Hayhoe - 2015-09-10

No connect. Specifically, the simpler lines in that definition are nothing other than syntactic sugar. The simple line

      sclk : in;

is actually default case shorthand for

      sclk : in std_logic is sclk;

But if it's not explicitly in the view list, it's not there.

-- RobGaddi - 2015-09-14

-- Return back to source.

Question: Mode View Equivalence to Signals, Variables & Constants

Where you equate a view instantiation syntax as needing to be in the same syntactical place as the signal/variable/constant part of the interface list structure: how do you differentiate a view of a signal with a composite type, to that of a variable with a composite type.

-- Brent Hayhoe - 2015-09-10

That's one of thing I came around to as a problem in all this. If we want to allow for bundles of composite types (and I know Ernst at least has identified a need for terminal as an option), then a bundle is by definition NOT a signal, even if it's a bundle containing only signals. This is one of the issues that needs some serious talking about, and is what I was trying to get at in the "Back to Bundles" section.

-- RobGaddi - 2015-09-14

-- Return back to source.

Comment: Generics Used for Instantiation Identification

The generic identification value is moot point. Should an instance be identified by the wiring to it, or should it be via a unique identification within the instance. Both may be valid concepts and have differing advantages.

-- Brent Hayhoe - 2015-09-10

Return back to source.

Topic revision: r13 - 2016-11-15 - 18:23:40 - BrentHahoe
 
Copyright © 2008-2019 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback