前言
在消费电子、工业电子等领域,会使用各种类型的芯片,如微控制器、电源管理、显示驱动、传感器、存储器、转换器等,它们有着不同的功能。有时需要快速地进行数据交互。为了使用最简单的方式使这些芯片互联互通,I2C(Inter-Integrated Circuit)协议应运而生。
**I2C协议(或称IIC)**是由飞利浦(现在的恩智浦半导体)公司开发的一种通用的总线协议。它使用两根线(时钟线和数据线)来传输数据,支持多个设备共享同一条总线。 I2C协议通常用于连接微控制器、传感器、存储器和其他外围设备。
I2C通讯规则
I2C总线包括两根信号线:SDA(串行数据线)和SCL(串行时钟线)。这两根信号线共用一个总线,因此在总线上可以连接多个设备。在I2C总线上,每个设备都有一个唯一的地址,用于标识设备。SCL线是时钟线,用于控制数据传输的速度和时序;SDA线是数据线,用于传输实际的数据.
I2C写操作
流程如下:
- 开始。
- 发送设备地址,等待从设备响应
- 发送寄存器地址,等待从设备响应
- 发送一个字节,等待从设备响应。这个操作是循环执行,直到没有数据。
- 停止。
代码解析
主函数初始化
由数据手册我们可以进行i2c的引脚查询,我做了下面的整理:
同时,查阅原理图我们可以对其相应引脚进行初始化
cpp
#define SCL_RCU RCU_GPIOB
#define SCL_PORT GPIOB
#define SCL_PIN GPIO_PIN_6
#define SDA_RCU RCU_GPIOB
#define SDA_PORT GPIOB
#define SDA_PIN GPIO_PIN_7
void I2C_soft_init(){
// 初始化I2C的GPIO
// SCL - PB6
rcu_periph_clock_enable(SCL_RCU);
gpio_mode_set(SCL_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, SCL_PIN);
gpio_output_options_set(SCL_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_MAX, SCL_PIN);
// SDA - PB7
rcu_periph_clock_enable(SDA_RCU);
gpio_mode_set(SDA_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, SDA_PIN);
gpio_output_options_set(SDA_PORT, GPIO_OTYPE_OD, GPIO_OSPEED_MAX, SDA_PIN);
}
写函数实现
我为大家提供的编程思路:根据I2C写操作流程,我们先将写操作的基本框架构建出来,具体包括开始、发送设备写地址、等待响应、发送寄存器地址、等待响应、循环发送所有字节、停止。然后我再根据各部分原理图去把对应的函数逐个实现出来。
大家先不要去看具体代码怎么编写,这里提供这张截图是为了让大家清楚我们构建的框架。
接下来,我们再去讲解里面的函数实现。
通讯信号------开始
由原理图我们看到第一步将SDA和SCL全都拉高,然后延时一段时间,将SDA拉低,延时后再将SCL拉低,当检测到有这种信号存在了,证明即将开始传输数据了。
部分代码展示:
cpp
static void start() {
SDA_OUT();
SDA(1);
delay_1us(5);
SCL(1);
delay_1us(5);
SDA(0);
delay_1us(5);
SCL(0);
delay_1us(5);
}
通讯信号------结束
部分代码展示:
cpp
static void stop() {
SDA_OUT();
SCL(0);
SDA(0);
SCL(1);
delay_1us(5);
SDA(1);
delay_1us(5);
}
通讯信号------发送数据
从SCL上升沿到下降沿的这个阶段数据是有有效的,因为如果SDA在左边虚线靠右位置才拉高,那么这个信号就和结束信号一样了(图一),同理,如果SDA在右边虚线靠左的位置就拉低了,那么这个信号就和开始信号一样了(图二)。
所以,总结:不能在这个数据有效性阶段动数据,要是改数据,要在SCL低电平的时候修改。
通讯信号------等待响应
等待响应信号,首先SDA拉高,等待一会然后SCL拉高,此时SDA将控制权交给从设备,也就是说SDA由输出模式转化为输入模式,然后我们对SDA的电平进行检测,如果检测到是低电平,说明应答成功,代码返回0。如果检测到高点平则说明无人应答,此时代码应该返回1或者其他设定的数。应答成功后,SDA收回控制权,SCL拉低。
部分代码:
cpp
#define SDA_STATE() gpio_input_bit_get(SDA_PORT, SDA_PIN)
static uint8_t wait_ack(){
// SDA拉高, 等待从设备拉低
SDA(1);
DELAY();
// SCL拉高, 同时释放SDA权限(变成输入模式)
SCL(1);
SDA_IN();
DELAY();
if(SDA_STATE() == RESET){
// 从设备拉低了SDA,应答成功
SCL(0);
SDA_OUT();
}else {
// 无人应答,应答失败, 直接结束
stop();
return 1;
}
return 0;
}
这样上面所有通讯信号开始时,都要将SDA控制权收回SDA_OUT(),保证能够正常输出。
补充
设备地址实际发送的是左移一位后的数据,最右边一位是0就是write,1就是read。
I2C读操作
流程如下
- 开始。
- 发送设备地址(写地址),等待从设备响应
- 发送寄存器地址,等待从设备响应。
- 开始
- 发送设备地址(读地址),等待从设备响应
- 接收一个字节,发送响应给从设备。这个操作是循环执行,直到没有数据。当是最后一个数据时,发送空响应。
- 停止。
代码解析
I2C读流程前半部分和写流程一样,我们只需在读流程的基础上进行修改和增加即可,这里注意的是发送的设备地址左移后最后一位要置为1,代表读数据。
通讯信号------接收数据
cpp
static uint8_t recv(){
// 释放SDA控制权,进入输入模式
SDA_IN();
uint8_t cnt = 8; //1个字节 8bit
uint8_t data = 0x00; // 空容器接收数据
while(cnt--){ // 接收一个bit (先收高位)
// SCL拉低
SCL(0); // 等待从设备准备数据
DELAY();
SCL(1); // 设置数据有效性
// 0000 0000 -> 1111 1011
// 0000 0001 8
// 0000 0011 7
// 0000 0111 6
// 0000 1111 5
// 0001 1111 4
// 0011 1110 3
// 0111 1101 2
// 1111 1011 1
data <<= 1;
if(SDA_STATE()) data++;
// SCL在高电平等待一会儿
DELAY();
}
// 最后一次低电平,不能忘
SCL(0);
return data;
}
先释放SDA的控制权,进入输入模式,也就是由从设备控制,然后进行循环从高位接收数据,将SCL拉低,等待从设备准备数据,延时一会后将SCL拉高,保证数据的有效性,准备一个空容器放数据,每次都左移一位然后存放数据,当检测到SDA是高电平1时,空容器左移后最后一位加1,即data++,如果检测到是低电平0时,则不进行操作,延时一会,最后别忘了把SCL拉低,保证后面的数据传输。
通讯信号------发送响应
cpp
static void send_ack(){
// 主机发送ACK响应
// 主机获取SDA控制权,进入输出模式
SDA_OUT();
// 拉低SDA
SDA(0);
DELAY();
// 拉高SCL
SCL(1);
DELAY();
// 拉低SCL
SCL(0);
DELAY();
}
cpp
static void send_nack(){
// 主机发送NACK响应
// 主机获取SDA控制权,进入输出模式
SDA_OUT();
// 拉高SDA
SDA(1);
DELAY();
// 拉高SCL
SCL(1);
DELAY();
// 拉低SCL
SCL(0);
DELAY();
}
补充
收到数据就要ACK反馈,收到最后一个数据的时候是NACK反馈。
总结
以上就是GD32的I2C的软实现过程,需要大家掌握,因为不同平台的硬实现可能会不同,但软实现每个平台都是一样的,所以大家重点掌握软实现,我们下期再见!