Content follows this message If you have enjoyed my articles, please consider these charities for donation: |
This is part of a series of posts detailing the steps and learning undertaken to design and implement a CPU in VHDL. Previous parts are available here, and I’d recommend they are read before continuing!
I’m using the Xilinx ISE WebPack suite of tools for this project. It’s available here for Windows and Linux, for free. Once installed and set up, you can run the project navigator and create a new project. I’ll go through some basic steps here, just for clarity – however this should not be taken as a VHDL primer, I’m assuming some basic knowledge. There are a variety of good VHDL tutorials that will lay down those basics for you.
You get presented with settings to choose from when you initially create a new project, after giving it a name and description. I’ve set my project up for VHDL and the same chip on my miniSpartan6+ board. Once we accept these settings, we have the IDE ready for getting some VHDL modules added.
In this part we want to concern ourselves with the register file. The registers, for this CPU, will come in the form of 8 16-bit values. I’ll go into more detail in the next part about Instruction Set Architecture choices, but for now we assume that we have a destination register value, and two sources for ALU operations.
With the register file as a black box, we know it must have 3 inputs to it for indicating what registers we want for the destination and two sources. We also need the data output from those sources, and an input for the destination. Finally, we want control line for the write enable functionality of the register file. Sometimes we will not want the destination value to be actually written, so it needs an input to enable the writing of data. The inputs which select the registers will address one of 8, so we need a 3 bit signal for the selection lines. For the data, we know they are 16-bit wide. We will also need a clock input, and an enable bit. With this information fixed, we can create a new VHDL module within our project and detail the ports it exposes.
ISE creates the skeleton for the source automatically. The empty module is as follows.
entity reg16_8 is
Port ( I_clk : in STD_LOGIC;
I_en : in STD_LOGIC;
I_dataD : in STD_LOGIC_VECTOR (15 downto 0);
O_dataA : out STD_LOGIC_VECTOR (15 downto 0);
O_dataB : out STD_LOGIC_VECTOR (15 downto 0);
I_selA : in STD_LOGIC_VECTOR (2 downto 0);
I_selB : in STD_LOGIC_VECTOR (2 downto 0);
I_selD : in STD_LOGIC_VECTOR (2 downto 0);
I_we : in STD_LOGIC);
end reg16_8;
architecture Behavioral of reg16_8 is
begin
end Behavioral;
It’s worth noting at this point that I’ve used STD_LOGIC_VECTOR (SLV) types everywhere. Upon posting Part 1 of this series, I had a few folk tell me where possible to use the integer types. A quick Google for more information does show many folk using those instead of the raw SLV types for various reasons. I may go into those later, and revisit the code to re-implement with less SLV usage. For now, however, I intend to just crack on and continue regardless.
The register file is very simple. we need it to do the following
For this, we will add a process block.
architecture Behavioral of reg16_8 is
begin
process(I_clk)
begin
if rising_edge(I_clk) and I_en='1' then
-- do our things!
end if;
end process;
end Behavioral;
We add I_clk into the process sensitivity list – the parameters after the process keyword. This means this process gets re-evaluated when the state of I_clk changes, which is exactly what you’d expect. The next thing we need to do is define our actual data store, the registers themselves. This is fairly easy and we just define a type, followed by a signal of that type.
architecture Behavioral of reg16_8 is
type store_t is array (0 to 7) of std_logic_vector(15 downto 0);
signal regs: store_t := (others => X"0000");
begin
..
regs is now an array of 8 SLVs containing 16 bits each – representing our registers. The others statement initializes all bits to 0. Now we just add our logic as per to two main use cases above. We cast our standard_logic_vector inputs to unsigned integers as to index the regs signal array.
process(I_clk)
begin
if rising_edge(I_clk) and I_en='1' then
O_dataA <= regs(to_integer(unsigned(I_selA)));
O_dataB <= regs(to_integer(unsigned(I_selB)));
if (I_we = '1') then
regs(to_integer(unsigned(I_selD))) <= I_dataD;
end if;
end if;
end process;
At this point its worth checking our syntax using the command listed under Synthesize when the module is selected in the hierarchy within ISE. This will show a few errors, as we are using functionality that you need to include a library for. Thankfully, the generated module has inserted comments near the top of the file indicating all we need is to include the statement ‘use IEEE.NUMERIC_STD.ALL;’ to use these functions. Doing this will allow for our register file to pass syntax checking.
To test our module, we will create a test bench for it within ISE. Right click the hierarchy, and add new source, of type VHDL Test Bench. I name my tests after what I’m testing, with ‘_tb’ appended. But you can call it anything. Associate the test with the register file module, and a new VHDL file containing a test harness will be created based on it’s definition – ready for you to add in some detail.
When simulating, you can assign values to the inputs and issue wait statements to account for cycle latencies. ISE automatically generates a clock process for you, so that input is taken care of. If we assign some values to the input, and attempt to write a value to a register D which is also one of the selected register A or B outputs, if we then wait a clock cycle we should see the output for register A change. This test looks as follows:
-- insert stimulus here
I_en <= '1';
I_selA <= "000";
I_selB <= "001";
I_selD <= "000";
I_dataD <= X"FAB5";
I_we <= '1';
wait for I_clk_period;
Now by running the ‘Simulate Behavioural Model’ process when in Simulation view (which you should be now after editing the test bench source file) you get an ISim instance showing a waveform timeline of all the signals. Here we can validate out expected output, and we do get what was expected. After a single clock cycle, the output from register A (the O_dataA signal) becomes 0xFAB5.
You can view the contents of the regs array using the memory pane.
We can extend the test a bit more, to cover some basic situations we know we will get to. Test further writes, make sure we do not write when the write enable is not asserted, writing multiple times to the same register, and finally reading the same register twice on the same clock cycle – useful for nops (using or), doubling values with add and also clearing with xor. The output from the simulator is shown below, with some notes annotated at respective cycles. The full test bench source is at the end of this post.
This test shows us the register file should be suitable for our needs!
A further test to add would be to also test the enable bit works (as in, when disabled nothing updates) – you’ll have to trust me when I say it does! It’s worth noting there are VHDL asserts, but the truth is despite them compiling fine I’ve not found where any pass/fail/errors appear – even when I’ve forced an assert condition to fail. Maybe someone could help me out in that regard (message @domipheus).
That’s it for this part. Thanks for reading, comments as always to @domipheus, and the next part will be about the ISA and decoder!
The next part in the series is now available.
[Thanks to @EmptyJayy for pointing out some formatting issues and finding the correct ISE URL!]
Source for the test bench used in the annotated simulation is below.
LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
-- Uncomment the following library declaration if using
-- arithmetic functions with Signed or Unsigned values
--USE ieee.numeric_std.ALL;
ENTITY reg16_8_tb IS
END reg16_8_tb;
ARCHITECTURE behavior OF reg16_8_tb IS
-- Component Declaration for the Unit Under Test (UUT)
COMPONENT reg16_8
PORT(
I_clk : IN std_logic;
I_en : IN std_logic;
I_dataD : IN std_logic_vector(15 downto 0);
O_dataA : OUT std_logic_vector(15 downto 0);
O_dataB : OUT std_logic_vector(15 downto 0);
I_selA : IN std_logic_vector(2 downto 0);
I_selB : IN std_logic_vector(2 downto 0);
I_selD : IN std_logic_vector(2 downto 0);
I_we : IN std_logic
);
END COMPONENT;
--Inputs
signal I_clk : std_logic := '0';
signal I_en : std_logic := '0';
signal I_dataD : std_logic_vector(15 downto 0) := (others => '0');
signal I_selA : std_logic_vector(2 downto 0) := (others => '0');
signal I_selB : std_logic_vector(2 downto 0) := (others => '0');
signal I_selD : std_logic_vector(2 downto 0) := (others => '0');
signal I_we : std_logic := '0';
--Outputs
signal O_dataA : std_logic_vector(15 downto 0);
signal O_dataB : std_logic_vector(15 downto 0);
-- Clock period definitions
constant I_clk_period : time := 10 ns;
BEGIN
-- Instantiate the Unit Under Test (UUT)
uut: reg16_8 PORT MAP (
I_clk => I_clk,
I_en => I_en,
I_dataD => I_dataD,
O_dataA => O_dataA,
O_dataB => O_dataB,
I_selA => I_selA,
I_selB => I_selB,
I_selD => I_selD,
I_we => I_we
);
-- Clock process definitions
I_clk_process :process
begin
I_clk <= '0';
wait for I_clk_period/2;
I_clk <= '1';
wait for I_clk_period/2;
end process;
-- Stimulus process
stim_proc: process
begin
-- hold reset state for 100 ns.
wait for 100 ns;
wait for I_clk_period*10;
-- insert stimulus here
I_en <= '1';
-- test for writing.
-- r0 = 0xfab5
I_selA <= "000";
I_selB <= "001";
I_selD <= "000";
I_dataD <= X"FAB5";
I_we <= '1';
wait for I_clk_period;
-- r2 = 0x2222
I_selA <= "000";
I_selB <= "001";
I_selD <= "010";
I_dataD <= X"2222";
I_we <= '1';
wait for I_clk_period;
-- r3 = 0x3333
I_selA <= "000";
I_selB <= "001";
I_selD <= "010";
I_dataD <= X"3333";
I_we <= '1';
wait for I_clk_period;
--test just reading, with no write
I_selA <= "000";
I_selB <= "001";
I_selD <= "000";
I_dataD <= X"FEED";
I_we <= '0';
wait for I_clk_period;
--at this point dataA should not be 'feed'
I_selA <= "001";
I_selB <= "010";
wait for I_clk_period;
I_selA <= "011";
I_selB <= "100";
wait for I_clk_period;
I_selA <= "000";
I_selB <= "001";
I_selD <= "100";
I_dataD <= X"4444";
I_we <= '1';
wait for I_clk_period;
I_we <= '0';
wait for I_clk_period;
-- nop
wait for I_clk_period;
I_selA <= "100";
I_selB <= "100";
wait for I_clk_period;
wait;
end process;
END;