AVR Coding Part 2: Digital Out

AVR microcontrollers have numerous input and output (I/O) capabilities.  Its internal I/O registers serve as bridges between hardware and software.

In the C programming language, these I/O registers resemble variables whose names and locations in memory are predetermined.  Today’s project will use a GNU Compiler Collection (GCC) application to control digital outputs.

Notice of Non-Affiliation and Disclaimer: As of the publication date, we are not affiliated with, associated with, authorized with, endorsed by, compensated by, or in any way officially connected with Microchip Technology Inc., or their owners, subsidiaries or affiliates.  The names Microchip Technology Inc., Atmel, AVR, as well as related names, marks, emblems, and images are trademarks of their respective owners.

External Links: Links to external web pages have been provided as a convenience and for informational purposes only. Unboxing Tomorrow and Voxidyne Media bear no responsibility for the accuracy, legality or content of the external site or for that of subsequent links. Contact the external site for answers to questions regarding its content.

Objectives

This project will demonstrate one way to blink small LEDs using the general-purpose I/O registers.  Large LEDs that are beyond the absolute maximum ratings of an AVR are beyond the scope of this guide, but they will likely be covered later.  Today’s objectives are to:

  • Identify the different ways AVR pins are numbered
  • Blink the LED using I/O registers
  • Adjust the blink rate using the delay.h library

The “Bad Practices to Avoid” section at the end will cover a risky coding pattern to avoid.

Hardware Resources

This guide will work with most 8-bit AVR microcontrollers with some adjustment to the code.  Today’s project will use a ATTiny2313:

  • ATTINY2313-20 (other AVRs work too)
  • Resistor, 1 kΩ
  • Resistor, 30 kΩ
  • Capacitor, 100 nF
  • Capacitor, 1uF
  • LED, Green, 5mm Through-hole
  • Fixed voltage supply, 5V
  • AVR programming tool
  • Solderless breadboard
  • Mixed jumper wires

Software Resources

  • Microchip Studio (Version: 7.0.2542)
  • AVR GCC C Compiler

Step 1: Identify the Pin Numbering

There are 2 pin numbering systems used by the device and related documentation…

  • Package-level pin numbering assigns a unique number to each metallic connection on the device.  Pin 1 may be marked with a dot.  Packages such as DIP, SOIC, SSOP, TQFP, etc. start with pin 1 and count upward in a counter-clockwise direction (when looking down at the device).
  • I/O Register pin numbering groups the pins into ports and then numbers them accordingly.  Ports are lettered alphabetically (Port A, Port B, Port C, etc.).  The pins within them count upward from zero.

This guide will use a Port-Pin combination when referring to I/O registers.  For example: on the ATTINY2313-20PU depicted in Figure 1 below, “pin 12” and “PORTB0” refer to the same pin.

Figure 1: Schematic Symbol for the ATTiny2313-20PU Microcontroller

Step 2: Select an LED-Resistor Pair

This project will add a 1 kΩ resistor in series with the LED.  This keeps the LED current well below the absolute max ratings of both the LED and the AVR.  For more details, refer to the manufacturer’s documents, or reference: LED-Resistor Pairing Tutorial (Unboxing Tomorrow).

Step 3: Assign the LED to a Pin

This project will assign the LED to PORTB0 (i.e. the 0th pin/bit of register PORTB).

Figure 2: Digital Out Test Project

The microcontroller here connects to the LED through the LED anode pin, so it will only illuminate when the microcontroller transmits a high state.  Alternatively, the arrangement in Figure 3 below will illuminate the LED when the pin transmits a low state.

Figure 3: Alternative Digital Out Test Circuit

In the ATTiny2313 datasheet, section “Register Description for I/O-Ports“ says this pin and others like it will initialize in input mode after a power reset.  Therefore, the first instructions of today’s program will:

  • Set the pin direction to output
  • (Optional) Clear the pin output state to binary zero

Step 4: Create a New Project

Open Microchip Studio.

Create a new GCC C Project by navigating to File –> New –> Project.  Select a project name and an AVR device.

