Event Driven State Machine

M

M. Norton

Okay, this is sort of a spin-off to my previous question about
bidirectionals and their behavior. This is a bit of a separate topic
though and I don't want to get them confused.

Essentially I'm looking for a semantic template for what I'm trying to
do, but I don't have a lot of experience with non-RTL construction.
Usually with RTL there's a fast clock I can use to synchronize
everything nicely. However, with this DAC, it does not have an
interior clock, and is completely driven by a SCLK input that is only
occasionally active, during
data transfers.

So I thought I'd try to model this device the way it behaves, but the
lack of a consistent clock makes things decidedly tricky (not to
mention the bidirectional port.)

Here's how I compartmentalized the behavior. I identified four main
states: idle, rx_cmd, rx_data, tx_data. Now, for each of those states
there are a number of similar behaviors that happen that I thought
would be good to break into procedures. rx_byte, tx_byte, rx_ack,
tx_ack, and wait_for_stop.

So, here's kind of how I thought the process might look. However, I'm
not sure it's going to work, so I thought I'd ask and see what folks
thought. It's not all filled in yet, but here's the basic structure.
After the code I'll ask the questions.

--
-- Reset behavior
--
reset <= '1', '0' after POR_DURATION;

DAC_FSM : process
procedure rx_byte(clk : in std_logic,
sda : in std_logic,
byte : out std_logic) is
begin
end procedure receive_byte;

procedure tx_byte(clk : in std_logic,
byte : in std_logic,
sda : out std_logic) is
begin
end procedure tx_byte;

procedure tx_ack(clk : in std_logic,
sda : out std_logic) is
end procedure tx_ack;

procedure rx_ack(clk : in std_logic,
sda : in std_logic,
ok : out boolean) is
end function rx_ack;

procedure

begin
--
-- Define behavior under reset
--
addr_reg <= (others => '0');
rw_reg <= (others => '0');
pd_reg <= (others => '0');
control_reg <= (others => '0');
sda <= 'Z';
dac_state <= IDLE;

wait until falling_edge(reset);

case dac_state is
when IDLE =>
sda <= 'Z';
wait until falling_edge(sda) and sclk = '1';
dac_state <= RX_CMD;
when RX_CMD =>
rx_byte(sclk, sda, byte);
tx_ack(sclk, sda);
if (byte(0) = '1') then
dac_state <= RX_DATA;
else
dac_state <= TX_DATA;
end if;
when RX_DATA =>

when TX_DATA =>
when others =>
null;
end case

end process;

So, some questions
1) If I suspend the process inside a procedure, when the process re-
enters, will it jump to the procedure the way it's supposed to? I
think this is true, but I need to check.
2) In that IDLE state, I select the next state based on a bit. From
what I can tell then the flow will move to end case, and exit the
process and.... a miracle happens? How can I get it to re-enter the
process to pick up on the next state? I don't have a clock to depend
on here, it's entirely based on some level transitions, so I can't put
in a "wait until rising_edge(clk)" for re-entry.
3) If it did re-enter after the idle... will it try to run those
default assignments and wait for reset again? That could be very bad.
4) Addressed in that other thread, but for inout port sda, assigning
it to Z and also checking to see if it's a value in the same region is
legal? Inouts are starting to drive me crazy and wish for some simple
RTL to design.

I think that's the main points of interest. I love the richness of
the language, but when I leave my little shelter of synthesizable
code, I feel a bit adrift at sea ;-).

Best regards,
Mark Norton
 
M

M. Norton

I actually fleshed this out a bit more and thought it might be more
illustrative than what I posted in the text above. Hopefully this
will give a little more insight (and some ideas on if I'm barking up
the wrong tree, or possible the right one).

DAC_FSM : process
procedure rx_byte(clk : in std_logic,
sda : in std_logic,
byte : out std_logic) is
begin
end procedure receive_byte;

procedure tx_byte(clk : in std_logic,
byte : in std_logic,
sda : out std_logic) is
begin
end procedure tx_byte;

procedure tx_ack(clk : in std_logic,
sda : out std_logic) is
end procedure tx_ack;

procedure rx_ack(clk : in std_logic,
sda : in std_logic,
ok : out boolean) is
end function rx_ack;

variable byte : std_logic_vector(7 downto 0) := X"00";

begin
--
-- Define behavior under reset
--
addr_reg <= (others => '0');
rw_reg <= '0';
pd_reg <= (others => '0');
control_reg <= (others => '0');
sda <= 'Z';
dac_state <= IDLE;
wait until falling_edge(reset);

case dac_state is
when IDLE =>
sda <= 'Z';
wait until falling_edge(sda) and sclk = '1';
dac_state <= RX_CMD;
when RX_CMD =>
rx_byte(sclk, sda, byte);
addr_reg <= byte(7 downto 1);
rw_reg <= byte(0);
tx_ack(sclk, sda);
if (rw_reg = '1') then
dac_state <= RX_DATA;
else
dac_state <= TX_DATA;
end if;
when RX_DATA =>
rx_byte(sclk, sda, byte);
pd_reg <= byte(5 downto 4);
control_reg(11 downto 8) <= byte(3 downto 0);
tx_ack(sclk, sda);
rx_byte(sclk, sda, byte);
control_reg(7 downto 0) <= byte;
tx_ack(sclk, sda);
wait_for_stop(sclk, sda);
dac_state <= IDLE;
when TX_DATA =>
byte := "00" & pd_reg & control_reg(11 downto 8);
tx_byte(sclk, sda, byte);
rx_ack(sclk, sda, ack);
if (not ack) then
-- report something here
end if;
byte := control_reg(7 downto 0);
if (ack) then
-- report something here
end if;
wait_for_stop(sclk, sda);
dac_state <= IDLE;
when others =>
null;
end case

end process;
 
P

Pieter Hulshoff

Interesting...
DAC_FSM : process
........
begin
........
wait until falling_edge(reset);
........
end process;

This implies that the process will only ever execute on a falling edge of the
reset. After the falling edge of the reset it will execute the code below it,
then loop around, and wait for the falling edge of the reset again. Are you sure
this is what you want? Seems to me like your dac_state will always be IDLE.

Kind regards,

Pieter Hulshoff
 
M

M. Norton

if it suspends at a WAIT is in the procedure, it will restart after the
WAIT.

Good. I ended up using this quite a lot.
And passing signals to the procedure as arguments will work nicely...
BUT if you are always connecting the same signal to the procedure AND
the procedure is defined in the process; the procedure can see the
signal and use the process's driver on the signal, so you don't need the
arguments. (If you wanted the procedure to read/write different signals
on calls in different places, you would need arguments to connect them)

I eliminated some of the procedural arguments and I think it reads
better now. Thanks for the suggestion.
But you don't have to let the process exit in the first place.
Place the case statement in an infinite loop, and check that each path
round the loop has a "Wait" somewhere. Then it won't need a sensitivity
list because re-entering it isn't an issue; it never exits, and it is
entered ONCE at simulation time 0.

Yes, that was the key to the whole puzzle really. Since the DAC is
just a lone device sitting out at the end of the bus, without a clock
of it's own (aside from the occasional serial clock) it has to operate
continuously and independently. The loop seems to do this nicely.
Now I have to write a testbench for my testbench and make sure it's
modelling the way I think it is ;-).

Here's what the final ended up being. It compiles cleanly, though I
haven't yet checked behavior.

entity dac_model is
port (
--
-- Basic 2-line interface
--
sclk : in std_logic;
sda : inout std_logic;

--
-- Reporting ports
--
addr_reg : out std_logic_vector(6 downto 0);
rw_reg : out std_logic;
pd_reg : out std_logic_vector(1 downto 0);
control_reg : out std_logic_vector(11 downto 0)
);

end dac_model;

architecture behavioral of dac_model is

type FSM_STATES is (IDLE, RX_CMD, RX_DATA, TX_DATA);

signal dac_state : FSM_STATES := IDLE;

--
-- DAC device does not have external reset pin, so
-- I will create an initial start-up time.
--
constant POR_DURATION : time := 10 ns;
signal reset : std_logic := '1';

begin

--
-- Reset behavior
--
reset <= '1', '0' after POR_DURATION;

DAC_FSM : process

procedure rx_byte(byte : out std_logic_vector(7 downto 0)) is
variable bit_cnt : integer := 0;
variable int_byte : std_logic_vector(7 downto 0) := X"00";
begin
--
-- Data is asserted during sclk=low and valid on rising
-- clock edge and held while clock is high. Using
internal
-- byte representation as cannot read from output
procedure
-- port.
--
for bit_cnt in 0 to 7 loop
wait until rising_edge(sclk);
int_byte := int_byte(6 downto 0) & sda;
end loop;
--
-- Assign output
--
byte := int_byte;
end procedure rx_byte;

