GPIO Pin Initialisation and Control - Theory I - Input or Output, On or Off

Introduction

Arduino boards, when used in the Arduino ecosystem, and many microcontrollers with helpful libraries, like the ATMEGA4809, automagically take of a lot of the complexity of initialising the IC at startup time and even setting and reading the state of those pins.

In this article we will aim to work out how to configure the simpler aspects of GPIO pin setups on the ATMEGA4809 and use those pins for I/O without relying on the MCC provided by Microchip.

Why?

GPIO setup is a critical aspect of working with microcontrollers. It’s hard to think of any program, however simple, in which GPIO pins do not need to be configured with directionality or the states of those pins do not need to be changed or queried. Blink(y) is the starting point for many new hardware projects. Such a simple & ubiquitous application already requires setting up output pins and changing their state. The next step up, switching an LED based when a button is pressed, also required setting up input pins and reading their state.

So when working with any IC we will need to know how to do this setup, and we won’t always have helpful libraries to depend upon, for example if we are working with uncommon or cutting edge ICs. In such cases it is vital to know how to control GPIO pins ourselves.

RTFDS

For GPIO functionality, we’ll want to check section 16 of the datasheet PORT - I/O Pin Configuration.

Primer

Before diving into the registers associated with the ports, the overview section 16.2 introduces a few key concepts.

All I/O pins are connected to ports. These are the ports we have already seen in the Pinout (section 3) and the I/O multiplexing table (section 4). Here we saw that there are PORTA, PORTB, …, up to PORTF (i.e. 6 ports).

Each port can have up to pins associated with it. We have seen this in those same sections, and we have also seen that not every port has the full 8 pins associated with it. For example in section 3.3, showing the DIP40 pinout, we can see that port C contains only 6 pins.

Pins are commonly refered to via their port number and pin number within that port, for example PORTD’s pin 3 can be referred to as PD3. Note that pin 3 is actually the fourth pin because the pins are counted from 0. This is deliberately chosen to match with how bits are counted in bytes, because each pin in a port will be controlled by the corresponding bits in the configuration registers for that port. For example in the whole-port configuration registers for PORTD the bit 3 (marked X 0b0000X000, remember that the least significant/rightmost bit is 0 and the most significant/leftmost bit is 7) controls the configuration of PD3.

Registers

There are a few more explanations of advanced features that follow, but let’s skip straight to section 16.4 where the registers are described, since this will contain all of the information that we need about basic configuration.

Note that each port has a set of 19 (!) registers that can be configured. 11 of these I refer to as “whole port” registers, i.e. each bit in those registers sets a certain configuration item for the pin in the port matching its position in the byte (like PD3 explained in the previous section). A further 8 (of the form PINXCTRL where X is a number from 0 to 7) contain various configuration settings each for individual pins, i.e. the pin with number matching the number in the port name. So for example PIN3CTRL in the register block for PORTD controls settings only for pin PD3.

DIR

This register sounds simple. “Data Direction” should control whether the pin is an input or an output, right? This is mostly correct as noted in the datasheet. Specifically it says that setting a bit to 1 in this register enables the pin for output, i.e. the pin can be driven high by the microcontroller. Setting it to 0 means that the microcontroller cannot drive the pin high.

The small “catch” is that setting the bit to 1 in this register does not disable input! This is also explicitly mentioned in the datasheet. So (I guess) if we don’t separately disable the pin input, in output mode we will still be able to read the pin input, which will potentially be set high if we have driven the pin high from within the microcontroller. This sounds like it would enable us to do some cool feedback/sensing, such as detect whether a pin can or cannot be brought high (something like this is used in multi-master I2C busses).

As a concrete example, let’s suppose we want to be able to drive pins 3 and 6 of a certain port high but don’t need to do this for the rest of the pins on that bus. We would set the DIR register for that port to 0b01001000 (1s for bits 3 and 6, 0s elsewhere).

DIRSET, DIRCLR, DIRTGL

Along with the direction register (DIR) the ATMEGA4809 provides these three convenience registers.

When writing a value to the direction register all 8 bits in that register will be updated with the corresponding values of the byte that is written to the register. To understand why this might be problematic, consider the following scenario - we need to change the configuration of pin number 2 only such that it is switched from an input-only pin to an output pin. How do we achieve this?

Should we write 0b00000100 to the port’s direction register, which will make pin 2 output capable? What if pins 0 and 1 are being used for output and they need to continue to be used for output along with pin 2? This byte value in the direction register will prevent pins 0 and 1 from providing outputs and will therefore introduce a bug in out program.

The less convenient way to update a configuration value for a specific pin is to

This is not terribly onerous, though it makes a single bit update take three code lines & potentially multiple instructions and thus could lead to excessive “boilerplate” code and less performance than would be ideal.

DIRSET, DIRCLR, DIRTGL are registers that enable convenient shortcuts for these kinds of operations.

DIRSET

The direction set register can specifically “set” the direction as an output only. When a 1 is written to any bit in this register, the corresponding bit in the direction register is set to 1 regardless of its current value. When a 0 is written to a bit in this register the value of the corresponding bit in the direction register is left unchanged.

