Sunday, January 18, 2015

Audible Frequency Chirp Sonar with the Stellaris Launchpad






Over the last year I've been working towards an underwater sonar system for ROVs and surface boats. In order to learn the basic signal processing required to detect the echoes, I initially got a simple sonar working in air with a desktop conferencing USB speaker/mic running on Windows. A writeup, including source, is here. That article describes the algorithms used in detail and would be a good read if you want the details of how this works.

The next logical step seemed to be to get it working on a microcontroller. There are plenty of low cost ultrasonic sonar modules available that work really well in air, but the idea was to work towards getting a sonar that worked in water. There are currently no low cost sonar modules for hobby use in water. Additionally, the low cost modules only give one echo - with a signal processing approach like this, you get a series of echoes that may convey more information about the environment. As an example, a boat floating above a school of fish could detect both the fish and the bottom.

I selected a Stellaris Launchpad because of the high speed analog to digital converters (ADC) and the 32 Kof RAM. At the required sample rates, the Launchpad has just enough RAM to send a chirp, and then record a fraction of a second of audio so that the echoes can be determined. Higher frequency sound will require a higher sampling rate, so I may need to switch to a Teensy 3.1, which has 64K of RAM.

A chirp waveform is computed and sent to a small piezo speaker driven by a simple transistor circuit. The piezo supply voltage (VCC in the diagram below) is provided by 3 nine-volt batteries in series to obtain 27V. This diagram shows how it is connected. This is not my diagram - I found it online, but I don't have a reference. If this is yours, please drop me a line.



The return echo is detected by a small amplified microphone from Adafruit. I like this module because it has an integrated level shift. Rather that swinging from -V to +V, it is shifted to 0 to +3.3V so that it can be connected to an ADC. It's very convenient.

A couple 3D printed parts hold it all to the board just to keep it pointed in the right direction.




The chirp is sent, and the audio immediately starts recording to catch the echo. The same correlation function as used in the previous article is used to pull the echoes out of the recorded audio. The intensities of the correlation function are sent through the debug port to the PC so that it can be plotted. 

I need to work on optimizing the echo detection code - currently it works on the audio from each pulse for 4 seconds or so. Also, the power output of the audio transducer is very low, so range is pretty limited. It has an effective range of between 3-9 feet. Closer than 3 feet, the echo is hard to pick out of the noise produced when the pulse is sent.

As in the original experiments with the speaker/mic, the results are plotted with a simple Python program set up similarly to a fishfinder display. The results of a test run are shown below. Source for the Python display is modified from code from the previous article. 





Source code for the Launchpad is given below.

Next steps are to work on getting transducers working under water and increasing transmit power. I've made a simple hydrophone to test transducers with - update coming soon.




Audible Frequency Chirp Sonar with the Stellaris Launchpad from Jason Bowling on Vimeo.


#include "inc/hw_ints.h"
#include "inc/hw_memmap.h"
#include "inc/hw_types.h"
#include "driverlib/sysctl.h"
#include "driverlib/interrupt.h"
#include "driverlib/gpio.h"
#include "driverlib/timer.h"
#include "driverlib/debug.h"
#include "driverlib/fpu.h"
#include "driverlib/pin_map.h"
#include "driverlib/rom.h"
#include "utils/uartstdio.h"
#include "driverlib/adc.h"
#include "inc/hw_timer.h"
#include "inc/hw_ints.h"

#define numSamples 6000 //size of receive buffer
#define sampleRate 80000 //sample rate at which the audio for sending and receiving is performed
#define pulseLength .0015  //transmitted pulse duration in seconds

#define chirpStartFreq 5000  //in Hz
#define chirpEndFreq 8000  //in Hz

int chirpLength = 0;

 unsigned long g_sampleCounter = 0;
 unsigned long ulADC0_Value[1];
 unsigned long rxBuffer[numSamples];
 int pulse[900]; //stores waveform for sending and comparison. Only need integers for square wave. Could do with bits to save memory
// must be at least pulseLength * sampleRate

 //double output[501];

void initConsole()
{
  // Initialize the UART at 115200.
     //
     ROM_SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOA);
     ROM_GPIOPinConfigure(GPIO_PA0_U0RX);
     ROM_GPIOPinConfigure(GPIO_PA1_U0TX);
     ROM_GPIOPinTypeUART(GPIO_PORTA_BASE, GPIO_PIN_0 | GPIO_PIN_1);
     //ROM_GPIOPinTypeUART(9600, GPIO_PIN_0 | GPIO_PIN_1);
     UARTStdioInit(0);
     UARTprintf("\nConsole Initialized. System clock is %4d\n", SysCtlClockGet());

}

void initADC()
{
   // The ADC0 peripheral must be enabled for use.
         //
         SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0);

         // For this example ADC0 is used with AIN0 on port E7.

         SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE);

         // Select the analog ADC function for these pins.

         GPIOPinTypeADC(GPIO_PORTE_BASE, GPIO_PIN_3);

         //
         // Enable sample sequence 3 with a processor signal trigger.  Sequence 3
         // will do a single sample when the processor sends a signal to start the
         // conversion.
         //
         ADCSequenceConfigure(ADC0_BASE, 3, ADC_TRIGGER_PROCESSOR, 0);

         //
         // Configure step 0 on sequence 3.  Sample channel 0 (ADC_CTL_CH0) in
         // single-ended mode (default) and configure the interrupt flag
         // (ADC_CTL_IE) to be set when the sample is done.  Tell the ADC logic
         // that this is the last conversion on sequence 3 (ADC_CTL_END).  Sequence
         // 3 has only one programmable step.

         ADCSequenceStepConfigure(ADC0_BASE, 3, 0, ADC_CTL_CH0 | ADC_CTL_IE |
                                  ADC_CTL_END);

         //
         // Since sample sequence 3 is now configured, it must be enabled.
         //
         ADCSequenceEnable(ADC0_BASE, 3);

         //
         // Clear the interrupt status flag.  This is done to make sure the
         // interrupt flag is cleared before we sample.
         //
         ADCIntClear(ADC0_BASE, 3);

}

initTimer()
{
 //configure 32 bit periodic timer
  SysCtlPeripheralEnable(SYSCTL_PERIPH_TIMER0);
  TimerConfigure(TIMER0_BASE, TIMER_CFG_32_BIT_PER);
}

void startTimer()
{
 unsigned long ulPeriod;

 //set timer rate
   ulPeriod = (SysCtlClockGet()/(sampleRate*3));
   TimerLoadSet(TIMER0_BASE, TIMER_A, ulPeriod -1);
   IntMasterEnable();

   IntEnable(INT_TIMER0A);
   TimerIntEnable(TIMER0_BASE, TIMER_TIMA_TIMEOUT);
   TimerEnable(TIMER0_BASE, TIMER_A);
}

void stopTimer()
{

         // Disable the Timer0A interrupt.
         //
         IntDisable(INT_TIMER0A);

         //
         // Turn off Timer0A interrupt.
         //
         TimerIntDisable(TIMER0_BASE, TIMER_TIMA_TIMEOUT);

         //
         // Clear any pending interrupt flag.
         //
         TimerIntClear(TIMER0_BASE, TIMER_TIMA_TIMEOUT);
}

void initLED()
{
 //enable GPIO pins for LED
  SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOF);
  GPIOPinTypeGPIOOutput(GPIO_PORTF_BASE, GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3);
}

void initPiezo()
{
 //enable GPIO pins for piezo
  SysCtlPeripheralEnable(SYSCTL_PERIPH_GPIOE);
  GPIOPinTypeGPIOOutput(GPIO_PORTE_BASE, GPIO_PIN_4|GPIO_PIN_5);
}

void generateChirpWaveform()
{
unsigned long int freq = chirpStartFreq;
int value = 1;
unsigned long int start = 0;
unsigned long int stop = 0;
unsigned long int counter = 0;
int count = 0; //temp
int sampleComplete = 0;

//step through array from 0 to 1/2*freq, setting value. Invert value. Proceed to 1/2*freq, setting value. Calculate new freq. Repeat.
//values stored in pulse[]

while (!sampleComplete)

{
stop = (int) start + ((1.00 / (freq * 2.00)) * sampleRate);

for (counter = start; counter < stop; counter ++)
 {//check position and set sampleComplete when at end of chirp
 if (counter < pulseLength * sampleRate)
  pulse[counter] = value;
 else
  sampleComplete = 1;
 }
//invert waveform value to be set for next half of cycle
if (value == 1)
 value = 0;
else
 value = 1;

//calculate new freq based on position in pulse. Ratio of stop/chirpLength vs freq increment / chirpEndFreq
freq = chirpStartFreq + (((chirpEndFreq- chirpStartFreq) * stop)/(pulseLength * sampleRate));

//position for writing next half cycle
start = stop;
chirpLength += 1;
} //end while

}

