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