0% found this document useful (0 votes)
37 views30 pages

PIC Microcontroller Programming Manual

Uploaded by

Ibrahim
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
37 views30 pages

PIC Microcontroller Programming Manual

Uploaded by

Ibrahim
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 30

PIC Microcontroller Programming Guide: From

Basics to Advanced Applications


Table of Contents
1. Introduction to PIC Microcontrollers
2. Development Environment Setup
3. PIC Architecture and Memory Organization
4. Programming Fundamentals with XC8
5. Digital Input/Output Operations
6. Analog-to-Digital Conversion
7. Timers and Interrupts
8. Serial Communication (UART, SPI, I2C)
9. PWM and Motor Control
10. Advanced Programming Techniques
11. Debugging and Troubleshooting
12. Project Examples and Applications

1. Introduction to PIC Microcontrollers


PIC (Peripheral Interface Controller) microcontrollers, developed by Microchip Technology, are
among the most popular 8-bit microcontrollers in embedded systems. Known for their reliability,
wide availability, and extensive development ecosystem, PIC microcontrollers power countless
applications from simple LED controllers to complex industrial automation systems.

PIC Microcontroller Families


Microchip offers several PIC families, each optimized for different applications:
PIC10 Series: Ultra-low-cost, 6-8 pin devices for simple applications
Limited I/O pins (3-6 pins)
1KB program memory maximum
Ideal for cost-sensitive applications
PIC12 Series: 8-pin devices with more features
6 I/O pins available
Up to 8KB program memory
Built-in oscillator options
Suitable for small embedded systems
PIC16 Series: Mid-range 8-bit microcontrollers (most popular)
14-bit instruction word
Harvard architecture
Up to 64KB program memory
Rich peripheral set including ADC, timers, UART
Examples: PIC16F877A, PIC16F18875
PIC18 Series: Enhanced 8-bit microcontrollers
16-bit instruction word
Extended instruction set
Up to 128KB program memory
Advanced peripherals and DMA
Better suited for complex applications
PIC24 and dsPIC: 16-bit microcontrollers
Higher performance and precision
Digital Signal Processing capabilities
Complex control applications

Key Features of PIC Microcontrollers


Harvard Architecture: Separate memory spaces for program and data, allowing simultaneous
access and improving performance.
RISC Instruction Set: Reduced Instruction Set Computer architecture with 35 instructions
(PIC16) provides predictable execution times and efficient code.
Peripheral Integration: Built-in peripherals eliminate the need for external components:
Analog-to-Digital Converters (ADC)
Timers and PWM modules
Serial communication interfaces
Comparators and operational amplifiers
Low Power Consumption: Multiple sleep modes and power management features extend
battery life in portable applications.
Wide Operating Voltage: Typically 2V to 5.5V operation accommodates various power supply
requirements.
Applications and Use Cases
PIC microcontrollers excel in numerous applications:
Industrial Control: Process control, motor drives, sensor interfaces
Automotive: Engine management, body control modules, dashboard displays
Consumer Electronics: Remote controls, appliances, toys
Medical Devices: Patient monitoring, diagnostic equipment
IoT and Smart Devices: Sensor nodes, home automation, wearables
Educational Projects: Learning platforms, robotics, science fair projects

Development Ecosystem
Microchip provides comprehensive development tools:
MPLAB X IDE: Free integrated development environment with project management, editor,
debugger, and programmer interface.
XC Compilers: Optimized C compilers for all PIC families (XC8 for 8-bit PICs).
PICkit Programmers: Hardware tools for programming and debugging PIC microcontrollers in-
circuit.
MPLAB Code Configurator (MCC): Graphical tool for peripheral configuration and code
generation.
Development Boards: Various evaluation and development boards for prototyping.

2. Development Environment Setup

MPLAB X IDE Installation


MPLAB X IDE is the official development environment for PIC microcontrollers, built on the
NetBeans platform.
System Requirements:
Windows 10/11, macOS 10.14+, or Linux
4GB RAM minimum (8GB recommended)
2GB free disk space
Java 8 or later (included with installer)
Installation Steps:
1. Download MPLAB X IDE from Microchip's website
2. Run the installer as administrator
3. Select components: IDE, XC8 Compiler, and PICkit drivers
4. Complete the installation and restart the system
XC8 Compiler Setup
The XC8 compiler translates C code into PIC assembly language and machine code.
Compiler Versions:
Free Version: No optimization limitations, suitable for learning and small projects
Standard/PRO: Advanced optimizations for commercial development
Installation Process:
1. Download XC8 compiler from Microchip
2. Install with default settings
3. Verify installation in MPLAB X IDE under Tools → Options → Embedded

PICkit Programmer Configuration


PICkit programmers provide the interface between your development computer and the target
PIC microcontroller.
PICkit 3/4/5 Features:
In-Circuit Serial Programming (ICSP)
In-Circuit Debugging (ICD)
Target power supply (limited current)
USB connectivity
Connection Setup:

PICkit Connector PIC Pin Function


1 MCLR Master Clear (Reset)
2 VDD Power Supply
3 VSS Ground
4 PGD Program Data
5 PGC Program Clock
6 NC Not Connected

Creating Your First Project


Project Creation Steps:
1. Open MPLAB X IDE
2. File → New Project
3. Select "Microchip Embedded" → "Standalone Project"
4. Choose device (e.g., PIC16F877A)
5. Select tool (PICkit 3/4)
6. Select compiler (XC8)
7. Name your project and set location
Project Structure:

MyProject/
├── main.c // Main application code
├── config.h // Configuration settings
├── nbproject/ // IDE project files
├── build/ // Compiled output
└── dist/ // Final hex files

Basic Project Template

/*
* File: main.c
* Author: Developer
* Description: Basic PIC16F877A template
* Created: Date
*/

// Configuration bits
#pragma config FOSC = HS // High-speed crystal oscillator
#pragma config WDTE = OFF // Watchdog timer disabled
#pragma config PWRTE = ON // Power-up timer enabled
#pragma config BOREN = ON // Brown-out reset enabled
#pragma config LVP = OFF // Low-voltage programming disabled
#pragma config CPD = OFF // Data EEPROM code protection disabled
#pragma config WRT = OFF // Flash program memory write protection disabled
#pragma config CP = OFF // Flash program memory code protection disabled

#include <xc.h>
#include <stdio.h>
#include <stdlib.h>

// Define CPU frequency for delay functions


#define _XTAL_FREQ 20000000 // 20MHz crystal

// Function prototypes
void init_system(void);
void main_loop(void);

int main() {
init_system();

while(1) {
main_loop();
}

return 0;
}

void init_system(void) {
// Initialize system here
// Configure ports, peripherals, etc.
}

void main_loop(void) {
// Main application logic
}

3. PIC Architecture and Memory Organization

Harvard Architecture
PIC microcontrollers use Harvard architecture, which separates program and data memory into
different address spaces. This architecture provides several advantages:
Simultaneous Access: The processor can fetch instructions while accessing data, improving
performance.
Different Word Sizes: Program memory uses wider words (14-bit for PIC16) while data memory
uses 8-bit words.
Security: Separate memory spaces prevent accidental program modification.

Program Memory
Program memory stores the application code and is organized in words rather than bytes.
PIC16 Program Memory:
14-bit wide instructions
Word-addressable (not byte-addressable)
Size varies by device: 1K to 32K words
Contains reset vector at address 0x0000
Interrupt vector at address 0x0004
Memory Organization:

Address Function
0x0000 Reset Vector (GOTO main)
0x0004 Interrupt Vector
0x0005 Main program starts here
... Application code
0x2007 Configuration bits (PIC16F877A)

Data Memory Organization


Data memory is divided into multiple banks, each containing Special Function Registers (SFRs)
and General Purpose Registers (GPRs).
Bank Structure (PIC16F877A):
Bank 0 (00h-7Fh):
00h-1Fh: SFRs (INDF, TMR0, PCL, STATUS, etc.)
20h-7Fh: GPR (96 bytes)

Bank 1 (80h-FFh):
80h-9Fh: SFRs (OPTION_REG, TRISA, TRISB, etc.)
A0h-FFh: GPR (96 bytes)

Bank 2 (100h-17Fh):
100h-10Fh: SFRs (TMR1L, TMR1H, etc.)
110h-17Fh: GPR (112 bytes)

Bank 3 (180h-1FFh):
180h-18Fh: SFRs (Additional control registers)
190h-1FFh: GPR (112 bytes)

Special Function Registers (SFRs)


SFRs control the microcontroller's operation and peripheral functions.
Core Registers:

// STATUS Register bits


#define STATUS_C 0x01 // Carry bit
#define STATUS_DC 0x02 // Digit carry bit
#define STATUS_Z 0x04 // Zero bit
#define STATUS_PD 0x08 // Power-down bit
#define STATUS_TO 0x10 // Time-out bit
#define STATUS_RP0 0x20 // Register bank select bit 0
#define STATUS_RP1 0x40 // Register bank select bit 1
#define STATUS_IRP 0x80 // Indirect addressing bank select

// OPTION_REG Register bits


#define OPTION_PS0 0x01 // Prescaler rate select bit 0
#define OPTION_PS1 0x02 // Prescaler rate select bit 1
#define OPTION_PS2 0x04 // Prescaler rate select bit 2
#define OPTION_PSA 0x08 // Prescaler assignment bit
#define OPTION_T0SE 0x10 // TMR0 source edge select
#define OPTION_T0CS 0x20 // TMR0 clock source select
#define OPTION_INTEDG 0x40 // Interrupt edge select
#define OPTION_RBPU 0x80 // PORTB pull-up enable

Port Registers:

// Each port has three associated registers:


// PORTx: Read port pins, write to latch
// TRISx: Data direction (1=input, 0=output)
// LATx: Output latch (on newer devices)

// Example for PORTA


volatile unsigned char PORTA @ 0x05; // Port A data register
volatile unsigned char TRISA @ 0x85; // Port A direction register

Bank Switching
Accessing different register banks requires setting the RP0 and RP1 bits in the STATUS register.
Manual Bank Switching:

// Switch to Bank 1
STATUS |= 0x20; // Set RP0
// or
BSF STATUS, RP0

// Switch to Bank 0
STATUS &= 0xDF; // Clear RP0
// or
BCF STATUS, RP0

// Switch to Bank 2
STATUS |= 0x40; // Set RP1
STATUS &= 0xDF; // Clear RP0

// Switch to Bank 3
STATUS |= 0x60; // Set RP1 and RP0

Compiler Bank Switching:


The XC8 compiler handles bank switching automatically in most cases, but you can force
specific banks:

// Automatic bank switching (recommended)


TRISA = 0x00; // Compiler generates bank switching code

// Manual bank switching (when needed)


asm("BSF STATUS, RP0"); // Inline assembly
TRISA = 0x00;
asm("BCF STATUS, RP0");

EEPROM Data Memory


Many PIC microcontrollers include built-in EEPROM for non-volatile data storage.
EEPROM Characteristics:
Retains data without power
Limited write/erase cycles (100K-1M typical)
Byte-addressable
Size varies: 64 bytes to 1KB
EEPROM Access Registers:
// EEPROM control registers (Bank 2/3)
volatile unsigned char EEDATA @ 0x10C; // EEPROM data register
volatile unsigned char EEADR @ 0x10D; // EEPROM address register
volatile unsigned char EEDATH @ 0x10E; // EEPROM high data register
volatile unsigned char EEADRH @ 0x10F; // EEPROM high address register
volatile unsigned char EECON1 @ 0x18C; // EEPROM control register 1
volatile unsigned char EECON2 @ 0x18D; // EEPROM control register 2

4. Programming Fundamentals with XC8

XC8 Compiler Overview


The XC8 compiler is specifically designed for 8-bit PIC microcontrollers, providing efficient code
generation and extensive optimization capabilities.
Compiler Features:
Full ANSI C compliance
Integrated assembler and linker
Advanced optimizations
Built-in delay functions
Peripheral libraries
Memory management tools

Data Types and Memory Usage


Standard Data Types:

// Integer types
char // 8-bit signed (-128 to 127)
unsigned char // 8-bit unsigned (0 to 255)
int // 16-bit signed (-32768 to 32767)
unsigned int // 16-bit unsigned (0 to 65535)
long // 32-bit signed
unsigned long // 32-bit unsigned
short // 16-bit signed (same as int)

// Floating point
float // 32-bit IEEE 754 format
double // Same as float (32-bit)

// Boolean
bit // Single bit variable (XC8 specific)
_Bool // C99 boolean type

// Examples
unsigned char led_state; // 8-bit variable
int sensor_value; // 16-bit variable
unsigned long timestamp; // 32-bit variable
bit button_pressed; // Single bit

Memory Qualifiers:

// Bank specification
bank1 unsigned char config_reg; // Variable in Bank 1
bank2 unsigned char timer_data; // Variable in Bank 2

// Persistent storage
persistent unsigned char counter; // Survives reset
eeprom unsigned char settings[10]; // EEPROM storage

// Absolute addressing
unsigned char control_reg @ 0x20; // Fixed address

// Bit variables
bit flag1 @ (unsigned)&PORTA*8+0; // Bit 0 of PORTA
bit flag2 @ (unsigned)&PORTA*8+1; // Bit 1 of PORTA

Configuration Bits
Configuration bits control fundamental microcontroller operation and must be set correctly for
proper function.
Common Configuration Options:

// Oscillator configuration
#pragma config FOSC = HS // High-speed crystal/resonator
#pragma config FOSC = XT // Crystal/resonator
#pragma config FOSC = LP // Low-power crystal
#pragma config FOSC = RC // Resistor-capacitor oscillator
#pragma config FOSC = INTRC_NOCLKOUT // Internal RC, no clock output
#pragma config FOSC = INTRC_CLKOUT // Internal RC, clock output

// Watchdog timer
#pragma config WDTE = ON // Watchdog enabled
#pragma config WDTE = OFF // Watchdog disabled

// Power-up timer
#pragma config PWRTE = ON // Power-up timer enabled
#pragma config PWRTE = OFF // Power-up timer disabled

// Brown-out reset
#pragma config BOREN = ON // Brown-out reset enabled
#pragma config BOREN = OFF // Brown-out reset disabled

// Low-voltage programming
#pragma config LVP = ON // Low-voltage ICSP enabled
#pragma config LVP = OFF // Low-voltage ICSP disabled

// Code protection
#pragma config CP = ON // Code protection enabled
#pragma config CP = OFF // Code protection disabled

// Data EEPROM code protection


#pragma config CPD = ON // Data code protection enabled
#pragma config CPD = OFF // Data code protection disabled

// Write protection
#pragma config WRT = ON // Write protection enabled
#pragma config WRT = OFF // Write protection disabled

Complete Configuration Example:

// PIC16F877A configuration
#pragma config FOSC = HS // 20MHz crystal
#pragma config WDTE = OFF // Disable watchdog
#pragma config PWRTE = ON // Enable power-up timer
#pragma config BOREN = ON // Enable brown-out reset
#pragma config LVP = OFF // Disable low-voltage programming
#pragma config CPD = OFF // Disable data protection
#pragma config WRT = OFF // Disable write protection
#pragma config DEBUG = OFF // Disable debug mode
#pragma config CP = OFF // Disable code protection

Delay Functions
XC8 provides built-in delay functions that generate precise delays based on the system clock
frequency.
Delay Function Setup:

// Define system frequency for delay calculations


#define _XTAL_FREQ 20000000 // 20MHz clock frequency

// Available delay functions


__delay_us(microseconds); // Microsecond delay
__delay_ms(milliseconds); // Millisecond delay

// Examples
__delay_us(10); // 10 microsecond delay
__delay_ms(100); // 100 millisecond delay
__delay_ms(1000); // 1 second delay

Custom Delay Functions:

// Software delay using loops


void delay_cycles(unsigned int cycles) {
while(cycles--) {
asm("NOP"); // No operation instruction
}
}

// Configurable delay function


void delay_ms(unsigned int ms) {
while(ms--) {
__delay_ms(1);
}
}

// Non-blocking delay using timers


typedef struct {
unsigned long start_time;
unsigned long duration;
bit active;
} delay_timer_t;

delay_timer_t timer1;

void start_delay_timer(delay_timer_t* timer, unsigned long duration_ms) {


timer->start_time = get_system_time_ms();
timer->duration = duration_ms;
timer->active = 1;
}

bit check_delay_timer(delay_timer_t* timer) {


if(timer->active) {
if((get_system_time_ms() - timer->start_time) >= timer->duration) {
timer->active = 0;
return 1; // Timer expired
}
}
return 0; // Timer still running or not active
}

5. Digital Input/Output Operations

Port Configuration
PIC microcontrollers organize I/O pins into ports (PORTA, PORTB, PORTC, etc.), with each port
having associated control registers.
Port Control Registers:

// Three main registers per port:


// PORTx - Data register (read input, write output)
// TRISx - Direction register (1=input, 0=output)
// LATx - Latch register (newer devices, write-only)

// Example: Configuring PORTB


TRISB = 0x00; // All pins as outputs
PORTB = 0x00; // Initialize all pins low

TRISB = 0xFF; // All pins as inputs


unsigned char input_data = PORTB; // Read input values
// Mixed configuration
TRISB = 0xF0; // RB7-RB4 inputs, RB3-RB0 outputs

Individual Pin Control:

// Using bit manipulation macros


#define LED1_PIN PORTDbits.RD0
#define LED1_TRIS TRISDbits.TRISD0

#define BUTTON_PIN PORTBbits.RB0


#define BUTTON_TRIS TRISBbits.TRISB0

// Configuration
void init_gpio(void) {
// Configure LED pin as output
LED1_TRIS = 0; // Output
LED1_PIN = 0; // Initially off

// Configure button pin as input


BUTTON_TRIS = 1; // Input

// Enable internal pull-up on PORTB


OPTION_REGbits.nRBPU = 0; // Enable pull-ups
WPUBbits.WPUB0 = 1; // Pull-up on RB0
}

Digital Output Control


LED Control Example:

#include <xc.h>
#define _XTAL_FREQ 20000000

// LED definitions
#define LED1 PORTDbits.RD0
#define LED2 PORTDbits.RD1
#define LED3 PORTDbits.RD2
#define LED4 PORTDbits.RD3

void init_leds(void) {
// Configure LED pins as outputs
TRISDbits.TRISD0 = 0;
TRISDbits.TRISD1 = 0;
TRISDbits.TRISD2 = 0;
TRISDbits.TRISD3 = 0;

// Turn off all LEDs initially


LED1 = LED2 = LED3 = LED4 = 0;
}

void led_sequence(void) {
// Running light pattern
LED1 = 1; __delay_ms(200); LED1 = 0;
LED2 = 1; __delay_ms(200); LED2 = 0;
LED3 = 1; __delay_ms(200); LED3 = 0;
LED4 = 1; __delay_ms(200); LED4 = 0;
}

void binary_counter(unsigned char value) {


// Display 4-bit binary value on LEDs
LED1 = (value & 0x01) ? 1 : 0;
LED2 = (value & 0x02) ? 1 : 0;
LED3 = (value & 0x04) ? 1 : 0;
LED4 = (value & 0x08) ? 1 : 0;
}

int main(void) {
init_leds();
unsigned char counter = 0;

while(1) {
led_sequence();
__delay_ms(500);

binary_counter(counter++);
__delay_ms(1000);

if(counter > 15) counter = 0;


}

return 0;
}

Seven-Segment Display Control:

// Seven-segment display patterns (common cathode)


const unsigned char seven_seg_digits[10] = {
0x3F, // 0: segments a,b,c,d,e,f
0x06, // 1: segments b,c
0x5B, // 2: segments a,b,d,e,g
0x4F, // 3: segments a,b,c,d,g
0x66, // 4: segments b,c,f,g
0x6D, // 5: segments a,c,d,f,g
0x7D, // 6: segments a,c,d,e,f,g
0x07, // 7: segments a,b,c
0x7F, // 8: segments a,b,c,d,e,f,g
0x6F // 9: segments a,b,c,d,f,g
};

void display_digit(unsigned char digit) {


if(digit <= 9) {
PORTD = seven_seg_digits[digit];
}
}

void display_number(unsigned int number) {


// Display 4-digit number with multiplexing
unsigned char digits[4];
// Extract individual digits
digits[0] = number % 10; // Units
digits[1] = (number / 10) % 10; // Tens
digits[2] = (number / 100) % 10; // Hundreds
digits[3] = (number / 1000) % 10; // Thousands

// Multiplex display (simplified)


for(int i = 0; i < 4; i++) {
// Select digit position (using PORTB for digit selection)
PORTB = (1 << i);

// Display digit pattern


PORTD = seven_seg_digits[digits[i]];

__delay_ms(5); // Short delay for persistence of vision


}
}

Digital Input Reading


Button Input Handling:

#define BUTTON1 PORTBbits.RB0


#define BUTTON2 PORTBbits.RB1
#define SWITCH1 PORTBbits.RB2

typedef struct {
unsigned char current_state;
unsigned char previous_state;
unsigned char pressed;
unsigned char released;
unsigned int debounce_timer;
} button_t;

button_t button1, button2;

