21.1 RT1052 的 LPI2C 特性及架构
直接控制 RT1052 的两个 GPIO 引脚,分别用作 SCL 及 SDA,按照上述信号的时序要求,就可以实现 I 2 C 通讯。
由于直接控制 GPIO 引脚电平产生通讯时序时,需要由 CPU 控制每个时刻的引脚状态,所以称之为“软件模拟协议”方式。
硬件协议方式:
RT1052 的 LPI2C(Low power I2C,低功耗 I2C)片上外设专门负责实现 I 2 C 通讯协议
- 只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来
- CPU 只要检测该外设的状态和访问数据寄存器,就能完成数据收发。
21.1.1 RT1052 的 LPI2C 外设简介
RT1052 的 I 2 C 外设可用作通讯的主机及从机
- 支持标准模式 100Kbit/s、快速模式 400Kbit/s、高速模式 3.4Mbit/s 以及超高速模式 5Mbit/s 的传输速率
- 支持 7 位、10 位设备地址
- 支持 DMA 数据传输
- 具有数据校验功能
- 支持 SMBus2.0 协议
- SMBus 协议与 I 2 C 类似,主要应用于笔记本电脑的电池管理中
21.1.2 RT0152 的 LPI2C 架构剖析
21.1.2.1 通讯引脚
I 2 C 的所有硬件架构都是根据图中右侧 SCL 线和 SDA 线展开的。
- RT1052 芯片有多个 I 2 C 外设,使用时必须配置到这些指定的引脚
在标准 I2C 协议之外,LPI2C 还增加了 HREQ、SCLS 及 SDAS 这三个引脚
- HREQ:用于外部设备向 LPI2C 主机申请发起通讯的请求。
- LPI2C 工作于主机模式时,可使能检测 HREQ 用于接收外部的电平信号,接收到有效的电平信号后且 I2C 总线空闲,那么 LPI2C 会发起 I2C 通讯。
- SCLS 和 SDAS:这两个被称为第二时钟和第二数据线,具有两种功能
- (1) 当 LPI2C 被配置为主、从机独立引脚的模式时,SCL 和 SDA 线用于主机模式,而 SCLS 和SDAS 线则用于从机模式,互不干扰。
- (2) LPI2C 支持 4 线工作模式以减少噪声、增强系统的鲁棒性。这种模式下 SDA 和 SCL 线用于输入,而 SDAS 和 SCLS 线则用于信号输出,同时还要在外部增加一些电阻、二极管和三极管构成的电路
21.1.2.2 驱动时钟
I2C 外设由功能时钟(Functional Clock)、外部时钟(Extern Clock)和总线时钟(BusClock)这三部分时钟驱动
功能时钟
I2C 主设备逻辑电路(Master Logic)由功能时钟驱动。
- 等价于 LPI2C 根时钟(LPI2C_CLK_ROOT)
可以使用选择的时钟来源:
- PLL3 的 8 分频,其中 PLL3 的常规配置为 480MHz,它的 8 分频即为 60MHz。
- 外部晶振 OSC ,它的常规配置为 24MHz。
- LPI2C_CLK_PODF 位配置分频
LPI2C_CLK_ROOT 输入到 LPI2C 外设内部后,经过LPI2C 内部的分频器(Prescaler)后用于驱动生成 SCL 时钟信号
- 该分频因子通过MCFGR1 寄存器的 PRESCALE 位设置
外部时钟
LPI2C 模块工作于从设备模式时,它的从设备逻辑电路(Slave Logic)直接以外部的 SCL 和 SDA 总线作为时钟进行驱动。
总线时钟
指 RT1052 内部外设总线(Internal Soc Peripheral Bus)的时钟,
- 仅用于内核访问 LPI2C 外设的控制和配置寄存器(包括 FIFO)
21.1.2.3 毛刺过滤器
SCL 和 SDA 信号线输入后都经过毛刺过滤器(Glitch Filter),用于消除输入信号的噪声
LPI2C 的主从模式下
- 对 MCFGR2 和 SCFGR2 寄存器的 FILTSDA 和 FILTSCL 位进行配置
- FILTSDA 和 FILSCL 位配置的是时钟周期数
当信号线接收到的信号周期小于对应的 FILTSDA 或 FILSCL位配置的周期时,该信号会被认为是毛刺而被过滤掉
21.1.2.4 配置寄存器和主从设备逻辑电路
主从设备逻辑电路(Master Logic 和 Slave Logic)分别用于管理主设备模式和从设备模式下 LPI2C外设的工作
- 通过配置寄存器这些寄存器可以达到控制其工作特性的目的
IIC 时序参数配置
MCCR0 寄存器
SCL 时钟信号的频率即通讯的波特率
- RT1052 的 LPI2C 外设中把 SCL 的单个周期分成低电平和高电平部分分别控制
- 上图中的 tLOW 及 tHIGH
- MCCR0 寄存器的CLKLO 和 CLKHI 寄存器位可以设置它们各占多少个功能时钟的周期
实际应用中我们可以直接通过调用 NXP 提供的库函数设置波特率
主状态寄存器 MSR
告知我们当前外设运行的情况
-
在进行通讯前可以检查 bit25 的 BBF 位
- 该位为 1 的时候表示总线忙碌
- 若同时 bit24 的MBF 位为 0,表明总线当前是由其它设备占用而导致忙碌的,
-
如通过检测 bit10 的 NDF 位即可知道从设备的响应状态。
21.1.2.5 收发数据
LPI2C 外设工作于 I2C 中的主机模式
- 有 4 个字(每个字 32 位)的“命令/发送FIFO”(Command/Tx FIFO)
- 有4 个字的“接收 FIFO”(RX FIFO)
起始、结束、应答、地址以及数据发送信号的产生,都由“命令/发送 FIFO”控制
内核可以通过向“主发送数据寄存器 MTDR”的写操作往“命令/发送 FIFO”写入内容
- 主设备逻辑电路会根据配置适时把该 FIFO 中的内容通过 I2C 总线发送出去
MTDR 寄存器
该寄存器分为 CMD 和 DATA 两个域
- CMD 域用于控制LPI2C 外设产生以上的各种 I2C 通讯信号
- DATA 域则存储要发送的数据
每次按 16 或 32 位的方式往 MTDR 寄存器写入内容后“命令/发送 FIFO”的计数指针会加 1,进入待发送状态
当 I2C 总线上接收到数据后,主设备逻辑电路会把数据存储到接收 FIFO 中
- 内核通过读取“主接收数据寄存器 MRDR”的 RXEMPTY 域可以获知接收 FIFO 中是否有数据,读取它的 DATA 域可以得到该数据
主 FIFO 状态寄存器 MFSR
通过查看“主 FIFO 状态寄存器 MFSR”的 RXCOUNT 及 TXCOUNT 域可以知道当前“接收 FIFO”和“命令/发送 FIFO”的使用状态
往“主发送数据寄存器 MTDR”写入数据前尤其要注意查看 TXCOUNT 域
- 若该值等于 FIFO 的数量时(即 4 个)表示“命令/发送 FIFO”已满
- 此时不应再对“主发送数据寄存器 MTDR”进行写入操作,而应等待至有空闲的 FIFO 再进行写入
当 LPI2C 外设工作于 I2C 中的从机模式时,收发数据时没有 FIFO 进行缓冲,直接通过“从发送数据寄存器 STDR”和“从接收数据寄存器 SRDR”进行发送和接收数据即可
21.2 LPI2C 初始化配置结构体详解
NXP 标准库提供了 LPI2C 初始化配置结构体及初始化配置函数来配置 LPI2C外设。
1 /*!
2 * @brief LPI2C 主机模式的初始化配置结构体
3 * 可以通过调用 LPI2C_MasterGetDefaultConfig() 函数
4 * 把本结构体变量赋值为一个适当的默认配置
5 */
6 typedef struct _lpi2c_master_config {
7 bool enableMaster; /*!< 是否使能主机模式 */
8 bool enableDoze; /*!< 是否使用主机的 doze 模式 */
9 bool debugEnable; /*!< 在调试模式暂停时,是否持续传输 */
10 bool ignoreAck; /*!< 是否忽略 ACK/NACK 响应 */
11 lpi2c_master_pin_config_t pinConfig; /*!< 引脚配置模式结构体 */
12 uint32_t baudRate_Hz; /*!< 期望通讯的波特率 */
13 /*!< 总线空闲检测确认时间( ns ),设置为 0 时禁止该功能 */
14 uint32_t busIdleTimeout_ns;
15 /*!< 总线低电平检测超时时间( ns ),设置为 0 时禁止该功能 */
16 uint32_t pinLowTimeout_ns;
17 /*!< SDA 信号线的毛刺滤波器宽度配置( ns ),设置为 0 时禁止该功能 */
18 uint8_t sdaGlitchFilterWidth_ns;
19 /*!< SCL 信号线的毛刺滤波器宽度配置( ns ),设置为 0 时禁止该功能 */
20 uint8_t sclGlitchFilterWidth_ns;
21 struct {
22 /*!< 使能主机请求功能 */
23 bool enable;
24 /*!< 主机请求源 */
25 lpi2c_host_request_source_t source;
26 /*!< 主机请求的有效信号极性 */
27 lpi2c_host_request_polarity_t polarity;
28 } hostRequest; /*!< 主机请求配置 */
29 } lpi2c_master_config_t;
21.2.1 enableMaster
本成员设置 LPI2C 是否工作于 I2C 通讯的主机模式,在 I2C 中主机。
22.2.2 enableDoze
本成员设置是否使能 doze 模式,
- 使能了 doze 模式后,即使 RT1052 芯片处于低功耗 stop 运行状态时 LPI2C 也能正常工作。
22.2.3 debugEnable
通过配置 de-bugEnable 可以决定在调试暂停时 LPI2C 是否依然进行数据传输。
22.2.4 ignoreAck
使能本成员配置后,LPI2C 的通讯会直接忽略其它设备的响应
- 即其它设备即使是发送了 NACK 信号,也会被认为是 ACK。这种模式并不常用。
22.2.5 pinConfig
这个成员是一个 lpi2c_master_pin_config_t 类型的枚举变量
1 /*! @brief LPI2C 引脚配置 */
2 typedef enum _lpi2c_master_pin_config {
3 /*!< LPI2C 配置为 2-pin 开漏模式 */
4 kLPI2C_2PinOpenDrain = 0x0U,
5 /*!< LPI2C 配置为 2-pin 只输出的模式 ( 超快速模式 ) */
6 kLPI2C_2PinOutputOnly = 0x1U,
7 /*!< LPI2C 配置为 2-pin 推挽模式 */
8 kLPI2C_2PinPushPull = 0x2U,
9 /*!< LPI2C 配置为 4-pin 推挽模式 */
10 kLPI2C_4PinPushPull = 0x3U,
11 /*!< LPI2C 配置为 2-pin 开漏并且主从机使用独立的总线 */
12 kLPI2C_2PinOpenDrainWithSeparateSlave = 0x4U,
13 /*!< LPI2C 配置为 2-pin 只输出的模式 ( 超快速模式 ) 并且主从机使用独立的总线 */
14 kLPI2C_2PinOutputOnlyWithSeparateSlave = 0x5U,
15 /*!< LPI2C 配置为 2-pin 推挽模式并且主从机使用独立的总线 */
16 kLPI2C_2PinPushPullWithSeparateSlave = 0x6U,
17 /*!< LPI2C 配置为 4-pin 反相输出推挽模式 */
18 kLPI2C_4PinPushPullWithInvertedOutput = 0x7U
19 } lpi2c_master_pin_config_t;
按标准的 I2C 协议,直接使用上面定义的 2 线开漏模式 kLPI2C_2PinOpenDrain 即可,其余的主从机独立总线模式、4 线模式可根据自己的应用进行扩展。
22.2.6 baudRate_Hz
本成员用于配置 I2C 通讯的波特率
- 直接给它赋予期望值即可,如 400000 表示 400KHz 的波特率
- 函数会根据此输入值和 LPI2C 根时钟 LPI2C_CLK_ROOT 配置内部分频因子 PRESCALE 和高低电平周期 CLKHI、CLKLO 等参数,以使实际的通讯波特率尽量接近这个配置值。
22.2.7 busIdleTimeout_ns
用于设置总线空闲检测的确认时间,单位为纳秒。
- 也就是说当 SCL 和 SDA线处于高电平状态(空闲状态)超过这个时间后,LPI2C 外设才会认为此时总线是空闲并设置标志位,才认为这时可以开始通讯。
- 若设置为 0 时不使用这个功能。
22.2.8 pinLowTimeout_ns
这用于设置 SCL 或 SDA 总线低电平的检测超时时间,单位为纳秒。
- 当 SCL 或 SDA 线低电平超过这个时间时,“引脚低电平超时 PLTF”标志位会被置 1,表示引脚低电平超时。
- 若 pinLowTimeout_ns 被配置为 0 则不使用这个功能。
22.2.9 sdaGlitchFilterWidth_ns 和 sclGlitchFilterWidth_ns
分别用于配置 SDA 和 SCL 信号线的毛刺过滤器
- 当信号宽度小于或等于配置值时会被认为是毛刺过滤掉。
- 单位同样为纳秒,设置为 0 时不使用本功能。
22.2.10 hostRequest
hostRequest 是一个包含三个成员的结构体变量,它用于配置主机请求相关的功
能。
- 其中 enable 用于设置是否使能主机请求功能;
- source 用于设置主机请求的来源
- 它的可选值为 HREQ 引脚(kLPI2C_HostRequestExternalPin)
- 输入触发器触发(kLPI2C_HostRequestInputTrigger)
1 /*! @brief LPI2C 主机请求来源选择 */
2 typedef enum _lpi2c_host_request_source {
3 /*!< 以 LPI2C_HREQ 引脚作为请求来源 */
4 kLPI2C_HostRequestExternalPin = 0x0U,
5 /*!< 以输入触发器作为请求源 */
6 kLPI2C_HostRequestInputTrigger = 0x1U,
7 } lpi2c_host_request_source_t;
- polarity 则用于设置主机请求来源为 HREQ 引脚时有效的电平极性
- 可选值为低电平有效(kLPI2C_HostRequestPinActiveLow)
- 高电平有效(kLPI2C_HostRequestPinActiveHigh)
1 /*! @brief LPI2C 主机请求引脚极性配置 */
2 typedef enum _lpi2c_host_request_polarity {
3 /*!< 配置 LPI2C_HREQ 引脚为低电平有效 */
4 kLPI2C_HostRequestPinActiveLow = 0x0U,
5 /*!< 配置 LPI2C_HREQ 引脚为高电平有效 */
6 kLPI2C_HostRequestPinActiveHigh = 0x1U
7 } lpi2c_host_request_polarity_t;
初始化配置结构体时,通常先直接调用 LPI2C_MasterGetDefaultConfig 函数赋予常用默认配置,然后再针对性地把初始化配置结构体修改成自己需要的内容
6 void LPI2C_MasterGetDefaultConfig(lpi2c_master_config_t *masterConfig)
7 {
8 masterConfig->enableMaster = true;
9 masterConfig->debugEnable = false;
10 masterConfig->enableDoze = true;
11 masterConfig->ignoreAck = false;
12 masterConfig->pinConfig = kLPI2C_2PinOpenDrain;
13 masterConfig->baudRate_Hz = 100000U;
14 masterConfig->busIdleTimeout_ns = 0;
15 masterConfig->pinLowTimeout_ns = 0;
16 masterConfig->sdaGlitchFilterWidth_ns = 0;
17 masterConfig->sclGlitchFilterWidth_ns = 0;
18 masterConfig->hostRequest.enable = false;
19 masterConfig->hostRequest.source = kLPI2C_HostRequestExternalPin;
20 masterConfig->hostRequest.polarity = kLPI2C_HostRequestPinActiveHigh;
21 }
还需要调用LPI2C_MasterInit 函数根据结构体的配置值向寄存器写入配置
2 * @brief 初始化 LPI2C 主机外设
3 *
4 * 本函数使能 LPI2C 时钟并根据结构体初始化 LPI2C 主机外设
5 * @param base LPI2C 设备号
6 * @param masterConfig 初始化结构体
7 * @param sourceClock_Hz LPI2C 的功能时钟,函数根据它计算波特率分频因子、
8 * 毛刺滤波器宽度及超时周期
9 */
10 void LPI2C_MasterInit(LPI2C_Type *base,
11 const lpi2c_master_config_t *masterConfig,
12 uint32_t sourceClock_Hz);
- base 参数指定要初始化哪个 LPI2C 设备,如 LPI2C1、LPI2C2 等;
- sorceClock_Hz 参数则用于通知该函数 LPI2C 功能时钟(即 LPI2C根时钟 LPI2C_CLK_ROOT)的频率,以便函数根据时钟和结构体配置计算出波特率分频因子、毛刺滤波器宽度及超时周期等要写入到寄存器域的配置值。
21.3 LPI2C 传输结构体详解
NXP 提供了传输结构体 lpi2c_master_transfer_t 和发送函数以简化 LPI2C 的数据通讯
1 /*!
2 * @brief 传输结构体
3 * 本结构体用于给 LPI2C_MasterTransferNonBlocking()
4 * 或 LPI2C_MasterTransferBlocking() 提供传输参数
5 *
6 */
7 struct _lpi2c_master_transfer {
8 uint32_t
9 flags; /*!< 传输的选项标志, 可选值为枚举类型 #_lpi2c_master_transfer_flags
10 设置为 0 或 #kLPI2C_TransferDefaultFlag 值时表示正常传输 */
11 uint16_t slaveAddress; /*!< 要访问的 I2C 设备地址( 7bit 、 10bit ) */
12 lpi2c_direction_t direction; /*!< 读 #kLPI2C_Read 或写 #kLPI2C_Write 方向 */
13 uint32_t subaddress; /*!< 子地址(设备内部的寄存器地址), MSB 先行 */
14 size_t subaddressSize; /*!< 字地址长度,最长为 4 字节 */
15 void *data; /*!< 指向要传输的数据指针 */
16 size_t dataSize; /*!< 要传输数据的字节数 */
17 };
18
19 /* 传输结构体 typedef 定义 */
20 typedef struct _lpi2c_master_transfer lpi2c_master_transfer_t;
21.3.1 flags
用于控制传输选项的标志,它的取值如下枚举:
1 /*!
2 * @brief 传输选项标志枚举变量
3 * @note 这些枚举标志支持使用 或 操作
4 * 来对 #_lpi2c_master_transfer::flags 域进行配置
5 */
6 enum _lpi2c_master_transfer_flags {
7 /*!< 传输以起始信号开始,以停止信号结束 */
8 kLPI2C_TransferDefaultFlag = 0x00U,
9 /*!< 不发送起始信号、 I2C 设备地址及子地址(寄存器地址) */
10 kLPI2C_TransferNoStartFlag = 0x01U,
11 /*!< 发送一个重复起始信号 */
12 kLPI2C_TransferRepeatedStartFlag = 0x02U,
13 /*!< 不发送停止信号 */
14 kLPI2C_TransferNoStopFlag = 0x04U,
15 };
且可通过“|”或运算同时设置几个选项
- 这些选项主要是设置传输时是否要发送起始信号、停止信号、设备地址等内容
- 使用标准 I2C 通讯时直接赋值为kLPI2C_TransferDefaultFlag 即可
21.3.2 slaveAddress
设置这次通讯要访问的 I2C 设备地址
- 注意赋值时要给它 7 位或 10 位地址,即不包含读写标志位的设备地址。
21.3.3 direction
设置本次通讯的传输方向,它可选的枚举变量值为 kLPI2C_Read(读方向)或kLPI2C_Write(写方向)。
21.3.4 subaddress
设置要访问的子地址,即 I2C 复合传输过程中的寄存器地址
- (设备内部地址),请注意区分它和上面的 I2C 设备地址结构体成员 slaveAddress。
21.3.5 subaddressSize
本成员用于表示上面 subaddress 的大小
- 若它非 0 则子地址会在复合传输的第一过程中会以写方向传输至从设备。
21.3.6 data
data 结构体成员是一个指针,使用时它指向一个缓冲区,在程序上的表现通常为一个数组的地址。
- 在写入传输方向时该数组中包含了要发送的数据
- 在读取传输方向时该数组用于保存接收到的数据
21.3.7 datasize
用于指定传输时要发送或接收数据的字节数。
调用库函数 LPI2C_MasterTransferBlocking 或LPI2C_MasterTransferNonBlocking 开始数据传输
21.4 LPI2C—读写 EEPROM
EEPOM 芯片最常用的通讯方式就是 I 2 C 协议。
21.4.1 硬件设计
EEPROM 的信号连接表
各个信号连接的说明如下:
- EEPROM 芯片 (型号:AT24C02) 的 SCL 及 SDA 引脚连接到了 RT1052 的LPI2C 外设对应的引脚中,结合上拉电阻,构成了 I2C 通讯总线,它们通过 I2C 总线进行交互。
- EEPROM 芯片的设备地址一共有 7 位,其中高 4 位固定为:1010 b,低 3 位则由 A0/A1/A2信号线的电平决定
- EEPROM 芯片中还有一个 WP 引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据,我们直接接地
21.4.2 编程要点
(1) 定义要使用的 LPI2C 号,控制相关的引脚号;
(2) 配置引脚的 MUX 复用模式及 PAD 属性配置;
(3) 配置 LPI2C 外设的时钟来源、分频得到 LPI2C 根时钟(LPI2C_CLK_ROOT);
(4) 配置 LPI2C 外设的模式、速率等参数;
(5) 编写 LPI2C 按字节收发的函数;
(6) 编写读写 EEPROM 存储内容的函数;
(7) 编写测试程序,对读写数据进行校验。
21.5.3 LPI2C 硬件相关宏定义
1 /* lpi2c 外设编号 */
2 #define EEPROM_I2C_MASTER_BASE (LPI2C1_BASE)
3 #define EEPROM_I2C_MASTER ((LPI2C_Type *)EEPROM_I2C_MASTER_BASE)
4 /* lpi2c 波特率 */
5 #define EEPROM_I2C_BAUDRATE 400000U
6
7 /*! @brief I2C 引脚定义 */
8 #define EEPROM_SCL_IOMUXC IOMUXC_GPIO_AD_B1_00_LPI2C1_SCL
9 #define EEPROM_SDA_IOMUXC IOMUXC_GPIO_AD_B1_01_LPI2C1_SDA
21.5.4 LPI2C 引脚的 IOMUXC 相关配置
1 /********************* 第 2 部分中使用的宏 **********************/
2 /* I2C 的 SCL 和 SDA 引脚使用同样的 PAD 配置 */
3 #define I2C_PAD_CONFIG_DATA (SRE_0_SLOW_SLEW_RATE| \
4 DSE_6_R0_6| \
5 SPEED_1_MEDIUM_100MHz| \
6 ODE_1_OPEN_DRAIN_ENABLED| \
7 PKE_1_PULL_KEEPER_ENABLED| \
8 PUE_0_KEEPER_SELECTED| \
9 PUS_3_22K_OHM_PULL_UP| \
10 HYS_0_HYSTERESIS_DISABLED)
11 /* 配置说明 : */
12 /* 转换速率 : 转换速率慢
13 驱动强度 : R0/6
14 带宽配置 : medium(100MHz)
15 开漏配置 : 使能
16 拉 / 保持器配置 : 使能
17 拉 / 保持器选择 : 保持器
18 上拉 / 下拉选择 : 22K 欧姆上拉 ( 选择了保持器此配置无效 )
19 滞回器配置 : 禁止 */
20
21 /************************ 第 1 部分 ****************************/
22 /**
23 * @brief 初始化 EEPROM 相关 IOMUXC 的 MUX 复用配置
24 * @param 无
25 * @retval 无
26 */
27 static void I2C_EEPROM_IOMUXC_MUX_Config(void)
28 {
29 /* SCL 和 SDA 引脚,需要使能 SION 以接收数据 */
30 IOMUXC_SetPinMux(EEPROM_SCL_IOMUXC, 1U);
31 IOMUXC_SetPinMux(EEPROM_SDA_IOMUXC, 1U);
32 }
33
34 /************************ 第 2 部分 ****************************/
35 /**
36 * @brief 初始化 EEPROM 相关 IOMUXC 的 PAD 属性配置
37 * @param 无
38 * @retval 无
39 */
40 static void I2C_EEPROM_IOMUXC_PAD_Config(void)
41 {
42 /* SCL 和 SDA 引脚 */
43 IOMUXC_SetPinConfig(EEPROM_SCL_IOMUXC, I2C_PAD_CONFIG_DATA);
44 IOMUXC_SetPinConfig(EEPROM_SDA_IOMUXC, I2C_PAD_CONFIG_DATA);
45 }
-
I2C_EEPROM_IOMUXC_MUX_Config 函数
- 它专门配置控制 EEPROM的 LPI2C 外设相关引脚的 MUX 模式。
- 该函数内部调用库函数 IOMUXC_SetPinMux,它利用前面定义的宏作为第一个输入参数,把引脚设置成 LPI2C 的 SCL 和 SDA 复用模式;
- 特别要注意的是调用 IOMUXC_SetPinMux 时的第二个参数必须设置为“1”以使能引脚的 SION功能,因为这两个引脚会被配置为开漏输出模式,同时 I2C 的通讯又需要引脚必须能读取总线上的电平状态,所以此处两个引脚都必须使能 SION 以接收数据。
-
定义一个 I2C_EEPROM_IOMUXC_PAD_Config 函数
- 它专门配置控制 EEPROM的 LPI2C 外设相关引脚的 PAD 属性
- 该函数内部调用库函数 IOMUXC_SetPinConfig,包含了 PAD 的各个配置选项,其中最重要的是使能了引脚的开漏模式
21.5.5 配置 LPI2C 的时钟和模式
1 /**** 第 1 部分使用的宏,本内容位于 bsp_i2c_eeprom.h 文件 **********/
2 /*
3 选择 LPI2C 的时钟源
4 0 derive clock from pll3_60m
5 1 derive clock from osc_clk
6 */
7 /* 选择 USB1 PLL/8 (480/8 = 60MHz) 作为 lpi2c 主机时钟源 , */
8 #define LPI2C_CLOCK_SOURCE_SELECT (0U)
9 /* lpi2c 主机 时钟源的时钟分频因子 */
10 #define LPI2C_CLOCK_SOURCE_DIVIDER (5U)
11 /* 获取 lpi2c 时钟频率 LPI2C_CLK_ROOT = 60/(5+1) = 10MHz */
12 #define LPI2C_CLOCK_FREQUENCY ((CLOCK_GetFreq(kCLOCK_Usb1PllClk) / 8) /
13 (LPI2C_CLOCK_SOURCE_DIVIDER + 1U))
14
15 #define LPI2C_MASTER_CLOCK_FREQUENCY LPI2C_CLOCK_FREQUENCY
16 /************** 以下内容位于 bsp_i2c_eeprom.c 文件 *************/
17 /**
18 * @brief 初始化 EEPROM 使用的 I2C 外设
19 * @param 无
20 * @retval 无
21 */
22 static void EEPROM_I2C_ModeInit(void)
23 {
24 lpi2c_master_config_t masterConfig;
6 /************************ 第 1 部分 ****************************/
27 /* 配置时钟 LPI2C */
28 CLOCK_SetMux(kCLOCK_Lpi2cMux, LPI2C_CLOCK_SOURCE_SELECT);
29 CLOCK_SetDiv(kCLOCK_Lpi2cDiv, LPI2C_CLOCK_SOURCE_DIVIDER);
30
31 /************************ 第 2 部分 ****************************/
32 /* 给 masterConfig 赋值为以下默认配置 */
33 /*
34 * masterConfig.debugEnable = false;
35 * masterConfig.ignoreAck = false;
36 * masterConfig.pinConfig = kLPI2C_2PinOpenDrain;
37 * masterConfig.baudRate_Hz = 100000U;
38 * masterConfig.busIdleTimeout_ns = 0;
39 * masterConfig.pinLowTimeout_ns = 0;
40 * masterConfig.sdaGlitchFilterWidth_ns = 0;
41 * masterConfig.sclGlitchFilterWidth_ns = 0;
42 */
43 LPI2C_MasterGetDefaultConfig(&masterConfig);
44
45 /************************ 第 3 部分 ****************************/
46 /* 把默认波特率改为 I2C_BAUDRATE */
47 masterConfig.baudRate_Hz = EEPROM_I2C_BAUDRATE;
48
49 /************************ 第 4 部分 ****************************/
50 /* 使用以上配置初始化 LPI2C 外设 */
51 LPI2C_MasterInit(EEPROM_I2C_MASTER,
52 &masterConfig,
53 LPI2C_MASTER_CLOCK_FREQUENCY);
54 }
55
56 /************************ 第 5 部分 ****************************/
57 /**
58 * @brief I2C 初始化
59 * @param 无
60 * @retval 无
61 */
62 void I2C_EEPROM_Init(void)
63 {
64 /* 初始化各引脚 IOMUXC 相关 */
65 I2C_EEPROM_IOMUXC_MUX_Config();
66 I2C_EEPROM_IOMUXC_PAD_Config();
67
68 /* 初始化 I2C 外设工作模式 */
69 EEPROM_I2C_ModeInit();
70 }
-
调用库函数 CLOCK_SetMux
- 选择 LPI2C 根时钟 LPI2C_CLK_ROOT 的时钟来源
- 可选值为 0(PLL3 的 8 分频,即 USB1 PLL 的 8 分频)或 1(OSC,即外部晶振)。
- 按照常规配置,PLL3 的频率为 480MHz,其 8 分频为 60MHz;而 OSC 的频率为 24MHz,选择哪个时钟源并无影响,关键时最后产生的 LPI2C 根时钟的频率。
- 第一个参数kCLOCK_Lpi2cMux 是库定义的宏,它指当前要配置时钟树的 LPI2C_MUX 节点;
- 输入的第二参数 LPI2C_CLOCK_SOURCE_SELECT 是为 0,即选择 PLL3 的 8 分频作为时钟源
- 选择 LPI2C 根时钟 LPI2C_CLK_ROOT 的时钟来源
-
调用库函数 CLOCK_SetDiv 设置时钟分频因子
- 对前面选择的时钟源进行分频,分频后得到 LPI2C 根时钟 LPI2C_CLK_ROOT。
- 第一个参数 kCLOCK_Lpi2cDiv是库定义的宏,它指当前要配置的是时钟树中 LPI2C_DIV 分频器;
- 第二个参数LPI2C_CLOCK_SOURCE_DIVIDER 是在本段代码开头自定义的宏,其宏值为 5,也就是说对时钟的分频为 6(即 5+1)。
-
调用 LPI2C_MasterGetDefaultConfig 函数
- 给函数中定义的初始化结构体变量 mas-terConfig 赋予常用默认值
- 在这样的配置中,使能了 LPI2C 的主机模式、doze 模式,使用 LPI2C 两线开漏配置,波特率为 100KHz,其它功能均被关闭。
-
对 baudRate_Hz 结构体成员重新赋值
- 赋予的值为代码清单 21‑9 定义的宏 EEPROM_I2C_BAUDRATE,即 400KHz。
-
调用库函数 LPI2C_MasterInit 初始化 LPI2C 外设
- 第一个参数为宏 EEPROM_I2C_MASTER,它的宏值为本程序使用的 LPI2C 设备号;
- 第二个参数就是初始化结构体的指针;
- 第三个参数是 LPI2C 根时钟 LPI2C_CLK_ROOT 的频率,此处使用LPI2C_MASTER_CLOCK_FREQUENCY 作为参数输入。
执行完本函数后,LPI2C 外设就完成了初始化。
- 对 IOMUXC 相关引脚操作以及 EEP-ROM_I2C_ModeInit 函数的封装
- 这样我们只要在 main 函数中直接调用 I2C_EEPROM_Init就可以了。
21.5.6 EEPROM 的页写入
EEPROM 根据标准 I2C 通讯协议写操作定义的一种页写入时序
上图为I2C 通讯中由主机发送给 EEPROM 的 SDA 信号
各个环节说明如下:
-
发送起始信号 START。
-
发送第 1 个字节:7 位的 I2C 设备地址(Device Address)加 1 位的写方向标志。
- 内容就是 0xA0,表示 EEPOM 的 I2C 设备写地址。
-
发送第 2 个字节:对于 I2C 通讯标准来说,这就是主机向从机发送的一个字节数据内容
- 在 I2C 设备地址后的这第一个“数据”,会被它识别为要写入的“内部存储单元的首地址(Word Address)”。
- 后面真正要存储到 EEPROM 的 n 个数据内容会被依次写入到 Word Address、Word Address +1、Word Address+2⋯这些地址的存储单元中。
-
发送第 3~10 个字节:在 Word Address 之后,可连续发送 n 个数据
- 这些数据会被存储到EEPROM 中。
- AT24C02 型号的 EEPROM 每页的大小为 8 个字节 (即 n = 8 ),即主机对它的一次页写入操作最多可以发送 8 个字节数据。
- 某些型号的芯片每个页写入时序最多可传输16 个数据。
-
发送停止信号 STOP。
1 /********* 第 1 部分,本内容位于 bsp_i2c_eeprom.h 文件 **********/
2 /* EEPROM 的总空间大小 */
3 #define EEPROM_SIZE 256
4 /* AT24C01/02 每页有 8 个字节 */
5 #define EEPROM_PAGE_SIZE 8
6 /* EEPROM 设备地址 */
7 #define EEPROM_ADDRESS_7_BIT (0xA0>>1)
8 #define EEPROM_WRITE_ADDRESS_8_BIT (0xA0)
9 #define EEPROM_READ_ADDRESS_8_BIT (0xA1)
10 /* EEPROM 内部存储单元地址的大小,单位:字节 */
11 #define EEPROM_INER_ADDRESS_SIZE 0x01
12
13 /************* 以下内容位于 bsp_i2c_eeprom.c 文件 ***************/
14 /************************ 第 2 部分 ****************************/
15 /**
16 * @brief 向 EEPROM 写入最多一页数据
17 * @note 调用本函数后必须调用 I2C_EEPROM_WaitStandbyState 进行等待
18 * @param ClientAddr:EEPROM 的 I2C 设备地址 (8 位地址 )
19 * @param WriteAddr: 写入的存储单元首地址
20 * @param pBuffer: 缓冲区指针
21 * @param NumByteToWrite: 要写的字节数,不可超过 EEPROM_PAGE_SIZE 定义的值
22 * @retval 1 不正常, 0 正常
23 */
24 uint32_t I2C_EEPROM_Page_Write( uint8_t ClientAddr,
25 uint8_t WriteAddr,
26 uint8_t* pBuffer,
27 uint8_t NumByteToWrite)
28 {
29 /************************ 第 3 部分 ****************************/
30 lpi2c_master_transfer_t masterXfer = {0};
31 status_t reVal = kStatus_Fail;
32
33 /************************ 第 4 部分 ****************************/
34 if (NumByteToWrite>EEPROM_PAGE_SIZE) {
35 EEPROM_ERROR("NumByteToWrite>EEPROM_PageSize\r\n");
36 return 1;
37 }
38
39 /************************ 第 5 部分 ****************************/
40 /* subAddress = WriteAddr, data = pBuffer 发送至从机
41 起始信号 start + 设备地址 slaveaddress(w 写方向 ) +
42 发送缓冲数据 tx data buffer + 停止信号 stop */
43
44 masterXfer.slaveAddress = (ClientAddr>>1);
45 masterXfer.direction = kLPI2C_Write;
46 masterXfer.subaddress = WriteAddr;
47 masterXfer.subaddressSize = EEPROM_INER_ADDRESS_SIZE;
48 masterXfer.data = pBuffer;
49 masterXfer.dataSize = NumByteToWrite;
50 masterXfer.flags = kLPI2C_TransferDefaultFlag;
51
52 /************************ 第 6 部分 ****************************/
53 reVal = LPI2C_MasterTransferBlocking(EEPROM_I2C_MASTER, &masterXfer);
54
55 if (reVal != kStatus_Success) {
56 return 1;
57 }
58 return 0;
59 }
第 1 部分。定义 EEPROM 相关的信息
- 页大小 EEPROM_PAGE_SIZE、EEPROM 的 7 位、8 位 I2C 设备地址以及内部存储单元地址的大小 EEPROM_INER_ADDRESS_SIZE。
第 2 部分。定义函数 I2C_EEPROM_Page_Write 封装页写入操作,这个函数包含四个输入参
数:
- ClientAddr:目标设备的 8 位 I2C 设备写地址。本例子中使用 EEPROM_WRITE_ADDRESS_8_BIT 作为输入参数。
- WriteAddr:本次页写入的 EEPROM 内部存储单元首地址,即图 21‑26 中的 Word Address。
- pBuffer:这是一个指针,它指向要传输的数据内容。例如要写入一页数据,可以定义一个
8 字节的数组,数组中包含要写入到 EEPROM 的内容,调用本函数时把该数组的地址作为
pBuffer 指针输入即可。 - NumByteToWrite:要传输的数据字节数据,由于是页写入,该值不能大于宏 EEP-
ROM_PAGE_SIZE 表示的页大小。
第 3 部分。使用 LPI2C 传输结构体类型定义一个 masterXfer 变量,
- 后续对该结构体进行赋值定制 LPI2C 的传输操作。
第 4 部分。确认要传输的字节数 NumByteToWrite 小于等于页大小 EEPROM_PAGE_SIZE,
否则函数直接返回退出。
- 在该条件分支中函数返回退出前调用了宏 EEPROM_ERROR 输出错误信息
- EEPROM_ERROR 是一个对 PRINTF 函数封装的宏,以便于区分不同的调试信息
第 5 部分。根据函数输入参数及 EEPROM 的特性对 masterXfer 变量赋值:
-
- slaveAddress 为 7 位地址,所以要对输入参数进行右移 1 位的操作 (ClientAddr»1)。
-
- direction 为 kLPI2C_Write,表示写入方向。
-
- subaddress 对应 EEPROM 内部存储单元的首地址,所以其值为输入参数 WriteAddr。
-
- subaddressSize 对应 EEPROM 内部存储单元的地址的大小,此处直接赋值为第 1 部分定义的宏 EEPROM_INER_ADDRESS_SIZE,即 1 个字节大小。
-
- data 是要传输的数据指针,即输入参数中要传输的数据指针 pBuffer。
-
- dataSize 是要传输的数据字节数,即输入参数中的 NumByteToWrite。
-
- flags 表 示 本 次 传 输 的 选 项, 此 处 赋 值kLPI2C_TransferDefaultFlag 表示正常传输,即包含起始、结束条件和设备地址等内容。
第 6 部分。调用库函数 LPI2C_MasterTransferBlocking
- 调用本函数会阻塞后续的代码执行,直至 I2C 通讯正常完成或失败返回。
- 本代码在调用后通过其返回值确认是否正常完成。
21.5.7 写入操作后的状态等待
在写入通讯结束后,要先等待 EEPROM 内部擦写完毕再进行后续访问
1 /********* 第 1 部分,本内容位于 bsp_i2c_eeprom.h 文件 **********/
2 /* 等待超时时间 */
3 #define I2CT_FLAG_TIMEOUT ((uint32_t)0x100)
4 #define I2CT_LONG_TIMEOUT ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
5
6 /************ 以下内容位于 bsp_i2c_eeprom.c 文件 ************/
7
8 /************************ 第 2 部分 ****************************/
9 /**
10 * @brief 用于等待 EEPROM 的内部写入时序,
11 * 在 I2C_EEPROM_Page_Write 函数后必须被调用
12 * @param ClientAddr: 设备地址( 8 位地址)
13 * @retval 等待正常为 0 ,等待超时为 1
14 */
15 uint8_t I2C_EEPROM_WaitStandbyState(uint8_t ClientAddr)
16 {
17 status_t lpi2c_status;
18 /* 等待超过 40*I2CT_LONG_TIMEOUT us 后认为超时 */
19 uint32_t delay_count = I2CT_LONG_TIMEOUT;
20
21 do {
22 /************************ 第 3 部分 ****************************/
23 /* 清除非应答标志,以便下一次检测 */
24 LPI2C_MasterClearStatusFlags(EEPROM_I2C_MASTER, kLPI2C_ MasterNackDetectFlag);
25
26 /* 对 EEPROM 发出写方向的寻址信号,以检测是否响应 */
27 lpi2c_status = LPI2C_MasterStart(EEPROM_I2C_MASTER, (ClientAddr>>1), kLPI2C_Write);
28
29 /* 必须等待至少 30us ,才会产生非应答标志 */
30 EEPROM_DELAY_US(40);
31
32 /************************ 第 4 部分 ****************************/
33 /* 检测 LPI2C MSR 寄存器的 NDF 标志,并且确认 delay_count 没减到 0 (减到 0 认
为超时,退出) */
34 } while (EEPROM_I2C_MASTER->MSR & kLPI2C_MasterNackDetectFlag
35 && delay_count-- );
36
37 /************************ 第 5 部分 ****************************/
38 /* 清除非应答标志,防止下一次通讯错误 */
39 LPI2C_MasterClearStatusFlags(EEPROM_I2C_MASTER, kLPI2C_MasterNackDetectFlag);
40
41 /* 产生停止信号,防止下次通讯出错 */
42 lpi2c_status = LPI2C_MasterStop(EEPROM_I2C_MASTER);
43 /* 必须等待至少 10us ,确保停止信号发送完成 */
44 EEPROM_DELAY_US(10);
45 /************************ 第 6 部分 ****************************/
46 /* 产生失败或前面的等待超时 */
47 if (delay_count == 0 || lpi2c_status != kStatus_Success) {
48 I2C_Timeout_Callback(3);
49 return 1;
50 }
51
52 return 0;
53 }
54
55 /************************ 第 7 部分 ****************************/
56 /**
57 * @brief IIC 等待超时调用本函数输出调试信息
58 * @param None.
59 * @retval 返回 0xff ,表示 IIC 读取数据失败
60 */
61 static uint32_t I2C_Timeout_Callback(uint8_t errorCode)
62 {
63 /* 在此处进行错误处理 */
64 EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
65 return 0xFF;
66 }·
向 I2C 总线发送 EEPROM 的设备地址并检测是否有响应
- 若检测到 EEPROM 返回应答信号,表示 EEPROM 的内部写时序完毕
- 否则重复以上过程等待至有应答或等待超时。
检 测 响 应 是 通 过 读 取 RT1052 的 LPI2C->MSR 寄 存 器 的 NDF 位即kLPI2C_MasterNackDetectFlag 标志实现的
- 当检测不到响应的时候 NDF 位为 1
- 检测到响应的时候该位为 0。
每次调用完 I2C_EEPROM_Page_Write 函数传输完数据后,都必须调用这个 I2C_EEPROM_WaitStandbyState 函数等待 EEPROM 内部写入完成,再进行其它访问操作。
21.5.8 无限制地写入多个字节
利用 EEPROM 的页写入方式,每次最多只可写入 8 个字节
- 当跨页写入时,在下一页的页首必须重新发送起始信号
- 第 0、8、16、24 等这些按 8 字节对齐的地址是 AT24C02 型号 EEPROM 每页的页首,往这些地址写入时,必须重新发送起始信号及存储器内部地址。
要往第 4、5、6、7、8、9、10、11 这 8 个地址写入内容
-
不能只进行一次 8 字节的页写入,即发送内部地址 4 后面直接跟着 8 个数据
- 这样写入的话地址 4、5、6、7 会被写入两次
- 最终内容会被覆盖为最后后 4 字节的数据,而地址 8、9、10、11 的内容丝毫不会被改变;
-
正确的操作是用两次页写入过程实现
- 第一次发送内部地址 4 后跟着 4 个数据,这些数据会被写入到地址 4、5、6、7
- 第二次发送内部地址 8 后再跟着 4 个数据,这些数据会被写入到地址 8、9、10、11。
-
只有当首地址是 8 字节对齐的时候,才能通过一次的页写入最多 8 个字节的数据。
I2C_EEPROM_Buffer_Write 函数对页写入函数 I2C_EEPROM_Page_Write 再进行一次封装
1 /************************ 第 1 部分 ****************************/
2 /**
3 * @brief 向 EEPROM 写入不限量的数据
4 * @param ClientAddr:EEPROM 的 I2C 设备地址 (8 位地址 )
5 * @param WriteAddr: 写入的存储单元首地址
6 * @param pBuffer: 缓冲区指针
7 * @param NumByteToWrite: 要写的字节数
8 * @retval 无
9 */
10 void I2C_EEPROM_Buffer_Write( uint8_t ClientAddr,
11 uint8_t WriteAddr,
12 uint8_t* pBuffer,
13 uint16_t NumByteToWrite)
14 {
15 /************************ 第 2 部分 ****************************/
16 uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
17 /* 后续要处理的字节数,初始值为 NumByteToWrite*/
18 uint8_t NumByteToWriteRest = NumByteToWrite;
19
20 /************************ 第 3 部分 ****************************/
21 /* 根据以下情况进行处理:
22 1. 写入的首地址是否对齐
23 2. 最后一次写入是否刚好写满一页 */
24 Addr = WriteAddr % EEPROM_PAGE_SIZE;
25 count = EEPROM_PAGE_SIZE - Addr;
26
27 /* 若 NumByteToWrite > count :
28 第一页写入 count 个字节,对其余字节再进行后续处理,
29 所以用 (NumByteToWriteRest = NumByteToWrite - count)
30 求出后续的 NumOfPage 和 NumOfSingle 进行处理。
31 若 NumByteToWrite < count :
32 即不足一页数据,直接用 NumByteToWriteRest = NumByteToWrite
33 求出 NumOfPage 和 NumOfSingle 即可 */
34 NumByteToWriteRest = (NumByteToWrite > count) ? (NumByteToWrite - count):NumByteToWrite;
37 /* 要完整写入的页数(不包括前 count 字节) */
38 NumOfPage = NumByteToWriteRest / EEPROM_PAGE_SIZE;
39 /* 最后一页要写入的字节数(不包括前 count 字节) */
40 NumOfSingle = NumByteToWriteRest % EEPROM_PAGE_SIZE;
41
42 /************************ 第 4 部分 ****************************/
43 /* NumByteToWrite > count 时,需要先往第一页写入 count 个字节
44 NumByteToWrite < count 时无需进行此操作 */
45 if (count != 0 && NumByteToWrite > count) {
46 I2C_EEPROM_Page_Write(ClientAddr, WriteAddr, pBuffer, count);
47 I2C_EEPROM_WaitStandbyState(ClientAddr);
48 WriteAddr += count;
49 pBuffer += count;
50 }
51
52 /************************ 第 5 部分 ****************************/
53 /* 处理后续数据 */
54 if (NumOfPage== 0 ) {
55 I2C_EEPROM_Page_Write(ClientAddr, WriteAddr, pBuffer, NumOfSingle);
56 I2C_EEPROM_WaitStandbyState(ClientAddr);
57 } else {
58 /************************ 第 6 部分 ****************************/
59 /* 后续数据大于一页 */
60 while (NumOfPage--) {
61 I2C_EEPROM_Page_Write(ClientAddr, WriteAddr, pBuffer, EEPROM_PAGE_
,→ SIZE);
62 I2C_EEPROM_WaitStandbyState(ClientAddr);
63 WriteAddr += EEPROM_PAGE_SIZE;
64 pBuffer += EEPROM_PAGE_SIZE;
65 }
66 /************************ 第 7 部分 ****************************/
67 /* 最后一页 */
68 if (NumOfSingle != 0) {
69 I2C_EEPROM_Page_Write(ClientAddr, WriteAddr, pBuffer,␣
,→ NumOfSingle);
70 I2C_EEPROM_WaitStandbyState(ClientAddr);
71 }
72 }
73 }
第 1 部分。函数包含四个输入参数
- 分别是 I2C 设备地址
- 要写入的存储器内部首地址
- 指向要写入数据的指针以及要写入的字节数
第 2 部分。定义一些变量
- NumOfPage 存储要完整写入一页的页数。
- NumOfSingle 存储最后不满一页要写入的字节数。
- Addr 存储写入首地址 WriteAddr 在要写入的页中是第几个字节。
- count 存储第一次页写入要写入多少个字节。
- NumByteToWriteRest 存储除去 count 个字节后剩余的字节数。
第 3 部分。根据输入参数求解以上各个变量的值
第 4 部分。按照前面的,先进行第一次页写入操作,写入 count 个字节
- 注意在页写入操作后调用了 I2C_EEPROM_WaitStandbyState 函数等待 EEPROM 内部时序完成
- 然后对写入地址 WriteAddr 及数据指针 pBuffer 进行相应的偏移。
- 特别地,这一部分并不是在任何情况下都会执行,它是被包含在条件分支内的,当 count 不等于 0 且 NumByteToWrite >count 才会被执行
- 当 NumByteToWrite < count 时,说明要写的数据不足一页,那么直接写入 NumByteToWrite 个字节即可,不需要这个环节。
第 5 部分。
- 当 NumOfPage 等于 0 的时候,等同于上面的 NumByteToWrite < count 条件,即要写入的数据不足一页
- 这时 NumOfSingle 等于 NumByteToWrite,所以直接进行一次页写入 NumOfSingle 个字节后就可以完成并退出整个函数。
第 6 部分。
- 这部分位于 NumOfPage 不等于 0 的分支中,它直接循环 NumOfPage 次完整的页写入操作
- 写入后同样需要等待 EEPROM 的内部写入时序完成以及对 WriteAddr 和 pBuffer进行偏移。
第 7 部分。
- 这是要写入的最后一页,即写入剩余不足一页的 NumOfSingle 个数据,等待内部时序结束后退出函数。
EEPROM 是支持随机访问的 (直接读写任意一个地址),也就是说使用页写入操作对任意一个地址写入一个字节数据是可以的,数据写入前不需要擦除整页。
21.5.9 从 EEPROM 读取数据
EEPROM 读取数据是一个复合的 I2C 时序,它实际上包含一个写过程和一个读过程
EEPROM 会向主机返回从“存储器内部地址”开始的数据
- 一个字节一个字节地传输,只要主机的响应为“应答信号”,它就会一直传输下去
- 主机想结束传输时,就发送“非应答信号”,并以“停止信号”结束通讯,作为从机的 EEPROM 也会停止传输
1 /**
2 * @brief 从 EEPROM 里面读取一块数据
3 * @param ClientAddr:EEPROM 的 I2C 设备地址 (8 位地址 )
4 * @param pBuffer[out]: 存放从 EEPROM 读取的数据的缓冲区指针
5 * @param ReadAddr: 接收数据的 EEPROM 的地址
6 * @param NumByteToWrite: 要从 EEPROM 读取的字节数
7 * @retval 无
8 */
9 uint32_t I2C_EEPROM_BufferRead( uint8_t ClientAddr,
10 uint8_t ReadAddr,
11 uint8_t* pBuffer,
12 uint16_t NumByteToRead)
13 {
14 lpi2c_master_transfer_t masterXfer = {0};
15 status_t reVal = kStatus_Fail;
16
17 /* subAddress = ReadAddr, data = pBuffer 自从机处接收
18 起始信号 start + 设备地址 slaveaddress(w 写方向 ) + 子地址 subAddress +
19 重复起始信号 repeated start + 设备地址 slaveaddress(r 读方向 ) +
20 接收缓冲数据 rx data buffer + 停止信号 stop */
21 masterXfer.slaveAddress = (ClientAddr>>1);
22 masterXfer.direction = kLPI2C_Read;
23 masterXfer.subaddress = (uint32_t)ReadAddr;
24 masterXfer.subaddressSize = EEPROM_INER_ADDRESS_SIZE;
25 masterXfer.data = pBuffer;
26 masterXfer.dataSize = NumByteToRead;
27 masterXfer.flags = kLPI2C_TransferDefaultFlag;
28
29 reVal = LPI2C_MasterTransferBlocking(EEPROM_I2C_MASTER, &masterXfer);
30
31 if (reVal != kStatus_Success) {
32 return 1;
33 }
34 return 0;
35 }
传输结构体 masterXfer 的 direction 成员值为 kLPI2C_Read 表示读方向。
- 直接通过这个函数就可以实现整个 EEPROM 的读取操作
- 读取数据不需要等待 EEPROM 的内部写时序,也没有读取数据个数的限制
21.5.10 测试代码
/* 测试读写的数据个数 */
2 #define EEPROM_TEST_NUM 256
3 /* 测试的起始地址 */
4 #define EEPORM_TEST_START_ADDR 0
5
6 /* 读写缓冲区 */
7 uint8_t EEPROM_Buffer_Write[256];
8 uint8_t EEPROM_Buffer_Read[256];
9
10 /**
11 * @brief I2C(AT24C02) 读写测试
12 * @param 无
13 * @retval 正常返回 0 ,不正常返回 1
14 */
15 uint8_t EEPROM_Test(void)
16 {
17 uint16_t i;
19 EEPROM_INFO("写入的数据");
20
21 for ( i=0; i<EEPROM_TEST_NUM; i++ ) { // 填充缓冲
22 EEPROM_Buffer_Write[i] = i;
23
24 PRINTF("0x%02X ", EEPROM_Buffer_Write[i]);
25 if ((i+1)%10 == 0 || i == (EEPROM_TEST_NUM-1))
26 PRINTF("\r\n");
27 }
28
29 // 将 I2c_Buf_Write 中顺序递增的数据写入 EERPOM 中
30 I2C_EEPROM_Buffer_Write(EEPROM_WRITE_ADDRESS_8_BIT,
31 EEPORM_TEST_START_ADDR,
32 EEPROM_Buffer_Write,
33 EEPROM_TEST_NUM);
34
35 EEPROM_INFO("写成功");
36
37 EEPROM_INFO("读出的数据");
38 // 将 EEPROM 读出数据顺序保持到 I2c_Buf_Read 中
39 I2C_EEPROM_BufferRead(EEPROM_READ_ADDRESS_8_BIT,
40 EEPORM_TEST_START_ADDR,
41 EEPROM_Buffer_Read,
42 EEPROM_TEST_NUM);
43
44 // 将 I2c_Buf_Read 中的数据通过串口打印
45 for (i=0; i<EEPROM_TEST_NUM; i++) {
46 if (EEPROM_Buffer_Read[i] != EEPROM_Buffer_Write[i]) {
47 PRINTF("0x%02X ", EEPROM_Buffer_Read[i]);
48 RGB_LED_COLOR_RED;
49 EEPROM_ERROR("错误:I2C EEPROM 写入与读出的数据不一致");
50 return 1;
51 }
52 PRINTF("0x%02X ", EEPROM_Buffer_Read[i]);
53 if ((i+1)%10 == 0 || i == (EEPROM_TEST_NUM-1))
54 PRINTF("\r\n");
55 }
56
57 RGB_LED_COLOR_GREEN;
58 EEPROM_INFO("I2C(AT24C02) 读写测试成功");
59 return 0;
60 }
21.5.11 主函数
1 /**
2 * @brief 主函数
3 * @param 无
4 * @retval 无
5 */
6 int main(void)
7 {
8 /* 初始化内存保护单元 */
9 BOARD_ConfigMPU();
10 /* 初始化开发板引脚 */
11 BOARD_InitPins();
12 /* 初始化开发板时钟 */
13 BOARD_BootClockRUN();
14 /* 初始化调试串口 */
15 BOARD_InitDebugConsole();
16 /* 打印系统时钟 */
17 PRINTF("\r\n");
18 PRINTF("***** 欢迎使用 野火 i.MX RT1052 开发板 *****\r\n");
19 PRINTF("CPU: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_CpuClk));
20 PRINTF("AHB: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_AhbClk));
21 PRINTF("SEMC: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_SemcClk));
22 PRINTF("SYSPLL: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_SysPllClk));
23 PRINTF("SYSPLLPFD0: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_
,→ SysPllPfd0Clk));
24 PRINTF("SYSPLLPFD1: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_
,→ SysPllPfd1Clk));
25 PRINTF("SYSPLLPFD2: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_
,→ SysPllPfd2Clk));
26 PRINTF("SYSPLLPFD3: %d Hz\r\n", CLOCK_GetFreq(kCLOCK_
,→ SysPllPfd3Clk));
27
28 PRINTF("读写 EEPROM\r\n");
29
30 /* 初始化 LED 引脚 */
31 LED_GPIO_Config();
32
33 /* 初始化 EEPROM */
34 I2C_EEPROM_Init();
35
36 /* 进行写入测试 */
37 EEPROM_Test();
38
39 while (1) {
40 }
41 }