Let’s say you’ve got some swinging AC current driving a string of 100 LED diodes slapped on the side of your house to attract a certain portly gift giver. Now you want to dim these beautiful bulbs in time with some festive holiday music. You could do this the usual way, walk down to the hardware store, get yourself a dimmer, monkey, and start a behavioral training regiment until the monkey learns to adjust the lights accordingly. I’ve found that this costs tens of dozens of dollars in monkeychow and typically just yields an angry primate that hates the Chipmunk Christmas Album. I want to take a more modern approach.
Enter the humble TRIAC: A mystical, magical bit of silicon that controls AC current just like a transistor controls DC… kind of. TRIACS can be triggered by running a small current through their gate at which point all of the current will flow between the other two pins. That current will keep flowing until you reach a zero crossing, ie. the current shifts from going in one direction to the other direction (it’s AC, remember). You then get a chance to stop/delay the flow of current again by keeping the gate voltage low. This "all on or all off" operation is why we call it triggering.
Take a look at the above diagram of a proud little AC waveform. It’s happily flying between positive and negative in bloody defiance of the electron’s gifts. If we put that current through a TRIAC, we can delay it at any point that isn’t a zero crossing. By delaying it for half of the time available we get this magnificent beast below:
In technical terms, the blinky blinks will get half the go juice and should be dimmer. Just like you now are having read the previous sentence. That being said, what we’re feeding this load looks like a dog’s dinner. I’m not entirely sure they can handle that hefty whomp of a vertical startup voltage, but whatever they’re $20 from Walmart so lets find out.
Lucky for me, there’s plenty of premade options available for anyone with a dream, a heart that can withstand a couple of shocks, and most importantly some money. I picked up this 8CH AC LED Light Dimmer Module Controller Board V3 from KRIDA Electronics out of Latvia:
|If you wire this thing up with the hot and the neutral wires in the correct positions as indicated on the PCB, the heatsinks for those TRIACs are live. The next time you reach over the board you’ll get a nice jolt of line voltage. I recommend swapping hot and neutral on the input, working one-handed, and wearing rubber shoes.
Anyway this has pins for VCC (3.3V or 5V), GND, sync, and channels 1-8. The sync pin goes high at every zero cross and the channel pins fire their corresponding TRIACs.
Firing a TRIAC precisely at a certain time period after registering the rising edge of a signal is a perfect use case for a microcontroller. So of course I chose to use a Raspberry Pi 3B+ and suffer the slings and arrows of using a multitasking OS to do a microcontroller’s job. They say that when you have a hammer, every problem looks like a nail. I’d add that when you have a Remington 476 Powder Actuated Fastening Tool you can put those nails through concrete.
So how much time do we have to do the things we need to? Well for a 60 Hz AC signal, you’ll get 8333 µS between crossings. But my LED lights couldn’t reliably hand anything clipped before 2083 µS or after 6250 µS from the zero crossing. This means we actually have some time in there where we won’t be triggering a TRIAC:
I want a daemon that reads stdin, takes commands, notes zero crossings, and triggers the TRIACs via GPIO pins accordingly. So my question was, "Is 2083 µS enough time to read user input, perform any calculations we need, and be ready to trigger up to 8 TRIACs?" In theory you could work through a zero crossing and net yourself 2083 x 2 µS, but you’d need to keep track of when that crossing occurs to get your timing right, which sounded like a headache to me.
Here’s the loop I came up with:
The trigger list is a perfect candidate for a linked list. It needs to be in order by delay and you’ll be inserting and removing things. Following my trend of doing the wrong thing, I ended up implementing it as an array with a marker for the end of the list. I didn’t want to spend time freeing and mallocing little bits of memory as the user updates the triggers. Likewise you can just iterate through an array without having to follow pointers, although honestly iterating through an array in C is following pointers anyway so I don’t know if there’s really much benefit.
Also, I made a lookup list for brightness level to delay time so you don’t have to do the calculations on the fly. Finally, I also try to keep as few triggers as possible. If you set a channel to 0 or 100 the TRIAC will simply be turned on or off instead of trigger at the start or end of its range.
The poll operation to test for user input seems sufficiently fast for this use case. Writing output is incredibly slow, but I’m not too concerned about that as the user can always just fix their mistakes on the input side and not get the error messages.
So the long and the short is you can run this all in one thread and it seems to perform OK. The penalties for missing a TRIAC firing are mostly blinking which still occurs occasionally. The worst case scenario is you don’t clear the GPIO pin by the next zero crossing, in which case you’ll get a cycle of full brightness.
As much as I did try to optimize this, ultimately it can still be preempted by another process and perform poorly.
As I stated in the beginning, good job for a microcontroller, not so much for an SBC.
no_preempt() section is in order, but that might be a step too far.