Arduino-研讨会第二版-全-
Arduino 研讨会第二版(全)
原文:
zh.annas-archive.org/md5/c2c45ad447836cd1a1ee0874a92fc3f7译者:飞龙
第一章:入门
你是否曾经看过某个小工具,并想知道它是如何真正工作的?也许它是遥控船、电梯、自动售货机,或是一款电子玩具。或者,你是否曾想过自己制作一个机器人,或为模型铁路制作电子信号?又或者你可能想要捕捉并分析天气数据?你该从哪里开始,如何开始?
Arduino 微控制器板(如图 1-1 所示)可以帮助你通过动手操作的方式解答一些电子学的奥秘。Arduino 系统由马西莫·班齐(Massimo Banzi)和大卫·库尔蒂埃尔斯(David Cuartielles)最初创立,提供了一种廉价的方式来构建互动项目,比如遥控机器人、GPS 跟踪系统和电子游戏。
自 2005 年推出以来,Arduino 项目迅猛发展。它现在已经成为一个蓬勃发展的产业,得到了一个共同致力于创造新事物的社区的支持。你会发现个人和团体,从小型俱乐部到地方黑客空间,再到教育机构,都在尝试使用 Arduino 制作各种物品。

图 1-1:Arduino 板
想要了解各种 Arduino 项目的多样性,只需搜索互联网。在那里,你将发现大量的项目、博客、经验和创意,展示了 Arduino 可以实现的可能性。
可能性无穷无尽
浏览本书,你会发现 Arduino 可以用来做一些简单的事情,比如让一个小灯闪烁,也可以做一些复杂的事情,比如与手机互动——以及介于两者之间的许多不同的事情。
例如,看看贝基·斯特恩(Becky Stern)的 Wi-Fi 天气显示器,不同的示例显示在图 1-2 中。它使用一块兼容 Arduino 的板子和 Wi-Fi 接口来接收本地天气预报。然后,它会显示当天的最高气温,并点亮一个彩色三角形来表示当天的天气预报。

图 1-2:天气预报显示设备的各种示例
由于可以轻松查询各种基于互联网的信息服务,你可以用它来显示天气以外的数据。欲了解更多信息,请访问www.instructables.com/id/WiFi-Weather-Display-With-ESP8266/。
如何重现过去经典的计算机?得益于 Arduino 内置处理器的强大功能,你可以模拟过去的计算机。一个例子是奥斯卡·维尔梅伦(Oscar Vermeulen)的 KIM Uno,如图 1-3 所示,它模拟了 1976 年的 KIM-1 计算机。访问en.wikipedia.org/wiki/KIM-1了解更多信息。

图 1-3:一台 Arduino 驱动的 KIM-1 模拟器
通过构建这个项目,用户可以理解早期微处理器的工作原理,这将为理解现代计算机打下基础。你可以用不到 50 美元的成本重建 Kim Uno,这个低廉的价格使得这个项目成为与有技术兴趣的其他人分享的理想工具。欲了解更多信息,请访问 obsolescence.wixsite.com/obsolescence/kim-uno-summary-c1uuh/。
还有 Michalis Vasilakis,他也喜欢自己动手制作工具,而且预算有限。他的一个很好的示例是他的 Arduino Mini CNC 绘图仪。这个项目使用了 Arduino、旧 CD 驱动器的机制和其他廉价物品,创建了一个计算机数控(CNC)设备,可以在平面表面上精确绘图(见 图 1-4)。欲了解更多信息,请访问 www.ardumotive.com/new-cnc-plotter.html。

图 1-4:Arduino Mini CNC 绘图仪
这些只是使用 Arduino 实现的一些随机示例。你可以轻松地创建自己的简单项目——而且在完成本书的学习后,更复杂的项目肯定也在你的能力范围之内。
人多力量大
如果你是一个更倾向于社交学习,喜欢以课堂为导向的环境,可以在网上搜索本地的黑客空间或爱好者小组,看看人们正在制作什么,并找到与 Arduino 相关的团体。Arduino 小组的成员可以做很多事情,比如从艺术家的角度介绍 Arduino 世界,或共同制作一个小型 Arduino 兼容板。这些小组可能非常有趣,能让你结识有趣的人,并与你他人分享你的 Arduino 知识。
部件与配件
与其他电子设备一样,Arduino 在许多零售商处都有售,提供各种产品和配件。购物时,一定要购买正品 Arduino 或高质量的衍生产品。否则,你可能会收到有缺陷或性能不佳的商品。为什么要拿你的项目去冒险,使用一个劣质的板子,最终可能会让你在长期使用中付出更多代价呢?要查看授权的 Arduino 经销商列表,请访问 arduino.cc/en/Main/Buy/。
下面是我推荐的当前供应商(按字母顺序排列),适合购买与 Arduino 相关的部件和配件:
-
Adafruit Industries (
www.adafruit.com/) -
Arduino Store USA (
store.arduino.cc/usa/) -
PMD Way (
pmdway.com/) -
SparkFun Electronics (
sparkfun.com/)
你可以在本书网站上下载本书中使用的零件清单并查看任何更新:nostarch.com/arduino-workshop-2nd-edition/。所有所需的零件都可以从上面列出的各个零售商处轻松购买,当然也可以通过其他你可能已经熟悉的零售商购买。
但不要急于购物。花些时间阅读前几章,了解你所需的东西,这样你就不会浪费钱购买不必要的物品。
所需软件
你应该可以用几乎任何计算机来编程你的 Arduino。你将首先安装一款叫做集成开发环境(IDE)的软件。要运行该软件,你的计算机需要有互联网连接,并安装以下操作系统之一:
-
macOS 10.14 64 位或更高版本
-
Windows 10 Home 32 位或 64 位,或更高版本
-
Linux 32 位或 64 位(如 Ubuntu)
IDE 自 2005 年以来一直在持续开发,目前版本为 2.x(具体数字可能会变化,但本书中的说明应该依然适用)。与版本 1.x 相比,版本 2.x 增加了一些功能,使得编写和编辑草图变得更容易,包括互动自动完成、改进的草图导航以及更友好的板卡和库管理器。此外,实时调试器允许你与某些 Arduino 板卡一起互动启动和停止 Arduino 草图。然而,如果你是 Arduino 新手,不需要担心这些功能。只需记住,Arduino 团队和社区始终在进行改进。
现在是下载并安装 IDE 的好时机,所以跳转到与你操作系统匹配的章节并按照说明进行操作。确保你有或购买适配的 USB 数据线,最好从供应商那里购买。如果你还没有 Arduino 板卡,你仍然可以下载并体验 IDE。
macOS
本节中,你将找到如何在 macOS 上下载和配置 Arduino IDE 的说明。
-
访问软件下载页面(
www.arduino.cc/en/software/)并下载适合你操作系统的最新版本 IDE。 -
双击 Arduino .dmg 文件,位于你的下载文件夹中。当安装窗口弹出时,将 Arduino 图标拖入应用程序文件夹。
-
打开 IDE,如图 1-5 所示。
![f01005]()
图 1-5:macOS 中的 IDE
-
现在配置 IDE 以支持 Arduino Uno 板卡。点击 IDE 左侧边栏顶部的图标,打开 Boards Manager。在列表中找到包含 Arduino Uno 的选项并点击安装。
-
在 IDE 顶部展开下拉菜单,点击“No Board Selected”,然后选择Select Other Board & Port。接着,从板卡列表中选择 Arduino Uno。
你可能会被提示安装 Apple 的命令行开发工具。
现在你的硬件和软件已经准备好,可以开始使用。接下来,继续阅读第 8 页的“安全使用 Arduino”部分。
Windows 10
在本节中,你将找到关于在 Windows 中下载和配置 IDE 的说明。
-
访问软件下载页面(
arduino.cc/en/software/),下载适合你操作系统的最新版本 IDE。 -
你的浏览器可能会询问你是否保存或运行下载的文件。点击运行,这样安装程序将在下载完成后自动启动。否则,可以在下载文件夹中启动 Arduino .exe文件来安装 IDE。安装完成后,运行 IDE。
-
现在配置 IDE 以支持 Arduino Uno 板卡。点击 IDE 左侧边栏顶部的图标,打开板卡管理器。找到包含 Arduino Uno 的选项并点击安装。
-
展开 IDE 顶部的下拉菜单,选择“未选择板卡”,然后选择选择其他板卡和端口。接着从板卡列表中选择 Arduino Uno。
现在你的 Arduino IDE 已经设置好,可以继续阅读第 8 页的“安全使用 Arduino”部分。
Ubuntu Linux
如果你正在使用 Ubuntu Linux,下面是下载和设置 Arduino IDE 的说明。
-
访问软件下载页面(
arduino.cc/en/software/),下载适合你操作系统的最新版本 IDE。 -
如果出现提示,选择保存文件并点击确定。
-
在归档管理器中找到 Arduino .zip文件并解压,保存到桌面。
-
在终端中导航到解压后的文件夹,输入
./arduino-ide启动 IDE。 -
现在开始配置 IDE。使用 USB 数据线将 Arduino 连接到你的电脑。
-
在 IDE 中选择工具▶ 端口,选择/dev/ttyACMx端口,其中x是一个数字(应该只有一个端口名称类似这样)。
现在你的硬件和软件已经准备好,可以开始使用。
安全使用 Arduino
就像任何爱好或手工艺一样,你需要自己照顾自己以及周围的人。正如你在本书中将看到的,我会讨论使用基本手工具、电池供电的电气设备、锋利的刀具和切割工具——有时还会使用烙铁。在你的项目中,绝不应该接触到家庭电网电流。这个工作交给受过训练的持证电工来做。记住,接触墙壁电源可能会致命。
展望未来
你即将开始一段有趣的旅程,创造出一些你以前可能从未想过的东西。本书中有 65 个 Arduino 项目,从简单到相对复杂的都有。所有项目都旨在帮助你学习并制作一些有用的东西。那么,让我们开始吧!
第二章:探索 Arduino 开发板和 IDE
在本章中,你将探索 Arduino 开发板以及你将用于创建和上传 Arduino 草图(Arduino 对其程序的称呼)的 IDE 软件。你将学习草图的基本框架和一些可以在草图中实现的基本功能,并且你将创建并上传你的第一个草图。
Arduino 开发板
Arduino 究竟是什么?根据 Arduino 官网 (www.arduino.cc/) 的定义,它是:
基于易于使用的硬件和软件的开源电子平台。它适用于任何制作互动项目的人。
简单来说,Arduino 是一个微型计算机系统,可以通过编程来与各种输入和输出设备进行交互。当前的 Arduino 开发板型号——Uno,相对于成人的手来说非常小,正如你在图 2-1 中所看到的那样。

图 2-1:Arduino Uno 相当小。
尽管对于初学者来说,它看起来可能不太起眼,但 Arduino 系统允许你创造能够与周围世界互动的设备。借助几乎无限范围的输入和输出设备,如传感器、指示灯、显示器、电动机等,你可以编程实现所需的精确交互,创造一个功能齐全的设备。例如,艺术家创造了响应路人动作的闪烁灯光装置,高中生制作了能够检测火焰并熄灭它的自动化机器人,地理学家设计了监测温度和湿度的系统,并通过短信将数据传输回办公室。实际上,快速的互联网搜索将会出现几乎无限数量的基于 Arduino 的设备示例。
让我们更详细地探索一下我们的 Arduino Uno 硬件(换句话说,"物理部分"),看看它有什么。不要太担心理解你在这里看到的内容,因为所有这些内容将在后续章节中详细讨论。
从开发板的左侧开始,你会看到两个连接器,如图 2-2 所示。

图 2-2:USB 和电源连接器
左侧是通用串行总线(USB)连接器。这个连接器将板子与电脑连接,原因有三:为板子提供电源,上传指令到 Arduino,以及向电脑发送和接收数据。右侧是电源连接器。通过这个连接器,你可以用标准的墙壁电源适配器为 Arduino 供电(当然是降压至 5 伏)。
在板子的中下部是板子的核心:微控制器,如图 2-3 所示。

图 2-3:微控制器
微控制器是 Arduino 的“大脑”。它是一个微型计算机,包含执行指令的处理器,具有各种类型的内存来存储我们的程序中的数据和指令,并提供多种途径来发送和接收数据。微控制器下方是两组小插座,如图 2-4 所示。

图 2-4:电源和模拟插座
左侧的那组提供了电源连接和使用外部 RESET 按钮的能力。右侧的那组提供了六个模拟输入,用于测量电压变化的电信号。此外,A4 和 A5 引脚也可用于与其他设备发送和接收数据。
板子顶部还有另外两组插座,如图 2-5 所示。

图 2-5:数字输入/输出引脚
编号为 0 到 13 的插座(或引脚)是数字输入/输出(I/O)引脚。它们可以检测电信号是否存在,或根据命令生成信号。0 号和 1 号引脚也被称为串口,用于与其他设备交换数据,例如通过 USB 连接电路与计算机连接。标有波浪号(~)的引脚也可以生成变化的电信号(在示波器上看起来像海浪—因此使用波浪号)。这对于创建灯光效果或控制电动机等用途非常有用。
Arduino 有一些非常有用的设备,叫做发光二极管(LED);这些非常小的设备在电流通过时会发光。Arduino 板上有四个 LED 灯:一个位于最右边,标有 ON,用于指示板子是否有电,另外三个在另一组中,如图 2-6 所示。
标有TX和RX的 LED 分别在数据通过串口和 USB 在 Arduino 与附加设备之间传输或接收时亮起。L LED 供你自己使用(它连接到数字 I/O 引脚 13 号)。LED 左侧的小黑色方块是一个微型控制器,控制着 USB 接口,允许你的 Arduino 与计算机之间传输数据,但你通常无需关心它。

图 2-6:板载 LED

图 2-7:RESET 按钮
最后,RESET 按钮如图 2-7 所示。
与普通计算机一样,Arduino 有时也可能出现问题。当其他方法都失败时,你可能需要重置系统并重新启动 Arduino。板上的简单 RESET 按钮用于重启系统,解决这些问题。
Arduino 系统的一个巨大优势是其易于扩展——也就是说,添加更多硬件功能非常简单。Arduino 两侧的两排插座允许连接扩展板,这是一块带有引脚的电路板,可以将其插入到 Arduino 中。例如,图 2-8 中显示的扩展板包含一个以太网接口,使 Arduino 能够通过网络和互联网进行通信。

图 2-8:Arduino 以太网接口扩展板
请注意,以太网扩展板也有一排排插座。这些插座允许你在上面插入一个或多个扩展板。例如,图 2-9 显示了一个包含大型数字显示器、温度传感器、额外数据存储空间和大 LED 的扩展板。
如果你在设备中使用 Arduino 扩展板,你需要记住哪个扩展板使用哪个单独的输入和输出,以确保不会发生“冲突”。你还可以购买完全空白的扩展板,允许你添加自己的电路。有关详细内容将在第七章进一步说明。

图 2-9:数字显示和温度扩展板
Arduino 硬件的伴侣是软件,一组指令告诉硬件该做什么以及如何做。
在第一章中,你已将 IDE 软件安装到个人计算机上,并将其配置为你的 Arduino 使用。现在,你将更仔细地查看 IDE,然后编写一个简单的程序——称为草图——来为 Arduino 编程。
浏览 IDE
如图 2-10 所示,Arduino IDE 类似于一个简单的文字处理器。IDE 分为三个主要区域:命令区、文本区和消息窗口区。
命令区
命令区,如图 2-10 所示,包含标题栏、菜单项和图标。标题栏显示草图的文件名(例如Blink),以及 IDE 的版本(例如Arduino 2.0.0-beta.4)。下面是一系列菜单项(文件、编辑、草图、工具和帮助)和图标,具体描述如下。

图 2-10:Arduino IDE
菜单项
与任何文字处理器或文本编辑器一样,你可以点击某个菜单项来显示其各种选项:
-
文件 包含保存、加载和打印草图的选项;一套完整的示例草图供打开使用;以及首选项子菜单
-
编辑 包含任何文字处理器常见的复制、粘贴和搜索功能
-
草图 包含在上传草图到板子之前验证草图的功能,以及一些草图文件夹和导入选项
-
工具 包含各种功能以及选择 Arduino 板类型和 USB 端口的命令
-
帮助 包含指向各种相关主题的链接以及 IDE 的版本信息
图标
菜单工具栏下方是六个图标。将鼠标悬停在每个图标上可显示其名称。图标从左到右依次如下:
-
验证 点击此处检查 Arduino 草图是否有效,并且没有任何编程错误。
-
上传 点击此处验证并上传您的草图到 Arduino 板。
-
新建 点击此处以在新窗口中打开一个新的空白草图。
-
调试 用于更复杂的 Arduino 板进行实时调试。
-
打开 点击此处打开一个已保存的草图。
-
保存 点击此处保存打开的草图。如果草图没有名称,您将被提示创建一个名称。
-
串行监视器 点击此处以打开一个新窗口,用于在 Arduino 和 IDE 之间发送和接收数据。
文本区域
文本区域显示在图 2-10 的中央。这是您将创建草图的地方。当前草图的名称显示在文本区域左上角的标签中。(默认名称为当前日期。)您将在这里输入草图内容,像在任何文本编辑器中一样。
输出窗口
输出窗口显示在图 2-10 的底部。来自 IDE 的消息出现在黑色区域。您看到的消息会有所不同,包括验证草图、状态更新等信息。
在输出窗口的右下角,您应该能够看到您的 Arduino 板类型及其连接的 USB 端口——此案例中为Arduino/Genuino Uno,COM4。
在 IDE 中创建您的第一个草图
Arduino 草图是一组指令,用来完成特定的任务;换句话说,草图就是一个程序。在本节中,您将创建并上传一个简单的草图,使 Arduino 的 LED(见图 2-11)反复闪烁,通过每秒开启一次然后关闭来实现。

图 2-11:Arduino 板上的 LED 灯,位于大写字母L旁边
首先,用 USB 线将 Arduino 连接到计算机。然后打开 IDE 并从下拉菜单中选择您的板类型(Arduino Uno)和 USB 端口类型,如图 2-12 所示。这样可以确保 Arduino 板正确连接。

图 2-12:选择 Arduino Uno 板
注释
首先,输入一条注释,以提醒您的草图将用于什么目的。草图中的注释是为用户提供的备注。注释可以是给自己或他人的提示,也可以包含说明或任何其他细节。当为您的 Arduino 创建草图时,最好添加一些关于代码意图的注释;这些注释在以后查看草图时会非常有用。
要在单行上添加注释,请输入两个斜杠,然后是注释,如下所示:
// Blink LED sketch by Mary Smith, created 07/01/2021
两个斜杠告诉 IDE 在验证草图时忽略该行文本,验证是指检查是否一切编写正确,没有错误。
若要输入跨越两行或更多行的注释,请在注释前的行中输入字符/*,并在下一行的末尾使用字符*/结束注释,如下所示:
/*
Arduino Blink LED Sketch
by Mary Smith, created 07/01/2021
*/
/* 和 */告诉 IDE 忽略它们括起来的文本。
使用以下方法之一输入描述您 Arduino 草图的注释。然后通过选择文件▶另存为来保存草图。为您的草图输入一个简短的名称(例如blinky),然后点击确定。
Arduino 草图的默认文件扩展名是.ino,并且 IDE 应自动添加此扩展名。您草图的名称应该是blinky.ino,并且应该能够在您的草图本中看到它。
setup()函数
创建任何草图的下一阶段是填写void setup()函数。该函数包含一组指令,供 Arduino 在每次重置或开机时仅执行一次。要创建setup()函数,请在注释后将以下几行添加到您的草图中:
**void setup()****{****}**
控制硬件
我们的程序将使 Arduino 上的用户 LED 闪烁。用户 LED 连接到 Arduino 的数字引脚 13。数字引脚可以检测电信号,也可以按命令生成电信号。在这个项目中,我们将生成一个电信号来点亮 LED。
将以下内容输入到您的草图中的大括号({ 和 })之间:
**pinMode(13, OUTPUT); // set digital pin 13 to output**
清单中的数字13表示您正在操作的数字引脚。您将此引脚设置为OUTPUT,这意味着它将生成一个电信号。如果您希望它检测输入的电信号,则应将引脚的模式设置为INPUT。请注意,pinMode()行的末尾有一个分号(;)。在您的 Arduino 草图中的每一行指令后面都将有一个分号。
此时保存您的草图,以确保不会丢失任何工作。
loop()函数
请记住,我们的目标是让 LED 反复闪烁。为此,我们将创建一个loop()函数,告诉 Arduino 重复执行一条指令,直到断电或有人按下 RESET 按钮。
在以下代码清单中,将加粗显示的代码输入到void setup()部分,以创建一个空的loop()函数。确保在此新部分的末尾添加另一个大括号(}),然后再次保存您的草图:
/*
Arduino Blink LED Sketch
by Mary Smith, created 07/01/21
*/
void setup()
{ pinMode(13, OUTPUT); // set digital pin 13 to output
}**void loop()****{****// place your main loop code here:****}**
接下来,将实际的函数输入到void loop()中,供 Arduino 执行。
将以下内容输入到loop()函数的大括号之间。然后点击验证以确保您输入的内容正确:
**digitalWrite(13, HIGH); // turn on digital pin 13****delay(1000); // pause for one second****digitalWrite(13, LOW); // turn off digital pin 13****delay(1000); // pause for one second**
我们来逐步分析。digitalWrite()函数控制从数字引脚输出的电压:在这种情况下,引脚 13 与 LED 连接。通过将此函数的第二个参数设置为HIGH,我们告诉引脚输出一个“高”数字电压;电流将从引脚流出,LED 将点亮。
delay()函数使草图在一段时间内不执行任何操作——在这种情况下,LED 被点亮,delay(1000)使其保持亮灯 1,000 毫秒,或 1 秒。
接下来,我们通过digitalWrite(13, LOW);关闭 LED 的电压。电流停止流过 LED,灯光熄灭。最后,我们再次暂停 1 秒钟,当 LED 关闭时,使用delay(1000);。
完整的草图应该是这样的:
/*
Arduino Blink LED Sketch
by Mary Smith, created 07/01/21
*/
void setup()
{ pinMode(13, OUTPUT); // set digital pin 13 to output
}
void loop()
{ digitalWrite(13, HIGH); // turn on digital pin 13 delay(1000); // pause for one second digitalWrite(13, LOW); // turn off digital pin 13 delay(1000); // pause for one second
}
在继续之前,保存你的草图!
验证你的草图
当你验证你的草图时,你确保它已经正确编写,以便 Arduino 能理解。要验证你的完整草图,在 IDE 中点击验证并稍等片刻。草图验证完成后,输出窗口应该会显示一条注释,如图 2-13 所示。

图 2-13:草图已被验证。
这个“编译完成”消息告诉你草图可以上传到你的 Arduino 上。它还显示了草图将使用的内存量(在这种情况下是 924 字节),以及 Arduino 上可用的总内存(32,256 字节)。
但是如果你的草图有问题呢?例如,你忘记在第二个delay(1000)函数的末尾加上分号。如果草图有问题,那么当你点击验证时,消息窗口应该显示类似于图 2-14 所示的验证错误信息。

图 2-14:带有验证错误的消息窗口
IDE 会自动显示错误(例如error: expected ';' before'}' token描述的缺少分号)。它还会突出显示错误的位置,或者就在其之后的位置。这可以帮助你轻松找到并修正错误。
上传并运行你的草图
一旦你确认草图正确输入,保存它。然后确保你的 Arduino 板与计算机连接,并在 IDE 中点击上传。IDE 将再次验证你的草图,然后将其上传到 Arduino 板。在此过程中,你的板子上的 TX/RX LED(如图 2-6 所示)应该会闪烁,表示信息正在 Arduino 和计算机之间传输。
现在是决定性时刻:你的 Arduino 应该开始运行草图。如果你做对了,LED 应该每秒闪烁一次!
恭喜你。现在你已经掌握了如何输入、验证和上传 Arduino 草图的基本操作。
修改你的草图
在运行你的草图之后,你可能想要通过调整 LED 的开关延迟时间等方式改变它的操作方式。因为 IDE 很像一个文字处理器,你可以打开保存的草图,调整数值,然后再次保存草图并上传到 Arduino。例如,要提高闪烁的速度,可以更改两个delay函数,将 LED 的闪烁时间调整为四分之一秒,通过将延迟时间调整为250,像这样:
**delay(250); // pause for one-quarter of one second**
然后再次上传草图。LED 现在应该会更快地闪烁,每次闪烁持续四分之一秒。
展望未来
拥有了如何输入、编辑、保存和上传 Arduino 草图的新知识后,你已经准备好进入下一章节,在那里你将学习如何使用更多功能,实施良好的项目设计,构建基本的电子电路,以及做更多的事情。
第三章:初步步骤
在本章中,您将
-
学习良好项目设计的概念
-
学习电的基本性质
-
介绍电阻器、发光二极管(LED)、晶体管、整流二极管和继电器
-
使用无焊面包板来构建电路
-
学习如何使用整数变量、
for循环和数字输出来创建各种 LED 效果
现在您将开始让您的 Arduino 活起来。正如您将看到的,与仅仅是板子本身一起使用 Arduino 还有更多内容。您将学习如何计划项目,以使您的想法变为现实,然后进行电力的快速入门。电力是本书中我们所做的一切的驱动力,了解基础知识对于创造自己的项目至关重要。您还将查看将真实项目带到生活的组件。最后,您将研究一些作为 Arduino 草图构建块的新功能。
规划您的项目
在开始您的前几个项目时,您可能会有冲动在想出新主意后立即编写草图。但在开始写作之前,需要进行一些基本的准备工作。毕竟,您的 Arduino 板不是读心者;它需要精确的指令,即使这些指令可以由 Arduino 执行,如果您忽略了一个细节,结果可能不会是您预期的。
无论您是创建一个简单闪烁灯光的项目还是控制自动化模型铁路信号的项目,如果您有一个详细的计划,您将更加成功。在设计您的 Arduino 项目时,请遵循以下基本步骤:
-
定义您的目标。 确定您想要实现的目标。
-
编写您的算法。 算法是一组说明,描述如何实现您的目标。您的算法将列出实现项目目标所需的步骤。
-
选择您的硬件。 确定您的硬件将如何连接到 Arduino。
-
编写您的草图。 创建告诉 Arduino 该做什么的初始程序。
-
连接它。 将您的硬件连接到 Arduino 板。
-
测试和调试。 它是否工作?在这个阶段,您将识别错误并找出其原因,无论是在草图、硬件还是算法中。
计划项目花费的时间越多,在测试和调试阶段就会越轻松。
关于电力
既然您很快将开始在 Arduino 项目中构建电子电路,让我们花点时间讨论一下电。简单来说,电是一种能量形式,我们可以利用它并将其转化为热能、光能、动能和电能。电具有三个主要属性,对我们在项目中构建过程中很重要:电流、电压和功率。
电流
电能的流动被称为电流。电流通过电路(电流的通道)从电源的正极流向电源的负极。例如,电池就是一个电源。这种流动叫做直流电(DC)。(在本书中,我们将不讨论交流电,或AC。)在一些电路中,负极被称为接地(GND)。电流的单位是安培或“安”(A);1 安培等于每秒通过某一点的 6.2415 × 10¹⁸个电子。较小的电流用毫安(mA)来衡量,1,000 毫安等于 1 安培。
电压
电压是电路正负两端之间潜在能量差的衡量标准,单位为伏特(V)。如果你将电子流动比作水流,那么电压就相当于压力:电压越大,电流在电路中的流动速度越快。
功率
功率是衡量电气设备将能量从一种形式转换为另一种形式的速度。功率的单位是瓦特(W)。例如,100 W 的灯泡比 60 W 的灯泡亮,因为功率更高的灯泡将更多的电能转换为光能。
电压、电流和功率之间有一个简单的数学关系:
- 功率(W)= 电压(V)× 电流(A)
电子组件
现在你对电的基础知识有了一些了解,我们来看看它是如何与各种电子组件和设备互动的。电子组件是控制电路中电流流动的各种部件。就像汽车引擎的各个部件共同工作以存储燃料、过滤燃料、泵送燃料和喷射燃料,帮助我们驾驶一样,电子组件协同工作来控制和利用电流的流动,帮助我们创造有用的设备。
在本书中,我会在我们使用的过程中解释一些专用组件。以下部分描述了一些基本的组件。
电阻器
各种组件,如 Arduino 的 LED,只需要少量电流就能工作——通常约为 10 mA。当 LED 接收到的电流超过其需求时,它会将多余的电流转化为热量——过多的热量可能会损坏 LED。为了减少流向 LED 等组件的电流,我们可以在电压源和组件之间添加一个电阻器。电流可以在普通铜线中自由流动,但当它遇到电阻器时,流动会被减慢。部分电流会转化为少量的热量,转化的热量与电阻器的数值成正比。图 3-1 显示了一些常用的电阻器。

图 3-1:典型电阻器
电阻
电阻的大小可以是固定的,也可以是可变的。电阻的单位是欧姆(Ω),其范围从零到数千欧姆(千欧姆,或 kΩ),甚至到数百万欧姆(兆欧姆,或 MΩ)。
读取电阻值
尽管你可以用万用表测试电阻值,也可以直接从物理电阻中读取电阻值。我们将使用的电阻器物理尺寸非常小,因此通常无法在电阻器上打印其电阻值。常见的表示组件电阻的方法是使用一系列从左到右阅读的彩色编码带,如下所示:
-
第一个电阻带 代表电阻的第一位数字
-
第二个电阻带 代表电阻的第二位数字
-
第三个电阻带 代表倍数(对于四带电阻)或第三个数字(对于五带电阻)
-
第四个电阻带 代表倍数(对于五带电阻)或公差,即元件电阻的精度(对于四带电阻)
-
第五个电阻带 显示五带电阻的公差
表 3-1 列出了电阻的颜色及其对应的值。
由于电阻器的制造精度问题,选择一个误差范围作为购买电阻时的百分比。对于五带电阻,位于第五位置的棕色带表示 1%的公差,金色表示 5%,银色表示 10%的公差。
图 3-2 展示了一个电阻示意图。黄色、紫色和橙色的电阻带分别表示 4、7 和 3,具体内容列于表 3-1。第三个电阻带表示倍数;在这个例子中,47 被乘以 10 的 3 次方,得出 47,000 Ω的值,通常读作 47 kΩ。棕色电阻带表示一个非常精确的电阻,其公差应在 1%以内。

图 3-2:电阻示意图
表 3-1:电阻上的带标值,单位为欧姆
| 颜色 | 欧姆值 |
|---|---|
| 黑色 | 0 |
| 棕色 | 1 |
| 红色 | 2 |
| 橙色 | 3 |
| 黄色 | 4 |
| 绿色 | 5 |
| 蓝色 | 6 |
| 紫色 | 7 |
| 灰色 | 8 |
| 白色 | 9 |
芯片电阻
表面贴装芯片电阻显示一个打印的数字和字母代码,如图 3-3 所示,而不是彩色条纹。前两位数字表示一个单独的数字,第三位数字表示跟随该数字后的零的个数。例如,图 3-3 中的电阻值为 10,000 Ω,或 10 kΩ。

图 3-3:表面贴装电阻
功率额定值
电阻的功率额定值是它在过热或损坏之前能够承受的功率,以瓦特为单位。图 3-1 中所示的电阻是 1/4W 电阻,这是 Arduino 系统中最常用的电阻。在本书的项目中,你只需要 1/4W 的电阻。
选择电阻时,考虑功率、电流和电压之间的关系。设计中的电流和/或电压越大,电阻的功率额定值应越大。
通常,电阻器的功率额定值越大,其物理尺寸也越大。例如,图 3-5 所示的电阻器是一个 5W 电阻器,其主体长度为 22 毫米,宽度为 10 毫米。

图 3-5:一个 5W 电阻器
发光二极管
LED 是一种非常常见、用途广泛的组件,可以将电流转化为光。LED 有各种形状、尺寸和颜色。图 3-6 显示了一个常见的 LED。

图 3-6:一个红色 LED,直径 5 毫米
在电路中连接 LED 时需要小心,因为它们是有极性的;这意味着电流只能在一个方向上进入和离开 LED。电流通过阳极(正)端进入,通过阴极(负)端离开,如 图 3-7 所示。任何试图让电流反向流过 LED 的做法都会损坏该元件。
幸运的是,LED 的设计使得你可以分辨其端点。阳极端的引脚较长(你可以将“正”端想象成“加长”了),而 LED 底部的边缘在阴极端是平的,如 图 3-8 所示。

图 3-7:LED 中的电流流动

图 3-8:LED 设计指示阳极(较长引脚)和阴极(平边缘)端
在项目中添加 LED 时,需要考虑其工作电压和电流。例如,常见的红色 LED 需要大约 1.7 V 的电压和 5 到 20 毫安的电流。这对我们来说有点问题,因为 Arduino 输出的是 5 V 的固定电压和一个更高的电流。幸运的是,我们可以使用限流电阻来减少流入 LED 的电流。但我们该选择哪个数值的电阻器呢?这就需要用到欧姆定律。
要计算 LED 所需的限流电阻,请使用以下公式:
R = (V[s] − V[f]) ÷ I
其中 V[s] 是电源电压(Arduino 输出 5 V),V[f] 是 LED 的正向电压降(假设为 1.7 V),I 是 LED 所需的电流(10 毫安)。(I 的单位必须是安培,所以 10 毫安转换为 0.01 A。)
现在,让我们将这个公式应用到我们的 LED 上,使用 V[s] = 5 V,V[f] = 1.7 V 和 I = 0.01 A。将这些数值代入公式中,得出 R 的值为 330 Ω。然而,当电流低于 10 毫安时,LED 也能正常发光。为了保护敏感的电子元件,最好尽量使用较小的电流,因此我们将使用 560 Ω、1/4W 的电阻器与我们的 LED 配合使用,这样可以使电流流过约 6 毫安。
无焊面包板
我们不断变化的电路需要一个基础——一个将它们连接在一起并构建其上的平台。一个很好的工具就是无焊接面包板。面包板是一个带有电气连接插座的塑料基座(只是不可以用它切面包)。它们有多种尺寸、形状和颜色,如图 3-10 所示。

图 3-10:各种形状和尺寸的面包板
使用面包板的关键是了解插座的连接方式——是按短列连接,还是按沿边缘或中央的长行连接。连接方式因面包板而异。例如,在图 3-11 顶部所示的面包板中,五个孔的列是垂直连接的,但水平方向上是隔离的。如果你将两根导线插入同一垂直行,它们就会被电气连接。同理,位于水平线之间的中央长行也是水平连接的。你经常需要将电路连接到电源电压和地线,这些长的水平孔线非常适合这个目的。
当你在构建更复杂的电路时,面包板会变得拥挤,你并不总能将组件放置在你想要的地方。然而,使用短连接线很容易解决这个问题。销售面包板的零售商通常也会销售各种长度的小盒子电线,例如在图 3-12 中所示的那种。

图 3-11:面包板内部连接

图 3-12:各种面包板连接线
项目 #1:创建一个闪烁的 LED 波动
让我们来用一些 LED 和电阻器。在这个项目中,我们将使用五个 LED 来模拟电视节目《骑士杀手》中的著名车辆 KITT 的前端,创建一种波动的光模式。
算法
这是我们这个项目的算法:
-
打开 LED 1。
-
等待半秒钟。
-
关闭 LED 1。
-
打开 LED 2。
-
等待半秒钟。
-
关闭 LED 2。
-
继续,直到 LED 5 被打开,此时过程会从 LED 5 反向到 LED 1。
-
无限重复。
硬件
这是你创建这个项目所需的材料:
-
五个 LED
-
五个 560 Ω 电阻器
-
一块面包板
-
各种连接线
-
Arduino 和 USB 电缆
我们将通过 560 Ω 限流电阻器,将 LED 连接到数字引脚 2 到 6。
原理图
现在让我们来构建电路。电路布局可以通过几种方式描述。在本书的前几个项目中,我们将使用类似于图 3-13 所示的物理布局图。
通过将接线图与草图中的功能进行对比,你可以开始理解电路。例如,当我们使用digitalWrite(2, HIGH)时,5V 的高电压从数字引脚 2 流出,通过限流电阻,再通过 LED 的阳极和阴极,最终回到 Arduino 的 GND 插槽,完成电路。然后,当我们使用digitalWrite(2, LOW)时,电流停止,LED 熄灭。

图 3-13:Project 1 的电路布局
草图
现在是草图的部分。将以下代码输入到 IDE 中:
// Project 1 - Creating a Blinking LED Wave1 void setup()
{ pinMode(2, OUTPUT); // LED 1 control pin is set up as an output pinMode(3, OUTPUT); // same for LED 2 to LED 5 pinMode(4, OUTPUT); pinMode(5, OUTPUT); pinMode(6, OUTPUT);
} 2 void loop()
{ digitalWrite(2, HIGH); // Turn LED 1 on delay(500); // wait half a second digitalWrite(2, LOW); // Turn LED 1 off digitalWrite(3, HIGH); // and repeat for LED 2 to 5 delay(500); digitalWrite(3, LOW); digitalWrite(4, HIGH); delay(500); digitalWrite(4, LOW); digitalWrite(5, HIGH); delay(500); digitalWrite(5, LOW); digitalWrite(6, HIGH); delay(500); digitalWrite(6, LOW); digitalWrite(5, HIGH); delay(500); digitalWrite(5, LOW); digitalWrite(4, HIGH); delay(500); digitalWrite(4, LOW); digitalWrite(3, HIGH); delay(500); digitalWrite(3, LOW);
// The loop() will now loop around and start from the top again
}
在void setup()中的第 1 行,数字 I/O 引脚被设置为输出,因为我们希望它们根据需求向 LED 发送电流。我们通过在草图的void loop()部分的第 2 行使用digitalWrite()函数来指定何时开启每个 LED。
运行草图
现在连接你的 Arduino 并上传草图。过一两秒后,LED 应该会从左到右闪烁,然后再返回。成功是件美妙的事——拥抱它吧!
如果什么都没有发生,那么立即拔掉 Arduino 的 USB 电缆,检查你是否正确输入了草图。如果发现错误,修正它并重新上传草图。如果草图完全匹配且 LED 仍然不闪烁,检查面包板上的接线。
你现在已经知道如何用 Arduino 使 LED 闪烁,但这个草图有些低效。例如,如果你想修改它让 LED 闪烁得更快,你就需要更改每个delay(500)。有一种更好的方法。
使用变量
在计算机程序中,我们可以使用变量来存储数据。Project 1 的草图存在一个问题,因为它没有使用变量,所以不够灵活。例如,我们使用delay(500)函数来保持 LED 亮着。如果我们想更改延迟时间,就必须手动更改每个条目。为了解决这个问题,我们将创建一个变量来表示delay()函数的值。
将以下行输入到 Project 1 的草图中,位于void setup()函数之前,并紧跟在初始注释之后:
**int d = 250;**
这将数字250赋给一个名为d的变量。int表示该变量包含一个整数——一个介于-32,768 到 32,767 之间的整数。简单来说,任何整数值都没有小数部分。
接下来,将草图中的每个500更改为d。现在,当草图运行时,Arduino 将使用d中的值来执行delay()函数。上传修改后的草图后,LED 将以更快的速度闪烁,因为延迟值变得更小。
现在,如果你想更改延迟,只需更改草图开始处的变量声明。例如,输入100作为延迟值将使得 LED 闪烁得更快:
**int d = 100;**
尝试修改草图,或许可以改变延时和 HIGH、LOW 的顺序。可以尽情尝试一下。不过,暂时别拆解电路;我们将在本章的更多项目中继续使用它。
项目 #2:使用 for 循环进行重复
在设计草图时,常常需要重复同一个函数。你可以简单地复制粘贴函数来重复它,但这样效率低下,浪费了 Arduino 的程序内存。相反,你可以使用 for 循环。使用 for 循环的好处是你可以决定循环内部代码的重复次数。
要查看 for 循环的工作方式,请输入以下代码作为新的草图:
// Project 2 - Repeating with for Loops
int d = 100;
void setup()
{ pinMode(2, OUTPUT); pinMode(3, OUTPUT); pinMode(4, OUTPUT); pinMode(5, OUTPUT); pinMode(6, OUTPUT);
}
void loop()
{ for ( int a = 2; a < 7 ; a++ ) { digitalWrite(a, HIGH); delay(d); digitalWrite(a, LOW); delay(d); }
}
for 循环将在大括号内的代码只要某个条件为真时就会重复执行。在这里,我们使用了一个新的整数变量 a,其初始值为 2。每次执行代码时,a++ 会将 a 的值增加 1。只要 a 的值小于 7(条件),循环就会继续。当 a 的值等于或大于 7 时,Arduino 会继续执行 for 循环之后的代码。
for 循环执行的循环次数也可以通过从较大的数字倒数到较小的数字来设置。为演示这一点,请在第一个 for 循环之后,向项目 2 的草图中添加以下循环:
1 for ( int a = 5 ; a > 1 ; a-- )
{ digitalWrite(a, HIGH); delay(d); digitalWrite(a, LOW); delay(d);
}
在这里,for 循环在 1 时将 a 的值设置为 5,并在每次循环后通过 a-- 将其减 1。只要 a 的值大于 1(a > 1),循环就会继续,直到 a 的值降到 1 或小于 1 为止。
我们现在用更少的代码重新创建了项目 1。上传草图并亲自体验一下吧!
使用脉宽调制(PWM)调整 LED 亮度
与其仅使用 digitalWrite() 快速地打开和关闭 LED,我们可以通过调整每个 LED 开关状态之间的时间来定义 LED 的亮度级别,方法是使用 脉宽调制(PWM)。PWM 可以通过快速开关 LED(每秒约 500 次)来创造 LED 在不同亮度级别的假象。我们感知到的亮度取决于数字输出引脚开启的时间与关闭的时间——即 LED 点亮或熄灭的时间。由于我们的眼睛无法看到每秒 50 次以上的闪烁,LED 看起来就像是常亮的。
占空比 越大(即每个周期内引脚保持开启的时间越长),连接到数字输出引脚的 LED 显得越亮。
图 3-14 显示了不同的 PWM 占空比。填充的灰色区域表示灯亮的时间。从图中可以看到,灯亮的时间随着占空比的增大而增加。

图 3-14:不同的 PWM 占空比
只有常规 Arduino 板上的数字引脚 3、5、6、9、10 和 11 可以用于 PWM。它们在 Arduino 板上标有波浪线(~),如图 3-15 所示。

图 3-15:PWM 引脚标有波浪线(~)。
要创建一个 PWM 信号,我们使用函数 analogWrite(``x``, y``),其中 x 是数字引脚,y 是占空比的值。y 可以是 0 到 255 之间的任何值,其中 0 表示 0% 的占空比,255 表示 100% 的占空比。
项目 #3:演示 PWM
现在让我们在项目 2 的电路基础上尝试一下。将以下草图输入 IDE,并上传到 Arduino:
// Project 3 - Demonstrating PWM
int d = 5;
void setup(){ pinMode(3, OUTPUT); // LED control pin is 3, a PWM-capable pin
}
void loop()
{ for ( int a = 0 ; a < 256 ; a++ ) { analogWrite(3, a); delay(d); } for ( int a = 255 ; a >= 0 ; a-- ) { analogWrite(3, a); delay(d); } delay(200);
}
数字引脚 3 上的 LED 将展示“呼吸效果”,即占空比增加和减少时,LED 的亮度逐渐增大,直到完全亮起,然后反向直到变暗。换句话说,LED 会亮起,亮度逐渐增强,直到完全亮起,然后再反向直到熄灭。你可以在草图和电路中进行实验。例如,可以让所有五个 LED 同时呼吸,或者让它们依次进行呼吸效果。
更多电气组件
你通常会发现,计划让数字输出完成某项工作时,未考虑到控制所需的电流大小。创建项目时,请记住,Arduino Uno 上的每个数字输出引脚最多可以提供 40 mA 的电流,所有引脚的总电流最大为 200 mA。然而,接下来讨论的三种电子硬件组件可以帮助你提高 Arduino 的电流处理能力。
晶体管
几乎每个人都听说过晶体管,但大多数人并不真正理解它是如何工作的。为了简洁起见,我将尽量简单地解释。晶体管可以打开或关闭比 Arduino Uno 能处理的电流更大的电流流动。然而,我们可以使用 Arduino 数字输出引脚安全地控制晶体管。一个常用的晶体管是 BC548,如图 3-16 所示。

图 3-16:典型的晶体管:BC548
与 LED 类似,晶体管的引脚也有独特的功能,并需要按照正确的方向连接。将晶体管的平面部分朝向你(如图 3-16 左侧所示),BC548 的引脚从左到右分别称为集电极(C)、基极(B)和发射极(E)。(注意,这种引脚顺序或引脚排列是针对 BC548 晶体管的;其他晶体管的排列方式可能不同。) 当在基极施加小电流时,例如来自 Arduino 数字 I/O 引脚,小电流会使我们希望开关的大电流通过集电极进入。在集电极流入后,电流与来自基极的小电流合并,然后通过发射极流出。当基极的控制电流被关闭时,晶体管中就不会有电流流动。
BC548 可在最大 30 V 的电压下切换最多 100 mA 的电流——远高于 Arduino 的数字输出。在本书后续的项目中,你将更详细地了解晶体管。
整流二极管
二极管 是一种非常简单但又非常有用的元件,它只允许电流单向流动。它看起来和电阻器很相似,如图 3-17 所示。

图 3-17:1N4004 型整流二极管
本书中的项目将使用 1N4004 型整流二极管。电流通过二极管的阳极进入,通过阴极流出,阴极处有一个环形标记在二极管的主体上。这些二极管可以保护电路的部分免受反向电流的影响,但也有代价:二极管会导致大约 0.7 V 的电压下降。1N4004 二极管的额定值为 1 A 和 400 V,远高于我们实际使用的电流和电压。它是一种耐用、常见且低成本的二极管。
继电器
继电器 与晶体管使用的原因相同——用于控制大电流和大电压。继电器的优点是它与控制电路电气隔离,使得 Arduino 可以切换非常大的电流和电压,而无需直接接触这些电压,从而避免损坏 Arduino。继电器内部有一对有趣的组件:机械开关触点和一个低电压线圈,如图 3-18 所示。


图 3-18:典型继电器内部结构
当电流作用于继电器时,线圈变成了电磁铁,吸引一根金属条,金属条的作用就像开关的拨动器。当电流接通时,磁铁会将金属条拉向一个方向,而当电流断开时,它会让金属条落回去,从而根据电流的接通或断开控制开关的开关状态。这一运动会发出独特的“咔哒”声,你可能从老式汽车的转向信号灯中听到过这个声音。
高电压电路
现在你已经了解了一些关于晶体管、整流二极管和继电器的知识,让我们将它们结合起来控制更高的电流和电压。例如,你可能希望打开或关闭一个大功率电动机。连接这些组件非常简单,如图 3-19 所示。

图 3-19:继电器控制电路
这个简单的示例电路控制一个 12V 线圈的继电器。这个电路的一种用途可能是控制连接到继电器切换触点的灯泡或冷却风扇。Arduino 的数字引脚 10 通过一个 1 kΩ的电阻连接到晶体管的基极。晶体管通过开关继电器线圈来控制电流的通断。当你观察晶体管的平面时,记住引脚顺序是C、B,然后是E。面包板左侧位置 1 上的物体代表继电器线圈的 12V 电源。位置 2 的 12V 电源的负极或地线、晶体管的发射极引脚和 Arduino 的 GND 都连接在一起。最后,一个 1N4004 整流二极管连接在继电器线圈的 3 号位置,阴极接在正电源侧。你可以查阅继电器的数据表来确定接触点的引脚,并将受控设备正确连接。
二极管的作用是保护电路。当继电器线圈从开到关时,线圈中会短暂地残留一些游离电流,这些电流会形成一个高压尖峰,需要释放。二极管允许这些游离电流通过线圈回路,直到它以微量热量的形式被耗散掉。它可以防止关闭尖峰损坏晶体管或 Arduino 引脚。
展望未来
现在第三章接近尾声。希望你在尝试示例和实验 LED 效果时玩得开心。在本章中,你通过多种方式创建了 Arduino 上的闪烁 LED,做了一些黑客实验,并学习了如何使用函数和循环来高效地控制连接到 Arduino 的组件。学习了这一章,为接下来的章节打下了更成功的基础。
第四章将会非常有趣。你将创建一些更先进的项目,包括交通信号灯、温度计、电池测试器等——所以当你准备好迎接更高挑战时,翻到下一页吧!
第四章:构建模块
在本章中,你将
-
学习如何阅读原理图,这是电子电路的“语言”
-
了解电容器
-
使用输入引脚
-
使用算术和测试值
-
使用
if语句做决策 -
学习模拟与数字的区别
-
在不同的精度水平下测量模拟电压源
-
了解可变电阻器、压电蜂鸣器和温度传感器
-
通过制作交通灯、电池测试仪和温度计来巩固你的知识
本章的内容将帮助你理解 Arduino 的潜力。我们将继续学习电子学,包括如何阅读原理图(电子电路的“路线图”)。我们还将探索一些新的元件和我们可以测量的信号类型。然后我们将讨论更多 Arduino 功能,如存储值、执行数学运算和做决策。最后,我们将审视一些其他元件,并在一些有用的项目中加以使用。
使用原理图
第三章描述了如何使用物理布局图来构建电路,这些布局图代表了面包板及其上安装的元件。虽然这种物理布局图看起来是绘制电路的最简单方式,但你会发现,随着更多元件的加入,直接表示的图纸会变得非常混乱。因为我们的电路将变得更加复杂,所以我们将开始使用原理图(也称为电路图)来说明它们,如图 4-1 所示。

图 4-1:原理图示例
原理图只是电路的路线图,显示了电流在各个元件中流动的路径。与显示元件和电线不同,原理图使用符号和线条来表示。
识别元件
一旦你知道这些符号的含义,读取原理图就变得容易了。首先,让我们来看看我们已经使用过的元件符号。
Arduino
图 4-2 展示了 Arduino 本身的符号。如你所见,所有 Arduino 的连接都被显示并清晰标记。

图 4-2:Arduino Uno 符号
电阻器
电阻器符号如图 4-3 所示。

图 4-3:电阻器符号
在电阻符号旁边显示电阻值和元件标识符(例如此处的 220 Ω和 R1)是一个很好的做法。这使得每个人在理解原理图时都更加轻松(包括你自己)。你常常会看到电阻值以R表示——例如 220 R。
整流二极管
整流二极管符号如图 4-4 所示。

图 4-4:整流二极管符号
回顾第三章,整流二极管是有极性的,电流从阳极流向阴极。在图 4-4 所示的符号中,阳极位于左侧,阴极位于右侧。记住这一点的一个简单方法是想象电流只会流向三角形的尖端。电流不能反向流动,因为垂直条形“阻止”它。
LED
LED 符号如图 4-5 所示。

图 4-5:LED 符号
二极管家族的所有成员都有一个共同的符号:三角形和垂直线。然而,LED 符号显示两个平行箭头指向三角形的外侧,表示光正在发出。
晶体管
晶体管符号如图 4-6 所示。我们将用这个符号表示我们的 BC548。

图 4-6:晶体管符号
符号顶部的垂直线(标记为C)表示集电极,左侧的水平线表示基极(标记为B),底部的线表示发射极(标记为E)。符号内的箭头指向下方和右侧,告诉我们这是一个NPN型晶体管,因为 NPN 晶体管允许电流从集电极流向发射极。(PNP型晶体管允许电流从发射极流向集电极。)
在编号晶体管时,我们使用字母Q,就像我们用R来编号电阻器一样。
继电器
继电器符号如图 4-7 所示。

图 4-7:继电器符号
继电器符号可以有多种变体,且可能有多组触点,但所有继电器符号都有一些共同的元素。第一个是线圈,即左侧的弯曲垂直线。第二个元素是继电器触点。COM(公共端)触点通常用作输入,而标记为NO(常开)和NC(常闭)的触点通常用作输出。
继电器符号总是显示在继电器处于断开状态,且线圈未通电的情况下——也就是说,COM 和 NC 引脚连接在一起。当继电器线圈通电时,符号中 COM 和 NO 引脚将连接。
原理图中的电线
当电线在原理图中交叉或连接时,它们会按照特定的方式绘制,如下例所示。
交叉但未连接的电线
当两根电线交叉但未连接时,交叉可以通过两种方式表示,如图 4-8 所示。没有一种正确的方法,这取决于个人偏好。

图 4-8:未连接的交叉电线
连接的电线
当电线要物理连接时,会在连接点绘制连接点,如图 4-9 所示。

图 4-9:连接的两根电线
连接到地的电线
当一根线连接回地线(GND)时,标准方法是使用图 4-10 所示的符号。

图 4-10:GND 符号
本书中原理图中线路末端的 GND 符号告诉你,这根线与 Arduino 的 GND 引脚物理连接。
解读原理图
现在你已经了解了各种元件及其连接的符号,接下来让我们分析我们将在第三章第 33 页为项目 1 绘制的原理图。回想一下,你让五个 LED 前后闪烁。
比较图 4-11 所示的原理图与第三章第 34 页的图 3-13,你会发现使用原理图是一种更简便的方式来描述电路。

图 4-11:项目 1 的原理图
从现在开始,我们将使用原理图来描述电路,并且在引入新元件时,我会向你展示它们的符号。
电容器
电容器是一种存储电荷的设备。它由两块导电板夹着一个绝缘层组成,允许电荷在两板之间积聚。当电流被切断时,电荷保持在电容器内,并可以在电容器中储存的电压遇到电流的新路径时(称为放电)流出。
测量电容器的容量
电容器能存储的电荷量以法拉(farad)为单位,一法拉实际上是一个非常大的数值。因此,通常电容器的容量以皮法(pF)或微法(μF)为单位进行标注。一皮法(pF)是法拉的 0.000000000001 倍,而一微法(μF)是法拉的 0.000001 倍。电容器也有额定的最大电压值。在本书中,我们只会使用低电压,因此我们不会使用额定电压超过 10 V 的电容器;然而,通常可以在低电压电路中使用高电压电容器。常见的电压额定值为 10、16、25 和 50 V。
阅读电容器数值
阅读陶瓷电容器的数值需要一些练习,因为其数值是以某种代码方式打印的。前两位数字代表皮法拉(pF)的数值,第三位数字是以十为倍数的乘数。例如,图 4-12 所示的电容器标注为104。这相当于 10 后面跟着四个零,即 100,000 pF(即 100 纳法(nF)或 0.1 微法(μF))。
电容器类型
我们的项目将使用两种类型的电容器:陶瓷电容器和电解电容器。
陶瓷电容器
陶瓷电容器,例如图 4-12 所示的电容器,非常小,因此储存的电荷量也很小。它们是非极性的,可以用于电流双向流动。非极性电容器的原理图符号如图 4-13 所示。

图 4-12:一个 0.1 µF 陶瓷电容器

图 4-13:非极性电容器电路符号,电容器的数值显示在右上角
陶瓷电容器在高频电路中表现优异,因为它们可以由于较小的电容而非常快速地充放电。
电解电容器
电解电容器,如图 4-14 所示,物理尺寸大于陶瓷类型,提供更大的电容,并且是极性的。外壳上的标记显示了正(+)极或负(–)极。在图 4-14 中,你可以看到条纹和标识负极的小负号(–)符号。像电阻一样,电容器的数值也有一定的公差。图 4-14 中的电容器公差为 20%,电容值为 100 μF。

图 4-14:电解电容器
电解电容器的电路符号,如图 4-15 所示,包含+符号以表示电容器的极性。

图 4-15:极性电容器电路符号
电解电容器常用于存储较大的电荷并平滑电源电压。像一个小型的临时电池,它们可以平滑电源并提供在快速从电源抽取大电流的电路或部件附近的稳定性。这可以防止电路中出现不必要的掉电和噪声。幸运的是,电解电容器的数值清晰地印刷在外面,不需要解码或解释。
你已经有了一些使用 LED 生成基本输出的经验。现在是时候学习如何通过数字输入将外部世界的信息传入 Arduino,并根据这些输入做出决策。
数字输入
在第三章中,我们使用数字 I/O 引脚作为输出,控制 LED 的开关。我们可以使用这些相同的引脚接受用户的输入——只要我们将信息限制为两种状态,高电平和低电平。
数字输入的最简单形式是按钮;图 4-16 中展示了几个按钮。你可以将其中一个按钮直接插入无焊面包板并将其连接到 Arduino 引脚。当按钮被按下时,电流通过开关流入数字输入引脚,后者检测到电压的存在。

图 4-16:面包板上的基本按钮
注意,图底部的按钮已插入面包板,桥接了第 23 行和第 25 行。当按钮被按下时,它将连接这两行。该按钮的电路符号如图 4-17 所示。符号代表按钮的两侧,两侧的编号以S为前缀。当按钮被按下时,线路桥接两部分,允许电压或电流通过。

图 4-17:按钮电路符号
项目 #4:演示数字输入
我们在这个项目中的目标是创建一个按钮,当按下时,点亮 LED 半秒钟。
算法
这是我们的算法:
-
测试按钮是否被按下。
-
如果按钮已被按下,点亮 LED 半秒钟,然后将其关闭。
-
如果按钮未被按下,则不执行任何操作。
-
无限重复。
硬件部分
以下是你需要的物品来创建这个项目:
-
一个按钮开关
-
一个 LED
-
一个 560 Ω 电阻
-
一个 10 kΩ 电阻
-
一个 100 nF 电容器
-
各种连接线
-
一个面包板
-
Arduino 和 USB 电缆
电路图
首先,我们根据 图 4-19 中的电路图在面包板上搭建电路。注意,10 kΩ 电阻连接在 GND 和数字引脚 7 之间。我们称之为 下拉电阻,因为它将数字引脚的电压几乎拉到零。此外,通过在 10 kΩ 电阻两端添加 100 nF 电容器,我们创建了一个简单的 去抖动 电路,帮助滤除开关抖动。当按钮按下时,数字引脚立即变为高电平。但是当按钮松开时,数字引脚 7 通过 10 kΩ 电阻被拉到 GND,并且 100 nF 电容器产生了一个小的延迟。这有效地覆盖了抖动脉冲,减缓了电压下降到 GND 的速度,从而消除了大部分由于浮动电压和按钮不稳定行为引起的错误读取。

图 4-19:项目 4 的电路图
因为这是你第一次根据电路图搭建电路,所以请按照这些逐步说明来完成电路图的操作;这将帮助你理解各个组件如何连接:
-
将按钮插入面包板,如 图 4-20 所示。
![f04020]()
图 4-20:按钮插入面包板
-
现在插入 10 kΩ 电阻、一根短接电线和电容器,如 图 4-21 所示。
![f04021]()
图 4-21:添加 10 kΩ 电阻和电容器
-
从 Arduino 5 V 引脚连接一根电线到面包板上按钮的右上行。再连接另一根电线从 Arduino GND 引脚到与电线连接和电阻的左侧相连的同一垂直行。这如 图 4-22 所示。
![f04022]()
图 4-22:连接 5 V(红色)和 GND(黑色)电线
-
从 Arduino 数字引脚 7 连接一根电线到面包板上按钮的右下行,如 图 4-23 所示。
![f04023]()
图 4-23:将按钮连接到数字输入
-
将 LED 插入面包板,将短腿(阴极)连接到 GND 列,长腿(阳极)插入右侧的某一行。接着,将 560 Ω电阻连接到 LED 的右侧,如图 4-24 所示。
![f04024]()
图 4-24:插入 LED 和 560 Ω电阻
-
将一根导线从 560 Ω电阻的右侧连接到 Arduino 数字引脚 3,如图 4-25 所示。

图 4-25:将 LED 分支连接到 Arduino
在继续之前,查看该电路的原理图,检查你的元件是否正确连接。将原理图与实际电路连接进行对比。
草图
对于草图,输入并上传列表 4-1。
// Listing 4-1, Project 4 - Demonstrating a Digital Input1 #define LED 3
#define BUTTON 7
void setup()
{2 pinMode(LED, OUTPUT); // output for the LED pinMode(BUTTON, INPUT); // input for the button
}
void loop()
{ if ( digitalRead(BUTTON) == HIGH ) { digitalWrite(LED, HIGH); // turn on the LED delay(500); // wait for 0.5 seconds digitalWrite(LED, LOW); // turn off the LED }
}
列表 4-1:数字输入
上传草图后,短按按钮。LED 应该保持亮起约半秒钟。
理解草图
让我们来检查项目 4 草图中的新内容——特别是#define、数字输入引脚和if语句。
使用#define创建常量
在void setup()之前,我们使用#define语句在第 1 行创建固定值:当草图被编译时,IDE 会将定义的单词的每个实例替换为它后面的数字。例如,当 IDE 在第 2 行看到LED时,它会将其替换为数字3。请注意,我们在#define值后面不使用分号。
我们基本上是使用#define语句在草图中给 LED 和按钮的数字引脚加标签。这样标注引脚编号和其他固定值(例如时间延迟)是一个好主意,因为如果该值在草图中被重复使用,你就不需要多次编辑相同的项。在这个例子中,LED在草图中使用了三次,但如果我们要更改这个值,只需在#define语句中的定义进行一次编辑。
读取数字输入引脚
为了读取按钮的状态,我们首先在void setup()中定义一个数字 I/O 引脚作为输入,使用如下代码:
pinMode(BUTTON, INPUT); // input for button
接下来,为了发现按钮是否将电压连接到数字输入(即按钮被按下),我们使用digitalRead(``pin``),其中pin是要读取的数字引脚编号。该函数返回HIGH(引脚上的电压接近 5V)或LOW(引脚上的电压接近 0V)。
使用if做决策
使用if,我们可以在我们的草图中做出决策,并告诉 Arduino 根据决策运行不同的代码。例如,在项目 4 的草图中,我们使用了列表 4-2。
// Listing 4-2
if (digitalRead(BUTTON) == HIGH)
{ digitalWrite(LED, HIGH); // turn on the LED delay(500); // wait for 0.5 seconds digitalWrite(LED, LOW); // turn off the LED
}
列表 4-2:一个简单的if-then例子
这段代码的第一行以if开始,因为它测试某个条件。如果条件为真(即电压为HIGH),则表示按钮被按下。Arduino 接着会执行花括号内的代码。
为了确定按钮是否被按下(digitalRead(BUTTON) 设置为 HIGH),我们使用了一个比较运算符,即双等号(==)。如果我们将 == 替换为 !=(不等于),那么当按钮被按下时,LED 会熄灭。试试看吧。
一旦你取得了一些成功,试着改变灯亮的时间,或者回到第三章第 38 页的项目 3,添加一个按钮控制。(不过不要拆解这个电路,我们将在下一个例子中再次使用它。)
修改你的草图:使用 if-else 进行更多决策
你可以通过使用 else 为 if 语句添加另一个动作。例如,如果我们通过添加 else 子句来重写 示例 4-1,如 示例 4-3 所示,那么当按钮被按下时,LED 将点亮,否则 它会熄灭。使用 else 会强制 Arduino 在 if 语句中的测试不为真时运行另一段代码。
// Listing 4-3
#define LED 3
#define BUTTON 7
void setup()
{ pinMode(LED, OUTPUT); // output for the LED pinMode(BUTTON, INPUT); // input for the button
}
void loop()
{ if ( digitalRead(BUTTON) == HIGH ) { digitalWrite(LED, HIGH); } **else** { digitalWrite(LED, LOW); }
}
示例 4-3:添加 else
布尔变量
有时你需要记录某物是否处于两种状态中的任意一种,例如开或关,或热或冷。布尔变量 就是计算机中的“位”,它的值只能是零(0,假)或一(1,真)。与其他变量一样,我们需要声明它才能使用:
boolean raining = true; // create the variable "raining" and first make it true
在草图中,你可以通过简单的重新赋值来改变布尔值的状态,例如这样:
raining = false;
由于布尔变量只能取真或假的值,它们非常适合用于 if 语句进行决策。真假布尔比较与比较运算符 != 和 == 配合得很好。以下是一个示例:
if ( raining == true )
{ if ( summer != true ) { // it is raining and not summer }
}
比较运算符
我们可以使用各种运算符来判断两个或多个布尔变量或其他状态。这些运算符包括 非(!)、与(&&)和 或(||)。
非运算符
非 运算符用感叹号(!)表示。这个运算符用于简化检查某事是否不为真。以下是一个示例:
if ( !raining )
{ // it is not raining (raining == false)
}
与运算符
逻辑与运算符用 && 表示。使用 与 运算符有助于减少单独的 if 测试次数。以下是一个示例:
if (( raining == true ) && ( !summer ))
{ // it is raining and not summer (raining == true and summer == false)
}
或运算符
逻辑或运算符用 || 表示。使用 或 运算符相当直观。以下是一个示例:
if (( raining == true ) || ( summer == true ))
{ // it is either raining or summer
}
进行两个或更多比较
你还可以使用相同的 if 语句进行两个或多个比较。这里有一个示例:
if ( snow == true && rain == true && !hot )
{ // it is snowing and raining and not hot
}
你还可以使用括号来设置操作的顺序。在下一个示例中,括号中的比较首先会被检查,并得到一个真假状态,然后该条件会接受 if 语句中的剩余测试:
if (( snow == true || rain == true ) && hot == false))
{
// it is either snowing or raining, and not hot
}
最后,就像之前使用 ! 运算符对值进行的示例一样,简单的真假测试可以在不需要在每次测试中写 == true 或 == false 的情况下执行。以下代码与之前的示例效果相同:
if (( snow || rain ) && !hot )
{ // it is either snowing or raining, and not hot // ( snow is true OR rain is true ) AND it is not hot
}
如你所见,使用布尔变量和比较运算符,Arduino 可以做出多种决策。随着你进行更复杂的项目,这将变得非常有用。
项目 #5:控制交通
现在让我们通过解决一个假设问题来运用我们新学到的知识。作为一个乡村县城的城镇规划师,我们面临一个单车道桥梁的问题。每周,在夜间,疲劳驾驶的司机匆忙穿越桥梁,而没有先停下来检查道路是否畅通,这时经常发生一到两起事故。我们建议安装交通信号灯,但市长希望在签署购买合同前看到信号灯的演示。我们可以租用临时信号灯,但它们非常昂贵。于是,我们决定使用 LED 和 Arduino 构建一个带有工作交通信号灯的桥梁模型。
目标
我们的目标是在单车道桥梁的每一端安装三色交通灯。该灯只允许交通一次按一个方向流动。当桥梁两端的传感器检测到红灯前等待的车辆时,交通灯会改变,并允许交通向相反方向流动。
算法
我们将使用两个按钮来模拟桥梁两端的车辆传感器。每组灯都会有红、黄、绿三种 LED。初始时,系统将允许西向的交通流向东,因此西向的交通灯设为绿灯,东向的交通灯设为红灯。
当车辆接近桥梁(通过按下按钮来模拟)且红灯亮起时,系统会将对面方向的交通信号灯从绿灯变为黄灯,再变为红灯,并等待一定时间,以允许已经在桥上的车辆完成通行。接下来,等待车辆一侧的黄灯会闪烁,作为对驾驶员的“准备好”提示,最后灯会变为绿灯。绿灯会一直保持,直到另一侧的车辆接近,此时整个过程会重复。
硬件
这是你创建此项目所需的物品:
-
两个红色 LED(LED1 和 LED2)
-
两个黄色 LED(LED3 和 LED4)
-
两个绿色 LED(LED5 和 LED6)
-
六个 560 Ω 电阻(R1 至 R6)
-
两个 10 kΩ 电阻(R7 和 R8)
-
两个 100 nF 电容(C1 和 C2)
-
两个按键(S1 和 S2)
-
一个中型面包板
-
Arduino 和 USB 数据线
-
各种连接线
原理图
由于我们只控制六个 LED 并接收来自两个按钮的输入,设计不会太复杂。图 4-26 显示了我们项目的原理图。

图 4-26:项目 5 的原理图
这个电路基本上是项目 4 中按钮和 LED 电路的一个更复杂版本,增加了电阻器、更多的 LED 和另一个按钮。
确保 LED 插入的方向正确:电阻连接到 LED 阳极,LED 阴极连接到 Arduino 的 GND 引脚,如图 4-27 所示。

图 4-27:完成的电路
草图
现在来看一下草图。你能看到它是如何与我们的算法匹配的吗?
// Project 5 - Controlling Traffic
// define the pins that the buttons and lights are connected to:1 #define westButton 3
#define eastButton 13
#define westRed 2
#define westYellow 1
#define westGreen 0
#define eastRed 12
#define eastYellow 11
#define eastGreen 10
#define yellowBlinkTime 500 // 0.5 seconds for yellow light blink 2 boolean trafficWest = true; // west = true, east = false3 int flowTime = 10000; // amount of time to let traffic flow4 int changeDelay = 2000; // amount of time between color changes
void setup()
{ // set up the digital I/O pins pinMode(westButton, INPUT); pinMode(eastButton, INPUT); pinMode(westRed, OUTPUT); pinMode(westYellow, OUTPUT); pinMode(westGreen, OUTPUT); pinMode(eastRed, OUTPUT); pinMode(eastYellow, OUTPUT); pinMode(eastGreen, OUTPUT); // set initial state for lights - west side is green first digitalWrite(westRed, LOW); digitalWrite(westYellow, LOW); digitalWrite(westGreen, HIGH); digitalWrite(eastRed, HIGH); digitalWrite(eastYellow, LOW); digitalWrite(eastGreen, LOW);
}
void loop()
{ if ( digitalRead(westButton) == HIGH ) // request west>east traffic flow { if ( trafficWest != true ) // only continue if traffic flowing in the opposite (east) direction { trafficWest = true; // change traffic flow flag to west>east delay(flowTime); // give time for traffic to flow digitalWrite(eastGreen, LOW); // change east-facing lights from green // to yellow to red digitalWrite(eastYellow, HIGH); delay(changeDelay); digitalWrite(eastYellow, LOW); digitalWrite(eastRed, HIGH); delay(changeDelay); for ( int a = 0; a < 5; a++ ) // blink yellow light { digitalWrite(westYellow, LOW); delay(yellowBlinkTime); digitalWrite(westYellow, HIGH); delay(yellowBlinkTime); } digitalWrite(westYellow, LOW); digitalWrite(westRed, LOW); // change west-facing lights from red // to green digitalWrite(westGreen, HIGH); } } if ( digitalRead(eastButton) == HIGH ) // request east>west traffic flow { if ( trafficWest == true ) // only continue if traffic flow is in the opposite (west) direction { trafficWest = false; // change traffic flow flag to east>west delay(flowTime); // give time for traffic to flow digitalWrite(westGreen, LOW); // change west-facing lights from green to yellow to red digitalWrite(westYellow, HIGH); delay(changeDelay); digitalWrite(westYellow, LOW); digitalWrite(westRed, HIGH); delay(changeDelay); for ( int a = 0 ; a < 5 ; a++ ) // blink yellow light { digitalWrite(eastYellow, LOW); delay(yellowBlinkTime); digitalWrite(eastYellow, HIGH); delay(yellowBlinkTime); } digitalWrite(eastYellow, LOW); digitalWrite(eastRed, LOW); // change east-facing lights from red // to green digitalWrite(eastGreen, HIGH); } }
}
我们的草图首先通过在第 1 行使用#define来将数字引脚号与所有使用的 LED 标签以及两个按钮的标签关联。我们有红色、黄色和绿色 LED,以及每个桥面东西两侧的一个按钮。布尔变量trafficWest在第 2 行用于跟踪交通流向——true表示西到东,false表示东到西。
整型变量flowTime在第 3 行是车辆必须通过桥梁的最短时间。当车辆停在红灯前时,系统会延长此时间,以便对面来车有时间通过桥梁。整型变量changeDelay在第 4 行表示从绿色到黄色再到红色的颜色变化之间的经过时间。
在草图进入void loop()部分之前,它已经在void setup()中设置为从西向东的交通流向。
运行草图
一旦开始运行,草图将不做任何操作,直到按下其中一个按钮。当按下东侧按钮时,代码行:
if ( trafficWest == true )
确保只有当交通流向相反方向时,交通灯才会改变。void loop()部分的其余代码是由简单的等待序列组成,然后是开启和关闭各种 LED 以模拟交通灯的工作。
模拟信号与数字信号
在这一部分,你将学习数字信号和模拟信号的区别,并学习如何使用模拟输入引脚来测量模拟信号。
到目前为止,我们的草图一直在使用数字电信号,只有两个离散的电平。具体来说,我们使用了digitalWrite(``pin``, HIGH)和digitalWrite(``pin``, LOW)来闪烁 LED,并使用digitalRead()来测量数字引脚是否有电压(HIGH)或没有(LOW)。图 4-28 是数字信号的视觉表示,信号在高低电平之间交替。

图 4-28:数字信号,顶部的水平线表示高电平,底部表示低电平
与数字信号不同,模拟信号可以在高低之间变化,且变化步数是无限的。例如,图 4-29 显示了正弦波的模拟信号。注意,随着时间的推移,电压在高低电平之间流动得非常平滑。

图 4-29:正弦波的模拟信号

图 4-30:Arduino Uno 上的模拟输入
在我们的 Arduino 中,高电平接近 5 V,低电平接近 0 V 或 GND。我们可以使用图 4-30 中显示的六个模拟输入来测量模拟信号的电压值。这些模拟输入可以安全地测量从 0(GND)到最多 5 V 的电压。
如果使用analogRead()函数,则 Arduino 将根据应用于模拟引脚的电压返回一个介于 0 和 1,023 之间的数字。例如,您可以使用analogRead()将模拟引脚 0 的值存储在整数变量a中,如下所示:
a = analogRead(0); // read analog input pin 0 (A0)
// returns 0 to 1023, which is usually 0.000 to 4.995 volts
项目#6:创建单节电池测试仪
尽管电池的流行度和使用率有所下降,但大多数家庭仍然拥有一些设备,如遥控器、时钟或儿童玩具,这些设备使用 AA、AAA、C 或 D 型电池。这些电池的电压远低于 5 V,因此我们可以使用 Arduino 测量电池的电压,从而判断电池的状态。在这个项目中,我们将创建一个电池测试仪。
目标
单节电池(如 AA 电池)通常在新电池时电压约为 1.6 V,使用和老化后电压会降低。我们将测量电压,并通过 LEDs 以视觉方式显示电池状态。我们将使用analogRead()的读取值,并将其转换为电压值。最大可读取的电压是 5 V,因此我们将 5 除以 1,024(可能的值的数量),得到 0.0048。我们将analogRead()返回的值乘以这个数字,以获取电压值。例如,如果analogRead()返回 512,则将该读取值乘以 0.0048,得到 2.4576 V。
算法
这是我们电池测试仪的算法:
-
从模拟引脚 0 读取数据。
-
将读取值乘以 0.0048 以创建电压值。
-
如果电压大于或等于 1.6 V,则短暂点亮绿色 LED。
-
如果电压大于 1.4 V 且小于 1.6 V,则短暂点亮黄色 LED。
-
如果电压小于 1.4 V,则短暂点亮红色 LED。
-
无限重复。
硬件
这是您需要的材料来创建这个项目:
-
三个 560 Ω的电阻(R1 到 R3)
-
一个绿色 LED(LED1)
-
一个黄色 LED(LED2)
-
一个红色 LED(LED3)
-
一个面包板
-
各种连接线
-
Arduino 和 USB 电缆
电路图
单节电池测试仪电路的原理图见图 4-31。左侧有两个端子,标记为+和-。将待测试的单节电池的匹配端连接到这些端子上。正极应连接到正极,负极应连接到负极。

图 4-31:项目 6 的电路图
草图
现在是草图部分。由于模拟值可能会在整数之间波动,我们将使用一种新的变量类型——float,它可以包含小数或分数值:
// Project 6 - Creating a Single-Cell Battery Tester
#define newLED 2 // green LED
#define okLED 4 // yellow LED
#define oldLED 6 // red LED
int analogValue = 0;1 float voltage = 0;
int ledDelay = 2000;
void setup()
{ pinMode(newLED, OUTPUT); pinMode(okLED, OUTPUT); pinMode(oldLED, OUTPUT);
}
void loop()
{2 analogValue = analogRead(0); 3 voltage = 0.0048*analogValue; 4 if ( voltage >= 1.6 ) { digitalWrite(newLED, HIGH); delay(ledDelay); digitalWrite(newLED, LOW); } 5 else if ( (voltage < 1.6) && (voltage) > 1.4 ) { digitalWrite(okLED, HIGH); delay(ledDelay); digitalWrite(okLED, LOW); } 6 else if ( voltage <= 1.4 ) { digitalWrite(oldLED, HIGH); delay(ledDelay); digitalWrite(oldLED, LOW); }
}
在这个草图中,Arduino 获取模拟引脚 0 在 2 处测量的值,并在 3 处将其转换为电压值。您将在下一节中了解更多关于新类型变量float的内容,下一节将讨论如何使用 Arduino 进行算术运算,并使用比较运算符来比较数字。
使用 Arduino 进行算术运算
像口袋计算器一样,Arduino 可以执行乘法、除法、加法和减法等计算。以下是一些示例:
a = 100;
b = a + 20;
c = b - 200;
d = c + 80; // d will equal 0
浮动变量
当你需要处理带小数点的数字时,可以使用 float 变量类型。float 变量可以存储的值范围在 3.4028235 × 10³⁸ 和 −3.4028235 × 10³⁸ 之间,通常精度限制在六到七位小数。你可以在计算中混合使用整数和 float 数字。例如,你可以将 float 数字 f 加到整数 a 上,并将结果存储为 float 变量 g:
int a = 100;
float f;
float g;
f = a / 3; // f = 33.333333
g = a + f; // g = 133.333333
计算的比较运算符
我们在项目 5 中使用了 == 和 != 等比较运算符与 if 语句以及数字输入信号。除了这些运算符外,我们还可以使用以下运算符来比较数字或数值变量:
-
<小于 -
>大于 -
<=小于或等于 -
>=大于或等于
我们在项目 6 的草图中,使用了这些运算符来比较第 4、5 和 6 行的数字。
使用参考电压提高模拟测量精度
如项目 6 中所示,analogRead() 函数返回一个与 0 到 5 V 电压成比例的值。上限值(5 V)是参考电压,即 Arduino 模拟输入将接受并返回最高值(1,023)的最大电压。
为了在读取更低电压时提高精度,我们可以使用较低的参考电压。例如,当参考电压为 5 V 时,analogRead() 的值范围是 0 到 1,023。然而,如果我们只需要测量最大为 2 V 的电压,那么我们可以调整 Arduino 输出,使其在 0 到 1,023 的范围内表示 2 V,从而实现更精确的测量。你可以使用外部或内部参考电压来做到这一点,具体内容将在下文中讨论。
使用外部参考电压
使用参考电压的第一种方法是通过 AREF(analog reference)引脚,如图 4-32 所示。

图 4-32:Arduino Uno 的 AREF 引脚
我们可以通过将电压连接到 AREF 引脚并将匹配的 GND 连接到 Arduino 的 GND 来引入新的参考电压。请注意,这样做可以降低参考电压,但不能提高它,因为连接到 Arduino Uno 的参考电压不得超过 5 V。设置较低参考电压的一种简单方法是使用两个电阻器构成电压分压器,如图 4-33 所示。

图 4-33:电压分压电路
根据图 4-34 中的公式,R1 和 R2 的值将决定参考电压。

图 4-34:参考电压公式
在该公式中,V[out] 是参考电压,V[in] 是输入电压—在本例中为 5 V。R1 和 R2 是电阻值,单位为欧姆。
分压的最简单方法是将V[in]一分为二,将R1和R2设为相同值——例如,每个 10 kΩ。在进行此操作时,最好使用精度最低的电阻器,例如 1% 的误差;用万用表确认它们的实际电阻值,并在计算中使用这些确认的值。此外,最好在 AREF 和 GND 之间放置一个 100 nF 的电容器,以避免 AREF 噪声并防止模拟读取不稳定。
使用外部参考电压时,请在您的草图的void setup()部分插入以下代码行:
**analogReference(EXTERNAL); // select AREF pin for reference voltage**
使用内部参考电压
Arduino Uno 还具有内部 1.1 V 参考电压。如果这满足您的需求,则无需更改硬件。只需在void setup()中添加以下代码行:
**analogReference(INTERNAL); // select internal 1.1 V reference voltage**
可变电阻器
可变电阻器,也称为电位器,通常可以调节从 0 Ω 到其额定值。其电路符号如图 4-35 所示。

图 4-35:可变电阻器(电位器)符号
可变电阻器有三个引脚连接:一个在中央引脚,另两个分别在两侧。当可变电阻器的轴旋转时,它会增加中心与一侧之间的电阻,并减少中心与另一侧之间的电阻。
可变电阻器可以是线性型或对数型。线性模型的电阻随着旋转以恒定的速度变化,而对数模型的电阻最初变化缓慢,然后迅速增加。对数电位器更常用于音频放大电路,因为它们模拟了人类的听觉反应。您通常可以通过后面的标记来识别电位器是对数型还是线性型。大多数电位器会在电阻值旁标有A或B:A表示对数型,B表示线性型。大多数 Arduino 项目使用线性可变电阻器,如图 4-36 所示。

图 4-36:典型的线性可变电阻
您还可以获得小型的可变电阻器,称为微调电位器或调节器(参见图 4-37)。由于其体积小,微调电位器非常适合用于电路调整,但它们在面包板工作中也非常有用,因为它们可以插入使用。

图 4-37:各种微调电位器
压电蜂鸣器
压电元件(简称压电),或蜂鸣器,是一种小型圆形设备,可用于产生响亮且令人烦恼的声音,适合用于报警——或用于娱乐。图 4-38 展示了一个常见的例子,TDK PS1240,旁边放着一枚美国一角硬币,以便让您了解其大小。

图 4-38:TDK PS1240 压电器
压电元件内部包含一个非常薄的板,当电流通过时,这个板会发生移动。当施加脉冲电流(例如开……关……开……关)时,板会振动并产生声波。
使用 Arduino 控制压电元件非常简单,因为它们可以像 LED 一样开关。压电元件没有极性,可以任意方向连接。
压电元件原理图
压电元件的原理图符号类似于扬声器(见图 4-39),这使得它容易识别。

图 4-39:压电元件原理图符号
项目 #7:尝试使用压电蜂鸣器
如果你手头有一个压电元件并且想尝试,首先将其连接到 Arduino 的 GND 和数字引脚 D3 到 D0(包括)之间。然后将以下演示草图上传到你的 Arduino:
// Project 7 - Trying Out a Piezo Buzzer
#define PIEZO 3 // pin 3 is capable of PWM output to drive tones
int del = 500;
void setup()
{ pinMode(PIEZO, OUTPUT);
}
void loop()
{1 analogWrite(PIEZO, 128); // 50 percent duty cycle tone to the piezo delay(del); digitalWrite(PIEZO, LOW); // turn the piezo off delay(del);
}
此草图使用数字引脚 3 上的脉宽调制(PWM)。如果你在 analogWrite() 函数中更改占空比(当前是 128,即 50% 开),你可以改变蜂鸣器的声音。
要增加蜂鸣器的音量,可以增加施加到蜂鸣器上的电压。目前电压限制为 5 V,但在 9 V 或 12 V 时蜂鸣器会更响。由于 Arduino 无法提供更高的电压,你需要使用外部电源为蜂鸣器供电,如 9 V 电池,然后使用 BC548 晶体管作为电子开关来控制蜂鸣器的电源。你可以使用与图 4-40 所示原理图相同的草图。
原理图中标记为 12 V 的部分将是高功率电源的正极,负极将连接到 Arduino 的 GND 引脚。

图 4-40:项目 7 原理图
项目 #8:创建一个快速读取温度计
温度可以通过模拟信号表示。我们可以使用模拟设备公司(Analog Devices)生产的 TMP36 电压输出温度传感器来测量温度(www.analog.com/tmp36/),该传感器如图 4-41 所示。

图 4-41:TMP36 温度传感器
请注意,TMP36 看起来就像我们在第三章继电器控制电路中使用的 BC548 晶体管。TMP36 输出与温度成比例的电压,因此你可以通过简单的转换来确定当前温度。例如,在 25 摄氏度时,输出电压为 750 mV,每增加 1 摄氏度,电压会变化 10 mV。TMP36 可以测量从 −40 到 125 摄氏度的温度。
函数analogRead()将返回一个介于 0 和 1,023 之间的值,这对应于一个介于 0 到接近 5,000 毫伏(5 V)之间的电压。如果我们将analogRead()的输出值乘以(5,000/1,024),我们就能得到传感器返回的实际电压。接下来,我们减去 500(TMP36 使用的偏移量,用于处理低于 0 度的温度),然后除以 10,最后得到摄氏度的温度。如果你使用华氏度,则将摄氏值乘以 1.8,并加上 32。
目标
在这个项目中,我们将使用 TMP36 创建一个快速读取温度的温度计。当温度低于 20 摄氏度时,蓝色 LED 灯亮起;当温度在 20 到 26 摄氏度之间时,绿色 LED 灯亮起;当温度高于 26 摄氏度时,红色 LED 灯亮起。
硬件
以下是你需要创建这个项目所需的组件:
-
三个 560 Ω电阻(R1 至 R3)
-
一个红色 LED 灯(LED1)
-
一个绿色 LED 灯(LED2)
-
一个蓝色 LED 灯(LED3)
-
一个 TMP36 温度传感器
-
一个面包板
-
各种连接线
-
Arduino 和 USB 线
原理图
电路很简单。当你看到 TMP36 的标记面时,左侧的引脚连接到 5 V 输入,中间的引脚是电压输出,右侧的引脚连接到地(GND),如图 4-42 所示。

图 4-42:项目 8 的原理图
草图
现在是草图部分:
// Project 8 - Creating a Quick-Read Thermometer
// define the pins that the LEDs are connected to:
#define HOT 6
#define NORMAL 4
#define COLD 2
float voltage = 0;
float celsius = 0;
float hotTemp = 26;
float coldTemp = 20;
float sensor = 0;
void setup()
{ pinMode(HOT, OUTPUT); pinMode(NORMAL, OUTPUT); pinMode(COLD, OUTPUT);
}
void loop(){ // read the temperature sensor and convert the result to degrees Celsius1 sensor = analogRead(0); voltage = ( sensor * 5000 ) / 1024; // convert raw sensor value to // millivolts voltage = voltage - 500; // remove voltage offset celsius = voltage / 10; // convert millivolts to Celsius // act on temperature range2 if ( celsius < coldTemp ) { digitalWrite(COLD, HIGH); delay(1000); digitalWrite(COLD, LOW); }3 else if ( celsius > coldTemp && celsius <= hotTemp ) { digitalWrite(NORMAL, HIGH); delay(1000); digitalWrite(NORMAL, LOW); } else { // celsius is > hotTemp digitalWrite(HOT, HIGH); delay(1000); digitalWrite(HOT, LOW); }
}
草图首先从 TMP36 读取电压,并将其转换为摄氏度温度,如步骤 1 所示。接着,在步骤 2 和 3 中,使用if-else语句,代码将当前温度与冷热的值进行比较,并打开相应的 LED 灯。delay(1000)语句用于防止灯光在温度迅速在两个范围之间波动时闪烁得太快。
你可以通过向传感器吹冷空气来让温度降低,或者通过用两根手指摩擦 TMP36 的表面来产生热量,进行实验。
展望未来
第四章到此结束。现在你有了更多的工具可以使用,包括数字输入和输出、新类型的变量以及各种数学函数。在下一章,你将更深入地玩转 LED,学习如何创建自己的函数,制作计算机游戏和电子骰子,等等。
第五章:使用函数
在本章中,您将
-
创建您自己的函数
-
学习使用
while和do-while进行决策 -
在您的 Arduino 和串口监视器窗口之间发送和接收数据
-
学习关于
long变量的知识
您可以通过创建自己的函数,使您的 Arduino 草图更易于阅读并简化设计。您还可以创建可在后续项目中重复使用的模块化代码。除了这些内容,本章还将介绍一种让 Arduino 做出决策并控制代码块的方法,您将了解一种名为long的整数变量类型。您还将使用自己的函数创建一种新的温度计类型。
一个函数由一组指令组成,作为一个单元打包并赋予名称,您可以在任何地方使用它。尽管许多函数已经在 Arduino 语言中可用,但有时您找不到适合您特定需求的函数——或者您可能需要反复运行程序的一部分以使函数工作,这会浪费内存。在这两种情况下,您可能会希望有一个更合适的函数来完成任务。好消息是,您可以自己创建这样的函数。
项目 #9:创建一个重复执行某个操作的函数
您可以编写简单的函数来按需重复执行操作。例如,以下函数将在 1 和 3 时打开内置 LED(开),在 2 和 4 时关闭内置 LED(关),并重复两次:
void blinkLED()
{1 digitalWrite(13, HIGH); delay(1000);2 digitalWrite(13, LOW); delay(1000);3 digitalWrite(13, HIGH); delay(1000);4 digitalWrite(13, LOW); delay(1000);
}
以下是这个函数在完整草图中的使用方式,您可以将其上传到 Arduino:
// Project 9 - Creating a Function to Repeat an Action
#define LED 13
#define del 200
void setup()
{ pinMode(LED, OUTPUT);
}
void blinkLED()
{ digitalWrite(LED, HIGH); delay(del); digitalWrite(LED, LOW); delay(del); digitalWrite(LED, HIGH); delay(del); digitalWrite(LED, LOW); delay(del);
}
void loop()
{1 blinkLED(); delay(1000);
}
当在void loop()中的 1 处调用blinkLED()函数时,Arduino 将执行void blinkLED()部分中的命令。换句话说,您已经创建了自己的函数,并在需要时使用了它。
项目 #10:创建一个设置闪烁次数的函数
我们刚刚创建的函数功能非常有限。如果我们想设置闪烁次数和延迟时间怎么办?没问题——我们可以创建一个允许我们修改这些值的函数,如下所示:
void blinkLED(int cycles, int del)
{ for ( int z = 0 ; z < cycles ; z++ ) { digitalWrite(LED, HIGH); delay(del); digitalWrite(LED, LOW); delay(del); }
}
我们新的void blinkLED()函数接受两个整数值:cycles(我们想要的 LED 闪烁次数)和del(LED 开关之间的延迟时间)。因此,如果我们想让 LED 闪烁 12 次,每次延迟 100 毫秒,我们将使用blinkLED(12, 100)。在 IDE 中输入以下代码,尝试这个函数:
// Project 10 - Creating a Function to Set the Number of Blinks
#define LED 13
void setup()
{ pinMode(LED, OUTPUT);
}
void blinkLED(int cycles, int del)
{ for ( int z = 0 ; z < cycles ; z++ ) { digitalWrite(LED, HIGH); delay(del); digitalWrite(LED, LOW); delay(del); }
}
void loop()
{1 blinkLED(12, 100); delay(1000);
}
如 1 所示,12和100这两个值——分别代表闪烁次数和延迟时间——被传递到我们自定义的blinkLED()函数中。因此,LED 将闪烁 12 次,每次闪烁之间有 100 毫秒的延迟。然后显示暂停 1,000 毫秒,即 1 秒,然后loop()函数重新开始。
创建一个返回值的函数
除了创建接受作为参数输入的值的函数(如项目 10 中的 void blinkLED()),你还可以创建返回值的函数,方法与 analogRead() 测量模拟输入时返回介于 0 到 1,023 之间的值相同,如项目 8 中演示的那样(请参见第四章第 91 页)。
到目前为止,我们看到的所有函数都以 void 开头。这告诉 Arduino 函数不返回任何值,仅仅是一个空的 void。但我们可以创建返回任何类型值的函数。例如,如果我们想要一个返回整数值的函数,我们会使用 int 来创建。如果我们希望它返回一个浮动点值,则函数将以 float 开头。让我们创建一些返回实际值的有用函数。
这是一个将摄氏度转换为华氏度的函数:
float convertTemp(float celsius)
{ float fahrenheit = 0; fahrenheit = (1.8 * celsius) + 32; return fahrenheit;
}
在第一行,我们定义了函数名(convertTemp)、返回值类型(float)以及我们可能想要传递给函数的任何变量(float celsius)。要使用这个函数,我们将其传递一个现有的值。例如,如果我们想将 40 摄氏度转换为华氏度,并将结果存储在一个名为 tempf 的 float 变量中,我们可以这样调用 convertTemp():
float tempf = convertTemp(40);
这将把 40 放入 convertTemp() 变量 celsius 并在 convertTemp() 函数中使用计算 fahrenheit() = (1.8 * celsius) + 32。结果会返回到变量 tempf,通过 convertTemp() 行 return fahrenheit;。
项目 #11:创建一个快速读取的温度计,它会根据温度闪烁
现在你已经知道如何创建自定义函数,我们将使用第四章中的 TMP36 温度传感器和 Arduino 内建的 LED 来制作一个快速读取的温度计。如果温度低于 20 摄氏度,LED 会闪烁两次然后暂停;如果温度介于 20 到 26 摄氏度之间,LED 会闪烁四次然后暂停;如果温度高于 26 摄氏度,LED 会闪烁六次。
我们将通过将草图分解为独立的、可重复使用的函数,使得草图更加模块化,同时也更容易理解。我们的温度计将执行两个主要任务:测量并分类温度,以及根据温度闪烁 LED 指定的次数。
硬件
所需的硬件很少:
-
一个 TMP36 温度传感器
-
一个面包板
-
各种连接电线
-
Arduino 和 USB 数据线
原理图
电路非常简单,如 图 5-1 所示。

图 5-1:项目 11 原理图
草图
我们需要为草图创建两个函数。第一个函数将从 TMP36 读取值,将其转换为摄氏度,然后返回 2、4 或 6,对应 LED 应该闪烁的次数。我们将从第 8 项中的草图开始,并做一些小的调整。
对于我们的第二个函数,我们将使用来自项目 9 的blinkLED()。我们的void循环将按顺序调用这些函数,然后暂停 2 秒钟再重新启动。
将此代码输入到 IDE 中:
// Project 11 - Creating a Quick-Read Thermometer That Blinks the Temperature
#define LED 13
int blinks = 0;
void setup()
{ pinMode(LED, OUTPUT);
}
int checkTemp()
{ float voltage = 0; float celsius = 0; float hotTemp = 26; float coldTemp = 20; float sensor = 0; int result; // read the temperature sensor and convert the result to degrees Celsius sensor = analogRead(0); voltage = (sensor * 5000) / 1024; // convert raw sensor value to millivolts voltage = voltage - 500; // remove voltage offset celsius = voltage / 10; // convert millivolts to Celsius // act on temperature range if (celsius < coldTemp) { result = 2; } else if (celsius >= coldTemp && celsius <= hotTemp) { result = 4; } else { result = 6; // (celsius > hotTemp) } return result;
}
void blinkLED(int cycles, int del)
{ for ( int z = 0 ; z < cycles ; z++ ) { digitalWrite(LED, HIGH); delay(del); digitalWrite(LED, LOW); delay(del); }
}1 void loop()
{ blinks = checkTemp(); blinkLED(blinks, 500); delay(2000);
}
因为我们使用了自定义函数,所以我们只需要在void_loop()中的第 1 行调用它们并设置延迟。函数checkTemp()返回一个值给整数变量blinks,然后blinkLED()将使 LED 闪烁blinks次,延迟 500 毫秒。草图然后暂停 2 秒钟再重复。
上传草图并观察 LED,看看这个温度计是如何工作的。和之前一样,看看你是否可以通过吹气或将它夹在手指之间来改变传感器的温度。一定要保持电路组装好,因为我们将在接下来的项目中使用它。
在串行监视器中显示来自 Arduino 的数据
到目前为止,我们已经将草图上传到 Arduino,并用 LED 显示输出(比如温度和交通信号)。闪烁的 LED 让我们容易从 Arduino 获得反馈,但闪烁的灯光只能告诉我们这么多信息。在本节中,你将学习如何使用 Arduino 的电缆连接和 IDE 的串行监视器窗口来显示来自 Arduino 的数据,并通过计算机键盘将数据发送到 Arduino。
串行监视器
要打开串行监视器,启动 IDE 并点击工具栏上的串行监视器图标,如图 5-2 所示。它会以一个新标签的形式出现在 IDE 中,包含输出窗口,并且应该看起来类似于图 5-3。

图 5-2:IDE 工具栏上的串行监视器图标

图 5-3:串行监视器
如图 5-3 所示,串行监视器在顶部显示一个输入框,由一行和一个发送按钮组成,下面是输出窗口,显示来自 Arduino 的数据。当勾选“自动滚动”框(时钟图标旁边的箭头按钮)时,最新的输出将显示在屏幕上,一旦屏幕满了,旧的数据会滚出屏幕,新的输出会覆盖它。如果你取消勾选“自动滚动”,可以使用垂直滚动条手动查看数据。
启动串行监视器
在我们可以使用串行监视器之前,需要通过在void setup()中添加这个函数来激活它:
Serial.begin(9600);
值9600是数据在计算机和 Arduino 之间传输的速度,也叫做波特率。这个值必须与串行监视器右下角的速度设置相匹配,如图 5-3 所示。
发送文本到串行监视器
要将文本从 Arduino 发送到串行监视器并在输出窗口中显示,你可以使用Serial.print():
Serial.print("Arduino for Everyone!");
这会将引号中的文本发送到串行监视器的输出窗口。
你还可以使用Serial.println()来显示文本,并强制任何后续的文本开始于下一行:
Serial.println("Arduino for Everyone!");
显示变量的内容
你还可以在串口监视器中显示变量的内容。例如,这将显示变量 results 的内容:
Serial.println(results);
如果变量是 float 类型,则显示默认保留两位小数。你可以通过在变量名后输入第二个参数来指定小数位数,范围从 0 到 6。例如,要将 float 类型的变量 results 显示为四位小数,你可以输入以下代码:
Serial.print(results,4);
项目 #12:在串口监视器中显示温度
使用项目 8 中的硬件,我们将在串口监视器窗口中显示摄氏度和华氏度的温度数据。为此,我们将创建一个函数来确定温度值,另一个函数来在串口监视器中显示这些值。
将此代码输入到 IDE 中:
// Project 12 - Displaying the Temperature in the Serial Monitor
float celsius = 0;
float fahrenheit = 0;
void setup()
{ Serial.begin(9600);
}1 void findTemps()
{ float voltage = 0; float sensor = 0; // read the temperature sensor and convert the result to degrees C and F sensor = analogRead(0); voltage = (sensor * 5000) / 1024; // convert the raw sensor value to // millivolts voltage = voltage - 500; // remove the voltage offset celsius = voltage / 10; // convert millivolts to Celsius fahrenheit = (1.8 * celsius) + 32; // convert Celsius to Fahrenheit
}2 void displayTemps() { Serial.print("Temperature is "); Serial.print(celsius, 2); Serial.print(" deg. C / "); Serial.print(fahrenheit, 2); Serial.println(" deg. F"); // use .println here so the next reading starts on a new line
}
void loop()
{ findTemps(); displayTemps(); delay(1000);
}
这个示例中发生了很多事情,但我们创建了两个函数,findTemps() 在 1 处和 displayTemps() 在 2 处,以简化问题。这些函数在 void loop() 中被调用,void loop() 本身很简单。因此,你可以看到创建自定义函数的一个原因:使得你的草图更容易理解,代码也更模块化,甚至可能可重用。
上传草图后,等待几秒钟,然后打开串口监视器。你所在区域的温度应该以类似 图 5-4 所示的方式显示。

图 5-4:项目 12 的结果
使用串口监视器进行调试
你可以使用串口监视器帮助调试(定位和修复草图中的错误)。例如,如果你在草图中插入 Serial.println(); 语句,并包含有关其位置的简短注释,那么你可以看到 Arduino 是否经过了每个语句的位置。例如,你可能会使用以下代码:
Serial.println("now in findTemps()");
在 findTemps() 函数内插入语句,让你知道 Arduino 正在运行该函数。
使用 while 语句进行决策
你可以在草图中使用 while 语句来重复执行指令,只要给定的条件(while)为真。
while
条件总是在执行 while 语句中的代码之前进行测试。例如,while ( temperature > 30 ) 将测试 temperature 的值是否大于 30。你可以在括号内使用任何比较运算符或布尔变量来创建条件。
在以下草图中,Arduino 会计数到 10 秒,然后继续其程序:
int a = 0; // an integer
while ( a < 10 )
{ a = a + 1; delay(1000);
}
该草图首先将变量 a 设置为 0。然后它检查 a 的值是否小于 10(while ( a < 10 )),如果是,它会将该值加 1,等待 1 秒钟(delay(1000)),然后再次检查该值。它会重复这个过程,直到 a 的值为 10。一旦 a 等于 10,while 语句中的条件为假;因此,Arduino 会继续执行 while 循环后的代码。
do-while
与while不同,do-while结构将测试放在do-while语句内部代码执行之后。以下是一个示例:
int a = 0; // an integer
do
{ delay(1000); a = a + 1;
} while ( a < 100 );
在这种情况下,大括号之间的代码会在测试条件(while ( a < 100 ))检查之前执行。结果,即使条件未满足,循环也会运行一次。你将在设计具体项目时决定是使用while语句还是do-while语句。
从串口监视器向 Arduino 发送数据
要从串口监视器向 Arduino 发送数据,我们需要让 Arduino 监听串口缓冲区——这是 Arduino 用来通过串口引脚(数字引脚 0 和 1)接收外部数据的部分,这些串口引脚也与计算机的 USB 接口相连接。串口缓冲区存储来自串口监视器输入窗口的传入数据。
项目 #13:将数字乘以二
为了演示通过串口监视器发送和接收数据的过程,让我们分析以下草图。该草图接受用户输入的单个数字,将其乘以 2,然后将结果显示在串口监视器的输出窗口中。在上传草图后,当你打开串口监视器窗口时,选择窗口下拉菜单中的无行结束选项。在串口监视器中输入数据时,你需要按 CTRL-ENTER 来将数据发送到 Arduino(而不仅仅是按 ENTER)。
// Project 13 - Multiplying a Number by Two
int number;
void setup()
{ Serial.begin(9600);
}
void loop()
{ number = 0; // set the variable to zero, ready for a new read Serial.flush(); // clear any "junk" out of the serial buffer before waiting1 while (Serial.available() == 0) { // do nothing until something enters the serial buffer } 2 while (Serial.available() > 0) { number = Serial.read() - '0';
// read the number in the serial buffer and
// remove the ASCII text offset for zero: '0' } // Show me the number! Serial.print("You entered: "); Serial.println(number); Serial.print(number); Serial.print(" multiplied by two is "); number = number * 2; Serial.println(number);
}
第一个while语句中的Serial.available()测试在 1 处返回 0,表示用户尚未向串口监视器输入任何内容。换句话说,它告诉 Arduino:“在用户输入数据之前,不做任何操作。”接下来的while语句在 2 处检测串口缓冲区中的数字,并将文本代码转换为整数。之后,Arduino 会显示来自串口缓冲区的数字和乘法结果。
Serial.flush()函数在草图开始时清空串口缓冲区,以防其中有任何意外数据,从而准备好接收下一个可用数据。图 5-5 展示了草图运行后串口监视器窗口的样子。

图 5-5:项目 13 的输入输出示例
尽管现在可以将数值数据输入到串口监视器中让 Arduino 进行处理,但它目前仅接受一位数字的输入。即使没有这个限制,使用整数变量也会限制可用数字的范围。我们可以使用long类型变量来增加这个范围,下面将讨论这一点。
long 类型变量
为了让串口监视器接收多位数字,我们需要在程序中添加一些新的代码,稍后你会看到。然而,在处理更大的数字时,int变量类型可能会有所限制,因为它的最大值为 32,767。幸运的是,我们可以通过使用long变量类型来扩展这一限制。long变量是一个整数,范围在−2,147,483,648 到 2,147,483,647 之间,比int变量的范围(−32,768 到 32,767)要大得多。
项目 #14:使用 long 变量
我们将使用串口监视器来接受long变量和大于一位的数字。这个程序接受一个多位数,将其乘以 2,然后将结果返回到串口监视器:
// Project 14 - Using long Variables
long number = 0;
long a = 0;void setup()
{ Serial.begin(9600);
}
void loop()
{ number = 0; // zero the incoming number ready for a new read Serial.flush(); // clear any "junk" out of the serial buffer before waiting while (Serial.available() == 0) { // do nothing until something comes into the serial buffer- // when something does come in, Serial.available will return how many // characters are waiting in the buffer to process } // one character of serial data is available, begin calculating while (Serial.available() > 0) { // move any previous digit to the next column on the left; // in other words, 1 becomes 10 while there is data in the buffer number = number * 10; // read the next number in the buffer and subtract the character 0 // from it to convert it to the actual integer number a = Serial.read() - '0'; // add this value a into the accumulating number number = number + a; // allow a short delay for more serial data to come into Serial.available delay(5); } Serial.print("You entered: "); Serial.println(number); Serial.print(number); Serial.print(" multiplied by two is "); number = number * 2; Serial.println(number);
}
在这个例子中,两个while循环允许 Arduino 从串口监视器接收多个数字。当输入第一个数字时(输入数字的最左边一位),它会被转换成数字并加到总变量number中。如果这是唯一的数字,程序就继续执行。如果输入了另一个数字(例如,42 中的 2),总数会乘以 10,移动第一个数字到左边,然后将新的数字加到总数中。这个循环会一直重复,直到最右边的数字被加到总数中。不要忘记在串口监视器窗口选择无行结束符。
图 5-6 显示了这个程序的输入和输出。

图 5-6:项目 14 的输入和输出示例
展望未来
创建自定义函数的能力是一项重要的技能,它将简化你的程序并节省时间和精力。你将在下一章中很好地运用这项知识,学习如何在 Arduino 上做更多的数学运算,包括制作一个游戏。
第六章:数字、变量与算术
本章你将学习:
-
生成随机数
-
创建电子骰子
-
学习二进制数字
-
使用移位寄存器集成电路(IC)获取更多的数字输出引脚
-
通过测验测试你对二进制数字的了解
-
学习变量数组
-
在七段 LED 模块上显示数字
-
学习如何使用取模数学函数
-
创建一个数字温度计
你将学习到许多有用的新函数,这些函数将创造更多项目选项,包括随机数生成、新类型的数学函数和变量存储在称为 数组 的有序列表中。此外,你还将学习如何使用 LED 显示模块以数字形式显示数据和简单的图像。最后,我们将结合这些工具来创建一个游戏,一个数字温度计等等。
生成随机数
程序生成随机数的能力在游戏和效果中非常有用。例如,你可以使用随机数来玩骰子或彩票游戏,创建 LED 灯光效果,或为 Arduino 的测验游戏创建视觉或听觉效果。不幸的是,Arduino 本身不能选择纯随机的数字。你需要通过提供一个 种子 来帮助它,这个种子是用于计算生成随机数的任意起始数字。
使用环境电流生成随机数
使用 Arduino 生成随机数的最简单方法是编写一个程序,读取来自空闲(断开连接的)模拟引脚的电压(例如,模拟引脚 0),在 void setup() 中加入以下这一行:
randomSeed(analogRead(0));
即使在 Arduino 的模拟输入上没有任何接线,环境中的静电也会产生微小的、可测量的电压。这个电压的量是相当随机的。我们可以利用这种环境电压作为随机数生成的种子,然后通过 random(``lower, upper``) 函数将其分配给一个整数变量。此外,我们可以使用参数 lower 和 upper 来设置随机数范围的下限和上限。例如,要生成一个介于 100 和 1,000 之间的随机数,你可以使用以下代码:
int a = 0;
a = random(100, 1001);
我们使用数字 1,001 而不是 1,000,因为上限是 不包括 在内的,即它不在范围内。
要生成一个介于 0 和某个数字之间的随机数,你只需要输入上限。以下是如何生成一个介于 0 和 6 之间的随机数:
a = random(7);
列表 6-1 中的示例草图将生成一个介于 0 和 1,000 之间的随机数,和另一个介于 10 和 50 之间的随机数。
// Listing 6-1
int r = 0;
void setup()
{ randomSeed(analogRead(0)); Serial.begin(9600);
}
void loop(){ Serial.print("Random number between zero and 1000 is: "); r = random(0, 1001); Serial.println(r); Serial.print("Random number between ten and fifty is: "); r = random(10, 51); Serial.println(r); delay(1000);
}
列表 6-1:一个随机数生成器
图 6-1 显示了 列表 6-1 在串行监视器中的结果。

图 6-1:来自 列表 6-1 的输出
现在你已经知道如何生成随机数,让我们通过创建电子骰子来好好利用这项知识。
项目 #15:创建一个电子骰子
我们的目标是随机点亮六个 LED 中的一个,模拟投掷骰子的过程。我们将选择一个介于 1 和 6 之间的随机数字,然后点亮相应的 LED 来表示结果。我们将创建一个函数,随机选择 Arduino 上的六个 LED 中的一个,并让该 LED 保持亮起一段时间。当运行草图的 Arduino 打开或重置时,它应该快速显示随机的 LED 一段指定的时间,然后逐渐减慢闪烁,直到最终 LED 被点亮。与结果匹配的随机选择的 LED 将保持亮起,直到 Arduino 被重置或关闭。
硬件
要构建这个骰子,我们需要以下硬件:
-
六个任意颜色的 LED(LED1 到 LED6)
-
一个 560 Ω 电阻(R1)
-
各种连接线
-
一个中型面包板
-
Arduino 和 USB 电缆
原理图
由于一次只有一个 LED 会亮起,所以可以在 LED 的阴极和 GND 之间放置一个限流电阻。图 6-2 显示了我们的骰子原理图。

图 6-2:项目 15 的原理图
草图
这是我们骰子的草图:
// Project 15 - Creating an Electronic Die
void setup()
{ randomSeed(analogRead(0)); // seed the random number generator for ( int z = 1 ; z < 7 ; z++ ) // LEDs on pins 1-6 are output { pinMode(z, OUTPUT); }
}void randomLED(int del)
{ int r; r = random(1, 7); // get a random number from 1 to 6 digitalWrite(r, HIGH); // output to the matching LED on digital pin 1-6 if (del > 0) {1 delay(del); // hold the LED on for the delay received }2 else if (del == 0) { do // delay entered was zero, hold the LED on forever {}3 while (1); } digitalWrite(r, LOW); // turn off the LED
}
void loop()
{ int a; // cycle the LEDs around for effect for ( a = 0 ; a < 100 ; a++ ) { randomLED(50); } // slow down4 for ( a = 1 ; a <= 10 ; a++ ) { randomLED(a * 100); } // and stop at the final random number and LED randomLED(0);
}
在这里,我们在 void setup() 中使用循环来激活数字输出引脚。randomLED() 函数接收一个整数,该整数用于 delay() 函数中的延迟 1,确保 LED 在选定的时间内保持亮起。如果在延迟 2 中接收到的值是 0,则该函数将使 LED 永远保持亮起,因为我们使用的是
do {} while (1);
在第 3 行,它会无限循环,因为 1 始终为 1。
要“投掷骰子”,我们重置 Arduino 来重新启动草图。为了在最终值显示之前逐渐减慢 LED 的变化,我们首先以每次 50 毫秒的间隔随机显示一个 LED 100 次。然后,在第 4 行,我们通过将 LED 闪烁之间的延迟从 100 毫秒增加到 1,000 毫秒来减慢闪烁,每次闪烁持续 100 毫秒。这样做的目的是模拟骰子“减速”的过程,直到它最终停留在某个值上。最后一行,Arduino 通过保持一个 LED 点亮来显示投掷结果:
randomLED(0);
修改草图
我们可以以多种方式修改这个项目。例如,我们可以再增加六个 LED 来同时投掷两个骰子,或者仅用内置 LED 显示结果,通过闪烁若干次来表示投掷的结果。或者使用按钮重新投掷骰子。发挥你的想象力和新技能,来玩得开心吧!
二进制快速入门
大多数孩子使用十进制系统来学习计数,但计算机(包括 Arduino)使用二进制数字系统来计数。
二进制数字
二进制数字 仅由 1 和 0 组成——例如,10101010。在二进制中,从右到左的每个数字代表该列数字的 2 的幂(从右到左依次增加)。每一列的积相加后,得到数字的值。
例如,考虑二进制数 10101010,如 表 6-1 所示。要将二进制数 10101010 转换为十进制,我们将表格底行中列出的每一列的总和相加:
128 + 0 + 32 + 0 + 8 + 0 + 2 + 0
总和为 170,因此二进制数 10101010 在十进制中等于 170。一个有八列(或 位)的二进制数包含 1 个 字节 的数据;1 字节的数据可以有从 0 到 255 的数值。最左边的位被称为 最重要位(MSB),最右边的是 最不重要位(LSB)。
表 6-1:二进制到十进制数字转换示例
| 2⁷ | 2⁶ | 2⁵ | 2⁴ | 2³ | 2² | 2¹ | 2⁰ | |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 二进制 |
| 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | 十进制 |
二进制数非常适合存储某些类型的数据,例如 LED 的开/关模式、真/假设置以及数字输出的状态。二进制数是计算机中所有类型数据的构建块。
字节变量
存储二进制数的一种方法是使用 字节变量。例如,我们可以使用以下代码创建字节变量 outputs:
byte outputs = B11111111;
B 前缀告诉 Arduino 将数字作为二进制数读取(在此情况下为 11111111),而不是其十进制等价物 255。 列表 6-2 进一步演示了这一点。
// Listing 6-2
byte a;
void setup()
{ Serial.begin(9600);
}
void loop()
{ for ( int count = 0 ; count < 256 ; count++ ) { a = count; Serial.print("Base-10 = ");1 Serial.print(a, DEC); Serial.print(" Binary = ");2 Serial.println(a, BIN); delay(1000); }
}
列表 6-2:二进制数演示
我们可以通过 DEC 1 以十进制数显示字节变量,或者通过 BIN 2 以二进制数显示字节变量,作为 Serial.print() 函数的一部分。上传代码后,您应该能在串口监视器中看到类似于 图 6-3 所示的输出。

图 6-3:列表 6-2 输出
使用移位寄存器增加数字输出
Arduino 板有 13 个数字引脚可以用作输出——但有时候 13 个不够用。为了增加输出,我们可以使用 移位寄存器,并且仍然可以在 Arduino 上留下大量空余空间用于输出。移位寄存器是一个集成电路(IC),它具有八个数字输出引脚,可以通过向 IC 发送一字节的数据来控制它。在我们的项目中,我们将使用 图 6-4 中显示的 74HC595 移位寄存器。

图 6-4:74HC595 移位寄存器 IC
74HC595 移位寄存器具有八个数字输出,可以像 Arduino 数字输出引脚一样操作。移位寄存器本身占用了三个 Arduino 数字输出引脚,因此净增的是五个输出引脚。
移位寄存器的原理很简单:我们向移位寄存器发送 1 字节数据(8 位),它根据该 1 字节数据的值打开或关闭相应的八个输出。数据的位与输出引脚按从高到低的顺序匹配,因此数据的最左边位表示移位寄存器的输出引脚 7,最右边的位表示输出引脚 0。例如,如果我们将B10000110发送到移位寄存器,它将打开输出 1、2 和 7,并关闭输出 0 和 3 到 6,直到接收到下一个字节数据或电源被关闭。
可以将多个移位寄存器连接在一起,为每个连接到相同三个 Arduino 引脚的移位寄存器提供额外的八个数字输出引脚;当你需要控制大量 LED 时,移位寄存器非常方便。现在让我们通过创建一个二进制数字显示来实现这一点。
项目#16:创建一个 LED 二进制数字显示器
在这个项目中,我们将使用八个 LED 来显示从 0 到 255 的二进制数字。我们的草图将使用一个for循环从 0 到 255 计数,并将每个值发送到移位寄存器,移位寄存器将使用 LED 显示每个数字的二进制等效值。
硬件
需要以下硬件:
-
一个 74HC595 移位寄存器 IC
-
八个 LED(LED1 至 LED8)
-
八个 560Ω电阻(R1 至 R8)
-
一个面包板
-
各种连接线
-
Arduino 和 USB 连接线
电路图
图 6-5 展示了 74HC595 的电路符号。

图 6-5:74HC595 电路符号
我们的移位寄存器有 16 个引脚:
-
引脚 15 和 1 到 7 是我们控制的八个输出引脚(分别标记为Q0到Q7)。
-
Q7 输出发送到移位寄存器的第一个位,Q0 输出最后一个位。
-
引脚 8 连接到 GND。
-
引脚 9 称为数据输出,用于将数据发送到另一个移位寄存器(如果存在的话)。
-
引脚 10 始终连接到 5V(例如,Arduino 上的 5V 连接器)。
-
引脚 11 和 12 分别称为时钟和锁存。
-
引脚 13 称为输出使能,通常连接到 GND。
-
引脚 14 用于接收来自 Arduino 的输入位数据。
-
引脚 16 用于电源:来自 Arduino 的 5V 电压。
为了让你了解引脚的布局,移位寄存器 IC 左端的半圆形凹槽位于引脚 1 和 16 之间,正如图 6-4 所示。
引脚按顺时针方向依次编号,正如在图 6-6 所示,这张图展示了我们的 LED 二进制数字显示电路图。

图 6-6:项目 16 的电路图
程序草图
接下来是草图:
// Project 16 – Creating an LED Binary Number Display
#define DATA 6 // digital 6 to pin 14 on the 74HC595
#define LATCH 8 // digital 8 to pin 12 on the 74HC595
#define CLOCK 10 // digital 10 to pin 11 on the 74HC595
void setup()
{ pinMode(LATCH, OUTPUT); pinMode(CLOCK, OUTPUT); pinMode(DATA, OUTPUT);
}
void loop()
{ int i; for ( i = 0; i < 256; i++ ) { digitalWrite(LATCH, LOW); shiftOut(DATA, CLOCK, MSBFIRST, i); digitalWrite(LATCH, HIGH); delay(200); }
}
在这个草图中,我们在void setup()中将连接到移位寄存器的三个引脚设置为输出,然后在void loop()中添加一个从 0 到 255 的循环并重复。当我们在for循环中将一个数据字节(例如,240,或B11110000)发送到移位寄存器时,发生了三件事:
-
锁存引脚 12 设置为
LOW(即,从 Arduino 数字输出引脚 8 提供一个低电平信号)。这是为将输出引脚 12 设置为HIGH做准备,当shiftOut()完成其任务后,数据将被锁存到输出引脚。 -
我们将数据字节(例如,
B11110000)从 Arduino 数字引脚 6 发送到移位寄存器,并告诉shiftOut()函数从哪个方向解释该数据字节。例如,如果我们选择了LSBFIRST,那么 LED 1 到 4 会亮起,其它的会熄灭。如果我们使用MSBFIRST,那么 LED 5 到 8 会亮起,其它的会熄灭。 -
最后,锁存引脚 12 设置为
HIGH(为其提供 5 V 电压)。这告诉移位寄存器所有的位已经移入并且准备好了。在此时,移位寄存器会调整其输出,以匹配接收到的数据。
项目 #17:制作二进制问答游戏
在这个项目中,我们将使用随机数、串口监视器以及项目 16 中创建的电路来制作一个二进制问答游戏。Arduino 将通过 LED 显示一个随机的二进制数,然后你将使用串口监视器输入该二进制数的十进制版本。串口监视器将告诉你答案是否正确,游戏将继续进行并显示一个新的数字。
算法
该算法可以分为三个函数。displayNumber()函数将使用 LED 显示一个二进制数。getAnswer()函数将从串口监视器接收一个数字并显示给用户。最后,checkAnswer()函数将用户输入的数字与生成的随机数进行比较,并显示正确/错误状态,如果猜测错误,还会显示正确答案。
草图
这个草图会生成一个介于 0 和 255 之间的随机数,使用 LED 以二进制形式显示它,要求用户给出答案,然后在串口监视器中显示结果。你已经见过草图中使用的所有函数,虽然这里有很多代码,但它应该看起来很熟悉。我们将在草图中通过注释和以下的解释来分析它:
// Project 17 - Making a Binary Quiz Game
#define DATA 6 // connect to pin 14 on the 74HC595
#define LATCH 8 // connect to pin 12 on the 74HC595
#define CLOCK 10 // connect to pin 11 on the 74HC595
int number = 0;
int answer = 0;1 void setup()
{ pinMode(LATCH, OUTPUT); // set up the 74HC595 pins pinMode(CLOCK, OUTPUT); pinMode(DATA, OUTPUT); Serial.begin(9600); randomSeed(analogRead(0)); // initialize the random number generator displayNumber(0); // clear the LEDs
}2 void displayNumber(byte a)
{ // send byte to be displayed on the LEDs digitalWrite(LATCH, LOW); shiftOut(DATA, CLOCK, MSBFIRST, a); digitalWrite(LATCH, HIGH);
}3 void getAnswer()
{ // receive the answer from the player int z = 0; Serial.flush(); while (Serial.available() == 0) { // do nothing until something comes into the serial buffer } // one character of serial data is available, begin calculating while (Serial.available() > 0) { // move any previous digit to the next column on the left; in // other words, 1 becomes 10 while there is data in the buffer answer = answer * 10; // read the next number in the buffer and subtract the character '0' // from it to convert it to the actual integer number z = Serial.read() - '0'; // add this digit into the accumulating value answer = answer + z; // allow a short delay for any more numbers to come into Serial.available delay(5); } Serial.print("You entered: "); Serial.println(answer);
}4 void checkAnswer()
{ // check the answer from the player and show the results if (answer == number) // Correct! { Serial.print("Correct! "); Serial.print(answer, BIN); Serial.print(" equals "); Serial.println(number); Serial.println(); } else // Incorrect { Serial.print("Incorrect, "); Serial.print(number, BIN); Serial.print(" equals "); Serial.println(number); Serial.println(); } answer = 0; delay(10000); // give the player time to review their answer
}5 void loop(){ number = random(256); displayNumber(number); Serial.println("What is the binary number in base 10? "); getAnswer(); checkAnswer();
}
让我们回顾一下这个草图是如何工作的。在 1,void setup()配置了数字输出引脚以使用移位寄存器,启动了串行监视器,并为随机数生成器提供了种子。在 2,定制函数displayNumber()接受一个字节的数据并将其发送到移位寄存器,后者通过连接的 LED 以二进制形式显示该字节(如在项目 16 中)。在 3,定制函数getAnswer()通过串行监视器接受用户输入的数字(如第五章项目 14 中所示),并显示出来,如图 6-7 所示。
在 4,函数checkAnswer()将玩家在getAnswer()中输入的数字与草图在void loop()中生成的随机数字进行比较。然后,程序会根据二进制和十进制值提示玩家是否答对。最后,在程序运行的主void loop()中,Arduino 生成用于测验的随机二进制数,调用匹配的函数通过硬件显示出来,然后接收并检查玩家的答案。
图 6-7 展示了串行监视器中的游戏画面。

图 6-7:项目 17 进行中
数组
数组是一组变量或值,它们被分组在一起,以便可以作为一个整体来引用。当处理大量相关数据时,使用数组来保持数据的组织是一个不错的主意。
定义一个数组
数组中的每一项称为元素。例如,假设有六个float变量,存储过去六小时内的温度数据;与其为每个变量命名,我们可以定义一个名为temperatures的数组,包含六个元素,代码如下:
float temperatures[6];
我们也可以在定义数组时插入值。这样做时,我们不需要定义数组的大小。以下是一个例子:
float temperatures[]={11.1, 12.2, 13.3, 14.4, 15.5, 16.6};
请注意,这次我们没有在方括号([])内显式定义数组的大小;相反,它的大小是根据花括号({})内设置的元素数量来推断的。请注意,任何大小的数组只能包含一种类型的变量。
引用数组中的值
我们从左开始,以 0 为起点来计数数组中的元素;temperatures[]数组的元素编号从 0 到 5。我们可以通过在方括号中插入元素的编号来引用数组中的单个值。例如,要将temperatures[]中的第一个元素(当前值为11.1)更改为12.34,我们可以使用以下代码:
temperatures[0] = 12.34;
向数组写入和从数组读取
在列表 6-3 中,我们演示了向一个包含五个元素的数组写入和读取值。草图中的第一个for循环将一个随机数写入数组的每个元素,第二个for循环检索这些元素并在串行监视器中显示它们。
// Listing 6-3
void setup()
{ Serial.begin(9600); randomSeed(analogRead(0));
}
int array[5]; // define our array of five integer elements
void loop()
{ int i; Serial.println(); for ( i = 0 ; i < 5 ; i++ ) // write to the array { array[i] = random(10); // random numbers from 0 to 9 } for ( i = 0 ; i < 5 ; i++ ) // display the contents of the array { Serial.print("array["); Serial.print(i); Serial.print("] contains "); Serial.println(array[i]); } delay(5000);
}
列表 6-3:数组读写演示
图 6-8 展示了该草图在串行监视器中的输出。

图 6-8:清单 6-3 运行示例
现在你已经了解了如何使用二进制数、移位寄存器和数组,是时候将这些知识付诸实践了。在我们的下一个项目中,我们将连接一些数字显示器。
七段 LED 显示器
LED 很有趣,但显示单一灯光的数据种类是有限的。在这一部分中,我们将开始使用七段 LED 显示器显示数字,如图 6-9 所示。

图 6-9:七段显示模块
这些显示器非常适合显示数字,这就是为什么你会在数字闹钟、速度计等设备中看到它们的原因。每个七段 LED 显示模块由八个 LED 组成,模块也有不同的颜色。为了减少显示器使用的引脚数量,所有 LED 的正极或负极都连接在一起——这些模块分别称为共阳极或共阴极模块。我们的项目将使用共阴极模块。
显示器的 LED 被标记为A 到 G 和 DP(表示小数点)。每个 LED 段都有一个正极引脚,而负极连接到一个公共负极引脚。七段 LED 显示的布局始终按照图 6-10 中所示的方式描述,LED 段 A 在顶部,B 在其右侧,以此类推。因此,例如,如果你想显示数字 7,则需要给 A、B 和 C 段供电。
每个 LED 显示模块的引脚可能会有所不同,具体取决于制造商,但它们总是遵循图 6-10 中显示的基本模式。当你使用这些模块时,务必从零售商那里获取模块的数据手册,以帮助你节省确定哪些引脚是哪个的时间。
我们将使用图 6-11 中显示的原理图符号来表示我们的七段 LED 显示模块。

图 6-10:典型七段显示模块的 LED 图

图 6-11:七段显示模块的原理图符号
控制 LED
我们将使用在项目 17 中讨论的方法,通过将引脚 A 到 DP 连接到移位寄存器输出 Q0 到 Q7 来控制 LED 显示。使用表 6-2 中显示的矩阵作为指南,帮助确定哪些段需要打开或关闭,以显示特定的数字或字母。
矩阵的第一行是控制第二行段的移位寄存器输出引脚。下面的每一行显示的是可以显示的数字,并包含发送到移位寄存器的相应二进制和十进制值。
表 6-2:显示段矩阵
| SR | Q0 | Q1 | Q2 | Q3 | Q4 | Q5 | Q6 | Q7 | |
|---|---|---|---|---|---|---|---|---|---|
| 段 | A | B | C | D | E | F | G | DP | 小数点 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 252 |
| 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 96 |
| 2 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 218 |
| 3 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0 | 242 |
| 4 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 102 |
| 5 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 182 |
| 6 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 190 |
| 7 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 224 |
| 8 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0 | 254 |
| 9 | 1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 246 |
| A | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 0 | 238 |
| B | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 62 |
| C | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 156 |
| D | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 122 |
| E | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0 | 158 |
| F | 1 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 142 |
例如,要显示数字 7,如 图 6-12 所示,我们需要点亮 LED 段 A、B 和 C,它们分别对应移位寄存器的输出 Q0、Q1 和 Q2。因此,我们将字节 B1110000 发送到移位寄存器(shiftOut() 设置为 LSBFIRST),以点亮与所需的 LED 相匹配的前 3 个输出。

图 6-12:显示数字 7
在我们的下一个项目中,我们将创建一个电路,依次显示数字 0 到 9,然后显示字母 A 到 F。循环会重复,并且小数点 LED 会亮起。
项目 #18:创建一个单数字显示器
在这个项目中,我们将组装一个电路来使用单数字显示器。
硬件
以下硬件是必需的:
-
一个 74HC595 移位寄存器 IC
-
一个共阴极七段 LED 显示器
-
一个 560 Ω 电阻(R1)
-
一个大号面包板
-
各种连接电线
-
Arduino 和 USB 电缆
原理图
原理图如 图 6-13 所示。

图 6-13:项目 18 的原理图
当你将 LED 模块接线到移位寄存器时,LED 引脚 A 到 G 分别连接到 Q0 到 Q6 引脚,而 DP 连接到 Q7。
草图
在项目 18 的草图中,我们将十进制值(见 表 6-2)存储在 int digits[] 数组中。在 void loop() 中,我们按顺序将这些值发送到移位寄存器,首先是 1,然后通过在发送到移位寄存器的值上加 1 来重复该过程并开启小数点:
// Project 18 - Creating a Single-Digit Display
#define DATA 6 // connect to pin 14 on the 74HC595
#define LATCH 8 // connect to pin 12 on the 74HC595
#define CLOCK 10 // connect to pin 11 on the 74HC595
// set up the array with the segments for 0 to 9, A to F (from Table 6-2)
int digits[] = {252, 96, 218, 242, 102, 182, 190, 224, 254, 246, 238, 62, 156, 122, 158, 142};
void setup()
{ pinMode(LATCH, OUTPUT); pinMode(CLOCK, OUTPUT); pinMode(DATA, OUTPUT);
}
void loop()
{ int i; for ( i = 0 ; i < 16 ; i++ ) // display digits 0-9, A-F { digitalWrite(LATCH, LOW);1 shiftOut(DATA, CLOCK, LSBFIRST, digits[i]); digitalWrite(LATCH, HIGH); delay(250); } for ( i = 0 ; i < 16 ; i++ ) // display digits 0-9, A-F with DP { digitalWrite(LATCH, LOW);2 shiftOut(DATA, CLOCK, LSBFIRST, digits[i]+1); // +1 is to turn on the DP bit digitalWrite(LATCH, HIGH); delay(250); }
}
七段 LED 显示器亮度高,易于阅读。例如,图 6-14 显示了此草图在请求显示数字 9 时的结果,带有小数点。

图 6-14:项目 18 显示的数字
修改草图:显示双位数字
要使用多个移位寄存器来控制额外的数字输出,请将 74HC595 的 9 号引脚(接收来自 Arduino 的数据)连接到第二个移位寄存器的 14 号引脚。一旦完成此连接,两个字节的数据将被发送:第一个字节控制第二个移位寄存器,第二个字节控制第一个移位寄存器。以下是一个示例:
digitalWrite(LATCH, LOW); shiftOut(DATA, CLOCK, MSBFIRST, 254); // data for second 74HC595 shiftOut(DATA, CLOCK, MSBFIRST, 254); // data for first 74HC595 digitalWrite(LATCH, HIGH);
项目#19:控制两个七段 LED 显示模块
该项目将向您展示如何控制两个七段 LED 显示模块,从而能够显示两位数字。
硬件
以下硬件是必需的:
-
两个 74HC595 移位寄存器 IC
-
两个共阴极七段 LED 显示器
-
两个 560Ω电阻(R1 到 R2)
-
一个大型面包板或两个较小的单元
-
各种连接线
-
Arduino 和 USB 电缆
原理图
图 6-15 显示了两个显示模块的原理图。
请注意,移位寄存器的数据和时钟引脚相互连接,然后连接到 Arduino。来自 Arduino 数字引脚 6 的数据线连接到移位寄存器 1,然后从移位寄存器 1 的 9 号引脚连接到移位寄存器 2 的 14 号引脚。
为了显示 0 到 99 之间的数字,我们需要一个更复杂的草图。如果数字小于 10,我们只需发送该数字后跟一个 0,因为右侧的数字将显示该数字,而左侧的数字将显示 0。然而,如果数字大于 10,那么我们需要确定该数字的两位数字,并将它们分别发送到移位寄存器。为了简化这一过程,我们将使用数学函数模运算。

图 6-15:项目 19 的原理图
模运算
模运算是一个返回除法操作余数的函数。例如,10 模(或mod)7 等于 3——换句话说,10 除以 7 的余数是 3。我们使用百分号(%)表示模运算。以下示例在草图中使用了模运算:
int a = 8;
int b = 3;
int c = a % b;
在这个示例中,c的值将是2。因此,要确定两位数字的右侧数字,我们使用模运算函数,它返回除以两个数字后的余数。
为了自动化显示单个或双位数字,我们将为我们的草图创建函数displayNumber()。我们在该函数中使用模运算来分离两位数字的每一位。例如,若要显示数字 23,我们首先通过将 23 除以 10 来提取左侧数字,结果是 2(我们可以忽略的小数部分)。为了提取右侧数字,我们执行 23 模 10 运算,结果是 3:
// Project 19 - Controlling Two Seven-Segment LED Display Modules
// set up the array with the segments for 0 to 9, A to F (from Table 6-2)
#define DATA 6 // connect to pin 14 on the 74HC595
#define LATCH 8 // connect to pin 12 on the 74HC595
#define CLOCK 10 // connect to pin 11 on the 74HC595
void setup()
{ pinMode(LATCH, OUTPUT); pinMode(CLOCK, OUTPUT); pinMode(DATA, OUTPUT);
}
int digits[] = {252, 96, 218, 242, 102, 182, 190, 224, 254, 246, 238, 62, 156, 122, 158, 142};
void displayNumber(int n)
{ int left, right=0;1 if (n < 10) { digitalWrite(LATCH, LOW); shiftOut(DATA, CLOCK, LSBFIRST, digits[n]); shiftOut(DATA, CLOCK, LSBFIRST, 0); digitalWrite(LATCH, HIGH); } else if (n >= 10) {2 right = n % 10; // remainder of dividing the number to display by 10 left = n / 10; // quotient of dividing the number to display by 10 digitalWrite(LATCH, LOW); shiftOut(DATA, CLOCK, LSBFIRST, digits[right]); shiftOut(DATA, CLOCK, LSBFIRST, digits[left]); digitalWrite(LATCH, HIGH); }
}3 void loop()
{ int i; for ( i = 0 ; i < 100 ; i++ ) { displayNumber(i); delay(100); }
}
在 1 处,函数检查要显示的数字是否小于 10。如果是,它将数字数据和一个空白数字发送到移位寄存器。但是,如果数字大于 10,函数将在 2 处使用模运算和除法来分离数字,然后将它们分别发送到移位寄存器。最后,在void loop()的 3 处,我们设置并调用函数来显示从 0 到 99 的数字。
项目 #20:制作数字温度计
在这个项目中,我们将把第四章项目 8 中创建的 TMP36 温度传感器,添加到项目 19 中构建的双数字电路上,制作一个能够显示 0 度及以上温度的数字温度计。算法非常简单:我们读取 TMP36 返回的电压(使用第五章项目 12 中的方法),并将读取值转换为摄氏度。
硬件
以下硬件是必需的:
-
项目 19 中的双数字电路
-
一个 TMP36 温度传感器
将 TMP36 的中心输出引脚连接到模拟引脚 5,左侧引脚连接到 5V,右侧引脚连接到 GND,这样你就可以开始测量了。
草图
这是草图:
// Project 20 - Creating a Digital Thermometer
#define DATA 6 // connect to pin 14 on the 74HC595
#define LATCH 8 // connect to pin 12 on the 74HC595
#define CLOCK 10 // connect to pin 11 on the 74HC595
int temp = 0;
float voltage = 0;
float celsius = 0;
float sensor = 0;
int digits[]={ 252, 96, 218, 242, 102, 182, 190, 224, 254, 246, 238, 62, 156, 122, 158, 142
};
void setup()
{ pinMode(LATCH, OUTPUT); pinMode(CLOCK, OUTPUT); pinMode(DATA, OUTPUT);
}
void displayNumber(int n)
{ int left, right = 0; if (n < 10) { digitalWrite(LATCH, LOW); shiftOut(DATA, CLOCK, LSBFIRST, digits[n]); shiftOut(DATA, CLOCK, LSBFIRST, digits[0]); digitalWrite(LATCH, HIGH); } if (n >= 10) { right = n % 10; left = n / 10; digitalWrite(LATCH, LOW); shiftOut(DATA, CLOCK, LSBFIRST, digits[right]); shiftOut(DATA, CLOCK, LSBFIRST, digits[left]); digitalWrite(LATCH, HIGH); }
}
void loop()
{ sensor = analogRead(5); voltage = (sensor * 5000) / 1024; // convert raw sensor value to millivolts voltage = voltage - 500; // remove voltage offset celsius = voltage / 10; // convert millivolts to Celsius temp = int(celsius); // change the floating-point temperature to an int displayNumber(temp); delay(500);
}
如图所示,这个草图借用了之前项目的代码:来自项目 19 的 displayNumber() 和来自项目 12 的温度计算。草图倒数第二行的 delay(500) 函数使得温度波动时,显示不会变化得太快。
展望未来
在这一章节中,你已经学习了很多基本技能,这些技能你将在自己的项目中反复使用。LED 显示屏相对耐用,因此可以尽情地进行实验。然而,它们能用来展示的效果是有限的,所以在下一章节中,我们将使用更为详细的文本和图形显示方法。
第七章:扩展你的 Arduino
本章内容:
-
了解各种 Arduino 扩展板
-
使用 ProtoShield 制作自己的 Arduino 扩展板
-
了解 Arduino 库如何扩展可用功能
-
使用存储卡模块记录可以在电子表格中分析的数据
-
构建一个温度记录设备
-
学习如何使用
micros()和millis()制作秒表 -
理解 Arduino 中断及其用途
扩展 Arduino 功能的另一种方式是使用扩展板。扩展板是通过引脚连接到 Arduino 两侧插座的电路板。在本章的第一个项目中,你将学习如何制作自己的扩展板。随着你在电子学和 Arduino 方面的实验,你可以通过将电路构建到ProtoShield上来使电路更加永久化,ProtoShield 是一个空白的印刷电路板,你可以用它安装定制电路。
接下来,我将介绍存储卡模块。我们将在本章中使用它来创建一个温度记录设备,用于记录随时间变化的温度;该扩展板将用于记录来自 Arduino 的数据,并将其转移到其他地方进行分析。
你将学习micros()和millis()函数,这两个函数在计时方面非常有用,正如你在秒表项目中看到的那样。最后,我们将研究中断。
扩展板
你可以通过连接扩展板来为你的 Arduino 板添加功能。市场上有数百种扩展板,它们可以组合或堆叠在一起协同工作。例如,一个流行的项目将 GPS 扩展板与 microSD 存储卡扩展板结合在一起,创建一个记录和存储位置的设备,记录的内容可以是汽车的行驶轨迹或新的徒步旅行路径的位置。其他项目包括以太网适配器,允许 Arduino 连接互联网(图 7-1)。

图 7-1:Arduino Uno 上的以太网扩展板
GPS 卫星接收器可以让你跟踪 Arduino 的位置(图 7-2)。MicroSD 存储卡接口让 Arduino 将数据存储到存储卡上(图 7-3)。
图 7-4 显示了一个堆叠的组合,包括一个 Arduino Uno,一个可以记录数据的 microSD 存储卡扩展板,一个用于连接互联网的以太网扩展板,以及一个用于显示信息的 LCD 扩展板。

图 7-2:一个 GPS 接收器扩展板(带有单独的 GPS 模块)

图 7-3:MicroSD 卡扩展板

图 7-4:一个堆叠的扩展板组合,包含一个 Arduino Uno
ProtoShields
你可以在线购买各种扩展板,或者使用 ProtoShield 自己制作扩展板。ProtoShield 是一个空白电路板,你可以用它制作自己的永久 Arduino 扩展板。ProtoShields 有预组装版或套件形式,类似于图 7-5 中展示的那种。
ProtoShield 还可以作为无焊接面包板的良好基础,因为它可以将小电路保持在你 Arduino 创作的物理边界内(如图 7-6 所示)。较小的无焊接面包板适合放置在插座的行内,可以使用可重复使用的 Blu Tack 胶泥暂时固定在电路板上,或者使用双面胶带更加永久性地安装。ProtoShield 还可以作为在面包板上测试过的电路的更永久性基础。

图 7-5:ProtoShield 套件

图 7-6:安装在 ProtoShield 无焊接面包板上的小项目示例
在 ProtoShield 上构建自定义电路需要一些前期规划。你需要设计电路,制作原理图,然后规划组件在 ProtoShield 上的布局。最后,你将电路焊接到你的自定义 Shield 上,但你应该始终先使用无焊接面包板进行测试,以确保电路正常工作。有些 ProtoShield 附带 PDF 原理图文件,可以下载并打印,专门用于绘制你的项目原理图。
项目#21:创建自定义 Shield
在这个项目中,你将创建一个包含两个 LED 和限流电阻的自定义 Shield。这个自定义 Shield 将使你在数字输出上实验 LED 变得更加容易。
硬件
本项目所需的硬件如下:
-
一个空白 Arduino ProtoShield,带有堆叠头
-
两个任何颜色的 LED
-
两个 560Ω电阻
-
两个 10kΩ电阻
-
两个按键
-
两个 100nF 电容
原理图
电路原理图见图 7-7。

图 7-7:项目 21 的原理图
ProtoShield 板的布局
下一步是了解 ProtoShield 上孔的布局。ProtoShield 上的孔的行列通常与无焊接面包板的行列匹配。然而,每个 ProtoShield 可能有所不同,因此请花时间确定孔的连接方式。在图 7-8 中展示的示例 ProtoShield 上,某些孔是连接的,正如孔之间的实线所示,但许多孔没有连接。这种设计为你使用 ProtoShield 提供了很大的灵活性。

图 7-8:从上方展示的空白 ProtoShield
注意 ProtoShield 顶部和底部被矩形包围的两组孔:这是我们焊接堆叠头的位置,使 ProtoShield 能够插入 Arduino 板。
设计
你需要将图 7-7 所示的电路转换为适合你 ProtoShield 的物理布局。一个很好的方法是使用图纸布局电路,如图 7-9 所示。然后,你可以在图纸上标记连接的孔,并轻松地进行实验,直到找到适合你特定 ProtoShield 的布局。如果你没有图纸,可以在 www.printfreegraphpaper.com/. 上生成并打印自己的图纸。

图 7-9:规划我们的定制扩展板
在你为电路绘制了计划图后,将元件进行测试安装,以确保它们能够安装进去且不会过于拥挤。如果 ProtoShield 上有空间放置复位按钮,最好添加一个,因为该扩展板会挡住你的 Arduino 上的 RESET 按钮。
焊接元件
一旦你对在 ProtoShield 上的电路布局感到满意,并且已经测试过电路以确保它正常工作,你就可以开始焊接元件了。使用焊接铁并不难,而且你不需要购买昂贵的焊接站来完成这类工作。一个额定功率为 25 到 40 瓦的简单焊接铁,比如图 7-10 中所示的那种,就足够用了。

图 7-10:焊接铁
在焊接元件时,你可能需要使用少量的焊锡和剪下的导线将它们连接起来,如图 7-11 所示。
在进行焊接时,检查每个焊接点,因为在项目完成之前,错误更容易被发现并修复。当需要焊接四个插座或插针时,可以通过使用现有的扩展板来保持它们对齐,如图 7-12 所示。

图 7-11:焊接桥

图 7-12:焊接插针
图 7-13 显示了完成的产品:一个带有两个 LED 和两个按钮的定制 Arduino 扩展板。

图 7-13:完成的定制扩展板
测试你的 ProtoShield
在继续之前,最好先测试 ProtoShield 的按钮和 LED。图 Listing 7-1 中的草图使用两个按钮来开关 LED。
// Listing 7-1: ProtoShield test
void setup()
{ pinMode(2, INPUT); pinMode(3, INPUT); pinMode(5, OUTPUT); pinMode(6, OUTPUT);
}
void loop()
{ if (digitalRead(2) == HIGH) { digitalWrite(5, HIGH); digitalWrite(6, HIGH); } if (digitalRead(3) == HIGH) { digitalWrite(5, LOW); digitalWrite(6, LOW); }
}
Listing 7-1:测试 ProtoShield 的按钮和灯光
使用库扩展草图
正如 Arduino 扩展板可以扩展我们的硬件一样,库可以为我们的草图添加有用的功能。这些功能允许我们使用特定制造商扩展板的硬件。任何人都可以创建库,就像各种 Arduino 扩展板的供应商经常编写他们自己的库来匹配他们的硬件一样。
Arduino IDE 已经包含了一些预安装的库。要在你的草图中使用它们,请选择 Sketch▶Include Library。你应该会看到一个包含 Ethernet、LiquidCrystal、Servo 等名称的预安装库集合。这些名称大多数都是自解释的。(如果本书中的项目需要使用某个库,将在这些页面中详细说明。)
如果你购买了一件新硬件,通常需要从硬件供应商的站点或提供的链接下载并安装其库。有两种方法可以安装 Arduino 库:下载库的 ZIP 文件或使用 Arduino 库管理器。我们通过演示下载 microSD 卡扩展板所需的库来看看这两种方法如何工作(图 7-3)。
下载 Arduino 库作为 ZIP 文件
首先,我们来尝试下载并安装一个 ZIP 格式的库。你将下载一个用于存储卡模块的高级库,以便读取和写入 microSD 和 SD 卡的数据:
-
访问
github.com/greiman/SdFat/并点击 Code。确保选择了 HTTPS,然后点击 Download ZIP,如图 7-14 所示。![f07014]()
图 7-14:库下载页面
-
稍等片刻,SdFat-master.zip 文件将出现在你的 Downloads 文件夹中,如图 7-15 所示。如果你使用的是 Apple 电脑,ZIP 文件可能会自动解压。
![f07015]()
图 7-15:包含 SdFat-master.zip 的 Downloads 文件夹
-
打开 Arduino IDE,选择 Sketch▶Include Library▶Add .ZIP Library,如图 7-16 所示。
![f07016]()
图 7-16:开始库安装过程
系统将显示一个文件管理对话框,如图 7-17 所示。导航到你的 Downloads 文件夹(或你保存 ZIP 文件的位置),然后点击 Open。
![f07017]()
图 7-17:定位 ZIP 文件
现在,Arduino IDE 将负责库的安装。片刻之后,IDE 输出窗口将显示一条消息,通知你库已安装,如图 7-18 所示。
![f07018]()
图 7-18:Arduino 库安装成功
-
你可以通过 IDE 的库管理器来验证 SdFat 库是否已安装并可用。为此,点击 IDE 左侧垂直组中的库管理器图标,然后在顶部的搜索框中搜索,或向下滚动直到看到你的库。例如,在图 7-19 中,你可以看到 SdFat 已出现在库管理器中。
![f07019]()
图 7-19:SdFat 库安装成功
使用库管理器导入 Arduino 库
安装 Arduino 库的另一种方式是通过 Arduino IDE 的内置库管理器。这个工具用于访问一个在线的库仓库,这些库对公众开放,且经过 Arduino 团队的个人批准,或者是非常流行的。通常,在硬件供应商的指示下,你会使用库管理器。
作为示例,我们将下载 FastLED Arduino 库,它是用于流行类型 RGB LED 的库。
为此,打开 Arduino IDE(如果还没有打开的话),然后打开库管理器。在管理器顶部的搜索框中输入FastLED,如图 7-20 所示。当你输入时,管理器会返回与你的搜索数据匹配的库,你会看到所需的库已经出现。

图 7-20:在库管理器中搜索
一旦在库管理器中找到并显示库,移动鼠标光标到库描述上。你可能会有选择版本号的选项。一般来说,默认会显示最新版本,你只需点击安装并等待安装完成。安装进度会在输出窗口中显示,如图 7-21 所示。

图 7-21:库安装过程
你可以使用本章前面描述的方法检查库是否已安装。
SD 存储卡
通过使用 SD 或 microSD 卡与 Arduino,你可以从许多来源捕获数据,例如我们在第四章中使用的 TMP36 温度传感器。你还可以使用这些卡片存储网页服务器数据或你的项目可能需要的任何文件。为了记录和存储你收集的数据,你可以使用像图 7-22 所示的存储卡。

图 7-22:一张 16GB 容量的 microSD 卡
microSD 和 SD 存储卡都可以与 Arduino 一起使用。
连接卡模块
在使用存储卡之前,你需要将六根电缆从卡片读取器模块连接到 Arduino。两种卡片读取器类型(microSD 和 SD)都具有相同的引脚,这些引脚应该像图 7-23 所示标记。

图 7-23:SD 卡模块
按照表 7-1 所示,连接你的 Arduino 和卡片读取器。
表 7-1:卡模块与 Arduino 之间的连接
| 模块引脚标签 | Arduino 引脚 | 模块引脚功能 |
|---|---|---|
| 5 V 或 Vcc | 5 V | 电源 |
| GND | GND | 地 |
| CS | D10 | 芯片选择 |
| MOSI | D11 | 数据输入到 Arduino |
| MISO | D12 | 数据输出到 Arduino |
| SCK | D13 | 时钟 |
测试你的 SD 卡
在你完成将卡模块连接到 Arduino 后——并且你拥有一张新的或新格式化的卡——现在是时候确保卡片正常工作了。为此,按照以下步骤操作:
-
将内存卡插入卡模块,然后通过 USB 电缆将模块连接到 Arduino,再将 Arduino 连接到你的电脑。
-
打开 IDE 并选择文件▶示例▶SdFat▶SdInfo。这将加载一个示例草图。
-
滚动到草图中的第 36 行,将
const int chipSelect的值从4更改为10,如图 7-24 所示。这是必要的,因为使用的引脚会根据 SD 卡硬件而有所不同。现在将这个草图上传到你的 Arduino 上。![f07024]()
图 7-24:修改测试草图
-
最后,打开串口监视器窗口,将其设置为 9,600 波特率,按下键盘上的任意键,然后按回车键。片刻之后,你应该能看到一些关于 microSD 卡的数据,如图 7-25 所示。

图 7-25:成功的内存卡测试结果
如果测试结果没有出现在串口监视器中,请尝试以下方法:
-
从 Arduino 中拔出 USB 电缆,取出并重新插入 microSD 卡。
-
确保接线连接与表 7-1 中的匹配。
-
检查串口监视器的波特率是否设置为 9,600,并确保使用的是普通的 Arduino Uno 兼容板。Mega 及其他一些板子上的 SPI 引脚位置不同。
-
重新格式化你的内存卡。
-
如果其他方法都无效,尝试使用一个新的品牌内存卡。
最后,在插入或移除内存卡之前,确保整个项目已断开与 USB 和/或电源的连接。
项目#22:向内存卡写入数据
在这个项目中,你将使用内存卡来存储数据——特别是乘法表。
草图
要向内存卡写入数据,请连接你的扩展板,插入 microSD 卡,然后输入并上传以下草图:
// Project 22 - Writing Data to the Memory Card
#include <SD.h>
int b = 0;
void setup()
{ Serial.begin(9600); Serial.println("Initializing SD card..."); pinMode(10, OUTPUT); // check that the memory card exists and is usable if (!SD.begin(10)) { Serial.println("Card failed, or not present"); // stop sketch return; } Serial.println("memory card is ready");
}
void loop()
{1 // create the file for writing File dataFile = SD.open("DATA.TXT", FILE_WRITE); // if the file is ready, write to it: if (dataFile) 2 { for ( int a = 0 ; a < 11 ; a++ ) { dataFile.print(a); dataFile.print(" multiplied by two is "); b = a * 2;3 dataFile.println(b, DEC); }4 dataFile.close(); // close the file once the system has finished with it // (mandatory) } // if the file isn't ready, show an error: else { Serial.println("error opening DATA.TXT"); } Serial.println("finished"); do {} while (1);
}

图 7-26:项目 22 的输出
该草图在 microSD 卡上创建一个名为DATA.TXT的文本文件,如图 7-26 所示。
让我们回顾一下草图中的void loop()部分,看看它是如何创建文本文件的。void loop()中 1 和 2 之间的代码用于创建并打开文件以供写入。要将文本写入文件,我们使用dataFile.print()或dataFile.println()。
这段代码的工作方式与Serial.println()类似,因此你可以像写入串口监视器那样写入它。在 1 处我们设置了创建的文本文件的名称,该名称必须是八个字符或更少,后跟一个点和三个字符,如DATA.TXT。
在 3 中,我们使用DEC作为第二个参数。这表示该变量是十进制数字,并应以这种方式写入文本文件。如果我们要写入的是float变量,则需要使用一个数字来指定小数位数(最多六位)。
当我们完成向文件写入数据时,在第 4 步,我们使用 dataFile.close() 来关闭文件。如果没有执行这一步,计算机将无法读取创建的文本文件。
项目 #23:创建一个温度记录设备
现在您已经知道如何记录数据,我们来使用第二十二章中的存储卡设置和第四章介绍的 TMP36 温度传感器电路,每分钟测量并记录 8 小时的温度。
硬件
以下硬件是必需的:
-
一个 TMP36 温度传感器
-
一块面包板
-
各种连接线
-
存储卡和模块
-
Arduino 和 USB 电缆
将 microSD 卡插入扩展板,然后将扩展板插入 Arduino。将 TMP36 的左侧(5 V)引脚连接到 Arduino 的 5 V,中央引脚连接到模拟输入,右侧引脚连接到 GND。
草图
输入并上传以下草图:
// Project 23 - Creating a Temperature-Logging Device
#include <SD.h>
float sensor, voltage, celsius;
void setup()
{ Serial.begin(9600); Serial.println("Initializing SD card..."); pinMode(10, OUTPUT); // check that the memory card exists and can be used if (!SD.begin(10)) { Serial.println("Card failed, or not present"); // stop sketch return; } Serial.println("memory card is ready");
}
void loop(){ // create the file for writing File dataFile = SD.open("DATA.TXT", FILE_WRITE); // if the file is ready, write to it: if (dataFile) { for ( int a = 0 ; a < 481 ; a++ ) // 480 minutes in 8 hours { sensor = analogRead(0); voltage = (sensor * 5000) / 1024; // convert raw sensor value to // millivolts voltage = voltage - 500; celsius = voltage / 10; dataFile.print(" Log: "); dataFile.print(a, DEC); dataFile.print(" Temperature: "); dataFile.print(celsius, 2); dataFile.println(" degrees C"); delay(599900); // wait just under one minute } dataFile.close(); // mandatory Serial.println("Finished!"); do {} while (1); }
}
这个草图完成需要大约 8 小时,但您可以通过降低 delay(599900) 中的值来改变此时间。
完成草图后,从 Arduino 中取出 microSD 卡,插入计算机,并在文本编辑器中打开日志文件,如图 7-27 所示。

图 7-27:项目 23 的结果
为了对捕获的数据进行更为严谨的分析,可以用空格或冒号分隔写入日志文件的文本行,这样文件就可以轻松导入到电子表格中。例如,您可以将文件导入 OpenOffice Calc 或 Excel,生成类似于图 7-28 所示的电子表格。

图 7-28:将数据导入电子表格
然后,您可以轻松地对数据进行一些统计分析,如图 7-29 所示。
温度示例可以根据您的数据分析项目进行修改。您可以使用相同的概念记录任何 Arduino 系统生成的数据。

图 7-29:温度分析
使用 millis() 和 micros() 的计时应用
每次 Arduino 开始运行草图时,它也会使用毫秒和微秒记录时间的流逝。1 毫秒是 1/1000 秒(0.001),1 微秒是 1/1000000 秒(0.000001)。您可以使用这些值来测量运行草图时的时间流逝。
以下函数将访问存储在 unsigned long 变量中的时间值:
unsigned long a,b; a = micros(); b = millis();
由于 unsigned long 变量类型的限制(只能存储正值),当值达到 4,294,967,295 后将重置为 0,这意味着使用 millis() 可以持续约 50 天的计时,而使用 micros() 则可以持续约 70 分钟。此外,由于 Arduino 微处理器的限制,micros() 值始终是 4 的倍数。
让我们使用这些值来看看 Arduino 将一个数字引脚从低电平转为高电平以及反向的时间。为此,我们将分别在digitalWrite()函数调用前后读取micros()的值,找出差值并在串口监视器中显示。所需的硬件只有你的 Arduino 和电缆。
输入并上传列表 7-2 中展示的草图。
// Listing 7-2
unsigned long starting, finished, elapsed;
void setup()
{ Serial.begin(9600); pinMode(3, OUTPUT); digitalWrite(3, LOW);
}
void loop()
{1 starting = micros(); digitalWrite(3, HIGH);2 finished = micros();3 elapsed = finished – starting; Serial.print("LOW to HIGH: "); Serial.print(elapsed); Serial.println(" microseconds"); delay(1000);4 starting = micros(); digitalWrite(3, LOW); finished = micros(); elapsed = finished - starting; Serial.print("HIGH to LOW: "); Serial.print(elapsed); Serial.println(" microseconds"); delay(1000);
}
列表 7-2:使用micros()定时数字引脚状态变化
该草图在digitalWrite(HIGH)函数调用前后,分别在 1 和 2 处读取micros()的值,然后计算它们的差值,并在 3 处显示在串口监视器中。对于相反的功能,这一过程在 4 处重复。
现在打开串口监视器查看结果,如图 7-30 所示。

图 7-30:列表 7-2 的输出
由于分辨率为 4 微秒,如果值为 8 微秒,我们知道持续时间大于 4 且小于或等于 8。
项目#24:创建一个秒表
现在我们可以测量两个事件之间的经过时间,接下来我们可以使用 Arduino 创建一个简单的秒表。我们的秒表将使用两个按钮:一个用于启动或重置计数,另一个用于停止计数并显示经过的时间。该草图将持续检查每个按钮的状态。当启动按钮被按下时,millis()值将被存储,当停止按钮被按下时,新的millis()值将被存储。自定义函数displayResult()将把经过的时间从毫秒转换为小时、分钟和秒。最后,时间将在串口监视器中显示。
硬件
使用本章前面描述的 ProtoShield 以及以下附加硬件:
-
一个面包板
-
两个按键(S1 和 S2)
-
两个 10 kΩ电阻(R1 和 R2)
-
各种连接线
-
Arduino 和 USB 电缆
原理图
电路原理图如图 7-31 所示。

图 7-31:项目 24 的原理图
草图
输入并上传此草图:
// Project 24 – Creating a Stopwatch
unsigned long starting, finished, elapsed;
void setup()
{ Serial.begin(9600); 1 pinMode(2, INPUT); // the start button pinMode(3, INPUT); // the stop button Serial.println("Press 1 for Start/reset, 2 for elapsed time");}
void displayResult()
{ float h, m, s, ms; unsigned long over;2 elapsed = finished - starting; h = int(elapsed / 3600000); over = elapsed % 3600000; m = int(over / 60000); over = over % 60000; s = int(over / 1000); ms = over % 1000; Serial.print("Raw elapsed time: "); Serial.println(elapsed); Serial.print("Elapsed time: "); Serial.print(h, 0); Serial.print("h "); Serial.print(m, 0); Serial.print("m "); Serial.print(s, 0); Serial.print("s "); Serial.print(ms, 0); Serial.println("ms"); Serial.println();
}
void loop()
{3 if (digitalRead(2) == HIGH) { starting = millis(); delay(200); // for debounce Serial.println("Started..."); }4 if (digitalRead(3) == HIGH) { finished = millis(); delay(200); // for debounce displayResult(); }
}
我们的秒表的基本原理很简单。在 1 处,我们设置了用于启动和停止按钮的数字输入引脚。在 3 处,如果启动按钮被按下,Arduino 将记录millis()的值,待停止按钮在 4 处按下后,我们使用该值来计算经过的时间。停止按钮按下后,经过的时间会在函数displayResult()中计算出来,在 2 处显示,并在串口监视器窗口中展示。
你应该在串口监视器中看到类似图 7-32 中的结果。

图 7-32:项目 24 的输出
中断
在 Arduino 中,中断基本上是一个信号,允许在程序中的任何时刻调用一个函数——例如,当数字输入引脚的状态发生变化,或触发计时器事件时。中断非常适合调用函数以中断程序的正常运行,例如当按钮被按下时。这类函数通常被称为中断处理程序。
当中断被触发时,程序的正常操作和运行会暂时中止,直到中断函数被调用并执行完毕。然后,当中断函数退出时,程序会继续从中断前的状态恢复执行。
中断函数应简短且简单。它们应该快速退出,并且要记住,如果中断函数执行的操作与主循环已经在做的操作相同,那么中断函数将暂时覆盖主循环的活动,直到主循环恢复。例如,如果主循环定期通过串口发送Hello,而中断函数在触发时发送---,那么你可能会在串口看到以下任何一种输出:H----ello、He----llo、Hel----lo、Hell----o或Hello----。
Arduino Uno 提供了两个中断,可以通过数字引脚 2 和 3 使用。当正确配置时,Arduino 将监控应用到引脚的电压。当电压以某种特定方式变化时(例如,当按钮被按下时),中断被触发,导致相应的函数执行——可能是发送“Stop Pressing Me!”。
中断模式
四种变化(或模式)中的任意一种都可以触发一个中断:
-
LOW没有电流应用到中断引脚。 -
CHANGE电流发生变化,无论是开关之间变化,还是关闭与打开之间变化。 -
RISING电流从关闭状态变化为 5V 的开启状态。 -
FALLING电流从 5V 的开启状态变化为关闭状态。
例如,要检测连接到中断引脚的按钮是否被按下,你可以使用RISING模式。或者,例如,如果你在花园里铺设了一条电动防护绳(连接在 5V 和中断引脚之间),你可以使用FALLING模式来检测当防护绳被触发并断开时。
配置中断
要配置中断,请在void setup()中使用以下内容:
attachInterrupt(0, *function*, *mode*); attachInterrupt(1, *function*, *mode*);
这里,0 对应数字引脚 2,1 对应数字引脚 3,function 是在中断触发时要调用的函数名称,mode 是触发中断的四种模式之一。
激活或停用中断
有时你可能不想在程序中使用中断。你可以通过以下命令停用单个中断:
detachInterrupt(digitalPinToInterrupt*(pin)*)
其中 pin 是所用的数字引脚编号。或者,你也可以使用以下命令停用所有中断:
noInterrupts(); // deactivate interrupts
然后通过以下方式重新激活它们:
interrupts(); // reactivate interrupts
中断工作快速且非常敏感。这些特点使它们非常适合时间关键型应用或项目中的“紧急停止”按钮。
项目 #25:使用中断
我们将使用项目 24 中的电路来演示中断的使用。我们的示例将每 500 毫秒闪烁内置 LED,在此期间,将监控两个中断引脚。当按下中断 0 上的按钮时,micros()的值将在串口监视器中显示;当按下中断 1 上的按钮时,millis()的值将显示出来。
草图
输入并上传以下草图:
// Project 25 – Using Interrupts
#define LED 13
void setup()
{ Serial.begin(9600); pinMode(13, OUTPUT); attachInterrupt(0, displayMicros, RISING); attachInterrupt(1, displayMillis, RISING);
}1 void displayMicros()
{ Serial.write("micros() = "); Serial.println(micros());
}2void displayMillis()
{ Serial.write("millis() = "); Serial.println(millis());
}3 void loop()
{ digitalWrite(LED, HIGH); delay(500); digitalWrite(LED, LOW); delay(500);
}
这个草图将根据void loop()中的代码以 3 的频率闪烁板载 LED。当中断 0 被触发时,将调用函数displayMicros()(见 1);当中断 1 被触发时,将调用函数displayMillis()(见 2)。无论哪个函数完成,草图都将恢复运行void loop()中的代码。
打开串口监视器窗口,按下两个按钮以查看millis()和micros()的值,如图 7-33 所示。

图 7-33:项目 25 输出
展望未来
本章为你提供了更多工具和选项,您可以根据需要进行调整,以创建和改进自己的项目。在未来的章节中,我们将使用更多的 Arduino 扩展板,利用中断进行定时项目,并在其他数据记录应用中使用存储卡模块。
第八章:LED 数字显示和矩阵
在本章中,您将
-
使用基于 MAX7219 的数字 LED 显示
-
自建数字秒表计时器
-
使用基于 MAX7219 的 LED 矩阵模块
-
构建滚动文本 LED 显示
尽管 LED 数字显示器(例如现代数字闹钟中使用的那种)可能不是最前沿的显示技术,但它们易于阅读,而且——更重要的是——与我们的 Arduino 板兼容,使用起来非常简单。
您在第六章中已经学习了如何使用一位和两位数字的 LED 显示器。然而,同时使用超过两位数字可能会变得杂乱无章——需要更多的接线、更复杂的控制 IC 等。幸运的是,有一种流行的集成电路(IC)可以通过三根控制线从我们的 Arduino 控制最多 64 个 LED(八位数字显示器):MAX7219 LED 驱动 IC,来自 Maxim Integrated 公司。
MAX7219 有穿孔封装类型,这意味着它具有金属引脚,可以插入电路板或无焊接面包板(如图 8-1 所示),也有表面贴装封装类型(如图 8-2 所示)。

图 8-1:MAX7219 的穿孔封装类型

图 8-2:MAX7219 的表面贴装封装类型
在本章中,您将学习如何使用 MAX7219 控制最多八个数字 LED。您还将学习如何使用 MAX7219 控制有趣的 LED 矩阵模块,这些模块支持滚动文本显示。
LED 数字显示
使用 MAX7219 的 LED 数字显示有很多形状和尺寸,通常模块上装有四到八位数字。对于我们的示例,我们使用的是一个八位数字模块,价格实惠且性价比高(见图 8-3)。

图 8-3:八位数字 LED 模块
这些模块的背面有 MAX7219 的表面贴装版本,如图 8-2 所示。模块通常包含一些内联的插头引脚,以便连接控制线。如果您还没有这样做,请将它们焊接到模块上,如图 8-4 所示。

图 8-4:内联插头引脚连接到八位数字 LED 模块
在您使用数字显示器之前,您需要将五根线连接到显示器和 Arduino。这可以通过将公对母跳线连接到您已焊接到板上的插头引脚来轻松完成。按照表 8-1 中所示的方式进行连接。
表 8-1:显示模块与 Arduino 之间的连接
| 模块引脚标签 | Arduino 引脚 | 模块引脚功能 |
|---|---|---|
| Vcc | 5V | 电源(+) |
| GND | GND | 电源(−)或地面 |
| DIN | D12 | 数据输入 |
| CS | D10 | 芯片选择 |
| CLK | D11 | 时钟信号 |
安装库
有几种 Arduino 库可以用于 MAX7219。这些库根据使用的显示模块配置不同而有所不同。我们将使用 LedControl 库。你需要从https://github.com/wayoda/LedControl/下载库的 ZIP 文件。点击克隆或下载,然后点击下载 ZIP,如图 8-5 所示。

图 8-5:下载 LedControl 库
一旦你有了 ZIP 文件,按照第七章的描述进行安装。接下来,为了使用显示模块,我们将首先检查一个示例程序,使用所需的函数。输入并上传清单 8-1 中显示的基本示例。
// Listing 8-11 #include "LedControl.h" // need the library
LedControl lc = LedControl(12, 11, 10, 1);
void setup()
{2 lc.shutdown(0, false); // enable display lc.setIntensity(0, 3); // set brightness lc.clearDisplay(0); // clear screen
}
void loop()
{ // numbers with decimal point for (int a = 0; a < 8; a++) {3 lc.setDigit(0, a, a, true); delay(500); lc.clearDisplay(0) ; // clear screen } // dashes for (int a = 0; a < 8; a++) {4 lc.setChar(0, a, '-', false); delay(500); lc.clearDisplay(0) ; // clear screen } // numbers without decimal point for (int a = 0; a < 8; a++) { lc.setDigit(0, a, a, false); delay(500); lc.clearDisplay(0) ; // clear screen }5 // display "abcdef" lc.setDigit(0, 5, 10, false); lc.setDigit(0, 4, 11, false); lc.setDigit(0, 3, 12, false); lc.setDigit(0, 2, 13, false); lc.setDigit(0, 1, 14, false); lc.setDigit(0, 0, 15, false); delay(500); lc.clearDisplay(0) ; // clear screen
}
清单 8-1:显示模块演示示例
让我们来看看清单 8-1 中的示例是如何工作的。在第 1 步,我们包含了用于加载显示模块库的必要代码。LedControl()函数有四个参数:
LedControl lc = LedControl(12, 11, 10, 1);
前三个参数指定了连接的数字引脚(见表 8-1),第四个参数是连接到 Arduino 的显示模块数量——在此例中为一个。(你可以串联多个模块。)
在第 2 步,我们有三个函数来控制显示的各个方面。第一个函数用来开启或关闭显示:
lc.shutdown(0, false);
第一个参数是显示器。我们使用 0,因为只连接了一个显示器。如果你连接了多个显示器,第二个参数是显示器 1,第三个是显示器 2,依此类推。第二个参数指定显示器的开关状态:false表示开启,true表示关闭。
第二个函数用于设置显示器中 LED 的亮度:
lc.setIntensity(0, 3);
第一个参数是显示器编号。第二个是亮度,范围从 0 到 15(包含)。
第三个函数简单地将所有 LED 关闭:
lc.clearDisplay(0);
这对于清除之前显示的数据非常有用。
在第 3 步,我们使用setDigit()在屏幕上显示一个数字:
lc.setDigit(0, a, b, true);
第一个参数是显示器编号。第二个是数字在显示器上的物理位置;对于一个八位数显示器,这个值从 7(最左边的数字)到 0(最右边的数字)。第三个参数是要显示的实际数字(0 到 9)。如果使用 10 到 16,你可以显示字母 A 到 F,就像我们在第 5 步中做的那样。最后,第四个参数控制小数点:true表示开启,false表示关闭。
你还可以使用setChar()写入字符 A 到 F、H、L、P、破折号、句点和下划线,如第 4 步所示:
lc.setChar(0, a, '-', false);
参数相同,唯一不同的是你用单引号括起来字符。
现在我们已经了解了所有用于在显示器上显示数字和字符的命令,接下来让我们将它们付诸实践。
项目#26:数字秒表
你在第七章的项目 24 中学习了定时方法,现在你刚刚学会了如何使用显示模块,因此你可以将这些概念结合起来创建一个数字秒表。虽然它的准确性无法与奥林匹克计时水平相媲美,但这是一个有趣且实用的项目。你的秒表将能够显示毫秒、秒、分钟以及最多九个小时。
你需要连接 ProtoShield(或等效电路),正如第七章中所描述的,并与本章早些时候使用的数字显示连接。然后只需上传以下草图:
// Project 26 – Digital Stopwatch
#include "LedControl.h" // need the library
LedControl lc = LedControl(12, 11, 10, 1);
unsigned long starting, finished, elapsed;
void setup()
{ pinMode(2, INPUT); // the start button pinMode(3, INPUT); // the stop button lc.shutdown(0, false); // enable display lc.setIntensity(0, 3); // set brightness lc.clearDisplay(0); // clear screen starting = millis();
}1 void displayResultLED()
{ float h, m, s, ms; int m1, m2, s1, s2, ms1, ms2, ms3; unsigned long over; finished = millis(); elapsed = finished - starting;2 h = int(elapsed / 3600000); over = elapsed % 3600000; m = int(over / 60000); over = over % 60000; s = int(over / 1000); ms = over % 1000; 3 // display hours lc.setDigit(0, 7, h, true); // display minutes m1 = m / 10; m2 = int(m) % 10; lc.setDigit(0, 6, m1, false); lc.setDigit(0, 5, m2, true); // display seconds s1 = s / 10; s2 = int(s) % 10; lc.setDigit(0, 4, s1, false); lc.setDigit(0, 3, s2, true); // display milliseconds (1/100 s) ms1 = int(ms / 100); ms2 = (int((ms / 10)) % 10); ms3 = int(ms) % 10; lc.setDigit(0, 2, ms1, false); lc.setDigit(0, 1, ms2, false); lc.setDigit(0, 0, ms2, false);
}
void loop()
{4 if (digitalRead(2) == HIGH) // reset count { starting = millis(); delay(200); // for debounce }5 if (digitalRead(3) == HIGH) // display count for five seconds then resume { finished = millis(); delay(5000); // for debounce } displayResultLED();
}
在草图上传后的短短一会儿,显示器将开始计数,如图 8-6 所示。

图 8-6:秒表工作中的样子
正如我们在项目 24 中所做的那样,在这个草图中,我们使用millis()来追踪经过的时间。我们将时间计算和显示放在了void displayResultLED()函数中 1。
在图 2 中,你可以看到毫秒数是如何被拆分为小时、分钟、秒和毫秒的。然后,显示器的每一位数字从左到右填充相应的时间值,从小时 3 开始。秒表的控制非常简单:当用户按下连接到数字输入 2 的按钮时,计数器会通过将起始时间设置为millis()函数当前返回的值来重置为零 4。当按下连接到数字输入 3 的按钮时,显示会被冻结 5;这个功能非常适合用于记录圈速。然而请注意,计时仍会继续,显示在大约五秒后会恢复。
这个项目可以很容易地修改为以更简单的格式显示数据——比如小时、分钟和秒——或者用于更长的时间段,例如最多 24 小时。但现在,让我们继续进行一个更复杂的项目,涉及 LED 矩阵显示板。
项目 #27:使用 LED 矩阵模块
MAX7219 可以控制最多 64 个 LED。在上一个项目中,我们展示了这些数字。这里,我们将使用以 8 × 8 矩阵形式排列 LED 的模块,这种形式非常适合更有趣的应用,比如显示固定或滚动的文本。
LED 矩阵模块通常以单独单元或四个一组的形式出售;两种形式如图 8-7 所示。
这些也可能作为套件出售;然而,节省的成本微乎其微,因此建议选择预组装版本来节省时间。LED 显示器可以插入模块上的插座引脚,如图 8-8 所示,使得更换颜色变得容易。

图 8-7:LED 矩阵模块

图 8-8:可拆卸的 LED 矩阵
在将 LED 矩阵插入模块时要小心,因为有些 LED 矩阵的引脚容易弯曲。经验表明,你仍然需要将直排针焊接到矩阵模块上。不过,这些引脚通常会随模块一起提供,并且如图 8-9 所示,能够整齐地适配。

图 8-9:与矩阵模块连接的内联头引脚
再次强调,在使用矩阵模块之前,你需要像连接数字显示器那样,将五根线连接到模块和 Arduino。按照表 8-2 所示进行连接。
表 8-2:矩阵模块与 Arduino 之间的连接
| 模块引脚标签 | Arduino 引脚 | 模块引脚功能 |
|---|---|---|
| Vcc | 5V | 电源(+) |
| GND | GND | 电源(−)或地 |
| DIN | D11 | 数据输入 |
| CS | D9 | 芯片选择 |
| CLK | D13 | 时钟信号 |
安装库
你将为这些模块使用与 MAX7219 不同的库。要获取该库,请访问github.com/bartoszbielawski/LEDMatrixDriver/,点击克隆或下载,然后选择下载 ZIP,如图 8-10 所示。

图 8-10:下载 LEDMatrixDriver 库
下载 ZIP 文件后,按照第七章的描述进行安装。接着输入并上传以下草图。(在此,我想提醒一下,书中所有的代码都可以从nostarch.com/arduino-workshop-2nd-edition/下载。)
// Project 27 - Using LED Matrix Modules1 #include <LEDMatrixDriver.hpp>
const uint8_t LEDMATRIX_CS_PIN = 9;
// Number of matrix modules you are connecting
const int LEDMATRIX_SEGMENTS = 4;
const int LEDMATRIX_WIDTH = LEDMATRIX_SEGMENTS * 8;
LEDMatrixDriver lmd(LEDMATRIX_SEGMENTS, LEDMATRIX_CS_PIN);
// Text to display2 char text[] = "** LED MATRIX DEMO! ** (1234567890) ++ \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" ++ <$%/=?'.@,> --";
// scroll speed (smaller = faster)3 const int ANIM_DELAY = 30;
void setup() {4 // init the display lmd.setEnabled(true); lmd.setIntensity(2); // 0 = low, 10 = high
}
int x = 0, y = 0; // start top left
// font definition5 byte font[95][8] = { {0, 0, 0, 0, 0, 0, 0, 0}, // SPACE {0x10, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18}, // EXCL {0x28, 0x28, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00}, // QUOT {0x00, 0x0a, 0x7f, 0x14, 0x28, 0xfe, 0x50, 0x00}, // # {0x10, 0x38, 0x54, 0x70, 0x1c, 0x54, 0x38, 0x10}, // $ {0x00, 0x60, 0x66, 0x08, 0x10, 0x66, 0x06, 0x00}, // % {0, 0, 0, 0, 0, 0, 0, 0}, // & {0x00, 0x10, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00}, // ' {0x02, 0x04, 0x08, 0x08, 0x08, 0x08, 0x08, 0x04}, // ( {0x40, 0x20, 0x10, 0x10, 0x10, 0x10, 0x10, 0x20}, // ) {0x00, 0x10, 0x54, 0x38, 0x10, 0x38, 0x54, 0x10}, // * {0x00, 0x08, 0x08, 0x08, 0x7f, 0x08, 0x08, 0x08}, // + {0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x08}, // COMMA {0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00}, // - {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06}, // DOT {0x00, 0x04, 0x04, 0x08, 0x10, 0x20, 0x40, 0x40}, // / {0x00, 0x38, 0x44, 0x4c, 0x54, 0x64, 0x44, 0x38}, // 0 {0x04, 0x0c, 0x14, 0x24, 0x04, 0x04, 0x04, 0x04}, // 1 {0x00, 0x30, 0x48, 0x04, 0x04, 0x38, 0x40, 0x7c}, // 2 {0x00, 0x38, 0x04, 0x04, 0x18, 0x04, 0x44, 0x38}, // 3 {0x00, 0x04, 0x0c, 0x14, 0x24, 0x7e, 0x04, 0x04}, // 4 {0x00, 0x7c, 0x40, 0x40, 0x78, 0x04, 0x04, 0x38}, // 5 {0x00, 0x38, 0x40, 0x40, 0x78, 0x44, 0x44, 0x38}, // 6 {0x00, 0x7c, 0x04, 0x04, 0x08, 0x08, 0x10, 0x10}, // 7 {0x00, 0x3c, 0x44, 0x44, 0x38, 0x44, 0x44, 0x78}, // 8 {0x00, 0x38, 0x44, 0x44, 0x3c, 0x04, 0x04, 0x78}, // 9 {0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00}, // : {0x00, 0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x08}, // ; {0x00, 0x10, 0x20, 0x40, 0x80, 0x40, 0x20, 0x10}, // < {0x00, 0x00, 0x7e, 0x00, 0x00, 0xfc, 0x00, 0x00}, // = {0x00, 0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08}, // > {0x00, 0x38, 0x44, 0x04, 0x08, 0x10, 0x00, 0x10}, // ? {0x00, 0x30, 0x48, 0xba, 0xba, 0x84, 0x78, 0x00}, // @ {0x00, 0x1c, 0x22, 0x42, 0x42, 0x7e, 0x42, 0x42}, // A {0x00, 0x78, 0x44, 0x44, 0x78, 0x44, 0x44, 0x7c}, // B {0x00, 0x3c, 0x44, 0x40, 0x40, 0x40, 0x44, 0x7c}, // C {0x00, 0x7c, 0x42, 0x42, 0x42, 0x42, 0x44, 0x78}, // D {0x00, 0x78, 0x40, 0x40, 0x70, 0x40, 0x40, 0x7c}, // E {0x00, 0x7c, 0x40, 0x40, 0x78, 0x40, 0x40, 0x40}, // F {0x00, 0x3c, 0x40, 0x40, 0x5c, 0x44, 0x44, 0x78}, // G {0x00, 0x42, 0x42, 0x42, 0x7e, 0x42, 0x42, 0x42}, // H {0x00, 0x7c, 0x10, 0x10, 0x10, 0x10, 0x10, 0x7e}, // I {0x00, 0x7e, 0x02, 0x02, 0x02, 0x02, 0x04, 0x38}, // J {0x00, 0x44, 0x48, 0x50, 0x60, 0x50, 0x48, 0x44}, // K {0x00, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x7c}, // L {0x00, 0x82, 0xc6, 0xaa, 0x92, 0x82, 0x82, 0x82}, // M {0x00, 0x42, 0x42, 0x62, 0x52, 0x4a, 0x46, 0x42}, // N {0x00, 0x3c, 0x42, 0x42, 0x42, 0x42, 0x44, 0x38}, // O {0x00, 0x78, 0x44, 0x44, 0x48, 0x70, 0x40, 0x40}, // P {0x00, 0x3c, 0x42, 0x42, 0x52, 0x4a, 0x44, 0x3a}, // Q {0x00, 0x78, 0x44, 0x44, 0x78, 0x50, 0x48, 0x44}, // R {0x00, 0x38, 0x40, 0x40, 0x38, 0x04, 0x04, 0x78}, // S {0x00, 0x7e, 0x90, 0x10, 0x10, 0x10, 0x10, 0x10}, // T {0x00, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x3e}, // U {0x00, 0x42, 0x42, 0x42, 0x42, 0x44, 0x28, 0x10}, // V {0x80, 0x82, 0x82, 0x92, 0x92, 0x92, 0x94, 0x78}, // W {0x00, 0x42, 0x42, 0x24, 0x18, 0x24, 0x42, 0x42}, // X {0x00, 0x44, 0x44, 0x28, 0x10, 0x10, 0x10, 0x10}, // Y {0x00, 0x7c, 0x04, 0x08, 0x7c, 0x20, 0x40, 0xfe}, // Z
};6 void drawString(char* text, int len, int x, int y)
{ for ( int idx = 0; idx < len; idx ++ ) { int c = text[idx] - 32; // stop if char is outside visible area if ( x + idx * 8 > LEDMATRIX_WIDTH ) return; // only draw if char is visible if ( 8 + x + idx * 8 > 0 ) drawSprite( font[c], x + idx * 8, y, 8, 8 ); }
}7 void scrollText(){ int len = strlen(text); drawString(text, len, x, 0); lmd.display(); delay(ANIM_DELAY); if ( --x < len * -8 ) { x = LEDMATRIX_WIDTH; }
}
void loop()
{ scrollText();
}
上传草图后过一会儿,你应该能看到文字从右到左滚动显示在 LED 显示模块上。
现在让我们深入了解一下这个草图是如何工作的。代码量很大,但不要因此感到害怕。从 1 开始,我们调用所需的函数来使用库并设置显示器。在 2 处,一个字符数组包含了要显示在显示模块上的文本。你可以稍后修改它。如果需要,你还可以通过改变 3 处的数值来调整滚动速度:数字越小,滚动速度越快。
在 4 处,我们有两个函数。这个函数用于开关显示:
lmd.setEnabled(true);
这个设置了显示模块中 LED 的亮度:
lmd.setIntensity(*x*);
setIntensity()函数的值介于 0(暗)和 9(亮)之间。
显示器使用的字体定义在 5 处的一个巨大数组中。我们将在下一节中详细介绍它。最后,drawstring() 6 和scrollText() 7 函数是显示操作所必需的。
编辑显示字体
你可以通过更改byte font数组 5 中的数据,轻松指定哪些字符可以在显示中使用。首先,回顾一下,每个矩阵模块由八行八个 LED 组成。这意味着你有 64 个 LED 可用于创建任何字符。
每一行 LED 由一个十六进制数定义,八个这样的十六进制数代表一个字符。例如,字母 N 由以下方式定义:
{0x00, 0x42, 0x42, 0x62, 0x52, 0x4a, 0x46, 0x42}, // N
为了显示字符,我们将十六进制数字转换为二进制。例如,我们的字母 N 从十六进制转换为二进制如下:
0 0 0 0 0 0 0 0 = 0x00
0 1 0 0 0 0 1 0 = 0x42
0 1 0 0 0 0 1 0 = 0x42
0 1 1 0 0 0 1 0 = 0x62
0 1 0 1 0 0 1 0 = 0x52
0 1 0 0 1 0 1 0 = 0x4a
0 1 0 0 0 1 1 0 = 0x46
0 1 0 0 0 0 1 0 = 0x42
你可以看到 1 表示字符与 0 的场域,其中 1 代表点亮的 LED,0 代表熄灭的 LED。因此,要创建自己的字符,只需反转这一过程。例如,一个漂亮的笑脸可以表示为:
0 1 1 1 1 1 1 0 = 0x7e
1 0 0 0 0 0 0 1 = 0x81
1 0 1 0 0 1 0 1 = 0xa5
1 0 0 0 0 0 0 1 = 0x81
1 0 1 0 0 1 0 1 = 0xa5
1 0 0 1 1 0 0 1 = 0x99
1 0 0 0 0 0 0 1 = 0x81
0 1 1 1 1 1 1 0 = 0x7e
这将在数组中表示为:
{0x7e,0x81,0xa5,0x81,0xa5,0x99,0x81,0x7e} // smiley
你可以用新的数据替换font数组中现有的某一行,或者将数据作为另一个元素添加到数组末尾。如果添加另一行,你需要增加byte声明中的第一个参数,使其等于已定义字符的数量(在本例中为 96):
byte font[96][8]
你现在可能在想如何在草图中引用你的自定义字符。显示库使用的是 ASCII 图表中的字符顺序,可以在www.arduino.cc/en/Reference/ASCIIchart/找到。
如果在草图中的最后一个字符(默认是 Z)后添加另一个字符,则下一个字符表中的字符是[。因此,要在显示器上滚动三个笑脸,你需要将显示文本的行设置为:
char text[] = " [ [ ";
这种输出的示例可以在[图 8-11 中看到。

图 8-11:使用自定义字符显示笑脸
展望未来
现在你知道如何使用它们了,操作 LED 数字和矩阵显示器将变得轻松。 However, 还有更多种类的显示器,接下来翻到下一章了解另一种:液晶显示器。
第九章:液晶显示器
在本章中,您将
-
使用字符 LCD 模块显示文本和数字数据
-
创建自定义字符以在字符 LCD 模块上显示
-
使用彩色 LCD 模块显示文本和数据
-
创建温度历史图表显示
对于一些项目,您可能希望将信息显示在除桌面计算机显示器以外的地方。显示信息的最简单且多用途的方式之一是使用液晶显示屏(LCD)模块和您的 Arduino。您可以使用字符 LCD 模块显示文本、自定义字符和数字数据,使用图形 LCD 模块显示彩色图形。
字符 LCD 模块
显示字符(如文本和数字)的 LCD 模块是所有 LCD 中最便宜且最简单使用的。它们可以购买多种尺寸,尺寸按它们能显示的字符行数和列数来衡量。一些模块包括背光,并允许您选择字符的颜色和背景颜色。任何具有 HD44780 或 KS0066 兼容接口并带有 5V 背光的 LCD 都应能与您的 Arduino 配合使用。我们将使用的第一个 LCD 是一个 16 字符×2 行的 LCD 模块,带有背光,如图 9-1 所示。

图 9-1:带调节电位器和引脚的 LCD 模块示例
调节电位器(用于 LCD 的可变电阻)值为 10 kΩ,用于调整显示对比度。如果 LCD 顶部的孔排未焊接上引脚,则需要先焊接,以便轻松插入面包板。
LCD 顶部的孔排编号从 1 到 16。第 1 号孔靠近模块的角落,在图 9-2 中标记为 VSS(连接到 GND)。我们将参考此原理图进行本书中的所有 LCD 示例。在一些罕见情况下,您可能会遇到具有 4.2 V 背光的 LCD,而不是 5 V 背光。(如果不确定,请与您的供应商确认。)如果是这种情况,将 1N4004 二极管串联在 Arduino 的 5V 与 LCD 的 LED+引脚之间。

图 9-2:基本 LCD 原理图
在草图中使用字符 LCD
要使用图 9-1 中显示的字符 LCD,我们将首先通过一些简单的演示来探索所需的函数及其工作原理。在继续之前,您需要通过库管理器安装所需的 Arduino 库。按照第七章中描述的方法,搜索并安装“LiquidCrystal by Arduino, Adafruit”库。然后,您可以输入并上传清单 9-1 中显示的基本草图。
// Listing 9-1
#include <LiquidCrystal.h>
LiquidCrystal lcd(4, 5, 6, 7, 8, 9); // pins for RS, E, DB4, DB5, DB6, DB7
void setup()
{ lcd.begin(16, 2); lcd.clear();
}
void loop(){ lcd.setCursor(0, 5); lcd.print("Hello"); lcd.setCursor(1, 6); lcd.print("world!"); delay(10000);
}
清单 9-1:LCD 演示草图
图 9-3 显示了清单 9-1 的结果。

图 9-3:LCD 演示:“Hello world!”
接下来,看看代码清单 9-1 中的草图是如何工作的。首先,我们需要添加一行代码,目的是引入 LCD 模块的库(该库会随着 Arduino IDE 自动安装)。然后,我们需要告诉库哪些引脚连接到 Arduino。为此,我们在void setup()方法之前添加以下代码:
#include <LiquidCrystal.h>
LiquidCrystal lcd(4, 5, 6, 7, 8, 9); // pins for RS, E, DB4, DB5, DB6, DB7
输入到 LiquidCrystal 函数中的数字与 LCD 上标记的引脚相对应。如果你不确定 LCD 的引脚排列,请联系供应商。
如果你需要使用 Arduino 上的不同数字引脚,可以在这段代码的第二行调整引脚编号。
接下来,在void setup()中,我们告诉 Arduino LCD 的列数和行数。例如,下面是如何告诉 Arduino LCD 有 2 行,每行 16 个字符:
lcd.begin(16, 2);
显示文本
完成 LCD 设置后,使用以下代码清除 LCD 的显示:
lcd.clear();
然后,为了定位光标,也就是文本的起始点,可以使用以下代码:
lcd.setCursor(`x`, `y`);
这里,x是列(0 到 15),y是行(0 或 1)。接下来,若要显示单词text,例如,可以输入以下代码:
lcd.print("text");
现在你已经可以定位和显示文本,接下来让我们看看如何显示变量数据。
显示变量或数字
要在 LCD 屏幕上显示变量的内容,请使用以下代码:
lcd.print(`variable`);
如果你要显示一个float变量,你可以指定使用的小数位数。例如,lcd.print(pi, 3)会告诉 Arduino 显示π的值,保留三位小数,如图 9-4 所示:
float pi = 3.141592654; lcd.print("pi: "); lcd.print(pi, 3);

图 9-4:LCD 显示浮点数
当你要在 LCD 屏幕上显示一个整数时,可以选择以十六进制或二进制形式显示,如代码清单 9-2 所示。
// Listing 9-2 int zz = 170; lcd.setCursor(0, 0); lcd.print("Binary: "); lcd.print(zz, BIN); // display 170 in binary lcd.setCursor(0, 1); lcd.print("Hexadecimal: "); lcd.print(zz, HX); // display 170 in hexadecimal
代码清单 9-2:显示二进制和十六进制数字的函数
LCD 将显示图 9-5 中的文本。

图 9-5:代码清单 9-2 中的代码结果
项目#28:定义自定义字符
除了使用键盘上可用的标准字母、数字和符号外,你还可以在每个草图中定义最多八个自定义字符。注意,在 LCD 模块中,每个字符由八行五个点或像素组成。图 9-6 显示了特写。

图 9-6:每个字符由八行五个像素组成。
要显示你自定义的字符,必须首先使用数组定义每个字符。例如,要创建一个微笑的表情符号,你可以使用以下代码:
byte a[8] = { B00000, B01010, B01010, B00000, B00100, B10001, B01110, B00000 };
数组中的每个数字表示显示器中的一个像素。0 表示关闭一个像素,1 表示打开它。数组中的元素表示显示器中像素的行;最上面的元素是顶行,接下来的元素是第二行,以此类推。
在这个例子中,由于第一个元素是B00000,顶部行的所有像素都被关闭。在下一个元素B01010中,每隔一个像素被点亮,1 的部分形成了眼睛的上方。接下来的行继续填充这个字符。
接下来,将数组(定义你新字符的内容)分配给void setup()中的第一个自定义字符插槽,方法如下:
lcd.createChar(0, a); // assign the array a[8] to custom character slot 0
最后,要显示字符,在void loop()中添加以下内容:
lcd.write(byte(0));
要显示我们的自定义字符,我们将使用以下代码:
// Project 28 - Defining Custom Characters
#include <LiquidCrystal.h>
LiquidCrystal lcd(4, 5, 6, 7, 8, 9); // pins for RS, E, DB4, DB5, DB6, DB7
byte a[8] = { B00000, B01010, B01010, B00000, B00100, B10001, B01110, B00000 };
void setup()
{ lcd.begin(16, 2); lcd.createChar(0, a);
}
void loop()
{ lcd.write(byte(0)); // write the custom character 0 to the next cursor // position
}
图 9-7 显示了 LCD 屏幕上的笑脸。

图 9-7:项目 28 的结果
字符 LCD 模块使用简单且有一定的多功能性。例如,利用你所学的内容,你可以将此 LCD 与第六章第 122 页项目 20 中的温度测量部分结合,制作一个详细的数字温度计。然而,如果你需要显示大量数据或图形项,你将需要使用图形 LCD 模块。
图形 LCD 模块
图形 LCD 模块比字符模块更大且更昂贵,但它们也更具多功能性。你不仅可以用它们显示文本,还可以绘制线条、点、圆圈等,创建视觉效果。本书中使用的图形 LCD 是一个 128 × 160 像素的彩色模块,具有 ST7735 兼容接口,如图 9-8 所示。

图 9-8:图形 LCD 模块
连接图形 LCD
在使用图形 LCD 之前,你需要将 LCD 与 Arduino 之间连接八根线。这可以通过公对母跳线轻松完成,因为 LCD 的连接引脚在工厂时已预焊接。按照表 9-1 所示进行连接。
表 9-1:图形 LCD 模块与 Arduino 之间的连接
| LCD 引脚标签 | 连接到 Arduino 引脚 | LCD 引脚功能 |
|---|---|---|
| Vcc | 5 V | VDD |
| GND | GND | VSS (GND) |
| CS | D10 | 片选 |
| RST | D8 | 重置 |
| A0(或 DC) | D9 | 控制 |
| SDA | D11 | 数据输入 |
| SCK | D13 | 时钟输入 |
| LED | 3.3 V | 背光 LED |
使用 LCD
在继续之前,你需要通过库管理器安装所需的 Arduino 库。使用第七章中描述的方法,搜索并安装“TFT by Arduino, Adafruit”库。
要使用 LCD,在void setup()之前插入以下三行:
#include <TFT.h> // include the graphics LCD library
#include <SPI.h> // include the library for the SPI data bus
TFT TFTscreen = TFT(10, 9, 8); // allocate pins to LCD
#include <SPI.h> // library for SPI data bus
(不要对“SPI 数据总线”感到慌张;目前为止,上面的内容已经足够了解。我们将在第十九章更详细地讨论 SPI 总线。)
然后,在void setup()中添加以下几行,以准备显示:
TFTscreen.begin(); // activate LCD TFTscreen.background(0, 0, 0); // clear the LCD screen
控制显示
你可以选择五种文本大小,如图 9-9 和 9-10 所示。
你首先需要考虑的是你生成的显示内容的背景色。可以通过以下设置来完成:
TFTscreen.background(b, g, r); // set background color

图 9-9:LCD 上可用的五种文本大小中的四种

图 9-10:LCD 上可用的五种文本大小中最大的一种
你使用 RGB(红色、绿色、蓝色)值设置背景颜色,值范围为 0 到 255。例如,白色背景会是最大红色、最大绿色和最大蓝色—也就是 255、255、255。纯红色背景则红色值为 255,绿色和蓝色值为 0。黑色背景则三个值都为零。(你可以在www.rapidtables.com/web/color/RGB_Color.html找到一个有用的 RGB 颜色表。)
接下来,如果你是第一次在 LCD 上写文本,或者需要在绘图过程中更改文本大小,你需要设置文本大小。为此,请使用:
TFTscreen.setTextSize(`x`);
其中,x是 1 到 5 之间的数字,匹配图 9-9 和 9-10 中显示的文本大小。
然后,使用以下函数设置文本的颜色:
TFTscreen.stroke(`B`, `G`, `R`);
其中,B、G和R分别是你设置的蓝色、绿色和红色的对应值。
最后,使用以下函数将文本写入屏幕:
TFTscreen.text("Hello, world!", `x`, `y`);
这将显示文本“Hello, world!”,并将文本的左上角定位在 LCD 的x,y位置。
这对于静态文本非常有效。然而,如果你想显示一个数字变量,你需要做更多的工作。该变量需要从数字类型转换为字符数组,数组的大小应与可能的最大值匹配。例如,如果你正在读取 Arduino 的模拟输入 0 并想显示该值,可以使用以下代码:
char analogZero[4];
然后在绘图过程中,在将模拟值发送到 LCD 之前,将值转换为字符串,如下所示:
String sensorVal = String(analogRead(A0));
这个字符串会被转换并插入到字符数组中:
sensorVal.toCharArray(analogZero, 4);
最后,为了在 LCD 上显示值,我们可以像往常一样使用.text()命令:
TFTscreen.text(`analogZero`, `x`, `y`);
其中,analogZero的值将在x,y位置显示,文本的左上角位置为x,y。
现在我们已经了解了所有用于在 LCD 上显示文本的命令,让我们在下一个项目中实际应用它们。
项目#29:实际演示文本功能
在这个项目中,你将使 LCD 显示五种文本大小以及从 Arduino 模拟输入 0 读取的数字值。
绘图
按照表 9-1 的描述连接你的 LCD,然后上传以下绘图:
// Project 29 - Seeing the Text Functions in Action
#include <TFT.h> // Arduino TFT LCD library
#include <SPI.h> // SPI bus library
TFT TFTscreen = TFT(10, 9, 8); // allocate digital pins to LCD
char analogZero[4];
void setup()
{ TFTscreen.begin(); // activate LCD TFTscreen.background(0, 0, 0); // set display to black
}
void loop()
{ TFTscreen.stroke(255, 255, 255); // white text TFTscreen.setTextSize(1); TFTscreen.text("Size One", 0, 0); TFTscreen.setTextSize(2); TFTscreen.text("Size Two", 0, 10); TFTscreen.setTextSize(3); TFTscreen.text("Size 3", 0, 30); TFTscreen.setTextSize(4); TFTscreen.text("Size 4", 0, 55); delay(2000); TFTscreen.background(0, 0, 0); // set display to black TFTscreen.setTextSize(5); TFTscreen.text("Five", 0, 0); delay(2000); TFTscreen.background(0, 0, 0); // set display to black TFTscreen.stroke(255, 255, 255); // white text TFTscreen.setTextSize(1); TFTscreen.text("Sensor Value :\n ", 0, 0); TFTscreen.setTextSize(3); String sensorVal = String(analogRead(A0)); // convert the reading to a char array sensorVal.toCharArray(analogZero, 4); TFTscreen.text(analogZero, 0, 20); delay(2000); TFTscreen.background(0, 0, 0); // set display to black
}
运行绘图
你应该看到五种文本大小显示在 LCD 的两个屏幕上。然后你应该看到一个第三个屏幕,显示来自模拟输入 0 的值,如图 9-11 所示。

图 9-11:在 TFT LCD 上显示的模拟输入值
使用图形函数创建更复杂的显示效果
现在让我们来看看我们可以使用的函数来创建各种显示效果。请记住,图形 LCD 屏幕的分辨率是 160 列和 128 像素,但当我们在草图中的函数中引用这些列和像素时,它们是从 0 到 159 横向计数,0 到 127 纵向计数。此外,和之前的文本示例一样,我们仍然需要使用“在草图中使用字符 LCD”一节中提到的五行代码来初始化显示。
有许多不同的函数可以让你在显示屏上显示点(单个像素)、线条、矩形和圆形。根据项目需求,再加上一些想象力,可以创造出色且实用的显示效果。我们现在将介绍这些函数,之后你可以通过示例草图看到它们的实际应用。
在绘制任何对象之前,你需要定义它的颜色。可以使用
TFTscreen.stroke(`B`, `G`, `R`);
B、G和R分别是蓝色、绿色和红色颜色级别的对应值。
要在显示屏上绘制一个单独的点,我们使用
TFTscreen.point(`X`, `Y`);
X和Y是点的水平和垂直坐标。对于我们的 LCD,X的范围是 0 到 159,Y的范围是 0 到 127。
要从一个点绘制到另一个点,我们使用
TFTscreen.line(`X1`, `Y1`, `X2`, `Y2`);
X1和Y1是起始点的坐标,X2和Y2是线段的结束坐标。
要绘制一个圆,我们使用
TFTscreen.circle(`X`, `Y`, `R`);
X和Y是圆心的坐标,R是圆的半径,单位为像素。如果你希望填充圆形(或稍后会描述的矩形)以颜色,而不仅仅是绘制轮廓,可以在circle()函数前加上
TFTscreen.fill(`B`, `G`, `R`);
B、G和R分别是蓝色、绿色和红色填充级别的对应值。请注意,填充颜色不会改变形状的轮廓,所以你仍然需要在形状函数前加上stroke()函数。
如果你希望绘制多个填充项,你只需使用一次fill()命令。如果你随后希望关闭填充并仅返回轮廓,可以使用以下命令:
TFTscreen.noFill();
最后,你可以使用以下函数绘制矩形:
TFTscreen.rect(`X1`, `Y1`, `X2`, `Y2`);
X1、Y1是矩形左上角的坐标,X2、Y2是矩形右下角的坐标。
项目 #30:图形功能的实际展示
现在我们已经了解了所有用于 LCD 图形功能的命令,接下来让我们在这个项目中实际运用它们。
草图
按照表 9-1 中的说明连接你的 LCD,然后上传以下草图:
// Project 30 - Seeing the Graphic Functions in Action
#include <TFT.h> // Arduino TFT LCD library
#include <SPI.h> // SPI bus library
TFT TFTscreen = TFT(10, 9, 8); // allocate digital pins to LCD
int a;
void setup()
{ TFTscreen.begin(); // activate LCD TFTscreen.background(0, 0, 0); // set display to black randomSeed(analogRead(0)); // for random numbers
}
void loop()
{ // random dots for (a = 0; a < 100; a++) { TFTscreen.stroke(random(256), random(256), random(256)); TFTscreen.point(random(160), random(120)); delay(10); } delay(1000); TFTscreen.background(0, 0, 0); // set display to black // random lines for (a = 0; a < 100; a++) { TFTscreen.stroke(random(256), random(256), random(256)); TFTscreen.line(random(160), random(120), random(160), random(120)); delay(10); } delay(1000); TFTscreen.background(0, 0, 0); // set display to black // random circles for (a = 0; a < 50; a++) { TFTscreen.stroke(random(256), random(256), random(256)); TFTscreen.circle(random(160), random(120), random(50)); delay(10); } delay(1000); TFTscreen.background(0, 0, 0); // set display to black // random filled circles for (a = 0; a < 50; a++) { TFTscreen.fill(random(256), random(256), random(256)); TFTscreen.stroke(random(256), random(256), random(256)); TFTscreen.circle(random(160), random(120), random(50)); delay(10); } delay(1000); TFTscreen.background(0, 0, 0); // set display to black // random rectangles TFTscreen.noFill(); for (a = 0; a < 50; a++) { TFTscreen.stroke(random(256), random(256), random(256)); TFTscreen.rect(random(160), random(120), random(160), random(120)); delay(10); } delay(1000); TFTscreen.background(0, 0, 0); // set display to black // random filled rectangles TFTscreen.noFill(); for (a = 0; a < 50; a++) { TFTscreen.fill(random(256), random(256), random(256)); TFTscreen.stroke(random(256), random(256), random(256)); TFTscreen.rect(random(160), random(120), random(160), random(120)); delay(10); } delay(1000); TFTscreen.background(0, 0, 0); // set display to black
}
上传草图后,显示屏将执行我们在本章中讨论的所有图形功能。例如,你应该能看到图 9-12 中展示的线条。
利用到目前为止讨论的函数和一些想象力,你可以创建各种显示效果或将数据以图形方式展示。在下一部分中,我们将基于我们的快速读取温度计项目,使用 LCD 屏幕和这些函数来扩展功能。

图 9-12:LCD 上的随机线条
项目 #31:创建温度历史监控器
在这个项目中,我们的目标是每隔 20 分钟测量一次温度,并以点图的形式显示最近 120 次读数。每个读数将表示为一个像素,纵轴为温度,横轴为时间。
最新的读数将出现在左侧,显示屏将不断地从左到右滚动显示读数。当前温度也会作为数字显示。
算法
尽管这个项目听起来可能很复杂,但实际上它非常简单,只需要两个函数。第一个函数从 TMP36 温度传感器获取温度读数,并将其存储在一个包含 120 个值的数组中。每当获取新的读数时,前 119 个值会向数组下方移动,为新读数腾出位置,并删除最旧的读数。
第二个函数用于在 LCD 屏幕上绘制图形。它显示当前温度、图表的刻度以及每个像素的位置,用于展示温度数据随时间变化的情况。
硬件
下面是你创建这个项目所需的内容:
-
一个 160 × 128 像素的 ST7735 TFT LCD 模块,如本章所使用的。
-
一个 TMP36 温度传感器
-
各种连接线
-
一块面包板
-
Arduino 和 USB 线
按照表 9-1 的描述连接图形 LCD,并将 TMP36 传感器连接到 5V、模拟 5 和 GND,正如你在第六章的项目 20 中所做的那样。
草图
我们的草图结合了第六章中用于测量温度的代码,以及本章前面描述的图形函数。输入并上传以下草图,其中包含有关所使用函数的相关注释:
// Project 31 - Creating a Temperature History Monitor
#include <TFT.h> // Arduino TFT LCD library
#include <SPI.h> // SPI bus library
TFT TFTscreen = TFT(10, 9, 8);
// allocate digital pins to LCD
int tcurrent = 0;
int tempArray[120];
char currentString[3];
void getTemp() // function to read temperature from TMP36
{ float sum = 0; float voltage = 0; float sensor = 0; float celsius; // read the temperature sensor and convert the result to degrees C sensor = analogRead(5); voltage = (sensor * 5000) / 1024; voltage = voltage - 500; celsius = voltage / 10; tcurrent = int(celsius); // insert the new temperature at the start of the array of past temperatures for (int a = 119 ; a >= 0 ; --a ) { tempArray[a] = tempArray[a - 1]; } tempArray[0] = tcurrent;
}
void drawScreen() // generate TFT LCD display effects{ int q; // display current temperature TFTscreen.background(0, 0, 0); // clear screen to black TFTscreen.stroke(255, 255, 255); // white text TFTscreen.setTextSize(2); TFTscreen.text("Current:", 20, 0); String tempString = String(tcurrent); tempString.toCharArray(currentString, 3); TFTscreen.text(currentString, 115, 0); // draw scale for graph TFTscreen.setTextSize(1); TFTscreen.text("50", 0, 20); TFTscreen.text("45", 0, 30); TFTscreen.text("40", 0, 40); TFTscreen.text("35", 0, 50); TFTscreen.text("30", 0, 60); TFTscreen.text("25", 0, 70); TFTscreen.text("20", 0, 80); TFTscreen.text("15", 0, 90); TFTscreen.text("10", 0, 100); TFTscreen.text(" 5", 0, 110); TFTscreen.text(" 0", 0, 120); TFTscreen.line(20, 20, 20, 127); // plot temperature data points for (int a = 25 ; a < 145 ; a++) { // convert the temperature value to a suitable y-axis position on the LCD q = (123 - (tempArray[a - 25] * 2)); TFTscreen.point(a, q); }
}
void setup()
{ TFTscreen.begin(); // activate LCD TFTscreen.background(0, 0, 0); // set display to black
}
void loop()
{ getTemp(); drawScreen(); for (int a = 0 ; a < 20 ; a++) // wait 20 minutes until the next reading { delay(60000); // wait 1 minute }
}
运行草图
最终的显示效果应该类似于图 9-13。

图 9-13:项目 31 的结果
修改草图
不同的人在看到不同的视觉格式呈现数据时,能更好地理解数据。出于这个原因,你可能希望改为创建一个条形图,垂直线表示不同的数值。
这种类型的项目也可以用来显示其他类型的数据,例如通过模拟输入引脚测量的各种传感器的电压。或者你可以添加另一个温度传感器,显示两个传感器的值。几乎任何返回值的东西都可以使用图形 LCD 模块进行显示。
展望未来
现在你已经有了使用 LCD 的经验,你可以看到 Arduino 实际上是一个小型计算机:它可以接收并处理输入数据,并将其显示到外部世界。但这仅仅是开始。在下一章中,你将更加深入地研究库,学习编写自己的库,然后在之前的项目中使用你新创建的库与温度传感器一起工作。
第十章:创建你自己的 Arduino 库
在本章中你将
-
了解 Arduino 库的组成部分
-
为重复任务创建一个简单的库
-
学习如何在 Arduino IDE 中安装库
-
创建一个接受值并执行功能的库
-
创建一个处理传感器数据并以易于使用的形式返回值的库
回想一下第七章中描述的第 22 个项目,你安装了一个 Arduino 库,该库包括将数据保存到 SD 卡所需的函数。使用这个库减少了编写草图所需的时间,因为该库提供了与卡模块相关的函数。
将来,当你编写草图来解决自己的问题和执行自己的任务时,你可能会发现自己重复使用某些已创建的函数。此时,创建自己的 Arduino 库将是明智之举,你可以轻松地在草图中安装和使用它。
在本章中,你将学习如何将函数转换为 Arduino 库。通过这里提供的示例,你将了解创建自定义库所需的知识。现在让我们开始吧。
创建你的第一个 Arduino 库
在我们的第一个示例中,参考清单 10-1。它包含两个函数,blinkSlow() 和 blinkFast(),分别用于以慢速或快速的速度闪烁 Arduino 的板载 LED。
// Listing 10-1
void setup()
{ pinMode(13, OUTPUT); // using onboard LED
}
void blinkSlow()
{ for (int i = 0; i < 5; i++) { digitalWrite(13, HIGH); delay(1000); digitalWrite(13, LOW); delay(1000); }
}
void blinkFast()
{ for (int i = 0; i < 5; i++) { digitalWrite(13, HIGH); delay(250); digitalWrite(13, LOW); delay(250); }
}
void loop()
{ blinkSlow(); delay(1000); blinkFast(); delay(1000);
}
清单 10-1:闪烁 Arduino 板载 LED
如果没有库,每次你编写新的草图并想使用 blinkSlow() 和 blinkFast() 函数时,都必须手动输入它们。另一方面,如果你将这些函数的代码放入一个库,从那时起,你只需要在草图的开始处调用该库,只用一行代码。
Arduino 库的构成
一个 Arduino 库由三个文件组成,此外还可以包含一些可选的示例草图,展示如何使用该库。每个 Arduino 库必备的三个文件如下:
-
.h 头文件 -
.cpp 源文件 -
KEYWORDS.TXT 关键字定义
在前两个文件名中,你将用库的实际名称替换
头文件
blinko.h 文件被称为头文件,因为它包含了库内部使用的函数、变量等的定义。blinko 库的头文件见于清单 10-2。
// Listing 10-2
/*1 blinko.h - Library for flashing an Arduino's onboard LED connected to D13
*/2 #ifndef blinko_h
#define blinko_h3 #include "Arduino.h" // gives library access to standard types and constants // of the Arduino language4 class blinko // functions and variables used in the library
{ public: blinko(); void slow(); void fast();
};5 #endif
清单 10-2:blinko 库头文件
头文件与典型的 Arduino 草图有一些相似之处,但也有一些区别。在第 1 行,有一个关于库用途的有用注释。虽然这样的注释不是必需的,但它们应该包含在内,以便让别人更容易使用这个库。
在第 2 行,代码检查库是否已经在主草图中声明。在第 3 行,包含了标准 Arduino 库,以便我们的 blinko 库可以访问标准 Arduino 草图函数、类型和常量。
然后,在第 4 行,我们创建了一个类。你可以将 类 理解为一个地方,集中包含了库所需的所有变量和函数,包括库的名称。在类中,可以有公共变量和函数,草图需要使用库时可以访问这些;也可以有私有变量和函数,这些只能在类内部使用。最后,每个类都有一个与类同名的 构造函数,用于创建类的实例。这个概念可能听起来有点复杂。然而,在本章中的示例进行复习并自己制作几个库后,你会对这些结构有信心。
在我们的类中,你可以看到我们有库的构造函数 blinko() 和两个库中的函数:slow() 和 fast()。它们位于 public: 声明之后,意味着任何访问 blinko 库的用户(“任何公众成员”)都可以使用这些函数。
最后,在第 5 行,我们结束了头文件的定义。通过将头文件定义包裹在 if 语句中,我们确保头文件不会被加载两次。
源文件
接下来,让我们看一下 blinko.cpp 文件。.cpp 文件被称为 源文件,因为它包含了在使用库时会运行的代码。blinko 库的源文件见 清单 10-3。
// Listing 10-3
/*1 blinko.cpp - Library for flashing an Arduino's onboard LED connected to D13
*/2 #include “Arduino.h” // gives library access to standard types and constants // of the Arduino language
#include "blinko.h"3 blinko::blinko() // things to do when library is activated
{
pinMode(13, OUTPUT);
}4 void blinko::slow()
{
for (int i=0; i<5; i++)
{ digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
}
}4 void blinko::fast()
{
for (int i=0; i<5; i++)
{
digitalWrite(13, HIGH); delay(250); digitalWrite(13, LOW);
delay(250);
}
}
清单 10-3:blinko 库源文件
源文件包含了我们编写的函数,这些函数我们希望可以重复使用。此外,这里还需要一些新的结构元素。在第 2 行,我们让库访问标准的 Arduino 函数、类型和常量,以及我们自己的库头文件。
在第 3 行,我们有构造函数的定义。构造函数包含了在使用库时应该发生的事情。在我们的例子中,我们已将数字引脚 13 设置为输出,因为我们正在使用 Arduino 的板载 LED。
从第 4 行开始,我们列出了想要包含在此库中的函数。它们就像你在独立的草图中创建的函数,只不过有一个重要的区别:它们的定义以库类名和两个冒号开头。例如,不是输入 void fast(),而是输入 void blinko::fast()。
KEYWORDS.TXT 文件
最后,我们需要创建 KEYWORDS.TXT 文件。Arduino IDE 使用该文件来确定库中的关键字,并在 IDE 中高亮显示这些词。清单 10-4 是我们 blinko 库的 KEYWORDS.TXT 文件。
// Listing 10-4
blinko KEYWORD1
slow KEYWORD2
fast KEYWORD2
清单 10-4:blinko 库关键字文件
第一行是库的名称,称为KEYWORD1。库的函数被称为KEYWORD2。请注意,关键字与其定义之间的空格必须通过按下 Tab 键而非空格键来创建。
此时,你已经拥有了工作库所需的三个文件。包含一个示例草图是个不错的主意,这样用户可以理解函数的功能。列表 10-5 是我们的 blinko 库的示例草图。
// Listing 10-5, blinkotest.ino1 #include <blinko.h>2 blinko ArduinoLED;
void setup() {
}
void loop(){3 ArduinoLED.slow(); // blink LED slowly, once every second delay(1000);4 ArduinoLED.fast(); // blink LED rapidly, four times per second delay(1000);
}
列表 10-5:我们的 blinko 库的示例草图
如你所见,草图是基础的。它仅展示了我们库中slow()和fast()两个函数的使用。最终用户在安装库之后所需做的,就是包含库 1,创建实例 2,然后在需要时调用这两个函数,如 3 和 4 所示。
安装你的新 Arduino 库
现在你已经创建了一个新的 Arduino 库,一个简便的存储和分发方式是创建一个 ZIP 文件。未来的用户获得 ZIP 文件后,可以像第七章中演示的那样轻松安装库。
使用 Windows 7 及更高版本创建 ZIP 文件
要使用 Windows 创建 ZIP 文件,请按照以下说明操作。
首先,将三个库文件和示例草图(存储在自己的文件夹中,就像所有草图一样)放置在同一个位置。图 10-1 展示了一个示例。

图 10-1:我们的 Arduino 库文件在一个文件夹中
选择所有文件,右键点击高亮显示的文件中的任意位置,选择发送到▶压缩(ZIP)文件夹,如图 10-2 所示。
文件夹中会出现一个新文件,扩展名为.zip,且启用了文件名编辑。对于我们的库,将名称更改为blinko,然后按回车键,如图 10-3 所示。
现在,你可以继续阅读第 193 页的“安装你的新库”部分。

图 10-2:压缩库文件

图 10-3:更改库 ZIP 文件名称
使用 Mac OS X 或更高版本创建 ZIP 文件
要使用 Mac OS X 创建 ZIP 文件,请将三个库文件和示例草图(存储在自己的文件夹中,就像所有草图一样)放置在同一个位置。图 10-4 展示了一个示例。

图 10-4:我们的 Arduino 库文件
选择所有文件,右键点击文件中的任意位置,选择压缩 4 个项目,如图 10-5 所示。

图 10-5:压缩库文件
一会儿后,名为Archive.zip的新文件将出现在文件夹中,如图 10-6 所示。

图 10-6:文件已经被压缩。
点击Archive.zip文件夹并将其名称更改为blinko.zip,如图 10-7 所示。

图 10-7:我们的 Arduino 库安装 ZIP 文件
现在,您已经拥有一个库 ZIP 文件,您可以轻松地将其分发给他人或自己安装。
安装您的新库
目前,您可以使用 ZIP 文件方法安装您的库,该方法在第七章第 134 页的《下载 Arduino 库作为 ZIP 文件》一节中有详细介绍。安装文件后,重新启动 Arduino IDE,选择草图▶包含库,即可看到您的库列出,如图 10-8 所示。

图 10-8:我们的 Arduino 库,现在可以在 IDE 中使用
此外,您现在可以轻松访问示例草图;选择文件▶示例▶blinko,如图 10-9 所示。

图 10-9:我们的 Arduino 库示例草图已经安装。
创建一个接受值以执行功能的库
现在,您已经掌握了创建基本 Arduino 库的知识,可以进入下一个层次:创建一个能够接受值并对此进行操作的库。我们将再次查看草图中的示例函数,并将其转换为一个更有用的库。
请参考 Listing 10-6 中的草图。它使用了函数void blinkType(),该函数告诉 Arduino 需要多少次闪烁板载 LED,并设置开/关周期。
// Listing 10-6
void setup() { pinMode(13, OUTPUT); // use onboard LED
}
void blinkType(int blinks, int duration)
// blinks - number of times to blink the LED
// duration – blink duration in milliseconds
{ for (int i = 0; i < blinks; i++) { digitalWrite(13, HIGH); delay(duration); digitalWrite(13, LOW); delay(duration); }
}
void loop()
{ // blink LED 10 times, with 250 ms duration blinkType(10, 250); delay(1000); // blink LED three times, with 1 second duration blinkType(3, 1000); delay(1000);
}
Listing 10-6:blinkType()函数的演示草图
如您所见,函数void blinkType()接受两个值,并据此执行操作。第一个值是打开和关闭板载 LED 的次数,第二个值是每次闪烁的延迟时间(以毫秒为单位)。
让我们将这个函数转换为一个名为 blinko2 的 Arduino 库。Listing 10-7 显示了这个库的头文件。
// Listing 10-7
/* blinko2.h - Blinking the Arduino's onboard LED on D13 Accepts number of blinks and on/off delay
*/
#ifndef blinko2_h#define blinko2_h
#include "Arduino.h"
class blinko2
{ public: blinko2(); void blinkType(int blinks, int duration); 1 private: int blinks; int duration;
};
#endif
Listing 10-7:blinko2 库的头文件
头文件与原始 blinko 库的头文件结构相同。然而,在第 1 行有一个新的部分叫做private。在private部分声明的变量仅供库内部使用,不能被更大的 Arduino 草图使用。您可以在 Listing 10-8 所示的库源文件中看到这些变量的使用。
// Listing 10-8
/* blinko2.cpp - Blinking the Arduino's onboard LED on D13 Accepts number of blinks and on/off delay
*/
#include "Arduino.h"
#include "blinko2.h"
blinko2::blinko2()
{1 pinMode(13, OUTPUT);
}2 void blinko2::blinkType(3int blinks, 4int duration)
{ for (int i=0; i<blinks; i++) { digitalWrite(13, HIGH); delay(duration); digitalWrite(13, LOW); delay(duration); }
}
Listing 10-8:blinko2 库源文件
blinko2 的源文件保持与原始 blinko 库源文件相同的结构。
我们将数字引脚 13 设置为输出(见 1)。在 2 处,我们声明了函数blinkType(),它接受三次闪烁的次数(见 3)和延迟时间(见 4)。您可以通过我们库的示例草图在 Listing 10-9 中看到这一操作。
// Listing 10-9
#include <blinko2.h>
blinko2 ArduinoLED;
void setup() {}
void loop()
{ ArduinoLED.blinkType(3,250);
// blink LED three times, with a duration of 250 ms delay(1000); ArduinoLED.blinkType(10,1000);
// blink LED 10 times, with a duration of 1 second delay(1000);
}
Listing 10-9:我们 blinko2 库的示例草图
接下来,我们需要为新的 blinko2 库创建关键词文件。不要忘记在单词之间使用制表符,而不是空格。以下是我们的KEYWORDS.TXT文件:
blinko2 KEYWORD1
blinkType KEYWORD2
现在创建你的 ZIP 文件,并使用本章前面描述的方法安装库。然后打开并运行 blinko2 示例草图,体验它的工作方式。
创建一个处理和显示传感器值的库
在我们的 Arduino 库最终示例中,我们将重新回顾多个早期项目中使用的模拟设备 TMP36 温度传感器。我们的 ArduinoTMP36 示例库将从 TMP36 获取原始值,并通过串口监视器显示摄氏度和华氏度的温度。
首先,通过连接你的 TMP36 与 Arduino,按照图 10-10 中的示意图进行连接。

图 10-10:ArduinoTMP36 库使用示意图
清单 10-10 是我们希望将其转换为库的一个草图。它使用两个函数,readC()和readF(),从 TMP36 传感器通过模拟引脚 0 获取原始读数,将其转换为摄氏度和华氏度,并返回结果。
// Listing 10-10
// display temperature from TMP36 sensor in C and F
float temperature;
float readC()
{ float tempC; tempC = analogRead(0); tempC = tempC = (tempC * 5000) / 1024; tempC = tempC - 500; tempC = tempC / 10; return tempC;
}
float readF()
{ float tempC; float tempF; tempC = analogRead(0); tempC = tempC = (tempC * 5000) / 1024; tempC = tempC - 500; tempC = tempC / 10; tempF = (tempC * 1.8) + 32; return tempF;
}
void setup()
{ Serial.begin(9600);
}
void loop()
{ Serial.print("Temperature in Celsius is: "); temperature = readC(); Serial.println(temperature); Serial.print("Temperature in Fahrenheit is: "); temperature = readF(); Serial.println(temperature); delay(1000);
}
清单 10-10:TMP36 演示草图
温度转换函数是包括在库中的理想候选项,我们将其命名为 ArduinoTMP36。头文件如清单 10-11 所示。
// Listing 10-111 #ifndef ArduinoTMP36_h
#define ArduinoTMP36_h
#include "Arduino.h"
class ArduinoTMP36
{2 public: ArduinoTMP36(); float readC(); float readF();3 private: float tempC; float tempF;
};
#endif
清单 10-11:ArduinoTMP36 库头文件
此时,你可能已经认识到头文件的结构。我们在第一部分设置了定义。在第二部分的class内部,我们声明了公共项,包括构造函数以及readC()和readF()函数。我们还在第三部分声明了私有项;这些包括库中使用的两个变量。
接下来是库源文件,见清单 10-12。
// Listing 10-12
#include "Arduino.h"
#include "ArduinoTMP36.h"
ArduinoTMP36::ArduinoTMP36()
{
}
float ArduinoTMP36::readC()
{ float tempC; tempC = analogRead(0); tempC = tempC=(tempC*5000)/1024; tempC = tempC-500; tempC = tempC/10; return tempC;
}
float ArduinoTMP36::readF()
{ float tempC; float tempF; tempC = analogRead(0); tempC = tempC=(tempC*5000)/1024; tempC = tempC-500; tempC = tempC/10; tempF = (tempC*1.8)+32; return tempF;
}
清单 10-12:ArduinoTMP36 库源文件
源文件包含用于计算温度的两个函数。它们被定义为float类型,因为它们返回浮动值。温度是使用与第四章第 8 个项目相同的公式计算的。
最后,我们需要为新的 ArduinoTMP36 库创建关键词文件。不要忘记在单词之间使用制表符,而不是空格。我们的KEYWORDS.TXT文件如下所示:
ArduinoTMP36 KEYWORD1
readC KEYWORD2
readF KEYWORD2
现在创建你的 ZIP 文件,并使用本章前面描述的方法安装库。然后打开并运行 ArduinoTMP36 示例草图,见清单 10-13。
// Listing 10-131 #include <ArduinoTMP36.h>
ArduinoTMP36 thermometer;2 float temperature;
void setup()
{ Serial.begin(9600);
}
void loop()
{ Serial.print("Temperature in Celsius is: ");3 temperature=thermometer.readC(); Serial.println(temperature); Serial.print("Temperature in Fahrenheit is: ");4 temperature=thermometer.readF(); Serial.println(temperature); delay(1000);
}
清单 10-13:ArduinoTMP36 库示例草图
只需包含库并在第 1 步创建实例。然后,在第 2 步声明一个变量来接收来自库的输出。之后,温度会在第 3 和第 4 步分别以摄氏度和华氏度返回。
打开串口监视器窗口,并将数据传输速度设置为 9600 波特率,你应该会看到一个滚动更新的当前温度列表,显示的温度单位为摄氏度和华氏度,如图 10-11 所示。

图 10-11:ArduinoTMP36 库输出示例
现在你可以体会到,通过使用库而不是每次创建新草图时都包含函数,节省了多少时间和草图大小。
展望未来
既然你已经有了编写 Arduino 库的经验,你可以创建自己的库。这将帮助你更深入地理解其他来源提供的库。你也可以通过为你已经完成的书中的项目创建库来进行练习。
在下一章,你将学习如何处理通过数字键盘输入的用户输入,所以翻到下一页开始吧。
第十一章:数字键盘
在本章中,你将
-
学习如何将数字键盘连接到你的 Arduino
-
从键盘读取值并在草图中使用
-
使用
switch case语句扩展决策系统 -
创建一个 PIN 码控制的锁或开关
使用数字键盘
随着你的项目变得更加复杂,当 Arduino 未连接到带键盘的设备时,你可能需要接受用户的数字输入。例如,你可能希望通过输入一个秘密数字来开启或关闭某个设备。一种选择是将 10 个或更多的按钮接入不同的数字输入引脚(对应数字 0 到 9),但使用一个数字键盘要更简单,就像图 11-1 所示。

图 11-1:数字键盘
使用键盘的好处之一是,它只需使用 8 个引脚就能支持 16 个活动按钮,并且借助一个聪明的 Arduino 库,你无需像在第四章那样为去抖动添加下拉电阻。
此时,你需要下载并安装 Arduino 键盘库,该库可从github.com/Chris--A/Keypad/archive/master.zip获取。
键盘接线
将键盘接线到 Arduino 上很简单。将键盘正面朝上,看看带状电缆的末端。你会看到一排八个女性连接器,如图 11-2 所示。

图 11-2:键盘连接器
从左到右读取插座,编号从 8 到 1。对于本书中的所有键盘项目,你将按照表 11-1 中所示,将键盘引脚插入 Arduino 引脚。
表 11-1:键盘与 Arduino 连接
| 键盘引脚编号 | Arduino 引脚 |
|---|---|
| 8 | 数字 9 |
| 7 | 数字 8 |
| 6 | 数字 7 |
| 5 | 数字 6 |
| 4 | 数字 5 |
| 3 | 数字 4 |
| 2 | 数字 3 |
| 1 | 数字 2 |
为键盘编程
当你为键盘编写草图时,必须包括一些代码行来启用键盘,这些代码在清单 11-1 中有说明。所需的代码从第 1 行开始,到第 5 行结束。
// Listing 11-11 // Beginning of keypad configuration code
#include <Keypad.h>
const byte ROWS = 4; // set display to four rows
const byte COLS = 4; // set display to four columns2 char keys[ROWS][COLS] = { {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'}
};3 byte rowPins[ROWS] = {9, 8, 7, 6}; 4 byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );5 // End of keypad configuration code
void setup()
{ Serial.begin(9600);
}
void loop(){ char key = keypad.getKey(); if (key){ Serial.print(key); }
}
清单 11-1:数字键盘示范草图
在第 2 行,我们介绍了 keys,这是一个字符变量数组,包含一个或多个可以通过计算机键盘生成的字母、数字或符号。在这个例子中,它包含了你的 Arduino 可以从键盘接收到的数字和符号。
第 3 行和第 4 行的代码定义了在 Arduino 上使用的数字引脚。通过这些代码和表 11-1,如果你想改变用于输入的数字引脚,可以进行修改。
测试草图
上传草图后,打开串口监视器并按下键盘上的一些按键。你按下的按键字符将显示在串口监视器中,如图 11-3 所示。

图 11-3:按下键盘上的按键后的结果
使用switch case做决策
当你需要将两个或更多变量与另一个值进行比较时,使用switch case语句通常会比使用if then语句更容易且更简洁,因为switch case语句可以进行任意次数的比较,并在比较为真时执行代码。例如,如果我们有一个整数变量xx,其可能的值为1、2或3,并且我们希望根据值为1、2或3来运行不同的代码,我们可以使用以下代码来替换if then语句:
switch(xx)
{ case 1: // do something when the value of xx is 1 break; // finish and move on with sketch case 2: // do something when the value of xx is 2 break; case 3: // do something when the value of xx is 3 break; default: // do something if xx is not 1, 2 or 3 // default is optional
}
这个代码段末尾的可选default:部分允许你选择在switch case语句中没有有效比较时运行某些代码。
项目 #32:创建一个键盘控制的锁
在这个项目中,我们将开始创建一个键盘控制的锁。我们将使用清单 11-1 中描述的基本设置,但还会包含一个六位数的秘密代码,用户需要在键盘上输入。串口监视器会告诉用户他们输入的代码是否正确。
秘密代码存储在草图中,但不会显示给用户。根据输入的代码(PIN)是否正确,草图将调用不同的函数。为了激活和解除激活锁,用户必须按下*,然后输入秘密号码,再按#。
草图
输入并上传此草图:
// Project 32 - Creating a Keypad-Controlled Lock
// Beginning of necessary code
#include <Keypad.h>
const byte ROWS = 4; // set display to four rows
const byte COLS = 4; // set display to four columns
char keys[ROWS][COLS] = { {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'}
};
byte rowPins[ROWS] = {9, 8, 7, 6};
byte colPins[COLS] = {5, 4, 3, 2};
Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );
// End of necessary code1 char PIN[6]={'1','2','3','4','5','6'}; // our secret number
char attempt[6]={0,0,0,0,0,0};
int z=0;
void setup(){ Serial.begin(9600);
}
void correctPIN() // do this if the correct PIN is entered
{ Serial.println("Correct PIN entered...");
}
void incorrectPIN() // do this if an incorrect PIN is entered
{ Serial.println("Incorrect PIN entered!");
}
void checkPIN()
{ int correct=0; 2 for (int i = 0; i < 6 ; i++ ) {
// Goes step-by-step through the 6-character array.
// If each char matches each char in the PIN, increments the
// counter.
if (attempt[i]==PIN[i]) { correct++; } } if (correct==6) {3 correctPIN(); } else {4 incorrectPIN(); } for (int i=0; i<6; i++) // removes previously entered code attempt { attempt[i]=0; }
}
void readKeypad()
{ char key = keypad.getKey(); if (key != NO_KEY) {5 switch(key) { case '*': z=0; break; case '#': delay(100); // removes the possibility of switch bounce checkPIN(); break; default: attempt[z]=key; z++; } }
}
void loop()
{6 readKeypad();
}
理解草图
在完成常规的初始化例程后(如清单 11-1 所示),草图会不断地“监听”键盘,通过运行第 6 行的readKeypad()函数来实现。按下按键后,Arduino 会使用第 5 行的switch case语句检查按下的按键值。Arduino 会将按下的按键值存储在attempt数组中,当用户按下#时,Arduino 会调用checkPIN()函数。
在第 2 行,Arduino 将按键值与存储在PIN数组中的 PIN 值进行比较。如果输入的序列正确,函数correctPIN()将在第 3 行被调用,你可以在其中添加自己的代码进行执行。如果输入的序列错误,则调用第 4 行的incorrectPIN()函数。最后,一旦用户的输入被检查完毕,代码会从内存中删除输入值,以便为下一个测试做好准备。
测试草图
在将草图上传到 Arduino 后,打开串口监视器窗口,按下数字键盘上的星号(*),输入秘密号码,然后按下井号(#)。尝试输入正确和错误的数字。你的结果应类似于图 11-4 中显示的输出。
这个示例为你自己的 PIN 激活设备(如锁、警报或任何你能想象的设备)提供了一个完美的基础。只需确保在correctPIN()和incorrectPIN()中替换你希望在输入正确或错误的序列时执行的代码。

图 11-4:输入正确和错误的 PIN 后的结果
展望未来
你已经学会了另一种为你的 Arduino 收集输入的方式。你还掌握了创建一个有用的方法来通过数字键盘控制草图的基础知识,以及创建一个组合锁来访问你 Arduino 能控制的任何内容的基础知识。此外,你还学会了非常有用的switch case语句。在下一章中,你将学习另一种输入形式:触摸屏。
第十二章:接受触摸屏的用户输入
在本章中,你将
-
学习如何将电阻式触摸屏连接到 Arduino
-
探索触摸屏可以返回的值
-
创建一个简单的开关
-
学习如何使用
map()函数 -
创建一个带有调光控制的开关
如今,我们随处可见触摸屏:在智能手机、平板电脑甚至便携式游戏机上。那么为什么不使用触摸屏来接收 Arduino 用户的输入呢?
触摸屏
触摸屏可能相当昂贵,但我们将使用一款价格便宜的型号,来自 Adafruit(零件号 333 和 3575),最初为 Nintendo DS 游戏机设计。
这个触摸屏的尺寸大约是 2.45 x 3 英寸,如 图 12-1 所示。

图 12-1:安装在无焊接面包板上的触摸屏
注意右侧小电路板上连接的水平排线。这个 扩展板 用来将 Arduino 和面包板连接到触摸屏。扩展板附带的插针需要在使用前进行焊接。图 12-2 显示了扩展板的特写。

图 12-2:触摸屏扩展板
连接触摸屏
按照 表 12-1 中的说明将触摸屏扩展板连接到 Arduino。
表 12-1:触摸屏扩展板连接
| 扩展板引脚 | Arduino 引脚 |
|---|---|
| X− | A3 |
| Y+ | A2 |
| X+ | A1 |
| Y− | A0 |
项目 #33:在触摸屏上处理区域
触摸屏有两层电阻涂层,位于上层塑料膜和下层玻璃之间。一层涂层作为 x 轴,另一层作为 y 轴。当电流通过每层涂层时,涂层的电阻会根据触摸位置的不同而变化;通过测量电流,可以确定触摸区域的 x 和 y 坐标。
在这个项目中,我们将使用 Arduino 来记录触摸屏上的触摸位置。我们还将把触摸信息转换为表示屏幕区域的整数。
硬件
以下硬件是必需的:
-
一块 Adafruit 触摸屏,零件号 333
-
一块 Adafruit 扩展板,零件号 3575
-
公对公跳线
-
一个无焊接面包板
-
Arduino 和 USB 电缆
按照 表 12-1 中的说明连接触摸屏,并通过 USB 电缆将 Arduino 连接到 PC。
草图
输入并上传以下草图:
// Project 33 - Addressing Areas on the Touchscreen
int x,y = 0;1 int readX() // returns the value of the touchscreen's x-axis
{ int xr=0; pinMode(A0, INPUT); pinMode(A1, OUTPUT); pinMode(A2, INPUT); pinMode(A3, OUTPUT); digitalWrite(A1, LOW); // set A1 to GND digitalWrite(A3, HIGH); // set A3 as 5V delay(5); xr=analogRead(0); // stores the value of the x-axis return xr;
}2 int readY() // returns the value of the touchscreen's y-axis
{ int yr=0; pinMode(A0, OUTPUT); pinMode(A1, INPUT); pinMode(A2, OUTPUT); pinMode(A3, INPUT); digitalWrite(14, LOW); // set A0 to GND digitalWrite(16, HIGH); // set A2 as 5V delay(5); yr=analogRead(1); // stores the value of the y-axis return yr;
}
void setup()
{ Serial.begin(9600);
}
void loop()
{ Serial.print(" x = "); x=readX();3 Serial.print(x); y=readY(); Serial.print(" y = ");4 Serial.println(y); delay (200);
}
函数 readX() 和 readY() 在 1 和 2 位置读取触摸屏电阻层的电流,使用 analogRead() 测量并返回读取的值。该草图快速运行这两个函数,以提供触摸的屏幕区域的实时位置,并在 3 和 4 位置的串口监视器中显示这些信息。(每个函数中的 delay(5) 是必要的,以允许输入/输出引脚有时间改变其状态。)
测试草图
测试草图时,触摸屏幕并观察串口监视器窗口,注意当你在屏幕上移动手指时,x 和 y 的值是如何变化的。同时注意当屏幕未被触摸时显示的值,如 图 12-3 所示。

图 12-3:触摸屏未触摸时显示的值
你可以在草图中使用当未触摸屏幕时显示的值,来检测屏幕是否未被触摸。此外,显示可能会略有不同,因此映射出你自己设备的显示范围非常重要。
映射触摸屏
你可以通过触摸屏幕的每个角并记录返回的值,来绘制触摸屏每个角的坐标,如 图 12-4 所示。

图 12-4:触摸屏地图
创建了触摸屏地图后,你可以将其数学上划分为更小的区域,然后使用 if 语句根据触摸屏幕的位置来触发特定的操作。在项目 34 中我们会这么做。
项目 #34:创建一个双区开/关触摸开关
在这个项目中,我们将使用触摸屏地图来创建一个开/关开关。首先,像 图 12-5 所示,垂直地将触摸屏分成两半。
Arduino 将通过将触摸记录的坐标与屏幕每一半的边界进行比较,来确定触摸的是屏幕的哪个区域。当区域确定后,代码通过返回 on 或 off 来响应(尽管它也可以向设备发送开或关的信号)。

图 12-5:开/关开关地图
草图
输入并上传以下草图:
// Project 34 - Creating a Two-Zone On/Off Touch Switch
int x,y = 0;
void setup()
{ Serial.begin(9600); pinMode(10, OUTPUT);
}
void switchOn()
{ digitalWrite(10, HIGH); Serial.print("Turned ON at X = "); Serial.print(x); Serial.print(" Y = "); Serial.println(y); delay(200);
}
void switchOff()
{ digitalWrite(10, LOW); Serial.print("Turned OFF at X = "); Serial.print(x); Serial.print(" Y = "); Serial.println(y); delay(200);
}
int readX() // returns the value of the touchscreen's x-axis
{ int xr=0; pinMode(A0, INPUT); pinMode(A1, OUTPUT); pinMode(A2, INPUT); pinMode(A3, OUTPUT); digitalWrite(A1, LOW); // set A1 to GND digitalWrite(A3, HIGH); // set A3 as 5V delay(5); xr=analogRead(0); return xr;
}
int readY() // returns the value of the touchscreen's y-axis
{ int yr=0; pinMode(A0, OUTPUT); pinMode(A1, INPUT); pinMode(A2, OUTPUT); pinMode(A3, INPUT); digitalWrite(A0, LOW); // set A0 to GND digitalWrite(A2, HIGH); // set A2 as 5V delay(5); yr=analogRead(1); return yr;
}
void loop()
{ x=readX(); y=readY();1 // test for ON if (x<=515 && x>=80) { switchOn(); }2 // test for OFF if (x<950 && x>=516) { switchOff(); }
}
理解草图
在 void loop() 中使用的两个 if 语句检查屏幕左侧或右侧的触摸。如果左侧被触摸,则触摸被检测为“开”按下(1)。如果右侧被触摸(“关”按下),则触摸被检测为(2)。
测试草图
该草图的输出如 图 12-6 所示。开关的状态和坐标在每次触摸屏幕后显示。

图 12-6:项目 34 的输出
使用 map() 函数
有时你可能需要将一个整数从一个范围转换为另一个范围。例如,触摸屏的 x 值可能从 100 到 900,但你可能需要将其转换为 0 到 255 的范围,以控制一个 8 位输出。
为了做到这一点,我们使用 map() 函数,其布局如下:
map(`value`, `fromLow`, `fromHigh`, `toLow`, `toHigh`);
例如,要将触摸屏上的 450 转换到 0-255 范围,你可以使用以下代码:
x = map(450, 100, 900, 0, 255);
这将给x一个值 95。你将在项目 35 中使用 map() 函数。
项目 #35:创建三区触摸开关
在这个项目中,我们将为数字引脚 3 上的 LED 创建一个三区触摸开关,它可以控制 LED 的开关状态并通过 PWM 调节亮度,从 0 到 255(如第三章所述)。
触摸屏地图
我们的触摸屏地图如图 12-7 所示。

图 12-7:三区触摸开关的触摸屏地图
触摸屏地图分为关机区、开机区和亮度控制区。我们测量触摸屏返回的值,以确定哪个区域被触摸,然后作出相应反应。
草图
输入并上传以下草图:
// Project 35 - Creating a Three-Zone Touch Switch
int x,y = 0;
void setup()
{ pinMode(3, OUTPUT); Serial.begin(9600);
}
void switchOn()
{ digitalWrite(3, HIGH); delay(200);
}
void switchOff()
{ digitalWrite(3, LOW); delay(200);
}
void setBrightness(){ int PWMvalue;1 PWMvalue=map(x, 80, 950, 0, 255); analogWrite(3, PWMvalue);
}
int readX() // returns the value of x-axis
{ int xr=0; pinMode(A0, INPUT); pinMode(A1, OUTPUT); pinMode(A2, INPUT); pinMode(A3, OUTPUT); digitalWrite(A1, LOW); // set A1 to GND digitalWrite(A3, HIGH); // set A3 as 5V delay(5); xr=analogRead(0); return xr;
}
int readY() // returns the value of y-axis
{ int yr=0; pinMode(A0, OUTPUT); pinMode(A1, INPUT); pinMode(A2, OUTPUT); pinMode(A3, INPUT); digitalWrite(A0, LOW); // set A0 to GND digitalWrite(A2, HIGH); // set A2 as 5V delay(5); yr=analogRead(1); return yr;
}
void loop()
{ x=readX(); y=readY();2 // test for ON if (x<=950 && x>=515 && y>= 500 && y>900) { switchOn(); }3 // test for OFF if (x>80 && x<515 && y>= 500 && y>900) { switchOff(); } // test for brightness4 if (y>=100 && y<=500) { setBrightness(); } Serial.println(x);
}
理解草图
与两区地图的草图类似,这个草图将检查在开机区和关机区的触摸(现在更小,因为一半屏幕已分配给亮度控制区),分别在 2 和 3 处检查,同时在水平分隔线以上的任何触摸我们用来确定亮度,在 4 处检查。如果触摸屏触摸到了亮度区域,则使用 map() 函数在 1 处将 x 轴上的位置转换为相对值,用于 PWM 控制,LED 将根据 setBrightness()* 函数进行调节。
你可以使用这些相同的函数来创建任意数量的开关或滑块,使用这个简单且便宜的触摸屏。此外,你还可以创建自己的库,以便轻松返回 X 和 Y 值,并在未来写的任何草图中控制亮度。
展望未来
本章介绍了触摸屏,另一种接受用户数据并控制 Arduino 的方式。在下一章,我们将重点讲解 Arduino 板本身,了解一些不同版本的 Arduino,并在无焊接面包板上创建我们自己的版本。
第十三章:认识 Arduino 家族
在本章中,你将
-
学习如何在无焊面包板上构建自己的 Arduino 电路
-
探索各种兼容 Arduino 的板卡的特性和优势
-
了解开源硬件
我们将 Arduino 设计分解为一组零件,然后你将在无焊面包板上构建自己的 Arduino 电路。自己制作电路可以节省费用,特别是当你处理不断变化的项目和原型时。你还将了解一些新组件和电路。接着,我们将探索不需要额外硬件就能上传草图到你自制的 Arduino 的方法。最后,我们将考察 Arduino Uno 的一些常见替代方案,并探索它们之间的差异。
项目 #36:创建你自己的面包板 Arduino
随着你的项目和实验变得越来越复杂或数量增加,购买每个任务所需的 Arduino 板的成本可能会迅速失控,尤其是当你喜欢同时处理多个项目时。此时,将 Arduino 板的电路集成到你的项目中,通过在无焊面包板上构建 Arduino 电路,并根据你的特定项目需求进行扩展,会更加便宜和方便。将基本的 Arduino 电路复制到面包板上(如果不太过粗暴使用,面包板通常是可以重复使用的)所需的零件费用通常不会超过 10 美元。如果你的项目有很多外部电路,自己制作更为简便,因为这样可以省去将许多线路从 Arduino 拉到面包板的麻烦。
硬件
要构建一个简约的 Arduino,你需要以下硬件:
-
一个面包板
-
各种连接线
-
一个 7805 线性电压调节器
-
一个 16 MHz 晶体振荡器
-
一个带 Arduino 启动加载程序的 ATmega328P-PU 微控制器
-
一个 1 µF,25 V 电解电容(C1)
-
一个 100 µF,25 V 电解电容(C2)
-
两个 22 pF,50 V 陶瓷电容(C3 和 C4)
-
一个 100 nF,50 V 陶瓷电容(C5)
-
两个 560 Ω 电阻(R1 和 R2)
-
一个 10 kΩ 电阻(R3)
-
两个你选择的 LED(LED1 和 LED2)
-
一个按键开关(S1)
-
一个六脚连接器
-
一个 PP3 型电池快扣
-
一个 9 V PP3 型电池
其中一些零件可能对你来说是新的。在接下来的章节中,我将解释每个零件,并展示每个零件的示例和原理图。
7805 线性电压调节器
一个 线性电压调节器 包含一个简单的电路,可以将一种电压转换为另一种电压。零件列表中包含的调节器是 7805 型,它可以将 7 到 30 V 之间的电压转换为固定的 5 V,最大电流为 1 A,非常适合驱动我们的面包板 Arduino。图 13-1 显示了一个 TO-220 封装的 7805 示例,旁边放有一个标有毫米刻度的尺子。

图 13-1:带有以毫米为单位的标尺的 7805 线性电压调节器

图 13-2:7805 电路符号
图 13-2 显示了 7805 的原理图符号。当你看到 7805 的标记面时,左侧的引脚(在字母 J 下方)是输入电压引脚,中央引脚连接到 GND,右侧的引脚(在字母 G 下方)是 5V 输出连接。顶部的金属片经过钻孔,可以连接到一个较大的金属件,称为散热器。当电路需要拉取最多 1A 的电流时,我们使用散热器,因为在这种使用情况下,7805 会变得非常温暖,就像一杯热咖啡。金属片也连接到 GND。我们的示例中需要一个 7805 稳压器。
16 MHz 晶体振荡器
更常见的名字是晶体,晶体振荡器产生一个非常精确频率的电信号。在这种情况下,频率为 16 MHz。我们将使用的晶体如图 13-3 所示。

图 13-3:带有毫米标尺的晶体振荡器
将这张图与您 Arduino 板上的晶体进行比较。它们的形状和大小应该完全相同。

图 13-4:晶体振荡器原理图符号
晶体是非极化的。其原理图符号如图 13-4 所示。
晶体决定了微控制器的运行速度。例如,我们将要组装的微控制器电路运行在 16 MHz,这意味着它每秒可以执行 1600 万条处理器指令。然而,这并不意味着它能那么快执行程序的每一行或函数,因为解释一行代码需要许多处理器指令。
Atmel ATmega328P-PU 微控制器 IC
如第二章所述,微控制器是一个微小的计算机,是我们面包板 Arduino 的“大脑”。它包含一个处理器,用于执行指令,包含多种类型的内存以存储来自我们程序的指令和数据,并且有多种方法用于发送和接收数据。图中的 ATmega328P-PU 示例如图 13-5 所示。观察 IC 时,注意引脚编号 1 位于 IC 的左下角,并由一个小点标记。

图 13-5:ATmega328P-PU
微控制器的原理图符号如图 13-6 所示。

图 13-6:微控制器原理图符号
并不是所有的微控制器都包含 Arduino 的引导程序,这个软件使其能够等待 Arduino IDE 发送新的程序供其运行。当选择微控制器以集成到自制的 Arduino 时,务必选择已经包含引导程序的微控制器。这些通常可以在与销售 Arduino 板的零售商(如 Adafruit、PMD Way 和 SparkFun)购买到。
原理图
图 13-7 显示了电路原理图。

图 13-7:项目 36 的原理图
电路图包含两个部分。左侧是电源部分,能够将电压降至平稳的 5V。你会看到一个 LED,当电源开启时它会亮起。右侧部分由微控制器、重置按钮、编程引脚和另一个 LED 组成。这个 LED 连接到用作 Arduino 引脚 13 的 ATmega328P-PU 引脚。
使用电路图连接你的 Arduino。别忘了将电线接到六孔引脚头(如图 13-8 所示),它由电路图底部的六个圆圈表示。我们将在本章稍后使用此连接上传一个草图到我们的自制 Arduino。
电路将使用 9 V 电池和匹配的快接连接器供电,如图 13-9 所示。将电池快接连接器的红色线连接到电路左侧的正极(+)点,黑色线连接到负极(–)点。

图 13-8:六孔引脚头

图 13-9:9 V 电池和快接连接器
确认 Arduino 引脚
我们的自制 Arduino 上有哪些 Arduino 引脚?在普通的 Arduino 板上的所有模拟、数字及其他可用引脚,在我们的面包板版本中也都可以使用;你只需要直接连接到微控制器。
在我们的面包板 Arduino 中,R2 和 LED2 连接在数字引脚 13 上。表 13-1 列出了左侧的 Arduino 引脚和右侧匹配的 ATmega328P-PU 引脚。
表 13-1:ATmega328P-PU 的引脚
| Arduino 引脚名称 | ATmega328P-PU 引脚 |
|---|---|
| RST | 1 |
| RX/D0 | 2 |
| TX/D1 | 3 |
| D2 | 4 |
| D3 | 5 |
| D4 | 6 |
| (5 V only) | 7 |
| GND | 8 |
| D5 | 11 |
| D6 | 12 |
| D7 | 13 |
| D8 | 14 |
| D9 | 15 |
| D10 | 16 |
| D11 | 17 |
| D12 | 18 |
| D13 | 19 |
| (5 V only) | 20 |
| AREF | 21 |
| GND | 22 |
| A0 | 23 |
| A1 | 24 |
| A2 | 25 |
| A3 | 26 |
| A4 | 27 |
| A5 | 28 |
为了避免混淆,零售商如 Adafruit 和 Freetronics 出售可以粘贴在微控制器上的标签,类似于图 13-10 中显示的标签(可在www.freetronics.com.au/collections/arduino/products/microcontroller-labels-arduino-pinout/购买)。

图 13-10:引脚标签
运行草图
现在是时候上传一个草图了。我们将首先上传一个简单的草图来闪烁 LED:
// Project 36 - Creating Your Own Breadboard Arduino
void setup()
{ pinMode(13, OUTPUT);
}
void loop()
{ digitalWrite(13, HIGH); delay(1000); digitalWrite(13, LOW); delay(1000);
}
你可以通过三种方式上传草图。
使用微控制器交换方法
上传草图的最便宜方法是从现有的 Arduino 上移除微控制器,将自制 Arduino 的微控制器插入,上传草图,然后再交换微控制器。
为了安全地从 Arduino 上移除微控制器,请使用 IC 取出器,如图 13-11 所示。

图 13-11:使用 IC 拔取器取出微控制器

图 13-12:弯曲微控制器引脚
在取出微控制器时,一定要均匀且缓慢地同时拔出两端——并且要有耐心!取出这个组件可能会有点困难,但最终微控制器会被取出。

图 13-13:微控制器在 Arduino 中的正确放置方向
当将微控制器插入面包板或 Arduino 时,可能需要稍微弯曲引脚,使其与微控制器主体垂直,这样才能轻松滑入。为此,可以将组件的一侧放在平坦的表面上,然后轻轻向下按压;然后在另一侧重复这个动作,如图 13-12 所示。
最后,当你将原来的微控制器放回 Arduino 板时,记得带有缺口的一端应该朝右侧,如图 13-13 所示。
连接到现有的 Arduino 板
你还可以使用 Arduino Uno 的 USB 接口向面包板 Arduino 上的微控制器上传程序。使用这种方法可以减少 Arduino 板插座的磨损,并节省你的开支,因为你不需要购买额外的 USB 编程线缆。
以下是如何通过 USB 接口向微控制器上传程序:
-
从 Arduino Uno 中移除微控制器并拔下 USB 线。
-
从面包板 Arduino 电路中移除电源(如果已连接)。
-
将一根线从 Arduino 的数字引脚 0 连接到面包板上 ATmega328P-PU 的引脚 2;将另一根线从 Arduino 的数字引脚 1 连接到 ATmega328P-PU 的引脚 3。
-
将 5V 和 GND 从 Uno 连接到面包板上的相应区域。
-
将一根线从 Arduino 的 RST 引脚连接到 ATmega328P-PU 的引脚 1。
-
将 USB 线缆插入 Arduino Uno 板。
此时,计算机应该表现得像一个普通的 Arduino Uno,因此你应该能够正常向面包板电路中的微控制器上传程序,并在必要时使用串口监视器。
使用 FTDI 编程线缆
最后这种方法是最简单的,但它需要购买一个 USB 编程线缆,称为FTDI 线缆(仅仅是因为里面的 USB 接口电路是由一家名为 FTDI 的公司制造的)。购买 FTDI 线缆时,请确保选择 5V 型号,因为 3.3V 型号不能正常工作。这个线缆(见图 13-14)一端是 USB 插头,另一端是带有六根线的插座。该线缆的 USB 端包含的电路等同于 Arduino Uno 板上的 USB 接口。六根线的插座连接到图 13-7 和图 13-8 中所示的插针。

图 13-14:FTDI 线缆
连接电缆时,请确保黑线的一侧连接到面包板的 GND 引脚。一旦电缆连接好,它还会为电路供电,就像普通的 Arduino 板一样。
在上传草图或使用串行监视器之前,通过选择工具▶板并选择正确的微控制器(图 13-15)来更改板类型为 Arduino Duemilanove 或 Diecimila。

图 13-15:在 IDE 中更改板类型
一旦选择了上传方法,通过上传项目 36 的草图来进行测试。现在,你应该能够仅使用面包板设计更复杂的电路,这将使你以更少的资金创造更多的项目。如果你学会了制作自己的印刷电路板,你甚至可以从零开始构建更持久的项目。
多种 Arduino 及替代板
尽管在本书中我们一直专注于使用 Arduino Uno 板,但你可以选择许多替代板。这些板的物理大小、输入输出引脚数量、草图存储空间和价格各不相同。
板之间的一个关键区别是使用的微控制器。目前的板通常使用 ATmega328 或 ATmega2560 微控制器,而 Due 使用另一款更强大的版本,名为 SAM3X8E。它们之间的主要区别(包括两种 ATmega328 版本)总结在表格 13-2 中。
表格 13-2:微控制器比较表
| ATmega328P-PU | ATmega328P SMD | ATmega2560 | SAM3X8E | |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
|
| 用户可更换? | 是 | 否 | 否 | 否 |
| 处理速度 | 16 MHz | 16 MHz | 16 MHz | 84 MHz |
| 工作电压 | 5 V | 5 V | 5 V | 3.3 V |
| 数字引脚数量 | 14(6 个支持 PWM) | 14(6 个支持 PWM) | 54(14 个支持 PWM) | 54(12 个支持 PWM) |
| 模拟输入引脚数量 | 6 | 8 | 16 | 12 |
| 每个 I/O 引脚的直流电流 | 40 mA | 40 mA | 40 mA | 3–15 mA |
| 可用闪存 | 31.5KB | 31.5KB | 248KB | 512KB |
| EEPROM 大小 | 1KB | 1KB | 4KB | 无 EEPROM |
| SRAM 大小 | 2KB | 2KB | 8KB | 96KB |
用于比较各种 Arduino 兼容板的主要参数是它们包含的内存类型和每种类型的数量。以下是三种类型的内存:
-
Flash memory是用于存储编译并通过 IDE 上传的草图的空间。
-
EEPROM(电可擦可编程只读存储器)是一个小的存储空间,可以存储字节变量,正如你将在第十九章中学到的那样。
-
SRAM是用于存储程序变量的空间。
让我们来探讨一下可用的板的范围。
Arduino Uno
Uno 目前被认为是标准的 Arduino 板。所有曾经制作的 Arduino 扩展板都应该与 Uno 兼容。由于它内置了 USB 接口和可拆卸的微控制器,Uno 被认为是最容易使用的 Arduino 板。
Freetronics Eleven
市场上有许多电路板模拟 Arduino Uno 的功能,其中一些甚至在标准设计的基础上进行了改进。其中之一就是 Freetronics Eleven,见图 13-16。

图 13-16:Freetronics Eleven
尽管 Eleven 与 Arduino Uno 完全兼容,但它提供了几项改进,使其成为值得购买的产品。首先是位于数字 I/O 引脚下方的大型原型区域。这个区域允许你直接在主板上构建自己的电路,这样可以节省空间和金钱,因为你不需要购买单独的原型扩展板。
其次,传输器/接收器(TX/RX)、电源和 D13 LED 位于板的最右侧;这种布局使得它们即使在安装了扩展板的情况下也能可见。最后,Eleven 使用微型 USB 接口,比 Uno 上使用的标准 USB 接口要小得多。这使得设计自己的扩展板变得更简单,因为你不需要担心连接会碰到 USB 接口。Eleven 可以从www.freetronics.com.au/products/eleven/购买。
Adafruit Pro Trinket
Adafruit Pro Trinket(见图 13-17)是 Arduino Uno 的微型版本,专为无焊面包板、可穿戴电子设备或任何需要更小电路板的情况而设计。

图 13-17:Adafruit Pro Trinket
它与 Arduino Uno 存在一些微小的差异(例如,除非使用外部 FTDI 电缆,否则没有串口输出);然而,考虑到价格,这款板具有很高的性价比。Pro Trinket 可以从www.adafruit.com/trinket/购买。
Arduino Nano
当你需要一块紧凑型、组装好的兼容 Arduino 的板时,Nano 应该是一个不错的选择。Nano(见图 13-18)也设计为可以在无焊面包板上使用,是一款小巧而强大的 Arduino。

图 13-18:Arduino Nano
Nano 的尺寸仅为 0.7 英寸×1.77 英寸(17.8 mm×44.9 mm),但它具备经典 Arduino Duemilanove 的所有功能。此外,它使用了 ATmega328P 的 SMD 版本,因此它有两个额外的模拟输入引脚(A6 和 A7)。Nano 可以从store.arduino.cc/usa/arduino-nano/购买。
LilyPad
LilyPad 设计用于集成到创意项目中,例如可穿戴电子设备。事实上,您实际上可以用水和温和的洗涤剂清洗 LilyPad,因此它非常适合用于点亮运动衫等。例如,板的设计独特,如 图 13-19 所示。

图 13-19:一款 Arduino LilyPad
LilyPad 上的 I/O 引脚需要将导线焊接到板子上,因此 LilyPad 更适用于永久性项目。作为其极简设计的一部分,它没有电压调节电路,因此用户需要提供 2.7 至 5.5 V 之间的电源。LilyPad 还缺少 USB 接口,因此需要使用 5 V 的 FTDI 电缆来上传草图。您可以从几乎任何 Arduino 零售商那里购买 Arduino LilyPad 或兼容板。
Arduino Mega 2560
当您的 Arduino Uno 上的 I/O 引脚用完或需要更多空间来处理更大的草图时,可以考虑使用 Mega 2560,如 图 13-20 所示。它的物理尺寸比 Uno 大得多,尺寸为 4.3 英寸 x 2.1 英寸(109.2 毫米 x 53.4 毫米)。

图 13-20:一款 Arduino Mega 2560
尽管 Mega 2560 板比 Uno 大得多,但您仍然可以使用大多数 Arduino 扩展板,且有 Mega 尺寸的原型扩展板可用于 Uno 无法容纳的大型项目。由于 Mega 使用 ATmega2560 微控制器,其内存空间和 I/O 能力(如 表 13-2 所描述)远大于 Uno。此外,四条独立的串行通信线路增强了其数据传输能力。您可以从几乎任何 Arduino 零售商那里购买 Mega 2560 板。
Freetronics EtherMega
当您需要一个 Arduino Mega 2560、一个 microSD 卡扩展板和一个以太网扩展板来连接互联网时,最好的选择是 EtherMega(如 图 13-21 所示),因为它将所有这些功能集成在一个板子上,且比单独购买每个组件便宜。EtherMega 可以通过 www.freetronics.com/em/ 购买。

图 13-21:一款 Freetronics EtherMega
Arduino Due
由于搭载 84 MHz 的处理器,可以让您的草图运行得更快,这是迄今为止发布的最强大的 Arduino 板。如 图 13-22 所示,该板与 Arduino Mega 2560 非常相似,但它有一个额外的 USB 端口供外部设备使用,并且引脚标签也不同。
此外,Due 的内存是 Uno 板的 16 倍多,因此您可以创建非常复杂和详细的草图。然而,Due 仅在 3.3 V 下工作——因此,连接到模拟或数字引脚的任何电路、扩展板或其他设备的电压不能大于 3.3 V。虽然您需要注意这些限制,但一般来说,使用 Due 的好处超过了硬件的变化。

图 13-22:一块 Arduino Due
展望未来
本章为你展示了可用硬件的更广泛概况,并介绍了你自己搭建的面包板 Arduino 的概念。你已经了解了构成 Arduino 设计的各个部分,并学会了如何使用无焊接面包板搭建自己的 Arduino。现在,你知道如何制作多个基于 Arduino 的原型,而无需购买更多的电路板。你还了解了市场上各种 Arduino 电路板的情况,应该能够选择最适合你需求的电路板。最后,你对开源运动和 Arduino 在其中的参与有了更深的了解。
在下一章,你将学习如何使用各种电机,并开始制作你自己的 Arduino 控制的电动机器人!
第十四章:电机与运动
在本章中你将
-
使用伺服电机创建一个模拟温度计
-
学习如何控制直流电动机的速度和方向
-
学习如何控制小型步进电机
-
使用 Arduino 电机扩展板
-
开始制作一辆电动机器人
-
使用简单的微动开关进行碰撞避免
-
使用红外和超声波距离传感器进行碰撞避免
使用伺服电机进行小范围运动
一个伺服电机(伺服机构的简称)是一个带有内建传感器的电动机。它可以被命令旋转到特定的角度位置。通过将伺服电机的轴连接到其他机器(如车轮、齿轮和杠杆),你可以精确地控制外部世界中的物体。例如,你可以使用伺服电机来控制遥控车的转向,通过将伺服电机连接到一个角度盘(一个小臂或杆,伺服电机旋转它)。一个角度盘的例子是模拟钟表上的时针。图 14-1 展示了伺服电机和三种类型的角度盘。

图 14-1:伺服电机和各种角度盘
选择伺服电机
在选择伺服电机时,考虑几个参数:
-
速度:伺服电机旋转所需的时间,通常以每角度度数的秒数、每分钟转速(RPM)或每 60 度的秒数来衡量。
-
旋转范围:伺服电机可以旋转的角度范围——例如,180 度(半圈旋转)或 360 度(完整旋转)。
-
电流:伺服电机所消耗的电流。在使用伺服电机与 Arduino 时,你可能需要为伺服电机使用外部电源。
-
扭矩:伺服电机旋转时能施加的力。扭矩越大,伺服电机能够控制的物体就越重。产生的扭矩通常与所用电流成正比。
图 14-1 中展示的伺服电机是一个常见的 SG90 型号。它价格便宜,最多可以旋转 180 度,如图 14-2 所示。

图 14-2:伺服电机旋转范围示例
连接伺服电机
将伺服电机连接到 Arduino 非常简单,因为它只需要三根线。如果你使用的是 SG90 型号,最深色的线连接到 GND,中间的线连接到 5V,最浅色的线(脉冲或数据线)连接到数字引脚。如果你使用的是其他型号的伺服电机,请查阅其数据表,了解正确的接线方式。
使用伺服电机
现在让我们开始使用伺服电机。在这个草图中,伺服电机会在其旋转范围内旋转。按照上述方法将伺服电机连接到 Arduino,脉冲线连接到数字引脚 4,然后输入并上传 Listing 14-1 中的草图。
// Listing 14-1
#include <Servo.h>
Servo myservo;
void setup()
{ myservo.attach(4);
}
void loop()
{ myservo.write(180); delay(1000); myservo.write(90); delay(1000); myservo.write(0); delay(1000);
}
Listing 14-1:伺服电机演示草图
在这个草图中,我们使用了 Servo 库,需要进行安装。请按照第七章中的说明操作。在库管理器中,找到并安装“Servo by Michael Margolis, Arduino”库。使用以下代码创建伺服电机实例:
#include <Servo.h>
Servo myservo;
然后,在void setup()中,我们告诉 Arduino 哪个数字引脚用于控制伺服电机:
myservo.attach(4); // control pin on digital 4
现在我们只需用以下代码来控制伺服电机:
myservo.write(*x*);
在这里,x是一个介于 0 和 180 之间的整数,表示伺服电机将要移动的角度位置。当运行清单 14-1 中的草图时,伺服电机会跨越其最大范围旋转,停在极端位置(0 度和 180 度)以及中间位置(90 度)。查看你的伺服电机时,注意 180 度的位置在左侧,0 度的位置在右侧。
除了推拉物体之外,伺服电机还可以像模拟仪表一样用来传输数据。例如,你可以使用伺服电机作为模拟温度计,就像在项目 37 中展示的那样。
项目#37:构建模拟温度计
使用我们之前章节中的伺服电机和 TMP36 温度传感器,我们将制作一个模拟温度计。我们将测量温度,然后将该测量值转换为 0 到 180 度之间的角度,以指示 0 到 30 摄氏度之间的温度。伺服电机会旋转到与当前温度相匹配的角度。
硬件
所需的硬件非常简单:
-
一个 TMP36 温度传感器
-
一个面包板
-
一个小型伺服电机
-
各种连接线
-
Arduino 和 USB 电缆
电路图
电路也非常简单,如图 14-3 所示。

图 14-3:项目 37 的电路图
草图
该草图将使用与第四章第 8 项目中相同的方法来确定温度。然后,它将把温度转换为伺服电机的角度旋转值。
输入并上传以下草图:
// Project 37 - Building an Analog Thermometer
float voltage = 0;
float sensor = 0;
float currentC = 0;
int angle = 0;
#include <Servo.h>
Servo myservo;
void setup()
{ myservo.attach(4);
}1 int calculateservo(float temperature)
{ float resulta; int resultb; resulta = -6 * temperature; resulta = resulta + 180; resultb = int(resulta); return resultb;
}
void loop()
{ // read current temperature sensor = analogRead(0); voltage = (sensor*5000)/1024; voltage = voltage-500; currentC = voltage/10; // display current temperature on servo 1 angle = calculateservo(currentC); // convert temperature to a servo position if (angle>=0 && angle <=30) { myservo.write(angle); // set servo to temperature delay(1000); }
}
到此为止,这个草图的大部分内容应该都很清楚,但 1 处的calculateservo()函数是新的。此函数根据以下公式将温度转换为伺服电机使用的匹配角度:
angle = (–6 × temperature) + 180
你可能会发现制作一个背板来显示伺服电机所显示的温度范围很有用,可以加一个小箭头来增强真实感。示例见图 14-4。你可以从本书的网站下载可打印版本的背板:nostarch.com/arduino-workshop-2nd-edition/。

图 14-4:背板显示我们的温度计上的温度。
使用电动马达
我们控制电机的下一步是使用小型电动马达。小型电动马达广泛应用于各种领域,从小风扇到玩具车,再到模型铁路。
选择一个马达
与伺服电机类似,选择电动机时需要考虑多个参数:
-
工作电压:电动机设计的工作电压。这可以变化,从 3 V 到超过 12 V。
-
无负载电流:电动机在自由旋转时使用的电流,即在电动机轴上没有任何连接物的情况下的工作电流。
-
卡死电流:电动机在试图旋转时,因为负载的存在而无法转动时所消耗的电流。
-
工作电压下的转速:电动机在 RPM 中的转速。
我们的示例将使用一款小型且价格便宜的电动机,其在 3 V 下运行时的转速为 8,540 转/分钟,类似于图 14-5 中所示的电动机。

图 14-5:我们的微型电动机
为了控制电动机,我们将使用一个晶体管,详情见第三章。由于我们的电动机使用最多 0.7 A 的电流(超过 BC548 晶体管的承载能力),因此我们将在本项目中使用一个名为达林顿的晶体管。
TIP120 达林顿晶体管
达林顿晶体管不过是两个晶体管连接在一起。它能处理高电流和高电压。TIP120 达林顿晶体管能够通过最多 5 A 的电流,且工作电压为 60 V,足以控制我们的微型电动机。TIP120 使用与 BC548 相似的电路符号,如图 14-6 所示,但 TIP120 晶体管的体积比 BC548 大。

图 14-6:TIP120 电路符号
TIP120 使用 TO-220 外壳样式,如图 14-7 所示。

图 14-7:TIP120
当你从标记的一侧查看 TIP120 时,针脚从左到右分别是基极(B)、集电极(C)和发射极(E)。金属散热片也连接到集电极。
项目 #38:控制电动机
在这个项目中,我们将通过调整电动机的速度来控制它。
硬件
需要以下硬件:
-
一个小型 3 V 电动机
-
一个 1 kΩ 电阻(R1)
-
一块面包板
-
一个 1N4004 二极管
-
一个 TIP120 达林顿晶体管
-
一个单独的 3 V 电源
-
各种连接线
-
Arduino 和 USB 电缆
必须为电动机使用单独的电源,因为 Arduino 在所有情况下都无法提供足够的电流。如果电动机卡住,它将拉取其卡死电流,这可能超过 1 A。这超过了 Arduino 所能提供的电流,如果 Arduino 尝试提供这么多电流,它可能会被永久损坏。
一个单独的电池座是一个简单的解决方案。对于 3 V 电源,一个带飞线的两节 AA 电池座就足够了,如图 14-8 所示。

图 14-8:一个两节 AA 电池座
电路图
按照图 14-9 中的电路图组装电路。

图 14-9:项目 38 的原理图
草图
在这个项目中,我们将调整电机的速度,从停止(零速)到最大速度,然后再减速回零。输入并上传以下草图:
// Project 38 - Controlling the Motor
void setup()
{ pinMode(5, OUTPUT);
}
void loop()
{1 for (int a=0; a<256; a++) { analogWrite(5, a);2 delay(100); }3 delay(5000);4 for (int a=255; a>=0; a--) { analogWrite(5,a); delay(100); } delay(5000);
}
我们通过脉宽调制来控制电机的速度(如第三章项目 3 中所示)。回想一下,我们只能在数字引脚 3、5、6、9、10 和 11 上执行此操作。使用这种方法,电流会以短时脉冲的方式施加到电机上:脉冲持续时间越长,电机的速度越快,因为电机在设定的时间内“开启”的时间比“关闭”的时间多。因此,在 1 时,电机的速度从零开始,逐渐加速;你可以通过改变 2 处的delay值来控制加速。3 时,电机以最快的速度运行并保持该速度 5 秒钟。然后,从 4 开始,过程反转,电机逐渐减速直至停止。
二极管的使用方式与图 3-19(第 42 页)中描述的继电器控制电路相同,用于保护电路。当电机的电流被切断时,电机线圈内会存在短暂的剩余电流,这些电流必须消耗掉。二极管允许这些剩余电流在电机线圈内循环,直到以微小的热量形式消散掉。
使用小型步进电机
步进电机与普通直流电机的不同之处在于,它们将电机的完整旋转分为固定数量的步伐。它们通过使用两个独立控制的线圈绕组来实现这一点。因此,不同于通过改变电压来控制旋转的普通直流电机,步进电机通过按照一定模式打开或关闭线圈,旋转轴在任意方向上旋转设定次数。这种控制方式使得步进电机非常适合需要精确电机定位的任务。它们在从计算机打印机到先进制造设备等各种设备中都有广泛应用。
我们将使用 28BYJ-48 型号的步进电机演示其操作,如图 14-10 所示。这种步进电机可以控制电机转到 4,096 个位置之一;也就是说,一整圈的旋转被分为 4,096 步。

图 14-10:步进电机及控制板
电机旁边的板子作为 Arduino 与步进电机之间的接口,方便快捷地进行连接。通常它会和步进电机一起提供。它的特写图见图 14-11。

图 14-11:步进电机控制板
现在将步进电机连接到 Arduino。按照表 14-1 中的示意图进行连接。
表 14-1:步进电机控制板与 Arduino 之间的连接
| 控制板引脚标签 | Arduino 引脚 |
|---|---|
| IN1 | D8 |
| IN2 | D9 |
| IN3 | D10 |
| IN4 | D11 |
| 5–12 V+ | 5 V |
| 5–12 V− | GND |
如果没有其他设备从 Arduino 获取电源,你可以简单地使用 Arduino 提供的电源来运行步进电机。然而,这并不推荐。建议使用外部 5V 电源,如插头包或其他便捷的电源。由于控制板没有直流插座,你可以使用带端子块的外部插座来实现简便、免焊接的连接,如图 14-12 所示。

图 14-12:直流插座端子块
然后,你可以将跳线从端子块的+和–连接器连接到步进电机控制板的对应端子。为了简化在 Arduino 草图中控制步进电机,你可以使用一个叫做 CheapStepper 的简洁 Arduino 库。你可以从github.com/tyhenry/CheapStepper/archive/master.zip下载它,并按照第七章中描述的方法安装。
一旦你成功安装了库并按照之前的描述连接了步进电机,就可以输入并上传列表 14-2。
// Listing 14-21 #include <CheapStepper.h>2 CheapStepper stepper (8, 9, 10, 11);3 boolean clockwise = true;
boolean cclockwise = false;4 void setup()
{ stepper.setRpm(20); Serial.begin(9600);
}
void loop()
{ Serial.println("stepper.moveTo (Clockwise, 0)");5 stepper.moveTo (clockwise, 0); delay(1000); Serial.println("stepper.moveTo (Clockwise, 1024)");5 stepper.moveTo (clockwise, 1024); delay(1000); Serial.println("stepper.moveTo (Clockwise, 2048)"); stepper.moveTo (clockwise, 2048); delay(1000); Serial.println("stepper.moveTo (Clockwise, 3072)"); stepper.moveTo (clockwise, 3072); delay(1000); Serial.println("stepper.moveTo (CClockwise, 512)");5 stepper.moveTo (cclockwise, 512); delay(1000); Serial.println("stepper.moveTo (CClockwise, 1536)");5 stepper.moveTo (cclockwise, 1536); delay(1000); Serial.println("stepper.moveTo (CClockwise, 2560)"); stepper.moveTo (cclockwise, 2560); delay(1000); Serial.println("stepper.moveTo (CClockwise, 3584)"); stepper.moveTo (cclockwise, 3584); delay(1000);
}
列表 14-2:测试步进电机
步进电机的操作非常简单。我们首先在第 1 行包含库,并在第 2 行创建电机的实例。(如果你希望更改控制板使用的数字引脚,请在此处更新。)控制函数使用true和false分别表示顺时针和逆时针旋转,因此我们在第 3 行将它们分配给布尔变量,以便更加清晰。最后,电机可以通过以下函数旋转到 4,096 个位置中的一个:
Stepper.moveTo(*direction*, *location*);
其中,方向要么是顺时针,要么是逆时针,位置是介于 0 和 4,095 之间的一个值。这个过程从 5 开始,并重复直到草图结束。
此外,在void setup()的第 4 行中,我们通过以下代码将电机的转速设置为 20 RPM:
stepper.setRpm(20);
这是我们步进电机的推荐速度。其他电机的速度可能不同,因此你应该向供应商咨询这些细节。
在你上传草图后不久,步进电机将开始旋转到不同的位置,你可以在串口监视器中看到回显的命令,如图 14-13 所示。

图 14-13:发送到步进电机的命令
项目#39:构建和控制机器人车辆
尽管控制一个直流电机的速度非常有用,但让我们进入更有趣的领域,同时控制四个直流电机并影响它们的速度和方向。我们的目标是构建一个四轮车辆风格的机器人,接下来的章节中我们将继续开发它。在这里,我将描述我们的机器人构建和基本控制。
我们的机器人有四个电机,每个电机控制一个车轮,使其能够以不同的速度行驶并在原地旋转。你将能够控制行驶的速度和方向,还将学会如何添加零件来实现避障和远程控制。完成本书中的项目后,你将为创造自己的机器人版本并将创意付诸实践打下坚实的基础。
硬件
你将需要以下硬件:
-
配备四个直流电机和车轮的机器人车辆底盘
-
四节 AA 电池座(带有接线输出)
-
四节碱性 AA 电池
-
L293D 电机驱动扩展板(适用于 Arduino)
-
Arduino 和 USB 电缆
底盘
任何机器人的基础都是一个坚固的底盘,包含电机、传动系统和电源。一个由 Arduino 驱动的机器人还需要有足够的空间来安装 Arduino 和各种外部部件。
你可以从市面上众多的底盘模型中进行选择。为了简化操作,我们使用了一个价格便宜的机器人底盘,包含四个小型直流电机,工作电压大约为 6V DC,并配有相应的车轮,如 图 14-14 所示。

图 14-14:我们的机器人底盘
机器人底盘的物理组装任务因模型不同而有所不同,你可能需要一些基本工具,如螺丝刀和钳子。如果你对最终设计不确定,但仍然希望让机器人开始移动,常用的一种技巧是使用粘性产品(如 Blu Tack)将电子元件固定到底盘上。
电源
机器人底盘包含的电机通常工作在大约 6V DC,因此我们将使用一个四节 AA 电池座为机器人供电,如 图 14-15 所示。

图 14-15:四节 AA 电池的电池座
一些 AA 电池座可能没有连接到我们项目所需的线路,而是配有 9V 电池卡扣的连接(如我们在 图 14-15 中的单元所示)。在这种情况下,你需要像 图 14-16 中的电池卡扣一样的设备。

图 14-16:用于将电池座连接到 Arduino 的电池线缆
电路图
最后的要求是创建电路来控制底盘中的四个电机。虽然我们可以为每个电机使用 图 14-9 中显示的电路,但这不能让我们控制电机的方向,且连接起来可能会稍显不便。相反,我们将使用一个电机扩展板。电机扩展板包含了处理电机所需的高电流的电路,并接受 Arduino 发出的命令来控制电机的速度和方向。对于我们的机器人,我们将使用 L293D 电机驱动扩展板,如 图 14-17 所示。

图 14-17:Arduino 的 L293D 电机驱动扩展板
连接电机扩展板
连接电机扩展板所需的接线非常简单:将电池盒的电线连接到扩展板左下角的接线端子,如图 14-18 所示。黑色电线(负极)必须连接到右侧,红色电线连接到左侧。

图 14-18:直流电源连接
接下来需要将每个直流电机连接到电机扩展板。我们将底盘前部的两个直流电机分别称为电机 2(左)和电机 1(右),底盘后部的两个直流电机分别称为电机 3(左)和电机 4(右)。每个电机都有一根红色和一根黑色电线,将它们连接到电机扩展板左侧和右侧相应的接线端子,如图 14-19 所示。

图 14-19:连接电机
连接直流电机的电线时,请注意黑色电线连接到接线端子的外侧,红色电线连接到内侧。此外,每个接线端子上都有相应的电机编号(M1、M2、M3 和 M4)以便参考。
如果你的电机电线没有颜色编码,可能需要在第一次运行后交换电线,以确定哪个方向是前进或后退。
在将电源和电机电线连接到扩展板并将扩展板连接到 Arduino 后,机器人应该类似于图 14-20 所示的样子。

图 14-20:我们的机器人准备好行动了!
草图
现在开始让机器人动起来。为了简化操作,我们首先需要下载并安装用于电机驱动扩展板的 Arduino 库。按照第七章中的说明操作。在库管理器中,找到并安装“Adafruit Motor Shield library by Adafruit”。
稍等片刻,Adafruit Motor Shield 库 v1 会出现。点击安装并等待库安装完成。然后你可以关闭库管理器窗口。
现在我们将创建一些函数来操作我们的机器人。由于涉及到两个电机,我们需要四个动作:
-
前进运动
-
反向运动
-
顺时针旋转
-
逆时针旋转
因此,我们需要在草图中创建四个函数来匹配我们的四个动作:goForward()、goBackward()、rotateLeft()和rotateRight()。每个函数接受一个毫秒值,表示操作所需的时间,以及一个介于 0 到 255 之间的 PWM 值。例如,要以全速前进 2 秒,我们可以使用goForward(255,2000)。
输入并保存以下草图(但暂时不要上传):
// Project 39 - Building and Controlling a Robot Vehicle
#include <AFMotor.h>1 AF_DCMotor motor1(1); // set up instances of each motor
AF_DCMotor motor2(2);
AF_DCMotor motor3(3);
AF_DCMotor motor4(4);2 void goForward(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(FORWARD); motor2.run(FORWARD); motor3.run(FORWARD); motor4.run(FORWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}2 void goBackward(int speed, int duration){ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(BACKWARD); motor2.run(BACKWARD); motor3.run(BACKWARD); motor4.run(BACKWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}2 void rotateLeft(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(FORWARD); motor2.run(BACKWARD); motor3.run(BACKWARD); motor4.run(FORWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}2 void rotateRight(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(BACKWARD); motor2.run(FORWARD); motor3.run(FORWARD); motor4.run(BACKWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void setup(){ delay(5000);
}
void loop()
{ goForward(127,5000); delay(1000); rotateLeft(127,2000); delay(1000); goBackward(127,5000); delay(1000); rotateRight(127,5000); delay(5000);
}
由于草图中包含了四个自定义函数,因此控制机器人非常简单。每个自定义函数利用了用于控制电机的库函数。在使用这些函数之前,需要为每个电机创建一个实例,如 1 所示。
每个电机的运动方向可以通过以下方式设置:
Motor.run(*direction*)
direction的值可以是FORWARD(前进)、REVERSE(后退)或RELEASE(释放),分别表示设置电机的旋转方向为前进或后退,或切断电机的电源。
要设置电机的速度,我们使用:
Motor.setSpeed(*speed*)
speed的值在0和255之间;它是用于控制电机转速的 PWM 范围。
因此,在我们四个自定义函数中的每一个(见第 2 点)中,我们使用电机速度和方向控制的组合来同时控制四个电机。每个自定义函数接受两个参数:speed(我们的 PWM 值)和duration(电机运行的时间)。
上传草图,移除 USB 电缆,然后将电池电缆连接到 Arduino 的电源插座。接着将机器人放在地毯或清洁的表面上,让它开始移动。通过实验草图中的运动功能来控制你的机器人,这将帮助你熟悉时间延迟以及它们与行驶距离的关系。
连接额外硬件到机器人
一些 Arduino 的电机驱动扩展板可能没有堆叠头插座,无法让你在上面再放一个扩展板,而且它们可能不方便连接传感器等设备的电线。在这种情况下,你应该使用适用于 Arduino 的端子扩展板,其示例如图 14-21 所示。

图 14-21:Arduino 的端子扩展板
端子扩展板可以方便地将硬件或传感器接入 Arduino 的输入和输出引脚,而无需焊接,并且它们还可以用于构建你自己的电路,方便以后做更多持久性的应用。
碰撞检测
现在我们的机器人能够移动了,我们可以开始添加传感器。这些传感器将告诉机器人何时碰到障碍物,或者它们将测量机器人与路径中物体之间的距离,以便它能够避免碰撞。我们将使用三种碰撞避免方法:微动开关、红外线和超声波。
项目 #40:使用微动开关检测机器人碰撞
微动开关可以像我们在第四章中使用的简单按钮一样工作,但微动开关的组件在物理上更大,并且包括一个作为执行器的大金属杆(见图 14-22)。

图 14-22:微动开关
使用微动开关时,你将一根电线连接到底部接触点,另一根电线连接到标有 NO(常开)的接触点,以确保电流只有在金属杆被按下时才会流动。我们将在机器人的前面安装微动开关,这样当机器人撞到物体时,金属杆就会被按下,导致电流流动,并使机器人改变方向或采取其他行动。
原理图
微动开关硬件的接线方式类似于一个简单的按钮,如图 14-23 所示。

图 14-23:项目 40 的原理图
草图
我们将微动开关连接到一个中断端口(数字引脚 2)。虽然你可能认为我们应该有一个由中断调用的函数来让机器人倒退几秒钟,但这是不可能的,因为delay()函数无法在中断调用的函数内部运行。在这种情况下,我们必须换个思路。
相反,函数goForward()将在满足变量crash和布尔值move的两个条件时启动电机。如果crash为true,电机将在较慢的速度下反向 2 秒,以避开碰撞。
我们不能使用delay(),因为有中断的存在,所以我们通过读取开始时的millis()值并与当前的millis()值进行比较来测量电机运行的时间。当差值大于或等于所需的持续时间时,move被设置为false,电机停止。
输入并上传以下草图:
// Project 40 – Detecting Robot Vehicle Collisions with a Microswitch
#include <AFMotor.h>
AF_DCMotor motor1(1); // set up instances of each motor
AF_DCMotor motor2(2);
AF_DCMotor motor3(3);
AF_DCMotor motor4(4);
boolean crash = false;
void goBackward(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(BACKWARD); motor2.run(BACKWARD); motor3.run(BACKWARD); motor4.run(BACKWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}1 void backOut()
{ crash = true;
}
void goForward(int duration, int speed){ long a, b; boolean move = true;2 a = millis(); do { if (crash == false) { motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(FORWARD); motor2.run(FORWARD); motor3.run(FORWARD); motor4.run(FORWARD); } if (crash == true) {3 goBackward(200, 2000); crash = false; }4 b = millis() - a; if (b >= duration) { move = false; } } while (move != false); // stop motors motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void setup()
{ attachInterrupt(0, backOut, RISING); delay(5000);
}
void loop()
{ goForward(5000, 127); delay(2000);
}
这个草图使用了一种高级的前进方法,即在机器人运动过程中使用两个变量来监控运动。第一个是布尔变量crash。如果机器人碰到某物并触发了微动开关,则会调用一个中断,运行第 1 步的函数backOut()。在这里,变量crash的值将从false更改为true。第二个被监控的变量是布尔变量move。在goForward()函数中,我们在第 2 步使用millis()不断计算机器人是否已完成所需时间(由参数duration设置)的移动。
在第 4 步,函数计算经过的时间是否小于所需时间,如果是,则变量move被设置为true。因此,机器人只有在没有发生碰撞且没有耗尽时间的情况下才能向前移动。如果检测到碰撞,第 3 步的函数goBackward()将被调用,此时机器人将慢慢后退 2 秒,然后恢复正常。
红外距离传感器
我们的下一种避碰方法使用红外(IR)距离传感器。该传感器通过将红外光信号从其前方表面反射回来,并返回一个与传感器与表面之间的距离相关的电压值。红外传感器在避碰检测中非常有用,因为它们价格低廉,但并不适合精确的距离测量。我们将在项目中使用 Sharp GP2Y0A21YK0F 模拟传感器,如图 14-24 所示。

图 14-24:Sharp IR 传感器
连接线路
要连接传感器,将传感器上的红色和黑色线分别连接到 5V 和 GND,白色线连接到你的 Arduino 的模拟输入引脚。我们将使用analogRead()来测量传感器返回的电压。图 14-25 中的图表显示了测量的距离与输出电压之间的关系。

图 14-25:红外传感器距离与输出电压的图表
测试红外距离传感器
由于距离和输出之间的关系无法通过公式简单表示,我们将把读数分成 5 cm 的阶段。为了演示这一点,我们将使用一个简单的示例。将红外传感器的白色引线连接到模拟引脚 0,红色引线连接到 5 V,黑色引线连接到 GND。然后输入并上传 Listing 14-3 中显示的草图。
// Listing 14-3
float sensor = 0;
int cm = 0;
void setup()
{ Serial.begin(9600);
}
void loop()
{1 sensor = analogRead(0); 2 if (sensor<=90) { Serial.println("Infinite distance!"); } else if (sensor<100) // 80 cm { cm = 80; } else if (sensor<110) // 70 cm { cm = 70; } else if (sensor<118) // 60 cm { cm = 60; } else if (sensor<147) // 50 cm { cm = 50; } else if (sensor<188) // 40 cm { cm = 40; } else if (sensor<230) // 30 cm { cm = 30; } else if (sensor<302) // 25 cm { cm = 25; } else if (sensor<360) // 20 cm { cm = 20; } else if (sensor<505) // 15 cm { cm = 15; } else if (sensor<510) // 10 cm { cm = 10; } else if (sensor>=510) // too close! { Serial.println("Too close!"); } Serial.print("Distance: "); Serial.print(cm); Serial.println(" cm"); delay(250);
}
Listing 14-3:红外传感器演示草图
该草图首先读取红外传感器的电压,然后通过一系列的 if 语句来选择返回的近似距离。我们通过传感器返回的电压使用两个参数来确定距离。第一个是电压与距离的关系,如 图 14-25 所示。然后,利用知识(来自第四章的项目 6),analogRead() 返回一个介于 0 和 1,023 之间的值,与 0 V 到大约 5 V 之间的电压成比例,我们可以计算出传感器返回的近似距离。
上传草图后,打开串口监视器,尝试通过将手或纸张在不同的距离处移动来进行实验。串口监视器应返回近似的距离,如 图 14-26 所示。

图 14-26:Listing 14-3 的结果
项目 #41:使用红外距离传感器检测机器人车辆碰撞
现在让我们用红外传感器替代微型开关,配合我们的机器人车。我们将使用一个略微修改过的第 40 项目版本。我们不使用中断,而是创建一个 checkDistance() 函数,当红外传感器测量的距离大约为 20 cm 或更小时,将变量 crash 改为 true。我们将在 goForward() 前进动作的 do-while 循环中使用它。
草图
将红外传感器连接到你的机器人,然后输入并上传以下草图:
// Project 41 - Detecting Robot Vehicle Collisions with an IR Distance Sensor
#include <AFMotor.h>
AF_DCMotor motor1(1); // set up instances of each motor
AF_DCMotor motor2(2);
AF_DCMotor motor3(3);
AF_DCMotor motor4(4);
boolean crash = false;
void goBackward(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(BACKWARD); motor2.run(BACKWARD); motor3.run(BACKWARD); motor4.run(BACKWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void checkDistance()
{1 if (analogRead(0) > 460) { crash = true; }
}
void goForward(int duration, int speed)
{ long a, b; boolean move = true; a = millis(); do { checkDistance(); if (crash == false) { motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(FORWARD); motor2.run(FORWARD); motor3.run(FORWARD); motor4.run(FORWARD); } if (crash == true) { goBackward(200, 2000); crash = false; } b = millis() - a; if (b >= duration) { move = false; } } while (move != false); // stop motors motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void setup(){ delay(5000);
}
void loop()
{ goForward(5000, 255); delay(2000);
}
这个草图使用了与第 40 项目中相同的方法,唯一的区别是这个版本不断地在 1 处进行距离测量,并且如果红外传感器与物体之间的距离小于约 20 cm,就将 crash 变量设置为 true。
修改草图:添加更多传感器
在运行机器人并使用此传感器后,你应该能够看到使用非接触式碰撞传感器的好处。向同一机器人添加更多传感器也非常简单,例如在前后或每个角落安装传感器。你应该能够添加代码,逐一检查每个传感器,并根据返回的距离值做出决策。
超声波距离传感器
我们的最终碰撞避免方法使用超声波距离传感器。该传感器将超高频声波(人耳无法听到的)从表面反射回来,并测量声音返回传感器所需的时间。我们将在这个项目中使用常见的 HC-SR04 型超声波距离传感器,如图 14-27 所示,因为它价格便宜且精度高,约为 2 厘米。

图 14-27:HC-SR04 超声波距离传感器
超声波传感器的准确性和范围意味着它可以测量大约 2 到 450 厘米之间的距离。然而,由于声波需要反射回传感器,传感器的角度必须与运动方向小于 15 度。
连接超声波传感器
要连接传感器,将 Vcc(5V)和 GND 引脚连接到电机驱动屏蔽板上的相应连接器,将 Trig 引脚连接到数字引脚 D2,将 Echo 引脚连接到数字引脚 D13。我们选择 D2 和 D13 是因为这些引脚不被电机驱动屏蔽板使用。不过,如果你只是单独测试或实验传感器而不使用机器人,可以直接将电线连接到 Arduino 板上。
为了简化传感器的操作,请从github.com/Martinsos/arduino-lib-hc-sr04/archive/master.zip下载 Arduino 库,并按照第七章的说明进行安装。安装完成后,你可以运行列表 14-4 中的测试草图,查看传感器的工作效果。
// Listing 14-4
#include <HCSR04.h>1 UltraSonicDistanceSensor HCSR04(2, 13); // trig - D2, echo - D132 float distance;
void setup ()
{ Serial.begin(9600);
}
void loop ()
{3 distance = HCSR04.measureDistanceCm(); Serial.print("Distance: "); Serial.print(distance); Serial.println(" cm"); delay(500);
}
列表 14-4:超声波传感器演示草图
由于库的帮助,从传感器中获取距离变得非常简单。在 1 中,我们创建了一个实例并声明与传感器连接的数字引脚。然后在 2 中,我们使用一个浮动变量来存储从传感器库函数返回的距离。最后,在 3 中,生成的距离会显示在串口监视器中。
测试超声波传感器
上传草图后,打开串口监视器,并将物体靠近或移开传感器。传感器应返回物体的距离,单位为厘米。查看其工作原理,请参考图 14-28。

图 14-28:列表 14-4 中的测试结果
项目#42:使用超声波距离传感器检测碰撞
现在你已经了解了传感器的工作原理,我们来将它与机器人一起使用。
草图
在以下草图中,我们检查机器人与物体之间的距离是否小于或等于 5 厘米,这将促使机器人后退。输入并上传以下草图,亲自体验:
// Project 42 - Detecting Collisions with an Ultrasonic Distance Sensor
#include <AFMotor.h>
#include <HCSR04.h>
// set up instances of each motor
AF_DCMotor motor1(1);
AF_DCMotor motor2(2);
AF_DCMotor motor3(3);
AF_DCMotor motor4(4);
// set up ultrasonic sensor
UltraSonicDistanceSensor HCSR04(2, 13); // trig - D2, echo - D13
boolean crash=false;
void checkDistance()
{ float distance; distance = HCSR04.measureDistanceCm();1 if (distance < 5) // crash distance is 5 cm or less { crash = true; }
}
void goBackward(int speed, int duration){ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(BACKWARD); motor2.run(BACKWARD); motor3.run(BACKWARD); motor4.run(BACKWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void goForward(int duration, int speed)
{ long a, b; boolean move = true; a = millis(); do { checkDistance(); if (crash == false) { motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(FORWARD); motor2.run(FORWARD); motor3.run(FORWARD); motor4.run(FORWARD); } if (crash == true) { goBackward(200, 2000); crash = false; } b = millis() - a; if (b >= duration) { move = false; } } while (move != false); // stop motors motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void setup(){ delay(5000);
}
void loop()
{ goForward(1000, 255);
}
这个草图的操作应该已经非常熟悉了。我们再次不断测量距离,当超声波传感器与路径中的物体之间的距离小于 5 厘米时,我们将变量 crash 更改为 true。看到机器人神奇地避开障碍物,或者和宠物进行智力较量,真的非常令人惊讶。
展望未来
在本章中,您学习了如何将基于 Arduino 的项目引入到运动的世界。通过使用简单的电机或一对电机,并配合电机扩展板,您可以创建能够自主移动甚至避开障碍物的项目。我们使用了三种类型的传感器,展示了不同的精度和传感器成本,因此您现在可以根据需求和项目预算做出决策。
到现在为止,我希望您已经体验并享受到了设计和构建这些项目的乐趣。但这还不止于此。在下一章中,我们将走到户外,利用卫星导航的力量。
第十五章:使用 GPS 与 Arduino 配合
本章内容包括:
-
学习如何连接 GPS 扩展板
-
创建一个简单的 GPS 坐标显示
-
在地图上显示 GPS 坐标的实际位置
-
构建一个精确的时钟
-
记录移动物体随时间变化的位置
您将学习如何使用一种便宜的 GPS 扩展板来确定位置,创建一个精确的时钟,并制作一个记录设备,将您设备的位置随时间记录到 microSD 卡上,之后可以将其绘制在地图上,显示移动历史。
什么是 GPS?
全球定位系统(GPS) 是一个基于卫星的导航系统,它通过绕地球轨道运行的卫星向地面上的 GPS 接收器发送数据,接收器可以利用这些数据确定地球上任何位置的当前坐标和时间。您可能已经熟悉用于汽车或智能手机的 GPS 导航设备。
尽管我们无法使用 Arduino 创建详细的地图导航系统,但您可以使用 GPS 模块来确定您的位置、时间以及近似速度(如果您正在移动)。在购买 GPS 模块时,通常有两种类型可供选择。第一种是独立的、便宜的 GPS 接收器模块,带有外部天线,如图 15-1 所示。

图 15-1:GPS 接收模块
您将遇到的第二种类型是适用于 Arduino 的 GPS 扩展板,如图 15-2 所示。这些扩展板非常方便,因为所有接线已经为您做好;它们还包括一个 microSD 卡插槽,非常适合记录数据,如本章稍后演示的那样。
确保您的 GPS 扩展板支持将 GPS 接收器的 TX 和 RX 线路连接到 Arduino 数字引脚 D2 和 D3,或者具有跳线可以手动设置这些连接(如图 15-2 所示的扩展板)。更多详情请咨询供应商。您可以在本章中使用这两种设备。不过,我强烈推荐使用扩展板,尤其是您可以轻松地将 LCD 扩展板连接到 GPS 扩展板上,作为显示器使用。

图 15-2:适用于 Arduino 的 GPS 扩展板
测试 GPS 扩展板
在购买 GPS 套件后,确保它能正常工作并接收 GPS 信号是个好主意。GPS 接收器需要与天空保持视距,但信号可以穿透窗户。所以,虽然最好在户外进行此测试,但您的 GPS 接收器通过无遮挡的窗户或天窗通常也能正常工作。为了测试接收效果,您需要设置扩展板或模块,并运行一个基本的草图,显示接收到的原始数据。
如果您使用的是 GPS 扩展板,请确保 GPS 的 TX 引脚连接到 Arduino 数字引脚 D2,RX 引脚连接到 Arduino 数字引脚 D3。如果您使用的是 GPS 模块,如图 15-1 所示,请将 Vcc 和 GND 分别连接到 Arduino 的 5V 和 GND;然后将 TX 连接到 Arduino 数字引脚 D2,RX 连接到 Arduino 数字引脚 D3。
要进行测试,请输入并上传 清单 15-1 中的草图。
// Listing 15-1
#include <SoftwareSerial.h>
// GPS TX to D2, RX to D3
SoftwareSerial Serial2(2, 3);
byte gpsData;
void setup()
{
// Open the Arduino Serial Monitor Serial.begin(9600);
// Open the GPS1 Serial2.begin(9600);
}
void loop()
{2 while (Serial2.available() > 0) { // get the byte data from the GPS gpsData = Serial2.read();3 Serial.write(gpsData); }
}
清单 15-1:基本的 GPS 测试草图
该草图在第 2 行监听软件串口,当从 GPS 模块或扩展板接收到一个字节的数据时,它会将数据发送到第 3 行的串口监视器。(注意,我们在第 1 行以 9,600 波特率启动软件串口,以匹配 GPS 接收器的数据速率。)
上传草图后,可能需要等待大约 30 秒;这是为了让 GPS 接收器有时间开始接收来自一个或多个 GPS 卫星的信号。GPS 扩展板或模块上将有一个内置 LED,当接收器开始找到 GPS 信号时,LED 将开始闪烁。LED 开始闪烁后,打开 IDE 中的串口监视器窗口,并将数据速率设置为 9,600 波特率。你应该看到类似于 图 15-3 中所示的持续数据流。

图 15-3:来自 GPS 卫星的原始数据
数据从 GPS 接收器按字符逐个发送到 Arduino,然后再发送到串口监视器。但是这些原始数据(称为GPS 句子)本身并不太有用,因此我们需要使用一个新的库,从原始数据中提取信息并将其转换为可用的格式。为此,请按照第七章中的方法下载并安装 TinyGPS 库,网址为 www.arduiniana.org/libraries/tinygps/。
项目 #43:创建一个简单的 GPS 接收器
我们将从创建一个简单的 GPS 接收器开始。因为你通常会在户外使用 GPS——为了让事情变得稍微简单一点——我们将添加一个 LCD 模块来显示数据,类似于 图 15-4 中所示的。

图 15-4:Freetronics LCD & Keypad 扩展板
结果将是一个非常基本的便携式 GPS,它可以由 9V 电池和连接器供电,并在 LCD 上显示你当前的位置坐标。
硬件
所需的硬件非常简单:
-
Arduino 和 USB 电缆
-
LCD 模块或 Freetronics LCD & Keypad 扩展板
-
一根 9V 电池至直流插座电缆
-
GPS 模块和 Arduino 用螺丝扩展板或 Arduino 用 GPS 扩展板
草图
输入并上传以下草图:
// Project 43 - Creating a Simple GPS Receiver1 #include <LiquidCrystal.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
#include <TinyGPS.h>
#include <SoftwareSerial.h>
// GPS TX to D2, RX to D3
SoftwareSerial Serial2(2, 3);
TinyGPS gps;
void getgps(TinyGPS &gps);
byte gpsData;2 void getgps(TinyGPS &gps)
// The getgps function will display the required data on the LCD
{ float latitude, longitude; // decode and display position data3 gps.f_get_position(&latitude, &longitude); lcd.setCursor(0, 0); lcd.print("Lat:"); lcd.print(latitude, 5); lcd.print(" "); lcd.setCursor(0, 1); lcd.print("Long:"); lcd.print(longitude, 5); lcd.print(" "); delay(3000); // wait for 3 seconds lcd.clear();
}
void setup(){ Serial2.begin(9600);
}
void loop()
{ while (Serial2.available() > 0) { // get the byte data from the GPS gpsData = Serial2.read(); if (gps.encode(gpsData)) {4 getgps(gps); } }
}
从第 1 到第 2 行,草图引入了 LCD 和 GPS 所需的库。在 void loop() 中,我们将从 GPS 接收器接收到的字符发送到 4 处的 getgps() 函数。通过在 3 处使用 gps.f_get_position() 获取数据,并将位置值插入到字节变量 &latitude 和 &longitude 中,然后将其显示在 LCD 上。
运行草图
上传草图并且 GPS 开始接收数据后,你的当前经纬度位置应该会显示在 LCD 上,如 图 15-5 所示。

图 15-5:来自项目 43 的纬度和经度显示
但是这个位置在地球上哪里呢?我们可以通过使用 Google Maps (maps.google.com/) 精确确定它的位置。在网站上,输入纬度和经度,用逗号和空格分隔,Google Maps 将返回该位置。例如,使用图 15-5 中返回的坐标可以生成如图 15-6 所示的地图。

图 15-6:在图 15-5 中显示的 GPS 坐标将我们定位到阿尔卡特拉兹岛。
项目 #44:创建一个基于 GPS 的精确时钟
使用 GPS 不仅仅是为了找到位置;系统还会传输时间数据,这些数据可以用来制作一个非常精确的时钟。
硬件
对于这个项目,我们将使用与项目 43 中相同的硬件。
草图
输入并上传以下草图以构建 GPS 时钟:
// Project 44 - Creating an Accurate GPS-Based Clock
#include <LiquidCrystal.h>
#include <TinyGPS.h>
#include <SoftwareSerial.h>
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
// GPS RX to D3, GPS TX to D2
SoftwareSerial Serial2(2, 3);
TinyGPS gps;
void getgps(TinyGPS &gps);
byte gpsData;
void getgps(TinyGPS &gps){ byte month, day, hour, minute, second, hundredths;1 gps.crack_datetime(&year,&month,&day,&hour,&minute,&second,&hundredths);2 hour=hour+10; // change the offset so it is correct for your time zone if (hour>23) { hour=hour-24; } lcd.setCursor(0,0); // print the date and time3 lcd.print("Current time: "); lcd.setCursor(4,1); if (hour<10) { lcd.print("0"); } lcd.print(hour, DEC); lcd.print(":"); if (minute<10) { lcd.print("0"); } lcd.print(minute, DEC); lcd.print(":"); if (second<10) { lcd.print("0"); } lcd.print(second, DEC);
}
void setup()
{ Serial2.begin(9600);
}
void loop()
{ while (Serial2.available() > 0) { // get the byte data from the GPS gpsData = Serial2.read(); if (gps.encode(gpsData)) { getgps(gps); } }
}
这个示例的工作方式与项目 43 中的草图类似,不同之处在于它提取的是时间数据(始终为格林威治标准时间,也称为 UTC),而不是位置数据。在步骤 1 中,系统会提取时间。在步骤 2 中,您可以加上或减去一定小时数,将时钟调整为您所在时区的时间。然后,时间应清晰地格式化,并在步骤 3 的 LCD 屏幕上显示。图 15-7 展示了时钟的示例。

图 15-7:项目 44 正在进行中
项目 #45:记录移动物体随时间变化的位置
现在我们知道如何接收 GPS 坐标并将其转换为普通变量,我们可以利用这些信息结合第七章介绍的 microSD 或 SD 卡来构建 GPS 记录仪。我们的记录仪将通过记录 GPS 数据随时间变化来记录我们的位置。增加内存卡将使您能够记录汽车、卡车、船只或任何其他能够接收 GPS 信号的移动物体的运动;之后,您可以在计算机上查看这些信息。
硬件
如果您有 Arduino 的 GPS 屏蔽板,正如本章前面推荐的那样,所需的硬件与之前示例中使用的是相同的,只是可以移除 LCD 屏蔽板。如果您使用的是 GPS 接收模块,您将需要螺丝屏蔽板,以便连接 GPS 和 SD 卡模块。无论使用哪种方法,您都需要为这个项目提供外部电源。在我们的示例中,我们将记录时间、位置信息和估算的旅行速度。
草图
在组装好硬件后,输入并上传以下草图:
// Project 45 - Recording the Position of a Moving Object over Time
#include <TinyGPS.h>
#include <SoftwareSerial.h>
#include <SD.h>
// GPS TX to D2, RX to D3
SoftwareSerial Serial2(2, 3);
TinyGPS gps;void getgps(TinyGPS &gps);
byte gpsData;
void getgps(TinyGPS &gps)
{ float latitude, longitude; int year; byte month, day, hour, minute, second, hundredths; // create/open the file for writing File dataFile = SD.open("DATA.TXT", FILE_WRITE); // if the file is ready, write to it:1 if (dataFile) {2 gps.f_get_position(&latitude, &longitude); dataFile.print("Lat: "); dataFile.print(latitude, 5); dataFile.print(" "); dataFile.print("Long: "); dataFile.print(longitude, 5); dataFile.print(" "); // decode and display time data gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths); // correct for your time zone as in Project 44 hour = hour + 10; if (hour > 23) { hour = hour - 24; } if (hour < 10) { dataFile.print("0"); } dataFile.print(hour, DEC); dataFile.print(":"); if (minute < 10) { dataFile.print("0"); } dataFile.print(minute, DEC); dataFile.print(":"); if (second < 10) { dataFile.print("0"); } dataFile.print(second, DEC); dataFile.print(" "); dataFile.print(gps.f_speed_kmph());3 dataFile.println("km/h"); dataFile.close();4 delay(15000); // record a measurement around every 15 seconds } // if the file isn't ready, show an error: else { Serial.println("error opening DATA.TXT"); }
}
void setup()
{ Serial.begin(9600); Serial2.begin(9600); Serial.println("Initializing SD card..."); pinMode(10, OUTPUT); // check that the memory card exists and is usable if (!SD.begin(10)) { Serial.println("Card failed, or not present"); // stop sketch return; } Serial.println("memory card is ready");
}
void loop()
{ while (Serial2.available() > 0) { // get the byte data from the GPS gpsData = Serial2.read(); if (gps.encode(gpsData)) {5 getgps(gps); } }
}
这个示例使用了在项目 43 和 44 中相同的代码,在void loop()中接收 GPS 接收器的数据并传递给其他函数。在 5 处,GPS 接收器的数据被传入 TinyGPS 库以解码为有用的变量。在 1 处,检查存储卡以确认是否可以写入数据,在 2 到 3 处,将相关的 GPS 数据写入到 microSD 卡上的文本文件中。因为每次写入后文件都会关闭,你可以在不通知示例的情况下断开 Arduino 的电源,并且在插入或移除 microSD 卡之前应先断开电源。最后,你可以通过更改delay()函数中的值来设置数据记录之间的间隔。
运行示例
在操作 GPS 日志器后,生成的文本文件应类似于图 15-8。

图 15-8:项目 45 的结果
一旦你得到了这些数据,你可以手动将其输入到 Google Maps 中,逐点查看 GPS 日志器的行进路线。但更有趣的方法是将整个路线显示在一张地图上。为此,打开文本文件作为电子表格,分离位置数据,并添加一个标题行,如图 15-9 所示。然后将其保存为 .csv 文件。

图 15-9:捕获的位置信息
现在访问 GPS Visualizer 网站(www.gpsvisualizer.com/)。在“Get Started Now”框中,点击Choose File并选择你的数据文件。选择Google Maps作为输出格式,然后点击Map It。你的 GPS 日志器的运动轨迹应该会显示在类似于图 15-10 的地图上,你可以进一步调整并探索。

图 15-10:映射的 GPS 日志器数据
展望未来
如你所见,原本你可能认为复杂的事情,如使用 GPS 接收器,通过 Arduino 就能变得简单。继续沿着这个主题,下一章你将学会如何创建自己的无线数据链接并通过遥控控制设备。
第十六章:无线数据
在这一章节中,你将学习如何使用各种类型的无线传输硬件发送和接收指令和数据。具体来说,你将学习如何
-
使用低成本无线模块发送数字输出信号
-
创建一个简单且廉价的无线遥控系统
-
使用 LoRa 无线数据接收器和收发器
-
创建一个遥控温度传感器
使用低成本无线模块
使用两台由 Arduino 控制的系统之间的无线链接传送单向文本信息是很容易的,这两台系统配备了廉价的射频(RF)数据模块,例如图 16-1 所示的发射器和接收器模块。这些模块通常是成对出售的,通常被称为RF Link模块或套件。良好的例子包括 PMD Way 的 44910433 零件,或者 SparkFun 的 WRL-10534 和 WRL-10532 零件。在我们的项目中,我们将使用最常见的、工作在 433 MHz 射频上的模块类型。
图 16-2 中发射器底部的连接端口从左到右依次为:数据输入、5V 和 GND。外部天线的连接端口位于板子的右上角。天线可以是单根导线,或者如果传输距离较短,也可以完全省略。(每种品牌的模块可能会有所不同,因此在继续操作之前,请检查你特定设备的连接方式。)

图 16-1:RF Link 发射器和接收器套件

图 16-2:发射器 RF Link 模块
图 16-3 展示了接收器模块,它比发射器模块稍大。

图 16-3:接收器 RF Link 模块
接收器上的连接很简单:V+和 V−针脚分别连接到 5V 和 GND,DATA 连接到分配给接收数据的 Arduino 针脚。这些针脚通常标注在模块的另一侧。如果没有标注,或者你不确定,可以查看模块的数据手册或联系供应商。
在你可以使用这些模块之前,你还需要从www.airspayce.com/mikem/arduino/VirtualWire/下载并安装最新版本的 VirtualWire 库,安装方法在第七章中有详细说明。此库也包含在本书的草图下载文件中,下载链接是nostarch.com/arduino-workshop-2nd-edition/。安装完库之后,你就可以进入下一部分了。
项目 #46:创建一个无线遥控器
我们将远程控制两个数字输出:您将按下连接到一个 Arduino 板的按钮,以控制位于远处的另一个 Arduino 上的匹配数字输出引脚。这个项目将向您展示如何使用 RF Link 模块。您还将学会如何确定您能离多远并远程控制 Arduino。在使用这些模块执行更复杂任务之前,了解这一点非常重要。(在开阔地,您通常可以达到大约 100 米的距离,但在室内或模块之间有障碍物时,距离会更短。)
发射器电路硬件
发射器电路需要以下硬件:
-
Arduino 和 USB 电缆
-
AA 电池座和接线(如第十四章所用)
-
一个 433 MHz RF Link 发射模块
-
两个 10 kΩ 电阻(R1 和 R2)
-
两个 100 nF 电容(C1 和 C2)
-
两个按钮
-
一个面包板
发射器原理图
发射器电路由两个带有去抖电路的按钮组成,连接到数字引脚 2 和 3,以及之前描述过的发射模块(见图 16-4)。

图 16-4:项目 46 的发射器原理图
接收器电路硬件
接收器电路需要以下硬件:
-
Arduino 和 USB 电缆
-
AA 电池座和接线(如第十四章所用)
-
一个 433 MHz RF Link 接收模块
-
一个面包板
-
两个您选择的 LED
-
两个 560 Ω 电阻(R1 和 R2)
接收器原理图
接收器电路由两个数字引脚 6 和 7 上的 LED 和连接到数字引脚 8 的 RF Link 接收模块数据引脚组成,如图 16-5 所示。

图 16-5:项目 46 的接收器原理图
您可以将面包板、LED、限流电阻和接收模块替换为 Freetronics 433 MHz 接收器扩展板,如图 16-6 所示。

图 16-6:Freetronics 433 MHz 接收器扩展板
发射器草图
现在让我们来看一下发射器的草图。输入并上传以下草图到带有发射器电路的 Arduino:
// Project 46 - Creating a Wireless Remote Control, Transmitter Sketch1 #include <VirtualWire.h>
uint8_t buf[VW_MAX_MESSAGE_LEN];
uint8_t buflen = VW_MAX_MESSAGE_LEN;2 const char *on2 = "a";
const char *off2 = "b";
const char *on3 = "c";
const char *off3 = "d";
void setup()
{3 vw_set_ptt_inverted(true); // Required for RF Link modules vw_setup(300); // set data speed 4 vw_set_tx_pin(8); pinMode(2, INPUT); pinMode(3, INPUT);
}
void loop()
{5 if (digitalRead(2)==HIGH) { vw_send((uint8_t *)on2, strlen(on2)); // send data out to the world vw_wait_tx(); // wait a moment delay(200); } if (digitalRead(2)==LOW) {6 vw_send((uint8_t *)off2, strlen(off2)); vw_wait_tx(); delay(200); } if (digitalRead(3)==HIGH) { vw_send((uint8_t *)on3, strlen(on3)); vw_wait_tx(); delay(200); } if (digitalRead(3)==LOW) { vw_send((uint8_t *)off3, strlen(off3)); vw_wait_tx(); delay(200); }
}
我们在第 1 行引入了 VirtualWire 库,并在第 3 行使用它的函数来设置 RF Link 发射模块并设定数据传输速度。在第 4 行,我们设置了数字引脚 8,用于连接 Arduino 到发射模块的数据引脚,并控制数据传输速度。(如果需要,您可以使用其他数字引脚,但 0 和 1 会干扰串行线。)
发射器的草图读取连接到数字引脚 2 和 3 的两个按钮的状态,并将与按钮状态匹配的单个文本字符发送到 RF 链接模块。例如,当数字引脚 2 上的按钮为HIGH时,Arduino 发送字符a;当按钮为LOW时,发送字符b;当数字引脚 3 上的按钮为HIGH时,Arduino 发送字符c;当按钮为LOW时,发送字符d。这四种状态从 2 开始声明。
文本字符的传输通过四个部分的if语句来处理,从第 5 行开始,例如第 6 行的if-then语句。传输的变量被使用了两次,如这里使用on2所示:
vw_send((uint8_t *)on2, strlen(on2));
函数vw_send()发送变量on2的内容,但它需要知道变量的字符长度,因此我们使用strlen()来完成这一操作。
接收器草图
现在让我们添加接收器草图。输入并上传以下草图到 Arduino,并连接接收电路:
// Project 46 - Creating a Wireless Remote Control, Receiver Sketch
#include <VirtualWire.h>
uint8_t buf[VW_MAX_MESSAGE_LEN];
uint8_t buflen = VW_MAX_MESSAGE_LEN;
void setup()
{1 vw_set_ptt_inverted(true); // Required for RF Link modules vw_setup(300); 2 vw_set_rx_pin(8); vw_rx_start(); pinMode(6, OUTPUT); pinMode(7, OUTPUT);
}
void loop()
{3 if (vw_get_message(buf, &buflen)) {4 switch(buf[0]) { case 'a': digitalWrite(6, HIGH); break; case 'b': digitalWrite(6, LOW); break; case 'c': digitalWrite(7, HIGH); break; case 'd': digitalWrite(7, LOW); break; } }
}
与发射器电路一样,我们使用 VirtualWire 函数在第 1 行设置 RF 链接接收模块并设置数据传输速度。在第 2 行我们设置 Arduino 数字引脚,连接到该链接的数据输出引脚(引脚 8)。
当草图运行时,来自发射器电路的字符被 RF 链接模块接收并发送到 Arduino。vw_get_message()函数在第 3 行获取由 Arduino 接收的字符,这些字符通过第 4 行的switch case语句进行解析。例如,按下发射器电路上的按钮 S1 时,会发送字符a。该字符由接收器接收,并使数字引脚 6 设置为HIGH,点亮 LED。
你可以使用这一对简单的演示电路,通过将代码作为基本字符发送并由接收电路解释,从而为 Arduino 系统创建更复杂的控制。
使用 LoRa 无线数据模块实现更大范围和更快速度
当你需要比之前使用的基本无线模块提供更大范围和更快数据速度的无线数据链接时,LoRa 数据模块可能是一个合适的选择。LoRa 是“长距离”(long range)的缩写,这些模块具有低功耗并能够在长距离范围内工作。这些模块是收发器,即既能发送又能接收数据的设备,因此你不需要单独的发射器和接收器。使用 LoRa 模块的另一个好处是,不同类型的模块之间可以互相通信,使得你作为设计者能够创建从简单到复杂的控制和数据网络。在本章中,你将创建几个基础模块,可以根据需要扩展用于各种目的。

图 16-7:Arduino 用 LoRa 扩展板
为了方便起见,我们将使用两个 Arduino 的 LoRa 扩展板,例如 PMD Way 型号 14290433,如图 16-7 所示。
购买 LoRa 扩展板时,你需要选择一个工作频率。正确的频率会根据你使用的国家/地区而有所不同。这样可以确保你的数据传输不会干扰到你所在地区的其他设备。LoRa 产品有三种工作频段:
-
433 MHz 用于美国和加拿大
-
868 MHz 用于英国和欧洲
-
915 MHz 用于澳大利亚和新西兰
你可以在www.thethingsnetwork.org/docs/lorawan/frequencies-by-country.html找到各国的频率范围完整列表。
最后,你需要下载并安装 Arduino 库,库文件可以在github.com/sandeepmistry/arduino-LoRa/archive/master.zip找到。
项目 #47:通过 LoRa 无线进行远程控制
这个项目将演示从一个配备 LoRA 的 Arduino 到另一个的简单数据传输,用于远程控制数字输出引脚。我们的发射器有两个按钮,用于打开和关闭接收电路的输出引脚。
发射器电路硬件
发射器电路所需的硬件如下:
-
Arduino 和 USB 电缆
-
Arduino 用 LoRa 扩展板
-
两个 10 kΩ 电阻 (R1 和 R2)
-
两个 100 nF 电容 (C1 和 C2)
-
两个按钮
-
AA 电池座和接线(在第十四章中使用)
发射器原理图
如图 16-8 所示,发射器电路由两个带有去抖动电路的按钮组成,分别连接到数字引脚 2 和 3。LoRa 扩展板安装在 Arduino Uno 上。一旦草图上传完成,电源由 AA 电池座和接线提供。

图 16-8:项目 47 的发射器原理图
在使用你的 LoRa 扩展板之前,需移除三个插针跳线,如图 16-9 所示。如果不移除它们,它们将干扰其他数字引脚。你可以完全移除它们,或者仅将插针连接到两个引脚之一。

图 16-9:从 LoRa 扩展板上移除的插针跳线
接收器电路硬件
接收器电路所需的硬件如下:
-
Arduino 和 USB 电缆
-
Arduino 用 LoRa 扩展板
-
一个 LED
-
一个 560 Ω 电阻 (R1)
接收器原理图
接收器电路,如图 16-10 所示,由一个 LED 和一个限流电阻组成,电阻连接在数字引脚 7 和 GND 之间。我们将其通过 USB 连接到 PC,因此不需要外部电源。

图 16-10:项目 47 的接收器原理图
发射器草图
现在让我们来查看发射器的草图。输入并上传以下草图到带有发射器电路的 Arduino:
// Project 47 - Remote Control over LoRa Wireless, Transmitter Sketch1 #define LORAFREQ (915000000L)2 #include <LoRa.h>
#include <SPI.h>3 void loraSend(int controlCode)
{4 LoRa.beginPacket(); // start sending data LoRa.print("ABC"); // "ABC" is our three-character code for receiver LoRa.print(controlCode); // send our instructions (controlCode codes)5 LoRa.endPacket(); // finished sending data5 LoRa.receive(); // start listening
}
void setup()
{ pinMode(4, INPUT); // on button pinMode(3, INPUT); // off button6 LoRa.begin(LORAFREQ); // start up LoRa at specified frequency
}
void loop()
{ // check for button presses to control receiver if (digitalRead(4) == HIGH) { loraSend(1); // '1' is code for turn receiver digital pin 5 HIGH delay(500); // allow time to send } if (digitalRead(3) == HIGH) { loraSend(0); // '0' is code for turn receiver digital pin 5 LOW delay(500); // allow time to send }
}
操作频率在第 1 行选择。我们的示例使用的是 915 MHz,因此根据你的国家和无线电盾牌,你可能需要将其更改为433000000L或868000000L。我们在第 2 行包含了 Arduino LoRa 库,并在第 4 行激活它。SPI 库也被包含,因为 LoRa 盾牌使用 SPI 总线与 Arduino 通信。在第 6 行,LoRa 收发器在适当的频率下被激活,数字引脚也被准备好作为按钮的输入。
在第 3 行,我们有一个自定义函数loraSend(int controlCode)。按下按钮时,会调用此函数。它首先发送一个三字符代码——在本例中是ABC——通过 LoRa 无线电波发送,然后发送控制代码。字符代码允许你将控制发送到特定的接收器电路。否则,如果你使用两个或更多接收器,就会混淆由哪个接收器接收发射器的控制信号。你将看到,接收器只有在收到ABC时才会行动。我们示例中的控制代码是1和0(分别用于打开或关闭接收器的数字输出)。
在第 4 行,LoRa 模块被切换到发送模式,然后通过无线电波发送字符和控制代码。在第 5 行,LoRa 模块停止发送并切换回接收数据。上传了发射器草图后,可以将发射器硬件从计算机上断开,并通过电池组供电。
接收器草图
现在让我们来看看接收器草图。输入并上传以下草图到带有接收器电路的 Arduino 中:
// Project 47 - Remote Control over LoRa Wireless, Receiver Sketch1 #define LORAFREQ (915000000L)2 #include <LoRa.h>
#include <SPI.h>
void takeAction(int packetSize)
// things to do when data received over LoRa wireless
{3 char incoming[4] = ""; int k; for (int i = 0; i < packetSize; i++) { k = i; if (k > 6) { k = 6; // make sure we don't write past end of string } incoming[k] = (char)LoRa.read();4 } // check the three-character code sent from transmitter is correct5 if (incoming[0] != 'A') { return; // if not 'A', stop function and go back to void loop() }5 if (incoming[1] != 'B') { return; // if not 'B', stop function and go back to void loop() }5 if (incoming[2] != 'C') { return; // if not 'C', stop function and go back to void loop() } // If made it this far, correct code has been received from transmitter. // Now to do something... if (incoming[3] == '1') { digitalWrite(7, HIGH); } if (incoming[3] == '0') { digitalWrite(7, LOW); }}
void setup()
{ pinMode(7, OUTPUT);6 LoRa.begin(LORAFREQ); // start up LoRa at specified frequency7 LoRa.onReceive(takeAction); // call function "takeAction" when data received 8 LoRa.receive(); // start receiving
}
void loop()
{
}
我们再次在第 2 行包含了 Arduino LoRa 库,并在第 6 行激活它。操作频率也在第 1 行选择。我们的示例使用的是 915 MHz,因此根据你的国家和无线电盾牌,你可能需要将其更改为433000000L或868000000L。同时也包含了 SPI 库,因为 LoRa 盾牌使用 SPI 总线与 Arduino 通信。在第 7 行,我们告诉草图在接收到数据时运行某个函数——在本例中是void takeAction()。然后在第 8 行,LoRa 模块被切换到接收模式。
在运行时,接收器只是等待 LoRa 模块接收到数据。此时,takeAction()函数会被调用。它将从发射器接收的每个字符放入一个名为incoming[4]的字符数组中,位置在第 3 行到第 4 行之间。接下来,接收器检查代码的每个字符(在我们的示例中是ABC),以确保传输是针对这个特定接收器的。最后,如果检查成功,控制字符会被检查。如果它是1,数字引脚 7 会被设置为HIGH,如果是0,数字引脚 7 会被设置为LOW。
现在你已经有了一个远程控制的基本框架。此外,通过为多个接收器分配不同的字符代码,你可以将系统扩展为通过一个发射器控制多个接收单元。
然而,对于一些重要的应用,你可能希望确认发射机的指令已被接收机成功执行,因此我们将在下一个项目中添加确认功能。
项目#48:通过 LoRa 无线遥控并进行确认
该项目为第 47 号项目中创建的接收机-发射机系统添加了确认功能,形成了一个双向数据系统。发射机电路上的 LED 将在接收机输出设置为HIGH时点亮,而在接收机输出设置为LOW时熄灭。
发射机电路硬件
发射机电路所需的硬件如下:
-
Arduino 及 USB 电缆
-
Arduino 的 LoRa 扩展板
-
两个 10 kΩ电阻(R1 和 R2)
-
一个 560 Ω电阻(R3)
-
一个 LED
-
两个 100 nF 电容(C1 和 C2)
-
两个按键
-
AA 电池座及接线(如第十四章所用)
发射机电路图
如图 16-11 所示,发射机电路由两个带去抖动电路的按键组成,这些按键连接到数字引脚 3 和 4;LED 和限流电阻连接到数字引脚 6。LoRa 扩展板安装在 Arduino Uno 上。一旦草图上传完成,电源由 AA 电池座和接线提供。

图 16-11:第 48 号项目的发射机电路图
本项目的接收机电路和电路图与第 47 号项目中的相同。
发射机草图
现在,让我们来查看发射机的草图。输入并上传以下草图到配有发射机电路的 Arduino 中:
// Project 48 - Remote Control over LoRa Wireless with Confirmation,
// Transmitter Sketch
#define LORAFREQ (915000000L)
#include <SPI.h>
#include <LoRa.h>
void loraSend(int controlCode)
{ LoRa.beginPacket(); // start sending data LoRa.print("DEF"); // "DEF" is our three-character code for the receiver. // Needs to be matched on RX. LoRa.print(controlCode); // send our instructions (controlCode codes) LoRa.endPacket(); // finished sending data LoRa.receive(); // start listening
}1 void takeAction(int packetSize)
// things to do when data received over LoRa wireless
{ char incoming[4] = ""; int k; for (int i = 0; i < packetSize; i++) { k = i; if (k > 6) { k = 18; // make sure we don't write past end of string } incoming[k] = (char)LoRa.read(); } // check the three-character code sent from receiver is correct if (incoming[0] != 'D') { return; // if not 'D', stop function and go back to void loop() } if (incoming[1] != 'E') { return; // if not 'E', stop function and go back to void loop() } if (incoming[2] != 'F') { return; // if not 'F', stop function and go back to void loop() } // If made it this far, correct code has been received from receiver. // Now to do something...2 if (incoming[3] == '1') { digitalWrite(6, HIGH); // receiver has turned output on and has sent a signal confirming this }2 if (incoming[3] == '0') { digitalWrite(6, LOW); // receiver has turned output off and has sent a signal confirming this }
}
void setup()
{ pinMode(4, INPUT); // on button pinMode(3, INPUT); // off button pinMode(6, OUTPUT); // status LED LoRa.begin(LORAFREQ); // start up LoRa at specified frequency LoRa.onReceive(takeAction); // call function "takeAction" when data received // over LoRa wireless
}
void loop()
{ // check for button presses to control receiver if (digitalRead(4) == HIGH) { loraSend(1); // '1' is code for turn receiver digital pin 7 HIGH delay(500); // button debounce } if (digitalRead(3) == HIGH) { loraSend(0); // '0' is code for turn receiver digital pin 7 LOW delay(500); // button debounce }
}
我们的发射机电路与第 47 号项目中的工作方式相同,首先发送一个字符编码用于识别,然后发送一个控制编码来打开或关闭接收机的输出。然而,在这个项目中,发射机会监听接收机的信号,一旦接收机完成了来自发射机的控制指令,接收机会将字符编码和控制编码返回给发射机。
因此,在 1 时,我们新增了一个函数takeAction(),用于检查来自接收机电路的字符编码DEF。接收机在其输出引脚打开时发送1,在输出关闭时发送0。我们的发射机电路可以通过控制数字引脚 6 上的 LED 来显示此状态,方法是通过 2 中的代码。
接收机草图
最后,让我们来看一下接收机的草图。输入并上传以下草图到配有接收机电路的 Arduino 中:
// Project 48 - Remote Control over LoRa Wireless with Confirmation, Receiver
// Sketch
#define LORAFREQ (915000000L)
#include <SPI.h>
#include <LoRa.h>
void loraSend(int controlCode){ LoRa.beginPacket(); // start sending data LoRa.print("DEF"); // "DEF" is our three-character code for the // transmitter LoRa.print(controlCode); // send our instructions (controlCode codes) LoRa.endPacket(); // finished sending data LoRa.receive(); // start listening
}
void takeAction(int packetSize)
// things to do when data received over LoRa wireless
{ char incoming[4] = ""; int k; for (int i = 0; i < packetSize; i++) { k = i; if (k > 6) { k = 18; // make sure we don't write past end of string } incoming[k] = (char)LoRa.read(); } // check the three-character code sent from transmitter is correct if (incoming[0] != 'A') { return; // if not 'A', stop function and go back to void loop() } if (incoming[1] != 'B') { return; // if not 'B', stop function and go back to void loop() } if (incoming[2] != 'C') { return; // if not 'C', stop function and go back to void loop() } // If made it this far, correct code has been received from transmitter. // Now to do something... if (incoming[3] == '1') { digitalWrite(7, HIGH);1 loraSend(1); // tell the transmitter that the output has been turned on } if (incoming[3] == '0') { digitalWrite(7, LOW);1 loraSend(0); // tell the transmitter that the output has been turned off }
}
void setup()
{ pinMode(7, OUTPUT); LoRa.begin(LORAFREQ); // start up LoRa at specified frequency LoRa.onReceive(takeAction); // call function "takeAction" when data received // over LoRa wireless LoRa.receive(); // start receiving
}
void loop()
{
}
我们的接收机工作原理与第 47 号项目中的接收机相同,唯一不同的是,接收机会将字符编码DEF发送回发射机,接着发送1或0,以指示输出引脚是否被打开或关闭。这一操作通过loraSend()函数在 1 时完成。
到此为止,你已经有了两个示例项目,展示了如何不仅能在比之前的项目更远的距离上无线控制数字输出引脚,还能确认操作是否已发生。现在你可以在这些示例的基础上扩展,创建你自己的遥控项目。不过接下来,我们将尝试通过 LoRa 无线链路发送传感器数据,项目 49 将展示这一实验。
项目#49:通过 LoRa 无线发送远程传感器数据
这个项目基于我们之前的工作,通过计算机请求远程传感器的温度数据。
发射器电路硬件
发射器电路所需的硬件如下:
-
Arduino 和 USB 电缆
-
Arduino 的 LoRa 扩展板
本项目使用 PC 上的串口监视器进行控制,因此发射器电路仅包括 Arduino 和 LoRa 扩展板,通过 USB 电缆连接到 PC。
接收器电路硬件
接收器电路所需的硬件如下:
-
Arduino 和 USB 电缆
-
Arduino 的 LoRa 扩展板
-
TMP36 温度传感器
-
无焊接面包板
-
Arduino 的外部电源
-
公对公跳线
接收器原理图
我们的电路简单地将 TMP36 温度传感器连接到模拟引脚 A0,并将 LoRa 扩展板放置在 Arduino 上,如图 16-12 所示。

图 16-12:项目 49 的接收器原理图
接收器电路可能距离计算机较远,因此你可以使用 USB 电源或之前项目中使用的电池解决方案。
发射器草图
现在,让我们检查一下发射器的草图。将以下草图输入并上传到 Arduino 与发射器电路:
// Project 49 - Sending Remote Sensor Data Using LoRa Wireless, Transmitter
// Sketch
#define LORAFREQ (915000000L)
#include <SPI.h>
#include <LoRa.h>
char command;
void loraSend(int controlCode)
{ LoRa.beginPacket(); // start sending data1 LoRa.print("ABC"); // "ABC" is our three-character code for the // transmitter LoRa.print(controlCode); // send our instructions (controlCode codes) LoRa.endPacket(); // finished sending data LoRa.receive(); // start listening
}
void takeAction(int packetSize)
// send text received from sensor Arduino via LoRa to Serial Monitor{ char incoming[31] = ""; int k; for (int i = 0; i < packetSize; i++) { k = i; if (k > 31) { k = 31; // make sure we don't write past end of string } incoming[k] = (char)LoRa.read(); Serial.print(incoming[k]); // display temp information from sensor board } Serial.println();
}
void setup()
{2 LoRa.begin(LORAFREQ); // start up LoRa at specified frequency LoRa.onReceive(takeAction); // call function "takeAction" when data received // over LoRa wireless LoRa.receive(); // start receiving Serial.begin(9600);
}
void loop()
{3 Serial.print("Enter 1 for Celsius or 2 for Fahrenheit then Enter: "); Serial.flush(); // clear any "junk" out of the serial buffer before waiting4 while (Serial.available() == 0) { // do nothing until something enters the serial buffer } while (Serial.available() > 0) { command = Serial.read() - '0'; // read the number in the serial buffer, // remove the ASCII text offset for zero: '0' } Serial.println();5 loraSend(command); delay(2000);
}
与本章之前的项目一样,我们在 2 处初始化 LoRa 硬件和串口监视器。然而,这次我们不再使用硬件按钮,而是通过串口监视器接受用户命令,并将这些命令发送到接收器硬件。在本项目中,用户需要在串口监视器的输入框中输入1或2,分别获取来自接收器硬件的摄氏度或华氏度温度。这一操作发生在 3 处。计算机在 4 处等待用户输入,然后通过loraSend()在 5 处将相应命令发送给接收器硬件。同样,我们使用一个三字符代码确保传输仅限于接收器板 1。
接收器草图
现在,让我们检查接收器的草图。将以下草图输入并上传到 Arduino 与接收器电路:
// Project 49 - Sending Remote Sensor Data Using LoRa Wireless, Receiver
// Sketch
#define LORAFREQ (915000000L)
#include <SPI.h>
#include <LoRa.h>
float sensor = 0;
float voltage = 0;
float celsius = 0;
float fahrenheit = 0;
void loraSendC()
{ LoRa.beginPacket(); // start sending data sensor = analogRead(0); voltage = ((sensor * 5000) / 1024); voltage = voltage - 500; celsius = voltage / 10; fahrenheit = ((celsius * 1.8) + 32);1 LoRa.print("Temperature: "); LoRa.print(celsius, 2); LoRa.print(" degrees C");2 LoRa.endPacket(); // finished sending data LoRa.receive(); // start listening
}
void loraSendF()
// send temperature in Fahrenheit
{ LoRa.beginPacket(); // start sending data sensor = analogRead(0); voltage = ((sensor * 5000) / 1024); voltage = voltage - 500; celsius = voltage / 10; fahrenheit = ((celsius * 1.8) + 32);1 LoRa.print("Temperature: "); LoRa.print(fahrenheit, 2); LoRa.print(" degrees F");2 LoRa.endPacket(); // finished sending data LoRa.receive(); // start listening
}
void takeAction(int packetSize)
// things to do when data received over LoRa wireless
{ char incoming[6] = ""; int k; for (int i = 0; i < packetSize; i++) { k = i; if (k > 6) { k = 6; // make sure we don't write past end of string } incoming[k] = (char)LoRa.read(); }3 // check the three-character code sent from transmitter is correct if (incoming[0] != 'A') { return; // if not 'A', stop function and go back to void loop() } if (incoming[1] != 'B') { return; // if not 'B', stop function and go back to void loop() } if (incoming[2] != 'C') { return; // if not 'C', stop function and go back to void loop() } // If made it this far, correct code has been received from transmitter if (incoming[3] == '1') {4 loraSendC(); } if (incoming[3] == '2') {5 loraSendF(); }
}
void setup()
{ LoRa.begin(LORAFREQ); // start up LoRa at specified frequency LoRa.onReceive(takeAction); // call function "takeAction" when data received LoRa.receive(); // start receiving
}
void loop()
{
}
使用与项目 48 相同的方法,我们的接收器硬件解码从发射器传输来的信号,以确保数据是发送给它的,通过检查在 3 处发送的字符代码。如果这是正确的,接收器板会分别在 4 或 5 调用loraSendC()或loraSendF()中的一个。这两个函数计算 TMP36 传感器的温度,并在 1 和 2 之间将包含温度和测量类型的文本字符串发送回发射器板。
一旦你组装好两个电路的硬件并上传了两个草图,将带电的接收器电路(包括传感器)放置在你希望从计算机测量温度的位置。确保发射器电路已连接到计算机。在 IDE 中打开串口监视器,并按照指示检查温度。如图 16-13 所示为示例。

图 16-13:项目 49 的示例输出
展望未来
本章展示了远程控制多 Arduino 系统是多么简单。例如,你可以通过从一个 Arduino 向另一个 Arduino 发送字符来控制数字输出,使用 LoRa 无线技术创建更复杂的多 Arduino 控制系统,并且可以包括数据返回。通过你迄今为止获得的知识,许多创意选项都已向你开放。
但在无线数据传输方面仍有许多内容需要探讨,因此,在下一章学习如何使用简单的电视遥控器与 Arduino 配合时,继续阅读并跟随示例进行实践。
第十七章:红外遥控器
在本章中,你将会:
-
创建并测试一个简单的红外接收器
-
遥控 Arduino 数字输出引脚
-
将遥控系统添加到我们在第十四章中创建的机器人车辆中
正如你将看到的,借助一个便宜的接收器模块,你的 Arduino 能够接收来自红外遥控器的信号并做出响应。
什么是红外线?
许多人在日常生活中使用红外遥控器,但大多数人并不知道它们是如何工作的。红外线(IR)信号是无法用肉眼看到的光束。所以下次你看遥控器上小小的 LED 并按下一个按钮时,你并不会看到 LED 亮起。
这是因为红外遥控器包含一个或多个特殊的红外光生成 LED,它们用于发射红外信号。当你按下遥控器上的一个按钮时,LED 会反复开关,以独特的模式传输每个按钮的信号。这个信号被被控制设备上的特殊红外接收器接收,并转换为电脉冲,接收器的电子设备会读取这些数据。如果你对这些模式感兴趣,可以通过手机相机或数码相机的取景器查看遥控器上的红外 LED 来观察这些模式。
设置红外接收
在继续之前,我们需要安装 Arduino IRremote 库,因此请访问github.com/z3t0/Arduino-IRremote/archive/master.zip下载所需的文件,并使用第七章中描述的方法安装。
红外接收器
下一步是设置红外接收器并测试它是否正常工作。你可以选择独立的红外接收器(如图 17-1 所示)或预接线模块(如图 17-2 所示),选择最适合你的方式。

图 17-1:红外接收器

图 17-2:预接线红外接收器模块
如图 17-1 所示,独立的红外接收器是 Vishay TSOP4138。接收器的底部引脚(如图所示)连接到 Arduino 数字引脚,中间引脚连接到 GND,顶部引脚连接到 5V。
图 17-2 显示了一个预接线的红外模块。预接线接收器模块可以从 PMD Way 及其他零售商处购买。使用这些模块的好处是,它们配有连接线,并且标有标签,便于参考。
无论你选择哪种模块,在接下来的所有示例中,你都会将 D(数据线)连接到 Arduino 的数字引脚 2,将 VCC 连接到 5V,将 GND 连接到 GND。
遥控器
最后,你需要一个遥控器。我使用了像图 17-3 中显示的索尼电视遥控器。如果你没有索尼遥控器,任何便宜的通用遥控器都可以使用,前提是将其重置为索尼代码。有关如何操作,请参阅遥控器附带的说明。

图 17-3:典型的 Sony 遥控器
测试草图
现在,让我们确保一切正常工作。在将 IR 接收器连接到 Arduino 后,输入并上传 列表 17-1 中的草图。
// Listing 17-11 #include <IRremote.h> // use the library 2 IRrecv irrecv(receiverpin); // create instance of irrecv3 decode_results results;
int receiverpin = 2; // pin 1 of IR receiver to Arduino digital pin 2
void setup()
{ Serial.begin(9600); irrecv.enableIRIn(); // start the IR receiver
}
void loop()
{4 if (irrecv.decode(&results)) // have we received an IR signal? { 5 Serial.print(results.value, HEX); // display IR code in the Serial Monitor Serial.print(" "); irrecv.resume(); // receive the next value }
}
列表 17-1:IR 接收器测试
这个草图相对简单,因为大部分工作都由 IR 库在后台完成。在第 4 行,我们检查是否接收到遥控器的信号。如果接收到信号,它将在第 5 行以十六进制显示在串行监视器中。第 1、2 和 3 行激活 IR 库,并创建一个红外库函数的实例,以便在草图的其余部分进行引用。
测试设置
上传草图后,打开串行监视器,将遥控器对准接收器,开始按下按钮。每次按下按钮后,您应该会在串行监视器中看到按钮的代码。例如,图 17-4 显示了分别按下 1、2 和 3 后的结果。

图 17-4:运行 列表 17-1 中的代码后按下按钮的结果
表 17-1 列出了我们将在后续草图中使用的基本 Sony 遥控器的代码。然而,在运行 列表 17-1 时,请注意每个代码数字会重复三次。这是 Sony 红外系统的一个特性,每次按下按钮时,系统会发送三次代码。通过一些巧妙的编程,您可以忽略这些重复代码,但现在我们跳到下一个项目,进行遥控操作。
表 17-1:示例 Sony IR 代码
| 按钮 | 代码 | 按钮 | 代码 |
|---|---|---|---|
| 电源 | A90 | 7 | 610 |
| 静音 | 290 | 8 | E10 |
| 1 | 10 | 9 | 110 |
| 2 | 810 | 0 | 910 |
| 3 | 410 | 音量增加 | 490 |
| 4 | C10 | 音量减少 | C90 |
| 5 | 210 | 渠道增加 | 90 |
| 6 | A10 | 渠道减少 | 890 |
项目 #50:创建一个 IR 遥控器 Arduino
本项目将演示如何使用 IR 遥控器控制数字输出引脚。您将使用 Sony 遥控器上的数字按钮 3 到 7 控制数字引脚 3 到 7。当您按下遥控器上的按钮时,相应的数字输出引脚将变为 HIGH 持续 1 秒钟,然后恢复为 LOW。您可以使用该项目作为基础或指南,将遥控控制功能添加到其他项目中。
硬件
本项目所需的硬件如下:
-
Arduino 和 USB 电缆
-
五个 LED
-
五个 560 Ω 电阻
-
红外接收器或模块
-
无焊面包板
-
各种跳线
原理图
电路由红外接收器组成,输出连接到数字引脚 2,五个 LED 通过限流电阻连接到数字引脚 3 到 7(包括 7),如 图 17-5 所示。

图 17-5:项目 50 的原理图
草图
输入并上传以下草图:
// Project 50 – Creating an IR Remote Control Arduino
#include <IRremote.h>
IRrecv irrecv(receiverpin); // create instance of irrecv
decode_results results;
int receiverpin = 2; // pin 1 of IR receiver to Arduino digital pin 2
void setup()
{ irrecv.enableIRIn(); // start the receiver for (int z = 3 ; z < 8 ; z++) // set up digital pins { pinMode(z, OUTPUT); }
}1 void translateIR()
// takes action based on IR code received
// uses Sony IR codes
{ switch(results.value) {2 case 0x410: pinOn(3); break; // 3 case 0xC10: pinOn(4); break; // 4 case 0x210: pinOn(5); break; // 5 case 0xA10: pinOn(6); break; // 6 case 0x610: pinOn(7); break; // 7 }
}3 void pinOn(int pin) // turns on digital pin "pin" for 1 second
{ digitalWrite(pin, HIGH); delay(1000); digitalWrite(pin, LOW);
}
void loop()
{4 if (irrecv.decode(&results)) // have we received an IR signal? { translateIR();5 for (int z = 0 ; z < 2 ; z++) // ignore the 2nd and 3rd repeated codes { irrecv.resume(); // receive the next value } }
}
这个草图有三个主要部分。首先,它在 4 的位置等待来自遥控器的信号。当信号被接收后,它会在 1 的位置通过函数translateIR()测试该信号,以确定按下的是哪个按钮以及应采取什么操作。
请注意在 2 的位置,我们比较了 IR 库返回的十六进制代码。这些代码是通过在清单 17-1 中进行的测试返回的。当接收到按钮 3 到按钮 7 的代码时,会调用 3 中的函数pinOn(),它会点亮匹配的数字引脚,持续 1 秒钟。
如前所述,索尼遥控器会为每个按钮按下发送三次代码,因此我们在 5 的位置使用一个小循环来忽略第二次和第三次的代码。最后,请注意在 2 的位置的case语句中,十六进制数字前面添加了0x。
修改草图
你可以通过测试更多按钮来扩展可用的选项或控制功能,从而控制你的接收设备。为此,使用清单 17-1 来确定每个按钮产生的代码,然后将每个新代码添加到switch case语句中。
项目 #51:创建 IR 遥控机器人车辆
为了向你展示如何将 IR 遥控器集成到现有项目中,我们将在第十四章的第 39 个项目中添加 IR 功能。在这个项目中,草图将展示如何用简单的索尼电视遥控器来控制机器人,而不是预设机器人的方向和距离。
硬件
所需的硬件与在第 39 个项目中为机器人构建时所需的硬件相同,只是在本章前面描述的 IR 接收器模块。此外,在以下草图中,机器人将根据你按下遥控器上的按钮作出响应:按 2 前进,按 8 后退,按 4 左转,按 6 右转。
草图
在重新组装你的车辆并添加 IR 接收器后,输入并上传以下草图:
// Project 51 - Creating an IR Remote Control Robot Vehicle
int receiverpin = 2; // pin 1 of IR receiver to Arduino digital pin 11
#include <IRremote.h>
IRrecv irrecv(receiverpin); // create instance of irrecvdecode_results results;
#include <AFMotor.h>
AF_DCMotor motor1(1); // set up instances of each motor
AF_DCMotor motor2(2);
AF_DCMotor motor3(3);
AF_DCMotor motor4(4);
void goForward(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(FORWARD); motor2.run(FORWARD); motor3.run(FORWARD); motor4.run(FORWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void goBackward(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(BACKWARD); motor2.run(BACKWARD); motor3.run(BACKWARD); motor4.run(BACKWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void rotateLeft(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(FORWARD); motor2.run(BACKWARD); motor3.run(BACKWARD); motor4.run(FORWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
void rotateRight(int speed, int duration)
{ motor1.setSpeed(speed); motor2.setSpeed(speed); motor3.setSpeed(speed); motor4.setSpeed(speed); motor1.run(BACKWARD); motor2.run(FORWARD); motor3.run(FORWARD); motor4.run(BACKWARD); delay(duration); motor1.run(RELEASE); motor2.run(RELEASE); motor3.run(RELEASE); motor4.run(RELEASE);
}
// translateIR takes action based on IR code received, uses Sony IR codes
void translateIR()
{ switch (results.value) { case 0x810: goForward(255, 250); break; // 2 case 0xC10: rotateLeft(255, 250); break; // 4 case 0xA10: rotateRight(255, 250); break; // 6 case 0xE10: goBackward(255, 250); break; // 8 }
}
void setup()
{ delay(5000); irrecv.enableIRIn(); // start IR receiver
}
void loop()
{ if (irrecv.decode(&results)) // have we received an IR signal? { translateIR(); for (int z = 0 ; z < 2 ; z++) // ignore the repeated codes { irrecv.resume(); // receive the next value } }
}
这个草图应该对你来说有些熟悉。基本上,它不是点亮数字引脚上的 LED,而是调用第十四章机器人车辆中使用的电机控制函数。
展望未来
完成本章项目后,你应该已经理解如何通过红外遥控设备向你的 Arduino 发送命令。凭借这些技能和你在前几章中学到的知识,你现在可以用遥控器代替物理输入形式,如按钮。
但乐趣并未就此结束。在接下来的章节中,我们将使用 Arduino 来控制一些东西,这对未经训练的眼睛来说是既迷人又充满未来感的:射频识别系统。
第十八章:读取 RFID 标签
在本章中,你将
-
学习如何用 Arduino 实现 RFID 读卡器
-
查看如何将变量保存在 Arduino EEPROM 中
-
设计基于 Arduino 的 RFID 访问控制系统框架
射频识别(RFID) 是一种无线系统,通过电磁场将数据从一个物体传输到另一个物体,两个物体之间无需接触。你可以构建一个 Arduino,读取常见的 RFID 标签和卡片,用于创建访问控制系统和控制数字输出。你可能曾经使用过 RFID 卡,比如用于解锁门的访问卡,或者在公交车上刷的公共交通卡。图 18-1 展示了一些 RFID 标签和卡片的示例。

图 18-1:RFID 设备示例
RFID 设备内部
RFID 标签内部有一个微小的集成电路,带有内存,可以被专门的读卡器访问。大多数标签内部没有电池;相反,RFID 读卡器中的线圈天线会广播一束电磁能量到标签。标签吸收这些能量并用它为自己的电路提供动力,从而向 RFID 读卡器广播响应。图 18-2 展示了我们在本章中使用的 RFID 读卡器的天线线圈。

图 18-2:我们的 RFID 读卡器
本章使用的读卡器来自 PMD Way(部件号 113990014)。它便宜且易于使用,工作频率为 125 kHz;确保购买两个或更多匹配该频率的 RFID 标签,例如可以在pmdway.com/collections/rfid-tags/找到的标签。
测试硬件
在本节中,你将连接 RFID 读卡器到 Arduino。然后,你将通过一个简单的示例程序来测试其是否正常工作,该程序读取 RFID 卡并将数据发送到串行监视器。为了避免 PC 和 Arduino 之间串行端口的冲突,RFID 将连接到其他数字引脚,并使用 SoftwareSerial,就像我们在第十五章中对 GPS 接收模块所做的那样。
原理图
图 18-3 显示了 RFID 模块连接的示意图,视图来自模块的顶部。

图 18-3:RFID 模块连接图
测试原理图
要建立 RFID 读卡器与 Arduino 之间的连接,请按照以下步骤操作,使用母对公跳线:
-
将随附的线圈插头连接到 RFID 读卡器板底左侧的天线引脚。这些引脚没有极性,可以任意连接。
-
将读卡器的 GND(引脚 2)连接到 Arduino 的 GND。
-
将读卡器的 5V(引脚 1)连接到 Arduino 的 5V。
-
将读卡器的 RX(引脚 4)连接到 Arduino 的 D3 引脚。
-
将读卡器的 TX(引脚 5)连接到 Arduino 的 D2 引脚。
测试示例
输入并上传清单 18-1。
// Listing 18-1
#include <SoftwareSerial.h>
SoftwareSerial Serial2(2, 3);
int data1 = 0;
void setup(){ Serial.begin(9600); Serial2.begin(9600);
}
void loop()
{ if (Serial2.available() > 0) { data1 = Serial2.read(); // display incoming number Serial.print(" "); Serial.print(data1, DEC); }
清单 18-1:RFID 测试示例
显示 RFID 标签 ID 号
打开串口监视器窗口,将 RFID 标签在天线前摆动。结果应该类似于图 18-4。

图 18-4:Listing 18-1 的示例输出
注意,串口监视器窗口中显示了 14 个数字。这些数字共同构成了 RFID 标签的唯一 ID 号,我们将在未来的草图中使用它来识别被读取的标签。扫描你所有的 RFID 标签并记录它们的 ID 号,因为你将在接下来的几个项目中需要它们。
项目#52:创建一个简单的 RFID 控制系统
现在,让我们将 RFID 系统投入使用。在这个项目中,你将学习如何在读取到两个正确 RFID 标签之一时触发一个 Arduino 事件。草图会存储两个 RFID 标签编号;当一个 ID 与其中之一匹配的卡片被读取时,它会在串口监视器中显示接受。如果读取到一个 ID 不匹配存储 ID 的卡片,则串口监视器会显示拒绝。我们将以此为基础,向现有项目中添加 RFID 控制功能。
草图
输入并上传以下草图。然而,在第 1 和第 2 行,将数组中的x替换为你在上一节中记录的两个 RFID 标签的数字。(我们在第六章讨论过数组。)
// Project 52 – Creating a Simple RFID Control System
#include <SoftwareSerial.h>
SoftwareSerial Serial2(2, 3);
int data1 = 0;
int ok = -1;
// use Listing 18-1 to find your tags' numbers1 int tag1[14] = {*x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*};2 int tag2[14] = {*x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*};
int newtag[14] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
// used for read comparisons3 boolean comparetag(int aa[14], int bb[14])
{ boolean ff = false; int fg = 0; for (int cc = 0 ; cc < 14 ; cc++) { if (aa[cc] == bb[cc]) { fg++; } } if (fg == 14) { ff = true; } return ff;
}4 void checkmytags() // compares each tag against the tag just read
{ ok = 0; // This variable supports decision making. // If it is 1, we have a match; 0 is a read but no match, // -1 is no read attempt made. if (comparetag(newtag, tag1) == true) {5 ok++; } if (comparetag(newtag, tag2) == true) {6 ok++; }
}
void setup(){ Serial.begin(9600); Serial2.begin(9600); Serial2.flush(); // need to flush serial buffer // otherwise first read may not be correct
}
void loop()
{ ok = -1; if (Serial2.available() > 0) // if a read has been attempted { // read the incoming number on serial RX delay(100); // needed to allow time for the data // to come in from the serial buffer7 for (int z = 0 ; z < 14 ; z++) // read the rest of the tag { data1 = Serial2.read(); newtag[z] = data1; } Serial2.flush(); // stops multiple reads // now to match tags up8 checkmytags(); }9 // now do something based on tag type if (ok > 0) // if we had a match { Serial.println("Accepted"); ok = -1; } else if (ok == 0) // if we didn't have a match { Serial.println("Rejected"); ok = -1; }
}
理解草图
当一个标签出现在 RFID 读取器前时,它会通过串口发送标签的数字,这些数字共同构成了标签的 ID 号。我们捕获这 14 个数字,并将它们存储在第 7 行的newtag[]数组中。接着,使用第 4 和第 8 行的checkmytags()函数将标签 ID 与第 1 和第 2 行存储的两个标签 ID 进行比较,实际的标签数组比较由第 3 行的comparetag()函数执行。
comparetag()函数接受两个数字数组作为参数,并返回(布尔值)数组是否相同(true)或不同(false)。如果匹配成功,变量ok将在 5 和 6 行被设置为1。最后,在第 9 行,我们有读取标签成功后要执行的操作。
上传草图后,打开串口监视器窗口,并将一些标签呈现给读取器。结果应类似于图 18-5。

图 18-5:项目 52 的结果
将数据存储在 Arduino 的内建 EEPROM 中
当你在 Arduino 草图中定义并使用一个变量时,存储的数据只会持续到 Arduino 重置或断电为止。但如果你想在未来使用这些值,比如在第十一章中提到的用户可更改的数字键盘密码,怎么办呢?这时就需要使用EEPROM(电可擦写只读存储器)了。EEPROM 将变量存储在 ATmega328 微控制器内部的内存中,而且在断电时,存储的值不会丢失。
Arduino 中的 EEPROM 可以在编号为 0 到 1,023 的位置存储 1,024 字节的变量。回想一下,一个字节可以存储一个值介于 0 和 255 之间的整数,你就能明白它为何非常适合存储 RFID 标签号码。为了在草图中使用 EEPROM,我们首先通过以下代码调用 EEPROM 库(该库已包含在 Arduino IDE 中):
#include <EEPROM.h>
然后,要将值写入 EEPROM,我们只需使用以下代码:
EEPROM.write(*a*, *b*);
在这里,a 是 EEPROM 存储信息的位置,而 b 是存储我们希望存储在位置 a 的信息的变量。
为了从 EEPROM 检索数据,我们使用这个函数:
*value* = EEPROM.read*(position)*;
这将把存储在 EEPROM 位置编号为 position 的数据存储到变量 value 中。
读取和写入到 EEPROM
下面是一个读取和写入 EEPROM 的示例。输入并上传 列表 18-2。
// Listing 18-2
#include <EEPROM.h>
int zz;
void setup()
{ Serial.begin(9600); randomSeed(analogRead(0));
}
void loop()
{ Serial.println("Writing random numbers..."); for (int i = 0; i < 1024; i++) { zz = random(255);1 EEPROM.write(i, zz); } Serial.println(); for (int a = 0; a < 1024; a++) {2 zz = EEPROM.read(a); Serial.print("EEPROM position: "); Serial.print(a); Serial.print(" contains ");3 Serial.println(zz); delay(25); }
}
列表 18-2:EEPROM 演示草图
在 1 处的循环中,一个介于 0 和 255 之间的随机数被存储在每个 EEPROM 位置中。存储的值会在 2 处的第二个循环中被检索,并显示在 3 处的串口监视器中。
一旦草图上传完成,打开串口监视器。你应该能看到类似于 图 18-6 的内容。

图 18-6:来自 列表 18-2 的示例输出
现在你可以开始使用 EEPROM 创建一个项目了。
项目 #53:创建带有“最后操作”记忆的 RFID 控制
尽管项目 52 展示了如何使用 RFID 控制某些设备,比如灯光或电动门锁,但我们不得不假设如果系统重置或断电,什么都不会被记住。例如,如果灯开着,电源断了,那么电源恢复后灯会熄灭。然而,你可能希望 Arduino 记住电源断开前的状态,并恢复到那个状态。让我们现在来解决这个问题。
在这个项目中,最后的操作将被存储在 EEPROM 中(例如,“已锁定”或“已解锁”)。当草图因断电或 Arduino 重置而重新启动时,系统将恢复到存储在 EEPROM 中的先前状态。
草图
输入并上传以下草图。像项目 52 一样,将数组 1 和 2 中的每个 x 替换为你两个 RFID 标签的号码。
// Project 53 – Creating an RFID Control with "Last Action" Memory
#include <SoftwareSerial.h>
SoftwareSerial Serial2(2, 3);
#include <EEPROM.h>
int data1 = 0;
int ok = -1;
int lockStatus = 0;
// use Listing 18-1 to find your tags' numbers1 int tag1[14] = { *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*
};2 int tag2[14] = { *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*, *x*
};int newtag[14] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
}; // used for read comparisons
// comparetag compares two arrays and returns true if identical
// this is good for comparing tags
boolean comparetag(int aa[14], int bb[14])
{ boolean ff = false; int fg = 0; for (int cc = 0; cc < 14; cc++) { if (aa[cc] == bb[cc]) { fg++; } } if (fg == 14) { ff = true; } return ff;
}
void checkmytags()
// compares each tag against the tag just read
{ ok = 0; if (comparetag(newtag, tag1) == true) { ok++; } if (comparetag(newtag, tag2) == true) { ok++; }
}3 void checkLock()
{ Serial.print("System Status after restart "); lockStatus = EEPROM.read(0); if (lockStatus == 1) { Serial.println("- locked"); digitalWrite(13, HIGH); } if (lockStatus == 0) { Serial.println("- unlocked"); digitalWrite(13, LOW); } if ((lockStatus != 1) && (lockStatus != 0)) { Serial.println("EEPROM fault - Replace Arduino hardware"); }
}
void setup()
{ Serial.begin(9600); Serial2.begin(9600); Serial2.flush(); // need to flush serial buffer pinMode(13, OUTPUT);4 checkLock();
}
void loop()
{ ok = -1; if (Serial2.available() > 0) // if a read has been attempted { // read the incoming number on serial RX delay(100); for (int z = 0; z < 14; z++) // read the rest of the tag { data1 = Serial2.read(); newtag[z] = data1; } Serial2.flush(); // prevents multiple reads // now to match tags up checkmytags(); }5 if (ok > 0) // if we had a match { lockStatus = EEPROM.read(0); if (lockStatus == 1) // if locked, unlock it {6 Serial.println("Status - unlocked"); digitalWrite(13, LOW); EEPROM.write(0, 0); } if (lockStatus == 0) {7 Serial.println("Status - locked"); digitalWrite(13, HIGH); EEPROM.write(0, 1); } if ((lockStatus != 1) && (lockStatus != 0)) {8 Serial.println("EEPROM fault - Replace Arduino hardware"); } } else if (ok == 0) // if we didn't have a match { Serial.println("Incorrect tag"); ok = -1; } delay(500);
}
理解草图
这个草图是项目 52 的修改版。我们使用板载 LED 来模拟我们希望在每次读取一个有效 RFID ID 标签时开启或关闭的设备状态。每次读取并匹配标签后,锁的状态会在 5 处改变。我们将锁的状态存储在 EEPROM 的第一个位置。这个状态由一个数字表示:0 表示解锁,1 表示锁定。每次成功读取标签后,状态会在 6 或 7 处发生变化(从锁定到解锁,再回到锁定)。
我们还引入了一个故障保护机制,以防 EEPROM 损坏。如果读取 EEPROM 返回的值不是 0 或 1,我们将在 8 处收到通知。此外,当草图在重置后重新启动时,使用 checkLock() 函数在 1、2、3 和 4 处检查状态,该函数读取 EEPROM 值,确定最后的状态,然后将锁设置为该状态(锁定或解锁)。
展望未来
再次说明,我们使用了一块 Arduino 板来简单地重现可能是一个非常复杂的项目。现在你有了一个基础,可以将 RFID 控制添加到你的项目中,这将允许你通过刷 RFID 卡来创建专业级的访问系统并控制数字输出。我们将在第二十章重新访问 RFID 时再次演示这一点。
第十九章:数据总线
在本章中,您将
-
了解 I²C 总线
-
了解如何在 I²C 总线上使用 EEPROM 和端口扩展器
-
了解 SPI 总线
-
了解如何在 SPI 总线上使用数字电位器
Arduino 通过数据总线与其他设备进行通信,数据总线是一种连接系统,允许两个或多个设备按顺序交换数据。数据总线可以为 Arduino 和各种传感器、I/O 扩展设备以及其他组件之间提供连接。
大多数 Arduino 使用的两个主要总线是串行外设接口(SPI)总线和集成电路互联(I²C)总线。许多有用的传感器和外部设备都使用这些总线进行通信。
I²C 总线
I²C 总线,也称为双线接口(TWI)总线,是一种简单易用的数据总线。数据通过两根线在设备和 Arduino 之间传输,这两根线分别是SDA(数据线)和SCL(时钟线)。在 Arduino Uno 中,SDA 引脚是 A4,SCL 引脚是 A5,如图 19-1 所示。
一些较新的 R3 板还在左上角有专用的 I²C 引脚,便于访问,如图 19-2 所示。如果使用这两个引脚,您不能将 A4 和 A5 引脚用于其他用途。

图 19-1:Arduino Uno 上的 I²C 总线连接器

图 19-2:额外专用的 I²C 引脚
由于用于重新编程 USB 接口微控制器的六个引脚占用了通常用于引脚标签的位置,您可以在 Arduino 的背面看到标签,如图 19-3 所示。

图 19-3:额外专用 I²C 引脚的标签
在 I²C 总线上,Arduino 是主设备,总线上每个 IC 都是从设备。每个从设备都有自己的地址,这是一个十六进制数字,允许 Arduino 寻址并与每个设备通信。每个设备通常有一系列 7 位 I²C 总线地址可供选择,详细信息可以在制造商的数据表中找到。具体可用的地址由 IC 引脚的连接方式决定。
要使用 I²C 总线,您需要使用 Wire 库(Arduino IDE 中包含的库):
#include <Wire.h>
接下来,在void setup()中,使用以下代码激活总线:
Wire.begin();
数据通过总线一次传输 1 字节。为了从 Arduino 向总线上某个设备发送一个字节的数据,需要三个函数:
-
第一个函数通过以下代码行启动通信(其中
address是次设备的总线地址,采用十六进制格式——例如0x50):Wire.beginTransmission(`address`); -
第二个函数将 1 字节数据从 Arduino 发送到前一个函数中所指定的设备(其中
data是包含 1 字节数据的变量;您可以发送多个字节,但每个字节都需要使用一次Wire.write()调用):Wire.write(`data`); -
最后,一旦完成向特定设备发送数据,使用此命令结束传输:
Wire.endTransmission();
要请求将数据从 I²C 设备发送到 Arduino,首先使用 Wire.beginTransmission(``address``),然后是以下代码(其中 x 是要请求的字节数):
Wire.requestFrom(`address`,`x`);
接下来,使用以下函数将每个传入的字节存储到变量中:
incoming = Wire.read(); // incoming is the variable receiving the byte of data
然后使用 Wire.endTransmission() 完成事务。我们将在下一个项目中使用这些功能。
项目 #54:使用外部 EEPROM
在第十八章中,我们使用了 Arduino 内部的 EEPROM 来防止由于板重置或断电导致的变量数据丢失。Arduino 内部的 EEPROM 只存储 1,024 字节的数据。为了存储更多的数据,你可以使用外部 EEPROM,如本项目所示。

图 19-4:Microchip Technology 的 24LC512 EEPROM
对于我们的外部 EEPROM,我们将使用 Microchip Technology 的 24LC512 EEPROM,它可以存储 64KB(65,536 字节)的数据(图 19-4)。它可以从像 Digi-Key(零件号 24LC512-I/P-ND)和 PMD Way(零件号 24LC512A)等零售商处购买。
硬件
以下是创建此项目所需的材料:
-
Arduino 和 USB 电缆
-
一个 Microchip Technology 24LC512 EEPROM
-
一个面包板
-
两个 4.7 kΩ 电阻
-
一个 100 nF 陶瓷电容
-
各种连接线
原理图
对于电路,将一个 4.7 kΩ 电阻连接在 5 V 和 SCL 之间,另一个连接在 5 V 和 SDA 之间,如 图 19-5 所示。

图 19-5:项目 54 的原理图
24LC512 EEPROM IC 的总线地址部分由它在电路中的连接方式决定。总线地址的最后 3 位由 A2、A1 和 A0 引脚的状态决定。当这些引脚连接到 GND 时,它们的值为 0;当它们连接到 5 V 时,它们的值为 1。
前 4 位预设为 1010。因此,在我们的电路中,由于 A0、A1 和 A2 直接连接到 GND,总线地址在二进制中表示为 1010000,即十六进制表示为 0x50。这意味着我们可以在草图中使用 0x50 作为总线地址。
草图
虽然我们的外部 EEPROM 可以存储最多 64KB 的数据,但我们的草图仅用于演示它的部分功能,因此我们只会在 EEPROM 的前 20 个存储位置存储和读取字节。
输入并上传以下草图:
// Project 54 - Using an External EEPROM1 #include <Wire.h>
#define chip1 0x50
byte d=0;
void setup()
{2 Serial.begin(9600); Wire.begin();
}
void writeData(int device, unsigned int address, byte data)
// writes a byte of data 'data' to the EEPROM at I2C address 'device'
// in memory location 'address'
{3 Wire.beginTransmission(device); Wire.write((byte)(address >> 8)); // left part of pointer address Wire.write((byte)(address & 0xFF)); // and the right Wire.write(data); Wire.endTransmission(); delay(10);
}4 byte readData(int device, unsigned int address)
// reads a byte of data from memory location 'address'
// in chip at I2C address 'device'
{ byte result; // returned value Wire.beginTransmission(device); Wire.write((byte)(address >> 8)); // left part of pointer address Wire.write((byte)(address & 0xFF)); // and the right Wire.endTransmission();5 Wire.requestFrom(device,1); result = Wire.read(); return result; // and return it as a result of the function readData
}
void loop()
{ Serial.println("Writing data..."); for (int a=0; a<20; a++) { writeData(chip1,a,a); } Serial.println("Reading data..."); for (int a=0; a<20; a++) { Serial.print("EEPROM position "); Serial.print(a); Serial.print(" holds "); d=readData(chip1,a); Serial.println(d, DEC); }
}
让我们来一步步分析草图。在 1 处,我们激活库并定义 EEPROM 的 I²C 总线地址为 chip1。在 2 处,我们启动串口监视器,然后启动 I²C 总线。包括两个自定义函数 writeData() 和 readData(),它们可以节省你的时间,并为将来使用该 EEPROM IC 提供一些可重复使用的代码。我们将分别使用它们来写入和读取数据。
writeData()函数在第 3 步启动与 EEPROM 的传输,使用接下来的两个Wire.write()函数调用发送存储数据字节的 EEPROM 地址,发送要写入的字节数据,然后结束传输。
readData()函数在第 4 步以与writeData()相同的方式操作 I²C 总线。然而,它首先设置要读取的地址,然后不是向 EEPROM 发送数据字节,而是使用Wire.requestFrom()来读取第 5 步中的数据。最后,从 EEPROM 发送的数据字节被接收到变量result中,并成为函数的返回值。
运行草图
在void loop()中,草图循环执行 20 次,每次写入一个值到 EEPROM 中。然后它再循环一次,检索这些值并在串口监视器中显示,如图 19-6 所示。

图 19-6:项目 54 的结果
项目#55:使用端口扩展器 IC
一个端口扩展器是另一个有用的 IC,通过 I²C 进行控制。它旨在提供更多的数字输出引脚。在这个项目中,我们将使用 Microchip Technology 的 MCP23017 16 位端口扩展器 IC(图 19-7),它有 16 个数字输出,可以增加到 Arduino 上。它可以从 Digi-Key(零件号 MCP23017-E/SP-ND)和 PMD Way(零件号 MCP23017A)等零售商购买。

图 19-7:Microchip Technology 的 MCP23017 端口扩展器 IC
在这个项目中,我们将 MCP23017 连接到 Arduino,并演示如何使用 Arduino 控制 16 个端口扩展器输出。每个端口扩展器的输出都可以像常规的 Arduino 数字输出一样使用。
硬件
创建这个项目所需的材料:
-
Arduino 和 USB 电缆
-
一个面包板
-
各种连接电线
-
一颗 Microchip Technology 的 MCP20317 端口扩展器 IC
-
两个 4.7 kΩ的电阻
-
(可选)相同数量的 560 Ω电阻和 LED
电路图
图 19-8 展示了 MCP23017 的基本电路图。与项目 54 中的 EEPROM 一样,我们可以通过特定的接线顺序来设置 I²C 总线地址。在 MCP23017 中,我们将引脚 15 到 17 连接到 GND,将地址设置为0x20。
在使用 MCP23017 时,查看 IC 数据手册中的引脚图非常有帮助,如图 19-9 所示。请注意,16 个输出被分成两个部分:右侧为 GPA7 到 GPA0,左侧为 GPB0 到 GPB7。我们将通过 560 Ω的电阻将 LED 连接到某些或所有的输出端,以演示输出端何时被激活。

图 19-8:项目 55 的电路图

图 19-9:MCP23017 的引脚图
草图
输入并上传以下草图:
// Project 55 - Using a Port Expander IC
#include "Wire.h"
#define mcp23017 0x20
void setup()
{1 Wire.begin(); // activate I2C bus // set up MCP23017 // set I/O pins to outputs Wire.beginTransmission(mcp23017); Wire.write(0x00); // IODIRA register Wire.write(0x00); // set all of bank A to outputs Wire.write(0x00); // set all of bank B to outputs2 Wire.endTransmission();
}
void loop()
{ Wire.beginTransmission(mcp23017); Wire.write(0x12); 3 Wire.write(255); // bank A4 Wire.write(255); // bank B Wire.endTransmission(); delay(1000); Wire.beginTransmission(mcp23017); Wire.write(0x12); Wire.write(0); // bank A Wire.write(0); // bank B Wire.endTransmission(); delay(1000);
}
要使用 MCP23017,我们需要在 void setup() 中列出的第 1 行到第 2 行。为了开启或关闭每个银行的输出,我们按顺序发送 1 字节代表每个银行;也就是说,我们首先发送代表 GPA0 到 GPA7 的值,然后发送代表 GPB0 到 GPB7 的值。
在设置单个引脚时,你可以将每个银行看作一个二进制数字(如第六章第 104 页的“二进制简明教程”所解释)。因此,要打开引脚 1 到 4,你将发送二进制数 11110000(十进制为 240),并将其插入到 Wire.write() 函数中,分别用于银行 GPA0 到 GPA7 或 GPB0 到 GPB7。
数百个设备使用 I²C 总线进行通信。现在你已经了解了如何使用这条总线的基本知识,你可以使用任何这些设备与 Arduino 板进行连接。
SPI 总线
SPI 总线与 I²C 总线的不同之处在于,它可以同时向设备发送和接收数据,并且可以根据所使用的微控制器以不同的速度进行传输。通信模式仍然是主/从:Arduino 作为主设备,决定与哪个从设备进行通信。
引脚连接
每个 SPI 设备使用四个引脚与主设备进行通信:MOSI(主设备输出,次设备输入)、MISO(主设备输入,次设备输出)、SCK(串行时钟)和 SS 或 CS(次设备选择或芯片选择)。这些 SPI 引脚按照 图 19-10 中所示的方式连接到 Arduino。

图 19-10:Arduino Uno 上的 SPI 引脚
如 图 19-11 所示,典型的 Arduino 到 SPI 设备连接。Arduino 的 D11 到 D13 引脚保留给 SPI,但 SS 引脚可以使用任何其他数字引脚(通常使用 D10,因为它靠近 SPI 引脚)。

图 19-11:典型的 Arduino 到 SPI 设备连接
实现 SPI
现在让我们看看如何在草图中实现 SPI 总线。在此之前,我们先回顾一下所用到的函数。首先,包含 SPI 库(随 Arduino IDE 软件一起提供):
#include "SPI.h"
接下来,你需要选择一个引脚作为 SS 引脚,并在 void setup() 中将其设置为数字输出。由于在我们的示例中仅使用一个 SPI 设备,我们将使用 D10 并首先将其设置为 HIGH,因为大多数 SPI 设备的 SS 引脚是“低有效”(这意味着将引脚连接到 GND 就会设置为 HIGH,反之亦然):
pinMode(10, OUTPUT);
digitalWrite(10, HIGH);
这是激活 SPI 总线的函数:
SPI.begin();
最后,我们需要告诉草图如何发送和接收数据。一些 SPI 设备要求先发送最高有效位(MSB),而一些设备要求最低有效位(LSB)先发送。(关于 MSB 的更多内容,请参见第六章的“二进制简明教程”)。因此,在 void setup() 中,我们在 SPI.begin() 后使用以下函数:
SPI.setBitOrder(`order`);
这里,order 是 MSBFIRST 或 MSBLAST。
向 SPI 设备发送数据
要向 SPI 设备发送数据,首先我们将 SS 引脚设置为 LOW,这告诉 SPI 设备主设备(Arduino)想要与它通信。接下来,我们通过以下代码行向设备发送字节数据,按需重复——即,每发送一个字节时都使用此代码:
SPI.transfer*(byte)*;
在与设备通信完成后,将 SS 引脚设置为 HIGH,告诉设备 Arduino 已经与其通信完成。
每个 SPI 设备需要一个独立的 SS 引脚。例如,如果你有两个 SPI 设备,第二个 SPI 设备的 SS 引脚可以是 D9,并按照 图 19-12 所示连接到 Arduino。

图 19-12:两个 SPI 设备连接到一个 Arduino
与第二个设备 #2 通信时,你需要使用 D9(而不是 D10)SS 引脚,通信前后均需如此。
项目 56 演示了如何使用 SPI 总线与数字电位器进行通信。
项目 #56:使用数字电位器
简单来说,电位器 设备类似于我们在第四章中研究的可调电阻,但电位器有两个引脚:一个用于滑片,一个用于回流电流。在本项目中,你将使用数字电位器在草图中设置电阻,而不是亲自转动电位器旋钮或轴。电位器通常是音频设备中音量控制的基础,这些设备使用按钮而不是旋钮,例如汽车音响。电位器的公差比普通固定电阻的公差要大——在某些情况下,大约大 20%。

图 19-13:Microchip Technology 的 MCP4162 数字电位器
对于项目 56,我们将使用 图 19-13 中显示的 Microchip Technology MCP4162。MCP4162 提供多种电阻值;此示例使用 10 kΩ 版本。它可从 Digi-Key(零件号 MCP4162-103E/P-ND)和 element14(零件号 1840698)等零售商处购买。电阻值可以通过 257 个步骤调整;每个步骤的电阻约为 40 Ω。要选择特定步骤,我们将 2 字节的数据发送到命令字节(值为 0)和数值字节(值介于 0 和 256 之间)。MCP4162 使用非易失性存储器,因此当断电并重新连接电源后,最后选择的值仍然有效。
我们将使用电位器控制 LED 的亮度。
硬件
以下是完成此项目所需的内容:
-
Arduino 和 USB 数据线
-
一块面包板
-
各种连接电线
-
一只 Microchip Technology MCP4162 数字电位器
-
一只 560 Ω 电阻
-
一只 LED
电路图
图 19-14 显示了电路图。MCP4162 的引脚编号从封装的左上角开始。引脚 1 由位于 IC 上 Microchip 标志左侧的凹点表示(见 图 19-13)。

图 19-14:项目 56 的电路图
草图
输入并上传以下草图:
// Project 56 - Using a Digital Rheostat1 #include "SPI.h" // necessary library
int ss=10; // using digital pin 10 for SPI secondary select
int del=200; // used for delaying the steps between LED brightness values
void setup()
{2 SPI.begin(); pinMode(ss, OUTPUT); // we use this for the SS pin digitalWrite(ss, HIGH); // the SS pin is active low, so set it up high first3 SPI.setBitOrder(MSBFIRST); // our MCP4162 requires data to be sent MSB (most significant byte) first
}4 void setValue(int value)
{ digitalWrite(ss, LOW); SPI.transfer(0); // send the command byte SPI.transfer(value); // send the value (0 to 255) digitalWrite(ss, HIGH);
}
void loop()
{5 for (int a=0; a<256; a++) { setValue(a); delay(del); }6 for (int a=255; a>=0; a--) { setValue(a); delay(del); }
}
让我们来一步步解析代码。首先,我们在 1 和 2 设置 SPI 总线。在 3 处,我们设置字节方向以适配 MPC4162。为了简化电阻设置,我们在 4 处使用自定义函数,该函数接受电阻步长(从 0 到 255)并将其传递给 MCP4162。最后,代码使用两个循环将电位器通过所有阶段,从 0 到最大值在 5 处,再从最大值回到 0 在 6 处。最后这段代码应该使 LED 的亮度增减,随程序运行时 LED 的亮度不断变化。
展望未来
在本章中,你了解并实验了两种重要的 Arduino 通信方法。现在你已经准备好将 Arduino 与各种传感器、更高级的组件以及其他市面上出现的设备进行连接。其中,今天最流行的组件之一是实时时钟集成电路(IC),它让你的项目能够保持时间并与之交互——这也是第二十章的主题。那么,开始吧!
第二十章:实时钟
在本章中,你将
-
设置并获取实时钟模块的时间和日期
-
探索将设备连接到 Arduino 的新方法
-
创建一个数字时钟
-
构建一个员工 RFID 打卡机
一个实时钟(RTC) IC 模块是一个小型的计时设备,为 Arduino 项目提供了各种可能性。一旦设置了当前的时间和日期,RTC 可以在请求时提供准确的时间和日期数据。
市场上有许多不同的 RTC IC,有些比其他的更精确。在本章中,我们将使用 Maxim DS3231;它除了备用电池外不需要任何外部电路,而且在模块形式下非常精确且相当稳健。DS3231 可以作为 breakout 板从各种零售商处购买,包括来自 PMD Way(零件号 883422)的版本,如图 20-1 所示。

图 20-1:一个实时钟 IC 模块
连接 RTC 模块
将 RTC 模块连接到 Arduino 很简单,因为它使用 I²C 总线(在第十九章中讨论过)。你只需要四根电线:GND 和 VCC 分别连接到 Arduino 的 GND 和 5 V;SDA 和 SCL 分别连接到 Arduino 的 A4 和 A5。我们的示例中不会使用其他引脚。由于模块的设计,I²C 总线上无需额外的上拉电阻。
为了方便起见,可以考虑将模块安装在一个空白的 ProtoShield 上,这样它可以轻松地与其他硬件集成用于其他项目。并确保已安装备用电池,否则当你关闭项目时,时间数据将丢失!
项目 #57:使用 RTC 添加并显示时间和日期
在这个项目中,你将学习如何设置 RTC 的时间和日期,然后从串口监视器中检索并显示它们。时间和日期信息对于各种项目都很有用,例如温度记录仪和闹钟。
硬件
这是你完成这个项目所需的材料:
-
Arduino 和 USB 电缆
-
各种连接电线
-
一颗 CR2032 电池(如果 DS3231 模块中未包含)
-
一块 Maxim DS3231 RTC 模块
草图
按照本章之前的描述将模块连接到 Arduino,然后输入但不要上传以下草图:
// Project 57 - Adding and Displaying Time and Date with an RTC1 #include "Wire.h"
#define DS3231_I2C_ADDRESS 0x68
// Convert normal decimal numbers to binary coded decimal2 byte decToBcd(byte val)
{ return( (val/10*16) + (val%10) );
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{ return( (val/16*10) + (val%16) );
}3 void setDS3231time(byte second, byte minute, byte hour, byte dayOfWeek, byte
dayOfMonth, byte month, byte year)
{ // sets time and date data in the DS3231 Wire.beginTransmission(DS3231_I2C_ADDRESS); Wire.write(0); // set next input to start at the seconds register Wire.write(decToBcd(second)); // set seconds Wire.write(decToBcd(minute)); // set minutes Wire.write(decToBcd(hour)); // set hours Wire.write(decToBcd(dayOfWeek)); // set day of week (1=Sunday, 7=Saturday) Wire.write(decToBcd(dayOfMonth)); // set date (1 to 31) Wire.write(decToBcd(month)); // set month Wire.write(decToBcd(year)); // set year (0 to 99) Wire.endTransmission();
}4 void readDS3231time(byte *second,
byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)
{ Wire.beginTransmission(DS3231_I2C_ADDRESS); Wire.write(0); // set DS3231 register pointer to 00h Wire.endTransmission(); Wire.requestFrom(DS3231_I2C_ADDRESS, 7); // request seven bytes of data from DS3231 starting from register 00h *second = bcdToDec(Wire.read() & 0x7f); *minute = bcdToDec(Wire.read()); *hour = bcdToDec(Wire.read() & 0x3f); *dayOfWeek = bcdToDec(Wire.read()); *dayOfMonth = bcdToDec(Wire.read()); *month = bcdToDec(Wire.read()); *year = bcdToDec(Wire.read());
}
void displayTime()
{ byte second, minute, hour, dayOfWeek, dayOfMonth, month, year; // retrieve data from DS3231 5 readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year); // send it to the Serial Monitor Serial.print(hour, DEC); // convert the byte variable to a decimal number when displayed Serial.print(":"); if (minute<10) { Serial.print("0"); } Serial.print(minute, DEC); Serial.print(":"); if (second<10) { Serial.print("0"); } Serial.print(second, DEC); Serial.print(" "); Serial.print(dayOfMonth, DEC); Serial.print("/"); Serial.print(month, DEC); Serial.print("/"); Serial.print(year, DEC); Serial.print(" Day of week: "); switch(dayOfWeek){ case 1: Serial.println("Sunday"); break; case 2: Serial.println("Monday"); break; case 3: Serial.println("Tuesday"); break; case 4: Serial.println("Wednesday"); break; case 5: Serial.println("Thursday"); break; case 6: Serial.println("Friday"); break; case 7: Serial.println("Saturday"); break; }
}
void setup()
{ Wire.begin(); Serial.begin(9600); // set the initial time here: // DS3231 seconds, minutes, hours, day, date, month, year6 setDS3231time(0, 56, 23, 6, 30, 10, 21);
}
void loop()
{ displayTime(); // display the real-time clock data in the Serial Monitor, delay(1000); // every second
}
理解并运行草图
这个草图看起来可能很复杂,但其实并不难。在第 1 行,我们导入 I²C 库并在草图中将 RTC 的总线地址设置为 0x68。这是 DS3231 的默认总线地址,可以在数据手册中找到。在第 2 行,两个自定义函数将十进制数转换为二进制编码十进制(BCD)值并返回这些值。我们进行这些转换,因为 DS3231 存储的是 BCD 格式的值。
在第 6 行,我们使用函数 setDS3231time() 将时间和日期信息传递给 RTC IC,代码如下:
setDS3231time(`second`, `minute`, `hour`, `dayOfWeek`, `dayOfMonth`, `month`, `year`)
要使用此功能,只需将所需数据插入到各个参数中。dayOfWeek参数是一个 1 到 7 之间的数字,分别代表周日到周六。(RTC 无法检查dayOfWeek是否与输入的日期匹配,因此需要格外小心,确保一切对齐。)year信息仅为两位数字——例如,2021 年的年份应使用21。(20 是默认的。)你可以插入固定值(如本草图中所示)或包含参数的字节变量。
因此,要设置 RTC 中的时间,我们需要在第 3 行将当前日期和时间值输入到setDS3231time()函数中。现在可以上传草图了。完成一次之后,我们通过在setDS3231time()函数前加上//来注释掉该函数,然后重新上传草图,确保每次草图启动时时间不会重置为原始值!
最后,第 4 行的readDS3231time()函数从 RTC 中读取时间和日期,并将数据插入字节变量中。此数据在第 5 行的displayTime()函数中使用,该函数简单地获取数据并通过打印时间变量的内容在串行监视器中显示。
一旦上传了草图,打开串行监视器。结果应与图 20-2 中显示的类似,但它们会根据你运行草图时的当前时间和日期有所不同。

图 20-2:项目 57 的结果
你可以将项目 57 中的草图内容作为其他时间相关项目的基础。decToBcd()、bcdToDec()、readDS3231time()和setDS3231time()等函数可以插入并在未来的项目中重复使用。这就是使用 Arduino 平台的一个好处:一旦写出一个有用的过程,它通常可以在以后几乎不做修改地重复使用。
项目#58:创建一个简单的数字时钟
在本项目中,我们将使用项目 57 中的函数将时间和日期显示在标准字符 LCD 上,类似于第十五章项目 43 中 GPS 接收器使用的 LCD。
硬件
这是你需要创建这个项目的硬件:
-
Arduino 和 USB 线
-
各种连接线
-
一块面包板
-
一块 Proto-ScrewShield 或类似产品
-
一块 LCD 模块或 LCD 遮罩
-
一块实时时钟模块(在本章前面展示过)
首先,重新创建项目 57 中使用的硬件。如果你是通过接线将 RTC 模块连接到 Arduino 的,请改用 Proto-ScrewShield 与 RTC 进行连接。然后,将 LCD 遮罩插入其他遮罩上面。
草图
输入但不要上传以下草图:
// Project 58 - Creating a Simple Digital Clock
#include "Wire.h"1 #include <LiquidCrystal.h>
#define DS3231_I2C_ADDRESS 0x68
LiquidCrystal lcd( 8, 9, 4, 5, 6, 7 );
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{ return( (val/10*16) + (val%10) );
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{ return( (val/16*10) + (val%16) );
}
void setDS3231time(byte second, byte minute, byte hour, byte dayOfWeek, byte dayOfMonth, byte month, byte year)
{ // sets time and date data in the DS3231 Wire.beginTransmission(DS3231_I2C_ADDRESS); Wire.write(0); // set next input to start at the seconds register Wire.write(decToBcd(second)); // set seconds Wire.write(decToBcd(minute)); // set minutes Wire.write(decToBcd(hour)); // set hours Wire.write(decToBcd(dayOfWeek)); // set day of week (1=Sunday, 7=Saturday) Wire.write(decToBcd(dayOfMonth)); // set date (1 to 31) Wire.write(decToBcd(month)); // set month Wire.write(decToBcd(year)); // set year (0 to 99) Wire.endTransmission();
}
void readDS3231time(byte *second,
byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)
{ Wire.beginTransmission(DS3231_I2C_ADDRESS); Wire.write(0); // set DS3231 register pointer to 00h Wire.endTransmission(); Wire.requestFrom(DS3231_I2C_ADDRESS, 7); // request seven bytes of data from DS3231 starting from register 00h *second = bcdToDec(Wire.read() & 0x7f); *minute = bcdToDec(Wire.read()); *hour = bcdToDec(Wire.read() & 0x3f); *dayOfWeek = bcdToDec(Wire.read()); *dayOfMonth = bcdToDec(Wire.read()); *month = bcdToDec(Wire.read()); *year = bcdToDec(Wire.read());
}
void displayTime()
{ byte second, minute, hour, dayOfWeek, dayOfMonth, month, year; // retrieve data from DS3231 readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year); // send the data to the LCD shield lcd.clear(); lcd.setCursor(4,0); lcd.print(hour, DEC); lcd.print(":"); if (minute<10) { lcd.print("0"); } lcd.print(minute, DEC); lcd.print(":"); if (second<10) { lcd.print("0"); } lcd.print(second, DEC); lcd.setCursor(0,1); switch(dayOfWeek){ case 1: lcd.print("Sun"); break; case 2: lcd.print("Mon"); break; case 3: lcd.print("Tue"); break; case 4: lcd.print("Wed"); break; case 5: lcd.print("Thu"); break; case 6: lcd.print("Fri"); break; case 7: lcd.print("Sat"); break; } lcd.print(" "); lcd.print(dayOfMonth, DEC); lcd.print("/"); lcd.print(month, DEC); lcd.print("/"); lcd.print(year, DEC);
}
void setup()
{ Wire.begin();2 lcd.begin(16, 2); // set the initial time here: // DS3231 seconds, minutes, hours, day, date, month, year3 // setDS3231time(0, 27, 0, 5, 15, 11, 20);
}
void loop()
{ displayTime(); // display the real-time clock time on the LCD, delay(1000); // every second
}
理解和运行草图
这个草图的操作与项目 57 类似,不同之处在于我们已经修改了 displayTime() 函数,使其将时间和日期数据发送到 LCD 上,而不是发送到串口监视器,并且在第 1 和第 2 步添加了 LCD 所需的设置行。(有关使用 LCD 模块的回顾,请参见第九章。)
别忘了先上传带有时间和日期数据的草图(见第 3 步),然后再上传将代码注释掉的草图。上传草图后,您的结果应该与图 20-3 所示的类似。

图 20-3:来自项目 58 的显示
现在,您已经完成了项目 57 和 58,应该已经掌握了如何在草图中读取和写入 RTC IC 的数据。接下来,您将利用所学的知识创建一个非常实用的系统。
项目 #59:创建一个 RFID 时钟系统
在这个项目中,我们将创建一个时钟系统。您将看到 Arduino 扩展板如何协同工作,以及 Proto-ScrewShield 如何帮助您引入没有安装在扩展板上的电子元件。这个系统可以由两个人使用,他们会被分配一个 RFID 卡或标签,当他们进入或离开某个区域(如工作场所或家庭)时,刷卡通过 RFID 读卡器。时间和卡片详细信息将被记录到 microSD 卡中,供后续分析。
我们在第十五章中介绍了如何将数据记录到 microSD 卡,如何在第十八章读取 RFID 标签,以及如何在本章前面连接 RTC 模块。现在我们将把这些部分整合在一起。
硬件
创建这个项目所需的材料:
-
Arduino 和 USB 电缆
-
各种连接线
-
一个实时时钟模块(在本章前面已展示)
-
一个 LCD 模块或 Freetronics LCD 扩展板
-
一个 microSD 卡扩展板和卡(来自第十五章)
-
一个 Proto-ScrewShield 或类似产品
-
一个 RFID 读卡器模块和两个标签(来自第十八章)
要组装系统,首先将 Arduino Uno 放在底部,然后添加 Proto-ScrewShield,接着将 microSD 卡扩展板放在 Proto-ScrewShield 上,再将 LCD 扩展板放在 microSD 卡扩展板上。像第十八章中所做的那样连接 RFID 读卡器,并按照本章前面描述的方式连接 RTC 模块。根据所用硬件的具体情况,组装后的外观应类似于图 20-4 所示。

图 20-4:时钟组件
草图
现在输入并上传以下草图。记住,在向连接了 RFID 的 Arduino 上传草图时,您需要确保移除 RFID 读卡器的 RX 引脚与 Arduino D0 引脚之间的线缆,上传成功后再重新连接。
// Project 59 - Creating an RFID Time-Clock System1 #include "Wire.h" // for RTC 2 #include "SD.h" // for SD card
#include <LiquidCrystal.h>
#define DS3231_I2C_ADDRESS 0x68
LiquidCrystal lcd( 8, 9, 4, 5, 6, 7 );
int data1 = 0;3 // Use Listing 18-1 to find your tag numbers
int Mary[14] = { 2, 52, 48, 48, 48, 56, 54, 67, 54, 54, 66, 54, 66, 3};
int John[14] = { 2, 52, 48, 48, 48, 56, 54, 66, 49, 52, 70, 51, 56, 3};
int newtag[14] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0}; // used for read comparisons
// Convert normal decimal numbers to binary coded decimal
byte decToBcd(byte val)
{ return( (val/10*16) + (val%10) );
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{ return( (val/16*10) + (val%16) );
}
void setDS3231time(byte second, byte minute, byte hour, byte dayOfWeek, byte dayOfMonth, byte month, byte year)
{ // Sets time and date data in the DS3231 Wire.beginTransmission(DS3231_I2C_ADDRESS); Wire.write(0); // set next input to start at the seconds register Wire.write(decToBcd(second)); // set seconds Wire.write(decToBcd(minute)); // set minutes Wire.write(decToBcd(hour)); // set hours Wire.write(decToBcd(dayOfWeek)); // set day of week (1=Sunday, 7=Saturday) Wire.write(decToBcd(dayOfMonth)); // set date (1 to 31) Wire.write(decToBcd(month)); // set month Wire.write(decToBcd(year)); // set year (0 to 99) Wire.endTransmission();
}
void readDS3231time(byte *second, byte *minute,
byte *hour,
byte *dayOfWeek,
byte *dayOfMonth,
byte *month,
byte *year)
{ Wire.beginTransmission(DS3231_I2C_ADDRESS); Wire.write(0); // set DS3231 register pointer to 00h Wire.endTransmission(); Wire.requestFrom(DS3231_I2C_ADDRESS, 7); // Request seven bytes of data from DS3231 starting from register 00h *second = bcdToDec(Wire.read() & 0x7f); *minute = bcdToDec(Wire.read()); *hour = bcdToDec(Wire.read() & 0x3f); *dayOfWeek = bcdToDec(Wire.read()); *dayOfMonth = bcdToDec(Wire.read()); *month = bcdToDec(Wire.read()); *year = bcdToDec(Wire.read());
}
// Compares two arrays and returns true if identical.
// This is good for comparing tags.
boolean comparetag(int aa[14], int bb[14])
{ boolean ff=false; int fg=0; for (int cc=0; cc<14; cc++) { if (aa[cc]==bb[cc]) { fg++; } } if (fg==14) { ff=true; // all 14 elements in the array match each other } return ff;
}
void wipeNewTag()
{ for (int i=0; i<=14; i++) { newtag[i]=0; }
}
void setup()
{ Serial.flush(); // need to flush serial buffer Serial.begin(9600); Wire.begin(); lcd.begin(16, 2); // set the initial time here: // DS3231 seconds, minutes, hours, day, date, month, year // setDS3231time(0, 27, 0, 5, 15, 11, 12); // Check that the microSD card exists and can be used 4 if (!SD.begin(8)) { lcd.print("uSD card failure"); // stop the sketch return; } lcd.print("uSD card OK"); delay(1000); lcd.clear();
}
}
void loop()
{ byte second, minute, hour, dayOfWeek, dayOfMonth, month, year; if (Serial.available() > 0) // if a read has been attempted { // read the incoming number on serial RX delay(100); // allow time for the data to come in from the serial buffer for (int z=0; z<14; z++) // read the rest of the tag { data1=Serial.read(); newtag[z]=data1; } Serial.flush(); // stops multiple reads // retrieve data from DS3231 readDS3231time(&second, &minute, &hour, &dayOfWeek, &dayOfMonth, &month, &year); } // now do something based on the tag type5 if (comparetag(newtag, Mary) == true) { lcd.print("Hello Mary "); File dataFile = SD.open("DATA.TXT", FILE_WRITE); if (dataFile) { dataFile.print("Mary "); dataFile.print(hour); dataFile.print(":"); if (minute<10) { dataFile.print("0"); } dataFile.print(minute); dataFile.print(":"); if (second<10) { dataFile.print("0"); } dataFile.print(second); dataFile.print(" "); dataFile.print(dayOfMonth); dataFile.print("/"); dataFile.print(month); dataFile.print("/"); dataFile.print(year); dataFile.println(); dataFile.close(); } delay(1000); lcd.clear(); wipeNewTag(); } if (comparetag(newtag, John)==true) { lcd.print("Hello John "); File dataFile = SD.open("DATA.TXT", FILE_WRITE); if (dataFile) { dataFile.print("John "); dataFile.print(hour); dataFile.print(":"); if (minute<10) { dataFile.print("0"); } dataFile.print(minute); dataFile.print(":"); if (second<10) { dataFile.print("0"); } dataFile.print(second); dataFile.print(" "); dataFile.print(dayOfMonth); dataFile.print("/"); dataFile.print(month); dataFile.print("/"); dataFile.print(year); dataFile.println(); dataFile.close(); } delay(1000); lcd.clear(); wipeNewTag(); }
}
理解草图
在这个草图中,系统首先等待一个 RFID 卡呈现给读卡器。如果 RFID 卡被识别,卡主的姓名、时间和日期将被附加到存储在 microSD 卡上的文本文件中。
在 1 处是 I²C 总线和实时钟所需的函数,在 2 处是设置 microSD 卡扩展板所需的代码。在 4 处,我们检查并报告 microSD 卡的状态。在 5 处,读取的卡片与存储的两个人的卡号进行比较——在这个例子中是 John 和 Mary。如果匹配,数据将被写入 microSD 卡。通过一些修改,你可以通过在 3 处现有卡号下方添加卡片的序列号,再添加像 5 处那样的比较函数,从而将更多的卡片添加到系统中。
当需要查看记录的数据时,从 microSD 卡中复制文件data.txt。然后使用文本编辑器查看数据,或将其导入到电子表格中进行进一步分析。数据的布局非常易于阅读,如图 20-5 所示。

图 20-5:项目 59 生成的示例数据
展望未来
在本章中,你学习了如何通过 RTC 芯片处理时间和日期数据。项目 59 中描述的 RFID 系统为你提供了创建自己的访问控制系统的框架,甚至可以跟踪例如你的孩子们何时到家。在接下来的两章中,我们将创建使用 Arduino 通过互联网和手机网络进行通信的项目。
第二十一章:互联网
在本章中,你将会
-
构建一个 web 服务器来显示网页上的数据
-
使用你的 Arduino 在 Twitter 上发送 tweets
-
从网页浏览器远程控制 Arduino 数字输出
本章将向你展示如何通过互联网将你的 Arduino 连接到外部世界。这使你能够从你的 Arduino 广播数据,并通过网络浏览器远程控制你的 Arduino。
你需要什么
要构建这些与互联网相关的项目,你需要一些常见的硬件、一根电缆和一些信息。
让我们从硬件开始。你需要一个带有 W5100 控制芯片的以太网 shield。你有两个选择:你可以使用正品 Arduino 品牌的以太网 shield,如图 21-1 所示,或者你可以使用兼容 Arduino Uno 的集成以太网硬件板,如 PMD Way part 328497,如图 21-2 所示。后者是新项目或希望节省物理空间和金钱的好选择。正如你所见,集成板有用于 Arduino shield 的连接器,一个 USB 端口,一个以太网插座和一个 microSD 卡插槽。

图 21-1:一个 Arduino 以太网 shield

图 21-2:带有集成以太网的 Arduino Uno 兼容板
无论你选择哪种硬件,你还需要一根标准的 10/100 CAT5、CAT5E 或 CAT6 网络电缆,将你的以太网 shield 连接到网络路由器或互联网调制解调器。
另外,你需要你网络的路由器网关或调制解调器的 IP 地址,格式应该像这样:192.168.0.1。你还需要你计算机的 IP 地址,格式与你的路由器 IP 地址相同。
最后,如果你想要从家庭或本地区域网络之外与你的 Arduino 通信,你需要一个静态的公共 IP 地址。静态 IP 地址是由你的互联网服务提供商(ISP)分配给你的物理互联网连接的固定地址。你的互联网连接可能默认没有静态 IP 地址;如果需要,联系你的 ISP 启用此功能。如果你的 ISP 无法提供静态 IP 或者费用太高,你可以获取一个自动重定向服务,提供一个主机名,通过第三方公司(如 No-IP (www.noip.com/) 或 Dyn (account.dyn.com/)将其重定向到你的连接 IP 地址。现在让我们通过一个简单的项目来测试我们的硬件。
项目#60:构建远程监控站
在前几章的项目中,我们收集了传感器数据来测量温度和光线。在这个项目中,你将学习如何在一个简单的网页上显示这些值,几乎可以从任何支持网络的设备访问。这个项目将显示模拟输入引脚的值和数字输入 0 到 9 的状态,这些功能将作为远程监控站的基础。
使用此框架,您可以添加具有模拟和数字输出的传感器,如温度、光照和开关传感器,并将传感器的状态显示在网页上。
硬件
以下是创建此项目所需的内容:
-
一根 USB 线
-
一根网线
-
一块 Arduino Uno 和以太网盾,或者一块带有集成以太网的 Arduino Uno 兼容板
草图
输入以下草图,但不要上传:
/* Project 60 – Building a Remote Monitoring Station created 18 Dec 2009 by David A. Mellis, modified 9 Apr 2012 by Tom Igoe modified August 2020 by John Boxall */
#include <SPI.h>#include <Ethernet.h>1 IPAddress ip(`xxx`,`xxx`,`xxx`,`xxx`); // Replace this with your project's IP address2 byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
EthernetServer server(80);
void setup()
{ // Start the Ethernet connection and server Ethernet.begin(mac, ip); server.begin(); for (int z=0; z<10; z++) { pinMode(z, INPUT); // set digital pins 0 to 9 to inputs }
}
void loop()
{ // listen for incoming clients (incoming web page request connections) EthernetClient client = server.available(); if (client) { // an HTTP request ends with a blank line boolean currentLineIsBlank = true; while (client.connected()) { if (client.available()) { char c = client.read(); if (c == '\n') && currentLineIsBlank) { client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println("Connection: close"); client.println(); client.println("<!DOCTYPE HTML>"); client.println("<html>"); // add a meta refresh tag, so the browser pulls again every 5 sec:3 client.println("<meta http-equiv=\"refresh\" content=\"5\">"); // output the value of each analog input pin onto the web page for (int analogChannel = 0; analogChannel < 6; analogChannel++) { int sensorReading = analogRead(analogChannel);4 client.print("analog input "); client.print(analogChannel); client.print(" is "); client.print(sensorReading); client.println("<br />"); } // output the value of digital pins 0 to 9 onto the web page for (int digitalChannel = 0; digitalChannel < 10; digitalChannel++) { boolean pinStatus = digitalRead(digitalChannel); client.print("digital pin "); client.print(digitalChannel); client.print(" is "); client.print(pinStatus); client.println("<br />"); } client.println("</html>"); break; } if (c == '\n') { // you're starting a new line currentLineIsBlank = true; } else if (c != '\r') { // you've gotten a character on the current line currentLineIsBlank = false; } } } // give the web browser time to receive the data delay(1); // close the connection: client.stop(); }
}
我们稍后会更详细地讨论这个草图。首先,在上传草图之前,您需要为以太网盾设置一个 IP 地址,以便它能够在本地网络或调制解调器中找到。您可以通过检查路由器的 IP 地址来确定地址的前三部分。例如,如果路由器的地址是 192.168.0.1,请将最后一位数字更改为一个随机值,且该数字不能与网络上其他设备的数字相同,选择一个 2 到 254 之间的未被占用的数字。在草图中的 1 处输入更改后的 IP 地址,像这样:
IPAddress ip(192, 168, 0, 69); // Ethernet shield's IP address

图 21-3:由我们的站点监控的引脚值,可通过任何连接网络的设备上的网页浏览器查看
一旦做出更改,保存并上传您的草图。接下来,如果需要,插入以太网盾到您的 Arduino,连接网络线到路由器或调制解调器和以太网连接器,并开启 Arduino 板电源。
等待大约 20 秒。然后,使用您网络中的任何设备或计算机上的网页浏览器,输入 1 处的 IP 地址。如果您看到类似图 21-3 的内容,则您的监控站框架正常工作。
故障排除
如果这个项目对您不起作用,请尝试以下方法:
-
检查草图中 1 处的 IP 地址是否设置正确。
-
检查草图是否正确并已上传到您的 Arduino。
-
再次检查本地网络。您可以检查连接的计算机是否能够访问互联网。如果可以,检查 Arduino 板是否通电并且已连接到路由器或调制解调器。
-
如果您是从智能手机访问项目网页,请确保您的智能手机连接的是本地 Wi-Fi 网络,而不是手机运营商的移动网络。
-
如果以太网盾的 LED 灯在 Arduino 有电并且以太网线连接到盾和路由器或调制解调器时没有闪烁,请尝试更换一根网线。
理解草图
一旦你的监控站点工作正常,你可以回到草图中最重要的部分。从开始到第 3 处的代码是必要的,因为它加载了必要的库,并在 void setup() 中启动了以太网硬件。在第 3 处之前,client.print() 语句是草图设置网页的地方,使其能够被网页浏览器读取。从第 3 处开始,你可以使用 client.print() 和 client.println() 函数在网页上显示信息,就像在串口监视器上一样。例如,下面的代码用于显示图 19-3 中所示的网页的前六行:
client.print("analog input ");
client.print(analogChannel);
client.print(" is ");
client.print(sensorReading);
在第 4 处,你会看到一个示例,展示了如何将文本和变量内容写入网页。在这里,你可以使用 HTML 来控制网页的外观,只要不超出 Arduino 的内存限制。换句话说,你可以使用任意多的 HTML 代码,直到达到最大草图大小,而这个大小由 Arduino 板的内存决定。(每种板型的内存大小请参见第 234 页的表格 13-2。)
需要注意的一点是 MAC 地址,网络可以通过它来检测连接到网络的每个硬件。网络上的每个硬件都有一个唯一的 MAC 地址,可以通过更改第 2 处的十六进制值来改变。如果有两个或更多基于 Arduino 的项目使用同一个网络,你必须为每个设备输入不同的 MAC 地址。如果你的开发板附带了一个 MAC 地址,请使用该值。
最后,如果你想从未连接到本地网络的设备(例如使用蜂窝连接的平板电脑或手机)查看网页,那么你需要在网络路由器或调制解调器中使用称为 端口转发 的技术,可以通过像 No-IP 或 Dyn 这样的组织提供的服务实现。端口转发通常与路由器的品牌和型号有关,因此可以在网上搜索“路由器端口转发”或者访问像 www.wikihow.com/Port-Forward 这样的教程网站获取更多信息。
现在你知道如何在网页上显示文本和变量了,让我们来使用 Arduino 发布推文。
项目 #61:创建一个 Arduino 推特账号
在这个项目中,你将学习如何让 Arduino 通过 Twitter 发送推文。你可以接收由任何可以访问 Twitter 的设备生成的各种信息。如果,比如说,你希望在国外时接收来自家里的每小时温度更新,或者当孩子们回家时收到通知,这可以提供一种经济实惠的解决方案。
你的 Arduino 需要一个独立的 Twitter 账号,因此请执行以下操作:
-
访问
twitter.com/并创建你的 Arduino 的 Twitter 账号。记下用户名和密码。 -
从第三方网站
arduino-tweet.appspot.com/获取一个令牌。令牌在 Arduino 与 Twitter 服务之间创建了一座桥梁。你只需要在这个网站上完成第 1 步。 -
将令牌以及你的 Arduino 新 Twitter 账户的详细信息复制并粘贴到电脑上的文本文件中。
-
从
github.com/NeoCat/Arduno-Twitter-library/archive/master.zip下载并安装 Twitter Arduino 库。
硬件部分
这是你需要创建这个项目的材料:
-
一根 USB 电缆
-
一根网络电缆
-
一块 Arduino Uno 和以太网扩展板,或一块与 Arduino Uno 兼容的集成以太网板
草图部分
输入以下草图,但不要上传:
// Project 61 - Creating an Arduino Tweeter
#include <SPI.h>
#include <Ethernet.h>
#include <Twitter.h>// Ethernet shield settings1 IPAddress ip(192,168,0,1); // Replace this with your project's IP address2 byte mac[] = { `0xDE`, `0xAD`, `0xBE`, `0xEF`, `0xFE`, `0xED` };3 Twitter twitter("`insertyourtokenhere`");
// Message to post4 char msg[] = "`I'm alive!`";
void setup()
{ delay(1000); Ethernet.begin(mac, ip); // or you can use DHCP for automatic IP address configuration // Ethernet.begin(mac); Serial.begin(9600); Serial.println("connecting ...");
}
void loop()
{5 if (twitter.post(msg)) { int status = twitter.wait(&Serial); if (status == 200) { Serial.println("OK."); } else { Serial.print("failed : code "); Serial.println(status); } } else { Serial.println("connection failed."); } while (1);
}

图 21-4:你的 Arduino 推文
如同项目 60 一样,在 1 处插入你的 IP 地址,并在 2 处根据需要修改 MAC 地址。然后在 3 处的双引号之间插入 Twitter 令牌。最后,在 4 处插入你想要发送的推文内容。现在上传草图并将硬件连接到网络。(别忘了使用你自己的账户关注 Arduino 的 Twitter 账户!)大约一分钟后,访问你的 Twitter 页面或在设备上加载应用,信息应该会显示出来,如图 21-4 所示。
在创建 Arduino 推特程序时,请记住每分钟只能发送一条推文,并且每条信息必须是独一无二的。(这些是 Twitter 的规定。)发送推文时,Twitter 还会回复一个状态码。草图会在串口监视器中接收并显示该状态码,使用位于 5 处的代码。图 21-5 显示了一个示例。

图 21-5:由于尝试重复发布,Twitter 返回的示例错误信息
如果你收到像这样的 403 消息,可能是令牌不正确或者你发送推文的速度过快。(有关完整的 Twitter 错误代码列表,请参见finderrorcode.com/twitter-error-codes.html)。
从网页控制你的 Arduino
你可以通过多种方式从网页浏览器控制你的 Arduino。经过一些研究,我找到了一个可靠、安全且免费的方法:Teleduino。
Teleduino 是由新西兰 Arduino 爱好者 Nathan Kennedy 创建的一个免费服务。它是一个简单而强大的工具,用于通过互联网与 Arduino 交互。它不需要任何特殊或定制的 Arduino 草图;你只需在网页浏览器中输入一个特殊的 URL 即可控制 Arduino。你可以使用 Teleduino 来控制数字输出引脚和舵机,或者发送 I²C 命令,更多功能正在不断添加。在项目 62 中,你将学习如何配置 Teleduino,并通过网络启用的设备远程控制数字输出。
项目 #62:为你的 Arduino 设置远程控制
在开始第一个 Teleduino 项目之前,你必须先在 Teleduino 服务平台注册并获取一个唯一的密钥来识别你的 Arduino。为此,访问 www.teleduino.org/tools/request-key/ 并输入所需信息。你应收到一封包含你的密钥的电子邮件,密钥大致如下:187654321Z9AEFF952ABCDEF8534B2BBF。
接下来,通过访问 www.teleduino.org/tools/arduino-sketch-key/ 将你的密钥转换为数组变量。输入你的密钥,页面应返回一个类似于 图 21-6 所示的数组。

图 21-6:作为数组的 Teleduino 密钥
每个密钥都是独特的,针对单个 Arduino,但如果你想同时运行多个 Teleduino 项目,你可以获取更多的密钥。
硬件
以下是你创建此项目所需的设备:
-
一根 USB 数据线
-
一根网络电缆
-
一块 Arduino Uno 和以太网扩展板,或一块集成以太网的 Arduino Uno 兼容板
-
一只 560 Ω 电阻(R1)
-
一块面包板
-
一只任意颜色的 LED
组装你的硬件并将 LED 连接到数字引脚 8,如 图 21-7 所示。

图 21-7:项目 62 的原理图
草图
Teleduino 项目仅使用一个草图,该草图包含在 Teleduino 库中。以下是如何访问该草图:
-
从
www.teleduino.org/downloads/下载并安装 Teleduino 库。 -
重启 Arduino IDE 并选择 文件▶示例▶Teleduino328▶TeleduinoEthernetClientProxy。
-
现在你应该能看到 Teleduino 草图。在将其上传到 Arduino 之前,替换默认的密钥为你的密钥数组。你需要替换的变量应该在草图的第 36 行。一旦替换完成,保存草图,然后上传到你的 Arduino。
现在连接你的硬件到网络并观察 LED。大约一分钟后,LED 应该会闪烁几次然后停下来。闪烁的次数表示 Teleduino 服务的状态,如 表 21-1 所示。
表 21-1:Teleduino 状态闪烁代码
| 闪烁次数 | 消息 |
|---|---|
| 1 | 初始化中 |
| 2 | 正在启动网络连接 |
| 3 | 正在连接到 Teleduino 服务器 |
| 4 | 身份验证成功 |
| 5 | 会话已存在 |
| 6 | 无效或未授权的密钥 |
| 10 | 连接已断开 |
如果你看到五次闪烁,那么另一个 Arduino 已经使用你的密钥进行编程并连接到 Teleduino 服务器。看到十次闪烁时,你应该检查硬件和网络连接。Arduino 连接成功后,LED 应每隔大约 5 秒闪烁一次。由于状态 LED 是由数字引脚 8 控制的,在使用 Teleduino 时,你不能将该引脚用于其他用途。
远程控制你的 Arduino
为了远程控制你的 Teleduino,你可以使用任何带有网页浏览器的设备。然而,首先需要设置你想控制的每个数字引脚的模式。控制 Arduino 的命令是通过输入你创建的网址来发送的:
http://us01.proxy.teleduino.org/api/1.0/328.php?k={*YOURKEY*}&r=definePinMode&pin=<*X*>&mode=<*Y*>
你需要在网址中更改三个参数。首先,将 {YOURKEY} 替换为你从 Teleduino 网站收到的长字母数字密钥。接着,将 <X> 替换为你想要控制的数字引脚号。最后,将 <Y> 更改为 1,以将数字引脚设置为输出。
现在你可以远程控制数字引脚了。执行此操作的命令是:
http://us01.proxy.teleduino.org/api/1.0/328.php?k={*YOURKEY*}
&r=setDigitalOutput&pin=<*X*>&output=<*S*>
再次,你需要更改网址中的三个参数。首先,将 {YOURKEY} 替换为你从 Teleduino 网站收到的长字母数字密钥。接着,将 <X> 替换为你想要控制的数字引脚号。最后,将 <S> 更改为 0(低电平)或 1(高电平)以改变数字输出。例如,要将数字引脚 7 设置为高电平,你应输入:
http://us01.proxy.teleduino.org/api/1.0/328.php?k={`YOURKEY`}&r=setDigitalOutput&pin=7&output=1
在命令成功执行后,你应该会在网页浏览器中看到类似以下内容:
{"status":200,"message":"OK","response"
{"result":0,"time":0.22814512252808,"values":[]}}
如果命令失败,你应该看到类似下面的错误信息:
{"status":403,"message":"Key is offline or invalid.","response":[]}
你可以通过修改网址发送命令,将数字引脚设置为高或低。
如果某个数字引脚支持脉宽调制(PWM),如第三章所述,你还可以使用以下命令控制引脚的 PWM 输出:
http://us01.proxy.teleduino.org/api/1.0/328.php?k={`YOURKEY`}&r=setPwmOutput&pin=<`X`>&output=<`Y`>
其中,<X> 是数字输出引脚,<Y> 是 PWM 水平,范围从 0 到 255。
在你为项目创建完网址后,可以将其加入浏览器书签,或者创建一个本地网页,其中的链接作为按钮。例如,你可以将一个网址书签为将数字引脚 7 设置为高电平,另一个书签为将其设置为低电平。
在某些情况下,你的 Arduino 输出的状态可能至关重要。作为一种安全保障,以防 Arduino 由于断电或其他中断而重置,设置数字引脚的默认状态。将你的项目连接到 Teleduino 服务后,访问 www.teleduino.org/tools/manage-presets/。在输入你的唯一密钥后,你应该会看到一个选项屏幕,允许你选择数字引脚的模式和数值,如 图 21-8 所示。

图 21-8:默认引脚状态设置页面
展望未来
除了可以轻松地通过互联网监控你的 Arduino 并让它发送推文到 Twitter,你还可以通过互联网控制你的 Arduino 项目,而无需编写复杂的草图、具备深厚的网络知识或承担月度费用。这使你几乎可以在任何地方控制 Arduino,并扩大其发送数据的能力。本章中的三个项目提供了一个框架,你可以在此基础上设计自己的远程控制项目。
本书的下一章,也是最后一章,将向你展示如何通过蜂窝网络连接让你的 Arduino 发送和接收命令。
第二十二章:移动通信
在本章中,你将会:
-
当事件发生时,让你的 Arduino 拨打一个电话号码
-
使用 Arduino 发送短信到手机
-
通过手机的短信控制连接到 Arduino 的设备
你可以将 Arduino 项目连接到手机网络,实现 Arduino 与手机或固定电话之间的简单通信。凭借一些想象力,你可以为这种通信类型设计许多应用,包括本章中的一些项目。
在购买任何硬件之前,一定要先审阅本章内容,因为项目的成功将依赖于你的手机网络。你的网络必须能够做到以下几点:
-
在 UMTS(3G)850 MHz、900 MHz、1900 MHz 或 2100 MHz 频段下运行。
-
允许使用网络提供商未提供的设备。
为了使用这些项目,你可能需要选择预付费通话计划或提供大量短信的计划,以防你的程序出现错误,导致项目发送出多个短信(短消息服务,SMS)。此外,确保 SIM 卡使用时不要求输入 PIN 码。(你可以通过将 SIM 卡插入普通手机并在安全设置菜单中更改该设置来轻松完成此操作。)
硬件
所有项目都使用共同的硬件配置,因此我们将首先进行设置。你需要特定的硬件来完成本章中的项目,首先是 SIM5320 类型的 3G GSM 扩展板和天线,如图 22-1 所示。这个扩展板可以从 TinySine (www.tinyosshop.com/)及其分销商处购买。SIM5320 扩展板有两种类型:SIM5320A 和 SIM5320E。
-E 版本使用 UMTS/HSDPA 900/2100 MHz 频段(主要供欧洲用户使用),而-A 版本使用 UMTS/HSDPA 850/1900 MHz 频段(主要供美国用户和使用 Telstra 网络的澳大利亚用户使用)。

图 22-1:附加天线的 3G 扩展板
你还需要一个电源供应。在某些情况下,3G 扩展板可能需要最多 2 A 的电流(比 Arduino 本身能够提供的电流还要多),如果在没有外部电源的情况下使用,将会损坏你的 Arduino。因此,你需要一个外部电源。这个电源可以是一个直流插头电源适配器、墙壁适配器电源(或者是一个大容量的 7.2V 可充电电池、太阳能电池板/电池源、12V 电池等,只要它不超过 12V DC),能够提供最多 2 A 的电流。
硬件配置与测试
现在让我们配置并测试硬件,确保 3G 模块可以与蜂窝网络和 Arduino 通信。我们首先需要设置串口通信跳线,因为 3G 模块通过串口与 Arduino 通信,方式与第十五章中使用的 GPS 模块相同。我们可以使用模块顶部右侧的跳线来设置模块与 Arduino 通信所使用的数字引脚。我们所有的项目将使用数字引脚 2 作为模块的发送引脚,数字引脚 3 作为接收引脚。要配置此设置,请在 TX2 和 RX3 引脚之间连接跳线,如 图 22-2 所示。

图 22-2:模块串口配置跳线
接下来,翻转模块,将 SIM 卡插入卡槽,如 图 22-3 所示。

图 22-3:已安装 SIM 卡
接下来,轻轻将 3G 模块插入 Arduino。连接外部电源和 Arduino 与 PC 之间的 USB 电缆。最后,就像使用手机一样,你需要使用模块左上角的电源按钮打开(和关闭)SIM 模块,如 图 22-4 所示。按住按钮 2 秒钟后松开。片刻之后,P(电源)和 S(状态)LED 会亮起,蓝色 LED 会开始闪烁,表示 3G 模块已成功注册到蜂窝网络。
供将来参考,模块的电源按钮连接到数字引脚 8,因此你可以从草图中控制电源,而无需手动开关按钮。

图 22-4:3G 模块电源按钮和状态 LED
现在输入并上传 列表 22-1 中显示的草图。
// Listing 22-11 #include <SoftwareSerial.h> // Virtual serial port2 SoftwareSerial cell(2,3);
char incoming_char = 0;
void setup()
{ // Initialize serial ports for communication Serial.begin(9600);3 cell.begin(4800); Serial.println("Starting SIM5320 communication...");
}
void loop()
{ // If a character comes in from 3G shield if( cell.available() > 0 ) { // Get the character from the cellular serial port incoming_char = cell.read(); // Print the incoming character to the Serial Monitor Serial.print(incoming_char); } // If a character is coming from the terminal to the Arduino... if( Serial.available() > 0 ) { incoming_char = Serial.read(); // Get the character from the terminal cell.print(incoming_char); // Send the character to the cellular module }
}
列表 22-1:3G 模块测试草图
这个草图简单地将来自 3G 模块的所有信息转发到串口监视器。3G 模块通过软件串口与 Arduino 的数字引脚 2 和 3 连接,这样它就不会干扰 Arduino 和 PC 之间的正常串口连接(该连接使用数字引脚 0 和 1)。我们为 3G 模块设置了一个虚拟串口,编号为 1、2 和 3。默认情况下,3G 模块通过串口以 4800 bps 的速度进行通信,这对于我们的项目来说已经足够了。
上传草图后,打开串口监视器窗口,等待大约 10 秒钟。然后,使用另一部电话拨打你 3G 模块的号码。你应该能看到类似于 图 22-5 中显示的数据。

图 22-5:列表 22-1 输出示例
当你拨打电话时,RING 通知会来自扩展板,而未接来电通知则会在你挂断电话时显示。如果你的移动网络支持来电显示,源电话号码将在时间后显示。(为了隐私保护,图 22-5 中的号码已经被打上了马赛克。)现在 3G 扩展板已经开始工作,我们可以利用其提供的各种功能来进行项目开发。
项目 #63:构建一个 Arduino 拨号器
在完成这个项目后,你的 Arduino 会在事件发生时拨打一个电话号码,事件由你的 Arduino 草图决定。例如,如果存储冷冻柜的温度超过某个水平,或者入侵报警系统被激活,你可以让 Arduino 从预设号码拨打给你,等待 20 秒钟后挂断。你的电话的来电显示将会把电话号码识别为 Arduino。
硬件
这个项目使用了章节开头描述的硬件以及你为应用选择的任何额外电路。为了演示,我们将使用按钮来触发电话拨打。
除了前面讨论的硬件外,创建这个项目还需要以下组件:
-
一个按钮
-
一个 10 kΩ 电阻
-
一个 100 nF 电容
-
各种连接线
-
一个面包板
电路图
按照 图 22-6 所示连接外部电路。

图 22-6:项目 63 的电路图
草图
输入 但不要上传 以下草图:
// Project 63 - Building an Arduino Dialer
#include <SoftwareSerial.h> // Virtual serial port
SoftwareSerial cell(2,3);
char incoming_char = 0;
void setup()
{ pinMode(7, INPUT); // for button pinMode(8, OUTPUT); // shield power control // initialize serial ports for communication Serial.begin(9600); cell.begin(4800);
}
void callSomeone()
{ // turn shield on1 Serial.println("Turning shield power on..."); digitalWrite(8, HIGH); delay(2000); digitalWrite(8, LOW); delay(10000);2 cell.println("ATD`xxxxxxxxxx`"); // dial the phone number `xxxxxxxxxx` // change `xxxxxxxxxx` to your desired phone number (with area code) Serial.println("Calling ..."); delay(20000); // wait 20 seconds3 cell.println("ATH"); // end call Serial.println("Ending call, shield power off."); // turn shield off to conserve power4 digitalWrite(8, HIGH); delay(2000); digitalWrite(8, LOW);
}
void loop()
{5 if (digitalRead(7) == HIGH) {6 callSomeone(); }
}
理解草图
在设置好软件串口和常规串口后,草图会等待按下连接到数字引脚 7 的按钮。当按钮被按下后,callSomeone() 函数会在第 6 步运行。在第 1 步时,数字引脚 8 会被切换为 HIGH,保持 2 秒钟,打开扩展板,并等待 10 秒钟让扩展板与移动网络注册。接下来,在第 2 步,草图发送拨打电话号码的指令。最后,在第 3 步通话结束后,扩展板在第 4 步关闭以节省电力。
你需要将 xxxxxxxxxx 替换为你希望 Arduino 拨打的电话号码。使用与手机拨号相同的方式。例如,如果你想让 Arduino 拨打 212.555.1212,你需要添加以下内容:
cell.println("ATD2125551212");
在输入电话号码后,你可以上传草图,等待一分钟让 3G 模块连接到网络,然后通过按下按钮来进行测试。将拨号功能集成到现有的草图中非常简单,因为只需要在第 2 步时调用它。之后,你可以找到触发条件——可能是温度传感器、光传感器,或者其他任何输入达到某个水平——来使 Arduino 拨打一个电话号码。
现在让我们通过发送短信把你的 Arduino 带入 21 世纪。
项目 #64:构建一个 Arduino 文字信息发送器
在本项目中,当事件发生时,Arduino 将向另一部手机发送短信。为简化代码,我们将使用 SerialGSM Arduino 库,您可以从github.com/meirm/SerialGSM/archive/master.zip下载该库。安装库后,重新启动 Arduino IDE。
您将在本项目中使用的硬件与项目 63 中使用的硬件相同。
草图
将以下草图输入到 Arduino IDE 中,但暂时不要上传:
// Project 64 - Building an Arduino Texter
#include <SerialGSM.h>
#include <SoftwareSerial.h> // Virtual serial port1 SerialGSM cell(2, 3);
void sendSMS()
{2 cell.Message("The button has been pressed!"); cell.SendSMS();
}
void setup()
{ pinMode(7, INPUT); // for button pinMode(8, OUTPUT); // shield power control // turn shield on Serial.println("Turning shield power on..."); digitalWrite(8, HIGH); delay(2000); digitalWrite(8, LOW); // initialize serial ports for communication Serial.begin(9600); cell.begin(4800); cell.Verbose(true); cell.Boot(); cell.FwdSMS2Serial();3 cell.Rcpt("`xxxxxxxxxxx`"); delay(10000);
}
void loop()
{4 if (digitalRead(7) == HIGH) { sendSMS(); }
}
理解草图
3G 扩展板如常在 1 处设置,并在void setup()中初始化。按钮按下事件在 4 处被检测到,并调用sendSMS()函数。这个简单的函数将短信发送到存储在 3 处的手机号码。
在上传草图之前,将xxxxxxxxxxx替换为接收者的手机号码;输入区号和号码,不要使用空格或括号。例如,要将短信发送到美国的 212.555.1212,您应存储2125551212。

图 22-7:接收到的示例短信
要发送的短信存储在第 2 步。(请注意,消息的最大长度为 160 个字符。)
存储了一个示例短信和目标号码后,上传草图,等待 30 秒钟,然后按下按钮。稍后,消息应送达目标手机,如图 22-7 所示。
项目 64 可以轻松集成到其他草图中,并且可以通过switch case语句根据参数比较数据发送不同的短信。
项目 #65:设置 SMS 远程控制
在本项目中,您将通过从手机发送短信来控制 Arduino 的数字输出引脚。您应该能够利用现有的知识来添加各种设备进行控制。我们将提供四个独立的数字输出,但您可以根据需要控制更多或更少的输出。
要打开或关闭四个数字输出(在此示例中为 10 到 13 号引脚),您需要以以下格式将短信发送到您的 Arduino:#a``x``b``x``c``x``d``x,将x替换为0(关闭)或1(打开)。例如,要打开所有四个输出,您将发送#a1b1c1d1。
硬件
本项目使用的是本章开始时描述的硬件,外加您选择的任何额外电路。我们将使用四个 LED 来指示数字输出的状态。因此,以下额外硬件是本示例所需的:
-
四个 LED
-
四个 560 Ω的电阻
-
各种连接线
-
一个面包板
原理图
按照图 22-8 所示连接外部电路。

图 22-8:项目 65 的原理图
草图
对于这个项目,我们并未使用 3G 模块库,而是直接依赖原始命令来控制该模块。此外,在整个草图中,我们没有打开或关闭模块,因为我们需要它处于开启状态才能监听传入的短信。输入并上传以下草图:
// Project 65 - Setting Up an SMS Remote Control
#include <SoftwareSerial.h>
SoftwareSerial cell(2,3);
char inchar;
void setup()
{ // set up digital output pins to control pinMode(10, OUTPUT); pinMode(11, OUTPUT); pinMode(12, OUTPUT); pinMode(13, OUTPUT); digitalWrite(10, LOW); // default state for I/O pins at power-up or reset, digitalWrite(11, LOW); // change as you wish digitalWrite(12, LOW); digitalWrite(13, LOW); // initialize the 3G shield serial port for communication cell.begin(4800); delay(30000);1 cell.println("AT+CMGF=1"); delay(200);2 cell.println("AT+CNMI=3,3,0,0"); delay(200);
}
void loop()
{ // if a character comes in from the cellular module...3 if(cell.available() > 0) { inchar = cell.read();4 if (inchar == '#') // the start of our command { delay(10); inchar = cell.read();5 if (inchar == 'a') { delay(10); inchar = cell.read(); if (inchar == '0') { digitalWrite(10, LOW); } else if (inchar == '1') { digitalWrite(10, HIGH); } delay(10); inchar = cell.read(); if (inchar == 'b') { inchar = cell.read(); if (inchar == '0') { digitalWrite(11, LOW); } else if (inchar == '1') { digitalWrite(11, HIGH); } delay(10); inchar = cell.read(); if (inchar == 'c') { inchar = cell.read(); if (inchar == '0') { digitalWrite(12, LOW); } else if (inchar == '1') { digitalWrite(12, HIGH); } delay(10); inchar = cell.read(); if (inchar == 'd') { delay(10); inchar = cell.read(); if (inchar == '0') { digitalWrite(13, LOW); } else if (inchar == '1') { digitalWrite(13, HIGH); } delay(10); } } cell.println("AT+CMGD=1,4"); // delete all SMS } } } }
}
理解草图
在这个项目中,Arduino 监控从 3G 模块发送的每个文本字符。因此,在步骤 1 中,我们告诉模块将传入的短信转化为文本并将内容发送到虚拟串口。在步骤 2 之后,Arduino 在步骤 3 等待来自模块的文本消息。
由于从手机发送的命令并通过 3G 模块传递给 Arduino 控制引脚的指令以#开头,因此草图会等待文本消息中的井号(#)出现。在步骤 5 中,首先检查第一个输出参数a——如果后面跟着0或1,则分别关闭或开启引脚。对于接下来的三个由b、c和d控制的输出,过程会重复进行。
激发你的想象力,想象一下使用这个项目创造一个遥控器来控制各种事物——如灯光、泵、警报等,会是多么的简单。
展望未来
通过本章的三个项目,你已经建立了一个良好的框架,能够构建自己的项目,实现在蜂窝网络上的通信。你唯一受限的是想象力——例如,你可以在地下室淹水时接收短信,或者用手机开启空调。再次提醒,在让你的项目“自由飞翔”之前,记得留意网络费用。
此时,经过阅读(并且希望已经动手构建)本书中的 65 个项目后,你应该已经具备了创造自己的 Arduino 项目所需的理解、知识和信心。你已经掌握了创建多个项目的基本构建块,并且我相信你能够运用这些技术解决各种问题,同时享受乐趣。
我始终很高兴收到关于本书的反馈,您可以通过本书网页上的联系方式留言:nostarch.com/arduino-workshop-2nd-edition/。
但请记住——这只是开始。你可以找到更多的硬件形式来进行操作,只要稍加思考和规划,你就可以与它们所有的设备进行合作。你会在互联网上找到庞大的 Arduino 用户社区(例如在forum.arduino.cc/上的 Arduino 论坛),甚至在本地的黑客空间或俱乐部中也可以找到。
所以不要只是坐在那里——做点什么吧!



















浙公网安备 33010602011771号