Shop OBEX P1 Docs P2 Docs Learn Events
Propeller DUTY D/A issue — Parallax Forums

Propeller DUTY D/A issue

BradCBradC Posts: 2,601
edited 2009-09-24 19:43 in Propeller 1
This has been driving me absolutely mental for weeks now and I've finally tracked it down.

If you still have vaguely youthful hearing and a good pair of headphones, run this code on a demoboard.

CON
  _clkmode      = xtal1 + pll16x
  _xinfreq      = 5_000_000

PUB Go
  dira := 1<<10 + 1<<11
  ctra := %00110 << 26 + 10
  frqa := $8003_0000
'  ctrb := %00110 << 26 + 11
'  frqb := $8000_0000

  repeat




You would think that as frqa never changes, you should never see a change in the output level. As the demoboard D/A output is coupled by a couple of 1uF caps to the headphone amp what you *should* get is absolute silence + what I would have thought was perhaps a very small hiss (it is being modulated after all).

What you actually get is a crystal clear tone.
Try values from $8003_0000 to $8008_0000 and see how the tone changes.

This also works on the negative side of analogue 0 here and means that as you reproduce a small analogue signal that crosses 0, you get a massive amount of harmonic content injected on top of your signal that manifests itself as noise.

If you use both counters it gets far worse as they appear to interact with one another.
Uncomment the counter b lines above to double the volume of the noise, even though counter b is set to $8000_0000, which in a solo counter creates no noise at all.

Have I done something completely wrong here?
I can't be the first to notice this, so I figure I'm doing something dumb.

I've been chasing this for weeks now on my proto-board, so I thought it might be something my hardware was doing, but I can reproduce it clear as a bell on a stock demo board, so maybe it's not my hardware after all.

Now the reason this has been driving me nuts is my D/A cog runs both counters as independent D/A units. I accept a signed input, scale it and add $8000_0000 to it to give me a nicely biased analogue signal. As my input signals disappear towards zero they get this noise superimposed on them which comes out has horrid harmonic distortion.
Is there a better way to achieve what I'm trying to do? (2 separate 16 bit D/A outputs)

▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
lt's not particularly silly, is it?

