STM32F767 I2C通信实验

IIC通信实验

IIC简介

I²C(Inter-Integrated Circuit)字面上的意思是集成电路之间,它其实是I²C Bus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C的正确读法为“I平方C”("I-squared-C"),而“I二C”("I-two-C")则是另一种错误但被广泛使用的读法。自2006年11月1日起,使用I²C协议已经不需要支付专利费,但制造商仍然需要付费以获取I²C从属设备地址。

STM32F767 I2C通信实验_第1张图片
时序图

为使用串行数据线(SDA)和串行时钟线(SCL)、拥有7bit寻址空间的总线。 总线上有两种类型角色的节点:

  • 主节点 - 产生时钟并发起与从节点的通信
  • 从节点 - 接收时钟并响应主节点的寻址

该总线是一种多主控总线,即可以在总线上放置任意多主节点。此外,在停止位(STOP)发出后,一个主节点也可以成为从节点,反之亦然。

总线上有四种不同的操作模式,虽然大部分设备只作为一种角色和使用其中两种操作模式:

  • 主节点发送 - 主节点发送数据给从节点
  • 主节点接收 - 主节点接收从节点数据
  • 从节点发送 - 从节点发送数据给主节点
  • 从节点接收 - 从节点接收主节点数据

一开始,主节点处于主节点发送模式,发送起始位START),跟着发送希望与之通信的从节点的7bit位地址,最后再发送一个bit读写位,该数据位表示主节点想要与从节点进行读(1)还是写(0)操作。

如果从节点在总线上,它将以ACK字符比特位应答(低有效)该地址。主节点收到应答后,根据它发送的读写位,处于发送模式或者接收模式,从节点则处于对应的相反模式(接收或发送)。

地址和数据首先发送最高有效位。 起始位在SCL位高时,由SDA上电平从高变低表示;停止位在SCL为高时,由SDA上电平从低变高表示。其他SDA上的电平变化在SCL为低时发生。

如果主节点想要向从节点写数据,它将发送一个字节,然后从节点以ACK位应答,如此重复。此时,主节点处于主节点发送模式,从节点处于从节点接收模式。

如果主节点想要读取从节点数据,它将不断接收从节点发送的一个个字节,在收到每个字节后发送ACK进行应答,除了接收到的最后一个字节。此时,主节点处于主节点接收模式,从节点处于从节点发送模式

此后,主节点要么发送停止位终止传输,要么发送另一个START比特以发起另一次传输(即“组合消息”)。

拓展

原始的I²C系统是在1980年代所创建的一种简单的内部总线系统,当时主要的用途在于控制由飞利浦所生产的芯片。

  • 1992年完成了最初的标准版本发布,新增了传输速率为400 kbit/s的快速模式及长度为10比特的地址模式可容纳最多1008个节点。
  • 1998年发布了2.0版,新增了传输速率为3.4Mbit/s的高速模式并为了节省能源而减少了电压及电流的需求。
  • 2.1版则在2001年完成,这是一个对2.0版做一些小修正,
  • 3.0版于2007年发布。
  • 2012年2月13日发布Specification Rev. 新增 5-MHz的超快速模式(UFM)。
  • 2012年,第4版增加5 MHz的超快速模式(UFM),使用推挽式逻辑没有上拉电阻新的USDA和USCS线,并增加了制造商指定的ID表。
  • 2012年,第5版修正错误。
  • 在2014年,第6版纠正了两个图。这是目前最新的标准。

实验

信号类型及实验

I2C总线在传送数据过程中共有三种类型的信号,他们分别是:

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

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

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

这些信号中,起始信号是必需的,结束信号和应答信号,都可以不要。I2C总线时序如下图:


STM32F767 I2C通信实验_第2张图片
总线时序

STM32F767上面板载的EEPROM(电子抹除式可复写只读存储器)芯片型号为24C02。该芯片的总容量为256个字节,该芯片通过I2C总线与外部连接,我们本实验就通过I2C来实现24C02的读写。

