AVR Internet Radio


Introduction

This tutorial presents a step by step guide on how to build an stand-alone Embedded Internet Radio. The device will be attached to a local Ethernet and connects MP3 streaming servers. If an Internet gateway (router) is available, you can listen to public radio stations listed at www.shoutcast.com, for example.


Required Hardware

Ethernut board, preferably version 2 or similar hardware. It's possible to run the code on version 1 boards, but because of the lack of sufficient RAM, the bitrates for MP3 streaming are limited.

Medianut board or similar hardware using a VS1001, VS1002 or VS1011 for MP3 decoding. These chips are manufactured by VLSI Solution Oy, Finland,

The Medianut can be mounted on top of the Ethernut board.






Assembled and tested boards as well as components may be purchased from egnite GmbH, Germany,


Required Software

The presented code had been tested on Nut/OS 3.9.2.1pre, but should also work on earlier releases of version 3.

Follow the instructions presented in the pdf Ethernut Software Manual to create the Nut/OS libraries and a sample application directory for your specific environment.


Step 1: Connecting an Internet Radio Station

We will utilize an RS232 port for debug messages. Nut/OS provides a rich set of stdio routines such as printf, scanf etc., which are fairly similar to Windows and Linux. However, the initialization is a bit different. There are no predefined devices for stdin, stdout and stderr. Devices used by an application have to be registered first by calling NutRegisterDevice(). We choose devDebug0, which is a very simple UART driver for RS232 output. Due to its simplicity it's possible to produce debug messages even from within interrupt routines.

After device registration, we are able to open a C stdio stream on this device. The function freopen() will attach the resulting stream to stdout. Thus we can use simple calls like printf(), puts() or putchar().

The _ioctl() routine provides device specific control functions like setting the baudrate.

We place an endless loop at the end of main(). In opposite to programs written for desktop computers, the main() function will not return because there is nothing to return to.

  #include <dev/debug.h>
  #include <stdio.h>
  #include <io.h>

  #define DBG_DEVICE devDebug0
  #define DBG_DEVNAME "uart0"
  #define DBG_BAUDRATE 115200

  int main(void)
  {
    NutRegisterDevice(&DBG_DEVICE, 0, 0);

    freopen(DBG_DEVNAME, "w", stdout);
    _ioctl(_fileno(stdout), UART_SETSPEED, &baud);

    puts("Reset me!");
    for(;;);
  }

The need for device registration also applies to the Ethernet device. The Ethernut hardware supports two different LAN controller chips, the LAN91C111 and the RTL8019AS.

  #ifdef ETHERNUT2
  #include <dev/lanc111.h>
  #else
  #include <dev/nicrtl.h>
  #endif

  NutRegisterDevice(&DEV_ETHER, 0x8300, 5);
ETHERNUT2 is typically defined in the file UserConf.mk located in the application directory.
  HWDEF += -DETHERNUT2
Remember to remove this line when the programs should run on Ethernut 1.3 or earlier board revisions.

TCP/IP over Ethernet interfaces typically require some sort of configuration, like the unique MAC address, local IP address, the network mask, routing information etc. The most comfortable way to define these variables is to make use of a local DHCP server. If there is none available in your local network, then you can install a simple one on your Windows PC. DHCP servers are also available on all Linux systems.

If you can't or don't want to install a DHCP server, you need to provide the required parameters in your application code defining them as preprocessor macros, for example.

  #define MY_MAC { 0x00, 0x06, 0x98, 0x10, 0x01, 0x10 }
  #define MY_IPADDR "192.168.192.100"
  #define MY_IPMASK "255.255.255.0"
  #define MY_IPGATE "192.168.192.1"

MY_MAC specifies the Ethernet address, which must be unique in your local Ethernet.

MY_IPADDR is the local TCP/IP address of the Ethernut board, which needs to be unique too, but not only in the local network. If you are connecting to the Internet, no other node in the Internet should have the same one. Fortunately, your Internet access provider will assign a unique address to your router and the router translates local IP addresses to this unique address. Special address ranges are reserved for local networks, like 192.168.x.y. So you only have to take care, that the address is not used by another computer in your local Ethernet.

MY_IPMASK must be equal on all computers in your local network. Note, that if your network uses 255.255.255.0 for example, then the first three parts of the MY_IPADDR must be equal on all computers too, 192.168.192 in our example.

