第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和混音器时你会轻松很多。下一章我们聊聊音频时钟树,那又是一个让人头大的话题。