计算机并口模拟I2C总线的试验

[复制链接]
查看3296 | 回复4 | 2006-6-12 01:57:00 | 显示全部楼层 |阅读模式

近来有网友要做一个利用I2C来存取文件类似“移动硬盘”的装置,提出许多关于并口和I2C的疑问,正好前段时间因为工作的需要,我做过一个用计算机并口模拟I2C总线存取EEPROM的试验,现将当时的试验记录做了一下整理,希望对做这类装置的朋友有帮助。

试验目的 :认识计算机并口和I2C总线,用计算机并口模拟I2C总线,最后,以24CL02为例,完成对I2C EEPROM的读写操作。

试验器材 :一台装有 Tubor C 2.0 的计算机、一条25针并口电缆(看 图1 插头可要选对了)、自制的用于插入EEPROM芯片的适配器( 图2 )、一片 EEPROM 如HT24LC02或AT24C02等。

试验前的准备知识:

一、I2C总线: i2c总线是 Philips 公司首先推出的一种两线制串行传输总线。它由一根数据线(SDA)和一根时钟线(SDL)组成。i2c总线的数据传输过程如 图3 所示,基本过程为:

1、主机发出开始信号。

2、主机接着送出1字节的从机地址信息,其中最低位为读写控制码(1为读、0为写),高7位为从机器件地址代码。

3、从机发出认可信号。

4、主机开始发送信息,每发完一字节后,从机发出认可信号给主机。

5、主机发出停止信号。

I2C总线上各信号的具体说明:

开始信号: 在时钟线(SCL)为高电平其间,数据线(SDA)由高变低,将产生一个开始信号。

停止信号: 在时钟线(SCL)为高电平其间,数据线(SDA)由低变高,将产生一个停止信号。

应答信号: 既认可信号,主机写从机时每写完一字节,如果正确从机将在下一个时钟周期将数据线(SDA)拉低,以告诉主机操作有效。在主机读从机时正确读完一字节后,主机在下一个时钟周期同样也要将数据线(SDA)拉低,发出认可信号,告诉从机所发数据已经收妥。(注:读从机时主机在最后1字节数据接收完以后不发应答,直接发停止信号)。

注意: 在I2C通信过程中,所有的数据改变都必须在时钟线SCL为低电平时改变,在时钟线SCL为高电平时必须保
持数据SDA信号的稳定,任何在时钟线为高电平时数据线上的电平改变都被认为是起始或停止信号。

下面以24LC02为例,对几个主要工作时序做详细说明。

24LC02的控制字(节)格式 图4发送时紧跟开始信号后的4位是器件选择位,通常为‘1010',它和后面的3位器件地址码(由24LC02的A0、A1、A2上的电平决定)共同构成了7位的从机地址。从机地址后紧跟1位读/写控制位,该位为1表示读,为0表示写。图中最后1位是应答位,这里它由从机给出。

24LC02写时序 图⑤) 主机发送开始信号,接着发出从机地址和写控制码,主机接收从机发出的应答,主机发送1字节的地址信息,主机接收应答,主机写1字节数据到从机,主机接收应答,主机发出停止信号。写操作完成,1字节数据被写入24LC02内指定地址。24LC02提供一种页写的方式,每次最多可连续写入8字节数据再发送停止信号,当写入数据多时可采用这种方式以加快速度。

24LCO2随机读时序 图⑥) 主机发送开始信号,接着发送从机地址和写控制码,主机接收应答,主机发送1字节的的地址信息,主机接收应答(注意:前面的时序为写操作,目的把起始地址写入24CL02缓冲中,以告知随后的读操作从哪个地址开始,这个步骤在读时序中有时被称为“伪写”),主机发送开始信号,主机发送从机地址和读控制码,主机接收应答,主机读取1字节数据,主机不发应答,主机发送停止信号。完成上面步骤,主机已从24LCO2中读出指定地址内1字节数据。

