外观
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。
| PCM | PDM | |
|---|---|---|
| 怎么记 | 直接记数值(比如 1523) | 记 0/1 码流,看密集程度 |
| 数据宽度 | 16 / 24 / 32 位 | 1 位 |
| 谁在用 | 大多数麦克风、所有音频文件 | 一些超小型 MEMS 麦克风 |
| 要不要转换 | 不用,拿来就能用 | 要转成 PCM |
::: 我们使用的是PDM数字麦克风 :::
四、什么是 I2S
I2S(Inter-IC Sound)是专门用于传输数字音频数据的串行总线标准。它使用独立的时钟线和数据线,保证音频采样的时序精确。
注意:是 I2S(Sound),不是 I2C,两者完全不同,别看混。
它最少用三根线,另外还有一根可选的第四根:
| 线 | 全称 | 作用 | 必须吗 |
|---|---|---|---|
| BCLK | Bit Clock | 位时钟,每传一位数据跳一下 | ✅ 必须 |
| WS | Word Select | 声道选择,现在传的是左还是右声道 | ✅ 必须 |
| SD | Serial Data | 真正的音频数据 | ✅ 必须 |
| MCLK | Master Clock | 给对方芯片当"心跳"的高速时钟 | ⚠️ 看芯片 |
下面的波形图画的是前三根(标配)。MCLK 在第六节单独讲。
I2S 的波形长什么样
先说一个看图容易困惑的地方
仔细看会发现:浅底色的"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 位 / 双声道为例):
| 线 | 频率 | 怎么算的 |
|---|---|---|
| MCLK | 4.096 MHz | 采样率 × 256 |
| BCLK | 1.024 MHz | 采样率 × 位深 × 声道数 |
| WS | 16 kHz | = 采样率 |
代码里怎么开
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 Name | mic_demo |
| Choose ESP-IDF Target | esp32s3(每节都一样,别选错) |
第 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 步:编译烧录
第 4 步:应该看到什么
安静时峰值很小,对着麦克风说话时数字变大、# 明显变长:
peak = 180
peak = 210
peak = 4820 #########
peak = 9350 ##################
peak = 6100 ############
peak = 240全是 0 就别往下走
说明麦克风根本没采到数据,先回头检查代码和引脚号,不要接着往下做。
动手试试
- 算一算:44100 采样率、16 位、立体声,录 1 分钟要多少 MB?
- 如果你有麦克风,采集 1 秒数据,打印其中几个数值,确认不是全 0。
- 算一算:44.1kHz 采样率下,MCLK(256 倍)是多少 MHz?BCLK(16 位立体声)又是多少?