3. Codec寄存器映射:寄存器地址空间、读写时序、寄存器位域定义
Codec芯片内部通过一组寄存器来控制音频路径、增益、采样率、静音等行为。理解寄存器映射是驱动开发的基础,它包含三个核心维度:地址空间(哪些寄存器可用)、读写时序(如何通过总线访问)、位域定义(每个bit控制什么功能)。
3.1 寄存器地址空间
大多数音频Codec采用7位或8位寄存器地址,地址空间通常为0x00~0x7F(128个寄存器)或0x00~0xFF(256个寄存器)。实际芯片只使用其中一部分,未使用的地址通常返回0或保留。
| 地址范围 | 功能区域 | 典型寄存器数 | 说明 |
|---|---|---|---|
| 0x00~0x0F | 芯片ID/版本/复位 | 4~8 | 只读ID、软件复位、电源状态 |
| 0x10~0x2F | 时钟与采样率控制 | 8~16 | PLL分频、MCLK/LRCK配置 |
| 0x30~0x4F | 音频路径与混音器 | 16~24 | 输入选择、输出路由、旁路开关 |
| 0x50~0x6F | 音量/增益/静音 | 16~24 | 数字音量、模拟增益、静音位 |
| 0x70~0x7F | GPIO/中断/特殊功能 | 4~8 | GPIO方向、中断状态、测试模式 |
3.2 读写时序
嵌入式Linux中Codec通常挂接在I2C或SPI总线上。寄存器读写时序由总线协议决定,但Codec自身可能要求特定的访问顺序或延迟。
3.2.1 I2C读写时序
I2C是最常见的Codec控制总线。标准I2C写操作:发送设备地址(7位)+ 写位(0),然后发送寄存器地址(1字节),再发送数据(1字节)。读操作需要先写寄存器地址,然后重新发送设备地址+读位(1),再读取数据。
/* I2C写单个寄存器 */
int codec_i2c_write(struct i2c_client *client, u8 reg, u8 val)
{
u8 buf[2] = {reg, val};
return i2c_master_send(client, buf, 2);
}
/* I2C读单个寄存器 */
int codec_i2c_read(struct i2c_client *client, u8 reg, u8 *val)
{
int ret;
ret = i2c_master_send(client, ®, 1);
if (ret < 0)
return ret;
return i2c_master_recv(client, val, 1);
}
3.2.2 SPI读写时序
SPI接口通常使用16位或24位传输帧:高位为读写标志位(R/W),中间为寄存器地址,低位为数据。SPI读写一般要求片选信号在传输期间保持低电平。
/* SPI写寄存器:16位帧格式 [R/W(1bit) + ADDR(7bit) + DATA(8bit)] */
int codec_spi_write(struct spi_device *spi, u8 reg, u8 val)
{
u16 frame = (0 << 15) | (reg << 8) | val; /* R/W=0 表示写 */
frame = cpu_to_be16(frame);
return spi_write(spi, &frame, 2);
}
/* SPI读寄存器 */
int codec_spi_read(struct spi_device *spi, u8 reg, u8 *val)
{
u16 tx_frame = (1 << 15) | (reg << 8) | 0x00; /* R/W=1 表示读 */
u16 rx_frame;
int ret;
tx_frame = cpu_to_be16(tx_frame);
ret = spi_write_then_read(spi, &tx_frame, 2, &rx_frame, 2);
if (ret == 0)
*val = rx_frame & 0xFF;
return ret;
}
udelay(10) 或使用 regmap 的 reg_update_bits 配合延迟标志。
3.3 寄存器位域定义
每个寄存器的bit位通常被划分为多个位域(bit field),分别控制不同的硬件功能。理解位域定义是配置Codec的关键。
3.3.1 典型位域结构
以音量控制寄存器为例,常见的位域布局如下:
| Bit位置 | 位域名 | 宽度 | 功能描述 | 典型值 |
|---|---|---|---|---|
| 7 | MUTE | 1 | 静音控制:0=正常,1=静音 | 0x00 |
| 6:4 | VOL_GAIN | 3 | 增益步进:000=-12dB, 001=-6dB, 010=0dB, 011=+6dB | 0x20 |
| 3:0 | VOL_ATTEN | 4 | 衰减步进:0000=0dB, 0001=-1dB, ... 1111=-15dB | 0x0A |
3.3.2 位域操作宏
在驱动中,通常使用位域宏来构造和解析寄存器值,避免硬编码魔法数。
/* 位域定义宏 */
#define MUTE_SHIFT 7
#define MUTE_MASK (1 << MUTE_SHIFT)
#define MUTE_ENABLE (1 << MUTE_SHIFT)
#define MUTE_DISABLE (0 << MUTE_SHIFT)
#define VOL_GAIN_SHIFT 4
#define VOL_GAIN_MASK (0x7 << VOL_GAIN_SHIFT)
#define VOL_GAIN_SET(g) ((g & 0x7) << VOL_GAIN_SHIFT)
#define VOL_ATTEN_SHIFT 0
#define VOL_ATTEN_MASK (0xF << VOL_ATTEN_SHIFT)
#define VOL_ATTEN_SET(a) ((a & 0xF) << VOL_ATTEN_SHIFT)
/* 构造寄存器值 */
#define VOL_REG_VAL(mute, gain, atten) \
(MUTE_DISABLE | VOL_GAIN_SET(gain) | VOL_ATTEN_SET(atten))
/* 使用示例:设置音量寄存器为 静音关闭、增益0dB、衰减-10dB */
u8 reg_val = VOL_REG_VAL(0, 0x2, 0x0A);
codec_i2c_write(client, 0x50, reg_val);
3.4 寄存器映射与位域可视化
下图展示了一个典型Codec的寄存器地址空间布局,以及单个音量寄存器的位域结构。
3.5 位域配置实战:音量控制
以WM8960 Codec的左侧耳机输出音量寄存器(地址0x50)为例,展示完整的位域配置流程。
- 读取当前寄存器值:通过I2C读取0x50,得到当前配置。
- 修改目标位域:使用位掩码清除目标位域,再写入新值。
- 写回寄存器:将修改后的值写回0x50。
- 验证:回读寄存器确认写入成功。
/* WM8960 左侧耳机输出音量寄存器 0x50 位域配置 */
#define WM8960_LOUT1_VOL 0x50
#define LOUT1_MUTE_SHIFT 7
#define LOUT1_MUTE_MASK (1 << LOUT1_MUTE_SHIFT)
#define LOUT1_VOL_SHIFT 0
#define LOUT1_VOL_MASK (0x7F << LOUT1_VOL_SHIFT)
/* 设置音量为 0dB(不静音) */
int wm8960_set_lout1_volume(struct i2c_client *client, u8 volume)
{
u8 reg_val;
int ret;
/* 读取当前值 */
ret = codec_i2c_read(client, WM8960_LOUT1_VOL, ®_val);
if (ret < 0)
return ret;
/* 清除音量位域,设置新音量,保持静音位不变 */
reg_val &= ~(LOUT1_VOL_MASK | LOUT1_MUTE_MASK);
reg_val |= (volume & 0x7F); /* 音量值 0~127 */
reg_val |= (0 << LOUT1_MUTE_SHIFT); /* 不静音 */
/* 写回寄存器 */
return codec_i2c_write(client, WM8960_LOUT1_VOL, reg_val);
}
regmap 框架提供了 regmap_update_bits() 函数自动完成此操作。
3.6 寄存器映射的驱动实现
在ASoC框架中,Codec驱动通过 struct snd_soc_codec_driver 的 read 和 write 回调函数实现寄存器访问。现代驱动更推荐使用 regmap 来管理寄存器缓存和同步。
/* 使用 regmap 实现寄存器映射 */
static const struct regmap_config wm8960_regmap = {
.reg_bits = 7, /* 7位寄存器地址 */
.val_bits = 8, /* 8位寄存器值 */
.max_register = WM8960_MAX_REGISTER,
.reg_defaults = wm8960_reg_defaults, /* 默认值表 */
.num_reg_defaults = ARRAY_SIZE(wm8960_reg_defaults),
.cache_type = REGCACHE_RBTREE, /* 使用红黑树缓存 */
.volatile_reg = wm8960_volatile_reg, /* 易失寄存器回调 */
};
/* 驱动初始化时创建 regmap */
static int wm8960_i2c_probe(struct i2c_client *i2c,
const struct i2c_device_id *id)
{
struct regmap *regmap;
regmap = devm_regmap_init_i2c(i2c, &wm8960_regmap);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
/* 后续通过 regmap 读写寄存器 */
regmap_write(regmap, 0x50, 0x2A); /* 设置音量寄存器 */
regmap_read(regmap, 0x50, &val); /* 回读验证 */
}