目前大部分MCU都带有I2C总线接口,STM32F767不例外。但是,我们这里不使用STM32F767的硬件I2C来读写24C02,而是通过软件模拟。ST为了规避飞利浦I2C的专利问题,将STM32的硬件I2C设计的比较复杂,而且稳定性极差,给开发带来非常多的不便,所以这里我们并不推荐使用,有兴趣的可以下来自己查资料,来研究下STM32F767的硬件I2C。

我们在这里使用了软件来模拟I2C协议,这样做的好处是,同一个代码兼容所有的MCU,任何一个单片机只要有IO口,就可以很快的移植过去,而且不需要特定的IO口,只需要简单的更改IO口的定义,就可以快速使用。而硬件I2C,则换一次MCU,基本上等于重新搞一次I2C驱动,非常之麻烦。

I2C的实验功能简介:开机的时候先检测24C02是否存在,然后在主循环里面检测两个按键,其中1个按键(KEY1)用来执行写入24C02操作,另外一个按键(KEY0)用来执行读出操作,在LCD模块上显示相关信息,同时DS0闪烁,提示程序运行正常。

硬件部分

实验需要用到指示灯DS2,以及按键KEY0,1和LCD显示屏,24C02。

前面的硬件咱们都已经基本介绍过了,这里我们只简单介绍以下24C02与STM32F767的连接,24C02的SCL与SDA分别连接在STM32F767的PH4和PH5上的,连接关系如下图:


STM32F767 I2C通信实验_第3张图片
连接图

软件部分

首先来看I2C的初始化,我们要使用软件来模拟,就要让硬件也做出I2C硬件协议相关的工作,所以我们来操作两个IO口来模拟I2C的SCL和SDA就行了,具体方法如下:

I2C初始化

void I2C_Init(void)
{
    GPIO_InitTypeDef I2C_Initure;
    
    __HAL_RCC_GPIOH_CLK_ENABLE();       //使能GPIOH时钟

    //PH4,5初始化设置
    I2C_Initure.Pin  = GPIO_PIN_4 | GPIO_PIN_5;
    I2C_Initure.Mode = GPIO_MODE_OUTPUT_PP;     //推挽输出
    I2C_Initure.Pull = GPIO_PULLUP;             //上拉
    I2C_Initure.Speed = GPIO_SPEED_FAST;        //快速
    
    HAL_GPIO_Init(GPIOH, &IC2_Initure);

    I2C_SDA(1);             //SDA线拉高
    I2C_SCL(1);             //SCL线拉高
}

我们在初始化中,将PH4,5两个IO口设置为推挽输出,然后拉上,并设为快速,然后调用HAL_GOIO_Init初始化函数,并且将两条IO先的输出电平先拉高,符合I2C协议的静默状态。至于后两行代码 I2C_SDA(),I2C_SCL 我们在对应的头文件里面用宏函数来定于,具体如下:

#define I2C_SDA(n)  (n?HAL_GPIO_WritePin(GPIOH, GPIO_PIN_4, GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOH, GPIO_PIN_4, GPIO_PIN_RESET))
#define I2C_SCL(n)  (n?HAL_GPIO_WritePin(GPIOH, GPIO_PIN_5, GPIO_PIN_SET):HAL_GPIO_WritePin(GPIOH, GPIO_PIN_5, GPIO_PIN_RESET))

那么这样SDA,SCL线都已经准备好了,那么要开始发送信号吧,代码如下:

产生I2C起始信号

软件模拟起始信号的代码如下:

void I2C_Strat(void)
{
    SDA_OUT();          //SDA线输出
    I2C_SDA(1);
    I2C_SCL(1);
    delay_us(4);
    I2C_SDA(0);         //在SCL线为高电平时,SDA线拉低为起始信号
    delay_us(4);
    I2C_SCL(0);         //拉低SCL线,准备开始发送或者接收数据
}

其中函数 SDA_OUT() 同样是一个宏函数,定义在头文件中,具体如下:

#define SDA_OUT()   {GPIOH->MODER &= ~(0x3 << (10));GPIOH->MODER |= 0x0 << 10;}

通过函数 I2C_Start() 就可以发送一个开始信号,来发送或者接受数据了,本质上来说,就是我们使用了IO操作来模拟了I2C的开始阶段的电压跳变,非常简单。

