Un generador de señales es un instrumento muy útil en el laboratorio para probar diferentes circuitos. Comprar un instrumento como este puede ser bastante caro, pero esto no tiene que ser así. Con una Raspberry Pi Pico y un puñado de resistores, es posible armar un generador de frecuencias, con diferentes ondas, de frecuencia configurable y de hasta 20MHz, sin muchas complicaciones.
Funcionamiento
El proyecto es bastante simple, consiste en generar una señal dada en un array, dentro del código en MicroPython e ir enviando los datos a través del puerto GPIO de la Pico. Los valores se convertirán en una señal analógica posteriormente a través de un arreglo R-2R, es decir, estaremos convirtiendo una señal digital en analógica.
Señales fundamentales
El código configura 6 señales elementales que puedes usar y combinar entre sí para formar otras formas de onda que te pueden servir. Estas funciones son sine, pulse, gaussian, sinc, exponential y noise. En el ejemplo que incluimos al final puedes encontrar unas porciones de código que puedes descomentar para activar cada función. Los parámetros de cada onda están comentados para que puedas ver cómo reaccionan a distintos valores.
#Señal senoidal, los valores van de 0 a 1.0
wave1=wave()
wave1.amplitude=0.1
wave1.offset=-0.4
wave1.phase=0.0
#Este parametro determina cuantas veces se replica la señal
#sobre el mismo periodo; i.e si replicate=3 f_final=3*f_base
#Si es 0, no hay señal
wave1.replicate=1
wave1.func=sine
wave1.pars=[]
# #Señal pulso, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=pulse
# wave1.pars=[0.1,0.5,0.1]
# #Señal gaussiana, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=gaussian
# wave1.pars=[2] #Usa valores entre 0.5 y 4, ajusta la amplitud acorde al parámetro (determina la longitud)
# #Señal sinc, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=sinc
# wave1.pars=[2] #Usa valores entre 0.5 y 4, ajusta la amplitud acorde al parámetro (determina la longitud)
# #Señal exponencial, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=exponential
# wave1.pars=[4] #Usa valores entre 1 y 4, ajusta la amplitud acorde al parámetro (determina la longitud)
# #Señal de ruido, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=noise
# wave1.pars=[10] #Usa valores entre 1 y 10, ajusta la amplitud acorde al parámetro (determina la longitud)
Código en MicroPython
El código es sencillo y para seleccionar la onda de salida, solo tienes que modificar algunas líneas del código. El código completo lo incluimos al final de la entrada. Al final del código encontrarás una función en donde puedes pasar como parámetros la frecuencia y las señales que quieres mezclar. Un ejemplo de como ajustar la frecuencia y las ondas es este ejemplo que suma una señal seno con una gaussiana a 1 kHz:
#Puedes controlar la amplitud, el offset, la fase de la señal y el tipo de señal, en este caso usamos una señal seno
wave1=wave()
wave1.amplitude=1.0
wave1.offset=0.0
wave1.phase=0.0
wave1.replicate=1
wave1.func=sine
wave1.pars=[0]
#En esta otra señal usamos una señal
wave2=wave()
wave2.amplitude=1.0
wave2.offset=0.0
wave2.phase=0.0
wave2.replicate=0 #Si ponemos 1 aqui la señal se llena de ondas cuadradas
wave2.func=gaussian
wave2.pars=[1]
#Aqui configuramos las señales de salida y la frecuencia, en este caso juntamos un seno con una gaussiana a una frecuencia de 1000
setupwaves(wavbuf[ibuf],1e3,wave1,wave2); ibuf=(ibuf+1)%2
Actualización 16/02/2022: Al realizar algunas pruebas se encontró que debes desactivar la señal wave2 ya sea configurando un valor 0 en replicate o en la amplitud. Esto se debe a que el autor del programa añadió un segundo canal a su ultima revisión, pero se sobreponen las señales.
Diagrama esquemático
Para armar el circuito solo necesitas resistores del mismo valor. Se trata de un arreglo R-2R para 8 bits, que en este caso usa resistores de precisión de 2kOhms. Puedes hacer los arreglos con otros resistores, pero este valor te dará un buen balance de impedancia de salida.

Conclusiones:
Si quieres llegar al rango de frecuencia más alto que puede producir este generador, debes usar una tarjeta de cobre con pistas más anchas. Coloca tu proyecto en un gabinete metálico y usa terminales BNC. Este proyecto se puede mejorar más con un poco de acondicionamiento de señal, usando amplificadores operacionales para aumentar el voltaje de salida. ¡Sigue nuestro blog para más ideas de proyecto con la Raspberry Pi Pico!
Referencias:
Arbitrary Wave Generator With the Raspberry Pi Pico

