野火学习笔记(15) —— I2C —— 读写 EEPROM

这里写目录标题

  • 1. I2C 协议简介
    • 1.1 I2C 物理层
    • 1.2 协议层
      • 1.2.1 I2C 基本读写过程
      • 1.2.2 通讯的起始和停止信号
      • 1.2.3 数据有效性
      • 1.2.4 地址及数据方向
      • 1.2.5 响应
  • 2. STM32 的 I2C 特性及架构
    • 2.1 STM32 的 I2C 外设简介
    • 2.2 STM32 的 I2C 架构剖析
      • 2.2.1 通讯引脚
      • 2.2.2 时钟控制逻辑
      • 2.2.3 数据控制逻辑
      • 2.2.4 整体控制逻辑
    • 2.3 通讯过程
      • 2.3.1 主发送器
      • 2.3.2 主接收器
  • 3. I2C 初始化结构体详解
  • 4. I2C — 读写 EEPROM 实验
    • 4.1 硬件设计
    • 4.2 软件设计
      • 4.2.1 编程要点
      • 4.2.2 代码分析
        • 4.2.2.1 硬件 I2C 例程
        • 4.2.2.2 模拟 I2C 例程
        • 4.2.2.3 注意事项
  • 5. 附属




1. I2C 协议简介

I2C 通讯协议 (Inter - Integrated Circuit) 是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、 CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路 (IC) 间的通讯。

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

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


1.1 I2C 物理层

I2C 通讯设备之间的常用连接方式见 图常见的 I2C 通讯系统。


野火学习笔记(15) —— I2C —— 读写 EEPROM_第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 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。


1.2.1 I2C 基本读写过程

先看看 I2C 通讯过程的基本结构,它的通讯过程见 图主机写数据到从机 、图主机由从机中读数据 及 图 I2C 通讯复合格式。


在这里插入图片描述


野火学习笔记(15) —— I2C —— 读写 EEPROM_第2张图片


野火学习笔记(15) —— I2C —— 读写 EEPROM_第3张图片


这些图表示的是主机和从机通讯时, SDA 线的数据包序列。

其中 S 表示由主机的 I2C 接口产生的传输起始信号 (S),这时连接到 I2C 总线上的所有从机都会接收到这个信号。

起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号 (SLAVE_ADDRESS)。在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据 I2C 协议,这个从机地址可以是 7 位或 10 位。

在地址位之后,是传输方向的选择位,该位为 0 时,表示后面的数据传输方向是由主机传输至从机,即主机向从机写数据。该位为 1 时,则相反,即主机由从机读数据。

从机接收到匹配的地址后,主机或从机会返回一个应答 (ACK) 或非应答 (NACK) 信号,只有接收到应答信号后,主机才能继续发送或接收数据。

写数据

若配置的方向传输位为“写数据”方向,即第一幅图的情况,广播完地址,接收到应答信号后,主机开始正式向从机传输数据 (DATA),数据包的大小为 8 位,主机每发送完一个字节数据,都要等待从机的应答信号 (ACK),重复这个过程,可以向从机传输 N 个数据,这个 N 没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号 (P),表示不再传输数据。

读数据

若配置的方向传输位为“读数据”方向,即第二幅图的情况,广播完地址,接收到应答信号后,从机开始向主机返回数据 (DATA),数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号 (ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号 (NACK),则从机自动停止数据传输。

读和写数据

除了基本的读写, I2C 通讯更常用的是复合格式,即第三幅图的情况,该传输过程有两次起始信号 (S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址 (注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

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


1.2.2 通讯的起始和停止信号

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


野火学习笔记(15) —— I2C —— 读写 EEPROM_第4张图片


1.2.3 数据有效性

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


野火学习笔记(15) —— I2C —— 读写 EEPROM_第5张图片


每次数据传输都以字节为单位,每次传输的字节数不受限制。


1.2.4 地址及数据方向

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


野火学习笔记(15) —— I2C —— 读写 EEPROM_第6张图片


读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,写数据方向时, SDA 由主机控制,从机接收信号。


1.2.5 响应

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


野火学习笔记(15) —— I2C —— 读写 EEPROM_第7张图片


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


2. STM32 的 I2C 特性及架构

如果我们直接控制 STM32 的两个 GPIO 引脚,分别用作 SCL 及 SDA,按照上述信号的时序要求,直接像控制 LED 灯那样控制引脚的输出 (若是接收数据时则读取 SDA 电平),就可以实现 I2C 通讯。同样,假如我们按照 USART 的要求去控制引脚,也能实现 USART 通讯。所以只要遵守协议,就是标准的通讯,不管您如何实现它,不管是 ST 生产的控制器还是 ATMEL 生产的存储器,都能按通讯标准交互。

由于直接控制 GPIO 引脚电平产生通讯时序时,需要由 CPU 控制每个时刻的引脚状态,所以称之为“软件模拟协议”方式。

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


2.1 STM32 的 I2C 外设简介

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


2.2 STM32 的 I2C 架构剖析


野火学习笔记(15) —— I2C —— 读写 EEPROM_第8张图片


2.2.1 通讯引脚

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


在这里插入图片描述


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 信号线的输出时钟公式如下:

标准模式:

T(high) = CCR * T(PCKL1) T(low) = CCR * T(PCLK1)

快速模式中 T:sub:‘low‘/T:sub:‘high‘=2 时:

T(high) = CCR * T(PCKL1) T(low) = 2 * CCR * T(PCKL1)

快速模式中 T:sub:‘low‘/T:sub:‘high‘=16/9 时:

T(high) = 9 * CCR * T(PCKL1) T(low) = 16 * CCR * T(PCKL1)

例如,我们的 PCLK1 = 36MHz,想要配置 400Kbit/s 的速率,计算方式如下:

PCLK 时钟周期: TPCLK1 = 1 / 36000000

目标 SCL 时钟周期: TSCL = 1 / 400000

SCL 时钟周期内的高电平时间: THIGH = TSCL / 3

SCL 时钟周期内的低电平时间: TLOW = 2 * TSCL / 3

计算结果得出 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 通讯的主机端时,向外发送数据时的过程。


野火学习笔记(15) —— I2C —— 读写 EEPROM_第9张图片


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

(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 设备产生一个停止信号 (P),这个时候会产生 EV8_2 事件, SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。

假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。


2.3.2 主接收器

再来分析主接收器过程,即作为 I2C 通讯的主机端时,从外部接收数据的过程,见 图主接收器过程。


野火学习笔记(15) —— I2C —— 读写 EEPROM_第10张图片


主接收器接收流程及事件说明如下:

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

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

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

(4) 发送非应答信号后,产生停止信号 (P),结束传输。在发送和接收过程中,有的事件不只是标志了我们上面提到的状态位,还可能同时标志主机状态之类的状态位,而且读了之后还需要清除标志位,比较复杂。我们可使用 STM32 标准库函数来直接检测这些事件的复合标志,降低编程难度。


3. I2C 初始化结构体详解

跟其它外设一样, STM32 标准库提供了 I2C 初始化结构体及初始化函数来配置 I2C 外设。初始化结构体及函数定义在库文件 “stm32f10x_i2c.h” 及 “stm32f10x_i2c.c” 中,编程时我们可以结合这两个文件内的注释使用或参考库帮助文档。了解初始化结构体后我们就能对 I2C 外设运用自如了,见 代码清单:I2C-1。


列表 1: 代码清单:I2C-1 I2C 初始化结构体

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;

这些结构体成员说明如下,其中括号内的文字是对应参数在 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. I2C — 读写 EEPROM 实验

EEPROM 是一种掉电后数据不丢失的存储器,常用来存储一些配置信息,以便系统重新上电的时候加载之。 EEPOM 芯片最常用的通讯方式就是 I2C 协议,本小节以 EEPROM 的读写实验为大家讲解 STM32 的 I2C 使用方法。实验中 STM32 的 I2C 外设采用主模式,分别用作主发送器和主接收器,通过查询事件的方式来确保正常通讯。


4.1 硬件设计


野火学习笔记(15) —— I2C —— 读写 EEPROM_第11张图片


本实验板中的 EEPROM 芯片 (型号: AT24C02) 的 SCL 及 SDA 引脚连接到了 STM32 对应的 I2C 引脚中,结合上拉电阻,构成了 I2C 通讯总线,它们通过 I2C 总线交互。 EEPROM 芯片的设备地址一共有 7 位,其中高 4 位固定为: 1010 b,低 3 位则由 A0/A1/A2 信号线的电平决定,见 图 EEPROM 设备地址 ,图中的 R/W 是读写方向位,与地址无关。


野火学习笔记(15) —— I2C —— 读写 EEPROM_第12张图片


按照我们此处的连接, A0/A1/A2 均为 0,所以 EEPROM 的 7 位设备地址是: 101 0000b,即 0x50。由于 I2C 通讯时常常是地址跟读写方向连在一起构成一个 8 位数,且当 R/W 位为 0 时,表示写方向,所以加上 7 位地址,其值为 “0xA0”,常称该值为 I2C 设备的“写地址”;当 R/W 位为 1 时,表示读方向,加上 7 位地址,其值为“0xA1”,常称该值为“读地址”。

EEPROM 芯片中还有一个 WP 引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据,我们直接接地,不使用写保护功能。

关于 EEPROM 的更多信息,可参考其数据手册《AT24C02》来了解。若您使用的实验板 EEPROM 的型号、设备地址或控制引脚不一样,只需根据我们的工程修改即可,程序的控制原理相同。


4.2 软件设计

为了使工程更加有条理,我们把读写 EEPROM 相关的代码独立分开存储,方便以后移植。在“工程模板”之上新建 “bsp_i2c_ee.c” 及 “bsp_i2c_ee.h” 文件,这些文件也可根据您的喜好命名,它们不属于 STM32 标准库的内容,是由我们自己根据应用需要编写的。


4.2.1 编程要点

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

(2) 使能 I2C 外设的时钟;

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

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

(5) 编写读写 EEPROM 存储内容的函数;

(6) 编写测试程序,对读写数据进行校验。


4.2.2 代码分析

本代码用到的库有:

#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_usart.h"
#include "stm32f10x_i2c.h"


4.2.2.1 硬件 I2C 例程

uart.h

#ifndef __UART_H
#define __UART_H	


#include "stm32f10x.h"
#include 


void UART_GPIO_Init(void);
void UART_Config(u32 bound);
void UART_NVIC_Init(void);
void UART_Init(void);
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch);
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num);
void Usart_SendString( USART_TypeDef * pUSARTx, char *str);
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch);


#endif


uart.c

#include "uart.h"


/*******************************************************************************
* 函 数 名         : UART_GPIO_Init
* 函数功能		   : UART GPIO 初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_GPIO_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	
	// 使能 PA 端口时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	// USART1_TX  GPIOA.9
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 
	// 复用推挽输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	
	// IO 口速度为 50MHz	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	// 根据设定参数初始化
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// USART1_RX  GPIOA.10初始化
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	// 浮空输入模式
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	// 根据设定参数初始化
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}


/*******************************************************************************
* 函 数 名         : UART_Config
* 函数功能		   : UART 配置初始化函数
* 输    入         : 串口波特率
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_Config(u32 bound)
{
	USART_InitTypeDef USART_InitStructure;
	
	// 使能 USART1 时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	// 串口波特率
	USART_InitStructure.USART_BaudRate = bound;
	// 字长为8位数据格式
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	// 一个停止位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	// 无奇偶校验位
	USART_InitStructure.USART_Parity = USART_Parity_No;
	// 无硬件数据流控制
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	// 收发模式
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	
	// 初始化串口1
	USART_Init(USART1, &USART_InitStructure); 
}


/*******************************************************************************
* 函 数 名         : UART_NVIC_Init
* 函数功能		   : UART NVIC 初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_NVIC_Init(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	
	// Usart1 NVIC 配置
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	// 抢占优先级 3
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
	// 子优先级 3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
	// IRQ 通道使能
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	// 根据指定的参数初始化 NVIC 寄存器
	NVIC_Init(&NVIC_InitStructure);	
	
	// 开启串口接收中断
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}


/*******************************************************************************
* 函 数 名         : UART_Init
* 函数功能		   : UART 总初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_Init(void)
{
	// UART GPIO 初始化
	UART_GPIO_Init();
	// UART 配置初始化,波特率为 9600
	UART_Config(9600);
	// UART NVIC 初始化
	UART_NVIC_Init();
	
	// 使能串口1 
	USART_Cmd(USART1, ENABLE);
}



/*******************************************************************************
* 函 数 名         : Usart_SendByte
* 函数功能		   : Usart 发送一个字节
* 输    入         : 
					pUSARTx:串口
					ch:字节
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendByte(USART_TypeDef * pUSARTx, uint8_t ch)
{
	// 发送一个字节数据到 USART
	USART_SendData(pUSARTx,ch);
		
	// 等待发送数据寄存器为空
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
}


/*******************************************************************************
* 函 数 名         : Usart_SendArray
* 函数功能		   : Usart 发送数组
* 输    入         : 
					pUSARTx:串口
					array:数组头
					num:数组个数
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendArray(USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num)
{
	uint8_t i;
	
	for(i=0; i<num; i++)
	{
		// 发送一个字节数据到 USART
		Usart_SendByte(pUSARTx,array[i]);	
	}
	
	// 等待发送完成
	while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET);
}


/*******************************************************************************
* 函 数 名         : Usart_SendString
* 函数功能		   : Usart 发送字符串
* 输    入         : 
					pUSARTx:串口
					str:字符串
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendString(USART_TypeDef * pUSARTx, char *str)
{
	unsigned int k = 0;
	
	do 
	{
		Usart_SendByte( pUSARTx, *(str + k) );
		k++;
	}while(*(str + k)!='\0');
  
	// 等待发送完成
	while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET);
}


/*******************************************************************************
* 函 数 名         : Usart_SendHalfWord
* 函数功能		   : Usart 发送一个16位数
* 输    入         : 
					pUSARTx:串口
					ch:16位数
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendHalfWord(USART_TypeDef * pUSARTx, uint16_t ch)
{
	uint8_t temp_h, temp_l;
	
	// 取出高八位
	temp_h = (ch&0XFF00)>>8;
	// 取出低八位
	temp_l = ch&0XFF;
	
	// 发送高八位
	USART_SendData(pUSARTx,temp_h);	
	// 等待发送完毕
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
	
	// 发送低八位
	USART_SendData(pUSARTx,temp_l);	
	// 等待发送完毕
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
}



/*******************************************************************************
* 函 数 名         : fputc
* 函数功能		   : 重定向 c 库函数 printf 到串口,重定向后可使用 printf 函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
int fputc(int ch, FILE *f)
{
	// 发送一个字节数据到串口
	USART_SendData(USART1, (uint8_t) ch);
	
	// 等待发送完毕
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);		

	return (ch);
}


/*******************************************************************************
* 函 数 名         : fgetc
* 函数功能		   : 重定向 c 库函数 scanf 到串口,重写向后可使用 scanf、getchar 等函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
int fgetc(FILE *f)
{
	// 等待串口输入数据
	while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);

	return (int)USART_ReceiveData(USART1);
}



/*******************************************************************************
* 函 数 名         : USART1_IRQHandler
* 函数功能		   : USART1 中断函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :每接收到一个数据就发送出去
*******************************************************************************/
void USART1_IRQHandler(void)
{
	uint8_t ucTemp;
	
	if(USART_GetITStatus(USART1, USART_IT_RXNE)!= RESET)
	{
		// 接收数据
		ucTemp = USART_ReceiveData(USART1);
		// 发送接收到的数据
		USART_SendData(USART1,ucTemp);    
	}
}


iic.h

#ifndef __IIC_H
#define __IIC_H


#include "stm32f10x.h"


// 这个地址只要与 STM32 外挂的 I2C 器件地址不一样即可
#define I2Cx_OWN_ADDRESS7 0X0A


// EEPROM Addresses defines
#define EEPROM_Block0_ADDRESS 0xA0


extern uint16_t EEPROM_ADDRESS;


extern void I2C_GPIO_Config(void);
extern void I2C_Mode_Config(void);
extern void I2C_EE_Init(void);


#endif


iic.c

#include "iic.h"


uint16_t EEPROM_ADDRESS;


/*******************************************************************************
* 函 数 名         : I2C_GPIO_Config
* 函数功能		   : I2C GPIO 初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure; 
	
	// 使能 PB 端口时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	// I2C1_SCL  GPIOB.6
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	// 复用开漏输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	// IO 口速度为 50MHz	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	// 根据设定参数初始化
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	// I2C1_SDA  GPIOB.7
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
	// 复用开漏输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	// IO 口速度为 50MHz	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	// 根据设定参数初始化
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}


/*******************************************************************************
* 函 数 名         : I2C_Mode_Configu
* 函数功能		   : I2C 工作模式配置函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_Mode_Config(void)
{
	I2C_InitTypeDef I2C_InitStructure; 
	
	// 使能 I2C 时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
	
	// 设置 I2C 为 I2C 模式
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
	// I2C 快速模式 Tlow / Thigh = 2
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
	// 设置第一个设备自身地址
	I2C_InitStructure.I2C_OwnAddress1 = I2Cx_OWN_ADDRESS7;
	// 使能应答(ACK)
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
	// 应答 7 位地址
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	// 设置时钟频率 400KHz
	I2C_InitStructure.I2C_ClockSpeed = 400000;
	// 初始化 I2C
	I2C_Init(I2C1, &I2C_InitStructure); 
}


/*******************************************************************************
* 函 数 名         : I2C_EE_Init
* 函数功能		   : I2C 外设 (EEPROM) 初始化
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_EE_Init(void)
{
	// I2C GPIO 初始化
	I2C_GPIO_Config();
	
	// I2C 工作模式配置
	I2C_Mode_Config();
	
	// 根据头文件 i2c_ee.h 中的定义来选择 EEPROM 的设备地址
#ifdef EEPROM_Block0_ADDRESS
	// 选择 EEPROM Block0 来写入
	EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
#endif	
}


at24c02.h

#include "at24c02.h"


static __IO uint32_t I2CTimeout = I2CT_LONG_TIMEOUT;


/*******************************************************************************
* 函 数 名         : I2C_TIMEOUT_UserCallback
* 函数功能		   : 错误显示函数
* 输    入         : 
					errorCode:错误代码,可以用来定位是哪个环节出错.
* 输    出         : 
					返回0,表示 IIC 读取失败.
* 说	明		   :无
*******************************************************************************/
uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
	// 打印错误代码
	printf("I2C 等待超时!errorCode = %d", errorCode);
	
	return 0;
}


/*******************************************************************************
* 函 数 名         : I2C_EEPROM_ByteWrite
* 函数功能		   : 写一个字节到 I2C EEPROM 中
* 输    入         : 
					pBuffer:缓冲区指针
					WriteAddr:写地址 
* 输    出         : 
					返回0,表示 IIC 读取失败.
* 说	明		   :无
*******************************************************************************/
uint32_t I2C_EEPROM_ByteWrite(u8 *pBuffer, u8 WriteAddr) 
{
	// I2C1 发送启动信号
	I2C_GenerateSTART(I2C1, ENABLE);
	
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV5 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))  
	{
		// 判断是否超时
		if((I2CTimeout--) == 0)
		{
			// 返回并发生错误代码 0
			return I2C_TIMEOUT_UserCallback(0);
		}
	} 
	
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 发送 EEPROM 地址进行写入
	I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
	// 检查 I2C EV6 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 1
			return I2C_TIMEOUT_UserCallback(1);
		}
	}  
	
	// 发送要写入的 EEPROM 内部地址
	I2C_SendData(I2C1, WriteAddr);

	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV8 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 2
			return I2C_TIMEOUT_UserCallback(2);
		}
	} 
	
	// 发送要写入的字节
	I2C_SendData(I2C1, *pBuffer); 

	I2CTimeout = I2CT_FLAG_TIMEOUT;  
	// 检查 I2C EV8 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 3
			return I2C_TIMEOUT_UserCallback(3);
		}
	} 
	
	// I2C1 发送停止信号
	I2C_GenerateSTOP(I2C1, ENABLE);
	
	return 1;
}


/*******************************************************************************
* 函 数 名         : I2C_EE_PageWrite
* 函数功能		   : 写多个字节到 I2C EEPROM 中
* 输    入         : 
					pBuffer:缓冲区指针
					WriteAddr:写地址 
					NumByteToWrite:写的字节数
* 输    出         : 
					返回0,表示 IIC 读取失败.
* 说	明		   :
					在 EEPROM 的一个写循环中可以写多个字节,但一次写入的字节数
					不能超过 EEPROM 页的大小,AT24C02 每页有 8 个字节
*******************************************************************************/
uint32_t I2C_EE_PageWrite(u8 *pBuffer, u8 WriteAddr, u8 NumByteToWrite)
{
	I2CTimeout = I2CT_LONG_TIMEOUT;

	// 检查 I2C 总线忙标志位
	while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))   
	{
		// 判断是否超时
		if((I2CTimeout--) == 0)
		{
			// 返回并发生错误代码 4
			return I2C_TIMEOUT_UserCallback(4);
		}
	} 

	// I2C1 发送启动信号
	I2C_GenerateSTART(I2C1, ENABLE);

	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV5 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))  
	{
		// 判断是否超时
		if((I2CTimeout--) == 0)
		{
			// 返回并发生错误代码 5
			return I2C_TIMEOUT_UserCallback(5);
		}
	} 

	// 发送 EEPROM 地址进行写入
	I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);

	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV6 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))  
	{
		// 判断是否超时
		if((I2CTimeout--) == 0)
		{
			// 返回并发生错误代码 6
			return I2C_TIMEOUT_UserCallback(6);
		}
	} 

	// 发送要写入的 EEPROM 内部地址
	I2C_SendData(I2C1, WriteAddr);  

	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV8 事件是否产生
	while(! I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0)
		{
			// 返回并发生错误代码 7
			return I2C_TIMEOUT_UserCallback(7);
		}
	} 

	// 等待所以数据写完
	while(NumByteToWrite--)  
	{
		// 发送要写入的字节
		I2C_SendData(I2C1, *pBuffer); 

		// 指向要写入的下一个字节
		pBuffer++; 

		I2CTimeout = I2CT_FLAG_TIMEOUT;

		// 检查 I2C EV8 事件是否产生
		while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
		{
			// 判断是否超时
			if((I2CTimeout--) == 0)
			{
				// 返回并发生错误代码 8
				return I2C_TIMEOUT_UserCallback(8);
			}
		} 
	}

	// I2C1 发送停止信号
	I2C_GenerateSTOP(I2C1, ENABLE);

	return 1;
}


/*******************************************************************************
* 函 数 名         : I2C_EE_BufferRead
* 函数功能		   : 从 EEPROM 里面读取一块数据
* 输    入         : 
					pBuffer: 存放从 EEPROM 读取的数据的缓冲区指针
					ReadAddr: 接收数据的 EEPROM 的地址
					NumByteToRead: 要从 EEPROM 读取的字节数
* 输    出         : 
					返回0,表示 IIC 读取失败.
* 说	明		   :无
*******************************************************************************/
uint32_t I2C_EE_BufferRead(u8 *pBuffer, u8 ReadAddr, u16 NumByteToRead)
{  
	I2CTimeout = I2CT_LONG_TIMEOUT;
  
	// 检查 I2C 总线忙标志位
	while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0)
		{
			// 返回并发生错误代码 9
			return I2C_TIMEOUT_UserCallback(9);
		}
	}
  
	// I2C1 发送启动信号
	I2C_GenerateSTART(I2C1, ENABLE);
  
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV5 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 10
			return I2C_TIMEOUT_UserCallback(10);
		}
	}
  
	// 发送 EEPROM 地址进行写入
	I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);

	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV6 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 11
			return I2C_TIMEOUT_UserCallback(11);
		}
	}
    
	// 使能 I2C 外设
	I2C_Cmd(I2C1, ENABLE);

	// 发送要写入的 EEPROM 内部地址
	I2C_SendData(I2C1, ReadAddr);  
   
	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV8 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 12
			return I2C_TIMEOUT_UserCallback(12);
		}
	}

	// I2C1 再次发送启动信号 
	I2C_GenerateSTART(I2C1, ENABLE);

	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV5 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 13
			return I2C_TIMEOUT_UserCallback(13);
		}
	}
    
	// 发送 EEPROM 地址进行读取
	I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);

	I2CTimeout = I2CT_FLAG_TIMEOUT;
	// 检查 I2C EV6 事件是否产生
	while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
	{
		// 判断是否超时
		if((I2CTimeout--) == 0) 
		{
			// 返回并发生错误代码 14
			return I2C_TIMEOUT_UserCallback(14);
		}
	}
  
	// 等待接收数据完毕
	while(NumByteToRead)  
	{
		// 如果接收最后一位数据
		if(NumByteToRead == 1)
		{
			// 失能指定 I2C 的应答功能
			I2C_AcknowledgeConfig(I2C1, DISABLE);

			// 产生 I2Cx 传输 STOP 条件
			I2C_GenerateSTOP(I2C1, ENABLE);
		}

		I2CTimeout = I2CT_LONG_TIMEOUT;
		// 检查 I2C EV7 事件是否产生
		while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)  
		{
			// 判断是否超时
			if((I2CTimeout--) == 0)
			{
				// 返回并发生错误代码 3
				return I2C_TIMEOUT_UserCallback(3);
			}
		} 

		// 把从 EEPROM 中读取一个字节放到 pBuffer 里面
		*pBuffer = I2C_ReceiveData(I2C1);

		// pBuffer 位置下移
		pBuffer++; 

		// 读取字节计数器 -1
		NumByteToRead--;  
	}

	// 使能指定 I2C 的应答功能
	I2C_AcknowledgeConfig(I2C1, ENABLE);

	return 1;
}


/*******************************************************************************
* 函 数 名         : I2C_EE_BufferWrite
* 函数功能		   : 将缓冲区中的数据写到 I2C EEPROM 中
* 输    入         : 
					pBuffer:缓冲区指针
					WriteAddr:写地址
					NumByteToWrite:写的字节数
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_EE_BufferWrite(u8 *pBuffer, u8 WriteAddr, u16 NumByteToWrite)
{
	// 存储能写满多少完整的页数
	u8 NumOfPage = 0;
	// 存储不满一页,剩余的个数
	u8 NumOfSingle = 0;
	// 存储从写数据的页数首地址到要写数据开始的位置
	u8 Addr = 0;
	// 存储从该首地址开始写满该地址所在的页,还能写多少个数据
	u8 count = 0;

	// 计算从写数据的页数首地址到要写数据开始的位置
	Addr = WriteAddr % I2C_PageSize;
	// 计算从该首地址开始写满该地址所在的页,还能写多少个数据
	count = I2C_PageSize - Addr;
	// 计算存储能写满多少完整的页数
	NumOfPage =  NumByteToWrite / I2C_PageSize;
	// 计算存储不满一页,剩余的个数
	NumOfSingle = NumByteToWrite % I2C_PageSize;
 
	// 如果写的地址与页首地址相同
	if(Addr == 0) 
	{
		// 如果写的数据不满一页
		if(NumOfPage == 0) 
		{
			// 写数据到 I2C EEPROM 中
			I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
			// 等待 EEPROM 待机状态
			I2C_EE_WaitEepromStandbyState();
		}
		// 如果写的数据超过一页(包括一页)
		else  
		{
			// 等待页数写完
			while(NumOfPage--)
			{
				// 写数据到 I2C EEPROM 中
				I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize); 
				// 等待 EEPROM 待机状态
				I2C_EE_WaitEepromStandbyState();
				// 切换到下一页
				WriteAddr +=  I2C_PageSize;
				// 写入的数据缓存也相应推到下页
				pBuffer += I2C_PageSize;
			}
			// 判断存储不满一页,剩余的个数是不是等于0,不是的话把剩下的数据也进行存储
			if(NumOfSingle != 0)
			{
				// 写数据到 I2C EEPROM 中
				I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
				// 等待 EEPROM 待机状态
				I2C_EE_WaitEepromStandbyState();
			}
		}
	}
	// 如果写的地址与页首地址不相同
	else 
	{
		// 假如存储的数据不满一页
		if(NumOfPage== 0) 
		{
			// 写数据到 I2C EEPROM 中
			I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
			// 等待 EEPROM 待机状态
			I2C_EE_WaitEepromStandbyState();
		}
		// 假如存储数据超过或等于1页
		else
		{
			// 地址不对齐,多出的 count 分开处理,不加入这个运算
			NumByteToWrite -= count;
			// 计算存储能写满多少完整的页数
			NumOfPage =  NumByteToWrite / I2C_PageSize;
			// 计算存储不满一页,剩余的个数
			NumOfSingle = NumByteToWrite % I2C_PageSize;	

			// 开始存储的地址不在页数首地址
			if(count != 0)
			{  
				// 写数据到 I2C EEPROM 中
				I2C_EE_PageWrite(pBuffer, WriteAddr, count);
				// 等待 EEPROM 待机状态
				I2C_EE_WaitEepromStandbyState();
				// 切换到存储地址
				WriteAddr += count;
				// 切换到下一个存储数据
				pBuffer += count;
			} 

			// 等待页数写完
			while(NumOfPage--)
			{
				// 写数据到 I2C EEPROM 中
				I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
				// 等待 EEPROM 待机状态
				I2C_EE_WaitEepromStandbyState();
				// 切换到下一页
				WriteAddr +=  I2C_PageSize;
				// 写入的数据缓存也相应推到下页
				pBuffer += I2C_PageSize;  
			}
			
			// 判断存储不满一页,剩余的个数是不是等于0,不是的话把剩下的数据也进行存储
			if(NumOfSingle != 0)
			{
				// 写数据到 I2C EEPROM 中
				I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
				// 等待 EEPROM 待机状态
				I2C_EE_WaitEepromStandbyState();
			}
		}
	}  
}


/*******************************************************************************
* 函 数 名         : I2C_EE_WaitEepromStandbyState
* 函数功能		   : 等待 EEPROM 待机状态
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_EE_WaitEepromStandbyState(void)
{
	volatile unsigned short int SR1_Tmp = 0;
	
	do
	{
		// I2C1 发送启动信号
		I2C_GenerateSTART(I2C1, ENABLE);
		// 读取 I2C1 SR1 寄存器
		SR1_Tmp = I2C_ReadRegister(I2C1, I2C_Register_SR1);
		// 发送 EEPROM 地址进行写入
		I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);	
	// 等待字节发送结束
	}while(!(I2C_ReadRegister(I2C1, I2C_Register_SR1) & 0x0002));
	// 清楚应答错误标志位
	I2C_ClearFlag(I2C1, I2C_FLAG_AF);
	// 产生 I2C1 传输 STOP 条件
	I2C_GenerateSTOP(I2C1, ENABLE);
}


main.c

#include "uart.h"
#include "iic.h"
#include "at24c02.h"


// 写入数据缓存
uint8_t I2c_Buf_Write[256];
// 读取数据缓存
uint8_t I2c_Buf_Read[256];

uint8_t I2C_Test(void);


int main(void)
{
	// 串口初始化
	UART_Init();
	// I2C 外设初 (AT24C02) 始化
	I2C_EE_Init();
	
	printf("\r\n 这是一个I2C外设(AT24C02)读写测试例程 \r\n");
	
	// 测试 I2C 通讯是否正常
	I2C_Test();
	
	while (1)
	{
	}
}


/*******************************************************************************
* 函 数 名         : I2C_Test
* 函数功能		   : I2C 测试函数
* 输    入         : 无
* 输    出         : 
					0:通讯异常
					1:通讯成功
* 说	明		   :无
*******************************************************************************/
uint8_t I2C_Test(void)
{
	uint16_t i;
	
	printf("写入的数据\n\r");
	
	// 填充缓冲
	for (i=0; i<=255; i++) 
	{   
		I2c_Buf_Write[i] = i;

		printf("0x%02X ", I2c_Buf_Write[i]);
		
		if(i%16 == 15)
		{			
			printf("\n\r");
		}
	}
	
	// 将 I2c_Buf_Write 中顺序递增的数据写入 EERPOM 中 
	I2C_EE_BufferWrite(I2c_Buf_Write, 0x00, 256);
	
	printf("\n\r写成功\n\r");
	
	printf("\n\r读出的数据\n\r");
	
	// 将 EEPROM 读出数据顺序保持到 I2c_Buf_Read 中
	I2C_EE_BufferRead(I2c_Buf_Read, 0x00, 256); 
	
	// 将 I2c_Buf_Read 中的数据通过串口打印
	for (i=0; i<256; i++)
	{	
		// 假如数据不一致,证明通讯有问题
		if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
		{
			printf("0x%02X ", I2c_Buf_Read[i]);
			
			printf("错误:I2C EEPROM写入与读出的数据不一致\n\r");
			
			return 0;
		}
		
		printf("0x%02X ", I2c_Buf_Read[i]);
		
		if(i%16 == 15)
		{
			printf("\n\r");
		}
	}
	
	printf("I2C(AT24C02)读写测试成功\n\r");
  
	return 1;
}




4.2.2.2 模拟 I2C 例程

uart.h

#ifndef __UART_H
#define __UART_H	


#include "stm32f10x.h"
#include 


void UART_GPIO_Init(void);
void UART_Config(u32 bound);
void UART_NVIC_Init(void);
void UART_Init(void);
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch);
void Usart_SendArray( USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num);
void Usart_SendString( USART_TypeDef * pUSARTx, char *str);
void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch);


#endif


uart.c

#include "uart.h"


/*******************************************************************************
* 函 数 名         : UART_GPIO_Init
* 函数功能		   : UART GPIO 初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_GPIO_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	
	// 使能 PA 端口时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	// USART1_TX  GPIOA.9
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 
	// 复用推挽输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;	
	// IO 口速度为 50MHz	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	// 根据设定参数初始化
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// USART1_RX  GPIOA.10 初始化
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	// 浮空输入模式
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	// 根据设定参数初始化
	GPIO_Init(GPIOA, &GPIO_InitStructure);
}


/*******************************************************************************
* 函 数 名         : UART_Config
* 函数功能		   : UART 配置初始化函数
* 输    入         : 串口波特率
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_Config(u32 bound)
{
	USART_InitTypeDef USART_InitStructure;
	
	// 使能 USART1 时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	// 串口波特率
	USART_InitStructure.USART_BaudRate = bound;
	// 字长为 8 位数据格式
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	// 一个停止位
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	// 无奇偶校验位
	USART_InitStructure.USART_Parity = USART_Parity_No;
	// 无硬件数据流控制
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	// 收发模式
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;	
	// 初始化串口1
	USART_Init(USART1, &USART_InitStructure); 
}


/*******************************************************************************
* 函 数 名         : UART_NVIC_Init
* 函数功能		   : UART NVIC 初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_NVIC_Init(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	
	// Usart1 NVIC 配置
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	// 抢占优先级 3
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
	// 子优先级 3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
	// IRQ 通道使能
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	// 根据指定的参数初始化 NVIC 寄存器
	NVIC_Init(&NVIC_InitStructure);	
	
	// 开启串口接收中断
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
}


/*******************************************************************************
* 函 数 名         : UART_Init
* 函数功能		   : UART 总初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void UART_Init(void)
{
	// UART GPIO 初始化
	UART_GPIO_Init();
	// UART 配置初始化,波特率为 9600
	UART_Config(9600);
	// UART NVIC 初始化
	UART_NVIC_Init();
	
	// 使能串口1 
	USART_Cmd(USART1, ENABLE);
}



/*******************************************************************************
* 函 数 名         : Usart_SendByte
* 函数功能		   : Usart 发送一个字节
* 输    入         : 
					pUSARTx:串口
					ch:字节
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendByte(USART_TypeDef * pUSARTx, uint8_t ch)
{
	// 发送一个字节数据到 USART
	USART_SendData(pUSARTx,ch);
		
	// 等待发送数据寄存器为空
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
}


/*******************************************************************************
* 函 数 名         : Usart_SendArray
* 函数功能		   : Usart 发送数组
* 输    入         : 
					pUSARTx:串口
					array:数组头
					num:数组个数
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendArray(USART_TypeDef * pUSARTx, uint8_t *array, uint16_t num)
{
	uint8_t i;
	
	for(i=0; i<num; i++)
	{
		// 发送一个字节数据到 USART
		Usart_SendByte(pUSARTx,array[i]);	
	}
	
	// 等待发送完成
	while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET);
}


/*******************************************************************************
* 函 数 名         : Usart_SendString
* 函数功能		   : Usart 发送字符串
* 输    入         : 
					pUSARTx:串口
					str:字符串
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendString(USART_TypeDef * pUSARTx, char *str)
{
	unsigned int k = 0;
	
	do 
	{
		Usart_SendByte( pUSARTx, *(str + k) );
		k++;
	}while(*(str + k)!='\0');
  
	// 等待发送完成
	while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TC) == RESET);
}


/*******************************************************************************
* 函 数 名         : Usart_SendHalfWord
* 函数功能		   : Usart 发送一个16位数
* 输    入         : 
					pUSARTx:串口
					ch:16位数
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void Usart_SendHalfWord(USART_TypeDef * pUSARTx, uint16_t ch)
{
	uint8_t temp_h, temp_l;
	
	// 取出高八位
	temp_h = (ch&0XFF00)>>8;
	// 取出低八位
	temp_l = ch&0XFF;
	
	// 发送高八位
	USART_SendData(pUSARTx,temp_h);	
	// 等待发送完毕
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
	
	// 发送低八位
	USART_SendData(pUSARTx,temp_l);	
	// 等待发送完毕
	while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);	
}



/*******************************************************************************
* 函 数 名         : fputc
* 函数功能		   : 重定向 c 库函数 printf 到串口,重定向后可使用 printf 函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
int fputc(int ch, FILE *f)
{
	// 发送一个字节数据到串口
	USART_SendData(USART1, (uint8_t) ch);
	
	// 等待发送完毕
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);		

	return (ch);
}


/*******************************************************************************
* 函 数 名         : fgetc
* 函数功能		   : 重定向 c 库函数 scanf 到串口,重写向后可使用 scanf、getchar 等函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
int fgetc(FILE *f)
{
	// 等待串口输入数据
	while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);

	return (int)USART_ReceiveData(USART1);
}



/*******************************************************************************
* 函 数 名         : USART1_IRQHandler
* 函数功能		   : USART1 中断函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :每接收到一个数据就发送出去
*******************************************************************************/
void USART1_IRQHandler(void)
{
	uint8_t ucTemp;
	
	if(USART_GetITStatus(USART1, USART_IT_RXNE)!= RESET)
	{
		// 接收数据
		ucTemp = USART_ReceiveData(USART1);
		// 发送接收到的数据
		USART_SendData(USART1,ucTemp);    
	}
}


iic.h

#ifndef __IIC_H
#define __IIC_H


#include "stm32f10x.h"


// 写控制 bit
#define EEPROM_I2C_WR 0
// 读控制 bit 
#define EEPROM_I2C_RD 1


extern void I2C_GPIO_Config(void);
extern void I2C_Delay(__IO uint32_t nCount);
extern void SCL_Level(unsigned char Level);
extern void SDA_Level(unsigned char Level);
extern uint8_t SDA_Read(void);
extern void I2C_Start(void);
extern void I2C_Stop(void);
extern void I2C_Send_Byte(uint8_t _ucByte);
extern uint8_t I2C_Read_Byte(void);
extern uint8_t I2C_WaitAck(void);
extern void I2C_Ack(void);
extern void I2C_NAck(void);


#endif


iic.c

#include "iic.h"


/*******************************************************************************
* 函 数 名         : I2C_GPIO_Config
* 函数功能		   : I2C GPIO 初始化函数
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_GPIO_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure; 
	
	// 使能 PB 端口时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	// I2C1_SCL  GPIOB.6
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	// 复用开漏输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	// IO 口速度为 50MHz	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	// 根据设定参数初始化
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	// I2C1_SDA  GPIOB.7
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
	// 复用开漏输出
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	// IO 口速度为 50MHz
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	// 根据设定参数初始化
	GPIO_Init(GPIOB, &GPIO_InitStructure);
}



/*******************************************************************************
* 函 数 名         : I2C_Delay
* 函数功能		   : I2C 总线位延迟
* 输    入         : 
					Count:延时时间
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_Delay(__IO uint32_t nCount)
{
	uint8_t i;
	
	for (i=0; i<nCount; i++)
	{
		// 空操作
		__NOP();
	}
}



/*******************************************************************************
* 函 数 名         : SCL_Level
* 函数功能		   : SCL —— IO 电平输出
* 输    入         : 
					Level: 1 —— 输出高;	0 —— 输出低
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void SCL_Level(unsigned char Level)
{
	if (Level == 1)
	{
		// PB.6(SCL) 输出高
		GPIO_SetBits(GPIOB, GPIO_Pin_6);
	}
	else if (Level == 0)
	{
		// PB.6(SCL) 输出低
		GPIO_ResetBits(GPIOB, GPIO_Pin_6);
	}
}


/*******************************************************************************
* 函 数 名         : SDA_Level
* 函数功能		   : SDA —— IO 电平输出
* 输    入         : 
					Level: 1 —— 输出高;	0 —— 输出低
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void SDA_Level(unsigned char Level)
{
	if (Level == 1)
	{
		// PB.7(SDA) 输出高
		GPIO_SetBits(GPIOB, GPIO_Pin_7);
	}
	else if (Level == 0)
	{
		// PB.7(SDA) 输出低
		GPIO_ResetBits(GPIOB, GPIO_Pin_7);
	}
}


/*******************************************************************************
* 函 数 名         : SDA_Read
* 函数功能		   : SDA —— IO 电平读取
* 输    入         : 无
* 输    出         : 
					1 —— 输入高;	0 —— 输入高
* 说	明		   :无
*******************************************************************************/
uint8_t SDA_Read(void)
{
	uint8_t Level = 0;
	Level = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7);
	return Level;
}



/*******************************************************************************
* 函 数 名         : I2C_Start
* 函数功能		   : CPU 发起 I2C 总线启动信号
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_Start(void)
{
	// SDA 输出高
	SDA_Level(1);
	// SCL 输出高
	SCL_Level(1);
	// 延时
	I2C_Delay(10);
	// SDA 输出低
	SDA_Level(0);
	// 延时
	I2C_Delay(10);
	// SCL 输出低
	SCL_Level(0);
	// 延时
	I2C_Delay(10);
}


/*******************************************************************************
* 函 数 名         : I2C_Stop
* 函数功能		   : CPU 发起 I2C 总线停止信号
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_Stop(void)
{
	// SDA 输出低
	SDA_Level(0);
	// SCL 输出高
	SCL_Level(1);
	// 延时
	I2C_Delay(10);
	// SDA 输出高
	SDA_Level(1);
}


/*******************************************************************************
* 函 数 名         : I2C_Send_Byte
* 函数功能		   : CPU 向 I2C 总线设备发送 8bit 数据
* 输    入         : ucByte : 等待发送的字节
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_Send_Byte(uint8_t _ucByte)
{
	uint8_t i;
	
	// 先发送字节的高位 bit7
	for (i = 0; i < 8; i++)
	{		
		// 将数据按位转化电平信号
		if (_ucByte & 0x80)
		{
			// SDA 输出高
			SDA_Level(1);
		}
		else
		{
			// SDA 输出低
			SDA_Level(0);
		}
		// 延时
		I2C_Delay(10);
		// SCL 输出高
		SCL_Level(1);
		// 延时
		I2C_Delay(10);	
		// SCL 输出低
		SCL_Level(0);
		
		if (i == 7)
		{
			// 释放总线
			SDA_Level(1);
		}
		// 左移一个bit
		_ucByte <<= 1;
		// 延时
		I2C_Delay(10);	
	}
}


/*******************************************************************************
* 函 数 名         : I2C_Read_Byte
* 函数功能		   : CPU 从 I2C 总线设备读取 8bit 数据
* 输    入         : 无
* 输    出         : 读到的数据
* 说	明		   :无
*******************************************************************************/
uint8_t I2C_Read_Byte(void)
{
	uint8_t i;
	uint8_t value;
	
	value = 0;
	// 读到第1个bit为数据的bit7
	for (i=0; i<8; i++)
	{
		// 数组左移一位
		value <<= 1;
		// SCL 输出高
		SCL_Level(1);
		// 延时
		I2C_Delay(10);
		// 接收数据
		if (SDA_Read() == 0x01)
		{
			value++;
		}
		// SCL 输出低
		SCL_Level(0);
		// 延时
		I2C_Delay(10);
	}
	
	return value;
}


