PWM/DC control of display brightness

Another report for the DIY folk, before I forget :slight_smile: This time about PWM/DC regulation of display brightness. Some displays, such as the Waveshare 70H-1024600 QLED are very bright by default and have a PWM/DC regulation of the backlight.

Their wiki suggests regulating the brightness by PWM signal using Python GPIO library. However, this causes terrible flickering either all the time or as soon as there is any load on the zynthian (playing a single note in Pianoteq is enough). I tried smoothing up the PWM signal into a more stable DC control using a RC filter, but the flickering just turned into equally annoying quick brightness changes. It seems the Python library uses just software PWM emulation, which is unstable under load (and probably adds to the system load as well?).

Fortunately, the RaspberryPi has hardware PWM available on 4 od its pins, too. Pins 18 and 19 are used for the I2S sound (unless you use an external USB sound card), so only the pins 12 and 13 remain available. To make them work, one needs to add the proper overlay configuration into /boot/config.txt:

dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4

After reboot, the pins 12 and 13 are available as “0” and “1” via the kernel pwm interface. From shell you can activate the pin 13 by sending:

echo 1 > /sys/class/pwm/pwmchip0/export

Next, you have to set the period (frequency), e.g. 1000ns = 1ms ~ 1000Hz, and then you can start the PWM:

echo 1000 > /sys/class/pwm/pwmchip0/pwm1/period
echo 1 > /sys/class/pwm/pwmchip0/pwm1/enable

Now, you can adjust the duty_cycle from 0 to 100% of the period value by sending the desired value - in this case also 0 to 1000:

# set brightness to 50% (of the period value)
echo 500 > /sys/class/pwm/pwmchip0/pwm1/duty_cycle

In this way, the brightness remains stable all the time at any load. You can still add the RC filter, but then you will never reach the full level of DC to turn the display completely off, even when you set the duty cycle to 100%. It will just reach the minimal possible brightness.

As for the range of settings otherwise: as expected, the change of brightness is not linear. On my display, the value of 20% is a minimal change and 50% just a slightly dimmer. More significant dimming is achieved by raising the duty cycle to 70, 80 or 90%. The maximum (i.e. minimal brightness) is reached somewhere between 95-99.5%, then the displays just turns off completely (depends on the period: e.g. when setting it to 1000000 you can reach slightly higher values without turning the display off completely).

I noticed there is already an implementation of a PWM system service to regulate a fan by @wyleu. But it also uses just the Python SW PWM. It is probably not a problem for a fan if the speed is not stable (unless you have a noisy one). I didn’t test how does it affect the load on the system, but it might be just negligible.

Open questions I would like to find an answer to next:

  1. Will it also work this way on a RPi5? On the way, I noticed people mentioning the Pi5 is different and these older overlays do not work there. But the situation may change quickly now, when the Pi5 is increasingly being deployed by everyone. So, maybe just switch to another overlay designed for the Pi5?
  2. Can this also be exposed via the /sys/class/backlight/rpi_backlight/brightness kernel interface in order to be directly used by the current UI settings for V5 as presented by @jofemodo in the thread V5 LED brightness control? Would it involve writing another overlay?
3 Likes

re question 1 - I believe the same concept of hardware PWM does apply, but the details of how to set the GPIO pin’s mode has changed for the RPi5.

I already noticed some updated overlays. Let’s see what becomes mainstream/standard and whether we will need to switch overlays depending on the Pi version.

1 Like

Answers to my second question: Yes. It looks like it would.

I couldn’t find a ready made solution and I have a minimal understanding of DT overlays, but I managed to extend the pwm-2chan overlay to expose one of the two available PWM channels as backlight control, so that (after a lot of fiddling with the numbers for the scale) the current Zynthian UI can use it to control the backlight out of the box.

Here is the overlay source configuring both PWM channels (by default the PINs 12 and 13) for PWM and exposing the second one for brightness control, with a somewhat logarithmic scale and a default brightness setting of 75% (i.e. 192 of 255) to start with:

/dts-v1/;
/plugin/;

/*
Legal pin,function combinations for each channel:
  PWM0: 12,4(Alt0) 18,2(Alt5) 40,4(Alt0)            52,5(Alt1)
  PWM1: 13,4(Alt0) 19,2(Alt5) 41,4(Alt0) 45,4(Alt0) 53,5(Alt1)
*/

/ {
        compatible = "brcm,bcm2835";

        fragment@0 {
                target = <&gpio>;
                __overlay__ {
                        pwm_pins: pwm_pins {
                                brcm,pins = <12 13>;
                                brcm,function = <4 4>; /* Alt0 */
                        };
                };
        };

        fragment@1 {
                target = <&pwm>;
                frag1: __overlay__ {
                        pinctrl-names = "default";
                        pinctrl-0 = <&pwm_pins>;
                        status = "okay";
                };
        };


        fragment@2 {
                target-path = "/";
                __overlay__  {
                        pwm_backlight: pwm_backlight {
                                compatible = "pwm-backlight";
                                brightness-levels = <0 1 2 3 4 6 9 13 19 28 40 58 84 122 176 255>;
                                num-interpolated-steps = <17>;
                                default-brightness-level = <192>;
                                pwms = <&pwm 1 1000000 1>;
                                status = "okay";
                        };
                };
        };

	__overrides__ {
		pin   = <&pwm_pins>,"brcm,pins:0";
		pin2  = <&pwm_pins>,"brcm,pins:4";
		func  = <&pwm_pins>,"brcm,function:0";
		func2 = <&pwm_pins>,"brcm,function:4";
		clock = <&frag1>,"assigned-clock-rates:0";
	};
};

As obvious from the source, the backlight control is exposed as pwm_backlight instead of rpi_backlight in /sys/class/backlight/, but that’s OK since the zynthian UI just takes the first backlight control it finds in this path (if it finds anything at all).

I have not tested the overriding parameters for selection of the PINs, but I suppose they should work like in the original pwm-2chan overlay. Anyway, the choice seems to really be just between PINs 18/19 and 12/13, and the first option is probably unusable for most zythianers. Changing the backlight control from the second to the first PWM channel (i.e. from PIN 13 to 12) involves changing the line pwms = <&pwm 1 1000000 1>; into pwms = <&pwm 0 1000000 1>;. If your display uses non-inverted brightness control, you should also change the last 1 to 0 on this line. It may probably be made customizable by additional parameters as well, but I don’t want to waste more of my time by trying to find out how.

I will provide the compiled overlays (exposing either PWM channel 1 or 2 as backlight) in some pull request to the zynthian-sys repository. I may possibly also have a look at the situation with Pi5 later.

And for those, who want to try it right away and don’t compile their overlays on everyday basis, like me, here is how to compile it (if saved as backlight-pwm.dts) and copy to the right place:

dtc -@ -I dts -O dtb -o backlight-pwm.dtbo backlight-pwm.dts
mv backlight-pwm.dtbo /boot/overlays/

Then you can just add the line dtoverlay=backlight-pwm to your /boot/config.txt.

2 Likes

As for question 1: Yes. The overlay described above works also on RPi5.

I wanted to add the overlays into zynthian distro, but I cannot find any established way to do that via zynthian-sys. Is it possible to add them via PR into some zynthian repository?

Submit a PR on zynthian-sys.

I dont see a folder to place such files into.