Hello and welcome to Part 6 of my Beginning Logic Design series. In the last post I laid out the start of an ALU design. In this round I will be completing the ADD operation including support for the various status flags and build some tests to validate it.
As a quick reference, here’s the description I laid out in the previous post.
The ALU I’ll be designing in this post will be influenced by the classic 6502 processor that’s at the heart of the NES, Apple II and many other early computer systems. This ALU with support 12 operations:
- Arithmetic Operations
- Add with Carry
- Subtract with Carry
- Increment
- Decrement
- Logic Operations
- AND
- OR
- XOR
- NOT/Negate (not in 6502, but I want my ALU to have it!)
- Shifting Operations
- Arithmetic Shift Left
- Logical Shift Right
- Rotate Left
- Rotate Right
This ALU will also have a few status signals, on the input side it will just have
carry in
, on the output side we’ll havecarry out
,zero
,sign
andoverflow
. I’ll dive a bit more into each of these operations and how the interact with status signals as we implement them.
Adding Status Signals to Addition
To complete out the ADD
operation as specified, it needs to factor in a carry
input signal and output the other status signals as well.
Following the general structure of what the 6502 architecture does, I’ll reference this 6502 opcode document for which flags are modified by which operations, and this 6502 processor document as a reference to the meanings for each flag. I also found this article on the overflow flag helpful to wrap my head around its meaning and purpose.
To understand what all these flags mean, some familiarity with representing signed numbers and two’s complement is needed to understand the semantics of these flags, and it also helps to understand how subtraction is implemented in digital machines. I suggest this Computerphile video for an overview on unsigned, signed magnitude and twos complement binary representation.
Here’s how each flag should be responding in our addition circuit:
zero
should be 1 if the result of the addition is zerosign
should be 1 if the the result could be interpreted as a negative number (in signed addition)carry
should be 1 if the addition results in an unsigned number that is wider than 8 bitsoverflow
should be 1 if the addition resulted in a number that is outside of the range supported by an 8 bit signed number:sum < -128
orsum > 127
To get started on this, the first thing I’ll do is add the input and output ports for these signals to my ALU. I’ll also use assign
to set them all to 0
for now.
- module alu (
- input logic clock,
- input opcode operation,
- input logic [7:0] a,
- input logic [7:0] b,
- input logic carry_in,
- output logic [7:0] y,
- output logic zero,
- output logic sign,
- output logic carry_out,
- output logic overflow
- );
- assign zero = 0,
- sign = 0,
- carry_out = 0,
- overflow = 0;
Then I’ll also update my top
module to include variables to route these in and out of the alu
.
- module top ();
- logic clock;
- opcode operation;
- logic [7:0] a;
- logic [7:0] b;
- logic carry_in;
- logic [7:0] y;
- logic zero;
- logic sign;
- logic carry_out;
- logic overflow;
- alu myALU (
- clock,
- operation,
- a,
- b,
- carry_in,
- y,
- zero,
- sign,
- carry_out,
- overflow
- );
- ...
I’ll also add carry_in = 0;
in my initial
block within top
to set that signal to 0
at the start.
Test Driven Development
Many software developers are supporters of test driven development, but in the hardware world the concept of writing your tests before your implementation this is the norm.
It can be incredibly easy to add a flaw to our designs. The ADD
operation here supports signed and unsigned addition. Interestingly enough the only difference between signed and unsigned addition is what the number means to you, the potential user of the ALU. If we do the operation 80 + 80
, and intended that be unsigned addition result would be 160
, but we intended signed addition it would erroneously be -96
because the number went beyond the maximum number a signed 8-bit number can support (127).
If we look at this in hexadecimal it is 0x50 + 0x50 = 0xA0
in either case, but the hexadecimal number 0xA0
has a different value if looked at as a signed (160
) or unsigned (-96
) number. We use the the carry_out
status flag to let us know when our addition exceeds what can be accurately represented in our unsigned integer range [0, 255]
. Similarly, we use the overflow
status flag to know when a signed addition goes beyond what can accurately be represented in our signed integer range [-128, 127]
.
Implementing an adder that let’s us see all of this can be tricky, so let’s identify some of the special cases we need to be able to handle. The first round of tests I will write will use the table from this article on the 6502 overflow flag as a base.
I’ll now start writing some tests for my ALU. For this, I’ll use assert
statements to check if the outputs are matching what I’d expect. Here’s the top module modified to include a test that validates a case where both overflow
and carry
should be set.
- initial begin
- clock = 0;
- // Test 208 + 144
- a = 208;
- b = 144;
- carry_in = 0;
- operation = ADD;
- #2 assert(y == 96);
- assert(zero == 0);
- assert(sign == 0);
- assert(carry_out == 1);
- assert(overflow == 1);
- #1 $finish();
- end
I added $finish();
at the end to stop simulation there. Now I’ll fire off the simulator and see how it goes.
In the simulator window there’s no indication of an error, but in the Tcl Console at the bottom I can see some output that indicates some assertions failed.
- Error: Assertion violation
- Time: 2 ns Iteration: 0 Process: /top/Initial30_1 File: /home/kwilke/suchprogramming/my-alu/my-alu.srcs/sources_1/new/top.sv
- Error: Assertion violation
- Time: 2 ns Iteration: 0 Process: /top/Initial30_1 File: /home/kwilke/suchprogramming/my-alu/my-alu.srcs/sources_1/new/top.sv
- $finish called at time : 3 ns : File "/home/kwilke/suchprogramming/my-alu/my-alu.srcs/sources_1/new/top.sv" Line 44
The error message shows when the assertion failed, but no information is given as to why. To make the error more informational I can add an else
statement followed by a $error()
call to have the simulator spit out a more descriptive error.
- #2 assert(y == 96) else $error("Sum assertion failed.");
- assert(zero == 0) else $error("Zero assertion failed.");
- assert(sign == 0) else $error("Sign assertion failed.");
- assert(carry_out == 1) else $error("Carry assertion failed.");
- assert(overflow == 1) else $error("Overflow assertion failed.");
Now when I run the simulation, I can more clearly see what has failed.
- Error: Carry assertion failed.
- Time: 2 ns Iteration: 0 Process: /top/Initial30_1 File: /home/kwilke/suchprogramming/my-alu/my-alu.srcs/sources_1/new/top.sv
- Error: Overflow assertion failed.
- Time: 2 ns Iteration: 0 Process: /top/Initial30_1 File: /home/kwilke/suchprogramming/my-alu/my-alu.srcs/sources_1/new/top.sv
- $finish called at time : 3 ns : File "/home/kwilke/suchprogramming/my-alu/my-alu.srcs/sources_1/new/top.sv" Line 44
Implementing The Flags
Finding the zero
flag is easy, we just need to see if our output is zero.
The sign
flag is also easy, we just need to check if the most significant output bit is 1
.
The carry
flag we can get by looking the result of our addition as 9 bits and looking at the most significant bit there.
The overflow
flag is the trickiest, but the easiest way to get that is to take the carry
of the 6th (counting from 0) bit’s sum and XOR that with the final carry
output.
If I use the y <= a + b;
syntax I have now, I can’t access any of the carry values. I need access to the last two carry values to set all of our flags properly. I also want to start factoring the carry in
signal.
To break these needs up into easily accessed chunks, I’ll declare a few more internal variables.
- logic [7:0] add;
- logic [6:0] add_lower_bits;
- logic add_carry_6;
- logic add_carry_7;
Next, I’ll define some combinational circuits with assign
. I’ll first write an assignment that adds the lower bits and provides me my 6th carry bit.
- assign {add_carry_6, add_lower_bits} = a[6:0] + b[6:0] + carry_in,
- {add_carry_7, add} = a + b + carry_in;
The curly brackets here combine bits together. In the first statement the addition on the right results 8 bits of output and I use the brackets on the left to put the highest bit in add_carry_6
and the remaining 7 bits in add_lower_bits
. In this design I don’t use the lower bits but I set the size so I can easily get to that internal carry. I’ll use a similar assignment for the final carry and sum.
With easy access to all of the addition results I need, I can modify my ADD
operation to use them to set my output and flags.
- ADD: begin
- y <= add;
- carry_out <= add_carry_7;
- sign <= add[7];
- overflow <= add_carry_7 ^ add_carry_6;
- if (add == 0)
- zero <= 1;
- else
- zero <= 0;
- end
Now I’ll need to remove my earlier assign
block that was setting all my output flags to 0
, and I can run the simulation again!
It works! I’ll write a few more tests to validate some of the other operations but it should be good to go now! Here’s how my files have ended up.
top.sv
- `timescale 1ns / 1ps
- import ALU::*;
- module top ();
- logic clock;
- opcode operation;
- logic [7:0] a;
- logic [7:0] b;
- logic carry_in;
- logic [7:0] y;
- logic zero;
- logic sign;
- logic carry_out;
- logic overflow;
- alu myALU (
- clock,
- operation,
- a,
- b,
- carry_in,
- y,
- zero,
- sign,
- carry_out,
- overflow
- );
- initial begin
- clock = 0;
- operation = ADD;
- // Test 208 + 144
- a = 208;
- b = 144;
- carry_in = 0;
- #2 assert(y == 96) else $error("Sum assertion failed.");
- assert(zero == 0) else $error("Zero assertion failed.");
- assert(sign == 0) else $error("Sign assertion failed.");
- assert(carry_out == 1) else $error("Carry assertion failed.");
- assert(overflow == 1) else $error("Overflow assertion failed.");
- // Test 208 + 48 (signed -48 + 48)
- a = 208;
- b = 48;
- carry_in = 0;
- #2 assert(y == 0) else $error("Sum assertion failed.");
- assert(zero == 1) else $error("Zero assertion failed.");
- assert(sign == 0) else $error("Sign assertion failed.");
- assert(carry_out == 1) else $error("Carry assertion failed.");
- assert(overflow == 0) else $error("Overflow assertion failed.");
- // Test 80 + 80
- a = 80;
- b = 80;
- carry_in = 0;
- #2 assert(y == 160) else $error("Sum assertion failed.");
- assert(zero == 0) else $error("Zero assertion failed.");
- assert(sign == 1) else $error("Sign assertion failed.");
- assert(carry_out == 0) else $error("Carry assertion failed.");
- assert(overflow == 1) else $error("Overflow assertion failed.");
- #1 $finish();
- end
- always begin
- #1 clock = ~clock;
- end
- endmodule
alu.sv
- `timescale 1ns / 1ps
- package ALU;
- typedef enum logic [3:0] {
- ADD,
- SUBTRACT,
- INCREMENT,
- DECREMENT,
- BIT_AND,
- BIT_OR,
- BIT_XOR,
- BIT_NOT,
- SHIFT_LEFT,
- SHIFT_RIGHT,
- ROTATE_LEFT,
- ROTATE_RIGHT
- } opcode;
- endpackage
- import ALU::*;
- module alu (
- input logic clock,
- input opcode operation,
- input logic [7:0] a,
- input logic [7:0] b,
- input logic carry_in,
- output logic [7:0] y,
- output logic zero,
- output logic sign,
- output logic carry_out,
- output logic overflow
- );
- logic [7:0] add;
- logic [6:0] add_lower_bits;
- logic add_carry_6;
- logic add_carry_7;
- assign {add_carry_6, add_lower_bits} = a[6:0] + b[6:0] + carry_in,
- {add_carry_7, add} = a + b + carry_in;
- always_ff @ (posedge clock) begin
- case (operation)
- ADD: begin
- y <= add;
- carry_out <= add_carry_7;
- sign <= add[7];
- overflow <= add_carry_7 ^ add_carry_6;
- if (add == 0)
- zero <= 1;
- else
- zero <= 0;
- end
- endcase
- end
- endmodule
With our ADD
operation fully functional that will finish this post. In the next post I will continue adding instructions and tests to this ALU to keep it moving closer to the model I outlined. If you have any feedback or questions please leave a comment. Keep tinkering!