What is an arbitrary waveform generator?

It’s a device that creates a wide range of electrical waveforms, allowing users to generate wave shapes like sine, square, and triangular. This versatility makes it ideal for testing and experimenting in fields like electronics, telecommunications, and signal processing.

____________________________________________________________________________________________________________________________________________

This project is based on the Raspberry Pi Pico which is used to create a simple arbitrary waveform generator. A few additional components including an OLED display, switches rotary potentiometers turn the the Pico into a useful signal source suitable for any hobbyist electronics enthusiast.

The circuit works by creating output waveforms through a digital process using a R-2R resistor ladder forming a Digital to Analogue Converter which is driven by 8 GP pins of the Pico. An advantage of this approach is it is easy to generate different wave types including: sine, sawtooth and triangular. The potentiometers provide control of the frequency (20Hz to 1MHz) and output amplitude (0 to 3V). A front panel push button switch sets the output on and off .

The circuit has been deliberately kept as easy to build as possible using readily available components. One slight drawback of this approach is with the operational amplifier MCP602. This a great device, which requires only one supply rail and very few other components. However, in this application, it is a little limited due to its slew rate (2.3V/µs) that causes the signal output amplitude to fall away on the higher frequency range. Therefore, to obtain optimum performance it would be worth considering a different op-amp.

All the construction details can be found here, including the circuit diagram, photographs, micropython code as well as the STL files for the 3D printed case.

The circuit diagram

The code

# Arbitrary Waveform Generator for Raspberry Pi Pico
# Requires 8-bit R2R DAC on pins 0-7. Works for R=2kOhm
# 20Hz to 1MHz sine, sawtooth and triangular waveforms
# front end code and design by G Nicholson 9/12/2023
# wave generation by code from Rolf Oldeman, 7/2/2021. CC BY-NC-SA 4.0 licence
from machine import Pin,mem32,I2C
from rp2 import PIO, StateMachine, asm_pio
from array import array
from utime import sleep
from math import sin,pi
import _thread
import machine
# Oled display
from ssd1306 import SSD1306_I2C
 
# Oled display 0.91″
 
sda=machine.Pin(20)
scl=machine.Pin(21)
i2c = I2C(0, sda=Pin(20), scl=Pin(21))
oled = SSD1306_I2C(128, 32, i2c)
oled.fill(0)
oled.show()
 
#set GPIO pins
fRange_a = Pin(16, Pin.IN, Pin.PULL_UP)
fRange_c = Pin(17, Pin.IN, Pin.PULL_UP)
outType_a = Pin(18, Pin.IN, Pin.PULL_UP)
outType_c = Pin(19, Pin.IN, Pin.PULL_UP)
GO =Pin(15, Pin.IN, Pin.PULL_UP)
led_onboard=machine.Pin(25,machine.Pin.OUT)
potentiometer=machine.ADC(26)
 
#intial settings
setFreq=1
outType=1
setFreq=2000
 
 
def read_freqControl():
   
    global  potentiometer,setFreq, fRange_a, fRange_c, outType_a, outType_c, outType
    sleep(0.3)
    print(setFreq)
    setFreq=int((potentiometer.read_u16() / 65535) *100)
     
    if fRange_a.value() == 1 and fRange_c.value()==0:      
        setFreq = (setFreq*10000)
    if fRange_a.value() == 0 and fRange_c.value()==1:      
        setFreq = (setFreq*1000000)
    if fRange_a.value() == 1 and fRange_c.value()==1:
        setFreq = (setFreq*100000)
    if outType_a.value() == 0:      
        outType= 3
    if outType_c.value() == 0:      
        outType= 1
    if outType_a.value()==1 and outType_c.value()==1:
        outType = 2
       
 
 
while True:
   
    read_freqControl()
    if setFreq <=2000:
        setFreq=2000
   
    oled.fill(0)
    oled.text(“Freq”,5,1,1)
    oled.text(“Wave”,5,24,1)
    oled.text(str(int(setFreq/100)), 50,1, 1)
    oled.text(“Hertz”, 50,12, 1)
    if outType ==1:
        oled.text(“Sine”, 50,24,1)
    if outType ==2:
        oled.text(“Saw”, 50,24,1)
    if outType ==3:
        oled.text(“Tri”, 50,24,1)
    oled.show()
 
   
    if GO.value() == 1:
        print(“loop”)
    else:
        break
 
oled.text(“ON”, 100,24, 1)
oled.show()
 
