i2c(或IIC)协议使用两根线进行通信(不包括电源正负极),它们分别为:

1、SDA:数据线,IIC 协议允许在单根数据线上进行双向通信——这条线既可以发送数据,也可以接收数据。

2、SCL:时钟线,注意了,这个时钟线跟我们平时所说的时钟没什么关系,不要以为这根线是用来接手表的。其实,这里所说的“时钟”,更像是我们看音乐会的时候,站在前面最中央处的那个指挥者,或者说节拍器。它的作用就是协调硬件之间的传输节奏,做到步伐一致,不然数据就会乱了。比如,IIC通信里面,当时钟线的电平拉高后,数据线的内容就不能改变,也就是说,SCL高电平时,不能写数据,但可以读。当SCL下降为低电平后,才能向数据线(SDA)写入数据。

IIC 通信以 Start 信号开始,以 Stop 信号结束。

传送开始信号的方法:拉高SCL和SDA的电平,在SCL处于高电平的情况下把SDA的电平拉低。

传送结束信号的方法:拉高SCL的电平,在SCL处于高电平的情况下,把SDA的电平拉高。

这其中,你会发现规律:无论是开始信号还是结束信号,SCL 都处于高电平,前文提过,时钟线拉高就是固定数据线上的内容,显然,在开始和结束信号中,是不能传数据的。在SDA上,开始信号和结束信号刚好相反,Start 时电平拉低,Stop 时电平拉高。下面这张图是从 IIC 的协议手册上盗来的。

写入数据时,主机先把时钟线SCL拉低,然后写入一个二进制位(高电平为1,低电平为0),然后把SCL拉高,此时从机读取这个二进制位。接着第二个二进制位也是这样,主机拉低SCL,写SDA,再拉高SCL,从机读……当发送完 8 个二进制(一个字节)后,在第九个时钟周期,主机把SDA拉高(有时候需要切换为输入模式),再拉高SCL,等待从机写应答;如果主机从SDA上读到低电平,表示从机有应答(你的红包我收到了),要是读到高电平,表示无应答(你啥时候发的红包?我都没看到)。

从机向主机发送数据的过程也一样,SCL仍然由主机操控,SCL拉低后向SDA写数据,SCL拉高后就不能写了,此时主机读SDA上的数据。通常主机在接收完最后一个字节后可以不应答(让SCL和SDA同时高电平),或直接发送 Stop 信号终止通信(毕竟主机权力大,生死予夺都是主机说了算)。

上面的东东看得好像很乱,刚接触时就是这样的,见多了就熟悉了。可以大概地总结一下:

1、SCL低电平时,发送方写SDA;

2、SCL高电平锁定SDA,发送方不能写,接收方读;

3、应答信号:SCL高 + SDA低---> 有应答;SCL高 + SDA高---> 无应答。

其实,我们实际开发中,不了解协议时序也没关系,我们也很少手动去模拟 IIC 通信过程。尤其是像树莓派这种带操作系统的开发板,更不应该手动去模拟,而是直接用现成的库(或者API)。不管你什么语言,你都是先向系统发送指令,然后系统去控制硬件,效率上都无法保证。而且,IIC 协议都是标准化的协议,你每次写程序都去手动模拟通信,浪费时间,意义也不大。这好比我们在 Socket 编程时一样,你不可能总去自己写个协议再来通信吧。一般都会直接用 TCP 或 UDP 协议。

所以,对于IIC协议也是如此,我们了解一下就行了。老周上面在介绍时也是简略化的,所以你可能看得有点晕,若想深入理解,可以看数据手册。毕竟老周不可能把手册上的内容复制过来的,那就是抄袭了。

好,继续。

IIC 总线可以挂多个从机,从机不会主动发起通信,都是由主机发起通信的。因此,主机必须知道要跟哪个从机通信,故挂到总线上的从机必须拥有唯一的地址——这就是所谓的器件地址。就像一个内网中的 N 台电脑一样,每台电脑都要给它分配唯一的 IP 地址,这样你才能知道你正在跟谁说话。哪怕是 UDP 广播,也是有广播地址,192.168.1.255。

IIC 器件地址,7位地址最常见,当然也有 10 位的(老周买的各种模块中都没见到),这个【位】是二进制位,常用的 7 位就是7个二进制位。7 位地址格式如下:

低位在右边,从右到左,我们看到第 1 位是 R/W,表示读写位,就是用来告诉从机,我要读数据还是写数据。“W”头顶上有个横线,表示低电平,即 0 表示写,1 表示读。从第二位到第八位就是从机的地址了。所以,现在你知道为啥地址是7位的原因了吧,就是要留一位来确定读还是写。

假如某品牌的自动铲屎机使用 IIC 通信协议,标签上告诉你它的从机地址是 0x47,先把它弄成二进制。

0100 0111

第八位是0,所以有效的值是第一位到第七位,属7位地址。当主机要向铲屎机发起通信时,需要把地址左移一位,变成:

1000 1110

左移后,第二到第七位表示器件地址,就能空出第一位用来放读写标志了。如果要写数据,就向从机发 1000 1110;要读数据,就向从机发 1000 1111。

注意,我们在调用库的时候,是不需要左移的,比如我们.NET中用的 System.Device.Gpio 库,内部会自动进行左移。

好了,基础知识就介绍到这儿,相信你对 IIC 协议已经有大概的了解,下面咱们来看看 System.Device.Gpio 给我们准备了哪些类。

A、命名空间:System.Device.I2c

B、I2cConnectionSettings 类,用来配置 IIC 通信的必要参数。其实就两个:第一个是总线ID,一般系统默认的是 1。第二个参数就是从机的地址(不需要左移)。

C、I2cDevice,核心类,用于读写数据。这是个抽象类,内部根据不同的系统有各自的实现版本,但我们在调用时不用关心是哪个版本。

D、I2cBus,这个一般可以不用,如果硬件上有多个总线,可以使用这个类指定使用哪个总线。其实树莓派有两路 i2c 总线的,我们平时用的是 i2c-1,还有一个 i2c-0 是隐藏的,留给摄像头用的,可以参考官方文档。

        i2c_arm                 Set to "on" to enable the ARM's i2c interface
(default "off") i2c_vc Set to "on" to enable the i2c interface
usually reserved for the VideoCore processor
(default "off") i2c An alias for i2c_arm

“i2c”和“i2c-arm”是同一个东东,只是名字不同罢了,所以,一块板子上就有 “i2c-arm”和“i2c-vc” 两路总线,“i2c-vc”分配给摄像头以及视频相关的接口使用。当然,你也可以拿“i2c-vc”作为常规总线用的,要把视频相关的接口禁用。如果两路都拿来用了,那么树莓派上就有两个总线ID,一个是 0,一个是 1。

另外,也可以使用软件模拟 i2c,这样你就可以弄出几个总线出来了——i2c-2、i2c-3、i2c-150 …… 配置如下:

Name:   i2c-gpio
Info: Adds support for software i2c controller on gpio pins
Load: dtoverlay=i2c-gpio,<param>=<val>
Params: i2c_gpio_sda GPIO used for I2C data (default "23") i2c_gpio_scl GPIO used for I2C clock (default "24") i2c_gpio_delay_us Clock delay in microseconds
(default "2" = ~100kHz) bus Set to a unique, non-zero value if wanting
multiple i2c-gpio busses. If set, will be used
as the preferred bus number (/dev/i2c-<n>). If
not set, the default value is 0, but the bus
number will be dynamically assigned - probably
3.

这个只是提一下,必要时可以用上,软件模拟的接口通信,性能和效率会相对差一点的。

树莓派默认是不打开 i2c 接口的,所以要在配置中将其打开。

sudo raspi-config

找到接口选项。

选择 P5 I2C 条目。

然后选择“YES”。

或者简单粗暴,修改 /boot/config.txt,加上这一行:

dtparam=i2c_arm=on

保存退出。

这一次的 IIC 演示实例,老周不使用传感器。主要担心有同学会误解,因为很多电子模块/传感器都是通过读写寄存器的方式来控制的,于是有同学会以为 IIC 是操作寄存来传递信息的。其实不然,跟 TCP 协议一样,你可以用 IIC 传递任何字节,只要能用二进制表示的就没问题了。

本例老周用一块 Arduino (读音:阿嘟伊诺,重音在后面,“伊诺”要读出来,别读什么“阿丢诺”)开发板做为 IIC 从机,型号为  Uno R3(读音:乌诺,意大利语“第一”的意思,表明这是 Arduino 的首套板子)。然后用树莓派作为主机,来控制 Arduino。

Arduino 上使用 Wire 库进行 IIC 通信。首先要包含 Wire.h 头文件。

#include <Wire.h>

在这个头文件中,注意有这么一行。

extern TwoWire Wire;

其实头文件中声明的封装类名为 TowWire,然后在头文件中用这个类声明了一个变量 Wire,加上 extern 关键字使得其他代码能访问到它,只要 include 这个头文件就OK了。Wire 变量的赋值代码在 Wire.cpp 文件中(提前给你实例化一个对象了)。

