更多文章內容,請關注微信公衆號“FPGA科技室”
IIC 基本特性
總線信號
SDA:串行數據線
SCL:串行數據時鐘
總線空閒狀態
SDA:高電平
SCL:高電平
IIC 協議起始位
SCL 爲高電平時,SDA 出現下降沿,產生一個起始位。
IIC 協議結束位
SCL 爲高電平時,SDA 出現上升沿,產生一個結束位。
IIC 讀寫單字節時序
IIC 主機對 IIC 從機寫入數據時,SDA 上的每一位數據在 SCL 的高電平期間被寫入從機中。對於主機,在 SCL 的低電平期間改變要寫入的數據。
IIC 主機從 IIC 從機中讀出數據時,從機在 SCL 的低電平期間將數據輸出到 SDA 總線上,在SCL 的高電平期間保持數據穩定。對於主機,在 SCL 的高電平期間將 SDA 線上的數據讀取並存儲。
數據接收方對數據發送方的響應
每當一個字節的數據或命令傳輸完成時,都會有一位的應答位。需要應答位時,數據發出方將 SDA 總線設置爲 3 態輸入,由於 IIC 總線上都有上拉電阻,因此此時總線默認爲高電平,若數據接收方正確接收到數據,則數據接收方將 SDA 總線拉低,以示正確應答。
例如當 IIC 主機對 IIC 從機寫入數據或命令時,每個字節都需要從機產生應答信號以告訴主機數據或命令成功被寫入。所以,當 IIC 主機將 8 位的數據或命令傳出後,會將 SDA 信號設置爲輸入,等待從機應答(等待 SDA 被從機拉低爲低電平),若從機正確應答,表明當前數據或命令傳輸成功,可以結束或開始下一個命令/數據的傳輸,否則表明數據/命令寫入
失敗,主機就可以決定是否放棄寫入或者重新發起寫入。
IIC 器件地址
每個 IIC 器件都有一個器件地址,有的器件地址在出廠時地址就設置好了,用戶不可以更改(ov7670:0x42),有的確定了幾位,剩下幾位由硬件確定(比如有三位由用戶確定,就留有 3 個控制地址的引腳,最常見的爲 IIC 接口的 EEPROM 存儲器),此類較多;還有的有地址寄存器。
嚴格講,主機不是向從機發送地址,而是主機往總線上發送地址,所有的從機都能接收到主機發出的地址,然後每個從機都將主機發出的地址與自己的地址比較,如果匹配上了,這個從機就會向主機發出一個響應信號。主機收到響應信號後,開始向總線上發送數據,與這個從機的通訊就建立起來了。如果主機沒有收到響應信號,則表示尋址失敗。
通常情況下,主從器件的角色是確定的,也就是說從機一直工作在從機模式。不同的器件定義地址的方式是不同的,有的是軟件定義,有的是硬件定義。例如某些單片機的 IIC 接口作爲從機時,其器件地址是可以通過軟件修改從機地址寄存器確定的。而對於一些其他器件,如 CMOS 圖像傳感器、EEPROM 存儲器,其器件地址在出廠時就已經設定好了,具體值可以在對應的數據手冊中查到。
對於 AT24C64 這樣一顆 EEPROM 器件,其器件地址爲 1010 加 3 位的片選信號。3 位片選信號由硬件連接決定。例如 SOIC 封裝的該芯片 pin1、pin2、pin3 爲片選地址。當硬件電路上分別將這三個 pin 連接到 GND 或 VCC 時,就實現了設置不通的片選地址。IIC 協議在進行數據傳輸時,主機需要首先向總線上發出控制命令,其中,控制命令就包含了從機地址/片選信號+讀寫控制。然後等待從機響應。以下爲 IIC 控制命令傳輸的數據格式。
IIC 傳輸時,按照從高到低的位序進行傳輸。控制字節的最低位爲讀寫控制位,當該位爲 0 時表示主機對從機進行寫操作,當該位爲 1 時表示主機對從機進行讀操作。例如,當需要對片選地址爲 100 的 AT24LC64 發起寫操作,則控制字節應該爲 CtrlCode = 1010_100_0。
若要讀,則控制字節應該爲 CtrlCode = 1010_100_1。
IIC 存儲器地址
我們要對一個器件中的存儲單元(寄存器和存儲器以下簡稱存儲單元)進行讀寫,就必須要能夠指定存儲單元的地址。IIC 協議設計了有從機存儲單元尋址地址段,該地址段爲一個字節或兩個字節長度,在主機確認收到從機返回的控制字節響應後,由主機發出。地址段長度視不同的器件類型,長度不同
IIC 讀寫時序
IIC 單字節寫時序
1 字節地址段器件單字節寫時序
2 字節地址段器件單字節寫時序
從主機角度看一次寫入過程
a. 主機設置 SDA 爲輸出
b. 主機發起起始信號
c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。
d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
e. 讀取應答信號成功,傳輸 1 字節地址數據
f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
g. 對於兩字節地址段器件,傳輸地址數據低字節,對於 1 字節地址段器件,傳輸待寫入的數據
h. 設置 SDA 爲輸入三態,讀取從機應答信號。
i. 對於兩字節地址段器件,傳輸待寫入的數據(2 字節地址段器件可選)
j. 設置 SDA 爲輸入三態,讀取從機應答信號(2 字節地址段器件可選)。
k. 主機產生 STOP 位,終止傳輸
IIC 連續寫時序(頁寫時序)
1 字節地址段器件多字節寫時序
2 字節地址段器件多字節寫時序
從主機角度看一次寫入過程
a. 主機設置 SDA 爲輸出
b. 主機發起起始信號
c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。
d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
e. 讀取應答信號成功,傳輸 1 字節地址數據
f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
g. 對於兩字節地址段器件,傳輸低字節地址數據,對於 1 字節地址段器件,傳輸待寫
入的第一個數據
h. 設置 SDA 爲輸入三態,讀取從機應答信號。
i. 寫入待寫入的第 2 至第 n 個數據並讀取應答信號。對於 AT24Cxx,一次可寫入的最
大長度爲 32 字節。
j. 主機產生 STOP 位,終止傳輸。
IIC 單字節讀時序
1 字節地址段器件單節讀時序
2 字節地址段器件單節讀時序
從主機角度看一次讀取過程
a. 主機設置 SDA 爲輸出
b. 主機發起起始信號
c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。
d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
e. 讀取應答信號成功,傳輸 1 字節地址數據
f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
g. 對於兩字節地址段器件,傳輸低字節地址數據,對於 1 字節地址段器件,無此段數
據傳輸。
h. 主機發起起始信號
i. 主機傳輸器件地址字節,其中最低爲 1,表明爲寫操作。
j. 設置 SDA 爲輸入三態,讀取從機應答信號。
k. 讀取 SDA 總線上的一個字節的數據
l. 產生無應答信號(高電平)(無需設置爲輸出高點片,因爲總線會被自動拉高)
m. 主機產生 STOP 位,終止傳輸。
IIC 多字節連續讀時序(頁讀取)
1 字節地址段器件多字節讀時序
2 字節地址段器件多字節讀時序
從主機角度看一次讀取過程
a. 主機設置 SDA 爲輸出
b. 主機發起起始信號
c. 主機傳輸器件地址字節,其中最低爲 0,表明爲寫操作。
d. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
e. 讀取應答信號成功,傳輸 1 字節地址數據
f. 主機設置 SDA 爲輸入三態,讀取從機應答信號。
g. 對於兩字節地址段器件,傳輸低字節地址數據,對於 1 字節地址段器件,無此段數據傳輸。
h. 主機發起起始信號
i. 主機傳輸器件地址字節,其中最低爲 1,表明爲寫操作。
j. 設置 SDA 爲輸入三態,讀取從機應答信號。
k. 讀取 SDA 總線上的 n 個字節的數據(對於 AT24Cxx,一次讀取長度最大爲 32 字節)
l. 產生無應答信號(高電平)(無需設置爲輸出高點片,因爲總線會被自動拉高)主機產生 STOP 位,終止傳輸
EEPROM 讀寫控制程序設計
EEPROM 存儲器芯片的型號爲 AT24C64,其存儲器容量爲 64kbit,器件
片選地址有 3 位,A2、A1、A0。數據存儲地址是 13 位,屬於 2 字節地址段器件。
根據上面 IIC 的基本概念中有關讀寫時 SDA 與 SCL 時序,不管對於從機還是主機 SDA上的每一位數據在 SCL 的高電平期間爲有效數據,在 SCL 的低電平期間是要改變的數據。
根據這個用 2 個標誌位對時鐘 SCL 的高電平和低電平進行標記,如下圖所示:scl_high 對SCL 高電平中間進行標誌,scl_low 對 SCL 低電平中間進行標誌。這個在具體的實現中也不難實現。
IIC 讀寫狀態機設計
SCL 時鐘總線以及其高低電平中間標誌位產生完成後其後就是 SDA 數據線的產生,這個需要根據具體的讀寫操作完成。這裏主要採用狀態機實現
module IIC_24LC64(
clk50M,
reset,
iic_en,
cs_bit,
address,
write,
write_data,
read,
read_data,
scl,
sda,
done
);
input clk50M; //系統時鐘 50MHz
input reset; //異步復位信號
input iic_en; //使能信號
input [2:0]cs_bit; //器件選擇地址
input [12:0]address; //13 位數據讀寫地址,24LC64 有 13 位數據存儲
地址
input write; //寫數據信號
input [7:0]write_data; //寫數據
input read; //讀數據信號
output reg[7:0]read_data; //讀數據
output reg scl; //IIC 時鐘信號
inout sda; //IIC 數據總線
output reg done; //一次 IIC 讀寫完成
parameter SYS_CLOCK = 50_000_000; //系統時鐘採用 50MHz
parameter SCL_CLOCK = 200_000; //scl 總線時鐘採用 200kHz
//狀態
parameter
Idle = 16'b0000_0000_0000_0001,
Wr_start = 16'b0000_0000_0000_0010,
Wr_ctrl = 16'b0000_0000_0000_0100,
Ack1 = 16'b0000_0000_0000_1000,
Wr_addr1 = 16'b0000_0000_0001_0000,
Ack2 = 16'b0000_0000_0010_0000,
Wr_addr2 = 16'b0000_0000_0100_0000,
Ack3 = 16'b0000_0000_1000_0000,
Wr_data = 16'b0000_0001_0000_0000,
Ack4 = 16'b0000_0010_0000_0000,
Rd_start = 16'b0000_0100_0000_0000,
Rd_ctrl = 16'b0000_1000_0000_0000,
Ack5 = 16'b0001_0000_0000_0000,
Rd_data = 16'b0010_0000_0000_0000,
Nack = 16'b0100_0000_0000_0000,
Stop = 16'b1000_0000_0000_0000;
//sda 數據總線控制位
reg sda_en;
//sda 數據輸出寄存器
reg sda_reg;
assign sda = sda_en ? sda_reg : 1'bz;
//狀態寄存器
reg [15:0]state;
//讀寫數據標誌位
reg W_flag;
reg R_flag;
//寫數據到 sda 總線緩存器
reg [7:0]sda_data_out;
reg [7:0]sda_data_in;
reg [3:0]bit_cnt;
reg [7:0]scl_cnt;
parameter SCL_CNT_M = SYS_CLOCK/SCL_CLOCK; //計數最大值
reg scl_cnt_state;
//產生 SCL 時鐘狀態標誌 scl_cnt_state,爲 1 表示 IIC 總線忙,爲 0 表示總線閒
always@(posedge clk50M or negedge reset)
begin
if(!reset)
scl_cnt_state <= 1'b0;
else if(iic_en)
scl_cnt_state <= 1'b1;
else if(done)
scl_cnt_state <= 1'b0;
else
scl_cnt_state <= scl_cnt_state;
end
//scl 時鐘總線產生計數器
always@(posedge clk50M or negedge reset)
begin
if(!reset)
scl_cnt <= 8'b0;
else if(scl_cnt_state)
begin
if(scl_cnt == SCL_CNT_M - 1)
scl_cnt <= 8'b0;
else
scl_cnt <= scl_cnt + 8'b1;
end
else
scl_cnt <= 8'b0;
end
//scl 時鐘總線產生
always@(posedge clk50M or negedge reset)
begin
if(!reset)
scl <= 1'b1;
else if(scl_cnt == (SCL_CNT_M>>1)-1)
scl <= 1'b0;
else if(scl_cnt == SCL_CNT_M - 1)
scl <= 1'b1;
else
scl <= scl;
end
//scl 時鐘電平中部標誌位
reg scl_high;
reg scl_low;
always@(posedge clk50M or negedge reset)
begin
if(!reset)
begin
scl_high <= 1'b0;
scl_low <= 1'b0;
end
else if(scl_cnt == (SCL_CNT_M>>2))
scl_high <= 1'b1;
else if(scl_cnt == (SCL_CNT_M>>1)+(SCL_CNT_M>>2))
scl_low <= 1'b1;
else
begin
scl_high <= 1'b0;
scl_low <= 1'b0;
end
end
//狀態機
always@(posedge clk50M or negedge reset)
begin
if(!reset)
begin
state <= Idle;
sda_en <= 1'b0;
sda_reg <= 1'b1;
W_flag <= 1'b0;
R_flag <= 1'b0;
done <= 1'b0;
end
else
case(state)
Idle:
begin
done <= 1'b0;
W_flag <= 1'b0;
R_flag <= 1'b0;
sda_en <= 1'b0;
sda_reg <= 1'b1;
if(iic_en && write) //使能 IIC 並且爲寫操作
begin
W_flag <= 1'b1; //寫標誌位置 1
sda_en <= 1'b1; //設置 SDA 爲輸出模式
sda_reg <= 1'b1; //SDA 輸出高電平
state <= Wr_start; //跳轉到起始狀態
end
else if(iic_en && read) //使能 IIC 並且爲讀操作
begin
R_flag <= 1'b1; //讀標誌位置 1
sda_en <= 1'b1; //設置 SDA 爲輸出模式
sda_reg <= 1'b1; //SDA 輸出高電平
state <= Wr_start; //跳轉到起始狀態
end
else
state <= Idle;
end
Wr_start:
begin
if(scl_high)
begin
sda_reg <= 1'b0;
state <= Wr_ctrl;
sda_data_out <= {4'b1010, cs_bit,1'b0};
bit_cnt <= 4'd8;
end
else
begin
sda_reg <= 1'b1;
state <= Wr_start;
end
end
Wr_ctrl: //寫控制字節 4'b1010+3 位片選地址+1 位寫控制
begin
if(scl_low)
begin
bit_cnt <= bit_cnt -4'b1;
sda_reg <= sda_data_out[7];
sda_data_out <= {sda_data_out[6:0],1'b0};
if(bit_cnt == 0)
begin
state <= Ack1;
sda_en <= 1'b0;
end
else
state <= Wr_ctrl;
end
else
state <= Wr_ctrl;
end
Ack1: //通過判斷 SDA 是否拉低來判斷是否有從機響應
begin
if(scl_high)
if(sda == 1'b0)
begin
state <= Wr_addr1;
sda_data_out <= {3'bxxx,address[12:8]};
bit_cnt <= 4'd8;
end
else
state <= Idle;
else
state <= Ack1;
end
Wr_addr1: //寫 2 字節地址中的高地址字節中的低五位
begin
if(scl_low)
begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt -4'b1;
sda_reg <= sda_data_out[7];
sda_data_out <= {sda_data_out[6:0],1'b0};
if(bit_cnt == 0)
begin
state <= Ack2;
sda_en <= 1'b0;
end
else
state <= Wr_addr1;
end
else
state <= Wr_addr1;
end
Ack2: //通過判斷 SDA 是否拉低來判斷是否有從機響應
begin
if(scl_high)
if(sda == 1'b0)
begin
state <= Wr_addr2;
sda_data_out <= address[7:0];
bit_cnt <= 4'd8;
end
else
state <= Idle;
else
state <= Ack2;
end
Wr_addr2: //寫 2 字節地址中的低地址字節
begin
if(scl_low)
begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt -4'b1;
sda_reg <= sda_data_out[7];
sda_data_out <= {sda_data_out[6:0],1'b0};
if(bit_cnt == 0)
begin
state <= Ack3;
sda_en <= 1'b0;
end
else
state <= Wr_addr2;
end
else
state <= Wr_addr2;
end
Ack3: //通過判斷 SDA 是否拉低來判斷是否有從機響應
begin
if(scl_high)
if(sda == 1'b0) //有響應就判斷是讀還是寫操作
begin
if(W_flag) //如果是寫數據操作,進入寫數據狀態
begin
sda_data_out <= write_data;
bit_cnt <= 4'd8;
state <= Wr_data;
end
else if(R_flag) //如果是讀數據操作,進入讀數據開始狀
態
begin
state <= Rd_start;
sda_reg <= 1'b1;
end
end
else
state <= Idle;
else
state <= Ack3;
end
Wr_data: //寫數據狀態,向 EEPROM 寫入數據
begin
if(scl_low)
begin
sda_en <= 1'b1;
bit_cnt <= bit_cnt -4'b1;
sda_reg <= sda_data_out[7];
sda_data_out <= {sda_data_out[6:0],1'b0};
if(bit_cnt == 0)
begin
state <= Ack4;
sda_en <= 1'b0;
end
else
state <= Wr_data;
end
else
state <= Wr_data;
end
Ack4: //通過判斷 SDA 是否拉低來判斷是否有從機響應
begin
if(scl_high)
if(sda == 1'b0) //有響應就進入停止狀態
begin
sda_reg <= 1'b0;
state <= Stop;
end
else
state <= Idle;
else
state <= Ack4;
end
Rd_start: //讀數據的開始操作
begin
if(scl_low)
begin
sda_en <= 1'b1;
end
else if(scl_high)
begin
sda_reg <= 1'b0;
state <= Rd_ctrl;
sda_data_out <= {4'b1010, cs_bit,1'b1};
bit_cnt <= 4'd8;
end
else
begin
sda_reg <= 1'b1;
state <= Rd_start;
end
end
Rd_ctrl: //寫控制字節 4'b1010+3 位片選地址+1 位讀控制
begin
if(scl_low)
begin
bit_cnt <= bit_cnt -4'b1;
sda_reg <= sda_data_out[7];
sda_data_out <= {sda_data_out[6:0],1'b0};
if(bit_cnt == 0)
begin
state <= Ack5;
sda_en <= 1'b0;
end
else
state <= Rd_ctrl;
end
else
state <= Rd_ctrl;
end
Ack5: //通過判斷 SDA 是否拉低來判斷是否有從機響應
begin
if(scl_high)
if(sda == 1'b0) //有響應就進入讀數據狀態
begin
state <= Rd_data;
sda_en <= 1'b0; //SDA 總線設置爲 3 態輸入
bit_cnt <= 4'd8;
end
else
state <= Idle;
else
state <= Ack5;
end
Rd_data: //讀數據狀態
begin
if(scl_high) //在時鐘高電平讀取數據
begin
sda_data_in <= {sda_data_in[6:0],sda};
bit_cnt <= bit_cnt - 4'd1;
state <= Rd_data;
end
else if(scl_low && bit_cnt == 0) //數據接收完成進入無應答
響應狀態
begin
state <= Nack;
end
else
state <= Rd_data;
end
Nack: //不做應答響應
begin
read_data <= sda_data_in;
if(scl_high)
begin
state <= Stop;
sda_reg <= 1'b0;
end
else
state <= Nack;
end
Stop: //停止操作,在時鐘高電平,SDA 上升沿
begin
if(scl_low)
begin
sda_en <= 1'b1;
end
else if(scl_high)
begin
sda_en <= 1'b1;
sda_reg <= 1'b1;
state <= Idle;
done <= 1'b1;
end
else
state <= Stop;
end
default:
begin
state <= Idle;
sda_en <= 1'b0;
sda_reg <= 1'b1;
W_flag <= 1'b0;
R_flag <= 1'b0;
done <= 1'b0;
end
endcase
end
endmodule