#sig gen code
DMA_BASE=0x50000000
CH0_READ_ADDR  =DMA_BASE+0x000
CH0_WRITE_ADDR =DMA_BASE+0x004
CH0_TRANS_COUNT=DMA_BASE+0x008
CH0_CTRL_TRIG  =DMA_BASE+0x00c
CH0_AL1_CTRL   =DMA_BASE+0x010
CH1_READ_ADDR  =DMA_BASE+0x040
CH1_WRITE_ADDR =DMA_BASE+0x044
CH1_TRANS_COUNT=DMA_BASE+0x048
CH1_CTRL_TRIG  =DMA_BASE+0x04c
 
PIO0_BASE     =0x50200000
PIO0_BASE_TXF0=PIO0_BASE+0x10
 
#state machine that just pushes bytes to the pins
@asm_pio(out_init=(PIO.OUT_HIGH,PIO.OUT_HIGH,PIO.OUT_HIGH,PIO.OUT_HIGH,PIO.OUT_HIGH,PIO.OUT_HIGH,PIO.OUT_HIGH,PIO.OUT_HIGH),
         out_shiftdir=PIO.SHIFT_RIGHT, autopull=True, pull_thresh=32)
def stream():
    out(pins,8)
 
sm = StateMachine(0, stream, freq=setFreq, out_base=Pin(0))
sm.active(1)
 
#2-channel chained DMA. channel 0 does the transfer, channel 1 reconfigures
p_ar=array(‘I’,[0]) #global 1-element array
@micropython.viper
def startDMA(ar,nword):
    p=ptr32(ar)
    mem32[CH0_READ_ADDR]=p
    mem32[CH0_WRITE_ADDR]=PIO0_BASE_TXF0
    mem32[CH0_TRANS_COUNT]=nword
    IRQ_QUIET=0x1 #do not generate an interrupt
    TREQ_SEL=0x00 #wait for PIO0_TX0
    CHAIN_TO=1    #start channel 1 when done
    RING_SEL=0
    RING_SIZE=0   #no wrapping
    INCR_WRITE=0  #for write to array
    INCR_READ=1   #for read from array
    DATA_SIZE=2   #32-bit word transfer
    HIGH_PRIORITY=1
    EN=1
    CTRL0=(IRQ_QUIET<<21)|(TREQ_SEL<<15)|(CHAIN_TO<<11)|(RING_SEL<<10)|(RING_SIZE<<9)|(INCR_WRITE<<5)|(INCR_READ<<4)|(DATA_SIZE<<2)|(HIGH_PRIORITY<<1)|(EN<<0)
    mem32[CH0_AL1_CTRL]=CTRL0
   
    p_ar[0]=p
    mem32[CH1_READ_ADDR]=ptr(p_ar)
    mem32[CH1_WRITE_ADDR]=CH0_READ_ADDR
    mem32[CH1_TRANS_COUNT]=1
    IRQ_QUIET=0x1 #do not generate an interrupt
    TREQ_SEL=0x3f #no pacing
    CHAIN_TO=0    #start channel 0 when done
    RING_SEL=0
    RING_SIZE=0   #no wrapping
    INCR_WRITE=0  #single write
    INCR_READ=0   #single read
    DATA_SIZE=2   #32-bit word transfer
    HIGH_PRIORITY=1
    EN=1
    CTRL1=(IRQ_QUIET<<21)|(TREQ_SEL<<15)|(CHAIN_TO<<11)|(RING_SEL<<10)|(RING_SIZE<<9)|(INCR_WRITE<<5)|(INCR_READ<<4)|(DATA_SIZE<<2)|(HIGH_PRIORITY<<1)|(EN<<0)
    mem32[CH1_CTRL_TRIG]=CTRL1
 
#setup waveform. frequency is 125MHz/nsamp  
 
def setup_waveform():
    global nsamp,wave,outType
    nsamp=100 #must be a multiple of 4
    wave=array(“I”,[0]*nsamp)
    for isamp in range(nsamp):
        if outType==1:
            val=128+127*sin((isamp+0.5)*2*pi/nsamp) #sine wave
        if outType==2:
            val=isamp*255/nsamp                     #sawtooth
        if outType==3:
            val=abs(255-isamp*510/nsamp)            #triangle
        if outType==4:
            val=int(isamp/20)*20*255/nsamp          #stairs
   
        wave[int(isamp/4)]+=(int(val)<<((isamp%4)*8))
setup_waveform()
#start
startDMA(wave,int(nsamp/4))
 
 
#processor free to do anything else
led_onboard.value(1)
sleep(2)
 
while True:
    if GO.value() == 1:
        print(“End”)
    else:
        machine.reset()