Unit 3 - Notes
Unit 3: PIC GPIO programming
1. Understanding GPIO and its Core Registers
General Purpose Input/Output (GPIO) pins are the fundamental interface between a microcontroller (MCU) and the external world. They can be configured to either drive a signal out (output) or read a signal in (input). In PIC microcontrollers, the behavior of these pins is controlled by a set of Special Function Registers (SFRs).
The primary registers for controlling a GPIO port (e.g., PORTA, PORTB, PORTC) are:
1.1 TRIS Register (Data Direction Register)
The TRIS (Tri-State) register is the most important register for GPIO configuration. It determines the direction of each pin in a port. Each bit in a TRIS register corresponds to a pin on the associated port.
1= Input: Setting a TRIS bit to1configures the corresponding pin as an input. The pin goes into a high-impedance state, effectively "listening" to the voltage level applied to it.0= Output: Clearing a TRIS bit to0configures the corresponding pin as an output. The MCU can now drive the pin to a high voltage level (Vdd) or a low voltage level (Vss/GND).
Syntax (MPLAB XC8 Compiler):
The x represents the port letter (e.g., A, B, C).
-
Configure a whole port:
CTRISB = 0x00; // Configure all 8 pins of PORTB as outputs TRISB = 0xFF; // Configure all 8 pins of PORTB as inputs TRISB = 0b00001111; // Configure RB0-RB3 as inputs, RB4-RB7 as outputs -
Configure a single pin (using bit-fields):
CTRISBbits.TRISB0 = 0; // Configure pin RB0 as an output TRISAbits.TRISA4 = 1; // Configure pin RA4 as an input
1.2 PORT Register (Port Data Register)
The PORT register has a dual function:
- Reading Inputs: When you read from a
PORTxregister, you get the current logical state (0 or 1) of the pins on that port. This is the primary way to read the status of an input pin. - Writing Outputs (Legacy): You can write to a
PORTxregister to set the state of output pins. However, this can lead to a "Read-Modify-Write" (RMW) problem. The MCU first reads the entire port's state, modifies the specific bit you want to change, and then writes the entire byte back. If an output pin is changing state at the exact moment of the read, the wrong value could be written back, causing unintended behavior.
1.3 LAT Register (Port Latch Register)
The LAT (Latch) register was introduced to solve the RMW problem.
- Writing Outputs (Modern/Recommended): When you write to a
LATxregister, the value is stored in an internal "latch" that is separate from the physical pin's read buffer. The output drivers are connected directly to this latch. This ensures that writing to one pin does not interfere with or depend on the state of any other pin on the same port.
Best Practice:
- Use
PORTxto READ from pins.
Cif (PORTBbits.RB0 == 1) { /* Do something */ } - Use
LATxto WRITE to pins.
CLATBbits.LATB1 = 1; // Set RB1 high
1.4 ANSEL / ANSELH Register (Analog Select Register)
Many PIC pins are multiplexed, meaning they can serve multiple functions (e.g., digital I/O, analog-to-digital converter input, comparator input).
- By default, many of these pins power up in analog mode to be compatible with legacy designs.
- In analog mode, the digital input buffer is disabled. If you try to read a digital value from a pin configured as an analog input, it will always read as
0. - You must clear the corresponding ANSEL bit to
0to use a pin for digital I/O.
Example (PIC18F4550):
// Pins on PORTA and PORTE are often analog by default
ANSEL = 0x00; // Configure PORTA pins as digital
ANSELH = 0x00; // Configure PORTB/PORTE pins as digital
// A newer equivalent for many PICs is the ANSELx register
ANSELA = 0x00;
ANSELB = 0x00;
Failing to configure ANSEL registers is one of the most common sources of error in PIC programming.
1.5 WPU Register (Weak Pull-Up Register)
The WPU (Weak Pull-Up) register enables internal, high-resistance resistors that connect the GPIO pin to Vdd. This is extremely useful for interfacing with switches and buttons, as it prevents the input pin from "floating" when a switch is open.
- Setting a bit in
WPUxenables the pull-up for the corresponding pin. - The pin must be configured as an input (
TRISxbit = 1). - Often, a global enable bit must also be set (e.g., on PIC18,
INTCON2bits.RBPU = 0;enables PORTB pull-ups).
2. Configuring PIC GPIO as Input/Output
A systematic approach ensures pins are configured correctly every time.
Configuration Checklist:
- Disable Analog Function: If the pin is multiplexed with an analog function, clear its corresponding bit in the
ANSELxregister. - Set Data Direction: Set the corresponding bit in the
TRISxregister (1for input,0for output). - For Inputs (Optional): Enable weak pull-up resistors using the
WPUxregister if needed to avoid floating inputs. - Write/Read: Use
LATxto write to output pins andPORTxto read from input pins.
Code Example:
// Target: PIC18F45K22
// Configure RD0 as output
// Configure RB1 as input with weak pull-up enabled
void main(void) {
// 1. Disable Analog on PORTB
ANSELBbits.ANSB1 = 0; // Set RB1 to digital mode
// 2. Set Data Direction
TRISDbits.TRISD0 = 0; // RD0 is an output
TRISBbits.TRISB1 = 1; // RB1 is an input
// 3. Enable Weak Pull-up for RB1
// First, enable PORTB pull-ups globally (this bit is inverted logic)
INTCON2bits.RBPU = 0;
// Then, enable the specific pull-up for RB1
WPUBbits.WPUB1 = 1;
while(1) {
// 4. Read from input and write to output
if (PORTBbits.RB1 == 0) { // Check if input RB1 is low
LATDbits.LATD0 = 1; // Turn on output RD0
} else {
LATDbits.LATD0 = 0; // Turn off output RD0
}
}
}
3. I/O Bit Manipulation Programming
Often, you need to change the state of a single pin without affecting the others on the same port. There are two primary methods to do this.
3.1 Using Bit-Fields (Compiler-Specific)
The MPLAB XC8 compiler provides predefined structs that allow you to access individual bits by name. This is the most readable and recommended method.
// Setting a bit (making a pin HIGH)
LATDbits.LATD0 = 1;
// Clearing a bit (making a pin LOW)
LATDbits.LATD0 = 0;
// Toggling a bit (inverting its state)
LATDbits.LATD0 = ~LATDbits.LATD0;
// Reading a bit
if (PORTBbits.RB1 == 1) {
// Pin RB1 is HIGH
}
3.2 Using Bitwise Operators (Standard C)
This method is portable across any C compiler and is fundamental to embedded programming. It involves using bitwise operators (|, &, ^, ~, <<).
-
Setting a Bit (Bitwise OR
|)
To set a bit, youORthe register with a "mask" where only the target bit is 1.
C// Set bit 2 (pin RC2) of PORTC LATC = LATC | 0b00000100; // More professionally written using bit-shifting: LATC |= (1 << 2); // Set bit 2 of LATC -
Clearing a Bit (Bitwise AND
&with NOT~)
To clear a bit, youANDthe register with a mask where only the target bit is 0. This is achieved by inverting (~) a mask where the bit is 1.
C// Clear bit 3 (pin RC3) of PORTC LATC = LATC & 0b11110111; // More professionally written using bit-shifting: LATC &= ~(1 << 3); // Clear bit 3 of LATC -
Toggling a Bit (Bitwise XOR
^)
To toggle (invert) a bit, youXORthe register with a mask where only the target bit is 1.
C// Toggle bit 4 (pin RC4) of PORTC LATC = LATC ^ 0b00010000; // More professionally written using bit-shifting: LATC ^= (1 << 4); // Toggle bit 4 of LATC -
Checking a Bit (Bitwise AND
&)
To check if a bit is set, youANDthe register with a mask. The result will be non-zero only if the bit was set.
C// Check if bit 1 (pin RB1) of PORTB is high if (PORTB & (1 << 1)) { // Pin RB1 is high }
4. LED Interfacing with PIC
4.1 Circuit Connections
An LED must be connected with a current-limiting resistor in series to prevent it from drawing too much current from the MCU pin and burning out either the LED or the pin. The resistor value can be calculated with Ohm's Law: R = (V_Source - V_Forward) / I_Forward.
V_Source: The MCU's supply voltage (e.g., 5V or 3.3V).V_Forward: The LED's forward voltage drop (typically ~1.8-2.2V for red, ~3.0-3.4V for blue/white).I_Forward: The desired LED current (a safe value is 10-20mA, or 0.01-0.02A).
For a 5V MCU and a red LED (2V drop, 15mA current): R = (5V - 2V) / 0.015A = 200Ω. A standard 220Ω resistor is a good choice.
There are two ways to connect an LED:
-
Current Sourcing:
MCU Pin -> Resistor -> LED Anode -> LED Cathode -> GND- To turn the LED ON, set the MCU pin HIGH (
LATx = 1). - The MCU "sources" or provides the current.
- To turn the LED ON, set the MCU pin HIGH (
-
Current Sinking:
VCC -> Resistor -> LED Anode -> LED Cathode -> MCU Pin- To turn the LED ON, set the MCU pin LOW (
LATx = 0). - The MCU "sinks" the current to ground.
- This is often preferred as many MCUs can sink more current per pin than they can source.
- To turn the LED ON, set the MCU pin LOW (
4.2 Programming Example: Blinking an LED
This program blinks an LED connected to pin RD0 once per second.
#include <xc.h>
// Configuration Bits (for a PIC18F4550)
#pragma config FOSC = HS // High-speed crystal oscillator
#pragma config WDT = OFF // Watchdog Timer disabled
#pragma config LVP = OFF // Low-Voltage Programming disabled
#define _XTAL_FREQ 20000000 // Define crystal frequency for delay functions (20MHz)
void main(void) {
// Configure RD0 as a digital output
TRISDbits.TRISD0 = 0; // Set RD0 as output
while (1) {
// Turn the LED ON (assuming current sourcing)
LATDbits.LATD0 = 1;
__delay_ms(500); // Wait for 500 milliseconds
// Turn the LED OFF
LATDbits.LATD0 = 0;
__delay_ms(500); // Wait for 500 milliseconds
}
}
5. Programming a Multi-Bit Binary Counter
This application uses an entire port to drive multiple LEDs, displaying a count in binary.
5.1 Concept and Circuit
Connect 8 LEDs (with their current-limiting resistors) to the 8 pins of a single port, for example, PORTD (RD0 to RD7). All LEDs are connected in a current sourcing configuration.
- LED for bit 0 is connected to RD0.
- LED for bit 1 is connected to RD1.
- ...
- LED for bit 7 is connected to RD7.
5.2 Programming Logic
The logic involves using a variable as a counter. The value of this variable is written directly to the entire port's Latch register in each iteration of a loop.
- Configure the entire PORTD as output (
TRISD = 0x00). - Initialize an 8-bit counter variable (e.g.,
unsigned char count = 0;). - Enter an infinite
whileloop. - Inside the loop:
a. Write thecountvariable's value to the latch:LATD = count;.
b. Increment the counter:count++;.
c. Add a delay to make the change visible to the human eye.
5.3 Code Example: 8-bit Binary Counter
#include <xc.h>
#pragma config FOSC = HS, WDT = OFF, LVP = OFF
#define _XTAL_FREQ 20000000
void main(void) {
unsigned char counter = 0; // 8-bit variable to hold the count
// Configure all PORTD pins as digital outputs
ANSELD = 0x00; // In case they have analog functions
TRISD = 0x00; // Set all 8 pins of PORTD as outputs
// Initialize LEDs to OFF state
LATD = 0x00;
while (1) {
LATD = counter; // Send the current count to the LEDs
__delay_ms(250); // Wait for 250ms
counter++; // Increment for the next loop
}
}
6. Switch Interfacing with PIC
6.1 The Problem of Floating Inputs
An input pin that is not connected to a defined voltage level (either Vdd or GND) is said to be "floating." Its voltage can drift randomly, causing the MCU to read unpredictable 0s and 1s. Switches, being mechanical, create an open circuit when not pressed, which leads to a floating input.
6.2 Circuit Connections: Pull-up and Pull-down Resistors
To solve the floating issue, we use resistors to "pull" the pin to a default state.
-
Pull-up Resistor:
- Circuit: A resistor connects the input pin to VCC. The switch connects the same pin to GND.
- Logic:
- Switch Open: The pin is pulled HIGH (reads
1) through the resistor. - Switch Pressed: The switch creates a direct path to GND, overriding the resistor. The pin is pulled LOW (reads
0).
- Switch Open: The pin is pulled HIGH (reads
- This is the most common configuration.
-
Pull-down Resistor:
- Circuit: A resistor connects the input pin to GND. The switch connects the same pin to VCC.
- Logic:
- Switch Open: The pin is pulled LOW (reads
0) through the resistor. - Switch Pressed: The switch creates a path to VCC. The pin is pulled HIGH (reads
1).
- Switch Open: The pin is pulled LOW (reads
-
Internal Weak Pull-ups:
- As discussed in Section 1.5, PIC MCUs contain internal weak pull-up resistors that can be enabled in software (
WPUxregisters). - This eliminates the need for external resistors, simplifying the circuit design. You only need to connect the switch from the pin to GND.
- As discussed in Section 1.5, PIC MCUs contain internal weak pull-up resistors that can be enabled in software (
6.3 Switch Debouncing
When a mechanical switch is pressed or released, the metal contacts can "bounce" for a few milliseconds, creating a series of rapid on-off signals. The fast MCU can interpret this as multiple presses. Debouncing is the process of ignoring these bounces. A simple software solution is to:
- Detect an initial press.
- Wait for a short period (e.g., 20ms).
- Read the switch again. If it's still in the pressed state, confirm it as a valid press.
6.4 Programming Example: LED control with a Switch
This program turns on an LED (on RD0) while a button (on RB0) is held down, using an internal weak pull-up.
#include <xc.h>
#pragma config FOSC = HS, WDT = OFF, LVP = OFF
#define _XTAL_FREQ 20000000
void main(void) {
// Configure LED pin RD0 as a digital output
TRISDbits.TRISD0 = 0;
LATDbits.LATD0 = 0; // Start with LED off
// Configure Switch pin RB0 as a digital input
ANSELBbits.ANSB0 = 0; // Digital mode for RB0
TRISBbits.TRISB0 = 1; // Input direction for RB0
// Enable internal weak pull-up for RB0
INTCON2bits.RBPU = 0; // Enable PORTB pull-ups globally (active low)
WPUBbits.WPUB0 = 1; // Enable pull-up on RB0 specifically
while (1) {
// Because of the pull-up, the pin is HIGH when the button is not pressed
// and LOW when it is pressed.
if (PORTBbits.RB0 == 0) { // Check if the button is pressed
__delay_ms(20); // Simple debounce delay
if(PORTBbits.RB0 == 0) { // Check again to confirm
LATDbits.LATD0 = 1; // Turn LED ON
}
} else {
LATDbits.LATD0 = 0; // Turn LED OFF
}
}
}