# 期望通過(guò)本文掌握I2C模塊的FPGA和軟件設(shè)計(jì)和實(shí)踐 #
1、I2C(Inter-Integrated Circuit)通信協(xié)議及FPGA模塊設(shè)計(jì)
I2C 是很常見(jiàn)的一種總線協(xié)議,使用兩條線在主控制器和從機(jī)之間進(jìn)行數(shù)據(jù)通信。一條是 SCL(串行時(shí)鐘線),另外一條是 SDA(串行數(shù)據(jù)線)。這兩條線都需要接上拉電 阻。因?yàn)閮H有一根數(shù)據(jù)線,所以I2C通信是半雙工的。
I2C 總線有標(biāo)準(zhǔn)模式(100kb/s)和快速模式(400kb/s)兩種。
“總線”指多個(gè)設(shè)備共用的信號(hào)線。在一個(gè)I2C總線中,支持多個(gè)從設(shè)備。不同的從設(shè) 備有不同的器件地址,這樣 I2C 主控制器就可以通過(guò) I2C 設(shè)備的器件地址訪問(wèn)指定的 I2C 設(shè)備了,一個(gè)I2C總線連接多個(gè) I2C 設(shè)備,如下所示,
I2C協(xié)議基本術(shù)語(yǔ)
起始信號(hào): I2C 通信起始標(biāo)志。主機(jī)告訴從機(jī),要開(kāi)始進(jìn)行 I2C 通信了。在 SCL 為高電平期間, SDA 出現(xiàn)下降沿就表示產(chǎn)生起始信號(hào)。起始信號(hào)產(chǎn)生后總線處于占用狀態(tài)。
停止信號(hào):I2C 停止通信的標(biāo)志。在 SCL 為高電平期間, SDA 出現(xiàn)上升沿就表示為停止信號(hào)。停止信號(hào)產(chǎn)生后總線被釋放,處于空閑狀態(tài)。
數(shù)據(jù)傳輸:I2C 總線在進(jìn)行數(shù)據(jù)傳輸時(shí)要保證在 SCL 高電平期間, SDA 上的數(shù)據(jù)穩(wěn)定,因此 SDA 上的數(shù)據(jù)變化只能在 SCL 低電平期間發(fā)生。
數(shù)據(jù)傳送時(shí),先傳送最高位,后傳送低位。
應(yīng)答信號(hào):當(dāng) I2C 主機(jī)發(fā)送完 8bit 數(shù)據(jù)后會(huì)將 SDA 設(shè)置為輸入狀態(tài),等待 I2C
從機(jī)應(yīng)答,也就是等到 I2C 從機(jī)告訴主機(jī)它接收到 8bit 數(shù)據(jù)了。
應(yīng)答信號(hào)是由從機(jī)發(fā)出的,主機(jī)需要提供應(yīng)答信號(hào)所需的時(shí)鐘。主機(jī)發(fā)送完數(shù)據(jù)以后的下一個(gè)時(shí)鐘信號(hào)就是給應(yīng)答信號(hào)使用的。從機(jī)通過(guò)將 SDA 拉低來(lái)表示發(fā)出應(yīng)答信號(hào)(ACK ,低有效),表示通信成功,否則表示通信失敗(NACK)。
設(shè)計(jì)一個(gè) I2C 模塊,要求如下所示,
-
- 支持總線仲裁丟失檢測(cè);
- 支持總線忙狀態(tài)檢測(cè);
- 支持不同的 I2C 通信模式:
- 標(biāo)準(zhǔn)模式(100kHz);
- 快速模式(400kHz);
- 支持產(chǎn)生起始、終止、重復(fù)起始和應(yīng)答信息;
- 支持起始、終止和重復(fù)起始檢測(cè);
- 支持 7 位尋址模式;
- 支持中斷;
主要包括 8 個(gè) 8 位寬的寄存器,如下所示
-
- I2C 分頻值低字節(jié)寄存器 I2C 分頻值高字節(jié)寄存器 I2C 控制寄存器
- I2C 發(fā)送數(shù)據(jù)寄存器
- I2C 接受數(shù)據(jù)寄存器
- I2C 命令寄存器
- I2C 狀態(tài)寄存器
- I2C 總線死鎖時(shí)間寄存器
根據(jù) I2C 協(xié)議,我們需要設(shè)計(jì)一個(gè)狀態(tài)機(jī),對(duì)應(yīng) I2C 的通信過(guò)程,各個(gè)狀態(tài)如下所示,
根據(jù) I2C 寄存器的配置以及狀態(tài)完成標(biāo)志進(jìn)行上述狀態(tài)的切換,如下所示(僅展示部分代碼),
每個(gè)狀態(tài)都需要持續(xù)多個(gè)周期,所以針對(duì)每個(gè)狀態(tài)細(xì)分了幾個(gè)子狀態(tài),如下所示,
I2C 有 2 個(gè)外部接口,分別是 I2C_SCL I2C_SDA 。對(duì)于 I2C_SCL ,這里我們只設(shè)計(jì) I2C 主模式,所以對(duì)于 FPGA 來(lái)說(shuō), I2C_SCL 始終是輸出。對(duì)于 I2C_SDA ,在主模式下,在接收響應(yīng)的狀態(tài)下是輸入,其他情況下為輸出。為了便于接口管理,我們將 I2C_SCL I2C_SDA 都設(shè)計(jì)為 IOBUF 。如下所示,
主機(jī)讀寫(xiě)數(shù)據(jù)的時(shí)序操作如下所示,
主機(jī)寫(xiě)數(shù)據(jù)
- 主機(jī)操作命令寄存器,使能開(kāi)始命令,使 I2C 總線發(fā)送開(kāi)始信號(hào)。
- 主機(jī)操作發(fā)送數(shù)據(jù)寄存器,寫(xiě)入從機(jī)地址 + 讀寫(xiě)位,決定訪問(wèn)哪個(gè)從機(jī)。這是 一個(gè)8位的數(shù)據(jù),其中高 7 位是從機(jī)地址,最后 1 位是讀寫(xiě)位。 1 表示讀操作,0 表示寫(xiě)操作(對(duì)主機(jī)而言)。這里讀寫(xiě)位為 0 。
- 主機(jī)操作命令寄存器,使能寫(xiě)命令,使 I2C 總線開(kāi)始傳輸數(shù)據(jù)。
- 主機(jī)讀取狀態(tài)寄存器的 TIP 位,以確保命令執(zhí)行完畢。
- 主機(jī)操作發(fā)送數(shù)據(jù)寄存器,寫(xiě)入從機(jī)存儲(chǔ)地址,決定待發(fā)送數(shù)據(jù)存儲(chǔ)在從機(jī)哪里。
- 主機(jī)操作命令寄存器,使能寫(xiě)命令,使 I2C 總線開(kāi)始傳輸數(shù)據(jù)。
- 主機(jī)讀取狀態(tài)寄存器的 TIP 位,以確保命令執(zhí)行完畢。
- 主機(jī)操作發(fā)送數(shù)據(jù)寄存器,寫(xiě)入 8 bit 的待發(fā)送數(shù)據(jù)。
- 主機(jī)操作命令寄存器,使能寫(xiě)命令,使 I2C 總線開(kāi)始傳輸數(shù)據(jù)。
- 主機(jī)讀取狀態(tài)寄存器的 TIP 位,以確保命令執(zhí)行完畢。
- 重復(fù)步驟 8 到 10,不斷向從機(jī)寫(xiě)數(shù)據(jù);
- 主機(jī)操作命令寄存器,使能結(jié)束命令,使 I2C 總線結(jié)束傳輸數(shù)據(jù)。
- 每次傳輸結(jié)束后需要延時(shí),保證下次能正常開(kāi)始傳輸。
主機(jī)讀數(shù)據(jù)
- 主機(jī)操作命令寄存器,使能開(kāi)始命令,使 I2C 總線發(fā)送開(kāi)始信號(hào)。
- 主機(jī)操作發(fā)送數(shù)據(jù)寄存器,寫(xiě)入從機(jī)地址 + 讀寫(xiě)位,決定訪問(wèn)哪個(gè)從機(jī)。這是 一個(gè)8位的數(shù)據(jù),其中高 7 位是從機(jī)地址,最后 1 位是讀寫(xiě)位。 1 表示讀操作,
0 表示寫(xiě)操作(對(duì)主機(jī)而言)。這里讀寫(xiě)位為 0 。 - 主機(jī)操作命令寄存器,使能寫(xiě)命令,使 I2C 總線開(kāi)始傳輸數(shù)據(jù)。
- 主機(jī)讀取狀態(tài)寄存器的 TIP 位,以確保命令執(zhí)行完畢。
- 主機(jī)操作發(fā)送數(shù)據(jù)寄存器,寫(xiě)入從機(jī)存儲(chǔ)地址,主機(jī)將會(huì)從該地址讀取數(shù)據(jù)。
- 主機(jī)操作命令寄存器,使能寫(xiě)命令,使 I2C 總線開(kāi)始傳輸數(shù)據(jù)。
- 主機(jī)讀取狀態(tài)寄存器的 TIP 位,以確保命令執(zhí)行完畢。
- 主機(jī)操作命令寄存器,使能開(kāi)始命令(這種情況是重復(fù)起始),使 I2C 總線發(fā)送開(kāi)始信號(hào)。
- 主機(jī)操作發(fā)送數(shù)據(jù)寄存器,寫(xiě)入從機(jī)地址 + 讀寫(xiě)位。這里讀寫(xiě)位為 1。
- 主機(jī)操作命令寄存器,使能寫(xiě)命令,使 I2C 總線開(kāi)始傳輸數(shù)據(jù)。
- 主機(jī)讀取狀態(tài)寄存器的 TIP 位,以確保命令執(zhí)行完畢。
- 主機(jī)操作命令寄存器,使能讀命令和應(yīng)答命令,使 I2C 總線開(kāi)始接收數(shù)據(jù)。
- 主機(jī)操作接收數(shù)據(jù)寄存器,讀取接收到的 8 bit 的數(shù)據(jù)。
- 重復(fù)步驟 12 到 13,不斷從從機(jī)讀數(shù)據(jù);
- 當(dāng)主機(jī)需要停止從從機(jī)讀數(shù)據(jù)時(shí),操作命令寄存器,使能讀命令,但不使能應(yīng)答命令,讀取最后一個(gè)字節(jié)的數(shù)據(jù)。
我們可以將 I2C 模塊作為一個(gè) APB 外設(shè),掛在 APB 總線上。那么就需要設(shè)計(jì)一個(gè) APB 接口來(lái)對(duì)寄存器進(jìn)行讀寫(xiě)操作。我們需要對(duì)每個(gè) APB 接口分配一個(gè)地址,這樣才能通過(guò)譯碼電路區(qū)分開(kāi)來(lái)不同的 APB 。在 config.h 文件中定了 9 路 APB 地址,默認(rèn)使用 APB0 作為 GPIO ,現(xiàn)在我們?yōu)?I2C 模塊分配 APB5 ,對(duì)應(yīng)地址為 0xbfe90000 ,如下所示,
//APB0 | ||
---|---|---|
`define APB_SLV0_ADDR_BASE | 32'hbfeb0000 | //APB0 base address |
`define APB_SLV0_ADDR_LEN | 32'h0000ffff | //APB0 length |
//APB1 | ||
`define APB_SLV1_ADDR_BASE | 32'hbfec0000 | //APB1 base address |
`define APB_SLV1_ADDR_LEN | 32'h0000ffff | //APB1 length |
//APB2 | ||
`define APB_SLV2_ADDR_BASE | 32'hbfed0000 | //APB2 base address |
`define APB_SLV2_ADDR_LEN | 32'h0000ffff | //APB3 length |
//APB3 | ||
`define APB_SLV3_ADDR_BASE | 32'hbfea0000 | //APB3 base address |
`define APB_SLV3_ADDR_LEN | 32'h0000ffff | //APB3 length |
//APB4 | ||
`define APB_SLV4_ADDR_BASE | 32'hbfe88000 | //APB4 base address |
`define APB_SLV4_ADDR_LEN | 32'h00000fff | //APB4 length |
//APB5 | ||
`define APB_SLV5_ADDR_BASE | 32'hbfe90000 | //APB5 base address |
`define APB_SLV5_ADDR_LEN | 32'h0000ffff | //APB5 length |
最后實(shí)現(xiàn)的結(jié)構(gòu)框圖如下所示,
在頂層文件 godson_mcu_top.v 中例化我們?cè)O(shè)計(jì)的模塊,如下所示,
在約束文件中,將例化好的 I2C 的輸出引腳與原理圖上的合適引腳進(jìn)行連接即可,如下所示,
2. 軟件設(shè)計(jì),基于 I2C 模塊與 AT24C64 的EEPROM 芯片通信
既然已經(jīng)設(shè)計(jì)好了硬件電路,我們就可以進(jìn)行軟件程序的編寫(xiě)了。在硬件 I2C 模塊設(shè)計(jì)過(guò)程中我們?yōu)?APB 分配的地址是 0xbfe90000 ,并且 I2C 相關(guān)寄存器的偏移地址是 0x00 0x01 0x02 0x03 0x04,所以軟件上需要對(duì)應(yīng)好。一個(gè)不錯(cuò)的方法是用結(jié)構(gòu)體指針來(lái)訪問(wèn)寄存器。由于這個(gè)結(jié)構(gòu)體指針使用頻率很高,所以通過(guò)宏定義進(jìn)行重命名 (I2C),如下所示,
編寫(xiě)的函數(shù)聲明如下所示,
然后可以基于 I2C 相函數(shù)編寫(xiě) I2C 讀寫(xiě) AT24C64 的函數(shù),如下所示,
返回值:無(wú)
說(shuō) 明:無(wú)
****************************************************************/ void AT24CXX_WriteByte(uint16_t u16Addr, uint8_t u8Data)
{
soc_I2C_GenerateSTART(ENABLE);// 起始信號(hào) soc_I2C_SendData(DEV_ADDR | WRITE_CMD);// 器件尋址+讀/寫(xiě)選擇 soc_I2C_wait();
soc_I2C_SendData((uint8_t)((u16Addr >> 8) & 0xFF)); soc_I2C_wait();
soc_I2C_SendData((uint8_t)(u16Addr & 0xFF)); soc_I2C_wait();
soc_I2C_SendData(u8Data); soc_I2C_wait(); soc_I2C_GenerateSTOP(ENABLE);// 停止信號(hào)
soc_I2C_delay(20);// 需要延時(shí) 2u ,保證下次能正常開(kāi)始傳輸
}
/****************************************************************
函數(shù)名:x24Cxx_ReadByte
功 能:讀一個(gè)字節(jié)
參 數(shù):u16Addr要讀取的地址
返回值:u8Data讀出的數(shù)據(jù)
說(shuō) 明:無(wú)
****************************************************************/ uint8_t AT24CXX_ReadByte(uint16_t u16Addr)
{
uint8_t u8Data = 0; soc_I2C_GenerateSTART(ENABLE);// 起始信號(hào)
soc_I2C_SendData(DEV_ADDR | WRITE_CMD);// 器件尋址+讀/寫(xiě)選擇
這里需要注意的是,對(duì)芯片完成單字節(jié)寫(xiě)入或者頁(yè)面寫(xiě)入命令后需要延時(shí) 10ms 。因?yàn)樵O(shè)備在此期間將不會(huì)對(duì)新的命令作出響應(yīng)。延時(shí)之后再進(jìn)行讀命令才可以讀到剛寫(xiě)入的數(shù)據(jù)。
我們可以在 main.c 中調(diào)用相應(yīng)函數(shù)驗(yàn)證 I2C 通信,如下所示,