Shop OBEX P1 Docs P2 Docs Learn Events
Can someone please explain propeller PLL-based PWM theory to me? — Parallax Forums

Can someone please explain propeller PLL-based PWM theory to me?

WossnameWossname Posts: 174
edited 2011-12-14 10:33 in Propeller 1
I know the general idea of PWM - a means to approximate an analog signal by toggling an output pin in a carefully governed pattern.

But I am finding it difficult to understand how to correctly write some PASM code to make a Cog generate a variable duty cycle PWM square wave using the built-in PLL counter system.

I've had a good read through of the relevant sections in both the Propeller manual and the datasheet, which both give lengthy descriptions of what all the registers DO, but I'm damned if I can find a real-world example of how to use them in a practical way.

For example, how do you make the Cog output a square wave of 35% duty cycle at 128KHz (rising edge to rising edge)? And then how do you change the duty cycle to 71%? These numbers are arbitrary of course but I can find no clues in the docs to suggest how this is done.

I don't want code, I just need a layman's terms explanation of how to set up the Prop to do this. My brain is starting to hurt and I'm getting datasheet blindness. Is a kind soul willing to write a few paragraphs to help me out or does anyone know a novice-level tutorial that covers all the salient topics?

I've gotten pretty far through the feature set of the Propeller up to now and this is one of the last obscure features (apart from VGA output!!!) I want to understand, and it seems to me that the manual/datasheet could REALLY use a decent set of code examples and a better procedural plain-text description of how to really use the PLL and (in particular) PWM capabilities of the Prop platform.

Any and all help gratefully received, thanks for reading.

Adam.

