第23章     I2C—读写EEPROM

全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn

野火视频教程优酷观看网址:http://i.youku.com/firege

本章参考资料:《STM32F4xx 中文参考手册》、《STM32F4xx规格书》、库帮助文档《stm32f4xx_dsp_stdperiph_lib_um.chm》及《I2C总线协议》。

若对I2C通讯协议不了解,可先阅读《I2C总线协议》文档的内容学习。若想了解SMBUS,可阅读《smbus20》文档。

关于EEPROM存储器,请参考"常用存储器介绍"章节,实验中的EEPROM,请参考其规格书《AT24C02》来了解。

23.1 I2C协议简介

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

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

23.1.1 I2C物理层

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

图 231 常见的I2C通讯系统

它的物理层有如下特点:

(1)    它是一个支持多设备的总线。"总线"指多个设备共用的信号线。在一个I2C通讯总线中,可连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。

(2)    一个I2C总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线 (SCL)。数据线即用来表示数据,时钟线用于数据收发同步。

(3)    每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。

(4)    总线通过上拉电阻接到电源。当I2C设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。

(5)    多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。

(6)    具有三种传输模式:标准模式传输速率为100kbit/s ,快速模式为400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多I2C设备尚不支持高速模式。

(7)    连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制 。

23.1.2 协议层

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

1.    I2C基本读写过程

先看看I2C通讯过程的基本结构,它的通讯过程见图 232、图 233及图 234。

图 232 主机写数据到从机

图 233 主机由从机中读数据

图 234 I2C通讯复合格式

图例: 数据由主机传输至从机 S : 传输开始信号

SLAVE_ADDRESS: 从机地址

数据由从机传输至主机 R/: 传输方向选择位,1为读,0为写

A/ : 应答(ACK)或非应答(NACK)信号

P : 停止传输信号

这些图表示的是主机和从机通讯时,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.    通讯的起始和停止信号

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

图 235 起始和停止信号

2.    数据有效性

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

图 236 数据有效性

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

3.    地址及数据方向

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

图 237 设备地址(7位)及数据传输方向

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

4.    响应

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

图 238 响应与非响应信号

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

23.2 STM32的I2C特性及架构

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

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

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

23.2.1 STM32的I2C外设简介

STM32的I2C外设可用作通讯的主机及从机,支持100Kbit/s和400Kbit/s的速率,支持7位、10位设备地址,支持DMA数据传输,并具有数据校验功能。它的I2C外设还支持SMBus2.0协议,SMBus协议与I2C类似,主要应用于笔记本电脑的电池管理中,本教程不展开,感兴趣的读者可参考《SMBus20》文档了解。

23.2.2 STM32的I2C架构剖析

图 239 I2C架构图

1.    通讯引脚

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

表 231 STM32F4xx的I2C引脚(整理自《STM32F4xx规格书》)

引脚

I2C编号

I2C1

I2C2

I2C3

SCL

PB6/PB10

PH4/PF1/PB10

PH7/PA8

SDA

PB7/PB9

PH5/PF0/PB11

PH8/PC9

2.    时钟控制逻辑

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

    可选择I2C通讯的"标准/快速"模式,这两个模式分别I2C对应100/400Kbit/s的通讯速率。

    在快速模式下可选择SCL时钟的占空比,可选Tlow/Thigh=2或Tlow/Thigh=16/9模式,我们知道I2C协议在SCL高电平时对SDA信号采样,SCL低电平时SDA准备下一个数据,修改SCL的高低电平比会影响数据采样,但其实这两个模式的比例差别并不大,若不是要求非常严格,这里随便选就可以了。

    CCR寄存器中还有一个12位的配置因子CCR,它与I2C外设的输入时钟源共同作用,产生SCL时钟,STM32的I2C外设都挂载在APB1总线上,使用APB1的时钟源PCLK1,SCL信号线的输出时钟公式如下:

标准模式:

Thigh=CCR*TPCKL1        Tlow = CCR*TPCLK1

快速模式中Tlow/Thigh=2时:

Thigh = CCR*TPCKL1        Tlow = 2*CCR*TPCKL1

快速模式中Tlow/Thigh=16/9时:

Thigh = 9*CCR*TPCKL1        Tlow = 16*CCR*TPCKL1

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

PCLK时钟周期:            TPCLK1 = 1/45000000

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

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

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

计算CCR的值:            CCR = THIGH/TPCLK1 = 37.5

计算结果为小数,而CCR寄存器是无法配置小数参数的,所以我们只能把CCR取值为38,这样I2C的SCL实际频率无法达到400KHz (约为394736Hz)。要想它实际频率达到400KHz,需要修改STM32的系统时钟,把PCLK1时钟频率改成10的倍数才可以,但修改PCKL时钟影响很多外设,所以一般我们不会修改它。SCL的实际频率不达到400KHz,除了通讯稍慢一点以外,不会对I2C的标准通讯造成其它影响

3.    数据控制逻辑

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

4.    整体控制逻辑

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

23.2.3 通讯过程

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

1.    主发送器

见图 2310。图中的是"主发送器"流程,即作为I2C通讯的主机端时,向外发送数据时的过程。

图 2310 主发送器通讯过程

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

(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),这个时候会产生EV2事件,SR1的TXE位及BTF位都被置1,表示通讯结束。

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

2.    主接收器

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

图 2311 主接收器过程

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

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

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

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

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

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

23.3 I2C初始化结构体详解

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

代码清单 231 I2C初始化结构体

1 typedef struct {

2 uint32_t I2C_ClockSpeed; /*!< 设置SCL时钟频率,此值要低于40 0000*/

3 uint16_t I2C_Mode; /*!< 指定工作模式,可选I2C模式及SMBUS模式 */

4 uint16_t I2C_DutyCycle; /*指定时钟占空比,可选low/high = 2:1及16:9模式*/

5 uint16_t I2C_OwnAddress1; /*!< 指定自身的I2C设备地址 */

6 uint16_t I2C_Ack; /*!< 使能或关闭响应(一般都要使能) */

位及10位 */

8 } 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位地址。

(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即可把结构体的配置写入到寄存器中。

23.4 I2C—读写EEPROM实验

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

23.4.1 硬件设计

图 2312 EEPROM硬件连接图

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

图 2313 EEPROM设备地址(摘自《AT24C02》规格书)

按照我们此处的连接,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的型号、设备地址或控制引脚不一样,只需根据我们的工程修改即可,程序的控制原理相同。

23.4.2 软件设计

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

1.    编程要点

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

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

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

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

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

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

2.    代码分析
I2C硬件相关宏定义

我们把I2C硬件相关的配置都以宏的形式定义到"bsp_i2c_ee.h"文件中,见代码清单 232。

代码清单 232 I2C硬件配置相关的宏

2 /* STM32 I2C 速率 */

3 #define I2C_Speed 400000

4

5 /* STM32自身的I2C地址,这个地址只要与STM32外挂的I2C器件地址不一样即可 */

6 #define I2C_OWN_ADDRESS7 0X0A

7

8 /*I2C接口*/

9 #define EEPROM_I2C I2C1

10 #define EEPROM_I2C_CLK RCC_APB1Periph_I2C1

11

12 #define EEPROM_I2C_SCL_PIN GPIO_Pin_6

13 #define EEPROM_I2C_SCL_GPIO_PORT GPIOB

14 #define EEPROM_I2C_SCL_GPIO_CLK RCC_AHB1Periph_GPIOB

15 #define EEPROM_I2C_SCL_SOURCE GPIO_PinSource6

16 #define EEPROM_I2C_SCL_AF GPIO_AF_I2C1

17

18 #define EEPROM_I2C_SDA_PIN GPIO_Pin_7

19 #define EEPROM_I2C_SDA_GPIO_PORT GPIOB

20 #define EEPROM_I2C_SDA_GPIO_CLK RCC_AHB1Periph_GPIOB

21 #define EEPROM_I2C_SDA_SOURCE GPIO_PinSource7

22 #define EEPROM_I2C_SDA_AF GPIO_AF_I2C1

以上代码根据硬件连接,把与EEPROM通讯使用的I2C号、引脚号、引脚源以及复用功能映射都以宏封装起来,并且定义了自身的I2C地址及通讯速率,以便配置模式的时候使用。

初始化I2C的 GPIO

利用上面的宏,编写I2C GPIO引脚的初始化函数,见代码清单 122。

代码清单 233 I2C初始化函数

2 /**

3 * @brief I2C1 I/O配置

4 * @param 无

5 * @retval 无

6 */

7 static void I2C_GPIO_Config(void)

8 {

9 GPIO_InitTypeDef GPIO_InitStructure;

10

11 /*使能I2C外设时钟 */

12 RCC_APB1PeriphClockCmd(EEPROM_I2C_CLK, ENABLE);

13

14 /*使能I2C引脚的GPIO时钟*/

15 RCC_AHB1PeriphClockCmd(EEPROM_I2C_SCL_GPIO_CLK |

16 EEPROM_I2C_SDA_GPIO_CLK, ENABLE);

17

18 /* 连接引脚源 PXx 到 I2C_SCL*/

19 GPIO_PinAFConfig(EEPROM_I2C_SCL_GPIO_PORT, EEPROM_I2C_SCL_SOURCE,

20 EEPROM_I2C_SCL_AF);

21 /* 连接引脚源 PXx 到 to I2C_SDA*/

22 GPIO_PinAFConfig(EEPROM_I2C_SDA_GPIO_PORT, EEPROM_I2C_SDA_SOURCE,

23 EEPROM_I2C_SDA_AF);

24

25 /*配置 SCL引脚 */

26 GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;

27 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;

28 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

29 GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;

30 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

31 GPIO_Init(EEPROM_I2C_SCL_GPIO_PORT, &GPIO_InitStructure);

32

33 /*配置 SDA引脚 */

34 GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;

35 GPIO_Init(EEPROM_I2C_SDA_GPIO_PORT, &GPIO_InitStructure);

36 }

同为外设使用的GPIO引脚初始化,初始化的流程与"串口初始化函数"章节中的类似,主要区别是引脚的模式。函数执行流程如下:

(1)    使用GPIO_InitTypeDef定义GPIO初始化结构体变量,以便下面用于存储GPIO配置;

(2)    调用库函数RCC_APB1PeriphClockCmd使能I2C外设时钟,调用RCC_AHB1PeriphClockCmd来使能I2C引脚使用的GPIO端口时钟,调用时我们使用"|"操作同时配置两个引脚。

(3)    向GPIO初始化结构体赋值,把引脚初始化成复用开漏模式,要注意I2C的引脚必须使用这种模式。

(4)    使用以上初始化结构体的配置,调用GPIO_Init函数向寄存器写入参数,完成GPIO的初始化。

配置I2C的模式

以上只是配置了I2C使用的引脚,还不算对I2C模式的配置,见代码清单 234。

代码清单 234 配置I2C模式

2 /**

3 * @brief I2C 工作模式配置

4 * @param 无

5 * @retval 无

6 */

7 static void I2C_Mode_Config(void)

8 {

9 I2C_InitTypeDef I2C_InitStructure;

10

11 /* I2C 配置 */

12 /*I2C模式*/

13 I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

14 /*占空比*/

15 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;

16 /*I2C自身地址*/

17 I2C_InitStructure.I2C_OwnAddress1 =I2C_OWN_ADDRESS7;

18 /*使能响应*/

19 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;

20 /* I2C的寻址模式 */

21 I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

22 /* 通信速率 */

23 I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;

24 /*写入配置*/

25 I2C_Init(EEPROM_I2C, &I2C_InitStructure);

26 /* 使能 I2C */

27 I2C_Cmd(EEPROM_I2C, ENABLE);

28 }

29

30 /**

31 * @brief I2C 外设初始化

32 * @param 无

33 * @retval 无

34 */

35 void I2C_EE_Init(void)

36 {

37 I2C_GPIO_Config();

38

39 I2C_Mode_Config();

40 }

熟悉STM32 I2C结构的话,这段初始化程序就十分好理解了,它把I2C外设通讯时钟SCL的低/高电平比设置为2,使能响应功能,使用7位地址I2C_OWN_ADDRESS7以及速率配置为I2C_Speed(前面在bsp_i2c_ee.h定义的宏)。最后调用库函数I2C_Init把这些配置写入寄存器,并调用I2C_Cmd函数使能外设。

为方便调用,我们把I2C的GPIO及模式配置都用I2C_EE_Init函数封装起来。

向EEPROM写入一个字节的数据

初始化好I2C外设后,就可以使用I2C通讯了,我们看看如何向EEPROM写入一个字节的数据,见代码清单 235。

代码清单 235 向EEPROM写入一个字节的数据

1

2 /***************************************************************/

3 /*通讯等待超时时间*/

4 #define I2CT_FLAG_TIMEOUT ((uint32_t)0x1000)

5 #define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))

6

7 /**

8 * @brief I2C等待事件超时的情况下会调用这个函数来处理

9 * @param errorCode:错误代码,可以用来定位是哪个环节出错.

,表示IIC读取失败.

11 */

12 static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)

