您现在的位置是:首页 >其他 >STM32F4_I2C(从机EEPROM/MPU-6050)协议详解网站首页其他

STM32F4_I2C(从机EEPROM/MPU-6050)协议详解

light_2025 2024-06-29 00:01:02
简介STM32F4_I2C(从机EEPROM/MPU-6050)协议详解

目录

1. I2C是什么

2. I2C物理层介绍

3. I2C协议层介绍

3.1 I2C基本读写过程

3.1.1 通讯复合格式

3.2 通讯的起始和停止信号

3.3 数据有效性

3.4 地址及数据方向

3.5 响应

4. STM32的I2C特性及架构

4.1 I2C架构剖析

5. I2C通讯过程

5.1 主发送器

5.2 主接收器

6. I2C库函数

7. EEPROM简介

8. 初始化I2C

9. MPU-6050简介

10. 软件读写MPU-6050

10.1 I2C时序基本单元

10.2 实验程序

10.2.1 main.c

10.2.2 I2C.c

10.2.3 I2C.h

10.2.4 MPU6050.c

10.2.5 MPU6050.h

10.2.6 MPU6050_Reg.h

11. 硬件读写MPU-6050

11.1 MPU6050.c

11.2 MPU6050.h

12. I2C与AT24C02通信

12.1 实验程序

12.1.1 main.c

12.1.2 MyI2C.c

12.1.3 MyI2C.h

12.1.4 AT24C02.c

12.1.5 AT24C02.h


1. I2C是什么

首先从大的方向上明确I2C协议到底是干嘛的,进而理解什么是I2C?

        我们已经学习过串口通讯USART,串口通信就是通过USART_TX和USART_RX引脚和外部芯片通信,当然也可以通过CH340转电平芯片实现和电脑进行通信。I2C就类似与USART,也是一种通信方式,一般用于实现芯片与芯片(外设与外设)之间的通信,只需要通过SCL和SDA两条总线即可实现通信,这两条总线类似于TX和RX,但并不是说所有通信方式都只需要两根线。

I2C(内部集成电路)总线接口用作微控制器(主机)和I2C串行总线之间的接口。提供多主模式功能,可以控制所有I2C总线特定的序列、协议、仲裁和时序。

I2C只需要两根线即可在连接于总线上的设备之间传送信息。主设备用于启动总线传送数据。此时任何被寻址的设备均为从设备。总线上主设备和从设备不是固定的,而是根据收发双方数据传送的方向规定的。 当主设备发送数据给从设备,那么主设备首先要寻址从设备,从设备此时接收主设备发送的数据,主设备停止发送时,通讯结束。如果是从设备发送数据给主设备,那么则需要从设备寻址主设备,主设备负责接收从设备发送的数据,从设备停止发送,通讯结束。

I2C通信协议的英文全称是:Inter-Integrate Circuit,I2C是由菲利普(Phiilps)公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通讯协议的外部收发设备,被广泛地使用在系统内多个集成电路IC间通讯。

2. I2C物理层介绍

物理层就是最基本的原理图这些,I2C接线最基本的特性。

I2C是一个支持多设备的总线。总线的意思就是多个设备共用的信号线。比如说MCU主机芯片与EEPROM进行通信,那么主机和从机只需要引出两个引脚接在SCL和SDA总线上,即可实现I2C协议通信。该协议简单就简单在不像USART和CAN通信,串口通信还需要CH340进行转电平处理,I2C不需要外围芯片参与。

正如上图中所示,一个I2C通信总线中,可以连接多个I2C通讯设备(触摸屏、传感器、EEPROM),支持多个通讯主机及多个通讯从机。既然可以实现多个主机和从机的通信,那么一定有具体的规则规定哪个主机和哪个从机进行通信(也就是说假设主机想要和EEPROM通信,怎么确定主机可以在多个从机中找到EEPROM从机?)

每个连接到总线的设备都有一个独立的地址(该地址是提前规定好的,I2C协议规定寻址的话地址可以是7位或者10位的,也就是说如果MCU地址设置是7位,采用寻址方法和EEPROM进行通信,那么EEPROM只能设置为7位,否则是无法寻址到EEPROM的,也就无法使双方进行通信),主机可以利用这个地址进行不同设备之间的访问。进而确定通信双方。

SCL总线:串行时钟线。任何外设通信都需要时钟的参与,I2C也不例外,SCL总线的作用是为了保持通讯双方收发一致,一方发送数据,另一方在收到发送标志后,能到在约定时间内收到数据。

SDA总线:串行数据线。串行数据线很明显是用来表示双方通信的数据。

串行数据总线SDA和串行时钟总线SCL一般都需要接上拉电阻外接电源。上拉电阻的阻值一般设置为4.7K欧。

当I2C设备空闲时,会输出高阻态。意思就是:假设现在主机不需要和EEPROM通信,EEPROM就会处于空闲,可以理解为EEPROM连接的两条总线断开,输出高阻态可以假想为一个很大的电阻,设备断路,那么主机就不会受到EEPROM电压的影响。

当所有设备都空闲时,都输出高阻态,上拉电阻会把总线拉成高电平。意思就是:所有设备都处于空闲状态,也就是所有设备都和总线断开,那么所有设备的电压变化都不会影响到主机,那么主机通过上拉电阻连接的电阻是多少V就会使主机变成多少v,假设是3.3V,那么主机就会呈现3.3V,也就是高电平。

多个主机同时使用总线时,为了防止多个主机之间的数据进行冲突,会利用仲裁的方式决定哪个设备占用总线。(类似于中断中NVIC中断优先级,DMA直接存储器中的仲裁器)

