|
第十四章 IIC_EEPROM实验
本章,我们将学习ESP32-S3的硬件IIC接口,将会大家如何使用IIC接口去驱动24C02器件。在本章中,实现和24C02之间的双向通信,并把数据通过串口打印出来。 本章分为如下几个小节: 14.1 IIC及24C02介绍 14.2 硬件设计 14.3 软件设计 14.4 下载验证
14.1 IIC及24C02介绍 14.1.1 IIC介绍 IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器以及其外围设备。它是由数据线SDA和时钟线SCL构成的串行总线,可发送和接收数据,在CPU与被控IC之间、IC与IC之间进行双向传送。 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总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。 了解前面的知识后,下面介绍一下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,但受限于SCL和SDA上拉强度。 可支持7位寻址模式和10位寻址模式 可支持双地址(从机地址和从机寄存器地址)寻址模式 下面介绍一下ESP32S3的IIC主机写入从机,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,通过其SCL和SDA与其他设备通信,芯片的引脚图如下图所示。 图14.1.3.1 24C02引脚图 上图的WP引脚是写保护引脚,接高电平只读,接地允许读和写,我们的板子设计是把该引脚接地。每一个设备都有自己的设备地址,24C02也不例外。前面提及到有7位寻址、11位寻址,这里的位数就是设备地址位数,24C02的设备地址就是7位的,具体格式如下图所示。 图14.1.3.2 24C02地址格式 24C02的设备地址是包括不可编程部分和可编程部分,不可编程部分也就是“1010”,可编程部分是根据上图的硬件引脚A0、A1和A2所决定。根据我们的板子设计,A0、A1和A2均接地处理,所以24C02设备地址为“1010000”即0x50。 这里还会涉及到24C02通信地址的概念,通信地址就是写操作地址和读操作地址,简单来说,就是设备地址和一个读写位的配合。上图中的地址格式最后一位R/W用于设置数据的传输方向,即读操作/写操作,0是写操作,1是读操作,所以24C02的读操作地址为:0xA1(0x50 << 1 | 1),写操作地址为:0xA0(0x50 << 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对象Wire和Wire1,对应的就是IIC0和IIC1,直接使用它们即可。 接下来,我们介绍一下本章节所用到的IIC作为主机模式相关函数。 第一个函数:begin函数,该函数功能是初始化IIC连接,并作为主设备加入IIC。 bool TwoWire::begin(int sdaPin, int sclPin, uint32_t frequency); 参数sdaPin为IIC总线的数据线引脚; 参数sclPin为IIC总线的时钟线引脚; 参数frequency为IIC总线通信频率; 返回值:布尔类型。初始化成功返回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. 例程功能 每按下KEY0,MCU通过IIC总线向24C02写入数据,在while循环中对24C02读取数据。通过串口输出写入的数据和读取的数据。 2. 硬件资源 1)独立按键 BOOT-IO0 2)USART0 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.cpp和24c02.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,代码如下: /** * @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 : 开始读出的地址 对24c02为0~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 : 开始写入的地址 对24c02为0~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); /* 从24C02的0地址处中读取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); /* 向24C02的0地址处写入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 串口助手打印信息
|