Unit 4 - Notes

ECE227 12 min read

Unit 4: PIC interfacing with input/output devices

1. Interfacing of 16x2 LCD in 8-bit mode

1.1 Introduction to 16x2 LCD

A 16x2 LCD (Liquid Crystal Display) is a common output device used in embedded systems to display alphanumeric characters.

  • 16x2: Means it can display 16 characters per line and has 2 such lines.
  • HD44780 Controller: Most character LCDs are based on the Hitachi HD44780 controller or a compatible one. This standardizes the commands and interfacing methods.
  • Modes of Operation: LCDs can be interfaced in 8-bit mode or 4-bit mode.
    • 8-bit Mode: All 8 data lines (D0-D7) are used to send data. It's faster as a full byte of data/command is sent in one cycle. It requires more I/O pins from the microcontroller (8 for data + 3 for control).
    • 4-bit Mode: Only 4 data lines (D4-D7) are used. A byte of data/command is sent in two halves (nibbles), making it slower but saving 4 I/O pins.

1.2 LCD Pinout and Functions

A standard 16x2 LCD module typically has 16 pins.

Pin No. Name Function Description
1 VSS Ground Connected to the system ground (0V).
2 VDD Power Supply Connected to the positive power supply (+5V).
3 VEE/V0 Contrast Adj. Controls the contrast of the display. Typically connected to the wiper of a 10k potentiometer.
4 RS Register Select RS=0: Command Register is selected (for sending instructions like clear display, set cursor position).
RS=1: Data Register is selected (for sending character data to be displayed).
5 R/W Read/Write R/W=0: Write to the LCD.
R/W=1: Read from the LCD (e.g., to check the busy flag). In most applications, this pin is tied to ground as we only write to the LCD.
6 E Enable A high-to-low pulse on this pin latches the data/command present on the data lines into the LCD's internal registers.
7-14 D0-D7 Data Bus 8-bit data lines. In 8-bit mode, all 8 are used.
15 A / LED+ Backlight Anode Anode (+) of the LED backlight. Connected to +5V, usually through a current-limiting resistor.
16 K / LED- Backlight Cathode Cathode (-) of the LED backlight. Connected to ground.

1.3 Interfacing Circuit (8-bit Mode)

  • PIC Microcontroller: e.g., PIC18F4550
  • LCD Control Pins: Connect RS, R/W, and E to three GPIO pins on the PIC (e.g., RD0, RD1, RD2).
  • LCD Data Pins: Connect D0-D7 to a full 8-bit port on the PIC (e.g., PORTB).
  • Power: Connect VSS to GND and VDD to +5V.
  • Contrast: Connect a 10k potentiometer between +5V and GND. Connect the wiper (middle pin) to the VEE pin of the LCD. This allows you to adjust the contrast for optimal viewing.

1.4 LCD Command and Data Operations

Sending a Command:

  1. Set RS = 0.
  2. Set R/W = 0 (usually hardwired to ground).
  3. Place the command byte on the data lines (D0-D7).
  4. Pulse the E pin (High -> Low).
  5. Wait for the command to execute (either by checking the busy flag or using a fixed delay).

Sending Data (a character):

  1. Set RS = 1.
  2. Set R/W = 0.
  3. Place the ASCII value of the character on the data lines (D0-D7).
  4. Pulse the E pin (High -> Low).
  5. Wait for the data to be processed.
Common LCD Commands (Hexadecimal): Command (Hex) Description
0x01 Clear display screen.
0x02 Return home (cursor to the beginning of the first line).
0x06 Entry mode set: Increment cursor, no display shift.
0x0C Display ON, Cursor OFF, Blink OFF.
0x0E Display ON, Cursor ON, Blink OFF.
0x0F Display ON, Cursor ON, Blink ON.
0x38 Function set: 8-bit mode, 2 lines, 5x8 dot font.
0x80 Force cursor to the beginning of the 1st line.
0xC0 Force cursor to the beginning of the 2nd line.

