【.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 能识别。

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

 

 

出处:https://www.cnblogs.com/tcjiaan/p/14348436.html

posted on 2021-02-09 10:56  jack_Meng  阅读(1142)  评论(0编辑  收藏  举报

导航