S Lazy-H
  • Home
  • About
  • Posts
  • Contact
  • Slide Rules
  • A Biker’s Tale

Pico Arbitrary Waveform Generator Part 2

electronics
pico
Author

Sam Hutchins

Published

February 23, 2023

As I mentioned in the first post about using the Pico as a arbitrary waveform generator (AWG), I had some issues with using a circuit allowing continuous varying of the frequency and/or duty cycle. For now I have abandoned that effort until I can find why the DMA wouldn’t work after the initial bootup. So I went the route of resetting the Pico each time I wanted to change something. This post I will get more into the modified software I am using for my current version of a Pico AWG.

Update: One thing I neglected to mention in the first post was the CA3338E D/A Converter VREF+ line is connected to the 3V3 output pin 36 on the Pico.

Again, I refer you to the original youtube video, Life with David, Episode 15, where David explains the original software workings. Several things I have modified in my efforts to make it work, and what I have now I show in this post.

Firstly, I tried placing all the setup steps in a function that I could refer to numerous times. As mentioned above, that didn’t work, but I have retained the function format. I don’t repeat the breadboard setup here, as it is in the first post. I start with the includes.

// Specify libraries and programs
#include <stdio.h>
#include <cmath>
#include <iostream>
#include "pico/stdlib.h"
#include "hardware/dma.h"
#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/resets.h"
#include "AWG_3.pio.h" // Our assembled PIO program
#define PI 3.1415926535897932
using namespace std;
uint32_t setup_buffers(char type,uint freq,float duty,int polarity);

The #include “hardware/resets.h” line is not currently used. It is left over if I decide to revisit the DMA ‘set/reset’ attempt. As I updated a couple of David’s lines to C++ format, I included using namespace std; and #include <iostream>. This does grow the program size significantly! Lastly is the function prototype. Next up are the global constants needed since the PIO and DMA setup are in a function.

uint SM_CLK_FREQ = 125000000;
uint OUT_PIN_NUMBER = 0; //Start of output pin group (GP0-GP7)
uint NPINS = 8; //Number of output pins
uint bufdepth=4096; //the number of samples in the AWG buffer table, must be a power of 2 (i.ie, 2,4,8,16,32,64...)
float factor;
uint cyclecount;
uint cyclestep;
uint cyclelength;
uint numcycle;
uint8_t awg_buff[4096] __attribute__((aligned(4096)));

The above mostly follows David’s program. The int main() portion is significantly shorter with the setup steps moved to the function.

int main()
{
    / ** Initialize variables 
    stdio_init_all();  // set up to print out to USB
    
    // ** Select wave parameters
    char type; // 'S/s' for sine, 'T/t' for triangle, 'P/p' for square (pulse) wave
    float duty=.5; //0 to 1
    int polarity = 1; // -1 for inverted signal from pico, will invert again from amplifier
    
    // ** USB communications for debugging and settings
    sleep_ms(1000);
    char str[10]; // for frequency

    cout << "\n\nPico Arbitrary Waveform Generator (0 - 100 KHz)\n";
    printf("\nEnter the desired frequency in Hz: ");
    scanf("%s",&str);
    uint freq = atoi(str);
    if(freq>=100000) { freq = 100000; } // cleaner signal below 100 KHz
    cout << "\nDuty cycle is 0 to 100 % (0.0 - 1).\n";
    cout << "For ramp, select 'T' and duty cycle of 0.01.\n";
    cout << "\nEnter 'S','T','P' for sine,triangle,pulse: ";
    cin >> type;
    if(type=='P' || type=='p' || type=='T' || type=='t') {
        cout << "\nEnter duty cycle (0.0 to 1):\n";
        scanf("%f",&duty);
        if((type=='t' || type=='t') && duty>=0.5) { duty = 0.5; }
        duty = 1-duty;
    }
    setup_buffers(type,freq,duty,polarity);
    return(0);
}

