本帖最后由 正点原子 于 2024-7-19 15:30 编辑
1)实验平台:正点原子ESP32S3开发板
2)购买链接:https://detail.tmall.com/item.htm?id=768499342659
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-347618-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子手把手教你学ESP32S3快速入门视频教程:https://www.bilibili.com/video/BV1sH4y1W7Tc
6)正点原子FPGA交流群:132780729
第十九章 IIC_EXIO实验
本章,我们将继续使用ESP32-S3的硬件IIC接口去驱动IO扩展芯片XL9555。在本章中,实现和XL9555之间的双向通信,将使用其IO的输入输出功能。
本章分为如下几个小节:
19.1 IIC简介
19.2 硬件设计
19.3 程序设计
19.4 下载验证
19.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总线挂载多个器件的示意图,如下图所示。
图19.1.1 IIC总线挂载多个器件
下面来学习IIC总线协议,IIC总线时序图如下所示:
图19.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的基本的读写通讯过程,包括主机写数据到从机即写操作,主机到从机读取数据即读操作。下面先看一下写操作通讯过程图,如下图所示。
图19.1.3 写操作通讯过程图
主机首先在IIC总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着发送从机地址+0(写操作)组成的8bit数据,所有从机接收到该8bit数据后,自行检验是否是自己的设备的地址,假如是自己的设备地址,那么从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据。注意:IIC总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
接着讲解一下IIC总线的读操作过程,先看一下读操作通讯过程图,如下图所示。
图19.1.4 读操作通讯过程图
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现,都是由主机发出起始信号,接着发送从机地址+1(读操作)组成的8bit数据,从机接收到数据验证是否是自身的地址。 那么在验证是自己的设备地址后,从机就会发出应答信号,并向主机返回8bit数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据。
19.1.1 IIC基本参数
(1)速率:I2C总线有标准模式(100kbit/s)和快速模式(400kbit/s)两种传输模式,还有更快的扩展模式和高速模式可供选择。
(2)器件地址:每个设备都有唯一的7位或10位地址,可以通过地址选择来确定与谁进行通信。
(3)总线状态:I2C总线有五种状态,分别是空闲状态、起始信号、结束信号、响应信号、数据传输。
(4)数据格式:I2C总线有两种数据格式,标准格式和快速格式。标准格式是8位数据字节加上1位ack/nack(应答/非应答)位,快速格式允许两个字节同时传输。
由于SCL和SDA线是双向的,它们也可能会由于外部原因(比如线路中的电容等)出现电平误差,而从而导致通信出错。因此,在IIC总线中,通常使用上拉电阻来保证信号线在空闲状态下的电平为高电平。
19.1.2 软件IIC与硬件IIC
IIC协议可以通过软件实现或者硬件实现。这两种方式的区别在于实现的方法和所需的硬件资源。
1,软件IIC
软件IIC是一种通过编程模拟IIC通信协议的技术。它巧妙利用微控制器的GPIO引脚来模拟IIC的数据线(SDA)和时钟线(SCL),通过软件精确控制引脚的电平变化,从而完成数据的传输和时序信号的生成。与硬件IIC相比,软件IIC的最大优势在于其普遍适用性,几乎任何支持GPIO功能的微控制器都可以实现。
在软件IIC的实现中,我们能够通过编程方式模拟IIC的主机和从机设备。通过精确控制GPIO引脚的状态,并严格按照IIC协议的时序要求进行操作,从而能够实现高效的数据传输和通信。软件IIC不仅灵活性极高,可以根据应用需求进行定制和扩展,还支持多从机设备和多主机环境,极大提升了通信的多样性和便捷性。
尽管软件IIC在性能上可能稍逊于硬件IIC,但在低速通信和简单通信需求的场景中,它无疑是一种经济且实用的解决方案。因此,软件IIC在资源受限的MCU系统中应用广泛,尤其适用于需要与多个外部设备进行通信的应用场景。
2,硬件IIC
硬件IIC指的是利用专门的硬件模块来执行IIC通信协议。现代微控制器及许多外部设备均集成了硬件IIC模块,这些模块负责处理IIC通信的所有复杂细节,如生成精确的时序信号、自动处理信号冲突、数据传输及错误检测等。采用硬件IIC,开发者可以直接通过硬件引脚与外部设备相连,无需编写繁琐的时序控制代码,从而简化了开发流程。
硬件IIC的使用通常更加便捷,开发者无需深入处理通信协议的内部机制,只需关注数据的传输与接收。硬件模块能够直接与外部设备通信,通过专用引脚进行数据和时钟信号的传输,确保通信的高效性与可靠性。
在决定使用软件IIC还是硬件IIC时,应综合考虑应用需求和硬件资源。软件IIC适用于资源有限的系统,其灵活性使其能在任何支持GPIO的微控制器上运行,但性能可能相对较低。而硬件IIC则以其出色的性能为特点,但通常需要特定的硬件支持,并可能占用部分引脚资源。因此,在选择时需要根据实际应用场景进行权衡。
19.1.3 IIC控制器介绍
ESP32-S3有两个IIC总线接口,根据用户的配置,总线接口可以用作IIC主机或从机模式。 IIC接口特点:
可支持标准模式(100Kbit/s)、快速模式(400Kbit/s),速度最高可达800Kbit/s,但受限于SCL和SDA上拉强度。
可支持7位寻址模式和10位寻址模式
可支持双地址(从机地址和从机寄存器地址)寻址模式
下面介绍一下ESP32S3的IIC主机写入从机,7位寻址,单次命令序列的场景,如下图所示。
图19.1.3.1 IIC主机写7位寻址的从机
在ESP32-S3硬件IIC控制器中,都有相对应的空间存放相对应的内容。比如上图中,在cmd内存区中存放的是就是命令序列,就比如前面提及到的起始信号、写过程、读过程、停止信号;在RAM内存区中存放的就是某些命令序列携带的内容。
当主机在软件配置好命令序列和RAM数据后,操作寄存器启动数据传输时。控制器的行为可分为以下四步:
(1)等待SCL线位高电平,以避免SCL线被其他主机或者从机占用。
(2)执行RSTART命令发送START位。即发送起始信号。
(3)执行WRITE命令从RAM的首地址开始取出N+1个字节并一次发送给从机,其中第一个字节为地址。这个过程中会产生对应的时序,携带数据进行发送。
(4)发送STOP命令,即发送停止信号。
19.1.4 XL9555介绍
XL9555是一款24引脚的CMOS器件,支持IIC总线或SMBus接口进行驱动。XL9555器件是一个16位通用并行输入/输出(GPIO)扩展器,可用其GPIO连接按键、LED、传感器等,解决需要额外的I/O的需求。
XL9555有如下特性:
IIC总线至16位GPIO扩展器
工作电源电压范围为2.3 V至5.5 V
低待机电流消耗
5 V容错I/O端口
400 kHz快速模式IIC总线时钟频率
SCL/SDA输入上的噪声滤波器
内部通电复位
器件地址由3个硬件地址引脚决定,最多可在总线上挂载8个器件
中断脚为开漏输出模式(低电平有效)
16个I/O引脚,默认为16个输入
简单概括一下,XL9555可使用400kHz速率的IIC通信接口与微控制器进行连接,也就是用2根通信线可扩展使用16个IO。XL9555器件地址会由三个硬件地址引脚决定,理论上在这个IIC总线上可挂载8个XL9555器件,足以满足IO引脚需求。XL9555上电进行复位,16个I/O口默认为输入模式,当输入模式的IO口状态发生变化时,即发生从高电平变低电平或者从低电平变高电平,中断脚会拉低。当中断有效后,必须对XL9555进行一次读取/写入操作,复位中断,才可以输出下一次中断,否则中断将一直保持。
XL9555引脚图如下图所示:
图19.1.4.1 XL9555器件引脚图
XL9555器件总共有24个管脚,分别为电源线VCC、地线GND、GPIO口、通信线、地址线,上图用不同底色标注出来了。16个IO分为了2组,一组是8个,分为是P0x和P1x,这些IO都可通过器件寄存器进行配置作为输出或者输出使用。通信线就是SDA和SCL,中断线INT也划分过来通信线。而地址线就是A0、A1和A2,用来决定器件地址。
1,XL9555寻址
要进行IIC通信,首先得知道器件地址,XL9555器件地址是7位的,具体格式如下图。
图19.1.4.2 XL9555器件地址
从上图可以知道,XL9555器件地址由两部分组成,一部分就是“Fixed bits”即固定的4位“0100”;另一部分就是“Programmable bits”即可编程的3位“A2 A1 A0”,在硬件上,都把这三个引脚接地处理,所以这三位为“000”。最终可得到,XL9555器件地址为“0100000”即0x20。读操作地址就为0x41,即0100 0001;写操作地址就为0x40,即0100 0000。
2, XL9555寄存器介绍
接下来,介绍一下XL9555器件的八个寄存器,如下图所示。
图19.1.4.3 XL9555寄存器
由于在IIC通信中,数据都是以字节作为单位,表示寄存器地址的数据也是1个字节。由于XL9555器件只有八个寄存器,所以这里1个字节用3个位表示,即Table 5中的B2、B1和B0。这8个寄存器都是XL9555器件的16个GPIO进行配置,其实分为4种:输入查询、输出设置、极性翻转和端口配置,每种都有两个寄存器对应的就是P0端口和P1端口。
地址0x00和0x01的寄存器是“Input Port0”和“Input Port1”寄存器,主要用于获取P0和P1的IO输入状态。寄存器如下图所示。
图19.1.4.4 XL9555的Input Port Register详情
寄存器只反应引脚输入逻辑电平情况,不管IO是设置成输入还是输出模式。打个比方,从0x00地址处(Input Port 0 Register)读出的数据是0x55,以二进制展开为01010101,从高位到低位对应的就是P07~P00的IO状态,P00的输入电平状态就为高电平。
地址0x02和0x03的寄存器是“Output Port0”和“Output Port1”寄存器,主要用于设置P0和P1的IO输出电平。寄存器如下图所示。
图19.1.4.5 XL9555的Output Port Register详情
该寄存器设置的是已经配置成输出模式的IO口的IO输出状态,1代表的是高电平,0代表的都是低电平,配置IO为输出模式的寄存器为Configuration Port寄存器。寄存器的一些位值对已经设置成输入模式的IO口是没有影响的。该寄存器还支持读取,读取到的值只是设置值,并不是实际引脚电平值,实际电平值通过Input Port寄存器查询即可。
地址0x04和0x05的寄存器是“Polarity Inversion Port0”和“Polarity Inversion Port1”寄存器,用于对端口0和端口1进行极性翻转。该寄存器值默认为0,所以对IO电平翻转功能并没有启用,且在本实验也没有用到,所以不做讲解,详细说明可看《XL9555数据手册》P13。
地址0x06和0x07的寄存器是“Configuration Port1”和“Configuration Port0”寄存器,用于配置P0和P1的IO输入/输出模式。寄存器如下图所示。
图19.1.4.6 XL9555的Configuration Port Register详情
该寄存器某一个位设置成1即作为输入模式,设置成0即作为输入模式。打个比方,要向0x06地址处(Configuration Port 0 Register)写入的数据是0x55,以二进制展开为01010101,从高位到低位对应的就是P07~P00的IO配置模式,P00、P02、P04、P06这四个IO口即配置为输入模式,而P01、P03、P05、P07这四个IO口配置为输出模式。XL9555上电复位后,所有IO口默认都是输入状态,即上图这两个寄存器读出来的值都是0xFF。
3, XL9555时序介绍
ESP32-S3是通过IIC总线跟XL9555进行通信的,对XL9555相关寄存器进行写入配置,对其16个IO进行使用。这里的时序主要就是写寄存器时序和读寄存器时序,我们一一介绍。
写寄存器时序
图19.1.4.7 单字节写入到寄存器时序图
上图中展示的是主机将单字节写入到寄存器的时序,主机在IIC总线发送第1个字节的数据为XL9555的写操作地址0x40(设备地址0x20 << 1 | 0),用于寻找总线上找到XL9555,在获得XL9555的应答信号之后,继续发送第2个字节数据,该字节数据是XL9555的寄存器地址,再等到XL9555的应答信号,主机继续发送第3字节数据,这里的数据即是写入在第2字节寄存器地址的数据。主机完成写操作后,可以发出停止信号,终止数据传输。
在《XL9555数据手册》P16中,还提供有对Output Port寄存器组(0x02和0x03)的写时序图,简单来说,就是在一个时序中,把两个寄存器都进行设置,大家自行去查看。当然用单字节写入寄存器时序也可完成配置,只不过是需要一个一个寄存器进行配置,这样子的配置过程更为清晰明了。
读寄存器时序
图19.1.4.8 单字节读取寄存器时序图
上图中展示的是主机从寄存器中读取一个字节数据的时序图。XL9555读取数据的过程是一个复合的时序,其中包含写时序和读时序。先看第一个通信过程,这里是写时序,起始信号产生后,主机发送XL9555的写操作地址0x40(设备地址0x20<< 1 | 0),获取从机应答信号后,接着发送需要读取的寄存器地址;在读时序中,起始信号产生后,主机发送XL9555的读操作地址0x41(设备地址0x20 << 1 | 1),获取从机应答信号后,接着从机返回刚刚在写时序中寄存器地址的数据,以字节为单位传输在总线上,主机接收到寄存器的数据后,发出非应答信号并以停止信号结束通信过程。
假如主机获取数据后返回的是应答信号,那么从机会一直传输数据,即把往后寄存器的数据也发送到总线上,这就是《XL9555数据手册》P17中从寄存器中读取多个字节的时序,大家可自行去查看。
19.2 硬件设计
19.2.1 例程功能
通过按下KEY0~4按键来控制蜂鸣器和LED灯开关状态,KEY0和KEY1控制蜂鸣器开与关;KEY2和KEY3控制LED灯开与关。
19.2.2 硬件资源
1.LED灯
LED-IO1
2.USART0
U0TXD-IO43
U0RXD-IO44
3.XL9555
IIC_SDA-IO41
IIC_SCL-IO42
IO_03-BEEP
IO_17-KEY0
IO_16-KEY1
IO_15-KEY2
IO_14-KEY3
19.2.3 原理图
XL9555器件相关原理图,如下图所示。
图19.3.3.1 XL9555硬件原理图
从上图可以看到,ESP32-S3开发板对XL9555器件16个IO口的设计情况。IO0_0、IO0_1和IO1_4~IO1_7被用作输入IO,IO0_2~IO0_7和IO1_0~IO1_3被用作输出IO。
本实验主要就是用到XL9555器件的5个IO,分为IO1_7(KEY0)、IO1_6(KEY1)、IO1_5(KEY2)、IO1_4(KEY3)和IO0_3(BEEP)。
这里设计有4个按键,为了提高对按键的实时检测,使用到了中断引脚。但是这里的中断引脚并没有直接与ESP32-S3引脚相连,而是需要通过跳线帽对IIC_INT和BOOT进行连接,这时候IIC_INT就连接到IO0。
特别注意:24C02、XL9555共用IIC0,所以这两个器件在使用时,必须分时复用才可以。
19.3 程序设计
19.3.1 程序流程图
程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图:
图19.3.1.1 IIC_EXIO实验程序流程图
19.3.2 IIC_EXIO函数解析
ESP-IDF提供了一套API来配置IIC。要使用此功能,需要导入必要的头文件:
接下来,作者将介绍一些常用的ESP32-S3中的IIC函数,以及IO扩展芯片中用到的函数,这些函数的描述及其作用如下:
1,设置IIC初始化参数
该函数用给定的配置,来配置IIC总线,该函数原型如下所示:
- esp_err_t i2c_param_config(i2c_port_t i2c_num, const i2c_config_t *i2c_conf);
复制代码 该函数的形参描述如下表所示:
表19.3.2.1 i2c_param_config函数形参描述
返回值:ESP_OK表示配置成功。其他值表示配置失败。
该函数使用i2c_config_t类型的结构体变量传入,该结构体的定义如下所示:
表19.3.2.2 i2c_config_t结构体参数值描述
完成上述结构体参数配置之后,可以将结构传递给 i2c_param_config () 函数,用以实例化IIC并返回IIC句柄。
2,安装IIC驱动
该函数设置某个管脚的中断服务函数,该函数原型如下所示:
- esp_err_t i2c_driver_install(i2c_port_t i2c_num,
- i2c_mode_t mode,
- size_t slv_rx_buf_len,
- size_t slv_tx_buf_len,
- int intr_alloc_flags);
复制代码 该函数的形参描述如下表所示:
表19.3.2.3 i2c_driver_install()函数形参描述
返回值:ESP_OK表示配置成功。其他值表示配置失败。
3,IIC读写操作
根据函数功能,以下函数可以归为一类进行讲解,下面将以表格的形式逐个介绍这些函数的作用与参数。
表19.3.2.4 IIC读写操作函数描述
4,初始化IO扩展芯片的IIC引脚
在配置XL9555芯片引脚之前,需要对IIC初始化进行一个判断。因为ESP32系统不支持同一个外设进行两次初始化,否则会出现系统不断复位的现象。因此,我们需要在IO扩展芯片的初始化前面添加IIC端口判断。对于,XL_INT引脚的初始化过程,笔者在这便不再赘述了,请读者回顾10.3.2 GPIO函数解析的内容。
5,配置XL9555相应IO的输入输出状态
上文提及了XL9555将16个IO分为了2组,一组是8个,分为是P0x和P1x,这些IO都可通过器件寄存器进行配置作为输出或者输出,不同于ESP32-S3芯片上的普通IO,XL9555芯片的IO输入输出模式需要特定函数进行配置,其函数原型如下所示:
- uint16_t xl9555_ioconfig(uint16_t config_value);
复制代码 该函数的形参描述,如下表所示:
表19.3.2.5 函数xl9555_ioconfig()形参描述
返回值:设置的数值
6,获取XL9555的IO状态
该函数用于获取XL9555某个IO的指定电平(高电平或低电平),其函数原型如下所示:
- int xl9555_pin_read(uint16_t pin);
复制代码 该函数的形参描述,如下表所示:
表19.3.2.6 函数xl9555_pin_read()形参描述
返回值:返回获取IO的电平数值。
7,控制XL9555的IO状态
该函数用于控制XL9555某个IO的指定电平(高电平或低电平),其函数原型如下所示:
- uint16_t xl9555_pin_write(uint16_t pin, int val);
复制代码 该函数的形参描述,如下表所示:
表19.3.2.7 函数xl9555_pin_write()形参描述
返回值:16位IO状态。
19.3.3 IIC_EXIO驱动解析
在IDF版的09_iic_exio例程中,作者在09_iic_exio \components\BSP路径下新增了一个IIC文件夹和一个XL9555文件夹,分别用于存放iic.c、iic.h和xl9555.c以及xl9555.h这四个文件。其中,iic.h和xl9555.h文件负责声明IIC以及XL9555相关的函数和变量,而iic.c和xl9555.c文件则实现了IIC以及XL9555的驱动代码。下面,我们将详细解析这四个文件的实现内容。
1,iic.h文件
- /* IIC控制块 */
- typedef struct _i2c_obj_t {
- i2c_port_t port;
- gpio_num_t scl;
- gpio_num_t sda;
- esp_err_t init_flag;
- } i2c_obj_t;
- /* 读写数据结构体 */
- typedef struct _i2c_buf_t {
- size_t len;
- uint8_t *buf;
- } i2c_buf_t;
- extern i2c_obj_t iic_master[I2C_NUM_MAX];
- /* 读写标志位 */
- #define I2C_FLAG_READ (0x01) /* 读标志 */
- #define I2C_FLAG_STOP (0x02) /* 停止标志 */
- #define I2C_FLAG_WRITE (0x04) /* 写标志 */
- /* 引脚与相关参数定义 */
- #define IIC0_SDA_GPIO_PIN GPIO_NUM_41 /* IIC0_SDA引脚 */
- #define IIC0_SCL_GPIO_PIN GPIO_NUM_42 /* IIC0_SCL引脚 */
- #define IIC1_SDA_GPIO_PIN GPIO_NUM_5 /* IIC1_SDA引脚 */
- #define IIC1_SCL_GPIO_PIN GPIO_NUM_4 /* IIC1_SCL引脚 */
- #define IIC_FREQ 400000 /* IIC通信频率 */
- #define I2C_MASTER_TX_BUF_DISABLE 0 /* I2C主机不需要缓冲区 */
- #define I2C_MASTER_RX_BUF_DISABLE 0 /* I2C主机不需要缓冲区 */
- #define ACK_CHECK_EN 0x1 /* I2C master将从slave检查ACK */
复制代码
2,iic.c文件
- i2c_obj_t iic_master[I2C_NUM_MAX]; /* 为IIC0和IIC1分别定义IIC控制块结构体 */
- /**
- * @brief 初始化IIC
- * @param iic_port:I2C编号(I2C_NUM_0 / I2C_NUM_1)
- * @retval IIC控制块0 / IIC控制块1
- */
- i2c_obj_t iic_init(uint8_t iic_port)
- {
- uint8_t i;
- i2c_config_t iic_config_struct = {0};
- if (iic_port == I2C_NUM_0)
- {
- i = 0;
- }
- else
- {
- i = 1;
- }
-
- iic_master<i>.port = iic_port;</i>
- iic_master.init_flag = ESP_FAIL;
- if (iic_master.port == I2C_NUM_0)
- {
- iic_master.scl = IIC0_SCL_GPIO_PIN;
- iic_master.sda = IIC0_SDA_GPIO_PIN;
- }
- else
- {
- iic_master.scl = IIC1_SCL_GPIO_PIN;
- iic_master.sda = IIC1_SDA_GPIO_PIN;
- }
- iic_config_struct.mode = I2C_MODE_MASTER; /* 设置IIC模式-主机模式 */
- iic_config_struct.sda_io_num = iic_master.sda; /* 设置IIC_SDA引脚 */
- iic_config_struct.scl_io_num = iic_master.scl; /* 设置IIC_SCL引脚 */
- /* 配置IIC_SDA引脚上拉使能 */
- iic_config_struct.sda_pullup_en = GPIO_PULLUP_ENABLE;
- /* 配置IIC_SCL引脚上拉使能 */
- iic_config_struct.scl_pullup_en = GPIO_PULLUP_ENABLE;
- iic_config_struct.master.clk_speed = IIC_FREQ; /* 设置IIC通信速率 */
- /* 设置IIC初始化参数 */
- i2c_param_config(iic_master.port, &iic_config_struct);
- /* 激活I2C控制器的驱动 */
- iic_master.init_flag = i2c_driver_install(iic_master.port,/* 端口号 */
- /* 主机模式 */
- iic_config_struct.mode,
- /* 从机模式下接收缓存大小(主机模式不使用) */
- I2C_MASTER_RX_BUF_DISABLE,
- /* 从机模式下发送缓存大小(主机模式不使用) */
- I2C_MASTER_TX_BUF_DISABLE,
- 0);/* 用于分配中断的标志(通常从机模式使用)*/
- if (iic_master.init_flag != ESP_OK)
- {
- while(1)
- {
- printf("%s , ret: %d", __func__, iic_master.init_flag);
- vTaskDelay(1000);
- }
- }
- return iic_master;
- }
- /**
- * @brief IIC读写数据
- * @param self:设备控制块
- * @param addr:设备地址
- * @param n :数据大小
- * @param bufs:要发送的数据或者是读取的存储区
- * @param flags:读写标志位
- * @retval 无
- */
- esp_err_t i2c_transfer(i2c_obj_t *self, uint16_t addr, size_t n,
- i2c_buf_t *bufs, unsigned int flags)
- {
- int data_len = 0;
- esp_err_t ret = ESP_FAIL;
- i2c_cmd_handle_t cmd = i2c_cmd_link_create();
- if (flags & I2C_FLAG_WRITE)
- {
- i2c_master_start(cmd); /* 启动位 */
- i2c_master_write_byte(cmd, addr<<1, ACK_CHECK_EN);/* 从机地址 + 写操作位 */
- i2c_master_write(cmd, bufs->buf, bufs->len, ACK_CHECK_EN);/* len个数据 */
- data_len += bufs->len;
- --n;
- ++bufs;
- }
- i2c_master_start(cmd); /* 启动位 */
- i2c_master_write_byte(cmd, addr << 1 |
- (flags & I2C_FLAG_READ),
- ACK_CHECK_EN); /* 从机地址 + 读/写操作位 */
- for (; n--; ++bufs)
- {
- if (flags & I2C_FLAG_READ)
- {
- i2c_master_read(cmd, /* 读取数据 */
- bufs->buf,
- bufs->len,
- n == 0 ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK);
- }
- else
- {
- if (bufs->len != 0)
- {
- i2c_master_write(cmd,
- bufs->buf,
- bufs->len,
- ACK_CHECK_EN); /* len个数据 */
- }
- }
- data_len += bufs->len;
- }
- if (flags & I2C_FLAG_STOP)
- {
- i2c_master_stop(cmd); /* 停止位 */
- }
- ret = i2c_master_cmd_begin(self->port,
- cmd,
- 100 * (1 + data_len) / portTICK_PERIOD_MS);
- i2c_cmd_link_delete(cmd); /* 释放命令链接使用的资源 */
- return ret;
- }
复制代码 与STM32不同,ESP32在IIC的配置上提供了两个IIC端口,但是在开发过程中会用到使用不同IIC端口的外设,为了保持代码的最大兼容性与减少两个IIC端口在使用过程中的冲突,我们在iic.h文件里定义了包含IIC端口的结构体参数,同时在IIC初始化函数中实现对这两个IIC端口的判断,再激活IIC控制器驱动函数完成对IIC的初始化。
在i2c_transfer()函数中,我们创建一个命令链接,将一系列待发送给从机的数据填充命令链接,接下来器件通信时序去决定flags参数,进而选择代码不同的执行情况。在最后释放命令链接使用资源前触发I2C控制器执行命令链接,并赋值于ret用作函数返回值。
IIC驱动中对IIC的各种操作,例如产生IIC起始信号、产生IIC停止信号等,请读者结合IIC的时序规定查看本实验的配套实验源码。
3,xl9555.h文件
通过前面的介绍可知,XL9555器件有8个寄存器,而且16个IO口在寄存器的位置都是固定的,基于单个IO操作单位的考虑,故这里我们也定义了对应的宏,如下所示。
- /* 引脚与相关参数定义 */
- #define XL9555_INT_IO GPIO_NUM_40 /* XL9555_INT引脚 */
- #define XL9555_INT gpio_get_level(XL9555_INT_IO) /* 读取XL9555_INT的电平 */
- #define EXIO_ADDR 0x20 /* 7位器件地址 */
- /* 器件寄存器 */
- #define XL9555_INPUT_PORT0_REG 0 /* 输入P0寄存器用于读取P0端口的输入值 */
- #define XL9555_INPUT_PORT1_REG 1 /* 输入P1寄存器用于读取P1端口的输入值 */
- #define XL9555_OUTPUT_PORT0_REG 2 /* 输出P0寄存器用于设置P0端口的输出值 */
- #define XL9555_OUTPUT_PORT1_REG 3 /* 输出P1寄存器用于设置P1端口的输出值 */
- #define XL9555_INVERSION_PORT0_REG 4
- /* 极性反转P0寄存器用于当P0端口做为输入时,对输入的电平进行反转处理,即管脚为高电平时,设置这个寄存器中相应的位为1时,读取到的输入寄存器0,1的值就是低电平0 */
- #define XL9555_INVERSION_PORT1_REG 5
- /* 极性反转P1寄存器用于当P1端口做为输入时,对输入的电平进行反转处理,即管脚为高电平时,设置这个寄存器中相应的位为1时,读取到的输入寄存器0,1的值就是低电平0 */
- #define XL9555_CONFIG_PORT0_REG 6
- /* 配置P0寄存器用于配置P0端口的做为输入(1)或是输出(0) */
- #define XL9555_CONFIG_PORT1_REG 7
- /* 配置P1寄存器用于配置P1端口的做为输入(1)或是输出(0) */
- /* XL9555各个IO的功能 */
- #define AP_INT_IO 0x0001 /* AP3216C中断引脚 P00 */
- #define QMA_INT_IO 0x0002 /* QMA6100P中断引脚 P01 */
- #define SPK_EN_IO 0x0004 /* 功放使能引脚 P02 */
- #define BEEP_IO 0x0008 /* 蜂鸣器控制引脚 P03 */
- #define OV_PWDN_IO 0x0010 /* 摄像头待机引脚 P04 */
- #define OV_RESET_IO 0x0020 /* 摄像头复位引脚 P05 */
- #define GBC_LED_IO 0x0040 /* ATK_MODULE接口LED引脚 P06 */
- #define GBC_KEY_IO 0x0080 /* ATK_MODULE接口KEY引脚 P07 */
- #define LCD_BL_IO 0x0100 /* RGB屏背光控制引脚 P10 */
- #define CT_RST_IO 0x0200 /* 触摸屏中断引脚 P11 */
- #define SLCD_RST_IO 0x0400 /* SPI_LCD复位引脚 P12 */
- #define SLCD_PWR_IO 0x0800 /* SPI_LCD控制背光引脚 P13 */
- #define KEY3_IO 0x1000 /* 按键3引脚 P14 */
- #define KEY2_IO 0x2000 /* 按键2引脚 P15 */
- #define KEY1_IO 0x4000 /* 按键1引脚 P16 */
- #define KEY0_IO 0x8000 /* 按键0引脚 P17 */
- #define KEY0 xl9555_pin_read(KEY0_IO) /* 读取KEY0引脚 */
- #define KEY1 xl9555_pin_read(KEY1_IO) /* 读取KEY1引脚 */
- #define KEY2 xl9555_pin_read(KEY2_IO) /* 读取KEY2引脚 */
- #define KEY3 xl9555_pin_read(KEY3_IO) /* 读取KEY3引脚 */
- #define KEY0_PRES 1 /* KEY0按下 */
- #define KEY1_PRES 2 /* KEY1按下 */
- #define KEY2_PRES 3 /* KEY1按下 */
- #define KEY3_PRES 4 /* KEY1按下 */
复制代码
4,xl9555.c文件
- /**
- * @brief 初始化XL9555
- * @param 无
- * @retval 无
- */
- void xl9555_init(i2c_obj_t self)
- {
- uint8_t r_data[2];
- if (self.init_flag == ESP_FAIL)
- {
- iic_init(I2C_NUM_0); /* 初始化IIC */
- }
- xl9555_i2c_master = self;
- gpio_config_t gpio_init_struct = {0};
-
- gpio_init_struct.intr_type = GPIO_INTR_DISABLE;
- gpio_init_struct.mode = GPIO_MODE_INPUT;
- gpio_init_struct.pin_bit_mask = (1ull << XL9555_INT_IO);
- gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE;
- gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE;
- gpio_config(&gpio_init_struct); /* 配置XL_INT引脚 */
- /* 上电先读取一次清除中断标志 */
- xl9555_read_byte(r_data, 2);
-
- xl9555_ioconfig(0xF003);
- xl9555_pin_write(BEEP_IO, 1);
- xl9555_pin_write(SPK_EN_IO, 1);
- }
复制代码 我们在上文提到,在XL9555初始化函数中,首先对IIC初始化进行一个判断,这是因为ESP32-S3芯片对第二次初始化的函数会返回配置错误的警告,从而导致芯片一直在复位,所以为了避免重复初始化某个函数我们一般会在相应的驱动初始化代码前对其进行判断,若在此前的步骤中没有初始化某个函数那么在这个判断语句中也可以对某函数进行初始化。此举有效避免了重复初始化以及遗漏初始化的问题。然后就是对XL9555引脚的配置了。这里需要注意的是我们在对某个引脚采取读写操作前会先读取一次清除中断表示的操作。
接下来,看一下如何向XL9555寄存器写入一个字节数据的函数xl9555_write_byte,代码如下:
- /**
- * @brief 向XL9555写入16位IO值
- * @param reg:寄存器地址
- * @param data:要写入的数据
- * @param len:要写入数据的大小
- * @retval ESP_OK:读取成功;其他:读取失败
- */
- esp_err_t xl9555_write_byte(uint8_t reg, uint8_t *data, size_t len)
- {
- i2c_buf_t bufs[2] = {
- {.len = 1, .buf = ®},
- {.len = len, .buf = data},
- };
- return i2c_transfer(&xl9555_i2c_master,
- XL9555_ADDR,
- 2,
- bufs,
- I2C_FLAG_STOP);
- }
复制代码 这里的写操作流程跟前面19.1.4小节中XL9555写寄存器时序图描述的过程是一致的。首先将寄存器地址以及要写入的数据传入结构体数组bufs中,该函数返回i2c_transfer(),i2c_transfer()创建一个命令链接,将一系列待发送给从机的数据填充命令链接。
继续看一下如何向XL9555相关寄存器读取一字节数据的函数xl9555_read_byte(),代码如下:
- /**
- * @brief 读取XL9555的16位IO值
- * @param data:读取数据的存储区
- * @param len:读取数据的大小
- * @retval ESP_OK:读取成功;其他:读取失败
- */
- esp_err_t xl9555_read_byte(uint8_t *data, size_t len)
- {
- uint8_t memaddr_buf[1];
- memaddr_buf[0] = XL9555_INPUT_PORT0_REG;
- i2c_buf_t bufs[2] = {
- {.len = 1, .buf = memaddr_buf},
- {.len = len, .buf = data},
- };
- return i2c_transfer(&xl9555_i2c_master,
- XL9555_ADDR,
- 2,
- bufs,
- I2C_FLAG_WRITE |
- I2C_FLAG_READ |
- I2C_FLAG_STOP);
- }
复制代码 这里的读操作流程跟XL9555写数据函数的过程是一致的,这里不再赘述。
基于写数据和读数据的函数接口,我们就可以为输出功能、输入功能以及端口配置功能封装成对应函数接口,方便调用,函数代码如下。
- /**
- * @brief 获取某个IO状态
- * @param pin : 要获取状态的IO
- * @retval 此IO口的值(状态, 0/1)
- */
- int xl9555_pin_read(uint16_t pin)
- {
- uint16_t ret;
- uint8_t r_data[2];
- xl9555_read_byte(r_data, 2);
- ret = r_data[1] << 8 | r_data[0];
- return (ret & pin) ? 1 : 0;
- }
- /**
- * @brief XL9555的IO配置
- * @param config_value:IO配置输入或者输出
- * @retval 返回设置的数值
- */
- uint16_t xl9555_ioconfig(uint16_t config_value)
- {
- /* 从机地址 + CMD + data1(P0) + data2(P1) */
- /* P00、P01、P14、P15、P16、P17为输入,
- 其他引脚为输出 -->1111 0000 0000 0011
- 注意:0为输出,1为输入*/
- uint8_t data[2];
- esp_err_t err;
- int retry = 3;
- data[0] = (uint8_t)(0xFF & config_value);
- data[1] = (uint8_t)(0xFF & (config_value >> 8));
- do
- {
- err = xl9555_write_byte(XL9555_CONFIG_PORT0_REG, data, 2);
-
- if (err != ESP_OK)
- {
- retry--;
- vTaskDelay(100);
- ESP_LOGE("IIC", "%s configure %X failed, ret: %d", __func__,
- config_value, err);
- xl9555_failed = 1;
-
- if ((retry <= 0) && xl9555_failed)
- {
- vTaskDelay(5000);
- esp_restart();
- }
- }
- else
- {
- xl9555_failed = 0;
- break;
- }
-
- } while (retry);
-
- return config_value;
- }
复制代码 xl9555_ioconfig函数主要就是设置XL9555某个IO的模式,可设置成输出,也可设置为输入。内部实现逻辑比较简单,在XL9555芯片中除去IIC通信引脚以及电源引脚还有16个引脚,其中每个引脚对应不同寄存器,所以每一个引脚都有一个与之对应的16进制寄存器地址。假如我们要设置IO0_0与IO0_1以及IO1_1为输入模式,其它引脚皆为输出模式,那么只需将这三个引脚配置为“1”,其它引脚配置为“0”,对应16位的二进制数为:“0000 0010 0000 0011”,将该二进制数转换为十六进制也就是:“0x203”我们将得到的数值作为形参传进该函数中,即可完成对这三个引脚的输入模式配置。
xl9555_pin_read ()函数主要就是获取XL9555某个IO的电平状态,通过调用 xl9555_read_byte()函数去查询输入端口寄存器的值,然后与该位进行比较,最终知道该位的电平状态。
19.3.4 CMakeLists.txt文件
打开本实验BSP下的CMakeLists.txt文件,其内容如下所示:
- set(src_dirs
- LED
- IIC
- XL9555)
- set(include_dirs
- LED
- IIC
- XL9555)
- set(requires
- driver)
- idf_component_register(SRC_DIRS ${src_dirs}
- INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
- component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
复制代码 上述的红色IIC、XLP555驱动需要由开发者自行添加,以确保IIC_EXIO驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了IIC_EXIO驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
19.3.5 实验应用代码
打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
- i2c_obj_t i2c0_master;
- /**
- * @brief 显示实验信息
- * @param 无
- * @retval 无
- */
- void show_mesg(void)
- {
- /* 串口输出实验信息 */
- printf("\n");
- printf("********************************\n");
- printf("ESP32-S3\n");
- printf("EXIO TEST\n");
- printf("ATOM@ALIENTEK\n");
- printf("KEY0:Beep On, KEY1:Beep Off\n");
- printf("KEY2:LED On, KEY3:LED Off\n");
- printf("********************************\n");
- printf("\n");
- }
- /**
- * @brief 程序入口
- * @param 无
- * @retval 无
- */
- void app_main(void)
- {
- uint8_t key;
- esp_err_t ret;
-
- ret = nvs_flash_init(); /* 初始化NVS */
- if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
- ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
- {
- ESP_ERROR_CHECK(nvs_flash_erase());
- ret = nvs_flash_init();
- }
- led_init(); /* 初始化LED */
- i2c0_master = iic_init(I2C_NUM_0); /* 初始化IIC0 */
- xl9555_init(i2c0_master); /* 初始化XL9555 */
- show_mesg(); /* 显示实验信息 */
- while(1)
- {
- key = xl9555_key_scan(0);
- switch (key)
- {
- case KEY0_PRES:
- {
- printf("KEY0 has been pressed \n");
- xl9555_pin_write(BEEP_IO, 0);
- break;
- }
- case KEY1_PRES:
- {
- printf("KEY1 has been pressed \n");
- xl9555_pin_write(BEEP_IO, 1);
- break;
- }
- case KEY2_PRES:
- {
- printf("KEY2 has been pressed \n");
- LED(0);
- break;
- }
- case KEY3_PRES:
- {
- printf("KEY3 has been pressed \n");
- LED(1);
- break;
- }
- default:
- {
- break;
- }
- }
- if (XL9555_INT == 0)
- {
- printf("XL9555_INT success!!! \n");
- }
- vTaskDelay(200);
- }
- }
复制代码 本实验通过串口进行信息显示,可以看到应用代码在初始化完XL9555后,就进入了一个while循环,在循环中,每间隔200毫秒就调用xl9555_key_scan()函数扫描此按键的状态,如果扫描到KEY0按键或KEY1或者KEY2以及KEY3按键被按下,则分别对蜂鸣器以及LED灯进行操作。
19.4 下载验证
下载代码完成后,按键被按下时,程序会执行特定的操作。例如,如果按下KEY0,程序会启动蜂鸣器;如果按下KEY1,程序会关闭蜂鸣器;如果按下KEY2,程序会关闭LED灯;如果按下KEY3,程序会打开LED灯。
|