This lesson starts at commit b9d3256d69143ecab22c8b939a4d09dfe6218f72.

1. Setting up the project

In this first session, we'll start from this template, which is nothing but a Vivado project with a single design file, a testbench, and a Makefile to make programming the Mimas A7 a bit easier.

We roughly know what the structure of our processor is going to look like. As discussed before, we will build a 5-stage pipeline with fetch, decode, execute, memory, and register writeback stages. We'll make different modules for these, and to keep things structured nicely we'll create a core folder for all the source files that are used in the core.

Let's start with the first stage, which is the fetch stage. First, we duplicate the top_level module from our template, rename it to fetch, place it in the core folder, clean up the logic and input/output ports, and rename the entity to fetch.

src/core/fetch.vhd ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+ use ieee.numeric_std.all;
4
+
5
+
6
+ entity fetch is
7
+ port (
8
+ clk: in std_logic
9
+ );
10
+ end fetch;
11
+
12
+
13
+ architecture rtl of fetch is
14
+ begin
15
+
16
+ process (clk)
17
+ begin
18
+ if rising_edge(clk) then
19
+ -- TODO: implement
20
+ end if;
21
+ end process;
22
+
23
+ end rtl;

In the core, the output of one stage will be used as the input to the next stage. To keep this a bit more manageable, we'll group the in- and outputs in records, which are defined in core/types.vhd.

src/core/fetch.vhd CHANGED
@@ -2,10 +2,13 @@ library ieee;
2
  use ieee.std_logic_1164.all;
3
  use ieee.numeric_std.all;
4
 
 
 
5
 
6
  entity fetch is
7
  port (
8
- clk: in std_logic
 
9
  );
10
  end fetch;
11
 
 
2
  use ieee.std_logic_1164.all;
3
  use ieee.numeric_std.all;
4
 
5
+ use work.core_types.all;
6
+
7
 
8
  entity fetch is
9
  port (
10
+ clk: in std_logic;
11
+ output: out fetch_output_t
12
  );
13
  end fetch;
14
 
src/core/types.vhd ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+
4
+
5
+ package core_types is
6
+ type fetch_output_t is record
7
+ end record fetch_output_t;
8
+ end package core_types;

It will also turn out to be useful to create constants for the "default" output of a stage. We'll define this (and possibly other constants that are used in the core) in core/constants.vhd. We'll use the convention of using all-caps for constants.

src/core/constants.vhd ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+
4
+ use work.core_types.all;
5
+
6
+
7
+ package core_constants is
8
+ constant DEFAULT_FETCH_OUTPUT: fetch_output_t := ();
9
+ end package core_constants;
src/core/fetch.vhd CHANGED
@@ -3,12 +3,13 @@ use ieee.std_logic_1164.all;
3
  use ieee.numeric_std.all;
4
 
5
  use work.core_types.all;
 
6
 
7
 
8
  entity fetch is
9
  port (
10
  clk: in std_logic;
11
- output: out fetch_output_t
12
  );
13
  end fetch;
14
 
 
3
  use ieee.numeric_std.all;
4
 
5
  use work.core_types.all;
6
+ use work.core_constants.all;
7
 
8
 
9
  entity fetch is
10
  port (
11
  clk: in std_logic;
12
+ output: out fetch_output_t := DEFAULT_FETCH_OUTPUT
13
  );
14
  end fetch;
15
 

We'll now add a placeholder for the next stage, the decode stage. It will be very similar to the placeholder for the fetch stage, so we just copy-paste that for now.

src/core/constants.vhd CHANGED
@@ -6,4 +6,5 @@ use work.core_types.all;
6
 
7
  package core_constants is
8
  constant DEFAULT_FETCH_OUTPUT: fetch_output_t := ();
 
9
  end package core_constants;
 
6
 
7
  package core_constants is
8
  constant DEFAULT_FETCH_OUTPUT: fetch_output_t := ();
9
+ constant DEFAULT_DECODE_OUTPUT: decode_output_t := ();
10
  end package core_constants;
src/core/decode.vhd ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+ use ieee.numeric_std.all;
4
+
5
+ use work.core_types.all;
6
+ use work.core_constants.all;
7
+
8
+
9
+ entity decode is
10
+ port (
11
+ clk: in std_logic;
12
+ output: out decode_output_t := DEFAULT_DECODE_OUTPUT
13
+ );
14
+ end decode;
15
+
16
+
17
+ architecture rtl of decode is
18
+ begin
19
+
20
+ process (clk)
21
+ begin
22
+ if rising_edge(clk) then
23
+ -- TODO: implement
24
+ end if;
25
+ end process;
26
+
27
+ end rtl;
src/core/types.vhd CHANGED
@@ -5,4 +5,7 @@ use ieee.std_logic_1164.all;
5
  package core_types is
6
  type fetch_output_t is record
7
  end record fetch_output_t;
 
 
 