13 {

14 /* 使用串口printf输出错误信息,方便调试 */

15 EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);

16 return 0;

17 }

18 /**

19 * @brief 写一个字节到I2C EEPROM中

20 * @param pBuffer:缓冲区指针

21 * @param WriteAddr:写地址

,异常返回0

23 */

24 uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)

25 {

26 /* 产生I2C起始信号 */

27 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

28

29 /*设置超时等待时间*/

30 I2CTimeout = I2CT_FLAG_TIMEOUT;

31 /* 检测 EV5 事件并清除标志*/

32 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

33 {

34 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);

35 }

36

37 /* 发送EEPROM设备地址 */

38 I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS,

39 I2C_Direction_Transmitter);

40

41 I2CTimeout = I2CT_FLAG_TIMEOUT;

42 /* 检测 EV6 事件并清除标志*/

43 while (!I2C_CheckEvent(EEPROM_I2C,

44 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))

45 {

46 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);

47 }

48

49 /* 发送要写入的EEPROM内部地址(即EEPROM内部存储器的地址) */

50 I2C_SendData(EEPROM_I2C, WriteAddr);

51

52 I2CTimeout = I2CT_FLAG_TIMEOUT;

53 /* 检测 EV8 事件并清除标志*/

54 while (!I2C_CheckEvent(EEPROM_I2C,

55 I2C_EVENT_MASTER_BYTE_TRANSMITTED))

56 {

57 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);

58 }

59 /* 发送一字节要写入的数据 */

60 I2C_SendData(EEPROM_I2C, *pBuffer);

61

62 I2CTimeout = I2CT_FLAG_TIMEOUT;

63 /* 检测 EV8 事件并清除标志*/

64 while (!I2C_CheckEvent(EEPROM_I2C,

65 I2C_EVENT_MASTER_BYTE_TRANSMITTED))

66 {

67 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);

68 }

69

70 /* 发送停止信号 */

