ARM-Linux-IIC设备的添加与驱动实现
2012-02-15怯肇乾吴志亮
怯肇乾,吴志亮
(郑州精益达汽车零部件有限公司,河南 郑州 100044)
广泛应用的ARM-Linux及其Android的嵌入式软硬件系统中,常常涉及到内部整合电路IIC (Inter-Integrated Circuit)总线的操作及其设备的添加与驱动的实现。基于ARM内核的各类微处理器芯片中,通常都会集成一到几个IIC总线控制器,用以连接存储器、传感器、人机界面等IIC设备。没有操作系统的常规应用中,根据IIC控制器和所选IIC设备的特点,按照IIC通信协议,通过直接明了的编程设计,很容易在系统中实现IIC设备的添加与驱动。而ARM-Linux嵌入式操作系统,对IIC采用分层分散的总线架构,虽然做到了稳定、高效、模块化,却使向其中添加需要的IIC设备及其驱动实现困难了很多,常规的驱动设计方法不能用了。ARMLinux下,IIC总线的体系框架是怎样的?如何因地制宜向其中添加IIC设备并顺利实现其驱动程序设计呢?本文以下将展开详细阐述。
1 IIC总线设备驱动及其实现分析
ARM-Linux下,IIC分为3个层次:IIC内核、IIC总线驱动和IIC设备驱动。IIC内核提供核心数据结构的定义和相关接口函数,实现驱动的注册、注销管理,设备的探测、检查,以及无关具体适配器的读写通信代码。IIC总线驱动定义具体的适配器结构i2c_adapter及其算法结构i2c_algorithm,控制适配器以主控方式产生IIC通信时序。IIC内核和IIC总线驱动共同完成硬件上的主机总线控制器驱动。IIC设备驱动定义描述具体设备的i2c_client,以具体的i2c_driver实现从机设备驱动,包括read、write以及ioctl等用户层接口。ARMLinux-IIC总线驱动的框架结构及其所在的软/硬件体系如图1所示。
图1 ARM-Linux-IIC总线驱动的框架结构图Fig.1 A framework drawing of ARM-Linux-IICbus driving
从图1可以看出,在ARM-Linux中添加IIC设备,首先是编写针对该设备的具体i2c_driver驱动程序。然后是构造描述该IIC设备的i2c_client,并向系统注册。构造的i2c_client是“桥梁”,完成指定i2c_adapter主机适配控制器与特定i2c_driver设备驱动的有机联结,从而实现需要的IIC主从串行通信。
一般地,ARM-Linux操作系统提供有实现了IIC内核框架的i2c/i2c-core.c文件及其包括GPIO模拟IIC在内的常用IIC各类通信算法(在目录i2c/algos/下),还有适用于大多数IIC从设备的i2c/i2c-dev.c文件。针对具体的ARM内核微处理器体系, 如 Samsung-CortexA8 的 S5PV110/S5PV210、TICortexA8 的 AM3515/AM3715、Freescale -CortexA8 的 i.MX515/i.MX535等,半导体厂家通常都为自己具体的IIC总线控制器提供有适配器驱动 (在目录i2c/algos/下,如i2cs3c2410.c)。
综上所述,在ARM-Linux中增加IIC新设备,可以借用IIC通用驱动i2c-dev.c快速实现,可以为其编写特定的IIC驱动。ARM-Linux设备驱动通常采用静态加载操作,它有两种方式:适配 (adapter)和探测 (probe),适配是传统方式(Legacy),探测是新类型(New Style),大多数设备驱动越来越趋向采用probe方式。也可以采用驱动设计人员习惯的动态加载方式编写简易的IIC“客服-驱动”。IIC总线,可以采用系统硬件实现的IIC总线控制器,也可以选用通用输入-输出端口GPIO模拟产生。如果理解不了ARM-Linux的IIC总线层次框架,也可以抛掉这种机制采用传统方式编写IIC字符型设备驱动程序。下面就上述几种方法,分别说明ARM-Linux-IIC设备的具体添加与驱动实现过程。
需要注意的是,ARM-Linux中,由于IIC总线及其控制器使用的普遍,默认系统是加载IIC总线适配器的。如果选用的平台没有采用己有的IIC总线及其控制器,使用ARMLinux-IIC总线驱动框架,首先要在编译形成系统映像文件前,通过menuconfig配置选择IIC及其相应的适配器驱动,还有通用或特定的IIC设备驱动。
2 借用通用i2c-dev.c驱动新设备
采用i2c-dev.c直接驱动新添加的IIC设备是最简便的方法,它实际上是通过在应用层操作IIC适配器来控制i2c设备的。i2c-dev.c是通用的IIC设备驱动,它提供有通用的read()、write()和 ioctl()等设备文件操作接口,应用层可以借用这些接口访问挂接在适配器上的IIC设备的存储空间或寄存器,并控制IIC设备的工作方式。需要注意的是,i2c-dev.c对应的read()和write()是分别调用IIC内核心的i2c_master_recv()和 i2c_master_send()函数,通过构造一条 IIC 消息并引发适配器algorithm通信函数的调用来完成消息传输的,而大多数稍微复杂一点IIC设备的读写流程并不对应于一条消息,往往需要两条甚至更多的消息来进行一次读写周期,即具有IIC重复开始位RepStart的情况,此时将不能正确地读写。对于两条以上消息组成的读写即IIC总线RepStart的情况,需要采用ioctl(),组织i2c_msg消息数组并调用I2C_RDWR IOCTL命令来完成。下面以IIC接口无线射频RFID卡读写模块的标识字节设置操作为例,简单加以说明。
采用read()和write()操作的主要程序代码如下:
int main(void)
{ unsigned int fd; unsigned char buf[10];
fd=open("/dev/i2c-1", O_RDWR);
if(!fd) return 0;
ioctl(fd, I2C_SLAVE, 0x58); //设置从机设备地址
ioctl(fd, I2C_TIMEOUT, 1); //设置超时值
ioctl(fd, I2C_RETRIES, 1); //设置重试次数
buf[0]=0x03; buf[1]=0x13;buf[2]=0x13;buf[3]=0x03;
write(fd, buf, 4); //RFID 卡标识字节设置
read(fd, buf, 3); //回读,以判断设置是否成功
close(fd); return 0;
}
采用ioctl()操作的主要程序代码如下,这里实现面向3个连续的IIC从机进行写入:
int main(void)
{ struct i2c_rdwr_ioctl_data work_queue;
unsigned int i, fd, buf[4]= {0x03, 0x13, 0x13,0x03};
fd=open("/dev/i2c-1", O_RDWR);
if(!fd) return 0;
work_queue.nmsgs=3; //消息数量
work_queue.msgs= (struct i2c_msg*)
malloc (work_queue.nmsgs*sizeof (struct i2c_msg));
for(i=0; i< work_queue.nmsgs; ++i)
{ work_queue.msgs[i].len=4; //数据长度
work_queue.msgs[i].addr=0x58+i;//IIC 从机地址
work_queue.msgs[i].buf=buf; //数据指针
work_queue.msgs[i].flags=I2C_M_WR;//写命令
}
ioctl(fd, I2C_TIMEOUT, 2); //设置超时
ioctl(fd, I2C_RETRIES, 1); //设置重试次数
ioctl(fd,I2C_RDWR,(unsigned long)&work_queue);
close(fd); return 0;
}
3 为新设备编写probe方式驱动
probe方式的驱动,供ARM-Linux-IIC驱动体系,在系统启动后进行IIC基本功能性探测并加载,在系统关闭时进行“去除”缷载。这种类型的驱动基本上分为3部分:面向用户的设备文件操作、面向IIC体系的驱动探测与去除、设备模块的加载初始化与缷载去除,设计的关键在于具体i2c_client的构造和ic2_driver的实例化。上述IIC接口RFID卡读写模块probe方式的主要驱动程序代码编写如下:
static struct i2c_client*rfid_client; //IIC客户
static const struct i2c_device_id rfid_i2c_id[]={{"rfid_iic",0}};//IIC设备标识
static int rfid_open(struct inode*inode, struct file*filp )
//设备文件打开
{ filp->private_data=NULL;
try_module_get(THIS_MODULE); return 0;
}
static int rfid_release(struct inode*inode,struct file*filp)
//设备文件关闭
{ filp->private_data=NULL;
module_put(THIS_MODULE); return 0;
}
static int rfid_write (struct file*filp, const char*buffer,size_t count, loff_t*offset) //设备写操作
{ char*tmp; int ret;
tmp=kzalloc(count, GFP_KERNEL);
//内核数据空间分配
if(tmp==NULL) return -ENOMEM;
if(copy_from_user(tmp, buffer, count))
//数据拷贝:用户空间-->内核空间
{kfree(tmp); return -EFAULT; }
ret=i2c_master_send(rfid_client, tmp, count);
//调用系统函数,完成数据发送
kfree(tmp); return ret;
}
static int rfid_read (struct file*filp, char*buffer, size_t count,loff_t*offset) //设备读操作
{ char *tmp; int ret;
tmp=kzalloc(count, GFP_KERNEL);
//内核数据空间分配
if(tmp==NULL)return -ENOMEM;
ret=i2c_master_recv(rfid_client, tmp, count);
//调用系统函数,执行数据接收
if(copy_to_user(buffer, tmp, count))
//数据拷贝:内核空间-->用户空间
{kfree(tmp); return -EFAULT; }
kfree(tmp); return ret;
}
static structfile_operationsrfid_fops=//设备文件系统实例化
{ owner:THIS_MODULE,
read: rfid_read,
write:rfid_write,
open:rfid_open,
release: rfid_release,
};
static int__devinit rfid_probe (struct i2c_client*client,const struct i2c_device_id*id)//IIC设备探测
{ if (!i2c_check_functionality (client->adapter,I2C_FUNC_I2C)) return -ENODEV;
return 0;
}
static int rfid_remove(struct i2c_client*client)
//IIC设备去除
{ kfree(rfid_client);
i2c_set_clientdata(client, NULL); return 0;
}
static struct i2c_driver rfid_i2c_driver=
//IIC设备驱动实例化
{ .driver={.name="rfidDriver",}, //驱动命名
.probe=rfid_probe, //设备探测
.remove=__devexit_p(rfid_remove), //设备去除
.id_table=rfid_i2c_id, //设备信息标识
};
static int__init rfid_drv_init(void) //IIC 设备初始化
{ int ret;
struct i2c_board_info info;
struct i2c_adapter *adapter;
adapter=i2c_get_adapter(1);
//选择 IIC 适配器号(0~2)
memset(&info,0,sizeof(struct i2c_board_info)); // 构造IIC设备(板信息-->IIC客户)
strlcpy(info.type, "rfidClient", I2C_NAME_SIZE);
//设备名字
info.addr=0x58; //从机地址
rfid_client=i2c_new_device(adapter, &info);
//注册特定的i2c_client
if(!rfid_client) return -ENODEV;
ret=register_chrdev(250, "rfidIIC", &rfid_fops);
//注册字符设备
if(ret==0)
{ ret=i2c_add_driver(&rfid_i2c_driver);
//指定特定IIC设备驱动
if(ret==0) printk("Rfid(0x058)_Creation success! ");
}
else unregister_chrdev(250, "rfidIIC");
return ret;
}
static void__exit rfid_drv_exit(void) //IIC设备缷载
{ i2c_del_driver(&rfid_i2c_driver);//注销 IIC 驱动
unregister_chrdev(250, "rfidIIC");
//注销字符设备
}
MODULE_LICENSE("GPL");
module_init(rfid_drv_init); //Linux 模块初始化
module_exit(rfid_drv_exit); //Linux 模块去除
4 简易“客服-驱动”型设备驱动设计
动态加载形式的设备驱动,便于调试,用时挂载,不用时随时缷载,既使运行时,因而广泛采用。将probe方式IIC设备驱动的xxx_probe()和xxx_remove()函数分别合并到xxx_init()和xxx_exit()函数,就可以得到动态加载形式的IIC设备驱动。i2c_client的构造仍是IIC设备驱动具体化的关键,因此特别称这种类型的驱动为简易“客服-驱动”型设备驱动。对上述IIC接口RFID卡读写模块probe方式的驱动作简易“客服-驱动”型设备驱动改造,变化部分的主要程序代码如下:
static int__init rfid_drv_init(void) //IIC 设备初始化
{ int ret;
struct i2c_board_info info;
struct i2c_adapter *adapter;
if(!i2c_check_functionality(client->adapter,
//IIC设备基本功能性探测
I2C_FUNC_I2C))return-ENODEV;
adapter=i2c_get_adapter(1);
//选择 IIC 适配器号(0~2)
memset(&info,0,sizeof(struct i2c_board_info));
//构造IIC设备(板信息-->IIC客户)
strlcpy(info.type, "rfidClient", I2C_NAME_SIZE);
//设备名字
info.addr=0x58; //从机地址
rfid_client=i2c_new_device(adapter, &info);
//注册特定的i2c_client
if(!rfid_client) return -ENODEV;
ret=register_chrdev(250, "rfidIIC", &rfid_fops);
//注册字符设备
if(ret<0) unregister_chrdev(250, "rfidIIC");
return ret;
}
static void__exit rfid_drv_exit(void) //IIC 设备缷载
{ kfree(rfid_client); //i2c_client描述释放
unregister_chrdev(250, "rfidIIC"); //注销字符设备
}
MODULE_LICENSE("GPL");
module_init(rfid_drv_init); //Linux 模块初始化
module_exit(rfid_drv_exit); //Linux 模块去除
可以看到,相对probe方式的IIC设备驱,简易“客服-驱动”型IIC设备驱动,没有了xxx_i2c_driver及其相关函数的实例化设计,整个程序框架结构简便多了,可设计性与可读性增加了。
5 GPIO模拟IIC设备驱动快速实现
选择GPIO端口模拟IIC总线驱动IIC设备,虽然对于系统整体效率不高,但是直截了当,易于操作实现。可以采用ARM-Linux已有的GPIO模拟程序,也可以选择GPIO自行独立设计。这里仍以上述IIC接口RFID卡读写模块为例,自选GPIO模拟IIC时序,以动态加载形式的字符型IIC简易“客服-驱动”设计,加以说明。主要程序代码如下:
#define ByteDelayTimeout 0x0700; //字节传输超时值
#define BitDelayTimeout 0x1000; //位传输超时值
#define SCL_H{gpio_set_value (S5PV210_GPG3(5), 1);} //IIC串行时钟线模拟
#define SCL_L{gpio_set_value (S5PV210_GPG3(5),0); }
#define SDA_H {gpio_set_value (S5PV210_GPG3(6),1);} //IIC串行数据线模拟
#define SDA_L{gpio_set_value(S5PV210_GPG3(6),0);}
#define SDA_IN {gpio_direction_input(S5PV210_GPG3(6));} //IIC 串行数据位输入输出
#define SDA_OUT {gpio_direction_output(S5PV210_GPG3(6),1); }
#define WHILE_SDA_HIGH (gpio_get_value (S5PV210_GPG3(6)))
static void ByteDelay(void) //字节传输延时
{ volatile unsigned int dwTimeout;
dwTimeout=ByteDelayTimeout;
while (--dwTimeout) {asm ("nop") ;}
}
static void BitDelay(void) //位传输延时
{ volatile unsigned int dwTimeout;
dwTimeout=BitDelayTimeout;
while ( --dwTimeout) {asm("nop"); }
}
static void I2C_Start(void) //IIC 传输启动位模拟
{ SDA_OUT; SDA_H; BitDelay();
SCL_H; BitDelay(); SDA_L; BitDelay();
}
static void I2C_Stop(void) //IIC传输停止位模拟
{ SDA_OUT; SDA_L; BitDelay();
SCL_H; BitDelay(); SDA_H; BitDelay();
}
static void I2C_Ack(void) //IIC 传输响应位
{ SDA_OUT; SDA_L; BitDelay();
SCL_H; BitDelay(); SCL_L;
BitDelay(); SDA_IN; BitDelay();
}
static void I2C_Ack1(void) //IIC传输响应位(带延时)
{ int i=0;
SCL_H; BitDelay(); SDA_IN;
while((WHILE_SDA_HIGH)&&(i<255)) i++; //无
应答延时一段时间后默认已经收到
SCL_L; BitDelay(); SDA_OUT; BitDelay();
}
static void I2C_Nack(void) //IIC传输非响应位
{ SDA_OUT; SDA_H; BitDelay();
SCL_H; BitDelay(); SCL_L;
BitDelay(); SCL_H;
}
static char Write_I2C_Byte(char byte)
//IIC总线字节写操作
{ char i;
SCL_L; BitDelay ();
for(i=0 ; i<8; i++)
{if((byte&0x80)==0x80) SDA_H;
else SDA_L;
BitDelay(); SCL_H; BitDelay();
SCL_L; BitDelay(); byte <<=1;
}
return 1;
}
static char Read_I2C_Byte(void) //IIC总线字节读操作
{ char i, buff=0;
SCL_L; BitDelay();
for(i=0; i< 8; i++)
{SDA_OUT;SDA_H;BitDelay();
SCL_H; SDA_IN; BitDelay();
if(WHILE_SDA_HIGH) buff|=0x01;
else buff&=~0x01;
if(i<7) buff<<=1 ;
SCL_L; BitDelay();
}
return buff;
}
static void InterfaceInit(void) //IIC接口的GPIO定义
{ gpio_direction_output(S5PV210_GPG3(5), 1);
//SCL OUT
gpio_direction_output(S5PV210_GPG3(6), 1);
//SDA OUT
gpio_set_value (S5PV210_GPG3(5), 1);
//初始高电平
gpio_set_value (S5PV210_GPG3(6), 1);
ByteDelay(); ByteDelay(); ByteDelay();
}
static int dvcIIC_open (struct inode*inode_ptr, struct file
*fptr) //设备文件打开
{ fptr->f_op=&dvcIIC_fops;
fptr->private_data=NULL;
try_module_get(THIS_MODULE); return 0;
}
static int dvcIIC_release (struct inode*inode_ptr, struct file*fptr)//设备文件关闭
{ //fptr->private_data=NULL;
module_put(THIS_MODULE); return 0 ;
}
static int dvcIIC_read(struct file*fptr, char*buffer, size_t count,loff_t*fp) //设备文件读
{ int i;char data[100];
I2C_Start(); //启动IIC传输,发送从机设备地址
Write_I2C_Byte((0x58<<1)+1);
I2C_Ack1();
for(i=0;i<count;i++) //按字节读入数据
{data[i]=Read_I2C_Byte();I2C_Ack();}
I2C_Nack();I2C_Stop();
//发送非响应位/停止位,结束操作
if(copy_to_user(buffer,data,count))return-1;
//向用户空间回传数据
else return 0;
}
static int dvcIIC_write(struct file*fptr,const char*buffer,size_t size,loff_t*fp)//设备文件写
{ int i;char data[100];
if(copy_from_user(data,buffer,size))return-1;
//从用户空间接收数据
I2C_Start(); //启动IIC传输,发送从机设备地址
Write_I2C_Byte((0x58<<1));I2C_Ack1();
for(i=0;i<size;i++) //按字节写入数据
{Write_I2C_Byte(data[i]);I2C_Ack1();}
I2C_Stop();I2C_Nack(); //发送非响应位/停止位,结束操作
I2C_Stop();return 0;
}
static struct file_operations dvcIIC_fops=//设备文件操作接口
{ .owner=THIS_MODULE,
.open=dvcIIC_open,
.read=dvcIIC_read,
.write=dvcIIC_write,
.release=dvcIIC_release,
};
static int dvcIIC_init(void)//IIC设备初始化:字符设备注册
{ int status;
InterfaceInit();
status=register_chrdev(239,"dvcIIC",&dvcIIC_fops);
if(status<0)return status;
return 0;
}
static void dvcIIC_exit(void) //IIC设备注销
{ unregister_chrdev(239,"dvcIIC");}
MODULE_LICENSE("GPL");
module_init(dvcIIC_init); //驱动模块初始化
module_exit(dvcIIC_exit); //驱动模块去除
6 结束语
ARM-Linux-IIC设备的添加与驱动,可以根据主机控制器与从机设备的特点采用传统字符型设备驱动的方式实现;也可以根据ARM-Linux-IIC总线设备层次驱动的软件特点,对常规IIC设备采用通用i2c-dev.c快速驱动,对特定IIC设备设计probe流行方式的驱动或动态加载的简易IIC“客服-驱动”;还可以选用GPIO模拟IIC总线快速驱动设备。其中,根据ARM-Linux-IIC层次驱动的总线架构设计IIC设备驱动,稍显繁琐,不易理解,但更符合ARM-Linux总线驱动模块化的规范,能够有效地融入ARM-Linux体系,充分利用Linux内核资源,实现Linux系统的稳定、高效,是应该主动选择与推荐使用的。
[1]怯肇乾.基于底层硬体的软件设计[M].北京:航空航天大学出版社,2008.
[2]宋宝华.Linux设备驱动开发详解[M].北京:人民邮电出版社,2008.
[3]刘红波.实例解析Linux内核I2C体系结构 [EB/OL](2009-12).http://www.dzsc.com/data/html/2009-12-22/81040.html.
[4]杜博,方向忠.嵌入式Linux系统下I2C设备驱动程序的开发[J].微计算机信息,2006,22(11):21-23.DU Bo,FANG Xiang-zhong.I2Cdevice driver development in embedded Linux OS[J].Microcomputer Information,2006,22(11):21-23.
[5]李力,厉谨.利用GPIO模拟I2C总线协议 [J].科技风,2009(12):33-36.LI Li,LI Jin.Simulating I2C bus communication with GPIO[J].Science-Technology Wind,2009(12):33-36.
[6]杨文铂,刑鹏康.Linux下I2C设备驱动的一种适配器层直接实现方法[J].单片机与嵌入式系统应用,2011,11(6):16-19.YANGWen-bo,XING Peng-kang.A I2Cdevice driver method about adapter layer in linux OS[J].Single Chip Microcomputer and Embedded System Application,2011,11(6):16-19.
[7]苏缨墩,钟汉如.嵌入式Linux中的Nand Flash驱动详解[J].工业仪表与自动化装置,2011(4):56-60.SU Ying-dun,ZHONG Han-ru.A detail explication on Nand Flash drive in embedded Linux[J].Industrial Instrumentation&Automation,2011(4):56-60.