Detecting tones from remote control - Tomy Omnibot

Greetings all,

I think I am stuck a bit. Let me start with a fast introduction and then get into the requirements.

I am working on an Arduino project to replace the "brain" of the Tomy Omnibot remote control robot. I put brain in quotes because the Omnibot is not really a robot and is more of a remote control toy. It's "brain" works by receiving and decoding tones of different frequency from a radio remote and then setting the state of output pins based on the command.

The tones are shaped into nice TTL level square waves.
The tones can come from the radio or the cassette deck.
The tones are 1400 to 2800 HZ in 200hz increments (1400, 1600, 1800, etc...) and a single outlier at 4600Hz. 9 tones in total.
The tone input is pulled down, so the tones are positive pulse first.

While the tones are nice square waves, they are not precise and there is of course radio noise to deal with which is not filtered out.

Requirements:

  1. Maintain all original hardware and functions of the Omnibot with the exception of the CPU
  2. Allow expansion of the original functions (to include new sensors, etc...)
  3. Remote commands may come in at any time. I need to either poll this quickly enough in the main code to catch remote commands or develop a reliable interrupt strategy.
  4. Reliably detect the tones that may vary by plus/minus 25HZ and reject noise quickly.
  5. Most commands are momentary, one is latched (tape start/stop) and the other is one tone for press and one tone for release (walkie talkie)
  6. Driving commands will drive motors as long as joystick is held, but stop when joystick is released. So I will need to poll this quickly enough to keep it driving when the joystick is held (the tone will be continuous in that case.)

My focus at the moment is on number 4: reliably detect tones and reject noise. I have some mostly working code that is ugly in my opinion and I am certain is not the best way to do it.

What I am doing (just testbed at the moment) is to call a function every loop to parse the tones. I am using pulseIn(2, HIGH) and since the tones are 50% duty cycle, I just multiply this by 2 to get the period and then divide this by 1 second to get the frequency. But then I have a ton of IF statements to reject anything out of band, then a ton of IF statements to convert anything +/- 50HZ to the correct frequency. Then a series of case statements to create an integer representing the remote command. This seems to be working fairly reliably and pretty fast, but it is ugly and I would really like some advice on how to make the routine more efficient. In the end, I want the remote to be responsive, but I do not want it to be the center-piece of the robot. I want to be able to move this code into an interrupt somehow so the newly intelligent Omnibot can act likely a real robot when not being commanded from the remote. Baby steps....

Here is my remote decoder function (serial print statements will be removed):

int getCommand(){
	int command;
	long highTime, freq, period;
	
	highTime = pulseIn(2,HIGH);

	period = highTime * 2;
	freq=1000000/period;
  
  if((freq < 1350) || (freq > 4650)) {
        Serial.println("Invalid Command");
        Serial.println("Freq: " + String(freq));
        return 0;
  }
  if((freq > 2850) && (freq < 4550)) {
        Serial.println("Invalid Command");
        Serial.println("Freq: " + String(freq));
        return 0;
  }

  if( (freq > 1350) && (freq < 1450)) freq = 1400;
  if( (freq > 1550) && (freq < 1650)) freq = 1600;
  if( (freq > 1750) && (freq < 1850)) freq = 1800;
  if( (freq > 1950) && (freq < 2050)) freq = 2000;
  if( (freq > 2150) && (freq < 2250)) freq = 2200;
  if( (freq > 2350) && (freq < 2450)) freq = 2400;
  if( (freq > 2550) && (freq < 2650)) freq = 2600;
  if( (freq > 2750) && (freq < 2850)) freq = 2800;
  if( (freq > 4550) && (freq < 4650)) freq = 4600;

  Serial.println("High: " + String(highTime));
  Serial.println("Period: " + String(period));
  Serial.println("Freq: " + String(freq));
  
	switch (freq) {
		case 1400: //Talk ON
			command = 1;
      break;
		case 1600: //Forward
			command = 2;
      break;
		case 1800: //Right
			command = 3;
      break;
		case 2000: //Reverse
			command = 4;
      break;
		case 2200: //Left
			command = 5;
      break;
		case 2400:  //TAPE START/STOP (latched)
			command = 6;
      break;
		case 2600:  //Sound 1
			command = 7;
      break;
		case 2800:  //Sound 2
			command = 8;
      break;
		case 4600: //Talk OFF
			command = 9;
		break;
    default:
      command = 0;
      break;
	}

	return command;
}

I am intermediate level when it comes to programming. On the Arduino platform, I mainly only know how to work within the core libraries. I assume this would be so much more efficient to have running in a library under timer control, or maybe using the counter hardware instead of the way that I am doing it. But I am not certain how to accomplish either of those things on the Arduino.

So, 0-5V or +/-5V?

There's lots of projects already done that measure frequency. Google "Arduino Frequency Counter" to see how they do it.

Since you have a huge gap (200hz) between exact tones, you can accept 1300 to 1500 as if it is 1400 Hz. Noise shouldn't be a problem.

0-5V, yes.

Agreed about sources for frequency counting. Most are focused around using the analog inputs however. I am only aware of two methods (after searching) to count frequency. Either capture the period and divide by 1 second or count the number of pulses within a given amount of time.

As you can see in my code, I am using IF statements to try to deal with the ranges and noise. It is ugly, but I can't really think of another way. I was hoping someone might have a better idea.

After thinking a bit more about this, I was able to reduce the code down to this, which is working just as reliably, but I will need to play with the ranges a bit to get it more accurate and make sure there isn't any false triggering or overlaps.

I got rid of the frequency calculation, because I don't really care what the frequency actually is. I grab the high pulse width and use that instead. And the case statements were left over from before I was using the IF filtering, so I got rid of those and just set my command in the if statement (duh!) I also added a timeout to the pulseIn statement of 1ms since a valid pulse in my range will never take longer than that.

I still feel like there must be a much better way to do this, though. Please any thoughts or ideas would be appreciated.

int getCommand(){
	int command;
	long highTime;
	
	highTime = pulseIn(2,HIGH,1000);

  if( (highTime > 360) || (highTime < 100)) command = 0; //Invalid
   
  if( (highTime > 320) && (highTime < 360)) command = 1;
  if( (highTime > 300) && (highTime < 320)) command = 2;
  if( (highTime > 270) && (highTime < 280)) command = 3;
  if( (highTime > 260) && (highTime < 270)) command = 4;
  if( (highTime > 240) && (highTime < 260)) command = 5;
  if( (highTime > 200) && (highTime < 240)) command = 6;
  if( (highTime > 180) && (highTime < 200)) command = 7;
  if( (highTime > 170) && (highTime < 180)) command = 8;
  
  if( (highTime < 170) && (highTime > 120)) command = 0; //Invalid
  
  if( (highTime > 100) && (highTime < 120)) command = 9;

  Serial.println("High: " + String(highTime));

	return command;
}

I had never seen the pulseIn() function before. This could be handy in my future projects.... Thanks

The only quibble I have with your code is, what happens if highTime==320? (Unlikely from your description, but plan for it).

This should fix it.

if( (highTime >= 320) && (highTime < 360)) command = 1;
if( (highTime >= 300) && (highTime < 320)) command = 2;
etc.
  

I am relying a little bit on the fact that even a quick press of a remote button generates a dozen or so milliseconds of pulses, so if it happens to miss one or even a few, it will catch a decent enough shaped pulse to trigger on at some point during the press. That is the hope anyway.

My bigger worry is mis-triggering and counting a partial pulse which gets decoded as a different command. Commanding the bot to move forward and he starts turning to the right would be a problem. That is what I am trying to weed out the most. So, I would rather miss a few pulses than decode the wrong command. That is my thought process, anyway.

My understanding of the pulseIn function is that it will block running of the code while waiting for the pulse. And I think it will only work on the interrupt pins. If frequency is what you really want, the code would look like this:

highTime = pulseIn(2,HIGH,timeout);
lowTime = pulseIn(2,LOW, timeout);
period = highTime+lowTime;
freq = 1000000/period;

Times will be in microseconds.

That gives a fairly accurate measurement, at least at the low frequencies being used here. It is supposedly good as fast as 10 microseconds (which would be about 50Khz at 50% Duty cycle.)

So,
1400 = 7 x 200
.
.
2800 = 14 x 200
and 4600.

Maybe with cycle.

command = 0; // no command
for ( word n = 7; n <= 14; n ++ )
{
  if ( freq >= ( n * 200 - 25 ) and freq <= ( n * 200 + 25 ) ) command = n - 6;
}

if ( command == 0 and freq >= 4575 and freq <= 4625 ) command = 9;

Not tested!

1 Like

There are other approaches to consider. Tone detection can also be done by correlation, where you compare one tone sample, taken over a specific period of time, with another. Autocorrelation, if they are subsequent samples of the same tone. You can set a bandwidth.

Here is a short program that detects a single tone (or frequency multiples of it) using autocorrelation. The frequency detected depend on the sample rate you select. It uses the built in analog comparator on ATmega processors to allow tone detection from a sine wave or music analog signal.

// fast audio tone detector, 8 bit autocorrelation function.
// This program detects the presence of an audio tone of
// the specified frequency f0 (below)
// output: lights LED with tone present within detection bandwidth
//
// for Arduino ATmega variants
// uses analog comparator module
// pin D6 = capacitively coupled audio input, biased to Vcc/2
// pin D7 = reference voltage, Vcc/2

//samples input for two full successive periods of the target tone frequency and 
// compares the two samples for match.
//in theory, exact match will occur only if the sample frequency is an integer multiple 
// of the input frequency.