TwoWire Wire = TwoWire();

这样布局代码的好处在于:包含 Wire.h 文件后,你马上就能用了,直接就可以通过 Wire 变量调用 TwoWire 的公共成员了。

Arduino 代码一般有两个特定的函数:

setup:初始化一些设置,比如某某引脚设定为输出模式。此函数会在程序在烧进板子上时执行一次,然后就不会执行,进入 loop 函数死循环。但是,如果你按了复位按钮,或者断电了重新上电,就会执行 setup 函数。

loop:这个函数被放在一个 die 循环里,它会无限期地被调用,只要程序被烧进开发板上就会永远地循环。

有同学会问:C/C++不是有入口点吗,main 函数滚哪里去了?main 函数在 main.cpp 文件中,编译时由 Arduino 编译器自动链接。

int main(void)
{
…… setup(); for (;;) {
loop();
if (serialEventRun) serialEventRun();
}
return 0;
}

从入口点函数的逻辑中也看到,setup 函数只调用了一次,然后 loop 函数死循环。

好了,题外话结束,下面咱们回到 Arduino 的项目中,在setup函数中调用 Wire.begin 方法,开始 IIC 通信。

void setup()
{
// 该从机的地址是 0x15
Wire.begin(0x15);
// 注册函数,当收到主机数据时调用
Wire.onReceive(onRecData);
// 注册函数,当主机请求数据时调用
Wire.onRequest(onRequestData);
}

如果 Arduino 作为 IIC 主机,调用 begin 方法时不需要指定地址;此例中 Arduino 充当从机,所以要指定从机地址 0x15(你可以改为其他地址,一般用7位)。树莓派上的应用会使用地址 0x15 来找到这块 Uno 板子。

注意这两行:

    Wire.onReceive(onRecData);
Wire.onRequest(onRequestData);

这两个方法的参数都是指向一个函数的指针,传递时直接写函数名即可。onRecieve 方法注册一个函数,当收到主机发来的数据时调用这个函数;onRepuest 方法注册一个函数,当主机希望从机发送数据时调用这个函数。

onRecData 和 onRequestData 函数定义如下:

void onRecData(int count)
{
if (Wire.available())
{
// 读一个字节
readData = Wire.read();
}
} void onRequestData(void)
{
// 向主机发数据
Wire.write(sendData);
}

在这个示例中,主机只向从机发一个字节,所以参数 count 可以忽略,直接调用 Wire.read 读一个字节,并保存在变量 readData 中;发送数据时调用 Wire.write 方法将 sendData 中的内容发送给主机。在loop循环中,根据readData的值生成sendData的内容——根据主机发的命令生成回复消息。

void loop()
{
// 根据主机传来的数据设置要发给主机的数据
switch (readData)
{
case 1:
strcpy(sendData, "SB");
break;
case 2:
strcpy(sendData, "NB");
break;
case 3:
strcpy(sendData, "XB");
break;
default:
strcpy(sendData, "SB");
break;
}
}

完整代码结构如下;

#include <Wire.h>

// 预声明函数
void onRecData(int);
void onRequestData(void); // 从主机读到的数据
uint8_t readData = 0; // 要发给主机的数据
// 两个字符 + \0,所以是3字节
// 但这里不需要 \0
char sendData[2] = { }; void setup()
{
// 该从机的地址是 0x15
Wire.begin(0x15);
// 注册函数,当收到主机数据时调用
Wire.onReceive(onRecData);
// 注册函数,当主机请求数据时调用
Wire.onRequest(onRequestData);
} void loop()
{
……
} void onRecData(int count)
{
if (Wire.available())
{
// 读一个字节
readData = Wire.read();
}
} void onRequestData(void)
{
// 向主机发数据
Wire.write(sendData);
}

接下来编写树莓派上的应用。

dotnet new console -n Myapp -o .

上面命令创建新的控制台项目,名为Myapp,存放在当前目录下。

添加 System.Device.Gpio 包的引用。

dotnet add package System.Device.Gpio

前文提到过,默认启用的 IIC 总线是 i2c-1,所以实例化 I2cConnectionSettings 时,Bus ID 是1,从机地址是 0x15。

    I2cConnectionSettings settings = new(1, 0x15);

随后获取 I2cDevice 对象。

    I2cDevice device = I2cDevice.Create(settings);

本例的逻辑为:由用户从键盘输入数字(1、2、3),然后把这个数字发给从机(Arduino 板子),然后读取从机回复的数据。

            byte input = 0; //读取键盘输入
