The chip is able to solve a maze, outputting the correct sequence of moves (up, down, left, right) needed to traverse the maze without any dead ends. It accomplishes this by requesting cell locations from off-chip memory and intaking their wall configurations as a 4-bit input.
- As the chip moves around the maze, it requests new memory locations until it reaches the end of the maze.
- The chip may take dead-end paths, but its solution does not include any dead ends due to the implementation of a hardware stack.
- Once it eaches the end, the chip outputs the solution as a series of 2-bit numbers corresponding to the series of moves (up, right, down, left) needed to successfully traverse the maze.
1.`INPUT (S1)`: Wait for the external input_enable input pin to go high, indicating that input `cur_wall [3:0]` is valid for the requested address `req_addr [7:0]` output. Then go to `LOAD (S2)`.
1.`LOAD (S2)`: Save `cur_wall` input at the start of the datapath. If the input is `0000`, we've reached the end so jump to LAST (S11); otherwise, continue to `RIGHT (S3)`.
In a maze, there are two ways of thinking about movement.
1. The absolute view, from the perspective of someone looking at the maze from the outside.
1. The relative view, from the perspective of someone who is walking around the maze themselves.
The chip uses **both** of these perspectives. In the datapath, we start with an absolute wall configuration, which is the walls surrounding a cell when viewing it from the perspective of the whole maze. This data is then translated into the relative wall configuration, which is the walls from the perspective of the current direction. For example, if the walls above and to the right a cell looking at it from the outside (absolute configuration), then if the chip is currently moving right through the maze, from its perspective there are walls *above and to the left* (relative configuration).
The relative move can then be decided based on this relative configuration, and the relative move can finally be translated back into an absolute move Using the pervious example, if we were moving right, and had walls above and to the left, we would turn right (relative move). But because we were already going right, turning right again would actually mean we're now going down, so the absolute move would be down.
The `cur_wall` input is saved at the beginning of the datapath. This input represents the **absolute** wall configuration of a cell. Walls in the datapath are 4 bits, each representing a wall surrounding the cell - up, right, down, left.
If the wall input = `0000`, a NOR gate called wall_check sets the no_walls flag to `1`. This flag tells the FSM that the chip has reached the end of the maze.
The absolute direction value is used to perform a barrel shift left on the current wall input, transforming it from an absolute configuration to a relative configuration. This is accomplished by the `rotate_wall` module in the datapath.
> *Example: We are moving left, which is direction `11`. Shifting `1001` by `11` results in `1100`, which is the relative configuration. We now know we have walls above and to our right from our perspective of moving left*
Since we transformed the absolute wall configuration to a relative one, there is no need to account for every combination of walls for every direction. The move decision is now easy. We follow this pattern:
In the datapath, this is as easy as selecting the right bit, then the up bit, then the left bit, etc. of the relative wall config to see if there's a wall there (this happens in the `wall_check` module of the datapath). We invert the wall input so if there is no wall and the move is valid, the `val_move` flag goes high. This flag is an input to the FSM.
The relative move is saved to the `rel_move_reg` register. Now we can tranform our relative move to an absolute move. This is done simply by adding our relative move to our absolute direction, and happens in the `rel_to_abs_alu` module.
We now have our absolute move, which is saved to the `abs_move_buf` register and is an input to the `back_test` module. The second input to the `back_test` module is the value at the top of the stack, representing the last valid move pushed to the stack. The `back_test` module checks to see if these moves are opposite; in other words, if the current move and the top move of the stack are inverse (up & down, or left & right), we know we are backtracking. If so, the back_test module sets the `back_flag` to `1`.
> *Example: If the last absolute valid move was left (`11`), and our current absolute move is down (`10`), then we are **not** backtracking because these are not inverses. The `back_flag` is **not** set to `1`.*
If we aren't backtracking, we can store the current move to the stack as a valid move. (This move could later become invalid if we backtrack over it, but for now it's valid.) To do this, the FSM sets `stack_en` to `1` and `stack_op` to `0`, which corresponds to a push operation. The current move is pushed to the stack.
If we are backtracking, the FSM sets `stack_en` to `1` and `stack_op` to `1`, corresponding to a pop operation. The previous move is popped from the stack and the current move is **not** pushed since they "cancel out".
Regardless of a push or pop, we still need to decide which address to request next from external memory. For a 16x16 maze, the address is stored as a single 8-bit number, with bits 7-4 for the x coordinate and bits 3-0 for the y coordinate. The `cur_addr_reg` register stores the current address. When the chip resets, this address is set to 0.
The `x_y` bus is added to or subtracted from the current address to form the next address, stored in the `next_addr_reg` register and split into its x and y chip outputs with the `addr_split` module. This address is requested from memory.
> *Example: Let's say our previous address was [x,y] = [13,5] (`11010101`), and our current move is down (`10`). We subtract 1 from the y coordinate: `11010101` - `00000001` = `11010100`, which is [13, 4]. The outputs x = 13 and y = 4 are requested from external memory.*
The final part of the main FSM loop involves updating the internal registers. The current move is written to the abs_dir_reg, since it is not the absolute direction we are travelling through the maze. The current address is updated to the next address. The FSM then returns to S1 where it waits for the next input.
> *Example: The current move, down (`10`), is written to the `abs_dir_reg` register. The address stored in the `next_addr_reg` register, `11010100`, is stored in the `cur_addr_reg` register.*
When the chip has reached the end of the maze, it's time to send the solution. This is all implemented in the FSM and stack. Details on the stack implementation can be found [here](#stack).
The stack has an internal pointer, separate from the stack pointer, which starts at stack address `0` and can be incremented by the FSM with the `stack_sol_inc` signal. The FSM increments this pointer and checks to see if it has reached the end of the path with the `stack_ptrs_eq` flag, set by the stack when the output pointer has reached the top of the stack, marked by the stack pointer.
Our highest-level module, chip_design, is at the bottom of the .sv file to preserve Verilog hierarchical organization rules, where the components of a module are declared above the module. Most of this breakdown will concern the datapath components, since this is where the actual processing is happening.
Our variable-size register file is defined as follows:
```SystemVerilog
module reg_nbit #(
parameter N = 8 // default to 8 bits
) (
input wire reg_clk,
input wire reg_en,
input wire reg_rst,
input wire [N-1:0] reg_in,
output logic [N-1:0] reg_out
);
always_comb begin
if (reg_rst) begin
reg_out <= {N{1'b0}}; // zero register on rst signal
end
else if (reg_en) begin
reg_out <= reg_in; // update reg on en signal
end
end
endmodule
```
We made this register of variable size so it can be used for all internal registers, and so chip can be expanded to accommodate larger mazes according to the `MAX_SIZE` parameter. These registers are:
- *8-bit: `cur_addr_reg`, `next_addr_reg` *for 16x16 mazes only
We used a `always_comb` block instead of an `always_ff` block to avoid the 1-cycle delay that is inherent to a flip-flop. Our register implementation is only a problem if we have a register feeding directly back into itself, but we have everything buffered so this isn't an issue.
The register is zeroed when it receives a `rst` signal, and when it receives a `reg_en` it saves the input.
### Rotate_Wall Module
The `rotate_wall` module takes a 4-bit wall input (`rot_in`) and rotates it by the 2-bit input `rot_val`. This is accomplished by rotating the input left and appending the bits that rotated out of bounds to the right side with an `assign` statement:
We made one ALU (really just an adder & subtractor) module to use for the two ALUs in the datapath:
1. The `rel_to_abs_alu`, which takes the relative move and converts it to an absolute move by adding the current absolute direction `[1:0]`
1. The `cur_to_next_alu`, which increments or decrements the x/y coordinates stored in the `cur_addr_reg` register to calculate the next address `[7:0]`
We needed our implementation to be able to overflow, since the `rel_to_abs_alu` relies on this behavior to function. We also needed to make the module expandable to account for the two different sizes needed.
The `add_sub_sel` input is used to select the operation. The `rel_to_abs_alu` does not use the subtract, so for its module instantiation, this signal is hardcoded as `0`:
```SystemVerilog
add_sub_wrap_nbit #(.N(2)) rel_to_abs_alu (
.a_in (relative_move_wire),
.b_in (absolute_dir_wire),
.add_sub_sel (1'b0),
.result (absolute_move_wire)
);
```
### Stack
Our expandable stack has 6 inputs and 3 outputs.
- Inputs
-`clk`: connected to external clk input pin
-`rst_stack`: resets stack's internal pointers and memory
-`stack_en`: when `1`, stack operation will be performed on `clk` signal
-`stack_op`: `0` = push, `1` = pop
-`stack_in [1:0]`: data input to the stack
-`stack_sol_inc`: increments `stack_sol_ptr` for output
- Outputs
-`stack_out [1:0]`: memory location pointed to by `stack_ptr`, inc/dec internally
-`stack_sol [1:0]`: memory location pointed to by `stack_sol_ptr + 1`, incremented by `stack_sol_inc` input
-`stack_ptrs_eq`: set to `1` when `stack_ptr` = `stack_sol_ptr`
Here is the SystemVerilog code:
```SystemVerilog
module stack_ncell #(
parameter CELLS = 256
) (
input wire clk,
input wire rst_stack,
input wire stack_en, // High to enable push/pop operations
input wire stack_op, // 0 for push, 1 for pop
input wire [1:0] stack_in,
input wire stack_sol_inc,
output logic [1:0] stack_out,
output logic [1:0] stack_sol,
output logic stack_ptrs_eq
);
localparam PTR_WIDTH = $clog2(CELLS);
logic [1:0] mem [0:CELLS-1];
logic [PTR_WIDTH-1:0] stack_ptr;
logic [PTR_WIDTH-1:0] stack_sol_ptr;
always_ff @(posedge clk or posedge rst_stack) begin
if (rst_stack) begin
stack_ptr <= 0;
stack_sol_ptr <= 0;
mem <= '{default:2'b00};
end else if (stack_sol_inc) begin
stack_sol_ptr <= stack_sol_ptr + 1;
end else if (stack_en) begin
if (!stack_op) begin // Push Operation (stack_op is 0)
// 1. Increment the stack pointer
stack_ptr <= stack_ptr + 1;
// 2. Write the input data to the new top location
If `stack_en` = `1` and `stack_op` = `0`, this indicates a push operation. We increment the stack pointer and write the stack input to the new memory location. If `stack_op` = `1`, this is a pop operation and we decrement the stack pointer. We don't actually pop data out of the stack to use; we're essentially deleting the top value of the stack by making it out of bounds.
The `stack_sol` output is the `stack_sol_ptr + 1` because the `stack_sol_ptr` starts at address `0`, but the first valid move will be at address `1`, not `0`. The FSM checks `stack_ptrs_eq` and then increments the pointer, so when `stack_sol_ptr` = `stack_ptr - 1`, the FSM will...
1. Read the value at `stack_sol_ptr + 1`, which is the top of the stack (`mem[stack_ptr]`)
1. Increment the `stack_sol_ptr`
1. Check `stack_ptrs_eq`, which is now `1`
1. Exit
This does mean that we only have 255 usable locations for a 16x16 solver instead of 256, but this doesn't matter since 255 is the maximum number of moves within a 16x16 grid. The stack is expandable to accommodate the `MAX_SIZE` parameter.
### Test_Move Module
The `test_move` module is a simple 4-bit bit selector, which sets the 1-bit output equal to the selected bit of the 4-bit input.
```SystemVerilog
module bit_sel_4bit (
input wire [3:0] sel_in,
input wire [1:0] sel_val, // bit to select
output logic sel_out
);
always_comb begin
case (sel_val)
2'b00: sel_out = sel_in[3]; // "up" direction
2'b01: sel_out = sel_in[2]; // "right" direction
2'b10: sel_out = sel_in[1]; // "left" direction
2'b11: sel_out = sel_in[0]; // "down" direction
default: sel_out = 1'b0; // default case
endcase
end
endmodule
```
### Back_Test Module
The `back_test` module implements clever logic to simplify the detection of backtracking. Instead of checking the four possible cases for current and previous values:
1. Up (00), Down (10)
1. Down (10), Up (00)
1. Right (01), Left (11)
1. Left (11), Right (01)
it instead checks two conditions with an `assign` statement:
1. The inputs are different
1. The inputs have the same least significant bit
```SystemVerilog
module back_test_logic (
input wire [1:0] cur_in,
input wire [1:0] prev_in,
output logic backtrack
);
// If cur_in and prev_in are different but have the same LSB, chip is backtracking
The `move_conv` logic block takes in a 2-bit move and outputs a 1-bit ALU operation (`add_sub`) and the data output `x_y`.
```SystemVerilog
module move_conv_logic #(
parameter COOR_WIDTH = 4
) (
input wire [1:0] move_in,
output logic [(COOR_WIDTH*2)-1:0] x_y,
output logic add_sub // 0 for add, 1 for sub
);
logic [COOR_WIDTH-1:0] add_x;
logic [COOR_WIDTH-1:0] add_y;
always_comb begin
case (move_in)
2'b00: begin // up
add_sub = 0;
add_x = 0;
add_y = 1;
end
2'b01: begin // right
add_sub = 0;
add_x = 1;
add_y = 0;
end
2'b10: begin // down
add_sub = 1;
add_x = 0;
add_y = 1;
end
2'b11: begin // left
add_sub = 1;
add_x = 1;
add_y = 0;
end
default: begin // default case
add_sub = 0;
add_x = 0;
add_y = 0;
end
endcase
end
assign x_y = {add_x, add_y}; // recombine x and y into one output
endmodule
```
The module has two separate internal wires for x and y called `add_x` and `add_y`, which it combines into the single `x_y` output with an `assign` statement. We chose this design so the module can be expanded according to the `MAX_SIZE` parameter.