Embedded System (MBD)


Module 1 - Setup

Module 1 - Setup

1. Proteus Installation Tutorial

Step 1 : Install Proteus

If you haven't install. Then Install, remember from DSD.

Step 2 : Download the Arduino Library for Proteus

Download from here : https://drive.google.com/file/d/1LMYOUn39nAZBdGL0SR3n30pf-fRXeP-B/view

Step 3 : Install the Library

Go to the System Settings in System Tab :

And then choose where you extract the downloaded folder :

Step 4 : Install Arduino IDE

Go and Install Arduino IDE from here : https://www.arduino.cc/en/software

Module 1 - Setup

2. Proteus Simulation Tutorial

Step 1 : Setup the Arduino IDE

Go to Preferences :

Checklist the Show Verbose output during compile and upload :

Step 2 : Compile the Code

Compile the code using this button :

Find the Hex link on the output terminal :

Step 3 : Connect the Hex

Double click the Arduino on the Simulation :

Copy the hex to the Arduino on the Proteus Simulation :
\

Step 4 : Run the Code

COMPLETE

Module 2 - Introduction to AVR Assembly

Module 2 - Introduction to AVR Assembly

1. Introduction to AVR Assembly Language

Assembly is a low-level programming language that allows manipulation of every bit in memory, resulting in highly efficient and fast code. It has a strong one-to-one correspondence with the machine code instructions of the computer architecture.

On Arduino microcontrollers (specifically the ATmega328P), Assembly programming enables high-level control suitable for real-time systems and applications requiring complex mathematical processes.

Advantages of Using Assembly:

Disadvantages:

Module 2 - Introduction to AVR Assembly

2. ATmega328P Hardware & Memory Architecture

A. Memory Map

The ATmega328P memory map provides information on how the Microcontroller Unit (MCU) uses memory. Here is the address division:

Category Address Size Description
General Purpose Registers 0x0000 - 0x001F 32 x 8 bit Registers R0 - R31
I/O Registers 0x0020 - 0x005F 64 x 8 bit Accessible via IN/OUT instructions
Extended I/O Registers 0x0060 - 0x00FF 160 x 8 bit Additional I/O registers
Internal SRAM 0x0100 - 0x08FF 2048 x 8 bit Internal data memory

B. General Purpose Working Registers (GPR)

The AVR architecture has 32 general-purpose registers labeled R0 through R31. These registers function as temporary storage for data during processing and are directly connected to the ALU (Arithmetic Logic Unit).

Register Division:

Group Registers Characteristics
Lower Registers R0 - R15 Limited functionality. Cannot store immediate values directly (cannot use LDI instruction).
Upper Registers R16 - R31 More flexible. Can work with immediate data, allowing direct storage of bytes or words.

Pointer Registers:

The last six registers (R26 through R31) can be combined into 16-bit pointers for indirect memory addressing:

Pointer Name Low Register High Register Function
X Register R26 (XL) R27 (XH) Pointer for memory access
Y Register R28 (YL) R29 (YH) Pointer for memory access
Z Register R30 (ZL) R31 (ZH) Pointer for memory & flash access
Module 2 - Introduction to AVR Assembly

3. Input/Output (I/O) Programming

On the Arduino Uno (ATmega328P), digital I/O is controlled through Port B, Port C, and Port D. Each port is 8-bit, allowing control of up to 8 pins simultaneously.

A. Port to Arduino Pin Mapping

Port Bits Arduino Pin Notes
Port B PB0 - PB5 Digital Pin 8 - 13 PB6-PB7 are used for crystal oscillator
Port C PC0 - PC5 Analog Pin A0 - A5 PC6 is the RESET pin
Port D PD0 - PD7 Digital Pin 0 - 7 PD0 (RX) and PD1 (TX) for serial communication

B. Main I/O Registers

Three main registers control the behavior of each port:

Register Full Name Access Function
DDRx Data Direction Register Read/Write Configures pin direction. 0 = Input, 1 = Output
PORTx Data Register Read/Write If Output: Sets logic High (1) or Low (0). If Input: Activates internal Pull-up resistor (1) or Tri-state (0)
PINx Input Pins Address Read Only Reads the physical logic state of the pin (0 or 1)

(Replace 'x' with Port name, e.g., DDRB, PORTB, PINB)

C. Register Bit Configuration Details

DDRx - Data Direction Register

DDRx Bit Value Pin Direction Explanation
0 Input Pin is configured as input (high impedance)
1 Output Pin is configured as output (source/sink current)

PORTx - Data Register (Depends on DDRx Configuration)

DDRx PORTx Mode Pin Condition
0 (Input) 0 Tri-state (Hi-Z) Pin is floating, no pull-up
0 (Input) 1 Input Pull-up Internal pull-up resistor active, pin defaults to HIGH
1 (Output) 0 Output Low Pin outputs 0V (GND)
1 (Output) 1 Output High Pin outputs 5V (VCC)

PINx - Input Pins Register

PINx Bit Value Pin Status Explanation
0 LOW Pin voltage is below threshold (near 0V)
1 HIGH Pin voltage is above threshold (near 5V)
Module 2 - Introduction to AVR Assembly

4. Assembly Integration with Arduino IDE

To combine Assembly with Arduino C++ code, the extern "C" directive is used in the .ino file and the .global directive is used in the .S (Assembly) file.

File Structure:

.ino File (C/C++):

extern "C" {
  void start();    // Declaration of function defined in Assembly
  void loop_asm(); // Another function from Assembly
}

void setup() {
  start();         // Call Assembly function for initialization
}

void loop() {
  loop_asm();      // Call Assembly function for main loop
}

.S File (Assembly):

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global start
.global loop_asm

start:
    SBI DDRB, 5      ; Set PB5 (Pin 13) as Output
    RET              ; Return to caller

loop_asm:
    SBI PORTB, 5     ; Turn on LED
    ; ... other code
    RET

Directive Explanations:

Module 2 - Introduction to AVR Assembly

5. AVR Assembly Instruction Set

Operand Notation

Before diving into the instructions, here are the common operand symbols used:

Symbol Description
Rd Destination register (R0-R31). The result of the operation is stored here.
Rr Source register (R0-R31). Used as input for the operation.
K Constant/Immediate value (8-bit: 0-255 or 0x00-0xFF).
k Address constant for SRAM or program memory.
A I/O register address (0-63 for IN/OUT, 0-31 for SBI/CBI).
b Bit number (0-7) within a register or I/O address.
X, Y, Z Pointer registers (X=R27:R26, Y=R29:R28, Z=R31:R30).

Note: Some instructions only work with upper registers (R16-R31), such as LDI, ANDI, ORI, SUBI, SBCI, and CPI.

A. Data Transfer Instructions

Used to move data between registers or between registers and memory/I/O.

Mnemonic Operand Description Example Notes
LDI Rd, K Load Immediate LDI R16, 0xFF Loads 8-bit constant K into register Rd (R16-R31 only)
MOV Rd, Rr Move/Copy Register MOV R0, R1 Copies contents of register Rr to Rd
IN Rd, A Input from I/O IN R16, PINB Reads data from I/O port A to register Rd
OUT A, Rr Output to I/O OUT PORTB, R16 Sends data from register Rr to I/O port A
LDS Rd, k Load from SRAM LDS R16, 0x0100 Loads data from SRAM address k to register Rd
STS k, Rr Store to SRAM STS 0x0100, R16 Stores register Rr contents to SRAM address k
LD Rd, X/Y/Z Load Indirect LD R16, X Loads data from address pointed by pointer X/Y/Z
ST X/Y/Z, Rr Store Indirect ST X, R16 Stores data to address pointed by pointer X/Y/Z
PUSH Rr Push to Stack PUSH R16 Saves register to stack
POP Rd Pop from Stack POP R16 Retrieves data from stack to register

B. Bit Manipulation Instructions (I/O Specific)

These instructions operate on the lower 32 I/O addresses ($00-$1F). Very efficient for changing one bit without affecting other bits.

Mnemonic Operand Description Example Notes
SBI A, b Set Bit in I/O SBI DDRB, 5 Sets bit b in I/O register A to 1
CBI A, b Clear Bit in I/O CBI PORTB, 5 Clears bit b in I/O register A to 0
BST Rr, b Bit Store to T BST R16, 3 Copies bit b from register Rr to T flag
BLD Rd, b Bit Load from T BLD R17, 5 Copies T flag to bit b of register Rd

C. Arithmetic Instructions

Mnemonic Operand Description Example Notes
ADD Rd, Rr Add ADD R1, R2 Rd = Rd + Rr
ADC Rd, Rr Add with Carry ADC R1, R2 Rd = Rd + Rr + C (Carry flag)
SUB Rd, Rr Subtract SUB R16, R17 Rd = Rd - Rr
SBC Rd, Rr Subtract with Carry SBC R16, R17 Rd = Rd - Rr - C
SUBI Rd, K Subtract Immediate SUBI R16, 10 Rd = Rd - K (R16-R31 only)
SBCI Rd, K Subtract Immediate with Carry SBCI R17, 0 Rd = Rd - K - C
INC Rd Increment INC R16 Rd = Rd + 1
DEC Rd Decrement DEC R16 Rd = Rd - 1
MUL Rd, Rr Multiply Unsigned MUL R16, R17 R1:R0 = Rd × Rr (16-bit result)
MULS Rd, Rr Multiply Signed MULS R16, R17 R1:R0 = Rd × Rr (signed)
NEG Rd Negate (Two's Complement) NEG R16 Rd = 0x00 - Rd

D. Logic Instructions

Mnemonic Operand Description Example Notes
AND Rd, Rr Logical AND AND R1, R2 Rd = Rd AND Rr
ANDI Rd, K AND Immediate ANDI R16, 0x0F Rd = Rd AND K (masking)
OR Rd, Rr Logical OR OR R1, R2 Rd = Rd OR Rr
ORI Rd, K OR Immediate ORI R16, 0x80 Rd = Rd OR K
EOR Rd, Rr Exclusive OR EOR R16, R17 Rd = Rd XOR Rr
COM Rd One's Complement COM R16 Rd = 0xFF - Rd (inverts all bits)
CLR Rd Clear Register CLR R16 Rd = 0 (same as EOR Rd, Rd)
SER Rd Set Register SER R16 Rd = 0xFF (R16-R31 only)

E. Shift & Rotate Instructions

Mnemonic Operand Description Example Notes
LSL Rd Logical Shift Left LSL R16 Shift left, bit 0 = 0, bit 7 → Carry
LSR Rd Logical Shift Right LSR R16 Shift right, bit 7 = 0, bit 0 → Carry
ROL Rd Rotate Left through Carry ROL R16 Rotate left through Carry flag
ROR Rd Rotate Right through Carry ROR R16 Rotate right through Carry flag
ASR Rd Arithmetic Shift Right ASR R16 Shift right, bit 7 remains (preserve sign)
SWAP Rd Swap Nibbles SWAP R16 Swaps upper and lower 4-bits in register

F. Branch & Control Flow Instructions

Mnemonic Operand Description Example Notes
RJMP k Relative Jump RJMP loop Jump to label k (±2K words)
JMP k Jump JMP far_label Jump to 22-bit address (all memory)
RCALL k Relative Call RCALL delay Call subroutine relative to PC
CALL k Call CALL far_sub Call subroutine at 22-bit address
RET - Return RET Return from subroutine
RETI - Return from Interrupt RETI Return from interrupt handler
CP Rd, Rr Compare CP R16, R17 Compare Rd with Rr (updates flags)
CPI Rd, K Compare Immediate CPI R16, 5 Compare Rd with constant K
CPC Rd, Rr Compare with Carry CPC R17, R19 For multi-byte comparison
BREQ k Branch if Equal BREQ target Jump if Z flag = 1 (result equal)
BRNE k Branch if Not Equal BRNE loop Jump if Z flag = 0 (result not equal)
BRLO k Branch if Lower BRLO less Jump if C flag = 1 (unsigned <)
BRSH k Branch if Same or Higher BRSH greater Jump if C flag = 0 (unsigned ≥)
BRLT k Branch if Less Than BRLT neg Jump if S flag = 1 (signed <)
BRGE k Branch if Greater or Equal BRGE pos Jump if S flag = 0 (signed ≥)

G. Skip Instructions

Mnemonic Operand Description Example Notes
SBIS A, b Skip if Bit in I/O Set SBIS PINB, 0 Skip next instruction if bit = 1
SBIC A, b Skip if Bit in I/O Cleared SBIC PIND, 2 Skip next instruction if bit = 0
SBRS Rr, b Skip if Bit in Register Set SBRS R16, 7 Skip if bit b in register = 1
SBRC Rr, b Skip if Bit in Register Cleared SBRC R16, 0 Skip if bit b in register = 0

H. Other Instructions

Mnemonic Operand Description Example Notes
NOP - No Operation NOP Does nothing (1 clock cycle)
SLEEP - Sleep SLEEP Enters sleep mode (power saving)
WDR - Watchdog Reset WDR Resets watchdog timer
SBIW Rd, K Subtract Immediate from Word SBIW R24, 1 Subtract K from 16-bit value (R25:R24)
ADIW Rd, K Add Immediate to Word ADIW R24, 1 Add K to 16-bit value
Module 2 - Introduction to AVR Assembly

6. Status Register (SREG)

The Status Register contains flags that indicate the results of arithmetic/logic operations. This register is crucial for branch instructions.

Bit Name Description
7 I (Global Interrupt Enable) Enables/disables global interrupts
6 T (Bit Copy Storage) Storage for BLD/BST instructions
5 H (Half Carry Flag) Carry from bit 3 to bit 4 (for BCD)
4 S (Sign Flag) S = N ⊕ V (for signed operations)
3 V (Overflow Flag) Two's complement overflow
2 N (Negative Flag) Result is negative (bit 7 = 1)
1 Z (Zero Flag) Result = 0
0 C (Carry Flag) Carry/borrow from operation
Module 2 - Introduction to AVR Assembly

7. Delay Implementation Without Library

Delays can be created using nested loops that consume a certain number of clock cycles.

Delay Calculation Concept:

Delay Implementation Examples:

; Delay approximately 1 second (with nested loop)
delay_1s:
    LDI R18, 64          ; Outer counter
outer_loop:
    LDI R24, lo8(62500)  ; Inner counter low byte
    LDI R25, hi8(62500)  ; Inner counter high byte
inner_loop:
    SBIW R24, 1          ; Subtract 16-bit counter (2 cycles)
    BRNE inner_loop      ; Loop if not 0 (2 cycles if taken)
    DEC R18              ; Subtract outer counter
    BRNE outer_loop      ; Loop outer if not 0
    RET

; Simple delay with single loop
delay_simple:
    LDI R16, 255         ; Load counter
delay_loop:
    DEC R16              ; Decrement counter (1 cycle)
    BRNE delay_loop      ; Branch if not zero (2 cycles)
    RET                  ; Return (approximately 765 cycles total)
Module 2 - Introduction to AVR Assembly

8. Complete Program Examples

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global main

main:
    SBI DDRB, 5          ; Set PB5 (Pin 13) as Output

loop:
    SBI PORTB, 5         ; Turn on LED (Output HIGH)
    RCALL delay          ; Call delay subroutine
    CBI PORTB, 5         ; Turn off LED (Output LOW)
    RCALL delay          ; Call delay subroutine
    RJMP loop            ; Repeat continuously

delay:
    LDI R18, 82          ; Outer loop counter
outer:
    LDI R24, lo8(60000)  ; Inner loop counter (low byte)
    LDI R25, hi8(60000)  ; Inner loop counter (high byte)
inner:
    SBIW R24, 1          ; Subtract word (R25:R24)
    BRNE inner           ; Loop if not 0
    DEC R18              ; Subtract outer counter
    BRNE outer           ; Loop outer if not 0
    RET                  ; Return to caller

B. Reading Button and Controlling LED

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global main

main:
    ; Setup
    SBI DDRB, 5          ; PB5 (Pin 13) as Output (LED)
    CBI DDRD, 2          ; PD2 (Pin 2) as Input (Button)
    SBI PORTD, 2         ; Activate Pull-up on PD2

loop:
    SBIC PIND, 2         ; Skip next instruction if button pressed (LOW)
    RJMP led_off         ; If not pressed, turn off LED
    
led_on:
    SBI PORTB, 5         ; Turn on LED
    RJMP loop            ; Return to loop

led_off:
    CBI PORTB, 5         ; Turn off LED
    RJMP loop            ; Return to loop

C. Toggle LED with Button (Simple Debounce)

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global main

main:
    ; Initialization
    SBI DDRB, 5          ; PB5 as Output (LED)
    CBI DDRD, 2          ; PD2 as Input (Button)
    SBI PORTD, 2         ; Activate internal Pull-up
    CLR R20              ; R20 = LED status (0 = off)

wait_press:
    SBIC PIND, 2         ; Wait for button pressed (LOW)
    RJMP wait_press
    
    ; Button pressed - toggle LED
    SBRC R20, 0          ; Skip if bit 0 of R20 = 0 (LED off)
    RJMP turn_off
    
turn_on:
    SBI PORTB, 5         ; Turn on LED
    LDI R20, 1           ; Set status = on
    RJMP debounce

turn_off:
    CBI PORTB, 5         ; Turn off LED
    CLR R20              ; Set status = off

debounce:
    RCALL delay          ; Delay for debounce
    
wait_release:
    SBIS PIND, 2         ; Wait for button released (HIGH)
    RJMP wait_release
    RCALL delay          ; Delay debounce after release
    RJMP wait_press      ; Return to wait for press

delay:
    LDI R18, 50
d_outer:
    LDI R24, lo8(10000)
    LDI R25, hi8(10000)
d_inner:
    SBIW R24, 1
    BRNE d_inner
    DEC R18
    BRNE d_outer
    RET

Module 3 - Serial Port

Module 3 - Serial Port

Introduction to USART

1. USART Definition

USART (Universal Synchronous/Asynchronous Receiver/Transmitter) is a communication protocol used to transfer data between electronic devices, such as microcontrollers, sensors, and other components. This protocol is highly flexible as it supports two main modes:

2. UART vs. USART

While often used interchangeably, there is a technical difference:

3. Operating Modes

A. Asynchronous Mode

In this mode, the USART module transmits data without an external clock signal. Synchronization is achieved using data frames consisting of:

Characteristics: Suitable for long-distance communication or between devices that do not share the same clock.

B. Synchronous Mode

Uses a clock signal to synchronize data transfer between the transmitter and the receiver.

Characteristics: Ideal for multimedia applications or high-speed bulk data transfers.

4. Configuration and Baud Rate

To use USART, several parameters must be defined:

  1. Baud Rate: The speed of data transmission (bits per second).
  2. Data Format: The number of data bits, parity, and stop bits.
  3. Interrupt: Enables notifications when data is finished being sent or received.

5. USART and Arduino Serial Monitor

In the Arduino ecosystem (such as the Uno), the USART peripheral is the primary way the microcontroller communicates with your computer:

  1. Hardware Connection: The ATmega328P uses its USART pins (TX on Pin 1, RX on Pin 0). These are connected to an onboard USB-to-Serial converter chip.
  2. Serial Monitor: When you open the Serial Monitor or Serial Plotter in the Arduino IDE, it acts as the "Receiver" (RX) for data sent by the Arduino and the "Transmitter" (TX) for data you type in.
  3. Baud Rate Alignment: For communication to work, the baud rate selected in the Serial Monitor (e.g., 9600) must match the baud rate configured in your code. If they do not match, you will see "garbage" characters or no data at all.

In Proteus, follow these steps to simulate using a Virtual Terminal to mimic Serial Monitor functionality:

  1. Click on Virtual Instruments Mode in the left sidebar.
    picture 0
  2. Select the Virtual Terminal component and place it on the schematic.
    picture 1
  3. Add an Arduino Uno to your schematic. Then, connect the TX and RX pins of the Virtual Terminal to the RX and TX pins of the Arduino Uno respectively.
    picture 2
  4. Double-click on the Virtual Terminal to set the Baud Rate (e.g., 9600) to match your code.
    picture 3

Baud Rate Calculation Formula (UBRR)

The UBRR (USART Baud Rate Register) value is calculated based on the CPU clock frequency (F_CPU) and the desired Baud Rate:

UBRR = (F_CPU / (16 * BAUD)) - 1

Calculation Example:
If F_CPU = 16 MHz and the target BAUD = 9600 bps:

  1. UBRR = (16,000,000 / (16 * 9600)) - 1
  2. UBRR = 104.16 - 1
  3. UBRR ≈ 103 (Hex: 0x67)
Module 3 - Serial Port

USART Register Architecture of ATmega328p

The ATmega328p microcontroller uses several specific registers to control and monitor USART communication.

1. UBRR (USART Baud Rate Register)

picture 1

A 16-bit register that determines the communication speed. It is divided into two 8-bit registers:

The following table provides a reference for UBRR0 (USART Baud Rate Register) settings corresponding to standard baud rates (bps). It details the required register values for three common oscillator frequencies (f_osc): 16.0000 MHz, 18.4320 MHz, and 20.0000 MHz. For each frequency, the table accounts for both normal speed (U2Xn = 0) and double speed (U2Xn = 1) modes, including the resulting percentage error for each configuration.

picture 0

2. UDR (USART Data Register)

picture 2

An 8-bit register that serves a dual purpose:

3. UCSR0A (USART Control and Status Register A)

picture 3

Used to monitor communication status and configure the speed mode.

Bit Name Description
RXC0 Receive Complete Set to 1 if there is new unread data in the UDR.
TXC0 Transmit Complete Set to 1 if all data has been transmitted.
UDRE0 UDR Empty Set to 1 if the UDR register is empty and ready for new data.
FE0 Frame Error Occurs when there is an error in the stop bit.
DOR0 Data Overrun Occurs when new data arrives before old data is read.
UPE0 Parity Error Occurs when there is a parity error in the received data.
U2X0 Double Speed If set to 1, the transmission speed is doubled.
MPCM0 Multi-processor Enables multi-processor communication mode.

4. UCSR0B (USART Control and Status Register B)

picture 4

Used to enable the module and interrupts.

5. UCSR0C (USART Control and Status Register C)

picture 5

Used for frame format configuration and operating mode.

Note: When writing to UCSR0C, ensure bit configurations are performed carefully according to the communication protocol requirements of the target device.

Module 3 - Serial Port

Implementation and Assembly Code Examples

This page contains basic implementation examples of USART serial communication using the Assembly programming language on an AVR Microcontroller (ATmega328p).

1. Printing Text to Serial Monitor

This code is used to repeatedly send the string "Programming Serial Interface!" to the Serial Monitor via the USART port.

;------------------------
; Assembly Code - Print Text
;------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main

main:
    CLR   R24
    STS   UCSR0A, R24               ; Clear UCSR0A register
    STS   UBRR0H, R24               ; Clear UBRR0H
    LDI   R24, 103                  ; Set UBRR value = 103 (9600 Baud Rate)
    STS   UBRR0L, R24

    LDI   R24, (1<<RXEN0) | (1<<TXEN0) ; Enable RX and TX
    STS   UCSR0B, R24

    LDI   R24, (1<<UCSZ00) | (1<<UCSZ01) ; Mode: 8-bit data, 1 stop bit, No Parity
    STS   UCSR0C, R24

print_msg:
    LDI   R30, lo8(message)
    LDI   R31, hi8(message)         ; Z points to string message
agn:
    LPM   R18, Z+                   ; Load character into R18
    CPI   R18, 0                    ; Check if end of string (null)
    BREQ  ext                       ; If yes, exit loop

l1:
    LDS   R17, UCSR0A
    SBRS  R17, UDRE0                ; Wait until buffer is empty (UDRE0=1)
    RJMP  l1
    STS   UDR0, R18                 ; Send character to Serial Monitor
    RJMP  agn                       ; Loop to next character

ext:
    RCALL delay_sec                 ; Wait for a moment
    RJMP  print_msg                 ; Repeat string transmission

message:
    .ascii "Programming Serial Interface!"
    .byte 10, 13, 0

delay_sec:                          ; Delay Subroutine (~3 seconds)
    LDI   R20, 255
l4: LDI   R21, 255
l5: LDI   R22, 255
l6: DEC   R22
    BRNE  l6
    DEC   R21
    BRNE  l5
    DEC   R20
    BRNE  l4
    RET

2. Reading Input from Serial Monitor

This code reads characters sent from the Serial Monitor and controls an LED. If the character 'H' is received, the LED turns ON; if 'L' is received, the LED turns OFF.

;------------------------
; Assembly Code - Input Text and Control LED
;------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main

main:
    CLR   R24
    STS   UBRR0H, R24
    LDI   R24, 103
    STS   UBRR0L, R24
    LDI   R24, (1<<RXEN0 | 1<<TXEN0)
    STS   UCSR0B, R24
    LDI   R24, (1<<UCSZ01 | 1<<UCSZ00)
    STS   UCSR0C, R24

    SBI   DDRB, 5               ; Set PB5 as output

wait_input:
    ; 1. Check if a byte arrived
    LDS   R17, UCSR0A
    SBRS  R17, RXC0             ; Wait for Receive Complete
    RJMP  wait_input

    ; 2. Read the character into R18
    LDS   R18, UDR0

    ; 3. Check if character is 'H'
    CPI   R18, 'H'
    BREQ  led_on

    ; 4. Check if character is 'L'
    CPI   R18, 'L'
    BREQ  led_off

    RJMP  wait_input

led_on:
    SBI   PORTB, 5              ; Turn LED ON
    RJMP  wait_input

led_off:
    CBI   PORTB, 5              ; Turn LED OFF
    RJMP  wait_input

Module 4 - Arithmetic

From memory access, addressing modes, the SREG, to arithmetic operations in AVR assembly,

Module 4 - Arithmetic

1. Memory in AVR Architecture

AVR Architecture is an 8 bit single-chip RISC microcontroller with a modified Harvard Architecture that is organized as the following which causes it to behave certain ways when handling memory.

eclipse.umbc.edu - AVR Architecture

You don't have to memorize all this don't worry.

There are several memory spaces in this organization, but for now we will look at the two most important ones to know.

Program/Flash Memory

rhjcoding.com - Flash memory When you compile and upload codes (flashing), it's stored in the Flash Memory section semi-permanently. During runtime, the control unit fetches instructions from this memory section.

This portion of the memory is non-volatile which means the data stored won't be erased after power loss.

Data Memory - DS (Data Space)

rhjcoding.com - Data Memory The Data Memory consists of several memory parts mapped into one contiguous memory addresses.

The Static RAM (SRAM) is a memory block used to store data used during runtime. One part of this is the stack that may come in handy for temporary data (more on this later).

thecoderscorner.com - SRAM

Module 4 - Arithmetic

2. Addressing Modes

Due to how the AVR Architecture and its memory are organized, AVR instructions, including arithmetic instructions, must follow certain addressing methods. Addressing methods are how the control unit may access different data locations according to the instruction which are usually 16 - 32 bits in length. Those data locations may include general purpose registers, I/O registers, extended I/O registers, and the SRAM.

Single Register Direct (Rd)

eclipse.umbc.edu - Single Register Direct

Operates on a single general purpose register Rd with d being values 0 - 31 (R0 - R31). Data is read from the register Rd, operated on, then stored back into the same register.

Some instructions with this addressing mode are ROL Rd and COM Rd which rotates the bits in Rd and inverts the bits in Rd respectively.

Double Register Direct (Rd, Rr)

eclipse.umbc.edu - Double Register Direct

Unlike Single Register Direct, Double Register Direct operates on two general purpose registers: source Rr and destination Rd. Data is read from Rr which are then operated alongside Rd to be stored back into Rd.

Instructions such as ADD Rd, Rr and AND Rd, Rr operates on both Rd and Rr which then stores the results (Addition and bitwise AND) in Rd.

Immediate Mode (Rd, K)

A constant value K alongside a register Rd is provided in the instruction itself, therefore the data itself is stored as a constant inside the Flash Memory. The constant value is limited to 255 (0xFF, 8 bits) and the register used is limited to R16 - R31 (4 bits with an offset).

Instructions such as ORI Rd, K and LDI Rd, K operates with a constant value K then storing it into Rd (bitwise OR and Loading).

Notice how the instructions have 'I' in its names (stands for Immediate).

Some instructions that have W (Word) in its name operates on 16 bit words which are stored in two consecutive registers R[d + 1]:Rd. One instruction that operates like this is the ADIW Rd, K that adds a constant K to the 16 bit data in R[d + 1]:Rd. Word Immediate instructions can only operate on 4 even-indexed GP registers {R24, R26, R28, R30} and are limited to 6 bits constant values (0 - 63).

The 16 bit opcode for ADIW is [1001 0110 KKdd KKK] which limits K to 6 bits and d to 2 bits (4 different registers max).

I/O Direct (Rd/Rr, A)

eclipse.umbc.edu - I/O Direct

Operates with I/O memory address A. Headers like avr/io.h defines these memory addresses into readable constants such as PORTB, PINC, and 'DDRA'. This doesn't include Extended I/O addresses such as UBBR0H.

Instructions such as IN Rd, A and OUT A, Rr stores IO(A) into Rd and Rr into IO(A) respectively.

Data Direct (Rd/Rr, k)

eclipse.umbc.edu - Data Direct

Instructions with this addressing utilizes 16 bit value k (0 - 65535) which is an address that corresponds to a space in memory, including the Extended I/O addresses scuh as UBRR0H and UCSR0A.

One instruction with Data Direct addressing is LDS Rd, k which loads the value in address k to register Rd.

Data Indirect (Rd/Rr, X/Y/Z +/- q)

eclipse.umbc.edu - Data Indirect

Registers X, Y, and Z corresponds to certain 16 bit register pairs in the memory.

Register Higher Byte Lower Byte
X R27 R26
Y R29 R28
Z R31 R30

Instructions with Data Indirect addressing operates on data stored in the address stored in the X/Y/Z registers. For example, if X contains the value 0x01FF, then LD Rd, X would load the data value stored in address 0x01FF into Rd.

A feature of Data Indirect addressing is pre/post increment/decrement operators. By adding '+' or '-' next to the X/Y/Z register (eg: X+), the address stored would be automatically incremented or decremented respectively. This operation can be pre-ordered or post-ordered meaning the address value change would happen before or after the instruction itself.

As an example, assume Y = 0x02. Instruction LD Rd, Y+ would store the value DS(Y) into Rd THEN increments Y to be 0x03. As opposed to this, putting the increment operator before Y (LD Rd, +Y) would first increment the value of Y to be 0x03 THEN loads the data in the newly updated address into Rd.

This may come in handy when operating with arrays stored as blocks of consecutive memory.

Certain instructions also allows accessing memory with displacement q on pointer registers Y/Z (X gak diajak). These instructions, usually ending with D, such as STD Y+3, Rd would offset the address stored in Y by 3 which stores the value in Rd into address DS(Y+3).


To see which instructions uses which memory addressing methods, refer to the AVR Instruction Set Manual. Knowing which instructions utilizes which memory addressing method would help you choose the best instruction to use in different situations when handling arithmetic and logic operations.

Module 4 - Arithmetic

3. The Status Register

The Status Register (SREG) is a special 8 bit register that saves different operational status flags in each bit. Different operations may affect different flags (bit) of the register which then would be useful to create decisions after.

In AVR architecture, SREG is an I/O register meaning that it can be operated with instructions such as OUT and IN.

Carry Flag (C) [0]

Zero Flag (Z) [1]

Negative Flag (N) [2]

Two's Complement Overflow Flag (V) [3]

Sign Flag (S) [4]

Half Carry (H) [5]

Bit Copy Storage (T) [6]

Global Interrupt Enable (I) [7]

Module 4 - Arithmetic

4. Advanced Arithmetic Operations

As a refresher, here are some fundamental AVR arithmetic and logical instructions.

Mnemonic Operand Description Example Notes
ADD Rd, Rr Add ADD R1, R2 Rd = Rd + Rr
ADC Rd, Rr Add with Carry ADC R1, R2 Rd = Rd + Rr + C (Carry flag)
ADIW Rd, K Add Immediate to Word ADIW R24, 40 R[d+1]:Rd = R[d+1]:Rd + K
SUB Rd, Rr Subtract SUB R16, R17 Rd = Rd - Rr
SBC Rd, Rr Subtract with Carry SBC R16, R17 Rd = Rd - Rr - C
SUBI Rd, K Subtract Immediate SUBI R16, 67 Rd = Rd - K
SBIW Rd, K Substract Immediate from Word SBIW R24, 40 R[d+1]:Rd = R[d+1]:Rd - K
SBCI Rd, K Subtract Immediate with Carry SBCI R17, 0 Rd = Rd - K - C
INC Rd Increment INC R16 Rd = Rd + 1
DEC Rd Decrement DEC R16 Rd = Rd - 1
MUL Rd, Rr Multiply Unsigned MUL R16, R17 R1:R0 = Rd × Rr (16-bit result)
MULS Rd, Rr Multiply Signed MULS R16, R17 R1:R0 = Rd × Rr (signed)
NEG Rd Negate (Two's Complement) NEG R16 Rd = 0x00 - Rd
AND Rd, Rr Logical AND AND R1, R2 Rd = Rd AND Rr
ANDI Rd, K AND Immediate ANDI R16, 0x0F Rd = Rd AND K
OR Rd, Rr Logical OR OR R1, R2 Rd = Rd OR Rr
ORI Rd, K OR Immediate ORI R16, 0x80 Rd = Rd OR K
EOR Rd, Rr Exclusive OR EOR R16, R17 Rd = Rd XOR Rr
COM Rd One's Complement COM R16 Rd = 0xFF - Rd (inverts all bits)
NEG Rd Two's Complement NEG R16 Rd = -Rd (Signed)
CLR Rd Clear Register CLR R16 Rd = 0
SER Rd Set Register SER R16 Rd = 0xFF (R16-R31 only)

Register Pair Arithmetic

To perform arithmetic operations with 16 bit words (0 - 65535), we can utilize the Carry Flag to perform ripple carry arithmetic operations.

16 Bit Word Addition

Suppose you want to add two big 16 bit numbers 26983 and 4882 stored in two register pairs R16:R17 and R18:R19. You can add the lower byte first then add the higher byte with a carry addition.

; DEC 26983 = HEX 0x6967
LDI R16, 0x69   ; upper byte
LDI R17, 0x67   ; lower byte

; DEC 4882 = HEX 0x1312
LDI R18, 0x13   ; upper byte
LDI R19, 0x12   ; lower byte 

ADD R17, R19    ; add lower byte
ADC R16, R18    ; add upper byte + carry from lower

; R16:R17 = 31865

16 Bit Word Substraction

A similar thing can be done for substracting two 16 bit numbers.

; DEC 26983 = HEX 0x6967
LDI R16, 0x69   ; upper byte
LDI R17, 0x67   ; lower byte

; DEC 4882 = HEX 0x1312
LDI R18, 0x13   ; upper byte
LDI R19, 0x12   ; lower byte 

SUB R17, R19    ; substract lower byte
SBC R16, R18    ; substract upper byte - borrow (C) from lower

; R16:R17 = 22101

Immediate Arithmetic

There are 3 immediate arithmetic instructions: ADIW, SBIW, and SUBI. In AVR there is no "Add with immediate" instruction. To do so, you can utilize the SUBI Rd, K instruction to substract a negative immediate number to achieve addition.

Suppose you want to add 67 to 61 stored in R16. You can do the following

LDI     R16, 61
SUBI    R16, -67 ; - -61 = + 61
; R16 = 128

Notice how there are only immediate instructions for word addition and substraction? Recall that word immediate addressing (ADIW & SBIW) can only operate on 6 bit values (0 - 63). How can we perform immediate arithmetic operations on bigger numbers like 26983?

Word Immediate Arithmetic

To perform addition/substraction operation with immediate numbers greater than 63, we simply split the word operation into two byte immediate operations similar to the previously explained 16 bit operations.

16 Bit Immediate Addition

; DEC 4882 = HEX 0x1312
LDI R16, 0x13       ; upper byte
LDI R17, 0x12       ; lower byte

.EQU num, 0x5457    ; immediate constant DEC 21591
SUBI R17, lo8(-num) ; 0x12 - -0x57 = 0x12 + 0x57         ; add lower byte
SBCI R16, hi8(-num) ; 0x13 - -0x54 - C = 0x13 + 0x54 + C ; add upper byte
; R16:R17 = 26473

16 Bit Immediate Substraction

LDI R16, 0x67       ; yeah you get the idea atp
LDI R17, 0x69

.EQU num, 0x1312
SUBI R17, lo8(num)  ; this time we simply just substract it
SBCI R16, hi8(num)  ; no need to be negative :)
; R16:R17 = 26983

Multiplying Numbers

There are multiple multiplication instructions in AVR, each supporting different data formats such as MUL Rd, Rr for unsigned numbers, MULS Rd, Rr for signed numbers, and MULSU Rd, Rr for multiplying a signed with an unsigned number.

Generally, the way they behave are similar: they multiply Rd with Rr then storing a 16 bit result in R1:R0.

LDI R16, 23
LDI R17, 9
MUL R16, R17
; R1:R0 = 23 * 9 = 207 (16 bits wide)
Module 4 - Arithmetic

5. The Stack

The Stack is a memory section of the SRAM that follows First-In-First-Out (FIFO) principles. It is generally used to store temporary data for quick and easy access.

The top of the stack is tracked by the 16 bit (adjusting to memory addressing) Stack Pointer that stores the address. It increments and decrements accordingly.

rjhcoding.com - Stack pointer

The stack grows downwards meaning that newer data is placed on lower memory address.

The top, is in fact, the bottom

The stack is used during subroutine (think of functions) calling to store the previous program counter when called with RCALL or ICALL. The program then restores the program counter to return to the previous location with RET.

Aside from subroutines, you can utilize the stack for your own use. The following are instructions that affects the stack.

Instruction Stack Pointer Description
PUSH Rd SP = SP - 1 Value in Rd is pushed onto the top of the stack.
POP Rr SP = SP + 1 The top of the stack is popped into Rr
RCALL ICALL SP = SP - 2 PC is stored onto the stack. Program jumps to the subroutine.
RET RETI SP = SP + 2 The top of the stack is popped back into PC. The program returns to the caller address.

Push Operation

rjhcoding.com - Push

Pop Operation

rjhcoding.com - Pop

Using Stacks

A very useful use case for stacks is as temporary storage without using up temporary registers to reduce mental overhead. For example, you might swap the contents of two registers R16 and R17.

PUSH R16        ; temporarily store R16
MOV  R16, R17   ; overwrite R16 with R17
POP  R17        ; return the temporary data to R17

This form of temporary container may become handy as your program grows. Even though AVR provides 32 general purpose registers, keeping track each and every single one of them may become harder as your program grows with more subroutines. One subroutine might unintentionally modify a register that you were using during its execution which might produce unexpected results.

For example, given a subroutine that interact with USART using R24

SER_send_byte:
  LDS   R24, UCSR0A     ; R24 is filled with the content of UCSR0A
  SBRS  R24, UDRE0                     
  ...                
  RET

Calling it from main would unexpectedly modify R24

main:
  LDI   R24, 0b00001010
  ...
  RCALL SER_send_byte
  OUT   TCCR0B, R24      ; this behaviour might be unexpected since R24 is no longer the same

Aside from giving proper documentations to subroutines which provides information on affected registers, the stack can be used to store values in affected register and retrieve it before returning.

; Sends R16 to USART
; Blocks while UDRE0 is not ready.
; Registers Affected: R24
SER_send_byte:
  PUSH  R24             ; stores R24
  LDS   R24, UCSR0A
  SBRS  R24, UDRE0                      
  RJMP  SER_send_byte                   
  STS   UDR0, R16
  POP   R24             ; retrieve the value back to R24                   
  RET

Do keep in mind that this would add an overhead by using additional cycles and memory for storing into the stack.

Module 4 - Arithmetic

6. Printing Bytes as Hexadecimal Values

To easily debug and view numbers, we can create a subroutine that outputs numbers into serial. Since our architecture uses 8 bits, it is easier to print bytes as two hexadecimal values 0x00 - 0xFF by splitting the upper and lower nibbles. In this page, we will create a subroutine that prints R16 as hexadecimal to serial.

Given the following subroutines to use the serial:

; Initializes USART 
; Registers Affected: R24
SER_init:
  CLR   R24
  STS   UCSR0A, R24                     ; clear UCSR0A register
  STS   UBRR0H, R24                     ; clear UBRR0H register
  LDI   R24, 103                        ; store in UBRR0L 103 value
  STS   UBRR0L, R24                     ; to set baud rate 9600
  LDI   R24, 1 << RXEN0 | 1 << TXEN0    ; enable RXB & TXB
  STS   UCSR0B, R24
  LDI   R24, 1 << UCSZ00 | 1 << UCSZ01  ; asynch, no parity, 1 stop, 8 bits
  STS   UCSR0C, R24
  RET
; Prints character in R16 to USART
; Blocks while UDRE0 is not ready.
; Registers Affected: R24
SER_send_byte:
  LDS   R24, UCSR0A
  SBRS  R24, UDRE0                      ; test data buffer if data can be sent
  RJMP  SER_send_byte                   ; loop back if not ready
  STS   UDR0, R16                       ; sends R16 to USART
  RET

Printing Nibbles

A 4 bit nibble, the lower parts of a byte, can be mapped into hexadecimal values 0 - F. Adding '0' to the nibble would map values 0 - 9 to its respective '0' - '9' ASCII characters.

SER_nibble:
  ANDI  R16, 0x0F ; mask that removes the higher nibble
  SUBI  R16, -'0' ; add '0' to R16 to represent ASCII '0' - '9'. 
  ...

Just simply doing this, however, wouldn't work on values A - F since they are not continuously mapped right after '9' which would instead prints ':' for 10. To do so, we can check whether the resulting ASCII is greater than '9'. If so, we can then add it by 7 since 'A' is located 7 characters after '9'.

SER_nibble:
  PUSH  R16           ; preserves R16
  ANDI  R16, 0x0F     ; mask that removes the higher nibble
  SUBI  R16, -'0'     ; add '0' to R16 to represent ASCII '0' - '9'. 
  CPI   R16, '9' + 1  ; compare with '9' + 1 = ':'
  BRLT  print_nibble  ; if no problem just simply print the character
  SUBI  R16, -7       ; otherwise add with 7 first to adjust for 'A'
print_nibble:
  RCALL SER_send_byte
  POP   R16           ; retrieves R16
  RET

With this subroutine, we can print the lower nibble of R16

main:
  RCALL SER_init

  LDI   R16, 0x0C
  RCALL SER_nibble      ; prints C

  LDI   R16, 0x14       ; prints 4
  RCALL SER_nibble

Print Nibbles

Printing Bytes

We can expand this further by printing entire bytes by first printing the upper nibble followed by the lower nibble. In AVR there is an instruction SWAP Rd that swaps the upper 4 bits with the lower 4 bits of Rd.

LDI R16, 0x14
SWAP R16
RCALL SER_nibble       ; prints 1 instead

With this, we can complete the hexadecimal printing subroutine.

; Prints R16 as HEX to USART
; R16 is preserved.
SER_hex:
  SWAP R16            ; swap to get upper nibble first
  RCALL SER_nibble
  SWAP R16            ; revert the nibbles back for the lower one
  RCALL SER_nibble
  RET

main:
  RCALL SER_init
  LDI   R16, 0x3C
  RCALL SER_hex      ; prints 3C

Print Bytes

Module 4 - Arithmetic

7. 𝔗𝔥𝔢 ℭ𝔬𝔡𝔢

Literally me

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global main
main:
  RCALL SER_init

  LDI   ZH, hi8(opening_msg)
  LDI   ZL, lo8(opening_msg)
  RCALL SER_print

  ; 16 Bit Addition
  LDI   ZH, hi8(addition_msg)
  LDI   ZL, lo8(addition_msg)
  RCALL SER_print

  ; DEC 26983 = HEX 0x6967
  LDI R16, 0x69   ; upper byte
  LDI R17, 0x67   ; lower byte

  ; DEC 4882 = HEX 0x1312
  LDI R18, 0x13   ; upper byte
  LDI R19, 0x12   ; lower byte 

  ADD R17, R19    ; add lower byte
  ADC R16, R18    ; add upper byte + carry from lower

  ; R16:R17 = 31865
  RCALL SER_hex
  MOV R16, R17  ; print the lower byte this time
  RCALL SER_hex

  ; 16 Bit Substraction
  LDI   ZH, hi8(substraction_msg)
  LDI   ZL, lo8(substraction_msg)
  RCALL SER_print

  ; DEC 26983 = HEX 0x6967
  LDI R16, 0x69   ; upper byte
  LDI R17, 0x67   ; lower byte

  ; DEC 4882 = HEX 0x1312
  LDI R18, 0x13   ; upper byte
  LDI R19, 0x12   ; lower byte 

  SUB R17, R19    ; substract lower byte
  SBC R16, R18    ; substract upper byte - borrow (C) from lower

  ; R16:R17 = 22101
  RCALL SER_hex
  MOV R16, R17  ; print the lower byte this time
  RCALL SER_hex

  ; 16 Bit Immediate Addition
  LDI   ZH, hi8(iaddition_msg)
  LDI   ZL, lo8(iaddition_msg)
  RCALL SER_print

  LDI R16, 0x13       ; upper byte
  LDI R17, 0x12       ; lower byte

  .EQU num, 0x5457    ; immediate variable directive
  SUBI R17, lo8(-num) ; 0x12 - -0x57 = 0x12 + 0x57
  SBCI R16, hi8(-num) ; 0x13 - -0x54 - C = 0x13 + 0x54 + C

  RCALL SER_hex
  MOV R16, R17  ; print the lower byte this time
  RCALL SER_hex

  ; 16 Bit Immediate Substraction
  LDI   ZH, hi8(isubstraction_msg)
  LDI   ZL, lo8(isubstraction_msg)
  RCALL SER_print

  LDI R16, 0x67       ; yeah you get the idea atp
  LDI R17, 0x69

  .EQU num, 0x1312
  SUBI R17, lo8(num)  ; this time we simply just substract it
  SBCI R16, hi8(num)  ; no need to be negative :)

  RCALL SER_hex
  MOV R16, R17  ; print the lower byte this time
  RCALL SER_hex

  ; Multiplication
  LDI   ZH, hi8(multiplication_msg)
  LDI   ZL, lo8(multiplication_msg)
  RCALL SER_print

  LDI R16, 23
  LDI R17, 9
  MUL R16, R17

  MOV R16, R1
  RCALL SER_hex
  MOV R16, R0
  RCALL SER_hex

loop:
  RCALL loop

; Initializes USART 
; Registers Affected: R24
SER_init:
  CLR   R24
  STS   UCSR0A, R24                     ; clear UCSR0A register
  STS   UBRR0H, R24                     ; clear UBRR0H register
  LDI   R24, 103                        ; store in UBRR0L 103 value
  STS   UBRR0L, R24                     ; to set baud rate 9600
  LDI   R24, 1 << RXEN0 | 1 << TXEN0    ; enable RXB & TXB
  STS   UCSR0B, R24
  LDI   R24, 1 << UCSZ00 | 1 << UCSZ01  ; asynch, no parity, 1 stop, 8 bits
  STS   UCSR0C, R24
  RET

; Prints character in R16 to USART
; Blocks while UDRE0 is not ready.
; Registers Affected: R24
SER_send_byte:
  LDS   R24, UCSR0A
  SBRS  R24, UDRE0                      ; test data buffer if data can be sent
  RJMP  SER_send_byte                   ; loop back if not ready
  STS   UDR0, R16                       ; sends R16 to USART
  RET

; Prints entire message of data pointed in Z until string end (0)
; To fill Z with string message:
;   LDI   ZH, hi8(message)
;   LDI   ZL, lo8(message)
; Registers Affected: R16, R24 (SER_send_byte)
SER_print:
  LPM   R16, Z+                         ; load char of string onto R18
  CPI   R16, 0                          ; check if R16 = 0 (end of string)
  BREQ  exit_SER_print                  ; if yes, exit
  RCALL SER_send_byte                   ; send the character byte
  RJMP  SER_print                       ; loop back & get next character
exit_SER_print:
  RET

; Prints the lower nibble of R16
; Registers Affected: R24 (SER_send_byte)
SER_nibble:
  PUSH  R16           ; preserves R16
  ANDI  R16, 0x0F     ; mask that removes the higher nibble
  SUBI  R16, -'0'     ; add '0' to R16 to represent ASCII '0' - '9'. 
  CPI   R16, '9' + 1  ; compare with '9' + 1 = ':'
  BRLT  print_nibble  ; if no problem just simply print the character
  SUBI  R16, -7       ; otherwise add with 7 first to adjust for 'A'
print_nibble:
  RCALL SER_send_byte
  POP   R16           ; retrieves R16
  RET

; Prints R16 as HEX to USART
; R16 is preserved.
SER_hex:
  SWAP R16            ; swap to get upper nibble first
  RCALL SER_nibble
  SWAP R16            ; revert the nibbles back for the lower one
  RCALL SER_nibble
  RET

opening_msg:
    .byte 10,13 ; new line, carriage return
    .ascii "== Literally Einstein and Tesla =="
    .byte 0

addition_msg:
    .byte 10,13 ; new line, carriage return
    .ascii "16 Bit Addition: "
    .byte 0

substraction_msg:
    .byte 10,13 ; new line, carriage return
    .ascii "16 Bit Addition: "
    .byte 0

iaddition_msg:
    .byte 10,13 ; new line, carriage return
    .ascii "16 Bit Immediate Addition: "
    .byte 0

isubstraction_msg:
    .byte 10,13 ; new line, carriage return
    .ascii "16 Bit Immediate Substraction: "
    .byte 0

multiplication_msg:
    .byte 10,13 ; new line, carriage return
    .ascii "Multiplication: "
    .byte 0

Output

Module 4 - Arithmetic

8. References

“AVR ® Instruction Set Manual AVR ® Instruction Set Manual.” Available: https://ww1.microchip.com/downloads/en/DeviceDoc/AVR-InstructionSet-Manual-DS40002198.pdf

“Lecture 02 – AVR Architecture,” Umbc.edu, 2025. https://eclipse.umbc.edu/robucci/cmpe311/Lectures/L02-AVR_Archetecture/

“How the Arduino memory model works - for AVR · The Coders Corner,” Thecoderscorner.com, 2018. https://www.thecoderscorner.com/electronics/microcontrollers/efficiency/how-arduino-avr-memory-model-works/

“AVR Tutorials - Working With Registers R0 - R31,” www.rjhcoding.com. http://www.rjhcoding.com/avr-asm-registers.php

“Lecture 04 – AVR CPU Registers,” eclipse.umbc.edu. https://eclipse.umbc.edu/robucci/cmpe311/Lectures/L05-AVR_Addressing_Modes/

“AVR Tutorials - The Status Register,” www.rjhcoding.com. http://www.rjhcoding.com/avr-asm-sreg.php

“Assembly via Arduino - Unsigned Arithmetic Operations.” https://akuzechie.blogspot.com/2021/10/assembly-via-arduino-unsigned.html

“AVR Tutorials - Assembly Subroutines,” Rjhcoding.com, 2018. http://www.rjhcoding.com/avr-asm-functions.php

M. Reynolds, “AVR® Stack Register - Developer Help,” Microchip.com, 2023. https://developerhelp.microchip.com/xwiki/bin/view/products/mcu-mpu/8-bit-avr/structure/stack/ (accessed Feb. 25, 2026).

“Lecture 04 – AVR CPU Registers,” Umbc.edu, 2025. https://eclipse.umbc.edu/robucci/cmpe311/Lectures/L04-AVR_CPU_Registers/

Module 5 - Timer

Module 5 - Timer

1. Introduction to AVR Timers

1.1. Overview

The ATmega328P is a widely popular 8-bit microcontroller, serving as the "brain" for many embedded systems, most notably the Arduino Uno. Among its most critical peripherals are the Timers. These components allow the microcontroller to perform time-sensitive tasks without stalling the CPU, such as measuring time intervals, generating PWM (Pulse Width Modulation) signals, or triggering specific events at precise moments.

The ATmega328P is equipped with three internal timers:

Note: This module only covers Timer0 and Timer1. If you want to know more about Timer2, you can read the ATMega328p Documentation.

1.2. Technical Specifications Overview

image

While all three timers share similar logic, they differ in resolution and specific features.

Timer Resolution Common Use Case Pins
Timer0 8-bit Basic time-slicing for multitasking, polling loops, and simple hardware PWM. PD6 (OC0A), PD5 (OC0B)
Timer1 16-bit High-resolution input capture (measuring pulse width), precise frequency generation, and 16-bit event counting. PB1 (OC1A), PB2 (OC1B)
Timer2 8-bit Asynchronous clocking (using a 32kHz crystal on TOSC1/2), real-time counters, and PWM. PB3 (OC2A), PD3 (OC2B)
Module 5 - Timer

2. Operating Modes

2.1. Normal Mode

image

In Normal Mode, the timer acts as a simple up-counter. It starts from 0 and increments with every clock pulse (after passing through the prescaler) until it reaches its maximum value (0xFF for 8-bit, 0xFFFF for 16-bit.

Once it hits the maximum, it "rolls over" or overflows back to 0. Upon overflow, the Timer Overflow Flag (TOVn) is set, which can trigger an Interrupt Service Routine (ISR). This is perfect for tracking elapsed time by incrementing a software counter every time an overflow occurs.

Formula for Overflow Frequency:

where N is the prescaler value.

2.2. CTC (Clear Timer on Compare) Mode

image

CTC Mode is far more precise for generating specific frequencies. Instead of waiting for an overflow at the maximum possible value, the timer counts until it matches a value you pre-defined in the Output Compare Register (OCRnx). As soon as the match occurs, the timer clears itself (resets to 0).

You can configure the timer to "toggle" an output pin automatically when the match occurs, creating a perfect square wave without CPU intervention. For example, to generate a 1 kHz square wave using a 16 MHz clock and a 64 prescaler, you would calculate the required OCR value:

Using the values above:

2.3. PWM (Pulse Width Modulation) Mode

PWM Mode is used to simulate an analog output using digital signals. By rapidly switching a pin between HIGH and LOW, you can control the average power delivered to a component.

Module 5 - Timer

3. Timer0

3.1. TCNT0 (Timer/Counter 0 Register)

image

The TCNT0 register is the core component of the 8-bit TIMER0 module. It acts as the actual counter that holds the current timer value. The value of TCNT0 increments (or decrements in certain PWM modes) based on the selected clock source and prescaler.

Users can read from or write to this register at any time. Note that manually writing a value to TCNT0 while the timer is running can cause the timer to "miss" a Compare Match with the OCR0x registers. This happens because the hardware comparison occurs in the clock cycle following a TCNT0 update.

In PWM modes, TCNT0 is constantly compared against OCR0A and OCR0B. When the values match, the output pins (OC0A/OC0B) toggle, clear, or set, depending on the configuration.

3.2. TCCR0 (Timer/Counter 0 Control Register)

In the ATmega328P, the control functionality is split into two registers: TCCR0A and TCCR0B. Together, they define the timer's behavior, including waveform generation, output modes, and clock scaling.

3.2.1. TCCR0A (Control Register A)

image

3.2.1.1. COM0x1:0 Description

This table shows the COM0x1:0 functionality when the timer is in a non-PWM mode (normal or CTC):

COM0x1 COM0x0 Description
0 0 Normal port operation, OC0x disconnected.
0 1 Toggle OC0x on Compare Match.
1 0 Clear OC0x on Compare Match (Set output to low).
1 1 Set OC0x on Compare Match (Set output to high).

3.2.1.2. WGM02:0 Description

This table shows how the WGM02:0 bits affect the counting sequence of the counter, the source for maximum (TOP) counter value, and what type of waveform generation to be used:

WGM02 WGM01 WGM00 Timer/Counter Mode of Operation TOP Update of OCRx at TOV Flag Set on
0 0 0 Normal 0xFF Immediate MAX
0 0 1 PWM, phase correct 0xFF TOP BOTTOM
0 1 0 CTC OCRA Immediate MAX
0 1 1 Fast PWM 0xFF BOTTOM MAX
1 0 0 Reserved
1 0 1 PWM, phase correct OCRA TOP BOTTOM
1 1 0 Reserved
1 1 1 Fast PWM OCRA BOTTOM TOP
Notes:

3.2.2. TCCR0B (Control Register B)

image

3.2.2.1. CS02:0 Prescaler Settings

CS02 CS01 CS00 Description
0 0 0 No clock source (Timer/Counter stopped)
0 0 1 clk / 1 (No prescaling)
0 1 0 clk / 8 (From prescaler)
0 1 1 clk / 64 (From prescaler)
1 0 0 clk / 256 (From prescaler)
1 0 1 clk / 1024 (From prescaler)
1 1 0 External clock source on T0 pin. Clock on falling edge.
1 1 1 External clock source on T0 pin. Clock on rising edge.

3.3. TIFR0 (Timer/Counter 0 Interrupt Flag Register)

image

Module 5 - Timer

4. Timer1

4.1. TCNT1 (Timer/Counter Register)

image

TCNT1 is functionally the same as TCNT0, serving as the core counter for its respective module, with several significant additions and architectural differences. TCNT1 is a 16-bit register divided into two 8-bit register, TCNT1H (high byte) and TCNT1L (low byte). This allows it to count from 0 to 65,535, providing much higher precision and longer timing.

TCNT1 includes a specialized Input Capture feature not found in Timer0. When a signal event occurs on the ICP1 pin, the current value of TCNT1 is instantly copied into the ICR1 register. This is used to measure external pulse widths with high accuracy.

4.2. TCCR1 (Timer/Counter 1 Control Register)

4.2.1. TCCR1A (Control Register A)

image

4.2.1.1. COM1x1:0 Description

This table shows the COM1x1:0 functionality when the timer is in a non-PWM mode (normal or CTC):

COM1x1 COM1x0 Description
0 0 Normal port operation, OC1x disconnected.
0 1 Toggle OC1x on Compare Match.
1 0 Clear OC1x on Compare Match (Set output to low).
1 1 Set OC1x on Compare Match (Set output to high).

4.2.1.2. WGM13:0 Description

This table shows how the WGM13:0 bits affect the counting sequence of the counter, the source for maximum (TOP) counter value, and what type of waveform generation to be used:

Mode WGM13 WGM12 WGM11 WGM10 Timer/Counter Mode of Operation TOP Update of OCR1x at TOV1 Flag Set on
0 0 0 0 0 Normal 0xFFFF Immediate MAX
1 0 0 0 1 PWM, phase correct, 8-bit 0x00FF TOP BOTTOM
2 0 0 1 0 PWM, phase correct, 9-bit 0x01FF TOP BOTTOM
3 0 0 1 1 PWM, phase correct, 10-bit 0x03FF TOP BOTTOM
4 0 1 0 0 CTC OCR1A Immediate MAX
5 0 1 0 1 Fast PWM, 8-bit 0x00FF BOTTOM TOP
6 0 1 1 0 Fast PWM, 9-bit 0x01FF BOTTOM TOP
7 0 1 1 1 Fast PWM, 10-bit 0x03FF BOTTOM TOP
8 1 0 0 0 PWM, phase and frequency correct ICR1 BOTTOM BOTTOM
9 1 0 0 1 PWM, phase and frequency correct OCR1A BOTTOM BOTTOM
10 1 0 1 0 PWM, phase correct ICR1 TOP BOTTOM
11 1 0 1 1 PWM, phase correct OCR1A TOP BOTTOM
12 1 1 0 0 CTC ICR1 Immediate MAX
13 1 1 0 1 (Reserved)
14 1 1 1 0 Fast PWM ICR1 BOTTOM TOP
15 1 1 1 1 Fast PWM OCR1A BOTTOM TOP

4.2.2. TCCR1B (Control Register B)

image

4.2.2.1. CS02:0 Prescaler Settings

CS12 CS11 CS10 Description
0 0 0 No clock source (Timer/Counter stopped)
0 0 1 clk / 1 (No prescaling)
0 1 0 clk / 8 (From prescaler)
0 1 1 clk / 64 (From prescaler)
1 0 0 clk / 256 (From prescaler)
1 0 1 clk / 1024 (From prescaler)
1 1 0 External clock source on T0 pin. Clock on falling edge.
1 1 1 External clock source on T0 pin. Clock on rising edge.

4.2.3. TCCR1C (Control Register C)

image

4.3. TIFR1 (Timer/Counter 1 Interrupt Flag Register)

image

Module 5 - Timer

5. Delay Using Timers

5.1. Delay Calculation in Normal Mode

In Normal Mode, the timer always counts up to its maximum value and then overflows. To get a specific delay, you preload the TCNTn register with a starting value so it only has to count a specific number of steps before overflowing.

Here is the formula to find the required preload value:

For example, if we want to create a 1ms delay using Timer0 with 1/64 presacaler:

5.2. Delay Calculation in CTC Mode

In CTC (Clear Timer on Compare) Mode, the timer is much easier to use for delays because the hardware automatically resets the counter to zero when it reaches a target value stored in the OCRnx (Output Compare Register).

Here is the formula to find the value to put in OCRnx register:

For example, if we want to create a 1ms delay using Timer0 with 1/64 presacaler:

Module 5 - Timer

6. Der Code

6.1. Code Example 1 (Timer0)

This code toggles PD5 every 0.5s. The delay_timer0 subroutine uses Timer0 in CTC Mode with a 1024 prescaler and a compare value of 156, creating a 10ms hardware delay per call. This subroutine is called 50 times using software loop with R18 as the counter.

;------------------------
; Assembly Code
;------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main
;===============================================================
    main:
    LDI R16, 0b00100000 ; to toggle PD5
    LDI R17, 0b00000000
    ;--------------------------------------------
    SBI DDRD, 5         ; set PD5 for o/p
    OUT PORTD, R17      ; PD5 = 0
    ;--------------------------------------------
    LDI R18, 50         ; set loop counter
l1: RCALL delay_timer0  ; apply delay via timer0
    DEC R18
    BRNE l1             ; & go back & repeat
    ;--------------------------------------------
    EOR R17, R16        ; R17 = R17 XOR R16
    OUT PORTD, R17      ; toggle PD5
    LDI R18, 50         ; re-set loop counter
    RJMP l1             ; go back & repeat toggle
;===============================================================
delay_timer0:           ; ~10ms delay via Timer0
    ;---------------------------------------------------------
    CLR R20
    OUT TCNT0, R20      ; initialize timer0 with count=0
    LDI R20, 156
    OUT OCR0A, R20      ; OCR0 = 9
    LDI R20, 0b00000010
    OUT TCCR0A, R20
    LDI R20, 0b00000101
    OUT TCCR0B, R20     ; timer0: CTC mode, prescaler 1024
    ;---------------------------------------------------------
l2: IN R20, TIFR0       ; get TIFR0 byte & check
    SBRS R20, OCF0A     ; if OCF0=1, skip next instruction
    RJMP l2             ; else, loop back & check OCF0 flag
    ;---------------------------------------------------------
    CLR R20
    OUT TCCR0B, R20     ; stop timer0
    ;---------------------------------------------------------
    LDI R20, (1<<OCF0A)
    OUT TIFR0, R20      ; clear OCF0 flag
    RET

6.2. Code Example 2 (Timer1)

This code toggles PD5 every 0.5s (just like Code Example 1). The delay_timer1 subroutine uses Timer1 in Normal Mode with a 1024 prescaler and a preload value of 57724, creating a 500ms hardware delay per call.

;------------------------
; Assembly Code
;------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main
;===============================================================
main:
    LDI R16, 0b00100000 ; to toggle PD5
    LDI R17, 0b00000000
    ;---------------------------------------------
    SBI DDRD, 5         ; set PD5 for o/p
    OUT PORTD, R17      ; PD5 = 0
    ;---------------------------------------------
l1: RCALL delay_timer1  ; 0.5 sec delay via timer1
    ;---------------------------------------------
    EOR R17, R16        ; R17 = R17 XOR R16
    OUT PORTD, R17      ; toggle PD5
    LDI R18, 61         ; re-set loop counter
    RJMP l1             ; go back & repeat toggle
;===============================================================
delay_timer1:           ; 0.5 sec delay via timer1
;-------------------------------------------------------
    .EQU value, 57724   ; value to give 0.5 sec delay
    LDI R20, hi8(value)
    STS TCNT1H, R20
    LDI R20, lo8(value)
    STS TCNT1L, R20     ; initialize counter TCNT1 = value
    ;-------------------------------------------------------
    LDI R20, 0b00000000
    STS TCCR1A, R20
    LDI R20, 0b00000101
    STS TCCR1B, R20     ; normal mode, prescaler = 1024
    ;-------------------------------------------------------
l2: IN R20, TIFR1       ; get TIFR1 byte & check
    SBRS R20, TOV1      ; if TOV1=1, skip next instruction
    RJMP l2             ; else, loop back & check TOV1 flag
    ;-------------------------------------------------------
    LDI R20, 1<<TOV1
    OUT TIFR1, R20      ; clear TOV1 flag
    ;-------------------------------------------------------
    LDI R20, 0b00000000
    STS TCCR1B, R20     ; stop timer1
    RET

Module 6 - Interrupt

Module 6 - Interrupt

1. Introduction to Interrupt

An interrupt is a mechanism used in microcontroller programming to pause the execution of the current program and call a specific routine or function when a particular event occurs. These events, defined by the programmer, can range from a specific condition on an input pin to a timer overflow or other hardware-defined triggers. Once the routine or function finishes executing, the program resumes from the exact point where it was interrupted.

The Arduino Uno (ATMega328P) provides two primary types of interrupts: External Interrupts and Timer Interrupts. While their functions and triggers differ, they share the same goal: interrupting the main program flow to handle specific events immediately.


External Interrupt

An External Interrupt is triggered by a change in the voltage level on specific input pins of the microcontroller. On the Arduino Uno, there are two pins dedicated to external interrupts: Pin 2 and Pin 3. These are the most commonly used pins when implementing interrupts via the Arduino programming language.

External Interrupts: INT0 and INT1

While the Arduino IDE refers to these simply as Pin 2 and Pin 3, the ATMega328P datasheet identifies them as INT0 and INT1. These are hardware-level designations that correspond to specific physical pins.

Trigger Modes

Both INT0 and INT1 can be configured to trigger the Interrupt Service Routine (ISR) based on four specific signal states:

  1. LOW: Triggered whenever the pin is at a logic low level.
  2. CHANGE: Triggered whenever the pin changes value (High to Low or Low to High).
  3. RISING: Triggered specifically when the pin goes from Low to High.
  4. FALLING: Triggered specifically when the pin goes from High to Low.

Internal Interrupt

An Internal Interrupt is triggered by modules located inside the microcontroller itself. In the ATMega328P, these are generated by timers and are referred to as Timer Interrupts. A Timer Interrupt is specifically triggered by a timer overflow. The Arduino Uno features three internal timers: Timer0, Timer1, and Timer2. (For more details on how timers operate, please refer to the previous module).

To expand on the specific hardware components of the ATMega328P (the heart of the Arduino Uno), we need to look at how the microcontroller labels and manages these specific interrupt sources.

Internal Interrupts: Timer0, Timer1, and Timer2

The Arduino Uno has three hardware timers, each capable of generating interrupts. These are essential for tasks that require precise timing without blocking the void loop().

1. Timer0 (8-bit)
2. Timer1 (16-bit)
3. Timer2 (8-bit)
Module 6 - Interrupt

2. Interrupt Handler

On the ATMega328P microcontroller, there are three essential requirements that must met to enable an interrupt:

  1. Global Interrupt Enabled: Interrupts must be allowed to occur globally across any part of the program. While the Arduino environment typically enables this by default, it is important to verify this when using different microcontrollers or during troubleshooting.
  2. Individual Interrupt Enabled: Each specific interrupt must be permitted to occur via dedicated interrupt control registers.
  3. Interrupt Condition Met: The microcontroller must receive a signal (either internal or external) that matches the predefined trigger criteria.

To enable a microcontroller to process interrupts, several steps must be followed. This module organizes these steps according to their placement in the code for easier implementation. The first step involves preparing the Interrupt Handler the specific code that will run when the interrupt is triggered.

Microcontrollers use a specialized memory block called the Interrupt Vector Table. This table stores the memory addresses of the programs to be executed for each specific type of interrupt.

Vector No. Program Address Source Interrupt Definition
1 0x0000 RESET External pin, Power-on Reset, Brown-Out Reset, Watchdog System Reset
2 0x0002 INT0 External Interrupt Request 0
3 0x0004 INT1 External Interrupt Request 1
4 0x0006 PCINT0 Pin Change Interrupt Request 0
5 0x0008 PCINT1 Pin Change Interrupt Request 1
6 0x000A PCINT2 Pin Change Interrupt Request 2
7 0x000C WDT Watchdog Time-out Interrupt
8 0x000E TIMER2_COMPA Timer/Counter2 Compare Match A
9 0x0010 TIMER2_COMPB Timer/Counter2 Compare Match B
10 0x0012 TIMER2_OVF Timer/Counter2 Overflow
11 0x0014 TIMER1_CAPT Timer/Counter1 Capture EVent
12 0x0016 TIMER1_COMPA Timer/Counter1 Compare Match A
13 0x0018 TIMER1_COMPB Timer/Counter1 Compare Match A
14 0x001A TIMER1_OVF Timer/Counter1 Overflow
15 0x001C TIMER0_COMPA Timer/Counter0 Compare Match A
16 0x001E TIMER0_COMPB Timer/Counter0 Compare Match B
17 0x0020 TIMER0_OVF Timer/Counter0 Overflow
18 0x0022 SPI STC SPI Serial Transfer Complete
19 0x0024 USART_RX Usart Rx Complete
20 0x0026 USART_UDRE Usart Data Register Empty
21 0x0028 USART_TX Usart Tx Complete
22 0x002A ADC ADC Conversion Complete
23 0x002C EE READY EEPROM Ready
24 0x002E ANALOG COMP Analog Comparator

These addresses are fixed and cannot be changed. When an interrupt occurs, the microcontroller automatically jumps to the corresponding address in the Interrupt Vector Table to execute the associated program.

For example if an external interrupt (INT0) is triggered, the microcontroller will jump to address 0x0002 to execute the code defined in that location.

See the program address between INT0 and INT1, which are 0x0002 and 0x0004 respectively. The gap between these addresses is only 2 bytes, which is the size of a single instruction in AVR assembly language. This means that the interrupt handler for INT0 must be concise and fit within this limited space, often requiring the use of a jump instruction to redirect to a larger block of code if necessary.

Example:

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.org 0x0002	 ; external interrupt 0 vector
    rjmp toggle_led

.global main

main:
 ; your main program here

toggle_led:
    ; main interrupt logic here
    in r16, PORTB
    eor r16, (1<<PB5)
    out PORTB, r16
    reti ; return from interrupt

From the above example, we have initialized the handler for the interrupt, and then created a routine named toggle_led, which contains the code that will be executed when the interrupt is triggered. When an INT0 occurs (for example, when a button connected to the INT0 pin is pressed), the microcontroller will jump to the toggle_led routine. After performing the necessary actions, we need to inform the microcontroller that the interrupt has been successfully handled and to continue with the main program. We can use the keyword RETI, which stands for Return from Interrupt, to achieve this.

An interrupt will take priority over the main program, meaning that when an interrupt occurs, the microcontroller will temporarily halt the execution of the main program to execute the interrupt handler. Programmer must be cautious when writing interrupt handlers, as they should be efficient and not contain long-running code, to avoid delaying the main program for too long.

Using Vector Names

Instead of using raw addresses for interrupts, AVR assembly language provides predefined vector names that can be used to improve code readability. For example, instead of using .org 0x0002 for the INT0 interrupt, we can use the predefined name INT0_vect as follows:

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global INT0_vect ; declare the interrupt vector as global
.global main

main:
 ; your main program here

INT0_vect: ; Toggle LED
    in r16, PORTB
    ldi r17, (1 << 5)
    eor r16, r17 ; Toggle PB5
    out PORTB, r16
    reti

Pro tip: Just always use Vector Names instead of raw addresses :)

Module 6 - Interrupt

3. External Interrupt Registers

For detailed information about the registers, please refer to the Atmega32p datasheet. here

From the previous section, we have set up the interrupt handler for external interrupt 0. Now, we will set up the registers to setup/initialize this interrupt. Let's say we want to toggle an LED on pin PB5 with falling edge trigger. We can write the following code in our setup section:

#define __SFR_OFFSET 0x00
#include "avr/io.h"

.global main
.global INT0_vect

main:
    ; initialize external interrupt 0
    ldi r16, (1<<ISC01) ; Set ISC01 bit = 1 | trigger on falling edge
    sts EICRA, r16 ; write to EICRA register to set the interrupt trigger condition
    ldi r16, (1<<INT0) ; set INT0 bit = 1 | enable external interrupt 0
    sts EIMSK, r16 ; write to EIMSK register to enable the interrupt

    ; enable global interrupts
    sei

INT0_vect:
    ; main interrupt logic here
    in r16, PORTB
    ldi r17, (1 << 5)
    eor r16, r17 ; Toggle PB5
    out PORTB, r16
    reti

Let's break it down register by register:

EICRA (External Interrupt Control Register A)

EIMSK (External Interrupt Mask Register)

sei (Set Global Interrupt Enable)

This instruction enables global interrupts. It MUST be called after setting up the individual interrupt configurations to allow the microcontroller to respond to interrupts.


You should get the rough idea for INT1 as well. You just need to set the ISC11 and ISC10 bits in EICRA for the trigger condition and set the INT1 bit in EIMSK to enable it. Read the datasheet for more details on the trigger conditions for INT1.

Module 6 - Interrupt

4. Internal/Timer Interrupts

Internal Interrupts

Now Internal interrupts or Timer interrupts is somewhat more complex then external interrupts. Before going for implementation let's understand on what use case we can use timer interrupts:

For now we will try replacing delay loops with TIMER1 compare match interrupt. We will set up the timer to generate an interrupt every 5 seconds and toggle an LED in the interrupt handler.

#define __SFR_OFFSET 0
#include <avr/io.h>

.global main
.global TIMER1_COMPA_vect ; The linker looks for this specific name

main:
    ; Set PB5 as output
    sbi DDRB, 5

    ; Clear r16 to use as a zero register
    clr r16
    sts TCCR1A, r16

    ; Set prescaler to 1024 and enable CTC mode (Clear Timer on Compare)
    ; CTC mode is better for toggling so you don't have to manually reset TCNT1
    ldi r17, (1 << WGM12) | (1 << CS12) | (1 << CS10)
    sts TCCR1B, r17

    ; Reset timer count
    sts TCNT1H, r16
    sts TCNT1L, r16

    ; Set compare match value (62499 = 0xF423)
    ldi r17, 0xF4
    sts OCR1AH, r17
    ldi r17, 0x23
    sts OCR1AL, r17

    ; Enable timer compare match interrupt
    ldi r17, (1 << OCIE1A)
    sts TIMSK1, r17

    sei ; Enable global interrupts

loop:
    rjmp loop

TIMER1_COMPA_vect:
    ; toggle LED on PB5
    in r16, PORTB
    ldi r17, (1 << 5)
    eor r16, r17 ; Toggle PB5
    out PORTB, r16
    reti

TCCR1A: Timer/Counter Control Register A for Timer1

TCCR1B: Timer/Counter Control Register B for Timer1

TCNT1L and TCNT1H: Timer/Counter Register for Timer1

OCR1AL and OCR1AH: Output Compare Register for Timer1

TIMSK: Timer/Counter Interrupt Mask Register

sei: Set Global Interrupt Enable


TO DO: Leaern more about timer interrupts (TIMER0, TIMER2) from the official datasheet

Module 7 - PWM and EEPROM

Module 7 - PWM and EEPROM

1. PWM (Pulse Width Modulation)

PWM is a technique used to simulate analog output using a digital signal. Instead of producing a true analog voltage, the microcontroller rapidly switches a pin between HIGH and LOW.

Term Meaning
Period Total time of one cycle
Duty Cycle Percentage of time signal is HIGH

Changing the duty cycle changes the average voltage seen by external devices.

AVR microcontrollers provide hardware timers that can automatically generate PWM signals in the background. It basically switches on and off on certain positions of the counter. To generate more finely controlled PWM output (such as those for servos), Timer1 is usually used as it is 16 bits thus allowing more precise features.

Phase Correct PWM

Phase correct PWM uses dual slop that goes up and down, switching between those operations. This mode performs more accurately in exchange for half the available frequency.

Fast PWM

Fast PWM provides PWM with higher frequency but lower resolution because it uses a single-slope operation, where the counter immediately returns to 0 after reaching its maximum value. Fast PWM is commonly used for devices that require high-frequency signals.

TCCRnA and TCCRnB — Timer/Counter Control Registers

TCR1A

TCR1B

COM

COM1x1 COM1x0 Description
0 0 Normal port operation (PWM disconnected)
0 1 Toggle OC1X on Compare Match (special case)
1 0 Clear on Compare Match, Set at BOTTOM (Non-inverting PWM)
1 1 Set on Compare Match, Clear at BOTTOM (Inverting PWM)

WGM

Certain modes in the Timer that affects PWM behavior

Mode WGM13 WGM12 WGM11 WGM10 Mode Name TOP
1 0 0 0 1 PWM Phase Correct 8-bit 0x00FF
2 0 0 1 0 PWM Phase Correct 9-bit 0x01FF
3 0 0 1 1 PWM Phase Correct 10-bit 0x03FF
5 0 1 0 1 Fast PWM 8-bit 0x00FF
6 0 1 1 0 Fast PWM 9-bit 0x01FF
7 0 1 1 1 Fast PWM 10-bit 0x03FF
8 1 0 0 0 PWM Phase & Frequency Correct (ICR1) ICR1
9 1 0 0 1 PWM Phase & Frequency Correct (OCR1A) OCR1A
10 1 0 1 0 PWM Phase Correct (ICR1) ICR1
11 1 0 1 1 PWM Phase Correct (OCR1A) OCR1A
14 1 1 1 0 Fast PWM (ICR1 TOP) ICR1
15 1 1 1 1 Fast PWM (OCR1A TOP) OCR1A

OCRn — Output Compare Register

The Output Compare Register stores a value that the timer constantly compares against the counter. When the counter hits that value, it triggers compare match that does different things aaccording to COM to the set output pin.

ICRn — Input Capture Register

The Input Capture Register is used in certain PWM modes as the TOP value of the timer. This means the timer counts from 0 up to the value stored in ICRn before restarting the counting cycle.

When used as TOP, ICRn determines the PWM period (frequency).

In PWM modes that use ICRn as TOP (such as mode 8 & 15), the timer behavior becomes:

Counter: 0 → ICRn → reset

Changing ICRn changes the PWM frequency, while OCRn still controls the duty cycle.

Module 7 - PWM and EEPROM

2. Servos

Servos are motors that adjusts to certain angles following certain PWM pulses.

Servo Signal Operation

Servo operates with 20 ms PWM periods.

Parameter Typical Value
PWM Period ≈ 20 ms
Pulse Width ≈ 1 – 2 ms

Servo operates with 20 ms PWM periods. This can hardly be achieved with Timer0 since Timer0 is an 8-bit timer.

This means its maximum counter value is:

2^8 - 1 = 255

20 ms period is hardly achievable with this which requires a 16-bit timer is preferred.


This allows precise generation of long PWM periods.

Using a prescaler of 8:

Timer tick duration:

Required servo period: 20 ms

Number of timer counts required:


Servo Initialization Code Example

  SBI DDRB, 1

Configure OC1A pin as output. Timer1 will control this pin automatically.


LDI R22, (1<<COM1A1) | (1<<WGM11)
STS TCCR1A, R22

TCCR1A configuration:

Bit Purpose
COM1A1 = 1 Enable PWM output on OC1A
WGM11 = 1 Part of Fast PWM mode selection

This connects Timer1 to the output pin.


LDI R22, hi8(40000)
STS ICR1H, R22
LDI R22, lo8(40000)
STS ICR1L, R22

Set ICR1 = 40000.

This is to make the Timer 1 count to 40 000 following the previous calculations.

LDI R22, (1<<WGM13) | (1<<WGM12) | (1<<CS11)
STS TCCR1B, R22

TCCR1B configuration:

Bit Purpose
WGM13 + WGM12 Complete Fast PWM Mode 14
CS11 = 1 Prescaler = 8

This starts Timer1 in Fast PWM mode.

Servo Initialization

; Sets Timer1 PWM in PB1 to use period of 20ms by using Fast PWM.
; Timer1 counts until ICR1
SERVO_init:
  SBI DDRB, 1

  LDI R22, (1<<COM1A1) | (1<<WGM11)
  STS TCCR1A, R22

  LDI R22, hi8(40000)
  STS ICR1H, R22
  LDI R22, lo8(40000)
  STS ICR1L, R22

  LDI R22, (1<<WGM13) | (1<<WGM12) | (1<<CS11)
  STS TCCR1B, R22

  RET

Servo Usage

  .EQU LEFT, 2400  ; Generates a 2400/40000 % duty cycle. 

  LDI R16, hi8(LEFT)
  STS OCR1AH, R16
  LDI R16, lo8(LEFT)
  STS OCR1AL, R16

This code makes the PWM generate compare match at count 2400 this allows it to produce a 2400/40000 * 20 ms = ~1.2 ms pulse which is around 0 degrees.

Module 7 - PWM and EEPROM

3. EEPROM

EEPROM is a small non-volatile memory inside the microcontroller.

Unlike SRAM:

EEPROM is commonly used to store:

EEPROM has limited write endurance, so it should only be used sparingly


EEPROM Registers

EEPROM operations are controlled using dedicated I/O registers.


EEARH:EEARL — EEPROM Address Register

Stores the memory address to be accessed.


EEDR — EEPROM Data Register

Stores the data to be written or read.


EECR — EEPROM Control Register

Controls read and write operations.

Important control bits:

Name Bit Function
EERE 0 EEPROM Read Enable
EEWE 1 EEPROM Write Enable
EEMWE 2 EEPROM Host Write Enable

EEPROM Write Operation

Writing EEPROM requires a specific sequence:

  1. Wait until EEWE becomes ‘0’.
  2. Write new EEPROM address to EEAR (optional).
  3. Write new EEPROM data to EEDR (optional).
  4. Write a logical ‘1’ to the EEMWE bit while writing a ‘0’ to EEWE in EECR.
  5. Within four clock cycles after setting EEMWE, write a logical ‘1’ to EEWE.
; Writes R21 to EEPROM address in 0x0052
EEPROM_write:
  SBIC  EECR, 1
  RJMP  EEPROM_write

  LDI R22, hi8(0x0052)
  OUT   EEARH, R22
  LDI R22, lo8(0x0052)
  OUT   EEARL, R22

  OUT   EEDR, R21

  SBI   EECR, 2
  SBI   EECR, 1
  RET

EEPROM Read Operation

Reading EEPROM is simpler:

  1. Load address into EEAR
  2. Start read operation by setting EERE
  3. Read data from EEDR
; Read R21 from EEPROM address in 0x0052
EEPROM_read:
  SBIC  EECR, 1
  RJMP  EEPROM_read

  LDI R22, hi8(0x0052)
  OUT   EEARH, R22
  LDI R22, lo8(0x0052)
  OUT   EEARL, R22

  SBI   EECR, 0
  IN    R21, EEDR
  RET
Module 7 - PWM and EEPROM

4. References

PWM Basics

PWM Overview

PWM Docs

Servo Basics

Wok Servo Wok

EEPROM

EEPROM Docs

Modul 8: ADC (Analog to Digital Conversion)

Amba to Digital

Modul 8: ADC (Analog to Digital Conversion)

1. Analog vs Digital Signal

1.1 Analog Signals

Analog signals are signals that are continuous — meaning their values can change smoothly without jumps, representing physical quantities from the real world such as temperature, light, sound, and pressure.

Characteristics of analog signals:

image

1.2 Digital Signals

Digital signals are signals that are discrete — their values can only be in two conditions: HIGH (1) or LOW (0), usually in the form of a square wave.

Characteristics of digital signals:

image

1.3 Comparison of Analog and Digital

Aspect Analog Signal Digital Signal
Nature Continuous Discrete
Values All real values 0 or 1
Noise Resistance Low Very high
Primary Use Sensors, real-world actuators Data processing, computing
Examples Microphone sound, LDR output, sensor temperature Serial data, clock signals, PWM
Energy Consumption Relatively larger More efficient

Both complement each other: analog signals capture real-world phenomena accurately, then are converted to digital so they can be processed by computers/microcontrollers.

Modul 8: ADC (Analog to Digital Conversion)

2. Analog to Digital Converter (ADC)

2.1 Understanding ADC

ADC (Analog to Digital Converter) is a component or circuit that converts analog signals (continuous values) into a digital representation (discrete values in the form of binary numbers) so they can be processed by digital systems such as microcontrollers or computers.

image

2.2 Stages of the ADC Conversion Process

In general, the ADC process is divided into four main stages:

No Stage Explanation
1 Sampling Capturing (sampling) values from an analog signal at specific discrete points in time. The higher the sampling frequency, the more accurate the digital representation.
2 Filtering Cleaning the signal from noise before conversion to ensure the conversion results are more accurate. NOTE: This is usually not discussed much in ADC methods
3 Quantizing Converting the analog value at a single discrete point in time into a specific level representation. The number of levels is determined by the ADC resolution (e.g., 10-bit = 1024 levels).
4 Encoding Converting the level value from quantization into digital binary code per discrete time unit.

We won't go deep into this process, as you will study it directly in the Telecommunications lab next semester 😂

2.3 Illustration of the ADC Process

image

Modul 8: ADC (Analog to Digital Conversion)

3. Why is ADC Needed in Embedded Systems?

image

The real world is analog — all physical phenomena (temperature, light, sound, pressure, humidity) are continuous signals. However, microcontrollers and computers can only process information in digital form (0 and 1).

ADC acts as a bridge between the physical world and the digital world:

  1. Sensors produce analog signals → For example, an LDR produces a voltage that changes according to light intensity.
  2. Microcontrollers only understand digital numbers → Without an ADC, a microcontroller cannot "read" that voltage value.
  3. ADC converts → An analog voltage of 0–5V is converted into a number from 0–1023 (for a 10-bit ADC).
  4. Programs can process the result → For example: if the ADC value is < 500, turn on the LED.

Examples of ADC usage in daily life:

Modul 8: ADC (Analog to Digital Conversion)

4. ADC In ATmega328p

4.1 ATmega328p ADC Specifications

The ATmega328p (used in the Arduino Uno) has a built-in ADC with the following specifications:

Specification Value
Resolution 10-bit (produces values from 0 – 1023)
Conversion Method Successive Approximation
Input Channels 8 analog channels (A0 – A7), multiplexed
Reference Voltage AVcc, Internal 2.56V, or external AREF pin
Conversion Speed 50 kHz – 200 kHz (depending on prescaler)
Result Registers ADCL (low-byte) + ADCH (high-byte)

4.2 Successive Approximation Method

image

The ATmega328p uses the Successive Approximation Register (SAR) method. In this method, the ADC works by performing a binary search for the Vin value.

At each step:

This process is repeated N times (N = ADC resolution, which is 10-bit for the ATmega328p).

An example of SAR implementation can be seen in this image:

image

  1. Started from the middle value (1000) This is the representation of ½ of Vref (MSB = 1).
  2. First comparison (Comparator)
    • If Vdac > Vin → move down (red arrow)
    • If Vdac < Vin → move up (green arrow)
  3. Determining the next bit Each step determines one additional bit. Example:
    • 1000 → 1100 (if Vin is larger)
    • 1000 → 0100 (if Vin is smaller)
  4. Repeat process (Binary Search) The value range is continuously narrowed until all bits (MSB → LSB) are determined.
  5. Final result The rightmost nodes show the final binary code. Example results: 1011, 0110, etc.
Modul 8: ADC (Analog to Digital Conversion)

5. Important ADC Parameters In ATmega328p

5.1 Reference Voltage (Vref)

Reference Voltage (Vref) is the maximum voltage that serves as the full-scale reference in the ADC conversion process. Vref determines the range of input voltage that can be read by the ADC.

On the ATmega328p, there are three options for the reference voltage source:

REFS1 REFS0 Vref Source Description
0 0 AREF pin Uses an external voltage connected to the AREF pin
0 1 AVcc Uses the supply voltage (VCC), typically 5V
1 0 (unused)
1 1 Internal 2.56V Uses a fixed internal 2.56V reference voltage, ignoring VCC

image

Influence of Vref on effective resolution:

Vref Resolution per step
5V (AVcc) 5V / 1024 ≈ 4.88 mV per step
2.56V (Internal) 2.56V / 1024 ≈ 2.5 mV per step

The smaller the Vref, the finer the resolution — but the measurable input range is also smaller.

5.2 Prescaler

The Prescaler is a frequency divider that determines the ADC clock speed from the main system clock (F_CPU). The ADC requires a clock within the range of 50 kHz – 200 kHz for accurate results.

On the ATmega328p (F_CPU = 16 MHz), prescaler options are configured via the ADPS2:ADPS0 bits in the ADCSRA register:

ADPS2 ADPS1 ADPS0 Divider ADC Clock (at 16 MHz)
0 0 0 CLK/2 8 MHz
0 0 1 CLK/2 8 MHz
0 1 0 CLK/4 4 MHz
0 1 1 CLK/8 2 MHz
1 0 0 CLK/16 1 MHz
1 0 1 CLK/32 500 kHz
1 1 0 CLK/64 250 kHz
1 1 1 CLK/128 125 kHz ✅ (most accurate)

Note: The recommended ADC clock is between 50 kHz–200 kHz. A CLK/128 prescaler at 16 MHz produces 125 kHz — well within the optimal range.

5.3 Conversion Rate

Conversion Rate is the number of ADC conversions that can be performed per second. Its value depends on the ADC clock and the number of clock cycles per conversion.

On the ATmega328p:

Prescaler ADC Clock Conversion Rate
CLK/64 250 kHz ≈ 19.2 kSPS
CLK/128 125 kHz ≈ 9.6 kSPS

kSPS = kilo Samples Per Second (thousands of samples per second)

5.4 Influence of Vref, Prescaler, and Conversion Rate on Accuracy

These three parameters are interrelated in determining the quality of ADC conversion results, in terms of resolution, accuracy, and the ability to track signal changes.

Parameter Smaller Value Larger Value
Vref Finer resolution (small LSB), but limited input range Wider input range, but coarser resolution (large LSB)
Prescaler (divider) Higher ADC frequency → risk of inaccuracy if exceeding specification limits Lower ADC frequency → more stable operation if within optimal range (50–200 kHz)
Conversion Rate Sparse sampling → risk of losing information (aliasing) More frequent sampling → better ability to track signal changes

Conclusion:

Modul 8: ADC (Analog to Digital Conversion)

6. Specific Registers for ADC In ATmega328p

6.1 ADMUX — ADC Multiplexer Selection Register

ADMUX is an 8-bit register that handles the basic ADC configuration: reference voltage source, data storage format, and which analog input channel to read.

image

Functions of each field:

a) REFS1:REFS0 — Reference Selection

Selects the ADC reference voltage source:

REFS1 REFS0 Reference Voltage
0 0 AREF Pin (external)
0 1 AVcc (supply voltage, typically 5V)
1 0 Unused
1 1 Internal 2.56V

b) ADLAR — ADC Left Adjust Result

Determines the storage position of the 10-bit result within the two 8-bit registers (ADCH + ADCL):

ADLAR ADCH (8-bit) ADCL (8-bit)
1 (Left-justified) D9 D8 D7 D6 D5 D4 D3 D2 D1 D0 (unused 6-bit)
0 (Right-justified) (unused 6-bit) D9 D8 D7 D6 D5 D4 D3 D2 D1 D0

c) MUX3:MUX0 — Analog Channel Selection

Selects which analog input pin will be converted:

MUX3 MUX2 MUX1 MUX0 Analog Pin
0 0 0 0 ADC0 / A0
0 0 0 1 ADC1 / A1
0 0 1 0 ADC2 / A2
0 0 1 1 ADC3 / A3
0 1 0 0 ADC4 / A4
0 1 0 1 ADC5 / A5
0 1 1 0 ADC6 / A6
0 1 1 1 ADC7 / A7

6.2 ADCSRA — ADC Control and Status Register A

ADCSRA is an 8-bit register that serves as the command center for controlling and monitoring the ADC process status.

image

Functions of each bit:

Bit Name Function
ADEN image ADC Enable Set to 1 to enable the ADC. If 0, the ADC will not run and analog pins won't be converted.
ADSC ADC Start Conversion Set to 1 to start a single conversion cycle. This bit stays at 1 while conversion is in progress, then automatically returns to 0.
ADATE ADC Auto Trigger Enable If 1, conversion starts automatically triggered by a specific event (e.g., timer overflow, external pin change).
ADIF ADC Interrupt Flag Set to 1 by hardware when conversion is complete (End of Conversion). Reset by manually writing a 1 to this bit.
ADIE ADC Interrupt Enable If 1, the program will jump to an ISR (Interrupt Service Routine) when conversion is complete. Useful so the CPU doesn't have to wait (polling).
ADPS2:ADPS0 ADC Prescaler Select 3 bits that determine the clock divider for the ADC (see the prescaler table above).

6.3 ADCL and ADCH — ADC Data Registers

ADCL and ADCH are two 8-bit registers where the 10-bit ADC conversion result is stored after conversion is complete.

Since the ATmega328p uses a 10-bit ADC, the result (0–1023) cannot fit into a single 8-bit register, so it's split across two registers:

image

IMPORTANT: ADCL must be read first before ADCH. Reading ADCL locks ADCH to prevent it from changing until ADCH is read, ensuring data consistency.

How to read the full 10-bit ADC value (right-justified):

// In C:
uint16_t adc_value = ADC;  // or:
uint8_t  low  = ADCL;
uint8_t  high = ADCH;
uint16_t adc_value = (high << 8) | low;
; In Assembly:
LDS R18, ADCL    ; read low-byte first
LDS R19, ADCH    ; then read high-byte
Modul 8: ADC (Analog to Digital Conversion)

7. ADC Conversion Flowchart

Here is the complete workflow for using the ADC on the ATmega328p:

image

The flowchart above illustrates the ADC reading process on the ATmega328p in single conversion mode using the polling method, with the following configuration:

  1. Conversion Mode: The ADC operates in single conversion mode, meaning each conversion starts manually by setting the ADSC = 1 bit.

  2. Synchronization Method: The conversion completion status is checked using polling of the ADIF bit in the ADCSRA register, instead of using interrupts.

  3. Auto Trigger: This flowchart assumes ADATE = 0, so conversions do not run automatically and must be restarted by the program each time a reading is taken.

  4. ADC Interrupt: This flowchart does not use ADC interrupts, so ADIE = 0.

  5. Input Channel: The configuration example in the flowchart uses ADC0 (pin A0 / PC0) as the analog input channel.

  6. Reference Voltage: This flowchart follows an assembly program example that uses the internal reference voltage via the configuration of the REFS1:REFS0 bits in the ADMUX register.

  7. Conversion Data Format: The ADC result is stored in right-justified format (ADLAR = 0), so the full 10-bit value is read through two registers:

    • ADCL as the low-byte
    • ADCH as the high-byte
  8. Data Register Reading Order: The ADCL register must be read first, followed by ADCH, to ensure the conversion data remains consistent.

  9. ADC Prescaler: This example uses a CLK/128 prescaler (ADPS2:ADPS0 = 111). If the system clock is 16 MHz, the ADC clock becomes:

    image

    This value is within the recommended ADC operating range.

Summary of Configuration Used

Modul 8: ADC (Analog to Digital Conversion)

8. ADC Assembly Code Example

8.1 Full Code

Here is an example of AVR Assembly code to read the ADC from the ADC0 pin using the internal 2.56V reference and a CLK/128 prescaler:

#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main

main:
    LDI R20, 0xFF
    OUT DDRD, R20       ; Set Port D as output (ADC result low byte)
    OUT DDRB, R20       ; Set Port B as output (ADC result high byte)
    SBI DDRC, 0         ; Set pin PC0 as input for ADC0

    ;-- ADC Initialization --
    LDI R20, 0xC0       ; REFS1:REFS0 = 11 → Internal 2.56V
                        ; ADLAR = 0 → Right-justified
                        ; MUX4:MUX0 = 00000 → ADC0
    STS ADMUX, R20

    LDI R20, 0x87       ; ADEN = 1 → Enable ADC
                        ; ADPS2:ADPS0 = 111 → Prescaler CLK/128
    STS ADCSRA, R20

;-- ADC Reading Loop --
read_ADC:
    LDI R20, 0xC7       ; Set ADSC = 1 to start conversion
    STS ADCSRA, R20

wait_ADC:
    LDS R21, ADCSRA     ; Read ADCSRA status register
    SBRS R21, 4         ; Skip jump if ADIF (bit 4) = 1 (conversion complete)
    RJMP wait_ADC       ; Wait loop until ADIF is set

    ;-- Reset ADIF flag --
    LDI R17, 0xD7       ; Set ADIF = 1 so the controller can reset the flag
    STS ADCSRA, R17

    ;-- Read conversion result --
    LDS R18, ADCL       ; Read low-byte from ADCL (MUST read first)
    LDS R19, ADCH       ; Read high-byte from ADCH

    ;-- Output result --
    OUT PORTD, R18      ; Send low-byte to Port D
    OUT PORTB, R19      ; Send high-byte to Port B

    RJMP read_ADC       ; Repeat reading

8.2 Code Explanation

Initialization Section:

LDI R20, 0xC0
STS ADMUX, R20
LDI R20, 0x87
STS ADCSRA, R20

Reading Section (Loop):

LDI R20, 0xC7
STS ADCSRA, R20
wait_ADC:
    LDS R21, ADCSRA
    SBRS R21, 4
    RJMP wait_ADC
LDI R17, 0xD7
STS ADCSRA, R17
LDS R18, ADCL
LDS R19, ADCH
OUT PORTD, R18
OUT PORTB, R19

Module 9 - SPI, I2C, and Sensor Interfacing

Module 9 - SPI, I2C, and Sensor Interfacing

1. Serial Peripheral Interface (SPI)

1.1 Overview

SPI is a synchronous serial communication protocol commonly used for fast peripheral communication in embedded systems. It typically uses four lines:

SPI is full-duplex, meaning data can be transmitted and received at the same time.

1.2 Communication Flow

  1. The master pulls SS low to select a slave.
  2. The master generates clock pulses on SCK.
  3. Data shifts out on MOSI and shifts in on MISO.
  4. The transfer ends when the expected bits/bytes are completed.
  5. The master releases SS high.

1.3 SPI Clock Modes

1.4 SPI Registers (Detailed Bits)

SPDR - SPI Data Register

Function:

Bit Name Description
7 SPD7 Data bit 7
6 SPD6 Data bit 6
5 SPD5 Data bit 5
4 SPD4 Data bit 4
3 SPD3 Data bit 3
2 SPD2 Data bit 2
1 SPD1 Data bit 1
0 SPD0 Data bit 0

SPSR - SPI Status Register

Function:

Bit Name Description
7 SPIF SPI interrupt/transfer complete flag
6 WCOL Write collision flag
5 Reserved Reserved
4 Reserved Reserved
3 Reserved Reserved
2 Reserved Reserved
1 Reserved Reserved
0 SPI2X Double SPI speed (master mode)

SPCR - SPI Control Register

Function:

Bit Name Description
7 SPIE Enable SPI interrupt
6 SPE Enable SPI peripheral
5 DORD Data order (1=LSB first, 0=MSB first)
4 MSTR Master select (1=master, 0=slave)
3 CPOL Clock polarity
2 CPHA Clock phase
1 SPR1 Clock rate select bit 1
0 SPR0 Clock rate select bit 0

Clock-rate note:

1.5 SPI Assembly Examples

SPI Master

;------------------------
; Assembly Code SPI Master
;------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main
;==============================================================
main:
.equ  SCK, 5
.equ  MOSI, 3
.equ  SS, 2
;--------------------------------------------------------------
      LDI   R17, (1<<MOSI)|(1<<SCK)|(1<<SS)
      OUT   DDRB, R17       ;set MOSI, SCK, SS as o/p
      ;--------------------------------------------------------
      LDI   R17, (1<<SPE)|(1<<MSTR)|(1<<SPR0)
      OUT   SPCR, R17       ;enable SPI as master, fsck=fosc/16
      ;--------------------------------------------------------
      LDI   R17, 0xAA       ;byte to be transmitted
      ;--------------------------------------------------------
again:CBI   PORTB, SS       ;enable slave device
      OUT   SPDR, R17       ;transmit byte to slave
      ;--------------------------------------------------------
loop: IN    R18, SPSR
      SBRS  R18, SPIF       ;wait for byte transmission
      RJMP  loop            ;to complete
      ;--------------------------------------------------------
      SBI   PORTB, SS       ;disable slave device
      ;--------------------------------------------------------
      RCALL my_delay        ;delay
      COM   R17             ;1's compliment of byte
      RJMP  again           ;repeat transmission
;==============================================================
my_delay:                   ;delay in ms
    LDI   R20, 255
l6: LDI   R21, 255
l7: LDI   R22, 40
l8: DEC   R22
    BRNE  l8
    DEC   R21
    BRNE  l7
    DEC   R20
    BRNE  l6
    RET

SPI Slave

;------------------------
; Assembly Code SPI Slave
;------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main
;============================================================
main:
;------------------------------------------------------------
      LDI   R17, 0xFF
      OUT   DDRD, R17       ;set PORTD for o/p
      ;------------------------------------------------------
      LDI   R17, (1<<SPE)
      OUT   SPCR, R17       ;enable SPI as slave
      ;------------------------------------------------------
agn:  IN    R18, SPSR
      SBRS  R18, SPIF       ;wait for byte reception
      RJMP  agn
      ;------------------------------------------------------
      IN    R18, SPDR       ;i/p byte from data register
      OUT   PORTD, R18      ;and o/p to PORTD
      ;------------------------------------------------------
      RJMP  agn             ;repeat reception
Module 9 - SPI, I2C, and Sensor Interfacing

2. Inter-Integrated Circuit (I2C)

2.1 Overview

I2C is a synchronous two-wire serial bus:

I2C supports multiple devices on the same bus using slave addressing and ACK/NACK handshaking.

2.2 I2C Transaction Flow

  1. Master sends START condition.
  2. Master sends Slave Address + R/W bit.
  3. Slave replies with ACK.
  4. Data bytes are exchanged with ACK/NACK after each byte.
  5. Master sends STOP condition.

2.3 I2C Speed Modes

2.4 I2C Registers (Detailed Bits)

TWSR - TWI Status Register

Function:

Bit Name Description
7 TWS7 Status code bit 7
6 TWS6 Status code bit 6
5 TWS5 Status code bit 5
4 TWS4 Status code bit 4
3 TWS3 Status code bit 3
2 Reserved Reserved/unused
1 TWPS1 Prescaler select bit 1
0 TWPS0 Prescaler select bit 0

Prescaler mapping:

TWPS1 TWPS0 Prescaler Value
0 0 1
0 1 4
1 0 16
1 1 64

TWBR - TWI Bit Rate Register

Function:

Bit Name Description
7 TWBR7 Bit-rate divider bit 7
6 TWBR6 Bit-rate divider bit 6
5 TWBR5 Bit-rate divider bit 5
4 TWBR4 Bit-rate divider bit 4
3 TWBR3 Bit-rate divider bit 3
2 TWBR2 Bit-rate divider bit 2
1 TWBR1 Bit-rate divider bit 1
0 TWBR0 Bit-rate divider bit 0

Clock formula:

SCL = F_CPU / (16 + 2 x TWBR x PrescalerValue)

TWCR - TWI Control Register

Function:

Bit Name Description
7 TWINT TWI interrupt flag (set by hardware when operation completes)
6 TWEA TWI enable acknowledge
5 TWSTA TWI start condition request
4 TWSTO TWI stop condition request
3 TWWC TWI write collision flag
2 TWEN TWI enable
1 Reserved Reserved
0 TWIE TWI interrupt enable

TWDR - TWI Data Register

Function:

Bit Name Description
7 TWD7 Data bit 7
6 TWD6 Data bit 6
5 TWD5 Data bit 5
4 TWD4 Data bit 4
3 TWD3 Data bit 3
2 TWD2 Data bit 2
1 TWD1 Data bit 1
0 TWD0 Data bit 0

TWAR - TWI Address Register

Function:

Bit Name Description
7 TWA6 Slave address bit 6
6 TWA5 Slave address bit 5
5 TWA4 Slave address bit 4
4 TWA3 Slave address bit 3
3 TWA2 Slave address bit 2
2 TWA1 Slave address bit 1
1 TWA0 Slave address bit 0
0 TWGCE General call recognition enable

Common TWI Status Codes

Status Code Meaning
0x08 START condition transmitted
0x10 Repeated START transmitted
0x18 SLA+W transmitted, ACK received
0x20 SLA+W transmitted, NACK received
0x28 Data transmitted, ACK received
0x30 Data transmitted, NACK received
0x38 Arbitration lost
0x40 SLA+R transmitted, ACK received
0x48 SLA+R transmitted, NACK received
0x50 Data received, ACK returned
0x58 Data received, NACK returned

2.5 I2C Assembly Examples

I2C Master Transmit

;--------------------------
; Assembly Code - Master Tx
;--------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main
;==============================================================
main:
    CBI   DDRC, 3         ;pin PC3 is i/p
    ;----------------------------------------------------------
    RCALL I2C_init        ;initialize TWI module
    ;----------------------------------------------------------
l1: SBIS  PINC, 3
    RJMP  l1              ;wait for "transmit" button press
    ;----------------------------------------------------------
    RCALL I2C_start       ;transmit START condition
    LDI   R27, 0b10010000 ;SLA(1001000) + W(0)
    RCALL I2C_write       ;write slave address SLA+W
    LDI   R27, 0b11110101 ;data byte to be transmitted
    RCALL I2C_write       ;write data byte
    RCALL I2C_stop        ;transmit STOP condition
    ;----------------------------------------------------------
    RJMP  l1              ;go back for another transmit
;==============================================================
I2C_init:
    LDI   R21, 0
    STS   TWSR, R21       ;prescaler = 0
    LDI   R21, 12         ;division factor = 12
    STS   TWBR, R21       ;SCK freq = 400kHz
    LDI   R21, (1<<TWEN)
    STS   TWCR, R21       ;enable TWI
    RET
;==============================================================
I2C_start:
    LDI   R21, (1<<TWINT)|(1<<TWSTA)|(1<<TWEN)
    STS   TWCR, R21       ;transmit START condition
    ;----------------------------------------------------------
wt1:LDS   R21, TWCR
    SBRS  R21, TWINT      ;TWI interrupt = 1?
    RJMP  wt1             ;no, wait for end of transmission
    ;----------------------------------------------------------
    RET
;==============================================================
I2C_write:
    STS   TWDR, R27       ;copy SLA+W into data register
    LDI   R21, (1<<TWINT)|(1<<TWEN)
    STS   TWCR, R21       ;transmit SLA+W
    ;----------------------------------------------------------
wt2:LDS   R21, TWCR
    SBRS  R21, TWINT
    RJMP  wt2             ;wait for end of transmission
    ;----------------------------------------------------------
    RET
;==============================================================
I2C_stop:
    LDI   R21, (1<<TWINT)|(1<<TWSTO)|(1<<TWEN)
    STS   TWCR, R21       ;transmit STOP condition
    RET

I2C Slave Receive

;-------------------------
; Assembly Code - Slave Rx
;-------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main
;==============================================================
main:
    LDI   R21, 0xFF
    OUT   DDRD, R21         ;port D is o/p
    CBI   DDRC, 3           ;pin PC3 is i/p
;--------------------------------------------------------------
agn:RCALL I2C_init          ;initialize TWI module
    RCALL I2C_listen        ;listen to bus to be addressed
    RCALL I2C_read          ;read data byte
    OUT   PORTD, R27        ;and o/p to port D
;--------------------------------------------------------------
l1: SBIS  PINC, 3
    RJMP  l1                ;wait for "listen" button press
;--------------------------------------------------------------
    LDI   R26, 0
    OUT   PORTD, R26        ;clear port D
    RJMP  agn               ;& go back & listen to bus
;==============================================================
I2C_init:
    LDI   R21, 0b10010000
    STS   TWAR, R21         ;store slave address 0b10010000
    LDI   R21, (1<<TWEN)
    STS   TWCR, R21         ;enable TWI
    LDI   R21, (1<<TWINT)|(1<<TWEN)|(1<<TWEA)
    STS   TWCR, R21         ;enable TWI & ACK
    RET
;==============================================================
I2C_listen:
    LDS   R21, TWCR
    SBRS  R21, TWINT
    RJMP  I2C_listen        ;wait for slave to be addressed
    RET
;==============================================================
I2C_read:
    LDI   R21, (1<<TWINT)|(1<<TWEA)|(1<<TWEN)
    STS   TWCR, R21         ;enable TWI & ACK
    ;----------------------------------------------------------
wt: LDS   R21, TWCR
    SBRS  R21, TWINT
    RJMP  wt                ;wait for data byte to be read
    ;----------------------------------------------------------
    LDS   R27, TWDR         ;store received byte
    RET
Module 9 - SPI, I2C, and Sensor Interfacing

3. DHT11 Sensor Interfacing

3.1 Sensor Fundamentals

A sensor captures physical phenomena from the real world and converts them into electrical signals (analog or digital) for computation.

For DHT11 specifically:

3.2 DHT11 Variants and Wiring

DHT11 can be found as:

Breakout modules are typically easier to wire and often include support components.

3.3 DHT11 Protocol and Timing

DHT11 uses a single-wire, pulse-width-based digital protocol. Communication sequence:

  1. MCU sends start signal (low pulse around 18-20 ms, then high).
  2. DHT11 sends response pulse.
  3. DHT11 transmits 40-bit serial data.
  4. Bit value is represented by pulse width:

3.4 40-bit Data Format

Byte Data Field
1 Humidity integer part
2 Humidity decimal part
3 Temperature integer part
4 Temperature decimal part
5 Checksum

Checksum rule:

Checksum = (Byte1 + Byte2 + Byte3 + Byte4) & 0xFF

3.5 Why Accurate Delay Matters

DHT11 decoding depends on timing precision. If delays are too short or too long:

3.6 DHT11 Assembly Example

;------------------------
; Assembly Code
;------------------------
#define __SFR_OFFSET 0x00
#include "avr/io.h"
;------------------------
.global main
;=================================================================
main:
;------------
    LDI   R17, 0xFF
    OUT   DDRC, R17     ;set port C for o/p
    OUT   DDRD, R17     ;set port D for o/p
;-----------------------------------------------------------------
agn:RCALL delay_2s      ;wait 2s for DHT11 to get ready
;-----------------------------------------------------------------
;send start signal
;------------
    SBI   DDRB, 1       ;pin PB0 as o/p
    CBI   PORTB, 1      ;first, send low pulse
    RCALL delay_20ms    ;for 20ms
    SBI   PORTB, 1      ;then send high pulse
;-----------------------------------------------------------------
;wait for response signal
;---------------
    CBI   DDRB, 1       ;pin PB0 as i/p
w1: SBIC  PINB, 1
    RJMP  w1            ;wait for DHT11 low pulse
w2: SBIS  PINB, 1
    RJMP  w2            ;wait for DHT11 high pulse
w3: SBIC  PINB, 1
    RJMP  w3            ;wait for DHT11 low pulse
;-----------------------------------------------------------------
    RCALL DHT11_reading ;read humidity (1st byte of 40-bit data)
    MOV   R19, R18
    RCALL DHT11_reading
    RCALL DHT11_reading ;read temp (3rd byte of 40-bit data)
;-----------------------------------------------------------------
    OUT   PORTD, R19    ;o/p temp byte to port C
    OUT   PORTC, R18    ;o/p humidity byte to port D
    RJMP  agn           ;go back & get another sensor reading
;=================================================================
DHT11_reading:
    LDI   R17, 8        ;set counter for receiving 8 bits
    CLR   R18           ;clear data register
    ;-------------------------------------------------------
w4: SBIS  PINB, 1
    RJMP  w4            ;detect data bit (high pulse)
    RCALL delay_timer0  ;wait 50us then check bit value
    ;-------------------------------------------------------
    SBIS  PINB, 1       ;if bit=1, skip next instruction
    RJMP  skp           ;else bit=0
    SEC                 ;C=1
    ROL   R18           ;shift in 1
    RJMP  w5
skp:LSL   R18           ;shift in 0
    ;-------------------------------------------------------
w5: SBIC  PINB, 1
    RJMP  w5            ;wait for low pulse
    ;-------------------------------------------------------
    DEC   R17
    BRNE  w4
    RET
Module 9 - SPI, I2C, and Sensor Interfacing

4. SPI vs I2C Comparison

Aspect SPI I2C
Signal lines SCK, MOSI, MISO, SS SDA, SCL
Duplex mode Full-duplex Half-duplex-style exchange
Addressing No built-in addressing Built-in slave addressing
Typical speed Generally faster Generally slower
Wiring complexity More wires, simple protocol Fewer wires, richer bus protocol

Final Project

Final Project

Final Project Guidelines

In this final module, you are given the opportunity to create a project with your group members under the following provisions:

Final Project Criteria

Here are the criteria for the final project:

README.md Structure

The README.md must include the following sections/headings:

  1. Introduction to the problem and the solution

  2. Hardware design and implementation details

  3. Software implementation details

  4. Test results and performance evaluation

  5. Conclusion and future work

Final Project Grading Weights