Implementation

Shows an implementation of the LC-3 instruction set in Verilog HDL. Includes test benches and simulation results.

Implementation

One dark Oregon winter afternoon, I said “Let’s build a micro processor”. What started as a noble thought became a rather intense but fun project.

This section describes the implementation of the LC-3 using a Field Programmable Logic Array. An FPGA is an array blocks with basic functionality such as Lookup table, a full adder and a flip-flop. For more information on FPGAs refer to the section Programmable Logic in the inquiry “How do computers do math?“.

The FPGA used to implement the LC-3 microprocessor is a Xilinx Spartan6, but others will fit equally well. My choice was inspired by the pricing of the development board and the fairly good free development tools. Other choices would be Altera for the FPGA, their IDE or Icarus Verilog for the synthesizer and simulator and GTKWave for the waveform viewer. Refer to the end of this article for links and references to introductory Verilog books.

Schematic

The top level schematic is shown below. The modules are defined using Verilog, an hardware description language (HDL) used to model digital logic.

LC3 schematic

This is my first Verilog implementation, please bear with me ..

State

State.v

Implementation of the LC-3 instruction set in Verilog, source file State.v:

 cCtrl,      // controller control signal
                input eREADY,           // external memory ready signal
                output wire pEn,        // update PC enable
                output wire fEn,        // fetch output enable
                output wire dEn,        // decode enable
                output wire [2:0] mOp,  // memory operation selector
                output wire rWe );      // register write enable

    `include "UpdatePC.vh"
    `include "Fetch.vh"
    `include "Decode.vh"
    `include "Registers.vh"
    `include "MemoryIF.vh"

    parameter [3:0] STATE_UPDATEPC = 4'd0,   // update program counter
                    STATE_FETCH    = 4'd1,   // fetch instruction
                    STATE_DECODE   = 4'd2,   // decode
                    STATE_ALU      = 4'd3,   // ALU
                    STATE_ADDRNPC  = 4'd4,   // calc tPC address
                    STATE_ADDRMEM  = 4'd5,   // calc memory address
                    STATE_INDMEM   = 4'd6,   // indirect memory address
                    STATE_RDMEM    = 4'd7,   // read memory
                    STATE_WRMEM    = 4'd8,   // write memory
                    STATE_WRREG    = 4'd9,   // write register
                    STATE_ILLEGAL  = 4'd15;  // illegal state

    parameter       EREADY_INA     = 1'b0,   // external memory not ready
                    EREADY_ACT     = 1'b1,   // external memory ready
                    EREADY_X       = 1'bx;

    wire [1:0] iType   = cCtrl[4:3];  // instruction type (00=alu, 01=ctrl, 10=mem)
    wire [1:0] maType  = cCtrl[2:1];  // memory access type (00=indaddr, 01=read, 02=write, 03=updreg)
    wire       indType = cCtrl[0];    // indirect memory access type

    reg [3:0] state;   // current state
    reg [3:0] nState;  // next state
    reg [6:0] out;     // current output signals
    reg [6:0] nOut;    // next output signals

    assign pEn = out[6];
    assign fEn = out[5];
    assign dEn = out[4];
    assign mOp = out[3:1];
    assign rWe = out[0];

        // the combinational logic

    always @(state, eREADY, iType, maType, indType, state, out)
        casex ({state, eREADY, iType, maType, indType})
        {STATE_UPDATEPC, EREADY_X,   ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = STATE_FETCH;    nOut = {PEN_0, FEN_1, DEN_0, MOP_NONE, RWE_0}; end
        {STATE_FETCH,    EREADY_ACT, ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = STATE_DECODE;   nOut = {PEN_0, FEN_0, DEN_1, MOP_NONE, RWE_0}; end
            {STATE_DECODE,   EREADY_X,   ITYPE_ALU, MATYPE_X,   INDTYPE_X}  : begin nState = STATE_ALU;      nOut = {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_0}; end
        {STATE_DECODE,   EREADY_X,   ITYPE_CTL, MATYPE_X,   INDTYPE_X}  : begin nState = STATE_ADDRNPC;  nOut = {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_0}; end
        {STATE_DECODE,   EREADY_X,   ITYPE_MEM, MATYPE_X,   INDTYPE_X}  : begin nState = STATE_ADDRMEM;  nOut = {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_0}; end
        {STATE_ADDRMEM,  EREADY_X,   ITYPE_X,   MATYPE_IND, INDTYPE_X}  : begin nState = STATE_INDMEM;   nOut = {PEN_0, FEN_0, DEN_0, MOP_RD,   RWE_0}; end
        {STATE_ADDRMEM,  EREADY_X,   ITYPE_X,   MATYPE_RD,  INDTYPE_X}  : begin nState = STATE_RDMEM;    nOut = {PEN_0, FEN_0, DEN_0, MOP_RD,   RWE_0}; end
        {STATE_INDMEM,   EREADY_ACT, ITYPE_X,   MATYPE_X,   INDTYPE_RD} : begin nState = STATE_RDMEM;    nOut = {PEN_0, FEN_0, DEN_0, MOP_RDI,  RWE_0}; end
        {STATE_ADDRMEM,  EREADY_X,   ITYPE_X,   MATYPE_WR,  INDTYPE_X}  : begin nState = STATE_WRMEM;    nOut = {PEN_0, FEN_0, DEN_0, MOP_WR,   RWE_0}; end
        {STATE_INDMEM,   EREADY_ACT, ITYPE_X,   MATYPE_X,   INDTYPE_WR} : begin nState = STATE_WRMEM;    nOut = {PEN_0, FEN_0, DEN_0, MOP_WR,   RWE_0}; end
        {STATE_ALU,      EREADY_X,   ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = STATE_WRREG;    nOut = {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_1}; end
        {STATE_ADDRMEM,  EREADY_X,   ITYPE_X,   MATYPE_REG, INDTYPE_X}  : begin nState = STATE_WRREG;    nOut = {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_1}; end
        {STATE_RDMEM,    EREADY_ACT, ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = STATE_WRREG;    nOut = {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_1}; end
        {STATE_WRMEM,    EREADY_ACT, ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = STATE_UPDATEPC; nOut = {PEN_1, FEN_0, DEN_0, MOP_NONE, RWE_0}; end
        {STATE_WRREG,    EREADY_X,   ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = STATE_UPDATEPC; nOut = {PEN_1, FEN_0, DEN_0, MOP_NONE, RWE_0}; end
        {STATE_ADDRNPC,  EREADY_X,   ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = STATE_UPDATEPC; nOut = {PEN_1, FEN_0, DEN_0, MOP_NONE, RWE_0}; end
        {STATE_FETCH,    EREADY_INA, ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = state;          nOut = out;                                    end
        {STATE_INDMEM,   EREADY_INA, ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = state;          nOut = out;                                    end
        {STATE_RDMEM,    EREADY_INA, ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = state;          nOut = out;                                    end
        {STATE_WRMEM,    EREADY_INA, ITYPE_X,   MATYPE_X,   INDTYPE_X}  : begin nState = state;          nOut = out;                                    end
        default                                                         : begin nState = STATE_ILLEGAL;  nOut = {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_0}; end
        endcase

        // the sequential logic

    always @(negedge clock, posedge reset)
    if (reset)
        begin
            state <= STATE_UPDATEPC;
            out <= {PEN_0, FEN_0, DEN_0, MOP_NONE, RWE_0};
        end
    else
    begin
        state <= nState;
        out <= nOut;
        end;
endmodule[/code]

Decode

Decode.vh

Implementation of the LC-3 instruction set in Verilog, header file Decode.vh: parameter DEN_0 = 1'b0, // Decode enable DEN_1 = 1'b1; parameter [1:0] ITYPE_ALU = 2'b00, // generalized instruction type ITYPE_CTL = 2'b01, ITYPE_MEM = 2'b10, ITYPE_HLT = 2'b11, ITYPE_X = 2'bxx; parameter [1:0] MATYPE_IND = 2'b00, // generalized memory access type MATYPE_RD = 2'b01, MATYPE_WR = 2'b10, MATYPE_REG = 2'b11, MATYPE_X = 2'bxx; parameter INDTYPE_WR = 1'b0, // generalized memory indirection type INDTYPE_RD = 1'b1, INDTYPE_X = 1'bx;

Decode.v

Implementation of the LC-3 instruction set in Verilog, source file Decode.v:

module Decode( input clock,
                input reset,
                input en,                   // input enable
                input [15:0] eDIN,          // external memory data input
                input [2:0] psr,            // processor status register
                output [4:0] cCtrl,         // various control signals
                output [1:0] drSrc,         // selects what to write to DR
                output [2:0] uOp,           // selecta ALU operation
                output aOp,                 // selects Address operation
                output pNext,               // selects if PC should branch
                output [2:0] sr1ID,         // source register 1 ID
                output [2:0] sr2ID,         // source register 2 ID
                output [2:0] drID,          // destination register ID
                output wire [4:0] imm,      // lower 5 bits from IR value
                output wire [8:0] offset ); // lower 9 bits from IR value

    `include "ALU.vh"
    `include "Address.vh"
    `include "MemoryIF.vh"
    `include "DrMux.vh"
    `include "UpdatePC.vh"
    `include "Decode.vh"

    parameter [2:0] ID_X = 3'bxxx;

        // Instruction Register (ir)
        // read instruction from external memory bus (after Fetch initiated the bus cycle)

    reg [15:0] ir;
    assign imm    = ir[4:0];  // output the lower 5 bits
    assign offset = ir[8:0];  // output the lower 9 bits

    always @(posedge clock, posedge reset)
    if (reset)
        ir = 16'hffff;
        else
        if (en == DEN_1)
            ir = eDIN;

    parameter [3:0]           // opcodes for the instructions
        I_BR  = 4'b0000,
        I_ADD = 4'b0001,
        I_LD  = 4'b0010,
        I_ST  = 4'b0011,
        I_AND = 4'b0101,
        I_LDR = 4'b0110,
        I_STR = 4'b0111,
        I_NOT = 4'b1001,
        I_LDI = 4'b1010,
        I_STI = 4'b1011,
        I_JMP = 4'b1100,
        I_LEA = 4'b1110,
        I_HLT = 4'b1111;

    reg [20:0] ctl;           // current control signal bundle

        // untangle control signal bundle

    assign cCtrl = ctl[ 20:16 ];  // { iType, maType, indRd }
    assign uOp   = ctl[ 15:13 ];
    assign aOp   = ctl[    12 ];
    assign drSrc = ctl[ 11:10 ];
    assign pNext = ctl[     9 ];
    assign drID  = ctl[  8: 6 ];
    assign sr1ID = ctl[  5: 3 ];
    assign sr2ID = ctl[  2: 0 ];

        // combinational logic to determine control signals

    wire [2:0] uOpAddC = (ir[5]) ? UOP_ADDIMM : UOP_ADDREG;         // candidate for uOp in case of ADD instruction
    wire [2:0] uOpAndC = (ir[5]) ? UOP_ANDIMM : UOP_ANDREG;         // candidate for uOp in case of AND instruction
    wire pNextC        = |(ir[11:9] & psr) ? PNEXT_TPC : PNEXT_NPC; // candidate for pNext in case of BR instruction

    always @(ir[15:12], uOpAddC, uOpAndC, pNextC)
                    // State      State       State       ALU      Address  DrSource    UpdatePC   Registers RegistersRegisters
    case (ir[15:12])// iType      maType      indType     uOp      aOp      drSrc       pNext      drID      sr1ID    sr2ID
        I_ADD   : ctl = {ITYPE_ALU, MATYPE_X,   INDTYPE_X,  uOpAddC, AOP_X,   DRSRC_ALU,  PNEXT_NPC, ir[11:9], ir[8:6], ir[2:0] };
        I_AND   : ctl = {ITYPE_ALU, MATYPE_X,   INDTYPE_X,  uOpAndC, AOP_X,   DRSRC_ALU,  PNEXT_NPC, ir[11:9], ir[8:6], ir[2:0] };
        I_NOT   : ctl = {ITYPE_ALU, MATYPE_X,   INDTYPE_X,  UOP_NOT, AOP_X,   DRSRC_ALU,  PNEXT_NPC, ir[11:9], ir[8:6], ID_X    };
        I_BR    : ctl = {ITYPE_CTL, MATYPE_X,   INDTYPE_X,  UOP_X,   AOP_NPC, DRSRC_X,    pNextC,    ID_X,     ID_X,    ID_X    };
        I_JMP   : ctl = {ITYPE_CTL, MATYPE_X,   INDTYPE_X,  UOP_X,   AOP_SR1, DRSRC_X,    PNEXT_TPC, ID_X,     ir[8:6], ID_X    };
        I_LD    : ctl = {ITYPE_MEM, MATYPE_RD,  INDTYPE_X,  UOP_X,   AOP_NPC, DRSRC_MEM,  PNEXT_NPC, ir[11:9], ID_X,    ID_X    };
        I_LDR   : ctl = {ITYPE_MEM, MATYPE_RD,  INDTYPE_X,  UOP_X,   AOP_SR1, DRSRC_MEM,  PNEXT_NPC, ir[11:9], ir[8:6], ID_X    };
        I_LDI   : ctl = {ITYPE_MEM, MATYPE_IND, INDTYPE_RD, UOP_X,   AOP_NPC, DRSRC_MEM,  PNEXT_NPC, ir[11:9], ID_X,    ID_X    };
        I_LEA   : ctl = {ITYPE_MEM, MATYPE_REG, INDTYPE_X,  UOP_X,   AOP_NPC, DRSRC_ADDR, PNEXT_NPC, ir[11:9], ID_X,    ID_X    };
        I_ST    : ctl = {ITYPE_MEM, MATYPE_WR,  INDTYPE_X,  UOP_X,   AOP_NPC, DRSRC_X,    PNEXT_NPC, ID_X,     ID_X,    ir[11:9]};
        I_STR   : ctl = {ITYPE_MEM, MATYPE_WR,  INDTYPE_X,  UOP_X,   AOP_SR1, DRSRC_X,    PNEXT_NPC, ID_X,     ir[8:6], ir[11:9]};
        I_STI   : ctl = {ITYPE_MEM, MATYPE_IND, INDTYPE_WR, UOP_X,   AOP_NPC, DRSRC_X,    PNEXT_NPC, ID_X,     ID_X,    ir[11:9]};
        default : ctl = {ITYPE_HLT, MATYPE_X,   INDTYPE_X,  UOP_X,   AOP_X,   DRSRC_X,    PNEXT_X,   ID_X,     ID_X,    ID_X    };
    endcase 

endmodule

UpdatePC

UpdatePC.vh

Implementation of the LC-3 instruction set in Verilog, header file UpdatePC.vh:

parameter PNEXT_NPC  = 1'b0,  // UpdatePC branch signal
            PNEXT_TPC  = 1'b1,
            PNEXT_X    = 1'bx;

parameter PEN_0      = 1'b0,  // UpdatePC enable
            PEN_1      = 1'b1;

UpdatePC.v

Implementation of the LC-3 instruction set in Verilog, source file UpdatePC.v:

module UpdatePC( input clock,
                    input reset,
                    input en,                 // enable signal
                    input [15:0] tPC,         // target program counter
                    input pNext,              // if 1 then branch to tPC
                    output reg [15:0] pc,     // program counter
                    output reg [15:0] nPC );  // next program counter (pc+1)

    `include "UpdatePC.vh"

    wire [15:0] a = (pNext) ? tPC : nPC;     // if pNext==1, then jump to tPC
    wire [15:0] b = (en == PEN_1) ? a : pc;  // change PC only in "Update PC" state
    wire [15:0] c = b + 1'b1;                // use carry input

    always @(posedge clock, posedge reset)
    if (reset)
        begin
            pc  <= 16'h3000;
            nPC <= 16'h3001;
        end
    else
        begin
        pc  <= b;
        nPC <= c;
        end;

endmodule[/code]

Fetch

Fetch.vh

Implementation of the LC-3 instruction set in Verilog, header file Fetch.vh: parameter FEN_0 = 1'b0, // fetch enable FEN_1 = 1'b1;

Fetch.v

Implementation of the LC-3 instruction set in Verilog, source file Fetch.v:

module Fetch( input en,                 // output enable
                input [15:0] pc,          // program counter
                    output reg iBR,           // internal memory address lines
                    output reg [15:0] iADDR,  // internal memory address lines
                output reg iWEA );        // internal memory write enable

    `include "Fetch.vh"

    always @(en, pc)
    begin
        iBR   <= ( en == FEN_1 ) ? 1'b1 : 1'b0;
        iADDR <= ( en == FEN_1 ) ? pc : 16'hxxxx;
        iWEA  <= ( en == FEN_1 ) ? 1'b0 : 1'bx;
    end

endmodule[/code]

Registers

Registers.vh

Implementation of the LC-3 instruction set in Verilog, header file Registers.vh: parameter RWE_0 = 1'b0, // register write enable RWE_1 = 1'b1; parameter [2:0] PSR_POSITIVE = 3'b001, // processor status register bits PSR_ZERO = 3'b010, // should match BR instruction PSR_NEGATIVE = 3'b100;

Registers.v

Implementation of the LC-3 instruction set in Verilog, source file Registers.v:

module Registers( input clock,
                  input reset,
                  input we,                // write enable
                  input [2:0] sr1ID,       // source register 1 ID
                  input [2:0] sr2ID,       // source register 2 ID
                  input [2:0] drID,        // destination register ID
                  input [15:0] dr,         // destination register value
                  output reg [15:0] sr1,   // source register 1 value
                  output reg [15:0] sr2,   // source register 2 value
                  output reg [2:0] psr );  // processor status register

    `include "Registers.vh"

    reg [3:0] id;
    reg [15:0] gpr [0:7];     // general purpose registers

        // write the destination register value, and update Process Status Register (psr)

    always @(posedge clock, posedge reset)
    if (reset)
        for (id = 0; id < 7; id = id + 1)  // initial all registers to 0
        gpr[ id ] <= 16'h0000;
    else
        if (we == RWE_1)          // when enabled by the FSM
            begin
            if (dr[ 15 ])         // update processor status register (neg,zero,pos)
            psr <= PSR_NEGATIVE;
            else if (|dr)
            psr <= PSR_POSITIVE;
            else
            psr <= PSR_ZERO;

                gpr[ drID ] <= dr;     // write the value dr to the register identified by drID
        end

        // output the value of the register identified by "sr1ID" on output "sr1"
        // output the value of the register identified by "sr2ID" on output "sr2"

    always @(sr1ID, sr2ID, gpr[ sr1ID ], gpr[ sr2ID ])
    begin
        sr1 = gpr[ sr1ID ];
        sr2 = gpr[ sr2ID ];
    end

