Revised SPI Example

What is SPI?

SPI refers to any number of variations on a 4 wire serial bus. The master asserts the chip select (usually active-low) and then performs a full duplex data transfer, presenting one bit at a time on MOSI (master out, slave in), accepting one bit at a time on MISO (master in, slave out), and toggling the SCLK (serial clock) to qualify the data. These names change to basically every possible variation, as does how the slave is supposed to relate MOSI, MISO, and SCLK, but the rule always remains that you only pay attention on MOSI and drive MISO when you are chip-selected. Most often, the rising edge of SCLK is used to tell the slave to 1) accept the MOSI data and 2) present a new bit of MISO data.

To connect a single master to multiple slaves, the master will support multiple chip selects. Only the device that is chip selected is active; the others ignore the transaction.

SPI is frequenly used to communicate to DACs, ADCs, EEPROMs, flash, and other fairly low bandwidth devices.

The QSPI examples have been removed from this discussion, they complicated things while providing no value.

Why SPI?

SPI is a trivial interface and commonly used. There's a bit of asymmetry between the master and slave sides of the link, but there are no complicated remappings or fancy math involved; the master has a bunch of chip-selects; each slave only sees one.

Any workable interface concept must be able to handle such a simple case. For that matter, in keeping with the design principle of "simple things should be simple", the syntactic load of designing SPI interface packages should be fairly low. If this is hard, taking on a major challenge like AXI would become mind-boggling.

Trying interfaces

I reworked the SPI example a bit to work in Lieven's latest cut at the interfaces concept. To start, here's the revised package structure.

------------------------------------------------------------------------
-- Unconstrained vectors, rather than generics, form the basis for
-- connecting together the various pieces of the puzzle.
------------------------------------------------------------------------
   
package spi_bus_pkg is

   -- Underlying types.  Masters use an unconstrained vector for
   -- chip selects; actual instantiations of the master interface
   -- will have to deal with making these concrete.

    type spi_master_r is record
        mosi : std_logic;                     -- Data from master to slave
        miso : std_logic;                     -- Data from slave to master
        sclk : std_logic;                     -- Serial clock
        ssel : std_logic_vector               -- Chip selects (active-low)
    end record spi_master_r;

    type spi_slave_r is record
        mosi : std_logic;                     -- Data from master to slave
        miso : std_logic;                     -- Data from slave to master
        sclk : std_logic;                     -- Serial clock
        ssel : std_logic;                     -- Chip select (active-low)
    end record spi_slave_r;

    port view master of spi_master_r is
        mosi => out;
        miso => in;
        sclk => out;
        ssel => out;
    end port view master;
    
    port view slave of spi_slave_r is
        mosi => in;
        miso => out;
        sclk => in;
        ssel => in;
    end port view slave;
    
end package spi_bus_pkg;

package body spi_bus_pkg is
end package body spi_bus_pkg;

------------------------------------------------------------------------
-- Using entities rather than mapfunctions to connect masters/slaves
-- to the physical wiring on the PCB, since they exist right now and
-- mapfunctions don't.
--
-- The use of the spaceship operator (<=>) below is intended to make
-- these connections without introducing delta cycles.
------------------------------------------------------------------------

-- Connect an SPI slave to an SPI bus.  Since the bus is of type
-- spi_master_r, but drives into us, we need to use the reverse of a
-- master interface (or an anti-master) on this entity, and an
-- anti-slave on the other side to connect to the slave.
--
--    FPGA              spi_slave_link        ADC
-- ----------       --------------------    ---------
--   Master |-------| Anti-      Anti- |----| Slave |
--          |       | Master     Slave |    |       |
-- ----------       --------------------    ---------
--

use work.spi_bus_pkg.all;
entity spi_slave_link is
    generic (
        CHIP_SELECT : natural
    );
    port (
        mst : bus spi_master_r(master)'reversed;
        slv : bus spi_slave_r(slave)'reversed;
    );
end entity spi_slave_link;