Each Port-Pin combination on the device is associated with three I/O registers.  Here, x represents the port letter and n represents the bit number…

  • Data Direction Register (DDRxn) controls whether a pin is an input or an output
  • Port (PORTxn) controls output pins
  • Pin (PINxn) reads input and output pins

These registers are 8 bits in length.  To change 1 bit at a time, the program will use bitwise operators

Table 1: Bitwise Operators

OPERATORDESCRIPTION
<< Bitwise shift left
~One’s complement (invert all bits)
|=Bitwise OR assignment
&=Bitwise AND assignment

The entire code to blink the LED is…

#include <avr/io.h>     // Part-specific I/O definitions
#define F_CPU 1000000UL // Clock frequency (unit: Hz)
#include <util/delay.h> // Includes _delay_ms and _delay_us

int main(void)
{	
       DDRB |= (1<<0);   
       PORTB &= ~(1<<0); // Turn off the LED (write 0 to PortB0)
    while (1) 
    {
              PORTB |= (1<<0);  // Will connect the pin to Vcc
              _delay_ms(250);   // Hold for 250 ms
              PORTB &= ~(1<<0); // Will connect the pin to ground
              _delay_ms(250);   // Hold for 250 ms
    }
}

Code Explanation

Initializing Pins

First, the following 2 lines were added at the start of the main function.  Their purpose is to initialize the direction and the early state of PORTB0. 

DDRB |= (1<<0);
PORTB &= ~(1<<0);

The second line above is redundant, since the pin would have been cleared low by default anyway, but it is good to be explicit.

Blinking the LED Endlessly

Second, two instructions were added for blinking the LED.  The first line sets the 0th pin in PORTB high by writing a “1” to the 0th bit.  All other bits in PORTB remain unchanged.

The second line below clears the 0th pin in PORTB by writing a “0” to the 0th bit.

PORTB |= (1<<0);  // Set PORTB0 high (will connect the output to Vcc)
PORTB &= ~(1<<0); // Clear PORTB0 low (will connect the pin to GND)

Adding Blink Delays

Uploading at this point would blink the LED thousands of times per second.  A millisecond-scale delay between on/off instructions will reduce the blink rate to a frequency human eyes can easily resolve.

The AVR GCC C library includes a header file “delay.h” and a macro called _delay_ms().  To use the library, the following was added above the main() function.

#define <util/delay.h>

Above that line, there needed to be a definition for the central processing unit (CPU) frequency.  Otherwise, delay.h will throw a warning due to not knowing the CPU frequency (F_CPU).  Specify the F_CPU in Hz.  For example, 1 MHz required…

#define F_CPU 1000000UL

…where the “UL” ensures the compiler will evaluate this as an unsigned long integer.

Inline delays

Finally, the following line was added after each PORTB command in the while loop.

_delay_ms(250);

According to the delay.h documentation, this delay must be a known constant at compile-time, and compiler optimizations must be enabled (which is the default for Microchip Studio).

Step 5: Compile and Upload

Compile and the code and upload the executable per AVR Coding Part 1: Microchip Studio.  Assuming F_CPU is accurate, the LED should blink about twice per second.

Code Variations

Controlling More Pins

More pins of PORTB could be controlled in this project.  As an example, here is a loop to blink PORTB pins 0 through pin 3…

#include <avr/io.h>     // Part-specific I/O definitions
#define F_CPU 1000000UL // Clock frequency (unit: Hz)
#include <util/delay.h> // Includes _delay_ms and _delay_us

int main(void)
{	
    DDRB |= (1<<3)+(1<<2)+(1<<1)+(1<<0);     // Set pins 0 ~ 3 as output
    PORTB &= ~((1<<3)+(1<<2)+(1<<1)+(1<<0)); // Turn off the LEDs
    while (1) 
    {
              // Blink pin 0 through pin 3
              PORTB |= (1<<3)+(1<<2)+(1<<1)+(1<<0);
              _delay_ms(250);
              PORTB &= ~((1<<3)+(1<<2)+(1<<1)+(1<<0));
              _delay_ms(250);
    }
}

