Files
lucky-cat/docs/backlight-pwm-brightness-control.zh-CN.md
T

8.8 KiB
Raw Blame History

LCD 背光 PWM 亮度控制实现说明

1. 问题背景与原因

原有 LCD 背光使用 GPIO7 作为普通数字输出,初始化时仅根据背光有效电平写入高电平或低电平:

gpio_set_level(CONFIG_TQ_SCREEN_BACKLIGHT_GPIO,
               CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH ? 1 : 0);

这种实现只能控制背光完全开启或完全关闭,不能设置中间亮度。设备进入轻待机后,即使减少 LCD 图片刷新,背光仍保持全亮,屏幕主要功耗无法有效降低。

项目配置中原先只有背光 GPIO 和有效电平配置,也没有可以通过 sdkconfig 设置默认亮度的选项。

2. 解决方式

本次将 GPIO7 背光控制从普通 GPIO 输出改为 ESP32-S3 LEDC PWM 输出,并增加以下能力:

  1. PWM 频率固定为 20 kHz,避免肉眼可见闪烁,并降低进入可闻频段的风险。
  2. 使用 10 bit PWM 分辨率,内部占空比范围为 0~1023。
  3. 支持 0~100 的亮度百分比。
  4. 兼容高电平有效和低电平有效两种背光电路。
  5. 增加 Kconfig 配置,可以通过 sdkconfig 设置开机默认亮度。
  6. 增加运行时亮度接口,为后续轻待机降亮度和唤醒恢复亮度提供基础能力。
  7. 使用独立互斥锁串行化运行时亮度更新,避免并发修改 LEDC 占空比。

3. 配置方式

新增配置项:

CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT=100

取值范围:

配置值 效果
0 背光关闭
30 约 30% PWM 占空比
50 约 50% PWM 占空比
100 背光全亮

Kconfig 定义如下:

config TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT
    int "Default backlight brightness (percent)"
    default 100
    range 0 100
    depends on TQ_SCREEN_ENABLE
    help
        Initial LCD backlight PWM duty after screen initialization.
        Set 0 to turn the backlight off or 100 for full brightness.

可以通过以下菜单修改:

Example Configuration
  -> TQ Screen
    -> Default backlight brightness (percent)

也可以直接修改 sdkconfig 中的配置值。

4. 具体代码

4.1 PWM 参数

#define LCD_DIRECT_BACKLIGHT_PWM_FREQUENCY_HZ 20000U
#define LCD_DIRECT_BACKLIGHT_PWM_DUTY_RESOLUTION LEDC_TIMER_10_BIT
#define LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX ((1U << 10U) - 1U)
#define LCD_DIRECT_BACKLIGHT_PWM_TIMER LEDC_TIMER_0
#define LCD_DIRECT_BACKLIGHT_PWM_CHANNEL LEDC_CHANNEL_0
#define LCD_DIRECT_BACKLIGHT_LOCK_TIMEOUT_MS 100U

4.2 百分比转换为 PWM 占空比

static uint32_t lcd_direct_backlight_duty(uint8_t brightness_percent)
{
    uint32_t duty =
        ((uint32_t)brightness_percent * LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX + 50U) / 100U;
#if !CONFIG_TQ_SCREEN_BACKLIGHT_ACTIVE_HIGH
    duty = LCD_DIRECT_BACKLIGHT_PWM_DUTY_MAX - duty;
#endif
    return duty;
}

计算中加入 50 后再除以 100,用于百分比转换时四舍五入。低电平有效的硬件会自动反转占空比。

4.3 LEDC PWM 初始化

static esp_err_t lcd_direct_backlight_init(void)
{
    const ledc_timer_config_t timer_config = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .duty_resolution = LCD_DIRECT_BACKLIGHT_PWM_DUTY_RESOLUTION,
        .timer_num = LCD_DIRECT_BACKLIGHT_PWM_TIMER,
        .freq_hz = LCD_DIRECT_BACKLIGHT_PWM_FREQUENCY_HZ,
        .clk_cfg = LEDC_AUTO_CLK,
    };
    ESP_RETURN_ON_ERROR(ledc_timer_config(&timer_config),
                        TAG,
                        "backlight PWM timer config failed");

    const ledc_channel_config_t channel_config = {
        .gpio_num = CONFIG_TQ_SCREEN_BACKLIGHT_GPIO,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LCD_DIRECT_BACKLIGHT_PWM_CHANNEL,
        .intr_type = LEDC_INTR_DISABLE,
        .timer_sel = LCD_DIRECT_BACKLIGHT_PWM_TIMER,
        .duty = lcd_direct_backlight_duty(
            CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT),
        .hpoint = 0,
    };
    ESP_RETURN_ON_ERROR(ledc_channel_config(&channel_config),
                        TAG,
                        "backlight PWM channel config failed");
    return ESP_OK;
}

LCD GPIO 初始化不再把背光引脚注册为普通 GPIO 输出,而是在 DC 和复位引脚初始化后调用 PWM 初始化:

