【STM32】I2C协议使用浅析·硬件I2C代码编写

目录

1.  概述

2.  物理层

3.  协议层

3.1  协议组成

3.1.1  起始信号

3.1.2  终止信号

3.1.3  发送一个字节

3.1.4  接收一个字节

3.1.5  发送应答

2.1.6  接收应答

3.2  主机写数据到从机

3.3  主机读从机数据

4.  特性

5.  架构

5.1  通讯引脚

5.2  时钟控制逻辑

5.3  数据控制逻辑

5.4  整体控制逻辑

6.  主模式

6.1  主发送

6.2  主接收

7.  从模式

7.1  从发送

7.2  从接收

8.  库函数讲解

8.1  I2C_InitTypeDef

8.1.1  I2C_ClockSpeed

8.1.2  I2C_Mode

8.1.3  I2C_DutyCycle     

8.1.4  I2C_OwnAddress1     

8.1.5  I2C_Ack_Enable     

8.1.6  I2C_AcknowledgeAddress     

8.2  库函数

9.  IIC代码编写

9.1  准备

9.2  引脚初始化

9.3  模式配置

9.4  初始化

9.5  写一个字节数据

9.6  写入多个字节数据

9.7  读取数据

9.8  等待函数

9.9  主函数


1.  概述

        I2C(Inter-Integrated Circuit)总线是由NXP Semiconductors(前身为Philips Semiconductor)公司开发的一种串行通信总线。它是一种用于连接微控制器和外部设备的串行通信协议,常用于连接各种集成电路、传感器、存储器芯片等外围设备。

        两根通信线:SCL(Serial Clock)、SDA(Serial Data)。SCL是时钟线,用于同步数据传输的时钟信号;SDA是数据线,用于传输实际的数据。

        同步,半双工:I2C总线是同步通信的,意味着数据的传输是基于时钟信号的。它是半双工的,即在同一时间内,数据传输只能单向进行,但是可以在数据传输的过程中切换方向。

        带数据应答:在I2C通信中,接收方需要给发送方一个应答信号,以确认数据是否成功接收。这种数据应答的机制有助于保证通信的可靠性。

        支持总线挂载多设备(一主多从,多主多从):I2C总线支持一主多从的架构,即一个主设备(通常是微控制器或者处理器)可以同时控制多个从设备(外部器件)。此外,它也支持多主多从的架构,允许多个主设备依次控制总线上的从设备。

2.  物理层

        所有IIC设备的SCL连在一起,SDA连在一起,这是I2C总线的基本连接方式。所有I2C设备的时钟线(SCL)都被连接在一起,以同步数据传输的时钟信号。同样,所有设备的数据线(SDA)也连接在一起,用于实际的数据传输。

        SCL和SDA各添加一个上拉电阻,电阻一般为4.7KΩ左右。为了确保在总线上没有设备发送数据时,SCL和SDA线保持高电平,通常会在每条线上连接一个上拉电阻:

        设备的SCL和SDA均要设置成开漏输出模式。这意味着设备可以将线拉低(输出低电平),但是只能释放线,而不能将线拉高(输出高电平)。这种模式使得多个设备可以共享总线,避免冲突,对于为什么使用开漏输出模式可以参考如下两个链接内容:

【STM32】轻松搞懂推挽和开漏输出怎么用-优快云博客

嵌入式面试八股文(一)·define和const的区别以及IIC为什么要加上拉电阻,为什么使用开漏输出_电阻const-优快云博客

        IIC具有三种传输模式:

  • 标准模式传输速率为100kbit/s
  • 快速模式为400kbit/s
  • 高速模式下可达 3.4Mbit/s

        但目前大多I2C设备尚不支持高速模式。

        主机完全掌控SCL,在空闲状态下主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机。


拓展:

        其中对于上拉电阻的取值,我们一般取值为4.7KΩ左右,这个值不能过大,也不能过小,首先如果过小,如取值为100Ω,其此时电路的电流就有33mA,对于STM32来说,其引脚的最大电流为25mA,这样容易烧毁电路:

        并且过小的电阻,由于线路电阻分压的作用,可能导致MOS管无法到达导通状态,使电路无法正常通信。

        如果电阻过大,在通常电路的设计时,每一个IO口可能对地有一些寄生电容,这样在电源对电容充电的过程中,会形成一个类似爬坡的弧度,不是标准的矩形:

        如果电容过大,充电时间过长可能电容未充好电就放点,导致信号失真:

        因此我们需要根据设备的增加,进行适当的调整上拉电阻的大小。


3.  协议层

        I2C的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。

3.1  协议组成

3.1.1  起始信号

        起始条件:SCL高电平期间,SDA从高电平切换到低电平:

3.1.2  终止信号

        终止条件:SCL高电平期间,SDA从低电平切换到高电平:

3.1.3  发送一个字节

        发送一个字节: SCL低电平期间,主机将数据位依次放到SDA线上(高位先行) ,然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节:

        主机首先在SCL低电平,主机如果想发送0,就拉低SDA到低电平,主机如果想发送1,就放手,SDA回弹到高电平,在SCL低电平期间允许改变SDA的电平,如图黄色部分:

        在SCL高电平期间不允许改变SDA的电平,从机需要尽快读取SDA,一般在上升沿从机就已经读取完成了。

高位先行:具体来说,对于每个字节,数据的最高有效位(MSB,Most Significant Bit)会先被发送或接收,然后是次高位、中间位,直到最低有效位(LSB,Least Significant Bit)。这种顺序被称为"高位先行",因为数据的高位先被处理。

3.1.4  接收一个字节

        接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)。

3.1.5  发送应答

        发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答:

2.1.6  接收应答

        接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA):

3.2  主机写数据到从机

        我们来看如下这张图,其中阴影部分表示主机传输到从机的数据,空白部分是从机给主机的数据:

        具体流程:

① 主机起始信号(S),表示我要开始发送数据了;

② 这个数据具体要发给谁呢?需要根据从机的名称去找到从机将数据发送给从机(SLAVE_ADDRESS从机的地址);

③ 我们找到了从机,告诉从机我们想要写数据(R)给从机,从机说好的(A接收应答),允许你写入;

④ 主机开始发送想要写入的数据(DATA),如果从机还想要接收数据,发送应答信号(ACK),如果不养接收发送非应答信号(NACK)。

⑤ 数据结束,主机发送终止信号(P)

3.3  主机读从机数据

        读的流程和写大体相同,只是一个数据流向是主机到从机,一个是从机到主机:

        完整的读写流程图:

4.  特性

        对于STM32来说,其IIC协议的实现可以分为软件IIC和硬件IIC:

  • 软件模拟协议:使用CPU直接控制通讯引脚的电平,产生出符合通讯协议标准的逻辑。
  • 硬件实现协议:由STM32的I2C片上外设专门负责实现I2C通讯协议,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU只要检测该外设的状态和访问数据寄存器,就能完成数据收发。这种由硬件外设处理I2C协议的方式减轻了CPU的工作,且使软件设计更加简单。

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

5.  架构

        其架构可以拆分为如下几部分:

① 通讯引脚

② 时钟控制逻辑

③ 数据控制逻辑

④ 整体控制逻辑
 

5.1  通讯引脚

        根据自己所有芯片查看数据手册,其中SCL表示时钟线,SDA表示数据线:

这里只是针对硬件的片上外设,如果使用软件IIC,不同GPIO口就可以。

        除此之外,我们还看到一条线SMBA,SMBA 是一个低电平有效的中断线,允许从设备在没有被主机寻址时,主动通知主机“我有话要说”。

        在标准的 I²C 系统中,主机(如微控制器)必须通过轮询 的方式来检查从设备是否有数据要上报。这会浪费总线带宽和主机资源。SMBA 提供了一种中断驱动的机制,提高了系统的效率。

5.2  时钟控制逻辑

        SCL线的时钟信号,由I2C接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率,并且前面我们也提到了,对于STM32来说其支持100Kbit/s和400Kbit/s的速率,这个速率是怎么来的呢?

        首先我们先找到数据手册,如下部分:

        由图可以看出对于STM32来说其IIC由标准模式和快速模式,并且其快速模式下其可选择SCL时钟的占空比,可选Tlow/Thigh=2或Tlow/Thigh=16/9模式。(其中Tlow、Thigh分表表示高低电平时间)

        那么我们是如何得到上述速率呢?以400Kbit/s为例,我们通过数据手册可以知道IIC是挂在到APB1总线上的外设,其中PCLK1=36MHz,想要配置400Kbit/s的速率,计算方式如下:

PCLK时钟周期:                         

        T=1/f  

        TPCLK1 = 1/PCLK1 = 1/36000000     

目标SCL时钟周期,我们想要配置的速率:                        

        TSCL = 1/f = 1/400000     

因为我们想要配置400Kbit/s,其速率是比较快的,因此需要配置告诉模式,而对于告诉模式我们有两种比例可以选择,这里我按照Tlow/Thigh=2,也就是所Tlow占总周期的2/3,Thigh占总周期的1/3,那么:

SCL时钟周期内的高电平时间:    

        THIGH = TSCL*(1/3)     

SCL时钟周期内的低电平时间:    

        TLOW = TSCL*(2/3)     

计算CCR的值:                             

        CCR  = 需要的时钟周期个数

                  = 期望的高电平时间 / 系统时钟周期

                  = THIGH/TPCLK1  

                  = 30

计算出来的CCR值写入到寄存器即可。

5.3  数据控制逻辑

        I2C的SDA信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器(DR)、地址寄存器(OAR)、PEC寄存器以及SDA数据线。

        当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过SDA信号线发送出去;

        当从外部接收数据的时候,数据移位寄存器把SDA信号线采样到的数据一位一位地存储到“数据寄存器”中。

5.4  整体控制逻辑

        整体控制逻辑负责协调整个I2C外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变。

        在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR1和SR2)”,只要读取这些寄存器相关的寄存器位,就可以了解I2C的工作状态。

6.  主模式

        在主模式时,I2C接口启动数据传输并产生时钟信号。串行数据传输总是以起始条件开始并以停止条件结束。当通过START位在总线上产生了起始条件,设备就进入了主模式。

6.1  主发送

        对于主模式的发送过程我们可以参考上面的这个图示:

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

        我们以七位主发送为例:

        控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对SR1寄存器的“SB”位置1,表示起始信号已经发送;

        发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8_1”,这时SR1寄存器的“ADDR”位及“TXE”位被置1,ADDR 为1表示地址已经发送,TXE为1表示数据寄存器为空;

        往I2C的“数据寄存器DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生“EV8”事件,即TXE位被置1,重复这个过程,可以发送多个字节数据;

        发送数据完成后,控制I2C设备产生一个停止信号(P),这个时候会产生EV2事件,SR1的TXE位及BTF位都被置1,表示通讯结束。

6.2  主接收

        流程起始都差不多:

        起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对SR1寄存器的“SB”位置1,表示起始信号已经发送;

        发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1寄存器的“ADDR”位被置1,表示地址已经发送。

        从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1寄存器的RXNE被置1,表示接收数据寄存器非空,读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时可以控制I2C发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;

        发送非应答信号后,产生停止信号(P),结束传输。

7.  从模式

        默认情况下,I2C接口总是工作在从模式。从从模式切换到主模式,需要产生一个起始条件。为了产生正确的时序,必须在I2C_CR2寄存器中设定该模块的输入时钟。输入时钟的频率必须至少是:
● 标准模式下为:2MHz 
● 快速模式下为:4MHz

7.1  从发送

        在接收到地址和清除ADDR位后,从发送器将字节从DR寄存器经由内部移位寄存器发送到SDA线上。从设备保持SCL为低电平,直到ADDR位被清除并且待发送数据已写入DR寄存器。

当收到应答脉冲时: 

● TxE位被硬件置位,如果设置了ITEVFEN和ITBUFEN位,则产生一个中断。

        如果TxE位被置位,但在下一个数据发送结束之前没有新数据写入到I2C_DR寄存器,则BTF位被置位,在清除BTF之前I2C接口将保持SCL为低电平;读出I2C_SR1之后再写入I2C_DR寄存器将清除BTF位。

7.2  从接收

        在接收到地址并清除ADDR后,从接收器将通过内部移位寄存器从SDA线接收到的字节存进DR寄存器。I2C接口在接收到每个字节后都执行下列操作: 

● 如果设置了ACK位,则产生一个应答脉冲
● 硬件设置RxNE=1。如果设置了ITEVFEN和ITBUFEN位,则产生一个中断。

        如果RxNE被置位,并且在接收新的数据结束之前DR寄存器未被读出,BTF位被置位,在清除BTF之前I2C接口将保持SCL为低电平;读出I2C_SR1之后再写入I2C_DR寄存器将清除BTF位。

8.  库函数讲解

8.1  I2C_InitTypeDef

        在正常使用过程中,我们需要对这些结构体成员变量进行赋值:

typedef struct
{
  uint32_t I2C_ClockSpeed;          /*!< Specifies the clock frequency.
                                         This parameter must be set to a value lower than 400kHz */

  uint16_t I2C_Mode;                /*!< Specifies the I2C mode.
                                         This parameter can be a value of @ref I2C_mode */

  uint16_t I2C_DutyCycle;           /*!< Specifies the I2C fast mode duty cycle.
                                         This parameter can be a value of @ref I2C_duty_cycle_in_fast_mode */

  uint16_t I2C_OwnAddress1;         /*!< Specifies the first device own address.
                                         This parameter can be a 7-bit or 10-bit address. */

  uint16_t I2C_Ack;                 /*!< Enables or disables the acknowledgement.
                                         This parameter can be a value of @ref I2C_acknowledgement */

  uint16_t I2C_AcknowledgedAddress; /*!< Specifies if 7-bit or 10-bit address is acknowledged.
                                         This parameter can be a value of @ref I2C_acknowledged_address */
}I2C_InitTypeDef;

        翻译一下:

typedef struct
{
  uint32_t I2C_ClockSpeed;          /*!< 指定时钟频率。
                                         该参数必须设置为低于400kHz的值 */

  uint16_t I2C_Mode;                /*!< 指定I2C模式。
                                         该参数可为 @ref I2C_mode 中的值 */

  uint16_t I2C_DutyCycle;           /*!< 指定I2C快速模式下的占空比。
                                         该参数可为 @ref I2C_duty_cycle_in_fast_mode 中的值 */

  uint16_t I2C_OwnAddress1;         /*!< 指定设备第一个自身地址。
                                         该参数可为7位或10位地址 */

  uint16_t I2C_Ack;                 /*!< 使能或禁用应答功能。
                                         该参数可为 @ref I2C_acknowledgement 中的值 */

  uint16_t I2C_AcknowledgedAddress; /*!< 指定应答地址类型为7位或10位。
                                         该参数可为 @ref I2C_acknowledged_address 中的值 */
}I2C_InitTypeDef;

8.1.1  I2C_ClockSpeed

        设置I2C的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到I2C的时钟控制寄存器CCR。而我们写入的这个参数值不得高于400KHz。     

        实际上由于CCR寄存器不能写入小数类型的时钟因子,影响到SCL的实际频率可能会低于本成员设置的参数值,这时除了通讯稍慢一点以外,不会对I2C的标准通讯造成其它影响。

8.1.2  I2C_Mode

        选择I2C的使用方式,有I2C模式(I2C_Mode_I2C )和SMBus主、从模式(I2C_Mode_SMBusHost、 I2C_Mode_SMBusDevice ) 。   

        I2C不需要在此处区分主从模式,直接设置I2C_Mode_I2C即可。

/** @defgroup I2C_mode 
  * @{
  */

#define I2C_Mode_I2C                    ((uint16_t)0x0000)
#define I2C_Mode_SMBusDevice            ((uint16_t)0x0002)  
#define I2C_Mode_SMBusHost              ((uint16_t)0x000A)
#define IS_I2C_MODE(MODE) (((MODE) == I2C_Mode_I2C) || \
                           ((MODE) == I2C_Mode_SMBusDevice) || \
                           ((MODE) == I2C_Mode_SMBusHost))

注意这里的模式配置,并不是配置什么标准模式和快速模式,速度的配置是根据上面我们配置的I2C_ClockSpeed参数进行配置的,小于100Kps为标准模式,大于为快速模式,设置完参数,其内部会进行配置。

8.1.3  I2C_DutyCycle     

        设置I2C的SCL线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2:1 ( I2C_DutyCycle_2)和16:9 (I2C_DutyCycle_16_9)。     

        其实这两个模式的比例差别并不大,一般要求都不会如此严格,这里随便选就可以了。

8.1.4  I2C_OwnAddress1     

        配置STM32的I2C设备自己的地址,每个连接到I2C总线上的设备都要有一个自己的地址,作为主机也不例外。地址可设置为7位或10位(受下面I2C_AcknowledgeAddress成员决定),只要该地址是I2C总线上唯一的即可。     

        STM32的I2C外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员I2C_OwnAddress1配置的是默认的、OAR1寄存器存储的地址,若需要设置第二个地址寄存器OAR2,可使用I2C_OwnAddress2Config函数来配置,OAR2不支持10位地址。

8.1.5  I2C_Ack_Enable     

        配置I2C应答是否使能,设置为使能则可以发送响应信号。一般配置为允许应答(I2C_Ack_Enable),这是绝大多数遵循I2C标准的设备的通讯要求,改为禁止应答(I2C_Ack_Disable)往往会导致通讯错误。

8.1.6  I2C_AcknowledgeAddress     

        选择I2C的寻址模式是7位还是10位地址。这需要根据实际连接到I2C总线上设备的地址进行选择,这个成员的配置也影响到I2C_OwnAddress1成员,只有这里设置成10位模式时,I2C_OwnAddress1才支持10位地址。

8.2  库函数

        这里只介绍一下都有哪些,后续具体使用的时候在进行讲解,这里在推荐一个官方的固件库文档:

链接: https://pan.baidu.com/s/16L6NxIPSKreqdnLR5kH0Ow?pwd=txjy

提取码: txjy 

        可以查找函数说明:

/* I2C模块初始化与配置函数 */

// I2C外设复位函数 - 将I2C寄存器恢复默认值
void I2C_DeInit(I2C_TypeDef* I2Cx);

// I2C初始化函数 - 根据初始化结构体参数配置I2C
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);

// I2C结构体初始化函数 - 用默认值填充初始化结构体
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);

// I2C使能控制函数 - 开启或关闭I2C外设
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/* DMA相关控制函数 */

// DMA请求使能函数 - 控制I2C的DMA传输功能
void I2C_DMACmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// DMA最后传输控制 - 在DMA最后一次传输时自动产生停止条件
void I2C_DMALastTransferCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/* 总线通信控制函数 */

// 起始条件生成函数 - 在I2C总线上产生起始信号
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 停止条件生成函数 - 在I2C总线上产生停止信号
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 应答配置函数 - 使能或禁用从设备应答功能
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 第二个自身地址配置 - 设置I2C设备的第二个地址(双地址模式)
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);

// 双地址模式控制 - 使能或禁用双地址识别功能
void I2C_DualAddressCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 广播呼叫控制 - 使能或禁用广播呼叫地址识别
void I2C_GeneralCallCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/* 中断与数据传输函数 */

// 中断配置函数 - 使能或禁用特定的I2C中断源
void I2C_ITConfig(I2C_TypeDef* I2Cx, uint16_t I2C_IT, FunctionalState NewState);

// 数据发送函数 - 通过I2C总线发送一个字节数据
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);

// 数据接收函数 - 从I2C总线读取一个字节数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);

// 7位地址发送函数 - 发送7位从设备地址和传输方向
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);

// 寄存器读取函数 - 读取I2C控制状态寄存器值
uint16_t I2C_ReadRegister(I2C_TypeDef* I2Cx, uint8_t I2C_Register);

// 软件复位控制 - 通过软件复位I2C外设
void I2C_SoftwareResetCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

/* 高级通信功能函数 */

// NACK位置配置 - 设置非应答信号的位置(当前/下一个字节)
void I2C_NACKPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_NACKPosition);

// SMBus警报配置 - 配置SMBus警报引脚工作模式
void I2C_SMBusAlertConfig(I2C_TypeDef* I2Cx, uint16_t I2C_SMBusAlert);

// PEC传输控制 - 使能或禁用数据包错误校验传输
void I2C_TransmitPEC(I2C_TypeDef* I2Cx, FunctionalState NewState);

// PEC位置配置 - 设置PEC字节在传输中的位置
void I2C_PECPositionConfig(I2C_TypeDef* I2Cx, uint16_t I2C_PECPosition);

// PEC计算控制 - 使能或禁用PEC校验值计算
void I2C_CalculatePEC(I2C_TypeDef* I2Cx, FunctionalState NewState);

// PEC值获取函数 - 返回当前计算的PEC校验值
uint8_t I2C_GetPEC(I2C_TypeDef* I2Cx);

// ARP协议控制 - 使能或禁用SMBus地址解析协议
void I2C_ARPCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 时钟延长控制 - 使能或禁用I2C时钟延长功能
void I2C_StretchClockCmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 快速模式占空比配置 - 设置快速模式下的时钟占空比
void I2C_FastModeDutyCycleConfig(I2C_TypeDef* I2Cx, uint16_t I2C_DutyCycle);

9.  IIC代码编写

9.1  准备

        可以自己找一个空白工程模版进行移植,这里我使用的是ZET6,使用我自己之前移植的空白模版:

STM32f103ZET6移植工程模版_freertos菜鸟教程资源-优快云下载

        在其中加入串口程序,方便打印数据进行查看,这里为了方便直接移植了江协的串口代码,不熟悉的可以看一下江协的视频:

#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

uint8_t Serial_TxPacket[4];				//定义发送数据包数组,数据包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];				//定义接收数据包数组
uint8_t Serial_RxFlag;					//定义接收数据包标志位

/**
  * 函    数:串口初始化
  * 参    数:无
  * 返 回 值:无
  */
void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA10引脚初始化为上拉输入
	
	/*USART初始化*/
	USART_InitTypeDef USART_InitStructure;					//定义结构体变量
	USART_InitStructure.USART_BaudRate = 9600;				//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;	//模式,发送模式和接收模式均选择
	USART_InitStructure.USART_Parity = USART_Parity_No;		//奇偶校验,不需要
	USART_InitStructure.USART_StopBits = USART_StopBits_1;	//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//字长,选择8位
	USART_Init(USART1, &USART_InitStructure);				//将结构体变量交给USART_Init,配置USART1
	
	/*中断输出配置*/
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);			//开启串口接收数据的中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);			//配置NVIC为分组2
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;					//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		//选择配置NVIC的USART1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;		//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);							//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行
}

/**
  * 函    数:串口发送一个字节
  * 参    数:Byte 要发送的一个字节
  * 返 回 值:无
  */
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

/**
  * 函    数:串口发送一个数组
  * 参    数:Array 要发送数组的首地址
  * 参    数:Length 要发送数组的长度
  * 返 回 值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:串口发送一个字符串
  * 参    数:String 要发送字符串的首地址
  * 返 回 值:无
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
	{
		Serial_SendByte(String[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:次方函数(内部使用)
  * 返 回 值:返回值等于X的Y次方
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	//设置结果初值为1
	while (Y --)			//执行Y次
	{
		Result *= X;		//将X累乘到结果
	}
	return Result;
}

/**
  * 函    数:串口发送数字
  * 参    数:Number 要发送的数字,范围:0~4294967295
  * 参    数:Length 要发送数字的长度,范围:0~10
  * 返 回 值:无
  */
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i ++)		//根据数字长度遍历数字的每一位
	{
		Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');	//依次调用Serial_SendByte发送每位数字
	}
}

/**
  * 函    数:使用printf需要重定向的底层函数
  * 参    数:保持原始格式即可,无需变动
  * 返 回 值:保持原始格式即可,无需变动
  */
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
void Serial_Printf(char *format, ...)
{
	char String[100];				//定义字符数组
	va_list arg;					//定义可变参数列表数据类型的变量arg
	va_start(arg, format);			//从format开始,接收参数列表到arg变量
	vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中
	va_end(arg);					//结束变量arg
	Serial_SendString(String);		//串口发送字符数组(字符串)
}

/**
  * 函    数:串口发送数据包
  * 参    数:无
  * 返 回 值:无
  * 说    明:调用此函数后,Serial_TxPacket数组的内容将加上包头(FF)包尾(FE)后,作为数据包发送出去
  */
void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}

/**
  * 函    数:获取串口接收数据包标志位
  * 参    数:无
  * 返 回 值:串口接收数据包标志位,范围:0~1,接收到数据包后,标志位置1,读取后标志位自动清零
  */
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)			//如果标志位为1
	{
		Serial_RxFlag = 0;
		return 1;					//则返回1,并自动清零标志位
	}
	return 0;						//如果标志位为0,则返回0
}

/**
  * 函    数:USART1中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;		//定义表示当前状态机状态的静态变量
	static uint8_t pRxPacket = 0;	//定义表示当前接收数据位置的静态变量
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)		//判断是否是USART1的接收事件触发的中断
	{
		uint8_t RxData = USART_ReceiveData(USART1);				//读取数据寄存器,存放在接收的数据变量
		
		/*使用状态机的思路,依次处理数据包的不同部分*/
		
		/*当前状态为0,接收数据包包头*/
		if (RxState == 0)
		{
			if (RxData == 0xFF)			//如果数据确实是包头
			{
				RxState = 1;			//置下一个状态
				pRxPacket = 0;			//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据*/
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket] = RxData;	//将数据存入数据包数组的指定位置
			pRxPacket ++;				//数据包的位置自增
			if (pRxPacket >= 4)			//如果收够4个数据
			{
				RxState = 2;			//置下一个状态
			}
		}
		/*当前状态为2,接收数据包包尾*/
		else if (RxState == 2)
		{
			if (RxData == 0xFE)			//如果数据确实是包尾部
			{
				RxState = 0;			//状态归0
				Serial_RxFlag = 1;		//接收数据包标志位置1,成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);		//清除标志位
	}
}
#ifndef __SERIALL_H
#define __SERIALL_H

#include <stdio.h>

extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);

void Serial_SendPacket(void);
uint8_t Serial_GetRxFlag(void);

#endif

        运行试一下能不能用:

9.2  引脚初始化

        首先我们先确定引脚,根据数据手册查找有I2C功能的外设引脚,这里我使用PB6和PB7,可以看出二者使用的是I2C1外设:

        而根据手册框图可以看出,其I2C1是挂载在APB1上的,且GPIOB引脚是挂载在APB2上的:

        则需要使能时钟:

    /* 使能与 I2C 有关的时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);        // 使能I2C1时钟 (APB1)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);       // 使能GPIOB时钟 (APB2)

        对于PB6和PB7的引脚我们前文也说了,需要配置为开漏输出(如果实在不知道引脚需要配置成什么样式的,可以去参考数据手册,第8小的内容),那么引脚初始化代码:

static void I2C_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure; 

    /* 使能与 I2C 有关的时钟 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);        // 使能I2C1时钟 (APB1)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);       // 使能GPIOB时钟 (APB2)
    
    /* 配置 I2C SCL 引脚 (PB6) */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;                   // SCL引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;             // 复用开漏输出
    GPIO_Init(GPIOB, &GPIO_InitStructure);                      // GPIOB端口
    
    /* 配置 I2C SDA 引脚 (PB7) */
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;                   // SDA引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;             // 复用开漏输出
    GPIO_Init(GPIOB, &GPIO_InitStructure);                      // GPIOB端口
}

        但是这样的代码,如果我们后续想要变更引脚,如果代码量多的话不方便维护,我们可以声明一些宏定义:

#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_GPIO_SDA                        RCC_APB2Periph_GPIOB

#define I2C_SCL_PORT                        GPIOB   
#define I2C_SCL_PIN                         GPIO_Pin_6
#define I2C_SDA_PORT                        GPIOB 
#define I2C_SDA_PIN                         GPIO_Pin_7

        将宏定义放到代码当中,如果想要更改引脚,直接对着上面宏定义修改即可:

//I2C I/O配置
static void I2C_GPIO_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure; 

    /* 使能与 I2C 有关的时钟 */
    I2C_APBxClock_FUN(I2C_CLK, ENABLE);                          // 使能I2C1时钟
    I2C_GPIO_APBxClock_FUN(I2C_GPIO_CLK | I2C_GPIO_SDA, ENABLE); // 使能GPIOB时钟
    
    /* 配置 I2C SCL 引脚 (PB6) */
    GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN;                  // GPIO_Pin_6
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;             // 复用开漏输出
    GPIO_Init(I2C_SCL_PORT, &GPIO_InitStructure);               // GPIOB
    
    /* 配置 I2C SDA 引脚 (PB7) */
    GPIO_InitStructure.GPIO_Pin = I2C_SDA_PIN;                  // GPIO_Pin_7
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;             // 复用开漏输出
    GPIO_Init(I2C_SDA_PORT, &GPIO_InitStructure);               // GPIOB
}

9.3  模式配置

        这里的模式配置,其实就是对于I2C_InitTypeDef结构体的配置:

typedef struct
{
  uint32_t I2C_ClockSpeed;          /*!< 指定时钟频率。
                                         该参数必须设置为低于400kHz的值 */

  uint16_t I2C_Mode;                /*!< 指定I2C模式。
                                         该参数可为 @ref I2C_mode 中的值 */

  uint16_t I2C_DutyCycle;           /*!< 指定I2C快速模式下的占空比。
                                         该参数可为 @ref I2C_duty_cycle_in_fast_mode 中的值 */

  uint16_t I2C_OwnAddress1;         /*!< 指定设备第一个自身地址。
                                         该参数可为7位或10位地址 */

  uint16_t I2C_Ack;                 /*!< 使能或禁用应答功能。
                                         该参数可为 @ref I2C_acknowledgement 中的值 */

  uint16_t I2C_AcknowledgedAddress; /*!< 指定应答地址类型为7位或10位。
                                         该参数可为 @ref I2C_acknowledged_address 中的值 */
}I2C_InitTypeDef;

        我们来一个一个进行配置,首先是速度,我们这里直接写到400KHz:

I2C_InitStructure.I2C_ClockSpeed = 400000;                  // 400kHz速率

        然后是模式,我们跳转看一下,可选模式如下:

/** @defgroup I2C_mode 
  * @{
  */

#define I2C_Mode_I2C                    ((uint16_t)0x0000)      /*!< 标准I2C模式 - 支持标准I2C通信协议 */
#define I2C_Mode_SMBusDevice            ((uint16_t)0x0002)      /*!< SMBus设备模式 - 作为SMBus从设备工作 */  
#define I2C_Mode_SMBusHost              ((uint16_t)0x000A)      /*!< SMBus主机模式 - 作为SMBus主设备工作 */
#define IS_I2C_MODE(MODE) (((MODE) == I2C_Mode_I2C) || \
                           ((MODE) == I2C_Mode_SMBusDevice) || \
                           ((MODE) == I2C_Mode_SMBusHost))

        这里我们就需要IIC模式即可:

I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;                   // I2C模式

        然后是占空比,也就是高低电平的时间,我们知道有两种:

#define I2C_DutyCycle_16_9              ((uint16_t)0x4000) /*!< I2C fast mode Tlow/Thigh = 16/9 */
#define I2C_DutyCycle_2                 ((uint16_t)0xBFFF) /*!< I2C fast mode Tlow/Thigh = 2 */
#define IS_I2C_DUTY_CYCLE(CYCLE) (((CYCLE) == I2C_DutyCycle_16_9) || \
                                  ((CYCLE) == I2C_DutyCycle_2))

        两种差别其实不到,这里随便选择一个:

I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;

        对于地址我们可以选择7或者10位,这里我选择7位的,地址随便取一个值即可,主要为了区分不同的IIC设备,相当于给不同设备取个名字:

I2C_InitStructure.I2C_OwnAddress1 = 0x0A; 

        使能或者禁用应答功能,这里我使能应答:

I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
/** @defgroup I2C_acknowledgement
  * @{
  */

#define I2C_Ack_Enable                  ((uint16_t)0x0400)
#define I2C_Ack_Disable                 ((uint16_t)0x0000)
#define IS_I2C_ACK_STATE(STATE) (((STATE) == I2C_Ack_Enable) || \
                                 ((STATE) == I2C_Ack_Disable))

        寻址模式也是7位的:

I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

        完整的:

// I2C 工作模式配置 - 无宏定义版本
static void I2C_Mode_Configu(void)
{
    I2C_InitTypeDef I2C_InitStructure; 

    /* I2C 配置 */
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;                   // I2C模式
	
    /* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;          // 快速模式占空比 16:9
	
    /* 设置I2C设备自身地址(7位地址模式) */
    I2C_InitStructure.I2C_OwnAddress1 = 0x0A;                   // 设备地址为0x0A
	 
    /* 使能应答 */
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;                 // 使能应答
	
    /* I2C的寻址模式 - 7位地址模式 */
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	
    /* 通信速率 - 400kHz 快速模式 */
    I2C_InitStructure.I2C_ClockSpeed = 400000;                  // 400kHz速率

    /* I2C1 初始化 */
    I2C_Init(I2C1, &I2C_InitStructure);
  
    /* 使能 I2C1 */
    I2C_Cmd(I2C1, ENABLE);  
}

        对于一些可能更改的值,取几个宏定义:

#define I2Cx                                I2C1
#define I2C_Speed                           400000
#define I2Cx_OWN_ADDRESS7                   0X0A 
static void I2C_Mode_Configu(void)
{
  I2C_InitTypeDef  I2C_InitStructure; 

  /* I2C 配置 */
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	
	/* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	
  I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7; 
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
	 
	/* I2C的寻址模式 */
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	
	/* 通信速率 */
  I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;	

	/* I2C 初始化 */
  I2C_Init(I2Cx, &I2C_InitStructure);
  
	/* 使能 I2C */
  I2C_Cmd(I2Cx, ENABLE);  
}

9.4  初始化

        把所有的初始化放到一起,方便管理:

//I2C外设初始化
void I2C_Config(void)
{
  I2C_GPIO_Config(); 
 
  I2C_Mode_Config();
}

9.5  写一个字节数据

        因为我们上面配置的是七位模式,找到这张图:

        根据上图进行配置逻辑,首先是产生起始信号,找到 I2C_GenerateSTART 函数,起作用是对I2CX产生起始信号:

/**
  * @brief  产生I2Cx通信起始条件。
  * @param  I2Cx: 其中x可以是1或2,用于选择I2C外设。
  * @param  NewState: I2C起始条件生成的新状态。
  *   该参数可以是: ENABLE 或 DISABLE。
  * @retval 无。
  */
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState)
{
  /* 检查参数有效性 */
  assert_param(IS_I2C_ALL_PERIPH(I2Cx));          // 验证I2C外设参数
  assert_param(IS_FUNCTIONAL_STATE(NewState));    // 验证功能状态参数
  
  if (NewState != DISABLE)
  {
    /* 产生START起始条件 */
    // 设置CR1控制寄存器的START位(通常为第8位)
    // 这将使I2C硬件在总线上产生起始信号(SDA在SCL高电平时从高到低跳变)
    I2Cx->CR1 |= CR1_START_Set;                   // CR1_START_Set 通常定义为 0x0100
  }
  else
  {
    /* 禁用START条件生成 */
    // 清除CR1控制寄存器的START位
    // 在起始条件已产生后,硬件会自动清除此位
    I2Cx->CR1 &= CR1_START_Reset;                 // CR1_START_Reset 通常定义为 0xFEFF
  }
}

        这里我们配置为:

	I2C_GenerateSTART(I2Cx,ENABLE);

        然后找到 I2C 事件检查函数 I2C_CheckEvent ,其作用是检查最近发生的I2Cx事件是否与传入的参数相等,这样我们就可以判断当前是否产生对于的事件,如果成功返回SUCCESS,如果失败返回ERROR:

/**
  * @brief  检查最近发生的I2Cx事件是否与传入的参数相等。
  * @param  I2Cx: 其中x可以是1或2,用于选择I2C外设。
  * @param  I2C_EVENT: 指定要检查的事件。
  *     该参数可以是以下值之一:
  *     @arg I2C_EVENT_SLAVE_TRANSMITTER_ADDRESS_MATCHED           : EV1 - 从设备发送器地址匹配
  *     @arg I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED              : EV1 - 从设备接收器地址匹配  
  *     @arg I2C_EVENT_SLAVE_TRANSMITTER_SECONDADDRESS_MATCHED     : EV1 - 从设备发送器第二地址匹配
  *     @arg I2C_EVENT_SLAVE_RECEIVER_SECONDADDRESS_MATCHED        : EV1 - 从设备接收器第二地址匹配
  *     @arg I2C_EVENT_SLAVE_GENERALCALLADDRESS_MATCHED            : EV1 - 从设备广播呼叫地址匹配
  *     @arg I2C_EVENT_SLAVE_BYTE_RECEIVED                         : EV2 - 从设备字节接收完成
  *     @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_DUALF)      : EV2 - 从设备字节接收完成(双地址模式)
  *     @arg (I2C_EVENT_SLAVE_BYTE_RECEIVED | I2C_FLAG_GENCALL)    : EV2 - 从设备字节接收完成(广播呼叫模式)
  *     @arg I2C_EVENT_SLAVE_BYTE_TRANSMITTED                      : EV3 - 从设备字节发送完成
  *     @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_DUALF)   : EV3 - 从设备字节发送完成(双地址模式)
  *     @arg (I2C_EVENT_SLAVE_BYTE_TRANSMITTED | I2C_FLAG_GENCALL) : EV3 - 从设备字节发送完成(广播呼叫模式)
  *     @arg I2C_EVENT_SLAVE_ACK_FAILURE                           : EV3_2 - 从设备应答失败
  *     @arg I2C_EVENT_SLAVE_STOP_DETECTED                         : EV4 - 从设备检测到停止条件
  *     @arg I2C_EVENT_MASTER_MODE_SELECT                          : EV5 - 主设备模式选择
  *     @arg I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED            : EV6 - 主设备发送器模式选择    
  *     @arg I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED               : EV6 - 主设备接收器模式选择
  *     @arg I2C_EVENT_MASTER_BYTE_RECEIVED                        : EV7 - 主设备字节接收完成
  *     @arg I2C_EVENT_MASTER_BYTE_TRANSMITTING                    : EV8 - 主设备字节正在发送
  *     @arg I2C_EVENT_MASTER_BYTE_TRANSMITTED                     : EV8_2 - 主设备字节发送完成
  *     @arg I2C_EVENT_MASTER_MODE_ADDRESS10                       : EV9 - 主设备10位地址模式
  *     
  * @注意: 关于事件的详细描述,请参考stm32f10x_i2c.h文件中的I2C_Events部分。
  *    
  * @retval 一个ErrorStatus枚举值:
  * - SUCCESS: 最近的事件等于I2C_EVENT
  * - ERROR: 最近的事件不同于I2C_EVENT
  */
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
  uint32_t lastevent = 0;    // 存储组合后的事件状态
  uint32_t flag1 = 0, flag2 = 0;  // 分别存储SR1和SR2状态寄存器的值
  ErrorStatus status = ERROR;     // 默认返回错误状态

  /* 检查参数有效性 */
  assert_param(IS_I2C_ALL_PERIPH(I2Cx));      // 验证I2C外设参数
  assert_param(IS_I2C_EVENT(I2C_EVENT));      // 验证事件参数

  /* 读取I2Cx状态寄存器 */
  flag1 = I2Cx->SR1;        // 读取状态寄存器1 - 包含主要状态标志
  flag2 = I2Cx->SR2;        // 读取状态寄存器2 - 包含附加状态信息
  flag2 = flag2 << 16;      // 将SR2的值左移16位,为组合做准备

  /* 从I2C状态寄存器获取最近的事件值 */
  // 将SR1和移位后的SR2进行或运算,然后与标志掩码进行与运算
  // FLAG_Mask通常用于过滤掉不相关的位
  lastevent = (flag1 | flag2) & FLAG_Mask;

  /* 检查最近的事件是否包含指定的I2C_EVENT */
  if ((lastevent & I2C_EVENT) == I2C_EVENT)
  {
    /* 成功: 最近的事件等于I2C_EVENT */
    status = SUCCESS;
  }
  else
  {
    /* 错误: 最近的事件不同于I2C_EVENT */
    status = ERROR;
  }
  
  /* 返回状态 */
  return status;
}

        根据官方流程图我们可以看出,当我们完成起始信号后会产生一个 EV5 事件,我们通过该函数判断此时是否是 EV5 事件,传递的参数为:

  *     @arg I2C_EVENT_MASTER_MODE_SELECT                          : EV5

        通过while循环判断是否是输出错误,如果一直失败则一直循环:

	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);

        如果跳出循环代表正常产生了EV5事件,然后需要发送设备地址:

/**
  * @brief  发送地址字节以选择从设备。
  * @param  I2Cx: 其中x可以是1或2,用于选择I2C外设。
  * @param  Address: 指定要传输的从设备地址(7位地址)
  * @param  I2C_Direction: 指定I2C设备将作为发送器还是接收器。
  *   该参数可以是以下值之一:
  *     @arg I2C_Direction_Transmitter: 发送器模式
  *     @arg I2C_Direction_Receiver: 接收器模式
  * @retval 无。
  */
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction)
{
  /* 检查参数有效性 */
  assert_param(IS_I2C_ALL_PERIPH(I2Cx));              // 验证I2C外设参数
  assert_param(IS_I2C_DIRECTION(I2C_Direction));      // 验证传输方向参数
  
  /* 根据传输方向设置/清除读/写位 */
  if (I2C_Direction != I2C_Direction_Transmitter)
  {
    /* 设置地址的第0位为1,表示读操作 */
    // 在7位地址模式下,地址字节的第0位用于表示读写方向:
    // - 0: 写操作 (主设备 -> 从设备)
    // - 1: 读操作 (从设备 -> 主设备)
    Address |= OAR1_ADD0_Set;                         // OAR1_ADD0_Set 通常定义为 0x01
  }
  else
  {
    /* 清除地址的第0位为0,表示写操作 */
    Address &= OAR1_ADD0_Reset;                       // OAR1_ADD0_Reset 通常定义为 0xFE
  }
  
  /* 将完整的8位地址(7位地址 + 1位方向)发送到数据寄存器 */
  I2Cx->DR = Address;
}

        这里宏定义一个地址方便后续更改:

#define I2C_Send_Resive_ADDR 0xA0

        配置为发送器模式:

	I2C_Send7bitAddress(I2Cx,I2C_Send_Resive_ADDR,I2C_Direction_Transmitter);

注意在库函数中,其实发送数据已经封装了接收应答,因此这里应答位其实不需要做处理。

        这里等待EV6事件:

        还是调用 I2C_CheckEvent 函数,通过while循环等待发送EV6事件:

  while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) == ERROR);

        后面EV8_1事件,因为移位寄存器和数据寄存器为空,直接写DR寄存器,因此不需要等待:

        检测到EV6事件完成后,通过SendData发送数据:

