Skip to content

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

Merged
merged 1 commit into from
Jul 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,6 @@ smoke-test:
tinygo build -size short -o ./build/test.elf -target=microbit ./examples/waveshare-epd/epd2in13x/main.go
tinygo build -size short -o ./build/test.elf -target=circuitplay-express ./examples/ws2812/main.go
tinygo build -size short -o ./build/test.elf -target=trinket-m0 ./examples/bme280/main.go
tinygo build -size short -o ./build/test.elf -target=circuitplay-express ./examples/microphone/main.go

test: clean fmt-check smoke-test
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func main() {
| [LIS3DH accelerometer](https://www.st.com/resource/en/datasheet/lis3dh.pdf) | I2C |
| [MAG3110 magnetometer](https://www.nxp.com/docs/en/data-sheet/MAG3110.pdf) | I2C |
| [BBC micro:bit LED matrix](https://github.com/bbcmicrobit/hardware/blob/master/SCH_BBC-Microbit_V1.3B.pdf) | GPIO |
| [Microphone - PDM](https://cdn-learn.adafruit.com/assets/assets/000/049/977/original/MP34DT01-M.pdf) | I2S/PDM |
| [MMA8653 accelerometer](https://www.nxp.com/docs/en/data-sheet/MMA8653FC.pdf) | I2C |
| [MPU6050 accelerometer/gyroscope](https://store.invensense.com/datasheets/invensense/MPU-6050_DataSheet_V3%204.pdf) | I2C |
| [PCD8544 display](http://eia.udg.edu/~forest/PCD8544_1.pdf) | SPI |
Expand Down
37 changes: 37 additions & 0 deletions examples/microphone/main.go
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,
Copy link
Member

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.

Copy link
Member Author

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.

})

mic := microphone.New(machine.I2S0)
mic.SampleCountForSPL = defaultSampleCountForSPL
mic.Configure()

for {
spl, maxval := mic.GetSoundPressure()
println("C", spl, "max", maxval)
}
}
173 changes: 173 additions & 0 deletions microphone/microphone.go
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to cap at 10bit?
While cheap microphones probably don't have anything but noise beyond that point, it seems better to me to not lose bits unnecessarily.

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.
Or am I missing something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a separate method ReadWithFilter() to provide this capability, and put in a simpler raw Read() implementation instead for the more general cases.


// 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the & 0xffff us unnecessary here but I agree it may be easier to read (to make the capping explicit).

Copy link
Member Author

Choose a reason for hiding this comment

The 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
}