3.2.3 Finite State Machine modeling

Finite State Machine (FSM) modeling is very common in RTL design and therefore deserves special attention.

For code clarity, the state values are typically represented by a set of identifiers. A standard Python idiom for this purpose is to assign a range of integers to a tuple of identifiers, like so:

>>> SEARCH, CONFIRM, SYNC = range(3)
>>> CONFIRM
1

However, this technique has some drawbacks. Though it is clearly the intention that the identifiers belong together, this information is lost as soon as they are defined. Also, the identifiers evaluate to integers, whereas a string representation of the identifiers would be preferable. To solve these issues, we need an enumeration type.

MyHDL supports enumeration types by providing a function enum(). The arguments to enum() are the string representations of the identifiers, and its return value is an enumeration type. The identifiers are available as attributes of the type. For example:

>>> from myhdl import enum
>>> t_State = enum('SEARCH', 'CONFIRM', 'SYNC')
>>> t_State
<Enum: SEARCH, CONFIRM, SYNC>
>>> t_State.CONFIRM
CONFIRM

We can use this type to construct a state signal as follows:

state = Signal(t_State.SEARCH)

As an example, we will use a framing controller FSM. It is an imaginary example, but similar control structures are often found in telecommunication applications. Suppose that we need to find the Start Of Frame (SOF) position of an incoming frame of bytes. A sync pattern detector continuously looks for a framing pattern and indicates it to the FSM with a syncFlag signal. When found, the FSM moves from the initial SEARCH state to the CONFIRM state. When the syncFlag is confirmed on the expected position, the FSM declares SYNC, otherwise it falls back to the SEARCH state. This FSM can be coded as follows:

from myhdl import *

ACTIVE_LOW = 0
FRAME_SIZE = 8
t_State = enum('SEARCH', 'CONFIRM', 'SYNC')

def FramerCtrl(SOF, state, syncFlag, clk, reset_n):
    
    """ Framing control FSM.

    SOF -- start-of-frame output bit
    state -- FramerState output
    syncFlag -- sync pattern found indication input
    clk -- clock input
    reset_n -- active low reset
    
    """
    
    index = Signal(0) # position in frame

    @always(clk.posedge, reset_n.negedge)
    def FSM():
        if reset_n == ACTIVE_LOW:
            SOF.next = 0
            index.next = 0
            state.next = t_State.SEARCH

        else:
            index.next = (index + 1) % FRAME_SIZE
            SOF.next = 0

            if state == t_State.SEARCH:
                index.next = 1
                if syncFlag:
                    state.next = t_State.CONFIRM

            elif state == t_State.CONFIRM:
                if index == 0:
                    if syncFlag:
                        state.next = t_State.SYNC
                    else:
                        state.next = t_State.SEARCH

            elif state == t_State.SYNC:
                if index == 0:
                    if not syncFlag:
                        state.next = t_State.SEARCH
                SOF.next = (index == FRAME_SIZE-1)

            else:
                raise ValueError("Undefined state")

    return FSM

At this point, we will use the example to demonstrate the MyHDL support for  waveform viewing. During simulation, signal changes can be written to a VCD output file. The VCD file can then be loaded and viewed in a waveform viewer tool such as gtkwave.

The user interface of this feature consists of a single function, traceSignals(). To explain how it works, recall that in MyHDL, an instance is created by assigning the result of a function call to an instance name. For example:

tb_fsm = testbench()

To enable VCD tracing, the instance should be created as follows instead:

tb_fsm = traceSignals(testbench)

Note that the first argument of traceSignals() consists of the uncalled function. By calling the function under its control, traceSignals() gathers information about the hierarchy and the signals to be traced. In addition to a function argument, traceSignals() accepts an arbitrary number of non-keyword and keyword arguments that will be passed to the function call.

A small test bench for our framing controller example, with signal tracing enabled, is shown below:

def testbench():

    SOF = Signal(bool(0))
    syncFlag = Signal(bool(0))
    clk = Signal(bool(0))
    reset_n = Signal(bool(1))
    state = Signal(t_State.SEARCH)
            
    framectrl = FramerCtrl(SOF, state, syncFlag, clk, reset_n)

    @always(delay(10))
    def clkgen():
        clk.next = not clk

    @instance
    def stimulus():
        for i in range(3):
            yield clk.posedge
        for n in (12, 8, 8, 4):
            syncFlag.next = 1
            yield clk.posedge
            syncFlag.next = 0
            for i in range(n-1):
                yield clk.posedge
        raise StopSimulation
        
    return framectrl, clkgen, stimulus


tb_fsm = traceSignals(testbench)
sim = Simulation(tb_fsm)
sim.run()

When we run the test bench, it generates a VCD file called testbench.vcd. When we load this file into gtkwave, we can view the waveforms:

Image tbfsm

Signals are dumped in a suitable format. This format is inferred at the Signal construction time, from the type of the initial value. In particular, bool signals are dumped as single bits. (This only works starting with Python 2.3, when bool has become a separate type). Likewise, intbv signals with a defined bit width are dumped as bit vectors. To support the general case, other types of signals are dumped as a string representation, as returned by the standard str() function.

Warning: Support for literal string representations is not part of the VCD standard. It is specific to gtkwave. To generate a standard VCD file, you need to use signals with a defined bit width only.

About this document