71 I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

72

73 return 1;

74 }

先来分析I2C_TIMEOUT_UserCallback函数,它的函数体里只调用了宏EEPROM_ERROR,这个宏封装了printf函数,方便使用串口向上位机打印调试信息。在I2C通讯的很多过程,都需要检测事件,当检测到某事件后才能继续下一步的操作,但有时通讯错误或者I2C总线被占用,我们不能无休止地等待下去,所以我们设定每个事件检测都有等待的时间上限,若超过这个时间,我们就调用I2C_TIMEOUT_UserCallback函数输出调试信息(或可以自己加其它操作),并终止I2C通讯。

了解了这个机制,再来分析I2C_EE_ByteWrite函数,这个函数实现了前面讲的I2C主发送器通讯流程:

(1)    使用库函数I2C_GenerateSTART产生I2C起始信号,其中的EEPROM_I2C宏是前面硬件定义相关的I2C编号;

(2)    对I2CTimeout变量赋值为宏I2CT_FLAG_TIMEOUT,这个I2CTimeout变量在下面的while循环中每次循环减1,该循环通过调用库函数I2C_CheckEvent检测事件,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当检测I2CT_FLAG_TIMEOUT次都还没等待到事件则认为通讯失败,调用前面的I2C_TIMEOUT_UserCallback输出调试信息,并退出通讯;

(3)    调用库函数I2C_Send7bitAddress发送EEPROM的设备地址,并把数据传输方向设置为I2C_Direction_Transmitter(即发送方向),这个数据传输方向就是通过设置I2C通讯中紧跟地址后面的R/W位实现的。发送地址后以同样的方式检测EV6标志;

(4)    调用库函数I2C_SendData向EEPROM发送要写入的内部地址,该地址是I2C_EE_ByteWrite函数的输入参数,发送完毕后等待EV8事件。要注意这个内部地址跟上面的EEPROM地址不一样,上面的是指I2C总线设备的独立地址,而此处的内部地址是指EEPROM内数据组织的地址,也可理解为EEPROM内存的地址或I2C设备的寄存器地址;

(5)    调用库函数I2C_SendData向EEPROM发送要写入的数据,该数据是I2C_EE_ByteWrite函数的输入参数,发送完毕后等待EV8事件;

(6)    一个I2C通讯过程完毕,调用I2C_GenerateSTOP发送停止信号。

在这个通讯过程中,STM32实际上通过I2C向EEPROM发送了两个数据,但为何第一个数据被解释为EEPROM的内存地址?这是由EEPROM的自己定义的单字节写入时序,见图 2314。

图 2314 EEPROM单字节写入时序(摘自《AT24C02》规格书)

EEPROM的单字节时序规定,向它写入数据的时候,第一个字节为内存地址,第二个字节是要写入的数据内容。所以我们需要理解:命令、地址的本质都是数据,对数据的解释不同,它就有了不同的功能。

多字节写入及状态等待

单字节写入通讯结束后,EEPROM芯片会根据这个通讯结果擦写该内存地址的内容,这需要一段时间,所以我们在多次写入数据时,要先等待EEPROM内部擦写完毕。多个数据写入过程见代码清单 236。

代码清单 236 多字节写入

1 /**

2 * @brief 将缓冲区中的数据写到I2C EEPROM中,采用单字节写入的方式,

3 速度比页写入慢

4 * @param pBuffer:缓冲区指针

5 * @param WriteAddr:写地址

6 * @param NumByteToWrite:写的字节数

7 * @retval 无

8 */

9 uint8_t I2C_EE_ByetsWrite(uint8_t* pBuffer,uint8_t WriteAddr,

10 uint16_t NumByteToWrite)

11 {

12 uint16_t i;

13 uint8_t res;

14

15 /*每写一个字节调用一次I2C_EE_ByteWrite函数*/

16 for (i=0; i<NumByteToWrite; i++)

17 {

18 /*等待EEPROM准备完毕*/

19 I2C_EE_WaitEepromStandbyState();

20 /*按字节写入数据*/

21 res = I2C_EE_ByteWrite(pBuffer++,WriteAddr++);

22 }

23 return res;

24 }

这段代码比较简单,直接使用for循环调用前面定义的I2C_EE_ByteWrite函数一个字节一个字节地向EEPROM发送要写入的数据。在每次数据写入通讯前调用了I2C_EE_WaitEepromStandbyState函数等待EEPROM内部擦写完毕,该函数的定义见代码清单 237。

代码清单 237 等待EEPROM处于准备状态

1 //等待Standby状态的最大次数

2 #define MAX_TRIAL_NUMBER 300

3 /**

4 * @brief 等待EEPROM到准备状态

5 * @param 无

,异常返回0

7 */

8 static uint8_t I2C_EE_WaitEepromStandbyState(void)

9 {

10 __IO uint16_t tmpSR1 = 0;

11 __IO uint32_t EETrials = 0;

12

13 /*总线忙时等待 */

14 I2CTimeout = I2CT_LONG_TIMEOUT;

15 while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))

16 {

17 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(20);

18 }

19

次 */

21 while (1)

22 {

23 /*开始信号 */

24 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

25

26 /* 检测 EV5 事件并清除标志*/

27 I2CTimeout = I2CT_FLAG_TIMEOUT;

28 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

29 {

30 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(21);

31 }

32

33 /* 发送EEPROM设备地址 */

34 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);

35

36 /* 等待ADDR标志 */

37 I2CTimeout = I2CT_LONG_TIMEOUT;

38 do

39 {

40 /* 获取SR1寄存器状态 */

41 tmpSR1 = EEPROM_I2C->SR1;

42

43 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(22);

44 }

45 /* 一直等待直到addr及af标志为1 */

46 while ((tmpSR1 & (I2C_SR1_ADDR | I2C_SR1_AF)) == 0);

47

48 /*检查addr标志是否为1 */

49 if (tmpSR1 & I2C_SR1_ADDR)

50 {

51 /* 清除addr标志该标志通过读SR1及SR2清除 */

52 (void)EEPROM_I2C->SR2;

53

54 /*产生停止信号 */

55 I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

56

57 /* 退出函数 */

58 return 1;

59 }

60 else

61 {

62 /*清除af标志 */

63 I2C_ClearFlag(EEPROM_I2C, I2C_FLAG_AF);

64 }

65

66 /*检查等待次数*/

67 if (EETrials++ == MAX_TRIAL_NUMBER)

68 {

69 /* 等待MAX_TRIAL_NUMBER次都还没准备好,退出等待 */

70 return I2C_TIMEOUT_UserCallback(23);

71 }

72 }

73 }

这个函数主要实现是向EEPROM发送它设备地址,检测EEPROM的响应,若EEPROM接收到地址后返回应答信号,则表示EEPROM已经准备好,可以开始下一次通讯。函数中检测响应是通过读取STM32的SR1寄存器的ADDR位及AF位来实现的,当I2C设备响应了地址的时候,ADDR会置1,若应答失败,AF位会置1。

EEPROM的页写入

在以上的数据通讯中,每写入一个数据都需要向EEPROM发送写入的地址,我们希望向连续地址写入多个数据的时候,只要告诉EEPROM第一个内存地址address1,后面的数据按次序写入到address2、address3… 这样可以节省通讯的内容,加快速度。为应对这种需求,EEPROM定义了一种页写入时序,见图 2315。

图 2315 EEPROM页写入时序(摘自《AT24C02》规格书)

根据页写入时序,第一个数据被解释为要写入的内存地址address1,后续可连续发送n个数据,这些数据会依次写入到内存中。其中AT24C02型号的芯片页写入时序最多可以一次发送8个数据(即n = 8 ),该值也称为页大小,某些型号的芯片每个页写入时序最多可传输16个数据。EEPROM的页写入代码实现见代码清单 238。

代码清单 238 EEPROM的页写入

1

2 /**

3 * @brief 在EEPROM的一个写循环中可以写多个字节,但一次写入的字节数

个字节

5 * @param

6 * @param pBuffer:缓冲区指针

7 * @param WriteAddr:写地址

8 * @param NumByteToWrite:要写的字节数要求NumByToWrite小于页大小

,异常返回0

10 */

11 uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr,

12 uint8_t NumByteToWrite)

13 {

14 I2CTimeout = I2CT_LONG_TIMEOUT;

15

16 while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))

17 {

18 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);

19 }

20

21 /* 产生I2C起始信号 */

22 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

23

24 I2CTimeout = I2CT_FLAG_TIMEOUT;

25

26 /* 检测 EV5 事件并清除标志 */

27 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

28 {

29 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);

30 }

31

32 /* 发送EEPROM设备地址 */

33 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);

34

35 I2CTimeout = I2CT_FLAG_TIMEOUT;

36

37 /* 检测 EV6 事件并清除标志*/

38 while (!I2C_CheckEvent(EEPROM_I2C,

39 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))

40 {

41 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);

42 }

43 /* 发送要写入的EEPROM内部地址(即EEPROM内部存储器的地址) */

44 I2C_SendData(EEPROM_I2C, WriteAddr);

45

46 I2CTimeout = I2CT_FLAG_TIMEOUT;

47

48 /* 检测 EV8 事件并清除标志*/

49 while (! I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))

50 {

51 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);

52 }

53 /* 循环发送NumByteToWrite个数据 */

54 while (NumByteToWrite--)

55 {

56 /* 发送缓冲区中的数据 */

57 I2C_SendData(EEPROM_I2C, *pBuffer);

58

59 /* 指向缓冲区中的下一个数据 */

60 pBuffer++;

61

62 I2CTimeout = I2CT_FLAG_TIMEOUT;

63

64 /* 检测 EV8 事件并清除标志*/

65 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))

66 {

67 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);

68 }

69 }

70 /* 发送停止信号 */

71 I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

72 return 1;

73 }

这段页写入函数主体跟单字节写入函数是一样的,只是它在发送数据的时候,使用for循环控制发送多个数据,发送完多个数据后才产生I2C停止信号,只要每次传输的数据小于等于EEPROM时序规定的页大小,就能正常传输。

快速写入多字节

利用EEPROM的页写入方式,可以改进前面的"多字节写入"函数,加快传输速度,见代码清单 239。

代码清单 239 快速写入多字节函数

1

个字节 */

3 #define I2C_PageSize 8

4

5 /**

6 * @brief 将缓冲区中的数据写到I2C EEPROM中,采用页写入的方式,加快写入速度

7 * @param pBuffer:缓冲区指针

8 * @param WriteAddr:写地址

9 * @param NumByteToWrite:写的字节数

10 * @retval 无

11 */

12 void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr,

13 u16 NumByteToWrite)

14 {

15 uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

16

17 /*mod运算求余,若writeAddr是I2C_PageSize整数倍,运算结果Addr值为0*/

18 Addr = WriteAddr % I2C_PageSize;

19

20 /*差count个数据,刚好可以对齐到页地址*/

21 count = I2C_PageSize - Addr;

22 /*计算出要写多少整数页*/

23 NumOfPage = NumByteToWrite / I2C_PageSize;

24 /*mod运算求余,计算出剩余不满一页的字节数*/

25 NumOfSingle = NumByteToWrite % I2C_PageSize;

26

27 /* Addr=0,则WriteAddr 刚好按页对齐 aligned */

28 if (Addr == 0)

29 {

30 /* 如果 NumByteToWrite < I2C_PageSize */

31 if (NumOfPage == 0)

32 {

33 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

34 I2C_EE_WaitEepromStandbyState();

35 }

36 /* 如果 NumByteToWrite > I2C_PageSize */

37 else

38 {

39 /*先把整数页都写了*/

40 while (NumOfPage--)

41 {

42 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);

43 I2C_EE_WaitEepromStandbyState();

44 WriteAddr += I2C_PageSize;

45 pBuffer += I2C_PageSize;

46 }

47

48 /*若有多余的不满一页的数据,把它写完*/

49 if (NumOfSingle!=0)

50 {

51 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

52 I2C_EE_WaitEepromStandbyState();

53 }

54 }

55 }

56 /* 如果 WriteAddr 不是按 I2C_PageSize 对齐 */

57 else

58 {

59 /* 如果 NumByteToWrite < I2C_PageSize */

60 if (NumOfPage== 0)

61 {

62 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

63 I2C_EE_WaitEepromStandbyState();

64 }

65 /* 如果 NumByteToWrite > I2C_PageSize */

66 else

67 {

68 /*地址不对齐多出的count分开处理,不加入这个运算*/

69 NumByteToWrite -= count;

70 NumOfPage = NumByteToWrite / I2C_PageSize;

71 NumOfSingle = NumByteToWrite % I2C_PageSize;

72

73 /*先把WriteAddr所在页的剩余字节写了*/

74 if (count != 0)

75 {

76 I2C_EE_PageWrite(pBuffer, WriteAddr, count);

77 I2C_EE_WaitEepromStandbyState();

78

79 /*WriteAddr加上count后,地址就对齐到页了*/

80 WriteAddr += count;

81 pBuffer += count;

82 }

83 /*把整数页都写了*/

84 while (NumOfPage--)

85 {

86 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);

87 I2C_EE_WaitEepromStandbyState();

88 WriteAddr += I2C_PageSize;

89 pBuffer += I2C_PageSize;

90 }

91 /*若有多余的不满一页的数据,把它写完*/

92 if (NumOfSingle != 0)

93 {

94 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);

95 I2C_EE_WaitEepromStandbyState();

96 }

97 }

98 }

99 }

很多读者觉得这段代码的运算很复杂,看不懂,其实它的主旨就是对输入的数据进行分页(本型号芯片每页8个字节),见表 232。通过"整除"计算要写入的数据NumByteToWrite能写满多少"完整的页",计算得的值存储在NumOfPage中,但有时数据不是刚好能写满完整页的,会多一点出来,通过"求余"计算得出"不满一页的数据个数"就存储在NumOfSingle中。计算后通过按页传输NumOfPage次整页数据及最后的NumOfSing个数据,使用页传输,比之前的单个字节数据传输要快很多。

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

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

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

3.    数据传输情况如表 232

表 232 首地址对齐到页时的情况

不影响

不影响

NumOfSingle=6

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

5.    同时,若NumOfPage=22,

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

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

8.    数据传输情况如表 233

表 233 首地址未对齐到页时的情况

不影响

不影响

count=7

NumOfSingle=7

最后,强调一下,EEPROM支持的页写入只是一种加速的I2C的传输时序,实际上并不要求每次都以页为单位进行读写,EEPROM是支持随机访问的(直接读写任意一个地址),如前面的单个字节写入。在某些存储器,如NAND FLASH,它是必须按照Block写入的,例如每个Block为512或4096字节,数据写入的最小单位是Block,写入前都需要擦除整个Block;NOR FLASH则是写入前必须以Sector/Block为单位擦除,然后才可以按字节写入。而我们的EEPROM数据写入和擦除的最小单位是"字节"而不是"页",数据写入前不需要擦除整页。

