首页/文章/ 详情

看时序图写I2C驱动,教你如何自己手撸非标I2C驱动函数

9天前浏览16

很多人不知道怎么看着时序图写程序,下面结合一个非标准的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引脚输出一个下降沿。


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产生一个上升沿,实现停止信号。


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,第二个字节为寄存器地址+读写位)。


写完一个字节后,从机会对写入事件进行应答,这个时候主机可以从总线上读取应答信号。


4、读取从机应答引号


应答信号在写完一个字节的低电平后由从机送出,在时钟为高电平的时候可以读取出来,我们注意到写字节操作后时钟线已经是低电平了,因此这个时候


只要拉高时钟线,接下来就可以读取应答信号,读取应答信号根据时序图应该拉低时钟准备下一个字节的写入。












bool I2C_RACK(){  bool ack;  pinMode(SDIO_PIN, INPUT);
 digitalWrite(SCLK_PIN, 1);//接收应答信号,当时钟拉高时候,从机送出应答信号  ack = digitalRead(SDIO_PIN);  digitalWrite(SCLK_PIN, 0);//读取完应答信号后拉低时钟。  return ack;}

如上代码所示,即为接收从机应答,拉高时钟,读取应答,再拉低,返回应答。如果从机应答了,这里会读取到一个低电平。


后面就是再写入一个寄存器+读写位的地址,参靠上面的写入操作。


写入寄存器地址后,紧跟着又一个接收从机应答信号,然后从机就会送出数据,送出的数据分高字节和低字节,高低字节间要有一个主机发送给从机的应答信号,这样从机就知道主机收到了数据,就会送出后面的低字节数据。


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位数据。


注意,操作完一个字节读取任务后,时钟线还是低电平。


读取完一个字节后,主机要给从机发送一个应答信号,这样从机会接着发低字节数据。


6、主机发送应答信号给从机









void I2C_ACK(){pinMode(SDIO_PIN, OUTPUT);digitalWrite(SDIO_PIN, 0);//给从机发送应答信号,即拉低数据线,然后拉高时钟让从机读取该应答digitalWrite(SCLK_PIN, 1);digitalWrite(SCLK_PIN, 0);//执行完应答后拉低时钟线,准备下一步动作。}

拉低数据线,然后在高电平的时候让从机去读取,之后拉低时钟线准备下一步接收动作。


当再接收一个字节后,就读取完成了,这个时候就是产生一个非应答信号,然后发给总线结束信号,告诉从机一个读写周期结束了。


7、主机非应答信号


什么是非应答信号呢?


就是接收完了数据,释放数据线,不去拉低数据线。










void I2C_NACK(){  //非应答信号:即主机不再对从机进行应答,主机释放数据线,即拉高数据线,然后给时钟一个周期信号(拉高再拉低)  pinMode(SDIO_PIN, OUTPUT);  digitalWrite(SDIO_PIN, 1);  digitalWrite(SCLK_PIN, 1);  digitalWrite(SCLK_PIN, 0);}

将SDIO引脚设置为输出,拉高数据线,即为释放数据线,然后拉高拉低时钟,即在时钟线产生一个时钟周期信号。


然后发送结束信号。结束信号在开头已经讲明,即在时钟线为高电平的状态下,在数据线产生一个上升沿。


观察以上代码没一个多余重复的操作动作,即完美的视线了时序图上的所有操作。


接下来就是利用上述的I2C成分进行对寄存器的读写操作了。


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();}

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
















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() {
}


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


最后晒出完整的测试代码:








































































































































#define SCLK_PIN 8#define SDIO_PIN 9

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);}
void I2C_Stop(){  pinMode(SDIO_PIN, OUTPUT);  //在SCL为高电平的时候让SDA产生一个上升沿是为停止信号  digitalWrite(SDIO_PIN, 0);  digitalWrite(SCLK_PIN, 1);  digitalWrite(SDIO_PIN, 1);}
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);//拉低准备下一个位的数据发送  }}
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;}

