自定義開發板指南

本指南介紹如何為小智AI語音聊天機器人專案定製一個新的開發板初始化程式。小智AI支援大量 ESP32 系列開發板,每個開發板的初始化程式碼都放在對應的目錄下。

重要提示

警告: 對於自定義開發板,當IO配置與原有開發板不同時,切勿直接覆蓋原有開發板的配置編譯韌體。必須建立新的開發板型別,或者透過config.json檔案中的builds配置不同的name和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. 建立新的開發板目錄

首先在boards/目錄下建立一個新的目錄,命名方式應使用 [品牌名]-[開發板型別] 的形式,例如 m5stack-tab5:

mkdir main/boards/my-custom-board

2. 建立配置檔案

config.h

在config.h中定義所有的硬體配置,包括:

  • 音訊取樣率和I2S引腳配置
  • 音訊編解碼晶片地址和I2C引腳配置
  • 按鈕和LED引腳配置
  • 顯示屏參數和引腳配置

參考示例(來自lichuang-c3-dev):

#ifndef _BOARD_CONFIG_H_
#define _BOARD_CONFIG_H_

#include <driver/gpio.h>

// 音訊配置
#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

// 按鈕配置
#define BOOT_BUTTON_GPIO        GPIO_NUM_9

// 顯示屏配置
#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中定義相容性相關的上報型別和自動化編譯配置:

{
    "type": "my-custom-board",  // 韌體上報的開發板型別,釋出後應保持穩定
    "target": "esp32s3",  // 目標晶片型號: esp32, esp32s3, esp32c3, esp32c6, esp32p4等
    "builds": [
        {
            "name": "my-custom-board",  // 開發板名稱,用於生成韌體包
            "sdkconfig_append": [
                // 特別 Flash 大小配置
                "CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y",
                // 特別分割槽表配置
                "CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""
            ]
        }
    ]
}

配置項說明:

  • manufacturer: 廠商子目錄名稱;使用廠商目錄時必須填寫,並作為 board.manufacturer 上報。平鋪社群板可以省略,上報為空字串
  • type: 韌體上報的開發板系列型別,釋出後應保持穩定
  • target: 目標晶片型號,必須與硬體匹配
  • name: release 構建上報的韌體變體名稱,通常與 type 一致
  • sdkconfig_append: 額外的 sdkconfig 配置項陣列,會追加到預設配置中

type 和 name 只能包含小寫字母、數字、點(.)和連字元(-), 不允許使用下劃線、空格或大寫字母。

常用的 sdkconfig_append 配置:

// Flash 大小
"CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y"   // 4MB Flash
"CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y"   // 8MB Flash

// 分割槽表
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/4m.csv\""  // 4MB 分割槽表
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"partitions/v2/8m.csv\""  // 8MB 分割槽表

// 音訊處理
"CONFIG_USE_DEVICE_AEC=y"          // 啟用裝置端 AEC

對於適用的目標晶片,專案預設使用 16MB Flash 和 partitions/v2/16m.csv。如果板子配置與專案及目標晶片的有效預設值相同, 不要在 sdkconfig_append 中重複填寫;這裡應只保留板子確實需要的覆蓋項。

不要在板子的 config.json 中選擇語言或具體喚醒詞。這些屬於使用者構建選項, 應統一透過 menuconfig 或構建指令碼參數配置,方便 CLI、Agent 和線上編譯介面 共用同一套參數。

3. 編寫板級初始化程式碼

建立一個my_custom_board.cc檔案,實現開發板的所有初始化邏輯。

一個基本的開發板類定義包含以下幾個部分:

  1. 類定義:繼承自WifiBoard或Ml307Board
  2. 初始化函式:包括I2C、顯示屏、按鈕、IoT等元件的初始化
  3. 虛擬函式重寫:如GetAudioCodec()、GetDisplay()、GetBacklight()等
  4. 註冊開發板:使用DECLARE_BOARD宏註冊開發板