endmodule[/code]

ALU

ALU.vh

parameter [2:0] UOP_ADDREG = 3'b000, // ALU operation UOP_ADDIMM = 3'b001, UOP_ANDREG = 3'b010, UOP_ANDIMM = 3'b011, UOP_NOT = 3'b100, UOP_X = 3'bxxx;

ALU.v

module ALU( input [2:0] uOp,          // operation selector
            input [15:0] sr1,         // source register 1 value (SR1)
            input [15:0] sr2,         // source register 2 value (SR2)
            input [4:0] imm,          // lower 5 bits from instruction register
                output reg [15:0] uOut ); // result of ALU operation

    `include "ALU.vh"

    wire [15:0] imm5 = ({ {11{imm[4]}}, imm[4:0] });  // sign extend to 16 bits

    always @(uOp or sr1 or sr2 or imm5)
    casex (uOp)
        3'b000: uOut = sr1 + sr2;   // ADD Mode 0
        3'b001: uOut = sr1 + imm5;  // ADD Mode 1
        3'b010: uOut = sr1 & sr2;   // AND Mode 0
        3'b011: uOut = sr1 & imm5;  // AND Mode 1
        3'b1xx: uOut = ~(sr1);      // NOT
    endcase
endmodule

Address

Address.vh

parameter AOP_SR1 = 1'b0,  // address operation
          AOP_NPC = 1'b1,
          AOP_X   = 1'bx;

Address.v

module Address( input aOp,                // operation selector
                input [15:0] sr1,         // value source register 1
                input [15:0] nPC,         // next program counter (PC), always PC+1
                input [8:0] offset,       // lower 9 bits from instruction register
                output reg [15:0] aOut ); // target program counter

    `include "Address.vh"

    wire [15:0] offset6 = ({{10{offset[5]}}, offset[5:0]});  // sign extended the 6-bit offset
    wire [15:0] offset9 = ({{ 7{offset[8]}}, offset[8:0]});  // sign extended the 9-bit offset

    always @(aOp or sr1 or nPC or offset6 or offset9)
    case (aOp)
        AOP_SR1 : aOut = sr1 + offset6;  // register + offset
        AOP_NPC : aOut = nPC + offset9;  // next PC  + offset
    endcase

endmodule

MemoryIF

MemoryIF.vh

parameter [2:0] MOP_NONE = 3'b000,  // MemoryIF operation
                MOP_RD   = 3'b100,
                MOP_RDI  = 3'b101,
                MOP_WR   = 3'b110,
                MOP_WRI  = 3'b111;

MemoryIF.v

module MemoryIF( input [2:0] mOp,         // memory operation selector
                 input [15:0] sr2,        // source register 2 value
                 input [15:0] addr,       // address for read or write
                 input [15:0] eDIN,       // external memory data input
                 output reg iBR,          // internal bus request
                 output reg [15:0] iADDR, // internal memory address lines
                 output tri [15:0] eDOUT, // internal memory data output
                 output reg iWEA );       // internal memory write enable

    `include "MemoryIF.vh"

    reg [15:0] eDOUTr;
    assign eDOUT = eDOUTr;

    always @(mOp, sr2, addr, eDIN)
    case (mOp)
        MOP_RD  : begin iBR=1; iWEA = 1'b0; iADDR = addr;     eDOUTr = 16'hzzzz; end
        MOP_RDI : begin iBR=1; iWEA = 1'b0; iADDR = eDIN;     eDOUTr = 16'hzzzz; end
        MOP_WR  : begin iBR=1; iWEA = 1'b1; iADDR = addr;     eDOUTr = sr2;      end
        MOP_WRI : begin iBR=1; iWEA = 1'b1; iADDR = eDIN;     eDOUTr = sr2;      end
        default : begin iBR=0; iWEA = 1'bx; iADDR = 16'hxxxx; eDOUTr = 16'hzzzz; end
    endcase

endmodule

DrMux

DrMux.vh

parameter [1:0] DRSRC_ALU  = 2'b00,  // destination register source selector
                DRSRC_MEM  = 2'b01,
                DRSRC_ADDR = 2'b10,
                DRSRC_X    = 2'bxx;

DrMux.v

module DrMux( input [1:0] drSrc,       // multiplexor selector
              input [15:0] eDIN,       // external memory data input
              input [15:0] addr,       // effective memory address
              input [15:0] uOut,       // result from ALU
              output reg [15:0] dr );  // data that will be stored in DR

    `include "DrMux.vh"

    always @(drSrc or uOut or eDIN or addr)
    case (drSrc)
        DRSRC_ALU  : dr = uOut;
        DRSRC_MEM  : dr = eDIN;
        DRSRC_ADDR : dr = addr;
        default    : dr = 16'hxxxx;
    endcase

endmodule:

BusDriver

BusDriver.v

module BusDriver( input br0,                // input 0, bus request
                    input [15:0] iADDR0,      // input 0, internal memory address lines
                        input iWEA0,              // input 0, internal memory write enable
                    input br1,                // input 1, bus request
                    input [15:0] iADDR1,      // input 1, internal memory address lines
                        input iWEA1,              // input 1, internal memory write enable
                    output tri [15:0] eADDR,  // external memory address lines
                        output tri  eWEA );       // external memory write enable

    assign eWEA  = br1 ? iWEA1 :
                    br0 ? iWEA0 : 1'bz;

    assign eADDR = br1 ? iADDR1 :
                    br0 ? iADDR0 : 16'hzzzz;

endmodule

Functional simulation

The functionality of our microprocessor can be tested by building a test bench. The bench will supply the clock signal and reset pulse and simulate a random access memory (RAM) containing the test program. The program is written using in a assembly language and compiled using LC3Edit.

Test program

Exercises a variety of instructions:

  • memory read
  • alu
  • memory write
  • control instructions

Written in assembly language

own work
LC3 memory asm

LC3Edit compiles this into the object file:

own work
LC3 memory obj

As part of the compilation, LC3Edit also creates a .hex file. The contents of this file can be tweaked into a .coe file to be preloaded in the test bench memory.

own work
LC3 Memory coe

Memory

The random access memory (RAM) is created using Xilinx IP's Block Memory Generator 6.2. The following parameters are used:

  • native i/f, single port ram, no byte write enable, minimum area algorithm, width 16, depth 4096, write first, use ENA pin, no output registers, no RSTA pin, enable all warnings. Initialize memory from the .coe file.

Test bench and clock/reset signals

Generate a 50 MHz symmetric clock.

Integrate the parts into a test bench using Verilog. `timescale 1ns / 1ps module SimpleLC3_SimpleLC3_sch_tb(); reg clock; // clock (generated by test fixture) reg reset; // reset (generated by test fixture) wire [15:0] eADDR; // external address (from LC3 to memory) wire [15:0] eDIN; // external data (from memory to LC3) wire [15:0] eDOUT; // external data (from LC3 to memory) wire eWEA; // external write(~read) enable (from LC3 to memory) // Instantiate the Unit Under Test SimpleLC3 UUT ( .eDOUT(eDOUT), .eWEA(eWEA), .clock(clock), .eREADY(1'b1), // ready (always 1, for now) .eDIN(eDIN), .reset(reset), .eADDR(eADDR) ); // Instantiate the Memory, created using Xilinx IP's Block Memory Generator 6.2: // Initialize from memory.coe, created from compiling memory.asm using LC3Edit. memory RAM( .clka(clock), .ena(eENA), .wea(eWEA), .addra(eADDR[11:0]), .dina(eDOUT), .douta(eDIN) ); wire eENA = |(eADDR[15:12] == 4'h3); // memory is at h3xxx initial begin clock = 0; reset = 0; #15 reset = 1; // wait for global reset to finish #22 reset = 0; end always #10 clock <= ~clock; // 20 ns clock period (50 MHz) endmodule;[/code]

Simulation results

For the functional simulation we use ISim that comes bundled with the Xilinx IDE.

The simulation needs to be ran for 1600 ns.

Waveform diagrams are shown below (click to enlarge)

LC3 Memory load
LC3 Memory load
LC3 ALU operations
LC3 ALU operations
LC3 Memory store
LC3 Memory store
LC3 Control instructions
LC3 Control instructions

Timing simulation

The free Xilinx IDE doesn't support timing simulations. Instead we will use Icarus Verilog for the synthesis and simulation, GTKWave for viewing the generated waveforms, and Emacs verilog-mode for editing. We will run them natively under Linux. For those interested, Windows binaries are available from bleyer.org.

This concludes the "Implementation of the LC-3 instruction set in Verilog".