/**
  * @brief  通过I2Cx外设发送一个数据字节。
  * @param  I2Cx: 其中x可以是1或2,用于选择I2C外设。
  * @param  Data: 要传输的字节数据。
  * @retval 无
  */
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data)
{
  /* 检查参数有效性 */
  assert_param(IS_I2C_ALL_PERIPH(I2Cx));    // 验证I2C外设参数
  
  /* 将待发送的数据写入数据寄存器(DR) */
  I2Cx->DR = Data;                          // 将数据字节写入I2C数据寄存器
}

        对于发送addr由于后续想要写入EEPROM,其第一个数据默认为想要存储的地址,这里就在声明的时候传递一个参数addr,然后等待EV8事件完成:

	//EV6事件被检测到,发送要操作的存储单元地址
	I2C_SendData (I2Cx,addr);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTING ) == ERROR);

        然后当我们最后一个数据发送完成需要等待EV8_2事件:

  //EV8事件被检测到,发送要存储的数据
	I2C_SendData (I2Cx,data);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED ) == ERROR);

        然后结束:

	//数据传输完成
	I2C_GenerateSTOP(I2Cx,ENABLE);	

        完整代码:

//写入一个字节
void I2C_Byte_Write(uint8_t addr,uint8_t data)
{
	//产生起始信号
	I2C_GenerateSTART(I2Cx,ENABLE);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);
	
	//EV5事件被检测到,发送设备地址
	I2C_Send7bitAddress(I2Cx,I2C_Send_Resive_ADDR,I2C_Direction_Transmitter);
	
    while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) == ERROR);

	//EV6事件被检测到,发送要操作的存储单元地址
	I2C_SendData (I2Cx,addr);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTING ) == ERROR);

  //EV8事件被检测到,发送要存储的数据
	I2C_SendData (I2Cx,data);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED ) == ERROR);
	
	//数据传输完成
	I2C_GenerateSTOP(I2Cx,ENABLE);		
}

9.6  写入多个字节数据

        写入多个字节,就是多次循环进行数据写入,直到数据完成:

	I2C_SendData (I2Cx,data);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED ) == ERROR);

        其他都一样只是对数据写入增加了一个while循环:

//写入多个字节
void I2C_Write(uint8_t addr,uint8_t *data,uint8_t numByteToWrite)
{
	//产生起始信号
	I2C_GenerateSTART(I2Cx,ENABLE);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);
	
	//EV5事件被检测到,发送设备地址
	I2C_Send7bitAddress(I2Cx,I2C_Send_Resive_ADDR,I2C_Direction_Transmitter);
	
  while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) == ERROR);

	//EV6事件被检测到,发送要操作的存储单元地址
	I2C_SendData (I2Cx,addr);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTING ) == ERROR);

	
	while(numByteToWrite)
	{
		//EV8事件被检测到,发送要存储的数据
		I2C_SendData (I2Cx,*data);
		
		while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTED ) == ERROR);
		
		data++;
		numByteToWrite--;
			
	}
	//数据传输完成
	I2C_GenerateSTOP(I2Cx,ENABLE);	

}

9.7  读取数据

        这里因为没有别的IIC设备,因此使用的是EEPROM,EEPROM的读取操作需要两次独立的I2C传输,第一次传输:设置读取地址(写操作),第二次传输:实际读取数据(读操作),

void I2C_Read(uint8_t addr,uint8_t *data,uint8_t numByteToRead)
{
	//产生起始信号
	I2C_GenerateSTART(I2Cx,ENABLE);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);
	
	//EV5事件被检测到,发送设备地址
	I2C_Send7bitAddress(I2Cx,I2C_Send_Resive_ADDR,I2C_Direction_Transmitter);
	
  while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ) == ERROR);

	//EV6事件被检测到,发送要操作的存储单元地址
	I2C_SendData (I2Cx,addr);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_TRANSMITTING ) == ERROR);

	
	//第二次起始信号
	//产生起始信号
	I2C_GenerateSTART(I2Cx,ENABLE);
	
	while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_MODE_SELECT) == ERROR);
	
	//EV5事件被检测到,发送设备地址
	I2C_Send7bitAddress(I2Cx,I2C_Send_Resive_ADDR,I2C_Direction_Receiver);
	
  while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED ) == ERROR);


	while(numByteToRead)
	{
		if(numByteToRead == 1)
		{		
			//如果为最后一个字节
			I2C_AcknowledgeConfig (I2Cx,DISABLE);
		}		
		
		//EV7事件被检测到	
		while(I2C_CheckEvent(I2Cx,I2C_EVENT_MASTER_BYTE_RECEIVED ) == ERROR);

		//EV7事件被检测到,即数据寄存器有新的有效数据	
		*data = I2C_ReceiveData(I2Cx);
		
		data++;
		
		numByteToRead--;
		
	}
	
	
	//数据传输完成
	I2C_GenerateSTOP(I2Cx,ENABLE);	
	
	//重新配置ACK使能,以便下次通讯
	I2C_AcknowledgeConfig (I2Cx,ENABLE);

}

9.8  等待函数

        EEPROM在接收到写入命令后,不会立即完成数据写入,而是需要一定的内部处理时间,因此需要建立一个等待:

void EEPROM_WaitForWriteEnd(void)
{
	
	do
	{
		//产生起始信号
		I2C_GenerateSTART(I2Cx,ENABLE);
		
		while(I2C_GetFlagStatus (I2Cx,I2C_FLAG_SB) == RESET);
		
		//EV5事件被检测到,发送设备地址
		I2C_Send7bitAddress(I2Cx,I2C_Send_Resive_ADDR,I2C_Direction_Transmitter);
	}  
	while(I2C_GetFlagStatus (I2Cx,I2C_FLAG_ADDR) == RESET );

	//EEPROM内部时序完成传输完成
	I2C_GenerateSTOP(I2Cx,ENABLE);	
}

9.9  主函数

        写入读取看一下:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

#include "LED.h"
#include "Serial.h"
#include "I2C_Test.h"

uint8_t read_buff[10];

int main(void)
{
	LED_GPIO_Config();
	Serial_Init();

	I2C_Config();

	printf("这是一个测试\r\n");

	I2C_Byte_Write(11,0x55);

	EEPROM_WaitForWriteEnd();

	I2C_Read(11,read_buff,1);

	printf("输出数据:%x",read_buff[0]);

	while (1)
	{

	}
}

        可以发现数据正常写入:

完整工程:

基于STM32的硬件IIC读取EEPROM.zip资源-优快云下载

STM32学习笔记_时光の尘的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值