static esp_err_t lcd_direct_gpio_init(void)
{
    uint64_t pin_mask = (1ULL << CONFIG_TQ_SCREEN_DC_GPIO);
    if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) {
        pin_mask |= (1ULL << CONFIG_TQ_SCREEN_RESET_PIN);
    }

    gpio_config_t gpio_config_data = {
        .pin_bit_mask = pin_mask,
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_DISABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    ESP_RETURN_ON_ERROR(gpio_config(&gpio_config_data), TAG, "gpio config failed");

    gpio_set_level(CONFIG_TQ_SCREEN_DC_GPIO, 0);
    if (CONFIG_TQ_SCREEN_RESET_PIN >= 0) {
        gpio_set_level(CONFIG_TQ_SCREEN_RESET_PIN, 1);
    }
    return lcd_direct_backlight_init();
}

4.4 运行时亮度接口

平台公共接口:

esp_err_t platform_lcd_set_backlight_brightness(uint8_t brightness_percent);

底层实现:

esp_err_t lcd_direct_set_backlight_brightness(uint8_t brightness_percent)
{
    ESP_RETURN_ON_FALSE(brightness_percent <= 100U,
                        ESP_ERR_INVALID_ARG,
                        TAG,
                        "backlight brightness must be between 0 and 100 percent");
    ESP_RETURN_ON_FALSE(s_initialized && s_backlight_lock != NULL,
                        ESP_ERR_INVALID_STATE,
                        TAG,
                        "LCD backlight is not initialized");
    ESP_RETURN_ON_FALSE(
        xSemaphoreTake(s_backlight_lock,
                       pdMS_TO_TICKS(LCD_DIRECT_BACKLIGHT_LOCK_TIMEOUT_MS)) == pdTRUE,
        ESP_ERR_TIMEOUT,
        TAG,
        "backlight lock timed out");

    esp_err_t err = ledc_set_duty(
        LEDC_LOW_SPEED_MODE,
        LCD_DIRECT_BACKLIGHT_PWM_CHANNEL,
        lcd_direct_backlight_duty(brightness_percent));
    if (err == ESP_OK) {
        err = ledc_update_duty(LEDC_LOW_SPEED_MODE,
                               LCD_DIRECT_BACKLIGHT_PWM_CHANNEL);
    }
    xSemaphoreGive(s_backlight_lock);
    return err;
}

后续轻待机可以调用:

platform_lcd_set_backlight_brightness(35);

唤醒后恢复默认亮度可以调用:

platform_lcd_set_backlight_brightness(
    CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT);

5. 解决成果

  1. LCD 背光从开关控制升级为 0~100% PWM 亮度控制。
  2. 默认亮度可以通过 sdkconfig 配置,不需要修改驱动源码。
  3. 默认值保持 100%,升级后现有设备的默认显示亮度不变。
  4. 已提供运行时接口,可以直接支持后续轻待机和唤醒状态切换。
  5. 高电平有效和低电平有效配置均得到支持。
  6. 使用 ESP-IDF 5.5.4 完整编译成功。
  7. 生成固件大小为 0x211bf0 字节,最小应用分区仍有约 41% 空余空间。

对应代码提交:

988dc92 feat(platform): add PWM backlight brightness control

6. 后续影响与注意事项

6.1 LEDC 资源占用

当前实现固定使用:

LEDC low-speed timer 0
LEDC channel 0

项目后续增加蜂鸣器、其他 LED 调光或舵机 PWM 时,不能重复占用该定时器和通道,或者需要增加统一的 LEDC 资源分配策略。

6.2 亮度百分比不是视觉线性

当前配置值与 PWM 占空比线性对应,但人眼对亮度的感知不是线性的。例如 50% 占空比不一定看起来正好是全亮的一半。后续如果需要更自然的渐亮、渐暗效果,可以增加 Gamma 校正表。

6.3 需要实机确认调光范围

软件编译已经通过,但仍需要在真实屏幕上确认:

  1. 20 kHz 下是否存在异常噪声。
  2. 低亮度时背光是否稳定。
  3. 10%、20%、30%、50% 等档位的视觉效果和实际电流。
  4. 背光接口所在硬件是否对高频 PWM 有额外限制。

6.4 对 LCD 刷新无影响

PWM只控制背光供电占空比,不改变 ST7789 的 SPI 时钟、图片刷新频率、颜色数据和显存内容。调整亮度不会中断当前表情动画。

6.5 对轻待机功能的影响

本次只提供背光控制基础能力,尚未实现5分钟无交互计时和自动降亮度。后续轻待机逻辑需要在业务状态层调用运行时接口,并处理以下状态:

  1. 无交互5分钟后降低亮度。
  2. 唤醒词、PTT按键或抽签开关触发时恢复亮度。
  3. OTA、配网、低电量提示等特殊状态是否覆盖待机亮度。
  4. 多个状态同时请求亮度时的优先级。

6.6 现有架构检查告警

项目架构检查仍会报告电池任务中存在 portMAX_DELAY 无界等待。该告警在本次改动之前已经存在,与背光 PWM 实现无关;固件完整编译不受影响。