Skip to content

Latest commit

 

History

History
714 lines (560 loc) · 22.2 KB

File metadata and controls

714 lines (560 loc) · 22.2 KB

透過冰箱溫度控制器深入理解 SPI 通訊

🎯 本文目標:透過實際的冰箱溫度控制器專案,讓完全沒有數位通訊背景的初學者也能理解 SPI 協定的每個細節。

目錄

  1. 從冰箱的需求開始
  2. 什麼是 SPI?用打電話來理解
  3. 專案中的 ADC SPI 介面完整剖析
  4. 完整追蹤一次溫度讀取
  5. Testbench 實戰解析
  6. 常見錯誤與除錯
  7. 深入問題解答

1. 從冰箱的需求開始

1.1 問題場景

想像你正在設計一個智慧冰箱控制器。這個控制器需要:

  • 保持冷藏室溫度在 2-4°C
  • 保持冷凍室溫度在 -18°C
  • 即時顯示當前溫度
  • 溫度偏離時啟動壓縮機

1.2 核心問題:控制器怎麼知道現在幾度?

現實世界                數位世界
溫度 3.5°C    →    控制器需要數字
(連續的)          (離散的)

解決方案鏈

  1. 溫度感測器:將溫度轉換成電壓(例如:3.5°C → 0.535V)
  2. ADC(類比數位轉換器):將電壓轉換成數字(0.535V → 2191)
  3. 問題來了:ADC 的數字怎麼傳給控制器?

1.3 為什麼不能直接連線?

假設 ADC 輸出 12 位元數據:

方案一:平行傳輸(12 條資料線)
ADC ====== 12條線 ====== 控制器
問題:
- 需要 12 個接腳 → 晶片變大、變貴
- 佈線複雜 → 容易出錯
- 擴充困難 → 加感測器要更多線
方案二:序列傳輸(SPI)
ADC ---- 4條線 ---- 控制器
優點:
- 只需 4 個接腳 → 節省成本
- 可連接多個設備 → 擴充容易
- 標準化協定 → 相容性好

2. 什麼是 SPI?用打電話來理解

2.1 SPI 就像打電話

SPI (Serial Peripheral Interface) 是一種通訊協定,我們用打電話來類比:

打電話的過程           SPI 通訊過程
1. 撥號               → CS_N 拉低(片選)
2. 等待接通           → CS 建立時間
3. 按照節奏對話       → SCLK 時脈同步
4. 你說話             → MOSI 送出資料
5. 對方回答           → MISO 接收資料
6. 掛電話             → CS_N 拉高

2.2 SPI 的四條線詳解

為什麼是四條線?每條線解決什麼問題?

┌─────────────┐                    ┌─────────────┐
│   控制器     │                    │    ADC      │
│  (Master)   │                    │  (Slave)    │
│             │                    │             │
│         CS_N├───────────────────>│CS_N         │
│ (片選)      │  "要跟你說話"        │ (被選中)    │
│             │                    │             │
│         SCLK├───────────────────>│SCLK         │
│ (時脈)      │  "說話的節奏"        │ (聽節奏)    │
│             │                    │             │
│         MOSI├───────────────────>│MOSI         │
│ (主出從入)  │  "我說的話"          │ (聽你說)    │
│             │                    │             │
│         MISO│<───────────────────┤MISO         │
│ (主入從出)  │  "聽你回答"          │ (我回答)    │
└─────────────┘                    └─────────────┘

每條線的作用

  1. CS_N(Chip Select,低電位有效)

    • 問題:如果有多個設備,怎麼知道在跟誰通話?
    • 解決:用 CS_N 選擇特定設備,拉低表示「我要跟你說話」
  2. SCLK(Serial Clock)

    • 問題:雙方怎麼知道什麼時候送/收一個位元?
    • 解決:提供統一的節奏,像節拍器一樣
  3. MOSI(Master Out Slave In)

    • 問題:主設備的資料怎麼送出去?
    • 解決:專門的單向資料線,主設備送出
  4. MISO(Master In Slave Out)

    • 問題:從設備的資料怎麼送回來?
    • 解決:另一條單向資料線,從設備送出

