Shop OBEX P1 Docs P2 Docs Learn Events
Tachyon's 32 channel PWM as an arbitrary or analog waveform generator — Parallax Forums

Tachyon's 32 channel PWM as an arbitrary or analog waveform generator

Peter JakackiPeter Jakacki Posts: 10,193
edited 2015-07-11 23:11 in Propeller 1
I was just thinking that since the 32 channel PWM uses a 256 long table to store its PWM samples and that since the table is read at an almost 2MHz rate that it would be an easy thing to add resistors to the port pins as ladder DACs (one or more) of variable width. Doing this means it's easy then to generate all kinds of waveforms simply by accessing the table.

Using a lower frequency will yield very smooth audio waveforms etc. At present there are a few words that calculate and write the values to the table whenever you set the duty cycle you want on those channels.
30 2 SETPWM \ Set P2 to 30/256 duty cycle
75 % 3 SETPWM \ Set P3 to 75 % duty cycle
50 % 8 16 SETPWMS \ Set P8..P16 to 50 % duty cycle

This has also meant that I can downgrade the resolution of individual channels and increase the update rate accordingly so that turning that pin into a 4-bit PWM results in >120kHz update rate.
4 16 1 SETPWMW \ Set P1 with a duty of 4/16 so that it repeats every 16 PWM cycles (>120KHz update)

Now I can add modes to specify which pins form a ladder DAC and generate sine waves etc, without affecting the other channels, some can be 8-bit PWMs, some faster 6-bit, and some analog waveforms. The waveforms can even be set from a table loaded into from SD if needed.

Some possible ways of specifying an analog waveform
256 16 23 SINWAVE \ Generates a sine wave over the full 256 samples on P16..P23
16 4 8 COSWAVE \ generates a cosine wave over 16 samples onto P4..P8
noise 64 16 23 WAVE \ copy 64 values (at a time) from "noise" table (just an address in memory) to P16..P23

When I have some time I will set it up and generate some sine/cosine waves alongside 8-bit/4-bit PWMs etc

BTW, this is all interactive so I can type and change it on the fly.

Comments

  • frank freedmanfrank freedman Posts: 1,983
    edited 2014-03-05 14:40
    The DAC part of your post is pretty much what I did when I had more time to play with my ADC stuff. The design was taking in samples from an MCP3201 and turning the value right around after shifting the sample to the pins I wanted to use. These pins were then applied to an R2R array of fairly matched 220k resistors feeding into an single supply op amp, It was working well up to 100Khz sample rate for a 50Khz max input signal. The limitation was the input side. Pictures of the setup and waveforms are in the thread Fun deviations into ADC/DAC. The R2R was later replaced by a single multiplying DAC.

    It seems to me that if your table is preloaded for a repetitive waveform, 10Mhz or so just moving the table value to the outa and incrementing the table pointer should be doable. maybe a small glitch when the table pointer rolls over to do it again. Then again, 256 points may be a bit ugly at lower frequencies. Using twelve bits to address a pre-loaded table in an external ram whose output goes to an R2R DAC may give a more accurate waveform from having many more points to reconstruct per unit time.

    Oh for more time to play........
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2014-03-05 15:34
    The DAC part of your post is pretty much what I did when I had more time to play with my ADC stuff. The design was taking in samples from an MCP3201 and turning the value right around after shifting the sample to the pins I wanted to use. These pins were then applied to an R2R array of fairly matched 220k resistors feeding into an single supply op amp, It was working well up to 100Khz sample rate for a 50Khz max input signal. The limitation was the input side. Pictures of the setup and waveforms are in the thread Fun deviations into ADC/DAC. The R2R was later replaced by a single multiplying DAC.

    It seems to me that if your table is preloaded for a repetitive waveform, 10Mhz or so just moving the table value to the outa and incrementing the table pointer should be doable. maybe a small glitch when the table pointer rolls over to do it again. Then again, 256 points may be a bit ugly at lower frequencies. Using twelve bits to address a pre-loaded table in an external ram whose output goes to an R2R DAC may give a more accurate waveform from having many more points to reconstruct per unit time.

    Oh for more time to play........

    I don't think it's possible to do 10MHz at all even if the table were loaded into cog ram as a mov+incptr+outa+jmp gives a maximum 5MHz update rate. The PWM routine however reads from hub ram which can be modified on the fly by another cog so hub access slows things down a lot, however kuroneko rewrote the whole PWM and increased the read rate from 1.28MHz to almost 2MHz.

    The main point about using the PWM this way is that it is a very versatile module because you can use your PWMs in the usual way on any and as many pins as you choose and also you can increase the update rate by decreasing the resolution on any pin without affecting the resolution on other pins. Being able to throw a few resistors on it so to speak means you can have analog waveform generators for peanuts. In this regards I find it very easy to use smd resnets as they normally four individual resistors and it's very easy to build cheap and compact R2Rs with these.
  • JonnyMacJonnyMac Posts: 9,107
    edited 2014-03-05 16:06
    however kuroneko rewrote the whole PWM and increased the read rate from 1.28MHz to almost 2MHz.

    Peter,

    I'd love to see that in stand-alone form. Is it available, or do I have to (attempt to) unwind it from the Tachyon source? ;)
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2014-03-05 16:30
    JonnyMac wrote: »
    Peter,

    I'd love to see that in stand-alone form. Is it available, or do I have to (attempt to) unwind it from the Tachyon source? ;)

    It's just as easy to see the source document but here's a link to the bookmarked webpage document which gets republished automatically from the source document. The formatting on the original is better but you need to sign in.
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2014-03-05 18:47
    I've only been testing eight of the possible channels so far but to demonstrate the flexibilty of a table based approach I have set 8 channels with varying duty cycles. The minor division on the trace are 100us.
    PWM32.png

    The bottom channel is running at 25% duty cycle but at four times the main PWM frequency, so that's 30.4kHz. Now the two channels up above it are out of phase with the other pwm channels so that might be useful for a dead band etc.
    387 x 330 - 10K
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2014-03-07 05:47
    I didn't have time to do a video the other day but I have thrown a quick intro together showing how PWM is started up and how we can make changes. Now I am running 18 channels and displaying this on the logic analyser.

    [video=youtube_share;QKN7ggWGnQk]

    Here's the sample code I used to demonstrate it:
    [FONT=courier new]DECIMAL
    ( STEP 1 - allocate memory for a table somewhere )
    TABLE pwms 1024 BYTES    \ allocate a 256 longs table for the PWM map    
    
    ( STEP 2 -specify a frequency )
    7600 PWMFREQ        \ Set the frequecy
    
    ( STEP 3 - specify the outputs and the address of the table and start the PWM cog )
    \ Start up PWM using from P0..7, P16..P23 for 16 channels    
    %00000110_00000000_11111111_11111111 pwms RUNPWM32        
    
    ( STEP 4 - set the duty cycle for indivdual pins or groups of pins )
    25 % 0 31 SETPWMS
    10 #P4 SETPWM        \ set P4 to 10/256 duty 
    50 % 2 SETPWM        \ set P2 to 50 % duty
    
    1  25 SETPWM
    255 26 SETPWM
    
    
    
    \ Read bytes from memory and set channels
    : LOADPWM ( addr -- )  32 0 DO DUP I + C@ I SETPWM LOOP DROP ;
    \ $FF80 LOADPWM
    
    
    \ Just set all 32 channels to a random level
    : RNDPWM  32 0 DO 256 0 GETRND I SETPWM LOOP ;
    
    
    \ create an easy alias for SETPWM
    : PW  SETPWM ;
    
    
    \ pwms DUP 128 + $3C0 <CMOVE 
    [/FONT]
    
  • D.PD.P Posts: 790
    edited 2014-03-07 07:23
    I didn't have time to do a video the other day but I have thrown a quick intro together showing how PWM is started up and how we can make changes. Now I am running 18 channels and displaying this on the logic analyser.

    [video=youtube_share;QKN7ggWGnQk]

    Here's the sample code I used to demonstrate it:
    [FONT=courier new]DECIMAL
    ( STEP 1 - allocate memory for a table somewhere )
    TABLE pwms 1024 BYTES    \ allocate a 256 longs table for the PWM map    
    
    ( STEP 2 -specify a frequency )
    7600 PWMFREQ        \ Set the frequecy
    
    ( STEP 3 - specify the outputs and the address of the table and start the PWM cog )
    \ Start up PWM using from P0..7, P16..P23 for 16 channels    
    %00000110_00000000_11111111_11111111 pwms RUNPWM32        
    
    ( STEP 4 - set the duty cycle for indivdual pins or groups of pins )
    25 % 0 31 SETPWMS
    10 #P4 SETPWM        \ set P4 to 10/256 duty 
    50 % 2 SETPWM        \ set P2 to 50 % duty
    
    1  25 SETPWM
    255 26 SETPWM
    
    
    
    \ Read bytes from memory and set channels
    : LOADPWM ( addr -- )  32 0 DO DUP I + C@ I SETPWM LOOP DROP ;
    \ $FF80 LOADPWM
    
    
    \ Just set all 32 channels to a random level
    : RNDPWM  32 0 DO 256 0 GETRND I SETPWM LOOP ;
    
    
    \ create an easy alias for SETPWM
    : PW  SETPWM ;
    
    
    \ pwms DUP 128 + $3C0 <CMOVE 
    [/FONT]
    

    Really Neat! Thanks for the video
  • JonnyMacJonnyMac Posts: 9,107
    edited 2014-03-07 09:52
    Thanks for the video, Peter -- now I understand how the code works and made a Spin version (for my own edification). I don't know that the Spin version (i.e., Spin interface to PASM code) is practical, though, because it takes ~10ms to update the table for a channel value (unless I'm really missing the mark here).
    pub set(pin, level) | mask, idx
    
    '' Sets pwm pin to level (0..255)
    
      mask := 1 << pin                                              ' create mask for pin
    
      repeat idx from 0 to 255                                      ' update map for pin 
        if ((level == 0) or (idx > level))
          pwmmap[idx] &= !mask
        else
          pwmmap[idx] |= mask
    
  • kuronekokuroneko Posts: 3,623
    edited 2014-03-08 01:59
    For completeness, the once per hub window solution. Nothing between 41 and 16 cycles (except maybe 32).
    CON
      _clkmode = XTAL1|PLL16X
      _xinfreq = 5_000_000
      
    VAR
      long  outputs, data[256]
      
    PUB null
    
      longfill(@data[128], -1, 128)
    
      data[-1] := $00FF0000
      cognew(@entry, @data{0})
      
    DAT             org     0
    
    entry           add     addr, par               ' @data[-1]
                    rdlong  dira, addr              ' set direction
    
    {optional}      mov     cnt, cnt                ' fetch and store reference count
    {optional}      wrlong  cnt, addr               ' transfer starts 2nd hub windows
                                                    ' after this (for sync'd updates)
                    
                    add     addr, oneK              ' par - 1 + 1024
    
    ' data buffer is sampled backwards due to sub #7/djnz approach
    
    loop            rdlong  outa, addr
                    sub     addr, #7
                    cmp     addr, par wz            ' reached beginning of buffer?
                    rdlong  outa, addr
            if_e    add     addr, oneK              ' par     + 1024
                    djnz    addr, #loop             '     - 1
    
    oneK            long    1024
    addr            long    -1
    
                    fit
                    
    DAT
    
  • Peter JakackiPeter Jakacki Posts: 10,193
    edited 2014-03-10 15:39
    JonnyMac wrote: »
    Thanks for the video, Peter -- now I understand how the code works and made a Spin version (for my own edification). I don't know that the Spin version (i.e., Spin interface to PASM code) is practical, though, because it takes ~10ms to update the table for a channel value (unless I'm really missing the mark here).

    Even in TF itself this operation would take around 1ms but I have a small PASM module loaded into the TF cog which takes 100us, certainly not a problem for control purposes. I think if you make the Spin loop count down that it is more efficient but if an application needed many fast changes to the speed the standard brute force loop in Spin might not be suitable and instead I would look at a tracking method which would only make the necessary incremental changes.


    BTW, I drafted this post the other day and forgot to send it.
Sign In or Register to comment.