Skip to content

2.6 I2S 音频:PDM 与 PCM ​

这节要搞懂:声音在芯片里是怎么表示和传输的。 需要基础:2.3

本节是必修

天气时钟的闹铃靠喇叭播放声音,所以 I2S 是必须掌握的。

一、声音怎么变成数字 ​

模数采样:声音本质是空气的连续振动。要让计算机处理它,需要每隔固定时间测量一次振幅,并把测得的值记录为数字。

   连续的声波              离散的采样点
      ╱╲                     ●   ●
     ╱  ╲        →          ●     ●
    ╱    ╲                 ●       ●

量得越勤、记得越精细,还原出来越像原声。 这引出三个参数:

参数含义常用值
采样率一秒测量多少次人声 16000 Hz,音乐 44100 Hz
位深每次用多少位记录16 位(够用)
声道数单声道还是立体声单声道(省一半空间)

三、什么是 PCM 和 PDM ​

PCM(Pulse Code Modulation,脉冲编码调制):把每个采样点的振幅直接记录成一个数值。这是最直观、最通用的音频表示方式。

PDM(Pulse Density Modulation,脉冲密度调制):只用 1 位(0 或 1)的高速码流,通过 "1"出现的密集程度来表示振幅大小。需要经过滤波才能转成 PCM。

PCMPDM
怎么记直接记数值(比如 1523)记 0/1 码流,看密集程度
数据宽度16 / 24 / 32 位1 位
谁在用大多数麦克风、所有音频文件一些超小型 MEMS 麦克风
要不要转换不用,拿来就能用要转成 PCM

::: 我们使用的是PDM数字麦克风 :::

四、什么是 I2S ​

I2S(Inter-IC Sound)是专门用于传输数字音频数据的串行总线标准。它使用独立的时钟线和数据线,保证音频采样的时序精确。

注意:是 I2S(Sound),不是 I2C,两者完全不同,别看混。

它最少用三根线,另外还有一根可选的第四根:

线全称作用必须吗
BCLKBit Clock位时钟,每传一位数据跳一下✅ 必须
WSWord Select声道选择,现在传的是左还是右声道✅ 必须
SDSerial Data真正的音频数据✅ 必须
MCLKMaster Clock给对方芯片当"心跳"的高速时钟⚠️ 看芯片

下面的波形图画的是前三根(标配)。MCLK 在第六节单独讲。

I2S 的波形长什么样 ​

BCLKWSSD1011010001101011WS 低电平区间WS 高电平区间MSB 先出延迟 1 拍延迟 1 拍▲ = BCLK 上升沿,接收方在这里采样(数据已稳定)左声道数据(8 位)右声道数据(8 位)

先说一个看图容易困惑的地方

仔细看会发现:浅底色的"WS 低电平区间"和下面"左声道数据"的括号,是错开一拍的。

这不是画错了,正是标准 I2S 的规定 —— WS 总是提前一个 BCLK 翻转。

所以会出现一个反直觉的现象:左声道的最后一位(LSB),是在 WS 已经变高之后才发出来的。

图中两处标着"延迟 1 拍"的地方,就是这个偏移。

四个关键点:

① WS 用电平高低区分左右声道 ⭐

这是 I2S 最特别的地方 —— 它不像 I2C 那样用地址区分设备,而是用一根线的电平表示"现在传的是哪只耳朵的声音":

  • WS = 低电平 → 左声道
  • WS = 高电平 → 右声道

所以 WS 的频率就等于采样率。16000Hz 采样率,WS 就每秒翻转 16000 次(一高一低算一次)。

② 数据在 WS 翻转后"晚一拍"才开始 ⭐

仔细看图:WS 跳变之后,要等一个 BCLK 周期,数据才开始出来。

这一拍延迟是标准 I2S 格式的规定,也是它和"左对齐格式"的唯一区别:

格式WS 翻转后
标准 I2S延迟 1 个 BCLK 才开始传数据
左对齐(Left Justified)立刻开始传

格式配错的后果

这一拍的差别,会让接收方整体错位一位 —— 声音听起来会失真、有噪音,或者左右声道对调。

