来源:嵌入式悦翔园
2023-06-16 12:15:22
一、前言
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_nr<0){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(irq<0){dev_err(&pdev->dev,"cannotfindrk3xIRQ");returnirq;}ret=devm_request_irq(&pdev->dev,irq,rk3x_i2c_irq,0,dev_name(&pdev->dev),i2c);if(ret<0){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(ret<0){dev_err(&pdev->dev,"Can"tpreparebusclk:%d",ret);returnret;}ret=clk_prepare(i2c->pclk);if(ret<0){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(ret<0){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_REPORTED1intnr;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*/#endifvoid*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或者芯片厂封装好了,我们需要做的就是使用他们提供的这些接口完成指定设备的读写操作,但是我们的学习不能止步于此,所以我们不但要会用,还要知其然知其所以然。
关于I2C驱动的知识我也是才疏学浅,也有很多地方不是很了解,关于上面的知识点也只是我的一些理解,如果有不对的地方欢迎大家指出,我们一起交流学习。
审核编辑:汤梓红
关键词: