adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.11k stars 1.22k forks source link

audioio.AudioOut(board.A0) using audiocore.RawSample() create wrong waveforms #3796

Closed digixx closed 3 years ago

digixx commented 3 years ago

I am using a Feather M4 Express to send a sequence of voltage-levels (bits) at a defined baudrate. Because of lack of timer interrupts, the function audiocore.RawSample() seems to fill the gap using digital to analog conversion on A0.

But a simple test pattern (squarewave) showed some strange behavior. Just run the code below and watch the DAC (A0) pin on your oscilloscope. In this example I use Pin D4 as trigger signal to sync the OSC.

The very last value of the sequence is not present at the end of the waveform but on the beginning of the next sequence. Sending it twice solves this.

import audiocore
import audioio
import board
import time
import digitalio
import array

Trigger = digitalio.DigitalInOut(board.D4)
Trigger.direction = digitalio.Direction.OUTPUT
dac = audioio.AudioOut(board.A0)

level_high = 1000
level_low = 0

while True:
    level_high += 10
    if level_high < 65535:
        level_mid = level_high // 2
        sequence = array.array('H',[level_low, level_high, level_low, level_high, level_low, level_high, level_low, level_high, level_mid, level_mid])
        data2send = audiocore.RawSample(sequence, sample_rate = 1000)
        Trigger.value = True
        dac.play(data2send, loop = False)
        while dac.playing:
            pass
        Trigger.value = False
        time.sleep(0.02)
    else:
        level_high = 1000
tannewt commented 3 years ago

Would you mind posting a picture or screenshot of the oscilloscope? That'll show the issue in a second way.

digixx commented 3 years ago

Yellow => DAC Signal Blue => Trigger Helper Sgnal

32767 This is the waveform with level_high = 32767

32768 Waveform with high_level = 32768

65535 Waveform with high_level = 65535

If you run the program posted above, you will see that at some high_levels values the waveform changes randomly the height of the pulses

tannewt commented 3 years ago

Ok, thanks for the images! I'm not sure what the issue is and am not sure when we'll have time to look at it. Let me know if you'd like to help debug it. I'm happy to give pointers.

digixx commented 3 years ago

I am happy to help debuging this issue.
What interests me most, how is the DAC initialized and in which mode it is operated. Is this software part of open source?

As a check, I had run this code, The waveform looks ok.

analog_out = AnalogOut(board.A0)
while True:
    for i in range(0, 65535, 64):
        analog_out.value = i

DSC01513

Maybe the DAC is not operated in the same mode for DMA-output and AnalogOut()

tannewt commented 3 years ago

Awesome! Yes, it is all open source.

AnalogOut is not DMA'd and the source is here: https://github.com/adafruit/circuitpython/blob/main/ports/atmel-samd/common-hal/analogio/AnalogOut.c

AudioOut is DMA'd and the source is here: https://github.com/adafruit/circuitpython/blob/main/ports/atmel-samd/common-hal/audioio/AudioOut.c

Build instructions for CircuitPython are here: https://learn.adafruit.com/building-circuitpython

tannewt commented 3 years ago

The discord is a good place to chat with us too. https://adafru.it/discord in the #circuitpython channel

kevinjwalters commented 3 years ago

I think this is same issue as Adafruit Learn: SAMD51 M4 DAC has problems getting it up.

digixx commented 3 years ago

Here is a video about the waveform counting from 1000 to 65535

https://www.dropbox.com/s/9r620vbzrwsiiid/MAH01524_2.mp4?dl=0

digixx commented 3 years ago

Hi there,

Today I did some tests with the DAC.

This is the software I use making the tests

import audiocore
import audioio
from analogio  import AnalogOut
import board
import time
import digitalio
import array

# Trigger signal for oscilloscope
Trigger = digitalio.DigitalInOut(board.D4)
Trigger.direction = digitalio.Direction.OUTPUT

dac = audioio.AudioOut(board.A0)

def play_sequence(sequence):
    data2send = audiocore.RawSample(sequence, sample_rate = 1000)
    dac.play(data2send, loop = False)
    while dac.playing:
        Trigger.value = True
    Trigger.value = False
    time.sleep(0.01)

def create_static_sequence():
    return array.array('H',[16384, 0, 2000])

while True:
    play_sequence(create_static_sequence())

The DAC RawSample() array contain three values: 16384, 0, 2000 This array is played every 10ms. While dac.playing is True, digital pin D4 is set to high. DAC_init

Beginning with the DAC init glitch, the output of the DAC rises to value 0x8000. This happens when the DAC is initialised by dac.play(). During while dac.playing is true, Trigger.value is set to high. DAC_init_zoom

It can be seen that during the first sample-rate-period the value of quiescent_value is set. One ms later the first value (16384) of the RawSample() array is set. Another one ms later the value of 0 follows. The falling signal of Trigger.value shows the end of playing.

But where is the last value of 2000 showing up? Look at the next picture. DAC_next_play

When Trigger goes high, the last played value (0) is showed on output for one period like the quiescent_value after the DAC initialisation. It looks like buffer handling misses the last value and reaches the end one value later on the next cycle.

Using DMA on DAC is a very fine feature to create precisely timed artificial waveform for all kind of purposes. Ramping up and down the DAC output to a static value does not make a sence to me. It will always jump from the initial level to the first in the array. I would recommend to init the DAC quiescent_value to the first value of the array. After the array is played, keep the last value.

This picture shows the signals when the software is restarted (aka saving the file) DAC_deinit_init

How can I debug the C code during runtime? In CircuitPython I can use print().

tannewt commented 3 years ago

Ramping up and down the DAC output to a static value does not make a sence to me. It will always jump from the initial level to the first in the array. I would recommend to init the DAC quiescent_value to the first value of the array. After the array is played, keep the last value.

I'd be ok with this change. That sounds reasonable.

How can I debug the C code during runtime? In CircuitPython I can use print().

In C you can do mp_printf(&mp_plat_print, <format>, <variables>);. I generally use GDB if that isn't enough. My process is documented here: https://learn.adafruit.com/debugging-the-samd21-with-gdb

digixx commented 3 years ago

Hi,

Now, after many hours of source code readings, trying some debug code, I have to say that I am lost. I never wrote much code in C. Mostly some small Arduino code running the bare metal. But the code base for CircuitPython is using so much wrapper code to have all the boards supported is killing me. Compared to one of my very first software on a C64, printing Mandelbrot fractals on the screen, this is beyond my brain.

Back to the topic

If a RawSample() is played, only the first cycle has all the problems documented in the earlier posts. When running with "loop=True" you cannot see it in the following output cycles.

array.array('H',[0, 45000, 20000, 3000])

DSC01551

At the very first beginning of my project, I wanted to create a self calculated waveform in python, played over the DAC to a radio sending digital data. Then i found out that the memory wasn't sufficient for 3 seconds of sound. Then I outsourced the waveform generation to an ATMEL Tiny 412 Controller. Now the CircuitPython board only has to calculate the bits for the data, not the waveform itself. Because there are no timer interrupts available in CircuitPython, I tryed to use audiocore.RawSample() to send timed signals to the 412 generating the waveform. And this is my problem: The first cycle is corrupt. But my project only needs one shot.

If I use AnalogOut(board.A0) it works like a charm, but I have no timed events at hand to set the next value at the right time.

kevinjwalters commented 3 years ago

@digixx As long as CIRCUITPY isn't mounted by a host over USB maybe you could write the data to a wav file and then play that back?

digixx commented 3 years ago

@kevinjwalters Yes, technically possible, but is it worth? I dont think so. Out of curiosity I will create a defined wav file and see if it is played correctly.

kevinjwalters commented 3 years ago

Oh, I was just talking about addressing the size (> memory) not the "fidelity" of the output. The SAMD51 DACs just seem to have a lot of strange behaviour. Asking the manufacturer would seem to be the next step here with some simple example C code.

digixx commented 3 years ago

HEUREKA!

We have here different problems like wrong signal levels and missing the last value of the array. I noticed that small changes in values are better converted than a jump from 0 tp 65535. Creating an sample array containing several hunderts of 0,65535,0,... I observed that when a level was low the next level was higher than the average max level. Touching the wire of A0 also injected some noise which can be seen on the scope. Energie problem?

Yes. In Circuitpython the max sample rate is limited to 1M. The SAM D5X data sheet shows some setting for the DAC at 47.6.9.2 Output Buffer Current Control. In the source the register is set with DAC_DACCTRL_CCTRL_CC100K instead of DAC_DACCTRL_CCTRL_CC12M. With this setting the output is like it has to be. Full swing from 0 to 65535 without any power problems.

kevinjwalters commented 3 years ago

I think 1 Msps is the physical limit of the hardware? And is this the same DAC setting as the one in https://github.com/adafruit/circuitpython/issues/2099#issuecomment-526865939 and Adafruit Forums: What's fastest practical sample_rate for M4 DACs?? Oh, re-reading that forum post it looked like CircuitPython had and maybe still has a lower max sample rate, I'd forgotten about that.

How are you changing the hardware settings? In C code or recompiling CircuitPython?

digixx commented 3 years ago

I changed the code in \circuitpython\ports\atmel-samd\common-hal\audioio\AudioOut.c

starting at line 189

if (channel0_enabled) {
        #ifdef SAMD21
        DAC->EVCTRL.reg |= DAC_EVCTRL_STARTEI;
        // We disable the voltage pump because we always run at 3.3v.
        DAC->CTRLB.reg = DAC_CTRLB_REFSEL_AVCC |
                         DAC_CTRLB_LEFTADJ |
                         DAC_CTRLB_EOEN |
                         DAC_CTRLB_VPD;
        #endif
        #ifdef SAM_D5X_E5X
        DAC->EVCTRL.reg |= DAC_EVCTRL_STARTEI0;
        DAC->DACCTRL[0].reg = DAC_DACCTRL_CCTRL_CC12M |
                              DAC_DACCTRL_ENABLE |
                              DAC_DACCTRL_LEFTADJ;
        DAC->CTRLB.reg = DAC_CTRLB_REFSEL_VREFPU;
        #endif
    }
    #ifdef SAM_D5X_E5X
    if (channel1_enabled) {
        DAC->EVCTRL.reg |= DAC_EVCTRL_STARTEI1;
        DAC->DACCTRL[1].reg = DAC_DACCTRL_CCTRL_CC12M |
                              DAC_DACCTRL_ENABLE |
                              DAC_DACCTRL_LEFTADJ;
        DAC->CTRLB.reg = DAC_CTRLB_REFSEL_VREFPU;
    }

Then I recompiled it getting the .uf2 file and loaded it to the board. The code does check if the calculated sample rate exceeds 1M and throws an execption (line 375)

tannewt commented 3 years ago

@digixx Please make a PR! That's an awesome find!

digixx commented 3 years ago

@tannewt Can you do it for me? I have problems commit a request do to GPG issues on Ubuntu.

tannewt commented 3 years ago

@digixx I can as a last resort. You can do it through the github website though: https://docs.github.com/en/free-pro-team@latest/github/managing-files-in-a-repository/editing-files-in-your-repository

digixx commented 3 years ago

Ok, I will try. This is all new to me, so please be patient.