ShuangChenYue ShuangChenYue
首页
  • Cpp之旅
  • Cpp专栏
  • Effective_CPP
  • muduo网络库
  • Unix环境高级编程
  • Cpp提高编程
  • 计算机网络
  • 操作系统
  • 数据结构
  • Linux
  • 算法
  • 基础篇
  • MySql
  • Redis
  • 电子嵌入式通信协议
  • 深入浅出SSD
  • 文件系统
  • 汇编语言
  • STM32
  • 随笔(持续更新)
  • Git知识总结
  • Git 创建删除远程分支
  • nvm使用小结
  • 虚拟机固定 IP 地址
  • Shell 脚本学习笔记
  • VScode 插件 CodeGeeX 使用教程
  • KylinV10 将项目上传至 Github教程
  • KylinV10 安装 MySQL 教程(可防踩雷)
  • kylinV10-SP1 安装 QT
  • 高并发内存池
  • USBGUARD 项目编译环境配置
  • Power_Destory 项目
  • U 盘清除工具编译教程
  • 个人博客代码推送教程
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • MFC编程随记
  • MFC实现ini配置文件的读取
  • MFC实现点击列表头排序
  • 贴图法美化Button按钮
  • 如何高效阅读嵌入式项目代码
  • NAND Flash
  • ARM 处理器
  • 嵌入式基础知识-存储器
  • 闪存存储和制造技术概述
  • 芯片IO驱动力
  • 主流先进封装技术介绍
  • 虎牙C++技术面经
  • 金山一面复习
  • 完美世界秋招 C++ 游戏开发面经(Cpp部分)
  • 博客搭建
  • 网站收藏箱
首页
  • Cpp之旅
  • Cpp专栏
  • Effective_CPP
  • muduo网络库
  • Unix环境高级编程
  • Cpp提高编程
  • 计算机网络
  • 操作系统
  • 数据结构
  • Linux
  • 算法
  • 基础篇
  • MySql
  • Redis
  • 电子嵌入式通信协议
  • 深入浅出SSD
  • 文件系统
  • 汇编语言
  • STM32
  • 随笔(持续更新)
  • Git知识总结
  • Git 创建删除远程分支
  • nvm使用小结
  • 虚拟机固定 IP 地址
  • Shell 脚本学习笔记
  • VScode 插件 CodeGeeX 使用教程
  • KylinV10 将项目上传至 Github教程
  • KylinV10 安装 MySQL 教程(可防踩雷)
  • kylinV10-SP1 安装 QT
  • 高并发内存池
  • USBGUARD 项目编译环境配置
  • Power_Destory 项目
  • U 盘清除工具编译教程
  • 个人博客代码推送教程
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • MFC编程随记
  • MFC实现ini配置文件的读取
  • MFC实现点击列表头排序
  • 贴图法美化Button按钮
  • 如何高效阅读嵌入式项目代码
  • NAND Flash
  • ARM 处理器
  • 嵌入式基础知识-存储器
  • 闪存存储和制造技术概述
  • 芯片IO驱动力
  • 主流先进封装技术介绍
  • 虎牙C++技术面经
  • 金山一面复习
  • 完美世界秋招 C++ 游戏开发面经(Cpp部分)
  • 博客搭建
  • 网站收藏箱
  • 电子嵌入式通信协议

    • I2C、SPI和UART的选择
    • I2C协议
      • 1.1 概述
        • 优点:
        • 缺点:
        • 应用案例:
      • 1.2 通信
        • 启动与停止信号:
        • 地址字节
        • ACK 与 NACK
        • 数据信号
        • 命令字节
        • 写入设备
        • 读取设备
      • 1.3 硬件
      • 1.4 软件
        • 启动 I2C
        • 主设备从从设备请求字节
        • 给指定地址的从设备传输数据
        • 写数据
        • 读数据
        • 完整程序
        • I2C OLED
    • SPI协议
    • UART协议
  • 深入浅出SSD

  • 文件系统

  • 汇编语言

  • STM32

  • 嵌入式软件开发
  • 电子嵌入式通信协议
霜晨月
2024-06-03
目录

I2C协议

# 1. I2C 协议

# 1.1 概述

