Fix LCD1602 blank display: HD44780 timing, pinmap switch, contrast hint.
Match LiquidCrystal_I2C init delays; add Map button for alternate PCF8574 wiring; warn about blue contrast pot. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
10
README.md
10
README.md
@@ -21,14 +21,20 @@ LCD1602A(HD44780)经 **PCF8574 I2C 转接板** 驱动:改两行文字、
|
||||
| SDA | GPIO22 |
|
||||
| SCL | GPIO27 |
|
||||
|
||||
上电会扫描 `0x27 / 0x3F / 0x26 / 0x3E`。串口与 TFT 状态栏会显示是否找到地址。
|
||||
上电会扫描 `0x27 / 0x3F / 0x26 / 0x3E` 等。串口与 TFT 状态栏会显示是否找到地址。
|
||||
|
||||
### 背光有、字没有?
|
||||
|
||||
1. **先拧 I2C 转接板上的蓝色电位器(对比度)**——这是最常见原因;背光晶体管和字符是两回事。
|
||||
2. 供电优先接 **5V**(不是 3.3V)。
|
||||
3. 仍无字:点 TFT 上 **Map** 切换另一种 PCF8574 脚位映射后再试 **Hello**。
|
||||
|
||||
## 操作
|
||||
|
||||
- 中间两行:当前 LCD 内容预览(16 字符/行)
|
||||
- **Demo**:循环示例文案
|
||||
- **Hello**:`Hello World!` / `LCD1602A`
|
||||
- **Clear**:清屏
|
||||
- **Map**:切换 I2C 背包脚位映射并重新初始化
|
||||
- **BL ON/OFF**:背光开关
|
||||
|
||||
## 构建 / 烧录
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: CYD LCD1602A (cyd)
|
||||
git: a4c7d54
|
||||
git: e3f4ea8
|
||||
date: 2026-10-23
|
||||
board: ESP32-2432S028R (ILI9341)
|
||||
features: SoftSPI touch, LVGL, LCD1602A I2C SDA=GPIO22 SCL=GPIO27 PCF8574
|
||||
@@ -12,5 +12,5 @@ offsets:
|
||||
sha256:
|
||||
bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8
|
||||
boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0
|
||||
firmware.bin 6A01742209690884333761FB7C4437965DF816F51771E93539A39CE32FD0E8C4
|
||||
firmware.bin 82F140A15F1E6312DDB8A88782FC1A9D912ED521148A94913701646886AC4B77
|
||||
partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,11 @@ void lcd_init();
|
||||
bool lcd_ok();
|
||||
uint8_t lcd_addr();
|
||||
|
||||
/** 0=标准 YwRobot(P0=RS..),1=部分国产板(EN/RS 换脚) */
|
||||
uint8_t lcd_pinmap();
|
||||
void lcd_set_pinmap(uint8_t map);
|
||||
void lcd_reinit();
|
||||
|
||||
void lcd_set_backlight(bool on);
|
||||
bool lcd_backlight();
|
||||
|
||||
@@ -15,6 +20,5 @@ void lcd_clear();
|
||||
void lcd_set_line(uint8_t row, const char *text);
|
||||
void lcd_get_line(uint8_t row, char *out, size_t out_len);
|
||||
|
||||
/** 写入两行示例文案并点亮背光 */
|
||||
void lcd_demo(uint8_t index);
|
||||
uint8_t lcd_demo_count();
|
||||
|
||||
164
src/lcd.cpp
164
src/lcd.cpp
@@ -5,13 +5,25 @@
|
||||
#include <Wire.h>
|
||||
#include <string.h>
|
||||
|
||||
// PCF8574 背包常见接线:P0=RS P1=RW P2=E P3=BL P4..P7=D4..D7
|
||||
static constexpr uint8_t BIT_RS = 0x01;
|
||||
static constexpr uint8_t BIT_RW = 0x02;
|
||||
static constexpr uint8_t BIT_E = 0x04;
|
||||
static constexpr uint8_t BIT_BL = 0x08;
|
||||
// 两套常见 PCF8574→HD44780 接线(背光位都在 bit3 / bit7 两种)
|
||||
struct PinMap {
|
||||
uint8_t rs;
|
||||
uint8_t rw;
|
||||
uint8_t en;
|
||||
uint8_t bl;
|
||||
uint8_t d4; // bit index 0..7 for D4; D5=d4+1 ... assumed contiguous high nibble OR scattered
|
||||
bool data_high_nibble; // true: D4..D7 on P4..P7
|
||||
const char *name;
|
||||
};
|
||||
|
||||
static constexpr uint8_t CANDIDATE_ADDRS[] = {0x27, 0x3F, 0x26, 0x3E};
|
||||
// Map0: YwRobot / 绝大多数 LiquidCrystal_I2C — P0=RS P1=RW P2=E P3=BL P4..7=D4..7
|
||||
// Map1: 部分模块把控制脚挪到高位 — P0..3=D4..7 P4=E P5=RW P6=RS P7=BL
|
||||
static const PinMap PINMAPS[] = {
|
||||
{0, 1, 2, 3, 4, true, "std P0=RS"},
|
||||
{6, 5, 4, 7, 0, false, "alt P6=RS"},
|
||||
};
|
||||
|
||||
static constexpr uint8_t CANDIDATE_ADDRS[] = {0x27, 0x3F, 0x26, 0x3E, 0x20, 0x38};
|
||||
|
||||
struct DemoMsg {
|
||||
const char *line0;
|
||||
@@ -21,16 +33,21 @@ struct DemoMsg {
|
||||
static const DemoMsg DEMOS[] = {
|
||||
{"Hello CYD!", "LCD1602A I2C"},
|
||||
{"SDA=GPIO22", "SCL=GPIO27"},
|
||||
{"Line1 16 chars!", "Line2 ready...."},
|
||||
{"Turn blue pot", "for contrast!"},
|
||||
{"Backlight ON", "Touch UI demo"},
|
||||
};
|
||||
|
||||
static bool g_ok = false;
|
||||
static bool g_bl = true;
|
||||
static uint8_t g_addr = 0;
|
||||
static uint8_t g_map = 0;
|
||||
static char g_lines[LCD_ROWS][LCD_COLS + 1];
|
||||
|
||||
static uint8_t bl_mask() { return g_bl ? BIT_BL : 0; }
|
||||
static const PinMap &pm() { return PINMAPS[g_map % (sizeof(PINMAPS) / sizeof(PINMAPS[0]))]; }
|
||||
|
||||
static uint8_t bit_of(uint8_t idx) { return (uint8_t)(1u << idx); }
|
||||
|
||||
static uint8_t bl_mask() { return g_bl ? bit_of(pm().bl) : 0; }
|
||||
|
||||
static bool i2c_write(uint8_t data) {
|
||||
Wire.beginTransmission(g_addr);
|
||||
@@ -38,32 +55,57 @@ static bool i2c_write(uint8_t data) {
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
static uint8_t pack_nibble(uint8_t nibble_hi_or_val, uint8_t mode_rs) {
|
||||
const PinMap &m = pm();
|
||||
uint8_t out = bl_mask();
|
||||
if (mode_rs) {
|
||||
out = (uint8_t)(out | bit_of(m.rs));
|
||||
}
|
||||
// RW always 0 (write)
|
||||
if (m.data_high_nibble) {
|
||||
// nibble in bits 7..4
|
||||
out = (uint8_t)(out | (nibble_hi_or_val & 0xF0));
|
||||
} else {
|
||||
// D4..D7 on P0..P3: put high nibble into low nibble
|
||||
out = (uint8_t)(out | ((nibble_hi_or_val >> 4) & 0x0F));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static void pulse_enable(uint8_t data) {
|
||||
i2c_write((uint8_t)(data | BIT_E));
|
||||
const uint8_t en = bit_of(pm().en);
|
||||
i2c_write((uint8_t)(data | en));
|
||||
delayMicroseconds(1);
|
||||
i2c_write((uint8_t)(data & (uint8_t)~BIT_E));
|
||||
i2c_write((uint8_t)(data & (uint8_t)~en));
|
||||
delayMicroseconds(50);
|
||||
}
|
||||
|
||||
static void write4(uint8_t nibble, uint8_t mode) {
|
||||
const uint8_t data = (uint8_t)((nibble & 0xF0) | mode | bl_mask());
|
||||
static void write4(uint8_t value_hi_nibble, bool rs) {
|
||||
// value_hi_nibble: data in upper 4 bits (same as LiquidCrystal_I2C write4bits)
|
||||
const uint8_t data = pack_nibble(value_hi_nibble, rs ? 1 : 0);
|
||||
i2c_write(data);
|
||||
pulse_enable(data);
|
||||
}
|
||||
|
||||
static void send(uint8_t value, uint8_t mode) {
|
||||
write4((uint8_t)(value & 0xF0), mode);
|
||||
write4((uint8_t)((value << 4) & 0xF0), mode);
|
||||
static void send(uint8_t value, bool rs) {
|
||||
write4((uint8_t)(value & 0xF0), rs);
|
||||
write4((uint8_t)((value << 4) & 0xF0), rs);
|
||||
}
|
||||
|
||||
static void cmd(uint8_t c) {
|
||||
send(c, 0);
|
||||
if (c <= 0x03) {
|
||||
delay(2);
|
||||
send(c, false);
|
||||
// clear/home need >1.52ms
|
||||
if (c == 0x01 || c == 0x02) {
|
||||
delay(3);
|
||||
} else {
|
||||
delayMicroseconds(40);
|
||||
}
|
||||
}
|
||||
|
||||
static void data(uint8_t d) { send(d, BIT_RS); }
|
||||
static void data(uint8_t d) {
|
||||
send(d, true);
|
||||
delayMicroseconds(40);
|
||||
}
|
||||
|
||||
static void pad_copy(char *dst, const char *src) {
|
||||
size_t i = 0;
|
||||
@@ -93,37 +135,51 @@ static bool probe_addr(uint8_t addr) {
|
||||
return Wire.endTransmission() == 0;
|
||||
}
|
||||
|
||||
static bool hd44780_init() {
|
||||
// 严格按 LiquidCrystal_I2C::begin 时序
|
||||
static void hd44780_init() {
|
||||
delay(50);
|
||||
// 强制 8-bit 再切 4-bit(手册上电时序)
|
||||
write4(0x30, 0);
|
||||
delay(5);
|
||||
write4(0x30, 0);
|
||||
i2c_write(bl_mask());
|
||||
delay(200);
|
||||
|
||||
// 复位到 8-bit,再切 4-bit
|
||||
write4(0x30, false);
|
||||
delayMicroseconds(4500);
|
||||
write4(0x30, false);
|
||||
delayMicroseconds(4500);
|
||||
write4(0x30, false);
|
||||
delayMicroseconds(150);
|
||||
write4(0x30, 0);
|
||||
delayMicroseconds(150);
|
||||
write4(0x20, 0); // 4-bit
|
||||
write4(0x20, false); // 4-bit
|
||||
delayMicroseconds(150);
|
||||
|
||||
cmd(0x28); // 4-bit, 2 lines, 5x8
|
||||
cmd(0x0C); // display on, cursor off
|
||||
cmd(0x06); // entry mode
|
||||
cmd(0x28); // 4-bit, 2-line, 5x8
|
||||
cmd(0x0C); // display on, cursor off, blink off
|
||||
cmd(0x06); // entry: increment, no shift
|
||||
cmd(0x01); // clear
|
||||
delay(2);
|
||||
return true;
|
||||
delay(3);
|
||||
}
|
||||
|
||||
static void apply_lines() {
|
||||
write_row(0);
|
||||
write_row(1);
|
||||
}
|
||||
|
||||
void lcd_init() {
|
||||
memset(g_lines, 0, sizeof(g_lines));
|
||||
pad_copy(g_lines[0], "LCD1602A");
|
||||
pad_copy(g_lines[1], "init...");
|
||||
pad_copy(g_lines[1], "turn blue pot");
|
||||
|
||||
Wire.begin(LCD_I2C_SDA_PIN, LCD_I2C_SCL_PIN, (uint32_t)LCD_I2C_FREQ_HZ);
|
||||
delay(20);
|
||||
Wire.end();
|
||||
delay(2);
|
||||
Wire.begin(LCD_I2C_SDA_PIN, LCD_I2C_SCL_PIN);
|
||||
Wire.setClock(LCD_I2C_FREQ_HZ);
|
||||
delay(50);
|
||||
|
||||
g_ok = false;
|
||||
g_addr = 0;
|
||||
g_bl = true;
|
||||
if (g_map >= (sizeof(PINMAPS) / sizeof(PINMAPS[0]))) {
|
||||
g_map = 0;
|
||||
}
|
||||
|
||||
for (uint8_t a : CANDIDATE_ADDRS) {
|
||||
if (probe_addr(a)) {
|
||||
@@ -135,33 +191,40 @@ void lcd_init() {
|
||||
}
|
||||
|
||||
if (!g_ok) {
|
||||
Serial.printf("LCD1602A NOT FOUND (SDA=GPIO%d SCL=GPIO%d). Tried:",
|
||||
Serial.printf("LCD1602A NOT FOUND (SDA=GPIO%d SCL=GPIO%d)\n",
|
||||
LCD_I2C_SDA_PIN, LCD_I2C_SCL_PIN);
|
||||
for (uint8_t a : CANDIDATE_ADDRS) {
|
||||
Serial.printf(" 0x%02X", a);
|
||||
}
|
||||
Serial.println();
|
||||
pad_copy(g_lines[0], "NO I2C LCD");
|
||||
pad_copy(g_lines[1], "check wiring");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hd44780_init()) {
|
||||
g_ok = false;
|
||||
Serial.println("LCD1602A HD44780 init failed");
|
||||
return;
|
||||
}
|
||||
|
||||
write_row(0);
|
||||
write_row(1);
|
||||
Serial.printf("LCD1602A OK SDA=GPIO%d SCL=GPIO%d addr=0x%02X\n",
|
||||
LCD_I2C_SDA_PIN, LCD_I2C_SCL_PIN, g_addr);
|
||||
hd44780_init();
|
||||
apply_lines();
|
||||
Serial.printf("LCD1602A OK addr=0x%02X map=%u(%s) — if blank, turn BLUE pot\n",
|
||||
g_addr, (unsigned)g_map, pm().name);
|
||||
}
|
||||
|
||||
bool lcd_ok() { return g_ok; }
|
||||
|
||||
uint8_t lcd_addr() { return g_addr; }
|
||||
|
||||
uint8_t lcd_pinmap() { return g_map; }
|
||||
|
||||
void lcd_set_pinmap(uint8_t map) {
|
||||
const uint8_t n = (uint8_t)(sizeof(PINMAPS) / sizeof(PINMAPS[0]));
|
||||
g_map = (uint8_t)(map % n);
|
||||
Serial.printf("LCD pinmap -> %u (%s)\n", (unsigned)g_map, pm().name);
|
||||
if (g_ok) {
|
||||
hd44780_init();
|
||||
apply_lines();
|
||||
}
|
||||
}
|
||||
|
||||
void lcd_reinit() {
|
||||
Serial.println("LCD reinit");
|
||||
lcd_init();
|
||||
}
|
||||
|
||||
void lcd_set_backlight(bool on) {
|
||||
g_bl = on;
|
||||
if (!g_ok) {
|
||||
@@ -180,7 +243,6 @@ void lcd_clear() {
|
||||
return;
|
||||
}
|
||||
cmd(0x01);
|
||||
delay(2);
|
||||
Serial.println("LCD clear");
|
||||
}
|
||||
|
||||
|
||||
43
src/ui.cpp
43
src/ui.cpp
@@ -14,16 +14,19 @@ static constexpr uint32_t COL_ON = 0x1B5E4A;
|
||||
static constexpr uint32_t COL_ACCENT = 0x7CDBFF;
|
||||
static constexpr uint32_t COL_DIM = 0x3A4656;
|
||||
static constexpr uint32_t COL_ERR = 0xC62828;
|
||||
static constexpr uint32_t COL_WARN = 0xFFB74D;
|
||||
|
||||
static lv_obj_t *lbl_title;
|
||||
static lv_obj_t *lbl_pin;
|
||||
static lv_obj_t *lbl_status;
|
||||
static lv_obj_t *lbl_hint;
|
||||
static lv_obj_t *lbl_line0;
|
||||
static lv_obj_t *lbl_line1;
|
||||
static lv_obj_t *btn_demo;
|
||||
static lv_obj_t *lbl_demo;
|
||||
static lv_obj_t *btn_hello;
|
||||
static lv_obj_t *btn_clear;
|
||||
static lv_obj_t *btn_map;
|
||||
static lv_obj_t *lbl_map;
|
||||
static lv_obj_t *btn_bl;
|
||||
static lv_obj_t *lbl_bl;
|
||||
|
||||
@@ -72,7 +75,8 @@ void ui_refresh_lcd() {
|
||||
|
||||
if (lbl_status) {
|
||||
if (lcd_ok()) {
|
||||
lv_label_set_text_fmt(lbl_status, "OK addr 0x%02X", lcd_addr());
|
||||
lv_label_set_text_fmt(lbl_status, "OK 0x%02X map%u", lcd_addr(),
|
||||
(unsigned)lcd_pinmap());
|
||||
lv_obj_set_style_text_color(lbl_status, lv_color_hex(COL_ON), 0);
|
||||
} else {
|
||||
lv_label_set_text(lbl_status, "FAIL no PCF8574");
|
||||
@@ -91,6 +95,9 @@ void ui_refresh_lcd() {
|
||||
(unsigned)(g_demo_idx % lcd_demo_count() + 1),
|
||||
(unsigned)lcd_demo_count());
|
||||
}
|
||||
if (lbl_map) {
|
||||
lv_label_set_text_fmt(lbl_map, "Map %u", (unsigned)lcd_pinmap());
|
||||
}
|
||||
}
|
||||
|
||||
static void on_demo(lv_event_t *e) {
|
||||
@@ -112,11 +119,14 @@ static void on_hello(lv_event_t *e) {
|
||||
ui_refresh_lcd();
|
||||
}
|
||||
|
||||
static void on_clear(lv_event_t *e) {
|
||||
static void on_map(lv_event_t *e) {
|
||||
if (lv_event_get_code(e) != LV_EVENT_CLICKED) {
|
||||
return;
|
||||
}
|
||||
lcd_clear();
|
||||
lcd_set_pinmap((uint8_t)(lcd_pinmap() + 1));
|
||||
lcd_set_backlight(true);
|
||||
lcd_set_line(0, "Hello World!");
|
||||
lcd_set_line(1, "try Map / pot");
|
||||
ui_refresh_lcd();
|
||||
}
|
||||
|
||||
@@ -137,48 +147,53 @@ void ui_create() {
|
||||
lv_label_set_text(lbl_title, "LCD1602A");
|
||||
lv_obj_set_style_text_color(lbl_title, lv_color_hex(COL_INK), 0);
|
||||
lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_28, 0);
|
||||
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 4);
|
||||
lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 2);
|
||||
|
||||
lbl_pin = lv_label_create(scr);
|
||||
lv_label_set_text_fmt(lbl_pin, "I2C SDA%d SCL%d PCF8574", LCD_I2C_SDA_PIN,
|
||||
LCD_I2C_SCL_PIN);
|
||||
lv_obj_set_style_text_color(lbl_pin, lv_color_hex(COL_MUTED), 0);
|
||||
lv_obj_set_style_text_font(lbl_pin, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(lbl_pin, LV_ALIGN_TOP_MID, 0, 36);
|
||||
lv_obj_align(lbl_pin, LV_ALIGN_TOP_MID, 0, 32);
|
||||
|
||||
lbl_status = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_status, "...");
|
||||
lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(lbl_status, LV_ALIGN_TOP_MID, 0, 54);
|
||||
lv_obj_align(lbl_status, LV_ALIGN_TOP_MID, 0, 50);
|
||||
|
||||
lbl_hint = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_hint, "Blank? turn BLUE pot on backpack");
|
||||
lv_obj_set_style_text_color(lbl_hint, lv_color_hex(COL_WARN), 0);
|
||||
lv_obj_set_style_text_font(lbl_hint, &lv_font_montserrat_14, 0);
|
||||
lv_obj_align(lbl_hint, LV_ALIGN_TOP_MID, 0, 68);
|
||||
|
||||
lbl_line0 = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_line0, "[ ]");
|
||||
lv_obj_set_style_text_color(lbl_line0, lv_color_hex(COL_ACCENT), 0);
|
||||
lv_obj_set_style_text_font(lbl_line0, &lv_font_montserrat_18, 0);
|
||||
lv_obj_align(lbl_line0, LV_ALIGN_TOP_MID, 0, 78);
|
||||
lv_obj_align(lbl_line0, LV_ALIGN_TOP_MID, 0, 90);
|
||||
|
||||
lbl_line1 = lv_label_create(scr);
|
||||
lv_label_set_text(lbl_line1, "[ ]");
|
||||
lv_obj_set_style_text_color(lbl_line1, lv_color_hex(COL_ACCENT), 0);
|
||||
lv_obj_set_style_text_font(lbl_line1, &lv_font_montserrat_18, 0);
|
||||
lv_obj_align(lbl_line1, LV_ALIGN_TOP_MID, 0, 104);
|
||||
lv_obj_align(lbl_line1, LV_ALIGN_TOP_MID, 0, 116);
|
||||
|
||||
const int16_t gap = 8;
|
||||
const int16_t bw = (UI_HOR_RES - 48 - gap) / 2;
|
||||
const int16_t bx0 = 24;
|
||||
const int16_t row0 = 140;
|
||||
const int16_t row1 = 188;
|
||||
const int16_t row0 = 148;
|
||||
const int16_t row1 = 196;
|
||||
|
||||
btn_demo = make_btn(scr, "Demo", bx0, row0, bw, 40, on_demo, &lbl_demo);
|
||||
btn_hello = make_btn(scr, "Hello", bx0 + bw + gap, row0, bw, 40, on_hello,
|
||||
nullptr);
|
||||
btn_clear =
|
||||
make_btn(scr, "Clear", bx0, row1, bw, 40, on_clear, nullptr);
|
||||
btn_map = make_btn(scr, "Map 0", bx0, row1, bw, 40, on_map, &lbl_map);
|
||||
btn_bl = make_btn(scr, "BL OFF", bx0 + bw + gap, row1, bw, 40, on_bl, &lbl_bl);
|
||||
|
||||
(void)btn_demo;
|
||||
(void)btn_hello;
|
||||
(void)btn_clear;
|
||||
(void)btn_map;
|
||||
|
||||
ui_refresh_lcd();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user