View Full Version : Hardware-only PWM output.

Phil Pilgrim (PhiPi)
01-09-2008, 12:44 PM
This should, in fairness, be considered a continuation of the thread "Adjustable Frequency/Duty with counters from Spin? (http://forums.parallax.com/showthread.php?p=637665)", since it builds on some of the ideas there. The topic is how to use the counters alone to get true pulse-width-modulated (PWM) output. PWM output, as opposed to the Propeller's native DUTY output, is very useful for driving inductive loads through a MOSFET, for example, something for which the DUTY output switches too quickly. There are software methods to produce PWM from the Propeller, which are examined in deSilva's tutorial (http://propeller.wikispaces.com/PWM). But to get PWM from the hardware alone has, to my knowledge, proven elusive. (Maybe it's been done, and I just missed it.)

In the thread cited above, it was noted that if you ORed two out-of-phase, but same-frequency clocks together, you could get duty cyles ranging from 50 to 100%. Since ORing is done internally to the Propeller, no external hardware would be required to get this kind of PWM output. Using XOR instead of OR, however, it's possible to get PWM output that ranges from 0 to 100%. In fact, an XOR gate is often used as the phase detector in phase-locked loops (PLLs). Here's an illustration:


There are three things to note here:

1. The top, or "master", clock is the same in all traces.

2. The output PWM value depends only on the relative phases of the master clock and the various other clocks.

3. The PWM frequency is twice the frequency of the other clocks.

Since the Propeller doesn't include an internal means to XOR two clock outputs, we shall have to resort to external means. So this is not a "Propeller-only" solution. But the external means are minimal. Quad XOR gates (e.g. CD4070, 74HC86, etc.) are small and cheap, and it only takes one to provide four PWM outputs.

From a programming standpoint, the objective was to do all the counter setup in Spin. Once set up, the counters would continue to output clocks of the proper phase for XORing, without further supervision — until it was time to change one of them. In the program below, one master clock can be XORed with multiple slave clocks for multiple PWM outputs. The master clock can exist in the main cog, in which case one additional slave clock is available in that cog for PWM output, or it can be started in a different cog (as in the demo code), freeing the main cog to have two PWM outputs. In addition, other cogs can share the master clock, so each can have two PWM outputs. In all, one master clock and 15 autonomous PWM slave clocks can be controlled from the Propeller's eight cogs.

Here's the program/object that does all this:


_clkmode = xtal1 + pll16x
_xinfreq = 5_000_000

init_phase = 208 'Spin delay to write PHSx.
add_phase = 286 'Spin delay to augment PHSx.


long master_pin 'Pin number of master clock output.
long pwm_frq 'Contents of ALL FRQx registers.
byte pwm_shift 'Amount by which to shift duty cyle left for PHSx.
long pwm_zero 'Amount to write to PHSx after master hi->lo for zero offset.
long pwm_max 'Maximum value for PWM duty cycle argument.
long pwm_phsa 'Current PWM duty cycle amount for CTRA.
long pwm_phsb 'Current PWM duty cycle amount for CTRB.

long stack[31] 'Stack for demo part of program.

PUB demo | i 'Demo program to setup and operate two PWM outputs.

cognew(demo_master_cog, @stack) 'Start the cog that outputs the master clock.
set_master(0, 13, 11) 'Tell this cog what pin and resolution the master clock is using.
set_slave("a", 14) 'Set PWMa to pin 14.
set_slave("b", 15) 'Set PWMb to pin 15.
repeat i from 0 to 2047
set_pwm("a", i) 'Set PWM duty cycle to i / 2048.
set_pwm("b", 2048 - i) 'Set PWM duty cycle to (2048 - i) / 2048.
waitcnt(cnt + clkfreq >> 11)
waitcnt(cnt + clkfreq)

PUB demo_master_cog 'Demo cog whose counter outputs the master clock.

set_master("a", 13, 11) 'Set master clock to CTRA, pin 13, 11 bits (0 - 2047) resolution (about 39KHz).
repeat 'That's all. Cog is free to do anything else.

PUB set_master(ctr, pin, bits) 'Setup the master clock.

' ctr: 0 = external clock, "a" = CTRA, "b" = CTRB
' pin: 0 - 31 = pin master clock outpus on.
' bits: 1 - 30 = PWM resolution. PWM frequency = clkfreq / (1 << bits).

master_pin := 1 << pin 'Set master_pin to mask of pin.
pwm_shift := 31 - bits 'Set pwm_shift to amount to shift duty-cycle for PHSx use.
pwm_max := 1 << bits - 1 'Maximum value of duty cycle (ANDed with arguments).
pwm_frq := $8000_0000 >> bits 'Amount to write to FRQx (master & slaves).
pwm_zero := init_phase << (32 - bits) 'Zero delay amount before write to PHSx.
if (ctr == "a") 'Using CTRA?
frqa := pwm_frq ' Yes: Set frequency and counter mode (NCO).
ctra := constant(%00100 << 26) | pin
dira[pin]~~ ' Set pin to output.
elseif (ctr == "b") 'Using CTRB?
frqb := pwm_frq ' Yes: Set frequency and counter mode (NCO).
ctrb := constant(%00100 << 26) | pin
dira[pin]~~ ' Set pin to output.

PUB set_slave(ctr, pin) | sync 'Setup the slave clock.

' ctr: "a" = CTRA, "b" = CTRB
' pin: 0 - 31

if (ctr == "a") 'Using CTRA?
frqa := pwm_frq ' Yes: FRQA same as all FRQx.
ctra := constant(%00100 << 26) | pin ' Set counter mode to NCO on pin.
waitpeq(master_pin, master_pin, 0) ' Wait for master high.
waitpeq(0, master_pin, 0) ' Wait for master high->low.
phsa := pwm_zero ' Set PHSA to be in phase with master clock.
dira[pin]~~ ' Set pin to output.
pwm_phsa~ ' Clear pwm duty variable for CLKA.
elseif (ctr == "b") 'Using CTRB?
frqb := pwm_frq ' Yes: FRQB same as all FRQx.
ctrb := constant(%00100 << 26) | pin ' Set coutner mode to NCO on pin.
waitpeq(master_pin, master_pin, 0) ' Wait for master high.
waitpeq(0, master_pin, 0) ' Wait for master high->low.
phsb := pwm_zero ' Set PHSB to be in phase with master clock.
dira[pin]~~ ' Set pin to output.
pwm_phsb~ ' Clear pwm duty variable for CLKB.

PUB set_pwm(ctr, value) | delta 'Set PWM duty cycle for counter.

value &= pwm_max 'AND duty cycle with maximum value.
if (ctr == "a") 'Using CTRA?
delta := ((value - pwm_phsa + add_phase) << pwm_shift) 'Yes: Compute change in PHSA.
phsa += delta ' Change PHSA by computed amount.
pwm_phsa := value ' Record new value.
elseif (ctr == "b") 'Using CTRB?
delta := ((value - pwm_phsb + add_phase) << pwm_shift) 'Yes: Compute change in PHSB.
phsb += delta ' Change PHSB by computed amount.
pwm_phsb := value ' Record new value.

The reason the master clock can be in a separate cog is that the slave clock routines do not need to read its PHSx register to synchronize to it. They just wait for the correct pin transistion instead to initialize the slave clock phases. After that, the slave clock phases are merely incremented relative to the last PWM value written, without further reference to the master clock. This makes nearly instantaneous phase changes possible, without waiting for an edge for synchronization.

One thing to note here is that the available PWM frequencies are limited to power-of-two divisions of the Propeller clock. Also the amount of resolution available for the duty cycle is inversely proportional to the PWM frequency. In fact, the resolution is used as the independent variable in the setup routines, with the PWM frequency falling out from it. With an 80MHz clock, and at a resolution of twelve bits (0 - 4097), for example, the PWM frequency would be about 19.5KHz. This is a nice frequency for many motor and solenoid drive apps, with plenty of resolution.

The code above has been tested, but not extensively; so there may be some boundary conditions I've missed considering. Also, there's no error checking. So if you tell the slave setup routine to look for a master clock where there isn't one, for example, it'll get hung up looking for it. In any event, this code could form the basis for more exotic PWM objects.


Fred Hawkins
01-10-2008, 11:38 AM
Phil, nice idea. I have two general questions --
1) how many steps can be distinguished between 0% and 100%?
2) do the end points in fact cancel? ie, exactly 0% or exactly 100%? Or would those end points be more simply hit by a off/on routine?

Phil Pilgrim (PhiPi)
01-10-2008, 12:15 PM
1. The resolution depends on the frequency (or vice-versa). At twelve bits resolution (0 - 4095) with an 80MHz clock, the PWM frequency is about 20KHz.

2. 0% is part of the natural range, but as it stands, there is no true 100%. With 12-bits resolution, for example, the highest duty cycle is 4095/4096. However, I was using a rather slow CD4070 for the XOR; and at this duty cycle, it couldn't react to a 12.5ns low pulse so was, in reality, 100%. It wouldn't be difficult to program 100% as a special case, though.


01-10-2008, 12:27 PM
Phil Pilgrim (PhiPi) said...
It wouldn't be difficult to program 100% as a special case, though.

That would make sense, since 100% isn't technically a "pulse".

Nice job, BTW!

Post Edited (MarkS) : 1/10/2008 5:41:35 AM GMT

01-11-2008, 02:10 AM
At least it will foil the selfclocking feature of PWM: When you are interested in the duty cycle only,
the receivers of PWM data need not know the period in advance!