22. I2C——读写EEPROM

本章参考资料:

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

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

22.1. I2C简介

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

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

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

22.1.1. I2C物理层

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

图 22‑1 常见的I2C通讯系统

图 22‑1 常见的I2C通讯系统

它的物理层有如下特点:

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

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

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

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

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

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

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

22.1.2. 协议层

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

22.1.2.1. I2C基本读写过程

先看看I2C通讯过程的基本结构,它的通讯过程见 图22_2图22_3图22_4

图 22‑2 主机写数据到从机

图 22‑2 主机写数据到从机

图 22‑3 主机由从机中读数据

图 22‑3 主机由从机中读数据

图 22‑4 I2C通讯复合格式

图 22‑4 I2C通讯复合格式

图例:

图例1

数据由主机传输至从机

S : 传输开始信号

SLAVE_ADDRESS: 从机地址

图例2

数据由从机传输至主机

R/W- : 传输方向选择位,1为读,0为写

A/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的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

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

22.1.2.2. 通讯的起始和停止信号

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

图 22‑5 起始和停止信号

图 22‑5 起始和停止信号

22.1.2.3. 数据有效性

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

图 22‑6 数据有效性

图 22‑6 数据有效性

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

22.1.2.4. 地址及数据方向

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

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

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

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

22.1.2.5. 响应

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

图 22‑8 响应与非响应信号

图 22‑8 响应与非响应信号

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

22.2. 瑞萨RA6M5的I2C特性及架构

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

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

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

22.2.1. 瑞萨RA6M5的I2C外设简介

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

瑞萨RA6M5的IIC支持频率

频率配置

标准模式 100-kHz

快速模式 400-kHz

快速plus模式 1-MHz

22.2.1.1. 通讯引脚

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

表 22‑1 RA6M5的I2C引脚(整理自《RA6M5 Group User_s Manual》)

引脚

I2C编号

I2C1

I2C2

I2C3

SCL

P408/P400

P512/P205

P415/P410

SDA

P407/P401

P511/P206

P411/P409

22.2.2. 瑞萨RA6M5的I2C功能框图

图

ICCR1

I2C总线控制寄存器1

ICCR2

I2C总线控制寄存器2

ICMR1

I2C总线模式寄存器1

ICMR2

I2C总线模式寄存器2

ICMR3

I2C总线模式寄存器3

ICFER

I2C总线功能使能寄存器

ICSER

I2C总线状态使能寄存器

ICIER

I2C总线中断使能寄存器

ICSR1

I2C总线状态寄存器1

ICSR2

I2C总线状态寄存器2

ICWUR

I2C总线唤醒单元寄存器

ICWUR2

I2C总线唤醒单元寄存器2

SARLy

从地址寄存器Ly (y = 0到2)

SARUy

从地址寄存器Uy (y = 0到2)

ICBRL

I2C总线比特率低寄存器

ICBRH

I2C总线比特率高寄存器

ICDRT

I2C总线发送数据寄存器

ICDRR

I2C总线接收数据寄存器

ICDRS

I2C总线移位寄存器

注解

我们这里主要介绍的是瑞萨的fsp库的使用,所以在这里寄存器我们在这里做一些简单的介绍。

22.2.3. 时钟控制逻辑

下面将介绍时钟的控制流程

22.2.3.1. IIC起始信号与重启信号

当ICCR2中的RS位被设置为1时,IIC会发出一个重启条件请求。当ICCR2的BBSY标志位为1(总线繁忙状态),ICCR2的MST位为1(主模式)时IIC发出重启条件。 发出重启条件:

  1. 释放SDAn线路。

  2. 确保ICBRL中设置的SCLn线路的低电平周期已过。

  3. 释放SCLn线(低电平到高电平)。

  4. 在SCLn线上检测高电平,并确保ICBRL中设置的时间和重启条件设置时间经过。

  5. 将SDAn线拉低(从高电平到低电平)。

  6. 确保ICBRH中设置的时间和重启条件保持时间。

  7. 驱动SCLn线低电平(高电平到低电平)。

  8. 检测SCLn线上的一个低水平,确保ICBRL中设置的SCLn线的低水平周期已经过去

