diff --git a/README.md b/README.md index fbba5b8..aa7a83b 100644 --- a/README.md +++ b/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**:背光开关 ## 构建 / 烧录 diff --git a/firmware/cyd/VERSION.txt b/firmware/cyd/VERSION.txt index 350161e..8b175c7 100644 --- a/firmware/cyd/VERSION.txt +++ b/firmware/cyd/VERSION.txt @@ -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 diff --git a/firmware/cyd/firmware.bin b/firmware/cyd/firmware.bin index b8e6817..61048f0 100644 Binary files a/firmware/cyd/firmware.bin and b/firmware/cyd/firmware.bin differ diff --git a/include/lcd.h b/include/lcd.h index 773f044..89e9566 100644 --- a/include/lcd.h +++ b/include/lcd.h @@ -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(); diff --git a/src/lcd.cpp b/src/lcd.cpp index 4a583b0..9ffd97f 100644 --- a/src/lcd.cpp +++ b/src/lcd.cpp @@ -5,13 +5,25 @@ #include #include -// 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"); } diff --git a/src/ui.cpp b/src/ui.cpp index 351fd4c..1c0a3d2 100644 --- a/src/ui.cpp +++ b/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(); }