PWM and Visual Brightness

When you want to control a light with a microcontroller

UPDATE: I’ve made an online version of the script too. So if you just want to get the values, go there & use it directly in your browser.

For my microcontroller projects I often want to convert some linear value (like a potentiometer position) into a light intensity. Since our eyes aren’t linear, this requires some math, and generally microcontrollers aren’t that good in math.

Our eyes roughly experience a doubling of the amount of light as a single “step”. Double it again, and we perceive it as a similarly-sized step. This means that, for our eyes to perceve the light becoming brighter linearly, it actually needs do that exponentially.

Exponentially growing light power, with a little linear start
10-bit Power levels produced by my script. The linear start is hard to see, but it helps in controlling the light.

My microcontrollers use either a 10-bit or 8-bit value to contol the brightness. This provides a bit of a technical hurdle, because the exponential growth of the light’s brightness actually starts out really slow. Converting that slow start to a sequence of integers would produce a sequence like 0 0 0 1 1 2 3 5. Those repeated numbers mean that whatever you use to control the light (like up/down buttons) will appear to do nothing at first. To prevent this, my script will simply start with linearly increasing numbers, like 0 1 2 3 4 5, before moving on to an exponential slope.

Using the script

UPDATE: I’ve made an online version of the script too. So if you just want to get the values, go there & use it directly in your browser.

The script needs Python to run. Download sybrens_light_intensity_script.py, save it, edit it in a text editor to adjust the parameters to your needs, and run it. It will output a few lines of C code that can be used for microcontrollers.

Example 10-bit output

This is what will be printed with the default settings of the script:

  • 10 bit intensity values
  • 30 steps in total
  • 6 linear steps to start with
static const uint16_t light_map_10bit[] = {
    0, 1, 2, 3, 4, 5, 7, 9, 11, 14, 18, 22, 28, 35, 43, 54, 67, 83, 102,
    126, 156, 192, 237, 293, 361, 444, 548, 674, 831, 1023};

Example 8-bit output

This is what will be printed with some modified settings.

  • 8 bit intensity values
  • 30 steps in total
  • 4 linear steps to start with
static const uint8_t light_map_8bit[] = {
    0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 12, 15, 18, 21, 24, 29, 34, 40, 47, 54,
    64, 74, 87, 102, 118, 138, 161, 188, 219, 255};

How I use it

The values in the light map produced by my script are fed to a Pulse Width Modulator. These simply count up until their maximum (1023 for a 10-bit one) and then restart at zero again. When they start counting, they turn on the light, and when they go past a given value, they turn off the light again. The higher the value, the longer the light stays on, and the brighter you see it. This happens all thousands of times per second, so to our eyes the light is stable, even though it’s actually rapidly blinking.

dr. Sybren A. Stüvel
dr. Sybren A. Stüvel
Open Source software developer, photographer, drummer, and electronics tinkerer

Related