I2C具有三种传输模式

  •         标准模式传输效率为100kbit/s
  •         快速模式为400kbit/s
  •         高速模式下可达3.4Mbit/s  (但需要注意:目前大多数I2C设备尚且不支持高速模式)

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

3. I2C协议层介绍

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

3.1 I2C基本读写过程

主机写数据到从机: 

S:起始信号

SLAVE ADDRESS:从机地址---7位;(主机根据从机地址去选择与哪个从机进行通信)

R/W:主从写方向; 当将该位设置为0时,表示主机写数据到从机;当该位写1时,表示从机写数据到主机。

以上都是数据从主机传输至从机;也就是上图中阴影部分;

A:应答(ACK)信号;由上图主机写数据到从机,并且设置好写方向;接下来从机进行应答,表示此处通讯的从机可以使用;(就像老师问问题,学生收到问题后给出应答一样)

当从机发送应答 A 给到主机后,主机就会传输数据 DATA 到从机。

传输完一个字节后,从机会在此应答给到主机。(实际上,每当主机传完一个字节后,从机都会发送一次应答)

A/A杠:应答(ACK)或非应答(NACK)信号。从机在接收完上一次主机发送的一位字节后,从机发送非应答(NACK),表示不再让主机发送字节了。也可以认为是从机想要终止传输信号。

P:数据传输的结束。P对应于S,既然有传输的起始信号,那么必然有数据传输的终止信号。

主机由从机中读数据:

既然是由从机中读数据,那么比方说是:主机和EEPROM进行通信,此时的读方向就是从EEPROM到主机。

S:起始信号。也可以说是传输开始的信号

SLAVE ADDRESS:从机地址

R/W:读写方向位。因为此时是由从机中读数据,所以该位须设置为1

以上不变的是起始信号、从机地址、读写方向位始终是由主机来发送数据的

A:应答(ACK)。从机发送应答。

DATA:数据位;不同于写的是,因为此时是由从机读数据,所以数据位在读的从机,然后由从机读一个字节,主机应答一次,这么依次循环,直到主机非应答(NACK)信号的来临。非应答信号就是不再进行数据传输了。

P:数据传输停止位

3.1.1 通讯复合格式

通讯复合格式不同于单纯的读写过程的地方在于:

通讯复合格式具有两次S,也就是两次传输开始的信号。

整个过程是这样的首先还是主机发送数据传输开始的信号,然后发送从机地址SLAVE_ADDRESS。这里不同的是,发送从机的地址一般是从机中某个确切位置的地址,比方说主机和EEPROM进行通信,从机EEPROM中的内存是很大的,分很多不同的地址,第一次传输的地址只是为了告诉从机,主机想和从机的哪块地址进行通讯。

接下来主机发送读写信号,一般复合格式下,第一次传输都是写信号,也就是将R/W位置0;然后从机进行响应,等待非应答来临,也可以说是从机已经明确了主机的信号,知道了主机想和我的哪块地址进行通讯。

接下来进行第二次传输开始的信号,这一次主机发送的地址SLAVE_ADDRESS是从机地址,和第一次传输的地址一般是一样的,R/W位设置为1,之所以设置为1,是因为第一次主机已经告诉了从机想要和从机的哪块地址进行通信,那么第二次显然是需要由从机中读数据,那么R/W位就需要设置为1,然后数据由从机一位一位的传输至主机,直到非应答信号的来临。

P:结束传输。

3.2 通讯的起始和停止信号

通过对51单片机的学习,我们知道通讯的起始和停止都是通过时序图的高低电位来设置的

当串行时钟总线SCL是高电平时,串行数据总线SDA从高电平向低电平切换时,表示通讯的开始。也就是起始信号

当串行时钟总线SCL是高电平时,串行数据总线SDA从低电平向高电平切换时,表示通讯的停止。也就是停止信号

注意:起始信号和停止信号一般由主机产生

3.3 数据有效性

I2C使用数据总线SDA来传输数据,使用时钟总线SCL来进行数据同步。SDA在SCL的每一个时钟周期内传输一位数据。

SCL为高电平时,传输的数据SDA有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。

当SCL为低电平时,SDA的数据无效,一般这个时候SDA在进行电平切换(也就是图中的交叉处),电平切换是为下一次表示数据做好准备。

3.4 地址及数据方向

I2C总线上每个设备都有自己独立的地址,当主机发起通讯时,主机会通过寻址的方式找到从机的地址,具体是通过SDA信号线发送设备地址(SLAVE_ADDRESS)来查找从机。设备地址可以是7位或是10位。

紧跟设备地址的一个数据位 R/W杠 是用来表示数据传输方向的,也就是主机向从机写数据,或者由从机中读数据。具体的数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机中写数据。

这也就解释了为什么设备地址可以是7位,我们通常使用的寄存器最低也要是8位,因为只用到了从机地址的高7位,最低位也就是0位是R/W,读写数据方向位。

图中MSB表示高位,LSB表示低位。

3.5 响应

I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。

        数据发送端控制SDA数据接收端控制SDA是正好相反的。传输时主机产生时钟,SLAVE_ADDRESS+R/W一般是8位,也就是需要8个SCL时钟周期。在第九个时钟时,数据发送端会释放SDA的控制权,具体是通过将时序设置为高电平,也就是我们前面介绍的高阻态,高阻态下表示从机和总线断开,从机的电压变化不再影响主机。也就是非应答信号。相应的,低电平表示应答信号。

4. STM32的I2C特性及架构

软件模拟协议:所谓软件模拟协议就是像我们之前提及的通过控制时序图产生高低电平就可以产生符合通讯协议标准的逻辑,精确的说是使用CPU直接控制通讯引脚的电平。

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

STM32的I2C外设可用作通讯的主机及从机,支持100Kbit/s和400Kbit/s的速率,支持7位、10位设备地址,支持DMA数据传输,并且具有数据效验功能

