In this tutorial we will be personalizing the greeter so that it first asks for your name and then prints "Hello NAME" where NAME is the name you entered. To do this we will need some form of memory and in this case we will use a single port RAM.

We will be continuing the project from the last tutorial so make sure you have read the ROMs and FSMs tutorial first.

With the project open from the last tutorial, you can make a copy to edit for this tutorial by going to Alchitry Icon->Save Project As.... Enter a new name in the dialog that pops up and click Create Project.

The RAM

We need to add the RAM component to our project. Open the Component Library and under Memory check off Simple RAM.

Go ahead and open up the simple_ram file.

module simple_ram #(
    parameter WIDTH = 1,                 // size of each entry
    parameter ENTRIES = 1                // number of entries
  )(
    input clk,                           // clock
    input [$clog2(ENTRIES)-1:0] address, // address to read or write
    output reg [WIDTH-1:0] read_data,    // data read
    input [WIDTH-1:0] write_data,        // data to write
    input write_enable                   // write enable (1 = write)
  );

  reg [WIDTH-1:0] ram [ENTRIES-1:0];     // memory array

  always @(posedge clk) begin
    read_data <= ram[address];           // read the entry

    if (write_enable)                    // if we need to write
      ram[address] <= write_data;        // update that value
  end

endmodule

Note that this component is written in Verilog instead of Lucid. This is because the tools that actually build your project can be very picky when it comes to deciding if something is a block of RAM or not. By using this module we can ensure that our RAM is properly recognized as RAM. This is important because FPGAs actually have dedicated block RAM (also known as BRAM). If your RAM is big enough, the tools will use BRAM to implement it instead of the FPGA fabric. Using BRAM is both substantially faster and smaller than the FPGA fabric.

A single port RAM like this works much the same as the ROM from the last tutorial. However, we now have the option to write to an address instead of only reading. To write to an address, we simply supply the address and data to write then set write_enable to 1. The data at that address will then be updated to whatever write_data is.

The parameters WIDTH and ENTRIES are used to specify how big we want the RAM to be. WIDTH specifies how big each entry is. In our case we will be storing letters and a letter is 8 bits wide so WIDTH will be set to 8. ENTRIES is used to specify how many entries we want. This will be the maximum name length we can accept.

The Greeter (revisited)

Just like the last tutorial we will have a greeter module. The interface to this module is exactly the same as before but it is now a bit more mannered and will greet you personally.

Like most tutorials, I'll post the entire module here and then break it down.

