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
This commit is contained in:
Adolfo Reyna
2026-02-10 20:29:10 -05:00
parent b16211f148
commit 8cbb95b181
7 changed files with 384 additions and 7 deletions
+1
View File
@@ -113,6 +113,7 @@ target_include_directories(basic1 PRIVATE
target_link_libraries(basic1
hardware_spi
hardware_i2c
hardware_pwm
pico_multicore
m
)
+133
View File
@@ -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<LowLevelDisplayST7796*>(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<LowLevelDisplayEPaper*>(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<LowLevelDisplayST7796*>(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<LowLevelDisplayEPaper*>(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;
+5
View File
@@ -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; }
+18 -3
View File
@@ -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) {
+8
View File
@@ -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;
+132 -4
View File
@@ -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);
}
}
+87
View File
@@ -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