Руководство по пользовательским платам
В этом руководстве описано, как добавить новую плату в проект голосового помощника XiaoZhi AI. XiaoZhi AI поддерживает множество плат серии ESP32; у каждой есть собственный каталог в main/boards/.
Важно
Предупреждение: для пользовательской платы, конфигурация IO которой отличается от существующей, ни в коем случае не перезаписывайте конфигурацию исходной платы. Всегда создавайте новый тип платы — или используйте массив
buildsвconfig.json, чтобы получить отдельное имя прошивки с другими макросамиsdkconfig. Для сборки прошивки используйтеpython scripts/build.py [каталог-платы].Перезапись конфигурации существующей платы опасна: OTA-обновления могут заменить вашу пользовательскую прошивку стандартной прошивкой для исходной платы. У каждой платы должна быть уникальная идентичность и собственный канал обновления прошивки.
Структура каталога
Каталог платы обычно содержит:
xxx_board.cc— инициализация на уровне платы и связующий код.config.h— назначение выводов и настройки уровня платы.config.json— сообщаемый тип платы и конфигурация релиза, которую читают CMake иscripts/build.py.README.md— заметки, относящиеся к плате.
Платы могут лежать прямо в main/boards/ или группироваться по производителям в main/boards/<manufacturer>/<board>/ (см. ниже Подкаталоги по производителям).
Шаги
1. Создайте каталог платы
Создайте в main/boards/ новый каталог в стиле имён [vendor]-[model] (например, m5stack-tab5):
mkdir main/boards/my-custom-board2. Создайте файлы конфигурации
config.h
Определите все аппаратные настройки в config.h:
- Частоты дискретизации аудио и распиновка I2S.
- Адрес I2C и выводы аудиокодека.
- Выводы кнопок и светодиодов.
- Параметры и выводы дисплея.
Пример (из 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
config.json задаёт сообщаемый тип, от которого зависит совместимость, и управляет scripts/build.py:
{
"type": "my-custom-board",
"target": "esp32s3",
"builds": [
{
"name": "my-custom-board",
"sdkconfig_append": [
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
]
}
]
}Поля:
type: семейство платы, которое сообщает прошивка; от него зависит совместимость. После релиза не меняйте его.target: целевой чип; должен соответствовать реальному оборудованию (esp32,esp32s3,esp32c3,esp32c6,esp32p4и т. д.).name: имя варианта прошивки, которое сообщают релизные сборки; от него зависит совместимость; обычно совпадает сtype.sdkconfig_append: дополнительные строки sdkconfig, добавляемые к значениям по умолчанию.
И type, и name могут содержать только строчные латинские буквы, цифры, точки (.) и дефисы (-). Подчёркивания, пробелы и заглавные буквы не допускаются.
Типичные записи sdkconfig_append:
// Flash size
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y"
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y"
// Partition table
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\""
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
// Audio pipeline
"CONFIG_USE_DEVICE_AEC=y" // enable on-device AECПо умолчанию проект использует 16 МБ флеш-памяти и partitions/v2/16m.csv для подходящих целей. Не повторяйте значения, которые уже совпадают с действующими значениями по умолчанию для проекта и цели; используйте sdkconfig_append только для действительных переопределений, специфичных для платы.
Не выбирайте язык или конкретное слово пробуждения в config.json платы. Это пользовательские параметры сборки, и их нужно задавать единообразно через menuconfig или параметры скрипта сборки, чтобы CLI, агент и онлайн-сборки пользовались одним интерфейсом. scripts/build.py также выбирает движок слова пробуждения, поэтому не добавляйте записи CONFIG_USE_*_WAKE_WORD и выбор моделей в пресеты платы или чипа. Плата, на которой нужно отключить обнаружение слова пробуждения из-за ограничений памяти, флеш-памяти или производительности, может задать CONFIG_WAKE_WORD_DISABLED=y в sdkconfig_append. Скрипт сборки сохраняет этот отказ и сообщает для варианта wake_word_supported равным false.
3. Реализуйте класс платы
Создайте my_custom_board.cc с реализацией на уровне платы.
В базовом классе платы есть:
- Объявление класса: наследуйте от
WifiBoardилиMl307Board. - Вспомогательные функции инициализации: I2C, дисплей, кнопки, инструменты IoT/MCP и т. д.
- Переопределения виртуальных функций:
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);4. Подключите систему сборки
Добавьте запись в Kconfig
В main/Kconfig.projbuild добавьте запись в блок choice BOARD_TYPE:
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Примечания:
- Идентификатор должен быть в верхнем регистре, слова разделяются подчёркиваниями.
depends onограничивает запись нужной целью (IDF_TARGET_ESP32S3,IDF_TARGET_ESP32C3и т. д.).- Подпись можно локализовать.
Добавьте ветку в CMakeLists.txt
Откройте main/CMakeLists.txt и дополните цепочку типов плат:
elseif(CONFIG_BOARD_TYPE_MY_CUSTOM_BOARD)
set(BOARD_DIR "my-custom-board")
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Рекомендации по шрифтам и эмодзи:
Выберите размер шрифта по разрешению дисплея:
- Малый (OLED 128x64):
font_puhui_basic_14_1/font_awesome_14_1 - Малый и средний (240x240):
font_puhui_basic_16_4/font_awesome_16_4 - Средний (240x320):
font_puhui_basic_20_4/font_awesome_20_4 - Большой (480x320 и выше):
font_puhui_basic_30_4/font_awesome_30_4
Наборы эмодзи:
twemoji_32— 32x32 пикселя (небольшие экраны).twemoji_64— 64x64 пикселя (большие экраны).
5. Сборка и прошивка
Способ A — вручную через idf.py
Задайте целевой чип (в первый раз или при смене цели):
idf.py set-target esp32s3 # ESP32-S3 idf.py set-target esp32c3 # ESP32-C3 idf.py set-target esp32 # ESP32Очистите устаревшую конфигурацию:
idf.py fullcleanВыберите плату в menuconfig:
idf.py menuconfigПерейдите в
Xiaozhi Assistant -> Board Typeи выберите свою плату.Соберите и прошейте:
idf.py build idf.py flash monitor
Способ B — через build.py (рекомендуется)
Если в каталоге платы есть config.json, плату можно настроить и собрать автоматически:
python scripts/build.py my-custom-boardВыбор языка и слова пробуждения — пользовательские параметры сборки:
python scripts/build.py my-custom-board \
--language en-US \
--wake-word wn9_jarvis_ttsЯзык по умолчанию в Kconfig проекта — английский (en-US). scripts/build.py тоже выбирает английский, если --language не указан, в том числе когда предыдущая сборка использовала другой язык. Для китайского передайте --language zh-CN, для остальных — одну из локалей из main/assets/locales/. Этот параметр выбирает тексты интерфейса и голосовые подсказки. Если на поддерживаемой плате --wake-word не указан, скрипт выбирает «你好小智» для zh-CN и zh-TW и «Hi ESP» для всех остальных языков. Неподдерживаемые платы и варианты с явным отказом на уровне платы остаются отключёнными.
--wake-word переопределяет значение по умолчанию, зависящее от языка. Он принимает имя модели ESP-SR, nihaoxiaozhi, hiesp или disabled. Два псевдонима выбирают модель, подходящую для цели. Цели ESP32-C3/C5/C6 используют модели WakeNet9s (wn9s_*); ESP32-S3/P4/S31 используют движок AFE и требуют PSRAM. ESP32 тоже требует PSRAM и использует движок без AFE. Запросы на включение обнаружения слова пробуждения на неподдерживаемой или явно отключённой плате отклоняются до настройки SDK.
Пользовательские assets и assets Emote сохраняют свои ресурсы интерфейса, но упакованные в них речевые модели заменяются моделями, выбранными в текущей sdkconfig (или удаляются, если обнаружение слова пробуждения отключено). Итоговый пакет должен помещаться в раздел assets. При прямой сборке через idf.py явно выберите реализацию слова пробуждения и модели в menuconfig: значения по умолчанию для чипа модель не выбирают, а свежая ручная конфигурация оставляет обнаружение слова пробуждения отключённым.
Допустимые значения можно получить в текстовом или машиночитаемом виде:
python scripts/build.py --list-languages
python scripts/build.py --list-languages --json
python scripts/build.py --list-wake-words
python scripts/build.py --list-wake-words --jsonСписок слов пробуждения читается из текущего разрешённого компонента ESP-SR, поэтому, если managed_components/ ещё не заполнен, сначала выполните idf.py reconfigure.
Скрипт делает следующее:
- Без аргументов выводит справку. Типы плат и варианты перечисляет
--list-boards. - Если у выбранной платы несколько сборок, запрашивает вариант. В неинтерактивных средах передайте
--name <variant>. - Читает
targetизconfig.json, очищает существующий каталог сборки только если его цель отличается, затем одним вызовомidf.py reconfigureнастраивает цель, имя платы, значения по умолчанию иsdkconfig_appendвыбранной сборки. Следующийidf.py buildиспользует эту конфигурацию. - Передаёт
nameвыбранной сборки как сообщаемое имя варианта прошивки. - По умолчанию собирает
build/merged-binary.binбез создания ZIP. Чтобы заново создатьreleases/v<version>_<name>.zip, передайте--zip.
Windows: слишком длинная командная строка
Если сборка в Windows завершается ошибкой Ninja CreateProcess из-за того, что команда компилятора превышает предел в 32 767 символов, включите файлы ответов (response files) CMake/Ninja. В том же каталоге проекта и том же терминале ESP-IDF, где сборка не удалась, выполните:
idf.py -D CMAKE_NINJA_FORCE_RESPONSE_FILE=ON reconfigure
idf.py buildТогда аргументы компилятора передаются через @file, а не разворачиваются в командной строке. После idf.py fullclean или очистки кэша CMake повторите команду reconfigure. Может помочь и перенос проекта на более короткий путь (например, E:\xz); включение поддержки длинных путей Windows предел длины командной строки не повышает. См. обходной путь в #2340.
6. Напишите README
В README.md опишите плату, требования к оборудованию, порядок сборки и любые особые замечания.
Подкаталоги по производителям
Платы можно группировать по производителям в main/boards/<manufacturer>/<board>/. Это рекомендуемая раскладка, когда один производитель выпускает несколько вариантов — например, main/boards/waveshare/esp32-p4-nano/ или main/boards/lceda-course-examples/eda-tv-pro/.
Для платы в подкаталоге производителя добавьте в config.json то же значение, например "manufacturer": "waveshare". Прошивка сообщает его как board.manufacturer вместе с board.type и board.name. Плоские платы сообщества без этого поля сообщают пустую строку вместо производителя.
Задайте BOARD_DIR полным путём относительно main/boards/:
elseif(CONFIG_BOARD_TYPE_WAVESHARE_ESP32_P4_NANO)
set(BOARD_DIR "waveshare/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)Система сборки загружает исходники из main/boards/${BOARD_DIR}/ и читает сообщаемый тип платы из config.json этого каталога. Если config.json или его поле верхнего уровня type отсутствует, в качестве резервного типа используется полный BOARD_DIR с /, заменёнными на -.
Практические правила:
- Используйте раскладку по производителям, если у вас две или больше платы одного производителя, у которых общие драйверы, assets или документация.
- Для единичных плат и примеров сообщества используйте плоскую раскладку.
- Имена каталогов — строчные буквы и дефисы (например,
waveshare,lceda-course-examples).
Общие компоненты плат
В main/boards/common/ лежат несколько повторно используемых компонентов. Их можно подключать прямо из класса платы:
Драйверы дисплеев
Поддерживаются, в частности, такие семейства LCD:
- ST7789 (SPI)
- ILI9341 (SPI)
- SH8601 (QSPI)
- и многие другие.
Аудиокодеки
Es8311AudioCodec(самый распространённый)Es8374AudioCodecEs8388AudioCodecEs8389AudioCodecBoxAudioCodec(сочетание массива микрофонов ES7210 и кодека, используемое на платах ESP-Box)NoAudioCodec(прямой I2S без внешнего кодека)DummyAudioCodec(заглушка для плат без звука)
Управление питанием
- Вспомогательные средства для микросхемы управления питанием
Axp2101. - Вспомогательные средства для зарядного устройства
Sy6970. AdcBatteryMonitor— простой монитор напряжения батареи на основе ADC.PowerSaveTimer/SleepTimer— помощники для планирования лёгкого сна.
Сеть
WifiBoard— базовый класс только для Wi-Fi.Ml307Board/Nt26Board— базовые классы для 4G-модемов.DualNetworkBoard— базовый класс с переключением Wi-Fi / 4G.RndisBoard— сеть RNDIS поверх USB (ESP32-S3 / ESP32-P4).- Помощники
EspVideoдля ESP-Video на ESP32-S3 / ESP32-P4.
Вспомогательные средства ввода
Button— стандартные кнопки (щелчок, долгое нажатие, несколько щелчков).Knob— обёртка над поворотным энкодером.PressToTalkMcpTool— инструмент «нажми и говори», который регистрирует себя через MCP.SystemReset— помощник, выполняющий безопасный сброс к заводским настройкам, когда кнопка удерживается при загрузке.
Интеграция с MCP
Любая плата может регистрировать пользовательские инструменты — управление динамиком, яркость экрана, показания батареи, управление освещением и т. д. См. Использование MCP для управления IoT.
Иерархия классов плат
Board— базовый классWifiBoard— плата с подключением по Wi-FiMl307Board/Nt26Board— платы с 4G-модемомDualNetworkBoard— плата с переключением Wi-Fi и 4GRndisBoard— плата с RNDIS поверх USB
Советы
- Начните с похожей платы — копировать и подправлять существующую плату обычно быстрее, чем начинать с нуля.
- Запускайте по частям — сначала дисплей, затем звук, затем весь стек.
- Перепроверьте назначение выводов — каждый вывод, определённый в
config.h, должен соответствовать вашей схеме. - Проверьте совместимость оборудования — особенно сочетания кодека, PMIC и контроллера сенсора.
Устранение неполадок
- Дисплей работает неправильно — проверьте конфигурацию SPI, зеркалирование и инверсию цвета.
- Нет звука — проверьте проводку I2S, вывод включения PA и адрес I2C кодека.
- Не удаётся подключиться к Wi-Fi — перепроверьте учётные данные Wi-Fi и способ настройки.
- Не удаётся достучаться до сервера — проверьте настройки конечной точки WebSocket / MQTT.
Справочные материалы
- Документация ESP-IDF: https://docs.espressif.com/projects/esp-idf/
- Документация LVGL: https://docs.lvgl.io/
- Документация ESP-SR: https://github.com/espressif/esp-sr
Источник: docs/custom-board.md в 78/xiaozhi-esp32