8
  end package core_types;
 
5
  package core_types is
6
  type fetch_output_t is record
7
  end record fetch_output_t;
8
+
9
+ type decode_output_t is record
10
+ end record decode_output_t;
11
  end package core_types;

The only difference is that the decode stage is not the first stage, so it takes the input from the previous stage, the fetch stage, as input.

src/core/decode.vhd CHANGED
@@ -9,6 +9,7 @@ use work.core_constants.all;
9
  entity decode is
10
  port (
11
  clk: in std_logic;
 
12
  output: out decode_output_t := DEFAULT_DECODE_OUTPUT
13
  );
14
  end decode;
 
9
  entity decode is
10
  port (
11
  clk: in std_logic;
12
+ input: in fetch_output_t;
13
  output: out decode_output_t := DEFAULT_DECODE_OUTPUT
14
  );
15
  end decode;

The placeholders for the register read, execute, and memory stages are similar to the one for the decode stage, so we copy-paste and rename the types, constants, and modules.

src/core/constants.vhd CHANGED
@@ -7,4 +7,6 @@ use work.core_types.all;
7
  package core_constants is
8
  constant DEFAULT_FETCH_OUTPUT: fetch_output_t := ();
9
  constant DEFAULT_DECODE_OUTPUT: decode_output_t := ();
 
 
10
  end package core_constants;
 
7
  package core_constants is
8
  constant DEFAULT_FETCH_OUTPUT: fetch_output_t := ();
9
  constant DEFAULT_DECODE_OUTPUT: decode_output_t := ();
10
+ constant DEFAULT_EXECUTE_OUTPUT: execute_output_t := ();
11
+ constant DEFAULT_MEMORY_OUTPUT: memory_output_t := ();
12
  end package core_constants;
src/core/execute.vhd ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+ use ieee.numeric_std.all;
4
+
5
+ use work.core_types.all;
6
+ use work.core_constants.all;
7
+
8
+
9
+ entity execute is
10
+ port (
11
+ clk: in std_logic;
12
+ input: in decode_output_t;
13
+ output: out execute_output_t := DEFAULT_EXECUTE_OUTPUT
14
+ );
15
+ end execute;
16
+
17
+
18
+ architecture rtl of execute is
19
+ begin
20
+
21
+ process (clk)
22
+ begin
23
+ if rising_edge(clk) then
24
+ -- TODO: implement
25
+ end if;
26
+ end process;
27
+
28
+ end rtl;
src/core/memory.vhd ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+ use ieee.numeric_std.all;
4
+
5
+ use work.core_types.all;
6
+ use work.core_constants.all;
7
+
8
+
9
+ entity memory is
10
+ port (
11
+ clk: in std_logic;
12
+ input: in execute_output_t;
13
+ output: out memory_output_t := DEFAULT_MEMORY_OUTPUT
14
+ );
15
+ end memory;
16
+
17
+
18
+ architecture rtl of memory is
19
+ begin
20
+
21
+ process (clk)
22
+ begin
23
+ if rising_edge(clk) then
24
+ -- TODO: implement
25
+ end if;
26
+ end process;
27
+
28
+ end rtl;
src/core/types.vhd CHANGED
@@ -8,4 +8,10 @@ package core_types is
8
 
9
  type decode_output_t is record
10
  end record decode_output_t;
 
 
 
 
 
 
11
  end package core_types;
 
8
 
9
  type decode_output_t is record
10
  end record decode_output_t;
11
+
12
+ type execute_output_t is record
13
+ end record execute_output_t;
14
+
15
+ type memory_output_t is record
16
+ end record memory_output_t;
17
  end package core_types;

The register write stage is similar again, but since it is the last stage it does not have output.

src/core/write.vhd ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+ use ieee.numeric_std.all;
4
+
5
+ use work.core_types.all;
6
+ use work.core_constants.all;
7
+
8
+
9
+ entity write is
10
+ port (
11
+ clk: in std_logic;
12
+ input: in memory_output_t
13
+ );
14
+ end write;
15
+
16
+
17
+ architecture rtl of write is
18
+ begin
19
+
20
+ process (clk)
21
+ begin
22
+ if rising_edge(clk) then
23
+ -- TODO: implement
24
+ end if;
25
+ end process;
26
+
27
+ end rtl;

Now, we're ready to make a module for our core. For now, it just takes the output from modules and passes it to the next module.