#define LED 13
float f0 = 1000.0; //target frequency in Hz
unsigned int p0 = 1.0E6 / (8.0 * f0); //sample delay in microseconds

union i2b {
  unsigned int i;
  unsigned char b[2];
} val;

void setup ()
{
  Serial.begin (9600);
  Serial.print("Sample delay us ");
  Serial.println(p0);
  delay(500);
  pinMode(LED, OUTPUT); //on board LED
  digitalWrite(LED, 0);

  ADCSRB = 0;           // (Disable) ACME: Analog Comparator Multiplexer Enable
  DIDR1 = 3;            //disable digital inputs on D6/D7 (comparator AIN0/AIN1)
  ACSR = (1 << ACI); //clear Analog Comparator interrupt flag, just in case
}  // end of setup

void loop () {
  static unsigned char i, result, sum;
  sum = 0;
  val.i = 0;
  for (i = 0; i < 16; i++) {
    if (ACSR & (1 << ACO)) val.i |= 1; //if input is HIGH, set bit in sample
    val.i <<= 1;
    delayMicroseconds(p0);
  } //loop i

  if ( (val.b[0] == 0) || (val.b[0] == 0xFF)) return; //no signal
  result = val.b[1] ^ val.b[0]; //XOR the two one-byte samples

  if (result == 0 ) digitalWrite(LED, 1); //match
    while (result) { //count set bits
      if (result & 1) sum++;
      result >>= 1;
    }
  if (sum < 2) digitalWrite(LED, 1); //allow 1 set bit (bandpass)
  else digitalWrite(LED, 0);
}  // end of loop

Another approach is to use a digital bandpass filter. Example below. Several of these could be run in parallel on fast Arduino.

/* Sketch implements single tone detection using a narrow bandpass filter
 *  Tone detection occurs when integrated power out of BPF is greater than
 *  one half the integrated power of the full ADC band
 */

// ***************************************************************************
/* Integrator class maintains running average of input using recursion formula
    xAvg = x + xAvg - xAvg/(2**N) where N is a positive integer
    Effective integration period is on the order of 2**N
*/
class Integrator {
  public:
    long int xAvg ;       // Running average left shifted N bits
    int shift ;

    Integrator(int N) {   // Constructor for class
      xAvg = 0 ;
      shift = N ;
    }

    // Update average with sample and return updated average
    long int integrate(long int xIn) {
      xAvg = xAvg + xIn - (xAvg >> shift) ;
      return (xAvg >> shift) ;
    }
} ;

// ***************************************************************************
/* Digital bandpass filter for sample rate 2500 Hz, passband 995-1005 Hz */
/* Using: https://www-users.cs.york.ac.uk/~fisher/mkfilter/trad.html */
/* Digital filter designed by mkfilter/mkshape/gencode   A.J. Fisher
   Command line: /www/usr/fisher/helpers/mkfilter -Bu -Bp -o 1 -a 3.9800000000e-01 4.0200000000e-01 -l */

#define NZEROS 2
#define NPOLES 2
#define GAIN   8.057026980e+01
static float xv[NZEROS + 1], yv[NPOLES + 1];
static int filterloop(int xIn) {
  xv[0] = xv[1]; xv[1] = xv[2] ;
  xv[2] = xIn * (1 / GAIN) ;
  yv[0] = yv[1]; yv[1] = yv[2] ;
  yv[2] =   (xv[2] - xv[0]) + ( -0.9751778762 * yv[0]) + ( -1.5980786463 * yv[1]);
  return int(yv[2]) ;
}

// ***************************************************************************
// Parameter and object definitions
#define sampleIntervalMicros 400 // Sample interval in microseconds
long int nextInterval ;         // Time of next ADC collection cycle
Integrator adcRunningMean(8) ;  // Integrator object to track ADC offset
Integrator channelPower(6) ;    // Integrator object to track full band channel power
Integrator tonePower(6) ;       // Integrator object to track filter band power

void setup() {
  digitalWrite(LED_BUILTIN, LOW) ;
  pinMode(LED_BUILTIN, OUTPUT) ;
  Serial.begin(115200) ;
  nextInterval = micros() + sampleIntervalMicros ;
}

void loop() {
  if (micros() >= nextInterval) {
    int x = analogRead(A0) ;
    long int adcMean = adcRunningMean.integrate(x) ;
    long int y = filterloop(x) ;

    long int chanPower = channelPower.integrate(sq((x - adcMean))) ;
    long int sigPower = tonePower.integrate(sq(y)) ;
    nextInterval += sampleIntervalMicros ;

    if (2 * sigPower > chanPower) {
      digitalWrite(LED_BUILTIN, HIGH) ;
    } else {
      digitalWrite(LED_BUILTIN, LOW) ;
    }
  }
}
1 Like

Thank you for the responses! I will experiment with all of the suggestions.