Skip to content

Improve I2C LCD example #675

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion i2c/lcd_1602_i2c/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
add_executable(lcd_1602_i2c
lcd_1602_i2c.c
main.c
i2c_lcd.c
)

# pull in common dependencies and additional i2c hardware support
Expand Down
255 changes: 255 additions & 0 deletions i2c/lcd_1602_i2c/i2c_lcd.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
/**
* Copyright (c) 2025 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/

#include "i2c_lcd.h"

#include <stdio.h>
#include <string.h>
#include "hardware/i2c.h"
#include <time.h>
#include <stdlib.h>
#include <stdarg.h>

#define INSTRUCTION_FUNCTION_SET (1 << 5)
#define FUNCTION_SET_DATALENGTH_8BIT (1 << 4) // 4-bit if not set
#define FUNCTION_SET_USE_2_LINES (1 << 3) // 1 line if not set
#define FUNCTION_SET_FONT_5x10 (1 << 2) // 5x7 if not set

#define INSTRUCTION_DISPLAY_CTRL (1 << 3)
#define DISPLAY_CTRL_DISPLAY_ON (1 << 2)
#define DISPLAY_CTRL_CURSOR_ON (1 << 1)
#define DISPLAY_CTRL_CURSOR_BLINK (1 << 0)

#define INSTRUCTION_CLEAR_DISPLAY (1 << 0)

#define INSTRUCTION_ENTRY_MODE_SET (1 << 2)
#define ENTRY_MODE_INCREMENT (1 << 1) // decrement if not set
#define ENTRY_MODE_SHIFT (1 << 0) // don't shift if not set

#define INSTRUCTION_GO_HOME (1 << 1)

#define INSTRUCTION_SET_RAM_ADDR (1 << 7)

#define N_ROWS 2
#define N_COLS 16
#define LINEBUF_LEN (N_ROWS * N_COLS + 1)

static const uint8_t ROW_OFFSETS[] = { 0x00, 0x40, 0x14, 0x54 }; // in display memory

typedef union
{
uint8_t dat;
struct
{
bool RS : 1; // bit 0
bool RW : 1; // bit 1
bool EN : 1; // bit 2
bool BACKLIGHT : 1; // bit 3
bool D4 : 1; // bit 4
bool D5 : 1; // bit 5
bool D6 : 1; // bit 6
bool D7 : 1; // bit 7
};
} PortExpanderData_t;

struct i2c_lcd
{
PortExpanderData_t portExpanderDat;
bool cursor;
bool cursorBlink;
bool displayEnabled;
i2c_inst_t* i2cHandle;
uint8_t address;
};

static void writeExpanderData(i2c_lcd_handle inst, bool clockDisplayController)
{
if (clockDisplayController)
{
// Assert enable signal
inst->portExpanderDat.EN = 1;

// Send to hardware
i2c_write_blocking(inst->i2cHandle, inst->address, &inst->portExpanderDat.dat, 1, false);
sleep_us(1);

// De-assert enable
inst->portExpanderDat.EN = 0;

// Send to hardware
i2c_write_blocking(inst->i2cHandle, inst->address, &inst->portExpanderDat.dat, 1, false);
sleep_us(50);
}
else
{
i2c_write_blocking(inst->i2cHandle, inst->address, &inst->portExpanderDat.dat, 1, false);
}
}

static void write4bits(i2c_lcd_handle inst, uint8_t bits)
{
// Load in the bits
inst->portExpanderDat.D4 = (bits & (1 << 0)) != 0;
inst->portExpanderDat.D5 = (bits & (1 << 1)) != 0;
inst->portExpanderDat.D6 = (bits & (1 << 2)) != 0;
inst->portExpanderDat.D7 = (bits & (1 << 3)) != 0;

writeExpanderData(inst, true);
}

static void writeByte(i2c_lcd_handle inst, uint8_t data, bool dstControlReg)
{
inst->portExpanderDat.RS = !dstControlReg;

// Write more significant nibble first
write4bits(inst, data >> 4);

// Then write less significant nibble
write4bits(inst, data);
}

static void executeInstruction(i2c_lcd_handle inst, uint8_t instruction, uint8_t flags)
{
writeByte(inst, instruction | flags, true);

switch (instruction)
{
case INSTRUCTION_FUNCTION_SET:
case INSTRUCTION_DISPLAY_CTRL:
case INSTRUCTION_ENTRY_MODE_SET:
case INSTRUCTION_SET_RAM_ADDR:
sleep_us(53);
break;

case INSTRUCTION_CLEAR_DISPLAY:
sleep_us(3000);
break;

case INSTRUCTION_GO_HOME:
sleep_us(2000);
break;
}
}

// https://web.alfredstate.edu/faculty/weimandn/lcd/lcd_initialization/lcd_initialization_index.html
i2c_lcd_handle i2c_lcd_init(i2c_inst_t* handle, uint8_t address)
{
i2c_lcd_handle inst = calloc(sizeof(struct i2c_lcd), 1);
inst->i2cHandle = handle;
inst->address = address;

// Special case of function set
write4bits(inst, 0b0011);
sleep_us(4100);
write4bits(inst, 0b0011);
sleep_us(100);
write4bits(inst, 0b0011);
sleep_us(100);

// Function set interface to 4 bit mode
write4bits(inst, 0b0010);
sleep_us(100);

executeInstruction(inst, INSTRUCTION_FUNCTION_SET, FUNCTION_SET_USE_2_LINES);
executeInstruction(inst, INSTRUCTION_ENTRY_MODE_SET, ENTRY_MODE_INCREMENT);

i2c_lcd_clear(inst);
i2c_lcd_setDisplayVisible(inst, true);
i2c_lcd_setCursorLocation(inst, 0,0);

return inst;
}

void i2c_lcd_writeChar(i2c_lcd_handle inst, char c)
{
writeByte(inst, c, false);
}

void i2c_lcd_writeString(i2c_lcd_handle inst, char* string)
{
for (int i = 0; i < strlen(string); i++)
{
i2c_lcd_writeChar(inst, string[i]);
}
}

void i2c_lcd_writeStringf(i2c_lcd_handle inst, const char* __restrict format, ...)
{
va_list args;
va_start(args, format);

char linebuf[LINEBUF_LEN];
vsnprintf(linebuf, LINEBUF_LEN, format, args);
i2c_lcd_writeString(inst, linebuf);

va_end(args);
}

void i2c_lcd_setCursorLocation(i2c_lcd_handle inst, uint8_t x, uint8_t y)
{
// Bounds check
if (x < N_COLS && y <= N_ROWS)
{
executeInstruction(inst, INSTRUCTION_SET_RAM_ADDR, x + ROW_OFFSETS[y]);
}
}

void i2c_lcd_setCursorLine(i2c_lcd_handle inst, uint8_t line)
{
// Bounds check
if (line <= N_ROWS)
{
executeInstruction(inst, INSTRUCTION_SET_RAM_ADDR, ROW_OFFSETS[line]);
}
}

void i2c_lcd_writeLines(i2c_lcd_handle inst, char* line1, char* line2)
{
char linebuf1[LINEBUF_LEN];
char linebuf2[LINEBUF_LEN];

snprintf(linebuf1, LINEBUF_LEN, "%-16s", line1);
snprintf(linebuf2, LINEBUF_LEN, "%-16s", line2);

i2c_lcd_setCursorLocation(inst, 0,0);
i2c_lcd_writeString(inst, linebuf1);
i2c_lcd_setCursorLocation(inst, 0,1);
i2c_lcd_writeString(inst, linebuf2);
}

void i2c_lcd_clear(i2c_lcd_handle inst)
{
executeInstruction(inst, INSTRUCTION_CLEAR_DISPLAY, 0);
}

void i2c_lcd_setBacklightEnabled(i2c_lcd_handle inst, bool en)
{
inst->portExpanderDat.BACKLIGHT = en;
writeExpanderData(inst, false);
}

static void updateDisplayConfiguration(i2c_lcd_handle inst)
{
uint8_t flags = 0;
if (inst->cursor) flags |= DISPLAY_CTRL_CURSOR_ON;
if (inst->cursorBlink) flags |= DISPLAY_CTRL_CURSOR_BLINK;
if (inst->displayEnabled) flags |= DISPLAY_CTRL_DISPLAY_ON;

executeInstruction(inst, INSTRUCTION_DISPLAY_CTRL, flags);
}

void i2c_lcd_setDisplayVisible(i2c_lcd_handle inst, bool en)
{
inst->displayEnabled = en;
updateDisplayConfiguration(inst);
}

void i2c_lcd_setCursorEnabled(i2c_lcd_handle inst, bool cusror, bool blink)
{
inst->cursor = cusror;
inst->cursorBlink = blink;
updateDisplayConfiguration(inst);
}
26 changes: 26 additions & 0 deletions i2c/lcd_1602_i2c/i2c_lcd.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2025 Raspberry Pi (Trading) Ltd.
*
* SPDX-License-Identifier: BSD-3-Clause
*/

#ifndef I2C_LCD_H
#define I2C_LCD_H
#include <stdbool.h>
#include <hardware/i2c.h>

typedef struct i2c_lcd* i2c_lcd_handle;

i2c_lcd_handle i2c_lcd_init(i2c_inst_t* handle, uint8_t address);
void i2c_lcd_setCursorLocation(i2c_lcd_handle inst, uint8_t x, uint8_t y);
void i2c_lcd_setCursorLine(i2c_lcd_handle inst, uint8_t line);
void i2c_lcd_writeString(i2c_lcd_handle inst, char* string);
void i2c_lcd_writeStringf(i2c_lcd_handle inst, const char* __restrict format, ...) _ATTRIBUTE ((__format__ (__printf__, 2, 3)));
void i2c_lcd_writeChar(i2c_lcd_handle inst, char c);
void i2c_lcd_writeLines(i2c_lcd_handle inst, char* line1, char* line2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function-signature seems to be assuming that you're using a 16x2 LCD, and precludes the usage of a 20x4 LCD?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct. The example name specifically says it's for 16x2. I also don't have 20x4 hardware to validate against.

void i2c_lcd_clear(i2c_lcd_handle inst);
void i2c_lcd_setBacklightEnabled(i2c_lcd_handle inst, bool enabled);
void i2c_lcd_setDisplayVisible(i2c_lcd_handle inst, bool en);
void i2c_lcd_setCursorEnabled(i2c_lcd_handle inst, bool cusror, bool blink);

#endif //I2C_LCD_H
Loading