4.1 I2C架构剖析

5. I2C通讯过程

使用I2C外设进行通讯时,在通讯的不同阶段它会对“状态寄存器“(SR1及SR2)”的不同数据位写入参数,通过读取这些寄存器标志来了解通讯状态

5.1 主发送器

所谓主发送器就是I2C作为主机对外发送数据的意思。

整个发送器的通讯过程可以分为上下两部分,上部分很明显就是I2C协议读写的过程,下部分可以理解为状态寄存器在主机发送命令之后所产生的状态位我们知道STM32的通讯速率是非常快的,可达72M,如果没有状态位的响应,那么就无法保证较快的通讯速率。

首先S起始位,发送通讯开始命令,状态寄存器SR就会产生EV5事件,置位0 SB=1;表示起始位已经发送

然后主机发送地址和应答,状态寄存器产生EV6和EV8事件,分别置ADDR=1和TxE=1;表示地址已经发送结束,并且数据寄存器为空(因为I2C是将数据寄存器复制到数据移位寄存器中,所以一旦将数据一位一位的移到数据移位寄存器后,就会将数据寄存器置空,表示可以发送下一次数据了)

然后依次发送数据、应答、发送数据、应答,直到非应答命令的到来,状态寄存器会产生EV8_2事件。表示数据字节传送成功

最后控制寄存器发送结束命令P。

5.2 主接收器

主接收器和主发送器本质上的意义是相同的,只不过主发送器表示从主机写数据到从机,而主接收器表示由从机读数据到主机一个是写数据,一个是读数据

主接收器同主发送器一样,依然是主机产生起始位S,然后状态寄存器SR产生EV5、EV6、EV7、EV7_1事件,响应控制寄存器产生的状态位,其目的都是为了加强通讯双方的效率。

起始信号S是由主机端产生的(这里需要说明I2C协议规定,不管是发送数据还是接受数据起始和停止信号都是主机产生的),控制发生起始信号后,产生事件EV5,并会对SR1寄存器的SB位置1,表示起始信号已经发送;

发送设备地址并等待应答信号,若有从机应答,而产生事件EV6,这时SR1寄存器的ADDR位被置1,表示地址已经发送。

后续的过程和发送器相同。 

6. I2C库函数

I2C初始化结构体:

typedef struct
{
    uint32_t I2C_ClockSpeed;  //设置I2C时钟频率,此值要低于40 0000
    uint16_t I2C_Mode;        // 指定工作模式,可选为I2C模式和SMBUS模式
    uint16_t I2C_DutyCycle;    //指定时钟占空比,可选为low/high=2:1及16:9模式
    uint16_t I2C_OwnAddress1;   //指定自身I2C设备地址
    uint16_t I2C_Ack;           //使能或关闭应答(一般来说都是要使能的)
    uint16_t I2C_AcknowledgedAddress;  //指定地址长度,可为7位及10位
}I2C_InitTypeDef;

①:I2C_ClockSpeed

设置I2C传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到 I2C 的时钟控制寄存器CCR。我们写入的这个参数值不得高于400KHz

②:I2C_Mode

选择I2C的使用方式。可供选择的方式有I2C模式(I2C_Mode_I2C)和SMBus主、从模式(I2C_Mode_SMBusHost、I2C_Mode_SMBusDevice)。

③:I2C_DutyCycle

设置I2C的SCL线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2:1(I2C_DutyCycle_2)和16:9(I2C_DutyCycle_16_9)

④:I2C_OwnAddress1

配置STM32F4的I2C设备自己的地址,每个连接到I2C总线上的设备都要有一个自己的地址,作为主机当然也不例外,在上面讲过的读写程序中,开启读写命令S后,就需要写入从机地址进行应答。地址可以设置为7位或者10位(受下面I2C_AcknowledgeAddress成员决定),只要该地址是I2C总线上唯一的即可。

⑤:I2C_Ack

配置I2C应答是否使能,设置为使能则可以发送响应信号。一般情况下配置为允许应答(I2C_Ack_Enable),这是绝大多数遵循I2C标准的设备的通讯要求,倘若要使用I2C,可还是将该位设置为I2C_Ack_Disable往往会导致通讯错误。

⑥:I2C_AcknowledgedAddress

选择I2C的寻址模式是7位还是10位地址。这需要根据I2C实际连接的从机设备的地址进行选择,比方说,I2C要与EEPROM通讯,I2C设置地址为7位,那么EEPROM地址也要是7位。这个成员的配置也影响到I2C_OwnAddress1成员,只有这里设置成10位模式时,I2C_OwnAddress1才支持10位地址。

7. EEPROM简介

EEPROM全称是:electrically erasable and programmable read-only memory  (电擦除的只读存储器

EEPROM引脚6和7连接总线SCL和SDA,直接和芯片引脚相接,接上拉电阻4.7K,外接电源3.3V。

24C02是芯片的型号,全称是AT24C02芯片。

封装:

A0-A2:地址输入

SCL:串行时钟线

SDA:串行数据线

WP:写保护(上图原理图中WP是直接接地的,也就是低电平,没有开启写保护,如果将其置为高电平,就意味着开启了写保护,就不能再对EEPROM进行写数据)

前四个位是固定的,1010 A2 A1 A0需要根据引脚来配置,R/W用来设置写保护。 

8. 初始化I2C

  •         初始化I2C相关的GPIO
  •         配置I2C外设的工作模式
  •         编写I2C写入EEPROM的Byte write函数
  •         编写I2C读取EEPROM和Random Read函数
  •         使用read函数及write函数进行读写校验
  •         编写page write函数及write函数进行读写校验

注意:Byte write以及page write都是AT24C02芯片中的读写过程函数。

9. MPU-6050简介

        MPU-60X0是全球首例 9 轴运动处理传感器。它集成了3轴MEMS陀螺仪,3轴MEMS加速度计,以及一个可拓展的数字运动处理器DMP(Digital Motion Processor),可用I2C接口连接一个第三方的数字传感器,比如磁力计。扩展之后就可以通过其I2C或SPI接口输出一个9轴的信号(SPI接口仅在MPU-6000可用)。MPU-60X0也可以通过其I2C接口连接非惯性的数字传感器,比如压力传感器。

MPU-6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度等参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景。

3轴加速度计:测量X、Y、Z轴的加速度

3轴陀螺仪传感器:测量X、Y、Z轴的角速度

既然说了MPU-6050可以用来测量加速度和角速度这些模拟量,那么必定有ADC参与其中,进行模拟转数字的操作。

I2C从机地址:1101000(AD0=0)

                        1101001(AD0=1)

MPU-6050也可以通过INT引脚产生中断。

10. 软件读写MPU-6050

10.1 I2C时序基本单元

起始条件:

SCL高电平期间,SDA从高电平切换到低电平

终止条件:

SCL高电平期间,SDA从低电平切换到高电平

发送一个字节:

SCL低电平期间,主机将数据依次放到SDA上(高位先行),然后释放SCL,从机将SCL在高电平期间读取数据位,SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。

接收一个字节:

SCL低电平期间,从机将数据依次放到SDA上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)

发送应答:

主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。

接收应答:

主机在发送完一个字节后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

10.2 实验程序

10.2.1 main.c

#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "usmart.h"
#include "I2C.h"
#include "MPU6050.h"

//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
	LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
	led_set(sta);
}
uint8_t ID;
int16_t AX,AY,AZ,GX,GY,GZ;

int main(void)
{
	delay_init(168);
	uart_init(115200);
	LCD_Init();
	MPU6050_Init();

	while(1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
		POINT_COLOR=RED;
		LCD_ShowxNum(30,50,AX,5,16,0);
		LCD_ShowxNum(30,70,AY,5,16,0);
		LCD_ShowxNum(30,90,AZ,5,16,0);
		LCD_ShowxNum(30,110,GX,5,16,0);
		LCD_ShowxNum(30,130,GY,5,16,0);
		LCD_ShowxNum(30,150,GZ,5,16,0);
	}
}


10.2.2 I2C.c

#include "stm32f4xx.h" 
#include "I2C.h"
#include "delay.h"

#define SCL GPIO_Pin_8
#define SDA GPIO_Pin_9

void MyI2C_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8|GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_OType=GPIO_OType_OD;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_8|GPIO_Pin_9);//初始化引脚设置为高电平,释放引脚,I2C处于空闲状态
}
void MyI2C_Start(void)
{
    //SCL高电平期间,SDA从高电平切换到低电平
	GPIO_SetBits(GPIOB,SCL|SDA);//引脚置高电平
	delay_ms(10);
	GPIO_ResetBits(GPIOB,SDA);//SDA置低电平
	delay_ms(10);
	GPIO_ResetBits(GPIOB,SCL);//SCL置低电平
	delay_ms(10);
}
void MyI2C_Stop(void)
{
	GPIO_ResetBits(GPIOB,SDA); //SDA低电平
	delay_ms(10);
	GPIO_SetBits(GPIOB,SCL);  //SCL高电平期间,SDA 由低电平变为高电平表示终止信号
	delay_ms(10);
	GPIO_SetBits(GPIOB,SDA); //SDA 高电平
	delay_ms(10);
}
void MyI2C_SendByte(uint8_t Byte)//发送一个字节 ,高位在前
{
	uint8_t i;
	for(i=0;i<8;i++)
	{
        //SCL低电平期间,主机依次将数据放到串行数据总线SDA上,这里之所以没有在程序里首先将SCL置
        //为低电平是因为开始信号(MyI2C_Start)的程序中,最后将SCL置为了低电平,                            
        //GPIO_ResetBits(GPIOB,SCL); 这句程序的目的就是:开始信号发出后,收到应答紧接着就会发    
        //送数据;
		GPIO_WriteBit(GPIOB,SDA,Byte&(0x80>>i));//写字节将数据写到SDA串行数据总线
		delay_ms(10);
        //将数据写入到串行数据总线上以后,释放SCL,从机会在SCL高电平期间读取数据,SCL高电平期间SDA不允许有数据变化
		GPIO_SetBits(GPIOB,SCL);
		delay_ms(10);
		GPIO_ResetBits(GPIOB,SCL);//SCL由高变低表示发送完成一位
		delay_ms(10);
        //通过for循环重复上述过程8次即可发送完成一个字节
	}
}
uint8_t MyI2C_ReceiveByte(void)//接收一个字节
{//首先需要明确,接收字节一定是数据寄存器中有数据,主机才可以接收数据
	GPIO_SetBits(GPIOB,SDA);//SDA设置为高电平是为了读取数据时判断的
	delay_ms(10);
	uint8_t i,Byte=0x00;
	for(i=0;i<8;i++)
	{
        //从发送字节程序出来以后,SCL是低电平;(这种情况意味着是主机先向从机写数据,主机再向从机读数据)(又或者是I2C读写方向直接就是主机向从机读数据,此时这种情况是开始信号S发出后,从机就从 从机读取数据,此时SCL也是低电平,因为开始程序中最后是将SCL置为低电平的)
        //通过上述分析,程序来到这里,SCL一定是低电平
        //SCL低电平期间,从机将数据依次放到SDA上,高位先行
		GPIO_SetBits(GPIOB,SCL); //释放SCL,主机在SCL高电平期间读取数据位
		delay_ms(10);
		if(GPIO_ReadInputDataBit(GPIOB,SDA)==1){Byte|=(0x80>>i);}//读取SDA数据位,因为每一次进入MyI2C_ReceiveByte时都先将SDA置为了高电流,所以条件语句成立,通过或运算将8位依次赋值给变量Byte
		delay_ms(10);
		GPIO_ResetBits(GPIOB,SCL); //SCL置低电平,为传输下一位做准备
		delay_ms(10);
        //重复上述操作8次,即可完整接收一个字节
	}
	return Byte;
}
void MyI2C_SendAck(uint8_t AckBit)//发送一个应答
{
	GPIO_WriteBit(GPIOB,SDA,AckBit);//往串行数据总线SDA最低位写应答,0表示应答,1表示非应答
	delay_ms(10);
	GPIO_SetBits(GPIOB,SCL);//SCL由高电平变为低电平
	delay_ms(10);
	GPIO_ResetBits(GPIOB,SCL);
	delay_ms(10);
}
uint8_t MyI2C_ReceiveAck(void)//接收一个应答
{
	uint8_t AckBit;
	GPIO_SetBits(GPIOB,SDA);
	delay_ms(10);
	GPIO_SetBits(GPIOB,SCL);
	delay_ms(10);
	AckBit=GPIO_ReadInputDataBit(GPIOB,SDA); //读取数据总线SDA的数据位
	delay_ms(10);
	GPIO_ResetBits(GPIOB,SCL); //SCL由高电平变为低电平
	delay_ms(10);
	return AckBit;
}


