ESP32S3 (MicroPython) + MAX98357 I2S Audio Amplifier to play tones

This post exercise on ESP32-S3-DevKitC-1 running MicroPython v1.24.1, to play tones using MAX98357 I2S Audio Amplifier.


Connection between MAX98357 and ESP32-S3-DevKitC-1:

		MAX98357	ESP32-S3-DevKitC-1
		==================================
		VCC             3V3
		GND             GND
		MAX98357_LRC    GPIO17
		MAX98357_BCLK   GPIO16
		MAX98357_DIN    GPIO15
		MAX98357_GAIN   GND
		

mpy_s3_max98357.py, a simple MicroPython to play single tone on MAX98357 I2S Audio Amplifier.
"""
ESP32-S3-DevKitC-1 running MicroPython v1.24.1
Play single tone output to MAX98357.

"""

import math
import array
import time
from machine import I2S, Pin

print("Start")

MAX98357_LRC = 17
MAX98357_BCLK = 16
MAX98357_DIN =15

i2s = I2S(0,
          sck=Pin(MAX98357_BCLK),
          ws=Pin(MAX98357_LRC),
          sd=Pin(MAX98357_DIN),
          mode=I2S.TX,
          bits=16,
          format=I2S.MONO,
          rate=44100,
          ibuf=44100,
          )
print("i2s:", i2s)

def generate_tone(sample_rate, frequency, duration):
    tick_ms_generate_tone_start = time.ticks_ms()
    sample_count = int(sample_rate * duration)
    amplitude = 14000 #32767
    wave = array.array("h", (0 for _ in range(sample_count)))
    for i in range(sample_count):
        wave[i] = int(amplitude * math.sin(2 * math.pi * frequency * i / sample_rate))
    tick_ms_generate_tone_end = time.ticks_ms()
    print("Time to generate Tone", frequency, "Hz for", duration,
          "s with sample_rate", sample_rate, "=", "ms",
          time.ticks_diff(tick_ms_generate_tone_end, tick_ms_generate_tone_start))
    return wave


duration = 1
tone_1k5 = generate_tone(44100, 1500, duration)

def play_tone():
    print("Play single tone")
    tick_ms_i2s_write_start = time.ticks_ms()
    i2s.write(tone_1k5)
    tick_ms_i2s_write_end = time.ticks_ms()
    print("Time for i2s.write()", ":",
          time.ticks_diff(tick_ms_i2s_write_end, tick_ms_i2s_write_start), "ms")
    time.sleep(duration)
    print("Playing stop")

play_tone()

while True:
    k = input("'q' to quit, others re-play tone.")
    if k == 'q':
        break
    play_tone()

print("~ bye ~")
i2s.deinit()


With 3.2" 320x240 IPS LCD (ILI9341 SPI), user interface added.

Connection:

mpy_s3_max98357_touchLCD.py
"""
ESP32-S3-DevKitC-1 running MicroPython v1.24.1
3.2" 320x240 IPS LCD (ILI9341 SPI) with Cap. Touch (FT6336) and Micro SD Slot
Touch LCD to play tone output to MAX98357.

Libraries needed, install to /lib:
- ili9341.py
- ft6x36.py
"""
import os, sys
from time import sleep
from machine import Pin, SPI, I2C, I2S

from ili9341 import Display, color565

from ft6x36 import FT6x36

import math
import array

print("====================================")
print(sys.implementation[0], os.uname()[3],
      "\nrun on", os.uname()[4])
print("====================================")

LCD_CS=14  # low to select
LCD_RST=13
LCD_RS=12
LCD_SDI=11 #MOSI
LCD_SCK=10
LCD_LED=9
LCD_SDO=8  #MISO

CTP_SCL=7
CTP_RST=6
CTP_SDA=5
CTP_INT=4  #Not used

SD_CS=18   # low to select

MAX98357_LRC = 17
MAX98357_BCLK = 16
MAX98357_DIN =15

