Arduino Ethernet Audio Streaming

1. Introduction
After finishing the serial port version of my Arduino audio streaming project, I verified that the Arduino can easily handle a 20KHz sample rate and that the eight bit resolution from the on-chip A/D converter doesn’t sound too bad. The next logical step if you happen to have an Ethernet shield laying around (and who doesn’t?) is to try and stream the audio signals over a local network.
I haven’t been able to find any easy way to stream audio data to a browser, much less from the Arduino, so I opted to use an ethernet version of the python audio playback/scope script. I was also able to use the same microphone amplifier circuit.

2. The Arduino Firmware
There are similarities between the serial port version and the ethernet version, but the main hurdle is controlling the Wiznet W5100 chip on the Ethernet Shield using embedded C. There are application notes on the Wiznet web-site but they are challenging to say the least. A more basic tutorial can be found at http://www.ermicro.com/blog/?p=1773, which is well worth a look. My firmware bears a suspicious resemblance to his, except that I’ve split the SPI and W5100 functions into separate modules.

1. The SPI Interface
The W5100 chip is accessed via the Atmega328’s built-in Serial Peripheral Interface (SPI), which are output via Port B. Initialization, shown below, configures Port B, and enables the SPI functions.

void SPI_Init(void)
{ 
 // Initialize the AVR ATMega328 SPI Peripheral
 // Set MOSI (PORTB3),SCK (PORTB5) and PORTB2 (SS) as output, others as input
 SPI_DDR = (1<<PORTB3)|(1<<PORTB5)|(1<<PORTB2);
 // CS pin is not active
 SPI_PORT |= (1<<SPI_CS);
 // Enable SPI, Master Mode 0, set the clock rate fck/2
 SPCR = (1<<SPE)|(1<<MSTR);
 SPSR |= (1<<SPI2X);
}

Once the SPI has been initialized, data can written to or read from a given on-chip address by sending a READ or WRITE op-code to the device as shown below:

void SPI_Write(unsigned char opcode, unsigned int addr,unsigned char data)
{
 // Activate the CS pin
 SPI_PORT &= ~(1<<SPI_CS);
 // Start Wiznet W5100 Write OpCode transmission
 SPDR = opcode;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));
 // Start Wiznet W5100 Address High Bytes transmission
 SPDR = (addr & 0xFF00) >> 8;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));
 // Start Wiznet W5100 Address Low Bytes transmission
 SPDR = addr & 0x00FF;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));

 // Start Data transmission
 SPDR = data;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));
 // CS pin is not active
 SPI_PORT |= (1<<SPI_CS);
}
unsigned char SPI_Read(unsigned char opcode, unsigned int addr)
{
 // Activate the CS pin
 SPI_PORT &= ~(1<<SPI_CS);
 // Start Wiznet W5100 Read OpCode transmission
 SPDR = opcode;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));
 // Start Wiznet W5100 Address High Bytes transmission
 SPDR = (addr & 0xFF00) >> 8;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));
 // Start Wiznet W5100 Address Low Bytes transmission
 SPDR = addr & 0x00FF;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));

 // Send Dummy transmission for reading the data
 SPDR = 0x00;
 // Wait for transmission complete
 while(!(SPSR & (1<<SPIF)));

 // CS pin is not active
 SPI_PORT |= (1<<SPI_CS);
 return(SPDR);
}

3. W5100 Functions
A more detailed description of the functions can be found by examining my hopefully well documented source files in the link below. I’ve kept some of code from the ’emicro’ web-site for future use, but the following is a summary of the routines used in this application:

W5100_Init(void)
– sets the device MAC address, gateway IP address, subnet mask, and desired IP address. These addresses are usually determined in the DHCP process, but since I’m using a stand-alone Pyhton Script, I need a more permanent IP address. What can I say? Its also easier and for a small network, shouldn’t present a problem.

void close(uint8_t socket) – marks the socket as closed

void disconnect(uint8_t socket) – sends a termination request to the remote peer

uint8_t socket(uint8_t sock, uint8_t eth_protocol, uint16_t tcp_port)
– initializes and opens a socket. Sockets are initialized with the selected protocol (TCP, UDP etc.) and port number (4096), then opened. Rteurns 1 if successful, 0 otherwise.

uint8_t listen(uint8_t sock)
– sets the W5100 to wait for a connection request from a remote peer. Returns 1 if command was successful, 0 otherwise.

uint16_t send(uint8_t sock, uint8_t *buf, uint16_t buflen)
– transmits a buffer of bytes at address buf, and length buflen (512 bytes) over a socket sock. returns a 1 if the operation was sucessful, 0 otherwise.