1.5 C Code Example (PIC18F4550, XC8 Compiler)

C
#include <xc.h>
#define _XTAL_FREQ 8000000 // System clock frequency for __delay_ms()

// Define LCD connections
#define LCD_DATA_PORT PORTB
#define LCD_DATA_TRIS TRISB
#define LCD_RS_PIN RD0
#define LCD_E_PIN  RD2
#define LCD_RS_TRIS TRISD0
#define LCD_E_TRIS  TRISD2

// Function Prototypes
void Lcd_Init(void);
void Lcd_Cmd(unsigned char cmd);
void Lcd_Data(unsigned char data);
void Lcd_WriteString(const char *str);
void Lcd_SetCursor(unsigned char row, unsigned char col);

// Helper function to send a pulse on the Enable pin
void Lcd_EnablePulse() {
    LCD_E_PIN = 1;
    __delay_us(1); // Small delay
    LCD_E_PIN = 0;
}

// Function to send a command to the LCD
void Lcd_Cmd(unsigned char cmd) {
    LCD_RS_PIN = 0;      // Select command register
    LCD_DATA_PORT = cmd; // Place command on data port
    Lcd_EnablePulse();
    __delay_ms(2);       // Delay for command execution
}

// Function to send data (a character) to the LCD
void Lcd_Data(unsigned char data) {
    LCD_RS_PIN = 1;      // Select data register
    LCD_DATA_PORT = data; // Place data on data port
    Lcd_EnablePulse();
    __delay_ms(1);
}

// Function to initialize the LCD in 8-bit mode
void Lcd_Init() {
    LCD_DATA_TRIS = 0x00; // Set PORTB as output for data
    LCD_RS_TRIS = 0;      // Set RS pin as output
    LCD_E_TRIS = 0;       // Set E pin as output
    
    __delay_ms(20);       // Power on delay
    Lcd_Cmd(0x38);        // Function Set: 8-bit mode, 2 lines, 5x8 dots
    Lcd_Cmd(0x0C);        // Display ON, Cursor OFF
    Lcd_Cmd(0x01);        // Clear Display
    Lcd_Cmd(0x06);        // Entry Mode Set: Increment cursor
}

// Function to write a string to the LCD
void Lcd_WriteString(const char *str) {
    while(*str != '\0') {
        Lcd_Data(*str);
        str++;
    }
}

// Function to set cursor position
void Lcd_SetCursor(unsigned char row, unsigned char col) {
    unsigned char address;
    if (row == 1) {
        address = 0x80 + col - 1;
    } else if (row == 2) {
        address = 0xC0 + col - 1;
    }
    Lcd_Cmd(address);
}

void main(void) {
    Lcd_Init(); // Initialize the LCD

    Lcd_SetCursor(1, 1); // Go to first line, first position
    Lcd_WriteString("PIC Interfacing");
    
    Lcd_SetCursor(2, 5); // Go to second line, fifth position
    Lcd_WriteString("with LCD");

    while(1) {
        // Loop forever
    }
}


2. Interfacing 7-Segment Display

2.1 Introduction to 7-Segment Displays

A 7-segment display is an array of 8 LEDs (7 segments arranged in a figure-8 and a decimal point) used to display numbers and some characters.

  • Segments: Labeled 'a' through 'g', with the decimal point labeled 'dp'.
  • Types:
    • Common Cathode (CC): All the cathodes (-) of the LEDs are connected to a common pin, which is connected to GND. Segments are lit by applying a HIGH logic level to the individual segment pins.
    • Common Anode (CA): All the anodes (+) of the LEDs are connected to a common pin, which is connected to VCC (+5V). Segments are lit by applying a LOW logic level to the individual segment pins.

2.2 Interfacing a Single 7-Segment Display

A single display is connected directly to a PIC's I/O port.

  • Circuit: Each segment pin (a-g, dp) is connected to a PIC I/O pin through a current-limiting resistor (typically 220Ω - 470Ω) to prevent damage to the LEDs and the PIC port. The common pin is connected to GND (for CC) or VCC (for CA).
  • Programming: A lookup table (an array) is used to store the 8-bit patterns corresponding to each digit (0-9). The program retrieves the pattern for the desired digit from the table and writes it to the output port.

Lookup Table Example (for a Common Cathode display):
Assume segments are connected as: PORTB = {dp, g, f, e, d, c, b, a}

Digit g f e d c b a Hex Value
0 0 1 1 1 1 1 1 0x3F
1 0 0 0 0 1 1 0 0x06
2 1 0 1 1 0 1 1 0x5B
3 1 0 0 1 1 1 1 0x4F
4 1 1 0 0 1 1 0 0x66
5 1 1 0 1 1 0 1 0x6D
6 1 1 1 1 1 0 1 0x7D
7 0 0 0 0 1 1 1 0x07
8 1 1 1 1 1 1 1 0x7F
9 1 1 0 1 1 1 1 0x6F

Note: For a Common Anode display, the patterns would be the bitwise inverse of the above (e.g., for '0', the pattern would be ~0x3F = 0xC0).

2.3 Interfacing Multiple 7-Segment Displays (Multiplexing)

To save I/O pins when using multiple displays (e.g., for a 4-digit counter), multiplexing is used.

  • Concept: The human eye has a property called Persistence of Vision (POV). If we switch between displays very quickly (faster than ~25 times per second), our eyes perceive them as being lit simultaneously.
  • Circuit:
    • The corresponding segment pins of all displays are connected together and driven by a single port (e.g., PORTB for segments a-g).
    • The common pins (cathode or anode) of each display are controlled individually by separate I/O pins (e.g., RD0, RD1, RD2, RD3).
    • For Common Anode displays, NPN transistors are often used to sink the current from the common pins. For Common Cathode, PNP transistors are used to source the current. This is because a PIC pin may not be able to source/sink enough current to light a full digit.
  • Programming Logic:
    1. Turn OFF all displays.
    2. Place the segment pattern for the first digit on the segment port.
    3. Turn ON (enable) only the first display.
    4. Wait for a short delay (e.g., 5 ms).
    5. Turn OFF all displays.
    6. Place the segment pattern for the second digit on the segment port.
    7. Turn ON (enable) only the second display.
    8. Wait for a short delay.
    9. Repeat for all digits.
    10. Loop this process continuously.

2.4 C Code Example (4-digit multiplexed counter, Common Cathode)

C
#include <xc.h>
#define _XTAL_FREQ 8000000

// Segment port
#define SEGMENT_PORT PORTB
#define SEGMENT_TRIS TRISB

// Digit control port
#define DIGIT_PORT PORTD
#define DIGIT_TRIS TRISD

// Lookup table for Common Cathode
const unsigned char segment_map[] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};

void main(void) {
    unsigned int counter = 0;
    unsigned char digit1, digit2, digit3, digit4;

    SEGMENT_TRIS = 0x00; // Segments are output
    DIGIT_TRIS = 0x00;   // Digit selectors are output
    
    // Timer0 setup for 1 second interrupt (for counting)
    // T0CON = ...; INTCON = ...; // Not shown for brevity
    
    while(1) {
        // Break the counter value into individual digits
        digit4 = counter / 1000;
        digit3 = (counter / 100) % 10;
        digit2 = (counter / 10) % 10;
        digit1 = counter % 10;

        // --- Multiplexing Routine ---
        // Display Digit 1 (units)
        DIGIT_PORT = 0x01; // Enable DIGIT 1
        SEGMENT_PORT = segment_map[digit1];
        __delay_ms(5);

        // Display Digit 2 (tens)
        DIGIT_PORT = 0x02; // Enable DIGIT 2
        SEGMENT_PORT = segment_map[digit2];
        __delay_ms(5);

        // Display Digit 3 (hundreds)
        DIGIT_PORT = 0x04; // Enable DIGIT 3
        SEGMENT_PORT = segment_map[digit3];
        __delay_ms(5);

        // Display Digit 4 (thousands)
        DIGIT_PORT = 0x08; // Enable DIGIT 4
        SEGMENT_PORT = segment_map[digit4];
        __delay_ms(5);
        
        // This loop runs continuously, creating the illusion of a solid display.
        // A real implementation would use a timer interrupt for the multiplexing
        // and a separate timer for updating the counter value.
        // For this example, let's just increment the counter slowly.
        counter++;
        if(counter > 9999) counter = 0;
        __delay_ms(100); // Slow down the counting for visibility
    }
}


3. ADC Programming

3.1 Introduction to ADC

The Analog-to-Digital Converter (ADC) is a peripheral inside the PIC microcontroller that converts an analog voltage input into a corresponding digital number. This allows the microcontroller to measure real-world analog signals like temperature, light level, or the position of a potentiometer.

Key Parameters:

  • Resolution: The number of bits in the output digital value. A 10-bit ADC (common in PICs) can represent an analog voltage with 2^10 = 1024 discrete steps (0 to 1023).
  • Reference Voltage (Vref): The maximum voltage the ADC can convert. The ADC's resolution is spread across the range from Vref- (usually GND) to Vref+ (usually VDD or an external reference).
  • Conversion Formula: Digital Value = (Analog Voltage / Vref) * (2^Resolution - 1)
    • For a 10-bit ADC with Vref = 5V: Digital Value = (Vin / 5.0) * 1023

3.2 ADC Registers in PIC18F4550

  • ADCON0 (ADC Control Register 0):

    • CHS3-CHS0: Analog Channel Select bits (selects which ANx pin to read).
    • GO/DONE: ADC Conversion Status bit. Set to 1 to start a conversion. It is cleared by hardware when the conversion is complete.
    • ADON: ADC Enable bit. Set to 1 to turn on the ADC module.
  • ADCON1 (ADC Control Register 1):

    • VCFG1, VCFG0: Voltage Reference Configuration bits (e.g., Vref- = VSS, Vref+ = VDD).
    • PCFG3-PCFG0: Port Configuration bits. Configures which ANx pins are analog inputs and which are digital I/O. This is a critical step.
  • ADCON2 (ADC Control Register 2):

    • ADFM: ADC Result Format Select bit.
      • 1 = Right justified (The 10-bit result is in the lower bits of ADRESH:ADRESL).
      • 0 = Left justified (The 10-bit result is in the upper bits of ADRESH:ADRESL).
    • ACQT2-ACQT0: Acquisition Time Select bits. Sets the time the ADC's sampling capacitor is allowed to charge.
    • ADCS2-ADCS0: ADC Clock Select bits. Sets the ADC conversion clock frequency (T_AD), which must be chosen carefully based on the device datasheet.
  • ADRESH:ADRESL (ADC Result Registers):

    • A 16-bit register pair that holds the 10-bit digital result of the conversion. How the result is stored depends on the ADFM bit.

3.3 Programming Steps for an ADC Conversion

  1. Configure I/O Pin: Set the corresponding TRIS bit for the analog pin to make it an input.
  2. Configure ADC Module:
    • ADCON1: Configure the pin as an analog input (PCFG bits) and set the voltage references (VCFG bits).
    • ADCON2: Select the result justification (ADFM), acquisition time (ACQT), and ADC clock (ADCS).
    • ADCON0: Select the analog channel (CHS bits).
  3. Enable ADC: Set the ADON bit in ADCON0.
  4. Wait Acquisition Time: Add a small delay (__delay_us()) to allow the sampling capacitor to charge.
  5. Start Conversion: Set the GO/DONE bit in ADCON0.
  6. Wait for Completion: Poll the GO/DONE bit in a loop. The conversion is finished when this bit becomes 0.
  7. Read Result: Read the digital value from the ADRESH and ADRESL registers. If right-justified, the 10-bit result can be calculated as (ADRESH << 8) | ADRESL.

3.4 C Code Example (Reading a Potentiometer on AN0)

C
#include <xc.h>
#define _XTAL_FREQ 8000000
// Assuming LCD functions from the previous section are available

void ADC_Init(void);
unsigned int ADC_Read(unsigned char channel);

void ADC_Init() {
    // 1. Configure I/O for AN0
    TRISA0 = 1; // Set RA0 pin as input
    
    // 2. Configure ADC module
    // ADCON1: Vref+=VDD, Vref-=VSS, AN0 is analog, others digital
    ADCON1 = 0x0E; 
    
    // ADCON2: Right justified, 2 TAD acquisition time, Fosc/32 clock
    ADCON2 = 0x8A;
    
    // 3. Enable ADC
    ADCON0bits.ADON = 1; 
}

unsigned int ADC_Read(unsigned char channel) {
    if(channel > 13) return 0; // Invalid channel
    
    // Select the channel
    ADCON0bits.CHS = channel; 
    
    // Wait for acquisition time
    __delay_us(30); 
    
    // Start the conversion
    ADCON0bits.GO_nDONE = 1; 
    
    // Wait for the conversion to complete
    while(ADCON0bits.GO_nDONE); 
    
    // Read the result (Right Justified)
    return ((ADRESH << 8) + ADRESL);
}

void main(void) {
    unsigned int adc_value;
    char buffer[5];

    Lcd_Init();  // Initialize LCD
    ADC_Init();  // Initialize ADC

    Lcd_WriteString("ADC Value:");
    
    while(1) {
        adc_value = ADC_Read(0); // Read from channel 0 (AN0)

        // Convert integer to string to display on LCD
        sprintf(buffer, "%04d", adc_value); 

        Lcd_SetCursor(2, 1);
        Lcd_WriteString(buffer);
        
        __delay_ms(200);
    }
}


4. DC Motor Interfacing

4.1 Why a Driver is Needed

A DC motor cannot be connected directly to a microcontroller's I/O pin for two main reasons:

  1. High Current Draw: Motors draw significant current (hundreds of mA to several Amps), far exceeding the maximum current a PIC pin can source or sink (typically ~25mA).
  2. Back EMF (Electro-Motive Force): A spinning motor acts like a generator, producing a "kickback" voltage spike when power is removed. This high-voltage spike can permanently damage the microcontroller.

A motor driver circuit acts as an interface that can handle the high current and protect the microcontroller.

4.2 Unidirectional Control (ON/OFF) using a Transistor

For simple ON/OFF control in one direction, a BJT (like a TIP120 Darlington) or a MOSFET can be used as a switch.

  • Circuit:

    • The PIC's I/O pin is connected to the base of the BJT through a base resistor (e.g., 1kΩ) to limit the current.
    • The motor is connected between VCC and the transistor's collector. The emitter is connected to GND.
    • A flyback diode (also called freewheeling or snubber diode, e.g., 1N4007) is connected in parallel with the motor, with its cathode facing the positive supply. This diode provides a safe path for the back EMF current to circulate when the transistor is turned off, protecting the transistor.
  • Operation:

    • PIC pin HIGH: The transistor turns ON, completing the circuit and allowing current to flow through the motor. The motor runs.
    • PIC pin LOW: The transistor turns OFF, cutting off the current. The motor stops.

4.3 Bidirectional Control using an H-Bridge

To control both the direction and speed of a motor, an H-Bridge is required. The L293D is a popular dual H-Bridge IC that can drive two DC motors.

  • L293D IC:

    • It contains four half-H-bridge drivers.
    • Requires two separate power supplies: Vcc1 (+5V) for the internal logic and Vcc2 (4.5V to 36V) for the motors.
    • Has input pins (e.g., IN1, IN2) to control the direction and an Enable pin (EN1) to turn the motor ON/OFF or control its speed via PWM.
  • Controlling Direction (L293D Truth Table):

