Using I2S causes Teensy 4.1 to not boot, no setup(), no serial, no debug.

tl,dr: When I declare an `AudioInputI2S` or `AudioOutputI2S` object and upload the code, my Teensy 4.1 won't boot: no USB devices (host computer doesn't even see it), no GPIO activity, nothing. It doesn't even get to `setup()`. Comment out the `AudioInputI2S` declaration, and it boots right up. Because I have no serial console, I'm not sure how to debug this.

Details:
* It happens with either Input or Output I2S objects.
* It does NOT happen with other `AudioStream` objects, like `AudioInputUSB` or `AudioSynthWaveformSine`
* It happens even if the instantiation COULD happen, but doesn't. If I wrap the `new AudioInputI2S()` with a conditional the compiler can't optimize out (eg: `if (digitalRead(PIN_TIED_TO_GROUND) == HIGH)`), it won't boot. Even though the `new AudioInputI2S()` never gets called.

The fact that it happens even when the `new AudioInputI2S()` never gets called, and the fact that it's happening before `setup()` runs, tells me that it's something that's happening with `static` code/constructors. The fact that it's NOT happening with other `AudioStream` objects, tells me it's not in `AudioStream` parent-class code, it's in the `AudioInputI2S` child-class code.

So looking in `class AudioInputI2S` for static code, I see:
Code:
protected:
	static bool update_responsibility;
	static DMAChannel dma;
	static void isr(void);
private:
	static audio_block_t *block_left;
	static audio_block_t *block_right;
	static uint16_t block_offset;

I'm willing to bet that defining some static variables, all set to 0 or NULL, isn't doing this, nor is defining (but not calling) a static function (ISR.) So the most likely candidate is `static DMAChannel dma;` which will have a constructor

It's initialized as:
Code:
DMAChannel AudioInputI2S::dma(false);

Which will call this constructor:
Code:
	DMAChannel(bool allocate) {
		if (allocate) begin();
	}
Which doesn't even do anything!

`DMAChannel` is a child of `DMABaseClass`, which only has this constructor:
Code:
	DMABaseClass() {}
Again, does nothing.

