[原创] 【TI原创】LM3S使用笔记之I2C总线(一)

柳叶舟   2011-10-6 15:09 楼主

STM32的IIC实在太难用了,一个很简单的东西,ST的人把它弄得很复杂,不得不说STM32的IIC很鸡肋。
首先请大家不要吃惊,本文没有发错版块。上面这句话不是我说的,是《stm32不完全手册》中在讲述I2C时说的一句话。
同时也请STM的Fans不要鄙视我,我这里也不是在贬低STM32来衬托Stellaris。因为起初我也是不怎么同意他的这句话的(我还没有用过STM32的芯片,起初是准备从STM32入门的,但是申请到了8962的开发板,就转到LM3S下了),但是当我开始使用8962的IIC总线时,我也有了同样的感受。
这一切还要从一次不规范的引用示例程序说起。
前段日子准备山寨一个USB-Blaster。网络上有一个开源的项目叫做ixo-jtag,里面有使用CY7C68013做的USB-Blaster资料。我以前也买过一个同样的下载线,这次我懒得去分析编译里面的程序了,于是准备将我的USB-Blaster中EEPROM(24C64)中的程序读取出来。
本来这也不算什么事情,我以前也写过基于51的I2C总线程序,只不过很久不玩51了,电脑里51的编译器都没有安装。8962带有现成的I2C接口,站在巨人的肩膀上,这事还不是三五分钟的事吗。
好了,马上打开文件夹,找范例。呵呵,反正已经占到人家肩膀头上了,索性就叠个罗汉,也可以省下些调试时间。
很快就找到了,周立功的EasyARM8962光盘里面的实验例程4.12。该例程演示了对I2C存储器24C02进行读写操作,跟我的要求很对口,就从它上面改吧。原示例代码

eeprom.rar (3.45 KB)
(下载次数: 314, 2011-10-6 15:09 上传)