产生I2C停止信号

有起始后,需要来停止,代码如下:

void I2C_Stop(void)
{
    SDA_OUT();
    I2C_SCL(0);
    I2C_SDA(0);
    delay_us(4);
    I2C_SCL(1);
    I2C_SDA(1);
}

依然遵从I2C的时序图,在停止信号处,先让SDA线输出,然后将SCL和SDA线拉低,待一段时间后,再将SCL和SDA线全部拉高,回到静默状态。

等待应答信号

在起始信号发送了后,需要等待应答,代码如下:

u8 I2C_Wait_Ack(void)
{
    u8 ucErrTime = 0;

    SDA_IN();           //SDA线切换为输入模式
    I2C_SDA(1); delay_us(1);
    I2C_SCL(1); delay_us(1);
    while(READ_SDA) {
        ucErrTime++;
        if (ucErrTime > 250) {
            IC_Stop();
            return 1;
        }
    }
    I2C_SLC(0);     //时钟线拉低
    return 0;
}

这里用到了两个宏函数,仍然定义在头文件当中,代码如下:

#define SDA_IN()    {GPIOH->MODER &= ~(0x3 << 10); GPIOH->MODER |= 0x0 << 10}
#define READ_SDA    HAL_GPIO_ReadPin(GPIOH, GPIO_PIN_5)     //输入SDA信号

这个函数也很容易理解,参照I2C的时序图,将SDA线设置为了输入模式,并拉高SDA线和SCL线,使用轮询读取PH5的电平值,但SDA线出现低电平,表示应答信号来到,拉低SCL线,return 0,表示接收应答成功。

产生应答信号

在作为接收方时,需要产生应答信号,代码如下:

void I2C_Ack(void)
{
    I2C_SCL(0);
    SDA_OUT();
    I2C_SDA(0);
    delay_us(2);
    I2C_SCL(1);
    delay_us(2);
    I2C_SCL(0);
}

这个函数根据I2C的时序图,将应答信号就可以发送出去了,代码很好理解。

不产生应答信号

如果不产生应答信号,代码如下:

void I2C_NAck(void)
{
    I2C_SCL(0);
    SDA_OUT();
    I2C_SDA(1);
    delay_us(2);
    I2C_SCL(1);
    delay_us(2);
    I2C_SCL(0);
}

和上边的代码反过来就行了,在SCL线拉低后,SDA继续输出高电平,那么就不会产生应答信号了。

I2C发送一个字节

void I2C_Send_Byte(u8 txd)
{
    u8 t;
    SDA_OUT();
    I2C_SCL(0);     //拉低时钟开始数据发送
    for(t = 0; t < 8; t++) {
        I2C_SDA((txd & 0x80) >> 7);
        txd <<= 1;
        delay_us(2);
        I2C_SCL(1);
        delay_us(2);
        I2C_SCL(0);
        delay_us(2); 
    }
}

这个函数的设计也是相当的简单了,一个字节是8位,用for循环,每次发送他的第8位,然后整体向左移动一位,每次发送一位后,通过调整SCL线电平来确定时序。

I2C读取一个字节

有了发送,就相应的来接收就行,代码如下:
u8 I2C_Read_Byte(u8 ack)
{
u8 i,receive = 0;

    SDA_IN();           //SDA线切换为输入,来接收数据
    for(i = 0; i < 8; i++) {
        I2C_SCL(0);
        delay_us(2);
        I2C_SCL(1);
        receive <<= 1;
        if (READ_SDA) receive++;
        delay_us(1);
    }

    if (!ack) {
        I2C_NAck();     //不发送应答信号
    } else {
        I2C_Ack();      //发送ACK信号
    }
    return receive;
}

这个函数和发送字节其实没有什么区别,就是反过来读,然后return就行了,区别在于和用参数来确定要不要发送ack应答信号。

I2C的处理函数,就介绍完了,代码非常简单,就是通过IO操作来设置I2C_SDA及SCL。接下来来看下24C02的处理函数。

初始化I2C接口