#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_;

    // I2C初始化
    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_));
    }

    // SPI初始化(用於顯示屏)
    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();
        });
    }

    // 顯示屏初始化(以ST7789為例)
    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);
    }

    // MCP Tools 初始化
    void InitializeTools() {
        // 參考 MCP 文件
    }

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.projbuild 中新增開發板選項

開啟 main/Kconfig.projbuild 檔案,在 choice BOARD_TYPE 部分新增新的開發板配置項:

choice BOARD_TYPE
    prompt "Board Type"
    default BOARD_TYPE_BREAD_COMPACT_WIFI
    help
        Board type. 開發板型別
    
    # ... 其他開發板選項 ...
    
    config BOARD_TYPE_MY_CUSTOM_BOARD
        bool "My Custom Board (我的自定義開發板)"
        depends on IDF_TARGET_ESP32S3  # 根據你的目標晶片修改
endchoice

注意事項:

  • BOARD_TYPE_MY_CUSTOM_BOARD 是配置項名稱,需要全大寫,使用下劃線分隔
  • depends on 指定了目標晶片型別(如 IDF_TARGET_ESP32S3、IDF_TARGET_ESP32C3 等)
  • 描述文字可以使用中英文

在 CMakeLists.txt 中新增開發板配置

開啟 main/CMakeLists.txt 檔案,在開發板型別判斷部分新增新的配置:

# 在 elseif 鏈中新增你的開發板配置
elseif(CONFIG_BOARD_TYPE_MY_CUSTOM_BOARD)
    set(BOARD_DIR "my-custom-board")  # 相對於 main/boards 的完整路徑
    set(BUILTIN_TEXT_FONT font_puhui_basic_20_4)  # 根據螢幕大小選擇合適的字型
    set(BUILTIN_ICON_FONT font_awesome_20_4)
    set(DEFAULT_EMOJI_COLLECTION twemoji_64)  # 可選,如果需要表情顯示
endif()

字型和表情配置說明:

根據螢幕解析度選擇合適的字型大小:

  • 小螢幕(128x64 OLED):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. 配置和編譯

方法一:使用 idf.py 手動配置

  1. 設定目標晶片(首次配置或更換晶片時):

    # 對於 ESP32-S3
    idf.py set-target esp32s3
    
    # 對於 ESP32-C3
    idf.py set-target esp32c3
    
    # 對於 ESP32
    idf.py set-target esp32
  2. 清理舊配置:

    idf.py fullclean
  3. 進入配置選單:

    idf.py menuconfig

    在選單中導航到:Xiaozhi Assistant -> Board Type,選擇你的自定義開發板。

  4. 編譯和燒錄:

    idf.py build
    idf.py flash monitor

方法二:使用 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

--language 接受 main/assets/locales/ 下已有的 locale;--wake-word 接受 ESP-SR 模型名、nihaoxiaozhi(自動選擇與目標晶片相容的模型)或 disabled。ESP32-C3/C5/C6 僅支援 WakeNet9s(wn9s_*)模型; ESP32-S3/P4/S31 構建會自動使用 AFE 喚醒引擎。

可以用文字或 JSON 格式查詢可用值:

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 <變體>
  • 讀取 config.json 中的 target;僅當已有 build 目錄的目標晶片不同時 執行清理,然後透過一次 idf.py reconfigure 同時配置目標晶片、開發板 名稱、defaults 和所選變體的 sdkconfig_append。隨後的 idf.py build 會複用這份配置
  • 將所選 build 的 name 作為韌體上報的變體名稱
  • 預設生成 build/merged-binary.bin,不建立 ZIP;指定 --zip 時會重新生成 releases/v<版本>_<名稱>.zip

Windows:命令列過長

如果 Windows 編譯時因編譯器命令超過 32,767 字元上限而出現 Ninja CreateProcess 錯誤,可啟用 CMake/Ninja 的 response file。在剛才編譯失敗的 同一個工程目錄、同一個 ESP-IDF 終端中執行:

idf.py -D CMAKE_NINJA_FORCE_RESPONSE_FILE=ON reconfigure
idf.py build

這樣會透過 @檔名 傳遞編譯參數,而不是在命令列中展開。執行 idf.py fullclean 或清空 CMake 快取後,需要重新執行上述 reconfigure 命令。 將工程移到較短的路徑(例如 E:\xz)也可能緩解問題;Windows 的“啟用長路徑” 設定不會提高整條命令列的長度上限。參見 #2340 中的解決方法。

6. 建立README.md

在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.type、board.name 一起上報為 board.manufacturer;沒有該欄位的平鋪社群開發板會上報空字串。

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 中的 / 替換為 - 後的完整路徑作為兜底型別。

經驗法則:

  • 同一廠商有兩款及以上開發板,且共用驅動、資源或文件時,使用廠商目錄。
  • 單一開發板或社群示例,使用平鋪目錄。
  • 目錄名使用小寫加連字元(例如 waveshare、lceda-course-examples)。

常見開發板元件

main/boards/common/ 中提供了若干可複用元件,可以直接在你的開發板類中使用:

顯示屏驅動

支援的 LCD 型別包括:

  • ST7789 (SPI)
  • ILI9341 (SPI)
  • SH8601 (QSPI)
  • 等等。

音訊編解碼器

  • Es8311AudioCodec(最常用)
  • Es8374AudioCodec
  • Es8388AudioCodec
  • Es8389AudioCodec
  • BoxAudioCodec(ESP-Box 系列開發板使用的 ES7210 麥克風陣列 + 編解碼器組合)
  • NoAudioCodec(不使用外部編解碼器,直接 I2S)
  • DummyAudioCodec(沒有音訊的開發板使用的佔位實現)

電源管理

  • Axp2101 電源管理晶片輔助類。
  • Sy6970 電池充電晶片輔助類。
  • AdcBatteryMonitor:基於 ADC 的簡易電池電壓監測。
  • PowerSaveTimer / SleepTimer:輕度睡眠排程輔助類。

網路

  • WifiBoard:僅 Wi-Fi 的基類。
  • Ml307Board / Nt26Board:4G 模組基類。
  • DualNetworkBoard:可在 Wi-Fi 與 4G 間切換的基類。
  • RndisBoard:基於 USB 的 RNDIS 網路(ESP32-S3 / ESP32-P4)。
  • 面向 ESP32-S3 / ESP32-P4 的 ESP-Video 輔助類 EspVideo。

輸入輔助

  • Button:標準按鍵(單擊、長按、多擊)。
  • Knob:旋轉編碼器封裝。
  • PressToTalkMcpTool:透過 MCP 註冊自身的按住說話工具。
  • SystemReset:開機時按住按鍵可安全執行恢復出廠設定的輔助類。

MCP 裝置控制

任何開發板都可以註冊自定義工具,例如揚聲器控制、螢幕亮度調節、電池電量讀取、燈光控制等。參見 MCP 物聯網控制用法。

開發板類繼承關係

  • Board:基礎板級類
    • WifiBoard:Wi-Fi 連線的開發板
    • Ml307Board / Nt26Board:使用 4G 模組的開發板
    • DualNetworkBoard:支援 Wi-Fi 與 4G 網路切換的開發板
    • RndisBoard:基於 USB RNDIS 的開發板

開發技巧

  1. 參考相似的開發板:如果您的新開發板與現有開發板有相似之處,可以參考現有實現
  2. 分步除錯:先實現基礎功能(如顯示),再新增更復雜的功能(如音訊)
  3. 管腳對映:確保在config.h中正確配置所有管腳對映
  4. 檢查硬體相容性:確認所有晶片和驅動程式的相容性

可能遇到的問題

  1. 顯示屏不正常:檢查SPI配置、映象設定和顏色反轉設定
  2. 音訊無輸出:檢查I2S配置、PA使能引腳和編解碼器地址
  3. 無法連線網路:檢查Wi-Fi憑據和網路配置
  4. 無法與伺服器通訊:檢查MQTT或WebSocket配置

參考資料

來源:78/xiaozhi-esp32 的 docs/custom-board_zh.md,廠商子目錄與元件清單參照同倉庫英文版 docs/custom-board.md。