-
Notifications
You must be signed in to change notification settings - Fork 212
microphone: PDM MEMS microphone support using I2S interface #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Example using the i2s hardware interface on the Adafruit Circuit Playground Express | ||
// to read data from the onboard MEMS microphone. | ||
// | ||
// Uses ideas from the https://github.com/adafruit/Adafruit_CircuitPlayground repo. | ||
// | ||
package main | ||
|
||
import ( | ||
"machine" | ||
|
||
"tinygo.org/x/drivers/microphone" | ||
) | ||
|
||
const ( | ||
defaultSampleRate = 22000 | ||
quantizeSteps = 64 | ||
msForSPLSample = 50 | ||
defaultSampleCountForSPL = (defaultSampleRate / 1000) * msForSPLSample | ||
) | ||
|
||
func main() { | ||
machine.I2S0.Configure(machine.I2SConfig{ | ||
Mode: machine.I2SModePDM, | ||
AudioFrequency: defaultSampleRate * quantizeSteps / 16, | ||
ClockSource: machine.I2SClockSourceExternal, | ||
Stereo: true, | ||
}) | ||
|
||
mic := microphone.New(machine.I2S0) | ||
mic.SampleCountForSPL = defaultSampleCountForSPL | ||
mic.Configure() | ||
|
||
for { | ||
spl, maxval := mic.GetSoundPressure() | ||
println("C", spl, "max", maxval) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
// Package microphone implements a driver for a PDM microphone. | ||
// For example, the Adafruit PDM MEMS breakout board (https://www.adafruit.com/product/3492) | ||
// | ||
// Datasheet: https://cdn-learn.adafruit.com/assets/assets/000/049/977/original/MP34DT01-M.pdf | ||
// | ||
package microphone // import "tinygo.org/x/drivers/microphone" | ||
|
||
import ( | ||
"machine" | ||
"math" | ||
) | ||
|
||
const ( | ||
defaultSampleRate = 22000 | ||
quantizeSteps = 64 | ||
msForSPLSample = 50 | ||
defaultSampleCountForSPL = (defaultSampleRate / 1000) * msForSPLSample | ||
defaultGain = 9.0 | ||
defaultRefLevel = 0.00002 | ||
) | ||
|
||
// Device wraps an I2S connection to a PDM microphone device. | ||
type Device struct { | ||
bus machine.I2S | ||
|
||
// data buffer used for SPL sound pressure level samples | ||
data []int32 | ||
|
||
// buf buffer used for sinc filter | ||
buf []uint32 | ||
|
||
// SampleCountForSPL is number of samples aka size of data buffer to be used | ||
// for sound pressure level measurement. | ||
// Once Configure() is called, changing this value has no effect. | ||
SampleCountForSPL int | ||
|
||
// Gain setting used to calculate sound pressure level | ||
Gain float64 | ||
|
||
// ReferenceLevel setting used to calculate sound pressure level. | ||
ReferenceLevel float64 | ||
} | ||
|
||
// New creates a new microphone connection. The I2S bus must already be | ||
// configured. | ||
// | ||
// This function only creates the Device object, it does not touch the device. | ||
func New(bus machine.I2S) Device { | ||
return Device{ | ||
bus: bus, | ||
SampleCountForSPL: defaultSampleCountForSPL, | ||
Gain: defaultGain, | ||
ReferenceLevel: defaultRefLevel, | ||
} | ||
} | ||
|
||
// Configure the microphone. | ||
func (d *Device) Configure() { | ||
d.data = make([]int32, d.SampleCountForSPL) | ||
d.buf = make([]uint32, (quantizeSteps / 16)) | ||
} | ||
|
||
// Read the raw microphone data. | ||
func (d *Device) Read(r []int32) (int, error) { | ||
count := len(r) | ||
|
||
// get the next group of samples | ||
machine.I2S0.Read(d.buf) | ||
|
||
if len(r) > len(d.buf) { | ||
count = len(d.buf) | ||
} | ||
for i := 0; i < count; i++ { | ||
r[i] = int32(d.buf[i]) | ||
} | ||
|
||
return count, nil | ||
} | ||
|
||
// ReadWithFilter reads the microphone and filters the buffer using the sinc filter. | ||
func (d *Device) ReadWithFilter(r []int32) (int, error) { | ||
// read/filter the samples | ||
var sum uint16 | ||
for i := 0; i < len(r); i++ { | ||
|
||
// get the next group of samples | ||
machine.I2S0.Read(d.buf) | ||
|
||
// filter | ||
sum = applySincFilter(d.buf) | ||
|
||
// adjust to 10 bit value | ||
s := int32(sum >> 6) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason to cap at 10bit? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This might be somewhat specific to the mic in the Circuit Playground Express. Might be good to make that a setting. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why a setting? No common devices are more efficient at 10bit than at 16bit (and most even support 32bit just as efficiently). Using more bits always avoids all this and is most likely both simpler to implement and to use, apart from being more efficient. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a separate method |
||
|
||
// make it close to 0-offset signed | ||
s -= 512 | ||
|
||
r[i] = s | ||
} | ||
|
||
return len(r), nil | ||
} | ||
|
||
// GetSoundPressure returns the sound pressure in milli-decibels. | ||
func (d *Device) GetSoundPressure() (int32, int32) { | ||
// read/filter the samples | ||
d.ReadWithFilter(d.data) | ||
|
||
// remove offset | ||
var avg int32 | ||
for i := 0; i < len(d.data); i++ { | ||
avg += d.data[i] | ||
} | ||
avg /= int32(len(d.data)) | ||
|
||
for i := 0; i < len(d.data); i++ { | ||
d.data[i] -= avg | ||
} | ||
|
||
// get max value | ||
var maxval int32 | ||
for i := 0; i < len(d.data); i++ { | ||
v := d.data[i] | ||
if v < 0 { | ||
v = -v | ||
} | ||
if maxval < v { | ||
maxval = v | ||
} | ||
} | ||
|
||
// calculate SPL | ||
spl := float64(maxval) / 1023.0 * d.Gain | ||
spl = 20 * math.Log10(spl/d.ReferenceLevel) | ||
|
||
return int32(spl * 1000), maxval | ||
} | ||
|
||
// sinc filter for 44 khz with 64 samples | ||
// each value matches the corresponding bit in the 8-bit value | ||
// for that sample. | ||
// | ||
// For more information: https://en.wikipedia.org/wiki/Sinc_filter | ||
// | ||
var sincfilter = [quantizeSteps]uint16{ | ||
0, 2, 9, 21, 39, 63, 94, 132, | ||
179, 236, 302, 379, 467, 565, 674, 792, | ||
920, 1055, 1196, 1341, 1487, 1633, 1776, 1913, | ||
2042, 2159, 2263, 2352, 2422, 2474, 2506, 2516, | ||
2506, 2474, 2422, 2352, 2263, 2159, 2042, 1913, | ||
1776, 1633, 1487, 1341, 1196, 1055, 920, 792, | ||
674, 565, 467, 379, 302, 236, 179, 132, | ||
94, 63, 39, 21, 9, 2, 0, 0, | ||
} | ||
|
||
// applySincFilter uses the sinc filter to process a single set of sample values. | ||
func applySincFilter(samples []uint32) (result uint16) { | ||
var sample uint16 | ||
pos := 0 | ||
for j := 0; j < len(samples); j++ { | ||
// takes only the low order 16-bits | ||
sample = uint16(samples[j] & 0xffff) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good to be explicit here, probably. |
||
for i := 0; i < 16; i++ { | ||
if (sample & 0x1) > 0 { | ||
result += sincfilter[pos] | ||
pos++ | ||
} | ||
sample >>= 1 | ||
} | ||
} | ||
|
||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this only now after the I2S PR was merged but a channel count might have been more flexible.
On the other hand, AFAIK I2S has really been built for mono/stereo and there are very few DAC/ADCs with more than two channels. I could be wrong, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit of research shows a number of multi-channel chips, but not too many MCUs. I suggest we defer that until a future iteration.