Using DMA and PIO for IRIG.B122 timecode generation #16055
Replies: 13 comments 22 replies
-
I have some suggestions that might help.
|
Beta Was this translation helpful? Give feedback.
-
1 & 2 done (let me know if you need more comments) 3 in your example, DMAs feed PIOs in ping-pong mode. Here, the first DMA is chained to the second, but the second feeds the triggered read address of the first one, and does not use the chain mecanism. My code is based on the Micropython example, which works fine. The only difference is I feed the second DMA with only 1 word ( I tried the other way, giving an initial read address to I may not use the Micropython DMA implementation the right way, but I don't see why. |
Beta Was this translation helpful? Give feedback.
-
Thank you for the help. I have a question. If your program works with one DMA, then why do you want to use two DMA's? If you need to use two DMA's then I believe you have two problems:
|
Beta Was this translation helpful? Give feedback.
-
My example with one DMA only send 1 bit wave. I need to send 100 bits, either 0, 1 or marker. They all have differente waveform (0 and 1 bits are shown in the above picture). The idea is to use a second DMA to feed the first one with each bit of the timecode frame. Both DMA are enabled, and only the master DMA ( I made many many tests, by giving an inital read address to I would like to find someone able to write this code in C, using the Pico SDK, to see how it should be done; there may also be a Micropython problem, I can't say. All I know, is I don't have anymore ideas. I spent the last 5 days on this problem... |
Beta Was this translation helpful? Give feedback.
-
According to the RP240 documentation, Writing to the READ_ADDR_TRIG register should trigger the DMA. If not, I don't see the point of these _TRIG registers. And as said, the Micropython DMA example works fine, but it only uses memory to memory transfert, not memory to PWM. I add a quick look at the C code, and it does not use the same technic as described in the Micropython DMA example. Instead of writing to the READ_ADDR_TRIG register, it writes to the normal READ_ADDR register, and both DMA are chained to each other. I made a test, and the code does more things, there are additionnal reconfigurations, but still not working correctly. I will dig further in both C and viper codes. Thanks for the links! PS: BTW, even in the C code, only one DMA is triggered (line 147). But the one corresponding to my |
Beta Was this translation helpful? Give feedback.
-
I guess there is an inifinite loop only if all inc_xxx flags are False? If say inc_read is True, once the read address becomes nul, it should stop? |
Beta Was this translation helpful? Give feedback.
-
What you describe is exactly what I thought I did... I probably missed something; I will study the viper example this week-end. |
Beta Was this translation helpful? Give feedback.
-
Try this. import math
import array
import uctypes
import machine
import rp2
import time
CARRIER_FREQ = 2 # Hz
NB_SAMPLES_PER_SINE = 8192
NB_SINES_PER_BIT = 2
PWM_FREQ = CARRIER_FREQ * NB_SAMPLES_PER_SINE # Hz
# Values are set for that specific PWM_FREQ, and GPIO25
CARRIER = [round(0xee6+0xee5*math.sin(2*math.pi*t/NB_SAMPLES_PER_SINE)) for t in range(NB_SAMPLES_PER_SINE)]
BIT_1_WAVE = array.array('L')
for b in CARRIER:
BIT_1_WAVE.append(b << 16)
CARRIER = [round(0xee6+0x3b9*math.sin(2*math.pi*t/NB_SAMPLES_PER_SINE)) for t in range(NB_SAMPLES_PER_SINE)]
BIT_0_WAVE = array.array('L')
for b in CARRIER:
BIT_0_WAVE.append(b << 16)
print(f"BIT_1_WAVE address: {hex(uctypes.addressof(BIT_1_WAVE))}")
print(f"BIT_0_WAVE address: {hex(uctypes.addressof(BIT_0_WAVE))}")
@micropython.viper
def configure_DMAs(nword:int, H_buffer_line_add:ptr32):
IRQ_QUIET = 0 # Do not generate an interrupt
RING_SEL = 0 # No wrapping
RING_SIZE = 0 # No wrapping
HIGH_PRIORITY = 1
INCR_WRITE = 0 # Non increment while writing
#Setting up the "data" DMA channel 1
TREQ_SEL = 28 # num of PWM4
INCR_READ = 1 # 1 increment while reading
DATA_SIZE = 2 # 32 bit transfer
CHAIN_TO = 0 # Chain to configure channel DMA 0 so it starts again
EN = 1 # Channel is enabled by the configure DMA chan0
DMA_control_word = ((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))
ptr32(0x5000007c)[0] = 0 # DMA Channel 1 Read Address pointer <- not important because reset by DMA0 "configure" channel
ptr32(0x50000074)[0] = uint(0x40050000+0x5c) # DMA Channel 1 Write Address pointer -> PWM4 address
ptr32(0x50000078)[0] = nword # DMA Channel 1 Transfer Count <- length of the Data array buffer
ptr32(0x50000070)[0] = DMA_control_word # DMA Channel 1 Control and Status (using alias to not start immediatly - will be started by DMA chanel 0)
#Setting up the "control" DMA channel 0 - to run the Channel 1 - Vertical Visible Area lines
TREQ_SEL = 0x3f # Max speed, however synchronization is achieved via the PIO irq 1
INCR_READ = 1 # No increment while reading
CHAIN_TO = 0 # chain to itself (no chaining)
EN = 1 # Start channel upon setting the trigger register
DMA_control_word = ((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))
ptr32(0x50000000)[0] = uint(H_buffer_line_add) # DMA Channel 0 Read Address pointer <- data array to reconfigure DMA1
ptr32(0x50000004)[0] = uint(0x5000007c) # DMA Channel 0 Write Address pointer -> DMA1 read_adress alias register 3 (CH1_AL3_READ_ADDR_TRIG ) - trigger the DMA1 start
ptr32(0x50000008)[0] = 1 # DMA Channel 0 Transfer Count <- Just one data (long) array to transfer continuously
ptr32(0x50000010)[0] = DMA_control_word # DMA Channel 0 Control and Status (using alias to not start immediatly - will be started by DMA trigger register)
ptr32(0x50000430)[0] |= 0b0001 #triggers DMA chan0
@micropython.viper
def stopsync():
ptr32(0x50000444)[0] |= 0b000011 # Aborts DMA chan0 and 1
class Test:
def __init__(self, pwmPin):
self._dmaFrame = rp2.DMA()
self._dmaFrame.irq(self._dmaFrameIsr, hard=False)
self._dmaPwm = rp2.DMA()
self._dmaPwm.irq(self._dmaPwmIsr, hard=False)
self._pwm = machine.PWM(pwmPin, freq=PWM_FREQ, duty_u16=0)
def _dmaFrameIsr(self, dma):
print("dmaFrame ended at", dma.read)
def _dmaPwmIsr(self, dma):
print("dmaPwm ended at", dma.read)
def start(self, timecode):
# Built the frame with bits waves addresses _dmaPwm need to send to PWM
frame = array.array('L')
for b in timecode:
if b == '0':
frame.append(uctypes.addressof(BIT_0_WAVE))
elif b == '1':
frame.append(uctypes.addressof(BIT_1_WAVE))
frame.append(0) # mark end of frame
dma_transfers = NB_SINES_PER_BIT*NB_SAMPLES_PER_SINE
configure_DMAs(dma_transfers,frame)
irigb122 = Test(machine.Pin(25, machine.Pin.OUT))
irigb122.start("0101")
time.sleep(5)
stopsync() |
Beta Was this translation helpful? Give feedback.
-
@sk8board thank your very much for this investigation! This is great to have something working, I will be able to go further in this project :o) I will still continue to investigate and try to make it in pure Python, as for now, I'm unable to make it work correctly. I don't think the registers is the only issue (in my previous tests, I did try to directly use the register address as uint, with no success). And if registers are not what they are supposed to be, how can the provided example work? I'm confused. |
Beta Was this translation helpful? Give feedback.
-
Still using the viper implementation, but it works great! |
Beta Was this translation helpful? Give feedback.
-
Ok, now, I would like to continuously send the frame, without breaking the sine wave. My first idea was to use another DMA, in ping-pong mode with the frame DMA, which feeds the wave DMA. But the problem is the wave DMA is chained to the frame DMA, so a new wave address can be loaded as soon as it has finished to output the previous wave. The issue with the ping-pong config is it would require to change the chained DMA to alternatly point to the ping frame DMA, or the pong frame DMA. Any idea if that is possible? Or do you see another way to do that? |
Beta Was this translation helpful? Give feedback.
-
It's the same process as before: one DMA feeds the PWM with the spécific sine wave (which is made of 10 periods with different amplitudes, depending on the bit value), and another DMA feeds the first DMA with the different sine waves addresses. When the second DMA is over (ie has sent all the sine waves addresses matching a complete IRIG.B frame), I need to start over, without glitch in the sine carrier. My problem is to trigger something very quickly when the second DMA is over, to start it again. I dont't think it can be done with an interruption and Python code... |
Beta Was this translation helpful? Give feedback.
-
Here is a version using interrupt to restart the frame DAM. As I expected, the carrier has some jitter... import math
import time
import array
import uctypes
import machine
import rp2
TREQ_PERMANENT = 0x3f
READ_ADD_TRIG_REG = 15 # see RP2040 datasheet "2.5.2.1 Aliases and Triggers"
PWM_CH0_CC = 0x4005000c
PWM_CH1_CC = 0x40050020
PWM_CH2_CC = 0x40050034
PWM_CH3_CC = 0x40050048
PWM_CH4_CC = 0x4005005c
PWM_CH5_CC = 0x40050070
PWM_CH6_CC = 0x40050084
PWM_CH7_CC = 0x40050098
PWM_CHn_CC = [
PWM_CH0_CC, PWM_CH0_CC, # GPIO00, GPIO01
PWM_CH1_CC, PWM_CH1_CC,
PWM_CH2_CC, PWM_CH2_CC,
PWM_CH3_CC, PWM_CH3_CC,
PWM_CH4_CC, PWM_CH4_CC,
PWM_CH5_CC, PWM_CH5_CC,
PWM_CH6_CC, PWM_CH6_CC,
PWM_CH7_CC, PWM_CH7_CC,
PWM_CH0_CC, PWM_CH0_CC,
PWM_CH1_CC, PWM_CH1_CC,
PWM_CH2_CC, PWM_CH2_CC,
PWM_CH3_CC, PWM_CH3_CC,
PWM_CH4_CC, PWM_CH4_CC,
PWM_CH5_CC, PWM_CH5_CC,
PWM_CH6_CC, PWM_CH6_CC # GPIO28, GPIO29
]
DREQ_PWM_WRAP0 = 0x18
DREQ_PWM_WRAP1 = 0x19
DREQ_PWM_WRAP2 = 0x1a
DREQ_PWM_WRAP3 = 0x1b
DREQ_PWM_WRAP4 = 0x1c
DREQ_PWM_WRAP5 = 0x1d
DREQ_PWM_WRAP6 = 0x1e
DREQ_PWM_WRAP7 = 0x1f
DREQ_PWM_WRAPn = [
DREQ_PWM_WRAP0, DREQ_PWM_WRAP0, # GPIO00, GPIO01
DREQ_PWM_WRAP1, DREQ_PWM_WRAP1,
DREQ_PWM_WRAP2, DREQ_PWM_WRAP2,
DREQ_PWM_WRAP3, DREQ_PWM_WRAP3,
DREQ_PWM_WRAP4, DREQ_PWM_WRAP4,
DREQ_PWM_WRAP5, DREQ_PWM_WRAP5,
DREQ_PWM_WRAP6, DREQ_PWM_WRAP6,
DREQ_PWM_WRAP7, DREQ_PWM_WRAP7,
DREQ_PWM_WRAP0, DREQ_PWM_WRAP0,
DREQ_PWM_WRAP1, DREQ_PWM_WRAP1,
DREQ_PWM_WRAP2, DREQ_PWM_WRAP2,
DREQ_PWM_WRAP3, DREQ_PWM_WRAP3,
DREQ_PWM_WRAP4, DREQ_PWM_WRAP4,
DREQ_PWM_WRAP5, DREQ_PWM_WRAP5,
DREQ_PWM_WRAP6, DREQ_PWM_WRAP6 # GPIO28, GPIO29
]
DMA_BASE = 0x50000000
DMA_READ_ADD_TRIG = [
DMA_BASE + 0x03c,
DMA_BASE + 0x07c,
DMA_BASE + 0x0bc,
DMA_BASE + 0x0fc,
DMA_BASE + 0x13c,
DMA_BASE + 0x17c,
DMA_BASE + 0x1bc,
DMA_BASE + 0x1fc,
DMA_BASE + 0x23c,
DMA_BASE + 0x27c,
DMA_BASE + 0x2bc,
DMA_BASE + 0x2fc
]
BIT_SHIFT = [
0, 16, # GPIO00, GPIO01
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16,
0, 16, # GPIO28, GPIO29
]
F_CPU = 125_000_000
CARRIER_FREQ = 1_000
NB_SAMPLES_PER_PERIOD = 1024
NB_PERIODS_PER_BIT = 1
PWM_FREQ = CARRIER_FREQ * NB_SAMPLES_PER_PERIOD
OFFSET = (F_CPU / PWM_FREQ) // 2
AMPLITUDE = OFFSET - 1
PWM_PIN = 1
class Test:
def __init__(self, pwmPin):
self._pwmPin = pwmPin
self._carrierWave = array.array('I')
for t in range(NB_PERIODS_PER_BIT*NB_SAMPLES_PER_PERIOD):
b = round(OFFSET + AMPLITUDE * math.sin(2 * math.pi * t / NB_SAMPLES_PER_PERIOD))
self._carrierWave.append(b << BIT_SHIFT[self._pwmPin])
self._dmaFrame = rp2.DMA()
self._dmaFrame.active(False)
self._dmaWave = rp2.DMA()
self._dmaWave.active(False)
self._dmaWave.irq(self._dmaWaveIsr, hard=True)
self._pwm = machine.PWM(machine.Pin(pwmPin, machine.Pin.OUT), freq=PWM_FREQ, duty_u16=0)
self._frame = array.array('I', 3 * [uctypes.addressof(self._carrierWave)])
def _dmaWaveIsr(self, dma):
# self._dmaFrame.registers[READ_ADD_TRIG_REG] = self._frame # MemoryError!
machine.mem32[DMA_READ_ADD_TRIG[self._dmaFrame.channel]] = uctypes.addressof(self._frame)
def run(self):
dmaWaveCtrl = self._dmaWave.pack_ctrl(enable=True,
high_pri=True,
size=2,
inc_read=True,
inc_write=False,
ring_size=0, ring_sel=False,
chain_to=self._dmaFrame.channel,
treq_sel=DREQ_PWM_WRAPn[self._pwmPin],
irq_quiet=True, # int only occurs at the end of the frame (or it should!)
bswap=False,
sniff_en=False,
write_err=False,
read_err=False)
self._dmaWave.config(write=PWM_CHn_CC[self._pwmPin],
count=NB_PERIODS_PER_BIT*NB_SAMPLES_PER_PERIOD,
ctrl=dmaWaveCtrl)
dmaFrameCtrl = self._dmaFrame.pack_ctrl(enable=True,
high_pri=True,
size=2,
inc_read=True,
inc_write=False,
ring_size=0, ring_sel=False,
chain_to=self._dmaFrame.channel, # no chaining
treq_sel=TREQ_PERMANENT, # unpaced transfert, synchronization is done by PWM wrap
irq_quiet=True,
bswap=False,
sniff_en=False,
write_err=False,
read_err=False)
self._dmaFrame.config(read=self._frame,
write=self._dmaWave.registers[READ_ADD_TRIG_REG:], # must use a slice - See https://github.com/micropython/micropython/issues/16083
count=1,
ctrl=dmaFrameCtrl,
trigger=True) # start DMA
while True:
pass
def cancel(self):
self._dmaFrame.active(False)
self._dmaWave.active(False)
def test():
print("test()")
test = Test(PWM_PIN)
try:
test.run()
finally:
test.cancel()
if __name__ == "__main__":
test() |
Beta Was this translation helpful? Give feedback.
-
Hi!
I would like to generate a French AFNOR NF S87-500 timecode, which is very close to the IRIG.B122. It needs a 1kHz sine carrier, each bit being 10 periods, with amplitude changing at zero crossing over the 10 periods depending on the bit value:
I plan to implement this using DDS, with a RP2040, in Micropython. My idea is to use some DMA, to feed a PIO with either a DAC value + R2R converter, or a PWM value + lowpass filter.
I first tried to use a single DMA to feed a PWM, which controls the brightness of the builtin LED of a RPi Pico. This code works fine:
Then, I used a second DMA to feed the first one with different waveforms. Which does not work:
This code is close to the Micropython example on the DMA doc page.
If I monitor the ctrl registers, I can see that the dmaPwm has a read error, and the dmaFrame has a write error.
So, it seems that the dmaFrame is unable to feed the dmaPwm.
Any idea why?
Beta Was this translation helpful? Give feedback.
All reactions