void 24CXX_Init(void)
{
    I2C_Init();     //直接调用I2C初始化就行
}

在24CXX指定地址读取一个数据

读操作的时候,要先确定读的地址,所以:
写模式-->写读的地址-->读模式-->读数据


STM32F767 I2C通信实验_第4张图片
mark

代码实现如下:

u8 24CXX_ReadOneByte(u16 ReadAdder)
{
    u8 temp = 0;

    I2C_Start();
    I2C_Send_Byte(0xa0 + ((ReadAdder / 256) << 1)); //发送器件地址0xa0,写数据
    I2C_Wait_Ack();
    I2C_Send_Byte(ReadAdder % 256);     //发送低地址
    I2C_Wait_Ack();
    I2C_Start();
    I2C_Send_Byte(0xa1);                //进入接收模式
    I2C_Wait_Ack();
    temp = I2C_Read_Byte(0);
    I2C_Stop();                         //产生停止信号
    return temp;
}

在开始的时候,首先发送起始信号,然后将要读取数据的地址写入,并发送到E2PROM,分两次,首先发送高8位,然后发送低8位,然后等待ack后,恢复到起始状态,进入接收模式,再一个ack后,就可以读取数据。

在24CXX指定地址写一个数据

写操作的时候,同样先确定写的地址,所以要写模式-->写地址-->写数据
STM32F767 I2C通信实验_第5张图片
mark

代码实现如下:

void 24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
    I2C_Start();
    I2C_Send_Byte(0xa0 + ((WriteAddr / 256) << 1));     //发送器件地址OXA0,写数据
    I2C_Wait_Ack();
    I2C_Send_Byte(WriteAddr % 256);             //发送低地址
    I2C_Wait_Ack();
    I2C_Send_Byte(DataToWrite);
    I2C_Wait_Ack();
    I2C_Stop();             //产生停止信号
    delay_ms(10);
}

这样单字节的写或者读非常繁琐,那么再给他封装一层,来个多字节读写,代码如下:

void 24CXX_WriteLneByte(u16 WriteAddr, u32 DataToWrite, u8 Len)
{
    u8 t;
    for(t = 0; t < Len; t++) {
        24CXX_WriteOneByte(WriteAddr + t, (DataToWrite >> (8 * t)) & 0xff);
    }
}

u32 24CXX_ReadLenByte(u16 ReadAddr, u8 Len)
{
    u8 t;
    u32 temp = 0;
    for (t = 0; t < Len; t++) {
        temp <= 8;
        temp += 24CXX_ReadOneByte(ReadAddr + Len -t - 1);
    }
    return temp;
}

这2个函数非常好理解,就是用for循环来调用单字节读写函数即可。

这里最好还需要一个函数来检测24C02的状态,当IC出错时能够反馈错误,代码如下:

u8 24CXX_Check(void)
{
    u8 temp;
    temp = 24CXX_ReadOneByte(255);      //避免每次开机都写24CXX
    if (temp == 0x55) return 0;
    else {                              //排除第一次初始化
        24CXX_WriteOneByte(255, 0x55);
        temp = 24CXX_ReadOneByte(255);
        if (temp == 0x55) return 0;
    }
    return 1;
}

这个函数就是使用24XX的最后一个地址(255)来存储标志字0x55,通过判断0x55来看是不是24C02设备,如果这里使用的其他24C系列,需要更改这个地址。

再定义两个在指定地址读写指定长度的数据的函数,代码如下:

void 24CXX_Read(u16 ReadAddr, u8  *pBuffer, u16 NumToRead)
{
    while(NumToRead) {
        *pBuffer++ = 24Cxx_ReadOneByte(ReadAddr++);
        NumToRead--;
    }
}

void 24CXX_Write(u16 WriteAddr, u8 *pBuffer, u16 NumToWrite)
{
    while(NumToWrite--) {
        24CXX_WriteOneByte(WriteAddr, *pBuffer);
        WriteAddr++;
        pBuffer++;
    }
}

以上的代码基本可以支持24C02了,我们的正点原子的开发板,把24C02地址引脚都设置为0。

你可能感兴趣的:(STM32F767 I2C通信实验)