Note that using multiple pins in tandem to avoid the absolute max rating of single pins is NOT  a recommended practice.

Longer delays

The documentation for delay.h states the following…

“delay_ms: The maximal possible delay is 262.14 ms / F_CPU in MHz.   When the user request delay which exceed the maximum possible one, _delay_ms() provides a decreased resolution functionality. In this mode _delay_ms() will work with a resolution of 1/10 ms, providing delays up to 6.5535 seconds (independent from CPU frequency). The user will not be informed about decreased resolution.”

To get even longer delays from this method, consider placing it inside a loop.

Shorter delays

The delay.h header also supports a _delay_us function for microsecond-level delays.

“delay_us: The maximal possible delay is 768 us / F_CPU in MHz.”

More Precision

The delay.h functions are included as a basic convenience and their timing isn’t exact.  For higher precision, there are other options outside the scope of this project, such as:

  • Adding NOP (no operation) commands as inline assembly
  • AVR internal timers (if supported)
  • AVR internal real-time clocks (if supported)

Ultimately, the AVR’s own timing can only be as accurate as its clock source.

Bad Practices to Avoid

Consider the following example, which attempts to configure two registers on the same line of code…

DDRB = DDRD = 0xFF;

According to the rules of the C language, this causes the following sequence:

  1. Assign value 0xFF to DDRD
  2. Read back the value of DDRD
  3. Assign the value from step 2 to DDRB

This is a bad practice because I/O registers may be considered “volatile” by the compiler and therefore the redundant 2nd step might not get removed.

Worse still: some bits within DDRx may be read-only.  This is exactly the case with ATTINY2313, where Port D pin 7 doesn’t exist and therefore the unused 7th bit of DDRD returns “0” no matter what.  This could get carried over from step 2 and step 3 above, and thereby write 0x7F to DDRB instead of the intended 0xFF.

When I tested whether this would actually happen, DDRB did contain the correct value of 0xFF.  This suggests the code optimization had worked anyway.

Figure 4: Disassembled Version of the “DDRB = DDRD = 0xFF” Assignment

Still, the one assignment per line approach is safer, portable, and more readable…

DDRB = 0xFF;

DDRD = 0xFF;

References

[1]Atmel Corporation, “8-bit Microcontroller with 2K Bytes In-System Programmable Flash Rev. 2543M,” Microchip Technology, Oct. 2016. [Online]. Available: http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-2543-AVR-ATtiny2313_Datasheet.pdf. [Accessed 17 July 2020].
[2]B. R. Mayes, “LED-Resistor Pairing Tutorial,” Voxidyne Media LLC, 29 Dec 2021. [Online]. Available: https://unboxing-tomorrow.com/led-resistor-pairing-tutorial/. [Accessed 3 Jan 2022].
[3]“Configuring the I/O Digital Pins,” Microchip Technology In.c, 2021. [Online]. Available: https://microchipdeveloper.com/8avr:ioports. [Accessed 3 Jan 2022].
[4]“delay.h,” avr-libc, 8 Feb 2016. [Online]. Available: http://avr-libc.nongnu.org/user-manual/delay_8h_source.html. [Accessed 3 Jan 2022].
[5]AVR Lib C, “FAQ Index,” 12 Aug 2014. [Online]. Available: http://avr-libc.nongnu.org/user-manual/FAQ.html. [Accessed 3 Jan 2022].

Important Notice: This article and its contents (the “Information”) belong to Unboxing-tomorrow.com and Voxidyne Media LLC. No license is granted for the use of it other than for information purposes. No license of any intellectual property rights is granted.  The Information is subject to change without notice. The Information supplied is believed to be accurate, but Voxidyne Media LLC assumes no responsibility for its accuracy or completeness, any error in or omission from it or for any use made of it.  Liability for loss or damage resulting from any reliance on the Information or use of it (including liability resulting from negligence or where Voxidyne Media LLC was aware of the possibility of such loss or damage arising) is excluded.