4. Serial Port Functions
The serial port functions in stdio_setup are identical to the functions in the serial streaming application, except that the baud rate has been reduced to 19200 baud, and a DEBUG flag has been added to stdio_setup.h. The DEBUG flag has been added to allow debugging messages to be printed using the printf function during the development of the firmware. When in use, the DEBUG flag should be off, since printf adds an unacceptable delay to the program, resulting in unintelligible audio.

5. The main() Module
The main module consists of two primary functions, data acquisition from the A/D converter, and control and data transfer to the W5100 chip and hence the network.
Data acquisition is almost identical to the method used in the serial port application. Timer 0 is configured to generate an interrupt after ever 50 usec, and the interrupt service routine copies data from the A/D converter to a buffer. In this application, two 512 byte buffers are used. While, the first buffer is writing data to the W5100, the second buffer is being filled with data from the A/D converters. When the second buffer is full, a flag is raised, and the data is copied to the W5100, while the first buffer is filled with A/D data.
The data acquisition interrupt service routine is shown below:

// ADC conversion complete ISR
ISR(ADC_vect)
{
 // Clear timer compare match flag
 TIFR0=(1<<OCF0A);
 // Toggle pin PD2 to track the end of ADC conversion
 PIND = (1<<PD2);
 
 *buffptr++ = ADCH;
 buffindex++;
 
 // When a buffer is full, switch buffers.
 if (buffindex >= BUFF_SIZE)
 {
 if (buffsel == 0)
 {
 buffsel = 1;
 buffptr = buff1;
 }
 else
 {
 buffsel = 0;
 buffptr = buff0;
 }
 buffindex = 0;
 flag = 1;
 }
}

The main() function initializes the various peripherals, then enters the data transfer loop which consists of a state machine which listens for connections from remote clients. The state machine monitors the status of a socket in the W5100’s internal status registers as detailed in the data sheet. When the socket is opened, it spends its time listening for new connections. When a new connection is established, data is transferred to the W5100 using the double buffered scheme outlined above:
The data transfer loop is shown below:

while (1)
{
 sockstat = SPI_Read(WIZNET_READ_OPCODE,S0_SR);
 switch(sockstat)
 {
 case SOCK_CLOSED:
 // Setup socket 0 for TCP, port 4096
 if (socket(sockreg,MR_TCP,4096) > 0) 
 {
 // Listen to Socket 0
 if (listen(sockreg) <= 0)
 delay_ms(1);
 #if _DEBUG_MODE
 printf("Socket Listen!\r\n");
 #endif
 }
 break;
 
 case SOCK_ESTABLISHED:
 if (flag)
 {
 // Write stored ADC values to the socket
 // Point to buffer not currently being filled
 if (buffsel == 0) send(sockreg,buff1,BUFF_SIZE);
 else send(sockreg,buff0,BUFF_SIZE);
 flag=0;
 }
 break;

 case SOCK_FIN_WAIT:
 case SOCK_CLOSING:
 case SOCK_TIME_WAIT:
 case SOCK_CLOSE_WAIT:
 case SOCK_LAST_ACK:
 // Force to close the socket
 close(sockreg);
 #if _DEBUG_MODE
 printf("Socket Close!\n");
 #endif 
 break;
 }
}

The complete Atmel Studio project and source code files can be found here: audioserv.zip

3. The Python Script
Thew Python script ethScope.py is also similar to the serial port version (Insert Link). The main difference being that this ethernet version has to establish a TCP connection with the Arduino server when the Run button is pressed, and the connection has to be closed when the Stop button is pressed.
The modified start_stop() function is shown below:


# Run/Stop acquisition button
    def start_stop(self):
        if self.runBtn['text'] == 'Run':
            # Clear out plot and audio queues
            while not self.audio_q.empty(): temp = self.audio_q.get()
            while not self.plot_q.empty(): temp = self.plot_q.get()            
            if self.audio_stat:
                self.playStream.start_stream()
            # Enable the data acquisition thread    
            data_aq.open_connection()   # Connect to the network
            data_aq.paused = False      # Enable the data acquisition thread
            self.runBtn['text'] = 'Stop'            
        elif self.runBtn['text'] == 'Stop':
            if self.audio_stat:
                self.playStream.stop_stream()
            # Suspend the data acquisition thread
            data_aq.pause()
            data_aq.disconnect = True   # Terminate the network connection
            self.runBtn['text'] = 'Run'

As shown, when Run is pressed, the function clears the queues, starts the audio stream if necessary, establishes the network connection, and starts the data acquisition thread. When Stop is pressed, the function stops the audio stream if necessary, suspends the data acquisition thread, and terminates the network connection.

The open_connection() function is shown below:


def open_connection(self):
        self.sock.settimeout(5)
        # Check for the Arduino audio server
        self.con_err = False
        try:
            self.sock.connect((self.host, self.port))
        except:
            self.con_err = True
            box.showerror('Connection Error',"Can't find Arduino audio server")

        if not self.con_err:
            Gui.plotter.title('Data Acquisition Plotter  Arduino: ' + self.host +':'+ str(self.port))
        else:
            Gui.plotter.title('Data Acquisition Plotter  Arduino: ethernet connection not found')

As shown, function looks for the Arduino server at IP address 192.168.2.10 at port 4096. If no connection is found, the con_err flag is raised, an error message is generated, and the title displays the connection status. Otherwise the script displays the address and port number in the title.
The data acquisition thread continuously checks the thread and network connection status, then reads 512 block bytes from the network and combines them, four at a time, into a 2048 array for the GUI and audio playback queues. If the Stop button has been pressed, the network connection is terminated by closing the socket, then creating a socket for the next connection.
The data acquisition function is shown below, and the complete script can be found here: ethScope.zip


def read_data(self):
        # Keep looping infinitely until the thread is stopped
        while self.stopped == False:
            # Get data from the ethernet socket and add it to the buffer
            if self.paused == False or self.disconnect == True:
                # Close the network connection
                if self.disconnect:
                    self.sock.close()
                    # Create a new socket for the next connection
                    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                    self.disconnect = False
                if self.paused == False:
                    if self.con_err == False:
                        rec_bytes = b''
                        while len(rec_bytes) < self.buff_size and self.con_err == False: 
                            try:
                                rec_bytes += self.sock.recv(512)
                            except:
                                self.con_err = True                               
                                box.showerror('Connection Error',"Can't find Arduino audio server")
                                Gui.plotter.title('Data Acquisition Plotter: Arduino ethernet connection not found')
                                Gui.start_stop()    
        
                        if self.con_err == False:
                            self.plot_q.put(rec_bytes)
                            # Store data in the audio monitor queue
                            if self.audio_stat:
                                audio_bytes = rec_bytes
                                self.audio_q.put(audio_bytes)
                    else:
                        # Generate simulated data
                        rand_bytes = []
                        for i in range(0, self.buff_size):
                            self.rand_bytes[i] = random.randint(0,255)
                        self.plot_q.put(self.rand_bytes)
                        # Wait so the thread doesn't monopolize the program 
                        time.sleep(0.1)

4. Conclusions
Getting a basic data acquisition/streaming network application is a good beginning for remote instrumentation and monitoring systems. In addition to custom network scripts and programs, it should be possible to stream data directly to a browser (with or without special plug-ins).

If youtube can do it so can I.

1. The Arduino IDE Version
One of the disadvantages of writing programs in traditional embedded C is that its difficult to use some of the Arduino C++ libraries such as, in this case, the Ethernet library. This gave me an incentive to try and port the ‘C’ version of my audio streaming server to the Arduino IDE, and adapt the Python script accordingly. The source code for the Arduino sketch and Python script can be found hereĀ  Arduino IDE Code.

1. The Arduino Program
As shown in the sketch, the Arduino code follows the structure of the ‘C’ version pretty closely. Since the Arduino uses Timer 0 for timekeeping, I’ve used Timer 1 to drive the A/D converter. Timer 1 is a 16 bit counter/timer with just enough differences from Timer 0 to make life interesting.

As shown, the first few lines are pretty straightforward. The Ethernet data is set up with at MAC address, IP address, a server at port 4096, and a potential cilent. The data buffers are set up as shown, and I’ve reduced the size from 512 to 256 bytes to save internal RAM space.

#include <Ethernet.h>

// Enter a MAC address and IP address for your controller below.
// The IP address will be dependent on your local network:
byte mac[] = 
{
 0x90, 0xA2, 0xDA, 0x0D, 0x8B, 0x3E
};
IPAddress ip(192, 168, 2, 10);

// Initialize the Ethernet server library
// with the IP address and port you want to use
EthernetServer server(4096);
EthernetClient client;

#define BUFF_SIZE 256

byte buff0[BUFF_SIZE];
byte buff1[BUFF_SIZE];
byte * buffptr = buff0;
int buffindex = 0;
int buffsel = 0;
int flag = 0;

1.1 Timer1 Set-up
The most significant change in the code is the Timer 1 initialization. As in the ‘C’ version, a 20 KHz clock is used to re-trigger the A/D converter after a conversion has completed. To implement this, the Clear Timer on Compare match (CTC) mode is used. In this mode the counter counts to a certain value and toggles the bit which starts the A/D conversion. In addition to being a 16 bit timer, Timer 1 differs from Timer 0 in another respect. There are two Output Compare Registers (OCR1A and OCR1B) which toggle bits OC1A and OC1B respectively. The difficulty is that the A/D converter can only be triggered by OC1B on Timer1. In CTC mode, the Timer/Counter register (TCNT1) only resets depending on the value of OCR1A. As a result, both registers have to be used. OCR1A to maintain the CTC mode, and OCR1B to initiate the A/D conversion.
This is shown in the following code snippet:

void InitTimer1(void)
{
 // Clear TCCR1A, TCCR11B, and TCCR1C from orginal Arduino defaults
 TCCR1A = 0;
 TCCR1B = 0;
 TCCR1C = 0;
 // Set Initial Timer value
 TCNT1H = 0;
 TCNT1L = 0;
 /*
 Place TOP timer value to Output compare register
 Note: In CTC mode TCNT1 is cleared after OCR1A times out.
 At the same time OCR1A toggles output compare bit
 OC1B which in turn triggers the A/D converter.
 Consequently both OCR1A and OCR1B are used at
 the same time.
 */
 OCR1AH = 0;
 OCR1AL = 99;
 OCR1BH = 0;
 OCR1BL = 99;
 // Set CTC mode
 TCCR1A |= (1<<COM1A0); // Set OC1A at PB1 to toggle
 TCCR1B |= (1<< WGM12);
}

The toggle frequency of OC1A and OC1B is given by the following formula:

f = fclk/(2*N*(1+OCR1A)

For the Arduino, fclk = 16MHz. N is the prescaler value which is selected as 8. A toggle rate of 20KHz is needed which means a square wave frequency of 10Kz, resulting in an OCR1a value of 99.

1.2 ADC Routines
The ADC initialization routine is the same as the ‘C’ version. The only difference is that OC1B is selected as the trigger source. Similarly, the ADC conversion complete interrupt service routine is the same as the ‘C’ version.

1.3 The Main Loop
The structure of the main loop is similar to the ‘C’ version, but since I’m using the Ethernet library, its somewhat simplified. The following code snippet, show the main loop:

void loop()
{
 // Listen for incoming clients
 client = server.available();
 if (client)
 {
   Serial.println("New Client");
   while (client.connected())
 { 
   // Clear the receive buffer 
   while (client.available())
   {
     char c = client.read();
     //Serial.write(c);
   }
 
   if (flag == 1)
   { 
     // Write stored ADC values to the ethernet connection
     // Point to buffer not currently being filled 
     if (buffsel == 0) server.write(buff1,BUFF_SIZE);
     else server.write(buff0,BUFF_SIZE);
     flag = 0;
    }
   }
 
   // Close the connection:
   client.stop(); 
   Serial.println("Client Disconnected"); 
 }
}

The loop starts by waiting for a remote client to make a connection. In the ‘C’ version, this was handled by socket functions. In the Ethernet library, a client must send data when initiating a connection. If the client disconnects and there is still data in the receive buffer, the Arduino will still maintain the connection, so the receive buffer must be cleared.
The loop then checks ‘flag’ to see if there is a data buffer available. If a buffer is available, ‘buffsel’ is used to selected the buffer not currently being filled by the ADC ISR. The selected buffer is then sent to the client via the server.write() function. The socket functions in the ‘C’ version timed out when the client connection was lost. The server.write() function doesn’t seem to have this capability, and will hang unless a proper socket disconnect command is sent from the client. This required a slight change to the Python GUI.

2. The Python GUI
The modified Python based GUI is very similar to the ‘C’ version except for some minor (but necessary) changes.

2.1 The Socket Connection
As mentioned, the Arduino Ethernet library requires data following a socket connection. This is shown in the following code snippet:

if not self.con_err:
 # Send some text to the Arduino to initiate a client connection
 self.sock.send('OK'.encode())
 Gui.plotter.title('Data Acquisition Plotter Arduino: ' + self.host +':'+ str(self.port))

The actual data sent doesn’t matter, since the Arduino program clears the receive buffer.

3.2 Graceful Exit
Since the Arduino server.write() function doesn’t detect a client connection loss, a means of disconnecting the program when exiting (if necessary) was added:

# Exit button
 def exit(self):
 # Add a more graceful way of exiting the application
 # Stop the data acquisition thread
 if self.runBtn['text'] == 'Stop':
 # Terminate the audio monitor stream
 if self.audio_stat:
 self.playStream.stop_stream()
 self.playStream.close()
 # Suspend the data acquisition thread
 data_aq.pause()
 data_aq.disconnect = True # Terminate the network connection 
 while data_aq_thread.is_alive(): pass
 self.pa.terminate()
 
 # Terminate the GUI
 self.plotter.destroy()

As shown, when the program is exiting, the status is checked and if necessary, the audio stream and socket connection are terminated.

3.3 Buffer Size
As mentioned in the Arduino code, the data buffers were changed from 512 to 256 bytes. This is reflected in the following line:

rec_bytes += self.sock.recv(256) # Change buffer size to 256