# 音频使用说明

## 概述

K230 的音频子系统提供三条采集通路和一条播放通路：

| 通路 | 信号来源 | 涉及引脚 | 所属声卡 |
| --- | --- | --- | --- |
| 内置 codec 采集 | 板载模拟麦克风 | 芯片内部，不占用引脚 | `K230_I2S_INNO` |
| 外置 I2S 数字麦采集 | I2S 从设备（数字麦克风） | IO32、IO33 + IO34 或 IO36 | `K230_I2S_INNO` |
| PDM 数字麦采集 | PDM 麦克风，最多 4 根数据线 8 声道 | IO26 + IO27（或 IO37）、IO28（或 IO35）、IO36、IO34 | `K230_PDM` |
| 播放 | 内置 codec 或外置 I2S DAC | 耳机座 / IO35 | `K230_I2S_INNO` |

默认镜像只使能内置 codec。**外置 I2S 数字麦和 PDM 数字麦都需要修改板级设备树**，
分别见下面两节。

查看系统中的声卡：

```bash
[root@canaan ~ ]#cat /proc/asound/cards
 0 [K230I2SINNO    ]: K230_I2S_INNO - K230_I2S_INNO
                      K230_I2S_INNO
 1 [K230PDM        ]: K230_PDM - K230_PDM
                      K230_PDM
```

声卡编号由驱动注册顺序决定，未配置 PDM 时只有 0 号一张卡。本文中的
`hw:0,0`、`hw:1,0`、`amixer -c0` 按上面这份实测编号书写，实际编号以
`cat /proc/asound/cards` 为准。

## 内置 codec

板级设备树中的 `sound` 节点（各板卡默认已有，不需要修改）：

```dts
sound {
    status                      = "okay";
    compatible                  = "canaan,k230-audio-inno";
    canaan,model                = "K230_I2S_INNO";
    canaan,k230-i2s-controller  = <&i2s>;
    canaan,k230-audio-codec     = <&inno_codec>;
};
```

录音和播放：

```bash
# 板载模拟麦录音
arecord -D hw:0,0 -f S16_LE -r 44100 -c 2 -d 10 test.wav

# 播放
aplay -D hw:0,0 test.wav
```

## 外置 I2S 数字麦

外置数字麦克风作为 I2S 从设备接在 K230 上，K230 提供时钟。支持两种接法，
在设备树中选择。

### 引脚

| 引脚 | function | 功能 | 用途 |
| --- | --- | --- | --- |
| io32 | alt2 | `IIS_CLK` | 位时钟，两种接法都要 |
| io33 | alt2 | `IIS_WS` | 帧时钟，两种接法都要 |
| io34 | alt2 | `IIS_D_IN0` | 接法 A 的数据输入，对应声道 0/1 |
| io36 | alt2 | `IIS_D_IN1` | 接法 B 的数据输入，对应声道 2/3 |
| io35 | alt2 | `IIS_D_OUT0` | 数据输出，接外置 I2S DAC 时才需要 |

### 设备树属性

在 `sound` 节点里新增，两个属性都是可选的，都不写时行为与默认镜像一致：

| 属性 | 取值 | 缺省 | 作用 |
| --- | --- | --- | --- |
| `canaan,i2s-route` | `internal-codec` / `external-i2s` | `internal-codec` | 声道 0/1 的来源：内置 codec 或 IO34 |
| `canaan,rx1-source` | `none` / `external-i2s` | `none` | 是否启用 IO36 作为声道 2/3 |

`canaan,i2s-route` 只是开机默认值，运行时可以用 `Codec Route` 控件切换；
`canaan,rx1-source` 决定采集声道数上限（2 或 4），**只能开机确定**。

### 接法 A：IO34 外置数字麦，2 声道

声道 0/1 来自 IO34，内置 codec 不参与采集。

```dts
/ {
    sound {
        status                      = "okay";
        compatible                  = "canaan,k230-audio-inno";
        canaan,model                = "K230_I2S_INNO";
        canaan,k230-i2s-controller  = <&i2s>;
        canaan,k230-audio-codec     = <&inno_codec>;

        canaan,i2s-route = "external-i2s";
    };
};

&iomux {
    i2s_pins: i2s_pins {
        pin_clk_ws {
            pins = "io32", "io33";  /* IIS_CLK、IIS_WS */
            function = "alt2";
            output-enable;
            input-disable;
            bias-disable;
            drive-strength = <7>;
        };
        pin_din0 {
            pins = "io34";
            function = "alt2";      /* IIS_D_IN0 */
            input-enable;
            output-disable;
            bias-disable;
        };
        pin_dout0 {
            pins = "io35";
            function = "alt2";      /* IIS_D_OUT0，只录音可省略 */
            output-enable;
            input-disable;
            bias-disable;
            drive-strength = <7>;
        };
    };
};

&i2s {
    pinctrl-names = "default";
    pinctrl-0 = <&i2s_pins>;
};
```

采集：

```bash
amixer -c0 cset name='Codec Route' 'External I2S'   # 切到 IO34
arecord -D hw:0,0 -f S16_LE -r 44100 -c 2 -d 10 test.wav

amixer -c0 cset name='Codec Route' 'Internal Codec' # 切回板载模拟麦
```

```{attention}
`Codec Route` 是双向的：切到 `External I2S` 后，采集来自 IO34，播放数据从
**IO35** 输出，**板载喇叭无声**。要听声音需要在 IO35 接外置 I2S DAC，或者先切回
`Internal Codec` 再 `aplay`。

录音或播放进行中不能切换，此时 `amixer` 会返回 `Device or resource busy`。

要让选择在重启后保留，推荐用设备树属性 `canaan,i2s-route` 设定开机默认值。
默认镜像开机**不执行** `alsactl restore`，所以只跑 `alsactl store` 是不生效的，
要用它得自己补一个开机恢复脚本：

    alsactl store                       # 存到 /var/lib/alsa/asound.state
    cat > /etc/init.d/S60alsa <<'EOF'
    #!/bin/sh
    [ "$1" = start ] && alsactl restore
    EOF
    chmod +x /etc/init.d/S60alsa
```

### 接法 B：内置 codec + IO36 外置数字麦，4 声道

声道 0/1 是板载模拟麦，声道 2/3 是 IO36 上的外置数字麦，两者同时采集。
`sound` 节点**只加 `canaan,rx1-source`**：

```dts
/ {
    sound {
        status                      = "okay";
        compatible                  = "canaan,k230-audio-inno";
        canaan,model                = "K230_I2S_INNO";
        canaan,k230-i2s-controller  = <&i2s>;
        canaan,k230-audio-codec     = <&inno_codec>;

        canaan,rx1-source = "external-i2s";
    };
};

&iomux {
    i2s_pins: i2s_pins {
        pin_clk_ws {
            pins = "io32", "io33";  /* IIS_CLK、IIS_WS */
            function = "alt2";
            output-enable;
            input-disable;
            bias-disable;
            drive-strength = <7>;
        };
        pin_din1 {
            pins = "io36";
            function = "alt2";      /* IIS_D_IN1 → 声道 2/3 */
            input-enable;
            output-disable;
            bias-disable;
        };
    };
};

&i2s {
    pinctrl-names = "default";
    pinctrl-0 = <&i2s_pins>;
};
```

这种接法下 IO34 不参与采集，不需要配置，`Codec Route` 保持默认值
`Internal Codec` 即可，播放从板载喇叭正常出声。

采集并拆分成两个文件：

```bash
arecord -D hw:0,0 -f S16_LE -r 44100 -c 4 -d 10 test_4.wav

ffmpeg -i test_4.wav -af "pan=stereo|c0=c0|c1=c1" 内置.wav
ffmpeg -i test_4.wav -af "pan=stereo|c0=c2|c1=c3" 外置.wav
```

4 声道的交织顺序是 `[内置L][内置R][外置L][外置R]`。

### 检查

```bash
# 接法 B 应打印 external-i2s。设备树字符串带结尾 '\0'，用 tr 去掉便于脚本比较
cat /proc/device-tree/sound/canaan,rx1-source | tr -d '\0'

arecord -D hw:0,0 --dump-hw-params              # 接法 B 应出现 CHANNELS: [2 4]
amixer -c0 cget name='Codec Route'              # 查看当前通路
```

没写这个属性时 `/proc/device-tree/sound/canaan,rx1-source` **不存在**，`cat` 报
`No such file or directory` 是正常的，表示走缺省值 `none`。

## PDM 数字麦

PDM 是一条独立的采集通路，不占用 I2S 控制器，注册为单独的声卡 `K230_PDM`，
**只能采集，不能播放**，可以和内置 codec 的录音、播放同时使用。

### 接线和引脚

PDM 麦克风只需要时钟、数据、地和供电。一根数据线上可以挂两个麦克风，
两者的 `L/R` 引脚一个接 GND、一个接 VDD，各自占用时钟的一个边沿：

```text
K230 IO26 ─── CLK ──┬──▶ MIC A   L/R 接 GND → 上升沿
                    └──▶ MIC B   L/R 接 VDD → 下降沿

K230 IO27 ◀── DATA ─┬─── MIC A
                    └─── MIC B   两个麦克风共用这一根数据线
```

芯片共有 4 根数据线，每根 2 个麦克风，最多 8 声道：

| 数据线 | 引脚 | function | 对应声道 |
| --- | --- | --- | --- |
| PDM_IN0 | io27（或 io37） | alt3（io37 为 alt2） | 0、1 |
| PDM_IN1 | io28（或 io35） | alt3（io35 为 alt2） | 2、3 |
| PDM_IN2 | io36 | alt2 | 4、5 |
| PDM_IN3 | io34 | alt2 | 6、7 |

时钟固定用 io26（alt3，`PDM_CLK`）。

### 设备树配置

在板级设备树的根节点下新增 `sound_pdm` 节点，并配置引脚。下面是
**一根数据线、2 声道**的最小配置：

```dts
/ {
    sound_pdm: sound_pdm {
        status        = "okay";
        compatible    = "canaan,k230-audio-pdm";
        canaan,model  = "K230_PDM";
        clocks        = <&pdm_clk>;
        clock-names   = "pdm";
        dmas          = <&pdma 1 0xfff 0 0x21>;
        dma-names     = "rx";
        pinctrl-names = "default";
        pinctrl-0     = <&pdm_pins>;
    };
};

&iomux {
    pdm_pins: pdm_pins {
        pin_clk {
            pins = "io26";
            function = "alt3";      /* PDM_CLK */
            output-enable;
            input-disable;
            bias-disable;
            drive-strength = <7>;
            input-schmitt = <0>;
        };
        pin_data0 {
            pins = "io27";
            function = "alt3";      /* PDM_IN0 */
            input-enable;
            output-disable;
            bias-disable;
            drive-strength = <4>;
            input-schmitt = <0>;
        };
    };
};
```

`clocks`、`dmas` 这几行是固定写法，照抄即可。

### 可选属性

四个属性都可以不写，不写就是下表中的缺省值（一根数据线、双边沿、2 声道）：

| 属性 | 取值 | 缺省 | 作用 |
| --- | --- | --- | --- |
| `canaan,pdm-data-lines` | `1` ~ `4` | `1` | 接了几根数据线，从 PDM_IN0 数起 |
| `canaan,pdm-edge` | `both` / `rising` / `falling` | `both` | 在哪个时钟边沿采样，`both` 表示一根线两个麦 |
| `canaan,pdm-oversample` | `32` / `64` / `128` | `64` | 过采样倍率，与采样率一起决定 PDM 时钟频率 |
| `canaan,pdm-io-group` | `io27-28` / `io35-37` | `io27-28` | 前两根数据线接在哪组引脚上 |

**声道数 = 数据线数 × (`both` ? 2 : 1)**，最多 8。

```{attention}
`canaan,pdm-io-group` 必须和实际接线一致。前两根数据线接在 io35/io37 上的板卡
要写 `canaan,pdm-io-group = "io35-37"`，配错的表现是全程静音。

选了 `io35-37` 之后，**io35 和 io37 就不能再作 I2S 数据输出**，也就是
io35（`IIS_D_OUT0`）上接不了外置 I2S DAC 播放；同时 io27、io28 无论 pinctrl
怎么配都收不到 PDM 数据了。第 3、4 根数据线（io36、io34）不受这个属性影响，
按 pinctrl 配置即可。
```

### 增加数据线

`canaan,pdm-data-lines` 和 `pdm_pins` 里的引脚必须一起改，写几根线就在
`pins` 列表里列几个引脚。接满 4 根线（8 声道）时：

```dts
    sound_pdm: sound_pdm {
        /* 其余属性同上 */
        canaan,pdm-data-lines = <4>;    /* 4 根线 × 双边沿 = 8 声道 */
    };
```

```dts
&iomux {
    pdm_pins: pdm_pins {
        pin_clk {
            pins = "io26";
            function = "alt3";      /* PDM_CLK */
            output-enable;
            input-disable;
            bias-disable;
            drive-strength = <7>;
            input-schmitt = <0>;
        };

        pin_data01 {
            pins = "io27", "io28";  /* PDM_IN0、PDM_IN1 */
            function = "alt3";
            input-enable;
            output-disable;
            bias-disable;
            drive-strength = <4>;
            input-schmitt = <0>;
        };

        pin_data23 {
            pins = "io36", "io34";  /* PDM_IN2、PDM_IN3 */
            function = "alt2";
            input-enable;
            output-disable;
            bias-disable;
            drive-strength = <4>;
            input-schmitt = <0>;
        };
    };
};
```

