Eliminate backlight flicker with Intel i915

3 minutes read

The brightness of LED backlight in TFT displays is sometimes controlled using a technique called PWM or pulse width modulation. If implemented incorrectly, particularly if PWM frequency is too low, it can introduce distracting visual effects and induce eye strain, headaches, and even dizziness in some people[1].

Many older laptops set PWM frequency for their backlight too low. Luckily, in laptops equipped with Intel i915 GPU the PWM frequency is often controlled by the GPU and thus can be adjusted by a software.

According to the manual[2], in i915 (or at least, in Sandy Bridge) PWM frequency is controlled by a value of the 4 upper bytes of the register 0xC8254; the value of these 4 bytes

represents the period of the PWM stream in PCH display raw clocks multiplied by 128

PCH display raw clocks is held in another register, 0xC6204, and it is PCH frequency in MHz. So if the value is 0x7D it means, that PCH frequency is 125 MHz.

Resulting PWM frequency in Hz can be calculated by the formula:

Important:

fpwm = ( reg0xC6204 × 1,000,000 ) / 128 / reg0xC8254[7..4]

This means, that to increase PWM frequency of LED backlight we have to decrease the value stored in 4 upper bytes of the register 0xC8254.

An application called intel_reg from the intel-gpu-tools[3] package lets reading and writing i915 registers, and thus the formula can be implemented as a simple shell script (if we ignore some defensive boilerplate):

Caution:

Although precautions have been taken, executing this script may cause damage to your system or even hardware

#!/usr/bin/env bash

function usage() {
  cat << EOF
intelpwm FREQ [PCH_RAWCLK_FREQ_REG [BLC_PWM_PCH_CTL2_REG]]

  FREQ                  desired PWM frequency in Hz
  PCH_RAWCLK_FREQ_REG   PCH raw clock register name or offset
  BLC_PWM_PCH_CTL2_REG  backlight control register name or offset
EOF
}

function reg_check() {
  # If the register is a mnemonic name and not an offset, make sure that it is
  # known to intel_reg utility. Unknown mnemonic likely means, that we are on a
  # different generation of hardware that may have different registers layout
  printf "%d" "$1" 1>/dev/null 2>&1  || \
    intel_reg list | grep "$1" 1>/dev/null 2>&1 || \
      (>&2 echo "Register $1 is not defined on this hardware" && false)
}

function reg_read() {
  # this assumes fixed position of the value in intel_reg output
  # so far this has been the case though
  intel_reg  read "$1" | cut -c51-60
}

FREQ=$1
PCH_RAWCLK_FREQ_REG=${2:-PCH_RAWCLK_FREQ}
BLC_PWM_PCH_CTL2_REG=${3:-BLC_PWM_PCH_CTL2}

if [ -z "$FREQ" ]; then
  usage
  exit 1
fi

reg_check "${PCH_RAWCLK_FREQ_REG}" || exit 1
reg_check "${BLC_PWM_PCH_CTL2_REG}" || exit 1

PCH_FREQ="$(reg_read ${PCH_RAWCLK_FREQ_REG})" 1
BLC_CTL2="$(reg_read ${BLC_PWM_PCH_CTL2_REG})"
CYCLE="${BLC_CTL2:6:4}"
HEX=$(printf "0x%08x" $((1000000*PCH_FREQ/128/FREQ)))
PERIOD="${HEX:6:9}"

>&2 echo "Writing 0x${PERIOD}${CYCLE} to register ${BLC_PWM_PCH_CTL2_REG}"
intel_reg write "${BLC_PWM_PCH_CTL2_REG}" "0x${PERIOD}${CYCLE}"
  1. boilerplate ends here, and actual formula implementation starts

And then, to set PWM frequency to some reasonable 800 Hz we would do

# intelpwm 800

Since the value we wrote to the register is not permanent, we have to write it again every time the power is cycled for the GPU or the backlight (screen is turned off, laptop suspended, etc.). This is easy to do with udev:

KERNEL!="intel_backlight", SUBSYSTEM!="backlight", ACTION!="change", GOTO="backlight_pwm_rules_end"

RUN="intelpwm 800"

LABEL="backlight_pwm_rules_end"

The script, slightly modified to receive values from a config file, and the corresponding udev rule are published on my github as intelpwm-udev[4].


  1. https://www.tftcentral.co.uk/articles/pulse_width_modulation.htm
  2. Intel HD Graphics programmer’s reference manual - https://01.org/linuxgraphics/sites/default/files/documentation/snb_ihd_os_vol3_part3.pdf
  3. http://cgit.freedesktop.org/xorg/app/intel-gpu-tools/
  4. https://github.com/edio/intelpwm-udev