
目录
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均要设置成开漏输出模式。这意味着设备可以将线拉低(输出低电平),但是只能释放线,而不能将线拉高(输出高电平)。这种模式使得多个设备可以共享总线,避免冲突,对于为什么使用开漏输出模式可以参考如下两个链接内容:
嵌入式面试八股文(一)·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,使用我自己之前移植的空白模版:
在其中加入串口程序,方便打印数据进行查看,这里为了方便直接移植了江协的串口代码,不熟悉的可以看一下江协的视频:
#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)
{
}
}
可以发现数据正常写入:

完整工程:


6977

被折叠的 条评论
为什么被折叠?



