AVR Microcontroller Programming Basics

Let’s go back to the absolute basics of programming a microcontroller like the ATmegaXX8. We'll cover the environment, theory, and essential concepts to help you fully understand what’s happening and how microcontroller programming works.


1. Microcontroller Programming Basics

What is a Microcontroller?

  • A microcontroller (e.g., ATmegaXX8) is a small computer on a single chip.
  • It has:
    • Processor (CPU): Executes instructions.
    • Memory:
      • Flash memory: Stores your program.
      • SRAM: Temporary data (variables).
      • EEPROM: Persistent data storage (non-volatile, even after power off).
    • I/O Pins: Interface with the outside world (e.g., LEDs, sensors, motors).

Why Program Microcontrollers?

Microcontrollers are the brains of embedded systems. By programming them, you can control devices like LEDs, motors, and sensors, making them perform specific tasks (e.g., blink an LED, measure temperature, or drive a robot).


2. Environment for Programming a Microcontroller

To program a microcontroller, you need:

  1. Hardware:

    • Microcontroller: (ATmegaXX8 in your case).
    • ISP Programmer: To load compiled code onto the microcontroller.
    • A development board or breadboard setup for connections.
  2. Software Development Tools:

    • IDE (Optional): A tool like Atmel Studio or VS Code with plugins.
    • Compiler (e.g., avr-gcc): Converts your C code into machine-readable code for the microcontroller.
    • Flasher (e.g., avrdude): Transfers the compiled program to the microcontroller.
  3. Languages:

    • Most microcontrollers are programmed in C or C++. Assembly is rarely needed for modern applications.

3. How a Microcontroller Works

When you program a microcontroller, you are:

  1. Writing machine instructions in C (or assembly) to control its hardware.
  2. Configuring registers to control its peripherals (e.g., GPIO, timers, UART).
  3. Using its clock to execute instructions sequentially.

4. Registers and Pins

Registers

A register is a small piece of memory inside the microcontroller used to control or monitor hardware. For example:

  • DDRx (Data Direction Register):
    • Configures whether a pin is an input (0) or output (1).
    • Example: DDRB = 0b00100000; // PB5 is output.
  • PORTx (Port Register):
    • Sets HIGH/LOW state for output pins.
    • Example: PORTB |= (1 << PB5); // Set PB5 HIGH.
  • PINx (Pin Register):
    • Reads the state of input pins.

Pins

  • Each pin on the microcontroller corresponds to a bit in a register.
    • E.g., PB5 refers to the 5th bit of PORTB.

5. Programming Workflow

  1. Write Code in C:

    • You write code that interacts with the microcontroller's registers and hardware.
  2. Compile Code:

    • Use avr-gcc to convert the C code into a hex file (machine-readable instructions).
  3. Upload Code:

    • Use an ISP programmer (e.g., USBasp) and avrdude to flash the hex file onto the microcontroller.
  4. Execute:

    • When powered, the microcontroller automatically starts executing your program from the reset vector (memory address 0x0000).

6. Common Concepts in Microcontroller Programming

a. Clock and Timing

  • Microcontrollers have an internal or external clock (crystal oscillator) that determines how fast they execute instructions.
  • The clock speed (e.g., 16 MHz) defines how many cycles the microcontroller performs per second.

b. GPIO (General-Purpose Input/Output)

  • GPIO pins can be configured as inputs (to read data from sensors) or outputs (to drive LEDs or motors).
  • Example:
    DDRB |= (1 << PB5); // Set PB5 as output
    PORTB |= (1 << PB5); // Set PB5 HIGH
    

c. Interrupts

  • Interrupts allow the microcontroller to respond to events (e.g., button presses) asynchronously.
  • Example: Timer interrupts for precise timing.

d. Timers

  • Built-in hardware to count time or events.
  • Useful for PWM (Pulse Width Modulation) and precise delays.

e. ADC (Analog-to-Digital Converter)

  • Converts analog signals (e.g., from sensors) into digital values.

7. Programming Concepts Refresher

Basic Syntax

#include <header.h> // Include libraries

int main(void) { // Program entry point
    // Code goes here
    return 0;
}

Variables

  • Store data during execution.
int x = 10;       // Integer variable
float pi = 3.14;  // Floating-point variable
char c = 'A';     // Character variable

Control Structures

  • If-Else:
    if (x > 5) {
        // Do something
    } else {
        // Do something else
    }
    
  • Loops:
    • For:
      for (int i = 0; i < 10; i++) {
          // Loop body
      }
      
    • While:
      while (x > 0) {
          x--;
      }
      

Functions

  • Modularize code into reusable blocks.
int add(int a, int b) {
    return a + b;
}

int main(void) {
    int result = add(3, 4); // result = 7
    return 0;
}

Pointers

  • Store memory addresses.
int x = 5;
int *ptr = &x;  // Pointer to x
*ptr = 10;      // Modify x through the pointer

Bit Manipulation

  • Microcontroller programming often involves working with individual bits in registers.
    PORTB |= (1 << PB5);  // Set PB5 HIGH
    PORTB &= ~(1 << PB5); // Set PB5 LOW
    

8. Theory Applied to the LED Code

Here’s how the theory connects to the LED blinking example:

  1. Set Pin as Output:

    • Use DDRB to configure PB5 as output. This allows it to drive the LED.
  2. Turn LED ON/OFF:

    • Use PORTB to set the state of PB5.
    • HIGH (1): LED ON.
    • LOW (0): LED OFF.
  3. Add Delay:

    • Use _delay_ms() to create a pause, making the LED blink.
  4. Repeat Forever:

    • The while (1) loop ensures the program runs indefinitely, as microcontrollers do not "terminate" like normal programs.

9. Example Workflow

  • Write code in blink.c:

    #include <avr/io.h>
    #include <util/delay.h>
    
    int main(void) {
        DDRB |= (1 << PB5); // Set PB5 as output
        while (1) {
            PORTB |= (1 << PB5); // Turn LED ON
            _delay_ms(500);
            PORTB &= ~(1 << PB5); // Turn LED OFF
            _delay_ms(500);
        }
    }
    
  • Compile:

    avr-gcc -mmcu=atmega328p -Os -o blink.elf blink.c
    avr-objcopy -O ihex blink.elf blink.hex
    
  • Flash:

    avrdude -c usbasp -p m328p -U flash:w:blink.hex
    

Summary

  • Microcontrollers operate at the register and pin level, and programming them involves manipulating these registers to control peripherals.
  • The environment involves writing code in C, compiling it, and flashing it onto the chip.
  • Concepts like GPIO, registers, bit manipulation, and clock speed form the foundation of microcontroller programming.

Let me know if you want to dig deeper into any part of this process!

Registers and Pins - detailed

What Are Registers?

  • Registers are special memory locations inside the microcontroller used to control and monitor hardware features like GPIO pins, timers, ADCs, and more.
  • They act as an interface between the CPU and the microcontroller's peripherals (e.g., I/O pins).
  • Registers are mapped to specific memory addresses in the microcontroller's memory.

How Registers Relate to Pins

  • Each GPIO (General Purpose Input/Output) pin is controlled by a few registers:
    • DDRx (Data Direction Register): Determines whether a pin is an input (0) or output (1).
    • PORTx (Data Register): Sets a pin HIGH or LOW when configured as output.
    • PINx (Input Pins Register): Reads the current state of the pins when configured as input.

For example:

  • PB5 refers to Pin 5 of Port B.
  • To set PB5 as an output, the 5th bit of the DDRB register is set to 1.
  • To make PB5 HIGH or LOW, the 5th bit of PORTB is modified.

Where Are These Registers Stored?

Microcontrollers like the ATmegaXX8 use a Harvard architecture, meaning program memory and data memory are physically separate. Registers are stored in:

  1. SRAM (Static RAM):

    • Registers are mapped to the first 64 bytes of SRAM.
    • In the ATmegaXX8, register memory starts at address 0x20 in SRAM. For example:
      • PORTB is mapped to address 0x25.
      • DDRB is mapped to address 0x24.
      • PINB is mapped to address 0x23.

    Memory Map Example:

    Address | Register
    --------|---------
    0x23    | PINB
    0x24    | DDRB
    0x25    | PORTB
    
  2. Flash Memory:

    • Flash stores your program code (instructions). Registers are not stored here.
  3. EEPROM:

    • EEPROM is used for persistent data storage (non-volatile). Registers are not stored here either.

2. Understanding the Operations in Code

Let’s break down:

PORTB |= (1 << PB5);  // Set PB5 HIGH
PORTB &= ~(1 << PB5); // Set PB5 LOW

What Does This Mean?

  • PORTB is the data register for Port B.
  • PB5 is the 5th pin of Port B (represented as a constant with a value of 5).
  • You are manipulating the 5th bit of the PORTB register.

Bitwise Operators Used

  1. | (Bitwise OR):

    • Sets specific bits to 1 while leaving others unchanged.
    • Example:
      PORTB |= (1 << PB5);
      
      • 1 << PB5: Creates a bitmask where only the 5th bit is 1 (e.g., 00100000 for PB5).
      • PORTB |=: Sets the 5th bit of PORTB to 1 without altering other bits.
  2. & (Bitwise AND):

    • Clears specific bits (sets them to 0) while leaving others unchanged.
    • Example:
      PORTB &= ~(1 << PB5);
      
      • 1 << PB5: Creates a bitmask where only the 5th bit is 1.
      • ~(1 << PB5): Inverts the bitmask so the 5th bit is 0 and others are 1 (e.g., 11011111).
      • PORTB &=: Clears the 5th bit of PORTB to 0 without altering other bits.
  3. ~ (Bitwise NOT):

    • Inverts all bits in a number.
    • Example:
      ~(1 << PB5)
      
      • If 1 << PB5 is 00100000, the result is 11011111.
  4. << (Bitwise Left Shift):

    • Shifts bits to the left by a specified number of positions.
    • Example:
      1 << PB5
      
      • Shifts the binary value 1 left by PB5 positions. If PB5 = 5, the result is 00100000.

How It Works Together

  • Set PB5 HIGH:

    PORTB |= (1 << PB5);
    
    • Step-by-step:
      1. 1 << PB5 creates a bitmask: 00100000.
      2. PORTB |= sets the 5th bit of PORTB to 1, turning the pin HIGH.
  • Set PB5 LOW:

    PORTB &= ~(1 << PB5);
    
    • Step-by-step:
      1. 1 << PB5 creates a bitmask: 00100000.
      2. ~(1 << PB5) inverts the bitmask: 11011111.
      3. PORTB &= clears the 5th bit of PORTB, turning the pin LOW.

3. Where Are PORTB, DDRB, and PB5 Defined?

These names come from the AVR header files included with avr-gcc. Specifically:

  1. <avr/io.h>:

    • This header defines all hardware-specific constants and macros for the ATmega microcontroller.
    • PORTB, DDRB, and PINB: These are defined as memory addresses corresponding to the registers for Port B.
    • PB5: Defined as 5, representing the 5th bit in the PORTB, DDRB, or PINB registers.

    Example from the header file (simplified):

    #define PORTB  _SFR_IO8(0x05) // PORTB mapped to memory address 0x25
    #define DDRB   _SFR_IO8(0x04) // DDRB mapped to memory address 0x24
    #define PINB   _SFR_IO8(0x03) // PINB mapped to memory address 0x23
    #define PB5    5              // PB5 is the 5th bit (bit position 5)
    
  2. _SFR_IO8:

    • A macro that maps these names to their corresponding memory addresses.

4. Registers Recap

GPIO Registers for Port B:

  • PINB: Reads the state of pins (input).
  • DDRB: Configures pins as input/output.
  • PORTB: Sets pins HIGH or LOW (output).

Memory Locations:

Registers are stored in the first 64 bytes of SRAM and mapped to specific addresses:

  • PINB at 0x23
  • DDRB at 0x24
  • PORTB at 0x25

Summary

  • Registers: Special memory locations mapped to SRAM, used to control hardware.
  • Pins: Represented as bits in these registers.
  • Operators:
    • |= (OR) sets bits.
    • &= (AND) clears bits.
    • << (SHIFT) creates bitmasks.
  • Defined Names:
    • PORTB, DDRB, etc., are predefined in <avr/io.h> and correspond to memory-mapped hardware registers.

The size of the bitmask is determined by the width (number of bits) of the variable or register it is being applied to. Let's break this down:


1. What is a Bitmask?

A bitmask is a binary number used to manipulate (set, clear, toggle) specific bits in a register or variable while leaving other bits unchanged.


2. Factors Determining the Size of the Bitmask

a. The Data Type of the Variable or Register

  • The size of the bitmask is the same as the size (in bits) of the variable or register being manipulated.
  • Common sizes for microcontrollers like the ATmegaXX8:
    • 8 bits for uint8_t (e.g., GPIO registers like PORTB or DDRB).
    • 16 bits for uint16_t (e.g., timer registers).
    • 32 bits for uint32_t in more advanced microcontrollers (not typical for AVR).

b. Examples of Common Bitmask Sizes

  1. 8-bit Register Example: For registers like PORTB (8 bits):

    PORTB |= (1 << PB5);
    
    • 1 << PB5 creates a bitmask with a 1 in the 5th bit position (e.g., 00100000).
    • The bitmask is 8 bits wide because PORTB is 8 bits wide.
  2. 16-bit Register Example: For a 16-bit timer register (e.g., TCCR1):

    TCCR1 |= (1 << 15);
    
    • 1 << 15 creates a bitmask with a 1 in the 15th bit position (e.g., 1000000000000000).
    • The bitmask is 16 bits wide because TCCR1 is 16 bits wide.
  3. 32-bit Variable Example (if used): If manipulating a uint32_t variable:

    uint32_t reg = 0;
    reg |= (1 << 20);
    
    • 1 << 20 creates a bitmask with a 1 in the 20th bit position (e.g., 000000010000000000000000).
    • The bitmask is 32 bits wide because uint32_t is 32 bits wide.

c. Compiler and Architecture

  • For AVR microcontrollers, registers like PORTB are fixed-width (8 bits), so the compiler uses 8-bit operations and masks.
  • In higher-end processors (e.g., ARM), registers can be 16, 32, or even 64 bits, and the bitmask size matches the register width.

3. Special Notes About Bitmask Size

a. Left Shifting and Overflow

The size of the bitmask also depends on the size of the number 1 before shifting. For example:

  • If 1 is treated as an int (typically 16 or 32 bits wide on AVR):
    1 << PB5
    
    • This creates a 32-bit number with a 1 in the PB5 position.

b. Automatic Truncation

When the bitmask is applied to a smaller-width register or variable, the excess bits are automatically truncated:

  • Example:
    PORTB |= (1 << 8); // PORTB is only 8 bits wide!
    
    • The result is equivalent to:
      PORTB |= 0b00000000; // The 9th bit is ignored
      

c. Explicit Data Types

To ensure the bitmask fits the register size, it's common to use fixed-width types (uint8_t, uint16_t, etc.) to avoid unintentional truncation or overflow.


4. Summary

The size of the bitmask is determined by:

  1. The width of the variable or register being manipulated (e.g., PORTB is 8 bits wide, so its bitmask will be 8 bits).
  2. The data type of the 1 being shifted (e.g., int, uint8_t, etc.), though it will be truncated to match the width of the register.

Would you like a specific example to clarify further?