R
Rob Gaddi
Guest
So things have been to my mind unfortunately quiet over here for a while
now, so I figured I'd try do get something new going, just in case any
of us have gotten bored with the 118th iteration of telling some
wet-nosed pup to use numeric_std instead of std_logic_arith and
explaining what a process is. Let's actually talk about VHDL code and
the improving thereof.
So the rules of the game are simple. Someone posts their code, and we
talk about it. Code should be at the bare minimum syntactically valid,
and preferably working. But how could it have been cleaner, or more
elegant? Does it scan when you read it? What obvious low-hanging fruit
of functionality was missed? Basically, if this was code that you were
going to bring in to your project, what would you have liked to be
different about it?
Today I've got code for an SPI transmitter. The goal was to create
thing that can be kicked off by a register write and update either a
serial DAC or relay-driving shift register. It requires no
configuration from the software side, and if the software stops writing
to the register it will always ultimately set the connected thing to the
last written value.
-------------------------------------------------------------------------------
-- Title : SPI Transmitter
-- Project : T904 TEM Testset
-------------------------------------------------------------------------------
-- File : spi_transmitter.vhd
-- Author : Rob Gaddi <rgaddi@highlandtechnology.com>
-- Company : Highland Technology, Inc.
-- Created : 04-Jun-2019
-- Last update: 04-Jun-2019
-- Platform : Xilinx Zynq 7020 (uZed)
-- Standard : VHDL-2002
-------------------------------------------------------------------------------
-- Description: Sends framed words out over SPI on request.
--
-- A pulse on go sets a request flag asking the engine to run. The engine
-- runs at the next available time, which is the time at which it has been
-- idle for at least the MIN_IDLE time. At "run" time it will latch whatever
-- the current data on din is and begin the SPI transaction.
--
-- This means that, if the engine is and has been idle, the go pulse will
-- cause the data on din to start immediately. If the engine is
-- currently running, an update to din and pulse on go will cause the engine
-- to finish the transaction it is currently running, then take the new din.
-- But a second update while the engine is running will cause the first to
-- simply be ignored; the idea being that devices being communicated with by
-- this engine have only a current state rather than a history or multiple
-- current states, and that the point of the engine is to make the current
-- state correct.
--
-- idle_out represents the state of the transmitter. done goes high for a
-- single clock on the completion of the transmit.
--
-- SPI Information: Clock idle state is low. Data is transitioned on the
-- falling edge of SCLK, under the assumption that it is sampled on the rising
-- edge. Data transmits MSB first.
-------------------------------------------------------------------------------
-- Revision History:
-------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.standard_functions.all;
entity spi_transmitter is
generic (
BUS_FREQ_MHZ : integer;
SCLK_PERIOD : time;
MIN_IDLE : time := 0 ns;
CS_EXTRA : time := 0 ns;
BITS : positive
);
port (
-- Logic side
din : in std_logic_vector(BITS-1 downto 0);
go : in boolean;
idle_out : out boolean;
done : out boolean;
-- Serial side
MOSI : out std_logic;
SCLK : out std_logic;
CS_N : out std_logic;
clk : in std_logic;
rst : in std_logic
);
end entity spi_transmitter;
architecture Behavioral of spi_transmitter is
type t_state is (
IDLE, DROP_CS,
SCLK_LOW, SCLK_HIGH,
HOLD_CS
);
constant tClk : time := 1 us / BUS_FREQ_MHZ;
-- Number of clocks to spend in each of the states. For IDLE this is
-- a minimum, for everyone else it's a fixed time.
function time_to_clocks(x: time) return natural is
begin
return time_to_clocks(x, tClk);
end function time_to_clocks;
constant IDLE_LEN : natural := time_to_clocks(MIN_IDLE);
constant CSD_LEN : natural := time_to_clocks(CS_EXTRA);
constant SCLK_LEN : natural := time_to_clocks(SCLK_PERIOD);
constant SCLKH_LEN : natural := SCLK_LEN / 2;
constant SCLKL_LEN : natural := SCLK_LEN - SCLKH_LEN;
constant CSH_LEN : natural := time_to_clocks(CS_EXTRA + SCLK_PERIOD/2);
-- Sharing a single timer for all these values requires that it be large
-- enough to hold the maximum value.
function MAXTIMER return natural is
variable x : natural;
begin
x := IDLE_LEN;
if x < SCLKH_LEN then x := SCLKH_LEN; end if;
if x < CSH_LEN then x := CSH_LEN; end if;
return x;
end function MAXTIMER;
subtype t_timer is integer range 0 to MAXTIMER-1;
begin
MACHINE: process(clk)
variable go_pending: boolean;
variable sr: std_logic_vector(din'range);
variable timer: t_timer;
variable bits_left : integer range din'range;
variable state: t_state;
-- Procedure for handling simple wait states.
procedure waitandthen(clocks : in positive; nextstate : in t_state) is
constant LIMIT : natural := clocks-1;
begin
if timer = LIMIT then
timer := 0;
state := nextstate;
else
timer := timer + 1;
end if;
end procedure waitandthen;
begin
if rising_edge(clk) then
-- Manage the state machine.
done <= false;
case state is
when IDLE =>
if timer = (IDLE_LEN-1) then
if go_pending then
go_pending := false;
sr := din;
bits_left := BITS-1;
timer := 0;
-- We don't even have a DROP_CS state if we don't
-- request any extra CS time.
if CSD_LEN = 0 then
state := SCLK_LOW;
else
state := DROP_CS;
end if;
end if;
else
timer := timer + 1;
end if;
when DROP_CS => waitandthen(CSD_LEN, SCLK_LOW);
when SCLK_LOW => waitandthen(SCLKL_LEN, SCLK_HIGH);
when SCLK_HIGH =>
if timer = (SCLKH_LEN-1) then
timer := 0;
sr := sr(sr'high-1 downto 0) & 'X';
if bits_left = 0 then
state := HOLD_CS;
else
bits_left := bits_left - 1;
state := SCLK_LOW;
end if;
else
timer := timer + 1;
end if;
when HOLD_CS =>
waitandthen(CSH_LEN, IDLE);
done <= (state = IDLE);
end case;
-- A new data request always sets the go_pending flag, but a reset
-- overrides everything.
if go then
go_pending := true;
end if;
if (rst = '1') then
state := IDLE;
timer := 0;
sr := (others => 'X');
go_pending := false;
end if;
-- Drive the outputs based on the state.
SCLK <= POS_LOGIC(state = SCLK_HIGH);
CS_N <= NEG_LOGIC(state /= IDLE);
MOSI <= sr(sr'high);
idle_out <= (state = IDLE);
end if;
end process MACHINE;
end architecture Behavioral;
--
Rob Gaddi, Highland Technology -- www.highlandtechnology.com
Email address domain is currently out of order. See above to fix.
now, so I figured I'd try do get something new going, just in case any
of us have gotten bored with the 118th iteration of telling some
wet-nosed pup to use numeric_std instead of std_logic_arith and
explaining what a process is. Let's actually talk about VHDL code and
the improving thereof.
So the rules of the game are simple. Someone posts their code, and we
talk about it. Code should be at the bare minimum syntactically valid,
and preferably working. But how could it have been cleaner, or more
elegant? Does it scan when you read it? What obvious low-hanging fruit
of functionality was missed? Basically, if this was code that you were
going to bring in to your project, what would you have liked to be
different about it?
Today I've got code for an SPI transmitter. The goal was to create
thing that can be kicked off by a register write and update either a
serial DAC or relay-driving shift register. It requires no
configuration from the software side, and if the software stops writing
to the register it will always ultimately set the connected thing to the
last written value.
-------------------------------------------------------------------------------
-- Title : SPI Transmitter
-- Project : T904 TEM Testset
-------------------------------------------------------------------------------
-- File : spi_transmitter.vhd
-- Author : Rob Gaddi <rgaddi@highlandtechnology.com>
-- Company : Highland Technology, Inc.
-- Created : 04-Jun-2019
-- Last update: 04-Jun-2019
-- Platform : Xilinx Zynq 7020 (uZed)
-- Standard : VHDL-2002
-------------------------------------------------------------------------------
-- Description: Sends framed words out over SPI on request.
--
-- A pulse on go sets a request flag asking the engine to run. The engine
-- runs at the next available time, which is the time at which it has been
-- idle for at least the MIN_IDLE time. At "run" time it will latch whatever
-- the current data on din is and begin the SPI transaction.
--
-- This means that, if the engine is and has been idle, the go pulse will
-- cause the data on din to start immediately. If the engine is
-- currently running, an update to din and pulse on go will cause the engine
-- to finish the transaction it is currently running, then take the new din.
-- But a second update while the engine is running will cause the first to
-- simply be ignored; the idea being that devices being communicated with by
-- this engine have only a current state rather than a history or multiple
-- current states, and that the point of the engine is to make the current
-- state correct.
--
-- idle_out represents the state of the transmitter. done goes high for a
-- single clock on the completion of the transmit.
--
-- SPI Information: Clock idle state is low. Data is transitioned on the
-- falling edge of SCLK, under the assumption that it is sampled on the rising
-- edge. Data transmits MSB first.
-------------------------------------------------------------------------------
-- Revision History:
-------------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.standard_functions.all;
entity spi_transmitter is
generic (
BUS_FREQ_MHZ : integer;
SCLK_PERIOD : time;
MIN_IDLE : time := 0 ns;
CS_EXTRA : time := 0 ns;
BITS : positive
);
port (
-- Logic side
din : in std_logic_vector(BITS-1 downto 0);
go : in boolean;
idle_out : out boolean;
done : out boolean;
-- Serial side
MOSI : out std_logic;
SCLK : out std_logic;
CS_N : out std_logic;
clk : in std_logic;
rst : in std_logic
);
end entity spi_transmitter;
architecture Behavioral of spi_transmitter is
type t_state is (
IDLE, DROP_CS,
SCLK_LOW, SCLK_HIGH,
HOLD_CS
);
constant tClk : time := 1 us / BUS_FREQ_MHZ;
-- Number of clocks to spend in each of the states. For IDLE this is
-- a minimum, for everyone else it's a fixed time.
function time_to_clocks(x: time) return natural is
begin
return time_to_clocks(x, tClk);
end function time_to_clocks;
constant IDLE_LEN : natural := time_to_clocks(MIN_IDLE);
constant CSD_LEN : natural := time_to_clocks(CS_EXTRA);
constant SCLK_LEN : natural := time_to_clocks(SCLK_PERIOD);
constant SCLKH_LEN : natural := SCLK_LEN / 2;
constant SCLKL_LEN : natural := SCLK_LEN - SCLKH_LEN;
constant CSH_LEN : natural := time_to_clocks(CS_EXTRA + SCLK_PERIOD/2);
-- Sharing a single timer for all these values requires that it be large
-- enough to hold the maximum value.
function MAXTIMER return natural is
variable x : natural;
begin
x := IDLE_LEN;
if x < SCLKH_LEN then x := SCLKH_LEN; end if;
if x < CSH_LEN then x := CSH_LEN; end if;
return x;
end function MAXTIMER;
subtype t_timer is integer range 0 to MAXTIMER-1;
begin
MACHINE: process(clk)
variable go_pending: boolean;
variable sr: std_logic_vector(din'range);
variable timer: t_timer;
variable bits_left : integer range din'range;
variable state: t_state;
-- Procedure for handling simple wait states.
procedure waitandthen(clocks : in positive; nextstate : in t_state) is
constant LIMIT : natural := clocks-1;
begin
if timer = LIMIT then
timer := 0;
state := nextstate;
else
timer := timer + 1;
end if;
end procedure waitandthen;
begin
if rising_edge(clk) then
-- Manage the state machine.
done <= false;
case state is
when IDLE =>
if timer = (IDLE_LEN-1) then
if go_pending then
go_pending := false;
sr := din;
bits_left := BITS-1;
timer := 0;
-- We don't even have a DROP_CS state if we don't
-- request any extra CS time.
if CSD_LEN = 0 then
state := SCLK_LOW;
else
state := DROP_CS;
end if;
end if;
else
timer := timer + 1;
end if;
when DROP_CS => waitandthen(CSD_LEN, SCLK_LOW);
when SCLK_LOW => waitandthen(SCLKL_LEN, SCLK_HIGH);
when SCLK_HIGH =>
if timer = (SCLKH_LEN-1) then
timer := 0;
sr := sr(sr'high-1 downto 0) & 'X';
if bits_left = 0 then
state := HOLD_CS;
else
bits_left := bits_left - 1;
state := SCLK_LOW;
end if;
else
timer := timer + 1;
end if;
when HOLD_CS =>
waitandthen(CSH_LEN, IDLE);
done <= (state = IDLE);
end case;
-- A new data request always sets the go_pending flag, but a reset
-- overrides everything.
if go then
go_pending := true;
end if;
if (rst = '1') then
state := IDLE;
timer := 0;
sr := (others => 'X');
go_pending := false;
end if;
-- Drive the outputs based on the state.
SCLK <= POS_LOGIC(state = SCLK_HIGH);
CS_N <= NEG_LOGIC(state /= IDLE);
MOSI <= sr(sr'high);
idle_out <= (state = IDLE);
end if;
end process MACHINE;
end architecture Behavioral;
--
Rob Gaddi, Highland Technology -- www.highlandtechnology.com
Email address domain is currently out of order. See above to fix.