PWM as DAC

Tip / Sign in to post questions, reply, level up, and achieve exciting badges. Know more

cross mob
lock attach
Attachments are accessible only for community members.
greg79
Level 4
Level 4
25 replies posted 25 sign-ins 10 replies posted

I want to test PWM with lowpass filter as a DAC, reading samples from a lookup table (sine wave). On the scope I do not see the width changing at all. What am I missing? Does the ISR have to be in sync with the PWM's clock?

pwm.PNG

 

#include "project.h"
#include "LUT.h"

int sineTableIndex;
int sineTableSize = 255;

uint16_t low = 10;
uint16_t high = 5000;

int main(void)
{
    PWM_1_Start();
    Clock_1_Start();
    Clock_2_Start();
    isr_1_Start();
    CyGlobalIntEnable; /* Enable global interrupts. */

    for(;;)
    {

    }
}
CY_ISR(isr_1){
    PWM_1_WriteCompare(sineWaveLUT[sineTableIndex]);    
    (sineTableIndex==sineTableSize) ? sineTableIndex=0 : sineTableIndex++;
};
/* [] END OF FILE */

 

 

0 Likes
1 Solution
lock attach
Attachments are accessible only for community members.

Greg,

Attached is basic test project. 

volatile int32 counter;
CY_ISR(isrDtr_Handler) {
     counter++;
     counter &=0xFF;
     PWM_1_WriteCompare(counter);
}


int main()
{
     CyGlobalIntEnable; //enable global interrupts.
     PWM_1_Start(); //
     isrDtr_StartEx(isrDtr_Handler); //start Timer interrupt

     for(;;) {}

}

PWM_DAC_01a_test_A.pngPWM_DAC_01a_test_D.png

View solution in original post

0 Likes
23 Replies
odissey1
Level 9
Level 9
First comment on KBA 1000 replies posted 750 replies posted

Greg,

I am away from the computer, so code is untested.

#include "project.h"
#include "LUT.h"

volatile int sineTableIndex; // notice volatile
int sineTableSize = 255;

 

CY_ISR(isr1_Handler) // interrupt handler

{
       PWM_1_WriteCompare(sineWaveLUT[sineTableIndex]);
       (sineTableIndex==sineTableSize) ? sineTableIndex=0 : sineTableIndex++;
};

 

int main(void)
{

     CyGlobalIntEnable;
     PWM_1_Start();
     isr_1_StartEx(isr1_Handler); //start isr and point to custom handler

     for(;;)
     {

     }
}

0 Likes

Other comments:

1. clock_1_Start() is not needed, all clocks start automatically unless specifically stopped. 

2. All global variables, which may change value inside interrupts, must be marked as "volatile" to prevent compiler optimization:

volatile int sineTableIndex;

3. int sineTableSize = 255; //?

The Table size is likely already defined in LUT. h, and most likely it is 256 (not 255). To avoid confusion i would rename it to maxTableIndex.

 

Alternatively, one can avoid slow compare operation and speed up code:

//int sineTableSize = 256; // not needed if default is 256

volatile uint16 sineTableIndex; 

CY_ISR(isr1_Handler) // interrupt handler

{

      sineTableIndex++;  // let it roll over
     sineTableIndex &=0xFF; // trim to 0-255
     PWM_1_WriteCompare(sineWaveLUT[sineTableIndex]);

};

 

4. The sine table is apparently 8-bit, so there is no point to use 16-bit PWM. 

5. Note that PWM_1_WriteCompare() won't affect actual Compare setting until the PWM counter rolls over. If, for example, the Period is set to 4999, it will take 5ms to complete using 1MHz clock. This is 5 times slower than interrupt updates the Compare register! My suggestion would be increasing PWM clock_2 to 16-32MHz.

 

0 Likes
ARH
Level 3
Level 3
10 replies posted 5 replies posted 5 sign-ins

 

I think that the problem is that the frequency of the interrupt, the cpu frequency and the frequency of the PWM are not coherent.

 

I didnt do the math but I bet you have to slow down the interrupt quite a bit to make this work.

 

I suppose if I were gonna do this I also would have DMA'd the compare values.

The other thing I wondered about is why not use the DAC? ... then you could have used the waved component?

 

Alan

 

greg79
Level 4
Level 4
25 replies posted 25 sign-ins 10 replies posted

I need 8 analog signals and I cannot have 8 DACs, especially not 16 bit ones. Thank you for the tips!

0 Likes
odissey1
Level 9
Level 9
First comment on KBA 1000 replies posted 750 replies posted

