The MaTouch AI ESP32-S3 with 2.8-inch TFT ST7789V display by Makerfabs is a compact development board aimed at projects that combine wireless connectivity, graphics output, and on-board machine learning.
Built around the ESP32-S3 microcontroller, it integrates dual-core Xtensa LX7 processors, Wi-Fi, and Bluetooth Low Energy (BLE). The addition of a 2.8-inch 240×320 TFT display based on the ST7789V driver enables full-color graphics, making the board suitable for user interfaces, data visualization, and embedded applications that require real-time interaction.
Makerfabs has positioned the board as a versatile platform for developers who want to explore computer vision, audio recognition, and graphical interfaces without requiring external modules. In this tutorial you will learn how to get started with the MaTouch AI ESP32S3 2.8″ TFT ST7789V display.
Required Parts
You will need a MaTouch AI ESP32S3 2.8″ TFT ST7789V board and a USB-C cable to program the board and to try out the code examples.

MaTouch AI ESP32S3 2.8″ TFT ST7789V

USB C Cable
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Hardware of the MaTouch AI ESP32S3 2.8″ TFT ST7789V
The MaTouch ESP32-S3 2.8″ TFT is built on the ESP32-S3 microcontroller, combining a dual-core Xtensa® LX7 CPU running at up to 240 MHz with 16 MB of flash memory and 8 MB of PSRAM.
This hardware foundation ensures the board can handle complex tasks such as real-time audio processing, graphical rendering, and machine learning inference directly on-device. Wireless connectivity includes Wi-Fi 802.11 b/g/n and Bluetooth 5.0 LE, making the board versatile for IoT and connected applications.
The following photo shows the back of the board with the individual components labelled:

Display and Touch
The board features a 2.8-inch TFT LCD driven by the ST7789V controller with a resolution of 320 × 240 pixels (QVGA) and support for 65K colors. It connects via an SPI interface. A capacitive multi-touch panel (GT911) supports up to five simultaneous touch points, enabling intuitive user interfaces for control panels or data visualization. The picture below shows the front of the board with the display and the camera:

Audio Subsystem
Audio input is handled by dual INMP441 digital MEMS microphones, providing stereo sound capture for applications such as voice recognition or audio sensing. For output, the board integrates a MAX98357 I²S amplifier, delivering up to 3.2 W into a 4 Ω speaker, enabling direct playback of audio or voice responses without the need for an external DAC or amplifier.
Storage and Expansion
For data storage and logging, the board includes a MicroSD card slot supporting cards up to 32 GB. GPIO headers are available for additional peripherals such as sensors or actuators. The picture below shows the pinout of the two GPIO ports (J1 and J3) that are accessible on the back of the board:

Camera Support
The board integrates a camera interface compatible with the OV3660 camera. This makes it possible to implement computer vision applications such as image classification, object detection, or simple video capture when combined with the ESP32-S3’s AI acceleration capabilities.
Power Management
The board can be powered via a USB Type-C interface (4.0 V–5.25 V) and includes a TP4056 charging circuit for lithium-ion or lithium-polymer batteries. It also features a battery connector, a hardware switch, and a MAX17048 fuel gauge, allowing the system to monitor battery capacity and charging status.
Interfaces and Control
For development, the board provides both a USB-to-UART interface (CH340K) and native USB connectivity, giving flexibility in programming and debugging. Physical Boot and Reset buttons allow low-level control during firmware flashing or troubleshooting.
LED and Clock
A WS2812 RGB LED provides visual status indication and can be programmed for notifications or user feedback. And an RTC module (PCF8563T) ensures accurate timekeeping.
Summary of Technical Specifications
| Feature | Specification |
|---|---|
| Controller | ESP32-S3, Xtensa LX7 dual-core CPU, up to 240 MHz |
| Memory | 16 MB Flash, 8 MB PSRAM |
| Wireless | Wi-Fi 802.11 b/g/n, Bluetooth 5.0 LE |
| Display | 2.8″ TFT LCD, 320 × 240 (QVGA), ST7789V driver, SPI interface |
| Touch Panel | Capacitive, GT911, 5-point multi-touch |
| Audio Input | Dual-channel INMP441 digital MEMS microphones |
| Audio Output | MAX98357 I²S amplifier, 3.2 W @ 4 Ω |
| Storage | MicroSD card slot (up to 32 GB) |
| Camera | OV3660 interface supported |
| RGB LED | 1 × WS2812 programmable LED |
| RTC | PCF8563T real-time clock |
| Battery | Battery port with switch, TP4056 charger, MAX17048 fuel gauge |
| USB Interfaces | USB-to-UART (CH340K), native USB |
| Buttons | Boot and Reset |
| Power Supply | USB Type-C 5 V (4.0–5.25 V) |
| Expansion | 2x GPIO Ports |
You can find the schematics of the board under the following link:
Installation of ESP32 Core
If this is your first project with a board of the ESP32 series, you will need to install the ESP32 core first. If ESP32 boards are already installed in your Arduino IDE, you can skip this section.
Start by opening the Preferences dialog by selecting “Preferences…” from the “File” menu. This will open the Preferences dialog shown below.
Under the Settings tab you will find an edit box at the bottom of the dialog that is labelled “Additional boards manager URLs“:

In this input field copy the following URL:
https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json
This will let the Arduino IDE know, where to find the ESP32 core libraries. Next we will install the ESP32 boards using the Boards Manager.
Open the Boards Manager via “Tools -> Boards -> Board Manager”. You will see the Boards Manager appearing in the left Sidebar. Enter “ESP32” in the search field at the top and you should see two types of ESP32 boards; the “Arduino ESP32 Boards” and the “esp32 by Espressif” boards. We want the “esp32 libraries by Espressif”. Click on the INSTALL button and wait until the download and install is complete.

Selecting Board
Finally we need to select a ESP32 board. In case of the MaTouch AI ESP32S3 board, we pick the generic “ESP32S3 Dev Module”. For that, click on the drop-down menu and then on “Select other board and port…”:

This will open a dialog where you can enter “esp32s3 dev” in the search bar. You will see the “ESP32S3 Dev Module” board under Boards. Click on it and the COM port to activate it and then click OK:

Note that you need to connect the board via the USB cable to your computer, before you can select a COM port. The board has two USB ports, one native and one for TTL/UART. For serial communication with the board you need to use the port labelled “USB TTL”, which is the one closer to the corner:

Tool Settings
In the next sections you will find code examples for the various hardware components of the board. Some of them require considerable memory and you will need the following settings to make them work. You can find those settings under the Tools menu:

Most importantly Flash Size is set to 16MB(128MB), Partition Scheme is set to 16M Flash (3MB APP/9.9MB FATFS), PSRAM is set to OPI PSRAM. The other settings should be default settings.
Code Example: Serial Interface
We start by testing the Serial communication. Open your Arduino IDE, enter the following code and upload the code to the MaTouch AI ESP32S3.
void setup() {
Serial.begin(115200);
}
void loop() {
Serial.println("Makerguides");
delay(2000);
}
Then open the Serial Monitor and you should see the text “Makerguides” printed every two seconds. If not make sure that your USB cable is plugged into the correct port of the board.
Code Example: RGB LED
Next we try the on-board RGB LED. You will need to install the Adafruit_NeoPixel library to use this example. It simply changes the color of the RGB LED from red, to green, to blue every 200 milliseconds:
#include "Adafruit_NeoPixel.h"
const byte dataIn = 0;
Adafruit_NeoPixel pixels(1, dataIn, NEO_GRB + NEO_KHZ800);
void setup() {
pixels.begin();
pixels.clear();
pixels.setBrightness(50);
pixels.show();
}
void loop() {
pixels.setPixelColor(0, 255, 0, 0); // red
pixels.show();
delay(200);
pixels.setPixelColor(0, 0, 255, 0); // green
pixels.show();
delay(200);
pixels.setPixelColor(0, 0, 0, 255); // blue
pixels.show();
delay(200);
}
If you need more information on the WS2812 RGB LED and how to use it have a look at the LED Ring Clock with WS2812 and the Use WS2812B LED Strip with Arduino tutorials.
Code Example: GPIO
The board has four GPIO pins (IO4, IO5, IO6, IO7). To test the GPIO we connect an LED with a 220 Ohm resistor to GND and one of the GPIO pins. I am using IO4 in this example:

Now we can use the usual blink program to switch the LED at GPIO4 on and off:
#define LED_PIN 4
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(1000);
digitalWrite(LED_PIN, LOW);
delay(1000);
}
Code Example: Battery Charge Monitor
The MaTouch AI ESP32-S3 board comes with a connector for an external LiPo-battery and a charging circuit based on the TP4056:

In addition there is a MAX17048 fuel gauge that allows you to monitor the battery charge. It is connected via an I2C interface to SDA=39 and SCL=38:

The following code examples shows you how to use the built-in fuel gauge to measure the voltage and remaining capacity of the battery. Note that you will need to install the MAX17048 Library for this code to work.
#include "Wire.h"
#include "MAX17048.h"
#define SDA 39
#define SCL 38
MAX17048 power;
void setup() {
Serial.begin(115200);
Wire.begin(SDA, SCL);
power.attatch(Wire);
}
void loop() {
float volts = power.voltage();
int pcnt = power.percent();
Serial.printf("%.2fV (%d%%)\n", volts, pcnt);
delay(3000);
}
This code will print the current voltage and remaining capacity (in percent) of the battery to the Serial Monitor.
Code Example: Display and Touch Screen
The next code examples shows you how to use the display and the touch screen. It prints the text “Makerguides” in the center of the screen and if you touch the screen a little red circle is drawn at the touch point. See the example below:

Before you can run the following code you will need to install the Adafruit-GFX-Library, the Adafruit-ST7735-Library and the BitBank Capacitive Touch Sensor Library (bb_captouch). All of them can be installed via the Library Manager of the Arduino IDE.
Have a quick look at the code first and then we discuss some of the details.
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <bb_captouch.h>
#define TFT_BLK 45
#define TFT_RES -1
#define TFT_SCLK 48
#define TFT_MISO 12
#define TFT_MOSI 13
#define TFT_SD_CS 47
#define TFT_CS 40
#define TFT_DC 21
#define TOUCH_INT 14
#define TOUCH_SDA 39
#define TOUCH_SCL 38
#define TOUCH_RST 18
#define SCREEN_H 320
#define SCREEN_W 240
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RES);
BBCapTouch touch;
void setup() {
pinMode(TFT_BLK, OUTPUT);
digitalWrite(TFT_BLK, HIGH);
tft.init(SCREEN_W, SCREEN_H);
tft.setRotation(0);
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_YELLOW);
tft.setTextSize(2);
tft.setCursor(50, 150);
tft.println("Makerguides");
touch.init(TOUCH_SDA, TOUCH_SCL, TOUCH_RST, TOUCH_INT);
}
void loop() {
TOUCHINFO ti;
if (touch.getSamples(&ti)) {
int x = SCREEN_W - ti.x[0];
int y = SCREEN_H - ti.y[0];
tft.fillCircle(x, y, 5, ST77XX_RED);
}
}
Libraries
First we include the Adafruit graphics and display libraries, which provide drawing functions for the ST7789 controller, and the bb_captouch.h header, which gives access to the capacitive touch panel.
Constants
Next we define the pins used to connect the TFT display and the touch controller. Constants are assigned for the backlight pin, the display control pins, and the I²C pins used by the touch panel. The screen width and height are also defined to simplify later calculations.
Objects
Then an Adafruit_ST7789 object named tft is created, which represents the connection to the TFT screen. A BBCapTouch object named touch is also created, which will be used to read touch events from the capacitive sensor.
Setup
In the setup function, first the display backlight pin is configured as an output and switched on. The TFT display is then initialized to a resolution of 240 by 320 pixels. The rotation is set to zero, meaning the screen is used in portrait orientation. The entire screen is cleared to black, and the text color is set to yellow. The program sets a font size of two, moves the cursor to position (50, 150), and prints the text “Makerguides” onto the screen. After setting up the display, the capacitive touch controller is initialized with the correct I²C pins and reset pin.
Loop
In the loop function, we continuously check if a new touch event has been detected. If touch data is available, the first sample is read into variables x and y. These coordinates are then transformed so that they match the screen’s coordinate system by subtracting them from the screen width and height. Finally, we draw a red circle with a radius of five pixels at the touch location.
Code Example: Real Time Clock
The MaTouch AI ESP32-S3 board contains a PCF8563T to provide a Real Time Clock (RTC). See the schematics below:

In this example we will use the RTC to keep time and show the current time on the TFT display. It will look like this:

You will need to install the RTCLib for the following code to work. Have a quick look at the code first and then we discuss some of the details:
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <RTClib.h>
#include <Wire.h>
#define TFT_BLK 45
#define TFT_RES -1
#define TFT_SCLK 48
#define TFT_MISO 12
#define TFT_MOSI 13
#define TFT_CS 40
#define TFT_DC 21
#define SCREEN_W 240
#define SCREEN_H 320
// RTC pins
#define RTC_SCL 38
#define RTC_SDA 39
#define RTC_INT 15
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RES);
RTC_PCF8563 rtc_pcf;
void rtc_pcf_init() {
if (!rtc_pcf.begin()) {
Serial.println("Couldn't find RTC");
Serial.flush();
while (1) delay(10);
}
rtc_pcf.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
void setup() {
Serial.begin(115200);
pinMode(TFT_BLK, OUTPUT);
digitalWrite(TFT_BLK, HIGH);
tft.init(SCREEN_W, SCREEN_H);
tft.setRotation(0);
tft.fillScreen(ST77XX_BLACK);
tft.setTextColor(ST77XX_YELLOW, ST77XX_BLACK);
tft.setTextSize(4);
Wire.begin(RTC_SDA, RTC_SCL);
rtc_pcf_init();
}
void loop() {
static char buf[16];
DateTime now = rtc_pcf.now();
sprintf(buf, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
tft.setCursor(20, 130);
tft.print(buf);
delay(500);
}
Libraries
We begin by including the Adafruit graphics and display libraries for drawing to the screen, the RTClib library for accessing the real-time clock, and the Wire library for I²C communication.
Constants
Next, the pins for the TFT display are defined, including the chip select, data/command, and SPI pins. The screen width and height are also defined. The pins for the RTC are listed as well, with RTC_SDA and RTC_SCL assigned to the I²C data and clock lines, and RTC_INT defined but not used in this program.
Objects
Then we create an Adafruit_ST7789 object called tft to represent the display. The RTC_PCF8563 object called rtc_pcf is created to communicate with the real-time clock.
rtc_pcf_init
The function rtc_pcf_init() attempts to initialize the RTC. If the device cannot be found on the I²C bus, the program prints an error message and stops. If the RTC is present, its time is set to the compile date and time of the program using rtc_pcf.adjust(). This ensures that the clock starts with a known reference point.
Setup
In the setup function, serial communication is started for debugging. The TFT backlight pin is enabled, and the display is initialized to 240 by 320 pixels. The rotation is set to zero, the screen is cleared to black, and the text color is set to yellow on a black background. The text size is made larger for easier reading. The I²C interface is initialized with the defined SDA and SCL pins, and the RTC is started with rtc_pcf_init().
Loop
The loop function runs repeatedly and retrieves the current time from the RTC with rtc_pcf.now(). The time is formatted as a string in the form “HH:MM:SS” using sprintf, and stored in a buffer. The cursor is positioned at coordinates (20, 130), which is near the center of the screen, and the time string is drawn on the display in yellow. The loop then waits for half a second before repeating, so the display updates twice per second.
In summary, this code initializes a TFT display and a PCF8563 real-time clock, sets the clock to the compile time, and then continuously displays the current time in hours, minutes, and seconds in the middle of the screen.
Code Example: Play Audio File
The MaTouch AI ESP32-S3 board has a built-in MAX98357A Class D amplifier to drive a small speaker with 3.2W at 4Ω.

In the following code example we play a WAV file stored on the SD card:
#include <driver/i2s_std.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#define SCLK 48
#define MISO 12
#define MOSI 13
#define SD_CS 47
// Speaker pins
#define I2S_OUT_BCLK 20
#define I2S_OUT_LRC 1
#define I2S_OUT_DOUT 19
#define SAMPLE_RATE 16000U
static i2s_chan_handle_t tx_chan;
void SpeakerInit() {
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &tx_chan, NULL));
// Standard mode, 16-bit mono
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = (gpio_num_t)I2S_OUT_BCLK,
.ws = (gpio_num_t)I2S_OUT_LRC,
.dout = (gpio_num_t)I2S_OUT_DOUT,
.din = I2S_GPIO_UNUSED,
},
};
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_LEFT; // left channel only
ESP_ERROR_CHECK(i2s_channel_init_std_mode(tx_chan, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(tx_chan));
}
void playWav(const char *filename) {
File file = SD.open(filename);
if (!file || file.size() <= 44) {
Serial.println("Invalid or missing WAV file.");
return;
}
file.seek(44); // skip header
uint8_t buffer[1024];
size_t bytesRead, bytesWritten;
while ((bytesRead = file.read(buffer, sizeof(buffer))) > 0) {
// optional ×2 volume boost
for (int i = 0; i < bytesRead; i += 2) {
int16_t *s = (int16_t *)&buffer[i];
*s <<= 1;
}
i2s_channel_write(tx_chan, buffer, bytesRead, &bytesWritten, portMAX_DELAY);
}
file.close();
i2s_channel_disable(tx_chan);
Serial.println("Playback finished.");
}
void setup() {
Serial.begin(115200);
SPI.begin(SCLK, MISO, MOSI);
if (!SD.begin(SD_CS, SPI, 80'000'000)) {
Serial.println(F("ERROR: SD mount failed!"));
return;
}
SpeakerInit();
delay(500);
playWav("/LightMusic.wav");
}
void loop() {}
Libraries
At the top of the code, the required libraries are included. The I²S driver is used to communicate with an external digital audio chip, the SD and SPI libraries provide access to the SD card, and the FS library defines the file system interface.
Constants
Pin numbers are then defined for the SPI bus that connects to the SD card, and for the I²S signals that connect to the audio output hardware. The sample rate is set to 16,000 samples per second, which defines the playback speed of the audio. A handle for the I²S transmit channel is declared so that audio data can be sent to the speaker.
SpeakerInit
The function SpeakerInit() configures and enables the I²S output. First, it creates a default channel configuration for I²S channel 1 operating in master mode. It then initializes a new channel, saving the transmit handle in tx_chan.
Next, it sets up a standard I²S configuration: the clock is defined for the 16 kHz sample rate, the slot configuration specifies 16-bit mono audio in Philips I²S format, and the GPIO configuration assigns the correct pins for bit clock, word select, and data output. The slot mask is restricted to the left channel only, since mono audio is being played. Finally, the channel is initialized in standard mode and enabled, making the hardware ready for audio playback.
playWav
The function playWav(const char *filename) opens a WAV file from the SD card. It checks whether the file exists and is larger than 44 bytes, because the first 44 bytes of a WAV file are the header and do not contain actual sound data.
If the file is valid, the program skips the header with file.seek(44). It then repeatedly reads blocks of 1024 bytes into a buffer. Before writing each block to the I²S interface, the samples are optionally amplified: each 16-bit signed sample is left-shifted by one bit, effectively doubling its amplitude.
The processed audio data is then written to the I²S transmit channel using i2s_channel_write(). When the end of the file is reached, the file is closed, the I²S channel is disabled, and a message is printed to indicate that playback has finished.
Setup
In the setup() function, the serial port is initialized for debugging. The SPI bus is started on the specified pins, and the SD card is mounted. If mounting fails, an error is printed and the program stops. Otherwise, the I²S speaker is initialized, a short delay ensures stability, and the function playWav("/LightMusic.wav") is called to begin playback of the file stored on the SD card.
Loop
The loop() function is left empty, since all the action occurs once in setup().
In summary, this code configures the ESP32-S3 to play a WAV file stored on the SD card through an I²S-connected speaker. It sets up the SD card and I²S output, skips the file’s header, streams the raw audio data to the I²S hardware, and produces audible sound from the stored audio track.
Code Example: Record Audio
The MaTouch AI ESP32-S3 board comes with two INMP441 digital-output, microphones. See the schematics below:

In the following code example we record 10 seconds of audio from the microphone and store it as a WAV file on the SD Card:
#include <driver/i2s_std.h>
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#define SCLK 48
#define MISO 12
#define MOSI 13
#define SD_CS 47
// Microphone pins
#define I2S_IN_BCLK 42
#define I2S_IN_LRC 2
#define I2S_IN_DIN 41
#define SAMPLE_RATE 16000U
#define SAMPLE_BITS 16
#define RECORD_TIME 10 // seconds
static i2s_chan_handle_t rx_chan;
void writeWavHeader(File &file, uint32_t dataSize) {
uint32_t chunkSize = 36 + dataSize;
uint16_t audioFormat = 1; // PCM
uint16_t numChannels = 1;
uint32_t sampleRate = SAMPLE_RATE; // copy macro into variable
uint16_t bitsPerSample = SAMPLE_BITS; // copy macro into variable
uint32_t byteRate = sampleRate * numChannels * bitsPerSample / 8;
uint16_t blockAlign = numChannels * bitsPerSample / 8;
file.seek(0);
file.write((const uint8_t *)"RIFF", 4);
file.write((uint8_t *)&chunkSize, 4);
file.write((const uint8_t *)"WAVE", 4);
file.write((const uint8_t *)"fmt ", 4);
uint32_t subChunk1Size = 16;
file.write((uint8_t *)&subChunk1Size, 4);
file.write((uint8_t *)&audioFormat, 2);
file.write((uint8_t *)&numChannels, 2);
file.write((uint8_t *)&sampleRate, 4);
file.write((uint8_t *)&byteRate, 4);
file.write((uint8_t *)&blockAlign, 2);
file.write((uint8_t *)&bitsPerSample, 2);
file.write((const uint8_t *)"data", 4);
file.write((uint8_t *)&dataSize, 4);
}
void MicInit() {
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &rx_chan)); // RX channel only
i2s_std_config_t std_cfg = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_MONO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = (gpio_num_t)I2S_IN_BCLK,
.ws = (gpio_num_t)I2S_IN_LRC,
.dout = I2S_GPIO_UNUSED,
.din = (gpio_num_t)I2S_IN_DIN,
},
};
std_cfg.slot_cfg.slot_mask = I2S_STD_SLOT_RIGHT;
ESP_ERROR_CHECK(i2s_channel_init_std_mode(rx_chan, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(rx_chan));
}
void recordWav(const char *filename) {
File file = SD.open(filename, FILE_WRITE);
if (!file) {
Serial.println("Failed to open file for writing!");
return;
}
// Reserve header space
for (int i = 0; i < 44; i++) file.write((uint8_t)0);
const size_t bufferSize = 1024;
uint8_t buffer[bufferSize];
size_t bytesRead;
uint32_t totalBytes = 0;
uint32_t startMs = millis();
while ((millis() - startMs) < RECORD_TIME * 1000) {
if (i2s_channel_read(rx_chan, buffer, bufferSize, &bytesRead, portMAX_DELAY) == ESP_OK) {
file.write(buffer, bytesRead);
totalBytes += bytesRead;
}
}
// Write real header
writeWavHeader(file, totalBytes);
file.close();
Serial.printf("Recording finished: %s (%lu bytes)\n", filename, (unsigned long)totalBytes);
}
void setup() {
Serial.begin(115200);
SPI.begin(SCLK, MISO, MOSI);
if (!SD.begin(SD_CS, SPI, 80'000'000)) {
Serial.println("SD mount failed!");
return;
}
MicInit();
delay(2000);
Serial.println("Recording...");
recordWav("/mic_record.wav");
}
void loop() {}
Libraries
At the beginning, the required libraries are included. The I²S driver provides access to the audio hardware interface, the FS and SD libraries allow files to be created and written on the SD card, and the SPI library handles communication with the SD card.
Constants
Constants are then defined for the SPI pins used by the SD card and for the I²S pins connected to the digital microphone. The sample rate is set to 16 kHz, the resolution to 16 bits per sample, and the recording length to ten seconds. A handle for the I²S receive channel is declared so that microphone data can be captured.
writeWavHeader
The function writeWavHeader() writes a proper 44-byte WAV header at the beginning of the file. It constructs the RIFF container, specifies the audio format as PCM, defines one channel for mono audio, and sets the sample rate, bit depth, and byte rate. It then writes the “data” section header followed by the total size of the audio samples. This ensures that the resulting file can be played back by any standard audio software.
MicInit
The function MicInit() configures the I²S interface to receive audio from the microphone. It creates a new I²S channel in master mode with default settings. The standard configuration specifies a clock for the 16 kHz sample rate, Philips I²S slot format with 16-bit samples, and mono channel mode. The GPIO configuration assigns the correct pins for bit clock, word select, and data input, while leaving the master clock and data output unused. Finally, the channel is initialized in standard mode and enabled, so that it is ready to capture audio samples.
recordWav
The function recordWav(const char *filename) performs the actual recording. It opens a file on the SD card for writing. If the file cannot be created, it reports an error and exits. To leave space for the WAV header, it writes 44 zero bytes at the start of the file. It then allocates a buffer of 1024 bytes. A loop runs for the duration defined by RECORD_TIME. During this period, audio data is read from the I²S channel into the buffer and written directly to the file. A running total of the number of audio bytes recorded is maintained. After the recording period ends, the function rewinds to the beginning of the file and writes the real WAV header with the correct sizes. The file is then closed, and a message is printed with the final file size.
Setup
In the setup() function, the serial port is started for debugging. The SPI bus is initialized on the given pins, and the SD card is mounted. If mounting fails, the program reports an error and does not continue. If successful, the microphone is initialized, a short delay gives the hardware time to settle, and a message announces the start of recording. The function recordWav("/mic_record.wav") is then called to capture and save ten seconds of audio to the SD card.
Loop
The loop() function is empty, since the recording task is performed only once at startup.
In summary, this program initializes the SD card and the I²S microphone, records ten seconds of audio at 16 kHz into a buffer, writes the data to the SD card, adds a proper WAV header, and produces a standard audio file that can be played back on any device.
Code Example: Camera and Display
In this last example we capture live video from the camera module connected to the ESP32-S3 and display it directly on the TFT screen.
For this example you will need to install the Arduino_GFX library by moononournation. Open the LIBRARY MANAGER, type “GFX Library for Arduino” into the search bar, find the “GFX Library for Arduino” by Moon, and click the INSTALL button:

Below is the complete code to stream video from the camera to the display. Have a quick look first, and then we will discuss the details:
#include <Arduino_GFX_Library.h>
#include "esp_camera.h"
// === Camera pins ===
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 9 // CSI_MCLK
#define SIOD_GPIO_NUM 39 // TWI_SDA
#define SIOC_GPIO_NUM 38 // TWI_SCK
#define Y9_GPIO_NUM 46 // CSI D7
#define Y8_GPIO_NUM 3 // CSI D6
#define Y7_GPIO_NUM 8 // CSI D5
#define Y6_GPIO_NUM 16 // CSI D4
#define Y5_GPIO_NUM 6 // CSI D3
#define Y4_GPIO_NUM 4 // CSI D2
#define Y3_GPIO_NUM 5 // CSI D1
#define Y2_GPIO_NUM 7 // CSI D0
#define VSYNC_GPIO_NUM 11 // CSI VSYNC
#define HREF_GPIO_NUM 10 // CSI HSYNC
#define PCLK_GPIO_NUM 17 // CSI PCLK
// === TFT pins ===
#define TFT_BLK 45
#define TFT_RES -1
#define TFT_CS 40
#define TFT_DC 21
#define MOSI 13
#define MISO 12
#define SCLK 48
Arduino_ESP32SPI *bus = new Arduino_ESP32SPI(
TFT_DC, TFT_CS, SCLK, MOSI, MISO, HSPI, true
);
Arduino_GFX *gfx = new Arduino_ST7789(
bus, TFT_RES, 1 , true
);
// === Camera init ===
void camera_init_s3() {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_QVGA; // 320x240
config.pixel_format = PIXFORMAT_RGB565; // direct TFT compatible
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 2;
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x\n", err);
return;
}
sensor_t *s = esp_camera_sensor_get();
if (s->id.PID == OV3660_PID) {
s->set_hmirror(s, 1);
s->set_vflip(s, 1);
}
}
void setup() {
Serial.begin(115200);
pinMode(TFT_BLK, OUTPUT);
digitalWrite(TFT_BLK, HIGH);
gfx->begin();
gfx->fillScreen(BLACK);
camera_init_s3();
}
void loop() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
delay(100);
return;
}
gfx->draw16bitBeRGBBitmap(0, 0, (uint16_t *)fb->buf, fb->width, fb->height);
esp_camera_fb_return(fb);
}
Libraries
At the beginning, we include the libraries for the display (Arduino_GFX_Library.h) and the camera (esp_camera.h).
Constants
The first section defines the hardware connections. The camera pins are listed for the parallel interface: eight data pins (Y2–Y9), clock and synchronization pins (XCLK, PCLK, HREF, and VSYNC), and the I²C pins (SIOD and SIOC) that configure the camera sensor. The TFT display pins are also defined for chip select, data/command, SPI communication lines, and the backlight.
Objects
Two objects are then created for the display. The first, bus, represents the SPI connection to the display, and the second, gfx, represents the ST7789 controller itself. These objects come from the Arduino GFX library, which provides efficient graphics functions for many displays.
camera_init_s3
The function camera_init_s3() configures and initializes the camera. A camera_config_t structure is filled with the correct pin assignments and parameters. The external clock for the camera is set to 20 MHz. The frame size is set to FRAMESIZE_QVGA, which corresponds to 320×240 pixels, matching the TFT resolution.
The pixel format is set to PIXFORMAT_RGB565, which produces a 16-bit color format that the TFT can use directly without conversion. The grab mode is set to capture frames whenever the buffer is empty, frame buffers are stored in PSRAM, and two frame buffers are used to improve performance.
After filling in the configuration, esp_camera_init() starts the camera driver. If initialization fails, an error code is printed. If the sensor is an OV3660, some extra configuration is applied to adjust the orientation and brightness.
Setup
In the setup() function, the serial port is started for debugging. The TFT backlight is enabled, and the display is initialized. The screen is cleared to black before the camera is started by calling camera_init_s3().
Loop
The loop() function repeatedly captures and displays frames. Each call to esp_camera_fb_get() retrieves a pointer to the latest frame buffer. If the capture fails, a message is printed and the program waits briefly before trying again. If successful, the frame buffer is drawn onto the TFT using gfx->draw16bitBeRGBBitmap(). This function directly transfers the RGB565 pixel data from the camera buffer to the display at position (0,0). Once the frame has been displayed, esp_camera_fb_return(fb) is called to release the buffer back to the driver so that it can be reused.
In summary, this program initializes the ESP32-S3 camera and the ST7789 TFT display, captures frames in RGB565 format, and streams them directly to the screen. The result is a live camera preview displayed in real time on the TFT.
Conclusions
This tutorial provided you with code examples to get started with the MaTouch AI ESP32-S3 with 2.8-inch TFT ST7789V display.
For more examples see Makerfabs’s Github repo for the Matouch display and the Wiki Page. Note, however, that most of code examples there use the lvgl library, which I avoided here to keep the complexity low. Furthermore, some of the examples don’t work with the current 3.x core.
If you have any questions feel free to leave them in the comment section.
Happy Tinkering 😉
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.

