搜索
查看: 17666|回复: 13

[分享] 基于stm32的I2C总线通讯简介及使用操作(附代码)

[复制链接]

该用户从未签到

450

主题

498

帖子

3

蝴蝶豆

社区小助手

最后登录
2020-7-30
发表于 2017-6-20 16:44:25 | 显示全部楼层 |阅读模式

1、I2C总线简介

I2C是两线式串行总线,用于连接微控制器及其外围设备。

I2C总线最主要的优点是其简单性和有效性。由于接口直接在组件之上,因此I2C总线占用的空间非常小,减少了电路板的空间和芯片管脚的数量,降低了互联成本。总线的长度可高达25英尺,并且能够以10Kbps的最大传输速率支持40个组件。I2C总线的另一个优点是,它支持多主控(multimastering), 其中任何能够进行发送和接收的设备都可以成为主总线。一个主控能够控制信号的传输和时钟频率。当然,在任何时间点上只能有一个主控。


单片机的通讯模块常用的有UART、SPI、I2C、CAN等等,当然还有无线模块这里不讨论。UART大多用于单片机与PC的通信,CAN总线常用于单片机与单片机通信,而SPI和I2C则用于单片机和外围设备、外围设备和其它外围设备的通信,根据自己的需要来设计通讯方式。


相比较SPI而言,I2C需要更少的接线,仅由数据线SDA和时钟SCL构成 。而SPI则需要四根引线,它们是SDI(数据输入),SDO(数据输出),SCLK(时钟),CS(片选)。但是常常因为I2C的通讯协议较为复杂,不容易在程序中实现而导致数据丢失、无应答、“死等”等问题。


下面我们就来通过I2C的时序来解析一下I2C的通讯。



2、I2C总线的时序

I2C总线是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,最高传送速率100kbps。各种被控制电路均并联在这条总线上,但就像电话机一样只有拨通各自的号码才能工作,所以每个电路和模块都有唯一的地址,在信息的传输过程中,I2C总线上并接的每一模块电路既是主控器(或被控器),又是发送器(或接收器),这取决于它所要完成的功能。CPU发出的控制信号分为地址码和控制量两部分,地址码用来选址,即接通需要控制的电路,确定控制的种类;控制量决定该调整的类别及需要调整的量。这样,各控制电路虽然挂在同一条总线上,却彼此独立,互不相关。


I2C总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答信号。


开始信号:SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。

结束信号:SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。

应答信号:接收数据的IC在接收到8bit数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据。CPU向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。


I2C总线通信过程中出现的几种信号状态和时序分析如下:

(1)总线空闲状态

I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。


(2)启动信号

在时钟线SCL保持高电平期间,数据线SDA上的电平被拉低(即负跳变),定义为I2C总线总线的启动信号,它标志着一次数据传输的开始。

启动信号是一种电平跳变时序信号,而不是一个电平信号。启动信号是由主控器主动建立的,在建立该信号之前I2C总线必须处于空闲状态,如图所示。

11.jpg


(3)停止信号

在时钟线SCL保持高电平期间,数据线SDA被释放,使得SDA返回高电平(即正跳变),称为I2C总线的停止信号,它标志着一次数据传输的终止。

停止信号也是一种电平跳变时序信号,而不是一个电平信号,停止信号也是由主控器主动建立的,建立该信号之后,I2C总线将返回空闲状态。


(4)数据位传送

在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。

进行数据传送时,在SCL呈现高电平期间,SDA上的电平必须保持稳定,低电平为数据0,高电平为数据1。

只有在SCL为低电平期间,才允许SDA上的电平改变状态。逻辑0的电平为低电压,而逻辑1的电平取决于器件本身的正电源电压VDD(当使用独立电源时),如图所示。

12.jpg


(5) 应答信号

I2C总线上的所有数据都是以8位字节传送的,发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。

应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。

对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。

如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P,如图所示。

13.jpg


(6)插入等待时间

如果被控器需要延迟下一个数据字节开始传送的时间,则可以通过把时钟线SCL电平拉低并且保持,使主控器进入等待状态。

一旦被控器释放时钟线,数据传输就得以继续下去,这样就使得被控器得到足够时间转移已经收到的数据字节,或者准备好即将发送的数据字节。

带有CPU的被控器在对收到的地址字节做出应答之后,需要一定的时间去执行中断服务子程序,来分析或比较地址码,其间就把SCL线钳位在低电平上,直到处理妥当后才释放SCL线,进而使主控器继续后续数据字节的发送,如图所示。


(7)重启动信号

在主控器控制总线期间完成了一次数据通信(发送或接收)之后,如果想继续占用总线再进行一次数据通信(发送或接收),而又不释放总线,就需要利用重启动Sr信号时序。

重启动信号Sr既作为前一次数据传输的结束,又作为后一次数据传输的开始。利用重启动信号的优点是,在前后两次通信之间主控器不需要释放总线,这样就不会丢失总线的控制权,即不让其他主器件节点抢占总线。


(8)时钟同步

如果在某一I2C总线系统中存在两个主器件节点,分别记为主器件1和主器件2,其时钟输出端分别为CLK1和CLK0,它们都有控制总线的能力。

假设在某一期间两者相继向SCL线发出了波形不同的时钟脉冲序列CLK1和CLK2(时钟脉冲的高、低电平宽度都是依靠各自内部专用计数器定时产生的),在总线控制权还没有裁定之前这种现象是可能出现的。

鉴于I2C总线的“线与”特性,使得时钟线SCL上得到的时钟信号波形,既不像主器件1所期望的CLK1,也不像主器件2所期望的CLK2,而是两者进行逻辑与的结果。

CLKI和CLK2的合成波形作为共同的同步时钟信号,一旦总线控制权裁定给某一主器件,则总线时钟信号将会只由该主器件产生,如图所示。

14.jpg



(9)总线冲突和总线仲裁

假如在某I2C总线系统中存在两个主器件节点,分别记为主器件1和主器件2,其数据输出端分别为DATA1和DATA2,它们都有控制总线的能力,这就存在着发生总线冲突(即写冲突)的可能性。

假设在某一瞬间两者相继向总线发出了启动信号,鉴于:I2C总线的“线与”特性,使得在数据线SDA上得到的信号波形是DATA1和DATA2两者相与的结果,该结果略微超前送出低电平的主器件1,其DATA1的下降沿被当做SDA的下降沿。

在总线被启动后,主器件1企图发送数据“101……”,主器件2企图发送数据“100101……”。

两个主器件在每次发出一个数据位的同时都要对自己输出端的信号电平进行抽检,只要抽检的结果与它们自己预期的电平相符,就会继续占用总线,总线控制权也就得不到裁定结果。

主器件1的第3位期望发送“1”,也就是在第3个时钟周期内送出高电平。

在该时钟周期的高电平期间,主器件1进行例行抽检时,结果检测到一个不相匹配的电平“0”,这时主器件1只好决定放弃总线控制杈;因此,主器件2就成了总线的惟一主宰者,总线控制权也就最终得出了裁定结果,从而实现了总线仲裁的功能。

从以上总线仲裁的完成过程可以得出:仲裁过程主器件1和主器件2都不会丢失数据;各个主器件没有优先级别之分,总线控制权是随机裁定的,即使是抢先发送启动信号的主器件1最终也并没有得到控制杈。

系统实际上遵循的是“低电平优先”的仲裁原则,将总线判给在数据线上先发送低电平的主器件,而其他发送高电平的主器件将失去总线控制权,如图所示。

15.jpg


(10)总线封锁状态。

在特殊情况下,如果需要禁止所有发生在I2C总线上的通信活动,封锁或关闭总线是一种可行途径,只要挂接于该总线上的任意一个器件将时钟线SCL锁定在低电平上即可。



3、参考代码

子函数文件 iic.c

  1. #include "myi2c.h"

  2. void I2C_GPIO_Config(void)
  3. {
  4.   GPIO_InitTypeDef  GPIO_InitStructure;

  5.   GPIO_InitStructure.GPIO_Pin =  SCL_PIN;
  6.   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  7.   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
  8.   GPIO_Init(GPIOB, &GPIO_InitStructure);
  9.   GPIO_InitStructure.GPIO_Pin =  SDA_PIN;
  10.   GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  11.   GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
  12.   GPIO_Init(GPIOB, &GPIO_InitStructure);
  13. }

  14. void I2C_delay(void)
  15. {
  16.   
  17.    u8 i=10; //这里可以优化速度 ,经测试最低到5还能写入
  18.    while(i)
  19.    {
  20.      i--;
  21.    }
  22. }
  23. void delay5ms(void)
  24. {
  25.   
  26.    int i=5000;
  27.    while(i)
  28.    {
  29.      i--;
  30.    }
  31. }

  32. uint16_t I2C_Start(void)
  33. {
  34. SDA_H;
  35. SCL_H;
  36. I2C_delay();
  37. if(!SDA_read)return FALSE; //SDA线为低电平则总线忙,退出
  38. SDA_L;
  39. I2C_delay();
  40. if(SDA_read) return FALSE; //SDA线为高电平则总线出错,退出
  41. SDA_L;
  42. I2C_delay();
  43. return TRUE;
  44. }

  45. void I2C_Stop(void)
  46. {
  47. SCL_L;
  48. I2C_delay();
  49. SDA_L;
  50. I2C_delay();
  51. SCL_H;
  52. I2C_delay();
  53. SDA_H;
  54. I2C_delay();
  55. }

  56. void I2C_Ack(void)
  57. {
  58. SCL_L;
  59. I2C_delay();
  60. SDA_L;
  61. I2C_delay();
  62. SCL_H;
  63. I2C_delay();
  64. SCL_L;
  65. I2C_delay();
  66. }  

  67. void I2C_NoAck(void)
  68. {
  69. SCL_L;
  70. I2C_delay();
  71. SDA_H;
  72. I2C_delay();
  73. SCL_H;
  74. I2C_delay();
  75. SCL_L;
  76. I2C_delay();
  77. }

  78. uint16_t I2C_WaitAck(void)   //返回为:=1有ACK,=0无ACK
  79. {
  80. SCL_L;
  81. I2C_delay();
  82. SDA_H;   
  83. I2C_delay();
  84. SCL_H;
  85. I2C_delay();
  86. if(SDA_read)
  87. {
  88.       SCL_L;
  89.    I2C_delay();
  90.       return FALSE;
  91. }
  92. SCL_L;
  93. I2C_delay();
  94. return TRUE;
  95. }

  96. void I2C_SendByte(unsigned char SendByte) //数据从高位到低位//
  97. {
  98.     u8 i=8;
  99.     while(i--)
  100.     {
  101.         SCL_L;
  102.         I2C_delay();
  103.       if(SendByte&0x80)
  104.         SDA_H;
  105.       else
  106.         SDA_L;  
  107.         SendByte<<=1;
  108.         I2C_delay();
  109.   SCL_H;
  110.         I2C_delay();
  111.     }
  112.     SCL_L;
  113. }

  114. unsigned char I2C_RadeByte(void)  //数据从高位到低位//
  115. {
  116.     u8 i=8;
  117.     u8 ReceiveByte=0;
  118.     SDA_H;   
  119.     while(i--)
  120.     {
  121.       ReceiveByte<<=1;     
  122.       SCL_L;
  123.       I2C_delay();
  124.    SCL_H;
  125.       I2C_delay();
  126.       if(SDA_read)
  127.       {
  128.         ReceiveByte|=0x01;
  129.       }
  130.     }
  131.     SCL_L;
  132.     return ReceiveByte;
  133. }
  134. //ZRX         
  135. //单字节写入*******************************************
  136. uint16_t Single_Write(unsigned char SlaveAddress,unsigned char REG_Address,unsigned char REG_data)       //void
  137. {
  138.    if(!I2C_Start())return FALSE;
  139.     I2C_SendByte(SlaveAddress);   //发送设备地址+写信号//I2C_SendByte(((REG_Address & 0x0700) >>7) | SlaveAddress & 0xFFFE);//设置高起始地址+器件地址
  140.     if(!I2C_WaitAck()){I2C_Stop(); return FALSE;}
  141.     I2C_SendByte(REG_Address );   //设置低起始地址     
  142.     I2C_WaitAck();
  143.     I2C_SendByte(REG_data);
  144.     I2C_WaitAck();  
  145.     I2C_Stop();
  146.     delay5ms();
  147.     return TRUE;
  148. }
  149. //单字节读取*****************************************
  150. unsigned char Single_Read(unsigned char SlaveAddress,unsigned char REG_Address)
  151. {   unsigned char REG_data;      
  152. if(!I2C_Start())return FALSE;
  153.     I2C_SendByte(SlaveAddress); //I2C_SendByte(((REG_Address & 0x0700) >>7) | REG_Address & 0xFFFE);//设置高起始地址+器件地址
  154.     if(!I2C_WaitAck()){I2C_Stop();return FALSE;}
  155.     I2C_SendByte((u8) REG_Address);   //设置低起始地址     
  156.     I2C_WaitAck();
  157.     I2C_Start();
  158.     I2C_SendByte(SlaveAddress+1);
  159.     I2C_WaitAck();
  160. REG_data= I2C_RadeByte();
  161.     I2C_NoAck();
  162.     I2C_Stop();
  163.     //return TRUE;
  164. return REG_data;
  165. }