ESP-IDF 里 I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG 就是标准 I2S 格式,I2S_STD_MSB_SLOT_DEFAULT_CONFIG 是左对齐。接不上就换一个试试。

③ 数据在下降沿变化,在上升沿被采样

和 I2C 是同一个思路(2.3 讲过):在时钟的一个边沿改数据,在另一个边沿读数据,这样采样点永远落在数据稳定的中间。

  • BCLK 下降沿 → 发送方把下一位放上 SD 线(图中每个竖虚线处)
  • BCLK 上升沿 → 接收方读走这一位(图中 ▲ 标记处)

④ 先传高位(MSB First)

和 UART 相反 —— I2S 是从最高位开始传的。这也是为什么 2.6 后面那个"右移取高位"的坑会出现:数据靠在高位那一端。

BCLK 的频率怎么算

BCLK 频率 = 采样率 × 位深 × 声道数

16000 × 32 × 2 = 1,024,000 Hz ≈ 1MHz

所以 I2S 的时钟线比 I2C 快得多,接线短一点、别飞太长的线。

五、第四根线:MCLK ⭐ ​

MCLK(Master Clock,主时钟)是一路高速时钟,用来给对方的音频芯片提供内部工作节拍。它不传数据,只是"心跳"。

为什么有些芯片需要它:

麦克风、功放这类简单器件,靠 BCLK 就能干活。但音频编解码芯片(Codec) 内部有滤波器、PLL、模数转换电路,光有 BCLK 不够快,需要一路更快的时钟驱动内部电路。

哪些设备需要 MCLK ​

设备需要 MCLK 吗例子
数字麦克风(PDM / I2S)❌ 不需要LMD4030T261-OA1、INMP441
I2S 功放❌ 不需要NS4168、MAX98357A
音频 Codec 芯片✅ 通常需要ES8388、WM8978、ES7210

本课程用不到

我们用的 LMD4030T261-OA1(PDM 麦克风)和 NS4168(功放)都不需要 MCLK,所以代码里写的是:

c
.mclk = I2S_GPIO_UNUSED,        // 不用 MCLK

了解一下就行,以后接 Codec 芯片时再回来看。

MCLK 有多快 ​

MCLK 通常是采样率的固定倍数,常见 256 倍(也有 384、512 倍):

MCLK = 采样率 × 倍数

16000 Hz × 256 = 4,096,000 Hz  ≈ 4.1 MHz
44100 Hz × 256 = 11,289,600 Hz ≈ 11.3 MHz

三根时钟线比一比(以 16kHz / 32 位 / 双声道为例):

线频率怎么算的
MCLK4.096 MHz采样率 × 256
BCLK1.024 MHz采样率 × 位深 × 声道数
WS16 kHz= 采样率
MCLKBCLK4.096 MHz1.024 MHz这个例子里 MCLK 是 BCLK 的 4 倍

代码里怎么开 ​

c
i2s_std_config_t std_config = {
    .clk_cfg = {
        .sample_rate_hz = 16000,
        .clk_src        = I2S_CLK_SRC_DEFAULT,
        .mclk_multiple  = I2S_MCLK_MULTIPLE_256,   // ⭐ 256 倍
    },
    .gpio_cfg = {
        .mclk = GPIO_NUM_2,          // ⭐ 把 MCLK 输出到某个引脚(举例)
        .bclk = I2S_BCLK,
        .ws   = I2S_WS,
        .din  = I2S_DIN,
        .dout = I2S_GPIO_UNUSED,
    },
};

24 位数据的坑

如果位深是 24 位,mclk_multiple 必须用 384 的倍数,不能用 256。

因为 24 不能整除 256,算出来的分频系数不是整数,时钟会不准 —— 表现为声音跑调或有杂音。

16 位和 32 位没这个问题,用 256 就行。

六、代码长什么样 ​

本课程板子上 I2S 用在两个地方,一个收、一个发:

器件用哪种 I2S
收LMD4030T261-OA1 麦克风PDM 接收(driver/i2s_pdm.h)
发NS4168 功放标准 I2S 发送(driver/i2s_std.h)

收:PDM 麦克风 ​

c
#include "driver/i2s_pdm.h"

