【.NET 与树莓派】i2c(IIC)通信
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)通信的更多相关文章
- 基于51单片机IIC通信的PCF8591学习笔记
引言 PCF8591 是单电源,低功耗8 位CMOS 数据采集器件,具有4 个模拟输入.一个输出和一个串行I2C 总线接口.3 个地址引脚A0.A1 和A2 用于编程硬件地址,允许将最多8 个器件连接 ...
- 基于51单片机IIC通信的AT24C02学习笔记
引言 最近在学习几种串行通信协议,感觉收获很多,这篇文章是学习IIC总线协议的第一篇文章,以后还会再写一篇关于PCF8591 IIC通信的ADDA转换芯片的文章. 关于IIC总线 IIC 即Inter ...
- linux i2c 的通信函数i2c_transfer在什么情况下出现错误
问题: linux i2c 的通信函数i2c_transfer在什么情况下出现错误描述: linux i2c设备驱动 本人在写i2c设备驱动的时候使用i2c transfer函数进行通信的时候无法进行 ...
- STM32F10x_硬件I2C主从通信(轮询发送,中断接收)
Ⅰ.写在前面 关注我分享文章的朋友应该知道我在前面讲述过(软件.硬件)I2C主机控制从机EEPROM的例子.在I2C通信主机控制程序是比较常见的一种,可以说在实际项目中,很多应用都会使用到I2C通信. ...
- STM32—IIC通信(软件实现底层函数)
使用GPIO引脚模拟SDA和SCL总线实现软件模拟IIC通信,IIC的具体通信协议层和物理层链接:IIC #ifndef __BSP_IIC_H #define __BSP_IIC_H #includ ...
- verilog中24LC04B iic(i2c)读写通信设计步骤,以及程序常见写法错误。
板子使用的是黑金的是xilinx spartan-6开发板,首先准备一份24LC04B芯片资料,读懂资料后列出关键参数. 如下: 1.空闲状态为SDA和SCL都为高电平 2.开始状态为:保持SCL,S ...
- 《我的嵌入式开发》---- IIC 通信
IIC 通用文件,文件是在NRF51xx 芯片基础,keil 平台开发测试通过,后期修改为STM32F2xx系列的配置. 文件百度云盘链接 : https://pan.baidu.com/s/1AFx ...
- 一个判断I2C总线通信异常原因的方法
此问题由某客户提出,应用处理器 AP与 MCU进行 I2C通信,通信会经常发生异常,需要定位原因. 首先需要定位的是因为哪个器件发的波形不正确导致通信异常,所以我们在 I2C 线路上增加了以下处理,增 ...
- I2C总线通信
UART 属于异步通信,比如电脑发送给单片机,电脑只负责把数据通过TXD 发送出来即可,接收数据是单片机自己的事情.而 I2C 属于同步通信, SCL 时钟线负责收发双方的时钟节拍, SDA 数据线负 ...
- 51单片机之IIC通信原理及软件仿真
关于IIC我觉这个博客里面说的已经够清楚了 如下图所示的写操作的时序图: 其实像这种通信协议的要求是很精确的,一点点不对都可能导致在实际工程中无法读取数据.我就是被一个应答位耽误了好久,还好最后被我发 ...
随机推荐
- 如果不空null并且不是空字符串才去修改这个值,但这样写只能针对字符串(String)类型,如果是Integer类型的话就会有问题了。 int i = 0; i!=''。 mybatis中会返回tr
mybatis 参数为Integer型数据并赋值0时,有这样一个问题: mybatis.xml中有if判断条件判断参数不为空时,赋值为0的Integer参数被mybatis判断为空,因此不执行< ...
- mysql免安装教程
1. 下载MySQL Community Server 5.6.13 2. 解压MySQL压缩包 将以下载的MySQL压缩包解压到自定义目录下,我的解压目录是: "D:\Prog ...
- java 反射给字段重新赋值
1.获取实体的所有字段,遍历 2.获取字段类型 3.调用字段的get方法,判断字段值是否为空 4.如果字段值为空,调用字段的set方法,为字段赋值 Field[] field = model.getC ...
- Thymeleaf Shiro标签
记录一下 guest标签 <shiro:guest> </shiro:guest> 用户没有身份验证时显示相应信息,即游客访问信息. user标签 <shiro:user ...
- udp聊天室--简易
package 聊天; /*一切随便消逝吧*/ import java.net.DatagramSocket; import java.net.SocketException; public clas ...
- springboot 不同环境读取不同配置
1. 3个配置文件(更多环境可以建多个): application.properties (公共配置文件) application-dev.properties (开发环境) applicatio ...
- python常用操作和内置函数
一.常用数据处理方法. 1.索引:按照号码将对应位置的数据取出使用 2.list将任意类型数据用逗号分割存在列表中 3.range:产生一堆数字(顾头不顾尾) 4.切片:可以从复制数据的一部分,不影响 ...
- hadoop目录结构
Hadoop目录结构 重要目录结构: bin目录:存放对Hadoop相关服务(HDFS,YARN)进行操作的脚本 etc目录:Hadoop的配置文件目录,存放Hadoop的配置文件 lib目录:存放H ...
- 【函数分享】每日PHP函数分享(2021-1-11)
str_shuffle() 随机打乱一个字符串. string str_shuffle ( string $str ) 参数描述 str 输入字符串.返回值:返回打乱后的字符串.实例: < ...
- 写给小白看的Mysql事务
1 为什么需要事务 在网上的很多资料里,其实没有很好的解释为什么我们需要事务.其实我们去学习一个东西之前,还是应该了解清楚这个东西为什么有用,硬生生的去记住事务的ACID特性.各种隔离级别个人认为没有 ...