Skip to content
Informatics

Data representation: binary, hexadecimal, two's complement and characters

Binary and hexadecimal, two's complement, overflow in C with examples, fixed-width types, fixed point, ASCII and UTF-8, bitwise operators.

Inside a computer everything is made of bits: numbers, letters, images, instructions. The same group of 8 bits, 01000001, can be the number 65, the letter A or part of an instruction, depending on how it is interpreted. In this lesson we see how signed and unsigned integers are represented, two’s complement, hexadecimal notation, characters and the overflow problems that follow.

The binary system

In base 2 each digit is worth a power of 2: (1011)_2 = 1\cdot 8 + 0\cdot 4 + 1\cdot 2 + 1\cdot 1 = 11. To convert a decimal number to binary, divide repeatedly by 2 and read the remainders from the bottom up: 13 → 6 remainder 1, 3 remainder 0, 1 remainder 1, 0 remainder 1, so 13 = (1101)_2.

With n bits you represent 2^n values: with 8 bits (a byte) from 0 to 255, with 16 bits from 0 to 65,535, with 32 bits from 0 to about 4.3 billion.

Hexadecimal

Binary numbers are long and hard to read. Base 16 uses the digits 0-9 and A-F (A = 10, …, F = 15), and each hexadecimal digit corresponds to exactly 4 bits. Conversion is immediate, in groups of four:

Binary1010001111110000
HexadecimalA3F0

So (1010\,0011\,1111\,0000)_2 = \text{0xA3F0}. In C hexadecimal numbers are written with the prefix 0x. Memory addresses, web page colours (#FF8000) and register values in datasheets are written in hexadecimal.

Signed numbers: two’s complement

The natural way to represent negatives, a sign bit plus the magnitude, has two flaws: two zeros (+0 and −0) and different adder circuits for positives and negatives. Every processor uses two’s complement instead: with n bits, the most significant bit weighs -2^{n-1} instead of +2^{n-1}.

Four-bit two's complement wheel with binary codes and signed values
With 4 bits the 16 codes represent the numbers from −8 to +7. Moving one step round the wheel adds 1; from 0111 (+7) you go to 1000 (−8): that is overflow.

With 4 bits: (1011)_2 = -8 + 2 + 1 = -5. The ranges are -2^{n-1} to 2^{n-1} - 1: with 8 bits −128 to 127, with 32 bits about −2.1 to +2.1 billion.

How to change sign. Invert all the bits and add 1. From +5 = 0101: invert, 1010; add 1, 1011 = −5. Check: 0101 + 1011 = 1\,0000, and the carry beyond the fourth bit is discarded: 0.

The advantage. Addition works the same way for positives and negatives: the same adder computes 7 + (-3) by adding 0111 + 1101 = 1\,0100, that is 4 discarding the carry. Subtraction becomes addition of the opposite.

Overflow

If the result does not fit in the available bits, it “wraps round” the wheel. With 8-bit signed numbers, 127 + 1 = -128. In C:

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    uint8_t u = 250;
    u = u + 10;               /* unsigned: 260 - 256 = 4 */
    printf("%u\n", u);        /* prints 4 */

    int16_t t = 30000;
    t = t + 10000;            /* signed: the result does not fit in 16 bits */
    printf("%d\n", t);        /* on almost every system prints -25536 */
    return 0;
}

For unsigned types C guarantees arithmetic modulo 2^n. For signed ones overflow is undefined behaviour: the compiler may assume it never happens, and results can be surprising. It is one of the most common sources of firmware bugs: a 32-bit unsigned millisecond counter wraps to zero after about 49.7 days, and code comparing times must allow for it.

The fixed-width types of <stdint.h> (uint8_t, int16_t, uint32_t, …) are preferred in microcontroller programs, because the size of int changes from one processor to another: 16 bits on an AVR, 32 on an ARM.

Real numbers

Numbers with a fractional part are represented in floating point according to IEEE 754 (the float and double types in C), described in the Numerical Analysis lesson on errors. On microcontrollers without a floating-point unit fixed point is often used instead: you work with integers that represent, for example, thousandths (a voltage of 3.301 V becomes 3301 mV).

Characters

A character is a number according to an encoding table. ASCII (1963) uses 7 bits for 128 characters: ‘A’ = 65, ‘a’ = 97, ‘0’ = 48, space = 32, newline = 10. In C a char is a small integer, and you can do arithmetic on characters: '7' - '0' is 7, the standard way to turn a digit into a number.

For all the world’s scripts there is Unicode, with over 150,000 characters, almost always encoded in UTF-8: ASCII characters take one byte, accented letters two (è = 0xC3 0xA8), ideograms three, emoji four. That is why a C string holding “café” takes 5 bytes, not 4.

Bitwise operators

C lets you work on individual bits with the operators & (AND), | (OR), ^ (XOR), ~ (NOT), << and >> (shifts). A left shift by k positions multiplies by 2^k: x << 3 equals x * 8. They are essential for communication protocols and for peripheral registers, as we will see in the lesson on C for embedded systems.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x
()
x