Microphones add lots of important features to your project, but unfortunately, they can be a pain to get working.
Today I would like to show you how to get an audio recording from a PDM microphone (there will be more on PDM later) and display it with LVGL.
Below you will find a definition of some terms, some code, and a walk-through of how that code works.
What is I2S
I2S is how we connect the microphone to our computer. It takes four wires to fully hook it up. If you are using only a microphone and not an additional speaker, then you can get away with three wires.
PDM vs. PCM
First I need to explain that PDM and PCM are the format that your microphone sends data in. So you still use I2S to get data from both types of microphones, there is no difference there.
PCM is essentially raw data. It’s high quality and simple to use.
PDM is more complicated, I don’t work with microphones that much so I don’t know what exactly makes it so special.
The microphone on my board is a PDM microphone, so the code below will work with PDM microphones. If you are use a PCM microphone, there is one line of code you will need to change. Down in the explanation there is a section on how to do that.
Micropython LVGL
Besides getting the actual microphone data, I wanted to display it on a screen, so I chose to use LVGL.
To get this example to work you will need to add your screen code to the beginning of this example. If you want more information try How to get a LVGL Micropython screen to work.
The Espidf Module
One of the features of micropython LVGL is that is has a module called espidf. This module is how we process the PDM signal. There are simpler ways if you are using a PCM microphone.
If you are using a PCM mic, try the machine module.
Two important facts to keep in mind are that the espidf module only works on esp boards like the esp32, and that you are unlikely to find it in a normal micropython firmware.
Here is the code:
import espidf,lvgl
config = espidf.i2s_config_t()
config.mode = espidf.I2S_MODE.MASTER| espidf.I2S_MODE.RX | espidf.I2S_MODE.PDM
config.sample_rate = 16000
config.bits_per_sample = espidf.I2S_BITS_PER_SAMPLE._16BIT
config.channel_format = espidf.I2S_CHANNEL_FMT.ONLY_RIGHT
config.communication_format = espidf.I2S_COMM_FORMAT.I2S
config.intr_alloc_flags = 0
config.dma_buf_count = 2
config.dma_buf_len = 800
config.use_apll = False
mic = espidf.i2s_pin_config_t()
mic.bck_io_num = 12
mic.ws_io_num = 0
mic.data_in_num = 34
mic.data_out_num = 2
espidf.i2s_driver_install(espidf.I2S_NUM._0,config,0,None)
espidf.i2s_set_pin(espidf.I2S_NUM._0,mic)
chart = lvgl.chart( lvgl.scr_act() )
chart.set_size( 320, 240 )
chart.center()
chart.set_point_count( 100 )
chart.set_style_size( 0, 0, lvgl.PART.INDICATOR )
chart.set_range(lvgl.chart.AXIS.PRIMARY_Y,0,240)
series1 = chart.add_series( lvgl.color_hex( 0x0099FF ), lvgl.chart.AXIS.PRIMARY_Y )
def get_round(data,High,nHigh):
div = High/nHigh
return int(data/div)
def get_val(data):
total = 0
for x in range(int(len(data)/2)):
total += get_round(data[2*x+1]+(data[2*x]*256),65536,240)
total = total/(len(data)/2)
return int(total)
import struct
val = 0
while True:
leng = struct.pack('P',val)
buffer= bytearray(800)
length = espidf.i2s_read(espidf.I2S_NUM._0,buffer,300,leng,1000000)
chart.set_next_value( series1, get_val(buffer))
How It Works
Now that you have seen the code, let’s step through it to see how it works behind the scenes.
The Setup
Before we do anything we need to import two modules. The lvgl module is a graphics library, and the espidf module is what lets us talk to the microphone.
import espidf,lvgl
The Config
First thing we need to do is create a I2S config object. This is what tells the the computer how to process the I2S signals.
config = espidf.i2s_config_t()
Then we need to set the configs mode. Modes control whether your devise is a master or slave, whether you device is transmitting or receiving, and whether your device uses PDM or PCM.
config.mode = espidf.I2S_MODE.MASTER| espidf.I2S_MODE.RX | espidf.I2S_MODE.PDM
Enabling PCM
If you have a PCM microphone you will need to replace the above code with the following line. This should work, but I have not tested it yet, so I can’t say for sure.
config.mode = espidf.I2S_MODE.MASTER| espidf.I2S_MODE.RX
Next we set the sample rate. This is how many chunks of audio you want to revive per second. 16000 seams to be the minimum value for the esp32. For reference, 44100 is CD quality.
config.sample_rate = 16000
After that we need to set the word length which tells the computer how big a chunck of audio is. For my microphone that is 16 bits.
config.bits_per_sample = espidf.I2S_BITS_PER_SAMPLE._16BIT
Many headphones or speakers are stereo, which means that the computer can tell the headphones to send data only to a specific ear, like your left ear. Because my microphone is not stereo, I have to set which channel I want it to send data to. I chose the right channel, but it doesn’t really matter.
config.channel_format = espidf.I2S_CHANNEL_FMT.ONLY_RIGHT
Another feature we are going to set is the audio format. Because there are some slight differences in how I2S data can be sent, we need to change which format we are using. For my microphone, I need standard I2S. If you are not sure which format to use, start with standard.
config.communication_format = espidf.I2S_COMM_FORMAT.I2S
Next we set the interrupt flags. Setting it to zero is a simple way to get it to work.
config.intr_alloc_flags = 0
Because the microphone sends so much data, we need to give it a buffer to temporally store the data in.
To start, we need to tell the computer how many buffers we want. You will want at least two, but having more can potentially speed up performance.
config.dma_buf_count = 2
Then we need to set how large each of those buffers are. I chose 800 samples per buffer.
config.dma_buf_len = 800
This last line of code controls if the apall clock is used. It will give you more accurate timing, but because we don’t need it, I turned it off.
config.use_apll = False
The Wiring
Next thing we need to do is setup the pins that we are using. I2S as four pins that can be used. They are as follows.
NAME | DESCRIPTION | OTHER NAMES |
---|---|---|
BCK | This is the clock pin | CLK |
WS | This pin selects whether the microphone is to send the left or right channel data | LRCK, LR |
DATA IN | This pin is how your computer receives audio. | DATA, DIN, RX |
DATA OUT | This pin is how your computer sends audio. | DATA, DOUT, TX |
Because I am using only one microphone I don’t not have to wire up the DATA OUT or WS pins. If your microphone has a WS pin than it might still be wise to wire it up, to avoid glitches.
NAME | ESP32 PIN |
---|---|
BCK | 12 |
WS (optional) | 0 |
DATA IN | 34 |
Setting Up the Pins
Now that we have deciede on the pins we are going to use we need to set them up.
first we create a I2S pin object.
mic = espidf.i2s_pin_config_t()
Then we set the BCK pin.
mic.bck_io_num = 12
We set the WS and DATA IN pin the same way.
mic.ws_io_num = 0
mic.data_in_num = 34
Next we have to give a value to the data out pin. There is a way to disable this pin, but it’s simpler just to give it an fake pin.
mic.data_out_num = 2
Turning the I2S on
Finally, we can take all the configs we have created and turn on the I2S.
To start, we add the driver config we created earlier to I2S port 0.
espidf.i2s_driver_install(espidf.I2S_NUM._0,config,0,None)
Then we setup the pins for I2S 0 as well.
espidf.i2s_set_pin(espidf.I2S_NUM._0,mic)
Displaying the Data
We have the microphone data now, but what do we do with it? I decided that I would build a graph that shows the sound being recorded.
First, we have to create a LVGL chart that is 320 pixels wide and 240 pixels tall.
chart = lvgl.chart( lvgl.scr_act() )
chart.set_size( 320, 240 )
Then we center it on the screen.
chart.center()
The code below sets up some of the features of the chart. Because this is a tutorial on microphones and not charts I will not waste your time explaining it.
chart.set_point_count( 100 )
chart.set_style_size( 0, 0, lvgl.PART.INDICATOR )
chart.set_range(lvgl.chart.AXIS.PRIMARY_Y,0,240)
series1 = chart.add_series( lvgl.color_hex( 0x0099FF ), lvgl.chart.AXIS.PRIMARY_Y )
The Rounding Functions
The microphone we are using gives so much data that the screen would be swamped in data if we tried to use it all. So instead, I created the following two rounder functions to help shrink the data.
def get_round(data,High,nHigh):
div = High/nHigh
return int(data/div)
def get_val(data):
total = 0
for x in range(int(len(data)/2)):
total += get_round(data[2*x+1]+(data[2*x]*256),65536,240)
total = total/(len(data)/2)
return int(total)
Getting the Audio
To begin, we need to import the struct module. This is used to create a pointer which is needed to read the mic’s data.
import struct
Then we create an empty variable called val.
val = 0
Next, we create a infinite loop so the computer constantly reads the microphone’s data.
while True:
After that we need to create a pointers so we can read the mic.
leng = struct.pack('P',val)
We also need a buffer to store the microphone’s data in.
buffer= bytearray(800)
And finaly, we actually read the microphone.
length = espidf.i2s_read(espidf.I2S_NUM._0,buffer,300,leng,1000000)
The last thing we do is update the chart so it has the new mic data.
chart.set_next_value( series1, get_val(buffer))
Wraping Up
Now that you have seen the code, try running it. If every thing works right, you should see a bunch a squiggly lines that change when you say something.
After you get the code to run you can experiment with it. Maybe you can hook up a speaker and record some audio then play it back.
When I was building this project, I started by recording data to an SD card. Then I played it on my computer. That is a great way to test that you are getting a good recording from your mic.
If you liked this project you might also like How To Use A PNG Image With Micropython LVGL.