Introduction to Binary Logic Circuits - by Nick Fletcher (2006)


home | page 1 | page 2 | page 3 | page 4 | page 5 | page 6

We can cheat a little bit if we want. We could just use the '^' operator instead of the code we wrote for the XOR GATE. But let's not. Let's wrap up the code and place it in the 'guts' of a new class called XOR. Once we have this class, we will continue on and arrange the XOR and the other GATES into an adding machine and test it. Here is where the generalised command line argument handling will save us a little bit of work. It can accept any number of inputs and present their values to us for reference.
I have just decided that instead of 'hard coding' our GATEs into some construct within main(), I will build a new class called "ADDER" that contains the gates as members. ADDER can accept an array as an argument and work with any number of inputs we like. In actual fact, we present adder with both binary numbers from the command line. ADDER can split the array in half to find the two operands.
To keep ADDER's code a little bit cleaner, it will have a more powerful constructor that initialises the 'machinery'. We want to hide all the mess of constructing the machine so we can test our GATES clearly. Here is the code to implement the XOR GATE as a seperate class:


#include<iostream>
#include<string>
#include
#include
using namespace std;

typedef unsigned short int USI;

//class declarations
class OR{
        public:
        OR(){};
        OR(string nme);                           //OR's default constructor
        ~OR(){};                        //Nothing to release - empty destructor

        USI Do(USI in1, USI in2);

        private:
        string m_name;
};

class AND{
        public:
        AND(){};
        AND(string nme);                          //AND's default constructor
        ~AND(){};                        //Nothing to release - empty destructor

        USI Do(USI in1, USI in2);

        private:
        string m_name;
};

class NOT{
        public:
        NOT(){};
        NOT(string name);
        ~NOT(){};

        USI Do(USI in1){
            //cout << endl << this->m_name << " performing NOT on " << in1 << " = ";
            return !(in1);
        }
        private:
        string m_name;
};
class XOR{
        public:
        XOR(){}
        XOR(string nme);
        ~XOR(){};

        USI OP(USI in1, USI in2);

        private:

        AND        m_and;
        OR         m_or;
        NOT        m_not;
        string     m_name;
};
//main
int main(int argc, char* argv[]){
    int i;
    cout << "\n\nThe inputs given are: ";
    for(i = 0; i < argc -1; i++){
            cout << atoi(argv[i+1]) << " ";
    }
    XOR x1("Nick");
    USI res = 0;
    res = x1.OP((USI)atoi(argv[1]),(USI)atoi(argv[2])) ;
    cout << "\n\nthe results of XORing them are " << res << "\n\n\n\n";
    return 0;
}
//class definitions
OR::OR(string nme){
                m_name = nme;
}
USI OR::Do(USI in1, USI in2){
        //cout << endl << this->m_name << " performing " << in1 << " OR " << in2 << " = ";
        return in1|in2;
}
USI AND::Do(USI in1, USI in2){
        //cout << endl << this->m_name << " performing " << in1 << " AND " << in2 << " = ";
        return in1 & in2;
}
AND::AND(string nme){
                m_name = nme;
}
NOT::NOT(string nme){
                m_name = nme;
}
XOR::XOR(string nme){
                m_name = nme;
}
USI XOR::OP(USI in1, USI in2){
    USI output = 0;

    output = this->m_or.Do(this->m_and.Do(in1, this->m_not.Do(in2) ),//first input
                            this->m_and.Do(in2, this->m_not.Do(in1)));//second input!
    return output;
}
I have altered the other classes slightly, namely the method names. The XOR class 'knows' how to use AND, OR and NOT GATEs because they are used in it's construction! It's now easy to perform an XOR operation on any inputs. In main(), to test the new class, I just wrote some simple driver code. The output from some tests was as folows:
PROGRAM OUTPUT:

C:\???>adder1 1 0 <ENTER>

The inputs given are: 1 0

the results of XORing them are 1


C:\???>adder1 1 1 <ENTER>

The inputs given are: 1 1

the results of XORing them are 0


C:\???>adder1 0 0 <ENTER>

The inputs given are: 0 0

the results of XORing them are 0


Hey presto! All that's left now is to decide on how to implement the adder machine in code. My first thought is to build an adder class. The adder objects will accept two inputs and have possible outputs from their XOR section and their AND section. A few points need to be understood before we can start coding. It's all very well and good to have a machine that can add two binary numbers together, and even carry any overflow to the next column! But, the carrying itself presents a new menace to out machine. Consider what will actually happen during a normal addition of two binary numbers.
10100010
          +
11100011
          =
10000101 with the MSB discarded.