One major difference in the above lines is replacing scanf() with cin as it responds immediately, so saves a button press on the single character waveform type input. As the duty cycle is superfluous for a sine wave, the if() section only shows the duty input if sawtooth or pulse is selected. BTW, as always, I have to reiterate I am no expert on C++, so the code could be improved by someone who is more knowledgeable than I. For me it’s a hobby, not a profession. Anyway, the penultimate line above is the function execution. And that’s it for the main program portion. The function itself is rather long and I hesitate to place it in the post. However, for completeness, here it is.

uint32_t setup_buffers(char type,uint freq,float duty,int polarity) {
        
    // ** Select between high speed and variable speed SM_CLK_FREQ
    if (freq > 1800000) { // 1.8 MHz
    // ** Determine number of cycles in wavetable and the State Machine Clock Frequency
        float numfloat= (float)freq * bufdepth / SM_CLK_FREQ; //floating point result to handle big numbers
        uint numcycle = numfloat;  //Truncation process to get an interger value
    } else {
        if (freq > 50000) { // 50 KHz
            numcycle = 64 ;
            SM_CLK_FREQ = (float) freq * bufdepth / numcycle ;
        }
        else { // freq < 50 KHz
            numcycle = 16 ;
            SM_CLK_FREQ = (float) freq * bufdepth / numcycle ;
        }
    }

    // ** Fill wavetable with data  
    switch(type){   
    // for sine wave
        case 'S':
        case 's':
            for (int i = 0; i < bufdepth; ++i) {
                factor=(float)numcycle*i/bufdepth; //convert integer division to floating point
                // put the AWG formula here:
                awg_buff[i] = 128+(sin((factor)*2*PI)*127); //Loads the AWG Buffer table with values of the sine wave
            }
            break;      
    //For triangle and sawtooth wave
        case 'T':
        case 't':
            cyclelength = bufdepth/numcycle ;
            for(cyclecount=0; cyclecount<numcycle; ++cyclecount) {  // steps through all the cycles in the buffer
                for(cyclestep = 0; cyclestep<cyclelength; ++cyclestep) { //steps through one of the several cycles
                    factor = (float)cyclestep/cyclelength;
                    if (polarity == -1) {
                        if(factor<duty) awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] = 255-((255/duty)*factor);
                        else awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] = 255-((255/duty)*(1-factor));
                    }
                    else {
                        if(factor<duty) awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] = (255/duty)*factor;
                        else awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] = (255/duty)*(1-factor);
                    }
                }
            }
            break;
    // For Square wave
        case 'P':
        case 'p':
            cyclelength = bufdepth/numcycle ;
            for(cyclecount=0; cyclecount<numcycle; ++cyclecount) {  // steps through all the cycles in the buffer
                for(cyclestep = 0; cyclestep<cyclelength; ++cyclestep) { //steps through one of the several cycles
                    factor = (float)cyclestep/cyclelength;
                    if (polarity == -1) {
                        if(factor<duty) awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] = 255;
                        else awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] =0;
                    }
                    else {
                        if(factor<duty) awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] = 0;
                        else awg_buff[(cyclecount*bufdepth/numcycle)+cyclestep] =255;
                    }
                }
            }
            break;
    }
    // ** Initialize PIO
    reset_block(RESETS_RESET_DMA_BITS|RESETS_RESET_PIO0_BITS);
    unreset_block_wait(RESETS_RESET_DMA_BITS|RESETS_RESET_PIO0_BITS);

    // Choose which PIO instance to use (there are two instances)
    PIO pio = pio0;
    uint sm = pio_claim_unused_sm(pio, true);
    uint offset = pio_add_program(pio, &pio_byte_out_program);
    pio_byte_out_program_init(pio, sm, offset, OUT_PIN_NUMBER, NPINS, SM_CLK_FREQ);
    
    // ** Initialize DMA
    // wave_dma_chan_a and wave_dma_chan_b loads AWG buffer table to PIO in ping pong method
    int wave_dma_chan_a = 0;
    int wave_dma_chan_b = 1;

