1. Output Devices
This week's assignment involves adding output devices to my custom-designed microcontroller board and programming them for functionality. For my final project "Smart Glasses", I need to integrate an LCD display and an audio playback module.
1.1. Audio Playback Module
Hardware Implementation
I selected the MAX98357A - an I2S protocol-based Class D audio amplifier with the following advantages:
- Integrated digital-to-analog converter (DAC)
- 3.2W output power at 4Ω load
- 92% power efficiency
Connection Diagram:
MAX98357A Pin | ESP32-S3 Pin |
---|---|
BCLK | GPIO2 |
LRC | GPIO3 |
DIN | GPIO1 |
GND | GND |
VIN | 3.3V |
Software Development
Core implementation steps:
- Configure I2S peripheral using ESP-IDF driver
- Implement audio buffer management
- Add WAV file decoding capability
Basic playback code:
#include <dirent.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2s_std.h"
#include "driver/gpio.h"
#include "esp_log.h"
#include "audio.h"
static i2s_chan_handle_t tx_handle = NULL;
void init_speaker(void)
{
i2s_chan_config_t tx_chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_AUTO, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&tx_chan_cfg, &tx_handle, NULL));
i2s_std_config_t tx_std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(8000),
.slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED, // some codecs may require mclk signal, this example doesn't need it
.bclk = 2,
.ws = 3,
.dout = 1,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_handle, &tx_std_cfg));
}
void app_main(void)
{
init_speaker();
while (1)
{
i2s_channel_enable(tx_handle);
i2s_channel_write(tx_handle, (uint8_t *)_acfablab, sizeof(_acfablab), NULL, 1000);
i2s_channel_disable(tx_handle);
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
Full implementation available at:
Demonstration:
Advanced Feature: Internet Radio
Implemented a streaming audio client using ESP32's WiFi capabilities:
#include "Arduino.h"
#include "WiFi.h"
#include "Audio.h"
#define I2S_DOUT 1
#define I2S_BCLK 2
#define I2S_LRC 3
Audio audio;
String ssid = "******";
String password = "******";
void setup() {
WiFi.disconnect();
WiFi.mode(WIFI_STA);
WiFi.begin(ssid.c_str(), password.c_str());
while (WiFi.status() != WL_CONNECTED){
delay(100);
Serial.print*(".");
}
delay(1500)
Serial.print("conneted");
audio.setPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
audio.setVolume(100);
audio.connecttohost("http://vis.media-ice.musicradio.com/CapitalMP3");
}
void loop()
{
audio.loop();
}
tip
Change the ssid and password to your own WiFi network.
Key Features:
- Automatic audio buffer management
- Bitrate adaptation
- Error correction for unstable networks
Streaming Demo:
1.2. LCD Display Module
Hardware Integration
Used a 0.85" GC9107-driven LCD with specifications:
- Resolution: 128×80 pixels
- Interface: SPI@20MHz
- Color Depth: 16-bit RGB (565 format)
- Power Consumption: 80mW typical
Connection Scheme:
LCD Pin | MCU Pin | Function |
---|---|---|
SCL | GPIO10 | SPI CLK |
SDA | GPIO11 | SPI MOSI |
DC | GPIO12 | Data/Cmd |
RST | GPIO13 | Reset |
CS | GPIO14 | Chip Select |
Driver Development
Created custom driver with these features:
- Double buffering implementation
- DMA-accelerated transfers
- LVGL integration for GUI development
Key driver functions:
#include "esp_timer.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "openGlasses.h"
static const char *TAG = "openGlasses";
static lv_disp_t *lvgl_disp = NULL;
static esp_lcd_panel_io_handle_t panel_io_handle = NULL;
static esp_lcd_panel_handle_t panel_handle = NULL;
void bsp_lvgl_rounder_cb(struct _lv_disp_drv_t *disp_drv, lv_area_t *area)
{
uint16_t x1 = area->x1;
uint16_t x2 = area->x2;
// round the start of coordinate down to the nearest 4M number
area->x1 = (x1 >> 2) << 2;
// round the end of coordinate up to the nearest 4N+3 number
area->x2 = ((x2 >> 2) << 2) + 3;
}
esp_err_t bsp_spi_bus_init(void)
{
static bool initialized = false;
if (initialized)
{
return ESP_OK;
}
const spi_bus_config_t spi_cfg = {
.mosi_io_num = BSP_SPI2_MOSI,
.miso_io_num = BSP_SPI2_MISO,
.sclk_io_num = BSP_SPI2_SCLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.isr_cpu_id = 0,
.max_transfer_sz = 4095,
};
BSP_ERROR_CHECK_RETURN_ERR(spi_bus_initialize(SPI2_HOST, &spi_cfg, SPI_DMA_CH_AUTO));
initialized = true;
return ESP_OK;
}
esp_err_t bsp_lcd_brightness_set(int brightness_percent)
{
if (brightness_percent > 100)
{
brightness_percent = 100;
}
if (brightness_percent < 0)
{
brightness_percent = 0;
}
ESP_LOGD(TAG, "Setting LCD backlight: %d%%", brightness_percent);
uint32_t duty_cycle = (BIT(BSP_LCD_LEDC_DUTY_RES) * (brightness_percent)) / 100;
BSP_ERROR_CHECK_RETURN_ERR(ledc_set_duty(LEDC_LOW_SPEED_MODE, BSP_LCD_LEDC_CH, duty_cycle));
BSP_ERROR_CHECK_RETURN_ERR(ledc_update_duty(LEDC_LOW_SPEED_MODE, BSP_LCD_LEDC_CH));
return ESP_OK;
}
static esp_err_t bsp_lcd_backlight_init()
{
const ledc_channel_config_t backlight_channel = { .gpio_num = BSP_LCD_BL,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = BSP_LCD_LEDC_CH,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LEDC_TIMER_1,
.duty = BIT(BSP_LCD_LEDC_DUTY_RES),
.hpoint = 0 };
const ledc_timer_config_t backlight_timer = { .speed_mode = LEDC_LOW_SPEED_MODE, .duty_resolution = BSP_LCD_LEDC_DUTY_RES, .timer_num = LEDC_TIMER_1, .freq_hz = 5000, .clk_cfg = LEDC_AUTO_CLK };
BSP_ERROR_CHECK_RETURN_ERR(ledc_timer_config(&backlight_timer));
BSP_ERROR_CHECK_RETURN_ERR(ledc_channel_config(&backlight_channel));
BSP_ERROR_CHECK_RETURN_ERR(bsp_lcd_brightness_set(100));
return ESP_OK;
}
static esp_err_t bsp_lcd_pannel_init(esp_lcd_panel_handle_t *ret_panel, esp_lcd_panel_io_handle_t *ret_io)
{
esp_err_t ret = ESP_OK;
ESP_LOGD(TAG, "Install panel IO");
const esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = BSP_LCD_DC,
.cs_gpio_num = BSP_LCD_CS,
.pclk_hz = BSP_LCD_PIXEL_CLK_HZ,
.lcd_cmd_bits = BSP_LCD_CMD_BITS,
.lcd_param_bits = BSP_LCD_PARAM_BITS,
.spi_mode = 0,
.trans_queue_depth = 10,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, ret_io), err, TAG, "New panel IO failed");
ESP_LOGD(TAG, "Install LCD driver");
const esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = BSP_LCD_RST, // Shared with Touch reset
.rgb_ele_order = BSP_LCD_RGB_ELEMENT_ORDER,
.bits_per_pixel = BSP_LCD_BITS_PER_PIXEL,
};
ESP_GOTO_ON_ERROR(esp_lcd_new_panel_gc9107(*ret_io, (const esp_lcd_panel_dev_config_t *)&panel_config, ret_panel), err, TAG, "New panel failed");
esp_lcd_panel_reset(*ret_panel);
esp_lcd_panel_init(*ret_panel);
esp_lcd_panel_invert_color(*ret_panel, true);
esp_lcd_panel_set_gap(*ret_panel, 2, 1);
return ESP_OK;
err:
if (*ret_panel)
{
esp_lcd_panel_del(*ret_panel);
}
if (*ret_io)
{
esp_lcd_panel_io_del(*ret_io);
}
spi_bus_free(BSP_LCD_SPI_NUM);
return ret;
}
static lv_disp_t *bsp_display_lcd_init(const bsp_display_cfg_t *cfg)
{
assert(cfg != NULL);
ESP_LOGD(TAG, "Initialize SPI bus");
if (bsp_spi_bus_init() != ESP_OK)
return NULL;
ESP_LOGD(TAG, "Initialize LCD panel");
if (bsp_lcd_pannel_init(&panel_handle, &panel_io_handle) != ESP_OK)
return NULL;
/* Add LCD screen */
ESP_LOGD(TAG, "Add LCD screen");
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = panel_io_handle,
.panel_handle = panel_handle,
.buffer_size = cfg->buffer_size,
.double_buffer = cfg->double_buffer,
.hres = BSP_LCD_H_RES,
.vres = BSP_LCD_V_RES,
.monochrome = false,
.rotation = {
.swap_xy = BSP_LCD_SWAP_XY,
.mirror_x = BSP_LCD_MIRROR_X,
.mirror_y = BSP_LCD_MIRROR_Y,
},
.flags = {
.buff_dma = cfg->flags.buff_dma,
.buff_spiram = cfg->flags.buff_spiram,
#if LVGL_VERSION_MAJOR == 9 && defined(CONFIG_LV_COLOR_16_SWAP)
.swap_bytes = true,
#endif
}};
return lvgl_port_add_disp(&disp_cfg);
}
lv_disp_t *bsp_lvgl_init(void)
{
#ifdef CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS
BSP_ERROR_CHECK_RETURN_NULL(heap_caps_register_failed_alloc_callback(heap_caps_alloc_failed_hook));
#endif
bsp_display_cfg_t cfg = { .lvgl_port_cfg = ESP_LVGL_PORT_INIT_CONFIG(),
.buffer_size = BSP_LCD_H_RES * LVGL_DRAW_BUFF_HEIGHT,
.double_buffer = LVGL_DRAW_BUFF_DOUBLE,
.flags = {
.buff_dma = false,
.buff_spiram = true,
} };
cfg.lvgl_port_cfg.task_priority = CONFIG_LVGL_PORT_TASK_PRIORITY;
cfg.lvgl_port_cfg.task_affinity = CONFIG_LVGL_PORT_TASK_AFFINITY;
cfg.lvgl_port_cfg.task_stack = CONFIG_LVGL_PORT_TASK_STACK_SIZE;
cfg.lvgl_port_cfg.task_max_sleep_ms = CONFIG_LVGL_PORT_TASK_MAX_SLEEP_MS;
cfg.lvgl_port_cfg.timer_period_ms = CONFIG_LVGL_PORT_TIMER_PERIOD_MS;
return bsp_lvgl_init_with_cfg(&cfg);
}
lv_disp_t *bsp_lvgl_init_with_cfg(const bsp_display_cfg_t *cfg)
{
if (lvgl_port_init(&cfg->lvgl_port_cfg) != ESP_OK)
return NULL;
if (bsp_lcd_backlight_init() != ESP_OK)
return NULL;
lvgl_disp = bsp_display_lcd_init(cfg);
if (lvgl_disp != NULL)
{
lvgl_disp->driver->rounder_cb = bsp_lvgl_rounder_cb;
}
return lvgl_disp;
}
lv_disp_t *bsp_lvgl_get_disp(void)
{
return lvgl_disp;
}
esp_lcd_panel_handle_t bsp_lcd_get_panel_handle(void)
{
return panel_handle;
}
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "openGlasses.h"
#include "lv_demos.h"
static const char *TAG = "app_main";
static lv_disp_t *lvgl_disp = NULL;
void app_main(void)
{
lvgl_disp = bsp_lvgl_init();
assert(lvgl_disp != NULL);
// Lock the mutex due to the LVGL APIs are not thread-safe
if (lvgl_port_lock(0))
{
lv_demo_benchmark(); /* A demo to measure the performance of LVGL or to compare different settings. */
lvgl_port_unlock();
}
while (1)
{
printf("free_heap_size = %ld\n", esp_get_free_heap_size());
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
Driver source code: