// 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);Pico Arbitrary Waveform Generator Part 2
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.
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!