探索这两种流行的通信协议并使用它们在LCD上显示数据

到目前为止我们已经研究了如何使用一些常见的硬件,但是随着我们构建更多项目,可能希望扩展到使用各种不同的传感器、执行器、和显示。你将如何与这些沟通?有时我们可能会发现有一个MicroPython 库可供您使用,其中有人已经将低级函数转换为易于使用的界面。然而,情况并非总是如此。

幸运的是,有几种在MicroPython中实现的将低级数字设备连接在一起的标准方法:内部集成电路I2C和串行外设接口SPI。在许多方面,它们非常相似,因为它们都定义了一种在两个设备之间连接双向接口的方式。事实上,许多部件都有带有任一界面的版本,因此您可以选择适合您项目的版本。在这两种情况下,都有一个控制通信的设备(你的Pico)和一个(或多个)等待来自主设备的指令。但是,存在一些差异。我们现在将研究这两种协议,然后我们将帮助您为每个项目选择合适的协议。

电压水平

Pico的GPIO引脚工作电压为3.3伏。对它们施加更高的电压可能会损坏它们。 幸运的是,这是一个常见的工作电压,您遇到的大部分设备都可以在3.3伏电压下工作。 但是,在将一些新硬件插入Pico之前,请务必仔细检查它是否为3.3V设备,因为I2C和SPI设备有时都可以在5V下运行,这会损坏您的Pico。

I2C通信发生在两条线上:时钟(通常标记为 SCL)和数据通道(通常标记为 SDA)。 这些必须连接到Pico上的特定引脚。有几个选择;查看选项的引脚图(图 10-1)。有两条I2C总线(I2C0 和 I2C1),您可以使用其中一个或两者。在我们的示例中,我们将使用I2C0——GP0用于 SDA,GP1用于SCL。

为了演示这些协议,我们将使用SSD1306 串行OLED模块。这样做的好处是它同时具有I2C和SPI接口,因此我们可以在相同的硬件下看到两种方法之间的差异。

此LCD可以显示两行,每行最多16个字符。它是输出有关我们系统的一些信息的有用设备。让我们来看看如何使用它们。

接线I2C只是将Pico上的SDA引脚与LCD上的SDA引脚连接起来的情况,SCL也是如此。由于I2C处理通信的方式,还需要一个电阻器将SDA连接到 3.3 V,将SCL连接到3.3V。通常这些电阻约为4.7kΩ。但是,对于我们的设备,这些电阻器已经包含在内,因此我们不需要添加任何额外的电阻器。 连接好后(参见图 10-2),在屏幕上显示信息非常简单:

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设备并发送一些数据。 但是,有些位可能看起来有点不寻常。 i2c.writeto()行中的114指的是I2C设备的地址。 您可以将许多设备连接到I2C总线(稍后会详细介绍),并且每次要发送或接收数据时,都需要指定要与之通信的设备的地址。这个地址是硬连线到设备中的(尽管你可以通过在PCB上切割走线或焊接一个 blob 来改变它——有关详细信息,请参阅设备的文档)。

您应该在文档中找到您设备的地址,但您可以扫描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我们正在发送一个十六进制字符串(请参阅“十六进制”框),这是确保您发送的确切数据的常用方法。 对于我们的LCD,7C 进入命令模式,2D使LCD变黑并将光标设置在开头。 在此之后我们可以发送显示在屏幕上的数据:

file.close()

十六进制

十六进制是基数为16的编号系统。 这意味着有16位数字:0–F。 所以,十进制数10是A 十六进制。 十进制的17是十六进制的11。这样做的好处是每个字节正好是两位数。这使它成为一种紧凑但仍然可以理解的数字信息书写方式。在处理将指令作为数字值的设备(例如我们的LCD)时,您会经常遇到它。

如果您感到困惑,可以使用在线十六进制转十进制转换器在两者之间进行切换。 例如:hsmag.cc/hextodec。

当然,只显示Hello World的屏幕并没有多大用处,所以让我们来看看把它变成更有用的东西——温度计。 在第8章中,您学习了如何使用ADC使用Pico的内部温度传感器读取温度。 我们现在可以在此代码的基础上构建一个独立的温度计,它不需要计算机来读取输出。 在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 传输到外围设备的线路(请参阅“SPI 术语”框),而MISO则是将数据从外围设备传输到Pico。 CS代表芯片选择,用于将许多设备连接到单个SPI总线。您只需为CS线供电即可启用SPI外设并将其拉低以禁用它。稍微混淆一下,这个特定的设备没有CS,但/CS代表NOT CS - 换句话说,它与CS相反,所以你把它调低以启用LCD并调高以禁用它。您可以将CS连接到GPIO引脚并打开和关闭它以启用和禁用显示,但由于我们只有一个设备,我们可以简单地将其接地以保持启用状态(图 10-3)。 因此,将SerLCD的电源线连接到VBUS和GND,我们只需要将其SDO连接到Pico的MISO(GP4/SPI0 RX)、SDI到MOSI(GP3/SPI0 TX)、SCK到SCLK(GP2/SPI0 SCK),和/CS到GND。

