• 【STM32】入门(七):I2C硬件控制方式


    1、简介

    之所以叫“I2C硬件控制方式”是与“软件控制方式”相对。I2C软件控制,就是写程序直接操作两个GPIO引脚,分别作为时钟线SCL和数据线SDA,按照I2C协议的时序要求,操作GPIO输入、输出、高电平、低电平。
    听着就很复杂,好在STM32中有I2C的硬件实现,即通过简单的操作寄存器即可实现收发数据。

    2、手册

    2.1 寄存器功能框图

    在这里插入图片描述

    2.2 I2C引脚

    STM32F407ZGT6中有3个I2C总线,对应的引脚如下图所示。
    其中I2C1默认引脚是PB6、PB7,可以重映射到PB8、PB9上。
    在这里插入图片描述

    2.3 寄存器

    寄存器地址:
    在这里插入图片描述
    在这里插入图片描述

    3、代码详解

    I2C基本编程步骤:初始化时钟、配置引脚、起始信号、读、写、终止信号

    3.1 I2C初始化

    3.1.1 初始化时钟

    I2C在APB1总线上,并且I2C1的引脚位PB6、PB7,因此使能使能GPIOB时钟,以及I2C的时钟

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); 
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); 
    
    • 1
    • 2

    在这里插入图片描述

    3.1.2 配置引脚位开漏输出

    static void I2C_GPIO_Config(void)
    {
      GPIO_InitTypeDef  GPIO_InitStructure; 
    
      #a)使能与 I2C 有关的时钟
      RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); 
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
        
      #b)配置SCL和SDA引脚位开漏输出GPIO_Mode_AF_OD
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
      GPIO_Init(GPIOB, &GPIO_InitStructure);
    	
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
      GPIO_Init(GPIOB, &GPIO_InitStructure);	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3.1.3 设置I2C工作模式

    设置工作模式、占空比、地址7位或10位、通信速率等

    工作模式有三种:I2C、SMBusDevice、SMBusHost。

    SMBus (System Management Bus,系统管理总线)和I2C类似,但是在时序特性上有一些差异:

    首先,SMBus需要一定数据保持时间,而 I2C总线则是从内部延长数据保持时间。
    SMBus具有超时功能,因此当SCL太低而超过35 ms时,从器件将复位正在进行的通信。相反,I2C采用硬件复位。
    SMBus具有一种警报响应地址(ARA),因此当从器件产生一个中断时,
    	它不会马上清除中断,而是一直保持到其收到一个由主器件发送的含有其地址的ARA为止。
    SMBus只工作在从10kHz到最高100kHz。最低工作频率10kHz是由SMBus超时功能决定的。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    static void I2C_Mode_Configu(void)
    {
      I2C_InitTypeDef  I2C_InitStructure; 
    
      #a)设置位I2C模式
      I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    	
      #b)设置高低电平占空比,这个不用纠结,可以随意设置,一般设备兼容性都没问题
      I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    
      #c)设置自己的地址为7
      I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7; 
      
      #d)默认使能ACK,当需要发送NACK时,再重新设置
      I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
    	 
      #e)设置I2C的寻址地址为7
      I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    	
      #f)设置时钟为:400000
      I2C_InitStructure.I2C_ClockSpeed = 400000;
      
      #g)初始化I2C1:(APB1PERIPH_BASE + 0x5400)
      I2C_Init(I2C1, &I2C_InitStructure);
      
      #h)使能 I2C1
      I2C_Cmd(I2C1, ENABLE);   
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    3.2 写操作

    在这里插入图片描述

    uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr) 
    {
      #a)发送起始信号“S”
      I2C_GenerateSTART(I2C1, ENABLE);
    
      I2CTimeout = I2CT_FLAG_TIMEOUT;  
      #b)等待起始信号发送成功:测试 EV5 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))  
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
      } 
      
      I2CTimeout = I2CT_FLAG_TIMEOUT;
      #c)发送从地址
      I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
      
      #d)等待从地址发送成功:测试 EV6 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
      }  
      #e)发送需要写入EEPROM的地址(本质也是数据)
      I2C_SendData(I2C1, WriteAddr);
      
      I2CTimeout = I2CT_FLAG_TIMEOUT;
      #f)等待数据发送成功:测试 EV8 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
      } 
      
      #g)发送需要写入的数据
      I2C_SendData(I2C1, *pBuffer); 
      
      I2CTimeout = I2CT_FLAG_TIMEOUT;  
      #h)等待数据发送成功:测试 EV8 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
      } 
      
      #i)发送停止信号“P”
      I2C_GenerateSTOP(I2C1, ENABLE);
      
      return 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    3.3 读操作

    在这里插入图片描述

    uint32_t I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)
    {  
      I2CTimeout = I2CT_LONG_TIMEOUT;
      
      #a)等待是否可以读
      //*((u8 *)0x4001080c) |=0x80; 
      while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
      }
      
      #b)发送起始信号“S”
      I2C_GenerateSTART(I2C1, ENABLE);
      //*((u8 *)0x4001080c) &=~0x80;
      
      I2CTimeout = I2CT_FLAG_TIMEOUT;
      #c)等待起始信号发送成功:测试 EV5 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
      }
    
      #d)发送从地址
      I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
    
      I2CTimeout = I2CT_FLAG_TIMEOUT;
      #e)等待从地址发送成功:测试 EV6 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
      }
        
      #f)EV6后执行EV6_1:清除EV6 (再次设置PE位)
      I2C_Cmd(I2C1, ENABLE);
    
      #g)发送需要读取的地址
      I2C_SendData(I2C1, ReadAddr);  
    
       
      I2CTimeout = I2CT_FLAG_TIMEOUT;
      #h)等待数据发送成功:测试 EV8 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);
      }
        
      #i)复合命令:再次发送起始信号“S”
      I2C_GenerateSTART(I2C1, ENABLE);
      
      I2CTimeout = I2CT_FLAG_TIMEOUT;
      #j)等待起始信号发送成功:测试 EV5 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);
      }
        
      #k)发送从地址
      I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);
      
      I2CTimeout = I2CT_FLAG_TIMEOUT;
      #l)等待从地址发送成功:测试 EV6 即可
      while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);
      }
      
      #m)循环读
      while(NumByteToRead)  
      {
        if(NumByteToRead == 1)
        {
          
          #n)读取完毕,发送NACK
          I2C_AcknowledgeConfig(I2C1, DISABLE);
          
          #o)发送停止信号“P”
          I2C_GenerateSTOP(I2C1, ENABLE);
        }
    
        #p)每次读取一个字节前,先测试 EV7    
        I2CTimeout = I2CT_LONG_TIMEOUT;
        while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)  
    	{
    		if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
    	} 
        {      
          #q)读取一字节
          *pBuffer = I2C_ReceiveData(I2C1);
          pBuffer++; 
          NumByteToRead--;        
        }   
      }
    
      #r)为下一次读取做准备,即将ACK设置为1
      I2C_AcknowledgeConfig(I2C1, ENABLE);
      
      return 1;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99

    3.4 待机状态

    向EEPROM写入数据后,调用这个函数等待EEPROM 内部擦写完毕。

    这个函数主要实现是向EEPROM 发送它设备地址,检测EEPROM 的响应,若EEPROM 接收到地址后返回应答信号,则表示EEPROM 已经准备好,可以开始下一次通讯。函数中检测响应是通过读取STM32 的SR1 寄存器的ADDR 位及AF 位来实现的,当I2C 设备响应了地址的时候,ADDR 会置1,若应答失败,AF 位会置1。

    void I2C_EE_WaitEepromStandbyState(void)      
    {
      vu16 SR1_Tmp = 0;
    
      do
      {
        #a)发送起始信号“S”
        I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
        #b)读取I2C1 SR1 寄存器
        SR1_Tmp = I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1);
        #c)发送从地址
        I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
      }while(!(I2C_ReadRegister(EEPROM_I2Cx, I2C_Register_SR1) & 0x0002));
      
      #d)清除AF信号
      I2C_ClearFlag(EEPROM_I2Cx, I2C_FLAG_AF);
      #e)发送停止信号“P”    
      I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    c++基础:指针
    我的创作纪念日
    【论文分享】异质图上的小样本学习:HG-Meta: Graph Meta-learning over Heterogeneous Graphs
    论文笔记: 多标签学习 DM2L
    git命令 本地
    第二课 我的第一个程序 hello world
    LeetCode 1282. 用户分组
    C#学习系列之装箱、拆箱、自定义转化、重载运算符
    几何光学的基本原理
    Java集合大总结——Set的简单使用
  • 原文地址:https://blog.csdn.net/u010168781/article/details/126257339