2.3 為什麼不是三條或五條線?

為什麼不能只用三條線?

  • 如果合併 MOSI 和 MISO → 會產生衝突
  • 如果省略 CS_N → 無法選擇設備
  • 如果省略 SCLK → 無法同步

為什麼不需要五條線?

  • 不需要額外的確認線 → 時序固定,不需確認
  • 不需要電源線 → 通常共用系統電源

3. 專案中的 ADC SPI 介面完整剖析

3.1 為什麼需要 adc_spi_interface.v 模組?

問題分析:
┌──────────────┐         ┌──────────────┐
│  主控制器     │         │  ADC128S022  │
│              │    ?    │              │
│ 說"中文"     │ ======= │  說"SPI"     │
│ (內部匯流排) │         │  (SPI協定)   │
└──────────────┘         └──────────────┘

解決方案:
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  主控制器     │    │  SPI 介面     │    │  ADC128S022  │
│              │───>│              │───>│              │
│ start=1      │    │  翻譯官      │    │  SPI 協定    │
│ channel=2    │    │              │    │              │
│              │<───│              │<───│              │
│ adc_data=2191│    │              │    │  12-bit數據  │
└──────────────┘    └──────────────┘    └──────────────┘

3.2 模組介面詳解

讓我們逐行理解這個模組的介面:

module adc_spi_interface (
    // 系統信號 - 為什麼需要這些?
    input  wire        clk,         // 系統時脈 10 MHz - 所有動作的基準
    input  wire        rst_n,       // 重置信號 - 出錯時重新開始
    input  wire        clk_1mhz,    // SPI 時脈 1 MHz - ADC 的速度限制
    
    // 控制介面 - 主控制器怎麼告訴我們要做什麼?
    input  wire        start,       // 開始信號 - "請幫我讀溫度"
    input  wire [2:0]  channel,     // 通道選擇 - "讀哪個感測器" (0-7)
    output reg  [11:0] adc_data,    // ADC 結果 - "溫度是 2191"
    output reg         adc_valid,   // 資料有效 - "我讀完了"
    
    // SPI 介面 - 實際的通訊線路
    input  wire        spi_miso,    // 從 ADC 來的資料
    output reg         spi_mosi,    // 送給 ADC 的命令
    output reg         spi_sclk,    // SPI 時脈 - 通訊節奏
    output reg         spi_cs_n     // 片選 - 告訴 ADC "在跟你說話"
);

為什麼有兩個時脈?

  • clk (10 MHz):系統的心跳,所有邏輯運作的基準
  • clk_1mhz:SPI 通訊的節奏,因為 ADC 最快只能 1 MHz

3.3 狀態機設計 - 為什麼需要 6 個狀態?

localparam STATE_IDLE       = 3'b000;  // 待機 - 等待開始命令
localparam STATE_CS_LOW     = 3'b001;  // 片選拉低 - 告訴 ADC 要通訊
localparam STATE_XFER_HIGH  = 3'b010;  // 傳輸高電平 - 時脈的上半週期
localparam STATE_XFER_LOW   = 3'b011;  // 傳輸低電平 - 時脈的下半週期
localparam STATE_CS_HIGH    = 3'b100;  // 片選拉高 - 準備結束通訊
localparam STATE_DONE       = 3'b101;  // 完成 - 資料準備好了

為什麼不能簡化成 3 個狀態?

錯誤想法:IDLE → TRANSFER → DONE

問題:
1. CS 需要建立時間 - ADC 需要時間準備
2. 傳輸需要分兩階段 - 上升沿和下降沿做不同事
3. CS 需要保持時間 - 確保最後一個位元被正確接收

3.4 關鍵程式碼逐行解釋

