Pulse Width Modulation (PWM) for LED Dimming

We have finally arrived at the part of the lesson where we can put all of this clock theory to good use and make pretty lighting displays in the real world!  In the past lessons, our LEDs were either on or off, and they blinked on or off at a programmed rate.  We will now use a very fast rate of blinking with the use of hardware timer PWM functions to actually dim the LEDs, and then change the rate of PWM blinking to fade the LEDs over time.   Finally, we will perform this fading across multiple LEDs in order to create an LED chaser.


If we were to dim an LED by reducing the source voltage for the LED, this could create a change in color and would not be a consistent degree of fading across all LEDs.  The proper way to dim an LED is by turning it on or off very quickly so that the human eye cannot see the blink rate.  The frequency of the PWM waveform is not important as long as it is faster than the human eye can see and not too fast that the LED fails to reach its saturation voltage.  The duty cycle of the PWM waveform is used to set the brightness of the LED.



Various Duty Cycle Waveforms


We could create all of these PWM waveforms through ordinary GPIO sets in firmware as we have used previously to turn on or off the LEDs, but setting GPIOs in firmware takes a lot of time and is vulnerable to the same software-timing issues mentioned previously in this lesson.  For example, take this small snippet of code, which is the quickest possible way to control GPIOs in firmware.  I am using the memory mapped method to set and clear the GPIO, which avoids function calling overhead.

      GPIO_PinModeSet(LED_PORT, LED_PIN, gpioModePushPull, 0);
      uint64_t timer = get_time_in_ms();
      for (int i= 0; i < 10000; i++)
            GPIO->P[LED_PORT].DOUTSET = 1 << LED_PIN;
            GPIO->P[LED_PORT].DOUTCLR = 1 << LED_PIN;
      timer = elapsed_ms(timer);

The value found in the timer variable after the for loop completes is 19ms to run this simple GPIO set and clear loop 10,000 times.  That is equivalent to 10,000 cycles / .019s = 526315Hz or ~ 0.5MHz.  That is orders of magnitude greater frequency than we need for LED dimming, but much slower than the hardware is capable of driving.  Remember, the hardware can run at many times that frequency, so controlling things at the hardware level strictly from firmware will always run slower than dedicated hardware peripherals.


Rather than control the LED dimming strictly from firmware, I will configure hardware timers to control the GPIOs directly.  We must first write firmware to initialize the timer and the PWM GPIO functions, and then the hardware automatically repeats the sequence forever, freeing your firmware to do other things.  We don’t need that faster hardware rate for our LED dimming project, but there are other applications that would benefit from the faster switching rate.


Let’s configure the peripheral timer to PWM a GPIO and set up a programmable duty cycle to be output to the TEST_LED0 on the Starter Kit.  If you recall, TEST_LED0 is on port E, pin 2.  I need to make sure that there is a timer that can be configured to output to PE2.  By looking at the Data Sheet for the Wonder Gecko that is on our kit, I find this GPIO is routed from TIMER3, Channel 2, on Location 1:



Data Sheet Selection of Timer Channel and Location for PE2 Control


Here is the code to set up and run this hardware timer-based dimmer to the TEST_LED0, forever, with no software control beyond the hardware configuration:

#define LED_PORT  gpioPortE
#define LED_PIN   2
#define TIMER_TOP                   100
#define DUTY_CYCLE                  1
#define TIMER_CHANNEL               2
int main(void)
      CMU_ClockEnable(cmuClock_GPIO, true);
      CMU_ClockEnable(cmuClock_TIMER3, true);
      // Enable LED output
      GPIO_PinModeSet(LED_PORT, LED_PIN, gpioModePushPull, 0);
      // Create the timer count control object initializer
      TIMER_InitCC_TypeDef timerCCInit = TIMER_INITCC_DEFAULT;
      timerCCInit.mode = timerCCModePWM;
      timerCCInit.cmoa = timerOutputActionToggle;
      // Configure CC channel 2
      TIMER_InitCC(TIMER3, TIMER_CHANNEL, &timerCCInit);
      // Route CC2 to location 1 (PE3) and enable pin for cc2
      // Set Top Value
      // Set the PWM duty cycle here!
      // Create a timerInit object, based on the API default
      TIMER_Init_TypeDef timerInit = TIMER_INIT_DEFAULT;
      timerInit.prescale = timerPrescale256;
      TIMER_Init(TIMER3, &timerInit);
      while (1)

The LED PWM frequency is found by dividing the natural frequency of the timer (which is based on the source clock and the divider of the timer) divided by the timer Top value.  I wanted my PWM switching frequency to be above 200Hz so that there is no noticeable flicker.  I also wanted the duty cycle to be easily set in 1% increments.  In order to do that, I had to play around with a spreadsheet calculation until I came up with the following settings:


  • Timer Clock Divider: 256, results in a timer natural frequency of 14 MHz / 256 = 54.7kHz
  • Timer Top Value: 100, by choice
  • LED PWM Frequency = 54.7kHz / 100 Top value = 547Hz

This gives me the >200Hz LED PWM frequency that I wanted while also giving me 100 for the Top value, which allows for 1% PWM increments.