I2C 全称是 Inter-Integrated Circuit,是飞利浦半导体公司(06年迁移到 NXP 了)在1982年发明的,是使用非常广泛的一种通信协议,很多传感器、存储芯片、OLED 等,都是在使用 I2C。标准输出模式下能达到 100kbps 的传输速率,快速模式下能达到 400kbps 的传输速率,高速模式下能达到 3.4Mbps,超高速下最快能达到5Mbps。

640

I2C 是一种串行通信协议,通常用于连接低速设备,如传感器、存储器和其他外设。它使用两根线(SCL 和 SDA)来实现双向通信,具有地址定向性和主从模式。

# 优点:

  • 多设备支持:I2C 支持多个设备连接到同一总线上,每个设备都有唯一的地址。
  • 简单:I2C 协议相对简单,易于实现和调试。
  • 低功耗:在空闲状态时,I2C 总线上的器件可以进入低功耗模式,节省能量。

# 缺点:

  • 速度较慢:I2C 通信速度较低,适用于低速设备。
  • 受限制:I2C 的总线长度和设备数量受到限制,过长的总线可能导致通信问题。
  • 冲突:当多个设备尝试同时发送数据时,可能会发生冲突,需要额外的冲突检测和处理机制。

# 应用案例:

就其应用而言,连接方面,I2C在需要简单且经济的通信环境中表现出色。它尤其擅长在小型传感器、LCD 屏幕和 RTC(实时时钟)模块中使用。

此外,I2C 由于其在紧凑电路中的效率,在温度控制设备、电池管理系统和 LED 控制器中很有用。但在需要快速或长距离数据传输的项目中,最好选择其他协议。

# 1.2 通信

与 UART 一样,I2C 仅用两条线在设备间通信:

image

  • SCL -- 时钟信号
  • SDA -- 数据信号

I2C 主机与从机之间共享时钟信号,时钟始终由 主机控制,总线下面可以挂多个设备,是一种同步,多主,多从,半双工的通信协议,下面我们简单介绍一下通信原理:

image

默认情况下,两条线都被上拉,SCL=1,SDA=1。

# 启动与停止信号:

通信开始,要先发开始启动信号,结束的时候,要发送结束信号。

  • 开始信号 由主设备发出启动,具体为在 SCL高电平 期间,SDA从高电平切换到低电平;
  • 停止信号 由主设备发出结束,具体为在 SCL高电平 期间,SDA从低电平切换到高电平;

image

当然,在传输过程中,有时候需要更改数据方向,重新传输等,我们没必要发停止信号,直接重新发启动信号启动即可。

image

# 地址字节

我们的总线上可能挂很多从设备,在我们主设备发送了启动信号之后,总线上的从设备就都被“唤醒”了,等着主设备发送地址。所以这里有一个从机地址的概念,从机地址以8位字节发送的,MSB 在前,最后一位表示接下来读或写,所以 高7位构成了从机地址,也可以看出,同一个总线上,可以寻址 128 个从设备。

# ACK 与 NACK

在每个字节传输之后,接收设备发送一个应答信号,确认或者不确认,接收设备通过在 SCL 高电平期间,将 SDA拉低 生成一个确认信号 ACK,拉高 生成一个不确认信号 NACK,这里 ACK 主要用于表示字节正确传输了,NACK 表示数据传输有错误,需要从新发送。应答信号主设备和从设备都可以产生,比如,主设备从从设备读取最后一个字节的数据后,就要发送 ACK 结束传输。

在 I2C 通信中,ACK(Acknowledge)和 NACK(Not Acknowledge)是用于确认数据传输是否成功的信号。

  • ACK(应答):主设备在发送一个字节数据后,会产生一个时钟脉冲来等待从设备的应答。如果从设备成功接收到数据,则会发送一个 ACK 信号给主设备,表示数据已成功接收。ACK 通常是接收设备在 SCL 高电平期间,将 SDA拉低 生成的。

  • NACK(非应答):如果从设备未能成功接收到数据,或者从设备不想继续接收更多的数据,它将发送一个 NACK 信号给主设备。主设备在收到 NACK 后可以根据需要采取相应的措施,例如重传数据或结束通信。NACK 通常是接收设备在 SCL 高电平期间,将 SDA拉高 来生成的。

在 I2C 通信中,主设备向从设备读取数据时,通常是在接收完最后一个字节数据后发送一个停止条件(Stop Condition),而不是发送 NACK 来结束传输。停止条件告诉从设备本次传输已经结束,从设备可以释放总线。 NACK 通常用于在主设备向从设备发送数据时,如果从设备不希望接收更多的数据时使用。

image

# 数据信号

数据以8位字节格式传输,高字节在前,传输的字节数量没有限制,但是每个字节后面必须要有一个数据接收方产生的应答信号。传输过程中,SCL 为低的时候,SDA 数据可以改变,SCL为高的时候,SDA的数据必须稳定。

image

# 命令字节

当写入或读取从设备中特定寄存器时,主机首先要向已寻址的从机写入寄存器地址,其实也是一个数据字节,我们这里称之为命令字节。

# 写入设备

主设备在发出启动信号之后,紧接着发送要操作从设备的地址,其中地址的 最后一位为低电平(0),表示主设备要写入数据。主设备在时钟信号的每个周期内逐位发送数据,并在每个字节后等待从设备的 ACK 应答。在发送完最后一个字节数据并收到从设备的 ACK 应答后,主设备决定是否需要结束通信。如果要结束通信,主设备发送停止条件告知从设备和总线上的其他设备本次通信已经结束,可以释放总线,从而结束通信。

# 读取设备

主设备在发出启动信号之后,紧接着发送要操作从设备的地址,其中地址的 最后一位为高电平(1),表示主设备要写入数据。主设备将释放 SDA 数据线,让从设备接管,并在时钟的控制下向主设备发送数据。主设备在每个字节接收完毕的时候要发送 ACK 响应,表示数据接收成功。当主设备不想接收时,会在最后一个字节接收完毕后发送 NACK 响应。接收 NACK 响应后,主设备会恢复对总线的控制,并发送停止条件来结束通信。

SCL 的控制权 始终在主机这里

image

# 1.3 硬件

ESP32 有2个硬件I2C总线接口,接口可以配置为主机或从机模式,支持如下特性:

  • 标准模式 (100 Kbit/s)
  • 快速模式 (400 Kbit/s)
  • 高达 5 MHz,但受 SDA 上拉强度的限制
  • 7位/10位寻址模式
  • 双寻址模式,用户可以通过编程命令寄存器来控制 I2C 接口,让他们有更大的灵活性

SDA 与 SCL 是低电平有效的,所以我们应该在两根数据线上用电阻上拉,IO 内部也是开漏输出的,一般 5V 系统接 4.7K 上拉,3.3V 系统接 2.4K 上拉即可。ESP32 上,SDA 默认连接 GPIO21,SCL 默认连接 GPIO22,当然,我们可以在代码中配置到任何引脚。

image

# 1.4 软件

# 启动 I2C

启动 Wire 库并作为主机或者从机加入总线,这个函数调用一次即可,参数为7位从机地址,不带参数就以主机的形式加入总线。

Wire.begin();
Wire.begin(address)
1
2

# 主设备从从设备请求字节

由主设备向从设备请求字节,之后用 available() 和 read() 函数读取字节,第三个参数位为stop,在请求后会发送停止消息,释放 I2C 总线,否则总线就不会被释放。

Wire.requestFrom(address, quantity);
Wire.requestFrom(address, quantity, stop);
1
2

# 给指定地址的从设备传输数据

给指定地址的从设备传输数据,之后调用 write() 函数排队传输字节,要通过endTransmission() 结束传输。

Wire.beginTransmission(address);
1

endTransmission() 有以下几个返回结果:

  • 0:成功
  • 1:数据太长,无法放入发送缓冲区
  • 2:在发送地址时收到 NACK
  • 3:在发送数据时收到 NACK
  • 4:其他错误

# 写数据

向从设备写入数据,在调用 beginTransmission() 和 endTransmission() 之间。

Wire.write(value);
Wire.write(string);
Wire.write(data, length);
1
2
3

举例:

#include <Wire.h>
byte val = 0;
void setup() {
    Wire.begin();	// join i2c bus
}
void loop() {
    Wire.beginTransmission(44);	  // transmit to device #44 (0x2c)
    					 	  // device address is specified in datasheet
    Wire.write(val);		 	  // sends value byte  
    Wire.endTransmission();		 // stop transmitting
    val++;
    if(val == 64) {	  // if reached 64th position (max)
        val = 0;	 // start over from lowest value
    }
    delay(500);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 读数据

调用 requestFrom() 后从从设备读取数据。

Wire.read();
1

举例:

#include <Wire.h>
void setup() {
  Wire.begin();        // join i2c bus (address optional for master)
  Serial.begin(9600);  // start serial for output
}
void loop() {
  Wire.requestFrom(2, 6);  		 // request 6 bytes from slave device #2
  while(Wire.available())		  // slave may send less than requested
  {
    char c = Wire.read();   	     // receive a byte as character
    Serial.print(c);         	  	     // print the character
  }
  delay(500);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

还有其它一些函数,例如修改时钟频率等等,用到的时候自行了解一下。

# 完整程序

这里用一个例子来演示一下,I2C 启动之后,开始 扫描总线上存在的设备,并通过串口打印结果出来,在 I2C 下面接了一个OLED的设备。

#include "Wire.h"
void setup() {
    Serial.begin(115200); 
    Serial.println();
    Serial.println("Scanning for I2C Devices ...");
    Serial.print("\r\n");
    int I2CDevices = 0;

    byte address;
    Wire.begin();
    for (address = 1; address < 127; address++) {
        Wire.beginTransmission(address);
        if (Wire.endTransmission() == 0) {
            Serial.print("Found I2C Device: ");
            Serial.print(" (0x");
            if (address < 16) {
                Serial.print("0");
            }
            Serial.print(address, HEX);
            Serial.println(")");
            I2CDevices++;
        }
    }

    if (I2CDevices == 0) {
        Serial.println("没有发现 I2C 设备!\n");
    }
    else {   
        Serial.print("发现了");
        Serial.print(I2CDevices);
        Serial.println("个 I2C 设备!\n");  
    }  
}

void loop(){
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

Wire.endTransmission() 返回0,代表这个地址通信成功,就认为总线上存在这个地址的设备。

# I2C OLED

I2C 只是个通信协议,具体的还是要结合实物来演示,比如一些传感器或者屏幕,这里用 I2C 协议的 0.96寸OLED屏幕 来演示下

image

OLED 使用 SSD1306 控制芯片,所以我们需要下载一个库 SSD1306,另外还需要配合图形库 GFX 操作,代码中,我们先包含对应头文件,然后创建一个Adafruit_SSD1306 对象,第三个参数是用的 I2C 对象。

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
1

初始化时候用 display.begin(SSD1306_SWITCHCAPVCC, 0x3C) 初始化显示对象,传入地址,然后就可以自由简单的显示我们想要显示的数据了。

#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
void setup() {
    Serial.begin(115200);
    if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
        Serial.println(F("SSD1306 allocation failed"));
        for(;;);
    }  
    delay(1000);
    display.display();

    display.clearDisplay();
    display.setTextColor(WHITE);
    display.setTextSize(1);
    display.setCursor(0,0);
    display.print("CHIPHOME");
    display.display();
    display.setCursor(0,8);
    display.print("12345678");
    display.display();
    delay(1000);
}
void loop() {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

参考文章

https://mp.weixin.qq.com/s?__biz=MzI0ODU0NDI1Mg==&mid=2247560830&idx=1&sn=7d6d690ea2a759984a1ae4ef2b6d7d6d&chksm=e99c80eddeeb09fbea0212da2e8386659e70db377b023524c0ceddee0f79f9b304aaabfc5d12&scene=21#wechat_redirect

https://mp.weixin.qq.com/s?__biz=MzI1MjM4MzAwNg==&mid=2247484573&idx=1&sn=422fdfd42abd72b41958403ded6d7e67&chksm=e9e5deecde9257fa64bd35186ad5c5d7221216098da3e4bc03c2361d57ae2160a0ba41bdfb6b&scene=21#wechat_redirect

上次更新: 2024/6/3 14:54:44
I2C、SPI和UART的选择
SPI协议

← I2C、SPI和UART的选择 SPI协议→

Theme by Vdoing | Copyright © 2023-2024 霜晨月
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式