featured

Using Ultrasonic PWM as Volume Control

Here’s a video of my Volume Library for Arduino producing 256 volume levels on a digital-only pin using ONLY a speaker:

How is this done using only 0 & 5 volts and no filtering components? We’ll get to that in a moment – for now let’s talk about the normal way this is done. The traditional way to convert a digital signal into an analog voltage is by using an RC low-pass filter like this:

RC-low-pass-filter-example

The science and physics behind this aside, the basic reason this circuit works is because of the capacitor’s resistance to low-frequency changes. Any high frequencies are effectively absorbed into the capacitor, allowing lower frequencies to pass through – a Low-Pass-Filter! The “RC” above describes the circuit: a Resistor and Capacitor. Let’s take a look at how the input signal looks: (Shown in pink)

2000px-Delta_PWM.svg

The RC filter will convert a PWM signal into the average of it’s HIGH and LOW periods. For example, a 5 volt PWM signal at 50% duty cycle (HIGH half the time, LOW half the time) will be averaged out to 2.5 volts by the RC filter. (A signal at 75% duty cycle will be 3.75 volts. Getting it now?) By changing amount of time the filter’s input is HIGH in each cycle, we change the averaged voltage seen at the output. (Pictured above in blue)

As nice and low-cost these filter components are to buy, they have a few requirements:

  1. Specific values of resistors and capacitors based on the PWM frequency
  2. Space on the finished PCB or breadboard
  3. Extra cost in pick-and-place assembly due to tooling of extra reels and a higher placement count on the PCB.

So knowing that, how to we get rid of it? We cheat the physics of the speaker!

Remember how the capacitor in the filter was resistant to low frequency changes? A speaker is resistant to high frequency changes above it’s sound production range! Most full-range speakers in the world can only reproduce sound in the 5-20,000 Hz range, which is about the limits of our human ears. So if we were to feed a speaker a 5V square wave running at something like 62,500 Hz, it wouldn’t be able oscillate fast enough to produce it and instead would react as if it was fed 2.5V. Now we can ditch the RC filter and let the speaker do the work for us using ultrasonic-frequency PWM!

Before I go any further, let me give a disclaimer:

  • This ONLY works because of the speaker. If you recorded the output straight off of the Arduino, it would remain unfiltered. If you want the filtering to happen without a speaker, you’ll have to stick to traditional methods.
  • If you don’t know how to use Timer Interrupts on an Arduino/AVR, I suggest trying this tutorial first to get an idea.
  • This code is only designed for ATmega328-based projects like the Arduino Uno, and only works on pins 5 & 6. (Learn why below)

With that disclaimer out of the way, let’s get into it!

PROGRAMMING TIME!

Our goal today is to produce at 440 Hz square wave (A4 on a piano) at various volumes using and ATmega328/Arduino Uno and a speaker. This technique will work on other AVR microcontrollers, but the pinouts and code will differ. Let’s start with the traditional Arduino tone() function so we know what 440 Hz sounds like:

byte speaker = 5;

void setup(){
  pinMode(speaker,OUTPUT);
}

void loop(){
  tone(speaker,440);
  delay(500);
  noTone(speaker);
  delay(500);
}

This will produce 440 Hz on Pin 5 once per second. When tone() is used, the Arduino/AVR uses hardware Timer 2 to flip the state of the output pin 880 times per second. The timer interrupt is called at twice the input frequency to account for both rising and falling edges of the wave.

Here is how we produce that same tone at 25% the original volume using our physics trick:

byte speaker = 5;
byte volume = 63;

byte state = 0;
bool enabled = false;

void setup() {
  pinMode(speaker, OUTPUT);

  cli();
  TCCR1A = 0;
  TCCR1B = 0;
  TCNT1 = 0;
  OCR1A = 18180;
  TCCR1B |= (1 << WGM12);
  TCCR1B |= (0 << CS12) | (0 << CS11) | (1 << CS10);
  TIMSK1 |= (1 << OCIE1A);
  sei();

  TCCR0B = (TCCR0B & 0b11111000) | 0x01;
}

void loop() {
  toneOn();
  delay(32000);
  toneOff();
  delay(32000);
}

void toneOn(){
  enabled = true;
}

void toneOff(){
  enabled = false;
}

ISR(TIMER1_COMPA_vect) {
  if (enabled == true) {
    state = !state;
    if (state == 1) {
      analogWrite(speaker, volume);
    }
    else if (state == 0) {
      analogWrite(speaker, 0);
    }
  }
}

There’s a lot of strange looking code here, but don’t worry – I’ll break it down piece-by-piece.

IMPORTANT VARIABLES:

  • byte volume is an 8-bit (0-255) value that corresponds to the perceived output volume. (0 = 0% volume, 255 = 100% volume)
  • byte state stores the HIGH/LOW state of the waveform as 0 or 1. Can also be boolean.
  • bool enabled stores whether or not we’re currently playing a tone.
  • OCR1A is a register for hardware Timer 1 that holds the compare/match value for TCNT1, which is incremented once per CPU cycle. When TCNT1 == the OCR1A value, we call the Interrupt Service Routine at the bottom of the code.

SETUP LOOP:

cli() disables all timer interrupts, which is a good practice when you’re about to redefine how they function.

Timer interrupt setup:

TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 18180;
TCCR1B |= (1 << WGM12);
TCCR1B |= (0 << CS12) | (0 << CS11) | (1 << CS10);
TIMSK1 |= (1 << OCIE1A);

This is the process where hardware Timer 1 is initialized to stop all current execution and jump to the ISR function at the bottom of the code at 880Hz or every 1,136 microseconds. These values were calculated using the AWESOME tool found here which will define them all for you based on what interrupt frequency or timer you’d like to use. Put simply, TCNT1 is incremented once per CPU cycle. When it counts up to equal OCR1A, the interrupt fires and TCNT1 resets to 0. Here’s the math to calculate what OCR1A should be for your desired tone frequency of 440 Hz:

OCR1A = CPU_Hz / (frequency*2) -1;

So to fire the interrupt at 880 Hz (to produce a 440 Hz tone) on a 16 MHz Arduino, our equation looks like this:

OCR1A = 16000000/ (440*2) -1;

(The -1 at the end is because TCNT1 starts and resets to 0 when execution starts or an interrupt completes.)

The result is a value of 18180, which means the interrupt will fire every 18,180 CPU cycles. (880 Hz on a 16 MHz chip)

TCCR0B:

TCCR0B = (TCCR0B & 0b11111000) | 0x01;

We’re also using hardware Timer 0 in this case, because it’s capable of producing PWM at the 62,500 Hz mentioned earlier. By default it runs at only 976 Hz, but this line changes it’s prescaler from 64 to 1 for a 64x speedup! Timer 0’s PWM is tied to pins 5 & 6 on the ATmega328/Uno, so this trick only works on those two pins!

sei() re-enables timer interrupts, now that we’ve set them up properly.

INTERRUPT ROUTINE:

ISR(TIMER1_COMPA_vect) {
  if (enabled == true) {
    state = !state;
    if (state == 1) {
      analogWrite(speaker, volume);
    }
    else if (state == 0) {
      analogWrite(speaker, 0);
    }
  }
}

This is the code that’s executed at 880 Hz to flip the output state of our speaker. Normally we’d just flip the output between HIGH and LOW, but because we now have ultrasonic PWM, we flip between a custom duty cycle (set in the volume variable) and a 0% duty cycle (LOW) at 880 Hz to produce a 440 Hz tone at a custom volume! This flip only happens if enabled == true. We can turn tone production on and off using this variable.

But what’s with that weird-ass 32 second delay???

The delay time between the toneOn() and toneOff() functions is set to 32,000 milliseconds, but in reality it will only delay for 500ms. This is because of the TCCR0B changes made in setup(). Timer 0 is used to produce our ultrasonic PWM, but it’s also used in Arduino’s timekeeping functions like delay() or millis(). By setting it to run 64 times faster than normal, it now perceives time at 64x speed! Whoops. Unfortunately that’s the cost of business. My Volume library mentioned at the top of this article has it’s own timekeeping functions that compensate for this issue.

Time To Experiment!

Go ahead and upload the new code above to your Arduino, and it will produce the 440 Hz tone at 25% the normal volume! Play around with the volume variable for different levels, and be sure to try my Volume Library to make things as simple as this:

#include "Volume.h"

byte volume = 63;

void setup() {
  vol.begin();
}

void loop(){
  vol.tone(440,volume);
  vol.delay(500);
  vol.noTone();
  vol.delay(500);
}

Keep hacking!
– Connor

 

Read the latest car news and check out newest photos, articles, and more from the Car and Driver Blog.

One comment

Leave a Reply

Your email address will not be published. Required fields are marked *