从EEPROM读取数据

从EEPROM读取数据是一个复合的I2C时序,它实际上包含一个写过程和一个读过程,见图 2316。

图 2316 EEPROM数据读取时序

读时序的第一个通讯过程中,使用I2C发送设备地址寻址(写方向),接着发送要读取的"内存地址";第二个通讯过程中,再次使用I2C发送设备地址寻址,但这个时候的数据方向是读方向;在这个过程之后,EEPROM会向主机返回从"内存地址"开始的数据,一个字节一个字节地传输,只要主机的响应为"应答信号",它就会一直传输下去,主机想结束传输时,就发送"非应答信号",并以"停止信号"结束通讯,作为从机的EEPROM也会停止传输。实现代码见代码清单 2310。

代码清单 2310 从EEPROM读取数据

1

2 /**

3 * @brief 从EEPROM里面读取一块数据

4 * @param pBuffer:存放从EEPROM读取的数据的缓冲区指针

5 * @param ReadAddr:接收数据的EEPROM的地址

6 * @param NumByteToRead:要从EEPROM读取的字节数

,异常返回0

8 */

9 uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr,

10 u16 NumByteToRead)

11 {

12 I2CTimeout = I2CT_LONG_TIMEOUT;

13

14 while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))

15 {

16 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);

17 }

18

19 /* 产生I2C起始信号 */

20 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

21

22 I2CTimeout = I2CT_FLAG_TIMEOUT;

23

24 /* 检测 EV5 事件并清除标志*/

25 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

26 {

27 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);

28 }

29

30 /* 发送EEPROM设备地址 */

31 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter);

32

33 I2CTimeout = I2CT_FLAG_TIMEOUT;

34

35 /* 检测 EV6 事件并清除标志*/

36 while (!I2C_CheckEvent(EEPROM_I2C,

37 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))

38 {

39 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);

40 }

41 /*通过重新设置PE位清除EV6事件 */

42 I2C_Cmd(EEPROM_I2C, ENABLE);

43

44 /* 发送要读取的EEPROM内部地址(即EEPROM内部存储器的地址) */

45 I2C_SendData(EEPROM_I2C, ReadAddr);

46

47 I2CTimeout = I2CT_FLAG_TIMEOUT;

48

49 /* 检测 EV8 事件并清除标志*/

50 while (!I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED))

51 {

52 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);

53 }

54 /* 产生第二次I2C起始信号 */

55 I2C_GenerateSTART(EEPROM_I2C, ENABLE);

56

57 I2CTimeout = I2CT_FLAG_TIMEOUT;

58

59 /* 检测 EV5 事件并清除标志*/

60 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))

61 {

62 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);

63 }

64 /* 发送EEPROM设备地址 */

65 I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Receiver);

66

67 I2CTimeout = I2CT_FLAG_TIMEOUT;

68

69 /* 检测 EV6 事件并清除标志*/

70 while (!I2C_CheckEvent(EEPROM_I2C,

71 I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))

72 {

73 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);

74 }

75 /* 读取NumByteToRead个数据*/

76 while (NumByteToRead)

77 {

78 /*若NumByteToRead=1,表示已经接收到最后一个数据了,

79 发送非应答信号,结束传输*/

80 if (NumByteToRead == 1)

81 {

82 /* 发送非应答信号 */

83 I2C_AcknowledgeConfig(EEPROM_I2C, DISABLE);

84

85 /* 发送停止信号 */

86 I2C_GenerateSTOP(EEPROM_I2C, ENABLE);

87 }

88

89 I2CTimeout = I2CT_LONG_TIMEOUT;

90 while (I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)

91 {

92 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);

93 }

94 {

95 /*通过I2C,从设备中读取一个字节的数据 */

96 *pBuffer = I2C_ReceiveData(EEPROM_I2C);

97

98 /* 存储数据的指针指向下一个地址 */

99 pBuffer++;

100

101 /* 接收数据自减 */

102 NumByteToRead--;

103 }

104 }

105

106 /* 使能应答,方便下一次I2C传输 */

107 I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE);

108 return 1;

109 }

这段中的写过程跟前面的写字节函数类似,而读过程中接收数据时,需要使用库函数I2C_ReceiveData来读取。响应信号则通过库函数I2C_AcknowledgeConfig来发送,DISABLE时为非响应信号,ENABLE为响应信号。

3.    main文件
EEPROM读写测试函数

完成基本的读写函数后,接下来我们编写一个读写测试函数来检验驱动程序,见代码清单 2311。

代码清单 2311 EEPROM读写测试函数

1 /**

2 * @brief I2C(AT24C02)读写测试

3 * @param 无

5 */

6 uint8_t I2C_Test(void)

