树莓派 Pico 的数字通信协议:I2C 和 SPI

前面的章节中,我们已经了解了如何使用一些常见的硬件,但随着构建的项目越来越多,你可能想要扩展到使用各种不同的传感器和显示器。要如何与这些设备通信?

有时,你可能会发现有一个 MicroPython 库可以使用,其中有人已经将低级函数转换为易于使用的接口。然而,情况并不总是这样。幸运的是,有一些连接低级数字设备的标准方法在 MicroPython 中通过 I2C 和 串行外设接口(SPI)得以实现。

I2C 和 SPI 在许多方面非常相似,因为它们都定义了一种将两个设备之间的双向接口连接的方式。事实上,许多部件都有这两种版本接口,这样你就可以选择一个适合你的项目的接口。在这两种情况下,都需要一个主控设备来控制通信(树莓派 Pico)和一个或多个设备等待来自主控设备的指令。然而,也有一些不同之处。我们会时不时地对比这两种协议帮助你从中作出合适的选择

I2C

I2C 的通信在两条线上进行:一个时钟(通常标记为 SCL)和一个数据通道(通常标记为SDA)。

这两个引脚都必须连接到 Pico 的特定引脚上。Pico 上有两个 I2C 总线(I2C0 和 I2C1),你可以使用其中一种。在我们的示例中,我们将使用 I2C0—GP0 用于 SDA,GP1 用于 SCL。

为了演示这些协议,我们将使用 SparkFun 的 SerLCD 模块。这样做的优点是它同时拥有 I2C 和 SPI 接口,因此我们可以看到在相同的硬件下这两种方法之间的区别。如果只使用 I2C 接口,也可以使用带 I2C 接口的 LCD1602 液晶屏模块替代。

这个液晶显示器可以显示两行,每一行最多 16 个字符。它是一种有用的设备,可以输出关于我们系统的信息。让我们看看如何使用它。

将 Pico 上的 SDA 引脚与 LCD 上的 SDA 引脚连接,SCL 引脚与 LCD 上的 SCL 引脚连接。由于 I2C 处理通信的方式,还需要一个电阻连接 SDA 到 3.3V 和 SCL 到 3.3V。通常是 4.7kΩ。然而,我们的设备已经包含了这些电阻,所以我们不需要添加任何额外的电阻。

图为 I2C 连接一个串行 LCD 模块,编程让它显示信息非常简单:

import machine
sda=machine.Pin(0)
scl=machine.Pin(1)
i2c=machine.I2C(0,sda=sda, scl=scl, freq=400000)
i2c.writeto(114, '\x7C')
i2c.writeto(114, '\x2D')
i2c.writeto(114, "hello world")

这段代码并没有做很多事情。它连接到 I2C 设备并发送一些数据。然而,也有一些看起来有点不寻常。writeto() 行中的 114 指的是 I2C 设备的地址。你可以将许多设备连接到 I2C 总线,每次要发送或接收数据时,都需要指定要与之通信的设备的地址。这个地址是硬连接到设备上的。

你应该在设备的文档中找到设备的地址,你也可以扫描 I2C 总线来查看当前使用的地址。设置好 I2C 总线后,可以运行 scan 方法输出当前使用的地址:

import machine
sda=machine.Pin(0)
scl=machine.Pin(1)
i2c=machine.I2C(0,sda=sda, scl=scl, freq=400000)
print(i2c.scan())

下一个看起来有点奇怪的位是写入的 \x7C 和 \x2D 命令。每一个 I2C 设备需要以特定格式发送数据。这方面没有标准,所以你必须参考设备的文档了解你正在设置的 I2C 设备。每一个开头的 \x 告诉 MicroPython,我们正在发送一个十六进制字符串,这是一种常见的方式,以确保你正在发送你想要的确切数据。对于我们的液晶显示器,7C 进入命令模式,2D 清空液晶显示器,并将光标设置到开始。接下来,我们可以发送数据显示到屏幕上:

file.close()

当然,在屏幕上只显示 Hello World 并没有多大用处,所以让我们看看如何把它变成一个更有用的东西——温度计。在之前的系列教程中,我们学习了如何使用 Pico 的内部温度传感器使用 ADC 读取温度。现在,我们可以在此代码的基础上构建一个独立的温度计,它不需要计算机读取输出。在你的 LCD 仍然像之前一样连接,运行以下代码:

import machine
import utime

sda=machine.Pin(0)
scl=machine.Pin(1)
i2c=machine.I2C(0,sda=sda, scl=scl, freq=400000)

adc = machine.ADC(4)
conversion_factor = 3.3 / (65535)

while True:
	reading = adc.read_u16() * conversion_factor
	temperature = 25 - (reading - 0.706)/0.001721
	i2c.writeto(114, '\x7C')
	i2c.writeto(114, '\x2D')
	out_string = "Temp: " + str(temperature)
	i2c.writeto(114, out_string)
	utime.sleep(2)

这看起来应该很熟悉。对之前的温度代码的唯一细微变化是,在输出计算结果之前,LCD 需要显示字符,因此我们使用 str 函数将数字转换为字符串。然后,我们可以将它与 “Temp:” 组合起来,将其构建为一个信息更丰富的输出。

