Beginning Logic Design – Part 10

Hello and welcome to part 10 of my Beginning Logic Design series!

There’s been a lot of ground covered so far. Through these posts I’ve implemented an ALU, a system bus, a RAM module, a ROM module. I explored state machines in the last post to build a design that can implement a more complex processing flow by breaking a process into individual operations.

All of this has laid out significant foundation to implement the final design of the series. A basic CPU and computer system!

Over the next few posts I will cover my planning, design and testing for this CPU. The planning will be very light, and this will certainly be an inefficient design in many ways… but I’ll keep changing it until it works!



System Design

I’m starting this design by planning my system architecture, how my largest building blocks will communicate.

For this design I’ve decided to give my ROM the full upper half of my address space, 32KB of ROM. I decided to split the lower half between 16KB of RAM and 16KB of I/O space. I felt this was a good balance of the address space and it’ll be easy to implement.

My system architecture

CPU Design and Instruction Set

There are many decisions to make in designing a CPU and computer system. For this design I’m going to build an 8-bit CPU with a 16-bit system bus to stick with my previous posts.

It will have 4 general purpose registers to use with the various instructions, they will be referred to as A, B and C.

Internally there will also be a PC (program counter) register to keep track of the memory address of the current instruction. An 8-bit stack register, S, will be held for stack related operations. There will be an instruction register to hold the current CPU instruction. It will also have the same status flags as my ALU had, zero, sign, overflow and carry.

The next major piece I have in mind is the instruction set architecture (ISA) itself. The ISA describes my machine code language that the processor will support. I’ve stirred over my ISA design and feel pretty certain that I will end up implementing some operations I don’t need while not thinking of others that would be extremely useful. I accept this and will push forward with a less elegant design to learn the lessons that could be applied to a future one.

My instruction format is split between 4 bits for the instruction family, and 4 bits to be interpreted different by the instruction.

Here are the instruction families I’m initially running with.

  1. 0: ADD
  2. 1: SUBTRACT
  3. 2: INCREMENT
  4. 3: DECREMENT
  5. 4: BIT_AND
  6. 5: BIT_OR
  7. 6: BIT_XOR
  8. 7: BIT_NOT
  9. 8: SHIFT_LEFT
  10. 9: SHIFT_RIGHT
  11. 10: ROTATE_LEFT
  12. 11: ROTATE_RIGHT
  13. 12: LOAD
  14. 13: STORE
  15. 14: BRANCH
  16. 15: EXTRA
0:  ADD
1:  SUBTRACT
2:  INCREMENT
3:  DECREMENT
4:  BIT_AND
5:  BIT_OR
6:  BIT_XOR
7:  BIT_NOT
8:  SHIFT_LEFT
9:  SHIFT_RIGHT
10: ROTATE_LEFT
11: ROTATE_RIGHT
12: LOAD
13: STORE
14: BRANCH
15: EXTRA

The first 12 operations are intentionally identical to the ALU operations, the latter 4 are for loading and storing register values, program code branching and other miscellaneous processor operations.

The CPU will also have a state machine to manage its flow of operation.

CPU state flow

I think that’s about enough planning for now, I want to start laying down the foundations so I can begin the actual designing.

Ground Work

I’m going to use my go-to Makefile to organize my building and testing.

Then I will setup my top module to include my CPU and system bus components.

  1. `timescale 1ns / 1ps
  2. module top ();
  3. logic clock;
  4. logic reset;
  5. // System Bus
  6. logic slave_clock;
  7. logic read;
  8. logic write;
  9. logic [15:0] address_bus;
  10. wire logic [7:0] data_bus;
  11. assign slave_clock = ~clock;
  12. cpu processor (
  13. reset,
  14. clock,
  15. read,
  16. write,
  17. address_bus,
  18. data_bus
  19. );
  20. ram memory (
  21. slave_clock,
  22. read,
  23. write,
  24. address_bus,
  25. data_bus
  26. );
  27. io devices (
  28. slave_clock,
  29. read,
  30. write,
  31. address_bus,
  32. data_bus
  33. );
  34. rom storage (
  35. slave_clock,
  36. read,
  37. write,
  38. address_bus,
  39. data_bus
  40. );
  41. initial begin
  42. clock = 0;
  43. reset = 1;
  44. #2 reset = 0;
  45. end
  46. always begin
  47. #1 clock = ~clock;
  48. end
  49. endmodule
`timescale 1ns / 1ps

module top ();
  logic clock;
  logic reset;

  // System Bus
  logic slave_clock;
  logic read;
  logic write;
  logic [15:0] address_bus;
  wire logic [7:0] data_bus;

  assign slave_clock = ~clock;

  cpu processor (
    reset,
    clock,
    read,
    write,
    address_bus,
    data_bus
  );

  ram memory (
    slave_clock,
    read,
    write,
    address_bus,
    data_bus
  );

  io devices (
    slave_clock,
    read,
    write,
    address_bus,
    data_bus
  );

  rom storage (
    slave_clock,
    read,
    write,
    address_bus,
    data_bus
  );

  initial begin
    clock = 0;
    reset = 1;
    #2 reset = 0;
  end

  always begin
    #1 clock = ~clock;
  end

endmodule

Then I’ll start my cpu module with a few of the planned features. I’ll include my cpu_state enumeration, the various registers/flags and I’ll add support for system bus write operations.

  1. `timescale 1ns / 1ps
  2. typedef enum logic [1:0] {
  3. RESET,
  4. FETCH,
  5. PERFORM,
  6. HALT
  7. } cpu_state;
  8. module cpu (
  9. input logic reset,
  10. input logic clock,
  11. output logic read,
  12. output logic write,
  13. output logic [15:0] address_bus,
  14. inout logic [7:0] data_bus
  15. );
  16. // CPU internals
  17. cpu_state state;
  18. logic [15:0] program_counter;
  19. logic [7:0] stack;
  20. logic [7:0] instruction;
  21. // General purpose registers
  22. logic [7:0] a;
  23. logic [7:0] b;
  24. logic [7:0] c;
  25. // System Bus support
  26. logic [7:0] write_data;
  27. assign data_bus = write ? write_data : 'bZ;
  28. endmodule
`timescale 1ns / 1ps

typedef enum logic [1:0] {
  RESET,
  FETCH,
  PERFORM,
  HALT
} cpu_state;

module cpu (
  input logic reset,
  input logic clock,
  output logic read,
  output logic write,
  output logic [15:0] address_bus,
  inout logic [7:0] data_bus
);
  // CPU internals
  cpu_state state;
  logic [15:0] program_counter;
  logic [7:0] stack;
  logic [7:0] instruction;

  // General purpose registers
  logic [7:0] a;
  logic [7:0] b;
  logic [7:0] c;

  // System Bus support
  logic [7:0] write_data;
  assign data_bus = write ? write_data : 'bZ;

endmodule

I’ll next pull in my RAM module from post 8. I’ll modify it slightly to look at the first two address bits and to contain 16KB of memory instead of 32KB.

  1. `timescale 1ns / 1ps
  2. module ram (
  3. input logic clock,
  4. input logic read,
  5. input logic write,
  6. input logic [15:0] address_bus,
  7. inout logic [7:0] data_bus
  8. );
  9. assign data_bus = (read && address_bus[15:14] == 0) ? memory[address_bus[13:0]] : 'bZ;
  10. logic [7:0] memory [0:(1<<14)-1];
  11. always_ff @ (posedge clock) begin
  12. if (address_bus[15:14] == 0 && write) begin
  13. memory[address_bus[13:0]] <= data_bus;
  14. end
  15. end
  16. endmodule
`timescale 1ns / 1ps