# Arbitrary waveform generator for Rasberry Pi Pico
# Requires 1 or 2 R2R DACs. Works for R=1kOhm
# Achieves up to 125Msps when running 125MHz clock
# Rolf Oldeman, 7/11/2021. CC BY-NC-SA 4.0 licence
# tested with rp2-pico-20210902-v1.17.uf2
# Revisado por Abraham Camarillo 16/02/2022
# Probado con rp2-pico-20220117-v1.18.uf2
from machine import Pin,mem32
from rp2 import PIO, StateMachine, asm_pio
from array import array
from utime import sleep
from math import pi,sin,exp,sqrt,floor
from uctypes import addressof
from random import random
fclock=250000000 #clock frequency to run the pico default 125MHz. Allow 100-250
nbit_ch1=11 #also type sum of nbit_ch1 and nbit_ch1 in 'stream' function !!
nbit_ch2=11
inv_ch1=False #set true if MSB comes first
inv_ch2=True
sampword=1 #number of samples per 32-bit word
pinbase=0 #first active pin
maxnword=2048 #maximum number of words per buffer (max 8192 = 128kbyte total)
#set desired clock frequency
if fclock<100e6 or fclock>250e6:
print("invalid clock speed",fclock)
exit(1)
PLL_SYS_BASE=0x40028000
PLL_SYS_FBDIV_INT=PLL_SYS_BASE+0x8
PLL_SYS_PRIM =PLL_SYS_BASE+0xc
if fclock<=130000000:
FBDIV=int(fclock/1000000)
POSTDIV1=6 #default 6
POSTDIV2=2 #default 2
else:
FBDIV=int(fclock/2000000)
POSTDIV1=3 #default 6
POSTDIV2=2 #default 2
mem32[PLL_SYS_PRIM]=(POSTDIV1<<16)|(POSTDIV2<<12)
mem32[PLL_SYS_FBDIV_INT]=FBDIV
print('clock speed',FBDIV*12/(POSTDIV1*POSTDIV2),'MHz')
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
CH1_AL1_CTRL =DMA_BASE+0x050
PIO0_BASE =0x50200000
PIO0_TXF0 =PIO0_BASE+0x10
PIO0_SM0_CLKDIV=PIO0_BASE+0xc8
#state machine that just pushes bytes to the pinsß
@asm_pio(out_init=(PIO.OUT_HIGH,)*(nbit_ch1+nbit_ch2),
out_shiftdir=PIO.SHIFT_RIGHT,
autopull=True,
fifo_join=PIO.JOIN_TX,
pull_thresh=(nbit_ch1+nbit_ch2)*sampword)
def stream():
out(pins,22)
#initialize PIO - the frequency setting assumes clock speed of 125MHz
sm = StateMachine(0, stream, freq=125000000, out_base=Pin(pinbase))
sm.active(1)
#2-channel chained DMA. channel 0 does the transfer, channel 1 reconfigures
p=array('I',[0]) #global 1-element array
def startDMA(ar,nword):
#first disable the DMAs to prevent corruption while writing
mem32[CH0_AL1_CTRL]=0
mem32[CH1_AL1_CTRL]=0
#setup first DMA which does the actual transfer
mem32[CH0_READ_ADDR]=addressof(ar)
mem32[CH0_WRITE_ADDR]=PIO0_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
#setup second DMA which reconfigures the first channel
p[0]=addressof(ar)
mem32[CH1_READ_ADDR]=addressof(p)
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
def invbits(x,n):
y=0
for i in range(n):
if (x&(1<<i))>0 : y+=(1<<(n-1-i))
return y
def setupwaves(buf,f,w1,w2):
if sampword==1: mindiv=3
if sampword==2: mindiv=2
if sampword==3: mindiv=1
if sampword==4: mindiv=1
div=fclock/(f*(maxnword*sampword)) # required clock division for maximum buffer size
if div<mindiv: #can't speed up clock, duplicate wave instead
dup=int(mindiv/div)
nword=int(maxnword*dup*div/mindiv+0.5)
clkdiv=mindiv
else: #stick with integer clock division only
clkdiv=int(div)+mindiv
nword=int(maxnword*div/clkdiv+0.5)
dup=1
nsamp=nword*sampword
print("div",div,"clkdiv",clkdiv,"dup",dup,"nword",nword,"nsamp",nsamp)
#fill the buffer
for iword in range(nword):
word=0
for i in range(sampword):
isamp=iword*sampword+i
val1=max(0,min((1<<nbit_ch1)-1,int((1<<nbit_ch1)*(0.5+0.5*eval(w1,dup*(isamp+0.5)/nsamp)))))
val2=max(0,min((1<<nbit_ch2)-1,int((1<<nbit_ch2)*(0.5+0.5*eval(w2,dup*(isamp+0.5)/nsamp)))))
if inv_ch1: val1=invbits(val1,nbit_ch1)
if inv_ch2: val2=invbits(val2,nbit_ch2)
word=word+(val1<<(i*(nbit_ch1+nbit_ch2)))
word=word+(val2<<(i*(nbit_ch1+nbit_ch2)+nbit_ch1))
#print(iword,i,isamp,val,word)
buf[iword*4+0]=(word&(255<< 0))>> 0
buf[iword*4+1]=(word&(255<< 8))>> 8
buf[iword*4+2]=(word&(255<<16))>>16
buf[iword*4+3]=(word&(255<<24))>>24
#print(iword,word)
#set the clock divider
clkdiv_int=min(clkdiv,65535)
clkdiv_frac=0 #fractional clock division results in jitter
mem32[PIO0_SM0_CLKDIV]=(clkdiv_int<<16)|(clkdiv_frac<<8)
#start DMA
startDMA(buf,nword)
#evaluate the content of a wave
def eval(w,x):
m,s,p=1.0,0.0,0.0
if 'phasemod' in w.__dict__:
p=eval(w.phasemod,x)
if 'mult' in w.__dict__:
m=eval(w.mult,x)
if 'sum' in w.__dict__:
s=eval(w.sum,x)
x=x*w.replicate-w.phase-p
x=x-floor(x) #reduce x to 0.0-1.0 range
v=w.func(x,w.pars)
v=v*w.amplitude*m
v=v+w.offset+s
return v
#some common waveforms. combine with sum,mult,phasemod
def sine(x,pars):
return sin(x*2*pi)
def pulse(x,pars): #risetime,uptime,falltime
if x<pars[0]: return x/pars[0]
if x<pars[0]+pars[1]: return 1.0
if x<pars[0]+pars[1]+pars[2]: return 1.0-(x-pars[0]-pars[1])/pars[2]
return 0.0
def gaussian(x,pars):
return exp(-((x-0.5)/pars[0])**2)
def sinc(x,pars):
if x==0.5: return 1.0
else: return sin((x-0.5)/pars[0])/((x-0.5)/pars[0])
def exponential(x,pars):
return exp(-x/pars[0])
def noise(x,pars): #p0=quality: 1=uniform >10=gaussian
return sum([random()-0.5 for _ in range(pars[0])])*sqrt(12/pars[0])
#make buffers for the waveform.
#large buffers give better results but are slower to fill
wavbuf={}
wavbuf[0]=bytearray(maxnword*4)
wavbuf[1]=bytearray(maxnword*4)
ibuf=0
#empty class just to attach properties to
class wave:
pass
#Puedes controlar la amplitud, el offset, la fase de la señal y el tipo de señal, en este caso usamos una señal seno
#Señal senoidal, los valores van de 0 a 1.0
wave1=wave()
wave1.amplitude=0.1
wave1.offset=-0.4
wave1.phase=0.0
#Este parametro determina cuantas veces se replica la señal
#sobre el mismo periodo; i.e si replicate=3 f_final=3*f_base
#Si es 0, no hay señal
wave1.replicate=1
wave1.func=sine
wave1.pars=[]
# #Señal pulso, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=pulse
# wave1.pars=[0.1,0.5,0.1]
# #Señal gaussiana, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=gaussian
# wave1.pars=[2] #Usa valores entre 0.5 y 4, ajusta la amplitud acorde al parámetro (determina la longitud)
# #Señal sinc, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=sinc
# wave1.pars=[2] #Usa valores entre 0.5 y 4, ajusta la amplitud acorde al parámetro (determina la longitud)
# #Señal exponencial, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=exponential
# wave1.pars=[4] #Usa valores entre 1 y 4, ajusta la amplitud acorde al parámetro (determina la longitud)
# #Señal de ruido, los valores van de 0 a 1.0
# wave1=wave()
# wave1.amplitude=1.0
# wave1.offset=0.0
# wave1.phase=0.0
# wave1.replicate=1
# wave1.func=noise
# wave1.pars=[10] #Usa valores entre 1 y 10, ajusta la amplitud acorde al parámetro (determina la longitud)
#En esta otra señal usamos una señal
wave2=wave()
wave2.amplitude=1
wave2.offset=0.0
wave2.phase=0.0
wave2.replicate=0
wave2.func=sine
wave2.pars=[]
#Aqui configuramos las señales de salida y la frecuencia, en este caso juntamos un seno con una gaussiana a una frecuencia de 1000
setupwaves(wavbuf[ibuf],1000,wave1,wave2); ibuf=(ibuf+1)%2