Ports as general digital I/O
Sometimes it is a bit difficult to understand the meaning of the most common terms we come across when reading documentation about microcontrollers. When reading about the general purpose input-output module, the terms port and pin often clash with our intuition of what these terms mean outside the technical meaning they assume.
In any microcontroller, pins are what the name suggests, the pins that stick out of a development board, which connects it electrically to the outside world through sensors and actuators, and are shown on the board pinout in schematics and diagrams.
A microcontroller communicates with the outside world by either setting the voltage on a pin high (5 volts) or low (zero volts), or by reading the TTL-voltage level of an input pin as being either high (1) or low (0). So probing a multimeter or an oscilloscope in any of these pins will give us information about its voltage levels.
If we count the pins in an Arduino Nano board, there are 30 of them. They are labelled differently. One is 5V, another one is GND, and these are easy to understand. 5V must be a pin that provides a constant 5V, GND must be…ground, for sure. But most of them are labelled as A0, A1, or D0, D1, D2. What are they? Here is where the concept of ports come.
Ports are a complex analogue circuit inside a microcontroller, that encompasses registers and other electronic components, which allow the software to control the state of the pins, or conversely, read the state of the pins if they are configured as inputs. If we can read or write to these ports, it means that they serve as either inputs or outputs for the microcontroller. This is where the name IO port comes.
Each of these ports is directly connected to a hardware register that is memory mapped into RAM address space. As a result all IO ports are memory mapped. The actual addresses for these IO ports are associated with logical names such as PORTB, PORTC, and PORTD for the case of the ATMega328P. Other microcontrollers with more ports can have names as PORTA all up to PORTG.
The ATmega328P has 23 General Purpose Digital I/O Pins assigned to 3 GPIO Ports, where Ports B and D are 8-bit and and Port C is 7-bit long. Summing 8+8+7 gives the 23 general purpose IO referred. As each port receives a name (PORTC, for example), and there are 8 bits in PORTC, we are presented with things as PORTC0, PORTC1, PORTC2, until PORTC7.
The same happens with PORTB0 up to PORTB7 and PORTD0 all the way up to PORTD6. To make things more generalist, we usually write the alias PORTxn, where x can assume the letters B, C or D and n is the number of the port, from 0 to 7 or 8.
Therefore, there is a one-to-one correspondence between the pins on the microcontroller and the bits in its registers. One pin that comes out of the microcontroller board is PORTC0, its neighbour pin can be PORTC1, and so one. In the end, we have these 23 pins + several others like 5V, GND, Aref, summing the 30 we counted before.
In the particular case of any Arduino board, the names PORTxn were replaced by the nomenclature A0..A7 for analogue pins and D0 to D10 for digital pins. But this is just an easy name that maps to the standard names given to the microcontroller pins.
Each I/O port pin may be configured as an output with symmetrical drive characteristics. Each pin driver is strong enough to drive LED displays directly, supplying a maximum current of 20 mA.
The directional states input and output are mutually exclusive, meaning a pin cannot be used for input and output at the same time, having thus a distinct direction state at a given time. In other words, once you’ve decided to output a voltage on a certain pin, you cannot try to read that voltage. You must reset the direction state of that pin before attempting to read the pin’s logical state.
In the ATMega 328P datasheet, right at the beginning of the I/O Ports section, it is stated that all AVR ports have true Read-Modify-Write functionality when used as general digital I/O ports. This means that the direction of one port pin can be changed without unintentionally changing the direction of any other pin.
The same applies when changing the drive value if the port is configured as an output, or enabling/disabling of pull-up resistors if the port is configured as an input.
Speaking about pull-up resistors, all port pins in a 328P microcontroller have individually selectable pull-up resistors with a supply-voltage invariant resistance. This feature is interesting as we can connect an LED, a pushbutton or any other device without the need of an external resistor, and we can also exercise our abilities in writing code using the correct registers to use these internal pullup resistors. I will show a small code example to do this in a later article.
Among all registers a microcontroller might have, independently of its vendor, we must expect at least the two most basic ones to be present: a data register, which holds the data we are reading from or writing to the port, and a direction register, which must define if the port is an input or an output. Naturally, these registers have different names according to each vendor.
In the case of Atmel’s ATMega328P, each port pin consists of three register bits: DDRxn, PORTxn and PINxn. I/O memory addresses locations are allocated for each pin port.
The data register is called PORTx and the direction Register is called DDRx.
They are both bidirectional read/write ports with internal pull-ups, and the subscript x refers to a specific number they can assume. For example, for pin 1, its correspondent number for the data register is PORT1 and its direction register is DDR1.
Each bit in the DDR register defines the direction of the pin: it can be either an input or an output. If DDRxn is written logic one, PINxn is configured as an output pin. If DDRxn is written logic zero, PINxn is configured as an input pin. This register needs to be defined right at the beginning of the code that deals with IOs.
The data register PORT receives either a logic zero or a logic one, according to the functionality desired by the software. If PORTxn is written logic one when the pin is configured as an output pin, the port pin is driven high. If PORTxn is written logic zero when the pin is configured as an output pin, the port pin is driven low.
It can be naturally observed that there is a close relationship between these two registers. A programmer cannot manipulate the data register of a port without defining it as an input or an output. If PORTxn is written logic one when the pin is configured as an input pin by the DDR register, the pull-up resistor is activated. To switch the pull-up resistor off, PORTxn has to be written logic zero or the pin has to be configured as an output pin by the DDR register. The port pins are tri-stated when reset condition becomes active, even if no clocks are running.
For the 328P, an additional register is also used along with PORTx and DDRx, the Port Input Pins – PINx. This register is read-only, and it works the following way: writing a logic one to a bit of this register toggles the corresponding bit in the data register PORTx, independent on the value of DDRxn.
Independent of the setting of Data Direction bit DDRxn, the port pin can be read through the PINxn Register bit.
Code example
This is an example of how these registers are used in a software written in C. This code toggles three LEDs, one red, one yellow and one green, in a 1 second interval, the way a traffic light would work in a real life example.
The microcontroller used here is the ATMega328P, and the board is the Arduino Nano, but the code also runs in the Arduino Uno, as both boards use the same microcontroller.
#include "avr/io.h"
#include "util/delay.h"
#define GREENLED 4
#define YELLOWLED 3
#define REDLED 2
int main()
{
DDRD |= (1 << YELLOWLED); //set D3, PD3 yellow led as output
DDRD |= (1 << GREENLED); //D4, PD4, green led as output
DDRD |= (1 << REDLED); //D2, PD2, red led as output
while(1)
{
PORTD |= (1 << REDLED);
PORTD &= ~(1 << YELLOWLED); //PORTD &= ~(0x08);
PORTD &= ~(1 << GREENLED);
_delay_ms(1000);
PORTD |= (1 << YELLOWLED); //PORTD |= 0x08;
PORTD &= ~(1 << GREENLED);
PORTD &= ~(1 << REDLED);
_delay_ms(1000);
PORTD |= (1 << GREENLED);
PORTD &= ~(1 << YELLOWLED); //PORTD &= ~(0x08);
PORTD &= ~(1 << REDLED);
_delay_ms(1000);
}
}
The first #defines define the port numbers each LED is attached to:
– The green LED is attached to pin D4, which maps to the PD4 pin in the microcontroller.
– The yellow LED is attached to pin D3, which maps to the PD3 pin in the microcontroller.
– The red LED is attached to pin D2, which maps to the PD2 pin in the microcontroller.
The first three lines in main use the DDR register to set the pins as outputs.
Notice that, as the yellow Led is connected to pin 3, I need to write the value one the third bit of the port. This is why the notation PORTD |= 0x08 is used.
Then inside the while loop, the PORTD register is used to write to the pin.
A 1 second delay is used to set the tone and can be adjusted to other intervals.
The code is simple and there was no need to use the PIN registers to perform the action we wanted. But if the reader wants more examples I can bring them in further articles.
That is all for this topic. Please do let me know if you want more examples or clarification.
References
https://electronics.stackexchange.com/questions/134605/pin-and-port-in-microcontroller
https://controls.ame.nd.edu/microcontroller/main/node17.html
https://docs.arduino.cc/hardware/nano
ATMega328 Datahseet