Arduino/AVR tutorial

Ready to build your first robot under 3h?

Download as .zip Download as .tar.gz View on GitHub

Input/Output handling

Contents

Hopefully, you own an Arduino Nano-compatible board, just like the Cytron’s one mentioned in the Bill of materials page:

Cytron Maker Nano pinout

[!IMPORTANT] I strongly encourage you to support the original Arduino Project and the idea behind it! Arduino enables thousands of people to develop hardware by reducing the learning curve. I strongly encourage you to buy one yourself!

The project can be completed using a standard Arduino Nano board. The Cytron’s board adds new features such as LEDs in all GPIO, a programmable button, and a buzzer. It’s a nice touch if you want to reduce wiring in your final project.

[!NOTE] The board uses CH340C as a UART-USB converter. It’s may not be compatible with your operating system out of the box. In Ubuntu 22.04.3 LTS, you’ll need to configure it by uninstalling a package from your system (it disables braille display): sudo apt remove brltty 1. MS Windows may require similar steps.

The Cytron’s board uses Atmega328P as it’s main microcontroller in the TQFP package. Luckily, you don’t need to follow PCB paths to determine which board pin correspond to which IC pin. The pinout diagram above does it perfectly for you! Still, if it happens one day you need create your own PCB, it’s worth to know physical dimensions and properties of your microcontroller (i.e. packaging, DIP package or some sort of SMD like TQFP). You also want make yourself familiar with the original datasheet: Atmega328P Datasheet. The datasheet provides all necessary information you need to start coding your microcontroller and tech specs that you should follow.

PORTs, PINs and power output

What is a port, then? Well, in a realm of microcontroller, a port is a module that captures a signal from the real word, encodes and sends it over a data bus to CPU (Processor, Central Processing Unit) and/or other microcontroller peripheral elements. A pin is a physical connector (a lead) that one can, for instance, solder onto a PCB. Pins are the true interface that the IC uses to interact with the world.

Atmega328p has 3 ports (PORTB, PORTC, PORTD) with built-in pull-up resistors. The microcontroller can therefore support up to 23 input/output pins (PORTC exposes 7 pins, the rest supports 8 pins). To visualize the overall Atmega architecture, please refer to the documentation (chapter: 2). The block diagram (extracted from the datasheet Chapter 2.1 Block diagram):

Source: Atmega 328p datasheet, Chapter 2.1 Block diagram

That’s a theory. You are not soldering your own PCB, you bought an off-the-shelf one. It means, you don’t need to map pins defined in Atmega datasheet to your board outputs. Your Cytron’s Nano is compatible with the original Arduino Nano, therefore you can re-use all resources known in Arduino to your advantage. Compare Cytron’s pinout (the very first image in this page) with the table and the Arduino pinout below.

PORT Ardunio Nano Cytron Maker Nano Atmega328P
DDRB D8 D8 PB0
DDRB D9 D9 PB1
DDRB D10 D10 PB2
DDRB D11 D11 PB3
DDRB D12 D12 PB4
DDRB D13 D13 PB5

Arduino Nano Pinout

D\d (for instance: D8, D13) is an alias for a given pin Arduino ecosystem. You don’t need to memorize anything. Your board has all the markings printed on the PCB solder mask itself. You will use the very same aliases such as D8, D13 in your code if you decide to use Arduino framework. The Atmega328P pin aliases found in Atmega toolchain and are simply a shorthand for underlying pin addresses (i.e., 0x00). You definitely want to use these aliases! It improves overall readability and keeps your sanity intact anytime you decide to debug your code in future :).

If you want to know more about Nano board, you can refer to this page 2.

Digital output

[!IMPORTANT] Atmega328p pins are limited to 20mA (milli amps) per pin and up to 100/150mA for a port. You should not connect anything more amp-consuming to a pin directly. Use a transistor to amplify digital signals to an output that can handle bigger loads. You can read more on amplifiers in Wiki. Please, see also 28. Electrical Characteristics in your Atmega datasheet 3.