#define PDM_CLK    3          // 时钟,ESP32 输出给麦克风
#define PDM_DIN   42          // 数据,麦克风送回来

static i2s_chan_handle_t s_rx;

void mic_init(void)
{
    // 1. 创建接收通道(PDM 接收只能用 I2S0)
    i2s_chan_config_t chan_cfg =
        I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
    i2s_new_channel(&chan_cfg, NULL, &s_rx);

    // 2. 配置成 PDM 接收:16kHz / 16 位 / 单声道
    i2s_pdm_rx_config_t pdm_cfg = {
        .clk_cfg  = I2S_PDM_RX_CLK_DEFAULT_CONFIG(16000),
        .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(
                        I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
        .gpio_cfg = {
            .clk = PDM_CLK,
            .din = PDM_DIN,
        },
    };
    i2s_channel_init_pdm_rx_mode(s_rx, &pdm_cfg);
    i2s_channel_enable(s_rx);
}

// 读数据:出来的直接就是 16 位 PCM
int16_t buf[512];
size_t got;
i2s_channel_read(s_rx, buf, sizeof(buf), &got, portMAX_DELAY);

PDM 麦克风只有两根信号线

LMD4030T261-OA1 的引脚就是 CLK + DATA(加上电源地和一个声道选择脚),没有 WS,也没有 DIN/DOUT 这种双向线 —— 它只会往外吐数据。

配置里也就只填 .clk 和 .din 两项,.din 指的是ESP32 这一侧的"数据进来",接的是麦克风的 DATA 脚。

发:NS4168 功放 ​

c
#include "driver/i2s_std.h"
#include "driver/gpio.h"

#define I2S_BCLK  46          // 位时钟
#define I2S_LRC    9          // 声道选择
#define I2S_DOUT   8          // 音频数据,ESP32 发出去
#define I2S_CTRL  38          // ⭐ 功放使能,拉高才出声

static i2s_chan_handle_t s_tx;

void speaker_init(void)
{
    // 1. 先把功放打开
    gpio_config_t ctrl = {
        .pin_bit_mask = 1ULL << I2S_CTRL,
        .mode         = GPIO_MODE_OUTPUT,
    };
    gpio_config(&ctrl);
    gpio_set_level(I2S_CTRL, 1);          // ⭐ 漏了这句,后面全白搭

    // 2. 创建发送通道
    i2s_chan_config_t chan_cfg =
        I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_1, I2S_ROLE_MASTER);
    i2s_new_channel(&chan_cfg, &s_tx, NULL);

    // 3. 配置成标准 I2S 发送
    i2s_std_config_t std_cfg = {
        .clk_cfg  = I2S_STD_CLK_DEFAULT_CONFIG(16000),
        .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
                        I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
        .gpio_cfg = {
            .mclk = I2S_GPIO_UNUSED,      // NS4168 不需要 MCLK
            .bclk = I2S_BCLK,
            .ws   = I2S_LRC,
            .dout = I2S_DOUT,
            .din  = I2S_GPIO_UNUSED,      // 只发不收
        },
    };
    i2s_channel_init_std_mode(s_tx, &std_cfg);
    i2s_channel_enable(s_tx);
}

// 发数据
i2s_channel_write(s_tx, pcm_buf, sizeof(pcm_buf), &written, portMAX_DELAY);

七、最容易踩的坑 ⭐ ​

① CTRL 不拉高,一点声音都没有

NS4168 的 CTRL(GPIO38)是使能脚。开机后 GPIO 默认是输入态,功放收不到"开工"信号,I2S 数据发得再欢也不会响。

这个坑特别难查 —— 代码没报错、波形也正常,就是没声音。先量一下 GPIO38 是不是 3.3V。

② PDM 接收只能用 I2S0

ESP32-S3 上只有 I2S0 支持 PDM 接收,I2S1 不行。写成 I2S_NUM_1 会在初始化时直接报错。

所以本课程的分配是:麦克风占 I2S0,功放占 I2S1,两边互不干扰。

③ 采样率对不上,声音会变调

播放的时候,I2S 配的采样率必须和音频数据本身的采样率一致。

拿 16kHz 录的音,按 8kHz 播 → 慢一倍,声音变低沉;按 32kHz 播 → 快一倍,变成尖细的"花栗鼠"。