void playChirp()
{
long int count = 0;
long int endSample;

endSample = sampleRate * pulseLength;

 while (count < endSample)
   {
  if (pulse[count])
    {
   GPIOPinWrite(GPIO_PORTE_BASE, GPIO_PIN_4, GPIO_PIN_4);
   GPIOPinWrite(GPIO_PORTE_BASE, GPIO_PIN_5, 0);
    }
  else
    {
   GPIOPinWrite(GPIO_PORTE_BASE, GPIO_PIN_4, 0);
   GPIOPinWrite(GPIO_PORTE_BASE, GPIO_PIN_5, GPIO_PIN_5);
    }

  SysCtlDelay((SysCtlClockGet() / (sampleRate * 3)));
  count ++;
   }

}


void ftoa(float f,char *buf)
{
 //code from http://forum.stellarisiti.com/topic/528-newbie-question-how-do-i-use-uartprintf-with-floats/
 int pos=0,ix,dp,num;
    if (f<0 data-blogger-escaped-buf="" data-blogger-escaped-dp="0;" data-blogger-escaped-f="" data-blogger-escaped-pos="" data-blogger-escaped-while="">=10.0)
    {
        f=f/10.0;
        dp++;
    }
    for (ix=1;ix<8 data-blogger-escaped-f="f-num;" data-blogger-escaped-if="" data-blogger-escaped-ix="" data-blogger-escaped-num="">9)
                buf[pos++]='#';
            else
                buf[pos++]='0'+num;
            if (dp==0) buf[pos++]='.';
            f=f*10.0;
            dp--;
    }
buf[pos - 1] = '\0';
}

void processSample()
{
int a = 0;
int bufferStartPosition = 0;


double normalizedSample = 0.0;
double windowSum = 0.00; //cumulative sum for this window
double temp = 0.0;


char buffer[20] , *str;
str = buffer;

//audio values in rxBuffer are shifted integers. Normalized audio is -1 to 1. Recorded samples are 0 to 4096
//Divide by 4096 and subtract .5 to shift to this range.

//stored pulse is stored 0 to 1. Multiply by 2 and subtract 1 to normalize.

while (bufferStartPosition < ( numSamples - chirpLength))
{
for (a = 0; a < chirpLength; a++)
 {

 normalizedSample = (rxBuffer[bufferStartPosition + a]/4096.0) - .5;
 //temp = normalizedSample * ((pulse[a] * 1.00));
 temp = normalizedSample * ((pulse[a] * 2.00) - 1.00);
 windowSum = windowSum + (normalizedSample * temp);
 }


ftoa(windowSum,str);
UARTprintf(str);
UARTprintf("\n");


bufferStartPosition += 1; //increment bufferStartPosition to move window
windowSum = 0.00;
//end outer loop
}

UARTprintf("stop\n");
}

int main(void)
{
SysCtlClockSet(SYSCTL_SYSDIV_5|SYSCTL_USE_PLL|SYSCTL_XTAL_16MHZ|SYSCTL_OSC_MAIN);
 initConsole();
 initLED();
 initPiezo();
 initADC();
 initTimer();
 generateChirpWaveform();
//initialization complete




 int pingCount = 0;

 while(pingCount < 1000)
   {
  playChirp();
  //record audio
  startTimer();
  processSample();
  pingCount = pingCount + 1;
   }

 while (1) {}

}


void Timer0IntHandler(void)
{
 // Clear the timer interrupt
 TimerIntClear(TIMER0_BASE, TIMER_TIMA_TIMEOUT);


 // Trigger the ADC conversion, Wait for conversion to be completed.
             //
 ADCProcessorTrigger(ADC0_BASE, 3);

 //everything after this can be moved out of the ISR
 //set a flag and poll for it in main()
 while(!ADCIntStatus(ADC0_BASE, 3, false))
       {}

 //take an ADC reading
 ADCIntClear(ADC0_BASE, 3);
 ADCSequenceDataGet(ADC0_BASE, 3, ulADC0_Value);

 if (g_sampleCounter < numSamples)
   {
  rxBuffer[g_sampleCounter] = ulADC0_Value[0];
  GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_2, GPIO_PIN_2);
   }
 else
  {
  GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_2, 0);
  g_sampleCounter = 0;
  stopTimer();
  }

g_sampleCounter++;

}



1 comment:

  1. Great article
    اجهزة سونار
    http://www.bestmedicaldevices.tk/

    ReplyDelete