0.序

我用的一个PCA9536老是出问题,怀疑是IIC应答或者停止位出问题了,所以特地来仔细看看IIC的原理和操作。

(已经排除软件问题,是硬件电路问题)

本文使用软件模拟IIC,没有使用硬件自带IIC,软件模拟用起来很香,我在51和STM32上用一样的代码了,除了IO口使用方式不一样,其他都一样。

本文前面的介绍和后面最终给的代码有一点出入,因为我把芯片看错了,领导说是24c02,我写了半天发现用不了,仔细拿放大镜看才发现是24c64, ヽ(ー_ー)ノ

1. IIC简介   

IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接 微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。 在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。目前大部分 MCU 都带有 IIC 总线接口,STM32F1 也不例外。

但是这里我们不使用 STM32F1 的硬件 IIC 来读写 24C02,而是通过软件模拟。ST 为了规避飞利浦 IIC 专利问题,将 STM32 的硬件 IIC 设计的比较复杂,而且稳定性不怎么好,所以这里我们不推荐使用。有兴趣的读者可以研究一下 STM32F1 的硬件 IIC。

用软件模拟 IIC,最大的好处就是方便移植,同一个代码兼容所有 MCU,任何一个单片机只要有 IO 口,就可以很快的移植过去,而且不需要特定的 IO 口。而硬件 IIC,则换一款 MCU, 基本上就得重新搞一次,移植是比较麻烦的,这也是推荐使用软件模拟 IIC 的另外一个原因。


2. 三种信号

器件不同对应的IIC时序不同,因此信号可能稍有差异。本文使用的是24C02,时序如下图所示。

CubeMx配置767的QSPi外接W25Q128_stm32

开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。

结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。

应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲, 表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。

在原子的参考手册里有这样一句话,"这些信号中,起始信号是必需的,结束信号和应答信号,都可以不要",具体情况我不清楚,但是我用的一个PCA9536老是出问题,怀疑是IIC应答或者停止位出问题了,所以特地来仔细看看IIC的原理和操作。

3. 硬件端口

我用的是正点原子精英板子,型号是F103ZET6,板子24C02硬件接口如下

CubeMx配置767的QSPi外接W25Q128_停止信号_02

我以为是任意IO口模拟IIC

CubeMx配置767的QSPi外接W25Q128_数据_03

没想到使用CubeMX中选了IIC之后,自动就出来PB6,PB7,看来他用的还是IIC特定的IO口

CubeMx配置767的QSPi外接W25Q128_stm32_04

4. 硬件资料——时序

如图,直接看24C02的datasheet,下图是时序图

CubeMx配置767的QSPi外接W25Q128_数据_05

下图是起止信号和应答信号

CubeMx配置767的QSPi外接W25Q128_数据_06

5. 硬件资料——地址

对于IIC,在代码层面,我们主要关注的就是他的设备地址,还有读写指令,如下图示设备地址,这里我用的是24C02,也就是2k的这个。

可以看出,前4位是1010,也就是十六进制的A。

CubeMx配置767的QSPi外接W25Q128_数据_07

再看看A2,A1,A0三位,我查了资料说是用来做多个IIC设备挂在一个IIC总线上,用来区分地址的。我们这里只用1个24C02把这三个都置0就可以了。

最后一位是读写操作,读的话置1,写的话置0。

CubeMx配置767的QSPi外接W25Q128_stm32_08

CubeMx配置767的QSPi外接W25Q128_停止信号_09

如下图是某位同学使用的多IIC设备,不过他貌似遇到了点问题

CubeMx配置767的QSPi外接W25Q128_停止信号_10

6. 硬件资料——读写操作

写步骤:

起始信号——发送设备地址——收到应答——发送写地址——收到应答——写一个字节——收到应答——停止信号

CubeMx配置767的QSPi外接W25Q128_stm32_11

 

读步骤:

起始信号——发送设备地址——收到应答——收到一个字节——单片机主机应答——停止信号

CubeMx配置767的QSPi外接W25Q128_软件设计_12

 

7. 软件设计——开始信号

如图,开始时候,SDA和SCL都从高电平变成低电平

CubeMx配置767的QSPi外接W25Q128_停止信号_13

看看下降沿和上升沿的时间

CubeMx配置767的QSPi外接W25Q128_数据_14

看看上升下降延时,这里有最大时间,没有最小时间,因此我们不需要对电平跳变进行延时,只要保证SDA先于SCL跳变就行

CubeMx配置767的QSPi外接W25Q128_stm32_15

代码表示就是这样的,借用原子哥的一段代码,从高电平到低电平,实际上就是让GPIO输出高低电平,因此前面还有一个SDA_OUT(),这个是原子写的寄存器方法配置IO口,学过51的同学会很熟悉(感兴趣的同学可以去看看我的51相关文章)。

原子在高低电平转换的地方加了一个4us的延时,我这里去掉试试。

CubeMx配置767的QSPi外接W25Q128_数据_16

