I2C 是最常用的通信接口,众多的传感器都会提供 I2C 接口来和主控相连,比如陀螺仪、
加速度计、触摸屏等等,所以 I2C 是做嵌入式开发必须掌握的,STM32MP157 有 6个 I2C 接口,可以通过这 6 个 I2C 接口来连接一些 I2C 外设。正点原子的STM32MP157开发板使用 I2C5接口连接了一个距离传感器 AP3216C,本章我们就来学习如何使用STM32MP157的 I2C 5接口来驱动 AP3216C,并读取AP3216C 的传感器数据。
本章分为如下几个小节:
25.1、IIC简介;
25.2、STM32MP157 I2C简介;
25.3、I2C寄存器介绍;
25.4、I2C的HAL库驱动;
25.5、AP3216C简介;
25.6、硬件I2C通信实验;
25.7、软件I2C通信实验;
25.1 IIC简介
IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器以及其外围设备(也叫器件)。IIC也可以写成I2C,有两根线(不算地线),它是由数据线SDA和时钟线SCL构成的串行总线,可发送和接收数据,在CPU与被控IC之间、IC与IC之间进行双向传送。随着科技的发展,现在已经有I3C了,也许有朋友已经在Intel路线图和DDR5相关的部分内存规格书中看到了这个名字了,I3C向下兼容I2C。
25.1.1 I2C总线特点
I2C总线有如下特点:
①总线由串行数据线SDA和串行时钟线SCL构成,数据线用来传输数据,时钟线用来同步数据收发。
②I2C设备都挂接到 SDA 和 SCL 这两根线上,总线上每一个设备都有一个唯一的地址识别,即器件地址,所以I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C设备。
③数据线SDA和时钟线SCL都是双向线路,都通过一个电流源或上拉电阻连接到正的电压,所以当总线空闲的时候,这两条线路都是高电平状态。
④总线上数据的传输速率在标准模式下可达 100kbit/s ,在快速模式下可达 400kbit/s ,随着科技的发展,在高速模式下可达3.4Mbit/s,不过,对于STM32MP157其I2C高速模式下能达到的是1Mbit/s。
⑤总线支持设备连接,在使用I2C通信总线时,可以有多个具备I2C通信能力的设备挂载在上面,同时支持多个主机和多个从机。当为7位地址时,理论上I2C可以挂载2^7-1=127(地址0x00不用,所以减去1)个从设备,I2C协议里没有规定总线上可挂载的设备的最大数目,但是规定了总线电容不能超过400pF,所以连接到总线的器件接口数量由总线400pF电容来限制。I2C总线挂载多个设备的示意图,如下图所示:
图25.1.1.1 I2C总线挂载多个器件
25.1.2 I2C总线时序图
下面来学习I2C总线协议,I2C总线时序图如下所示:
图25.1.2.1 I2C总线时序图
为了便于大家更好的了解I2C协议,我们从时序图中的起始位、停止位、应答信号、数据有效性、数据传输以及空闲状态等6个方面进行讲解。
① 起始位
顾名思义,也就是I2C通信开始的标志,起始信号由主机发出,在起始信号产生后,总线就会处于被占用状态,准备开始数据传输。在 SCL 为高电平的时候,SDA 出现下降沿时就表示起始位,起始信号是一种电平跳变时序信号,而不是一个电平信号。
图25.1.2.2 起始位
② 停止位
停止信号由主机发出,在停止信号发出后,总线就会处于空闲状态,停止位就是停止I2C通信的标志位,和起始位的功能相反。当SCL为高电平期间,SDA出现上升沿就表示为停止位,停止信号也是一种电平跳变时序信号,而不是一个电平信号。
图25.1.2.3 停止位
⑤ 数据传输
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。I2C总线通过SDA数据线来传输数据,通过SCL时钟线进行数据同步,SDA数据线在 SCL的每个时钟周期传输一位数据。 在数据传输的时候,当SCL 为高电平期间,SDA 上的数据有效(稳定)。当 SCL为低电平时,SDA的数据无效(不稳定),也就是通过这个时候,SDA 进行电平切换,SDA 上的数据发生变化,为下一次数据传输做准备,数据在SCL的上升沿到来之前就需准备好。并在在下降沿到来之前必须稳定。
图25.1.2.4 I2C数据传输
③ 应答信号
当I2C主机每发送完一个字节数据后,就在时钟脉冲9期间释放数据线,此时SDA 设置为输入状态,等待I2C从机应答,也就是等到I2C从机告诉主机它接收到数据了,应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的,从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。应答信号为低电平时,规定为有效应答位(ACK简称应答位),应答信号为高电平时,规定为非应答位(NACK)。
如果接收器是主机,则在它收到最后一个字节后,若希望终止数据传输,主机发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主机接收器发送一个停止信号。
④ 数据有效性
I2C总线进行数据传送时,SCL为高电平期间,SDA上的数据有效;SCL为低电平期间,SDA上的数据无效。
⑥ 空闲状态
I2C总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
在了解了I2C以上时序的基本概念后,下面介绍一下I2C的基本的读写通讯过程。
25.1.3 I2C总线时序
图25.1.3.1 写操作通讯过程图
对于IIC来说,从机是不能主动发送数据的,开始条件都是由主机生成。主机首先在I2C总线上发送起始信号,那么这时总线上的从机都会等待接收由主机发出的数据。主机接着由 发送从机地址+0(写操作位) 组成的8bit数据,所有从机接收到该8bit数据后,自行检验是否是自己的设备地址,假如是自己的设备地址,那么对应设备地址的从机就会发出应答信号。主机在总线上接收到有应答信号后,才能继续向从机发送数据,数据包的大小为 8位。主机每发送完一个字节数据,都要等待从机的应答信号。当主机向从机发送一个停止信号时,数据传输结束。注意:I2C总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
2. 读时序
接着讲解一下IIC总线的读操作过程,先看一下读操作通讯过程图,如下图所示。
图25.1.3.2 读操作通讯过程图
主机向从机读取数据的操作,一开始的操作与写操作有点相似,观察两个图也可以发现,都是由主机发出起始信号,接着发送 从机地址+1(读操作位) 组成的8bit数据,从机接收到数据验证是否是自身的地址,在验证是自己的设备地址后,对应设备地址的从机就会发出应答信号,并向主机返回8bit数据,发送完之后从机就会等待主机的应答信号。假如主机一直返回应答信号,那么从机可以一直发送数据,也就是图中的(n byte + 应答信号)情况,直到主机发出非应答信号,从机才会停止发送数据,当主机发出非应答信号后,紧接着主机会发出停止信号,停止I2C通信。
25.2 STM32MP157 I2C简介
25.2.1 I2C特性
STM32MP157D有6个I2C接口,其中I2C4和I2C6可以在A7安全模式或者A7非安全模式下使用,M4无法使用。I2C提供多主模式功能,可以控制所有I2C总线特定的序列、协议、仲裁和时序,STM32MP157的 I2C 部分特性如下:
①、兼容 I 2 C 总线规范第 03 版;
②、支持从模式和主模式,支持多主模式功能;
③、支持标准模式 (Sm)、快速模式 (Fm) 和超快速模式 (Fm+),其中,标准模式(高达 100 kHz),快速模式(高达 400 kHz),超快速模式(高达 1 MHz);
④、7 位和 10 位寻址模式;
⑤、多个 7 位从地址,所有 7 位地址应答模式;
⑥、软件复位;
⑦、带 DMA 功能的 1 字节缓冲;
⑧、独立时钟:选择独立时钟源可使 I2C 通信速度不受 i2c_pclk 重新编程的影响;
⑨、地址匹配时从停止模式唤醒;
⑩、广播呼叫。
25.2.2 I2C框图
STM32MP157的I2C框图如下所示:
图25.2.2.1 I2C总线框图
图25.2.2.2 I2C时钟
i2c_ker_ck 周期 tI2CCLK必须遵循以下条件:
tI2CCLK < (tLOW - tfilters ) / 4 且 tI2CCLK < tHIGH
其中,tLOW:SCL 低电平时间;tHIGH:SCL 高电平时间;tfilters:滤波器使能时模拟滤波器和数字滤波器引入的延时总和。
当I2C内核由i2c_pclk提供时钟时,该时钟必须遵守tI2CCLK的条件,i2c_pclk 时钟周期 tPCLK 必须遵循以下条件:
tPCLK < 4/3 tSCL
其中,tSCL:SCL 周期
2. I2C的通讯总线
每个I2C有两根通讯总线SDA和SCL,这些总线的接口接到不同的GPIO上,通过《STM32MP157A&D数据手册》可以查询具体接到哪个GPIO上,如下:
表25.2.2.1 I2C引脚
3. 报警控制状态
在该功能模块的介绍前,我们先来了解SMBus。SMBus 是 System Management Bus 的缩写,即系统管理总线,是Intel与Duracell(金顶电池)研发的,它基于I2C操作原理进行设计,主要应用于移动PC和桌面PC系统中的低速率通讯,在低功耗方面优于I2C。SMBus和I2C在诸多方面是相似的,它也是两条总线,SMBus的时钟线称SMBCLK,数据线为SMBDAT,它工作在主/从模式:主器件提供时钟,在其发起一次传输时提供一个起始位,在其终止一次传输时提供一个停止位;从器件拥有一个唯一的7或10位从器件地址。
SMBus相较于I2C,它具有一种特用的ALERT(警讯)机制,用于Slave向Master报警,ALERT其实和中断(Interrupt)类似,ALERT 是低电平有效的,当Slave将SMBSUS线路的电位拉低时,SMBSUS系统向Master发出一个中断警讯,要求Master尽速为某一Slave提供传输服务,Master响应该服务。从框图中可以看到报警控制和状态的接口是I2C_SMBA,只具备从功能的器件可通过I2C_SMBA引脚向主机发出信号,指示它想要通信,主机会处理该中断并通过报警响应地址 (0b0001 100) 同时访问所有器件,只有那些将I2C_SMBA 拉到低电平的器件会应答报警响应地址。
STM32的I2C适配器兼容SMBus协议,可以说SMBus是I2C的一个子集,I2C和SMBus两者在一般应用下,区别不大,甚至大多数情况下可以把smbus等同于I2C来设置和使用。
图25.2.2.3 SMBus总线
SMBus与I2C进行比较,它和I2C有着相似和不同之处:
表25.2.2.2 SMBus与I2C对比
4. I2C的数据控制和时钟控制部分
数据控制部分
I2C的SDA数据传输由发送和接收数据寄存器(I2C_TXDR和I2C_RXDR)以及移位寄存器来管理。
接收情况:数据移位寄存器把SDA信号线采样到的数据一位一位地存储到I2C_RXDR中。在第 8 个 SCL 脉冲后,如果I2C_RXDR 寄存器为空 (RXNE=0),则移位寄存器的内容会复制到I2C_RXDR中;如果 RXNE=1(意味着尚未读取上一次接收到的数据字节),则将延长 SCL 线的低电平时间,直到读取数据I2C_RXDR为止。
发送情况:数据移位寄存器把I2C_TXDR的数据一位一位地通过 SDA信号线发送出去。如果 I2C_TXDR 寄存器不为空 (TXE=0),则其内容会在第 9 个 SCL 脉冲(应答脉冲)后复
制到移位寄存器中,然后移位寄存器的内容会移出到 SDA 线上;如果 TXE=1(意味I2C_TXDR 内尚未写入任何数据),则将延长 SCL 线的低电平时间,直到写入了 I2C_TXDR
为止。
可以看到框图中有SMBus,SMBus规范中引入了数据包错误校验机制来提高可靠性和通信稳定性,如果开启了数据校验(控制寄存器的PECEN位为1,PEC 使能),接收到的数据会经过 PCE进行运算,运算结果存储在PEC寄存器 (I2C_PECR)中。
时钟控制部分
时序配置需要通过时序寄存器 (I2C_TIMINGR)来控制,配置时序的目的是保证主模式和从模式下使用正确的数据保持和建立时间,配置方法是编程 I2C_TIMINGR 寄存器中PRESC[3:0]、SCLDEL[3:0] 和 SDADEL[3:0] 位。必须配置好时序后才可以使用I2C,后面我们讲解改寄存器的时候会分析配置方法,因为配置的过程繁琐,之前ST官方提供的I2C Timing Configuration Tool工具对I2C时钟频率有限制,对于不大于72MHz的I2C时钟频率才可以通过该工具进行配置,这里我们可以直接使用STM32CubeMX来配置,如下图,Timing参数的值就是最终要写入TIMINGR 寄存器的值,即时序配置:
图25.2.2.4 使用STM32CubeMX配置Timing值
这里说明一下数据保持和建立时间:
系统的大部分器件的动作都是在时钟的条边沿上进行,例如I2C的数据变化是在SCL为低电平期间,在这个跳变沿期间数据会被打入触发器,如果时钟信号延时差较大,那么这些数据就不能够稳定打入触发器了,得到的数据就不准确了,所以,数据稳定传输必须满足建立和保持时间的要求。
建立时间:指在时钟沿到来之前数据从不稳定到稳定所需的时间,如果建立的时间不满足要求那么数据将不能在这个时钟上升沿被稳定的打入触发器。
保持时间:指数据稳定后保持的时间,如果保持时间不满足要求,那么数据同样也不能被稳定的打入触发器
5. 寄存器部分
这部分主要涉及I2C外设的工作状态控制,涉及几个重要的寄存器,例如控制寄存器(I2C_CR1和I2C_CR2),控制寄存器配置I2C的工作方式,例如,开启PEC、SMBus 报警使能、DMA 发送/接收请求使能、数字噪声滤波器设置、中断是能、外设使能等等,此外还有中断和状态寄存器(I2C_ISR),当外设工作时,外设的工作状态修改可以通过该寄存器的对应位来查询,关于寄存器我们后面会进行介绍。
6. 噪声滤波器
图中,模拟噪声滤波器和SDA以及SCL接口连接,默认情况下,SDA 和 SCL 输入上集成了模拟噪声滤波器,从SDA和SCL总线进入的信号先经过噪声滤波器再经过数字滤波器。该模拟滤波器符合I2C规范,此规范要求在快速模式和超快速模式下对脉宽在 50ns 以下的脉冲都要抑制。如果要使能噪声滤波器,按照要求配置I2C_CR1寄存器相应的位即可,要注意的是,使能 I2C 时(PE 位置 1),不允许更改滤波器配置。
25.2.3 主从模式和地址
I2C总线既可以作为从模式(Slave),也可以作为主模式(Master),作为主模式和从模式的配置在STM32CubeMX上是一样的,不同的是I2C设备的地址。在主模式下,有主发送器和主接收器,在从模式下,有从发送器和从接收器,I2C接口在工作时可选用这4种模式之一。
默认情况下,它以从模式工作。接口在生成起始位后会自动由从模式切换为主模式,并在出现仲裁丢失或生成停止位时从主模式切换为从模式,从而实现多主模式功能。在从模式下,该接口能够识别其自身地址(7 或 10 位)以及广播呼叫地址,广播呼叫地址检测可由软件使能或禁止。
图25.2.3.1 I2C时序图
主模式通信初始化(地址阶段)过程:
如果主机要发起通信,需要配置I2C_CR2 寄存器,为寻址的从器件配置以下参数:
①、配置ADD10位以实现寻址模式:7 位(ADD10位为0)或 10 位(ADD10位为1);
②、配置SADD[9:0]位以确定主机要发送的从地址;
③、配置RD_WRN位以确定数据传输方向:主机请求写传输(0)、主机请求读取传输(1);
④、当读取 10 位地址时,对应HEAD10R位进行相应配置;
⑤、配置NBYTES[7:0]确定待传输的字节数;
⑥、以上配置完成后,将 I2C_CR2 寄存器中的 START 位置 1生成起始位,当START 位置 1 时,以上所有位不能再更改。
⑦、之后,当主器件检测到总线空闲时,它会在经过一定时间的延时后自动发送起始位,随后发出从器件地址。当仲裁丢失时,主器件将自动切换回从模式,如果作为从器件被寻址,还可对其自身地址进行应答。只要已在总线上发送从地址,START位便会由硬件复位。如果仲裁丢失,START位也会由硬件复位。
2. 从模式
I2C从模式特性:可编程I2C地址检测、双寻址模式,可对2个从地址应答,停止位检测,7位/10位寻址以及广播呼叫的生成和检测、支持不同的通信速度等。
如果STM32MP157要在从模式下工作,即CPU作为从机,用户必须至少使能一个从地址,主机通过该地址找到从机。从机地址可使用 I2C_OAR1 和 I2C_OAR2 这两个寄存器来编程,从设备自身的地址为 OA1 和 OA2,关于该寄存器我们后面会介绍。OA1 既可配置为7位寻址模式(默认),也可通过将 I2C_OAR1 寄存器的 OA1MODE位置 1 配置为 10 位寻址模式,如果需要额外的从地址,可配置第2个从地址 OA2,注意的是,I2C_OAR2只支持7位寻址。
这里还是再提醒,当CPU作为从机时才需要配置OA1 和 OA2,如果CPU作为主机时,就不需要配置OA1 和 OA2了,但是OA1 和 OA2的值还是存在的,只是要注意OA1 和 OA2的值不能和从设备的地址一致,不然就有冲突了,毕竟从设备的地址标识是唯一的。STM32CubeMX生成的工程中,主模式和从模式的配置一样,生成的初始化代码中OA1 和 OA2默认为0,如果需要将CPU作为从机,再手动修改OA1 和 OA2的值即可。
从模式初始化过程:
①、先将I2C_OAR1 和 I2C_OAR2的OA1EN和OA2EN清零,禁止从设备自身地址 1和2;
②、配置I2C_OAR1 和 I2C_OAR2设置寻址模式并使能使能设备自身地址 1和2;
③、将 I2C_CR1 寄存器中的 GCEN 位置 1 来使能广播呼叫地址;
3. 地址左移和右移
I2C标准的文件中介绍到,在起始位后,发送的第一个数据字节包括:从机地址和数据处理方向,字节格式为:从机地址[7:1]+读/写位(0:发送数据,1:读数据),该字节格式如下:
表25.2.3.2 I2C地址
播呼叫地址用于寻址连接到I2C上的每个器件,在从模式时,I²C接口能识别它自己的地址(7位或10位)和广播呼叫地址,通过软件能够控制开启(如果ENGC=1)或禁止广播呼叫地址的识别。下面我们来看看7位地址和10位地址。
1)7位地址
I2C协议里地址为左对齐的,即从左到右高位在前低位在后,如果I2C的从机地址是7位时,I2C读写过程中总要在后面再加1个读/写位(在最低位加,0表示写,1表示读),也就构成了8位地址,所以将这个7位地址左移1位后,最后1位就是0,表示对从机进行写操作。I2C发送地址数据时,先发送高位,再发送低位,也就是说最后才发送读写位,所以很多地方也会说第8位是读/写位。
图 25.2.3.2 7位地址
2)8位地址
一些芯片手册上可能提供的是带了读写位的8位地址模式,也就是8个位,最后一个位是0或者1(读或者写位),那么,将这8位右移1位后,就会得到这个器件的7位地址(不带读写位)
图25.2.3.3 8位地址
3)10位地址
10位寻址用的不是很多,I2C的7位地址和10位地址是兼容的,即7位地址和10位地址的设备可以同时连接到相同的I2C上。我们先来看看10位地址的格式,我们先以主发送器向从机写的过程为例子进行分析:
图25.2.3.4 10位地址写过程
第一位START是起始位,起始位后面跟着的是第一个字节的8位,即由第一个7位加上一个读/写位,第一个7位的格式是:11110 XX,X表示可以是0也可以是1,XX属于10位地址的高2位,另外的低8位位于第二个字节中,该格式的7位用来指示当前传输的是10位地址。那么,10位从机地址就是前面为XX的两位和第二个字节组合成,如上示意图。
写过程
①、主机发送起始位,总线上的从机等待主机继续发送数据;
②、主机发送第一个字节,即第一个7位加一个写位(0),所有从机把自己的地址和这第一个字节(1111 0XX0)进行比较,此时可能有多个从机匹配然后产生第一个应答信号A1;
③、主机发送第二个字节,也就是10位地址的低8位,所有从机把自己地址的低8位和这第二个字节进行比较,这是只有一个从机可以匹配成功,该从机发出第二个应答信号A2;
④、从机发送一个8位数据DATA,已经匹配的从机保持被寻址的状态,接收接下来主机发送的数据,当接收到数据后,从机返回一个应答位A,该过程和7位地址的数据发送和接收类似,直到从机接收到主机发送的终止条件§,或者接收到重复开始信号(SR)跟着另一个8位字节。
读过程
如果主机要读数据,在第二个应答信号A2之前的过程和上面写数据的过程类似,只不过到了A2之后紧跟着的是一个起始信号(我们叫重复起始信号SR)和带读位(1)的第一个字节,匹配的从机会产生应答信号A3,并保持被寻址的状态和主机进行通信,直到从机接收到主机发送的终止条件§,或者接收到重复开始信号(SR)跟着另一个8位字节。该过程如下:
图25.2.3.5 10位地址读过程
4)单片机下的地址和Linux操作系统下设备树的地址
经过前面的分析,单片机裸机程序开发使用的地址都要带一个读/写位,如果后期学习Linux操作系统的驱动开发部分必定少不了修改设备树,在设备树中,I2C相关的配置节点中,从机地址是不带读写位的,一般用7位地址,这点大家稍微注意即可。关于Linux部分的资料,可参考《【正点原子】STM32MP1嵌入式Linux驱动开发指南》。
25.3 I2C寄存器介绍
下面我们来看I2C相关的寄存器:
25.3.1 控制寄存器
控制寄存器有控制寄存器 1 (I2C_CR1)和控制寄存器 2 (I2C_CR2),介绍如下:
图25.3.1.1 I2C_CR1寄存器
该寄存器的位比较多,这里我们介绍一些比较重要的位:
PE为外设使能位,0:禁止外设;1:使能外设。当PE=0时,I2C SCL 线和SDA 线被释放。
TXIE为发送中断使能位,0:禁止发送中断;1:使能发送中断。
RXIE为接收中断使能位,0:禁止接收中断;1:使能发送中断。
ADDRIE为从模式下地址匹配中断使能位,0:禁止地址匹配(ADDR)中断,1使能地址匹配(ADDR)中断。
STOPIE为停止位检测中断使能位,0:禁止停止位检测(STOPF)中断;1:使能停止位检测(STOPF)中断。
TCIE为传输完成中断使能位,0:禁止传输完成中断;1:使能传输完成中断。
DNF[3:0] 用于配置 SDA 和 SCL 输入端的数字噪声滤波器,0000:禁止数字滤波器,其它配置表示使能数字滤波器并配置不同宽度的可滤除的噪声尖峰脉宽。
ANFOFF表示模拟噪声滤波器使能位,0:使能模拟噪声滤波器;1:禁止模拟噪声滤波器。
TXDMAEN和RXDMAEN分别是DMA 发送请求使能和DMA 接收请求使能位,0表示禁止,1表示使能。
ALERTEN是SMBus 报警使能位,要使用SMBus 报警功能的话,将该位置1即可。
PECEN是PEC 使能位,只在支持 SMBus功能下使用,PEC 即数据包错误校验机制。
2. 控制寄存器 2(I2C_CR2)
图25.3.1.2 I2C_CR2寄存器
SADD0为主模式下的从设备地址位0,只在10位寻址模式下有意义,在10位寻址模式下,该位是待发送从地址的第 0 位。
SADD[7:1]为主模式下从设备地址的7~1位,不管是7位寻址还是10位寻址下,这些位为待发送从地址的第 1 到第 7 位。
SADD[9:8]为主模式下从设备地址的第 8 到第 9 位,只针对10位寻址模式有效。
RD_WRN是传输方向位,也就是读/写位,0:主器件请求写传输;1:主器件请求读传输。注意的是,当发送起始位(START=1)时不能再更改此位;
ADD10是主模式下寻址模式选择位,0:主器件工作在 7 位寻址模式下;1:主器件工作在 10 位寻址模式下。同样的,当发送起始位(START=1)时不能再更改此位;
START是主模式下起始位生成,向该位写入0无效,读取该位时,0:不生成起始位;1:生成重复起始/起始位。如果要软件清除此位,向 I2C_ICR 寄存器中的 ADDRCF位写入1即可。
STOP是主模式下停止位生成,该位由软件置 1,并可在检测到停止位时或 PE = 0 时由硬件清零,向该位写入0 不起作用。读取该位时,0:不生成停止位;1:在当前字节传输完成后生成停止位。
NACK是从模式下应答和非应答信号生成位,向该位写入“ 0 ”不起作用,此位由软件置 1。当接收到停止位或匹配地址时、在发送 NACK 时或者 PE=0 ,该位硬件清零。读取该位时,0:在当前接收的字节后发送 应答信号ACK,1:在当前接收的字节后发送 非应答信号NACK
NBYTES[7:0]用于设置待发送/接收的字节数。
25.3.2 设备自身地址寄存器
设备自身地址寄存器有设备自身地1寄存器(I2C_OAR1)和设备自身地址2寄存器 (I2C_OAR2),这两个地址是STM32在从模式下时需要设置的,在主模式下可以设置或者不设置这两个寄存器,即当CPU做主机时不需要设置,做从机时就要设置,所设置的地址就是CPU作为从机时的地址。注意的是,CPU在主模式下时,这两个地址不能与从机地址相同,否则会引起冲突,毕竟从机地址是唯一的,主机要通过从机的这个唯一的标识来找到要通信的从设备。
此外,如果使用STM32CubeMX进行配置生成初始化代码,不管STM32的CPU是做为从机还是主机时,在STM32CubeMX上的配置步骤都一样,最后生成的初始化代码也是一样的,默认I2C_OAR1和I2C_OAR2寄存器的OA1以及OA2都是0,那如果要将CPU做为从机,手动修改OA1和OA2即可,要是CPU做为从机,可以不用管,毕竟从设备地址不是0。
图25.3.2.1 I2C_OAR1寄存器
OA1[9:0]:在从模式下,即STM32的CPU作为从机时的地址1(主机是其它设备,也可以是另外一块STM32),这里分为两种情况:
当为7位寻址模式时,OA1 [7:1]就是STM32作为从机时的地址1,此时OA1 [9]、OA1 [8]和OA1 [0]位没有用到;当为10位寻址模式时,OA1[9:0]就是STM32作为从机时的10位地址1。
OA1MODE:用于配置是7位地址模式还是10位地址模式。当该位为0时,表示STM32作为从机时的地址1为7位模式,当该位为0时,表示STM32作为从机时的地址1为10位模式.
OA1EN:用于是否使能STM32作为从机时的地址1,该位为0表示禁用STM32的地址1,该位为1表示启用STM32的地址1。
以上是STM32作为从机时STM32的地址1,下面我们看看STM32作为从机时,它的地址2:
2. 设备自身地址2寄存器(I2C_OAR2)
I2C_OAR2只是在7位地址模式下使用,该寄存器各位如下:
图25.3.2.2 I2C_OAR2寄存器
OA2 [7:1]是STM32作为从机时,STM32的地址2,为7位,且仅当OA2EN = 0时才能写入这些位。改寄存器第0位属于保留位。
OA2MSK [2:0]位是设置STM32作为从机时,STM32自身的地址2的哪个位被屏蔽,被屏蔽的位就变成无关位,没有被屏蔽的位就可以用于寻址比较。这些位仅在OA2EN=0时可写入,这些位配置如下:
000:无屏蔽,7个地址都会进行比较;
001:OA2[1] 被屏蔽,为无关位,仅比较 OA2[7:2];
010:OA2[2:1] 被屏蔽,为无关位,仅比较 OA2[7:3];
011:OA2[3:1] 被屏蔽,为无关位,仅比较 OA2[7:4];
…
111:OA2[7:1] 被屏蔽,为无关位。不进行比较,对接收到的全部 7 位地址(保留位除外)
应答。
OA2EN是STM32作为从机时,STM32的地址2使能位,该位为0时,禁止STM32的地址2,为1时,使能STM32的地址2。
25.3.3 时序寄存器
时序寄存器I2C_TIMINGR如果要手动配置的话比较麻烦,这里推荐大家使用STM32CubeMX来配置,在I2C配置项处可以自动生成需要的Timing值,即I2C_TIMINGR的值。
图25.3.3.1 I2C_TIMINGR寄存器
SCLL[7:0]:是主模式下SCL 低电平周期,tSCLL = (SCLL+1) * tPRESC;
SCLH[7:0]:是主模式下SCL 高电平周期,tSCLH = (SCLH+1) * tPRESC;
最终,主时钟SCL的周期是tSCLL+ tSCLH=[ (SCLL+1)+ (SCLH+1)] * tPRESC。
SDADEL[3:0]:是数据保持时间配置位,数据保持时间tSDADEL = SDADEL * tPRESC;
SCLDEL[3:0]是数据建立时间配置位,数据建立时间tSCLDEL = (SCLDEL+1) * tPRESC;
PRESC[3:0]是时序预分频因子配置位,该字段用于对 i2c_ker_ck 进行预分频,以生成用于数据建立和保持计数器以及 SCL 高电平和低电平计数器可用的时钟周期 tPRESC。时钟周期:
tPRESC = (PRESC+1) x tI2CCLK
25.3.4 中断和状态寄存器
读取I2C_ISR寄存器的对应位可以知道此时的中断和I2C工作状态,在I2C设备工作时,外设的工作状态会被写入I2C_ISR寄存器中,我们来看看这些位:
图25.3.4.1 I2C_ISR寄存器
该寄存器对应位很多,我们这里就不一一介绍了,这里会介绍部分位:
TXE为发送数据寄存器为空时的标志位。I2C_TXDR 寄存器为空时,该位由硬件置 1。下一个待发送的数据写入 I2C_TXDR 寄存器时,该位被清零。该位可由软件写入“1”,以刷新发送数据寄存器 I2C_TXDR。
TXIS是发送中断状态标志位,当 I2C_TXDR 寄存器为空时,该位由硬件置 1,待发送的数据必须写入 I2C_TXDR 寄存器。下一个待发送的数据写入 I2C_TXDR 寄存器时,该位被清零。
RXNE是接收数据寄存器不为空标志位,当接收到的数据已复制到 I2C_RXDR 寄存器且准备就绪可供读取时,该位由硬件置 1。读取I2C_RXDR 时,将清零该位。
ADDR是地址匹配(从模式)标志位,地址匹配(从模式)。
NACKF是接收到应答标志位,传输完字节后接收到 NACK 时,该标志由硬件置 1。该标志由软件清零,方法是将 NACKCF位置 1。
STOPF是停止位检测标志位,当在总线上检测到停止位,且外设也参与本次传输时,该标志由硬件置 1。
TC是传输完成(主模式)标志位,当 RELOAD=0、AUTOEND=0 且 NBYTES 数据传输完成时,该标志由硬件置 1。当 START位或 STOP 位置 1 时,该标志由软件清零 。
ALERT是SMBus 报警标志位,仅在支持 SMBus 功能时有效。当 SMBHEN=1(SMBus 主机配置)、ALERTEN=1 且在 SMBA 引脚上检测到 SMBALERT 事件(下降沿)时,该标志由硬件置 1,该位为1时则报警,为0时不报警。
BUSY是总线繁忙标志,忙时该位为1,不忙时该位为0。该标志用于指示总线上正在进行通信。当检测到起始位时,该位由硬件置 1。当检测到停止位或 PE = 0 时,该位由硬件清零。
DIR是传输方向(从模式)标志位,该位为0:写传输,从器件进入接收器模式;1:读传输,从器件进入发送器模式。
ADDCODE[6:0]是地址匹配代码(从模式)时标志位,发生地址匹配事件时 (ADDR = 1),这些位更新为接收到的地址。
25.3.5 中断清零寄存器
如果要清除中断标志位,对中断清零寄存器(I2C_ICR)的对应位中写入1即可清除,该寄存器属于只写寄存器,我们来看看该寄存器:
图25.3.5.1 I2C_ICR寄存器
如下表是该寄存器的相关位描述:
表25.3.5.1 I2C_ICR寄存器的位描述
25.3.6 PEC 寄存器
PEC寄存器的低8位PEC[7:0]用于保存数据包错误校验值,改寄存器只读,读取该位即可得到数据包错误校验值。
图 25.3.6.1 I2C_PECR寄存器
25.3.7 接收数据寄存器和发送数据寄存器
图25.3.7.1 I2C_RXDR寄存器
2. 发送数据寄存器 (I2C_TXDR)
I2C_TXDR寄存器的低8位TXDATA[7:0]保存的是待发送到I2C总线的数据字节,可将要发送的数据写入TXDATA[7:0]中,注意的是,仅当TXE=1时才可以写入这些位。
图25.3.7.2 I2C_TXDR寄存器
25.4 I2C的HAL库驱动
I2C相关的API函数在stm32mp1xx_hal_i2c.c和stm32mp1xx_hal_i2c.h文件中。本小节我们介绍和本实验相关的API函数。
25.4.1 结构体和句柄
typedef struct
{
uint32_t Timing; /* 指定I2C_TIMINGR寄存器的值 */
uint32_t OwnAddress1; /* 指定从设备自己的地址1,此参数可以是7位或10位地址 */
uint32_t AddressingMode; /* 指定地址的长度模式是选择7位还是10位寻址模式 */
uint32_t DualAddressMode; /* 指定是否选择双地址模式 */
uint32_t OwnAddress2; /* 若使用双地址模式,指定从设备自己的地址2,此地址只能是7位 */
uint32_t OwnAddress2Masks; /* 如果使用双地址模式,确认I2C_OAR2的OA2MSK[2:0]位的值 */
uint32_t GeneralCallMode; /* 指定广播呼叫模式 */
uint32_t NoStretchMode; /* 指定禁止时钟延长模式 */
} I2C_InitTypeDef;
Timing的值就是时序寄存器I2C_TIMINGR的值,通过该值可以计算出数据保持时间、数据建立时间、时序预分频值、主时钟SCL的周期,关于这些参数的计算可以参考前面寄存器介绍部分。
OwnAddress1是STM32作为从设备时的地址1,实际上就是I2C_OAR1寄存器的OA1[9:0]指,可配置为7位或10位地址模式。如果使用双地址模式,那么还需要配置I2C_OAR2寄存器,OA2 [7:1]位的值就是STM32作为从机时的地址2,即以上结构体中OwnAddress2的值。如果STM32作为主机,以上OwnAddress1和OwnAddress2可以不配置,但要确定其值不能和从设备的地址相同。关于这些注意事项我们前面都有介绍。
typedef struct __I2C_HandleTypeDef
{
I2C_TypeDef *Instance; /* I2C寄存器的基地址 */
I2C_InitTypeDef Init; /* I2C通讯参数 */
uint8_t *pBuffPtr; /* 指向I2C发送缓冲区的指针 */
uint16_t XferSize; /* I2C传输数据的大小 */
__IO uint16_t XferCount; /* I2C发送数据的个数 */
__IO uint32_t XferOptions; /* I2C定量转移选项 */
__IO uint32_t PreviousState; /* I2C通讯先前状态 */
HAL_StatusTypeDef(*XferISR)(struct __I2C_HandleTypeDef *hi2c, uint32_t ITFlags,\ uint32_t ITSources); /* I2C传输IRQ处理程序函数指针 */
DMA_HandleTypeDef *hdmatx; /* I2C Tx DMA句柄参数 */
DMA_HandleTypeDef *hdmarx; /* I2C Tx DMA句柄参数 */
#ifdef HAL_MDMA_MODULE_ENABLED
MDMA_HandleTypeDef *hmdmatx; /* I2C Tx MDMA句柄参数 */
MDMA_HandleTypeDef *hmdmarx; /* I2C Rx MDMA句柄参数 */
#endif
HAL_LockTypeDef Lock; /* I2C锁定对象 */
__IO HAL_I2C_StateTypeDef State; /* I2C通讯状态 */
__IO HAL_I2C_ModeTypeDef Mode; /* I2C通讯模式 */
__IO uint32_t ErrorCode; /* I2C错误代码 */
__IO uint32_t AddrEventCount; /* I2C地址事件计数器 */
} I2C_HandleTypeDef;
此句柄内容有很多,我们先关注几个重要的:
(1)Instance
I2C_TypeDef结构体在stm32mp157dxx_cm4.h头文件中有声明,声明的Instance指针变量指向I2C寄存器基地址,通过此变量即可操作对应的结构体成员(寄存器)。
(2)Init
I2C_InitTypeDef结构体前面我们已经介绍了,此处声明I2C_InitTypeDef结构体变量Init,可以通过Init来设置I2C的Timing值以及地址等信息。
(3)pBuffPtr、XferSize、XferCount
pBuffPtr、XferSize、XferCount分别是指向I2C发送缓冲区的指针、I2C传输数据的大小、I2C发送数据的个数
(4)PreviousState
PreviousState表示I2C外围设备的状态,如主发送器忙I2C_STATE_MASTER_BUSY_TX、主接收器忙I2C_STATE_MASTER_BUSY_RX、从发送器忙I2C_STATE_SLAVE_BUSY_TX、从接收器忙I2C_STATE_SLAVE_BUSY_RX等。
(5)Lock
Lock是对资源操作增加操作锁保护功能,可选HAL_UNLOCKED或者HAL_LOCKED两个参数。如果State的值等于HAL_I2C_STATE_RESET,则可认为I2C未被初始化,此时,将Lock标记为HAL_UNLOCKED,并且调用HAL_I2C_MspInit函数来对I2C的GPIO和时钟进行初始化。
(6)State
State 是HAL_I2C_StateTypeDef结构体变量,HAL_I2C_StateTypeDef结构体定义如下:
typedef enum
{
HAL_I2C_STATE_RESET = 0x00U, /* 外围设备没有初始化 */
HAL_I2C_STATE_READY = 0x20U, /* 外围设备已经初始化且可以使用 */
HAL_I2C_STATE_BUSY = 0x24U, /* 内部流程正在进行中 */
HAL_I2C_STATE_BUSY_TX = 0x21U, /* 数据在发送 */
HAL_I2C_STATE_BUSY_RX = 0x22U, /* 数据在接收 */
HAL_I2C_STATE_LISTEN = 0x28U, /* 地址监听模式正在进行中 */
HAL_I2C_STATE_BUSY_TX_LISTEN = 0x29U, /* 地址监听模式和数据在发送 */
HAL_I2C_STATE_BUSY_RX_LISTEN = 0x2AU, /* 地址监听模式和数据在接收 */
HAL_I2C_STATE_ABORT = 0x60U, /* 正在中止用户请求 */
HAL_I2C_STATE_TIMEOUT = 0xA0U, /* 超时状态 */
HAL_I2C_STATE_ERROR = 0xE0U /* 错误 */
} HAL_I2C_StateTypeDef;
通过读取State可知道当前I2C的状态。
(7)Mode
Mode 用于配置I2C设备的工作模式,HAL_I2C_ModeTypeDef结构体如下:
typedef enum
{
HAL_I2C_MODE_NONE = 0x00U, /* 没有I2C通讯在进行 */
HAL_I2C_MODE_MASTER = 0x10U, /* I2C通讯在主模式下 */
HAL_I2C_MODE_SLAVE = 0x20U, /* I2C通讯在从模式下 */
HAL_I2C_MODE_MEM = 0x40U /* I2C通信处于内存模式 */
} HAL_I2C_ModeTypeDef;
以上模式中,我们比较常见的是从模式和主模式。
25.4.2 HAL库的API函数
HAL库的API函数在stm32mp1xx_hal_i2c.c文件中有定义,涉及的函数有很多。I2C的通讯模式可以分为阻塞模式和非阻塞模式,阻塞模式下,通信以轮询模式进行,非阻塞模式下,可以使用中断或DMA进行。当使用中断模式时,将通过专用的I2C IRQ指示数据处理的结束;当使用DMA模式时,将通过DMA IRQ指示数据处理的结束。
阻塞模式功能常用的API函数有:
HAL_I2C_Master_Transmit 在主模式下传输大量数据
HAL_I2C_Master_Receive 在主模式下接收大量数据
HAL_I2C_Slave_Transmit 在从属模式下传输大量数据
HAL_I2C_Slave_Receive 在从属模式下接收大量数据
HAL_I2C_Mem_Write 将大量数据写入特定内存地址
HAL_I2C_Mem_Read 从特定的内存地址读取大量数据
HAL_I2C_IsDeviceReady 检查目标设备是否已准备好进行通信
表25.4.2.1 I2C阻塞模式相关API函数
非塞模式(中断方式)功能常用的API函数有:
HAL_I2C_Master_Transmit_IT 在主模式下以中断方式传输大量数据
HAL_I2C_Master_Receive_IT 在主模式下通过中断接收大量数据
HAL_I2C_Slave_Transmit_IT 在从模式下以中断发送大量数据
HAL_I2C_Slave_Receive_IT 在从模式下以中断接收大量数据
HAL_I2C_Mem_Write_IT 以中断方式将大量数据写入特定的内存地址
HAL_I2C_Mem_Read_IT 以中断方式从特定的内存地址读取大量数据
HAL_I2C_EnableListen_IT 以中断方式启用地址侦听模式
HAL_I2C_DisableListen_IT 以中断方式禁用地址侦听模式
HAL_I2C_Master_Abort_IT 以中断方式终止I2C通信。
表25.4.2.2 I2C中断方式相关API函数
非塞模式(DMA方式)功能常用的API函数有:
HAL_I2C_Master_Transmit_DMA 以DMA方式以主模式发送大量数据
HAL_I2C_Master_Receive_DMA 以DMA方式以主模式接收大量数据
HAL_I2C_Slave_Transmit_DMA 以DMA方式在从模式下传输大量数据
HAL_I2C_Slave_Receive_DMA 以DMA方式在从属模式下接收大量数据
HAL_I2C_Mem_Write_DMA 以DMA方式将大量数据写入特定内存地址
HAL_I2C_Mem_Read_DMA 以DMA方式从存储器地址读取大量数据。
表25.4.2.3 I2CDMA方式相关API函数
非塞模式下的一些回调函数:
HAL_I2C_MasterTxCpltCallback 主发送传输完成回调函数
HAL_I2C_MasterRxCpltCallback 主接收完成回调函数
HAL_I2C_SlaveTxCpltCallback 从发送完成回调函数
HAL_I2C_SlaveRxCpltCallback 从接收完成回调函数
HAL_I2C_MemTxCpltCallback 内存发送传输完成回调函数
HAL_I2C_MemRxCpltCallback 内存接收传输完成回调函数
HAL_I2C_AddrCallback 从地址匹配回调函数
HAL_I2C_ListenCpltCallback 监听完成回调函数
HAL_I2C_ErrorCallback I2C通信过程错误回调函数
HAL_I2C_AbortCpltCallback I2C停止回调函数
表25.4.2.4 回调函数
上函数中,我们这里仅介绍和本实验有关的函数以及一些常用的函数,其它函数大家想了解的可以直接通过stm32mp1xx_hal_i2c.c文件查询。
图25.5.1.1 AP3216C结构
AP3216C的芯片手册位于开发板光盘A-基础资料\6、硬件资料\1、芯片资料\《AP3216C》。
25.5.2 AP3216C设备地址和寄存器
图25.5.2.1 AP3216C的设备地址
AP3216C的从设备地址为0X1E,有7位,不带读/写位,二进制为 001 1110,裸机程序中要带读写位,带读位的地址为:0x3D,写地址为0x3C。我们使用STM32相AP3216C写数据,直接使用地址0x3C。
2. AP3216C存器地址
AP3216C寄存器,通过配置这些寄存器我们可以设置AP3216C的工作模式,并读取相应的数据,下面我们来了解AP3216C的寄存器,如下表所示:
表25.5.2.1 AP3216C相关寄存器
发生ALS或PS或(ALS + PS)中断事件时,INT引脚将被设置为低电平并设置INT状态位,用户在读取寄存器0xD(ALS)时可以清除INT位和单个状态位。
1)控制寄存器
0X00是模式控制寄存器的地址,用于打开/关闭设备电源以及设置 AP3216C 的工作模式,一般设置步骤为:
①、先将其设置该寄存器为 0X04,即先软件复位一次 AP3216C;
②、接下来根据实际使用情况选择合适的工作模式,比如设置为 0X03,也就是开启 ALS+PS+IR。
此外,要注意的是,AP3216C芯片手册有提到软件复位时,设备的所有寄存器将在10毫秒后变为默认值,所以在复位的这10ms时间内,不要对AP3216C执行任何命令,所以我们在程序设计的时候,复位后尽量要延时10ms以上的时间以后再对AP3216C进行读写操作。
2)中断状态寄存器
地址为0x01和0x02的两个寄存器和中断有关,本节实验我们不涉及中断。如果地址0x01的位0为1,表示发生ALS中断,如果为0,表示ALS中断已经被清除或者未发生ALS中断;如果地址0x01的位1为1,表示发生PS中断,如果为0,表示PS中断已经被清除或者未发生PS中断;复位后,这两位默认为0。
3)中断清除寄存器
0X02这个地址对应的寄存器用于清除中断标志位的,将1写入位0则清除ALS中断,将1写入位1则清除PS中断。
4)数据寄存器
连续的地址为0X0A~0X0F 的这6个寄存器就是数据寄存器,保存着 ALS、PS 和 IR 这三个传感器获取到的数据值,读取这些寄存器就可以得到传感器采集到的数据,这里注意,AP3216C芯片手册有提到:ALS的典型转换时间为100ms、IR的典型转换时间为12.5ms,所以如果同时打开ALS+IR+PS的话,读取数据时,两次数据读取的时间间隔要大于112.5ms,所以在程序设计时,记住此时间不能小于112.5ms:
图25.5.2.2 数据寄存器的转换时间
此外要注意,读取数据寄存器的值时:
如果读取寄存器0X0A的最高位(位7)得到1时,说明IR和PS 的数据无效,否则有效;
如果读取寄存器0X0E的第6位得到1时,说明IR和PS 数据无效,否则有效。
以上在程序设计时一定要注意。经过I2C以及AP3216C工作模式的了解,下面我们使用I2C5来对AP3216C进行控制。
25.5.3. AP3216C数据传输时序
如要对AP3216C进行操作,我们先了解AP3216C的通信时序。如下图是AP3216C的读写时序图,其中:
S是主机发送的起始信号位,Sr是主机发送的重复起始信号位;Slave address是AP3216C的设备地址,不带读写位就共7位;Register address是AP3216C的寄存器的地址,共8位;Register Comand是要写到AP3216C的寄存器的值;W是写信号位(0),R是读信号位(1);P是停止信号位。这里,STM32MP157主控是主机,AP3216C是从机。
图25.5.3.1 写时序
2. 读时序
如下图,AP3216C的读时序比较特殊,它是先写再读。主机发送起始位,然后发送AP3216C的设备地址,由于是写操作,所以设备地址后再跟1个写位(0),这个是主机发送的第一个字节;当主机收到AP3216C的应答信号后,主机发送第二个字节:AP3216C的寄存器地址;主机接收到AP3216返回的应答信号后,接着先发送一个重复起始位,然后再发送AP3216的从机地址外加一个读位(1),这是第三个字节;当主机接收到AP3216C的应答信号后,就发送要写到AP3216C的寄存器的数值,这是第四个字节;最后主机发送非应答位和停止位,读取操作停止。
图25.5.3.2 读时序
25.6 硬件I2C通信实验
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 18-1 I2C_Hardware。
25.6.1 硬件设计
图25.6 3.1开发板硬件示意图
程序下载后,通过用手靠近或者遮挡AP3216C,即可得到变化的值。
2. 硬件资源
1)LED灯:LED0
2)UART4
3)I2C5
4)AP3216C
LED0 AP3216C I2C5_SCL I2C5_SDA UART4_TX UART4_RX
PI0 接在I2C5上 PA11 PA12 PG11 PB2
表25.6 2.1硬件资源
3. 原理图
如下图,AP3216C的时钟引脚和数据引脚分别接在I2C5_SCL和I2C5_SDA上:
图25.6.1.2原理图部分
25.6.2 软件设计
图25.6.2.1 I2C5时钟源选择
(2)配置I2C5
本实验还会用到LED0和串口,关于这两个外设的配置我们就不再重复讲解了。下面我们讲解配置I2C5部分。配置I2C5的两个引脚PA11和PA12,为开漏输出、上拉模式,如下:
图25.6.2.2 I2C5引脚的配置
找到I2C5配置项,按步骤配置如下:
图25.6.2.3 I2C5参数配置
在步骤3中,可以选:
I2C:使用的I2C进行通信,这里我们要选I2C;
SMBus-Alert-mode:SMBus警报模式;
SMBus-two-wire-Interface:SMBus两线接口,即使用SMBus进行通信,前面我们分析过,SMBus其实和I2C是很相似的,也是两根总线。
在步骤4中,我们要手动配置I2C的参数,实际上不管STM32是处于从模式下还是主模式下,这些参数配置都是一样的,我们解释一下这些配置项的信息:
Timing configuration(时序配置)
Custom Timing:该选项如果使能的话,可以自己手动配置时序Timing值,如果不使能的话,则通过STM32CubeMX来配置。这里我们选择Disabled,即通过STM32CubeMX来配置Timing值。
12C Speed Mode :用于配置I2C的速度模式,I2C速度模式有标准(速度可达100KHz)、快速(速度可达400 kHz)、超快速模式(速度可达1 MHz),这里我们就选快速模式,那么配置出来的I2C速度等级大概就在400KHz前后。
12C Speed Frequency(KHz):用于配置I2C的速度,如果是标准模式,此值在1100之间;如果是快速模式,此值在1400之间,要是超快速模式,此值在1~1000之间。这里我们选的是快速模式,所以STM32CubeMX会自动配置为400,当然我们也可以手动调整该值。
Rise Time(ns):SDA 和 SCL 信号的上升时间。
Fall Time(ns):SDA 和 SCL 信号的下降时间
关于上升时间和下降时间,我们就不配置了,就默认为0,如果大家要配置,就按照参考手册给的范围值来配置:
图25.6.2.4 I2C5的上升和下降时间
Coefficient of Digital Filter:是数字滤波器的系数,配置范围可在0~15之间,这里我们就不配置了,默认配置为0。
Analog Fiter:是模拟滤波器,这里我们就默认使用模拟滤波器了,此项配置为 Enabled 。
Timing:是根据以上的参数配置出来的最终的时序值,也就是最终写入I2C_TIMINGR寄存器的值。
Slave Features(从机功能)
本实验我们是把STM32MP157当做主机,所以就不需要配置从机功能了。
Clock No Stretch Mode:时钟延长模式,时钟延长是指通过将SCL线拉低来暂停传输,直到释放SCL线为高电平后,传输才可以继续进行,注意的是,数据必须在建立时间内保持有效,之后才能释放时钟,时钟延长是可选的。
General Call Address Detection:通用地址呼叫检测,即广播呼叫,可以选择开启或者不开启。这里我们就保持默认设置,即不开启。
Primary Address Length selecti…:用于配置从设备地址长度,可选7位或者10位,这里保持默认为7位。
Dual Address Acknowledged :用于配置双地址确认,也是保持默认配置,即不开启。
Primary slave address:用于配置从设备初始地址,本节实验我们是把STM32当做主设备,所以就不需要配置此项了,如果作为从设备,可以配置此项,或者在生成初始化代码的时候再手动添加。这里可配置的值是0~127,实际上就是配置OwnAddress1的值,如果配置该项为0,则OwnAddress1的值为0,如果配置该项为1,则OwnAddress1的值为2,如果配置该项为2,则OwnAddress1的值为4,以此类推。
(3)生成初始化代码
记得配置工程生成独立的.c和.h文件,保存工程配置,生成工程,然后在工程的Src/下添加BSP文件夹,因为我们要使用BSP文件夹里的LED驱动。最后生成M4工程如下:
图25.6.2.5 生成工程
2. I2C初始化代码分析
I2C初始化代码如下,代码中已经附上了详细的注释,可以很快看懂代码,注意的是,根据配置,我们计算出I2C5的SCL的频率约为423KHz,而实际上并不是这个值,具体是多少,可以使用逻辑分析仪进行测试。
1 #include "i2c.h"
2 I2C_HandleTypeDef hi2c5; /* I2C5句柄 */
3 /**
4 * @brief IIC初始化函数
5 * @note 本函数支持使用I2C5,I2C时钟源为PCLK1=104.5Mhz,tI2CCLK=1/104500000
6 * Timing负责I2C速度设置,对应I2CX_TIMINGR寄存器,比寄存器各位描述如下:
7 * bit[31:28] PRESC : 时间分频值,时钟周期tPRESC = (PRESC+1)*tI2CCLK
8 * bit[23:20] SCLDEL : 数据建立时间,也就是SDA跳变沿与SCL上升沿之间的延时,
9 * tSCLDEL = (SCLDEL+1)*tPRESC
10 * bit[19:16] SDADEL : 数据保持时间,也就是SCL下降沿与SDA跳变沿之间的延时,
11 * tSDADEL = (SDADEL+1)*tPRESC
12 * bit[15:8] SCLH : SCL高电平周期,tSCLH=(SCLH+1)*tPRESC
13 * bit[7:0] SCLL : SCL低电平周期,tSCLL=(SCLL+1)*tPRESC
14 * 通过PRESC、SCLH和SCLL即可确定I2C时钟频率,比如:
15 * 400KHz时:I2CX_TIMINGR寄存器=0x00A037BE,PRESEC=0000=0,
16 * tPRESC=(0+1)*(1/104500000)=9.57ns.
17 * SCLH=00110111=55, tSCLH=(55+1)*9.57ns=535.9ns,
18 * SCLL=1011 1110=190, tSCLL=(190+1)*9.57ns=1827.8ns,
19 * SCL周期理论值为tSCLH+tSCLL=535.9ns+1827.8ns=2363.7ns约等于423KHz。
20 * 注意!0x00A037BE是STM32CubeMX计算出来的,因此在实际使用中,
21 * Timing属性最好是直接用STM32CubeMX来计算
22 * @param 无
23 * @retval 无
24 */
25 void MX_I2C5_Init(void)
26 {
27 hi2c5.Instance = I2C5; /* I2C5 */
28 hi2c5.Init.Timing = 0x00A037BE; /* 高速模式400KHz的时序 */
29 hi2c5.Init.OwnAddress1 = 0; /* I2C主机地址1(自定义,不要和从机冲突即可) */
30 hi2c5.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; /* 7位地址模式 */
31 hi2c5.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; /* 关闭双地址模式 */
32 hi2c5.Init.OwnAddress2 = 0; /* I2C主机地址2(自定义,不要和从机冲突即可) */
33 hi2c5.Init.OwnAddress2Masks = I2C_OA2_NOMASK; /* 地址2没有配置,此项可以忽略 */
34 hi2c5.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; /* 关闭广播呼叫模式 */
35 hi2c5.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; /* 关闭时钟延长模式 */
36 if (HAL_I2C_Init(&hi2c5) != HAL_OK) /* 调用HAL库将以上配置写入对应寄存器 */
37 {
38 Error_Handler();
39 }
40 /* 配置模拟噪声滤波器 */
41 if (HAL_I2CEx_ConfigAnalogFilter(&hi2c5, I2C_ANALOGFILTER_ENABLE) != HAL_OK)
42 {
43 Error_Handler();
44 }
45 /* 配置数字噪声滤波器 */
46 if (HAL_I2CEx_ConfigDigitalFilter(&hi2c5, 0) != HAL_OK)
47 {
48 Error_Handler();
49 }
50
51 }
52 /**
53 * @brief I2C底层驱动,时钟配置
54 * @param i2cHandle:I2C句柄
55 * @note 此函数会被HAL_I2C_Init()调用
56 * @retval 无
57 */
58 void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
59 {
60 GPIO_InitTypeDef GPIO_InitStruct = {0};
61 RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
62 if(i2cHandle->Instance==I2C5)
63 {
64 /* 手动将GPIO时钟使能代码添加到该位置 */
65 __HAL_RCC_GPIOA_CLK_ENABLE(); /* 使能GPIOA时钟 */
66 __HAL_RCC_I2C5_CLK_ENABLE(); /* 使能I2C5时钟 */
67 if(IS_ENGINEERING_BOOT_MODE())
68 {
69 /* 配置I2C5和I2C3时钟 */
70 PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_I2C35;
71 /* I2C3,5时钟源位PCLK1=104.5MHz */
72 PeriphClkInit.I2c35ClockSelection = RCC_I2C35CLKSOURCE_PCLK1;
73 if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)/* 初始化I2C5时钟 */
74 {
75 Error_Handler();
76 }
77 }
78 /**I2C5 GPIO Configuration
79 PA12 ------> I2C5_SDA
80 PA11 ------> I2C5_SCL
81 */
82 GPIO_InitStruct.Pin = GPIO_PIN_12|GPIO_PIN_11; /* PA11和PA12 */
83 GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; /* 开漏输出 */
84 GPIO_InitStruct.Pull = GPIO_PULLUP; /* 上拉 */
85 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速模式 */
86 GPIO_InitStruct.Alternate = GPIO_AF4_I2C5; /* 复用为I2C5 */
87 HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* 初始化GPIOA */
88 }
89 }
90 /**
91 * @brief I2C取消初始化函数
92 * @param i2cHandle:I2C句柄
93 * @note 如果有需要,可以调用此函数关闭I2C5
94 * @retval 无
95 */
96 void HAL_I2C_MspDeInit(I2C_HandleTypeDef* i2cHandle)
97 {
98
99 if(i2cHandle->Instance==I2C5)
100 {
101 __HAL_RCC_I2C5_CLK_DISABLE();/* 关闭I2C5时钟 */
102 /**I2C5 GPIO Configuration
103 PA12 ------> I2C5_SDA
104 PA11 ------> I2C5_SCL
105 */
106 /* 将GPIOx外设寄存器初始化为其默认复位值 */
107 HAL_GPIO_DeInit(GPIOA, GPIO_PIN_12);
108 HAL_GPIO_DeInit(GPIOA, GPIO_PIN_11);
109 }
110 }
#ifndef __AP3216C_H
#define __AP3216C_H
#include"i2c.h"
extern I2C_HandleTypeDef hi2c5;
#define AP3216C_ADDR 0X3C /* AP3216C器件I2C带写位的地址(左移了一位) */
uint8_t ap3216c_init(void);
void ap3216c_write_one_byte(uint8_t reg,uint8_t data);
uint8_t ap3216c_read_one_byte(uint8_t reg);
void ap3216c_read_data(uint16_t* ir,uint16_t* ps,uint16_t* als);
#endif
AP3216C.h文件代码主要是函数的声明,并定义了AP3216C作为从设备时的器件地址0X3C,该地址带了写位,我们在前面已经讲解该地址的由来。
(2)AP3216C.c文件代码
首先我们来看看AP3216C的初始化函数,如下:
1 /**
2 * @brief 初始化AP3216C
3 * @param 无
4 * @retval 0:初始化成功
5 * 1:初始化失败
6 */
7 uint8_t ap3216c_init(void)
8 {
9 uint8_t temp=0;
10
11 MX_I2C5_Init(); /* 初始化IIC */
12 ap3216c_write_one_byte(0x00, 0X04); /* 复位AP3216C */
13 HAL_Delay(50); /* AP33216C复位至少10ms */
14 ap3216c_write_one_byte(0x00, 0X03); /* 开启ALS、PS、IR */
15 temp=ap3216c_read_one_byte(0X00); /* 读取刚刚写进去的0X03 */
16 if(temp == 0X03)
17 return 0; /* AP3216C正常 */
18 else
19 return 1; /* AP3216C失败 */
20 }
第11行,调用生成的I2C初始化代码初始化I2C5;
第12行,先软件复位AP3216C;
第13行,AP3216C软件复位后,至少要等待10ms的时候后才可以操作AP3216C,这里我们等待50ms;
第14行,0X00的地址是AP3216C的模式控制寄存器,我们将0X03写入该寄存器中,也就是开启 ALS+PS+IR;
第15行,读取0X00寄存器的值;
第16~19行,判断读0X00寄存器的值是否为我们刚写进去的0X03,如果是则返回0,如果不是,则返回1。
为什么要这么做呢,这个目的就是判断AP3216C是已经被I2C5识别了,我们来分析一下:
一般的芯片是有个 ID 寄存器,通过读取 ID 寄存器判断 ID 是否正确就可以检测芯片是否存在,但是 AP3216C 没有 ID 寄存器,所以我们就通过向寄存器 0X00 写入一个值,然后再读取 0X00 寄存器,判断读出得到值和写入的是否相等,如果相等就表示 AP3216C 存在,否则的话 AP3216C 就不存在。
AP3216C的初始化函数调用了ap3216c_write_one_byte和ap3216c_read_one_byte函数,分别为写AP3216C函数和读AP3216C函数,我们来分写一下这两个函数:
1 /**
2 * @brief 向AP3216C指定寄存器写一个字节
3 * @param reg: 寄存器地址
4 * @param data: 要写入的数据
5 * @retval 无
6 */
7 void ap3216c_write_one_byte(uint8_t reg,uint8_t data)
8 {
9 uint8_t senddata[2];
10 senddata[0] = reg; /* 寄存器地址 */
11 senddata[1] = data; /* 要写的数据 */
12 HAL_I2C_Master_Transmit(&hi2c5, AP3216C_ADDR, senddata, 2, 1000);
13 }
14 /**
15 * @brief 读取AP3216C指定寄存器数据
16 * @param reg: 寄存器地址
17 * @retval 读到的数据
18 */
19 uint8_t ap3216c_read_one_byte(uint8_t reg)
20 {
21 uint8_t res;
22 uint8_t senddata;
23 /* 1、先发送要读取的寄存器地址 */
24 senddata = reg; /* 寄存器地址 */
25 HAL_I2C_Master_Transmit(&hi2c5, AP3216C_ADDR, &senddata, 1, 1000);
26 /* 2、读取数据 */;
27 HAL_I2C_Master_Receive(&hi2c5, AP3216C_ADDR, &res, 1, 1000);
28 return res;
29 }
第7~13行是向AP3216C的某个寄存器写数据:
第9行,定义一个长度为2的数组senddata;
第10行,senddata[0]存放要写到AP3216C的寄存器地址;
第11行,senddata[1]存放要写到AP3216C寄存器的数据;
第12行,调用HAL_I2C_Master_Transmit函数向AP3216C发送数据,这里注意的是,发送的数据有2个字节,第一个字节是AP3216C寄存器的地址(在senddata[0]中),第二个字节是要写入寄存器的数据(在senddata[1]中);
第19~28行是读取AP3216C寄存器的数值:
第25行,先发送要读取的从机的寄存器的地址;
第27行,再读取从机的寄存器,读取到的值放在res里,并返回res的值;
接下来我们看看如何使用以上的函数读取AP3216C涉及的寄存器的数据,本节实验我们要读取AP3216C的数据寄存器的值,通过这些值计获取ALS、PS 和 IR的值:
1 /**
2 * @brief 读取AP3216C的数据
3 * @note 注意!如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
4 * @param ir: 读取到的ir值
5 * @param ps: 读取到的ps值
6 * @param als: 读取到的als值
7 * @retval 无
8 */
9 void ap3216c_read_data(uint16_t* ir,uint16_t* ps,uint16_t* als)
10 {
11 uint8_t i;
12 uint8_t buf[6];
13 for(i=0;i<6;i++)
14 {
15 buf[i]=ap3216c_read_one_byte(0X0A + i); /* 循环读取所有传感器数据*/
16 }
17 if(buf[0] & 0X80)
18 *ir = 0; /* IR_OF位为1,则数据无效 */
19 else
20 *ir = ((uint16_t)buf[1] << 2) | (buf[0] & 0X03);/* 读取IR传感器的数据 */
21 *als = ((uint16_t)buf[3] << 8) | buf[2];/* 读取ALS传感器的数据 */
22 if(buf[4] & 0x40)
23 *ps=0; /* IR_OF位为1,则数据无效 */
24 else
25 *ps=((uint16_t)(buf[5]&0X3F)<<4)|(buf[4]&0X0F);/* 读取PS传感器的数据 */
26 }
第12行,定义一个长度为6的数组,数组中的6个元素buf[0]~ buf[5]用于依次存放读取到的数据寄存器0X0A~0X0F的值;
第15行,调用前面的ap3216c_read_one_byte函数循环读出数据寄存器0X0A~0X0F的值;
第17~21行,判断IR和ALS的值是否有效,有效的话则读取其值:
第17和19行,如果读取0X0A寄存器的第7位得到1,说明IR和ALS的数据无效,否则有效;
第20行,当IR和ALS的数据有效时,获取IR的数据。0X0A寄存器的最低2位保存的是IR的最低2位的值,0X0B寄存器的0~8位保存的是IR 高 8 位数据,只要获取这些位的数据就可以得到IR的值;
第21行,0X0C寄存器的低7位保存的是ALS 低 8 位数据,0X0D寄存器的低7位保存的是ALS 高 8 位数据,只要获取这些位的值就可以得到ALS的值;
第22~25行,同样的,先判断寄存器0X0E的第6位是否是1,是1的话,IR和PS 数据无效,反之则有效,如果有效,就获取PS的值。
(3)main.c文件代码
main.c文件部分代码如下,手动在标红的字体之间添加代码:
1 #include "main.h"
2 #include "i2c.h"
3 #include "usart.h"
4 #include "gpio.h"
5 /* USER CODE BEGIN Includes */
6 #include "./BSP/Include/led.h"
7 #include "./BSP/Include/AP3216C.h"
8 /* USER CODE END Includes */
9 void SystemClock_Config(void);
10 int main(void)
11 {
12 HAL_Init(); /* 初始化HAL库 */
13 if(IS_ENGINEERING_BOOT_MODE())
14 {
15 SystemClock_Config(); /* 配置系统时钟 */
16 }
17 MX_GPIO_Init(); /* 初始化GPIO */
18 MX_I2C5_Init(); /* 初始化I2C5 */
19 MX_UART4_Init(); /* 初始化UART4 */
20 /* USER CODE BEGIN 2 */
21 /* printf("请输入字符,并按下回车键结束\r\n"); */
22 HAL_UART_Receive_IT(&huart4,&RxBuffer,1);/* 以中断方式接收函数 */
23 led_init(); /* 初始化LED */
24 uint16_t ir,als,ps;
25 /* USER CODE END 2 */
26 while (1)
27 {
28 /* USER CODE BEGIN 3 */
29 while(ap3216c_init()) /* 检测不到AP3216C */
30 {
31 printf("AP3216C Check Failed!\r\n");/* 打印提示信息 */
32 HAL_Delay(1000); /* 延时1分钟 */
33 LED0_TOGGLE(); /* LED0闪烁 */
34 }
35 while(1)
36 {
37 ap3216c_read_data(&ir,&ps,&als); /* 读取数据 */
38 /* 打印 IR、PS、ALS的值 */
39 printf("ir = %d, ps = %d, als = %d\r\n", ir, ps, als);
40 LED0_TOGGLE(); /* LED0闪烁 */
41 HAL_Delay(120); /* 延时120ms */
42 }
43 }
44 /* USER CODE END 3 */
45 }
第29~34行,先初始化AP3216C,并测试是否有检查到AP3216C,如果有的话,返回0,如果没测试到,返回1,程序就会每隔1分钟打印提示信息“AP3216C Check Failed!”;
第35~42行,读取IR、PS、ALS的值并打印出来,注意第41行的代码,我们前面说过,如果同时打开 ALS、PS 和 IR,读取数据的间隔至少要112.5ms,这里我们间隔120ms。
25.6.3 编译和测试
编译工程无报错后,在线调试运行程序,串口打印数据,同时LED0在闪烁,用手挡住AP3216C,串口打印的ALS值明显发生变化,用手靠近AP3216C,串口打印的PS值也明显发生变化,PS值最小为0,ALS值最大为1023。大家也可以使用逻辑分析仪测试一下SCL的频率为多少,笔者测试本实验的SCL频率为370KHz左右,和前面计算的理论值是有一些相差的。
图25.6.3.1 测试结果
25.7 软件I2C实验
本节实验使用两个GPIO口来模拟SCL时钟线和SDA数据线,编写I2C读和写时序逻辑,实现读取AP3216C对应寄存器的值。
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\11、M4 CubeIDE裸机驱动例程\CubeIDE_project\ 18-2 I2C_Software。
25.7.1 硬件设计
硬件设计部分和前面硬件I2C实验一致,下面我们开始软件设计部分。
25.7.2 软件设计
图25.7.2.1配置GPIO引脚为输出模式
再配置IO口为开漏输出、上拉、高速模式:
25.7.2.2 配置GPIO引脚模式
这里解释一下为什么要配置为开漏输出:
图25.7.2.3 GPIO的基本结构图
主机要用模拟的SCL输出高低电平,即时钟信号,要用模拟的SDA输出高低电平和输入扫描,我们在第十章有介绍过开漏输出,"漏"指的是MOS管的漏极,其输出端相当于三极管的集电极,如果不接上拉电阻,开漏只能输出低电平,没有输出高电平的能力,要得到有驱动能力的高电平状态需要加上拉电阻才行。试想,如果不接上拉电阻时:IO口输出逻辑1,P-MOS关闭,输出电路处于开路状态;IO口输出逻辑0,N-MOS导通,输出低电平。无法输出高电平。当接上上拉电阻时,IO输出逻辑1,P-MOS关闭,因为有上拉电阻的存在,所以输出高电平;IO口输出逻辑0,N-MOS导通,输出低电平。
这里大家会问,那是否可以配置为推挽输出呢?这里说明一下:SCL线用于输出时钟信号,可以配置为推挽或者开漏输出;SDA线必须要配置为开漏输出,因为SDA线要作为输入扫描功能,如果配置为推挽输出,当要实现输入扫描检测时,会受到输出电路没有关闭的影响,之前的输出电平还是存在,造成输入电路和输出电路的短接,可能会损坏芯片;而配置为开漏输出时,当要作为输入检测时,SDA输出逻辑1,P-MOS关闭,输出电路开路,不会对输入电路产生影响。
接下来,配置工程生成独立的.c和.h文件,然后生成初始化代码,再将上一章节实验的BSP文件夹拷贝到本工程的Src目录下,本节实验,我们需要在BSP文件夹中新建.c文件和.h文件,手动编写模拟I2C的程序以及读取AP3216C寄存器的数据:
图25.7.2.4 生成的工程
2. 添加用户驱动代码
本节实验我们要用到us延时,所以把第二十三章实验的延时函数代码文件delay.h和delay.c文件拷贝到BSP文件夹下,然后,我们在BSP文件夹下新建I2C.c文件,在BSP/Include文件夹下新建I2C.h文件,使用GPIO口模拟I2C驱动的代码就在这两个文件中添加:
图25.7.2.5 新建文件
下面我们先添加GPIO口模拟I2C的驱动代码:
(1)I2C.h文件代码
I2C.h文件代码主要是函数的声明、引脚定义、IO操作函数定义:
1 #ifndef _IIC_H
2 #define _IIC_H
3 #include"gpio.h"
4
5 /* PA11引脚 定义 */
6 #define IIC_SCL_GPIO_PORT GPIOA
7 #define IIC_SCL_GPIO_PIN GPIO_PIN_11
8 /* PA11引脚 定义 */
9 #define IIC_SDA_GPIO_PORT GPIOA
10 #define IIC_SDA_GPIO_PIN GPIO_PIN_12
11 /* IO操作 */
12 #define IIC_SCL(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) /* SCL */
13 #define IIC_SDA(x) do{ x ? \
HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \ HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \ }while(0) /* SDA */
14 /* 读取SDA */
15 #define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN)
16
17 /* IIC所有操作函数 */
18 void iic_init(void); /* 初始化IIC的IO口 */
19 void iic_start(void); /* 发送IIC开始信号 */
20 void iic_stop(void); /* 发送IIC停止信号 */
21 void iic_ack(void); /* IIC发送ACK信号 */
22 void iic_nack(void); /* IIC不发送ACK信号 */
23 uint8_t iic_wait_ack(void); /* IIC等待ACK信号 */
24 void iic_send_byte(uint8_t txd); /* IIC发送一个字节 */
25 uint8_t iic_read_byte(unsigned char ack); /* IIC读取一个字节 */
26
27 #endif
第6~10行,定义模拟I2C要用到的两个引脚PA11和PA12;
第12行,参数X为0表示PA11引脚输出低电平,参数X为1表示PA12引脚输出高电平,通过该函数PA11可以模拟SCL信号;
第13行,参数X为0表示PA12引脚输出低电平,参数X为1表示PA12引脚输出高电平,通过该函数PA12可以模拟SDA信号;
第15行,读取PA12引脚,也就是模拟读取SDA信号;
第18~25行是I2C.c文件的一些函数声明,下面,我们来看看这些函数。
(2)I2C.c文件代码
I2C初始化函数
I2C.c文件的函数比较多,我们来逐个分析,首先是I2C初始化函数:
/**
* @brief 初始化IIC
* @param 无
* @retval 无
*/
void iic_init(void)
{
/* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */
MX_GPIO_Init(); /* 初始化PA11和PA12,配置为开漏输出、上拉、高速模式 */
iic_stop(); /* 停止总线上所有设备 */
}
iic_init函数主要是初始化软件模拟I2C用到的两个IO口为开漏输出、上拉、高速模式,并调用iic_stop产生I2C停止信号,初始化的时候,所有I2C设备不能进行通信。
延时函数
如下,iic_delay函数用于实现2us延时函数,目的就是控制I2C的读写速度,通过示波器检测读写速度在250KHz内,所以一秒钟传送500Kb数据,换算一下即一个bit位需要2us,在这个延时时间内可以让器件进行获得一个稳定性的数据采集。
/**
* @brief IIC延时函数,用于控制IIC读写速度
* @param 无
* @retval 无
*/
static void iic_delay(void)
{
delay_us(2); /* 2us的延时, 读写速度在250Khz以内 */
}
主机发送停止信号函数
iic_stop函数是主机发送停止信号函数,如下:
/**
* @brief 产生IIC停止信号
* @param 无
* @note 停止信号是:在 SCL 为高电平的时候,SDA 出现上升沿。
* 这里配置SAD先输出低电平,然后SCL输出高电平,SDA再输出
* 高电平,这样就模拟SDA上升沿了。
* @retval 无
*/
void iic_stop(void)
{
IIC_SDA(0);
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SDA(1); /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
iic_delay();
}
I2C的停止信号就是,当SCL为高电平时,SDA上升沿的时刻,以上是先将PA12拉低一段时间了再拉高,模拟出上升沿,PA11拉高,模拟SCL为高电平。
主机发送起始信号函数
iic_start函数是主机发送起始位函数:
1 /**
2 * @brief 产生IIC起始信号
3 * @param 无
4 * @note 起始信号是:在 SCL 为高电平的时候,SDA 出现下降沿。
5 * 这里配置SAD和SCL先输出高电平,然后延时2us,SDA再输出低电平,
6 * 这样一来就模拟SCL为高电平,SDA为下降沿了。
7 * @retval 无
8 */
9 void iic_start(void)
10 {
11 IIC_SDA(1);
12 IIC_SCL(1);
13 iic_delay();
14 IIC_SDA(0); /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
15
16 iic_delay();
17 IIC_SCL(0); /* 钳住I2C总线,准备发送或接收数据 */
18 iic_delay();
19 }
第11~16行,PA12先输出高电平一段时间再输出低电平,这个过程模拟了SDA的上升沿;
第17行,SCL输出低电平,前面我们分析I2C时序图的时候说过,当 SCL为低电平时,SDA的数据无效(不稳定),也就是通过这个时候,SDA 进行电平切换,SDA 上的数据发生变化,为下一次数据传输做准备,数据在SCL的上升沿到来之前就需准备好,并在在下降沿到来之前必须稳定。
为了大家更加清晰了解代码实现的过程,下面单独把起始信号和停止信号从iic总线时序图中抽取出来,如下图:
图25.7.2.6 起始信号和停止信号
产生ACK应答信号
iic_ack函数用于模拟从机产生应答信号,从机通过将SDA拉低来产生应答信号:
1 /**
2 * @brief 产生ACK应答
3 * @param 无
4 * @note
5 * @retval 无
6 */
7 void iic_ack(void)
8 {
9 IIC_SDA(0); /* SCL = 1 时, SDA = 0,表示应答 */
10 iic_delay();
11 IIC_SCL(1);
12 iic_delay();
13
14 IIC_SCL(0); /* SCL 1 -> 0 */
15 iic_delay();
16 IIC_SDA(1); /* 主机释放SDA线 */
17 iic_delay();
18 }
第9~12行,PA12输出低电平,PA11输出高电平,模拟的是当SCL为高电平时,SDA被从机拉低,表示从机发出应答信号;
第14~17行,PA12输出高电平,表示SDA被拉高,即模拟主机释放SDA线。
不产生应答信号
/**
* @brief 不产生ACK应答
* @param 无
* @retval 无
*/
void iic_nack(void)
{
IIC_SDA(1); /* SCL高电平 时, SDA = 1,表示不应答 */
iic_delay();
IIC_SCL(1);
iic_delay();
IIC_SCL(0); /* SCL 1 -> 0产生一个时钟 */
iic_delay();
}
如上代码,如果不产生应答信号,在SCL为高电平期间,SDA为高电平,以代码先让PA12输出高电平,模拟SDA为高电平,然后PA11先输出高电平再输出低电平,表示一个时钟信号。
主机发送一个字节
如下,iic_send_byte表示主机发送一个字节函数:
1 /**
2 * @brief IIC发送一个字节
3 * @param data: 要发送的数据
4 * @retval 无
5 */
6 void iic_send_byte(uint8_t data)
7 {
8 uint8_t t;
9 for (t = 0; t < 8; t++)
10 {
11 IIC_SDA((data & 0x80) >> 7); /* 高位先发送 */
12 iic_delay();
13 IIC_SCL(1);
14 iic_delay();
15 IIC_SCL(0); /* SCL 1 -> 0产生一个时钟 */
16 data <<= 1; /* 左移1位,用于下一次发送 */
17 }
18 IIC_SDA(1); /* 发送完成, 主机释放SDA线 */
19 }
第9行,一个字节8位,逐位进行发送;
第11行,先发送字节的最高位,再发送字节的最低位;
第12~15行,PA11先拉高,再拉低,模拟一个时钟信号;
第16行,左移1位,用于下一次发送;
在I2C总线传输中,一个时钟信号就发送一个bit,所以该函数需要循环八次,模拟八个时钟信号,才能把形参的8个位数据都发送出去,这里使用的是形参data和0x80与运算的方式,判断其最高位的逻辑值,假如为1即需要控制SDA输出高电平,否则为0控制SDA输出低电平。为了更好说明,数据发送的过程,单独拿出数据传输时序图,如下图:
图25.7.2.7 数据发送过程
通过上图就可以很清楚了解数据传输时的细节,经过第一步的SDA高低电平的确定后,接着需要延时,确保SDA输出的电平稳定,在SCL保持高电平期间,SDA线上的数据是有效的,此过程也是需要延时,使得从设备能够采集到有效的电平。然后准备下一位的数据,所以这里需要的是把data左移一位,等待下一个时钟的到来,从设备进行读取。把上述的操作重复8次就可以把data的8个位数据发送完毕,循环结束后,把SDA线拉高,等待接收从设备发送过来的应答信号。
主机读取一个字节
iic_read_byte函数就是主设备读取一个字节函数,如下:
/**
* @brief IIC读取一个字节
* @param ack: ack=1时,发送ack; ack=0时,发送nack
* @retval 接收到的数据
*/
uint8_t iic_read_byte(uint8_t ack)
{
uint8_t i, receive = 0;
for (i = 0; i < 8; i++ ) /* 接收1个字节数据 */
{
receive <<= 1; /* 高位先输出,所以先收到的数据位要左移 */
IIC_SCL(1);
iic_delay();
if (IIC_READ_SDA)
{
receive++;
}
IIC_SCL(0);
iic_delay();
}
if (!ack)
{
iic_nack(); /* 发送nACK */
}
else
{
iic_ack(); /* 发送ACK */
}
return receive;
}
iic_read_byte函数具体实现的方式跟iic_send_byte函数有所不同。首先可以明确的是时钟信号是通过主机发出的,而且接收到的数据大小为1字节,但是IIC传输的单位是bit,所以就需要执行8次循环,才能把一字节数据接收完整。
具体实现过程:首先需要一个变量receive存放接收到的数据,在每一次循环开始前都需要对receive进行左移1位操作,那么receive的bit0位每一次赋值前都是空的,用来存放最新接收到的数据位,然后在SCL线进行高低电平切换时输出IIC时钟,在SCL高电平期间加入延时,确保有足够的时间能让数据发送并进行处理,使用宏定义IIC_READ_SDA就可以判断读取到的高低电平,假如SDA为高电平,那么receive++即在bit0置1,否则不做处理即保持原来的0状态。当SCL线拉低后,需要加入延时,便于从机切换SDA线输出数据。
在8次循环结束后,我们就获得了8bit数据,把它作为返回值返回,然而按照时序图,作为主机就需要发送应答或者非应答信号,去回复从机。
以上模拟了I2C的时序,下面我们来看看怎么使用这些函数来操作AP3216C。
(3)AP3216C.h文件代码
AP3216C.h文件主要是定义了AP3216C的读和写地址定义以及函数声明,如下:
#ifndef __AP3216C_H
#define __AP3216C_H
#include"gpio.h"
#include "./I2C.h"
#define AP3216C_ADDR_W 0X3C /* AP3216C器件IIC写地址(左移了一位) */
#define AP3216C_ADDR_R 0x3D /* AP3216C器件IIC读地址(最右边一位为0) */
uint8_t ap3216c_init(void);
void ap3216c_write_one_byte(uint8_t reg,uint8_t data);
uint8_t ap3216c_read_one_byte(uint8_t reg);
void ap3216c_read_data(uint16_t* ir,uint16_t* ps,uint16_t* als);
#endif
AP3216C的读写地址我们在前面的25.5.2小节已经进行了讲解。
(4)AP3216C.c文件代码
AP3216C.c文件代码主要就是实现对AP3216C的写和读操作,程序的实现过程是根据AP3216C的时序来写的,如下:
初始化AP3216C
ap3216c_init用于初始化AP3216C,代码如下:
1 /**
2 * @brief 初始化AP3216C
3 * @param 无
4 * @retval 0,初始化成功
5 * 1,初始化失败
6 */
7 uint8_t ap3216c_init(void)
8 {
9 uint8_t temp=0;
10 iic_init(); /* 初始化IIC */
11 ap3216c_write_one_byte(0x00, 0X04); /* 复位AP3216C */
12 delay_ms(50); /* AP33216C复位至少10ms */
13 ap3216c_write_one_byte(0x00, 0X03); /* 开启ALS、PS、IR */
14 temp=ap3216c_read_one_byte(0X00); /* 读取刚刚写进去的0X03 */
15 if(temp == 0X03)
16 return 0; /* AP3216C正常 */
17 else
18 return 1; /* AP3216C失败 */
19 }
第10行,初始化PA11和PA12两个引脚,即SCL和SDA;
第11行,先软件复位AP3216C;
第12行,AP3216C软件复位后,至少要等待10ms的时候后才可以操作AP3216C,这里我们等待50ms;
第13行,0X00的地址是AP3216C的模式控制寄存器,我们将0X03写入该寄存器中,也就是开启 ALS、PS、IR;
第14行,读取0X00寄存器的值;
第15~18行,判断读0X00寄存器的值是否为我们刚写进去的0X03,如果是则返回0,如果不是,则返回1。
为什么要这么做呢,这个目的就是判断AP3216C是已经被I2C5识别了,我们来分析一下:
一般的芯片是有个 ID 寄存器,通过读取 ID 寄存器判断 ID 是否正确就可以检测芯片是否存在,但是 AP3216C 没有 ID 寄存器,所以我们就通过向寄存器 0X00 写入一个值,然后再读取 0X00 寄存器,判断读出得到值和写入的是否相等,如果相等就表示 AP3216C 存在,否则的话 AP3216C 就不存在。
主机向AP3216C写一个字节数据和向AP3216C读一个字节数据
下面我们来看看以上函数中ap3216c_write_one_byte和ap3216c_read_one_byte函数,这两个函数是分别向AP3216C写数据和读数据:
/**
* @brief 向AP3216C指定寄存器写一个字节
* @param reg: 寄存器地址
* @param data: 要写入的数据
* @retval 无
*/
void ap3216c_write_one_byte(uint8_t reg,uint8_t data)
{
iic_start(); /* 主机发送起始信号 */
iic_send_byte(AP3216C_ADDR_W); /* 主机发送AP3216C的设备地址 */
iic_wait_ack(); /* 主机等待AP3216C应答 */
iic_send_byte(reg); /* 主机收到应答后,发送AP3216C寄存器地址 */
iic_wait_ack(); /* 主机等待AP3216C的应答信号 */
iic_send_byte(data); /* 主机发送要传输到寄存器的数据 */
iic_wait_ack(); /* 主机等待AP3216C的应答信号 */
iic_stop(); /* 主机发送停止信号,完成写的过程 */
}
/**
* @brief 读取AP3216C指定寄存器数据
* @param reg: 寄存器地址
* @retval 读到的数据
*/
uint8_t ap3216c_read_one_byte(uint8_t reg)
{
uint8_t res;
iic_start(); /* 主机发送起始信号 */
iic_send_byte(AP3216C_ADDR_W); /* 主机发送AP3216C的设备地址 */
iic_wait_ack(); /* 主机等待AP3216C的应答 */
iic_send_byte(reg); /* 主机收到应答信号后,发送AP3216C的寄存器地址 */
iic_wait_ack(); /* 主机等待AP3216C的应答信号 */
iic_start(); /* 主机发送重复起始信号 */
iic_send_byte(AP3216C_ADDR_R); /* 主机发送AP3216C的设备地址 */
iic_wait_ack(); /* 主机等待AP3216C的应答信号 */
res=iic_read_byte(0); /* 主机读取AP3216C寄存器的数据 */
iic_stop(); /* 主机发送停止信号,完成读的过程 */
return res; /* 返回主机读取到的数据 */
}
以上两个函数是向AP3216C写一个数据和对AP3216C读一个数据,代码的设计过程就是根据AP3216C的时序来实现的,关于AP3216C的时序我们在前面的25.5.3小节有进行讲解,代码中已经附上了详细的注释,结合AP3216C的时序图可以很容易看明白。
读取AP3216C的数据
ap3216c_read_data用于读取AP3216C寄存器的数据,该函数定义如下:
1 /**
2 * @brief 读取AP3216C的数据
3 * @note 注意!如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
4 * @param ir: 读取到的ir值
5 * @param ps: 读取到的ps值
6 * @param als: 读取到的als值
7 * @retval 无
8 */
9 void ap3216c_read_data(uint16_t* ir,uint16_t* ps,uint16_t* als)
10 {
11 uint8_t i;
12 uint8_t buf[6];
13 for(i=0;i<6;i++)
14 {
15 buf[i]=ap3216c_read_one_byte(0X0A + i); /* 循环读取所有传感器数据 */
16 }
17 if(buf[0] & 0X80)
18 *ir = 0; /* IR_OF位为1,则数据无效 */
19 else
20 *ir = ((uint16_t)buf[1] << 2) | (buf[0] & 0X03);/* 读取IR传感器的数据 */
21 *als = ((uint16_t)buf[3] << 8) | buf[2]; /* 读取ALS传感器的数据 */
22 if(buf[4] & 0x40)
23 *ps=0; /* IR_OF位为1,则数据无效 */
24 else
25 *ps=((uint16_t)(buf[5]&0X3F)<<4)|(buf[4]&0X0F);/* 读取PS传感器的数据 */
26 }
第12行,定义一个长度为6的数组,数组中的6个元素buf[0]~ buf[5]用于依次存放读取到的数据寄存器0X0A~0X0F的值;
第15行,调用前面的ap3216c_read_one_byte函数循环读出数据寄存器0X0A~0X0F的值;
第17~21行,判断IR和ALS的值是否有效,有效的话则读取其值:
第17和19行,如果读取0X0A寄存器的第7位得到1,说明IR和ALS的数据无效,否则有效;
第20行,当IR和ALS的数据有效时,获取IR的数据。0X0A寄存器的最低2位保存的是IR的最低2位的值,0X0B寄存器的0~8位保存的是IR 高 8 位数据,只要获取这些位的数据就可以得到IR的值;
第21行,0X0C寄存器的低7位保存的是ALS 低 8 位数据,0X0D寄存器的低7位保存的是ALS 高 8 位数据,只要获取这些位的值就可以得到ALS的值;
第22~25行,同样的,先判断寄存器0X0E的第6位是否是1,是1的话,IR和PS 数据无效,反之则有效,如果有效,就获取PS的值。
(5)main.c文件代码
main.c文件部分代码如下,手动在标红的字体之间添加代码:
1 #include "main.h"
2 #include "i2c.h"
3 #include "usart.h"
4 #include "gpio.h"
5 /* USER CODE BEGIN Includes */
6 #include "./BSP/Include/led.h"
7 #include "./BSP/Include/AP3216C.h"
8 /* USER CODE END Includes */
9 void SystemClock_Config(void);
10 int main(void)
11 {
12 HAL_Init(); /* 初始化HAL库 */
13 if(IS_ENGINEERING_BOOT_MODE())
14 {
15 SystemClock_Config(); /* 配置系统时钟 */
16 }
17 MX_GPIO_Init(); /* 初始化GPIO */
18 MX_I2C5_Init(); /* 初始化I2C5 */
19 MX_UART4_Init(); /* 初始化UART4 */
20 /* USER CODE BEGIN 2 */
21 /* printf("请输入字符,并按下回车键结束\r\n"); */
22 HAL_UART_Receive_IT(&huart4,&RxBuffer,1);/* 以中断方式接收函数 */
23 led_init(); /* 初始化LED */
24 uint16_t ir,als,ps;
25 /* USER CODE END 2 */
26 while (1)
27 {
28 /* USER CODE BEGIN 3 */
29 while(ap3216c_init()) /* 检测不到AP3216C */
30 {
31 printf("AP3216C Check Failed!\r\n"); /* 打印提示信息 */
32 HAL_Delay(1000); /* 延时1分钟 */
33 LED0_TOGGLE(); /* LED0闪烁 */
34 }
35 while(1)
36 {
37 ap3216c_read_data(&ir,&ps,&als); /* 读取数据 */
38 /* 打印 IR、PS、ALS的值 */
39 printf("ir = %d, ps = %d, als = %d\r\n", ir, ps, als);
40 LED0_TOGGLE(); /* LED0闪烁 */
41 HAL_Delay(120); /* 延时120ms */
42 }
43 }
44 /* USER CODE END 3 */
45 }
25.7.3 编译和测试
编译工程无报错后,在线调试运行程序,串口打印数据,同时LED0在闪烁,用手挡住AP3216C,串口打印的ALS值明显发生变化,用手靠近AP3216C,串口打印的PS值也明显发生变化,用手机的灯照射AP3216C,IR、ALS和PS的值都在明显变化,PS值最小为0,ALS值最大为1023。大家也可以使用逻辑分析仪测试一下SCL的频率为多少,笔者测试本实验的SCL频率为370KHz左右,和前面计算的理论值是有一些相差的。
图25.7.3.1 测试结果