I2C 总线(Inter-Integrated Circuit Bus)是设备与设备间通讯方式的一种。它是一种串行通讯总线,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以链接低速周边设备而发展[1]。I2C 总线包含两根信号线,一根为信号线 SDA ,另外一根为时钟线 SCL 。总线上能够挂载多个设备,以 7 位 I2C 地址为例,总线上最多能够挂载 27 - 1 个设备,即 127 个,地址 0x00 不用(相似于网络中的广播地址)。I2C 还包括一个子集叫 SMBus (System Management Bus),是 1995 年由 Intel 提出的[2]。为何说是子集,是由于 SMBus 是 I2C 的简化版,电气特性和传输速率等方面上略有不一样。下图展现了一个 I2C 主设备和三个 I2C 从设备的示意图,总线上只能有一个主设备,而一般状况下你的主机(如 Raspberry Pi,Arduino)就是主设备,传感器为从设备。css
图1:I2C 设备html
I2C 总线也并非那么完美。由于 I2C 只有两根信号线,与 SPI 的四根信号线相比,传输速率上并不占优,并且数据在同一时间内只能向一个方向传输。但反过来看,I2C 总线的最大优势是只须要占用两个 IO 接口,在单片机等 IO 接口数量较少的设备上也算是一种优点吧。git
在 Raspberry Pi 的引脚中,引出了一组 I2C 接口,其内部总线 ID 为 1,引脚中的 GPIO 2 为 SDA,GPIO 3 为 SCL(以下图所示)。至于 I2C-0,它用于 Raspberry Pi 内部的 GPIO 扩展器、相机、显示器等其余设备。Raspberry Pi 的 I2C 引脚中内置了一个 1.8 kΩ 的上拉电阻,这意味着在通常状况下使用 I2C 总线时没必要再链接一个额外的上拉电阻。github
Raspberry Pi B+/2B/3B/3B+/Zero 引脚图docker
I2C 操做的相关类位于 System.Device.I2c 命名空间下。编程
I2cConnectionSettings
类位于 System.Device.I2c 命名空间下,表示 I2C 设备的链接设置。c#
public sealed class I2cConnectionSettings { // 构造函数 // busId 是 I2C 总线的内部 ID,在 Raspberry Pi 上只能填 1 // deviceAddress 是要链接设备的 I2C 地址 public I2cConnectionSettings(int busId, int deviceAddress); }
I2cDevice
是一个抽象类,经过单例模式建立具体的对象。具体实现是经过两个内部类 UnixI2cDevice
和 Windows10I2cDevice
,分别表明 Unix 和 Windows10 下的 I2C 控制器。数组
public abstract partial class I2cDevice : IDisposable { // 建立 I2cDevice 对象 public static I2cDevice Create(I2cConnectionSettings settings); // 从从设备中读取一段数据,数据长度由 Span 的长度决定 public abstract void Read(Span<byte> buffer); // 从从设备中读取一个字节的数据 public abstract byte ReadByte(); // 向从设备中写入一段数据,一般 Span 中的第一个数据为要写入数据的寄存器的地址 public abstract void Write(ReadOnlySpan<byte> data); // 向从设备中写入一个字节的数据,一般这个字节为寄存器的地址 public abstract void WriteByte(byte data); // 向从设备中写入一段数据,而后从该设备中读取一段数据 // 该方法多用于 SMBus 协议 public abstract void WriteRead(ReadOnlySpan<byte> writeBuffer, Span<byte> readBuffer); }
在开始实验以前,首先说明一下 I2C 总线的读取和写入的步骤。由于 .NET 帮咱们封装好了一些操做方法,这大大简化了 I2C 的操做难度,即便你没有丰富的硬件知识也能够顺利的操做硬件,因此咱们没必要像开发单片机同样去研究设备之间通讯的时序图(固然,若是通讯出现错误的话仍是须要用时序图帮助判断)。网络
向从设备写入要读取的寄存器的地址svg
这相似于数组的指针,须要先定位到相应的位置才能读取。一般地址是一位的,只须要调用 WriteByte()
方法便可,但也有特殊状况,好比两个字节的地址或者命令+地址时,就须要调用 Write()
方法。
读取从设备中的数据
定位完成后就能够向从设备请求数据了。若是要读取一个字节的数据,那么就调用 ReadByte()
方法,若是要读取多个字节,首先须要实例化一个 byte 数组
,经过调用 Read()
方法来读取多个数据,读取的数据取决于数组的长度。好比要读取 8 个字节的数据,代码以下:
C# Span<byte> readBuffer = stackalloc byte[8]; sensor.Read(readBuffer);
写入通常用于配置从设备的寄存器。由于你不可能只向从设备写入寄存器的地址吧,因此一般会调用 Write()
方法。好比向地址为 0x01 的寄存器写入一个字节的数据,代码以下:
Span<byte> writeBuffer = stackalloc byte[] { 0x01, 0xFF }; sensor.Write(writeBuffer);
本实验选用的传感器为奥松的 DHT12。主要考虑到这个传感器读取很是简单,不用配置,价格便宜,很适合用来练手。数据手册地址:https://wenku.baidu.com/view/325b7096eff9aef8941e06f9.html 。
提示
数据手册(Datasheet)是电子元件的使用说明书,包括介绍、电气特性、通讯协议、性能等方面的内容。拿到数据手册时咱们应该关注什么?
1. 关注该元件的通讯协议。有些设备支持多种通讯协议,如本实验用到的 DHT12 不只支持 I2C,还支持 1-Wire 协议。选择合适的通讯协议进行编程。
2. 关注打算使用的通讯协议的细节。好比 I2C 总线,你须要关注元件的地址、各个寄存器的地址、最大传输速率等等。
3. 关注该元件的通讯的细节。有些设备的通讯很简单,并不须要拐弯抹角,但还有一些设备须要发送一些额外的命令。好比你在发送完寄存器地址后还须要紧接着发送一段命令,用于决定是读仍是写该寄存器,返回数据时是按字节(byte)返回仍是按字(word)返回等。
4. 关注各个寄存器的做用和配置。数据手册中基本上都会把每一个寄存器逐条列出。
名称 | 数量 |
---|---|
DHT12 | x1 |
4.7 kΩ 电阻 | x2 |
杜邦线 | 若干 |
若是你的 DHT12 是裸板的话须要像电路图中同样给 SDA 和 SCL 加上上拉电阻。
示例地址:https://github.com/ZhangGaoxing/dotnet-core-iot-demo/tree/master/src/Dhtxx
docker build -t dht-sample -f Dockerfile . docker run --rm -it --device /dev/i2c-1 dht-sample
新建类 Dht12,替换以下代码:
public class Dht12 : IDisposable { /// <summary> /// DHT12 默认 I2C 地址 /// </summary> public const byte DefaultI2cAddress = 0x5C; // 若数据手册中给的是8位的I2C地址要记得右移1位 private I2cDevice _sensor; private double _temperature; /// <summary> /// DHT12 温度 /// </summary> public double Temperature { get { ReadData(); return _temperature; } } private double _humidity; /// <summary> /// DHT12 湿度 /// </summary> public double Humidity { get { ReadData(); return _humidity; } } /// <summary> /// 实例化一个 DHT12 对象 /// </summary> /// <param name="sensor">I2C Device</param> public Dht12(I2cDevice sensor) { _sensor = sensor; } private void ReadData() { Span<byte> readBuff = stackalloc byte[5]; // 数据手册第三页提供了寄存器地址表 // DHT12 湿度寄存器地址 _sensor.WriteByte(0x00); // 连续读取数据 // 湿度整数位,湿度小数位,温度整数位,温度小数位,校验和 _sensor.Read(readBuff); // 校验数据,校验方法见数据手册第五页 // 校验位=湿度高位+湿度低位+温度高位+温度低位 if ((readBuff[4] == ((readBuff[0] + readBuff[1] + readBuff[2] + readBuff[3]) & 0xFF))) { // 温度小数位的范围在0-9,因此与上0x7F便可 double temp = readBuff[2] + (readBuff[3] & 0x7F) * 0.1; // 温度小数位第8个bit为1则表示采样得出的温度为负温 temp = (readBuff[3] & 0x80) == 0 ? temp : -temp; double humi = readBuff[0] + readBuff[1] * 0.1; _temperature = temp; _humidity = humi; } else { _temperature = double.NaN; _humidity = double.NaN; } } }
在 Program.cs 中,将主函数代码替换以下:
static void Main(string[] args) { I2cConnectionSettings settings = new I2cConnectionSettings(1, Dht12.DefaultI2cAddress); I2cDevice device = I2cDevice.Create(settings); using (Dht12 dht = new Dht12(device)) { while (true) { Console.WriteLine($"Temperature: {dht.Temperature.ToString("0.0")} °C, Humidity: {dht.Humidity.ToString("0.0")} %"); Thread.Sleep(2000); } } }
发布、拷贝、更改权限、运行