roboattic Lab
Published © CC BY-NC-SA

Portable Decibel Meter Using Raspberry Pi Pico, Micro Python

Portable Decibel Meter using Raspberry Pi Pico and a high-precision INMP441 I2S MEMS microphone. The device measures real-time sound levels.

BeginnerFull instructions provided53

Things used in this project

Hardware components

Raspberry Pi Pico
Raspberry Pi Pico
×1
SparkFun MEMS Microphone Breakout - INMP401 (ADMP401)
SparkFun MEMS Microphone Breakout - INMP401 (ADMP401)
×1

Software apps and online services

Thonny IDE
Fusion
Autodesk Fusion

Story

Read more

Custom parts and enclosures

Decibel Meter Box

Sketchfab still processing.

Decibel Meter Lid

Sketchfab still processing.

Schematics

Schematic Decibel Meter

Circuit Diagram

Code

main.py

MicroPython
#main.py
from machine import I2S, Pin, I2C
from ssd1306 import SSD1306_I2C
import math
import time
import array

# --- I2S CONFIGURATION (Microphone) ---
SCK_PIN = 16
WS_PIN = 17
SD_PIN = 18
SAMPLE_RATE = 16000
BITS_PER_SAMPLE = 32
BUFFER_LENGTH = 64
DB_OFFSET = -46.72

audio_in = I2S(
    0, sck=Pin(SCK_PIN), ws=Pin(WS_PIN), sd=Pin(SD_PIN),
    mode=I2S.RX, bits=BITS_PER_SAMPLE, format=I2S.MONO,
    rate=SAMPLE_RATE, ibuf=2048
)
read_buffer = bytearray(BUFFER_LENGTH * 4)

# --- OLED CONFIGURATION (Display) ---
OLED_WIDTH = 128
OLED_HEIGHT = 64
i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400000)
oled = SSD1306_I2C(OLED_WIDTH, OLED_HEIGHT, i2c)
SMOOTHING_FACTOR = 0.15  
smoothed_db = 0.0
is_first_reading = True

# Peak hold for visual effect
peak_db = 0.0
peak_hold_time = 0
PEAK_HOLD_DURATION = 20

print("Starting Professional Decibel Meter...")

def draw_meter_bar(oled, db_value):
    """Draw a professional-looking meter bar with segments"""
    # Map dB range (30-90 dB) to bar width (0-100 pixels)
    db_min = 30.0
    db_max = 90.0
    
    # Clamp value
    db_clamped = max(db_min, min(db_max, db_value))
    
    # Calculate bar width (100 pixels max)
    bar_width = int((db_clamped - db_min) / (db_max - db_min) * 100)
    
    # Draw border for meter
    oled.rect(0, 28, 102, 14, 1)
    
    # Draw segmented bar (10 segments)
    for seg in range(10):
        seg_start = seg * 10 + 1
        seg_end = seg_start + 8
        
        if bar_width > seg_start:
            # Fill this segment
            fill_width = min(8, bar_width - seg_start)
            oled.fill_rect(seg_start + 1, 30, fill_width, 10, 1)
    
    # Draw level indicators
    for i in range(0, 101, 20):
        oled.vline(i + 1, 42, 3, 1)
    
    # Draw labels
    oled.text("30", 0, 46, 1)
    oled.text("60", 44, 46, 1)
    oled.text("90", 88, 46, 1)

def draw_peak_indicator(oled, peak_value):
    """Draw a small peak hold indicator"""
    db_min = 30.0
    db_max = 90.0
    peak_clamped = max(db_min, min(db_max, peak_value))
    peak_pos = int((peak_clamped - db_min) / (db_max - db_min) * 100)
    
    if peak_pos > 0 and peak_pos <= 100:
        # Draw peak marker
        oled.vline(peak_pos + 1, 29, 12, 1)

while True:
    num_bytes_read = audio_in.readinto(read_buffer)
    samples_read = num_bytes_read // 4
    
    if samples_read > 0:
        mic_samples = array.array('i', read_buffer)
        sum_squares = 0.0
        
        for i in range(samples_read):
            processed_sample = mic_samples[i] >> 8
            sum_squares += processed_sample * processed_sample
            
        rms = math.sqrt(sum_squares / samples_read)
        if rms <= 0:
            rms = 1
            
        db = 20.0 * math.log10(rms)
        final_db = db + DB_OFFSET
        
        if is_first_reading:
            smoothed_db = final_db
            is_first_reading = False
        else:
            smoothed_db = (SMOOTHING_FACTOR * final_db) + ((1 - SMOOTHING_FACTOR) * smoothed_db)
        
        # Peak detection with hold
        if smoothed_db > peak_db:
            peak_db = smoothed_db
            peak_hold_time = PEAK_HOLD_DURATION
        else:
            peak_hold_time -= 1
            if peak_hold_time <= 0:
                # Slowly decay peak
                peak_db = peak_db * 0.95
        
        # Print to Serial Monitor
        print(f"Raw: {final_db:.2f} | Smoothed: {smoothed_db:.2f} | Peak: {peak_db:.2f}")
        
    
        oled.fill(0)  
        
        # Title with box
        oled.rect(0, 0, 128, 12, 1)
        oled.text("dB METER", 35, 2, 1)
        
        # Large dB value display
        db_str = f"{smoothed_db:.1f}"
        oled.text(db_str, 30, 15, 1)
        oled.text("dB", 75, 15, 1)
        
        # Draw meter bar with segments
        draw_meter_bar(oled, smoothed_db)
        
        # Draw peak hold indicator
        draw_peak_indicator(oled, peak_db)
        
        # Status indicator (small dot that blinks)
        if int(time.ticks_ms() / 500) % 2:
            oled.fill_rect(122, 2, 4, 4, 1)
        
        oled.show()  # Update display
    
    time.sleep(0.05)

