Microcontroller Systems: Combinational logic

last updated: 23/03/19


Song of this chapter: Supertramp > Breakfast in America > The logical song

We have seen in the first chapter, that digital circuits are often calculating (arithmetic). A processor or microcontroller needs one or more Arithmetic logic units (ALU). Now let's see for the logic part.

We can split the logic up in time-independent logic where no memory is needed called the combinational logic and the sequential logic, in which the output depends on the present input but also on the history of the input.

Combinational logic changes instantly, meaning the output of the circuit responds as soon as the input changes.

(Naturally there is a little delay because of the propagation of the signal through the circuit.)

Digital logic (Boolean algebra) (wiki)

Digital logic is based on Boolean (George Boole) algebra. The Boolean algebra is based on the truth values true and false (1 and 0). The main operations of Boolean algebra are the conjunction AND (), the disjunction OR (), and the negation NOT (¬).

Boolean algebra has been fundamental in the developing of digital electronics and thus all modern computer systems. It is a system of rules that allow to make extremely complicated decisions based on simple yes or no questions.

Digital logic is important in programming and provided for in all modern programming languages. Today circuits are often replaced with software in microcontroller.

Basic operations AND, OR, NOT

Truth tables help to quickly understand Boolean operations:

       AND  OR NOT
B A | A ∧ B A ∨ B  Ā
0 0 |   0   0  1
0 1 |   0   1  0
1 0 |   0   1
1 1 |   1   1

It is possible to to define the conjunction in terms of the negation and the disjunction, or to define the disjunction in terms of the negation and the conjunction (De Morgan's laws):

boolean identities

So one basic operation with a NOT suffices to build all digital circuits. We will see that the combination of the conjunction AND with the negation NOT gives one basic gate (NAND) to be used as universal gate.

Boolean laws:

boolean identities

Logic gates (wiki)

We have already build an AND-gate and OR-gate with diodes in electronics fundamentals. Logic gates are devices implementing a Boolean function. They perform a logical operation on one or more binary inputs and produce one binary output. They can be constructed with diodes and transistors, but also with electromagnetic relays (relay logic), mechanical elements, molecules, fluidic logic, pneumatic logic, optics ....

Amplified logic gates can be cascaded and so allow the construction of physical models using all of the Boolean algorithms and mathematics. Thus are created circuits like multiplexers, registers, ALUs, computer memory an even complete microprocessors, which may contain more than 100 million gates.

Our simple diode logic gates from electronic fundamentals had no amplifying element. So we need transistors. Gates using bipolar transistors and resistors (resistor–transistor logic RTL) were used in early integrated circuits. After diodes in replacement for the resistors (diode–transistor logic DTL with higher speed and better density) bipolar transistors replaced the diodes for transistor–transistor logic (TTL). To reduce power consumption today gates are made from MOSFET's (CMOS).

Prefabricated logic gates in integrated circuits as the famous TTL 74xx series from Texas Instruments simplified the life of the engineers.

Today these fixed-function logic gates are often replaced by programmable logic devices as Field-Programmable Gate Arrays (FPGA). With these it is easily possible to change the logic design of a hardware system by reprogramming it.

Basic gates

To draw circuit diagrams, we need as in electronics standardized symbols. We will use DIN EN 60617-12 (IEC 60617-12:1997). Here the most basic gates: AND, OR and NOT, and the important gates NAND, NOR and XOR:

6 gates

"Just do it" CL1:
    B A   XOR
  0 0 |  0
  0 1 |  1
  1 0 |  1
  1 1 |  0

Arithmetic circuits

Half and full adder

Let’s do a little arithmetic using logic :) :

Do you remember the first 4 rules (truth table) of the binary addition? A circuit doing this calculations is called a half adder. It has two inputs and two outputs.

S1 S2   Sum Carry (C)
 0  0 |  0   0
 0  1 |  1   0
 1  0 |  1   0
 1  1 |  0   1

For the sum we need an XOR-gate and for the carry an AND.

halfadder circuit

To get the Boole formula of a truth table we look at the cases where the output is 1. For these cases we make a conjunction (AND) of the inputs named a conjunction clause. We get the formula by a disjunction (OR) of these different clauses. The formula is called Disjunctive Normal Form (DNF) and is an OR of ANDs.

Let’s try it on our half adder:

halfadder NAND

If we want to use NAND's we can transform our formulas. A double negation does not change the formula! The De Morgan’s law gives us our NAND gates:

halfadder NAND

