I2C通信(Inter-Integrated Circuit)
本文主要介绍了I2C总线协议的基础知识、物理层和协议层的相关内容,并通过软件模拟I2C的方式实现了对EEPROM的读写操作。最后,通过主程序演示了如何使用I2C协议进行数据存储和读取。
声明
此下是学习(跟尚硅谷视频学习的,里面的内容是根据其课程的笔记)过程中的笔记和自己的认识,希望也可以帮助到大家。有错误的地方可以一起讨论。
1.基础知识
- 引脚少,硬件实现简单,可移植性。
- 广泛应用在系统内多个集成电路(IC)间的低速通信。
- 简单的双向两线制总线协议标准,支持同步串行半双工通讯。(所以有一根是时钟线,另一根是信号线,复合同步半双工)。
- 传输速率:标准模式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 响应
接收方收到数据后要给发送方响应,有两种响应:
- 应答响应,给发送方一个低电平。
- 非应答响应,处于高电平状态。

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();
}
}
更多推荐



所有评论(0)