复制代码

头文件 iic.h
  1. #ifndef __MYI2C_H__
  2. #define __MYI2C_H__
  3. #include "stm32f10x.h"
  4. #define FALSE 0
  5. #define TRUE  1

  6. #define SCL_PIN GPIO_Pin_6
  7. #define SDA_PIN GPIO_Pin_7

  8. #define SCL_H         GPIOB->BSRR = SCL_PIN
  9. #define SCL_L         GPIOB->BRR  = SCL_PIN

  10. #define SDA_H         GPIOB->BSRR = SDA_PIN
  11. #define SDA_L         GPIOB->BRR  = SDA_PIN

  12. #define SCL_read      GPIOB->IDR  & SCL_PIN
  13. #define SDA_read      GPIOB->IDR  & SDA_PIN

  14. void I2C_GPIO_Config(void);
  15. void I2C_delay(void);
  16. void delay5ms(void);
  17. uint16_t I2C_Start(void);
  18. void I2C_Stop(void);
  19. void I2C_Ack(void);
  20. void I2C_NoAck(void);
  21. uint16_t I2C_WaitAck(void);
  22. void I2C_SendByte(unsigned char SendByte);
  23. unsigned char I2C_RadeByte(void);
  24. uint16_t Single_Write(unsigned char SlaveAddress,unsigned char REG_Address,unsigned char REG_data);
  25. unsigned char Single_Read(unsigned char SlaveAddress,unsigned char REG_Address);


  26. #endif // __MYI2C_H__
复制代码


评分

参与人数 3ST金币 +11 收起 理由
danshuizz + 1 很给力!
epochal + 8
MrJiu + 2

查看全部评分

回复

使用道具 举报

该用户从未签到

0

主题

2

帖子

0

蝴蝶豆

新手上路

最后登录
2017-9-5
发表于 2017-9-5 16:13:15 | 显示全部楼层
本帖最后由 huaxiuyiwei 于 2017-9-5 16:16 编辑

这样写才行
#define  I2C_Direction_Transmitter      ((uint8_t)0x00)

uint16_t Single_Write(unsigned char SlaveAddress,unsigned char REG_Address,unsigned char REG_data)       //void
{
   if(!I2C_Start())return FALSE;
    I2C_SendByte(SlaveAddress<<1|I2C_Direction_Transmitter);  
    if(!I2C_WaitAck()){I2C_Stop(); return FALSE;}
    I2C_SendByte(REG_Address );   //ÉèÖõÍÆðʼµØÖ·     
    I2C_WaitAck();
    I2C_SendByte(REG_data);
    I2C_WaitAck();  
    I2C_Stop();
    delay5ms();
    return TRUE;
}
#define  I2C_Direction_Receiver         ((uint8_t)0x01)
u8 Single_Read(unsigned char SlaveAddress,unsigned char REG_Address)
{   
        unsigned char REG_data;      
        if(!I2C_Start())
        return FALSE;
    I2C_SendByte(SlaveAddress<<1|I2C_Direction_Transmitter);
    if(!I2C_WaitAck())
     {
           I2C_Stop();
          return FALSE;
         }
    I2C_SendByte((u8) REG_Address);
    I2C_WaitAck();
    I2C_Start();
    I2C_SendByte(SlaveAddress<<1|I2C_Direction_Receiver);
    I2C_WaitAck();
    REG_data= I2C_RadeByte();
    I2C_NoAck();
    I2C_Stop();
    //return TRUE;
    return  REG_data;
}
回复 支持 1 反对 0

使用道具 举报

该用户从未签到

74

主题

1442

帖子

194

蝴蝶豆

版主

最后登录
2023-10-26
发表于 2017-6-20 16:47:35 | 显示全部楼层
有硬件IIC了,还用模拟方式?
回复 支持 反对

使用道具 举报

该用户从未签到

1

主题

456

帖子

2

蝴蝶豆

金牌会员

最后登录
2019-5-7
发表于 2017-6-20 18:53:55 | 显示全部楼层
用模拟方式非常好的1种,你用多了就很喜欢。
回复 支持 反对

使用道具 举报

该用户从未签到

64

主题

1138

帖子

140

蝴蝶豆

论坛元老

最后登录
2020-12-3
发表于 2017-6-20 22:22:50 | 显示全部楼层
samhong 发表于 2017-6-20 18:53
用模拟方式非常好的1种,你用多了就很喜欢。

还是模拟好,不受限制
回复 支持 反对

使用道具 举报

该用户从未签到

3

主题

1002

帖子

363

蝴蝶豆

版主

最后登录
2021-4-15
发表于 2017-6-21 09:34:52 | 显示全部楼层
我自己也写了一个模拟的,可以支持任意IO口,而且支持多个I2C通道
回复 支持 反对

使用道具 举报

该用户从未签到

6

主题

302

帖子

2

蝴蝶豆

高级会员

最后登录
2019-11-13
发表于 2017-6-21 21:57:34 | 显示全部楼层
回复

使用道具 举报

该用户从未签到

1

主题

469

帖子

0

蝴蝶豆

金牌会员

最后登录
2020-11-9
发表于 2017-6-21 23:26:07 | 显示全部楼层
谢谢楼主分享
回复 支持 反对

使用道具 举报

该用户从未签到

0

主题

2

帖子

0

蝴蝶豆

新手上路

最后登录
2017-9-5
发表于 2017-9-5 16:11:16 | 显示全部楼层
代码里有个错误
回复 支持 反对

使用道具 举报

该用户从未签到

0

主题

2290

帖子

3

蝴蝶豆

论坛元老

最后登录
2021-5-1
发表于 2017-9-5 21:12:17 | 显示全部楼层
回复

使用道具 举报

您需要登录后才可以回帖 注册/登录

本版积分规则

关闭

站长推荐上一条 /3 下一条

Archiver|手机版|小黑屋|论坛-意法半导体STM32/STM8技术社区

GMT+8, 2024-4-24 14:45 , Processed in 0.217230 second(s), 46 queries .

Powered by Discuz! X3.4

Copyright © 2001-2024, Tencent Cloud.

快速回复 返回顶部 返回列表