Forth can be used in a way similar to VHDL or Verilog to simulate synchronous circuits. Registers have a current and a pending value. At the clock edge, the pending values are copied to the current values by POSEDGE or NEGEDGE. Logic definitions redefine POSEDGE and NEGEDGE in such a way that when they are executed, the logic definitions are performed from newest to oldest, ending in a register update.
Here is the code to turn an ANS Forth into a hardware simulator:
DECIMAL
100 CONSTANT maxpregs \ # of pos edge regs
20 CONSTANT maxnregs \ # of neg edge regs
100 CONSTANT maxwires \ # of wires allowed in model
20 CONSTANT maxfaults \ # of faults allowed in model
: possize ( -- len ) maxpregs CELLS ; : negsize ( -- len ) maxnregs CELLS ;
: wirsize ( -- len ) maxwires CELLS ; : faultsize ( -- len ) maxfaults 4 * CELLS ;
CREATE posreg possize ALLOT posreg possize -1 fill \ current state of registers
CREATE negreg negsize ALLOT negreg negsize -1 fill
CREATE wirbuf wirsize ALLOT wirbuf wirsize -1 fill \ current
state of wires
CREATE posnext possize ALLOT posnext possize -1 fill \ state after the next pos clock
CREATE negnext negsize ALLOT negnext negsize -1 fill \ state after the next neg clock
CREATE faultb faultsize ALLOT
\ list of simulated faults
VARIABLE pregs 0 pregs ! VARIABLE nregs 0 pregs ! \ registers in the list
VARIABLE wires 0 wires !
\ wires in the list
VARIABLE faults : nofault 0 faults ! ; nofault \ faults in the model
\ Fault simulation -----------------------------------------------------------------------
: _slow ( aux addr bits -- addr slow fast bits ) >R OVER @ >R DUP @ ROT ! R> OVER @ R> ;
:noname ( aux addr bits -- ) INVERT OVER @ AND SWAP ! DROP ;
CONSTANT stuck_lo
:noname ( aux addr bits -- ) OVER @ OR SWAP ! DROP ;
CONSTANT stuck_hi
:noname ( aux addr bits -- ) _slow TUCK INVERT AND -ROT AND OR SWAP ! ; CONSTANT slow
:noname ( aux addr bits -- ) _slow ROT AND OR SWAP ! ;
CONSTANT slow_fall
:noname ( aux addr bits -- ) _slow INVERT ROT OR AND SWAP ! ;
CONSTANT slow_rise
: fault ( bits xt <name> -- )
\ define a new fault
' >BODY -ROT faults @ faultb + TUCK ! CELL+ 2!
\ mem: xt bits pfa ??
4 CELLS faults +! faults @ faultsize = ABORT" Too many faults" ;
: _fault ( type -- ) >R faults @ BEGIN DUP WHILE 4 CELLS -
DUP faultb + DUP 2 CELLS + @ CELL+ @ R@ =
\ insert wire faults of this type
IF DUP >R 3 CELLS + R@ CELL+ 2@ SWAP @ SWAP R> @ EXECUTE
ELSE DROP THEN REPEAT R> DROP DROP ;
: settle ( -- ) 0 _fault ;
: posedge ( -- ) posnext posreg pregs @ MOVE 1 _fault ; \ clock positive edge clocked regs
: negedge ( -- ) negnext negreg nregs @ MOVE 2 _fault ; \ clock negative edge clocked regs
\ Define wires and registers -------------------------------------------------------------
: w: ( <name> -- ) CREATE wires @ wirbuf + , 0 , \ wire
1 CELLS wires +! wires @ wirsize = ABORT" Too many wires"
DOES> @ @ ;
: r: ( <name> -- ) CREATE pregs @ posreg + , 1 , \ register clocked on rising edge
1 CELLS pregs +! pregs @ possize = ABORT" Too many pos registers "
DOES> @ @ ;
: nr: ( <name> -- ) CREATE nregs @ negreg + , 2 , \ register clocked on falling edge
1 CELLS nregs +! nregs @ negsize = ABORT" Too many neg registers "
DOES> @ @ ;
: => ( val <name> -- ) ' >BODY 2@ SWAP CASE
\ compile store to reg or wire
1 OF posnext + posreg - ENDOF
2 OF negnext + negreg - ENDOF ENDCASE
POSTPONE LITERAL POSTPONE ! ; IMMEDIATE
\ Miscellaneous --------------------------------------------------------------------------
: undef -1 ABORT" Undefined I/O signal detected" ;
: $ ( <name> -- ) VARIABLE ;
\ for enumerated types
: in: ( xt <name> -- ) >IN @ >R DEFER R> >IN ! \ an input signal
( exec: -- n ) POSTPONE IS ;
\ xt is an action for the signal
: ev: ( <name> -- ) >IN @ >R DEFER R> >IN ! \ an undefined event
( exec: n -- ) ['] DROP POSTPONE IS ;
: _hdstats ( a addr len -- ) CR ROT @ 1 CELLS / . TYPE ;
: hdlstats ( -- )
pregs S" positive-edge registers" _hdstats
nregs S" negative-edge registers" _hdstats
wires S" wires"
_hdstats ;
The simulator can simulate common defects in ASICs to check testbenches for fault coverage. The
following words allow wires and registers to be rendered defective in various
ways. Signals delayed by defect are late by one clock. Internal nodes such as
those in adders aren't covered, so the testbench should give adders special
treatment accordingly.
The CD16 simulator supplies some words for batch testing:
NOFAULT ( -- )
Clears the fault list (default).
FAULT ( mask xt <name> -- ) Creates a fault. <name> is a wire or register.
STUCK_LO ( -- xt )
Stuck at '0' fault.
STUCK_HI ( -- xt )
Stuck at '1' fault.
SLOW ( -- xt )
Delayed past clock fault.
SLOW_RISE ( -- xt )
Slow 0>1 transition fault.
SLOW_FALL ( -- xt )
Slow 1>0 transition fault.
The following code runs several simulations with stuck bits.
GOLDSIM ( steps -- )
Marks the results of a good simulation.
MUSTFAIL ( -- )
Runs the testbench one time expecting failure.
NOFAULT 5000 GOLDSIM
\ 5000 steps with known good hardware
NOFAULT 1 STUCK_LO FAULT W MUSTFAIL \ bit 0 of W is stuck low
NOFAULT 4 STUCK_HI FAULT W MUSTFAIL \ bit 2 of W is stuck high
NOFAULT 5 SLOW_FALL FAULT SP MUSTFAIL \ bits 2 and 0 of SP are slow to fall
NOFAULT 1 SLOW FAULT OV MUSTFAIL \ overflow bit is delayed
NOFAULT 1 SLOW_RISE FAULT predec MUSTFAIL \ predec line is slow to rise
.( Congratulations!)
\ Port inputs are words that return a bit or bit vector. A and B return testbench data.
variable sigA :noname sigA @ ; in: A \ test inputs
variable sigB :noname sigB @ ; in: B
\ port( A, B: in std_logic_vector(1 downto 0);
\ C, D: out std_logic_vector(1 downto 0));
r: C r: cd \ signal C, cd: std_logic_vector(1 downto 0); )
w: ci \ signal ci: std_logic_vector(1 downto 0); )
\ Port outputs return a bit or bit vector.
: D cd C and ; ( D <= cd and C; ) \ output
: settle A B and => ci ( ci <= A and B; ) \ wire logic
settle ;
: posedge ci => C ( C <= ci; )
\ perform one clock cycle
C => cd ( cd <= C; )
\ on register logic
posedge ;
: status ( -- ) cr ." A=" A . ." B=" B . \ status before the clock
." ci=" ci . ." C=" C . ." cd=" cd . ." D=" D . ;
: step ( A B steps -- ) -rot sigB ! sigA !
0 ?do settle status posedge negedge loop ;
: test 3 0 2 step
\ a testbench that
3 3 4 step
\ displays states on the console
0 3 3 step ;
nofault cr .( no faults ) test \ faults on ci(1):
nofault 2 stuck_lo fault ci cr .( stuck_lo fault ) test
nofault 2 stuck_hi fault ci cr .( stuck_hi fault ) test
nofault 2 slow fault ci cr .( slow wire fault ) test
nofault 2 slow_fall fault ci cr .( slow_fall fault ) test
nofault 2 slow_rise fault ci cr .( slow_rise fault ) test
SETTLE sets up logic levels on the wires. The order of assignments only matters if settled wires are needed by other equations before they are executed. A common usage is to assign wires a default value at the beginning of a SETTLE definition and let other equations (such as the ones in IF and CASE structures) make new assignments. These translate easily to VHDL or Verilog syntax.
POSEDGE sets up pending register values. The order of assignments doesn't matter. SETTLE is typically performed before POSEDGE to simulate wire logic settling before the rising clock edge.
NEGEDGE must define wire logic first if the registers require those wires for input and the logic involves the output of a positive-edge triggered register. This simulates the wires settling between the positive and negative clock edges. If fault simulation is used on such wires, the fault will be inserted late so the negative-edge triggered register won't see it. This is a rare occurence and since little if any of the logic in a typical design is negative-edge triggered, this won't be fixed.