diff --git a/README.md b/README.md index 125a1e0..3cd1e5f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Waveshare ESP32-S3-Touch-LCD-3.49 - C64 READY -PlatformIO starter showing Commodore 64-style READY. with a blinking cursor. +PlatformIO retro prompt selector with six classic computer looks and a blinking cursor. + +- Tap **BOOT** to cycle through Commodore 64, Sepia UNIX, Amiga Workbench, + Apple II, MS-DOS, and BBC Micro prompts. +- Hold **PWR** for 1.2 seconds to turn the device off. Battery operation uses + the board's TCA9554 power latch; USB operation falls back to deep sleep. +- While connected to USB, tap **PWR** to wake from deep sleep. Hardware: ESP32-S3R8, 16 MB flash, 8 MB PSRAM, 172x640 AXS15231B QSPI LCD. diff --git a/platformio.ini b/platformio.ini index 3275d77..566986c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -17,5 +17,7 @@ build_flags = -DLV_COLOR_DEPTH=16 -DLV_COLOR_16_SWAP=1 -DLV_FONT_MONTSERRAT_48=1 + -DLV_FONT_MONTSERRAT_24=1 + -DLV_FONT_MONTSERRAT_20=1 lib_deps = lvgl/lvgl@8.4.0 diff --git a/src/c64_ui.c b/src/c64_ui.c index b47a826..eef281b 100644 --- a/src/c64_ui.c +++ b/src/c64_ui.c @@ -1,81 +1,107 @@ #include "c64_ui.h" #include -#include - -#define C64_BLUE lv_color_make(64, 49, 141) -#define C64_LIGHT_BLUE lv_color_make(120, 105, 196) -#define PIXEL_SCALE 6 -#define CELL_SIZE (8 * PIXEL_SCALE) +#include typedef struct { - char character; - uint8_t rows[8]; -} c64_glyph_t; + const char *name; + const char *prompt; + uint32_t background; + uint32_t foreground; +} retro_theme_t; -/* Exact uppercase/graphics glyph rows from the C64 system character ROM. */ -static const c64_glyph_t glyphs[] = { - {'R', {0x7C, 0x66, 0x66, 0x7C, 0x78, 0x6C, 0x66, 0x00}}, - {'E', {0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x7E, 0x00}}, - {'A', {0x18, 0x3C, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00}}, - {'D', {0x78, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0x78, 0x00}}, - {'Y', {0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x00}}, - {'.', {0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00}}, +static const retro_theme_t themes[] = { + {"COMMODORE 64", "READY.", 0x40318d, 0x7869c4}, + {"SEPIA UNIX", "Last login: Mon Aug 3 08:00:00\n\nhelios@unix:~$", + 0x302314, 0xe1bf85}, + {"AMIGA WORKBENCH 3.1", "AmigaDOS\nCopyright 1985-1993 Commodore-Amiga\n\n1.System3.1:>", + 0x0c2c7e, 0xffffff}, + {"APPLE II", "APPLE ][\n\n]CALL -151\n*", + 0x041405, 0x52ff58}, + {"MS-DOS 6.22", "Microsoft(R) MS-DOS(R) Version 6.22\n\nC:\\>", + 0x000000, 0xc0c0c0}, + {"BBC MICRO", "BBC Computer 32K\n\nBASIC\n>", + 0x000000, 0xffffff}, }; -static const uint8_t *find_glyph(char character) { - for (unsigned i = 0; i < sizeof(glyphs) / sizeof(glyphs[0]); ++i) { - if (glyphs[i].character == character) return glyphs[i].rows; - } - return NULL; -} - -static void draw_glyph(lv_obj_t *parent, char character, lv_coord_t x, lv_coord_t y) { - const uint8_t *rows = find_glyph(character); - if (!rows) return; - - for (int row = 0; row < 8; ++row) { - for (int column = 0; column < 8; ++column) { - if ((rows[row] & (0x80 >> column)) == 0) continue; - lv_obj_t *pixel = lv_obj_create(parent); - lv_obj_remove_style_all(pixel); - lv_obj_set_size(pixel, PIXEL_SCALE, PIXEL_SCALE); - lv_obj_set_pos(pixel, x + column * PIXEL_SCALE, y + row * PIXEL_SCALE); - lv_obj_set_style_bg_color(pixel, C64_LIGHT_BLUE, LV_PART_MAIN); - lv_obj_set_style_bg_opa(pixel, LV_OPA_COVER, LV_PART_MAIN); - } - } -} - +static unsigned theme_index; +static lv_obj_t *cursor_label; +static lv_timer_t *cursor_timer; static bool cursor_visible = true; -static void cursor_blink(lv_timer_t *timer) { +static void cursor_blink(lv_timer_t *timer) +{ + (void)timer; + if (cursor_label == NULL) return; cursor_visible = !cursor_visible; - lv_obj_t *cursor = (lv_obj_t *)timer->user_data; - lv_obj_set_style_bg_opa(cursor, - cursor_visible ? LV_OPA_COVER : LV_OPA_TRANSP, - LV_PART_MAIN); - lv_timer_set_period(timer, 400); + lv_obj_set_style_text_opa(cursor_label, + cursor_visible ? LV_OPA_COVER : LV_OPA_20, + LV_PART_MAIN); } -void c64_ui_create(void) { - static const char text[] = "READY."; +static void render_theme(void) +{ + const retro_theme_t *theme = &themes[theme_index]; lv_obj_t *screen = lv_scr_act(); - lv_obj_set_style_bg_color(screen, C64_BLUE, LV_PART_MAIN); - lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE); - - const lv_coord_t start_x = 48; - const lv_coord_t text_y = 24; - for (unsigned i = 0; i < sizeof(text) - 1; ++i) { - draw_glyph(screen, text[i], start_x + (lv_coord_t)i * CELL_SIZE, text_y); + if (cursor_timer != NULL) { + lv_timer_del(cursor_timer); + cursor_timer = NULL; } + cursor_label = NULL; + lv_obj_clean(screen); + lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_color(screen, lv_color_hex(theme->background), LV_PART_MAIN); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN); - lv_obj_t *cursor = lv_obj_create(screen); - lv_obj_remove_style_all(cursor); - lv_obj_set_size(cursor, CELL_SIZE, CELL_SIZE); - lv_obj_set_pos(cursor, start_x, text_y + CELL_SIZE + 16); - lv_obj_set_style_bg_color(cursor, C64_LIGHT_BLUE, LV_PART_MAIN); - lv_obj_set_style_bg_opa(cursor, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_t *header = lv_label_create(screen); + lv_label_set_text(header, theme->name); + lv_obj_set_pos(header, 28, 14); + lv_obj_set_style_text_color(header, lv_color_hex(theme->foreground), LV_PART_MAIN); + lv_obj_set_style_text_font(header, &lv_font_montserrat_24, LV_PART_MAIN); - lv_timer_create(cursor_blink, 400, cursor); + lv_obj_t *prompt = lv_label_create(screen); + lv_label_set_text(prompt, theme->prompt); + lv_obj_set_pos(prompt, 28, 50); + lv_obj_set_width(prompt, 580); + lv_obj_set_style_text_color(prompt, lv_color_hex(theme->foreground), LV_PART_MAIN); + lv_obj_set_style_text_font(prompt, &lv_font_montserrat_20, LV_PART_MAIN); + lv_obj_set_style_text_line_space(prompt, 4, LV_PART_MAIN); + + cursor_label = lv_label_create(screen); + lv_label_set_text(cursor_label, "_"); + lv_obj_set_pos(cursor_label, 590, 128); + lv_obj_set_style_text_color(cursor_label, lv_color_hex(theme->foreground), LV_PART_MAIN); + lv_obj_set_style_text_font(cursor_label, &lv_font_montserrat_20, LV_PART_MAIN); + + cursor_visible = true; + cursor_timer = lv_timer_create(cursor_blink, 500, NULL); +} + +void c64_ui_create(void) +{ + theme_index = 0; + render_theme(); +} + +void retro_ui_next(void) +{ + theme_index = (theme_index + 1) % (sizeof(themes) / sizeof(themes[0])); + render_theme(); +} + +void retro_ui_show_powering_off(void) +{ + lv_obj_t *screen = lv_scr_act(); + if (cursor_timer != NULL) { + lv_timer_del(cursor_timer); + cursor_timer = NULL; + } + cursor_label = NULL; + lv_obj_clean(screen); + lv_obj_set_style_bg_color(screen, lv_color_black(), LV_PART_MAIN); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN); + lv_obj_t *label = lv_label_create(screen); + lv_label_set_text(label, "POWERING OFF..."); + lv_obj_center(label); + lv_obj_set_style_text_color(label, lv_color_white(), LV_PART_MAIN); + lv_obj_set_style_text_font(label, &lv_font_montserrat_24, LV_PART_MAIN); } diff --git a/src/c64_ui.h b/src/c64_ui.h index 0e1146b..eb53b8b 100644 --- a/src/c64_ui.h +++ b/src/c64_ui.h @@ -3,6 +3,8 @@ extern "C" { #endif void c64_ui_create(void); +void retro_ui_next(void); +void retro_ui_show_powering_off(void); #ifdef __cplusplus } #endif diff --git a/src/i2c_bsp.c b/src/i2c_bsp.c index 5cae425..c263c6f 100644 --- a/src/i2c_bsp.c +++ b/src/i2c_bsp.c @@ -5,6 +5,7 @@ static i2c_master_bus_handle_t user_i2c_port0_handle = NULL; static i2c_master_bus_handle_t user_i2c_port1_handle = NULL; +static i2c_master_dev_handle_t power_expander_handle = NULL; i2c_master_dev_handle_t disp_touch_dev_handle = NULL; i2c_master_dev_handle_t rtc_dev_handle = NULL; i2c_master_dev_handle_t imu_dev_handle = NULL; @@ -52,6 +53,39 @@ void i2c_master_Init(void) } +bool power_latch_init(void) +{ + if (power_expander_handle == NULL) { + i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = 0x20, + .scl_speed_hz = 300000, + }; + if (i2c_master_bus_add_device(user_i2c_port0_handle, &dev_cfg, + &power_expander_handle) != ESP_OK) { + return false; + } + } + + uint8_t output = 0xff; + uint8_t direction = 0xff; + i2c_read_buff(power_expander_handle, 0x01, &output, 1); + i2c_read_buff(power_expander_handle, 0x03, &direction, 1); + output |= (1U << 6); + direction &= ~(1U << 6); + return i2c_write_buff(power_expander_handle, 0x01, &output, 1) == ESP_OK && + i2c_write_buff(power_expander_handle, 0x03, &direction, 1) == ESP_OK; +} + +void power_latch_off(void) +{ + if (power_expander_handle == NULL) return; + uint8_t output = 0xff; + i2c_read_buff(power_expander_handle, 0x01, &output, 1); + output &= ~(1U << 6); + i2c_write_buff(power_expander_handle, 0x01, &output, 1); +} + uint8_t i2c_write_buff(i2c_master_dev_handle_t dev_handle,int reg,uint8_t *buf,uint8_t len) { uint8_t ret; diff --git a/src/i2c_bsp.h b/src/i2c_bsp.h index 84f35fe..822feb8 100644 --- a/src/i2c_bsp.h +++ b/src/i2c_bsp.h @@ -1,5 +1,6 @@ #ifndef I2C_BSP_H #define I2C_BSP_H +#include #include "driver/i2c_master.h" extern i2c_master_dev_handle_t disp_touch_dev_handle; @@ -11,6 +12,8 @@ extern "C" { #endif void i2c_master_Init(void); +bool power_latch_init(void); +void power_latch_off(void); uint8_t i2c_write_buff(i2c_master_dev_handle_t dev_handle,int reg,uint8_t *buf,uint8_t len); uint8_t i2c_master_write_read_dev(i2c_master_dev_handle_t dev_handle,uint8_t *writeBuf,uint8_t writeLen,uint8_t *readBuf,uint8_t readLen); uint8_t i2c_read_buff(i2c_master_dev_handle_t dev_handle,int reg,uint8_t *buf,uint8_t len); @@ -20,4 +23,4 @@ uint8_t i2c_master_touch_write_read(i2c_master_dev_handle_t dev_handle,uint8_t * } #endif -#endif \ No newline at end of file +#endif diff --git a/src/lvgl_port.c b/src/lvgl_port.c index 5f2de77..5ec766a 100644 --- a/src/lvgl_port.c +++ b/src/lvgl_port.c @@ -120,13 +120,13 @@ static void example_lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) } } -static bool example_lvgl_lock(int timeout_ms) +bool lvgl_port_lock(int timeout_ms) { const TickType_t timeout_ticks = (timeout_ms == -1) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms); return xSemaphoreTake(lvgl_mux, timeout_ticks) == pdTRUE; } -static void example_lvgl_unlock(void) +void lvgl_port_unlock(void) { assert(lvgl_mux && "bsp_display_start must be called first"); xSemaphoreGive(lvgl_mux); @@ -137,11 +137,11 @@ void example_lvgl_port_task(void *arg) uint32_t task_delay_ms = EXAMPLE_LVGL_TASK_MAX_DELAY_MS; for(;;) { - if (example_lvgl_lock(-1)) + if (lvgl_port_lock(-1)) { task_delay_ms = lv_timer_handler(); //Release the mutex - example_lvgl_unlock(); + lvgl_port_unlock(); } if (task_delay_ms > EXAMPLE_LVGL_TASK_MAX_DELAY_MS) { @@ -260,9 +260,9 @@ void lvgl_port_init(void) lvgl_mux = xSemaphoreCreateMutex(); assert(lvgl_mux); xTaskCreatePinnedToCore(example_lvgl_port_task, "LVGL", 4000, NULL, 4, NULL,0); //运行于内核_0 - if (example_lvgl_lock(-1)) + if (lvgl_port_lock(-1)) { c64_ui_create(); - example_lvgl_unlock(); + lvgl_port_unlock(); } -} \ No newline at end of file +} diff --git a/src/lvgl_port.h b/src/lvgl_port.h index 42d219e..5289a5e 100644 --- a/src/lvgl_port.h +++ b/src/lvgl_port.h @@ -1,5 +1,6 @@ #ifndef LVGL_PORT_H #define LVGL_PORT_H +#include @@ -9,6 +10,8 @@ extern "C" { void lvgl_port_init(void); +bool lvgl_port_lock(int timeout_ms); +void lvgl_port_unlock(void); #ifdef __cplusplus diff --git a/src/main.cpp b/src/main.cpp index 2ae2893..56eef91 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,91 @@ #include +#include "esp_sleep.h" #include "i2c_bsp.h" #include "lvgl_port.h" +#include "c64_ui.h" #include "lcd_bl_bsp/lcd_bl_pwm_bsp.h" -void setup(){Serial.begin(115200);i2c_master_Init();lvgl_port_init();lcd_bl_pwm_bsp_init(LCD_PWM_MODE_255);Serial.println("READY.");} -void loop(){delay(1000);} + +static constexpr gpio_num_t BOOT_BUTTON = GPIO_NUM_0; +static constexpr gpio_num_t POWER_BUTTON = GPIO_NUM_16; +static constexpr uint32_t DEBOUNCE_MS = 35; +static constexpr uint32_t POWER_HOLD_MS = 1200; + +struct ButtonState { + gpio_num_t pin; + bool raw; + bool stable; + uint32_t changed_at; + uint32_t pressed_at; + bool long_handled; +}; + +static ButtonState boot_button{BOOT_BUTTON, true, true, 0, 0, false}; +static ButtonState power_button{POWER_BUTTON, true, true, 0, 0, false}; + +static void shutdown_device() +{ + if (lvgl_port_lock(250)) { + retro_ui_show_powering_off(); + lvgl_port_unlock(); + } + delay(250); + setUpduty(LCD_PWM_MODE_0); + power_latch_off(); + + // Battery power is now removed. On USB, wait for release and use deep sleep. + while (gpio_get_level(POWER_BUTTON) == 0) delay(10); + esp_sleep_enable_ext0_wakeup(POWER_BUTTON, 0); + esp_deep_sleep_start(); +} + +static bool update_button(ButtonState &button) +{ + const uint32_t now = millis(); + const bool raw = gpio_get_level(button.pin) != 0; + if (raw != button.raw) { + button.raw = raw; + button.changed_at = now; + } + if (raw != button.stable && now - button.changed_at >= DEBOUNCE_MS) { + button.stable = raw; + if (!raw) { + button.pressed_at = now; + button.long_handled = false; + } else { + return true; + } + } + return false; +} + +void setup() +{ + Serial.begin(115200); + i2c_master_Init(); + power_latch_init(); + pinMode(BOOT_BUTTON, INPUT_PULLUP); + pinMode(POWER_BUTTON, INPUT_PULLUP); + lvgl_port_init(); + lcd_bl_pwm_bsp_init(LCD_PWM_MODE_255); + Serial.println("Retro prompt selector ready"); +} + +void loop() +{ + const bool boot_released = update_button(boot_button); + update_button(power_button); + + if (boot_released && !boot_button.long_handled) { + if (lvgl_port_lock(250)) { + retro_ui_next(); + lvgl_port_unlock(); + } + } + + if (!power_button.stable && !power_button.long_handled && + millis() - power_button.pressed_at >= POWER_HOLD_MS) { + power_button.long_handled = true; + shutdown_device(); + } + delay(5); +}