halfadder NAND circuit

Though the found solution is not necessarily the solution with the fewest gates. By using the Boolean laws we can also find the following solution:

halfadder NAND

"Just do it" CL2:

To get circuits with a reduced number of gates we can use our intuition with the Boolean laws. An easier way are Karnaugh maps, a graphical method of simplifying Boolean algebra expressions. For big truth tables and more than 4 input variables the Karnaugh maps attain their limits. A similar method, the Quine–McCluskey algorithm can then be used. It is a tabulation method that makes it more efficient for use in computer algorithms.

To reduce formulas with the above method, you can use online tools like:

http://www.32x8.com/var3kmapn.html or http://tma.main.jp/logic/index_en.html

"Just do it" CL3:
"Just do it" CL4:
"Just do it" CL5:

Logic Levels (wiki)

Logic Level are the voltages in which a signal can exist.

There are different logic families (e.g. DTL, TTL, CMOS) with varying logic levels. As seen, binary 1 is referred to as a HIGH signal and a binary 0 is referred to as a LOW signal. The logic level voltages can be found in the data sheets. We distinguish:


One of the most used logic families is the Transistor-Transistor Logic TTL build with bipolar transistors and working with 5 V. The logic level voltages can be found in the data sheets.

ttl logic levels

The minimum input voltage level for LOW VIL of a device is higher than the output voltage for LOW of a device VOL, because of possible noise adding to the signal. The maximum output voltage level for HIGH VOH is higher than the input voltage for HIGH of a device VIH, because of possible voltage losses on lengthy connection wires.

If the voltage lies in the floating zone, between VIL and VIH, we don't know if the chip decides for a HIGH or a LOW. The state can bounce arbitrarily between HIGH and LOW.


The Arduino Uno uses an ATmega328 chip from ATMEL. Let's look at the ATmega328 data sheet. The typically voltage VCC is 5V:

Arduino logic levels

The levels of Arduino make working with other hardware simpler because the invalid region of voltages is only between 1.5 V and 3.0 V. The noise margin is greater, as is the threshold for a LOW signal.

ESP8266 and ESP32 (3.3V)

The ESP8266 and the ESP32 both work with 3.3V, but their is a big difference! The ESP8266 is 5 V-tolerant means it will not be destroyed by 5 V on an input pin, and even works with 5V as HIGH. This is not true for an ESP32. An ESP32 will be destroyed by using voltages above 3.6V! Here we must use a simple voltage divider or a logic level shifter to get 3.3V from 5V.

"Just do it" CL6:
Open collector (wiki)

