Gamma and cosinus correction

A project log for LOLB

lots of lightbulbs

Moritz WalterMoritz Walter 09/01/2015 at 11:370 Comments

So, it's official, we're going after phase control. The electronical basics behind building a phase control circuits is quite simple. If you want to get into it check out the detailed documentation on the arduino website:

Basically, we'll kind of cut out power chunks out of each half wave of the mains power line, sorry for not putting that better, and the size of those chunks is our parameter for adjusting the brightness of the bulb. Triacs are used to do so. Triacs are a bit like transistors, just for AC, and they have the property to stay on after have been triggered until the current running through their terminals is dropping below a specific holding current, for AC that basically means they stay on until the end of the current half-wave.

So for dimming, we let the µC keep track the mains phase and register when it's crossing zero. Then we let the µC trigger the triac after a certain amount of delay time, the triac turns on and current starts to flow through the bulb until the half-wave ends. The delay between the zero crossing and triggering the triac is our parameter for adjusting the brightness of the bulb. If we set the delay to 0, current will flow through the bulb for the entire half wave, if we set it to 5000 µs (assuming 50 Hz mains frequency and 100Hz half-wave frequency), current will flow through the bulb for half of the half wave, which will basically result in the lamp beeing dimmed to 50% power output. If we wait forever and never trigger the triac, the bulb is plain off. This repeats 100 times a second, for every half-wave.

This project log however shall deal with a different problem that appears when using phase controlled dimming, which is that the timing delay between zero crossing and does not map linear to the perceived brightness of the light bulb. As a consequence, 8 bit timing control won't give you anything even close to 8 bit brightness control. Think of it like this: 1 µs of on-time at 23 V gives you about 100 times less power than 1 µs of on-time at 230 V. Since after the zero crossing, the sinoid voltage curve makes it through all the voltages within the duration of one half wave (10000µs), every µs of timing delay is weighted with a different increase of power output.

First, the voltage across the light bulb follows a sine wave, so the power output of the light bulb maps with a inverse cosinus function to the delay. I called this cosinus correction in the title.

And second, the brightness perception of the human eye is pretty much logarithmic, so an exponentially increasing light intensity will be perceived as a linear brightness increase. This can be fixed with a regular gamma correction.

And third, the light bulbs themselves will have their own power input / light output curve. I'll just asume that this is also a simple gamma curve, which makes it pretty much irrelevant. Since I'll implement a gamma curve I can simply choose a gamma that compensates for the logarithmic perception and the input/output curve of the light bulb.

So if we were to implement a function that turns our desired power output into a timing delay, it'd be as simple as that:

// power represents power values between 0 and 255, power to delay represents timing delays between 0 and 10000 µs, INTERVAL is the duration of one half wave in µs, PI is π
uint16_t power_to_delay(uint8_t power){
    return round(float(INTERVAL)*(acos(-1f+2f*float(power)/255f)/PI));
The next thing we have to take care about is the gamma curve:
// power and power_to_brightness represent the light intensity with values betwee 0 and 1, GAMMA is the desired gamma value between 0.0 and 1.0
uint8_t power_to_brightness(uint8_t power){
    return round(pow(float(power)/255f,GAMMA)*255f);
However, by combining the above functions, we get twice the rounding errors, better is having one function for both tasks:
uint8_t brightness_to_delay(uint8_t power){
    return round(float(INTERVAL)*pow((acos(-1+2*float(power)/255.0)/PI),GAMMA));
And because we want to save processor cycles during runtime, we put all that in a nice LUT:
// TRIGGER_LATEST is the latest moment we can trigger the triac within the halve wave, STEPS is the 
uint32_t intensity_to_time[255]
for(int i=1;i<255;i++){

And as a little reward for all that math, a little graph:

Note that is the inverse graph of the above function. Since we're more interested in calculating the timing delay from the desired brightness.