# un-select both LCD and SD in power-up,
# to prevent un-used cs unstable and false selected.
pin_cs_lcd = Pin(LCD_CS, Pin.OUT)
pin_cs_lcd.value(1)
pin_cs_sd = Pin(SD_CS, Pin.OUT)
pin_cs_sd.value(1)

pin_backlight=Pin(LCD_LED, Pin.OUT)
pin_backlight.value(0)

# output a negative pulse to CTP_RST pin, to reset FT6336U
pin_CTP_RST=Pin(CTP_RST, Pin.OUT)
pin_CTP_RST.value(1)
sleep(0.1)
pin_CTP_RST.value(0)
sleep(0.1)
pin_CTP_RST.value(1)
sleep(0.1)

# rot and rot_set
# are used to better manager rotation/width/height of display and touch
rot=1   # 0 rotation = 0
        # 1 rotation = 90
        # 2 rotation = 180
        # 3 rotation = 270
# rot_set[x][0] : ili9341 Display rotation
# rot_set[x][1] : ili9341 Display width
# rot_set[x][2] : ili9341 Display height
# rot_set[x][3] : ft6336U Touch rotation
# rot_set[x][4] : ft6336U Touch width
# rot_set[x][5] : ft6336U Touch height
rot_set = [[0, 240, 320, FT6x36.PORTRAIT_INVERTED, 240, 320],
           [90, 320, 240, FT6x36.LANDSCAPE_INVERTED, 240, 320],
           [180, 240, 320, FT6x36.PORTRAIT, 240, 320],
           [270, 320, 240, FT6x36.LANDSCAPE, 240, 320]]

# spi share by LCD and SD
spi = SPI(1, baudrate=40000000, sck=LCD_SCK, mosi=LCD_SDI, miso=LCD_SDO)

display = Display(spi,
                  rotation=rot_set[rot][0],
                  width=rot_set[rot][1], height=rot_set[rot][2],
                  dc=Pin(LCD_RS), cs=Pin(LCD_CS), rst=Pin(LCD_RST))
display.invert(enable=True)

# i2c
i2c = I2C(0, scl=Pin(CTP_SCL), sda=Pin(CTP_SDA))
touch = FT6x36(i2c,
               width=rot_set[rot][4],
               height=rot_set[rot][5],
               rotation=rot_set[rot][3],)

i2s = I2S(0,
          sck=Pin(MAX98357_BCLK),
          ws=Pin(MAX98357_LRC),
          sd=Pin(MAX98357_DIN),
          mode=I2S.TX,
          bits=16,
          format=I2S.MONO,
          rate=44100,
          ibuf=44100,
          )
#print("i2s:", i2s)

pin_backlight.value(1)
display.clear()
# draw a rectangle show the boundary of the screen
display.draw_rectangle(0, 0, display.width, display.height, color565(255, 255, 255))

def generate_tone(sample_rate, frequency, duration):
    sample_count = int(sample_rate * duration)
    amplitude = 14000 #32767
    wave = array.array("h", (0 for _ in range(sample_count)))
    for i in range(sample_count):
        wave[i] = int(amplitude * math.sin(2 * math.pi * frequency * i / sample_rate))
    return wave

num_of_tone = 7
tone_list = [[261.63],
             [293.66],
             [329.63],
             [349.23],
             [392.00],
             [440.00],
             [493.88]]

button_width = display.width // num_of_tone
button_height = 180
button_margin_x = 5
button_top = 50
button_bottom = button_top+button_height

tone_sample_rate = 44100
tone_duration = 0.5

display.draw_text8x8(5, 5,
                     "Generating tone, please wait!",
                     color565(255, 255, 255),
                     background=color565(0, 0, 0)
                     )
