top of page
  • Writer's pictureUFRJ Nautilus

Programming Microcontrollers in C

Updated: May 7, 2021


Introduction

Microcontrollers (MCUs) are small programmable computers that can interact with the physical world. They are used in all kinds of embedded systems, from rockets to household appliances and we give endless possibilities for our projects.


You should be familiar with the Arduino platform, which saves us all the dirty work of low-level programming and allows us to prototype quickly. But why abandon the simplicity of Arduino and take the trouble to program the MCU directly?


Creating your project directly on the MCU, without the abstraction provided by arduino allows you to use its full capacity and run your code much more efficiently. If you intend to have your project commercially available, forget it, the arduino ecosystem license forces you to make your project open source. And finally, programming your microcontroller natively is a great opportunity to fully understand its operation and architecture, and learn how to work based on your limitations.


Now let's meet the microcontroller that will accompany us in this article!


AVR microcontrollers

In this article we will work with 8-bit microcontrollers from the AVR family (the same as your arduinos!), if you've had experiences with Arduino you already know the possibilities they give us, but it won’t hurt remembering.

In this article we will use ATTiny85, but everything is analogous for other MCUs of the same family. If you have any questions, consult the datasheet.


Architecture of MCUs

The 8-bit AVR microcontrollers are based on the RISC architecture, but we don't really need to know what it means, let's focus on the vital parts, common to all microcontrollers.

  • CPU: Responsible for logical and mathematical operations.

  • Memory: We’ve got two kinds:

    • ROM(Flash): Permanent memory, where our code will be stored.

    • RAM: Temporary memory, stores values necessary to execute the code such as variables and results.

  • Peripherals: These are independent modules within the MCU that perform specific functions, the most useful are:

    • I/O: Through the input and output doors we can interact with the outside world using electrical signals.

    • USART, I2C and SPI: They are serial communication protocols, we can communicate the MCU with a computer, sensors and even another MCU.

    • ADC: The famous "analogRead()" in arduinos. Sensors usually give us analog data (a wide range of values, and not the 0s or 1s we work with in the MCU), using this peripheral we can manipulate these values digitally.


-ATTINY85



Now let's meet our microcontroller! The ATTINY85 has 8kb of Flash memory and 512 bytes of RAM, it may seem little, but the computer that took man to the moon had 4kb of RAM and 32 kb of ROM. We have 6 digital inputs/outputs, 4 analog inputs (4 ADC channels) and up to 2 PWM outputs. The ATTINY85 is known for its very low consumption and can be powered by a coin cell battery for years.



Programming the AVRs

We will need external hardware to program our microcontroller, an ISP programmer, we will use the popular USBasp and the Atmel Studio IDE.

I will not dwell on the installation and configuration of the programmer in our IDE, take a look at this tutorial.

If you have an arduino laying arround you can also use it as an ISP programmer!


Our first project!




We'll create our first project at Atmel Studio:


File->New->Project


Name the project and select the ATTiny85 device (our target MCU). Paste the code below, compile and submit to ATTiny as shown in the link above. It's a simple code to flash an LED (A rite of passage in electronics). The LED is just an abstraction, we can use the same ideas to interact with any kind of external circuit, like H-bridge, relays and "pure" electronics.


#define F_CPU 1000000UL 
#include <avr/io.h> 
#include <util/delay.h> 

int main(void) 
{ 
    //Define pin 4 as 
    DDRB output |= (1 << DDB4); 

    while (1) 
    { 
        //Pin 4 HIGH 
        PORTB |= (1 << PB4); 
        //Delay(hold) 1 second 
        _delay_ms(1000); 
        PORTB &= ~(1 << PB4); 
        //Pin 4 LOW 
        _delay_ms(1000); 
    } 
} 

After sending the code, we'll set up the circuit.


Understanding the code


Digital Logic


First of all let us clarify some digital logic concepts:


  • Bit: It represents a logical value. True or false, High or Low, 1 or 0, and in the physical world: 5 Volts or 0 Volts.

  • Byte: A 8-bit set(8 may seem like an arbitrary value, but don't worry about it).

Hardware Registers


These are special places in memory, bytes, whose values affect the MCU hardware, we’ll take a closer look at them now.


DDRx:


Data Direction Register, indicates whether the pin is an input or an output. In the code above we’ve used DDRB, the B indicates that we are working with the pins of port B. The pins in the AVRs are divided into ports, notice the Atmega 328P pinout below, we have PB,PC and PD. In the ATiny85 as there are only a few pins we only have PB.



There are two ways to change the values in the registers:


1°: This is the most intuitive way, we associate 0(Input) or 1(Output) to each bit, remembering that each bit represents a pin from pin 0, from right to left.


DDRB = 0b00010000;


Only pin 4 will be an output, notice that it is the bit in position 4 (counting from 0 from right to left).


2°: This is the most elegant form.


DDRB |= (1 << DDB4);


We make the bit in position 4 a 1. We cannot change the 1 for 0 and expect the bit to become 0, we should do the following operation to zero the 4 bit.


DDRB &= ~(1 << DDB4);


PORTx: Port Data Register, defines whether the pin is HIGH or LOW.


Now that we know what the hardware registers do it's easy to understand the code above. First we set pin 4 of port B as output, inside the loop we change the pin state with 1s intervals using the _delay_ms() function.

Try adding another LED, make them flash alternately, the sky is the limit.


Inputs!




We can't do much just by controlling the state of the pins, can we ? Now let's check their condition to get information from the outside world. In this example we will control our LED with a button and make it flash manually.



#define F_CPU 1000000UL 
#include <avr/io.h> 
#include <util/delay.h> 

int main(void) 
{ 
    //Pin 4 as 
    DDRB output |= (1 <<< DDB4); 
    //Pin 0 with 
    DDRB input &= ~(1<<<DDB0); 
    //Activate the Pull-up resistor on pin 0 
    PORTB |= (1<<<PB0); 
    //Pin 4 LOW 
    PORTB &= ~(1<<PB4); 
    
    while (1) { 
    //Button pressed ? (0V on pin 0 ?) 
        if((PINB & (1 << PINB0)) == 0) 
        { 
            //LED on 
            PORTB|= (1<<PB4); 
        }
        else 
        { 
            //LED off 
            PORTB &= ~(1<<PB4); 
        } 
    } 
} 

We have a new register in the code above, PINx, responsible for saving the current state of the pins.


A little addendum:


You must be confused by expressions like:


(PINB & (1 << PINB0))

PORTB &= ~(1<<PB4);

DDRB |= (1 << DDB4);


These are logical operations with the bits of the registers, explaining them goes beyond the scope of this article. But here's a guide to make your life easier:


Put 1 in bit x:

BYTE |= (1 << x);


Put 0 in bit x:

BYTE &= ~(1 << x);


Alternate the value in bit x:

BYTE ^= (1 << i);


Conclusion

We went through the basics of AVR I/O manipulation, with that knowledge alone we can think of infinite applications for those MCUs. Many possibilities were not explored here, analog digital conversion, serial communication, interrupts, among others.


Written by Gabriel Guimarães

0 comments

Recent Posts

See All
bottom of page