Console.WriteLine("现在开始,输入 end 可退出");
while (true)
{
Console.Write("请输入:");
string sl = Console.ReadLine();
if (sl.Equals("end", StringComparison.InvariantCultureIgnoreCase))
{
break;
}
// 将输入内容转为byte
if (!byte.TryParse(sl, out input))
{
input = 0;
}
/*
//发送数据
device.WriteByte(input);
Thread.Sleep(3);
// 接收从机发来的数据
Span<byte> buffer = stackalloc byte[3];
device.Read(buffer);
*/
// 可以一步到位,写完就读
byte[] sendBuf = new byte[] { input };
byte[] recvBuf = new byte[2];
device.WriteRead(sendBuf, recvBuf);
string sr = Encoding.Default.GetString(recvBuf);
Console.WriteLine("接收到的数据:{0}", sr);
}
device.Dispose();

可以调用 WriteXXX 类似方法写入要发送的数据,调用 ReadXXX 类似的方法读入接收到的数据。也可以用 WriteRead 方法,写入数据后接收数据,一步完成。

接线方法:树莓派默认的 IIC 引脚为 GPIO 2和3,即板子上的3、5脚;Arduino 的 SDA 引脚为 A4,SCL引脚为 A5(A4和A5为模拟量读入口,可重用为 IIC 接口),其实 Arduino 还有一路 IIC 接口,位于数字引脚 D13 、GND、AREF后面,就是这里:

所以,接线图如下:

也就是,树莓派的 GPIO 2 接 Arduino 的 A4,树莓派的 GPIO 3 接 Arduino 的 A5。另外,还要把两个板子的 GND 连起来(共地),虽然不共地也能通信,但可能存在被干扰的情况,共地后使用低电平的“0V”有了统一的参考标准,这样传递信号准确更高。

如果 Arduino 开发板没有独立供电,可以把树莓派的 5V 与 Arduino 的 VIN 连接起来,用树莓派给 Arduino 供电(VIN的输入电压不能高于 5.5V,因为这个引脚没有保护措施,过压会炸板子)。

编译 .NET 应用并上传到树莓派,然后运行,输入不同数字,Arduino 会回复对应的消息。

好了,完工,示例代码请点击这里下载。

有人会问,树莓派有没有山寨版?有,比如橙子派什么的,某宝上还有荔枝派。这些板子大多数不贵,但是不太敢买,还是买原装的好一些。 Arduino 是开源板子,版本也很多(也有山寨的),像 DFRobot 好像也可以,还有很多十几块的没名字的,所以也叫不出什么版本,只能说山寨了。不过说实话,还是原装的运行稳定,尽管贵一些。老周当初也是买了几块那种十几块的,上传程序经常出错,装驱动也头疼。原版的稳定,起码用到现在也出过错,也不用找驱动,Windows 能识别。

所以说嘛,一分价钱一分货,后来老周干脆发点血买原装版本的。

