ELE-210 · Lesson 2 of 12 · 23 min

Binary arithmetic and codes

Lesson 1 showed how bit patterns represent unsigned numbers. Real systems also need negative numbers, decimal digits for displays, characters for text, and special codes for sensors and error detection. This lesson covers binary addition and subtraction, the signed number formats used in hardware, overflow, and the standard codes a digital designer meets every week: binary-coded decimal, Gray code, ASCII and Unicode, and parity. Each is a choice of how to map meaning onto bits, and each was invented to make some circuit simpler or more reliable.

Binary addition

Binary addition works exactly like decimal addition, column by column from the right, but with only two digits. The rules are short: 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1, and 1 + 1 = 10, which is a sum bit of 0 with a carry of 1 into the next column. With a carry coming in, 1 + 1 + 1 = 11, a sum bit of 1 and a carry of 1. Lesson 7 builds circuits that do exactly this, one column at a time.

\[\begin{array}{r} 0110\,1011 \\ +\ 0011\,1001 \\ \hline 1010\,0100 \end{array} \qquad 107 + 57 = 164\]

In a fixed-width register, a carry out of the most significant column has nowhere to go. For unsigned numbers this carry is the overflow signal: if it is 1, the true sum did not fit. Adding 200 and 100 in 8 bits gives 300 − 256 = 44 with a carry out of 1. Processors keep this carry in a status flag so that software can detect the overflow or chain several additions together to handle wider numbers.

Representing negative numbers

There are three classic ways to represent signed integers in n bits. All three use the most significant bit as an indication of sign, 0 for positive and 1 for negative, and all three represent positive numbers in the same way as unsigned binary. They differ in how they represent negative numbers.

In sign-magnitude form, the MSB is the sign and the remaining bits are the size of the number, just as people write −5. It is easy to read, but it has two zeros (+0 and −0), and addition circuits must compare signs and magnitudes before they know whether to add or subtract. It survives in floating-point numbers, where the sign is stored separately.

In ones' complement form, a negative number is formed by inverting every bit of the positive number. It also has two zeros, and addition needs an end-around carry. It is now mostly of historical interest, although the internet checksum still uses ones' complement arithmetic.

In two's complement form, a negative number is formed by inverting every bit and adding 1. Equivalently, the MSB of an n-bit number has a weight of −2ⁿ⁻¹ instead of +2ⁿ⁻¹, and all other bits keep their usual positive weights. Two's complement has a single zero, and, most importantly, ordinary binary addition gives the right answer for any mix of positive and negative numbers. That is why virtually every processor and digital signal processor uses it.

\[N = -b_{n-1}2^{n-1} + \sum_{i=0}^{n-2} b_i 2^i\]
Selected 4-bit patterns interpreted four ways. The same bits mean different numbers depending on the agreed format.
BitsUnsignedSign-magnitudeOnes' complementTwo's complement
00000+0+00
00113+3+33
01117+7+77
10008−0−7−8
101111−3−4−5
110113−5−2−3
111115−7−0−1

An n-bit two's complement number ranges from −2ⁿ⁻¹ to 2ⁿ⁻¹ − 1. For 8 bits that is −128 to +127. The range is lopsided by one because zero takes one of the non-negative patterns. Negating the most negative number gives itself: the two's complement of 1000 0000 is 1000 0000 again, a corner case that has caused real software bugs.

A circle of sixteen 4-bit patterns from 0000 to 1111 arranged like a clock. Inside each pattern its unsigned value is shown, and outside its two's complement value. Moving clockwise adds one. The boundary between 0111 and 1000 is marked as the signed overflow point, and the boundary between 1111 and 0000 as the unsigned carry point.
The 4-bit number wheel. Adding moves clockwise; unsigned overflow happens crossing from 1111 to 0000, signed overflow crossing from 0111 to 1000.

Worked problem

Two's complement in the tachometer

