Audio codec FPGA 验证时I2C读写寄存器小结

本文详细介绍了如何在FPGA上实现ZX2000内置音频编解码器的I2C接口操作,包括在dts文件中添加I2C配置、编写驱动代码进行寄存器读写,以及通过应用层代码进行实时寄存器值修改。重点讨论了不同数据长度下的读写实现和调试注意事项。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Audio codec由于涉及到D/A,A/D等模拟相关的操作,所以在FPGA上无法实现其功能,但是其内部的寄存器,是可以通过I2C对其读写的。
ZX2000内置CODEC是挂在芯片的I2C2上,所以第一步需要在dts中添加I2C2,注意zx2000内置audio codec的i2c slave addr为0x8。


zx2000.dtsi:
  i2c2:
i2c@d83a0000 {
   #address-cells = <1>;
   #size-cells = <0>;
   compatible =  "zx,zx2000-i2c";
   linux,i2c-index = <2>;
   reg = < 0xd83a0000 0x10000 >;
   interrupts = <0 46 0x04>;
  };
zx2000-fpga.dts:
  i2c2:
i2c@d83a0000 {
   zx2kdac:
zx2kdac@8 {
    compatible = "zx,zx2kdac";
    reg = <0x8>;
    };
  };
  sound-i2s {
   compatible = "zhaoxin,zx2000-i2s-audio";
   zx,model = "zx2000-i2s-audio";
   zx,i2s-controller = <&i2s0>;
   zx,audio-codec = <&zx2kdac>;
  };
zx2000内置codec的i2c时序如下:

写:
‘slave_addr[6:0] + R/W’ + ‘register_addr[6:0] + 0’ + ‘data[7:0]’
读:
 ‘slave_addr[6:0] + R/W’ + ‘register_addr[6:0] + 0’ + ‘slave_addr[6:0] + R/W’+ data[7:0]
也就是,上面图的‘左 + 中 + 右’是写的流程;上面图的‘左 + 中’+ 下面图是读的流程。

具体实现到代码里,写如下:
static int zx2kdac_hw_write(void *context, unsigned int reg, unsigned int val)
{
 struct i2c_client *client = context;
 u8 data[2];
 int ret;

 data[0] = ((reg & 0x7f) << 1); // 要写入的寄存器地址左移一位, bit0写0。register_addr[6:0] + 0
 data[1] = val & 0xff; //要写入8位的数据。

 ret = i2c_master_send(client, data, 2);
 if(ret == 2)
  return 0;
 if (ret < 0)
  return ret;
 else
  return -EIO;
}
如果数据是16bit, 该怎么办?
static int zx2kdac_hw_write(void *context, unsigned int reg, unsigned int val)
{
 struct i2c_client *client = context;
 u8 data[3];
 int ret;

 data[0] = ((reg & 0x7f) << 1);
 data[1] = (0xff00 & val) >> 8; //高8位数据放在data[1]
 data[2] = val & 0xff; //低8位数据放在data[2]

 ret = i2c_master_send(client, data, 3);//传输3个data
 if (ret == 3)
  return 0;
 if (ret < 0)
  return ret;
 else
  return -EIO;
}
实际上,在drivers/i2c/busses/i2c-nomadik.c中,对于写,还提供了另外一种方法:
写两个字节(0x23, 0x46),从地址0x1:
Wr_buff[0] = 0x1; //reg_addr 地址
Wr_buff[1] = 0x23;//数据
Wr_buff[2] = 0x46; //数据
Msg[0].buf = Wr_buff;
Msg[0].flags = 0x0;
Msg[0].len = 3;
Msg[0].addr = client->addr; //slave_addr
I2c_transfer(client->adapter, msg, 1);
由于zx2000内置codec只是每次写8bit(1byte)数据所以应该写成这样:
Wr_buff[0] = 0x1; //reg_addr 地址
Wr_buff[1] = 0x23;//数据
Msg[0].buf = Wr_buff;
Msg[0].flags = 0x0;
Msg[0].len = 2;
Msg[0].addr = client->addr; //slave_addr
I2c_transfer(client->adapter, msg, 1);

读的代码:
static int zx2kdac_hw_read(void *context, unsigned int reg, unsigned int *val) //读出的值,放到val中
{
 struct i2c_client *client = context;
 struct i2c_msg xfer[2];
 u8 r[1];
 u8 data[1];
 u8 data2;
 int ret;

 /* Write register */
 r[0] = ((reg & 0x7f) << 1); // 要写入的寄存器地址左移一位, bit0写0。register_addr[6:0] + 0
 xfer[0].addr = client->addr; //i2c slave_addr
 xfer[0].flags = 0; //flags = 0 为写
 xfer[0].len = 1; //1 byte, 8 bit
 xfer[0].buf = &r[0];//register_addr要写入到i2c_msg.buf中

 /* Read data */
 xfer[1].addr = client->addr;//从这个i2c slave addr器件
 xfer[1].flags = I2C_M_RD;//读
 xfer[1].len = 1; //1个byte, 8个bit
 xfer[1].buf = &data[0]; //放到i2c_msg[1].buf中,数据可以从data[0]读出来。

 ret = i2c_transfer(client->adapter, xfer, 2);
 if (ret != 2) {
  dev_err(&client->dev, "i2c_transfer() returned %d\n", ret);
  return -EIO;
 }

 *val = data[0];

 printk("read %02x => %04x\n", reg, *val);

 return 0;
}
如果是16比特数据,该怎么读:
static int zx2kdac_hw_read(void *context, unsigned int reg, unsigned int *val)
{
 struct i2c_client *client = context;
 struct i2c_msg xfer[2];
 u8 r[1];
 u8 data[2];
 int ret;

 /* Write register */
 r[0] = ((reg & 0x7f) << 1);
 xfer[0].addr = client->addr;
 xfer[0].flags = 0;
 xfer[0].len = 1;
 xfer[0].buf = &r[0];

 /* Read data */
 xfer[1].addr = client->addr;
 xfer[1].flags = I2C_M_RD;
 xfer[1].len = 2; //读2个byte, 16bit
 xfer[1].buf = data;

 ret = i2c_transfer(client->adapter, xfer, 2); //两个 i2c_msg
 if (ret != 2) {
  dev_err(&client->dev, "i2c_transfer() returned %d\n", ret);
  return -EIO;
 }

 *val = (data[0] << 8) | data[1];//两个byte的前一个byte放在data[0], 后一个byte放在data[1]

 printk("read %02x => %04x\n", reg, *val);

 return 0;
}
在代码里,可以通过调用上面的读和写来操作codec中的寄存器,如:
zx2kdac_hw_write(i2c, 0x1, 0x70);
zx2kdac_hw_read(i2c, 0x1, &val);//val中保存返回值。
上面的读和写可以进一步被封装:
static const struct regmap_config zx2kdac_regmap = {
 .reg_bits = 8, //register_addr 8位
 .val_bits = 8, //register_val值也是8位
 .max_register = 19+1, //zx2000 codec总共20个寄存器,
 .reg_defaults = zx2kdac_reg,
 .num_reg_defaults = ARRAY_SIZE(zx2kdac_reg),
 .writeable_reg = zx2kdac_writeable_register,
// .volatile_reg = zx2kdac_volatile_register,
 .readable_reg = zx2kdac_readable_register,
 .reg_read = zx2kdac_read,//读被赋到这里
 .reg_write = zx2kdac_write,//写被赋到这里
 .cache_type = REGCACHE_RBTREE,
};
zx2kdac_regmap可以被下面两个函数接口调用:
1: zx2kdac->regmap = devm_regmap_init_i2c(i2c, &zx2kdac_regmap);
 devm_regmap_init_i2c ---〉devm_regmap_init(&i2c->dev, &regmap_i2c, &i2c->dev, config)
 regmap_i2c中会定义.write/.read操作, 和上面的write/read操作差不多,只不过里面针对的是标准的i2c的操作, 对于zx2000 7位register_addr这种情况是不适用的。
有一种方法,将7位地址转成8位地址之后,可以调用这个接口:
比如,原来是要读codec中0x1寄存器的值,用
regmap_read(zx2kdac->regmap, 0x1, &val);
 需要替换成
regmap_read(zx2kdac->regmap, 0x2, &val); // 0x1<<1 = 0x2, 7位register_addr转成8位,最后一位为0
 在regmap_read/regmap_write中会根据是否定义了regmap_i2c来决定调用哪种形式的.write/.read操作。如果定义了regmap_i2c,就用regmap_i2c中的.write/.read操作,如果没有定义regmap_i2c, 就用config中的.write/.read操作,也就是我们codec中regmap_config中的.reg_read/.reg_write操作。
