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.