该程序采用的是一种现在比较流行的状态机写法。即将对24C04进行的每一步操作细分,每一个步骤定义为一个状态,然后根据操作流程定义状态机转换规则。这个在其ISR程序中得到了完整的体现,代码如下:
  1. void I2C_ISR(void)
    {
    I2CMasterIntClear (I2C_MASTER_BASE); /* 清除I2C中断 */
    switch ( g_ulState ) { /* 根据当前状态执行相关操作 */

    case STATE_IDLE: /* 空闲状态 */
    break;

    case STATE_WRITE_NEXT: /* 写下一个数据 */
    I2CMasterDataPut (I2C_MASTER_BASE, *g_pucData++); /* 将下一个字节写入数据寄存器 */
    g_ulCount--;
    I2CMasterControl (I2C_MASTER_BASE,
    I2C_MASTER_CMD_BURST_SEND_CONT); /* 继续执行块写操作 */
    if (g_ulCount == 1) { /* 如果只剩下一个字节,则将下一
    个状态设置为最终写状态 */
    g_ulState = STATE_WRITE_FINAL;
    }
    break;

    case STATE_WRITE_FINAL: /* 写最后一个数据 */
    I2CMasterDataPut (I2C_MASTER_BASE, *g_pucData++); /* 写最后的字节到数据寄存器 */
    g_ulCount--;
    I2CMasterControl (I2C_MASTER_BASE, /* 完成块写 */
    I2C_MASTER_CMD_BURST_SEND_FINISH);
    g_ulState = STATE_IDLE; /* 下一个状态为等待块写完成状态*/
    break;

    case STATE_WAIT_ACK: /* 等待应答信号 */
    if (I2CMasterErr (I2C_MASTER_BASE) == I2C_MASTER_ERR_NONE) { /* 判断前一次读操作是否有错误 */
    I2CMasterDataGet (I2C_MASTER_BASE); /* 读取接收到的数据 */
    g_ulState = STATE_IDLE; /* 如果没有错误,进入空闲状态 */
    break;
    }

    case STATE_SEND_ACK: /* 返回一个应答信号,以指示读操
    作已经完成 */
    I2CMasterSlaveAddrSet (I2C_MASTER_BASE, CSI24c02, true); /* 设置I2C主机为接收模式 */
    I2CMasterControl (I2C_MASTER_BASE,
    I2C_MASTER_CMD_SINGLE_RECEIVE); /* 进行单字节读操作 */
    g_ulState = STATE_WAIT_ACK; /* 等待ACK信号 */
    break;

    case STATE_READ_ONE: /* 读取一个字节的数据 */
    I2CMasterSlaveAddrSet (I2C_MASTER_BASE, CSI24c02, true); /* 设置I2C主机为接收模式 */
    I2CMasterControl (I2C_MASTER_BASE,
    I2C_MASTER_CMD_SINGLE_RECEIVE); /* 进行单字节读操作 */
    g_ulState = STATE_READ_WAIT; /* 下一个状态机将为等待最终读状
    态 */
    break;

    case STATE_READ_FIRST: /* 读取字符串的首数据 */
    I2CMasterSlaveAddrSet(I2C_MASTER_BASE, CSI24c02, true); /* 设置I2C主机为接收模式 */
    I2CMasterControl(I2C_MASTER_BASE, /* 开始接收块 */
    I2C_MASTER_CMD_BURST_RECEIVE_START);
    g_ulState = STATE_READ_NEXT; /* 下一个状态为块读取中状态 */
    break;

    case STATE_READ_NEXT: /* 读取下一个数据 */
    *g_pucData++ = I2CMasterDataGet (I2C_MASTER_BASE); /* 读取接收到的字符 */
    g_ulCount--;
    I2CMasterControl (I2C_MASTER_BASE, /* 继续块读取操作 */
    I2C_MASTER_CMD_BURST_RECEIVE_CONT);
    if (g_ulCount == 2) { /* 如果仅剩下两个字节,下一状态
    将为快读取结束状态 */
    g_ulState = STATE_READ_FINAL;
    }
    break;

    case STATE_READ_FINAL: /* 块读取结束状态 */
    *g_pucData++ = I2CMasterDataGet (I2C_MASTER_BASE); /* 读取接收到的字符 */
    g_ulCount--;
    I2CMasterControl (I2C_MASTER_BASE, /* 完成块读取操作 */
    I2C_MASTER_CMD_BURST_RECEIVE_FINISH);
    g_ulState = STATE_READ_WAIT; /* 下一个状态为等待块读取最终状
    态 */
    break; /* 此状态已完成 */

    case STATE_READ_WAIT: /* 读字节或者读块的最终状态 */
    *g_pucData++ = I2CMasterDataGet (I2C_MASTER_BASE); /* 读取接收到的字符 */
    g_ulCount--;
    g_ulState = STATE_IDLE; /* 设置状态机为空闲 */
    break; /* 此状态已完成 */
    }
    /* I2CMasterIntClear(I2C_MASTER_BASE); */
    }

这段代码我就不再详细解读了,其工作原理是读取进入中断时所处的操作状态,然后进行相应的操作,并判断下一步要进行的操作并设置下一状态。

回复评论 (17)

其实刚一开始我也没有细细的读这段程序。因为如果只是应用的话,这些应该算是封装起来的操作。大家不需要关心,真正关心的是接口的操作。其操作接口则是在下面两个函数中:
void EEPROMWrite (unsigned char *pucData, unsigned long ulOffset, unsigned long ulCount);
void EEPROMRead  (unsigned char *pucData, unsigned long ulOffset, unsigned long ulCount);
我们看一下其中的一个函数:

  1. void EEPROMWrite (unsigned char *pucData,
    unsigned long ulOffset,
    unsigned long ulCount)
    {
    g_pucData = pucData; /* 将要写入的数据存入缓冲区 */
    g_ulCount = ulCount;
    if (ulCount != 1) { /* 根据将要写的字节数设定中断
    状态机的下一状态 */
    g_ulState = STATE_WRITE_NEXT;
    }
    else {
    g_ulState = STATE_WRITE_FINAL;
    }
    I2CMasterSlaveAddrSet (I2C_MASTER_BASE, CSI24c02 , false); /* 设置从地址,准备发送数据 */
    I2CMasterDataPut (I2C_MASTER_BASE, ulOffset); /* 将写地址发送到数据寄存器 */
    I2CMasterControl (I2C_MASTER_BASE, I2C_MASTER_CMD_BURST_SEND_START);/* 开始循环写字节操作,写该地址
    作为第一个地址 */
    while ( g_ulState != STATE_IDLE ) { /* 等待I2C为空闲状态 */
    }
    }

这段程序中所进行的操作非常少,只有两个,一是判断写入的是否只有一个字节,然后发送第一个字节。
在这里,我用的是24C64,其内部地址长度为两个字节,而示例程序用的是24C02,内部地址长度只有一个字节。这样问题就来了,我要多发送一个地址数据。怎么发呢?当时也是为了省事,直接在其发送第一字节后面又增加了发送一个字节的代码。结果可想而知,因为是采用的中断操作,所以在发送完第一字节后立即进入中断,后面的事情都由中断来完成了,如果此时主程序和中也有相应的操作的话,就会和中断中的操作相冲突,造成不可预料的后果。不过还好当时没有发现,才有了后来对I2C的进一步分析。因祸得福?
由于该程序是用库函数API写的,所以出现问题后首先想到的就是去查库函数,看看它都做了什么。
I2CMasterSlaveAddrSet和I2CMasterDataPut都没有什么问题,和预料中的操作一样,只有这个I2CMasterControl了。而该函数核心还是其命令常量的设置,如下:
#define I2C_MASTER_CMD_SINGLE_SEND              0x00000007
#define I2C_MASTER_CMD_SINGLE_RECEIVE           0x00000007
#define I2C_MASTER_CMD_BURST_SEND_START         0x00000003
#define I2C_MASTER_CMD_BURST_SEND_CONT          0x00000001
#define I2C_MASTER_CMD_BURST_SEND_FINISH        0x00000005
#define I2C_MASTER_CMD_BURST_SEND_ERROR_STOP    0x00000004
#define I2C_MASTER_CMD_BURST_RECEIVE_START      0x0000000b
#define I2C_MASTER_CMD_BURST_RECEIVE_CONT       0x00000009
#define I2C_MASTER_CMD_BURST_RECEIVE_FINISH     0x00000005
#define I2C_MASTER_CMD_BURST_RECEIVE_ERROR_STOP 0x00000005
解读这个命令编码还需要一个表格的帮助,那就是I2CMCS寄存器的说明
  未命名.JPG
从这个表格中,可以看到,起作用的四位每位代表一种操作。本来是一种很普通的控制方式,每次设置一位来完成一个操作,简单明了。但是复杂就复杂在有些操作组合是必然的,处理器就替你封装了,而且不允许你独立进行操作。这些操作是一种怎样的组合方式呢,我们还是先看一下这个寄存器位的定义。
I2CMCS寄存器其实是两个寄存器,一个只读的状态寄存器和一个只写的控制寄存器共用同一个地址空间。在控制寄存器中,有四位可用,分别是:ACK、STOP、START、RUN。关于这四位的描述在Datasheet中是这么写的:
ACK:数据应答使能(Data Acknowledge Enable);当该位置位时,主机自动应答已接收的数据字节。
STOP:产生停止条件(Generate STOP);当该位置位时,产生停止条件。
START:产生起始条件(Generate START);当该位置位时,产生起始或重复起始条件。
RUN:I2C 主机使能(I2C Master Enable);当该位置位时,允许主机发送或接收数据。
不知道大家有没有注意到,上述四个位的描述中,有两个采用了使能(Enable)来作为其描述动词。使能在控制中意义是将一设备设置为活动状态,等到条件满足即执行,并非立即执行的意思。因此,这里的I2C总线的操作则是以命令序列的方式来执行的(当然序列里也可以只有一个命令),用一句PC里面的术语来说,采用的是批处理,以此来提高效率。
这样我们再来看前面这个表格的组合就不难发现,为什么没有产生起始条件的命令,因为起始之后肯定要发送或接收数据的,所以所以在所有的Start位设置时RUN位也必须设置。其他诸如ACK与STOP位互斥也是这个原理,表格中有些ACK位与STOP位不是互斥,那是因为在发送时ACK位是无效的。
此外对于表格中的ACK位是该I2C控制器管理中最智能化的,对于需要设置ACK的场合都是在I2C协议中没有明确规定是否使用ACK的场合,而在协议中明确规定有或者没有ACK的场合,ACK是可以自动产生的,这样也最大化的减小了错误的发生。比如I2C_MASTER_CMD_SINGLE_RECEIVE这个命令。在该表格中没有定义独立产生ACK的命令,这是因为ACK的产生是需要一定条件的,即接收完一个完整的数据。但是在接收结束时的最后一个数据接收完成时,是不产生ACK的,所以在命令表格中ACK与STOP位是互斥的,但是在单字节接收时又有一个特例,此时要产生ACK。所以在I2C_MASTER_CMD_SINGLE_RECEIVE这个命令中,虽然ACK位为0,但是依然会产生ACK,这个就是由处理器自动完成的了。
那么如何手动产生ACK呢?前面说了,ACK是在接收完一个字节后产生的,如果你要手动产生的ACK位置无法设置自动产生ACK,那么你就将该ACK前的字节用I2C_MASTER_CMD_SINGLE_RECEIVE命令来接收。
那么上面一段又产生了一个新的问题,如果这是在一个序列中的一个过程而不是读取单个字节呢?那么就要研究一下“重复起始条件”这个过程了。其实在I2C中对于起始条件和结束条件的要求很不严格,起始条件就是通知从器件占据总线,而结束条件则是要求从器件释放总线,在这中间的起始条件则没有限制。I2C的写操作总是要先指定写入地址然后写入数据,则是一种完全的随机写入方式。而读取则只有一种,当前地址读取,所有的读取操作都是建立在这一基础之上的,这就是为什么在读取之前先要有一个虚拟写入操作。而在读取过程中可以随时中断然后继续读取,都不会有影响。
当然上面的手动发送ACK情况大家应该是找不到特例的,因为I2C总线协议已经很全面了,基本上需要考虑的情况已经完全考虑了,但谁也无法保证滴水不漏,所以才有了上面那些特殊的操作方式。
好了,关于I2C的操作今天就先说这些吧,接下来将对ZLG的程序做一些修改以扩展其应用范围。

点赞  2011-10-6 15:11

本来想对ZLG的4.12例程进行一些扩展,不过后来在EasyARM8962光盘里发现ZLG早已写好了一个完整的软件包,我这里就不再献丑了,函数的用法在头文件里面有说明。

lm硬件I2C软件包.rar (4.13 KB)
(下载次数: 317, 2011-10-9 00:55 上传)

 

不过对4.12进行改造也有一定的必要,那就是其中断程序比较简单,中断运行时间也较短。

对其改造的方法是在主程序中不要打开I2C中断,在读写子程序中,在执行完最后一条地址写入命令后打开中断。同时子程序退出时关闭中断。

 

我在应用中用的就是这种方法,想知道我山寨USB-Blaster的成果吗?

我在空白24C64芯片上调试完成读写程序后(串口回报数据完全正确),把USB-Blaster上的24C64连上开发板,准备改成只读程序后读出程序。插上电后看到程序运行指示灯闪烁(我增加的读写指示灯)心里大叫糟糕,芯片中原来的写入程序还在

 

