I2C通信协议简介

前言:深入学习IIC的主要目的是为了在STM32平衡小车的项目中读取MPU6050的数据,这对平衡小车来说是至关重要的!!

I2C通信协议(Inter-Integrated Circuit),引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通讯协议的外部收发设备,被广泛地使用在系统内多个集成电路(IC)间的通讯。

I2C物理层特点

它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个I2C通讯总线中,可以连接多个I2C通讯设备,支持多个通讯主机及多个通讯从机。

一个I2C总线只使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。

每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。

总线通过上拉电阻接到电源上,当I2C设备空闲时,会输出高阻态,而当所有设备都空闲输出高阻态的时候,由上拉电阻把总线拉成高电平。

多个主机同时使用总线的时候,为了防止数据冲突,会利用仲裁的方式决定由哪个设备占用总线,I2C设备一般使用开漏的结构,可以实现线与的逻辑,例如在SDA总线上有一个设备输出了低电平,那么这一条SDA总线的电平就为低电平,别的设备对这条总线输出高电平的时候,这条SDA始终为低电平。(线与:有0为0)。

具有三种传输模式“标准模式传输速率为100kbit/s,快速模式为400kbit/s,高速模式下可以达到3.4Mbit/s,但目前大部分I2C设备尚不支持高速模式。

连接到相同总线的IC数量受到总线的最大电容400pF限制的。

I2C的协议层

I2C的协议定义了通讯的起始和停止信号、数据的有效性、响应总裁、时钟同步和地址广播等环节。

II C总线有如下操作模式:主发送模式、主接收模式、从发送模式、从接收模式。下面介绍其通用传输过程及格式。

(1)起始条件和停止条件

当II C接口处于从模式时,要想数据传输,必须检测SDA线上的起始条件,起始条件由主器件产生。起始条件发生在SCL信号为高时,SDA产生一个由高变低的电平变化处。

当II C总线上产生了一个起始条件,那么这条总线就被发出起始条件的主器件占用了,变成“忙”状态:停止条件发生在SCL信号为高时,SDA产生一个由低变高的电平变化处。停止条件也由主器件产生,作用是停止与某个从器件之间的数据传输。当II C总线上产生了一个停止条件,那么在几个时钟周期之后总线就被释放,变成“闲”状态。当主器件送出一个起始条件,它还会立即送出一个从地址,来通知将与它进行数据通信的从器件。1个字节的地址包括7位的地址信息和1位的传输方向指示位,如果第7位为0,表示马上要进行一个写操作;如果为1,表示马上要进行一个读操作。

(2)地址及其数据方向

I2C总线上的每个设备都有主机的独立地址,主机发起通讯时,通过SDA信号线发送设备地址来查找从机。设备地址可以是7位或10位。

紧跟着设备地址的一个数据位就是R/W读写位,用来表示数据的方向,数据方向位为“1“时,表示从主机由从机读数据,数据方向位为“0“时,表示主机向从机写数据。

(3)数据传输格式

SDA线上传输的每个字节长度都是8位,每次传输中字节的数量是没有限制的。在起始条件后面的第一个字节是地址域,之后每个传输的字节后面都有一个应答(A CK)位传输中串行数据的MS B(字节的高位)首先发送。

(4)应答信号ACK

为了完成1个字节的传输操作,接收器应该在接收完1个字节之后发送A CK位到发

送器,告诉发送器,已经收到了这个字节。A CK脉冲信号在SCL线上第9个时钟处发出(前面8个时钟完成1个字节的数据传输,SCL上的时钟都是由主器件产生的)。当发送器要接收A CK脉冲时,应该释放SDA信号线,即将SDA置高。接收器在接收完前面8位数据后,将SDA拉低。发送器探测到SDA为低,就认为接收器成功接收了前面的8位数据。

主机写数据到从机:

主机从从机那读数据:

II C总线工作原理:

IIC总路线用两线来连接多支路总线中的多个设备。这种总线是双向、低速的,并与公共时钟同步。可以直接将一个设备接到IC总线上或是从该总线上取下,而不会影响其他设备。一些生产商,如Microchip公司、Philips公司、Intel公司等生产的小型微处理器都内置有IIC接口。IIC总线的数据传输率比SPI总线要慢一些,在标准模式下传输速度为100 Kb/s,在快速模式下为400 Kb/s。利用II C接口在设备之间进行连接所使用的两根线是SDA(串行数据)和SCL(串行时钟),它们都是开漏(open-drain),通过一个上拉电阻接到正电源,因此在不使用的时候仍保持高电平。使用IIC总线进行通信的设备驱动这两根线变为低电平,在不使用时就让它们保持高电平。每个连到IC的设备都有一个唯一地址,这个设备可以是数据发送者(总线主机)、接收者(总线从机),也可以二者都是。IIC是多主机总线,这意味着可以有多个设备充当总路线主机的角色。

