声明

此下是学习(跟尚硅谷视频学习的,里面的内容是根据其课程的笔记)过程中的笔记和自己的认识,希望也可以帮助到大家。有错误的地方可以一起讨论。

1.基础知识

  1. 引脚少,硬件实现简单,可移植性。
  2. 广泛应用在系统内多个集成电路(IC)间的低速通信。
  3. 简单的双向两线制总线协议标准,支持同步串行半双工通讯。(所以有一根是时钟线,另一根是信号线,复合同步半双工)。
  4. 传输速率:标准模式100kbit/s,快速模式400kbit/s,高速模式下可达3.1Mbit/s,目前大多数i2c不支持高速模式。

2.物理层

2.1 认识

SCL(Serial Clock):串行时钟总线,用于数据收发同步。同步时钟的作用。

SDA(Serial Data):串行数据总线,用于数据的传输,用高低电平表示数据。

可连接多个I2C设备,支持一主多从,多主多从

每个设备都有一个唯一的地址,可以通过地址与从机通信。

当然主设备与从设备有区别,只有主设备能控制时钟。时钟线上的时钟是由主设备提供。

时钟由主设备提供,起到一二一喊口号的作用。

总线通过上拉电阻接到电源,连接在I2C上面的设备都是进行开漏输出。不进行数据传输时,设备都是高阻态,总线呈现高电平,当要进行数据传输时,设备把电平拉低,总线呈现低电平。

一般都是由SDA数据线传输数据,当主机发送数据过快,从机接收不过来的时候,从机会向SCL时钟线发送0,来降低速度。这是扩展的状态。

3.协议层

3.1 起始和停止

那么:已知设备通过拉低数据线来传输数据,那么如果只是用数据线SDA传输,怎么辨别第一个0信号是传输的0信号还是设备要开始通信时的提醒信号呢

我们就要用到时钟线(SCL):我们有一个起始信号就是时钟线SCL处于高电平,数据线SDA有一个拉低的动作(高电平->低电平的切换)。

停止信号:SCL在高电平的时候,SDA从低电平到高电平切换

在SCL低电平的时候才进行数据传输

在SCL高电平的时候一般进行数据的采样

在程序中,我们先宏定义一下:建议先看完理论再看程序

#define SCL_HIGH  (GPIOB->ODR|=GPIO_ODR_ODR10)
#define SCL_LOW   (GPIOB->ODR&=~GPIO_ODR_ODR10)
#define SDA_HIGH  (GPIOB->ODR|=GPIO_ODR_ODR11)
#define SDA_LOW   (GPIOB->ODR&=~GPIO_ODR_ODR11)
#define SDA_WRITE (GPIOB->IDR&GPIO_IDR_IDR11)
#define IIC_DELAY Delay_us(10)
void iic_start(void)
{
    //首先在SCL和SDA都为高电平
    SCL_HIGH;
    SDA_HIGH;
    IIC_DELAY;
    //在SCL处于高电平状态,SDA拉低,表示开始
    SDA_LOW;
    IIC_DELAY;
    
}
void iic_stop(void)
{
    //首先在SCL为高电平 SDA为低电平
    SCL_HIGH;
    SDA_LOW;
    IIC_DELAY;
    //在SCL处于高电平状态,SDA拉高,表示结束
    SDA_HIGH;
    IIC_DELAY;
}

3.2 传输地址

主机通过从机的地址来查找从机

从机的地址可以是7位或者10位,实际运用中7位比较广泛。2的7次方为128,可以挂100多个设备。

紧跟地址位的数据位是数据传输方向的信号,他是数据方向位(R/W)(1/0),为1时表示主机要从从机读取数据,为0时表示要向从机写入数据。

3.3 数据的有效性

我们知道了数据传输是怎么开始和结束的,那数据到底是怎么传输的呢?

SDA在SCL为高电平时要保持数据。否则就是停止信号或者起始信号。

当SCL为低电平时向SDA写数据(与起始和停止区别开),当SCL为高电平时,从数据线上读数据

3.4 响应

接收方收到数据后要给发送方响应,有两种响应:

  1. 应答响应,给发送方一个低电平。
  2. 非应答响应,处于高电平状态。

4.软件模拟I2C

EEPROM用的协议就是I2C。我们用I2C来写一个。

4.1 型号 (M24C02)

已知400和100是传输的速度。一个EEPROM大小一般是256个bytes大概为2的8次方

16个字节为一页,一共16页。

其中,E1,E2,E3,3位接地为0,WC#是写保护:

  • 当 WC# 接高电平(VCC)时,芯片的写操作被禁止(保护模式),无法通过 I²C 接口修改存储器内容(包括写入、擦除等操作)。

  • 当 WC# 接低电平(GND)时,写操作允许,可以正常通过 I²C 接口读写数据。

