PWM
Title:PWM
Author:khakiisu
Published:Sat, 03 Oct 2009 15:21:24 GMT

Pulse Width Modulation

The topic of PWM is complex - and trivial at the same time.
When you have the need to dim an LED connected to I/O = ledPin between dimPercent = 0 to 99%, do just this:
DIRA[ledPin] := 1
CTRA := %0_0110<<26 + ledPin
FRQA := $7FFF_FFFF/50 * dimPercent
In the following sections we shall discuss
  1. What is PWM in the first place?
  2. How to control a PWM channel with SPIN
  3. How to control two PDM channels using the timers/counters
  4. How to control A LOT of PWM channels
  5. Adding a low-pass filter makes a DAC!

... but maybe AFTER Xmas..


What is PWM in the first place?



Someone "doing PWM" will generate some of the signals shown in the following sketch. You notice that they are digital wrt ("with respect to") amplitude. They can also be discrete wrt the time axis. A signal transports information. The information with PWM ("pulse width modulation") is coded into the relative length of the pulse wrt the period.
The quotient between both is called duty cycle, which is a value between 0 and 100% (or between 0 and 1, when you are using REAL numbers)
PWM-A.jpg

This is the definition "by the book". Note the beauty of it: It is not only independent of the amplitude, making it immune against typical "noise" effects, but also independent of the choice of the period, so avoiding a strict synchronization of clock speeds.

In most situations however the period is a fixed design parameter, giving the pulse width (measured in absolute units) a meaning of its own. So you speak of a "2ms pulse" when driving servos, independent of the "duty cycle" or period.

In both cases the receiver can have problems with duty cycles of exactly 0 or 100%, so theses values should be avoided.

So the information transported consists of a series of values (the "duty cycles", or the absolute "pulse widths") within (generally) fixed length time slots (= each "period"). Such kind of information can also be transported in many other ways:

Each of those modulations has its pros and its cons wrt to noise immunity, ease of transmission ("encoding"), ease of receiving ("decoding"), cost,....

In "microcontrolling" we use PWM (and also PDM) mainly for three reasons.
  1. To control a servo (period: 20ms, pulse width: 1 .. 2ms)
  2. To control the average current transported to an inert device (light bulb, motor, the system LED/human eye,... )
  3. DAC: To generate a duty cycle proportional voltage, utilizing an appropriate low-pass filter (period < R*C, pulse width: 0.. period)



How to control a PWM channel with SPIN



So "hands-on"! An LED is such a useful device; one should invent it if it wasn't already there. The brightness of an LED is roughly proportional to the current flowing through it. According to this current the LED controls its voltage drop. It is always close to the forward voltage, a little bit higher with high current, a little bit lower with low current.

We can easily construct a Current-DAC by - say - connecting the LED with three Propeller pins, each with a different resistor, as 220 Ohms, 470 Ohms, 1k, e.g. So we can readily provide different currents (20 mA ... 2mA), but at the cost of three pins.

The more common solution however is to pulse the LED, e.g. 50 ms off, 50 ms on, using the full output of one single I/O pin. The signal will look exactly as the middle situation in the above sketch. The LED will now shine with half its intensity. So we hope.

DIRA[0] := 1
dim_try1(0,50)                      ' LED is connected to I/O 0 '
 
 
PUB dim_try1(ledPin, dutyCycle) | onePercentTick, deadline
' version 3.1 2007 by deSilva '
' Dims an LED, dutyCycle is 0 to 100, ledPin is 0 to 31 '
 
  IF dutyCycle =< 0
     OUTA[ledPin] := 0
     RETURN
  IF dutyCycle =>100
     OUTA[ledPin] := 1
     RETURN
 
  deadline := CNT
  onePercentTicks := CLKFREQ/1000      ' 1ms = 1%'
  REPEAT
    deadline += onePercentTicks*100   ' 100 ms loop'
    WAITCNT(deadline)
    OUTA[ledPin] := 1
    WAITCOUNT(deadline + onePercentTicks*dutyCycle)
    OUTA[ledPin] := 0

Hey, it works! However...
(a) It flickers!
(b) The routine DIM_TRY1 never returns!

The period has to be adjusted to the application! We are sending zeros and ones - so we "see" zeros and ones. To trick our eyes we have to be faster, like a true magician. Have you already spotted the relevant parameter? Right, it's 1000, setting 1% of a period to 1ms (thus the period to 100 ms). We can readily change this to 10_000, making the period 10 ms which will suffice to do the trick.

Can we make it even faster? Try it out!
There are limits with SPIN: We are waiting for a time gap of "onePercentTicks" for a 1% dutyCyle; this must be >800, which means around a 1 kHz period. Sorry folks, that's simple SPIN . We will do much better soon, with a little help from a timer/counter.

It is typical for Propeller programming to have routines that can never return, as they have to be on the alert for the environment. We often call those routines "drivers". Other microcontrollers use "Interrupts" for this; the Propeller way is to engage a COG.
SUB main | dutyCycle, someStack[20]
  COGNEW(dim_try2(0, @dutyCycle), @someStack)
 
  REPEAT
    REPEAT WHILE ++dutyCycle <100
      WAITCNT(CNT+CLKFREQ/100)
    REPEAT WHILE --dutyCycle >0
      WAITCNT(CNT+CLKFREQ/100)
 
We have to make some modifications to our routine:
PUB dim_try2(ledPin, dutyCycleAddr) | onePercentTicks, deadline, dutyCycle
' version 4.0 2007 by deSilva '
' Dims an LED, dutyCycle is 0 to 100, ledPin is 0 to 31 '
 
  DIRA[ledPin] := 1
  deadline := CNT
  onePercentTicks := CLKFREQ/10_000      ' 100µs = 1% '
  REPEAT
    dutyCycle := 0 #> LONG[dutyCycleAddr] <# 100
    deadline += onePercentTicks*100   ' 10 ms loop'
    WAITCNT(deadline)
    IF dutyCycle
       OUTA[ledPin] := 1
       IF dutyCycle < 100
          WAITCOUNT(deadline + onePercentTicks*dutyCycle)
          OUTA[ledPin] := 0

---> I left a BUG here, for the gentle reader to spot :-)

Now, onto the last part of this section. Isn't it a shame that we have to constrain ourselves to pulses of at most 10 µs within a period of 1 ms? The main issue will most likely be not the period, but the accuracy of the pulse length. Having 100 choices only would mean an angular accuracy of 3.6° for a servo.
Do we really need assembly language to overcome that?

Not at all! There is something much better in every COG: a timer (even two A and B - see next section).
The working of the timers is thoroughly explained in Parallax AN001 (and most likely somewhere here in the wiki soon...link?)
It's simplicity itself: In their NCO mode
Thats all, really! Think some time what different things you can do with this!

The PWM algorithm then goes like this:
.. and here comes the code..
PUB dim_try3(ledPin, dutyCycleAddr) | onePercentTicks, deadline, dutyCycle
' version  2007 by deSilva '
' Dims an LED using TMRA, dutyCycle is 0 to 100, ledPin is 0 to 31 '
 
  DIRA[ledPin] := 1
  deadline := CNT
  onePercentTicks := CLKFREQ/10_000      ' 100µs = 1% '
  REPEAT
       dutyCycle := 0 #> LONG[dutyCycleAddr] <# 100
       WAITCNT(deadline += onePercentTicks*100)   ' 10 ms loop'
' ---- this part contains the improvement using a timer:                        '
'  Programming timerA for PWM-mode ("NCO")                                      '
'  i.e. pulse = sign bit (bit 31); thus preset register with MINUS pulse width  '
       CTRA := 0      ' reset timer                     '
       FRQA := 1      ' adding 1 @ system clock = 80 MHz'
       PHSA := -(onePercentTicks * dutyCycle)
       CTRA :=  (%0_00100 << 26) + ledPin
' Now there is a all the time of the world to do other things ....              '
' Note that it is not very critical as the pulse is reset automatically!        '
 


How to control two PDM channels using the timers/counters



(...coming soon)
In the meantime have a look at Martin Hebels BS2 functions. This is exactly what we did in the intro example



How to control A LOT of PWM channels



(...coming soon)
In the meantime please refer to some forum-threads, e.g. http://forums.parallax.com/forums/default.aspx?f=25&m=227109&g=229281



Adding a low-pass filter makes a DAC!



PWM-B.jpg

(to be continued...)