I2C 驱动开发 实例

一、        开发背景

开发环境:DM355开发板

内核版本:2.6.10

二、        BQ27501驱动开发的需求

BQ27501是一个锂电池管理的芯片,可以向外提供锂电池的有关的信息。在用户空间用户不能直接访问bq27501的寄存器,所以要为其编写驱动,该驱动能够根据用户空间的命令,返回对应的电池信息。

三、        I2C 驱动的架构

bq27501是通过I2C总线与DM355通信的,故bq27501的驱动实际上就是bq27501的I2C读写的驱动。

Linux内核中的 i2c 驱动程序可以分为三个层次,如下图所示:



图1 Linux I2C体系结构

 

1)  i2c 驱动框架

i2c框架主要有i2c.h和 i2c-core.c文件实现。它们定义驱动中使用的核心数据结构,完成 i2c适配器和设备驱动的注册,注销,并且实现 i2c驱动的algorithm。 i2c驱动中的algorithm与适配器无关,它属于上层代码,还包括探测设备、检测设备地址的上层代码。另外,i2c-dev.c还用于对 i2c设备节点的创建,并完成其访问方法的实现等。

2)  i2c 总线驱动

总线驱动的职责,是为系统中每个I2C总线增加相应的读写方法。但是总线驱动本身并不会进行任何的通讯,它只是存在那里,等待设备驱动调用其函数。在系统开机时,首先装载的是I2C总线驱动。一个总线驱动用于支持一条特定的I2C总线的读写。这部分主要定义i2c_adapter和i2c_algorithm数据结构,前者用来描述具体的 i2c 总线适配器,后者则描述i2c 总线的通信方法。一般总线驱动由平台提供实现,davinci的i2c总线驱动在i2c-davinci.c文件中定义了,然后通过i2c_davinci_init函数调用i2c_add_adapter(i2c_davinci_adap)将这个两个模块注册到操作系统中,总线驱动就装上了。

static struct i2c_adapter i2c_davinci_adap = {
	.owner = THIS_MODULE,
	.name = "DAVINCI I2C adapter",
	.id = I2C_ALGO_EXP,
	.algo = &i2c_davinci_algo,
	.algo_data = NULL,
	.client_register = NULL,
	.client_unregister = NULL,
};
static struct i2c_algorithm i2c_davinci_algo = {
	.name = "DAVINCI I2C algorithm",
	.id = I2C_ALGO_EXP,
	.master_xfer = i2c_davinci_xfer,
	.smbus_xfer = NULL,
	.slave_send = NULL,
	.slave_recv = NULL,
	.algo_control = NULL,
	.functionality = i2c_davinci_func,
};


3)  i2c 设备驱动

设备驱动则是与挂在I2C总线上的具体的设备通讯的驱动。通过I2C总线驱动提供的函数,设备驱动可以忽略不同总线控制器的差异,不考虑其实现细节地与硬件设备通讯。它实现对具体的i2c设备的描述,另外还包括一些可能用到的数据结构。它借助 i2c框架中的 i2c_probe函数实现设备的attach_adapter方法,完成设备检测成功后i2c_client 数据结构回调函数的创建。实际操作过程中可以跳过i2c_probe函数直接调用实现i2c_client的函数,这样可以不必遵循调用i2c_probe固有的参数格式,从而可以提高效率和节省存储空间。bq27501的驱动加载流程如下图2所示。

 

图2 bq27501的i2c驱动加载流程图

 

bq27501_init()函数为该驱动模块的模块初始化函数,当在linux下使用insmod命令装载该模块时会执行该函数。在初始化函数中,对i2c_driver结构体变量初始化,然后调用register_chrdev()函数注册字符设备。接着调用i2c_add_driver()函数添加一个i2c的driver。i2c_add_driver函数的执行会引发i2c_driver中attach_adapter指向的函数bq27501_i2c_probe_adapter()函数的执行,该函数是用来探测物理设备的。它需要通过调用_i2c_attach_client函数来实现探测,并且在_i2c_attach_client函数内调用i2c_attach_client函数在总线上附加一个新的client;或者调用i2c-core.c中的i2c_probe函数,由i2c_probe函数再调用探测物理设备的函数,bq27501驱动中是采用的第一种方式。

四、        实现I2C驱动编写方式

最新的内核支持两种编写i2c驱动的方式,一个是“Adapter方式(legacy)”,另一个是“Probe方式(new style)”。两种方式的区别在于i2c_driver结构体不同,“Adapter方式”的i2c_driver结构是:

struct i2c_driver {
      structmodule *owner;
      charname[32];
      intid;
      unsignedint class;
      unsignedint flags; 
      int(*attach_adapter)(struct i2c_adapter*);
      int(*detach_adapter)(struct i2c_adapter*);
      int(*detach_client)(struct i2c_client*);
      int(*command)(struct i2c_client*client,unsigned int cmd, void *arg);
      structdevice_driver driver;
      structlist_head list;
};


“Probe方式”的i2c_driver结构是: 

struct i2c_driver {
      unsignedint class;
      int(*attach_adapter)(struct i2c_adapter*);
      int(*detach_adapter)(struct i2c_adapter*);
      int (*probe)(struct i2c_client *, const struct i2c_device_id *);
      int (*remove)(struct i2c_client *);
      void(*shutdown)(struct i2c_client *);
      int(*suspend)(struct i2c_client *,pm_message_t mesg);
      int(*resume)(struct i2c_client *);
      int(*command)(struct i2c_client*client, unsigned int cmd, void *arg);
      structdevice_driver driver;
      const struct i2c_device_id *id_table;
      int(*detect)(struct i2c_client *,int kind, struct i2c_board_info*);
      conststruct i2c_client_address_data*address_data;
      structlist_head clients;
};


两种方式i2c_driver结构主要的不同是后者添加了probe和remove函数指针和id_table。bq27501使用的Ti-davinci内核是较早版本的,只支持Adapter方式,不支持Probe方式,所以bq27501采用的是Adapter方式。Adapter方式编写的流程就是上节中所描述的i2c驱动加载的流程相同。至于Probe方式,在本次驱动编写中没有使用,所以在此不作详细的介绍。两种方式的对比可以参加网页资料http://www.embedu.org/Column/Column213.htm

五、        BQ27501的I2C驱动编写

要使bq27501的i2c驱动模块能够运行,必须至少要编写两个文件,第一个是驱动的源文件,第二个是编译源文件的Makefile文件。另外该驱动的两个关键点是i2c通信和与用户空间交互数据。

1)  Makefile文件编写

#如果已定义KERNELRELEASE,则说明是从内核构造系统调用的,因此可以利用其内建语句。

ifneq($(KERNELRELEASE),)

       obj-m := bq27501.o

#否则,是直接从命令开始调用的,这时要调用内核构造系统。

 

else

KDIR ?=/home/zl/ti-davinci

PWD := $(shellpwd)

CROSS_COMPILE=arm-v5t-le-

CC=$(CROSS_COMPILE)gcc

default:

       make -C $(KDIR) M=$(PWD) modules

endif

clean:

       rm -rf *.o *.cmd *.mod.c *.sysmvers

 

注:

1. Makefile文件的文件名中M一定要大写。这是因为编译的时候首先看环境变量KERNELRELEASE是否定义,如果没定义则调用Linux内核编译build脚本。该脚本会首先编译内核,其间会创建环境变量KERNELRELEASE,接着编译当前工作目录下的hello模块,此时会第二遍读取Makefile,再次判断环境变量KERNELRELEASE是否定义,已经定义的情况下开始编译hello模块。

2. Makefile文件中的命令行,以Tab键开头(不能是空格),例如make,clean。依赖条件顶格,例如default,clean。

3. KDIR:指向嵌入系统的linux内核,而不是正在运行的系统的内核。

4.CROSS_COMPILE:交叉编译环境,也就是安装的dvdsdk。

5. CC:指明编译器,加上CROSS_COMPILE,定义了一个交叉编译器。

6. clean:当时使用makeclean命令时清除编译的结果。

7. default:当使用make命令后面不加任何参数时,默认执行的语句。

将驱动代码的源文件和Makefile文件放到同一个文件夹下面。在终端进入到目录下,使用make命令对其进行编译。

 

2)  模块编程

      为了bq27501驱动测试的方便,所以采用模块编程的方式来实现。将bq27501驱动作为一个内核模块动态加载到内核中,而不是采用在内核树中添加代码实现这种静态的方法实现的。

第一,模块编程的程序的头文件。

#include <linux/init.h>

#include <linux/module.h>

#include <linux/kernel.h>

这三个头文件是编写内核模块程序必须包含的3个头文件。

        第二,内核模块必须包含两个函数。一个是加载时模块初始化函数,另一个是模块卸载时卸载的函数。bq27501的初始化函数为staticint __initbq27501_init(void);module_init(bq27501_init);初始化函数声明为static,因为初始化函数在特定文件文件之外没有其他意义,并且驱动初始化函数前面加上__init标记,这表明该函数仅在初始化期间使用。在模块被装载之后,模块装载器就会将初始化函数扔掉,这样可以将该函数占用的内存释放出来以作他用(注,不能在初始化之后仍要使用的函数,或者数据结构)。module_init这个宏会在模块的目标代码中增加一个特殊的段,用于说明内核初始化函数所在的位置。如果没有这个定义,那么初始化函数bq27501将不会被调用。所以是必须使用的module_init。bq27501的卸载函数为staticvoid __exit bq27501_exit(void);module_exit(bq27501_exit);该函数在模块被卸载前注销接口并向系统中返回所有资源。卸载函数没有返回值,故要声明为void。__exit标记表示该代码用于模块卸载。如果模块被直接内核的配置不允许卸载模块,则被标记为__exit的函数将被简单地丢弃。所以被标记为__exit的函数只能在模块被卸载或者系统关闭时被调用,其他任何用法都是错误的。和module_init类似,module_exit用于内核可以找到模块的卸载函数。如果模块没有定义卸载函数,那么内核不允许卸载该模块。

         第三,模块的声明与描述。MODULE_LICENSE("GPLv2");描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核的警告。 在Linux2.6 内核中,可接受的LICENSE包括“GPL”(任一版本的GNU通用公共许可证),“GPL v2”(GPL版本2),“GPLand additional rights”(MPL/GPL双重许可证),“DualBSD/GPL”(双重许可证),“DualMPL/GPL”,“Proprietary”。 MODULE_AUTHOR("zl"); 声明该模块的作者。MODULE_DESCRIPTION("BQ27501Driver");对该模块功能简单的描述。

以上是内核模块编程的三个特点。其他函数的编写和调用和普通的GNU C一样。

3)  bq27501的i2c通信

       bq27501驱动的核心和关键就是完成i2c的通信,对于不同的设备i2c通信的消息格式可能不同,需要查阅对应的数据手册。从bq27501的数据手册中获得与其通信的message的格式,如下图(3)所示:


图(3)  bq27501的i2c通信消息格式

选择1-byte read消息格式,ADDR+CMD为一条写消息,ADDR+DATA为一条读消息。CMD为要读信息的command code,DATA是返回的数据。例如电压voltage的commandcode为0x08/0x09两个字节,返回值为一个unsignedint型的值,即也为两个字节,所以要读取电压值,要执行两次1-byte read。构造的i2c_msg格式如下面的代码片段所示。

//the first "write" message:ADDR+CMD1
      msg.addr= client->addr;
      msg.flags= 0;  // the "write" flag
      msg.len= I2C_ONE_BYTE_TRANSFER;
      msg.buf= &cmd_data;
      cmd_data= reg;
      err= i2c_transfer(client->adapter,&msg, 1);
      if(err>=0){
             //thefirst "read" message: ADDR+DATA1
             msg.addr=client->addr;
             msg.flags=I2C_M_RD;             //the"read" flag    
             msg.len=I2C_ONE_BYTE_TRANSFER;
             msg.buf= &data1;
             err=i2c_transfer(client->adapter, &msg, 1);
             if(err>=0){
                    //thesecond "write" message: ADDR+CMD2
                    msg.addr= client->addr;
                    msg.flags= 0;
                    msg.len= I2C_ONE_BYTE_TRANSFER;
                    msg.buf= &cmd_data;
                    cmd_data= reg+1;
                    err= i2c_transfer(client->adapter,&msg, 1);
                    if(err>=0){
                           //thesecond "read" message: ADDR+DATA2
                           msg.addr=client->addr;
                           msg.flags=I2C_M_RD;                   
                           msg.len=I2C_ONE_BYTE_TRANSFER;
                           msg.buf= &data2;
                           err=i2c_transfer(client->adapter, &msg, 1);
                           if(err>=0){
                                  *val= ((data2& I2C_TXRX_DATA_MASK)
                                         <<I2C_TXRX_DATA_SHIFT)
                                         |(data1 & I2C_TXRX_DATA_MASK);
                                         }
                                  }                  
                           }
                    }
 


4)  驱动与用户空间的数据交互

       内核中的数据与用户空间数据交互常用的函数有copy_to_user,copy_from_user,和宏定义put_user,get_user,__put_user,__get_user。copy_from_user和copy_to_user函数复制块数据,如数组,结构体;put_user,get_user,__put_user,__get_user复制的内存是简单类型,如char,int,long,而且只能复制1,2,4,8个字节。put_user,get_user,__put_user,__get_user执行效率比copy_to_user,copy_from_user的效率要高很多。put_user和__put_user区别在于,前者会调用access_ok进行内核地址的检查,而后者不进行地址检查。bq27501驱动的功能是向用户空间提供电池相关的信息;而且不需要用户空间向电池输入数据;另外电池的信息都是很小的数据,都可以使用unsigned short表示,所以没有必要使用copy_to_user块数据传递的函数;另外再根据《Linux驱动程序》书中“大多数驱动程序代码中都不需要access_ok,内存管理程序会处理它”,所以选择__put_user来向用户空间传递电池信息数据。

__put_user(var, ptr),var将内核中的数据var复制到用户空间;ptr是用户地址空间的指针,指向内核空间中ioctl最后一个参数。bq27501与用户空间交互的代码片段如下:

static long bq27501_ioctl(struct file*file, unsigned int cmd, unsigned long arg)
{
      BATT_INFO*pBattInfo = &BattInfo;
      unsignedshort temp;
      switch(cmd)
      {
             caseGET_RM:
                    if((i2c_read_reg(&bq27501_i2c_client,RM_REG_ADDR_L,&pBattInfo->RemainingCapacity))>=0){
                           printk(KERN_ALERT"RaminingCapacityis %u mAh ...\n",pBattInfo->RemainingCapacity);
                           __put_user(pBattInfo->RemainingCapacity,(int __user *)arg);
                    }
                    break;
……
}


在用户空间, ioctl 系统调用的原型为:intioctl(int fd, unsigned long cmd, ...); 这个原型中的点表示函数有一个单个可选的参数, 传统上标识为 char *argp. 这些点在那里只是为了阻止在编译时的类型检查。第二个参数,是用户向驱动传递的命令(如读取剩余电量值)。第三个参数的实际特点依赖所发出的特定的控制命令,即第二个参数。一些命令不用参数, 一些用一个整数值, 以及一些使用指向其他数据的指针。是否使用参数和指针是根据打开字符设备的方式决定,打开设备的方式有三种,在fcntl.h有其宏定义,只读,只写,读写,如下所示:

#define O_RDONLY	     00
#define O_WRONLY	     01
#define O_RDWR		     02
用户空间读取驱动中参数代码片段如下:
if((fd = open("/dev/bq27501",2)) == -1){
	perror("device open err!\n");
	return -1;
}
printf("open ok!\n");
ioctl(fd,atoi(argv[1]),&v); //v是一个整型,用于保存内核空间传递过来的数据
printf("(2)........cmd = [%d],val = [%d]...........\n",atoi(argv[1]),v);

六、        bq27501驱动的测试

1. 测试目的:

验证bq27501驱动能否读取寄存器的值,和用户空间的交互。

 

2. 测试过程和结果:

        bq27501驱动的测试是要在用户空间编写测试程序。Linux将所有设备都作为文件来处理的,所以在用户空间必须打开bq27501设备文件。fd =open("/dev/bq27501",2);这里使用open打开在dev下创建的bq27501的字符设备,“2”表示以读写的方式打开(也可以用O_RDWR这个宏来表示),fd用来保存文件句柄。通过ioctl向设备文件发送命令,并接收返回的电池统计信息值,ioctl(fd,atoi(argv[1]),&v);fd是打开的设备文件的句柄,atoi(argv[1])是将main的参数转换成整型作为参数传给驱动,v用来保存驱动返回的数值。

       测试程序编写完成之后,因为要在DM355平台上运行,所以必须要交叉编译源文件testbq.c。使用交叉编译环境dvsdk下的命令arm_v5t_le-gcc testbq.c –o testbq,生成可执行的testbq文件。

        使用make命令编译驱动的源文件bq27501.c,生成bq27501.ko文件。在DM355中使用tftp命令tftp –g–r bq27501.ko 10.10.101.138”下载bq27501.ko到开发板上(10.10.101.138为tftp服务器的地址)。使用insmod命令加载驱动模块,即“insmod bq27501.ko”。加载成功会打印添加的信息如下:

registersucceed!...

bq27501_driver->id225 ....

I2C:detect address is 55 ...

Adaptername is DAVINCI I2Cadapter..

Adddriver succeed!...

使用mknod命令为bq27501创建一个设备节点,即命令“mknod /dev/bq27501 c 225 0”。节点的名称为bq27501,是在/dev目录下创建的;c表示该节点是一个字符设备;225表示主设备号,这个必须和驱动中注册时的一样;0表示从设备号。

在DM355中使用tftp命令“tftp –g –r testbq 10.10.101.138”下载testbq到开发板上。使用命令“chmod 777 testbq”修改执行的权限。使用命令“./testbq 3”执行testbq文件,“3”是传给main函数的参数,表示获得电池的电压值。运行结果为“Voltage is 3779 mV ... ”,即3.78v,和电池的额定输出电压3.7一样。另外还测试了温度“Temperature is 2974 k...”,即24.25摄氏度;剩余电量可用的时间“Time to Empty is 65535 min..”,即65535分钟,表示电池没有处于放电状态,符合实际情况。

根据测试的结果,可以说明bq27501驱动和用户测试程序的基本功能点已经正确实现。

七、        总结

1. 不能省略用户空间测试文件的编写,在初始化函数中调用i2c_read_reg函数测试驱动i2c通信是否成功。模块初始化module_init调用初始化函数bq27501_init函数需要系统调用sys_init_module()。sys_init_module要得到bq27501_init的返回值才能完成模块的初始化。sys_init_module中有这么段代码:

      /*Start the module */
      if(mod->init != NULL)
             ret= do_one_initcall(mod->init);
      if(ret < 0) {
             /*Init routine failed: abort.  Try toprotect us from
                   buggy refcounters. */
             mod->state= MODULE_STATE_GOING;
             synchronize_sched();
             module_put(mod);
             blocking_notifier_call_chain(&module_notify_list,
                                       MODULE_STATE_GOING, mod);
             mutex_lock(&module_mutex);
             free_module(mod);
             mutex_unlock(&module_mutex);
             wake_up(&module_wq);
             returnret;
      }
      if(ret > 0) {
             printk(KERN_WARNING"%s: '%s'->init suspiciously returned %d, "
                               "it should follow 0/-Econvention/n"
                    KERN_WARNING "%s: loading moduleanyway.../n",
                    __func__, mod->name, ret,
                    __func__);
             dump_stack();
      }


从上面的代码可以看出,模块的init函数只能返回0或者负的错误码,否则会提示错误。所以在return语句之前调用模块中的其他函数时不正确的,因为这时模块还未真正初始化完成。

2. 在编写bq27501驱动过程中最常见的错误是“segmentation fault”。该错误的原因是访问了非法的内存,也就是使用了空指针或者未给一个指针变量分配内存空间就直接使用。在内核中对一个结构体的指针变量,要先使用kmalloc函数为其分配空间,然后才能赋值。

3. 嵌入式linux系统中,由于内存较小,可以将需要在多个函数之间传递的参数的变量设为全局变量,减少相同变量的申请,这样就可以节省内存资源。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值