When I first had a look at implementing an Audio USB device for the RP2040 with TinyUSB, I could make things work, but taking up the task to make this work in Arduino was too big.

Then Adafruit released their TinyUSB for Arduino that was quite nicely integrated into the RP2040 Arduino core of Earle Phil Hower.

I was hoping that Adafruit would provide some audio implementation with examples.

Since this did not happen, I decided to give it a try: Here I am documenting my experiences.

Design Goals for my new Adafruit_USBD_Audio class:

The microcontroller should be able to be an USB Audio Device:

  • provide data access via callbacks
  • configure audio info via begin method
  • implement a Speaker (audio sink)
  • implement a Microphone (audio source)
  • provide all potential TinyUSB audio calback methods so that we can easily create a subclass and overwrite them

I was considering of implementing this as a subclass of Stream, but in the end I decided that this might be better done in my AudioTools project.

Device descriptors (the Microphone)

The first challenge was to come up with the device descriptors. I used the ones from the TinyUSB project as starting point. I wanted to have a 2 channels microphone, so I extended the examples for this case and tested it with the original build environment with a Rasperry Pico RP2040.

Next I implemented an Audio Device C++ class in my Adafruit_TinyUSB_Arduino fork that generates the descriptor dynamically and implemented the tinyusb callbacks as virtual methods.

Device descriptors (the Speaker)

TinyUSB was providing this 2 channel speaker example, but this was giving compile errors. So I needed to first do some corrections to make it compile. After my corrections, it was working, so I integrated it into my C++ Aduio class as well.

Testing/Debugging Tools

USB development is quite tricky since it is very difficult to track what’s happening. Using a debugger simply does not work because of the strict timing requirements and even adding logging commands turns out to be too slow.

I was using the following debugging/testing approch (in Linux):

  • I used lsusb to compare my new generated device descriptor with the orignal one and first made sure that they are abolutely equal
  • I used dmesg to check if the usb generated errors
  • I added some digitalWrites in the callback functions and used a logic analyser to check if and how often some functions were called.
  • I used the stacktrace of the debugger to pin down crashes
  • I did some extensive code reviews to make sure that the orignal code was matching the ported C++ code.
  • I used Audacity to test the recording and output functionality. Fortunately there is a Rescan Audio devices in the Transport menu

Problems with CDC (using the Microphone example)

Initially I deactivated CDC and the functionality was working great. But as soon as I activated CDC (Serial support) things initially were working, but when testing, just changing the sample rate in Audacity was crashing the microcontroller.

Here the debugger came to my help and so I found that I was not the first with this issue and that there was even an open pull request that addressed the problem. After applying it to my fork, the issue was gone!

Problems with CDC (using the Speaker example)

When I was implementing the Speaker it was also working w/o CDC and activating CDC was immediately crashing. Here dmesg was giving the hint:

   [29056.169071] usb 1-1.3: config 1 interface 3 altsetting 1 has a duplicate endpoint with address 0x82, skipping

I messed up the endpoints in the device descriptor and after correcting this, things started to work as well.

Current Status

It seems to work quite nicely now. Here is a simple Arduino example microphone test sketch:

#include "Adafruit_TinyUSB.h"

Adafruit_USBD_Audio usb;

size_t readCB(uint8_t* data, size_t len, Adafruit_USBD_Audio& ref) {
  int16_t* data16 = (int16_t*)data;
  size_t samples = len / sizeof(int16_t);
  size_t result = 0;
  // generate random stereo data
  for (int j = 0; j < samples; j+=2) {
    data16[j] = random(-32000, 32000);
    data16[j+1] = random(-32000, 32000);;
    result += sizeof(int16_t)*2;
  }
  return result;
}

void setup() {
  Serial.begin(115200);

  // Start USB device as Microphone
  usb.setReadCallback(readCB);
  usb.begin(44100, 2, 16);

  if (TinyUSBDevice.mounted()) {
    TinyUSBDevice.detach();
    delay(10);
    TinyUSBDevice.attach();
  }
}

void loop() {
  // optionally use LED do display status
  usb.updateLED();
}

Dependencies

https://github.com/pschatzmann/Adafruit_TinyUSB_Arduino

Consult the Wiki to try it out in your environment.


2 Comments

Curtis · 15. October 2024 at 15:34

Hey there, this is really impressive. I’ve taken a look at the branch and the examples but I’m a bit new to USB and trying to figure out how you would interface the speaker example with Arduino-Audio-Tools. How, for example, would you take the USB data and turn it into a stream?

    pschatzmann · 15. October 2024 at 15:39

    After I have everything working, I will extend the AudioTools with an USBAudio Stream.
    For the time beeing you can use use the regular Arduino Print/Stream API in the callback.
    E.g. on an I2SStream you would just do something like i2s.write(data, len);

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *