DDS Sine Generator, Inline Assembler, 2nd Core, Double PWM Output

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
Post Reply
cebersp
Posts: 30
Joined: Mon Feb 08, 2021 12:07 pm

DDS Sine Generator, Inline Assembler, 2nd Core, Double PWM Output

Post by cebersp » Mon Mar 01, 2021 6:36 pm

Hi, perhaps this might be of interest for someone?
It is a Sine Generator using DDS. The wave is generated using the second core. To achieve higher resolution in combination with higher frequency it adds 2 pwm signals with 6bits each. Both channels from pin15 (1.5k) and pin15 (100k) are connected to a 4.7nF filter capacitor.
See: http://www.openmusiclabs.com/learning/d ... index.html

setF(Frequency) sets the frequency. Zero to stop the second core.
Be aware that you have to edit the register address, if you want to use different pins!

EDIT: It turns out, that activity on core0 does influence frequency accuracy of this code running in core1. Frequency of 1kHz sinks to 974Hz. See next post for an improved version.

Enjoy!
Christof

Code: Select all

# DDS Sine Generator in second core by CWE
# Example using Double PWM
# http://www.openmusiclabs.com/learning/digital/pwm-dac/dual-pwm-circuits/index.html 100k and 1.5k + 4.7nF

import time, _thread, sys
from machine import Pin, PWM
import math
import uarray

# Construct PWM object, with LED on Pin(25).
pwmA = PWM(Pin(15, Pin.OUT)) # upper 6 bits
pwmB = PWM(Pin(14, Pin.OUT)) # low 6 bits


# Set the PWM frequency.
pwmFreq= int(125_000_000/64) # 6 bit
pwmA.freq(pwmFreq)
pwmB.freq(pwmFreq)
pwmA.duty_u16(31<<10)
pwmB.duty_u16(31<<10)

sineBufLen= 6249 #1kHz: 6249
sineBuf=uarray.array("H",range(0,sineBufLen))
for x in range(0,sineBufLen):
    xr= x/sineBufLen*2*math.pi
    sineBuf[x]= 2047+int(2047*math.sin(xr)) # 12 bit resolution
    #print(sineBuf[x])

pwmA.duty_u16(0<<10)
pwmB.duty_u16(0<<10)

ddsCtrl = uarray.array('i',[
    0x40050000 + 0x98, # 0 cc7
    sineBufLen, # 4
    1, # 8 
    int((1<<16)*1.0) # 12 1=run, step in half
    ])

@micropython.asm_thumb
def dds(r0, r1): # Buffer, ctrl-array

    mov(r2,r0) # Buffer Start Address
    ldr(r5, [r1,4]) # buffer length
        
    ldr(r4, [r1,0]) # pwm counter compare register
    ldr(r7, [r1,12]) # step index fine
    
    mov(r3,0) # Buffer Index fine
    
    label(nextVal)
    
    lsr(r0,r3,16) # index coarse 
    lsl(r0,r0,1) # index coarse half words
    add(r0,r0,r2)
    ldrh(r0,[r0,0]) # read buffer     regPoke(0x40050000 + 0x98, ((a>>6)<<16) + (a&63))
    lsl(r6,r0,26) # get the lowest 6 bit
    lsr(r0,r0,6) # shift bits right (upper)
    lsl(r0,r0,16)
    
    lsr(r6,r6,26) # lower 6 bits
    add(r0,r0,r6)
    
    str(r0,[r4,0])
    #b(retu)
    
    add(r3,r3,r7) # next buffer index fine
    lsr(r0,r3,16)
   
    cmp(r5,r0) # end not yet reached
    bhi(nextVal)
    
    lsl(r0,r5,16)
    sub(r3,r3,r0)    
    mov(r0,r3)
    
    ldr(r7, [r1,12]) # reload step index repeat?
    cmp(r7,0)
    bne(nextVal)
    
    label(retu)

    


@micropython.asm_thumb
def regPeek(r0): # Address
    mov(r1,r0)
    ldr(r0,[r1,0])

@micropython.asm_thumb
def regPoke(r0, r1): # Address, Data
    str(r1,[r0,0])
    mov(r0,r1)
    
def regSet(adress, mask):
    regPoke(adress, regPeek(adress) | mask)

def setF(f):
    ddsCtrl[3]=int(f/1000*(1<<16))

#sineBuf[0]=1<<6
#print(dds(sineBuf, ddsCtrl))
_thread.start_new_thread(dds, (sineBuf, ddsCtrl))