4.2 地址

通过主机向从机写入时,首先要通过地址找到从设备。

前4位是已经固定好的,后3位由信号线的电平决定,这7位决定了EEPROM的地址。

RW位代表read/write,读和写操作,代表数据的传输方向,当RW=1时代表read,RW=0代表write。

所以“0xA0”代表主机向从设备写入数据,“0xA1”代表主机向从机读取数据

4.3 写读字节

4.3.1 写一个字节

4.3.2 读一个字节

注意:RW为1,且如果读字节的地址与数据传输方向的信号0xA1发出后,数据发送方变成从设备,此时就不能设置传输的数据存放在哪一个地址中,所以,我们先0xA0,设置写,然后传输数据要存放的地址,然后结束,再传0xA1,数据就会从上面传输地址开始存放地址

4.3.3 单次写多个字节

注意:一次性写入多个字节也叫页写入,因为这个芯片(M24C02)一页只有16个字节,所以我们单次最多写入16个字节,若超过16个字节,将从这一页的头部重新写。

4.3.4 单次读多个字节

对于读多个字节,主设备只需要ACK应答而不是(NO ACK)这样从设备继续发直到主设备(NO ACK)。

5.全部的程序:

I2C.h

#ifndef _I2C_H_
#define _I2C_H_

#include"stm32f10x.h"

#define SCL_HIGH  (GPIOB->ODR|=GPIO_ODR_ODR10)
#define SCL_LOW   (GPIOB->ODR&=~GPIO_ODR_ODR10)
#define SDA_HIGH  (GPIOB->ODR|=GPIO_ODR_ODR11)
#define SDA_LOW   (GPIOB->ODR&=~GPIO_ODR_ODR11)
#define SDA_WRITE (GPIOB->IDR&GPIO_IDR_IDR11)
#define IIC_DELAY Delay_us(10)

//SCL PB10 cnf 01  mode 11    SDA PB11
//初始化
void iic_init(void);
//开始信号
void iic_start(void);
//停止信号
void iic_stop(void);
//主机应答
void iic_Ack(void);
//主机不应答
void iic_Nack(void);

uint8_t wait_ack(void);
//写信号
void iic_write(uint8_t byte);
//读信号
uint8_t iic_read(void);


#endif

I2C.c

#include"I2C.h"
#include"Delay.h"

//初始化
void iic_init(void)
{
     RCC->APB2ENR|=RCC_APB2ENR_IOPBEN;

    GPIOB->CRH|=GPIO_CRH_MODE10;
    GPIOB->CRH|=GPIO_CRH_CNF10_0;
    GPIOB->CRH&=~GPIO_CRH_CNF10_1;
    GPIOB->CRH|=GPIO_CRH_MODE11;
    GPIOB->CRH|=GPIO_CRH_CNF11_0;
    GPIOB->CRH&=~GPIO_CRH_CNF11_1;
}
//开始信号
void iic_start(void)
{
    //首先在SCL和SDA都为高电平
    SCL_HIGH;
    SDA_HIGH;
    IIC_DELAY;
    //在SCL处于高电平状态,SDA拉低,表示开始
    SDA_LOW;
    IIC_DELAY;
    
}
//停止信号
void iic_stop(void)
{
    //首先在SCL为高电平 SDA为低电平
    SCL_HIGH;
    SDA_LOW;
    IIC_DELAY;
    //在SCL处于高电平状态,SDA拉高,表示结束
    SDA_HIGH;
    IIC_DELAY;
}

//主机应答
//对于应答信号,在SCL为低的时候 SDA拉低
void iic_Ack(void)
{
    //1.SCL为低,SDA为高
    SCL_LOW;
    SDA_HIGH;
    IIC_DELAY;
    //2.SCL为低,SDA拉低
    SDA_LOW;
    IIC_DELAY;
    //3.SDA保持,SCL拉高,保持,采样
    SCL_HIGH;
    IIC_DELAY;
    //4.SCL拉低
    SCL_LOW;
    IIC_DELAY;
    //5.SCL保持 SDA还原拉高
    SDA_HIGH;
    IIC_DELAY;
}
//主机不应答
void iic_Nack(void)
{
    SCL_LOW;
    SDA_HIGH;
    IIC_DELAY;

    SCL_HIGH;
    IIC_DELAY;

    SCL_LOW;
    IIC_DELAY;
}

