lib_adat: ADAT lightpipe#
Introduction#
The ADAT Lightpipe, officially the ADAT Optical Interface, is a standard for the transfer of digital audio between equipment via optical cable. The data transmission rate is determined by the transmitter, and the receiver has to recover the sample rate. ADAT can carry eight channels of uncompressed digital audio at sample-rates of 44.1 or 48 kHz.
Important characteristics of lib_adat
are the following:
The sample rate(s) supported. Typical values are 44.1 or 48. Higher rates are supported with a reduced number of samples via S/MUX (‘sample multiplexing’)
Transmit and Receive support. Some systems require only ADAT output, or only ADAT input. Others require both.
Note that ADAT of eight channels at 48 Khz is identical to four channels at 96 KHz - a single bit in the data stream differentiates it (but the bit rates, transmit, and receive code are identical).
Transmit#
This module can transmit ADAT signals at the following rates (assuming eight threads on a 500 MHz part)
Functionality provided |
Resources required |
|||
Channels |
Sample Rate |
1-bit port |
Threads |
Memory |
8 |
up to 48 KHz |
1-2 |
1+ |
3.6K |
8 |
up to 48 KHz |
1-2 |
1 |
3.5K |
It requires a single thread to run the transmit code. The number of 1-bit ports depends on whether the master clock is already available on a one-bit port. If available, then only a single 1-bit port is required to output ADAT. If not, then two ports are required, one for the signal output, and one for the master-clock input.
The precise transmission frequencies supported depend on the availability of an external clock (eg, a PLL or a crystal oscillator) that runs at a frequency of:
512 * sampleRate
or a power-of-2 multiple. For example, for 48 Khz the external clock has to run at a frequency of 24.576 MHz. If both 44,1 and 48 Khz frequencies are to be supported, both a 24.587 MHz and a 22.579 MHz master clock are required. This is normally not an issue since the same clocks can be used to drive the audio codecs.
When using an xcore.ai based device these frequencies can be generated by the on-chip application/secondary PLL.
Receive#
This module can receive ADAT signals at the following rates (assuming 8 threads on a 500 MHz part)
Functionality provided |
Resources required |
||
Channels |
Sample Rate |
1-bit port |
Memory |
8 |
up to 48 KHz |
1 |
3.5 KB |
A single 62.5-MIPS core is required. The receiver does not require any external clock, but can only recover 44.1 and 48 KHz sample rates.
ADAT receive#
The ADAT receiver comprises a single thread that parses data as it arrives on a one-bit port and that outputs words of data onto a streaming channel end. Each word of data carries 24 bits of sample data and 4 bits of channel information.
The receiver requires the xcore reference clock being exactly 100 Mhz (default value).
The receiver API comprises two functions, one that receives adat at 48 KHz, and one
that receives ADAT at 44.1 KHz. If the frequency of the input signal is
known a priori, the call that function in a non terminating while(1)
loop. If the frequency could be either, then call the two functions in
succession from a while(1)
loop (recommended).
Receive API#
Compile time defines#
- ADAT_REF
Define this to 100 to state that the reference clock is exactly 100 MHz (for example when using a 20 or 25 MHz crystal), or 999375 to state that the reference clock is 99.9375 MHz (the result of using a 13 MHz crystal on an XS1-L series devices). Other values are at present not supported.
Functions#
-
void adatReceiver48000(in_buffered_port_32_t p, streaming_chanend_t oChan)#
ADAT Receive Thread (48kHz sample rate).
When a data rame is received, samples will be output onto the streaming channel At first a word 0x000000Z1 will be output, where
Z
are the user data; after that eight words 0x0ZZZZZZ0 will be output whereZZZZZZ
is a 24-bit sample value. The eight words may refer to sample values on eight channels, or on fewer channels if muxing is used.The function will return if it cannot lock onto a 48,000 Hz signal. Normally the 48000 function is called in a while(1) loop. If both 44,100 and 48,000 need to be supported, they should be called in sequence in a while(1) loop. Note that the functions are large, and that 44,100 should not be called if 44.1 KHz does not need to be supported.
- Parameters:
p – ADAT port - should be 1-bit, 32-bit buffered, and clocked at 100MHz
oChan – channel on which decoded samples are output
-
void adatReceiver44100(in_buffered_port_32_t p, streaming_chanend_t oChan)#
ADAT Receive Thread (44.1kHz sample rate).
When a data rame is received, samples will be output onto the streaming channel At first a word 0x000000Z1 will be output, where
Z
are the user data; after that eight words 0x0ZZZZZZ0 will be output whereZZZZZZ
is a 24-bit sample value. The eight words may refer to sample values on eight channels, or on fewer channels if muxing is used.The function will return if it cannot lock onto a 44,100 Hz signal. Normally the 44,100 function is called in a while(1) loop. If both 44,100 and 48,000 need to be supported, they should be called in sequence in a while(1) loop. Note that the functions are large, and that 48,000 should not be called if 48 Khz does not need to be supported.
- Parameters:
p – ADAT port - should be 1-bit 32-bit buffered, and clocked at 100MHz
oChan – channel on which decoded samples are output
Receive example#
A simple receive program is provided in examples/app_adat_rx_example
. This application simply
receives samples and periodically emits the number of frames received to the terminal. This section
examines this simple example.
The input port needs to be declared as a buffered port:
buffered in port:32 p_adat_rx = PORT_ADAT_IN;
The receive functions should be called from a while(1)
loop.
void receive_adat(streaming chanend c)
{
while(1)
{
adatReceiver48000(p_adat_rx, c);
adatReceiver44100(p_adat_rx, c);
}
}
A data handler task inspects the received data samples and synchronises with the beginning of each frame. In this case, we expect every 9th value to be marked with a ‘1’ nibble to indicate end-of-frame.
void collect_samples(streaming chanend c)
{
unsigned head, channels[9];
int count = 0;
while(1)
{
for(int i = 0; i < 9; i++)
{
c :> head;
if ((head & 0xF) == 1)
{
break;
}
channels[i] = head;
}
++count;
if ((count % 100000) == 0)
{
printstr("Frames received: ");
printintln(count);
}
// One whole frame in channels [0..7]
}
}
main()
simply forks the data handling task and the receiver in
parallel in two threads:
int main(void)
{
streaming chan c;
par
{
on tile[0]:
{
board_setup();
receive_adat(c);
}
on tile[0]: collect_samples(c);
}
return 0;
}
ADAT transmit#
There are two functions in the API that can produce an ADAT signal. The simplest is a single thread that inputs samples over a channel and that outputs data on a 1-bit port.
A more complex version has a thread that inputs samples over a channel and that produces an ADAT signal onto a second channel. Another thread is required to copy this data from the channel onto a port. This second version is useful if the ADAT output port resides on a different tile.
This document provides example usage for both variants.
An identical protocol is used by both variants for inputting sample values
to be transmitted over ADAT. The first word transmitted over the
chanend should be the multiplier of the master clock (either 1024 or 512),
the second word should be the S/MUX setting (either 0 or 2), then there should be N
x 8 words of sample values, terminated by an XS1_CT_END
control token. If no
control token is sent, the transmission process will not terminate, and an
infinite stream of ADAT data can be sent.
The multiplier is the ratio between the master clock and the bit-rate; 1024 refers to a 49.152 MHz master-clock, 512 assumes a 24.576 MHz master clock.
The output of the ADAT transmit thread has to be synchronised with an external flip-flop. In order to make sure that the flip-flop captures the signal on the right edge, the output port should be set up as follows:
set_clock_src(mck_blk, mck); // Connect Master Clock Block to mclk pin
set_port_clock(adat_port, mck_blk); // Set ADAT_tx to be clocked from mck_blk
set_clock_fall_delay(mck_blk, 7); // Delay falling edge of mck_blk
start_clock(mck_blk); // Start mck_blk
Transmit API#
Functions#
-
void adat_tx(chanend c_data, chanend c_port)#
Function that takes data over a channel end, and that outputs this in ADAT format onto a 1-bit port. The 1-bit port should be clocked by the master-clock, and an external flop should be used to precisely align the edge of the signal to the master-clock.
Data should be send onto c_data using outuint only, the first two values should be The multiplier and the smux values, after that output any number of eight samples (24-bit, right aligned), and if the process is to be terminated send it an control token 1.
The data is output onto a channel, which a separate process should output to a port. This process should byte-reverse every word read over the channel, and then output the reversed word to a buffered 1-bit port.
- Parameters:
c_data – Channel over which to send sample values to the transmitter
c_port – Channel on which to generate the ADAT stream
-
void adat_tx_port(chanend c_data, out_buffered_port_32_t p_data)#
Function that takes data over a channel end, and that outputs this in ADAT format onto a 1-bit port. The 1-bit port should be clocked by the master-clock, and an external flop should be used to precisely align the edge of the signal to the master-clock.
Data should be send onto c_data using outuint only, the first two values should be The multiplier and the smux values, after that output any number of eight samples (24-bit, right aligned), and if the process is to be terminated send it an control token 1.
- Parameters:
c_data – Channel over which to send sample values to the transmitter
p_data – 1-bit, 32-bit buffered, port on which to generate the ADAT stream
Transmit example#
Example applications are provided for both the ‘direct port’ and ‘remote port’ API variants. These
are app_adat_tx_direct_example
and app_adat_tx_example
respectively.
Both examples transmit sine waves on all channels and are described in this section.
Direct port example#
The output port is declared as a 32-bit buffered port, and the master clock input must be declared as an unbuffered input port. A clock block is also required:
buffered out port:32 p_adat_tx = PORT_ADAT_OUT;
in port p_mclk_in = PORT_MCLK_IN;
out port p_ctrl = PORT_CTRL;
on tile[1]: clock clk_audio = XS1_CLKBLK_2;
The ports are setup such that the output port is clocked from the master clock with a suitable delay (to enable the external flop to latch the signal). Starting the clock block is a critical step, otherwise outputs to the transmit port will pause:
set_clock_src(clk_audio, p_mclk_in);
configure_out_port_no_ready(p_adat_tx, clk_audio, 0);
set_clock_fall_delay(clk_audio, 7);
start_clock(clk_audio);
The data generator task initially communicates the clock multiplier and the S/MUX flags, prior to transmitting data.
The task uses a table to generate sine-waves of various frequencies and phase-shifts (allowing for channel identification):
void generate_samples(chanend c) {
int count1 = 0;
int count2 = 0;
int count4 = 0;
outuint(c, MCLK_FREQUENCY_48 / 48000); // clock multiplier value
outuint(c, 1); // S/MUX value
unsafe {
volatile unsigned * unsafe sample_ptr = (unsigned * unsafe) &samples[0];
outuint(c, (unsigned) sample_ptr);
}
while(1) {
inuint(c);
// Update sample values
samples[0] = sine_table[count1]; // 500Hz sine
samples[1] = sine_table[SINE_TABLE_SIZE - 1 - count1]; // 500Hz sine, phase-shifted from channel 0
samples[2] = sine_table[count2]; // 1000Hz sine
samples[3] = sine_table[SINE_TABLE_SIZE - 1 - count2]; // 1000Hz sine, phase-shifted from channel 2
samples[4] = sine_table[count4]; // 2000Hz sine
samples[5] = sine_table[SINE_TABLE_SIZE - 1 - count4]; // 2000Hz sine, phase-shifted from channel 4
samples[6] = sine_table[count1]; // same as channel 0
samples[7] = sine_table[SINE_TABLE_SIZE - 1 - count1]; // same as channel 1
unsafe {
volatile unsigned * unsafe sample_ptr = (unsigned * unsafe) &samples[0];
outuint(c, (unsigned) sample_ptr);
}
// Handle rollover of the sine_table array indices
count1 += 1;
count2 += 2;
count4 += 4;
if (count1 == SINE_TABLE_SIZE) {
count1 = 0;
count2 = 0;
count4 = 0;
} else if (count2 == SINE_TABLE_SIZE) {
count2 = 0;
count4 = 0;
} else if (count4 == SINE_TABLE_SIZE) {
count4 = 0;
}
}
}
The main program simply forks the data generating and the transmitter tasks in parallel in two
threads. A channel is declared and passed to both tasks to allow communication.
A board_setup
tasks is also spawned that configures the external hardware and configures
the xcore.ai application PLL to generate a suitable master-clock.
int main(void)
{
chan c;
par
{
on tile[0]: board_setup();
on tile[1]: transmit_adat(c);
on tile[1]: generate_samples(c);
}
return 0;
}
Remote port example#
Much of the remote port example matches the direct port example. The output port is declared as a buffered port, and the master clock input must be declared as an unbuffered input port. A clock block is also required:
buffered out port:32 p_adat_tx = PORT_ADAT_OUT;
in port p_mclk_in = PORT_MCLK_IN;
out port p_ctrl = PORT_CTRL;
on tile[1]: clock clk_audio = XS1_CLKBLK_2;
//*
#define MCLK_FREQUENCY_48 24576000
void board_setup(void)
{
set_port_drive_high(p_ctrl);
// Drive control port to turn on 3V3.
// Bits set to low will be high-z, pulled down.
p_ctrl <: 0xA0;
// Wait for power supplies to be up and stable.
delay_milliseconds(10);
sw_pll_fixed_clock(MCLK_FREQUENCY_48);
while (1) {}
}
/* Port driver */
void drive_port(chanend c_port)
{
while (1)
{
p_adat_tx <: byterev(inuint(c_port));
}
}
Again, the ports are setup so that the output port is clocked from the master clock with a suitable delay (to enable the external flop to latch the signal). Starting the clock block is a critical step, otherwise outputs to the transmit port will pause:
set_clock_src(clk_audio, p_mclk_in);
configure_out_port_no_ready(p_adat_tx, clk_audio, 0);
set_clock_fall_delay(clk_audio, 7);
start_clock(clk_audio);
The thread that drives the port should input words from the channel, and output them with reversed byte order. Note that this activity of input, byte-reverse and output takes only three instructions and can often be merged with other task; for example if there is an I²S thread that delivers data synchronised to the same master clock, then that thread can simultaneously drive the ADAT and I²S ports:
void drive_port(chanend c_port)
{
while (1)
{
p_adat_tx <: byterev(inuint(c_port));
}
}
For simplicity, this example spawns the adat_tx
and port-driving tasks on the same tile:
par
{
adat_tx(c, c_port);
drive_port(c_port);
}
The data generation task closely matches the previous example, first communicating the clock multiplier and the S/MUX flags, prior to transmitting data:
void generate_samples(chanend c) {
int count1 = 0;
int count2 = 0;
int count4 = 0;
outuint(c, MCLK_FREQUENCY_48 / 48000); // clock multiplier value
outuint(c, 0); // S/MUX value
for (int idx = 0; idx < 8; ++idx) {
outuint(c, 0);
}
while(1) {
// Send the next samples
outuint(c, sine_table[count1]); // 500Hz sine
outuint(c, sine_table[SINE_TABLE_SIZE - 1 - count1]); // 500Hz sine, phase-shifted from channel 0
outuint(c, sine_table[count2]); // 1000Hz sine
outuint(c, sine_table[SINE_TABLE_SIZE - 1 - count2]); // 1000Hz sine, phase-shifted from channel 2
outuint(c, sine_table[count4]); // 2000Hz sine
outuint(c, sine_table[SINE_TABLE_SIZE - 1 - count4]); // 2000Hz sine, phase-shifted from channel 4
outuint(c, sine_table[count1]); // same as channel 0
outuint(c, sine_table[SINE_TABLE_SIZE - 1 - count1]); // same as channel 1
// Handle rollover of the sine_table array indices
count1 += 1;
count2 += 2;
count4 += 4;
if (count1 == SINE_TABLE_SIZE) {
count1 = 0;
count2 = 0;
count4 = 0;
} else if (count2 == SINE_TABLE_SIZE) {
count2 = 0;
count4 = 0;
} else if (count4 == SINE_TABLE_SIZE) {
count4 = 0;
}
}
}
The main program simply forks the data generating thread and the transmitter in parallel in two threads. Prior to starting the transmitter, the clocks should be set up:
int main(void) {
chan c;
par
{
on tile[0]: board_setup();
on tile[1]: transmit_adat(c);
on tile[1]: generate_samples(c);
}
return 0;
}
Additional examples#
An additional example, app_adat_loopback
, is also provided. This expects a cable to be connected
between transmit and receive. The application transmits counters on all channels and checks for the
correct reception of these counters. This application can be useful for initial debugging and
validation of external transmit/receive circuitry.