• 【.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;
    
    // 要发给主机的数据
    // 两个字符 + ,所以是3字节
    // 但这里不需要 
    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

    您的资助是我最大的动力!
    金额随意,欢迎来赏!
    款后有任何问题请给我留言。

    如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的推荐按钮。
    如果,您希望更容易地发现我的新博客,不妨点击一下绿色通道的关注我。(●'◡'●)

    如果你觉得本篇文章对你有所帮助,请给予我更多的鼓励,求打             付款后有任何问题请给我留言!!!

    因为,我的写作热情也离不开您的肯定支持,感谢您的阅读,我是【Jack_孟】!

  • 相关阅读:
    pat03-树1. 二分法求多项式单根(20)
    pat05-图1. List Components (25)
    pat06-图4. Saving James Bond
    pat05-图3. 六度空间 (30)
    pat05-图2. Saving James Bond
    pat04-树9. Path in a Heap (25)
    pat04-树8. Complete Binary Search Tree (30)
    pat04-树7. Search in a Binary Search Tree (25)
    pat04-树5. File Transfer (25)
    Two Sum
  • 原文地址:https://www.cnblogs.com/mq0036/p/14392209.html
Copyright © 2020-2023  润新知