精华内容
下载资源
问答
  • 如何计算i2c地址
    千次阅读
    2021-12-06 21:41:39

    I2C 总线能挂多少设备?

    理论上:
    7-bit address :2 的 7 次方,能挂 128 个设备。
    10-bit address :2 的 10 次方,能挂 1024 个设备。

    当然,要把预留设备地址去除,7 bit 协议规定了 8个预留地址,厂商不可以使用。保留地址如下:
    在这里插入图片描述

    但是I2C协议规定,总线上的电容不可以超过 400pF。管脚都是有输入电容的,PCB上也会有寄生电容,所以会有一个限制。实际设计中经验值大概是不超过8个器件。

    总线之所以规定电容大小是因为,I2C 使用的GPIO为开漏结构,要求外部有电阻上拉,电阻和总线电容产生了一个 RC 延时效应,电容越大信号的边沿就越缓,有可能带来信号质量风险。

    传输速度越快,信号的窗口就越小,上升沿下降沿时间要求更短更陡峭,所以 RC 乘积必须更小。

    I2C 架构
    在这里插入图片描述

    我们知道I2C有不同的速度模式,标准(100KHz)、快速(400KHz)、快速plus(1MHz)采用的GPIO都是开漏模式,支持线与,但是开漏模式无法输出高电平,所以需要外部有上拉,接一个电阻然后接Vdd。

    Vdd可以采用 5V、3.3V、1.8V等,电源电压不同,上拉电阻阻值也不同。

    一般总线上认为,低于0.3Vdd为低电平,高于0.7Vdd为高电平。

    关于为什么GPIO开漏模式支持线与,无法输出高电平,我在GPIO系列文章写过:

    gpio subsystem(1) – linuxer

    线与,是I2C协议的基础!!!!

    线与:当总线上只要有一个设备输出低电平,整条总线便处于低电平状态,这时候总线被称为占用状态。

    上拉电阻计算

    1、上拉电阻过小,电流增大,端口输出低电平增大。

    2、上拉电阻过大,上升沿时间增大,方波可能会变成三角波。

    因此计算出一个精确的上拉电阻阻值是非常重要的。

    计算上拉电阻的阻值,是有明确计算公式的:
    在这里插入图片描述
    在这里插入图片描述

    最大电阻和上升沿时间 tr 、总线电容 Cb 、标准上升沿时间 0.8473 有关。

    最小电阻和电源Vdd电压、GPIO口自己最大输出电压 Vol、 GPIO口自己最大电流 Vol 有关。

    查《I2C-bus specification and user manual.pdf》7.1节:

    在这里插入图片描述
    在这里插入图片描述

    查《I2C-bus specification and user manual.pdf》表10:

    在这里插入图片描述

    1、标准模式:0~100KHz,上升沿时间 tr = 1us

    2、快速模式:100~400KHz,上升沿时间 tr = 0.3us

    3、高速模式:up to 3.4MHz,上升沿时间 tr = 0.12us

    由此公式,假设 Vdd 是 1.8V 供电,Cb总线电容是200pF(虽然协议规定负载电容最大400pF,实际上超过200pF波形就很不好,我们以200pF来计算)

    标准模式 :
    在这里插入图片描述

    快速模式:
    在这里插入图片描述

    高速模式:
    在这里插入图片描述

    最小电阻(Vdd越大,上拉电阻就要越大):

    在这里插入图片描述

    注意,高速模式下,电源电压一般采用 1.8 V,不会采用 3.3V,因为如果用 3.3V 计算你会发现最小电阻比最大电阻大。

    采用合适的电源电压和合适的上拉电阻,才会让你的 I2C 传输信号最优。

    大家在不同速率采用的电阻一般有以下几种:1.5K、2.2K、4.7K。

    在这里插入图片描述

    一般很少会用到 高速模式(HS),因为这种模式协议比较复杂,既然要快不如用 SPI 了。

    欢迎关注我的微信公众号,持续更新:

    在这里插入图片描述

    关于 I2C 协议的十篇文章,包括传输协议、传输机制、上拉电阻的计算、Linux 下软件驱动框架,都写在这里:

    I2C(一):I2C bus spec

    更多相关内容
  • 一文搞懂I2C总线通信

    千次阅读 2022-03-22 22:12:24
    本来不打算写这篇文章,因为网上关于I2C总线通信的资料很多很全。但是最近刚换工作,主要做驱动开发,第一个驱动就是I2C通信,想了想还是结合网上的资料再整理下思路,方便今后的查阅和温习。

    目录

    1、简介

    2、物理特性

    3、通讯特性

    3.1、开始和停止条件

    3.2、地址传送

    3.3、数据传送

    3.4、总线应答

    3.5、总线仲裁

    3.6、时钟同步/时钟延展

    4、通信时序和协议

    4.1、起始条件

    4.2、重复起始条件

    4.3、地址帧

    4.4、读⁄写位

    4.5、ACK⁄NACK位

    4.6、数据帧

    4.7、停止条件

    5、工作过程

    5.1、单个主设备连接多个从机

    5.2、多个主设备连接多个从机 


    1、简介

    I2C(集成电路总线),由Philips公司(2006年迁移到NXP)在1980年代初开发的一种简单、双线双向的同步串行总线,它利用一根时钟线和一根数据线在连接总线的两个器件之间进行信息的传递,为设备之间数据交换提供了一种简单高效的方法。每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。

    I2C 标准是一个具有冲突检测机制和仲裁机制的真正意义上的多主机总线,它能在多个主机同时请求控制总线时利用仲裁机制避免数据冲突并保护数据。作为嵌入式开发者,使用I2C总线通信的场景有很多,例如驱动FRAM、E2PROM、传感器等。

    总结来说,I2C总线具有以下特点:

    • 只需要SDA、SCL两条总线;
    • 没有严格的波特率要求;
    • 所有组件之间都存在简单的主/从关系,连接到总线的每个设备均可通过唯一地址进行软件寻址;
    • I2C是真正的多主设备总线,可提供仲裁和冲突检测;
    • 传输速度分为四种模式:
    1. 标准模式(Standard Mode):100 Kbps
    2. 快速模式(Fast Mode):400 Kbps
    3. 高速模式(High speed mode):3.4 Mbps
    4. 超快速模式(Ultra fast mode):5 Mbps
    • 最大主设备数:无限制;
    • 最大从机数:理论上,1008个从节点,寻址模式的最大节点数为2的7次方或2的10次方,但有16个地址保留用于特殊用途。

    I2C有16个保留I2C地址。这些地址对应于以下两种模式之一:0000 XXX或1111 XXX。下表显示了为特殊目的而保留的I2C地址。

    I2C 节点地址R/W位功能描述
    0000 0000广播地址
    0000 0001起始字节
    0000 001XCBUS 地址
    0000 010X保留用于不同总线格式
    0000 011X保留供未来使用
    0000 1XXX高速模式主代码
    1111 1XXX保留供未来使用
    1111 0XXX10位节点地址

    I2C还有两个变体,分别专注于系统和电源应用,称为系统管理总线(SMBus)和电源管理总线(PMBus)。

    2、物理特性

    I2C 总线使用连接设备的 "SDA"( 串行数据总线)和"SCL"( 串行时钟总线 来传送信息。

    I2C 总线内部使用漏极开路输出驱动器,因此 SDASCL 可以被拉低为低电平,但是不能被驱动为高电平,所以每条线上都要使用一个上拉电阻,默认情况下将其保持在高电平。

    I2C 总线上拉电阻阻值取决于系统应用,TI 官方手册推荐使用以下公式来计算上拉电阻值:

    根据上表,这里不难发现需要在做电阻选择需要满足几个条件:

    • 灌电流最大值为3mA;
    • 低电平输出电压设置了最大值为0.4V。

    所以根据上述公式可以计算,对于5V的电源,每个上拉电阻阻值至少1.53kΩ,而对于3.3V的电源,每个电阻阻值至少967Ω。

    如果觉得计算电阻值比较麻烦,也可以使用典型值 4.7kΩ。若各位想了解更多可直接参见手册说明。

    3、通讯特性

    通常情况下,一个完整的I2C通信过程包括以下 4 部分:

    • 开始条件
    • 地址传送
    • 数据传送
    • 停止条件

    主机在 SCL 线上输出串行时钟信号,数据在 SDA 线上进行传输,每传输一个字节(最高位 MSB 开始传输)后面跟随一个应答位,一个 SCL 时钟脉冲传输一个数据位。

    标准的I2C时序如下图所示:

    3.1、开始和停止条件

    当总线上的主机都不驱动总线,总线进入空闲状态, SCL SDA 都为高电平。总线空闲状态下总线上设备都可以通过发送开始条件启动通信。
    当 SCL 线为高时,SDA 线上出现由高到低的信号,表明总线上产生了起始信号。 SDA 线上出现由低到高的信号,表明总线上产生了停止信号,如下图所示:

    当两个起始信号之间没有停止信号时,即产生了重复起始信号。主机采用这种方法与另一个从机或相同的从机以不同传输方向进行通信(例如:从写入设备到从设备读出)而不释放总线。如下图所示:

    3.2、地址传送

    开始条件或者重新开始条件后面的帧是地址帧(一个字节),用于指定主机通信的对象地址,在发送停止条件之前,指定的从机一直有效。
    I2C通讯支持:7 位寻址和10 位寻址两种模式。
    7 位寻址模式,地址帧(8bit)的高 7 位为从机地址,地址帧第 8 位来决定数据帧传送的方向:7 位从机地址 + 1位 读/写位,读/写位控制从机的数据传输方向(0:写; 1:读) 。帧格式如下所示:

    10 位寻址模式,主机发送帧,第一帧 发送头序列(11110XX0,其中 XX 表示 10 位地址的高 两位),然后第二帧发送低八位从机地址。 主机接收,第一帧发送头序列(11110XX0,其中 XX 表示 10 位地址的高两位),然后第二帧发送低八位从机地址。接下来会发送一个重新开始条件,然后再发送一帧头序列(11110XX1 ,其中 XX 表示 10 位地址的高两位)帧格式如下所示:

    解析如下:
    • S :表示开始条件;
    • SLA :表示从机地址;
    • R/W#:表示发送和接收的方向。当 R/W# “1” 时,将数据从从机发送到主机;当 R/W#“0” 时,将数据从主机发送到从机;
    • Sr :表示重新开始条件;
    • DATA :表示发送和接收的数据;
    • P :表示停止条件。

    3.3、数据传送

    地址匹配一致后,总线上的主机根据 R/W 定义的方向一帧一帧的传送数据。 所有的地址帧后传送的数据都视为数据帧。即使是 10 位地址格式的低 8 位地址也视为数据帧。
    数据帧的长度是 8 位。 SCL 的低电平 SDA 变化, SCL 的高电平 SDA 保持,每个时钟周期发送一位数据。数据帧后的第 9 个时钟是应答位,是接收方向发送方传送的握手信号。

    如果总线上从机接收数据,在第 9 个时钟周期不响应主机,从机必须发送 NACK。如果总线上主机接收数据,第 9 个周期发送 NACK,从机接收到 NACK,从机停止发送数据。

    无论主机还是从机发送了 NACK,数据传送终止。主机可以做下列任一动作:

    • 发送停止条件释放总线 ;
    • 发送重新开始条件开始一个新的通信。
    以华大MCU(HC3F4A0系列)为例,在主机接收模式中,主机输出 SCL 时钟,接收从机数据并返回应答。主机接收数据的运行时序例如下图所示:
    7 位地址格式的主机发送数据时序图

    在主机接收模式中,主机输出 SCL 时钟,接收从机数据并返回应答。主机接收数据的运行时序例如下图所示:

    7 位地址格式的主机接收数据的时序图
    在从机发送模式中,接收来自主机的 SCL 时钟,本产品为从机发送数据,并且接收主机返回应答。从机发送数据的运行时序例如下图所示:
    7 位地址格式的从机发送模式时序图
    在从机接收模式中,接收来自主机的 SCL 时钟和数据,接收完数据后返回应答。从机接收数据的运行时序例如下图所示:
    7 位地址格式从机接收模式时序图

    3.4、总线应答

    每传输一个字节,后面跟随一个应答位。通过将 SDA 线拉低,来允许接收端回应发送端。ACK 为 一个低电平信号,当时钟信号为高时, SDA 保持低电平则表明接收端已成功接收到发送端的数据。
    当主机作为发送器件时,如果从机上产生无响应信号(NACK) ,主机可以产生停止信号来退出数据传输,或者产生重复起始信号开始新一轮的数据传输。当主机作为接收器件时,发生无响应信号(NACK) ,从机释放 SDA 线,使主机产生停止信号或重复起始信号。
    I2C 总线上应答信号

    3.5、总线仲裁

    I2C 总线上的仲裁分为两个部分: SCL 线上的同步和 SDA 线上的仲裁。
    • SCL 线上的同步(时钟同步)
    由于 I2C 总线具有线“与”的逻辑功能, SCL 线上只要有一个节点发送低电平,总线上就表现低电平。当所有的节点都发送高电平时,总线才能表现为高电平。所以,时钟低电平的时间由时钟电平期最长的器件决定,而时钟的高电平时间由时钟高电平期最短的器件决定。
    由于 I2C 这种特性,当多个主机同时发送时钟信号时,在总线上表示的是统一的时钟信号。如果从机希望主机降低传送速度可以通过将 SCL 主动拉低延长其低电平时间来通知主机,当主机在准备下一次传送时发现 SCL 的电平被拉低时进行等待,直到从机完成操作并释放 SCL 线的控制权。
    • SDA 线上的仲裁
    SDA 线上的仲裁也是由于 I2C 总线具有线“与”的逻辑功能。主机在发送数据后,通过比较总线上的数据来决定是否退出竞争。丢失仲裁的主机立即切换到未被寻址的从机状态,以确保自身能被仲裁胜利的主机寻址到。仲裁失败的主机继续输出时钟脉冲(在 SCL 上),直到发送完当前的串行字节。通过这种原理可以保证 I2C 总线在多个主机企图控制总线时保证数据的不丢失。
    I2C 总线上的仲裁

    解析如下:

    (1)另一器件发送串行数据;
    (2)另一器件通过拉低 SDA 先撤消了该 I2C 主机发送的一个逻辑 1 (虚线)。仲裁丢失,I2C 进入从接收模式;
    (3)此时 I2C 处于从接收模式,但仍产生时钟脉冲,直至发送完当前字节。 I2C 将不为下个字节的传输产生时钟脉冲。一旦赢得仲裁,SDA 上的数据传输由新的主机来启动。

    3.6、时钟同步/时钟延展

    I2C规范没有为时钟同步规定任何超时条件,也就是说,任何器件都可以根据需要保持SCL。

    在I2C通信协议中,时钟速度和信号始终由主器件产生。I2C主器件产生的信号提供主器件和节点连接之间的同步。

    在某些情况下,节点或子节点不是以全状态工作,在接收主器件生成的时钟之前,需要减慢速度。这是通过一种称为"时钟同步/时钟延展"的机制来实现的。

    在时钟同步/时钟延展期间,为了降低总线速度,允许节点压低时钟。而在主器件方面,在其变为高电平状态后,必须回读时钟信号。然后,它必须等待,直至线路达到高电平状态。

    通过时钟同步/时钟延展,I2C节点器件可以强制主器件进入等待状态。当节点器件需要更多时间来管理数据时,例如存储接收到的数据或准备发送另一字节的数据时,它可能会执行时钟同步/时钟延展。这通常发生在节点器件接收并确认收到一个字节的数据之后。

    是否需要时钟延展取决于节点器件的功能。这里有两个例子:

    • 处理器件(如微处理器或微控制器)可能需要额外的时间来处理中断,接收和管理数据,以及执行适当的功能;
    • 较简单的器件(如EEPROM)不在内部处理数据,因此不需要时钟延展来执行任何功能。

    4、通信时序和协议

    说起I2C通信协议必然离不开通信时序,主器件和从节点必须遵守I2C时序规格才能正确传输数据。

    下表显示了时序规格表上给出的符号和参数。

    符号参数单位
    fSCLSCL 时钟频率kHz
    tHD(STA)(重复)起始条件的保持时间µs
    tLOW引脚的低电平周期µs
    tHIGH引脚的高电平周期µs
    tSU(STA)重复起始条件的建立时间µs
    tHD(DAT)数据保持时间µs
    tSU(DAT)数据建立时间ns
    trSDA 信号的上升时间ns
    tfSDA 信号的下降时间ns
    tSU(STO)停止条件的建立时间µs

    4.1、起始条件

    起始条件总是在传输开始时出现,并由主器件发起。这样做是为了唤醒总线上的空闲节点器件。SDA线从高电平切换到低电平,然后SCL线从高电平切换到低电平。时序和协议如下图所示:

    4.2、重复起始条件

    在不发出停止条件的情况下,起始条件可以在传输期间重复。这是一种特殊情况,称为重复起始,用于改变数据读、写传输方向、重复尝试传输、同步多个IC,甚至控制串行存储器等。如下图所示:

    4.3、地址帧

    地址帧包含7位或10位序列,具体取决于可用性(参见数据手册)。如下图所示:

    不像SPI协议,I2C没有节点选择线路,因此它需要另一种方法来让节点知道数据正向其发送,而不是向另一个节点发送。这是通过寻址来实现的。地址帧始终是新消息中起始位之后的第一帧。

    主器件将其想要与之通信的节点地址发送到其所连接的每个节点。然后,每个节点将主器件所发送的地址与其自己的地址进行比较。如果地址匹配,它便向主器件发送一个低电压ACK位。如果地址不匹配,则节点什么也不做,SDA线保持高电平。

    4.4、读⁄写位

    地址帧的最后一位告知节点,主器件是想要将数据写入其中还是从中接收数据。如果主器件希望将数据发送到节点,则读⁄写位处于低电平。如果主器件请求从节点得到数据,则该位处于高电平。如下图所示:

    4.5、ACK⁄NACK位

    消息中的每一帧后面都跟随一个应答⁄不应答位。如果成功接收到一个地址帧或数据帧,则从机会向主机返回一个ACK位。如下图所示:

    4.6、数据帧

    主器件检测到来自从节点的ACK位之后,就准备发送第一数据帧。数据帧总是8位长,并以MSB优先方式发送。每个数据帧之后紧接着一个ACK⁄NACK位,以验证该帧是否已成功接收。主器件或节点(取决于谁发送数据)必须收到ACK位,然后才能发送下一数据帧。时序和协议如下图所示:

    4.7、停止条件

    发送完所有数据帧之后,主器件可以向节点发送停止条件以停止传输。停止条件是指SCL线上的电压从低电平变为高电平,然后在SCL线保持高电平的情况下,SDA线上的电压从低电平变为高电平。时序和协议如下图所示:

    5、工作过程

    最后整体叙述一下I2C通讯过程,本小节内容整理来源于:微信公众号:小麦大叔,作者菜刀和小麦。

    第1步:起始条件

    主设备通过将SDA线从高电平切换到低电平,再将SCL线从高电平切换到低电平,来向每个连接的从机发送启动条件,如下图所示:

    第2步:发送从设备地址

    主设备向每个从机发送要与之通信的从机的7位或10位地址,以及相应的读/写位,如下图所示:

    第3步:接收应答

    每个从设备将主设备发送的地址与其自己的地址进行比较。如果地址匹配,则从设备通过将SDA线拉低一位以表示返回一个ACK位

    如果来自主设备的地址与从机自身的地址不匹配,则从设备将SDA线拉高,表示返回一个NACK位。

     第4步:收发数据

    主设备发送或接收数据到从设备,如下图所示:

    第5步:接收应答

    在传输完每个数据帧后,接收设备将另一个ACK位返回给发送方,以确认已成功接收到该帧,如下图所示:

    第6步:停止通信

    为了停止数据传输,主设备将SCL切换为高电平,然后再将SDA切换为高电平,从而向从机发送停止条件,如下图所示:

    5.1、单个主设备连接多个从机

    I2C单个主设备连接多个从机。使用7位地址可提供128 (2的7次方)个唯一地址。使用10位地址很罕见,但可提供1024 (2的10次方)个唯一地址。要将多个节点连接到单个主器件,请使用4.7 kΩ上拉电阻连接这些节点,并将SDA和SCL线连接到VCC,如下图所示:

    5.2、多个主设备连接多个从机 

    多个主设备可以连接到一个或多个从机。

    当两个主设备试图通过SDA线路同时发送或接收数据时,同一系统中的多个主设备就会出现问题。

    为了解决这个问题,每个主设备都需要在发送消息之前检测SDA线是低电平还是高电平

    • 如果SDA线为低电平,则意味着另一个主设备可以控制总线,并且主设备应等待发送消息;
    • 如果SDA线为高电平,则可以安全地发送消息。


    拓展学习:

    1、I2C Bus

    2、https://www.nxp.com.cn/docs/en/application-note/AN10216.pdf

    3、https://www.ti.com/lit/an/slva689/slva689.pbeiz、

    4、I2C协议文档,中文,周立功翻译版本

    5、https://www.nxp.com/docs/en/user-guide/UM10204.pdf

    6、I2C通信协议:了解I2CPrimer、PMBus和SMBus 

    7、I2C-bus specification and user manual  

    展开全文
  • STM32硬件I2C与软件模拟I2C超详解

    千次阅读 多人点赞 2022-05-10 13:47:10
    I2C协议时序详细解剖,硬件I2C的实现,已经软件模拟I2C的实现,以及EEPROM的详细介绍

    ✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
    📃个人主页:@rivencode的个人主页
    🔥系列专栏:玩转STM32
    💬推荐一款模拟面试、刷题神器,从基础到大厂面试题👉点击跳转刷题网站进行注册学习

    一.I2C协议简介

    I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备(那些电平转化芯片),现在被广泛地使用在系统内多个集成电路(IC)间的通讯。

    I2C只有一跟数据总线 SDA(Serial Data Line),串行数据总线,只能一位一位的发送数据,属于串行通信,采用半双工通信

    • 半双工通信:可以实现双向的通信,但不能在两个方向上同时进行,必须轮流交替进行,其实也可以理解成一种可以切换方向的单工通信,同一时刻必须只能一个方向传输,只需一根数据线.

    对于I2C通讯协议把它分为物理层和协议层物理层规定通讯系统中具有机械、电子功能部分的特性(硬件部分),确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准(软件层面)。

    二.I2C物理层

    I2C 通讯设备之间的常用连接方式

    在这里插入图片描述
    (1) 它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。

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

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

    什么是普通的开漏输出详情请参考–》GPIO端口的八种工作模式
    在这里插入图片描述

    开漏输出PMOS不工作
    1.当输出寄存器输出高电平,引脚输出高阻态相当于(开路),假设该引脚接到I2C的SDA总线上,则总线被默认拉成高电平。
    2.当输出寄存器输出低电平,引脚输出低电平。
    在这里插入图片描述
    在这里插入图片描述
    复用功能开漏输出

    复用功能模式中,输出使能,输出速度可配置,可工作在开漏模式, 但是输出信号源于其它外设(来自I2C外设),输出数据寄存器 GPIOx_ODR 无效;输入可用,可以通过输入数据寄存器可获取 I/O 实际状态,但一般直接用外设的寄存器来获取该数据信号

    这里SMT32,I2C外设的两个引脚SDA,SCL就要配置成复用功能的开漏输出模式,输出信号源于I2C外设。

    为什么引脚要设置成开漏模式
    以及为什么两根总线要上拉电阻接高电平,总线默认情况是高电平,详情看下图。
    在这里插入图片描述
    为什么要设备空闲的时候SDA与SCL引脚要输出高阻态(相当于断开与SDA与SCL总线的连接),根本目的就是为了不干扰其他正在通信的设备。

    (4) 多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线,也就是设备在发送数据之前会检测I2C总线是否忙碌(忙碌总线应该为低电平)。

    (5)I2C 具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。

    每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问的,地址也是一个数据,主机可以同过SDA发送这个地址出去,则挂载在总线上的设备会自行匹配,匹配成功之后就可以互相通信了

    三.I2C协议层

    STM32即可以作为主机,也可以做为从机,我主要介绍STM32作为主机如何进行读写数据。
    I2C规定通信时的时钟,起始信号,停止信号只能由主机产生

    下面以STM32做为主机,EEPROM存储器作为从机举例

    I2C 基本读写过程

    • 1.主机写数据到从机
      在这里插入图片描述
      这里发送完最后一个字节时,主机不一定要接收到从机发送的非应答信号才可以发送停止信号,就算从机应答了主机也可以直接发送停止信好终止通讯

    其中 S 表示由主机的 I2C 接口产生的传输起始信号(S),这时连接到 I2C 总线上的所有从机都会接收到这个信号。起始信号产生后,所有从机就开始等待主机紧接下来 广播(由SDA线传输数据)
    从机地址(SLAVE_ADDRESS)。在 I2C 总线上,每个设备的地址都是唯一的,当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号(引脚输出高阻态与两根总线断开连接)。

    根据 I2C 协议,这个从机地址可以是 7 位或 10 位,从机接收到匹配的地址后,主机或从机会返回一个应答(ACK)或非应答(NACK)信号,只有接收到应答信号后,主机才能继续发送或接收数据。

    在地址位之后,是传输方向的选择位,表示后面的数据传输方向
    该位为 0 时:主机向从机写数据。
    该位为 1 时:主机由从机读数据。

    • 2.主机向从机读取数据

    在这里插入图片描述
    记住,数据接收方要产生应答信号(代表我还要数据)或非应答信号(我不要要数据了),不一定就是主机或从机某一个产生。

    • 3.读和写数据混合格式
      在这里插入图片描述
      第一次通讯是确定读写从机设备内部寄存器或存储器的地址,第二次则是读或写上一次确定内部寄存器或存储器的地址上面的数据。

    1.空闲状态

    I2C总线的SDA和SCL两条信号线同时处于高电时,则为总线空闲状态,所有挂载在总线上的设备都输出高阻态(相当于断开与总线的连接),两条总线被上拉电阻的把电平拉高。

    2.起始信号与停止信号

    在这里插入图片描述
    起始信号:当SCL 线在高电平期间 SDA 线从高电平向低电平切换。
    停止信号:当SCL线在高电平期间 SDA 线由低电平向高电平切换

    注意:
    起始信号和停止信号是在SCL 是高电平期间,SDA线电平切换的过程,而不是单纯的高低电平。

    起始和停止信号只能由主机产生。

    3.数据有效性

    在这里插入图片描述
    在这里插入图片描述
    SDA数据线在 SCL 的每个时钟周期(时钟脉冲)传输一位数据。

    • SCL为高电平期间:SDA 表示的数据有效,此时SDA的电平要稳定,SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。

    • SCL为低电平期间:SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。

    在这里插入图片描述
    数据和地址按8位/字节进行传输,先传输数据的高位,每次传输的字节数不受限制。

    4.地址及数据方向

    I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,第 8 位或第 11 位。

    • 数据方向位为“1”:表示主机由从机读数据
    • 数据方向位为“0”:表示主机向从机写数据
      在这里插入图片描述

    读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线(向主机发送数据),主机接收信号,写数据方向时,SDA 由主机控制(向从机发送数据),从机接收信号。

    5.应答与非应答信号

    I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当数据接收端(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。

    在一个字节传输的8个时钟后的第9个时钟期间,接收器必须回送一个应答位(ACK)或者是非应答位(NACK)给发送器。
    在这里插入图片描述
    在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接收端控制 SDA,给发送端传输应答或非应答信号

    • SDA 为高电平:表示非应答信号(NACK)

    • SDA为低电平:表示应答信号(ACK)

    为什么数据发送端要释放 SDA 的控制权(将SDA总线置为高电平)
    在这里插入图片描述

    四.硬件I2C

    在讲硬件I2C之前不得不吐槽一下这个硬件I2C外设,有时候就突然会卡在某个事件的检测,需要关闭电源重新启动才有用,不过虽然可能硬件I2C可能会有问题,可能以后不一定用的到但是我们主要是学习如何用硬件实现I2C协议,对我们以后学别的协议肯定会有帮助。

    • 硬件 I2C:是指直接利用 STM32 芯片中的硬件 I2C 外设,该硬件 I2C 外设跟 USART串口外设类似,只要配置好对应的寄存器,外设就会产生标准串口协议的时序。使用它的I2C 外设则可以方便地通过外设寄存器来控制硬件I2C外设产生 I2C 协议方式的通讯,而不需要内核直接控制引脚的电平

    • 软件模拟I2C:即直接使用CPU内核按照 I2C 协议的要求控制GPIO输出高低电平。如控制产生 I2C 的起始信号时,先控制作为 SCL 线的 GPIO 引脚输出高电平,然后控制作为 SDA 线的GPIO引脚在此期间完成由高电平至低电平的切换,最后再控制SCL 线切换为低电平,这样就输出了一个标准的 I2C 起始信号。

    硬件 I2C 直接使用外设来控制引脚,可以减轻 CPU 的负担。不过使用硬件I2C 时必须使用某些固定的引脚作为 SCL 和 SDA,软件模拟 I2C 则可以使用任意 GPIO 引脚,相对比较灵活。

    I2C外设功能框图(重点)

    在这里插入图片描述

    1.通信引脚

    STM32中有两个I2C外设,硬件I2C必须要使用这些引脚,因为这些引脚才连接到I2C引脚,就比如说PB6与PB7引脚就连接到芯片内部的I2C1外设

    在这里插入图片描述
    就拿正点原子的STM32mini版为例,主机(stm32)使用PB6,PB7作为SCL与SDA引脚,但是PB6,PB7并没有连接到我们要通信的EEPROM的SCL,SDA引脚组成I2C总线,而是PC12与PC11连接到了EEPROM的SCL,SDA引脚,所以我们要把PB6与PB7引脚用杜邦线连接到PC12与PC11,这样就间接将PB6,PB7连接到EEPROM的SCL,SDA引脚上,组成I2C总线。

    这一步十分重要,如果你用的I2C1外设与EEPROM通信而没有把PB6,PB7连接到EEPROM的SCL,SDA引脚上不然你代码写出花来都没有用。
    原理图:
    在这里插入图片描述
    实物图:
    在这里插入图片描述

    2.时钟控制逻辑

    在这里插入图片描述
    时钟控制寄存器
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    这里解释一下为什么是用Tpclk1,因为I2C1外设是挂载在APB1总线上的
    在这里插入图片描述

    这里只是演示一下这么计算寄存器写入的值,用库函数我们只要配置好相应寄存器的参数,库函数会帮我计算自动写入的,不要慌。

    3.数据控制逻辑

    在这里插入图片描述

    • 当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过SDA信号线发送出去;

    • 当从外部接收数据的时候,数据移位寄存器把SDA信号线采样到的数据一位一位地存储到“数据寄存器”中。

    然后通过CPU或DMA向数据寄存器写入或者读出数据(一般保存在一个数组当中)。

    数据寄存器DR
    在这里插入图片描述
    自身地址寄存器1
    在这里插入图片描述
    在这里插入图片描述

    4.整体控制逻辑

    这里挑一些重点的寄存器位,我们只需配置好寄存器就可以让I2C外设硬件逻辑自动控制SDA,SCL总线去产生I2C协议的时序如:起始信号、应答信号、停止信号等等
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    接下来就是了解的知识:

    • 总线错误(BERR)

    一个地址或数据字节传输期间,当I2C接口检测到一个外部的停止或起始条件则产生总线错误。此时:

    ● BERR位被置位为’1’;如果设置了ITERREN位,则产生一个中断;
    ● 在从模式情况下,数据被丢弃,硬件释放总线:
    ─ 如果是错误的开始条件,从设备认为是一个重启动,并等待地址或停止条件。
    ─ 如果是错误的停止条件,从设备按正常的停止条件操作,同时硬件释放总线。
    ● 在主模式情况下,硬件不释放总线,同时不影响当前的传输状态。此时由软件决定是否要中止当前的传输

    在这里插入图片描述
    主机模式与从机模式
    在这里插入图片描述

    • 应答错误(AF)

    当STM32检测到一个无应答位时,产生应答错误。此时:

    ● AF位被置位,如果设置了ITERREN位,则产生一个中断;
    ● 当发送器接收到一个NACK时,必须复位通讯:
    ─ 如果是处于从模式,硬件释放总线。
    ─ 如果是处于主模式,软件必须生成一个停止条件

    在这里插入图片描述

    • 过载/欠载错误(OVR)

    从模式下,如果禁止时钟延长,I2C接口正在接收数据时,当它已经接收到一个字节(RxNE=1),但在DR寄存器中前一个字节数据还没有被读出,则发生过载错误。此时:
    ● 最后接收的数据被丢弃;
    ● 在过载错误时,软件应清除RxNE位,发送器应该重新发送最后一次发送的字节。

    从模式下,如果禁止时钟延长,I2C接口正在发送数据时,在下一个字节的时钟到达之前,新的数据还未写入DR寄存器(TxE=1),则发生欠载错误。此时:
    ● 在DR寄存器中的前一个字节将被重复发出
    ● 用户应该确定在发生欠载错时,接收端应丢弃重复接收到的数据。发送端应按I2C总线标准在规定的时间更新DR寄存器。
    在发送第一个字节时,必须在清除ADDR之后并且第一个SCL上升沿之前写入DR寄存器;如果不能做到这点,则接收方应该丢弃第一个数据

    STM32做为从机时写入数据和读出数据时应该连续,取个例子主机要10个字节的数据而你只发5个字节此时就发生欠载错误:在下一个字节的时钟到达之前,新的数据还未写入DR寄存器

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    5.STM32的I2C外设通信过程(超级重要)

    I2C模式选择:
    接口可以下述4种模式中的一种运行:
    ● 从发送器模式
    ● 从接收器模式
    ● 主发送器模式
    ● 主接收器模式

    该模块默认地工作于从模式。接口在生成起始条件后自动地从从模式切换到主模式;当仲裁丢失或产生停止信号时,则从主模式切换到从模式。允许多主机功能。

    • 主模式:STM32作为主机通信(发送器与接收器)
    • 从模式:STM32作为从机通信(发送器与接收器)

    这里我主要将STM32做为主机通信

    I2C主模式:
    默认情况下,I2C接口总是工作在从模式。从从模式切换到主模式,需要产生一个起始条件。

    在主模式时,I2C接口启动数据传输并产生时钟信号。串行数据传输总是以起始条件开始并以停止条件结束。当通过START位在总线上产生了起始条件,设备就进入了主模式

    主发送器

    在这里插入图片描述

    • EV5事件
      在这里插入图片描述

    起始条件当BUSY=0时,设置START=1,I2C接口将产生一个开始条件并切换至主模式(M/SL位置位)
    在这里插入图片描述
    一旦发出开始条件,我们需要检测SB是否置1,判断是否成功发送起始信号

    在这里插入图片描述
    ● SB位被硬件置位,如果设置了ITEVFEN位,则会产生一个中断。
    然后主设备等待读SR1寄存器,紧跟着将从地址写入DR寄存器

    • EV6事件
      在这里插入图片描述

    从机地址的发送

    ● 在7位地址模式时,只需送出一个地址字节。
    一旦该地址字节被送出,
    ─ ADDR位被硬件置位,如果设置了ITEVFEN位,则产生一个中断。
    随后主设备等待一次读SR1寄存器,跟着读SR2寄存器。

    根据送出从地址的最低位,主设备决定进入发送器模式还是进入接收器模式
    ● 在7位地址模式时,
    ─ 要进入发送器模式,主设备发送从地址时置最低位为’0’。
    ─ 要进入接收器模式,主设备发送从地址时置最低位为’1’

    在这里插入图片描述
    从机地址发送完成从机应答之后检测EV6事件:
    在这里插入图片描述
    确保从机应答,之后才传输下一个数据,如果你不检测万一地址发送失败或者从机无应答,直接就开始传输数据那传给谁??

    • EV8_1事件:
      在这里插入图片描述
      这个检测是地址发送完之后进行检测,其实我们只要检测EV6事件就可以了,因为EV6事件成功之后就已经代表地址(数据)发送出去,而且从机还应答了,地址已经发送完成那肯定数据寄存器,与移位寄存器肯定为空呐,所以不检测也可以。

    • EV8事件
      在这里插入图片描述
      在这里插入图片描述
      我们在发送完一个数据之后必须判断数据寄存器是否为空,数据寄存器为空(TXE),才能向数据寄存器写入新的数据,不然上一个数据们还没有转移到移位寄存器,CPU又写入一个数据则会覆盖上一个数据。

    在这里插入图片描述

    • EV8_2事件
      在这里插入图片描述
      在我们发送完最后一个字节之后我们应该检测EV8_2事件,主要检测BTF位。
      在这里插入图片描述
      为什么呢,主要是检测数据移位寄存器的数据全部发送完成,则才算最后一个字节全部发送完毕

    • 关闭通信

    在DR寄存器中写入最后一个字节后,通过设置STOP位产生一个停止条件,然后I2C接口将自动回到从模式(M/S位清除)。

    主接收器

    在这里插入图片描述
    因为虽然STM32做为接收器,但是STM32是主机,起始信号与发送从机地址都是必须由主机干的活,所以前面EV5,EV6,EV6_1事件与主接收器是一模一样

    • EV7事件
      在这里插入图片描述
      主机使能ACK位就可以自动接收完数据产生应答信号。

    在这里插入图片描述
    接收数据之前,判断数据寄存器是否有数据,也就数据寄存器非空(RNXE),CPU就可以读取数据寄存器中的数据啦。

    • EV7_1事件
      关闭通信
      主设备在从设备接收到最后一个字节后发送一个NACK。接收到NACK后,从设备释放对SCL和SDA线的控制;主设备就可以发送一个停止/重起始条件。
      ● 为了在收到最后一个字节后产生一个NACK脉冲,在读倒数第二个数据字节之后(在倒数第二个RxNE事件之后)必须清除ACK位。
      ● 为了产生一个停止/重起始条件,软件必须在读倒数第二个数据字节之后(在倒数第二个RxNE事件之后)设置STOP/START位。

      ● 只接收一个字节时,刚好在EV6之后(EV6_1时,清除ADDR之后)要关闭应答和停止条件的产生位。在产生了停止条件后,I2C接口自动回到从模式(M/SL位被清除)

    这里产生一个NACK其实就是清除ACK位,将ACK位置0,后面接收的一个字节不在产生应答就是非应答咯
    在这里插入图片描述
    然后主机产生停止信号
    在这里插入图片描述
    然后通过判断EV7事件,CPU向数据寄存器读取最后一个字节数据

    硬件I2C写代码必须熟练掌握和理解主发送器和主接收器的过程,只要你理解了写代码还不是信手拈来,简简单单,然后写代码你会发送就是上面的过程一模一样

    6.I2C初始化结构体

    在这里插入图片描述

    • I2C_ClockSpeed

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

    CCR寄存器不能写入小数类型的时钟因子,影响到SCL的实际频率可能会低于本成员设置的参数值,这时除了通讯稍慢一点以外,不会对I2C的标准通讯造成其它影响。

    在这里插入图片描述
    初始化函数
    在这里插入图片描述

    • I2C_Mode

    选择I2C的使用方式,有I2C模式(I2C_Mode_I2C )和SMBus主、从模式(I2C_Mode_SMBusHost、 I2C_Mode_SMBusDevice ) 。
    在这里插入图片描述

    • I2C_DutyCycle

    设置I 2 C的SCL线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2:1 ( I2C_DutyCycle_2)和16:9(I2C_DutyCycle_16_9)。
    这个模式随便选反正区别不大。
    在这里插入图片描述

    • I2C_OwnAddress1

    配置STM32的I2C设备自己的地址,每个连接到I2C总线上的设备都要有一个自己的地址,作为主机也不例外。

    地址可设置为7位或10位,只要该地址是I2C总线上唯一的即可。
    其实可以有两个地址,这里是设置的第一个地址。

    第二个地址要另外用库函数设置而且只能是7位
    在这里插入图片描述

    • I2C_Ack_Enable

    配置I 2 C应答是否使能,设置为使能则可以发送响应信号。一般配置为允许应答(I2C_Ack_Enable)若STM32接收一个字节数据自动产生应答,必须要使能
    在这里插入图片描述

    • I2C_AcknowledgeAddress

    选择I2C的寻址模式是7位还是10位地址。这需要根据实际连接到I2C总线上设备的地址进行选择,这个成员的配置也影响到I2C_OwnAddress1成员,只有这里设置成10位模式时,I2C_OwnAddress1才支持10位地址。
    在这里插入图片描述
    配置完成之后调用一下I2C初始化函数就搞定

    记得使能I2C外设
    在这里插入图片描述
    在这里插入图片描述

    五.EEPROM简介

    EEPROM全称: electrically-erasable, and programmable read-only memory --》可电擦除的可编程的只读存储器,这里的只读并不是只能读,是以前ROM不能写只能读,现在的EEPROM已经是可读写的啦,为什么还叫可读:只不过是保留下来的名字而已。

    在这里插入图片描述
    原理图:
    在这里插入图片描述
    在这里插入图片描述
    WP引脚直接
    在这里插入图片描述

    EEPROM的设备地址(作为从机)
    在这里插入图片描述

    EEPROM中硬件I2C
    在这里插入图片描述
    EEPROM通信的时候也遵循I2C协议,向产生起始信号,停止信号,应答什么的都一样的。

    1.STM32向从机EEPROM写入一个字节

    在这里插入图片描述

    2.STM32向从机EEPROM写入多个字节(页写入)

    在这里插入图片描述
    写入的8个字节是连续的地址,不连续的话不能使用页写入
    在这里插入图片描述
    在这里插入图片描述

    总结:

    • 进行页写入时,写入的存储器地址要对齐到8,也就是说只能写入地址为 0 8 16 32… 能整除8
    • 页写如只能一次写入8个字节

    规定就是规定我也没有办法,不然就会出错

    • 确认EEPROM是否写入完成:

    在这里插入图片描述
    这段话什么意思呢:EEPROM做为我们的非易失存储器(掉电不会丢失数据),相当于我们电脑中的硬盘,它的读写速度是非常慢的,所以STM32把数据发送过去之后,必须等待EEPROM去把数据写入自己内部的存储器才能写入下一波数据(可以是单字节写入也可以是页写入),如果不等待EEPROM把上一次的数据写完又去写入EEPROM是不会搭理你的,也就是说EEPROM处于忙碌状态。

    检测EEPROM数据是否写入完成:
    STM32主机不断向EEPROM发送起始信号,然后发送EEPROM的设备的地址等待EEPROM的应答信号,如果不应答,重复在来一遍,直到EEPROM应答则代表EEPROM上一次的数据写入完成,然后才可以传输下一次的数据!!!

    3.STM32随机读取EEPROM内部任何地址的数据

    在这里插入图片描述
    在这里插入图片描述

    4.STM32随机顺序读取EEPROM内部任何地址的数据

    在这里插入图片描述
    EEPROM一共有256个字节对应的地址为(0~255)
    当读取到最后一个字节,也就是255地址,第256个字节,在读取又会从头(第一个字节数据)开始读取。

    六.硬件I2C读写EEPROM实验

    实验目的

    STM32作为主机向从机EEPROM存储器写入256个字节的数据
    STM32作为主机向从机EEPROM存储器读取写入的256个字节的数据

    读写成功亮绿灯,读写失败亮红灯

    实验原理

    • 硬件设计
      原理图
      在这里插入图片描述
      实物图
      在这里插入图片描述

    编程要点
    (1) 配置通讯使用的目标引脚为开漏模式;
    (2) 编写模拟 I2C 时序的控制函数;
    (3) 编写基本 I2C 按字节收发的函数;
    (4) 编写读写 EEPROM 存储内容的函数;
    (5) 编写测试程序,对读写数据进行校验。

    两个引脚PB6,PB7都要配置成复用的开漏输出
    这里有一个注意的点,你配置成输出模式,并不会影响引脚的输入功能

    详情请看——>GPIO端口的八种工作模式
    在这里插入图片描述

    源码

    i2c_ee.h
    前面理论已经讲得已经很详细了,直接上代码叭!!

    #ifndef __IIC_EE_H
    #define __IIC_EE_H
    
    #include "stm32f10x.h"
    #include <stdio.h>
    //IIC1
    #define  EEPROM_I2C                       I2C1
    #define  EEPROM_I2C_CLK                   RCC_APB1Periph_I2C1
    #define  EEPROM_I2C_APBxClkCmd            RCC_APB1PeriphClockCmd
    #define  EEPROM_I2C_BAUDRATE              400000
    
    // IIC1 GPIO 引脚宏定义
    #define  EEPROM_I2C_SCL_GPIO_CLK           (RCC_APB2Periph_GPIOB)
    #define  EEPROM_I2C_SDA_GPIO_CLK           (RCC_APB2Periph_GPIOB)
    #define  EEPROM_I2C_GPIO_APBxClkCmd        RCC_APB2PeriphClockCmd
         
    #define  EEPROM_I2C_SCL_GPIO_PORT         GPIOB   
    #define  EEPROM_I2C_SCL_GPIO_PIN          GPIO_Pin_6
    #define  EEPROM_I2C_SDA_GPIO_PORT         GPIOB
    #define  EEPROM_I2C_SDA_GPIO_PIN          GPIO_Pin_7
    
    //STM32自身地址1 与从机设备地址不相同即可(7位地址)
    #define   STM32_I2C_OWN_ADDR             0x6f
    //EEPROM设备地址
    #define   EEPROM_I2C_Address             0XA0
    #define   I2C_PageSize                     8
    
    
    //等待次数
    #define I2CT_FLAG_TIMEOUT         ((uint32_t)0x1000)
    #define I2CT_LONG_TIMEOUT         ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))
    
    
    
    /*信息输出*/
    #define EEPROM_DEBUG_ON                    0
    #define EEPROM_INFO(fmt,arg...)           printf("<<-EEPROM-INFO->> "fmt"\n",##arg)
    #define EEPROM_ERROR(fmt,arg...)          printf("<<-EEPROM-ERROR->> "fmt"\n",##arg)
    #define EEPROM_DEBUG(fmt,arg...)          do{\
                                              if(EEPROM_DEBUG_ON)\
                                              printf("<<-EEPROM-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
                                              }while(0)
    
    void I2C_EE_Config(void);
    void EEPROM_Byte_Write(uint8_t addr,uint8_t data);	
    uint32_t  EEPROM_WaitForWriteEnd(void);	
    uint32_t  EEPROM_Page_Write(uint8_t addr,uint8_t *data,uint16_t Num_ByteToWrite);																					
    uint32_t  EEPROM_Read(uint8_t *data,uint8_t addr,uint16_t Num_ByteToRead);
    void I2C_EE_BufferWrite(uint8_t* pBuffer,uint8_t WriteAddr, uint16_t NumByteToWrite);
    #endif /* __IIC_EE_H */
    
    

    i2c_ee.c

    #include "i2c_ee.h"
    
    
    //设置等待时间
    static __IO uint32_t  I2CTimeout = I2CT_LONG_TIMEOUT;   
    
    //等待超时,打印错误信息
    static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode);
    
    
    void I2C_EE_Config(void)
    {
    	GPIO_InitTypeDef    GPIO_InitStuctrue;
    	I2C_InitTypeDef     I2C_InitStuctrue;
    	//开启GPIO外设时钟
    	EEPROM_I2C_GPIO_APBxClkCmd(EEPROM_I2C_SCL_GPIO_CLK|EEPROM_I2C_SDA_GPIO_CLK,ENABLE);
    	//开启IIC外设时钟
    	EEPROM_I2C_APBxClkCmd(EEPROM_I2C_CLK,ENABLE);
    	
    	//SCL引脚-复用开漏输出
      GPIO_InitStuctrue.GPIO_Mode=GPIO_Mode_AF_OD;
      GPIO_InitStuctrue.GPIO_Pin=EEPROM_I2C_SCL_GPIO_PIN;
    	GPIO_InitStuctrue.GPIO_Speed=GPIO_Speed_50MHz;
    	GPIO_Init(EEPROM_I2C_SCL_GPIO_PORT,&GPIO_InitStuctrue);
    	//SDA引脚-复用开漏输出
    	GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_AF_OD;
    	GPIO_InitStuctrue.GPIO_Pin = EEPROM_I2C_SDA_GPIO_PIN;
    	GPIO_InitStuctrue.GPIO_Speed=GPIO_Speed_50MHz;
    	GPIO_Init(EEPROM_I2C_SDA_GPIO_PORT,&GPIO_InitStuctrue);
    	
    	//IIC结构体成员配置
       I2C_InitStuctrue.I2C_Ack=I2C_Ack_Enable;
    	I2C_InitStuctrue.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
    	I2C_InitStuctrue.I2C_ClockSpeed=EEPROM_I2C_BAUDRATE;
    	I2C_InitStuctrue.I2C_DutyCycle=I2C_DutyCycle_2;
    	I2C_InitStuctrue.I2C_Mode=I2C_Mode_I2C;
    	I2C_InitStuctrue.I2C_OwnAddress1=STM32_I2C_OWN_ADDR;
    	I2C_Init(EEPROM_I2C,&I2C_InitStuctrue);
    	I2C_Cmd(EEPROM_I2C,ENABLE);
    
    }
    
    //向EEPROM写入一个字节
    void  EEPROM_Byte_Write(uint8_t addr,uint8_t data)
    {
    	//发送起始信号
    	I2C_GenerateSTART(EEPROM_I2C,ENABLE);
    	//检测EV5事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
    	//发送设备写地址
    	I2C_Send7bitAddress(EEPROM_I2C,EEPROM_I2C_Address,I2C_Direction_Transmitter);
    	//检测EV6事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
    	//发送要操作设备内部的地址
    	I2C_SendData(EEPROM_I2C,addr);
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTING )==ERROR);
      I2C_SendData(EEPROM_I2C,data);
    	//检测EV8_2事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED )==ERROR);
    	//发送停止信号
    	I2C_GenerateSTOP(EEPROM_I2C,ENABLE);
    	
    }
    
    //向EEPROM写入多个字节
    uint32_t  EEPROM_Page_Write(uint8_t addr,uint8_t *data,uint16_t Num_ByteToWrite)
    {
    	
    	 I2CTimeout = I2CT_LONG_TIMEOUT;
    	//判断IIC总线是否忙碌
    	while(I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))   
    	{
    		if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
    	} 
    	//重新赋值
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//发送起始信号
    	I2C_GenerateSTART(EEPROM_I2C,ENABLE);
    	//检测EV5事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_MODE_SELECT)==ERROR)
    	{
    		 if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
    	} 
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//发送设备写地址
    	I2C_Send7bitAddress(EEPROM_I2C,EEPROM_I2C_Address,I2C_Direction_Transmitter);
    	//检测EV6事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR)
    	{
    		 if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
    	} 
    
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//发送要操作设备内部的地址
    	I2C_SendData(EEPROM_I2C,addr);
    	//检测EV8事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTING )==ERROR)
    	{
    		 if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);
    	} 
    
    	while(Num_ByteToWrite)
    	{
    		I2C_SendData(EEPROM_I2C,*data);
    		I2CTimeout = I2CT_FLAG_TIMEOUT;
    		while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTING )==ERROR)
    		{
    				if((I2CTimeout--) == 0) return   I2C_TIMEOUT_UserCallback(5);
    		} 
    		 Num_ByteToWrite--;
    		 data++;
    	}
    
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//检测EV8_2事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED )==ERROR)
    	{
    				if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
    	 } 
    	//发送停止信号
    	I2C_GenerateSTOP(EEPROM_I2C,ENABLE);
    	 return 1;
    }
    
    //向EEPROM读取多个字节
    uint32_t EEPROM_Read(uint8_t *data,uint8_t addr,uint16_t Num_ByteToRead)
    {
    	 I2CTimeout = I2CT_LONG_TIMEOUT;
      //判断IIC总线是否忙碌
      while(I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))   
      {
        if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
      } 
    	
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//发送起始信号
    	I2C_GenerateSTART(EEPROM_I2C,ENABLE);
    	//检测EV5事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_MODE_SELECT )==ERROR)
      {
            if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
       } 
    	
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//发送设备写地址
    	I2C_Send7bitAddress(EEPROM_I2C,EEPROM_I2C_Address,I2C_Direction_Transmitter);
    	//检测EV6事件等待从机应答
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED )==ERROR)
     {
            if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);
      }
      
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//发送要操作设备内部存储器的地址
    	I2C_SendData(EEPROM_I2C,addr);
    	//检测EV8事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTING )==ERROR)
     {
            if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
      }
    	I2CTimeout = I2CT_FLAG_TIMEOUT;
    	//发送起始信号
    	I2C_GenerateSTART(EEPROM_I2C,ENABLE);
    	//检测EV5事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_MODE_SELECT )==ERROR)
    	{
            if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
       }
    	I2CTimeout = I2CT_FLAG_TIMEOUT;	 
    	//发送设备读地址
    	I2C_Send7bitAddress(EEPROM_I2C,EEPROM_I2C_Address,I2C_Direction_Receiver);
    	//检测EV6事件
    	while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED )==ERROR)
    	{
           if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
       }
    	 
    	while(Num_ByteToRead--)
    	{
    		//是否是最后一个字节,若是则发送非应答信号
    		if( Num_ByteToRead==0)
    	 {
    		 //发送非应答信号
    		 I2C_AcknowledgeConfig(EEPROM_I2C,DISABLE);
    		 //发送停止信号
    	   I2C_GenerateSTOP(EEPROM_I2C,ENABLE);
    	 }
    	 
    	 I2CTimeout = I2CT_FLAG_TIMEOUT;	 
    	 //检测EV7事件
       while( I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_RECEIVED )==ERROR)
      {
           if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
       }
    	 
        *data=I2C_ReceiveData(EEPROM_I2C);
    	  data++; 
    	 
    	}
    	
    	//重新开启应答信号
    	I2C_AcknowledgeConfig(EEPROM_I2C,ENABLE);
      return 1;
    }
    void I2C_EE_BufferWrite(uint8_t* pBuffer,uint8_t WriteAddr, uint16_t NumByteToWrite)
    {
      u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
      //I2C_PageSize=8
      Addr = WriteAddr % I2C_PageSize;
      count = I2C_PageSize - Addr;
      NumOfPage =  NumByteToWrite / I2C_PageSize;
      NumOfSingle = NumByteToWrite % I2C_PageSize;
     
      /* 写入数据的地址对齐,对齐数为8 */
      if(Addr == 0) 
      {
        /* 如果写入的数据个数小于8 */
        if(NumOfPage == 0) 
        {
          EEPROM_Page_Write(WriteAddr, pBuffer, NumOfSingle);
          EEPROM_WaitForWriteEnd();
        }
        /* 如果写入的数据个数大于8 */
        else  
        {
    			//按页写入
          while(NumOfPage--)
          {
            EEPROM_Page_Write(WriteAddr, pBuffer, I2C_PageSize); 
        	  EEPROM_WaitForWriteEnd();
            WriteAddr +=  I2C_PageSize;
            pBuffer += I2C_PageSize;
          }
          //不足一页(8个)单独写入
          if(NumOfSingle!=0)
          {
            EEPROM_Page_Write(WriteAddr, pBuffer, NumOfSingle);
            EEPROM_WaitForWriteEnd();
          }
        }
      }
      /*写的数据的地址不对齐*/
      else 
      {
          NumByteToWrite -= count;
          NumOfPage =  NumByteToWrite / I2C_PageSize;
          NumOfSingle = NumByteToWrite % I2C_PageSize;	
          
          if(count != 0)
          {  
            EEPROM_Page_Write(WriteAddr, pBuffer, count);
            EEPROM_WaitForWriteEnd();
            WriteAddr += count;
            pBuffer += count;
          } 
          
          while(NumOfPage--)
          {
            EEPROM_Page_Write(WriteAddr, pBuffer, I2C_PageSize);
            EEPROM_WaitForWriteEnd();
            WriteAddr +=  I2C_PageSize;
            pBuffer += I2C_PageSize;  
          }
          if(NumOfSingle != 0)
          {
            EEPROM_Page_Write(WriteAddr, pBuffer, NumOfSingle); 
            EEPROM_WaitForWriteEnd();
          }
        } 
    }
    
    uint32_t EEPROM_WaitForWriteEnd(void)
    {
    	I2CTimeout = I2CT_FLAG_TIMEOUT;	
    	
    	do
    	{
    		  I2CTimeout = I2CT_FLAG_TIMEOUT;
    			//发送起始信号
    			I2C_GenerateSTART(EEPROM_I2C,ENABLE);
    			//检测EV5事件
    			while( I2C_GetFlagStatus(EEPROM_I2C,I2C_FLAG_SB )==RESET)
    			{
    					 if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
    			 }
    			I2CTimeout = I2CT_FLAG_TIMEOUT;	
    			//发送设备写地址
    			I2C_Send7bitAddress(EEPROM_I2C,EEPROM_I2C_Address,I2C_Direction_Transmitter);
    		
    	}while( (I2C_GetFlagStatus(EEPROM_I2C,I2C_FLAG_ADDR )==RESET) && (I2CTimeout--) );
    	
    	//发送停止信号
    	I2C_GenerateSTOP(EEPROM_I2C,ENABLE);
    	return 1;
    }
    
    
    
    static  uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
    {
      /* Block communication and all processes */
      EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
      
      return 0;
    }
    
    

    main.c

    #include "stm32f10x.h"
    #include "led.h"
    #include  "./i2c/i2c_ee.h"
    #include  <string.h>
    #include "usart.h"
    #define SOFT_DELAY Delay(0x0FFFFF);
    
    void Delay(__IO u32 nCount); 
    
    //声明I2C测试函数
    uint8_t I2C_EE_Test(void);
    int main(void)
    {	
    	//初始化IIC
       I2C_EE_Config();
       //初始化USART 
       Usart_Config();
    	//初始化LED
       LED_GPIO_Config();
    	printf("\r\nIIC读写EEPROM测试实验\r\n");
    	
    	//读写成功亮绿灯,失败亮红灯
       if( I2C_EE_Test()==1 )
    	 {
    		 LED_G(NO);
    	 }
    	 else
    	 {
    		 LED_R(NO);
    	 }
    	
    while(1)
    {
    ;
    }
     
     }
    	 uint8_t I2C_EE_Test(void)
    	 {	
    		  uint8_t ReadData[256]={0};
          uint8_t WriteDdta[256]={0};
    		  uint16_t i;
    		  //初始化写入数组
    		   for(i=0;i<256;i++)
    	    {
    		    WriteDdta[i]=i; 
    	     }
    			 //向EEPROM从地址为0开始写入256个字节的数据 
    				I2C_EE_BufferWrite(WriteDdta,0,256);
    				//等待EEPROM写入数据完成 
    				EEPROM_WaitForWriteEnd();	 
    			 //向EEPROM从地址为0开始读出256个字节的数据
    				EEPROM_Read(ReadData,0,256);
    
    			 for (i=0; i<256; i++)
    				{	
    				 if(ReadData[i] != WriteDdta[i])
    					{
    						EEPROM_ERROR("0x%02X ", ReadData[i]);
    						EEPROM_ERROR("错误:I2C EEPROM写入与读出的数据不一致\n\r");
    						return 0;
    					}
    					 printf("0x%02X ", ReadData[i]);
    					 if(i%16 == 15)    
    					 printf("\n\r");   
    				}
    				EEPROM_INFO("I2C(AT24C02)读写测试成功\n\r");
    				return 1;
    	 }
    
    void Delay(__IO uint32_t nCount)	 //简单的延时函数
    {
    	for(; nCount != 0; nCount--);
    }
    
    

    重点讲一下,如何解决以下页写入问题,实现连续写入

    • 进行页写入时,写入的存储器地址要对齐到8,也就是说只能写入地址为 0 8 16 32… 能整除8
    • 页写如只能一次写入8个字节

    现在来解释代码中下图函数如何解决问题
    在这里插入图片描述

    如果地址对齐:
    在这里插入图片描述
    在这里插入图片描述
    如果地址不对齐:
    在这里插入图片描述

    实验效果

    请添加图片描述

    七.软件模式I2C协议

    实验目的

    STM32作为主机向从机EEPROM存储器写入256个字节的数据
    STM32作为主机向从机EEPROM存储器读取写入的256个字节的数据

    读写成功亮绿灯,读写失败亮红灯

    实验原理

    在这里插入图片描述
    软件模式I2C由我们CPU来控制引脚产生I2C时序,所以我们随便选引脚都可以,不过你选择的引脚肯定要连接到通信的EEPROM的SCL,SDA引脚上。这里是用了PC12,PC11充当主机STM32SCL,SDA引脚。

    • 主机产生起始信号
      在这里插入图片描述
    • 主机产生停止信号
      在这里插入图片描述
    • 主机产生应答信号或非应答信号
      在这里插入图片描述
      在这里插入图片描述
    • 等待从机EEPROM应答

    在这里插入图片描述

    • 主机发送一个字节给从机
      在这里插入图片描述
    • 主机向EEPROM接收一个字节
      在这里插入图片描述
      value应该初始化为0,我忘了sorry

    源码

    i2c_gpio.h

    #ifndef _I2C_GPIO_H
    #define _I2C_GPIO_H
    
    
    #include "stm32f10x.h"
    
    #define EEPROM_I2C_WR	0		/* 写控制bit */
    #define EEPROM_I2C_RD	1		/* 读控制bit */
    
    #define EEPROM_GPIO_PORT_I2C         GPIOB
    #define EEPROM_RCC_I2C_PORT          RCC_APB2Periph_GPIOB
    #define EEPROM_I2C_SCL_PIN           GPIO_Pin_6
    #define EEPROM_I2C_SDA_PIN           GPIO_Pin_7
    
    /*当 STM32 的 GPIO 配置成开漏输出模式时,它仍然可以通过读取
    GPIO 的输入数据寄存器获取外部对引脚的输入电平,也就是说它同时具有浮空输入模式的
    功能*/
    
    #define EEPROM_I2C_SCL_1()  EEPROM_GPIO_PORT_I2C->BSRR |= EEPROM_I2C_SCL_PIN		/* SCL = 1 */
    #define EEPROM_I2C_SCL_0()  EEPROM_GPIO_PORT_I2C->BRR  |= EEPROM_I2C_SCL_PIN		/* SCL = 0 */
    	
    #define EEPROM_I2C_SDA_1()  EEPROM_GPIO_PORT_I2C->BSRR |= EEPROM_I2C_SDA_PIN		/* SDA = 1 */
    #define EEPROM_I2C_SDA_0()  EEPROM_GPIO_PORT_I2C->BRR  |= EEPROM_I2C_SDA_PIN		/* SDA = 0 */
    
    #define EEPROM_I2C_SDA_READ()  ((EEPROM_GPIO_PORT_I2C->IDR & EEPROM_I2C_SDA_PIN)!=0 )	/* 读SDA口线状态 */
    
    
    void i2c_Start(void);
    void i2c_Stop(void);
    void i2c_Ack(void);
    void i2c_NAcK(void);
    uint8_t i2c_WaitAck(void);
    void i2c_SendByte(uint8_t data);
    uint8_t i2c_ReadByte(void);
    uint8_t i2c_CheckDevice(uint8_t Address);
    #endif  /* _I2C_GPIO_H */
    
    

    i2c_gpio.c

    #include "i2c_gpio.h"
    
    #include  "stm32f10x.h"
    
    void I2c_gpio_config(void)
    {
    	GPIO_InitTypeDef  GPIO_InitStructure;
    	RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE);
    	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
    	GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;
    	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    	GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure);
    	
    	/* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
    	i2c_Stop();
    }
    
    static void i2c_Delay(void)
    {
    	uint8_t i;
    	for(i=0;i<10;i++)
    	{
    	}
    }
    
    
    void i2c_Start(void)
    {
    	EEPROM_I2C_SCL_1();
    	EEPROM_I2C_SDA_1();
    	i2c_Delay();
    	EEPROM_I2C_SDA_0();
    	i2c_Delay();
    	EEPROM_I2C_SCL_0();
    	i2c_Delay();
    }
    
    void i2c_Stop(void)
    {
    	EEPROM_I2C_SDA_0();
    	EEPROM_I2C_SCL_1();
    	i2c_Delay();
    	EEPROM_I2C_SDA_1();
    	i2c_Delay();
    }
    
    void i2c_Ack(void)
    {
    	EEPROM_I2C_SCL_0();
    	i2c_Delay();
    	EEPROM_I2C_SDA_0();
    	i2c_Delay();
    	EEPROM_I2C_SCL_1();
    	i2c_Delay();
    	EEPROM_I2C_SCL_0();
    	i2c_Delay();
    	EEPROM_I2C_SDA_1();
    	i2c_Delay();
    
    }
    
    void i2c_NAcK(void)
    {
    	EEPROM_I2C_SDA_1();
    	i2c_Delay();
    	EEPROM_I2C_SCL_1();
    	i2c_Delay();
    	EEPROM_I2C_SCL_0();
    	i2c_Delay();
    
    }
    
    uint8_t i2c_WaitAck(void)
    {
    	uint8_t ret;
    	EEPROM_I2C_SDA_1();
    	EEPROM_I2C_SCL_1();
    	i2c_Delay();
    	if( EEPROM_I2C_SDA_READ() )
    	{
    		ret=1;
    	}
    	else
    	{
    		ret=0;
    	}
    	EEPROM_I2C_SCL_0();
    	i2c_Delay();
      return ret;
    
    }
    	
    
    void i2c_SendByte(uint8_t data)
    {
    	uint8_t i;
    	for(i=0;i<8;i++)
    	{
    		if( data&0x80 )
    	 {
    		  EEPROM_I2C_SDA_1();
    	 }
    	 else
    	 {
    		  EEPROM_I2C_SDA_0();
    	 }
    	 i2c_Delay();
    	 EEPROM_I2C_SCL_1();
    	 i2c_Delay();
    	 EEPROM_I2C_SCL_0();
    	 i2c_Delay();
    	 if( i==7 )
    	 {
    		 EEPROM_I2C_SDA_1();
    		 i2c_Delay();
    	 }
    	 data=data<<1;
    	}
    	
    }
    
    uint8_t i2c_ReadByte(void)
    {
    	uint8_t value=0;
    	uint8_t i;
    	for(i=0;i<8;i++)
    	{
    		value=value<<1;
    		EEPROM_I2C_SCL_1();
    	  i2c_Delay();
    		if( EEPROM_I2C_SDA_READ() )
    	  {
    	 	  value++;
    	  }
    	  EEPROM_I2C_SCL_0();
    	  i2c_Delay();
    	}
    	return value;
    }
    
    uint8_t i2c_CheckDevice(uint8_t Address)
    {
    	uint8_t ucACK;
    	I2c_gpio_config();
    	i2c_Start();
    	i2c_SendByte(Address|EEPROM_I2C_WR);
    	ucACK=i2c_WaitAck();
    	i2c_Stop();
      return ucACK;	
    	
    }
    
    
    

    i2c_ee.h

    #ifndef _I2C_EE_H
    #define _I2C_EE_H
    
    
    #include "stm32f10x.h"
    
    
    #define EEPROM_DEV_ADDR			0xA0		/* 24xx02的设备地址 */
    #define EEPROM_PAGE_SIZE		  8			  /* 24xx02的页面大小 */
    #define EEPROM_SIZE				  256			  /* 24xx02总容量 */
    
    
    uint8_t ee_Checkok(void);
    uint8_t  ee_ReadByte( uint8_t *pReaddata,uint16_t Address,uint16_t num );
    uint8_t  ee_WriteByte( uint8_t *Writepdata,uint16_t Address,uint16_t num );
    uint8_t ee_WaitStandby(void);
    uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize);
    uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize);
    uint8_t ee_Test(void) ;
    #endif  /* _I2C_EE_H*/
    
    

    i2c_ee.c

    #include "i2c_ee.h"
    #include "i2c_gpio.h"
    
    //检测EEPORM是否忙碌
    uint8_t ee_Checkok(void)
    {
    	if(i2c_CheckDevice(EEPROM_DEV_ADDR)==0)
    	{
    		return 1;
    	}
    	else
    	{
        i2c_Stop();  
    		return 0;
     	}
    }	
    //检测EEPROM写入数完成
    uint8_t ee_WaitStandby(void)
    {
    	uint32_t wait_count = 0;
    	
    	while(i2c_CheckDevice(EEPROM_DEV_ADDR))
    	{
    		//若检测超过次数,退出循环
    		if(wait_count++>0xFFFF)
    		{
    			//等待超时
    			return 1;
    		}
    	}
    	//等待完成
    	return 0;
    }
    
    
    //向EEPROM写入多个字节
    uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
    {
    	uint16_t i,m;
    	uint16_t addr;
    	addr=_usAddress;
      for(i=0;i<_usSize;i++)
    	{
    		  //当第一次或者地址对齐到8就要重新发起起始信号和EEPROM地址
    		  //为了解决8地址对齐问题
    			if(i==0 || (addr % EEPROM_PAGE_SIZE)==0 )
    			{
    				 //循环发送起始信号和EEPROM地址的原因是为了等待上一次写入的一页数据\
    				写入完成
    				 for(m=0;m<1000;m++)
    				 {
    					 //发送起始地址
    					 i2c_Start();
    					 //发送设备写地址
    					 i2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);
    					 //等待从机应答
    					 if( i2c_WaitAck()==0 )
    					 {
    						break;
    					 }
    				 } 
    				  //若等待的1000次从机还未应答,等待超时
    				  if( m==1000 )
    			  	{
    					goto cmd_fail;
    			   	}	
    				//EEPROM应答后发送EEPROM的内部存储器地址
    				i2c_SendByte((uint8_t)addr);
    				//等待从机应答
    				if( i2c_WaitAck()!=0 )
    				{
    					goto cmd_fail;
    					
    				}	
    			}
    		 //发送数据
    		 i2c_SendByte(_pWriteBuf[i]);
    		 //等待应答
    	   if( i2c_WaitAck()!=0 )
    	   {
    		  goto cmd_fail;			
         }
    		 //写入地址加1
    		 addr++;		
    	}
    	
    	i2c_Stop();
    	return 1;
    	
    	cmd_fail:
    	i2c_Stop();
    	return 0;
    }
    
    
    uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
    {
    	uint16_t i;
    	
    	  i2c_Start();
    		i2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_WR);
    	 if( i2c_WaitAck()!=0 )
    	 {
    			 goto cmd_fail;		
    	  }
    		i2c_SendByte((uint8_t)_usAddress);
    	 if( i2c_WaitAck()!=0 )
    	 {
    			  goto cmd_fail;
    	  }
    		i2c_Start();
    		i2c_SendByte(EEPROM_DEV_ADDR|EEPROM_I2C_RD);
    		 if( i2c_WaitAck()!=0 )
    		 {
    				  goto cmd_fail;				
    	   }
    	 for(i=0;i<_usSize;i++)
    	{	
    		_pReadBuf[i]=i2c_ReadByte();
    		/* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */
    		if (i != _usSize - 1)
    		{
    //			i2c_NAcK();	/* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
    			i2c_Ack();	/* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
    		}
    		else
    		{
    			i2c_NAcK();	/* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
    		}
    	}
    	i2c_Stop();
    	return 1;
    	
    	cmd_fail:
    	i2c_Stop();
    	return 0;
    }
    
    uint8_t ee_Test(void) 
    {
      uint16_t i;
    	uint8_t write_buf[EEPROM_SIZE];
      uint8_t read_buf[EEPROM_SIZE];
      
    /*-----------------------------------------------------------------------------------*/  
      if (i2c_CheckDevice(EEPROM_DEV_ADDR) == 1)
    	{
    		/* 没有检测到EEPROM */
    		printf("没有检测到串行EEPROM!\r\n");
    				
    		return 0;
    	}
    /*------------------------------------------------------------------------------------*/  
      /* 填充测试缓冲区 */
    	for (i = 0; i < EEPROM_SIZE; i++)
    	{		
    		write_buf[i] = i;
    	}
    /*------------------------------------------------------------------------------------*/  
      if (ee_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0)
    	{
    		printf("写EEPROM出错!\r\n");
    		return 0;
    	}
    	else
    	{		
    		printf("写EEPROM成功!\r\n");
    	}  
    
    /*-----------------------------------------------------------------------------------*/
      if (ee_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0)
    	{
    		printf("EEPROM出错!\r\n");
    		return 0;
    	}
    	else
    	{		
    		printf("EEPROM成功,数据如下:\r\n");
    	}
    /*-----------------------------------------------------------------------------------*/  
      for (i = 0; i < EEPROM_SIZE; i++)
    	{
    		if(read_buf[i] != write_buf[i])
    		{
    			printf("0x%02X ", read_buf[i]);
    			printf("错误:EEPROM读出与写入的数据不一致");
    			return 0;
    		}
        printf(" %02X", read_buf[i]);
    		
    		if ((i & 15) == 15)
    		{
    			printf("\r\n");	
    		}		
    	}
      printf("EEPROM读写测试成功\r\n");
      return 1;
    }
    
    
    

    main

    #include "stm32f10x.h"
    #include "led.h"
    #include  "usart.h"
    #include  <string.h>
    #include "i2c_ee.h"
    #include "i2c_gpio.h"
    
    #define SOFT_DELAY Delay(0x0FFFFF);
    
    void Delay(__IO u32 nCount); 
    
    
    int main(void)
    {	
    
    	/* LED 端口初始化 */
    	LED_GPIO_Config();	
    
      /*初始化USART 配置模式为 115200 8-N-1,中断接收*/
      USART_Config();
    
    		printf("EEPROM 软件模拟i2c测试例程 \r\n");		
    	 
      if(ee_Test() == 1)
      	{
    			LED_G(NO);
        }
        else
        {
          LED_R(NO);
        }
    	 
    while(1)
    {  
    }
    	
      }
    
    void Delay(__IO uint32_t nCount)	 //简单的延时函数
    {
    	for(; nCount != 0; nCount--);
    }
    

    效果与硬件I2C一模一样就不演示了

    八.总结

    不管是硬件I2C还是软件I2C先不管他们的优缺点,主要我们是要在实现的过程中理解IC2协议这个才是最重要的,反正I2C必须得会因为应用太广泛了,最后如果文章内容有疑问的来评论区一起讨论讨论!!!

    结束语:
    最近发现一款刷题神器,如果大家想提升编程水平,玩转C语言指针,还有常见的数据结构(最重要的是链表和队列)后面嵌入式学习操作系统的时如freerots、RT-Thread等操作系统,链表与队列知识大量使用。
    大家可以点击下面连接进入牛客网刷题

    点击跳转进入网站(C语言方向)
    点击跳转进入网站(数据结构算法方向)

    在这里插入图片描述

    展开全文
  • 详细介绍GPIO、I2C、SPI通讯原理以及物理层原理

    万次阅读 多人点赞 2021-08-26 10:03:20
    针对GPIO、I2C、SPI的讲解,对这些通讯方式有个大致框架,对日后的开发会有帮助,了解因为所以然。 一. GPIO 1. 什么是GPIO? GPIO全拼叫General Purpose Input Output(通用输入输出)简称IO口也叫总线扩展器,...

    目录

    一. GPIO

    1. 什么是GPIO?

    2. GPIO组成原理

    3. GPIO工作原理

    二. I2C

    1. 什么是I2C?

    2. I2C组成原理

    3. I2C的特性

    4. I2C的通讯模式

    5. I2C的通讯过程

    6. I2C接口工作模式

    7. 硬件拉高拉低的过程

    8. 一对多

    9. 开发流程

    三. SPI

    1. 什么是SPI?

    2. SPI优与缺点

    3. SPI组成原理

    4. SPI通讯模式

    5. 通讯过程


    前言 

    针对GPIO、I2C、SPI的讲解,对这些通讯方式有个大致框架,对日后的开发会有帮助,了解因为所以然。

    一. GPIO

    1. 什么是GPIO?

    GPIO全拼叫General Purpose Input Output(通用输入输出)简称IO口也叫总线扩展器,GPIO口是由引脚,功能寄存器组成,不同的架构中的GPIO封装不同,所使用的引脚数与寄存器数不同,具体可以参考芯片手册里的GPIO篇。

    GPIO的作用是用来控制连接在此GPIO口上的外设,我们一般通过观察原理图找到当前板子的GPIO口引出在哪个口上或者排针上,我们把我们的外设接到上面去就可以通过GPIO与这个外设进行交互控制,在驱动层我们通过读写GPIO口中的功能寄存器来改变连接在此GPIO上的外设状态。

     
    

    2. GPIO组成原理

    GPIO原理图如下:

    取自STM32F7系列,不同架构的GPIO口封装不同,这里仅用于当前示例

    从上面原理图可以看出STM32F7系列GPIO是由三个寄存器以及“TTL肖特基触发器、二极管、P-MOS管和N-MOS管”组成

    3. GPIO工作原理

    电子器件的作用:

    器件名

    作用

    输出数据寄存器此寄存器的值会通过输出驱动器输出,其中指定位用于决定使用P-MOS还是N-MOS,有些位表示输出高低电平,如寄存器是4位的,其中0-2位代表输出模式,第3位表示输出电平信号,数字信号1则高电平,数字信号0则低电平
    P-MOSVgs(电荷)小于一定的值就会导通,利用这个特性和实现推挽与开漏输出
    N-MOSVgs(电荷)大于一定的值就会导通,利用这个特性和实现推挽与开漏输出
    保护二极管IO引脚上下两边两个二极管用于防止引脚外部过高、过低的电压输入。当引脚电压高于VDD时,上方的二极管导通;当引脚电压低于VSS时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁,即只有电流不高于VDD也不低于VSS时才能正常通过
    TTL肖特基触发器用于将电流信号转化为模拟数字信号0或1,当使用模拟输入时不会通过触发器,模拟输入是指不通过内部转换器,需要外部外接adc进行ad采样转换

    每个GPIO口都有一组引脚连接,并且这组引脚,如PA-PG(引脚编号)连接到GPIOA中,其中有一个引脚的作用是输出,说的直接一点GPIO口是一个可编程的引脚,我们通过对GPIO口里的寄存器进行配置,让CPU对特定的引脚输出高电平。

    上面涉及到一个知识点,推挽与开漏输出,这两个模式是使用P-MOS与N-MOS两个晶体管实现的

    P-MOS与N-MOS的区别

    P-MOS是接了VDD(正极电源),所以它拥有输出高电流的能力,所以给P-MOS高电平就可以导通P-MOS管,因为接了电源线的原因一旦导通了就可以让电源线里的工作电压流通,而N-MOS上面接了VSS,若给高电平是无法导通的,因为它没有接电源线是无法输出高电平的,如果给了N-MOS高电平即便流通了因为没有VDD电源线它也无法正常输出电流,若给了低电平则会流向VSS接地处,所以N-MOS只能给低电平才有效,若想N-MOS驱动可以外接上拉电阻。

    所以这里输出低电流的话输出到N-MOS晶体管的引脚上,那么始终输出的是接地部分,可以有效降低材料的消耗率。

    这里说一下高电平与低电平在电路里的区分:

    这里以TTL电路为列,高电平>3.4V,低电平<0.2V,所以即便N-MOS没有输出任何电流,它也是低电平。

    使用这两个器件能够在输出高低电平时保证材料的消耗率降低,如果使用一个线路输出高低电平的话会损耗会提升,N-MOS会一直接地,所以输出端其实一点电流都没有,这会极大降低消耗率。

    MOS是场效应管,它有两种类型,P沟道与M沟道,至于哪种类型取决于MOS源接在VDD还是VSS,接在VDD就是P沟道,接在VSS就是M沟道

    推挽输出

    连接在输出控制电路里的数字器件,可以输出高电平与低电平,输出数字信号0时N-MOS导通,P-MOS高阻,输出数字信号1时P-MOS导通,N-MOS高阻抗

     

    开漏输出

    只能输出低电平,输出数字0时N-MOS导通,P-MOS高阻,无法输出高电平,若需要输出高电平需要在N-MOS晶体管后的引脚上接一个上拉电阻,将所有不确定的信号变成高电平,它只能从N-MOS这条线路上流过,所以就导致了它不能输出高电平

    输出电路会获取输出数据寄存器里当前的模式位是什么,如推挽或开漏,若推挽则用电路比较器进行电流比较来确定输出到P-MOS还是N-MOS,若是开漏模式则根据N-MOS导通特性以及当前数字信号的值0/1,来输出对应的电流导通,同时电路工作时,两只对称的功率开关管每次只有一个导通,所以导通损耗小、效率高。

    开漏负载能力较强,因为有上拉电阻,所以一般长时间的设备会用开漏输出

    二. I2C

    1. 什么是I2C?

    I2C全拼Inter Integrated Circuit,简称IIC或I2C,是由Philips公司开发的两线时串行总线,用于SOC与外设的连接通讯,它只需要两根线就能实现I2C的通讯,采用主从模式,主的一方可以读写数据,而从的一方只能等待被读写。从的一方没有主动权。

    I2C是双向通讯的,由两根线完成,分别是:SDA(串行数据线)、SCL(串行时钟线),接口输出模式为开漏输出,其总线接口已经集成到SOC内部,我们只需要通过原理图找到它的接口,在用外设的杜邦线或者其它方法连接到此接口上就可以实现I2C的通讯。

    SDA与SCL都外接了上拉电阻,所以当SDA空闲时刻输出的永远是高电平,它对外设也有一定要求,要求外设的输出模式也是开漏输出,因为这跟它本身的电路实现有关,若两个电路接口模式不一则是无法完成正常通讯的。

    其中I2C里的上拉电阻也不是随便用的,因为电阻值越高意味着信号拉高周期越长,那么通讯周期时间就越高,速率就下来了。

    I2C为每个设备提供了一个地址,可以通过这个地址找到不同的设备,来表明不同的设备,只有从设备收到是自己的地址时才响应。

    因为连接在I2C上的可能有很多个设备,从设备需要设置自己的地址,主设备不用,因为主设备是不会被从设备读写的,主设备是负责读写其它设备的。

    通讯时会有一个起始数据,这个数据是9个bit位,前7位是从设备地址,最后一位是方向(0/1(读或写))

    这也就表明从设备的地址不能超过7个bit

    2. I2C组成原理

    架构图:

    SMBA线用于SMBUS 的警告信号,I2C 通讯没有使用,可以忽略

    I2C内部是有自己的控制单元的,用于处理I2C的通讯,可以说是一个小芯片,它不属于CPU,它只是被集成SOC里(看架构,有的架构是没有的)

    3. I2C的特性

    • 仅需两根线
    • 接口集成在PCB内部
    • 因为是开漏输出,电流消耗较小(高电流由上拉电阻完成,通常情况下器件不需要输出高电平所以这块功耗较小)
    • 世界级标准,大多数开发板都支持
    • 一对多,一根线接多跟外设,通过地址区分,节省排版空间

    4. I2C的通讯模式

    双向传输总线:

    模式

    速率(kbit/s)

    标准模式(Standard-mode)100
    快速模式(Fast-mode)400
    快速模式+(Fast-mode Plus)1M
    高速模式(High-speed mode)3.4M

    单向传输总线:

    模式

    速率(kbit/s)

    超快速模式(Ultra Fast-mode)5M

    这些模式是根据速率来决定的,也就是说我们将I2C通讯速率设置成与上面对应的速率那么就是处于这个模式当中,其中说是这么快其实是有延迟的,因为要流过上拉电阻,这个取决于上拉电阻的阻值,因为信号在经过上拉电阻时会有一个周期变化,这个周期变化区别于阻值。

    设置方法:

    我们需要根据当前的PCLK时钟频率来计算的,这和设置PWM占空比类似,都需要经过计算然后设置I2C里的CCR时钟控制寄存器的值,因为如果频率不同是无法正常工作的。

    如:时钟频率是66MHZ,预分频值是65MHZ,系数是1/2那么计算公式如下:

    时钟频率=PCLK/((预分频值+1))/分频系数

    PCLK我们得知是66,分频值我们设置的是65,分频系数是1/2也就是除于2

    所以公式:66/(65+1)/2 =0.5

    在将0.5转化为HZ的单位:0.5*1000=500KHZ,这里还有一个公式,就是算HZ转秒,赫兹的倒数就是它的秒这个公式:(1/500)=0.002ms,1毫秒=1000微秒,0.002毫秒等于2微秒,通过这样的算法公式就可以得知我们现在的频率是每2微秒工作一次

    我们想让它每0.5毫秒工作一次,所以TCNTB的值=500毫秒/当前微秒频率2

    500/2=250,所以TCNTB的值应该为250,当然你也可以这也算,2微秒一次,1毫秒=1000微秒,0.5毫秒就等于500微秒,500微秒/2微秒=250,就得出经过250次2微秒后就到达了500微秒,而500微秒就等于0.5毫秒

    5. I2C的通讯过程

    通讯步骤:

    • 开始信号:SCL为高电平而SDA由高到低的跳变,表示产生一个起始条件
    • 结束信号:SCL为高电平而SDA由低到高的跳变,表示产生一个 停止条件
    • 应答信号:当发送完信号之后则拉低

    总线在空闲状态时,SCL和SDA都保持着高电平

    这三种信号里,起始信号是必须需要的,而结束信号和应答信号都可以视情况不要。

    在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他I2C器件无法访问总线;而在停止条件产生后,本次数据传输的主从设备 将释放总线,总线再次处于空闲状态

    传输过程:

    • 每次传输时的数据/地址以9位bit进行传输,前8位是数据,最后一位是应答位
    • 传输时高位在前,低位在后
    • 主设备在SCL和SDA都处于高电平状态时,先将SDA拉到低电平,然后在将SCL也拉入低电平,这样SDA产生了一个下降沿的信号,从设备检测到之后便知道主设备发来了起始信号,那么从设备需要主动拉高SDA线告诉主设备我准备好了

    • 这个电平变化不是说一个时间点的变化,比如上一秒是高电平下一秒是低电平这不算变化,时间点变化是要求在一个周期里某一时间段处于高电平,某一时间段处于低电平,整个周期里完成这样一个动作才属于上升沿或下降沿变化,这个周期取决于I2C的时钟频率
    • 如下图的周期变化,假设一个周期是1.2毫秒,那么这个毫秒里的周期变化如下

    • T1到T2之间处于高电平,T3到T4之间处于低电平,然后整个周期就变成了下降沿的信号
    • 然后主设备会在SCL线上产生时钟周期的脉冲信号,即发高电平,每产生一个脉冲信号都会从SDA线上发送一个数据出去,然后从设备检测到脉冲信号了就从SDA线上去读一个BIT位,在产生脉冲信号之后,SCL是处于低电平的,那么此时SDA电平会根据传输的数字信号进行翻转(为了防止数据不稳定,要求在SCl处于低电平时SDA才允许改变电平值,SCL发送一次脉冲信号后会要求拉低电平)
    • 首先最开始发送的是地址信息,每次有效位为8位,当主设备发送8位之后就不会在发了,且发送完八位以后会主动让SDA线一直处于高电平(因为SDA线发送BIT可能是高或低电平,可能第8位是低电平所以需要拉高回来),然后就会一直等待从设备拉低,当从设备拉低以后那么主设备监听到之后就会认为此次通讯是正确的


    • 然后接下来就可以发送数据位了,与地址信息一样,SCL产生脉冲之后SDA进行翻转,从设备进行读取,当发送完成之后就需要产生停止位,这个是由主设备完成的,会拉高SCL电平使其一直处于高电平,然后将SDA进行周期变化:上升沿,即在一定周期里拉低电平然后在拉高电平,然后此时SCL与SDA都处于高电平模式,则代表总线处于空闲状态可以被其它设备占用

    从设备是需要实时监听SCL的电平变化,来完成对应的动作

    以下是发送170(十六进制:0xAA; 二进制:1010 1010)到从设备的传输时序:


    总结理解起始与结束信号就是:

    • 起始条件:SCL线是高电平时,SDA线从高电平向低电平切换。
    • 停止条件:SCL线是高电平时,SDA线从低电平向高电平切换。

    这其中还有重复起始条件,即不用停止符号,只需要发送一个重复起始条件后续可以发送任意字节的数据,当全部发送完成之后在发送停止条件

    • 即在不释放总线的情况下给从设备发送Sr信号,然后重复上一次的传输,这样做的好处在于不会出现在传输期间被别的设备抢占的情况,因为每次传输结束都会有一定的空闲时间,如果这个时间周期被别的设备占用会出现一些数据不完整的情况,因为每次传输只能传输一个字节的有效数据位,每次通讯都要产生起始和结束信号,所以会有一定的空闲时间

    具体的通讯步骤可以参考I2C官方文档

    6. I2C接口工作模式

    I2C接口有四种工作模式

    工作模式

    介绍

    从发送器模式从设备收到主设备发来的ADDR地址后,将内部的数据寄存器里的值通过数据控制模块,发送给SDA总线,这种模式一般应用于主设备读取从设备里的数据
    从接收器模式从设备收到主设备发来的ADDR地址后,通过数据控制模块将SDA总线的数据存于到数据寄存器中
    主发送器模式在发送ADDR地址后且得到从设备响应后将数据寄存器里的值发送到SDA总线上
    主接收器模式在发送ADDR地址后且得到从设备响应后将SDA总线上的数据写入到数据寄存器中

    数据寄存器一般是DR寄存器(数据缓存寄存器),这块寄存器是等待内部程序将其读走的

    其中因为是高位到地位发送的,所以内部会有一个位移寄存器的过程才能写入到DR寄存器

    7. 硬件拉高拉低的过程

    I2C的SDA与SCL都外接了上拉电阻,如果一直处于拉高状态那么只能输出高电平无法输出低电平,那么数字逻辑信号时钟是1,这样结果是不对的,所以需要有一个拉低的过程才能保证传输的bit位是有效且正确的。

    I2C内部有一个逻辑控制器,负责控制这一块,当需要上拉时候会控制FET(场效应管,主要作用是控制半导体器件,控制电阻值),来拉低上拉电阻值使其电流固定在低电平值范围,产生低电平信号

    拉低:

    拉高:

     

     S3C2400的物理层的拓扑结构(仅供参考):

    8. 一对多

    I2C给每个从设备设置地址,然后通过发送地址位来选择要操控哪个从设备。

    在每个从设备连接后需要设置自己的地址,便于I2C主设备查找。

    每个设备连接到I2C的SDA与SCL上,然后I2C主设备对这条线发送地址当从设备收到以后来确定是否是寻找自己

    下图的连接就是将所有设备的SCL与SDA连接到I2C的主设备上,也就是说将I2C主设备的SDA与SCL引出来,然后从设备接到这根线上去,然后发送数据时接在这跟线上的所有设备都能监听到电平变化,就像socket的广播模式一样。

    主设备是不需要设置自身地址的,因为不会有人找它,当然如果要与其它外设进行合作开发,可以设置一个自己的地址,并编写监听代码让自己做主或从

     

    9. 开发流程

    以下是我在资料里找到的开发流程,若裸机编程可以用作参考

    主机接收模式:

    主机发送模式:

     

    三. SPI

    1. 什么是SPI?

    SPI全拼Serial Peripheral interface(串行外围设备接口),是由Motorola(摩托罗拉)在MC68HCXX系列处理器上定义的,主要应用于EEPROM(带电可擦可编程只读存储器),FLASH,RTC(实时时钟),AD转换器,还有数字信号处理器和数字信号解码器。

    SPI,是一种高速的,全双工,同步的通信总线,且只需要四根引脚线,与I2C一样都大大简化了PCB板子的布局空间,SPI依然是一种世界级的流行标准,大多数板子都内置了SPI的接口。

    Motorola设计它的初衷也是为了节约排版空间。

    2. SPI优与缺点

    优点

    • 支持全双工通信
    • 通信简单
    • 数据传输速率块

    缺点

    • 没有指定的读写方向控制
    • 没有应答机制确认是否接收到数据
    • 跟I2C总线协议比较在数据,可靠性上有一定的缺陷

    3. SPI组成原理

    SPI模块框架图


    SPI结构

    上图结构中的SCK、SS、SDO、SDI是比较重要的几根线,其作用如下:

    名称

    作用

    SCK串行时钟信号,主设备产生
    SS片选线,主要用于发送片选CS信号,即选择从设备
    SDO主设备输入,从设备输出
    SDI主设备输出,从设备输入

    对于SS片选信号这里在多解释一下,在SPI中可能存在许多设备,但是只能有一个主设备,主设备通过SS线来控制选择哪个设备,默认情况下SS线是低电平状态,主设备通过选择连接在对应SS线下的设备,然后使其发送高电平,当连接在对应SS线上的设备收到高电平时就会做出响应,开始接收SDO或SDI的数据,SS线会有多个,可以外接许多设备,SCK与SDO、SDI只有一个,其它设备都接在上面。

    其内部是有DR寄存器与位移寄存器,用来存数据与写数据

    4. SPI通讯模式

    SPI有四种通讯模式,分别是:

    1. Mode0
    2. Mode1
    3. Mode2
    4. Mode3

    SPI官方没有给定名字,只给了模式0-3,其作用如下:

    模式

    作用

    Mode0

    SCLK输出数字信号0时处于空闲状态,SCLK输出数字信号为1时则代表有效状态
    Mode1SCLK输出数字信号0时处于空闲状态,SCLK输出数字信号为1时则代表有效状态,数据采样是在第2个边沿,数据发送在第1个边沿

    Mode2

    SCLK输出数字信号0时处于有效状态,SCLK输出数字信号1时处于空闲状态
    Mode3SCLK输出数字信号0时处于有效状态,SCLK输出数字信号1时处于空闲状态,数据采样是在第1个边沿,数据发送在第2个边沿

    这些模式我们需要通过设置SCLK的”CPOL(时钟极性)和CPHA(时钟相位)”模式来完成,你可以把CPOL与CPHA理解为一个标志位,它们标志位不同会使SCLK的输出电平状态不同,也会改变数据输出的边沿

    SPI工作模式与CPOL和CPHA的对应关系以及它俩的作用:

    模式

    CPOL

    CPHA

    Mode000
    Mode101
    Mode210
    Mode311

    边沿

    这里说一下在SPI中什么是边沿,这里的第1个边沿与第2个边沿表示的是电平变化

    边沿表示的是上升沿或下降沿

    上升沿:即在一定周期时由低电平变化到高电平,然后此时在进行数据采样,即电平信号到数字信号转换

    下降沿:即在一定周期时由高电平变化到低电平,然后此时在进行数据采样,即电平信号到数字信号转换

    CPOL

    用来决定SCL空闲时应该是怎样的电平状态,若为0,SCLK空闲时输出低电平,若为1空闲时输出高电平

    CPHA

    它用来表示该SDI有效还是SDO有效,若它的值为0则代表第0个边沿数据采样,即SDI上可以进行数据采样,若它为1时则SDO数据可以进行采样

    同时它与CPOL是关联的:

    若CPHA为0,CPOL为0时:

    若CPOL为0,则第一个边沿有效,即SCLK空闲时是低电平,那么边沿就等于上升沿,同时SDI输出信号,SDO可以采样了

    若CPHA为0,CPOL为1时:

    若CPOL为1,则第一个边沿有效,即SCLK空闲时是高电平,那么边沿就等于下降沿,同时SDO输出信号,SDI可以采样了

    SDI与SDO线对主设备和从设备方向不同,SDI线在主设备上是输出,从设备上是输入,所以它们是相反的,所以当SDI输出信号时,对于从设备来说,它就可以读数据了所以主设备与从设备两根线SDO与SDI两根线作用是不一样的,所以上面的状态对于不同的设备是相反的,也就是刚好实现了主设备写时,从设备读,主设备读时,从设备写
    采样意思是设备可以去某个线上去读数据了

    SPI是没有控制信号的,只能通过SCLK时钟的状态来告诉从设备现在的流方向,从设备需要实时检测当前的SCLK状态

    工作时需要保证从设备与主设备一致的工作模式,其次许多从设备在出厂时就已经设定好了工作模式,是无法修改的,有些从设备是可以修改的。

    5. 通讯过程

    • 首先拉高对应设备的SS线,选择对应的从设备
    • 通过SLCk产生时钟边沿变化,来告诉从设备读写
    • 主设备里的控制逻辑通过位移寄存器从高到低的方式移动给SDI总线上
    • 从设备接收到之后也通过位移寄存器的方式按照顺序移到DR寄存器中
    • 这里给大家看一下SCLK产生时钟周期时做了什么
    • 如下图,当SCLK周期开始变成低电平时则代表数据有输出了,当变成高电平时则代表数据可以采样(读)了,即输出→输出完成,这是整个周期做的事情,下位机只需要等待周期变化完成之后根据当前模式去到指定的线上读或写数据就可以了

     

    • SPI是双向的,主机发的时候一样可以收从机的数据,在一个周期里完成数据交换,但是只有一个DR寄存器,所以SPI的方法是位移,主机高到低,从机低到高的交换,当主机高位移出去,低位就空一个出来,而从机低位移出一位,高位就空出一位

     在通讯之前需要保证双方时钟频率是一致的,一般情况下从设备是无法修改时钟频率的,只有主设备能够修改时钟频率,将主设备的时钟频率修改与从设备一致,默认情况下是不需要修改这些的,这个可以根据芯片手册中SPI上的使能时钟接的是谁,如果提供了GPIO口进行修改那么就支持可编程时钟,若没有则代表当前的MCU不支持编程SPI的时钟频率

    展开全文
  • I2C库函数

    千次阅读 2018-08-08 17:10:35
    1.void I2C_DeInit(I2C_TypeDef* I2Cx) 功能:将I2Cx外设寄存器重设为默认值 注释:该函数调用了RCC_APB1PeriphResetCmd来进行挂载于APB上外设的寄存器的复位 例如:I2C_DeInit();   2.void I2C_Init(I2C_...
  • 硬件i2c

    千次阅读 2022-02-11 17:42:27
    在stm32f103中文参考手册120页i2c重映射引脚 时钟控制逻辑 SCL线的时钟信号,由IIC接口根据时钟控制寄存器(CCR)控制,控制的参数主要位时钟频率。 ·可选择IIC通讯的“标准/快速”模式,这两个模式分别对应100...
  • I2c总线驱动

    千次阅读 2022-03-21 10:55:06
    I2c总线的方式 接口 功能 使用标准 GND 所有设备共地 SCK 时钟 频率 SDA 数据传输 详见电平时序图 对应时序图的基本函数 void I2C_Configuration(void); { GPIO_InitTypeDef GPIO_InitStructure;...
  • I2C详解(一)

    万次阅读 多人点赞 2019-01-03 17:22:21
    I2C Bus(Inter-Integrated Circuit Bus) 最早是由Philips半导体(现被NXP收购)开发的两线时串行总线,常用于微控制器与外设之间的连接。要想了解详细I2C协议,目前最新的I2C标准协议是2014年第6版本,如下: I2C-...
  • I2C—读写EEPROM

    千次阅读 2022-03-23 16:51:46
    当STM32的I2C工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到的地址与STM32的自身的“I2C地址寄存器”的值作比较,以便响应主机的寻址。STM32的自身I2C地址可通过修改“自身地址寄存器”修改...
  • [STM32学习]——一文搞懂I2C总线

    千次阅读 2022-01-07 09:51:00
  • I2C总线设计规范之基础篇

    千次阅读 2018-12-19 19:50:23
    I2C通信协议分析  常用IIC接口通用器件的器件地址是由种类型号,及寻址码组成的,共7位。如格式如下:D7 D6 D5 D4 D3 D2 D1 D0 1、器件类型由:D7-D4 共4位决定的。这是由半导公司生产时就已固定此类型的...
  • 根据datesheet上,其I2C slave 地址如图所示, ![图片说明](https://img-ask.csdn.net/upload/201909/18/1568795870_312806.png) 然而板子通过i2cdetect -y -r 1命令,显示出的设备地址却是0x50,这个是为什么呢...
  • 【组件】i2c抽象/模拟i2c

    万次阅读 2017-12-23 09:27:26
    关于i2c的使用,并不陌生,C51、ARM、MSP430等,都基本集成硬件i2c,或者不集成i2c的,可以根据总线时序图使用普通IO口翻转模拟一根i2c总线。对于当下流行的stm32饱受诟病的硬件i2c,相信很多人都是使用模拟i2c。...
  • stm32软件模拟i2c通讯读取lm75a温度

    千次阅读 多人点赞 2019-03-24 21:25:50
    stm32硬件i2c有着一些bug,此外对于i2c这种通用的串行通信协议,从源头掌握和使用显然更加靠谱一些,当然,对于arm,还是直接操作寄存器来得方便的多。 1、I2C协议 1.1 i2c串行总线概述 采用串行总线技术可以使系统...
  • I2C总线时序详解

    千次阅读 多人点赞 2021-05-06 11:21:52
    1 I2C简介 总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线。而I2C总线最大的优点之一就在于,它只需数据(SDA)和时钟(SCL)两根总线就可以支持主设备与多个从设备之间进行通信。图1显示了嵌入式系统...
  • I2C协议靠这16张图彻底搞懂(超详细)

    万次阅读 多人点赞 2020-10-20 20:34:47
    I²C(Inter-Integrated Circuit),中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,是由飞利浦公司在1980年代初设计的,方便了主板、嵌入式系统或手机与周边设备组件之间的通讯。由于其简单性,它...
  • I2C通信协议详细讲解

    千次阅读 多人点赞 2020-12-13 23:08:59
    I2C协议讲解讲解流程我们为什么要学习I2C通信I2C协议简介:I2C物理层特点I2C协议层写数据读数据读和写数据通讯的起始和停止信号地址及数据方向 讲解流程 我们为什么要学习I2C通信 Stm32的最常用的板间通信有很多,有...
  • STM32系统学习——I2C (读写EEPROM)

    万次阅读 多人点赞 2017-12-08 13:54:24
    I2C 通讯协议(Inter-Integrated Circuit)引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。 在计算机科学里,大部分复杂的...
  • I2C通讯协议详解

    千次阅读 多人点赞 2020-04-05 20:57:02
    I2C协议总结两个方面物理层电气特性协议层I2C基本读写过程#写过程通讯复合格式通讯信号的判断通讯的起始和停止信号数据有效性地址及数据方向响应信号通讯过程部分代码讲解总结**顺便找一下玩过stm32+ESP8266+onenet...
  • I2C 时序详解,精确到每一个时钟

    万次阅读 多人点赞 2017-06-28 21:47:28
     i2c_set_line(DRI_I2C_SDA|DRI_I2C_SCL);  i2c_delay_us(5);     /*SDA由高电平到低电平,产生开始信号 */  i2c_clear_line(DRI_I2C_SDA);   i2c_delay_us(5);     /*钳住总线,...
  • 一、I2C接口技术 1.I2C总线系统组成 2.I2C总线的状态及信号 3.I2C总线基本操作 4.启动和停止条件 5.I2C总线数据传输格式 二、I2C总线上拉电阻的估算与选取 三、树莓派与AT24C02接口实验电路及Python SMBus串行I2C ...
  • I2C完整讲解(一)

    千次阅读 2020-03-19 21:19:54
    **I2C 通讯协议(Inter-Integrated Circuit)**是由Phiilps公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要USART,CAN等通讯 协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。...
  • STM32F030_I2C详细配置说明

    万次阅读 2017-04-11 14:59:04
    STM32F030_I2C详细配置说明本文主要总结STM32F030_I2C的相关功能与源代码分享。 I2C(Inter-Integrated Circuit)总线是由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。是微电子通信控制领域...
  • STM32F4模拟i2c

    千次阅读 2018-06-27 00:13:30
      STM32F4系列出来已有好几年,价格上也越来越...在之前“i2c模型/模拟i2c”文章中有使用到模拟i2c,现在将其移植到F4系列的MCU上。F1中用的是标准库,F4中用的是HAL库,函数API有差别,但使用思维是一样的。 ...
  • I2C总线详解

    千次阅读 2020-02-28 20:21:08
    1 I2C简介 总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线。而I2C总线最大的优点之一就在于,它只需数据(SDA)和时钟(SCL)两根总线就可以支持主设备与多个从设备之间进行通信。图1显示了嵌入式系统...
  • 基于I2C协议实现温湿度采集

    千次阅读 2021-11-20 18:28:41
    学习I2C总线通信协议,使用STM32F103完成基于I2C协议的AHT20温湿度传感器的数据采集,并将采集的温度-湿度值通过串口输出。具体任务:1)解释什么是“软件I2C”和“硬件I2C”? (阅读野火配套教材的第23章“I2C–...
  • STM32F767+STM32CubeMX I2C通信读写EEPROM数据(采用轮询、DMA、中断三种方式) 摘要-前言 作为一名STM32的初学者,在学习过程中会遇到很多问题,解决过程中会看到很多博主发过的文章,每次都是零零总总的学习各个大...
  • 一、 I2C接口简介 I2C最早是Philips公司开发的一款简单的双向总线,实现有效的IC控制。把这个总线写成Inter IC 简称IIC或I2C(PS:说白了2就是两个“I”的意思。) I2C总线一些特征: 1、 只有两根线分别是串行数据...
  • 详解Linux-I2C驱动

    千次阅读 2016-02-25 18:06:54
    1.2 I2C 1.3 硬件 1.4 软件 1.5 参考二、LinuxI2C驱动--I2C总线 2.1 I2C总线物理结构 2.2 I2C总线特性 2.3 开始和停止条件 2.4 数据传输格式 2.5 响应 2.6 总线仲裁三、LinuxI2C驱动--解析EEPROM的读写 ...
  • I2C通信和ADC

    千次阅读 2016-10-03 22:48:52
    I2C接口I2C接口只有两根线,SCL和SDA: SCL:时钟线,传输时钟信号,由主设备向从设备传输时钟信号, SDA:数据线,传输数据信号,主从设备之间相互传递数据的通道 I2C属于串行通信,数据以bit为单位在SDA线上串行...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 520,341
精华内容 208,136
关键字:

如何计算i2c地址