10.2.3 I2C.h

#ifndef _I2C__H_
#define _I2C__H_

void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);

#endif


10.2.4 MPU6050.c

#include "stm32f4xx.h"               
#include "MPU6050.h"
#include "I2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS 0xD0

void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)  //写一个字节的时序,整个过程和流程图是一模一样的
{
	MyI2C_Start(); //发送起始信号
	MyI2C_SendByte(MPU6050_ADDRESS); //发送MPU6050写地址
	MyI2C_ReceiveAck();//主机接收从机的应答,表示从机响应主机,可以开始通信了
	MyI2C_SendByte(RegAddress);//发送从机寄存器地址,表示主机想要和从机的哪块地址进行通讯
	MyI2C_ReceiveAck(); 
	MyI2C_SendByte(Data); //发送数据
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}
uint8_t MPU6050_ReadReg(uint8_t RegAddress)//读一个字节的时序
{
	//整个过程是复合时序,所有对应博客中的内容,有两个起始
	uint8_t Data;
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress);
	MyI2C_ReceiveAck();//以上述和写是一样的,因为不管是读还是写,一开始都需要主机进行以上的操作
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS|0x01);//因为0xD0是MU6050的写地址,或上0x01变为0xD1,读地址
	MyI2C_ReceiveAck();//告诉主机要开始读了
	Data=MyI2C_ReceiveByte();//把接收到的字节给到数据位
	MyI2C_SendAck(1);//因为是只发送一个字节,所有发送应答为1,就是非应答,如果需要接着发,该位设置为0
	MyI2C_Stop();
	
	return Data;
}
void MPU6050_Init(void)
{
	MyI2C_Init();
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}


10.2.5 MPU6050.h

#ifndef _MPU6050__H_
#define _MPU6050__H_

void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
void MPU6050_Init(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

#endif

10.2.6 MPU6050_Reg.h

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

#define	MPU6050_SMPLRT_DIV		0x19
#define	MPU6050_CONFIG			0x1A
#define	MPU6050_GYRO_CONFIG		0x1B
#define	MPU6050_ACCEL_CONFIG	0x1C

#define	MPU6050_ACCEL_XOUT_H	0x3B
#define	MPU6050_ACCEL_XOUT_L	0x3C
#define	MPU6050_ACCEL_YOUT_H	0x3D
#define	MPU6050_ACCEL_YOUT_L	0x3E
#define	MPU6050_ACCEL_ZOUT_H	0x3F
#define	MPU6050_ACCEL_ZOUT_L	0x40
#define	MPU6050_TEMP_OUT_H		0x41
#define	MPU6050_TEMP_OUT_L		0x42
#define	MPU6050_GYRO_XOUT_H		0x43
#define	MPU6050_GYRO_XOUT_L		0x44
#define	MPU6050_GYRO_YOUT_H		0x45
#define	MPU6050_GYRO_YOUT_L		0x46
#define	MPU6050_GYRO_ZOUT_H		0x47
#define	MPU6050_GYRO_ZOUT_L		0x48

#define	MPU6050_PWR_MGMT_1		0x6B
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75

#endif

11. 硬件读写MPU-6050

硬件读写不需要在最基层的I2C程序上更改,只需要更改初始化IIC,写数据到MPU-6050,从MPU-6050中读数据三大方面即可。

11.1 MPU6050.c

#include "stm32f4xx.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS		0xD0  //MPU6050 学数据的起始地址

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)   //时序图中规定,主机发送地址或者写一位数据,都需要从机EV5或者EV8等等事件的应答
{
	uint32_t Timeout;
	Timeout = 10000;  //设置一个时间,防止应答时间过长,程序崩溃
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)  //检查时间状态位
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
}

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)  // MPU6050写数据
{
	I2C_GenerateSTART(I2C2, ENABLE); //首先产生起始位函数
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待从机应答,I2C_EVENT_MASTER_MODE_SELECT是EVx应答的宏定义函数
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//发送一个7位字节的数据地址,等待应答
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	
	I2C_SendData(I2C2, RegAddress);//发送数据位地址,等待应答
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);
	
	I2C_SendData(I2C2, Data);//发送数据,等待应答
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	
	I2C_GenerateSTOP(I2C2, ENABLE);//产生停止位函数
}

uint8_t MPU6050_ReadReg(uint8_t RegAddress)//MPU6050读数据
{
	uint8_t Data;//设置一个接收读到数据的Data,用于返回
	
	I2C_GenerateSTART(I2C2, ENABLE);//产生起始位,等待应答
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);//发送一个7位地址,等待应答
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	
	I2C_SendData(I2C2, RegAddress);//发送数据位地址,等待应答
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	
	I2C_GenerateSTART(I2C2, ENABLE); //二次起始位,准备读数据
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);//发送接收位地址,等待应答
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
	
	I2C_AcknowledgeConfig(I2C2, DISABLE);//使能IIC2
	I2C_GenerateSTOP(I2C2, ENABLE);//产生停止位P
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);//等待读数据结束后的应答
	Data = I2C_ReceiveData(I2C2);//将I2C2读到的数据给到Data
	
	I2C_AcknowledgeConfig(I2C2, ENABLE);//使能IIC2
	
	return Data;//返回读到的数据
}

void MPU6050_Init(void)
{
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);//初始化IIC时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//初始化GPIOB引脚
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	I2C_InitTypeDef I2C_InitStructure;//初始化IIC结构体
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;  //模式设置为IIC
	I2C_InitStructure.I2C_ClockSpeed = 50000;
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;//设置IIC占空比
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;//应答
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;//从机地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;//主机地址
	I2C_Init(I2C2, &I2C_InitStructure);
	
	I2C_Cmd(I2C2, ENABLE); //使能开启IIC2
	
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}

11.2 MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);

void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

#endif

12. I2C与AT24C02通信

12.1 实验程序

实验现象:开机的时候首先先检测AT24C02是否存在,然后主循环里检测两个按键,按键1(KEY1)执行写入AT24C02的操作,按键0(KEY0)执行读出操作,在LCD模块上显示相关信息。

12.1.1 main.c

#include "stm32f4xx.h"
#include "delay.h"
#include "usart.h"
#include "LED.h"
#include "lcd.h"
#include "Key.h"
#include "usmart.h"
#include "MyI2C.h"
#include "AT24C02.h"

//LCD状态设置函数
void led_set(u8 sta)//只要工程目录下有usmart调试函数,主函数就必须调用这两个函数
{
	LED1=sta;
}
//函数参数调用测试函数
void test_fun(void(*ledset)(u8),u8 sta)
{
	led_set(sta);
}
//要写到AT24C02的字符串数组
const u8 TEXT_Buffer[]={"Explorer STM32F4 IIC TEST"};
#define SIZE sizeof(TEXT_Buffer)

int main(void)
{
	u8 key;
	u16 i=0;
	u8 datatemp[SIZE];
	delay_init(168);
	uart_init(115200);
	
	LED_Init();
	LCD_Init();
	Key_Init();
	AT24C02_Init();
	POINT_COLOR=RED;
	LCD_ShowString(30,50,200,16,16,"Explorer STM32F4");
	LCD_ShowString(30,70,200,16,16,"IIC TEST");
	LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
	LCD_ShowString(30,110,200,16,16,"2023/20/23");
	LCD_ShowString(30,130,200,16,16,"KEY1:Write  KEY0:Read");
	
	while(AT24C02_Check())//AT24C02_Check()返回0表示检测成功,返回1表示检测失败,while中参数如果是1,则表示检测不到AT24C02
	{
		LCD_ShowString(30,150,200,16,16,"AT24C02 Check Failed!");
		delay_ms(500);
		LCD_ShowString(30,150,200,16,16,"Please Check!        ");
		delay_ms(500);
		LED0=!LED0;
	}
	LCD_ShowString(30,150,200,16,16,"AT24C02 Ready!"); //只要从while循环中出来,就表示检测到了AT24C02
	
	POINT_COLOR=BLUE;
	while(1)
	{
		key=KEY_Scan(0);  //单次扫描按键,不是循环扫描按键
		if(key==2)  //KEY1按下,写入AT24C02
		{
			LCD_Fill(0,170,239,319,WHITE);  //清除半屏
			LCD_ShowString(30,170,200,16,16,"Start Write AT24C02……");
			AT24C02_Write(0,(u8*)TEXT_Buffer,SIZE);
			LCD_ShowString(30,170,200,16,16,"AT24C02 Write Finished!"); //提示写成功
		}
		if(key==1)  //KEY0按下,读AT24C02
		{
			LCD_ShowString(30,170,200,16,16,"Start Read AT24C02……");
			AT24C02_Read(0,datatemp,SIZE); //事先已经定义好了一个和TEXT_Buffer同样大小的数组datatemp,先把读到的数据放到该数组,最后再显示这个数组
			LCD_ShowString(30,170,200,16,16,"The Data Readed Is:   "); 
			LCD_ShowString(30,190,200,16,16,datatemp); //显示读到的字符串
		}
		i++;
		delay_ms(10);
		if(i==20)
		{
			LED0=!LED0;
			i=0;
		}
	}
}

12.1.2 MyI2C.c

#include "stm32f4xx.h"                 
#include "MyI2C.h"
#include "delay.h"

