DMA, adresses

RP2040 based microcontroller boards running MicroPython.
Target audience: MicroPython users with an RP2040 boards.
This does not include conventional Linux-based Raspberry Pi boards.
rgcoldeman
Posts: 14
Joined: Sat Jan 30, 2021 11:18 pm

DMA, adresses

Post by rgcoldeman » Sun Jan 31, 2021 7:50 pm

I would like to use DMA on the pico to continuously stream data every clock cycle to the pins (either directly or indirectly).
I think there is no explicit support for DMA yet in micropython, but I don't mind configuring it through the registers with mem32[] writes.
To do this, I need to pass the address of an array to the DMA registers, so that it knows where to start reading from.
Is there anyway to get access to the address of an array? uctypes is supposed to have a 'getaddressof' function but I get the error "ImportError: no module named 'uctypes'" when doing "'from uctypes import addressof". Any ideas or hints?

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

Re: DMA, adresses

Post by rgcoldeman » Thu Feb 04, 2021 8:08 am

So far, viper seems to work, as least it returns an address in the SRAM area:
.....
ar=array("I",[0 for _ in range(nsamp)])

@micropython.viper
def run(freq: uint):
GPIO_OUT_ptr=ptr32(GPIO_OUT)
ar_ptr=ptr32(ar)
print(hex(ar_ptr))
.....

returns "0x2000c3d0"

another nice thing would be to 'reserve' a full bank exclusively for the samples so that the DMA access to that bank does not compete with processor access to that bank. Is that possible at all in micropython?

User avatar
jimmo
Posts: 2262
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: DMA, adresses

Post by jimmo » Fri Feb 05, 2021 12:16 am

rgcoldeman wrote:
Sun Jan 31, 2021 7:50 pm
I would like to use DMA on the pico to continuously stream data every clock cycle to the pins (either directly or indirectly).
Can this be done using PIO? My understanding is that you can run the SM with a DMA buffer?

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

Re: DMA, adresses

Post by rgcoldeman » Fri Feb 05, 2021 7:34 am

Yes, from what I read in the datasheet it should be possible to do DMA->PIO->GPIO. That's probably the best solution, it gives some extra flexibility on frequency from the PIO clock divider and on the choice of pins. Just not easy for a newbie trying to stick to micropython...

User avatar
jimmo
Posts: 2262
Joined: Tue Aug 08, 2017 1:57 am
Location: Sydney, Australia
Contact:

Re: DMA, adresses

Post by jimmo » Sat Feb 06, 2021 4:28 am

rgcoldeman wrote:
Fri Feb 05, 2021 7:34 am
Just not easy for a newbie trying to stick to micropython...
FWIW, you can control the PIO from MicroPython. See https://github.com/micropython/micropyt ... _ws2812.py for an example that uses data from memory to control a pin.

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

Re: DMA, adresses

Post by rgcoldeman » Sat Feb 06, 2021 8:19 am

jimmo wrote:
Sat Feb 06, 2021 4:28 am
rgcoldeman wrote:
Fri Feb 05, 2021 7:34 am
Just not easy for a newbie trying to stick to micropython...
FWIW, you can control the PIO from MicroPython. See https://github.com/micropython/micropyt ... _ws2812.py for an example that uses data from memory to control a pin.
Thanks! yes it's very powerful. Anyway the pieces are coming together.
The syntax for telling the PIO to output to 8 pins seems a little clunky but works.
Amazing how the PIO can do sensible things with a single statement (wrapping seems implied in the micropython setup)


@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=125000000, out_base=Pin(0))
sm.active(1)

User avatar
marfis
Posts: 206
Joined: Fri Oct 31, 2014 10:29 am
Location: Zurich / Switzerland

Re: DMA, adresses

Post by marfis » Sat Feb 06, 2021 8:43 pm

> when doing "'from uctypes import addressof". Any ideas or hints?

I'm using a local build and this works (uctypes is enabled in mpconfigport.h).

There was an issue in official releases not including some libs (array, possibly others too..) but this has been resolved meanwhile. So you might just try flashing an updated micropython version and try again?

Keep us updated if you make any progress on using DMA + PIO. I would be interested.

mw66
Posts: 3
Joined: Sun Feb 07, 2021 11:45 am

Re: DMA, adresses

Post by mw66 » Sun Feb 07, 2021 1:24 pm

I have only just started looking into DMA, but have created a quick helper class, so thought I would post it here in case it is of any help to anyone. It has limited function at the moment, but can be used to do basic DMA transfers.

Code: Select all