src/core.vhd ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+ use ieee.numeric_std.all;
4
+
5
+ use work.core_types.all;
6
+ use work.core_constants.all;
7
+
8
+
9
+ entity core is
10
+ port (
11
+ clk: in std_logic
12
+ );
13
+ end core;
14
+
15
+
16
+ architecture rtl of core is
17
+ signal fetch_output: fetch_output_t;
18
+ signal decode_output: decode_output_t;
19
+ signal execute_output: execute_output_t;
20
+ signal memory_output: memory_output_t;
21
+
22
+ component fetch is
23
+ port (
24
+ clk: in std_logic;
25
+ output: out fetch_output_t
26
+ );
27
+ end component;
28
+
29
+ component decode is
30
+ port (
31
+ clk: in std_logic;
32
+ input: in fetch_output_t;
33
+ output: out decode_output_t
34
+ );
35
+ end component;
36
+
37
+ component execute is
38
+ port (
39
+ clk: in std_logic;
40
+ input: in decode_output_t;
41
+ output: out execute_output_t
42
+ );
43
+ end component;
44
+
45
+ component memory is
46
+ port (
47
+ clk: in std_logic;
48
+ input: in execute_output_t;
49
+ output: out memory_output_t
50
+ );
51
+ end component;
52
+
53
+ component write is
54
+ port (
55
+ clk: in std_logic;
56
+ input: in memory_output_t
57
+ );
58
+ end component;
59
+
60
+ begin
61
+ fetch_inst: fetch port map(clk => clk, output => fetch_output);
62
+
63
+ decode_inst: decode port map(clk => clk, input => fetch_output, output => decode_output);
64
+
65
+ execute_inst: execute port map(clk => clk, input => decode_output, output => execute_output);
66
+
67
+ memory_inst: memory port map(clk => clk, input => execute_output, output => memory_output);
68
+
69
+ write_inst: write port map(clk => clk, input => memory_output);
70
+
71
+ end rtl;

Now, we create a simple testbench for the core module.

sim/core_tb.vhd ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library ieee;
2
+ use ieee.std_logic_1164.all;
3
+ use ieee.numeric_std.all;
4
+
5
+
6
+ entity core_tb is
7
+ end core_tb;
8
+
9
+
10
+ architecture behavioral of core_tb is
11
+ constant clk_period: time := 10 ns;
12
+ signal clk: std_logic := '1';
13
+
14
+ component core is
15
+ port (
16
+ clk: in std_logic
17
+ );
18
+ end component;
19
+
20
+ begin
21
+ clk_process :process
22
+ begin
23
+ clk <= '1';
24
+ wait for clk_period / 2;
25
+ clk <= '0';
26
+ wait for clk_period / 2;
27
+ end process;
28
+
29
+ core_inst: core port map(clk => clk);
30
+
31
+ end behavioral;

When we add all the files in Vivado and try to simulate the core, we get a wonderful error message about how empty records are not supported... Sigh. OK. Let's add some placeholders then.

src/core/constants.vhd CHANGED
@@ -5,8 +5,19 @@ use work.core_types.all;
5
 
6
 
7
  package core_constants is
8
- constant DEFAULT_FETCH_OUTPUT: fetch_output_t := ();
9
- constant DEFAULT_DECODE_OUTPUT: decode_output_t := ();
10
- constant DEFAULT_EXECUTE_OUTPUT: execute_output_t := ();
11
- constant DEFAULT_MEMORY_OUTPUT: memory_output_t := ();
 
 
 
 
 
 
 
 
 
 
 
12
  end package core_constants;
 
5
 
6
 
7
  package core_constants is
8
+ constant DEFAULT_FETCH_OUTPUT: fetch_output_t := (
9
+ placeholder => '0'
10
+ );
11
+
12
+ constant DEFAULT_DECODE_OUTPUT: decode_output_t := (
13
+ placeholder => '0'
14
+ );
15
+
16
+ constant DEFAULT_EXECUTE_OUTPUT: execute_output_t := (
17
+ placeholder => '0'
18
+ );
19
+
20
+ constant DEFAULT_MEMORY_OUTPUT: memory_output_t := (
21
+ placeholder => '0'
22
+ );
23
  end package core_constants;
src/core/types.vhd CHANGED
@@ -4,14 +4,18 @@ use ieee.std_logic_1164.all;
4
 
5
  package core_types is
6
  type fetch_output_t is record
 
7
  end record fetch_output_t;
8
 
9
  type decode_output_t is record
 
10
  end record decode_output_t;
11
 
12
  type execute_output_t is record
 
13
  end record execute_output_t;
14
 
15
  type memory_output_t is record
 
16
  end record memory_output_t;
17
  end package core_types;
 
4
 
5
  package core_types is
6
  type fetch_output_t is record
7
+ placeholder: std_logic;
8
  end record fetch_output_t;
9
 
10
  type decode_output_t is record
11
+ placeholder: std_logic;
12
  end record decode_output_t;
13
 
14
  type execute_output_t is record
15
+ placeholder: std_logic;
16
  end record execute_output_t;
17
 
18
  type memory_output_t is record
19
+ placeholder: std_logic;
20
  end record memory_output_t;
21
  end package core_types;

Now, when we try to simulate core_tb again, it works. Of course, it does nothing, but it works, and we're ready to start working on the first CPU stage.