Welsh Centre for Printing and Coatings
Wednesday October 29, 2025
title: Interfacing to digital I/O with C author: name: Ben Clifford affiliation: Welsh Centre for Printing and Coatings copyright: Copyright © 2021-2025 Swansea University. All rights reserved. date: 10/29/2025 code-annotations: below — ::: {.notes} Presentation version of these notes. :::
In this lecture we will be looking at two different approaches to reading and writing to ports on a microcontroller.
We begin by looking at digital inputs and outputs before moving onto to show a detailed example program implemented on the Atmel ATmega328 microcontroller.
Imagine a circuit with LEDs connected to D8
and D9
of the Atmel ATmega328 microcontroller.
Both LEDs will be wired to PORTD. How can the LED at D9
be switched on without changing the state of the LED at D8
?
The answer is to use the pin functions provided by the Arduino library. These functions allow programmers to gain direct access to particular pins
pinMode
takes two unsigned 8-bit integers as arguments and has return type of void (returns nothing).digitalWrite
takes two unsigned 8-bit integers as arguments and has return type of void (returns nothing).digitalRead
takes one unsigned 8-bit integer as an argument and returns a signed integer value.The wiring_digital.c file contains the definitions for the pinMode
, digitalWrite
and digitalRead
functions.
See Listing 2, Listing 3 and Listing 4 for extracts from this library file.
pinMode
void pinMode(uint8_t pin, uint8_t mode)
{
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *reg, *out;
if (port == NOT_A_PIN) return;
// JWS: can I let the optimizer do this?
reg = portModeRegister(port);
out = portOutputRegister(port);
if (mode == INPUT) {
uint8_t oldSREG = SREG;
cli();
*reg &= ~bit;
*out &= ~bit;
SREG = oldSREG;
} else if (mode == INPUT_PULLUP) {
uint8_t oldSREG = SREG;
cli();
*reg &= ~bit;
*out |= bit;
SREG = oldSREG;
} else {
uint8_t oldSREG = SREG;
cli();
*reg |= bit;
SREG = oldSREG;
}
}
digitalWrite
void digitalWrite(uint8_t pin, uint8_t val)
{
uint8_t timer = digitalPinToTimer(pin);
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *out;
if (port == NOT_A_PIN) return;
// If the pin that support PWM output, we need to turn it off
// before doing a digital write.
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
out = portOutputRegister(port);
uint8_t oldSREG = SREG;
cli();
if (val == LOW) {
*out &= ~bit;
} else {
*out |= bit;
}
SREG = oldSREG;
}
digitalRead
int digitalRead(uint8_t pin)
{
uint8_t timer = digitalPinToTimer(pin);
uint8_t bit = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
if (port == NOT_A_PIN) return LOW;
// If the pin that support PWM output, we need to turn it off
// before getting a digital reading.
if (timer != NOT_ON_TIMER) turnOffPWM(timer);
if (*portInputRegister(port) & bit) return HIGH;
return LOW;
}
Logical Operation | Operator |
---|---|
AND | & |
OR | | |
XOR | ^ |
NOT | ~ |
Shift right | >> |
Shift left | << |
The truth tables for bitwise logical operators are given for AND (&
) in Table 2, OR (|
) in Table 3, XOR (^
) in Table 4, and NOT (~
) in Table 5.
A | B | A & B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
A | B | A | B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
A | B | A ^ B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
A | ~A |
---|---|
0 | 1 |
1 | 0 |
Consider an example where you want to know if bits 0 and 7 are both on / high / logic 1 but you don’t care about any other bits.
If bits 0 and 7 are HIGH
\[ \begin{array}{lcrr} \mathrm{Bit\ No.} & & 7654 & 3210 \\\hline \mathrm{Input} & & \mathbf{1}010 & 001\mathbf{1} \\ \mathrm{Mask} & \& & 1000 & 0001 \\\hline \mathrm{Result} & & 1000 & 0001 \end{array} \]
If bits 0 and 7 are LOW
\[ \begin{array}{lcrr} \mathrm{Bit\ No.} & & 7654 & 3210 \\\hline \mathrm{Input} & & \mathbf{0}010 & 001\mathbf{0} \\ \mathrm{Mask} & \& & 1000 & 0001 \\\hline \mathrm{Result} & & 0000 & 0000 \end{array} \]
Imagine a circuit with LED’s connected to D8 and D9 of the Atmel ATmega328 microcontroller.
How can the LED at D9
be switched on without changing the state of the LED at D8
?
Imagine a circuit with LED’s connected to D8 and D9 of the Atmel ATmega328 microcontroller.
How can the LED at D9
be switched on without changing the state of the LED at D8
?
D8 = PortB0
D9 = PortB1
Let us assume Port B currently reads \(1010\,0001\) and we execute5:
\[ \begin{array}{lrrl} & 1010 & 0001 & \mathrm{Port\ B}\\ | & 0000 & 0010 & \mathrm{Bitmask\ for\ D9} \\ \hline & 1010 & 00\mathbf{1}1 & \mathrm{D9\ is\ on} \end{array} \]
To turn the LED on D9
off, we use the logical AND.
\[ \begin{array}{lrrl} & 1010 & 0001 & \mathrm{Port\ B}\\ \& & 1111 & 1101 & \mathrm{Bitmask\ for\ D9} \\ \hline & 1010 & 00\mathbf{0}1 & \mathrm{D9\ is\ off} \end{array} \]
Figure 2: A photograph of the example circuit which has two buttons and two LEDs. When the left button is pressed, the red LED lights up. When the right button is pressed the green LED lights up.
What does the code for this look like without using the predefined Arduino functions pinMode
and digitalRead
?
<stdint.h>
.Specifier | Signing | Bits | Bytes | Minimum Value | Maximum Value |
---|---|---|---|---|---|
int8_t |
Signed | 8 | 1 | \(-2^7\) | \(2^7-1\) |
uint8_t |
Unsigned | 8 | 1 | \(0\) | \(2^8 - 1\) |
int16_t |
Signed | 16 | 2 | \(-2^{16}\) | \(2^{16} - 1\) |
uint16_t |
Unsigned | 16 | 2 | \(0\) | \(2^{16} - 1\) |
int32_t |
Signed | 32 | 4 | \(-2^{31}\) | \(2^{31} - 1\) |
uint32_t |
Unsigned | 32 | 4 | \(0\) | \(2^{32} - 1\) |
int64_t |
Signed | 64 | 8 | \(-2^{63}\) | \(2^{63} - 1\) |
uint64_t |
Unsigned | 64 | 8 | \(0\) | \(2^{64} - 1\) |
The I/O memory map is shown in Figure 3.
Figure 3: Memory map for the I/O ports in the Atmel ATmega328
We need to map a port to the address used by the port. We use #define
for this:
#define
PORTD
uint8_t
0x2B
.The full command is6:
The full set up which sets up the ports, data direction registers and pins is:
//I/O and ADC Register definitions taken from datasheet
#define PORTD (*(volatile uint8_t *)(0x2B))
#define DDRD (*(volatile uint8_t *)(0x2A))
#define PIND (*(volatile uint8_t *)(0x29))
#define PORTB (*(volatile uint8_t *)(0x25))
#define DDRB (*(volatile uint8_t *)(0x24))
#define PINB (*(volatile uint8_t *)(0x23))
This is the starting point for any program7.
Figure 4: Video illustrating the setting up of the data direction registers.
Figure 5: Video illustrating the setting of the port D for input and resetting port B
The infinite for loop is quite a common idiom in C:
Any code that is placed inside the for loop will run forever.
Figure 6: Video showing how the state of the buttons is interogated using bit masks.
Similar code is used to test the left button on PORTD
pin 3 to illuminate the green LED on PORTB
pin 1.
See if you can write this code.
The full program is available as a GitHub gist: main.c. You will need a fully featured IDE, such as Microchip Studio, to compile and upload the code to the Ardino nano board.
Wokwi is an online Electronics simulator. You can use it to simulate Arduino, ESP32, STM32, and many other popular boards, parts and sensors. – Welcome to Wokwi!
My 2024-2025 EG-353 Individual Engineering Project student, Yousef Alsayegh, has created Wokwi simulations of Ben Clifford’s demonstration programs. Here is the simulation of this week’s simulation Week 5: Interfacing to digital I/O with C. You can run the simulation and play with the code.
Draw a flow-chart for the full program. Use subprocesses for the init()
and loop()
blocks.
In this section we have:
This week on the canvas course page, you will find the sample programs from today’s lecture.
Look through these and ensure you are confident in how they work and how the masks are defined.
There is also a short quiz to test your knowledge on these topics.
Please use the Course Question Board on Canvas or take advantage of the lecturers’ office hours.
Arduino.h
and wiring_digital.c
are hidden away in the installation folders for the Arduino IDE. The examples versions shown here are taken from the GitHub repository github.com/arduino/ArduinoCore-avr.
To see the full contents see: Arduino.h on GitHub.
In the case of I/O specific bits refers to individual I/O pins.
Pins D8
and D9
are respectively bits 0 and 1 of Port B.
Idiomatic C often uses the shortcut PORTB |= 0x00000010;
There are many such assignment operators in C. For example: +=
, -=
, &=
etc. They all mean the same thing: var = var op argument
. Internally, the C compiler treats both forms the same so their use is a matter of style. The full version is easier to read and understand. The shortcut is quicker to type, but arguably less easy to read and understand.
In this definition we use the qualifier volatile to inform the compiler that the variable value can be changed any time without any task given by the source code. Without this qualifier, depending on the optimisation level of the compiler this may result in code that doesn’t work as it doesn’t know the value can be changed by external sources. The asterisk symbol is used to denote a pointer, for now you do not need to know what this explicitly mean but in short a pointer is a variable whose value is the address of another variable, i.e., direct address of the memory location rather than a value.
The actual code is slightly different and if you are interested you can access it here: main.cpp. Note that the sketch function setup()
is called once first, then loop()
is called each time through the infinite loop.
The actual code is slightly different and if you are interested you can access it here: main.cpp. Note that the sketch function setup()
is called once first, then loop()
is called each time through the infinite loop.