2: zx2kdac->regmap = devm_regmap_init(&i2c->dev, NULL, i2c, &zx2kdac_regmap);
 由于zx2000的codec i2c时序比较特殊, 所以,没有用内核中的devm_regmap_init_i2c这个接口,而是调用里面更深一级的接口,同时将regmap_i2c设置成NULL ,这样,regmap_read/write就会调用我自己在codec中定义的.reg_read/.reg_write操作。

 

上面介绍的是在codec的驱动中,通过i2c接口来读写寄存器。
有些情况下, 为了调试的方便, 可以通过应用层代码的调用(ioctl),来实时的改变i2c外设里面寄存器的值,思路和上面是一样的,就是构造i2c msg,通过ioctl将i2c msg传递到底层。

这个文件时我在网上找到的:
http://blog.youkuaiyun.com/21cnbao/article/details/7919055
使用下面命令执行:
./i2c /dev/i2c-2 0x8 0x4 0 //读
./i2c /dev/i2c-2 0x8 0x4 1 0x7c //写
注释:
/dev/i2c-2 //设备挂在i2c总线2上,对应的节点‘/dev/i2c-2’
0x8 //该器件的i2c slave_addr 为8
0x4 //操作i2c外设内部的这个地址
0/1 //读/写
0x7c //写进去的值为0x7c
我用i2c_baohua.c测试的时候,发现是可以读寄存器值的,但是写一个新值进去之后,再读,读出来的就都是0。很奇怪,后来进一步调试发现,是由于新值被当成0写进去了,原因是没有按照格式写新值。
代码中原来是:
sscanf(argv[5], "%d", &w_val);
新值是十进制的格式,我用了16进制的格式。
所以需要改为:
sscanf(argv[5], "%x", &w_val);

注意:
1: Machine driver那边对应的codec_name应该是zx2kdac.2-0008(name.i2c_bus-i2c_salve_addr)这种形式。
2: 要首先让i2s那边正确设置audio DAC 之后,才可以通过i2c在线读写codec寄存器的值。
两种方法:
1):在I2S hw_params 操作的时候,进行audio DAC I2C相关寄存器的设置,然后在播放音乐的过程中使用上面的测试代码在线读写audio DAC 寄存器值的。
2):在I2S 做init的时候,进行audio DAC I2C相关寄存器的设置。这样,系统起来之后,不需要播放音乐也可以在线读写audio DAC 寄存器的值。
另外,在调试过程中遇到一个问题:在codec i2c的probe中可以通过i2c读写寄存器,但是在codec的probe中却不可以通过i2c读写寄存器,这个需要在调用codec probe之前,在i2s代码中设置audio DAC。可以通过上面的第2种方法解决。

<think>我们正在讨论的是`audio_codec_new_i2c_ctrl`函数的使用方法。根据之前的上下文,这个函数用于创建一个I2C控制接口实例,该实例符合`audio_codec_ctrl_if_t`结构体定义的接口。这样,编解码器(Codec)就可以通过这个接口与I2C总线进行通信。 ### 函数原型 通常,该函数的声明可能如下(具体名称可能因平台而异,但功能类似): ```c const audio_codec_ctrl_if_t *audio_codec_new_i2c_ctrl(const audio_codec_i2c_cfg_t *cfg); ``` ### 参数说明 - `cfg`: 指向I2C配置结构体的指针,该结构体包含I2C控制接口所需的配置信息,例如: - `i2c_port`: 使用的I2C端口号(如I2C_NUM_0, I2C_NUM_1等) - `i2c_addr`: 编解码器设备的I2C地址(7位地址) - 其他可能的配置,如钟频率、超设置等 ### 返回值 - 成功:返回一个指向`audio_codec_ctrl_if_t`结构体的指针(实际上是实现了该接口的I2C控制对象) - 失败:返回NULL ### 使用步骤 #### 1. 准备I2C配置 首先,我们需要配置I2C参数。假设我们有一个配置结构体`audio_codec_i2c_cfg_t`,我们可以这样初始化: ```c audio_codec_i2c_cfg_t i2c_cfg = { .i2c_port = I2C_NUM_1, // 使用I2C1控制器 .i2c_addr = 0x18, // 编解码器设备地址(根据具体设备) .clk_speed = 100000, // I2C钟频率(100kHz) .timeout_ms = 1000 // 超间(毫秒) }; ``` #### 2. 创建I2C控制接口实例 使用上述配置创建I2C控制接口: ```c const audio_codec_ctrl_if_t *i2c_ctrl = audio_codec_new_i2c_ctrl(&i2c_cfg); if (i2c_ctrl == NULL) { // 错误处理:创建失败 } ``` #### 3. 将控制接口绑定到编解码器 在初始化具体的编解码器(如ES8311),将此控制接口作为参数传入: ```c es8311_codec_cfg_t es_cfg = { .ctrl_if = i2c_ctrl, // 使用上面创建的I2C控制接口 .pa_pin = GPIO_NUM_25, // 功放控制引脚(可选) .mic_bias = MIC_BIAS_2V2 // 麦克风偏置电压(可选) }; audio_codec_handle_t codec = es8311_codec_init(&es_cfg); if (codec == NULL) { // 错误处理:编解码器初始化失败 } ``` #### 4. 使用编解码器 之后,就可以通过编解码器提供的API进行音频操作,例如设置音量、开始录音、播放等。这些操作内部会通过我们设置的I2C控制接口与硬件通信。 ### 完整示例代码 ```c #include "audio_codec.h" #include "es8311_codec.h" void setup_audio_system() { // 步骤1: 配置I2C audio_codec_i2c_cfg_t i2c_cfg = { .i2c_port = I2C_NUM_1, .i2c_addr = 0x18, .clk_speed = 100000, .timeout_ms = 1000 }; // 步骤2: 创建I2C控制接口 const audio_codec_ctrl_if_t *i2c_ctrl = audio_codec_new_i2c_ctrl(&i2c_cfg); if (i2c_ctrl == NULL) { printf("创建I2C控制接口失败\n"); return; } // 步骤3: 初始化ES8311编解码器 es8311_codec_cfg_t es_cfg = { .ctrl_if = i2c_ctrl, .pa_pin = GPIO_NUM_25, .mic_bias = MIC_BIAS_2V2 }; audio_codec_handle_t codec = es8311_codec_init(&es_cfg); if (codec == NULL) { printf("初始化ES8311编解码器失败\n"); return; } // 步骤4: 使用编解码器 // 例如:设置音量 audio_codec_set_volume(codec, 60); // 设置音量为60% // 更多操作... } ``` ### 注意事项 1. **I2C总线初始化**:在调用`audio_codec_new_i2c_ctrl`之前,确保已经初始化了对应的I2C总线(例如,通过`i2c_driver_install`等函数)。 2. **设备地址**:确保提供的I2C地址与硬件连接一致(注意:有些设备使用7位地址,有些使用8位,这里通常使用7位地址)。 3. **错误处理**:创建控制接口或编解码器失败,应进行适当的错误处理(如打印日志、返回错误码等)。 4. **多设备支持**:如果系统中有多个编解码器,每个编解码器都需要自己的控制接口实例(即使它们共享同一个I2C端口,但地址不同,所以需要分别创建)。 ### 引用说明 在用户提供的引用[1]中,出现了一个错误:“使能i2c0后,挂载在i2c1上的codec报错”。这提示我们在使用多个I2C端口,需要确保每个控制接口正确绑定到对应的I2C端口。在配置`audio_codec_i2c_cfg_t`,务必设置正确的`i2c_port`(如`I2C_NUM_1`)[^1]。 [^1]: 使能i2c0后,挂载在i2c1上的codec报错 ### 相关问题 1. 如何解决多I2C端口共存的冲突问题? 2. 如何调试I2C控制接口的通信故障? 3. 如何为不同的编解码器芯片(如ES8311、WM8960)配置I2C控制接口?</think>### `audio_codec_new_i2c_ctrl` 函数详解与使用教程 `audio_codec_new_i2c_ctrl` 是音频驱动中用于**创建 I2C 总线控制接口**的核心函数,它实现了 `audio_codec_ctrl_if_t` 接口的 I2C 版本。以下是详细使用方法和示例: --- #### 一、函数原型 ```c const audio_codec_ctrl_if_t *audio_codec_new_i2c_ctrl(const audio_codec_i2c_cfg_t *config); ``` #### 二、参数说明 | 参数 | 类型 | 说明 | |------|------|------| | `config` | `audio_codec_i2c_cfg_t*` | I2C 总线配置结构体指针 | **`audio_codec_i2c_cfg_t` 结构体定义**: ```c typedef struct { i2c_port_t i2c_port; // I2C 端口号 (如 I2C_NUM_0, I2C_NUM_1) uint8_t i2c_addr; // 编解码器设备地址 (7位地址) int sda_io_num; // SDA 引脚号 int scl_io_num; // SCL 引脚号 uint32_t clk_speed; // I2C 钟频率 (Hz) } audio_codec_i2c_cfg_t; ``` --- #### 三、使用步骤 ##### 1. 初始化 I2C 总线 ```c #include "driver/i2c.h" void init_i2c_bus(i2c_port_t port) { i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = GPIO_NUM_21, // 根据实际接线修改 .scl_io_num = GPIO_NUM_22, // 根据实际接线修改 .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = 100000, // 100kHz }; i2c_param_config(port, &conf); i2c_driver_install(port, conf.mode, 0, 0, 0); } ``` ##### 2. 创建 I2C 控制接口 ```c audio_codec_i2c_cfg_t i2c_cfg = { .i2c_port = I2C_NUM_1, // 使用 I2C1 总线 .i2c_addr = 0x18, // 编解码器设备地址 (查芯片手册) .sda_io_num = GPIO_NUM_21, // 与初始化一致 .scl_io_num = GPIO_NUM_22, // 与初始化一致 .clk_speed = 100000 // 100kHz }; const audio_codec_ctrl_if_t *i2c_ctrl = audio_codec_new_i2c_ctrl(&i2c_cfg); if (i2c_ctrl == NULL) { ESP_LOGE("AUDIO_CODEC", "I2C控制接口创建失败!"); return; } ``` ##### 3. 绑定到编解码器 ```c es8311_codec_cfg_t es8311_cfg = { .ctrl_if = i2c_ctrl, // 注入I2C控制接口 .pa_pin = GPIO_NUM_25, // 功放控制引脚 .mic_bias = MIC_BIAS_2V2 // 麦克风偏置电压 }; audio_codec_handle_t codec = es8311_codec_init(&es8311_cfg); ``` ##### 4. 使用控制接口操作编解码器 ```c // 写寄存器示例 i2c_ctrl->write_reg(i2c_ctrl, 0x0D, 0x80); // 启动DAC // 读寄存器示例 uint16_t reg_val; i2c_ctrl->read_reg(i2c_ctrl, 0x0F, &reg_val); // 读取状态寄存器 ``` --- #### 四、关键注意事项 1. **I2C 端口冲突** 确保编解码器连接的 I2C 端口与配置一致: ```c // 错误示例:编解码器挂在I2C1,但配置使用I2C0 audio_codec_i2c_cfg_t i2c_cfg = { .i2c_port = I2C_NUM_0, // 错误配置 ... }; ``` 将导致报错:`ASoC: CODEC DAI ... not registered`[^1] 2. **设备地址确认** 编解码器地址需查芯片手册确定(如ES8311常用0x18或0x1A) 3. **多设备支持** 同一I2C总线可挂载多个设备: ```c // 设备1: ES8311 (地址0x18) audio_codec_i2c_cfg_t cfg1 = {.i2c_port=I2C_NUM_1, .i2c_addr=0x18}; const audio_codec_ctrl_if_t *ctrl1 = audio_codec_new_i2c_ctrl(&cfg1); // 设备2: WM8960 (地址0x1A) audio_codec_i2c_cfg_t cfg2 = {.i2c_port=I2C_NUM_1, .i2c_addr=0x1A}; const audio_codec_ctrl_if_t *ctrl2 = audio_codec_new_i2c_ctrl(&cfg2); ``` --- #### 五、完整工作流程 ```mermaid graph TD A[初始化I2C总线] --> B[创建I2C控制接口] B --> C[绑定到编解码器] C --> D[配置音频参数] D --> E[启动音频流] E --> F[通过控制接口读写寄存器] ``` > 通过此设计,编解码器操作与物理总线解耦,使同一驱动可适配不同硬件平台[^1]。 ### 相关问题 1. 如何解决 I2C 总线地址冲突问题? 2. 当 I2C 控制接口初始化失败,如何进行诊断? 3. 如何扩展支持 SPI 控制接口的编解码器? 4. 在多编解码器系统中如何优化 I2C 总线利用率? [^1]: 使能i2c0后,挂载在i2c1上的codec报错
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值