1.寫在前面
I2C總線是由PHILIPS公司開發的一種簡單、「雙向二線制同步串行總線」。
關于i2c的使用,并不陌生,STM32、C51、ARM、MSP430等,都基本集成硬件i2c,或者不集成i2c的,可以根據總線時序圖使用普通IO口翻轉模擬一根i2c總線。
對于流行的STM32飽受詬病的硬件I2C,相信很多人都是使用模擬I2C。
模擬i2c的源碼比較多,大多都是大同小異,對于各類例程,提供的模擬i2c似乎都不是太規范(個人見解),特別是一根i2c總線掛多個外設、模擬多根i2c總線、以及更換一個i2c外設時,都需要大幅度修改源碼、復制源碼、重新調試時序等重復的工作。
在閱讀過Linux設備驅動框架和RT-Thread的驅動框架,發現在總線分層上處理就特別好,完美解決了上述提及的問題。參考RT-Thread和Linux下的模擬i2c,整理修改在裸機上使用。
2.Linux、RT-Thread設備驅動模型
1)模型分為總線驅動和設備驅動;
2) 總線驅動與外設驅動分離,方便一根總線掛多個外設,方便移植;
3) 底層(與硬件相關)與上層分離,方便添加總線及移植到不同處理器,移植到其他處理器,只需重新實現硬件相關的“寄存器”層即可;
3.MCU下裸機形式i2c總線抽象
此部分實現源碼為:i2c_core.c i2c_core.h
“i2c_bus_xfer”為i2c封裝對外的API,函數原型如下,提供一個函數模型,具體需要實例化函數指針。
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)此函數即作為驅動外設的對外接口,所有操作通過此函數接口,與底層總線實現分離,如EEPROM、RTC、溫度傳感器等;
b)一個對外函數已經實現90%的情況使用,對應一些特殊情況,后期再完善或增加API。
c)struct i2c_dev_device *i2c_dev
2)i2c總線抽象API參數
a)i2c_dev:i2c設備指針,類型為“struct i2c_dev_device”,驅動一個i2c外設時,首先要對此指針設備初始化;
b)msgs:i2c一幀數據,發送數據及存放返回數據的緩存;
c)num:數據幀數量。
3)struct i2c_dev_device
該結構體為關鍵,調用API驅動外設時,首先對此初始化(類似于Linux/RT-Thread注冊設備)。完整的設備包括兩部分,數據操作函數和i2c相關信息(如硬件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)第一個參數是函數指針,數據收發通過此函數指針調用實體函數實現;
b)第二個參數是一個void指針,初始化時指向我們使用的物理i2c(硬件/模擬),使用時可強制轉換為對應的類型。
4)xfer
該函數與i2c總線設備對外接口函數“i2c_bus_xfer”具有相同的參數,形參參數參考此項的第2點,初始化時實例化指向實體函數。
5)struct i2c_dev_message
“struct i2c_dev_message”為i2c總線訪問外設的一幀數據信息,包括發送數據、外設從地址、訪問標識等。原型如下:
struct i2c_dev_message { unsigned short addr; unsigned short flags; unsigned short size; unsigned char *buff; unsigned char retries; };a)addr:i2c外設從機地址,常用為7位,10位較少用;
b)flags:標識,發送、接收、應答、地址位選擇等標識;幾種標識如下:
#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:發送的數據大小,或者接收的緩存大小;
d)buff:緩存區;
e)retries:i2c啟動失敗時,重啟的次數。
4.模擬i2c抽象
對于模擬i2c,在以往的實現方式中,基本是時序圖和外設代碼混合在一起,增加外設或者使用新的i2c外設時,需要對模擬i2c代碼進行較大工作量的修改,或者以“復制”的方式實現一套新的i2c總線。
但同理,可以把模擬i2c時序部分代碼抽象出來,以“復用”代碼的形式實現。此部分實現源碼為:i2c_bitops.c i2c_bitops.h
1)模擬i2c抽象對外接口
根據上述封裝的對外API,使用時,首先需要實現入口參數“i2c_dev”實例化,用模擬i2c即是調用模擬i2c相關接口。
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總線時,對外的操作函數都通過上訴函數;i2c信息幀相關參數由上層調用傳遞進入,此處主要增加“struct ops_i2c_dev”的封裝;
b)該函數使用到的函,其中入口參數為“struct ops_i2c_dev”類型的都是模擬i2c相關;
d)模擬i2c封裝實現主要針對“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:數據線輸出;
b)set_scl:時鐘線輸出;
c)get_sda:數據線輸入(捕獲);
d)get_scl:時鐘線輸入(捕獲);
e)delayus:延時函數;
要實現一個模擬i2c,只需將上訴函數指針的實體實現即可,具體看后面描述。
3)模擬i2c時序
以產生i2c起始信號函數為例子,簡要分析:
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); }入口參數為struct ops_i2c_dev * i2c_bus,其實就是i2c_bitops_bus_xfer應用層函數傳入的參數,最終是在此調用,底層需要實現的就是io模擬的輸入/輸出狀態函數。
其他函數,如
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)
等等,入口參數都是i2c_bus,時序實現與常規裸機程序設計是一致的,不同的是函數指針的分離調用,具體看附件源碼。
4)標識位
在以往的模擬i2c或者硬件i2c中,操作外設時都有各類情況,如讀和寫方向的切換、連續操作(不需啟動i2c總線,如寫EEPROM,先寫地址再寫數據)等。對于這類情況,我們處理辦法是選擇相關的宏標識即可,具體實現由“中間層”實現,讓i2c外設驅動起來更簡單!以上述對外函數為例:
a)通過標識位判斷是讀還是寫狀態
if (msg->flags & I2C_BUS_RD) {//read ret = i2c_bitops_bus_read(i2c_bus,msg); if(ret < msg->size) { ret = -1; goto out; } }b)應答狀態標識
ignore_nack = msg->flags & I2C_BUS_IGNORE_NACK;
5)讀寫函數
讀寫函數最終是通過io口1bit的翻轉模擬出時序,從而獲得數據,這部分與常規模擬i2c一致,通過函數指針方式操作。主要實現接口函數:
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總線實現
此部分實現源碼為:i2c_hw.c i2c_hw.h
以stm32f1為硬件平臺,采用上述模擬i2c封裝,實現一根模擬i2c總線。
1)實現struct ops_i2c_dev函數實體
除了“delayus”函數外,其余為io翻轉,以“set_sda”和“delayus”為例,實現如下:
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)為例提高速率,上訴代碼采用寄存器方式操作,可以用庫函數操作io口;
b)延時可以用硬件定時器延時,或者軟件延時,具體根據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設備實例化,其中“ops_i2c1_dev”和“i2c1_dev”即是我們定義的總線設備,后面使用該總線時主要通過“i2c1_dev”實現對底層的調用。
6.驅動EEPROM(AT24C16)
此部分實現源碼為:24clxx.c 24clxx.h
上面總線完成后,驅動一個i2c外設可以說就是信手拈來的事情了,而且模擬i2c總線抽象出來后,不需在做重復調試時序的工作。
假設初始化的i2c設備為i2c1_dev。
1)寫EEPROM
寫一個字節,頁寫算法詳細見源碼附件(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)注意事項
驅動一個外設相對容易了,注意的事項就是標識位部分。
a)此處外設地址(addr),是實際地址,不含讀寫位(7bit),比如AT24C16外設地址為0x50,可能大家平常用的是0xA0,因為包括讀寫位;
b)寫數據時,如果以2幀i2c_dev_message消息發送,需要注意“I2C_BUS_NO_START”宏,此宏標識意思是不需要再次啟動i2c了,一般看i2c外設手冊時序圖可知道。如寫EEPROM是先寫地址,然后寫數據這個過程是連續的,此時就需用到“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的驅動框架的,再驅動其他i2c外設,就是很容易的事情了,剩下的就是配置寄存器、應用算法的問題了。
7.總結
1)整體思路比較易理解,本質就是函數指針,將與硬件底層無關的部分抽象出來,相關聯的地方分層明確,通過函數指針的方式進行調用。
2)事務分離,通用、重復的事情交給總線處理,特殊任務留給外設驅動。
審核編輯:湯梓紅
-
mcu
+關注
關注
146文章
17357瀏覽量
352790 -
Linux
+關注
關注
87文章
11351瀏覽量
210499 -
STM32
+關注
關注
2273文章
10926瀏覽量
357770 -
I2C
+關注
關注
28文章
1495瀏覽量
124705 -
串行總線
+關注
關注
1文章
183瀏覽量
30717
原文標題:STM32指針抽象出I2C的數據實例,附代碼
文章出處:【微信號:c-stm32,微信公眾號:STM32嵌入式開發】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
《振南電子STM32視頻教程》第八講:I2C總線
一文知道32位指針內部區域尋址
i2c總線用來做什么_i2c總線數據傳輸過程
STM32學習之I2C協議(讀寫EEPROM)
![<b class='flag-5'>STM32</b>學習之<b class='flag-5'>I2C</b>協議(讀寫EEPROM)](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
STM32的硬件I2C有BUG嗎?
![<b class='flag-5'>STM32</b>的硬件<b class='flag-5'>I2C</b>有BUG嗎?](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
I2C接口配置ES7243錄音芯片,MCU(STM32)收不到I2C ACK的問題
![<b class='flag-5'>I2C</b>接口配置ES7243錄音芯片,MCU(<b class='flag-5'>STM32</b>)收不到<b class='flag-5'>I2C</b> ACK的問題](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
STM32F103硬件I2C Slave
![<b class='flag-5'>STM32</b>F103硬件<b class='flag-5'>I2C</b> Slave](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
硬件I2C與模擬I2C
![硬件<b class='flag-5'>I2C</b>與模擬<b class='flag-5'>I2C</b>](https://file.elecfans.com/web1/M00/D9/4E/pIYBAF_1ac2Ac0EEAABDkS1IP1s689.png)
I2C子系統SW Architecture
![<b class='flag-5'>I2C</b>子系統SW Architecture](https://file1.elecfans.com/web2/M00/8D/81/wKgaomS7jI6AcVIDAAFQF5uK5G8249.jpg)
評論