Last week we announced the SPLear™, a module that integrates a PDM microphone along with a small ARM CPU to act as a sensor that measures Sound Pressure Level or SPL. Today I want to show how the basic firmware can be extended to also drive a hobby servo motor with a position proportional to the measured SPL. This is part of our series of articles on the general subject of audio signal processing from air to information.
See the announcement for a more complete description of the board. The key components are the bottom port microphone and the NXP LPC812 ARM CPU. The basic firmware implements an analog output driven by a PWM, an LED controlled by PWM to flicker when the room is louder than about 30 dB, along with a logic level UART, and access to most pins of the mic and the CPU.
Much of the firmware has been discussed and published in past posts on this blog, and the source code is available from a public repository. The firmware discussed here and running the demo seen in the video is checked in as [3b4f54236e].
Here it is waving its white plastic arm when honked at. Watch the servo, volt meter, and red LED seen reflected from the underside of the board:
The rest of this post will describe what we changed in the basic firmware to implement control of the servo.
Hobby Servos
Hobby servos are an inexpensive way to make something move without investing in a lot of elaborate motor control electronics or mechanics. The particular servo we used here only cost a few dollars, has just enough torque to move small objects, and operates just fine from the power available from a single USB port. This makes it particularly convenient for this sort of demonstration.
These servos are widely used in radio controlled model aircraft, cars, and similar toys, and are available in a wide range of sizes allowing operation of large objects under heavy loads (such as model sailboat winches and larger model airplane control surfaces) when enough power is available, as well as really tiny servos used for small models. Generally, they require a power supply which may be at 6 to 12 volts for larger motors, along with a TTL logic level control signal. When the Tower Pro SG90 Micro Servo is run from 5V, it is happy with a 3.3V logic level driving its control input.
The control is straightforward. The signal consists of a periodic pulse, where the width of the pulse corresponds to the position of the output shaft. The shaft is nominally centered when fed a 1.5 ms pulse. A working range of 1 ms to 2 ms is safe for nearly all makes and models, and a range from 0.5 ms to 2.5 ms is commonly allowed. Mechanical constraints prevent the shaft from turning through more than about 270 degrees, and 180 degrees is the usual working range. The SG90 appears to turn through 90 degrees over the range from 1 ms to 2 ms.
Breadboard
Adding hobby servo control involves one key decision: the specific GPIO pin to use to drive the servo. I’ve chosen the pin assigned to P11
, which is found on J3.3 of the SPLear board. The other reasonable alternative would be P13
, found on J1.4 and more easily available to a breadboard.
I picked P11
because it is a 5V tolerant open collector pin of the CPU, and so can be pulled up with a moderate value resistor to the same 5V rail driving the servo. P13
is a 3.3V logic output, and would actually drive the SG90 motor just fine but might have trouble talking with standard sized servos that really expect 5V logic and 6V or better power. With the SPLear plugged in to a breadboard, I had to add a flying wire from J3.3 to the breadboard, and add the needed pullup resistor. I used a 10K 5% resistor I had laying around, but any value above about 1K would do the job.
I mounted an SPLear on a solderless breadboard with pins soldered in to all 10 positions of J1 on the top face of the board. While that has the slight disadvantage of facing the LED at the table, it does face the microphone’s input port up at the room.
I wired up a logic level serial cable to the UART pins, and added a switch to the ISP entry pin so that I can conveniently reflash the firmware as well as monitor the output level on a terminal.
For the video, I also wired the analog SPL output signal to a classic analog meter movement so that movement of the meter can be compared to movement of the servo.
Generating the Servo Pulse
Near the top of pdmspi.c
I declare the pin, using one of the constants defined in splear.h
. The rest of the code introduced to this file will be guarded by the presence of the definition of PULSEPIN
so that the extra code can be removed from a production build by simply commenting out the definition of PULSEPIN
.
/* * Make variable width pulses on P11 which is open collector 5V tolerant. * Wire to a pull-up to 5V along with the servo motor. */ #define PULSEPIN HW_P11
I also define a global variable to hold the remaining width of any pulse that might be in progress, guarded by the presence of the definition of PULSEPIN
so that the extra code can be removed from a production build by simply commenting out the definition of PULSEPIN
.
#ifdef PULSEPIN uint16_t pulsewidth = 0; #endif
I extended the pin initialization to map PULSEPIN
to the GPIO, and make it an output with the right properties by adding two lines to pdmspi_init_pins()
.
static void pdmspi_init_pins(void) { //... #ifdef PULSEPIN Chip_GPIO_SetPinDIROutput(LPC_GPIO_PORT, 0, PULSEPIN); Chip_GPIO_SetPinState(LPC_GPIO_PORT, 0, PULSEPIN, FALSE); #endif //... }
I’m using the TXRDY
interrupt for the SPI peripheral to implement the pulse width. This is a good choice because it runs at 1/16th the microphone bit rate, and is continuously firing as it is needed to keep the clock running in the SPI peripheral master mode. The current firmware has about a 1 MHz bit clock, so this interrupt fires every 16 µs. If a 1 ms range corresponds to 90 degrees of motion, then varying the pulse width by one TXRDY
tick moves the shaft by 1.44 degrees. That is plenty fine enough for a quick demo. If higher resolution is required, then either MCLK
must be a higher frequency, or a different timer used to implement the pulse.
To implement the servo pulse then, we extend the SPI0_IRQHandler()
where it handles TXRDY
, being careful to add our new feature after it has already handled the empty buffer. The pulse itself is signaled by the value of pulsewidth
. If non-zero, the output is set high and pulsewidth
is decremented, and if zero it is set low and pulsewidth
is left unchanged.
/** Interrupt handler for the SPI0 peripheral. * * The RXRDY and TXRDY will fire at MCLK / 16 since we run the PDM capture * 16 bits at a time. We can use this as a convenient moderately high * frequency event to also do pulse generation for a hobby servo motor. */ void SPI0_IRQHandler(void) { //... if (status & SPI_STAT_TXRDY) { /* keep the mic clock active by always transmitting another 16 bits when possible. */ LPC_SPI0->TXDATCTL = SPI_TXDATCTL_FLEN(15) | SPI_TXDATCTL_DATA(0); #ifdef PULSEPIN // Also manage a pulse with width expressed in ticks at MCLK/16, which should // in the 16 us ballpark (assuming MCLK is about 1MHz). if (pulsewidth) { pulsewidth--; LPC_GPIO_PORT->SET[0] = 1UL << PULSEPIN; } else { LPC_GPIO_PORT->CLR[0] = 1UL << PULSEPIN; } #endif } //... }
The final change in pdmspi.c
is to create a function that fires a pulse with the suggested width. This function has a couple of unstated caveats. First, we do not round the requested width. This means that if less than 16 µs is requested, then no pulse will be made at all. Second, we don’t prevent calling this while a pulse is currently in progress, we just immediately change the value of pulsewidth
which might result in extending or clipping the pulse arbitrarily.
void pdmspi_pulse(uint16_t us) { #ifdef PULSEPIN pulsewidth = us / 16; #else us=us; #endif }
We also declare the function in pdmspi.h
, matching the signature in an obvious way.
Notice that we don’t make the declaration conditional on PULSEPIN
, only the body of the function. If PULSEPIN
is not defined, then pdmspi_pulse()
may still be called, but it will have no effect.
Using SPL to Move the Servo
To actually use the servo output, we change the main loop to occasionally call pdmspi_pulse()
to schedule an output pulse. The first change is to define an update rate in units of PCM sample times for the servo drive pulse. Typical servo motors like to be driven at more than 30 Hz, but not a lot more than 60 Hz. We used about 40 Hz here, but this can be adjusted if your motor doesn’t hold its position well enough.
/* PCM Samples per servo pulse output, picked to be between 30 and 60 Hz */ #define SERVORATE 200
We need a variable to count down to the next servo pulse. I’ve added this a local in the main loop at the top of main()
. I’ve also moved the variable storing the last computed SPL level out of its innermost scope and out to the outer scope of main()
so that it can be used to schedule servo pulses more than once per computed SPL.
int spl = 0; uint16_t servoupdate = SERVORATE;
Finally, at the bottom of the main loop we count down servoupdate
, and act when it hits zero. At that time, we compute the servo pulse width to command, stretching the SPL codes above 30 to cover a range of 1000 µs to 2000 µs:
int main(void) { //... while (1) { //... if (!--servoupdate) { uint16_t servo = ((spl-30)*1000/(MAXLG2SPL-30)) + 1000; if (servo < 1000) servo = 1000; if (servo > 2000) servo = 2000; pdmspi_pulse(servo); servoupdate = SERVORATE; } } // Not reached return 0 ; }
With these changes in place, each computed SPL value can be used to schedule 3.9 pulses (actually 789/200) which will be frequent enough to keep the servo motor happy.
We made the fairly arbitrary decision to mute the servo’s motion unless the raw SPL level is above 30 counts. This is the same as the LED, which we found has the nice property of leaving the LED dark unless a noise actually happens.
For the servo, this means that it sits quietly on my desk until someone speaks or something bumps the desk. Ordinary noises (HVAC turning on and off, wall clock ticking, etc.) don’t usually cause it to twitch.
Let us know what you think!
(Written with StackEdit.)