From 8cbb95b181aad860845d5bfb1ad40b73b335fda2 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Tue, 10 Feb 2026 20:29:10 -0500 Subject: [PATCH] Add auto-sleep/wake functionality with timer-based dimming - Added PWM brightness control to ST7796 driver (0-100%) - Implemented hardware sleep mode for ST7796 (saves ~150mA) - Added sleep/wake functions preserving framebuffer and settings - Implemented timer-based inactivity detection (checks every 10s) - Auto-sleep after 5 minutes of no user input - Touch controller remains active during TFT sleep for instant wake - E-ink display also sleeps after timeout, requires re-init on wake - Added hardware_pwm library dependency to CMakeLists.txt - Brightness and sleep/wake methods exposed through display abstraction layer --- CMakeLists.txt | 1 + basic1.cpp | 133 ++++++++++++++++++++++++++ display/low_level_display.h | 5 + display/low_level_display_st7796.cpp | 21 ++++- display/low_level_display_st7796.h | 8 ++ lib/st7796/st7796.c | 136 ++++++++++++++++++++++++++- lib/st7796/st7796.h | 87 +++++++++++++++++ 7 files changed, 384 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 895ed29..92d30f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,6 +113,7 @@ target_include_directories(basic1 PRIVATE target_link_libraries(basic1 hardware_spi hardware_i2c + hardware_pwm pico_multicore m ) diff --git a/basic1.cpp b/basic1.cpp index 7352037..d74bea6 100644 --- a/basic1.cpp +++ b/basic1.cpp @@ -49,6 +49,7 @@ extern "C" { #include "display/low_level_render.h" #include "display/low_level_display.h" #include "display/low_level_display_epaper.h" +#include "display/low_level_display_st7796.h" #include "display/low_level_touch.h" #include "input_manager.h" #include "game.h" @@ -151,6 +152,110 @@ struct GameConfig { bool debug_verbose; // Print debug messages }; +// ============================================================================ +// DISPLAY DIMMING CONFIGURATION +// ============================================================================ + +// Display dimming settings +#define DIM_TIMEOUT_MS (5 * 60 * 1000) // 5 minutes in milliseconds +#define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds +#define DIM_BRIGHTNESS 5 // Dimmed brightness level (0-100) +#define NORMAL_BRIGHTNESS 100 // Normal brightness level (0-100) + +// Display dimming state +static uint32_t last_interaction_time = 0; // Last time user interacted +static bool is_dimmed = false; // Current dimming state +static uint8_t saved_brightness = NORMAL_BRIGHTNESS; // Brightness before dimming +static volatile bool dim_check_flag = false; // Flag set by timer to check dimming +static LowLevelDisplay* global_display = nullptr; // Global display pointer for timer callback + +/** + * @brief Update last interaction time and restore display if dimmed + * + * Call this whenever the user interacts with the device (touch, button press) + * to reset the dimming timer and restore display if it was dimmed/sleeping. + * + * For TFT displays: Restores brightness level + * For e-ink displays: Wakes display from sleep and refreshes screen + * + * @param display Pointer to display interface + */ +static inline void record_user_interaction(LowLevelDisplay* display) { + last_interaction_time = to_ms_since_boot(get_absolute_time()); + + // If display was dimmed, restore it based on display type + if (is_dimmed) { + if (display->get_type() == DISPLAY_TYPE_ST7796) { + // TFT: Wake from sleep mode (touch was always active) + LowLevelDisplayST7796* tft = static_cast(display); + tft->wake(); + printf("TFT display woken from sleep\n"); + } else if (display->get_type() == DISPLAY_TYPE_EPAPER) { + // E-ink: Wake from sleep and re-initialize + printf("Waking e-paper display from sleep...\n"); + LowLevelDisplayEPaper* epaper = static_cast(display); + epaper->init(); // Re-initialize after sleep + printf("E-paper display ready\n"); + } + is_dimmed = false; + } +} + +/** + * @brief Timer callback to periodically check dimming status + * + * This alarm callback fires every DIM_CHECK_INTERVAL_MS milliseconds + * to wake the CPU from __wfi() and check if dimming should occur. + * Running in interrupt context, so just sets a flag for main loop. + * + * @param id Alarm ID (unused) + * @param user_data User data pointer (unused) + * @return Next alarm time (relative to current time) + */ +static int64_t dim_check_alarm_callback(alarm_id_t id, void *user_data) { + // Set flag to check dimming in main loop + dim_check_flag = true; + + // Return interval in microseconds for next alarm + // Negative value means schedule relative to now + return -(DIM_CHECK_INTERVAL_MS * 1000); +} + +/** + * @brief Check if display should be dimmed and apply dimming if needed + * + * Checks if the timeout has elapsed since last interaction and dims/sleeps + * the display based on its type: + * - TFT displays (ST7796): Reduces brightness to minimum + * - E-ink displays: Puts display to sleep mode (turns off) + * + * @param display Pointer to display interface + */ +static inline void check_and_apply_dimming(LowLevelDisplay* display) { + uint32_t current_time = to_ms_since_boot(get_absolute_time()); + uint32_t elapsed = current_time - last_interaction_time; + + // Check if timeout has elapsed and display is not yet dimmed + if (!is_dimmed && elapsed >= DIM_TIMEOUT_MS) { + if (display->get_type() == DISPLAY_TYPE_ST7796) { + // TFT: Put to sleep (saves power, touch stays active) + saved_brightness = display->get_brightness(); + LowLevelDisplayST7796* tft = static_cast(display); + tft->sleep(); + printf("TFT display put to sleep after %d seconds of inactivity\n", + DIM_TIMEOUT_MS / 1000); + printf("Touch controller remains active - touch to wake\n"); + } else if (display->get_type() == DISPLAY_TYPE_EPAPER) { + // E-ink: Put to sleep (turns off display) + LowLevelDisplayEPaper* epaper = static_cast(display); + epaper->sleep(); + printf("E-paper display put to sleep after %d seconds of inactivity\n", + DIM_TIMEOUT_MS / 1000); + } + is_dimmed = true; + } +} + // ============================================================================ // INTERRUPT HANDLERS (Keep these minimal!) // ============================================================================ @@ -448,6 +553,24 @@ int main() uint32_t last_touch_time = 0; bool pending_refresh = false; // Track if we have a pending refresh + // Initialize last interaction time to current time + last_interaction_time = to_ms_since_boot(get_absolute_time()); + global_display = display; + + // Set up repeating alarm to periodically check dimming status + // This wakes the CPU from __wfi() every DIM_CHECK_INTERVAL_MS + add_alarm_in_ms(DIM_CHECK_INTERVAL_MS, dim_check_alarm_callback, nullptr, true); + + if (display->get_type() == DISPLAY_TYPE_ST7796) { + printf("Auto-sleep enabled: TFT will sleep after %d minutes of inactivity\n", + DIM_TIMEOUT_MS / 60000); + printf("Touch controller remains active to wake display\n"); + } else if (display->get_type() == DISPLAY_TYPE_EPAPER) { + printf("Auto-sleep enabled: E-paper will sleep after %d minutes of inactivity\n", + DIM_TIMEOUT_MS / 60000); + } + printf("Dimming check timer set to %d seconds\n", DIM_CHECK_INTERVAL_MS / 1000); + printf("\nEntering reactive game loop (Core 0 - input & logic)\n"); printf("Display refreshes handled by Core 1\n\n"); @@ -476,6 +599,9 @@ int main() // 3. Process input based on current state if (input.valid) { + // Record user interaction for dimming timer + record_user_interaction(display); + // if debugging enabled, print input event if (config.debug_verbose) { printf("Input Event: type=%d, x=%d, y=%d, gesture=0x%02X, button=%d, pressure=%d\n", @@ -575,6 +701,13 @@ int main() // Core 0 continues immediately, Core 1 handles the refresh } + + // 5. Check if display should be dimmed due to inactivity + // This flag is set by timer alarm every DIM_CHECK_INTERVAL_MS + if (dim_check_flag) { + dim_check_flag = false; + check_and_apply_dimming(display); + } } return 0; diff --git a/display/low_level_display.h b/display/low_level_display.h index 2e1e6cb..7f417c5 100644 --- a/display/low_level_display.h +++ b/display/low_level_display.h @@ -32,6 +32,11 @@ public: // Optional: Backlight control (if supported) virtual void set_backlight(bool on) { (void)on; } + // Optional: Brightness control (if supported) + // brightness: 0-100 (percentage), 0=off, 100=full brightness + virtual void set_brightness(uint8_t brightness) { (void)brightness; } + virtual uint8_t get_brightness() const { return 100; } // Default to full brightness + // Optional: Orientation control (not commonly needed for bitmap displays) virtual void set_rotation(uint8_t rotation) { (void)rotation; } diff --git a/display/low_level_display_st7796.cpp b/display/low_level_display_st7796.cpp index f318e14..e473605 100644 --- a/display/low_level_display_st7796.cpp +++ b/display/low_level_display_st7796.cpp @@ -69,9 +69,24 @@ void LowLevelDisplayST7796::refresh() { } void LowLevelDisplayST7796::set_backlight(bool on) { - // ST7796 driver doesn't have backlight control yet - // TODO: Add GPIO control for backlight pin - (void)on; + // Use brightness control: on = 100%, off = 0% + st7796_set_brightness(on ? 100 : 0); +} + +void LowLevelDisplayST7796::set_brightness(uint8_t brightness) { + st7796_set_brightness(brightness); +} + +uint8_t LowLevelDisplayST7796::get_brightness() const { + return st7796_get_brightness(); +} + +void LowLevelDisplayST7796::sleep() { + st7796_sleep(); +} + +void LowLevelDisplayST7796::wake() { + st7796_wake(); } void LowLevelDisplayST7796::set_rotation(uint8_t rotation) { diff --git a/display/low_level_display_st7796.h b/display/low_level_display_st7796.h index cd8c394..a7098f5 100644 --- a/display/low_level_display_st7796.h +++ b/display/low_level_display_st7796.h @@ -32,6 +32,14 @@ public: // Backlight control void set_backlight(bool on) override; + + // Brightness control (0-100) + void set_brightness(uint8_t brightness) override; + uint8_t get_brightness() const override; + + // Power management + void sleep(); // Put display to sleep (low power, touch still active) + void wake(); // Wake display from sleep // Orientation control void set_rotation(uint8_t rotation) override; diff --git a/lib/st7796/st7796.c b/lib/st7796/st7796.c index ccbedb9..13c052e 100644 --- a/lib/st7796/st7796.c +++ b/lib/st7796/st7796.c @@ -58,6 +58,7 @@ #include "hardware/gpio.h" #include "hardware/spi.h" +#include "hardware/pwm.h" #include "pico/binary_info.h" #include "pico/stdlib.h" @@ -118,6 +119,9 @@ static uint16_t width; // Display width in pixels (e.g., 480) static uint16_t height; // Display height in pixels (e.g., 320) static uint16_t x_offset; // X offset for display alignment (currently 0) static uint16_t y_offset; // Y offset for display alignment (currently 0) +static uint8_t current_brightness = 100; // Current brightness level (0-100) +static uint pwm_slice; // PWM slice number for backlight control +static uint pwm_channel; // PWM channel number for backlight control /** * @brief Activate chip select (pull CS LOW) @@ -359,12 +363,27 @@ void st7796_init(const struct st7796_config *c, uint16_t w, uint16_t h) { gpio_init(config->gpio_rst); gpio_set_dir(config->gpio_rst, GPIO_OUT); - // Initialize backlight pin + // Initialize backlight pin with PWM for brightness control // Most TFT displays have LED backlights that need power if (config->gpio_bl >= 0) { - gpio_init(config->gpio_bl); - gpio_set_dir(config->gpio_bl, GPIO_OUT); - gpio_put(config->gpio_bl, 1); // Turn on backlight immediately + // Configure GPIO for PWM function + gpio_set_function(config->gpio_bl, GPIO_FUNC_PWM); + + // Find PWM slice and channel for this GPIO + pwm_slice = pwm_gpio_to_slice_num(config->gpio_bl); + pwm_channel = pwm_gpio_to_channel(config->gpio_bl); + + // Configure PWM + // PWM frequency = clock_freq / (wrap + 1) + // We want ~1 kHz to avoid flicker: 125 MHz / 125000 = 1000 Hz + pwm_set_wrap(pwm_slice, 65535); // 16-bit resolution + pwm_set_clkdiv(pwm_slice, 1.907f); // 125 MHz / 1.907 / 65536 ≈ 1 kHz + + // Start at full brightness + pwm_set_chan_level(pwm_slice, pwm_channel, 65535); + pwm_set_enabled(pwm_slice, true); + + current_brightness = 100; } // Hardware reset sequence @@ -934,3 +953,112 @@ void st7796_fill_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, st7796_fill_rect(a, y, b - a + 1, 1, color); } } + +/** + * @brief Set display brightness using PWM on backlight pin + * + * Controls the backlight LED brightness by adjusting the PWM duty cycle. + * The brightness parameter is a percentage that's converted to the + * appropriate PWM level (0-65535 for 16-bit PWM). + * + * Implementation: + * - brightness = 0: PWM level = 0 (backlight off) + * - brightness = 100: PWM level = 65535 (backlight fully on) + * - brightness = 50: PWM level = 32767 (50% duty cycle) + * + * The conversion formula: PWM_level = (brightness * 65535) / 100 + * + * Human perception of brightness is logarithmic, but we use linear + * PWM for simplicity. For perceived linear brightness, a gamma + * correction curve could be applied. + * + * @param brightness Brightness level (0-100 percent) + */ +void st7796_set_brightness(uint8_t brightness) { + // Clamp brightness to valid range + if (brightness > 100) { + brightness = 100; + } + + // Store current brightness + current_brightness = brightness; + + // Convert percentage to 16-bit PWM level (0-65535) + // Using 32-bit intermediate to avoid overflow + uint32_t pwm_level = ((uint32_t)brightness * 65535) / 100; + + // Set PWM duty cycle + if (config->gpio_bl >= 0) { + pwm_set_chan_level(pwm_slice, pwm_channel, (uint16_t)pwm_level); + } +} + +/** + * @brief Get current display brightness level + * + * Returns the brightness level that was last set via st7796_set_brightness() + * or the default value (100) if brightness was never changed. + * + * @return Current brightness level (0-100 percent) + */ +uint8_t st7796_get_brightness(void) { + return current_brightness; +} + +/** + * @brief Put display into sleep mode (low power) + * + * Enters deep sleep mode to minimize power consumption while maintaining + * initialization state. The framebuffer contents and all settings are + * preserved, so waking up is fast. + * + * Sleep sequence: + * 1. Turn off display (DISPOFF) - stops showing framebuffer + * 2. Enter sleep mode (SLPIN) - stops oscillator, minimal power + * 3. Turn off backlight - set PWM to 0 + * + * Total power savings: Display draws ~10μA in sleep vs ~150mA active + * Touch controller on separate I2C bus remains fully functional + */ +void st7796_sleep(void) { + // Turn off display output first + write_command(ST7796_DISPOFF); + sleep_ms(10); + + // Enter sleep mode (stops internal oscillator) + write_command(ST7796_SLPIN); + sleep_ms(120); // Wait for sleep mode to take effect (spec: 120ms) + + // Turn off backlight to save power + if (config->gpio_bl >= 0) { + pwm_set_chan_level(pwm_slice, pwm_channel, 0); + } +} + +/** + * @brief Wake display from sleep mode + * + * Exits sleep mode and restores display to full operation. + * All framebuffer contents and settings are preserved. + * + * Wake sequence: + * 1. Exit sleep mode (SLPOUT) - restarts oscillator + * 2. Wait for oscillator to stabilize (120ms) + * 3. Turn on display (DISPON) - starts showing framebuffer + * 4. Restore backlight to previous brightness + */ +void st7796_wake(void) { + // Exit sleep mode (restart oscillator) + write_command(ST7796_SLPOUT); + sleep_ms(120); // Wait for oscillator to stabilize (spec: 120ms) + + // Turn on display output + write_command(ST7796_DISPON); + sleep_ms(10); + + // Restore backlight to previous brightness level + if (config->gpio_bl >= 0) { + uint32_t pwm_level = ((uint32_t)current_brightness * 65535) / 100; + pwm_set_chan_level(pwm_slice, pwm_channel, (uint16_t)pwm_level); + } +} diff --git a/lib/st7796/st7796.h b/lib/st7796/st7796.h index 6279ba8..6dd286c 100644 --- a/lib/st7796/st7796.h +++ b/lib/st7796/st7796.h @@ -357,6 +357,93 @@ void st7796_draw_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, ui */ void st7796_fill_triangle(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); +/** + * @brief Set display brightness using PWM on backlight pin + * + * Controls the backlight LED brightness using hardware PWM. The brightness + * is specified as a percentage (0-100), where: + * - 0 = Backlight completely off (minimum brightness) + * - 100 = Backlight fully on (maximum brightness) + * - Values in between = Proportional PWM duty cycle + * + * PWM Configuration: + * - Frequency: 1000 Hz (1 kHz) - high enough to avoid flicker + * - Resolution: 16-bit (0-65535 range internally) + * - Smooth transitions without visible flickering + * + * Use cases: + * - Power saving: Reduce brightness when idle + * - Auto-dimming: Lower brightness after timeout + * - Manual control: User brightness preferences + * - Night mode: Very low brightness for dark environments + * + * @param brightness Brightness level (0-100 percent) + * 0 = off, 100 = full brightness + * + * Performance: Instant - PWM is handled by hardware + * + * Note: Must be called after st7796_init() which configures the backlight pin + */ +void st7796_set_brightness(uint8_t brightness); + +/** + * @brief Get current display brightness level + * + * Returns the currently configured brightness level as a percentage (0-100). + * This reflects the last value set by st7796_set_brightness() or the + * default value (100) if brightness was never explicitly set. + * + * @return Current brightness level (0-100 percent) + */ +uint8_t st7796_get_brightness(void); + +/** + * @brief Put display into sleep mode (low power) + * + * Enters sleep mode to save power while keeping the controller initialized. + * In sleep mode: + * - Display is turned off (blank screen) + * - Internal oscillator is stopped + * - Framebuffer contents are preserved + * - Backlight is turned off (PWM set to 0) + * - Power consumption is minimized (~10μA typical) + * + * Touch controller remains active and functional since it's on a separate + * I2C bus. Touch interrupts can wake the system from sleep. + * + * Use st7796_wake() to exit sleep mode and restore display. + * + * Commands sent: + * - DISPOFF (0x28): Turn off display + * - SLPIN (0x10): Enter sleep mode + * - Backlight PWM set to 0 + * + * Performance: ~120ms to enter sleep mode + */ +void st7796_sleep(void); + +/** + * @brief Wake display from sleep mode + * + * Exits sleep mode and restores display to normal operation. + * After waking: + * - Internal oscillator restarts + * - Display controller becomes active + * - Framebuffer contents are preserved + * - Display is turned on + * - Backlight is restored to previous brightness + * + * Commands sent: + * - SLPOUT (0x11): Exit sleep mode + * - DISPON (0x29): Turn on display + * - Backlight PWM restored to saved level + * + * Performance: ~120ms to fully wake up + * + * Note: Must call st7796_init() before first use of sleep/wake + */ +void st7796_wake(void); + #ifdef __cplusplus } #endif