Going back to `DMAChannel()` constructor: If for some reason it DID get to the point where it called `begin()`, then there's some code in `DMAChannel::begin()` that looks like it might be problematic, but looks good:
Code:
void DMAChannel::begin(bool force_initialization)
{
	uint32_t ch = 0;

	__disable_irq();
	if (!force_initialization && TCD && channel < DMA_MAX_CHANNELS
	  && (dma_channel_allocated_mask & (1 << channel))
	  && (uint32_t)TCD == (uint32_t)(0x400E9000 + channel * 32)) {
		// DMA channel already allocated
		__enable_irq();
		return;
	}
	while (1) {
		if (!(dma_channel_allocated_mask & (1 << ch))) {
			dma_channel_allocated_mask |= (1 << ch);
			__enable_irq();
			break;
		}
		if (++ch >= DMA_MAX_CHANNELS) {
			__enable_irq();
			TCD = (TCD_t *)0;
			channel = DMA_MAX_CHANNELS;
			return; // no more channels available
			// attempts to use this object will hardfault
		}
	}
	channel = ch;
[...other stuff...]
I see a `while(1)` and messing with IRQs. Those are the kinds of things that could lead to lock-ups, but in my brief review of the code, I don't see any obvious problems. Never mind the fact that it doesn't look like it gets called.

So, I'm stumped. Any ideas? Am I even looking in the right place?

Here's the real kick-to-the-shins: While writing up this post, I pulled out all my Audio code to put in a dummy program to reproduce the problem AND THE SILLY THING WORKS. IT DOESN'T FAIL. I'm happy to accept that this is a problem in my code, but I can't for the life of me figure out what I'm doing that would conflict with any of this static code in `AudioInputI2S` or `DMAChannel`. Where to even look for that?

Here's my dummy code anyway. This is from Platform.IO. `SOARAudio.h` and `.cpp` are copied directly from my larger project, `main.cpp` just does what it needs to call it. Hopefully someone will see something in my code that's a race condition? Or a resource consumption that when stripped down has plenty of resources? Or something...

I appreciate any help you can provide, or suggests on where to look next. Thank you!

73 de N6MTS
-Mark

main.cpp:
Code:
#include <Arduino.h>
#include "SOARAudio.h"

SOARAudio *audio;

void setup() {
    Serial.begin(115200);
    while (!Serial) delay(500);
    Serial.println("Getting started, waiting 2 seconds for console.");
    delay(2000);
    pinMode(31, INPUT_PULLUP);

    if (digitalRead(31) == LOW) {
        audio = new SOARAudio();
    }
}

void loop() {
    Serial.println("Loop top");
    delay(1000);
}

SOARAudio.h:
Code:
#ifndef SOAR_AUDIO_H
#define SOAR_AUDIO_H
#include <Audio.h>
#include <Wire.h>
#include <map>

class SOARAudio {
public:
    SOARAudio();

    const static uint8_t num_audio_buffers = 12;

    std::map<String, AudioStream *> Streams;
    std::map<String, AudioConnection *> Connections;
    std::map<String, AudioControl *> Controllers;
    // Needs to be public so the Audio library can see it.
    static DMAMEM audio_block_t data[num_audio_buffers];

protected:
    void begin_v2_2b();

};


#endif // SOAR_AUDIO_H

SOARAudio.cpp:
Code:
#include "SOARAudio.h"


DMAMEM audio_block_t SOARAudio::data[num_audio_buffers];

SOARAudio::SOARAudio() {
    Serial.printf("Setting up audio for v2.2b hardrware.");
    begin_v2_2b();
}

void SOARAudio::begin_v2_2b() {
    /*
    AudioSynthWaveformSine   TestTone440Hz;          //xy=621,364
    AudioInputUSB            USB_in;           //xy=643,416
    AudioInputI2S            CODEC_in;           //xy=702,555
    AudioMixer4              mixer2;         //xy=814,457
    AudioMixer4              mixer1;         //xy=815,381
    AudioOutputUSB           USB_out;           //xy=903,554
    AudioOutputI2S           CODEC_out;           //xy=957,420
    AudioConnection          patchCord1(TestTone440Hz, 0, mixer1, 0);
    AudioConnection          patchCord2(TestTone440Hz, 0, mixer2, 3);
    AudioConnection          patchCord3(USB_in, 0, mixer1, 3);
    AudioConnection          patchCord4(USB_in, 1, mixer2, 0);
    AudioConnection          patchCord5(CODEC_in, 0, USB_out, 0);
    AudioConnection          patchCord6(CODEC_in, 1, USB_out, 1);
    AudioConnection          patchCord7(mixer2, 0, CODEC_out, 1);
    AudioConnection          patchCord8(mixer1, 0, CODEC_out, 0);
    AudioControlWM8731       wm8731;       //xy=825,645
    */

    bool ret;

    // Streams
    AudioSynthWaveformSine *testTone = new AudioSynthWaveformSine();
    testTone->amplitude(0.0);
    testTone->frequency(440.0);
    testTone->phase(0.0);
    Streams["TestTone440Hz"] = testTone;

    Streams["USB_in"] = new AudioInputUSB();
    Streams["USB_out"] = new AudioOutputUSB();
    Serial.println("Allocated USB Streams.");
    Streams["I2S_in"] = new AudioInputI2S();
    Streams["I2S_out"] = new AudioOutputI2S();
    Serial.println("Allocated I2S Streams.");
    Streams["MixerUplink"] = new AudioMixer4();
    Streams["MixerDownlink"] = new AudioMixer4();
    Serial.println("Allocated Mixers.");

    // Connections
    Connections["TestToneToUplink"] = new AudioConnection(*Streams["TestTone440Hz"], 0, *Streams["MixerUplink"], 0);
    Connections["TestToneToDownlink"] = new AudioConnection(*Streams["TestTone440Hz"], 0, *Streams["MixerDownlink"], 0);
    Connections["USBToUplink"] = new AudioConnection(*Streams["USB_in"], 0, *Streams["MixerUplink"], 1);
    Connections["USBToDownlink"] = new AudioConnection(*Streams["USB_in"], 1, *Streams["MixerDownlink"], 1);
    Connections["I2SToUSBUp"] = new AudioConnection(*Streams["I2S_in"], 0, *Streams["USB_out"], 0);
    Connections["I2SToUSBDown"] = new AudioConnection(*Streams["I2S_in"], 1, *Streams["USB_out"], 1);
    Connections["MixerUplinkToI2S"] = new AudioConnection(*Streams["MixerUplink"], 0, *Streams["I2S_out"], 0);
    Connections["MixerDownlinkToI2S"] = new AudioConnection(*Streams["MixerDownlink"], 1, *Streams["I2S_out"], 1);

    // Controllers
    AudioControlWM8731 *wm8731 = new AudioControlWM8731();
    ret = wm8731->enable();
    Serial.printf("wm8731.enable(): %s\n", ret ? "True" : "False");
    ret = wm8731->volume(0.0);       // This is the headphone amp, which we aren't using.
    Serial.printf("wm8731.volume(): %s\n", ret ? "True" : "False");
    ret = wm8731->inputLevel(1.0);
    Serial.printf("wm8731.inputLevel(): %s\n", ret ? "True" : "False");
    ret = wm8731->inputSelect(AUDIO_INPUT_LINEIN);
    Serial.printf("wm8731.inputSelect(): %s\n", ret ? "True" : "False");
    Controllers["CODEC"] = wm8731;

    // This is a #define that expects to be run outside of a class.  We have to do this 
    // differently to make sure `data` doesn't leave scope.
    //AudioMemory(12);
    // expands to:
    //{ static DMAMEM audio_block_t data[12]; AudioStream::initialize_memory(data, 12); }
    AudioStream::initialize_memory(data, num_audio_buffers);
}
 
If reading right ... the working code has this explicit call in setup(): audio = new SOARAudio();

Where the failing code has the same code done in the constructor called implicitly when the static element is created?

That could point to the problem - the constructor is being called too early before the system is ready.

Other posts have noted calling for 'stuff' to happen in constructors can cause such troubles.

That might be in > DMAChannel::begin

That should just init values to show it is ready for an explicit .begin() call in setup.
 
When begin_v2_b2() returns all the audio objects have their destructors called and cease to exist. This won't be useful, and won't
work at all because audio objects that do I/O set up DMA and interrupts which are not tidied up in the destructor, there is no
explicit destructor in fact... So the DMA or interrupts are then running after the memory they are using has been de-allocated,
hence immediate crash. [ I think they are actually spraying stuff all over the stack in fact ]

The audio library is intended to be used as per the audio lib design tool, declared globally and statically. There are some threads
here about more dynamic use of the audio library with some other branches of it. If you do go down this route you will have to
use explicit pointers to audio objects in your SOARAudio class.
 
If reading right ... the working code has this explicit call in setup(): audio = new SOARAudio();

Where the failing code has the same code done in the constructor called implicitly when the static element is created?

The failing code is also calling `new SOARAudio()` in `setup()`. But it's wrapped in a conditional that will never be true, so it never actually gets called. The compiler can't optimize the code out, which forces the `static` code elements of `AudioInputI2S` to be run on boot. At least, that's my theory of what's happening.

That could point to the problem - the constructor is being called too early before the system is ready.

Other posts have noted calling for 'stuff' to happen in constructors can cause such troubles.

That might be in > DMAChannel::begin

That should just init values to show it is ready for an explicit .begin() call in setup.

`DMAChannel::begin(true)` might be a problem, but it's being called with `false` which just short circuits and returns.

Hence my confusion. Argh.

When begin_v2_b2() returns all the audio objects have their destructors called and cease to exist. This won't be useful, and won't
work at all because audio objects that do I/O set up DMA and interrupts which are not tidied up in the destructor, there is no
explicit destructor in fact... So the DMA or interrupts are then running after the memory they are using has been de-allocated,
hence immediate crash. [ I think they are actually spraying stuff all over the stack in fact ]

The Audio objects are being `new`'d and stored in a std::map in an object who's pointer is at global scope. So, several pointer levels removed from global scope, but they are not being destructed by going out of scope.

The audio library is intended to be used as per the audio lib design tool, declared globally and statically. There are some threads
here about more dynamic use of the audio library with some other branches of it. If you do go down this route you will have to
use explicit pointers to audio objects in your SOARAudio class.

This has me thinking that the audio stuff may not like the fact that my Audio objects AREN'T created yet. The `static` code may be assuming that the objects also exist in the `global` context and already exist when the `static` code runs. This would be a bit of a bummer from an Object Oriented stand point; it makes it impossible for me to write code that responds to the actual hardware its deployed to and change its configuration in response to a hardware-version detection.

But at the very least, this is easy to test. I'll give it a shot to remove all the object-ness and go back to global objects, see if that fixes the problem.
 
When begin_v2_b2() returns all the audio objects have their destructors called and cease to exist.

Sorry, I think I see the confusion here. The block that displays first is the block from the Audio tool that's comment out. I just used that as a reference when connecting together the objects below it. That CODE block is scrolling, look below the first page where the objects are actually being created.

Having said that, moving everything back to the global scope objects as defined by the Audio tool, didn't fix the problem. :-(
 
Works fine for me here, having transposed it to the Arduino IDE. I had to
  • enable instantiation of the SOARaudio class in setup()
  • change from WM8731 to SGTL5000 because that's what I've got
  • set the volume and test tone amplitudes to non-zero
  • fix Connections["MixerDownlinkToI2S"] = new AudioConnection(*Streams["MixerDownlink"], 0, *Streams["I2S_out"], 1);, as AudioMixer4 only has one output
None of which would appear to have a direct bearing on your issue... I tried with both the current static Audio library and my revised dynamic one, both seem equally happy. I didn't test USB.

Then ... on closer reading, I think you've said you posted a "dummy" program which doesn't reproduce the issue ... which is a bit of an odd question, "why does this code work?": not very usual for the forum. Can you post a minimal example that actually has a problem?
 
Then ... on closer reading, I think you've said you posted a "dummy" program which doesn't reproduce the issue ... which is a bit of an odd question, "why does this code work?": not very usual for the forum. Can you post a minimal example that actually has a problem?

I agree that none of your changes should impact this, and thanks for catching the AudioConnection bug, I'll fix that in my code. :)

I agree that my post was .. awkward.. What I'm actually asking is, how should I even proceed debugging this problem? The normal path of "strip everything away to minimal code to reproduce the problem" led to a program that did NOT reproduce the problem. So, it's likely something in the rest of my code; unfortunately, there's a lot of intertwined "rest of" code, that won't be easy to start stripping away. But that's my only next step, so I'll get started on this today.

Usually I'd start debugging with Serial.println(), or finally take the time to get some experience with GDB, but neither of those are possible because of where it's failing: before any outside communication starts up.

Seriously, thank you everyone for your help. Any frustration leaking through my words is with my situation, not at all with anyone here or the advice you've given.
 
Ah, OK, I understand. I don't use PlatformIO myself, so if that's anything to do with your problem I can't help...

As far as I can tell from other semi-related work, there are no problems caused by having the static elements of Audio library objects compiled in, followed by using 'new' to instantiate them later. This is true even if the objects are mutually-incompatible, for instance AudioOutputI2S and AudioOutputSPDIF (which use the same hardware). However, deleting objects (without using my dynamic audio library) or instantiating multiple copies of incompatible objects definitely causes problems.

As you say, about the only thing to do is to methodically strip stuff out until you have a single change that provokes the observed failure - it's either then blindingly obvious what you've done, or you have something we can look at! I would recommend trying to do this using the Arduino IDE and Teensyduino, simply because it's the most widely-used platform. I tend to use conditional compilation and / or stub functions to strip down functionality; for a really huge project doing that under version control can be useful, just because it makes it easier to see what changes you've made if it takes more than a few hours' work.

The only other thing I can suggest is to look at the startup hooks - I don't know much about these so you'll have to search this forum or the PJRC website. As I understand them they let you inject code fragments at key points of the startup process, and might let you show an LED pattern or trigger a scope to help narrow things down a bit.
 
I’m out running errands now so I’ll hopefully have more time to bang on this later tonight. But the “look at start up hooks” is a good pointer. Thank you.

Before I left, I did the easy part of commenting out all of the intertwined code and it’s still failing. So I’ve got an easier situation to debug now. It would have been worse if commending out the big blob of intertwined code made the problem go away. :)
 