module ram (
  input logic clock,
  input logic read,
  input logic write,
  input logic [15:0] address_bus,
  inout logic [7:0] data_bus
);

  assign data_bus = (read && address_bus[15:14] == 0) ? memory[address_bus[13:0]] : 'bZ;

  logic [7:0] memory [0:(1<<14)-1];

  always_ff @ (posedge clock) begin
    if (address_bus[15:14] == 0 && write) begin
      memory[address_bus[13:0]] <= data_bus;
    end
  end


endmodule

I’ll also modify the writer module from post 8 to listen to the appropriate addresses and will change the output format to show both the address and the data.

  1. `timescale 1ns / 1ps
  2. module io (
  3. input logic clock,
  4. input logic read,
  5. input logic write,
  6. input logic [15:0] address_bus,
  7. inout logic [7:0] data_bus
  8. );
  9. always_ff @ (posedge clock) begin
  10. if (address_bus[15:14] == 1 && write) begin
  11. $display("%h: %h (%c)", address_bus, data_bus, data_bus);
  12. end
  13. end
  14. endmodule
`timescale 1ns / 1ps

module io (
  input logic clock,
  input logic read,
  input logic write,
  input logic [15:0] address_bus,
  inout logic [7:0] data_bus
);

  always_ff @ (posedge clock) begin
    if (address_bus[15:14] == 1 && write) begin
      $display("%h: %h (%c)", address_bus, data_bus, data_bus);
    end
  end


endmodule

The ROM module can be directly pulled in from the previous post.

  1. `timescale 1ns / 1ps
  2. module rom (
  3. input logic clock,
  4. input logic read,
  5. input logic write,
  6. input logic [15:0] address_bus,
  7. inout logic [7:0] data_bus
  8. );
  9. assign data_bus = (read && address_bus[15] == 1) ? memory[address_bus[14:0]] : 'bZ;
  10. logic [7:0] memory [0:(1<<15)-1];
  11. initial begin
  12. $readmemh("rom.hex", memory);
  13. end
  14. endmodule
`timescale 1ns / 1ps

module rom (
  input logic clock,
  input logic read,
  input logic write,
  input logic [15:0] address_bus,
  inout logic [7:0] data_bus
);

  assign data_bus = (read && address_bus[15] == 1) ? memory[address_bus[14:0]] : 'bZ;

  logic [7:0] memory [0:(1<<15)-1];

  initial begin
    $readmemh("rom.hex", memory);
  end


endmodule

With these base module in place, I attempt a build to catch the various syntax errors I’ve made along the way to ensure the code I’m sharing actually works 🙂

Processor Functionality

With the foundation set, we can start building! The first part I want to implement is the case statement to handle the various states and the reset logic.

  1. always_ff @ (posedge clock or posedge reset) begin
  2. if (reset) begin
  3. state <= RESET;
  4. end else begin
  5. case (state)
  6. RESET: begin
  7. state <= FETCH;
  8. program_counter <= 'h8000;
  9. stack <= 0;
  10. read <= 0;
  11. write <= 0;
  12. address_bus <= 0;
  13. write_data <= 0;
  14. end
  15. FETCH: begin
  16. state <= PERFORM;
  17. end
  18. PERFORM: begin
  19. state <= HALT;
  20. end
  21. HALT: begin
  22. $finish();
  23. end
  24. endcase
  25. end
  26. end
always_ff @ (posedge clock or posedge reset) begin
  if (reset) begin
    state <= RESET;
  end else begin
    case (state)
      RESET: begin
        state <= FETCH;
        program_counter <= 'h8000;
        stack <= 0;
        read <= 0;
        write <= 0;
        address_bus <= 0;
        write_data <= 0;
      end
      FETCH: begin
        state <= PERFORM;
      end
      PERFORM: begin
        state <= HALT;
      end
      HALT: begin
        $finish();
      end
    endcase
  end
end

For now the processing loop just progresses one step at a time until it halts, via simulation I can verify all the important things are cleared, and I can see what’s left unset as well.

