OK. I was a little ambiguous. I should have made it clearer that the first and last values in the wave table aren't equal, but that they are in sequence. That's why I used the term "match". I tried to take a shortcut; but unfortunately it added to the confusion. Sorry.
I love your videos about the pico, especially making an AWG ❤️❤️❤️ I think it would be fun to see you make some additions like adding a display and rotary encoders to change settings, frequencies, and modulations with a pico since the main processor isn't doing very much ❤️❤️❤️ keep up the amazing work!
Thanks. That is a good idea; the main core is just loafing. I guess I'll have to get back into C/C++; for a while I've had my own little boycott of the C/C++ SDK. Not for any good reason; just because I wanted to challenge myself to go bare metal. Thanks for the suggestion!
Hey David, First of all, let me tell you I’ve been watching all your videos regarding the Pi Pico and I find them very informative and entertaining. Keep them coming. One thing that I think you forgot to mention regarding the length of the wave table, it is that the last sample contained in the table should the last one before the cycle repeats. If the wave table length is 256 values, item 0 should the beginning of the cycle, but item 255 should be the previous value before the next cycle begins. Another improvement would be to generate a cosine wave table instead of a sine wave table. This way you would only have to match the extreme values of the function without having to match the spots where the function crosses the x-axis. For instance, if you have a 14-value wave table (comprising 0 to 2π): value[0] = 1 // cos(2π*0/14) value[1] = 0.901... // cos(2π*1/14) value[2] = 0.623... // cos(2π*2/14) value[3] = 0.223... // cos(2π*3/14) value[4] = -0.223... // cos(2π*4/14) value[5] = -0.623... // cos(2π*5/14) value[6] = -0.901... // cos(2π*6/14) value[7] = -1 // cos(2π*7/14) value[8] = -0.901... // cos(2π*8/14) value[9] = -0.623... // cos(2π*9/14) value[10] = -0.223... // cos(2π*10/14) value[11] = 0.223... // cos(2π*11/14) value[12] = 0.623... // cos(2π*12/14) value[13] = 0.901... // cos(2π*13/14) The intersection with the x-axis would be handled by the external circuitry. Have a great day
You are right, I didn't say the ends were equal, but that they should match. However, I was a little ambiguous and I should have made it clearer. I appreciate the comment and I'll pin a comment to the top of the comments to make it clearer, Thanks for watching!
A couple of other ideas for getting more choices of waveform frequency: 1. Adjust the Pico's main clock frequency (typically 125MHz). The big downside is that it will affect anything else timing sensitive that is dependent on the main clock, like serial ports or USB (unless they have their own dedicated clock?). 2. Use a non-power-of-2 for the waveform buffer size. You won't be able to take advantage of automatic wrap-around on the read address, so you'll have to reset it every time you get to the end of the buffer. If your second DMA channel just does a single transfer of the read address to the first DMA channel's register, it should only stall the DMA briefly. (There are multiple copies of the DMA registers with different memory alignment. Pick alias 3 at offset 0x30, where the read address register is also the trigger to start the next DMA cycle.) And if you're using an input (transmit?) FIFO on the PIO, the DMA's job is to keep the FIFO full (not provide data directly to the PIO), so the PIO shouldn't see any stall. The brief stall while the first DMA channel's read address gets reset should be orders of magnitude faster than the time it takes the PIO to completely drain the FIFO. Once the DMA resumes, it should be able to refill the FIFO much faster than the PIO can drain it, until the DMA stalls because the FIFO is full. Your sample frequency still depends on the state machine's frequency, but you've got a lot more choices in how you fit the waveform into the sample frequency. If you want a sine wave to exactly sample the peaks and zero crossings, you'll need 4N samples per waveform. You'd get N-1 samples between each peak/crossing pair.
Those are all good ideas. I thought about changing the system clock. The only stable clock is the crystal. The ring oscillators vary with process and temperature. Perhaps you could add feedback to correct for variations. You could increase the wave buffer even more. 8k would double the number of "prime" frequencies available. I've done the "ping pong" handoff between DMA channels and it does take at least 1 clock cycle. I'll look at sampling more, you've whetted my interest. Back on the 70's I studied Nyquist and Bode plots, never thinking I use them for a hobby. Time to revisit. Thanks for the comments and for watching!
I got that to work easily with the Arduino IDE: I just renamed the main() to setup() and created an empty loop(). Then I deleted the stdio_init_all() and printf() as these are done differently in Arduino (Serial.print) but are not needed for the waveforms. I had to manually upload the compiled uf2 by explorer drag and drop, as the interface did not like to cooperate, but it worked fine and my oscilloscope shows a beautiful sine wave.
Hi David, thank you very much for your great videos! I would like to take up Bill's suggestion and think it is a good idea to implement the signal generator according to the DDS principle. With the appropriate assembler routine I was able to reduce the loop function to 7 clock cycles, which results in a 19MHz DDS clock (with a 133MHz system clock). Although signals beyond 1MHz are a bit rough, on the other hand, the frequency can be set exactly in steps of 0.031Hz and jitter is absolutely not an issue.
Thank you for the comment. I just looked into DDS in greater detail. It seems that what I presented in this video utilizes the same principles; however the phase accumulator values are calculated during initialization instead of real time. Then those values are used to calculate the amplitude (phase to amplitude converter), again during initialization, which is then stored in the wave table. The larger the wave table, the finer the steps in frequency can be. The downside with this implementation of DDS is that the wave table has a finite length, limiting the number of frequency steps available, in this case about every 0.031 MHz at 4 MHz. Using assembler with the Pico is something I haven't tried yet. That's why I've limited my "assembly" work to the PIO and DMA. That's something I'll have to look into more. Thanks for watching.
I see W2AEW has episode 88 *Cheap and simple TDR using an oscilloscope and 74AC14 Schmitt Trigger Inverter* where for < $2 those square wave edges can be crisp down to just 2 nano-seconds while safely driving 50 ohm loads.
Thanks for doing this series, it's been very helpful. I'm about to start integrating a W into an open source piezo printhead controller. Still looking for project ideas?
That sounds like an awesome project. I'm always looking for projects; but I may not get to them. I'm kinda drifted toward assembly language programming for the RP2040 plus a few other surprises. Thanks for watching!
You see the triangle wave in your "new" method? Look at the bad tops. I think that is due to your new op-amp as it would require some bad samples to cave in the top otherwise. I have seen this pattern before.
I think the squashed tops are due to not having a sample at the exact top of the curve. There are only a few frequencies where we can assure that the top of the triangle wave will coincide with a sample. Nyquest is good for sine waves; but can't deal with discontinuities in triangle or even square waves.
Typically DDS oscillators (direct digital synthesis) use phase accumulation prior to wave table look up, i.e the address of the table is incremented by a variable amount to provide the variable frequency. This might be too cpu intensive for the pico , but might be worth a try.
That's interesting! As I was working through this exercise; I wondered if that was a possibility. As I'm outputting one byte per system clock cycle, the RP2040 isn't fast enough to adjust the wave table address on the fly. If you knew which clock pulse would be short (or long) in advance, you might be able to generate a series of wave tables that would match up with the frequency. However, my brain starts to hurt when I think about how I could do that. lol Thanks for watching.
@@LifewithDavid1 It may not be as difficult as it sounds. The top 12bits of the phase accumulator are the address of your table, so if the accumulator is 32bit there's only need to do a simple add and mask operation . BTW the output of the dac should technically be a pulse (as short as possible) , it is the job of the anti alias filter to reconstruct the waveform.
Hey David, I've been following your series for a long time and I'm currently in need to do a function generator myself for a student project. However I have a changing in my frequency based on an input. As I need many pins for other parts of the project I am not rlly sure how I should do it. I will definetly need an SPI DAC cause I won't have enough pins available for a self made DAC. Should I convert the data to SPI and then make a pio program that forwards spi data? or should i just calculate the data and let pio transfer it into spi and send it? and should I manipulate the wavetable (or be switching between 2 wavetables and i manipulate the one that is currently unused) or should I use 1 wavetable and change the frequency of the pio (idk if this works with spi but i could also just let the pio only send a portion of the data to reduce it, can't i? by saying like ignore every 4th data and defining the max possible frequency in the beginning) I hope you or anyone else who digged deeper into Pio can give me some answers on this. Thank you if you read the full comment!
What's the maximum frequency for the AWG. If it's relatively slow (like audio), you might be able to use SPI. However, I would guess that the DAC is the most time critical part of your project. You may want to use SPI for your other inputs and outputs and the GPIO ports directly for the DAC. If the waveform is complex or you need sharp edges, you may need a very fast DAC. Run the math and look at the harmonics you will need to accurately describe your wave. Again, the maximum frequency will determine the direction you should take on changing the state machine clock frequency or modifying the wave table. If it is slow, like less than 1 MHz, then changing the clock frequency will work well. If it is faster than that, then modifying the wavetable may work better. Hope this helps. Good luck!
@@LifewithDavid1 Thank you for your answer! The maximum frequency will actually be pretty low at only 45-55Hz but I will need 32.000 samples per wave at minimum. The wave will be a simple sine wave. Yeah I thought that changing the clock might just work fine. I will try it out!
@@LifewithDavid1 I might consider sending the data via dma directly to the spi data out without using pio or the core. I would just use a dma channel with the wave-table and another dma toggling it. would that be possible? or even a better solution? or would it be much worse?
@@gehtniemandenwasan9789 You should be able to do that. Using the PIO for pacing the DMA data can make it easy to control the frequency of the wave. Without a bit of review, I'm not sure how DMA could be paced using another DMA channel (but there probably is a way). PIO and DMA can work together without any assistance from the core. Good luck!
The Danube was awesome. Hopefully you saw some of my "shorts" from last fall. The thumbnail represents "The Lady's Man", unfortunately, it's not Courvoisier, but Boone's Farm.
I think that the first and last sample in the wave table should not be the same. Consider if you stepped through the wave table by one step. You would be putting out a zero value twice in succession. this may be contributing to the jitter.
That's right, that's why I said they should match. Maybe not the best term, but I didn't want to say they were equal; but rather in sequence. I probably should have made it clearer.
OK. I was a little ambiguous. I should have made it clearer that the first and last values in the wave table aren't equal, but that they are in sequence. That's why I used the term "match". I tried to take a shortcut; but unfortunately it added to the confusion. Sorry.
I like how clearly you explain what you are doing in the videos. I just found your videos and look forward to seeing more.
Thank you very much. I've always heard that the best way to learn something is when you want to teach it to someone else.
I love your videos about the pico, especially making an AWG ❤️❤️❤️ I think it would be fun to see you make some additions like adding a display and rotary encoders to change settings, frequencies, and modulations with a pico since the main processor isn't doing very much ❤️❤️❤️ keep up the amazing work!
Thanks. That is a good idea; the main core is just loafing. I guess I'll have to get back into C/C++; for a while I've had my own little boycott of the C/C++ SDK. Not for any good reason; just because I wanted to challenge myself to go bare metal. Thanks for the suggestion!
Thank you very much for your sharing. It's constructive and full of details.
Glad it was helpful!
Hey David,
First of all, let me tell you I’ve been watching all your videos regarding the Pi Pico and I find them very informative and entertaining. Keep them coming.
One thing that I think you forgot to mention regarding the length of the wave table, it is that the last sample contained in the table should the last one before the cycle repeats. If the wave table length is 256 values, item 0 should the beginning of the cycle, but item 255 should be the previous value before the next cycle begins.
Another improvement would be to generate a cosine wave table instead of a sine wave table. This way you would only have to match the extreme values of the function without having to match the spots where the function crosses the x-axis.
For instance, if you have a 14-value wave table (comprising 0 to 2π):
value[0] = 1 // cos(2π*0/14)
value[1] = 0.901... // cos(2π*1/14)
value[2] = 0.623... // cos(2π*2/14)
value[3] = 0.223... // cos(2π*3/14)
value[4] = -0.223... // cos(2π*4/14)
value[5] = -0.623... // cos(2π*5/14)
value[6] = -0.901... // cos(2π*6/14)
value[7] = -1 // cos(2π*7/14)
value[8] = -0.901... // cos(2π*8/14)
value[9] = -0.623... // cos(2π*9/14)
value[10] = -0.223... // cos(2π*10/14)
value[11] = 0.223... // cos(2π*11/14)
value[12] = 0.623... // cos(2π*12/14)
value[13] = 0.901... // cos(2π*13/14)
The intersection with the x-axis would be handled by the external circuitry.
Have a great day
You are right, I didn't say the ends were equal, but that they should match. However, I was a little ambiguous and I should have made it clearer. I appreciate the comment and I'll pin a comment to the top of the comments to make it clearer, Thanks for watching!
A couple of other ideas for getting more choices of waveform frequency:
1. Adjust the Pico's main clock frequency (typically 125MHz).
The big downside is that it will affect anything else timing sensitive that is dependent on the main clock, like serial ports or USB (unless they have their own dedicated clock?).
2. Use a non-power-of-2 for the waveform buffer size.
You won't be able to take advantage of automatic wrap-around on the read address, so you'll have to reset it every time you get to the end of the buffer. If your second DMA channel just does a single transfer of the read address to the first DMA channel's register, it should only stall the DMA briefly. (There are multiple copies of the DMA registers with different memory alignment. Pick alias 3 at offset 0x30, where the read address register is also the trigger to start the next DMA cycle.)
And if you're using an input (transmit?) FIFO on the PIO, the DMA's job is to keep the FIFO full (not provide data directly to the PIO), so the PIO shouldn't see any stall. The brief stall while the first DMA channel's read address gets reset should be orders of magnitude faster than the time it takes the PIO to completely drain the FIFO. Once the DMA resumes, it should be able to refill the FIFO much faster than the PIO can drain it, until the DMA stalls because the FIFO is full.
Your sample frequency still depends on the state machine's frequency, but you've got a lot more choices in how you fit the waveform into the sample frequency. If you want a sine wave to exactly sample the peaks and zero crossings, you'll need 4N samples per waveform. You'd get N-1 samples between each peak/crossing pair.
Those are all good ideas. I thought about changing the system clock. The only stable clock is the crystal. The ring oscillators vary with process and temperature. Perhaps you could add feedback to correct for variations.
You could increase the wave buffer even more. 8k would double the number of "prime" frequencies available. I've done the "ping pong" handoff between DMA channels and it does take at least 1 clock cycle.
I'll look at sampling more, you've whetted my interest. Back on the 70's I studied Nyquist and Bode plots, never thinking I use them for a hobby. Time to revisit. Thanks for the comments and for watching!
I got that to work easily with the Arduino IDE:
I just renamed the main() to setup() and created an empty loop().
Then I deleted the stdio_init_all() and printf() as these are done differently in Arduino (Serial.print) but are not needed for the waveforms.
I had to manually upload the compiled uf2 by explorer drag and drop, as the interface did not like to cooperate, but it worked fine and my oscilloscope shows a beautiful sine wave.
That's great! I've only tried PIO using the Arduino IDE a few times; I'm glad it worked. Thanks for watching!
Hi David,
thank you very much for your great videos!
I would like to take up Bill's suggestion and think it is a good idea to implement the signal generator according to the DDS principle.
With the appropriate assembler routine I was able to reduce the loop function to 7 clock cycles, which results in a 19MHz DDS clock (with a 133MHz system clock).
Although signals beyond 1MHz are a bit rough, on the other hand, the frequency can be set exactly in steps of 0.031Hz and jitter is absolutely not an issue.
Thank you for the comment. I just looked into DDS in greater detail. It seems that what I presented in this video utilizes the same principles; however the phase accumulator values are calculated during initialization instead of real time. Then those values are used to calculate the amplitude (phase to amplitude converter), again during initialization, which is then stored in the wave table. The larger the wave table, the finer the steps in frequency can be. The downside with this implementation of DDS is that the wave table has a finite length, limiting the number of frequency steps available, in this case about every 0.031 MHz at 4 MHz.
Using assembler with the Pico is something I haven't tried yet. That's why I've limited my "assembly" work to the PIO and DMA. That's something I'll have to look into more. Thanks for watching.
I see W2AEW has episode 88 *Cheap and simple TDR using an oscilloscope and 74AC14 Schmitt Trigger Inverter* where for < $2 those square wave edges can be crisp down to just 2 nano-seconds while safely driving 50 ohm loads.
I just checked it out; pretty cool. Thanks for the info!
Thanks for doing this series, it's been very helpful. I'm about to start integrating a W into an open source piezo printhead controller. Still looking for project ideas?
That sounds like an awesome project. I'm always looking for projects; but I may not get to them. I'm kinda drifted toward assembly language programming for the RP2040 plus a few other surprises. Thanks for watching!
You see the triangle wave in your "new" method? Look at the bad tops. I think that is due to your new op-amp as it would require some bad samples to cave in the top otherwise. I have seen this pattern before.
I think the squashed tops are due to not having a sample at the exact top of the curve. There are only a few frequencies where we can assure that the top of the triangle wave will coincide with a sample. Nyquest is good for sine waves; but can't deal with discontinuities in triangle or even square waves.
Typically DDS oscillators (direct digital synthesis) use phase accumulation prior to wave table look up, i.e the address of the table is incremented by a variable amount to provide the variable frequency. This might be too cpu intensive for the pico , but might be worth a try.
That's interesting! As I was working through this exercise; I wondered if that was a possibility. As I'm outputting one byte per system clock cycle, the RP2040 isn't fast enough to adjust the wave table address on the fly. If you knew which clock pulse would be short (or long) in advance, you might be able to generate a series of wave tables that would match up with the frequency. However, my brain starts to hurt when I think about how I could do that. lol Thanks for watching.
@@LifewithDavid1 It may not be as difficult as it sounds. The top 12bits of the phase accumulator are the address of your table, so if the accumulator is 32bit there's only need to do a simple add and mask operation .
BTW the output of the dac should technically be a pulse (as short as possible) , it is the job of the anti alias filter to reconstruct the waveform.
Hey David, I've been following your series for a long time and I'm currently in need to do a function generator myself for a student project. However I have a changing in my frequency based on an input. As I need many pins for other parts of the project I am not rlly sure how I should do it. I will definetly need an SPI DAC cause I won't have enough pins available for a self made DAC.
Should I convert the data to SPI and then make a pio program that forwards spi data? or should i just calculate the data and let pio transfer it into spi and send it?
and should I manipulate the wavetable (or be switching between 2 wavetables and i manipulate the one that is currently unused) or should I use 1 wavetable and change the frequency of the pio (idk if this works with spi but i could also just let the pio only send a portion of the data to reduce it, can't i? by saying like ignore every 4th data and defining the max possible frequency in the beginning)
I hope you or anyone else who digged deeper into Pio can give me some answers on this.
Thank you if you read the full comment!
What's the maximum frequency for the AWG. If it's relatively slow (like audio), you might be able to use SPI. However, I would guess that the DAC is the most time critical part of your project. You may want to use SPI for your other inputs and outputs and the GPIO ports directly for the DAC. If the waveform is complex or you need sharp edges, you may need a very fast DAC. Run the math and look at the harmonics you will need to accurately describe your wave.
Again, the maximum frequency will determine the direction you should take on changing the state machine clock frequency or modifying the wave table. If it is slow, like less than 1 MHz, then changing the clock frequency will work well. If it is faster than that, then modifying the wavetable may work better. Hope this helps. Good luck!
@@LifewithDavid1 Thank you for your answer! The maximum frequency will actually be pretty low at only 45-55Hz but I will need 32.000 samples per wave at minimum. The wave will be a simple sine wave.
Yeah I thought that changing the clock might just work fine. I will try it out!
@@LifewithDavid1 I might consider sending the data via dma directly to the spi data out without using pio or the core. I would just use a dma channel with the wave-table and another dma toggling it.
would that be possible? or even a better solution? or would it be much worse?
@@gehtniemandenwasan9789 You should be able to do that. Using the PIO for pacing the DMA data can make it easy to control the frequency of the wave. Without a bit of review, I'm not sure how DMA could be paced using another DMA channel (but there probably is a way). PIO and DMA can work together without any assistance from the core. Good luck!
Down the Danube?
The Danube was awesome. Hopefully you saw some of my "shorts" from last fall. The thumbnail represents "The Lady's Man", unfortunately, it's not Courvoisier, but Boone's Farm.
I think that the first and last sample in the wave table should not be the same.
Consider if you stepped through the wave table by one step. You would be putting out a zero value twice in succession. this may be contributing to the jitter.
That's right, that's why I said they should match. Maybe not the best term, but I didn't want to say they were equal; but rather in sequence. I probably should have made it clearer.