第十六章 IIC協議詳解+UART串口讀寫EEPROM


十六、IIC協議詳解+Uart串口讀寫EEPROM

本文由杭電網友曾凱峰根據小梅哥FPGA IIC協議基本概念公開課內容整理並最終編寫Verilog代碼實現使用串口讀寫EEPROM的功能。

以下為原文內容:

在看完小梅哥講解IIC總線基本概念后,就有種想躍躍欲試的想法,下面先復習下梅哥講解的IIC總線若干基本概念。以下基本概念均為小梅哥總結,我就直接拿過來供大家參考學習。

IIC基本特性

總線信號

SDA:串行數據線

SCL:串行數據時鍾

總線空閑狀態

SDA:高電平

SCL:高電平

IIC協議起始位

SCL為高電平時,SDA出現下降沿,產生一個起始位。

圖片1

IIC協議結束位

SCL為高電平時,SDA出現上升沿,產生一個結束位。

圖片2

IIC讀寫單字節時序

IIC主機對IIC從機寫入數據時,SDA上的每一位數據在SCL的高電平期間被寫入從機中。對於主機,在SCL的低電平期間改變要寫入的數據。

圖片3

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控制命令傳輸的數據格式。

圖片4

IIC傳輸時,按照從高到低的位序進行傳輸。控制字節的最低位為讀寫控制位,當該位為0時表示主機對從機進行寫操作,當該位為1時表示主機對從機進行讀操作。例如,當需要對片選地址為100的AT24LC64發起寫操作,則控制字節應該為CtrlCode = 1010_100_0。若要讀,則控制字節應該為CtrlCode = 1010_100_1。

IIC存儲器地址

每個支持IIC協議的器件,內部總會有一些可供讀寫的寄存器或存儲器,例如,對於我們用到的EEPROM存儲器,內部就是順序編址的一系列存儲單元。對於我們常接觸的CMOS攝像頭如OV7670(OV7670的該接口叫SCCB接口,其實質也是一種特殊的IIC協議,可以直接兼容IIC協議),其內部就是一系列編址的可供讀寫的寄存器。因此,我們要對一個器件中的存儲單元(寄存器和存儲器以下簡稱存儲單元)進行讀寫,就必須要能夠指定存儲單元的地址。IIC協議設計了有從機存儲單元尋址地址段,該地址段為一個字節或兩個字節長度,在主機確認收到從機返回的控制字節響應后,由主機發出。地址段長度視不同的器件類型,長度不同,例如同是EEPROM存儲器,AT24C04的址段長度為一個字節,而AT24C64的地址段長度為兩個字節。具體是一個字節還是兩個字節,與器件的存儲單元數量有關。

AT24C01地址段:

圖片5

AT24C64地址段:

圖片6

IIC讀寫時序

IIC單字節寫時序

1字節地址段器件單字節寫時序

圖片7

2字節地址段器件單字節寫時序

圖片8

從主機角度看一次寫入過程

a. 主機設置SDA為輸出

b. 主機發起起始信號

c. 主機傳輸器件地址字節,其中最低為0,表明為寫操作。

d. 主機設置SDA為輸入三態,讀取從機應答信號。

e. 讀取應答信號成功,傳輸1字節地址數據

f. 主機設置SDA為輸入三態,讀取從機應答信號。

g. 對於兩字節地址段器件,傳輸地址數據低字節,對於1字節地址段器件,傳輸待寫入的數據

h. 設置SDA為輸入三態,讀取從機應答信號。

i. 對於兩字節地址段器件,傳輸待寫入的數據(2字節地址段器件可選)

j. 設置SDA為輸入三態,讀取從機應答信號(2字節地址段器件可選)。

k. 主機產生STOP位,終止傳輸。

IIC連續寫時序(頁寫時序)

注:IIC連續寫時序僅部分器件支持。

1字節地址段器件多字節寫時序

圖片9

2字節地址段器件多字節寫時序

圖片10

從主機角度看一次寫入過程

a. 主機設置SDA為輸出

b. 主機發起起始信號

c. 主機傳輸器件地址字節,其中最低為0,表明為寫操作。

d. 主機設置SDA為輸入三態,讀取從機應答信號。

e. 讀取應答信號成功,傳輸1字節地址數據

f. 主機設置SDA為輸入三態,讀取從機應答信號。

g. 對於兩字節地址段器件,傳輸低字節地址數據,對於1字節地址段器件,傳輸待寫入的第一個數據

h. 設置SDA為輸入三態,讀取從機應答信號。

i. 寫入待寫入的第2至第n個數據並讀取應答信號。對於AT24Cxx,一次可寫入的最大長度為32字節。

j. 主機產生STOP位,終止傳輸。

IIC單字節讀時序

1字節地址段器件單節讀時序

圖片11

2字節地址段器件單節讀時序

圖片12

從主機角度看一次讀取過程

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字節地址段器件多字節讀時序

圖片13

2字節地址段器件多字節讀時序

圖片14

從主機角度看一次讀取過程

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芯片編寫IIC協議讀寫EEPROM里的數據,首先查看小梅哥開發板中EEPROM存儲器芯片的型號為AT24C64,其存儲器容量為64kbit,器件片選地址有3位,A2、A1、A0。數據存儲地址是13位,屬於2字節地址段器件。

圖片15

這里EEPROM讀寫控制模塊的設計主要是針對讀寫EEPROM單字節數據,總的來說就是2字節地址段器件單節讀寫控制模塊的設計。有關多字節的數據的讀寫讀者可以自己學習設計。

根據上面IIC的基本概念中有關讀寫時SDA與SCL時序,不管對於從機還是主機SDA上的每一位數據在SCL的高電平期間為有效數據,在SCL的低電平期間是要改變的數據。根據這個用2個標志位對時鍾SCL的高電平和低電平進行標記,如下圖所示:scl_high對SCL高電平中間進行標志,scl_low對SCL低電平中間進行標志。這個在具體的實現中也不難實現。

圖片16

SCL時鍾設計

首先考慮到SCL最大時鍾周期為400kHz,這里SCL就實現周期為200kHz的時鍾,這個具體讀者可以做修改,系統時鍾直接采用小梅哥開發板的50MHz外部時鍾,采用計數器方法產生SCL,這樣計數器最大計數值SCL_CNT_M = 500_000_000/200_000 - 1 =249。只需在計數到最大值一半值和最大值進行翻轉SCL就可實現,具體實現代碼如下:

 

    parameter SYS_CLOCK = 50_000_000;  //系統時鍾采用50MHz
parameter SCL_CLOCK = 200_000;     //scl總線時鍾采用200kHz

    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

上述代碼中iic_en信號為IIC通信使能信號,done信號為一次IIC讀/寫數據完成標志位。在此基礎上,對SCL高電平中間標志位scl_high和低電平標志位scl_low就很容易實現,只需在計數到四分之一和四分之三時分別置1就能得到。具體代碼如下:

//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

 

 

IIC讀寫狀態機設計

SCL時鍾總線以及其高低電平中間標志位產生完成后其后就是SDA數據線的產生,這個需要根據具體的讀寫操作完成。這里主要采用狀態機實現,根據上面IIC基本概念中2字節地址段器件單字節讀和寫的過程設計大致的狀態機如下,具體的有些條件和輸出等沒有寫。這是按照小梅哥講解IIC讀寫過程想到的,對於實現單字節的讀寫是沒有問題,但對於讀寫多字節還需要做修改,這個后續再做優化。

圖片19

整個模塊的具體實現代碼如下:

//模塊文件名:IIC.v
//模塊功能:實現IIC總線協議控制
//時間:2016.11.2
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 

IIC讀寫狀態機仿

仿真驗證需要編寫一個模塊模擬EEPROM的存儲器,這里仿真片選地址就省掉不做考慮。模擬EEPROM的存儲器的主要功能是接收存儲器數據地址,根據開始信號的次數(主機寫數據有一次開始信號,主機讀數據有2次開始信號)判斷是讀還是寫數據,進而做出數據的輸出/接收。這個仿真模型我是借用夏宇聞Verilog數字系統設計教程一書中的代碼稍作修改以滿足AT24C64芯片2字節地址段的情況。

`timescale 1ns/1ns
`define timeslice 1250

module EEPROM_AT24C64(
    scl, 
    sda
);
    input scl;               //串行時鍾線
    inout sda;               //串行數據線
    
    reg out_flag;            //SDA數據輸出的控制信號
    
    reg[7:0] memory[8191:0]; //數組模擬存儲器
    reg[12:0]address;        //地址總線
    reg[7:0]memory_buf;      //數據輸入輸出寄存器
    reg[7:0]sda_buf;         //SDA數據輸出寄存器
    reg[7:0]shift;           //SDA數據輸入寄存器
    reg[7:0]addr_byte_h;     //EEPROM存儲單元地址高字節寄存器
    reg[7:0]addr_byte_l;     //EEPROM存儲單元地址低字節寄存器
    reg[7:0]ctrl_byte;       //控制字寄存器
    reg[1:0]State;           //狀態寄存器
    
    integer i;
    
    //---------------------------
    parameter  
        r7 = 8'b1010_1111,  w7 = 8'b1010_1110,   //main7
        r6 = 8'b1010_1101,  w6 = 8'b1010_1100,   //main6
        r5 = 8'b1010_1011,  w5 = 8'b1010_1010,   //main5
        r4 = 8'b1010_1001,  w4 = 8'b1010_1000,   //main4
        r3 = 8'b1010_0111,  w3 = 8'b1010_0110,   //main3
        r2 = 8'b1010_0101,  w2 = 8'b1010_0100,   //main2
        r1 = 8'b1010_0011,  w1 = 8'b1010_0010,   //main1
        r0 = 8'b1010_0001,  w0 = 8'b1010_0000;   //main0    
    //---------------------------
    
    assign sda = (out_flag == 1) ? sda_buf[7] : 1'bz;
    
    //------------寄存器和存儲器初始化---------------
    initial
    begin
        addr_byte_h    = 0;
        addr_byte_l    = 0;
        ctrl_byte    = 0;
        out_flag     = 0;
        sda_buf      = 0;
        State        = 2'b00;
        memory_buf   = 0;
        address      = 0;
        shift        = 0;
        
        for(i=0;i<=8191;i=i+1)
            memory[i] = 0;  
    end

    //啟動信號
    always@(negedge sda)
    begin
        if(scl == 1)
        begin
            State = State + 1;
            if(State == 2'b11)
                disable write_to_eeprom;
        end 
    end
    
    //主狀態機
    always@(posedge sda)
    begin
        if(scl == 1)                //停止操作
            stop_W_R;
        else
        begin
            casex(State)
                2'b01:begin
                    read_in;
                    if(ctrl_byte == w7 || ctrl_byte == w6 
                        || ctrl_byte == w5  || ctrl_byte == w4
                        || ctrl_byte == w3  || ctrl_byte == w2
                        || ctrl_byte == w1  || ctrl_byte == w0)
                    begin
                        State = 2'b10;
                        write_to_eeprom;    //寫操作                 
                    end
                    else
                        State = 2'b00;          
                end
                
                2'b11:
                    read_from_eeprom;               
                
                default:
                    State = 2'b00;          
            endcase     
        end 
    end     //主狀態機結束
    
    //操作停止
    task stop_W_R;
    begin
        State        = 2'b00;
        addr_byte_h  = 0;
        addr_byte_l  = 0;
        ctrl_byte    = 0;
        out_flag     = 0;
        sda_buf      = 0;   
    end
    endtask
    
    //讀進控制字和存儲單元地址
    task read_in;
    begin
        shift_in(ctrl_byte);
        shift_in(addr_byte_h);
        shift_in(addr_byte_l);      
    end 
    endtask
    
    //EEPROM的寫操作
    task write_to_eeprom;
    begin
        shift_in(memory_buf);
        address = {addr_byte_h[4:0], addr_byte_l};
        memory[address] = memory_buf;       
        State = 2'b00;
    end
    endtask
    
    //EEPROM的讀操作
    task read_from_eeprom;
    begin
        shift_in(ctrl_byte);
        if(ctrl_byte == r7 || ctrl_byte == w6 
            || ctrl_byte == r5  || ctrl_byte == r4
            || ctrl_byte == r3  || ctrl_byte == r2
            || ctrl_byte == r1  || ctrl_byte == r0)
        begin
            address = {addr_byte_h[4:0], addr_byte_l};
            sda_buf = memory[address];
            shift_out;
            State = 2'b00;
        end
    end
    endtask
    
    //SDA數據線上的數據存入寄存器,數據在SCL的高電平有效
    task shift_in;  
        output[7:0]shift;
        begin
            @(posedge scl) shift[7] = sda;
            @(posedge scl) shift[6] = sda;
            @(posedge scl) shift[5] = sda;
            @(posedge scl) shift[4] = sda;
            @(posedge scl) shift[3] = sda;
            @(posedge scl) shift[2] = sda;
            @(posedge scl) shift[1] = sda;
            @(posedge scl) shift[0] = sda;
            
            @(negedge scl)
            begin
                #`timeslice;
                out_flag = 1;     //應答信號輸出
                sda_buf = 0;
            end
            
            @(negedge scl)
            begin
                #`timeslice;
                out_flag = 0;               
            end         
        end 
    endtask
    
    //EEPROM存儲器中的數據通過SDA數據線輸出,數據在SCL低電平時變化
    task shift_out;
    begin
        out_flag = 1;
        for(i=6; i>=0; i=i-1)
        begin
            @(negedge scl);
            #`timeslice;
            sda_buf = sda_buf << 1;         
        end
        @(negedge scl) #`timeslice sda_buf[7] = 1;    //非應答信號輸出
        @(negedge scl) #`timeslice out_flag = 0;
    end
    endtask

endmodule 
//eeprom.v文件結束

IIC讀寫狀態機仿真測試頂層設計

仿真驗證的框圖如下,將EEPROM讀寫控制模塊與EEPROM模型相連接就行了。

圖片22

代碼如下:

`timescale 1ns/1ns
`define clk_period 20

module IIC_AT24C64_tb;

    reg clk50M;
    reg reset;
    reg iic_en;
    reg [12:0]address;
    reg write;
    reg [7:0]write_data;
    reg read;

    wire [7:0]read_data;
    wire scl;
    wire sda;
    wire done;
    
    integer i;
    
    IIC_AT24C64 IIC_AT24C64(
        .clk50M(clk50M),
        .reset(reset),
        .iic_en(iic_en),
        .cs_bit(3'b001),
        .address(address),
        .write(write),
        .write_data(write_data),
        .read(read),
        .read_data(read_data),
        .scl(scl),
        .sda(sda),
        .done(done)
    );

    EEPROM_AT24C64 EEPROM(
        .scl(scl), 
        .sda(sda)
    );
    
    initial clk50M = 1'b1;
    always #(`clk_period/2)clk50M = ~clk50M;
    
    initial
    begin
        reset      = 1'b0;
        iic_en     = 1'b0;
        address    = 13'h0;
        write      = 1'b0;
        write_data = 1'b0;
        read       = 1'b0;
        
        #(`clk_period*200 + 1)
        reset      = 1'b1;
        #200;
        
        //寫數據,寫200個數據
        write  = 1'b1;
        address = 200;
        write_data = 200;
        iic_en = 1'b1;      
        #(`clk_period)
        iic_en = 1'b0;
        
        for(i=199;i>0;i=i-1)
        begin
            @(posedge done);
            #2000;
            address = i;
            write_data = i;
            iic_en = 1'b1;
            #(`clk_period)
            iic_en = 1'b0;
        end     
        @(posedge done);
        #2000;
        write  = 1'b0;
        #5000;
        
        //讀數據,讀取寫入的200個數據
        read  = 1'b1;
        iic_en = 1'b1;
        #(`clk_period)
        iic_en = 1'b0;      
        
        for(i=200;i>0;i=i-1)
        begin
            @(posedge done);
            #2000;
            address = i;            
            iic_en = 1'b1;
            #(`clk_period)
            iic_en = 1'b0;
        end     
        @(posedge done);
        #20000;
        read  = 1'b0;
        #5000;
        
        $stop;
    end

endmodule 

IIC讀寫邏輯仿真結果分析

仿真驗證主要是對模擬的EEPROM存儲器器寫入200個數據,然后讀出寫入的200個數據,觀察波形情況及讀寫是否和預期一樣。

仿真波形如下:

寫數據和讀數據總體情況波形如下:

圖片24

向EEPROM寫數據波形圖如下:

圖片25

讀取EEPROM數據波形如下:

圖片26

由波形可以看出我們的設計是滿足要求的。

串口讀寫EEPROM應用系統設計

接下來就是設計通過串口發送地址和寫入數據,通過一定的數據處理后對EEPROM進行讀寫操作。設計的框圖如下:

圖片27

讀寫控制位是通過一個按鍵來切換的,EEPROM讀寫使能也是通過一個按鍵來使能的,前期為了方便測試就是按鍵按下一次進行一次讀寫,在調試完成后可以將這個按鍵去掉,通過發送的數據進行一定的判斷后使能。按鍵和串口收發模塊的代碼都是學習小梅哥課程上的,在芯航線FPGA數字系統設計教程+實例解析教程上有詳細的設計過程。

數據轉發模塊設計

下面主要是數據轉換模塊的設計。前面EEPROM設計的是2字節地址段單字節讀寫,串口發送一組數據包括2個字節地址和1字節寫數據(如果是讀操作該字節可以為任意數),就相當於串口每次讀寫EEPROM時發送3個字節,數據處理模塊將接收的3個字節數賦值個讀寫地址和寫數據(讀數據時第個字節可以為任意數)。具體實現代碼如下:

module DATA_TRF(
    clk50M,
    reset,
    rx_data,
    rx_done,
    tx_data,
    tx_en,
    iic_wr,
    address,
    write_data,
    read_data,
    done    
);

    input clk50M;               //系統時鍾
    input reset;                //系統復位
    input [7:0]rx_data;         //從串口接收數據
    input rx_done;              //串口接收完成標志位
    output [7:0]tx_data;        //串口發送數據
    output tx_en;               //串口發送數據使能
    input iic_wr;               //IIC讀寫控制位,1為寫,0為讀
    output reg [12:0]address;   //IIC讀寫數據地址
    output reg [7:0]write_data; //IIC寫操作數據
    input [7:0]read_data;       //IIC讀操作讀取數據
    input done;                 //IIC一次讀寫完成標志位
    
    
    
    //串口接收數據寄存器
    reg [23:0] rx_data_buf;
    
    //串口接收字節數
    reg [1:0]rx_bit_cnt;
    
    always@(posedge clk50M or negedge reset)
    begin
        if(!reset)
            rx_bit_cnt <= 2'b0;
        else if(done)
            rx_bit_cnt <= 2'b0;
        else if(rx_done)
            rx_bit_cnt <= rx_bit_cnt + 2'b1;
        else
            rx_bit_cnt <= rx_bit_cnt;       
    end
    
    //串口接收數據寄存器
    always@(posedge clk50M or negedge reset)
    begin   
        if(!reset)
            rx_data_buf <= 24'h0;
        else if(rx_done)
            rx_data_buf <= {rx_data_buf[15:0],rx_data};
        else
            rx_data_buf <= rx_data_buf;
    end
    
    //接收的串口寄存器賦給IIC地址和寫數據
    always@(posedge clk50M or negedge reset)
    begin
        if(!reset)
        begin
            address <= 13'd0;
            write_data <= 8'd0;
        end
        else if(rx_bit_cnt == 2'd3)
        begin
            address <= rx_data_buf[20:8];
            write_data <= rx_data_buf[7:0];
        end
        else
        begin
            address <= address;
            write_data <= write_data;
        end
    end
    
    //讀取IIC數據,並通過串口發送出去
    assign tx_en = (iic_wr == 1'b0) && done;

    assign tx_data = read_data;
    
endmodule 

 

 

大致的思路是,,串口接收數據存入一個3字節緩存器中,,當接受到3個字節后就將緩存器中的數據賦值給EEPROM讀寫控制模塊的地址address和寫數據write_data。這樣再通過外部的按鍵使能這次的讀/寫操作就可完成一次讀/寫操作。

數據轉發模塊仿真文件設計

下面對數據轉換模塊進行仿真驗證,仿真利用上面已經編寫和的EEPROM讀寫控制模塊和EEPROM模塊進行仿真,代碼如下:

`timescale 1ns/1ns
`define clk_period 20

module DATA_TRF_tb;

    reg clk50M;          //系統時鍾
    reg reset;           //系統復位信號
    reg [7:0]rx_data;    //串口接收數據
    reg rx_done;         //串口接收完成標志位
    wire [7:0]tx_data;   //串口發送數據
    wire tx_en;          //串口發送完成標志位
    reg iic_wr;          //IIC讀寫控制
    wire [12:0]address;  //IIC讀寫地址 
    wire [7:0]write_data;//IIC寫數據
    wire [7:0]read_data; //IIC讀數據
    wire done;           //IIC一次讀寫數據完成標志位
    
    reg iic_en;
    wire scl;
    wire sda;
    
    integer i;

    //數據轉換模塊例化
    DATA_TRF DATA_TRF(
        .clk50M(clk50M),
        .reset(reset),
        .rx_data(rx_data),
        .rx_done(rx_done),
        .tx_data(tx_data),
        .tx_en(tx_en),
        .iic_wr(iic_wr),
        .address(address),
        .write_data(write_data),
        .read_data(read_data),
        .done(done)
    );
    
    //EEPROM讀寫控制模塊例化
    IIC_AT24C64 IIC_AT24C64(
        .clk50M(clk50M),
        .reset(reset),
        .iic_en(iic_en),
        .cs_bit(3'b001),
        .address(address),
        .write(iic_wr),
        .write_data(write_data),
        .read(~iic_wr),
        .read_data(read_data),
        .scl(scl),
        .sda(sda),
        .done(done)
    );

    //EEPROM模塊例化
    EEPROM_AT24C64 EEPROM(
        .scl(scl), 
        .sda(sda)
    );
    
    initial clk50M = 1'b1;
    always #(`clk_period/2) clk50M = ~clk50M;
    
    initial
begin
    //初始化
        reset = 1'b0;
        rx_data = 8'h0;
        rx_done = 1'b0;
        iic_wr = 1'b0;
        iic_en = 1'b0;
        
        #(`clk_period*200 + 1)
        reset = 1'b1;
        #2000;      
        
        //寫數據,寫入200個數據
        iic_wr = 1'b1;
        
        rx({16'd200,8'd200},3);
        #2000;
        iic_en = 1'b1;      
        #(`clk_period)
        iic_en = 1'b0;      
        
        for(i=199;i>0;i=i-1)
        begin
            @(posedge done);
            #2000;
            rx({i[15:0],i[7:0]},3);
            #2000;
            iic_en = 1'b1;
            #(`clk_period)
            iic_en = 1'b0;
        end
        
        @(posedge done);
        #2000;
    
        //讀數據,讀取寫入的200個數據
        iic_wr = 1'b0;      
        rx({16'd200,8'd200},3);
        #2000;
        iic_en = 1'b1;      
        #(`clk_period)
        iic_en = 1'b0;
        
        for(i=199;i>0;i=i-1)
        begin
            @(posedge done);
            #2000;
            rx({i[15:0],i[7:0]},3);
            #2000;
            iic_en = 1'b1;
            #(`clk_period)
            iic_en = 1'b0;
        end
        @(posedge done);
        #2000;
        $stop;  
    end
    
    //模擬串口一次發送多個字節的情況
    task rx;
    input [23:0]data;
    input [1:0] data_cnt;   
    
    repeat(data_cnt)
    begin
        rx_data = data[23:16];
        rx_done = 1'b1;
        #(`clk_period)
        rx_done = 1'b0;
        #2000;
        data = data << 8;       
    end 
    endtask

endmodule

數據轉發模塊仿真結果分析

仿真波形如下:

寫數據和讀數據總的波形圖如下:

圖片30

寫數據波形圖如下:

圖片31

讀數據波形如下:

圖片32

觀察波形,與預期效果一致。

系統頂層文件設計

下面就是頂層文件的設計,根據上面的框圖很容易寫出頂層文件,代碼如下:

module IIC_top(
    clk50M,
    reset,
    key_iic_en,
    key_iic_wr,
    rs232_rx,
    rs232_tx,

    scl,
    sda,
    led
);

    input  clk50M;       //系統時鍾
    input  reset;        //系統復位
    input  key_iic_en;   //IIC讀寫使能按鍵控制
    input  key_iic_wr;   //IIC讀寫控制
    input  rs232_rx;     //串口接收
    output rs232_tx;     //串口發送

    output scl;          //IIC時鍾總線
    inout  sda;          //IIC數據總線
    output [1:0]led;     //LED讀寫控制指示燈,亮為寫,滅為讀

    
    wire key_flag_en;
    wire key_state_en;
    wire key_flag_wr;
    wire key_state_wr;
    wire iic_en;
    reg  iic_wr;
    wire [7:0]rx_data;
    wire rx_done;
    wire[7:0]tx_data;
    wire tx_en;
    wire [12:0]address; 
    wire [7:0]write_data;   
    wire [7:0]read_data;
    wire done;
    
    //按鍵控制IIC讀寫使能
    key_filter key_en(
        .Clk(clk50M), 
        .Rst_n(reset), 
        .key_in(key_iic_en), 
        .key_flag(key_flag_en), 
        .key_state(key_state_en)
    );
    
    //IIC讀寫使能信號
    assign iic_en = key_flag_en && (~key_state_en);
    assign led[1] = key_state_en;
    
    //按鍵切換讀寫控制
    key_filter key_wr(
        .Clk(clk50M), 
        .Rst_n(reset), 
        .key_in(key_iic_wr), 
        .key_flag(key_flag_wr), 
        .key_state(key_state_wr)
    );
    
    //按鍵按一次切換一次,初始默認寫
    always@(posedge clk50M or negedge reset)
    begin
        if(!reset)
            iic_wr <= 1'b1;
        else if(key_flag_wr && (~key_state_wr))
            iic_wr <= ~iic_wr;
        else
            iic_wr <= iic_wr;
    end
                        
    //LED指示讀寫控制,亮代表寫操作,滅代表讀操作                   
    assign led[0] = ~iic_wr;

    //數據轉換模塊的例化
    DATA_TRF DATA_TRF(
        .clk50M(clk50M),
        .reset(reset),
        .rx_data(rx_data),
        .rx_done(rx_done),
        .tx_data(tx_data),
        .tx_en(tx_en),
        .iic_wr(iic_wr),
        .address(address),
        .write_data(write_data),
        .read_data(read_data),
        .done(done)
    );
    
    //IIC讀寫控制模塊的例化
    IIC_AT24C64 IIC_AT24C64(
        .clk50M(clk50M),
        .reset(reset),
        .iic_en(iic_en),
        .cs_bit(3'b001),
        .address(address),
        .write(iic_wr),
        .write_data(write_data),
        .read(~iic_wr),
        .read_data(read_data),
        .scl(scl),
        .sda(sda),
        .done(done)
    );
    
    //串口發送模塊
    uart_byte_tx uart_byte_tx(
        .Clk(clk50M), 
        .Rst_n(reset), 
        .send_en(tx_en),
        .baud_set(3'b0),      //波特率9600
        .Data_Byte(tx_data),

        .Rs232_Tx(rs232_tx),
        .Tx_Done(),
        .uart_state()
    );  
    
    //串口接收模塊
    uart_byte_rx uart_byte_rx(
        .Clk(clk50M),
        .Rst_n(reset),
        .Rs232_rx(rs232_rx),
        .baud_set(3'b0),     //波特率9600

        .Data_Byte(rx_data),
        .Rx_Done(rx_done)
    );
    
endmodule

 

 

在頂層文設計中增加了兩個LED,LED0用指示當前的讀/寫操作狀態,亮表示寫EEPROM操作狀態,滅表示讀EEPROM數據操作狀態;LED1表示EEPROM讀/寫使能按鍵按下指示,按鍵按下燈亮,按鍵松開燈滅。完成后進行引腳分配,編譯,板級下載后測試。

系統測試

測試如下:

初始默認狀態下是寫EEPROM操作(通過LED0亮可以觀察),此時,用串口工具發送十六進制數00 00 56 ,然后按下引腳key_icc_en對應的按鍵key0,使能EEPROM讀寫操作,完成了一次寫操作。可以多次使用同樣的方法寫入不同地址中的數據。這里我再寫入2組數據,分別為00 AB 39 、00 B1 AB 。寫操作完成后按下引腳key_icc_wr對應的按鍵key1,此時LED0滅,表示可以進行讀操作。這里我們依次對上面寫入數據的地址進行讀操作,同樣在串口發送個字節,前兩個字節中低13位表示的地址,第3個字節可以為任意數都行(這里具體可以看代碼理解,讀者也可以想其他方法實現),串口工具依次發送00 00 12 后按下按鍵key0;發送00 AB 23  后按下按鍵key0;發送00 B1 AB 后按下按鍵key0;每次按下按鍵會接收到讀出的數據,如下圖所示:

圖片34

這里串口每次發送3個字節,然后按一下使能按鍵key0,完成一次讀寫操作,寫操作中第3個字節為寫入的數據,在讀操作中,第3個字節是可以為任意值,但是不能省掉,要省掉可以更改設計進行優化,讀者可以自己尋找優化方法。至於每次按鍵一次才進行一次讀寫操作有點麻煩,讀者也可以進行修改優化。本次的是除了用modelsim仿真驗證以外,還用到了SignalTap II Logic Analyzer工具對波形進行了抓取分析

如有更多問題,歡迎加入芯航線 FPGA 技術支持群交流學習:472607506

小梅哥

芯航線電子工作室

關於學習資料,小梅哥系列所有能夠開放的資料和更新(包括視頻教程,程序代碼,教程文檔,工具軟件,開發板資料)都會發布在我的雲分享。(記得訂閱)鏈接:http://yun.baidu.com/share/home?uk=402885837&view=share#category/type=0

贈送芯航線AC6102型開發板配套資料預覽版下載鏈接:鏈接:http://pan.baidu.com/s/1slW2Ojj 密碼:9fn3

贈送SOPC公開課鏈接和FPGA進階視頻教程。鏈接:http://pan.baidu.com/s/1bEzaFW 密碼:rsyh


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM