更多文章内容,請關注微信公衆号“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