SDA和SCL都是双向的。SPI总线有两根单独的数据线,分别用于两个方向的通信,而II C总线共享同一根信号线来完成主机传送和外设响应:另外,与SPI总线具有多个工作模式不同的是,IIC总线只有一个工作模式。时钟线SCL和数据线SDA之间的时序关系很简单直观:当空闲的时候,SDA和SCL都是高电平,只有SDA变为低电平,接着SCL也变为低电平时才开始II C总线的数据传输。当SDA和SCL都变为低电平时,就是告诉总线上的所有接收设备:数据包的传输开始了。在SCL变为低电平后,SDA才发送(高电平或者低电平)第一个有效数据位,这被称为开始条件。对于被传输的每一位,当SCL为低电平时,在SDA上位必须变为有效。该位是在SCL的上升沿对SDA上的数据位进行采样的,也必须一直保持有效直到SCL再次变为低电平,然后SDA就在SCL再次变为高电平之前传输下一个位。最后,SCL变回高电平(无效),接着SDA也变为高电平,数据传输结束,这被称为结束条件。无论多大的数据包都可以通过II C总线进行传输。像SPI总线一样,IC也是高位先传输。如果数据接收者无法再接收更多的数据,它可以通过将SCL保持低电平来中断传输这样可以迫使数据发送者等待,直到SCL被重新释放。

IIC总线在传送数据过程中共有3种类型信号,它们分别是开始信号、结束信号和应答信号。

开始信号:SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据,

结束信号:SCL为低电平时,SDA由低电平向高电平跳变,结束传送数据。

应答信号:接收数据的IC在接收到8位数据后,向发送数据的IC发出特定的低电平脉冲,表示已收到数据。

CPU向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。发送方发出的每个字节都必须经过接收方确认,每个字节的第8个数据位一旦传送结束,发送方就释放数据线SDA。然后主机在SCL上产生一个额外的时钟脉冲,这会触发接收方通过将SDA置为低电平来表示对接收到的字节进行;如果接收方没能将SDA置为低电平,发送方就会中断传输,并且采取适当的错误处理措施。II C是多主机总线,因此存在同一时间会有多个主机试图开始数据传输的可能。由于II C总线默认的状态是高电平,所以一个主机发送一个数据位0时会将SDA置为低电平,而如果这个数据位是1时就将总线置为默认状态。所以,如果两个主机要同时传输数据,一个主机发送数据位1将总线置为默认状态,但又检测到总线被另外一个主机置为了低电平(发送数据位0),该主机将记录一个错误状态,并且终止数据的传输。

如何使用STM32的产生I2C协议信号

软硬件模拟协议

软件模拟协议:使用CPU直接控制通讯引脚的电平,产生出符合通讯协议标准的逻辑。

硬件实现协议:由STM32的I2C片上外设专门负载实现I2C通讯协议,只要配置好该外设,他就会自动的根据协议要求产生通讯信号,收发数据并且缓存起来,CPU只要检测到该外设的状态和访问数据寄存器,就能完成数据的收发,这种硬件外设处理I2C协议的方式减轻了CPU的工作,并且使其软件设计变得简单。

如果我们直接控制 STM32 的两个 GPIO 引脚,分别用作 SCL 及 SDA,按照上述信号的时序要求,直接像控制 LED 灯那样控制引脚的输出 (若是接收数据时则读取 SDA 电平),就可以实现 I2C 通讯。同样,假如我们按照 USART 的要求去控制引脚,也能实现 USART 通讯。所以只要遵守协议,就是标准的通讯,不管您如何实现它,不管是 ST 生产的控制器还是 ATMEL 生产的存储器,都能按通讯标准交互。由于直接控制 GPIO 引脚电平产生通讯时序时,需要由 CPU 控制每个时刻的引脚状态,所以称之为“软件模拟协议”方式。