Next up, I want to start fetching instructions from ROM similar to how I fetched data from ROM in the previous post. I have the program_counter register as my instruction pointer. The fetch will have 2 cycles, a read request, and the read itself. Since an operation will eventually transition back to FETCH and this state never writes, I’ll ensure write  is set low here as well.

  1. FETCH: begin
  2. write <= 0;
  3. if (!read) begin
  4. read <= 1;
  5. address_bus <= program_counter;
  6. end else begin
  7. read <= 0;
  8. instruction <= data_bus;
  9. state <= PERFORM;
  10. end
  11. end
FETCH: begin
  write <= 0;
  if (!read) begin
    read <= 1;
    address_bus <= program_counter;
  end else begin
    read <= 0;
    instruction <= data_bus;
    state <= PERFORM;
  end
end

So this should fetch our first instruction, I’ll test it! Here’s my first ROM (in hex format):

  1. de ad be ef
de ad be ef

It’s a total garbage program 🙂 but it’s just here to make sure our instruction register becomes set to de, the first byte. In simulation it does work just fine, as FETCH transitions to PERFORM my instruction register becomes de!

Now, to more easily identify what this de instruction is, I’m going to add another enumeration for my operation family.

  1. typedef enum logic [3:0] {
  2. CPU_ADD,
  3. CPU_SUBTRACT,
  4. CPU_INCREMENT,
  5. CPU_DECREMENT,
  6. CPU_AND,
  7. CPU_OR,
  8. CPU_XOR,
  9. CPU_NOR,
  10. CPU_SHIFT_LEFT,
  11. CPU_SHIFT_RIGHT,
  12. CPU_ROTATE_LEFT,
  13. CPU_ROTATE_RIGHT,
  14. LOAD,
  15. STORE,
  16. BRANCH,
  17. EXTRA
  18. } instruction_type;
typedef enum logic [3:0] {
  CPU_ADD,
  CPU_SUBTRACT,
  CPU_INCREMENT,
  CPU_DECREMENT,
  CPU_AND,
  CPU_OR,
  CPU_XOR,
  CPU_NOR,
  CPU_SHIFT_LEFT,
  CPU_SHIFT_RIGHT,
  CPU_ROTATE_LEFT,
  CPU_ROTATE_RIGHT,
  LOAD,
  STORE,
  BRANCH,
  EXTRA
} instruction_type;

Then, in my CPU internals, I’ll add an instance of this type. My intention is that this will refer to the 4 most significant bits of my instruction register. I’ll add instruction_type op_type; to my CPU internal variables.

Then, I’ll use $cast(); to map the upper 4 bits of the 8-bit logic type to my instruction_type variable. If I did not cast this, Vivado would be unhappy with me. I’ll put this bit of code in my FETCH state.

  1. FETCH: begin
  2. write <= 0;
  3. if (!read) begin
  4. read <= 1;
  5. address_bus <= program_counter;
  6. end else begin
  7. read <= 0;
  8. instruction <= data_bus;
  9. $cast(op_type, data_bus[7:4]);
  10. state <= PERFORM;
  11. end
  12. end
FETCH: begin
  write <= 0;
  if (!read) begin
    read <= 1;
    address_bus <= program_counter;
  end else begin
    read <= 0;
    instruction <= data_bus;
    $cast(op_type, data_bus[7:4]);
    state <= PERFORM;
  end
end

Now I’ll check this in simulation.