try:
    while True:
        f=10
        while f<25_000:
            setF(f)
            time.sleep(.2)
            print(f, end=" ")
            f=f*1.5849 #math.sqrt(10)
        
except KeyboardInterrupt:
    ddsCtrl[3]=0
    sys.exit()

   
"""
x=0
delta=10


while True:
    x+= delta
    if x>(sineBufLen-1):
        x-=sineBufLen
    a= sineBuf[x]
    #pwmA.duty_u16( (a<<4) & (255<<8)) # upper 6 bit
    #pwmB.duty_u16( (a<<10) & (255<<8)) # lower 6 bit
    regPoke(0x40050000 + 0x98, ((a>>6)<<16) + (a&63))
    #regPoke(0x40050000 + 0x98, ((a>>6)<<16)) 
 """  



cebersp
Posts: 30
Joined: Mon Feb 08, 2021 12:07 pm

Re: DDS Sine Generator, Inline Assembler, 2nd Core, Double PWM Output

Post by cebersp » Wed Mar 03, 2021 6:29 am

Improved Version with fixed timing using the 1MHz system timer. Updates at 500kHz.
(I still don't know how to finish code cleanly running on the second core. There is some discussion going on about this.)

Christof

Code: Select all

# SinePwmF.py
# DDS Sine Generator in second core by CWE
# Example using Double PWM
# http://www.openmusiclabs.com/learning/digital/pwm-dac/dual-pwm-circuits/index.html 100k and 1.5k + 4.7nF

import time, _thread, sys
from machine import Pin, PWM
import math
import uarray

# Construct PWM object, with LED on Pin(25).
pwmA = PWM(Pin(15, Pin.OUT)) # upper 6 bits
pwmB = PWM(Pin(14, Pin.OUT)) # low 6 bits


# Set the PWM frequency.
pwmFreq= int(125_000_000/64) # 6 bit
pwmA.freq(pwmFreq)
pwmB.freq(pwmFreq)
pwmA.duty_u16(31<<10) # upper
pwmB.duty_u16(0<<10)

sineBufLen= 500 #for 1kHz and 500kHz update frequ
sineBuf=uarray.array("H",range(0,sineBufLen))

def setAmpl(amp):
    global sineBuf
    for x in range(0,sineBufLen):
        xr= x/sineBufLen*2*math.pi
        sineBuf[x]= 2047+int(amp*math.sin(xr)) # 12 bit resolution
        #print(sineBuf[x])
        
setAmpl(1500) # lm358 schafft 0V nicht ganz!

#pwmA.duty_u16(0<<10)
#pwmB.duty_u16(0<<10)

ddsCtrl = uarray.array('i',[
    0x40050000 + 0x98, # 0 cc7
    sineBufLen, # 4
    0x40054000 + 0x28, # 8 TIMERAWL Register 1 MHz S. 553 
    int((1<<16)*1.0) # 12 Index Step <>0 run, step in half words
    ])

@micropython.asm_thumb
def dds(r0, r1): # Buffer, ctrl-array

    mov(r2,r0) # Buffer Start Address
    ldr(r5, [r1,4]) # buffer length
    ldr(r4, [r1,0]) # pwm counter compare register
    mov(r3,0) # Buffer Index fine
    
    label(nextVal)
    
    label(waitLoop)
    ldr(r6, [r1,8]) # Timer address
    ldr(r6, [r6,0]) # 1Mhz Timer
    lsr(r6,r6,1)    # 500kHz
    cmp(r6,r7)
    beq(waitLoop)
    mov(r7,r6)      # store time slice
    
    lsr(r0,r3,16) # index coarse 
    lsl(r0,r0,1) # index coarse half words
    add(r0,r0,r2)
    ldrh(r0,[r0,0]) # read buffer     regPoke(0x40050000 + 0x98, ((a>>6)<<16) + (a&63))
    lsl(r6,r0,26) # get the lowest 6 bit
    lsr(r0,r0,6) # shift bits right (upper)
    lsl(r0,r0,16)
    
    lsr(r6,r6,26) # lower 6 bits
    add(r0,r0,r6)
    
    str(r0,[r4,0])
    #b(retu)

    ldr(r6, [r1,12]) # reload step index repeat?
    cmp(r6,0)
    beq(retu)
    
    add(r3,r3,r6) # next buffer index fine
    lsr(r0,r3,16)
   
    cmp(r5,r0) # end not yet reached
    bhi(nextVal)
    
    lsl(r0,r5,16)
    sub(r3,r3,r0)    
    mov(r0,r3)
    
    b(nextVal)
    
    label(retu)

    
@micropython.asm_thumb
def regPeek(r0): # Address
    mov(r1,r0)
    ldr(r0,[r1,0])

@micropython.asm_thumb
def regPoke(r0, r1): # Address, Data
    str(r1,[r0,0])
    mov(r0,r1)
    
def regSet(adress, mask):
    regPoke(adress, regPeek(adress) | mask)

def setF(f):
    ddsCtrl[3]=int(f/1000*(1<<16))

#sineBuf[0]=1<<6
#print(dds(sineBuf, ddsCtrl))
def ddsWrap(sineBuf, ddsCtrl):
    dds(sineBuf, ddsCtrl)
    print("<<<End of DDS>>>")
    _thread.exit()

_thread.start_new_thread(ddsWrap,(sineBuf, ddsCtrl))

"""
try:
    while True:
        f=27.5
        while f<25_000:
            setF(f)
            time.sleep(.2)
            print(f)
            #f=f*1.25893 #math.sqrt(10)
            f=f*1.0594630943593 # Tone 12te Wurzel aus 2: https://de.wikipedia.org/wiki/Frequenzen_der_gleichstufigen_Stimmung
        
except KeyboardInterrupt:
    ddsCtrl[3]=0
    sys.exit()
"""
   
"""
x=0
delta=10


while True:
    x+= delta
    if x>(sineBufLen-1):
        x-=sineBufLen
    a= sineBuf[x]
    #pwmA.duty_u16( (a<<4) & (255<<8)) # upper 6 bit
    #pwmB.duty_u16( (a<<10) & (255<<8)) # lower 6 bit
    regPoke(0x40050000 + 0x98, ((a>>6)<<16) + (a&63))
    #regPoke(0x40050000 + 0x98, ((a>>6)<<16)) 
 """  



rgcoldeman
Posts: 14
Joined: Sat Jan 30, 2021 11:18 pm

Re: DDS Sine Generator, Inline Assembler, 2nd Core, Double PWM Output

Post by rgcoldeman » Wed Mar 03, 2021 8:09 am

Nice work!

I've done a DDS generator with a completely different approach, it may be some of the techniques can be combined; it is documented on https://www.instructables.com/Arbitrary ... ry-Pi-Pic/
(also a thread here: viewtopic.php?f=21&t=9697 )

Where you use double PWM and assembly on 2nd core, I used R2R DAC and DMA+PIO.

I need more pins (8 per channel) but get higher sampling rate (1 sample per clock cycle - 125Msps)

DMA+PIO usage means neither CPU is used for signal generation.

My PIO program is trivially simple - it just pushes bits to the output pins with a single statement.

From the little bit that I've seen about the PIO, it looks like it might well be able to do PWM or double PWM based on data passed on to it from the DMA. That would leave the CPU's free, and it may be possible to achieve higher sampling rates and a larger number of channels.

cebersp
Posts: 30
Joined: Mon Feb 08, 2021 12:07 pm

Re: DDS Sine Generator, Inline Assembler, 2nd Core, Double PWM Output

Post by cebersp » Wed Mar 03, 2021 2:44 pm

@rgcoldeman,
thank you very much to point me to you project, which shows how to use DMA!
What I don't understand: How do you know, that these channels are not used by anything?

( I had no knowledge of mem32[], so easy.)

Yes, I think, DMA should be able to be clocked at a certain clock rate too and fill the pwm register.
As only one register is used for two pwm channels, only one dma channel should be enough?
Christof

rgcoldeman
Posts: 14
Joined: Sat Jan 30, 2021 11:18 pm

Re: DDS Sine Generator, Inline Assembler, 2nd Core, Double PWM Output

Post by rgcoldeman » Wed Mar 03, 2021 4:23 pm

cebersp wrote:
Wed Mar 03, 2021 2:44 pm
@rgcoldeman,
thank you very much to point me to you project, which shows how to use DMA!
What I don't understand: How do you know, that these channels are not used by anything?

( I had no knowledge of mem32[], so easy.)

Yes, I think, DMA should be able to be clocked at a certain clock rate too and fill the pwm register.
As only one register is used for two pwm channels, only one dma channel should be enough?
Christof
For stand-alone applications there is full control: the DMA channels are not used if I did not explicitly start them up for something else (it is bare-metal, no operating system or multi-tasking). For making a library clearly it'd be better to have the code check what channel is available, and pick 2 free channels (same argument for PIO I guess)

Actually for PWM there are two ways, it can be done through the built-in PWM, or by the PIO itself: I recall seeing examples where it does counting and effectively produce PWM. Both will have their pros and cons...

Post Reply