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.
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.
For GPIO functionality, we’ll want to check section 16 of the datasheet PORT - I/O Pin Configuration.
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.
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
byte currentValue = 0b00000011
byte updatedValue = currentValue | 0b00000100 // -> 0b00000111
0b00000111
back to
the registerThis 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
DIR == 0b00100001
-> write
DIRSET = 0b10001000
->
DIR == 0b10101001
DIR == 0b00001111
-> write
DIRSET = 0b00000110
->
DIR == 0b00001111
DIR == 0b00001111
->
DIRSET = 0b00000000
->
DIR == 0b00001111
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.
byte currentValue = 0b00000011
byte updatedValue = currentValue & 0b11111101 // -> 0b00000001
0b00000001
back to
the direction registerThis 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
DIR == 0b00001111
-> write
DIRCLR = 0b00000110
->
DIR == 0b00001001
DIR == 0b00001111
-> write
DIRCLR = 0b00111100
->
DIR == 0b00000011
DIR == 0b00001111
-> write
DIRCLR = 0b00000000
->
DIR == 0b00001111
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
byte currentValue = 0b00110101
byte updatedValue = currentValue ^ 0b00011000
0b00101101
back to
the registerWith the direction toggle register available such operations require only a single write.
DIR == 0b00001111
-> write
DIRTGL = 0b00011000
-> DIR == 0b00010111
(see bits 3 and 4)DIR == 0b01010000
-> write
DIRTGL = 0b11001111
-> DIR == 0b10011111
(see bits 4 and 5)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”.
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.
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.