Update report with datapath breakdown

This commit is contained in:
Owen Dorweiler 2025-12-09 13:25:11 -05:00
commit 8e47edbe88

View file

@ -1,21 +1,51 @@
# Maze Solver Chip Design Report
*An explanation of the design of our 16x16 expandable maze solver chip, including a detailed dive into the SystemVerilog.*
## Table of Contents
[1. High-Level Chip Purpose & Design](##1.-high-level-chip-purpose-&-design)
[2. FSM Design & General Flow](##2.-fsm-design-&-general-flow)
[3. Datapath Design](##3.-datapath-design)
[4. Code Implementation](##4.-code-implementation)
## 1. High-Level Chip Purpose and Design
The chip is able to solve a maze 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 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.
## 2. FSM and General Flow
### 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
- 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
## 2. FSM Design & General Flow
### FSM Implementation
The chip's controller is a 14-state FSM. The FSM state diagram is shown below.
![Maze Solver Circuit Diagram](./media/maze_solver_fsm.png)
Note: only inputs and outputs to the FSM are shown, **not** the datapath.
### General Flow
This is the general flow of the chip:
**Setup**
@ -23,26 +53,119 @@ This is the general flow of the chip:
**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).
2. 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. 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).
**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).
2. UP (S4): Check to see if going straight is possible. If so, go to CHECK (S7); otherwise, continue to LEFT (S5).
3. LEFT (S5): Check to see if a left turn is possible. If so, go to CHECK (S7); otherwise, continue to DOWN (S6).
4. DOWN (S6): This move will always be possible (for details, see datapath section). Go to CHECK (S7).
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).
2. PUSH (S8): Push the current move to the stack. Go to UPDATE (S10).
3. POP (S9): Pop from the stack. Go to UPDATE (S10).
4. UPDATE (S10): Update internal registers in preparation for next cycle. Go to INPUT (S2).
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).
2. OUTPUT (S12): Read value from stack, starting from the bottom, to the solution [1:0] output. Go to LAST (S11).
3. DONE (S13): Wait for external rst signal to go back to IDLE (S0).
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. Code Organization
## 3. Datapath Design
This is our circuit diagram:
![Maze Solver Circuit Diagram](./media/maze_solver_circuit_diagram.png)
### Absolute vs. Relative Directions/Moves
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.
### 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.
*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.
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
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*
### Deciding a Move
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:
1. Try to move right
1. Try to move up (go stright)
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.
*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.
*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.
*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 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.*
### 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.
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)
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.*
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.*
### 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).
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.
This marks the end of the datapath.
## 4. Code Implementation
Our maze solver code is organized hierarchically as follows:
```SystemVerilog
@ -90,7 +213,8 @@ 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.
## 4. Datapath
### Registers
This is our circuit diagram:
![Maze Solver Circuit Diagram](./media/maze_solver_circuit_diagram.png)
### Adder/Subtractor
### The Stack