一个子节点里的所有引脚共用同一条 `function` 和同一份配置，所以数据脚按档位分成
两个子节点：**io27、io28 是 alt3，io36、io34 是 alt2**，档位不同的引脚不能写在一起。

只要 6 声道就写 `canaan,pdm-data-lines = <3>`，并把 `pin_data23` 的 `pins`
改成只有 `"io36"`；只要 4 声道就写 `<2>`，整个 `pin_data23` 都不要。

```{attention}
**用到第 4 根线（io34）时，必须把 `Codec Route` 切到 `External I2S`**，
否则声道 6、7 始终是一个恒定的满量程值：

    amixer -c0 cset name='Codec Route' 'External I2S'

切过去之后内置 codec 的采集和播放都不可用（板载模拟麦、板载喇叭）。
只用 3 根线（io27 + io28 + io36，6 声道）没有这个限制。
```

### 采集

```bash
# 先确认驱动认到的配置和实际接线一致
dmesg | grep -i pdm
# PDM capture: 1 data line(s) on IO27/IO28, both edge, 64x oversampling, up to 2 channels

# 录音
arecord -D hw:1,0 -f S16_LE -r 48000 -c 2 -d 10 pdm.wav

# 从板载喇叭放出来听
aplay -D hw:0,0 pdm.wav
```

硬件出的是 24bit，驱动支持三种格式：

| `-f` | 得到的数据 | 使用场景 |
| --- | --- | --- |
| `S16_LE` | 24bit 的高 16 位 | 常规录音、语音识别，推荐 |
| `S24_LE` | 完整 24bit，装在 4 字节容器低位 | 需要满精度 |
| `S32_LE` | 24bit 左移 8 位 | 后续处理链要求 32bit 对齐 |

多声道采集，声道数只能从 1 到上限连续取，不能跳着指定：

```bash
arecord -D hw:1,0 --dump-hw-params 2>&1 | grep -i channels
# 根线时应为 CHANNELS: [1 8]

amixer -c0 cset name='Codec Route' 'External I2S'   # 需要声道 6、7 时才执行
arecord -D hw:1,0 -f S16_LE -r 48000 -c 8 -d 5 八声道.wav

# 拆成单声道文件
for i in 0 1 2 3 4 5 6 7; do
    ffmpeg -y -i 八声道.wav -af "pan=mono|c0=c$i" ch$i.wav
done
```

`canaan,pdm-edge = "both"` 时的声道对应关系：

| 声道 | 来源 | 声道 | 来源 |
| --- | --- | --- | --- |
| 0 | io27，L/R 接 GND | 4 | io36，L/R 接 GND |
| 1 | io27，L/R 接 VDD | 5 | io36，L/R 接 VDD |
| 2 | io28，L/R 接 GND | 6 | io34，L/R 接 GND |
| 3 | io28，L/R 接 VDD | 7 | io34，L/R 接 VDD |

PDM 和内置 codec 可以同时使用：

```bash
arecord -D hw:1,0 -f S16_LE -r 48000 -c 2 -d 20 pdm.wav &
arecord -D hw:0,0 -f S16_LE -r 48000 -c 2 -d 20 板载麦.wav &
aplay   -D hw:0,0 test.wav
wait
```

## 资源冲突

io34 和 io36 同时是 I2S 数据输入和 PDM 数据输入，同一时刻只能给一条通路使用：

| 需求 | PDM 数据线 | 可用的外置 I2S 数字麦 |
| --- | --- | --- |
| 2 声道 PDM | io27 | io34、io36 都可用 |
| 4 声道 PDM | io27 + io28 | io34、io36 都可用 |
| 6 声道 PDM | + io36 | 只剩 io34 |
| 8 声道 PDM | + io36 + io34 | 都不可用，且需要切 `Codec Route` |

板载模拟麦（内置 codec 采集）和喇叭播放在上述组合下都不受影响，
但 `Codec Route` 切到 `External I2S` 后不可用。

上表是前两根数据线接 io27、io28（缺省的 `canaan,pdm-io-group = "io27-28"`）的情况。
改接 io35、io37（`canaan,pdm-io-group = "io35-37"`）时还会多一条冲突：io35 是
`IIS_D_OUT0`，被 PDM 占走后**外置 I2S DAC 播放不可用**，板载喇叭播放不受影响。
