六、硬件

传感器和执行器是几乎所有物联网产品不可或缺的组成部分。传感器从环境中收集数据,如温度、湿度和光照水平,并将其转化为微控制器或其他系统可以做出反应的电信号。致动器的作用正好相反:它们接收电信号,并将其转化为物理动作,比如启动马达、打开灯或播放声音。

正如有不同的网络协议定义如何在网络上共享数据一样,也有不同的硬件协议定义传感器和执行器如何与其连接的微控制器通信。可修改的 SDK 包括用于各种硬件协议的 JavaScript APIs,包括数字、模拟、PWM、伺服和 I 2 C。这些 API 使您能够从 ESP32 或 ESP8266 与现成的硬件或您自己的电路进行交互。

在本章中,您将学习如何开始编写自己的 JavaScript 代码来与硬件交互。本章包括许多例子,只需要几个简单,广泛可用,廉价的传感器和执行器。

本章中的代码使用不同的硬件协议直接与硬件通信。一旦您学会了如何使用一些常见的硬件协议,您就拥有了将使用这些协议的许多硬件组件整合到您自己的项目中所需的知识。当给计算机安装新硬件时,通常需要安装软件驱动程序,即知道如何通过底层硬件协议与硬件交互的软件;实际上,本章教你如何为各种硬件组件编写软件驱动程序。以这种方式直接控制硬件的物联网产品有很多好处,包括更精确的控制、更小的代码和更低的延迟。当然,许多组件也有软件驱动程序;在可修改的 SDK 中,您可以在modules/drivers中找到它们。

安装硬件主机

本章中的示例使用第 1 章中描述的模式进行安装:您使用mcconfig在您的设备上安装主机,然后使用mcrun安装示例应用程序。

主机在$EXAMPLES/ch6-hardware/host目录中。从命令行导航到这个目录,用mcconfig安装它。

接线注意事项

与本书中的其他章节不同,这一章要求你在运行大多数示例之前对你的设备进行额外的设置:你需要将各种传感器和执行器连接到你的设备上。如果你是新手,开始时可能会感到困惑。如果您以前这样做过,您会知道很容易出错,并且故障排除有时需要时间。本节提供了在运行示例之前应该了解的关于连接的重要信息。

遵循接线说明

本章提供了示例中使用的大多数传感器和致动器的接线表和接线图。布线图显示了 NodeMCU 板的布线,因此也显示了 NodeMCU 管脚号,如 D6 或 D7。这些标签不一定与代码中使用的 GPIO 号匹配。如果您使用的是不同的开发板,请确保查看接线表,该表提供了 GPIO 号,例如 GPIO12 或 GPIO13,以及括号中的 NodeMCU 引脚号。所有开发板的引脚标签都不同,因此您必须相应地映射引脚。可修改开发板标有“GP ”,后面是代码中使用的 GPIO 号,例如 GP12 或 GP13,因此,如果接线表显示某个引脚应连接到 GPIO12,请将其插入可修改板上标有 GP12 的引脚。

布线问题故障排除

仔细遵循接线说明很重要。如果您在布线中出错,可能会发生一些事情:

  • 会引发一个错误。这很常见,通常也是最容易解决的问题。例如,如果您交换 I 2 C 传感器的 SDA 和 SCL 引脚,您将在读取和写入时出错。利用xsbug并使用错误消息来诊断您的问题。有时候你只需要修理你的电线;其他时候,您可能有一个有故障的传感器或执行器。

  • 该应用程序可以工作,但会产生意想不到的结果。这也很常见,但是很难捕捉到。例如,如果您将传感器的数字引脚插入开发板上的错误引脚,应用程序会从没有连接任何东西的引脚读取数据;它不会抛出错误,但会给出意外的结果。如果你按下一个按钮,应用程序没有像预期的那样响应,或者你正在写一个三色 LED 的引脚,颜色没有更新,请仔细检查你的接线。

  • 你破坏了你的传感器或执行器。与前两个问题相比,这种情况不太常见,但也有可能发生。例如,当传感器设计为接受 3.3V 电压时,用 5V 电压供电会损坏传感器上的电子元件。

LED 闪烁

您可以使用 ESP32 或 ESP8266 创建的最简单的物理输出是打开和关闭板上 LED(图 6-1 )。ESP32 和 ESP8266 NodeMCU 板都有一个板载 LED 连接到引脚 2。

img/474664_1_En_6_Fig1_HTML.jpg

图 6-1

ESP8266(顶部)和 ESP32(底部)上的板载 LED

Digital类提供对设备上 GPIO 引脚的访问:

import Digital from "pins/digital";

您可以为输入或输出配置数字引脚。配置完成后,引脚的值可以是 1,表示电压为高电平,也可以是 0,表示电压为低电平。$EXAMPLES/ch6-hardware/blink示例使用Digital类和一个定时器来闪烁板载 LED。如清单 6-1 所示,该示例使用了Digital类的静态write方法,该方法将引脚(由第一个参数指定)设置为Digital.Output模式,并将其值设置为 0 或 1(第二个参数)。

let blink = 1;
Timer.repeat(() => {
    blink = blink ^ 1;
    Digital.write(2, blink);
}, 200);

Listing 6-1

或者,您可以构造一个Digital类的实例,并调用该实例的write方法。使用构造函数允许完整配置 pin。当您调用构造函数时,您传入一个带有pinmode属性的字典。以下模式值可用于数字输出引脚:

Digital.Output
Digital.OutputOpenDrain

清单 6-2 展示了编写blink例子的另一种方式,使用Digital构造函数。

let led = new Digital({
    pin: 2,
    mode: Digital.Output
});

let blink = 1;
Timer.repeat(() => {
    blink = blink ^ 1;
    led.write(blink);
}, 200);

Listing 6-2.

使用Digital的实例比使用静态Digital.write方法更有效;构造函数初始化 pin 一次,而Digital.write必须在每次写操作时初始化它。Digital.write对于不频繁的写入很方便,但是如果您的项目频繁地写入数字输出,则创建一次实例并写入它。

阅读按钮

按钮是向项目添加物理输入的一种简单方式。ESP32 和 ESP8266 NodeMCU 模块内置了两个按钮。其中一个按钮连接到数字引脚 0,可以在您的项目中用作数字输入;根据您使用的模块,此按钮标记为 FLASH、BOOT 或 IO0。

$EXAMPLES/ch6-hardware/button示例使用了Digital类和一个计时器来读取板上按钮。如清单 6-3 所示,该示例使用了Digital类的静态read方法,该方法将引脚(由第一个参数指定)设置为Digital.Input模式并读取其值,返回 0 或 1。每次按下按钮时,该示例都会跟踪到调试控制台。它还维护按钮按下次数的计数,并将其包含在输出中。

let previous = 1;
let count = 0;
Timer.repeat(id => {
    let value = Digital.read(0);
    if (value !== previous) {
        if (value)
            trace(`button pressed: ${++count}\n`);
        previous = value;
    }
}, 100);

Listing 6-3

或者,您可以构造一个Digital类的实例,并调用该实例的read方法。使用构造函数允许完整配置 pin。当您调用构造函数时,您传入一个带有pinmode属性的字典。以下模式值可用于数字输入引脚:

Digital.Input
Digital.InputPullUp
Digital.InputPullDown
Digital.InputPullUpDown

清单 6-4 展示了如何重写button示例以使用Digital构造函数。

let button = new Digital({
    pin: 0,
    mode: Digital.Input
});
let previous = 1;
let count = 0;
Timer.repeat(id => {
    let value = button.read();
    if (value !== previous) {
        if (value)
            trace(`button pressed: ${++count}\n`);
        previous = value;
    }
}, 100);

Listing 6-4.

其他数字输入模式

模式Digital.InputPullUpDigital.InputPullDownDigital.InputPullUpDown用于使能上拉和下拉电阻,这些电阻内置于 ESP32 和 ESP8266 的一些 GPIO 引脚中。这并不总是必要的,但对于如图 6-2 所示的按钮很有用,这需要一个下拉电阻来防止它在未按下的状态下接收随机噪声。你可以从 SparkFun(产品 ID COM-10302)和 Adafruit(产品 ID 1009)获得类似这样的按钮。

img/474664_1_En_6_Fig2_HTML.jpg

图 6-2

触觉按钮

$EXAMPLES/ch6-hardware/external-button示例与button示例具有相同的功能,但它使用的是类似图 6-2 中的按钮,而不是内置按钮。如果您想要运行此示例,首先按照此处给出的接线说明将其连接到您的 ESP32 或 ESP8266。

ESP32 接线说明

6-1 和图 6-3 显示了如何将按钮连接到 ESP32。

img/474664_1_En_6_Fig3_HTML.jpg

图 6-3

将按钮连接到 ESP32 的接线图

表 6-1

将按钮连接到 ESP32 的接线

|

纽扣

|

ESP32

| | --- | --- | | 压水反应堆 | 3V3 | | 联邦德国工业标准 | GPIO16 (RX2) |

ESP8266 接线说明

6-2 和图 6-4 显示了如何将按钮连接到 ESP8266。

img/474664_1_En_6_Fig4_HTML.jpg

图 6-4

将按钮连接到 ESP8266 的接线图

表 6-2

将按钮连接到 ESP8266 的接线

|

纽扣

|

ESP8266

| | --- | --- | | 压水反应堆 | 3V3 | | 联邦德国工业标准 | GPIO16 (D0) |

理解external-button代码

external-button示例使用了Digital构造函数,如下面的代码所示。它用模式Digital.InputPullDown配置引脚 16,使能引脚 16 上的内置下拉电阻。

let button = new Digital({
    pin: 16,
    mode: Digital.InputPullDown
});

代码的其余部分与重写的button示例(列表 6-4 )非常相似,除了一些小的改动以说明下拉电阻的使用。

关于上拉和下拉电阻的更多信息

ESP32 和 ESP8266 都在引脚 16 上有一个内置下拉电阻,这就是为什么external-button示例可以在任一引脚上运行,而无需对代码进行任何更改。也就是说,您可以修改它,使其使用内置下拉电阻的任何引脚,并且您构建的其他应用程序也可以使用其他引脚。ESP32 在除引脚 34–39 之外的所有 GPIO 引脚上都有内置下拉电阻,而 ESP8266 仅在引脚 16 上有内置下拉电阻。

其他传感器可能需要上拉电阻。除了引脚 34–39,ESP32 在所有 GPIO 引脚上都有内置上拉电阻;ESP8266 在 GPIO 引脚 1–15 上有内置上拉电阻。

除了使用内置电阻,您还可以将上拉或下拉电阻直接添加到传感器。如果这样做,您可以使用任何 GPIO 引脚,而不仅仅是带有内置电阻的引脚。还要注意,如果你这样做,你应该总是使用模式Digital.Input。换句话说,如果向传感器本身添加下拉电阻,不要使能内置下拉电阻;同样,如果向传感器本身添加上拉电阻,也不要使能内置上拉电阻。

监控变更

通过使用 digital Monitor类,可以更有效地检测数字输入值的变化。它使用微控制器的功能来监控变化,而不是定期轮询。Monitor的实例被配置为触发从 0 到 1 的变化(即上升沿)和/或从 1 到 0 的变化(下降沿)。当硬件检测到一个触发事件时,Monitor类调用一个回调函数。

清单 6-5 展示了button示例如何使用Monitor类。请注意,这个版本要小得多。

let monitor = new Monitor({
    pin: 0,
    mode: Digital.Input,
    edge: Monitor.Rising
});
let count = 0;
monitor.onChanged = function() {
    trace(`button pressed: ${++count}\n`);
}

Listing 6-5.

最初的button示例使用了valueprevious变量来跟踪按钮的状态;使用Monitor类大大简化了代码,因为该类跟踪按钮本身的状态,仅在状态改变时通知应用程序。

Digital构造函数一样,Monitor构造函数接受一个带有pinmode属性的字典。它还包括一个edge属性,指定触发onChanged回调的事件;edge可能是Monitor.RisingMonitor.Falling,也可能是Monitor.Rising | Monitor.Falling。应用程序必须在实例上安装一个onChanged回调,在指定的边缘事件发生时被调用。

使用Monitor类代替轮询的好处不仅仅是简化了代码。因为该类使用微控制器的内置硬件来检测变化,所以不需要运行任何代码来监视变化,从而为其他工作释放 CPU 周期。此外,监视器会立即检测到更改,而轮询方法每隔 100 毫秒才检查一次更改。当然,您可以更频繁地轮询,但是这需要更多的 CPU 周期。此外,轮询方法会错过两次读取之间发生的快速按键,而监视器总是处于活动状态,因此不会错过任何按键。

控制三色 LED

与前面的blink示例中的基本开/关单色 led 不同,三色 LED(也称为 RGB LED)将三种 LED(红色、绿色和蓝色)结合到一个封装中,使您能够精确控制颜色和亮度。控制三色 LED 的三种颜色需要四个引脚:一个引脚控制三个 LED 中的每一个,加上一个由所有颜色共享的电源引脚。

本节中的示例假设您使用的是普通阳极 LED,如图 6-5 所示,可从 SparkFun(产品 ID COM-10821)和 Adafruit(产品 ID 159)获得。

img/474664_1_En_6_Fig5_HTML.jpg

图 6-5

三色 LED

在运行示例之前,请按照说明设置三色 LED,并按照接线说明将其连接到 ESP32 或 ESP8266。

LED 设置

如图 6-6 所示,LED 要求在除电源引脚之外的所有引脚上增加限流电阻,以防止它们消耗过多的电流。使用 330 欧姆的电阻。

img/474664_1_En_6_Fig6_HTML.jpg

图 6-6

带限流电阻的三色 LED

ESP32 接线说明

6-3 和图 6-7 显示了如何将 LED 连接到 ESP32。

img/474664_1_En_6_Fig7_HTML.jpg

图 6-7

将 LED 连接到 ESP32 的接线图

表 6-3

将 LED 连接到 ESP32 的接线

|

发光二极管

|

ESP32

| | --- | --- | | 压水反应堆 | 3V3 | | 稀有 | GPIO12 (D12) | | G | GPIO13 (D13) | | B | GPIO14 (D14) |

ESP8266 接线说明

6-4 和图 6-8 显示了如何将三色 LED 连接到 ESP8266。

img/474664_1_En_6_Fig8_HTML.jpg

图 6-8

将 LED 连接到 ESP8266 的接线图

表 6-4

将 LED 连接到 ESP8266 的接线

|

发光二极管

|

ESP8266

| | --- | --- | | 压水反应堆 | 3V3 | | 稀有 | GPIO12 (D6) | | G | GPIO13 (D7) | | B | GPIO14 (D5) |

使用带三色 LED 的Digital

三色 LED 的红色、绿色和蓝色引脚连接到数字输出。在$EXAMPLES/ch6-hardware/tricolor-led-digital示例中,可以使用与在blink示例中用于控制简单单色 led 的相同的Digital类来单独控制它们。如清单 6-6 所示,区别在于你可以通过混合三原色来显示八种不同的颜色。

let r = new Digital(12, Digital.Output);
let g = new Digital(13, Digital.Output);
let b = new Digital(14, Digital.Output);
Timer.repeat(() => {
    // black (all off)
    r.write(1);
    g.write(1);
    b.write(1);
    Timer.delay(100);

    // red (red on)
    r.write(0);
    Timer.delay(100);

    // magenta (red and blue on)
    b.write(0);
    Timer.delay(100);

    // white (all on)
    g.write(0);
    Timer.delay(100);
}, 1);

Listing 6-6.

三色 LED 不仅可以显示黑色、白色、红色、绿色、蓝色、品红色、青色和黄色的原色和二次色。为了实现这一点,你需要更多的控制,而不仅仅是简单地打开和关闭红色、绿色和蓝色发光二极管;您需要能够将它们设置为 on 和 off 之间的值,即 0 和 1 之间的值。数字输出不能做到这一点,因为它的输出总是 0 或 1。在下一节中,您将学习如何克服这个限制。

使用带三色 LED 的PWM

为了显示更大范围的颜色和亮度,三色 led 可以改为使用脉宽调制PWM 来控制,这是一种常用于电机和 LED(包括三色 LED)的特殊类型的数字信号。PWM 大致相当于模拟输出,但使用数字信号产生。更具体地说,数字引脚输出具有不同高低值宽度的方波。随着时间的推移,取这些高和低脉冲的平均值,产生与脉冲宽度成比例的高和低值之间的功率水平。结果是,您可以输出介于 0 和 1 之间的任何值,而不是仅限于 0 和 1 作为输出值。

PWM类提供对 PWM 输出引脚的访问。$EXAMPLES/ch6-hardware/tricolor-led-pwm的例子使用了PWM和一个定时器来循环显示不同的颜色。

import PWM from "pins/pwm";

该示例需要三个PWM类的实例,三色 LED 上的每根电线一个,用于控制一种颜色的亮度。PWM构造函数获取一个指定 pin 号的字典。

let r = new PWM({pin: 12});
let g = new PWM({pin: 13});
let b = new PWM({pin: 14});

write方法设置引脚的当前值。您传递的值是一个从 0 到 1023 的数字,要合成的模拟值。较低的值对应较高的亮度。当应用程序运行时,它使 LED 变成绿色。PWM 值为 0 相当于数字输出设为 0,PWM 值为 1023 相当于数字输出设为 1。以下代码通过将绿色 LED 设置为全亮度并将红色和蓝色 LED 设置为关闭,将三色 LED 设置为绿色:

r.write(1023);
g.write(0);
b.write(1023);

如清单 6-7 所示,代码通过调整单个管脚的值来循环显示颜色。首先,它通过降低蓝色引脚的值将颜色从绿色变为青色。在对write的调用之间,Timer类的delay方法用于将执行延迟 50 毫秒。

while (bVal >= 21) {
    bVal -= 20;
    b.write(bVal);
    Timer.delay(50);
}
b.write(1);

Listing 6-7.

从绿色渐变为青色后,LED 从青色渐变为蓝色,蓝色渐变为洋红色,最后洋红色渐变为红色(列表 6-8 )。

while (gVal <= 1003) {
    gVal += 20;
    g.write(gVal);
    Timer.delay(50);
}
g.write(1023);

while (rVal >= 21) {
    rVal -= 20;
    r.write(rVal);
    Timer.delay(50);
}
r.write(0);

while (bVal <= 1003) {
    bVal += 20;
    b.write(bVal);
    Timer.delay(50);
}
b.write(1023);

Listing 6-8.

旋转伺服系统

伺服电机是控制旋转输出的电机。输出可以精确地转向一个弧内的特定位置,通常为 180 度。伺服系统通常用于机器人学中,以控制机器人的运动,并用于旋转物体,如照相机的镜头,以控制聚焦和变焦。图 6-9 显示了 Adafruit 提供的微型伺服系统(产品 ID 169)。像这样的微型伺服系统可以使用 ESP32 或 ESP8266 供电。也有更大,更强大的伺服移动更大的物体;这些伺服系统需要比微控制器所能提供的更多的功率来运行,因此需要外部电源。

img/474664_1_En_6_Fig9_HTML.jpg

图 6-9

Adafruit 的微伺服系统

伺服系统配置有Servo类,它使用数字引脚来控制伺服电机。

$EXAMPLES/ch6-hardware/servo示例将伺服从 0 度旋转到 180 度,每次 2.5 度。在运行该示例之前,请按照这里给出的接线说明将其连接到您的 ESP32 或 ESP8266。

ESP32 接线说明

6-5 和图 6-10 显示了如何将伺服连接到 ESP32。

img/474664_1_En_6_Fig10_HTML.jpg

图 6-10

将伺服连接到 ESP32 的接线图

表 6-5

将伺服机构连接到 ESP32 的接线

|

伺服系统

|

ESP32

| | --- | --- | | 压水反应堆 | 3V3 | | 地线 | 地线 | | 伺服(DOUT) | GPIO14 (D14) |

ESP8266 接线说明

6-6 和图 6-11 显示了如何将伺服连接到 ESP8266。

img/474664_1_En_6_Fig11_HTML.jpg

图 6-11

将伺服连接到 ESP8266 的接线图

表 6-6

将伺服机构连接到 ESP8266 的接线

|

伺服系统

|

ESP8266

| | --- | --- | | 压水反应堆 | 3V3 | | 地线 | 地线 | | 伺服(DOUT) | GPIO14 (D5) |

理解servo代码

Servo类使用数字引脚来控制伺服电机:

import Servo from "pins/servo";

servo示例创建了一个Servo类的实例,并通过调用实例的write方法定期改变位置。如清单 6-9 所示,write方法的参数是旋转到的角度;请注意,这可能是一个分数。

let servo = new Servo({pin: 14});
let angle = 0;
Timer.repeat(() => {
    angle += 2.5;
    if (angle > 180)
        angle -= 180;
    servo.write(angle);
}, 250);

Listing 6-9.

伺服旋转到新位置需要时间;时间的长短取决于你使用的伺服。根据伺服机构的不同,使用较短的时间间隔可能不会使伺服机构旋转得更快,而是可能会导致混乱的行为,因为伺服机构会尽最大努力跟上变化,而变化的速度会超过其运行速度。

Servo类也有一个writeMicroseconds方法,通过让您提供信号脉冲的微秒数(而不是度数),它允许更高的精度。可接受值的范围因伺服机构而异;将脉冲长度设置得太低或太高都会损坏伺服系统,所以一定要检查伺服系统的数据表。

获取温度

测量温度是物联网产品的一项常见任务,传感器制造商已经创建了许多不同的温度传感器。这些传感器使用各种硬件协议与微控制器通信。本节解释了两种易于使用且广泛可用的方法:

img/474664_1_En_6_Fig12_HTML.jpg

图 6-12

TMP36 传感器

  • TMP36(图 6-12 )使用模拟值来传达温度。这是两个传感器中较为简单的一个,它只有一个输出,即连接到微控制器模拟输入的模拟输出,并且没有配置选项。它可以从 SparkFun(产品 ID SEN-10988)和 Adafruit(产品 ID 165)获得。

img/474664_1_En_6_Fig13_HTML.jpg

图 6-13

TMP102 传感器

  • TMP102(图 6-13 )使用 I 2 C 总线进行温度通信。它使用 I 2 C 硬件协议进行连接,这比模拟输入复杂得多,但使传感器能够提供额外的功能和配置选项。它可以从 SparkFun 获得(产品 ID SEN-13314)。

本节还说明了如何使用传感器的数据手册来理解传感器提供的数据,并将其转换为人类可读的格式。

TMP36

$EXAMPLES/ch6-hardware/tmp36示例从 TMP36 传感器读取温度,并将以摄氏度为单位的值跟踪到调试控制台。在运行该示例之前,请按照这里给出的接线说明将 TMP36 连接到您的 ESP32 或 ESP8266。

ESP32 接线说明

6-7 和图 6-14 显示了如何将 TMP36 连接到 ESP32。

img/474664_1_En_6_Fig14_HTML.jpg

图 6-14

连接 TMP36 和 ESP32 的接线图

表 6-7

连接 TMP36 和 ESP32 的接线

|

TMP36

|

ESP32

| | --- | --- | | 压水反应堆 | 3V3 | | 模拟的 | NodeMCU 板上的 ADC0 (VP)可修改 2 上的 ADC7 (GP35) | | 地线 | 地线 |

ESP8266 接线说明

6-8 和图 6-15 显示了如何将 TMP36 连接到 ESP8266。

img/474664_1_En_6_Fig15_HTML.jpg

图 6-15

连接 TMP36 和 ESP8266 的接线图

表 6-8

连接 TMP36 和 ESP8266 的接线

|

TMP36

|

ESP8266

| | --- | --- | | 压水反应堆 | 3V3 | | 模拟的 | ADC0 (A0) | | 地线 | 地线 |

理解tmp36代码

TMP36 上的模拟引脚输出与温度成比例的电压。Analog类提供对设备上模拟输入的访问:

import Analog from "pins/analog";

与其他硬件协议类不同,Analog类从不被实例化。它只提供了一个静态方法:一个对指定管脚的值进行采样的read方法,返回值从 0 到 1023。tmp36示例调用read方法,并将返回的电压转换为温度。

TMP36 ( learn.adafruit.com/tmp36-temperature-sensor/overview )的优秀 Adafruit 教程提供了以下将电压转换为温度的公式:

温度单位为摄氏度= [(Vout 单位为毫伏)–500]/10

tmp36的例子就是基于这个公式,正如你在这里看到的:

let value = (Analog.read(0) / 1023) * 330 - 50;
trace(`Celsius temperature: ${value}\n`);

Note

如果你运行在 Moddable Two 上,由于接线不同,你需要将 pin 号从 0 改为 7。

TMP36 设计用于精确测量–40°C 至+125°C 之间的温度,对于超出此范围的温度,它会返回读数,但精度较低。模拟输入具有 10 位分辨率,读数精度约为 0.25°c,这一精度足以满足多种用途,但并非所有用途;如下所述,TMP102温度传感器为温度测量提供了更高的分辨率。

TMP102

$EXAMPLES/ch6-hardware/tmp102示例从 TMP102 传感器读取温度,并将以摄氏度为单位的值跟踪到调试控制台。在运行该示例之前,请按照这里给出的接线说明将 TMP102 连接到您的 ESP32 或 ESP8266。

本节参考 TMP102 原理图和 TMP102 数据手册,二者均可在 SparkFun 网站的 TMP102 产品页面上找到: sparkfun.com/products/13314

ESP32 接线说明

6-9 和图 6-16 显示了如何将 TMP102 连接到 ESP32。

img/474664_1_En_6_Fig16_HTML.jpg

图 6-16

连接 TMP102 和 ESP32 的接线图

表 6-9

连接 TMP102 和 ESP32 的接线

|

TMP102

|

ESP32

| | --- | --- | | 地线 | 地线 | | VCC | 3V3 | | 国家药品监督管理局 | GPIO21 (D21) | | 圣地亚哥 | GPIO22 (D22) |

ESP8266 接线说明

6-10 和图 6-17 显示了如何将 TMP102 连接到 ESP8266。

img/474664_1_En_6_Fig17_HTML.jpg

图 6-17

连接 TMP102 和 ESP8266 的接线图

表 6-10

连接 TMP102 和 ESP8266 的接线

|

TMP102

|

ESP8266

| | --- | --- | | 地线 | 地线 | | VCC | 3V3 | | 国家药品监督管理局 | GPIO5 (D1) | | 圣地亚哥 | GPIO4 (D2) |

理解tmp102代码

tmp102示例从 TMP102 检索温度数据,并将其转换为摄氏度,以输出到调试控制台。这个特殊的例子值得仔细研究,因为它引入了在大量传感器中使用的I2C硬件协议。I 2 C 是一种串行协议,用于将多个设备连接到单条双线总线。

一旦你学会了在 JavaScript 中使用 I 2 C 的基本原理,你就可以根据硬件数据表或示例实现(如 Arduino 草图)快速编写代码与新的传感器进行通信。使用 I 2 C 的 SMBus 子集的另一种方法将在下一节讨论。了解如何使用 I 2 C 和 SMBus 将使你能够探索大量可用传感器的许多选项。

请注意,本章介绍了 TMP102 的许多功能,但并非全部。数据手册是了解任何传感器性能的最佳途径。对 TMP102 的进一步研究表明,它包括了用于恒温器等的特性。

I 2 C 受欢迎的一个特点是,因为它是一条总线,它允许几个不同的传感器连接到相同的两个微控制器引脚。每个传感器都有一个唯一的地址,可以独立访问。将总线用于这种硬件协议减少了连接多个传感器所需的引脚总数,这是很有价值的,因为可用引脚的数量通常是有限的。

I2C类提供对连接到一对管脚的 I 2 C 总线的访问:

import I2C from "pins/i2c";

tmp102示例创建了一个I2C类的实例。传递给构造函数的字典包含目标设备的 I 2 C 地址。您可以通过指定sdascl属性在字典中包含 pin 号;此示例使用目标设备的默认管脚,因此不在字典中指定管脚号。ESP32 或 ESP8266 的默认引脚号与上图中的接线相匹配。电路板的地址0x48在 TMP102 原理图中给出。

let sensor = new I2C({address: 0x48});

这个I2C类的实例现在可以访问 I 2 C 总线上地址为0x48的传感器。TMP102 有四个 16 位寄存器,通过 I 2 C 使用读写来访问。寄存器如表 6-11 所示。(另请参见 TMP102 数据手册的表 1 和表 2。)

表 6-11

TMP102 寄存器

|

注册号码

|

寄存器名

|

目的

| | --- | --- | --- | | Zero | 温度 | 读取最近的温度 | | one | 配置 | 读取或设置温度转换率、电源管理等选项 | | Two | T 低电平 | 使用内置比较器时,读取或设置低温 | | three | T 高电平 | 使用内置比较器时,读取或设置高温 |

要读取或写入寄存器,首先要将目标寄存器编号写入器件。一旦完成,你就可以读取或写入寄存器值。该示例读取温度(寄存器 0),因此它首先将值 0 写入传感器,然后读取两个字节。

const TEMPERATURE_REG = 0;
sensor.write(TEMPERATURE_REG);
let value = sensor.read(2);

read方法返回名为valueUint8Array实例中的字节。它可以从目标设备读取多达 40 个字节,尽管大多数 I 2 C 读取的只是几个字节。

在读取的两个字节中,第一个字节是最高有效字节。第二个字节(最低有效位)的低 4 位设为 0,分辨率为 12 位。以下代码行将value数组中的两个字节组合成一个 12 位整数值:

value = (value[0] << 4) | (value[1] >> 4);

该值的格式如 TMP102 数据手册表 5 所示。负值以二进制补码格式表示。如果value的第一位为 1,则温度低于 0 ℃,需要额外的计算(列表 6-10 )来生成正确的负值。

if (value & 0x800) {
    value -= 1;
    value = ~value & 0xFFF;
    value = -value;
}

Listing 6-10.

最后一步是将该值转换为摄氏度,并跟踪到调试控制台。由于总分辨率为 12 位,其中 4 位用于数值的小数部分,因此 TMP102 提供的温度值精确到 0.0625°C 以内,温度读数的精确范围为–55°C 至+128°C。

value /= 16;
trace(`Celsius temperature: ${value}\n`);

使用 SMBus

系统管理总线SMBus 协议建立在 I 2 C 之上。它使用 I 2 C 定义的方法子集来形式化许多基于寄存器的 I 2 C 设备使用的约定,包括 TMP102。如前所述,TMP102 使用四个寄存器在传感器和微控制器之间读写值。要读取或写入寄存器,首先要发送寄存器号,然后发送读或写命令。

您可以像前面一样使用 I 2 C 与 SMBus 设备通信,但是由于 SMBus 设备非常常见,为了方便起见,Moddable SDK 包含了一个SMBus类:

import SMBus from "pins/smbus";

SMBus类是I2C类的子类,它的构造函数接受相同的字典参数。SMBus增加了对I2C的额外调用,以直接读写寄存器。在tmp102的例子中,使用I2C直接读取寄存器需要两次调用:一次写操作将寄存器设置为读取,然后是实际读取。SMBus将这两个调用合并成一个单独的readWord调用。

let sensor = new SMBus({address: 0x48});
let value = sensor.readWord(TEMPERATURE_REG, true) >> 4;

readWord方法有两个参数:首先是要读取的寄存器,然后是true(如果两个字节是大端顺序)或false(如果是小端顺序)(默认)。因为这里返回的第一个字节是最重要的字节,值是 big-endian,所以第二个参数是true。由于这两个字节已经组合成一个整数,剩下的就是右移 4 位来生成 12 位值。

SMBus类提供readByte来读取单个字节,提供readBlock来读取指定数量的字节。它还提供了相应的写入方法writeBytewriteWordwriteBlock

配置 TMP102

TMP102 能够支持各种配置选项,因为它通过 I 2 C 进行通信,这是一种灵活且可扩展的硬件协议。本节讨论四个这样的选项。

注意,为了简化代码,本节中的例子使用了I2CSMBus子类,而不是直接使用I2C

使用扩展模式读取更高的温度

TMP102 可以测量最高 150°C 的温度,但要做到这一点,需要将分辨率从默认的 12 位提高到 13 位,这可以通过使能扩展模式来实现。与 TMP102 的大多数选项一样,该模式由 16 位配置寄存器(寄存器 1)控制。要使能扩展模式,需要将配置寄存器中的 EM 位设为 1。

因为配置寄存器控制许多选项,为了避免无意中更改选项,代码(清单 6-11 )首先读取配置寄存器的当前值,然后设置 EM 位,最后写回该值。

const CONFIGURATION_REG = 1;
const EM_MASK = 0b0000_0000_0001_0000;

let configuration = sensor.readWord(CONFIGURATION_REG, true);
sensor.writeWord(CONFIGURATION_REG, configuration | EM_MASK,
                 true);

Listing 6-11.

在您自己的物联网产品中,您可能已经知道配置寄存器的值,而不需要读取它。在这种情况下,您可以直接设置它,无需初始读取。

使能扩展模式时,温度寄存器返回 13 位值,而不是 12 位值,这需要在摄氏度的计算中稍作调整。在 SMBus 版本中,右移位值从 4 变为 3,负数的计算也发生变化。清单 6-12 显示了修改后的代码。

let sensor = new SMBus({address: 0x48});

let configuration = sensor.readWord(CONFIGURATION_REG, true);
sensor.writeWord(CONFIGURATION_REG, configuration | EM_MASK,
                 true);

let value = sensor.readWord(TEMPERATURE_REG, true) >> 3;
if (value & 0x1000) {
    value -= 1;
    value = ~value & 0x1FFF;
    value = -value;
}

value /= 16;
trace(`Celsius temperature: ${value}\n`);

Listing 6-12.
设置转换率

转换速率是指 TMP102 每秒完成一次温度测量并更新温度寄存器中的值的次数。TMP102 完成一次温度测量大约需要 26 毫秒。默认情况下,转化率为每秒四次。从读取数据到开始下一次读取数据之间的 224 毫秒内,TMP102 进入低功耗模式,功耗降低约 94%,从 40 μA 降至 2.2 μA。

了解转换率对您的应用很重要。如果从传感器读取温度的频率高于温度更新的频率,您会收到相同的值,不必要地使用有限的 CPU 周期。另一方面,如果传感器执行温度读数的频率高于您的应用要求,那么它使用的能量会超过需要,因为它会产生未使用的读数。

转换速率由配置寄存器中的 2 位控制,因此有四种可能的值,如下所示(以及数据手册的表 8):

  • 00–每 4 秒一次(0.25 赫兹)

  • 01–每秒一次(1 赫兹)

  • 10–每秒四次(4 Hz,默认)

  • 11–每秒八次(8 赫兹)

清单 6-13 中的代码将转换率设置为每秒 8 次。

const CONVERSION_RATE_SHIFT = 6;
const CONVERSION_RATE_MASK = 0b0000_0000_1100_0000;

let configuration = sensor.readWord(CONFIGURATION_REG, true);
configuration &= ~CONVERSION_RATE_MASK;
sensor.writeWord(CONFIGURATION_REG,
        configuration | (0b11 << CONVERSION_RATE_SHIFT), true);

Listing 6-13.
关机模式节能

降低温度转换的频率可以节省能源。然而,最低的频率仍然是每 4 秒转换一次,这可能比您的物联网产品要求的频率更高。TMP102 提供关断模式,完全禁用温度转换硬件,将功耗降至 0.5 μA,您的应用可以在读数间隔期间进入关断模式,然后重新使能转换。

清单 6-14 中的代码通过设置配置寄存器中的关机模式位进入关机模式。

const SHUTDOWN_MODE_MASK = 0b0000_0001_0000_0000;

let configuration = sensor.readWord(CONFIGURATION_REG, true);
sensor.writeWord(CONFIGURATION_REG,
                 configuration | SHUTDOWN_MODE_MASK, true);

Listing 6-14.

退出关断模式类似于进入关断模式,但会清除关断模式位,而不是将其置位:

let configuration = sensor.readWord(CONFIGURATION_REG, true);
sensor.writeWord(CONFIGURATION_REG,
                 configuration & ~SHUTDOWN_MODE_MASK, true);

退出关断模式时需要记住一个重要细节,因为转换大约需要 26 毫秒,所以退出关断模式后立即读取温度寄存器会返回一个陈旧值。要在不阻止执行的情况下等待新的温度读数完成,请使用一个计时器,如清单 6-15 所示。

Timer.set(() => {
    let value = sensor.readWord(TEMPERATURE_REG);
    // Perform conversion to Celsius as before
    ...
}, 27);

Listing 6-15.
获取一次性温度读数

至此,您已经将 TMP102 传感器配置为定期连续获取温度读数。TMP102 还支持单次触发模式,只读取一个读数(参见清单 6-16 )。单次触发功能仅在器件处于关断模式时可用,读取完成后,TMP102 返回关断状态。这使得它成为最节能的方式来获取不频繁的读数,例如,如果您的产品每小时获取一次读数,或者只响应用户按下的按钮。

const ONESHOT_MODE_MASK = 0b1000_0000_0000_0000;

let configuration = sensor.readWord(CONFIGURATION_REG, true);
sensor.writeWord(CONFIGURATION_REG,
                 configuration | ONESHOT_MODE_MASK, true);

Listing 6-16.

启用单次触发模式后,需要等待读数准备就绪,大约需要 26 毫秒。然而,您可以使用一次性模式的一个特殊功能,让您知道何时可以读取,而不是等待一个固定的时间间隔。这一点很重要,因为读取读数所需的实际时间因当前温度而异。在配置寄存器中将单触发位设置为 1 后,轮询该位以了解新读数何时准备好;当温度读数正在进行时,TMP102 返回 0,当读数可用时,返回 1。清单 6-17 显示了等待读数准备就绪的代码。

while (true) {
    let configuration = sensor.readWord(CONFIGURATION_REG, true);
    if (configuration & ONESHOT_MODE_MASK)
        break;
}
// new temperature reading now available

Listing 6-17.

前面的代码在等待温度读数时阻止执行。这对于某些产品来说是可以接受的,但对于其他产品来说却不是。要执行非阻塞轮询,请使用计时器(清单 6-18 )。

Timer.repeat(id => {
    let configuration = sensor.readWord(CONFIGURATION_REG, true);
    if (!(configuration & ONESHOT_MODE_MASK))
        return;
    Timer.clear(id);
    // new temperature reading now available
}, 1);

Listing 6-18.

一次性模式还有另一个有趣的用途。由于读取温度需要大约 26 毫秒,理论上每秒可以读取大约 38 个读数。但是,请记住,配置寄存器支持的最大转换速率是每秒 8 次。使用连续、连续的单次读数,可以在硬件支持的情况下尽可能快地获取温度读数,这对于想要精确捕捉温度随时间变化的情况非常有用。

结论

既然您已经了解了一些硬件协议的基础知识,并且知道如何与一些传感器和执行器交互,那么您可以做很多事情来使提供的简单示例更加有趣。例如,你可以让执行器响应来自传感器的输入,或者利用你在第 3 章中学到的关于与云服务通信的知识,用它将数据从你的传感器传输到云端。在第 8910 章中,你将学习如何使用触摸屏,这对于显示传感器数据和构建与硬件配合使用的用户界面非常有用。

在网上和电子商店里可以买到不计其数的其他传感器和执行器。本章使用了来自 SparkFun 和 Adafruit 的一些内容,这两个网站都是电子初学者的优秀资源。除了提供许多传感器和致动器及其数据表,他们还提供了许多产品的教程,这是编写自己的 JavaScript 模块与它们进行交互的有用起点。