When I started the Audio DAC project, I wanted to focus on the DAC and amplifier section and not get bogged down with the protocol crap. To this end, I chose to buy an off-the-shelf USB to I2S chip, and built a board around it. Fortunately, it worked as advertised! However, as I designed my DAC board, some limitations showed up.
Specifically, the issue was the I2S master clock. The DAC chip I chose requires a power-of-2 multiple of the input audio rate (e.g. if the input is 48kHz, the I2S clock has to be a power-of-2 multiple of it, e.g. 12.288MHz). However, the chip I chose (CP2615) outputs a fixed 12MHz I2S master clock. So rather than trying to find a different DAC chip, I am looking at some different paths.
The PCM5242 has the option to take an input clock into a GPIO, multiply it up, and then optionally output it via a GPIO. This has seemingly worked, as I showed in my last post. However, I find this annoying for a couple of reasons. One, it doesn’t seem like the place to be mucking with the clock signals – I want the DAC chip to do one thing and do it well: create the analog audio signals. Secondly, this requires having something program the I2C registers to set up the chip. This means adding a microcontroller, which could also do some other things…
…other things like actually receiving the audio packets via USB and outputting them to the DAC via I2S. So let’s see what this might look like. One downside is that a microcontroller has a decent amount of dependencies (e.g. crystals, capacitors, bootloaders, etc) that I had hoped to avoid.
At a previous company, we used STM32 microcontrollers a good deal. They are relatively cheap, powerful and have good documentation and support tools. I happened to have a NUCLEO-F746ZG on hand, so I started there. This particular chip is rather overpowered for this task, but it’s a good starting point. Also, it supports USB device mode and I2S output, which is what’s important for me right now.
A nice thing about the STM32 ecosystem is that they have a configuration tool to get started fairly quickly. This tool is called STM32CubeMX, and it is a nice GUI that helps to set up the peripherals, clocks and such. At least for me, this saves a good amount of time!
They also have their own Eclipse setup that they distribute as STM32CubeIDE. This has STM32CubeMX built in, so it’s a pretty decent way to start a project, compile it, upload it to the board and to debug it (using the handle Nucleo board’s built-in debug interface). All of these things are good!
However, I ran into some annoying things that I wanted to document, as much for my future self as anything!
Setting up a new CubeMX project is pretty straight-forward. I simply selected my board from a list, and we’re off!
Note: it is important to select Yes
on this screen unless you want to manually figure out how to set up all of the clock
sources.
This step compiles the project, uploads it to the board and initializes the debugger, halted at main()
. The first time
you select this option (keyboard shortcut: F11
), you’ll be given a configuration window. I left the defaults.
Selecting Resume (F8
) lets the program run, and everything seems to be off and running.
Ok, the board seems to be booting and running OK. Now let’s enable the USB Audio interface.
Back in the C/C++
perspective, open the .ioc
file, which brings up the CubeMX configuration tool.
Select “Middleware” and then “USB_DEVICE”:
Select “Audio Device Class” in the “Class for FS IP” dropdown box.
Note that this field defaults to 22100 samples/sec. I am not sure why this is, since the code does not appear to support
this; according to a comment at the top of usbd_audio.c
, only 48kHz is supported:
The current audio class version supports the following audio features:
- Pulse Coded Modulation (PCM) format
- sampling rate: 48KHz.
- Bit resolution: 16
- Number of channels: 2
- No volume control
- Mute/Unmute capability
- Asynchronous Endpoints
Change this value to 48000
. Click “Save” to generate the updated code.
Once again, select F11
to debug the project. This rebuilds the .ELF file, flashes the board and prepares the board to
run. Pressing F8
to resume, the project begins to run.
I then plugged my Nucleo board into my Windows 10 Desktop and….
Well that’s no fun. Using the Windows USB Device Viewer
, I got a bit more information about this:
[Port4] FailedEnumeration : Unknown USB Device (Device Descriptor Request Failed)
Is Port User Connectable: yes
Is Port Debug Capable: no
Companion Port Number: 0
Companion Hub Symbolic Link Name:
Protocols Supported:
USB 1.1: yes
USB 2.0: yes
USB 3.0: no
---===>Device Information<===---
ConnectionStatus: FailedEnumeration
Current Config Value: 0x00 -> Device Bus Speed: Full (is not SuperSpeed or higher capable)
Device Address: 0x00
Open Pipes: 0
*!*ERROR: No open pipes!
===>Device Descriptor<===
*!*ERROR: bLength of 0 incorrect, should be 18
bLength: 0x00
bDescriptorType: 0x00
bcdUSB: 0x0000
bDeviceClass: 0x00
*!*ERROR: Device enumeration failure
Huh, this doesn’t make much sense. After a bit of googling, I found some helpful indications: this issue is due to a lack of heap space; the project needs to be reconfigured to increase its heap in order to accomodate the audio buffers.
Back in the C/C++
perspective, open the .ioc
file again. This time, click the “Project Manager” tab:
Update the “Minimum Heap Size” field to 0x2800
. Note that this field is weird, and it seems that if you move the mouse
off of the textbox, it will lose focus. Save the project again (which will re-generate the source files).
Re-run the project (F11
–> F8
–> Connect USB), and… same problem!
Ok, well I know that Windows tries to be smart about remembering devices, so maybe it’s not doing a full enumeration for the device. I uninstalled the device and plugged it back in, but had the same issue.
Interestingly, the project contains two linker files, one for running out of Flash and one for running out of RAM (presumably).
The two files are STM32F746ZGTX_FLASH.ld
and STM32F746ZGTX_RAM.ld
.
The _FLASH.ld
file has been updated correctly:
_Min_Heap_Size = 0x2800 ; /* required amount of heap */
_Min_Stack_Size = 0x400 ; /* required amount of stack */
but the _RAM.ld
file has not:
_Min_Heap_Size = 0x200; /* required amount of heap */
_Min_Stack_Size = 0x400; /* required amount of stack */
I’m not sure why this is, but changing the _RAM.ld
file to have the same _Min_Heap_Size
did not make a difference.
Ok, so I had a known-good project that was working, so I compared against that. For the most part, there was no difference!
However, for some reason, the known-good project had a call to MX_USB_DEVICE_Init()
that was missing from this project
I was creating for this blog post. I’m not really sure how this is configured via the CubeMX interface, but I ended up
disabling the USB Middleware and USB Device options, re-generating the project, then re-enabling the USB Device. On that
page, I enabled the USB On The Go FS global interrupt
first, and then went to enable the USB Middleware.
Once I did that, then re-generated the project, re-built the project and ran it on the target hardware, huzzah, the device enumerated correctly on Windows!
For good measure, here is the updated USB Device Viewer output:
[Port4] : USB Composite Device
Is Port User Connectable: yes
Is Port Debug Capable: no
Companion Port Number: 0
Companion Hub Symbolic Link Name:
Protocols Supported:
USB 1.1: yes
USB 2.0: yes
USB 3.0: no
Device Power State: PowerDeviceD0
---===>Device Information<===---
English product name: "STM32 Audio Class"
ConnectionStatus:
Current Config Value: 0x01 -> Device Bus Speed: Full (is not SuperSpeed or higher capable)
Device Address: 0x30
Open Pipes: 0
*!*ERROR: No open pipes!
===>Device Descriptor<===
bLength: 0x12
bDescriptorType: 0x01
bcdUSB: 0x0201
bDeviceClass: 0x00 -> This is an Interface Class Defined Device
bDeviceSubClass: 0x00
bDeviceProtocol: 0x00
bMaxPacketSize0: 0x40 = (64) Bytes
idVendor: 0x0483 = STMicroelectronics
idProduct: 0x5740
bcdDevice: 0x0200
iManufacturer: 0x01
English (United States) "STMicroelectronics"
iProduct: 0x02
English (United States) "STM32 Audio Class"
iSerialNumber: 0x03
English (United States) "377C315A3234"
bNumConfigurations: 0x01
---===>Full Configuration Descriptor<===---
===>Configuration Descriptor<===
bLength: 0x09
bDescriptorType: 0x02
wTotalLength: 0x006D -> Validated
bNumInterfaces: 0x02
bConfigurationValue: 0x01
iConfiguration: 0x00
bmAttributes: 0xC0 -> Self Powered
MaxPower: 0x32 = 100 mA
===>Interface Descriptor<===
bLength: 0x09
bDescriptorType: 0x04
bInterfaceNumber: 0x00
bAlternateSetting: 0x00
bNumEndpoints: 0x00
bInterfaceClass: 0x01 -> Audio Interface Class
bInterfaceSubClass: 0x01 -> Audio Control Interface SubClass
bInterfaceProtocol: 0x00
iInterface: 0x00
===>Audio Control Interface Header Descriptor<===
bLength: 0x09
bDescriptorType: 0x24 (CS_INTERFACE)
bDescriptorSubtype: 0x01 (HEADER)
bcdADC: 0x0100
wTotalLength: 0x0027
bInCollection: 0x01
baInterfaceNr[1]: 0x01
===>Audio Control Input Terminal Descriptor<===
bLength: 0x0C
bDescriptorType: 0x24 (CS_INTERFACE)
bDescriptorSubtype: 0x02 (INPUT_TERMINAL)
bTerminalID: 0x01
wTerminalType: 0x0101 (USB streaming)
bAssocTerminal: 0x00
bNrChannels: 0x01
wChannelConfig: 0x0000
iChannelNames: 0x00
iTerminal: 0x00
===>Audio Control Feature Unit Descriptor<===
bLength: 0x09
bDescriptorType: 0x24 (CS_INTERFACE)
bDescriptorSubtype: 0x06 (FEATURE_UNIT)
bUnitID: 0x02
bSourceID: 0x01
bControlSize: 0x01
bmaControls[master]: 01
(Mute)
bmaControls[channel 0]: 00
iFeature: 0x00
===>Audio Control Output Terminal Descriptor<===
bLength: 0x09
bDescriptorType: 0x24 (CS_INTERFACE)
bDescriptorSubtype: 0x03 (OUTPUT_TERMINAL)
bTerminalID: 0x03
wTerminalType: 0x0301 (Speaker)
bAssocTerminal: 0x00
bSourceID: 0x02
iTerminal: 0x00
===>Interface Descriptor<===
bLength: 0x09
bDescriptorType: 0x04
bInterfaceNumber: 0x01
bAlternateSetting: 0x00
bNumEndpoints: 0x00
bInterfaceClass: 0x01 -> Audio Interface Class
bInterfaceSubClass: 0x02 -> Audio Streaming Interface SubClass
bInterfaceProtocol: 0x00
iInterface: 0x00
===>Interface Descriptor<===
bLength: 0x09
bDescriptorType: 0x04
bInterfaceNumber: 0x01
bAlternateSetting: 0x01
bNumEndpoints: 0x01
bInterfaceClass: 0x01 -> Audio Interface Class
bInterfaceSubClass: 0x02 -> Audio Streaming Interface SubClass
bInterfaceProtocol: 0x00
iInterface: 0x00
===>Audio Streaming Class Specific Interface Descriptor<===
bLength: 0x07
bDescriptorType: 0x24 (CS_INTERFACE)
bDescriptorSubtype: 0x01 (AS_GENERAL)
bTerminalLink: 0x01
bDelay: 0x01
wFormatTag: 0x0001 (PCM)
===>Audio Streaming Format Type Descriptor<===
bLength: 0x0B
bDescriptorType: 0x24 (CS_INTERFACE)
bDescriptorSubtype: 0x02 (FORMAT_TYPE)
bFormatType: 0x01 (FORMAT_TYPE_I)
bNrChannels: 0x02
bSubframeSize: 0x02
bBitResolution: 0x10 (16)
bSamFreqType: 0x01 (Discrete)
tSamFreq[1]: 0x00BB80 (48000 Hz)
===>Endpoint Descriptor<===
bLength: 0x09
bDescriptorType: 0x05
bEndpointAddress: 0x01 -> Direction: OUT - EndpointID: 1
bmAttributes: 0x01 -> Isochronous Transfer Type, Synchronization Type = No Synchronization, Usage Type = Data Endpoint
wMaxPacketSize: 0x00C0 = 0xC0 bytes
wInterval: 0x0001
bSyncAddress: 0x00
===>Audio Streaming Class Specific Audio Data Endpoint Descriptor<===
bLength: 0x07
bDescriptorType: 0x25 (CS_ENDPOINT)
bDescriptorSubtype: 0x01 (EP_GENERAL)
bmAttributes: 0x00
bLockDelayUnits: 0x00 (Undefined)
wLockDelay: 0x0000
===>BOS Descriptor<===
bLength: 0x05
bDescriptorType: 0x0F
wTotalLength: 0x000C
bNumDeviceCaps: 0x01
===>USB 2.0 Extension Descriptor<===
bLength: 0x07
bDescriptorType: 0x10
bDevCapabilityType: 0x02
bmAttributes: 0x00000002 -> Supports Link Power Management protocol