/*******************************************************************************
* 函 数 名         : I2C_WaitAck
* 函数功能		   : CPU产生一个时钟,并读取器件的ACK应答信号
* 输    入         : 无
* 输    出         : 无
* 说	明		   :返回0表示正确应答,1表示无器件响应
*******************************************************************************/
uint8_t I2C_WaitAck(void)
{
	uint8_t re;
	
	// SDA 输出高
	SDA_Level(1);
	// 延时
	I2C_Delay(10);
	// SCL 输出高
	SCL_Level(1);
	// 延时
	I2C_Delay(10);
	if (SDA_Read() == 0x01)
	{
		re = 1;
	}
	else
	{
		re = 0;
	}
	// SCL 输出低
	SCL_Level(0);
	// 延时
	I2C_Delay(10);
	// 返回应答位
	return re;
}


/*******************************************************************************
* 函 数 名         : I2C_Ack
* 函数功能		   : CPU 产生一个 ACK 信号
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_Ack(void)
{
	// SDA 输出低
	SDA_Level(0);
	// 延时
	I2C_Delay(10);
	// SCL 输出高
	SCL_Level(1);
	// 延时
	I2C_Delay(10);
	// SCL 输出低
	SCL_Level(0);
	// 延时
	I2C_Delay(10);
	// SDA 输出高
	SDA_Level(1);
}


/*******************************************************************************
* 函 数 名         : I2C_NAck
* 函数功能		   : CPU 产生 1 个 NACK 信号
* 输    入         : 无
* 输    出         : 无
* 说	明		   :无
*******************************************************************************/
void I2C_NAck(void)
{
	// SDA 输出高
	SDA_Level(1);
	// 延时
	I2C_Delay(10);
	// SCL 输出高
	SCL_Level(1);
	// 延时
	I2C_Delay(10);
	// SCL 输出低
	SCL_Level(0);
	// 延时
	I2C_Delay(10);
}


at24c02.h

#ifndef __AT24C02_H
#define __AT24C02_H


#include "iic.h"


// 24xx02 的页面大小
#define EEPROM_PAGE_SIZE 8
// 24xx02 的设备地址
#define EEPROM_DEV_ADDR 0xA0


extern uint8_t I2C_EE_BufferWrite(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize);
extern uint8_t I2C_EE_BufferRead(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize);


#endif


at24c02.c

#include "at24c02.h"


/*******************************************************************************
* 函 数 名         : I2C_EE_BufferWrite
* 函数功能		   : 向串行 EEPROM 指定地址写入若干数据,采用页写操作提高写入效率
* 输    入         : 
					_usAddress : 起始地址
					_usSize : 数据长度,单位为字节
					_pWriteBuf : 存放读到的数据的缓冲区指针
* 输    出         : 0 表示失败,1 表示成功
* 说	明		   :
写串行 EEPROM 不像读操作可以连续读取很多字节,每次写操作只能在同一个 page。
对于 24xx02,page size = 8
简单的处理方法为:按字节写操作模式,没写 1 个字节,都发送地址
为了提高连续写的效率: 本函数采用 page wirte 操作。
*******************************************************************************/
uint8_t I2C_EE_BufferWrite(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i;
	uint16_t m;
	uint16_t usAddr;
	
	usAddr = _usAddress;
	for (i = 0; i < _usSize; i++)
	{
		// 发送第1个字节或是页面首地址时,需要重新发起启动信号和地址
		if ((i == 0) || ((usAddr & (EEPROM_PAGE_SIZE - 1)) == 0))
		{
			// 第0步:发停止信号,启动内部写操作
			I2C_Stop();
			
			/*
				通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
				CLK 频率为 200KHz 时,查询次数为 30 次左右
			*/
			for (m=0; m<1000; m++)
			{
				// 第1步:发起 I2C 总线启动信号
				I2C_Start();
				
				// 第2步:发起控制字节,高 7bit 是地址,bit0 是读写控制位,0 表示写,1 表示读
				// 此处是写指令
				I2C_Send_Byte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);
				
				// 第3步:发送一个时钟,判断器件是否正确应答
				if (I2C_WaitAck() == 0)
				{
					break;
				}
			}
			
			if (m  == 1000)
			{
				// EEPROM 器件写超时
				goto cmd_fail;
			}
			
			// 第4步:发送字节地址,24C02 只有 256 字节,因此 1 个字节就够了,如果是 24C04 以上,那么此处需要连发多个地址
			I2C_Send_Byte((uint8_t)usAddr);
			
			// 第5步:等待 ACK
			if (I2C_WaitAck() != 0)
			{
				// EEPROM 器件无应答
				goto cmd_fail;
			}
		}
		
		// 第6步:开始写入数据
		I2C_Send_Byte(_pWriteBuf[i]);
		
		// 第7步:发送 ACK
		if (I2C_WaitAck() != 0)
		{
			// EEPROM 器件无应答
			goto cmd_fail;
		}
		
		// 地址增 1
		usAddr++;
	}
	
	// 命令执行成功,发送 I2C 总线停止信号
	I2C_Stop();
	// 执行成功
	return 1;
	
cmd_fail:
	// 命令执行失败后,切记发送停止信号,避免影响 I2C 总线上其他设备
	// 发送 I2C 总线停止信号
	I2C_Stop();
	// 执行错误
	return 0;
}


/*******************************************************************************
* 函 数 名         : I2C_EE_BufferRead
* 函数功能		   : 从串行 EEPROM 指定地址处开始读取若干数据
* 输    入         : 
					_usAddress : 起始地址
					_usSize : 数据长度,单位为字节
					_pReadBuf : 存放读到的数据的缓冲区指针
* 输    出         : 0 表示失败,1表示成功
* 说	明		   :
采用串行 EEPROM 随即读取指令序列,连续读取若干字节
*******************************************************************************/
uint8_t I2C_EE_BufferRead(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
	uint16_t i;
	
	// 第1步:发起 I2C 总线启动信号
	I2C_Start();
	
	// 第2步:发起控制字节,高 7bit 是地址,bit0 是读写控制位,0 表示写,1 表示读
	// 此处是写指令
	I2C_Send_Byte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);
	// 第3步:等待 ACK
	if (I2C_WaitAck() != 0)
	{
		// EEPROM 器件无应答
		goto cmd_fail;
	}
	
	// 第4步:发送字节地址,24C02 只有 256 字节,因此 1 个字节就够了,如果是 24C04 以上,那么此处需要连发多个地址
	I2C_Send_Byte((uint8_t)_usAddress);
	// 第5步:等待 ACK
	if (I2C_WaitAck() != 0)
	{
		// EEPROM 器件无应答
		goto cmd_fail;
	}
	
	// 第6步:重新启动 I2C 总线。前面的代码的目的向 EEPROM 传送地址,下面开始读取数据
	I2C_Start();
	// 第8步:发送 ACK
	if (I2C_WaitAck() != 0)
	{
		// EEPROM 器件无应答
		goto cmd_fail;
	}
	
	// 第9步:循环读取数据
	for (i=0; i<_usSize; i++)
	{
		// 读 1 个字节
		_pReadBuf[i] = I2C_Read_Byte();
		
		// 每读完 1 个字节后,需要发送 Ack, 最后一个字节不需要 Ack,发 NAck
		if (i != _usSize - 1)
		{
			// 中间字节读完后,CPU 产生 ACK 信号(驱动 SDA = 0)
			I2C_Ack();
		}
		else
		{
			// 最后1个字节读完后,CPU 产生 NACK 信号(驱动 SDA = 1)
			I2C_NAck();
		}
	}
	
	// 发送 I2C 总线停止信号
	I2C_Stop();
	// 执行成功
	return 1;
	
cmd_fail:
	// 命令执行失败后,切记发送停止信号,避免影响 I2C 总线上其他设备
	// 发送 I2C 总线停止信号
	I2C_Stop();
	// 执行错误
	return 0;
}


main.c

#include "uart.h"
#include "iic.h"
#include "at24c02.h"


// 写入数据缓存
uint8_t I2c_Buf_Write[256];
// 读取数据缓存
uint8_t I2c_Buf_Read[256];

uint8_t I2C_Test(void);


int main(void)
{
	// 串口初始化
	UART_Init();
	// I2C GPIO 初始化
	I2C_GPIO_Config();
	
	printf("\r\n 这是一个I2C外设(AT24C02)读写测试例程 \r\n");
	
	// 测试 I2C 通讯是否正常
	I2C_Test();
	
	while (1)
	{
	}
}


/*******************************************************************************
* 函 数 名         : I2C_Test
* 函数功能		   : I2C 测试函数
* 输    入         : 无
* 输    出         : 
					0:通讯异常
					1:通讯成功
* 说	明		   :无
*******************************************************************************/
uint8_t I2C_Test(void)
{
	uint16_t i;
	
	printf("写入的数据\n\r");
	
	// 填充缓冲
	for (i=0; i<=255; i++) 
	{   
		I2c_Buf_Write[i] = i;

		printf("0x%02X ", I2c_Buf_Write[i]);
		
		if(i%16 == 15)
		{			
			printf("\n\r");
		}
	}
	
	// 将 I2c_Buf_Write 中顺序递增的数据写入 EERPOM 中 
	if (I2C_EE_BufferWrite(I2c_Buf_Write, 0x00, 256) == 0)
	{
		printf("写eeprom出错!\r\n");
		return 0;
	}
	else
	{
		printf("\n\r写成功\n\r");
	}
	
	// 写完之后需要适当的延时再去读,不然会出错
	I2C_Delay(0x0FFFFF);
	
	// 将 EEPROM 读出数据顺序保持到 I2c_Buf_Read 中
	if (I2C_EE_BufferRead(I2c_Buf_Read, 0x00, 256) == 0)
	{
		printf("读eeprom出错!\r\n");
		return 0;
	}
	else
	{		
		printf("读eeprom成功,数据如下:\r\n");
	}
	
	// 将 I2c_Buf_Read 中的数据通过串口打印
	for (i=0; i<256; i++)
	{
		// 假如数据不一致,证明通讯有问题
		if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
		{
			printf("0x%02X ", I2c_Buf_Read[i]);
			printf("错误:EEPROM读出与写入的数据不一致");
			return 0;
		}
		printf(" %02X", I2c_Buf_Read[i]);
		
		if ((i & 15) == 15)
		{
			printf("\r\n");	
		}		
	}
	
	printf("I2C(AT24C02)读写测试成功\n\r");
	
	return 1;
}


4.2.2.3 注意事项

一、硬件 I2C 与 软件 I2C 的区别

硬件 I2C 根据不同的芯片,配置 I2C 方式会有所不同,而且需要求引脚支持 I2C 功能。

软件 I2C 方式一致,可能需要根据芯片改变引脚输出输入方式,普通引脚都可以使用,灵活性强。



二、例程上硬件 I2C 与 软件 I2C 写 EEPROM 方式不一样。

硬件 I2C 是按页为单位写入数据,软件 I2C 是直接任意一个地址写入数据的,实际上 EEPROM 是支持随机访问的。



三、硬件 I2C 理解 I2C_EE_BufferWrite 函数思路

它的主旨就是对输入的数据进行分页 (本型号芯片每页 8 个字节),见表首地址对齐到页时的情况。通过“整除”计算要写入的数据 NumByteToWrite 能写满多少“完整的页”,计算得的值存储在 NumOfPage 中,但有时数据不是刚好能写满完整页的,会多一点出来,通过“求余”计算得出“不满一页的数据个数”就存储在 NumOfSingle 中。计算后通过按页传输 NumOfPage 次整页数据及最后的 NumOfSingle 个数据,使用页传输,比之前的单个字节数据传输要快很多。

除了基本的分页传输,还要考虑首地址的问题,见 表 24‑3。若首地址不是刚好对齐到页的首地址,会需要一个 count 值,用于存储从该首地址开始写满该地址所在的页,还能写多少个数据。实际传输时,先把这部分 count 个数据先写入,填满该页,然后把剩余的数据 (NumByteToWrite - count),再重复上述求出 NumOPage 及 NumOfSingle 的过程,按页传输到 EEPROM。

  1. 若 WriteAddr = 16,计算得 Addr = 16 % 8 = 0 , count = 8 - 0 = 8;

  2. 同时,若 NumOfPage = 22,计算得 NumOfPage = 22 / 8 = 2, NumOfSingle = 22 % 8 = 6。

  3. 数据传输情况如表首地址对齐到页时的情况


野火学习笔记(15) —— I2C —— 读写 EEPROM_第13张图片


  1. 若 WriteAddr = 17,计算得 Addr = 17 % 8 = 1, count = 8 - 1 = 7;

  2. 同时,若 NumByteToWrite = 22,

  3. 先把 count 去掉,特殊处理,计算得新的 NumByteToWrite = 22 - 7 = 15

  4. 计算得 NumOfPage = 15 / 8 = 1, NumOfSingle = 15 % 8 = 7。

  5. 数据传输情况如表首地址未对齐到页时的情况


野火学习笔记(15) —— I2C —— 读写 EEPROM_第14张图片


四、在某些存储器,如 NAND FLASH,它是必须按照 Block 写入的,例如每个 Block 为 512 或 4096 字节,数据写入的最小单位是 Block,写入前都需要擦除整个 Block; NOR FLASH 则是写入前必须以 Sector / Block 为单位擦除,然后才可以按字节写入。而我们的 EEPROM 数据写入和擦除的最小单位是“字节”而不是“页”,数据写入前不需要擦除整页。


5. 附属

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

I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答信号。

开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据。

结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据。

应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。

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


野火学习笔记(15) —— I2C —— 读写 EEPROM_第15张图片


部分摘抄自:
[野火EmbedFire]《STM32库开发实战指南——基于野火霸道开发板》.pdf
[正点原子]《STM32F1开发指南-库函数版本》.pdf

你可能感兴趣的:(STM32-固件库,单片机,stm32)