1. 项目概述
最近在匠芯创D213开发板(Luban SDK,Linux 5.10内核)上成功移植了CL1026音频Codec驱动,实现了模拟麦克风(AMIC)和数字麦克风(DMIC)的录音功能,以及音频播放功能。CL1026是一款集成了I2S数字音频接口和I2C控制接口的音频编解码芯片,支持立体声ADC/DAC、数字麦克风输入、耳机输出等功能。
这个项目的主要挑战在于:
- 需要理解Linux ALSA音频子系统的架构
- 正确配置内核和设备树
- 编写符合ASoC规范的Codec驱动
- 调试硬件连接和时钟同步问题
2. 硬件设计与连接
2.1 CL1026芯片特性
CL1026是一款低功耗、高性能的音频编解码器,主要特性包括:
- 24位立体声ADC和DAC
- 支持8kHz到192kHz的采样率
- 集成数字麦克风接口
- 耳机驱动能力:40mW/32Ω
- 信噪比(SNR):100dB(DAC), 95dB(ADC)
- 工作电压:3.3V(模拟/数字)
2.2 硬件连接方案
CL1026与D213开发板的连接关系如下表所示:
| CL1026引脚 | D213引脚 | 功能说明 |
|---|---|---|
| SCL | I2C0_SCL | I2C时钟线 |
| SDA | I2C0_SDA | I2C数据线 |
| MCLK | I2S1_MCLK | 主时钟输入 |
| BCLK | I2S1_BCLK | 位时钟 |
| LRCK | I2S1_LRCK | 帧时钟 |
| DOUT | I2S1_DIN | 录音数据输入 |
| DIN | I2S1_DOUT | 播放数据输出 |
| VDD | 3.3V | 电源 |
| GND | GND | 地 |
注意:I2C地址设置为0x42(7位地址),工作模式为I2S主模式(由D213提供时钟),MCLK配置为12.288MHz以支持常用采样率。
2.3 时钟设计考虑
音频系统的时钟设计至关重要,CL1026需要以下时钟信号:
- MCLK(主时钟):12.288MHz
- BCLK(位时钟):由采样率和数据格式决定
- LRCK(帧时钟):等于采样率
对于48kHz采样率、32位数据(左右各16位)的情况:
- LRCK = 48kHz
- BCLK = 48kHz × 32 × 2 = 3.072MHz
3. 软件架构与内核配置
3.1 ALSA软件架构
Linux音频子系统采用分层架构:
code复制用户空间应用 (arecord/aplay/amixer)
↑
ALSA核心层 (sound/core)
↑
ASoC中间层 (sound/soc)
↑
平台驱动层 (I2S控制器驱动 + Codec驱动)
3.2 内核配置修改
在Luban SDK的Linux内核配置中需要启用以下选项:
bash复制CONFIG_SOUND=y
CONFIG_SND=y
CONFIG_SND_SOC=y
CONFIG_SND_SOC_AIC_I2S=y # 匠芯创I2S控制器驱动
CONFIG_SND_SIMPLE_CARD=y # 简单声卡驱动
这些配置可以通过以下方式添加:
- 使用
make kernel-menuconfig交互式配置 - 直接修改
linux-5.10/arch/riscv/configs/d211_demo128_nand_defconfig文件
提示:CONFIG_SND_SOC_AIC_CODEC是D213内置的Codec驱动,与CL1026驱动可以共存。
4. 设备树配置详解
4.1 I2C节点配置
在设备树中添加CL1026的I2C设备节点:
dts复制&i2c0 {
status = "okay";
cl1026: cl1026@42 {
#sound-dai-cells = <0>;
compatible = "cl,cl1026";
reg = <0x42>;
clocks = <&cmu CLK_I2S1>;
clock-names = "mclk";
};
};
关键参数说明:
reg = <0x42>:指定I2C设备地址clocks:指定MCLK时钟源#sound-dai-cells = <0>:表示这个节点不包含子DAI
4.2 I2S控制器配置
启用I2S1控制器并配置引脚复用:
dts复制&i2s1 {
pinctrl-names = "default";
pinctrl-0 = <&i2s1_clk_pins>, <&i2s1_mclk_pins>,
<&i2s1_din_pins_b>, <&i2s1_dout_pins>;
status = "okay";
};
4.3 声卡节点配置
使用simple-audio-card连接I2S控制器和CL1026:
dts复制cl1026_simple_sound: cl1026-simple-sound {
status = "okay";
compatible = "simple-audio-card";
simple-audio-card,format = "i2s";
simple-audio-card,name = "cl1026";
simple-audio-card,cpu {
sound-dai = <&i2s1>;
frame-master;
bitclock-master;
system-clock-frequency = <12288000>;
};
simple-audio-card,codec {
sound-dai = <&cl1026>;
system-clock-frequency = <12288000>;
};
};
配置说明:
frame-master和bitclock-master:表示I2S控制器作为主设备system-clock-frequency:设置MCLK为12.288MHzformat = "i2s":使用标准I2S格式
5. 驱动开发关键实现
5.1 寄存器映射与初始化
使用regmap框架管理I2C寄存器访问:
c复制static const struct reg_default cl1026_reg_defaults[] = {
{CL1026_PWRCTRL1, 0xF1},
{CL1026_PWRCTRL2, 0xDF},
// 更多寄存器默认值...
};
static struct regmap_config cl1026_regmap = {
.reg_bits = 8,
.val_bits = 8,
.max_register = CL1026_MAX_REGISTER,
.reg_defaults = cl1026_reg_defaults,
.num_reg_defaults = ARRAY_SIZE(cl1026_reg_defaults),
.volatile_reg = cl1026_volatile_reg,
.readable_reg = cl1026_readable_reg,
.cache_type = REGCACHE_RBTREE,
};
5.2 DAPM路由与控件
定义音频路径和ALSA控件:
c复制static const struct snd_kcontrol_new cl1026_snd_controls[] = {
// 耳机模拟音量控制
SOC_DOUBLE_R_S_TLV("HP Analog Playback Volume",
CL1026_HPLVOL, CL1026_HPRVOL,
0, 0x3F, 1, cl1026_hp_vol_tlv),
// 输入PGA增益控制
SOC_DOUBLE_R_S_TLV("Input PGA Analog Volume",
CL1026_MIC1PGA, CL1026_MIC2PGA,
0, 0x1F, 0, cl1026_inpga_vol_tlv),
// 播放路径选择
SOC_ENUM("Playback Path", cl1026_playback_enum),
// 录音源选择
SOC_ENUM("Capture Source", cl1026_capture_enum),
};
5.3 PCM参数设置
配置采样率和时钟:
c复制static int cl1026_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct snd_soc_component *component = dai->component;
int coeff, rate;
rate = params_rate(params);
coeff = get_mclk_coeff(rate); // 根据采样率获取分频系数
// 配置MCLK分频
snd_soc_component_update_bits(component, CL1026_ASPMMCC,
0x0F, coeff);
return 0;
}
5.4 驱动注册
使用标准ASoC Codec驱动框架:
c复制static struct snd_soc_component_driver cl1026_component = {
.controls = cl1026_snd_controls,
.num_controls = ARRAY_SIZE(cl1026_snd_controls),
.dapm_widgets = cl1026_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(cl1026_dapm_widgets),
.dapm_routes = cl1026_dapm_routes,
.num_dapm_routes = ARRAY_SIZE(cl1026_dapm_routes),
};
static int cl1026_probe(struct i2c_client *i2c)
{
struct cl1026_priv *cl1026;
cl1026 = devm_kzalloc(&i2c->dev, sizeof(*cl1026), GFP_KERNEL);
// 初始化regmap
cl1026->regmap = devm_regmap_init_i2c(i2c, &cl1026_regmap);
// 注册Codec组件
return devm_snd_soc_register_component(&i2c->dev,
&cl1026_component,
&cl1026_dai, 1);
}
6. 测试与验证
6.1 基本功能测试
- 检查声卡是否注册成功:
bash复制cat /proc/asound/cards
预期输出:
code复制1 [cl1026 ]: cl1026 - cl1026
- 验证I2C通信:
bash复制i2cdetect -y 0
正常应看到地址0x42有响应。
6.2 录音测试
- 设置录音参数:
bash复制amixer -c 1 sset 'Capture MIC Opt' 'OPEN MIC A'
amixer -c 1 sset 'Input PGA Analog Volume' 20
- 开始录音:
bash复制arecord -D hw:1,0 -f S16_LE -r 48000 -c 2 -d 5 test.wav
注意:CL1026的AMIC和DMIC使用同一通道,不能同时使用。
6.3 播放测试
播放录音文件:
bash复制aplay -D hw:1,0 test.wav
6.4 常见问题排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 录音无声 | 麦克风偏置电压未开启 | 检查CL1026的MICBIAS寄存器配置 |
| 播放杂音 | 时钟不同步 | 测量MCLK/BCLK/LRCK波形 |
| I2C通信失败 | 上拉电阻缺失 | 检查I2C总线上拉电阻(通常4.7kΩ) |
| 设备未识别 | 驱动未加载 | 检查dmesg日志中的probe信息 |
7. 性能优化建议
-
低功耗优化:
- 在空闲时关闭未使用的模块电源
- 动态调整采样率以降低功耗
- 实现
set_bias_level回调管理电源状态
-
音质优化:
- 调整DAC/ADC的过采样率
- 优化模拟部分的供电和滤波电路
- 使用高质量的时钟源
-
延迟优化:
- 减小ALSA缓冲区大小
- 使用更高的中断频率
- 考虑使用低延迟音频框架(PulseAudio的实时模式)
8. 扩展功能实现
8.1 多路输入选择
CL1026支持多种输入源,可以通过寄存器配置实现动态切换:
c复制static int cl1026_set_input_source(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
int val = ucontrol->value.integer.value[0];
switch(val) {
case 0: // 模拟麦克风
snd_soc_component_update_bits(component, CL1026_INPSEL, 0x03, 0x00);
break;
case 1: // 数字麦克风
snd_soc_component_update_bits(component, CL1026_INPSEL, 0x03, 0x03);
break;
case 2: // 线路输入
snd_soc_component_update_bits(component, CL1026_INPSEL, 0x03, 0x01);
break;
}
return 0;
}
8.2 自动增益控制
可以实现简单的AGC功能:
c复制static void cl1026_agc_work(struct work_struct *work)
{
struct cl1026_priv *cl1026 = container_of(work, struct cl1026_priv, agc_work.work);
unsigned int adc_value;
int new_gain;
// 读取ADC值
regmap_read(cl1026->regmap, CL1026_ADCVAL, &adc_value);
// 根据ADC值调整增益
if (adc_value > 0x7F0000) {
new_gain = cl1026->current_gain - 3;
} else if (adc_value < 0x100000) {
new_gain = cl1026->current_gain + 3;
}
// 应用新增益
if (new_gain != cl1026->current_gain) {
regmap_update_bits(cl1026->regmap, CL1026_PGAGAIN, 0x1F, new_gain);
cl1026->current_gain = new_gain;
}
// 重新调度
schedule_delayed_work(&cl1026->agc_work, msecs_to_jiffies(100));
}
9. 开发经验分享
-
时钟同步问题:
在实际调试中发现,当MCLK频率不是采样率的整数倍时,会出现音频断续的问题。解决方案是确保MCLK=256×Fs或512×Fs。 -
寄存器初始化顺序:
CL1026对某些寄存器的写入顺序有严格要求,特别是电源管理相关寄存器。正确的初始化顺序应该是:- 先配置时钟相关寄存器
- 然后配置模拟电路偏置
- 最后开启各模块电源
-
PCB布局建议:
- I2S信号线应尽可能短,并保持等长
- 模拟和数字地要分开布局,单点连接
- 电源引脚需要足够的去耦电容(建议0.1μF+10μF组合)
-
调试技巧:
- 使用示波器检查MCLK/BCLK/LRCK波形
- 通过
regmap的调试功能查看寄存器读写 - 使用
tinymix工具快速调整混音器设置
10. 后续改进方向
- 支持更多音频格式(如DSP模式、左对齐格式)
- 实现硬件参数(采样率、位宽)的动态切换
- 添加唤醒词检测等低功耗语音功能
- 集成到PulseAudio或PipeWire音频服务器
- 支持Android音频HAL层
这个项目成功地在匠芯创D213平台上实现了高质量的音频输入输出功能,为后续的语音交互、音频处理等应用打下了坚实基础。在实际开发过程中,深入理解了Linux ALSA子系统的运作机制,积累了丰富的音频驱动调试经验。