../../_images/iic_time1.jpg

22.2.3.2. IIC循环发送

  1. 初始化IIC。

  2. 读取ICCR2中的BBSY标志,检查总线是否打开,然后设置ICCR2中的ST位为1(启动条件发出请求)。收到请求后,IIC发出启动条件。与此同时,BBSY位和START为被设置为1,ST位自动设置为0。如果检测到启动条件,并且SDA输出状态的内部电平和SDAn线路上的电平在ST位为1时匹配,IIC就会识别出,由于ST位已经成功完成了请求,启动条件已经成功发出。ICCR2的MST和TRS位自动设置为1,IIC处于主发送模式。当TRS位设置为1时,ICSR2中的TDRE标志也会自动设置为1。

  3. 检查ICSR2中的TDRE标志位是否为1,然后将传输值(从地址和R/W#位)写入ICDRT。传输数据写入ICDRT后,TDRE标志位自动设置为0,数据从ICDRT传输到ICDRS后,TDRE标志位再次设置为1。字节后包含从地址和当发送了R/W#位时,TRS位的值会根据发送的R/W#位的值自动更新,选择主发送或主接收模式。如果R/W#为0,则IIC继续主传输模式。如果此时ICSR2.NACKF标志位为1,表示从设备没有识别到该地址,或者通信出现错误,并向ICCR2.SP位写入1发出停止条件。如果要传输10位地址格式的数据,首先将11110b和从地址的高两位和W写入ICDRT作为第一个地址传输。然后将从地址低8位写入ICDRT作为第二个地址传输。

  4. 确认ICSR2的TDRE标志位为1后,将传输数据写入ICDRT寄存器。IIC自动保持SCLn线路处于低位,直到传输数据就绪,发出重启条件或停止条件。

  5. 当所有要传输的数据字节写入ICDRT寄存器后,等待ICSR2中的TEND标志位的值返回1。确认ICSR2的START标志位为1后,将ICSR2的START标志位设置为0。

  6. 设置ICCR2中的RS位为1(重启条件问题请求)。在接收到请求时,IIC发出一个重启条件。

  7. 检查ICSR2中的START标志位为1后,将传输值(从地址和R/W#位)写入ICDRT。

../../_images/iic_time2.jpg

22.3. EEPROM简介

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

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

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

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

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

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

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

22.4. I2C——读写EEPROM实验

22.4.1. 硬件设计

野火启明6M5开发板的 EEPROM 电路图如图所示:

图 22‑13a EEPROM硬件连接图

图 22‑13a EEPROM硬件连接图

野火启明4M2开发板的 EEPROM 电路图如图所示:

图 22‑13b EEPROM硬件连接图

图 22‑13b EEPROM硬件连接图

野火启明2L1开发板的 EEPROM 电路图如图所示:

图 22‑13c EEPROM硬件连接图

图 22‑13c EEPROM硬件连接图

EEPROM 芯片连接到 MCU 的引脚如下表所示。

EEPROM引脚

野火启明开发板

引脚连接

启明6M5

  • P415——SCL2

  • P414——SDA2

启明4M2

  • P400——SCL0

  • P401——SDA0

启明2L1

  • P400——SCL0

  • P401——SDA0

22.4.2. 软件设计

使用瑞萨官方提供的FPS库进行编程,瑞萨官方提供的FPS库具有方便、快捷、简洁的特性。

22.4.2.1. 新建工程

因为本章节的 EEPROM 相关实验例程需要用到板子上的串口功能,因此我们可以直接以前面的“19_UART_Receive_Send”工程为基础进行修改。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “22_EEPROM_Hardware”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程模板 “19_UART_Receive_Send”, 然后将工程文件夹重命名为 “22_EEPROM_Hardware”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

22.4.2.2. FSP配置

新建工程后,我们先打开 “22_EEPROM_Hardware” 项目的 FSP 配置界面进行配置。

在 FSP 配置界面里面我们依次点击 “Stacks”-> “New Stack”-> “Connectivity”-> “I2C Master” 来配置IIC模块。如 图22_14

图 22-14 加入IIC

图 22-14 加入IIC

按照图片顺序依次进行点击 然后点击 “I2C Master” 在左下角的属性界面里进行配置。如 图22_15

配置图

图 22-15 配置图

配置完成之后可以按下快捷键“Ctrl + S”保存, 最后点右上角的 “Generate Project Content” 按钮,让软件自动生成配置代码即可。

22.4.2.3. R_IIC_MASTER_Write 函数

代码清单 22_1:R_IIC_MASTER_Write 结构体
fsp_err_t R_IIC_MASTER_Write (i2c_master_ctrl_t * const p_api_ctrl,uint8_t * const p_src,uint32_t const bytes,bool const restart)

当我们调用该函数,在数据传输的开始时会发送从设备的地址位。之后根据p_src数组发送第一个位在数据位,传输的过程中硬件会自动发送确认位和结束位。 传输数据的长度与bytes有关,完成之后restart来决定此次通信之后是否通过发出重复的START条件来保持总线。

注解

在调用这个函数之后我们需要延时一段时间或者使用回调函数判断,之后再调用下一段IIC函数,详细的可以看下面IIC的写入代码, 原因是因为当你使用了R_IIC_MASTER_Write函数之后,IIC通信还未完成, 如果你再次调用其他的IIC函数就会覆盖掉第一次执行的函数, 从而出现时序错误。

22.4.2.4. R_IIC_MASTER_Read 函数

代码清单 22_2:R_IIC_MASTER_Read 结构体
fsp_err_t R_IIC_MASTER_Read (i2c_master_ctrl_t * const p_api_ctrl,uint8_t * const p_dest,uint32_t const bytes,bool const  restart)

当我们调用该函数,在数据传输的开始时会发送从设备的地址位。之后根据p_src数组保存第一个获取的数据,传输的过程中硬件会自动加载应答位和结束位。 传输数据的长度与bytes有关,完成之后restart来决定此次通信之后是否通过发出重复的START条件来保持总线。

注解

该函数与之前的R_IIC_MASTER_Write函数一样在使用之后需要需要延时一段时间或者使用回调函数判断。

22.4.2.5. 向EEPROM写入一个字节

初始化好I2C外设后,就可以使用I2C通讯了,更具上面两个函数的介绍我们就可以写出EEPROM的写入以及读取函数。我们看看如何向EEPROM写入一个字节的数据,见 代码清单22_3

代码清单 22_3:EEPROM写入一个字节函数
/**
* @brief 以单字节的方式到I2C EEPROM中
* @param
*   @arg address:写地址
*   @arg byte:写的数据
* @retval  无
*/
void I2C_EE_ByteWrite(unsigned char address, unsigned char byte)
{
   iic_complete = false;
   unsigned char send_buffer[2] = {};

   send_buffer[0] = address;
   send_buffer[1] = byte;
   R_IIC_MASTER_Write(&EEPROM_ctrl, &send_buffer[0], 2, false); //每当写完数据 false 总线拉高

   while ((I2C_MASTER_EVENT_TX_COMPLETE != g_i2c_callback_event) && timeout_ms)
   {
      R_BSP_SoftwareDelay(1U, BSP_DELAY_UNITS_MILLISECONDS);
      timeout_ms--;
   }
   timeout_ms = 500;
}

这里我们只是简单调用库函数R_IIC_MASTER_Write就可以实现,通过封装一次使用更为方便。

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

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

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

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

22.4.2.6. EEPROM的页写入

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

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

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

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

代码清单 22‑4 EEPROM的页写入
/**
* @brief   将缓冲区中的数据以页写入的方式写到I2C EEPROM中
* @param
*   @arg ptr_write:缓冲区指针
*   @arg WriteAddr:写地址
*     @arg len:写的长度
* @retval  无
*/
void I2C_EE_Writepage(unsigned char* ptr_write , unsigned char WriteAddr,unsigned char len)      //页写入   page 0~31
{

   unsigned char send_buffer[9] = {};
   send_buffer[0] = WriteAddr;

   for(unsigned char i = 0;i<len;i++)
   {
      send_buffer[1+i] = *(ptr_write+i);
   }

   R_IIC_MASTER_Write(&EEPROM_ctrl, &send_buffer[0], len+1 , false);

   while ((I2C_MASTER_EVENT_TX_COMPLETE != g_i2c_callback_event) && timeout_ms)
   {
      R_BSP_SoftwareDelay(1U, BSP_DELAY_UNITS_MILLISECONDS);
      timeout_ms--;
   }
   timeout_ms = 500;

   I2C_EE_WaitState();
}

22.4.2.7. 多字节写入

多次写入数据时,利用EEPROM的页写入方式,避免单字节读写时候的等待。多个数据写入过程 见 代码清单22_5

代码清单 22‑5 多字节写入
/**
* @brief   将缓冲区中的数据写到I2C EEPROM中
* @param
*   @arg pBuffer:缓冲区指针
*   @arg WriteAddr:写地址
*     @arg NumByteToWrite:写的字节数
* @retval  无
*/
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr,uint16_t NumByteToWrite)
{
   uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

   Addr = WriteAddr % EEPROM_PAGESIZE;
   count = EEPROM_PAGESIZE - Addr;
   NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;
   NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;

   /* If WriteAddr is I2C_PageSize aligned  */
   if (Addr == 0) {
      /* If NumByteToWrite < I2C_PageSize */
      if (NumOfPage == 0) {
            I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
      }
      /* If NumByteToWrite > I2C_PageSize */
      else {
            while (NumOfPage--) {
               I2C_EE_Writepage(pBuffer, WriteAddr, EEPROM_PAGESIZE);
               WriteAddr +=  EEPROM_PAGESIZE;
               pBuffer += EEPROM_PAGESIZE;
            }

            if (NumOfSingle!=0) {
               I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
            }
      }
   }
   /* If WriteAddr is not I2C_PageSize aligned  */
   else {
      /* If NumByteToWrite < I2C_PageSize */
      if (NumOfPage== 0) {
            I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
      }
      /* If NumByteToWrite > I2C_PageSize */
      else {
            NumByteToWrite -= count;
            NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;
            NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;

            if (count != 0) {
               I2C_EE_Writepage(pBuffer, WriteAddr, count);
               WriteAddr += count;
               pBuffer += count;
            }

            while (NumOfPage--) {
               I2C_EE_Writepage(pBuffer, WriteAddr, EEPROM_PAGESIZE);
               WriteAddr +=  EEPROM_PAGESIZE;
               pBuffer += EEPROM_PAGESIZE;
            }
            if (NumOfSingle != 0) {
               I2C_EE_Writepage(pBuffer, WriteAddr, NumOfSingle);
            }
      }
   }
}

22.4.2.8. EEPROM读取函数

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

图 22‑18 EEPROM数据读取时序

图 22‑18 EEPROM数据读取时序

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

代码清单 22_6:EEPROM读取函数
/**
* @brief 读取I2C EEPROM数据
* @param
*   @arg ptr_read:读取缓冲区指针
*   @arg address:地址
*     @arg byte:读取的字节数
* @retval  无
*/
void I2C_EE_BufferRead(unsigned char* ptr_read,unsigned char address,unsigned char byte)
{

   unsigned char send_buffer[2] = {};
   unsigned char read_buffer[1] = {};

   send_buffer[0] = address;
   R_IIC_MASTER_Write(&EEPROM_ctrl, &send_buffer[0], 1, true);
   while ((I2C_MASTER_EVENT_TX_COMPLETE != g_i2c_callback_event) && timeout_ms)
   {
      R_BSP_SoftwareDelay(400U, BSP_DELAY_UNITS_MICROSECONDS);
      timeout_ms--;
   }
   timeout_ms = 500;

   R_BSP_SoftwareDelay(250U, BSP_DELAY_UNITS_MICROSECONDS);

   R_IIC_MASTER_Read(&EEPROM_ctrl, ptr_read, byte, false);

}

这个函数是在指定的地址读取一个字节的数据,第一个变量EEPROM的地址,最后会返回一个整型的数据。如果不想使用printf函数可以将其进行注释,但需要适当增加延时时间。 这里代码非常简单,我们只需要确定I2C的地址、数据格式、数据存储指针、数据大小、超时设置,就可以把想要的数据读回来。

22.4.2.9. EEPROM测试函数

代码清单 22_7:EEPROM测试函数
/**
* @brief  I2C(AT24C02)读写测试
* @param  无
* @retval 正常返回1 ,不正常返回0
*/
uint8_t I2C_Test(void)
{
   uint16_t i;
   unsigned char DATA_Size = 30;
   unsigned char I2c_Buf_Write[33] = {};
   unsigned char I2c_Buf_Read[33] = {};

   //将I2c_Buf_Write中顺序递增的数据写入EERPOM中
   printf("写入的数据\r\n");
   for ( i=0; i<DATA_Size; i++ ) { //填充缓冲
      I2c_Buf_Write[i] =i;
      printf("0x%02X ", I2c_Buf_Write[i]);
      if (i%16 == 15)
            printf("\n");
   }
   I2C_EE_BufferWrite( I2c_Buf_Write, 0x00 , DATA_Size);



   //将EEPROM读出数据顺序保持到I2c_Buf_Read中
   printf("\r\n读出的数据\n");
   R_BSP_SoftwareDelay(10U, BSP_DELAY_UNITS_MILLISECONDS);
   I2C_EE_BufferRead(I2c_Buf_Read, 0x00 , DATA_Size);



   //将I2c_Buf_Read中的数据通过串口打印
   R_BSP_SoftwareDelay(10U, BSP_DELAY_UNITS_MILLISECONDS);
   for (i=0; i<DATA_Size; i++) {
      if (I2c_Buf_Read[i] != I2c_Buf_Write[i]) {
            printf("0x%02X \n", I2c_Buf_Read[i]);
            printf("错误:I2C EEPROM写入与读出的数据不一致\n");
            printf("%d\n",i);
            return 0;
      }
      printf("0x%02X ", I2c_Buf_Read[i]);
      if (i%16 == 15)
            printf("\n");

   }
   printf("\r\nI2C(AT24C02)读写测试成功\n");
   return 1;
}

22.4.2.10. 主函数

代码清单 22_8:主函数
void hal_entry(void)
{

   I2C_EE_Init();
   Debug_UART4_Init();

   printf("欢迎使用野火  RA6M5 开发板。\r\n");
   printf("这是一个I2C外设(AT24C02)读写测试例程 \r\n");
   R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS);

   while (1)
   {
      I2C_EE_Writedrase();
      if (I2C_Test() ==1) {
            LED_GREEN;
      } else {
            LED_RED;
      }

      while(1);
   }

#if BSP_TZ_SECURE_BUILD
   /* Enter non-secure code */
   R_BSP_NonSecureEnter();
#endif
}

22.4.3. 下载验证

保证开发板相关硬件连接正确,用Type-C线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板, 此时串口调试助手即可收到开发板发过来的数据。 在串口调试助手可看到EEPROM测试的调试信息。如下图所示:

../../_images/debug_ok.jpg