void init_buttons(void) {
// Configure as inputs
TRISBbits.TRISB0 = 1;
TRISBbits.TRISB1 = 1;
TRISBbits.TRISB2 = 1;

// Enable weak pull-ups (if available)


OPTION_REGbits.nRBPU = 0; // Enable PORTB pull-ups
WPUBbits.WPUB0 = 1; // Pull-up on RB0
WPUBbits.WPUB1 = 1; // Pull-up on RB1
WPUBbits.WPUB2 = 1; // Pull-up on RB2

// Initialize button structures


button1.current_state = 1;
button1.previous_state = 1;
button2.current_state = 1;
button2.previous_state = 1;
}
void update_button(button_t* btn, unsigned char pin_state) {
btn->previous_state = btn->current_state;
btn->current_state = pin_state;

// Debouncing
if(btn->current_state != btn->previous_state) {
btn->debounce_timer = 0;
} else {
if(btn->debounce_timer < 50) {
btn->debounce_timer++;
}
}

// Edge detection (after debouncing)


if(btn->debounce_timer >= 10) { // 10ms debounce
btn->pressed = (btn->previous_state == 1) && (btn->current_state
btn->released = (btn->previous_state == 0) && (btn->current_stat
} else {
btn->pressed = 0;
btn->released = 0;
}
}

int main(void) {
init_buttons();
init_leds();

unsigned char led_state = 0;

while(1) {
// Update button states (call every 1ms)
update_button(&button1, BUTTON1);
update_button(&button2, BUTTON2);

// Button 1 toggles LED


if(button1.pressed) {
led_state = !led_state;
LED1 = led_state;
}

// Button 2 controls LED while pressed


LED2 = !button2.current_state;

__delay_ms(1);
}

return 0;
}

Keypad Interface:

// 4x4 keypad interface


#define KEYPAD_ROWS 4
#define KEYPAD_COLS 4
// Row pins (outputs) - PORTD
#define ROW1 PORTDbits.RD0
#define ROW2 PORTDbits.RD1
#define ROW3 PORTDbits.RD2
#define ROW4 PORTDbits.RD3

// Column pins (inputs) - PORTB


#define COL1 PORTBbits.RB0
#define COL2 PORTBbits.RB1
#define COL3 PORTBbits.RB2
#define COL4 PORTBbits.RB3

const char keypad_map[KEYPAD_ROWS][KEYPAD_COLS] = {


{'1', '2', '3', 'A'},
{'4', '5', '6', 'B'},
{'7', '8', '9', 'C'},
{'*', '0', '#', 'D'}
};

void init_keypad(void) {
// Configure rows as outputs
TRISDbits.TRISD0 = 0;
TRISDbits.TRISD1 = 0;
TRISDbits.TRISD2 = 0;
TRISDbits.TRISD3 = 0;

// Configure columns as inputs with pull-ups


TRISBbits.TRISB0 = 1;
TRISBbits.TRISB1 = 1;
TRISBbits.TRISB2 = 1;
TRISBbits.TRISB3 = 1;

// Enable weak pull-ups


OPTION_REGbits.nRBPU = 0;
WPUB = 0x0F; // Pull-ups on RB0-RB3

// Initialize all rows high


PORTD |= 0x0F;
}

char scan_keypad(void) {
unsigned char row, col;
unsigned char col_data;

for(row = 0; row < KEYPAD_ROWS; row++) {


// Set all rows high
PORTD |= 0x0F;

// Set current row low


PORTD &= ~(1 << row);

__delay_us(10); // Settling time

// Read columns
col_data = PORTB & 0x0F;
if(col_data != 0x0F) { // Key pressed
// Find which column
for(col = 0; col < KEYPAD_COLS; col++) {
if(!(col_data & (1 << col))) {
// Debounce
__delay_ms(20);

// Verify key still pressed


if(!(PORTB & (1 << col))) {
// Wait for key release
while(!(PORTB & (1 << col)));
__delay_ms(20); // Debounce release

return keypad_map[row][col];
}
}
}
}
}

return 0; // No key pressed


}

6. Analog-to-Digital Conversion

ADC Module Overview


The Analog-to-Digital Converter (ADC) module converts analog voltages to digital values,
enabling the microcontroller to interface with analog sensors and signals.
ADC Specifications (PIC16F877A):
10-bit resolution (0-1023 digital values)
8 analog input channels (AN0-AN7)
Reference voltage options: VDD, VSS, external VREF+/VREF-
Conversion time: 12-20 μs typical
Sample and hold capability
ADC Control Registers:

// ADCON0 - ADC Control Register 0


typedef union {
struct {
unsigned ADON:1; // ADC enable bit
unsigned :1;
unsigned GO_DONE:1; // ADC conversion status
unsigned CHS:3; // Channel select bits
unsigned ADCS:2; // ADC clock select
};
unsigned char value;
} ADCON0_t;

// ADCON1 - ADC Control Register 1


typedef union {
struct {
unsigned PCFG:4; // Port configuration bits
unsigned :2;
unsigned ADCS2:1; // ADC clock select bit 2
unsigned ADFM:1; // Result format select
};
unsigned char value;
} ADCON1_t;

// ADC result registers


volatile unsigned char ADRESL @ 0x9E; // Low byte
volatile unsigned char ADRESH @ 0x1E; // High byte

ADC Configuration
Basic ADC Setup:

void init_adc(void) {
// Configure analog pins
// PCFG bits determine which pins are analog/digital
ADCON1bits.PCFG = 0x0E; // AN0 analog, rest digital
// PCFG = 0x00: All analog
// PCFG = 0x0F: All digital

// Configure ADC clock


ADCON0bits.ADCS = 0x02; // Fosc/32
ADCON1bits.ADCS2 = 0;
// Clock options:
// 000: Fosc/2 001: Fosc/8
// 010: Fosc/32 011: FRC (internal RC)
// 100: Fosc/4 101: Fosc/16
// 110: Fosc/64 111: FRC

// Result format
ADCON1bits.ADFM = 1; // Right justified
// ADFM = 1: ADRESH:ADRESL = 00000000:00xxxxxx (10-bit)
// ADFM = 0: ADRESH:ADRESL = xxxxxxxx:xx000000 (10-bit)

// Enable ADC module


ADCON0bits.ADON = 1;

// Allow settling time


__delay_us(20);
}

unsigned int read_adc(unsigned char channel) {


// Select channel (0-7)
ADCON0bits.CHS = channel & 0x07;

// Wait for channel selection to settle


__delay_us(20);
// Start conversion
ADCON0bits.GO_DONE = 1;

// Wait for conversion to complete


while(ADCON0bits.GO_DONE);

// Return 10-bit result


if(ADCON1bits.ADFM) {
// Right justified
return ((unsigned int)ADRESH << 8) | ADRESL;
} else {
// Left justified
return ((unsigned int)ADRESH << 2) | (ADRESL >> 6);
}
}

Multi-Channel ADC Reading:

#define NUM_ADC_CHANNELS 4

typedef struct {
unsigned int raw_value;
float voltage;
unsigned char channel;
} adc_reading_t;

adc_reading_t adc_channels[NUM_ADC_CHANNELS];

void init_multi_channel_adc(void) {
// Configure pins AN0-AN3 as analog
ADCON1bits.PCFG = 0x0B; // AN0-AN3 analog, others digital

// Configure ADC
ADCON0bits.ADCS = 0x02; // Fosc/32
ADCON1bits.ADCS2 = 0;
ADCON1bits.ADFM = 1; // Right justified
ADCON0bits.ADON = 1; // Enable ADC

__delay_us(20);

// Initialize channel structure


for(int i = 0; i < NUM_ADC_CHANNELS; i++) {
adc_channels[i].channel = i;
adc_channels[i].raw_value = 0;
adc_channels[i].voltage = 0.0;
}
}

void read_all_adc_channels(void) {
for(int i = 0; i < NUM_ADC_CHANNELS; i++) {
adc_channels[i].raw_value = read_adc(i);

// Convert to voltage (assuming 5V reference)


adc_channels[i].voltage = (adc_channels[i].raw_value * 5.0) / 1023.0;
}
}

// Get averaged reading to reduce noise


unsigned int read_adc_averaged(unsigned char channel, unsigned char samples) {
unsigned long sum = 0;

for(unsigned char i = 0; i < samples; i++) {


sum += read_adc(channel);
__delay_us(100); // Small delay between samples
}

return (unsigned int)(sum / samples);


}

Sensor Interface Examples


Temperature Sensor (LM35):

// LM35: 10mV/°C, 0°C = 0V


float read_temperature_lm35(unsigned char channel) {
unsigned int adc_value = read_adc_averaged(channel, 16);
float voltage = (adc_value * 5.0) / 1023.0;
float temperature = voltage * 100.0; // LM35: 10mV/°C
return temperature;
}

// Temperature with calibration


typedef struct {
float offset;
float scale;
} temp_calibration_t;

temp_calibration_t temp_cal = {0.0, 1.0}; // Default: no correction

float read_calibrated_temperature(unsigned char channel) {


float raw_temp = read_temperature_lm35(channel);
return (raw_temp * temp_cal.scale) + temp_cal.offset;
}

Light Sensor (LDR with voltage divider):

// Light sensor using LDR in voltage divider


typedef enum {
LIGHT_DARK,
LIGHT_DIM,
LIGHT_NORMAL,
LIGHT_BRIGHT
} light_level_t;

light_level_t read_light_level(unsigned char channel) {


unsigned int adc_value = read_adc_averaged(channel, 8);
// Define thresholds based on your circuit
if(adc_value < 200) {
return LIGHT_DARK;
} else if(adc_value < 500) {
return LIGHT_DIM;
} else if(adc_value < 800) {
return LIGHT_NORMAL;
} else {
return LIGHT_BRIGHT;
}
}

unsigned char get_light_percentage(unsigned char channel) {


unsigned int adc_value = read_adc_averaged(channel, 8);
return (unsigned char)((adc_value * 100UL) / 1023UL);
}

Potentiometer Reading:

// Potentiometer for user input/control


typedef struct {
unsigned int current_value;
unsigned int previous_value;
unsigned char changed;
unsigned char deadband;
} potentiometer_t;

potentiometer_t pot1;

void init_potentiometer(potentiometer_t* pot, unsigned char deadband) {


pot->current_value = 0;
pot->previous_value = 0;
pot->changed = 0;
pot->deadband = deadband;
}

void update_potentiometer(potentiometer_t* pot, unsigned char channel) {


pot->previous_value = pot->current_value;
pot->current_value = read_adc_averaged(channel, 4);

// Check for significant change (deadband filtering)


if(abs(pot->current_value - pot->previous_value) > pot->deadband) {
pot->changed = 1;
} else {
pot->changed = 0;
}
}

// Map potentiometer to different ranges


unsigned char pot_to_percent(potentiometer_t* pot) {
return (unsigned char)((pot->current_value * 100UL) / 1023UL);
}

unsigned char pot_to_range(potentiometer_t* pot, unsigned char min_val, unsigned char max
unsigned long range = max_val - min_val;
return min_val + (unsigned char)((pot->current_value * range) / 1023UL);
}

int main(void) {
init_adc();
init_potentiometer(&pot1, 10); // 10-count deadband

while(1) {
update_potentiometer(&pot1, 0); // Read from AN0

if(pot1.changed) {
unsigned char brightness = pot_to_percent(&pot1);
// Use brightness value for PWM control, etc.
}

__delay_ms(50); // Update every 50ms


}

return 0;
}

7. Timers and Interrupts

Timer Modules Overview


PIC microcontrollers include multiple timer modules for time measurement, event counting, and
periodic interrupt generation.
Timer Types:
Timer0 (TMR0): 8-bit timer with prescaler
Timer1 (TMR1): 16-bit timer with crystal oscillator capability
Timer2 (TMR2): 8-bit timer with postscaler, used for PWM

Timer0 Configuration
Timer0 Features:
8-bit counter (0-255)
Internal or external clock source
Programmable prescaler (1:2 to 1:256)
Interrupt on overflow (255 → 0)
Timer0 Control:

// OPTION_REG bits for Timer0


#define T0CS 5 // Timer0 Clock Source Select
#define T0SE 4 // Timer0 Source Edge Select
#define PSA 3 // Prescaler Assignment
#define PS2 2 // Prescaler Rate Select bit 2
#define PS1 1 // Prescaler Rate Select bit 1
#define PS0 0 // Prescaler Rate Select bit 0

// Prescaler values
typedef enum {
PRESCALER_2 = 0, // 000: 1:2
PRESCALER_4 = 1, // 001: 1:4
PRESCALER_8 = 2, // 010: 1:8
PRESCALER_16 = 3, // 011: 1:16
PRESCALER_32 = 4, // 100: 1:32
PRESCALER_64 = 5, // 101: 1:64
PRESCALER_128 = 6, // 110: 1:128
PRESCALER_256 = 7 // 111: 1:256
} prescaler_t;

void init_timer0(prescaler_t prescaler) {


// Configure Timer0
OPTION_REGbits.T0CS = 0; // Internal clock (Fosc/4)
OPTION_REGbits.T0SE = 0; // Rising edge (not used for internal clock)
OPTION_REGbits.PSA = 0; // Prescaler assigned to Timer0

// Set prescaler value


OPTION_REG = (OPTION_REG & 0xF8) | (prescaler & 0x07);

// Clear Timer0
TMR0 = 0;

// Enable Timer0 interrupt


INTCONbits.T0IE = 1; // Timer0 interrupt enable
INTCONbits.GIE = 1; // Global interrupt enable
}

Timer0 Interrupt Example:

volatile unsigned int timer0_ticks = 0;


volatile bit second_flag = 0;

// Timer0 interrupt service routine


void __interrupt() isr(void) {
if(INTCONbits.T0IF) {
// Clear interrupt flag
INTCONbits.T0IF = 0;

// Reload Timer0 for precise timing


TMR0 = 6; // Preload value for exact timing

timer0_ticks++;

// Generate 1-second flag (example: 1000 interrupts = 1 second)


if(timer0_ticks >= 1000) {
timer0_ticks = 0;
second_flag = 1;
}
}
}
int main(void) {
init_timer0(PRESCALER_64); // Configure Timer0

while(1) {
if(second_flag) {
second_flag = 0;
// Execute once per second
LED1 = !LED1; // Toggle LED
}

// Main program continues...


}

return 0;
}

Timer1 Configuration
Timer1 Features:
16-bit counter (0-65535)
Multiple clock sources
Crystal oscillator for precision timing
Capture/Compare capability

// Timer1 control registers


typedef union {
struct {
unsigned TMR1ON:1; // Timer1 enable
unsigned TMR1CS:1; // Clock source select
unsigned T1SYNC:1; // External clock synchronization
unsigned T1OSCEN:1; // Timer1 oscillator enable
unsigned T1CKPS:2; // Prescaler select
unsigned T1GE:1; // Timer1 gate enable
unsigned :1;
};
unsigned char value;
} T1CON_t;

// Timer1 registers
volatile unsigned char TMR1L @ 0x0E; // Timer1 low byte
volatile unsigned char TMR1H @ 0x0F; // Timer1 high byte

void init_timer1(void) {
// Configure Timer1
T1CONbits.TMR1CS = 0; // Internal clock (Fosc/4)
T1CONbits.T1CKPS = 3; // 1:8 prescaler
T1CONbits.T1OSCEN = 0; // Timer1 oscillator disabled
T1CONbits.T1SYNC = 1; // Do not synchronize (not used for internal clock)

// Clear Timer1
TMR1H = 0;
TMR1L = 0;

// Enable Timer1 interrupt


PIE1bits.TMR1IE = 1; // Timer1 interrupt enable
INTCONbits.PEIE = 1; // Peripheral interrupt enable
INTCONbits.GIE = 1; // Global interrupt enable

// Start Timer1
T1CONbits.TMR1ON = 1;
}

// Timer1 16-bit read/write functions


unsigned int read_timer1(void) {
unsigned char low, high;

// Read low byte first, then high byte


low = TMR1L;
high = TMR1H;

return ((unsigned int)high << 8) | low;


}

void write_timer1(unsigned int value) {


// Write high byte first, then low byte
TMR1H = (unsigned char)(value >> 8);
TMR1L = (unsigned char)(value & 0xFF);
}

// Precision delay using Timer1


void delay_timer1_ms(unsigned int milliseconds) {
unsigned int timer_value;

while(milliseconds--) {
// Calculate timer value for 1ms delay
// Assuming 20MHz clock, Fosc/4 = 5MHz, 1:8 prescaler = 625kHz
// For 1ms: 625 counts
timer_value = 65536 - 625;

write_timer1(timer_value);
PIR1bits.TMR1IF = 0; // Clear interrupt flag
T1CONbits.TMR1ON = 1; // Start timer

while(!PIR1bits.TMR1IF); // Wait for overflow

T1CONbits.TMR1ON = 0; // Stop timer


}
}

Timer2 and PWM


Timer2 is primarily used for PWM generation and has unique features for precise period control.

// Timer2 control register


typedef union {
struct {
unsigned T2CKPS:2; // Prescaler select
unsigned TMR2ON:1; // Timer2 enable
unsigned TOUTPS:4; // Postscaler select
unsigned :1;
};
unsigned char value;
} T2CON_t;

// PWM period register


volatile unsigned char PR2 @ 0x92;

void init_timer2_pwm(void) {
// Configure Timer2 for PWM
T2CONbits.T2CKPS = 0; // Prescaler 1:1
T2CONbits.TOUTPS = 0; // Postscaler 1:1

// Set PWM period (determines frequency)


PR2 = 249; // For ~1kHz PWM at 20MHz
// PWM frequency = Fosc / (4 * (PR2+1) * prescaler)
// = 20MHz / (4 * 250 * 1) = 20kHz

// Enable Timer2
T2CONbits.TMR2ON = 1;
}

Interrupt System
Interrupt Sources:
External interrupts (INT pin, PORTB change)
Timer overflows (TMR0, TMR1, TMR2)
Peripheral interrupts (USART, ADC, etc.)
Interrupt Control Registers:

// INTCON - Interrupt Control Register


typedef union {
struct {
unsigned RBIF:1; // PORTB interrupt flag
unsigned INTF:1; // External interrupt flag
unsigned T0IF:1; // Timer0 interrupt flag
unsigned RBIE:1; // PORTB interrupt enable
unsigned INTE:1; // External interrupt enable
unsigned T0IE:1; // Timer0 interrupt enable
unsigned PEIE:1; // Peripheral interrupt enable
unsigned GIE:1; // Global interrupt enable
};
unsigned char value;
} INTCON_t;

// PIE1 - Peripheral Interrupt Enable 1


typedef union {
struct {
unsigned TMR1IE:1; // Timer1 interrupt enable
unsigned TMR2IE:1; // Timer2 interrupt enable
unsigned CCP1IE:1; // CCP1 interrupt enable
unsigned SSPIE:1; // SSP interrupt enable
unsigned TXIE:1; // USART transmit interrupt enable
unsigned RCIE:1; // USART receive interrupt enable
unsigned ADIE:1; // ADC interrupt enable
unsigned :1;
};
unsigned char value;
} PIE1_t;

Comprehensive Interrupt Handler:

// Global interrupt variables


volatile bit timer0_flag = 0;
volatile bit timer1_flag = 0;
volatile bit external_int_flag = 0;
volatile bit portb_change_flag = 0;
volatile unsigned char portb_old_value;

void init_interrupts(void) {
// Configure external interrupt
OPTION_REGbits.INTEDG = 1; // Rising edge trigger
INTCONbits.INTE = 1; // Enable external interrupt

// Configure PORTB change interrupt


TRISBbits.TRISB4 = 1; // RB4 as input
TRISBbits.TRISB5 = 1; // RB5 as input
portb_old_value = PORTB; // Initialize old value
INTCONbits.RBIE = 1; // Enable PORTB change interrupt

// Enable peripheral interrupts


INTCONbits.PEIE = 1; // Peripheral interrupt enable

// Enable global interrupts


INTCONbits.GIE = 1; // Global interrupt enable
}

void __interrupt() interrupt_handler(void) {


// Timer0 interrupt
if(INTCONbits.T0IF && INTCONbits.T0IE) {
INTCONbits.T0IF = 0; // Clear flag
timer0_flag = 1; // Set application flag
TMR0 = 6; // Reload for precise timing
}

// Timer1 interrupt
if(PIR1bits.TMR1IF && PIE1bits.TMR1IE) {
PIR1bits.TMR1IF = 0; // Clear flag
timer1_flag = 1; // Set application flag
// Reload Timer1 if needed
write_timer1(15536); // Example reload value
}

// External interrupt (INT pin)


if(INTCONbits.INTF && INTCONbits.INTE) {
INTCONbits.INTF = 0; // Clear flag
external_int_flag = 1; // Set application flag
}

// PORTB change interrupt


if(INTCONbits.RBIF && INTCONbits.RBIE) {
INTCONbits.RBIF = 0; // Clear flag
portb_change_flag = 1; // Set application flag
portb_old_value = PORTB; // Update old value
}

// ADC conversion complete


if(PIR1bits.ADIF && PIE1bits.ADIE) {
PIR1bits.ADIF = 0; // Clear flag
// Handle ADC completion
}

// USART receive interrupt


if(PIR1bits.RCIF && PIE1bits.RCIE) {
// Note: RCIF is cleared automatically when RCREG is read
// Handle received data
unsigned char received_data = RCREG;
// Process received_data
}

// USART transmit interrupt


if(PIR1bits.TXIF && PIE1bits.TXIE) {
PIE1bits.TXIE = 0; // Disable interrupt (re-enable when needed)
// Handle transmission complete
}
}

// Application using interrupts


int main(void) {
init_system();
init_interrupts();
init_timer0(PRESCALER_64);
init_timer1();

while(1) {
// Handle Timer0 events
if(timer0_flag) {
timer0_flag = 0;
// Execute Timer0 periodic task
LED1 = !LED1;
}

// Handle Timer1 events


if(timer1_flag) {
timer1_flag = 0;
// Execute Timer1 periodic task
update_display();
}

// Handle external interrupt


if(external_int_flag) {
external_int_flag = 0;
// Process external event
handle_button_press();
}

// Handle PORTB change


if(portb_change_flag) {
portb_change_flag = 0;
// Process port change
check_switches();
}

// Main application tasks


process_sensors();
update_outputs();

// Small delay to prevent excessive polling


__delay_ms(1);
}

return 0;
}

This comprehensive guide provides the foundation for PIC microcontroller programming with
practical examples and real-world applications. The content covers essential topics from basic
I/O operations to advanced interrupt handling, enabling developers to create sophisticated
embedded systems using PIC microcontrollers.

You might also like