Teensy 4.1 I2C issues when using Audio

I'm trying to build a synthesizer using a Teensy. My current test setup consists of:

I'm polling the potentiometers and encoders 1 by 1 in a function called from the loop() section.

The code used to read the values from I2C (using Wire.h):
Code:
    int32_t Unit::readLong(uint8_t addr, uint8_t reg) const
    {
        int32_t value;

        Wire.beginTransmission(addr);
        Wire.send(reg);
        Wire.endTransmission();

        Wire.requestFrom(addr, (uint8_t)4);
        Wire.readBytes((uint8_t *)&value, 4);

        return value;
    }

And I'm writing the RGB values to the leds using:
Code:
    void Unit::writeBytes(uint8_t addr, uint8_t reg, uint8_t *bytes, size_t size) const
    {
        Wire.beginTransmission(addr);
        Wire.send(reg);
        Wire.write(bytes, size);
        Wire.endTransmission();
    }

The display communication is handled through the LiquidCrystal_I2C library.

All seemed to work wel until I added a Ladder filter component to the code (probably because it's a more CPU intensive component). The sound would start but almost instantly hang. Disabling the code for interaction with the LCD made no difference. Disabling the code for polling the I2C potentiometers and encoders fixed the problem.

It felt to me that there was probably something going on with audio interrupts causing I2C communication to fail and hang with timeouts, so I tried wrapping the code that reads from the potentiometers and encoders with AudioNoInterrupts() and AudioInterrupts():
Code:
            AudioNoInterrupts();
            int16_t value = m5Unit8Angle.getAnalogInput(angleId, 127);
            AudioInterrupts();
That seems to fix the problem :D

I tried searching for other people having the similar issues or more information in general about Teensy 4 audio / I2C interrupt interaction, but couldn't find anything useful.

The Audio Library Processor Usage & Interrupts page on the PJRC website mentions:
The AudioNoInterrupts function allows you to briefly suspend the audio library, which allows you to change multiple object's settings and have them all take effect at the same time when the library resumes.
So my usage of AudioNoInterrupts() and AudioInterrupts() methods doesn't seem to be the intended usage of those functions.

The bottom of that page contains a section called Understanding Audio Library Scheduling, which unfortunately only contains several TODOs.

I have the following questions:
  1. Just to be sure: is my I2C code correct?
  2. What could be the cause of the problem and why does using AudioNoInterrupts() and AudioInterrupts() solve the problem?
  3. Is there a better / different way to do the I2C communication I could try to see if it makes any difference?
I'm hoping to get a better understanding of the cause of the problem and perhaps come up with a better solution, perhaps someone here has the knowledge to help me out. Thanks!
 
So my usage of AudioNoInterrupts() and AudioInterrupts() methods doesn't seem to be the intended usage of those functions.

Yup, confirmed.

AudioNoInterrupts() is generally meant for you to call the functions on 1 or more audio objects. Especially if you have 2 or more that work together (like a waveform and envelope) you would set up both and then call AudioInterrupts() to be sure both use the new settings at the same update.

Usually you should not use the Wire library or other I/O which could take substantial time while AudioNoInterrupts() prevents the audio library from updating. Normally you would read inputs to a variable or array, then call AudioNoInterrupts() when all the data you need is sitting in variables, so you can just call the audio library functions as quickly as possible to change parameters.
 
Thanks for the swift reply!

Do you have any idea why the Audio and I2C code together could cause hangs? And why sandwiching the I2C calls between AudioNoInterrupts() and AudioInterrupts() could be solving the problem?

In an attempt to further analyse the problem, I switched Wire.h over to teensy4_i2c (2.0.0-beta.2) using their implementation of the Wire API, and the problem is gone. No need to add the AudioNoInterrupts() and AudioInterrupts() around the I2C calls. I switched back to Wire.h and stuff starts to hang quite quickly again without the AudioNoInterrupts() and AudioInterrupts() around the I2C calls.
 
I switched back to Wire.h and stuff starts to hang quite quickly again without the AudioNoInterrupts() and AudioInterrupts() around the I2C calls.

Can you give me a small but complete program I can run here on a Teensy 4.1 to reproduce the problem?

Please also be specific about which other hardware I need connected to the SDA & SCL pins. Are all 3 of the I2C devices you're using really necessary? Can the problem be reliably reproduced with only 1, like the LCD (and again, be specific about exactly which one... many are on the market with similar names).
 
Can you give me a small but complete program I can run here on a Teensy 4.1 to reproduce the problem?

Please also be specific about which other hardware I need connected to the SDA & SCL pins. Are all 3 of the I2C devices you're using really necessary? Can the problem be reliably reproduced with only 1, like the LCD (and again, be specific about exactly which one... many are on the market with similar names).

Sure, I created a new quick & dirty program (using PlatformIO) to contain the minimum amount of code to reproduce the problem.

main.cpp contains:
Code:
#include <Arduino.h>
#include "USBHost_t36.h"
#include <array>


#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthWaveformDc     dc1;            //xy=512.75,617
AudioSynthWaveformModulated waveformMod1;   //xy=538.75,257
AudioEffectEnvelope      envelope1;      //xy=663.75,617
AudioEffectEnvelope      envelope2;      //xy=663.75,657
AudioEffectMultiply      multiply1;      //xy=858.75,257
AudioSynthWaveformDc     dc2;            //xy=972.75,407
AudioSynthWaveformDc     dc3;            //xy=989.75,467
AudioFilterLadder        ladder1;        //xy=1129.75,266
AudioOutputI2S           i2s1;           //xy=1520.75,280
AudioConnection          patchCord2(dc1, envelope1);
AudioConnection          patchCord3(dc1, envelope2);
AudioConnection          patchCord4(waveformMod1, 0, multiply1, 0);
AudioConnection          patchCord5(envelope1, 0, multiply1, 1);
AudioConnection          patchCord6(multiply1, 0, ladder1, 0);
AudioConnection          patchCord7(dc2, 0, ladder1, 1);
AudioConnection          patchCord8(dc3, 0, ladder1, 2);
AudioConnection          patchCord9(ladder1, 0, i2s1, 0);
AudioConnection          patchCord10(ladder1, 0, i2s1, 1);
// GUItool: end automatically generated code



USBHost usbHost;
MIDIDevice_BigBuffer usbHostMidi(usbHost);

const std::array<float, 128> PROGMEM MIDI_NOTE_FREQ{{8.17580f, 8.66196f, 9.17702f, 9.72272f, 10.3009f, 10.9134f, 11.5623f, 12.2499f, 12.9783f, 13.7500f, 14.5676f, 15.4339f, 16.3516f, 17.3239f, 18.3540f, 19.4454f, 20.6017f, 21.8268f, 23.1247f, 24.4997f, 25.9565f, 27.5000f, 29.1352f, 30.8677f, 32.7032f, 34.6478f, 36.7081f, 38.8909f, 41.2034f, 43.6535f, 46.2493f, 48.9994f, 51.9131f, 55.0000f, 58.2705f, 61.7354f, 65.4064f, 69.2957f, 73.4162f, 77.7817f, 82.4069f, 87.3071f, 92.4986f, 97.9989f, 103.826f, 110.000f, 116.541f, 123.471f, 130.813f, 138.591f, 146.832f, 155.563f, 164.814f, 174.614f, 184.997f, 195.998f, 207.652f, 220.000f, 233.082f, 246.942f, 261.626f, 277.183f, 293.665f, 311.127f, 329.628f, 349.228f, 369.994f, 391.995f, 415.305f, 440.000f, 466.164f, 493.883f, 523.251f, 554.365f, 587.330f, 622.254f, 659.255f, 698.456f, 739.989f, 783.991f, 830.609f, 880.000f, 932.328f, 987.767f, 1046.50f, 1108.73f, 1174.66f, 1244.51f, 1318.51f, 1396.91f, 1479.98f, 1567.98f, 1661.22f, 1760.00f, 1864.66f, 1975.53f, 2093.00f, 2217.46f, 2349.32f, 2489.02f, 2637.02f, 2793.83f, 2959.96f, 3135.96f, 3322.44f, 3520.00f, 3729.31f, 3951.07f, 4186.01f, 4434.92f, 4698.64f, 4978.03f, 5274.04f, 5587.65f, 5919.91f, 6271.93f, 6644.88f, 7040.00f, 7458.62f, 7902.13f, 8372.02f, 8869.84f, 9397.27f, 9956.06f, 10548.1f, 11175.3f, 11839.8f, 12543.9f}};

uint8_t m5Stack8AngleAddr{0x43};

uint8_t lastFilterFreqValue{0};

void readBytes(uint8_t addr, uint8_t reg, uint8_t *bytes, size_t size)
{
    Wire.beginTransmission(addr);
    Wire.write(reg);
    Wire.endTransmission();

    Wire.requestFrom(addr, size);
    Wire.readBytes(bytes, size);
}

void onNoteOn(uint8_t channel, uint8_t note, uint8_t velocity)
{
    Serial.printf("USB MIDI note on : channel%d note %d velocity %d\n", channel, note, velocity);
    waveformMod1.begin(1.0, MIDI_NOTE_FREQ[note], WAVEFORM_SAWTOOTH_REVERSE);
    envelope1.noteOn();
}

void onNoteOff(uint8_t channel, uint8_t note, uint8_t velocity)
{
    Serial.printf("USB MIDI note off: channel%d note %d velocity %d\n", channel, note, velocity);
    envelope1.noteOff();
}


void setup() {
    USBHost::begin();
    Wire.begin();

    AudioMemory(10);

    waveformMod1.frequencyModulation(0.0);

    dc1.amplitude(1.0);

    envelope1.attack(1.0);
    envelope1.hold(0.0);
    envelope1.decay(200.0);
    envelope1.sustain(0.9);
    envelope1.release(450.0);

    ladder1.octaveControl(4.0f);
    dc2.amplitude(0);
    dc3.amplitude(0.3f);


    usbHostMidi.setHandleNoteOn(onNoteOn);
    usbHostMidi.setHandleNoteOff(onNoteOff);

    // Wire.setClock(100000);
    // Wire.setClock(400000);
}

void loop() {
    USBHost::Task();
    usbHostMidi.read();

    // adding a slight delay also solves the problem
    // delay(1);

    u_int16_t angle0Value;
    // removing AudioNoInterrupts() and AudioInterrupts() makes the I2C communication fail
    AudioNoInterrupts();
    readBytes(m5Stack8AngleAddr, 0, (u_int8_t *)&angle0Value, 2);
    AudioInterrupts();

    uint8_t filterFreqValue = round(((4095 - angle0Value) / 4095.0f) * 127);

    if (filterFreqValue != lastFilterFreqValue) {
      lastFilterFreqValue = filterFreqValue;

      float dc2Value = (filterFreqValue / 127.0f) * 2.0f - 1.0f;
      
      dc2.amplitude(dc2Value);

      Serial.printf("angle0Value = %d, filterFreqValue = %d , dc2Value = %f\n", angle0Value, filterFreqValue, dc2Value);
    }
}

I disconnected all hardware except for an I2S stereo DAC (iHaospace Interface I2S PCM5102 DAC Decoder GY-PCM5102), USB MIDI keyboard connected to the USB host connector and a single M5Stack 8Angle connected to SDA and SCL. I have 2K2 pullup resistors connected from SDA and SCL to 3v3.

Playing notes on the keyboard should produce monophonic sound. Turning the first potentiometer from the 8Angle should change the filter resonance. The 8Angle returns a value just below 4095 when it's turned left and 0 turned right (odd choice). Turn the potentiometer about halfway before starting the code, when it's turned left the filter will block all sound. The code shows some debug messages on the serial interface.

Removing AudioNoInterrupts() and AudioInterrupts() makes the I2C communication fail and audio hang. Adding a delay(1) however fixes things again.

Changing the wire clock speed between 100khz and 400khz doesn't seem to make a difference.

I do realize that placing the I2C polling code in the main loop like this causes quite intensive I2C communication. This doesn't seem to cause issues when using AudioNoInterrupts() and AudioInterrupts() around the I2C calls or when using teensy4_i2c (2.0.0-beta.2) however.

Let me know if you need any additional information from me.
 
This one intrigued me, and besides, the M5Stack controllers looked too good not to have a play with...

I think I found "it", although there could be something more subtle going on. It looks as if you can get the occasional NACK when writing the register address, which then stuffs the Wire library quite badly. It may be the NACK is a result of a hardware problem (my I²C cable is about 40cm...), but it should be possible to recover from it. So I dug about a bit, and it looks as if TwoWire::endTransmission() (in WireIMXRT.cpp) needs another error bit checking. Here's my mod, starting about line 117 of the current code:
Rich (BB code):
        // monitor status
        uint32_t status = port->MSR; // pg 2884 & 2891
        if (status & LPI2C_MSR_ALF) {
            port->MCR |= LPI2C_MCR_RTF | LPI2C_MCR_RRF; // clear FIFOs
            return 4; // we lost bus arbitration to another master
        }
       
        if (status & LPI2C_MSR_FEF) {
            port->MCR |= LPI2C_MCR_RTF | LPI2C_MCR_RRF; // clear FIFOs
            // empirical evidence suggests NOT trying to send a STOP condition!
            return 5; // FIFO error
        }
       
        if (status & LPI2C_MSR_NDF) {
            port->MCR |= LPI2C_MCR_RTF | LPI2C_MCR_RRF; // clear FIFOs
            port->MTDR = LPI2C_MTDR_CMD_STOP;
            return 2; // NACK (assume address, TODO: how to tell address from data)
        }

Note that the application should check the error return from TwoWire::endTransmission() and not attempt to request data if it's failed - you'll probably get something stale!
 
Last edited:
@h4yn0nnym0u5e: I can confirm that your patch, combined with checking the error returned by TwoWire::endTransmission(), solves the issue.

Without the patch, TwoWire::endTransmission() will end up in a continuous error 4 "we lost bus arbitration to another master" state.

What is next, a pull request to get this merged?
 
@h4yn0nnym0u5e: I can confirm that your patch, combined with checking the error returned by TwoWire::endTransmission(), solves the issue.

Without the patch, TwoWire::endTransmission() will end up in a continuous error 4 "we lost bus arbitration to another master" state.

What is next, a pull request to get this merged?
Good news, glad it worked for you.

Yes, the next step is a PR. After that it’s up to Paul to accept and merge ready for the next Teensyduino release. That’s a bit of a lottery … there’s huge numbers of PRs awaiting triage, and competing for rather limited bandwidth against very real commercial pressures. But we can hope…
 
PR #43 has now been merged into Teensyduino 1.59 beta 3, though the announcement doesn't mention it. So it should be in the final release (unless it proves to stuff something else up!), all will be lovely and official, and the world a slightly better place...

Great to hear this, thanks! I will try it later on, I changed all my project code to teensy4_i2c so I need to change everything back again to test it out. Unfortunately I have some other projects I have to work on at this moment, so it may take a while.
 
No problem, though it will be comforting to have a Real User’s confirmation that all is well. I’m actually using it myself for a standalone audio patch editor, wrapped into M5w_8angle and M5w_8encoder classes for ease of use. I’m learning a lot about std:: classes and TFT drivers in the process… so, thanks for introducing me to the M5stack ecosystem!
 
Back
Top