uint8_t wait_ack(void)
{
    //1.SCL拉低,SDA拉高
    SCL_LOW;
    SDA_HIGH;
    IIC_DELAY;

    //2.SCL拉高,
    SCL_HIGH;
    IIC_DELAY;
    //3.采样
    uint16_t ack=SDA_WRITE;
    //4.SCL拉低
    SCL_LOW;
    IIC_DELAY;

    return ack? 1:0;
}
//写信号
void iic_write(uint8_t byte)
{
    //1.SDA拉低SCL拉低
    SCL_LOW;
    SDA_LOW;
    IIC_DELAY;
    //2.SDA开始写数据,SCL不变
    /*在IIC(总线中,数据是按字节(8位)进行传输的。在单字节写入作中,通常是从高位先写。
    具体来说,IIC协议规定数据字节的传输是从最高位(MSB,Most Significant Bit)开始,
    然后逐步传输到最低位(LSB,Least Significant Bit)。*/
    for(uint8_t i=0;i<8;i++)
    {
        if(byte & 0x80)
        {
            SDA_HIGH;
        }
        else
        {
            SDA_LOW;
        }
        IIC_DELAY;

        //3.SCL拉高,采集
        SCL_HIGH;
        IIC_DELAY;

        //4.采样完成 SCL拉低
        SCL_LOW;
        IIC_DELAY;

        //5.拉低又要开始传输数据,回到循环,此时要传输第二个
        byte<<=1;
    }
    
}

uint8_t iic_read(void)
{
    uint8_t data=0;
    //1.先把SCL拉低,SDA不能碰,因为是接收信号
    SCL_LOW;
    IIC_DELAY;

    for(uint8_t i=0;i<8;i++)
    {
        //2.把SCL拉高,要开始采集信号
        SCL_HIGH;
        IIC_DELAY;

        //3.开始采集信号
        data<<=1;
        if (SDA_WRITE)
        {
            data|=0x01;
        }
        IIC_DELAY;
        //4.采集完成 SCL拉低
        SCL_LOW;
        IIC_DELAY; 
    }
    return data;
}

eeprom.c

#include"eeprom.h"

void eeprom_init(void)
{
    iic_init();
}
//写入一个字节
void eeprom_write(uint8_t add,uint8_t data)
{
    iic_start();
    iic_write(0xA0);
    wait_ack();

    iic_write(add);
    wait_ack();

    iic_write(data);
    wait_ack();
    iic_stop();
    Delay_ms(10);
}
//接受一个字节
uint8_t eeprom_read(uint8_t add)
{
    uint8_t data=0;
    iic_start();
    iic_write(0xA0);
    wait_ack();

    iic_write(add);
    wait_ack();

    iic_start();
    iic_write(0xA1);
    wait_ack();

    data=iic_read();
    iic_Nack();

    iic_stop();
    return data;
}
//写入多个字节
void eemprom_writebytes(uint8_t add,uint8_t *buf,uint8_t size)
{
    iic_start();
    iic_write(0xA0);
    wait_ack();

    iic_write(add);
    wait_ack();

    for(uint8_t i=0;i<size;i++)
    {
        iic_write(*buf++);
        wait_ack();
    }
    iic_stop();
    Delay_ms(10);
}
//读取多个字节
void eeprom_readbytes(uint8_t add,uint8_t *buf,uint8_t size)
{
    iic_start();
    iic_write(0xA0);
    wait_ack();

    iic_write(add);
    wait_ack();

    iic_start();
    iic_write(0xA1);
    wait_ack();

    for(uint8_t i=0;i<size;i++)
    {
        *buf++=iic_read();
        if(i<size-1)
        {
            iic_Ack();
        }
        else
        {
            iic_Nack();
        }
    }
    iic_stop();
}

main.c

#include"Delay.h"
#include"led.h"
#include "usart.h"
#include "i2c.h"
#include"eeprom.h"
int main()
{
   usart_init();
   iic_init();
   printf("anccd\n");
	 eeprom_write(0x00,'a');
    eeprom_write(0x01,'b');
    eeprom_write(0x02,'c');
    printf("%c\t %c\t %c\t",eeprom_read(0x00),eeprom_read(0x01),eeprom_read(0x02));
	
	uint8_t buffer[100]={0};
	eemprom_writebytes(0x00,"123456",6);
	eeprom_readbytes(0x00,buffer,6);
	printf("%s",buffer);
   while(1)
   {
      
   }
}

对其进行硬件实现:

6.硬件部分

STM32有专门负责协议的I2C外设,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器,就能完成数据收发。

I2C外设还支持 SMBus2.0协议,SMBus协议与I2C类似。

6.1 内部的电路

6.2 寄存器

CR2主要是用于时钟频率配置

注意:修改CR1前需确保I2C处于禁用状态(PE=0),配置完成后再使能(PE=1)。

7. 代码部分分析

7.1 起始和终止

