qmk / qmk_firmware

Open-source keyboard firmware for Atmel AVR and Arm USB families
https://qmk.fm
GNU General Public License v2.0
17.96k stars 38.59k forks source link

[Help] logarithmic backlight control #1756

Closed ivancuric closed 6 years ago

ivancuric commented 6 years ago

How would one go about implementing a logarithmic brightness control on an ATmega32u4? I've fiddled on my Arduino a bit, and came up with the following using an LED and a rotary encoder:

...
mapped = map(count, 0, steps, 0, 100);
reduced = mapped * 0.01;
eased = (pow(2, reduced) - 1) * 100;

ledmapped = map(eased, 0, 100, 0, 255);
analogWrite(ledPin, count);
...

Basically, mapping 100 steps to 255, but on a quadratic scale, so that the brightness has a perceptively linear scale. The current implementation has a way too sharp exponential curve.

I tested my implementation out and it works great, but I don't have the handy map function, plus the code doesn't seem well optimized.

I found the code responsible for the current implementation of the backlight in quantum.c: https://github.com/qmk/qmk_firmware/blob/master/quantum/quantum.c#L824

For instance, for 6 (+ off / 0) backlight levels (8 is max for some reason, even though the type is uint16_t), the resulting values are ​​​​​[ 0, 3, 63, 511, 8191, 65535 ]​​​​​, corresponding to the percentages of ​​​​​[ 0, 0, 0.10, 0.78, 12.5, 100]​​​​​.

What I came up with is

OCR1x =((level * level * 0xFFFF) - 0xFFFF) / (BACKLIGHT_LEVELS * BACKLIGHT_LEVELS - 1);

instead of the current

OCR1x = 0xFFFF >> ((BACKLIGHT_LEVELS - level) * ((BACKLIGHT_LEVELS + 1) / 2));

with the fancy bitwise operations.

This function gives a quadratic output of ​​​​​[ 0, 5617, 14979, 28086, 44938, 65535 ]​​​​​, however from values 1 < x <= BACKLIGHT_LEVELS. I need to tweak it to give that output for 0 < x <= BACKLIGHT_LEVELS

ivancuric commented 6 years ago

I can't math today, I have the dumb. Any ideas how to fix the new() output to go from 0x0 to 0xffff ?

#include <stdint.h>
#include <stdio.h>

#define BACKLIGHT_LEVELS 8

void main()
{
    uint16_t i;
    uint16_t orig(uint16_t level) {
        if ( level == 0 ) {
            return 0;
        } else if (level == BACKLIGHT_LEVELS) {
            return 0xFFFF;
        }
        return 0xFFFF >> ((BACKLIGHT_LEVELS - level) * ((BACKLIGHT_LEVELS + 1) / 2));
    }

    uint16_t new(uint16_t level) {
        return ((level * level * 0xFFFF) - 0xFFFF) / (BACKLIGHT_LEVELS * BACKLIGHT_LEVELS - 1);
    }

    printf("Orig\tNew\n-------------\n");
    for (i = 0; i <= BACKLIGHT_LEVELS; i++)
    {
        printf("%d\t", orig(i));
        printf("%d\n", new(i));
    }
}

Output:

Orig    New
-------------
0       fbf0
0       0
0       c30
0       2081
0       3cf3
f       6185
ff      8e38
fff     c30b
ffff    ffff

For now I've just added a lookup table:

uint16_t exp_table[] = {
  0x0,
  0x3ff,
  0xfff,
  0x23ff,
  0x3fff,
  0x63ff,
  0x8fff,
  0xc3ff,
  0xffff
};

And switched L861 to OCR1x = exp_table[level];

Works great: https://www.youtube.com/watch?v=5SQ2ObVLRU4

AleksandarDev commented 6 years ago

The formula should be: (x^2)*(map_max/(map_steps^2)) where map_max is max value required, map_steps is number of horizontal steps you have.

You can optimize this equation by caching/calculating (map_max/(map_steps^2)) value (since these never change)


return (level*level*0xFFFF)/(BACKLIGHT_LEVELS*BACKLIGHT_LEVELS); which is x^2 mapped from 0 .. 8 to 0x0000 - 0xFFFF (including edge values)

will give you following response&eqn2_color=2&eqn2_eqn=&eqn3_color=3&eqn3_eqn=&eqn4_color=4&eqn4_eqn=&eqn5_color=5&eqn5_eqn=&eqn6_color=6&eqn6_eqn=&x_min=0&x_max=8&y_min=0&y_max=65535&x_tick=1&y_tick=1000&x_label_freq=5&y_label_freq=5&do_grid=0&do_grid=1&bold_labeled_lines=0&bold_labeled_lines=1&line_width=4&image_w=850&image_h=525).

If you want the curve to be steeper, just increment exponential eg. (level*level*level*0xFFFF)/(BACKLIGHT_LEVELS*BACKLIGHT_LEVELS*BACKLIGHT_LEVELS) which is x^3 mapped to 0x0000 - 0xFFFF


I just saw

This function gives a quadratic output of ​​​​​[ 0, 5617, 14979, 28086, 44938, 65535 ]​​​​​, however from values > 1 < x <= BACKLIGHT_LEVELS. I need to tweak it to give that output for 0 < x <= BACKLIGHT_LEVELS

return (level * level * 0xFFFF) / (BACKLIGHT_LEVELS * BACKLIGHT_LEVELS);

This should work for you.

@wedranb

ivancuric commented 6 years ago

Thanks for the response! However the function gives out the final value as 0xFFC0 instead of 0xFFFF.

AleksandarDev commented 6 years ago

@ivancuric It's because you're clipping remainder from division (65535 is not dividable by 9 without remainder). Check 3rd paragraph for correct solution (rearranged brackets)

ivancuric commented 6 years ago

Ah, got it. Thanks a lot.

OCR1x = (level * level * 0xFFFF) / (BACKLIGHT_LEVELS * BACKLIGHT_LEVELS);

Gives good results in the console when printing out with %#X (0x0, 0x3FF, 0xFFF, 0x23FF, 0x3FFF, 0x63FF, 0x8FFF, 0xC3FF, 0xFFFF), but for some reason only works on 3 values when programming the board 0, 1, BACKLIGHT_LEVELS. Guess it has something to do with the types.

For some reason if I assign a hex value directly, eg 0xFFF, it works, but having it as a returned value from the function does not.

@AleksandarDev any ideas?

drashna commented 6 years ago

Ever finish this?

ivancuric commented 6 years ago

Nope, but @BalzGuenat and @jackhumbert fixed it in https://github.com/qmk/qmk_firmware/commit/4931510ad38aadb1769c9241bfad0c3d77ad687f! https://github.com/qmk/qmk_firmware/blob/master/quantum/quantum.c#L933

drashna commented 6 years ago

Ah, glad to hear it then! Specifically, fixed by #2187 (tagging for reference)