3.4.1 時脈邊緣偵測 - 為什麼需要?

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        clk_1mhz_prev <= 1'b0;  // 重置時清除
    end else begin
        clk_1mhz_prev <= clk_1mhz;  // 記住上一個狀態
    end
end

// 偵測上升邊緣:從 0 變成 1 的瞬間
assign clk_1mhz_posedge = clk_1mhz & ~clk_1mhz_prev;
// 偵測下降邊緣:從 1 變成 0 的瞬間
assign clk_1mhz_negedge = ~clk_1mhz & clk_1mhz_prev;

為什麼要偵測邊緣?

時脈信號:  ___┐   ┌───┐   ┌───
               └───┘   └───┘

如果只看電平:
- 高電平期間會執行很多次
- 無法精確控制時序

偵測邊緣後:
- 上升邊緣:只在 ↑ 瞬間動作一次
- 下降邊緣:只在 ↓ 瞬間動作一次

3.4.2 CS 延遲計數 - 為什麼要等 4 個時脈?

STATE_CS_LOW: begin
    spi_cs_n <= 1'b0;  // 拉低 CS
    // 等待 4 個時脈週期
    if (cs_delay_counter < 4'd3)
        cs_delay_counter <= cs_delay_counter + 1'b1;
    else
        cs_delay_counter <= 4'd0;
end

為什麼要延遲?

時序需求(來自 ADC 規格書):
CS↓ 到第一個 SCLK 至少要 100ns

我們的時脈:10 MHz = 100ns 週期
所以等 4 個週期 = 400ns > 100ns ✓

如果不等待:
CS   ‾‾\_______________
SCLK ___/‾\_/‾\_/‾\_   ← 太快了!ADC 還沒準備好

正確的時序:
CS   ‾‾\_______________
SCLK _______/‾\_/‾\_   ← ADC 有時間準備
      ← 4個週期 →

3.4.3 資料傳輸的核心 - 移位暫存器

// 準備要送出的資料
tx_shift_reg <= {2'b00, channel, 11'b0};  // 通道放在位元 13-11

為什麼是這個格式?

ADC128S022 的命令格式(16 位元):
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│15 │14 │13 │12 │11 │10 │ 9 │ 8 │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
│ 0 │ 0 │ A2│ A1│ A0│ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
         └─通道選擇─┘
         
例如選擇通道 2 (010):
0000 1000 0000 0000 = 0x0800
// 傳送資料(在 STATE_XFER_LOW)
spi_mosi <= tx_shift_reg[15];  // 送出最高位元
tx_shift_reg <= {tx_shift_reg[14:0], 1'b0};  // 左移一位

移位暫存器如何工作?

第 1 個時脈:送出 bit 15
tx_shift_reg = 0000_1000_0000_0000
spi_mosi = 0 ────┘

第 2 個時脈:送出 bit 14(左移後的 bit 15)
tx_shift_reg = 0001_0000_0000_0000  (左移了)
spi_mosi = 0 ────┘

第 3 個時脈:送出 bit 13(原本的通道 bit A2)
tx_shift_reg = 0010_0000_0000_0000  (再左移)
spi_mosi = 0 ────┘

...以此類推 16 個時脈週期

3.4.4 同時接收資料

// 接收資料(在 STATE_XFER_HIGH)
rx_shift_reg <= {rx_shift_reg[14:0], spi_miso};

接收如何與傳送同步?

時脈週期 4-15 時,ADC 送出 12 位元資料:

SCLK  ___/‾\___/‾\___/‾\___/‾\___
MISO  ----<b11><b10><b9 ><b8 >----

每個上升沿,將 MISO 的值存入移位暫存器:
週期 4: rx_shift_reg = xxxx_xxxx_xxxx_xxx1  (假設 b11=1)
週期 5: rx_shift_reg = xxxx_xxxx_xxxx_xx10  (假設 b10=0)
...
週期 15: rx_shift_reg = xxxx_1010_1010_1010 (完整 12 位元)

3.5 為什麼最後要提取 bits [11:0]?

STATE_DONE: begin
    adc_data  <= rx_shift_reg[11:0];  // 提取低 12 位元
    adc_valid <= 1'b1;                // 告訴主控制器:資料好了
end

16 位元通訊,但只有 12 位元有效資料:

接收到的 16 位元:
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│15 │14 │13 │12 │11 │10 │ 9 │ 8 │ 7 │ 6 │ 5 │ 4 │ 3 │ 2 │ 1 │ 0 │
├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤
│ ? │ ? │ ? │ ? │D11│D10│D9 │D8 │D7 │D6 │D5 │D4 │D3 │D2 │D1 │D0 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
└──無效資料──┘  └────────────── 12 位元 ADC 值 ──────────────┘

4. 完整追蹤一次溫度讀取

4.1 觸發條件:主控制器要讀取冷藏室溫度

// 在主控制器中
if (timer_1sec) begin  // 每秒讀一次
    adc_start <= 1'b1;
    adc_channel <= 3'd0;  // 通道 0 = 冷藏室感測器
end

4.2 詳細追蹤每個狀態

讓我們追蹤讀取溫度 3.5°C 的完整過程:

步驟 1:計算預期的 ADC 值

溫度轉 ADC 值公式(從 testbench):
ADC值 = (溫度 + 50) × 4096 / 100

當溫度 = 3.5°C:
ADC值 = (3.5 + 50) × 4096 / 100
     = 53.5 × 40.96
     = 2191

二進位:2191 = 0000_1000_1000_1111

步驟 2:STATE_IDLE → STATE_CS_LOW

時間點 T0:
- start = 1, channel = 000
- next_state = STATE_CS_LOW
- 準備 tx_shift_reg = 0000_0000_0000_0000 (通道 0)

時間點 T1:進入 STATE_CS_LOW
- spi_cs_n = 0  (告訴 ADC:要開始通訊)
- 開始計數延遲

步驟 3:CS 建立時間(T1 - T4)

CS   ‾‾‾‾‾\___________________
          T1  T2  T3  T4
          └─ 400ns 延遲 ─┘

步驟 4:資料傳輸(T5 - T36)

每個位元需要 2 個狀態:
- STATE_XFER_HIGH:SCLK = 1,讀取 MISO
- STATE_XFER_LOW:SCLK = 0,更新 MOSI

16 個位元 × 2 個狀態 = 32 個狀態轉換

詳細時序(顯示關鍵位元):

位元 0-3:送出 00(固定)
CS_N  \_________________________
SCLK  ___/‾\___/‾\___/‾\___/‾\__
MOSI  ---<0 >---<0 >---<0 >---<0>
MISO  ---------------------------

位元 4-11:ADC 開始回應
SCLK  ___/‾\___/‾\___/‾\___/‾\__
MOSI  ---<0 >---<0 >---<0 >---<0>
MISO  ---<1 >---<0 >---<0 >---<0>  ← ADC 送出 1000_1000 (0x88)

位元 12-15:收到最後 4 位元
SCLK  ___/‾\___/‾\___/‾\___/‾\__
MOSI  ---<0 >---<0 >---<0 >---<0>
MISO  ---<1 >---<1 >---<1 >---<1>  ← ADC 送出 1111 (0xF)

步驟 5:STATE_CS_HIGH(T37 - T40)

結束通訊,但要確保 ADC 正確結束:
CS_N  _________________/‾‾‾‾‾‾‾‾
SCLK  _________________________
      T37 T38 T39 T40
      └─ 400ns 保持 ─┘

步驟 6:STATE_DONE(T41)

// 提取 12 位元資料
adc_data = rx_shift_reg[11:0] = 12'b1000_1000_1111 = 2191
adc_valid = 1  // 通知主控制器

// 主控制器計算實際溫度
temperature = (2191 × 100 / 4096) - 50 = 3.5°C ✓

4.3 完整時序圖

         10    20    30    40
         |     |     |     |
start    _/‾‾‾\_________________
         
state    IDLE │CS_L│  XFER...  │CS_H│DONE│IDLE

cs_n     ‾‾‾‾‾\__________________/‾‾‾‾‾‾‾‾‾‾

sclk     ___________/‾\_/‾\_/‾\..._________

bit_cnt  ----<0>----<1><2><3>...<15>-------

adc_valid_________________________/‾‾‾\____

時間(μs) 0    0.4   0.5         16.5 16.9

5. Testbench 實戰解析

5.1 ADC 模擬器的巧妙設計

temp_ctrl_top_tb.v 中,ADC 模擬器模擬真實 ADC 的行為:

// ADC 模型 - 接收命令
always @(posedge adc_sclk or posedge adc_cs_n) begin
    if (adc_cs_n) begin
        spi_bit_count <= 5'd0;
        spi_shift_reg <= 16'd0;
    end else begin
        spi_bit_count <= spi_bit_count + 1'b1;
        spi_shift_reg <= {spi_shift_reg[14:0], adc_mosi};  // 接收命令
    end
end

為什麼要計數位元?

  • 知道何時開始送出資料(第 4 個位元後)
  • 知道送哪個位元的資料
// ADC 模型 - 送出資料
always @(negedge adc_sclk or posedge adc_cs_n) begin
    if (adc_cs_n) begin
        adc_miso <= 1'b0;
    end else begin
        case (spi_bit_count)
            5'd4:  adc_miso <= adc_value[11];  // 開始送 MSB
            5'd5:  adc_miso <= adc_value[10];
            // ... 依序送出 12 位元
            5'd15: adc_miso <= adc_value[0];   // 送出 LSB
            default: adc_miso <= 1'b0;
        endcase
    end
end

為什麼從第 4 個位元開始?

ADC128S022 協定規定:
位元 0-3:接收命令期間,ADC 不送資料
位元 4-15:ADC 送出 12 位元轉換結果

這模擬了真實 ADC 的行為!

5.2 執行測試觀察結果

步驟 1:執行測試

cd fridge_temp_controller_sky130/testbench
make sim_top

步驟 2:觀察終端輸出

Setting temperature: 5.0°C (ADC: 2253)
Time: 1050000 | Temp: 5.0°C | PWM: 0 | State: NORMAL
Time: 2050000 | Temp: 5.0°C | PWM: 256 | State: NORMAL

輸出解釋

  • ADC 值 2253 = (5.0 + 50) × 4096 / 100
  • PWM 256 = 25% 功率(開始製冷)
  • State NORMAL = 正常運行狀態

5.3 在 GTKWave 中深入觀察

執行波形檢視

make wave_top

關鍵信號觀察清單

  1. SPI 通訊信號(最重要)

    DUT.adc_intf.spi_cs_n     - 片選信號
    DUT.adc_intf.spi_sclk     - SPI 時脈
    DUT.adc_intf.spi_mosi     - 送出的命令
    DUT.adc_intf.spi_miso     - 收到的資料
    
  2. 狀態機信號

    DUT.adc_intf.current_state - 當前狀態
    DUT.adc_intf.bit_counter   - 位元計數
    
  3. 資料暫存器

    DUT.adc_intf.tx_shift_reg  - 傳送移位暫存器
    DUT.adc_intf.rx_shift_reg  - 接收移位暫存器
    DUT.adc_intf.adc_data      - 最終結果
    

測量時序是否正確

  1. 測量 CS 建立時間

    • 設置游標在 CS 下降沿
    • 設置游標在第一個 SCLK 上升沿
    • 測量時間差 ≥ 400ns ✓
  2. 測量 SPI 時脈週期

    • 測量 SCLK 週期應該是 1μs(1 MHz)
  3. 驗證資料正確性

    • 在 bit_counter = 15 時
    • 查看 rx_shift_reg[11:0]
    • 應該等於設定的 ADC 值

6. 常見錯誤與除錯

6.1 溫度讀數完全錯誤

症狀:讀到 0 或 4095(最大值)

可能原因與解決

  1. SPI 連線問題

    // 檢查連線是否正確
    .adc_miso(adc_miso),  // 不要接反!
    .adc_mosi(adc_mosi),
  2. 時序問題

    檢查 GTKWave:
    - CS 和 SCLK 之間有延遲嗎?
    - SCLK 頻率是 1 MHz 嗎?
    

6.2 SPI 完全沒反應

症狀:所有 SPI 信號保持不變

除錯步驟

  1. 檢查 start 信號是否有觸發
  2. 檢查 rst_n 是否正確釋放
  3. 檢查狀態機是否卡在 IDLE
// 加入除錯訊息
always @(posedge clk) begin
    if (state != state_prev) begin
        $display("State: %b -> %b", state_prev, state);
    end
end

6.3 間歇性錯誤

症狀:有時正確,有時錯誤

可能是時序邊界問題

// 確保使用同步設計
always @(posedge clk) begin  // 統一使用 clk
    // 不要混用不同時脈域
end

7. 深入問題解答

7.1 為什麼用 1 MHz 而不是更快?

ADC128S022 規格限制

  • 最高 SCLK 頻率:3.2 MHz
  • 我們選擇 1 MHz 的原因:
    1. 安全餘量:1 MHz << 3.2 MHz
    2. 易於產生:10 MHz ÷ 10 = 1 MHz
    3. 足夠快:16 位元 × 1μs = 16μs(夠快)

如果用 5 MHz 會怎樣?

  • 超過規格 → ADC 可能出錯
  • 訊號完整性問題 → 雜訊增加

7.2 為什麼選 SPI 而不是 I2C?

比較表

特性 SPI I2C 我們的選擇理由
線數 4 2 SPI 雖然多 2 線,但更簡單
速度 快(MHz) 慢(kHz) 需要快速讀取溫度
複雜度 簡單 複雜(需 ACK) SPI 不需握手,更可靠
多從設備 每個需要 CS 地址識別 我們只有幾個感測器

7.3 如何支援多個溫度感測器?

方案 1:多個 CS 線

output wire cs_n_fridge,    // 冷藏室
output wire cs_n_freezer,   // 冷凍室
output wire cs_n_ambient    // 環境溫度

// 根據地址選擇
case (sensor_select)
    2'd0: cs_n_fridge = ~cs_active;
    2'd1: cs_n_freezer = ~cs_active;
    2'd2: cs_n_ambient = ~cs_active;
endcase

方案 2:使用多通道 ADC(我們的選擇)

ADC128S022 有 8 個通道:
通道 0:冷藏室
通道 1:冷凍室  
通道 2:環境溫度
通道 3-7:預留擴充

7.4 實際產品中的改進

  1. CRC 校驗:確保資料正確
  2. 多次讀取平均:提高準確度
  3. 異常值過濾:排除雜訊
  4. 校正表:補償感測器誤差

總結

透過這個冰箱溫度控制器的實例,我們深入理解了:

  1. 為什麼需要 SPI:解決多位元資料傳輸問題
  2. SPI 如何工作:透過 4 條線完成雙向通訊
  3. 實際設計考量:時序、狀態機、錯誤處理
  4. 驗證的重要性:透過 testbench 確保正確

記住,每一行程式碼都是為了解決特定問題而存在的。理解「為什麼」比記住「怎麼做」更重要!


下一步學習建議

  1. 修改通道試試看:改變 channel 值,觀察不同感測器
  2. 調整時序參數:改變延遲,看看會發生什麼
  3. 加入錯誤處理:如果 ADC 沒回應怎麼辦?
  4. 實作 CRC 校驗:確保資料完整性

祝您學習愉快!如果有任何問題,歡迎查看專案的其他文件或參考 GTKWave 波形分析。


文件版本:1.0
最後更新:2024-12-19
相關文件:GTKWave 使用指南 | 驗證策略