AVR Coding Part 3: Digital Input

AVR Microcontroller and solderless breadboard prototype
AVR Microcontroller and solderless breadboard prototype

Digital inputs bridge real-world signals with their digital representations.  Many (but not all) digital devices communicate with each other using collections of high or low voltages.  To prevent miscommunication, logic level specifications provide rules for how voltage gets digitized into 0’s and 1’s.  Today’s project will explore these signals using AVR microcontrollers.

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 Texas Instruments, or their owners, subsidiaries or affiliates.  The names Microchip Technology Inc., Atmel, AVR, Texas Instruments, 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

Previously, this blog series introduced input and output registers (I/O registers).  In the C language, these I/O registers behave almost like variables whose names are reserved.  Part 2 covered how to set up an AVR pin as a digital output. 

Today’s project (Part 3) will cover both digital inputs and digital outputs.  The objective is to use a momentary pushbutton switch to control an LED.  The objective will resemble a LED controlled by a latching pushbutton.  That’s to say: the switch and LED will have a push-on, push-off relationship.  This will require…

  • Designing the physical circuit
  • Reading digital inputs in the program
  • Controlling program flow using a variable and if statements

The final section will discuss pull resistors and options for unused pins.

Hardware Resources

This project was powered by a 5-volt power supply (not shown in the schematics below).  Adjust your positive supply (VCC) according to your project’s needs and your specific microcontroller.

Table 1: List of Materials

DESCRIPTIONIDVALUENOTE
ResistorR130k 
ResistorR21k 
ResistorR310k 
CapacitorC11uF 
CapacitorC2100nFRecommended type: ceramic
CapacitorC3100nFRecommended type: ceramic
PushbuttonSW1 Momentary, normally open tactile switch
DiodeD1Green LED3 mm LED with VF close to 2.2 volts
MicrocontrollerU1Attiny2313ATiny2313-20PU is a through-hole device
ConnectorJ1  
AVR ToolUsed here: AVR ISP mkII, AVR ICE
Solderless Breadboard   
Table 1: List of Materials

Software Resources

  • Microchip Studio (version 7.0.2542 is used here)

Step 1: Retrieve the AVR Datasheet

A digital input has a limited range of voltages it can identify as a 1 or 0.  For single-ended inputs like the one being used today, a digital input will measure an incoming voltage relative to a ground (GND) pin, which acts as a 0-volt reference.  This document will distinguish reference grounds from other ground types.

The digital input will classify a voltage as “low” (or 0) if it falls below a threshold named: input low voltage (VIL).  A signal is classified “high” (or 1) if it rises above an upper threshold named: input high voltage (VIH).  The positive supply voltage (VCC) is as high as an input should go.

Digital outputs have an output low voltage (VOL) and output high voltage (VOH).  These figures refer to the lowest and highest stable voltages a device can send under normal conditions.

In a well-designed input-output pair: VOL < VIL AND VOH > VIH.  These upper and lower ranges are effectively the two logic levels of the device.  Any input-output pair must match each other’s logic levels.  Otherwise, the input device may misidentify a 0 or a 1, or vice versa.

An AVR’s datasheet will specify its logic levels as part of its “DC characteristics.”

For more details on logic levels, Texas Instruments has a free white paper titled: “Logic Guide.”

Step 2: Create a New Circuit

A mechanical pushbutton produces no voltage.  It therefore depends on an external pull resistor to carry 2 distinct voltages.  This project will assign the pushbutton to PORTB1 and the LED to PORTB0…

The pushbutton will be wired such that pressing it will connect PORTB1 to GND.  Pull-up resistor R3 provides a connection to the collector-collector voltage (VCC) when the button is released.

Figure 1: Digital Input Test Schematic

Alternatively, the switch and R3 may be swapped, with R3 acting as a pull-down resistor.  This project will only address the pull-up version shown in Figure 1 above.

Figure 2: Alternate Digital Input Test Schematic

Mechanical switch contacts can generate brief bursts of electrical noise as they make and break physical contact.  This noise can fool software into over-reporting the number of button presses.

Capacitor C3 was added in Figure 1 and Figure 2 to work as a noise filter.  Its connection with resistor R3 forms a resistor-capacitor filter (or RC low-pass filter), which passes low frequencies and rejects switch noise.

Step 3: Create a New Program

Create a new Microchip Studio project:

  • Navigate to: File –> New –> Project.
  • Select “GCC C Executable Project”
  • Make up a name for the project
  • Select your AVR device

First Draft Code

The first draft code to blink the LED goes as follows…

#include <avr/io.h>
#define F_CPU 1000000UL  // Define the clock frequency in Hz
#include <util/delay.h>  // Used by _delay_ms()

#define OUTPUT_LED (0)         // Pin B.0...0 = LED off, LED on
#define INPUT_SWITCH (1)       // Pin B.1...0 = Switch pressed, 1 = Switch not pressed
#define HOLD_DURATION_ms (100) // Wait this long after toggling the LED

int main(void)
{
// Setup...
	uint8_t inputState = 0;  // Variable to store input pin states
	DDRB = (1<<OUTPUT_LED);  // Set LED pin to output
	// While the AVR is powered on...
    while (1) 
    {
		// Read the pushbutton state.  Ignore all other inputs
		inputState = PINB;
		inputState = inputState & (1<<INPUT_SWITCH);
		// If the switch is pressed, invert the LED, then pause
		if (inputState == 0)
		{
			PORTB ^= (1<<OUTPUT_LED);
			_delay_ms(100);
		}
    }
}

Code Explanation

First, this project made all the same declarations as the earlier Part 2 Digital Out project…

#include <avr/io.h>
#define F_CPU 1000000UL  // Define the clock frequency in Hz
#include <util/delay.h>  // Used by _delay_ms()

As a good practice, the source code assigns meaningful names to constants and variables.  As a rule, this blog series will use UPPERCASE for constants, except where standardized units of measure appear.

#define OUTPUT_LED (0)         // Pin B.0...0=LED off, 1=LED on
#define INPUT_SWITCH (1)       // Pin B.1...0=Pressed, 1=Not pressed
#define HOLD_DURATION_ms (100) // Wait this long after toggling the LED

Main Function: Initializing Variables and Pins

Next, the program declared a new unsigned, 8-bit integer (uint8_t) named inputState.  AVR I/O registers are also 8-bit unsigned bit collections, so either uint8_t or char would work here.

Next, PORTB0 and PORTB1 had to be initialized as output and input.  All pins are inputs by default, so this line only sets the bit belonging to the LED pin (OUTPUT_LED).

// Setup...
uint8_t inputState = 0;  // Variable to store input pin states
DDRB = (1<<OUTPUT_LED);  // Set LED pin to output

Reading a Pin

The AVR’s internal hardware will constantly update the PINB register with the latest known logic states for all Port B pins.  This happens even for pins that are configured as outputs.

The first line directly above stores the 8 pin states to the buttonState variable.  The line below that applies a bit mask such that only the bit belonging to the switch goes unchanged.  It clears the other bits to zero.

If Statement

In Figure 1 above, the INPUT_BUTTON signal will be:

  • 0 if the button is pressed
  • 1 if the button is released

The if statement will execute the block of code only if all bits within buttonState are 0 (i.e. if the button is pressed down).  This check happens before the if statement attempts to run its code.

The next code block will use another bit mask.  This time, a bitwise exclusive OR assignment, was used to flip only the bit belonging to the LED. 

The final line uses the delay macro to insert a millisecond-scale delay.

Final Code

Uploading this code now would switch the LED with every button press.  However, it would also blink the LED, if the button were ever held longer than DELAY_TIME_ms.  This was not the intention.

Mechanical Action

This brings up the need to think about of the mechanical action of switches.  A button press can be thought of as two steady states and two transitions:

  • Released state
  • Contact state
  • Make (transition from released to contact)
  • Break (transition from contact to released)

The next code revision will implement these states and their transitions accordingly…

Figure 3: Software States and Transitions
#include <avr/io.h>
#define F_CPU 1000000UL  // Define the clock frequency in Hz
#include <util/delay.h>  // Used by _delay_ms()

#define OUTPUT_LED (0)         // Pin B.0...0 = LED off, LED on
#define INPUT_SWITCH (1)       // Pin B.1...0 = Switch pressed, 1 = Switch not pressed
#define HOLD_DURATION_ms (100) // Wait this long after toggling the LED

int main(void)
{
  // Setup...
  uint8_t inputState = 0;  // Variable to store input pin states
  DDRB = (1<<OUTPUT_LED);  // Set LED pin to output
  // While the AVR is powered on...
  while (1) 
  {
    // Read the pushbutton state.  Ignore all other inputs
    inputState = PINB;
    inputState = inputState & (1<<INPUT_SWITCH);
    // If the switch is pressed, invert the LED, then pause
    if (inputState == 0)
    {
      PORTB ^= (1<<OUTPUT_LED);
      _delay_ms(100);
      //---NEW CODE: Re-sample the input
      inputState = PINB;
      inputState = inputState & (1<<INPUT_SWITCH);
      //---NEW CODE: Re-sample until switch is released
      while (inputState == 0)
      {
        //---Re-sample
        inputState = PINB;
        inputState = inputState & (1<<INPUT_SWITCH);
      }
    }
  }
}

The original objective was a “push on, push off” behavior where the LED state only changes during the “make” state change.  Therefore, this project will add a new while(1) loop that iterates until the switch releases.

Closing Remarks

The act of continuously checking the switch state is inconvenient because the program can do little else while it is looping.  A practical design will use hardware interrupts, which can trigger immediately when the button state changes.  Interrupts will be used in a future Unboxing Tomorrow project.

Hardware Variations

To test the benefit of capacitor C3, try removing it.  Doing so may cause the LED to unpredictably switch twice after some button presses and not others.

The RC low-pass filter formed by resistor R3 and capacitor C3 of Figure 1 above can optionally be adjusted.  Removing switch contact noise is sometimes called “debouncing.”  The physical condition of a switch and the force moving it are what determine the noise severity.

The cutoff frequency (fC) of an RC filter is the frequency that will lose approximately half its magnitude. 

f_C=\frac{1}{2\pi RC}

The project estimated R and C such that 1/fc would be less than the number of seconds between keystrokes, but greater than the number of seconds it takes for the switching noise to stop.  In practical applications, the ideal values R3 and C3 may be determined through trial experimentation.

AVR Internal Pull-Up Resistors

The ATTiny2313 microcontroller and many others like it have internal pull-up resistors.  Activating an internal pull-up resistor requires 2 steps:

  1. Set the pin direction to INPUT by writing 0 to the corresponding DDRxn register (where: x is the port letter and n is the bit number)
  2. Write 1 to the corresponding PORTxn register.

As an example, activating the internal pull-up resistor for today’s push button switch might involve…

// Activate the internal pull-up resistor for input pin DDRB &= ~(1<<INPUT_BUTTON); // Set pin direction to INPUT PORTB |= (1<<INPUT_BUTTON); // Activate internal pull-up

An internal pull-up may be useful as an alternative to external pull-ups like resistor R3 in Figure 1 above.  The tradeoff is a loss of precision.  According to the ATTINY2313 datasheet: the internal pull-up resistors may have resistance anywhere between 20 kΩ and 50 kΩ.

Handling Unused Pins

By default, an unused pin will be an input with no internal pull-up.  Such pins are said to be in a high-impedance (Hi-Z) state.

High impedance pins have no strong path to any known voltage.  The true voltage of a Hi-Z pin may randomly fluctuate, or “float” between 0 and 1, depending on the electric field it is caught in.

Although it’s not an absolute necessity: Microchip’s documentation recommends activating the internal pull-ups for any input pins that are unused.  This avoids the floating state, and may slightly reduce the AVR’s power consumption.