🎯 本文目標:透過實際的冰箱溫度控制器專案,讓完全沒有數位通訊背景的初學者也能理解 SPI 協定的每個細節。
想像你正在設計一個智慧冰箱控制器。這個控制器需要:
- 保持冷藏室溫度在 2-4°C
- 保持冷凍室溫度在 -18°C
- 即時顯示當前溫度
- 溫度偏離時啟動壓縮機
現實世界 數位世界
溫度 3.5°C → 控制器需要數字
(連續的) (離散的)
解決方案鏈:
- 溫度感測器:將溫度轉換成電壓(例如:3.5°C → 0.535V)
- ADC(類比數位轉換器):將電壓轉換成數字(0.535V → 2191)
- 問題來了:ADC 的數字怎麼傳給控制器?
假設 ADC 輸出 12 位元數據:
方案一:平行傳輸(12 條資料線)
ADC ====== 12條線 ====== 控制器
問題:
- 需要 12 個接腳 → 晶片變大、變貴
- 佈線複雜 → 容易出錯
- 擴充困難 → 加感測器要更多線
方案二:序列傳輸(SPI)
ADC ---- 4條線 ---- 控制器
優點:
- 只需 4 個接腳 → 節省成本
- 可連接多個設備 → 擴充容易
- 標準化協定 → 相容性好
SPI (Serial Peripheral Interface) 是一種通訊協定,我們用打電話來類比:
打電話的過程 SPI 通訊過程
1. 撥號 → CS_N 拉低(片選)
2. 等待接通 → CS 建立時間
3. 按照節奏對話 → SCLK 時脈同步
4. 你說話 → MOSI 送出資料
5. 對方回答 → MISO 接收資料
6. 掛電話 → CS_N 拉高
┌─────────────┐ ┌─────────────┐
│ 控制器 │ │ ADC │
│ (Master) │ │ (Slave) │
│ │ │ │
│ CS_N├───────────────────>│CS_N │
│ (片選) │ "要跟你說話" │ (被選中) │
│ │ │ │
│ SCLK├───────────────────>│SCLK │
│ (時脈) │ "說話的節奏" │ (聽節奏) │
│ │ │ │
│ MOSI├───────────────────>│MOSI │
│ (主出從入) │ "我說的話" │ (聽你說) │
│ │ │ │
│ MISO│<───────────────────┤MISO │
│ (主入從出) │ "聽你回答" │ (我回答) │
└─────────────┘ └─────────────┘
每條線的作用:
-
CS_N(Chip Select,低電位有效)
- 問題:如果有多個設備,怎麼知道在跟誰通話?
- 解決:用 CS_N 選擇特定設備,拉低表示「我要跟你說話」
-
SCLK(Serial Clock)
- 問題:雙方怎麼知道什麼時候送/收一個位元?
- 解決:提供統一的節奏,像節拍器一樣
-
MOSI(Master Out Slave In)
- 問題:主設備的資料怎麼送出去?
- 解決:專門的單向資料線,主設備送出
-
MISO(Master In Slave Out)
- 問題:從設備的資料怎麼送回來?
- 解決:另一條單向資料線,從設備送出
為什麼不能只用三條線?
- 如果合併 MOSI 和 MISO → 會產生衝突
- 如果省略 CS_N → 無法選擇設備
- 如果省略 SCLK → 無法同步
為什麼不需要五條線?
- 不需要額外的確認線 → 時序固定,不需確認
- 不需要電源線 → 通常共用系統電源
問題分析:
┌──────────────┐ ┌──────────────┐
│ 主控制器 │ │ ADC128S022 │
│ │ ? │ │
│ 說"中文" │ ======= │ 說"SPI" │
│ (內部匯流排) │ │ (SPI協定) │
└──────────────┘ └──────────────┘
解決方案:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 主控制器 │ │ SPI 介面 │ │ ADC128S022 │
│ │───>│ │───>│ │
│ start=1 │ │ 翻譯官 │ │ SPI 協定 │
│ channel=2 │ │ │ │ │
│ │<───│ │<───│ │
│ adc_data=2191│ │ │ │ 12-bit數據 │
└──────────────┘ └──────────────┘ └──────────────┘
讓我們逐行理解這個模組的介面:
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
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 需要保持時間 - 確保最後一個位元被正確接收
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;為什麼要偵測邊緣?
時脈信號: ___┐ ┌───┐ ┌───
└───┘ └───┘
如果只看電平:
- 高電平期間會執行很多次
- 無法精確控制時序
偵測邊緣後:
- 上升邊緣:只在 ↑ 瞬間動作一次
- 下降邊緣:只在 ↓ 瞬間動作一次
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個週期 →
// 準備要送出的資料
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 個時脈週期
// 接收資料(在 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 位元)
STATE_DONE: begin
adc_data <= rx_shift_reg[11:0]; // 提取低 12 位元
adc_valid <= 1'b1; // 告訴主控制器:資料好了
end16 位元通訊,但只有 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 值 ──────────────┘
// 在主控制器中
if (timer_1sec) begin // 每秒讀一次
adc_start <= 1'b1;
adc_channel <= 3'd0; // 通道 0 = 冷藏室感測器
end讓我們追蹤讀取溫度 3.5°C 的完整過程:
溫度轉 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
時間點 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:要開始通訊)
- 開始計數延遲
CS ‾‾‾‾‾\___________________
T1 T2 T3 T4
└─ 400ns 延遲 ─┘
每個位元需要 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)
結束通訊,但要確保 ADC 正確結束:
CS_N _________________/‾‾‾‾‾‾‾‾
SCLK _________________________
T37 T38 T39 T40
└─ 400ns 保持 ─┘
// 提取 12 位元資料
adc_data = rx_shift_reg[11:0] = 12'b1000_1000_1111 = 2191
adc_valid = 1 // 通知主控制器
// 主控制器計算實際溫度
temperature = (2191 × 100 / 4096) - 50 = 3.5°C ✓ 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
在 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 的行為!
cd fridge_temp_controller_sky130/testbench
make sim_topSetting 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 = 正常運行狀態
make wave_top-
SPI 通訊信號(最重要)
DUT.adc_intf.spi_cs_n - 片選信號 DUT.adc_intf.spi_sclk - SPI 時脈 DUT.adc_intf.spi_mosi - 送出的命令 DUT.adc_intf.spi_miso - 收到的資料 -
狀態機信號
DUT.adc_intf.current_state - 當前狀態 DUT.adc_intf.bit_counter - 位元計數 -
資料暫存器
DUT.adc_intf.tx_shift_reg - 傳送移位暫存器 DUT.adc_intf.rx_shift_reg - 接收移位暫存器 DUT.adc_intf.adc_data - 最終結果
-
測量 CS 建立時間
- 設置游標在 CS 下降沿
- 設置游標在第一個 SCLK 上升沿
- 測量時間差 ≥ 400ns ✓
-
測量 SPI 時脈週期
- 測量 SCLK 週期應該是 1μs(1 MHz)
-
驗證資料正確性
- 在 bit_counter = 15 時
- 查看 rx_shift_reg[11:0]
- 應該等於設定的 ADC 值
症狀:讀到 0 或 4095(最大值)
可能原因與解決:
-
SPI 連線問題
// 檢查連線是否正確 .adc_miso(adc_miso), // 不要接反! .adc_mosi(adc_mosi),
-
時序問題
檢查 GTKWave: - CS 和 SCLK 之間有延遲嗎? - SCLK 頻率是 1 MHz 嗎?
症狀:所有 SPI 信號保持不變
除錯步驟:
- 檢查 start 信號是否有觸發
- 檢查 rst_n 是否正確釋放
- 檢查狀態機是否卡在 IDLE
// 加入除錯訊息
always @(posedge clk) begin
if (state != state_prev) begin
$display("State: %b -> %b", state_prev, state);
end
end症狀:有時正確,有時錯誤
可能是時序邊界問題:
// 確保使用同步設計
always @(posedge clk) begin // 統一使用 clk
// 不要混用不同時脈域
endADC128S022 規格限制:
- 最高 SCLK 頻率:3.2 MHz
- 我們選擇 1 MHz 的原因:
- 安全餘量:1 MHz << 3.2 MHz
- 易於產生:10 MHz ÷ 10 = 1 MHz
- 足夠快:16 位元 × 1μs = 16μs(夠快)
如果用 5 MHz 會怎樣?
- 超過規格 → ADC 可能出錯
- 訊號完整性問題 → 雜訊增加
比較表:
| 特性 | SPI | I2C | 我們的選擇理由 |
|---|---|---|---|
| 線數 | 4 | 2 | SPI 雖然多 2 線,但更簡單 |
| 速度 | 快(MHz) | 慢(kHz) | 需要快速讀取溫度 |
| 複雜度 | 簡單 | 複雜(需 ACK) | SPI 不需握手,更可靠 |
| 多從設備 | 每個需要 CS | 地址識別 | 我們只有幾個感測器 |
方案 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:預留擴充
- CRC 校驗:確保資料正確
- 多次讀取平均:提高準確度
- 異常值過濾:排除雜訊
- 校正表:補償感測器誤差
透過這個冰箱溫度控制器的實例,我們深入理解了:
- 為什麼需要 SPI:解決多位元資料傳輸問題
- SPI 如何工作:透過 4 條線完成雙向通訊
- 實際設計考量:時序、狀態機、錯誤處理
- 驗證的重要性:透過 testbench 確保正確
記住,每一行程式碼都是為了解決特定問題而存在的。理解「為什麼」比記住「怎麼做」更重要!
- 修改通道試試看:改變 channel 值,觀察不同感測器
- 調整時序參數:改變延遲,看看會發生什麼
- 加入錯誤處理:如果 ADC 沒回應怎麼辦?
- 實作 CRC 校驗:確保資料完整性
祝您學習愉快!如果有任何問題,歡迎查看專案的其他文件或參考 GTKWave 波形分析。
文件版本:1.0
最後更新:2024-12-19
相關文件:GTKWave 使用指南 | 驗證策略