AI小智硬件程序(八)
构建开发板抽象层

我们已经完成了音频抽象的部分,现在开始实现板子抽象的部分。
一、创建板子抽象层

board.h
cpp
#pragma once
#include <audio_hal.h>
void* create_board();
class Board
{
protected:
Board();
private:
// 禁用拷贝构造函数
Board(const Board&) = delete;
// 禁用赋值操作
Board& operator=(const Board&) = delete;
public:
virtual ~Board() = default;
static Board& GetInstance() {
static Board* instance = static_cast<Board*>(create_board());
return *instance;
}
virtual AudioHAL* GetAudioHAL() = 0;
};
#define REGISTER_BOARD(BOARD_TYPE_CLASS_NAME) \
void* create_board() { \
return new BOARD_TYPE_CLASS_NAME(); \
}
board.c
cpp
#include "board.h"
Board::Board()
{
}
本质上是在实现一个基于单例模式和工厂模式的开发板抽象层,为不同硬件的开发板提供统一的接口,同时保证整个程序中只有一个开发板实例。
bash
static Board* instance = static_cast<Board*>(create_board());
这句是最关键的,定义了一个全局只初始化一次的Board基类指针instance,它的初始值是:调用「开发板工厂函数」create_board()创建一个具体的开发板子类对象(比如 ESP32 板子),再把这个子类对象的指针转成基类指针,存到instance里。
二、修改 CmakeList.txt

cpp
"board/board.cpp"
"board"
三、创建开发板类型

ioelin_dev_board.h
cpp
#pragma once
#include "board.h"
#include <audio_hal.h>
class IoelinDevBoard:public Board
{
private:
/* data */
public:
IoelinDevBoard();
~IoelinDevBoard();
AudioHAL* GetAudioHAL() override;
};
ioelin_dev_board.cpp
cpp
#include "ioelin_dev_board.h"
IoelinDevBoard::IoelinDevBoard()
{
}
IoelinDevBoard::~IoelinDevBoard()
{
}
AudioHAL* IoelinDevBoard::GetAudioHAL()
{
return nullptr;
}
REGISTER_BOARD(IoelinDevBoard);
这两段代码,是针对「IoelinDevBoard」这款具体开发板的实现代码,也是我们之前聊的「开发板抽象层」的实际落地------ 简单说,就是为这款特定板子实现了抽象基类Board要求的接口,并且完成了注册,让整个程序能识别并使用这款板子。
四、创建 Kconfig.projbuild
Kconfig.projbuild是项目级的配置文件(常见于 ESP-IDF/Zephyr 等嵌入式框架),核心作用是通过menuconfig图形界面定义项目的可配置选项。

cpp
menu "Board Configuration"
choice BOARD_TYPE
prompt "Board Type"
default BOARD_TYPE_IOELIN_DEV
config BOARD_TYPE_IOELIN_DEV
bool "杯面_白ESP32-S3开发板"
endchoice
endmenu

五、修改 CmakeList.txt

cpp
# 新增开发板需要再这里类似写一个配置BOARD_TYPE的宏为板子类型文件夹名称
if(CONFIG_BOARD_TYPE_IOELIN_DEV)
set(BOARD_TYPE "ioelin-dev")
# elseif(CONFIG_BOARD_TYPE_XXX_XXX)
# set(BOARD_TYPE "xxx-xxx")
endif()
file(GLOB BOARD_TYPE_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/board/${BOARD_TYPE}/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/board/${BOARD_TYPE}/*.c
)
list(APPEND SOURCES ${BOARD_TYPE_SOURCES})
通过 Kconfig.projbuild 定义 CONFIG_BOARD_TYPE_XXX 配置项,再结合 CMake 的 if-else 和 file(GLOB) 自动匹配文件,核心价值就是避免手动修改 CMakeLists.txt 里的文件列表------ 新增开发板时,只需要加 Kconfig 选项和对应的文件夹,不用改 CMake 代码,极大降低了维护成本。
总的结构
project/
├── board/
│ ├── ioelin-dev/ # Ioelin板子的代码
│ │ ├── ioelin_dev_board.h
│ │ └── ioelin_dev_board.cpp
│ └── esp32-dev/ # ESP32板子的代码
│ ├── esp32_dev_board.h
│ └── esp32_dev_board.cpp
├── CMakeLists.txt
└── Kconfig.projbuild
开发板抽象功能实现
1、管脚配置

cpp
/* I2C port and GPIOs */
#define AUDIO_I2C_NUM (I2C_NUM_0)
#define AUDIO_I2C_SDA_IO GPIO_NUM_2
#define AUDIO_I2C_SCL_IO GPIO_NUM_1
/* I2S port and GPIOs */
#define AUDIO_I2S_MCK_IO GPIO_NUM_38
#define AUDIO_I2S_BCK_IO GPIO_NUM_14
#define AUDIO_I2S_WS_IO GPIO_NUM_13
#define AUDIO_I2S_DI_IO GPIO_NUM_12
#define AUDIO_I2S_DO_IO GPIO_NUM_45
/* 功放使能IO */
#define AUDIO_PA_EN_IO GPIO_NUM_18
/*** 音频输入输出采样率*/
#define AUDIO_OUT_SAMPLE_RATE (16000)
#define AUDIO_IN_SAMPLE_RATE (16000)
#define ES8311_I2C_ADDR 0x18
#define ES7210_I2C_ADDR 0x41
这里是在为ioelin_dev_board这款开发板,定义硬件相关的 "管脚、通信参数、芯片配置" 等宏常量,相当于把这款板子的硬件细节(比如 I2C 用哪个端口、音频芯片的 I2C 地址)统一集中在头文件里,方便后续代码直接引用。
2、初始化 i2c

cpp
i2c_master_bus_handle_t init_i2c(i2c_port_t i2c_port,gpio_num_t i2c_sda_pin,gpio_num_t i2c_scl_pin);
定义一个通用的 I2C 初始化接口,规定 "初始化 I2C 需要传入 3 个参数"(I2C 端口号、SDA 引脚、SCL 引脚),返回初始化后的 I2C 总线句柄

cpp
i2c_master_bus_handle_t Board::init_i2c(i2c_port_t i2c_port,gpio_num_t i2c_sda_pin,gpio_num_t i2c_scl_pin) {
i2c_master_bus_handle_t i2c_bus;
i2c_master_bus_config_t i2c_bus_cfg = {
.i2c_port = i2c_port,
.sda_io_num = i2c_sda_pin,
.scl_io_num = 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, &i2c_bus));
return i2c_bus;
}
实现通用的 I2C 初始化逻辑 ------ 不管什么板子,只要传入正确的端口和引脚,就能用这段代码初始化出可用的 I2C 总线

cpp
i2c_master_bus_handle_t i2c0_bus;
为IoelinDevBoard专属的 I2C0 总线预留 "存储位置",后续初始化后的 I2C 句柄会存在这里,方便这款板子的其他函数(比如音频初始化)调用

cpp
i2c0_bus = Board::init_i2c(AUDIO_I2C_NUM,AUDIO_I2C_SDA_IO,AUDIO_I2C_SCL_IO);
给通用的 I2C 初始化逻辑 "喂" 上IoelinDevBoard的专属硬件参数(这些宏就是你之前看到的AUDIO_I2C_NUM=I2C_NUM_0、AUDIO_I2C_SDA_IO=GPIO2等),初始化出这款板子的 I2C0 总线,并把句柄存到i2c0_bus里
3、初始化音频

cpp
AudioHAL* audio_hal;

cpp
audio_hal = new AudioEs8311Es7210(i2c0_bus,
AUDIO_I2C_NUM,
AUDIO_IN_SAMPLE_RATE,
AUDIO_OUT_SAMPLE_RATE,
AUDIO_I2S_MCK_IO,
AUDIO_I2S_BCK_IO,
AUDIO_I2S_WS_IO,
AUDIO_I2S_DO_IO,
AUDIO_I2S_DI_IO,
AUDIO_PA_EN_IO,
true,
ES8311_I2C_ADDR,
ES7210_I2C_ADDR,
true);
cpp
AudioHAL* IoelinDevBoard::GetAudioHAL()
{
return audio_hal;
}