architecture Behavioral of spi_slave_link is
begin
    slv.mosi <=> slv.mosi;                --   <----
    slv.miso <=> slv.miso;                --   ---->
    slv.sclk <=> slv.sclk;                --   <----
    slv.ssel <=> mst.ssel(CHIP_SELECT);   --   <----
end architecture Behavioral;

This can be used to create our synthesizable code:

-- Read 16 bits at a time from 3 ADCs, present them broadside.
-- Write 16 bits to 1 DAC.

-- The spi port declaration below is the only place that the number
-- of chip selects is ever defined; everything else inherits that from
-- this single point.  SSEL(2 downto 0) are ADCs, SSEL(3) is the DAC.
--
use work.spi_bus_pkg.all;
entity spi_communicator is
    port (
        spi : bus spi_master_r(master)(ssel(3 downto 0));
        
        dac  : in  std_logic_vector(15 downto 0);
        adc0 : out std_logic_vector(15 downto 0);
        adc1 : out std_logic_vector(15 downto 0);
        adc2 : out std_logic_vector(15 downto 0);
        
        clk  : in  std_logic;
        rst  : in  std_logic
    );
end entity spi_communicator;

architecture Behavioral of spi_communicator is
begin

    MACHINE: process
        variable data : std_logic_vector(15 downto 0);
        variable bitn : integer range data'range;
        variable dev  : integer range spi_ssel'range;
        type state is (RESET, SSEL_GO, SCLK_FALL, SCLK_RISE, SSEL_STOP);
            
    begin
        wait until rising_edge(clk);
        case state is
            when RESET =>
                spi.mosi <= 'U';    
                spi.ssel <= (others => '1');
                spi.sclk <= '1';
                adc0 <= (others => 'U');
                adc1 <= (others => 'U');
                adc2 <= (others => 'U');
                state := SSEL_GO;
                dev   := 0;
                
            when SSEL_GO =>
                spi.ssel(dev) <= '0';
                state := SCLK_FALL;
                bitn  := data'high;
                
            when SCLK_FALL =>
                spi.sclk <= '0';
                state := SCLK_RISE;
                
            when SCLK_RISE =>
                spi.sclk <= '1';
                spi.mosi <= dac(bitn);
                data(bitn) := spi.miso;
                if bitn = 0 then
                    state := SSEL_STOP;
                else
                    state := SCLK_FALL;
                    bitn  := bitn - 1;
                end if;
                
            when SSEL_STOP =>
                spi.ssel <= (others => '1');
                case adc is
                    when 0 => adc0 <= data;
                    when 1 => adc1 <= data;
                    when 2 => adc2 <= data;
                    when 3 => null;
                end case;
                adc := 0 when adc = 3 else (adc + 1);
                state := SSEL_GO;
        end case;
        
        if rst then
            state := RESET;
        end if;
    end process MACHINE;
    
end architecture spi_communicator;

----------------------------------------------------------------------
-- Wrap it in an FPGA that does, presumably, other things as well.

use work.spi_bus_pkg.all;
entity FPGA is
    port(
        -- Number of chip selects here is undefined; the spi_communicator is
        -- sufficient to provide a definite size.
        spi             : bus spi_master_r(master);
--      ...
        clk             : in  std_logic;
        rst             : in  std_logic
    );
end entity FPGA

architecture Structural of FPGA is
--        ...
begin
    SPIMST: entity work.spi_communicator
        port map (
            spi =>  spi,
            adc0 => intl_adc0,
            adc1 => intl_adc1,
            adc2 => intl_adc2,
            dac  => dac,
            clk => clk,
            rst => rst
        );
--        ...
end architecture Structural;

And a testbench around it:

----------------------------------------------------------------------
-- Simulation models of ADC/DAC

use ieee.numeric_std.all;
use work.spi_bus_pkg.all;
entity adc is
    port (
        spi : bus spi_slave_r(slave);
        vin : in real;
    );
end entity adc;

architecture Behavioral of adc is
    signal intl_miso : std_logic_vector := 'Z';
begin
    FAKEIT: process
        variable data : unsigned(15 downto 0);
        variable bitn : integer range data'high downto -1;
    begin
        intl_miso <= 'Z';
        wait until falling_edge(spi.ssel);
        data := TO_UNSIGNED(vin / 5.0 * 65536, data'length);
        bitn := data'high;
        loop
            if bitn = -1 then
                intl_miso <= 'U';
            else
                intl_miso <= data(bitn);
                bitn := bitn - 1;
            end if;
            wait until rising_edge(spi.sclk) or rising_edge(spi.ssel);
            exit when spi.ssel'event;
        end loop;
    end process FAKEIT;
    
    spi.miso <= intl_miso after 10 ns;
end architecture Behavioral;

use ieee.numeric_std.all;
use work.spi_bus_pkg.all;
entity dac is
    port (
        spi : bus spi_slave_r(slave);
        vout : out real := 0.0;
    );
end entity dac;

architecture Behavioral of dac is
begin
    FAKEIT: process
        variable data : unsigned(15 downto 0);
        variable bitn : integer range data'high downto -1;
    begin
        spi.miso <= 'Z';
        wait until falling_edge(spi.ssel);
        data := (others => '0');
        loop
            wait until rising_edge(spi.sclk) or rising_edge(spi.ssel);
            exit when spi.ssel'event;
            data := data(14 downto 0) & spi.mosi;
        end loop;
        vout <= REAL(TO_INTEGER(data)) * 5.0 / 65536.0 after 10 ns;
    end process FAKEIT;
end architecture Behavioral;

----------------------------------------------------------------------
-- And the complete testbench

use work.spi_bus_pkg.all;
entity Testbench
end entity Testbench;

architecture TB of Testbench is

    signal clk : std_logic;
    signal rst : std_logic;
    -- Notice that SPI is just a normal signal here; the top level has no
    -- interest in the whole interfaces concept.  Size is defined by the FPGA,
    -- which gets it from the spi_communicator.
    signal spi : spi_master_r;
    signal daclnk : spi_slave_r;
    ...

begin
    DUT: entity work.FPGA
        port map (
            clk => clk,
            rst => rst,
            spi => spi
        );

    ADCS: for i in 0 to 2 generate
        signal lnk : spi_slave_r;
    begin
        -- Translate the master bus to a slave bus.
        RIPPER: entity spi_slave_link
            generic map (
                CHIP_SELECT => i
            ) port map (
                mst => spi,
                slv => lnk
            );
            
        -- And hook the ADC to that slave bus.
        ADC: entity work.adc
            port map (
                spi => lnk,
                vin => analog_voltage(i)
            );
    end generate ADCs;

    DACRIPPER: entity spi_slave_link
        generic map (
            CHIP_SELECT => 3
        ) port map (
            mst => spi,
            slv => daclnk
        );
            
    DAC: entity work.dac
        port map (
            spi => daclnk,
            vout => driven_voltage
        );

    -- And a resistive pullup for the tri-state MISO line.
    spi.miso <= 'H';

end architecture TB;

The use of unconstrained arrays, here used for the SPI SSEL lines, allows all this to be done without the use of generic packages, nor adding genericizers to the underlying record concept. This allows everything in the package to see everything else, and avoids the incompatibilities I had previously been concerned about.

Entities are usable as mapfunctions, without having to add anything new to the language. That said, they sure ain't elegant. A functional syntax could be substantially cleaner, as they could avoid having to create and name intermediate signals. Especially if we also do FunctionKnowsVectorSize.

-- Rob Gaddi - 2016-08-31

Comments


Topic attachments
I Attachment Action Size Date Who Comment
Unknown file formatvhd FPGA.vhd manage 3.4 K 2016-08-31 - 19:48 RobGaddi  
Unknown file formatvhd spi_bus_pkg.vhd manage 3.0 K 2016-08-31 - 19:48 RobGaddi  
Unknown file formatvhd testbench.vhd manage 3.4 K 2016-08-31 - 19:48 RobGaddi  
Topic revision: r5 - 2016-08-31 - 19:48:54 - RobGaddi
 
Copyright © 2008-2021 by the contributing authors. All material on this collaboration platform is the property of the contributing authors.
Ideas, requests, problems regarding TWiki? Send feedback