Comments

  • MagIO2MagIO2 Posts: 2,243
    edited 2011-12-13 02:27
    You'd simply setup a counter to count and tie it to a pin. What happens is that the counter somewhen will have bit 31 (or an overflow bit - depends on the counter setup) set to 1 which is directly influencing the output pin. But that alone would only toggle the pin.
    The point is that there is no PWM-mode for the counters. For PWM you also need the COG to do some stuff. It is waiting for one period (1/125kHz in your case) and after that has to update the PHSA register.
    With 0% duty you'd choose a value for PHSA with which the counter does not reach $8000_0000 in your period - that value of course depends on the FREQA and PLL settings. With 100% you'd directly set PHSA to $8000_0000.

    With the PLL setup and FREQA you can influence the resolution of your PWM.

    Hope this helps at least a bit ;o)
  • Christof Eb.Christof Eb. Posts: 1,237
    edited 2011-12-13 03:35
    Hi Adam,
    here:
    http://forums.parallax.com/showthread.php?89958-Propeller-Education-Kit-Labs-Tools-and-Applications
    you can download the "Propeller Education Kit Labs" and starting page 121 you will find some info and examples how to use the counters. To get PWM with fixed duty cycle you will need a cog with some code in addition to a counter, see page 157.
    Best regards Christof
  • WossnameWossname Posts: 174
    edited 2011-12-13 03:52
    @MagIO2:I think I understand what you mean. CTRMODE %00110 ("DUTY single-ended") would output the PHSA-Carry bit every clock cycle right?

    So I'd have to pick a value for FRQA that over repeated additions, causes that carry bit to toggle at the duty cycle I want?

    Is there actually any benefit to using the PLL counter functionality to generate variable duty cycle PWM, when I'd have to make the Cog do such a lot of supporting math? Wouldn't it just be more sensible to do something like...


    (pseudocode...)

    [PHP]:loop
    set pin high
    sleep for (35/100) * pwm_frequency //35% duty cycle
    set pin low
    sleep for (65/100) * pwm_frequency
    jmp loop[/PHP]
  • Mike GMike G Posts: 2,702
    edited 2011-12-13 05:06
    I found the Counters Application notes very helpful.
    http://www.parallaxsemiconductor.com/an001

    Sigma Delta Analog to Digital Conversion
    http://www.parallaxsemiconductor.com/an008

    Propeller Education Kit Labs Tools and Applications
    http://forums.parallax.com/showthread.php?89958-Propeller-Education-Kit-Labs-Tools-and-Applications
    Find the "Transmit Square Wave Frequencies" section.
  • MagIO2MagIO2 Posts: 2,243
    edited 2011-12-13 05:07
    There is no big math and you have the whole period to calculate! And .... in your example you will never have real 0% and real 100% without adding some more instructions (if-statements). And ... it's not as accurate as using the counters thus you cant reach the same accuracy. Up to a certain PWM frequency you can do the whole thing in SPIN without loosing accuracy because accuracy is guaranteed by the counter. You only have to be sure that the SPIN code finishes before end of period.

    In the end your approach also eats up a whole COG as a resource, so why not do it with a counter having 12,5ns accuracy?

    Actually the code is pretty easy:
    1. setup the counter and calculate the clock-ticks once that you have to wait to match your PWM frequency
    2. optionally do some other tasks here
    3. fetch the duty cycle from HUB-RAM and calculate the next value to be moved to PHSA in 5.
    4. waitcnt ' this will also calculate the next cnt-value to wait for
    5. update PHSA
    6. goto 2

    For 3.: 80_000_000 / 125 000 gives you the range you have to subtract from $8000_0000. (0-640 in this case) So, 0 means 100% duty and 640 means 0% duty.
    The range itself is the value you have to use in the waitcnt to add up.

    A range of 640 means that at 125kHz you have a resolution of a little bit more than 8 bits.
  • kuronekokuroneko Posts: 3,623
    edited 2011-12-13 05:09
    @Wossname: you may find this object useful.
  • WossnameWossname Posts: 174
    edited 2011-12-13 07:18
    Thanks for elaborating, MagIO2, that seems more comprehensible to me than the manual/datasheet.

    I'll do some experimenting this evening and reappear with my results and code if successful.
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2011-12-13 08:55
    As you know, the NCO mode of the counters produces a symmetrical square wave, and you can set up both counters in one cog to run at the same frequency. By fine adjusting the phase, you can have any desired amount of overlap. By directing the output of the two counters to the same pin, and and by fine tuning the phase from 0% to 50%, the output will have a duty cycle from 50% down to 0% low, or to say the same thing, from 50% to 100% high. Constant frequency chosen independently. This comes about due to the wiring of multiple output sources on the propeller, wired OR. With an external XOR gate, this can achieve 0% to 100% duty cycle, as a control input to the XOR provides the invert/non-invert when the object's methods are called to change the duty cycle. It is also possible to make an extra COG counter perform the conditional inversion, in which case an external XOR is not needed, but that approach is a bit awkward from the programming standpoint. The object that Kuroneko pointed out is an implementation this two or three COG counter approach.
  • WossnameWossname Posts: 174
    edited 2011-12-13 12:12
    I am starting to feel like a real moron now. Been looking at this for hours and now I'm basically reduced to randomly trying things and thus it's time to step back and ask for help again. I just don't understand the datasheet at all on this topic, I must be really dumb.

    My code below is not even remotely sensible and it doesn't work at all. I get random patterns out of it. The hardware I'm using to try this is "known good", so no worries on that score.

    Please can someone have a look at this code and tell me how to make it work. I can't even work out the blasted PWM frequency for this thing and I'm beginning to lose my mind with frustration.

    I just want it to output ANY duty cycle that isn't 50%.


    [PHP]CON
    _clkmode = xtal1 + pll16x
    _xinfreq = 5_000_000


    PUB Main

    cognew(@PWM_SETUP, 0)


    DAT

    org 0
    PWM_SETUP mov dira, #0
    or dira, LED


    mov FRQA, #256
    mov PHSA, #0
    mov CTRA, CTRMODE_VAL 'starts the PLL counter *now*'

    mov Time, cnt
    add Time, #9
    :loop waitcnt Time, Delay
    mov PHSA, #0
    jmp #:loop


    Delay long $8000_0000 / 256
    LED long |< 0


    CTRMODE_VAL long %0_00100_000_00000000_000000_000_000000


    Time res 1[/PHP]


    Going rapidly insane.
  • MagIO2MagIO2 Posts: 2,243
    edited 2011-12-13 15:06
    Some code I found for doing PWM for a RGB LED.

    You have to call it with an address pointing to a variable that holds the duty.
    org 0
    entry1
                  mov       dira, #%11100000
    
                  mov       ctra, ctraval1
                  mov       time1, period1
    
                  add       time1, cnt
                  mov       frqa, #1
    
    loop1         rdlong    phsa, par
                  waitcnt   time1, period1
                  jmp       #loop1
    
    ctraval1       long      %00100 << 26 + PIN_B
    period1        long      65536
    
    time1          res       1
    
  • kuronekokuroneko Posts: 3,623
    edited 2011-12-13 17:57
    Wossname wrote: »
    My code below is not even remotely sensible and it doesn't work at all. I get random patterns out of it. The hardware I'm using to try this is "known good", so no worries on that score.
    Just a few things:
    • SPIN arithmetic is signed, meaning $8000_0000 / 256 is not what you really want, try $8000_0000 / -256 or $8000_0000 >> 8
    • Delay is setup for a frequency of N Hz, your counter for N/2 Hz, so the output gets reset before it can even change to 1
    Regarding frequency calculations check the latest counter AppNote. For NCOs it comes down to frqx/232*clkfreq (page 6). In your example we end up with 256/232*80M = 4.76Hz. Have a look at this and see if you can make it work for you:
    CON                                  
      _clkmode = XTAL1|PLL16X
      _xinfreq = 5_000_000
     
    VAR
      long  parameter
      
    PUB main | n
    
    ' Our [COLOR="red"]chosen frequency[/COLOR] (PASM) is about 1kHz (78125 cycles @80MHz).
    '
    ' The theory here is that we initialise the counter phsx register to
    ' a negative value which causes the output to go high. Then the counter
    ' simply counts up (undoing the "damage"). So let's say we told it to
    ' go back by 40000. This means we have high for 40000 cycles and low for
    ' the remaining 38125 cycles, then the update will force high again.
    '
    ' Which also means going back by 78125 will hold the output high for the
    ' whole period and 0 will give you constant low output.
    
      cognew(@entry, @parameter)
      waitcnt(8192 + cnt)                                   ' startup delay
    
      repeat parameter from 0 to 78125 step 625
        waitcnt(clkfreq/10 + cnt)
    
      repeat parameter from 78125 to 0 step 625
        waitcnt(clkfreq/10 + cnt)
      
    CON
      pin = 16
      
    DAT             org     0
    
    entry           mov     dira, diraval           ' drive output
    
                    movs    ctra, #pin
                    movi    ctra, #%0_00100_000     ' NCO
                    mov     frqa, #1
    
                    [COLOR="red"]rdlong  period, #0              ' get clkfreq
                    shr     period, #10{/1024}      ' ~1kHz[/COLOR]
                    mov     time, cnt
                    add     time, period
    
    :loop           rdlong  value, par              ' get update
                    waitcnt time, period            ' sync to period
                    neg     phsa, value             ' (re)start count at -phsa
                    jmp     #:loop                  ' repeat
    
    diraval         long    |< pin
    
    time            res     1
    value           res     1
    period          res     1
    
                    fit
                    
    DAT
    
  • AribaAriba Posts: 2,690
    edited 2011-12-13 17:59
    With a few changes in your code the output is a 12% pulse (and can be any width from 0 to 100%):
    CON                                  
      _clkmode = xtal1 + pll16x
      _xinfreq = 5_000_000
        
     
    PUB Main 
     
      cognew(@PWM_SETUP, 0)
     
       
    DAT 
                             
                  org       0                
    PWM_SETUP     mov       dira, LED
    
                  mov       FRQA, #1
                  mov       CTRA, CTRMODE_VAL 'starts the PLL counter *now*'
                                   
                  mov       Time, cnt       
                  add       Time, #9
    :loop         waitcnt   Time, Delay
                  neg       PHSA, PulsWidth
                  jmp       #:loop
             
     
    Delay         long      8000     '10kHz PWM frequency (80MHz / 10kHz)
    PulsWidth     long      1000     '1/8 pulswidth (range 0..8000)
    LED           long      |< 0
    
    
    CTRMODE_VAL   long      %0_00100_000_00000000_000000_000_000000
    
    Time          res       1
    
    Basically you have an Assembly loop with a fixed frequency (10kHz here) and at the begin of every loop a one-shot pulse from a counter is started. The PulsWidth register defines the length of the pulse and the Delay register defines the length of the PWM periode.

    Andy

    Edit: You can also do a PWM in Spin only. This makes it much simpler to pass the values (just use global variables):
    CON                                  
      _clkmode = xtal1 + pll16x
      _xinfreq = 5_000_000
    
      PIN = 0  
     
    VAR
      long  periode, pulswidth
      long  stack[8]
      
    PUB Main 
     
      periode := clkfreq / 10_000
      cognew(PWM, @stack)
    
      repeat
        pulswidth += 1                    'increase pulswidth from 0 to 100%
        if pulswidth > periode
           pulswidth := 0
        waitcnt(clkfreq/2000 + cnt)
     
    
    PRI PWM : time
      ctra := %00100<<26 + PIN
      frqa := 1
      dira[PIN] := 1
      time := cnt
      repeat
        phsa := -pulswidth
        time += periode
        waitcnt( time )
    ' 
    
  • MagIO2MagIO2 Posts: 2,243
    edited 2011-12-13 23:03
    Hmmm ... without having the possibility to check it at the moment I'd have some doubts.

    First of all I'd move the phsa:=-pulswidth directly behind the waitcnt and maybe let the "-" operation been done by the code that sets pulswidth. My expectation is that due to the many SPIN-operations you have in between waitcnt and and setting the phsa (repeat, -pulswith, :=) you introduce some unlinearity in the low duty-cycle range. At least with the fact that the SPIN-COG always has to synchronize with the HUB-timing you will loose some precision, right?

    And of course the SPIN is more limited in the max. PWM-frequency. I'm not sure if the frequency of 128kHz can be achieved by SPIN.
  • AribaAriba Posts: 2,690
    edited 2011-12-14 05:41
    It does not really matter at which point in the loop the counter-pulse is started, as long as the point is always the same. So if it takes always the same amount of cycles to loop and to calculate the " -pulswidth" in Spin then this is equal to the Assembly version. If not then ONE puls is a liittle longer or shorter (some nanoseconds) at the moment you change the pulswidth, but then it is stable again until the next change.
    For sure you can not reach 128kHz PWM frequency, I would not go over 20kHz with Spin.
    The ideal PWM frequency depends on the application, and the resolution you want.

    Andy
  • WossnameWossname Posts: 174
    edited 2011-12-14 08:09
    Again, many thanks for all your help everyone. I'll try out your suggestions tonight.

    I'll put my findings on my blog when I get all this working, I bet there are others out there who are having similar trouble as I did.
  • WossnameWossname Posts: 174
    edited 2011-12-14 10:33
    Yes!! Thank you Andy, I used your version of my code and now I understand what's going on.

    It seems obvious now, I didn't even get close to this from the info in the datasheet/manual. Perhaps it's an area that would benefit from some clarification for slow-witted folk like myself. :D

    Anyway, I'm going to adapt this code to generate audio waveforms through an op-amp into a speaker!
Sign In or Register to comment.