STM32 的 I2C 外设可用作通讯的主机及从机,支持 100Kbit/s 和 400Kbit/s 的速率,支持 7 位、10位设备地址,支持 DMA 数据传输,并具有数据校验功能。它的 I2C 外设还支持 SMBus2.0 协议,SMBus 协议与 I2C 类似,主要应用于笔记本电脑的电池管理中。

分析F103的I2C中文参考手册

打开参考手册可以看到I2C的主要特点:

下面的是I2C的框图,由于我们用的是HAL库开发,就不需要像标准库一样知道每一个寄存器的作用和配置,只需要调用封装好的HAL库函数就可以实现对I2C的使用,所以感兴趣的小伙伴们可以自己去了解一下I2C的框图中每个寄存器的作用。

如何利用CubeMX对I2C进行配置

 话不多说,我们直接利用CubeMX对I2C进行配置吧。除了基础的设置,就可以直接对I2C进行设置了。I2C分为主机和从机的配置:先配置主机

快速模式下还可以配置时钟的占空比!!!!

下面是对从机的配置

接下来是对I2C的NVIC的配置

由I2C的主要特性可以知道,他具有两个中断,事件中断和错误中断,通常这两个中断都是需要打开的。

当然我们也可以配置I2C利用DMA来传输数据,

再查看一下GPIO的自动配置:

可以看到I2C的两个引脚分别是PB6和PB7,根据我手上的开发板原理图也可以看到

E2PROM的SCL与SDA也连接着PB6和PB7。所以读取E2PROM通常都是使用I2C来读取。

E2PROM的芯片手册与理解

如果想读取E2PROM,我们还需要对E2PROM有进一步了解,这需要去查看E2PROM的芯片手册。

可以看到A0、A1、A2是设备地址的输入端,也就是从设备地址,SDA与SCL分别是数据端和时钟端。WP是写保护端。

可以看到芯片手册中对Device Address Inputs和WP他们的解释如下:

A0, A1和A2引脚是硬连线(直接到GND或VCC)的设备地址输入与其他双线串行EEPROM设备的兼容性。当引脚硬连接时,有多达八个设备可以在一个总线系统上寻址。设备被选中时,对应的硬件和软件匹配是真的。如果这些引脚保持浮动,A0、A1和A2引脚将保持浮动内部下拉到GND。然而,由于电容耦合,可能会出现在客户在应用程序中,Microchip建议始终将地址引脚连接到已知状态。当使用上拉电阻,建议使用10 kΩ或更少。

E2PROM的写入模式

E2PROM的写入模式有:字节写入和页写入

当写保护输入连接到GND时,允许正常的写操作。当WP引脚为直接连接到VCC,所有对受保护内存的写操作都被禁止。如果引脚处于浮动状态,则WP引脚将在内部向下拉至GND。然而,由于电容性在客户应用中可能出现的耦合,Microchip建议始终连接WP固定到已知状态。当使用上拉电阻时,建议使用10 kΩ或更少。

可以看到芯片手册中Device Address前4位是固定的也就是1010,A2A1A0是由硬件决定的,最后一位读写位是数据传输方向的决定,是由用户决定的。也就是如何A2A1A0接地的话,E2PROM作为从设备被写入,那么他的I2C的Device Address 就是0xA0。如果作为从设备被读取,那么久是0xA1。

E2PROM也可以分为是字节写入和页写入,上图位字节写入的模式的说明。下图为字写入的模式的说明。

页写入可以大量的写入数据,不需要想字节传输模式一样,重复发送设备地址,只需要发送完一个数据后有应答,就可以继续不用发送设备地址的传输数据。(就像火车一样,只需要一个火车头,后面的数据就是车厢一样链接上去,链接处需要ACK应答来保证车厢与车厢之间链接上)

E2PROM读取三种模式

E2PROM的读取也有三种模式:顺序读、随机读、当前地址读。

顺序读取:内部数据字地址计数器维护上次读或写期间访问的最后一个地址运算,加1。只要VCC是有效的,这个地址在操作之间就保持有效维护到部分。读取期间的地址滚转是从最后一页的最后一个字节到第一个字节内存第一页的字节。当前地址读取操作将根据内部数据字的位置输出数据地址计数器。这是由一个Start条件启动的,后面跟着一个有效的设备地址字节R/W位设置为逻辑' 1 '。设备将确认这个序列,当前地址数据字是串行的