24LC02读时序 图⑦) 如图⑦所示,与随机读时序相比,主机没有给从机写入起始地址,所以这种方式用于读取当前地址内的数据。另,24LC02也可以采用连续读的方式 (见图⑧) ,这样每次最多可以读取8字节。 注意: 连续读时每读完1字节后主机要发应答给主机,但在最后1字节后(即停止信号前)主机不发应答。

数据线(SDA)上的信号: 读时,从机在SCL的上升沿将数据放到SDA上,写时,遇到SCL的上升沿,从机将接收SDA上的数据。

二、并行口: 它包含了一批输入/输出端口,在PC机上它是一个25针的 D 型插口,一般用于连接打印机,因此有时也称为打印口。

并行口信号: 以打印机为例,并口I/0信号中有些是专门用来把数据传送给打印机的,有些则是用来对传送过程给以控制的,还有将打印机的各种工作状态信息发送给CPU的。详细如 表1 所示。表中所有信号用低电平(0V)表示逻辑0,用高电平(5V)表示逻辑1(电压都是相对于18-25脚上的接地电势而言),凡是前缀用符号‘-'表示的信号均指低电平为现役信号。

可以看出,编号为2-9针脚上的信号是传递实际数的信号,而其它线上的信号则是用在对打印机进行初始化处理和对打印机动作进行同步上。下面简单介绍一下打印过程以加深对并口的理解,CPU通过并口中16和17脚上的信号来选择打印机,并给以初始化处理。且用13脚上的信号给以响应。在打印机已准备好接收数据时,就将11脚置为低电平(表示可以接收),CPU把数据放到并口的数据线(2-9)上,并通过1脚上的选通信号对打印机的数据进行选通。打印机在收到选通信号时将忙信号(11)置为高电平,表示正在接收数据。数据接收完毕后,打印机在短时间内把现役的确认信号(10脚低电平)发送出去,然后再把忙信号(11)置成低电平(既非现役)并准备好接收更多数据。

并行口硬件: 并口行现在通常被集成在系统板上,25针插口上的信号可通过数据锁存器、打印状态和打印机控制三个寄存器(也就是三个输入/输出端口)进行程序设计和控制。计算机系统中通常有多个并行端口, 表2 列出了它们在输入/输出系统中的地址。 需要注意 的是这些地址是由系统 BIOS 给出的,并不是硬件的物理地址,所以可以通过设置 BIOS 来改变当前端口的配置。

端口寄存器: 表3 列出了并行端口寄存器各位的意义。这些信号也是在外部插头上出现的主要信号。不过寄存器中有些信号的极性与插头上相应信号的极性正好相反。比如,选通信号在插头上为低电平时,信号是现役的,而在打印机控制寄存器中则为高电平是现役的。

通过上面的准备知识,应有以下理解: 1、可以把并口的25个针脚理解为三个寄存器对外的映射,除了传送8位实际数据的引脚外,还有用于控制打印机和取得打印机当前状态的引脚,这些引脚有的为输入,有的为输出,因此可以像用单片机I/O一样灵活的运用它们。2、I2C总线在通讯过程中,数据线(SDA)上的信号流动方向是不断变化的,如:主机正在写24LC02时,SDA的方向为主机到从机,SDA为输出,写完一字节后,要接收应答时,SDA的方向变为从机到主机,SDA为输入(对于主机)。3、并口模拟I2C总线,其实是用软件控制并口的 I/O 来输入输出 I2C 总线需要的高、低电平信号,从而产生I2C总线的各种时序。

制作试验电路:

试验用的电路如 图⑨ ,分析如后:P1的4-7脚并联(为了加大输出电流),接IC1的VCC端,为IC1供电。P1的2脚接IC1的SCL端,用做I2C总线的串行时钟输出。因I2C总线中数据线(SDA)在不同的时间可能是输入也可能是输出,所以接在IC1 SDA端上的信号也有两路,输出时,P1 3脚输出低电平T1导通,SDA被置为低电平,P1 3 脚输出高电平T1截止,因 R1的作用SDA被置为高电平。输入时,P1 通过判断 13 脚上的电平高低,来读取SDA上的数据。 要注意的是 用于输入时T1必须是截止的,以免SDA被箝位。

这个电路具有通用性,24C01、24CO2、24LC64等24系列的I2C EEPROM 均可按这个电路与并口连接,所以不妨把它当作实用工具来认真制作。先找一条并口电缆,看电缆插头的形式,找一个与之配套的25针插座,购买一个拨动式的IC插座,将IC插座按图中IC1的连接方法与找来的并口插座相连,然后按图将T1、R1、C1、直接焊在IC插座或并口插座上,要尽量作的紧凑些。最后将电路固定在一个合适的小塑料盒内,好了,现在它是我们的试验器材,等看过后面的内容,你会发现只要为其配上软件,它就是一个用于读写I2C EEPROM 的好工具。

试验程序编写:

和其它高级语言相比,C 更适合于对硬件编程。本试验所用的程序就是在 Tubor C 2.0 环境下编译通过的。

gongkonglxw | 2006-6-12 01:57:00 | 显示全部楼层

一、C 语言相关: 对本试验较关键的几个函数和运算。

读端口函数 inprotb(); 可从指定的输入端口读入一个字节,并返回这个字节,用法为:inprotb(端口号或端口地址);例如:b=inprotb(379H);由于379H为‘打印机状态\'寄存器的地址,因此执行后变量 b中将存放由函数读取的 379H 的值。

写端口函数 outprotb(); 可写一字节数据到指定的输出端口,用法为:outprotb(端口地址,整型数);例如:outprotb(378H,1);由于端口地址378H为并口的‘数据锁存器\'地址,因此执行后将在并口的 2 脚输出高电平,3-9脚输出低电平。

位运算: 位运算的对象只能是整型或字符型数据,本试验程序中用到了两种位运算。左移运算(<<):运算符左边是位移对象,右边是整形表达式,代表左移的位数,左移时,右端补 0;左端移出的部分舍弃。右移运算(>>):运算符的使用方法与左移运算符一样,所不同的是移位方向相反。右移时,右端(低位)移出的二进制数舍弃,左端(高位)移入的二进制数分两种情况:对于无符号整数和正整数,高位补 0,对于负整数,高位补 1。举例:假设b和c为字符型变量,并且 b 已赋初值,用二进制表示时 b 的值为 01110110 ;现在若要求的 b 的第 3 位的二进制数是 1 ,还是 0 ,可暂将 b 的值赋给变量 c (c=b;),再对 c 进行位移运算,先将 c 右移 2 位(c=c>>2;),再左移 7 位(c=c<<7;),然后用程序判断 c 的值是否为 0,为0则所求位的二进制数为 0,否则为所求位的二进制数为 1。经过位移 c 的值变为‘10000000\',而不是0,因此可以判断 b 的第 3 位中的二进制数是 1。后面的试验程序就是用这种方法来接收应答和读取SDA上的数据的。

二、编程前的分析: 现在从编程的角度对 图⑨ 的电路再次分析。参见 表1 、 表2 、 表3 。现在计算机上的并口通常被[s:292]的设置成端口2,既数据锁存器地址为378H的端口。

并行口(P1)13脚: 它是一个输入端,是‘打印机状态\'寄存器(见表2、表3)中的位 4。‘打印机状态\'寄存器地址为379H,可以用 C 语言中的 inprotb() 函数来读取379H的值,然后通过位运算即可获得当前P1 13脚(IC1的SDA端)的电平状态。 注意: 在读端口时,要确认T1是截止的。

并行口(P1)2 脚: 它是‘数据锁存\'寄存器中的位 0,在这里作为一个输出端。‘数据锁存\'寄存器的地址为378H,可以用 C 语言中的 outprotb() 函数给378H的位 0 写入1或0,,从而模拟出 I2C 总线中SCL上的高、低电平。这里 需要注意的是 ,从2脚输出时,用函数写数据锁存器每次只能改变位 0 的状态,而不能影响到其它位的状态。