声音能出来但不对劲,先怀疑采样率。

本节要点 ​

  • 采样 = 每隔固定时间测一次振幅,记成数字
  • 三个参数:采样率、位深、声道数,决定数据量
  • 录人声用 16000 / 16 位 / 单声道就够了
  • PCM 记数值、PDM 记密度,新手选 PCM 麦克风
  • I2S = 传音频的专用总线,三根必须的线:BCLK / WS / SD
  • MCLK 是可选的第四根,给 Codec 芯片当心跳;麦克风、功放不需要
  • 本课程用 PDM 麦克风,硬件自动转 PCM,读出来直接能用
  • 数据在 BCLK 下降沿变化、上升沿采样;MSB 先出
  • ⭐ 功放的 CTRL(GPIO38)必须拉高,否则一点声音都没有
  • ⭐ PDM 接收只能挂 I2S0;本课程麦克风占 I2S0、功放占 I2S1

操作过程 ​

目标:采集 PDM 麦克风的音频,串口打印音量大小 —— 对着麦克风说话数字会变大。

第 1 步:建工程 ​

建工程流程见 2.2 第 2 步

用 VS Code 的 ESP-IDF 新项目向导,四步点完。每节都一样,只有这两处不同:

字段本节填
Project Namemic_demo
Choose ESP-IDF Targetesp32s3(每节都一样,别选错)

第 2 步:写代码 ​

c
#include <stdio.h>
#include <stdlib.h>
#include "driver/i2s_pdm.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#define PDM_CLK    3
#define PDM_DIN   42
#define SAMPLES   512

static i2s_chan_handle_t s_rx;

void app_main(void)
{
    // 1. 创建接收通道(PDM 只能用 I2S0)
    i2s_chan_config_t chan_cfg =
        I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
    ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, NULL, &s_rx));

    // 2. 配置成 PDM 接收:16kHz / 16 位 / 单声道
    i2s_pdm_rx_config_t pdm_cfg = {
        .clk_cfg  = I2S_PDM_RX_CLK_DEFAULT_CONFIG(16000),
        .slot_cfg = I2S_PDM_RX_SLOT_DEFAULT_CONFIG(
                        I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
        .gpio_cfg = {
            .clk = PDM_CLK,
            .din = PDM_DIN,
        },
    };
    ESP_ERROR_CHECK(i2s_channel_init_pdm_rx_mode(s_rx, &pdm_cfg));
    ESP_ERROR_CHECK(i2s_channel_enable(s_rx));

    // 3. 循环采集,算出这一批的峰值
    static int16_t buf[SAMPLES];

    while (1) {
        size_t got = 0;
        i2s_channel_read(s_rx, buf, sizeof(buf), &got, portMAX_DELAY);

        int count = got / sizeof(int16_t);
        int peak = 0;

        for (int i = 0; i < count; i++) {
            int v = abs(buf[i]);
            if (v > peak) {
                peak = v;
            }
        }

        // 用字符长度直观表示音量
        printf("peak = %5d  ", peak);
        for (int i = 0; i < peak / 500 && i < 40; i++) {
            printf("#");
        }
        printf("\n");

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

第 3 步:编译烧录 ​

操作流程见 2.2 第 4 步

在 VS Code 底部状态栏依次点:编译 → 烧录(或者直接点"编译、烧录并监视")。

烧录方式、串口、芯片型号第一次设好就不用再动了。

第 4 步:应该看到什么 ​

安静时峰值很小,对着麦克风说话时数字变大、# 明显变长:

peak =   180
peak =   210
peak =  4820  #########
peak =  9350  ##################
peak =  6100  ############
peak =   240

全是 0 就别往下走

说明麦克风根本没采到数据,先回头检查代码和引脚号,不要接着往下做。

动手试试 ​

  1. 算一算:44100 采样率、16 位、立体声,录 1 分钟要多少 MB?
  2. 如果你有麦克风,采集 1 秒数据,打印其中几个数值,确认不是全 0。
  3. 算一算:44.1kHz 采样率下,MCLK(256 倍)是多少 MHz?BCLK(16 位立体声)又是多少?