【.NET 与树莓派】i2c(IIC)通信的更多相关文章

  1. 基于51单片机IIC通信的PCF8591学习笔记

    引言 PCF8591 是单电源,低功耗8 位CMOS 数据采集器件,具有4 个模拟输入.一个输出和一个串行I2C 总线接口.3 个地址引脚A0.A1 和A2 用于编程硬件地址,允许将最多8 个器件连接 ...

  2. 基于51单片机IIC通信的AT24C02学习笔记

    引言 最近在学习几种串行通信协议,感觉收获很多,这篇文章是学习IIC总线协议的第一篇文章,以后还会再写一篇关于PCF8591 IIC通信的ADDA转换芯片的文章. 关于IIC总线 IIC 即Inter ...

  3. linux i2c 的通信函数i2c_transfer在什么情况下出现错误

    问题: linux i2c 的通信函数i2c_transfer在什么情况下出现错误描述: linux i2c设备驱动 本人在写i2c设备驱动的时候使用i2c transfer函数进行通信的时候无法进行 ...

  4. STM32F10x_硬件I2C主从通信(轮询发送,中断接收)

    Ⅰ.写在前面 关注我分享文章的朋友应该知道我在前面讲述过(软件.硬件)I2C主机控制从机EEPROM的例子.在I2C通信主机控制程序是比较常见的一种,可以说在实际项目中,很多应用都会使用到I2C通信. ...

  5. STM32—IIC通信(软件实现底层函数)

    使用GPIO引脚模拟SDA和SCL总线实现软件模拟IIC通信,IIC的具体通信协议层和物理层链接:IIC #ifndef __BSP_IIC_H #define __BSP_IIC_H #includ ...

  6. verilog中24LC04B iic(i2c)读写通信设计步骤,以及程序常见写法错误。

    板子使用的是黑金的是xilinx spartan-6开发板,首先准备一份24LC04B芯片资料,读懂资料后列出关键参数. 如下: 1.空闲状态为SDA和SCL都为高电平 2.开始状态为:保持SCL,S ...

  7. 《我的嵌入式开发》---- IIC 通信

    IIC 通用文件,文件是在NRF51xx 芯片基础,keil 平台开发测试通过,后期修改为STM32F2xx系列的配置. 文件百度云盘链接 : https://pan.baidu.com/s/1AFx ...

  8. 一个判断I2C总线通信异常原因的方法

    此问题由某客户提出,应用处理器 AP与 MCU进行 I2C通信,通信会经常发生异常,需要定位原因. 首先需要定位的是因为哪个器件发的波形不正确导致通信异常,所以我们在 I2C 线路上增加了以下处理,增 ...

  9. I2C总线通信

    UART 属于异步通信,比如电脑发送给单片机,电脑只负责把数据通过TXD 发送出来即可,接收数据是单片机自己的事情.而 I2C 属于同步通信, SCL 时钟线负责收发双方的时钟节拍, SDA 数据线负 ...

  10. 51单片机之IIC通信原理及软件仿真

    关于IIC我觉这个博客里面说的已经够清楚了 如下图所示的写操作的时序图: 其实像这种通信协议的要求是很精确的,一点点不对都可能导致在实际工程中无法读取数据.我就是被一个应答位耽误了好久,还好最后被我发 ...

随机推荐

  1. C# 9 新特性 —— 增强的模式匹配

    C# 9 新特性 -- 增强的模式匹配 Intro C# 9 中进一步增强了模式匹配的用法,使得模式匹配更为强大,我们一起来了解一下吧 Sample C# 9 中增强了模式匹配的用法,增加了 and/ ...

  2. Logstash学习之路(三)Logstash处理时区、类型转换、删除字段的案例配置

    #输入 input { file { path => ["文件路径"] #自定义类型 type => "自定义" start_position =& ...

  3. 用python+sklearn(机器学习)实现天气预报数据 模型和使用

    用python+sklearn机器学习实现天气预报 模型和使用 项目地址 系列教程 0.前言 1.建立模型 a.准备 引入所需要的头文件 选择模型 选择评估方法 获取数据集 b.建立模型 c.获取模型 ...

  4. Lock锁 精讲

    1.为什么需要Lock 为什么synchronized不够用,还需要Lock Lock和synchronized这两个最常见的锁都可以达到线程安全的目的,但是功能上有很大不同. Lock并不是用来代替 ...

  5. SpringCloud系列之SpringCloud Stream

    SpringCloud Stream SpringCloud Config SpringCloud Gatewa SpringCloud Hystrix SpringCloud 第一部分 文章目录 S ...

  6. /var/lib/zabbix/percona/scripts/get_mysql_stats_wrapper.sh: line 19: mysql: command not found

    [root@test ~]# tail -f /tmp/zabbix_agentd.log /var/lib/zabbix/percona/scripts/get_mysql_stats_wrappe ...

  7. 彻底搞懂MySQL为什么要使用B+树索引

    目录 MySQL的存储结构 表存储结构 B+树索引结构 B+树页节点结构 为什么要用B+树索引 二叉树 多叉树 B树 B+树 搞懂这个问题之前,我们首先来看一下,MySQL表的存储结构 MySQL的存 ...

  8. 【ASM】介绍Oracle自带的一些ASM维护工具 (kfod/kfed/amdu)

    转自:http://blog.csdn.net/wenzhongyan/article/details/47043253 非常感谢作者的文章,很有价值!至此转载,非常感谢 1.前言 ASM(Autom ...

  9. C# 合并和拆分PDF文件

    一.合并和拆分PDF文件的方式 PDF文件使用了工业标准的压缩算法,易于传输与储存.它还是页独立的,一个PDF文件包含一个或多个"页",可以单独处理各页,特别适合多处理器系统的工作 ...

  10. IOC技术在前端项目中的应用

    目录 背景 什么是IOC 如何实现一个IOC 第一步:实现一个容器 第二步:用好装饰器 第三步:使用容器 扩展和展望 最后 背景 前端发展至今已经过去30余年,前端应用领域在不断壮大的过程中,也变得越 ...