/*  if(dma_channel_is_busy(wave_dma_chan_a)) {
        printf("\nDMA Channel 0 busy!\n");
    } else {printf("\nChannel 0 idle.\n"); }
    if(dma_channel_is_busy(wave_dma_chan_b)) {
        printf("\nDMA Channel 1 busy!\n");
    } else {printf("\nChannel 1 idle.\n"); }
*/

    //set up the wave_dma_chan_a DMA channel
    dma_channel_config wave_dma_chan_a_config = dma_channel_get_default_config(wave_dma_chan_a);
    channel_config_set_chain_to(&wave_dma_chan_a_config, wave_dma_chan_b); //after block has been transferred, wave_dma_chan b
    channel_config_set_dreq(&wave_dma_chan_a_config, DREQ_PIO0_TX0);// Transfer when PIO asks for a new value
    channel_config_set_ring(&wave_dma_chan_a_config, false, 12); //wrap every 4096 bytes
    
    // Setup the wave_dma_chan_b DMA channel
    dma_channel_config wave_dma_chan_b_config = dma_channel_get_default_config(wave_dma_chan_b);
    channel_config_set_chain_to(&wave_dma_chan_b_config, wave_dma_chan_a);//after block has been transferred, wave_dma_chan a
    channel_config_set_dreq(&wave_dma_chan_b_config, DREQ_PIO0_TX0); // Transfer when PIO asks for a new value
    channel_config_set_ring(&wave_dma_chan_b_config, false, 12);    //wrap every 4096 bytes (2**8)
    
    // Setup the first wave DMA channel for PIO output
    dma_channel_configure(
        wave_dma_chan_a,
        &wave_dma_chan_a_config,
        &pio0_hw->txf[sm], // Write address (sm1 transmit FIFO)
        awg_buff, // Read values from waveform buffer
        bufdepth, // 4096 values to copy
        false // Don't start yet.
    );
    // Setup the second wave DMA channel for PIO output
    dma_channel_configure(
        wave_dma_chan_b,
        &wave_dma_chan_b_config,
        &pio0_hw->txf[sm], // Write address (sm1 transmit FIFO)
        awg_buff, // Read values from waveform buffer
        bufdepth, // 4096 values to copy
        false //  Don't start yet.
    );
    
    // ** Everything is ready to go. Now start the first DMA
    dma_start_channel_mask(1u << wave_dma_chan_a);
    
/*  if(dma_channel_is_busy(wave_dma_chan_a)) { // testing
        printf("\nDMA Channel 0 is now busy!\n");
    } else {printf("\nChannel 0 still idle.\n"); }
    if(dma_channel_is_busy(wave_dma_chan_b)) {
        printf("\nDMA Channel 1 is now busy!\n");
    } else {printf("\nChannel 1 still idle.\n"); }
*/
    //printf("\nCLK: %i numcycle: %i Freq: %i Type: %c Duty: %.2f\n\n", SM_CLK_FREQ, numcycle, freq, type, 1-duty);
    printf("\nSelected Freq: %i Hz, Type: %c, Duty: %.2f\n\n", freq, type, 1-duty);
    while(1) {
        tight_loop_contents(); // NOP
    }
    return(0);
}

Whew, that’s really long! (Xie, Dervieux, and Riederer 2020) has functions for R code block folding, but I haven’t discovered how to make them work for normal blockquotes. Also, I just noticed the while() loop in the function end really belongs in the main() section. I threw it in here because the minicom display wasn’t updating after the last printf() statement, but it really should be moved. A future effort may be using thumbwheels for frequency and/or duty cycle input and maybe a rotary switch for waveform type selection, as that would mitigate the computer terminal input necessity.

This post is long enough, so that’s it for now. I pray you know God and are right with Him, and have accepted His son Jesus as your Lord and Savior, before it’s too late! Stay safe!

References

Xie, Yihui, Christophe Dervieux, and Emily Riederer. 2020. R Markdown Cookbook. Boca Raton, Florida: Chapman; Hall/CRC. https://bookdown.org/yihui/rmarkdown-cookbook.
© S Lazy-H 2019 -