我的代码:https://gitee.com/a1422749310/gd32_-official_-code
I2C 具体代码位置:https://gitee.com/a1422749310/gd32_-official_-code/blob/master/Hardware/i2c/i2c.c
黑马 - I2C原理
官方 - IIC 协议介绍
个人学习过程中的理解,有错误,欢迎指出
移植
【I2C 具体代码位置】中,要更改代码的地方
#define SCL(bit) gpio_bit_write(GPIOB, GPIO_PIN_6, bit == 1 ? SET : RESET);
#define SDA(bit) gpio_bit_write(GPIOB, GPIO_PIN_7, bit == 1 ? SET : RESET);
#define DELAY() delay_1us(5);
#define SDA_IN() gpio_mode_set(GPIOB, GPIO_MODE_INPUT, GPIO_PUPD_NONE, GPIO_PIN_7);
#define SDA_OUT() gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_7);
#define SDA_STATE() gpio_input_bit_get(GPIOB, GPIO_PIN_7)
以及
void I2C_Init()
其他地方,都是和平台无关的
写数据
SCL、SDA 数据的四种形式
SCL 和 SDA 的高低电平的持续时间是多少?
回答这个问题,需要涉及:I2C总线的时钟频率
I2C总线的时钟频率,通常在100kHz到400kHz之间,其中
- 100kHz是标准模式(Standard Mode)
- 1秒钟,就是 1 000 000us,100 000 个bit,相除,可得:10us 1bit
- 👆 10us,才得到一个 bit,那么,睡眠的时候,就是 5us(一半,用于开始、停止的变化)
- 400kHz是快速模式(Fast Mode)
- 1秒钟,就是 1 000 000us,400 000 个bit,相除,可得:2.5us 1bit
此外,I2C总线还支持更高速度
- 1秒钟,就是 1 000 000us,400 000 个bit,相除,可得:2.5us 1bit
- 高速模式(High Speed Mode):1MHz
- 超高速模式(Ultra-Fast Mode):5MHz
因此,SCL 和 SDA 的高低电平的持续时间是多少?要看具体使用的 I2C 的外设的 datasheet
比如,如果某个 datasheet 上面写了 100kHz,那么,就是 10us 1bit,就 #define DELAY() delay_1us(5);
开始、结束、发送、等待响应总结
- 开始(Start):正弦函数
- 结束(Stop):左边有 1 竖(都是低电平)的【凹函数】
- 发送(Send):SDA准备 🏃,持续3个delay;SCL 从暴富到破产(或者理解为跳水,但是,跳水,SDA 用了。 所以,可以这么理解:SCL,老板,指挥官,与财富相关,SDA,员工,行动派,与行为相关)
- 等待响应(Wait ACK):SCL、SDA 亲亲😘;SCL 从暴富到破产,SDA 控制权转移
👆 很奇怪,之前用【亲亲】,感觉不好意思🥵,于是改成【靠近】,后来发现,还是亲亲好,上头😓
👇 可以看到,只有【开始】(绿点)和【结束】(橙点),有比较长的 SCL(上面的信号是 SCL)(也印证了SCL 连续高两次)
懒得复制图片了,直接看这个链接吧 黑马 - IIC
1、开始 Start:【正弦函数】
[图片]
- 1:SCL 之前,SDA,一定是高的,不然没法下落
- 2:SCL,暴富,站起来了(起作用了)
- 3:将 SDA 拉低
- 4:SCL,破产,没落了,代表之后不起作用了(虎落平阳被犬欺)
买的淘宝链接(👈 LIXIN 给的链接,买贵了。。。)19.5 USB逻辑分析仪
黑马 - 逻辑分析仪
下载:逻辑分析仪Logic-2.4.9-windows-x64.exe
[图片]
[图片]
I2C 要进行配置 👇
[图片]
[图片]
[图片]
最终 👇,注意采样率,不要太低,我之前设置为 100kS/s,采样的波形,是错误的(当时没理解概念,将 100kS/s 和之前的 100kHz是标准模式 那个联系在一起了🤡。。。)
[图片]
2、结束 Stop:左边有 1 竖的【凹函数】
[图片]
[图片]
👆,这里,开始的 4 和结束的 1,共了
-
1:SCL、SDA 同时变低,方便 SCL 站起来
-
2:SCL,暴富,站起来了(起作用了)
-
3:SDA,上岸(游泳🏊结束)
static void stop() {
SCL(0);
SDA(0);
DELAY(); // <— 视频中没有加 delaySCL(1);
DELAY();SDA(1);
DELAY();
}
2.1、结束代码,有问题
👆 视频中代码有问题,没有加 delay
[图片]
[图片]
测试代码如下 👇,对应波形 👆
可以看到,stop 中,SCL(0)、SDA(0) 后,立马 SCL(1),在波形上,显示的是,是很窄的一个波形(👆 右图中,绿色,1、1 的框框)
另外,注意上面图片中,中间的区域,我将其设置为 11、00、11,目的是为了对比(之后 2.2 中,有关于对这个波形的讨论)
static void stop()
{
SDA_OUT(); // SDA 重新获得控制权,SDA 变成输出
SCL(0);
SDA(0);
// DELAY(); 未加 delay
SCL(1);
DELAY();
SDA(1);
DELAY();
}
void test_wave()
{
start();
// 中间
SCL(1); SDA(1); DELAY();
SCL(0); SDA(0); DELAY();
SCL(1); SDA(1); DELAY();
stop();
}
加上 delay 之后的波形 👇
[图片]
[图片]
所以我认为,虽然效果是一样,但是,还是加下好些
2.2、结束代码,有问题例子中,中间波形分析
// 中间
SCL(1); SDA(1); DELAY();
SCL(0); SDA(0); DELAY();
SCL(1); SDA(1); DELAY();
[图片]
[图片]
👆,并不是我之前认为的,SCL(1) 、SDA(1),波形就同时变高,而是有一个偏移(SDA,向右偏一点)
并且,SCL(1)、SDA(1),宽度也是不一样的
那么,如果我先设置 SDA 呢?
// 中间
SDA(1); SCL(1); DELAY();
SDA(0); SCL(0); DELAY();
SDA(1); SCL(1); DELAY();
[图片]
[图片]
👆 右图,因为 SDA 提前设置,所以,在 SCL 之前,变低(图中显示,红色在黄色,内部)
但是,之后,第 3 个 SDA(1); SCL(1);,预想中,应该是 SDA 先拉高,SCL 在拉高(可实际上,是 SCL 先拉高)
一种解释,是程序编译的时候,SDA(1); SCL(1); 交换了位置???那为什么 SDA(0); SCL(0); 没有交换?
所以,应该不是这个原因
另一种解释:SDA(1),SDA 拉高的时候,相比于 SCL(1) ,比较耗时,我认为这个是比较合理一点的解释
问下 ChatGPT
你的分析非常到位!让我们一起深入探讨你提出的两个可能的解释:
- 程序编译时的代码顺序
这种解释是比较不太可能的原因。在编译阶段,SDA(1) 和 SCL(1) 的顺序应该是严格按照程序中写的顺序执行的,编译器并不会自动交换这些信号的顺序,除非有特别的宏或硬件抽象层对这些函数做了特殊的处理。所以,SDA(1) 和 SCL(1) 顺序交换的问题,可以排除。 - SDA 拉高的耗时问题
这个解释是比较合理的。
- SDA 拉高时的时间问题:由于 I2C 总线的物理特性,SDA 线上的信号变化相对较慢,尤其是在高电平拉升时。如果你直接将 SDA 设置为高电平,可能需要一点时间来使 SDA 线达到稳定的高电平状态。而 SCL 线在此时是由外部的时钟拉升的,这意味着如果没有延时,SCL 会很快拉高,但 SDA 可能还没有达到稳定的高电平,导致了信号的不稳定或者错误。
- SCL 的变化时间较短:SCL 线的状态变化通常由外部时钟控制,所以其变化速度通常比较快。而 SDA 由于是由主设备控制,并且存在内部拉升电阻或者其他电路因素,所以 SDA 的拉高通常会稍微慢一些,尤其是在低电平拉升到高电平时,可能会有延迟。
为什么 SDA(0); SCL(0); 没有问题?
当你将 SDA 和 SCL 都拉低时,情况就简单了。因为 I2C 的标准定义在 SDA 和 SCL 都为低电平时,它们处于稳定状态,几乎没有信号的延迟或时序问题。所以 SDA(0); SCL(0); 不会引起问题。
总结:
更可能的原因是 SDA 拉高时的时间延迟。 由于 SDA 拉高的过程相较于 SCL 更加耗时,可能会导致在 SDA(1) 和 SCL(1) 同时设置时,SCL 先拉高了,而 SDA 还没有完全稳定。
3、发送 Send:SDA准备,持续3个delay;SCL 从暴富到破产
[图片]
[图片] - 1:SDA 准备了
- 2:SCL,暴富,站起来了(起作用了)
- 3:SCL,破产,没落了,代表之后不起作用了(虎落平阳被犬欺)
👆,可以看到,起作用的传输数据,持续了 3 的 delay(准备前、SCL 变高时、SCL 变低时)
4、等待响应 Wait ACK:SCL、SDA 亲亲😘;SCL 从暴富到破产,SDA 控制权转移
[图片]
[图片]
- 1:SCL、SDA 亲亲😘:SDA 变高,期望跳水;;;SCL 变低,破产,期望暴富,起作用,站起来
- 2:SCL 高电平,站起来了(起作用了),此时,SDA,已经让出控制权了,然后,观察 SDA
- SDA:低电平,代表应答
- SDA:高电平,代表不应答
- SDA,代表了,主动权
- 3:SCL,没落了,代表之后不起作用了(虎落平阳被犬欺)
模拟下成功,然后看看波形
模拟的时候,不要开 SDA_IN,会影响波形
static void wait_ack()
{
SDA(1);
SCL(0);
DELAY();
#if MOCK_WAIT_ACK_SUCCESS == 0
// SDA 交出控制权,SDA 变为输入
SDA_IN();
#endif
SCL(1);
#if MOCK_WAIT_ACK_SUCCESS
// 模拟从机,将 SDA 置 1,代表成功响应主机
SDA(0);
#endif
DELAY();
if (RESET == SDA_STATE() || 1 == MOCK_WAIT_ACK_SUCCESS)
{
// 成功
SCL(0);
SDA_OUT(); // SDA 重新获得控制权,SDA 变成输出
}
else
{
stop();
}
}
最终波形如下 👇
[图片]
[图片]
读
read
rece 和 send 通讯信号都是一样的,但是区别:SDA 的信号来源,来自:从设备
[图片]
发送响应:SCL、SDA 开始:ACK 不亲亲🫸,NACK 亲亲😘;SCL 从暴富到破产;最后都亲亲😘
[图片]
[图片]
最后,I2C_Read(0x01, 0x02, &data, 1);(后面 len 改为 2,可以分析 ACK) 输出波形如下
[图片]
[图片]
[图片]
[图片]
可以看到,接收,只有 1、2 —— 设置,是在 SCL(0) 的时候,设置的
模拟代码如下 👇,1011 0010 = 0xB2,和波形一致
#define MOCK_READ_DATA 1
static uint8_t recv()
{
#if MOCK_READ_DATA == 0
// SDA 交给从设备,主设备,应该是输入状态
SDA_IN();
#endif
#if MOCK_READ_DATA
// 1011 0010 = 0xB2
uint8_t mock_bit_array[8] = {1, 0, 1, 1, 0, 0, 1, 0};
#endif
uint8_t data = 0;
for (uint8_t i = 0; i < 8; i++)
{
// 给机会,给从设备,准备数据
SCL(0);
#if MOCK_READ_DATA
SDA(mock_bit_array[i]); // <— 设置,是在 SCL(0) 的时候,设置的
#endif
DELAY();
// 开始设置数据有效性
SCL(1);
#if MOCK_READ_DATA
data |= mock_bit_array[i] << (7 - i);
#else
data |= SDA_STATE() << (7 - i);
#endif
DELAY();
// SCL(0); 因为下一个循环,还是低电平,这里可以省略
}
// 最后一次,是高电平,不会循环回来,所以,要加上低电平,代表 SCL 没落了
SCL(0);
return data;
}