The duty cycle of our PWM GPIO output is produced by the timer compare registers, listed in the Reference Manual as TIMERn_CCx_CCVB, yikes!  This crazy acronym stands for “Counter Control on Channel X Capture/Compare Value Buffer” register.  I am using the buffered version because the timer will wait until a PWM period is completed before making the change to the compare registers, resulting in glitch-free operation.  Once the CNT value in the timer is greater than this long-named compare register, the PWM mechanism will switch off the configured output.  Therefore, by writing a zero to this register, the output will never be asserted high.  By writing a value of 100 or greater to this register, the output will always be high.  And by programming this value to anything between zero and 100, the register will hold the output high until the CNT value matches the compare value, and then switch off the output for the remainder of the Top period of 100. This is how we can specify the exact PWM duty cycle in 1% increments.


If you execute this code on your kit, you will notice that the test LED does not glow as brightly as before.  You can experiment with 0% all the way to 100% and see how it affects the brightness of the LED. 


Programmatically Fading LEDs

Now that you can programmatically dim an LED using a hardware timer, the next step in our LED chaser is to create a programmable rate of change to the dimming effect on a single LED to produce a fading effect.  We already have the ability to dim an LED to any value.  In order to fade the LED, we must change the level of dimming on a regular rate.  So it is a rate on top of another rate.  This reminds me of Calculus!


The LED PWM frequency is 547Hz (from the above math), which means that each PWM cycle completes in 1/547 = 1.8ms.  If we were to smoothly ramp from zero (off) to 100 (full on) at 1% step size, it would take about 180ms to reach full brightness.  It would take another 180ms to ramp it back down to zero brightness.  Therefore at the lower limit we can fade the LED through almost three complete cycles per second with 1% resolution.  We could fade the LED through even more cycles if we picked a larger step size.  I am going to assume that we don’t need to cycle more than three times per second and keep the step size at 1%.  This means that all of the durations for this particular solution must be greater than 180ms.


To produce a generic fader, the following figure should describe everything that I must implement in firmware.



The following code implements the figure shown.  Tack this onto the end of the existing code.  See the source documentation for a full code listing.

#define RAMP_UP_TIME_MS             500
#define RAMP_DOWN_TIME_MS           700
#define HIGH_DURATION_MS            1000
#define LOW_DURATION_MS             500
#define MAX_BRIGHTNESS              100
#define MIN_BRIGHTNESS              0
      enum mode_values { RAMPING_UP, HIGH, RAMPING_DOWN, LOW};
      // Check for properly sized constants
      uint16_t delta = MAX_BRIGHTNESS - MIN_BRIGHTNESS;
      if ( delta == 0 || RAMP_UP_TIME_MS % delta 
||RAMP_DOWN_TIME_MS % delta) { DEBUG_BREAK } // Set the initial condition uint16_t mode = RAMPING_UP; uint32_t time_step = RAMP_UP_TIME_MS / delta; uint16_t brightness = MIN_BRIGHTNESS; TIMER_CompareBufSet(TIMER3, TIMER_CHANNEL, brightness); uint64_t mode_timeout = set_ms_timeout(RAMP_UP_TIME_MS); while (1) { switch (mode) { case RAMPING_UP: delay_ms(time_step); brightness++; TIMER_CompareBufSet(TIMER3, TIMER_CHANNEL, brightness); if (expired_ms(mode_timeout)) { mode = HIGH; mode_timeout = set_ms_timeout(HIGH_DURATION_MS); } break; case HIGH: if (expired_ms(mode_timeout)) { mode = RAMPING_DOWN; time_step = RAMP_DOWN_TIME_MS / delta; mode_timeout = set_ms_timeout(RAMP_DOWN_TIME_MS); } break; case RAMPING_DOWN: delay_ms(time_step); brightness--; TIMER_CompareBufSet(TIMER3, TIMER_CHANNEL, brightness); if (expired_ms(mode_timeout)) { mode = LOW; mode_timeout = set_ms_timeout(LOW_DURATION_MS); } break; case LOW: if (expired_ms(mode_timeout)) { mode = RAMPING_UP; time_step = RAMP_UP_TIME_MS / delta; mode_timeout = set_ms_timeout(RAMP_UP_TIME_MS); } break; } } }

This code gives you the ability to set constants to specify how long the LED should take to go from off to full LED brightness or vice versa, as well as set the maximum and minimum brightness.   Note that because I am using a millisecond-based timer and I am dividing the fader parameters by 100 using integer arithmetic, the fader will work best if the fader parameters are in even multiples of the delta = MAX_BRIGHTNESS – MIN_BRIGHTNESS range.  If I were to program a value of 950 for the ramp up duration with MIN_BRIGHTNESS at zero and MAX_BRIGHTNESS at 100, for example, the step size would be set to 950 / 100 = 9ms, which would not fully complete the brightness sweep and things would go off course. 


I wrote this code as a sequential program rather than as fully interrupt-driven program to keep things simpler.  Keep in mind that the smooth performance of this dimmer will be impacted by anything else that takes time away from adjusting the timer compare values.  It also cannot take advantage of sleep states, so it will waste power.  I will sometimes write code with while loops as a sequential program to get things working and then later rewrite it as an interrupt-driven program.


Try it out on your Starter Kit and play around with the values to see how you can change how it glows.



  • Blog Posts
  • Makers
  • If we were to dim an LED by reducing the source voltage for the LED, this could create a change in color and would not be a consistent degree of fading across all LEDs. 

    LEDs are current devices, NEVER try to control a LED by voltage. 



    This gives me the >200Hz LED PWM frequency that I wanted

    an oddity here. 120+HZ is enough for indicators, but for lighting (just so you know) you need 1kHz+ or headaches occur.