If the "Basic CPU" and "ROMs and FSMs" tutorials had a child

While trying out the “Basic CPU” tutorial, I decided to try to implement “ROMs and FSMs” greeter/serial functionality on that CPU. Changes I made to the “Basic CPU” code are detailed below.

In alchitry_top.luc, I implemented a memory map that includes 4 addresses for USB serial soft-switches, and ROM containing a null-terminated string to print:

module alchitry_top (
    input clk,              // 100MHz clock
    input rst_n,            // reset button (active low)
    output led[8],          // 8 user controllable LEDs
    input usb_rx,           // USB->Serial input
    output usb_tx           // USB->Serial output
) {
    const TEXT = $reverse ("Hello World!\r\n")
    const NEW_RX_ADDR = 8d252
    const RX_DATA_ADDR = 8d253
    const TX_BUSY_ADDR = 8d254
    const TX_DATA_ADDR = 8d255
    
    sig rst                 // reset signal
    
    .clk(clk) {
        // The reset conditioner is used to synchronize the reset signal to the FPGA
        // clock. This ensures the entire FPGA comes out of reset at the same time.
        reset_conditioner reset_cond
        
        .rst(rst) {
            cpu cpu        // our snazzy CPU
            dff new_rx_reg
            dff rx_data_reg[8]
            #BAUD(1_000_000), #CLK_FREQ(100_000_000) {
                uart_rx rx
                uart_tx tx
            }
        }
    }
    
    always {
        reset_cond.in = ~rst_n  // input raw inverted reset signal
        rst = reset_cond.out    // conditioned reset
        
        led = 0
        
        rx.rx = usb_rx          // connect rx input
        usb_tx = tx.tx          // connect tx output
        
        cpu.din = 8hxx          // default to don't care
        
        tx.new_data = 0         // no new data by default
        tx.data = 8hxx           // don't care when new_data is 0
        
        // if data is received, save it for the cpu's next request
        if (rx.new_data) {
            new_rx_reg.d = 1
            rx_data_reg.d = rx.data
        }
        
        // map last 4 addresses to serial soft switches,
        // first addresses for string ROM,
        // and remainder as zeroes (effectively null-terminating string)
        case (cpu.address) {
            NEW_RX_ADDR:
                if (cpu.read)
                    cpu.din = new_rx_reg.q  // tell cpu if we received a char
            RX_DATA_ADDR:
                if (cpu.read) {
                    cpu.din = rx_data_reg.q // tell cpu the last char received
                    new_rx_reg.d = 0        // reset for next character
                }
            TX_BUSY_ADDR:
                if (cpu.read)
                    cpu.din = tx.busy       // tell cpu if tx is busy, assuming no other contention
            TX_DATA_ADDR:
                if (cpu.write) {
                    tx.new_data = 1
                    tx.data = cpu.dout      // send out character requested by cpu
                }
            default:
                if (cpu.read) {
                    if (cpu.address < $width(TEXT,0))
                        cpu.din = TEXT[cpu.address]
                    else
                        cpu.din = 0
                }
        }
              
    }
}

I modified assembly-file.asm to wait for an “h” from USB serial, and then send “Hello World!” back over USB serial, like the Greeter in “ROMs and FSMs”:

begin:
  SET R1, 252         // R1 = base of (NEW_RX, RX_DATA, TX_BUSY, TX_DATA)
read_loop:
  LOAD R2, R1, 0      // R2 = M[NEW_RX]
  BEQ R2, 1           // skip next if R2 == 1 (data available)
  SET R0, read_loop   // goto read_loop
  LOAD R2, R1, 1      // R2 = M[RX_DATA]
  BEQ R2, 104         // skip next if R2 == 'h' (char read)
  SET R0, read_loop   // goto read_loop
  SET R3, 0           // R3 = 0 (beginning of string)
write_loop:
  LOAD R2, R3, 0      // R2 = M[R3] (character from string)
  BNEQ R2, 0          // skip next if not null terminator
  SET R0, begin       // wait for next command
write_wait:
  LOAD R4, R1, 2      // R4 = M[TX_BUSY]
  BEQ R4, 0           // skip next if R4 == 0 (not busy)
  SET R0, write_wait  // goto write_wait
  STORE R2, R1, 3     // M[TX_DATA] = R2 (write character)
  SET R2, 1           // R2 = 1
  ADD R3, R3, R2      // R3++ (move to next char)
  SET R0, write_loop  // write next char

Assembling this resulted in this modified instRom.luc:

module instRom (
    input address[8],
    output inst[16]
  ) {

  always {
    inst = c{Inst.NOP, 12b0};

    case (address) {
      // begin:
      0: inst = c{Inst.SET, 4d1, 8d252};            // SET R1, 252
      // read_loop:
      1: inst = c{Inst.LOAD, 4d2, 4d1, 4d0};        // LOAD R2, R1, 0
      2: inst = c{Inst.BEQ, 4d2, 8d1};              // BEQ R2, 1
      3: inst = c{Inst.SET, 4d0, 8d1};              // SET R0, read_loop
      4: inst = c{Inst.LOAD, 4d2, 4d1, 4d1};        // LOAD R2, R1, 1
      5: inst = c{Inst.BEQ, 4d2, 8d104};            // BEQ R2, 104
      6: inst = c{Inst.SET, 4d0, 8d1};              // SET R0, read_loop
      7: inst = c{Inst.SET, 4d3, 8d0};              // SET R3, 0
      // write_loop:
      8: inst = c{Inst.LOAD, 4d2, 4d3, 4d0};        // LOAD R2, R3, 0
      9: inst = c{Inst.BNEQ, 4d2, 8d0};             // BNEQ R2, 0
      10: inst = c{Inst.SET, 4d0, 8d0};             // SET R0, begin
      // write_wait:
      11: inst = c{Inst.LOAD, 4d4, 4d1, 4d2};       // LOAD R4, R1, 2
      12: inst = c{Inst.BEQ, 4d4, 8d0};             // BEQ R4, 0
      13: inst = c{Inst.SET, 4d0, 8d11};            // SET R0, write_wait
      14: inst = c{Inst.STORE, 4d2, 4d1, 4d3};      // STORE R2, R1, 3
      15: inst = c{Inst.SET, 4d2, 8d1};             // SET R2, 1
      16: inst = c{Inst.ADD, 4d3, 4d3, 4d2};        // ADD R3, R3, R2
      17: inst = c{Inst.SET, 4d0, 8d8};             // SET R0, write_loop
    }
  }
}

If you make those changes, add the UART Rx and UART Tx components, and build/load, you should be able to hit “h” in the Serial Terminal to have the CPU send back “Hello World!”. I found it to be an interesting variation to ponder.