1.寫在前面
I2C總線是由PHILIPS公司開發(fā)的一種簡單、「雙向二線制同步串行總線」。
關(guān)于i2c的使用,并不陌生,STM32、C51、ARM、MSP430等,都基本集成硬件i2c,或者不集成i2c的,可以根據(jù)總線時序圖使用普通IO口翻轉(zhuǎn)模擬一根i2c總線。
對于流行的STM32飽受詬病的硬件I2C,相信很多人都是使用模擬I2C。
模擬i2c的源碼比較多,大多都是大同小異,對于各類例程,提供的模擬i2c似乎都不是太規(guī)范(個人見解),特別是一根i2c總線掛多個外設(shè)、模擬多根i2c總線、以及更換一個i2c外設(shè)時,都需要大幅度修改源碼、復制源碼、重新調(diào)試時序等重復的工作。
在閱讀過Linux設(shè)備驅(qū)動框架和RT-Thread的驅(qū)動框架,發(fā)現(xiàn)在總線分層上處理就特別好,完美解決了上述提及的問題。參考RT-Thread和Linux下的模擬i2c,整理修改在裸機上使用。
2.Linux、RT-Thread設(shè)備驅(qū)動模型
1)模型分為總線驅(qū)動和設(shè)備驅(qū)動;
2) 總線驅(qū)動與外設(shè)驅(qū)動分離,方便一根總線掛多個外設(shè),方便移植;
3) 底層(與硬件相關(guān))與上層分離,方便添加總線及移植到不同處理器,移植到其他處理器,只需重新實現(xiàn)硬件相關(guān)的“寄存器”層即可;
3.MCU下裸機形式i2c總線抽象
此部分實現(xiàn)源碼為:i2c_core.c i2c_core.h
“i2c_bus_xfer”為i2c封裝對外的API,函數(shù)原型如下,提供一個函數(shù)模型,具體需要實例化函數(shù)指針。
int i2c_bus_xfer(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num) { int size; size = dev->xfer(dev,msgs,num); return size; }
a)此函數(shù)即作為驅(qū)動外設(shè)的對外接口,所有操作通過此函數(shù)接口,與底層總線實現(xiàn)分離,如EEPROM、RTC、溫度傳感器等;
b)一個對外函數(shù)已經(jīng)實現(xiàn)90%的情況使用,對應一些特殊情況,后期再完善或增加API。
c)struct i2c_dev_device *i2c_dev
2)i2c總線抽象API參數(shù)
a)i2c_dev:i2c設(shè)備指針,類型為“struct i2c_dev_device”,驅(qū)動一個i2c外設(shè)時,首先要對此指針設(shè)備初始化;
b)msgs:i2c一幀數(shù)據(jù),發(fā)送數(shù)據(jù)及存放返回數(shù)據(jù)的緩存;
c)num:數(shù)據(jù)幀數(shù)量。
3)struct i2c_dev_device
該結(jié)構(gòu)體為關(guān)鍵,調(diào)用API驅(qū)動外設(shè)時,首先對此初始化(類似于Linux/RT-Thread注冊設(shè)備)。完整的設(shè)備包括兩部分,數(shù)據(jù)操作函數(shù)和i2c相關(guān)信息(如硬件i2c或者模擬i2c)。因此“struct i2c_dev_device”的原型為:
struct i2c_dev_device { int (*xfer)(struct i2c_dev_device *dev,struct i2c_dev_message msgs[],unsigned int num); void *i2c_phy; };
a)第一個參數(shù)是函數(shù)指針,數(shù)據(jù)收發(fā)通過此函數(shù)指針調(diào)用實體函數(shù)實現(xiàn);
b)第二個參數(shù)是一個void指針,初始化時指向我們使用的物理i2c(硬件/模擬),使用時可強制轉(zhuǎn)換為對應的類型。
4)xfer
該函數(shù)與i2c總線設(shè)備對外接口函數(shù)“i2c_bus_xfer”具有相同的參數(shù),形參參數(shù)參考此項的第2點,初始化時實例化指向?qū)嶓w函數(shù)。
5)struct i2c_dev_message
“struct i2c_dev_message”為i2c總線訪問外設(shè)的一幀數(shù)據(jù)信息,包括發(fā)送數(shù)據(jù)、外設(shè)從地址、訪問標識等。原型如下:
struct i2c_dev_message { unsigned short addr; unsigned short flags; unsigned short size; unsigned char *buff; unsigned char retries; };a)addr:i2c外設(shè)從機地址,常用為7位,10位較少用;
b)flags:標識,發(fā)送、接收、應答、地址位選擇等標識;幾種標識如下:
#define I2C_BUS_WR 0x0000 #define I2C_BUS_RD (1u << 0) #define I2C_BUS_ADDR_10BIT (1u << 2) #define I2C_BUS_NO_START (1u << 4) #define I2C_BUS_IGNORE_NACK (1u << 5) #define I2C_BUS_NO_READ_ACK (1u << 6)
c)size:發(fā)送的數(shù)據(jù)大小,或者接收的緩存大小;
d)buff:緩存區(qū);
e)retries:i2c啟動失敗時,重啟的次數(shù)。
4.模擬i2c抽象
對于模擬i2c,在以往的實現(xiàn)方式中,基本是時序圖和外設(shè)代碼混合在一起,增加外設(shè)或者使用新的i2c外設(shè)時,需要對模擬i2c代碼進行較大工作量的修改,或者以“復制”的方式實現(xiàn)一套新的i2c總線。
但同理,可以把模擬i2c時序部分代碼抽象出來,以“復用”代碼的形式實現(xiàn)。此部分實現(xiàn)源碼為:i2c_bitops.c i2c_bitops.h
1)模擬i2c抽象對外接口
根據(jù)上述封裝的對外API,使用時,首先需要實現(xiàn)入口參數(shù)“i2c_dev”實例化,用模擬i2c即是調(diào)用模擬i2c相關(guān)接口。
int i2c_bitops_bus_xfer(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message msgs[],unsigned long num) { struct i2c_dev_message *msg; unsigned long i; unsigned short ignore_nack; int ret; ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK; i2c_bitops_start(i2c_bus); for (i = 0; i < num; i++) { msg = &msgs[i]; if (!(msg->flags & I2C_BUS_NO_START)) { if (i) { i2c_bitops_restart(i2c_bus); } ret = i2c_bitops_send_address(i2c_bus,msg); if ((ret != 0) && !ignore_nack) goto out; } if (msg->flags & I2C_BUS_RD) {//read ret = i2c_bitops_bus_read(i2c_bus,msg); if(ret < msg->size) { ret = -1; goto out; } } else {//write ret = i2c_bitops_bus_write(i2c_bus,msg); if(ret < msg->size) { ret = -1; goto out; } } } ret = i; out: i2c_bitops_stop(i2c_bus); return ret; } int ops_i2c_bus_xfer(struct i2c_dev_device *i2c_dev,struct i2c_dev_message msgs[],unsigned int num) { return (i2c_bitops_bus_xfer((struct ops_i2c_dev*)(i2c_dev->i2c_phy),msgs,num)); }
a)模擬一根i2c總線時,對外的操作函數(shù)都通過上訴函數(shù);i2c信息幀相關(guān)參數(shù)由上層調(diào)用傳遞進入,此處主要增加“struct ops_i2c_dev”的封裝;
b)該函數(shù)使用到的函,其中入口參數(shù)為“struct ops_i2c_dev”類型的都是模擬i2c相關(guān);
d)模擬i2c封裝實現(xiàn)主要針對“struct ops_i2c_dev”原型的實例化。
2)struct ops_i2c_dev
“struct ops_i2c_dev”原型如下:
struct ops_i2c_dev { void (*set_sda)(int8_t state); void (*set_scl)(int8_t state); int8_t (*get_sda)(void); int8_t (*get_scl)(void); void (*delayus)(uint32_t us); };
a)set_sda:數(shù)據(jù)線輸出;
b)set_scl:時鐘線輸出;
c)get_sda:數(shù)據(jù)線輸入(捕獲);
d)get_scl:時鐘線輸入(捕獲);
e)delayus:延時函數(shù);
要實現(xiàn)一個模擬i2c,只需將上訴函數(shù)指針的實體實現(xiàn)即可,具體看后面描述。
3)模擬i2c時序
以產(chǎn)生i2c起始信號函數(shù)為例子,簡要分析:
static void i2c_bitops_start(struct ops_i2c_dev *i2c_bus) { i2c_bus->set_sda(0); i2c_bus->delayus(3); i2c_bus->set_scl(0); }入口參數(shù)為struct ops_i2c_dev * i2c_bus,其實就是i2c_bitops_bus_xfer應用層函數(shù)傳入的參數(shù),最終是在此調(diào)用,底層需要實現(xiàn)的就是io模擬的輸入/輸出狀態(tài)函數(shù)。
其他函數(shù),如
static void i2c_bitops_restart(struct ops_i2c_dev *i2c_bus) static char i2c_bitops_wait_ack(struct ops_i2c_dev *i2c_bus) staticinti2c_bitops_send_byte(structops_i2c_dev*i2c_bus,unsignedchardata)
等等,入口參數(shù)都是i2c_bus,時序?qū)崿F(xiàn)與常規(guī)裸機程序設(shè)計是一致的,不同的是函數(shù)指針的分離調(diào)用,具體看附件源碼。
4)標識位
在以往的模擬i2c或者硬件i2c中,操作外設(shè)時都有各類情況,如讀和寫方向的切換、連續(xù)操作(不需啟動i2c總線,如寫EEPROM,先寫地址再寫數(shù)據(jù))等。對于這類情況,我們處理辦法是選擇相關(guān)的宏標識即可,具體實現(xiàn)由“中間層”實現(xiàn),讓i2c外設(shè)驅(qū)動起來更簡單!以上述對外函數(shù)為例:
a)通過標識位判斷是讀還是寫狀態(tài)
if (msg->flags & I2C_BUS_RD) {//read ret = i2c_bitops_bus_read(i2c_bus,msg); if(ret < msg->size) { ret = -1; goto out; } }b)應答狀態(tài)標識
ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;
5)讀寫函數(shù)
讀寫函數(shù)最終是通過io口1bit的翻轉(zhuǎn)模擬出時序,從而獲得數(shù)據(jù),這部分與常規(guī)模擬i2c一致,通過函數(shù)指針方式操作。主要實現(xiàn)接口函數(shù):
static unsigned long i2c_bitops_bus_write(struct ops_i2c_dev *i2c_bus,struct i2c_dev_message *msg); staticunsignedlongi2c_bitops_bus_read(structops_i2c_dev*i2c_bus,structi2c_dev_message*msg);
5.模擬i2c總線實現(xiàn)
此部分實現(xiàn)源碼為:i2c_hw.c i2c_hw.h
以stm32f1為硬件平臺,采用上述模擬i2c封裝,實現(xiàn)一根模擬i2c總線。
1)實現(xiàn)struct ops_i2c_dev函數(shù)實體
除了“delayus”函數(shù)外,其余為io翻轉(zhuǎn),以“set_sda”和“delayus”為例,實現(xiàn)如下:
static void gpio_set_sda(int8_t state) { if (state) I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN; else I2C1_SDA_PORT->BRR = I2C1_SDA_PIN; } static void gpio_delayus(uint32_t us) { #if 0 volatile int32_t i; for (; us > 0; us--) { i = 30; //mini 17 while(i--); } #else Delayus(us); #endif }
a)為例提高速率,上訴代碼采用寄存器方式操作,可以用庫函數(shù)操作io口;
b)延時可以用硬件定時器延時,或者軟件延時,具體根據(jù)cpu時鐘計算;
c)其他源碼看附件中“i2c_hw.c”
2)初始化一根模擬i2c總線
void stm32f1xx_i2c_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitStructure.GPIO_Pin = I2C1_SDA_PIN | I2C1_SCL_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(I2C1_SDA_PORT, &GPIO_InitStructure); I2C1_SDA_PORT->BSRR = I2C1_SDA_PIN; I2C1_SCL_PORT->BSRR = I2C1_SCL_PIN; //device init ops_i2c1_dev.set_sda = gpio_set_sda; ops_i2c1_dev.get_sda = gpio_get_sda; ops_i2c1_dev.set_scl = gpio_set_scl; ops_i2c1_dev.get_scl = gpio_get_scl; ops_i2c1_dev.delayus = gpio_delayus; i2c1_dev.i2c_phy = &ops_i2c1_dev; i2c1_dev.xfer = ops_i2c_bus_xfer; }a)i2c io初始化;
b)i2c設(shè)備實例化,其中“ops_i2c1_dev”和“i2c1_dev”即是我們定義的總線設(shè)備,后面使用該總線時主要通過“i2c1_dev”實現(xiàn)對底層的調(diào)用。
6.驅(qū)動EEPROM(AT24C16)
此部分實現(xiàn)源碼為:24clxx.c 24clxx.h
上面總線完成后,驅(qū)動一個i2c外設(shè)可以說就是信手拈來的事情了,而且模擬i2c總線抽象出來后,不需在做重復調(diào)試時序的工作。
假設(shè)初始化的i2c設(shè)備為i2c1_dev。
1)寫EEPROM
寫一個字節(jié),頁寫算法詳細見源碼附件(24clxx.c):
char ee_24clxx_writebyte(u16 addr,u8 data) { struct i2c_dev_message ee24_msg[1]; u8 buf[3]; u8 slave_addr; if(EEPROM_MODEL > 16) { slave_addr =EE24CLXX_SLAVE_ADDR; buf[0] = (addr >>8)& 0xff; buf[1] = addr & 0xff; buf[2] = data; ee24_msg[0].size = 3; } else { slave_addr = EE24CLXX_SLAVE_ADDR | (addr>>8); buf[0] = addr & 0xff; buf[1] = data; ee24_msg[0].size = 2; } ee24_msg[0].addr = slave_addr; ee24_msg[0].flags = I2C_BUS_WR; ee24_msg[0].buff = buf; i2c_bus_xfer(&i2c1_dev,ee24_msg,1); return 0; }
2)讀EEPROM
voidee_24clxx_readbytes(u16 read_ddr, char* pbuffer, u16 read_size) { struct i2c_dev_message ee24_msg[2]; u8 buf[2]; u8 slave_addr; if(EEPROM_MODEL > 16) { slave_addr =EE24CLXX_SLAVE_ADDR; buf[0] = (read_ddr>>8)& 0xff; buf[1] = read_ddr& 0xff; ee24_msg[0].size = 2; } else { slave_addr =EE24CLXX_SLAVE_ADDR | (read_ddr>>8); buf[0] = read_ddr & 0xff; ee24_msg[0].size = 1; } ee24_msg[0].buff = buf; ee24_msg[0].addr = slave_addr; ee24_msg[0].flags = I2C_BUS_WR; ee24_msg[1].addr = slave_addr; ee24_msg[1].flags = I2C_BUS_RD; ee24_msg[1].buff = (u8*)pbuffer; ee24_msg[1].size = read_size; i2c_bus_xfer(&i2c1_dev,ee24_msg,2); }
3)注意事項
驅(qū)動一個外設(shè)相對容易了,注意的事項就是標識位部分。
a)此處外設(shè)地址(addr),是實際地址,不含讀寫位(7bit),比如AT24C16外設(shè)地址為0x50,可能大家平常用的是0xA0,因為包括讀寫位;
b)寫數(shù)據(jù)時,如果以2幀i2c_dev_message消息發(fā)送,需要注意“I2C_BUS_NO_START”宏,此宏標識意思是不需要再次啟動i2c了,一般看i2c外設(shè)手冊時序圖可知道。如寫EEPROM是先寫地址,然后寫數(shù)據(jù)這個過程是連續(xù)的,此時就需用到“I2C_BUS_NO_START”標識。程序可改成這樣:
char ee_24clxx_writebyte(u16 addr,u8 data) { struct i2c_dev_message ee24_msg[2]; u8 buf[2]; u8 slave_addr; if(EEPROM_MODEL > 16) { slave_addr =EE24CLXX_SLAVE_ADDR; buf[0] = (addr>>8)& 0xff; buf[1] = addr &0xff; ee24_msg[0].size = 2; } else { slave_addr =EE24CLXX_SLAVE_ADDR | (addr>>8); buf[0] = addr &0xff; ee24_msg[0].size = 1; } ee24_msg[0].addr = slave_addr; ee24_msg[0].flags = I2C_BUS_WR; ee24_msg[0].buff = buf; ee24_msg[1].addr = slave_addr; ee24_msg[1].flags = I2C_BUS_WR |I2C_BUS_NO_START; ee24_msg[1].buff = &data; ee24_msg[1].size = 1; i2c_bus_xfer(&i2c1_dev,ee24_msg,2); return 0; }
4)其他
理解之后,或者使用過Linux、RT-Thread的驅(qū)動框架的,再驅(qū)動其他i2c外設(shè),就是很容易的事情了,剩下的就是配置寄存器、應用算法的問題了。
7.總結(jié)
1)整體思路比較易理解,本質(zhì)就是函數(shù)指針,將與硬件底層無關(guān)的部分抽象出來,相關(guān)聯(lián)的地方分層明確,通過函數(shù)指針的方式進行調(diào)用。
2)事務(wù)分離,通用、重復的事情交給總線處理,特殊任務(wù)留給外設(shè)驅(qū)動。
審核編輯:湯梓紅
-
mcu
+關(guān)注
關(guān)注
146文章
17149瀏覽量
351224 -
Linux
+關(guān)注
關(guān)注
87文章
11304瀏覽量
209537 -
STM32
+關(guān)注
關(guān)注
2270文章
10900瀏覽量
356091 -
I2C
+關(guān)注
關(guān)注
28文章
1487瀏覽量
123787 -
串行總線
+關(guān)注
關(guān)注
1文章
182瀏覽量
30626
原文標題:STM32指針抽象出I2C的數(shù)據(jù)實例,附代碼
文章出處:【微信號:c-stm32,微信公眾號:STM32嵌入式開發(fā)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論