Add hardware power-off and retro prompt selector

This commit is contained in:
2026-08-03 00:46:39 +02:00
parent d0b5e04bad
commit a3f6d061e3
9 changed files with 236 additions and 75 deletions
+7 -1
View File
@@ -1,6 +1,12 @@
# Waveshare ESP32-S3-Touch-LCD-3.49 - C64 READY # 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. Hardware: ESP32-S3R8, 16 MB flash, 8 MB PSRAM, 172x640 AXS15231B QSPI LCD.
+2
View File
@@ -17,5 +17,7 @@ build_flags =
-DLV_COLOR_DEPTH=16 -DLV_COLOR_DEPTH=16
-DLV_COLOR_16_SWAP=1 -DLV_COLOR_16_SWAP=1
-DLV_FONT_MONTSERRAT_48=1 -DLV_FONT_MONTSERRAT_48=1
-DLV_FONT_MONTSERRAT_24=1
-DLV_FONT_MONTSERRAT_20=1
lib_deps = lib_deps =
lvgl/lvgl@8.4.0 lvgl/lvgl@8.4.0
+90 -64
View File
@@ -1,81 +1,107 @@
#include "c64_ui.h" #include "c64_ui.h"
#include <lvgl.h> #include <lvgl.h>
#include <stdint.h> #include <stdbool.h>
#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)
typedef struct { typedef struct {
char character; const char *name;
uint8_t rows[8]; const char *prompt;
} c64_glyph_t; uint32_t background;
uint32_t foreground;
} retro_theme_t;
/* Exact uppercase/graphics glyph rows from the C64 system character ROM. */ static const retro_theme_t themes[] = {
static const c64_glyph_t glyphs[] = { {"COMMODORE 64", "READY.", 0x40318d, 0x7869c4},
{'R', {0x7C, 0x66, 0x66, 0x7C, 0x78, 0x6C, 0x66, 0x00}}, {"SEPIA UNIX", "Last login: Mon Aug 3 08:00:00\n\nhelios@unix:~$",
{'E', {0x7E, 0x60, 0x60, 0x78, 0x60, 0x60, 0x7E, 0x00}}, 0x302314, 0xe1bf85},
{'A', {0x18, 0x3C, 0x66, 0x7E, 0x66, 0x66, 0x66, 0x00}}, {"AMIGA WORKBENCH 3.1", "AmigaDOS\nCopyright 1985-1993 Commodore-Amiga\n\n1.System3.1:>",
{'D', {0x78, 0x6C, 0x66, 0x66, 0x66, 0x6C, 0x78, 0x00}}, 0x0c2c7e, 0xffffff},
{'Y', {0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x00}}, {"APPLE II", "APPLE ][\n\n]CALL -151\n*",
{'.', {0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00}}, 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) { static unsigned theme_index;
for (unsigned i = 0; i < sizeof(glyphs) / sizeof(glyphs[0]); ++i) { static lv_obj_t *cursor_label;
if (glyphs[i].character == character) return glyphs[i].rows; static lv_timer_t *cursor_timer;
}
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 bool cursor_visible = true; 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; cursor_visible = !cursor_visible;
lv_obj_t *cursor = (lv_obj_t *)timer->user_data; lv_obj_set_style_text_opa(cursor_label,
lv_obj_set_style_bg_opa(cursor, cursor_visible ? LV_OPA_COVER : LV_OPA_20,
cursor_visible ? LV_OPA_COVER : LV_OPA_TRANSP, LV_PART_MAIN);
LV_PART_MAIN);
lv_timer_set_period(timer, 400);
} }
void c64_ui_create(void) { static void render_theme(void)
static const char text[] = "READY."; {
const retro_theme_t *theme = &themes[theme_index];
lv_obj_t *screen = lv_scr_act(); lv_obj_t *screen = lv_scr_act();
lv_obj_set_style_bg_color(screen, C64_BLUE, LV_PART_MAIN); if (cursor_timer != NULL) {
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN); lv_timer_del(cursor_timer);
lv_obj_clear_flag(screen, LV_OBJ_FLAG_SCROLLABLE); cursor_timer = NULL;
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);
} }
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_t *header = lv_label_create(screen);
lv_obj_remove_style_all(cursor); lv_label_set_text(header, theme->name);
lv_obj_set_size(cursor, CELL_SIZE, CELL_SIZE); lv_obj_set_pos(header, 28, 14);
lv_obj_set_pos(cursor, start_x, text_y + CELL_SIZE + 16); lv_obj_set_style_text_color(header, lv_color_hex(theme->foreground), LV_PART_MAIN);
lv_obj_set_style_bg_color(cursor, C64_LIGHT_BLUE, LV_PART_MAIN); lv_obj_set_style_text_font(header, &lv_font_montserrat_24, LV_PART_MAIN);
lv_obj_set_style_bg_opa(cursor, LV_OPA_COVER, 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);
} }
+2
View File
@@ -3,6 +3,8 @@
extern "C" { extern "C" {
#endif #endif
void c64_ui_create(void); void c64_ui_create(void);
void retro_ui_next(void);
void retro_ui_show_powering_off(void);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+34
View File
@@ -5,6 +5,7 @@
static i2c_master_bus_handle_t user_i2c_port0_handle = NULL; 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_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 disp_touch_dev_handle = NULL;
i2c_master_dev_handle_t rtc_dev_handle = NULL; i2c_master_dev_handle_t rtc_dev_handle = NULL;
i2c_master_dev_handle_t imu_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 i2c_write_buff(i2c_master_dev_handle_t dev_handle,int reg,uint8_t *buf,uint8_t len)
{ {
uint8_t ret; uint8_t ret;
+3
View File
@@ -1,5 +1,6 @@
#ifndef I2C_BSP_H #ifndef I2C_BSP_H
#define I2C_BSP_H #define I2C_BSP_H
#include <stdbool.h>
#include "driver/i2c_master.h" #include "driver/i2c_master.h"
extern i2c_master_dev_handle_t disp_touch_dev_handle; extern i2c_master_dev_handle_t disp_touch_dev_handle;
@@ -11,6 +12,8 @@ extern "C" {
#endif #endif
void i2c_master_Init(void); 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_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_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); uint8_t i2c_read_buff(i2c_master_dev_handle_t dev_handle,int reg,uint8_t *buf,uint8_t len);
+6 -6
View File
@@ -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); const TickType_t timeout_ticks = (timeout_ms == -1) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
return xSemaphoreTake(lvgl_mux, timeout_ticks) == pdTRUE; 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"); assert(lvgl_mux && "bsp_display_start must be called first");
xSemaphoreGive(lvgl_mux); 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; uint32_t task_delay_ms = EXAMPLE_LVGL_TASK_MAX_DELAY_MS;
for(;;) for(;;)
{ {
if (example_lvgl_lock(-1)) if (lvgl_port_lock(-1))
{ {
task_delay_ms = lv_timer_handler(); task_delay_ms = lv_timer_handler();
//Release the mutex //Release the mutex
example_lvgl_unlock(); lvgl_port_unlock();
} }
if (task_delay_ms > EXAMPLE_LVGL_TASK_MAX_DELAY_MS) if (task_delay_ms > EXAMPLE_LVGL_TASK_MAX_DELAY_MS)
{ {
@@ -260,9 +260,9 @@ void lvgl_port_init(void)
lvgl_mux = xSemaphoreCreateMutex(); lvgl_mux = xSemaphoreCreateMutex();
assert(lvgl_mux); assert(lvgl_mux);
xTaskCreatePinnedToCore(example_lvgl_port_task, "LVGL", 4000, NULL, 4, NULL,0); //运行于内核_0 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(); c64_ui_create();
example_lvgl_unlock(); lvgl_port_unlock();
} }
} }
+3
View File
@@ -1,5 +1,6 @@
#ifndef LVGL_PORT_H #ifndef LVGL_PORT_H
#define LVGL_PORT_H #define LVGL_PORT_H
#include <stdbool.h>
@@ -9,6 +10,8 @@ extern "C" {
void lvgl_port_init(void); void lvgl_port_init(void);
bool lvgl_port_lock(int timeout_ms);
void lvgl_port_unlock(void);
#ifdef __cplusplus #ifdef __cplusplus
+87 -2
View File
@@ -1,6 +1,91 @@
#include <Arduino.h> #include <Arduino.h>
#include "esp_sleep.h"
#include "i2c_bsp.h" #include "i2c_bsp.h"
#include "lvgl_port.h" #include "lvgl_port.h"
#include "c64_ui.h"
#include "lcd_bl_bsp/lcd_bl_pwm_bsp.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);
}