第 9 章 板级抽象:main/boards/
110 个板子目录、4250+ 行共用代码。这一层把芯片型号、网络栈(Wi-Fi vs 4G)、屏幕型号、按键布局、电源管理、外设差异全部封装在
Board类层级里。本章先讲共用基础设施(common/),再挑 3 个代表性板子拆开看。
9.1 板级机制鸟瞰
┌───────────────────────────────────┐
│ Board (abstract base) │
│ GetUuid / GetAudioCodec / │
│ GetDisplay / GetLed / GetCamera │
│ GetNetwork / StartNetwork / │
│ GetSystemInfoJson / │
│ GetDeviceStatusJson / ... │
└────────────┬──────────────────────┘
│
┌────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌────────────────────┐
│ WifiBoard │ │ Ml307Board │ │ DualNetworkBoard │
│ (Wi-Fi) │ │ (4G Cat.1) │ │ (Wi-Fi + 4G) │
└────┬─────┘ └──────┬────────┘ └──────┬────────────┘
│ │ │
│ │ │ 内部持有
│ │ │ WifiBoard 和 Ml307Board
│ │ │ 切换调用
▼ ▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 各板子的最终类(110 个 board.cc 文件) │
│ │
│ CompactWifiBoard : WifiBoard │
│ CompactMl307Board : DualNetworkBoard │
│ EspBox3Board : WifiBoard │
│ AtomEchoS3RBoard : WifiBoard │
│ AtkDnesp32s3Box2_4G : Ml307Board │
│ ... │
└──────────────────────────────────────────────────────────┘
共用零件 (在 main/boards/common/):
- Button 按键封装
- Backlight 背光控制
- PowerSaveTimer / SleepTimer
- SystemReset 长按复位
- PressToTalkMcpTool 按住说话 MCP 工具
- Esp32Camera 摄像头封装
- Axp2101 / Sy6970 电源管理芯片 I²C 驱动
- I2cDevice 通用 I²C 设备基类
- Knob 旋钮编码器
- LampController 灯控(MCP 演示用)
- AfskDemod 声波配网解调
- Blufi BLE 配网
- AdcBatteryMonitor ADC 测电池电压
110 个 boards/<xxx>/<xxx>_board.cc 都遵守一个套路:
- 继承
WifiBoard/Ml307Board/DualNetworkBoard; - 私有成员:按键对象、I²C bus、面板句柄、display 指针;
- Initialize 系列*:分别初始化 I²C / 显示屏 / 按键 / 工具;
- 重写:
GetLed()/GetAudioCodec()/GetDisplay()(这三个必须); - 构造函数:按 init 函数顺序依次调用;
DECLARE_BOARD(BoardClassName)宏注册类工厂。
9.2 Board 抽象基类详解
9.2.1 单例工厂模式
class Board {
public:
static Board& GetInstance() {
static Board* instance = static_cast<Board*>(create_board());
return *instance;
}
// ...
};
#define DECLARE_BOARD(BOARD_CLASS_NAME) \
void* create_board() { \
return new BOARD_CLASS_NAME(); \
}
create_board() 是个全局 C 函数,由具体板子在文件末尾用 DECLARE_BOARD(MyBoard) 宏定义。链接时只能有一个实现——所以编译时通过 CMake 选择板子:
# main/CMakeLists.txt 大致
if(CONFIG_BOARD_TYPE_BREAD_COMPACT_WIFI)
set(BOARD_TYPE "bread-compact-wifi")
endif()
target_sources("${COMPONENT_LIB}" PRIVATE "${BUILD_PATH}/boards/${BOARD_TYPE}/${BOARD_TYPE}_board.cc")
Board::GetInstance() 内 create_board() 返回新 new 的具体板子对象。裸 new + 永不 delete——程序运行期单例。
9.2.2 关键接口
class Board {
public:
virtual std::string GetBoardType() = 0; // "wifi" or "ml307"
virtual std::string GetUuid() { return uuid_; } // 软件 UUID
virtual Backlight* GetBacklight() { return nullptr; }
virtual Led* GetLed(); // 默认 NoLed
virtual AudioCodec* GetAudioCodec() = 0; // 必须
virtual bool GetTemperature(float& esp32temp);
virtual Display* GetDisplay(); // 默认 NoDisplay
virtual Camera* GetCamera(); // 默认 nullptr
virtual NetworkInterface* GetNetwork() = 0;
virtual void StartNetwork() = 0;
virtual void SetNetworkEventCallback(NetworkEventCallback);
virtual const char* GetNetworkStateIcon() = 0;
virtual bool GetBatteryLevel(int &level, bool& charging, bool& discharging);
virtual std::string GetSystemInfoJson();
virtual void SetPowerSaveLevel(PowerSaveLevel level) = 0;
virtual std::string GetBoardJson() = 0;
virtual std::string GetDeviceStatusJson() = 0;
};
| 接口 | 默认实现 | 设计意图 |
|---|---|---|
| GetUuid | 返回 uuid_ | NVS 生成或读取 |
| GetLed | NoLed | 不会崩,板子可省略 |
| GetDisplay | NoDisplay | 同上 |
| GetCamera | nullptr | take_photo 工具自动跳过 |
| GetBacklight | nullptr | screen.set_brightness 自动跳过 |
| GetBatteryLevel | false | 状态栏不显示电池 |
| GetAudioCodec | 纯虚 | 必须实现,没声音的板子 → NoAudioCodec |
| GetNetwork / StartNetwork / GetBoardJson 等 | 纯虚 | 网络栈相关,必须实现 |
9.2.3 Board::Board() 构造 —— UUID 生成
Board::Board() {
Settings settings("board", true);
uuid_ = settings.GetString("uuid");
if (uuid_.empty()) {
uuid_ = GenerateUuid();
settings.SetString("uuid", uuid_);
}
}
UUID 设计:
- 首次启动 NVS 没有 → 调
GenerateUuid()生成; - 后续启动直接读 NVS;
- 即使设备重置 NVS(恢复出厂),下次开机会重新随机一次——所以 UUID 不是硬件唯一标识,MAC 才是。
9.2.4 GenerateUuid() —— 标准 UUID v4
std::string Board::GenerateUuid() {
uint8_t uuid[16];
esp_fill_random(uuid, sizeof(uuid)); // ESP32 硬件 TRNG
uuid[6] = (uuid[6] & 0x0F) | 0x40; // 版本 4
uuid[8] = (uuid[8] & 0x3F) | 0x80; // 变体 1
char uuid_str[37];
snprintf(uuid_str, sizeof(uuid_str),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
uuid[0], ..., uuid[15]);
return std::string(uuid_str);
}
按 RFC 4122 UUIDv4 规范:
- 第 7 字节(uuid[6])高 4 位强制 0100(版本号 4);
- 第 9 字节(uuid[8])高 2 位强制 10(变体 RFC 4122);
- 其它 122 位由硬件 TRNG 填充(
esp_fill_random用芯片随机数发生器,质量好)。
输出形如 f47ac10b-58cc-4372-a567-0e02b2c3d479。
9.2.5 GetSystemInfoJson() —— 设备自报家门
这个函数是 OTA 阶段 POST 给服务器的请求体,含:
- 协议版本:
"version": 2 - 语言代码:从
Lang::CODE拿(zh-CN / en-US / 日语等) - Flash 大小、剩余堆
- MAC、UUID
- 芯片型号(cores / revision / features 位)
- 应用名 / 版本号 / 编译时间 / IDF 版本 / ELF SHA256
- 分区表(label / type / address / size 每个分区)
- 当前 OTA 运行分区 label
- 显示信息(monochrome / width / height)
- 板子自己附加的 JSON(
GetBoardJson()返回)
手写字符串拼接 JSON——不用 cJSON 因为是一次性序列化,自己拼性能更好:
json += R"("mac_address":")" + SystemInfo::GetMacAddress() + R"(",)";
R"(...)" 是 C++11 原始字符串字面量——\" 这种转义可以直接写 ",写 JSON 模板省心。
服务器拿到这堆信息可以做:
- 按芯片型号下发不同 mqtt/websocket 配置;
- 按版本号决定是否给新固件 URL;
- 按 elf_sha256 做篡改检测(已知设备就该跑某版固件);
- 按显示器类型给不同 emoji 资源包。
9.3 WifiBoard —— Wi-Fi 板基类
359 行实现,处理 Wi-Fi 板的核心逻辑:
9.3.1 状态机:连接 → 超时 → 配网
void WifiBoard::StartNetwork() {
auto& wifi_manager = WifiManager::GetInstance();
WifiManagerConfig config;
config.ssid_prefix = "Xiaozhi";
config.language = Lang::CODE;
wifi_manager.Initialize(config);
wifi_manager.SetEventCallback([this, &wifi_manager](WifiEvent event) {
// 把 WifiEvent 翻译成统一的 NetworkEvent
std::string ssid = wifi_manager.GetSsid();
switch (event) {
case WifiEvent::Connected: OnNetworkEvent(NetworkEvent::Connected, ssid); break;
// ...
}
});
TryWifiConnect();
}
void WifiBoard::TryWifiConnect() {
auto& ssid_manager = SsidManager::GetInstance();
bool have_ssid = !ssid_manager.GetSsidList().empty();
if (have_ssid) {
esp_timer_start_once(connect_timer_, CONNECT_TIMEOUT_SEC * 1000000ULL); // 60 秒超时
WifiManager::GetInstance().StartStation();
} else {
vTaskDelay(pdMS_TO_TICKS(1500)); // 等屏幕显示版本信息
StartWifiConfigMode();
}
}
void WifiBoard::OnWifiConnectTimeout(void* arg) {
auto* board = static_cast<WifiBoard*>(arg);
WifiManager::GetInstance().StopStation();
board->StartWifiConfigMode();
}
3 种状态:
- 有 SSID + 能连:60 秒内
Connected触发 →esp_timer_stop取消超时; - 有 SSID 但连不上:60 秒到 → 进入配网模式;
- 没 SSID:等 1.5 秒(让屏幕显示完版本号)→ 进入配网模式。
9.3.2 三种配网方式 (Kconfig 任选)
void WifiBoard::StartWifiConfigMode() {
in_config_mode_ = true;
Application::GetInstance().SetDeviceState(kDeviceStateWifiConfiguring);
#ifdef CONFIG_USE_HOTSPOT_WIFI_PROVISIONING
// 方式 1: 设备开热点 + 网页配置
auto& wifi_manager = WifiManager::GetInstance();
wifi_manager.StartConfigAp();
Application::GetInstance().Schedule([&wifi_manager]() {
std::string hint = Lang::Strings::CONNECT_TO_HOTSPOT;
hint += wifi_manager.GetApSsid();
hint += Lang::Strings::ACCESS_VIA_BROWSER;
hint += wifi_manager.GetApWebUrl();
Application::GetInstance().Alert(Lang::Strings::WIFI_CONFIG_MODE, hint.c_str(), "gear", Lang::Sounds::OGG_WIFICONFIG);
});
#endif
#if CONFIG_USE_ESP_BLUFI_WIFI_PROVISIONING
// 方式 2: BLE 配网(手机 App)
Blufi::GetInstance().init();
#endif
#if CONFIG_USE_ACOUSTIC_WIFI_PROVISIONING
// 方式 3: 声波配网 (AFSK)
xTaskCreate([](void* arg) {
auto ch = reinterpret_cast<intptr_t>(arg);
audio_wifi_config::ReceiveWifiCredentialsFromAudio(&app, &wifi, disp, ch);
vTaskDelete(NULL);
}, "acoustic_wifi", 4096, reinterpret_cast<void*>(channel), 2, NULL);
#endif
}
| 配网方式 | 实现 | 用户操作 |
|---|---|---|
| Hotspot 网页 | wifi_manager.StartConfigAp + HTTP server | 手机连 "Xiaozhi-xxx" 热点 → 浏览器输 SSID/密码 |
| BLE BluFi | Espressif BluFi 协议 | 手机 App "EspBluFi" 蓝牙发 SSID/密码 |
| 声波 AFSK | 麦克风听 AFSK 调制信号解调 | 手机 App 播放声音 → 设备麦克风收 |
三种可同时启用——用户选最方便的方式。声波最神奇:完全不需要 BLE/AP,弱网环境下也能搞定。
9.3.3 OnNetworkEvent —— 事件分发
void WifiBoard::OnNetworkEvent(NetworkEvent event, const std::string& data) {
switch (event) {
case NetworkEvent::Connected:
esp_timer_stop(connect_timer_); // 取消超时
Blufi::GetInstance().deinit(); // 释放 BluFi 资源
in_config_mode_ = false;
break;
case NetworkEvent::WifiConfigModeExit:
in_config_mode_ = false;
TryWifiConnect(); // 重新连
break;
// ...
}
if (network_event_callback_) {
network_event_callback_(event, data); // 转发给上层
}
}
业务层(Application)通过 SetNetworkEventCallback 注册自己的回调,所有网络事件都会过来。
9.3.4 GetDeviceStatusJson() —— MCP 工具用的状态查询
std::string WifiBoard::GetDeviceStatusJson() {
// 拼成类似:
// { "audio_speaker": {"volume": 70}, "screen": {"brightness": 50},
// "battery": {"level": 80, "charging": true}, "network": {"type": "wifi", "ssid": "xxx", "rssi": -55} }
// 返回给 self.get_device_status 工具
}
MCP 工具 self.get_device_status(第 6 章 6.7.2)的实现来源。
9.3.5 GetNetworkStateIcon()
const char* WifiBoard::GetNetworkStateIcon() {
if (in_config_mode_) return FONT_AWESOME_GEAR;
auto rssi = WifiManager::GetInstance().GetRssi();
if (rssi == 0) return FONT_AWESOME_WIFI_SLASH;
if (rssi > -50) return FONT_AWESOME_WIFI_HIGH;
if (rssi > -65) return FONT_AWESOME_WIFI_MEDIUM;
return FONT_AWESOME_WIFI_LOW;
}
屏幕顶部 network_label_ 显示什么 Font Awesome 图标。RSSI 阈值是经验值(-50 强、-65 中、其它弱)。
9.4 Ml307Board —— 4G 蜂窝板基类
274 行。基于上海移芯通信的 ML307R Cat.1 模块(UART AT 命令控制)。
特点(vs WiFi):
- 无配网阶段——SIM 卡插上就能用,无需用户参与;
- PPP 拨号上网:ESP32 通过 UART 跟 ML307 通信,ML307 跟基站建立 PPP 链路;
- 网络速度有限(Cat.1 上行约 5Mbps 下行 10Mbps,但延迟稳定);
- 状态比 Wi-Fi 多:SIM 卡未插 / 网络未注册 / 信号弱 / 漫游等。
9.4.1 Modem 检测和错误事件
enum class NetworkEvent {
// ... Wi-Fi 共用的事件
ModemDetecting, // 正在自动识别 baud rate + 模块型号
ModemErrorNoSim, // 没 SIM 卡
ModemErrorRegDenied, // 网络注册被运营商拒绝
ModemErrorInitFailed, // 初始化失败
ModemErrorTimeout // 超时
};
Ml307Board::StartNetwork() 内部依次:
- 初始化 UART(最常见 921600 baud);
- 发 AT 命令检测模块在线(响应 OK);
- 检测模块型号(
AT+CGMM); - 检测 SIM 卡状态(
AT+CPIN?); - 等待网络注册(
AT+CREG?返回 1 或 5 表示已注册本地或漫游); - 启动 PPP;
- 触发
Connected事件。
每一步失败都触发对应 ModemError* 事件,让屏幕显示具体原因。
9.4.2 GetNetwork() 返回的不同栈
NetworkInterface* WifiBoard::GetNetwork() {
return EspNetworkAdapter::GetInstance(); // ESP-IDF 自带 LWIP 栈
}
NetworkInterface* Ml307Board::GetNetwork() {
return Ml307AtModem::GetInstance(); // ML307 内置 TCP/IP 栈,通过 AT 命令调用
}
关键设计:NetworkInterface 是抽象接口,定义了 CreateHttp / CreateWebSocket / CreateMqtt / CreateUdp 4 个工厂方法。Wi-Fi 板返回基于 LWIP 的实现,4G 板返回基于 ML307 AT 命令的实现——协议层完全感知不到差异。
这就是为什么第 5 章 protocols 全部代码可以同时在 Wi-Fi 和 4G 板上跑。
9.5 DualNetworkBoard —— 双网切换
98 行,最简洁但用法巧妙。
9.5.1 思路
某些板子既有 Wi-Fi 又有 4G 模块(如 atk-dnesp32s3-box2-4g),用户可以选用其中之一:
- 室内 Wi-Fi 信号好 → 用 Wi-Fi 省流量;
- 户外 / Wi-Fi 不稳定 → 切到 4G 用流量包。
实现方式不是合并两套代码,而是内部包两个 sub-board:
class DualNetworkBoard : public Board {
private:
std::unique_ptr<WifiBoard> wifi_board_;
std::unique_ptr<Ml307Board> ml307_board_;
NetworkType current_type_ = NetworkType::WIFI;
public:
DualNetworkBoard(int ml307_tx, int ml307_rx, gpio_num_t reset) {
// 创建两个内部子板
Settings settings("board", false);
std::string saved = settings.GetString("network_type", "wifi");
if (saved == "ml307") current_type_ = NetworkType::ML307;
}
Board& GetCurrentBoard() {
if (current_type_ == NetworkType::WIFI) return *wifi_board_;
else return *ml307_board_;
}
// 所有接口都转发给 GetCurrentBoard()
NetworkInterface* GetNetwork() override { return GetCurrentBoard().GetNetwork(); }
void StartNetwork() override { GetCurrentBoard().StartNetwork(); }
// ...
void SwitchNetworkType() {
current_type_ = (current_type_ == NetworkType::WIFI) ? NetworkType::ML307 : NetworkType::WIFI;
Settings settings("board", true);
settings.SetString("network_type", current_type_ == NetworkType::WIFI ? "wifi" : "ml307");
esp_restart(); // 简单粗暴:重启重新初始化
}
};
设计要点:
- 当前网络类型存 NVS——下次启动直接用上次的;
- 接口透传给当前 sub-board;
SwitchNetworkType()直接重启——避免在运行时切换网络栈带来的复杂状态机问题(哪个网络断开、新的开起来、所有连接重建等)。重启代价就是几秒钟。
用户触发切换:长按某个按键、按某个组合键或菜单选项——具体哪个按键由板子决定。
9.6 共用零件:Button / Backlight / PowerSaveTimer
9.6.1 Button —— 6 种事件
class Button {
public:
Button(gpio_num_t gpio_num, bool active_high = false,
uint16_t long_press_time = 0, uint16_t short_press_time = 0,
bool enable_power_save = false);
void OnPressDown(std::function<void()>);
void OnPressUp(std::function<void()>);
void OnLongPress(std::function<void()>);
void OnClick(std::function<void()>);
void OnDoubleClick(std::function<void()>);
void OnMultipleClick(std::function<void()>, uint8_t click_count = 3);
};
底层基于 iot_button 组件(Espressif 提供的按键库):
- 去抖:内部 debounce 算法过滤毛刺;
- 6 类事件:按下/抬起/长按/单击/双击/多击;
enable_power_save:light-sleep 模式下保持响应(特殊配置);active_high:按下为高电平还是低电平。
典型用法(compact_wifi_board.cc 第 103 行):
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode();
return;
}
app.ToggleChatState(); // 启动后单击 boot 键开始对话
});
touch_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
touch_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
按住 touch 按键说话(PressDown → PressUp 配对),单击 boot 按键切换状态。
9.6.2 Backlight —— 屏幕背光控制
backlight.h:
class Backlight {
public:
Backlight(...);
void SetBrightness(uint8_t brightness, bool save = false);
uint8_t GetBrightness() const;
void RestoreBrightness(); // 从 NVS 读回
};
背光是独立的 PWM GPIO(不是 LCD 的内部控制),用 LEDC 外设调节亮度。save = true 时写 NVS display.brightness,下次开机恢复。
9.6.3 PowerSaveTimer + SleepTimer
两个 timer 配合实现省电:
- PowerSaveTimer:进入 idle 状态 N 秒后降频(CPU 80MHz → 40MHz);
- SleepTimer:再 idle M 秒后进入 light_sleep(CPU 完全停,等中断唤醒)。
// 简化实现
class PowerSaveTimer {
void Start() { esp_timer_start_once(timer_, timeout * 1000000); }
void Stop() { esp_timer_stop(timer_); }
void OnTimer() {
auto state = Application::GetInstance().GetDeviceState();
if (state == kDeviceStateIdle) {
Board::GetInstance().SetPowerSaveLevel(PowerSaveLevel::LOW_POWER);
}
}
};
非 Idle 状态(说话/听话/升级中)会重置 timer——保证不会在工作时降频。
电池供电板(atom-echo 等)一定要开省电否则跑不了几小时,USB 供电板可以选择性关闭。
9.7 代表板子 1:bread-compact-wifi(最小 Wi-Fi 麦克风)
定位:入门面包板套件。最便宜的配置——ESP32-S3 + OLED + 单麦克风 + 单喇叭 + 4 按键 + 1 WS2812B。
9.7.1 配置 (config.h)
#define AUDIO_INPUT_SAMPLE_RATE 16000
#define AUDIO_OUTPUT_SAMPLE_RATE 24000
#define AUDIO_I2S_METHOD_SIMPLEX
// I²S 麦克风(MEMS, 标准 SPH0645)
#define AUDIO_I2S_MIC_GPIO_WS GPIO_NUM_4
#define AUDIO_I2S_MIC_GPIO_SCK GPIO_NUM_5
#define AUDIO_I2S_MIC_GPIO_DIN GPIO_NUM_6
// I²S 喇叭(PCM5102 / MAX98357 等)
#define AUDIO_I2S_SPK_GPIO_DOUT GPIO_NUM_7
#define AUDIO_I2S_SPK_GPIO_BCLK GPIO_NUM_15
#define AUDIO_I2S_SPK_GPIO_LRCK GPIO_NUM_16
#define BUILTIN_LED_GPIO GPIO_NUM_48 // WS2812B
#define BOOT_BUTTON_GPIO GPIO_NUM_0
#define TOUCH_BUTTON_GPIO GPIO_NUM_47
#define VOLUME_UP_BUTTON_GPIO GPIO_NUM_40
#define VOLUME_DOWN_BUTTON_GPIO GPIO_NUM_39
#define DISPLAY_SDA_PIN GPIO_NUM_41
#define DISPLAY_SCL_PIN GPIO_NUM_42
#define DISPLAY_WIDTH 128
// Kconfig 选择哪种 OLED
#if CONFIG_OLED_SSD1306_128X32
#define DISPLAY_HEIGHT 32
#elif CONFIG_OLED_SSD1306_128X64
#define DISPLAY_HEIGHT 64
#elif CONFIG_OLED_SH1106_128X64
#define DISPLAY_HEIGHT 64
#define SH1106
#endif
#define LAMP_GPIO GPIO_NUM_18 // MCP 演示:控制一颗灯
Simplex I²S:用两组 I²S 引脚(一组麦克风 in,一组喇叭 out),不共享 BCLK/WS。逻辑简单,开发板布线灵活。
为什么 input 16k / output 24k:
- 输入 16k 够覆盖人声频段(8k 奈奎斯特),AFE/wake word 都按 16k 设计;
- 输出 24k 是 LLM TTS 的常用配置(音色更好),但稍微多耗带宽。
9.7.2 实现 (compact_wifi_board.cc)
class CompactWifiBoard : public WifiBoard {
private:
i2c_master_bus_handle_t display_i2c_bus_;
esp_lcd_panel_io_handle_t panel_io_;
esp_lcd_panel_handle_t panel_;
Display* display_ = nullptr;
Button boot_button_;
Button touch_button_;
Button volume_up_button_;
Button volume_down_button_;
void InitializeDisplayI2c() {
i2c_master_bus_config_t bus_config = {
.i2c_port = 0,
.sda_io_num = DISPLAY_SDA_PIN,
.scl_io_num = DISPLAY_SCL_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags = { .enable_internal_pullup = 1 },
};
i2c_new_master_bus(&bus_config, &display_i2c_bus_);
}
void InitializeSsd1306Display() {
esp_lcd_panel_io_i2c_config_t io_config = {
.dev_addr = 0x3C, // SSD1306 I²C 地址固定 0x3C
.control_phase_bytes = 1,
.dc_bit_offset = 6,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.scl_speed_hz = 400 * 1000, // 400 kHz Fast Mode
};
esp_lcd_new_panel_io_i2c_v2(display_i2c_bus_, &io_config, &panel_io_);
esp_lcd_panel_dev_config_t panel_config = {};
panel_config.reset_gpio_num = -1;
panel_config.bits_per_pixel = 1;
esp_lcd_panel_ssd1306_config_t ssd1306_config = { .height = static_cast<uint8_t>(DISPLAY_HEIGHT) };
panel_config.vendor_config = &ssd1306_config;
#ifdef SH1106
esp_lcd_new_panel_sh1106(panel_io_, &panel_config, &panel_);
#else
esp_lcd_new_panel_ssd1306(panel_io_, &panel_config, &panel_);
#endif
esp_lcd_panel_reset(panel_);
if (esp_lcd_panel_init(panel_) != ESP_OK) {
display_ = new NoDisplay(); // 屏挂了 fallback
return;
}
esp_lcd_panel_disp_on_off(panel_, true);
display_ = new OledDisplay(panel_io_, panel_, DISPLAY_WIDTH, DISPLAY_HEIGHT, DISPLAY_MIRROR_X, DISPLAY_MIRROR_Y);
}
void InitializeButtons() {
boot_button_.OnClick([this]() {
auto& app = Application::GetInstance();
if (app.GetDeviceState() == kDeviceStateStarting) {
EnterWifiConfigMode(); // 启动阶段单击进配网
return;
}
app.ToggleChatState(); // 已启动单击开始/结束对话
});
touch_button_.OnPressDown([this]() {
Application::GetInstance().StartListening();
});
touch_button_.OnPressUp([this]() {
Application::GetInstance().StopListening();
});
volume_up_button_.OnClick([this]() {
auto codec = GetAudioCodec();
auto volume = codec->output_volume() + 10;
if (volume > 100) volume = 100;
codec->SetOutputVolume(volume);
GetDisplay()->ShowNotification(Lang::Strings::VOLUME + std::to_string(volume));
});
volume_up_button_.OnLongPress([this]() {
GetAudioCodec()->SetOutputVolume(100);
GetDisplay()->ShowNotification(Lang::Strings::MAX_VOLUME);
});
// ... 类似 volume_down_button_
}
void InitializeTools() {
static LampController lamp(LAMP_GPIO); // 注册一个 MCP 工具控制 GPIO18 上的灯
}
public:
CompactWifiBoard() :
boot_button_(BOOT_BUTTON_GPIO),
touch_button_(TOUCH_BUTTON_GPIO),
volume_up_button_(VOLUME_UP_BUTTON_GPIO),
volume_down_button_(VOLUME_DOWN_BUTTON_GPIO) {
InitializeDisplayI2c();
InitializeSsd1306Display();
InitializeButtons();
InitializeTools();
}
Led* GetLed() override {
static SingleLed led(BUILTIN_LED_GPIO); // 单颗 WS2812B
return &led;
}
AudioCodec* GetAudioCodec() override {
static NoAudioCodecSimplex audio_codec(
AUDIO_INPUT_SAMPLE_RATE, AUDIO_OUTPUT_SAMPLE_RATE,
AUDIO_I2S_SPK_GPIO_BCLK, AUDIO_I2S_SPK_GPIO_LRCK, AUDIO_I2S_SPK_GPIO_DOUT,
AUDIO_I2S_MIC_GPIO_SCK, AUDIO_I2S_MIC_GPIO_WS, AUDIO_I2S_MIC_GPIO_DIN);
return &audio_codec;
}
Display* GetDisplay() override { return display_; }
};
DECLARE_BOARD(CompactWifiBoard);
典型流程:
- 构造函数初始化 4 个按键句柄;
- 函数体里依次跑 4 个 Init 函数;
- 5 个虚函数重写(implicit Wi-Fi 网络)。
注意 LampController lamp(LAMP_GPIO) 是 static 局部变量——首次调用 InitializeTools() 时构造,构造里向 McpServer 注册一个 "灯开关" 工具,让 LLM 可以"打开桌上的灯"。这是项目自带的最小 MCP 演示。
9.8 代表板子 2:bread-compact-ml307(4G 双模版本)
跟 9.7 几乎一样,但继承 DualNetworkBoard:
class CompactMl307Board : public DualNetworkBoard {
public:
CompactMl307Board() : DualNetworkBoard(ML307_TX_PIN, ML307_RX_PIN, GPIO_NUM_NC), ... { }
};
特有逻辑:
boot_button_.OnClick([this]() {
if (GetNetworkType() == NetworkType::WIFI) {
if (app.GetDeviceState() == kDeviceStateStarting) {
// 转换 wifi_board 调 EnterWifiConfigMode
auto& wifi_board = static_cast<WifiBoard&>(GetCurrentBoard());
wifi_board.EnterWifiConfigMode();
return;
}
}
app.ToggleChatState();
});
boot_button_.OnDoubleClick([this]() {
if (app.GetDeviceState() == kDeviceStateStarting || app.GetDeviceState() == kDeviceStateWifiConfiguring) {
SwitchNetworkType(); // 双击切换 Wi-Fi / 4G
}
});
双击切换网络类型,单击在 Wi-Fi 模式下进配网。static_cast<WifiBoard&> 是因为已经知道 GetNetworkType() == WIFI。
9.9 代表板子 3:esp-box-3(旗舰彩屏板)
文件清单不一样:
esp-box-3/
├── config.h GPIO 定义
├── config.json 板子元数据
├── esp_box3_board.cc 板子实现(继承 WifiBoard)
├── emote.json EmoteDisplay 用的表情/图标/布局
├── layout.json UI 元素位置
└── README.md
特点:
- 彩色 320×240 IPS LCD(ST7789 SPI 总线,走 SpiLcdDisplay 路径,但本板用 EmoteDisplay);
- 电容触摸屏(GT911 I²C);
- ES8311 音频编解码器(专业 codec 替代裸 I²S+MEMS 麦克风);
- 背光 PWM 控制;
- 加速度计 BMI270(可选体感操作);
- 更大 SPI Flash(16 MB) + PSRAM(必须,跑 LVGL 彩屏耗内存)。
esp_box3_board.cc 比 compact 板代码量大 3-4 倍,但套路完全一样:
- I²C bus 初始化(共用一条 bus 接 GT911 + ES8311 + 加速度计);
- LCD 初始化(SPI panel + ST7789 driver);
- 触摸初始化;
- 音频 codec 初始化(
Es8311AudioCodec); - 按键(板上 3 个物理键);
- Backlight;
- 各种 MCP 工具注册。
由于代码量较大不全文复述。读者按需求点开 esp_box3_board.cc 看。关键认知:所有彩屏板都按"I²C bus + LCD panel + 触摸 + codec + 按键 + 工具"6 部分组织。
9.10 其它 107 个板子的简表
按命名前缀整理:
| 前缀 | 数量 | 类型 | 代表 |
|---|---|---|---|
atk-dnesp32s3* | 7 | 正点原子 S3 系列(含 4G) | atk-dnesp32s3-box, atk-dnesp32s3m-4g |
atom-* / atoms3* | 5 | M5Stack Atom Echo 系列 | atom-echos3r, atoms3-echo-base |
bread-compact-* | 5 | 面包板套件(最入门) | bread-compact-wifi/-ml307/-lcd |
esp-box* | 3 | Espressif 官方 Box 板 | esp-box, esp-box-3, esp-box-lite |
df-* | 2 | DFRobot 系列 | df-k10, df-s3-ai-cam |
esp32-* | 4 | 普通 ESP32(非 S3) | esp32-cgc, esp32-cgc-144 |
esp32-s3-touch-* | 多个 | 触摸彩屏板 | s3-touch-amoled-1.8, s3-touch-lcd-1.46 |
doit-s3-aibox / du-chatx / echoear | 3 | 设备厂私有板 | — |
esp-p4-function-ev-board | 1 | P4 评估板(最高端) | — |
esp-s3-lcd-ev-board* | 2 | Espressif S3 RGB LCD 评估板 | — |
esp-spot | 1 | 最便宜的 S3 套件 | — |
esp-hi | 1 | 高端音质板 | — |
electron-bot | 1 | 桌面机器人形态 | — |
esp-sensairshuttle | 1 | 带传感器扩展板 | — |
esp-sparkbot | 1 | 小机器人 | — |
| 其它 60+ | — | 各种第三方板子 | 看名字推测 |
每个板子大致这些信息:
config.h:GPIO 定义 + 屏幕尺寸 + 采样率;config.json:板子元数据(名字、描述、链接、屏幕信息);<name>_board.cc:板子类实现(按 9.7 的套路);- 可选
emote.json/layout.json:EmoteDisplay 资源; - 可选
README.md:板子专属说明。
按 chip / 显示器 / 麦克风 / 喇叭 / 网络栈 / 电源 6 个维度去推断板子组合即可。
9.11 加 boards/<new_board>/ 添新板的最小步骤
- 复制最接近的板子目录作为模板,重命名;
- 改
config.h:所有 GPIO + 屏幕尺寸 + 采样率改成新板; - 改
<name>_board.cc:类名 + 显示器 driver + codec 类型 + Init 函数; - 加
DECLARE_BOARD(NewBoardName)在文件末尾; - 在
Kconfig.projbuild加config BOARD_TYPE_<NEW_NAME>,并把它加入choice BOARD_TYPE; - 在
CMakeLists.txt加对应的if(CONFIG_BOARD_TYPE_<NEW_NAME>) set(BOARD_TYPE "<new_name>") endif(); - 更新
partitions/:根据 Flash 大小选 4MB / 8MB / 16MB 分区表; menuconfig选板子 → 编译 → 烧录。
整个过程 100% 不动其他 109 个板子和上层代码。
9.12 本章用到的关键技术
| 技术 | 应用 |
|---|---|
| 抽象基类 + 多层继承 | Board → WifiBoard → 具体板 |
| DECLARE_BOARD 宏 + 链接选板 | 编译时选板,零运行时开销 |
| NetworkInterface 抽象 | Wi-Fi/4G 网络栈对协议层透明 |
std::unique_ptr 持有 sub-board | DualNetworkBoard 双栈 |
| RFC 4122 UUIDv4 + esp_fill_random | 设备软件唯一标识 |
R"(...)" 原始字符串字面量 | 手写 JSON 模板 |
| NVS 持久化 network_type / uuid / brightness | 跨重启状态 |
iot_button + 6 类事件 | 按键标准化 |
| LEDC 外设 PWM 控背光 | 屏幕亮度 |
| I²C bus 共享多设备 | 节省 GPIO(display + codec + touch 共一组 SDA/SCL) |
esp_lcd_panel_* API | 屏幕驱动统一接口(SSD1306 / SH1106 / ST7789 / GC9A01 等) |
| dynamic_cast 检查子板类型 | DualNetworkBoard 切到 WifiBoard 调专属方法 |
| Static 局部变量做 lazy init | GetLed/GetAudioCodec 等的常见模式 |
| 3 种配网(Hotspot / BLE / 声波)+ Kconfig 同时启用 | 灵活适配 |
| Modem AT 命令 + PPP | 4G 板的网络初始化 |
PowerSaveTimer + SleepTimer | 自动降频 + light_sleep |
9.13 看完本章你应该掌握的
Board单例工厂 +DECLARE_BOARD宏的工作原理- 4 个继承层级(Board / WifiBoard / Ml307Board / DualNetworkBoard)
- 网络栈抽象 NetworkInterface 让协议层不感知 Wi-Fi vs 4G
- Wi-Fi 板的连接→超时→配网状态机
- 3 种配网方式(Hotspot / BluFi / Acoustic AFSK)
- 4G 板的 Modem 检测和错误事件
- DualNetworkBoard 双栈切换的"重启大法"
- Board 基类的 UUID 生成逻辑
- GetSystemInfoJson 手写 JSON 的内容结构
- Button 6 类事件 + 典型用法
- 一个具体板子(compact-wifi)的 Init* / GetXxx 6 个标准函数
- 添加新板子的最小修改清单
下一章覆盖剩下的辅助资源:scripts/ / docs/ / partitions/ / Kconfig / CMake。