目录
- 简介
- 通信流程
- 开始信号和停止信号
- 数据有效性
- 仲裁
- 从设备地址与确认应答
- 软件控制流程
- 主机发送方
- 主机接收方
- 方案1(需软件对中断快速响应)
- 方案2
- 从机发送方
- 从机接收方
- 例程
- 主机发送从机接收
- 主机接收从机发送
简介
I2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息。
I2C总线是一个真正的多主机总线,如果两个或多个主机同时初始化数据传输,可以通过冲突检测和仲裁防止数据破坏,每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。数据传输和地址设定由软件设定,非常灵活。总线上的器件增加和删除不影响其他器件正常工作。
I2C有3种总线速率——标速(100kHz)、快速(400kHz)和高速(3.4MHz),但GD32F103C8T6只支持标速和快速两种速率。
通信流程
开始信号和停止信号
I2C的通信由一个起始信号开始,传输结束后由一个停止信号结束。
开始信号为SDA线由高电平拉低,接着SCL线开始输出同步时钟信号;传输结束后SCL线停止同步时钟输出,接着SDA线也由低拉高,发出结束信号。
数据有效性
I2C是一个同步传输协议,因此数据的更新需根据同步时钟线来决定。
I2C规定,时钟信号的高电平期间SDA线上的数据必须稳定。只有在时钟信号SCL变低的时候数据线SDA的电平才能跳变。
仲裁
上面说过,I2C是一个多主机总线协议,因此当多个主机同时申请总线的控制权时,则需要对其进行仲裁,以决定哪一个主机获得总线的控制权。
I2C的仲裁比较特别,跟之前介绍的DMA的仲裁不同,DMA的仲裁是通过检查优先级的方式来进行的,而I2C的仲裁是通过比较实际输出电平的方式进行的。
在每一位数据的发送期间,当SCL为高时,每个主机都检查SDA电平是否和自己发送的相同。理论上讲,如果两个主机所传输的内容完全相同,那么他们能够成功传输而不出现错误。如果一个主机发送高电平但检测到SDA电平为低,则认为自己仲裁失败并关闭自己的SDA输出驱动,而另一个主机则继续完成自己的传输。
从设备地址与确认应答
I2C总线上可以连接多个从设备,每个从设备都共享一条SDA和SCL线。
所以当主机想要给总线上的某一个从设备通信就需要有一个机制来寻找对应的从设备,这个机制就是从设备地址。
每个从设备都有一个自己的地址可以是7位的,也可以是10位的,地址的值的范围具体由厂商来决定。
主机在向总线发送开始信号并成功获得总线控制权后,将会向总线发送对应从机的地址,这个地址所有的从机都会接收,然后从机将收到的地址与自己的地址比较,若地址一致则该从机将会向总线发送ACK信号,也就是确认应答,表示可以开始通信;而其他的从机将会忽略总线上的信号,直到下一个开始信号来临。
主机与从机每传输一组数据,从机接收后都必须发送ACK信号,表示接收成功。
软件控制流程
I2C具有4种运行状态——主机发送方、主机接收方、从机发送方、从机接收方。对于这4种不同的运行状态都有不同的软件控制流程。
这一部分所有的流程图都是基于10位地址I2C通信的,因此如果各位使用的是7位地址的I2C通信,那么把地址头发送的那一部分忽略即可。
主机发送方
简要流程:
- 使能I2C外设时钟,配置时钟相关寄存器来确保正确的I2C时序;
- 软件将START位置1,在I2C总线上产生一个START起始位,SBSEND置1,进入主机模式;
- 写一个7位地址位或带有地址头的10位地址位到数据寄存器,SBSEND置0,若是10位地址,那么先写入高地址ADD10SEND置1,发送后再写入低地址ADD10SEND置0;
- I2C开始发送地址或者地址头到总线上,ADDSEND置1,软件将ADDSEND置0;
- I2C进入数据发送状态,软件写第一个字节数据到数据寄存器,此时,写入数据寄存器的字节被立即移入内部移位寄存器,TBE置1;
- 移位寄存器非空,I2C开始发送数据到总线;
- 在第一个字节的发送过程中,软件可以写第二个字节到数据寄存器,TBE置0;
- 任何时候TBE被置1,软件都可以向数据寄存器写入一个字节,只要还有数据待发送。
- 在倒数第二个字节发送过程中,软件写入最后一个字节数据到数据寄存器来清除TBE标志
位; - 最后一个字节发送结束后,I2C主机将BTC位置1,因为移位寄存器和数据寄存器此时都为空;
- 软件将STOP位置1来发送一个STOP结束位,此后TBE和BTC状态位都将被置0。
说明:
SBSEND全称Start Bit Send,“起始位发送”标志位。
ADDSEND全称Address Send,“地址发送”标志位,同样ADD10SEND就是“10位地址发送”标志位
TBE全称Transfer Buffer Empty,“发送缓冲区空”标志位。
BTC全称Byte Transfer Complete,“字节传输完成”标志位。
主机接收方
方案1(需软件对中断快速响应)
简要流程:
- 使能I2C外设时钟,配置时钟相关寄存器来确保正确的I2C时序,等待START起始位;
- 软件将START位置1,从而在I2C在总线上产生一个START起始位,SBSEND置1,进入主机模式;
- 写一个7位地址位或带有地址头的10位地址位到数据寄存器,SBSEND置0,若是10位地址,那么先写入高地址,发送后ADD10SEND置1,再写入低地址ADD10SEND置0;
- 7位或10位的地址位发送出去之后,ADDSEND置1,软件应该将ADDSEND置0,如果地址是10位格式,软件应该再次将START位置1来重新产生一个START(Sr);在START产生后,SBSEND位置1,软件将SBSEND置0,然后地址头被发到I2C总线,ADDSEND再次被置1,软件将ADDSEND置0;
- 接收到第一个字节,RBNE位置1,软件可以从数据寄存器读取第一个字节,RBNE位置0。
- 此后任何时候RBNE被置1,软件就可以从数据寄存器读取一个字节;
- 接收完倒数第二个字节(N-1)数据之后,软件应该立即将ACKEN位置0,并将STOP位置1,
这一过程需要在最后一个字节接收完毕之前完成,以确保NACK发送给最后一个字节; - 最后一个字节接收完毕后,RBNE位置1,软件读取最后一个字节,由于ACKEN已经在前一步骤中被清0,I2C不再为最后一个字节发送ACK,并在最后一个字节发送完毕后产生一个STOP结束位。
说明:
RBNE全称Read Buffer Not Empty,“接收缓冲区非空”标志位。
ACKEN全称Acknowledge Enable,“应答帧使能”标志位。
方案2
简要流程:
- 使能I2C外设时钟,配置时钟相关寄存器来确保正确的I2C时序,等待START起始位;
- 软件将START位置1,从而在I2C在总线上产生一个START起始位,SBSEND置1,进入主机模式;
- 写一个7位地址位或带有地址头的10位地址位到数据寄存器,SBSEND置0,若是10位地址,那么先写入高地址,发送后ADD10SEND置1,再写入低地址ADD10SEND置0;
- 7位或10位的地址位发送出去之后,ADDSEND置1,软件应该将ADDSEND置0,如果地址是10位格式,软件应该再次将START位置1来重新产生一个START(Sr);在START产生后,SBSEND位置1,软件将SBSEND置0,然后地址头被发到I2C总线,ADDSEND再次被置1,软件将ADDSEND置0;
- 第一个字节被接收,RBNE位置1,软件从数据寄存器读读取第一个字节,RBNE位被置0;
- 此后任何时候,一旦RBNE位被置1,软件就可以从数据寄存器读取一个字节的数据,直到主机接收了N-3个字节;
- 软件从数据寄存器读出倒数第三个(N-2)字节,同时也将BTC位置0;
- 此后第N-1个字节从移位寄存器被移到数据寄存器,总线得到释放然后开始接收最后一个字节;
- 最后一个字节接收完毕后,硬件再次把BTC位和RBNE置1,软件将STOP位置1,主机发出一个STOP结束位;
- 软件读取第N-1个字节,BTC置0,此后最后一个字节从移位寄存器被移动到数据寄存器;
- 软件读取最后一个字节,RBNE置0;
从机发送方
简要流程:
- 使能I2C外设时钟,配置时钟相关寄存器来确保正确的I2C时序,等待START起始位和地址;
- 接收一个START起始位及随后的地址,ADDSEND位置1,软件将ADDSEND置0;如果地址是10位格式,从机在检测到START(Sr)和紧接着的地址头之后会继续将ADDSEND位置1,软件第二次将ADDSEND位置0;
- I2C进入数据发送状态,TBE位置1,软件写入第一个字节数据到数据寄存器,写入数据寄存器的字节被立即移入内部移位寄存器;
- 移位寄存器非空,I2C开始发送数据到总线上;
- 第一个字节的发送期间,软件写第二个字节到数据寄存器,此时TBE位被置0;
- 在此之后,任何时候TBE被置1,只要依然有数据待被发送,软件都可以写入一个字节到数据寄存器;
- 倒数第二个字节发送期间,软件写最后一个数据到数据寄存器来将TBE置0;
- 倒数第二个字节发送完成后,TBE置1;
- 检测到STOP结束位,TBE置0。
根据I2C协议,I2C主机将不会对接收到的最后一个字节发送应答,所以在最后一个字节发
送结束后,I2C从机的AERR会置1以通知软件发送结束,软件写0到AERR位可以清除此
位。
AERR全称Acknowledge Error,“应答错误”标志位。
从机接收方
简要流程:
- 使能I2C外设时钟,配置时钟相关寄存器来确保正确的I2C时序,等待START起始位以及地址;
- 在接收到START起始条件和匹配的7位或10地址之后,ADDSEND位置1,软件将ADDSEND位置0;
- 一旦接收到第一个字节,RBNE位被硬件置1,软件可以读取数据寄存器的第一个字节,此时RBNE位也被清0;
- 任何时候RBNE被置1,软件可以从数据寄存器读取一个字节;
- 接收到最后一个字节后,RBNE被置1,软件可以读取最后的字节;
- 当I2C检测到I2C总线上一个STOP结束位,STPDET位被置1,软件将STPDET位置0。
STPDET全称Stop Detect,“停止位检测”标志位。
例程
GD32F103C8T6上有2个I2C设备,正好用来测试,它们的对应管脚如下表所示。
设备 | SCL | SDA |
---|---|---|
I2C0 | PB6 | PB7 |
I2C1 | PB10 | PB11 |
在对管脚初始化时,I2C的两个管脚都必须设置为复用开漏模式。
因为GD32F10C8T6的I2C外设没有内部上拉电阻,所以在接线的时候要在SDA和SCL分别加外部上拉电阻,如下图。
VCC为电源电压,一般为3.3V或5V;上拉电阻的阻值推荐为4.7kΩ或10kΩ。
主机发送从机接收
现象:主机每秒向从机发送一串数据
i2c.c文件
#include "i2c.h"
void I2C_MasterInit(void)
{
/* 初始化GPIO */
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7);
/* 初始化I2C */
rcu_periph_clock_enable(RCU_I2C0);
/* 初始化时钟,频率10kHz,占空比50% */
i2c_clock_config(I2C0, 100000, I2C_DTCY_2);
/* 设置地址,7位 */
i2c_mode_addr_config(I2C0, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, 0x82);
/* 使能I2C0 */
i2c_enable(I2C0);
/* 使能应答 */
i2c_ack_config(I2C0, I2C_ACK_ENABLE);
}
void I2C_SlaveInit(void)
{
/* 初始化GPIO */
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_10 | GPIO_PIN_11);
/* 初始化I2C */
rcu_periph_clock_enable(RCU_I2C1);
/* 初始化时钟,频率10kHz,占空比50% */
i2c_clock_config(I2C1, 100000, I2C_DTCY_2);
/* 设置地址,7位 */
i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, 0x72);
/* 使能I2C1 */
i2c_enable(I2C1);
/* 使能应答 */
i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}
void I2C_MasterSendSlaveReceive(void)
{
uint8_t i2c_transmitter[16];
uint8_t i2c_receiver[16];
memset(i2c_transmitter, 0x00, sizeof(i2c_transmitter));
memset(i2c_receiver, 0x00, sizeof(i2c_receiver));
printf("Master transmit: ");
for(uint8_t i = 0; i < 16; i++)
{
i2c_transmitter[i] = i + 0x80;
printf("%x ", i2c_transmitter[i]);
}
printf("\n");
/* 等待总线空闲 */
while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY));
/* 主机发送起始位 */
i2c_start_on_bus(I2C0);
/* 主机等待SBSEND置1 */
while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND));
/* 主机发送从机地址 */
i2c_master_addressing(I2C0, 0x72, I2C_TRANSMITTER);
/* 等待ADDSEND置1 */
while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND));
while(!i2c_flag_get(I2C1, I2C_FLAG_ADDSEND));
/* ADDSEND置0 */
i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);
i2c_flag_clear(I2C1, I2C_FLAG_ADDSEND);
for(uint8_t i = 0; i < 16; i++)
{
/* 发送一个字节 */
i2c_data_transmit(I2C0, i2c_transmitter[i]);
/* 等待主机发送完成 */
while(!i2c_flag_get(I2C0, I2C_FLAG_TBE));
/* 等待从机接收完成 */
while(!i2c_flag_get(I2C1, I2C_FLAG_RBNE));
/* 从机读取数据 */
i2c_receiver[i] = i2c_data_receive(I2C1);
}
/* 发送停止位 */
i2c_stop_on_bus(I2C0);
/* 等待STPDET位置1 */
while(I2C_CTL0(I2C0)&0x0200);
while(!i2c_flag_get(I2C1, I2C_FLAG_STPDET));
/* STPSET置0 */
i2c_enable(I2C0);
printf("Slave receive: ");
for(uint8_t i = 0; i < 16; i++)
{
printf("%x ", i2c_receiver[i]);
}
printf("\n");
}
main.c文件
#include "gd32f10x.h"
#include "main.h"
#include "systick.h"
#include "usart.h"
#include "i2c.h"
#include <stdio.h>
#include <string.h>
int main(void)
{
systick_config();
USART_Config();
I2C_MasterInit();
I2C_SlaveInit();
while(1)
{
I2C_MasterSendSlaveReceive();
delay_ms(1000);
}
}
主机接收从机发送
现象:主机每秒接收从机发送的一串数据
i2c.c文件
#include "i2c.h"
void I2C_MasterInit(void)
{
/* 初始化GPIO */
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_6 | GPIO_PIN_7);
/* 初始化I2C */
rcu_periph_clock_enable(RCU_I2C0);
/* 初始化时钟,频率10kHz,占空比50% */
i2c_clock_config(I2C0, 100000, I2C_DTCY_2);
/* 设置地址,7位 */
i2c_mode_addr_config(I2C0, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, 0x82);
/* 使能I2C0 */
i2c_enable(I2C0);
/* 使能应答 */
i2c_ack_config(I2C0, I2C_ACK_ENABLE);
}
void I2C_SlaveInit(void)
{
/* 初始化GPIO */
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_10 | GPIO_PIN_11);
/* 初始化I2C */
rcu_periph_clock_enable(RCU_I2C1);
/* 初始化时钟,频率10kHz,占空比50% */
i2c_clock_config(I2C1, 100000, I2C_DTCY_2);
/* 设置地址,7位 */
i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, 0x72);
/* 使能I2C1 */
i2c_enable(I2C1);
/* 使能应答 */
i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}
void I2C_MasterReceiveSlaveSend(void)
{
uint8_t i2c_transmitter[16];
uint8_t i2c_receiver[16];
memset(i2c_transmitter, 0x00, sizeof(i2c_transmitter));
memset(i2c_receiver, 0x00, sizeof(i2c_receiver));
printf("Slave transmit: ");
for(uint8_t i = 0; i < 16; i++)
{
i2c_transmitter[i] = i + 0x80;
printf("%x ", i2c_transmitter[i]);
}
printf("\n");
/* 等待总线空闲 */
while(i2c_flag_get(I2C0, I2C_FLAG_I2CBSY));
/* 主机发送起始信号 */
i2c_start_on_bus(I2C0);
/* 主机等待SBSEND置1 */
while(!i2c_flag_get(I2C0, I2C_FLAG_SBSEND));
/* 主机发送从机地址 */
i2c_master_addressing(I2C0, 0x72, I2C_RECEIVER);
/* 主机等待ADDSEND置1 */
while(!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND));
/* 主机ADDSEND置0 */
i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);
/* 从机等待ADDSEND置1 */
while(!i2c_flag_get(I2C1, I2C_FLAG_ADDSEND));
/* 从机ADDSEND置0 */
i2c_flag_clear(I2C1, I2C_FLAG_ADDSEND);
/* 从机发送n-1个字节 */
for(uint8_t i = 0; i < 15; i++)
{
/* 发送一个字节 */
i2c_data_transmit(I2C1, i2c_transmitter[i]);
/* 从机等待发送完成 */
while(!i2c_flag_get(I2C1, I2C_FLAG_TBE));
/* 主机等待接收完成 */
while(!i2c_flag_get(I2C0, I2C_FLAG_RBNE));
/* 获取一个字节 */
i2c_receiver[i] = i2c_data_receive(I2C0);
}
/* 关闭ACK */
i2c_ack_config(I2C0, I2C_ACK_DISABLE);
/* 发送最后一个字节 */
i2c_data_transmit(I2C1, i2c_transmitter[15]);
/* 从机等待发送完成 */
while(!i2c_flag_get(I2C1, I2C_FLAG_TBE));
/* 主机等待应答错误位置1 */
while(!i2c_flag_get(I2C1, I2C_FLAG_AERR));
/* 主机发送停止信号 */
i2c_stop_on_bus(I2C0);
while(I2C_CTL0(I2C0)&0x0200);
/* 获取最后一个字节 */
i2c_receiver[15] = i2c_data_receive(I2C0);
i2c_ack_config(I2C0, I2C_ACK_ENABLE);
/* 从机AERR置0 */
i2c_flag_clear(I2C1, I2C_FLAG_AERR);
printf("Master receive: ");
for(uint8_t i = 0; i < 16; i++)
{
printf("%x ", i2c_receiver[i]);
}
printf("\n");
}
main.c文件文章来源:https://uudwc.com/A/6Bmj
#include "gd32f10x.h"
#include "main.h"
#include "systick.h"
#include "usart.h"
#include "i2c.h"
#include <stdio.h>
#include <string.h>
int main(void)
{
systick_config();
USART_Config();
I2C_MasterInit();
I2C_SlaveInit();
while(1)
{
I2C_MasterReceiveSlaveSend();
delay_ms(1000);
}
}
文章来源地址https://uudwc.com/A/6Bmj