文章目录
1 前言
客户问codec的DAC/ADC的采样率是否是动态可调的,正好学习一下时钟及采样率的关系.
2 硬件连接图
项目为sda660+dmic,使用的是sda660内部的codec。
具体的电路图就不放了,参见高通的连接框图,如图连接到digital codec上。
dmic内部大致框架,由PDM modulator + ADC + Amp + MEMS transducer组成。
- PDM modulator是PDM调制器,用来产生1bit的数据
- ADC是模数转换,模拟信号转成数字信号
- Amp是功率放大器
- MEMS传感器输出模拟信号
dmic有两根通信线路,分别是dmic_clk和dmic_data,即PDM通信协议。
其中需要关注的几点:
- DMIC_CLK是如何定义的
- DMIC中的ADC采样速率如何,是否等于DMIC_CLK
- 内部codec中的I2S的clk如何定义的
- tinycap的sample_rate,channel,width这三个参数最终传给什么
3 通信协议
参见几个参考文档
audio相关的通信协议主要的四种: I2S, PDM, PCM,Slimbus
3.1 PDM
信号线:一根时钟线,一根数据线
如DMIC和internal codec的连接线。
DMIC_CLK 时钟线
DMIC_DATA 数据线
参考文章:PDM接口介绍
data输出只有1bit,输出0/1,具体如何处理的有空再细看。
3.2 I2S
参考文档:I2S的理解
至少三个信号
SCK,串行时钟(即BCLK位时钟),一位数据对应一个SCK脉冲
WS,声道选择,选择左右声道
SDATA,串行数据,1位
注意:
SCK = 采样率sample_rate * 位深 width* 声道数channel
这三个参数tinycap可以设置。
sda660中默认channel数为2
3.3 PCM
参见文章:PCM / I2S / AC97/PDM
ALSA 架构
3.4 slimbus
PDM连接PMIC
soundwire连接双wsa
slimbus连接WCD
4 编码方式
参考文章
https://blog.csdn.net/xjw1874/article/details/81536127
https://tieba.baidu.com/p/4400689097
4.1 PCM
图片来源:揭开DSD的神秘面纱
4.2 DSD
图片来源:揭开DSD的神秘面纱
5 DMIC_rate设置
80-P7747-5C
如图sda660的internal codec支持4个dmic,其中当MCLK为9.6Mhz时支持4种。
MCLK是晶振频率,询问硬件确认的。
DMIC的MCLK数值有两个地方可以设定,其中dts文件中定义的最终还是会调用到code的设置。
差异在于,内部codec为未在dts文件中找到直接设置的属性,目前只看到了外部codec(如wcd934x的)。
5.1 dts设置(外部codec)
外部codec
qcom,cdc-mclk-clk-rate
qcom,cdc-dmic-sample-rate
qcom,cdc-mad-dmic-rate
如图,其中设置外部codec的dts node定义,这里有属性直接设置。
5.2 codec驱动定义(内部codec)
XXX_enable_dmic()
@android\vendor\qcom\opensource\audio-kernel\asoc\codecs\xxx_cdc.c
msm_dig_cdc_codec_enable_dmic()
重点关注上电设置。
将0X04赋值给dmic_clk_reg,只设置前三位。前三位代表的含义如下:
Regarding the register MSM89XX_CDC_CORE_CLK_DMIC_Bx_CTL:
Bit 3:1 is defined as:
0x0: DIV2
0x1: DIV3
0x2: DIV4
0x3: DIV6
0x4: DIV16
0x0E == 1110,计算前3位
0x04 == 0100,所以是0x2,即DIV4
所以除数为4,即MCLK/4=9.6/4=2.4Mhz
所以内部codec连接的dmic_clk为2.4Mhz。
存疑:dmic_clk时钟是否为ADC的采样时钟,即是否(1/dmic_clk)s采一次数值
5.3 codec_dai
codec的dai接口定义,以sda660的internal codec为例。
注意code中定义的dai driver和实际的框图一一对应。查看codec即可知道该i2s的rate/channel/format有哪些可选。
注意:
BCLK=sample_rate * width * channel
6 tinycap命令的rate,width设置
code路径
external\tinyalsa\tinycap.c
6.1 初始化
6.2 tinycap命令
tinycap /data/test_dmic3.wav -c 2 -r 48000 -T 10
其中/data/test_dmic3.wav为存放路径,-c为channel数,-r为采样率,-T为采样时间
这里未设置width,就使用默认的数值,即为16.
代码中先定义了wav_head,后续接收到命令传下的参数会再更新录音设置参数。
6.3 获取命令参数更新数值
传下来的width选择了format,对应关系如上。
6.4 录音
先预留音频文件头信息,用来存放采样率等信息。
然后调用capture_sample进行录音。
录音结束后会打印当前录音的信息,多少frames。
unsigned int capture_sample(FILE *file, unsigned int card, unsigned int device,
unsigned int channels, unsigned int rate,
enum pcm_format format, unsigned int period_size,
unsigned int period_count, unsigned int cap_time)
{
struct pcm_config config;
struct pcm *pcm;
char *buffer;
unsigned int size;
unsigned int bytes_read = 0;
unsigned int frames = 0;
struct timespec end;
struct timespec now;
memset(&config, 0, sizeof(config));
config.channels = channels;
config.rate = rate;
config.period_size = period_size;
config.period_count = period_count;
config.format = format;
config.start_threshold = 0;
config.stop_threshold = 0;
config.silence_threshold = 0;
//打开pcm设备
pcm = pcm_open(card, device, PCM_IN, &config);
if (!pcm || !pcm_is_ready(pcm)) {
fprintf(stderr, "Unable to open PCM device (%s)\n",
pcm_get_error(pcm));
return 0;
}
//确定一帧的大小并分配
size = pcm_frames_to_bytes(pcm, pcm_get_buffer_size(pcm));
buffer = malloc(size);
if (!buffer) {
fprintf(stderr, "Unable to allocate %u bytes\n", size);
free(buffer);
pcm_close(pcm);
return 0;
}
printf("Capturing sample: %u ch, %u hz, %u bit\n", channels, rate,
pcm_format_to_bits(format));
clock_gettime(CLOCK_MONOTONIC, &now);
end.tv_sec = now.tv_sec + cap_time;
end.tv_nsec = now.tv_nsec;
//while循环读取,最终执行是fwrite函数,将数据存到file
while (capturing && !pcm_read(pcm, buffer, size)) {
if (fwrite(buffer, 1, size, file) != size) {
fprintf(stderr,"Error capturing sample\n");
break;
}
bytes_read += size;
if (cap_time) {
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > end.tv_sec ||
(now.tv_sec == end.tv_sec && now.tv_nsec >= end.tv_nsec))
break;
}
}
frames = pcm_bytes_to_frames(pcm, bytes_read);
free(buffer);
pcm_close(pcm);
return frames;
}
所以最终采样率,位深,声道数会存放到config中,然后作为pcm_open的入参。
struct pcm *pcm_open(unsigned int card, unsigned int device,
unsigned int flags, struct pcm_config *config)
{
//将config存到pcm->config中
pcm->config = *config;
//获取pcm设备名称
snprintf(fn, sizeof(fn), "/dev/snd/pcmC%uD%u%c", card, device,
flags & PCM_IN ? 'c' : 'p');
pcm->flags = flags;
//打开pcm设备,这个就调用到pcm操作了。
pcm->fd = open(fn, O_RDWR|O_NONBLOCK);
if (pcm->fd < 0) {
oops(pcm, errno, "cannot open device '%s'", fn);
return pcm;
}
//设置位深,采样率以及声道数到硬件参数
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_SAMPLE_BITS,
pcm_format_to_bits(config->format));
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_FRAME_BITS,
pcm_format_to_bits(config->format) * config->channels);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_CHANNELS,
config->channels);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_PERIODS, config->period_count);
param_set_int(¶ms, SNDRV_PCM_HW_PARAM_RATE, config->rate);
}
dsp的驱动lsm中定义pcm硬件信息
//msm-lsm-client.c
static struct snd_pcm_hardware msm_pcm_hardware_capture = {
.info = (SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_PAUSE | SNDRV_PCM_INFO_RESUME),
.formats = (SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S24_LE),
.rates = (SNDRV_PCM_RATE_16000 |
SNDRV_PCM_RATE_48000),
.rate_min = 16000,
.rate_max = 48000,
.channels_min = LSM_INPUT_NUM_CHANNELS_MIN,
.channels_max = LSM_INPUT_NUM_CHANNELS_MAX,
.buffer_bytes_max = CAPTURE_MAX_NUM_PERIODS *
CAPTURE_MAX_PERIOD_SIZE,
.period_bytes_min = CAPTURE_MIN_PERIOD_SIZE,
.period_bytes_max = CAPTURE_MAX_PERIOD_SIZE,
.periods_min = CAPTURE_MIN_NUM_PERIODS,
.periods_max = CAPTURE_MAX_NUM_PERIODS,
.fifo_size = 0,
};
具体的硬件链路相关推荐文章:Linux ALSA 音频系统:物理链路篇
Linux ALSA音频系统:platform,machine,codec
tinycap命令的参数最终会设置到i2s,至于i2s则关系到dai相关的驱动,后续有空再看。
7 小结
本文重点是了解dmic_clk和内部codec的i2s时钟。
-
dmic_clk针对外部codec可以直接在dts文件中有属性可以查看及设置,内部codec则需要查看代码,找到DMIC_MCLK的时钟除率。
在XXX_enable_dmic()函数中可以找到上电设置,配置的clk寄存器的值则对应DIV_X。
用晶振频率除以该X则得到DMIC_CLK。 -
i2s的时钟计算公式:BCLK=sample_rate * width * channel
其中三大参数是tinycap命令设置的,当然在dai相关代码中的也可以找到设置范围。