• IIC协议


    IIC协议

    1、协议简介

    IIC (Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。在 CPU 与被控 IC 之间、 IC 与 IC 之间进行双向传送, 高速 IIC 总线一般可达 400kbps 以上。

    在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设;STM32 标准库则是在寄存器与用户代码之间的软件层。对于通讯协议,我们也以分层的方式来理解,最基本的是把它分为物理层和协议层。物理层规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。简单来说物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英文来交流。

    下面我们分别对 I2C 协议的物理层及协议层进行讲解

    1.1物理层

    在这里插入图片描述
    它的物理层有如下特点:

    1. 它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连
      接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
    2. 一个 I2C 总线只使用两条总线线路,一条双向串行数据线 (SDA) ,一条串行时钟线 (SCL)。数
      据线即用来表示数据,时钟线用于数据收发同步。
    3. 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访
      问。
    4. 总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都
      输出高阻态时,由上拉电阻把总线拉成高电平。
    5. 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
    6. 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达
      3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
    7. 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。

    1.2 协议层

    I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号结束信号应答信号
    开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据。
    结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据。
    应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。

    这些信号中,起始信号是必需的,结束信号和应答信号,都可以不要。 IIC 总线时序图如下图所示:

    在这里插入图片描述

    目前大部分 MCU 都带有 IIC 总线接口, STM32 也不例外。但是这里我们依次使用 STM32 的硬件 IIC和软件IIC 来讲解,

    1.2.1 I2C 基本读写过程

    我们先看看I2C通信过程的基本结构,它的通讯过程:
    在这里插入图片描述
    在这里插入图片描述
    这些图表示的是主机和从机通讯时, SDA 线的数据包序列。
    其中 S 表示由主机的 I2C 接口产生的传输起始信号 (S),这时连接到 I2C 总线上的所有从机都会接收到这个信号。
    起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号。在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据 I2C 协议,这个从机地址可以是7 位或 10 位。
    在地址位之后,是传输方向的选择位,该位为 0 时,表示后面的数据传输方向是由主机传输至从 机,即主机向从机写数据。该位为 1 时,则相反,即主机由从机读数据。
    从机接收到匹配的地址后,主机或从机会返回一个应答 (ACK) 或非应答 (NACK) 信号,只有接收到应答信号后,主机才能继续发送或接收数据。
    写数据
    若配置的方向传输位为“写数据”方向,即第一幅图的情况,广播完地址,接收到应答信号后,主机开始正式向从机传输数据 (DATA),数据包的大小为 8 位,主机每发送完一个字节数据,都要等待从机的应答信号 (ACK),重复这个过程,可以向从机传输 N 个数据,这个 N 没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号 §,表示不再传输数据。
    读数据
    若配置的方向传输位为“读数据”方向,即第二幅图的情况,广播完地址,接收到应答信号后,从机开始向主机返回数据 (DATA),数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号 (ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希停止接收数据时,就向从机返回一个非应答信号 (NACK),则从机自动停止数据传输。
    读和写数据
    除了基本的读写, I2C 通讯更常用的是复合格式,即第三幅图的情况,该传输过程有两次起始信号 (S)。一般在第一次传输中,主机通过 从机地址信号寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址 (注意区分它与从机地址信号的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

    以上通讯流程中包含的各个信号分解如下:

    1.2.2 通讯的起始和停止信号

    之前提到的起始 (S) 和停止 § 信号是两种特殊的状态 。当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当SCL 是高电平时 SDA 线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。

    在这里插入图片描述

    1.2.3 数据有效性

    I2C 使用 SDA 信号线来传输数据,使用 SCL 信号线进行数据同步 。 SDA 数据线在 SCL 的每个时钟周期传输一位数据。传输时, SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL 为低电平时, SDA的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备

    在这里插入图片描述
    每次数据传输都以字节为单位,每次传输的字节数不受限制。

    1.2.4 地址及数据方向

    I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址
    (SLAVE_ADDRESS) 来查找从机。 I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位 (R/),第 8位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写
    数据。
    在这里插入图片描述
    读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,
    写数据方向时, SDA 由主机控制,从机接收信号。

    1.2.5 响应

    I2C 的数据和地址传输都带响应。响应包括“应答 (ACK)”和“非应答 (NACK)”两种信号。作为数据接收端时,当设备 (无论主从机) 接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答 (ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答 (NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。

    在这里插入图片描述
    传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制SDA,若 SDA 为高电平,表示非应答信号 (NACK),低电平表示应答信号 (ACK)。

    2、 STM32 的 I2C 特性及架构

    如果我们直接控制 STM32 的两个 GPIO 引脚,分别用作 SCL 及 SDA,按照上述信号的时序要求,可以像控制 LED 灯那样控制引脚的输出 (若是接收数据时则读取 SDA 电平),就可以实现 I2C 通讯。同样,假如我们按照 USART 的要求去控制引脚,也能实现 USART 通讯。所以只要遵守协议,就是标准的通讯,不管如何实现它,都能按通讯标准交互。
    由于直接控制 GPIO 引脚电平产生通讯时序时,需要由 CPU 控制每个时刻的引脚状态,所以称之为“软件模拟协议”方式。

    模拟协议简单的说就是自定义GPIO引脚状态,通过调电平模仿时序。

    相对地,还有“硬件协议”方式, STM32 的 I2C 片上外设专门负责实现 I2C 通讯协议,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来, CPU 只要检测该外设的状态和访问数据寄存器,就能完成数据收发。这种由硬件外设处理 I2C 协议的方式减轻了CPU 的工作,且使软件设计更加简单。

    2.1 STM32 的 I2C 外设简介

    STM32 的 I2C 外设可用作通讯的主机及从机,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7 位、 10位设备地址,支持 DMA 数据传输,并具有数据校验功能。

    2.2 STM32 的 I2C 架构剖析

    在这里插入图片描述

    2.2.1 通讯引脚

    I2C 的所有硬件架构都是根据图中左侧 SCL 线和 SDA 线展开的 (其中的 SMBA 线用于 SMBUS 的警告信号, I2C 通讯没有使用)。 STM32 芯片有多个 I2C 外设,它们的 I2C 通讯信号引出到不同的GPIO 引脚上,使用时必须配置到这些指定的引脚。关于 GPIO 引脚的复用功能,以规格书为准。

    引脚I2C1I2C2
    SCLPB6 / PB8(重映射)PB10
    SDAPB7 / PB9(重映射)PB11

    2.2.2 时钟控制逻辑

    SCL 线的时钟信号,由 I2C 接口根据时钟控制寄存器 (CCR) 控制,控制的参数主要为时钟频率。
    配置 I2C 的 CCR 寄存器可修改通讯速率相关的参数:

    • 可选择 I2C 通讯的“标准/快速”模式,这两个模式分别 I2C 对应 100/400Kbit/s 的通讯速率。
    • 在快速模式下可选择 SCL 时钟的占空比,可选 Tlow/Thigh=2 或 Tlow/Thigh=16/9 模式,我们知道 I2C 协议在 SCL 高电平时对 SDA 信号采样, SCL 低电平时 SDA 准备下一个数据,修改 SCL 的高低电平比会影响数据采样,但其实这两个模式的比例差别并不大,若不是要求非常严格,这里随便选就可以了。
    • CCR 寄存器中还有一个 12 位的配置因子 CCR,它与 I2C 外设的输入时钟源共同作用,产生SCL 时钟, STM32 的 I2C 外设都挂载在 APB1 总线上,使用 APB1 的时钟源 PCLK1, SCL信号线的输出时钟公式如下:
      标准模式:
      Thigh=CCR * TPCKL1 Tlow = CCR * TPCLK1
      快速模式中 Tlow/Thigh=2 时:
      Thigh = CCR * TPCKL1 Tlow = 2 * CCR* TPCKL1
      快速模式中 Tlow/Thigh=16/9 时:
      Thigh = 9 * CCR * TPCKL1 Tlow = 16 * CCR * TPCKL1
      例如,我们的 PCLK1=36MHz,想要配置 400Kbit/s 的速率,计算方式如下:
      PCLK 时钟周期: TPCLK1 = 1/36000000
      目标 SCL 时钟周期: TSCL = 1/400000
      SCL 时钟周期内的高电平时间: THIGH = TSCL/3
      SCL 时钟周期内的低电平时间: TLOW = 2*TSCL/3
      计算 CCR 的值: CCR = THIGH/TPCLK1 = 30

    计算结果得出 CCR 为 30,向该寄存器位写入此值则可以控制 IIC 的通讯速率为 400KHz,其实即使配置出来的 SCL 时钟不完全等于标准的 400KHz, IIC 通讯的正确性也不会受到影响,因为所有数据通讯都是由 SCL 协调的,只要它的时钟频率不远高于标准即可。

    2.2.3 数据控制逻辑

    I2C 的 SDA 信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器 (DR)、地址寄存器 (OAR)、 PEC 寄存器以及 SDA 数据线
    当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过 SDA 信号线发送出去;
    当从外部接收数据的时候,数据移位寄存器把 SDA 信号线采样到的数据一位一位地存储到“数据寄存器”中。
    若使能了数据校验,接收到的数据会经过 PCE 计算器运算,运算结果存储在“PEC 寄存器”中。
    当 STM32 的 I2C 工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到的地址与 STM32 的自身的I2C 地址寄存器的值作比较,以便响应主机的寻址。 STM32 的自身 I2C 地址可通过修改“自身地址寄存器”修改,支持同时使用两个 I2C 设备地址,两个地址分别存储在 OAR1 和 OAR2 中。

    2.2.4 整体控制逻辑

    整体控制逻辑负责协调整个 I2C 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器 (SR1 和 SR2)”,我们只要读取这些寄存器相关的寄存器位,就可以了解 I2C 的工作状态。除此之外,控制逻辑还根据要求,负责控制产生 I2C 中断信号、 DMA 请求及各种 I2C 的通讯信号(起始、停止、响应信号等)

    2.3 通讯过程

    使用 I2C 外设通讯时,在通讯的不同阶段它会对“状态寄存器 (SR1 及 SR2)”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。

    2.3.1 主发送器

    主发送器流程,即作为 I2C 通讯的主机端时,向外发送数据时的过程。

    在这里插入图片描述

    主发送器发送流程及事件说明如下:

    1. 控制产生起始信号 (S),当发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
    2. 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”“EV8”,这时SR1 寄存器的“ADDR”位及“TXE”位被置1,ADDR 为 1 表示地址已经发送, TXE 为 1 表示数据寄存器为空;
    3. 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数据,这时 TXE 位会被重置 0,表示数据寄存器非空, I2C 外设通过 SDA 信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了;
    4. 当我们发送数据完成后,控制 I2C 设备产生一个停止信号 §,这个时候会产生 EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。

    2.3.2 主接收器

    主接收器过程,即作为 I2C 通讯的主机端时,从外部接收数据的过程,

    在这里插入图片描述
    主接收器接收流程及事件说明如下:

    1. 同主发送流程,起始信号 (S) 是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
    2. 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时 SR1 寄存器的“ADDR”位被置 1,表示地址已经发送。
    3. 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件, SR1 寄存器的 RXNE 被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制 I2C 发送应答信号 (ACK) 或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;
    4. 发送非应答信号后,产生停止信号 §,结束传输。在发送和接收过程中,有的事件不只是标志了我们上面提到的状态位,还可能同时标志主机状态
      之类的状态位,而且读了之后还需要清除标志位,比较复杂。我们可使用 STM32 标准库函数来直接检测这些事件的复合标志,降低编程难度。

    3、 I2C 初始化结构体详解

    跟其它外设一样, STM32 标准库提供了 I2C 初始化结构体及初始化函数来配置 I2C 外设。初始化结构体及函数定义在库文件“stm32f10x_i2c.h”及“stm32f10x_i2c.c”中。

    typedef struct {
     uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/
     uint16_t I2C_Mode; /*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 */
     uint16_t I2C_DutyCycle; /* 指定时钟占空比,可选 low/high = 2:1 及 16:9 模式
    */
     uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 设备地址 */
     uint16_t I2C_Ack; /*!< 使能或关闭响应 (一般都要使能) */
     uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为 7 位及 10 位 */
     } I2C_InitTypeDef;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这些结构体成员说明如下,其中括号内的文字是对应参数在 STM32 标准库中定义的宏:

    1. I2C_ClockSpeed
      本成员设置的是I2C 的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到 I2C 的时钟控制寄存器 CCR。而我们写入的这个参数值不得高于 400KHz。实际上由于 CCR 寄存器不能写入小数类型的时钟因子,影响到 SCL 的实际频率可能会低于本成员设置的参数值,这时除了通讯稍慢一点以外,不会对 I2C 的标准通讯造成其它影响。
    2. I2C_Mode
      本成员是选择 I2C 的使用方式,有 I2C 模式 (I2C_Mode_I2C) 和 SMBus主、从模式
      (I2C_Mode_SMBusHost、 I2C_Mode_SMBusDevice) 。 I2C 不需要在此处区分主从模式,直接设置 I2C_Mode_I2C 即可。
    3. I2C_DutyCycle
      本成员设置的是 I2C 的 SCL 线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为 2: 1 (I2C_DutyCycle_2) 和 16: 9 (I2C_DutyCycle_16_9) 。其实这两个模式的比例差别并不大,一般要求都不会如此严格,这里随便选就可以
    4. I2C_OwnAddress1
      本成员配置的是 STM32 的 I2C 设备自己的地址,每个连接到 I2C 总线上的设备都要有一个自己的地址,作为主机也不例外。地址可设置为 7 位或 10 位 (受下面 I2C_AcknowledgeAddress 成员决定),只要该地址是 I2C 总线上唯一的即可。STM32 的 I2C 外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员I2C_OwnAddress1 配置的是默认的、 OAR1 寄存器存储的地址,若需要设置第二个地址寄存器OAR2,可使用I2C_OwnAddress2Config 函数来配置, OAR2 不支持 10 位地址,只有 7 位。
    5. I2C_Ack_Enable
      本成员是关于 I2C 应答设置,设置为使能则可以发送响应信号。本实验配置为允许应答 (I2C_Ack_Enable) ,这是绝大多数遵循 I2C 标准的设备的通讯要求,改为禁止应答(I2C_Ack_Disable) 往往会导致通讯错误。
    6. I2C_AcknowledgeAddress
      本成员选择 I2C 的寻址模式是 7 位还是 10 位地址。这需要根据实际连接到 I2C 总线上设备的地址进行选择,这个成员的配置也影响到 I2C_OwnAddress1 成员,只有这里设置成 10 位模式时,I2C_OwnAddress1 才支持 10 位地址。配置完这些结构体成员值,调用库函数 I2C_Init 即可把结构体的配置写入到寄存器中。

    4、 软件设计

    1. 配置通讯使用的目标引脚为开漏模式;

    2. 使能 I2C 外设的时钟;

    3. 配置 I2C 外设的模式、地址、速率等参数并使能 I2C 外设;

    4. 编写基本 I2C 按字节收发的函数;

    硬件IIC

    iic.c

    #include "iic.h"
    #include "stm32f10x_i2c.h"
    #include 
    #include "stdio.h"
    
    static __IO uint32_t  I2CTimeout = I2CT_LONG_TIMEOUT;
    
    static uint32_t  I2C_TIMEOUT_UserCallback(uint8_t errorCode);
    
    //iic初始化
    void I2C_Configuration(void)
    {
    	I2C_InitTypeDef  I2C_InitStructure;
    	GPIO_InitTypeDef  GPIO_InitStructure; 
    
    	I2C_APBxClock_FUN(I2C_CLK,ENABLE);
    	I2C_GPIO_APBxClock_FUN(I2C_GPIO_CLK,ENABLE);
    
    	/*STM32F103C8T6芯片的硬件I2C: PB6 -- SCL; PB7 -- SDA */
    	GPIO_InitStructure.GPIO_Pin =  I2C_SCL_PIN | I2C_SDA_PIN;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//I2C必须开漏输出
    	GPIO_Init(I2C_PORT, &GPIO_InitStructure);
    
    	I2C_DeInit(I2Cx);//使用I2C1
    	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    	I2C_InitStructure.I2C_OwnAddress1 = I2Cx_OWN_ADDRESS;//主机的I2C地址,随便写的
    	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    	I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;//400K
    
    	I2C_Cmd(I2Cx, ENABLE);
    	I2C_Init(I2Cx, &I2C_InitStructure);
    }
    
    void I2C_WriteByte(uint8_t addr,uint8_t data)
    {
    
        while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));
    	
    	I2C_GenerateSTART(I2C1, ENABLE);//开启I2C1
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));/*EV5,主模式*/
    
    	I2C_Send7bitAddress(I2C1, I2C_ADDRESS, I2C_Direction_Transmitter);//器件地址 -- 默认0x78
    	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    
    	I2C_SendData(I2C1, addr);//寄存器地址
    	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    
    	I2C_SendData(I2C1, data);//发送数据
    	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    	
    	I2C_GenerateSTOP(I2C1, ENABLE);//关闭I2C1总线
    }
    
    
    /**
      * @brief   写一个字节到I2C设备中
      * @param
      *		@arg pBuffer:缓冲区指针
      *		@arg Cmd_or_Data:写地址(数据、命令)
      * @retval  正常返回1,异常返回0
      */
    uint8_t Write_Byte(u8 Cmd_or_Data,u8 pBuffer)
    {
        //总线忙标志位
        while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));
    	
    	/* 发送参数条件 */
    	I2C_GenerateSTART(I2Cx, ENABLE);
    	
        I2CTimeout = I2CT_FLAG_TIMEOUT;
    
        /* EV5,主模式*/
        while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
        {
            if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
        }
    
        /* 器件地址 -- 默认0x78*/
        I2C_Send7bitAddress(I2Cx, I2C_ADDRESS, I2C_Direction_Transmitter);
    
        I2CTimeout = I2CT_FLAG_TIMEOUT;
        /* 测试EV6并清除它*/
        while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
        {
            if((I2CTimeout--) == 0) 
    					return I2C_TIMEOUT_UserCallback(1);
        }
    
        /* 寄存器地址*/
        I2C_SendData(I2Cx, Cmd_or_Data);
        //发送命令还是发送数据,命令0x00,数据0x40
        I2CTimeout = I2CT_FLAG_TIMEOUT;
        /* 测试EV8并清除它 */
        while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
        {
            if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
        }
    
        /* 发送数据*/
        I2C_SendData(I2Cx, pBuffer);
    
        I2CTimeout = I2CT_FLAG_TIMEOUT;
    
        /*测试EV8并清除它*/
        while(!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
        {
            if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
        }
        /* 关闭I2C1总线 */
        I2C_GenerateSTOP(I2Cx, ENABLE);
    
        return 1; //正常返回1
    }
    
    
    void WriteCmd(unsigned char I2C_Command)//写命令
    {
    	Write_Byte(0x00, I2C_Command);
    }
    
    void WriteDat(unsigned char I2C_Data)//写数据
    {
    	Write_Byte(0x40, I2C_Data);
    }
    
    
    static  uint32_t  I2C_TIMEOUT_UserCallback(uint8_t errorCode)
    {
        ERROR("I2C 等待超时!errorCode = %d",errorCode);
        return 0;
    }
    
    
    • 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
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135

    iic.h

    #ifndef _IIC_H
    #define _IIC_H
    
    #include "stm32f10x.h"
    
    /**************************I2C参数定义,I2C1或I2C2********************************/
    #define             I2Cx                          I2C1
    #define             I2C_APBxClock_FUN             RCC_APB1PeriphClockCmd
    #define             I2C_CLK                       RCC_APB1Periph_I2C1
    #define             I2C_GPIO_APBxClock_FUN        RCC_APB2PeriphClockCmd
    #define             I2C_GPIO_CLK                  RCC_APB2Periph_GPIOB
    #define             I2C_PORT                      GPIOB
    #define             I2C_SCL_PIN                   GPIO_Pin_6
    #define             I2C_SDA_PIN                   GPIO_Pin_7
    
    /* STM32 I2C 快速模式 */
    #define 		    I2C_Speed      		          400000  //*
    
    /* 这个地址只要与STM32外挂的I2C器件地址不一样即可 */
    #define 		    I2Cx_OWN_ADDRESS              0X30
    
    #define				I2C_ADDRESS    			      0X78
    
    
    /*等待超时时间*/
    #define I2CT_FLAG_TIMEOUT         ((uint32_t)0x1000)
    #define I2CT_LONG_TIMEOUT         ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
    
    /*信息输出*/
    #define DEBUG_ON         1
    
    #define INFO(fmt,arg...)           printf("<<--INFO->> "fmt"\n",##arg)
    #define ERROR(fmt,arg...)          printf("<<--ERROR->> "fmt"\n",##arg)
    #define DEBUG(fmt,arg...)          do{\
                                              if(DEBUG_ON)\
                                              printf("<<--DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
                                              }while(0)
    
    
    void I2C_Configuration(void);
    uint32_t  I2C_TIMEOUT_UserCallback(uint8_t errorCode);
    uint8_t  Write_Byte(u8 Cmd_or_Data,u8 pBuffer);
    void WriteCmd(unsigned char I2C_Command);//写命令
    void WriteDat(unsigned char I2C_Data);//写数据
    #endif
    
    
    • 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

    模拟IIC

    切记尽量避免使用PB3、PB4,具体看stm32f103c8t6使用PB3和PB4做普通GPIO使用时发现异常

    iic.c

    #include "iic.h"
    #include "delay.h"
    
    //初始化IIC
    void IIC_Init(void)
    {
        GPIO_InitTypeDef GPIO_InitStructure;
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	/* 打开GPIO时钟 */
        GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
        GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;/* 开漏输出 */
        GPIO_Init(GPIOB, &GPIO_InitStructure);
        /* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
    
        IIC_SCL=1;
        IIC_SDA=1;
    }
    
    
    
    //产生IIC起始信号
    void IIC_Start(void)
    {
        SDA_OUT();     //sda线输出
        IIC_SDA=1;
        IIC_SCL=1;
        Delay_us(4);
        IIC_SDA=0;//START:when CLK is high,DATA change form high to low
        Delay_us(4);
        IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
    }
    
    //产生IIC停止信号
    void IIC_Stop(void)
    {
        SDA_OUT();//sda线输出
        IIC_SCL=0;
        IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
        Delay_us(4);
        IIC_SCL=1;
        IIC_SDA=1;//发送I2C总线结束信号
        Delay_us(4);
    }
    //等待应答信号到来
    //返回值:1,接收应答失败
    //        0,接收应答成功
    u8 IIC_Wait_Ack(void)
    {
        u8 ucErrTime=0;
        SDA_IN();      //SDA设置为输入
        IIC_SDA=1;
        Delay_us(1);
        IIC_SCL=1;
        Delay_us(1);
        while(READ_SDA)
        {
            ucErrTime++;
            if(ucErrTime>250)
            {
                IIC_Stop();
                return 1;
            }
        }
        IIC_SCL=0;//时钟输出0
        return 0;
    }
    //产生ACK应答
    void IIC_Ack(void)
    {
        IIC_SCL=0;
        SDA_OUT();
        IIC_SDA=0;
        Delay_us(2);
        IIC_SCL=1;
        Delay_us(2);
        IIC_SCL=0;
    }
    //不产生ACK应答
    void IIC_NAck(void)
    {
        IIC_SCL=0;
        SDA_OUT();
        IIC_SDA=1;
        Delay_us(2);
        IIC_SCL=1;
        Delay_us(2);
        IIC_SCL=0;
    }
    //IIC发送一个字节
    void IIC_Send_Byte(u8 txd)
    {
        u8 t;
        SDA_OUT();
        IIC_SCL=0;//拉低时钟开始数据传输
        for(t=0; t<8; t++)
        {
            IIC_SDA=(txd&0x80)>>7;
            txd<<=1;
            Delay_us(2); 
            IIC_SCL=1;
            Delay_us(2);
            IIC_SCL=0;
            Delay_us(2);
        }
    }
    //读1个字节,ack=1时,发送ACK,ack=0,发送nACK   
    u8 IIC_Read_Byte(unsigned char ack)
    {
    	unsigned char i,receive=0;
    	SDA_IN();//SDA设置为输入
      for(i=0;i<8;i++ )
    	{
        IIC_SCL=0; 
        delay_us(2);
    		IIC_SCL=1;
        receive<<=1;
        if(READ_SDA)receive++;   
    		delay_us(1); 
      }					 
    	if (ack)
    			IIC_Ack();//发送ACK
    	else
    			IIC_NAck(); //发送NACK   
    	return receive;
    }
    
    
    //IIC连续写
    void IIC_WriteBytes(u8 WriteAddr,u8* data,u8 dataLength)
    {
        u8 i;
        IIC_Start();
    
        IIC_Send_Byte(WriteAddr);	    //发送写命令
        IIC_Wait_Ack();
    
        for(i=0; i<dataLength; i++)
        {
            IIC_Send_Byte(data[i]);
            IIC_Wait_Ack();
        }
        IIC_Stop();//产生一个停止条件
        Delay_ms(10);
    }
    //IIC连续读
    void IIC_ReadBytes(u8 deviceAddr, u8 writeAddr,u8* data,u8 dataLength)
    {
        u8 i;
        IIC_Start();
    
        IIC_Send_Byte(deviceAddr);	    //发送读命令
        IIC_Wait_Ack();
        IIC_Send_Byte(writeAddr);
        IIC_Wait_Ack();
        IIC_Send_Byte(deviceAddr|0X01);//进入接收模式
        IIC_Wait_Ack();
    
        for(i=0; i<dataLength-1; i++)
        {
            data[i] = IIC_Read_Byte(1); //发送应答信号
        }
        data[dataLength-1] = IIC_Read_Byte(0);//停止读发送不应答
        IIC_Stop();//产生一个停止条件
        Delay_ms(10);
    }
    
    void IIC_Read_One_Byte(u8 daddr,u8 addr,u8* data)
    {
        IIC_Start();
    
        IIC_Send_Byte(daddr);	   //发送读命令
        IIC_Wait_Ack();
        IIC_Send_Byte(addr);//发送地址
        IIC_Wait_Ack();
        IIC_Start();
        IIC_Send_Byte(daddr|0X01);//进入接收模式
        IIC_Wait_Ack();
        *data = IIC_Read_Byte(0);
        IIC_Stop();//产生一个停止条件
    }
    
    void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data)
    {
        IIC_Start();
    
        IIC_Send_Byte(daddr);	    //发送写命令
        IIC_Wait_Ack();
        IIC_Send_Byte(addr);//发送地址
        IIC_Wait_Ack();
        IIC_Send_Byte(data);     //发送字节
        IIC_Wait_Ack();
        IIC_Stop();//产生一个停止条件
        Delay_ms(10);
    }
    
    
    
    • 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
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196

    iic.h(注意IO方向设置)

    #ifndef _IIC_H
    #define _IIC_H
    
    #include "sys.h"
    
    
    //IO方向设置
    /*
    CRL(引脚的0-7脚) CRH(引脚的8-16脚)
    
    0X0FFFFFFF=0000 1111 1111 1111 1111 1111 1111 1111          即为第7引脚-〉PB7
    
    (u32)8 为控制模式 ,转为二进制:1000意思就是Input模式, Input pull-up 即输出模式!!!(i2c一般固定是8)
    
    <<28 左移28位  即将上面 0X0FFFFFFF=0000 1111 1111 1111 1111 1111 1111 1111  左移28 直到0000 位.
    
    (u32)3   3换成2进制是0011, 结合上面的就是00就是outpu的push-pull, 11表示速度是50MHz
    
    */
    #define SDA_IN() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}
    #define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}
    
    //IO操作函数
    #define IIC_SCL    PBout(6) //SCL
    #define IIC_SDA    PBout(7) //SDA	 
    #define READ_SDA   PBin(7)  //输入SDA 
    
    //IIC所有操作函数
    void IIC_Init(void);                //初始化IIC的IO口
    void IIC_Start(void);				//发送IIC开始信号
    void IIC_Stop(void);	  			//发送IIC停止信号
    void IIC_Send_Byte(u8 txd);			//IIC发送一个字节
    u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节
    u8 IIC_Wait_Ack(void); 				//IIC等待ACK信号
    void IIC_Ack(void);					//IIC发送ACK信号
    void IIC_NAck(void);				//IIC不发送ACK信号
    
    void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data);
    void IIC_Read_One_Byte(u8 daddr,u8 addr,u8* data);
    
    void IIC_WriteBytes(u8 WriteAddr,u8* data,u8 dataLength);
    void IIC_ReadBytes(u8 deviceAddr, u8 writeAddr,u8* data,u8 dataLength);
    #endif
    
    
    • 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

    IIC代码文件

    链接:https://pan.baidu.com/s/1uxNszZKhz7FEMclhsbfUbw 提取码:0000

  • 相关阅读:
    【实例项目:基于多设计模式下的日志系统(同步&异步)】
    Vue中使用vue-router
    Spring refresh 方法分析之一
    LeetCode 刷题记录——从零开始记录自己一些不会的
    七、2023.10.1.Linux(一).7
    招募 AIGC 训练营助教 @上海
    【数论】卡特兰数
    Golang import
    基于SPI机制手动模拟数据库驱动打成Jar包使用
    mysql函数汇总之系统信息函数
  • 原文地址:https://blog.csdn.net/weixin_55999942/article/details/126316990