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 all commits
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 write_expander_data(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 write_4_bits(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;

write_expander_data(inst, true);
}

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

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

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

static void send_instruction(i2c_lcd_handle inst, uint8_t instruction, uint8_t flags)
{
write_byte(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* i2c_peripheral, uint8_t address)
{
i2c_lcd_handle inst = calloc(sizeof(struct i2c_lcd), 1);
inst->i2cHandle = i2c_peripheral;
inst->address = address;

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

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

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

i2c_lcd_clear(inst);
i2c_lcd_set_display_visible(inst, true);
i2c_lcd_set_cursor_location(inst, 0,0);

return inst;
}

void i2c_lcd_write_char(i2c_lcd_handle inst, char c)
{
write_byte(inst, c, false);
}

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

void i2c_lcd_write_stringf(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_write_string(inst, linebuf);

va_end(args);
}

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

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

void i2c_lcd_write_lines(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_set_cursor_location(inst, 0,0);
i2c_lcd_write_string(inst, linebuf1);
i2c_lcd_set_cursor_location(inst, 0,1);
i2c_lcd_write_string(inst, linebuf2);
}

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

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

static void update_display_configuration(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;

send_instruction(inst, INSTRUCTION_DISPLAY_CTRL, flags);
}

void i2c_lcd_set_display_visible(i2c_lcd_handle inst, bool en)
{
inst->displayEnabled = en;
update_display_configuration(inst);
}

void i2c_lcd_set_cursor_enabled(i2c_lcd_handle inst, bool cusror, bool blink)
{
inst->cursor = cusror;
inst->cursorBlink = blink;
update_display_configuration(inst);
}
114 changes: 114 additions & 0 deletions i2c/lcd_1602_i2c/i2c_lcd.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* 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>

/*
* This is a library-like example - it demonstrates creating a driver that can be
* instantiated multiple times to control more than one device, as well as hiding
* internal driver state from user code, and showing how to document function
* arguments and return values.
*/

// "Opaque" type; implementation defined "privately" inside the .c file
// so that user code cannot mess with the internal state of the driver
typedef struct i2c_lcd* i2c_lcd_handle;

/*! \brief Create an instance of the LCD driver
*
* Initializes the LCD driver and performs initial configuration of the display
*
* \param i2c_peripheral a reference to the I2C hardware bus that the LCD is connected to
* \param address the I2C address of the LCD module
* \return Pointer to newly created driver instance
*/
i2c_lcd_handle i2c_lcd_init(i2c_inst_t* i2c_peripheral, uint8_t address);

/*! \brief Move the cursor to a certain location on the LCD
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param x the column (0-indexed) to move the cursor to
* \param y the row (0-indexed) to move the cursor to
*/
void i2c_lcd_set_cursor_location(i2c_lcd_handle inst, uint8_t x, uint8_t y);

/*! \brief Move the cursor to the beginning of a certain line on the LCD
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param line the line number (0-indexed) to the move the cursor to the beginning of
*/
void i2c_lcd_set_cursor_line(i2c_lcd_handle inst, uint8_t line);

/*! \brief Write a string into the display memory starting from the current cursor location
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param string the string to send to the display
*/
void i2c_lcd_write_string(i2c_lcd_handle inst, char* string);

/*! \brief Write a formatted string into the display memory starting from the current cursor location
*
* This is a convenience function to allow "printf" functionality to the display without
* having to do snprintf on a buffer yourself
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param format the format string used to generate the final string send to the display
* \param ... the arguments for string formatting
*/
void i2c_lcd_write_stringf(i2c_lcd_handle inst, const char* __restrict format, ...) _ATTRIBUTE ((__format__ (__printf__, 2, 3)));

/*! \brief Write a single character into the display memory starting from the current cursor location
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param c the character to write into display memory
*/
void i2c_lcd_write_char(i2c_lcd_handle inst, char c);

/*! \brief Write a string to each line of the display
*
* Convenience function to avoid having to move the cursor around when writing to
* both lines of the display.
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param line1 the string to be shown on the first line of the display
* \param line2 the string to be shown on the second line of the display
*/
void i2c_lcd_write_lines(i2c_lcd_handle inst, char* line1, char* line2);

/*! \brief Clear the contents of the display memory
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
*/
void i2c_lcd_clear(i2c_lcd_handle inst);

/*! \brief Enable or disable the LCD backlight
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param enabled whether to enable the backlight
*/
void i2c_lcd_set_backlight_enabled(i2c_lcd_handle inst, bool enabled);

/*! \brief Control display visibility
*
* This allows "blanking" the display without actually clearing the memory contents
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param enabled whether the display should be visible
*/
void i2c_lcd_set_display_visible(i2c_lcd_handle inst, bool enabled);

/*! \brief Control display cursor visibility
*
* \param inst pointer to an instance of this driver, obtained from \link i2c_lcd_init
* \param cursorVisible whether the cursor should be visible
* \param blink whether the cursor should also blink
*/
void i2c_lcd_set_cursor_enabled(i2c_lcd_handle inst, bool cursorVisible, bool blink);

#endif //I2C_LCD_H
Loading