0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 未分類
  3. 正文

I2S 通訊協定完整教學:從時序到 STM32/ESP32 音訊實作

2026 年 5 月 26 日 715點熱度 0人點贊 0條評論

I2S 通訊協定完整教學封面:小型揚聲器與發光的數位音訊波形

前言

我還記得第一次把 MEMS 麥克風接上 MCU 的那個下午。資料線接了、電源也給了,程式跑起來卻只拿到一整片雜訊。後來才發現問題出在時脈 —— 我把 I2S 當成 SPI 在用,少想了一件事:這個協定是為「連續不斷的音訊」設計的,不是為「偶爾傳幾個位元組」設計的。

I2S 通訊協定(Inter-IC Sound,也寫作 I²S)是飛利浦在 1986 年制定的數位音訊序列匯流排標準。不同於 SPI 或 I2C 的通用性,I2S 是專門為數位音訊設計的,用於在音訊編解碼晶片(Audio Codec)、MEMS 麥克風、DSP 和 MCU 之間傳輸 PCM 音訊資料。ESP32 內建了兩組 I2S 控制器,STM32 也有多組 SPI/I2S 可配置的周邊,這使得 I2S 成為嵌入式音訊專案的首選介面。

如果你還沒碰過序列匯流排,先看 SPI 通訊實戰會比較有感覺,I2S 的時序概念跟它最接近。這篇我打算從時序、模式、硬體設計一路講到實際程式碼,把 I2S 一次搞懂。

一、I2S 通訊協定基本概念

1.1 訊號線

I2S 使用三條訊號線(加上可選的 MCLK):

  • SCK(Serial Clock / Bit Clock):位元時脈,每個 SCK 週期傳送一個位元
  • WS(Word Select / Frame Sync):聲道選擇,0=左聲道,1=右聲道
  • SD(Serial Data):序列資料線,可配置為輸入(SDI)或輸出(SDO)
  • MCLK(Master Clock,可選):主時脈,通常為 256×FS 或 512×FS

三條線的分工很乾淨:SCK 負責「什麼時候看」,WS 負責「現在講左耳還是右耳」,SD 才是真正的資料。我在心裡的記法是 SCK 是節拍器、WS 是左右手、SD 才是球。

時序圖:SCK、WS 與 SD 三條線在一個立體聲幀裡的相對位置

看圖就知道為什麼它比 SPI 適合音訊:SCK 從頭到尾沒有停過,WS 每半個幀翻一次,SD 則是在 WS 變換後先空一拍,才把 MSB 推出來。這一拍就是 I2S 最重要的身分證。

1.2 時序特性

I2S 的核心特性是 MSB 先行(MSB First)且在 WS 變換後延遲一個 SCK 週期開始傳送(標準 Philips 模式)。這與 SPI 的 CPOL/CPHA 概念類似,但專門為連續的立體聲 PCM 資料做了優化。

  • WS 變換表示聲道切換(左→右→左→右...)
  • SD 資料在 SCK 的 rising edge 鎖存
  • 發送端在 SCK 的 falling edge 更新資料

為什麼要特地延遲一拍?因為發送端要留時間把下一個聲道的第一個位元推上線。這個設計有個副作用值得記著:同一個幀裡,資料的位數可以比實際有效位數多(例如 16-bit 資料塞進 32-bit 幀),多出來的位置補 0 就好,接收端只要知道對齊方式就不會誤判。

1.3 常見模式比較

雖然大家都叫 I2S,但「資料從哪一拍開始」這件事其實有三種派別,這張表是我最常回頭查的一張:

模式 MSB 起始點 相容性
I2S Philips(標準) WS 變換後延遲 1 SCK 最廣泛支援
Left-Justified WS 變換後立即輸出 MSB 某些老 Codec 使用
Right-Justified WS 變換後延遲到 LSB 對齊幀尾 日本廠商常見
TDM(Time Division Multiplexed) 多聲道分時共用一條 SD 線 多聲道系統(8ch+)

把它畫成波形就更清楚:標準模式空一拍、左對齊不空拍,差別只在 MSB 的位置。

邏輯分析圖:I2S 標準對齊與 Left-Justified 左對齊的 MSB 位置差異

這也是我踩過的第一個坑:Codec 的 datasheet 寫「Left-Justified」時,程式如果還是照 I2S Philips 設定,結果通常不是完全沒聲音,而是音量忽大忽小、聲音像被砍掉一半 —— 因為 MSB 被當成第二個位元,整個數值位移了。遇到這種症狀,先懷疑對齊模式。

二、取樣率與 SCK 計算

I2S 的 SCK 頻率由取樣率和資料寬度決定:

SCK = FS × Bits × Channels

例如 CD 音質(44.1 kHz、16-bit、立體聲):

SCK = 44100 × 16 × 2 = 1.4112 MHz

如果 Codec 要求 32-bit 幀(即使實際只用 16-bit data 也補 0):

SCK = 44100 × 32 × 2 = 2.8224 MHz (= 64 × FS)

算完公式,接著把常見的取樣率一次列出來對照。MCLK 的倍率我也一起放進來,因為有些 Codec 是「沒有 MCLK 就完全不動」的那種:

表格:16000、22050、44100 Hz 對應的 SCK 與 256×FS 的 MCLK

實務上注意:ESP32 的 I2S clock 來自 APLL(Audio PLL),頻率解析度有限。使用非標準取樣率(如 22050 Hz)時務必驗證實際 SCK 與目標誤差。

「誤差」這件事在音訊裡很現實:SCK 差 1% 對 UART 可能沒事,但對連續播放來說就是每 100 秒漏掉 1 秒的資料,聽起來會是週期性的喀噠聲。所以正式產品我一定會拿儀器量一次實際頻率。

三、I2S 硬體設計

3.1 系統架構

方塊圖:ESP32/STM32 當 Master 產生 SCK 與 WS,Codec 與麥克風當 Slave

架構只有一句話:ESP32/STM32 作為 Master 產生 SCK 和 WS,Audio Codec 和 MEMS 麥克風作為 Slave。Master 出時脈、Slave 跟時脈,這件事沒得商量 —— 你在很多音訊模組上看到「只能當 Slave」的說明,就是這個意思。

四條線裡唯一可選的是 MCLK。要不要接,取決於另一頭是誰:單純的 DAC(像 MAX98357)常常自己搞定時脈,但 WM8960 這類 Codec 就會明確要求 MCLK。

3.2 常見 I2S 元件

玩過的元件大概就是這幾顆,先看表再決定要買哪一塊:

元件 類型 說明
MAX98357 / I2S 放大器 DAC 3W 單聲道 D 類功放,I2S 輸入直驅喇叭
WM8960 Codec 雙聲道 DAC+ADC,常見於 ESP32-Audio-Kit
INMP441 MEMS Mic 全向 MEMS 麥克風,I2S 輸出
CS4344 DAC 低價立體聲 DAC,24-bit/192kHz

如果你要的是「把數位訊號變成聽得到的聲音」,這幾顆 I2S DAC 跟 DAC 原理講的是同一件事,只是 I2S DAC 把比較器與輸出級都包進去了。

3.3 接線注意事項

  1. MCLK 問題:某些 Codec(如 WM8960)需要 MCLK = 256×FS。ESP32 可輸出 MCLK,STM32 某些系列無獨立 MCLK 輸出
  2. 電壓位準:3.3V 為共識;5V Codec 需電平轉換
  3. SCK 頻率上限:ESP32 I2S 最高約 40 MHz(實際受 APLL 限制);STM32 SPI/I2S 最高約 37.5 MHz(@150 MHz HCLK)
  4. BCK 極性:I2S 標準要求 SCK idle = low,但某些 Codec 支援 idle = high

接線出錯的機率遠高於程式寫錯。我自己的順序是:先接三條必要的(SCK、WS、SD)、先讓它出聲,最後才處理 MCLK 與極性。一次把全部接上去,出事時你會有太多可疑對象。

四、ESP32 I2S 實作(Arduino 框架)

先看整條路徑長什麼樣,心裡有個地圖再寫程式:

方塊圖:從 WAV 檔或 Wi-Fi 串流經過應用層緩衝、I2S 週邊、DAC 到喇叭

Arduino 框架的好處是它把 I2S 的初始化壓成兩行,壞處是你會不知道底下發生了什麼。這節兩個方向(播放與錄音)我都給範例。

4.1 I2S 音訊輸出(播放 WAV)

#include <I2S.h>

#define I2S_BCK  26
#define I2S_WS   25
#define I2S_DOUT 22  // 資料輸出
#define I2S_DIN  21  // 資料輸入(麥克風用)

void setup()
{
    Serial.begin(115200);

    // 配置 I2S
    I2S.setPins(I2S_BCK, I2S_WS, I2S_DOUT, I2S_DIN);

    if (!I2S.begin(I2S_PHILIPS_MODE, 44100, 16))
    {
        Serial.println("I2S init failed!");
        while (1);
    }
    Serial.println("I2S OK - 44.1kHz 16-bit");
}

void loop()
{
    // 產生 440 Hz 正弦波測試音
    static float phase = 0;
    int16_t sample[128];
    const float freq = 440.0;
    const float fs   = 44100.0;

    for (int i = 0; i < 128; i++)
    {
        float val = sin(2 * PI * freq * phase / fs);
        sample[i] = (int16_t)(val * 16000);  // 振幅 ±16000
        phase++;
    }

    // 寫入 I2S(立體聲:左右聲道相同)
    size_t written = I2S.write((uint8_t *)sample, sizeof(sample));
    if (written != sizeof(sample))
        Serial.printf("Underrun! Wrote %d / %d\n", written, sizeof(sample));
}

這段有兩個細節值得停下來看:

  • I2S.begin(I2S_PHILIPS_MODE, 44100, 16) —— 模式、取樣率、位元深度,三個參數就決定後面所有的時序
  • sample[i] = (int16_t)(val * 16000) —— 振幅刻意留了 headroom(16-bit 的滿刻度是 32767),這是我吃過爆音之後學到的習慣

另外那個 written != sizeof(sample) 的檢查不要刪。它就是在抓 Buffer Underrun —— 播放速度比填資料快的時候,你會先在這裡看到訊息,而不是等到耳朵聽到斷音。

4.2 I2S 音訊輸入(MEMS 麥克風錄音)

#include <I2S.h>

#define I2S_BCK  26
#define I2S_WS   25
#define I2S_DIN  34     // INMP441 資料輸出腳

const int sample_rate = 16000;  // 語音辨識常用 16 kHz
const int bits = 16;
const int buffer_size = 512;

int16_t samples[buffer_size];

void setup()
{
    Serial.begin(115200);
    I2S.setPins(I2S_BCK, I2S_WS, -1, I2S_DIN);  // 僅輸入

    if (!I2S.begin(I2S_PHILIPS_MODE, sample_rate, bits))
    {
        Serial.println("I2S init failed!");
        while (1);
    }
}

void loop()
{
    size_t bytes_read = I2S.readBytes((char *)samples, buffer_size * 2);
    int samples_read = bytes_read / 2;

    // 計算 RMS 音量
    float sum_sq = 0;
    for (int i = 0; i < samples_read; i++)
        sum_sq += (float)samples[i] * samples[i];

    float rms = sqrt(sum_sq / samples_read);
    Serial.printf("RMS: %.2f\tPeak: %d\n", rms, samples[0]);

    delay(100);
}

錄音跟播放最大的差別有兩個:一是 I2S.setPins() 把輸出腳位設成 -1(告訴它我只要輸入);二是取樣率換成 16000 Hz,因為語音辨識常用 16 kHz,資料量直接少一半。

輸出用 RMS 而不是瞬時值,是因為音量的感知本來就是平均值。你如果直接印 samples[0],看到的數字會像樂透開獎一樣亂跳。

4.3 使用 I2S 讀取 INMP441 並透過 Wi-Fi 串流

#include <WiFi.h>
#include <I2S.h>

const char *ssid = "SSID";
const char *pass = "PASSWORD";
WiFiServer server(8080);

void setup()
{
    Serial.begin(115200);
    WiFi.begin(ssid, pass);

    I2S.setPins(26, 25, -1, 34);
    I2S.begin(I2S_PHILIPS_MODE, 16000, 16);

    server.begin();
    Serial.printf("Server started: %s:8080\n", WiFi.localIP().toString().c_str());
}

