Using Forth to Model Hardware.

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 ;


Fault Coverage

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. 

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 CD16 simulator supplies some words for batch testing:

GOLDSIM   ( steps -- )          Marks the results of a good simulation.
MUSTFAIL  ( -- )                Runs the testbench one time expecting failure.

The following code runs several simulations with stuck bits.

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!)


Example

\ 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


Caveats

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.