PWM - rosco-pc/propeller-wiki GitHub Wiki
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
- What is PWM in the first place?
- How to control a PWM channel with SPIN
- How to control two PDM channels using the timers/counters
- How to control A LOT of PWM channels
- Adding a low-pass filter makes a DAC!
... but maybe AFTER Xmas..
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 ("**<span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">pulse width modulation") is coded into the relative length of the pulse wrt the period.
The <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">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)
This is the definition "by the book". Note the beauty of it: It is not only <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">independent of the amplitude, making it immune against typical "noise" effects, but also <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">independent of the choice of the period, so avoiding a strict <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">synchronization of clock speeds.
In most situations however the period is a fixed design parameter, giving the pulse width (<span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">measured in <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">absolute units) a meaning of its own. So you speak of a "2ms pulse" when driving servos, <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">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:
- as (P)AM ("<span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">amplitude modulation"): The value of the signal voltage is modified - this is what we are accustomed to.
- as (P)FM ("frequency modulation"): The number of on/off signals within the time slot (the "frequency") is modified - old-fashioned modems worked that way.
- as PPM ("phase modulation"): The exact location of a small spike within the time slot is modified - this is very related to PWM, a <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">differentiated PWM signal as it were.
- as PDM ("density modulation"): A certain number of fixed length pulses ( "spikes" again!) is arranged within the time slot (=period). This is quite similar to PWM (where all those pulses are assembled at one side of the period, so to speak) but not exactly the same. It is the way we used the DUTY-mode of the Propeller timer/counter in the first example above.
A PDM signal is generally interpreted as a Bitstream of "ones" and "zeroes"; the decoded value being the (moving) average.
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.
- To control a servo (period: 20ms, pulse width: 1 .. 2ms)
- To control the average current transported to an inert device (light bulb, motor, the system LED/human eye,... )
- DAC: To generate a duty cycle proportional voltage, utilizing an appropriate low-pass filter (period < R*C, pulse width: 0.. period)
So "hands-on"! An LED is such a <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">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 <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">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 <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">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:
- In a fresh new COG the I/O characteristics have to be set again (DIRA in this case)
- We now provide an address rather than a value for the dutyCycle parameter
- We have to care for the <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">occurrence of "bad values" within the main loop
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 <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">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 <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-font-size: 11.0pt; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">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
- A timer keeps adding the FREQ <span style="color: black; font-family: "Arial","sans-serif"; font-size: 10.0pt; line-height: 115%; mso-ansi-language: EN-US; mso-bidi-language: AR-SA; mso-fareast-font-family: "Times New Roman"; mso-fareast-language: EN-US;">register to the PHS register each and every tick.
- The MSB (bit 31) of the PHS becomes connected to one of the I/O pins. Thats all, really! Think some time what different things you can do with this!
The PWM algorithm then goes like this:
- Set FRQ to 1
- Set PHS to the negative number of ticks for your pulse
- Start the timer
- Do this in a loop of the length of your period .. 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! '
(...coming soon) In the meantime have a look at Martin Hebels BS2 functions. This is exactly what we did in the intro example
(...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
(to be continued...)