The tachometer measures the change in speed between two readings, which can be negative. Represent −45 in 8-bit two's complement, then add it to +100 and check the result.

Show solution
  1. +45 in 8 bits is 0010 1101.
  2. Invert every bit: 1101 0010. Add 1: 1101 0011. So −45 is 1101 0011 (0xD3).
  3. Check with the weights: -128 + 64 + 16 + 2 + 1 = −45.
  4. Add +100 (0110 0100) and −45 (1101 0011): the sum is 1 0011 0111. Discard the carry out of bit 7.
  5. \[0011\,0111_2 = 55_{10} = 100 - 45\]

Answer: −45 is 1101 0011, and ordinary binary addition gives 55 once the carry out is ignored.

The carry out of the MSB was simply thrown away in that example. In two's complement arithmetic, the carry out does not indicate overflow. A signed overflow happens instead when two numbers with the same sign are added and the result has the opposite sign: two positives giving a negative, or two negatives giving a positive. Adding two numbers with different signs can never overflow. In hardware, signed overflow is detected by comparing the carry into the MSB with the carry out of it: if they differ, the result has overflowed.

\[V = c_{n} \oplus c_{n-1}\]

Subtraction and sign extension

Because negation is so easy in two's complement, subtraction is done by adding the negation: A − B = A + (B̄ + 1). An adder circuit can subtract by inverting B and setting the carry into the least significant column to 1. Lesson 7 builds exactly this adder-subtractor, which is the heart of every arithmetic logic unit.

When a signed number moves into a wider register, it must be sign-extended: copy the MSB into all the new upper bits. The 4-bit value 1011 (−5) becomes 1111 1011 in 8 bits, still −5. Filling with zeros instead, as you would for an unsigned number, would turn it into 0000 1011, which is +11. Mixing up signed and unsigned extension is one of the most common errors in hardware and software alike.

The same 8-bit addition read two ways. C flags unsigned overflow; V flags signed overflow.
# Interpret and add 8-bit patterns as unsigned and as two's complement
BITS = 8
def as_signed(x, n=BITS):
    return x - (1 << n) if x & (1 << (n - 1)) else x

def add(a, b, n=BITS):
    mask = (1 << n) - 1
    raw = a + b
    s = raw & mask
    carry = raw >> n                          # unsigned overflow
    sa, sb, ss = as_signed(a), as_signed(b), as_signed(s)
    v = int((sa >= 0) == (sb >= 0) and (ss >= 0) != (sa >= 0))   # signed overflow
    return s, carry, v

for a, b in [(100, 0xD3), (200, 100), (0x70, 0x30), (0x90, 0xA0)]:
    s, c, v = add(a, b)
    print(f"{a:08b} + {b:08b} = {s:08b}  unsigned {a}+{b}={s} C={c}  "
          f"signed {as_signed(a)}+{as_signed(b)}={as_signed(s)} V={v}")
Output
01100100 + 11010011 = 00110111  unsigned 100+211=55 C=1  signed 100+-45=55 V=0
11001000 + 01100100 = 00101100  unsigned 200+100=44 C=1  signed -56+100=44 V=0
01110000 + 00110000 = 10100000  unsigned 112+48=160 C=0  signed 112+48=-96 V=1
10010000 + 10100000 = 00110000  unsigned 144+160=48 C=1  signed -112+-96=48 V=1

The four sums show every case. The first, 100 + (−45), is correct as a signed sum even though it carries out. The second, 200 + 100, overflows as unsigned but not as signed (−56 + 100 = 44 is fine). The third, 112 + 48, is fine unsigned but overflows signed, because two positives gave a negative. The fourth overflows both ways. The hardware performs the same addition every time; only the interpretation, and therefore which flag matters, changes.

Binary-coded decimal

Many products show numbers to people in decimal: meters, clocks, scales, fuel pumps and Cedarline's tachometer display. Converting a binary number to decimal digits takes repeated division, which is expensive in hardware. An alternative is to keep each decimal digit separately in four bits, a code called binary-coded decimal (BCD) or 8421 code after the bit weights. The number 2,795 in BCD is 0010 0111 1001 0101, each nibble one digit. The patterns 1010 to 1111 are unused and illegal.

BCD wastes some bits: four BCD digits use 16 bits to hold 0 to 9,999, while 14 bits of binary would do. In return, each digit can drive a display directly through a decoder (lesson 6), and decimal arithmetic is exact, which is why financial systems and pocket calculators have long used it. Adding two BCD digits uses a binary adder followed by a correction: if the 4-bit sum exceeds 9 or produces a carry, add 6 (0110) to skip the six unused patterns and generate a decimal carry.

Worked problem

BCD addition with correction

Add the BCD digits 7 and 8.

Show solution
  1. \[0111 + 1000 = 1111\]
  2. 1111 is 15, greater than 9, so it is not a valid BCD digit. Add 6: 1111 + 0110 = 1 0101.
  3. The carry gives the tens digit 1 and the low nibble 0101 gives the units digit 5.

Answer: 7 + 8 = 15 is 0001 0101 in BCD.

Gray code

In ordinary binary counting, several bits can change at once: going from 0111 to 1000 changes all four. If a sensor reads a position as a binary number and the bits do not all change at precisely the same instant, it can briefly report a wildly wrong value, such as 1111 or 0000, during the transition. Gray code fixes this by arranging the patterns so that consecutive values differ in exactly one bit. It is used in rotary and linear position encoders, in counters that cross between clock domains (lesson 11) and in the labeling of Karnaugh maps (lesson 5).

The 3-bit reflected Gray code. Each row differs from the next, and the last from the first, in exactly one bit.
DecimalBinaryGray
0000000
1001001
2010011
3011010
4100110
5101111
6110101
7111100

To convert binary to Gray, keep the MSB and make each other Gray bit the exclusive-OR of a binary bit and the bit to its left: gᵢ = bᵢ ⊕ bᵢ₊₁. To convert back, the MSB stays and each binary bit is the exclusive-OR of the Gray bit and the binary bit already found to its left. In a single expression, g = b ⊕ (b ≫ 1).

Characters: ASCII and Unicode

Text is stored by assigning a number to each character. ASCII, the American Standard Code for Information Interchange, uses 7 bits for 128 characters: control codes such as carriage return and line feed (0 to 31), the space, punctuation, digits (0x30 to 0x39), upper-case letters (0x41 to 0x5A) and lower-case letters (0x61 to 0x7A). The layout has handy properties: the digit '7' is 0x37, so subtracting 0x30 gives its value, and upper and lower case differ only in bit 5.

ASCII covers English only. Unicode assigns a code point to every character in every writing system, including accented French letters, Indigenous syllabics, Chinese characters and emoji, more than 150,000 characters in all. The UTF-8 encoding stores each code point in one to four bytes and keeps the 128 ASCII characters as single bytes with the same values, which is why it has become the standard encoding for the web and for most files. A display controller that shows only digits and a few letters may still use plain ASCII internally.

Error detection with parity

Bits can be corrupted by noise on a cable, a radio link or even by a particle striking a memory cell. The simplest protection is a parity bit: an extra bit chosen so that the total number of 1s is even (even parity) or odd (odd parity). The receiver counts the 1s again; if the count has the wrong parity, at least one bit has flipped. Parity detects any single-bit error and any odd number of errors, but it misses an even number of errors and cannot say which bit is wrong.

The parity of a word is the exclusive-OR of all its bits, which is why parity generators and checkers are built from trees of XOR gates. More powerful codes go further. Hamming codes add several parity bits, each covering a different subset of the data, so that a single error can be located and corrected; error-correcting memory in servers uses this idea. Cyclic redundancy checks (CRCs) detect bursts of errors on communication links and in storage. All of them are built from the same XOR operation.

Gray conversion and parity are both chains of XOR operations, which is why they are cheap in hardware.
# Gray code and parity, both built from exclusive-OR
def to_gray(b):
    return b ^ (b >> 1)

def from_gray(g):
    b = 0
    while g:
        b ^= g
        g >>= 1
    return b

def even_parity_bit(x):
    p = 0
    while x:
        p ^= x & 1
        x >>= 1
    return p

print("n  binary  gray  back")
for n in range(8):
    g = to_gray(n)
    print(f"{n}  {n:03b}     {g:03b}   {from_gray(g)}")
word = 0b1011001
print(f"data {word:07b} -> parity bit {even_parity_bit(word)} for even parity")
Output
n  binary  gray  back
0  000     000   0
1  001     001   1
2  010     011   2
3  011     010   3
4  100     110   4
5  101     111   5
6  110     101   6
7  111     100   7
data 1011001 -> parity bit 0 for even parity

Common mistakes

  • MistakeTreating the carry out as the overflow flag for signed numbers.

    InsteadSigned overflow is the XOR of the carries into and out of the MSB; the carry out matters only for unsigned numbers.

  • MistakeNegating in two's complement by inverting only.

    InsteadInvert every bit and then add 1.

  • MistakeZero-filling a negative number when widening it.

    InsteadSign-extend: copy the MSB into all the new upper bits.

  • MistakeForgetting the +6 correction in BCD addition.

    InsteadIf a digit sum exceeds 9 or carries out, add 0110 and pass a decimal carry.

  • MistakeAssuming parity can correct errors.

    InsteadA single parity bit only detects an odd number of bit errors.

Summary and key terms

  • Binary addition follows decimal rules with 1 + 1 = 10; an unsigned overflow shows as a carry out of the MSB.
  • Two's complement negates by inverting and adding 1; the MSB has weight −2ⁿ⁻¹ and the range is −2ⁿ⁻¹ to 2ⁿ⁻¹ − 1.
  • Signed overflow occurs when two same-sign operands give an opposite-sign result, detected as c_n ⊕ c_{n−1}.
  • Subtraction is addition of the two's complement; widening a signed number needs sign extension.
  • BCD stores each decimal digit in four bits and needs a +6 correction after binary addition.
  • Gray code changes one bit between neighbors; parity bits detect odd numbers of bit errors.
Two's complement
The signed format in which the MSB has negative weight; negation is inversion plus one.
Overflow
A result too large for the word width, flagged by the carry out for unsigned numbers and by c_n ⊕ c_{n−1} for signed numbers.
Sign extension
Copying the MSB of a signed number into new upper bits when it is widened.
BCD
Binary-coded decimal: each decimal digit stored separately in four bits.
Gray code
An ordering of binary patterns in which consecutive values differ in exactly one bit.
Parity bit
An extra bit that makes the total number of 1s even or odd so that single-bit errors can be detected.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Give the 8-bit two's complement form of −100.

    Show answer

    +100 = 0110 0100; invert to 1001 1011; add 1: 1001 1100 (0x9C).

  2. What decimal value does 1110 0101 represent in two's complement?

    Show answer

    −128 + 64 + 32 + 4 + 1 = −27.

  3. Add 0101 0000 and 0011 0000 as 8-bit signed numbers. Is there overflow?

    Show answer

    80 + 48 = 128 → 1000 0000, which reads as −128: two positives gave a negative, so V = 1.

  4. Sign-extend the 6-bit value 101100 to 8 bits and give its value.

    Show answer

    1110 1100 = −20.

  5. Write 593 in BCD.

    Show answer

    0101 1001 0011.

  6. Convert binary 1011 to Gray code.

    Show answer

    g₃ = 1, g₂ = 1 ⊕ 0 = 1, g₁ = 0 ⊕ 1 = 1, g₀ = 1 ⊕ 1 = 0, so 1110.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz