Unit 5 - Notes
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
-
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.
-
TMR0L&TMR0H(Timer0 Register Low/High Byte):- In 8-bit mode, only
TMR0Lis used. It holds the 8-bit timer value. - In 16-bit mode, both
TMR0LandTMR0Hare used together to form a 16-bit timer value.
- In 8-bit mode, only
-
INTCON(Interrupt Control Register):TMR0IF(orT0IF): 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 intoTMR0L/TMR0Hto 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 = 0xB6andTMR0L = 0xC2.
#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
-
T1CON(Timer1 Control Register):TMR1ON: Timer1 On bit. (1 = Enables Timer1, 0 = Stops Timer1)RD16: 16-Bit Read/Write Mode Enable bit. (1 = Reads/writesTMR1H:TMR1Lin 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)
-
TMR1L&TMR1H(Timer1 Register Low/High Byte): These two 8-bit registers combine to form the 16-bit timer value. -
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 = 0xCFandTMR1L = 0x2C.
#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
- An interrupt event occurs (e.g., Timer overflow, button press on an external pin).
- The corresponding interrupt flag bit is set (e.g.,
TMR0IF = 1). - The microcontroller checks if the corresponding interrupt enable bit is set (e.g.,
TMR0IE = 1). - It then checks if the global interrupt enable bit is set (
GIE/GIEH= 1). - If all are enabled, the microcontroller completes its current instruction.
- The address of the next instruction (from the Program Counter) is pushed onto the stack.
- The Program Counter is loaded with the address of the corresponding ISR from the Interrupt Vector Table (IVT).
- The microcontroller executes the code inside the ISR.
- Crucially, the programmer must clear the interrupt flag in the ISR. If not cleared, the ISR will be called again immediately after it finishes.
- 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. - The main program resumes exactly where it left off.
Key Interrupt Registers
-
INTCONRegister: 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.,TMR1IEfor Timer1,TXIEfor 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). -
RCONRegister: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
- Enable Priority Levels: Set
RCONbits.IPEN = 1;to enable the priority interrupt system. - Configure the Timer: Set up the timer (e.g., Timer0) as desired (mode, clock source, prescaler).
- Load Initial Value: Load the
TMRxLandTMRxHregisters with the starting count. - Clear the Interrupt Flag: Clear the timer's interrupt flag (
TMRxIF) before enabling interrupts to avoid an immediate, false trigger. - Enable the Specific Interrupt: Set the timer's interrupt enable bit (e.g.,
INTCONbits.TMR0IE = 1;). - Set Priority (Optional but Recommended): Set the priority bit for the interrupt (e.g.,
INTCON2bits.TMR0IP = 1;for high priority). - Enable Global Interrupts: Enable high-priority (
GIEH) and/or low-priority (GIEL) global interrupts. - 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:
// 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.
#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
INTCONRegister:INT0IF: External Interrupt 0 Flag.INT0IE: External Interrupt 0 Enable.
INTCON2Register:INTEDG0,INTEDG1,INTEDG2: Interrupt Edge Select bits. (1 = Interrupt on rising edge, 0 = Interrupt on falling edge).
INTCON3Register: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)
- Configure Pin Direction: Set the corresponding pin (e.g., RB0 for INT0) as an input.
TRISBbits.TRISB0 = 1;. - Enable Priority Levels:
RCONbits.IPEN = 1;. - Select Trigger Edge: Configure
INTCON2bits.INTEDG0for rising (1) or falling (0) edge. A button press is typically detected on a falling edge. - Clear the Interrupt Flag:
INTCONbits.INT0IF = 0;. - Enable the Specific Interrupt:
INTCONbits.INT0IE = 1;. - Enable Global Interrupts:
INTCONbits.GIEH = 1;. - 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.
#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.
}
}