for i in range(num_of_tone):
    x = i * button_width + button_margin_x
    y = button_top
    w = button_width - (2 * button_margin_x)
    h = button_bottom - button_top
    
    display.fill_rectangle(x, y,
                           w, h,
                           color565(50, 50, 50))
    display.draw_rectangle(x, y,
                           w, h,
                           color565(150, 150, 150))
    tone_list[i].append([x, y, x+w, y+h])
    
    # generate tone
    tone_list[i].append(generate_tone(tone_sample_rate,
                                      tone_list[i][0],   #freq
                                      tone_duration))
    print("Tone", i, ":", tone_list[i][0], "generated.",
          "len() =", len(tone_list[i][2]))
    display.fill_rectangle(x, y,
                           w, h,
                           color565(150, 150, 150))
    display.draw_rectangle(x, y,
                           w, h,
                           color565(255, 255, 255))
    str_freq = "{:.2f} Hz".format(tone_list[i][0])
    display.draw_text8x8(x+1, y+1,
                         str_freq,
                         color565(255, 255, 255),
                         background=color565(150, 150, 150),
                         rotate=90)
    i2s.write(tone_list[i][2])

display.draw_text8x8(5, 5,
                     "                              ",
                     color565(255, 255, 255),
                     background=color565(0, 0, 0)
                     )

touched = False
while True:
    p = touch.get_positions()
    
    if len(p) > 0:
        x = p[0][0]
        y = p[0][1]
        
        if touched == False:
            for i in range(len(tone_list)):
                if x >= tone_list[i][1][0] and \
                   x <= tone_list[i][1][2] and \
                   y >= tone_list[i][1][1] and \
                   y <= tone_list[i][1][3]:
                
                    i2s.write(tone_list[i][2])
                    print("touched within button", i, tone_list[i][0])
                    touched = True
                    break
    else:
        touched = False
    
    sleep(0.1)


It's found that sometimes false touched on LCD, so I add a simple debounce on touch.

mpy_s3_max98357_touchLCD_debounce.py
"""
ESP32-S3-DevKitC-1 running MicroPython v1.24.1
3.2" 320x240 IPS LCD (ILI9341 SPI) with Cap. Touch (FT6336) and Micro SD Slot
Touch LCD to play tone output to MAX98357, with debounce.

Libraries needed, install to /lib:
- ili9341.py
- ft6x36.py
"""
import os, sys
from time import sleep
from machine import Pin, SPI, I2C, I2S

from ili9341 import Display, color565

from ft6x36 import FT6x36

import math
import array

from micropython import const

print("====================================")
print(sys.implementation[0], os.uname()[3],
      "\nrun on", os.uname()[4])
print("====================================")

LCD_CS=14  # low to select
LCD_RST=13
LCD_RS=12
LCD_SDI=11 #MOSI
LCD_SCK=10
LCD_LED=9
LCD_SDO=8  #MISO

CTP_SCL=7
CTP_RST=6
CTP_SDA=5
CTP_INT=4  #Not used

SD_CS=18   # low to select

MAX98357_LRC = 17
MAX98357_BCLK = 16
MAX98357_DIN =15
#MAX98357_GAIN connect to GND

# un-select both LCD and SD in power-up,
# to prevent un-used cs unstable and false selected.
pin_cs_lcd = Pin(LCD_CS, Pin.OUT)
pin_cs_lcd.value(1)
pin_cs_sd = Pin(SD_CS, Pin.OUT)
pin_cs_sd.value(1)

pin_backlight=Pin(LCD_LED, Pin.OUT)
pin_backlight.value(0)

# output a negative pulse to CTP_RST pin, to reset FT6336U
pin_CTP_RST=Pin(CTP_RST, Pin.OUT)
pin_CTP_RST.value(1)
sleep(0.1)
pin_CTP_RST.value(0)
sleep(0.1)
pin_CTP_RST.value(1)
sleep(0.1)

# rot and rot_set
# are used to better manager rotation/width/height of display and touch
rot=1    # 0 rotation = 0
        # 1 rotation = 90
        # 2 rotation = 180
        # 3 rotation = 270
