Good audio result using PWM: fast PWM assignment, channels, register, ...

Hi everybody,

I'm working on a small synth and I was very disappointed when I discover how noisy DACs are.
So I've tried PWM method and I get better results.

For inspiration source I use two main codes:

My test code for a sine wave generator looks like this:

//   It 's a fact Arduino Due's DACs are very noisy, this code is another approach to obtain good results by using audio PWM method.
//   You can find here a simple sine wave generator. Audio is 14 bit encoded. It could be a good start to make real time audio synthesis.

//   2 pins are used for a mono audio out (pins 34 and 36, while pins 35 and 37 are copy). Each pin provides a 656Khz / 7 bits PWM encoded audio.
//   pin 34 provides 7 Most Significant Bits and pin 36 provides 7 Least Significant Bits  for a 14bit range.
//   PWMs have to be filtered and sum to create analog audio mono out.


static int sineWaveform[256] = {
  //    256 samples / 14 bits  Sine wave. It is a non-optimized example.
    8192 , 8393 , 8593 , 8794 , 8994 , 9194 , 9394 , 9592 , 9790 , 9986 , 10182 , 10376 , 10570 , 10761 , 10951 , 11140 , 11326 , 11511 , 11694 , 11875 , 12053 , 12229 , 12403 , 12574 , 12743 , 12909 , 13071 , 13231 , 13388 , 13542 , 13693 , 13840 , 13984 , 14125 , 14261 , 14395 , 14524 , 14650 , 14771 , 14889 , 15003 , 15113 , 15218 , 15319 , 15416 , 15509 , 15597 , 15681 , 15760 , 15835 , 15905 , 15970 , 16031 , 16087 , 16138 , 16184 , 16226 , 16263 , 16295 , 16322 , 16344 , 16361 , 16374 , 16381 , 16383 , 16381 , 16374 , 16361 , 16344 , 16322 , 16295 , 16263 , 16226 , 16184 , 16138 , 16087 , 16031 , 15970 , 15905 , 15835 , 15760 , 15681 , 15597 , 15509 , 15416 , 15319 , 15218 , 15113 , 15003 , 14889 , 14771 , 14650 , 14524 , 14395 , 14261 , 14125 , 13984 , 13840 , 13693 , 13542 , 13388 , 13231 , 13071 , 12909 , 12743 , 12574 , 12403 , 12229 , 12053 , 11875 , 11694 , 11511 , 11326 , 11140 , 10951 , 10761 , 10570 , 10376 , 10182 , 9986 , 9790 , 9592 , 9394 , 9194 , 8994 , 8794 , 8593 , 8393 , 8192 , 7990 , 7790 , 7589 , 7389 , 7189 , 6989 , 6791 , 6593 , 6397 , 6201 , 6007 , 5813 , 5622 , 5432 , 5243 , 5057 , 4872 , 4689 , 4508 , 4330 , 4154 , 3980 , 3809 , 3640 , 3474 , 3312 , 3152 , 2995 , 2841 , 2690 , 2543 , 2399 , 2258 , 2122 , 1988 , 1859 , 1733 , 1612 , 1494 , 1380 , 1270 , 1165 , 1064 , 967 , 874 , 786 , 702 , 623 , 548 , 478 , 413 , 352 , 296 , 245 , 199 , 157 , 120 , 88 , 61 , 39 , 22 , 9 , 2 , 0 , 2 , 9 , 22 , 39 , 61 , 88 , 120 , 157 , 199 , 245 , 296 , 352 , 413 , 478 , 548 , 623 , 702 , 786 , 874 , 967 , 1064 , 1165 , 1270 , 1380 , 1494 , 1612 , 1733 , 1859 , 1988 , 2122 , 2258 , 2399 , 2543 , 2690 , 2841 , 2995 , 3152 , 3312 , 3474 , 3640 , 3809 , 3980 , 4154 , 4330 , 4508 , 4689 , 4872 , 5057 , 5243 , 5432 , 5622 , 5813 , 6007 , 6201 , 6397 , 6593 , 6791 , 6989 , 7189 , 7389 , 7589 , 7790 , 7990
  };


  void setup() {
    pwmc_setup();
    isr_setup();
  }

  void isr_setup()        // set ISR for real time audio synthesis
  {
    /* turn on the timer clock in the power management controller */
    pmc_set_writeprotect(false);
    pmc_enable_periph_clk(ID_TC4);

    /* we want wavesel 01 with RC */
    TC_Configure(/* clock */TC1,/* channel */1, TC_CMR_WAVE | TC_CMR_WAVSEL_UP_RC | TC_CMR_TCCLKS_TIMER_CLOCK2);
    TC_SetRC(TC1, 1, 238); // sets <> 44.1 Khz interrupt rate
    TC_Start(TC1, 1);

    // enable timer interrupts on the timer
    TC1->TC_CHANNEL[1].TC_IER = TC_IER_CPCS;
    TC1->TC_CHANNEL[1].TC_IDR = ~TC_IER_CPCS;

    /* Enable the interrupt in the nested vector interrupt controller */
    /* TC4_IRQn where 4 is the timer number * timer channels (3) + the channel number (=(1*3)+1) for timer1 channel1 */
    NVIC_EnableIRQ(TC4_IRQn);
  }

  void pwmc_setup()    // Set PWM channels ( 656KHz )
  {
    //Configure PWM channels 0,1,2,3      (note I've deleted ch2 and ch3 commands from this section)(PWML0,PWMH0,PWML1,PWMH1,PWML2,PWMH2,PWML3,PWMH3), (port C.2,C.3,C.4,C.5,C.6,C.7,C.8,C.9), (pins P34,P35,P36,P37,P38,P39,P40,P41)
    REG_PIOC_PDR = 0x3FC;  //B1111111100, PIO Disable Register
    REG_PIOC_ABSR = REG_PIOC_ABSR | 0x3FCu; //B1111111100, Peripheral AB Select Register
    REG_PMC_PCER1 = REG_PMC_PCER1 | 16; //Peripheral Clock Enable Register 1 (activate clock for PWM, id36, bit5 of PMC_PCSR1)
    REG_PWM_ENA = REG_PWM_SR | B0011; //PWM Enable Register | PWM Status Register (activate channels 0,1,not2,not3)
    // Use 0x100000 to enable dead time
    REG_PWM_CMR0 = 0x00000; //Channe0 Mode Register: Dead Time Enable DTE=1
    REG_PWM_CMR1 = 0x00000; //Channe1 Mode Register: Dead Time Enable DTE=1
    // available range is same as duty and depends on PWM frequency (see datasheet)
    REG_PWM_DT0 = 0x000000; //Channe0 Dead Time Register for pins 34,35 (PWML0,PWMH0)
    REG_PWM_DT1 = 0x000000; //Channe1 Dead Time Register for pins 36,37 (PWML1,PWMH1)
    REG_PWM_CPRD0 = 128; //Channe0 Period Register (84mhz/128 = 656250 Hz )    note 7 bits:  2^7 = 128
    REG_PWM_CPRD1 = 128; //Channe1 Period Register (84mhz/128 = 656250 Hz)     note 7 bits:  2^7 = 128
  }

  void loop() {
    // place your code here
  }


  void TC4_Handler()   //ISR is useful to process real time synthesis
  {
    // We need to get the status to clear it and allow the interrupt to fire again
    TC_GetStatus(TC1, 1);

    static byte myWavePointer;
    myWavePointer++;

    int audioBridge = (sineWaveform[(myWavePointer)]) ;       // read 14bit sine waveform
    // here you can process audio synthesis by making operations on audioBridge

    // prepare audio to set PWM duty cycle:
    int myDutyCycleMSB = audioBridge >> 7;                        // channel 0 :  7 most significant bits
    int myDutyCycleLSB = audioBridge - (myDutyCycleMSB << 7);     // channel 1 :  7 less significant bits

    pwmc_duty(myDutyCycleMSB, myDutyCycleLSB);
  }

  //Set the PWM duty-cycle
  inline void pwmc_duty(unsigned short myDutyCycleMSB, unsigned short myDutyCycleLSB) {
    REG_PWM_CDTY0 = myDutyCycleMSB;
    REG_PWM_CDTY1 = myDutyCycleLSB;
  }

// It 's a fact Arduino Due's DACs are very noisy This code is another approach to obtain good results by using audio PWM method.
// You can find here a simple sine wave generator. Audio is 14 bit encoded. It could be a good start to make real time audio synthesis.
// 2 pins are used for a mono audio out (pins 34 and 36, while pins 35 and 37 are copy).
// Each pin provides a 656Khz / 7 bits PWM encoded audio.
// pin 34 provides 7 Most Significant Bits and pin 36 provides 7 Least Significant Bits for a 14bits range.
// PWMs have to be filtered and sum to create analog audio mono out.

It works great but to be honest I'm beginner and do not understand PWM part, especially how PWM is assigned to pins. I've search words like "PIO, PIOC, ABSR, PMC, PCER, LASER SABER, PWML0, PDR..." in datasheet but this document seems to have been written by Klingon or Egyptian back from the past.

For my synth project I just need a mono audio out , which requires 2 pins only (one pin for MSB and one pin for LSB) while the PWM code I use is written for 8 PWM outputs. I wonder how this can be optimized.
In another discussion dlloys member told about "synchronous channel mode"as a possible answer.

Another bad thing: I have already make my synth PCB with 16 buttons, 32 leds, 12 potentiometers , a MIDI port...and pins 34/36 and 35/37 are busy, it will not be easy to desolder it.
So I wonder If i can address PWM to other pins (pin12 and 13 would be perfect).

Thanks.

Referring to the pinout diagram, look for PWMH or PWML to find the Due pin and Port pin. PWMH7 and PWMH4 aren't available. PWMH1 and PWMH2 are available at 2 pins. In total, there are 16 Due pins you could use:

DUE PWM     PORT
PIN CHANNEL PIN
 -  PWMH7   -
45  PWMH6   C.18
44  PWMH5   C.19
 -  PWMH4   -
41  PWMH3   C.9
53  PWMH2   B.14
39  PWMH2   C.7
42  PWMH1   A.19
37  PWMH1   C.5
35  PWMH0   C.3
 6  PWML7   C.24
 7  PWML6   C.23
 8  PWML5   C.22
 9  PWML4   C.21
40  PWML3   C.8
38  PWML2   C.6
36  PWML1   C.4
34  PWML0   C.2

The synchronous channel mode would be required only if you need to have the hardware update the signals at exactly the same time ... the PWM signals would then be synchronous (aligned). Otherwise, there could be a few clock cycles difference when each signal starts.

Thanks lloyd,

in my case pin 8 et 9 are a good alternative.

So with first method I think I have to change this code to assign PWML5 and PWML4:

REG_PWM_DT0 = 0x000000; //Channe0 Dead Time Register for pins 34,35 (PWML0,PWMH0)
REG_PWM_DT1 = 0x000000; //Channe1 Dead Time Register for pins 36,37 (PWML1,PWMH1)

Isn't it ?

But you're right, more synchronous signals (for MSB and LSB good synchronisation ) could be a better method.