Comments

  • LawsonLawson Posts: 870
    edited 2009-09-19 04:03
    Just a recap, DUTY mode of the counters adds FRQx to PHSx every clock cycle and sets the given pin equal to the carry bit. So with 32-bits setting FRQx to the hex value of $8000_0000 should result in a carry every other clock cycle. What if we FRQx is set to $8003_0000? The $8000_0000 part still causes a carry every other cycle, but now the $3_000 part causes an extra carry every $1_0000_0000 / $3_000 cycles. With an 80Mhz clock this would show up as inverting the output pulse train at a ~3.7 Khz frequency. With a 40Mhz pulse train, inductive coupling of adjacent pins is quite reasonable. I'd guess starting a second counter next door with a FRQx of $8000_0000 next door adds an amplitude modulation signal as the pure and phase modulated signals add and subtract.

    Two potential solutions spring to mind. Reset your zero signal value to some other value less prone to harmonic generation, say 2/5th i.e. $6666_6666 or some prime number. Alternatively an anti-aliasing noise signal could be added to the lower FRQx or PHSx bits. Something as simple as XORing CNT with the lower 16-17bits of FRQx might work. Using an actual random number generator is likely to work better. Here is a List of RNGs from Wikipedia, and a bloody fast RNG I just coded in ASM myself. (Look in the external link provided by wikipedia, it's got a simpler version of the RNG on the top of page 4)

    My two bits,
    Lawson

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Lunch cures all problems! have you had lunch?
  • BradCBradC Posts: 2,601
    edited 2009-09-19 07:50
    Hrm, so I had the thought last night about adding dither to the low 16bits of frqa. But in reality it did nothing to the perceptible distortion. I figured this *must* have been bumped up against sometime in the past, and sure enough in Chips StereoSpatializer object there is a programmable dither generator for precisely this reason. I had to oversample by 4x to make it vaguely acceptable though. It's still very noisy and there is quite a bit of low level distortion.

    I'll just order some external DAC's..

    Makes me wonder about the amount of distortion people are getting when they use the duty D/A for playing WAV files..

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    lt's not particularly silly, is it?
  • LawsonLawson Posts: 870
    edited 2009-09-19 15:06
    Have you tried adding dither to PHSx? I have a hunch the whole 32-bits of PHSx could be XOR'd with a random number every time a sample is copied to FRQx without creating audible distortion. With a FRQx of $8003_0000 and scrambling PHSx i bet the probability of the extra carry is still $1_0000_0000 / $3_0000 but now the pattern of when the extra carry happens should be completely destroyed.

    Crazy ideas R'us
    Lawson


    *edit* I think 16-bits precision at audio frequencies is beyond the capabilities of DUTY mode counters at 80Mhz. To get 16-bits of precision the output low pass filter needs to average at least 2^16 clock cycles of output. I.e. the cut off frequency of the output filter would have to be 1.22Khz or lower. (12 to 13-bit precision looks much more reasonable)

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Lunch cures all problems! have you had lunch?

    Post Edited (Lawson) : 9/19/2009 4:47:25 PM GMT
  • lonesocklonesock Posts: 917
    edited 2009-09-19 20:23
    Lawson said...
    ///The $8000_0000 part still causes a carry every other cycle, but now the $3_000 part causes an extra carry every $1_0000_0000 / $3_000 cycles. With an 80Mhz clock this would show up as inverting the output pulse train at a ~3.7 Khz frequency....
    Please forgive my ignorance here, but I noticed that typical DAC code simply updates the FRQx register...what would happen if immediately afterward you also reset the PHSx register to 0? Assuming that you are updating your sample at ~22kHz or so, you should be resetting this 3.7kHz inversion before it ever happens. Hopefully any glitch caused by resetting PHSx every time would be above the cutoff freq of the low-pass RC on the output.

    (Sorry I can't just test this, I don't have a demo board...I'm currently doing a quick protoboard solder-up to hear this for myself.)

    Jonathan

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    lonesock
    Piranha are people too.
  • Tracy AllenTracy Allen Posts: 6,664
    edited 2009-09-19 20:55
    Here is another thread where the issue of jitter and subharmonic content in the NCO output came up early on, acute at higher frequencies.
    High frequency distortion?

    Brad, is this the same project as the noisy A/D performance? There I think you concluded that it would take an external A/D, right? Now an external D/A too.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    Tracy Allen
    www.emesystems.com
  • BradCBradC Posts: 2,601
    edited 2009-09-20 03:09
    Tracy Allen said...
    Here is another thread where the issue of jitter and subharmonic content in the NCO output came up early on, acute at higher frequencies.
    High frequency distortion?

    Thanks Tracy, that is precisely the problem and I now see there is really no practical resolution to it.
    Tracy Allen said...

    Brad, is this the same project as the noisy A/D performance? There I think you concluded that it would take an external A/D, right? Now an external D/A too.

    Yeah it is unfortunately.
    I'm seeing how minimal I can get for a guitar effects processor. I had hoped I'd be able to get as minimal as a propeller, eeprom and quad op-amp as for what I'm doing I could get away with fairly low resolution if it was clean, but I appear to need an external A/D and D/A to get anything vaguely clean and linear. It's only an extra 2 8 pin chips + a voltage reference, but it's still an extra $6 in parts and board realestate. If I'm adding a pair of converters I may as well add a high speed/resolution codec instead (which I was hoping to avoid).

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    lt's not particularly silly, is it?
  • Phil Pilgrim (PhiPi)Phil Pilgrim (PhiPi) Posts: 23,514
    edited 2009-09-20 05:08
    Brad,

    I've heard some really clean-sounding audio from the Demo Board, so I'm betting your issues can be resolved without resorting to an external D/A. You need to make sure that "analog zero" is $8000_0000. If your digital input signal varies from that for some reason, it's easy enough to program a low-pass filter to compute a moving average. Each new frqa would then become:

    ····frqa := $8000_0000 + instantaneous_signal - moving_average

    -Phil

    Edit: Said phsa, meant frqa.

    Post Edited (Phil Pilgrim (PhiPi)) : 9/20/2009 8:05:54 AM GMT
  • BradCBradC Posts: 2,601
    edited 2009-09-20 05:57
    Phil Pilgrim (PhiPi) said...
    Brad,

    I've heard some really clean-sounding audio from the Demo Board, so I'm betting your issues can be resolved without resorting to an external D/A. You need to make sure that "analog zero" is $8000_0000. If your digital input signal varies from that for some reason, it's easy enough to program a low-pass filter to compute a moving average. Each new phsa would then become:

    ····phsa := $8000_0000 + instantaneous_signal - moving_average

    -Phil

    My analogue zero is $8000_0000. I use a very long average low pass filter to be sure of that (and I spit the actual values out to the TV to verify what I'm hearing on the converter). As the audio crosses around $800[noparse][[/noparse]12345]_0000 and the other side of the zero it incurs harmonic distortion.

    I even fed it a synthesized sine wave at 0.5HZ, and still got audible noise around the zero cross.

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    lt's not particularly silly, is it?
  • Phil Pilgrim (PhiPi)Phil Pilgrim (PhiPi) Posts: 23,514
    edited 2009-09-20 06:43
    BradC said...
    I even fed it a synthesized sine wave at 0.5HZ, and still got audible noise around the zero cross.
    Well, of course you would. A 0.5Hz sine wave is the kind of thing I was thinking should be filtered out completely. The idea is to move through those $800x_000 values quickly without lingering long enough to hear anything. A moving average "analog zero" is basically just a high-pass filter. If you set the cutoff frequency high enough, I'm betting the problem will go away.

    Can you post some simple example code that runs on the Demo Board to illustrate this phenomenon with real audio?

    -Phil
  • BradCBradC Posts: 2,601
    edited 2009-09-22 10:17
    Phil Pilgrim (PhiPi) said...
    BradC said...
    I even fed it a synthesized sine wave at 0.5HZ, and still got audible noise around the zero cross.
    Well, of course you would. A 0.5Hz sine wave is the kind of thing I was thinking should be filtered out completely. The idea is to move through those $800x_000 values quickly without lingering long enough to hear anything. A moving average "analog zero" is basically just a high-pass filter. If you set the cutoff frequency high enough, I'm betting the problem will go away.

    Can you post some simple example code that runs on the Demo Board to illustrate this phenomenon with real audio?

    -Phil

    Difficult to set the cut off frequency high enough when you have low frequency components that go down below 110HZ and slowly drop in amplitude to analogue zero. You tend to spend quite a bit of time in that nasty portion of the waveform.

    Here's some code that generates a nice clean(ish) sine wave at about 130HZ. It slowly steps the volume down by effectively right shifting the sine values (ie left shifting them less) prior to bias adjustment and remains relatively clean until it gets down to the lower volume levels, where the DAC noise becomes quite clearly audible.

    Not the greatest test in the world, but clearly demonstrates the noise I'm on about (unless of course I've bolloxed the code and it's generating some weird interference at low volume levels). Short of digitising some low frequency guitar waveforms and putting a huge DAT block in this is the best I can probably manage.

    CON
      _clkmode      = xtal1 + pll16x
      _xinfreq      = 5_000_000
    
    PUB Go
      cognew(@lfo_sine,0)
    
    DAT
                  org       0
    lfo_sine
                  mov       ctra, lfo_setup
                  mov       dira, lfo_dirval
                  mov       lfo_acc, #0
                  mov       lfo_timer, cnt
                  add       lfo_timer, lfo_step
                  mov       lfo_r1, lfo_bias
                  mov       lfo_shift, #10
    :loop
                  waitcnt   lfo_timer, lfo_step
                  mov       frqa, lfo_r1
    
                  add       lfo_acc, lfo_incr
                  and       lfo_acc, lfo_sinmask
                  mov       lfo_sin, lfo_acc
                  call      #lfo_getsin
                  mov       lfo_r1, lfo_sin
                  shl       lfo_r1, lfo_shift
                  add       lfo_r1, lfo_bias
                  djnz      lfo_dly, #:loop
                  mov       lfo_dly, lfo_reload
                  djnz      lfo_shift, #:loop
                  mov       lfo_shift, #10
                  jmp       #:loop
    
    
    lfo_getsin    test      lfo_sin, lfo_sin90 wc
                  test      lfo_sin, lfo_sin180 wz
                  negc      lfo_sin, lfo_sin
                  or        lfo_sin, lfo_sintable
                  shl       lfo_sin, #1
                  rdword    lfo_sin, lfo_sin
                  negnz     lfo_sin, lfo_sin
    lfo_getsin_ret
                  ret
    
    lfo_incr      long      $36                             ' ~ 130HZ
    lfo_sintable  long      $E000 >> 1
    lfo_sin90     long      $0800
    lfo_sin180    long      $1000
    lfo_sinmask   long      $1FFF
    lfo_setup     long      %00111 << 26 + 11 << 9 + 10
    lfo_dirval    long      1 << 10 + 1 << 11
    lfo_step      long      4095
    lfo_bias      long      $8000_0000
    lfo_offset    long      $1FFF >> 1
    lfo_dly       long      16000
    lfo_reload    long      16000
    lfo_shift     long      0
    lfo_acc       res       1
    lfo_timer     res       1
    lfo_r1        res       1
    lfo_sin       res       1
    
    

    ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
    lt's not particularly silly, is it?
  • Phil Pilgrim (PhiPi)Phil Pilgrim (PhiPi) Posts: 23,514
    edited 2009-09-24 19:43
    Brad,

    Here's your code, which I've modified to dither the amplitude just slightly in order to eliminate the pattern noise you're hearing. The amount of dithering is set by the variable ditheramt. It it's set too high, the white noise it produces will be all too apparent.

    [b]CON[/b]
      [b]_clkmode[/b]      = [b]xtal1[/b] + [b]pll16x[/b]
      [b]_xinfreq[/b]      = 5_000_000
    
    [b]PUB[/b] Go
      [b]cognew[/b](@lfo_sine,0)
    
    [b]DAT[/b]
                  [b]org[/b]       0
    lfo_sine
                  [b]mov[/b]       [b]ctra[/b], lfo_setup
                  [b]mov[/b]       [b]dira[/b], lfo_dirval
                  [b]mov[/b]       lfo_acc, #0
                  [b]mov[/b]       lfo_timer, [b]cnt[/b]
                  [b]add[/b]       lfo_timer, lfo_step
                  [b]mov[/b]       lfo_r1, lfo_bias
                  [b]mov[/b]       lfo_shift, #10
    :loop
                  [b]waitcnt[/b]   lfo_timer, lfo_step
                  [b]mov[/b]       [b]frqa[/b], lfo_r1
                  [b]call[/b]      #dither                  '<------ Comment out to compare with undithered.
    
                  [b]add[/b]       lfo_acc, lfo_incr
                  [b]and[/b]       lfo_acc, lfo_sinmask
                  [b]mov[/b]       lfo_sin, lfo_acc
                  [b]call[/b]      #lfo_getsin
                  [b]mov[/b]       lfo_r1, lfo_sin
                  [b]shl[/b]       lfo_r1, lfo_shift
                  [b]add[/b]       lfo_r1, lfo_bias
                  [b]djnz[/b]      lfo_dly, #:loop
                  [b]mov[/b]       lfo_dly, lfo_reload
                  [b]djnz[/b]      lfo_shift, #:loop
                  [b]mov[/b]       lfo_shift, #10
                  [b]jmp[/b]       #:loop
    
    
    lfo_getsin    [b]test[/b]      lfo_sin, lfo_sin90 [b]wc[/b]
                  [b]test[/b]      lfo_sin, lfo_sin180 [b]wz[/b]
                  [b]negc[/b]      lfo_sin, lfo_sin
                  [b]or[/b]        lfo_sin, lfo_sintable
                  [b]shl[/b]       lfo_sin, #1
                  [b]rdword[/b]    lfo_sin, lfo_sin
                  [b]negnz[/b]     lfo_sin, lfo_sin
    lfo_getsin_ret
                  [b]ret[/b]
    
    '-------[noparse][[/noparse]* Dither the frequency slightly to eliminate pattern noise. ]----------
    
    dither        [b]test[/b]      rand,taps [b]wc[/b]            'Generate a random bit.
                  [b]rcl[/b]       rand,#1                 'Rotate back into random seed.
                  [b]sumc[/b]      [b]frqa[/b],ditheramt          'Add or subtract depending on bit.
    dither_ret    [b]ret[/b]
    
    taps          [b]long[/b]      $d000_0001
    ditheramt     [b]long[/b]      $0004_0000
    rand          [b]long[/b]      1
    
    '------------------------------------------------------------------------------
    
    lfo_incr      [b]long[/b]      $36                             ' ~ 130HZ
    lfo_sintable  [b]long[/b]      $E000 >> 1
    lfo_sin90     [b]long[/b]      $0800
    lfo_sin180    [b]long[/b]      $1000
    lfo_sinmask   [b]long[/b]      $1FFF
    lfo_setup     [b]long[/b]      %00111 << 26 + 11 << 9 + 10
    lfo_dirval    [b]long[/b]      1 << 10 + 1 << 11
    lfo_step      [b]long[/b]      4095
    lfo_bias      [b]long[/b]      $8000_0000
    lfo_offset    [b]long[/b]      $1FFF >> 1
    lfo_dly       [b]long[/b]      16000
    lfo_reload    [b]long[/b]      16000
    lfo_shift     [b]long[/b]      0
    lfo_acc       [b]res[/b]       1
    lfo_timer     [b]res[/b]       1
    lfo_r1        [b]res[/b]       1
    lfo_sin       [b]res[/b]       1
    
    
    


    -Phil

    Post Edited (Phil Pilgrim (PhiPi)) : 9/24/2009 7:52:17 PM GMT
Sign In or Register to comment.