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.
Note, that this document refers to Medianut Board Version 1 (VS1001). In the meantime a new board revision 2 (VS1011) became available.
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 Nut/OS 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
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; }
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.
[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
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"
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);
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!
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);
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 */
- 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.
NutSegBufInit(8192); NutSegBufReset(); for(;;) { buf = NutSegBufWriteRequest(&rbytes); got = fread(buf, 1, rbytes, stream); NutSegBufWriteCommit(got); }
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
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));
u_short mss = 1460; NutTcpSetSockOpt(sock, TCP_MAXSEG, &mss, sizeof(mss));
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 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';
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!
- LCD connector
- 4 button keyboard connector
- Infrared remote control receiver connector
Wireless radio prototype with Ethernut, Datanut and Medianut.