就这样,我的USB-Blaster挂掉了

 

我在连接有程序的24C64的时候为了防止误操作,还把WP通过一个电阻上拉了,但是还是擦除了程序,后来查看了芯片手册,原来其保护功能只是保护的最高四分之一的存储区域。手册中用了quandrant这个词,查了一下,这个词意为“象限”,不知道怎么跟四分之一联系上的,难道仅仅是从一个平面有四个象限引申出来的意思。

点赞  2011-10-9 00:55
讲的很好,谢谢楼主的东东。
点赞  2011-10-9 13:23
讲的很好
点赞  2011-10-11 17:19

回复 沙发 柳叶舟 的帖子

楼主是否知道,为什么主发送后,从的 CLK 一直是低电平?
点赞  2011-11-11 14:56
拿去看看,给楼主顶一个帖子,算是对于楼主给大家做贡献的一个鼓励吧,嗯嗯,值得值得,希望楼主继续努力啊!加油
我爱电子!
点赞  2011-11-16 16:21
找了好久那硬件的软件包。。。
点赞  2012-7-18 10:57
引用: 原帖由 Study_Stellaris 于 2011-11-11 14:56 发表
楼主是否知道,为什么主发送后,从的 CLK 一直是低电平?
不好意思,很少关注时间比较久的帖子,也一直没有发现你的回复。
是这样的,在I2C 协议中,当一个传输过程没有结束之前(指停止位)器件处于工作状态,无法响应时,就会拉低CLK 电平,其他器件靠检测CLK 电平来确定当前总线是否空闲。所以,对于发送之后从器件需要响应时间的,从器件会拉低CLK。而在发送过程中,当发送完一个字节后(指已经完成了ACK 应答),如果没有发送停止位,主器件会拉低CLK 电平从而避免总线被其他器件抢占而无法完成一次完整的数据传送。当你发送完停止位后,CLK 会恢复高电平。
点赞  2012-9-21 15:46
TI的I2C可以既做主设备又做从设备吗
点赞  2012-9-23 08:01

附件中的程序包有问题

看了lm硬件I2C软件包.rar里的代码,如果我没理解错的话,里面带器件子地址任意字节写的函数ISendStr()不能只写单字节数据。因为在中断里面对STATE_WRITE_NEXT状态的发送数据操作是先发送数据,然后字节长度减一,最后才判断字节长度是否为1(最后一个字节)。这样的话,若是发送字节长度为1,则会由于被减1后,长度变成0,接着在后面的最后字节判断中得不到正确结果,从而导致发送出错。
点赞  2012-11-30 16:43
我想用不进入中断,用轮询方式发送数据,为什么发的是三个字节,而从机接收到的是四个字节呢
很是郁闷
点赞  2013-1-11 17:33
能不能请哪位大侠帮一下忙呀,很急,纠结了两天的东西了
点赞  2013-1-11 17:33
如果用轮询方式读从机数据,程序应该怎么写,希望指点一下,可以发到我邮箱
,谢谢
点赞  2013-1-15 09:39
博主,想问一下,初始化需要具体指明是I2C0还是I2C1吗?还是笼统的初始化就可以了?特别是设备使能的时候!
点赞  2013-3-1 16:04
好东西
点赞  2014-8-23 20:10
我用LM3S 9B81,ARM做主机,且只有一个主机读取一个丛机。有时会发生I2C仲裁丢失的情况,至今原因未知,然后只能断电,重启。
点赞  2015-3-22 14:27
顶楼主对I2C的解读,不过,周立功给的例程也非原创,
而是TI的例程,其所用芯片不是24c02而是atmel的
AT2408,应该可以解决你提到的问题---仅猜想,
TI的例程命---i2c_atmel
good
点赞  2015-4-4 13:53
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复