Without this facility, the cumbersome way to achieve a similar update is as explained in the previous section. To revisit that example, let’s suppose we need to update pin 2 in this port to be output capable. Now we can just write 0b00000100 to the direction set register. Before that write the direction register would have value 0b00000011, and after the write 0b00000111.

Here’s a couple more examples to illustrate how the direction set register works

DIRCLR

Direction clear is more-or-less the opposite to the direction set register.

Let’s return to the example two sections ago, assume that the direction register is in some state, but this time we want to stop the 1st bit from acting as an output. We need to set only bit 1 in that register to 0 - obviously we cannot blindly write 0b00000000 to the register for similar reasons as explained before. We would need to follow a similar read-modify-write cycle.

This read-modify-write cycle can be reduced to a single write operation with the direction clear register. Any bits which are set to 1 in the byte written to the direction clear register will be set to 0 in the direction register regardless of their previous values. This is best illustrated again by a few examples

DIRTGL

I think, given what we know about the previous two registers, the name of this register should now immediately give away its function. Direction toggle is used to flip the state of the direction register for specific pins without affecting the state of the register for other pins. That is to say writing a 1 to a bit in this register will cause the corresponding bit in the direction register to be set to 1 if it is currently 0, or to be set to 0 if it is currently 1.

Had we only access to the direction register we would have to achieve this in the following way

With the direction toggle register available such operations require only a single write.

OUT

For any given port, the output value register controls whether the pin is driven high or not driven high if the pin is configured in the direction register as output capable. Specifically each bit in this register sets the output state - 0 for high off (~low), 1 for high on - of the corresponding pin in the port. For example in PORTD’s output value register, bit 3 controls whether pin PD3 is being driven high if it is configured as output capable.

As noted in the datasheet, the values in this register will have no effect for pins which are not set as output capable. So with a pin connected to nothing else, this will be its state given the value of its bits in the direction and output value registers.

/-----------------------\
| DIR | OUT | PIN STATE |
|-----|-----|-----------|
|  0  |  0  |    LOW    |
|  1  |  0  |    LOW    |
|  0  |  1  |    LOW    |
|  1  |  1  |    HIGH   |
\-----------------------/

Or, more straightforwardly, taking 0 == LOW and 1 == HIGH, the pin’s state will be (DIR & (1 << n)) & (OUT & (1 << n)) where n is the number of the pin in the port.

For a concrete example let’s suppose we have set all pins in PORT D as output capable, and now we want to put pins 2 and 6 in PORTD only in the high state (all other pins will not be driven). We would write to the OUT register a byte with only the bits 2 and 6 set to 1, like SET = 0b01000100.

OUTSET, OUTCLR, OUTTGL

Setting, clearing and toggling individual bits directly on the output value register naturally comes with the same complexity and performance penalties as discussed for the direction register. Similarly the ATMEGA4809 has set, clear and toggle registers for the output state of each port to allow these operations to be done in a single write.

The OUTSET, OUTCLR and OUTTGL registers affect the OUT register in exactly the same was as the DIRSET, DIRCLR and DIRTGL registers affect the DIR register. So to understand how they work, consult the previous three sections and substitute everywhere DIR by OUT and “directio” with “output value”.

Typical Usage of DIR and OUT

The direction and output registers are likely to be used in slightly different ways in programs that we write, more specifically they are likely to be used mostly at different points of the program.

The direction register is likely to be used primarily at the start of the program when all of the setup code is being run, to set up which pins are inputs and which are outputs. Typically once we have implemented a chip in some application, we will have chosen which pins will do what/will be connected to what, and this will not change very often. Those pins will then usually continue to function as inputs or outputs only throughout the life of the program or even the life of the device.

The application code is likely to write to the output value register through its entirety, and much more often than the direction register. Whenever a user controlled GPIO output is toggled between on and off, the output value register will be changed. For example toggling LEDs, sending other signals manually or bit-twiddling software implementations of communication protocols.

IN

As made clear by the name of the register, Input Value, this is used to query the current state of the pin. In accordance with all other registers above, the value of each bit n in IN corresponds to the current state of pin n in the port to which the register belongs. Explicitly, e.g. bit 3 of PORTD.IN gives the input state of pin PD3. A value of 0 means the state is low and a value of 1 means that the state is high.

Since this register is an input and we generally won’t write to it, there isn’t much else interesting to say about it. Reading from it is a simple as it sounds, i.e. byte states = PORTx.IN.

One curious aspect mentioned in the data sheet is

Writing to a bit of PORTx.IN will toggle the corresponding bit in PORTx.OUT

I assume this implies “writing 1 to a bit of …”. It sounds like Input Value register can act like the OUTTGL register, for some reason! A curious choice, but as reasonable a default behaviour as any other.

A final snippet to note from the datasheet

If the digital input buffers are disabled, the input is not sampled and cannot be read.

This relates to another item of pin configuration which will be discussed in a future article.

Practical

In a future article, we will take this information from the datasheet and use it to configure & reconfigure some pins on the ATMEGA4809, change their output states and read back their input states.

Other articles will show us about more advanced items of pin configuration & state (interrupts, input sensing), which will have an impact on some of the configuration items that we looked at above.