Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 886c4f0e1d | |||
| 0e6161df7b | |||
| 136ede3e64 | |||
| 14d13dce3e | |||
| 7db5e42501 | |||
| cb629ef3cf | |||
| 0fe9966f7b | |||
| f5599af6f1 | |||
| fb2b8b03c4 | |||
| a1c4ffda3d |
4 changed files with 342 additions and 64 deletions
|
|
@ -3,3 +3,5 @@
|
|||
*A variable-size maze solver made in SystemVerilog, manufactured by TSMC in Spring 2026.*
|
||||
|
||||
See the [design report](./maze_solver_design_report.md) for details on chip and its implemenation.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -392,6 +392,7 @@ module stack_ncell #(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -3,38 +3,38 @@
|
|||
|
||||
## Table of Contents
|
||||
|
||||
[1. High-Level Chip Purpose & Design](##1.-high-level-chip-purpose-&-design)
|
||||
[1. High-Level Chip Purpose & Design](#1-high-level-chip-purpose--design)
|
||||
|
||||
[2. FSM Design & General Flow](##2.-fsm-design-&-general-flow)
|
||||
[2. FSM Design & General Flow](#2-fsm-design--general-flow)
|
||||
|
||||
[3. Datapath Design](##3.-datapath-design)
|
||||
[3. Datapath Design](#3-datapath-design)
|
||||
|
||||
[4. Code Implementation](##4.-code-implementation)
|
||||
[4. Code Implementation](#4-code-implementation)
|
||||
|
||||
## 1. High-Level Chip Purpose and Design
|
||||
## 1. High-Level Chip Purpose & Design
|
||||
|
||||
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.
|
||||
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 accepting 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.
|
||||
- Once it reaches 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.
|
||||
|
||||
### Inputs & Outputs
|
||||
|
||||
We have a total of 21 I/O pins.
|
||||
|
||||
- Inputs
|
||||
- 1 start pin
|
||||
- 1 rst pin
|
||||
- 1 clk pin
|
||||
- 1 input_enable pin
|
||||
- 4 bits for the cur_wall input
|
||||
- 1 `start` pin
|
||||
- 1 `rst` pin
|
||||
- 1 `clk` pin
|
||||
- 1 `input_enable` pin
|
||||
- 4 bits for the `cur_wall` input
|
||||
- Outputs
|
||||
- 1 done output pin
|
||||
- 1 req_next_addr output pin
|
||||
- 1 out_val pin
|
||||
- 4 bits for the next_x output
|
||||
- 4 bits for the next_y output
|
||||
- 2 bits for the solution output
|
||||
- 1 `done` output pin
|
||||
- 1 `req_next_addr` output pin
|
||||
- 1 `out_val` pin
|
||||
- 4 bits for the `next_x` output
|
||||
- 4 bits for the `next_y` output
|
||||
- 2 bits for the `solution` output
|
||||
|
||||
## 2. FSM Design & General Flow
|
||||
|
||||
|
|
@ -49,28 +49,28 @@ Note: only inputs and outputs to the FSM are shown, **not** the datapath.
|
|||
This is the general flow of the chip:
|
||||
|
||||
**Setup**
|
||||
1. IDLE (S0): Wait for the external start input pin to go high before proceeding. Then go to INPUT (S1).
|
||||
1. `IDLE (S0)`: Wait for the external start input pin to go high before proceeding. Then go to `INPUT (S1)`.
|
||||
|
||||
**Intake Walls**
|
||||
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 [3:0] 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).
|
||||
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)`.
|
||||
|
||||
**Move Decision**
|
||||
1. RIGHT (S3): Check to see if a right turn is possible. If so, go to CHECK (S7); otherwise, continue to UP (S4).
|
||||
1. UP (S4): Check to see if going straight is possible. If so, go to CHECK (S7); otherwise, continue to LEFT (S5).
|
||||
1. LEFT (S5): Check to see if a left turn is possible. If so, go to CHECK (S7); otherwise, continue to DOWN (S6).
|
||||
1. DOWN (S6): This move will always be possible (for details, see datapath section). Go to CHECK (S7).
|
||||
1. `RIGHT (S3)`: Check to see if a right turn is possible. If so, go to `CHECK (S7)`; otherwise, continue to `UP (S4)`.
|
||||
1. `UP (S4)`: Check to see if going straight is possible. If so, go to `CHECK (S7)`; otherwise, continue to `LEFT (S5)`.
|
||||
1. `LEFT (S5)`: Check to see if a left turn is possible. If so, go to `CHECK (S7)`; otherwise, continue to `DOWN (S6)`.
|
||||
1. `DOWN (S6)`: This move will always be possible (for details, see datapath section). Go to `CHECK (S7)`.
|
||||
|
||||
**Backtrack Detection**
|
||||
1. CHECK (S7): Check to see if current move is is backtracking on a previous move. If so, go to POP (S9); otherwise, go to PUSH (S8).
|
||||
1. PUSH (S8): Push the current move to the stack. Go to UPDATE (S10).
|
||||
1. POP (S9): Pop from the stack. Go to UPDATE (S10).
|
||||
1. UPDATE (S10): Update internal registers in preparation for next cycle. Go to INPUT (S2).
|
||||
1. `CHECK (S7)`: Check to see if current move is is backtracking on a previous move. If so, go to `POP (S9)`; otherwise, go to `PUSH (S8)`.
|
||||
1. `PUSH (S8)`: Push the current move to the stack. Go to `UPDATE (S10)`.
|
||||
1. `POP (S9)`: Pop from the stack. Go to `UPDATE (S10)`.
|
||||
1. `UPDATE (S10)`: Update internal registers in preparation for next cycle. Go to `INPUT (S2)`.
|
||||
|
||||
**Output Solution**
|
||||
1. LAST (S11): Check to see if the last move has been read. If so, go to DONE (S13); otherwise, go to OUTPUT (S12).
|
||||
1. OUTPUT (S12): Read value from stack, starting from the bottom, to the solution [1:0] output. Go to LAST (S11).
|
||||
1. DONE (S13): Wait for external rst signal to go back to IDLE (S0).
|
||||
1. `LAST (S11)`: Check to see if the last move has been read. If so, go to `DONE (S13)`; otherwise, go to `OUTPUT (S12)`.
|
||||
1. `OUTPUT (S12)`: Read value from stack, starting from the bottom, to the `solution [1:0]` output. Go to `LAST (S11)`.
|
||||
1. `DONE (S13)`: Wait for external rst signal to go back to `IDLE (S0)`.
|
||||
|
||||
## 3. Datapath Design
|
||||
|
||||
|
|
@ -89,21 +89,21 @@ The relative move can then be decided based on this relative configuration, and
|
|||
|
||||
### Loading an Input
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
*Example: A cell with walls above and to the left of it would have the encoding 1001.*
|
||||
> *Example: A cell with walls above and to the left of it would have the encoding `1001`.*
|
||||
|
||||
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.
|
||||
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 current absolute direction is saved in the abs_dir_reg register in the datapath. Direction, both absolute and relative, is represented as follows:
|
||||
1. 00 - Up
|
||||
1. 01 - Right
|
||||
1. 10 - Down
|
||||
1. 11 - Left
|
||||
1. `00 - Up`
|
||||
1. `01 - Right`
|
||||
1. `10 - Down`
|
||||
1. `11 - Left`
|
||||
|
||||
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.
|
||||
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*
|
||||
> *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*
|
||||
|
||||
### Deciding a Move
|
||||
|
||||
|
|
@ -113,55 +113,55 @@ Since we transformed the absolute wall configuration to a relative one, there is
|
|||
1. Try to move left
|
||||
1. Go down (backwards)
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
*Example: Our relative wall config is 1100, which means we can't go right or up. We can go left (11), so we'll choose that move.*
|
||||
> *Example: Our relative wall config is `1100`, which means we can't go right or up. We can go left (`11`), so we'll choose that move.*
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
*Example: The selected move is left (11), and our current direction is left (11). 11 + 11 = (1)10, so our absolute move is 10 (down).*
|
||||
> *Example: The selected move is left (`11`), and our current direction is left (`11`). `11` + `11` = (`1`)`10`, so our absolute move is `10` (down).*
|
||||
|
||||
### Detecting a Backtrack
|
||||
|
||||
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.
|
||||
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.*
|
||||
> *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`.*
|
||||
|
||||
### Storing the Correct paths
|
||||
|
||||
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 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".
|
||||
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".
|
||||
|
||||
*Example: We push our left (11) move to the stack since we aren't backtracking.*
|
||||
> *Example: We push our down (`10`) move to the stack since we aren't backtracking.*
|
||||
|
||||
### Updating the Next Address & Internal Registers
|
||||
|
||||
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.
|
||||
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 current move is translated into an ALU operation and input like this:
|
||||
1. 00 (Up) - Add 1 to the y coordinate (add_sub = 0, x_y = 00000001)
|
||||
1. 01 (Right) - Add 1 to the x coordinate (add_sub = 0, x_y = 00010000)
|
||||
1. 10 (Down) - Subtract 1 from the y coordinate (add_sub = 1, x_y = 00000001)
|
||||
1. 11 (Left) - Subtract 1 from the x coordinate (add_sub = 1, x_y = 00010000)
|
||||
1. `00` (Up) - Add `1` to the y coordinate (`add_sub` = `0`, `x_y` = `00000001`)
|
||||
1. `01` (Right) - Add `1` to the x coordinate (`add_sub` = `0`, `x_y` = `00010000`)
|
||||
1. `10` (Down) - Subtract `1` from the y coordinate (`add_sub` = `1`, `x_y` = `00000001`)
|
||||
1. `11` (Left) - Subtract `1` from the x coordinate (`add_sub` = `1`, `x_y` = `00010000`)
|
||||
|
||||
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.
|
||||
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 left (11). We subtract 1 from the x coordinate: 11010101 - 00010000 = 11000101, which is [12, 5]. The outputs x = 12 and y = 5 are requested from external 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, left (11), is written to the abs_dir_reg register. The address stored in the next_addr_reg register, 11000101, is stored in the cur_addr_reg register.*
|
||||
> *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.*
|
||||
|
||||
### Outputting the Solution
|
||||
|
||||
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](###the-stack).
|
||||
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.
|
||||
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.
|
||||
|
||||
On each increment of the solution pointer, the FSM...
|
||||
1. enables the sol_reg register, writing to the solution [1:0] output pins, and
|
||||
1. sets out_val to 1, marking the output valid.
|
||||
1. enables the sol_reg register, writing to the `solution [1:0]` output pins, and
|
||||
1. sets `out_val` to 1, marking the output valid.
|
||||
|
||||
This marks the end of the datapath.
|
||||
|
||||
|
|
@ -211,10 +211,285 @@ Our maze solver code is organized hierarchically as follows:
|
|||
);
|
||||
```
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### Registers
|
||||
|
||||
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:
|
||||
- 2-bit: `abs_dir_reg`, `rel_mov_reg`, `abs_move_buf`, `sol_reg`
|
||||
- 4-bit: `cur_wall_reg`
|
||||
- *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:
|
||||
```SystemVerilog
|
||||
assign rot_out = (rot_in << rot_val) | (rot_in >> (4 - rot_val));
|
||||
```
|
||||
|
||||
### Adder/Subtractor
|
||||
|
||||
### The Stack
|
||||
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.
|
||||
|
||||
Here is the SystemVerilog code:
|
||||
```SystemVerilog
|
||||
module add_sub_wrap_nbit #(
|
||||
parameter N = 2
|
||||
) (
|
||||
input wire [N-1:0] a_in,
|
||||
input wire [N-1:0] b_in,
|
||||
input wire add_sub_sel, // 0 = add, 1 = sub
|
||||
|
||||
output logic [N-1:0] result
|
||||
);
|
||||
assign result = add_sub_sel ? (a_in - b_in) : (a_in + b_in);
|
||||
endmodule
|
||||
```
|
||||
|
||||
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
|
||||
mem[stack_ptr + 1] <= stack_in;
|
||||
end else begin // Pop Operation (stack_op is 1)
|
||||
stack_ptr <= stack_ptr - 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
assign stack_out = mem[stack_ptr];
|
||||
assign stack_sol = mem[stack_sol_ptr + 1];
|
||||
assign stack_ptrs_eq = (stack_ptr == stack_sol_ptr);
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
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
|
||||
assign backtrack = (cur_in != prev_in) && (cur_in[0] == prev_in[0]);
|
||||
|
||||
endmodule
|
||||
```
|
||||
|
||||
### Move_Conv Module
|
||||
|
||||
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.
|
||||
|
||||
See the previous section for details on the [move conversion logic](#updating-the-next-address--internal-registers).
|
||||
|
||||
### Splitter
|
||||
|
||||
The `addr_split` module takes the 8-bit next address output (for a 16x16 maze) and splits it into its x and y components with two `assign` statements:
|
||||
```SystemVerilog
|
||||
assign split_high_out = split_in[N-1:N/2];
|
||||
assign split_low_out = split_in[(N/2)-1:0];
|
||||
```
|
||||
|
||||
### Wall_Invert & Wall_Check
|
||||
|
||||
The `wall_invert` and `wall_check` modules are simple NOT and NOR gates, respectively.
|
||||
|
||||
The `wall_invert` module takes the 4-bit absolute wall configuration and inverts each bit with an `assign` statement:
|
||||
```SystemVerilog
|
||||
assign not_out = ~not_in;
|
||||
```
|
||||
|
||||
Similarly, the `wall_check` module uses an `assign` statement to produce the NOR output of the 4-bit relative wall configuration:
|
||||
```SystemVerilog
|
||||
assign nor_out = ~(|nor_in);
|
||||
```
|
||||
|
|
|
|||
BIN
media/final_chip.png
Normal file
BIN
media/final_chip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Loading…
Reference in a new issue