第4章 I2S与PCM接口驱动:I2S控制器驱动开发、PCM时隙配置、三星Exynos I2S IP寄存器详解、DMA传输配置
各位同学,欢迎来到第四章。这一章我们聊聊音频驱动里最核心的接口——I2S和PCM。
说实话,我刚开始接触Exynos平台时,最头疼的就是I2S控制器。寄存器多,时序要求严,稍不注意就出怪声。后来做多了才发现,其实套路就那么几个。今天我把这些经验掰开揉碎了讲给你听。
4.1 I2S控制器驱动开发——从硬件到软件
I2S,全称是Inter-IC Sound。说白了,它就是数字音频设备之间传音频数据的串行总线。标准I2S有三根线:
- SCK(位时钟)—— 决定每个bit的传输速率
- WS(声道选择)—— 区分左右声道
- SD(数据线)—— 传输音频样本数据
嗯,这里要注意,有些芯片会把SD拆成SDI和SDO,分别用于收和发。Exynos就是这样做的。
驱动开发的第一步,是搞清楚你的I2S控制器在芯片里的位置。我习惯先看数据手册里的memory map,找到I2S基地址。比如Exynos4412的I2S0基地址是0x03830000。
然后就是设备树。在三星平台上,I2S节点通常长这样:
i2s0: i2s@03830000 {
compatible = "samsung,exynos-i2s";
reg = <0x03830000 0x100>;
interrupts = <0 84 0>;
dmas = <pdma0 12>, <pdma0 11>;
dma-names = "tx", "rx";
clocks = <&clock_audss 0>;
clock-names = "iis", "i2s_opclk0", "i2s_opclk1";
pinctrl-names = "default";
pinctrl-0 = <&i2s0_bus>;
#sound-dai-cells = <0>;
status = "disabled";
};
这里dmas指定了发送和接收使用的DMA通道。我在项目中遇到过有人把tx和rx搞反,结果录音没声音,播放也没声音,折腾了两天。
4.2 PCM时隙配置——多路音频的调度艺术
PCM接口和I2S很像,但有个关键区别:PCM支持时分复用(TDM)。你想想看,一根数据线上可以同时传4路、8路甚至16路音频,靠的就是时隙(Time Slot)。
每个时隙对应一个音频通道。比如你要做4通道的音频采集,就需要配置4个时隙。Exynos的PCM控制器支持最多16个时隙。
时隙配置的核心参数有三个:
- 时隙长度(Slot Width)—— 每个时隙占几个bit,通常是16或32
- 时隙数量(Slot Count)—— 一帧里有多少个时隙
- 时隙偏移(Slot Offset)—— 每个时隙在帧里的起始位置
我曾经在做一个8通道麦克风阵列项目时,发现第5通道的声音总是串到第6通道。查了半天,原来是时隙偏移配置错了1个bit。这种问题用逻辑分析仪一看就明白,但光看代码很难发现。
配置时隙的代码片段:
/* 配置PCM时隙 */
void exynos_pcm_set_slots(struct snd_dai *dai, unsigned int slots)
{
struct exynos_pcm_info *pcm = snd_dai_get_drvdata(dai);
u32 reg;
/* 设置时隙数量 */
reg = readl(pcm->regs + PCM_CTRL);
reg &= ~PCM_CTRL_SLOT_MASK;
reg |= (slots - 1) << PCM_CTRL_SLOT_SHIFT;
writel(reg, pcm->regs + PCM_CTRL);
/* 设置每个时隙为16bit */
reg = readl(pcm->regs + PCM_SLOT_CFG);
reg |= PCM_SLOT_16BIT;
writel(reg, pcm->regs + PCM_SLOT_CFG);
}
4.3 三星Exynos I2S IP寄存器详解
Exynos的I2S IP,内部叫I2S控制器(I2S Controller),版本从v1到v5都有。我主要用过v3和v5,这里以v5为例。
核心寄存器就那么几个,我列个表给你看:
| 寄存器名称 | 偏移地址 | 功能描述 |
|---|---|---|
| I2SCON | 0x00 | 控制寄存器,使能I2S、选择主从模式 |
| I2SMOD | 0x04 | 模式寄存器,配置数据位宽、声道数 |
| I2SFIC | 0x08 | FIFO控制寄存器,设置FIFO阈值 |
| I2SPSR | 0x0C | 预分频寄存器,配置位时钟频率 |
| I2STXD | 0x10 | 发送数据寄存器,写入音频样本 |
| I2SRXD | 0x14 | 接收数据寄存器,读取音频样本 |
我最常用的是I2SCON寄存器。它的bit0是I2S使能位,bit1是发送使能,bit2是接收使能。配置顺序有讲究:先配模式,再配时钟,最后使能。顺序反了,控制器可能罢工。
举个例子,配置I2S为发送模式:
void exynos_i2s_tx_enable(struct exynos_i2s *i2s)
{
u32 con = readl(i2s->regs + I2SCON);
/* 先确保I2S处于复位状态 */
con &= ~I2SCON_I2S_EN;
writel(con, i2s->regs + I2SCON);
/* 配置为发送模式 */
con |= I2SCON_TX_EN;
con |= I2SCON_I2S_EN;
writel(con, i2s->regs + I2SCON);
}
这里有个坑:使能I2S后,要等至少3个位时钟周期,FIFO才能正常工作。我刚开始不知道,直接往FIFO里写数据,结果前几个样本全丢了。
4.4 DMA传输配置——让数据飞起来
I2S控制器本身不带数据搬运能力。你得靠DMA把内存里的音频数据搬到I2S的FIFO里,或者从FIFO搬到内存。
Exynos平台用的是PL330 DMA控制器。配置DMA传输,核心是设置好这几个参数:
- 源地址(Source Address)—— 内存缓冲区地址
- 目的地址(Destination Address)—— I2S FIFO地址
- 传输宽度(Transfer Width)—— 每次传多少字节,通常是4字节(32位)
- 突发长度(Burst Length)—— 每次突发传几个数据,我一般设4或8
- 循环模式(Circular Mode)—— 音频播放需要循环传输
DMA配置代码示例:
/* 配置DMA传输 */
int exynos_i2s_dma_config(struct snd_pcm_substream *substream,
struct snd_dma_buffer *buf)
{
struct dma_slave_config config;
memset(&config, 0, sizeof(config));
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
config.direction = DMA_MEM_TO_DEV;
config.src_addr = buf->addr;
config.dst_addr = i2s->regs + I2STXD;
config.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
config.dst_maxburst = 8;
} else {
config.direction = DMA_DEV_TO_MEM;
config.src_addr = i2s->regs + I2SRXD;
config.dst_addr = buf->addr;
config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
config.src_maxburst = 8;
}
return dmaengine_slave_config(substream->dma_chan, &config);
}
我个人习惯把DMA的突发长度设成8。设太小,DMA频繁中断,CPU负载高。设太大,I2S FIFO可能溢出。8是个比较平衡的值。
核心要点:I2S驱动开发,本质就是配好寄存器、搭好DMA通道、处理好中断。这三件事做好了,音频就能跑起来。
小技巧:调试I2S时,先用loopback模式测试。把发送和接收短接,发一段正弦波,看收回来的是不是一样的。能对上,说明硬件通路没问题。
警告:千万不要在I2S正在传输时修改时钟配置。我曾经在调试时手贱改了预分频值,结果I2S控制器直接挂死,只能复位整个音频子系统。
好了,这一章的内容就到这里。I2S和PCM接口是音频驱动的基石,搞懂了它们,后面讲音频DSP和混音器时你会轻松很多。下一章我们聊聊音频时钟树,那又是一个让人头大的话题。