Oh wow, I found it. It's TeensyDebug. Let me clean up everything I've just been hacking at, get it back to a state that doesn't resemble a Rube Goldberg device and verify.
 
Ok, yes, confirmed: It's TeensyDebug [1]. I don't know the mechanism; once I confirmed that was the conflict, I didn't dig any deeper. I've included what my platformio.ini file looked like that wasn't working, in case this helps anyone in the future. Again, I don't know what of this is breaking it, but I completely removed (well, commented out) all the debugging stuff (including from lib_deps) and the problem went away.

No more GDB for me I guess.

Thank you again everyone for your help. I consider this issue closed for me. Hopefully this will help some person from the future who's having similar issues.

[1] https://github.com/ftrias/TeensyDebug

Code:
[env:teensy41]
platform = teensy
board = teensy41
framework = arduino
lib_deps = 
	adafruit/Adafruit GFX Library@^1.10.12
	adafruit/Adafruit BusIO@^1.9.6
	mikalhart/TinyGPSPlus@^1.0.2
	bblanchon/ArduinoJson@^6.18.5
	git@github.com:ftrias/TeensyDebug.git
 	git@github.com:SmittyHalibut/MTP_t4.git
        git@github.com:SmittyHalibut/arduino-dra818.git
        git@github.com:dl9sec/AioP13.git
        git@github.com:Halibut-Electronics/ILI9341_t3.git