# rot_set[x][0] : ili9341 Display rotation
# rot_set[x][1] : ili9341 Display width
# rot_set[x][2] : ili9341 Display height
# rot_set[x][3] : ft6336U Touch rotation
# rot_set[x][4] : ft6336U Touch width
# rot_set[x][5] : ft6336U Touch height
rot_set = [[0, 240, 320, FT6x36.PORTRAIT_INVERTED, 240, 320],
           [90, 320, 240, FT6x36.LANDSCAPE_INVERTED, 240, 320],
           [180, 240, 320, FT6x36.PORTRAIT, 240, 320],
           [270, 320, 240, FT6x36.LANDSCAPE, 240, 320]]

# spi share by LCD and SD
spi = SPI(1, baudrate=40000000, sck=LCD_SCK, mosi=LCD_SDI, miso=LCD_SDO)

display = Display(spi,
                  rotation=rot_set[rot][0],
                  width=rot_set[rot][1], height=rot_set[rot][2],
                  dc=Pin(LCD_RS), cs=Pin(LCD_CS), rst=Pin(LCD_RST))
display.invert(enable=True)

# i2c
i2c = I2C(0, scl=Pin(CTP_SCL), sda=Pin(CTP_SDA))
touch = FT6x36(i2c,
               width=rot_set[rot][4],
               height=rot_set[rot][5],
               rotation=rot_set[rot][3],)

i2s = I2S(0,
          sck=Pin(MAX98357_BCLK),
          ws=Pin(MAX98357_LRC),
          sd=Pin(MAX98357_DIN),
          mode=I2S.TX,
          bits=16,
          format=I2S.MONO,
          rate=44100,
          ibuf=44100,
          )
#print("i2s:", i2s)

pin_backlight.value(1)
display.clear()
# draw a rectangle show the boundary of the screen
display.draw_rectangle(0, 0, display.width, display.height, color565(255, 255, 255))

def generate_tone(sample_rate, frequency, duration):
    sample_count = int(sample_rate * duration)
    amplitude = 14000 #32767
    wave = array.array("h", (0 for _ in range(sample_count)))
    for i in range(sample_count):
        wave[i] = int(amplitude * math.sin(2 * math.pi * frequency * i / sample_rate))
    return wave

num_of_tone = 7
tone_list = [[261.63],
             [293.66],
             [329.63],
             [349.23],
             [392.00],
             [440.00],
             [493.88]]

button_width = display.width // num_of_tone
button_height = 180
button_margin_x = 5
button_top = 50
button_bottom = button_top+button_height
button_pad_width = button_width - (2 * button_margin_x)

BUTTON_BACKGROUND_INIT = color565(40, 40, 40)
BUTTON_BORDER_INIT = color565(100, 100, 100)
BUTTON_BACKGROUND = color565(100, 100, 100)
BUTTON_BORDER_RELEASED = color565(150, 150, 150)
BUTTON_BORDER_PRESSED = color565(255, 255, 255)

tone_sample_rate = 44100
tone_duration = 0.5

display.draw_text8x8(5, 5,
                     "Generating tone, please wait!",
                     color565(255, 255, 255),
                     background=color565(0, 0, 0)
                     )
for i in range(num_of_tone):
    x = i * button_width + button_margin_x
    y = button_top
    w = button_pad_width
    h = button_height
    
    display.fill_rectangle(x, y,
                           w, h,
                           BUTTON_BACKGROUND_INIT)
    display.draw_rectangle(x, y,
                           w, h,
                           BUTTON_BORDER_INIT)
    tone_list[i].append([x, y, x+w, y+h])
    
    # generate tone
    tone_list[i].append(generate_tone(tone_sample_rate,
                                      tone_list[i][0],   #freq
                                      tone_duration))
    print("Tone", i, ":", tone_list[i][0], "generated.",
          "len() =", len(tone_list[i][2]))
    display.fill_rectangle(x, y,
                           w, h,
                           BUTTON_BACKGROUND)
    display.draw_rectangle(x, y,
                           w, h,
                           BUTTON_BORDER_RELEASED)
    str_freq = "{:.2f} Hz".format(tone_list[i][0])
    display.draw_text8x8(x+1, y+1,
                         str_freq,
                         color565(255, 255, 255),
                         BUTTON_BACKGROUND,
                         rotate=90)
    i2s.write(tone_list[i][2])