MY_IPGATE should be set to the IP address of your router. If you don't intend to connect to the Internet, then set this parameter to "0.0.0.0".

We use a specific routine to configure the network interface in three trial and error steps. The advantage is, that this code will work with DHCP, hard coded values and, not mentioned yet, previously saved EEPROM values.

  int ConfigureLan(char *devname)
  {
      if (NutDhcpIfConfig(devname, 0, 60000)) {
          u_char mac[6] = MY_MAC;
          if (NutDhcpIfConfig(devname, mac, 60000)) {
              u_long ip_addr = inet_addr(MY_IPADDR);
              u_long ip_mask = inet_addr(MY_IPMASK);
              u_long ip_gate = inet_addr(MY_IPGATE);
              if(NutNetIfConfig(devname, mac, ip_addr, ip_mask)) {
                  return -1;
              }
              if(ip_gate) {
                  if(NutIpRouteAdd(0, 0, ip_gate, &DEV_ETHER) == 0) {
                      return -1;
                  }
              }
          }
      }
      return 0;
  }
The first call to NutDhcpIfConfig() assumes, that a complete configuration is available in the ATmega EEPROM already. This may be the case, if you previously ran the Basemon application and entered these parameters before starting the sample Webserver. If the fuse "Preserve EEPROM Contents" has been enabled in the ATmega128, then the network configuration remains intact when erasing the device during the programming cycle, while uploading a new application.

This first call will fail, if the EEPROM is empty. A second call is done, which provides the MAC address. Again, this will fail, if DHCP is not available in the local network. The final call to NutNetIfConfig() will provide all required parameters, except the default route to our Internet gateway. If this parameter is not zero, a call to NutIpRouteAdd() will pass the IP address of the router.

After having configured the network interface, we can use it to establish a TCP/IP connection to an MP3 streaming server. Three items are required to specify an MP3 stream.

  • The server's IP address.
  • The server's TCP port.
  • The server's URL.
All three can be obtained by visiting www.shoutcast.com. Choose one of the stations offered and download the related playlist entry. Simply klicking on the "Tune In" button may automatically start an application like Winamp or whatever has been registered as an MP3 player plugin with your browser. Try to click the "Tune In" button with the right mouse button to download the shoutcast-playlist.pls file instead. This is a simple text file with a contents like the following:
  [playlist]
  numberofentries=2
  File1=http://64.236.34.196:80/stream/1020
  Title1=(#1 - 90/29131) Smoothjazz.Com
  Length1=-1
  File2=http://64.236.34.4:80/stream/1020
  Title2=(#2 - 124/38816) Smoothjazz.Com
  Length2=-1
  Version=2
The File1 (alternative File2) entry contains the information we need to create the proper parameters in our radio application.
  #define RADIO_IPADDR "64.236.34.196"
  #define RADIO_PORT 80
  #define RADIO_URL "/stream/1020"
Connecting the radio station is simple.
  TCPSOCKET *sock;
  u_long ip = inet_addr(RADIO_IPADDR);

  sock = NutTcpCreateSocket();
  NutTcpConnect(sock, ip, RADIO_PORT);
  stream = _fdopen((int) sock, "r+b");

  fprintf(stream, "GET %s HTTP/1.0\r\n", RADIO_URL);
  fprintf(stream, "Host: %s\r\n", inet_ntoa(ip));
  fprintf(stream, "User-Agent: Ethernut\r\n");
  fprintf(stream, "Accept: */*\r\n");
  fprintf(stream, "Icy-MetaData: 1\r\n");
  fprintf(stream, "Connection: close\r\n\r\n");
  fflush(stream);

  line = malloc(512);
  while(fgets(line, 512, stream)) {
      cp = strchr(line, '\r');
      if(cp == 0)
          continue;
      *cp = 0;
      if(*line == 0)
          break;
      printf("%s\n", line);
  }
  free(line);
This code fragment creates a TCP socket, establishes a connection to a given IP address and port number and opens a stream on the TCP socket. Next, the HTTP request is send to the server and the response is received and printed to stdout until the first empty line.

mnut01-041111.zip
Contains complete source code and binaries of step 1. Here's a sample output:

  Medianut Tutorial Part 1 - Nut/OS 3.9.2.1 pre - AVRGCC
  29877 bytes free

  Configure eth0...OK
  MAC : 00-06-98-21-02-B0
  IP  : 192.168.192.202
  Mask: 255.255.255.0
  Gate: 192.168.192.1

  Connecting 64.236.34.196:80...OK
  GET /stream/1040 HTTP/1.0

  ICY 200 OK
  icy-notice1: <BR>This stream requires <aef="http://www.winamp.com/">Winamp</a><BR>
  icy-notice2: SHOUTcast Distributed Network Audio Server/SolarisSparc v1.9.4<BR>
  icy-name: CLUB 977 The 80s Channel (HIGH BANDWIDTH)
  icy-genre: 80s Pop Rock
  icy-url: http://www.club977.com
  icy-pub: 1
  icy-metaint: 8192
  icy-br: 128
  icy-irc: #shoutcast
  icy-icq: 0
  icy-aim: N/A

  Reset me!
No music yet, but in the next step.


Step 2: Playing an MP3 Stream

Following the empty line, which marks the end of the header, the streaming server will send an endless stream of binary data, the MP3 encoded audio data. Reading this data into a buffer is nothing special.

  int got;
  u_char *buf;

  buf = malloc(2048);
  got = fread(buf, 1, 2048, stream);
The problem is to pass this data to the MP3 decoder.

Nut/OS includes a device driver for the VS1001K decoder chip. Actually this is not a common device driver with a NUTDEVICE structure and support for stdio read and write. This wouldn't make much sense, because tiny systems like Ethernut suffer from buffer copying. The following code would result in low performance.

  /* Bad example */
  got = fread(buf, 1, 2048, stream);
  fwrite(buf, 1, got, decoder); /* Not available */
The data needs to be copied at least four times.
  • From the Ethernet Controller to the TCP buffer.
  • From the TCP buffer to the application buffer.
  • From the application buffer to the decoder buffer.
  • From the decoder buffer to the decoder chip.
To reduce CPU utilization the MP3 driver expects the data in a Nut/OS segmented buffer. Our radio application uses the following code to fill this buffer.
  NutSegBufInit(8192);
  NutSegBufReset();

  for(;;) {
      buf = NutSegBufWriteRequest(&rbytes);
      got = fread(buf, 1, rbytes, stream);
      NutSegBufWriteCommit(got);
  }
Another advantage of the segmented buffer API is, that it can handle non-continous memory like the banked RAM on Ethernut 2.

NutSegBufInit() initializes the buffer. For the banked memory on Ethernut 2 boards, the parameter is ignored. All memory banks are automatically occupied. For Ethernut 1 boards the specified number of bytes are taken from heap memory to create the buffer.

NutSegBufReset() clears the buffer.

NutSegBufWriteRequest() returns the continous buffer space available at the current write position.

NutSegBufWriteCommit() commits the specified number of bytes written.

With this scheme, data copying is reduced by 25% and takes place

  • From the Ethernet Controller to the TCP buffer.
  • From the TCP buffer to the segmented buffer.
  • From the segmented buffer to the decoder chip.

As stated above, the VS1001 driver doesn't support stdio read and write routines. Instead a number of individual routines are provided to control the decoding process.

VsPlayerInit() resets the decoder hardware and software and should be the first routine called by our application.

VsPlayerReset() initializes the decoder and must be called before decoding a new data stream.

VsGetStatus() can be used to query the current status of the driver.

VsSetVolume() sets the analog output attenuation of both stereo channels.

VsPlayerKick() finally starts decoding the data in the segmented buffer.

It is possible to access the segmented buffer from within interrupt routines and the Nut/OS VS1001 driver makes use of this feature. However, calling NutSegBufReset(), NutSegBufWriteRequest() or NutSegBufWriteCommit() modifies certain multibyte values using non-atomic operations, which needs to be protected from access during interrupts. We could use the Nut/OS NutEnterCritical() and NutExitCritical() calls, but this disables all interrupts, system-wide. This includes interrupts initiated by out Ethernet controller, leading to a degradation of our TCP response time and overall transfer rate. Luckily, the VS1001 driver offers a routine named VsPlayerInterrupts(), which disables/enables decoder interrupts only.

  u_char ief;

  ief = VsPlayerInterrupts(0);
  /* Exclusive call here. */
  VsPlayerInterrupts(ief);

mnut02-041111.zip
Contains complete source code and binaries of step 2. A sample output is here:

  Medianut Tutorial Part 2 - Nut/OS 3.9.2.1 pre - AVRGCC
  29743 bytes free

  Configure eth0...OK
  MAC : 00-06-98-21-02-B0
  IP  : 192.168.192.202
  Mask: 255.255.255.0
  Gate: 192.168.192.1

  Connecting 64.236.34.196:80...OK
  GET /stream/1020 HTTP/1.0

  ICY 200 OK
  icy-notice1: <BR>This stream requires <a href="http://www.winamp.com/">Winamp</a><BR>
  icy-notice2: SHOUTcast Distributed Network Audio Server/SolarisSparc v1.9.4<BR>
  icy-name: Smoothjazz.Com - The worlds best Smooth Jazz - Live From Monterey Bay
  icy-genre: smooth jazz
  icy-url: http://www.smoothjazz.com
  icy-pub: 1
  icy-metaint: 8192
  icy-br: 32
  icy-irc: #shoutcast
  icy-icq: 0
  icy-aim: N/A

  Read 594 of 16384
  Read 512 of 15790
  Read 512 of 15278
  Read 512 of 14766
  Read 512 of 14254
  Read 512 of 13742
  Read 512 of 13230
  Read 512 of 12718
  Read 512 of 12206
  Read 144 of 11694
  4834 buffered
When connecting a headphone or line input of an amplifier to the Medianut, we should be able to listen to the radio station we connected. Though, we may notice hiccups or the stream may stop shortly after establishing the connection. There's obviously something left to do.

Step 3: Refining the Player

The default setup of the Nut/Net TCP stack is optimized for tiny embedded systems with data exchange in both directions at minimal memory usage. We can use NutTcpSetSockOpt() to optimize two of the parameters for MP3 streaming.

  u_short tcpbufsiz = 8760;

  NutTcpSetSockOpt(sock, SO_RCVBUF, &tcpbufsiz, sizeof(tcpbufsiz));
This increases the buffer, which is initially offered to the server. It means, that the server can send up to 8760 bytes without waiting for any response from us.

  u_short mss = 1460;

  NutTcpSetSockOpt(sock, TCP_MAXSEG, &mss, sizeof(mss));
This is the maximum number of data bytes the server may send in a single TCP segment. By default, the Nut/Net TCP stack uses a maximum segment size of 536, which most probably saves us from packet fragmentation, which is not supported by Nut/Net. Fragmentation occurs, when a packet passes a network with lower MTU (maximum transfer unit) on its way through the Internet from the server to our Ethernut. On Ethernet networks, the MTU is 1500 bytes and the TCP header is 40 bytes, which results in an MSS of 1460 bytes. In the Internet today fragmentation of segments with 1460 bytes is quite seldom.

Another problem appears, when the server or the connection dies. In such a case our MP3 player, the TCP client, may never return from the fread(). Setting a socket read timeout solves this problem. After the specified number of milliseconds fread() will return with zero bytes read.

  u_long rx_to = 3000;

  NutTcpSetSockOpt(sock, SO_RCVTIMEO, &rx_to, sizeof(rx_to));

As we found out in the last step, audio output contains hiccups or may even become completely scrambled. The reason is, that the stream contains some additional information, the so called metadata tags. In the previous step we passed this unfiltered to the decoder chip, which is of course quite picky about extra bytes included in the MP3 stream.

The server sends an information about how many bytes of MP3 data are between the metadata tags in the initial header lines.

  icy-metaint: 8192
The hiccups should disappear, when we use this value to strip the metadata tags from the stream.

The metadata tag begins with a single byte, which indicates the total size of the tag when multiplied by 16. Here's an example of the contents of such a metadata tag.

  StreamTitle='Alphaville, Big in Japan';StreamUrl='http://www.club977.com/ads';
It contains a number of name/value pairs, separated by semicolons. An equals sign is used to separate the name from its value and each value is surrounded by quotes. The value of StreamTitle typically informs us about the music title currently transmitted. For now we print this to our debug device.

mnut03-041111.zip
Contains complete source code and binaries of step 3.

Quite often we will something like this after starting the player.

  Connecting 64.236.34.196:80...Error: Connect failed with 10060
  Reset me!
The reason for not connecting to a station, which worked before resetting the Ethernut is, that we didn't close our previous connection to this the server. To do this, we need some kind user interface. Medianut supports three hardware interfaces:
  • LCD connector
  • 4 button keyboard connector
  • Infrared remote control receiver connector
Future parts of this tutorial will show us how to use these.




Wireless radio prototype with Ethernut, Datanut and Medianut.