Unit 5 - Notes

ECE227 13 min read

Unit 5: Timer Programming and Interrupt Programming

1. Programming Timers in C

Timers and counters are fundamental peripherals in microcontrollers. They allow the system to perform tasks at precise time intervals or to count external events without halting the main program flow.

  • Timer: A register that increments automatically at a specific rate, determined by the system's internal oscillator clock. It's used for measuring time and generating delays.
  • Counter: A register that increments in response to an external event, typically a signal transition (rising or falling edge) on an input pin.

1.1 Programming Timer 0 in C

Timer0 is a versatile timer/counter available in PIC18 microcontrollers. It can be configured as an 8-bit or a 16-bit timer/counter.

Key Registers for Timer 0

  1. T0CON (Timer0 Control Register): This is the main register for configuring Timer0.

    • TMR0ON: Timer0 On/Off Control bit. (1 = Enables Timer0, 0 = Stops Timer0)
    • T08BIT: Timer0 8-Bit/16-Bit Control bit. (1 = 8-bit timer/counter, 0 = 16-bit timer/counter)
    • T0CS: Timer0 Clock Source Select bit. (0 = Internal clock (Fosc/4), 1 = External clock on T0CKI pin)
    • T0SE: Timer0 Source Edge Select bit. (Used in counter mode. 0 = Increments on low-to-high, 1 = Increments on high-to-low)
    • PSA: Prescaler Assignment bit. (0 = Prescaler is assigned to Timer0, 1 = Prescaler is not assigned)
    • T0PS2:T0PS0: Timer0 Prescaler Select bits. These 3 bits select the prescaler rate from 1:2 to 1:256.
  2. TMR0L & TMR0H (Timer0 Register Low/High Byte):

    • In 8-bit mode, only TMR0L is used. It holds the 8-bit timer value.
    • In 16-bit mode, both TMR0L and TMR0H are used together to form a 16-bit timer value.
  3. INTCON (Interrupt Control Register):

    • TMR0IF (or T0IF): Timer0 Overflow Interrupt Flag. This bit is automatically set to 1 when the timer register (TMR0L or TMR0H:TMR0L) overflows from its maximum value (0xFF or 0xFFFF) back to 0. It must be cleared manually in software.

Calculating Delay with Timer 0

The time for one timer "tick" is calculated as:
Tick Time = (4 / Fosc) * Prescaler

The total delay is calculated as:
Total Delay = (Max Count - Initial Value) * Tick Time

  • Fosc: Oscillator frequency of the microcontroller.
  • Max Count: 256 for 8-bit mode (0xFF), 65536 for 16-bit mode (0xFFFF).
  • Initial Value: The value preloaded into TMR0L/TMR0H to start counting from.

Example: Generating a Delay using Timer 0 (16-bit mode, polling)

This example generates a delay using Timer0 in 16-bit mode. The code will poll the TMR0IF flag to check for overflow. Assume Fosc = 48MHz.

Goal: Generate a ~100ms delay.

  • Fosc = 48 MHz
  • Instruction Clock Cycle = 48 MHz / 4 = 12 MHz
  • Let's use a 1:64 prescaler.
  • Tick Time = (1 / 12 MHz) * 64 = 5.333 µs
  • Number of ticks for 100ms delay = 100,000 µs / 5.333 µs ≈ 18750 ticks.
  • Initial Timer Value = 65536 - 18750 = 46786 = 0xB6C2.
  • So, TMR0H = 0xB6 and TMR0L = 0xC2.

C
#include <xc.h>

// Assuming FOSC = 48MHz, defined in project settings
// #pragma config FOSC = HSPLL_HS, PLLDIV = 5, CPUDIV = OSC1_PLL2 

void T0_delay(void) {
    // Configure Timer0
    T0CONbits.TMR0ON = 0;   // Stop the timer
    T0CONbits.T08BIT = 0;   // Set to 16-bit mode
    T0CONbits.T0CS = 0;     // Use internal clock (Fosc/4)
    T0CONbits.PSA = 0;      // Assign prescaler to Timer0
    T0CONbits.T0PS = 0b101; // Set prescaler to 1:64

    // Load the initial value for a ~100ms delay
    TMR0H = 0xB6;
    TMR0L = 0xC2;

    INTCONbits.TMR0IF = 0;  // Clear the overflow flag
    T0CONbits.TMR0ON = 1;   // Start the timer

    // Poll the overflow flag. Wait here until it's set.
    while (INTCONbits.TMR0IF == 0) {
        // Do nothing, just wait
    }

    T0CONbits.TMR0ON = 0;   // Stop the timer
    INTCONbits.TMR0IF = 0;  // Clear the flag for the next use
}

void main(void) {
    TRISBbits.TRISB0 = 0; // Set RB0 as output
    LATBbits.LATB0 = 0;   // Turn off LED initially

    while (1) {
        LATBbits.LATB0 = ~LATBbits.LATB0; // Toggle the LED
        T0_delay();                      // Wait for ~100ms
    }
}

1.2 Programming Timer 1 in C

Timer1 is a 16-bit timer/counter. It is often used for longer delays or as a real-time clock source with an external crystal.

Key Registers for Timer 1

  1. T1CON (Timer1 Control Register):

    • TMR1ON: Timer1 On bit. (1 = Enables Timer1, 0 = Stops Timer1)
    • RD16: 16-Bit Read/Write Mode Enable bit. (1 = Reads/writes TMR1H:TMR1L in one 16-bit operation, 0 = in two 8-bit operations)
    • T1RUN: Timer1 Oscillator is running status bit (Read-only).
    • T1CKPS1:T1CKPS0: Timer1 Input Clock Prescale Select bits (1:1, 1:2, 1:4, 1:8).
    • T1OSCEN: Timer1 Oscillator Enable bit. (1 = Oscillator is enabled for Timer1)
    • TMR1CS: Timer1 Clock Source Select bit. (0 = Internal clock (Fosc/4), 1 = External clock from T1CKI pin)
  2. TMR1L & TMR1H (Timer1 Register Low/High Byte): These two 8-bit registers combine to form the 16-bit timer value.

  3. PIR1 (Peripheral Interrupt Request Register 1):

    • TMR1IF: Timer1 Overflow Interrupt Flag. Set when Timer1 overflows from 0xFFFF to 0x0000. Must be cleared in software.

Example: Generating a Delay using Timer 1 (polling)

This example generates a 1-second delay using Timer1 and polling. Assume Fosc = 8MHz.

Goal: Generate a 1-second delay.

  • We need to make the timer overflow multiple times. Let's make it overflow every 50ms.
  • 1 second / 50ms = 20 overflows.
  • Fosc = 8 MHz
  • Instruction Clock Cycle = 8 MHz / 4 = 2 MHz
  • Let's use a 1:8 prescaler.
  • Tick Time = (1 / 2 MHz) * 8 = 4 µs
  • Number of ticks for 50ms delay = 50,000 µs / 4 µs = 12500 ticks.
  • Initial Timer Value = 65536 - 12500 = 53036 = 0xCF2C.
  • So, TMR1H = 0xCF and TMR1L = 0x2C.

C
#include <xc.h>

void delay_1_sec(void) {
    unsigned char i;
    for (i = 0; i < 20; i++) { // Repeat 20 times for 1 second
        // Configure Timer1
        T1CONbits.TMR1ON = 0;    // Stop the timer
        T1CONbits.TMR1CS = 0;    // Use internal clock (Fosc/4)
        T1CONbits.T1CKPS = 0b11; // Set prescaler to 1:8

        // Load initial value for a 50ms delay
        TMR1H = 0xCF;
        TMR1L = 0x2C;

        PIR1bits.TMR1IF = 0;   // Clear the overflow flag
        T1CONbits.TMR1ON = 1;    // Start the timer

        // Poll the flag
        while (PIR1bits.TMR1IF == 0);

        T1CONbits.TMR1ON = 0;    // Stop the timer
        PIR1bits.TMR1IF = 0;   // Clear flag for next loop
    }
}

void main(void) {
    TRISBbits.TRISB0 = 0; // Set RB0 as output
    LATBbits.LATB0 = 0;   // Turn off LED initially

    while (1) {
        LATBbits.LATB0 = ~LATBbits.LATB0; // Toggle the LED
        delay_1_sec();                   // Wait for 1 second
    }
}


2. PIC18 Interrupts

An interrupt is a signal to the processor that an event of higher priority has occurred, requiring immediate attention. This allows the microcontroller to pause its current task, execute a special function called an Interrupt Service Routine (ISR) to handle the event, and then resume the original task.

Polling vs. Interrupts:

  • Polling: The microcontroller continuously checks the status of a device or flag. This is inefficient as it wastes CPU cycles.
  • Interrupts: The CPU works on its main task and only stops to service the device when the device itself requests attention. This is highly efficient and allows for better multitasking and responsiveness.

The Interrupt Process

  1. An interrupt event occurs (e.g., Timer overflow, button press on an external pin).
  2. The corresponding interrupt flag bit is set (e.g., TMR0IF = 1).
  3. The microcontroller checks if the corresponding interrupt enable bit is set (e.g., TMR0IE = 1).
  4. It then checks if the global interrupt enable bit is set (GIE/GIEH = 1).
  5. If all are enabled, the microcontroller completes its current instruction.
  6. The address of the next instruction (from the Program Counter) is pushed onto the stack.
  7. The Program Counter is loaded with the address of the corresponding ISR from the Interrupt Vector Table (IVT).
  8. The microcontroller executes the code inside the ISR.
  9. Crucially, the programmer must clear the interrupt flag in the ISR. If not cleared, the ISR will be called again immediately after it finishes.
  10. The ISR ends with a retfie (Return from Interrupt) instruction. This pops the address from the stack back into the Program Counter and re-enables global interrupts.
  11. The main program resumes exactly where it left off.

Key Interrupt Registers

  • INTCON Register: A central register for many core interrupts.

    • GIE/GIEH: Global Interrupt Enable (High Priority).
    • PEIE/GIEL: Peripheral Interrupt Enable (Low Priority).
    • TMR0IE: Timer0 Overflow Interrupt Enable.
    • TMR0IF: Timer0 Overflow Interrupt Flag.
    • INT0IE: External Interrupt 0 Enable.
    • INT0IF: External Interrupt 0 Flag.
  • PIE (Peripheral Interrupt Enable) Registers (e.g., PIE1, PIE2): Contain enable bits for various peripheral interrupts (e.g., TMR1IE for Timer1, TXIE for USART Transmit).

  • PIR (Peripheral Interrupt Request) Registers (e.g., PIR1, PIR2): Contain the flag bits for the corresponding peripheral interrupts (e.g., TMR1IF, TXIF).

  • IPR (Interrupt Priority) Registers (e.g., IPR1, IPR2): Set the priority (high or low) for each peripheral interrupt. (1 = High priority, 0 = Low priority).

  • RCON Register:

    • IPEN: Interrupt Priority Enable bit. Setting this bit to 1 enables the high/low priority interrupt system. If it is 0, all interrupts are directed to the high-priority vector at 0x0008.

3. Programming Timer Interrupts

Combining timers with interrupts is the most common and efficient way to create precise, periodic events in an embedded system. The CPU is free to perform other tasks, and the ISR is automatically called at exact intervals.

Steps for Programming a Timer Interrupt

  1. Enable Priority Levels: Set RCONbits.IPEN = 1; to enable the priority interrupt system.
  2. Configure the Timer: Set up the timer (e.g., Timer0) as desired (mode, clock source, prescaler).
  3. Load Initial Value: Load the TMRxL and TMRxH registers with the starting count.
  4. Clear the Interrupt Flag: Clear the timer's interrupt flag (TMRxIF) before enabling interrupts to avoid an immediate, false trigger.
  5. Enable the Specific Interrupt: Set the timer's interrupt enable bit (e.g., INTCONbits.TMR0IE = 1;).
  6. Set Priority (Optional but Recommended): Set the priority bit for the interrupt (e.g., INTCON2bits.TMR0IP = 1; for high priority).
  7. Enable Global Interrupts: Enable high-priority (GIEH) and/or low-priority (GIEL) global interrupts.
  8. Write the ISR: Create the interrupt service routine that will be executed upon overflow.

The Interrupt Service Routine (ISR)

In C (using XC8 compiler), an ISR is defined with a special syntax:

C
// High Priority ISR
void __interrupt(high_priority) high_priority_isr(void) {
    // Check which interrupt flag caused this interrupt
    if (INTCONbits.TMR0IF == 1) {
        // --- ISR Code for Timer0 ---
        
        // 1. Clear the interrupt flag
        INTCONbits.TMR0IF = 0;
        
        // 2. Reload timer registers for the next period
        TMR0H = 0xXX; 
        TMR0L = 0xXX;
        
        // 3. Perform the desired task (e.g., toggle an LED)
        LATBbits.LATB0 = ~LATBbits.LATB0;
    }
    // Check for other high-priority flags if necessary...
}

// Low Priority ISR
void __interrupt(low_priority) low_priority_isr(void) {
    // Check for low-priority flags
}

Example: Blinking an LED with Timer0 Interrupt

This code will blink an LED on RB0 every 500ms using a Timer0 interrupt, while the main() loop does nothing. Assume Fosc = 16MHz.

  • Goal: Interrupt every 20ms. We will use a counter in the ISR to reach 500ms.
  • Fosc = 16 MHz -> Instruction Clock = 4 MHz.
  • Let's use a 1:256 prescaler.
  • Tick Time = (1 / 4 MHz) * 256 = 64 µs.
  • Ticks for 20ms = 20,000 µs / 64 µs = 312.5. We will use 313.
  • Initial Value (16-bit) = 65536 - 313 = 65223 = 0xFE07.
  • TMR0H = 0xFE, TMR0L = 0x07.
  • To get 500ms, we need 500ms / 20ms = 25 interrupts.

C
#include <xc.h>

// Define the preload values for a 20ms interrupt
#define TMR0_RELOAD_H 0xFE
#define TMR0_RELOAD_L 0x07

volatile unsigned char interrupt_count = 0;

// High Priority ISR
void __interrupt(high_priority) high_priority_isr(void) {
    // Check if the interrupt is from Timer0
    if (INTCONbits.TMR0IF == 1) {
        INTCONbits.TMR0IF = 0; // MUST clear the flag

        // Reload the timer for the next 20ms period
        TMR0H = TMR0_RELOAD_H;
        TMR0L = TMR0_RELOAD_L;

        interrupt_count++; // Increment our software counter
    }
}

void main(void) {
    // System configuration
    TRISBbits.TRISB0 = 0; // RB0 as output
    LATBbits.LATB0 = 0;   // LED off

    // Interrupt configuration
    RCONbits.IPEN = 1;      // Enable priority levels
    INTCONbits.GIEH = 1;    // Enable high-priority global interrupts
    INTCONbits.GIEL = 1;    // Enable low-priority global interrupts

    // Timer0 configuration
    T0CONbits.TMR0ON = 0;   // Timer is initially off
    T0CONbits.T08BIT = 0;   // 16-bit mode
    T0CONbits.T0CS = 0;     // Internal clock (Fosc/4)
    T0CONbits.PSA = 0;      // Prescaler is assigned
    T0CONbits.T0PS = 0b111; // 1:256 prescaler
    
    // Load initial value
    TMR0H = TMR0_RELOAD_H;
    TMR0L = TMR0_RELOAD_L;

    // Timer0 interrupt configuration
    INTCONbits.TMR0IE = 1;  // Enable Timer0 overflow interrupt
    INTCON2bits.TMR0IP = 1; // Set Timer0 interrupt as high priority
    INTCONbits.TMR0IF = 0;  // Clear the flag

    T0CONbits.TMR0ON = 1;   // Start the timer

    while (1) {
        // Check if 25 interrupts (25 * 20ms = 500ms) have occurred
        if (interrupt_count >= 25) {
            LATBbits.LATB0 = ~LATBbits.LATB0; // Toggle the LED
            interrupt_count = 0;              // Reset the counter
        }
        // The CPU is free to do other tasks here
    }
}


4. Programming External Hardware Interrupts

External interrupts are triggered by a signal change (a rising or falling edge) on one of the dedicated external interrupt pins (INT0, INT1, INT2). They are ideal for responding to asynchronous events like a button press or a signal from another device.

Key Registers for External Interrupts

  • INTCON Register:
    • INT0IF: External Interrupt 0 Flag.
    • INT0IE: External Interrupt 0 Enable.
  • INTCON2 Register:
    • INTEDG0, INTEDG1, INTEDG2: Interrupt Edge Select bits. (1 = Interrupt on rising edge, 0 = Interrupt on falling edge).
  • INTCON3 Register:
    • INT1IF, INT1IE, INT1IP: Flag, Enable, and Priority for INT1.
    • INT2IF, INT2IE, INT2IP: Flag, Enable, and Priority for INT2.

Note: INT0 is always a high-priority interrupt and does not have a separate priority bit.

Steps for Programming an External Interrupt (INT0)

  1. Configure Pin Direction: Set the corresponding pin (e.g., RB0 for INT0) as an input. TRISBbits.TRISB0 = 1;.
  2. Enable Priority Levels: RCONbits.IPEN = 1;.
  3. Select Trigger Edge: Configure INTCON2bits.INTEDG0 for rising (1) or falling (0) edge. A button press is typically detected on a falling edge.
  4. Clear the Interrupt Flag: INTCONbits.INT0IF = 0;.
  5. Enable the Specific Interrupt: INTCONbits.INT0IE = 1;.
  6. Enable Global Interrupts: INTCONbits.GIEH = 1;.
  7. Write the ISR: Create the interrupt service routine to handle the event.

Example: Toggling an LED with a Button Press on INT0

This program toggles an LED on RC0 every time a button connected to RB0 (INT0 pin) is pressed.

C
#include <xc.h>

// A simple software debounce function
void debounce_delay(void) {
    for (int i=0; i < 5000; i++);
}

// High Priority ISR
void __interrupt(high_priority) high_priority_isr(void) {
    // Check if the interrupt is from INT0
    if (INTCONbits.INT0IF == 1) {
        debounce_delay(); // Simple delay for button debounce
        
        // Toggle the LED
        LATCbits.LATC0 = ~LATCbits.LATC0;
        
        // MUST clear the flag to be able to detect the next press
        INTCONbits.INT0IF = 0; 
    }
}

void main(void) {
    // Pin Configuration
    TRISBbits.TRISB0 = 1; // RB0 (INT0) as input for the button
    TRISCbits.TRISC0 = 0; // RC0 as output for the LED
    LATCbits.LATC0 = 0;   // LED initially OFF

    // Interrupt Configuration
    RCONbits.IPEN = 1;          // Enable priority levels
    INTCONbits.GIEH = 1;        // Enable high-priority global interrupts
    INTCONbits.INT0IE = 1;      // Enable the INT0 external interrupt
    INTCON2bits.INTEDG0 = 0;    // Interrupt on falling edge (button press)
    INTCONbits.INT0IF = 0;      // Clear the INT0 flag

    while (1) {
        // The main loop can be empty or perform other non-critical tasks.
        // The CPU waits here until the button is pressed.
    }
}