One more thing, Arduino Nano works in 5V logic. It means the high state (also referred as 1) is 5V and the low state is 0V (you guessed it! It’s referred as 0). That’s the ideal condition, of course. In the real world, 0 state can be considered as 1/3 VDD or lower of your voltage supply. Similarly, the high state is 2/3 VDD or higher 4. These are pretty good approximations. You can also refer to 28.2 DC Characteristics in datasheet for more details on input/out (high|low) voltages 3.

Lots of theory, and no code so far. It’s time to change it. Let’s take a closer look to the blinking example we had in the previous chapter:

// Source: https://github.com/arduino/arduino-examples/blob/main/examples/01.Basics/Blink/Blink.ino

void setup() {
  // initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// the loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}

The setup() function initializes hardware. It includes setting up a desired state on a pin, interrupts, serial devices, etc.. Effectively, anything you need to run your main application code should be placed within the loop().

The loop() function is effectively anything you want to run indefinitely as your firmware. This involves all ALU operations you want to run with help of your peripherals such as I/O pins, UART etc.

In order to blink an LED you always need to configure your port by telling a direction of a given pin, whether it’s input or output. Possible options: OUTPUT, INPUT, INPUT_PULLUP.

Once this is done, you can then either write or read a pin state, depending on your direction configuration. To do so, you can use digitalWrite(<<pin alias>>, <<state>>) function. Pin alias can be a uint8_t value or a label such as LED_BUILTIN. State can be either LOW or HIGH.

Let’s connect your own LED… You need an LED and a resistor. You should never connect an LED to a DC output as you will likely fry it.

Basic LED circuit

First of all, if you have some experience in electronics you must know that LED is directional. It means it acts as conductor if connected with correct polarity. If you connect it the other way around it won’t conduct (well… until it does, briefly :)). The term is reverse current and it can be a feature sometimes. As this no 101 tutorial in electrical components, I suggest to take a look at Wiki 5. If you are in rush, I recommend taking a look at voltage-current chart to understand how different LEDs can be.

S1 voltage supply is the Arduino. It gives you 5V. LED requires 0.7V and 0.2mA to light up. Now, you need to calculate what resistor is needed. The formula uses Ohm’s Law and some principles on connecting circuits in series. I transformed it so it corresponds to the circuit

\[R_1 = \frac{S_1 - D_1}{I} = \frac{5V - 0.7V}{20 \cdot 10^{-3}A} = 215 \frac{V}{A} = 215\Omega\]

Isn’t the physics of electrics amazing? Now, you need to pick a resistor that matches the calculations. If you apply lower resistance value, you may fry your LED. I suggest to pick 220Ohm resistor or higher. It’s all math and diagrams. How to connect it? Well, let’s go back to the real world. An LED has two leads: a short one and a longer one. The longer one is the ‘+’ (katode) and the shorter one is ‘-’ (anode). You want to connect your polarities in series, ie: V+ - +R- - +D- - -V. A ‘-‘ in component is a plus for the other one. You can also take a look at Adafruit’s tutorial for more details 6. Oh, and as an interesting fact - the colors on your resistor packaging matter!.

Please, construct your circuit as proposed here. You can use your jumper cables if you want: Arduino circuit
Figure: Connecting an LED with a resistor

Now, the coding part! It’s not that complicated (source: Incremental blink):

static constexpr const uint8_t HARDWARE_LED = 8;
static constexpr const uint8_t DELAY_RATE = 100;
static constexpr const uint16_t MAX_DELAY = 3000;

static uint8_t hardwareLedState = HIGH;
static uint8_t hardwareLedDelayCounter = 1;

void setup() {
  pinMode(HARDWARE_LED, OUTPUT);
  digitalWrite(HARDWARE_LED, HIGH); 
}

void loop() {
  uint16_t delayValue = DELAY_RATE * hardwareLedDelayCounter;
  ++hardwareLedDelayCounter;
  if (delayValue > MAX_DELAY) {
    delayValue = DELAY_RATE;
    hardwareLedDelayCounter = 1;
    hardwareLedState = HIGH;
  }

  hardwareLedState ^= 1;
  digitalWrite(HARDWARE_LED, hardwareLedState); 
  delay(delayValue);
}

The code increases delay time in each consecutive led state, i.e, 200ms - LOW, 300ms - HIGH and so on. Note a new constant HARDWARE_LED - it matches D8 pin in the Fritzing diagram above. If you use Cytron’s board, you’ll hear beeping sound too as D8 pin is connected to a buzzer

Ok, so how does it look like in pure AVR then. Well, in AVR context setup() and loop() functions translate to this form:

int main() {
  setup();
  while(1) {
    loop();
  }
}

What Arduino Framework does is really it provides a layer of abstraction over the AVR microcontroller. It’s way more developer friendly than dealing with raw registers. Speaking of registers, do you remember the blink example? This is pure AVR code (source: AVR Blink):

#include <avr/io.h>
#include <avr/delay.h>

// This is the same hardware LED we introduced earlier!
#define HW_LED (1 << PB0)

int main() {

  /* setup() */
  DDRB = (1 << PB0);
  PORTB = (1 << PB0);  // light up the LED
  /* end: setup() */

  /* loop() */
  while (1) {
    PORTB ^= (1 << PB0);
    _delay_ms(1000);
  }
  /* end: loop() */
}

Isn’t it way more complex, is it? Lots of bit shifts and poor readability. You certainly can see setup() and loop() blocks.

To initialize pin mode (remember: pinMode() function?), you need to follow documentation 13.2.3 Switching Between Input and Output 3 and set DDRB’s value to 1, or precisely 0b0000 0001. The port shall work as the output device allowing you to set/unset state. If you want to connect an LED to D12 (also known as PB4), you can enable direction by assigning DDRB = (1 << PB4) or even DDRB = 0b0001000. To enable pin, you need to operate on a different register: PORTB. Operation ^= means toggling a value under the given bit location. In Arduino, you would need to call digitalWrite(pin, LOW|HIGH) twice.

Clearly, AVR can be much harder to comprehend. So why bother? Well, some day you may be asked to implement a simple firmware on ATtiny13 (or similar), a microcontroller with very little resources. The AVR example requires 166 bytes, the Arduino one: 924bytes. Any framework shall add additional overhead and consume more flash disk space on your device. You need to be very conscious about resources and the platform limits (i.e., stack depth - nesting functions calls may also lead to errors!).

Digital input and pull-up resistors

It’s time to get some input events from the world. An obvious choice is a push button. A simple device that breaks a circuit if anyone pushes it…

Now, we already know that turning on an LED is to provide state 1, which translates to 5V potential at a pin. Good! How about we reverse the order. It’ll be you who changes a potential on Arduino lead, which translates to a different state on a pin. All you need to do is to verify the state in the code!

Some theory is needed. You need to provide stable voltage on your pin. Otherwise, you can read any state state between 0 and 1, it’ll float. This is why you need to construct a special circuit to support a button with a pullup resistor, such as this one:

Button circuit with pullup resistor

You are not limited to such a simple design. You can of course connect your button through a transitor, i.e., common collector circuit and/or a capacitor to the circuit to eliminate contact bounce 7. Eliminating contact bouncing can be done programmatically but it’s a rather tedious job to do.

More on pullup resistors can be found on Sparkfun’s learning portal 8 and debouncing here 9.

Ok, that’s all for now. It’s a 3h tutorial after all. Let’s do some coding. The Cytron board you have already has a push button! Let’s use it! This is the button schematics (retrieved from Cytron’s docs):

Cytron Nano board - push button schematics.

