8.8 KiB
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 输出,并增加以下能力:
- PWM 频率固定为 20 kHz,避免肉眼可见闪烁,并降低进入可闻频段的风险。
- 使用 10 bit PWM 分辨率,内部占空比范围为 0~1023。
- 支持 0~100 的亮度百分比。
- 兼容高电平有效和低电平有效两种背光电路。
- 增加 Kconfig 配置,可以通过
sdkconfig设置开机默认亮度。 - 增加运行时亮度接口,为后续轻待机降亮度和唤醒恢复亮度提供基础能力。
- 使用独立互斥锁串行化运行时亮度更新,避免并发修改 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. 解决成果
- LCD 背光从开关控制升级为 0~100% PWM 亮度控制。
- 默认亮度可以通过
sdkconfig配置,不需要修改驱动源码。 - 默认值保持 100%,升级后现有设备的默认显示亮度不变。
- 已提供运行时接口,可以直接支持后续轻待机和唤醒状态切换。
- 高电平有效和低电平有效配置均得到支持。
- 使用 ESP-IDF 5.5.4 完整编译成功。
- 生成固件大小为
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 需要实机确认调光范围
软件编译已经通过,但仍需要在真实屏幕上确认:
- 20 kHz 下是否存在异常噪声。
- 低亮度时背光是否稳定。
- 10%、20%、30%、50% 等档位的视觉效果和实际电流。
- 背光接口所在硬件是否对高频 PWM 有额外限制。
6.4 对 LCD 刷新无影响
PWM只控制背光供电占空比,不改变 ST7789 的 SPI 时钟、图片刷新频率、颜色数据和显存内容。调整亮度不会中断当前表情动画。
6.5 对轻待机功能的影响
本次只提供背光控制基础能力,尚未实现5分钟无交互计时和自动降亮度。后续轻待机逻辑需要在业务状态层调用运行时接口,并处理以下状态:
- 无交互5分钟后降低亮度。
- 唤醒词、PTT按键或抽签开关触发时恢复亮度。
- OTA、配网、低电量提示等特殊状态是否覆盖待机亮度。
- 多个状态同时请求亮度时的优先级。
6.6 现有架构检查告警
项目架构检查仍会报告电池任务中存在 portMAX_DELAY 无界等待。该告警在本次改动之前已经存在,与背光 PWM 实现无关;固件完整编译不受影响。