In this tutorial I cover the following topics
- Buttons, buttons... to many buttons
- The resistor ladder
- Multiplexing
- Ready to use multiplexer
- Expanders
Although most of microcontrollers have many input/output pins, there are no problems to spend all of them and find yourselves in a situation when there would be no option to attach enything else. In this part I will show what you can do in case of such a situation. Other words, I will show how to "multiply" our pins.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
#define PIN_0 5 #define PIN_1 4 #define PIN_DELETE 3 #define PIN_ENTER 2 #define PIN_LED 7 #define MAX_CODE_LEN 32 #define CODE "10011" char codeArray[MAX_CODE_LEN+1]; byte codeLen = 0; void setup() { pinMode(PIN_0, INPUT); pinMode(PIN_1, INPUT); pinMode(PIN_DELETE, INPUT); pinMode(PIN_ENTER, INPUT); pinMode(PIN_LED, OUTPUT); Serial.begin(9600); digitalWrite(PIN_LED, LOW); for (int i=0; i<=MAX_CODE_LEN; i++){ codeArray[i] = '\0'; } Serial.println("Ready..."); } void loop() { if(digitalRead(PIN_0) == HIGH){ delay(20); while(digitalRead(PIN_0) == HIGH); delay(20); codeArray[codeLen] = '0'; codeArray[codeLen+1] = '\0'; codeLen += 1; if (codeLen == MAX_CODE_LEN) { codeLen -= 1; } Serial.println(codeArray); } else if(digitalRead(PIN_1) == HIGH){ delay(20); while(digitalRead(PIN_1) == HIGH); delay(20); codeArray[codeLen] = '1'; codeArray[codeLen+1] = '\0'; codeLen += 1; if (codeLen == MAX_CODE_LEN) { codeLen -= 1; } Serial.println(codeArray); } else if(digitalRead(PIN_DELETE) == HIGH){ delay(20); while(digitalRead(PIN_DELETE) == HIGH); delay(20); /* Funny bug if codeLen is declared as byte (to save memory for example). In such case codeLen -= 1 results in codeLen = 255 which is not lower than 0. codeLen -= 1; if (codeLen < 0) { codeLen = 0; } */ if (codeLen > 0) { codeLen -= 1; } codeArray[codeLen] = '\0'; Serial.println(codeArray); } else if(digitalRead(PIN_ENTER) == HIGH){ delay(20); while(digitalRead(PIN_ENTER) == HIGH); delay(20); if(strcmp(codeArray, CODE) == 0) { digitalWrite(PIN_LED, HIGH); Serial.println("Access granted..."); delay(2000); digitalWrite(PIN_LED, LOW); // Don't forget to clear whole buffer to prevent memmory leaks. for (int i=0; i<=MAX_CODE_LEN; i++){ codeArray[i] = '\0'; } codeLen = 0; Serial.println("Ready..."); } Serial.println(codeArray); } } |
Recal voltage divider idea discussed in Voltage divider tutorial. General schema looks like it is showned on the following image
where the formula used to calculate $V_{out}$ takes a form
$$V_{out} = \frac{V_{in}R_{2}}{R_{1}+R_{2}}$$
$$V_{out} = \frac{V_{in}\cdot 0}{R_{1} + 0} = 0$$ | |
$$V_{out} = \frac{V_{in}R_{2}}{R_{1}+R_{2}} = \frac{V_{in}R_{2_{1}}}{R_{1}+R_{2_{1}}} = \frac{V_{in}}{\frac{R_{1}}{R_{2_{1}}}+1}$$ | |
$$V_{out} = \frac{V_{in}R_{2}}{R_{1}+R_{2}} = \frac{V_{in}(R_{2_{1}} + R_{2_{2}})}{R_{1}+R_{2_{1}}+R_{2_{2}}}= \frac{V_{in}}{\frac{R_{1}}{R_{2_{1}}+R_{2_{2}}}+1}$$ | |
$$V_{out} = \frac{V_{in}R_{2}}{R_{1}+R_{2}} = \frac{V_{in}(R_{2_{1}} + R_{2_{2}} + R_{2_{3}})}{R_{1}+R_{2_{1}}+R_{2_{2}}+R_{2_{3}}}= \frac{V_{in}}{\frac{R_{1}}{R_{2_{1}}+R_{2_{2}}+R_{2_{3}}}+1}$$ |
Examples given in previous subsection show few similar but different dividers. You can combine all of them into on schema.
How it works is explained in a set of images below.
This way you have had created a variable voltage divider called a multiple voltage divider
or more often a resistor ladder
.
Now it's time to make a real test.
- Create circuit given in the schema below
- Attach multimeter to our circuit
- Use the following code to programm Arduino
123456789101112131415161718192021222324#define VOLTAGE A0int sensorValue = 0;int i = 0;void printVolts(int val) {float voltage = val * (5.0 / 1023.0);Serial.print(" ADC value: ");Serial.print(val);Serial.print(", volts: ");Serial.println(voltage);}void setup() {Serial.begin(9600);}void loop() {sensorValue = analogRead(VOLTAGE);Serial.print(i);i++;printVolts(sensorValue);delay(1000);} -
Write down results: voltage displayed on multimeter and analog value readed by Arduino and changed into numerical values by analog-digital converter (ADC in short). In this case you got
Switch
pressedResistance value of variable
divider part $R_{2}$
(for constant $R_{1}=10kO$)Voltage used measured as integer
from ADCconverted to volts
from ADC valuemeasured by
multimetertheoretical
value1 0kO 0 0 0.0 0.00 0.0 2 $R_{2}$ = 1kO 0.98kO 95 0.46 0.47 0.45 3 $R_{2}+R_{2}$ = 1kO+1kO=2kO 1.97kO 180 0.89 0.83 0.83 4 $R_{2}+R_{2}+R_{2}$ = 1kO+1kO+1kO=3kO 2.95kO 238 1.16 1.16 1.15
This allows us to interprete each reading as a different button being pressed with a few lines of code -- see listing below. Obviously rather than telling the microcontroler to look for the exact values you expect the analog pin to read, you set a range of values that can be interpreted as belonging to a specific button. I do this by calculating the points halfway between each expected ADC reading and set these as as the boundaries of our ranges (see also image)
Switch | Range | ||
from | to | midpoint | |
1 | 0 | 95 | 47 |
2 | 95 | 180 | 137 |
3 | 180 | 238 | 209 |
4 | 238 | 1023 | 630 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#define VOLTAGE A0 int sensorValue = 0; int i = 0; int getButtonNumber(int val) { if (val <= 47) return 1; else if (val <= 137) return 2; else if (val <= 209) return 3; else if (val <= 630) return 4; else if (val <= 1023) return 0; } void setup() { Serial.begin(9600); } void loop() { sensorValue = analogRead(VOLTAGE); Serial.print(i); i++; Serial.print(" "); Serial.println(getButtonNumber(sensorValue)); delay(1000); } |
The downside is that this can only detect one button press at a time, the button with the lowest resistor value is returned, and the output varies when different voltages are used to power it requiring an update to our software.
So far, so good but this soltion has two drawbacks which makes it working only for a project with a small number of buttons.
- Every time you change the number of buttons, you have to measure what have changed -- you have to measure ADC values for newly added button(s). Neext you have to update our source code.
- What is worse, voltage resolution is not constant. The more buttons you have, the shortest voltage ranges you will have.
Adding another switches, each with a 1kO resistor, you obtain results presented in the table below. To make it more visible, also results for 21, 31 and 41 buttons obtained with 10kO resistors instead 10 times 1kO are included.Switch Range from to midpoint Midpoint
distance1 0 95 47 --- 2 95 180 137 90 3 180 238 209 66 4 238 294 266 57 5 294 342 318 52 6 342 384 363 45 7 384 424 404 41 8 424 458 441 37 9 458 491 474 33 10 491 518 504 30 11 518 21 682 31 766 41 816 1023 919
and depicted in the image
The situation could be even worse if you use 1kO pull-up resistor
Switch Range from to midpoint Midpoint
distance1 15 518 266 --- 2 518 687 602 336 3 687 769 728 126 4 769 820 794 66 5 820 853 836 42 6 853 877 865 29 7 877 895 886 21 8 895 909 902 16 9 909 920 914 12 10 920 930 925 11 11 930 21 974 31 990 41 998 1023 1010
Variable and short voltage ranges may lead to errors both in code and in real devices as a result of noise and components tollerance. Anyway, magic numbers are always bad solution and should be avoided whenever possible. If you should somehow predict ADC value based on ADC resolution and number of buttons then you could implement better solution.
So, lets try find out resistors values to have equidistance measured in voltage, based on the following specification:
- $V_{in}=5V$,
- four buttons,
- pull-up resistor $R_{1}=1kO$,
- voltage distance $V_{d}=0.5$, e.g. $V_{S1}=0$ (voltage output for switch $S1$), $V_{S2}=V_{d}$ (voltage output for switch $S2$), $V_{S3}=2V_{d}$, $V_{S3}=3V_{d}$.
You know the input voltage, output voltage, and resistor $R_{1}$, but not $R_{2}$ which should be calculated. Starting from previously given formula
$$V_{out} = \frac{V_{in}R_{2}}{R_{1}+R_{2}}$$
rearranging its components
$$V_{out}(R_{1}+R_{2}) = V_{in}R_{2}$$
$$V_{out}R_{1}+V_{out}R_{2} = V_{in}R_{2}$$
$$V_{out}R_{1} = V_{in}R_{2}-V_{out}R_{2}$$
$$V_{out}R_{1} = R_{2}(V_{in}-V_{out})$$
you finaly obtain formula for missing resistance $R_{2}$
$$R_{2} = \frac{R_{1}V_{out}}{V_{in}-V_{out}}$$
Based on this formula and notation from the following schema you can calculate desired resistances
- Calculations for $R_{21}$ -- in this case $V_{out}$ should be equal to 0.5V
$$
R_{2} = R_{21} = \frac{R_{1}\cdot V_{out}}{V_{in}-V_{out}} = \frac{1.0 \cdot 0.5}{5.0 - 0.5} = \frac{0.5}{4.5} = 0.11
$$
To get resistance close to theoretical value of 0.11kO which is 110 Ohm, you will use one 100 Ohm resistor and one 10 Ohm so $R_{21}$ would be equal to 110 Ohm. - Calculations for $R_{22}$ -- in this case $V_{out}$ should be equal to 1.0V
$$
R_{2} = R_{21} + R_{22}
$$
On the other hand you know that
$$R_{2} = \frac{R_{1}V_{out}}{V_{in}-V_{out}}$$
so in consequence
$$R_{21} + R_{22} = \frac{R_{1}V_{out}}{V_{in}-V_{out}}$$
and then
$$R_{22} = \frac{R_{1}V_{out}}{V_{in}-V_{out}} - R_{21}$$
$$R_{22} = \frac{1.0 \cdot 1.0}{5.0-1.0} - 0.11 = \frac{1}{4} - 0.11 = 0.25 - 0.11 = 0.14$$
To get resistance close to theoretical value of 0.14kO which is 140 Ohm, you will use one 150 Ohm resistor so $R_{21}$ would be equal to 150 Ohm. - Calculations for $R_{23}$ -- in this case $V_{out}$ should be equal to 1.5V
$$
R_{2} = R_{21} + R_{22} + R_{23}
$$
On the other hand you know that
$$R_{2} = \frac{R_{1}V_{out}}{V_{in}-V_{out}}$$
so in consequence
$$R_{21} + R_{22} + R_{23} = \frac{R_{1}V_{out}}{V_{in}-V_{out}}$$
and then
$$R_{23} = \frac{R_{1}V_{out}}{V_{in}-V_{out}} - R_{21} - R_{22}$$
$$R_{23} = \frac{1.0 \cdot 1.5}{5.0-1.5} - 0.11 - 0.15 = \frac{1.5}{3.5} - 0.26 = 0.428 - 0.26 = 0.168$$
To get resistance close to theoretical value of 0.168kO which is 168 Ohm, you will use one 120 Ohm resistor and one 51 Ohm so $R_{23}$ would be equal to 171 Ohm.
Now you can implement more universal method for detecting the pin which was pressed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#define VOLTAGE A0 #define VOLTAGE_RANGE 100 #define BUTTONS 4 int sensorValue = 0; int i = 0; int getButtonNumber(int val) { if (val > VOLTAGE_RANGE * BUTTONS - VOLTAGE_RANGE/2) { return 0; } else { return (val + VOLTAGE_RANGE/2)/VOLTAGE_RANGE + 1; } } void setup() { Serial.begin(9600); } void loop() { sensorValue = analogRead(VOLTAGE); Serial.print(i); i++; Serial.print(" "); Serial.println(getButtonNumber(sensorValue)); delay(1000); } |
and measure real voltage and ADC values as it is presented in the table below
Switch pressed |
Resistance value of variable divider part $R_{2}$ (for constant $R_{1}=1kO$) |
Resistance distance |
Voltage | |||||
used | measured | calculated | used | as integer from ADC |
converted to volts from ADC value |
measured by multimeter |
theoretical value |
|
0 | --- | --- | --- | --- | 1023 | 5.00 | 4.95 | 5.00 |
1 | 0k$\Omega$ | 0 | 0 | 0 | 0 | 0.0 | 0.02 | 0.0 |
2 | $R_{21}$ = 110 $\Omega$ | 113 $\Omega$ | $R_{21}=110 \Omega$ | $R_{21}=110 \Omega$ | 100 | 0.5 | 0.51 | 0.5 |
3 | $R_{21}+R_{22}$ = 260 $\Omega$ | 260 $\Omega$ | $R_{22}=140 \Omega$ | $R_{22}=150 \Omega$ | 208 | 1.0 | 1.04 | 1.0 |
4 | $R_{21}+R_{22}+R_{23}$ = 431 $\Omega$ | 430 $\Omega$ | $R_{23}=168 \Omega$ | $R_{23}=171 \Omega$ | 306 | 1.5 | 1.51 | 1.5 |
Below you have a real life example of resistor ladder application:
You may ask: What if I press more than one button at once? For example: two or three? Nothing. Really nothing. You don't believe me? Let's make a test in Tinkercad.
Start with pressing one button at a time:
Now, press two buttons and more:
In this case result is the same as you would press third button. If you continue this experiment you will not get different results in a sense that what you get agrees with corresponding one button case:
Multiplexing
is the generic term used to describe the operation of sending one or more analogue or digital signals over a common transmission line at different times or speeds. The device you use to do that is called a multiplexer
and demultiplexer
. An electronic multiplexer (also mux
or data selector
) makes it possible for several signals to share one device or resource, for example, one A/D converter or one communication line, instead of having one device per input signal. It selects between several analog or digital input signals and forwards it to a single output line.
You should be familiar with this concept, as every day you use it (or rather it is used by some devices) to transmit your data through an network:
Generally, the selection of each input line in a multiplexer is controlled by an additional set of inputs called control lines
and according to the binary condition of these control inputs, one appropriate data input is connected directly to the output. Normally, a multiplexer has an even number of $2^{n}$ input lines and a corresponding number of $n$ control inputs. An electronic multiplexer can be considered as a multiple-input, single-output switch. The schematic symbol for a multiplexer is an isosceles trapezoid with the longer parallel side containing the input pins and the short parallel side containing the output pin. The schematic below shows a 2-to-1 multiplexer (also denoted as 2:1 muliplexer or 2:1 mux) on the left and an equivalent switch on the right.
Multiplexers can be either digital circuits made from high speed logic gates used to switch digital or binary data or they can be analogue types using transistors, MOSFET’s or relays to switch one of the voltage or current inputs through to a single output.
To start out easy, you’ll create a multiplexer taking two inputs and a single selector line as it was depicted above. With inputs $X_{0}$ and $X_{1}$ and select line $S$, if $S$ is 0, the $X_{0}$ input will be the output $Y$. If S is 1, the $X_{1}$ will be the output $Y$.
To find a digital circuit imlementing this functionality, first you have to create corresponding truth table:
Inputs | Output | ||
---|---|---|---|
$S$ | $X_{0}$ | $X_{1}$ | $Y$ |
0 | 0 | 0 | 0 |
0 | 0 | 1 | 0 |
0 | 1 | 0 | 1 |
0 | 1 | 1 | 1 |
1 | 0 | 0 | 0 |
1 | 0 | 1 | 1 |
1 | 1 | 0 | 0 |
1 | 1 | 1 | 1 |
Having this table, you can derive a corresponding Boolean formula:
$$
\begin{align}
Y =
& \overline{S} \cdot X_0 \cdot \overline{X_1} + \overline{S} \cdot X_0 \cdot X_1 + S \cdot \overline{X_0} \cdot X_1 + S \cdot X_0 \cdot X_1 = \\
& \overline{S} \cdot X_0 \cdot (\overline{X_1} + X_1) + S \cdot X_1 \cdot (\overline{X_0} + X_0) = \\
& \overline{S} \cdot X_0 + S \cdot X_1\\
\end{align}
$$
The boolean formula for the 2-to-1 multiplexer looks like this:
$$Y= \overline{S} \cdot X_{0} + S \cdot X_{1}$$
or the same as programming logic expression
1 |
Y = (~S && X0) || (S && X1) |
or in the pseudocode style
1 |
Y = OR( AND(NOT(S), X0), AND(S, X1)) |
Based on the given above formulas, you can make a corresponding logic circuit
Of course it can be completed also with other set of gates, for example NAND gates. From the previous material, Completeness of NAND gate set, you know that using only NAND gates you can have equivalents of all three basic gates: AND, OR and NOT as it is showned again below.
Using these ,,replacemants'' to the schema with basic set of gates you obtain
This schema could be simplify. Let's introduce some additional symbols
Now you can write
$$
Y = \overline{E \cdot F} = \overline{\overline{C \cdot C} \cdot \overline{D \cdot D}}
$$
Because in Boolean algebra the following is true
$$
X \cdot X = X
$$
you can write
$$
Y = \overline{\overline{C} \cdot \overline{D}} = \overline{\overline{\overline{A \cdot A}} \cdot \overline{\overline{B \cdot B}}}
$$
Again from the law given above you obtain
$$
Y = \overline{\overline{\overline{A}} \cdot \overline{\overline{B}}}
$$
and applying the following rule
$$
\overline{\overline{X}} = X
$$
you finally get
$$
Y = \overline{A \cdot B}
$$
Other words, none of the gate with output $C$, $D$, $E$ or $F$ is needed, so final chema od multiplexer build only with NAND gates looks like below
You can combine two lower order multiplexers like 2:1 or 4:1 to get higher order multiplexer like 8:1. Now, for example let us try to implement a 4:1 multiplexer using a 2:1 multiplexer. To construct a 4:1 multiplexer using a 2:1 multiplexer, you will have to combine three 2:1 multiplexer together. As it is showned on the image below
The end result should give us 4 input pins ($X_{0}$, $X_{1}$, $X_{2}$ and $X_{3}$), 2 control/select pins ($S_{0}$ and $S_{1}$) and one output pin ($Y$).
Conversely to multiplexer, a
demultiplexer
(or demux
) and pass through to one of multiple output lines, using a select line to choose which output the input goes to.
To find a digital circuit imlementing this functionality, first you have to create corresponding truth table:
Inputs | Outputs | ||
---|---|---|---|
$S$ | $X$ | $Y_{0}$ | $Y_{1}$ |
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 0 | 0 |
1 | 1 | 0 | 1 |
Having this table, you can derive a corresponding Boolean formula which in case of the 1-to-2 demultiplexer is really simple and looks like this:
$$Y_{0}= \overline{S} \cdot X$$
$$Y_{1}= S \cdot X$$
or the same as programming logic expression
1 2 |
Y0 = ~S && X Y1 = S && X |
or in the pseudocode style
1 2 |
Y0 = AND(NOT(S), X) Y1 = AND(S, X) |
Based on the given above formulas, you can make a corresponding logic circuit
Of course it can be completed also with other set of gates, for example NAND gates
There are a lot of different ready to use (de)multiplexer.
- The basic trio of analog multiplexers stems from the venerable 4000-series CMOS logic chips: the 4051 is a single eight-way multiplexer, the 4052 has two four-way multiplexers, and the 4053 has three two-way switches.
- The 74HC405x series chips with higher switching speed or low-voltage analog multiplexers meant for battery-powered use: 74LV405x.
- The high-speed and low-resistance 3251, 3252, and 3253 series chips: CBT3251, FST3252, SN74CBT3253, etc.
In most cases multiplexer can work also as a demultiplexer, so further I will use the term multiplexer in both meanings.
In this part I will show how to work with
CD4051B
and CD4052B
chip (CD4051B, CD4052B, CD4053B datasheet 
CD4051 is an eight (8) channel multiplexer and has three control input named as A
, B
and C
. These inputs connect only 1 out of 8 channels to the output in order to obtain the desired output. Channel I/O terminals became outputs and common O/I become input terminals when CD 4051 is used as a demultiplexer. Pinous for all chips from this family are depicted on the image below and for CD4051 chip is summarized in the following table
Pin |
Meaning | |
A |
selector line coding first bit of binary channel number | |
B |
selector line coding second bit of binary channel number | |
C |
selector line coding third bit of binary channel number | |
0-7 |
independent inputs/outputs channels | |
Vee |
negative supply voltage (connected to ground (GND) in our case) | |
Vss |
ground (GND, 0V) | |
Vdd |
positive supply voltage (from 5 to 20 volts) | |
COM AUT/IN |
common input/output | |
INH |
enable input active on LOW (connected to ground (GND) in our case) |
![]() |
![]() |
Left: Basic schema to test CD4051 chip. Right: Close to real view of a circuit schema given on the right. |
Let's improve static hardware channel selection case and turn it into static software channel selection example. I show a very basic application accompanied by a very simple code. Connect all the components according to the schema given bellow
![]() |
![]() |
Left: Basic schema to test CD4051 chip. Right: Close to real view of a circuit schema given on the right. |
and use the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#define ZERO LOW #define ONE HIGH #define SELECT_PIN_A 2 #define SELECT_PIN_B 3 #define SELECT_PIN_C 4 void setup(){ pinMode(SELECT_PIN_A, OUTPUT); pinMode(SELECT_PIN_B, OUTPUT); pinMode(SELECT_PIN_C, OUTPUT); digitalWrite(SELECT_PIN_A, ZERO); digitalWrite(SELECT_PIN_B, ONE); digitalWrite(SELECT_PIN_C, ZERO); } void loop () { } |
Now I am going to extend our example: instead of "statically" defined channel I will "scan" the whole range of them. For this purpose I add four switches
![]() |
![]() |
Left: Basic schema to test CD4051 chip. Right: Close to real view of a circuit schema given on the right. |
The crucial part of the code designed to work with multiplexer is channel selection function. It doesn't matter if you use it as a multiplexer or demultiplexer, channels are selected the same way. To manipulate bits you can use standard binary operation as well as
bitRead()
function.
Our goal is to read state of one of many switches (in example you will use 4 switches) using only one input. This will work for one button at the time pressed, and also for multipress combination of buttons.
Inside a loop you select one channel in order 4, 5, 6, 7. This way selected channel is connected to COM OUT/IN
pin of 4051 (pin number 3
). Our switches are connected to pin corresponding to channels 4, 5, 6, 7. If you press one of them, say switch of channel 5, then digitalRead()
will return 1 only when channel 5 is looped as only for this channel you will read HIGH
state. This will set switchState
for this channel to be high, while rest of channels will be set to LOW. This way, as long as you keep given switch pressed you should get HIGH state for corresponding channel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
#define ZERO LOW #define ONE HIGH #define SELECT_PIN_A 2 #define SELECT_PIN_B 3 #define SELECT_PIN_C 4 #define INPUT_PIN 5 #define INPUT_CHANNELS 4 byte channels[INPUT_CHANNELS] = {4, 5, 6, 7}; byte channel; byte inputValue; byte switchState[INPUT_CHANNELS] = {LOW, LOW, LOW, LOW}; void setup(){ Serial.begin(9600); pinMode(SELECT_PIN_A, OUTPUT); pinMode(SELECT_PIN_B, OUTPUT); pinMode(SELECT_PIN_C, OUTPUT); pinMode(INPUT_PIN, INPUT); digitalWrite(SELECT_PIN_A, ZERO); digitalWrite(SELECT_PIN_B, ZERO); digitalWrite(SELECT_PIN_C, ZERO); } void loop () { for(channel = 0; channel < INPUT_CHANNELS; channel++) { selectChannelV1(channels[channel]); inputValue = digitalRead(INPUT_PIN); if (switchState[channel] != inputValue) { switchState[channel] = inputValue; Serial.println("Switch " + String(channel) + " has changed value to " + String(inputValue)); } //Serial.print(String(inputValue) + "\t"); } //Serial.println(); delay(100); } void selectChannelV1(byte number){ byte bitValue = bitRead(number, 0); digitalWrite(SELECT_PIN_A, bitValue); bitValue = bitRead(number, 1); digitalWrite(SELECT_PIN_B, bitValue); bitValue = bitRead(number, 2); digitalWrite(SELECT_PIN_C, bitValue); } void selectChannelV2(byte number){ byte bitValue = (number & (1)) ? ONE : ZERO; digitalWrite(SELECT_PIN_A, bitValue); bitValue = (number & (1<<1)) ? ONE : ZERO; digitalWrite(SELECT_PIN_B, bitValue); bitValue = (number & (1<<2)) ? ONE : ZERO; digitalWrite(SELECT_PIN_C, bitValue); } |
Now I stop for a while and try to explain how it works. You use only "left part" - the one with switches; part with leds is unused.
Inside a loop you select one channel in order 4, 5, 6, 7. This way selected channel is connected to COM OUT/IN
pin of 4051 (pin number 3
). Our switches are connected to pin corresponding to channels 4, 5, 6, 7. If you press one of them, say switch of channel 5, then digitalRead()
will return 1 only when channel 5 is looped as only for this channel you will read HIGH
state. This will set switchState
for this channel to be high, while rest of channels will be set to LOW. This way, as long as you keep given switch pressed you should get HIGH state for corresponding channel. This should work also if you press more than one button.
Finally, let's try to have multiple inputs and multiple output. You will try to keep ability to read multiple buttons and in the same time power multiple LEDs. Use the following code to test if it is possible to power all four LEDs at the same time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#define ZERO LOW #define ONE HIGH #define SELECT_PIN_A 2 #define SELECT_PIN_B 3 #define SELECT_PIN_C 4 #define OUTPUT_CHANNELS 2 byte channels[OUTPUT_CHANNELS] = {1, 2}; byte channel; void setup(){ pinMode(SELECT_PIN_A, OUTPUT); pinMode(SELECT_PIN_B, OUTPUT); pinMode(SELECT_PIN_C, OUTPUT); digitalWrite(SELECT_PIN_A, ZERO); digitalWrite(SELECT_PIN_B, ZERO); digitalWrite(SELECT_PIN_C, ZERO); } void loop () { for(channel = 0; channel < OUTPUT_CHANNELS; channel++) { selectChannelV2(channels[channel]); delay(10); } } void selectChannelV1(byte number){ byte bitValue = bitRead(number, 0); digitalWrite(SELECT_PIN_A, bitValue); bitValue = bitRead(number, 1); digitalWrite(SELECT_PIN_B, bitValue); bitValue = bitRead(number, 2); digitalWrite(SELECT_PIN_C, bitValue); } void selectChannelV2(byte number){ byte bitValue = (number & (1)) ? ONE : ZERO; digitalWrite(SELECT_PIN_A, bitValue); bitValue = (number & (1<<1)) ? ONE : ZERO; digitalWrite(SELECT_PIN_B, bitValue); bitValue = (number & (1<<2)) ? ONE : ZERO; digitalWrite(SELECT_PIN_C, bitValue); } |
Whatever you do with (de)multiplexer, always one of many chanels is connected with in/out pin. There is no chance to have for example multiple leds to be truly on -- you can only mimic this, as you have seen this in a last experiment in a previous subsection. Multiplexers are pretty dumb. They just feeds signals through to the connected pins. Only the signal that is currently fed through can have anything to be done with it. So only one signal can have a value written on it or read from. There are no pullup resistors and interrupts.
Instead (de)multiplexer you can try expander. As it name stands, expander expanders something -- your pins. It gives you "extra" real in/out pins. You can read and write to all of the pins. Each one has facilities such as pull-up resistors, change notification interrupts, etc -- the kind of things you expect from real IO pins.
If you ask, why I talk abour multiplexers if you have expanders, the answer is as usual -- because of money. Expanders are about four time more expensive than multiplexers:
Prices in Polish currency (on 2021-11-22):
1 2 3 4 5 6 7 |
Multiplexer: 4052 - 2PLN Expander: MCP23S08 - 10PLN PCF8574N - 7PLN PCF8574 - 8PLN |
Because expanders give you more options how you can use them, this section would be longer than previous. I will start from communication.
Multiplexer, at least the one I'm about to describe (PCF8574N), communicate with other components via the two-line bidirectional bus known as Inter-integrated Circuit, pronounced I-squared-C (I²C) or I2C. Actually you need 4 wires because you also need also two power linees: the VCC and Ground.
The two data wires are:
- SDA - Serial Data (data line) and
- SCL - Serial Clock (clock line).
- Voltages used: +5 V or +3.3 V
- Maximum number of Masters: Unlimited
- Maximum number of Slaves: 1008
- Maximum Speed:
- Standard Mode = 100kbps
- Fast Mode = 400kbps
- High Speed Mode = 3.4 Mbps
- Ultra Fast Mode = 5 Mbps
- for PCF8574:
- general address takes the form
0 1 0 0 A2 A1 A0
, - the lowest address is
0 1 0 0 0 0 0
which is 32 decimaly and 0x20 hexadecimaly, - the highest address is
0 1 0 0 1 1 1
which is 39 decimaly and 0x27 hexadecimaly;
- general address takes the form
- for PCF8574A:
- general address takes the form
0 1 1 1 A2 A1 A0
, - the lowest address is
0 1 1 1 0 0 0
which is 56 decimaly and 0x38 hexadecimaly, - the highest address is
0 1 1 1 1 1 1
which is 63 decimaly and 0x3F hexadecimaly.
- general address takes the form
- Arduino UNO pin A4 (SDA) and A5 (SCL) from ANALOG IN group,
- Arduino UNO R3 pins dedicated to SDA and SCL and located in the same row as digital pins after pin 13, GND and AREF (last two pins in the row) but also you can use pin A4 (SDA) and A5 (SCL) from ANALOG IN group,
- Arduino Nano pin A4 (SDA) and A5 (SCL),
- Arduino MEGA pin 20 (SDA) and 21 (SCL) from COMMUNICATION group.
expander.write(value)
It allows you to set the binary value of thevalue
argument on all output pins of the chip.expander.read ()
It allows you to read the binary value of all pins.expander.clear ()
Sets all output pins toLOW
.expander.set ()
Sets all pins toHIGH
.
The I2C bus technology was originally designed by Philips Semiconductors in the early ’80s to allow easy communication between components which reside on the same circuit board. Usually there is one master and one or multiple slaves on the line, however there can be multiple masters aswell. Both masters and slaves can transmit or receive data.
I2C is a short distance, serial communication protocol, so data is transferred 'bit-by-bit' along the single wire or the SDA line. The output of bits is synchronized to the sampling of bits by a clock signal 'shared' between the master and the slave. The clock signal is always controlled by the master. The Master generates the clock and initiates communication with slaves.
Other parameters you may be interested in:
Devicen on I2C bus are recognized by its address. In case of PCF8574, you can connect at the maximum 8 of these devices in a project to the same I2C bus -- more about this a little bit later.
If you want to know more about the I2C technology please check out my I2C tutorial (not ready yeat).
In this part I will show how to work with PCF8574
chip (NXP PCF8574 datasheet , Texas Instruments PCF8574 datasheet
)
Pinous for typical PCF8574 chip enclosed in DIP16 package is depicted on the image below (you can also find this chip enclosed in SSOP20 package):
![]() |
![]() |
![]() |
Left: Pin configuration of PCF8574 chip in DIP16 package. Middle: Real photo of PCF8574. Right: Pin configuration of PCF8574 chip in SSOP20 package. |
Pin |
Meaning |
---|---|
A0-A2 |
address input 0-2 |
P0-P7 |
quasi-bidirectional I/O 0-7 |
SCL |
serial clock line |
SDA |
serial data line |
VSS |
supply ground |
VDD |
supply voltage |
INT |
interrupt output (active LOW) |
As you can see, PCF8574 has three "address" pins: A0-A2
. Saying the truth, these pins are responsible only for part of reall I2C address. Chips from the PCF8574 family has a 7-bit address. The first 3 bits of the address are given by the user by setting the pins A0, A1, A2. Another 4 are factory-set permanently. The PCF8574 chip has them set to 0100 and the PCF8574A chip set to 0111. In consequence
Different boards offers I2C data lines on different pins. In case of the most popular Arduino bords data lines are located at:
Blink code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <PCF8574.h> #include <Wire.h> PCF8574 expander; void setup() { expander.begin(0x20); expander.pinMode(4, OUTPUT); } void loop() { expander.digitalWrite(4, LOW); delay(1000); expander.digitalWrite(4, HIGH); delay(1000); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <PCF8574.h> #include <Wire.h> PCF8574 expander; void setup() { expander.begin(0x20); expander.pinMode(4, OUTPUT); } void loop() { expander.toggle(4); delay(1000); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <PCF8574.h> #include <Wire.h> PCF8574 expander; void setup() { Serial.begin(9600); expander.begin(0x20); expander.pinMode(4, INPUT); } void loop() { byte value = expander.digitalRead(4); Serial.println(value); delay(100); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <PCF8574.h> #include <Wire.h> PCF8574 expander; void setup() { Serial.begin(9600); expander.begin(0x20); expander.pinMode(4, INPUT); expander.pinMode(5, INPUT); expander.pullUp(4); expander.pullDown(5); } void loop() { byte valueUp = expander.digitalRead(4); byte valueDown = expander.digitalRead(5); Serial.println(valueUp); Serial.println(valueDown); delay(1000); } |
Parallel access to the pins are functions thanks to which you can handle all the pins of the PCF8574 system at once with one call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <PCF8574.h> #include <Wire.h> PCF8574 expander; void setup() { expander.begin(0x20); for (byte i=0; i<8; i++) { expander.pinMode(i, OUTPUT); } } void loop() { byte value = random(0, 255); expander.write(value); delay(100); } |
Interrupt signal tells microcontroller that something is happend. With this, you don't have to constantly check the state of the pin on the chip -- you will be notified when a specific type of change occurs. In most cases you can select the type of change:
You can read more about interrupts in my Multitasking tutorial.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#include <PCF8574.h> #include <Wire.h> PCF8574 expander; void onInterrupt() { Serial.println("Interrupt"); } void setup() { Serial.begin(9600); expander.begin(0x20); expander.pinMode(4, INPUT); expander.pullUp(4); pinMode(2, INPUT); digitalWrite(2, HIGH); expander.enableInterrupt(2, onInterrupt); } void loop() { } |
The connection of an interrupt to a function can be broken with the function “expander.disableInterrupt” which has no argument.
1 |
EXAMPLE IS NEEDED |
This solution has one disadvantage. Any input pin on the chip calls the same function.
If you put a call to the method “expander.checkForInterrupt” in the interrupt handler (“onInterrupt”), the library will gain new possibilities.
From now on you can use the “expander.attachInterrupt” methods to record functions called separately for each pin of the PCF8574 chip.
The method "expander.attachInterrupt" has 3 arguments. The first is the input pin number of the chip you want to connect your function to. The next argument is the name of the function you want to connect. The last one is the type of event the function is to trigger. There are 4 types of events.
CHANGE - runs functions on each change of the pin state
LOW - runs the function if the chip pin is low
FALLING - runs the function when the state changes from high to low
RISING - runs functions when the state changes from low to high
In the example, I set pins 0 and 1 of the chip to inputs with the default high state. Then I registered the "OnPin0" and "OnPin1" functions for them, triggered during the change of state from high "HIGH" to low "LOW" ("FALLING").
The pin connection with the function can be canceled with the function “expander.detachInterrupt”. Its one parameter is the pin number of the chip.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
"FIX" IT - add detachInterrupt #include <PCF8574.h> #include <Wire.h> PCF8574 expander; void onPin0() { Serial.println("Pin 0"); } void onPin1() { Serial.println("Pin 1"); } void onInterrupt() { Serial.println("Interrupt"); expander.checkForInterrupt(); } void setup() { Serial.begin(9600); expander.begin(0x20); expander.pinMode(3, INPUT); expander.pullUp(3); expander.pinMode(4, INPUT); expander.pullUp(4); pinMode(2, INPUT); digitalWrite(2, HIGH); expander.enableInterrupt(2, onInterrupt); expander.attachInterrupt(3, onPin0, FALLING); expander.attachInterrupt(4, onPin1, FALLING); } void loop() { } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
FIX IT - implement more functionality #include <PCF8574.h> #include <Wire.h> PCF8574 expander1; PCF8574 expander2; void onInterrupt() { expander1.checkForInterrupt(); expander2.checkForInterrupt(); } void setup() { expander1.begin(0x20); expander2.begin(0x21); pinMode(2, INPUT); digitalWrite(2, HIGH); expander1.enableInterrupt(2, onInterrupt); } void loop() { } |