|.: Home :.||.: Productions :.||.: Misc :.|
Picogen is a simple signal generator. It can output basic waveforms (sine, sawtooth, noise…) at frequencies ranging from 10 Hz to 100 kHz. This is a modest and inexpensive device based on a Seeed Xiao using a Raspberry RP2040 microcontroller. I use it mainly for testing and troubleshooting audio equipment.
Disclamer: Picogen is by no way a precision measurement tool. It is mainly an experiment on the cheap side, but it is suprisingly effective and helpful for numerous situations.
Features an rough specs:
The project is well suited for DIY. It can be adapted in various ways, for example the Xiao could be replaced with another RP2040 device, like a Pico. I used here a manufactured PCB for the circuit but this is not a requirement. Circuit complexity and component count are low. Picogen is acually an upgrade of my previous Nanogen1 and its very similar circuit was smoothly built on a simple prototyping board. In any case, please fully read these instructions before beginning.
Picogen has three user controls:
There is no visual feedback
There are two outputs: a TS jack socket and two hooks (ground and signal) to attach probes or aligator clips.
Though the generator is mainly designed for audio range, it can go much higher. For some reasons I wanted sharp analogue waveforms even at 20 kHz, so I set the internal sampling rate to 1 MHz, allowing to create acute sawtooths and rectangles at high frequencies. I limited the fundamental generation to 100 kHz to get at least 4 harmonics so the shape is still identifiable. Moreover, the bandwidth of audio devices rarely go over 96 kHz (192 kHz sampling rate).
Second-order sigma-delta modulation with 4× oversampling (running at 4 MHz) brings down the theoretical noise floor very low on the audio range. It is all done in software, but the tight timings require to slightly overclock the RP2040 at 152 MHz. This is not an issue for this microcontroller which usually supports much higher frequencies even if 133 MHz is the maximum official rate.
In the RP2040, one core is dedicated to the waveform generation (16 bits at 1 MHz), and the other one to the sigma-delta modulation. PIO microcode brings data to the GPIO pins, but the non-contiguity of the pin numbers in the Xiao breakout makes this output code more complex than it would be necessary on a Raspberry Pico for example.
Highest output frequencies are filtered out with a 3rd-order low-pass filter set to about 500 kHz.
|Name||Qty||Value||Specific constraint or model|
|C1||1||100 µF||Electrolytic, 25 V, ⌀ 6.3 mm, pitch 2.5 mm|
|C2||1||470 µF||Electrolytic, 10 V, ⌀ 8 mm, pitch 5 mm.|
The higher the better, but even 100 µF can do the job
|C3||1||10 µF||Electrolytic, 10 V, ⌀ 6.3 mm, pitch 2.5 mm|
|C4||1||100 nF||Ceramic, pitch 2.5 mm|
|C5, C6, C10||3||1 µF||Ceramic, pitch 2.5 mm|
|C7||1||3.3 nF||Film, 7.2 × 5.5 mm², pitch 5 mm|
Could be class 1 ceramic (C0G/NP0)
|D1||1||Standard diode||1N4001 or equivalent, DO-41 package|
|J1, J2, J3||3||2 × 1 pin header||Optional. Wires may be soldered directly.|
|Rx1, R3||9||20 kΩ||See § on the R-2R resistor ladder in the building section|
|R15, R18||2||100 Ω|
|RV1||1||10 kΩ linear||horizontal potentiometer, pitch 5 mm|
Alpha RV16AF-41 series
|RV2||1||250 kΩ log|
|SW1||1||Momentary ON switch, pitch 5 mm|
|SW2||1||Panel-mount SPST switch, for the power supply|
|U1||1||5 V linear voltage regulator||LM7805 or equiv., TO-220 package|
|U2||1||RP2040 breakout||Seeed Xiao RP2040|
2 7 × 1 female headers are optional but recommended.
|U3||1||Dual op. amp.||NE5532 or equiv.|
Optional: 8-pin DIP socket.
|Power supply||1||8–15 V||9 V battery socket, 2.1 mm power jack…|
|Mounting hardware||4||M3||⌀ 3 mm. Screws, nuts, 10 mm spacers…|
|Output connectors||2||Could be annything: jack, banana, RCA…|
|Knobs||2||For the potentiometers|
|Wires||⌀ ≤ 0.8 mm for direct soldering on the PCB|
|USB cable||1||Type A to C||For loading the RP2040 code into the Xiao|
Download this archive: picogen-r1.zip (609 kB).
kicad directory contains the schematics and PCB as a
KiCAD 7 project.
kicad/picogen/gerber contains Gerber files that
could be sent to a PCB manufacturer.
src directory contains the RP2040 source code and compiled
code ready for the installation.
The included PCB is double-sided (two layers) and simple enough to be fabricated by any manufacturer. However some traces and gaps are probably too small for manual etching.
It is not required to populate the test points (TPx). If you want to, make a loop with bent leftover legs from soldered parts.
It is also possible to wire everything on a prototyping board (pads or stripes), but I haven’t any design to provide so you’re on your own for creating one from the schematics. The positive point is that it will be less complex than the PCB because you can hard-code the variation you want for the R-2R network.
As generic building instruction, populate the parts sorted by height. First the resistors and diodes, the ceramic capacitors, the IC socket, film capacitors, etc.
The core of the DAC is the R-2R resistor network. It is made of 16 parts, labelled Rx1 and Rx3, where x corresponds to the DAC bit number, from 0 to 7. Highest bits are very sensitive to resistance inaccuracy, which translates to discontinuities in the output curve, measured as harmonic distortion. Therefore a great care must be taken to select and match the resistors for getting consistent values. There are several non-exclusive ways to proceed here.
Whatever you do, don’t take this accuracy business too seriously. There are other factors of inaccuracy we cannot control. RP2040 GPIO outputs are not strictly identical, and the voltage delivered on a given pin may slightly vary depending on the load on the others.
The easiest way is to use sets of 0.1 % 10 kΩ and 20 kΩ resistors. Parts of this accuracy are more expensive, but you only need them for the 3 highest bits (7, 6 and 5). Other bits can use standard 1 % accuracy without compromising the result quality.
Other ways involve measuring the resistors with an ohmmeter. Make sure your meter can measure more accurately than the tolerance of your parts. Measure all the 10 kΩ resistors you can get and collect the results in a spreadsheet. Do the same thing with the 20 kΩ parts. Part identification will be easier if your resistors are provided in a single cut of tape. Then sort your results by resistance value. You can compute their mean value, as it sometimes deviates from the theoretical value. You will have more resistors close to the mean value, which is an important information to know. From there, two possibilities.
You can use serial and parallel combinations. If the errors of two resistors are of the same magnitude but in opposite directions, pairing them in serial or parallel will cancel the error almost completely. Therefore you can form serial pairs of 10 kΩ to obtain 20 kΩ and parallel pairs of 20 kΩ to obtain 10 kΩ. The PCB allows single or pairs of resistors to be populated. Single resistors are laid horizontally over a group of 4 holes, covering two unused holes, as shown in the pictures of the model I built. Pairs of resistors are mounted vertically in the same space, one next to each other, using adjacent holes. Parallel and serial association types are handled by the PCB. As mentioned previously, focus on the highest bits first.
An cheap way to do this is to purchase a large amount of resistors of the same value, say 100 pieces of 10 kΩ (we need 10 kΩ resistors everywhere so the remaining ones won’t be lost). They often come as cut tape which makes identification easier. Then use serial pairs of them to obtain the 20 kΩ values.
As an alternative to pairing, you can use the
spreadsheet from the archive.
Build the ladder step by step, letting the spreadsheet calculate the best
value for the next step to minimize the final error.
Fill the green cells (“Selected” and “Sel. ref. #” columns) with the values of the chosen resistors and their associated identifiers. The “Suggested” column indicates what is the most appropriate value for the next resistor. You can start from the lowest bit (top) or the highest bit (bottom). Once again, it is recommended to start from the highest one.
If the mean value of your set is different from 10 kΩ, you can set it to the “Rbase” cell. You can even change single cells in the “Target” column for finetuning, depending on the evolution of your resistor set while building the ladder.
It is probably possible to automate everything and provide the algorithm with just a list of resistors, but I’m too lazy to implement this.
It’s up to you for the power supply and the enclosure. Prefer the latter in metal for basic electromagnetic compatibility. The centers of the potentiometers are located out of the board (pins are just at the edge), so the global footprint is a bit larger that it could have been. Pots and the switch are mounted on the back of the PCB.
The PCB file contains in the user layer the dimensions to drill the enclosure when using the specified parts. You may have to change the potentiometer locations if you use other parts. Specified pots and switch work great with 9–10 mm spacers between the board and the panel.
Controls (potentiometers and switch) are mounted on the board but it’s possible to mount them remotely on the front panel and connect them with wires. If you keep the controls on the board, solder them only once everything is mount (even temporarily). Therefore you could account for slight misplacements without forcing on the solder joints once the complete circuit is being mount in the enclosure.
Important: there is no protection for power supply polarity inversion, make sure to connect it correctly or you could fry everything. On all the headers, ground is on pin 1 (square pad). If you need a protection, add a 1N4001 diode between the VCC and ground pins of the power input (J1), cathode on the VCC side.
Also the SW2 power switch is not part of the PCB, make sure to include it if you run Picogen on a battery.
The following instructions are related to the Windows platform. But the Picogen code is not dependent on this system and can be built and loaded from other operating systems. See the Raspberry Pico documentation for more details.
This is an optional step. Make sure you have the Raspberry Pi Pico C/C++ SDK and its associated toolchain installed. The document Getting started with Raspberry Pi Pico contains detailed information about manually installing the toolchain.
If you are using VSCode on Windows:
GCC (version) arm-none-eabi
Check that the
src/rp2-picogen/build/rp2-picogen.uf2 file is
here and replaces the default one.
There is no need to remove the Xiao from your circuit during this operation. However, if you keep it connected, make sure the power is off during the first step.
RPI-RP2should appear on the Windows explorer.
src/rp2-picogen/build/rp2-picogen.uf2on its root.
From there, the Picogen program is stored in the Xiao flash memory, is currently running and will run automatically everytime the Xiao is powered up without holding the Boot button (standard boot method). You can now disconnect the Xiao from your computer.
The gain potentiometer is quite trivial, nothing special to say here. It makes vary the amplitude between −∞ dB and 0 dB. The gain operates in the analogue world, and does only attenuation on the 3.3 Vpp signal.
The frequency pot is a bit more complex: to allow both a large range and accurate settings, the pot operates on a floating sub-range covering about one octave. When the pot is turned fully clockwise or counterclockwise, the floating range starts shifting in the cursor direction. As soon as the cursor quits the ends of the pot course, the floating range stops shifting. Also, there is a non-linear mapping making the potentiometer more accurate near the center of its course.
The button cycles across the waveforms when pressed :
Picogen is actually a constant-rate DAC, the same kind you could find in an audio interface. Waveform synthesis and frequency adjustment are done in software, like it was a softsynth.
The DAC output resolution is 8 bits, sampled at 4 MHz. The input signal resolution is 16 bits, sampled at 1 MHz. Conversion between both systems is achieved with 2nd-order delta-sigma modulation and 4× linear oversampling. This helps improving the dynamic range capability of the output and make it more on par with the input quality.
Each DAC bit is a GPIO pin from the RP2040. The most efficient way to send data stored in memory to the GPIO with accurate timings and good throughput is a PIO state machine fed by a DMA channel. The PIO is a kind of very simple processor within the RP2040. It has its own instruction set and can be programmed to execute a task involving I/O. Here, the PIO reads N bits from the 32-bit word stored in its input FIFO and sends them to its associated range of GPIO pins. Then it waits a few clocks and starts again.
The wait time is calculated to achieve the desired sampling rate. Here the PIO clock is exactly synchronised on the system clock, and the wait time has been configured to follow tightly the delta-sigma calculation rate. Therefore we run at the maximum possible speed, limited by the processing power of the RP2040. Changing the sampling rate is done by changing the system clock.
When a word from the input FIFO is entierly consummed, the DMA is polled to read another word from the memory. The PIO state machine is fed by two DMA channels arranged in ping-pong. When one transfer is finished, it chains directly to the other one, and an interrupt is generated so we can configure the next chaining.
The Xiao exposes only 14 pins and is much more compact than the Pico. But because of this design and the requirement to expose useful pins, the GPIO pins are not contiguous (at least, not 8 of them). This is a problem for the PIO which is designed to send bits to contiguous GPIO numbers.
The solution here is to use as many independent PIO state machines as there are continuous ranges. We need three ranges of GPIO to obtain 8 bits : 0–4, 6–7 and 29. Therefore we have 3 PIO state machines.
But for technical reasons, we also need to duplicate the DMA channels ! So that’s a total of 6 DMA channels required to transfer the samples from memory to physical pins.
Fortunately, multiple DMA and PIO channels can be started at once, so we can keep the 3 machines perfectly synchronised. However there is a small issue: bit ranges accessed by each PIO state machines are different, and for two of them, it is necessary to discard a few bits before reaching the actual payload. This causes a single sytem cycle delay (~7 ns) between bit ranges, but this is not important here.
The aforementioned DMA interrupt triggered by the PIO also starts the delta-sigma modulation code. This code oversamples a buffer of 16-bit data, dithers it and stores it to another buffer of 8 bit data, which will be read by the DMA at the next interrupt.
This happens on the RP2040 Core 1. This core is entierly dedicated to delta-sigma modulation. But there is another DMA interrupt which goes to the Core 0. This is where the synthesis occurs, where we generate the 16-bit buffer processed by Core 1.
The following picture shows how things are handled :
If you want to run the code on another RP2040 breakout like the Raspberry Pico, you may want to do the following modifications:
gpio_freqfor the frequency potentiometer, and
gpio_waveformfor the waveform switch, still in
set_sys_clock_khz()functions handles this. Check the formula in the code (
Cst.h. Usually when the delta-sigma code cannot complete in time, the DMA chaining occurs too late, and the whole signal generation stops quickly after starting up.
PIN_LED_occurences in the files and modify them. You can also disable the whole LED code if you don’t need them at all.
Otherwise, the code should be quite portable.
Side note : the DAC is pretty decent and easy to build. With minor modifications, it would be possible to use it like a regular audio DAC for synthesisers, samplers or whatever playing sounds. This could be a good alternative to the PWM output.
Note : on the picture above, you’ll see that the R-2R network is made of 1 kΩ and 2 kΩ resistors. I think I’ve done a mistake in my first design by reducing the resistor range in order to minimize the effect of leaking current from the output pins. But this effect is probably neglictible compared to the one caused by the overall load increase.
|[Home] [Productions] [Misc]||Website by Laurent de Soras, 2003-2023|