MPLAB® Harmony supports usage of Inter-IC Sound (I²S) by enabling the Audio Protocol mode in the Serial Peripheral Interface (SPI) peripheral. Harmony provides support for I²S through a dedicated driver. The I²S driver can operate in either Interrupt mode or Direct Memory Access (DMA) mode.
The selection of which mode to operate in is made when setting up the I²S device in the MPLAB Harmony Configurator (MHC).
Interrupt Mode vs. DMA Mode
In the Interrupt mode, the data request submitted by the application is handled by writing/reading directly to/from the I²S peripheral register. When the data request is completed, the I²S interrupt occurs. The driver’s state machine is called (through the function DRV_I2S_Tasks) from the Interrupt Service Routine (ISR) of the I²S interrupt. The DRV_I2S_Tasks function calls back the application-specific routine to handle the data transfer completion events.
In the DMA mode, the data request by the application is handled by submitting write/read requests through dedicated DMA channels. The DMA takes care of moving the data between the application data buffers and the I²S peripheral register. The completion of the data transfer request is indicated by the occurrence of the DMA channel interrupt. The DMA system service calls the I²S driver’s state machine (DRV_I2S_Tasks), which subsequently calls back the application-specific routine to handle the data transfer completion events.
DMA Mode Implementation
The DMA mode implementation is the preferred method for an I²S data transfer for audio applications as it has a minimum CPU intervention allowing a block of data transfer between the I²S peripheral and the application data memory. Henceforth, this tutorial focuses on the DMA mode implementation of the I²S driver.
The DMA mode is enabled by selecting the DMA Channel Instance Configuration in the MHC. The DMA channel instance needs to be appropriately mapped to a physical DMA channel under the MHC configuration for DMA System Service.
The following source files are added to the application project when it is generated with the DMA mode option in the MHC.
Audio Protocol Modes
The I²S driver supports four audio protocol modes and can be operated in any one of these modes:
- I²S mode
- Left-justified mode
- Right-justified mode
- Pulse Code Modulation (PCM)/ Digital Signal Processor (DSP) mode
These modes enable communication to different types of audio devices and control the edge relationships of the Left and Right Clock (LRCK) and Serial Data Input (SDI)/Serial Data Output (SDO), with respect to the Bit Clock (BCLK).
For details on the supported Audio Protocol modes, refer to the "PIC32 Family Reference Manual", "Section 23. Serial Peripheral Interface (SPI)" section 23.4.5.
The application enables the driver to enable one of the above modes by selecting the appropriate MHC configuration.
Client Usage
Your application, or other device drivers which use the I²S driver, is a client of the I²S driver. The Client Usage Model of an I²S driver is the same as that of generic MPLAB Harmony Driver-Client Usage Model. It has an open and close function.
For a client to be able to use the driver, it must call the open function DRV_I2S_Open to obtain a driver handle. The handle is then passed into all other client-level interface functions as a parameter (for example, in the DRV_I2S_BufferAddWrite function to submit a write request).
Single Client: In a USB audio speaker application, the I²S lines are connected to the audio Digital-to-Analog Converter (DAC) device on the board for the playback capability. The audio DAC driver would be a single client to the I²S driver.
The application uses the MHC configuration option Number of I2S Driver Clients to mention the number of clients desired from the I²S driver.
The application opens communication with the audio DAC device by calling the function DRV_I2S_Open. The handle returned by this function is a required parameter for all subsequent client level function calls to the I²S driver.
Multiple Clients: An important benefit, most useful to application developers, is the ability of the I²S driver to share the I²S peripheral among several tasks. This is useful when several different devices are attached to a single I²S channel and each of the applications tasks communicates with a different Slave device.
In a USB audio headset application, the I²S lines are connected to the audio Coder-Decoder (CODEC) device on the board for the playback and microphone capability.
The connection would be to an audio CODEC device with functionality for microphone and playback. Two distinct functional tasks need to use the I²S peripheral for communication. In this application, the audio CODEC driver would have multiple (two) clients to the I²S driver.
The application uses the MHC configuration option Number of I2S Driver Clients to specify the number of clients desired from the I²S driver.
The application opens communication with the audio CODEC device by calling the function DRV_I2S_Open two times with the same module index (indicating the single I²S peripheral). The handle returned by this function is a required parameter for all subsequent client level function calls to the I²S driver.
Multiple Instances
The I²S driver supports only dynamic implementation. The dynamic implementation allows one instance of the driver’s object code to manage multiple instances of physical peripherals. This is in contrast to a static implementation wherein separate copies of code need to be managed to access multiple physical peripheral instances.
The MHC configuration option Driver Implementation is grayed out to select Dynamic Implementation by default.
The dynamic driver is an optimal implementation for an I²S device as it seamlessly manages multiple physical instances of the peripheral. It is also a conducive implementation to be used for audio applications.
Use-Case Scenario: Developing Bluetooth® audio application on a Curiosity PIC32 MX470 Development Board (DM320103).
The application functionality is for the Bluetooth module to receive an audio data stream from the source (typically a Bluetooth-enabled smartphone) and sends it over the I²S interface to the PIC32 devices. The PIC32 device forwards the audio data to the CODEC for playback on the speaker.
The Curiosity PIC32 MX470 Development Board features:
- X32 header for audio I/O using Microchip’s audio daughter boards. The compatible daughterboard supports the PIC32 Audio CODEC Daughter Card consisting of CODEC AK4642EN.
- BM64 Bluetooth v4.2 stereo audio module footprint for Bluetooth connectivity.
The data interface to the CODEC board and the Bluetooth module is I²S. The PIC32 has separate I²S lines interfaced to each of these boards. The CODEC board is interfaced to the I²S peripheral with ID2, while the Bluetooth module is interfaced to the I²S peripheral with ID1.
In the PIC32 CODEC interface, the PIC32 acts as the I²S Master and the CODEC acts as the I²S Slave.
In the PIC32 Bluetooth interface, the Bluetooth module acts as the I²S Master and the PIC32 acts as the I²S Slave.
The Harmony I²S dynamic driver offers an efficient and effective way to manage the two instances of I²S peripherals to control the Bluetooth audio application. The application uses the MHC configuration option Number of I2S Driver Instances to specify the number of instances of the I²S peripheral the application desires to use.
The application uses the MHC configuration options to assign the I²S module ID to the respective driver instance (I²S module ID1 for communicating with the Bluetooth module, and I²S Module ID2 for communicating with CODEC device).
Note the role of the PIC32 as the Master for communication with the CODEC device, and as the Slave for communication with the Bluetooth device. While in Master mode, the PIC32 SPI module has the ability to generate its own clock internally via the Master Clock (MCLK) from various internal sources such as the primary clock, Peripheral Bus Clock (PBCLK), USB clock, FRC and other internal sources. In addition, the SPI module has the ability to provide the MCLK to the CODEC device. In Slave mode, the BCLK and LRCK are received from the Master (Bluetooth Device).
The I²S driver instance 0 (DRV_I2S_INDEX_0) is used by the CODEC driver to access and manage the CODEC device. The CODEC driver opens the I²S driver by calling the function DRV_I2S_Open as below.
The I²S driver instance 1 (DRV_I2S_INDEX_1) is used by the application to access and manage the Bluetooth device. The application opens the I²S driver by calling the function DRV_I2S_Open as below.
In the above snapshots, notice the first parameter in the function call, DRV_I2S_Open. The first parameter indicates the instance of the I²S driver being opened.
Data Buffering
The I²S driver implements an Asynchronous Buffer Queuing Data Transfer Model to meet the requirement of audio applications which expect continuous, uninterrupted data transfer capabilities. The following Application Programming Interfaces (APIs) are provided to achieve asynchronous data transfer:
DRV_I2S_BufferAddWrite
DRV_I2S_BufferAddRead
DRV_I2S_BufferAddWriteRead
The APIs are non-blocking, and allow the client to call the BufferAdd function multiple times without waiting for each transfer to complete. This allows the caller to queue up more than one buffer at a time, potentially before the first buffer has finished (depending on buffer size and data transfer speed).
Illustrating I²S Driver data buffer queuing with three write requests.
Note that the buffers, buffer1, buffer2, and buffer3 need to be defined as global variables.
In the above example, three buffers, buffer1, buffer2, and buffer3, are queued to write I²S data. When the first call to DRV_I2S_BufferAddWrite occurs, the driver will place the address and size of buffer1, as buffer object BO1 into its queue. Then it will store a unique handle, identifying the data transfer request, into the bufferHandle1 variable and begin transferring data from buffers to I²S peripheral. Then the call will return. When the second, and subsequently the third cal,l to DRV_I2S_BufferAddWrite occurs, the process repeats. If the driver has not yet finished transferring buffer1/buffer2, it will add the address and size of buffer2/buffer3 as BO2/BO3 objects into its queue, provide a handle to it, and return.
I²S driver provides a callback notification function to indicate to the application when the data transfer request has completed. The callback function is registered with the driver by calling DRV_I2S_BufferEventHandlerSet.
The application registers the callback function I2SBufferEventHandler with the I²S driver before calling DRV_I2S_BufferAddWrite. When the driver has completely transferred all data from buffer1, it will call the I2SBufferEventHandler function, as shown in the following example.
The I²S driver passes the DRV_I2S_BUFFER_EVENT_COMPLETE ID in the event parameter to indicate that the data from buffer1 has been completely transmitted by the I²S. The value of the bufferHandle parameter will match the value assigned to the bufferhandle1 parameter of the DRV_I2S_BufferAddWrite function call so the application can verify which buffer transfer has completed.
This callback mechanism allows the application to synchronize to the timing of when the data transfers have completed, and schedule further data transfers (if any), from the callback functions. The I²S driver uses I²S/DMA interrupts as triggers for callback functions. The callback function executes in an interrupt context.
In an audio application, to maintain the time synchronization of audio data, the application calls the BufferAdd functions DRV_I2S_BufferAddWrite, DRV_I2S_BufferAddRead, or DRV_I2S_BufferAddWriteRead for rescheduling a transfer request from a buffer event handler.
Note the following, with respect to calling the BufferAdd functions from the event handler:
1
A BufferAdd function can be called from the event handler, registered by the client calling DRV_I2S_BufferEventHandlerSet.
2
A BufferAdd function should not be called from the event handler registered by a client that belonged to a different driver instance (attached to a different peripheral device). The buffer object queue is dedicated to a specific driver instance, calling BufferAdd functions from the event handler of a different driver instance has the potential to corrupt the driver instance-specific data structure.
For example, there are two I²S peripherals. I²S peripheral-1 tied to I²S driver instance-0, and I²S peripheral-2 tied to I²S driver instance-1. Each of them has a dedicated buffer object. The application opens two clients. Client-1 is opened for I²S driver instance-0 and Client-2 is opened for I²S driver instance-1. Both the clients register their dedicated event handlers. Handler-1 for Client-1 and Handler-2 for Client-2. With this setup note the following:
- BufferAdd function with Client-1 as the handle can be called from Handler-1.
- BufferAdd function with Client-2 as the handle can be called from Handler-2.
Advanced Data Buffering
Audio system applications pose the typical producer-consumer problems associated with a real-time system. The timing constraints include latency, sampling rate, sampling period, and real-time response.
The I²S driver provides data buffering support, particularly the DMA Implementation mode explained above, for applications to implement a buffering mechanism to accommodate and address the timing challenges.
The following are the two possible methods an application uses for implementing a solution to audio data timing challenges:
Ping-pong Buffer Method: The ping-pong method uses two buffers. One buffer reads data from the source, while the other buffer is submitting the data to the destination. When the submission to the destination is complete, the roles of the two are switched. The ping-pong is accomplished by modifying the pointers to the buffers.
Buffer Queuing Method: In the buffer queuing method, the application maintains a pool of memory buffers, wherein it stores the received audio data to certain levels. Once the levels are reached, it starts submitting the read data to the destination in a First In, First Out (FIFO) preference. Henceforth, the reading from the source and writing to the destination happens simultaneously through a circular queue implementation of the data buffers.
The above two methods generally work well in typical audio applications, but there could be some advanced, feature-rich applications wherein the producer-consumer problem is amplified by the fact that the application needs to perform more tasks, which could lead to issues with the above methods.
For example, a USB audio 1.0 speaker implementation with 48 kHz sampling rate, 16-bit, 2-channel data works well with the above buffering methods, while a high-resolution USB audio 2.0 speaker implementation with 192 kHz sampling rate, 24-bit, 2-channel data would show audio quality issues with the above methods. The issues would be due to a number of factors including the amount of data that is being read from the USB and then processed and fed to the audio CODEC. All of these factors, along with different clock domains between the producer and consumer (USB domain and CODEC domain in these examples), contributes to the audio quality issues.
The below calculation shows the bytes per frame (or micro-frame) for the above USB audio configurations:
USB Audio 1.0 Speaker: A frame is transferred every 1 ms.
Sampling Rate: 48 kHz
Bit Depth: 16-bits = 2 bytes
Number of Channels: 2
(48000 samples/second) x (1/1000) ms = 48 samples/ms
Each sample is of 2 x 2 = 4 bytes
Bytes per frame (i.e. per millisecond) = 48 x 4 = 192
USB Audio 2.0 High-Resolution Audio Speaker: A micro frame is transferred every 125 microseconds:
Sampling Rate: 192 kHz
Bit Depth: 24-bits = 3 bytes
Number of Channels: 2
(192000 samples/second) x (1/1000) ms = 192 samples/ms
Each sample is of 3 x 2 = 6 bytes
Bytes per millisecond = 192 x 6 = 1152
Bytes per micro frame (packet) = 1152/8 = 144
In the above examples, notice that the number of bytes being read every millisecond differs. The number of bytes that are read and processed in the USB audio 2.0 speaker application are substantially larger than the USB audio 1.0 speaker application.
The application implementing the above buffering methods has an additional challenge of real-time data submission to the I²S driver. As explained, the I²S driver, in DMA implementation mode, submits subsequent BufferAdd requests from the event handler triggered by the DMA complete event. The DMA complete event occurs when the last word from the data buffer is put into the I²S peripheral register. If a buffer is already available in the queue, the I²S driver task (DRV_I2S_Tasks) picks the same from the queue and schedules the next request immediately. If there is no buffer available in the driver queue, it is the responsibility of the application to submit the next BufferAdd request within the available maximum time before the last word put in the peripheral register is transferred.
For the USB audio 1.0 Speaker example, this time would be 5.2 microseconds, calculated as below:
Sampling Rate: 48 kHz
Bit Depth: 16-bits
MCLK = 256 x Sampling Rate = 12288000 Hz
BCLK = MCLK/4 = 3072000 Hz
1-bit time = 1/BCLK = 3.25e-7
16-bits time = 16 x 1-bit time = 5.2 microseconds
For the USB audio 2.0 Speaker example this time would be 2.60 microseconds, calculated as below:
Sampling Rate: 192 kHz
Bit Depth: 24-bits
MCLK = 128 x Sampling Rate = 24576000 Hz
BCLK = MCLK/2 = 12288000 Hz
1-bit time = 1/BCLK = 8.13e-8
32-bits time = 32 x 1-bit time = 2.60 microseconds
(Note: 24-bits size is rounded to 32-bits as the size of the peripheral register would be either 1, 2, or 4 bytes wide)
Note that the time available for the application before which it needs to submit a new BufferAdd request is less for the USB audio 2.0 speaker application. This time constraint problem can be addressed by using the channel chaining feature of the DMA module in the implementation of I²S driver.
DMA Channel Chaining: channel chaining is an enhancement to the DMA channel operation. A channel (Slave channel) can be chained to an adjacent channel (Master channel). The Slave channel is enabled when a block transfer of the Master channel completes.
The channel chaining feature of the DMA module is used by I²S driver to implement cyclic ping-pong channels.
Initially, both the channels are initialized with the data to be transferred. Once enabled, the data transmission starts from Channel-1. On completion of the data transfer from Channel-1, the Channel-2 transmission starts automatically. At the same time, the driver triggers a buffer complete event on Channel-1. The application submits a BufferAdd request on Channel-1 while the transmission is happening over Channel-2. On completion of the data transfer from Channel-2, the Channel-1 transmission starts automatically. At the same time, the driver triggers a buffer complete event on Channel-2. The application submits a BufferAdd request on Channel-2 while the transmission is happening over Channel-1. This process repeats, allowing the application ample amount of time, (the time required to transfer Data bit size number of bytes submitted in BufferAdd requests), to prepare and submit subsequent BufferAdd requests.
The application enables the DMA channel chaining implementation in the MHC.
The following source file is added to the application project when it is generated with DMA Mode and Use DMA Channel Chaining? options selected in the MHC.
Data Buffer Queue
The I²S driver maintains a queue of buffer objects. A buffer object essentially contains three elements:
uint8_t *txbuffer; : pointer to write buffer.
uint8_t *rxbuffer; : pointer to read buffer.
size_t size;: size of the buffer.
The size of the read data queue object and write data queue object is configured through the MHC.
The combined size of the read and write buffer object is generated by the MHC and is available in system_config.h as a macro.
Water Mark Levels
In advanced audio applications, the producer-consumer problem is augmented by the fact that the producer and consumer are running at different clock rates. In such situations, the application needs to interact with the producer/consumer and instruct the other side to either speed up or slow down. The application provides feedback to the peer by maintaining and managing the levels of its internal data objects.
For example, in the implementation of a USB Audio Device Asynchronous Endpoint Implementation, the application maintains a count or watermark levels of the data buffer objects available to be sent to the CODEC device. The application achieves synchronization by sending a feedback message to the USB host through the feedback endpoint.
The I²S driver provides assistance to the application in maintaining its watermark levels by providing an API DRV_I2S_BufferCombinedQueueSizeGet, which returns the number of data buffer objects currently available in the buffer object queue.
Queue Flush
In audio applications, there is often a requirement to stop the current operation and switch to other functionality. For example, on a Bluetooth speaker and headset device, the audio playback needs to be stopped to attend and receive a phone call. To achieve this, the application stops the ongoing audio data streaming and clears off the data buffer queues. The I²S driver provides an API DRV_I2S_BufferQueueFlush, which removes the entire data buffer objects associated with the calling client.
CODEC Clock and Data Rate Tuning
Typically, CODEC devices need a fine clock source to generate accurate audio sampling rates. The clock source (MCLK) can also be generated internally by the CODEC device. PIC32 devices provide a flexible Reference Clock Output (REFCLKO). The REFCLKO module is used to generate the fractional clock that will be used by the audio CODEC device to accommodate various sample rates.
An MCLK-multiplier to the sampling frequency (fs) is provided which produces an MCLK to the CODEC. This MCLK-multiplier value should be one of the values supported by the CODEC for various sampling rates.
For example, in 256 fs, the MCLK-multiplier value is 256 and ‘fs’ is the sampling frequency.
For sampling rate 48 kHz, and MCLK-multiplier value of 256, the MCLK = 256 x 48000 = 12288000 Hz.
I²S driver configuration options MCLK Sampling rate Multiplier in the MHC allows setting up the MCLK-multiplier value.
The BCLK provided to the CODEC by PIC32 is derived from the MCLK. The common BCLKs that can be generated for the given combination of BCLK-multiplier and sampling rate (e.g., 32 fs and 64 fs) would be MCLK/1, MCLK/2, MCLK /4 or MCLK /8.
For example, for BCLK value 32 fs or 64 fs, 48 kHz sampling rate:
The BCLK would be 64 x 48000 = 3072000 Hz.
Therefore the MCLK/BCLK ratio is 12288000/3072000 = 4.
I²S driver configuration options MCLK/BCLK in the MHC allows setting up the MCLK/BCLK ratio.
The sampling rate, MCLK-multiplier, and MCLK/BCLK ratio provide inputs to the clock configurator in the MHC to generate the desired clocks.
An advanced method of addressing the producer-consumer problem is to adjust the output data rate based on the input data rate. Typically, the application maintains an average number of samples received over a defined period. It compares the average number of samples with the upper and lower level values for an average number of samples. When the average number of samples received goes out of the acceptable range, the application tunes the output clock to the CODEC by slightly increasing or decreasing the output data rate of the buffered data. The tuning of the CODEC clock prevents buffer underrun and overrun conditions, thereby reducing the audible artifacts in the audio stream.
The PIC32 device provides a REFCLKO that is used to generate and tune the required output clock to an audio CODEC device. The REFCLKO has the ability to be tuned on-the-fly and can be tuned in steps between a specified range. The range should be such that it ensures a swing of the sample rate, typically about ±0.2%, which is well in a range that might introduce audible artifacts. For example, a swing of ±0.2% of the data stream with a sample rate of 48 kHz requires tuning of the REFCLKO between the ranges of 12, 263, 424 Hz, and 12,312,576 Hz. The tuned sampling rates would be in the range of 47.88 kHz to 48.12 kHz. This capability of reference clock prevents buffer underrun and overrun while maintaining an acceptable CODEC sample rate with a swing range of 0.2% and still achieving high-quality audio.
The MPLAB Harmony I²S driver API DRV_I2S_SamplingRateSet is used to set the desired sampling rate to speed up or slow down the associated CODEC clock on the fly.
Application Examples
MPLAB Harmony provides a number of application examples in the form of application demonstrations and self-paced training modules that use the I²S driver in audio applications.
In the application examples cited below, the I²S driver is not used by the application implementation directly. Instead, the application configures the I²S driver in the MHC for necessary parameters like sampling rate, MCLK, data bits, etc., and generates the code. The CODEC driver implementation internally uses the I²S driver APIs to implement the audio data path. The application uses CODEC driver APIs to access the audio control and data paths.
Training Modules
The training module SD card audio [player is developed as a series of lab exercises to understand how the following exercise enhances and adds new functionality to the earlier exercise. This series progresses from a basic tone playback example to a full-fledged SD card audio player.
Lab1: Audio tone generation using lookup table
Lab2: Audio tone generation using text file stored in SD card
Lab3: SD card reader support to load audio files
Lab4: Play WAV audio file from SD card
Lab5: Display graphics support to select and play audio file
Lab1 is of particular interest as it configures and initializes the I²S driver to be used in the labs.
Other training modules which use I²S drivers:
Voice Recorder/Player
USB Audio Speaker
USB Flash Drive Audio Player
Application Demonstrations
MPLAB Harmony comes with pre-build application demonstrations. These application demonstrations are available at <Harmony install path>/apps.
Audio specific application demonstrations are available at <Harmony install path>/apps/audio. All the audio applications use the I²S driver for realizing audio data transfers.
In the training modules and application demonstrations cited above, the I²S driver is used in a variety of ways that includes DMA mode, single client, multi-client, DMA channel chaining, etc. explained in this tutorial.