本文及之前的几篇文章立足于立创实战派S3的开发板的学习,仅做学习笔记,可能有些错误,我自己也做过一些修正,但随着自己编写程序的验证,思路上没有问题,希望这种方法也能给其他业余电子爱好者带来帮助。
很遗憾的是这次只是把程序过程看懂,关于ES8311和ES7210的寄存器这块,因为时间关系并没有全部整理出来,想搞明白寄存器作和还要再等等。

关于回调函数

分析程序前首先回顾回调函数的概念,这里不具体举例了,网上很多,可以多找几个带例子的视频看看,加深点直观感觉。
我自己的理解是回调函数是一个函数变量,函数变量里面并不是存放一整段函数,而是具体用来指向某个函数的首地址,因此变量的赋值是函数的首地址指针。
每定义出一个函数变量,其本身在没赋值前没有意义,通过赋值以后,就具体指向了某个函数。这种用法很灵活,在今天读代码的过程中,用法主要是对接驱动。
比如现在有了实现具体功能的程序,也有硬件设备的驱动程序,功能程序有自己的参数和需求,这种需求不能提前告知所有驱动,驱动程序随着硬件变化,提供的功能可能有些小区别,调用时需要的参数规范可能也不一样,不能每次换硬件或是实现不同功能时,都要修改驱动程序或是功能程序。
于是必须要有分工,模块化工作,功能模块干功能模块的事,驱动模块干驱动模块的事,大家的结合找中间商,没什么事是加一层中间层不能解决的,如果不行,那就再加一层中间层,对功能模块和驱动模块进行结合,这是数字电路中比较常见的需求。
今天分析的程序模块中就要用到这个回调函数,这样在程序编写中,即便不知道驱动是什么样的,只要给出能提供的参数和功能程序所需的返回值,定义一个回调函数(也就是函数变量)就行了。
然后根据驱动程序和功能程序要求,自己编写具体适应功能模块和驱动模块要求的函数作为中间层。使用时,通过初始化,把自己编写的用于耦合的中间函数地址赋值给回调函数,功能程序部分就能利用回调函数顺利的调用驱动了。就如同电脑是USB接口(功能模块),但手机上有的TYPE-C接口,有的是MINI USB(驱动模块),如何便利的连接?用根转接线,一头是USB,另一头根据手机变化来变。这里电脑是功能模块,手机是驱动模块,那么USB就是电脑的回调函数。
其实这种事大家不用回调函数也能干成,直接调用一个固定名称的函数,将来要用不同驱动,那把这个固定名称的函数进行修改就可以了。但使用回调函数的优点是传递参数更方便,因为是指针,所以调用过程中占用资源少,参数及返回值实现了共享,另外,既然是变量,那么把变量,也就是回调函数定义成结构体变量,分别指向不同但又相关的中间商,那就构成了一组操作方法,比较灵活。
回调函数的核心其实还是要看中间商的内容。
回调函数的缺点就是套娃,有时候为了多适应,要一层层的套,由于命名的相似性,套多了读程序就很费劲了。

关于组件

前面在学习立创实战派S3的开发板时,主要是看嘉立创官方提供的单个例程,在寄存器等功能上了解的不深入,整合使用没有基础。于是下定决心从乐鑫官方的组件入手读程序,深入了解寄存器。
乐鑫官方对ES7210和ES8311都有单独的IDF组件提供,缺点是这个组件是以前的,不更新了,使用的I2C接口是旧版IDF的,不兼容新版。现在组件新版整合了很CODEC芯片,统一用ESP Codec Device组件,例程中在寄存器使用上也与旧版有些区别。
本次主要就是通过分析ESP Codec Device的代码来深入了解ESP8311这个芯片。
注意,ESP Codec Device组件把ES系列芯片大体分为输入设备和输出设备两种,对应不同的芯片,在中间层又套了个关于输入输出的分类,这样程序又复杂了点。
具体抽象的这些内容可以看一下https://components.espressif.com/components/espressif/esp_codec_dev

读程序

找到esp_codec_dev\test_apps\test_board.c,找到其中
TEST_CASE(“esp codec dev test using S3 board”, “[esp_codec_dev]”)
从这段开始跟进