display.draw_text8x8(5, 5,
                     "                              ",
                     color565(255, 255, 255),
                     background=color565(0, 0, 0)
                     )

"""
Touch Debouncing

Here I assign touch_st as a state machine, in states of:
    TOUCH_ST_0_idle
    TOUCH_ST_1_touch_debouncing
    TOUCH_ST_2_touched
    TOUCH_ST_3_untouch_debouncing
"""
TOUCH_ST_0_idle = const(0)
TOUCH_ST_1_touch_debouncing = const(1)
TOUCH_ST_2_touched = const(2)
TOUCH_ST_3_untouch_debouncing = const(3)

def doState_0():
    #print("TOUCH_ST_0_idle")
    global touch_st
    
    if current_touched:
        # un-touched -> touched, go to debounce
        touch_st = TOUCH_ST_1_touch_debouncing
def doState_1():
    #print("TOUCH_ST_1_touch_debouncing")
    global touch_st
    global last_pressed_button
    
    if current_touched:
        # touched -> touched, within debouncing
        # Two consecutive touch will change from TOUCH_ST_0_idle to TOUCH_ST_2_touched,
        # otherwise back to TOUCH_ST_0_idle
        touch_x = touch_p[0][0]
        touch_y = touch_p[0][1]
        #print("Touch-Down:", touch_x, "-", touch_y)
        touch_st = TOUCH_ST_2_touched
        
        #check if touched on button
        for i in range(len(tone_list)):
            if touch_x >= tone_list[i][1][0] and \
               touch_x <= tone_list[i][1][2] and \
               touch_y >= tone_list[i][1][1] and \
               touch_y <= tone_list[i][1][3]:
                i2s.write(tone_list[i][2])
                print("touched within button", i, tone_list[i][0])
                display.draw_rectangle(tone_list[i][1][0], tone_list[i][1][1],
                                       button_pad_width, button_height,
                                       BUTTON_BORDER_PRESSED)
                last_pressed_button = i
                
                break
        
    else:
        touch_st = TOUCH_ST_0_idle
def doState_2():
    #print("TOUCH_ST_2_touched")
    global touch_st
    
    if current_touched:
        # touched -> touched,
        pass
    else:
        # touched - > un-touched
        touch_st = TOUCH_ST_3_untouch_debouncing
def doState_3():
    #print("TOUCH_ST_3_untouch_debouncing")
    global touch_st
    global last_pressed_button
    
    if current_touched:
        touch_st =TOUCH_ST_2_touched 
    else:
        #print("Touch-Up:")
        touch_st = TOUCH_ST_0_idle
        
        if last_pressed_button != None:
            display.draw_rectangle(tone_list[last_pressed_button][1][0], tone_list[last_pressed_button][1][1],
                                   button_pad_width, button_height,
                                   BUTTON_BORDER_RELEASED)
            last_pressed_button = None

touch_actions = [doState_0, doState_1, doState_2, doState_3]
touch_st = TOUCH_ST_0_idle
last_pressed_button = None
while True:
    
    touch_p = touch.get_positions()
    if len(touch_p) > 0:
        current_touched = True
    else:
        current_touched = False
    
    touch_actions[touch_st]()
    sleep(0.05)


Comments

Popular posts from this blog

480x320 TFT/ILI9488 SPI wih EP32C3 (arduino-esp32) using Arduino_GFX Library

Drive 320x240 ILI9341 SPI TFT using ESP32-S3 (NodeMCU ESP-S3-12K-Kit) using TFT_eSPI library, in Arduino Framework.