ssd1306.py

MicroPython
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces

from micropython import const
import framebuf

# register definitions
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)

# Subclassing FrameBuffer provides support for graphics primitives
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
class SSD1306(framebuf.FrameBuffer):
    def __init__(self, width, height, external_vcc):
        self.width = width
        self.height = height
        self.external_vcc = external_vcc
        self.pages = self.height // 8
        self.buffer = bytearray(self.pages * self.width)
        super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            SET_DISP | 0x00,  # off
            # address setting
            SET_MEM_ADDR,
            0x00,  # horizontal
            # resolution and layout
            SET_DISP_START_LINE | 0x00,
            SET_SEG_REMAP | 0x01,  # column addr 127 mapped to SEG0
            SET_MUX_RATIO,
            self.height - 1,
            SET_COM_OUT_DIR | 0x08,  # scan from COM[N] to COM0
            SET_DISP_OFFSET,
            0x00,
            SET_COM_PIN_CFG,
            0x02 if self.width > 2 * self.height else 0x12,
            # timing and driving scheme
            SET_DISP_CLK_DIV,
            0x80,
            SET_PRECHARGE,
            0x22 if self.external_vcc else 0xF1,
            SET_VCOM_DESEL,
            0x30,  # 0.83*Vcc
            # display
            SET_CONTRAST,
            0xFF,  # maximum
            SET_ENTIRE_ON,  # output follows RAM contents
            SET_NORM_INV,  # not inverted
            # charge pump
            SET_CHARGE_PUMP,
            0x10 if self.external_vcc else 0x14,
            SET_DISP | 0x01,
        ):  # on
            self.write_cmd(cmd)
        self.fill(0)
        self.show()

    def poweroff(self):
        self.write_cmd(SET_DISP | 0x00)

    def poweron(self):
        self.write_cmd(SET_DISP | 0x01)

    def contrast(self, contrast):
        self.write_cmd(SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(SET_NORM_INV | (invert & 1))

    def show(self):
        x0 = 0
        x1 = self.width - 1
        if self.width == 64:
            # displays with width of 64 pixels are shifted by 32
            x0 += 32
            x1 += 32
        self.write_cmd(SET_COL_ADDR)
        self.write_cmd(x0)
        self.write_cmd(x1)
        self.write_cmd(SET_PAGE_ADDR)
        self.write_cmd(0)
        self.write_cmd(self.pages - 1)
        self.write_data(self.buffer)


class SSD1306_I2C(SSD1306):
    def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
        self.i2c = i2c
        self.addr = addr
        self.temp = bytearray(2)
        self.write_list = [b"\x40", None]  # Co=0, D/C#=1
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.temp[0] = 0x80  # Co=1, D/C#=0
        self.temp[1] = cmd
        self.i2c.writeto(self.addr, self.temp)

    def write_data(self, buf):
        self.write_list[1] = buf
        self.i2c.writevto(self.addr, self.write_list)


class SSD1306_SPI(SSD1306):
    def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
        self.rate = 10 * 1024 * 1024
        dc.init(dc.OUT, value=0)
        res.init(res.OUT, value=0)
        cs.init(cs.OUT, value=1)
        self.spi = spi
        self.dc = dc
        self.res = res
        self.cs = cs
        import time

        self.res(1)
        time.sleep_ms(1)
        self.res(0)
        time.sleep_ms(10)
        self.res(1)
        super().__init__(width, height, external_vcc)

    def write_cmd(self, cmd):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(0)
        self.cs(0)
        self.spi.write(bytearray([cmd]))
        self.cs(1)

    def write_data(self, buf):
        self.spi.init(baudrate=self.rate, polarity=0, phase=0)
        self.cs(1)
        self.dc(1)
        self.cs(0)
        self.spi.write(buf)
        self.cs(1)

Decibel Meter GitHub repo

Credits

roboattic Lab
18 projects • 10 followers
YouTube Content Creator Robotics Enthusiast

Comments