CubeMx配置767的QSPi外接W25Q128_数据_17

//产生IIC起始信号
void IIC_Start(void)
{
	SDA_OUT();     //sda线输出
	IIC_SDA=1;	  	  
	IIC_SCL=1;
	delay_us(4);
 	IIC_SDA=0;//START:when CLK is high,DATA change form high to low 
	//delay_us(4);
	IIC_SCL=0;//钳住I2C总线,准备发送或接收数据 
}

这里看到cubeMX自带的代码,最后面尝试分析一下这些代码的作用(当前先用软件模拟分析IIC的原理,最后再使用CubeMX自带代码试试)

CubeMx配置767的QSPi外接W25Q128_数据_18

8. 软件设计——停止信号

同理可得停止信号(因为另一个项目刚好用上24c02,这里详细分析一下,因此后面代码会改成51单片机的代码,因为用的是软件模拟,因此本代码与平台无关)

CubeMx配置767的QSPi外接W25Q128_数据_19

CubeMx配置767的QSPi外接W25Q128_停止信号_20

void EEP_I2c_Stop(void)
{
    SCL=0;
    SDA=0;
    DelayUs(2);
    SCL=1;
    SDA=1;
    DelayUs(2);
}

9. 软件设计——应答信号

可以看到,在每个写命令后面都会跟上一个应答信号

这里把前面的三种信号里面说的应答信号再复制一遍:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲, 表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。

CubeMx配置767的QSPi外接W25Q128_stm32_21

我们在这里输出一下,前面加一个0x99是为了区分是哪一帧的数据

CubeMx配置767的QSPi外接W25Q128_stm32_22

这里对0xff写一个0x33

CubeMx配置767的QSPi外接W25Q128_数据_23

在这里输出SDA的值

CubeMx配置767的QSPi外接W25Q128_软件设计_24

我们通过串口输出SDA看看,发现只有三个0,说明在数据完成后,会直接把SDA置为0,因为前面SDA=1做过一次置1操作

CubeMx配置767的QSPi外接W25Q128_软件设计_25

实际上我发现在数据写完之后,24C02会立刻把SDA置0,并且是持续的置0脉冲。这个我们在后面写数据里面分析。

10. 软件设计——写数据

本节先做指定地址单字节写操作,页写暂时不做分析

写一个字节时序如下

CubeMx配置767的QSPi外接W25Q128_软件设计_26

写一个字节对应代码如下 

CubeMx配置767的QSPi外接W25Q128_软件设计_27

这个操作是从高位起,一个位一个位的向设备写入数据 0x80对应1000 0000。

CubeMx配置767的QSPi外接W25Q128_stm32_28

向指定地址写一个字节,使用下面函数,主要步骤如下:起始信号——发送设备地址——收到应答——发送写地址——收到应答——写一个字节——收到应答——停止信号

CubeMx配置767的QSPi外接W25Q128_数据_29

这里说说上一节说过的SDA置零问题

在SCL跳变之前打印SDA,代码和打印结果如下

CubeMx配置767的QSPi外接W25Q128_数据_30

CubeMx配置767的QSPi外接W25Q128_stm32_31

在SCL跳变之后打印SDA,代码和打印结果如下,可以看到在最后SDA被置零,结合前面的应答信号来看,在数据写完之后,存储芯片发会送连续的低电平脉冲,因为在应答信号中我们先把SDA置为1,然后还是会得到0的结果

CubeMx配置767的QSPi外接W25Q128_数据_32

CubeMx配置767的QSPi外接W25Q128_数据_33

10. 软件设计——读数据

读数据3种方式

第一种是读数据流,发送设备地址之后就会读第一个数据

CubeMx配置767的QSPi外接W25Q128_stm32_34

第二种是随机读取,我们需要用到的也是这种方式

起始信号——发送设备地址——收到应答——发送需要读取的数据地址——收到应答——发送设备地址——收到应答——收到一个字节——单片机主机应答——停止信号

CubeMx配置767的QSPi外接W25Q128_软件设计_35

第三种是连续读取,在第一种方式的基础上读取指定个数的字节,本节暂时不讨论次方法

CubeMx配置767的QSPi外接W25Q128_软件设计_36

读取一个字节如下

CubeMx配置767的QSPi外接W25Q128_数据_37

从特定地址读一个字节如下

CubeMx配置767的QSPi外接W25Q128_数据_38

看下读取结果

左边是往0xf0地址写数据0x33,右边是从0xf0地址读数据,发现读出来的是0xff,读取结果不正确

CubeMx配置767的QSPi外接W25Q128_数据_39

看了原子的代码发现,我和它的唯一区别是地址不同,于是加上了

CubeMx配置767的QSPi外接W25Q128_软件设计_40

发现结果还是一样

CubeMx配置767的QSPi外接W25Q128_软件设计_41

难道是地址不对??

CubeMx配置767的QSPi外接W25Q128_软件设计_42

结果还是不对???

CubeMx配置767的QSPi外接W25Q128_软件设计_43

11. 软件设计——发现错误

仔细看我的存储芯片型号发现wtmd用的是24c64,不是24c02,本文以上内容代码部分大家不要看了,后面重新写,啊啊啊啊啊!!!

12. 重新开始写代码

代码内容和上面描述的相差不大,少许差别,仔细可以看出来。

一个是地址,8位改成16位,一个是写完延时5ms。

#include <IIC.H> //内存读写 EEprom
#include <Delay.h>

sbit SCL=P5^5;
sbit SDA=P5^4;

void EEP_I2c_Start(void)
{
    SDA=1;
    SCL=1;
    DelayUs(2);
    SDA=0;
    SCL=0;
}

void EEP_I2c_Stop(void)
{
    SCL=0;
    SDA=0;
    DelayUs(2);
    SCL=1;
    SDA=1;
}

u8 IIC_hostWait_Ack(void)//ack creat from slave
{
    u8 ucErrTime=0;
    //SDA_IN();      //SDA设置为输入
    SDA=1;
    DelayUs(2);
    SCL=1;
    DelayUs(2);
    while(SDA==1)
    {
        //PutChar(SDA);
        ucErrTime++;
        if(ucErrTime>22)
        {
            EEP_I2c_Stop();
            return 1;
        }
    }
    SCL=0;
    //PutChar(SDA);
    DelayUs(2);
    return 0;
}

/产生ACK应答//4
void IIC_hostAck(void)
{
    SCL=0;
    SDA=0;
    SCL=1;
    DelayUs(2);
    SCL=0;
}
/不产生ACK应答	//5
void IIC_hostNAck(void)
{
    SCL=0;
    SDA=1;
    DelayUs(2);
    SCL=1;
    DelayUs(2);
    SCL=0;
}
/------------
void EEP_I2c_WriteByte(u8 txByte)
{
    u8 t;
    SCL=0;//拉低时钟开始数据传输
    for(t=0; t<8; t++)
    {
        SDA=(txByte&0x80)>>7;
        //PutChar(SDA);
        txByte<<=1;
        DelayUs(2);
        SCL=1;
        //PutChar(SDA);  //数据传输完成之后 SCL改变之前SDA不会被置零
        DelayUs(2);
        SCL=0;
        DelayUs(2);
        //PutChar(SDA);  //数据传输完成之后 会被置0
    }
//check slave ack after send write
}

/------------7
u8 EEP_I2c_ReadByte( u8 ack)
{
    u8 i,receive;
    for(i=0; i<8; i++ )
    {
        SCL=0;
        DelayUs(2);
        SCL=1;
        //PutChar(SDA);
        receive<<=1;
        if(SDA==1)
            receive |= 1;
        //PutChar(SDA);
        DelayUs(2);
    }
    if (!ack)
        IIC_hostNAck();//发送nACK
    else
        IIC_hostAck(); //发送ACK
    DelayUs(2);
    return receive;
}


/*-------------------------------AT24c02---------------------------------------------*/

u8 AT24CXX_ReadOneByte(u8 ReadAddr)
{
    u8 temp=0;
    EEP_I2c_Start();
    EEP_I2c_WriteByte(0XA0);	   //发送写命令
    IIC_hostWait_Ack();
    EEP_I2c_WriteByte(ReadAddr>>8);//发送高地址
    IIC_hostWait_Ack();
    EEP_I2c_WriteByte(ReadAddr);   //发送地址
    IIC_hostWait_Ack();
    EEP_I2c_Start();
    //PutChar(0x99);
    EEP_I2c_WriteByte(0XA1);           //进入接收模式
    IIC_hostWait_Ack();
    //PutChar(0x88);
    temp=EEP_I2c_ReadByte(0);
    EEP_I2c_Stop();//产生一个停止条件
    return temp;
}
/
//AT24CXX  Byte Write
//WriteAddr  :写入数据的目的地址
//DataToWrite:要写入的数据
void AT24CXX_WriteOneByte(u8 WriteAddr,u8 DataToWrite)
{
    EEP_I2c_Start();
    //PutChar(0x99);
    //EEP_I2c_WriteByte(0XA0);
    //EEP_I2c_WriteByte(0XA0+((WriteAddr/256)<<1)); //发送器件地址0XA0,写数据
    EEP_I2c_WriteByte(0XA0);	    //发送写命令
    IIC_hostWait_Ack();
    EEP_I2c_WriteByte(WriteAddr>>8);//发送高地址
    IIC_hostWait_Ack();
    //PutChar(0x99);
    EEP_I2c_WriteByte(WriteAddr);   //发送地址
    IIC_hostWait_Ack();
    //PutChar(0x99);
    EEP_I2c_WriteByte(DataToWrite);     //发送字节
    IIC_hostWait_Ack();
    EEP_I2c_Stop();//产生一个停止条件
    DelayMs(5);
}