Programmable LED Dimmer

Intro

A few months ago, I installed an LED strip on the back of the headboard of my bed. I really like this look and it gives a soft diffuse light in the room, but it also gave me an idea: wouldn't it be great to have the lights slowly fade up over half an hour or so before my alarm goes off in the morning? Additionally, it would be great to dim the LEDs manually myself if I wished.

I already have a Raspberry Pi Zero W in my room (for which I designed and 3D-printed a custom case, I might put another page on for that later), which does a couple of general automation tasks. This would also be the ideal way to control the LEDs. My general idea was to write a Python script to control the LEDs by outputting a PWM signal on one of the GPIO pins on the Pi. I could then use MQTT to easily send commands from my phone or PC to program the timing and profile of the brightness ramp.

Switching

The LED strip uses 12 V, and draws approximately 1.3 A, so it requires a reasonable amount of power. In order to control this using a Pi, I decided to use a MOSFET. I chose the IRLZ44N N-channel MOSFET, which worked fine in tests on my desktop, but this system has a drawback: the Pi GPIO pins cannot drive much current; 2 mA would be a good target. To drive the gate to 3.3 V at 2 mA, a current-limiting resistor of 1650 Ω would be required. In order to charge to 2 V (the maximum threshold turn-on gate-source voltage) with an input of 3.3 V, approximately one time constant is required (0.63 x 3.3 ~ 2), and the time constant is given by $$\tau = RC$$. Thus, it will take around 3 us to turn the gate on. This would allow 500 Hz PWM switching down to around 1% duty cycle, which should be enough. However, I found in testing that the brightness control did not behave quite right at PWM frequencies above ~100 Hz when driving the MOSFET directly from the Pi, so I decided to design a MOSFET gate driver into the system to make sure that the current demands on the Pi were minimal.

In order to meet these requirements, I designed a system in which an input signal from the Pi would feed the input of a TC4427 MOSFET driver, whose output would then connect to the MOSFET gate. Separately, a PWM signal would be generated by a circuit based around a 555 timer, with duty cycle controlled by a potentiometer. A switch would then connect the TC4427 input to either the PWM signal from the 555 circuit, or to the signal from the header pin input. In this way, the user can switch between manual control using the rotary potentiometer, or software control using the Pi. A second switch simply turns the system on and off.

The MOSFET driver system is surely overkill for this system - indeed, the TC4427 alone is capable of pushing 1.5 A, enough to drive the LED strip without using the MOSFET! Nonetheless, this method ensures that all components are well within spec, and that there is enough overhead to driver longer strips in the future with the same system if I wish.

Circuit and PCB design

I designed this schematic using Autodesk Eagle, and then laid out a PCB. In order to add flexibility, I added a DNP resistor spot to bypass the MOSFET driver if desired; a current-limiting resistor (though these are built into the LED strip so I will populate this with a 0 Ω part); and a pull-down resistor on the MOSFET gate (though the driver has an internal push-pull configuration so this is not required in the standard format). I used a DC jack to accept the 12 V input from the power supply; a screw-type PCB terminal block for the output to the LED strip; and a pair of header pins to share GND with the Pi and accept an external PWM signal. For the two switches and the rotary potentiometer, I used three sets of three header pins, one trio to connect each part. This would allow me to place the switches and knob somewhere convenient on the outside of the case I would design, and connect them easily with jumper leads.

I used 1.6 mm traces where power would run for the LEDs, in order to ensure that the PCB would be able to take ~1.5 A with headroom at a standard 1 oz copper weight - such wide traces should be able to carry around 3 A with temperature rise below 10 °C. I added plenty of ground vias around components that might generate heat, such as the MOSFET - although with even 3.3 V gate voltage, the IRLZ44N should have a drain-source resistance of just 25 mΩ, so should dissipate extremely little power.

Enclosure design

Importing this PCB into Fusion 360, I was able to directly design the enclosure around the PCB design. I had to create 3D models for some of the components I was using, such as the DC jack, and this was very helpful with this stage of design. I decided on a standard base-and-lid design, with holes on the side for the IO, registration ridges to keep the lids placed well, snap-fit clips to keep the lid in place and raised posts to screw the PCB into. I included heat-set threaded brass inserts in the design to screw the PCB down. This isn't really necessary as I'm not planning to take the PCB in and out very often, but it's a nice touch. I also plan to add some diagonal slots to the design to allow a little air flow to deal with the small amount of heat I expect the part to generate in use.

The holes in the case allow for convenient and neat connection of the three sets of IO - power in, PWM signal in (optional), and LED power out.

Building the PCB

I ordered the PCB from JLCPCB for a couple of dollars, no special settings were required. These came fairly quickly, and I built one board up. Initially I had issues with the board, until I discovered a dry solder on the MOSFET driver input gate. With this fixed, the board worked perfectly. Using a jumper or switch, the PWM signal source can come from the input pins, and be very happily driven by a Pi Zero W, or from the 555 circuit and controlled using a 10k rotary potentiometer.

Writing the Code

Before the project began, I had imagined that writing the code should be pretty straightforward. After all, I have used MQTT before, and it's pretty simple. After that, it's just a case of executing the commands that get sent, right? Right?


In fact, the code took a lot of turning around in my head. I could of course have easily implemented a system that just did immediate on and off, and programming one event in the future. That was not enough for me though, of course - I wanted lots of features!


As a result, the code, though definitely in the "working alpha" stage, is still under development. Features already implemented include:

  • Immediate on, off, and setting to any brightness value

  • Brightness values are calculated according to the CIE 1931 formula for perceived brightness, so setting 50% looks half as bright as 100%, even though it is actually only less than 20% duty cycle. I precalculated these into a lookup table for performance

  • Immediate fades to a target brightness over a specified period. Useful if you tend to fall asleep while reading!

  • Programming any number of future events, with fades from start to end values over an arbitrary specified period. Great for wake-up alarms in the morning

  • Handling of overlapping events - new instant commands or programmed events check for conflicts with all queued events, and the more recent command takes priority

  • Multi-threading ensures slick operation and response at all times, even when executing other commands

  • Listing all events queued

  • Deletion of specified events, with assignment of unique IDs to events

  • Resuming from a savefile in case of crashes


Features currently under development (see Issues and branches on Gitlab):

  • Advanced sequences of light changes, for example, a morning alarm that fades up to 100% over 30 minutes, but then turns off after an hour (unless it is turned off in the meantime)

  • Programmable series of events using cron. I have begun the framework for this, but the plan is to use the Python library croniter to accept cron commands to repeat events, including sequences. So if you want that alarm to fade up from 0 to 100%, reaching 100% at 07:30 and turning off after an hour, to repeat every weekday but not at the weekends? No problem


As you can imagine, these more complex features take a good amount of careful handling. The result is very pleasing though, and I'm looking forward to finalising the features in progress, as that will leave a very capable final system.


Using the free, and excellent, MQTT Dash app from routix.net provides an easy control interface on my phone without needing to create a custom app... Yet!


Future feature ideas:

  • "Burglar mode" - either completely random patterns or semi-random patterns to mimic real use, to be set for certain periods

  • "Holiday mode" - suspend all operation for a given period

  • Default quickfade effect for turning on and off

  • Different non-linear fade schemes