Greg,

You may check these two PSoC5 projects showing PWM DACs:

Sine wave generation using WaveGen and PWM (SPWM) (using DMA)

WaveGen_PWM_02a_basic_A.png

and

Building a 16-bit PWM DAC out of 8-bit Fractional PWM and DeltaSigma Modulator

FracPWM_01c_A.png

 

The latter one doesn't use DMA, but can be modified if (really) necessary. However, since you need only control voltages for the voice chip, I believe that the 8-bit might suffice.

/odissey1

There is signal processing and routing multiple signals to outputs. I know all of this sound bizarre but in synthesizers, and in both digital and analog audio, mixing different signals of differing amplitude, waveform, phase is normal. It's not a function generator (that would be so simple!) I'm afraid, not a polyphonic synth. 

I need 8 DACs (PWMs) that can reproduce mix of arbitrary waveforms (AC) and DC signals. 

0 Likes

These two projects are brilliant but unfortunately I cannot use them. I need 8x 16bit DACs. I do appreciate the comment and these two projects are very interesting. 

 

This is so elegant and simple, thank you! I've been wondering if there is a way to reduce 'if' statements.

CY_ISR(isr1_Handler) // interrupt handler

{

      sineTableIndex++;  // let it roll over
     sineTableIndex &=0xFF; // trim to 0-255
     PWM_1_WriteCompare(sineWaveLUT[sineTableIndex]);

};

 

0 Likes
lock attach
Attachments are accessible only for community members.
greg79
Level 4
Level 4
25 replies posted 25 sign-ins 10 replies posted

With all the suggestions applied, I still don't see the pulse width changing. @ARH aka Alan has a good point about the ISR and the PWM not being in sync. Even if I slow down the ISR clock to 10Hz I cannot see the pulses changing. Is there a rule on when can I overwrite the compare value? Does it have to be in sync with the PWM's internal (or external) clock? Thank you for all your kind help @odissey1 and @ARH ! 

0 Likes
lock attach
Attachments are accessible only for community members.

Greg,

Attached is basic test project. 

volatile int32 counter;
CY_ISR(isrDtr_Handler) {
     counter++;
     counter &=0xFF;
     PWM_1_WriteCompare(counter);
}


int main()
{
     CyGlobalIntEnable; //enable global interrupts.
     PWM_1_Start(); //
     isrDtr_StartEx(isrDtr_Handler); //start Timer interrupt

     for(;;) {}

}

PWM_DAC_01a_test_A.pngPWM_DAC_01a_test_D.png

0 Likes

Thank you, it is working! In my original code I had to add an interrupt handler: 

CY_ISR(isr_1_Handler)

instead of 

CY_ISR()

 

Lesson learned! 

0 Likes
ARH
Level 3
Level 3
10 replies posted 5 replies posted 5 sign-ins

My guess is the assembly language is the same... for what you did...

CY_ISR(isrDtr_Handler) {
     counter++;
     counter &=0xFF;
     PWM_1_WriteCompare(counter);
}

 

I often like to do this slightly differently...

void ISR_Handler()

{

static uint32_t  count = 0;

count = (count +1 ) % 256;

write_compare(count) ...

}

You could also define the count as uint8_t ... in which case it would rollover automatically...

One thing that you need to be careful of is if this is an upcounter that you dont set the compare before the counter... if that happens you will end up with one or more long output which can be hard to see.... which is why I typically downcount

 

 

 

greg79
Level 4
Level 4
25 replies posted 25 sign-ins 10 replies posted

Excellent, thank you!

0 Likes
ARH
Level 3
Level 3
10 replies posted 5 replies posted 5 sign-ins

 

The issues that you need to be careful about which I can see in this designer are

The values of that RC filter

The speed at which you switch the output

The speed at which you change the duty cycle of the pwm

The speed at which you get the analog voltage you want on the output will depend on the frequency of the PWM and how many periods you are on the value

 

Im sure that there is some analog guy (or girl) who can tell you what the period of the pwm needs to be to get the best performance of this circuit.

greg79
Level 4
Level 4
25 replies posted 25 sign-ins 10 replies posted

Just one more thing: how do I target the compare value register with DMA? In the DMA Wizzard tool it is not possible to chose the PWM but I saw other projects where it was done. What am I missing?

0 Likes

Greg,

The project is attached above. It shows exactly DMA transfer to PWM Compare register.

0 Likes

The DDS component is not found, where can I get it from?

0 Likes

DMA to PWM  project on Github does the DMA transfer to PWM from an ADC. I cannot wrap my head around it, the DMA target is set to the PWM:

 

#include <project.h>
#include "cyapicallbacks.h"

/* Defines for DMA */
#define DMA_BYTES_PER_BURST 1
#define DMA_REQUEST_PER_BURST 1
#define DMA_SRC_BASE (CYDEV_PERIPH_BASE)
#define DMA_DST_BASE (CYDEV_PERIPH_BASE)

volatile uint8_t foo;

void DMA_Config(void);

int main(){
    
    CyGlobalIntEnable;

    /* Components init */
    PWM_Start();
    ADC_Start();
    isr_Start();
    
    /* DMA configuration */
    DMA_Config();
    
    /* Start the ADc convertion */
    ADC_StartConvert();

    for(;;){
        /* Here we can put the MCU to sleep to save amps */
    }
}

void DMA_Config(){

    /* Variable declarations for DMA */
    /* Move these variable declarations to the top of the function */
    uint8 DMA_Chan;
    uint8 DMA_TD[1];

    /* DMA Configuration for DMA */
    DMA_Chan = DMA_DmaInitialize(DMA_BYTES_PER_BURST,
                                    DMA_REQUEST_PER_BURST,
                                    HI16(DMA_SRC_BASE),
                                    HI16(DMA_DST_BASE));
    DMA_TD[0] = CyDmaTdAllocate();
    
    CyDmaTdSetConfiguration(DMA_TD[0], 1, DMA_TD[0], DMA__TD_TERMOUT_EN);
    /* Transfer data from ADC Result register to PWM CMP register */
    CyDmaTdSetAddress(DMA_TD[0], LO16((uint32)ADC_DEC_SAMP_PTR), LO16((uint32)PWM_COMPARE1_LSB_PTR));
    CyDmaChSetInitialTd(DMA_Chan, DMA_TD[0]);
    CyDmaChEnable(DMA_Chan, 1);
}

void isr_Interrupt_InterruptCallback(void){
    foo ^= 1; 
}

dmatopwm.PNG 

0 Likes
lock attach
Attachments are accessible only for community members.

Greg,

Attached is a basic example showing 8-bit RAM array to PWM transfer using DMA. I believe that it has been published in the forum before, but anyway.

Project shows 60Hz generation using PWM, which Compare register is continuously updated using DMA. The clock_1 frequency, Timer_1 period (256), and Buffer size (32) are selected to produce 60 Hz (averaged) output: clock_1 = 32 x 256 x 60Hz = 491.52 kHz.

External RC is not depicted.

/odissey1

P.S. The DDS32 component can be found in this thread (scroll down to message posted on Jun 02, 2018) DDS24: 24-bit DDS arbitrary frequency generator component

 

Figure  1. Project schematic: 60Hz sine generation using PWM. PWM Period is set to 255 (actual divider is 256), and Buffer size to 32.

RAM-DMA-PWM8_01a.png

 

Figure 2. PWM digital output (unfiltered). Each 60Hz period has 32 PWM updates.DS1Z_QuickPrint7.png

 

Figure 3. PWM output filtered using 1-st order RC filter.DS1Z_QuickPrint4.png

 

0 Likes
greg79
Level 4
Level 4
25 replies posted 25 sign-ins 10 replies posted

I don't understand how any of this works. I understand that you created  WaveGen in which the DMA can access the PWM but how? How is this possible:

 

 

CyDmaTdSetAddress(DMA1_TD[0], LO16((uint32)pBuffer), LO16((uint32)WaveGen_Data_PTR)); // RAM->PWM

 

 

Can you explain this, I am very confused.  What does the disabled interrupt do? Why is it necessary? How exactly the DMA accesses the PWM compare register? Where does 'WaveGen_Data_PTR' come from?

 

Also, how does the DMA access PWM in the Github project? It compiles properly and there is no error even though this line: 

 

 

CyDmaTdSetAddress(DMA_TD[0], LO16((uint32)ADC_DEC_SAMP_PTR), LO16((uint32)PWM_COMPARE1_LSB_PTR));

 

 

 

Once again, DMA is connected to the PWM's compare  again.... Could someone clarify why cannot I target the PWM with the DMA when it can be done? What am I missing? What's the trick?

0 Likes

Greg,

There are too many projects listed in this thread, so it is hard to understand, which one you referring to. 

The easiest one is RAM-DMA-PWM8_01b posted on Sep 20. 

RAM-DMA-PWM8_01a.png

It uses no interrupts (please disregard the disabled remnant isr). It needs neither DDS32, nor WaveGen component.

The following sets DMA from/to address

CyDmaTdSetAddress(DMA1_TD[0], LO16((uint32)pBuffer), LO16((uint32)PWM_COMPARE1_LSB_PTR)); // RAM Buffer->PWM,

where pBuffer = &Buffer[0], and PWM's compare1 register is the target.

P.S. I don't want to comment on ADC-PWM project from GitHub. I believe it shows Perifery-to-Perifery DMA transfer, which is irrelevant in your case.

0 Likes
greg79
Level 4
Level 4
25 replies posted 25 sign-ins 10 replies posted

I wonder if this could work? The PWM compare register is the target of the DMA and the compiler does not complain. It still doesn't work, I guess something to do with how I referenced the SineWaveLUT array in the TD configuration. 

greg79_0-1632217093505.png

 

 

#include "project.h"
#include "LUT.h"
 
void dmaConfig(void);


int main(void)
{
    CyGlobalIntEnable; /* Enable global interrupts. */
    dmaConfig();
    /* Place your initialization/startup code here (e.g. MyInst_Start()) */

    for(;;)
    {
        /* Place your application code here. */
    }
}

void dmaConfig(){
/* Defines for DMA_1 */
#define DMA_1_BYTES_PER_BURST 1
#define DMA_1_REQUEST_PER_BURST 1
#define DMA_1_SRC_BASE (CYDEV_SRAM_BASE)
#define DMA_1_DST_BASE (CYDEV_SRAM_BASE)

/* Variable declarations for DMA_1 */
/* Move these variable declarations to the top of the function */
uint8 DMA_1_Chan;
uint8 DMA_1_TD[1];

/* DMA Configuration for DMA_1 */
DMA_1_Chan = DMA_1_DmaInitialize(DMA_1_BYTES_PER_BURST, DMA_1_REQUEST_PER_BURST, 
    HI16(DMA_1_SRC_BASE), HI16(DMA_1_DST_BASE));
DMA_1_TD[0] = CyDmaTdAllocate();
CyDmaTdSetConfiguration(DMA_1_TD[0], 1, CY_DMA_DISABLE_TD, CY_DMA_TD_INC_SRC_ADR | CY_DMA_TD_INC_DST_ADR);
CyDmaTdSetAddress(DMA_1_TD[0], LO16((uint32)sineWaveLUT), LO16((uint32)PWM_1_COMPARE1_LSB_PTR));
CyDmaChSetInitialTd(DMA_1_Chan, DMA_1_TD[0]);
CyDmaChEnable(DMA_1_Chan, 1);

 

 

0 Likes

Greg,

The following DMA configuration is incorrest

CyDmaTdSetConfiguration(DMA_1_TD[0], 1, CY_DMA_DISABLE_TD, CY_DMA_TD_INC_SRC_ADR | CY_DMA_TD_INC_DST_ADR);

You need no to disable TD, nor to increment destination address (it is fixed compare1 register) . Please copy configuration line from the project RAM-DMA-PWM8_01a above.

0 Likes

Indeed, thank you! Working version:

#include "project.h"
#include "LUT.h"

#define DMA_1_BYTES_PER_BURST 1
#define DMA_1_REQUEST_PER_BURST 1
#define DMA_1_SRC_BASE (CYDEV_SRAM_BASE)
#define DMA_1_DST_BASE (CYDEV_PERIPH_BASE)
 
void dmaConfig(void);

int main(void)
{
    CyGlobalIntEnable;
    PWM_1_Start();
    dmaConfig();
    for(;;){}
}

void dmaConfig(){

uint8 DMA_1_Chan;
uint8 DMA_1_TD[1];

DMA_1_Chan = DMA_1_DmaInitialize(DMA_1_BYTES_PER_BURST, DMA_1_REQUEST_PER_BURST, 
    HI16(DMA_1_SRC_BASE), HI16(DMA_1_DST_BASE));
DMA_1_TD[0] = CyDmaTdAllocate();
CyDmaTdSetConfiguration(DMA_1_TD[0], 255, DMA_1_TD[0], TD_INC_SRC_ADR);

CyDmaTdSetAddress(DMA_1_TD[0], LO16((uint32)sineWaveLUT), LO16((uint32)PWM_1_COMPARE1_LSB_PTR));
CyDmaChSetInitialTd(DMA_1_Chan, DMA_1_TD[0]);
CyDmaChEnable(DMA_1_Chan, 1);
}

 

0 Likes