1module greeter (
2 input clk, // clock
3 input rst, // reset
4 input new_rx, // new RX flag
5 input rx_data[8], // RX data
6 output new_tx, // new TX flag
7 output tx_data[8], // TX data
8 input tx_busy // TX is busy flag
9) {
10 const HELLO_TEXT = $reverse("\r\nHello @!\r\n")
11 const PROMPT_TEXT = $reverse("Please type your name: ")
12
13 enum States {IDLE, PROMPT, LISTEN, HELLO} // our state machine
14
15 .clk(clk) {
16 .rst(rst) {
17 dff state[$width(States)]
18 }
19 // we need our counters to be large enough to store all the indices of our text
20 dff hello_count[$clog2($width(HELLO_TEXT, 0))]
21 dff prompt_count[$clog2($width(PROMPT_TEXT, 0))]
22
23 dff name_count[5] // 5 allows for 2^5 = 32 letters
24 // we need our RAM to have an entry for every value of name_count
25 simple_ram ram (#WIDTH(8), #ENTRIES($pow(2,$width(name_count.q))))
26 }
27
28 always {
29 ram.address = name_count.q // use name_count as the address
30 ram.write_data = 8hxx // don't care
31 ram.write_enable = 0 // read by default
32
33 new_tx = 0 // default to no new data
34 tx_data = 8hxx // don't care
35
36 case (state.q) { // our FSM
37 // IDLE: Reset everything and wait for a new byte.
38 States.IDLE:
39 hello_count.d = 0
40 prompt_count.d = 0
41 name_count.d = 0
42 if (new_rx) // wait for any letter
43 state.d = States.PROMPT
44
45 // PROMPT: Print out name prompt.
46 States.PROMPT:
47 if (!tx_busy) {
48 prompt_count.d = prompt_count.q + 1 // move to the next letter
49 new_tx = 1 // send data
50 tx_data = PROMPT_TEXT[prompt_count.q] // send letter from PROMPT_TEXT
51 if (prompt_count.q == $width(PROMPT_TEXT, 0) - 1) // no more letters
52 state.d = States.LISTEN // change states
53 }
54
55 // LISTEN: Listen to the user as they type his/her name.
56 States.LISTEN:
57 if (new_rx) { // wait for a new byte
58 ram.write_data = rx_data // write the received letter to RAM
59 ram.write_enable = 1 // signal we want to write
60 name_count.d = name_count.q + 1 // increment the address
61
62 // We aren't checking tx_busy here that means if someone types super
63 // fast we could drop bytes. In practice this doesn't happen.
64 new_tx = rx_data != "\n" && rx_data != "\r" // only echo non-new line characters
65 tx_data = rx_data // echo text back so you can see what you type
66
67 // if we run out of space or they pressed enter
68 if (&name_count.q || rx_data == "\n" || rx_data == "\r") {
69 state.d = States.HELLO
70 name_count.d = 0 // reset name_count
71 }
72 }
73
74 // HELLO: Prints the hello text with the given name inserted
75 States.HELLO:
76 if (!tx_busy) { // wait for tx to not be busy
77 if (HELLO_TEXT[hello_count.q] != "@") { // if we are not at the sentry
78 hello_count.d = hello_count.q + 1 // increment to next letter
79 new_tx = 1 // new data to send
80 tx_data = HELLO_TEXT[hello_count.q] // send the letter
81 } else { // we are at the sentry
82 name_count.d = name_count.q + 1 // increment the name_count letter
83
84 if (ram.read_data != "\n" && ram.read_data != "\r") // if we are not at the end
85 new_tx = 1 // send data
86
87 tx_data = ram.read_data // send the letter from the RAM
88
89 // if we are at the end of the name or out of letters to send
90 if (ram.read_data == "\n" || ram.read_data == "\r" || &name_count.q) {
91 hello_count.d = hello_count.q + 1 // increment hello_count to pass the sentry
92 }
93 }
94
95 // if we have sent all of HELLO_TEXT
96 if (hello_count.q == $width(HELLO_TEXT, 0) - 1)
97 state.d = States.IDLE // return to IDLE
98 }
99 }
100 }
101}

No More ROM

So unlike last tutorial, we aren't going to use an explicit ROM module. This is because some convenient features of Lucid allow us to easily use constants with strings as ROMs. Let us take a look at the constant declaration.

10const HELLO_TEXT = $reverse("\r\nHello @!\r\n") // reverse so index 0 is the left most letter
11const PROMPT_TEXT = $reverse("Please type your name: ")

Here we are using a function called $reverse(). This function takes a constant expression and reverses the order of the top most dimension of the array. Since strings are 2D arrays with the top most dimension being the letter order, this is exactly the same as typing the string backwards like we did in the last tutorial. This is just a little bit cleaner and easier to deal with.

Because strings are 2D arrays, we can simply use HELLO_TEXT[i] to access the ith letter of it.

Note that we are using the @ symbol in place of a name. This will signal to our design where to insert the name that was recorded.

Modules and DFFs

Just like before we have an FSM state. This will store the current state of our module. States.IDLE is where we will start, and it will initialize everything. States.PROMPT will print the prompt asking for your name. States.LISTEN will listen to you type your name and echo it back. Finally, States.HELLO will greet you personally.

We need counters to keep track of what letter in each ROM we are currently positioned.

20dff hello_count[$clog2($width(HELLO_TEXT, 0))]
21dff prompt_count[$clog2($width(PROMPT_TEXT, 0))]

Let us take a look at hello_count. We need it to be wide enough so that we can index all the letters in HELLO_TEXT. We can get how many letters there are in the string by using the $width() function.

The $width(expr, dim) function takes two arguments. expr is the value to get width of and dim is the dimension along we're measuring. If expr is a 1D array or bit, then dim is optional and assumed to be 0.

Because HELLO_TEXT is a multidimensional array, we need to specify dim explicitly as 0 to get the outermost dimension. With dim as 0, $width() will return the number of letters. If it was 1 instead, $width() would return 8 since each letter is 8 bits wide.

We can then use the $clog2() function as before to make sure it is large enough to store values from 0 to $width(HELLO_TEXT, 0)-1.

Next take a look at name_count. This will be used to index into the RAM. We can set this width to be whatever we want, but the size of the RAM will grow exponentially with it. I set it to 5 which will allow for a name of 25, or 32 letters long. We will play with this towards the end of the tutorial.

We need the size of the RAM to match the size of name_count.

25simple_ram ram (#WIDTH(8), #ENTRIES($pow(2,$width(name_count.q))))

Here we are using the function $pow() which takes two constants and returns the first to the power of the second. In this case, $width(name_count.q) is 5, so 25 is 32. By using $width(name_count.q) instead of typing in 5 or 32 directly, we ensure that if we change the width of name_count then everything will still work.

The FSM

The IDLE and PROMPT states should look very familiar to the last tutorial so we will jump to the LISTEN state.

55// LISTEN: Listen to the user as they type his/her name.
56States.LISTEN:
57 if (new_rx) { // wait for a new byte
58 ram.write_data = rx_data // write the received letter to RAM
59 ram.write_enable = 1 // signal we want to write
60 name_count.d = name_count.q + 1 // increment the address
61
62 // We aren't checking tx_busy here that means if someone types super
63 // fast we could drop bytes. In practice this doesn't happen.
64 new_tx = rx_data != "\n" && rx_data != "\r" // only echo non-new line characters
65 tx_data = rx_data // echo text back so you can see what you type
66
67 // if we run out of space or they pressed enter
68 if (&name_count.q || rx_data == "\n" || rx_data == "\r") {
69 state.d = States.HELLO
70 name_count.d = 0 // reset name_count
71 }
72 }

Here we wait until new_rx is 1. This signals that we have a new byte to process and that the data on rx_data is valid. We then write rx_data into our RAM. We are writing to the address specified by name_count.q as ram.address is set to this in the beginning of the always block.

We also need to send the character we received back so that you can see your name as you type it. We simply set new_tx to 1 and tx_data to rx_data. Note that we aren't checking tx_busy so it is possible this byte will be dropped. However, in practice you can't type fast enough for this to be an issue. If you wanted to make this more robust you would need to buffer the received letters and send them out only when tx_busy was 0.

The if statement is used to know when to stop. We have two conditions to stop on. The first is if we simply run out of space. To check of this we use &name_count.q. The & operator here ands all the bits of name_count.q together into a single bit. This tells us if all the bits of name_count.q are 1. The second condition is that the user pressed the enter key. We want to accept "\n" or "\r" as a stop character so we check for both.

When we are moving onto the next state, notice that we reset name_count. This is so that we can start printing the name from the beginning.

74// HELLO: Prints the hello text with the given name inserted
75States.HELLO:
76 if (!tx_busy) { // wait for tx to not be busy
77 if (HELLO_TEXT[hello_count.q] != "@") { // if we are not at the sentry
78 hello_count.d = hello_count.q + 1 // increment to next letter
79 new_tx = 1 // new data to send
80 tx_data = HELLO_TEXT[hello_count.q] // send the letter
81 } else { // we are at the sentry
82 name_count.d = name_count.q + 1 // increment the name_count letter
83
84 if (ram.read_data != "\n" && ram.read_data != "\r") // if we are not at the end
85 new_tx = 1 // send data
86
87 tx_data = ram.read_data // send the letter from the RAM
88
89 // if we are at the end of the name or out of letters to send
90 if (ram.read_data == "\n" || ram.read_data == "\r" || &name_count.q) {
91 hello_count.d = hello_count.q + 1 // increment hello_count to pass the sentry
92 }
93 }
94
95 // if we have sent all of HELLO_TEXT
96 if (hello_count.q == $width(HELLO_TEXT, 0) - 1)
97 state.d = States.IDLE // return to IDLE
98 }

In this state, we are going to use two counters, hello_count and name_count. First we will start by sending each letter of HELLO_TEXT. However, once we hit the "@" letter we will send all the letters in our RAM. Once that is done, we will finish sending the rest of HELLO_TEXT.

Once everything has been sent, we return to the IDLE state to await another key press to start it all over again.

The Top Level

The top level tile file is exactly the same as last time since the interface to our greeter module is the same.

1module alchitry_top (
2 input clk, // 100MHz clock
3 input rst_n, // reset button (active low)
4 output led[8], // 8 user controllable LEDs
5 input usb_rx, // USB->Serial input
6 output usb_tx // USB->Serial output
7) {
8
9 sig rst // reset signal
10
11 .clk(clk) {
12 // The reset conditioner is used to synchronize the reset signal to the FPGA
13 // clock. This ensures the entire FPGA comes out of reset at the same time.
14 reset_conditioner reset_cond
15
16 .rst(rst) {
17 #BAUD(1_000_000), #CLK_FREQ(100_000_000) {
18 uart_rx rx
19 uart_tx tx
20 }
21
22 greeter greeter
23 }
24 }
25
26 always {
27 reset_cond.in = ~rst_n // input raw inverted reset signal
28 rst = reset_cond.out // conditioned reset
29
30 led = 8h00 // turn LEDs off
31
32 rx.rx = usb_rx // connect rx input
33 usb_tx = tx.tx // connect tx output
34
35 greeter.new_rx = rx.new_data
36 greeter.rx_data = rx.data
37
38 tx.new_data = greeter.new_tx
39 tx.data = greeter.tx_data
40 greeter.tx_busy = tx.busy
41 tx.block = 0 // no flow control, do not block
42 }
43}

Building the Project

You should now be all set to build the project. Once the project has built successfully, load it onto your board and open up the serial port monitor to test it out. Note that you have to send it a letter to get it to prompt you for your name.

Here is some demo output.

Please type your name: Justin
Hello Justin!
Please type your name: Steve
Hello Steve!
Please type your name: 01234567890123456789012345678901
Hello 01234567890123456789012345678901!

Notice that the moment you type 32 letters it cuts you off and says hello.