在SDA线上打卡如果总线主机不这样做,所有类型的读操作都将被终止在第九个时钟周期内响应ACK(如果NACKs)。在NACK响应之后,主服务器可以发送一个停止条件来完成协议,或者发送一个开始条件来开始下一个协议序列。

随机读取:随机读取开始的方式与字节写入操作加载新数据字的方式相同地址。这就是所谓的“假写”序列;的数据字节和停止条件必须省略字节写入,以防止部件进入内部写入周期。一旦设备地址和字地址被写入并由EEPROM确认,总线主机必须生成另一个启动条件。总线主机现在通过发送Start来初始化当前地址读取条件,然后是一个有效的设备地址字节,R/W位设置为逻辑' 1 '。EEPROM将ACK设备地址和串行时钟在SDA线上的数据字。所有类型的读操作都会如果总线主机在第九个时钟周期内没有响应ACK(如果NACKs),则终止。在NACK响应后,主机可以发送一个停止条件来完成协议,也可以发送a开始下一个序列的Start条件。

当前地址读:顺序读由当前地址读或随机读发起。在总线主机之后接收到一个数据字,它以一个确认响应。只要EEPROM接收到ACK,它就会继续增加字地址并串行地时钟出顺序数据字。最大值是什么时候到达内存地址后,数据字地址将滚转,顺序读取将继续从内存数组的开始。如果总线主控,所有类型的读操作将被终止在第9个时钟周期内不响应ACK(如果NACKs)。在NACK响应之后master可以发送停止条件来完成协议,也可以发送开始条件来开始协议下一个序列。

对CubeMX生成的代码进行分析

可以看到CubeMX生成的代码中对I2C1进行了初始化

那么我们来看看他的初始化函数做了什么,以方便我们日后对I2C的使用和移植。

可以看见MX_I2C1_Init()对I2C进行了配置,并且返回到I2C_HandleTypeDef hi2c1这个句柄中。我们刨根问底,去看看这个句柄结构体是如何定义的。

可以看到I2C_HandleTypeDef hi2c1这个句柄的结构体的成员是这样定义的,在这里就不过多解释了,前面的文章具体的讲了如何分析句柄结构体成员的方法和例子,在这里也是大同小异的。我们再回来看看MX_I2C1_Init()是不是按照I2C_HandleTypeDef hi2c1这个句柄的结构体的成员来定义的,结果显然肯定是的!!

我们着重看一下I2C_InitTypeDef是如何配置I2C的

先去看I2C_InitTypeDef的结构体

那么就可以解释MX_I2C1_Init()是如何配置I2C的啦。由此也可以看出软件给主机地址赋予了0x00的地址。

如果还有哪些地方不懂的话,就要去看HOW TO USE THIS DRIVER,在此也不过多介绍了,大家可以结合CubeMX生成的代码去深入了解。

在下面也可以看到I2C对GPIO的设置,是在HAL_I2C_MspInit()内实现的。

在此也不多赘述了。

基本读写EEPROM(模拟i2c)工程实践

接下来我们就来完成模拟I2C的实现吧。主要功能是读写板载的 I2C EEPROM 芯片。需要完成的功能:

1:串口输出基本文字

2:I2C数据写入E2PROM

3:用I2C读取写入E2PROM的数据

4:测试写入和读取的数据一不一致,一致绿灯,错误红灯

5:用串口打印输出I2C读取的数据

串口输出和打印我们就不多赘述了,大家可以看看我上一篇关于通讯和USART的文章。

我们着重讲一下I2C数据写入E2PROM和用I2C读取写入E2PROM的数据

I2C数据写入E2PROM

首先需要定义

#define  DATA_Size                   256

uint8_t I2c_Buf_Write[DATA_Size];

uint8_t I2c_Buf_Read[DATA_Size];

DATA_Size是传入数据的数量,

Buf_Write[DATA_Size]是写入缓冲区

Buf_Read[DATA_Size]是读取缓冲区

为什么要引入缓冲区的概念呢??

高速设备与低速设备的不匹配,势必会让高速设备花时间等待低速设备,我们可以在这两者之间设立一个缓冲区。

缓冲区的作用:

1.可以解除两者的制约关系,数据可以直接送往缓冲区,高速设备不用再等待低速设备,提高了计算机的效率。例如:我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的CPU可以处理别的事情。

2.可以减少数据的读写次数,如果每次数据只传输一点数据,就需要传送很多次,这样会浪费很多时间,因为开始读写与终止读写所需要的时间很长,如果将数据送往缓冲区,待缓冲区满后再进行传送会大大减少读写次数,这样就可以节省很多时间。例如:我们想将数据写入到磁盘中,不是立马将数据写到磁盘中,而是先输入缓冲区中,当缓冲区满了以后,再将数据写入到磁盘中,这样就可以减少磁盘的读写次数,不然磁盘很容易坏掉。

简单来说,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来存储数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放出CPU,使其能够高效率工作。

E2PROM的换页问题

然后我们要完成将I2c_Buf_Write中顺序递增的数据写入EERPOM中这一个功能,就需要写一个特定的函数在i2c.c中,这一个函数I2C_EE_BufferWrite有3个参数(传入数据的缓冲区的指针,写入E2PROM的地址,数据大小的字节数)。

这部分代码很长,需要注意很多问题,由于E2PROM的换页问题,在这里简单的讲一下。如果写入的数据量大于一页的话,我们就需要换页处理,借助表格来说明一下。

下图是EEPROM的内存格式,为什么0,1,2…代表的是内存的地址,为什么每一页起始地址是0、8、16…呢?这是由AT24C01决定的,定义了EEPROM每一页有8个字节,但我们页需要在头文件定义一下。#define EEPROM_PAGESIZE        8。所以每一页起始地址都是0、8、16这些8的整数倍。

第一种情况就是写入数据存放在EEPROM的页起始地址

他们都是由每一页的起始地址开始存放。对于这种情况就好办了,我们只需要计算他的数据能写满多少页,剩下的就放下下一页。举个例子,我们需要在第一页的起始地址写入15个数据,那么我们就需要用15/ EEPROM_PAGESIZE,得到1.875,取整是1,那也就是写满了1页,取余就是7。那么程序先会将第一页填满.

然后剩下的7个数据再填到第二页去,也就是换页的操作。

再例如我们需要在第2页的起始地址写入9个数据,那填充好的结果是这样的。

填入的起始地址并不是页的起始地址

再考虑另外一个情况,也就是填入的起始地址并不是页的起始地址,就如下面,是从1或21或27开始写入数据。

那么这种情况就需要多一部的处理。

  1. 我们需要先将写入地址的那一页先填满
  2. 再重复上一个情况的操作

就例如填入的起始地址是10,写入24个数据,那么程序需要先将第2页填满,

然后在进行填入的起始地址并是页的起始地址的操作,继续存放。

这就是数据写入EERPOM需要注意的情况。

取整取余:定义NumOfPage为可以填满的整数页,NumOfSingle剩下的数据(取余)

NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;

NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;

大家可以结合注释看看下面的代码,也可以直接使用,在此就不多解释啦,还有不懂的地方可以私信问问我。

void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
{
  uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

  Addr = WriteAddr % EEPROM_PAGESIZE;
  count = EEPROM_PAGESIZE - Addr;
  NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;
  NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;
 
  /* If WriteAddr is I2C_PageSize aligned  */
  if(Addr == 0) 
  {
    /* If NumByteToWrite < I2C_PageSize */
    if(NumOfPage == 0) 
    {
      I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
    /* If NumByteToWrite > I2C_PageSize */
    else  
    {
      while(NumOfPage--)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE); 
        WriteAddr +=  EEPROM_PAGESIZE;
        pBuffer += EEPROM_PAGESIZE;
      }

      if(NumOfSingle!=0)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
      }
    }
  }
  /* If WriteAddr is not I2C_PageSize aligned  */
  else 
  {
    /* If NumByteToWrite < I2C_PageSize */
    if(NumOfPage== 0) 
    {
      I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
    /* If NumByteToWrite > I2C_PageSize */
    else
    {
      NumByteToWrite -= count;
      NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;
      NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;	
      
      if(count != 0)
      {  
        I2C_EE_PageWrite(pBuffer, WriteAddr, count);
        WriteAddr += count;
        pBuffer += count;
      } 
      
      while(NumOfPage--)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
        WriteAddr +=  EEPROM_PAGESIZE;
        pBuffer += EEPROM_PAGESIZE;  
      }
      if(NumOfSingle != 0)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
      }
    }
  }  
}

从中需要调用字节写入和页写入这两个函数,大家可以了解一下,如果不想了解也可以直接用,这里涉及到的问题太多,就不多说了。


uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)
{
	HAL_StatusTypeDef status = HAL_OK;

	status = HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDRESS, (uint16_t)WriteAddr, I2C_MEMADD_SIZE_8BIT, pBuffer, 1, 100); 

	/* Check the communication status */
	if(status != HAL_OK)
	{
	/* Execute user timeout callback */
	//I2Cx_Error(Addr);
	}
	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{

	}

	/* Check if the EEPROM is ready for a new operation */
	while (HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);

	/* Wait for the end of the transfer */
	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{
		
	}
	return status;
}


uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
{
	HAL_StatusTypeDef status = HAL_OK;
	/* Write EEPROM_PAGESIZE */
	status=HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDRESS,WriteAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(pBuffer),NumByteToWrite, 100);

	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{
		
	}

	/* Check if the EEPROM is ready for a new operation */
	while (HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);

	/* Wait for the end of the transfer */
	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{
		
	}
	return status;
}

用I2C读取写入E2PROM的数据

创建一个函数在i2C.c中,用I2C读取写入E2PROM的数据,I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr),他有三个参数(传入的缓冲区指针,读取的首地址,读取的数据大小)

相对于写入数据,读取数据就显得简单太多了,只需要调用HAL库的HAL_I2C_Mem_Read()就可以大致完成这个功能,需要详细了解这个HAL_I2C_Mem_Read(),就需要去到i2c的HOW TO USE THIS DRIVER中取阅读HAL库的驱动使用方法。

uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)
{
	HAL_StatusTypeDef status = HAL_OK;
	
	status=HAL_I2C_Mem_Read(&hi2c1,EEPROM_ADDRESS,ReadAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t *)pBuffer, NumByteToRead,1000);

	return status;
}

我们看看HOW TO USE THIS DRIVER是如何驱动这个函数的。

HAL_I2C_Mem_Read()需要传入七个参数,(I2C的句柄,EEPROM从设备的地址,从EEPROM哪里读数据,EEPROM内部地址的大小,数据存放缓冲区的指针,要读取多少个数据,超时时间)

还需要比较写入缓冲区的数据和写出缓冲区的数据一不一致,如果不一致就显示错误。

如果一样的话,那就通过串口打印输出,并且亮绿灯,如果错误,那么就量红灯。

if(I2C_Test() ==1)

  {

      LED_GREEN;

  }

  else

  {

      LED_RED;

  }

最后我们将全部功能合成在main.c的I2C_Test()中

#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "usart.h"
#include "gpio.h"
#include "bsp_led.h"


#define  DATA_Size			256
#define  EEP_Firstpage      0x00
uint8_t I2c_Buf_Write[DATA_Size];
uint8_t I2c_Buf_Read[DATA_Size];
uint8_t I2C_Test(void);

void SystemClock_Config(void);

int main(void)
{
  HAL_Init();
 SystemClock_Config();

  MX_GPIO_Init();
  MX_I2C1_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  LED_GPIO_Config();
  /* 串口输出基本字符  */
  printf("\r\n 我是Super·Ultraman,这是一个 STM32 F103 开发板。\r\n");		 
  printf("\r\n 这是一个I2C外设(AT24C02)读写测试例程 \r\n");

  /* 正确绿灯亮,错误红灯亮  */
if(I2C_Test() ==1)
  {
      LED_GREEN;
  }
  else
  {
      LED_RED;
  }
  
 while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}


  /* 实现I2C写入和读取  */
uint8_t I2C_Test(void)
{
	uint16_t i;

	 printf("\r\n"写入的数据"\r\n") ;

	for ( i=0; i<DATA_Size; i++ ) //填充缓冲
	{   
		I2c_Buf_Write[i] =i;
		printf("0x%02X ", I2c_Buf_Write[i]);
		if(i%16 == 15)    
		printf("\n\r");    
	}

	//将I2c_Buf_Write中顺序递增的数据写入EERPOM中 
	I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, DATA_Size);

    printf("\r\n"读出的数据"\r\n") ;
	//将EEPROM读出数据顺序保持到I2c_Buf_Read中
	I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, DATA_Size); 
	//将I2c_Buf_Read中的数据通过串口打印
	for (i=0; i<DATA_Size; i++)
	{	
		if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
		{
			printf("0x%02X ", I2c_Buf_Read[i]);
			EEPROM_ERROR("错误:I2C EEPROM写入与读出的数据不一致");
			return 0;
		}
		printf("0x%02X ", I2c_Buf_Read[i]);
		if(i%16 == 15)    
		printf("\n\r");

	}
    
    printf("\r\n"I2C(AT24C02)读写测试成功"\r\n") ;
	return 1;
}
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

 I2C.c中的代码如下:

#include "i2c.h"

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

I2C_HandleTypeDef hi2c1;
DMA_HandleTypeDef hdma_i2c1_rx;

/* I2C1 init function */
void MX_I2C1_Init(void)
{

  /* USER CODE BEGIN I2C1_Init 0 */

  /* USER CODE END I2C1_Init 0 */

  /* USER CODE BEGIN I2C1_Init 1 */

  /* USER CODE END I2C1_Init 1 */
  hi2c1.Instance = I2C1;
  hi2c1.Init.ClockSpeed = 400000;
  hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
  hi2c1.Init.OwnAddress1 = 0;
  hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
  hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
  hi2c1.Init.OwnAddress2 = 0;
  hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
  hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
  if (HAL_I2C_Init(&hi2c1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN I2C1_Init 2 */

  /* USER CODE END I2C1_Init 2 */

}

void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(i2cHandle->Instance==I2C1)
  {
  /* USER CODE BEGIN I2C1_MspInit 0 */

  /* USER CODE END I2C1_MspInit 0 */

    __HAL_RCC_GPIOB_CLK_ENABLE();
    /**I2C1 GPIO Configuration
    PB6     ------> I2C1_SCL
    PB7     ------> I2C1_SDA
    */
    GPIO_InitStruct.Pin = GPIO_PIN_6|GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* I2C1 clock enable */
    __HAL_RCC_I2C1_CLK_ENABLE();

    /* I2C1 DMA Init */
    /* I2C1_RX Init */
    hdma_i2c1_rx.Instance = DMA1_Channel7;
    hdma_i2c1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_i2c1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_i2c1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_i2c1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_i2c1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_i2c1_rx.Init.Mode = DMA_NORMAL;
    hdma_i2c1_rx.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_i2c1_rx) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(i2cHandle,hdmarx,hdma_i2c1_rx);

    /* I2C1 interrupt Init */
    HAL_NVIC_SetPriority(I2C1_EV_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
    HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
  /* USER CODE BEGIN I2C1_MspInit 1 */

  /* USER CODE END I2C1_MspInit 1 */
  }
}

void HAL_I2C_MspDeInit(I2C_HandleTypeDef* i2cHandle)
{

  if(i2cHandle->Instance==I2C1)
  {
  /* USER CODE BEGIN I2C1_MspDeInit 0 */

  /* USER CODE END I2C1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_I2C1_CLK_DISABLE();

    /**I2C1 GPIO Configuration
    PB6     ------> I2C1_SCL
    PB7     ------> I2C1_SDA
    */
    HAL_GPIO_DeInit(GPIOB, GPIO_PIN_6);

    HAL_GPIO_DeInit(GPIOB, GPIO_PIN_7);

    /* I2C1 DMA DeInit */
    HAL_DMA_DeInit(i2cHandle->hdmarx);

    /* I2C1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(I2C1_EV_IRQn);
    HAL_NVIC_DisableIRQ(I2C1_ER_IRQn);
  /* USER CODE BEGIN I2C1_MspDeInit 1 */

  /* USER CODE END I2C1_MspDeInit 1 */
  }
}

/* USER CODE BEGIN 1 */
void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint16_t NumByteToWrite)
{
  uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;

  Addr = WriteAddr % EEPROM_PAGESIZE;
  count = EEPROM_PAGESIZE - Addr;
  NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;
  NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;
 
  /* If WriteAddr is I2C_PageSize aligned  */
  if(Addr == 0) 
  {
    /* If NumByteToWrite < I2C_PageSize */
    if(NumOfPage == 0) 
    {
      I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
    /* If NumByteToWrite > I2C_PageSize */
    else  
    {
      while(NumOfPage--)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE); 
        WriteAddr +=  EEPROM_PAGESIZE;
        pBuffer += EEPROM_PAGESIZE;
      }

      if(NumOfSingle!=0)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
      }
    }
  }
  /* If WriteAddr is not I2C_PageSize aligned  */
  else 
  {
    /* If NumByteToWrite < I2C_PageSize */
    if(NumOfPage== 0) 
    {
      I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
    /* If NumByteToWrite > I2C_PageSize */
    else
    {
      NumByteToWrite -= count;
      NumOfPage =  NumByteToWrite / EEPROM_PAGESIZE;
      NumOfSingle = NumByteToWrite % EEPROM_PAGESIZE;	
      
      if(count != 0)
      {  
        I2C_EE_PageWrite(pBuffer, WriteAddr, count);
        WriteAddr += count;
        pBuffer += count;
      } 
      
      while(NumOfPage--)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, EEPROM_PAGESIZE);
        WriteAddr +=  EEPROM_PAGESIZE;
        pBuffer += EEPROM_PAGESIZE;  
      }
      if(NumOfSingle != 0)
      {
        I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 
      }
    }
  }  
}

