# LCD 背光 PWM 亮度控制实现说明 ## 1. 问题背景与原因 原有 LCD 背光使用 GPIO7 作为普通数字输出,初始化时仅根据背光有效电平写入高电平或低电平: ```c 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. 配置方式 新增配置项: ```ini CONFIG_TQ_SCREEN_BACKLIGHT_BRIGHTNESS_PERCENT=100 ``` 取值范围: | 配置值 | 效果 | | --- | --- | | `0` | 背光关闭 | | `30` | 约 30% PWM 占空比 | | `50` | 约 50% PWM 占空比 | | `100` | 背光全亮 | Kconfig 定义如下: ```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. ``` 可以通过以下菜单修改: ```text Example Configuration -> TQ Screen -> Default backlight brightness (percent) ``` 也可以直接修改 `sdkconfig` 中的配置值。 ## 4. 具体代码 ### 4.1 PWM 参数 ```c #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 占空比 ```c 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 初始化 ```c 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 初始化: ```c 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 运行时亮度接口 平台公共接口: ```c esp_err_t platform_lcd_set_backlight_brightness(uint8_t brightness_percent); ``` 底层实现: ```c 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; } ``` 后续轻待机可以调用: ```c platform_lcd_set_backlight_brightness(35); ``` 唤醒后恢复默认亮度可以调用: ```c 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% 空余空间。 对应代码提交: ```text 988dc92 feat(platform): add PWM backlight brightness control ``` ## 6. 后续影响与注意事项 ### 6.1 LEDC 资源占用 当前实现固定使用: ```text 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 实现无关;固件完整编译不受影响。