Part 2 – a simple test core
To demonstrate how the control module is built, we need a core to which we can add the control module. In the interests of keeping the project as simple as possible and avoiding needless distractions, I’ve started a new project for this purpose, which can be found on github at https://github.com/robinsonb5/CtrlModuleTutorial
I shall tag this at key points, and at the time of writing there are two tags in place.
To play with this, check out a local copy of the core, like so:
> git clone https://github.com/robinsonb5/CtrlModuleTutorial.git > cd CtrlModuleTutorial > git submodule init > git submodule update > git checkout <tag name>
The first tag, called “StartingPoint” contains a VGA test pattern generator for the DE1 board, which has four slightly different test patterns selectable by the DE1’s switches. In the coming parts I shall show how to eliminate the switches and replace them with an On Screen Display.
The project directory contains DE1-specific files in Board/de1, Project-specific HDL files in RTL/, and Quartus project files in fpga/de1
The second tag, called “Step1” adds a simplified control module, and all files specific to the Control Module will be in the directory CtrlModule.
The module is instantiated like so:
MyCtrlModule : entity work.CtrlModule port map ( clk => CLK, reset_n => reset, -- DIP switches dipswitches(1 downto 0) => testpattern -- Replaces previous binding from the physical DIP switches );
In this simplified form the control module requires only a clock and reset signal, and supplies an output signal which is under CPU control. The control module in its simplified form looks like this:
library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.numeric_std.ALL; library work; use work.zpupkg.ALL; entity CtrlModule is generic ( sysclk_frequency : integer := 1000 -- Sysclk frequency * 10 ); port ( clk : in std_logic; reset_n : in std_logic; -- DIP switches dipswitches : out std_logic_vector(15 downto 0) ); end entity; architecture rtl of CtrlModule is -- ZPU signals constant maxAddrBit : integer := 20; -- Optional - defaults to 32 - but helps keep the logic element count down. signal mem_busy : std_logic; signal mem_read : std_logic_vector(wordSize-1 downto 0); signal mem_write : std_logic_vector(wordSize-1 downto 0); signal mem_addr : std_logic_vector(maxAddrBit downto 0); signal mem_writeEnable : std_logic; signal mem_readEnable : std_logic; signal mem_hEnable : std_logic; signal mem_bEnable : std_logic; signal zpu_to_rom : ZPU_ToROM; signal zpu_from_rom : ZPU_FromROM; begin
The interface between the ZPUFlex and its ROM is encapsulated in the ZPU_[To|From]ROM types, so we don’t have to worry about those, apart from making sure that they’re connected to the ROM – but any accesses that fall outside the ROM will be notified by the CPU using the mem_* signals. At this stage we won’t add any SDRAM access or suchlike, but we do need to implement some hardware registers for the ZPU’s ROM to poke. When the ZPU needs to access a register, it will place the address on the mem_addr signal, then assert either mem_readEnable or mem_writeEnable for a single cycle. Our circuitry needs to respond by acting upon the data on mem_write or placing data on mem_read, then driving mem_busy low for a single cycle.
The mem_hEnable and mem_bEnable are asserted as well as the read/writeEnable signals if the CPU’s performing a halfword (16-bit) or byte access. At this stage we can ignore those.
Here we instantiate the ROM itself, which is built by the makefile in CtrlROM/Firmware. CtrlROM/Firmware/CtrlROM_ROM.vhd will need to be added to the project.
The ROM as it stands simply cycles between writing “00”, “01”, “10” and “11” to a hardware register, which sends the written value to the host core, changing which test pattern is displayed.
-- ROM myrom : entity work.CtrlROM_ROM generic map ( maxAddrBitBRAM => 13 -- This needs to match the signal of the same name in the ZPU's instantiation. ) port map ( clk => clk, from_zpu => zpu_to_rom, to_zpu => zpu_from_rom );
Now we instantiate the ZPU itself. (The ZPUFlex requires that ZPUFlex/RTL/zpu_core_flex.vhd and ZPUFlex/RTL/zpu_pkg.vhd are added to the project.)
There are a number of options that can be set here, mostly relating to which optional instructions are enabled. If we want to reduce the logic-element footprint of the ZPU we can do so by disabling these – but if we do, then we have to include emulation code in the ROM, adding to the block RAM footprint, so it’s a case of having to strike a balance.
-- Main CPU -- We instantiate the CPU with the optional instructions enabled, which allows us to reduce -- the size of the ROM by leaving out emulation code. zpu: zpu_core_flex generic map ( IMPL_MULTIPLY => true, IMPL_COMPARISON_SUB => true, IMPL_EQBRANCH => true, IMPL_STOREBH => true, IMPL_LOADBH => true, IMPL_CALL => true, IMPL_SHIFT => true, IMPL_XOR => true, REMAP_STACK => false, -- We're not using SDRAM so no need to remap the Boot ROM / Stack RAM EXECUTE_RAM => false, -- We don't need to execute code from external RAM. maxAddrBit => maxAddrBit, maxAddrBitBRAM => 13 ) port map ( clk => clk, reset => not reset_n, in_mem_busy => mem_busy, mem_read => mem_read, mem_write => mem_write, out_mem_addr => mem_addr, out_mem_writeEnable => mem_writeEnable, out_mem_hEnable => mem_hEnable, out_mem_bEnable => mem_bEnable, out_mem_readEnable => mem_readEnable, from_rom => zpu_from_rom, to_rom => zpu_to_rom );
So far so good.
Now to define our hardware register. We’re actually fairly free to place this anywhere we like in the memory map, provided we don’t clash with the program ROM, but there’s a ZPU convention that IO should happen in the top half of the memory map. Because of the way the instruction set works, it’s more efficient for registers to be at the very top of the memory map, so we’ll place our “dipswitch” register at 0xfffffffc. (We’ve actually configured the ZPU to use fewer than 32-bits for addressing, and we’re not going to do a complete address decoding anyway, but the ROM itself won’t care, so the ROM’s source code defines the register as 0xfffffffc.)
I’m actually going to divide the upper memory space in 256-byte chunks and decode them separately, which will make life easier in future parts. For now it might look a bit odd to be decoding the first “F” separately from the “FC”, but…
process(clk) begin if reset_n='0' then elsif rising_edge(clk) then mem_busy<='1'; -- Write from CPU? if mem_writeEnable='1' then -- we decode just a partial address to save logic elements. case mem_addr(maxAddrBit)&mem_addr(10 downto 8) is when X"F" => -- Peripherals at 0xFFFFFF00 case mem_addr(7 downto 0) is when X"FC" => -- Host SW mem_busy<='0'; dipswitches<=mem_write(15 downto 0); when others => -- Prevent a hang if we accidentally access a register that doesn't exist. mem_busy<='0'; null; end case; when others => mem_busy<='0'; end case; -- Read from CPU? elsif mem_readEnable='1' then case mem_addr(maxAddrBit)&mem_addr(10 downto 8) is when X"F" => -- Peripherals case mem_addr(7 downto 0) is -- We don't have any readable registers yet. when others => -- Prevent a hang if we accidentally access a register that doesn't exist. mem_busy<='0'; null; end case; when others => mem_busy<='0'; end case; end if; end if; -- rising-edge(clk) end process; end architecture;
The complete project displays one of four test patterns, and cycles rapidly between them under the control of the ZPU.
In the coming parts, we'll add interrupt handling, PS/2 keyboard control, the On-screen Display itself and add a menu system.