uint32_t I2C_EE_ByteWrite(uint8_t* pBuffer, uint8_t WriteAddr)
{
	HAL_StatusTypeDef status = HAL_OK;

	status = HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDRESS, (uint16_t)WriteAddr, I2C_MEMADD_SIZE_8BIT, pBuffer, 1, 100); 

	/* Check the communication status */
	if(status != HAL_OK)
	{
	/* Execute user timeout callback */
	//I2Cx_Error(Addr);
	}
	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{
		
	}

	/* Check if the EEPROM is ready for a new operation */
	while (HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);

	/* Wait for the end of the transfer */
	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{
		
	}
	return status;
}


uint32_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, uint8_t NumByteToWrite)
{
	HAL_StatusTypeDef status = HAL_OK;
	/* Write EEPROM_PAGESIZE */
	status=HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDRESS,WriteAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t*)(pBuffer),NumByteToWrite, 100);

	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{
		
	}

	/* Check if the EEPROM is ready for a new operation */
	while (HAL_I2C_IsDeviceReady(&hi2c1, EEPROM_ADDRESS, EEPROM_MAX_TRIALS, I2Cx_TIMEOUT_MAX) == HAL_TIMEOUT);

	/* Wait for the end of the transfer */
	while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY)
	{
		
	}
	return status;
}

uint32_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, uint16_t NumByteToRead)
{
	HAL_StatusTypeDef status = HAL_OK;
	
	status=HAL_I2C_Mem_Read(&hi2c1,EEPROM_ADDRESS,ReadAddr, I2C_MEMADD_SIZE_8BIT, (uint8_t *)pBuffer, NumByteToRead,1000);

	return status;
}

/* USER CODE END 1 */

串口usart.c的内容和配置如下:

#include "usart.h"

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

UART_HandleTypeDef huart1;

/* USART1 init function */

void MX_USART1_UART_Init(void)
{

  /* USER CODE BEGIN USART1_Init 0 */

  /* USER CODE END USART1_Init 0 */

  /* USER CODE BEGIN USART1_Init 1 */

  /* USER CODE END USART1_Init 1 */
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN USART1_Init 2 */

  /* USER CODE END USART1_Init 2 */

}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* USART1 clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspInit 1 */

  /* USER CODE END USART1_MspInit 1 */
  }
}

void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{

  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspDeInit 0 */

  /* USER CODE END USART1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_USART1_CLK_DISABLE();

    /**USART1 GPIO Configuration
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX
    */
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);

    /* USART1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspDeInit 1 */

  /* USER CODE END USART1_MspDeInit 1 */
  }
}

/* USER CODE BEGIN 1 */
/*****************  发送字符串 **********************/
void Usart_SendString(uint8_t *str)
{
	unsigned int k=0;
  do 
  {
      HAL_UART_Transmit(&huart1,(uint8_t *)(str + k) ,1,1000);
      k++;
  } while(*(str + k)!='\0');
  
}
//重定向c库函数printf到串口huart1,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
	/* 发送一个字节数据到串口DEBUG_USART */
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);	
	
	return (ch);
}

//重定向c库函数scanf到串口DEBUG_USART,重写向后可使用scanf、getchar等函数
int fgetc(FILE *f)
{		 
	int ch;
	HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 1000);	
	return (ch);
}

 将程序烧如开发板,利用串口调试助手,查看串口打印的信息。

可以看到串口打印出来的数据与我们写入的数据一模一样,开发板亮绿灯,

工程实践成功!!!!

本文就到这里啦,希望大家通过本文可以对I2C有深入的了解,毕竟知道外设原理和代码的实现才能更好的去使用外设,如果有错大家可以指出哟,感兴趣或者有不明白的地方的小伙伴可以私信问我!!如果喜欢可以点赞+关注哟!!!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