Essential PIC Guide: PIC12, PIC16 & PIC18 Deep Dive
Interfacing HD44780 LCD and Matrix Keypad with a PIC
In this tutorial, you will learn how to interface a standard alphanumeric LCD module and a matrix keypad to a PIC microcontrollerIntroduction to PIC: Exploring the Basics of Microcontroller ArchitectureExplore the core principles of PIC microcontroller architecture, including Harvard design, RISC processing, and efficient memory organization.. These two peripherals are often paired together in embedded applications, allowing for user input through the keypad and data display on the LCD. The concepts and techniques outlined here will help you create interactive interfaces for your PIC-based projects.
Introduction🔗
Liquid Crystal Displays (LCDs) provide a user-friendly way to present messages, sensor readings, and status information in real timeImplementing Interrupt-Driven Systems for Real-Time ApplicationsLearn to configure and optimize PIC microcontroller interrupts for real-time performance. Enhance responsiveness and efficiency using best practices.. A common choice is the HD44780-compatible 16×2 or 20×4 character LCD, featuring a built-in controller that simplifies communication with the microcontroller.
Matrix keypads are popular for applications requiring multiple input keys (e.g., simple passcode systems, menu navigation, or data entry). They are arranged in rows and columns, reducing the number of I/O pins needed to detect a key press.
Interfacing an HD44780 LCD with PIC🔗
Typical LCD Pin Connections
Below is a typical pin mapping for a 16×2 alphanumeric LCD (HD44780-based). You can interface it with the PIC via 4-bit or 8-bit data mode. Here, we use a 4-bit mode to save I/O pins.
LCD Pin | Function | Typical PIC Connection |
---|---|---|
1 (Vss) | GND | GND |
2 (Vdd) | +5V | +5V |
3 (Vo) | Contrast | Potentiometer output |
4 (RS) | Register Select | RC0 (example) |
5 (RW) | Read/Write | GND (for write-only) |
6 (E) | Enable | RC1 (example) |
11 (D4) | Data line 4 | RB0 (example) |
12 (D5) | Data line 5 | RB1 (example) |
13 (D6) | Data line 6 | RB2 (example) |
14 (D7) | Data line 7 | RB3 (example) |
15 (LED+) | LED Anode | +5V (through resistor) |
16 (LED-) | LED Cathode | GND |
- RS (Register Select): Determines whether incoming data is interpreted as command or character data.
- RW (Read/Write): If set to low, data is written to the LCD; if high, data is read from the LCD. Typically tied to GND for write-only operations.
- E (Enable): Triggers read/write operations on the falling edge.
LCD Initialization Steps (4-Bit Mode)
1. Power on the LCD module, ensure a stable power supply (+5 V) and proper contrast setting.
2. Function set: Switch the LCD into 4-bit interface mode and specify the display lines and character font.
3. Display control: Turn on the display, cursor, or cursor blinking, as needed.
4. Clear display: Erase any residual data and move cursor to the home position.
5. Entry mode set: Configure how the cursor moves after each character is written.
Below is an example sequence in code using C (XC8Getting Started with MPLAB X and the XC8 CompilerSet up MPLAB X IDE and XC8 compiler for PIC programming with our comprehensive guide detailing installation, configuration, and debugging techniques.):
#include <xc.h>
#define _XTAL_FREQ 8000000
// Define control pins
#define LCD_RS LATCbits.LATC0
#define LCD_E LATCbits.LATC1
// Define data pins (4-bit)
#define LCD_D4 LATBbits.LATB0
#define LCD_D5 LATBbits.LATB1
#define LCD_D6 LATBbits.LATB2
#define LCD_D7 LATBbits.LATB3
// Helper function to send four bits to the LCD
void lcd_send_nibble(unsigned char nibble) {
// Write nibble to data lines
LCD_D4 = (nibble >> 0) & 0x01;
LCD_D5 = (nibble >> 1) & 0x01;
LCD_D6 = (nibble >> 2) & 0x01;
LCD_D7 = (nibble >> 3) & 0x01;
// Pulse enable
LCD_E = 1;
__delay_us(1);
LCD_E = 0;
__delay_us(100);
}
// Send a byte (command or data)
void lcd_send_byte(unsigned char b, char isData) {
LCD_RS = isData; // RS=0 for command, RS=1 for data
lcd_send_nibble(b >> 4); // High nibble first
lcd_send_nibble(b & 0x0F);// Low nibble next
}
// Initialize the LCD in 4-bit mode
void lcd_init(void) {
// Wait for LCD power up
__delay_ms(30);
// Set initial function (in 8-bit mode first then switch)
lcd_send_nibble(0x03);
__delay_ms(5);
lcd_send_nibble(0x03);
__delay_us(100);
lcd_send_nibble(0x03);
// Switch to 4-bit mode
lcd_send_nibble(0x02);
// Now use lcd_send_byte for full commands
// 4-bit, 2-line, 5x8 font
lcd_send_byte(0x28, 0);
// Display on, cursor off, blink off
lcd_send_byte(0x0C, 0);
// Clear display
lcd_send_byte(0x01, 0);
__delay_ms(2);
// Entry mode: increment, no shift
lcd_send_byte(0x06, 0);
}
// Send a string to the LCD
void lcd_puts(const char *s) {
while(*s) {
lcd_send_byte(*s++, 1);
}
}
void main(void) {
// Configure I/O directions
TRISB = 0x00; // All output for data lines
TRISCbits.TRISC0 = 0; // RS
TRISCbits.TRISC1 = 0; // E
// Initialize LCD
lcd_init();
// Display a message
lcd_puts("Hello, PIC!");
while(1) {
// Main loop
}
}
In the above code, the function lcd_init()
follows the initialization steps recommended by the LCD datasheet. Always consult the datasheet for timing requirements and command values.
Interfacing a Matrix Keypad🔗
Matrix keypads (often 4×3 or 4×4) are made up of rows and columns where each intersection corresponds to a possible key.
Wiring the Keypad
For a 4×4 keypad, you typically have 8 pins: 4 row lines (input or output) and 4 column lines (input or output). A common arrangement is to drive the rows and read the columns, or vice versa.
Keypad Pin | Function | PIC Connection (Example) |
---|---|---|
R1 | Row 1 | RA0 |
R2 | Row 2 | RA1 |
R3 | Row 3 | RA2 |
R4 | Row 4 | RA3 |
C1 | Column 1 | RA4 |
C2 | Column 2 | RA5 |
C3 | Column 3 | RB0 |
C4 | Column 4 | RB1 |
- (Pin assignment may vary; choose whichever I/O pins suit your project.)
Scanning Algorithm
To detect key presses, you can set one row to a known logic level (e.g., low) and read each column. If a column pin is pulled low, it means the key at that row-column intersection is pressed. Then you move to the next row, repeating until all rows are checked.
Below is a simplified flowchart describing one method of scanning (rows as outputs, columns as inputs):
Example Keypad Code
#include <xc.h>
#define _XTAL_FREQ 8000000
// Assume 4 rows on RA0-RA3, 4 columns on RA4, RA5, RB0, RB1
// (This is just an example; you can map differently.)
// A function to scan the keypad and return the key pressed
// Return '\0' if no key is pressed
char keypad_get_key(void) {
// Map keypad characters
static const char keypad_map[4][4] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// For each row
for (int row = 0; row < 4; row++) {
// Set one row to 0, others to 1
// Example: if row == 0 -> RA0=0, RA1=1, RA2=1, RA3=1
// Drive the row low
switch(row) {
case 0: LATAbits.LATA0 = 0; LATAbits.LATA1 = 1; LATAbits.LATA2 = 1; LATAbits.LATA3 = 1; break;
case 1: LATAbits.LATA0 = 1; LATAbits.LATA1 = 0; LATAbits.LATA2 = 1; LATAbits.LATA3 = 1; break;
case 2: LATAbits.LATA0 = 1; LATAbits.LATA1 = 1; LATAbits.LATA2 = 0; LATAbits.LATA3 = 1; break;
case 3: LATAbits.LATA0 = 1; LATAbits.LATA1 = 1; LATAbits.LATA2 = 1; LATAbits.LATA3 = 0; break;
}
__delay_ms(1); // Allow settling time
// Now read each column to see which one is low
// Example: RA4, RA5, RB0, RB1 as the column inputs
if(!PORTAbits.RA4) return keypad_map[row][0];
if(!PORTAbits.RA5) return keypad_map[row][1];
if(!PORTBbits.RB0) return keypad_map[row][2];
if(!PORTBbits.RB1) return keypad_map[row][3];
}
// No key pressed
return '\0';
}
void main(void) {
// Configure rows as output
TRISAbits.TRISA0 = 0;
TRISAbits.TRISA1 = 0;
TRISAbits.TRISA2 = 0;
TRISAbits.TRISA3 = 0;
// Configure columns as input
TRISAbits.TRISA4 = 1;
TRISAbits.TRISA5 = 1;
TRISBbits.TRISB0 = 1;
TRISBbits.TRISB1 = 1;
// Initialize your LCD (from previous sections)
// lcd_init();
char key;
while(1) {
key = keypad_get_key();
if(key != '\0') {
// Display the pressed key on the LCD
// lcd_send_byte(key, 1); // as character data
}
__delay_ms(100); // Polling delay
}
}
Note: This simple approach polls the keypad regularly. For more responsive designs, you might use an interruptImplementing Interrupt-Driven Systems for Real-Time ApplicationsLearn to configure and optimize PIC microcontroller interrupts for real-time performance. Enhance responsiveness and efficiency using best practices. or shorter polling intervals. You can also debounce the keys in software to avoid detecting spurious presses.
Bringing It All Together🔗
Combining the LCD and keypad in a single project allows for interactive applications. For example, you could prompt the user for a numeric input on the LCD, read the keypad entry, and then display the entered data or perform an action based on the key pressed.
Key steps in combining them:
1. Initialize the PIC’s I/O ports for both the LCD (4-bit interface) and keypad (rows and columns).
2. Perform the LCD initialization routine.
3. In the main loop, continuously scan the keypad for user input.
4. Display relevant messages and data on the LCD in response to key presses.
By carefully managing your I/O pins and software routines, you can build a robust user interface on limited hardware resources.
Tips and Best Practices🔗
- Use Pull-ups
Mastering Digital I/O on PIC MCUs with Practical ExamplesLearn hands-on techniques for configuring and using digital I/O pins on PIC microcontrollers to control LEDs, sensors, and more in practical projects. on Columns: Some PIC MCUs
Mastering Digital I/O on PIC MCUs with Practical ExamplesLearn hands-on techniques for configuring and using digital I/O pins on PIC microcontrollers to control LEDs, sensors, and more in practical projects. have internal pull-up resistors that can be enabled on inputs to keep them from floating when no key is pressed.
- Debounce Mechanisms: Mechanical switches can cause transient signals. Implement either a short delay after detecting a press or a more advanced debounce algorithm for reliability.
- LCD Libraries: Many C compilers come with standard libraries or example code for HD44780-type LCDs. These can speed up development and reduce coding errors.
- Pin Resource Conflicts: Ensure your chosen row and column pins do not conflict with other peripheral functions (e.g., communication pins) in your project.
Conclusion🔗
Driving an LCD display and interfacing a keypad with a PIC microcontrollerIntroduction to PIC: Exploring the Basics of Microcontroller ArchitectureExplore the core principles of PIC microcontroller architecture, including Harvard design, RISC processing, and efficient memory organization. is a classic demonstration of embedded systems at work. By mastering these techniques, you gain the ability to create intuitive user interfaces, displaying real-time information on the LCD while accepting user input from the keypad.
From a small menu-based setting to more advanced control panels, the combination of LCD + keypad lays a strong foundation for interactive embedded projects. With practice, you can easily adapt the code and hardware connections presented here to fit your unique application requirements.
Author: Marcelo V. Souza - Engenheiro de Sistemas e Entusiasta em IoT e Desenvolvimento de Software, com foco em inovação tecnológica.
References🔗
- Microchip Developer Help for detailed documentation and support on PIC microcontroller programming and interfacing: microchipdeveloper.com/
- Microchip's official website for comprehensive information on PIC microcontrollers and development resources: www.microchip.com