//初始化IIC
void IIC_Init(void)
{
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);  //初始化GPIOB时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;//普通输出模式
	GPIO_InitStructure.GPIO_OType=GPIO_OType_PP;  //推挽输出
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8|GPIO_Pin_9;
	GPIO_InitStructure.GPIO_PuPd=GPIO_PuPd_UP;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_100MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	IIC_SCL=1;
	IIC_SDA=1; //初始化设置串行数据总线和串行时钟总线为高电平,表示初始化时总线处于空闲状态 
}
//产生起始信号
void IIC_Start(void)
{
	SDA_OUT();  // SDA线输出
	IIC_SDA=1;
	IIC_SCL=1;
	delay_us(4);
	IIC_SDA=0;// 时钟线为高电平时,数据总线由高电平变为低电平表示起始信号
	delay_us(4);
	IIC_SCL=0;  // 准备发送和接收数据,因为将数据放到SDA线上是在SCL低电平期间执行的
    // 为发送和接收数据做准备
}
//产生IIC停止信号
void IIC_Stop(void)
{
	SDA_OUT(); // SDA线输出
	IIC_SCL=0;
	IIC_SDA=0; // SCL为低电平时,SDA从低电平转换为高电平表示停止信号
	delay_us(4);
	IIC_SCL=1;
	IIC_SDA=1;
	delay_us(4);
}
//等待应答信号的到来
//返回值: 1,接收应答失败
//		  0,接收应答成功
u8 IIC_Wait_Ack(void)
{
	u8 ucErrTime=0;
	SDA_IN();  // SDA设置为输入,应答信号来临以后是要接收数据的,所以设置为输入
	IIC_SDA=1;
	delay_us(1);
	IIC_SCL=1; // SCL由高电平变为低电平表示产生一次应答
	delay_us(1);
	while(READ_SDA)
	{
		ucErrTime++;
		if(ucErrTime>250)// 防止应答时间过长,程序崩溃
		{
			IIC_Stop();
			return 1;
		}
	}
	IIC_SCL=0;
	return 0;
}
//产生ACK应答
void IIC_Ack(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=0;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;  // SCL由高电平转到低电平表示一次应答
}
//不产生ACK应答
void IIC_NACK(void)
{
	IIC_SCL=0;
	SDA_OUT();
	IIC_SDA=1;  // SDA高电平期间是不产生应答的
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0; 
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 Byte)
{
	u8 i;
	SDA_OUT();
	IIC_SCL=0;  //拉低时钟开始数据传输
	for(i=0;i<8;i++)
	{
		IIC_SDA=(Byte&0x80)>>7;
		Byte<<=1;
		delay_us(2);
		IIC_SCL=1;
		delay_us(2);
		IIC_SCL=0;
		delay_us(2);  //时钟由高电平到低电平表示发送一个字节
	}
}
//读字节
//Ack=1,发送ACK
//Ack=0,发送nACK
u8 IIC_Read_Byte(u8 Ack)
{
	u8 i,Data;
	SDA_IN();
	for(i=0;i<8;i++)
	{
		IIC_SCL=0; //SCL低电平期间,从机将数据依次放到SDA上(高位先行)
		delay_us(2);
		IIC_SCL=1;  //读一个字节之前首先释放总线(释放SCL)
        //主机在SCL高电平期间读取数据位
		Data<<=1; //Data初始时为1,接收第一位是左移1位,右侧补0
		if(READ_SDA) //读取数据总线上的数据位
			Data++; //Data++,会加在最低位,接收下一位时,Data首先会左移一位,所以接收的上一位会放到次低位上,这样经过8次循环,第一次接收的数据位会放在最高位上,也就达成了高位先行的原则
		delay_us(1);
	}
	if(!Ack) //如果没有发送应答
		IIC_NACK(); //则调用不产生应答的函数
	else //如果发送应答
		IIC_Ack(); //则调用产生应答的函数
	return Data; //将循环8次接收的字节返回
}


12.1.3 MyI2C.h

#ifndef _MYI2C__H_
#define _MYI2C__H_

#define SDA_IN() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<<9*2;}  //PB9输入模式
#define SDA_OUT() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=1<<9*2;}  //PB9输出模式
 
#define IIC_SCL PBout(8) //SCL
#define IIC_SDA PBout(9) //SDA
#define READ_SDA PBin(9) //输出SDA

void IIC_Init(void);
void IIC_Start(void);
void IIC_Stop(void);
u8 IIC_Wait_Ack(void);
void IIC_Ack(void);
void IIC_NACK(void);
void IIC_Send_Byte(u8 Byte);
u8 IIC_Read_Byte(u8 Ack);

#endif

12.1.4 AT24C02.c

#include "stm32f4xx.h"                 
#include "AT24C02.h"
#include "MyI2C.h"
#include "delay.h"

//初始化IIC接口
void AT24C02_Init(void)
{
	IIC_Init();
}
//在AT24C02指定的地址写入一个数据
//WriteAddress:写入数据的目的地址
//WriteData:要写入的数据
void AT24C02_WriteByte(u16 WriteAddress,u8 WriteData)
{
	IIC_Start();
	if(EE_TYPE>AT24C16)
	{
		IIC_Send_Byte(0xA0);  //发送写命令 AT24C02的写命令是0xA0,也可以在头文件中进行宏定义,方便后续修改
		IIC_Wait_Ack();
		IIC_Send_Byte(WriteAddress>>8);  //发送高地址
	}
	else
		IIC_Send_Byte(0xA0+((WriteAddress/256)<<1));  //发送器件地址,写数据 
    //WriteAddress是16位的,WriteAddress/256表示取出16位的高8位,左移1位,因为最低位是读写位
	IIC_Wait_Ack();
	IIC_Send_Byte(WriteAddress%256);   //发送低地址
	IIC_Wait_Ack();
	IIC_Send_Byte(WriteData);  //发送数据
	IIC_Wait_Ack();
	IIC_Stop();
	delay_ms(10);
}
//在AT24C02指定的地址读出一个数据
//ReadAddress:开始读出的地址
//返回值:读到的数据
u8 AT24C02_ReadByte(u16 ReadAddress)
{
	u8 Data;
	IIC_Start();
	if(EE_TYPE>AT24C16)
	{
		IIC_Send_Byte(0xA0);  //发送写命令AT24C02的写命令是0xA0,也可以在头文件中进行宏定义,方便后续修改
		IIC_Wait_Ack();
		IIC_Send_Byte(ReadAddress>>8);  //发送高地址
	}
	else
		IIC_Send_Byte(0xA0+((ReadAddress/256)<<1));  //发送器件地址,写数据
	IIC_Wait_Ack();
	IIC_Send_Byte(ReadAddress%256);   //发送低地址
	IIC_Wait_Ack();
	
	//从AT24C02中读数据是需要经历两次起始位的,第一次起始位之前和写数据是一模一样的
	
	IIC_Start();
	IIC_Send_Byte(0xA0|0x01); //0XA0是写数据的地址,0xA1就是读数据的地址
	IIC_Wait_Ack();
	Data=IIC_Read_Byte(0);  //根据IIC文件下IIC_Read_Byte的设置,如果设置参数为0,那么意味着只读一次,发送非应答,如果需要接着读,那么就需要发送应答,也就是参数设置为1
	IIC_Stop();
	return Data;
}
//在AT24C02里面的指定地址开始写入长度为Len的数据
//该函数用于写入16bit或者32bit的数据
//WriteAddress:开始写入的地址
//WriteData:数据元素首地址
//Len:要写入数据的长度2,4
void AT24C02_WriteLenByte(u16 WriteAddress,u32 WriteData,u8 Len)
{
	u8 t;
	for(t=0;t<Len;t++)
	{
		AT24C02_WriteByte(WriteAddress+t,(WriteData>>(8*t))&0xFF);
	}// WriteAddress+t 的意义就相当于一个指针一样,保证每一次循环都指向下一个地址
     // AT24C02_WriteByte 写入的数据是8位,所以每次都需要 &0xFF 取出高8位
     // WriteData>>(8*t) 的意思是根据 for 循环第一次取出32位的高8位,第二次取出32位的次8位,
     // 循环的次数根据 len 来决定,最终写入长度为 len 的数据   
}
//在AT24CXX里面的指定地址开始读出长度为Len的数据
//该函数用于读出16bit或者32bit的数据.
//ReadAddress: 开始读出的地址 
//返回值     :数据
//Len        :要读出数据的长度2,4
u32 AT24C02_ReadLenByte(u16 ReadAddress,u8 Len)
{
	u8 t;
	u32 Data;
	for(t=0;t<Len;t++)
	{
		Data<<=8;//首先每次进入循环Data左移8位,把最低8位置为0,方便对低八位进行赋值
		Data+=AT24C02_ReadByte(ReadAddress+Len-t-1);
        //AT24C02_ReadByte每次读出的数据长度也是8位,ReadAddress+Len表示锁定要读取的长度为len 的数据
	}
	return Data;
}
//检查AT24C02是否正常
//这里运用一个AT24C02最后一个地址(255)来存储标记字,如果运用AT24Cxx的其他系列,那么这个地址就需要进行更改
//返回1:检测失败
//返回0:检测成功
u8 AT24C02_Check(void)
{
	u8 ID;
	ID=AT24C02_ReadByte(255);  //解除睡眠,避免每次开机都写AT24C02
	if(ID==0x55)
		return 0;//AT24C02在地址为255上读出的为0x55,如果是就表示检测成功
	else//排除第一次初始化的影响
	{
		AT24C02_WriteByte(255,0x55);//主动在255地址上写0x55
		ID=AT24C02_ReadByte(255);//自己主动在地址上写的0x55,如果还是读不出来0x55,只能表明之前写的读写函数有问题了
		if(ID==0x55)
			return 0;
	}
	return 1;
}
//在AT24C02里面的指定地址开始写入指定个数的数据
//WriteAddress:开始写入的地址
//pBuffer:数据数组首地址
//Num:要写入数据的个数
void AT24C02_Write(u16 WriteAddress,u8 *pBuffer,u16 Num)
{
	while(Num--)
	{
		AT24C02_WriteByte(WriteAddress,*pBuffer);
		WriteAddress++;
		pBuffer++;
	}
}
//在AT24C02里面的指定地址开始读出指定个数的数据
//WriteAddress:开始读出的地址
//pBuffer:数据数组首地址
//Num:要读出数据的个数
void AT24C02_Read(u16 WriteAddress,u8 *pBuffer,u16 Num)
{
	while(Num)
	{
		*pBuffer++=AT24C02_ReadByte(WriteAddress++);
		Num--;
	}
}

12.1.5 AT24C02.h

#ifndef _AT24C02__H_
#define _AT24C02__H_

#define AT24C01		127
#define AT24C02		255
#define AT24C04		511
#define AT24C08		1023
#define AT24C16		2047
#define AT24C32		4095
#define AT24C64	    8191
#define AT24C128	16383
#define AT24C256	32767  
//Mini STM32开发板使用的是24c02,所以定义EE_TYPE为AT24C02
#define EE_TYPE AT24C02


void AT24C02_Init(void);
void AT24C02_WriteByte(u16 WriteAddress,u8 WriteData);
u8 AT24C02_ReadByte(u16 ReadAddress);
void AT24C02_WriteLenByte(u16 WriteAddress,u32 WriteData,u8 Len);
u32 AT24C02_ReadLenByte(u16 ReadAddress,u8 Len);
u8 AT24C02_Check(void);
void AT24C02_Write(u16 WriteAddress,u8 *pBuffer,u16 Num);
void AT24C02_Read(u16 WriteAddress,u8 *pBuffer,u16 Num);

#endif

        本文篇幅较长,望读者细心研读,之所以在介绍IIC的基础上,附加介绍了AT24C02、MPU-6050、EEPROM,是因为IIC可以分别和上述三种外设进行一主一次的通信,也可以进行一主多从的通信,也就是一个主机挂载多个外设进行通信。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。