Close

System overview

A project log for Bit-serial CPU based on crossbar switch

256 switches and few shift registers to implement a working 16 or 32-bit integer arithmetic calculator (+, -, *, /, isqrt, BCD / bin conv..)

zpekiczpekic 04/24/2022 at 18:270 Comments

The calculator/CPU cannot function on its own, without being embedded in a system that gives it the I/O, clock, outside hardware connections etc. on the FPGA board.


The top-level object that does this is sys_sbc8816.vhd 

(simplified schema, bolded names correspond to components in the source code)

Why is this so complicated? The reason is that lots of debugging components have been added to the design:

For the basic operation of the calculator, only 3 components are needed:


Clock generation

All clocks derive from 100MHz clock integrated on the Anvyl board. From this, 3 separate paths are taken:

CPU clock is selectable using switches 2..0. 0 selects single step (useful for much painful debugging), and 7 is full-speed. In addition speeds 0..3 enable tracing the microcode.

-- select the clock
with sw_clksel select mt_cnt <= 
    ss_cnt    when "000",    -- single step
    freq_2048(9 downto 8) when "001",    -- 4Hz
    freq_2048(7 downto 6) when "010",    -- 16Hz
    freq_2048(5 downto 4) when "011",    -- 64Hz
    freq_50M(8 downto 7) when "100",        -- 0.195312
    freq_50M(7 downto 6) when "101",        -- 0.390625
    freq_50M(6 downto 5) when "110",        -- 0.781250
    freq_50M(5 downto 4) when others;    -- 1.5625MHz
    
-- 4 phase clock to activate strobe at right time
phi0 <= '1' when (mt_cnt = "00") else '0';
phi1 <= '1' when (mt_cnt = "01") else '0';
phi2 <= '1' when (mt_cnt = "10") else '0';
phi3 <= '1' when (mt_cnt = "11") else '0';

-- single step cnt
on_button3: process(button(3), ss_cnt)
begin
    if (rising_edge(button(3))) then
        ss_cnt <= std_logic_vector(unsigned(ss_cnt) + 1);
    end if;
end process;

MT8816 control signals have some requirement of setup and hold times before/after STB, so these are achieved by using a 4-phase clock, and activating control signals at their duty cycle. 

Instruction input

The CPU / calculator executes instructions which are 8-bit ASCII character code. These characters can come from:

Both of these generate a strobe pulse, and first one received wins, loading the ASCII value into input register. This is actually the "instruction register" of the CPU. This register holds the character until CPU clears it (instruction is executed). 

key <= rx_ready or kypd_keypressed;
input_clear <= '1' when (hc_status = status_done) else reset;

on_key: process(key, kypd_hex, kypd_shift, rx_char, input_clear)
begin
    if (input_clear = '1') then
        input <= X"00";
    else
        if (rising_edge(key)) then
            if (kypd_keypressed = '1') then
                input <= kypd2ascii(to_integer(unsigned(kypd_shift & kypd_hex)));
            else
                input <= rx_char;
            end if;
        end if;
    end if;
end process;

Inside the microcode there is simple loop waiting until input register !=0, meaning new character has been received. The input register could be replaced with a FIFO queue. 

//    indicate availability and wait for start signal
//    ---------------------------------------------------------------------------
deadloop:    STATUS = ready, if input_is_zero then repeat else next;
        echo(input);
        if true then fork else fork;    // jump to entry point per character code (see .map statements below)

Serial text output path

Two components can generate a stream of text (lines + CR + LF sequence):

Two components can handle these stream:

Which source goes to which destination can be selected with switch(6..5)


Visualizing CPU state

It is very useful for debugging and also to explain the concept to see the internals of the CPU, as execution is unfolding. There are few problems:

-- hardware window that shows the system state
win: hardwin Port map( 
        left => X"18", -- col 24, TODO make it dynamic
        top  => X"10",    -- row 16, TODO make it dynamic
        vga_x => vga_x,
        vga_y => vga_y,
        active => win_active,
        matrix => win_matrix,
        char     => win_char,
        index     => win_index,
        win_x  => win_x,
        win_y  => win_y,
        sy_cnt => sy_cnt,
        switch => switch, -- display the modes in the last row of the window
        mt_x   => hc_mt_x(to_integer(unsigned(win_y(3 downto 0)))),
        mt_y   => mt_y(to_integer(unsigned(win_x(3 downto 0)))),
        mt_c     => hc_carry,
        mt_d     => hc_delay, 
        mt_daa => hc_daa,
        mt_z     => hc_zero(to_integer(unsigned(win_y(3 downto 0)))),
        mt_hex => hc_reg
        );

The key are values vga_x (0 to 79) and vga_y (0 to 59) that are generated by VGA controller. If these values fall within the 32*32 where the window is positioned (note the "top, left" offset of origin), win_active is '1' and win_x and win_y are valid - these are fed to CPU to extract a particular value from the register (e.g. 1 : 7 means least significant nibble from register 1). The hardwin component then internally generates ASCII representation which is output as win_char. If it is !=0, this character takes precedence over the one on same location in video RAM, so it appears that window floats above scrollable text screen. 

The win_char is externally overriden in 16-bit mode and for the first 4 columns of the window to 0x00, meaning that it will be "chopped off" to hide upper 16-bits of the 8 registers:

-- if in 16-bit mode, simply chop off first four columns from hardware window (register bits 31..16)
win_char_x <= X"00" when ((sw_32 = '0') and (win_x(4 downto 2) = "000")) else win_char;

To show TOS on the 7-segment LED as an old fashioned calculator should, the hc_reg value should be intercepted at the right time, when win_y (row) is 0 and win_x (column) is 0...7, except 0 is most significant nibble, 7 least (only 2 to 7 are needed as the board has only 6 LEDs not 8). At those right moments, the values are captured into hc_tos display register which feeds the 7-seg using the led6:sixdigitsevensegmentled component in a standard multiplexed fashion. 

-- catch stacktop appearing to display it on the 7seg LED
on_dot_clk: process(dot_clk, win_x, win_y, hc_reg)
begin
    if (rising_edge(dot_clk)) then
        if (win_y = "00000") then
            case win_x is
                when "00010" =>
                    hc_tos(23 downto 20) <= hc_reg;
                when "00011" =>
                    hc_tos(19 downto 16) <= hc_reg;
                when "00100" =>
                    hc_tos(15 downto 12) <= hc_reg;
                when "00101" =>
                    hc_tos(11 downto 8) <= hc_reg;
                when "00110" =>
                    hc_tos(7 downto 4) <= hc_reg;
                when "00111" =>
                    hc_tos(3 downto 0) <= hc_reg;
                when others =>
                    null;
            end case;
        end if;
    end if;
end process;

Discussions