diff --git a/Makefile b/Makefile index c052910df..53da3434f 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index c1fd7ceb1..a9b8a368b 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/examples/microphone/main.go b/examples/microphone/main.go new file mode 100644 index 000000000..ccb643bfd --- /dev/null +++ b/examples/microphone/main.go @@ -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) + } +} diff --git a/microphone/microphone.go b/microphone/microphone.go new file mode 100644 index 000000000..e6b6e8a6e --- /dev/null +++ b/microphone/microphone.go @@ -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) + + // 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) + for i := 0; i < 16; i++ { + if (sample & 0x1) > 0 { + result += sincfilter[pos] + pos++ + } + sample >>= 1 + } + } + + return +}