We are interested in the bottom part of the diagram. We can use programmatically D2 pin. I hope you spotted that the button does not support physical debouncing, unfortunately. You can either code it on your own, based on the given resources or simply dismiss it. Here’s a simple code snippet that changes an LED state anytime you press the button and keeps it until you push the button again (source - 3_pullup_and_software_debouncing):

static constexpr const uint8_t PUSH_BUTTON = 2;
static uint8_t builtinLedState = LOW;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(PUSH_BUTTON, INPUT_PULLUP);
  digitalWrite(LED_BUILTIN, builtinLedState);
}

void loop() {
  uint8_t pushButtonState = digitalRead(PUSH_BUTTON);
  if (pushButtonState == LOW) {
    delay(50);                                    // simple programmatic debouncing
                                                  // You should measure the precise debouncing period
                                                  // with an oscilloscope. 50ms is just an educated guess
    pushButtonState = digitalRead(PUSH_BUTTON);

    if (pushButtonState == LOW) {
      builtinLedState ^= 1;                       // toggling the LED state
      digitalWrite(LED_BUILTIN, builtinLedState);

      while (digitalRead(PUSH_BUTTON) == LOW);    // blocking the program until
                                                  // the user releases the button
    }
  }
}

Interesting code parts: pinMode(<<pin>>, INPUT_PULLUP|INPUT). This is a pullup configuration for D2 button. From now on, you can read the state on the D2 pin!. digitalRead(<<pin>>) function reads the state, either HIGH or LOW.

Another, weird part of the code is the nested if-statement and a delay function. Well, this is software debouncing. You push the button and connectors start to bounce back and forth. The program waits another 50ms until the connectors stabilize. The state should no longer float on that pin by the time the program reads the state again. If it is indeed low again, you change the state of the button.

Note the last while loop at the end of the snippet. It blocks the application until the user releases the button. This is to prevent instant state toggling of the LED. Comment this out and see how your board behaves!

Now, how can you code the same thing in AVR? Let’s see (source: 4_pullup_and_software_debouncing_avr):

#define LED_BUILTIN (1 << PB5)
#define PUSH_BUTTON (1 << PD2)

int main() {

  // setup()
  DDRB = LED_BUILTIN;
  DDRD = 0x00;          //set the enitre port as input - including PD2

  PORTB = 0x00;
  PORTD = PUSH_BUTTON;  //PD2 is set as input and the state as HIGH - pullup enabled

  // loop()
  while (true) {

    // the push button is pressed if it's state is 0

      if (bit_is_clear(PIND, PD2)) {  // much clearer than: !(PIND & (1<<PD2))
        _delay_ms(50);
        if (!(PIND & PUSH_BUTTON)) {
          PORTB ^= LED_BUILTIN;
          while (bit_is_clear(PIND, PD2));
        }
      }
  }
}

There are some major differences between this code and the Arduino one. First of all, the state is kept in registers rather than in an application stack. There is also an additional register used: PIND (see docs: 13.4.10 PIND 3). The register is responsible for reading the state on Atmega’s lead. It returns all 8 bytes, each byte corresponds to one of the pins in your board. To read the state on PD2, you need to run some shifts to get data. Let’s decipher this bit, assuming you actually keep holding the button:

values for:
  PIND = 0b???? ?0?? = 8 (? means state unknown)
  PD2 = 2
  1<<PD2 = 0b0000 0100 = 4

// button is pressed, state on the button lead is 0
1. !(PIND & (1<<PD2))
2. PIND & (1<<PD2)  --> 0b?????0?? & 0b00000100 = 0b00000000 = 0
3. !(0b00000000) = !(0) = !(false) = true  // zero is considered as false in C/C++/python
4. result: true

// button is released, state on the button lead is 1
  PIND = 0b???? ?1?? = 8 (? means state unknown)

1. !(PIND & (1<<PD2))
2. PIND & (1<<PD2)  --> 0b?????1?? & 0b00000100 = 0b00000100 = 8
3. !(0b00000100) = !(8) = !(true) = false // any non-zero number in C/C++/python is always considered as true
4. result: false

So really, all this complex math on bytes is all about reading a state on a given lead. If you decide to use Arduino library, it’s way easier to do so! Of course, it all comes with a price. This is hex size for each framework:

Good job! You know how to blink your LEDs both programmatically and with a push button. That’s quite a lot. Really, interacting with a microcontroller is all about sending and receiving ones and zeros. All subsequent chapters only extend that notion! Can you imagine manually pushing and releasing a button 1000 times a second? This is what you will learn in the PWM chapter. Well, you’ll learn how to do it programmatically, at least ;)

Let’s move on to the next topic: interrupts. It’s not the best idea to run blocking operations in the main program loop after all.

Interrupts

Last but not least, we shall take a look at interrupts in this section. What is an interrupt? Well, it’s a mechanism that allows reacting to external (or internal) events outside of the main program loop? Wait, what? Let’s take a look at an example, this time it’ll be AVR code first (source - 5_interrupts_avr_toggle_led):

#include <avr/io.h>
#include <avr/interrupt.h>
#define LED_BUILTIN (1 << PB5)
#define PUSH_BUTTON (1 << PD2)

ISR(INT0_vect) {                   // magic here: Interrupt Service Routine
  PORTB ^= LED_BUILTIN;
}

static void setup() {
  DDRB = LED_BUILTIN;
  DDRD &= !(PUSH_BUTTON);          // pin as input
  PORTD = PUSH_BUTTON;             // pull-up

  EIMSK = 1 << INT0;               // enable PD2/INT0 as an interrupt source
  EICRA = 1 << ISC01;              // Enable direction of interrupt on INT0, 
                                   // falling edge
}

int main() {
  setup();
  sei();                           // enable global interrupts, SREG register

  while (true) {
    // idle, do nothing
    _delay_ms(10000);
  }
}

If you take a closer look at while (true) loop, you see it does nothing but sleeping. A good nap is nice but this is not why you bought your board to simply let it sleep… You want it to work for you and blink an LED!. If you compile it and start pressing the button you see that the LED turns on and off. Why? The interrupt!

There are some lines that can be somehow surprising. First of all sei() function. It enables global interrupts in SREG registry as documented in 6.3.1 SREG – AVR Status Register 3. It’s important not to forget to use it. In my personal experience, forgetting running this function is the most common reason why my interrupt routines don’t work… it’s because I don’t enable them in the first place… There’s also a function that has exactly opposite effect: cli() - it disables interrupts. It sometimes is handy as well.

Ok, interrupts are enabled. Now, we need to use PB5 (push button) pin as interrupt! Atmega328p offers two I/O pins as a source for external interrupts. Not much but still, we need to work with things we have*. If you take a look at Arduino pinout from in the top of this page you’ll notice that PB5 has also INT0 label. That’s the interrupt! Smartly design board, isn’t it?

*you can set up interrupts as PCIEx that generates interrupts on many pins but it’s out of scope of this tutorial

The setup functions sets up the PB5 as a standard pullup-enabled input pin. There are two suspicious registers EIMSK and EICRA. EIMSK allows you to enable INT0 as an interrupt source (vector) - see documentation 12.2.2 EIMSK – External Interrupt Mask Register 3. The documentation states you need to set up an activation property to either raising or falling edge. You need the falling edge as you use a pullup-resistor. It means the high potential on the lead. So anytime you press the button, the potential goes to zero, hence the falling edge. Of course, you can reverse that logic and use to your advantage. After all, it’s all about making engineering decisions.

Now, the most important part: ISR(). ISR is a special macro that handles interrupts in AVR. An average ISR body should be very concise and efficient. No long running operations as you block the main loop! Disabling interrupts and re-enabling them can be also a good idea if your code is highly asynchronous. You also should store SREG value to make sure you don’t disable anything by mistake. ISR should then look more like this:

volatile uint8_t tmpSREG = 0;

ISR(INT0_vect) {                   // magic here: Interrupt Service Routine
  tmpSREG = SREG;
  cli();
  PORTB ^= LED_BUILTIN;
  SREG = tmpSREG;
}

Luckily, Atmega does this operation automatically, so you don’t need to specify any tmpSREG variables 10. Hope, you noticed volatile keyword. Interrupts modify data outside of a regular program loop impacting the value that can be read from such a variable. The keyword prevents a compiler from optimizing the variable, improving stability of your application.

So what’s an interrupt? A routine that reacts on an external/internal event and runs small portions of code called routines outside of a regular program loop. Hope, it’s clear now. However, can you do the same thing in an easier way? Yes, you guessed it! Let’s use Arduino this time! So here’s the code that does the same thing, with no debouncing (source - 5_interrupts_avr_toggle_led):

static constexpr uint8_t PUSH_BUTTON = 2;
volatile uint8_t builtinLedState = LOW;


void toggleLed() {
  builtinLedState ^= 1;                       // toggling the LED state
  digitalWrite(LED_BUILTIN, builtinLedState);
}

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(PUSH_BUTTON, INPUT_PULLUP);
  digitalWrite(LED_BUILTIN, builtinLedState);
  attachInterrupt(                            // enabling interrupts on D2
    digitalPinToInterrupt(PUSH_BUTTON),       // mapping D2 to Atmega's INT0
                                              // this is for readability
    toggleLed,                                // a pointer to function, simply pass
                                              // a name of your function, it's not that scary!
    FALLING);                                 // activate the interrupt on falling edge
}

void loop() {
  delay(10000);
}

Hope, the resemblance is obvious… toggleLed() function simply performs a toggle operation on your built-in LED, using an external state variable. enableInterrupt well… enables the interrupt in nearly plain English. digitalPinToInterrupt maps a pin number to a corresponding event source pin, here INT0. This is certainly more readable and does not require much of knowledge on your board pinout. toggleLed here is simply passing a pointer to toggleLed function. You can use a more explicit way to show it’s a pointer: &toggleLed, although I find it a bit of an overkill. Finally, you need to define a trigger that activates your interrupt. Choose FALLING trigger and keep in mind that there other options such as LOW|CHANGE|RISING 11.

The enableInterrupt function works for digital pins only. Implementing a custom ISR can be more generic. Atmega328p support 26 input vectors as defined in 11.1 Interrupt Vectors in ATmega328P 3. You can also browse avr/iom328p.h header to get the vector list:

[...]
/* Interrupt Vectors */
/* Interrupt Vector 0 is the reset vector. */

#define INT0_vect_num     1
#define INT0_vect         _VECTOR(1)   /* External Interrupt Request 0 */
// [...]

To summarize, the interrupts mechanism is a very handy tool if you want to support multiple actions in almost the same time. It allows you to act upon any external event and handle it efficiently.

Using interrupts and other Atmega peripherals allows you to implement a solution that appears to perform several actions in the same time. Multitasking, my friend :)!

Analog input and output - ADC and PWM

Digital electronics, as your Atmega328p, is all about zeros and ones. There are no values in between, ideally. Thus, it’s a rather hard task to talk about analog values in between 0-5V. Luckily, we are not exactly bound to 2 states only. We can change a pin state fast enough to reduce output power in a given timespan - this is called PWM, Pulse Width Modulation. It’s a rather important aspect of embedded programming, therefore please read the next chapter that handles PWM in details.

There are also Arduino boards that support DAC - Digital-to-Analog converters, such as Arduino Due. Unfortunately, Atmega 328p microcontroller, as the one in your board, does not support DAC. For more details, take a look at Wiki: Digital-to-analog converter.

You can also read analog values with your microcontroller. Atmega328p comes with a 10-bit ADC and comparator built-in! ADC stands for Analog-Digital-Converter, which effectively translates voltage to a 10-bit value: 0-1023 integer value. More on ADC in the next chapter!

References