小夏天的大西瓜 发表于 2024-3-26 14:48

看时序图写I2C驱动

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:53 编辑

很多人不知道怎么看着时序图写程序,下面结合一个非标准的I2C器件,教大家如何写一个高效的IO模拟I2C时序。



观察该时序,具备I2C的开始信号,I2C的结束信号,I2C的应答、非应答、响应应答,以及写字节和读字节的基本操作时序。

下面,我们一步一步分析。
1、I2C开始信号

观察时序图,在SCLK高电平的状态下,在SDIO产生一个下降沿是为开始信号。


void I2C_Start()
{
//设置I2C使用的两个引脚为输出模式
pinMode(SCLK_PIN, OUTPUT);
pinMode(SDIO_PIN, OUTPUT);

//在SCL为高电平的时候让SDA产生一个下降沿是为开始信号
digitalWrite(SDIO_PIN, 1);
digitalWrite(SCLK_PIN, 1);
digitalWrite(SDIO_PIN, 0);
}

上述代码即先将两个引脚设置为输出模式,然后在SCLK为高电平的时候在SDIO引脚输出一个下降沿。


小夏天的大西瓜 发表于 2024-3-26 14:48

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:53 编辑

2、I2C停止信号
观察时序图,在SCLK为高电平的时候在SDIO引脚产生一个上升沿是为停止信号。
void I2C_Stop()
{
pinMode(SDIO_PIN, OUTPUT);
//在SCL为高电平的时候让SDA产生一个上升沿是为停止信号
digitalWrite(SDIO_PIN, 0);
digitalWrite(SCLK_PIN, 1);
digitalWrite(SDIO_PIN, 1);
}这里采用的是Arduino编写的IO基本操作,你可以替换成任意单片机的IO操作。

由于整个过程SCLK引脚一直是输出状态,所以仅在开始信号中对SCLK初始化为输出模式,而过程中可能会修改SDIO的输入输出模式,所以其他的函数开头都要根据情况对SDIO引脚的模式进行设置。

通过三行代码实现在SCLK为高电平的时候在SDIO产生一个上升沿,实现停止信号。

小夏天的大西瓜 发表于 2024-3-26 14:50

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:54 编辑

3、写字节操作
接下来,按照时序的顺序编写方便认读
I2C的读写字节是这么定义的:当时钟线为低电平的时候,允许修改数据线的电平状态,在时钟线为高电平的时候读取数据线的状态。
因为是写操作,因此我们要先将时钟线SCLK拉低,再修改SDIO的值,然后拉高时钟。拉高后,从机就会从总线上读取SDIO的状态,接着一位一位的这么发送。
void I2C_Write(uint8_t dat)
{
pinMode(SDIO_PIN, OUTPUT);
//拉低时钟线后可修改数据线的状态
digitalWrite(SCLK_PIN, 0);
for(int i=0;i<8;i++)
{
    digitalWrite(SDIO_PIN, (bool)(dat&0x80));
    digitalWrite(SCLK_PIN, 1);//在高电平时候送出数据
    dat=dat<<1;
    digitalWrite(SCLK_PIN, 0);//拉低准备下一个位的数据发送
}
}上述代码正描述了这一情况:为了保证最后是低电平,这里将SCLK的第一次拉低放到循环外面,这样可以用最少的执行次数完成一个字节的写任务;同时,结束完一个字节写入后时钟线是低电平状态(时序图中写入的第一个字节为DeviceID,第二个字节为寄存器地址+读写位)。
写完一个字节后,从机会对写入事件进行应答,这个时候主级可以从总线上读取应答信号。

小夏天的大西瓜 发表于 2024-3-26 14:50

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:54 编辑

4、读取从机应答引号
应答信号在写入完一个字节后的低电平后由从机送出,在时钟为高电平的时候可以读取出来,我们注意到写入自己操作后时钟线已经是低电平了,因此这个时候
只要拉高时钟线,接下来就可以读取应答信号,读取完应答信号根据时序图应该拉低时钟准备下一个字节的写入。
bool I2C_RACK()
{
bool ack;
pinMode(SDIO_PIN, INPUT);

digitalWrite(SCLK_PIN, 1);//接收应答信号,当时钟拉高时候,从机送出应答信号
ack = digitalRead(SDIO_PIN);
digitalWrite(SCLK_PIN, 0);//读取完应答信号后拉低时钟。
return ack;
}如上代码所示,即为接收从机应答,拉高时钟,读取应答,再拉低,返回应答。如果从机应答了,这里会读取到一个低电平。
后面就是再写入一个寄存器+读写位的地址,参靠上面的写入操作。
写入寄存器地址后,紧跟着又一个接收从机应答信号,然后从机就会送出数据,送出的数据分高字节和低字节,高低字节间要有一个主机发送给从机的应答信号,这样从机酒知道主机收到了数据,就会送出后面的低字节数据。

小夏天的大西瓜 发表于 2024-3-26 14:51

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:54 编辑

5、读字节操作
注意,前面说过,读写都是总线在时钟低电平时候修改数据线,在高电平送出。
因此,主机读取从机送来的数据仍然是在高电平时候读取。
uint8_t I2C_Read()
{
uint8_t dat=0;
pinMode(SDIO_PIN, INPUT);
for(int i=0;i<8;i++)
{
    digitalWrite(SCLK_PIN, 1);//读取数据时候是在时钟的高电平状态读取
    dat=dat<<1;
    if(digitalRead(SDIO_PIN))
    {
      dat=dat|1;
    }
    digitalWrite(SCLK_PIN, 0);//拉低时钟线准备下一个位的读取
}
return dat;
}操作过程是将SDIO数据线的IO设置为输入模式,准备读取,然后拉高时钟,读取数据,移位,拉低循环读取8位数据。
注意,操作完一个字节读取任务后,时钟线还是低电平。
读取完一个字节后,主机要给从机发送一个应答信号,这样从机会接着发低字节数据。

小夏天的大西瓜 发表于 2024-3-26 14:52

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:55 编辑

6、主机发送应答信号给从机
void I2C_ACK()
{
pinMode(SDIO_PIN, OUTPUT);
digitalWrite(SDIO_PIN, 0);//给从机发送应答信号,即拉低数据线,然后拉高时钟让从机读取该应答
digitalWrite(SCLK_PIN, 1);
digitalWrite(SCLK_PIN, 0);//执行完应答后拉低时钟线,准备下一步动作。
}拉低数据线,然后在高电平的时候让从机去读取,之后拉低时钟线准备下一步接收动作。
当再接收一个字节后,就读取完成了,这个时候就是产生一个非应答信号,然后发给总线结束信号,告诉从机一个读写周期结束了。

小夏天的大西瓜 发表于 2024-3-26 14:52

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:55 编辑

7、主机非应答信号
什么是非应答信号呢?
就是接收完了数据,释放数据线,不去拉低数据线。
void I2C_NACK()
{
//非应答信号:即主机不再对从机进行应答,主机释放数据线,即拉高数据线,然后给时钟一个周期信号(拉高再拉低)
pinMode(SDIO_PIN, OUTPUT);
digitalWrite(SDIO_PIN, 1);
digitalWrite(SCLK_PIN, 1);
digitalWrite(SCLK_PIN, 0);
}将SDIO引脚设置为输出,拉高数据线,即为释放数据线,然后拉高拉低时钟,即在时钟线产生一个时钟周期信号。
然后发送结束信号。结束信号在开头已经讲明,即在时钟线为高电平的状态下,在数据线产生一个上升沿。
观察以上代码没一个多余重复的操作动作,即完美的视线了时序图上的所有操作。
接下来就是利用上述的I2C成分进行对寄存器的读写操作了。

小夏天的大西瓜 发表于 2024-3-26 14:53

本帖最后由 小夏天的大西瓜 于 2024-3-26 14:58 编辑

8、读寄存器
由于图中设备的DeviceID 为0x80,即直接写进来,从机判断是读还是写的字节在寄存器地址。
因此,将寄存器的地址左移一位,在末尾补上是读(1)还是写(0)。
uint16_t read_reg(uint8_t reg)

{

uint16_t dat=0;

reg=(reg<<1)|1;

I2C_Start();

I2C_Write(0x80);

I2C_RACK();

I2C_Write(reg);

I2C_RACK();

dat=I2C_Read();

dat=dat<<8;

I2C_ACK();

dat=dat|I2C_Read();

I2C_NACK();

I2C_Stop();

return dat;

}9、写寄存器操作
void write_reg(uint8_t reg, uint16_t dat)

{

reg=(reg<<1);

I2C_Start();

I2C_Write(0x80);

I2C_RACK();

I2C_Write(reg);

I2C_RACK();

I2C_Write(dat>>8);

I2C_RACK();

I2C_Write(dat&0xFF);

I2C_NACK();

I2C_Stop();

}

小夏天的大西瓜 发表于 2024-3-26 14:57

最后,对寄存器读写函数测试。

void setup()
{
Serial.begin(115200);
Serial.println("Hello I2C");
write_reg(0x02,0x2250);
Serial.println(read_reg(0x02),HEX);
write_reg(0x02,0x2281);
Serial.println(read_reg(0x02),HEX);
}

void loop()
{

}





读取的数值与写入的是一样的。

LOVEEVER 发表于 2024-3-27 16:21

楼主的这个IIC程序分析的很透彻,通俗易懂

jf101 发表于 2024-3-28 19:05

手撸IIC真的很详细

星辰大海不退缩 发表于 2024-3-29 14:18

程序步骤编写很详细,非常值得推荐

IFX-Wanmin 发表于 2024-5-27 16:26

有没有模拟I3C的程序?

小小蚂蚁举千斤 发表于 2024-5-31 16:21

手搓IIC,学习一下

g36xcv 发表于 2024-6-30 22:32

在SCLK高电平的状态下,在SDIO产生一个下降沿是为开始信号

逢dududu必shu 发表于 2024-8-17 01:05

可能还需要处理时序延迟和时钟频率等细节

kmnqhaha 发表于 2024-12-2 16:47

I2C的开始信号是由SDA在SCL高电平时产生下降沿。这是所有I2C通信的起始标志。我们可以通过程序模拟此时序。

地瓜patch 发表于 2024-12-2 20:00

很多外设用硬件IIC读写不成功

泡椒风爪 发表于 2025-4-30 19:24

在I2C中,数据是以字节为单位发送的。每发送一个字节,都需要保证每一位都正确地传输,包括对每位的应答信号。

泡椒风爪 发表于 2025-4-30 22:07

模拟I2C协议的时序需要对时序图有清晰的理解,并准确地控制SCL和SDA引脚的状态和时序。
页: [1] 2
查看完整版本: 看时序图写I2C驱动