bool I2C_RACK(){  bool ack;  pinMode(SDIO_PIN, INPUT);
 digitalWrite(SCLK_PIN, 1);//接收应答信号,当时钟拉高时候,从机送出应答信号  ack = digitalRead(SDIO_PIN);  digitalWrite(SCLK_PIN, 0);//读取完应答信号后拉低时钟。  return ack;}
void I2C_ACK(){  pinMode(SDIO_PIN, OUTPUT);  digitalWrite(SDIO_PIN, 0);//给从机发送应答信号,即拉低数据线,然后拉高时钟让从机读取该应答  digitalWrite(SCLK_PIN, 1);  digitalWrite(SCLK_PIN, 0);//执行完应答后拉低时钟线,准备下一步动作。}
void I2C_NACK(){  //非应答信号:即主机不再对从机进行应答,主机释放数据线,即拉高数据线,然后给时钟一个周期信号(拉高再拉低)  pinMode(SDIO_PIN, OUTPUT);  digitalWrite(SDIO_PIN, 1);  digitalWrite(SCLK_PIN, 1);  digitalWrite(SCLK_PIN, 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;}
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();}

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() {
}

看完这篇文章,你学会纯手工撸IO模拟I2C时序的代码了吗?

声明:


 
声明:本号对所有原创、转载文章的陈述与观点均保持中立,推送文章仅供读者学习和交流。文章、图片等版权归原作者享有,如有侵权,联系删除。  

来源:硬件笔记本
电路电子SCL
著作权归作者所有,欢迎分享,未经许可,不得转载
首次发布时间:2025-11-11
最近编辑:9天前
硬件笔记本
本科 一点一滴,厚积薄发。
获赞 156粉丝 45文章 641课程 0
点赞
收藏
作者推荐

在MOS管栅极前加100Ω电阻,究竟有啥妙用?

我们经常会听到在MOSFET栅极前增加一个电阻。那么,为什么要增加这个电阻,进一步地来讲,为什么要增加一个100Ω电阻?在MOSFET的栅极前增加一个电阻?MOS管是电压型控制器件,一般情况下MOS管的导通,只需要控制栅极的电压超过其开启阈值电压即可,并不需要栅极电流。所以从本质上来讲,MOS管工作时栅极上并不需要串联任何电阻。还有一种情况,也就是MOS管栅极存在的寄生电容。一般为了加快MOS管导通和截止的速度,降低其导通和截止过程中的产生损耗,栅极上的等效电阻是应该越小越好,最好为0。但我们却经常会看到关于MOSFET的电路中,栅极前串联着一个电阻。如下图:那为什么要串联这个电阻呢?在开关状态下,通常解释就是为了防止MOSFET在开关过程中会产生震荡波形,因为这会增加MOSFET开关损耗,不仅如此,如果震荡过大,还会引起MOS管被击穿。再进一步讲,为什么电阻是100Ω呢?我在网上看到一个仿真试验,实验在MOSFET电路中的栅极串联电阻R3,分别对它取1欧姆,10欧姆,50欧姆进行仿真实验:a. 当R3为1欧姆时,输出电压Vds上出现高频震荡信号。b. 当R3为10欧姆时,输出电压Vds的高频震荡信号明显被衰减。c. 当R3为50欧姆时,输出电压Vds的上升沿变得缓慢。其栅极电压上,因为漏极-栅极之间的米勒电容效应引发了台阶。此时对应的MOS管的功耗大大增加。简单来说,如果它的取值小了,就会引起输出振铃,如果大了就会增加MOS管的开关过渡时间,从而增加其功耗。是不是看到这里,还是不太清楚选值为100Ω的作用在哪里?我们以上面提到的开关震荡再进深一步探讨。这是一张MOSFET的驱动电路图:功率MOS管的驱动电路中会分布各种电感,例如图中的L,它们与MOSFET的Cgd, Cge会形成谐振电路:对开关驱动信号中的高频谐波分量产生谐振,进而引起功率管输出电压的波动。MOS管的栅极串联电阻Rg,会增大MOS管驱动回路中的损耗,然后降低谐振回路的Q值,使得电感与电容谐振现象快速衰减。在这里我们可以理解到,MOS管栅极上所串联的电阻,是根据具体的MOS管和电路分布杂散电感来确定。这跟我们上面提到阻值影响相关的,下面会详细提到:如上图,当Rg值比较小时,驱动电压上冲会比较高,震荡较多,L(电感)越大也越明显,此时会对MOSFET及其他器件性能产生一定影响。此外,驱动电流的峰值也比较大,但是一般情况下,IC的驱动电流输出能力是有一定限制的。当阻值过大时,实际驱动电流达到IC输出的最大值时,IC输出就相当于一个恒流源,会对Cgs线性充电,驱动电压波形的上升率会变慢。而驱动波形上升比较慢的话,如果MOSFET有较大电流通过时就有不利影响。可以得出,阻值过大过小都是对MOSFET驱动电路产生一定不利影响的,而如何确定出合适的阻值,一般是根据管子的电流容量和电压额定值以及开关频率,来选取Rg的数值。还记得上面那句话吗?MOS管栅极上所串联的电阻,是根据具体的MOS管和电路分布杂散电感来确定。声明: 声明:文章来源网络。本号对所有原创、转载文章的陈述与观点均保持中立,推送文章仅供读者学习和交流。文章、图片等版权归原作者享有,如有侵权,联系删除。 来源:硬件笔记本

未登录
还没有评论
课程
培训
服务
行家
VIP会员 学习计划 福利任务
下载APP
联系我们
帮助与反馈