7 {

8 u16 i;

9 EEPROM_INFO("写入的数据");

10

11 for ( i=0; i<=255; i++ ) //填充缓冲

12 {

13 I2c_Buf_Write[i] = i;

14

15 printf("0x%02X ", I2c_Buf_Write[i]);

16 if (i%16 == 15)

17 printf("\n\r");

18 }

19

20 //将I2c_Buf_Write中顺序递增的数据写入EERPOM中

21 //页写入方式

22 // I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256);

23 //字节写入方式

24 I2C_EE_ByetsWrite( I2c_Buf_Write, EEP_Firstpage, 256);

25

26 EEPROM_INFO("写结束");

27

28 EEPROM_INFO("读出的数据");

29 //将EEPROM读出数据顺序保持到I2c_Buf_Read中

30 I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256);

31

32 //将I2c_Buf_Read中的数据通过串口打印

33 for (i=0; i<256; i++)

34 {

35 if (I2c_Buf_Read[i] != I2c_Buf_Write[i])

36 {

37 printf("0x%02X ", I2c_Buf_Read[i]);

38 EEPROM_ERROR("错误:I2C EEPROM写入与读出的数据不一致");

39 return 0;

40 }

41 printf("0x%02X ", I2c_Buf_Read[i]);

42 if (i%16 == 15)

43 printf("\n\r");

44

45 }

46 EEPROM_INFO("I2C(AT24C02)读写测试成功");

47 return 1;

48 }

代码中先填充一个数组,数组的内容为1,2,3至N,接着把这个数组的内容写入到EEPROM中,写入时可以采用单字节写入的方式或页写入的方式。写入完毕后再从EEPROM的地址中读取数据,把读取得到的与写入的数据进行校验,若一致说明读写正常,否则读写过程有问题或者EEPROM芯片不正常。其中代码用到的EEPROM_INFO跟EEPROM_ERROR宏类似,都是对printf函数的封装,使用和阅读代码时把它直接当成printf函数就好。具体的宏定义在"bsp_i2c_ee.h文件中",在以后的代码我们常常会用类似的宏来输出调试信息。

main函数

最后编写main函数,函数中初始化了LED、串口、I2C外设,然后调用上面的I2C_Test函数进行读写测试,见代码清单 2312。

代码清单 2312 main函数

1

2 /**

3 * @brief 主函数

4 * @param 无

5 * @retval 无

6 */

7 int main(void)

8 {

9 LED_GPIO_Config();

10

11 LED_BLUE;

12 /*初始化USART1*/

13 Debug_USART_Config();

14

15 printf("\r\n欢迎使用秉火 STM32 F429 开发板。\r\n");

16

17 printf("\r\n这是一个I2C外设(AT24C02)读写测试例程 \r\n");

18

19 /* I2C 外设(AT24C02)初始化 */

20 I2C_EE_Init();

21

22 if (I2C_Test() ==1)

23 {

24 LED_GREEN;

25 }

26 else

27 {

28 LED_RED;

29 }

30

31 while (1)

32 {

33 }

34

35 }

36

23.4.3 下载验证

用USB线连接开发板"USB TO UART"接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手可看到EEPROM测试的调试信息。

23.5 每课一问

1.    在EEPROM测试程序中,分别使用单字节写入及页写入函数写入数据,对比它们消耗的时间。

2.    尝试使用EEPROM存储int整型变量,float型浮点变量,编写程序写入数据,并读出校验。

3.    尝试把I2C通讯引脚的模式改成非开漏模式,测试是否还能正常通讯,为什么?

4.    查看"bsp_i2c_ee.h"文件中EEPROM_ERROR、EEPROM_INFO、EEPROM_DEBUG宏,解释为何要使用这样的宏输出调试信息,而不直接使用printf函数。

最新文章

  1. 一些PHP性能优化汇总
  2. nginx+nginx-rtmp-module+ffmpeg搭建流媒体服务器[转]
  3. HIVE中的几种排序
  4. mysql备份脚本
  5. 阿里云服务器如何安装memcached
  6. iOS开发知识点:理解assign,copy,retain变strong
  7. 跟Google学习Android开发-起始篇-用碎片构建一个动态的用户界面(3)
  8. nodejs定时任务node-schedule
  9. ArrayList和数组间的相互转换
  10. BitmapUtil【缩放bitmap以及将bitmap保存成图片到SD卡中】
  11. HTML DOM 事件对象 ondragend 事件
  12. JAVA多线程之CountDownLatch与join的区别
  13. Netty入门——客户端与服务端通信
  14. 移除元素的golang实现
  15. 【maven】maven查看项目依赖并解决依赖冲突的问题
  16. GoJS拖动设计
  17. Wannafly挑战赛27
  18. Chrome与之驱动对应的版本
  19. Android如何使用Https
  20. 使用 Selenium 实现基于 Web 的自动化测试

热门文章

  1. Hive学习(一)
  2. 百度 CDN公共库
  3. Linux下wget下载软件小技巧以及安装jdk、tomcat与ftp服务器
  4. WSGI学习系列Pecan
  5. http学习笔记(三):报文
  6. S3C2440 中断相关寄存器小探
  7. 织梦DEDECMS {dede:arclist},{dede:list}获取附加表字段内容
  8. Spring IOC + AOP 的实现
  9. 安装BI Publisher Desktop报错:“Template Builder Installer Failed:Unexpected Error”
  10. Touch事件传递的实验