Most of us begin our experiments with microcontrollers by blinking an LED; this is the microcontroller version of the “Hello, World!” program that is introduced in traditional computer programming training. On the P2, we can blink an LED like this:
main() repeat pinhigh(LED) waitms(250) pinlow(LED) waitms(750)
This is satisfying and fun because it’s a quick program and easy to understand. That satisfaction begins to fade, however, because the LED can’t. Fade, that is. At some point we seek the ability to go beyond simple on and off state control to variable brightness control.
The mechanism typically used to control the brightness of an LED is called PWM (Pulse Width Modulation). In our blink example the LED was either always on or always off. If we want an intermediate brightness, we need to have it partially on; this is the purpose of PWM.
In this figure the on-time portion of the waveform is 30% of the entire cycle (on-time plus off-time). The ratio of on-time to cycle-time is called the duty cycle.
Going back to our blink example, we are alternating the duty cycle between 0% (always off) and 100% (always on).
With the waveform above, the LED is on 30% of the time, which means it will not be as bright as when it is fully on. This is why PWM is used; it provides brightness control of the LED using a simple mechanism. This works when the cycle time is fast enough so that our eyes integrate the on- and off-times into a singular brightness level. This same effect, called persistence of vision, is what allows our mind to convert a series of still images into a “moving picture.”
The P2’s smart pin circuitry allows any pin to be configured as a PWM output. Before we can configure the pin, there is a decision: the PWM frequency. This is the number of times per second that the LED turns on and off when set between the 0% and 100% endpoints. If the PWM frequency is too low, the LED may appear to flicker when that is not the intention. If the PWM frequency is too high, the LED or external driver circuity (e.g., for high-power LEDs) may not work properly. I tend to start with 1000Hz when experimenting. For specialty projects that involve high-speed photography or video, the PWM frequency may be higher.
Finally, we need to set the value provided to the smart pin that commands the LED run at 100% duty cycle. For standardized designs, that value is 255 (maximum value of a byte). In the lighting control world, the DMX-512 protocol is used to transmit a ‘universe’ of bytes to lighting fixtures. We’ll stick with the lighting pros and use 255 here.
That leads us to configuring the LED pin for PWM operation. The first step is to select the PWM mode (sawtooth is the easiest to implement) and to make the smart pin an output with the P_OE constant. The output enable flag is required because in smart pin mode, the pin direction bit is used to enable or disable the smart pin.
m := P_PWM_SAWTOOTH | P_OE
The next steps are a little more involved, but not difficult. The high word of the smart pin X register holds the value that will set the output to 100% duty cycle. As discussed, we will use 255.
x.word := 255
Finally, the low word of the smart pin X register holds the number of system ticks in one unit for the desired PWM frequency. This takes a little bit of math, but, again, is fairly straightforward.
x.word := 1 #> ((clkfreq / hz) / 255) <# $FFFF
It works out like this: the system clock frequency (clkfreq) is divided by the desired PWM frequency (hz); this gives us the number of system ticks in one PWM period. That is divided by the number of units in 100% (255) to get the number of system ticks in one unit. The #> and <# operators constrain the value to a legal 16-bit number for the low word of X.
The last step is to start the smart pin.
pinstart(LED, m, x, 0)
At this point the LED will be off. The fourth parameter of pinstart() is the Y register which holds the current level; in our setup this will be 0 (0%) to 255 (100%). To change the LED brightness at any time we can write to the smart pin Y register like this:
This will set the PWM output to 50%.
Of course, we don’t want to have to remember all of this each time need an LED with brightness control, so let’s wrap it into an object.
There are other considerations for LEDs when we go beyond the basics. The previous code assumed that the P2 pin was connected to the LED anode (+). We call this the common cathode configuration because multiple LEDs on this same project will have the cathode side in common. We could, however control the cathode side of the LED; this is called the common anode configuration. These diagrams shows the difference in connections.
The challenge we face when using PWM is that the common anode configuration requires a mathematical inversion of the PWM value used with the common cathode configuration. There is an easier way: The P2 pin configuration allows the output to be inverted. Using pin output level inversion, we don’t have to manipulate the PWM value; 0 to 255 is 0% to 100% brightness for either configuration.
And then we get to the apparent LED behavior versus the control value. After configuring the LED pin for PWM output we would expect to be able to loop through the values 0 to 255 and see a corresponding change in brightness with the LED. Yes, the LED will change brightness, but no, what we perceive with our eyes will not match our expectations.
Have a look at this graph. The black line is the control value, 0 to 255, to the PWM pin. The blue line is how our eyes perceive the LED brightness. As the graph plainly illustrates, what we see is always brighter than what we expect, and the behavior is not linear.
Thankfully, the correction is very simple: instead of using the direct values, 0 to 255, we can look up a gamma (perceived brightness) value from a table. Now the command curve is bent, and the apparent brightness is linear across the control range.
The object jm_gamma8.spin2 holds the gamma correction table and a method for converting a standard level value to its gamma-corrected level.
The new object, jm_led.spin2 encapsulates the requirements for controlling an LED from a P2 pin:
- P2 pin used
- Connection type (common cathode or common anode)
- PWM frequency
The object also has a link to the gamma correction object so that we can “fix” LED brightness output if required. The startx() method handles these details. Note the use of P_INVERT_OUTPUT when the connection type is common anode.
pub startx(pin, ctype, hz) | m, x led := pin m := P_PWM_SAWTOOTH | P_OE if (ctype <> C_CATHODE) m |= P_INVERT_OUTPUT x.word := 1 #> (clkfreq / hz) / 255 <# $FFFF x.word := 255 pinstart(led, m, x, 0)
The attached demo program gives a few examples of using the LED with basic control with the on() and off() methods (heartbeat, Morse output), and advanced control with the set() method providing the target level and [optional] gamma correction (throb, candle, heartbeat).