Unit 3 - Notes

ECE227 6 min read

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 to 1 configures 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 to 0 configures 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:

    C
        TRISB = 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):

    C
        TRISBbits.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:

  1. Reading Inputs: When you read from a PORTx register, 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.
  2. Writing Outputs (Legacy): You can write to a PORTx register 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 LATx register, 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 PORTx to READ from pins.
    C
        if (PORTBbits.RB0 == 1) { /* Do something */ }
        
  • Use LATx to WRITE to pins.
    C
        LATBbits.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 0 to use a pin for digital I/O.

Example (PIC18F4550):

C
// 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 WPUx enables the pull-up for the corresponding pin.
  • The pin must be configured as an input (TRISx bit = 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:

  1. Disable Analog Function: If the pin is multiplexed with an analog function, clear its corresponding bit in the ANSELx register.
  2. Set Data Direction: Set the corresponding bit in the TRISx register (1 for input, 0 for output).
  3. For Inputs (Optional): Enable weak pull-up resistors using the WPUx register if needed to avoid floating inputs.
  4. Write/Read: Use LATx to write to output pins and PORTx to read from input pins.

Code Example:

C
// 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.

C
// 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, you OR the 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, you AND the 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, you XOR the 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, you AND the 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:

  1. 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.
  2. 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.

4.2 Programming Example: Blinking an LED

This program blinks an LED connected to pin RD0 once per second.

C
#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.

  1. Configure the entire PORTD as output (TRISD = 0x00).
  2. Initialize an 8-bit counter variable (e.g., unsigned char count = 0;).
  3. Enter an infinite while loop.
  4. Inside the loop:
    a. Write the count variable'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

C
#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).
    • 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).
  • Internal Weak Pull-ups:

    • As discussed in Section 1.5, PIC MCUs contain internal weak pull-up resistors that can be enabled in software (WPUx registers).
    • This eliminates the need for external resistors, simplifying the circuit design. You only need to connect the switch from the pin to GND.

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:

  1. Detect an initial press.
  2. Wait for a short period (e.g., 20ms).
  3. 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.

C
#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
        }
    }
}