TEST_CASE("esp codec dev test using S3 board", "[esp_codec_dev]")
{
    // Need install driver (i2c and i2s) firstly
    int ret = ut_i2c_init(0);
    TEST_ESP_OK(ret);
    ret = ut_i2s_init(0);
    TEST_ESP_OK(ret);
    //主要内容找ut_i2c_init和ut_i2s_init这两个中间商,来初始化I2C和I2S总线,参数是使用哪个I2C端口以及哪个I2S端口,跟进去会有对应的端口号设置以及与硬件相关的引脚设置
    //初始化后,会把I2S和I2C的句柄放入提前定义好的全局变量中

    // Do initialize of related interface: data_if, ctrl_if and gpio_if
    audio_codec_i2s_cfg_t i2s_cfg = {
        .rx_handle = i2s_keep[0]->rx_handle,
        .tx_handle = i2s_keep[0]->tx_handle,
    };
    //事先定义了全局变量*i2s_keep[I2S_MAX_KEEP],用作存放i2s的通道句柄(一堆控制I2S资源的首地址),这里I2S_MAX_KEEP根据I2S的通道数来定义,S3为2
    //官方把收发的I2S配置按收发分开存放

    const audio_codec_data_if_t *data_if = audio_codec_new_i2s_data(&i2s_cfg);
    TEST_ASSERT_NOT_NULL(data_if);
    //这儿是新建I2S数据区,具体实现时用的是回调函数,并初始化了一个实例data_if,提供了操作方法,并通过返回值赋给了data_if
    //后面就可以用data_if->或data_if.提供的方法来实现声音数据发送和接收

    audio_codec_i2c_cfg_t i2c_cfg = {.addr = ES8311_CODEC_DEFAULT_ADDR};
#ifdef USE_IDF_I2C_MASTER
    i2c_cfg.bus_handle = i2c_bus_handle;
#endif
    const audio_codec_ctrl_if_t *out_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
    TEST_ASSERT_NOT_NULL(out_ctrl_if);
    //这段创建了一个控制I2C的具体控制实例out_ctrl_if,地址是ES8311的,初始化了各个回调函数具体指向函数,并添加上I2C设备端口号地址等各种属性,供后续的调用
    //这里注意的是关于SCK频率不是通过配置变量传递的,而是在中间商的具体操作中直接引用定义,这个过程中进行了一部分初始化内容

    i2c_cfg.addr = ES7210_CODEC_DEFAULT_ADDR;
    const audio_codec_ctrl_if_t *in_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);
    TEST_ASSERT_NOT_NULL(in_ctrl_if);
    //把地址换成ES7210的,再创建一个in_ctrl_if具体控制实例
    //注意创建这两个实例时有个坑,巨坑。这位组件开发者把I2C的地址按1-7位来处理,不是我们通常用的0-6位
    //一般我们说ES8311地址是0x18,换成二进制是00011000,有效数据在0-6位。如果按1-7位就是00110000,十六进制是0x30
    //由于I2S的官方标准库中都是按0-6位来处理数据的,所以组件开发者又在后面写了个右移1位指令再送给驱动模块处理,测试程序时没想那么多,就直接引用了自己定义的地址,结果找不到器件

    const audio_codec_gpio_if_t *gpio_if = audio_codec_new_gpio();
    TEST_ASSERT_NOT_NULL(gpio_if);
    //ES8311的输出需要接功放芯片,功放芯片通常有个控制开关,官方从ESP32上定义了引脚,所以要初始化
    //立创S3的板子用了IO扩展芯片,这段程序就没用了,换成用I2C初始化IO口扩展芯片,并提供了一个开关的函数pa_en(uint8_t level),具体立创的例程中有定义,比较简单

    // New output codec interface
    es8311_codec_cfg_t es8311_cfg = {
        .codec_mode = ESP_CODEC_DEV_WORK_MODE_DAC,
        .ctrl_if = out_ctrl_if,
        .gpio_if = gpio_if,
        .pa_pin = TEST_BOARD_PA_PIN,
        .use_mclk = true,
    };
    const audio_codec_if_t *out_codec_if = es8311_codec_new(&es8311_cfg);
    TEST_ASSERT_NOT_NULL(out_codec_if);
    //之前已经有了输出对象的控制实例,但具体使用时还要对ES8311芯片寄存器进行写入,才能让ES8311工作起来,于是新创建一个设备实例ES8311并初始化回调函数,
    //其实就是用控制实例对8311有针对性写入初始化内容,同时把操作方法和已写入的参数通过回调函数再返回给out_codec_if,供使用时调用

    // New input codec interface
    es7210_codec_cfg_t es7210_cfg = {
        .ctrl_if = in_ctrl_if,
        .mic_selected = ES7120_SEL_MIC1 | ES7120_SEL_MIC2,
    };
    const audio_codec_if_t *in_codec_if = es7210_codec_new(&es7210_cfg);
    TEST_ASSERT_NOT_NULL(in_codec_if);
    //同样的事对ES7210再做一次

    // New output codec device
    esp_codec_dev_cfg_t dev_cfg = {
        .codec_if = out_codec_if,
        .data_if = data_if,
        .dev_type = ESP_CODEC_DEV_TYPE_OUT,
    };
    esp_codec_dev_handle_t play_dev = esp_codec_dev_new(&dev_cfg);
    TEST_ASSERT_NOT_NULL(play_dev);
    //把ES8311的播放所需操作进行一次封装,之前是提供修改初始化ES8311的内容,到这就开始由play_dev专门控制播放功能

    // New input codec device
    dev_cfg.codec_if = in_codec_if;
    dev_cfg.dev_type = ESP_CODEC_DEV_TYPE_IN;
    esp_codec_dev_handle_t record_dev = esp_codec_dev_new(&dev_cfg);
    TEST_ASSERT_NOT_NULL(record_dev);
    //同样把ES7120的录音所需操作进行一次封装由record_dev专门控制录音功能

    ret = esp_codec_dev_set_out_vol(play_dev, 60.0);
    TEST_ESP_OK(ret);
    //设置输出音量

    ret = esp_codec_dev_set_in_gain(record_dev, 30.0);
    TEST_ESP_OK(ret);
    //设置录音增益

    esp_codec_dev_sample_info_t fs = {
        .sample_rate = 48000,
        .channel = 2,
        .bits_per_sample = 16,
    };
    ret = esp_codec_dev_open(play_dev, &fs);
    TEST_ESP_OK(ret);
    //设置播放采样率,主要是对I2S的操作
       
    ret = esp_codec_dev_open(record_dev, &fs);
    TEST_ESP_OK(ret);
    //设置录音采样率,采用了和ES8311一样的采样率

    uint8_t *data = (uint8_t *) malloc(512);
    int limit_size = 10 * fs.sample_rate * fs.channel * (fs.bits_per_sample >> 3);
    int got_size = 0;
    // Playback the recording content directly
    while (got_size < limit_size) {
        ret = esp_codec_dev_read(record_dev, data, 512);
        TEST_ESP_OK(ret);
        ret = esp_codec_dev_write(play_dev, data, 512);
        TEST_ESP_OK(ret);
        int max_sample, min_sample;
        codec_max_sample(data, 512, &max_sample, &min_sample);
         // Verify recording data not constant
        TEST_ASSERT(max_sample > min_sample);
        got_size += 512;
    }
    //录音,然后播放

    //后面都是将前面那么多套娃用的资源释放的程序
    free(data);

    ret = esp_codec_dev_close(play_dev);
    TEST_ESP_OK(ret);
    ret = esp_codec_dev_close(record_dev);
    TEST_ESP_OK(ret);
    esp_codec_dev_delete(play_dev);
    esp_codec_dev_delete(record_dev);

    // Delete codec interface
    audio_codec_delete_codec_if(in_codec_if);
    audio_codec_delete_codec_if(out_codec_if);
    // Delete codec control interface
    audio_codec_delete_ctrl_if(in_ctrl_if);
    audio_codec_delete_ctrl_if(out_ctrl_if);
    audio_codec_delete_gpio_if(gpio_if);
    // Delete codec data interface
    audio_codec_delete_data_if(data_if);

    ut_i2c_deinit(0);
    ut_i2s_deinit(0);
}

上述程序想要具体看初始化哪些参数,就要跟进具体回调函数了,下面把找一个初始化回调函数的内容进行分析,其余的大同小异,不多分析了。
跟进前,先初步了解几个结构体:

结构体1

typedef struct {
    uint8_t port;       /*!< I2C port, this port need pre-installed by other modules */
    uint8_t addr;       /*!< I2C address, default address can be gotten from codec head files */
    void   *bus_handle; /*!< I2C Master bus handle (for IDFv5.3 or higher version) */
} audio_codec_i2c_cfg_t;

这个结构体定义了i2c_cfg变量
audio_codec_i2c_cfg_t i2c_cfg

通过以下语句进行了赋值
i2c_cfg.addr = es8311地址或是i2c_cfg.addr = es7210地址
i2c_cfg.bus_handle = i2c_master_bus_t的结构体首地址,具体内容见i2c_master_bus_t结构体,官方定义的。
这个结构体我把它称为I2S的总线句柄。

结构体2

struct audio_codec_ctrl_if_t {
    int (*open)(const audio_codec_ctrl_if_t *ctrl, void *cfg, int cfg_size); 
    bool (*is_open)(const audio_codec_ctrl_if_t *ctrl);
    int (*read_reg)(const audio_codec_ctrl_if_t *ctrl, int reg, int reg_len, void *data, int data_len);
    int (*write_reg)(const audio_codec_ctrl_if_t *ctrl, int reg, int reg_len, void *data, int data_len);
    int (*close)(const audio_codec_ctrl_if_t *ctrl);
};

里面主要定义几个回调函数
(*open)这个回调函数,第一个参数传递以自己这个结构体创建的实例的首地址,第二个参数和第三个参数是可以提供给中间商的参数
(*is_open)这个回调函数,就一个参数,传递以自己这个结构体创建的实例的首地址
(*read_reg)第一个参数同上,后面同样是可以提供给中间商的参数,但比较容易看出,这个回调函数将来是用来读I2C设备寄存器的
(*write_reg)这个回调函数将来是用来写I2C设备寄存器的
(*close)只有一个参数,比较容易看出是为了删掉实例,因此把实例的首地址传递,按结构体所占空间释放存储空间资源
这个结构体我把它称为操作具体编码器的方法

结构体3

struct i2c_master_dev_t {
    i2c_master_bus_t *master_bus;         // I2C master bus base class
    uint16_t device_address;              // I2C device address
    uint32_t scl_speed_hz;                // SCL clock frequency
    uint32_t scl_wait_us;                // SCL await time (unit:us)
    i2c_addr_bit_len_t addr_10bits;       // Whether I2C device is a 10-bits address device.
    bool ack_check_disable;               // Disable ACK check
    i2c_master_callback_t on_trans_done;  // I2C master transaction done callback.
    void *user_ctx;                       // Callback user context
};

包含内容有
*master_bus,指向I2S总线句柄的首地址
device_address,16位的I2C设备地址
scl_speed_hz,I2S的SCL频率
scl_wait_us,等待时长
addr_10bits,设备地址是否是10位,枚举型的,0是7位,1是10位,官方库分别用I2C_ADDR_BIT_LEN_7和I2C_ADDR_BIT_LEN_10表示
ack_check_disable,是否允许I2C协议中应答位
on_trans_done,回调函数,暂时不理解
*user_ctx,暂时不理解
这个结构体又重新进行了指针类型的封装
typedef struct i2c_master_dev_t *i2c_master_dev_handle_t;
我把它称为i2c总线的一些基本参数

这时候,再分析这句就容易多了

const audio_codec_ctrl_if_t *out_ctrl_if = audio_codec_new_i2c_ctrl(&i2c_cfg);

首先了解参数内容,i2c_cfg包含要用哪个i2s地址,i2s设备的地址是多少,还有I2C总线的句柄
具体函数如下

