数码之家

 找回密码
 立即注册

QQ登录

只需一步,快速开始

微信登录

微信扫一扫,快速登录

搜索
查看: 436|回复: 0

[Arduino] 《ESP32S3 Arduino开发指南》第十四章 IIC_EEPROM实验

[复制链接]
发表于 2025-5-12 09:27:15 | 显示全部楼层 |阅读模式
第十四章 IIC_EEPROM实验

本章,我们将学习ESP32-S3的硬件IIC接口,将会大家如何使用IIC接口去驱动24C02器件。在本章中,实现和24C02之间的双向通信,并把数据通过串口打印出来。
本章分为如下几个小节:
14.1 IIC24C02介绍
14.2 硬件设计
14.3 软件设计
14.4 下载验证


14.1 IIC24C02介绍
14.1.1 IIC介绍
IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器以及其外围设备。它是由数据线SDA和时钟线SCL构成的串行总线,可发送和接收数据,在CPU与被控IC之间、ICIC之间进行双向传送。
IIC总线有如下特点:
①总线由数据线SDA和时钟线SCL构成的串行总线,数据线用来传输数据,时钟线用来同步数据收发。
②总线上每一个器件都有一个唯一的地址识别,所以我们只需要知道器件的地址,根据时序就可以实现微控制器与器件之间的通信。
③数据线SDA和时钟线SCL都是双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是高电平。
④总线上数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可达3.4Mbit/s。
⑤总线支持设备连接。在使用IIC通信总线时,可以有多个具备IIC通信能力的设备挂载在上面,同时支持多个主机和多个从机,连接到总线的接口数量只由总线电容400pF的限制决定。IIC总线挂载多个器件的示意图,如下图所示。
图14.1.1.1 IIC总线挂载多个器件
下面来学习IIC总线协议,IIC总线时序图如下所示:
                                               图14.1.1.2 IIC总线时序图
为了便于大家更好的了解IIC协议,我们从起始信号、停止信号、应答信号、数据有效性、数据传输以及空闲状态等6个方面讲解,大家需要对应图14.1.1.2的标号来理解。
① 起始信号
SCL为高电平期间,SDA由高到低的跳变。起始信号是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在起始信号产生后,总线就处于被占用状态,准备数据传输。
② 停止信号
SCL为高电平期间,SDA由低到高的跳变。停止信号也是一种电平跳变时序信号,而不是一个电平信号。该信号由主机发出,在停止信号发出后,总线就处于空闲状态。
③ 应答信号
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
观察上图标号③就可以发现,有效应答的要求是从机在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。如果接收器是主机,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主机接收器发送一个停止信号。
④ 数据有效性
IIC总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。数据在SCL的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定。
⑤ 数据传输
I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。数据位的传输是边沿触发。
⑥ 空闲状态
IIC总线的SDASCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
了解前面的知识后,下面介绍一下IIC的基本的读写通讯过程,包括主机写数据到从机即写操作,主机到从机读取数据即读操作。下面先看一下写操作通讯过程图,如下图所示。
图14.1.1.3 写操作通讯过程图
主机首先在IIC总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地址+0(写操作)组成的8bit数据,所有从机接收到该8bit数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
接着讲解一下IIC总线的读操作过程,先看一下读操作通讯过程图,如下图所示。
图14.1.1.4 读操作通讯过程图
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现,都是由主机发出起始信号,接着发送从机地址+1(读操作)组成的8bit数据,从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回8bit数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据。
14.1.2 IIC控制器介绍
ESP32-S3有两个IIC总线接口,根据用户的配置,总线接口可以用作IIC主机或从机模式。 IIC接口特点:
可支持标准模式(100Kbit/s)、快速模式(400Kbit/s),速度最高可达800Kbit/s,但受限于SCLSDA上拉强度。
可支持7位寻址模式和10位寻址模式
可支持双地址(从机地址和从机寄存器地址)寻址模式
下面介绍一下ESP32S3IIC主机写入从机,7位寻址,单次命令序列的场景,如下图所示。
14.1.2.1 IIC主机写7位寻址的从机
ESP32-S3硬件IIC控制器中,都有相对应的空间存放相对应的内容。比如上图中,在cmd内存区中存放的是就是命令序列,就比如前面提及到的起始信号、写过程、读过程、停止信号;在RAM内存区中存放的就是某些命令序列携带的内容。
当主机在软件配置好命令序列和RAM数据后,操作寄存器启动数据传输时。控制器的行为可分为以下四步:
1、 等待SCL线位高电平,以避免SCL线被其他主机或者从机占用。
2、 执行RSTART命令发送START位。即发送起始信号。
3、 执行WRITE命令从RAM的首地址开始取出N+1个字节并一次发送给从机,其中第一个字节为地址。这个过程中会产生对应的时序,携带数据进行发送。
4、 发送STOP命令,即发送停止信号。
14.1.3 24C02介绍
首先科普一下何为EEPROM存储器?
其实EEPROM全程是“电可擦除可编程只读存储器”,即“Electrically Erasable Programmable Read-Only Memory”,特性就是数据掉电不丢失。
24C02是一个2K bit的串行EEPROM存储器,内部含有256个字节。在24C02里面还有一个8字节的页写缓冲器。该设备的通信方式IIC,通过其SCLSDA与其他设备通信,芯片的引脚图如下图所示。
图14.1.3.1 24C02引脚图
上图的WP引脚是写保护引脚,接高电平只读,接地允许读和写,我们的板子设计是把该引脚接地。每一个设备都有自己的设备地址,24C02也不例外。前面提及到有7位寻址、11位寻址,这里的位数就是设备地址位数,24C02的设备地址就是7位的,具体格式如下图所示。
图14.1.3.2 24C02地址格式
24C02的设备地址是包括不可编程部分和可编程部分,不可编程部分也就是1010”,可编程部分是根据上图的硬件引脚A0A1A2所决定。根据我们的板子设计,A0A1A2均接地处理,所以24C02设备地址为1010000”即0x50。
这里还会涉及到24C02通信地址的概念,通信地址就是写操作地址和读操作地址,简单来说,就是设备地址和一个读写位的配合。上图中的地址格式最后一位R/W用于设置数据的传输方向,即读操作/写操作,0是写操作,1是读操作,所以24C02的读操作地址为:0xA10x50 << 1 | 1),写操作地址为:0xA00x50 << 1 | 0)。
下面把实验中到的数据传输时序讲解一下,分别是对24C02的写时序和读时序。24C02写时序图如下图所示。
图14.1.3.3 24C02写时序图
上图展示的主机向24C02写操作时序图,主机在IIC总线发送第1个字节的数据为24C02的写操作地址0xA0(设备地址0x50 << 1 | 0),用于寻找总线上找到24C02,在获得24C02的应答信号之后,继续发送第2个字节数据,该字节数据是24C02的内存地址,再等到24C02的应答信号,主机继续发送第3字节数据,这里的数据即是写入在第2字节内存地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
上面的写操作只能单字节写入到24C02,效率比较低,所以24C02有页写入时序,大大提高了写入效率,下面看一下24C02页写时序图,如下图所示。
图14.1.3.4 24C02页写时序
在单字节写时序时,每次写入数据时都需要先写入设备的内存地址才能实现,在页写时序中,只需要告诉24C02第一个内存地址1,后面数据会按照顺序写入到内存地址2,内存地址3等,大大节省了通信时间,提高了时效性。因为24C02每次只能8bit数据,所以它的页大小也就是1字节。页写时序的操作方式跟上面的单字节写时序差不多,所以不作过多解释了。参考以上说明去理解页写时序。
说完两种写入方式之后,下图是关于24C02的读时序。
图14.1.3.5 24C02读时序图
24C02读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送24C02的写操作地址0xA0(设备地址0x50 << 1 | 0),获取从机应答信号后,接着发送需要读取的内存地址;在读时序中,起始信号产生后,主机发送24C02的读操作地址0xA1(设备地址0x50 << 1 | 1),获取从机应答信号后,接着从机返回刚刚在写时序中内存地址的数据,以字节为单位传输在总线上,假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,当主机发出的是非应答信号并以停止信号发出为结束,从机就会结束传输。
14.1.4 IIC接口函数介绍
本小节介绍到的函数可在以下文件中找到:
Arduino15\packages\esp32\hardware\esp32\2.0.11\libraries\Wire\src\Wire.cpp
Wire.cpp中已经定义好了两个IIC对象WireWire1,对应的就是IIC0IIC1,直接使用它们即可。
接下来,我们介绍一下本章节所用到的IIC作为主机模式相关函数。
第一个函数:begin函数,该函数功能是初始化IIC连接,并作为主设备加入IIC
bool TwoWire::begin(int sdaPin, int sclPin, uint32_t frequency);
参数sdaPin为IIC总线的数据线引脚;
参数sclPin为IIC总线的时钟线引脚;
参数frequencyIIC总线通信频率;
返回值:布尔类型。初始化成功返回true,否则返回false
第二个函数:beginTransmission函数,该函数功能是将要进行数据通信的从设备地址,并将地址加入到发送数据队列。注意:数据队列的长度默认为128字节。
void TwoWire::beginTransmission(uint16_t address);
参数address为要发送的从设备的地址;
无返回值。
第三个函数:write函数,该函数功能是将向从机发送的数据加入发送数据队列。
size_t TwoWire::write(uint8_t data);
参数data为要发送的一个字节数据;
返回值:size_t类型。加入成功返回1,否则返回0
第四个函数:endTransmission函数,该函数功能是写入数据,主设备将发送数据队列中的数据发送给从设备。
uin8_t TwoWire::endTransmission(bool sendStop);
参数sendStop为0时,将在通讯结束后,不产生STOP信号;为1时,在通讯结束后,生成STOP信号,释放总线。
其实该函数也可不传参数,当无输入参数时,在通讯结束后,产生STOP信号,释放总线。
返回值:表示本次传输的状态,写入数据成功返回0,数据太长无法加入到发送数据缓冲区返回1,发送地址时收到NACK返回2,数据发送时收到NACK返回3,其他错误返回4,超时返回5
第五个函数:requestFrom函数,该函数功能是读取数据,主设备向从设备发送读取数据请求,并将读取的数据保存到缓冲区。注意:缓冲区的默认长度为128字节。
uint8_t TwoWire::requestFrom(uint8_t address, uint8_t len);
参数address为从设备的地址;
参数len为读取的字节数;
返回值:读取数据成功返回0
第六个函数:available函数,该函数功能是返回缓冲区中数据的字节数。
int TwoWire::available(void);
无参数;
返回值:字节数。
第七个函数:read函数,该函数功能是从缓冲区读取一个字节的数据。主设备中使用requestFrom函数发送数据读取请求信号后,需要使用read函数来获取数据。
int TwoWire::read(void);
无参数;
返回值:读到的字节数据。
14.2 硬件设计
1. 例程功能
每按下KEY0MCU通过IIC总线向24C02写入数据,在while循环中对24C02读取数据。通过串口输出写入的数据和读取的数据。
2. 硬件资源
1)独立按键
BOOT-IO0
2USART0
U0TXD-IO43
U0RXD-IO44
3)AT24C02
IICSDA-IO41
IICSCL-IO42
3. 原理图
24C02原理图,如下图所示。
图14.2.1 24C02原理图
14.3 软件设计
14.3.1 程序流程图
下面看看本实验的程序流程图:
图14.3.1.1 程序流程图
14.3.2 程序解析
1. 24c02驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。24C02驱动源码包括两个文件:24c02.cpp24c02.h
下面我们先解析24c02.h的程序。对IIC引脚和24C02器件的地址做了相关定义。
#define IIC_SCL       42
#define IIC_SDA       41
#define EEPROM_ADDR  0x50    /* 7位器件地址 */
我们选择使用IO42作为IIC的时钟线,IO41作为IIC的数据线,24C02的器件地址为0x50。
下面我们再解析24c02.cpp的程序,首先先来看一下初始化函数at24c02_init,代码如下:
/**
* @brief   初始化EEPROM器件
* @param
* @retval
*/
void at24c02_init(void)
{
    Wire.begin(IIC_SDA, IIC_SCL, 400000);    /* 初始化IIC连接 */
}
在24C02初始化函数中,直接调用Wire.begin函数接口就决定使用了IIC0,然后把IIC_SDA引脚和IIC_SCL引脚作为IIC0的数据线和时钟线使用。由于24C02手册中描述到400kHz通信频率,所以这里直接把IIC0的通信速率设为400kHz。
接下来,就来看一下如何向at24c02写入一个字节数据的函数at24c02_write_one_byte,代码如下。
/**
* @brief   AT24C02指定地址写入一个数据
* @param    addr: 写入数据的目的地址
* @param   data: 要写入的数据
* @retval  
*/
void at24c02_write_one_byte(uint8_t addr, uint8_t data)
{
    Wire.beginTransmission(EEPROM_ADDR); /* 发送从机的7位器件地址到发送队列 */
    Wire.write(addr);                        /* 发送要写入从机数据的地址到发送队列 */
    Wire.write(data);                        /* 发送要写入从机数据到发送队列 */
Wire.endTransmission(1);               
/* IIC 发送 发送队列的数据(传参为1,表示发送stop信号,结束传输) */
    delay(10);    /* 注意: EEPROM 写入比较慢,必须等到10ms后再写下一个字节 */
}
这里的写操作流程跟前面14.1.3小节中24C02写时序图描述的过程是一致的。首先调用Wire.beginTransmission函数将从机地址0x50加入到发送数据队列,然后调用Wire.write函数将要写入数据的内存地址加入到发送数据队列,继续调用Wire.write函数将要写入内存地址的数据加入到发送数据队列,最后调用Wire.endTransmission函数将数据队列的数据发送到24C02,最终实现对内存地址数据的写入。有条件的小伙伴可以用示波器或者逻辑分析仪观察一下IIC波形,跟14.1.1小节描述的是一致的。
继续看一下如何向at24c02读取一个字节数据的函数at24c02_read_one_byte,代码如下。
/**
* @brief   AT24C02指定地址读出一个数据
* @param   addr: 开始读取数据的地址
* @retval  读到的数据 / 0xFF:未接收到数据
*/
uint8_t at24c02_read_one_byte(uint8_t addr)
{
    Wire.beginTransmission(EEPROM_ADDR); /* 发送从机的7位器件地址到发送队列 */
    Wire.write(addr);                          /* 发送要读取从机数据的地址到发送队列 */
Wire.endTransmission(0);
/* IIC 发送 发送队列的数据(传参为0,表示重新发送一个start信号,保持IIC总线有效连接) */
    Wire.requestFrom(EEPROM_ADDR, 1);    /* 主机向从机发送数据请求,并获取到数据 */
    if (Wire.available() != 0)                 /* 得到已经接收到的数据字节数 */
    {
        return Wire.read();                    /* 到数据缓冲区读取数据 */
    }
    return 0xFF;
}
这里的读操作流程跟前面14.1.3小节中24C02读时序图描述的过程是一致的。首先调用Wire.beginTransmission函数将从机地址0x50加入到发送数据队列,然后调用Wire.write函数将要要读取数据的内存地址加入到发送数据队列,继续调用Wire.endTransmission函数将数据队列的数据发送到24C02。注意:Wire.endTransmission函数带参数0表明会重新发送一个起始信号,保持IIC总线的连接。后面就通过调用Wire.requestFrom函数向24C02指定内存空间读取1字节数据并保存到接收缓冲区,Wire.available函数用于查询缓冲区是否有可读数据,而Wire.read函数就是用来读取缓冲区一字节数据。
有了基本的读写函数接口,就可以写一个比较简单的检测函数at24c02_check,用来测试IIC总线上是否存在24C02或者说器件是否正常,代码如下所示。
/**
* @brief    检查AT24C02是否正常
* @note  检测原理: 在器件的末地址写如0X55, 然后再读取, 如果读取值为0X55
*          则表示检测正常. 否则,则表示检测失败.
*
* @param  
* @retval  检测结果
*           0: 检测成功
*            1: 检测失败
*/
uint8_t at24c02_check(void)
{
    uint8_t temp;
    temp = at24c02_read_one_byte(255);   /* 避免每次开机都写AT24CXX */
    if (temp == 0X55)                        /* 读取数据正常 */
    {
        return 0;
    }
    else                                      /* 排除第一次初始化的情况 */
    {
        at24c02_write_one_byte(255, 0X55);   /* 先写入数据 */
        temp = at24c02_read_one_byte(255);   /* 再读取数据 */
        
        if (temp == 0X55)
        {
            return 0;
        }
    }
    return 1;
}
在这里,就是利用EEPROM芯片掉电不丢失的特性,在第一次写入了某个值之后,再去读一下看是否写入成功,这种方式就可以去检测芯片是否可以正常工作。
有时候操作单位往往不是单个字节,所以这里我们也提供了多字节写和多字节读的函数接口,代码如下所示。
/**
* @brief   AT24C02里面的指定地址开始读出指定个数的数据
* @param   addr    : 开始读出的地址 对24c020~255
* @param  pbuf    : 数据数组首地址
* @param   datalen : 要读出数据的个数
* @retval
*/
void at24c02_read(uint8_t addr, uint8_t *pbuf, uint8_t datalen)
{
    while (datalen--)
    {
        *pbuf++ = at24c02_read_one_byte(addr++);
    }
}
/**
* @brief   AT24C02里面的指定地址开始写入指定个数的数据
* @param   addr    : 开始写入的地址 对24c020~255
* @param   pbuf    : 数据数组首地址
* @param   datalen : 要写入数据的个数
* @retval   
*/
void at24c02_write(uint8_t addr, uint8_t *pbuf, uint8_t datalen)
{
    while (datalen--)
    {
        at24c02_write_one_byte(addr, *pbuf);
        addr++;
        pbuf++;
    }
}
以上两个函数都是基于单个字节读和单个字节写函数实现的,这里就不多讲了。
2. 08_iic_eeprom.ino代码
在08_iic_eeprom.ino里面编写如下代码:
#include "24c02.h"
#include "key.h"
#include "uart.h"
const uint8_t g_text_buf[] = {"ESP32S3 IIC TEST"}; /* 要写入到24c02的字符串数组 */
#define TEXT_SIZE   sizeof(g_text_buf)               /* TEXT字符串长度 */
uint8_t datatemp[TEXT_SIZE];                         /* EEPROM读取到的数据 */
/**
* @brief  当程序开始执行时,将调用setup()函数,通常用来初始化变量、函数等
* @param  
* @retval
*/
void setup()
{
    key_init();                 /* KEY初始化 */
    uart_init(0, 115200);      /* 串口0初始化 */
    at24c02_init();            /* 初始化24CXX */
   
    while (at24c02_check())    /* 检测不到24c02 */
    {
        Serial.println("24C02 Check Failed!");
        delay(500);
    }
    Serial.println("24C02 Ready!");
}
/**
* @brief   循环函数,通常放程序的主体或者需要不断刷新的语句
* @param   
* @retval  
*/
void loop()
{
at24c02_read(0, datatemp, TEXT_SIZE);
/* 24C020地址处中读取TEXT_SIZE长度数据 */
    Serial.printf("The Data Readed Is:%s \r\n", datatemp);
    if (KEY == 0)
    {
        at24c02_write(0, (uint8_t *)g_text_buf, TEXT_SIZE);     
/* 24C020地址处写入TEXT_SIZE长度数据 */
        Serial.printf("24C02 Write %s Finished! \r\n", g_text_buf);
    }
    delay(1000);
}
在setup函数中,调用key_init函数完成按键初始化,调用uart_init函数完成串口初始化,调用at24c02_init函数完成24c02初始化,然后调用at24c02_check函数去检测器件是否正常。
loop函数中,调用at24c02_read函数去读取24c02内存地址0处开始存储的有效数据,通过串口打印出来。当按下按键时,调用at24c02_write函数向24c02内存地址0处开始写入ESP32S3 IIC TEST”信息,同样的,串口也会显示数据写入成功。
14.4 下载验证
将程序下载到开发板后,打开串口助手,会显示从24c02内存空间读取到的内容,当按下KEY按键时,会向24c02写入数据ESP32S3 IIC TEST”,然后串口显示从24c02内存空间读取到的内容就为“ESP32S3 IIC TEST”,串口助手打印信息如下。
14.4.1 串口助手打印信息

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册 微信登录

x
您需要登录后才可以回帖 登录 | 立即注册 微信登录

本版积分规则

APP|手机版|小黑屋|关于我们|联系我们|法律条款|技术知识分享平台

闽公网安备35020502000485号

闽ICP备2021002735号-2

GMT+8, 2025-10-25 06:02 , Processed in 0.202800 second(s), 12 queries , Gzip On, Redis On.

Powered by Discuz!

© 2006-2025 MyDigit.Net

快速回复 返回顶部 返回列表