Weekend Project: MMA8451Q Accelerometer as a Microphone?
Description
Last weekend was the Australia Day long weekend, so aside from chasing Shortwave Radiogram broadcasts and posting radiofax galleries, I also decided to build something I thought was rather interesting.
In the past, I remember vaguely seeing articles about how computer hard drives are so sensitive to disturbances that they could be used to detect earthquakes and that smartphone accelerometers could be used as a rudimentary microphone. What might that actually sound like? I was rather intrigued so I wanted to build my own.
Rationale and Hardware
Ultimately, sound waves are vibrations in air and accelerometers are quite capable of picking up vibrations. Unfortunately, the air displaced isn’t quite enough to make an accelerometer vibrate, so instead, the accelerometer is probably best used as a contact microphone – kind of like an “inverse” bone-conduction speaker.
I decided to use the parts I had lying about which included an Arduino Leonardo for its superior USB-CDC capability and a low-cost AU$4 NXP MMA8451Q 14-bit digital accelerometer to initially prove the concept. A few simple Dupont-style jumper wires completes the build for testing. As the GY-45 module appears to have onboard voltage regulation and level shifting, there was no need to worry about 3.3V/5V compatibility issues.
Unfortunately, the MMA8451Q is capable of just 800Hz sample rate, which would result in very band-limited audio. That’s good enough for a try. A higher sample rate would be nice (e.g. the LIS3DH which has 5376Hz) but this is often traded off with greater noise (220uG/sqrt(Hz) vs 99uG/sqrt(Hz)). A nice middle-ground seems to be the ADXL355 with 20-bit resolution and 4000Hz sample rate, or the ADXL354 (analog) version, but both of these units are very pricey – about $50 for the bare IC alone.
Sketch and Testing
While the MMA8451Q does have various libraries available from third parties, I made a conscious decision to avoid using any libraries at all and instead just rely on writing “straight” Wire-library commands. My reasoning is that many of the libraries are becoming almost needlessly bloated with additional conversions (e.g. floating point calculations) which slow down operation, increase code size and cause additional complications. Many of the libraries are designed for “asynchronous” one-shot sample reading, which makes it extremely difficult for audio – how do you avoid double-reading or skipping a sample?
Instead, a quick look at the datasheet and application notes revealed that the default settings for the MMA8451Q to be the ones we desire – i.e. 800Hz ODR, no filtering, no FIFO (for simplicity). Using I2C fast mode to ensure the bus does not hinder data transfer, it is as simple as the following code:
// MMA8451 Accelerometer Microphone Experiment
// Direct access code with NO ERROR CHECKING (!!!)
// Maximal 800Hz sample rate + 14-bit/axis + 2G full-scale
// 3-channels of RAW data returned over Serial
// Does not enforce output byte alignment - import as raw
// data with 16-bit/LE/3ch/800Hz + 0/1 byte offset!
//
// January 2019 - goughlui.com
#include <Wire.h>
#define MMA_ADDR 0x1C
uint8_t axdat[6];
void setup(void) {
Wire.begin();
Wire.setClock(400000); // I2C Fast Mode (400kHz)
writeRegister8(0x2B,0x40); // Device Soft RST
delay(50); // Wait for Reset
writeRegister8(0x2A,0x00); // Device Standby
writeRegister8(0x2D,0x01); // Enable DRDY Interrupt
writeRegister8(0x2A,readRegister8(0x2A)|0x01); // Enable Device
Serial.begin(921600); // High baud rate to prevent overflow
// Although for CDC-based Arduinos
// it probably has no effect.
while(!Serial) {} // Leo wait for Serial Connect
}
// Operating in non-FIFO mode only works as Arduino loops faster
// than accelerometer can generate data inclusive of bus read
// overheads which have been reduced through use of burst-read.
void loop() {
if(readRegister8(0x0C)&0x01) { // If DRDY asserted
readAllAccel(axdat); // Burst read all accelerometer data
for(int i=0;i<6;i++) {
Serial.write(axdat[i]); // Write raw array data to Serial
}
}
}
void writeRegister8(uint8_t reg, uint8_t value) {
Wire.beginTransmission(MMA_ADDR);
Wire.write((uint8_t)reg);
Wire.write((uint8_t)(value));
Wire.endTransmission();
}
uint8_t readRegister8(uint8_t reg) {
Wire.beginTransmission(MMA_ADDR);
Wire.write(reg);
Wire.endTransmission(false);
Wire.requestFrom(MMA_ADDR, 1);
return (Wire.read());
}
// Extra function that reads all of the current accelerometer
// values in burst-read to save bus transaction time.
void readAllAccel(uint8_t *axdat) {
Wire.beginTransmission(MMA_ADDR);
Wire.write(0x01);
Wire.endTransmission(false);
Wire.requestFrom(MMA_ADDR, 6);
for(int i=0;i<6;i++) {
*(axdat+i)=Wire.read();
}
}
The code makes no error checks – it assumes an accelerometer is there and it assumes it is working. It resets everything to default and enables the data ready interrupt to be alerted whenever there is a new sample. In the main code, it checks if the interrupt register has a flag set indicating new data – if so, grab the six bytes corresponding to the data and output them out on the USB-CDC serial port.
Note that the code itself is very crude – but that’s part of the reason why it works. Just blindly polling the interrupt flag and copying out data wouldn’t work if the sensor was converting data faster than it was being polled! That’s where FIFO buffering and burst-read operations to minimise bus overhead come into play. I’ve already made advantage of the burst read and auto-increment feature when reading out the X, Y and Z two-byte values – hence Wire.requestFrom(MMA_ADDR,6) rather than reading one byte from each address which takes much longer.
But of course, you shouldn’t just take my word for it – it would be good to know that we are safely polling faster than the sensor is generating data. As a result, I hooked up the Rohde & Schwarz RTM3004 (that won me RoadTest Review of the Year at element14) to do some bus decoding and give me a chance to test the new V1.400 firmware at the same time.
First step is just to capture a series of transactions for analysis and enable/configure the bus decoding for the situation – SCL on C1 and SDA on C2 with a 1.5V threshold.
Examining the bus table makes it clear what is happening – we are polling faster than the data is being generated – it doesn’t fit on the screen here but we poll about seven times before we get a sample.
Notice the series of reads returning 0x00 (i.e. no interrupt), before we get one that returns 0x01 (i.e. DRDY interrupt), which triggers our attempt to read 6-bytes from 0x01 returning the accelerometer data for X, Y and Z (two bytes each).
Given the fact we are polling so much more frequently than the accelerometer generates data, we aren’t at any risk of losing data. But we can see that there is a “pause” right after the data is returned, as the microcontroller needs to push the data out via USB-CDC emulated serial – that code does consume some processing time. If the bus rate were to be slower and an inefficient library is used, there is a potential that data could be lost resulting in a “variable” sample rate.
In Use
As I wasn’t really happy with the length of the jumper cables – they made it hard to attach it to anything (e.g. my throat, my











