lcd_i2c.tc¶
lcd_i2c.tc — I2C LCD Character Display Driver (HD44780 + PCF8574)
// ═══════════════════════════════════════════════════════════════════
// lcd_i2c.tc — I2C LCD Character Display Driver (HD44780 + PCF8574)
// For ESP8266/ESP32 with standard I2C LCD backpack (address 0x27 or 0x3F)
// Supports 16x2 and 20x2 displays (set LCD_COLS below)
//
// Features: Auto-detect I2C address, backlight control, custom text,
// clock display, heap/WiFi status, console commands
//
// Line 1: Time + date | Line 2: Heap + WiFi status or custom text
// Web UI: editable text for both lines, backlight toggle
//
// Console commands:
// LCDPrint1 Hello — print "Hello" on line 1 (switches to custom mode)
// LCDPrint2 World — print "World" on line 2
// LCDClear — clear display
// LCDOn / LCDOff — backlight control
// LCDAuto — switch back to auto mode (time/heap)
// ═══════════════════════════════════════════════════════════════════
// ─── PCF8574 I2C backpack pin mapping ───
// P0=RS P1=RW P2=EN P3=Backlight P4-P7=D4-D7
#define LCD_RS 0x01
#define LCD_RW 0x02
#define LCD_EN 0x04
#define LCD_BL 0x08
// ─── HD44780 commands ───
#define LCD_CLEAR 0x01
#define LCD_HOME 0x02
#define LCD_ENTRY_MODE 0x06
#define LCD_DISPLAY_ON 0x0C
#define LCD_DISPLAY_OFF 0x08
#define LCD_FUNC_4BIT2L 0x28
#define LCD_LINE1 0x80
#define LCD_LINE2 0xC0
// Set to 16 for 16x2 display, 20 for 20x2 or 20x4 display
#define LCD_COLS 16
// ─── State ───
int lcd_addr; // detected I2C address
int lcd_bus; // detected I2C bus (0 or 1)
int lcd_ok; // 1 = LCD initialized
int lcd_bl; // backlight state (LCD_BL or 0)
// ─── Display mode ───
// 0 = auto (time/heap), 1 = custom text
persist watch int dmode;
persist watch int backlight;
int line2_timer; // countdown for line 2 refresh
char line1[24]; // line buffer (max LCD_COLS chars displayed)
char line2[24]; // line buffer
char line1_custom[24]; // user-set custom text line 1
char line2_custom[24]; // user-set custom text line 2
char buf[64];
char tmp[32];
// ═══════════════════════════════════════════════════════════════════
// Low-level: write a single byte to PCF8574
// ═══════════════════════════════════════════════════════════════════
void lcd_i2c_write(int val) {
i2cWrite0(lcd_addr, val | lcd_bl, lcd_bus);
}
// ═══════════════════════════════════════════════════════════════════
// Pulse EN line (data latched on falling edge)
// I2C transaction time (~100us at 100kHz) provides sufficient delay
// ═══════════════════════════════════════════════════════════════════
void lcd_pulse(int val) {
lcd_i2c_write(val | LCD_EN);
lcd_i2c_write(val);
}
// ═══════════════════════════════════════════════════════════════════
// Send 4 bits (one nibble)
// ═══════════════════════════════════════════════════════════════════
void lcd_send4(int nibble, int mode) {
// mode: 0=command, LCD_RS=data
int val = (nibble & 0xF0) | mode;
lcd_pulse(val);
}
// ═══════════════════════════════════════════════════════════════════
// Send full byte as two nibbles (high first, then low)
// ═══════════════════════════════════════════════════════════════════
void lcd_send(int byte, int mode) {
lcd_send4(byte & 0xF0, mode);
lcd_send4((byte * 16) & 0xF0, mode);
}
// ═══════════════════════════════════════════════════════════════════
// Send command byte
// ═══════════════════════════════════════════════════════════════════
void lcd_cmd(int cmd) {
lcd_send(cmd, 0);
}
// ═══════════════════════════════════════════════════════════════════
// Send data byte (character)
// ═══════════════════════════════════════════════════════════════════
void lcd_data(int ch) {
lcd_send(ch, LCD_RS);
}
// ═══════════════════════════════════════════════════════════════════
// Print string at current cursor position
// ═══════════════════════════════════════════════════════════════════
void lcd_print(char str[]) {
char lbuf[20];
strcpy(lbuf, str);
int i = 0;
while (lbuf[i] != 0 && i < LCD_COLS) {
lcd_data(lbuf[i]);
i = i + 1;
}
// Pad with spaces to clear remainder of line
while (i < LCD_COLS) {
lcd_data(0x20);
i = i + 1;
}
}
// ═══════════════════════════════════════════════════════════════════
// Set cursor to line (0 or 1), column (0-15)
// ═══════════════════════════════════════════════════════════════════
void lcd_setCursor(int row, int col) {
if (row == 0) {
lcd_cmd(LCD_LINE1 + col);
} else {
lcd_cmd(LCD_LINE2 + col);
}
}
// ═══════════════════════════════════════════════════════════════════
// Print string on specific line (0 or 1)
// ═══════════════════════════════════════════════════════════════════
void lcd_printLine(int row, char str[]) {
lcd_setCursor(row, 0);
lcd_print(str);
}
// ═══════════════════════════════════════════════════════════════════
// Clear display
// ═══════════════════════════════════════════════════════════════════
void lcd_clear() {
lcd_cmd(LCD_CLEAR);
delay(2);
}
// ═══════════════════════════════════════════════════════════════════
// Backlight on/off
// ═══════════════════════════════════════════════════════════════════
void lcd_backlight(int on) {
if (on) {
lcd_bl = LCD_BL;
} else {
lcd_bl = 0;
}
lcd_i2c_write(0);
}
// ═══════════════════════════════════════════════════════════════════
// HD44780 4-bit initialization sequence (must be called from main)
// ═══════════════════════════════════════════════════════════════════
int lcd_init() {
// Auto-detect PCF8574 address on both I2C buses
int found = 0;
int bus = 0;
while (bus <= 1 && found == 0) {
if (i2cExists(0x27, bus)) {
lcd_addr = 0x27;
lcd_bus = bus;
found = 1;
} else if (i2cExists(0x3F, bus)) {
lcd_addr = 0x3F;
lcd_bus = bus;
found = 1;
}
bus = bus + 1;
}
if (found == 0) {
addLog("LCD: no PCF8574 found on bus 0/1");
return -1;
}
sprintf(buf, "LCD: found at 0x%x bus %d", lcd_addr, lcd_bus);
addLog(buf);
lcd_bl = LCD_BL;
// HD44780 requires specific init sequence to enter 4-bit mode
// Wait >40ms after power-on
delay(50);
// Send 0x30 three times to ensure 8-bit mode first
lcd_i2c_write(0);
delay(5);
lcd_send4(0x30, 0);
delay(5);
lcd_send4(0x30, 0);
delay(5);
lcd_send4(0x30, 0);
delay(2);
// Switch to 4-bit mode
lcd_send4(0x20, 0);
delay(2);
// Now in 4-bit mode — configure display
lcd_cmd(LCD_FUNC_4BIT2L); // 4-bit, 2 lines, 5x8 font
lcd_cmd(LCD_DISPLAY_ON); // display on, cursor off, blink off
lcd_cmd(LCD_CLEAR); // clear display
delay(2);
lcd_cmd(LCD_ENTRY_MODE); // increment cursor, no shift
i2cSetActiveFound(lcd_addr, "LCD", lcd_bus);
addLog("LCD: initialized 16x2");
return 0;
}
// ═══════════════════════════════════════════════════════════════════
// Build auto-mode display content
// ═══════════════════════════════════════════════════════════════════
void build_auto_display() {
// Line 1: HH:MM:SS DD.MM
strcpy(line1, "");
sprintf(tmp, "%02d", tasm_hour);
strcat(line1, tmp);
strcat(line1, ":");
sprintf(tmp, "%02d", tasm_minute);
strcat(line1, tmp);
strcat(line1, ":");
sprintf(tmp, "%02d", tasm_second);
strcat(line1, tmp);
strcat(line1, " ");
sprintf(tmp, "%02d", tasm_day);
strcat(line1, tmp);
strcat(line1, ".");
sprintf(tmp, "%02d", tasm_month);
strcat(line1, tmp);
// Line 2: Heap + WiFi status
sprintf(line2, "Heap:%dkB ", tasm_heap / 1000);
if (tasm_wifi) {
strcat(line2, "WiFi:OK");
} else {
strcat(line2, "WiFi:--");
}
}
// ═══════════════════════════════════════════════════════════════════
// EverySecond — update LCD content
// ═══════════════════════════════════════════════════════════════════
void EverySecond() {
if (lcd_ok != 1) { return; }
// Handle backlight change
if (changed(backlight)) {
snapshot(backlight);
lcd_backlight(backlight);
saveVars();
}
// Handle mode change
if (changed(dmode)) {
snapshot(dmode);
lcd_clear();
saveVars();
}
if (dmode == 0) {
// Auto mode: update time (line 1) every second
build_auto_display();
lcd_printLine(0, line1);
// Update line 2 (heap/wifi) every 10 seconds to save instructions
line2_timer = line2_timer - 1;
if (line2_timer <= 0) {
line2_timer = 10;
lcd_printLine(1, line2);
}
} else {
// Custom text mode — only update when text changes
lcd_printLine(0, line1_custom);
lcd_printLine(1, line2_custom);
}
}
// ═══════════════════════════════════════════════════════════════════
// Web UI — configuration
// ═══════════════════════════════════════════════════════════════════
void WebCall() {
webSend("{s}<b style='color:cyan'>LCD 16x2</b>{m}{e}");
if (lcd_ok == 1) {
sprintf(buf, "{s}I2C Address{m}0x%x{e}", lcd_addr);
webSend(buf);
webSend("{s}Status{m}OK{e}");
} else {
webSend("{s}Status{m}NOT FOUND{e}");
}
}
void WebUI() {
webCheckbox(backlight, "Backlight");
webCheckbox(dmode, "Custom text mode");
if (dmode == 1) {
webText(line1_custom, 20, "Line 1");
webText(line2_custom, 20, "Line 2");
}
}
// ═══════════════════════════════════════════════════════════════════
// Command handler — registered as "LCD" prefix
// Commands:
// LCDPrint1 <text> — print text on line 1
// LCDPrint2 <text> — print text on line 2
// LCDClear — clear display
// LCDOn — backlight on
// LCDOff — backlight off
// ═══════════════════════════════════════════════════════════════════
void Command(char cmd[]) {
char arg[24];
char resp[64];
if (strFind(cmd, "Print1") == 0) {
if (strlen(cmd) > 7) {
strSub(arg, cmd, 7, 0);
} else {
strcpy(arg, "");
}
dmode = 1;
strcpy(line1_custom, arg);
lcd_printLine(0, line1_custom);
sprintf(resp, "{\"LCDPrint1\":\"%s\"}", line1_custom);
responseCmnd(resp);
} else if (strFind(cmd, "Print2") == 0) {
if (strlen(cmd) > 7) {
strSub(arg, cmd, 7, 0);
} else {
strcpy(arg, "");
}
dmode = 1;
strcpy(line2_custom, arg);
lcd_printLine(1, line2_custom);
sprintf(resp, "{\"LCDPrint2\":\"%s\"}", line2_custom);
responseCmnd(resp);
} else if (strFind(cmd, "Clear") == 0) {
lcd_clear();
strcpy(line1_custom, "");
strcpy(line2_custom, "");
responseCmnd("{\"LCDClear\":\"done\"}");
} else if (strFind(cmd, "On") == 0) {
backlight = 1;
lcd_backlight(1);
saveVars();
responseCmnd("{\"LCDOn\":\"done\"}");
} else if (strFind(cmd, "Off") == 0) {
backlight = 0;
lcd_backlight(0);
saveVars();
responseCmnd("{\"LCDOff\":\"done\"}");
} else if (strFind(cmd, "Auto") == 0) {
dmode = 0;
lcd_clear();
responseCmnd("{\"LCDAuto\":\"done\"}");
} else {
responseCmnd("{\"LCD\":\"cmds: Print1,Print2,Clear,On,Off,Auto\"}");
}
}
// ═══════════════════════════════════════════════════════════════════
// JSON output
// ═══════════════════════════════════════════════════════════════════
void JsonCall() {
sprintf(buf, ",\"LCD\":{\"Address\":\"0x%x\"", lcd_addr);
responseAppend(buf);
sprintf(buf, ",\"Backlight\":%d}", backlight);
responseAppend(buf);
}
// ═══════════════════════════════════════════════════════════════════
// OnExit — release I2C address
// ═══════════════════════════════════════════════════════════════════
void OnExit() {
if (lcd_ok == 1) {
I2cResetActive(lcd_addr, lcd_bus);
addLog("LCD: released I2C address");
}
}
// ═══════════════════════════════════════════════════════════════════
// MAIN
// ═══════════════════════════════════════════════════════════════════
int main() {
lcd_ok = 0;
strcpy(line1_custom, "Hello TinyC!");
strcpy(line2_custom, "");
// Defaults
if (backlight == 0 && dmode == 0) {
backlight = 1;
}
snapshot(backlight);
snapshot(dmode);
int ok = lcd_init();
if (ok != 0) {
return -1;
}
lcd_ok = 1;
lcd_backlight(backlight);
// Register console command prefix
addCommand("LCD");
// Show welcome message
lcd_printLine(0, "TinyC LCD Driver");
lcd_printLine(1, "Initializing...");
delay(1500);
return 0;
}