SPI术语

SPI需要四个连接:一个从主设备到从设备获取数据,另一个从相反方向获取数据,加上电源和地。 两条数据线意味着数据可以同时双向传输。 这些通常称为主出从入(MOSI)和主入从出(MISO)。 但是,您会遇到它们具有不同的名称。 如果您查看Raspberry Pi Pico引脚分配(附录 B),它们被称为SPI TX(发送)和SPI RX(接收)。 这是因为Pico可以是一个主设备或从设备,所以这些连接是MOSI还是 MISO取决于Pico的当前功能。 在我们使用的LCD上,它们标有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时,您必须在发送数据时指定地址,而使用SPI时则不需要(但请记住,如果您连接了多个设备,您需要指定地址)需要切换CS GPIO以选择适当的设备)。

那么,如果它们如此相似,您在构建项目时应该选择哪种协议?有几个因素需要考虑。首先是您要附加的东西的可用性。有时传感器只能用作I2C或SPI,因此您必须使用它。然而,如果你有选择在硬件方面,当您使用多个额外设备时,影响最大。使用I2C,您最多可以将128个设备连接到单个I2C总线;但是,它们都需要有一个单独的地址。这些地址是硬接线的。有时可以通过可焊接(或可切割)连接来更改地址,但有时则不然。如果您想拥有多个相同类型的传感器(例如,如果您在项目的多个点监测温度),您可能会受到传感器I2C地址数量的限制。在这种情况下,SPI可能是更好的选择。

或者,SPI可以连接无限数量的设备;但是,每个人都必须有自己的CS线。在Pico上,有26个GPIO引脚。您需要其中三个用于SPI总线,这意味着有23个可用于CS线。这是假设您不需要任何其他东西。如果可用的GPIO非常珍贵,那么您可能需要查看I2C。

实际上,对于许多项目,您可以很高兴地使用任一协议,并且您可能会发现选择使用哪个协议更多地与您在零件盒中找到的零件有关,而不是两者之间的技术差异。

比特敲打

您的Pico有两条硬件I2C总线和两条硬件SPI总线。 但是,如果您愿意,您可以使用更多。 I2C和SPI 都可以用软件而不是硬件来实现。 这意味着主处理核心处理通信协议,而不是微控制器的专用位。 这是众所周知的

作为'位砰砰'。 虽然它很有用,但与使用专用硬件相比,它会给您的处理器内核带来更多压力,并且您可能会发现无法达到高波特率。 Pico有一个技巧——PIO。 我们将在本书后面(附录 C)更仔细地研究这一点,但它是微控制器中的额外硬件,可以专用于I2C和SPI等输入/输出协议。 使用PIO,您可以创建额外的I2C或SPI总线,而不会增加主处理器内核的负担。

扫描I2C总线获取设备的信息

# Scanner i2c en MicroPython | MicroPython i2c scanner
# Renvoi l'adresse en decimal et hexa de chaque device connecte sur le bus i2c
# Return decimal and hexa adress of each i2c device
# https://projetsdiy.fr - https://diyprojects.io (dec. 2017)
 
import machine
i2c = machine.I2C(scl=machine.Pin(5), sda=machine.Pin(4))
 
print('Scan i2c bus...')
devices = i2c.scan()
 
if len(devices) == 0:
  print("No i2c device !")
else:
  print('i2c devices found:',len(devices))
 
  for device in devices:  
    print("Decimal address: ",device," | Hexa address: ",hex(device))

读取测量的数据

import machine, time, bme280
i2c = machine.I2C(scl=machine.Pin(22), sda=machine.Pin(18))
bme = bme280.BME280(i2c=i2c,address=0x76)
while True:
  print("BME280 values:")
  temp,pa,hum = bme.values 
  print(temp)
  print(pa) 
  print(hum)
  time.sleep_ms(2000)

显示读取的数据

import machine, ssd1306
i2c = machine.I2C(scl=machine.Pin(22), sda=machine.Pin(18))
oled = ssd1306.SSD1306_I2C(128, 64, i2c, 0x3c)
oled.fill(0)
oled.text("Hello World", 0, 0)
oled.show()