返回列表 发新帖我要提问本帖赏金: 80.00元(功能说明)

[PIC®/AVR®/dsPIC®产品] 看看时序图写I2C驱动,教你如何自己手撸非标I2C驱动函数

[复制链接]
30565|102
 楼主| gaoyang9992006 发表于 2023-11-26 20:49 | 显示全部楼层 |阅读模式
本帖最后由 gaoyang9992006 于 2023-11-27 14:23 编辑

#申请原创# @21小跑堂 [/url]
很多人不知道怎么看着时序图写程序,这里结合一个非标准的I2C器件,教大家如何写一个高效的IO模拟I2C时序。

观察该时序,具备I2C的开始信号,I2C的结束信号,I2C的应答,非应答,响应应答,以及写字节,读字节的基本操作时序。
我们一步一步的分析:
(1)I2C开始信号
观察时序图,在SCLK高电平的状态下,在SDIO产生一个下降沿是为开始信号。
  1. void I2C_Start()
  2. {
  3.   //设置I2C使用的两个引脚为输出模式
  4.   pinMode(SCLK_PIN, OUTPUT);
  5.   pinMode(SDIO_PIN, OUTPUT);

  6.   //在SCL为高电平的时候让SDA产生一个下降沿是为开始信号
  7.   digitalWrite(SDIO_PIN, 1);
  8.   digitalWrite(SCLK_PIN, 1);
  9.   digitalWrite(SDIO_PIN, 0);
  10. }
上述代码即先将两个引脚设置为输出模式,然后在SCLK为高电平的时候在SDIO引脚输出一个下降沿。

(2)I2C停止信号
观察时序图,在SCLK为高电平的时候在SDIO引脚产生一个上升沿是为停止信号
  1. void I2C_Stop()
  2. {
  3.   pinMode(SDIO_PIN, OUTPUT);
  4.   //在SCL为高电平的时候让SDA产生一个上升沿是为停止信号
  5.   digitalWrite(SDIO_PIN, 0);
  6.   digitalWrite(SCLK_PIN, 1);
  7.   digitalWrite(SDIO_PIN, 1);
  8. }
这里采用的是Arduino编写的IO基本操作,你可以替换成任意单片机的IO操作。
由于整个过程SCLK引脚一直是输出状态,所以仅在开始信号中对SCLK初始化为输出模式,而过程中可能会修改SDIO的输入输出模式,所以其他的函数开头都要根据情况对SDIO引脚的模式进行设置。
通过三行代码实现在SCLK为高电平的时候在SDIO产生一个上升沿,实现停止信号。
(3)写字节操作
接下来按照时序的顺序编写方便认读
I2C的读写字节是这么定义的:当时钟线为低电平的时候允许修改数据线的电平状态,在时钟线为高电平的时候读取数据线的状态。
因为是写操作,因此我们要先将时钟线SCLK拉低,然后修改SDIO的值,然后拉高时钟,拉高后,从机就会从总线上读取SDIO的状态。接着一位一位的这么发送
  1. void I2C_Write(uint8_t dat)
  2. {
  3.   pinMode(SDIO_PIN, OUTPUT);
  4.   //拉低时钟线后可修改数据线的状态
  5.   digitalWrite(SCLK_PIN, 0);
  6.   for(int i=0;i<8;i++)
  7.   {
  8.     digitalWrite(SDIO_PIN, (bool)(dat&0x80));
  9.     digitalWrite(SCLK_PIN, 1);//在高电平时候送出数据
  10.     dat=dat<<1;
  11.     digitalWrite(SCLK_PIN, 0);//拉低准备下一个位的数据发送
  12.   }
  13. }
上述代码正描述了这一情况,为了保证最后是低电平,这里将SCLK的第一次拉低放到循环外面,这样可以用最少的执行次数完成一个字节的写任务,同时结束完一个字节写入后时钟线是低电平状态。(时序图中写入的第一个字节为DeviceID,第二个字节为寄存器地址+读写位)
写完一个字节后,从机会对写入事件进行应答,这个时候主级可以从总线上读取应答信号。
(4)读取从机应答引号
应答信号在写入完一个字节后的低电平后由从机送出,在时钟为高电平的时候可以读取出来,我们注意到写入自己操作后时钟线已经是低电平了,因此这个时候
只要拉高时钟线,接下来就可以读取应答信号,读取完应答信号根据时序图应该拉低时钟准备下一个字节的写入。
  1. bool I2C_RACK()
  2. {
  3.   bool ack;
  4.   pinMode(SDIO_PIN, INPUT);

  5.   digitalWrite(SCLK_PIN, 1);//接收应答信号,当时钟拉高时候,从机送出应答信号
  6.   ack = digitalRead(SDIO_PIN);
  7.   digitalWrite(SCLK_PIN, 0);//读取完应答信号后拉低时钟。
  8.   return ack;
  9. }
如上代码所示,即为接收从机应答,拉高时钟,读取应答,再拉低,返回应答。如果从机应答了,这里会读取到一个低电平。
后面就是再写入一个寄存器+读写位的地址,参靠上面的写入操作。
写入寄存器地址后,紧跟着又一个接收从机应答信号,然后从机就会送出数据,送出的数据分高字节和低字节,高低字节间要有一个主机发送给从机的应答信号,这样从机酒知道主机收到了数据,就会送出后面的低字节数据。
(5)读字节操作
注意,前面说过,读写都是总线在时钟低电平时候修改数据线,在高电平送出。
因此主机读取从机送来的数据仍然是在高电平时候读取。
  1. uint8_t I2C_Read()
  2. {
  3.   uint8_t dat=0;
  4.   pinMode(SDIO_PIN, INPUT);
  5.   for(int i=0;i<8;i++)
  6.   {
  7.     digitalWrite(SCLK_PIN, 1);//读取数据时候是在时钟的高电平状态读取
  8.     dat=dat<<1;
  9.     if(digitalRead(SDIO_PIN))
  10.     {
  11.       dat=dat|1;
  12.     }
  13.     digitalWrite(SCLK_PIN, 0);//拉低时钟线准备下一个位的读取
  14.   }
  15.   return dat;
  16. }
操作过程是将SDIO数据线的IO设置为输入模式,准备读取,然后拉高时钟,读取数据,移位,拉低循环读取8位数据。
注意到,操作完一个字节读取任务后,时钟线还是低电平。
读取完一个字节后,主机要给从机发送一个应答信号,这样从机会接着发低字节数据。

(6)主机发送应答信号给从机
  1. void I2C_ACK()
  2. {
  3. pinMode(SDIO_PIN, OUTPUT);
  4. digitalWrite(SDIO_PIN, 0);//给从机发送应答信号,即拉低数据线,然后拉高时钟让从机读取该应答
  5. digitalWrite(SCLK_PIN, 1);
  6. digitalWrite(SCLK_PIN, 0);//执行完应答后拉低时钟线,准备下一步动作。
  7. }
拉低数据线,然后在高电平的时候让从机去读取,之后拉低时钟线准备下一步接收动作。

当再接收一个字节后,就读取完成了,这个时候就是产生一个非应答信号,然后发给总线结束信号,告诉从机一个读写周期结束了。
(7)主机非应答信号
什么是非应答信号呢?
就是接收完了数据,释放数据线,不去拉低数据线
  1. void I2C_NACK()
  2. {
  3.   //非应答信号:即主机不再对从机进行应答,主机释放数据线,即拉高数据线,然后给时钟一个周期信号(拉高再拉低)
  4.   pinMode(SDIO_PIN, OUTPUT);
  5.   digitalWrite(SDIO_PIN, 1);
  6.   digitalWrite(SCLK_PIN, 1);
  7.   digitalWrite(SCLK_PIN, 0);
  8. }
将SDIO引脚设置为输出,拉高数据线,即为释放数据线,然后拉高拉低时钟,即在时钟线产生一个时钟周期信号。
然后发送结束信号。结束信号在开头已经讲明,即在时钟线为高电平的状态下,在数据线产生一个上升沿。
观察以上代码没一个多余重复的操作动作,即完美的视线了时序图上的所有操作。
接下来就是利用上述的I2C成分进行对寄存器的读写操作了。
(8)读寄存器
由于图中设备的DeviceID 为0x80,即直接写进来,
从机判断是读还是写的字节在寄存器地址
因此将寄存器的地址左移一位,在末尾补上是读(1)还是写(0)
  1. uint16_t read_reg(uint8_t reg)
  2. {
  3.   uint16_t dat=0;
  4.   reg=(reg<<1)|1;
  5.   I2C_Start();
  6.   I2C_Write(0x80);
  7.   I2C_RACK();
  8.   I2C_Write(reg);
  9.   I2C_RACK();
  10.   dat=I2C_Read();
  11.   dat=dat<<8;
  12.   I2C_ACK();
  13.   dat=dat|I2C_Read();
  14.   I2C_NACK();
  15.   I2C_Stop();
  16.   return dat;
  17. }
(9) 写寄存器操作
  1. void write_reg(uint8_t reg, uint16_t dat)
  2. {
  3.   reg=(reg<<1);
  4.   I2C_Start();
  5.   I2C_Write(0x80);
  6.   I2C_RACK();
  7.   I2C_Write(reg);
  8.   I2C_RACK();
  9.   I2C_Write(dat>>8);
  10.   I2C_RACK();
  11.   I2C_Write(dat&0xFF);
  12.   I2C_NACK();
  13.   I2C_Stop();
  14. }
最后对寄存器读写函数测试
  1. void setup()
  2. {
  3.   Serial.begin(115200);
  4.   Serial.println("Hello I2C");
  5.   write_reg(0x02,0x2250);
  6.   Serial.println(read_reg(0x02),HEX);
  7.   write_reg(0x02,0x2281);
  8.   Serial.println(read_reg(0x02),HEX);
  9. }

  10. void loop()
  11. {

  12. }



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

最后晒出完整的测试代码
  1. #define SCLK_PIN 8
  2. #define SDIO_PIN 9


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

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

  13. void I2C_Stop()
  14. {
  15.   pinMode(SDIO_PIN, OUTPUT);
  16.   //在SCL为高电平的时候让SDA产生一个上升沿是为停止信号
  17.   digitalWrite(SDIO_PIN, 0);
  18.   digitalWrite(SCLK_PIN, 1);
  19.   digitalWrite(SDIO_PIN, 1);
  20. }

  21. void I2C_Write(uint8_t dat)
  22. {
  23.   pinMode(SDIO_PIN, OUTPUT);
  24.   //拉低时钟线后可修改数据线的状态
  25.   digitalWrite(SCLK_PIN, 0);
  26.   for(int i=0;i<8;i++)
  27.   {
  28.     digitalWrite(SDIO_PIN, (bool)(dat&0x80));
  29.     digitalWrite(SCLK_PIN, 1);//在高电平时候送出数据
  30.     dat=dat<<1;
  31.     digitalWrite(SCLK_PIN, 0);//拉低准备下一个位的数据发送
  32.   }
  33. }

  34. uint8_t I2C_Read()
  35. {
  36.   uint8_t dat=0;
  37.   pinMode(SDIO_PIN, INPUT);
  38.   for(int i=0;i<8;i++)
  39.   {
  40.     digitalWrite(SCLK_PIN, 1);//读取数据时候是在时钟的高电平状态读取
  41.     dat=dat<<1;
  42.     if(digitalRead(SDIO_PIN))
  43.     {
  44.       dat=dat|1;
  45.     }
  46.     digitalWrite(SCLK_PIN, 0);//拉低时钟线准备下一个位的读取
  47.   }
  48.   return dat;
  49. }


  50. bool I2C_RACK()
  51. {
  52.   bool ack;
  53.   pinMode(SDIO_PIN, INPUT);

  54.   digitalWrite(SCLK_PIN, 1);//接收应答信号,当时钟拉高时候,从机送出应答信号
  55.   ack = digitalRead(SDIO_PIN);
  56.   digitalWrite(SCLK_PIN, 0);//读取完应答信号后拉低时钟。
  57.   return ack;
  58. }

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

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

  74. uint16_t read_reg(uint8_t reg)
  75. {
  76.   uint16_t dat=0;
  77.   reg=(reg<<1)|1;
  78.   I2C_Start();
  79.   I2C_Write(0x80);
  80.   I2C_RACK();
  81.   I2C_Write(reg);
  82.   I2C_RACK();
  83.   dat=I2C_Read();
  84.   dat=dat<<8;
  85.   I2C_ACK();
  86.   dat=dat|I2C_Read();
  87.   I2C_NACK();
  88.   I2C_Stop();
  89.   return dat;
  90. }

  91. void write_reg(uint8_t reg, uint16_t dat)
  92. {
  93.   reg=(reg<<1);
  94.   I2C_Start();
  95.   I2C_Write(0x80);
  96.   I2C_RACK();
  97.   I2C_Write(reg);
  98.   I2C_RACK();
  99.   I2C_Write(dat>>8);
  100.   I2C_RACK();
  101.   I2C_Write(dat&0xFF);
  102.   I2C_NACK();
  103.   I2C_Stop();
  104. }


  105. void setup()
  106. {
  107.   Serial.begin(115200);
  108.   Serial.println("Hello I2C");
  109.   write_reg(0x02,0x2250);
  110.   Serial.println(read_reg(0x02),HEX);
  111.   write_reg(0x02,0x2281);
  112.   Serial.println(read_reg(0x02),HEX);
  113. }

  114. void loop()
  115. {

  116. }
最后再问一句,看完本帖,你学会纯手工撸IO模拟I2C时序的代码了吗?

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

×

打赏榜单

21小跑堂 打赏了 80.00 元 2023-11-28
理由:恭喜通过原创审核!期待您更多的原创作品~

评论

网上模拟IIC千千万,IIC简介万万千,但是作者的解释缺十分清晰,一步步教你手搓IIC协议,掌握此技能,万般皆可用。(蓝V用户打赏已提升~)  发表于 2023-11-28 15:18
734774645 发表于 2023-11-26 21:03 | 显示全部楼层
纯手工撸I2C。。。
Bowclad 发表于 2023-11-27 14:02 来自手机 | 显示全部楼层
学废了学废了
储小勇_526 发表于 2023-11-29 10:30 | 显示全部楼层
突然怀念之前工作中可以玩CPLD,时序玩的飞起。
储小勇_526 发表于 2023-11-29 15:38 | 显示全部楼层
今天上午看到你的帖子,下午就看到同事自己写的I2C代码,SMBus不香吗?
 楼主| gaoyang9992006 发表于 2023-11-29 15:51 | 显示全部楼层
储小勇_526 发表于 2023-11-29 15:38
今天上午看到你的帖子,下午就看到同事自己写的I2C代码,SMBus不香吗?

SMBus是I2C的子集,一般用于电池盒计算机外设通信,比如笔记本电池用的就是这个。
储小勇_526 发表于 2023-11-29 16:15 | 显示全部楼层
自己写的I2C代码如何确认自己的速率达到100K?
 楼主| gaoyang9992006 发表于 2023-11-29 20:28 | 显示全部楼层
储小勇_526 发表于 2023-11-29 16:15
自己写的I2C代码如何确认自己的速率达到100K?

这种IO模拟的就不要讲具体速度了。
星辰大海不退缩 发表于 2023-12-1 08:44 | 显示全部楼层
确实很清晰符合大众认知
jpx001 发表于 2023-12-1 15:26 | 显示全部楼层
值得学习,受用了
Henryko 发表于 2023-12-4 10:16 来自手机 | 显示全部楼层
io模拟的速度得看自身翻转速度了
springvirus 发表于 2023-12-4 17:26 | 显示全部楼层
储小勇_526 发表于 2023-11-29 16:15
自己写的I2C代码如何确认自己的速率达到100K?

模拟I2C程序+逻辑分析仪,不错的搭配,可以玩玩各种I2C外设小器件,比如RTC
储小勇_526 发表于 2023-12-4 17:31 | 显示全部楼层
springvirus 发表于 2023-12-4 17:26
模拟I2C程序+逻辑分析仪,不错的搭配,可以玩玩各种I2C外设小器件,比如RTC ...

IIC这种低速的还不需要逻辑分析仪,示波器完全可以。看同事自己写的IIC通讯代码,一塌糊涂,ack读都是直接return1,自己写太随意,不如标准的代码。
woai32lala 发表于 2023-12-6 08:42 | 显示全部楼层
OliviaSH 发表于 2023-12-13 08:56 来自手机 | 显示全部楼层
把那几个数据帧模拟出来就行了
 楼主| gaoyang9992006 发表于 2023-12-13 15:10 | 显示全部楼层
储小勇_526 发表于 2023-12-4 17:31
IIC这种低速的还不需要逻辑分析仪,示波器完全可以。看同事自己写的IIC通讯代码,一塌糊涂,ack读都是直 ...

对,网上很多写的烂狗屎代码,所以我按照协议完整的重新写了一遍,每个函数都是按照协议写的,没有串台代码
zhuotuzi 发表于 2024-1-18 23:02 | 显示全部楼层
原来可以这么简单,学会了,copy
huahuagg 发表于 2024-1-23 23:19 | 显示全部楼层
认真看了一遍,掌握到诀窍了。
643757107 发表于 2024-1-23 23:20 | 显示全部楼层
试了一下,在51上也可以一次搞定。
drawingchips 发表于 2024-1-25 11:21 | 显示全部楼层
这个应该是软件的I2C吧?
您需要登录后才可以回帖 登录 | 注册

本版积分规则

个人签名:如果你觉得我的分享或者答复还可以,请给我点赞,谢谢。

2052

主题

16403

帖子

222

粉丝
快速回复 在线客服 返回列表 返回顶部