在我們的應用開發過程中,經常會使用到外部的EEPROM外部存儲器來保存一些參數和配置數據等。而比較常用的就是AT24Cxx系列產品,這一節我們來開發用于操作AT24Cxx系列產品的驅動。
1 、功能概述
AT24Cxx系列EEPROM包括從1Kbit到2Mbit的各種容量。AT24Cxx系列產品采用I2C總線數據傳送協議。盡管容量跨度很大,但它們都擁有相同的封裝和引腳排布,具體的引腳分配如下:
由于A0、A1和A2可以組成000~111八種情況,即通過器件地址輸入端A0、A1和A2可以實現將最多8個器件連接到同一條總線上,通過不同的配置進行器件的選擇。
對于AT24Cxx系列EEPROM不同的容量對地址的分配有較大差異,這涉及到設備地址和寄存器地址。從1K容量到2M容量寄存器地址分別采用7到18位來表示。16K及以下容量的EEPROM采用一個字節的寄存器地址配合設備地址段實現7到11位的寄存器地址尋址。而32k及以上的EEPROM采用兩個字節的寄存器地址配合設備地址段實現12到18位的寄存器地址尋址。具體的地址分配如下:
從上表我們很容易明白,設備地址的低3位的定義決定了在同一條I2C總線上,最多可以掛載多少個AT24Cxx設備。有3位用于設備地址則最多可掛載8個設備;有2位用于設備地址則最多可掛載4個設備;有1位用于設備地址則最多可掛載2個設備;有0位用于設備地址則最多可掛載1個設備。需要注意的是,不同定義的位的設備混用于同一總線時,相同的定義位必須一樣,否則用作寄存器地址的位可能讓總線上的總線無法識別。
在一些AT24Cxx系列EEPROM型號中,帶有序列號的專用存儲單元。這些存儲單元不占用存儲器的存儲單元。序列號為128位,讀取序列號的設備地址以0xB0開頭,以區別于EEPROM存儲區域的讀取。
在一些AT24Cxx系列EEPROM型號中,除了帶有序列號的專用存儲單元外,還有帶有48位或者64位的MAC地址,固定在專用的存儲單元。這些單元不占用存儲器的存儲單元。讀取序列號和讀取MAC地址采用同樣的設備地址,均以0xB0開頭。有一些型號該區域并未用于MAC定制可用于用戶操作。
需要注意的是有些型號的AT24Cxx系列EEPROM存儲器的設備地址是固化的,需通過型號的后綴標識來識別。
2 、驅動設計與實現
我們已經了解了AT24Cxx存儲器的基本功能及讀寫方式,接下來我們將開發操作AT24Cxx系列EEPROM存儲器的驅動程序。
2.1 、對象定義
在使用一個對象之前我們需要獲得一個對象。同樣的我們想要AT24Cxx系列EEPROM存儲器就需要先定義AT24Cxx系列EEPROM存儲器的對象。
2.1.1 、對象的抽象
我們要得到AT24Cxx系列EEPROM存儲器對象,需要先分析其基本特性。一般來說,一個對象至少包含兩方面的特性:屬性與操作。接下來我們就來從這兩個方面思考一下AT24Cxx系列EEPROM存儲器的對象。
先來考慮屬性,作為屬性肯定是用于標識或記錄對象特征的東西。我們來考慮AT24Cxx系列EEPROM存儲器對象屬性。首先AT24Cxx系列EEPROM存儲器采用的是I2C接口,對于每一個I2C接口元件都有一個設備地址用于區別總線上的設備,所以我們將I2C設備地址作為對象的屬性用以區別總線設備。AT24Cxx系列EEPROM存儲器存在多個型號對應不同的容量和特性,所以我們將其型號設置為對象的屬性以區別對象的類型。前面我們也說過,不同容量的AT24Cxx系列EEPROM存儲器由于尋址空間不同,所以寄存器地址長度也是不同的,所以我們將其地址長度作為屬性以區分處理。
接著我們還需要考慮AT24Cxx系列EEPROM存儲器對象的操作問題。我們需要對AT24Cxx系列EEPROM存儲器進行數據讀寫操作,無論讀寫其實都依賴于對I2C接口的操作,而這些操作基本都會依賴于具體的硬件平臺,所以我們將讀寫操作作為對象的操作。
根據上述我們對AT24Cxx系列EEPROM存儲器的分析,我們可以定義AT24Cxx系列EEPROM存儲器的對象類型如下:
1 typedef struct At24cObject {
2 uint8_t devAddress; //設備地址
3 At24cModeType mode; //設備類型
4 At24cMemAddLengthType memAddLength; //寄存器地址長度
5 void (*Read)(struct At24cObject *at,uint16_t regAddress,uint8_t *rData,uint16_t rSize); //讀數據操作指針
6 void (*Write)(struct At24cObject *at,uint16_t regAddress,uint8_t *wData,uint16_t wSize); //寫數據操作指針
7 void (*Delayms)(volatile uint32_t nTime); //毫秒延時操作指針
8 }At24cObjectType;
2.1.2 、對象初始化
我們知道,一個對象僅作聲明是不能使用的,我們需要先對其進行初始化,所以這里我們來考慮AT24Cxx系列EEPROM存儲器對象的初始化函數。一般來說,初始化函數需要處理幾個方面的問題。一是檢查輸入參數是否合理;二是為對象的屬性賦初值;三是對對象作必要的初始化配置。據此我們設計AT24Cxx系列EEPROM存儲器對象的初始化函數如下:
1 /* 初始化AT24CXX對象 */
2 void At24cxxInitialization(At24cObjectType *at, //AT24CXX對象實體
3 uint8_t devAddress, //AT24CXX設備地址
4 At24cModeType mode, //AT24CXX對象類型
5 At24cMemAddLengthType length, //寄存器地址長度
6 At24cRead read, //讀AT24CXX對象操作指針
7 At24cWrite write, //寫AT24CXX對象操作指針
8 At24cDelayms delayms //延時操作指針
9 )
10 {
11 if((at==NULL)||(read==NULL)||(write==NULL)||(delayms==NULL))
12 {
13 return;
14 }
15
16 if((devAddress&0xF0)==0xA0)
17 {
18 at->devAddress=devAddress;
19 }
20 else
21 {
22 at->devAddress=0x00;
23 }
24
25 at->mode=mode;
26 at->memAddLength=length;
27
28 at->Read=read;
29 at->Write=write;
30 at->Delayms=delayms;
31 }
2.2 、對象操作
我們已經完成了AT24Cxx系列EEPROM存儲器對象類型的定義和對象初始化函數的設計。但我們的主要目標是獲取對象的信息,接下來我們還要實現面向AT24Cxx系列EEPROM存儲器的各類操作。
2.2.1 、寫單個字節
AT24Cxx系列EEPROM存儲器支持單字節寫數據,收到正確的設備地址和字地址字節后,EEPROM將發送一個確認。然后設備將準備接收8位數據字。在接收到8位數據字之后,EEPROM將返回一個ACK。然后,尋址設備(如總線主機)必須使用停止條件終止寫操作。此時,EEPROM將進入一個內部自動定時的寫周期,這個寫周期將在一定時間內完成,而數據字將被編程到非易失性EEPROM中。在這個寫周期中,所有的輸入都是禁用的,EEPROM在寫完成之前不會響應。
如果AT24Cxx對象是一個使用7位到11位地址表示寄存器地址的話,其數據格式如下圖所示:
如果AT24Cxx對象是一個使用12位到18位地址表示寄存器地址的話,其數據格式如下圖所示:
根據上述時序圖,我們可以編寫AT24Cxx系列EEPROM存儲器寫單個字節數據程序如下:
1 /*向AT24CXX寫入單個字節*/
2 void WriteByteToAT24CXX(At24cObjectType *at,uint32_t regAddress,uint8_t data)
3 {
4 uint8_t temp;
5 uint16_t regAdd;
6
7 if(at->memAddLength==AT24C8BitMemAdd)
8 {
9 regAdd=(uint16_t)(regAddress&0xFF);
10 temp=(uint8_t)(regAddress>>8);
11 }
12 else
13 {
14 regAdd=(uint16_t)regAddress;
15 temp=(uint8_t)(regAddress>>16);
16 }
17 temp=(temp&(~(devAddMask[at->mode]>>1)))<<1;
18 at->devAddress=(at->devAddress & devAddMask[at->mode])|temp;
19
20 at->Write(at,regAdd,&data,1);
21 }
2.2.2 、寫多個字節
AT24Cxx系列EEPROM存儲器支持多字節的寫操作,但對于AT24Cxx對象來說最多只支持寫到發送地址所在的頁尾,所以資料中也稱其為頁寫。而對于不同型號的AT24Cxx系列EEPROM存儲器每頁所包含的字節數是不一樣的,從8個字節到256個字節不等,我們需要注意寫入的字節數。
整頁寫的初始化方式與單字節寫的初始化方式相同,但是總線主機在第一個數據字被鎖定后不會發送停止條件。相反,在EEPROM承認接收到第一個數據字之后,總線主機可以傳輸最多到所在頁結尾的數據字。EEPROM將在接收到每個數據字后返回一個ACK。一旦所有要寫的數據都被發送到設備,總線主機必須發出一個停止條件此時內部的自計時寫周期將開始。字地址的下四位在接收到每個數據字后進行內部遞增,高階位地址位元不會增加。整頁寫操作僅限于在單個物理頁中寫入字節,而不管實際寫入的字節數。當增加的字地址到達頁面邊界時,地址計數器將滾動到同一頁面的開頭。這是必須要注意的,一旦繼續寫數據可能會將頁面中先前加載的數據無意中更改。
如果AT24Cxx對象是一個使用7位到11位地址表示寄存器地址的話,其寫多個字節的數據格式如下圖所示:
如果AT24Cxx對象是一個使用12位到18位地址表示寄存器地址的話,其寫多個字節的數據格式如下圖所示:
根據上述時序圖,我們可以編寫AT24Cxx系列EEPROM存儲器寫多個字節數據程序如下:
1 /*向AT24CXX寫入多個字節,從指定地址最多到所在頁的結尾*/
2 void WriteBytesToAT24CXX(At24cObjectType *at,uint32_t regAddress,uint8_t *wData,uint16_t wSize)
3 {
4 uint16_t regAdd;
5 uint8_t size;
6 uint8_t temp;
7
8 if(at->memAddLength==AT24C8BitMemAdd)
9 {
10 regAdd=(uint16_t)(regAddress&0xFF);
11 temp=(uint8_t)(regAddress>>8);
12 }
13 else
14 {
15 regAdd=(uint16_t)regAddress;
16 temp=(uint8_t)(regAddress>>16);
17 }
18 temp=(temp&(~(devAddMask[at->mode]>>1)))<<1;
19 at->devAddress=(at->devAddress & devAddMask[at->mode])|temp;
20
21 if((wSize<=pageBytes[at->mode])&&(wSize<=(pageBytes[at->mode]-(regAddress®AddMask[at->mode]))))
22 {
23 size=wSize;
24 }
25 else
26 {
27 size=pageBytes[at->mode]-(regAddress®AddMask[at->mode]);
28 }
29
30 at->Write(at,regAdd,wData,size);
31 }
2.2.3 、讀單個字節
AT24Cxx系列EEPROM存儲器支持單字節的讀操作。這一方式其實有兩種模式,讀當前位置和讀隨機位置。其實讀當前位置是讀隨機位置特例,我們考慮一般性則只考慮隨機讀取就可以了。隨機讀的開始方式與字節寫操作加載新數據字地址的方式相同。這就是所謂的“偽寫”序列;但是,必須省略數據字節和字節寫的停止條件,以防止該部分進入內部寫循環。一旦設備地址和字地址被鎖定并被EEPROM確認,總線主機必須生成另一個啟動條件。總線主機現在通過發送一個啟動條件來初始化一個讀取的當前地址,接著是一個有效的設備地址字節,其R/W位設置為邏輯“1”。之后EEPROM將對設備地址進行ACK處理,并在SDA線路上連續地輸出數據字。如果總線主機在第9個時鐘周期內沒有響應ACK,則所有類型的讀操作都將終止。在NACK響應之后,主進程可以發送一個停止條件來完成協議。
如果AT24Cxx對象是一個使用7位到11位地址表示寄存器地址的話,其讀單個字節的數據格式如下圖所示:
如果AT24Cxx對象是一個使用12位到18位地址表示寄存器地址的話,其讀單個字節的數據格式如下圖所示:
根據上述時序圖,我們可以編寫AT24Cxx系列EEPROM存儲器隨機讀取一個字節數據程序如下:
1 /*從AT24CXX讀取單個字節,從隨機地址讀取*/
2 uint8_t ReadByteFromAT24CXX(At24cObjectType *at,uint32_t regAddress)
3 {
4 uint8_t rData;
5 uint16_t regAdd;
6 uint8_t temp;
7
8 if(at->memAddLength==AT24C8BitMemAdd)
9 {
10 regAdd=(uint16_t)(regAddress&0xFF);
11 temp=(uint8_t)(regAddress>>8);
12 }
13 else
14 {
15 regAdd=(uint16_t)regAddress;
16 temp=(uint8_t)(regAddress>>16);
17 }
18 temp=(temp&(~(devAddMask[at->mode]>>1)))<<1;
19 at->devAddress=(at->devAddress & devAddMask[at->mode])|temp;
20
21 at->Read(at,regAdd,&rData,1);
22
23 return rData;
24 }
2.2.4 、讀多個字節
AT24Cxx系列EEPROM存儲器支持多字節的讀操作,類似于單字節讀取,可以從當前位置開始讀也可以從指定地址開始讀。多字節讀取也稱為順序讀取由當前地址讀取或隨機讀取啟動。總線主接收到一個數據字后,它以ACK應答。只要EEPROM接收到ACK,它就會繼續增加字地址,并連續地計時輸出連續的數據字。當達到最大內存地址時,數據字地址將滾動,順序讀取將從內存數組的開頭開始。如果總線主機在第9個時鐘周期內沒有響應ACK,則所有類型的讀操作都將終止。在NACK響應之后,主進程可以發送一個停止條件來完成協議。
如果AT24Cxx對象是一個使用7位到11位地址表示寄存器地址的話,其讀多個字節的數據格式如下圖所示:
如果AT24Cxx對象是一個使用12位到18位地址表示寄存器地址的話,其讀多個字節的數據格式如下圖所示:
根據上述時序圖,我們可以編寫AT24Cxx系列EEPROM存儲器順序讀取多個字節數據程序如下:
1 /*從AT24CXX讀取多個字節,從指定地址最多到所在頁的結尾*/
2 void ReadBytesFromAT24CXX(At24cObjectType *at,uint32_t regAddress,uint8_t *rData,uint16_t rSize)
3 {
4 uint16_t regAdd;
5 uint16_t size;
6 uint8_t temp;
7
8 if(at->memAddLength==AT24C8BitMemAdd)
9 {
10 regAdd=(uint16_t)(regAddress&0xFF);
11 temp=(uint8_t)(regAddress>>8);
12 }
13 else
14 {
15 regAdd=(uint16_t)regAddress;
16 temp=(uint8_t)(regAddress>>16);
17 }
18 temp=(temp&(~(devAddMask[at->mode]>>1)))<<1;
19 at->devAddress=(at->devAddress & devAddMask[at->mode])|temp;
20
21 if((rSize<=pageBytes[at->mode])&&(rSize<=(pageBytes[at->mode]-(regAddress®AddMask[at->mode]))))
22 {
23 size=rSize;
24 }
25 else
26 {
27 size=pageBytes[at->mode]-(regAddress®AddMask[at->mode]);
28 }
29
30 at->Read(at,regAdd,rData,size);
31 }
3 、驅動的使用
在上一節我們設計并實現了AT24Cxx系列EEPROM存儲器的驅動程序,而這一節我們將設計一個簡單的應用來驗證這一驅動程序。
3.1 、聲明并初始化對象
使用基于對象的操作我們需要先得到這個對象,所以我們先要使用前面定義的AT24Cxx系列EEPROM存儲器對象類型聲明一個AT24Cxx系列EEPROM存儲器對象變量,具體操作格式如下:
At24cObjectType at24c;
聲明了這個對象變量并不能立即使用,我們還需要使用驅動中定義的初始化函數對這個變量進行初始化。這個初始化函數所需要的輸入參數如下:
At24cObjectType *at,AT24CXX對象實體
uint8_t devAddress,AT24CXX設備地址
At24cModeType mode,AT24CXX對象類型
At24cMemAddLengthType length,寄存器地址長度
At24cRead read,讀AT24CXX對象操作指針
At24cWrite write,寫AT24CXX對象操作指針
At24cDelayms delayms,延時操作指針
對于這些參數,對象變量我們已經定義了。對象類型與寄存器地址長度為枚舉,根據實際情況選擇就好了。設備地址根據我們的實際使用情況設置就可以了。主要的是我們需要定義幾個函數,并將函數指針作為參數。這幾個函數的類型如下:
1 /* 定義讀數據操作函數指針類型 */
2 typedef void (*At24cRead)(struct At24cObject *at,uint16_t regAddress,uint8_t *rData,uint16_t rSize);
3
4 /* 定義寫數據操作函數指針類型 */
5 typedef void (*At24cWrite)(struct At24cObject *at,uint16_t regAddress,uint8_t *wData,uint16_t wSize);
6
7 /* 定義延時操作函數指針類型 */
8 typedef void (*At24cDelayms)(volatile uint32_t nTime);
對于這幾個函數我們根據樣式定義就可以了,具體的操作可能與使用的硬件平臺有關系。片選操作函數用于多設備需要軟件操作時,如采用硬件片選可以傳入NULL即可。具體函數定義如下:
1 /*讀AT24C寄存器值*/
2 static void ReadDataFromAT24C(At24cObjectType *at24c,uint16_t regAddress,uint8_t *rData,uint16_t rSize)
3 {
4 uint16_t cSize;
5 uint8_t cmd[2];
6
7 if(at24c->memAddLength==AT24C8BitMemAdd)
8 {
9 cSize=1;
10 cmd[0]=(uint8_t)regAddress;
11 }
12 else
13 {
14 cSize=2;
15 cmd[0]=(uint8_t)(regAddress>>8);
16 cmd[1]=(uint8_t)regAddress;
17 }
18
19 HAL_I2C_Master_Transmit(&at24chi2c,at24c->devAddress,cmd,cSize,1000);
20
21 HAL_I2C_Master_Receive(&at24chi2c,at24c->devAddress+1,rData, rSize, 1000);
22 }
23
24 /*寫AT24C寄存器值*/
25 static void WriteDataToAT24C(At24cObjectType *at24c,uint16_t regAddress,uint8_t *wData,uint16_t wSize)
26 {
27 uint8_t tData[wSize+2];
28 uint16_t tSize;
29
30 if(at24c->memAddLength==AT24C8BitMemAdd)
31 {
32 tSize=wSize+1;
33 tData[0]=(uint8_t)regAddress;
34 }
35 else
36 {
37 tSize=wSize+2;
38 tData[0]=(uint8_t)(regAddress>>8);
39 tData[1]=(uint8_t)regAddress;
40 }
41
42 for(int i=0;i43 {
44 tData[i+2]=wData[i];
45 }
46
47 HAL_I2C_Master_Transmit(&at24chi2c,at24c->devAddress,wData,wSize,1000);
48 }
對于延時函數我們可以采用各種方法實現。我們采用的STM32平臺和HAL庫則可以直接使用HAL_Delay()函數。于是我們可以調用初始化函數如下:
1 At24cxxInitialization(&at24c, //AT24CXX對象實體
2 0xAE, //AT24CXX設備地址
3 AT24C01C, //AT24CXX對象類型
4 AT24C8BitMemAdd, //寄存器地址長度
5 ReadDataFromAT24C, //讀AT24CXX對象操作指針
6 WriteDataToAT24C, //寫AT24CXX對象操作指針
7 HAL_Delay //延時操作指針
8 );
3.2 、基于對象進行操作
我們定義了對象變量并使用初始化函數給其作了初始化。接著我們就來考慮操作這一對象獲取我們想要的數據。我們在驅動中已經將獲取數據并轉換為轉換值的比例值,接下來我們使用這一驅動開發我們的應用實例。
1 /*AT24XXX數據操作*/
2 void AT24CReadWriteData(void)
3 {
4 uint16_t regAddress=0x02;
5 uint8_t readByte;
6 uint8_t writeByte=0x0A;
7 uint8_t rData[2];
8 uint16_t rSize=2;
9 uint8_t wData[]={0x5A,0xA5};
10 uint16_t wSize=2;
11
12 /*從AT24CXX讀取單個字節,從隨機地址讀取*/
13 readByte=ReadByteFromAT24CXX(&at24c,regAddress);
14
15 /*向AT24CXX寫入單個字節*/
16 WriteByteToAT24CXX(&at24c,regAddress,writeByte);
17
18 /*從AT24CXX讀取多個字節,從指定地址最多到所在頁的結尾*/
19 ReadBytesFromAT24CXX(&at24c,regAddress,rData,rSize);
20
21 /*向AT24CXX寫入多個字節,從指定地址最多到所在頁的結尾*/
22 WriteBytesToAT24CXX(&at24c,regAddress,wData,wSize);
23 }
4 、應用總結
這一篇中,我們設計了AT24Cxx系列EEPROM存儲器的讀寫驅動,而且設計了一個簡單的應用驗證了驅動程序,讀寫操作都能按預期要求完成,而且操作也很穩定。
在使用驅動時我們需要注意,因為不同容量的AT24Cxx系列EEPROM存儲器的每一頁的字節數數不一樣的。在多字節讀寫時,最多支持到所在頁尾。到頁尾后,EEPROM存儲器的內部指針將回到頁首,此時執行讀則得到的是錯誤數據,若執行寫則會覆蓋原有數據造成錯誤。所以在程序中若讀寫的范圍超越了一頁的范圍將會被舍棄。
在使用驅動時還需注意,因為不同容量的AT24Cxx系列EEPROM存儲器的尋址范圍是不一樣的,所以用于表示寄存器地址的寄存器地址位數有1個字節和2個字節的差別,為了便于區分需要在對AT24Cxx系列EEPROM存儲器對象進行初始化時指定。
源碼下載:https://github.com/foxclever/ExPeriphDriver
評論
查看更多