Huzzah! I have fetched the instruction and can identify it’s family. Here’s my cpu module at this point:

  1. `timescale 1ns / 1ps
  2. typedef enum logic [1:0] {
  3. RESET,
  4. FETCH,
  5. PERFORM,
  6. HALT
  7. } cpu_state;
  8. typedef enum logic [3:0] {
  9. CPU_ADD,
  10. CPU_SUBTRACT,
  11. CPU_INCREMENT,
  12. CPU_DECREMENT,
  13. CPU_AND,
  14. CPU_OR,
  15. CPU_XOR,
  16. CPU_NOR,
  17. CPU_SHIFT_LEFT,
  18. CPU_SHIFT_RIGHT,
  19. CPU_ROTATE_LEFT,
  20. CPU_ROTATE_RIGHT,
  21. LOAD,
  22. STORE,
  23. BRANCH,
  24. EXTRA
  25. } instruction_type;
  26. module cpu (
  27. input logic reset,
  28. input logic clock,
  29. output logic read,
  30. output logic write,
  31. output logic [15:0] address_bus,
  32. inout logic [7:0] data_bus
  33. );
  34. // CPU internals
  35. cpu_state state;
  36. logic [15:0] program_counter;
  37. logic [7:0] stack;
  38. logic [7:0] instruction;
  39. instruction_type op_type;
  40. // General purpose registers
  41. logic [7:0] a;
  42. logic [7:0] b;
  43. logic [7:0] c;
  44. // System Bus support
  45. logic [7:0] write_data;
  46. assign data_bus = write ? write_data : 'bZ;
  47. always_ff @ (posedge clock or posedge reset) begin
  48. if (reset) begin
  49. state <= RESET;
  50. end else begin
  51. case (state)
  52. RESET: begin
  53. state <= FETCH;
  54. program_counter <= 'h8000;
  55. stack <= 0;
  56. read <= 0;
  57. write <= 0;
  58. address_bus <= 0;
  59. write_data <= 0;
  60. end
  61. FETCH: begin
  62. write <= 0;
  63. if (!read) begin
  64. read <= 1;
  65. address_bus <= program_counter;
  66. end else begin
  67. read <= 0;
  68. instruction <= data_bus;
  69. $cast(op_type, data_bus[7:4]);
  70. state <= PERFORM;
  71. end
  72. end
  73. PERFORM: begin
  74. state <= HALT;
  75. end
  76. HALT: begin
  77. $finish();
  78. end
  79. endcase
  80. end
  81. end
  82. endmodule
`timescale 1ns / 1ps

typedef enum logic [1:0] {
  RESET,
  FETCH,
  PERFORM,
  HALT
} cpu_state;

typedef enum logic [3:0] {
  CPU_ADD,
  CPU_SUBTRACT,
  CPU_INCREMENT,
  CPU_DECREMENT,
  CPU_AND,
  CPU_OR,
  CPU_XOR,
  CPU_NOR,
  CPU_SHIFT_LEFT,
  CPU_SHIFT_RIGHT,
  CPU_ROTATE_LEFT,
  CPU_ROTATE_RIGHT,
  LOAD,
  STORE,
  BRANCH,
  EXTRA
} instruction_type;

module cpu (
  input logic reset,
  input logic clock,
  output logic read,
  output logic write,
  output logic [15:0] address_bus,
  inout logic [7:0] data_bus
);
  // CPU internals
  cpu_state state;
  logic [15:0] program_counter;
  logic [7:0] stack;
  logic [7:0] instruction;
  instruction_type op_type;

  // General purpose registers
  logic [7:0] a;
  logic [7:0] b;
  logic [7:0] c;

  // System Bus support
  logic [7:0] write_data;
  assign data_bus = write ? write_data : 'bZ;

  always_ff @ (posedge clock or posedge reset) begin
    if (reset) begin
      state <= RESET;
    end else begin
      case (state)
        RESET: begin
          state <= FETCH;
          program_counter <= 'h8000;
          stack <= 0;
          read <= 0;
          write <= 0;
          address_bus <= 0;
          write_data <= 0;
        end
        FETCH: begin
          write <= 0;
          if (!read) begin
            read <= 1;
            address_bus <= program_counter;
          end else begin
            read <= 0;
            instruction <= data_bus;
            $cast(op_type, data_bus[7:4]);
            state <= PERFORM;
          end
        end
        PERFORM: begin
          state <= HALT;
        end
        HALT: begin
          $finish();
        end
      endcase
    end
  end

endmodule

With a significant start to the organization and flow to this CPU, I will call that a wrap for this post. In the next post I will begin implementing some of the planned instructions. As always, I welcome your feedback and questions in the comments. Keep tinkering!

Leave a Reply