docs/custom-board.md
This guide describes how to add a new board to the XiaoZhi AI voice assistant project. XiaoZhi AI supports 70+ ESP32-series boards; each one lives in its own directory under main/boards/.
Warning: for a custom board whose IO configuration differs from an existing board, never overwrite the original board's configuration. Always create a new board type - or use the
buildsarray inconfig.jsonto produce a distinct firmware name with differentsdkconfigmacros. Usepython scripts/release.py [board-directory]to build and package the firmware.Overwriting an existing board's configuration is dangerous because OTA updates may replace your custom firmware with the stock firmware for the original board. Every board must have a unique identity and its own firmware update channel.
A board directory typically contains:
xxx_board.cc - board-level initialization and glue code.config.h - pin assignments and board-level settings.config.json - build configuration consumed by scripts/release.py.README.md - board-specific notes.Boards can live directly under main/boards/ or be grouped by manufacturer under main/boards/<manufacturer>/<board>/ (see Manufacturer Sub-directories below).
Create a new directory under main/boards/ using the [vendor]-[model] naming style (e.g. m5stack-tab5):
mkdir main/boards/my-custom-board
Define all hardware settings in config.h:
Example (from lichuang-c3-dev):
#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_
#include <driver/gpio.h>
// Audio
#define AUDIO_INPUT_SAMPLE_RATE 24000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_I2S_GPIO_MCLK GPIO_NUM_10
#define AUDIO_I2S_GPIO_WS GPIO_NUM_12
#define AUDIO_I2S_GPIO_BCLK GPIO_NUM_8
#define AUDIO_I2S_GPIO_DIN GPIO_NUM_7
#define AUDIO_I2S_GPIO_DOUT GPIO_NUM_11
#define AUDIO_CODEC_PA_PIN GPIO_NUM_13
#define AUDIO_CODEC_I2C_SDA_PIN GPIO_NUM_0
#define AUDIO_CODEC_I2C_SCL_PIN GPIO_NUM_1
#define AUDIO_CODEC_ES8311_ADDR ES8311_CODEC_DEFAULT_ADDR
// Buttons
#define BOOT_BUTTON_GPIO GPIO_NUM_9
// Display
#define DISPLAY_SPI_SCK_PIN GPIO_NUM_3
#define DISPLAY_SPI_MOSI_PIN GPIO_NUM_5
#define DISPLAY_DC_PIN GPIO_NUM_6
#define DISPLAY_SPI_CS_PIN GPIO_NUM_4
#define DISPLAY_WIDTH 320
#define DISPLAY_HEIGHT 240
#define DISPLAY_MIRROR_X true
#define DISPLAY_MIRROR_Y false
#define DISPLAY_SWAP_XY true
#define DISPLAY_OFFSET_X 0
#define DISPLAY_OFFSET_Y 0
#define DISPLAY_BACKLIGHT_PIN GPIO_NUM_2
#define DISPLAY_BACKLIGHT_OUTPUT_INVERT true
#endif // _BOARD_CONFIG_H_
config.json drives scripts/release.py:
{
"target": "esp32s3",
"builds": [
{
"name": "my-custom-board",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
]
}
]
}
Fields:
target: target chip, must match the real hardware (esp32, esp32s3, esp32c3, esp32c6, esp32p4, ...).name: firmware package name; typically matches the directory name.sdkconfig_append: extra sdkconfig lines merged into the defaults.Common sdkconfig_append entries:
// Flash size
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y"
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y"
"CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y"
// Partition table
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\""
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/16m.csv\""
// Language
"CONFIG_LANGUAGE_EN_US=y"
"CONFIG_LANGUAGE_ZH_CN=y"
// Wake word configuration
"CONFIG_USE_DEVICE_AEC=y" // enable on-device AEC
"CONFIG_WAKE_WORD_DISABLED=y" // disable wake word detection
Create my_custom_board.cc containing the board-level implementation.
A basic board class has:
WifiBoard or Ml307Board.GetAudioCodec(), GetDisplay(), GetBacklight(), ...DECLARE_BOARD(ClassName).#include "wifi_board.h"
#include "codecs/es8311_audio_codec.h"
#include "display/lcd_display.h"
#include "application.h"
#include "button.h"
#include "config.h"
#include "mcp_server.h"
#include <esp_log.h>
#include <driver/i2c_master.h>
#include <driver/spi_common.h>
#define TAG "MyCustomBoard"
class MyCustomBoard : public WifiBoard {
private:
i2c_master_bus_handle_t codec_i2c_bus_;
Button boot_button_;
LcdDisplay* display_;
void InitializeI2c() {
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = I2C_NUM_0,
.sda_io_num = AUDIO_CODEC_I2C_SDA_PIN,
.scl_io_num = AUDIO_CODEC_I2C_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags = {
.enable_internal_pullup = 1,
},
};
ESP_ERROR_CHECK(i2c_new_master_bus(&i2c_bus_cfg, &codec_i2c_bus_));
}
void InitializeSpi() {
spi_bus_config_t buscfg = {};
buscfg.mosi_io_num = DISPLAY_SPI_MOSI_PIN;
buscfg.miso_io_num = GPIO_NUM_NC;
buscfg.sclk_io_num = DISPLAY_SPI_SCK_PIN;
buscfg.quadwp_io_num = GPIO_NUM_NC;
buscfg.quadhd_io_num = GPIO_NUM_NC;
buscfg.max_transfer_sz = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t);
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO));
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState();
});
}
void InitializeDisplay() {
esp_lcd_panel_io_handle_t panel_io = nullptr;
esp_lcd_panel_handle_t panel = nullptr;
esp_lcd_panel_io_spi_config_t io_config = {};
io_config.cs_gpio_num = DISPLAY_SPI_CS_PIN;
io_config.dc_gpio_num = DISPLAY_DC_PIN;
io_config.spi_mode = 2;
io_config.pclk_hz = 80 * 1000 * 1000;
io_config.trans_queue_depth = 10;
io_config.lcd_cmd_bits = 8;
io_config.lcd_param_bits = 8;
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi(SPI2_HOST, &io_config, &panel_io));
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = GPIO_NUM_NC;
panel_config.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB;
panel_config.bits_per_pixel = 16;
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(panel_io, &panel_config, &panel));
esp_lcd_panel_reset(panel);
esp_lcd_panel_init(panel);
esp_lcd_panel_invert_color(panel, true);
esp_lcd_panel_swap_xy(panel, DISPLAY_SWAP_XY);
esp_lcd_panel_mirror(panel, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
display_ = new SpiLcdDisplay(panel_io, panel,
DISPLAY_WIDTH, DISPLAY_HEIGHT,
DISPLAY_OFFSET_X, DISPLAY_OFFSET_Y,
DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y, DISPLAY_SWAP_XY);
}
void InitializeTools() {
// Register MCP tools here; see docs/mcp-usage.md.
}
public:
MyCustomBoard() : boot_button_(BOOT_BUTTON_GPIO) {
InitializeI2c();
InitializeSpi();
InitializeDisplay();
InitializeButtons();
InitializeTools();
GetBacklight()->SetBrightness(100);
}
virtual AudioCodec* GetAudioCodec() override {
static Es8311AudioCodec audio_codec(
codec_i2c_bus_,
I2C_NUM_0,
AUDIO_INPUT_SAMPLE_RATE,
AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_GPIO_MCLK,
AUDIO_I2S_GPIO_BCLK,
AUDIO_I2S_GPIO_WS,
AUDIO_I2S_GPIO_DOUT,
AUDIO_I2S_GPIO_DIN,
AUDIO_CODEC_PA_PIN,
AUDIO_CODEC_ES8311_ADDR);
return &audio_codec;
}
virtual Display* GetDisplay() override {
return display_;
}
virtual Backlight* GetBacklight() override {
static PwmBacklight backlight(DISPLAY_BACKLIGHT_PIN, DISPLAY_BACKLIGHT_OUTPUT_INVERT);
return &backlight;
}
};
DECLARE_BOARD(MyCustomBoard);
In main/Kconfig.projbuild, add an entry to the choice BOARD_TYPE block:
choice BOARD_TYPE
prompt "Board Type"
default BOARD_TYPE_BREAD_COMPACT_WIFI
help
Board type.
# ... other entries ...
config BOARD_TYPE_MY_CUSTOM_BOARD
bool "My Custom Board"
depends on IDF_TARGET_ESP32S3 # pick the matching target
endchoice
Notes:
depends on restricts the entry to the correct target (IDF_TARGET_ESP32S3, IDF_TARGET_ESP32C3, ...).Open main/CMakeLists.txt and extend the board-type chain:
elseif(CONFIG_BOARD_TYPE_MY_CUSTOM_BOARD)
set(BOARD_TYPE "my-custom-board") # must match the directory name
set(BUILTIN_TEXT_FONT font_puhui_basic_20_4) # pick a font for the display
set(BUILTIN_ICON_FONT font_awesome_20_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64) // optional, for emoji display
Font and emoji guidance:
Pick a font size that matches the display resolution:
font_puhui_basic_14_1 / font_awesome_14_1font_puhui_basic_16_4 / font_awesome_16_4font_puhui_basic_20_4 / font_awesome_20_4font_puhui_basic_30_4 / font_awesome_30_4Emoji collections:
twemoji_32 - 32x32 pixels (small screens).twemoji_64 - 64x64 pixels (large screens).idf.py manuallySet the target chip (first time, or when switching targets):
idf.py set-target esp32s3 # ESP32-S3
idf.py set-target esp32c3 # ESP32-C3
idf.py set-target esp32 # ESP32
Clean stale configuration:
idf.py fullclean
Select the board via menuconfig:
idf.py menuconfig
Navigate to Xiaozhi Assistant -> Board Type and choose your board.
Build and flash:
idf.py build
idf.py flash monitor
release.py (recommended)If the board directory contains a config.json, you can build and package automatically:
python scripts/release.py my-custom-board
The script:
target from config.json and calls idf.py set-target.sdkconfig_append.In README.md, describe the board, hardware requirements, build instructions, and any special notes.
Boards can be grouped by manufacturer under main/boards/<manufacturer>/<board>/. This is the recommended layout when a single vendor ships several variants - for example main/boards/waveshare/esp32-p4-nano/ or main/boards/lceda-course-examples/eda-tv-pro/.
To enable the layout, set the MANUFACTURER variable in main/CMakeLists.txt for your board:
elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_NANO)
set(MANUFACTURER "waveshare")
set(BOARD_TYPE "esp32-p4-nano")
set(BUILTIN_TEXT_FONT font_puhui_basic_30_4)
set(BUILTIN_ICON_FONT font_awesome_30_4)
set(DEFAULT_EMOJI_COLLECTION twemoji_64)
When MANUFACTURER is set, the build system globs source files from main/boards/${MANUFACTURER}/${BOARD_TYPE}/. When it is empty, it falls back to the flat main/boards/${BOARD_TYPE}/ layout.
Rules of thumb:
waveshare, lceda-course-examples).Several reusable components live in main/boards/common/. You can include them directly from your board class:
Supported LCD families include:
Es8311AudioCodec (most common)Es8374AudioCodecEs8388AudioCodecEs8389AudioCodecBoxAudioCodec (ES7210 mic array + codec combo used on ESP-Box boards)NoAudioCodec (direct I2S without external codec)DummyAudioCodec (placeholder for boards without audio)Axp2101 power management IC helpers.Sy6970 battery charger helpers.AdcBatteryMonitor - simple ADC-based battery voltage monitor.PowerSaveTimer / SleepTimer - helpers for light-sleep scheduling.WifiBoard - WiFi-only base class.Ml307Board / Nt26Board - 4G modem base classes.DualNetworkBoard - switchable WiFi / 4G base class.RndisBoard - RNDIS-over-USB networking (ESP32-S3 / ESP32-P4).EspVideo helpers for ESP-Video on ESP32-S3 / ESP32-P4.Button - standard push buttons (click, long-press, multi-click).Knob - rotary encoder wrapper.PressToTalkMcpTool - push-to-talk tool that registers itself through MCP.AfskDemod - AFSK demodulator used by some acoustic provisioning flows.SystemReset - helper that performs a safe factory reset when a button is held at boot.Any board can register custom tools - speaker control, screen brightness, battery readout, light control, etc. See MCP IoT control usage.
Board - base class
WifiBoard - WiFi-connected boardMl307Board / Nt26Board - 4G modem boardsDualNetworkBoard - WiFi + 4G switchable boardRndisBoard - RNDIS-over-USB boardconfig.h must match your schematic.