monitor_port = /dev/ttyACM0
build_flags = -D USB_MIDI_AUDIO_SERIAL
test_filter = test_*
test_build_project_src = yes


; Tryin GDB debuggin
build_type = debug
; See https://github.com/platformio/platform-teensy/issues/65
; build_unflags = -DUSB_SERIAL
; build_flags = -DUSB_DUAL_SERIAL
;debug_port = /dev/cu.usbmodem61684903 
debug_port = /dev/ttyACM0
debug_tool = custom
debug_load_mode = manual
debug_server = 
debug_init_cmds =
  target extended-remote $DEBUG_PORT
  $INIT_BREAK
  define pio_reset_run_target
  interrupt
  tbreak loop
  continue
  end
  define pio_restart_target
  echo Restart is undefined for now.
  end

debug_init_break =
 
Repeat disclaimer: I Am Not A PlatformIO User! However...

Looking at the line "; See https://github.com/platformio/platform-teensy/issues/65", I did, and it may be you have to put back the "build_unflags = -DUSB_SERIAL". That should leave USB_MIDI_AUDIO_SERIAL defined but not the conflicting USB_SERIAL. I'd expect it to give you serial but no debug (because you only have one serial port). Or possibly debug but no serial?

Somewhere in this forum is a set of edits to give you a USB_MIDI_AUDIO_DUAL_SERIAL option, which may help to make your debug work again.
 
Repeat disclaimer: I Am Not A PlatformIO User! However...

Looking at the line "; See https://github.com/platformio/platform-teensy/issues/65", I did, and it may be you have to put back the "build_unflags = -DUSB_SERIAL". That should leave USB_MIDI_AUDIO_SERIAL defined but not the conflicting USB_SERIAL. I'd expect it to give you serial but no debug (because you only have one serial port). Or possibly debug but no serial?

Somewhere in this forum is a set of edits to give you a USB_MIDI_AUDIO_DUAL_SERIAL option, which may help to make your debug work again.

That's there as an easier way to say "remove the -D USB_SERIAL above" In reality, I've created my own profile that includes Audio, Serial, and MTP. But I backed off from the MTP features and tested with the provided MIDI_AUDIO_SERIAL to verify I didn't screw up my custom USB profile. If I ended up making heavy use of the GDB stuff (never did), I'd have added a second serial port to my custom USB profile.

This is all by way of saying that wasn't the problem. The way these -D's are used (A large "#if defined() - #elif defined()" chain; see https://github.com/PaulStoffregen/cores/blob/master/teensy4/usb_desc.h) only one will apply anyway. If USB_SERIAL was accidentally still defined, it would take precedence over the MIDI_AUDIO_SERIAL. They wouldn't both clobber each other.
 
True, but if USB_SERIAL "wins" then you don't have an audio interface, which will clobber stuff!

Just out of interest I had another play with the TeensyDebug add-on, having failed to get it to work ages ago and given up with it. I did manage to get it working this time, both with USB_MIDI_AUDIO_SERIAL and "Take over Serial" mode and a custom USB_MIDI_AUDIO_DUAL_SERIAL and "Manual device selection". Example code, apologies for any incorrect/irrelevant comments, I just grabbed my minimal Blink code to start from:
Code:
// Derived from Blink example
// make it possible to compile for different hardware 
//#define PRO_MINI
#define TEENSY
#if defined(PRO_MINI)
  #define LED 13
  #define ON HIGH
  #define OFF LOW
  #define TXLED0
  #define TXLED1
  #define RXLED0
  #define RXLED1
#elif defined(TEENSY)
  #define LED 13
  #define ON HIGH
  #define OFF LOW
  #define TXLED0
  #define TXLED1
  #define RXLED0
  #define RXLED1
#else // Pro Micro/Leonardo (USB)
  #define LED 17 // aka RXLED
  #define OFF HIGH
  #define ON LOW
#endif

#include "TeensyDebug.h"
#include "Audio.h"

// GUItool: begin automatically generated code
AudioSynthWaveform       wav1;      //xy=642,262
AudioOutputI2S           i2s1;           //xy=851,263
AudioConnection          patchCord1(wav1, 0, i2s1, 0);
AudioConnection          patchCord2(wav1, 0, i2s1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=853,305
// GUItool: end automatically generated code


// the setup function runs once when you press reset or power the board
void setup() 
{
  debug.begin(SerialUSB1);
  halt_cpu();

  sgtl5000_1.enable();
  sgtl5000_1.volume(0.05);

  wav1.begin(0.5,220.0,WAVEFORM_SINE);
  
  AudioMemory(10);
  
  // initialize digital pin 13 as an output.
  pinMode(LED, OUTPUT);
  Serial.begin(38400);
  TXLED0;

}

// Gives a pulse of variable length when called
// followed by a fixed-length off time
#define LONG 300
#define SHORT 100
#define OFF_TIME 200
void blink(int length, int led=LED)
{
  digitalWrite(led, ON);     // turn the LED on 
  delay(length);             // wait for time given by parameter
  digitalWrite(led, OFF);    // turn the LED off 
  delay(OFF_TIME);           // wait for fixed time
}

// the loop function runs over and over again forever
void loop() {
  blink(LONG);
  blink(LONG);
  blink(SHORT);
  RXLED1;
  delay(SHORT);
  RXLED0;
  Serial.println("Blop!");
  delay(700);              // wait for a second
}
This is a mild torture test as the audio blocks are initialised after the I2S output and waveform are started - this seems fine. Apart from audio stopping when the processor is halted at a breakpoint (the claim is that interrupts continue), everything worked as expected. My worst issue was that TeensyDebug seems really poor at identifying the correct debug COM port, though I did give it a pretty hostile environment: COM5 and COM7 are separate non-Teensy ports, single serial was coming up as COM3, and dual as COM4+COM6!

So my experience is that you can get Teensy 4.1 to run I2S audio, serial and debug all at once, and there must be something else going on with your application.
 
OK, I've encountered this problem again, done some digging, thought I'd found a culprit, and it's now gone away again of its own accord! Very frustrating...

However, for anyone who might stumble across this or a similar issue in the future, I'd possibly narrowed it down to the first call to resetPFD() in cores/teensy4/startup.c, and more specifically to the setting of CCM_ANALOG_PFD_480. Removing the call altogether, or just the change to that one register, seemed to fix the issue without causing any other side-effects. Note there's a second call to resetPFD(), commented "//TODO: is this really needed?" - obviously if you remove the first call, it probably is needed, and should set both analogue PFD registers' values.
 
Last edited:
Back
Top