Let's go through that calculation bit by bit starting, as with any type of number, from the right digits. 1 + 0 in binary is 1 , as it is in decimal also. That's ok, move on to the next one. 1 + 1 in binary is 10b (a 'b' appended to a number signals binary notation) or 2 in decimal. We can't fit 10b into the first column, so we place a 0 there and we have a carry. 0 + 0 + carry = 1. Next column, 0 + 0 = 0. Same for the next column, 0 + 0 = 0. Now another 1 + 1 = 0 with a carry. This makes the sum 1 + 0 + carry = 0 with a carry. The last sum is 1 + 1 + carry = 1 with a carry!
I'm starting to dread coding this one! We will need to employ some clever GATE set ups. Now, we should take a step back and consider what we are trying to achieve and what tools we have at our disposal.
We are trying to build an adding machine that can take two 8 bit binary numbers and give the sum of them both. Earlier, I said that the adding machine simply gives any carries to the next 1bit adder to it's left. This is ok, but we have to be very careful from here on in! The first bits (bits 0 or LSBs) in our sum are the only ones that can be treated in this simple fashion. All the following bits need more complex circuitry to achieve the correct outputs. Why???
Consider what will happen when we need to add 1 and 1 and a carry. The correct answer is 1 with a carry passed on. The way to achieve the correct functioning is to handle the input of the carry with more gates!

Each ADDER object in the line of eight has two possible outputs from two operands that are given to it. The XOR GATE of the ADDER object reflects an addition of the two operands and the AND GATE reflects a carry to be sent to the next most significant bit in the bank of 8. An important fact to realise here is that there can only ever be one carry passed from any addition of single bit values. We will never have the problem of carry bits building up as we move from right to left. It's actually the same for decimal. The most that is ever passed to the left in an addition operation is a 1 numeral of the next power! 999 + 999 still only makes one unit of carry.
Knowing this, we can start to think about what must happen, gate wise, when carries are encountered. We know that we must add any carries to the current sum. So, to do a binary addition, we use an XOR GATE. We don't want two carries, so we send the first XOR output that does the initial addition to an OR along with the output from the AND that tests for a carry. This covers us, and the output from that OR becomes the new carry! Also, we need to XOR the carry in with the output of the initial addition XOR. I'll now run through what needs to be done, then we can code it.

etc etc…

Can you see how it will work? Try drawing a picture of it, as I can't provide drawings here… Basically, if there is a carry in and results of the main addition produce a carry, we only send one carry out to the next bit. We only XOR the CarryIn with the result of the main addition XOR to arrive at zero if both are 1 or 1 if either is one and the other is 0. Phew. Let's design some code for this machine...!

You can design yours any way you wish of course, but I have chosen to build mine as a hard coded 8bit adder. This is a simpler way of doing it. Working with logic gates is, really, a waste of time. It's not a waste of time learning about them and seeing how they work, but we now know these things, and we can move on to the next stage of my tutorial that will focus on computer hardware and how it works. My point is, you would never get anything done if you had to worry about the gates inside the CPU! But it's nice to know that they are there. Anyway, we should think about the design for the 8bit adder.

I will use a class of adder that works with one bit at a time. Then, I can arrange them to send relative inputs/outputs to the next bit in line. The class should have a method that accepts three inputs(A, B and Carry). There should also be a method for providing output so we can send any carryOuts to the next column/bit. Finally, each one bit adder should be able to display it's own output value.

The inputs will be provided from the command line and using a helper function, converted into USI (Usigned Short Int. A typedef)arrays. Just to re-cap, I'll make a class that does the 8 bit adding using eight 1bit adders.


#include<iostream>
#include<string>
using namespace std;

typedef unsigned short int USI;

void Addend_Init(int args, char** theAddend, USI arr[]);

//class declarations
class OR{
      public:
      OR(){};
      OR(string nme); // OR's default constructor
      ~OR(){}; // Nothing to release - empty destructor

      USI Do(USI in1, USI in2);

      private:
      string m_name;
};

class AND{
      public:
      AND(){};
      AND(string nme); //AND's default constructor
      ~AND(){}; //Nothing to release - empty destructor

      USI Do(USI in1, USI in2);

      private:
      string m_name;
};

class NOT{
      public:
      NOT(){};
      NOT(string name);
      ~NOT(){};

      USI Do(USI in1){
          //cout << endl << this->m_name << " performing NOT on " << in1 << " = ";
          return !(in1);
      }
      private:
      string m_name;
};
class XOR{
      public:
      XOR(){}
      XOR(string nme);
      ~XOR(){};

      USI OP(USI in1, USI in2);

      private:

      AND        m_and;
      OR         m_or;
      NOT        m_not;
      string     m_name;
};
class ADDER{
      public:
      ADDER(){}
      ADDER(string nme);

      void         DoAdder(USI in1, USI in2);
      USI          GetAnd();
      USI          GetXor();

      private:
      USI     m_and_output;
      USI     m_xor_output;
      AND     m_and;
      XOR     m_xor;
      string  m_name;
};
class ADDER8{
      public:

      void DoBitAdd(USI A, USI B, USI CarryIn = 0); //note the default value to cover the LSB.
      void SetOutput(USI a){ m_output = a; }
      void SetCarryOut(USI a){ m_carry_out = a; }

      USI GetOutput(){ return m_output; }
      USI GetCarryOut(){ return m_carry_out; };

      private:
      USI     m_output;
      USI     m_carry_out;
      ADDER   m_adder;
      OR      m_or;
      XOR     m_xor;
      AND     m_and;
};
//main
int main(int argc, char* argv[]){
    int i;
    cout << "\n\nThe inputs given are: ";
    for(i = 0; i < argc -1; i++){
          cout << atoi(argv[i+1]) << " ";
    }

    int args = argc/2;
    USI addend1[8];
    USI addend2[8];
    Addend_Init(args, argv, addend1);
    Addend_Init(args, argv+8, addend2);

    cout << "\n\nThe two addends are:\n\nAddend1: ";
    for(i = 0; i < args; i++){
          cout << addend1[i];
    }
    cout << "\nAddend2: ";
    for(i = 0; i < args; i++){
          cout << addend2[i];
    }
    //make eight ADDER8 objects and put 'em through their paces!
    ADDER8 add0, add1, add2, add3, add4, add5, add6, add7;

    //it's now a simple case of presenting each ADDER8 object with
    //the two input addends, and the carryin from the last adder8 object
    //which is actually that object's m_carry out.

    add0.DoBitAdd(addend1[7], addend2[7], 0);
    add1.DoBitAdd(addend1[6], addend2[6], add0.GetCarryOut());
    add2.DoBitAdd(addend1[5], addend2[5], add1.GetCarryOut());
    add3.DoBitAdd(addend1[4], addend2[4], add2.GetCarryOut());
    add4.DoBitAdd(addend1[3], addend2[3], add3.GetCarryOut());
    add5.DoBitAdd(addend1[2], addend2[2], add4.GetCarryOut());
    add6.DoBitAdd(addend1[1], addend2[1], add5.GetCarryOut());
    add7.DoBitAdd(addend1[0], addend2[0], add6.GetCarryOut());

    cout << "\n\nOutput : ";
    cout << add7.GetOutput();
    cout << add6.GetOutput();
    cout << add5.GetOutput();
    cout << add4.GetOutput();
    cout << add3.GetOutput();
    cout << add2.GetOutput();
    cout << add1.GetOutput();
    cout << add0.GetOutput() << "\n\n\n\n\n";


    return 0;
}
void Addend_Init(int args, char* theAddend[], USI addend[]){
                int i;

                for(i = 0; i < args; i++){
                      addend[i] = (USI)atoi(theAddend[i+1]);
                }
}
//class definitions
OR::OR(string nme){
              m_name = nme;
}
USI OR::Do(USI in1, USI in2){
      //cout << endl << this->m_name << " performing " << in1 << " OR " << in2 << " = ";
      return in1|in2;
}
USI AND::Do(USI in1, USI in2){
      //cout << endl << this->m_name << " performing " << in1 << " AND " << in2 << " = ";
      return in1 & in2;
}
AND::AND(string nme){
                m_name = nme;
}
NOT::NOT(string nme){
                m_name = nme;
}
XOR::XOR(string nme){
                m_name = nme;
}
USI XOR::OP(USI in1, USI in2){
    USI output = 0;

    output = this->m_or.Do(this->m_and.Do(in1, this->m_not.Do(in2) ),//first input
                           this->m_and.Do(in2, this->m_not.Do(in1)));//second input!
    return output;
}
ADDER::ADDER(string nme){
                    m_name = nme;
}
void ADDER::DoAdder(USI in1, USI in2){
            this->m_and_output = this->m_and.Do(in1, in2);
            this->m_xor_output = this->m_xor.OP(in1, in2);
}
USI ADDER::GetAnd(){
    return m_and_output;
}
USI ADDER::GetXor(){
    return m_xor_output;
}
void ADDER8::DoBitAdd(USI A, USI B, USI CarryIn){
//after this, the next bit can get it's inputs from this object and from the addends array.
//we have to take care of this bit's output(including the carry in) and the carry out.
    USI tmp1 = 0, tmp2 = 0;

    this->m_adder.DoAdder(A,B); //set the addends results. m_adder holds this data
    this->SetOutput(this->m_xor.OP(this->m_adder.GetXor(),CarryIn)); //catch the overflow from adding the carryin to the output

    //The carry out is thus...

    tmp2 = this->m_or.Do(this->m_adder.GetAnd(), //the carry_out produced by this 'bit'
    this->m_and.Do(this->m_adder.GetXor(),CarryIn)); //the carry in, ANDed with the output and the CarryIn!
    this->SetCarryOut(tmp2);
    cout << "\n\n-- Carry out:" << this->GetCarryOut();
    cout << "\n-- Carry in :" << CarryIn;
    cout << "\n-- Output   :" << this->GetOutput();


}
PROGRAM OUTPUT:

The inputs given are: 1 1 0 0 1 0 1 0 1 1 0 1 1 1 0 1

The two addends are:

Addend1: 11001010
Addend2: 11011101

-- Carry out:0
-- Carry in :0
-- Output   :1

-- Carry out:0
-- Carry in :0
-- Output   :1

-- Carry out:0
-- Carry in :0
-- Output   :1

-- Carry out:1
-- Carry in :0
-- Output   :0

-- Carry out:1
-- Carry in :1
-- Output   :0

-- Carry out:0
-- Carry in :1
-- Output   :1

-- Carry out:1
-- Carry in :0
-- Output   :0

-- Carry out:1
-- Carry in :1
-- Output   :1

Output : 10100111

I have output the comings and goings of all the bits from each column to higlight what's going on.
This code is very simple. The ADDER8 class builds on what we have built previously. The ADDER8 object has all of the gates we have built as members, plus the ADDER from the last example. The ADDER member is used as a kind of halfway step to providing the full output. Before we can output the XORed result of the two inputs, we need to XOR that result with any Carry In that may be there. This means that this particular column will hold no output if there is a carry in and also a 1 result from adding both addends. That takes care of that, but that's not the only handling of the initial XOR of the addends. They are also sent to an AND GATE along with any Carry In that came from the previous column. That ouput is then merged, through an OR GATE, with the value of this column's carry.

The inputs I choose were 202 and 221 in decimal. The addition of these two decimal numbers should produce 423, right? My stupid machine got it wrong!! It came up with 167… Oh dear! Back to the drawing board? Hang on a minute. I reckon my machine works fine. In fact, I reckon, if we examine the ADDER8 object we'll find where things went wrong. Some of you may have already guessed what went wrong.
The machine is set up to accept 8 bits worth of input. It's hard coded that way. Have a look at what the last 'bit' was doing. Hmm… It has a carry out! Where does that carry out go? It goes into computer hell. It's burnt up in the cyber wasteland of the never ending for(;;) loop... No, not really. It is discarded. Our machine has no where to hold it, so it is ignored. That's exactly what could happen in a real CPU, if you're not careful. This example shows conclusively that we have built an impatial machine that can add numbers based soley on switches! By the way, the highest number that you can represent with eight bits is 255. That is why we had our little problem!

To wrap up this first installment, I'd just like to make a few points about this exercise. Firstly, you may think this is just a program, and it is rather useless. Well, as a program it is! But let me assure you, if you got out some light switches and wired them up the same way we have in our code here, you would achieve the same results. You would, of course, need to flick each switch manually to input your individual bits! If you wanted to get a little bit more into this side of things, you could go and buy some real logic gates from your local electronics store. They come on a chip called an IC (intergrated circuit) and allow you to play around with what we have been discussing here. You can get these ICs with AND OR XOR and other flavours of switch, with between 4 and 12 switches on each IC. Anyway, that's for another discussion!

My second point is that what you have seen here is not the way a real, modern computer CPU would do things. That also, is another topic. What we made would work as far as allowing a simple computer to compute the sum of two numbers, but it would be slow if you wanted to process millions on numbers one after the other. And all our machine can do is ADD. A real CPU can subtract, divide and multiply. Don't worry though. What you have learnt is certainly valid! The same switches are used. They just use more of them and wire them up much more cleverly.

So I hope this was an enjoyable and informative read. I will now start work on the next section, and it will be ready ASAP. The next section will be a look at computer hardware and how to emulate our own computer with software. Yes... It'll be quite involving, but fun!


home | page 1 | page 2 | page 3 | page 4 | page 5 | page 6
nickeax AT gmail.com