void loop()
{
    WiFiClient client = server.available();
    if (!client) return;

    // 持續串流 PCM 資料到客戶端
    uint8_t buf[512];
    while (client.connected())
    {
        size_t n = I2S.readBytes((char *)buf, 512);
        if (n > 0) client.write(buf, n);
    }
    client.stop();
}

這段就是把麥克風變成一個 TCP 音訊伺服器:server.available() 等到有人連進來,接著就是無止境地 readBytes() 再 write()。寫起來很短,但它同時點出了 I2S 的實務限制 —— I2S 只負責把資料搬進來,怎麼送出去是另一件事,而 Wi-Fi 的抖動會直接反映在你的緩衝策略上。

五、ESP32 I2S 實作(ESP-IDF 框架)

5.1 基本 I2S 初始化

ESP-IDF 的設定看起來很長,但結構其實很一致:通道參數、時脈參數、slot 參數、GPIO,一層一層填進去。

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

#define I2S_PORT  I2S_NUM_0

i2s_chan_handle_t tx_handle;

void i2s_init(void)
{
    i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(
        I2S_PORT, I2S_ROLE_MASTER);

    i2s_std_config_t std_cfg = {
        .clk_cfg = {
            .sample_rate_hz  = 44100,
            .clk_src         = I2S_CLK_SRC_DEFAULT,
            .mclk_multiple   = I2S_MCLK_MULTIPLE_256,
        },
        .slot_cfg = {
            .data_bit_width  = I2S_DATA_BIT_WIDTH_16BIT,
            .slot_bit_width  = I2S_SLOT_BIT_WIDTH_16BIT,
            .slot_mode       = I2S_SLOT_MODE_STEREO,
            .slot_mask       = I2S_STD_SLOT_BOTH,
            .ws_width        = I2S_DATA_BIT_WIDTH_16BIT,
            .ws_pol          = false,
            .bit_shift       = true,    // I2S Philips: delay 1 SCK
        },
        .gpio_cfg = {
            .mclk = GPIO_NUM_NC,
            .bclk = GPIO_NUM_26,
            .ws   = GPIO_NUM_25,
            .dout = GPIO_NUM_22,
            .din  = GPIO_NUM_21,
            .invert_flags = {
                .mclk_inv = false,
                .bclk_inv = false,
                .ws_inv   = false,
            },
        },
    };

    i2s_new_channel(&chan_cfg, &tx_handle, NULL);
    i2s_channel_init_std_mode(tx_handle, &std_cfg);
    i2s_channel_enable(tx_handle);
}

void play_sine(void)
{
    int16_t buf[256];
    static float phase = 0;

    for (int i = 0; i < 256; i += 2)
    {
        float val = sin(phase);
        int16_t s = (int16_t)(val * 16000);
        buf[i]   = s;   // 左聲道
        buf[i+1] = s;   // 右聲道
        phase += 2 * 3.14159 * 440.0 / 44100.0;
    }

    size_t written;
    i2s_channel_write(tx_handle, buf, 256 * 2, &written, portMAX_DELAY);
}

有兩行是我每次都會特別確認的:

  • .bit_shift = true —— 這就是「延遲一個 SCK」的開關,對應 I2S Philips 模式(註解原文就是這樣寫的)
  • .mclk_multiple = I2S_MCLK_MULTIPLE_256 —— MCLK 的倍率,要跟 Codec 的需求一致

跟 Arduino 版比起來,IDF 版多給你的是控制權:i2s_new_channel()、i2s_channel_init_std_mode()、i2s_channel_enable() 是三個獨立步驟,所以你也可以先建好通道、之後再決定要不要啟用。彈性變大,出錯時的檢查點也變多。

六、STM32 I2S 實作(HAL Library)

STM32 的 SPI 周邊可以配置為 I2S 模式(SPI_I2S)。以 STM32F407 為例,SPI2 可映射到 I2S2。這個「同一個周邊、兩種身分」的設計是 STM32 的特色,也是我第一次看 CubeMX 時最困惑的地方。

6.1 CubeMX 配置

  1. 設定 SPI2 / I2S2 為 Full-Duplex Master
  2. Standard = I2S Philips
  3. Data format = 16-bit
  4. Audio frequency = 44.1 kHz
  5. CK/Pin = PB13(SCK), WS = PB12, SD = PB15, MCK = PC6

這五項裡最容易漏掉的是最後一項的 MCK。CubeMX 預設不一定把 MCK 腳位打開,等你發現 Codec 沒反應才回頭找,就會多花半個小時。

6.2 I2S 初始化程式碼

#include "stm32f4xx_hal.h"

SPI_HandleTypeDef hspi2;

void MX_I2S2_Init(void)
{
    hspi2.Instance = SPI2;
    hspi2.Init.Mode = SPI_MODE_MASTER;
    hspi2.Init.Direction = SPI_DIRECTION_2LINES;
    hspi2.Init.DataSize = SPI_DATASIZE_16BIT;
    hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
    hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
    hspi2.Init.NSS = SPI_NSS_SOFT;
    hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4;
    hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
    hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
    hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi2.Init.CRCPolynomial = 10;
    HAL_SPI_Init(&hspi2);
}

void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    GPIO_InitTypeDef gpio = {0};

    __HAL_RCC_SPI2_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();

    // PB13 = SCK, PB12 = WS, PB15 = SD
    gpio.Mode = GPIO_MODE_AF_PP;
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_MEDIUM;
    gpio.Alternate = GPIO_AF5_SPI2;

    gpio.Pin = GPIO_PIN_13 | GPIO_PIN_12 | GPIO_PIN_15;
    HAL_GPIO_Init(GPIOB, &gpio);
}

void i2s_send_sample(uint16_t left, uint16_t right)
{
    uint16_t frame[2] = {right, left};  // I2S: WS=0=左, WS=1=右
    HAL_SPI_Transmit(&hspi2, (uint8_t *)frame, 2, HAL_MAX_DELAY);
}

void play_tone(void)
{
    for (int i = 0; i < 44100; i++)
    {
        float t = (float)i / 44100.0;
        int16_t s = (int16_t)(sin(2 * 3.14159 * 440 * t) * 16000);
        i2s_send_sample(s, s);  // 左右聲道相同
    }
}

這段裡我特別想指出一行:uint16_t frame[2] = {right, left};。順序是右聲道先放。原因是 WS 在一個幀開始時是 0(左聲道),但 I2S 標準又要求資料延遲一拍,所以第一個被送出去的其實是屬於 WS=1 的那個半幀。這個「看起來寫反了」的地方,就是很多人第一次用 STM32 接 Codec 時左右聲道顛倒的元凶。

另外 HAL_SPI_MspInit() 裡的 Alternate = GPIO_AF5_SPI2 也值得記:GPIO 的 AF 號碼對不上,波形就完全不會出現,而且不會有任何錯誤訊息。除錯時如果連 SCK 都量不到,先回來查這個。

七、實戰專案:Wi-Fi 網路音訊播放器

結合 ESP32 的 Wi-Fi 和 I2S,可以實作一個簡單的網路音訊播放器:

// 伺服器端(發送 PCM 資料)
# ESP32 作為 HTTP 音訊串流伺服器
// 見 4.3 的 Wi-Fi 串流範例

// 客戶端(收聽端)
# 任何支援 HTTP 的裝置:
curl http://esp32-ip:8080 --output - | aplay -r16000 -fS16_LE -c1

這個架構很適合當成第一個音訊專案,因為它把兩端都拆得很乾淨:ESP32 只負責「把 PCM 資料丟出來」,收聽端只要支援 HTTP 就聽得到。不過它有個天生的缺點 —— TCP 為了可靠度會重傳,重傳就意味著延遲。

如果要實現低延遲(<200ms)串流,改用 UDP/RTP 而非 TCP:

#include <WiFiUdp.h>

WiFiUDP udp;
const int port = 1234;
uint8_t pcm_buf[512];

void udp_stream_task(void *param)
{
    while (1)
    {
        // 從 I2S 讀取音訊
        size_t n = I2S.readBytes((char *)pcm_buf, 512);

        // 透過 UDP 發送
        udp.beginPacket(dest_ip, dest_port);
        udp.write(pcm_buf, n);
        udp.endPacket();
    }
}