并行口(P1)3 脚: 它是‘数据锁存\'寄存器中的位 1,在这里作为输出端与T1基极相连,可以用 C 语言中的 outprotb() 函数给‘数据锁存\'378H的位 1 写入1或0,从而控制 T1 的导通和截止,配合 R1 的作用,模拟出I2C时钟线SDA上的高、低电平信号。 3 脚输出低电平将使 T1 导通,SDA既被置为低电平,3 脚输出高电平 T1 截止,由R1将SDA上拉为高电平。要注意操作这一位时不能影响到其它位。

并行口的 4-7 脚: 它们分别是‘数据锁存\'寄存器中的位2、位3、位4和位5,这4 位全部作为输出端接在IC1的VCC上,通过写端口函数将它们全部写入1(既都输出高电平),用于给IC1提供电源。注意,因这4位是作为电源使用的,必须保证这4位的值始终为1 , 所以每次写378H时要特别注意 。这4个引脚是并在一起的,其中若有1位被写成0,就会因高低电平抵消而中断IC1的电源使操作失败,甚至可能会损坏并口。

三、编程: 通过上面分析,要用并口来模拟I2C总线来读写 24LC02 ,程序需有以下几部分。

发送I2C开始信号: 用 outprotb() 函数向378H写入16进制数“0XFF”(即2-9脚全部输出高电平),SCL和SDA都为高电平,延时一段时间后,向378H写入“0XFD”(其它脚状态不变,只是将位 1 置为低电平),使SDA由高电平变为低电平,即产生了I2C的开始信号。最后将在378H中写入“0XFC”(即其它脚不变,将位0和位1置为低电平)使SCL为低电平,以完成一个时钟,也为后面的读写作准备。

发送I2C停止信号: I2C的停止信号是在SCL为高时,SDA由低变高。程序可按下面步骤来写,用写端口函数向378H写入“0XFC”,使SCL和SDA为低电平,延时一段时间,向378H写入“0XFD”,使SCL变为高电平,SDA为低电平,延时,向378H写入“0XFF”SCL保持不变,使SDA由原来的低电平变为高电平,即产生了一个停止信号。延时一段时间,最后向378H写入“0XFE”,使SCL为低电平,以完成一个时钟。

发送数据: 先把要发送的数据放在一个变量里,然后按位发送。方法为,通过位运算求得欲发送位的值(1或0),然后用写端口函数模拟出SCL和SDA,并按I2C的写时序将一位数据发送出去,程序中可用while循环语句来控制发送的位数和字节数。

主机(并口)发送应答: I2C总线,主机发送应答用在连续读时序中,每读取一字节(8位)后,主机使SDA保持一个时钟周期的低电平。可以用写端口函数先将SDA、SCL置为 0,然后将SCL变高,SDA保持低电平,一个应答信号既被发送,最后将SCL置低,完成一个时钟。

接收数据: 并口读取I2C总线的数据时,必须让 T1截止,使用并口的13脚来接收SDA上的数据。可按下面步骤操作,先用写端口函数使SCL为低电平,同时在并口3脚输出高电平使 T1 截止。然后用写端口函数单独将SCL置1,其它位保持不变,模拟出时钟上升沿,IC1 将把一位数据放到数据线SDA上,用读端口函数 inprotb() 读取‘打印机状态\'寄存器379H当前的值,将结果赋值给一个变量,然后对这个变量进行先右移4位,再左移7位的运算(用以获得13 脚电平状态,即打印机状态寄存器的位 4 的值),判断该变量是否为0,最后将判断结果移入另外的一个用于存放‘已读取数据\'的变量中,完成读取一位数据的操作,用写端口函数使SCL为低电平,在下一个SCL的上升沿,同样用上面的方法将一位数据加入‘已读取数据\'变量中。可用while循环控制要读的位数和字节数。 注意: 以上过程都是在 T1 为截止态时进行的。

主机(并口)接收应答: 接收应答用于写 I2C 时,每写一字节数据到从机后,如果操作成功,从机在下一个时钟内使 SDA 为低。主机查询应答可以加强操作的可靠性。接收应答和上面说的接收数据大致相同,只是仅接收一位数据并且不存储,直接判断其值是否为 0,不为 0 时(即没有收到应答)转错误处理程序,为 0则继续后面的操作。在实际编程时将这个步骤合并到写I2C的操作中。

有关延时: I2C器件对SDA和SCL上的高、低电平信号需保持的时间是有规定的。如:开始信号的高、低电平要保持多长时间,数据信号的高、低电平最低要保持多长时间等。不同的器件对这个时间有不同的规定。查找24LO02的数据手册,可以知道,它在不同的电压下对各信号要保持的时间分别在几百纳秒到几微秒之间。这个时间也体现了I2C器件的读写速度。因为计算机的速度不同,要用计算机并口来模拟I2C很难将这个时间精确到微秒。为了能够在不同的计算机上可靠的操作I2C总线,试验程序用了C语言的延时函数delay();这个函数能产生的最小延时为1毫秒。虽然这样做降低了I2C的读写速度,但可以保证操作的可靠性。

四、用并口读写I2C总线的源程序: 程序中把I2C的一些操作时序定义成了独立的函数供主函数调用,这样增加了程序的灵活性,也方便对程序的修改和扩充。

gongkonglxw | 2006-6-12 01:58:00 | 显示全部楼层

源程序如下:


#include \"stdio.h\"
#include \"dos.h\"
#include \"conio.h\"


/***********void i2cstart()***********/
void i2cstart(){
outportb(0x378,0xff);/*scl 1, sda 1*/
delay(1);/**/
outportb(0x378,0xfd);/*scl 1, sda 0*/
delay(1);/**/
outportb(0x378,0xfc);/*scl 0, sda 0*/
delay(1);
}

/***********void i2cstop()***********/
void i2cstop(){
outportb(0x378,0xfc);/*scl 0, sda 0*/
delay(1);/**/
outportb(0x378,0xfd);/*scl 1, sda 0*/
delay(1);/***/
outportb(0x378,0xff);/*scl 1, sda 1*/
delay(1);/**/
outportb(0x378,0xfe);/*scl 0, sad 1*/
}

