last updated: 2022-05-07
For students:
Beginning with this chapter we will come across many "Just do it" tasks. These tasks have to be carried out and have to be documented thoroughly!
Your marks will depend on the documentation!
To better understand parts of this module, first work through the module electronics fundamentals ELEFU
.
Song of this chapter: Kraftwerk > Computerwelt > Nummern
We live in a digital world. Digital comes from digit or digiti in Latin meaning "fingers". Fingers are used to count and nine of them correspond for us to the ten digits 0-9 (symbols) of the decimal system.
If we use numbers we quantize a value, making the step from analog (continuous) to digital (sequence of discrete values). Computers need numbers. We will see later in this course how analog signals are converted into digital signals.
So why did digital electronics won over analog electronics?
Analog signals are subject to electronic noise and distortion which can progressively degrade the signal.
With the increasing speed of digital circuits and their miniaturization the analog world didn't stand a chance in most applications.
OK, it's not the most tingling subject to begin with, but without a good understanding how digital systems calculate and use only two states (high voltage and low voltage) to do all their job, you will not really progress if problems occur in your projects (and they will occur!). So let' begin at the beginning:
We have ten fingers, to count from one to ten, an unary numeral system in which every natural number is represented by a corresponding number of symbols (e.g fingers). After the invention of the new symbol Zero
(0
), it was possible to develop a positional numeral system because zero is of crucial importance to be able to "skip" a power.
Today we use a base-10 numeral system called decimal system with 10 digits from zero to nine.
Mankind had other possibilities! If we would use our fingers in a binary positional number system with each finger as a position we could count from zero to 1023
. Or why don't we use today a base-20 (vigesimal, fingers and toes) numeral system like in the Mesoamerican Long Count calendar from the Maya?
In a positional base b numeral system we get b basic digits (symbols).
As zero is included, b-1 natural numerals are used.
Base | digits |
---|---|
2 |
0-1 |
8 |
0-7 |
10 |
0-9 |
16 |
0-9 + A-F |
Numeral systems with different bases can have sometimes funny names like Quaternary, Nonary, Duodecimal, Tritridecimal or Duosexadecimal. For a list look here.
The binary
(base 2) and hexadecimal
(base 16) systems are, extensively used in computer science. Sometimes also the octal (base 8) system. The binary system uses only the digits 0
and 1
. The hexadecimal system uses all the digits from the decimal system, plus the letters A
through F
(decimal numerals 10 to 15).
A numeral is a sequence of digits, which may be of arbitrary length. Each position in the sequence has a decimal place value. Place values for different bases:
Base | Place | |||||
---|---|---|---|---|---|---|
b | 6 | 5 | 4 | 3 | 2 | ones place |
Base | Values | |||||
b |
b5 |
b4 |
b3 |
b2 |
b1 |
b0 |
2 | 32 | 16 | 8 | 4 | 2 | 1 |
8 | 32768 | 4096 | 512 | 64 | 8 | 1 |
10 | 100000 | 10000 | 1000 | 100 | 10 | 1 |
16 | 1048576 | 65536 | 4096 | 256 | 16 | 1 |
The place value can be calculated with the base:
place value = b(place number-1)
The value of the numeral is calculated by multiplying each digit's value in the sequence by its place value, and summing the results. Let's see some examples:
Base | example | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
2 | 1101 | = | 1·8 | + | 1·4 | + | 0·2 | + | 1·1 | = | 13(10) |
8 | 1101 | = | 1·512 | + | 1·64 | + | 0·8 | + | 1·1 | = | 577(10) |
10 | 1101 | = | 1·1000 | + | 1·100 | + | 0·10 | + | 1·1 | = | 1101(10) |
16 | 1101 | = | 1·4096 | + | 1·256 | + | 0·16 | + | 1·1 | = | 4353(10) |
For mathematicians a number is a count that is an idea in our minds. A numeral is a symbol or name that stands for a number. In short: the number is an idea, the numeral is how we write it. In normal language, the words number and numeral are used for the same thing. But we use always the word digit when talking about the single symbols that make up numerals (Ziffer und Zahl).
Because we don't want to confound numbers of different numeral systems, it's important to use subscribed parentheses with the base included:
1001(2) = 9(10), 1001(2) ≠ 1001(10)
.
There exist also positions for numbers less than 1
, so that floats are no problem.
Base | Place | |||||
---|---|---|---|---|---|---|
b | 3 | 2 | 1 | -1 | -2 | -3 |
Base | Values | |||||
b |
b2 |
b1 |
b0 |
b-1 |
b-2 |
b-3 |
2 | 4 | 2 | 1 | 1/2 | 1/4 | 1/8 |
16 | 256 | 16 | 1 | 1/16 | 1/256 | 1/4096 |
Calculate by hand (note the calculus) the values in decimal for the following numbers:1010011(2)
, 4567(8)
, 53(16)
, BABA(16)
.
Calculate by hand (note the calculus) the values in decimal for the following numbers:11100,11(2)
, 1100001100,001(8)
, 7B3A,4(16)
.
Read the clock (each column is a binary number hh:mm:ss) and note the result.
Computers and electronics have no fingers but another way to represent the state of anything: high voltage and low (or no) voltage (current, field).
So all calculations and manipulations of computers rely on a base-2 number system, the binary numeral system.
In binary we have the two symbols: 0
and 1
. But even by using only two symbols we can create any number that a decimal system can.
The basic unit of information used in computing and digital communications is the bit
(BInary digiT). The bit can only have one of two values. To work with bits we use two-state devices like per example a switch.
The unit symbol for the bit is bit
.
The two physical states of a binary digit represented by a device can be interpreted as logical values
(true
/false
, yes
/no
), activation states (on
/off
) or any other two-valued attribute.
The assignment is a matter of convention, and different assignments may be used even within the same device or program.
In our image the convention is that of the positive logic
, meaning the higher voltage level represents the value 1
and the lower voltage level represents the value 0
.
negative logic
(look in "Special voltage dividers: circuits with pull-up or pull-down resistors" in ELEFU
chapter Kirchhoff).Any number can be represented by a sequence of bits. As seen above we use a positional system. The greater the number the more positions we need. As the number of positions in a binary system can get very high, the hexadecimal system (base 16) is often used by humans to ease the handling.
Binary |
Hex. | Dec. | | | Binary |
Hex. | Dec. | | | Binary |
Hex. | Dec. | | | Binary |
Hex. | Dec. |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 |
0 | 0 | | | 10000 |
10 | 16 | | | 100000 |
20 | 32 | | | 110000 |
30 | 48 |
1 |
1 | 1 | | | 10001 |
11 | 17 | | | 100001 |
21 | 33 | | | 110001 |
31 | 49 |
10 |
2 | 2 | | | 10010 |
12 | 18 | | | 100010 |
22 | 34 | | | 110010 |
32 | 50 |
11 |
3 | 3 | | | 10011 |
13 | 19 | | | 100011 |
23 | 35 | | | 110011 |
33 | 51 |
100 |
4 | 4 | | | 10100 |
14 | 20 | | | 100100 |
24 | 36 | | | 110100 |
34 | 52 |
101 |
5 | 5 | | | 10101 |
15 | 21 | | | 100101 |
25 | 37 | | | 110101 |
35 | 53 |
110 |
6 | 6 | | | 10110 |
16 | 22 | | | 100110 |
26 | 38 | | | 110110 |
36 | 54 |
111 |
7 | 7 | | | 10111 |
17 | 23 | | | 100111 |
27 | 39 | | | 110111 |
37 | 55 |
1000 |
8 | 8 | | | 11000 |
18 | 24 | | | 101000 |
28 | 40 | | | 111000 |
38 | 56 |
1001 |
9 | 9 | | | 11001 |
19 | 25 | | | 101001 |
29 | 41 | | | 111001 |
39 | 57 |
1010 |
A | 10 | | | 11010 |
1A | 26 | | | 101010 |
2A | 42 | | | 111010 |
3A | 58 |
1011 |
B | 11 | | | 11011 |
1B | 27 | | | 101011 |
2B | 43 | | | 111011 |
3B | 59 |
1100 |
C | 12 | | | 11100 |
1C | 28 | | | 101100 |
2C | 44 | | | 111100 |
3C | 60 |
1101 |
D | 13 | | | 11101 |
1D | 29 | | | 101101 |
2D | 45 | | | 111101 |
3D | 61 |
1110 |
E | 14 | | | 11110 |
1E | 30 | | | 101110 |
2E | 46 | | | 111110 |
3E | 62 |
1111 |
F | 15 | | | 11111 |
1F | 31 | | | 101111 |
2F | 47 | | | 111111 |
3F | 63 |
The length of a binary number may be referred to as its bit-length.
As seen above the value of the numeral is calculated by multiplying each digit's value in the sequence by its place value, and summing the results.
Base | example | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
2 | 1101 | = | 1·8 | + | 1·4 | + | 0·2 | + | 1·1 | = | 13(10) |
16 | 1101 | = | 1·4096 | + | 1·256 | + | 0·16 | + | 1·1 | = | 4353(10) |
One method to to convert a decimal number to binary is repeatedly dividing the decimal number by 2
, until it is reduced to zero. Every time we divide the remainder of the division becomes a digit in the binary number. To get the remainder is easy because we are dividing by two: If the dividend is even, the remainder will be 0 and if the dividend is odd the remainder is 1.
Example converting 187(10)
to binary:
remainder | direction to read | |||
---|---|---|---|---|
187 |
/ 2 = |
93 |
1 |
⇧ |
93 |
/ 2 = |
46 |
1 |
⇧ |
46 |
/ 2 = | 23 |
0 |
⇧ | |
23 |
/ 2 = |
11 |
1 |
⇧ |
11 |
/ 2 = |
5 |
1 |
⇧ |
5 |
/ 2 = |
2 |
1 |
⇧ |
2 |
/ 2 = |
1 |
0 |
⇧ |
1 |
/ 2 = |
0 |
1 |
⇧ |
187(10) = 10111011(2)
By grouping 4 bits we get the hexadecimal number:
1011 1011(2) = BB(16) = 11·16 + 11 = 187(10)
.
The same dividing method used for binary numbers can be used to convert decimal numbers to hexadecimal numbers:
Example converting 2019(10)
to hexadezimal:
remainder | direction to read | |||
---|---|---|---|---|
2019 |
/ 16 = |
126 |
3 |
⇧ |
126 |
/ 16 = |
7 |
14 |
⇧ |
7 |
/ 16 = | 0 |
7 |
⇧ |
2019(10) = 7E3(16)
Calculate by hand (note the calculus) the binary value of 2019(10)
.
As stated above, we get the hexadecimal number by grouping 4 bits to one hexadecimal digit beginning with the least significant 4 bits. Get the hexadecimal number of the binary equivalent of 2019(10)
by grouping 4 bits beginning with the lowest bits. Compare the result with the example above.
Summand + Summand = Sum
Addend + Addend = Sum
Rules:
S1 | + | S2 | = | Sum | Carry (C) |
---|---|---|---|---|---|
0 |
+ | 0 |
= | 0 |
0 |
0 |
+ | 1 |
= | 1 |
0 |
1 |
+ | 0 |
= | 1 |
0 |
1 |
+ | 1 |
= | 0 |
1 |
S1 | + | S2 | + | Ci | = | Sum | Carry (C) |
---|---|---|---|---|---|---|---|
1 |
+ | 1 |
+ | 1 |
= | 1 |
1 |
Ci stands for Carry in.
Examples:
1011(2) |
+ |
control in decimal: | 11(10) |
+ |
||
10011(2) |
19(10) |
|||||
11 |
C |
1 |
C |
|||
----- |
-- |
|||||
11110(2) |
30(10) |
1111(2) |
+ |
control in decimal: | 15(10) |
+ |
||
1111(2) |
15(10) |
|||||
10101(2) |
21(10) |
|||||
1 |
||||||
1 11 |
C |
1 |
C |
|||
------ |
-- |
|||||
110011(2) |
51(10) |
To build a binary adder we need a digital adder circuit. We will learn about such a circuit in the next chapter.
Minuend - Subtrahend = Difference
Rules:
M | - | S | = | Difference | Borrower (B) |
---|---|---|---|---|---|
0 |
- | 0 |
= | 0 |
0 |
0 |
- | 1 |
= | 1 |
1 |
1 |
- | 0 |
= | 1 |
0 |
1 |
- | 1 |
= | 0 |
0 |
M | - | S1 | - | S2 | = | Difference | Borrower (B) |
---|---|---|---|---|---|---|---|
0 |
- | 1 |
- | 1 |
= | 0 |
1 |
1 |
- | 1 |
- | 1 |
= | 1 |
1 |
This rules are only valid if we subtract a smaller number from a bigger number (Minuend ≥ Subtrahend)). If the Subtrahend is bigger we exchange Subtrahend and Minuend and add a minus sign to the result.
Examples:
11101(2) |
- |
control in decimal: | 29(10) |
- |
||
1010(2) |
10(10) |
|||||
1 |
B |
B |
||||
----- |
-- |
|||||
10011(2) |
19(10) |
111000111(2) |
- |
control in decimal: | 455(10) |
- |
||
101001011(2) |
331(10) |
|||||
1111 |
B |
B |
||||
------ |
-- |
|||||
001111100(2) |
124(10) |
One drawback of direct subtraction is that borrowing frequently over many positions is tedious and confusing. As our processor has only digital adders it's by ways more convenient to use a method doing the subtraction by adding the bases complement of the subtrahend (this can be done in all positional numeral systems: e.g tenth complement in a decimal system).
To get the complement the easiest way is often to get b-1-complement and add the number one (1) afterwards. This gets very easy in a binary system because we get the b-1-complement (2 - 1 = 1: one's complement) by inverting the subtrahend. Before doing this, the subtrahend must be stuffed with leading zeros to match the length of the minuend!
To get the two's complement we add the number one to the one's complement. In binary systems the one's complement is calculated by inverting the number!
Examples:
Subtrahend | One's complement | Two's complement | |||
---|---|---|---|---|---|
11101(2) |
00010(2) |
+ | 1 | = | 00011(2) |
0000100(2) |
1111011(2) |
+ | 1 | = | 1111100(2) |
Another advantage of the complements method is that the minuend can be lesser then the subtrahend which is not possible in the direct method.
Rules:
Examples:
We use the same examples as in the direct subtraction:
29(10) - 10(10) = 19(10)
stuffed | 1's complement | 2's complement | ||||
---|---|---|---|---|---|---|
11101(2) |
- |
|||||
1010(2) |
01010(2) |
10101(2) |
10110(2) |
|||
11101(2) |
+ |
|||||
10110(2) |
||||||
1 |
11 |
C |
positive |
|||
----- |
||||||
10011(2) |
455(10) - 331(10) = 124(10)
1's complement | 2's complement | ||||
---|---|---|---|---|---|
111000111(2) |
- |
||||
101001011(2) |
010110100(2) |
010110101(2) |
|||
111000111(2) |
+ |
||||
010110101(2) |
|||||
1 |
11 111 |
C |
positive |
||
--------- |
|||||
001111100(2) |
In both examples a carry occurred, so the results are positive. Let' try the last example by changing minuend and subtrahend, so that the minuend is lesser:
331(10) - 455(10) = -124(10)
1's complement | 2's complement | ||||
---|---|---|---|---|---|
101001011(2) |
- |
||||
111000111(2) |
000111000(2) |
000111001(2) |
|||
101001011(2) |
+ |
||||
000111001(2) |
|||||
0 |
1111 11 |
C |
negative |
||
--------- |
|||||
110000100(2) |
No carry, so the result is negative. We get the absolute value with the two's complement of the result:
Result: 110000100(2)
One's complement: 001111011(2)
Two's complement: 001111100(2) = 124(10)
To build a binary subtraction circuit we need the digital adder from above and an inverter circuit.
Multiplicand · Multiplier = Product
Factor · Factor = Product
Rules:
Md | · | Mr | = | Product |
---|---|---|---|---|
0 |
· | 0 |
= | 0 |
0 |
· | 1 |
= | 0 |
1 |
· | 0 |
= | 0 |
1 |
· | 1 |
= | 1 |
This is the truth table of an AND
gate. The AND
gate is a one bit multiplier circuit.
AND
with the bits of the multiplier beginning with the least significant bit (20
).AND
's) are shifted by one position to the left and added to the momentary result.Example:
11011(2) |
· |
control in decimal: | 27(10) |
· |
|
1101(2) |
13(10) |
||||
--------- |
--- |
||||
11011 |
81 |
||||
00000 |
27 |
||||
11011 |
1 |
C |
|||
11011 |
--- |
||||
11111 |
C |
351(10) |
|||
--------- |
|||||
101011111(2) |
To build a binary multiplier we need AND
gates, shift registers and adder circuits.
Dividend / Divisor = Quotient
Numerator / Denominator = Quotient
The division is a successive subtraction.
Rules:
Example:
Let's do the following division: 1011101,11(2) / 110(2)
.
The two's complement of the divisor is 001(2)+1 = 010(2)
.
If we do a subtraction with 3 positions and 1001(2)+1 = 1010(2)
if we do a subtraction with 4 positions:
1011101,110(2)/ |
110(2) = 1111,101(2) |
decimal control: | 93,75(10) / 6(10) = 15,625(10) |
||
010|||| ||| |
6 |
||||
---|||| ||| |
--- |
||||
|
C |
no carry n+1 : discard |
0 ⇩ |
33 |
|
1011||| ||| |
30 |
||||
1010||| ||| |
-- |
||||
1 1 ||| ||| |
C |
carry n+1 : positive |
1 ⇩ |
3 7 |
|
----||| ||| |
3 6 |
||||
01011|| ||| |
--- |
||||
1010|| ||| |
15 |
||||
1 1 || ||| |
C |
carry n+1 : positive |
1 ⇩ |
12 |
|
----|| ||| |
-- |
||||
01010| ||| |
30 |
||||
1010| ||| |
30 |
||||
1 1 | ||| |
C |
carry n+1 : positive |
1 ⇩ |
-- |
|
----| ||| |
0 |
||||
01001 ||| |
|||||
1010 ||| |
|||||
1 ||| |
C |
carry n+1 : positive |
1 ⇩ |
||
---- ||| |
|||||
0011 1|| |
|||||
101 0|| |
|||||
111 || |
C |
carry n+1 : positive |
1 ⇩ |
||
-----|| |
|||||
000 11| |
|||||
10 10| |
|||||
0 1 | |
C |
no carry n+1 : discard |
0 ⇩ |
||
-----| |
|||||
|
|||||
0 110 |
|||||
1 010 |
|||||
11 1 |
C |
carry n+1 : positive |
1 ⇩ |
||
----- |
|||||
0 000 |
To do a binary division we need shift registers, adders and inverters.
The rules are the same as in decimal calculations. Here two examples:
Addition
ABC(16) |
+ |
control in decimal: | 2748(10) |
+ |
||
983(16) |
2435(10) |
|||||
11 |
C |
1 1 |
C |
|||
----- |
---- |
|||||
143F(16) |
5183(10) |
Subtraction
ABC(16) |
- |
control in decimal: | 2748(10) |
- |
||
9F3(16) |
2547(10) |
|||||
1 |
B |
B |
||||
--- |
---- |
|||||
0C9(16) |
201(10) |
Multiplication and Division
As child we learn the decimal multiplication table by heart. To facilitate the hex multiplication you can use a table for hexadecimal multiplication (Learn it by heart ;)).
The division works the same as for decimal numbers.
It's easier to do the calculations in binary and convert the results in hexadecimal numbers.
To do the four basic arithmetic operations we need:
All these circuits are implemented in the Arithmetic Logic Unit (ALU
) of modern processors and controllers.
37(10) + 425(10)
4477(10) - 425(10)
121(10) · 425(10)
4477(10) / 37(10)
.Digital circuits, processors and micro-controller are often built to work with a fixed length of bits. First processors worked with 4 bits called a nibble
. A nibble
is half a byte and big enough to hold a Binary-Coded Decimal (BCD
, digits 0-9). A nibble
is also one digit in hexadecimal!
The byte
was historically the number of bits used to encode a single character of text in a computer. Often it was also the smallest addressable unit of memory in many computer architectures. As no standards existed until 1993, the byte could have different length (e.g. six bit or 9 bit).
The international standard IEC 80000-13 defined as unit symbol for the byte the upper-case character B
.
A byte has 8 bit permitting the values 0 through 255 (28 = 256
).
A multiple of the unit byte is not the kilobyte, because kilo
is defined as Thousand (1000 decimal) and does not fit in the binary system. For quantities of digital information, the binary prefix Ki(kibi)
(bi for binary!) was defined and is part of part of the International System of Quantities.
Kibi
means 210
, or 1024
.
One kibibyte is 1024 bytes and the unit symbol for the kibibyte is KiB.
Other binary prefixes are mebi
, gibi
and tebi
.
binary prefix | unit symbol | value | difference to decimal in % |
---|---|---|---|
kibi | KiB | 210 = 1024 | 2.4 |
mebi | MiB | 220 = 10242 = 1048576 | 4.9 |
gibi | GiB | 230 = 10243 = 1073741824 | 7.4 |
tebi | TiB | 240 = 10244 = 1099511627776 | 10 |
pebi | PiB | 250 = 10245 = 1125899906842624 | 12.6 |
exbi | EiB | 260 = 10246 = 1152921504606846976 | 15.3 |
If we find indications in kB
(MB
,GB
) we calculate with Thousand.
A hard-disk with 6TB
has 6,000,000,000,000 byte or 5.457TiB
.
The word
is the piece of data used by by the instruction set respectively the hardware of a processor. The bit-length of the word (word length) is an important characteristic of the used processor or computer architecture.
The word size varies in modern processors and micro-controllers from 8 to 64 bits (steps of 8 bits).
As we often use fixed length (mostly multiple of 1 byte) to store data in computers, leading zeros are necessary for numbers with less bits. E.g. the number 101(2)
will be written 00000101(2)
if we use one byte.
Leading zeros are not always required but they do help by showing the bit-length of the number.
Negative numbers are created with the two’s complement (see above). The highest bit (the "sign-bit"), is needed to flag the number as a negative number. To get the two’s complement, the rest of the bits are inverted and 1
is added. As the highest bit is no longer available, the maximal positive number we can get with a byte is only 127(10).
bytes | range unsigned | range signed |
---|---|---|
1 |
0 to 255(10) (28-1) |
-128(10) to 127(10) |
2 |
0 to 65535(10) (216-1) |
-32768(10) to 32767(10) |
4 |
0 to 4294967295(10) (232-1) |
-2147483648(10) to 2147483647(10) |
Test the following program on 3 different Arduino platforms (e.g. Teensy 2.0 or Arduino Uno, Arduino Due, ESP8266, ESP32). Make a table in calc or excel to compare the values.
// jdinc5_test_data_types.ino
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("Numbers of bytes for:");
Serial.println("-------- unsigned whole numbers ----");
Serial.println("bool: " + String(sizeof(bool)));
Serial.println("byte: " + String(sizeof(byte)));
Serial.println("uint16_t: " + String(sizeof(uint16_t)));
Serial.println("word: " + String(sizeof(word)));
Serial.println("unsigned int: " + String(sizeof(unsigned int)));
Serial.println("unsigned long: " + String(sizeof(unsigned long)));
Serial.println("unsigned long long: " + String(sizeof(unsigned long long)));
Serial.println("-------- signed whole numbers ------");
Serial.println("char: " + String(sizeof(char)));
Serial.println("short: " + String(sizeof(short)));
Serial.println("int: " + String(sizeof(int)));
Serial.println("long: " + String(sizeof(long)));
Serial.println("long long: " + String(sizeof(long long)));
Serial.println("-------- floating point numbers ----");
Serial.println("float: " + String(sizeof(float)));
Serial.println("double: " + String(sizeof(double)));
Serial.println("-------- special -------------------");
Serial.println("size_t: " + String(sizeof(size_t)));
}
void loop() {}
As seen, binary is what drives all electronics. The electronic circuit knows only two states. An important point is the storing of the states, respectively our information. In the image above the information (one byte) is stored mechanically in the switches.
In computers we need a circuit capable of storing the information. This can be done e.g. with one transistor and one capacitor for volatile dynamic random-access memory (DRAM) or with more transistors to build memory cells with bistable flip-flops in static random-access memory (SRAM). This will be a chapter of it's own in this tutorial :).
Our circuits memorize states. The interpretation of this states is arbitrary! Let's look at an example. We take a double register of our Arduino (ATmega328), filled with the following ones and zeros:
1011101001100010(2)
Test the following program. Open the serial monitor to watch the output. Look at the data types and interpret the different outputs. Try to figure out why the second character displayed (high-byte) is weird.
// jdinc6_test_numbers.ino
int i = 0B1011101001100010;
bool b = i;
unsigned int ui = i;
float f = i;
char sl = lowByte(i);
char sh = highByte(i);
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("bool:\t\t\t" + String(b));
Serial.println("unsigned int:\t\t" + String(ui));
Serial.println("unsigned int (bin):\t" + String(ui,BIN));
Serial.println("unsigned int (oct):\t" + String(ui,OCT));
Serial.println("unsigned int (hex):\t" + String(ui,HEX));
Serial.println("int:\t\t\t" + String(i));
Serial.println("float:\t\t\t" + String(f));
Serial.println("char low nibble:\t" + String(sl));
Serial.println("char high nibble:\t" + String(sh));
}
void loop() {}
We see that the same bits can mean a number, even in different numeral systems, or character(s) or an instruction of our programming language:
The flash of the micro-controller used for Arduino Uno (where the program is stored) is organized in words of 16 bit. If we put the same ones and zeros in the flash, they will be interpreted as a command line of a program written in machine code. In assembly language this would be the command out PORTD,r6
, to copy the byte of register number six to the eight pins of port D.
The assignment of a unique sequence of bits to the representation of each one of a set of numbers, letters or instructions is called a binary code (see next chapter).
While programming, it is important to use the correct data type! A wrong data type can give erratic results. E.g. the following instruction will provoke a faulty data-rate for a serial communication:
int bit_rate = 115200;
Sometimes it's very difficult to find these errors, because they can show up very randomly. So think twice before using a data type.
All data needs to be stored, and the memory of micro-controller is very limited. Also the selection of how to store data makes an impact on performance. So select always the smallest variable that is large enough, and make the data type unsigned where suited. To hold e.g. a digital pin number in Arduino (0-54(10)) an int
(-32768(10) to 32767(10)) is overkill and can lead to errors. A byte is largely enough (0-255(10)).
The reference is here.
bool
occupies one byte of memory. 00000000(2)
means false
, all other values mean true
. Boolean is a non-standard type alias for bool. Use bool
instead.
byte
stores an 8-bit unsigned number (0 to 255(10)). Other types that mean the same are unsigned char
and uint8_t
. Use byte
or uint8_t
.
word
stores an unsigned number, same as unsigned integer
or uint16_t
. The lengths must not be the same. The length of uint16_t
is always 16 bit (0 to 65535(10)). The lengths of word
and unsigned integer
depend on the architecture. Use the program from above if you need to get the length of your platform.
unsigned long
and uint32_t
are the same and can be used for big numbers (4 byte: 0 to 4294967295(10)). If we use constants, we must add ul
or UL
to the number.
int
(integer) is normally our primary data-type for number storage. It is signed, so the highest bit is used to show if the number is negative (1
) or not (0
). On ATmega based boards (Uno, Teensy 2.0) an int
is a 16-bit value with the number range from -32768(10) to 32767(10). On other boards (Teensy 3.1, ESP8266, ESP32) we have a 32-bit (4-byte) value for int
(-2147483648(10) to 2147483647(10)). If we want only 16 bit on all platforms we can use the type short
(seldom needed). The 8-bit variant of integer is char
(-128(10) to 127(10)) mostly used to store characters (see later).
long
is used for bigger integers (4 byte, -2147483648(10) to 2147483647(10)). If we use constants, we must add l
or L
to the number.
float
is the datatype for floating-point numbers (number with decimal point). Floating-point numbers can be much bigger than integers and are stored in 4 bytes. There range is from -3.4028235·1038(10) to 3.4028235·1038(10), but they have only 6-7 decimal digits of precision. Floating point numbers are not exact! so pay attention when comparing float
numbers. Also the calculations with floating point numbers on the original Arduino platform (ATmega) are much slower than with integer and should be avoided (if needed use an ESP32 instead). Don't forget the decimal point, otherwise the number will be treated as an int
(see here for details).
char
is the data type that stores a character value (1 byte). Characters are stored as signed numbers, so it is possible to do math with them ('A'+1 = 'B'). There encoding can be found in the ASCII chart. Single quotes are used in C to assign characters (e.g. char big_b = 'B'
).
double
is normally the bigger float
with more precision (e.g. up to 15 digits) and 8 byte. This is not true on the original Arduino platform, where double is the same size as float. If you need double use an ESP8266, ESP32 or Teensy3.x).
long long
, unsigned long long
, and uint64_t
with 8 byte are defined in C but should not be used on the original Arduino platform, because of the restricted amount of memory.
String()
constructs an instance of the String class, so we use here object oriented programming (OOP). Because of the little memory of original Arduino boards, the improper use of this data type can make your sketches fragile and unstable. So use String
only if you know what you do and on bigger platforms (e.g. ESP32). Better use arrays of char
.
array
is a data type for a collection of variables that are accessed with an index number. For more information on arrays look in the Arduino array reference.
void
is only used in function declarations to indicate that the function does not return information to the function from which it is called.
Table with byte-length of data types
data type | Uno (T2.0) | Teensy 3.6 | ESP8266 | ESP32 | |
---|---|---|---|---|---|
unsigned whole numbers: | |||||
bool | 1 | 1 | 1 | 1 | |
byte | 1 | 1 | 1 | 1 | |
uint16_t | 2 | 2 | 2 | 2 | |
word |
2 |
2 |
4 |
4 |
|
unsigned int |
2 |
4 |
4 |
4 |
|
unsigned long | 4 | 4 | 4 | 4 | |
unsigned long long | 8 | 8 | 8 | 8 | |
signed whole numbers: | |||||
char | 1 | 1 | 1 | 1 | |
short | 2 | 2 | 2 | 2 | |
int |
2 |
4 |
4 |
4 |
|
long | 4 | 4 | 4 | 4 | |
long long | 8 | 8 | 8 | 8 | |
floating point numbers: | |||||
float | 4 | 4 | 4 | 4 | |
double | 4 | 8 | 8 | 8 | |
special: | |||||
size_t |
2 |
4 |
4 |
4 |
Our data is normally stored in variables, because we mostly want to work with the data, and so change this data. In our micro-controller all bytes in the random access memory (RAM) are variables (Arduino Uno 2 kibibyte).
The read-only memory (ROM
, flash
) is much bigger (Arduino Uno 32 kibibyte) than the SRAM
. So if we use constants, it is a good practice to add the const
keyword to our data type. The compiler will store this data in the flash, saving memory in the SRAM.
In Arduino (C) we must always assign a data type to a variable or a constant. Her an example with a variable and a constant:
const float PI = 3.1415;
float circ, r;
void setup() {
Serial.begin(115200);
r = 11;
circ = 2*PI*r;
Serial.println(circ);
}
void loop() {}
In the sketch above we use also the number 2
as a constant. We can use integer constants or floating point constants.
Without further indications the number is treated as an integer. To make it a floating point constant we have to add a decimal point:
circ = 2.0*PI*r;
If we want to specify an integer constant with another data type we add a u
or U
to force the constant into an unsigned data format, a l
or L
to force the constant into a long data format, or both for an unsigned long constant.
a = 33u+v;
b = 33L*w;
c = 100000000UL/d;
If we want to add values in other bases than the base 10, we use the special formatters 0b
for binary and 0x
for hexadecimal (B like mentionned in the Arduino reference is not convenient, because of its limitation to one byte, we use the C++ variant "0b" instead):
int bina = 0b11011101;
uint16_t hexa = 0xFAC3;
If we need to convert one data type to another (also known as cast) we get 6 different functions to do this:
byte()
,char()
,float()
,int()
,long()
and word()
.
Serial.print()
Often we use the serial terminal for debugging or for showing results, if no display is available. The function Serial.print()
has the ability to convert a number with a second parameter to another base. Try the following program:
int dec_number = 1458;
int neg_dec_number = -1458;
void setup() {
Serial.begin(115200);
delay(500);
Serial.println(dec_number,BIN);
Serial.println(neg_dec_number,BIN);
Serial.println(dec_number,HEX);
Serial.println(neg_dec_number,HEX);
}
void loop() {}
Write an Arduino function that converts a positive decimal number (unsigned int
) to binary. The output will be an array of characters, also known as a C-string (the 32 characters are accessed with the indexes 0-31, the 33 character automatically given to terminate the string (null-terminated string). Use the modulo operator in your program. The following code is already given:
// iot_jdinc7_dec2bin_string.ino
long dec_number = 1459;
void setup() {
Serial.begin(115200);
delay(1500);
Serial.println(dec2bin_string(dec_number));
}
void loop() {}
char * dec2bin_string(int dec_number) {
static char bin_string[33] = {"00000000000000000000000000000000"};
byte remainder;
//
// here comes your code
//
return bin_string;
}
In the above "Just do it"-exercise you had to calculate by hand. Write an Arduino program to verify the calculations with the output below in the serial terminal. Document the source code and the output of the program. The program should begin with the following lines :
// iot_jdinc8_calculations.ino
long add1 = 37;
long add2 = 425;
long sub1 = 4477;
long sub2 = 425;
long mul1 = 121;
long mul2 = 425;
long div1 = 4477;
long div2 = 37;
void setup() {
Serial.begin(115200);
delay(500);
Serial.println("------------ ADDITIONS -------------------");
....
}
void loop() {}
------------ ADDITIONS -------------------
37 + 425 = 462
25 + 1a9 = 1ce
100101 + 110101001 = 111001110
------------ SUBTRACTIONS ----------------
4477 - 425 = 4052
117d - 1a9 = fd4
1000101111101 - 110101001 = 111111010100
------------ MULTIPLICATIONS -------------
121 · 425 = 51425
79 · 1a9 = c8e1
1111001 · 110101001 = 1100100011100001
------------ DIVISIONS -------------------
4477 / 37 = 121
117d / 25 = 79
1000101111101 / 100101 = 1111001
Tip:
You can use the String()
function to get less Serial.print()
commands. Example:
Serial.print(String(add1,HEX) + " + " + String(add2,HEX) + " = " + String(add1+add2,HEX));
The assignment of a unique sequence of bits to the representation of each one of a set of numbers, letters or instructions is called a binary code.
Even the assignment of bits to the binary numeral system is a code. A binary-code working with 8 bit can represent the numbers from 0-255(10). It can also represent 256 different characters or 256 different machine code instructions.
An often needed code, that is close family with the binary numeral system, is the BCD-code. BCD stands for Binary Coded Decimals.
Decimal- |
23 |
22 |
21 |
20 |
---|---|---|---|---|
0 |
0 | 0 | 0 | 0 |
1 |
0 | 0 | 0 | 1 |
2 |
0 | 0 | 1 | 0 |
3 |
0 | 0 | 1 | 1 |
4 |
0 | 1 | 0 | 0 |
5 |
0 | 1 | 0 | 1 |
6 |
0 | 1 | 1 | 0 |
7 |
0 | 1 | 1 | 1 |
8 |
1 | 0 | 0 | 0 |
9 |
1 | 0 | 0 | 1 |
1 | 0 | 1 | 0 | |
1 | 0 | 1 | 1 | |
1 | 1 | 0 | 0 | |
1 | 1 | 0 | 1 | |
1 | 1 | 1 | 0 | |
1 | 1 | 1 | 1 |
One decimal number is represented with a nibble (4 bit) also known as tetrade. From the 16 tetrades only 10 are used. The don't care-states (unused tetrades) are named pseudo-tetrad(e)s.
To show that working with codes is not always straightforward we will look at an addition in BCD.
For results less than 10(10) we have no problems. If the result is bigger, we have to deal with carry or a pseudo-tertade. In both cases a correction with the number 6(10) is needed.
Examples
0001 1000(BCD) |
+ |
control in decimal: | 18(10) |
+ |
||
0101 0100(BCD) |
54(10) |
|||||
1 |
C |
1 |
C |
|||
---- ---- |
-- |
|||||
0110 1100 |
+ |
72(10) |
||||
0110 |
correction! (pseudo-tetrade) | |||||
1 1 |
C |
|||||
---- ---- |
||||||
0111 0010(BCD) |
0001 1000(BCD) |
+ |
control in decimal: | 18(10) |
+ |
||
0011 1000(BCD) |
38(10) |
|||||
111 |
C |
1 |
C |
|||
---- ---- |
-- |
|||||
0101 0000 |
+ |
56(10) |
||||
0110 |
correction! (carry) | |||||
|
C |
|||||
---- ---- |
||||||
0101 0110(BCD) |
Other Tetrade-Codes are e.g. the Gray-code, the Excess-3-code or the Aiken-code.
A very important (and old :)) code, because it is used to represent text in computers, telecommunications equipment, and other devices is the American Standard Code for Information Interchange ASCII
.
Most modern character-encoding codes support many additional characters, but they all support the basic set of 128 ASCII characters (7 Bit).
Interesting are the first 32 codes. These were used as control characters to control devices (such as teletype machines or printers).
Two important control characters used even today are:
line feed
(10(10), 0x0A, LF, '\n'
) andcarriage return
(13(10), 0x0D, CR, '\r'
).In the physical media of typewriters and printers, line feed meant "down" and carriage return "across". (two axes of motion).
Line feed, also named newline
is used to signify the end of a line of text and the start of a new one (↵ Enter key on the keyboard). Carriage return can mean the same in software, so newline in character encoding can be defined as LF or CR or LF and CR combined into one (commonly called CR+LF or CRLF).
Linux and Unix-like systems use LF
('\n'
) as newline.
Windows and OS/2 (Apple) use CRLF
('\r\n'
).
Older systems like Apple II or Commodore C64 used CR
('\r'
) only.
Dec. | Oct. | Hex | Binary | Value | Dec. | Oct. | Hex | Binary | Value | ||
---|---|---|---|---|---|---|---|---|---|---|---|
000 | 000 | 000 | 00000000 | NUL | (Null char.) | 064 | 100 | 040 | 01000000 | @ | |
001 | 001 | 001 | 00000001 | SOH | (Start of Header) | 065 | 101 | 041 | 01000001 | A | |
002 | 002 | 002 | 00000010 | STX | (Start of Text) | 066 | 102 | 042 | 01000010 | B | |
003 | 003 | 003 | 00000011 | ETX | (End of Text) | 067 | 103 | 043 | 01000011 | C | |
004 | 004 | 004 | 00000100 | EOT | (End of Transmission) | 068 | 104 | 044 | 01000100 | D | |
005 | 005 | 005 | 00000101 | ENQ | (Enquiry) | 069 | 105 | 045 | 01000101 | E | |
006 | 006 | 006 | 00000110 | ACK | (Acknowledgment) | 070 | 106 | 046 | 01000110 | F | |
007 | 007 | 007 | 00000111 | BEL | (Bell) | 071 | 107 | 047 | 01000111 | G | |
008 | 010 | 008 | 00001000 | BS | (Backspace) | 072 | 110 | 048 | 01001000 | H | |
009 | 011 | 009 | 00001001 | HT | (Horizontal Tab) | 073 | 111 | 049 | 01001001 | I | |
010 | 012 | 00A | 00001010 | LF | (Line Feed) | 074 | 112 | 04A | 01001010 | J | |
011 | 013 | 00B | 00001011 | VT | (Vertical Tab) | 075 | 113 | 04B | 01001011 | K | |
012 | 014 | 00C | 00001100 | FF | (Form Feed) | 076 | 114 | 04C | 01001100 | L | |
013 | 015 | 00D | 00001101 | CR | (Carriage Return) | 077 | 115 | 04D | 01001101 | M | |
014 | 016 | 00E | 00001110 | SO | (Shift Out) | 078 | 116 | 04E | 01001110 | N | |
015 | 017 | 00F | 00001111 | SI | (Shift In) | 079 | 117 | 04F | 01001111 | O | |
016 | 020 | 010 | 00010000 | DLE | (Data Link Escape) | 080 | 120 | 050 | 01010000 | P | |
017 | 021 | 011 | 00010001 | DC1 | (XON) (Device Control 1) | 081 | 121 | 051 | 01010001 | Q | |
018 | 022 | 012 | 00010010 | DC2 | (Device Control 2) | 082 | 122 | 052 | 01010010 | R | |
019 | 023 | 013 | 00010011 | DC3 | (XOFF)(Device Control 3) | 083 | 123 | 053 | 01010011 | S | |
020 | 024 | 014 | 00010100 | DC4 | (Device Control 4) | 084 | 124 | 054 | 01010100 | T | |
021 | 025 | 015 | 00010101 | NAK | (Negative Acknowledgement) | 085 | 125 | 055 | 01010101 | U | |
022 | 026 | 016 | 00010110 | SYN | (Synchronous Idle) | 086 | 126 | 056 | 01010110 | V | |
023 | 027 | 017 | 00010111 | ETB | (End of Trans. Block) | 087 | 127 | 057 | 01010111 | W | |
024 | 030 | 018 | 00011000 | CAN | (Cancel) | 088 | 130 | 058 | 01011000 | X | |
025 | 031 | 019 | 00011001 | EM | (End of Medium) | 089 | 131 | 059 | 01011001 | Y | |
026 | 032 | 01A | 00011010 | SUB | (Substitute) | 090 | 132 | 05A | 01011010 | Z | |
027 | 033 | 01B | 00011011 | ESC | (Escape) | 091 | 133 | 05B | 01011011 | [ | |
028 | 034 | 01C | 00011100 | FS | (File Separator) | 092 | 134 | 05C | 01011100 | \ | |
029 | 035 | 01D | 00011101 | GS | (Group Separator) | 093 | 135 | 05D | 01011101 | ] | |
030 | 036 | 01E | 00011110 | RS | (Req to Send)(Rec Sep) | 094 | 136 | 05E | 01011110 | ^ | |
031 | 037 | 01F | 00011111 | US | (Unit Separator) | 095 | 137 | 05F | 01011111 | _ | |
032 | 040 | 020 | 00100000 | SP | (Space) | 096 | 140 | 060 | 01100000 | ` | |
033 | 041 | 021 | 00100001 | ! | 097 | 141 | 061 | 01100001 | a | ||
034 | 042 | 022 | 00100010 | " | 098 | 142 | 062 | 01100010 | b | ||
035 | 043 | 023 | 00100011 | # | 099 | 143 | 063 | 01100011 | c | ||
036 | 044 | 024 | 00100100 | $ | 100 | 144 | 064 | 01100100 | d | ||
037 | 045 | 025 | 00100101 | % | 101 | 145 | 065 | 01100101 | e | ||
038 | 046 | 026 | 00100110 | & | 102 | 146 | 066 | 01100110 | f | ||
039 | 047 | 027 | 00100111 | ' | 103 | 147 | 067 | 01100111 | g | ||
040 | 050 | 028 | 00101000 | ( | 104 | 150 | 068 | 01101000 | h | ||
041 | 051 | 029 | 00101001 | ) | 105 | 151 | 069 | 01101001 | i | ||
042 | 052 | 02A | 00101010 | * | 106 | 152 | 06A | 01101010 | j | ||
043 | 053 | 02B | 00101011 | + | 107 | 153 | 06B | 01101011 | k | ||
044 | 054 | 02C | 00101100 | , | 108 | 154 | 06C | 01101100 | l | ||
045 | 055 | 02D | 00101101 | - | 109 | 155 | 06D | 01101101 | m | ||
046 | 056 | 02E | 00101110 | . | 110 | 156 | 06E | 01101110 | n | ||
047 | 057 | 02F | 00101111 | / | 111 | 157 | 06F | 01101111 | o | ||
048 | 060 | 030 | 00110000 | 0 | 112 | 160 | 070 | 01110000 | p | ||
049 | 061 | 031 | 00110001 | 1 | 113 | 161 | 071 | 01110001 | q | ||
050 | 062 | 032 | 00110010 | 2 | 114 | 162 | 072 | 01110010 | r | ||
051 | 063 | 033 | 00110011 | 3 | 115 | 163 | 073 | 01110011 | s | ||
052 | 064 | 034 | 00110100 | 4 | 116 | 164 | 074 | 01110100 | t | ||
053 | 065 | 035 | 00110101 | 5 | 117 | 165 | 075 | 01110101 | u | ||
054 | 066 | 036 | 00110110 | 6 | 118 | 166 | 076 | 01110110 | v | ||
055 | 067 | 037 | 00110111 | 7 | 119 | 167 | 077 | 01110111 | w | ||
056 | 070 | 038 | 00111000 | 8 | 120 | 170 | 078 | 01111000 | x | ||
057 | 071 | 039 | 00111001 | 9 | 121 | 171 | 079 | 01111001 | y | ||
058 | 072 | 03A | 00111010 | : | 122 | 172 | 07A | 01111010 | z | ||
059 | 073 | 03B | 00111011 | ; | 123 | 173 | 07B | 01111011 | { | ||
060 | 074 | 03C | 00111100 | < | 124 | 174 | 07C | 01111100 | |||
061 | 075 | 03D | 00111101 | = | 125 | 175 | 07D | 01111101 | } | ||
062 | 076 | 03E | 00111110 | > | 126 | 176 | 07E | 01111110 | ~ | ||
063 | 077 | 03F | 00111111 | ? | 127 | 177 | 07F | 01111111 | Nul |
An escape sequence is a combination of characters which represents no text. They are supposed to be intercepted by the program and perform a special function. In the C resp. C++ (Arduino) languages, it's a series of 2 or more characters, starting with a backslash (\
). They can be used directly in a string. Important escape characters:
Special character | Escape sequence |
---|---|
line feed | \n |
carriage return | \r |
tabulator | \t |
Example:
Serial.print("Hello\tWorld\n");
Even with always better and cheaper dot matrix displays, seven-segment displays (SSD's) with LED's are a cheap and effective possibility for displaying decimal numerals even in the dark.
A single byte can encode the full state of a 7-segment-display including a dot (8 LED's).
We want to use a 7-segment display (5161AS), to display the value of a nibble (4 bit) in hexadecimal. Search the data sheet and draw a circuit to connect the display to the Arduino Uno or Teensy 2.0. We will use one full Arduino Port (portB for Teensy 2.0 or portD for Arduino Uno), so connect segment A to pin 0 of portB (PB0, 20, LSB), segment B to Pin 1 of portB (PB1, 21) etc.. Don't forget the series resistors. Document the circuit and note your calculations for the resistors.
Test with an Arduino program that lights one LED after another with a delay of 1 second. Document the program.
Next we will light all LED's at once.
Tip: To address all Pins of portB we can write PORTB = 0xFF;
to light all LED's. With the command DDRB = 0xFF;
we can replace all 8 pinMode()
commands). Look for more info here: https://www.instructables.com/Arduino-and-Port-Manipulation/.
To use our display we will use a decoding lookup table. The value we want to represent gives us the index of the table where we find the right byte to send to our SSD. Complete the following lookup table:
Bits of PortB: | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | ||
---|---|---|---|---|---|---|---|---|---|---|
Segment: | dp |
g |
f |
e |
d |
c |
b |
a |
Byte: |
|
0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0x3F |
||
0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0x06 |
||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 | ||||||||||
0 |
Let's build a voltmeter with a 4 digit SSD showing the voltage from from 0 to 5000 mV. We will use a 4 digit SSD with time multiplex and the analog input A0 (0 V-5 V) of a Teensy 2.0 or Arduino Uno.
As seen in electronics fundamentals because of the persistence of vision our eye doesn't see LED's flicker if the frequency is above 50 Hz. This is used for LED POV displays. We will switch the four digits on and off for 1 ms one after the other. This permits to use only 12 pins instead of 33. Look at the data sheet of our 4-digit SSD SH5461AS, calculate the resistors and draw the circuit.
Connect the 7 segments to portB (PB0-PB6) for Teensy 2.0 or portD for Arduino Uno. The 4 digits are switched with the lowest nibble of portD (Teensy, portB for Arduino Uno). Test the voltmeter program with a potentiometer (outer terminals to 5 V and ground, wiper (mostly the middle terminal) connected to the analog digital converter (ADC) pin A0 (ADC0)).