1. Introduction
This document describes the USB configuration component and implementation of the Audio 1.0 class application example on the MIMXRT1060-EVK board. It focuses on the MCUXpresso IDE and Config Tools. It provides an example of the Quad Timer peripheral component used as a digital-to-analog converter for processing audio data.
The USB Audio class is supported in the SDK packages that are available for a wide range of MCUs. One of the last i.MX Real-Time (RT) crossover MCUs is the i.MX RT1060. It is available on the MIMXRT1060-EVK evaluation board. The SDK package already contains a demo application that demonstrates the USB Audio speaker functionality. However, a detailed guide on how to develop the application and customize the processing of audio data is not provided.
2. Prerequisites
The USB Audio application is developed in the MCUXpresso IDE 11.2.1 that is available on the http://www.nxp.com/mcuxpresso/ide website. Download and install this or newer version. The Config Tools that is used for the application design is integrated into the MCUXpresso IDE.
Download the MIMXRT1060-EVK SDK support package inside the MCUXpresso IDE (see Install MCUXpresso SDKs in quick access) or separately on https://mcuxpresso.nxp.com/ pages (the SDK package must be built with the USB middleware selected).
3. USB Audio concept
The USB Audio class is supported in the USB middleware of the SDK package. The Peripherals tool from provides a USB middleware component that provides GUI for the configuration of the USB class, interfaces, and generating USB code examples. The USB Audio 1.0 speaker example, enumerated as a playback device, is available as a preset in the USB component. In this demo example, the device receives audio data and stores it in the internal buffer. The playback of audio is not a part of the generated code example, and the implementation is a part of this document.
The USB Audio 1.0 speaker demo application that is described in this document consists of the following parts:
Figure 1. Structure of the USB audio speaker application
The USB component supports the configuration of the Audio control interface and streaming interfaces for the Audio 1.0 class (see the MCUXpresso Config Tools Project chapter for the steps of creating the project with the USB component).
3.1. Audio control Interface
The Audio control subclass provides a configuration of the USB Audio device topology by using terminals and units.
The USB component supports one interrupt endpoint (control endpoint) and the audio control interface settings (the topology). See the example of the Audio speaker configuration below.
Figure 2. Interface setting of the USB component
The USB component supports the following terminals and units of the Audio 1.0 specification:
Input Terminal
Feature Unit
Output Terminal
All these units are used in the Audio speaker example.
Input Terminal
The Input Terminal (IT) is used to interface between the audio function’s ‘outside world’ and other units in the audio function. It serves as a receptacle for audio information flowing into the audio function. Its function is to represent a source of incoming audio data after this data has been properly extracted from the original audio stream into separate logical channels that are embedded in this stream (the decoding process). The logical channels are grouped into an audio channel cluster and leave the Input Terminal through a single Output Pin.
The configuration of the input terminal consists of the ID (must be unique), type, number of channels, and spatial location, see below. The Input Terminal provides:
Figure 3. Input terminal configuration
Feature Unit
The Feature Unit (FU) provides basic manipulation of multiple single-parameter on the incoming logical channels. It supports the following features: Mute, Volume, Tone Control, Graphic Equalizer, Automatic Gain Control, Delay, Bass Boost, and Loudness.
The configuration of the feature unit consists of the unit ID (must be unique), source unit/terminal, and selection of features for every channel.
Figure 4. Feature unit configuration
Output Terminal
The Output Terminal (OT) is used to interface between the units inside the audio function and the ‘outside world’. It serves as an outlet for audio information, flowing out of the audio function. Its function is to represent a sink of outgoing audio data before this data is properly packed from the original separate logical channels into the outgoing audio stream (the encoding process). The audio channel cluster enters the Output Terminal through a single Input Pin.
The configuration of the output terminal consists of the ID (must be unique), type, and source ID (unit/terminal), see below.
Figure 5. Output terminal configuration
3.2. Audio streaming interface
Audio Streaming (AS) interfaces are used to interchange digital audio data streams between the Host and the audio function. Each Audio Streaming interface can have at most one isochronous data endpoint and an optional associated isochronous sync endpoint for synchronization purposes. The isochronous data endpoint is required to be the first endpoint in the AS interface.
The AS interface is also used for the specification of the audio data format. The USB component Audio 1.0 class supports the Format I type of audio data only.
The configuration (in the Audio speaker example) does not provide any audio class-specific interface and endpoints. See the following configuration:
Figure 6. Zero bandwidth interface configuration
Data output alternative interface settings provide a full configuration of audio streaming class-specific settings including the audio data format. There are additional audio class-specific settings for isochronous endpoints.
Figure 7. Data output interface configuration
Figure 8. Isochronous feedback endpoint configuration
The Audio speaker example uses two endpoints:
#1 – isochronous data endpoint (host output) for receiving audio data from the USB host.
#2 – isochronous feedback endpoint that is used for synchronization between the USB audio device and the USB host.
Audio stream interface-specific settings contain the Link to terminal setting which is linked to this audio streaming interface configuration and the audio data format configuration. The USB component supports just the Format I types. There is an additional configuration of the format – number of channels, subframe size, audio data resolution, and supported sampling frequencies.
4. MCUXpresso Config Tools project
This chapter provides a for the implementation of the USB Audio 1.0 demo project the MCUXpresso IDE 11.2.1 and using SDK 2.8.5 for the MIMXRT1060-EVK board. The newer version of the MCUXpresso IDE and SDK package for the MIMXRT1060-EVK shall be also compatible.
4.1. Board support project
Open the MCUXpresso IDE. In the Quickstart Panel, select New project, in the SDK Wizard write RT1060 in the filter line, select the evkmimxrt1060 board, click the Next button.
Figure 9. New EVKMIMXRT1060 project
In the next window, write the project name, for example, MIMXRT1062xxxxA_ and select the USB Device Audio class on the Middleware tab (all the required components of the USB middleware are automatically selected), see below:
Figure 10. USB middleware components of the new project
Click the Finish button (the default libraries, compiler, linker, and memory map settings can be used).
The MIMXRT1062xxxxA_USB_audio_speaker project is created in the IDE. The content is available in the Project Explorer window.
4.2. Peripherals
Open the Peripherals tool by using the Config Tools -> Peripherals.
Figure 11. Selection of the Peripherals tool
In the Peripherals tool, select the MIMXRT1062xxxxA_USB_audio_speaker project and create an instance of the USB 2.6.0 (or newer version) component for the USB1 peripheral in the Peripherals window, see below:
Figure 12. Adding of the USB component
The USB component is added to the Peripherals project (in the default functional group that is named BOARD_InitPeripherals). An empty USB component is created (there is no interface created and the USB component reports errors). The easiest way to create a functional USB configuration is by selecting from a preset. Select the Audio 1.0 – speaker (bare metal) preset.
Figure 13. Selection of the Audio 1.0 speaker demo project configuration
When you select the Audio 1.0 – speaker (bare metal) item, the whole configuration of the example is configured. Two interfaces are created:
#0 – Audio_control
#1 – Audio_streaming
These interfaces contain all settings that are required for the USB Audio 1.0 speaker example. configuration of all audio units is provided in the Audio_control interface. The Device role configuration is also provided for the USB Audio demo example. See details of the provided settings in the previous chapter USB Audio concept.
There is still the following error in the Problem view:
Issue: USB Function Clock (USBPHY1_CLK) is inactive and USB module will not work.
Level: Error
Type: Validation
Tool: Clocks
Origin: Peripherals:BOARD_InitPeripherals
Target: Clocks: BOARD_BootClockRUN
Resource: USBPHY1_CLK
Figure 14. USB clock source error
Right-click the error and select Show problem in BOARD_BootClockRun.
Figure 15. Resolving of the USB clock source error
The Clocks tool is opened, and you are navigated to the problem in the Details view. The USBPHY1 PLL clock is disabled by default (to save power). You can enable it by selecting the enabled value in the USBPHY1 clock output setting:
Figure 16. Enabling of the USB clock source in the Clocks tool
This setting enables the 480 MHz reference clock for USBPHY1 and the USB peripheral runs.
Go back to the Peripherals tool. You can see that the error was resolved, and the code is generated.
If you click the Update Code button, you can see what code is provided by the Config Tools. There are the following functions.
Pins initialization functions:
pin_mux.c/h – initialization of the routed pins for the peripherals (USB1, TMR3 in our use case).
Clocks initialization functions:
clock_config.c/h – initialization function for a configuration of the clock source, selectors, dividers, PLLs, and other system clock settings of the MCU. A configuration of the USB1 clocks for PHY that was enabled is also provided.
Peripherals initialization functions and the USB code example:
peripherals.c/h initialization functions for configured peripherals (USB1, TMR3 components).
usb_device_composite.c/h contains initialized structures, initialization functions, device callback function, interrupt routine, and other USB middleware-related functions.
usb_device_config.h contains USB middleware configurations (definitions) that are directly included in the USB middleware.
usb_device_descriptor.c/hcontains definitions of the device descriptor, device configuration descriptor, initialized interface and endpoints configuration structures, function for device speed selection, and other USB device-related functions.
usb_device_interface_0_audio_control.c/h contains the generated code example of the audio speaker. There is an application code for processing USB device requests, processing audio data, and configuration structure of the audio speaker example (buffer, runtime settings, …).
When any of these generated files are customized (modified in the project), uncheck the file in the Update Code dialog. Otherwise, the changes would be lost.
Figure 17. Update of the generated code in MCUXpresso IDE project
The USB Audio example requires the Pins, Clocks, and Peripherals tool. Therefore, the generated code is used to replace the original code that is available in the default board support project. You can click the Cancel button and continue in the project configuration.
4.3. DAC converter
The i.MX RT1060 does not contain any DAC (Digital-to-Analog Converter). The MIMXRT1060 contains an external device that can be used to provide audio playback functionality. This use case is already covered in the SDK Audio speaker example.
You can create a simple DAC converter by using a timer of the MCU. Select. The board provides Arduino headers that can be used for this demo purpose. The J23 header provides an output of the TMR3 – channels 0 and 1 (J23[6] and J23[5] pins). You can find this information in the Pins tool.
Open the Pins tool and navigate to the Pins window. Select the BOARD_InitPins functional group (it is selected by default, and it is also included in the default initialization).To order the table lines according to the TMR peripherals, click the TMR column header. You can see that QTIMER3 (TMR3) timers #0 and #1 can be routed on Arduino header J23. When you click QTIMER3_TIMER0 and QTIMER3_TIMER1 in the TMR column, these timer outputs are routed automatically to the J23[6] and J23[5] pins on the Arduino header on the board.
Figure 18. Routing of the timer pins in the Pins tool
Switch to the Peripherals tool and add an instance of the Quad Timer (qtmr) component to the TMR3 peripheral. Check the checkbox of the TMR3 – the Quad Timer component is the only supported component and it is added automatically.
The quad timer peripheral (TMR) supports four channels. You need two channels for each audio channel – Left and Right and the third channel for frequency.
Figure 19. Quad timer component selection
When the Quad Timer component is added, you can see the clock source – Bus clock 150 MHz. The USB Audio streaming contains 48 kHz. It means that the PWM must work at the multiplication of this sampling frequency. It simplifies the implementation of the DAC design.
150000000 / 48000 = 3125There are 3125 ticks of the timer per sample of audio data. To provide a better DAC output, the PWM must work on a frequency higher than 48 MHz. For example, the frequency can be 48 kHz * 5 = 240 kHz. It means that the PWM output frequency is 240 kHz and each audio sample value is set for 5 periods of the timer. However, it also means that the resolution of the timer is reduced to 3125 / 5 = 625 ticks. Finally, you have a 9-bits DAC converter that can be used for playback of audio data.
The PWM output signal can be processed through a low-pass filter (R1, C1) with an output filter capacitor (C2) to remove some of the DC from the signal.
Figure 20. Low-pass output filter
fc=1/(2πRC)
For example, use the following low pass filter:
R1= 4700 Ω, C1 = 1.5 nF
f c = 22575 Hz
TMR3 component configuration
To provide two PWM channels, use the following configuration. First channel:
Chanel ID: Left Primary timer/counter reference: Bus clock divided by 1 Counting operation: Rising edge of the primary source Timer mode initialization: PWM output PWM frequency/period: 240 kHz DMA/Interrupt mode: Polling Interrupt request: enable Compare 2
Second channel:
Chanel ID: Right Primary timer/counter reference: Bus clock divided by 1 Counting operation: Rising edge of the primary source Timer mode initialization: PWM output PWM frequency/period: 240 kHz DMA/Interrupt mode: Polling
Third channel for invoking the interrupt routine at sample frequency:
Chanel ID: SampleFreq Primary timer/counter reference: Bus clock divided by 1 Counting operation: Rising edge of the primary source Timer mode initialization: Timer PWM frequency/period: 48 kHz DMA/Interrupt mode: Interrupt Interrupt request: enable Compare 1
Interrupt vector
Enable interrupt: enabled (checked) Enable custom handle name: enabled (checked) Interrupt handler name: DAC_TMR_IRQ
Note: The interrupt of the SampleFreq channel is only used to load the compare load registers of both channels at the sampling frequency. The Digital-to-Analog conversion of both channels is synchronized and one simple interrupt routine can be used.
The Quad Timer components provide initialization but you must provide runtime functionality. Therefore, you must know the details of the PWM mode functionality (specified in the reference manual of the MCU). The PWM mode with variable frequency is driven by both compare registers. The second compare register provides a high-level period of the PWM period (duty), and the first compare register specifies the inactive part of the PWM period. The sum of these two compare registers must always be the computed resolution of the timer (625). An example of the initialization of compare registers can be found in the fsl_qtmr SDK driver, in the QTMR_SetupPwm() function.Compare registers are initialized before the counter/timer is started. The next values can be stored by using compare load registers. Thus, the loading of the compare registers is synchronized automatically by the timer itself (it is a part of the initialization setting in the fsl_qtmr driver).
Therefore, the interrupt routine can be used for loading the audio data into compare load registers.
The audio data is sent in the following PCM 16-bit format:
one sample (4 bytes)
0
1
2
3
Left channel (low byte)
Left channel (high byte)
Right channel (low byte)
Right channel (high byte)
int16_t value (little endian format)
int16_t value (little endian format)
Finally, configure the TMR3 peripheral in the following way:
Figure 21. Quad timer configuration for the DAC
In the Problems view, an error is reported. The error is caused by the missing fsl_qtmr SDK driver in the project (it was not added in the project wizard of the MCUXpresso IDE).
Figure 22. Quad timer SDK driver error
To fix the error, use the context menu of the error in the Problems view and select the Add SDK component “QTMR Driver” into the project “MIMXRT1062xxxA_USB_audio_speaker.
Figure 23. Adding of the Quad timer SDK driver in the MCUXPresso project.
When the option is selected, the update of the files is processed and the error disappears. The initialization of the application is done, and the generated code can be used in the project.
Select the Update Code command and select update of the following files:
Figure 24. Update of the generated code in MCUXpresso project
All generated initialization files of Pins, Clocks, and Peripherals are stored to the project and the Develop perspective of the MCUXpresso IDE is automatically selected. You can check that all generated files are available in the project now.
Note: When any of these generated files are later customized (modified in the project), the file must be unchecked in the Update Code dialog. Otherwise, the changes would be lost.
4.4. Audio application design
The USB Audio code example has been generated into the project and it works. But the playback of the audio data is not provided. Only the USB_Interface0AudioControlProcessNextAudioData() function is available in the source/usb_device_interface_0_audio_control.c source file. The received audio data is available in the internal USB_Interface0AudioControlDataBuff buffer and the index () of the newly received data is s_UsbDeviceAudioSpeaker->tdWriteNumberPlay. It is used to send data into the DAC converter.The DAC converter has not been implemented yet. There are just two PWM channels of the TMR3 that generate a zero-duty PWM output signal. Use the interrupt subroutine for the implementation of the audio playback. Copy the template of the interrupt routine from the TMR3 component in the Peripherals tool, see the Interrupt settings:
Figure 25. Template of the interrupt service routine.
When you click the Copy to clipboard button of the Handler template setting, you obtain the following code in the clipboard.
/* TMR3_IRQn interrupt handler */
void DAC_TMR_IRQ(void) {
/* Place your code here */
/* Add for ARM errata 838869, affects Cortex-M4, Cortex-M4F
Store immediate overlapping exception return operation might vector to incorrect interrupt. */
#if defined __CORTEX_M && (__CORTEX_M == 4U)
__DSB();
#endif
}
Place this code into the main program module source/MIMXRT1062xxxxA_USB_audio_speaker.c. It is used for the implementation of the main part of the application.
The USB component contains a task call that must be placed into a loop of the main process. In this simple use case, use directly the main() function:
Figure 26. USB device task function call code.
/*
* @brief Application entry point.
*/
int main(void) {
/* Init board hardware. */
BOARD_InitBootPins();
BOARD_InitBootClocks();
BOARD_InitBootPeripherals();
#ifndef BOARD_INIT_DEBUG_CONSOLE_PERIPHERAL
/* Init FSL debug console. */
BOARD_InitDebugConsole();
#endif
/* Enter an infinite loop */
while(1) {
USB_DeviceTasks();
}
return 0 ;
}
4.4.1. Audio data processing
The audio data can be read directly from the existing buffer. Therefore, you can update the implementation of the USB_Interface0AudioControlProcessNextAudioData() function and use parameters to pass a pointer to data and data size. See the following update of the function:
#define SAMPLE_SIZE (AUDIO_FORMAT_CHANNELS * AUDIO_FORMAT_SIZE)
void USB_Interface0AudioControlProcessNextAudioData(uint8_t **audioData, uint8_t *dataSize) {
if ((s_UsbDeviceAudioSpeaker->audioSendTimes >= s_UsbDeviceAudioSpeaker->usbRecvTimes) &&
(s_UsbDeviceAudioSpeaker->startPlayHalfFull == 1))
{
s_UsbDeviceAudioSpeaker->startPlayHalfFull = 0;
s_UsbDeviceAudioSpeaker->speakerDetachOrNoInput = 1;
}
if (s_UsbDeviceAudioSpeaker->startPlayHalfFull)
{
/*
* size of data packet that can be sent = FS_ISO_OUT_ENDP_PACKET_SIZE
* pointer to data = USB_Interface0AudioControlDataBuff + s_UsbDeviceAudioSpeaker->tdWriteNumberPlay
*/
*audioData = &(USB_Interface0AudioControlDataBuff[s_UsbDeviceAudioSpeaker->tdWriteNumberPlay]);
*dataSize = FS_ISO_OUT_ENDP_PACKET_SIZE / SAMPLE_SIZE;
s_UsbDeviceAudioSpeaker->audioSendCount += FS_ISO_OUT_ENDP_PACKET_SIZE;
s_UsbDeviceAudioSpeaker->audioSendTimes++;
s_UsbDeviceAudioSpeaker->tdWriteNumberPlay += FS_ISO_OUT_ENDP_PACKET_SIZE;
if (s_UsbDeviceAudioSpeaker->tdWriteNumberPlay >=
AUDIO_SPEAKER_DATA_WHOLE_BUFFER_LENGTH * FS_ISO_OUT_ENDP_PACKET_SIZE)
{
s_UsbDeviceAudioSpeaker->tdWriteNumberPlay = 0;
}
}
else
{
/*
* size of data packet that can be sent = FS_ISO_OUT_ENDP_PACKET_SIZE
* pointer to data = USB_Interface0AudioControlDataBuff
*/
*audioData = USB_Interface0AudioControlDataBuff;
*dataSize = FS_ISO_OUT_ENDP_PACKET_SIZE / SAMPLE_SIZE;
}
}
The audioData pointer reference is set to point into the audio data buffer (USB_Interface0AudioControlDataBuff) that contains the audio data received from the host. The packet size is fixed. s_UsbDeviceAudioSpeaker->tdWriteNumberPlay is the index of the next data that must be converted by using DAC.
Update the declaration of the function in the header file accordingly:
/*
* Function for procession of next audio data from the data buffer. It can be used as a callback.
*/
void USB_Interface0AudioControlProcessNextAudioData(uint8_t **audioData, uint8_t *dataSize);
4.4.2. DAC interrupt routine
The interrupt routine of the TMR3 peripheral provides the functionality of the DAC converter. It processes a stream of audio data packets and uses the sample values to control the duty of the output PWM signal (the first PWM channel as the left channel and the second PWM channel as the right channel). The sampling frequency is driven by the third channel of the TMR3 (the SampleFreq channel). See the implementation of the interrupt routine and related definitions:
#include "usb_device_interface_0_audio_control.h"
/* Size of one received packet */
#define PCM_PACKET_SIZE AUDIO_SAMPLING_RATE_KHZ
/* PCM data pointer and sample index counter */
uint8_t *pcmData;
uint8_t sampleIndex = PCM_PACKET_SIZE;
/* Number of ticks for each PWM period */
#define PWM_PERIOD_LENGTH_IN_TICKS 625
/* size of one sample in streaming data (PCM format) */
#define SAMPLE_SIZE (USB_INTERFACE_1_AUDIO_STREAMING_SETTING_1_AUDIO_STREAM_SETTING_1_NUMBER_OF_CHANNEL * USB_INTERFACE_1_AUDIO_STREAMING_SETTING_1_AUDIO_STREAM_SETTING_1_SUBFRAME_SIZE)
/* TMR3_IRQn interrupt handler */
void DAC_TMR_IRQ(void) {
static uint16_t dutyValue;
/* Add for ARM errata 838869, affects Cortex-M4, Cortex-M4F
Store immediate overlapping exception return operation might vector to incorrect interrupt. */
#if defined __CORTEX_M && (__CORTEX_M == 4U)
__DSB();
#endif
/* Clear the Compare Flag */
TMR3_PERIPHERAL->CHANNEL[TMR3_SAMPLEFREQ_CHANNEL].CSCTRL &= ~TMR_CSCTRL_TCF1_MASK;
/* audio data processing */
/* load next audio data when the whole packet has been sent */
if (sampleIndex >= PCM_PACKET_SIZE) {
// load next data
uint8_t dataLength;
USB_Interface0AudioControlProcessNextAudioData(&pcmData, &dataLength);
/* if new packet has been received */
if (dataLength == PCM_PACKET_SIZE) {
sampleIndex = 0;
}
}
/* if the buffer (packet) contain any data to be converted */
if (sampleIndex < PCM_PACKET_SIZE) {
// PCM left channel data
dutyValue = ((((int16_t*)pcmData)[SAMPLE_SIZE/2 * sampleIndex]) / 128) + 256;
TMR3_PERIPHERAL->CHANNEL[TMR3_LEFT_CHANNEL].CMPLD2 = dutyValue;
TMR3_PERIPHERAL->CHANNEL[TMR3_LEFT_CHANNEL].CMPLD1 = PWM_PERIOD_LENGTH_IN_TICKS - dutyValue;
// PCM right channel data
dutyValue = ((((int16_t*)pcmData)[(SAMPLE_SIZE/2 * sampleIndex) + 1]) / 128) + 256;
TMR3_PERIPHERAL->CHANNEL[TMR3_RIGHT_CHANNEL].CMPLD2 = dutyValue;
TMR3_PERIPHERAL->CHANNEL[TMR3_RIGHT_CHANNEL].CMPLD1 = PWM_PERIOD_LENGTH_IN_TICKS - dutyValue;
/* next sample */
sampleIndex++;
}
}
The audio data is loaded (the pointer to audio data buffer is used) using the USB_Interface0AudioControlProcessNextAudioData() function. The whole packet of audio data is always provided (the PCM_PACKET_SIZE definition).
When any audio data is available, the sample data is used to control the PWM duty of the left and right channels. The audio data is in the int16_t format, but the compare load register needs a 9-bit unsigned integer value. Therefore, these sample values are converted by the expression (see the source code above).
5. Conclusion
This tutorial provides an example of how you can use the USB component to generate a USB Audio 1.0 demo example and how you can use the generated interface to process the stream of audio data. The provided DAC example uses the interrupts and is not optimized. In a real application, use the DMA to process the audio stream data to reduce CPU usage. The volume control, mute, and suspending of the timers (when muted or zero bandwidth interface is selected) are other improvements that should be implemented in an advanced application.
6. References
USB Audio specification on https://usb.org:
Universal Serial Bus Device Class Definition for Audio Devices
Universal Serial Bus Device Class Definition for Audio Data Formats
Universal Serial Bus Device Class Definition for Terminal Types
NXP application note and i.MX RT1060 documentation :
How to Use Freescale USB Stack to Implement Audio Class Device
i.MX RT1060 Processor Reference Manual
MIMXRT1060/1064 Evaluation Kit Board Hardware User's Guide
View full article