procedure tx_byte(byte : in std_logic_vector(7 downto 0)) is
variable bit_cnt : integer := 0;
begin
--
-- We enter this while clock is low after
-- a falling edge, so assert immediately and then
-- wait for the next falling edge.
for bit_cnt in 7 downto 0 loop
sda <= byte(bit_cnt);
wait until falling_edge(sclk);
end loop;
--
-- End by releasing the bus.
--
sda <= 'Z';
end procedure tx_byte;

procedure tx_ack is
begin
--
-- Wait until the next clock low period,
-- and assert bus until the next clock low
-- period.
--
wait until falling_edge(sclk);
sda <= '0';
wait until falling_edge(sclk);
sda <= 'Z';
end procedure tx_ack;

procedure rx_ack(ack : out boolean) is
begin
--
-- Master will assert acknowledge during a
-- low period and will be valid during the
-- rising edge of the clock.
--
wait until rising_edge(sclk);
if (sda = '0') then
ack := true;
else
ack := false;
end if;
end procedure rx_ack;

variable byte : std_logic_vector(7 downto 0) :=
X"00";
variable ack : boolean :=
false;
-- Internal representation of registers
variable pd_reg_i : std_logic_vector(1 downto 0) :=
"00";
variable control_reg_i : std_logic_vector(11 downto 0) :=
X"000";

begin
--
-- Loop always
--
loop
case dac_state is
when IDLE =>
--
-- Reset behavior should happen here
--
if (reset = '1') then
addr_reg <= (others => '0');
rw_reg <= '0';
pd_reg <= (others => '0');
control_reg <= (others => '0');
sda <= 'Z';
dac_state <= IDLE;
end if;
--
-- Keep sda undriven for now.
--
sda <= 'Z';
--
-- Defined as I2C Master Start Sequence
--
wait until falling_edge(sda) and sclk = '1';
dac_state <= RX_CMD;

when RX_CMD =>
--
-- Receive the 8-bit command.
--
rx_byte(byte);
--
-- Parse the received byte and place
-- on output reporting ports
--
addr_reg <= byte(7 downto 1);
rw_reg <= byte(0);
--
-- Transmit the acknowledge
-- This exits after a falling edge.
--
tx_ack;
--
-- Determine which state has been
-- commanded.
-- rw_reg == high -- Master reads from DAC
-- rw_reg == low -- Master writes to DAC
-- rw_reg is byte(0) but cannot be used directly
-- since it is an output port and may not be read
from.
--
if (byte(0) = '1') then
dac_state <= TX_DATA;
else
dac_state <= RX_DATA;
end if;

when RX_DATA =>
--
-- Receive the first 8 bits.
--
rx_byte(byte);
--
-- Parse the received byte.
-- Note the two MSB's are don't care.
--
pd_reg_i := byte(5 downto 4);
control_reg_i(11 downto 8) := byte(3 downto 0);
--
-- Transmit acknowledge
--
tx_ack;
--
-- Receive the second 8 bits.
--
rx_byte(byte);
--
-- Parse the received byte.
--
control_reg_i(7 downto 0) := byte;
--
-- Transmit acknowledge
--
tx_ack;
--
-- Wait for the stop condition and then return to
-- idling. Also at this time, assert the
reporting
-- output ports.
--
wait until rising_edge(sda) and sclk = '1';
pd_reg <= pd_reg_i;
control_reg <= control_reg_i;
dac_state <= IDLE;

when TX_DATA =>
--
-- Commanded to report the contents of the control
-- register back to Master. The last acknowledge
-- left us just following the falling edge of sclk
--
-- Form up the byte to be transmitted and transmit
--
byte := "00" & pd_reg_i & control_reg_i(11 downto
8);
tx_byte(byte);
--
-- Wait for acknowledgement by Master
-- Flag error by Master if acknowledgement does
not occur.
--
rx_ack(ack);
assert ack
report "DAC Model: Master failed to
acknowledge byte read-back."
severity failure;
--
-- Form up next byte and transmit.
--
byte := control_reg_i(7 downto 0);
tx_byte(byte);
--
-- The last byte is denoted by the Master as No
Ack
--
rx_ack(ack);
assert not ack
report "DAC Model: Master failed to transmit
no-ack for 2nd byte."
severity failure;
--
-- Wait for the stop condition and return to idle.
--
wait until rising_edge(sda) and sclk = '1';
dac_state <= IDLE;

when others =>
-- Kick state back into something known.
dac_state <= IDLE;

end case;
end loop;
end process;

end behavioral;
 

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

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top