基于Linux系统的多种串行总线统一接口的实现
扫描二维码
随时随地手机看文章
引言
在Linux内核中单独实现TTY、I2C、SPI、ISA、USB等多种总线驱动时,每一种总线的实现都有各自的特点,如参数设置不同,实现的结构不同等。以TTY、I2C为例,TTY采用的是基于线路规程的三层结构,而i2c则是基于用户句柄和适配器的三层结构。当然,这些驱动都是功能齐全而强大的,但对于并不复杂的应用而言,这样的控制是比较繁琐的,而且,对于移植也是不利的。例如,某个应用系统原先使用一款I2C接口的时钟芯片,但后来系统升级换成了一款SPI接口的时钟芯片,这时就不得不对程序做较大的改动了。本文给出了一种多种串行总线统一接口的实现方法,并以ARM9为平台,以I2C、1-Wire,SPI为例验证了方法的可行性。
1总线协议及其工作过程
多数的串行总线都基于主从结构,如果总线中包含了时钟信号线,那么,该时钟信号就由主机提供,而如果还包含了片选信号,通常也由主机来控制。也就是说,主机发起通信,从机处于被动状态,所以,对于总线时序的分析,只需讨论主控制器端的时序,而从设备的时序就是它的逆向过程。
1.1 SPI协议及其工作过程
SPI总线是摩托罗拉公司提出的一种串行总线协议,该总线由4根基本的信号线组成,分别是CS、SI、SO、SCK。其中SCK是串行总线时钟,由主设备提供;而SI、SO分别对应于数据输入和数据输出信号。在一主多从的系统中,片选信号决定当前有效的从设备。
SPI总线的工作过程是:首先,主机发起通信,通过片选信号激活从设备;然后,主机在串行时钟SCK信号的同步下,将地址、命令、数据信息从串行数据输出信号(相对主机而言)SI送出;而从设备则在SCK信号的同步下接收主机发送来的数据,并作出相应反应,最后将结果从数据输入信号(相对主机而言)SO送出。
S3C2440中对SPI总线的控制,就是集中于对rSPCONn、rSPSTAn、rSPPINn、rSPPREn、rSPTDATn和rSPRDATn的控制。其中rSPCONn用于DMA设置、工作模式选择、时钟相位选择,rSPSTAn用于控制器状态查询,rSPPINn用于多主机下出错检测和片选释放,rSPPREn用于控制预分频状态寄存器,rSPTDATn是数据发送寄存器,rSPRDATn是数据接收寄存器。
1.2 I2C协议及其工作过程
I2C总线是由飞利浦公司提出的一种接口标准,该总线由SDA、SCL两根信号线组成。其中SCL为时钟信号,由主机提供,最大传输速率为400kb/s;而SDA为数据信号。连接到总线上的每一个设备都有一个唯一的地址,通过这个地址使得主机能够找到目标从机并与之进行通信。
以主机发送为例,i2c总线的工作过程是:首先,主机控制时钟信号SCL为高电平时,数据信号SDA产生一个下降沿,作为起始条件。然后,主机发出7位的从设备地址和1位R/W标志,并激活将要与之通信的从设备,而从设备则会产生一个应答信号。对于写数据,主机紧接着就将一个字符或一串数据写入到从设备;而对于读数据,则紧接着读取从设备输出的数据。
I2C总线中的S3C2440对I2C的控制主要集中于对rIICCON、rllCSTAT、rIICADD和rIICDS的控制。其中rIICCON用于时钟源选择、中断控制和I2C控制器使能,rIICSTAT用于工作模式选择、控制器状态查询,rIICADD是从设备地址(当S3C2440设置为从设备模式时使用),rIICDS是发送接收移位寄存器。
1.3 1-Wire协议及其工作过程
1-Wire总线是Maxim全资子公司Dallas提出的一种总线接口。1-Wire总线与其他的串行总线有比较大的区别:普通的串行总线通常由两根或两根以上的信号线组成;而1-Wire总线仅有一根信号线,同时用于时钟、数据、命令的传输,具有资源利用率高、结构简单、成本低廉、易于总线扩展等优点。
1-Wire总线工作过程:1-Wire总线包含复位、读、写三种基本时序。在复位状态下,主机将总线拉低480~960卜后释放总线,由于上拉电阻的作用,此时的电平为高,等待15~60卜之后,从设备将总线拉低表示复位成功。写操作时,若写入数据位为0,则主机将总线拉低60卜后释放;若写入数据位为1,则主机将总线拉低1~153后释放。由于很少有控制器集成了1-Wire总线控制器,所以,一般使用GPIO模拟的方式,这时,对于时序的控制就要求得比较精确。
2Linux下的统一驱动
这些总线有一些共性,也就是驱动要实现的内容,主要包括单字节数据收发、数据流收发以及工作模式控制等。在这些共性的基础上,一般都需要向上层提供一个统一的接口,以使得对使用这些API的应用程序而言(下层总线无论是RS-232,SPI、I2C,还是1-Wire)都不需要做任何改变。同时,还要对下层也提供一个通用接口,使得不同的总线都能与上层统一接口协调通信。该驱动的结构框架如图1所示。
图1 驱动结构
本文主要讨论的是总线驱动部分,而应用层和物理层在测试的时候,也可用两个简单的例子来验证设计结果。
2.1注册一个新设备号
首先可为统一接口的总线定义一个新的设备号240,而且以后注册的总线子设备都以此为主设备号。假如现在注册了一个1-Wire和一个I2C总线接口,那么,它们两者的主设备号都为240,而次设备号不同。如果1-Wire的次设备号为0,而I2C的次设备号为1,那么就可将两条总线区分开来了。此时的程序如程序片段一所示。
程序片段一:
#defineBUSES_MAJOR240
staticint__initbuses_drv_init(void){
intret;
ret=register_chrdev(BUSES_MAJOR,"buses",&buses_ops);
pBuses_dev_class=class_create(THIS_MODULE,
"buses_class");
return0;
}
module_init(buses_drv_init);
2.2设备接口层
为了实现统一的接口,有必要定义一个统一的字符设备接口buses_ops[6,7],应用程序访问总线都通过这个接口,这样,所讨论的统一接口问题也就实现了。该接口的主要函数成员如程序片段二所示。
程序片段二:
structfile_operationsbuses_ops={.owner=THIS_MODULE,.release=buses_close,.open=buses_open,.ioctl=buses_ioctl,
.read=buses_read,
.write=buses_write,
};
应用程序打开设备的时候,利用子设备号可以找到总线对应的底层适配器,也就是说,子设备号兼具了适配器索引的功能,其具体实现如程序片段三所示。
程序片段三:
staticintbuses_open(structinode*inode,structfile*filp){structlist_head*pos;
structbuses_dev*dev;
unsignedintminor二iminor(inode);
list_for_each(pos,&buses_list_head){//buses_list_head用来挂载buses_dev的链表
dev=list_entry(pos,structbuses_dev,list);
if(dev->adapter->nr==minor){filp->private_data=dev;
}
}
return0;
}
事实上,buses_dev是设备层和适配器层的桥梁,在open操作里被赋值给文件指针的私有数据域。那么,在读与写函数中,就可以反其道而行,通过文件指针的私有数据域就可获得buses_dev数据结构体。
2.3适配器接口层
适配器负责对底层数据的操作,由于不同的总线之间存在共性,所以,一般来说,它们都包含了单字节读、单字节写、多字节读、多字节写以及一些特殊控制。综上所述,该数据结构如程序片段四所示。
程序片段四:
structbuses_adapter{
intnr;
unsignedintflags;
structdevice*dev;
/*单字节写操作*/
int(*single_write)(unsignedlongaddr,constunsignedchar*dat);
/*单字节读操作*/
int(*single_read)(unsignedlongaddr,unsignedchar*dat);
/*多字节写操作*/
int(*muti_write)(unsignedlongaddr,constunsigned
可靠传输ReliableTransmission,価厠I曜
.町4JIVTUKTOf1HIMInCHMMCI
char*buff,intdatLen);
/*多字节读操作*/
int(*muti_read)(unsignedlongaddr,unsignedchar*buff,intdatLen);
/*特殊控制*/
int(*adapter_ioctl)(unsignedintcmd,unsignedlongarg);
};
所谓适配器注册,就是将适配器添加到全局链表buses_list_head中,只有这样,才能在字符设备接口的open操作中通过子设备号索引找到适配器,具体如程序片段五所示。
程序片段五:
intbuses_dev_register(intminor){
adapter->nr=minor;dev->adapter二adapter;
adaptei->dev=device_create(pBuses_de^class,NULL,de\_t,NULL,device_name);
list_add(&dev->list,&buses_list_head);
return0;
}
3实验测试
这里分别以1-Wire、SPI、I2C总线为例来初始化三条总线适配器,同时实现适配器的单字节写、单字节读、特殊控制等三种基本操作。具体操作如下面的程序所示:
程序片段六:
#defineONEWIRE_DEV_MIONR0
#defineSPI_DEV_MIONR1
#defineI2C_DEV_MIONR2
structbuses_adapteri2c_adapter={//I2C适配器
};
structbuses_adapteroneWire_adapter={//1-Wire适配器
};
structbuses_adapterspi_adapter={//spi适配器
};
staticint__initi2c_drv_init(void){
if(buses_dev_register(I2C_DEV_MINOR)<0)//I2C
模块加载
return-EFAULT;
if(buses_dev_register(ONEWIRE_DEV_MINOR)<0)//1-Wire模块加载
return-EFAULT;
if(buses_dev_register(SPI_DEV_MINOR)<0)//SPI模块加载
return-EFAULT;
return0;
}
完成设备驱动加载之后,就会在/dev目录下生成如图2所示的文件节点。通过打开节点,就可以打开总线的统一接口,从而实现对总线的读、写和控制操作。
图2/dev目录下生成的文件节点
同时,还会在/sys目录下生成关于注册的总线属性目录和文件,主要包含有设备号的属性文件、电源管理属性目录、到类目录的链接、特殊事件属性文件等,具体如图3所示。
这里分别对I2C接口的E2PROM芯片AT24C02、1-Wire接口的EEPROM芯片DS2433和SPI接口的EEPROM芯片25AA010进行测试。其测试结果如图4所示。
图4测试结果图
其测试过程是:通过打开/dev/bus-0、/dev/bus-1、/dev/bus-2节点,调用写操作写一段数据到EEPROM,然后,再调用读操作读出刚才写入的数据,并验证两者是否一致,从而判断本文的接口函数的正确性。
4结语
实践证明,使用设备接口层与适配器接口层的这种分层方式,能够让应用程序进一步忽略底层的接口操作,实现接口的统一。而且,该方法具有适应性强,易于系统升级,占用资源少等特点,能有效提高应用程序的开发效率。
20210913_613f65a1375be__基于Linux系统的多种串行总线统一接口的实现