文章目录
- 1. IIC总线协议
- 1. IIC简介
- 2. IIC时序
- 1. 数据有效性
- 2. 起始信号和终止信号
- 3. 数据格式
- 4. 应答和非应答信号
- 5. 时钟同步
- 6. 写数据和读数据
- 2. AT24C02
- 3. AT24C02读写时序
- 4. AT24C02配置步骤
- 5. 代码部分
- 1. IIC基本信号
- 2. AT24C02驱动代码
- 3. 实验结果分析
1. IIC总线协议
1. IIC简介
-
简介:
IIC
(Inter-Integrated Circuit
)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器以及其外围设备。它是由数据线SDA和时钟线SCL构成的串行总线,可发送和接收数据,在CPU与被控IC之间、IC与IC之间进行双向传送。 -
特点:
- 总线由数据线 SDA 和时钟线 SCL 构成的串行总线,数据线用来传输数据,时钟线用来同步数据收发;
- 总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时序就可以实现微控制器与器件之间的通信;
- 数据线 SDA 和时钟线 SCL 都是双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是高电平;
- 总线上数据的传输速率在标准模式下可达 100kbit/s 在快速模式下可达 400kbit/s 在高速模式下可达 3.4Mbit/s;
- 总线支持设备连接。在使用 IIC 通信总线时,可以有多个具备 IIC 通信能力的设备挂载在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容 400pF 的限制决定。
-
总线挂载器件示意图:
-
IIC通信的优缺点:
优点 (Advantages) 缺点 (Disadvantages) 简化的连线:仅需两根线(SDA 和 SCL) 速度较慢:最高速率为 3.4 Mbps 多主多从架构:支持多个主设备和从设备 协议复杂性:处理起始位、停止位、应答信号等 灵活的速度选择:支持多种速度模式 功耗较高:使用开漏驱动方式需上拉电阻 芯片地址分配:每个从设备有唯一地址 有限的从设备数量:地址有数量限制 内置握手和校验机制:提高通信可靠性 信号完整性问题:高速模式下可能影响通信 -
硬件IIC与软件IIC对比:
IIC 用法 速度 稳定性 管脚 硬件IIC 比较复杂 快 较稳定 需使用特定管脚 软件IIC 操作过程比较清晰 较慢 稳定 任意管脚,比较灵活
2. IIC时序
1. 数据有效性
SDA 线上的数据在时钟的高电平期间必须保持稳定。只有当 SCL 线上的时钟信号为低电平时,数据线的高电平或低电平状态才会改变。每传输一个数据位都会产生一个时钟脉冲。
2. 起始信号和终止信号
3. 数据格式
SDA线上的每个字节长度必须为8位,每次可以传输的字节数不受限制。每个字节后面必须有一个确认位。数据传输时,首先传输最高有效位。如果从设备在执行其他功能(例如处理内部中断)之前无法接收或传输另一个完整的数据字节,则可以将时钟线SCL保持在低位以强制主设备进入等待状态。当从设备准备好接收另一个数据字节并释放时钟线SCL时,数据传输将继续。
4. 应答和非应答信号
每个字节后都会应答。应答位允许从机向主机发出应答信号,表示已成功接收该字节并且可以发送另一个字节。主机生成所有时钟脉冲,包括应答的第九个时钟脉冲。
检测应答信号:
应答信号定义如下:主机在应答时钟脉冲期间释放 SDA 线,以便从机可以将 SDA 线拉低,并且在此时钟脉冲的高电平期间保持稳定的低电平。还必须考虑设置和保持时间。
非应答信号定义如下:当 SDA 在第九个时钟脉冲期间保持高电平时,这被定义为未确认信号。然后,主机可以生成 STOP 条件以中止传输,或生成重复的 START 条件以开始新的传输。
有五种情况会导致生成 NACK:
- 总线上没有传输地址的从机,因此没有设备可以做出确认信号;
- 从机无法接收或发送,因为它正在执行某些实时功能并且尚未准备好开始与主设备通信;
- 在传输过程中,从机收到其无法理解的数据或命令;
- 在传输期间,从机无法再接收任何数据字节;
- 主机必须向从机发送传输结束信号。
5. 时钟同步
两个主设备可以同时在空闲总线上开始传输,并且必须有一种方法来决定由哪个主设备控制总线并完成传输。这是通过时钟同步和仲裁来实现的。在单主系统中,不需要时钟同步和仲裁。
6. 写数据和读数据
写操作:
主机首先在IIC总线上发送起始信号,此时总线上的从机都会等待接收由主机发送的数据。主机接下来发送从机地址+0(写操作)组成的8位数据,所有从机接收该8位数据后,自行检验是否是自己的设备地址,如果是,就会发出应答信号。主机在总线上接收到应答信号后,才能继续向从机发送数据。
读操作:
主机向从机读取数据的操作,主机发出起始信号,接着发送从机地址+1(读操作)组成的8位数据,从机接收到数据验证是否是自身的地址。验证成功后从机会发出应答信号, 并向主机返回8位数据,发送完之后主机就会等待主机的应答信号。如果主机一直返回应答信号,那么从机可以一直发送数据,直到主机发出非应答信号,从机才会停止发送信号。
发送1字节数据:
读取1字节数据:
2. AT24C02
引脚定义:
WP:写保护引脚,接高电平只读,接低电平读和写,默认接低电平。
A0/1/2:可编程地址部分,由8位组成,最后一位是读写操作,0是读操作,1是写操作。
写操作地址:0xA0; 读操作地址:0xA1.
3. AT24C02读写时序
写字节时序:
主机在 IIC 总线发送第 1 个字节的数据为 24C02的设备地址 0xA0,用于寻找总线上找到 24C02,在获得 24C02 的应答信号之后,继续发送第2 个字节数据,该字节数据是 24C02 的内存地址,再等到 24C02 的应答信号,主机继续发送第 3 字节数据,这里的数据即是写入在第 2 字节内存地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
连续写时序:
在单字节写时序时,每次写入数据时都需要先写入设备的内存地址才能实现,在页写时序 中,只需要告诉24C02 第一个内存地址 1,后面数据会按照顺序写入到内存地址 2,内存地址 3等,大大节省了通信时间,提高了时效性。因为 24C02 每次只能 8bit 数据,所以它的页大小也就是 1 字节。
读字节时序:
24C02读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送24C02设备地址0xA0,获取从机应答信号后,接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送24C02设备地址0xA1,获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非应答信号并以停止信号发出为结束,从机就会结束传输。
4. AT24C02配置步骤
-
使能SCL和SDA对应时钟
__HAL_RCC_GPIOB_CLK_ENABLE()
-
设置GPIO工作模式
HAL_GPIO_Init()
-
编写基本信号
void iic_init(void); static void iic_delay(void); void iic_start(void); void iic_stop(void); uint8_t iic_wait_ack(void); void iic_ack(void); void iic_nack(void); void iic_send_byte(uint8_t data); uint8_t iic_read_byte(uint8_t ack);
-
编写读和写函数
void at24c02_write_one_byte(uint8_t addr, uint8_t data); uint8_t at24c02_read_one_byte(uint8_t addr);
5. 代码部分
1. IIC基本信号
-
软件模拟IIC使用的引脚:
#define IIC_SCL(x) do{ x ? \ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); \ }while(0) /* SCL */ #define IIC_SDA(x) do{ x ? \ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET) : \ AL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); \ }while(0) /* SDA */ #define IIC_READ_SDA HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) /* 读取SDA */
-
延时函数:
static void iic_delay(void) { delay_us(2); }
在
iic_delay
函数中使用2微秒延迟对于确保I²C通信的正确时序和信号稳定性至关重要。这种延迟有助于满足I²C的标准和快速模式的时序要求,适应微控制器的速度,并确保在总线的电气特性下信号转换的稳定性。选择的延迟时间平衡了及时通信和信号转换稳定性的需求。 -
起始信号:
void iic_start(void) { IIC_SDA(1); // 确保SDA线为高电平 IIC_SCL(1); // 确保SCL线为高电平 iic_delay(); // 短暂延迟 IIC_SDA(0); // 将SDA线拉低,生成起始条件 iic_delay(); // 短暂延迟 IIC_SCL(0); // 将SCL线拉低,准备进行数据传输 iic_delay(); // 短暂延迟 }
-
确保总线空闲状态:
IIC_SDA(1);
:将SDA线设置为高电平。IIC_SCL(1);
:将SCL线设置为高电平。- 在I²C协议中,空闲状态是SDA和SCL都为高电平。
-
生成起始信号:
IIC_SDA(0);
:将SDA线从高电平拉低,在SCL高电平期间,这是I²C的起始条件。- 这个信号告诉所有从设备,主设备即将开始通信。
-
准备数据传输:
IIC_SCL(0);
:将SCL线拉低,表示开始数据传输。
-
-
终止信号:
void iic_stop(void) { IIC_SDA(0); // 确保SDA线为低电平 iic_delay(); // 短暂延迟 IIC_SCL(1); // 将SCL线拉高 iic_delay(); // 短暂延迟 IIC_SDA(1); // 将SDA线拉高,生成停止条件 iic_delay(); // 短暂延迟 }
-
准备停止信号:
IIC_SDA(0);
:将SDA线设置为低电平。
-
生成停止信号:
IIC_SCL(1);
:将SCL线拉高,准备生成停止条件。IIC_SDA(1);
:将SDA线从低电平拉高,在SCL高电平期间,这是I²C的停止条件(Stop Condition)。- 这个信号通知所有从设备,主设备已经结束通信,释放总线。
-
-
等待应答信号:
uint8_t iic_wait_ack(void) { IIC_SDA(1); // 主机释放SDA线(将SDA线设置为高电平,准备接收从机的ACK信号) iic_delay(); // 短暂延迟,等待信号稳定 IIC_SCL(1); // 主机拉高SCL线,准备读取SDA线的状态 iic_delay(); // 短暂延迟,等待信号稳定 if(IIC_READ_SDA) // 在SCL高电平时读取SDA线的状态 { iic_stop(); // 如果SDA线为高电平,表示从机未发送ACK信号,执行停止条件 return 1; // 返回1表示未接收到ACK信号 } IIC_SCL(0); // 主机拉低SCL线 iic_delay(); // 短暂延迟 return 0; // 返回0表示成功接收到ACK信号 }
-
释放SDA线:
IIC_SDA(1);
:主机将SDA线设置为高电平,准备接收从机的ACK信号。
-
短暂延迟:
iic_delay();
:等待一段时间,以确保信号稳定。
-
拉高SCL线:
IIC_SCL(1);
:主机拉高SCL线,使数据稳定在SDA线上,并准备读取SDA线的状态。
-
读取SDA线的状态:
if(IIC_READ_SDA)
:在SCL高电平时读取SDA线的状态。如果SDA线为高电平,表示从机未发送ACK信号(从机没有将SDA线拉低)。
-
停止条件:
iic_stop();
:如果未接收到ACK信号(SDA线为高电平),主机生成停止条件终止通信。
-
返回值:
return 1;
:返回1表示未接收到ACK信号。return 0;
:返回0表示成功接收到ACK信号。
-
拉低SCL线:
IIC_SCL(0);
:主机拉低SCL线,准备进行下一步操作。
-
延迟:
iic_delay();
:再次短暂延迟以确保信号稳定。
-
-
应答信号:
void iic_ack(void) { IIC_SCL(0); // 确保SCL线为低电平 iic_delay(); IIC_SDA(0); // 数据线为低电平,表示应答信号 iic_delay(); IIC_SCL(1); // 拉高SCL线,发送应答信号 iic_delay(); }
-
IIC_SCL(0):
- 这一步确保时钟线(SCL)为低电平,表示准备发送应答信号。
-
IIC_SDA(0):
- 将数据线(SDA)拉低,表示发送应答信号(ACK)。
- ACK信号表示接收设备成功接收到数据,并准备接收下一个字节。
-
IIC_SCL(1):
- 拉高SCL线,完成应答信号的发送。
- 接收设备在此时刻确认它已准备好接收数据。
-
-
非应答信号:
void iic_nack(void) { IIC_SCL(0); iic_delay(); IIC_SDA(1); //数据线为高电平,表示非应答 iic_delay(); IIC_SCL(1); iic_delay(); }
-
IIC_SCL(0):
- 这一步确保时钟线(SCL)为低电平,表示准备发送非应答信号。
-
IIC_SDA(1):
- 将数据线(SDA)拉高,表示发送非应答信号(NACK)。
- NACK信号表示接收设备未能成功接收到数据,或发送设备需要停止传输。
-
IIC_SCL(1):
- 拉高SCL线,完成非应答信号的发送。
- 发送设备在此时刻了解接收设备未能成功接收数据或接收设备不再准备接收更多数据。
-
-
发送一个字节数据:
void iic_send_byte(uint8_t data) { for(uint8_t t = 0; t < 8; t++) { IIC_SDA((data & 0x80) >> 7); iic_delay(); IIC_SCL(1); iic_delay(); IIC_SCL(0); data <<= 1; } IIC_SDA(1); //发送完成,主机释放SDA线 }
-
循环发送每一位数据:
for(uint8_t t = 0; t < 8; t++) { IIC_SDA((data & 0x80) >> 7); // 设置数据线的值 iic_delay(); // 延时 IIC_SCL(1); // 设置时钟线为高电平 iic_delay(); // 延时 IIC_SCL(0); // 设置时钟线为低电平 data <<= 1; // 左移一位,准备发送下一位数据 }
for
循环从t = 0
开始,一直执行到t < 8
,每次循环t
自增。IIC_SDA((data & 0x80) >> 7);
:设置数据线(SDA)的值为data
的最高位(第7位),data & 0x80
是用来获取data
的最高位,>> 7
是右移操作,将最高位移到最低位,然后通过IIC_SDA
函数设置数据线的值。iic_delay();
:延时一段时间,为了等待数据稳定或者设备处理数据。IIC_SCL(1);
:设置时钟线(SCL)为高电平。iic_delay();
:再次延时一段时间。IIC_SCL(0);
:将时钟线(SCL)设置为低电平。data <<= 1;
:将数据左移一位,这样下一次循环时处理的是数据的下一位(次高位)。
这样循环执行完毕后,就完成了对一个字节数据的逐位发送。
-
释放数据线:
IIC_SDA(1); //发送完成,主机释放SDA线
- 将数据线(SDA)设置为高电平,表示数据发送完成后,释放数据线,让其他设备可以使用这条数据线。
-
-
读取一个字节数据:
uint8_t iic_read_byte(uint8_t ack) { uint8_t receive = 0; for(uint8_t t = 0; t < 8; t++) { receive <<= 1; IIC_SCL(1); iic_delay(); if(IIC_READ_SDA) receive++; IIC_SCL(0); iic_delay(); } if(!ack) iic_nack(); else iic_ack(); return receive; }
-
初始化接收变量和循环接收每一位数据:
uint8_t receive = 0; for(uint8_t t = 0; t < 8; t++) { receive <<= 1; IIC_SCL(1); iic_delay(); if(IIC_READ_SDA) receive++; IIC_SCL(0); iic_delay(); }
receive
是用来存储接收到的数据的变量,初始值为0。for
循环从t = 0
开始,一直执行到t < 8
,每次循环t
自增。receive <<= 1;
:将接收变量receive
左移一位,为下一位数据的接收做准备。IIC_SCL(1);
:设置时钟线(SCL)为高电平,表示准备接收数据。iic_delay();
:延时一段时间,以确保数据稳定。if(IIC_READ_SDA) receive++;
:检查数据线(SDA)的状态,如果为高电平(1),则将receive
的最低位(第0位)设置为1。IIC_SCL(0);
:将时钟线(SCL)设置为低电平,完成一个时钟周期的接收。
这样循环执行完毕后,
receive
变量中存储了从设备发送过来的8位数据。 -
发送应答或不应答信号:
if(!ack) iic_nack(); else iic_ack();
- 根据函数参数
ack
的值来决定发送应答(ACK)或不应答(NACK)信号。 - 如果
ack
为0,调用iic_nack()
函数发送不应答信号。 - 如果
ack
不为0(即非0),调用iic_ack()
函数发送应答信号。
- 根据函数参数
-
返回接收到的数据:
return receive;
- 将接收到的完整字节数据返回给调用该函数的地方。
-
-
总结:
函数 功能 void iic_init(void) IIC初始化函数 static void iic_delay(void) IIC延时函数 void iic_start(void) 起始信号 void iic_stop(void) 终止信号 uint8_t iic_wait_ack(void) 等待应答信号 void iic_ack(void) 应答信号 void iic_nack(void) 非应答信号 void iic_send_byte(uint8_t data) 发送一个字节数据 uint8_t iic_read_byte(uint8_t ack) 读取1个字节数据
2. AT24C02驱动代码
-
写字节函数:
void at24c02_write_one_byte(uint8_t addr, uint8_t data) { //1.发送起始信号 iic_start(); //2.发送通信地址 iic_send_byte(0xA0); //3.等待应答信号 iic_wait_ack(); //4.发送内存地址 iic_send_byte(addr); //5.等待应答信号 iic_wait_ack(); //6.发送数据 iic_send_byte(data); //7.等待应答信号 iic_wait_ack(); //8.发送停止信号 iic_stop(); //等待EEPROM写入完成 delay_ms(10); }
-
读字节函数:
uint8_t at24c02_read_one_byte(uint8_t addr) { uint8_t rec = 0; //1.发送起始信号 iic_start(); //2.发送通信地址 iic_send_byte(0xA0); //3.等待应答信号 iic_wait_ack(); //4.发送内存地址 iic_send_byte(addr); //5.等待应答信号 iic_wait_ack(); //6.发送起始信号 iic_start(); //7.发送读操作地址 iic_send_byte(0xA1); //8.等待应答信号 iic_wait_ack(); //9.等待接收数据 rec = iic_read_byte(0); //10.发送非应答(获取该地址) //11.发送停止信号 iic_stop(); //等待EEPROM写入完成 delay_ms(10); return rec; }
3. 实验结果分析
以上是写时序的实验结果,三个字节分别是设备地址、内存地址、数据内容。黄色线条为时钟线SCL,绿色线条为数据线SDA,从图中可以清晰的看到起始信号、终止信号、应答信号、以及发送的数据。
声明:资料来源(战舰STM32F103ZET6开发板资源包)
- Cortex-M3权威指南(中文).pdf
- STM32F10xxx参考手册_V10(中文版).pdf
- STM32F103 战舰开发指南V1.3.pdf
- STM32F103ZET6(中文版).pdf
- 战舰V4 硬件参考手册_V1.0.pdf