因为本人代码跑失败了,大家如果有知道哪里不对的可以评论。

I2C.c

#include"I2C.h"
#include"Delay.h"
#include"usart.h"

//初始化
void iic_init(void)
{
    //1.开启时钟
    RCC->APB1ENR|=RCC_APB1ENR_I2C2EN;
    RCC->APB2ENR|=RCC_APB2ENR_IOPBEN;
    

    //2.配置PB10 SCL CNF 11 MODE  11    PB11 SDA    复用开漏输出
    // GPIOB->ODR|=GPIO_ODR_ODR11;
    // GPIOB->ODR|=GPIO_ODR_ODR10;
    GPIOB->CRH|=(GPIO_CRH_CNF10|GPIO_CRH_CNF11 |GPIO_CRH_MODE10 |GPIO_CRH_MODE11);

    //3.配置I2C模式而不是SMBUS
    I2C2->CR1 &=~I2C_CR1_SMBUS;

    //4.配置速度大小  100kbit/s
    I2C2->CCR&=~I2C_CCR_FS; 
    //5.配置时钟  36M  
    I2C2->CR2 |=36;
    I2C2->CCR |=180;
    I2C2->TRISE |=37;

    I2C2->CR1|=I2C_CR1_PE;
}

uint8_t iic_start(void)
{
    I2C2->CR1|=I2C_CR1_START;       //注意:当主机发送开始信号时,如果数据线SDA正在传输数据,他就会一直向SDA总线发送信号

    uint16_t timeout=0xffff;
    while(!(I2C2->SR1&I2C_SR1_SB)&&timeout)    //判断开始信号是否发送成功,超时则发送失败,因为这是阻塞的。
    {
       timeout--;
    }    
        printf("start finsh\n");
    return timeout? OK:FAIL;
}

void iic_stop(void)
{
    I2C2->CR1|=I2C_CR1_STOP;
}

void iic_ack(void)
{
    I2C2->CR1|=I2C_CR1_ACK;
}

void iic_nack(void)
{
    I2C2->CR1&=~I2C_CR1_ACK;
}

uint8_t iic_sendaddr(uint8_t addr)
{
    I2C2->DR=addr;//因为地址是第一个传输,所以不需要判断数据寄存器中是否为空。

    uint16_t timeout=0xffff;
    while (((I2C2->SR1 &I2C_SR1_ADDR)==0)&&timeout)
    {
        timeout--;
    }
   
    I2C2->CR2;

    return timeout? OK:FAIL;
}

uint8_t iic_send(uint8_t data)
{
    uint16_t timeout=0xffff;
    while (((I2C2->SR1 & I2C_SR1_TXE)==0) && timeout)
    {
        timeout--;
    }
    

    I2C2->DR=data;

    timeout=0xffff;
    while((I2C2->SR1 & I2C_SR1_BTF)==0 && timeout)
    {
        timeout--;
    }
    
    return timeout? OK:FAIL;
}

uint8_t iic_receive(void)
{
    uint16_t timeout =0xffff;
    while (((I2C2->SR1 & I2C_SR1_RXNE)==0) && timeout)
    {
        timeout--;
    }


    return timeout? I2C2->DR:0; 
}

eeprom.c

#include"eeprom.h"

void eeprom_init(void)
{
    iic_init();
}


//写入一个字节
void eeprom_write(uint8_t add,uint8_t data)
{
    iic_start();
    iic_sendaddr(0xA0);

    iic_send(add);

    iic_send(data);

   iic_stop();

    Delay_ms(10);
}
//接受一个字节
uint8_t eeprom_read(uint8_t adda)
{
    iic_start();
    iic_sendaddr(0xA0);

    iic_send(adda);

    iic_start();
    iic_sendaddr(0xA1);

    iic_nack();
    iic_stop();

    uint8_t data=iic_receive();

    return data;
}
//写入多个字节
void eemprom_writebytes(uint8_t add,uint8_t *buf,uint8_t size)
{
    iic_start();
    iic_sendaddr(0xA0);

    iic_send(add);

    for (uint8_t i = 0; i < size; i++)
    {
        iic_send(*buf++);
    }
    iic_stop();

    Delay_ms(10);
    
}
//读取多个字节
void eeprom_readbytes(uint8_t add,uint8_t *buf,uint8_t size)
{
    iic_start();
    iic_sendaddr(0xA0);

    iic_send(add);

    iic_start();
    iic_sendaddr(0xA1);

    for (uint8_t i = 0; i < size; i++)
    {
       
        if (i<size-1)
        {
            iic_ack();
        }
        else
        {
            iic_nack();
            iic_stop();
        }
         *buf++=iic_receive();
    }
      
}

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