EN1 IN1 IN2 Motor State
1 0 0 Motor Stop (Brake)
1 0 1 Reverse
1 1 0 Forward
1 1 1 Motor Stop (Brake)
0 X X Coast (Freewheel)
  • Circuit:
    • Connect IN1 and IN2 to two PIC I/O pins.
    • Connect OUT1 and OUT2 to the motor terminals.
    • Connect the Enable pin (EN1) to another PIC pin.
    • Provide appropriate power to Vcc1, Vcc2, and GND pins.

4.4 Speed Control using PWM

Pulse Width Modulation (PWM) is a technique for controlling the average power delivered to a device by rapidly switching the power ON and OFF.

  • Duty Cycle: The percentage of time the signal is HIGH within one period.
  • Speed Control: By varying the duty cycle of the signal applied to the H-Bridge's Enable pin, we control the average voltage supplied to the motor, thus controlling its speed.
    • 0% duty cycle: Motor is OFF.
    • 50% duty cycle: Motor runs at half speed.
    • 100% duty cycle: Motor runs at full speed.

PIC microcontrollers have a built-in CCP (Capture/Compare/PWM) module to generate PWM signals with minimal CPU overhead.

Steps to configure PWM (e.g., on CCP1/RC2 pin):

  1. Set PWM Period: Configure Timer2. The PWM period is determined by the PR2 register and the Timer2 prescaler. PWM Period = [(PR2) + 1] * 4 * TOSC * (TMR2 Prescale Value)
  2. Set PWM Duty Cycle: The duty cycle is set by writing a 10-bit value to the CCPR1L register and the DC1B bits in the CCP1CON register.
  3. Configure CCP Module: Set the CCP1CON register to PWM mode.
  4. Configure I/O Pin: Make the CCP1 pin (RC2) an output by clearing the TRISC2 bit.
  5. Start PWM: Start Timer2 by setting the TMR2ON bit in T2CON.

4.5 C Code Example (L293D Speed and Direction Control)

C
#include <xc.h>
#define _XTAL_FREQ 8000000

// L293D Connections
#define IN1 RD0
#define IN2 RD1
// PWM is on CCP1 (RC2) which is connected to L293D Enable pin

void PWM_Init(void) {
    // 1. Set PWM Period (using Timer2)
    PR2 = 249; // Results in a PWM frequency of ~1KHz with 8MHz Fosc and 1:4 prescaler
    T2CON = 0b00000101; // Timer2 ON, Prescaler 1:4

    // 2. & 3. Configure CCP1 for PWM mode
    CCP1CON = 0b00001100; // PWM mode

    // 4. Configure RC2 as output
    TRISC2 = 0; 
}

void PWM_Set_Duty(unsigned int duty_cycle) {
    // Duty cycle is a 10-bit value (0-1023)
    // For PR2=249, max duty cycle value is (249+1)*4 = 1000
    if(duty_cycle > 1000) duty_cycle = 1000;
    
    // Set the 8 MSBs
    CCPR1L = duty_cycle >> 2; 
    // Set the 2 LSBs
    CCP1CON = (CCP1CON & 0b11001111) | ((duty_cycle & 0b00000011) << 4);
}

void main(void) {
    TRISD0 = 0; // IN1 as output
    TRISD1 = 0; // IN2 as output
    
    PWM_Init();

    while(1) {
        // --- Example 1: Forward, Half Speed ---
        IN1 = 1;
        IN2 = 0;
        PWM_Set_Duty(500); // ~50% duty cycle
        __delay_ms(3000);

        // --- Example 2: Reverse, Full Speed ---
        IN1 = 0;
        IN2 = 1;
        PWM_Set_Duty(1000); // 100% duty cycle
        __delay_ms(3000);

        // --- Example 3: Forward, Quarter Speed ---
        IN1 = 1;
        IN2 = 0;
        PWM_Set_Duty(250); // ~25% duty cycle
        __delay_ms(3000);

        // --- Example 4: Stop (Brake) ---
        IN1 = 0;
        IN2 = 0;
        PWM_Set_Duty(0); // 0% duty cycle
        __delay_ms(3000);
    }
}