/***********writebyte()***********/
writebyte(char s){
short int a=7;
char d,e;
outportb(0x378,0xfc);/*scl 0, sda 0*/
delay(1);/***/
while(a>=0){

d=s>>a; d=d<<7;

if (d==\'\\x80\')/*****\"1\"***/
{
outportb(0x378,0xfe);/*scl 0, sda 1*/
delay(1);/***/
outportb(0x378,0xff);/*scl 1, sda 1*/
}
else
{
outportb(0x378,0xfc);/*scl 0, sda 0*/
delay(1);/***/
outportb(0x378,0xfd);/*scl 1, sda 0*/
}

a=(a-1);
}
/**ask**/
delay(1);/***/
outportb(0x378,0xfe);/*scl 0, sda 1*/
delay(1);/***/
outportb(0x378,0xff);/*scl 1, sda 1*/
delay(1);/***/
outportb(0x378,0xfc);/*scl 0, sda 1*/
delay(1);/***/
e=inportb(0x379); d=e>>4; d=d<<7;
if (d==\'\\x0\') return 0;
else
printf(\"not acknowledge!\\n\");
return 1;
}

/***********readbyte()***************/

char readbyte(){
unsigned short a=8;
char d,e,f=\'\\x0\';
while(a>0){
delay(1);/***/
outportb(0x378,0xfe);/*scl 0, sda 1*/
delay(1);/***/
outportb(0x378,0xff);/*scl 1, sda 1*/
delay(1);/***/
e=inportb(0x379); d=e>>4; d=d<<7;

if(d==\'\\x80\') d=\'\\x1\';

f=f<<1; f=(f+d); a=(a-1);

outportb(0x378,0xfe);/*scl 0, sda 1*/
delay(1);/***/
}

return f;
}

/************mainask()*****************/
mainask(){
delay(1);/**/
outportb(0x378,0xfc);/*scl 0, sda 0*/
delay(1);/**/
outportb(0x378,0xfd);/*scl 1, sda 0*/
delay(1);
outportb(0x378,0xfc);/*scl 0, sda 0*/
}

/*************************************/

main(){
unsigned short a,b,c,g;
char d,e,f;
textcolor(2);
clrscr();

printf(\"press \'r\' or \'w\' :\");
scanf(\"%c\",&f);
if(f==\'w\')
{

/************ W 256 BYTES ****/
e=\'\\x0\'; c=32; /* 24lc02: 32=256/8 */
while(c>0){
i2cstart();/*****start****/

writebyte(\'\\xa0\');/***send contbyte***/

writebyte(e);/***send start address***/

/************W 8 bytes****/
b=8; d=\'\\x0\'; /* num */
while(b>0){
if ((writebyte(d))==1) exit(0);/***send a byte***/
b=(b-1);d=(d+1);
}
i2cstop();
delay(40); /****writer delay****/
c=(c-1);e=(e+8);
}

printf(\"write ok!!\\n\"); exit(0);

}


if(f==\'r\')
{

/****** read ***********************************/
printf(\"please import start address:\");
scanf(\"%x\",&b);
a=(256-b); c=(a%8); a=(a/8);

while(a>0){
g=8;


i2cstart();/*****start****/

writebyte(\'\\xa0\');/***send contbyte***/
d=(char)b;/****/
writebyte(d);/***send start address***/
i2cstart();/*****start****/
writebyte(\'\\xa1\');/***send contbyte***/

while(g>0){
d=readbyte();
if(d==\'\\xff\')
printf(\" FF\");
else
printf(\" %.2X\",d);
g=(g-1); if(g>0) mainask();
b=(b+1);
}
i2cstop();

a=(a-1);

}
while(c>0){
i2cstart();/*****start****/

writebyte(\'\\xa0\');/***send contbyte***/
d=(char)b;/****/
writebyte(d);/***send start address***/
i2cstart();/*****start****/
writebyte(\'\\xa1\');/***send contbyte***/
d=readbyte();
if(d==\'\\xff\')
printf(\" FF\");
else
printf(\" %.2X\",d);
c=(c-1); if(c>0) mainask();
}
printf(\"\\nREAD OK!\\n\");
exit(0);
}
else {printf(\"\\nCommand Error!!!\"); exit(0);}

}

以上程序是在 Tubor C 2.0 环境下编译通过的,运行结果如后:程序先在屏幕上提示“press \'r\' or \'w\' :”\'r\'为读24LC02,\'w\'为写。如果输入\'r\'并按回车,程序将会提示:“please import start address:”这时请按16进制的格式输入要读的起始地址,然后回车,程序将会从该地址开始把后面的所有数据读出并按大写 16进制的格式在屏幕上打印出来。完成后提示“READ OK!”并结束程序。如果在程序开始时输入的是\'w\',程序将从24LC02的00H地址开始,按“00 01 02 03 04 05 06 07”的格式,每8字节循环一次,直到写满24LC02所有的存储空间,即256个字节。写的过程中如果出现错误将提示“not acknowledge!”,如果操作顺利完成,程序将提示“write ok!!”并结束运行。在程序开始时如果输入的不是\'r\'也不是\'w\',程序将提示“Command Error!!!”并退出运行。

这个程序虽然只是个简单的演示,但却是并口模拟I2C的最关键的核心部分,只要给它加些改动并配上简单的界面,即可以成为一个很实用的并口 I2C 总线读写程序。

yaochengbao | 2007-4-5 15:24:00 | 显示全部楼层
[em01]
xuliang987 | 2007-5-16 20:06:00 | 显示全部楼层
有点难啊
您需要登录后才可以回帖 登录 | 注册哦

本版积分规则