Adjusting laptop backlight PWM frequency
By Patrick Wigmore, , published: , updated
What?
I did some experiments to investigate the perceptibility of screen flicker at different backlight PWM frequencies, on a laptop with Intel HD Graphics, running Linux.
These notes are only a log of what I did, not advice or recommendations. Changing the PWM frequency might damage the laptop or cause unwanted emission of electromagnetic radiation. I did not end up using a modified PWM frequency in the long term.
Why and how?
The backlight PWM frequency of my N350DW laptop defaults to about 200Hz, which I find to be noticeable. It turns out the PWM frequency can be altered by setting the value of a GPU register.
The programmer’s reference manual (PRM) for the GPU in the i5-6600T CPU describes the register involved.
What were the results?
When the backlight is set at 100% brightness, the PWM has 100% duty cycle and there appears to be no detectable flicker when waving a steel ruler back and forth in front of the screen. However, as the brightness is reduced, flicker rapidly appears.
- At the default 200Hz PWM frequency, flicker is obvious to me.
- At about 1000Hz, flicker is less apparent but still obvious when testing.
- At about 2000Hz, flicker is much harder to discern, but still present.
- At about 4000Hz, I can no longer perceive the flicker.
Looking at the GPU register
This information is specific to the GPU model. In my case it’s for the GPU in an i5-6600T processor in the 2015-2016 “Skylake” generation. The details may vary for other models, and may be described in a different Intel manual.
For this particular processor, register 0xc8254 controls the backlight PWM. You should not fiddle with register 0xc8254 without first confirming that it is supposed to do the same thing on the hardware you have at hand.
Bits 31 to 16 of the register represent the period of the backlight PWM. Bits 15 to 0 of the register represent the duty cycle of the backlight PWM.
The duty cycle value expresses the length of the “on” time and must therefore be less than or equal to the total PWM period. Lower values result in shorter duty cycle and dimmer perceived backlight.
For example, to set half brightness, the duty cycle value should be half that of the period value.
The values are relative to the PWM clock period. According to the Intel programmer’s reference manual, the default PWM clock frequency is 24MHz.
According to a blog post I found about slightly different hardware, the Rawclk frequency can be used to determine the PWM clock frequency. For the i5-6600T, the Rawclk frequency can be read in MHz from the GPU register 0x6204, according to the PRM.
Both the backlight period and backlight duty cycle values are specified in 128ths of a clock period. The Intel PRM states that they are in 16ths of a clock period as standard, or 128ths alternately. Experimentally, I found them to be in 128ths of a clock period for this machine.
I was initially a bit thrown by Intel’s expression of the above. They say that the period and duty cycle are in “clock periods multiplied by 16 (default increment) or 128 (alternate increment)”. At first glance this seemed ambiguous: was I multiplying the period or duty cycle value (in clock periods) or the value of the clock period itself. But actually, these are equivalent. Multiplication is commutative: a×(16T) = (a×16)T
Manipulating the register values
Under Linux, in order to modify the contents of the GPU registers, we can
use the intel_reg
utility, from the intel-gpu-tools
package.
To read the contents of 0xc8254, we can run
# intel_reg read 0xc8254
Helpfully, this indicates the function of the two values held in the register, with a decimal representation of the values. For example, the default value on the N350DW is:
(0x000c8254): 0x03a903a9 (freq 937, cycle 937)
Note that this is MSB-first, like the conventional way of writing numbers. Bit 31 is on the left, bit 0 is on the right.
intel_reg
also provides a write
command, to alter the value.
The value returns to the default when the GPU resets, including when suspending the system or rebooting it.
Adjusting brightness when the frequency has been changed
To get correct brightness levels, the duty cycle value needs to be scaled to fit the period.
When adjusting the screen brightness by normal means, the duty cycle gets changed. But it is not calculated based on the current value of the period. Instead, it is calculated based on the period that was set by default.
This can produce unintended results:
When the PWM frequency has been set lower than the default, the brightness can only be set, by normal means, to values below 100%.
When the PWM frequency has been set higher than the default, adjustment by normal means will reduce the brightness only at the low end of the range of adjustment, or not at all. At the high end of the range, the duty cycle will be set to values higher than the period, indicating >100% duty cycle and resulting in 100% duty cycle.
However, the brightness can still be changed to other levels by directly altering the duty cycle value in the register.
Existing values
For my machine, the default settings are as follows:
Period | Duty cycle |
---|---|
937 (200.1Hz) | 937 (100%) |
Stepping through brightness levels using keyboard hotkeys gives:
Brightness | Duty cycle | Duty cycle percentage |
---|---|---|
100% | 937 | 100% |
95% | 893 | 95% |
90% | 849 | 90% |
85% | 804 | 85% |
80% | 761 | 81% |
75% | 717 | 77% |
70% | 672 | 72% |
65% | 628 | 67% |
60% | 584 | 62% |
55% | 540 | 58% |
50% | 496 | 53% |
45% | 452 | 48% |
40% | 408 | 44% |
35% | 364 | 39% |
30% | 320 | 34% |
25% | 275 | 29% |
20% | 231 | 25% |
15% | 188 | 20% |
10% | 143 | 15% |
5% | 99 | 11% |
0% | 55 | 6% |
The 6% duty cycle shows as the backlight being off completely.
Calculating frequency represented by existing register value
To calculate the frequency represented by a given register value, x, we can use the formula: or, more concisely or
Calculating register value to set for a given frequency
To calculate the period register value, x, to set for a given frequency, f, we can calculate or
Upper and lower bounds for the frequency
3Hz is the minimum frequency it is possible to set in 4-bits.
187.5kHz is clearly the maximum frequency it is possible to set, with a value of 0x1 in the register, though that is likely an inadvisably high frequency.
Experimentally, 3Hz, 10Hz and 20Hz do not work on the hardware. Experimentally, 50Hz, the default 200Hz, 997Hz and 1994Hz do work.
Optimal values?
Given that brightness seems to be adjusted, ordinarily, in 5% steps, it would make sense to choose a factor of 20 for the period, so that the duty cycle can be adjusted in neat whole numbers.
The obvious values to use, then, are periods of 0x14 (20; that is 9375Hz) or 0xa (10; that is 18750Hz). Both appear to work and can be divided into 20 or 10 brightness steps respectively. 0x28 (40; 4687.5Hz) also works, but creates audible noise.
However, 18750Hz results in glitchy flicker of the backlight after some time, suggesting it might be causing an electronic component to overheat, or some problem of that nature. 9375Hz also does this eventually.
Perhaps 2347.75Hz is a better choice (0x50, 80).
Dithering?
My laptop only has a 6-bit-per-pixel panel, so I was also interested to find out whether the screen was using temporal dithering, which can also cause visible flickering or shimmer. However, further investigation shows that this is not the case – the screen is not being temporally dithered; only spatially dithered.
The Intel PRM shows that bits 3:2 of the PIPE_MISC register at 0x70030 specify the type of dithering being used. The default on my system is 00b, which specifies Spatial (00b), not Temporal (11b) dithering.
Altering these bits of the register results in visible change to the dithering. Spatio-Temporal 1 (01b) produces smoother colours with less obvious dithering, but is temporal so perhaps not ideal if I don’t want flicker. Spatio-Temporal 2 (10b) is practically indistinguishable from Spatial. Temporal results in a very low-frequency flicker which is highly obvious and would be practically unusable.
Clearly my system is not configured to function correctly with temporal dithering without altering some other parameters. Perhaps the actual LCD panel is not capable of it, or perhaps there is a frequency that needs configuring somewhere in another GPU register.
Work-arounds for brightness adjustment with a modified PWM frequency
I did not get into trying to override the default brightness adjustment behaviour to take into account the adjusted frequency, but there are a number of ways this could be attempted.
Re-initialise the driver
I haven’t dug into the documentation or source code to find out, but it is possible that there might be a way to re-initialise the code that normally controls the brightness, so that it takes into account a new value for the period. The kernel knows a “max_brightness” which can be read with:
cat /sys/class/backlight/intel-backlight/max-brightness
This value is effectively the period value. Unfortunately it is read-only, but if there is some way to re-initialise it then that might be the cleanest way to get the brightness adjustment to work properly with an altered backlight frequency.
Re-assign hotkeys
A rudimentary and relatively straightforward workaround would be to reassign the brightness adjustment hotkeys on the keyboard to run scripts that alter the value in the register.
But this would not change the behaviour of non-user-initiated brightness adjustments.
Use the desktop-environment’s power management events
In KDE, power management events could trigger scripts via the KDE notifications system, if the built-in brightness setting were disabled. The script for this would need a parameter for the desired brightness, and would need to be run both on entering and exiting a given state.
Write a udev rule
Evidently, it is possible to use a udev rule to run a program whenever the brightness is changed via the kernel. This could be a fairly clean approach. A program or script which can take the brightness value (which might be available as a variable in udev, or perhaps could be read from the register) and convert it into the appropriate duty cycle value to write to the register.
Conclusions
I ended up trying quite a lot of different frequencies, but I don’t think I’d be comfortable running the backlight above about 2kHz in practice, as I’m not sure what the limits of the hardware might be.
At 4kHz I could not perceive any flicker, so if I was looking for a flicker-free display that used PWM dimming, then I’d be looking for something above 4kHz. But I would not run this frequency on my current hardware day-to-day, because of the audible noise it created.
As noted above:
- At the default 200Hz PWM frequency, flicker is obvious to me.
- At about 1000Hz, flicker is less apparent but still obvious when testing.
- At about 2000Hz, flicker is much harder to discern, but still present.
- At about 4000Hz, I can no longer perceive the flicker.
I was originally motivated to investigate the flicker because I was getting headaches after I started using the new laptop. However, this turned out to be mere coincidence; the headaches were not caused by the flicker.
Although I nevertheless find the 200Hz PWM obnoxious, the risk of damaging what is still fairly new hardware, combined with the loss of strong motivation, has put me off from trying to run it at a different frequency in the long term. Instead I just tend to avoid dimming the screen at all.
These notes were reformatted in July 2023 to better suit the new website. The basic content remains the same.