一、前言
I2C協議是在開發中使用非常頻繁的一種協議,相信大家在學習單片機的時候經常會用到支持I2C協議的模塊,I2C 總線僅僅使用 SCL、SDA 這兩根信號線就實現了設備之間的數據交互,極大地簡化了對硬件資源和 PCB 板布線空間的占用。因此,I2C 總線被非常廣泛地應用在 EEPROM、實時鐘、小型 LCD 等設備與 CPU 的接口中。
但是與裸機開發不同的是在 Linux 系統中,I2C 驅動由 3 部分組成,即 I2C 核心、I2C 總線驅動和 I2C 設備驅動。今天就從這三個部分來給大家講解一下Linux中的I2C驅動,以及我們應該如何為我們的開發板添加一個I2C設備。
二、Linux 的 I2C 體系結構
由上面分析可知,Linux驅動分為三部分:I2C 核心、I2C 總線驅動和 I2C 設備驅動

2.1 Linux I2C 核心
I2C 核心提供了 I2C 總線驅動和設備驅動的注冊、注銷方法,這部分主要是一些與硬件無關的的接口函數,這部分的代碼一般不用我們普通開發者進行開發和修改,但是理解這部分的代碼邏輯和接口還是非常必要的。
I2C 核心中的主要函數如下:
注冊/注銷適配器(adapter) inti2c_add_adapter(structi2c_adapter*adap); inti2c_del_adapter(structi2c_adapter*adap); 注冊/注銷I2C設備驅動程序 inti2c_register_driver(structmodule*owner,structi2c_driver*driver); inti2c_del_driver(structi2c_driver*driver); inlineinti2c_add_driver(structi2c_driver*driver); 創建并注冊一個新的I2C設備 structi2c_client*i2c_new_device(structi2c_adapter*adap,structi2c_board_infoconst*info); I2C傳輸、發送和接收 inti2c_transfer(structi2c_adapter*adap,structi2c_msg*msgs,intnum); inti2c_master_send(structi2c_client*client,constchar*buf,intcount); inti2c_master_recv(structi2c_client*client,char*buf,intcount);
上邊三個函數用于實現與I2C設備之間的數據交換。i2c_transfer函數可以進行復雜的多消息傳輸,而i2c_master_send和i2c_master_recv函數用于單個數據消息的發送和接收。
這些函數提供了對于I2C總線讀寫操作的基本支持,簡化了I2C設備驅動的開發,有了這些接口我們就不用關注I2C協議方面的代碼了,只需要調用該接口即可完成數據的傳輸。
注意: i2c_transfer函數本身不具備驅動適配器物理硬件完成消息交互的能力,它只是尋找到 i2c_adapter 對應的 i2c_algorithm,并使用 i2c_algorithm 的 master_xfer函數真正驅動硬件流程。
2.2 Linux I2C 適配器驅動
通過上面的介紹我們知道了I2C驅動主要分為三個部分,上面我們已經介紹了I2C核心這一部分,現在我們來介紹一下I2C 適配器驅動,我們知道I2C驅動和其他的那些字符設備驅動有所不同,I2C驅動中維持著一套自己的總線。
I2C 適配器驅動是Linux內核中的一個核心模塊,總線層負責管理所有注冊到系統的I2C總線適配器和設備,并提供與設備通信的API函數。它提供了一些基本的操作函數,如啟動總線、停止總線、發送起始信號、發送停止信號等。但是這部分是由Linux內核完成的,并不需要我們開發者進行修改或添加,所以了解即可。
下面我們用一張圖來看一下上面描述的這個過程:

2.3 Linux I2C 設備驅動
I2C 設備驅動要使用 i2c_driver 和 i2c_client 數據結構并填充其中的成員函數。 i2c_client 一般被包含在設備的私有信息結構體 yyy_data 中,而 i2c_driver 則適合被定義為全局變量并初始化。
看到I2C設備驅動的這兩個結構體大家是不是很熟悉了,I2C設備驅動是針對特定類型的I2C設備編寫的驅動程序。它包含了對具體設備的操作和控制邏輯,通過調用I2C總線核心驅動提供的API函數與設備進行通信。設備驅動的主要任務包括初始化設備、讀寫數據、配置設備參數等。
因為這部分是針對特定類型的I2C設備編寫的驅動程序,所以這部分才是要我們開發人員來完成編寫的,我們如果需要在自己的開發板上添加一個新的I2C模塊,我們就要首先編寫I2C設備驅動這部分,這部分的編寫需要調用上面我們介紹的I2C核心和I2C總線中接口函數來完成模塊的初始化。
關于I2C設備驅動我們這里先做一個了解即可,后面會詳細介紹這部分的內容,也是我們學習I2C驅動的重點內容。
2.4 Linux I2C驅動總結
I2C總線核心驅動(I2C Core Driver):【系統廠編寫】I2C總線核心驅動是Linux內核中的一個核心模塊,負責管理所有注冊到系統的I2C總線適配器和設備,并提供與設備通信的API函數。它提供了一些基本的操作函數,如啟動總線、停止總線、發送起始信號、發送停止信號等。
I2C適配器驅動(I2C Adapter Driver):【芯片廠提供】I2C適配器驅動負責與硬件的I2C控制器進行交互,完成硬件層面的初始化、配置和操作。它將底層硬件的特定接口與I2C總線核心驅動進行連接,使得核心驅動能夠通過適配器驅動來訪問硬件。
I2C設備驅動(I2C Device Driver):【開發者編寫】I2C設備驅動是針對特定類型的I2C設備編寫的驅動程序。它包含了對具體設備的操作和控制邏輯,通過調用I2C總線核心驅動提供的API函數與設備進行通信。設備驅動的主要任務包括初始化設備、讀寫數據、配置設備參數等。
三部分之間的關系如下:
I2C核心層驅動作為頂層驅動,管理整個I2C子系統,并提供了基本的I2C操作接口。
I2C適配器驅動負責與底層硬件的I2C控制器進行交互,通過適配器驅動,I2C總線核心驅動能夠與硬件進行通信。
I2C設備驅動則針對具體的I2C設備編寫,實現了對設備的初始化、讀寫數據等操作。

三、具體設備驅動分析
由于作為開發者我們需要關注并且需要我們親自編寫的部分就只有設備驅動了,所以我們今天就詳細介紹一下設備驅動這部分。
當我們需要編寫具體的I2C設備驅動程序時,我們需要編寫以下內容:**probe函數、remove函數、操作函數以及數據傳輸與處理**,下面將對每部分進行詳細介紹。
3.1 Probe函數
具體設備中的probe函數是I2C設備驅動中最重要的函數之一,用于在I2C設備與驅動匹配成功后進行初始化和注冊設備。在probe函數中,可以執行以下任務:
進行設備的特定初始化操作,例如配置設備寄存器、申請內存資源等。
注冊字符設備、輸入設備或其他設備類別,使系統能夠識別和使用該設備。
存儲設備私有數據,通常使用i2c_set_clientdata函數將私有數據與i2c_client相關聯,方便后續的操作函數訪問。
我們在學習其他設備驅動的時候就知道了probe函數是設備與驅動匹配成功后被調用執行的。它的原型通常如下所示:
staticinti2c_device_probe(structi2c_client*client,conststructi2c_device_id*id);
下面我們就找一個設備驅動來分析一下我們應該如何編寫:
這里以rk3x_i2c_probe為例給大家進行分析:
staticintrk3x_i2c_probe(structplatform_device*pdev)
{
structdevice_node*np=pdev->dev.of_node;
conststructof_device_id*match;
structrk3x_i2c*i2c;
structresource*mem;
intret=0;
intbus_nr;
u32value;
intirq;
unsignedlongclk_rate;
i2c=devm_kzalloc(&pdev->dev,sizeof(structrk3x_i2c),GFP_KERNEL);
if(!i2c)
return-ENOMEM;
match=of_match_node(rk3x_i2c_match,np);
i2c->soc_data=(structrk3x_i2c_soc_data*)match->data;
/*usecommoninterfacetogetI2Ctimingproperties*/
i2c_parse_fw_timings(&pdev->dev,&i2c->t,true);
strlcpy(i2c->adap.name,"rk3x-i2c",sizeof(i2c->adap.name));
i2c->adap.owner=THIS_MODULE;
i2c->adap.algo=&rk3x_i2c_algorithm;
i2c->adap.retries=3;
i2c->adap.dev.of_node=np;
i2c->adap.algo_data=i2c;
i2c->adap.dev.parent=&pdev->dev;
i2c->dev=&pdev->dev;
spin_lock_init(&i2c->lock);
init_waitqueue_head(&i2c->wait);
i2c->i2c_restart_nb.notifier_call=rk3x_i2c_restart_notify;
i2c->i2c_restart_nb.priority=128;
ret=register_i2c_restart_handler(&i2c->i2c_restart_nb);
if(ret){
dev_err(&pdev->dev,"failedtosetupi2crestarthandler.
");
returnret;
}
mem=platform_get_resource(pdev,IORESOURCE_MEM,0);
i2c->regs=devm_ioremap_resource(&pdev->dev,mem);
if(IS_ERR(i2c->regs))
returnPTR_ERR(i2c->regs);
/*TrytosettheI2Cadapternumberfromdt*/
bus_nr=of_alias_get_id(np,"i2c");
/*
*SwitchtonewinterfaceiftheSoCalsoofferstheoldone.
*ThecontrolbitislocatedintheGRFregisterspace.
*/
if(i2c->soc_data->grf_offset>=0){
structregmap*grf;
grf=syscon_regmap_lookup_by_phandle(np,"rockchip,grf");
if(IS_ERR(grf)){
dev_err(&pdev->dev,
"rk3x-i2cneeds'rockchip,grf'property
");
returnPTR_ERR(grf);
}
if(bus_nr0)?{
???dev_err(&pdev->dev,"rk3x-i2cneedsi2cXalias");
return-EINVAL;
}
/*27+i:writemask,11+i:value*/
value=BIT(27+bus_nr)|BIT(11+bus_nr);
ret=regmap_write(grf,i2c->soc_data->grf_offset,value);
if(ret!=0){
dev_err(i2c->dev,"CouldnotwritetoGRF:%d
",ret);
returnret;
}
}
/*IRQsetup*/
irq=platform_get_irq(pdev,0);
if(irq0)?{
??dev_err(&pdev->dev,"cannotfindrk3xIRQ
");
returnirq;
}
ret=devm_request_irq(&pdev->dev,irq,rk3x_i2c_irq,
0,dev_name(&pdev->dev),i2c);
if(ret0)?{
??dev_err(&pdev->dev,"cannotrequestIRQ
");
returnret;
}
platform_set_drvdata(pdev,i2c);
if(i2c->soc_data->calc_timings==rk3x_i2c_v0_calc_timings){
/*Onlyoneclocktouseforbusclockandperipheralclock*/
i2c->clk=devm_clk_get(&pdev->dev,NULL);
i2c->pclk=i2c->clk;
}else{
i2c->clk=devm_clk_get(&pdev->dev,"i2c");
i2c->pclk=devm_clk_get(&pdev->dev,"pclk");
}
if(IS_ERR(i2c->clk)){
ret=PTR_ERR(i2c->clk);
if(ret!=-EPROBE_DEFER)
dev_err(&pdev->dev,"Can'tgetbusclk:%d
",ret);
returnret;
}
if(IS_ERR(i2c->pclk)){
ret=PTR_ERR(i2c->pclk);
if(ret!=-EPROBE_DEFER)
dev_err(&pdev->dev,"Can'tgetperiphclk:%d
",ret);
returnret;
}
ret=clk_prepare(i2c->clk);
if(ret0)?{
??dev_err(&pdev->dev,"Can'tpreparebusclk:%d
",ret);
returnret;
}
ret=clk_prepare(i2c->pclk);
if(ret0)?{
??dev_err(&pdev->dev,"Can'tprepareperiphclock:%d
",ret);
gotoerr_clk;
}
i2c->clk_rate_nb.notifier_call=rk3x_i2c_clk_notifier_cb;
ret=clk_notifier_register(i2c->clk,&i2c->clk_rate_nb);
if(ret!=0){
dev_err(&pdev->dev,"Unabletoregisterclocknotifier
");
gotoerr_pclk;
}
clk_rate=clk_get_rate(i2c->clk);
rk3x_i2c_adapt_div(i2c,clk_rate);
ret=i2c_add_adapter(&i2c->adap);
if(ret0)?{
??dev_err(&pdev->dev,"Couldnotregisteradapter
");
gotoerr_clk_notifier;
}
dev_info(&pdev->dev,"InitializedRK3xxxI2Cbusat%p
",i2c->regs);
return0;
err_clk_notifier:
clk_notifier_unregister(i2c->clk,&i2c->clk_rate_nb);
err_pclk:
clk_unprepare(i2c->pclk);
err_clk:
clk_unprepare(i2c->clk);
returnret;
}
從上面的代碼我們可以發現rk3x_i2c_probe主要做了以下幾件事情:
1、通過devm_kzalloc函數為rk3x_i2c結構體分配內存空間; 2、從設備樹中獲取I2C設備信息并填充rk3x_i2c結構體; 3、使用devm_platform_ioremap_resource函數來映射設備的寄存器資源到內存中; 4、獲取并配置中斷; 5、使用i2c_add_adapter注冊設備
基本上這個驅動就是一個比較完整的I2C設備初始化流程了,我們如果想要編寫其他設備的驅動可以參考該驅動初始化來進行編寫。
3.2 讀寫函數
由于rk3x_i2c中的讀寫函數和該設備關聯性較大,不具備通用性,這里以sx1_i2c_write_byte和sx1_i2c_read_byte來給大家進行分析,該函數更具有通用性。
/*WritetoI2Cdevice*/
intsx1_i2c_write_byte(u8devaddr,u8regoffset,u8value)
{
structi2c_adapter*adap;
interr;
structi2c_msgmsg[1];
unsignedchardata[2];
adap=i2c_get_adapter(0);
if(!adap)
return-ENODEV;
msg->addr=devaddr;/*I2Caddressofchip*/
msg->flags=0;
msg->len=2;
msg->buf=data;
data[0]=regoffset;/*registernum*/
data[1]=value;/*registerdata*/
err=i2c_transfer(adap,msg,1);
i2c_put_adapter(adap);
if(err>=0)
return0;
returnerr;
}
/*ReadfromI2Cdevice*/
intsx1_i2c_read_byte(u8devaddr,u8regoffset,u8*value)
{
structi2c_adapter*adap;
interr;
structi2c_msgmsg[1];
unsignedchardata[2];
adap=i2c_get_adapter(0);
if(!adap)
return-ENODEV;
msg->addr=devaddr;/*I2Caddressofchip*/
msg->flags=0;
msg->len=1;
msg->buf=data;
data[0]=regoffset;/*registernum*/
err=i2c_transfer(adap,msg,1);
msg->addr=devaddr;/*I2Caddress*/
msg->flags=I2C_M_RD;
msg->len=1;
msg->buf=data;
err=i2c_transfer(adap,msg,1);
*value=data[0];
i2c_put_adapter(adap);
if(err>=0)
return0;
returnerr;
}
從上面的代碼可以看出,sx1_i2c_write_byte主要完成了以下功能:
1、通過調用i2c_get_adapter(0)函數獲取指定索引的I2C適配器對象并賦值給adap變量。 2、初始化一個structi2c_msg類型的數組msg,該數組包含一個元素用于I2C消息的傳輸。 3、設置msg結構體中的字段: addr:設備的I2C地址。 flags:傳輸標志位,此處為0表示寫操作。 len:要傳輸的字節數,此處設置為2,即寄存器地址和寄存器數據兩個字節。 buf:數據緩沖區的指針,用于存儲要發送的數據。 4、將要寫入的設備寄存器地址和數據分別存儲在data數組的第一個和第二個元素中,即data[0]=regoffset;和data[1]=value;。 5、調用i2c_transfer()函數進行I2C消息傳輸,將數據寫入設備寄存器。 6、使用i2c_put_adapter()函數釋放先前獲取的I2C適配器對象。
sx1_i2c_read_byte主要完成了以下功能:
1、通過調用i2c_get_adapter(0)函數獲取指定索引的I2C適配器對象并賦值給adap變量。 2、初始化一個structi2c_msg類型的數組msg,該數組包含一個元素用于I2C消息的傳輸。 3、設置msg結構體中的字段: addr:設備的I2C地址。 flags:傳輸標志位,此處為0表示寫操作。 len:要傳輸或接收的字節數。 buf:數據緩沖區的指針,用于存儲要發送或接收的數據。 4、將要讀取的設備寄存器地址存儲在data數組的第一個元素中,即data[0]=regoffset;。 5、調用i2c_transfer()函數進行I2C消息傳輸,將數據寫入設備寄存器。 6、更改flags字段為I2C_M_RD,表示接收模式(讀操作)。 7、再次調用i2c_transfer()函數進行I2C消息傳輸,從設備中讀取數據。 8、將讀取到的數據存儲在data數組的第一個元素中,即*value=data[0];。 9、使用i2c_put_adapter()函數釋放先前獲取的I2C適配器對象。
對比I2C讀和寫的過程大家可能會發現I2C讀的過程為什么調用了兩次i2c_transfer函數呢?多調用了一次i2c_transfer函數是因為我們在調用i2c_transfer讀取數據時,需要先發送要讀取的寄存器地址給設備,然后再從設備讀取實際的數據。所以第一次使用i2c_transfer發送的信息為需要讀取的地址信息,第二次將標志位改為讀,然后使用i2c_transfer將從設備返回的信息存儲到i2c_adapter中。
四、I2C驅動中幾個重要的結構體
在I2C驅動中,有三個比較重要的結構體用于描述和管理I2C設備和傳輸操作。下面就這三個結構體的成員以及作用來給大家講解一下:
4.1 i2c_adapter 結構體
定義位置:i2c.h結構體原型:
structi2c_adapter{
structmodule*owner;
unsignedintclass;/*classestoallowprobingfor*/
conststructi2c_algorithm*algo;/*thealgorithmtoaccessthebus*/
void*algo_data;
/*datafieldsthatarevalidforalldevices*/
conststructi2c_lock_operations*lock_ops;
structrt_mutexbus_lock;
structrt_mutexmux_lock;
inttimeout;/*injiffies*/
intretries;
structdevicedev;/*theadapterdevice*/
unsignedlonglocked_flags;/*ownedbytheI2Ccore*/
#defineI2C_ALF_IS_SUSPENDED0
#defineI2C_ALF_SUSPEND_REPORTED1
intnr;
charname[48];
structcompletiondev_released;
structmutexuserspace_clients_lock;
structlist_headuserspace_clients;
structi2c_bus_recovery_info*bus_recovery_info;
conststructi2c_adapter_quirks*quirks;
structirq_domain*host_notify_domain;
structregulator*bus_regulator;
};
幾個重要的成員:
name:適配器的名稱。 nr:適配器的編號。 bus_lock和bus_unlock:用于保護對適配器的并發訪問的鎖機制。 algo:指向I2C算法結構體的指針,包含了適配器的通信算法,如標準模式、快速模式、高速模式等。
4.2 i2c_client 結構體
定義位置:i2c.h結構體原型:
structi2c_client{
unsignedshortflags;/*div.,seebelow*/
#defineI2C_CLIENT_PEC0x04/*UsePacketErrorChecking*/
#defineI2C_CLIENT_TEN0x10/*wehaveatenbitchipaddress*/
/*MustequalI2C_M_TENbelow*/
#defineI2C_CLIENT_SLAVE0x20/*wearetheslave*/
#defineI2C_CLIENT_HOST_NOTIFY0x40/*WewanttouseI2Chostnotify*/
#defineI2C_CLIENT_WAKE0x80/*forboard_info;trueiffcanwake*/
#defineI2C_CLIENT_SCCB0x9000/*UseOmnivisionSCCBprotocol*/
/*MustmatchI2C_M_STOP|IGNORE_NAK*/
unsignedshortaddr;/*chipaddress-NOTE:7bit*/
/*addressesarestoredinthe*/
/*_LOWER_7bits*/
charname[I2C_NAME_SIZE];
structi2c_adapter*adapter;/*theadapterwesiton*/
structdevicedev;/*thedevicestructure*/
intinit_irq;/*irqsetatinitialization*/
intirq;/*irqissuedbydevice*/
structlist_headdetected;
#ifIS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_tslave_cb;/*callbackforslavemode*/
#endif
void*devres_group_id;/*IDofprobedevresgroup*/
};
幾個重要的成員:
flags:標志位,用于指定設備的特性和行為。 addr:設備的I2C地址。 adapter:指向i2c_adapter的指針,表示所屬的I2C適配器。 driver:指向設備驅動程序的指針,表示設備所使用的驅動。
4.3 i2c_driver 結構體
定義位置:i2c.h結構體原型:
structi2c_driver{
unsignedintclass;
union{
/*Standarddrivermodelinterfaces*/
int(*probe)(structi2c_client*client);
/*
*Legacycallbackthatwaspartofaconversionof.probe().
*Todayithasthesamesemanticas.probe().Don'tusefornew
*code.
*/
int(*probe_new)(structi2c_client*client);
};
void(*remove)(structi2c_client*client);
/*drivermodelinterfacesthatdon'trelatetoenumeration*/
void(*shutdown)(structi2c_client*client);
/*Alertcallback,forexamplefortheSMBusalertprotocol.
*Theformatandmeaningofthedatavaluedependsontheprotocol.
*FortheSMBusalertprotocol,thereisasinglebitofdatapassed
*asthealertresponse'slowbit("eventflag").
*FortheSMBusHostNotifyprotocol,thedatacorrespondstothe
*16-bitpayloaddatareportedbytheslavedeviceactingasmaster.
*/
void(*alert)(structi2c_client*client,enumi2c_alert_protocolprotocol,
unsignedintdata);
/*aioctllikecommandthatcanbeusedtoperformspecificfunctions
*withthedevice.
*/
int(*command)(structi2c_client*client,unsignedintcmd,void*arg);
structdevice_driverdriver;
conststructi2c_device_id*id_table;
/*Devicedetectioncallbackforautomaticdevicecreation*/
int(*detect)(structi2c_client*client,structi2c_board_info*info);
constunsignedshort*address_list;
structlist_headclients;
u32flags;
};
幾個重要的成員:
driver:是一個structdevice_driver結構體,用于向Linux設備模型注冊驅動程序。 probe和remove:指向探測和移除設備的函數指針,通過這兩個函數,驅動程序可以在發現匹配的設備時執行初始化操作,并在設備被移除時執行清理操作。 id_table:用于指定驅動程序支持的I2C設備ID列表,以便匹配對應的設備。
這些結構體共同構成了Linux內核中的I2C驅動框架,提供了對I2C總線、適配器和設備的抽象和管理功能。開發者可以基于這些結構體來編寫自己的I2C驅動程序,并實現與I2C設備的通信和控制。所以我們的工作就是填充這些結構體然后調用對應的接口把我們填充好的結構體傳遞給I2C設備器驅動和核心驅動從而完成設備的初始化和讀寫操作。
五、總結
I2C驅動的學習有一個特點:弄懂比較難,會用比較簡單,這是因為有很多的有難度的內容以及和協議相關的內容都已經被Linux或者芯片廠封裝好了,我們需要做的就是使用他們提供的這些接口完成指定設備的讀寫操作,但是我們的學習不能止步于此,所以我們不但要會用,還要知其然知其所以然。
審核編輯:劉清
-
驅動器
+關注
關注
54文章
9083瀏覽量
155542 -
SDA
+關注
關注
0文章
125瀏覽量
29611 -
I2C協議
+關注
關注
0文章
29瀏覽量
9223 -
Linux驅動
+關注
關注
0文章
47瀏覽量
10482
原文標題:Linux驅動:I2C驅動學習看這一篇就夠了
文章出處:【微信號:嵌入式悅翔園,微信公眾號:嵌入式悅翔園】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
GPIO模擬I2C總線的驅動設計與實現
I2C LCD 器件通過驅動帶有 I2C 接口的 2 線式 16 字符 LCD
Linux的I2C驅動架構
I2C通信協議應該如何學習
I2C系列的合集,可以系統學習I2C協議
STM32學習之I2C協議(讀寫EEPROM)
Linux驅動:I2C設備驅動(基于Freescale i.MX6ULL平臺了解I2C的驅動框架,順便寫個簡陋的MPU6050驅動)
嵌入式內核及驅動開發-09IIC子系統框架使用(I2C協議和時序,I2C驅動框架,I2C從設備驅動開發,MPU6050硬件連接
ESP32 之 ESP-IDF 教學(六)——I2C數據總線(I2C)
I2C驅動學習看這一篇就夠了
評論