Many gates and integrated circuits (IC) have as output no specific voltage or current. The output pin is connected to the collector of an internal NPN transistor (common emitter, working as a switch, but this collector is internally not connected to a pull-up resistance going to VCC (similar for a MOSFET where the output is called open drain).

open collector

The pull-up resistor will be external, and so we can vary the current by the resistance and use another supply voltage. Often open-collector transistors withstand higher voltage than the chip supply voltage, so that motors or relays van be driven.

Another advantage is that multiple open-collector outputs can be connected to a single line.When all outputs attached to the line are in an high-impedance state, the pull-up resistor holds the wire on HIGH. If one or more devices outputs LOW (connection to ground) we get a low. This allows shared buses like I²C.

Active-low and active-high

When working with ICs and microcontrollers, we see sometimes pins that are active-low. The other "normal" pins are active-high, meaning they use the positive logic (activated with HIGH). For an active-low pin, we must pull that pin to LOW (ground) to activate the function (negative logic).

Often both active-low and active-high pins can be found on the same IC. The active-low pins have a NOT notation (line over pin description). For example, many ICs have a chip enable pin CE. If we find a line over CE then that pin is active-low and would need to be pulled to GND to get the chip to become enabled. With no line over CE, the pin is active high, and needs to be pulled to HIGH (VCC) to enable the Chip.


Digital logic in programming

Very often programming is making decisions using Boolean logic: "if true, then do ...". So let's see how Boolean logic is used in programming.

Bitwise Logic

Our gates do their logic "bitwise". As we work with bytes the logic is applied bitwise on each single bit of our byte. The basic operators are:

Other interesting operators are:

Here some examples in Arduino and the corresponding output:

    // bitwise operations

    void setup() {
      byte a = 0b01100110;
      byte b = 0b10111010;
      Serial.println ("01100110 &     01100110 |   ~(01100110)");
      Serial.println ("10111010       10111010       -------- ");
      Serial.print   ("--------       --------       ");
      Serial.println (byte(~a),BIN);
      Serial.print   ("00");
      Serial.print   (a & b,BIN);
      Serial.print   ("       ");  
      Serial.println (a ^ b,BIN);
      Serial.println ();
      Serial.println ("01100110 ^     01100110 >> 3  01100110 << 5");
      Serial.println ("10111010       --------       -------- ");
      Serial.print   ("--------       0000");  
      Serial.print   (a >> 3,BIN);
      Serial.print   ("       ");  
      Serial.println (byte(a << 5),BIN);   
      Serial.println (a ^ b,BIN);

    void loop() {}


    01100110 &     01100110 |   ~(01100110)
    10111010       10111010       -------- 
    --------       --------       10011001
    00100010       11011100

    01100110 ^     01100110 >> 3  01100110) << 5
    10111010       --------       -------- 
    --------       00001100       11000000

We use some (dirty) tricks here to get a decent output. The NOT-operation and the "left-shift"-operation use more than one byte for their result, so we force the result in a byte with the byte() function. The BIN parameter of Serial.print() omits leading zeroes, so we adjust a little bit for this result. The right way would be to write a function to return the corrected string.

"Just do it (if you feel like it)":

Masking (wiki)

Masking is the voluntary setting of a bit or bits to 0 or 1 by using a bit-mask and a Boolean operation like AND, OR or XOR without changing the other bits of e.g a byte.

Masking with AND
A mask with AND sets bits to 0

An AND-operation with 0 sets the bit to 0!

AND mask   new
  0  &  0 =  0
  1  &  0 =  0

An AND-operation with 1 does not affect the state of a bit (new value = old value).

AND mask   new
  0  &  1 =  0
  1  &  1 =  1
Masking with OR
A mask with OR sets bits to 1

An OR-operation with 1 sets the bit to 1!

OR mask   new
  0 |  1 =  1
  1 |  1 =  1

An OR-operation with 0 does not affect the state of a bit (new value = old value).

OR mask   new
  0 |  0 =  0
  1 |  0 =  1
Masking with XOR
A mask with XOR toggles bits

An XOR-operation with 1 toggles the bit!

XOR mask   new
  0  ^  1 =  1
  1  ^  1 =  0

An XOR-operation with XOR does not affect the state of a bit (new value = old value).

XOR mask   new
  0  ^  0 =  0
  1  ^  0 =  1
Example: Masking SF-register in Arduino

As seen the Arduino Uno uses the ATmega328 (DIP) micro-controller. This controller has 23 programmable input/output (I/O, GPIO pins General Purpose Input Output) pins. Often GPIO's are grouped to 8 pins forming one byte named a port. ATmega328 has two full 8-bit ports, portB and portD, and one portC with 6 usable digital pins PC0-PC5 (PC6 is needed for Reset). The pins can also be used as analog input ports, and have the Arduino notation A0-A5.

Arduino Uno circuit

If we use the pins for digital input or output, this is done inside our micro-controller with 3 Special- Function-registers (SF-register) per port, sometimes named I/O-register.

Let's look at a simple setup from an Arduino program and do a little reverse engineering. We will use the Teensy 2.0 board because the code of the libraries is easier to understand and compiling generates a list file . Teensy 2.0 uses an ATmega32u4, the same chip as for the Arduino Leonardo. We get with this chip the whole portB and portD and 6 pins from portF.

Teensy 2.0 circuit

    void setup() {
      pinMode(2,INPUT);         // Teensy 2.0 digital pin 2 is PB2
      pinMode(8,OUTPUT);        // Teensy 2.0 digital pin 8 is PD3
      pinMode(A1,INPUT_PULLUP); // Teensy 2.0 digital pin 20 or analog pin A1 is PF1

    void loop() {}

After compiling, we find in a temporary folder (/tmp on linux) the hex file (.hex) and the list file (.lst) of our program. The hex file doesn't help much, because it's our machine program in hexadecimal. Here is the line with the setup commands, the interesting code is between the 2 vertical bars :) :


The stripped list file gives more information:

000001b2 <setup>:
                CORE_PIN2_DDRREG  &= ~CORE_PIN2_BITMASK;
1b2:    22 98           cbi 0x04, 2
                CORE_PIN2_PORTREG &= ~CORE_PIN2_BITMASK;
1b4:    2a 98           cbi 0x05, 2 ; 5
                CORE_PIN8_DDRREG |= CORE_PIN8_BITMASK;
1b6:    53 9a           sbi 0x0a, 3 ; 10
                CORE_PIN20_DDRREG  &= ~CORE_PIN20_BITMASK;
1b8:    81 98           cbi 0x10, 1 ; 16
                CORE_PIN20_PORTREG |=  CORE_PIN20_BITMASK;
1ba:    89 9a           sbi 0x11, 1 ; 17
1c6:    08 95           ret    

In the first column we see the address of the flash. The setup begins at 0x1B2. After the address comes the 2 byte machine code for the flash. And then the code in assembler language. There are only 2 assembler commands here, set bit (sbi) and clear bit (cbi). Both can handle single bits of our SF-register. So in assembly there is no masking needed for this register (but we need masks for other SF-register). After the commands we get the address of the SF-register. Here is a little table with the addresses for ATmega32u4:

address register address register address register
0x03 PINB 0x09 PIND 0x0F PINF
0x04 DDRB 0x0A DDRD 0x10 DDRF
0x05 PORTB 0x0B PORTD 0x11 PORTF

So we get:

pinMode(2,INPUT);            cbi DDRB,PB2  ; set Arduino digital Pin 2 as input `0`
                             cbi PORTB,PB2 ; no internal pull-up resistor! PORTB = `0`
pinMode(8,OUTPUT);           sbi DDRD,PD3  ; set Arduino digital Pin 8 as output `1`
pinMode(A1,INPUT_PULLUP);    cbi DDRF,PF1  ; set Arduino digital Pin 20 (A1) as input `0`
                             sbi PORTF,PF1 ; connect internal pull-up resistor! PORTB = `1`

The interesting part is the C-code in the list file used by the internal Arduino library:


stands for cbi DDRB,PB2 to clear bit 2 in DDRB register. So the CORE_PIN2_DDRREG stands for DDRB, and the CORE_PIN2_BITMASK must be 0b00000100 or 0x04 because the byte is inverted. So we get:

    DDRB  = DDRB & ~(0b00000100);
    (or DDRB = DDRB & 0b11111011; or DDRB &= 0b11111011; or DDRB &= 0xFB;)

So we can rewrite our Arduino program this way using SF-register and masking:

    void setup() {
      DDRB =  DDRB & 0b11111011;   // Set PB2 in DDRB to 0 with and AND mask (PB2 = input)
      PORTB = PORTB & 0b11111011;  // Set PB2 in PORTB to 0 with and AND mask (PB2 has no pull-up)
      DDRD =  DDRD | 0b00001000;   // Set PD2 in DDRD to 1 with and OR mask (PD3 = output)
      DDRF =  DDRF & 0b11111101;   // Set PF1 in DDRF to 0 with and AND mask (PF1 = input)
      PORTD = PORTD | 0b00000010;  // Set PF1 in PORTF to 1 with and OR mask (PF1 has pull-up)

    void loop() {}

Naturally we would not do it this way, because the code is less readable and not portable (it will not run on another Arduino not using the ATmega32u4 chip). But masking is often necessary in our programs, and working with ports can also simplify the programs as seen in our SSD example from the previous chapter.

Bit shifting

A very useful bitwise operation is the arithmetic bit shift to right or left (Arduino reference). With the shift operators >> or << we simply slide the data right or left by a certain number of places. The bits that are shifted out disappear. A zero is shifted in from the other end. This operation equates a logical shift in assembler (logical shift right lsr resp. logical shift right lsl).

    byte a = 0b10010110;
    a = a>>3;  // right shift three positions: a = 0b00010010
    a = a<<5;  // left shift five positions:   a = 0b01000000

The shift of one position to the right equates a division by 2 in binary! A right shift of two positions is a division by 4 etc..

    byte a = 0b00010110;  // = 22
    byte b = 0b00010110;  // = 22
    a = a>>1;  // right-shift one positions:   a = 0b00001011 = 11 = 22/2
    a = a>>1;  // right-shift two positions:   a = 0b00000101 = 5  = 22/2/2
    a = a>>1;  // right-shift three positions: a = 0b00000010 = 2  = 22/2/2/2
    a = b>>3;  // right-shift three positions: a = 0b00000010 = 2  = 22/8

On the other hand a shift of one position to the left equates a multiplication by 2 in binary! A left shift of two positions is a multiplication by 4 etc..

    byte a = 0b00010110;  // = 22(10)
    byte b = 0b00010110;  // = 22(10)
    a = a<<1;  // left shift one positions:  a = 0b00101100 = 44 = 22*2
    a = a<<1;  // left shift two positions:  a = 0b01011000 = 88 = 22*2*2
    a = b<<4;  // left-shift four positions: a = 0b01100000 = 96 error

In the last example we get an error because 22·16 = 352 and this can't fit in a byte. But shifting also works with integer or long!

Assignment operators

We often need to change a value on it's self (a = a<<1 or a = a + 1) in programming. in C++ language and thus in Arduino we got a a shorthand notation. The operator is placed before the equal sign. This is true for most operators. Here some examples:

    int a = 2;  // 0b0000000000000010 = 2
    a += 4;     // 0b0000000000000110 = 6 = 4+2
    a <<= 4;    // 0b0000000001100000 = 6*16 = 96 = 64+32
    a |= 3;     // 0b0000000001100011 = 99 (mask 3 = 0b11)
    a &= ~64;   // 0b0000000000100011 = 35 (mask ~64 = 0b1111111110111111)
    a ^= 7;     // 0b0000000000100100 = 36 (mask 7 = 0b111)
    a ^= 7;     // 0b0000000000100011 = 35 (mask 7 = 0b111)
"Just do it" CL7:
    // NAND with bitwise logic

    byte in1, in2, in, out_NAND;

    void setup() {
      DDRB = DDRB & 0b11111100;   // PB0, PB1 input
      PORTB = PORTB | 0b00000011; // PB0, PB1 pullup
      DDRB = DDRB | 0b00001100;   // PB2 output  

    void loop() {
      in = ~(PINB);  // invert to get positive logic (pull up)
      in1 = in & 0b00000001; //
      in2 = in & 0b00000010;
      in2 = in2 >> 1; //shift one bit to the right
      out_NAND = ~(in1 & in2);  
      out_NAND = out_NAND << 2; //shift two bit to the left
      out_NAND = out_NAND << 7; //shift two bit to the left
      out_NAND = out_NAND & 0b00000100;
      if (out_NAND == 0) {
        PORTB = PORTB & 0b11111011;  // clear output        
      else {  
        PORTB = PORTB | 0b00000100;   // set output        
"Just do it" CL8:
"Just do it" CL9:

Boolean (logical) and comparison operators

Boolean operators

It is important to not confound these Boolean operators with the bitwise operators!

Example with Boolean operators:

    // NAND and NOR gate

    const byte in1_pin = 0;
    const byte in2_pin = 1;
    const byte out1_pin = 2;
    const byte out2_pin = 3;

    bool in1, in2, out1_NAND, out2_NOR;

    void setup() {

    void loop() {
      in1 = !digitalRead(in1_pin); // invert to positive logic (Pull-Up!)
      in2 = !digitalRead(in2_pin);
      out1_NAND = !(in1 && in2);
      out2_NOR  = !(in1 || in2); // alt. de Morgan !in1 && !in2
Comparison operators

Often we have to compare values and need a Boolean TRUE or FALSE.

These operators are often used for flow control in if/else or switch/case statements but also in while()/do..while() or for() loops.

    if ((raining != true) && (windSpeed <= 3) && (holiday == true)) {
    if ( holiday != true ) {
    else if ((raining != true) && (windSpeed <= 3)) {
    else {
    while (windy != true) {
    for (byte i = 0; i < 500; i++) {
      Serial.print("do your homework!");

Use parentheses around the subclauses to keep your code as readable as possible by grouping subclauses together.

Lookup tables (LUT)

Every truth table can be programmed with a lookup table. The inputs are interpreted as number (case) and are used as index to access the table (e.g. truth table with 3 inputs gives the numbers from 0-7).

Here the same example as above using lookup tables.

    // NAND and NOR gate with Look Up Table (LUT)

    const byte in1_pin = 0;
    const byte in2_pin = 1;
    const byte out1_pin = 2;
    const byte out2_pin = 3;

    // Look Up Tables (LUT's)
    bool NAND[4] = {1, 1, 1, 0}; 
    bool NOR[4]  = {1, 0, 0, 0};

    byte in1, in2, in;
    bool out1_NAND, out2_NOR;

    void setup() {

    void loop() {
    in1 = !digitalRead(in1_pin); // invert to positive logic (Pull-Up!)
    in2 = !digitalRead(in2_pin);
    in = in2*2+in1; // calculate index
    out1_NAND = NAND[in];
    out2_NOR = NOR[in];
"Just do it" CL10:

Interesting links: