彻底解决Linux下LED的背光闪烁

前言

因为家里的那台笔记本太重了,前段时间换了台Thinkpad X250方便外出携带使用。X250的屏幕分辨率是1080P,但不知道为什么,每次使用一段时间后眼睛都非常难受,还伴有轻微的头晕。但是接上外接显示器使用时就很正常,所以我一直认为是显示器太小或者分辨率太高的原因。
一次偶然的机会对着屏幕拍照,发现屏幕上竟然有明暗相间的条纹!还以为是显示器坏了,Google了一下资料后才发现这是普遍的LED背光闪烁(LED backlight flicker)。
LED flicker

上图:在低背光频率下,铅笔快速晃动可以看到明显的残影

目前市场上主要的背光技术主要是LED背光和CCFL背光。CCFL即冷阴极荧光灯,类似于家里使用的灯管,因为其具有的荧光图层可以使屏幕不闪烁,成为了大屁股显示器的绝佳代替品。但同事也有功耗较高、易老化导致屏幕变黄等缺点,在LED背光技术出来之后,原来越少厂家使用这种技术了。
LED作为背光源具有超高亮度、寿命长、功耗高的特点,已经成为主流的背光技术,目前市面上的大部分液晶显示器都是采用了这种技术。而LED的主流亮度调节技术采用的则是低成本的PWM调光。
什么是PWM调光呢?拿家里的电灯说明一下,把电灯的开关关掉,家里的亮度是0%; 打开0.5秒,关闭0.5秒,不停地重复这个操作,我们认为亮度是50%; 打开0.6秒,关闭0.4秒,亮度是60%...打开之后不关闭,亮度就是100%了。PWM也是这个原理,通过调整LED背光的占空比来输出不同的亮度。当然调整的屏幕比我们手动的快多了,据说最低是200Hz。
我们家里的冷光灯频率一般是60Hz,肉眼已经感觉不到闪烁了,按理说200Hz已经超越眼睛感受的极限了,应该不会感到难受才对。但是LED有个非常糟糕的优点:极快速的开关响应。也就是说,在关闭状态下瞬间关闭,在开启状态下瞬间开启,这就导致LED背光给肉眼的感受还是闪烁的。而冷光灯上面的荧光涂层具有光线残留的特点,在关闭的情况下还能保留一段时间,所以肉眼感受起来屏幕一直是亮的,不会有闪烁感。
要解决LED背光闪烁的最后途径就是加快PWM频率,200Hz太闪,2000Hz就没什么感觉了。但是加快频率之后有导致了另外一个问题:亮度增加!相对于传统的CCFL背光来说,LED的亮度简直会亮瞎眼睛!
如果有办法在增加频率的情况下保持低亮度就好了!

解决LED背光闪烁

原理

总结一下就是两点:增加频率,降低亮度。因为X250用的是Intel的显卡,有个寄存器(0xc8254)可以调节PWM的频率。安装intel_gpu_tools之后就可以读写这个寄存器了。这里(Eliminate LED screen flicker with Intel i915),比如我就设置成1500HZ,闪烁问题立马解决!

sudo intel_reg write 0xC8254 0x035400b4

LED high PWM freq

上图:在高背光频率下,铅笔快速晃动的图像比较平滑,无残影

增加频率虽然解决了闪烁,但这个亮度却足以亮瞎眼,所以还要想办法降低亮度。我一度认为降低亮度需要通过硬件来降低电压控制,因为通过键盘上的背光按键即使调到最小值亮度还是非常高!后来在 Archlinux上看到亮度其实是可以比按键调的更小的(后来知道系统限制按键最小只能调到48,可以通过udev规则控制),试了下写入1:

sudo tee /sys/class/backlight/intel_backlight/brightness <<< 1

奇迹出现了,在保持高PWM屏幕的情况下,亮度降到了一个可用(准确的说应该是眼睛比较舒服)的级别!

自动化脚本

既然已经实现了调高LED背光屏幕,并且保持低亮度,我么还想要一个脚本来自动设置,这样就可以系统的自动背光调节和按键背光控制了。不然LED的亮度是定死的,调整非常不方便。
经过一段时间的折腾,克服了好几个大坑之后,终于完成了一个初级版本,可以自动设置PWM频率、恢复亮度和增加、降低亮度等功能。点此下载

#!/bin/sh
# @Author: lance
# @Copyright:   www.shuyz.com
# @Date:   2015-11-07 14:48:31
# @Last Modified by:   lance
# @Last Modified time: 2015-11-07 21:04:21


# Configure a high PWM freq at which there is no flicker
# Calculate the freq here: http://devbraindom.blogspot.com/2013/03/eliminate-led-screen-flicker-with-intel.html
# In the example below the freq is set to 1500Hz
PWM_REG=0xc8254                 # address of the reg to configure freq
PWM_DFT='freq 852'              # the default freq pattern from read command
PWM_DFT_VAL=0x035400b4          # the default reg value of freq
PWM_VAL=0x7d007d                # reg value to provide the freq
XB_VAL_FILE=/var/lib/xbacklight # file to remember last backlight value

XBMAX=80    # max brightness
XBMIN=0     # min brightness, do not set it to 0 if you are not sure about the result
XINT=5      # initical brightness, DO NEVER SET THIS TO 0!
XSTEP=2     # step
XBRIGHTNESS=/sys/class/backlight/intel_backlight/brightness

INTEL_REG=/usr/bin/intel_reg
BC=/usr/bin/bc
AWK=/usr/bin/awk
SUDO=/usr/bin/sudo

# The backlight may be set to max on startup after change freq
# we use systemd-backlight to do the trick
SYSTEM_BACKLIGHT=/usr/lib/systemd/systemd-backlight
XBNAME=backlight:intel_backlight

# This function has return value, do not add other echos
init_freq()
{
    cvstr=$($SUDO $INTEL_REG read $PWM_REG)

    if [[ $cvstr == *$PWM_DFT* ]]; then
        $SUDO $INTEL_REG write $PWM_REG $PWM_VAL
        echo 1
    else
        echo 0
    fi
}

reset_freq()
{
    echo 'reset freq to default' $PWM_DFT_VAL
    $SUDO $INTEL_REG write $PWM_REG $PWM_DFT_VAL
}

get_freq()
{
    cvstr=$($SUDO $INTEL_REG read $PWM_REG)
    echo 'current backlight PWM freq is ' $cvstr
}

set_xbacklight()
{
    xbval=$1
    echo 'attempt to set backlight to ' $xbval

    # make sure it's digits
    if ! [[ $xbval =~ ^[0-9.]+$ ]] ; then
        xbval=$XBMIN
    fi

    # remove the dots
    xbval=$(printf "%.0f\n" $xbval)

    if [ $xbval -gt $XBMAX ]; then
        xbval=$XBMAX
    fi

    if [ $xbval -lt $XBMIN ]; then
        xbval=$XBMIN
    fi

    init_freq

    echo 'backlight value is set to ' $xbval
    $SUDO tee $XBRIGHTNESS <<< $xbval
    $SUDO sh -c "echo $xbval > $XB_VAL_FILE; chmod 777 $XB_VAL_FILE" # This script will be called by both root and normal users
}

init_xbacklight()
{
    intval=$XINT

    if [ -f $XB_VAL_FILE ]; then
        intval=$(cat $XB_VAL_FILE)
    fi

    chg=$(init_freq)

    # only init if freq changes
    if ! [[ $chg -eq 0 ]]; then
        echo 'freq changed, restore backlight.'

        # fix max backlight when change freq
        # use systemd-backlight to set it to min then restore our value
        if [ -f $SYSTEM_BACKLIGHT ]; then
            # set to a small value fo systemd-backlight will remember it
            set_xbacklight $XINT

            $SUDO $SYSTEM_BACKLIGHT save $XBNAME
            $SUDO $SYSTEM_BACKLIGHT load $XBNAME
        fi

        set_xbacklight $intval
    else
        if ! [[ $# -eq 0 ]]; then
            echo 'set brightness to write init value.'
            set_xbacklight $XINT
        else
            # auto fix max brightness
            cval=$(cat $XBRIGHTNESS)
            if [[ $cval -gt $XBMAX ]]; then
                echo 'current brightness value too high, reset'
                set_xbacklight $intval
            else
                echo 'nothing changed.' 
            fi
        fi
    fi
}

get_backlight()
{
    cval=$(cat $XBRIGHTNESS)
    echo 'current backlight value is ' $cval

    get_freq    
}

inc_xbacklight()
{
    cval=$(cat $XBRIGHTNESS)
    cval=$(echo $cval '+' $XSTEP '+0.1' | $BC)

    echo 'trying to increase backlight to ' $cval
    set_xbacklight $cval
}

dec_xbacklight()
{
    cval=$(cat $XBRIGHTNESS)
    cval=$(echo $cval'-' $XSTEP '+0.1'| $BC)

    echo 'trying to decrease backlight to ' $cval
    set_xbacklight $cval
}

check_cmds()
{
    if [ ! -f $BC ]; then
        echo 'command bc is not found!'
        exit 1
    fi

    if [ ! -f $AWK ]; then
        echo 'command awk is not found!'
        exit 1
    fi

    if [ ! -f $INTEL_REG ]; then
        echo 'command intel_reg is not found!'
        exit 1
    fi

    if [ ! -f $SUDO ]; then
        echo 'command sudo is not found!'
        exit 1
    fi

    if [ ! -f $XBRIGHTNESS ]; then
        echo 'brightness interface is not found!'
        exit 1
    fi
}

check_cmds

if [ $# -eq 0 ]; then 
    echo 'no parameters are specified, init backlight as default.'
    init_xbacklight
    exit 0
fi

if [[ $1 = "init" ]]; then
    init_xbacklight
elif [[ $1 = 'reset' ]]; then
    reset_freq
elif [[ $1 = 'freq' ]]; then
    get_freq
elif [[ $1 = 'inc' ]]; then
    inc_xbacklight
elif [[ $1 = 'dec' ]]; then
    dec_xbacklight
elif [[ $1 = 'min' ]]; then
    set_xbacklight $XBMIN
elif [[ $1 = 'max' ]]; then
    set_xbacklight $XBMAX
elif [[ $1 = 'set' ]]; then
    set_xbacklight $2
elif [[ $1 = 'fix' ]]; then
    init_xbacklight force
elif [[ $1 = 'get' ]]; then
    get_backlight
else
    echo 'script help:'
    echo 'init      init PWM freq and restore backlight'
    echo 'reset     reset backlight to default value'
    echo 'freq      read the value PWM freq reg'
    echo 'inc       increase backlight'
    echo 'dec       decrease backlight'
    echo 'min       set min backlight'
    echo 'max       set max backlight'
    echo 'set [val] specify the backlight value'
    echo 'fix       fix the brightness if set to 0(bind to shortcut)'
    echo 'get       read current backlight value'
fi



脚本调用很简单,smartbl.sh [参数名]就可以了,无参数自动恢复亮度。 参数列表:

  • init 初始化,恢复上次的频率和亮度
  • reset 恢复背光频率到出厂设置
  • freq 获取当前频率
  • inc 增加亮度
  • dec 降低亮度
  • min 调整到最低亮度
  • max 调整到最高亮度
  • set [val] 调整到指定亮度
  • fix 调成到默认亮度
  • get 获取当前亮度和频率

注意:脚本支持在xog 和console窗口下调整亮度。

取代系统的背光控制系统

脚本调试完毕之后,我们就可以用它来取代系统的亮调节了。系统的亮度调节系统会在开机、休眠恢复、背光按键按下、插拔外接显示器等事件时触发,我们需要将这些事件都关联到我们的脚本,这样就可以取代系统的背光控制了。

禁用系统自带的亮度调节

Archlinux 的背光服务为systemd-backlight@.service,它会在开机的时候自动恢复上次的亮度,我们需要将它禁用掉:

service mask systemd-backlight@backlight:intel_backlight.service

注意:
取决于你的硬件平台,非Intel显卡的系统并不是backlight:intel_backlight.service,有可能是backlight:acpi_video0.service等。

udev规则

我们需要为背光设置一个udev规则,主要有两个作用

  • 取消系统对最低亮度的限制
  • 背光相关事件(如插拔投影仪)发生时自动设置频率和背光亮度

添加一个ude规则:/etc/udev/rules.d/91-backlight.rules,内容如下:

KERNEL=="intel_backlight", SUBSYSTEM=="backlight", ENV{ID_BACKLIGHT_CLAMP}="0", RUN+="/usr/bin/sh -c '/opt/bin/for_acpi/smartbl.sh >> /tmp/smartbl.log'"

设置完成之后运行sudo udevadm control --reload-rules重新载入所有规则,然后检查一下这条规则是否有用,运行udevadm test /sys/class/backlight/intel_backlight,输出结果必须包含运行这个脚本的信息:

...
SUBSYSTEM=backlight
SYSTEMD_WANTS=systemd-backlight@backlight:intel_backlight.service
TAGS=:systemd:
USEC_INITIALIZED=3555916
run: '/usr/bin/sh -c '/opt/bin/for_acpi/smartbl.sh >> /tmp/smartbl.log''
Unload module index
Unloaded link configuration context.

注意:

  • 需要根据系统来设置KERNEL等字段,运行udevadm info -a -p /sys/class/backlight/intel_backlight查看这些信息,大致输出类此:
...
looking at device '/devices/pci0000:00/0000:00:02.0/drm/card0/card0-eDP-1/intel_backlight':
    KERNEL=="intel_backlight"
    SUBSYSTEM=="backlight"
    DRIVER==""
    ATTR{actual_brightness}=="7"
    ATTR{bl_power}=="0"
    ATTR{brightness}=="7"
    ATTR{max_brightness}=="852"
    ATTR{type}=="raw"
...
  • ENV{ID_BACKLIGHT_CLAMP}="0"的意思是允许讲最低亮度设置为0(关闭显示器),如果不设置的话每次开机亮度最低设置为28(我的机器),在脚本里面设置为1也没用!
  • udev不能运行脚本,只能运行一条命令!所以只能把脚本作为参数传入,命令必须使用完整路径,脚本里面的每条命令也需要完整路径!

自启动设置

开始的时候自动设置频率和恢复亮度,添加一个自启动服务即可:/etc/systemd/system/backlight_restoreb.service,内容如下:

[Unit]
Description=restore backlight on boot
After=systemd-udev-settle.service

[Service]
User=root
Type=oneshot
ExecStart=/opt/bin/for_acpi/smartbl.sh
TimeoutSec=0
StandardOutput=syslog

[Install]
WantedBy=multi-user.target

编辑完成之后运行 sudo systemd enable backlight_restoreb.service启用即可。

休眠恢复

休眠恢复的服务和开机的差不多,只不过是事件不同,/etc/systemd/system/backlight_restores.service

[Unit]
Description=restore backlight after suspend
After=suspend.target

[Service]
User=root
Type=oneshot
ExecStart=/opt/bin/for_acpi/smartbl.sh
TimeoutSec=0
StandardOutput=syslog

[Install]
WantedBy=suspend.target

编辑完成之后同样运行 sudo systemd enable backlight_restores.service启用服务。这时候可以让电脑休眠,测试一下从休眠状态下恢复之后亮度有没有恢复。

外接显示器断开

外接显示器连接或者断开时(如仅使用第二屏幕之后再断开就会出发背光重置),也有可能出发背光更改事件,这个通过上面的udev规则调用脚本就handle掉了,这里不需要其他处理。

注意:
需要说明的是开机也是会触发udev规则的,但是udev在开机状态下对背光的设置无效,可能是udev规则先于系统的背光设置吧。

背光调整按键

Archlinux的背光按键默认被Power Managerhandle掉,没按一下都有一个巨大的step(大概40),在那么搞得频率下其实五六十的最高亮度就已经很高了,一次按键就可以达到最高亮度!我们可以通过讲按键映射到我们的脚本讲step设为2就可以了,这样背光级数就比较高。
首先取消Power Manager对按键的Handle:

Backlight handled by Power Manager

然后我们需要将按键映射到我们的脚本就行了,我们需要借助acpid 的event handle来处理,编辑/etc/acpi/handler.sh文件,在最顶上加上亮度加减按键的处理:

case "$1" in
    video/brightnessup)
        case "$2" in
            BRTUP)
                /opt/bin/for_acpi/smartbl.sh inc
                ;;
            *)
                logger "ACPI action undefined: $2"
                ;;
        esac
        ;;
    video/brightnessdown)
        case "$2" in
            BRTDN)
                /opt/bin/for_acpi/smartbl.sh dec
                ;;
            *)
                logger "ACPI action undefined: $2"
                ;;
        esac
        ;;
...

注意:

  1. 如果不知道按键名称,可以运行acpi_listen命令,然后按下按键查看输出的按键名称。
  2. 亮度调节按键似乎不受xmodemap的控制,在xmodmap里更改按键映射似乎无效。
  3. 如果更改Power Manager之后亮度调节按键仍然被系统handle掉,可以直接编辑/usr/share/X11/xkb/symbols/inet这个文件,将所有具有XF86MonBrightnessDownXF86MonBrightnessUp关键字的行注释掉,然后重启。

写在后面

到此终于完美解决了LED背光闪烁的问题,用起来明显感觉眼睛比以前舒服。目前的大部分显示器都有LED背光闪烁的问题,虽然目前没有科学证明其对眼睛的伤害,但是从个人感觉上来说伤害还是有的,特别是对于长时间使用电脑的人来说,还是CCFL背光的显示器比较合适。可惜市场上CCFL背光的显示器已经很少了,LED技术带我们回到了大屁股显示器的闪烁时代,这也和那些厂商急于追逐利益又关系吧,据说明基掌握LED了不闪屏的黑科技,希望这种技术能尽快普及。

参考资料

关键字:flicker, archlinux, x250, led

本文链接:树叶的BLOG >> 彻底解决Linux下LED的背光闪烁

本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 Unported许可协议进行许可。

上一篇 : 防止树莓派网络配置出错及各种修复方法 下一篇 : 解决Linux下笔记本休眠立即唤醒的问题