14.深入浅出 I2C 通信协议:从电平信号到完整驱动实现

I2C协议详解(图文并茂+超详细) I²C 叫集成电路总线 它是一种串行通信接口,具有双向两线同步串行总线,通常由两根线组成——SDA(串行数据线)和SCL(串行时钟线)和上拉电阻。 阅读详情

摘要:本文从电平信号与差分信号的区别讲起,系统梳理 I2C 通信协议的核心知识点,包括硬件接线、上拉电阻作用、起始/终止信号、数据传输时序、应答机制与寻址方式。随后以 STM32 为例,从零搭建软件 IIC 驱动,逐步实现微秒延时、起始信号、8bit 发送、应答检测及单字节/多字节读写函数,并对比硬件 IIC 的 HAL 库用法。最后结合 ENS160 多气体传感器,演示如何将 I2C 驱动落地到真实项目,帮助读者快速掌握从原理到实战的完整链路。

一、差分和电平信号

1.1简介

1. 电平信号:电平信号有一根是参考电平线,信号线的信号值由信号线和参考电平线之间的电压差决定。

2. 差分信号:差分信号没有参考电平线,传输一路数据需要两根信号线,信号线的信号值由两根信号线的电压差决定。

区别:

(1) 信号值的表示不同:电平信号的信号值是信号线和参考电平线的电压差决定;差分信号的信息值是由两条信号线的电压差决定。

(2) 传输相同路数的信号,电平信号用的数据线根数比差分信号少。比如传输 3 路信号,电平信号需要 1 根参考电平 + 3 根数据线;而差分信号需要 2x3=6 根数据线。

(3) 现在通信大部分是差分信号,因为差分信号的抗干扰性更强,可以在更短的周期传输一个数据。

1.2两种逻辑电平标准

1.2.1TTL

TTL电平:逻辑1: 2.4V--5V 逻辑0: 0V--0.5V

TTL电平下的串口协议

假设发送的数据为:01010101(由低位向高位发送)

  1. RS232

电平标准:+3V~+15V对应0,

-3V~-15V对应1

RS232电平下的串口协议

假设发送的数据为:01010101

需要注意的是:

rs232 的逻辑电平和TTL 不一样但是协议一样。

1.3差分信号-RS485

RS-485仅是一个电气标准,描述了接口的物理层,像协议、时序、串行或并行数据以及链路全部由设计者或更高层协议定义。 RS-485定义的是使用平衡多点传输线的驱动器和接收器的电气特性。

RS-485能够进行远距离传输主要得益于使用差分信号进行传输,当有噪声干扰时仍可以使用线路上两者差值进行判断,使传输数据不受噪声干扰。

半双工、是电气协议(逻辑1:+2V–+6V 逻辑0: -6V— -2V)是二线制差分信号,也就是实际传输的数据是通过判断这两条信号线上的电压差来实现的,RS-485总线弥补了RS-232通信距离短,速率低的缺点,RS-485的速率可高达10Mbit/s,理论通讯距离可达1200米;RS-485和RS-232的单端传输不一样,是差分传输,使用一对双绞线

共模抑制

二、通信学习方式

1 了解电路接线

2 了解通信协议

3 使用代码实现该协议

4 硬件通信练习

三、硬件接线

同步串行半双工

半双工通信,输出口都必须是开漏,如果是推挽则容易在通信双方一个发1一个发0是产生短路从而烧毁引脚

开漏输出本身无法实现高电平输出,为了能够实现高电平输出,此时需要外接上拉电阻

四、上拉电阻的作用

4.1避免总线冲突和实现“线与”(Wire-AND)功能

这是最核心的原因。I²C协议支持多主多从(Multiple Masters and Slaves)的架构。

  • 开漏输出:意味着芯片内部的输出级电路只有一个连接到地的MOSFET(像是一个开关),而没有直接连接到电源(VCC)的上拉部分。

    • 当MOSFET关闭(开关断开)时,输出引脚为高阻态,不影响总线。

    • 当MOSFET开启(开关闭合)时,输出引脚被强行拉低到地(GND),输出逻辑'0'。

  • “线与”逻辑:由于所有设备的总线(SDA和SCL)都是开漏结构并通过上拉电阻连接到VCC,任何一个设备只要主动拉低总线(输出'0'),整条总线就是低电平。

    • 逻辑“与”关系:总线的最终状态 = 设备1的输出 AND 设备2的输出 AND ... 设备N的输出。只有所有设备都输出'1'(即释放总线,不拉低),总线才为高电平'1'。

  • 如何避免冲突:这种“线与”特性使得总线天生就具备冲突检测(Arbitration) 机制。

    • 假设两个主设备同时开始发送数据。它们一边发送,一边会读取总线上的实际电平,并与自己发送的电平进行比较。

    • 如果某个主设备发送了一个高电平'1'(释放总线),但它检测到总线实际是低电平'0'(被另一个主设备拉低了),它就立刻知道自己失去了仲裁,会立即停止操作,转为从接收模式。

    • 这个过程不会损坏硬件,也无需额外的冲突检测线路。这是推挽输出(Push-Pull)绝对无法实现的,如果两个推挽输出的设备一个输出高一个输出低,会造成电源对地短路,烧毁器件。

4.2电平转换的灵活性(兼容不同电压的设备)

这是另一个巨大的优势。由于总线上的高电平电压完全由上拉电阻所连接的电源(VCC) 决定,而与各个设备自身的工作电压(VDD)无关。

  • 场景:一个3.3V的STM32(主设备)想要和一个5V的传感器(从设备)通信。

  • 实现:

    • 将I²C总线的上拉电阻连接到5V电源。

    • 3.3V的STM32的I²C引脚设置为开漏模式,并且其耐压需要能承受5V(很多现代MCU的IO口是5V tolerant的)。

  • 工作原理:

    • 当STM32要输出'0'时,它内部MOSFET导通,将总线拉低到GND(0V)。对于5V的传感器来说,0V显然是逻辑'0'。

    • 当STM32要输出'1'时,它释放总线(高阻态),上拉电阻将总线拉高到5V。STM32会读取到这个5V的高电平,虽然超过了其本身的3.3V供电,但只要引脚耐压足够就没有问题。同时,5V的传感器也正确地看到了5V的逻辑'1'。

  • 好处:你可以在一条I²C总线上混用不同工作电压的芯片,而不需要任何额外的电平转换电路。只需将上拉电阻接到一个所有设备都能接受的最高电压即可。

4.3简化驱动电路

开漏结构非常简单,只需要一个MOSFET管来控制对地的导通,而不需要更复杂的推挽电路(既有上拉P-MOS,又有下拉N-MOS)。这使得IC内部的电路更简单,成本更低。

五、协议时序分析

5.1起始信号与终止信号

  • SCL线为高电平期间,SDA线由高电平向低电平的变化表示起始信号;

  • SCL线为高电平期间,SDA线由低电平向高电平的变化表示终止信号;

  • 起始和终止信号都是由主机发出,起始信号产生后,总线就处于占用的态;

  • 终止信号产生后,总线就处于空闲态。

5.2数据传输时序

  • I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定; 只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。

  • 在时钟为低电平期间,发送器向数据线上写入数据,因此数据线上的数据运行变化; 在时钟为高电平期间,接收器从数据线上读取数据,因此必须保持数据线上的数据稳定。

  • 最终完成一个时钟周期内,发送器发生一个bit位数据,接收器接收一个bit位数据。

5.3字节传输和应答信号 8+1

  • 每一个字节必须保证是8位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。

  • 在第9个时钟周期的低电平期间,接收器向数据线上写入数据。

  • 在第9个时钟周期的高电平期间,发送器从数据线上读取数据。

  • 如果读到的是低电平信号,表示应答信号。

  • 如果读到的是高电平信号,表示非应答信号。

5.4 I2C寻址

  • I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。

  • 主机在起始信号后必须传送一个从机的地址(7位),第8位是数据的传送方向位(R/W),用“0”表示主机发送数据(W),“1”表示主机接收数据(R)。 “w0我”

  • 总线上的每个从机都将这7位地址码与自己的地址进行比较,如果相同,则认为自己被主机寻址,根据R/W位将自己定为发送器或接收器。

//IIC地址的表示方式一般使用两位16进制表示

//但是IIC地址只有7位,两位16进制表示了8位二进制

//此时会有两种分配方式

// 假设IIC地址为1100 111

//1从最低位补零的方式 1100 1110=0xCE 此种方式写的话地址可以直接用,而读时需要或等于0x01

//2从最高位补零 0110 0111=0x67 此种方式发送时需要先左移1位然后根据都或者写操作进行或等于或者与等于

5.4.1 主机给从机发送一个字节

    5.4.2 主机给从机发送多个字节

      5.4.3 主机从从机接收一个字节

        5.4.4 主机从从机接收多个字节

          六、IIC

          6.1 软件IIC定位引脚

            6.1.1 定位引脚

              6.1.2 配置引脚为开漏,默认高阻态

                6.1.3 给引脚添加标签

                  6.2 驱动框架搭建

                    6.2.1 添加驱动文件,进行编写

                      6.3 编写.h的框架

                        #ifndef __MY_IIC__
                        #define __MY_IIC__
                        
                        
                        
                        
                        #endif

                        6.3.1 编写.h文件的宏定义

                        对时钟、数据线函数、引脚进行宏定义,方便后续引脚变化时调整代码,也方便使用者阅读

                          #ifndef __MY_IIC__
                          #define __MY_IIC__
                          
                          #include "main.h"
                          //声明函数
                          #define IIC_SCL_PORT GPIOA
                          #define IIC_SCL_PIN  GPIO_PIN_8
                          #define IIC_SDA_PORT GPIOC
                          #define IIC_SDA_PIN  GPIO_PIN_9
                          
                          #define IIC_SCL_1() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,1)
                          #define IIC_SCL_0() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,0)
                          
                          #define IIC_SDA_1() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,1)
                          #define IIC_SDA_0() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,0)
                          
                          #endif

                          6.4 编写.c 文件编写发送单个字节的完整时序函数

                          #include "myiic.h"
                          
                          /*
                                  0 初始化IIC
                                  1 写单个字节
                                  2 写多个字节
                                  3 读单个字节
                                  4 读多个字节        
                          */
                          
                          /* 
                                  单片机发送单个字节给从机
                                  param : 从机地址+W  寄存器地址  发送的内容
                          */
                          void Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data)
                          {
                                  //1 起始标识
                                  
                                  //2 发送从机地址+写
                                  //3 接收应答
                                  //4 发送寄存器地址
                                  //5 接收应答
                                  //6 发送8为数据
                                  //7 接收应答
                                  //8 发送停止标识
                          }

                          6.5 为了提高IIC的通信速度,需要用到微秒级延时,创建微秒级延时的驱动代码

                          mydelay.c

                          #include "mydelay.h"
                          //使用滴答定时器实现微秒级延时函数
                          //编写思路:初始值与当前计数值相减,
                          //数值大于等于我们设定的延时值,就跳出循环
                          void Delay_us(uint32_t us)
                          {
                            //1.定义变量来存储时间
                            uint32_t ticks;//需要的滴答数
                            uint32_t t_old,t_now,t_cnt = 0;//之前的时间、现在的时间、计数值
                            uint32_t reload = SysTick->LOAD;//将重载值放在变量reload里
                            
                            //2.计算需要的滴答数
                            ticks = us * 100; 
                            t_old = SysTick->VAL;//将最开始的值赋给t_old
                            
                            //3.比较计时时间与设定时间大小
                            while(1)
                            {
                              t_now = SysTick->VAL;//获取当前计数值
                              if(t_now != t_old)
                              {
                                
                                if(t_now < t_old)
                                {
                                  t_cnt += t_old - t_now;//将两者差值累加
                                }
                                else//VAL减到零重置时
                                {
                                  t_cnt += reload-t_now+t_old;//把重新装载之后减得数进行累加
                                }
                                t_old = t_now;//更新原来的t_old值
                                if(t_cnt >= ticks)//计时时间大于设定时间,则跳出while
                                {
                                  break;
                                }
                              }
                            }
                          }
                          
                          void Delay_ms(uint32_t ms)
                          {
                                          for(int i =0;i<ms;i++)
                                                  Delay_us(1000);
                          }

                          mydelay.h

                          #ifndef __MY_Delay__
                          #define __MY_Delay__
                          #include "main.h"
                          //高精延时
                          void Delay_us(uint32_t us);
                          void Delay_ms(uint32_t ms);
                          #endif
                          
                          

                          6.6 添加延迟

                          6.6.1 对延时函数添加宏定义

                            6.7 因为起始信号会被多次复用,所以封装成函数更易理解和模块化,编写起始函数

                            void Myiic_Start(void)
                            {                                
                                    IIC_SCL_0();        
                                    IIC_SDA_1();
                                    IIC_Delay();
                                    IIC_SCL_1();
                                    IIC_SDA_0();
                                    IIC_Delay();  
                            }
                            

                            6.8 封装一个发送8bit数据的函数

                              //发送8bit
                              void Myiic_8Bit(uint8_t send_data)
                              {
                                      for(int i= 0; i<8;i++)
                                      {
                                              /* 时钟线拉低 发送最高位 然后拉高 等待从机读取 再拉低 发送次高位*/
                                              IIC_SCL_0();
                                              if(send_data & 0x80)
                                              {
                                                              IIC_SDA_1();
                                              }else{
                                                              IIC_SDA_0();
                                              }
                                              IIC_Delay();        
                                              //拉高时钟线,从机可以读取数据了
                                              IIC_SCL_1();
                                              IIC_Delay();//让从机有充分的时间来读取        
                                              send_data <<= 1;                
                                      }
                              }
                              

                              6.9 以此类推可完成写单个函数

                                
                                
                                #include "myiic.h"
                                
                                
                                void Myiic_Start(void)
                                {
                                                        //防止发送起始标识之前,SDA处于低电平,需要提前拉高
                                                        IIC_SCL_0();
                                                        IIC_SDA_1();
                                                        IIC_Delay();
                                                        IIC_SCL_1();
                                                        IIC_SDA_0();//时钟线高电平时拉低数据线
                                                        IIC_Delay();        
                                }
                                
                                void Myiic_Stop(void)
                                {
                                                        IIC_SCL_0();
                                                        IIC_SDA_0();
                                                        IIC_Delay();
                                                        IIC_SCL_1();
                                                        IIC_SDA_1();
                                                        IIC_Delay();
                                }
                                void Myiic_8bit(uint8_t data)
                                {
                                        //拉低时钟线准备发送
                                                for(int i = 0; i<8;i++)
                                        {
                                                        IIC_SCL_0();
                                                        //p7 p6 p5 p4 p3 p2 p1 p0
                                                        // 1 0  0  0  0  0  0  0
                                                        if(data &0x80)//0000 000p7
                                                                IIC_SDA_1();
                                                        else
                                                                IIC_SDA_0();
                                                        IIC_Delay();
                                                        //拉高时钟线 从机可以读取数据
                                                        IIC_SCL_1();
                                                        IIC_Delay();//让高电平保持一段时间,给从机留时间读
                                                        data <<=1;
                                                }        
                                }
                                
                                int Myiic_WaitAck(void)
                                {
                                        //拉低时钟线 让从机发送应答
                                        IIC_SCL_0();
                                        //松手 
                                        IIC_SDA_1();
                                        //延迟
                                        IIC_Delay();
                                        //拉高时钟线
                                        IIC_SCL_1();
                                        //读信号线
                                        //根据读到的高低电平判断有无应答
                                        if(IIC_SDA_READ() == 1)
                                        {        
                                                        //无应答
                                                  //发送停止结束通信
                                                        IIC_SCL_0();
                                                        IIC_SDA_0();
                                                        IIC_Delay();
                                                        IIC_SCL_1();
                                                        IIC_SDA_1();
                                                        IIC_Delay();
                                                        return NACK;
                                        }else{
                                                        return ACK;
                                        }
                                }
                                /*
                                        发送单个字节到从机
                                param:
                                iic_addr 最低位为0 默认写的IIC地址
                                reg_addr 要写的寄存器地址
                                send_data 要发送的数据
                                return  0应答  1非应答
                                */
                                int Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data)
                                {
                                                //1 起始标识
                                                Myiic_Start();
                                                //2 发送8位数---从机地址+写
                                                Myiic_8bit(iic_addr);
                                          //3 接收应答
                                                if(Myiic_WaitAck() == NACK)
                                                {
                                                                return NACK;
                                                }
                                          //4 发送8位数---寄存器地址
                                                Myiic_8bit(reg_addr);
                                          //5 接收应答
                                                if(Myiic_WaitAck() == NACK)
                                                {
                                                                return NACK;
                                                }
                                          //6 发送8位数---要写的数据
                                                Myiic_8bit(send_data);
                                          //7 接收应答        
                                                if(Myiic_WaitAck() == NACK)
                                                {
                                                                return NACK;
                                                }
                                          //8 发送停止标识
                                                Myiic_Stop();
                                }

                                
                                
                                #ifndef __MY_IIC__
                                #define __MY_IIC__
                                
                                #include "main.h"
                                #include "mydelay.h"
                                #define IIC_Delay() Delay_us(2)
                                
                                #define IIC_SCL_PORT GPIOA
                                #define IIC_SCL_PIN  GPIO_PIN_8
                                #define IIC_SDA_PORT GPIOC
                                #define IIC_SDA_PIN  GPIO_PIN_9
                                
                                enum{
                                        ACK=0,
                                        NACK
                                };
                                
                                #define IIC_SCL_1() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,1)
                                #define IIC_SCL_0() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,0)
                                
                                #define IIC_SDA_1() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,1)
                                #define IIC_SDA_0() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,0)
                                
                                #define  IIC_SDA_READ()         HAL_GPIO_ReadPin(IIC_SDA_PORT,IIC_SDA_PIN)
                                #endif

                                6.10 完整驱动

                                  
                                  
                                  #include "myiic.h"
                                  
                                  
                                  void Myiic_Start(void)
                                  {
                                                          //防止发送起始标识之前,SDA处于低电平,需要提前拉高
                                                          IIC_SCL_0();
                                                          IIC_SDA_1();
                                                          IIC_Delay();
                                                          IIC_SCL_1();
                                                          IIC_SDA_0();//时钟线高电平时拉低数据线
                                                          IIC_Delay();        
                                  }
                                  
                                  void Myiic_Stop(void)
                                  {
                                                          IIC_SCL_0();
                                                          IIC_SDA_0();
                                                          IIC_Delay();
                                                          IIC_SCL_1();
                                                          IIC_SDA_1();
                                                          IIC_Delay();
                                  }
                                  void Myiic_8bit(uint8_t data)
                                  {
                                          //拉低时钟线准备发送
                                                  for(int i = 0; i<8;i++)
                                          {
                                                          IIC_SCL_0();
                                                          //p7 p6 p5 p4 p3 p2 p1 p0
                                                          // 1 0  0  0  0  0  0  0
                                                          if(data &0x80)//0000 000p7
                                                                  IIC_SDA_1();
                                                          else
                                                                  IIC_SDA_0();
                                                          IIC_Delay();
                                                          //拉高时钟线 从机可以读取数据
                                                          IIC_SCL_1();
                                                          IIC_Delay();//让高电平保持一段时间,给从机留时间读
                                                          data <<=1;
                                                  }        
                                  }
                                  
                                  int Myiic_WaitAck(void)
                                  {
                                          //拉低时钟线 让从机发送应答
                                          IIC_SCL_0();
                                          //松手 
                                          IIC_SDA_1();
                                          //延迟
                                          IIC_Delay();
                                          //拉高时钟线
                                          IIC_SCL_1();
                                          //读信号线
                                          //根据读到的高低电平判断有无应答
                                          if(IIC_SDA_READ() == 1)
                                          {        
                                                          //无应答
                                                    //发送停止结束通信
                                                          IIC_SCL_0();
                                                          IIC_SDA_0();
                                                          IIC_Delay();
                                                          IIC_SCL_1();
                                                          IIC_SDA_1();
                                                          IIC_Delay();
                                                          return NACK;
                                          }else{
                                                          return ACK;
                                          }
                                  }
                                  void Myiic_NACK(void)
                                  {
                                                  //拉低时钟线
                                                  IIC_SCL_0();
                                                  //松手
                                                  IIC_SDA_1();
                                                  //延迟
                                                  IIC_Delay();
                                                  //拉高时钟线
                                                  IIC_SCL_1();
                                                  //延迟 通知从机读取信号线
                                                  IIC_Delay();        
                                  }
                                  
                                  
                                  void Myiic_ACK(void)
                                  {
                                                  //拉低时钟线
                                                  IIC_SCL_0();
                                                  //拉低数据线
                                                  IIC_SDA_0();
                                                  //延迟
                                                  IIC_Delay();
                                                  //拉高时钟线
                                                  IIC_SCL_1();
                                                  //延迟 通知从机读取信号线
                                                  IIC_Delay();        
                                                  IIC_SCL_0();
                                                  IIC_SDA_1();
                                  }
                                  
                                  
                                  void Myiic_RECV8Bit(uint8_t *data)
                                  {
                                                  int recv_data=0;
                                                  //接收8位数据
                                                  for(int i=0;i<8;i++)
                                                  {
                                                          //拉低时钟线 延迟 让对方来发送
                                                          IIC_SCL_0();
                                                    IIC_Delay();
                                                          //拉高时钟线
                                                          IIC_SCL_1();
                                                          //读取当前线的电平,赋值recv_data第8位
                                                          recv_data <<= 1;
                                                          recv_data |= IIC_SDA_READ();                
                                                    //延迟
                                                          IIC_Delay();
                                                  }
                                                  *data = recv_data;
                                  }
                                  
                                  /*
                                          发送单个字节到从机
                                  param:
                                  iic_addr 最低位为0 默认写的IIC地址
                                  reg_addr 要写的寄存器地址
                                  send_data 要发送的数据
                                  return  0应答  1非应答
                                  */
                                  int Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data)
                                  {
                                                  //1 起始标识
                                                  Myiic_Start();
                                                  //2 发送8位数---从机地址+写
                                                  Myiic_8bit(iic_addr);
                                            //3 接收应答
                                                  if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }
                                            //4 发送8位数---寄存器地址
                                                  Myiic_8bit(reg_addr);
                                            //5 接收应答
                                                  if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }
                                            //6 发送8位数---要写的数据
                                                  Myiic_8bit(send_data);
                                            //7 接收应答        
                                                  if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }
                                            //8 发送停止标识
                                                  Myiic_Stop();
                                  }
                                  
                                  uint8_t Myiic_SendByte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *send_data,uint8_t send_len)
                                  {
                                                  //起始标识
                                                  Myiic_Start();
                                                  Myiic_8bit(iic_addr);
                                                  if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }         
                                                  Myiic_8bit(reg_addr);                
                                                  if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }         
                                                  for(int i=0;i<send_len;i++)
                                                  {
                                                          Myiic_8bit(send_data[i]);                
                                                          if(Myiic_WaitAck() == NACK)
                                                          {
                                                                          return NACK;
                                                          }                        
                                                  }
                                                  Myiic_Stop();
                                  }
                                  
                                  uint8_t Myiic_Read_1Byte(uint8_t iic_addr,uint8_t reg_addr,uint8_t *recv_data)
                                  {
                                          //起始标识
                                          Myiic_Start();
                                          //发送从机地址+写
                                          Myiic_8bit(iic_addr);
                                          if(Myiic_WaitAck() == NACK)
                                          {
                                                          return NACK;
                                          }        
                                                  Myiic_8bit(reg_addr);                
                                                  if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }                 
                                                  //重新发送起始
                                                  Myiic_Start();
                                                  Myiic_8bit(iic_addr | 0x01);
                                                  if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }        
                                                  Myiic_RECV8Bit(recv_data);
                                                  //发送非应答
                                                  Myiic_NACK();
                                                  //发送结束信号
                                                  Myiic_Stop();
                                  }
                                          
                                  /* 
                                                  iic主机接收多字节函数
                                     param : 从机地址+W  寄存器地址  接收的变量地址  接收长度
                                  */
                                  uint8_t Myiic_Read_Byte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *data,uint8_t recv_len)
                                  {
                                                  //1起始标识
                                                  Myiic_Start();
                                          //2 发送从机地址+写
                                          Myiic_8bit(iic_addr);
                                           //3等待应答
                                                          if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }        
                                          //4 发送寄存器地址
                                                          Myiic_8bit(reg_addr);
                                          //5等待应答
                                                          if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }                        
                                          //6发送起始标识
                                          Myiic_Start();
                                          //7发送从机地址+读
                                                          Myiic_8bit(iic_addr | 0X01);
                                          //8等待应答
                                                          if(Myiic_WaitAck() == NACK)
                                                  {
                                                                  return NACK;
                                                  }                        
                                          //9接收第1个字节,连读8位
                                          //10发送应答
                                          //。。重复9,10,当接收达到recv_len时,发送非应答
                                                  for(int i = 0;i<recv_len;i++)
                                                  {
                                                          Myiic_RECV8Bit(data+i);
                                                          if(i != recv_len-1)
                                                          {
                                                                          Myiic_ACK();
                                                          }
                                                  }
                                                  Myiic_NACK();
                                          //发送停止标识
                                                  Myiic_Stop();
                                  }
                                  
                                  
                                  
                                  

                                  
                                  
                                  #ifndef __MY_IIC__
                                  #define __MY_IIC__
                                  
                                  #include "main.h"
                                  #include "mydelay.h"
                                  #define IIC_Delay() Delay_us(2)
                                  
                                  #define IIC_SCL_PORT GPIOA
                                  #define IIC_SCL_PIN  GPIO_PIN_8
                                  #define IIC_SDA_PORT GPIOC
                                  #define IIC_SDA_PIN  GPIO_PIN_9
                                  
                                  enum{
                                          ACK=0,
                                          NACK
                                  };
                                  
                                  #define IIC_SCL_1() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,1)
                                  #define IIC_SCL_0() HAL_GPIO_WritePin(IIC_SCL_PORT,IIC_SCL_PIN,0)
                                  
                                  #define IIC_SDA_1() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,1)
                                  #define IIC_SDA_0() HAL_GPIO_WritePin(IIC_SDA_PORT,IIC_SDA_PIN,0)
                                  
                                  #define  IIC_SDA_READ()         HAL_GPIO_ReadPin(IIC_SDA_PORT,IIC_SDA_PIN)
                                  
                                  int Myiic_SendByte(uint8_t iic_addr,uint8_t reg_addr,uint8_t send_data);
                                  uint8_t Myiic_SendByte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *send_data,uint8_t send_len);
                                  uint8_t Myiic_Read_1Byte(uint8_t iic_addr,uint8_t reg_addr,uint8_t *recv_data);
                                  uint8_t Myiic_Read_Byte_len(uint8_t iic_addr,uint8_t reg_addr,uint8_t *data,uint8_t recv_len);
                                  #endif

                                  七、 硬件IIC函数

                                    
                                    
                                    HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

                                    hi2c: I2C 句柄指针(比如 &hi2c1)。

                                    DevAddress: 目标从设备的地址。

                                    • STM32 的 HAL 库要求这里传入的是 包含读写位的 8 位地址,底层的代码会自动帮你把最低位变成 0(表示写操作)。

                                    pData: 要发送的数据缓冲区的指针。

                                    Size: 要发送的数据字节数。

                                    Timeout: 超时时间(毫秒)。

                                    HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);

                                    hi2c: 同上,I2C 句柄指针。

                                    DevAddress: 目标从设备的地址。同样需要把 7 位地址左移 1 位 (DevAddress << 1)。底层代码会自动帮你把最低位变成 1(表示读操作)。

                                    pData: 用来存放接收到的数据的缓冲区指针。

                                    Size: 你期望读取的数据字节数。

                                    Timeout: 超时时间(毫秒)。

                                    八、总结特点

                                      软件IIC :

                                      通过CPU直接控制GPIO引脚去生成IIC时序

                                      优点:引脚配置更加的灵活 ,使用起来更加稳定可控

                                      缺点:需要自己写时序函数,速度上限不如硬件IIC

                                      硬件IIC:使用硬件IIC外设实现,CPU只需要将要发送的数据传给IIC外设,时序由IIC外设借助GPIO引脚自动生成

                                      优点是速度上限高,使用简单方便

                                      缺点:引脚太固定了,IIC外设容易死机

                                      IIC通信协议 I2C由时钟线(SCL)和数据线(SDA)两根线构成通信线路,总线空闲时通过上拉电阻拉高电平。I2C是一种主从结构(Master/Slave)总线,总线上每个设备都可以作为主机或从机,主设备通常是CPU,用来产生传输的时钟信号,并初始化总线的数据传输,而从设备只能被动响应主机请求。因为一个I2C总线可以接多个从设备,这就需要主机通过地址来确定与哪个从机进行通信。I2C总线上的每个从设备都有一个唯一的7bit物理识别地址。因为I2C地址全0表示广播地址,所以一个I2C总线理论最多能挂载2^7 - 1=127个 阅读详情

                                      相关推荐

                                      I2C通信协议详解

                                      学习笔记

                                      FYBZ2020的博客 7595

                                      I2C通信协议

                                      可以看到当前地址读相对于前面的指定地址读缺少了第二步,就是没有指定是哪一个地址,此时当前地址指针指向谁就给谁进行读的操作,如果下一次还进行当前地址读的操作,此时地址指针自动加一。

                                      m0_74225397的博客 6056

                                      LCD1602 I2C模块:从硬件连接到代码驱动的完整指南

                                      I2C(Inter-Integrated Circuit)是一种广泛应用的串行通信协议,通过SDA(数据线)和SCL(时钟线)两根线实现多设备间的数据交换。其工作原理基于主从架构和地址寻址,主设备通过时钟信号同步数据传输,从设备根据唯一地址响应。这种协议极大简化了硬件连接,减少了微控制器的IO占用,提升了系统扩展性。在嵌入式开发和电子制作中,I2C常用于连接各类传感器、存储器和显示模块。LCD1602 I2C模块正是这一技术的典型应用,它将传统的并行接口LCD屏幕转换为I2C接口,解决了**Arduino*

                                      weixin_33430573的博客 282

                                      【一文吃透】常见通信协议(SPI、IIC、UART、CAN)[面试重点]

                                      主机输入,从机输出(数据来自从机);主机输出,从机输入(数据来自主机);SCLK串行时钟信号,由主机产生发送给从机;SS片选信号,由主机发送,以控制与哪个从机通信,通常是低电平有效信号。其他制造商可能会遵循其他命名规则,但是最终他们指的相同的含义。以下是一些常用术语;MISO也可以是SIMO,DOUT,DO,SDO或SO(在主机端);MOSI也可以是SOMI,DIN,DI,SDI或SI(在主机端);NSS也可以是CE,CS或SSEL;SCLK也可以是SCK;

                                      Mr_Guan的博客 5万+

                                      STM32F415RG双路温度采集:本地PJ85718DM与远程Modbus RTU实战

                                      在嵌入式温度采集和暖通空调控制场景中,本地与远程温度数据往往需要由同一颗主控统一管理。理解传感器数据链路差异是方案设计的前提:本地温度走I2C等短距离总线,重在采样精度与滤波;远程温度则依赖RS-485、Modbus RTU等标准协议,核心在于物理层加固与帧解析可靠性。STM32F415RG凭借丰富外设与稳定性能,成为这类双路采集方案的理想载体。从数字温度传感器的寄存器读取、符号扩展,到Modbus CRC校验、字节序转换,再到限幅滤波与标定逻辑,本文结合工程实践,系统梳理了温度采集系统从硬件布局到软件实现

                                      weixin_34067102的博客 365

                                      ESP32接入CAN总线:网关设计、硬件协同与MicroPython实战

                                      CAN总线是一种面向实时控制的高可靠性差分通信协议,核心特性包括非破坏性仲裁、硬件级优先级调度、强抗干扰能力及确定性延迟,广泛应用于汽车电子、工业自动化等严苛场景。其物理层依赖收发器实现TTL与差分信号转换,协议层强调ID驱动的事件机制而非地址寻址。ESP32虽无原生CAN接口,但内置兼容ISO 11898-1的CAN控制器,结合外部收发器(如TJA1050)可构建边缘智能网关节点,典型应用涵盖OBD-II数据采集、CAN-to-MQTT协议转换等。MicroPython需定制编译启用CAN模块,并通过se

                                      weixin_36487018的博客 391

                                      STM32F407开发实战:50例程深度解析与工程移植指南

                                      在嵌入式系统开发中,微控制器(MCU)是核心硬件平台,其外设驱动与系统集成是工程师必备技能。通过理解外设工作原理(如GPIO、定时器、ADC/DAC、通信接口)和实时操作系统(RTOS)基础,开发者能构建高效可靠的嵌入式应用。这些技术广泛应用于物联网设备、工业控制、消费电子等领域。本文聚焦于STM32F407高性能单片机,针对其**工程移植**与**外设驱动**开发中的实际问题,通过分析一套包含50个实验例程的合集,详解如何从学习例程过渡到独立项目开发。内容涵盖时钟配置、中断管理、通信协议栈集成以及**US

                                      weixin_34296641的博客 350

                                      STM32基础回顾

                                      STM32单片机回顾

                                      weixin_42690036的博客 1452

                                      threejs开发可视化数字城市效果

                                      theme: condensed-night-purple 灵感图 现在随着城市的发展,越来越多的智慧摄像头,都被互联网公司布到城市的各个角落,举一个例子,一个大楼上上下下都被布置了智能摄像头,用于监控火势,人员进出,工装工牌佩戴等监控,这时候为了美化项目,大公司都会将城市的区域作为对象,进行3d可视化交互,接下来的内容,就是基于以上元素,开发的一款城市数据可视化的demo,包含楼宇特...

                                      weixin_41770018的博客 1580

                                      coreldraw sp2精简版 x4_CorelDRAW X4下载-CorelDRAW X4 SP2 精简版_Win10镜像官网

                                      CorelDRAW X4 SP2是一个专业图形设计软件,专用于矢量图形编辑与排版,借助其丰富的内容和专业图形设计、照片编辑和网站设计软件,您将能够随心所欲地表达自己的风格与创意,轻松创作徽标标志、广告标牌、车身贴和传单、模型绘制、插图描画、排版及分色输出等。CorelDRAW X4 SP2软件特色CorelDraw X4 相比两年前的 CorelDraw X3 加入了大量新特性,总计有50项以上,...

                                      weixin_30346025的博客 8905

                                      GPIO入门到精通完全指南

                                      GPIO是芯片提供的通用输入/输出引脚,这些引脚可以通过软件配置为输入或输出模式,实现与外部设备的信号交互。与UART、I2C等专用外设不同,GPIO没有固定的通信协议,灵活性极高,是嵌入式系统与外部世界连接的“通用接口”。核心特点:通用性:可根据需求配置为输入或输出,适配多种外部设备;可编程性:通过寄存器或软件库函数灵活配置工作模式、电平状态;双向性:部分引脚支持输入/输出模式切换,实现信号的双向传输。

                                      qq_64417645的博客 481

                                      SQL注入:联合查询的三个绕过技巧

                                      在SQL注入中,可以利用科学计数法来绕过对输入的限制。例如,可以使用科学计数法来绕过对字符串长度的限制,通过将字符串拆分成多个部分并使用科学计数法来表示。此外,还可以利用数据库的特定特性或语法来替代原始的查询语句,从而执行联合查询或其他恶意操作。另外,还可以尝试利用无列名注入的技巧,通过在原始查询中使用通配符或其他方式来绕过对列名的限制,从而执行恶意操作。

                                      qq_68163788的博客 3795

                                      不花钱学STM32:纯软件仿真玩转寄存器与HAL库外设开发

                                      嵌入式入门常被高昂的硬件成本劝退,但其实理解寄存器映射、外设中断和总线协议并不一定要依赖实体开发板。借助ARM指令级模拟器与虚拟外设模型,开发者可以在PC上完整复现STM32裸机开发流程,观察GPIO输出电平、UART数据帧、定时器溢出以及I2C应答时序的每一个bit变化。这种纯仿真学习方式拆掉了硬件不确定性带来的排查干扰,让代码逻辑、寄存器状态和中断行为自然透明,尤其适合预算有限的学生、转行者和希望快速验证代码逻辑的工程师。从GPIO点灯到DMA与中断嵌套,用软件仿真练透外设驱动开发,既节省成本,也建立更

                                      weixin_34212762的博客 1482

                                      5G-A无源物联网:深度解析“不插电“智能的底层技术原理

                                      北京移动推出5G-A无源物联网技术,实现电动车标签"零功耗"运行。该技术通过射频能量收集(利用5G-A基站高功率信号转换为电能)和反向散射通信(调制反射信号传输数据)两大核心技术,使设备无需电池即可工作。5G-A相比4G在信号能量密度、通信速率和定位精度等方面显著提升,结合通感一体等创新技术,为无源物联网提供商用基础。该方案已在北京小区试点应用,实现电动车精准定位管理,标签寿命可达5年以上。专家预测,这项技术将大幅降低物联网部署成本,推动"万物智联"时代的到来。

                                      通信行业老兵、物联网从业者,做一个有趣的普通人。 2014

                                      Flotherm XT学习历程

                                      大家好,通过半年的热仿真软件学习使用,最终发现最好用,最好上手的软件就是Flotherm XT,对新手非常友好,但由于软件近几年才发布,网上教程非常少,后面我将在这里持续分析软件使用心得,和大家一起学习。

                                      xiaoredred的博客 6268

                                      控制器局域网CAN

                                      本文细致讲解了CAN的结构、仲裁机制、同步、波特率、报文、邮箱、过滤器以及中断,囊括了CAN通信的基础知识。

                                      weixin_72007132的博客 1723

                                      YOLOv12改进策略【注意力机制篇】| 2024 PPA 并行补丁感知注意模块,提高小目标关注度(二次改进A2C2f)

                                      本文记录的是利用改进检测精度,详细说明了优化原因,注意事项等。原论文在红外小目标检测任务中,小目标在多次下采样操作中容易丢失关键信息。通过替代编码器和解码器基本组件中的传统卷积操作,更好地保留小目标的重要信息。HCF-Net: Hierarchical Context Fusion Network for Infrared Small Object Detection采用多分支特征提取策略,通过不同分支提取不同尺度和层次的特征。利用局部、全局和串行卷积分支,对输入特征张量进行处理。通过控制 patch si

                                      Limiiiing的博客 1888

                                      Linux 驱动研究 —— V4L2 (7)

                                      这篇文章以瑞芯微 RKCIF 驱动为例,系统剖析了 Videobuf2 (vb2) 框架的核心工作流程。文章重点梳理了 vb2_ops 回调函数,涵盖缓冲区设置与队列管理(rkcif_queue_setup、rkcif_buf_queue),并最终深入到控制硬件上下电与 DMA 传输的开流(rkcif_start_streaming)与关流(rkcif_stop_streaming)核心枢纽。

                                      kdxiaojie的博客 251

                                      Android 编译系统(Build System)剖析

                                      make是一种自动化构建工具,通过读取Makefile将源代码自动编译为可执行程序和库文件。

                                      虎哥LoveDroid 2170

                                      Claude Desktop新功能:让非技术人士也能自主编程

                                      摘要: Anthropic近日在Claude桌面应用中集成了Claude Code功能,显著降低了非技术用户的使用门槛。Claude Code作为代理式编程工具,支持多任务并行处理、Git隔离和可视化会话管理,无需命令行操作即可实现代码调试、重构等复杂任务。新增的“计划模式”通过先分析后执行的流程,确保安全性,使产品经理、数据分析师等非开发者也能参与技术工作。这一更新标志着AI技术正打破专业壁垒,让领域专家无需编码背景即可利用高级工具,推动技术民主化。

                                      独立开发,substack顶级编辑 1540

                                      XY2-100振镜控制:FPGA时序设计与激光振镜矫正实战

                                      XY2-100并非传统通信协议,而是面向高速光学执行器的硬件级时序规范,其本质是FPGA对DAC的纳秒级电平控制。理解该规范需突破‘串口思维’,深入信号链物理层——从SYNC边沿采样、坐标缩放、非线性补偿,到运放驱动与线圈响应,每一级都影响微秒级定位精度。技术价值在于实现<1μs抖动的稳定光点输出,支撑激光打标、增材制造等高精度工业场景。本文聚焦Verilog单周期流水线设计、LUT查表式激光振镜矫正及滑动窗口滤波等工程落地方法,直击量产控制器开发核心。

                                      weixin_34007886的博客 638

                                      老版QQ原生表情gif动态

                                      这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

                                      基于Arduino Uno与OLED的PONG游戏开发实战

                                      在嵌入式系统开发中,微控制器通过I2C通信协议驱动外设是实现人机交互的基础。I2C作为一种两线制串行总线,允许主控芯片与多个从设备高效通信,是连接传感器、显示屏等模块的通用技术。其核心价值在于以极少的硬件资源实现可靠的数据交换,广泛应用于物联网设备、智能硬件等场景。本文以经典的PONG游戏为载体,具体展示了如何利用Arduino Uno的硬件I2C引脚驱动SSD1306 OLED显示屏,并结合数字输入引脚读取轻触开关状态,实现一个完整的实时交互系统。项目涵盖了从电路连接到软件消抖、从游戏逻辑到碰撞检测的全流

                                      weixin_30499315的博客 334

                                      VC中sqlite的使用

                                      在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

                                      OV9734 DATASHEET,完整规格书

                                      OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

                                      深入解析TI ISO734x数字隔离器:原理、选型与PCB布局实战

                                      在工业自动化、电机驱动等复杂电气系统中,电气隔离是保障系统安全与信号完整性的核心技术。其核心原理是在电路的不同电位部分之间建立高阻抗屏障,阻断危险的直流和低频电流,同时允许数字信号或特定形式的能量(如通过电容、磁场耦合)安全传输。这项技术通过隔离栅实现了信号与电源的分离,从根本上解决了高压侧噪声、浪涌对低压控制电路的干扰问题,其技术价值在于提升系统可靠性、安全性并满足严格的安规标准。在应用场景上,它广泛用于可编程逻辑控制器(PLC)、变频器、伺服驱动、太阳能逆变器以及医疗设备等对安全性和抗干扰性要求极高的领

                                      weixin_30765577的博客 448

                                      Statistics and Machine Learning Toolbox

                                      Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

                                      STM32小白视角笔记(标准库)(乱序版)

                                      本文介绍了小白视角下的STM32学习的基本操作与应用

                                      obsidian_1_的博客 436
                                      上一篇: 13. STM32 定时器与 PWM 实战指南
                                      下一篇: 15.ENS160传感器使用秘籍:一文看懂多气体检测核心技巧
                                      硅基造物主
                                      博客等级 码龄4年 17粉丝 42原创
                                      评论
                                      成就一亿技术人!
                                      拼手气红包6.0元
                                      还能输入1000个字符
                                       
                                       条评论被折叠 查看
                                      添加红包

                                      请填写红包祝福语或标题

                                      个

                                      红包个数最小为10个

                                      元

                                      红包金额最低5元

                                      当前余额3.43元 前往充值 >
                                      需支付:10.00元
                                      成就一亿技术人!
                                      领取后你会自动成为博主和红包主的粉丝 规则
                                      hope_wisdom
                                      发出的红包
                                      实付元
                                      使用余额支付
                                      点击重新获取
                                      扫码支付
                                      钱包余额 0

                                      抵扣说明:

                                      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
                                      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

                                      余额充值