class DmaChannel:
    def __init__(self, channelNumber):
        offset = channelNumber * 0x40
        self.ChannelNumber = channelNumber
        self.ReadRegister = 0x50000000 + offset
        self.WriteRegister = 0x50000004 + offset
        self.TransferCountRegister = 0x50000008 + offset
        self.TriggerControlRegister = 0x5000000C + offset
        self.ControlRegister = 0x50000010 + offset
        self.ControlValue = 0x3F8033 + (channelNumber << 11) #so that the chain value is set to itself
        
    @micropython.viper
    def SetWriteAddress(self, address: uint):
        ptr= ptr32(self.WriteRegister)
        ptr[0] = address
        #self.WriteAddress = address
        
    @micropython.viper
    def SetReadAddress(self, address: uint):
        ptr= ptr32(self.ReadRegister)
        ptr[0] = address
        #self.ReadAddress = address
        
    @micropython.viper
    def SetTransferCount(self, count: uint):
        ptr= ptr32(self.TransferCountRegister)
        ptr[0] = count
        #self.TransferCount = count
        
    @micropython.viper
    def SetControlRegister(self, controlValue: uint):
        ptr= ptr32(self.ControlRegister)
        ptr[0] = controlValue
        self.ControlValue = controlValue
        
    @micropython.viper
    def SetTriggerControlRegister(self, controlValue: uint):
        ptr= ptr32(self.TriggerControlRegister)
        ptr[0] = controlValue
        self.ControlValue = controlValue
    
    @micropython.viper
    def TriggerChannel(self):
        ptr= ptr32(self.TriggerControlRegister)
        ptr[0] = uint(self.ControlValue)
        
    #@micropython.viper
    def SetChainTo(self, chainNumber : uint):
        self.ControlValue  &= ~ 0x7800
        self.ControlValue |= (chainNumber <<11)
        #ptr= ptr32(self.ControlRegister)
        #ptr[0] =  uint(self.controlValue)
        
    def SetByteTransfer(self):
        self.ControlValue  &= ~ 0xC
        
    def SetHalfWordTransfer(self):
        self.ControlValue  &= ~ 0xC
        self.ControlValue |= 0x4
        
    def SetWordTransfer(self):
        #self.ControlValue  &= ~ 0xC
        self.ControlValue |= 0xC
        
    @micropython.viper
    def SetChannelData(self, readAddress : uint , writeAddress : uint, count: uint, trigger : bool):
        ptr= ptr32(self.ReadRegister)
        ptr2= ptr32(self.WriteRegister)
        ptr3= ptr32(self.TransferCountRegister)
        ptr[0] = readAddress
        ptr2[0] = writeAddress
        ptr3[0] = count
        if trigger:
            ptr4= ptr32(self.TriggerControlRegister)
            ptr4[0] = uint(self.ControlValue)

A couple of quick snippets on how to use it:
1 A simple transfer from one array to another

Code: Select all

screen = bytearray(100)
screen1 = bytearray(100)

for x in range (50):
    screen[x]= x
    
DmaChan0 = DmaChannel(0) #dma channel number to use
DmaChan0.SetChannelData(uctypes.addressof(screen), uctypes.addressof(screen1), 30, True) # address of read location, address of write location, number of transfers to do, Trigger transfer?
2: Chain two Channels together, so first channel triggers second channel when it completes its transfer.

Code: Select all

screen = bytearray(100)
screen1 = bytearray(100)
screen2 = bytearray(100)
for x in range (50):
    screen[x]= x

DmaChan0 = DmaChannel(0) 
DmaChan1 = DmaChannel(1) 
DmaChan0.SetChannelData(uctypes.addressof(screen), uctypes.addressof(screen1), 30, False)
DmaChan1.SetChannelData(uctypes.addressof(screen1), uctypes.addressof(screen2), 30, False)

DmaChan0.SetChainTo(1) # channel number to chain to
DmaChan0.TriggerChannel() # start transfer
As I said there are a lot of features that would require manually setting the control value for. Although there are helper functions for a few features, like changing the size of transfer. Default is set to byte transfers.
Last edited by mw66 on Mon Feb 08, 2021 9:03 am, edited 2 times in total.

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

Re: DMA, adresses

Post by rgcoldeman » Sun Feb 07, 2021 6:04 pm

OK it works, it is amazing: true 125Msps AWG without even overclocking, and without CPU!!

Trick was to use a second DMA channel to reconfigure with a single write to the READ register the first.
The DMA sends 32-bit words but the PIO only consumes 8 bits a time, so there is no delay in the reconfiguration.

I soldered an 8-bit R2R DAC to GPIO pins 0-7 and get very neat sine, triangle, saw-tooth and ladder shapes. Each wave consists of 100 samples, and the resulting output is exactly 1.25MHz. Parasitic capacitance or inductance with a characteristic time of ~20ns deform sharp edges a bit, but overall analog bandwidth appears to be at least 20MHz if not more. I have no idea how to make a decent front-end for this, my previous Arduino-based AWG had 381ksps and could be buffered and amplified by a sluggish LM358 opamp.

The code is still a mess but it's a proof of principle, next step would be a nice interface for 20+ shapes, amplitude control, sweeping etc

Code: Select all

# Arbitrary waveform generator for Rasberry Pi Pico
# Requires 8-bit R2R DAC on pins 0-7. Works for R=1kOhm
# Achieves 125Msps when running 125MHz clock
# Rolf Oldeman, 7/2/2021. CC BY-NC-SA 4.0 licence
from machine import Pin,mem32
from rp2 import PIO, StateMachine, asm_pio
from array import array
from utime import sleep
from math import sin,pi

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=125000000, 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  
nsamp=100 #must be a multiple of 4
wave=array("I",[0]*nsamp)
for isamp in range(nsamp):
    val=128+127*sin((isamp+0.5)*2*pi/nsamp) #sine wave
    #val=isamp*255/nsamp                     #sawtooth
    #val=abs(255-isamp*510/nsamp)            #triangle
    #val=int(isamp/20)*20*255/nsamp            #stairs
    
    wave[int(isamp/4)]+=(int(val)<<((isamp%4)*8)) 

#start
startDMA(wave,int(nsamp/4))

#processor free to do anything else
Attachments
Screenshot 2021-02-07 at 19.19.15.jpg
Screenshot 2021-02-07 at 19.19.15.jpg (91.43 KiB) Viewed 897 times
DS1Z_QuickPrint14.png
DS1Z_QuickPrint14.png (51.78 KiB) Viewed 903 times

User avatar
marfis
Posts: 206
Joined: Fri Oct 31, 2014 10:29 am
Location: Zurich / Switzerland

Re: DMA, adresses

Post by marfis » Sun Feb 07, 2021 9:13 pm

cool, looks really good! The PIO's capabilities are pretty awesome.

A small note: I think the snippet is missing the `SetChannelData`method?

Post Reply