把 TCP 換成 UDP 之後,丟包就變成你要自己面對的問題。這也是我在音訊專案裡對緩衝特別有感覺的原因:FIFO 緩衝的深度算得不夠,換什麼協定都還是會斷音。

八、常見問題與除錯

I2S 的除錯其實很線性:先確認時脈有沒有動、再看對齊與極性、最後才懷疑緩衝與電源。我把症狀整理成一張表,出事時直接對號入座:

表格:沒聲音、爆音、聲道相反、低延遲做不到的排查方向

8.1 沒有聲音?

  1. BCK 頻率錯誤:用邏輯分析儀或示波器量測 SCK 頻率是否等於 FS × bits × 2
  2. WS 極性:某些 Codec 的 WS 極性與 I2S 標準相反(如左聲道=1),程式需設定 WS polarity
  3. MCLK 缺失:Codec 需要 MCLK 但未提供,某些 Codec 可配置為不使用 MCLK
  4. I2S 模式不符:Codec 是 Left-Justified 但程式設成 I2S Philips

這四項的排查成本差很多:量 SCK 只要一支示波器,10 秒就有答案;懷疑模式則是回頭翻 datasheet。所以我永遠從第一項開始。

8.2 爆音或雜音

  1. 音量太大:正規化振幅不要超過 ±16383(16-bit),保留 headroom
  2. Pop Noise:Codec 電源開啟時先 mute,等輸出穩定後再播放
  3. Buffer Underrun:I2S 寫入速度趕不上播放速度,加大 buffer 或改善 Wi-Fi 延遲
  4. 時脈抖動:I2S 對 SCK 抖動敏感,避免在 I2S 正在播放時改變 APLL 參數

第三項的診斷方式很簡單:如果你的爆音是「週期性」的,幾乎就是 Underrun;如果是「隨機的雜訊」,那比較像是時脈或電源的問題。

8.3 I2S DMA 配置(STM32)

聲音要連續,就不能靠 CPU 一直輪詢、一直手動搬資料 —— 那正是 DMA 傳輸要解決的問題:

// I2S + DMA 循環模式播放
#define BUF_SIZE 512
uint16_t audio_buf[BUF_SIZE];

// 初始化 DMA
__HAL_LINKDMA(&hspi2, hdmatx, hdma_tx);

// DMA 傳輸完成回呼
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi->Instance == SPI2)
    {
        // 填寫下一段音訊資料到 audio_buf
        fill_next_buffer(audio_buf, BUF_SIZE);
    }
}

// 啟動 DMA 循環播放
HAL_SPI_Transmit_DMA(&hspi2, (uint8_t *)audio_buf, BUF_SIZE);

整個循環的邏輯我畫成流程圖:

流程圖:DMA 綁定、啟動傳輸、傳完觸發回呼、補資料的循環

這裡有個很容易被忽略的重點:DMA 幫你省掉的是「搬資料」,不是「準備資料」。真正決定聲音連不連續的,是你在回呼裡補得夠不夠快。緩衝越大越安全,但延遲也越高 —— 這就是音訊工程裡永遠的兩難。

九、總結

I2S 通訊協定是嵌入式音訊開發的必備技能。從簡單的 MEMS 麥克風錄音到高品質音訊串流,I2S 提供了專為音訊設計的可靠傳輸協定。重點回顧:

  1. 時序核心:MSB First + WS 延遲 1 SCK(I2S Philips)
  2. SCK 計算:FS × bits × channels;64×FS 是最常見配置
  3. 硬體考慮:MCLK 需求、電壓位準、BCK 上限
  4. 程式實作:ESP32 Arduino/ESP-IDF 與 STM32 HAL 都有完善支援
  5. 除錯方法:量測 SCK 頻率、檢查 WS 極性、觀察 DMA 狀態

無論是做語音辨識、藍牙音箱、還是音訊分析,I2S 都是你不可或缺的工具。

回顧整篇,我最想留下來的一句話是:I2S 的難點從來不在「傳什麼」,而在「什麼時候傳」。把時脈、聲道、對齊這三件事分別確認過一遍,剩下的就只是照公式填參數而已。

標籤: RS485 工業通訊
最後更新:2026 年 9 月 30 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang