<< >>

An XMOS AVB application (tutorial)

This tutorial walks through the application code for the XR-AVB-LC-BRD demonstration application. This application provides both a talker transmitting a single eight channel stream and a listener that can receive an eight channel stream.

The code for this demo is found in the app_xr_avb_lc_demo/src directory.

avb_conf.h

The avb_conf.h file sets the required defines for configuring the AVB system. Every application must include this file and the required and optional that can be set in this file are described in Section Configuration Defines.

First, the file sets up the ethernet buffering. This is a balance between the required memory for the rest of the application and the amount of buffering needed for the audio.

#define PHY_ADDRESS 0x0
#define MAX_ETHERNET_PACKET_SIZE (1518)
#define NUM_MII_RX_BUF 6
#define NUM_MII_TX_BUF 3
#define ETHERNET_RX_HP_QUEUE 1
#define MAX_ETHERNET_CLIENTS   5
#define MII_RX_BUFSIZE_HIGH_PRIORITY (700)    
#define MII_RX_BUFSIZE_LOW_PRIORITY (300)
#define MII_TX_BUFSIZE_HIGH_PRIORITY (300)    
#define MII_TX_BUFSIZE_LOW_PRIORITY (200)
#define ETHERNET_MAX_TX_HP_PACKET_SIZE (300)

Some general settings are needed for memory allocation across the whole AVB code base. Here the maximum name length (use for remote control identification) and the maximum channels per stream are set:

#define AVB_MAX_NAME_LEN 25
#define AVB_MAX_CHANNELS_PER_STREAM 8

The application can listen to a single eight channel stream. This requires a single sink that can be handled by a single listener unit:

#define AVB_NUM_SINKS 1
#define AVB_NUM_LISTENER_UNITS 1

The application will produce a single eight channel stream. This requires a single source that can be handled by a single talker unit:

#define AVB_NUM_SOURCES 1
#define AVB_NUM_TALKER_UNITS 1 

The audio I/O side of the application must be configured. The board has eight digital I/Os in and out so this determines the number of input/output FIFOs. In addition the maximum sample rate of these inputs is set. A media unit is one that controls media FIFOs. Due to the way the I2S component works, two media units are required (one for input and one for output).

#define AVB_1722_FORMAT_61883_6
#define AVB_NUM_MEDIA_OUTPUTS 8
#define AVB_NUM_MEDIA_INPUTS 8
#define AVB_NUM_MEDIA_UNITS 2

The demo is synchronous in that it has one clock for both the input and output (this will end up being a clock divided down from the PTP clock).

#define AVB_NUM_MEDIA_CLOCKS 1

Finally, the XR-AVB-LC-BRD has eight digital inputs but only two of them are connected to an ADC on board. For this demo another setting is added, which causes the I2S component to ignore the input on every stereo pair but the first, and instead adds synthesized sine waves to these inputs:

#define I2S_SYNTH_FROM 1

// Defining this makes SRP auto-start and auto-stop a stream when listeners come and go
#define SRP_AUTO_TALKER_STREAM_CONTROL

The toplevel main

The main file for the demo is xr_avb_demo.xc. This file contains three main parts:

  • First, the file declares the ports needed by the application.
  • Second, the top-level main() function runs the main components that make up the application.
  • Finally, the demo() function implements the main control thread that implements the demo.

The demo runs at a particular sample rate. This affects the AVB control thread and also the thread that drives the external PLL. To co-ordinate this, the program sets some #defines relating to the sample rate:

#define SAMPLE_RATE 48000

// This is the number of master clocks in a word clock
#define MASTER_TO_WORDCLOCK_RATIO 512

Here, MASTER_TO_WORDCLOCK_RATIO controls the ratio between the master clock and the wordclock, which must match the setting in the clock generation PLL.

The next part of the file (not shown) declares the ports for ethernet, audio CODECs and the PLL.

The next part of the file contains the top-level main that runs the components of the application. It is of the form:

int main(void) {
  channel declarations
  ...
  par {
    ...
    component functions
    ...
  }
}

The first component functions are the ethernet components: the ethernet MAC and the TCP/IP stack server. The ethernet component section reads the MAC address out of OTP and then runs the ethernet server based on this. The channel arrays rx_link, tx_link and xtcp are connected to other parts of the system that use the ethernet and TCP/IP stack:

on stdcore[1]:
{
	int mac_address[2];
	ethernet_getmac_otp(otp_data,
                                          otp_addr,
                                          otp_ctrl,
                                          (mac_address, char[]));
	phy_init(clk_smi, p_mii_resetn,
			smi,
			mii);

	ethernet_server(mii, mac_address,
			rx_link, 4,
			tx_link, 4,
			smi, connect_status);
}

// TCP/IP stack
on stdcore[1]:
{
	uip_server(rx_link[1],
                                 tx_link[2],
                                 xtcp, 1,
                                 null,
                                 connect_status);
}

The next components that are run are also core components of an AVB application, namely the PTP server and the media clock server. Note that the initialization of the PLL is run before the PTP server simply because it must be initialized on core 1. The actual code that drives the PLL is on core 0. In order to save threads, the GPIO and the PTP servers are combined into a single thread.

on stdcore[1]:
{
	// We need to initiate the PLL from core 1, so do it here before
	// launching  the main function of the thread
	audio_clock_CS2300CP_init(r_i2c, MASTER_TO_WORDCLOCK_RATIO);

	ptp_server_and_gpio(rx_link[0], tx_link[0], ptp_link, 3,
			PTP_GRANDMASTER_CAPABLE,
			c_gpio_ctl);
}

on stdcore[1]:
{
	media_clock_server(media_clock_ctl,
			ptp_link[1],
			buf_ctl,
			AVB_NUM_LISTENER_UNITS,
			clk_ctl,
			AVB_NUM_MEDIA_CLOCKS);
}

As mentioned previously, the application has one outgoing stream which is handled by a single talker unit. The talker unit is instantiated next with a call to avb_1722_talker():

on stdcore[0]: avb_1722_talker(ptp_link[0],
		tx_link[1],
		talker_ctl[0],
		AVB_NUM_SOURCES);

There is also a single listener unit, which takes incoming packets and splits them into media FIFOs. However, the I2S component takes samples to play over an XC channel. So media_output_to_xc_channel_split_lr() is called to take samples from the shared memory media FIFOs and output them over an XC channel.

on stdcore[0]: avb_1722_listener(rx_link[3],
		buf_ctl[0],
		null,
		listener_ctl[0],
		AVB_NUM_SINKS);

on stdcore[0]:
{	init_media_output_fifos(ofifos, ofifo_data, AVB_NUM_MEDIA_OUTPUTS);
	media_output_fifo_to_xc_channel_split_lr(media_ctl[1],
			c_samples_to_codec,
			0, // clk_ctl index
			ofifos,
			AVB_NUM_MEDIA_OUTPUTS);
}

The above components comprise the core of the AVB system. In addition there is a debugging thread and a couple of control threads. The debugging thread calls the XLog server which redirects print statements across the system to pass over the UART port to the XTAG2. The resulting print statements can be viewed using xrun with the --uart option.

on stdcore[0]:
{
	xlog_server_uart(p_uart_tx);
}

Finally, the application has an application specific control thread.

First the AVB system must be initialized with a call to avb_init(). This call is needed before any other AVB API calls.

This function takes channels connected to all the different components of the system to be able to control them.

on stdcore[0]:
{
	// First initialize avb higher level protocols
	avb_init(media_ctl, listener_ctl, talker_ctl, media_clock_ctl, rx_link[2], tx_link[3], ptp_link[2]);

	demo(xtcp[0], rx_link[2], tx_link[3], c_gpio_ctl);

The main control thread

The main control thread is implemented in the function demo:

void demo(chanend tcp_svr, chanend c_rx, chanend c_tx, chanend c_gpio_ctl) {

This demo uses Zeroconf to advertise a configuration protocol. The name is registered as xmos_attero_endpoint, so on the local network it will have a DNS name of xmos_attero_endpoint.local. The server attero-cfg which is a configuration service over UDP on port ATTERO_CFG_PORT (with the value 40404) is also advertised. This service can be discovered by the AtteroTech host configuration utility (see the AtteroTech/XMOS XR-AVB-LC-BRD Quickstart Guide). The control API server itself must be initialized so that it can handle requests to this port.

mdns_init(tcp_svr);

// Register all the zeroconf names
mdns_register_canonical_name("xmos_attero_endpoint");
mdns_register_service("XMOS/Attero AVB", "_attero-cfg._udp",
		ATTERO_CFG_PORT, "");

// Initialize the control api server
c_api_server_init(tcp_svr);

The next section of code configures the clocking and the source streams the demo will transmit. Firstly, as mentioned earlier in the walkthrough, this demo has one media clock which is derived from the global PTP clock. This allows all endpoints to run a talker and listener on the same clock with a minimum of configuration. Another possible scheme would be for every endpoint to have a media clock that is derived from the same AVB stream.

//printstr("Media clock: LOCAL\n");
//set_device_media_clock_type(0, MEDIA_FIFO_DERIVED);
set_device_media_clock_type(0, LOCAL_CLOCK);
//set_device_media_clock_type(0, PTP_DERIVED);
set_device_media_clock_rate(0, SAMPLE_RATE);
set_device_media_clock_state(0, DEVICE_MEDIA_CLOCK_STATE_ENABLED);

// Configure the source stream
set_avb_source_name(0, "multi channel stream out");

set_avb_source_channels(0, AVB_NUM_MEDIA_INPUTS);
for (int i = 0; i < AVB_NUM_MEDIA_INPUTS; i++)
	map[i] = i;
set_avb_source_map(0, map, AVB_NUM_MEDIA_INPUTS);
set_avb_source_format(0, AVB_SOURCE_FORMAT_MBLA_24BIT, SAMPLE_RATE);
set_avb_source_sync(0, 0); // use the media_clock defined above

After the initial configuration the application enters its main control loop. This is in the standard XC form of a “while (1), select” loop. The loop iterates and at each point selects one of the case statements to handle. The cases may be activated by an incoming packet, a timer event for periodic processing or some communication from the gpio thread.

while (1) {
   ...
   select {
      case ...
         break;
      case ...
         break;
      ...
   }
}

The first case handled is an incoming AVB control packet from the MAC. The avb_get_control_packet() function fires to this case and places the packet in the array buf.

case avb_get_control_packet(c_rx, buf, nbytes):

This packet may be an 802.1Qat packet or a 1722 MAAP packet. First we pass it to the AVB packet handler (it will ignore the packet if it is not a relevant protocol).

avb_status = avb_process_control_packet(buf, nbytes, c_tx);
switch (avb_status)
{
	case AVB_SRP_TALKER_ROUTE_FAILED:
	avb_srp_get_failed_stream(streamId);
	// handle a routing failure here
	break;
	case AVB_SRP_LISTENER_ROUTE_FAILED:
	avb_srp_get_failed_stream(streamId);
	// handle a routing failure here
	break;
	case AVB_MAAP_ADDRESSES_LOST:
	// oh dear, someone else is using our multicast address
	for (int i=0;i<AVB_NUM_SOURCES;i++)
	set_avb_source_state(i, AVB_SOURCE_STATE_DISABLED);

	// request a different address
	avb_1722_maap_request_addresses(AVB_NUM_SOURCES, null);
	break;
	default:
	break;

This result of this processing may be a report of a failed route which in this application is just ignored. Alternatively, the result may be that we have lost our reserved addresses (since some other node on the network has a claim to them). In this case we request new address for our streams.

The next event the main loop responds to is an incoming TCP/IP packet.

case xtcp_event(tcp_svr, conn):
{
	if (conn.event == XTCP_IFUP)
	{
		avb_start();

		// Request a multicast addresses for stream transmission
		avb_1722_maap_request_addresses(AVB_NUM_SOURCES, null);
	}
	else if (conn.event == XTCP_IFDOWN)
	{
		for(int i=0; i<AVB_NUM_SOURCES; i++)
		{
			set_avb_source_state(i, AVB_SOURCE_STATE_DISABLED);
		}
	}

	{
		mdns_event res;
		res = mdns_xtcp_handler(tcp_svr, conn);
		if (res & mdns_entry_lost)
		{
			printstr("Media clock: FIFO\n");
			set_device_media_clock_type(0, MEDIA_FIFO_DERIVED);
		}
	}

	c_api_xtcp_handler(tcp_svr, conn);

	// add any special tcp/ip packet handling here
}
break;

Here, two handlers are called. One to handle any Zeroconf packets and one to handle any control protocol packets. The control protocol packets are used to communicate with the Atterotech PC control application. The code used to do this within the firmware is in the module module_avb_attero_cfg.

The gpio thread controls the buttons on the device. It can signal the main control thread of certain events. The next case handles this:

case c_gpio_ctl :> int cmd:
{
	switch (cmd)
	{
		case STREAM_SEL:
		change_stream = 1;
		break;
		case CHAN_SEL:
		{
			enum avb_sink_state_t cur_state;

			c_gpio_ctl :> selected_chan;
			get_avb_sink_state(0, cur_state);
			set_avb_sink_state(0, AVB_SINK_STATE_DISABLED);
			for (int j=0;j<AVB_NUM_MEDIA_INPUTS;j++)
			map[j] = (j+selected_chan*2) & 0x7;
			set_avb_sink_map(0, map, AVB_NUM_MEDIA_INPUTS);
			if (cur_state != AVB_SINK_STATE_DISABLED)
			set_avb_sink_state(0, AVB_SINK_STATE_POTENTIAL);
		}
		break;
	}
}
break;

One possibility is that the STREAM_SEL button is pressed to change the stream being listened to. This just sets the change_stream variable to be passed to the stream manager of the application later.

The other possibility is that the CHAN_SEL button is pressed which changes the channels being listened to within the stream. This disables the listener sink, reconfigures the mapping between the incoming stream and the output FIFOs and then re-enables the stream.

The final event that can be responded to is a periodic event that occurs via an XCore timer. This event happens once every 50us.

case tmr when timerafter(timeout) :> void:
timeout += PERIODIC_POLL_TIME;

do
{
	avb_status = avb_periodic();
	switch (avb_status)
	{
		case AVB_MAAP_ADDRESSES_RESERVED:
		for(int i=0;i<AVB_NUM_SOURCES;i++) {
			avb_1722_maap_get_offset_address(macaddr, i);
			// activate the source
			set_avb_source_dest(i, macaddr, 6);
			set_avb_source_state(i, AVB_SOURCE_STATE_POTENTIAL);
		}
		break;
	}
} while (avb_status != AVB_NO_STATUS);

The avb_periodic() function performs general AVB periodic processing and may return a report that the MAAP addresses that were requested have been allocated. At this point we can set the destination of the talker stream and enable it.

The other piece of periodic processing is to call the demo’s stream manager. This is a function contained in demo_stream_manager.xc, which manages the streams that have been seen and the stream that is being listened to.

demo_manage_listener_stream(change_stream, selected_chan);

The demo stream manager

The demo stream manager is contained in the file demo_stream_manager.xc. It maintains a table of streams that have been seen in the array stream_table. A stream is first seen via the function avb_check_for_new_stream().

res = avb_check_for_new_stream(streamId, vlan, addr);
      

If there is no current stream and a new stream is seen, or if the change_stream variable is set (due to a button press, see the preceding Section), then the current listened to stream is updated. This is done by reconfiguring the sink in this section of code:

curStreamId[0] = new_hi;
curStreamId[1] = new_lo;
simple_printf("Mapping %x%x ---> %d\n", 
              curStreamId[0],
              curStreamId[1],
              0);

for (int j=0;j<8;j++)
  map[j] = (j+selected_chan*2) & 0x7;

set_avb_sink_sync(0, 0);
set_avb_sink_channels(0, 8);
set_avb_sink_map(0, map, 8);
set_avb_sink_state(0, AVB_SINK_STATE_DISABLED);
set_avb_sink_id(0, curStreamId);
set_avb_sink_vlan(0, new_vlan);
set_avb_sink_addr(0, addr, 6);
set_avb_sink_state(0, AVB_SINK_STATE_POTENTIAL);

change_stream = 0;