如你所见,I2C 是一种将其他硬件链接到 Pico 的简单方法。你需要确保你有任何你想要连接的设备的设备文档,让你知道什么命令做什么,但只要你知道这一点,你可以很容易地添加各种各样的比特和鲍勃到你的 Pico 和创建令人印象深刻的构建。

串行外围接口

我们已经了解了 I2C 的工作原理,现在让我们来看看 SPI。我们将使用完全相同的 LCD,因此命令和其他一切都是相同的,只是我们发送数据的协议不同 SPI 有四个连接:SCLK、MOSI、MISO 和 CS(有时标记为SS)。SCLK 是时钟,MOSI 是将数据从你的 Pico 到外围设备。MISO 将数据从外围设备发送到 Pico。CS 代表芯片选择用于将多个设备连接到单个 SPI 总线。你可以把 CS 到 GPIO 引脚并切换这个开关来启用和禁用显示,但由于我们只有一个设备,我们可以简单地将它连接到 GND,以保持它的启用。因此,SerLCD 的电源线连接到 VBUS 和 GND,我们只需要连接 itsSDO 到 Pico 的 MISO (GP4/SPI0 RX),SDI 接到 MOSI (GP3/SPI0 TX),SCK 接到 SCLK(GP2/SPI0 TX),SCK 和 CS 接到 GND。

SPI 需要四个连接:一个从主服务器获取数据设备到从设备,另一个在相反的方向上获取数据,加上电源和 GND。两根数据线意味着数据可以同时在两个方向上旅行。这些通常被称为 Master Out Slave In (MOSI) 和 Master In Slave Out (MISO)。然而,你会见到不同的叫法。如果你看一下 Raspberry Pi Pico 引脚图,它们被称为 SPI TX(发送)和 SPI RX(接收)。这是因为 Pico 可以是两者之一主设备或从设备,所以无论这些连接是 MOSI 还是 MISO,取决于 Pico 的当前功能。在我们使用的液晶显示器上,它们被标记为 SDI(串行数据输入)和 SDO(串行数据输出)SPI 中没有地址,所以我们可以直接写代码:

import machine

spi_sck=machine.Pin(2)
spi_tx=machine.Pin(3)
spi_rx=machine.Pin(4)

spi=machine.SPI(0,baudrate=100000,sck=spi_sck, mosi=spi_tx, miso=spi_rx)

spi.write('\x7C')
spi.write('\x2D')
spi.write("hello world")

在这种情况下,我们使用的是 SPI0,一组可用的引脚是 GP2、GP3 和 GP4。大多数类型的串行通信有一个速度或波特率,这基本上是多少每秒可以通过信道传输的数据位。很多事情都会影响这个,比如连接的两个设备的能力和它们之间的连接(多长时间)如果有其他设备的干扰。如果你发现数据错误或丢失问题,那么你可能需要减少它。对于我们的小屏幕,我们只发送一个字节的数据字符,因此我们发送它的速度并不重要,但是对于其他一些SPI设备,微调波特率可能很重要。

让我们看看下面我们的使用的温度计代码:

import machine
import utime

spi_sck=machine.Pin(2)
spi_tx=machine.Pin(3)
spi_rx=machine.Pin(4)

spi=machine.SPI(0,baudrate=100000,sck=spi_sck, mosi=spi_tx, miso=spi_rx)

adc = machine.ADC(4)
conversion_factor = 3.3 / (65535)

while True:
	reading = adc.read_u16() * conversion_factor
	temperature = 25 - (reading - 0.706)/0.001721
	spi.write('\x7C')
	spi.write('\x2D')
	out_string = "Temp: " + str(temperature)
	spi.write(out_string)
	utime.sleep(2)

正如你所看到的,I2C 和 SPI 之间的代码实际上差别很小。一旦你完成了电路和代码,唯一的真正改变是与 I2C 你指定的地址发送数据。

那么,既然它们如此相似,在构建项目时你应该选择哪种协议呢?有需要考虑的几个因素。第一个是你想要附加的东西的可用性。有时,一个传感器只能支持 I2C 或 SPI,所以你必须使用它。但是,如果你有选择的话对于硬件来说,当你使用多个额外的设备时,影响最大。通过 I2C 你可以在一个 I2C 总线上连接多达 128 个设备;然而,它们都需要有一个单独的地址。这些地址是硬连接的。如果你想要更多相同类型的传感器,你可能会受到传感器的 I2C 地址数量的限制。在本例中,SPI 也许是更好的选择。

另外,SPI 可以有无限数量的设备连接;然而,每一个都必须有自己的 CS 引脚。在 Pico 上,有 26 个 GPIO 引脚。所以这意味着有 23 哥可用的 CS 引脚。这是假设你不需要任何其他东西。如果可用的 GPIO 非常紧缺,那么你可能需要考虑 I2C。

实际上,对于许多项目,你都可以随意地使用其中任何一种协议,你可能会发现这选择使用哪一个更多的是与你在零件箱里找到的零件有关,而不是两者在技术上的区别。

你还可以:
查看系列教程中的其他文章「树莓派 Pico 上手指南(在 Pico 上使用 MicroPython)」
购买本教程所用到的 Pico 上手套件



坐沙发

发表评论

你的邮件地址不会公开


*