const audio_codec_ctrl_if_t *audio_codec_new_i2c_ctrl(audio_codec_i2c_cfg_t *i2c_cfg)
{
    if (i2c_cfg == NULL) {
        ESP_LOGE(TAG, "Bad configuration");
        return NULL;
    }
    i2c_ctrl_t *ctrl = calloc(1, sizeof(i2c_ctrl_t));
    if (ctrl == NULL) {
        ESP_LOGE(TAG, "No memory for instance");
        return NULL;
    }
    ctrl->base.open = _i2c_ctrl_open;
    ctrl->base.is_open = _i2c_ctrl_is_open;
    ctrl->base.read_reg = _i2c_ctrl_read_reg;
    ctrl->base.write_reg = _i2c_ctrl_write_reg;
    ctrl->base.close = _i2c_ctrl_close;
    int ret = _i2c_ctrl_open(&ctrl->base, i2c_cfg, sizeof(audio_codec_i2c_cfg_t));
    if (ret != 0) {
        free(ctrl);
        return NULL;
    }
    ctrl->is_open = true;
    return &ctrl->base;
}

函数一开始是参数的检查内容,然后定义了一个i2c_ctrl_t类型的变量指针ctrl并分配存储空间
i2c_ctrl_t类型是个结构体

typedef struct {
    audio_codec_ctrl_if_t   base;
    bool                    is_open;
    uint8_t                 port;
    uint8_t                 addr;
#ifdef USE_IDF_I2C_MASTER
    i2c_master_dev_handle_t dev_handle;
#endif
} i2c_ctrl_t;

base是上面提到的操作具体编码器的方法,这个建立一个实例,然后把实例地址返回,实现向调用函数赋值具体回调函数
bool是标志位
后面两个是使用的i2c端口和i2c设备地址
dev_handle是个指针,指向i2c_master_dev_t结构体定义出的变量首地址,也就是i2c总线的一些基本参数存放地点的首地址
从这可以看出,这个叫i2c_ctrl_t的结构体创建出来的ctrl变量同时包含了操作具体编码器的方法和i2c总线的一些基本参数
即:audio_codec_ctrl_if_t = base
base + is_open + port + addr + dev_handle = audio_codec_ctrl_if_t + is_open + port + addr + i2c_master_dev_t
进一步展开,i2c_master_dev_t = 总线句柄 + 设备地址 + …

后面接着运行了一次_i2c_ctrl_open函数,这个回调函数第一个参数是自身,&ctrl->base
也就是audio_codec_ctrl_if_t结构体创建出来的变量base的首地址指针
其次传递了i2c_cfg,也就是把地址,端口,I2C句柄传入,还传入一块存储空间,大小是操作具体编码器的方法结构体大小一样,也就是各个回调函数集合的大小
最后返回的也是指向操作具体编码器的方法结构体,完成了关于某个具体器件回调函数的赋值。
看起来有些内容与参数传递的有重复,命名又很接近,虽然接口很简约舒服,但一眼看过去不太容易分辨,里面太多太乱,具体内容细说起来很烦,可以自行跟进函数去看。

看着这些像套娃一样的封装,很是无语,主要是这些音频芯片寄存器没搞明白,需要参考官方程序,否则业余爱好玩单片机搞的像组装电脑系统一样要兼容那么多硬件,累啊。

关于大坑!!!
官方对I2C地址的理解和我想的不一样,IIC的7位地址,我是按从低到高来处理的,也就是占0-6位,地址范围是0-127,这样硬件上有时有改动,加1或减1也方便。但乐鑫官方新版中没有这么做,不知道是哪位大神把这个地址按1-7位来处理,第0位补了零,然后在后面的程序中专门写了右移1位指令,再交给官方IIS库重新左移处理,这条指令在esp_codec_dev\platform\audio_codec_ctrl_i2c.c文件中,大家可以自己看看,也可以想象一下这个右移给我一开始的测试带来多大的麻烦
.device_address = (i2c_cfg->addr>>1)

具体ES8311的初始化和寄存器的关系,多次套娃操作把内容已经分散在各个回调函数中。因为时间关系没理全,具体内容还需要等下一次再分析。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