My USB-C port replicator does not pass on brightness commands to my external monitors.
The idea of a hardware solution was floating around in the back of my mind for a while. Until my friend made exactly this. I decided, if he was no longer fiddling with the screen’s built-in menu, neither was I!
HDMI (and Displayport, DVI, and modern VGA connectors) have a control channel called DDC, Display Data Channel. It is simply an I2C bus with defined commands for things like brightness, contrast and input switching.
For my use case, I need to control the brightness of two monitors. I rarely change contrast or switch inputs, so I opted for a minimal solution with just two buttons for controlling brightness.
Proof-of-concept
To make this as simple as possible, I would prefer not to put my device in-line with the signal already going from my computer to the monitors. There was an unused HDMI port on each monitor, but I was not sure if DDC data would be received if the screen was showing the picture from another input?
For the first proof-of-concept, I grabbed a Raspberry Pi Pico and a HDMI breakout just to verify that it would work. (Don’t mind the HDMI connector hanging off the back of the Pico, it unfortunately did not connect to the DDC pins!)
I decided to try out the RP2040 for this task because it runs Micropython well. Typically, I would turn to Arduino for prototyping but I think Micropython could have some benefits. For one, there is no compiling, so there is no waiting. This also means it is possible to run single lines of code straight in the REPL, on the device, via serial.
With the physical connections sorted out, we need a valid DDC command to send. I had been through a similar journey with my Linux desktop a few years ago, so I knew there was software that could give me the right data. (Only in Linux, and without a port replicator in between, I could actually make it work with just code.) In a dusty corner of my hard drive, I found a copy of python-ddcci. With a few small modifications, I could make it print commands to the terminal instead of trying to send them to the screen. Here is the diff, in case you are curious. Now I could save payload data for ten brightness settings from 0 – 100%.
$ python ddccli.py --brightness 10 ['0x51', '0x84', '0x3', '0x10', '0x0', '0xa', '0xa2']
I will not go into the whole DDC protocol here, but the reason I did it this way is to save myself the time and hassle of writing the code to create the payload dynamically. In theory, only the brightness value changes with each setting, but then we also have the last byte which is a CRC. So, instead of spending time trying to figure out what permutation of CRC is used, I opted for a big old array with the commands. Also, I’m 100% sure this uses up less memory in the end.
Long story short, I verified that I could cycle through brightness settings with some janky cables hanging off my Pi Pico. The project is a go! ๐
Here is the code:
https://github.com/albertskog/display-dimmer/blob/main/firmware-pico-poc/main.py
Prototype
Nothing in my parts bin felt appealing for this project so I decided to invest in something new. I was curious about RISC-V and had heard about really cheap microcontrollers on the EEVBlog. Like the CH32V003 from WCH:
I’m a little conflicted about ordering cheap stuff from China, but it sure is refreshing to get a devkit, programmer and five chips for a fraction of the cost of what big “western” manufacturers charge. I got mine from the official WCH store on Aliexpress.
As mentioned above, I usually use Arduino (and Platformio) for prototyping whenever possible. The level of hardware support is simply amazing โ I can use the same familiar code APIs I’m used to and the hard work of adapting it to the manufacturers SDK is done by someone else. Of course there is an Arduino core for WCH parts. And a Platformio platform that supports Arduino and seven(!) other frameworks. Open source at its best!
I fired up VSCode, installed the CH32 platform and created a new Platformio project. Within minutes, I had ported my Micropython POC to run on the CH32V003F4P6-EVT-R0.
Then I realized.
In the process of deciding on what part to use, I had neglected the fact that i needed to control two monitors. The CH32V003 has only one I2C peripheral.
No worries, usually it is possible to re-map what pins the peripheral is using. Not ideal, but should work for our use case. I found the CH32V003 is indeed has three different pin configurations for the I2C peripheral, phew!
Then I spent more than a little time figuring out why my code refused work with these pins. Eventually, I realized the alternate mappings had not been added to the WCH Arduino core. Yes, I know I was just praising Arduino and Open Source… I choose to see it as an opportunity to contribute a fix to anyone that comes after. At the time of writing, my pull request for adding support for the alternate mappings is still open:
https://github.com/openwch/arduino_core_ch32/pull/152
Long story short, I verified that I could cycle through brightness settings with some janky cables hanging off my CH32V003F4P6-EVT-R0. The project is a go! ๐
Here is the code, but note you need to apply the changes from my PR above if it has not been merged!
https://github.com/albertskog/display-dimmer/blob/main/firmware-platformio/src/main.cpp
Custom PCB
When designing the first version of a PCB, I like to have a lot of flexibility and think ahead what might go wrong. For this reason, I added footprints for a 3.3 V regulator, an external crystal, I2C pull-up resistors and plenty of decoupling capacitors. All unused pins from the microcontroller, everything power-related and some of the unused HDMI signals were routed out to pin headers. Most of this was not mounted on the board and has not been used, yet. While it was probably overkill, I also researched and added protections like series resistors and ESD-diodes as a learning exercise.
The bill of materials can be found here:
https://github.com/albertskog/display-dimmer/?tab=readme-ov-file#bom
I designed the board in Kicad and sent it off to JLCPCB. Here is the minimal component setup ready for soldering:
I don’t have a reflow oven a hot-air station. In my career, I worked more with hot-plates. There are several ways to do hot-plate soldering at home, I opted for this technique which I shall call the Ghetto Hotplate:
To my surprise delight, this actually worked quite well! I put the populated board on top of a blank one, put them on the cold plate, then turn it on max. As solder started melting, I turned the heat off and waited until all the solder had melted. When it looked good, I took the boards off the heat and let them cool down.
I did add too much solder paste on one of the HDMI connectors the first time. I simply put the board back on the stove and lifted the connector back off. After removing the excess solder with a soldering iron and solder wick, I tried again with a more appropriate amount of paste. In hindsight, I probably should have used a new HDMI connector. The pins seem to slowly short out over time, probably because of the rework?
Anyway, I’m quite happy with the end result!
I made some tweaks to the firmware, adding support for long-press and limiting the number of command sent back-to-back and am now using this daily.
I did also Rewrite It In Rust, but that is a story for another day!
If you want to check out the project, I have put everything on Github:
Leave a Reply