面向-Arduino-爱好者的-Arduino-指南-全-
面向 Arduino 爱好者的 Arduino 指南(全)
原文:
zh.annas-archive.org/md5/9692561ddc61985c536e940f247d432a译者:飞龙
前言

我写这本书是为了那些了解 Arduino 微控制器环境基础知识但渴望学习更多的人。也许你已经完成了我的第一本为初学者编写的 Arduino 书《Arduino 工作坊》中的项目,或者读过其他的入门书籍。你知道如何使用数据总线,控制小型电机,处理各种传感器的数据,做很多其他事情。但是,还有更多知识等待你去学习,你已经准备好挑战更复杂的项目了。
在本书中,你将继续进入基于 Arduino 的学习和娱乐世界。每一章都包含关于你可能听说过但还未尝试的高级技巧和工具的详细教程,并附有构建 70 个复杂而实用项目的说明。我选择这些主题是基于我在帮助他人创造有趣、实用和创新的 Arduino 电路和产品时,发现的最有价值的技能。
本书适合谁阅读?
本书适合那些具备中级 Arduino 经验并准备扩展技能的人。你可能已经完成了 Arduino 入门套件中的示例,或阅读过一本关于 Arduino 项目构建的基础书籍,或者参加过一门使用 Arduino 的基础计算机工程大学课程。
你可能希望将本书作为参考,构建自己的项目创意,或者你可能只是想学习一系列新的技能。如果你有兴趣提高功率效率、使用常见数据总线与旧设备进行接口、将项目数据记录到在线或离线存储、通过互联网控制项目等,那么这本书就是为你而写的。
为了让尽可能多的人能够完成本书,我最大程度地减少了构建每个项目的成本。你可以轻松地从各种在线零售商处购买所需的所有零件。
你可以期待学到什么?
在本书的每一章中,我都会介绍一组新的概念或工具,并教你如何将它们应用于总共 70 个有趣且富有教育意义的项目。我希望这些内容能够激发你的创造性想象力,并成为你未来工作的参考。
一般来说,你不需要按顺序阅读,每一章的项目可以在完成前一章后再进行,也可以选择任何你觉得有趣或有用的章节。第十九章及其后续章节介绍了联网项目,因此最好按顺序阅读这些章节,但这并不是强制要求。当某一章节依赖于前一章节的内容时,我会提供必要的参考,你可以回头查看,学习所需的技能。一旦开始阅读某一章节,请从头到尾读完。
以下列表简要描述了每一章的内容。
第一章:通过一个模拟输入读取多个按钮状态 解释了如何通过一个模拟到数字输入引脚读取多个按钮的状态。当你没有足够的输入引脚时,这种方法可以用来创建带有多个按钮的用户输入。
第二章:端口操作 解释了如何使用 Arduino 的数字输出引脚进行端口操作。这样可以减少控制多个输出所需的代码,并且允许你同时控制多个数字输出。
第三章:使用 ATtiny 微控制器 演示了如何在 Arduino 环境中使用更小、更便宜的 ATtiny 微控制器。如果你的项目不需要整个 Arduino 板,使用 ATtiny 可以节省成本。
第四章:构建看门狗定时器 演示了如何构建和使用看门狗定时器——一个小型外部电路,可以在硬件或软件故障导致项目停止工作时重新启动你的项目,通过重置来解决问题。
第五章:使用查理复用控制 LED 介绍了查理复用的概念,一种用更少的数字输出引脚控制多个 LED 的高效方法。
第六章:添加专业电源控制 展示了如何构建一个外部电路,用于为你的 Arduino 项目提供软开关和软关机电源控制。你可以利用这个功能使 Arduino 在任务完成时自动关闭。
第七章:控制交流电主电源插座 讲解了一种通过使用无线遥控插座和一些简单焊接的方法来安全地控制交流电插座。
第八章:控制高功率设备 演示了如何控制高电流移位寄存器 IC,从而让你的项目能够控制比常见移位寄存器 IC 更高的电流。最终项目是一个由超亮 LED 显示屏组成的巨大时钟。
第九章:构建数字音乐播放器和音效板 介绍了廉价的 MP3 播放器模块,并展示了如何为 Arduino 项目添加音频功能,用于通知和娱乐。
第十章:使用相同地址的多个 I²C 设备 演示了如何使用外部控制器 IC 来控制多个共享相同总线地址的 I²C 总线设备。
第十一章:使用 Leonardo 模拟 USB 鼠标和键盘 展示了如何使用 Arduino Leonardo 或兼容板来模拟 USB 鼠标或键盘,实现直接向 PC 软件记录数据或进行有趣的创意项目。
第十二章:向 USB 闪存驱动器传输数据 介绍了如何使用廉价模块直接将数据保存到或从 USB 闪存驱动器读取。
第十三章:与 PS/2 键盘接口 展示了如何将 Arduino 与 PS/2 风格的 PC 键盘接口,使你能够直接将数据输入到 Arduino 中,并为用户界面提供另一种选择。
第十四章:通过蓝牙控制 Arduino 演示了如何使用廉价的蓝牙接口模块与 Arduino 配合使用,并构建一个 Android 应用来远程控制 Arduino 项目。
第十五章:便携式项目的能效 展示了如何将 Arduino 置于各种低功耗和休眠模式,以便在使用电池电源时获得更长的自主性。
第十六章:通过 CAN 总线监控汽车电子 教你如何将机动车 CAN 数据总线与 Arduino 接口,以便查询车辆的发动机管理系统,获取汽车的速度、转速等信息。
第十七章:Arduino 与 RS232 通信 演示了如何通过流行的 RS232 数据总线将 Arduino 与另一个 Arduino 或其他设备接口。
第十八章:Arduino 与 RS485 通信 演示了如何使用 RS485 数据总线与 RS485 兼容设备接口,或在更长的有线距离上传输 Arduino 与 Arduino 之间的通信。
第十九章:ESP32 微控制器平台与物联网 剩余章节使用带有 Wi-Fi 功能的 ESP32 Arduino 兼容板,通过 Wi-Fi 接入点连接到互联网。在第十九章中,你将使用 ESP32 板构建基于 Web 的远程控制系统,用于控制你的项目。
第二十章:通过 Telegram 进行远程控制 展示了如何使用 Telegram 社交媒体应用在移动设备上,几乎在全球任何地方远程控制和与 Arduino 项目进行交互。
第二十一章:从互联网时间服务器获取当前时间 教你如何使用 Wi-Fi 从互联网上的网络时间服务器获取准确的时间和日期。
第二十二章:捕获并将数据记录到 Google 表格 展示了如何将从 ESP32 Arduino 兼容板捕获的数据直接记录到 Google 表格中进行分析和分发。
第二十三章:构建迷你 Web 服务器 解释了如何构建你自己的网页,展示由 ESP32 Arduino 兼容板捕获并分析的数据。
第二十四章:ESP32 摄像头板 展示了如何使用和控制带有数字摄像头的 ESP32 Arduino 兼容板,用于视频流传输或根据请求控制数字摄像头。
最后,如果你需要提醒,可以参考附录,学习如何使用 ZIP 文件安装方法创建并安装 Arduino 库。
我期望你知道的内容
由于这是一本中级教程书籍,我预计你已经知道如何制作一系列 Arduino 项目,从简单的到较为复杂的。你还应该熟悉编写自己的 Arduino 程序并将其上传到开发板。
你应该能够理解电路图,以便构建每个项目所需的电路。在描述与 Arduino 或兼容板的连接时,电路图不会显示整个电路板,而是使用标签来表示应连接到的 Arduino 引脚。例如,考虑图 1 中的电路图。

图 1:一个示例电路图
Arduino 的 D11 引脚连接到 R3,Arduino 的 D10 引脚连接到 R4,Arduino 的 D9 引脚连接到 R5,LED 的负极连接到 GND,而 GND 连接到 Arduino 的 GND 引脚。贯穿本书使用电路图也会提高你阅读复杂电路的能力。
最后,准备好为你的工作站采取适当的安全措施。对于本书中的项目,你将使用基本的手工具、电池供电的电气设备、锋利的刀具、剪刀、烙铁等。像任何爱好或手工艺一样,确保自己和周围人的安全是你的责任。
警告
在你的项目中,绝对不要直接接触市电电流。这应该交给持证的电工来处理,他们经过专业培训。接触市电电流可能会致命。
如果你需要帮助,或者对本书内容有任何反馈或建议,请通过出版商的官网 https://
所需材料
完成本书中的项目,你需要最新版本的 Arduino IDE 软件以及各种硬件部件和配件,包括一些印刷电路板。
Arduino IDE 和 Sketch 文件
如果你还没有这样做,下载并安装最新版的 Arduino IDE 软件。你可以在 https://
接下来,访问本书的官方网站 https://
零件和配件
与其他电子设备一样,Arduino 可以通过许多零售商购买,他们提供各种产品和配件。在购买微控制器时,请选择原装 Arduino 或质量较高的衍生产品——否则你可能会收到故障或性能差的商品。为什么冒着购买劣质板子、最终可能让你付出更高代价的风险呢?要查看正品 Arduino 供应商的列表,请访问 http://
所有项目所需的零件都在各自的章节中列出。我推荐以下这些供应商(按字母顺序排列),用于购买与 Arduino 相关的零件和配件:
Altronics (澳大利亚) https://
Arduino 商店 美国 https://
Newark/element14 https://
PMD Way https://
RS 组件 https://
SparkFun 电子产品 https://
本书中所有项目所需的零件都是常见且容易从列出的各个零售商以及其他你可能已经熟悉的商家处获得的。但别急着去购物。花些时间阅读几章,了解你需要哪些东西,这样你就不会浪费钱购买不必要的东西。
项目 PCB
本书中的所有项目都可以使用一个或多个无焊面包板进行组装,这可能还需要对其他设备进行一些外部焊接。例如,在第七章中,你将用无焊面包板搭建无线交流电插座遥控器的接口电路。然而,对于某些项目,如第五章中的 30 个 LED 矩阵显示器,订购印刷电路板(PCB)将使你的项目更加可靠,且更易于构建。这也能为你的努力提供一个更持久的实例。
下载并解压从 No Starch 网站获取的本书文件后,你会发现一组 ZIP 文件,每个文件都带有一个项目编号。每个项目的 ZIP 文件包含一组名为 Gerber 的文件,PCB 制造商使用这些文件来控制机器,塑造并钻孔空白 PCB,形成所需的配置。
你可以通过使用如 https://

图 2:使用在线 Gerber 查看器显示第 19 项项目的 PCB
让我们一起了解一下订购 PCB 的过程。你不必一次性订购所有书中的 PCB,但请注意,生产周期可能需要两到三周。
首先,访问你首选的 PCB 制造商。我发现以下公司比较可靠(按字母顺序排列):
JLCPCB https://
OSH Park https://
PCB Way https://
如果你想节省开支,可以选择 JLCPCB。我发现总部位于美国的 OSH Park 提供高质量的 PCB,而 PCB Way 则以合理的价格出售优质产品。这仅是一个简单的列表,是我多年来使用过并成功的三家公司。
你选择的制造商网站通常会在首页显示“立即报价”或“快速订购”按钮。点击该按钮后,应该会引导你到一个“添加 Gerber 文件”的按钮。例如,图 3 显示了 PCB Way 网站该页面的截图。

图 3:上传 Gerber 文件到 PCB 工厂
点击 添加 Gerber 文件,导航到你所需的 ZIP 文件,然后点击 打开 或 确定 上传 ZIP 文件到工厂网站。(上传前不需要解压单独的项目 ZIP 文件,因为制造商可以接受未修改的文件。)上传 ZIP 后,网站应显示你的 PCB 的正面和背面,并可能显示尺寸以及初步的价格估算,如 图 4 所示。通常,PCB 的费用可以从较小的项目约 10 美元到较大的项目约 40 美元不等。

图 4:上传 PCB ZIP 文件后显示的 PCB 价格和运费报价示例
一旦你看到你正在订购的 PCB 图像,向下滚动即可看到许多设计选项供你选择,如图 5 所示。

图 5:选择 PCB 选项
不用慌张——你只需要关注其中的一些选项。你可以根据需要更改以下内容:
数量 PCB 工厂通常有最低数量要求,通常是三块或五块电路板,但你可以根据需要订购任意数量的电路板。
焊盘掩膜 这是 PCB 双面保护涂层的颜色。通常,默认颜色是绿色,但你可以根据个人口味或需求更改颜色。
丝网印刷 这是印刷在焊盘掩膜上方的文本颜色,例如电路板的标签或标题。同样,你可以根据个人偏好更改此颜色。
表面处理 默认情况下,工厂通常使用铅基工艺,这已经是几十年的标准。如果你更倾向于无铅环境,或者你将电路板运送到要求无铅 PCB 的国家,你可以将表面处理更改为 HASL 无铅或浸金。
一旦你选择了选项或保留了默认设置,点击下一步或计算来查看最终价格。网站还应该提供多种配送选项,从便宜的邮寄到空运快递,正如图 6 所示。将你的 PCB 保存到购物车中,然后完成最后的支付和配送步骤。

图 6:最终 PCB 报价
你应该收到一套精细加工的 PCB,如图 7 所示。

图 7:用于各类项目的 PCB
这些电路板将成为可靠且耐用项目的基础。
继续
在本介绍中,你了解了本书的结构,在哪里获取零件,以及如何订购本书项目中使用的 PCB。你现在已经准备好继续你的 Arduino 之旅,创造一些你从未想到过的东西,从简单实用到相对复杂。你还将学习到新的方法,应用于你自己的设计,让原创项目栩栩如生。让我们开始吧!
第一章:1 使用一个模拟输入读取多个按钮

与其使用多个数字输入来读取多个按钮,你可以通过一个模拟输入引脚读取多个按钮。这不仅减少了 Arduino 上使用的输入/输出(I/O)引脚数量,还可以节省你的开支,因为你不需要任何外部集成电路(IC),例如移位寄存器或端口扩展器。
在本章中,你将学习:
-
使用电压分压器和按钮创建不同的电压,区分连接到模拟到数字转换器(ADC)引脚的按钮
-
使用一个模拟输入读取三个或六个按钮
-
构建一个 12 按钮键盘,可以通过一个模拟输入读取
电压分压器
本章使用的多按钮方法基于你的 Arduino 的模拟到数字转换器(ADC)引脚,这些引脚返回与连接到 ADC 引脚的电压相关的值。回想一下,你的 Arduino Uno 或兼容板有六个 ADC 引脚,如图 1-1 所示。

图 1-1:Arduino Uno 的模拟输入
若要构建电路,以便通过一个 ADC 引脚一次读取最多 12 个按钮,你需要使用电压分压:即使用两个或更多电阻将较大的电压分压成多个较小的电压,Arduino 可以通过模拟输入引脚逐个读取这些电压。图 1-2 展示了如何使用两个电阻来实现这一目的。

图 1-2:基本电压分压器的原理图
你可以使用以下公式计算输出电压(V[out]):

用相同值的电阻和任何电压替代时,将导致输出电压 V[out]为输入电压 V[in]的一半。例如,如果 R1 和 R2 都是 1 kΩ,并且你有一个 5 V 的输入电压,那么计算为 V[out] = 5 (1,000 / (1,000 + 1,000)),这将得到 2.5 V 的 V[out]。
电阻没有精确的值;电阻越精确,制造起来就越困难。你可以选择不同公差的电阻,例如 5%或 1%。例如,一个 1 kΩ、1%公差的电阻,其值在 990 和 1,010 Ω之间。在构建电压分压器或一般电路时,尽量使用公差为 1%的电阻——它们与 5%电阻相比价格差不多,但你的结果会更精确。
在构建电压分压器时,确保你的电阻能够承受你计划通过它们的功率。例如,如果你使用的是 0.25 W 的电阻,并且需要将 24 V DC 分压为 12 V,你最多只能通过分压器流过 20 mA 的电流。使用公式 W = V × A(瓦特 = 电压(DC)× 电流(安培))来帮助你计算消耗的功率。
将 图 1-2 中的示例电压分压器与两个按钮结合,得到如 图 1-3 所示的电路。

图 1-3:带有两个按钮的电压分压器
当你按下第一个按钮时,A 点的电压应为 2.5 V DC,因为电流从 5 V 电源流出并通过两个电阻分压。当按下第二个按钮时,B 点的电压应为 0 V DC,因为 B 点直接连接到 GND。如果你将 A 点和 B 点连接到模拟输入端口,你应该能够检测到按下的是哪个按钮,因为 ADC 返回的值对于每个按钮会有所不同(理论上,按钮 1 的值应该为 512,按钮 2 的值应该为 0)。你只用一个输入端口读取了两个按钮的状态!
如果你同时按下两个或更多按钮,另一个电压—由按下的按钮与电阻分压器的组合决定—应返回给 ADC。确定值的最简单方法是搭建电路并进行测试。通过电压分压,你可以使用一系列相同值的按钮和电阻,生成不同的输出电压,使得 Arduino 的模拟输入能够读取,从而区分每个按钮。
项目 #1:三个按钮与一个模拟输入
让我们从一个简单的多按钮使用示例开始:通过一个模拟输入读取三个按钮。你需要以下零件来完成此项目:
-
一块 Arduino Uno 或兼容板及 USB 电缆
-
三个 1 kΩ、0.25 W、1% 的电阻
-
一个 10 kΩ、0.25 W、1% 的电阻
-
三个触觉按钮
-
一个无焊接面包板
-
公对公跳线
按照 图 1-4 所示的电路在无焊接面包板上组装电路,并将标有 5V、GND 和 A0 的点分别连接到 Arduino 的 5V、GND 和 A0 引脚。

图 1-4:项目 #1 的电路
现在输入并上传项目 #1 的代码到你的 Arduino。稍等片刻,然后在 Arduino IDE 中打开串口监视器,开始依次按下按钮。当你没有按下任何按钮时,ADC 返回的值应保持在大约 1,010 左右。试着按下按钮 1、2 和 3,查看不同的返回值,如 图 1-5 所示。

图 1-5:项目 #1 的示例输出
让我们看看代码,了解它是如何工作的:
// Project #1 - Reading three buttons from one analog input
void setup()
{
Serial.begin(9600);
}
void loop()
{
❶ int sensorValue = analogRead(A0);
❷ Serial.println(sensorValue);
delay(100);
}
项目 #1 中的代码读取模拟引脚 A0 ❶ 测得的值,并在串口监视器 ❷ 上显示出来。这是为了演示三按钮电路生成的不同模拟值。上传代码后,打开串口监视器并开始按下按钮。当按下按钮 1 时,示例返回的值介于 700 和 704 之间;按下按钮 2 时,值介于 383 和 385 之间;按下按钮 3 时,值为 0。由于电阻的公差,您的值可能会略有不同。
当按下按钮时,电流至少会通过一个电阻作为您的 R1,并且根据所按的按钮,电流会通过某些电阻,这些电阻作为电压分压器。
使用电阻分压公式,您可以计算出按下按钮 1 时,R1 值为 1 kΩ,R2 值为 2 kΩ(按钮连接点以下的电阻之和)。使用 V[out] = 5 (2,000 / (1,000 + 2,000)),理论上呈现给 ADC 的电压为 3.33 V。如果按下按钮 2,则 V[out] = 5 (1,000 / (1,000 + 2,000)) 结果为 1.66 V。如果按下按钮 3,则 V[out] = 5 (0 / (3,000 + 0)) 结果为 0 V,因为 ADC 直接连接到 GND。
最后,如果没有按下任何按钮,ADC 返回的值应接近 1,010。该项目通过电路中的 10 kΩ 上拉电阻实现此功能,电阻将 5V 电源与模拟输入连接。如果没有这个电阻,ADC 将“浮动”,报告随机值,这些值可能会落入其他按钮的范围内。这一点很重要,因为我们需要在没有按下任何按钮时返回一个恒定的值范围。
实际操作中,ADC 返回的值会有些许变化。例如,Arduino 的 5V 引脚(如果通过 USB 供电)测得的值略低,因为一些电压在微控制器中损失,这会影响 ADC 代码中的计算。始终使用您预期的电源(USB 或外部 DC 电源)为 Arduino 供电,并测试代码,以确保按钮读取代码的准确性。
现在,您已经实验了三个按钮,我们来尝试一个更复杂的设计。
项目 #2:六个按钮与一个模拟输入
该项目通过一个模拟引脚读取六个按钮,并提供改进的按钮状态报告草图。您将需要以下部件:
-
一块 Arduino Uno 或兼容板以及 USB 电缆
-
六只 1 kΩ、0.25 W、1% 的电阻
-
一只 1 kΩ、0.25 W、1% 的电阻
-
六个触觉按钮
-
一块无焊接面包板
-
公对公跳线
按照 图 1-6 所示,在无焊接面包板上组装电路,并将标记为 5V、GND 和 A0 的点分别连接到 Arduino 的 5V、GND 和 A0 引脚。

图 1-6:项目 #2 的电路
现在输入并上传以下草图到你的 Arduino:
// Project #2 - Reading six buttons from one analog input
void setup()
{
Serial.begin(9600);
}
❶ int analogButton()
// Returns number of button pressed by comparing ADC value
{
int adcValue;
int button;
❷ adcValue = analogRead(A0);
if (adcValue>1000) {button = 0;}
else if (adcValue>857 && adcValue<861) {button = 1;}
else if (adcValue>696 && adcValue<700) {button = 2;}
else if (adcValue>535 && adcValue<539) {button = 3;}
else if (adcValue>371 && adcValue<375) {button = 4;}
else if (adcValue>199 && adcValue<203) {button = 5;}
❸ else if (adcValue>=0 && adcValue<16) {button = 6;}
❹ return button;
}
void loop()
{
❺ switch(analogButton()) // Read button status
❻ {
case 0 : Serial.println("No button pressed"); break;
case 1 : Serial.println("Button 1 pressed"); break;
case 2 : Serial.println("Button 2 pressed"); break;
case 3 : Serial.println("Button 3 pressed"); break;
case 4 : Serial.println("Button 4 pressed"); break;
case 5 : Serial.println("Button 5 pressed"); break;
case 6 : Serial.println("Button 6 pressed"); break;
}
delay(250);
}
为了简化按钮电路的使用,自定义的 analogButton() 函数 ❶ 返回被按下的按钮编号,如果没有按钮被按下,则返回 0。该函数从模拟引脚 A0 ❷ 获取读数,并将其与每个按钮按下时返回的期望范围进行比较 ❸。该草图返回按钮编号,作为 analogButton() 函数的结果 ❹。主循环中的自定义函数检查按钮是否被按下 ❺,而 switch…case 函数则根据不同的按钮按下执行相应操作 ❻。
你从模拟输入获取的所需范围会有所不同;使用项目 #1 中的草图来确定本项目中需要使用的确切范围。
项目 #3:一个带有一个模拟输入的 12 按钮键盘
本项目将项目 #2 的电路扩展至 12 个按钮,创建一个更大的最终设计,并使用自己的 Arduino 库。使用这个库可以减少读取按钮所需的主草图中的代码。虽然可以在无焊接面包板上搭建这个电路,但我推荐你下载本项目的 PCB 文件并制作一个永久版。
如果你使用面包板,你将需要以下部件:
-
一个 Arduino Uno 或兼容板和 USB 线
-
十二个 1 kΩ, 0.25 W, 1% 的电阻
-
一个 10 kΩ, 0.25 W, 1% 的电阻
-
十二个触觉按钮
-
一个无焊接面包板
-
公对公跳线
如果你使用 PCB,你将需要以下部件:
-
一个 Arduino Uno 或兼容板和 USB 线
-
十二个 1 kΩ, 0.25 W, 1% 的电阻
-
一个 1 kΩ, 0.25 W, 1% 的电阻
-
十二个 6 × 6 × 4.3 mm 触觉按钮
-
1 × 3 排 2.54 mm 排头针
-
公对母跳线
图 1-7 显示了本项目的电路原理图。

图 1-7:项目 #3 的电路原理图
如果你正在构建 PCB 电路,部件上会有标签,帮助你将组件准确放置在 PCB 上,如图 1-8 所示。

图 1-8:项目 #3 的 PCB 顶面
安装并焊接电阻后再安装按钮。通常,在 PCB 上组装电路时,应首先从最低高度的部件开始,然后逐步安装较大或较重的部件。排头针通常以 40 针为一条出售,因此你需要修剪出一段 3 针的部分来使用于本项目。
一旦组装完成,您的项目应该类似于图 1-9 所示的电路板。

图 1-9:完成的项目#3 PCB
现在,按照图 1-10 所示,通过 5V、GND 和 A0 引脚将电路连接到 Arduino。

图 1-10:连接到 Arduino Uno 的键盘
接下来,通过从项目#1 上传草图,确定每个按钮的模拟值,并将其插入到库的源文件中。打开串行监视器,依次按下每个按钮,并记录监视器显示的值。您可以使用这些值为该项目创建库的范围。
为了简化键盘的使用,安装以下模拟键盘库,接下来的三个文件列出了该库的内容。(有关编写和安装您自己的 Arduino 库的详细信息,请参见附录 A。)以下代码是源文件;使用项目#1 中的草图和本项目的键盘来确定您的ADCvalue范围。
// analogkeypad.cpp source file
#include "Arduino.h"
#include "analogkeypad.h"
analogkeypad::analogkeypad()
{
}
int analogkeypad::readKeypad()
{
int adcValue;
int button;
adcValue = analogRead(A0);
if (adcValue>`1000`) {button = 0;}
else if (adcValue>`941` && adcValue<`944`) {button = 1;}
else if (adcValue>`863` && adcValue<`868`) {button = 2;}
else if (adcValue>`790` && adcValue<`795`) {button = 3;}
else if (adcValue>`716` && adcValue<`720`) {button = 4;}
else if (adcValue>`643` && adcValue<`648`) {button = 5;}
else if (adcValue>`570` && adcValue<`573`) {button = 6;}
else if (adcValue>`493` && adcValue<`496`) {button = 7;}
else if (adcValue>`413` && adcValue<`418`) {button = 8;}
else if (adcValue>`325` && adcValue<`329`) {button = 9;}
else if (adcValue>`231` && adcValue<`235`) {button = 10;}
else if (adcValue>`129` && adcValue<`133`) {button = 11;}
else if (adcValue>`13` && adcValue<`18`) {button = 12;}
return button;
}
这是头文件:
// analogkeypad.h header file
#ifndef analogkeypad_h
#define analogkeypad_h
#include "Arduino.h"
class analogkeypad
{
public:
analogkeypad();
int readKeypad();
private:
int adcValue;
int button;
};
#endif
这是KEYWORDS.TXT文件:
Analogkeypad. KEYWORD1
readKeypad. KEYWORD2
安装库后,上传项目#3 的匹配草图:
// Project #3 - Reading 12 buttons from 1 analog input
#include <analogkeypad.h>
❶ analogkeypad keypad;
void setup()
{
Serial.begin(9600);
}
void loop()
{
❷ switch (keypad.readKeypad()) // Read button status
{
❸ case 0 : Serial.println("No button pressed"); break;
case 1 : Serial.println("Button 1 pressed"); break;
case 2 : Serial.println("Button 2 pressed"); break;
case 3 : Serial.println("Button 3 pressed"); break;
case 4 : Serial.println("Button 4 pressed"); break;
case 5 : Serial.println("Button 5 pressed"); break;
case 6 : Serial.println("Button 6 pressed"); break;
case 7 : Serial.println("Button 7 pressed"); break;
case 8 : Serial.println("Button 8 pressed"); break;
case 9 : Serial.println("Button 9 pressed"); break;
case 10 : Serial.println("Button 10 pressed"); break;
case 11 : Serial.println("Button 11 pressed"); break;
case 12 : Serial.println("Button 12 pressed"); break;
}
delay(250);
}
该草图的结果与项目#2 相同,但允许在主循环中添加更多按钮。它首先包含模拟键盘库❶,并启动键盘的一个实例;接下来,通过库函数❷获取按下的按钮编号。然后,草图将按钮编号发送到串行监视器,在switch…case函数❸中显示。
上传草图后,打开串行监视器并依次按下按钮。这应该会产生类似于图 1-11 所示的输出。

图 1-11:项目#3 的示例输出
您可以在自己的项目中使用此草图,只需在需要时查询键盘,同时使用库和readKeypad()函数。
注意
在您自己的项目中,如果使用了本章的草图,可以使用除 Uno 以外的其他 Arduino 类型,只要它们有模拟输入。如果您的电路板使用 3.3V 而不是 5V,请在更新项目#2 中的自定义函数或项目#3 中使用的库之前,使用项目#1 中的草图来确定电路所需的模拟值。
继续
在本章中,你学会了构建一个电路,使得你的 Arduino 仅使用一个模拟输入引脚就能读取多达 12 个按钮的状态。在下一章中,你将学习另一种提高项目效率的方法:通过端口操作同时读取和写入数字 I/O 引脚。
第二章:2 端口操作

本章介绍了端口操作,即同时访问你的 Arduino 数字输入和输出引脚的过程,以提高控制或读取它们状态的速度。这样可以减少响应时间,即完成数字读写所需的时间,从而提高草图的操作速度。端口操作还比常规的数字写入/读取函数使用更少的代码,减少了草图的大小,这在遇到 Arduino 内存限制时非常有用。
本章中,你将学习:
-
使用端口操作快速控制数字输出
-
控制七段 LED 显示器
-
同时读取多个数字输入引脚
-
读取二进制编码十进制(BCD)开关
在这个过程中,你将构建一个二进制数字显示器、一个发光二极管(LED)闪烁波形和一个单数字 LED 骰子。本章中的说明适用于 Arduino Uno 或兼容板、原始 Nano 或其他使用 ATmega328 系列微控制器的 Arduino 兼容设备。
端口、引脚和寄存器
端口是一个包含顺序排列 I/O 引脚的集合。每个端口都有一个寄存器(微控制器内的一段内存),写入该寄存器可以控制输出状态,或者如果引脚状态被设置为输入,则从该寄存器读取。你还需要使用另一个寄存器——数据方向寄存器(DDR)——来设置端口中的引脚为输入或输出。
图 2-1 显示了引脚及其所在的端口。端口 B 包含数字引脚 D13 到 D8,端口 C 包含模拟引脚 A5 到 A0,端口 D 包含数字引脚 D7 到 D0。

图 2-1:Arduino Uno 的端口
要将引脚设置为输入或输出,你不需要像通常那样在 void setup() 中使用 pinMode()。相反,使用以下简单语法:
DDR`x` = B`yyyyyyyy`
DDRx是数据方向寄存器,其中x是端口(B、C 或 D),每个y对应端口中的一个引脚(从最高到最低)。B表示yyyyyyyy是一个二进制数。例如,要将整个 D 端口设置为数字输出,你可以在void setup()中使用以下代码:
DDRD = B11111111;
要将 D13、D12 和 D11 数字引脚设置为输入,将 D9、D8 和 D7 数字引脚设置为输出,请使用以下代码:
DDRB = B000111; // Port B has six pins in total
你可以看到与多个或循环使用pinMode()相比,这样节省了多少草图空间。
除了简化代码和缩短响应时间,端口操作还可以在控制数字输出引脚时节省硬件成本。例如,过去你可能会使用移位寄存器 IC 来控制 LED 显示器,而端口操作可以让你使用备用的 I/O 引脚组来实现相同的功能。
让我们通过一些项目将这一理论付诸实践。
项目#4:提高数字输出引脚速度
在这个项目中,你将通过控制八个数字输出进行端口操作练习,使用 LED 显示输出状态。
你需要以下元件:
-
一块 Arduino Uno 或兼容板及 USB 电缆
-
八个 1 kΩ,0.25 W,1%电阻
-
八个 LED
-
一块无焊面包板
-
公对公跳线
为了减少本书中使用的元件类型数量,从而降低完成项目的成本,本项目使用了与前一章相同的 1 kΩ电阻,而不是通常使用的 560 Ω或类似的电阻。LED 的亮度依然足够。
按照图 2-2 所示,将电路组装在无焊面包板上,连接 D7 到 D0 以及 GND 标记的点到 Arduino 的相应引脚。

图 2-2:项目#4 的原理图
输入并上传以下草图到你的 Arduino:
// Project #4 - Fast digital outputs
void setup()
{
❶ DDRD = B11111111; // Set PORTD (digital 7 through 0) to outputs
}
void loop()
{
❷ PORTD = B11111111;
delay(500);
❸ PORTD = B00000000;
delay(500);
}
上传完成后,所有 LED 应每半秒闪烁一次。D 端口上的引脚被设置为输出❶,并且先全部打开❷,然后再次关闭❸。这个简单的例子展示了实现大量数字引脚控制所需的极少代码。
请将本项目的硬件保管好,因为你将在项目#5 中再次使用它。
速度优势
Arduino 环境使用一个硬件抽象层,借助软件函数和类简化了控制 Arduino 板或兼容电路的硬件操作。此代码需要额外的处理时间,通常会导致草图执行速度变慢。端口操作可以绕过硬件抽象层,从而大大减少 CPU 执行相同任务所需的代码量。
为了观察端口操作如何加速 I/O 引脚的操作,本节将展示如何使用测试设备(如频率计或数字存储示波器(DSO))来测量数字引脚端口的开关频率。如果你有示波器或频率计,可以跟着一起操作。
首先,使用常规的 Arduino 草图函数测试引脚速度,使用清单 2-1 中的草图,并将数字引脚连接到示波器。
void setup()
{
for (int a = 0; a < 8; a++)
{
pinMode(a, OUTPUT);
}
}
void loop()
{
for (int a = 0; a < 8; a++)
{
digitalWrite(a, HIGH);
}
for (int a = 0; a < 8; a++)
{
digitalWrite(a, LOW);
}
}
清单 2-1:一个引脚速度测试,依次打开和关闭数字引脚 D7 至 D0,并重复此操作
图 2-3 展示了此测量的结果:平均频率为 15.15 kHz,意味着将所有八个数字引脚开关一次的时间间隔约为 0.000066 秒(66 微秒)。

图 2-3:运行清单 2-1 测试的结果
现在,使用清单 2-2 中修改过的草图,使用端口操作重复此测试。
void setup()
{
DDRD = B11111111; // Set PORTD (digital 7 through 0) to outputs
}
void loop()
{
PORTD = B11111111;
PORTD = B00000000;
PORTD = B11111111;
PORTD = B00000000;
PORTD = B11111111;
PORTD = B00000000;
PORTD = B11111111;
PORTD = B00000000;
PORTD = B11111111;
PORTD = B00000000;
PORTD = B11111111;
PORTD = B00000000;
PORTD = B11111111;
PORTD = B00000000;
}
清单 2-2:使用端口操作测试引脚速度
清单 2-2 中的PORTD命令展示了将引脚开关足够长时间,以便示波器能够捕捉到此操作的真实速度。如果只有两个PORTD命令(分别控制开和关),时间测量将包括引脚关闭与循环重新开始之间的时间间隔。
图 2-4 展示了该草图在示波器上的结果。

图 2-4:“快速”测试的结果
第二次测试的结果是平均频率为 8.06 MHz,即将所有八个数字引脚开关一次的时间间隔约为 0.00000012406 秒(0.12405 微秒)。平均而言,使用端口操作使这些引脚的开关速度比正常草图函数快约 532 倍。
你可以看到,使用端口操作如何提高操作速度,并减少草图中所需的代码量。在下一个项目中,我们将通过控制 LED 显示来演示端口操作的这些优势。
项目 #5:显示二进制数字
在这个项目中,你将继续巩固端口操作的知识,通过这种方法使用八个 LED 显示二进制数字。
这个项目使用与项目 #4 相同的硬件。一旦上传了草图,LED 应依次点亮,显示从 0 到 255 的二进制数字,最不重要的位位于 LED 的右侧。
// Project #5 - Display binary numbers
void setup()
{
❶ DDRD = B11111111; // Set PORTD (digital 7 through 0) to outputs
}
void loop()
{
for (int a = 0; a < 256; a++)
{
❷ PORTD = a;
delay(250);
}
}
我们再次将端口 D 的引脚设置为输出 ❶。然后将PORTD 设置为整数 a 的递增值。由于端口寄存器包含 8 位,我们可以将 0 到 255 之间的整数赋值给端口,从而使输出与分配给寄存器的数字的二进制等效值匹配 ❷。
在继续之前,让我们通过在一个方向上点亮和熄灭 LED,然后在另一个方向上进行练习,掌握端口操作中的位移技巧。
项目 #6:创建一个闪烁的 LED 波
在这个项目中,使用来自项目 #4 和 #5 的硬件,你将创建一个波浪状的光效图案,以模拟 1980 年代电视剧《骑士先锋》中著名的 K.I.T.T. 车辆的车灯。一旦上传了这个草图,你应该会看到你自己版本的 K.I.T.T. 的标志性滚动灯光。
// Project #6 - Creating a blinking LED wave
void setup()
{
DDRD = B11111111; // Set PORTD (digital 7 through 0) to outputs
}
void loop()
{
❶ for (int k = 0; k < 8; k++)
{
❷ PORTD = 1 << k;
delay(100);
}
❸ for (int k = 6; k > 0; —k)
{
PORTD = 1 << k;
delay(100);
}
}
这个草图有两个循环:一个从右到左闪烁 LED,另一个则是相反的方向。我们使用位移操作将数字 1 从右到左移动沿着端口寄存器。我们循环八次,处理八个 LED ❶。通过将 1 赋值给端口寄存器 ❷,激活 LED,这将在第二次循环中点亮数字引脚 D0 上的 LED。
k 的值为 1,所以我们将位向左移一位(使用 <<)在端口寄存器中,点亮数字引脚 D1 上的第二个 LED。这个过程会重复,直到所有八个 LED 被依次点亮和熄灭。以下是代表这一事件序列的端口寄存器字节列表:
00000001 // PORD = 1 << 0
00000010 // PORD = 1 << 1
00000100 // PORD = 1 << 2
00001000 // PORD = 1 << 3
00010000 // PORD = 1 << 4
00100000 // PORD = 1 << 5
01000000 // PORD = 1 << 6
10000000 // PORD = 1 << 7
该过程然后在第二个循环中以反向重复 ❸,从将位放入第七位开始(01000000),然后继续到位 1。对于这个第二个循环,我们不需要在位置 0 放置位,因为那样会连续点亮第一个 LED 两次。
接下来的两个项目展示了端口操作的另一个便捷应用:使用七段 LED 显示器,这实际上是八个 LED 排列成一个数字并可以选择性地显示小数点。
项目 #7:控制七段 LED 显示屏
人们通常使用外部移位寄存器 IC,如 74HC595,通过七段 LED 显示屏显示简单的数字信息。然而,如果你有多余的数字 I/O 引脚,你可以通过使用本项目中的方法节省零件费用。本项目还包含了按位运算的简要回顾。
你将需要以下零件来完成这个项目:
-
一块 Arduino Uno 或兼容板以及 USB 电缆
-
八个 1 kΩ、0.25 W、1% 的电阻
-
一个共阴极七段 LED 显示屏(兼容 FND500)
-
一块无焊接面包板
-
公对公跳线
按照图 2-5 中所示的电路图,在无焊接面包板上组装电路,并将标记为 D7 至 D0 和 GND 的点连接到 Arduino 的相应引脚。

图 2-5:项目 #7 的电路图
一旦上传了草图,数字 0 到 9 应该依次显示,然后再次显示一次,这次 LED 显示屏上的小数点会亮起。
让我们看看这如何工作:
// Project #7 - Controlling seven-segment LED displays with port manipulation
❶ int digits[] = {B11111100, // 0
B01100000, // 1
B11011010, // 2
B11110010, // 3
B01100110, // 4
B10110110, // 5
B10111110, // 6
B11100000, // 7
B11111110, // 8
B11110110}; // 9
void setup()
{
❷ DDRD = B11111111; // Set PORTD (digital 7 through 0) to outputs
}
void loop()
{
for (int a = 0; a < 10; a++)
{
❸ PORTD = digits[a];
delay(250);
}
for (int a = 0; a < 10; a++)
{
❹ PORTD = digits[a]|B00000001; // Activate decimal point (D0)
delay(250);
}
}
该草图定义了一个包含 10 个元素 ❶ 的数组,每个元素包含一个二进制数字,表示需要打开或关闭的 LED,以显示每个数字。从图 2-5 的电路图中可以看到,显示屏的第一个引脚(A)连接到数字引脚 D7,依此类推。在这个项目中,端口 D 被设置为 ❷。显示所需数字的操作很简单,只需将所需的数组元素(0 到 9)分配给 ❸ 和 ❹ 处的 PORTD 即可。
循环 ❹ 打开小数点。每个数组元素中的第 0 位代表显示屏中的小数点 LED,因此草图需要使用按位运算中的 OR 函数(|)来更改第 0 位。当你使用 OR 比较两个比特时,如果其中一个比特是 1,或者两个比特都是 1,结果就是 1。
1|0 = 1 // 1 or 0 = 1
0|1 = 1 // 0 or 1 = 1
1|1 = 1 // 1 or 1 = 1
0|0 = 0 // 0 or 0 = 0
因此,你可以通过以下方法将小数点添加到数字字节中(在草图中,数字字节被设置为显示数字 1 和小数点):
B01100000 | // Byte to display digit 1
B00000001 = // "or" decimal point bit
B01100001 // Gives us digit 1 with decimal point
未来,如果你在自己的项目中使用带有端口操作的 LED 显示屏,可以利用以下自定义函数显示带或不带小数点的数字。你也可以通过将其添加到项目 #7 来进行测试。
void display(int n, boolean point)
{
if (point == false) // No decimal point
{
PORTD = digits[n];
}
else if (point == true) // Decimal point
{
PORTD = digits[n]|B00000001;
}
}
如果你想关闭当前已打开的某个特定输出,而不影响其他引脚,可以使用按位与函数 &。当比特与 & 0 比较时,如果比特是 1,结果为 0。例如:
1&0 = 0 // 1 or 0 = 0
0&1 = 0 // 0 or 1 = 0
要关闭第一个和最后一个输出(当所有引脚都打开时),你可以使用以下代码,这将保留位 6 到位 1 打开,而位 7 和位 0 关闭:
PORTx = B11111111 &
B01111110;
现在你已经连接了 LED 显示电路,接下来让我们制作一个电子骰子。
项目 #8:创建一个电子骰子
在这个项目中,你将创建一个电子骰子,生成一个 1 到 6 之间的随机数,模拟掷物理骰子的过程。
该项目使用与项目 #7 相同的硬件。上传草图后,显示屏应迅速滚动显示数字,然后逐渐变慢直到停止。
// Project #8 - Single-digit electronic die
int digits[] = {B11111100, // 0
B01100000, // 1
B11011010, // 2
B11110010, // 3
B01100110, // 4
B10110110, // 5
B10111110, // 6
B11100000, // 7
B11111110, // 8
B11110110}; // 9
❶ void display(int n, boolean point)
{
if (point == false)
{
PORTD = digits[n];
} else if (point == true)
{
PORTD = digits[n] | B00000001;
}
}
void displayRandom(int _delay)
{
display(random(1, 7), false); // Display the number
if (_delay > 0)
{
delay(_delay); // Hold the display on for the delay received
}
else if (_delay == 0)
{
do // The delay entered was 0, hold the display on
{}
while (1);
}
}
void setup()
{
DDRD = B11111111; // Set PORTD (digital 7 through 0) to outputs
❷ randomSeed(analogRead(0)); // Seed the random generator
}
void loop()
{
int a;
// Cycle the LEDs around for effect
❸ for (a = 0; a < 100; a++)
{
displayRandom(50);
}
// Display numbers with increasing delay
❹ for (a = 1; a < 10; a++)
{
displayRandom(a * 100);
}
// Stop at the final random number and LED
❺ displayRandom(0);
}
该草图结合了新代码和项目 #7 中使用的函数。草图首先使用该项目中的自定义函数显示数字 ❶,然后初始化随机数生成器 ❷。它迅速显示 100 个随机数以产生视觉效果 ❸,然后显示接下来的 10 个随机数,每个数字之间的时间延迟逐渐增加 ❹。最后,在显示最后一个随机数 ❺ 后,一切停止。要显示另一个随机数,按下重置按钮。
项目 #9:同时读取多个数字输入
你还可以使用端口式操作快速读取数字输入引脚,而不是通过使用多个 digitalRead() 函数。因为每个端口返回 8 位数据(包括 B 和 C 端口,虽然它们只有六个物理引脚),所以我们只需将所需端口设置为输入,然后将端口的值赋给变量。该值是一个 8 位数字,表示输入引脚的状态。
一旦使用 DDRx 函数在 void setup() 中设置了输入,只需将端口赋值给一个变量来读取引脚的状态。例如,一旦将端口 D 设置为输入,就可以使用以下代码读取其状态:
int port = PIND; // Value of pins on PORTD is stored in integer "port"
该项目演示了如何一次性读取整个端口,然后使用另一个端口的 LED 显示该端口的状态。
你将需要以下部件:
-
一块 Arduino Uno 或兼容板及 USB 数据线
-
四个 1 kΩ,0.25 W,1% 的电阻
-
四个 10 kΩ,0.25 W,1% 的电阻
-
四个 LED
-
四个触摸按钮
-
无焊面包板
-
男对男跳线
按照 图 2-6 所示的电路在无焊面包板上组装电路,并将标记为 D7 到 D4,D13 到 D10,5V 和 GND 的点连接到 Arduino 的相应引脚。

图 2-6:项目 #9 的电路图
上传草图后,按下按钮应点亮对应的 LED。
让我们看看这个是如何工作的:
// Project #9 - Simultaneous reading of Arduino digital inputs
void setup()
{
❶ DDRB = B00000000; // Set PORTB (digital 13 through 8) to inputs
❷ DDRD = B11111111; // Set PORTD (digital 7 through 0) to outputs
}
void loop()
{
❸ PORTD = PINB<<2; // Bit-shift PORTB value as only 6 bits
}
这个示例展示了通过端口操作使代码最小化的可能性:你可以在一行中读取最多八个按钮的状态,而不是使用八个单独的 digitalRead() 函数。在设置引脚状态❶后,示例将引脚的值(PINB)赋给端口 D,从而按照指示设置输出❷。
接下来,代码将 PINB 的值左移 2 位❸。B 端口只有六个物理引脚,但读取时仍会返回一个 8 位数字。最显著的两位是 0,最后六位代表 D13 到 D8 的状态。因此,本项目将 PINB 的值左移,以便与端口 D 的输出控制匹配。
二进制编码十进制开关
项目 #9 中展示的数字输入读取形式非常适合多输出的输入设备,例如带有 BCD 输出的旋转开关,BCD 输出代表 0 到 9 之间数字的二进制值。BCD 开关是一种方便的用户输入方法,用于设置选项、数值或其他项目需求,其中用户输入一个 0 到 9 之间的数字。

图 2-7:典型 BCD 开关的原理符号
BCD 开关提供 10 个位置选择,并且有 4 个输出引脚,可以通过 4 个数字输入引脚轻松读取。第五个引脚通常连接到 5V 引脚,这允许电流通过切换的输出引脚。图 2-7 展示了典型 BCD 开关的原理符号。

图 2-8:推轮 BCD 开关
BCD 开关的例子包括复古风格的“推轮”开关,如图 2-8 所示,以及旋转开关,如图 2-9 所示。

图 2-9:旋转 BCD 开关
为了进行简单的 BCD 实验,使用图 2-9 中展示的旋转 BCD 开关更为方便,因为它不需要任何焊接。
使用任何开关时,用户必须小心将杠杆或指针移到正确的位置。例如,如果你改变旋转开关并将指针留在两个数字之间,它将无法返回正确的值。下一个项目展示了如何使用端口操作实现这些开关。
项目 #10:读取 BCD 开关
本项目演示了如何通过端口操作读取 BCD 开关,将值以二进制和十进制返回到串行监视器,允许你简单高效地接收数字用户输入。
你需要以下组件:
-
一块 Arduino Uno 或兼容板和 USB 线
-
一个 BCD 旋转开关或推钮开关
-
一块无焊接面包板
-
公对公跳线
按照图 2-10 所示,在无焊接面包板上组装电路,并将标记为 5V 和 D11 到 D8 的点连接到 Arduino 的相应引脚。

图 2-10:项目#10 的电路图
输入并上传草图,然后打开串行监视器。你应该能看到开关设置的值以二进制和十进制显示,如图 2-11 所示。

图 2-11:项目#10 的示例输出
让我们看看它是如何工作的:
// Project #10 - Reading BCD switches
void setup()
{
❶ DDRB = B11110000; // Set PORTB (digital 11 through 8) to inputs
Serial.begin(9600);
}
void loop()
{
Serial.print("Switch value - Binary: ");
❷ Serial.print(PINB,BIN);
Serial.print(", Decimal: ");
❸ Serial.println(PINB);
delay(250);
}
代码将 D11 到 D8 的引脚设置为输入,将 D12 和 D13 设置为输出❶,因为它不想返回 D12 和 D13 的引脚值。然后,它简单地将端口 B 的值以二进制❷和十进制❸形式返回给串行监视器。二进制值应反映来自开关到 Arduino 输入引脚的实际信号,而十进制是相同信号的整数等价物。
继续前进
本章向你展示了如何通过使用端口操作,更快速高效地使用 Arduino 的数字输入和输出引脚,控制许多 LED 而无需外部 IC,并接收设置而不需要使用显示器或多个开关。在下一章,你将学习如何使用体积更小、成本更低的 ATtiny 系列微控制器来进行不太复杂的 Arduino 项目。
第三章:3 使用 ATtiny 微控制器

到目前为止,在你的 Arduino 旅程中,你可能最常使用的是微芯片 ATmega328P-PU。然而,对于较小的项目,你可以通过使用像 ATtiny85 这样的微控制器来节省成本和功耗。本章将教你如何为 ATtiny 微芯片配置 Arduino IDE。
在为你的 ATtiny85 配置 Arduino IDE,并通过闪烁 LED 演示工具链正常工作后,你将学习如何:
-
了解 ATtiny 引脚参考及其在 Arduino 环境中的功能
-
为 ATtiny 电路添加重置按钮
-
使用 ATtiny 进行端口操作
-
更改 ATtiny 的操作速度
你还将构建一个快速读取的温度计,并设计一个 Arduino 编程扩展板,让你轻松将代码上传到 ATtiny85 微控制器。
ATtiny85 微控制器
图 3-1 展示了紧凑型的 ATtiny85 微控制器。

图 3-1:ATtiny85 微控制器的通孔封装
除了尺寸外,ATtiny85 和常见的 ATmega328P-PU 微控制器之间还有一些重要的差异,这些差异影响它们在 Arduino 环境中的使用,正如在图 3-2 和表 3-1 中所示。

图 3-2:ATtiny85(左)和 ATmega328P-PU(右)的引脚图。
表 3-1: ATtiny85 和 ATmega328P-PU 的规格
| ATtiny85 | ATmega328P-PU | |
|---|---|---|
| 最大处理速度 | 20 MHz | 16 MHz |
| 数字引脚数量 | 最多 5 | 14 |
| 模拟输入引脚数量 | 最多 3 | 6 |
| 闪存 | 8KB | 32KB |
| 静态随机存取内存(SRAM) | 512 字节 | 2KB |
这个表格帮助您确定哪些电子组件可以与 ATtiny85 一起实际使用。简而言之,ATtiny 适合预算有限、需要较少输入输出引脚但能从提高能效中受益的项目。
使用 Arduino IDE 的 ATtiny 芯片
麻省理工学院的高低技术小组首先创建了在 Arduino IDE 中使用 ATtiny85 微控制器的代码。然而,在实现此代码之前,您需要在 Arduino IDE 中安装 ATtiny 支持。
手头有 ATtiny85 时,打开 Arduino IDE,然后选择文件
偏好设置。当偏好设置对话框出现时,点击附加板管理器 URLs 字段右侧的小按钮。附加板管理器 URLs 对话框将出现,如图 3-3 所示。

图 3-3:Arduino IDE 附加板管理器网址对话框
在字段中输入以下 URL。如果您之前的项目中已经有其他 URL,请在最后一个 URL 后加上逗号,并在逗号后输入这一新行:
https://raw.githubusercontent.com/damellis/attiny/ide-1.6.x-boards-manager/package_damellis_attiny_index.json
点击确定关闭附加 URL 对话框,然后点击确定关闭偏好设置对话框。
关闭并重新打开 IDE。如果您的计算机未连接互联网,请现在连接。选择工具
板管理器。当板管理器在 IDE 的左侧打开时,在搜索框中输入attiny。
ATtiny 包将出现,如图 3-3 所示。点击安装,然后等待几秒钟安装完成。您可以在 IDE 的输出窗口底部看到进度:
Downloading packages
Attiny:avr@1.0.2
Installing attiny:avr@1.0.2
Configuring platform
attiny:@1.0.2 installed
最后,通过选择工具
板
attiny
ATtiny25/45/85来检查安装是否成功,如图 3-4 所示。

图 3-4:现在可以在板菜单中选择 ATtiny85
现在您已经配置好了 IDE,接下来需要配置硬件程序员,即计算机与 ATtiny85 之间的接口,使用 Arduino Uno 或兼容的板子。打开 Arduino IDE,选择文件
示例
11.ArduinoISP
ArduinoISP,如图 3-5 所示。

图 3-5:选择 ArduinoISP 示例
在将这个示例程序上传到你的 Arduino Uno 或兼容板之后,该板将充当硬件编程器。你随时可以向 Uno 上传其他示例程序,但如果你想向 ATtiny 上传程序,你需要先在 Arduino 上上传 ArduinoISP 示例程序。
让我们检查一下你的硬件和软件环境是否正常工作,可以使用一个典型的示例程序来使 LED 闪烁。
项目 #11:构建硬件的“Hello, World”程序
这个项目不会让你的 ATtiny85 在屏幕上打印“Hello, world”,而是做一个硬件等效的动作:闪烁 LED 来演示工具链的工作。
你将需要以下部件:
-
一块 Arduino Uno 或兼容板和 USB 数据线
-
一只 ATtiny85 微控制器
-
一只 1 kΩ, 0.25 W, 1% 的电阻
-
一只 10 µF, 16 V 电解电容
-
一只 LED
-
一块无焊面包板
-
公对公跳线
按照图 3-6 所示,在无焊面包板上组装电路,并将标有 5V、GND、RESET 和 D10 到 D13 的点连接到 Arduino 的相应引脚。

图 3-6:项目 #11 的原理图
通过 D10 到 D13 的连接到 Arduino 的 SPI 总线,以及 10 µF 电容与 Arduino 重置引脚之间的连接,仅在上传每个 ATtiny 项目时需要。上传示例程序后,移除这些连接。
接下来,你需要更改 IDE 设置以适应硬件的变化。打开 IDE 并将板卡类型更改为ATtiny25/45/85,如图 3-4 所示。选择 工具
时钟
内部 1 MHz。最后,通过选择 工具
Arduino 作为 ISP 来更改编程器。
现在输入并上传项目 #11 的示例程序。LED 应该开始以大约一秒钟的延迟闪烁,确认你的 ATtiny 开发环境已经正常工作。
// Project #11: Build the "Hello, world" of hardware
void setup()
{
❶ pinMode(3, OUTPUT);
}
void loop()
{
❷ digitalWrite(3, HIGH);
delay(1000);
❸ digitalWrite(3, LOW);
delay(1000);
}
ATtiny85 的物理引脚 2 在 Arduino 环境中对应的是数字引脚 3,因此示例程序声明引脚 3 为输出❶,然后将其打开❷,再在延迟后关闭❸。
现在我将介绍你在构建下一个项目之前需要了解的三件事:Arduino Uno 和 ATtiny 引脚参考的差异、如何向 ATtiny 电路添加复位按钮,以及 Arduino 函数如何在 ATtiny 版本的 Arduino 项目中使用。
Arduino Uno 与 ATtiny85 引脚参考
在 Arduino 环境中,ATtiny85 的物理引脚参考编号与您在典型的 Arduino Uno 和兼容板上使用的不同。因此,当您使用 Arduino 环境制作基于 ATtiny85 的项目时,您需要参考表 3-2 来确定 ATtiny85 的物理引脚及其在 Arduino Uno 环境中的对应引脚。
表 3-2: ATtiny85 与 Arduino 引脚参考
| ATtiny 物理引脚 | Arduino (1) | Arduino (2) |
|---|---|---|
| 2 | 数字引脚 3 (D3) | 模拟引脚 3 (A3) |
| 3 | 数字引脚 4 (D4) | 模拟引脚 2 (A2) |
| 5 | 数字引脚 0 (D0) | 脉宽调制(使用 analogWrite(0)) |
| 6 | 数字引脚 1 (D1) | 脉宽调制(使用 analogWrite(1)) |
| 7 | 数字引脚 2 (D2) | 模拟引脚 1 (A1) |
在上传程序时,D0 至 D2 上的 HIGH 和 LOW 信号会触发任何连接的外部设备,如继电器。在这种情况下,最好先将程序上传到微控制器,然后再将其插入最终项目中。
由于 ATtiny85 与 ATmega328 类型的微控制器具有相同的基本架构,因此您可以使用第二章中描述的端口操作来控制 I/O 引脚。ATtiny 的 I/O 引脚位于 PORTB 的最低 4 位(位 3 至 0),并与物理引脚 2、7、6 和 5 匹配。只需将更高的 4 位设置为 0,并使用 DDRB 和 PORTB 函数即可。
为 ATtiny85 电路添加复位按钮
与其他 Arduino 和兼容板一样,你可以在 ATtiny85 电路中添加复位按钮。你将在下一个项目中使用这个按钮,它也可能在未来的项目中方便地帮助你进行重启。
ATtiny85 的复位按钮将物理引脚 1 与 GND 连接,并与一个 10 kΩ的上拉电阻一起工作。当引脚 1 连接到 GND 时,电路会复位。图 3-7 显示了电路图。

图 3-7:ATtiny85 复位按钮电路图
在设计不会在运行过程中连接到主机 Arduino 板的电路时,可以在 5V 和 GND 之间放置一个 0.1µF 电容器,以保持更平稳的电源供应。
ATtiny85 可用的 Arduino 函数
如果你熟悉 Arduino 环境,并且想直接进入自己的基于 ATtiny 的项目,注意 ATtiny85 提供的 Arduino 函数比大型微控制器要少,但仍然有很多可用的函数:
analogRead() 告诉 ADC 返回一个 0 到 1,023 之间的值,表示 0 到 5V 直流电压范围
analogWrite() 生成 PWM 输出
delay() 暂停程序运行(以毫秒为单位)
delayMicroseconds() 暂停程序运行(以微秒为单位)
digitalWrite() 开启或关闭数字输出引脚
digitalRead() 读取数字输入引脚的状态
micros() 返回程序开始运行以来的微秒数
millis() 返回程序开始运行以来的毫秒数
pinMode() 设置数字引脚的状态
pulseIn() 返回在数字输入引脚上测量到的脉冲宽度
shiftOut() 将一个字节的数据输出到数字引脚
如果你对这些函数不熟悉,可以查阅我的书籍Arduino 工作坊第二版,或者访问 Arduino 语言参考:https://
你在使用 ATtiny85 时也可以使用各种 Arduino 库。然而,如果这些库是为访问 GPIO 引脚设计的,你需要修改库代码,以便更新 ATtiny85 的引脚引用。一些 Arduino 库可能还需要比 ATtiny85 提供的更多内存。
项目#12:创建快速读取温度计
在这个项目中,您将使用流行的 TMP36 温度传感器创建一个记录并显示三个预定温度范围的温度计:过冷、适宜和过热。这还展示了如何在 Arduino 环境中使用 ATtiny85 的模拟输入和数字输出引脚。
您将需要以下零件:
-
一块 Arduino Uno 或兼容板和 USB 电缆
-
一个 ATtiny85 微控制器
-
三个 1 kΩ、0.25 W、1%电阻
-
一个 10 kΩ、0.25 W、1%电阻
-
一个触觉按钮
-
一个 0.1 µF 陶瓷电容器
-
三个 LED
-
一个 TMP36 模拟温度传感器
-
一个无焊面包板
-
公对公跳线
根据无焊面包板上显示的图 3-8 组装电路。此原理图仅显示最终产品,但您首先需要连接线路以上传草图——您可以参考项目#11 来复习如何操作。使用您的 Arduino Uno 作为快速 5V 电源。

图 3-8:项目#12 的原理图
上传草图后,从 Arduino Uno 的 D10 至 D13 引脚和 RESET 引脚中移除线路,但保留 5V 和 GND 以供电温度计。当草图开始运行时,三个 LED 中的一个应该表示草图中设置的温度范围。
让我们看看这是如何工作的:
// Project #12 - A "quick-read" thermometer
// Define the pins that the LEDs are connected to:
❶ #define HOT 2
#define NORMAL 1
#define COLD 0
float voltage = 0;
float celsius = 0;
❷ float hotTemp = 25;
float coldTemp = 15;
float sensor = 0;
void setup()
{
❸ pinMode(HOT, OUTPUT);
pinMode(NORMAL, OUTPUT);
pinMode(COLD, OUTPUT);
pinMode(HOT, LOW);
pinMode(NORMAL, LOW);
pinMode(COLD, LOW);
}
void loop()
{
// Read sensor and convert result to degrees Celsius
❹ sensor = analogRead(2);
❺ voltage = (sensor * 5000) / 1024;
voltage = voltage - 500;
celsius = voltage / 10;
// Act on temperature range
❻ if (celsius < coldTemp)
{
digitalWrite(COLD, HIGH);
delay(250);
digitalWrite(COLD, LOW);
}
❼ else if (celsius > coldTemp && celsius < hotTemp)
{
digitalWrite(NORMAL, HIGH);
delay(250);
digitalWrite(NORMAL, LOW);
}
else
{
❽ // Celsius is > hotTemp
digitalWrite(HOT, HIGH);
delay(250);
digitalWrite(HOT, LOW);
}
}
草图定义了读出 LED 引脚的值,并将它们设置为 LOW ❶。您可以为热和冷设置自己的值 ❷;“正常”温度将高于coldTemp值,并且低于或等于hotTemp值。
草图将数字引脚设置为输出 ❸,然后循环,从温度传感器中获取值 ❹ 并将其转换为摄氏度 ❺。最后,它确定温度为冷 ❻、正常 ❼ 或热 ❽ 并点亮相应的 LED。
增加 ATtiny85 的速度
ATtiny85 可以在 Arduino 环境和其他环境中以三种不同的速度运行:1 MHz(默认速度)、8 MHz 或 16 MHz。到目前为止,您的项目都使用了默认速度,这需要最少的功率,并且适用于电池供电项目。对于涉及更多计算的项目,您需要将速度更改为 8 MHz 或更高,这将消耗更多电力。
为了准备下一个项目,您将使用 IDE 烧写一个新的bootloader——这是加载到 ATtiny85 微控制器中的软件,使其能够通过 SPI 或 USB 连接接收代码。这将擦除最后上传的草图并设置微控制器内部的振荡器速度,从而确定操作速度。
振荡器速度
按照上传草图到微控制器时的电路设置进行连接。接下来,打开 IDE 并选择 工具
时钟
内部 16 MHz,如图 3-9 所示。

图 3-9:准备更改时钟速度
接下来,选择 工具
烧录引导加载程序。该操作应在几秒钟内完成,如图 3-10 所示。

图 3-10:烧录引导加载程序后 Arduino IDE 界面
现在,上传的草图应以 16 MHz 的速度运行。
振荡器精度
环境温度会影响微控制器的速度精度。当你仅仅使用延时来闪烁 LED 或进行其他简单任务时,这通常不是问题,但如果你使用 millis() 和 micros() 函数进行更精确的计时时,温度变化可能会成为问题。例如,在外部温度约为 25°C 时,速度可能会变化±10%。
解决方案是使用像 Arduino Uno 等板卡所使用的外部晶体电路。你需要两个 22 pF 的陶瓷电容和一个 8、16 或 20 MHz 的通孔型 HC-49 晶体,像图 3-11 所示的那种。

图 3-11:一个通孔型 HC-49 晶体
注意
如果你做过 Arduino 工作坊第二版中描述的“面包板 Arduino”,你应该对这种电路类型很熟悉。
图 3-12 显示了添加到 ATtiny85 的晶体振荡器电路。

图 3-12:带有外部晶体子电路的 ATtiny85 电路
使用外部晶体时,不要忘记将振荡器速度设置为外部,并与晶体频率匹配。使用外部晶体的一个缺点是,它会占用数字引脚 2 和 3,这意味着你不能再将它们用作输入或输出。
下一个项目将帮助你在日后的工作中定期使用 ATtiny85 进行紧凑的 Arduino 兼容项目。
项目 #13:创建 ATtiny85 编程扩展板
本项目创建了一个小型屏蔽板,您可以通过 Arduino Uno 向 ATtiny 微控制器上传草图。该屏蔽板包含所有必需的电路,并配有两个 LED,用于快速原型设计或实验。然后,您可以将微控制器从屏蔽板上取下,单独使用它进行其他项目。
您将需要以下部件:
-
项目 #13 PCB
-
两个 1 kΩ, 0.25 W, 1% 的电阻
-
一个 10 µF, 16 V 的电解电容
-
两个 5 mm 的 LED
-
1 × 40 内联 2.54 mm 引脚头
-
一个八针 IC 插座
图 3-13 显示了本项目的原理图。

图 3-13:项目 #13 的原理图
要组装电路,请按照 图 3-14 上 PCB 上标记的方式连接各个元件。首先是电阻器,然后是 IC 插座。接着安装电容,并注意 PCB 上标明的极性。然后安装 LED,确保其短脚与 PCB 上的方形孔对接。

图 3-14:项目 #13 的 PCB
最后,将内联引脚修剪成一个六针长度和一个五针长度,然后按 图 3-15 所示插入 Arduino Uno。

图 3-15:准备内联引脚头
将 PCB 放置在引脚头上方,并按照 图 3-16 所示焊接引脚。

图 3-16:已组装的屏蔽板
现在,您可以通过在上传 Arduino 作为 ISP 程序时移除屏蔽板,然后在上传 ATtiny 程序时再插入屏蔽板,轻松上传草图。这样可以让您在未来更快速、更轻松地搭建基于 ATtiny85 的项目,避免手动将 Uno 连接到无焊接面包板。
继续进行
本章向您展示了如何使用 ATtiny 微控制器来构建更小、更简单且成本更低的 Arduino 兼容项目。Arduino 编程屏蔽板还为您提供了一个更快的方式,将代码上传到 ATtiny。
在下一章中,您将学习如何使用看门狗定时器保持 Arduino 持续运行。
第四章:4 构建看门狗定时器

长时间无人看管的项目可能会因为事故、设计不佳或电力问题而停止工作或死机。为了解决这些问题,你可以使用看门狗定时器,这是一种小型电路,当项目正常运行时它保持静止,但如果未收到正常信号,它会强制重置微控制器,重新启动操作。这些定时器可以帮助你构建更加先进、专业且可靠的项目。
在本章中,你将学习:
-
配置 555 定时器 IC 为不稳定定时器
-
使用公式计算 555 定时器 IC 的延迟周期
-
使用 PCB 构建自己的看门狗定时器
确保恒定且可靠的运行
随着你掌握更多的微控制器知识,你可能会发现自己在构建一些长时间无需你持续关注的项目或产品。这些项目可以是天气监测系统、数据记录设备、硬件网页主机(允许远程查看当前数据),或者其他一些位置难以接触到的设备(如埋在地下室或阁楼中的设备)。
当最终的项目交到你手中时,如何确保其恒定且可靠的运行?即使你的硬件和草图或代码看似万无一失,意外情况仍可能导致你的 Arduino 停止运行。短暂的电源电压下降可能会使微控制器死机(即停止草图运行)。极端的温度变化也可能引发问题,或者你的草图可能有未预料的错误。由于这些原因及更多问题,你将需要一个看门狗定时器。
在本节中,我将解释看门狗定时器背后的理论,并展示如何构建自己的看门狗定时器电路。
看门狗定时器理论
看门狗定时器电路是一个外部定时器,连接到 Arduino 板上的 RESET 引脚。看门狗电路开始时发送一个高电平信号,该信号会在预定的时间后变为低电平,这将导致 Arduino 重置并重新启动。然而,Arduino 可以通过向看门狗电路连接的数字输出引脚发送定期信号或心跳来防止这种重置。心跳信号会在看门狗定时器重置 Arduino 之前重置定时器电路。
请参考图 4-1 中的时序图。

图 4-1:看门狗定时器输出与 Arduino 心跳输出的时序图
上方的信号是来自看门狗定时器的输出,持续在高电平(HIGH)和低电平(LOW)之间振荡。高电平信号的周期远长于低电平信号的周期。看门狗的输出连接到 Arduino 的重置(RESET)引脚。当看门狗定时器重置时,输出从高电平开始,这意味着 Arduino 必须不断重置看门狗定时器,以防止看门狗定时器重置 Arduino。
为了实现这一功能,连接到看门狗定时器的 Arduino 数字输出引脚会向定时器发送一个快速脉冲(高电平然后低电平)❶,重置定时器并阻止定时器输出变为低电平(LOW)。如果 Arduino 出现问题并停止向看门狗定时器发送心跳信号❷,看门狗定时器将继续振荡,并且其信号将变为低电平❸,从而重置 Arduino。重置后,Arduino 应该恢复并再次开始发送心跳信号❹。
看门狗定时器是一种硬件解决方案,较软件实现更不易出错。如果重置无法让 Arduino 重新启动,说明项目中存在重大故障,需要你物理检查硬件或代码。
看门狗定时器电路配置
看门狗定时器电路的核心是 图 4-2 所示的 555 定时器 IC。这个紧凑的部件包含了一个可定制的定时电路,可以通过多种方式使用。

图 4-2:555 定时器 IC
要将 555 用作看门狗定时器,你需要将其配置为非稳态定时器,即生成一个持续的信号输出,高电平周期比低电平周期更长,如 图 4-3 所示。

图 4-3:一个基本的非稳态定时器电路
非稳态定时器电路相当简单。在 555 内部有一个电压比较器电路和一个触发器,它在低电平和高电平之间切换,以驱动输出引脚。当电源接入时,管脚 2(触发引脚)的电压低于 V[CC] 的三分之一,这意味着触发器切换并将 555 的输出设置为高电平。然后,电容 C 通过电阻 R[A] 和 R[B] 充电。
在输出为高电平(HIGH)一段时间后,管脚 6(阈值引脚)的电压超过 V[CC] 的三分之二。触发器切换并将 555 的输出设置为低电平(LOW)。这也启用了放电功能,使得电容 C 通过电阻 R[B] 放电。
在输出为低电平(LOW)一段时间后,管脚 2 的电压低于 V[CC] 的三分之一,因此触发器会切换,将 555 的输出设置为高电平(HIGH)……然后循环重复。输出来自管脚 3,它作为一个开路集电极工作——也就是说,当为低电平(LOW)时,它可以将电流吸入地(GND)。
输出信号的占空比及高低电平周期由电阻 R[1]、R[2]和电容 C 的值决定。R[L]部分是负载,或者说是由输出控制的电路。当输出关闭时,电流可以从 V[CC]通过较高的 R[L]流动;当输出开启时,电流则从输出通过较低的 R[L]流向 GND。555 的输出脚最多可以流入或流出 200 mA 的电流。
为了确定输出的高低电平周期时间——如果你有兴趣的话,输出信号的频率(虽然我们不使用它,但也可以设置!)——我们使用以下三个公式:

T[low]是 555 定时器输出为低电平的时间长度,T[high]是输出为高电平的时间长度。例如,若 R[1]的值为 100 kΩ,R[2]的值为 4.7 kΩ,电容 C 的值为 100 uF,则计算如下:
T[high] = 0.693 (4,700) 0.0001 = 7.255 s
T[low] = 0.693 (100,000 + 4,700) 0.0001 = 0.3257 s
一个包含这些公式的电子表格可以在https://
在构建 555 定时器电路时,请使用 1%公差的电阻。较大的电容可能会有很大的公差,尤其是在温度波动时—有时高达+/-20%的误差—因此尽量保持电容的值尽可能小。
现在你知道如何操作 555 定时器,让我们为 Arduino 构建一个看门狗定时器电路。
项目#14:构建看门狗定时器
在这个项目中,你将为 Arduino Uno 或其他微控制器构建一个便捷且可调的看门狗定时器,使用了所讨论的复位电路类型。你可以在无焊接面包板上搭建电路,作为一个临时设置,或者下载该项目的 PCB 文件,制作自己的 PCB,创建一个永久性的看门狗定时器。
你将需要以下部件:
-
项目#14 的 PCB 或无焊接面包板
-
一只 NE555 定时器 IC(不是 CMOS 7555 版本!)
-
一个八针 IC 插座
-
一只 1N4001 二极管
-
一只 5 mm 的 LED
-
1 × 40 个 2.54 mm 的直插针(如果使用 PCB)
-
各种跳线
-
一只 2N7000 N 型 MOSFET
-
两只 1 kΩ,0.25 W,1%精度的电阻
-
一只 10 kΩ,0.25 W,1%精度的电阻
-
一只 100 Ω,0.25 W,1%精度的电阻
-
其他电阻(请参见以下部分)
-
一只 0.01 µF 的陶瓷电容
-
一只 0.1 µF 的陶瓷电容
-
一只 100 µF,16 V 的电解电容
图 4-4 展示了该项目的原理图。

图 4-4:项目#14 的原理图
在组装电路之前,根据前一节提供的公式确定用于控制输出 HIGH 和 LOW 时段长度的 R[1] 和 R[2] 值。结果将根据你使用的是 PCB 还是无焊面包板而有所不同,因为面包板内部的接触点也有其自身的电阻值。
对于那些使用 PCB 构建的用户,原理图显示了三个电阻器用于 R[e]:R[1A]、R[1B] 和 R[1C]。我为最多使用三个电阻器作为 R[1] 的组合值留出了空间,这样你可以根据需要通过在 R[1B] 或 R[1C] 空间添加新的电阻器并切除不需要的值来更改电阻值。或者,你可能需要使用两个或三个电阻器并联以获得精确的所需值。
计算并联电阻值时,使用以下公式:
R[T] = 1 / (1 / R[1A] + 1 / R[1B] + 1 / R[1C] + …)
如果你只需要使用一个电阻器用于 R[1],将其插入 PCB 上的 R[1A] 空间。
如果你不确定该为此项目使用哪些电阻值,一个不错的默认选项是将 R[1] 设置为大约 100 kΩ,R[2] 设置为 4.7 kΩ。这样可以创建一个大约 57 秒的 HIGH 延迟,接近一分钟。通过这个延迟,你的 Arduino 必须比每 7 秒重置一次计时器更频繁地重置,以避免被计时器本身重置。
555 电路的内部工作原理
让我们更仔细地了解 555 电路的内部工作原理。当你为电路供电时,555 应该按照前面描述的那样开始工作,输出信号连接到 Arduino 的 RESET 引脚,并以 HIGH 开始。Arduino 应该正常工作。二极管 D[1],连接在 555 的输出引脚和 Arduino RESET 引脚之间,确保没有杂散信号能够重置 Arduino。
随着时间的推移,根据你的 R[1] 值,电容器 C[1] 应该开始通过 555 的触发引脚充电。当 Arduino 通过心跳引脚发送 HIGH 信号时,它应该打开 N-MOSFET Q[1]。这样做不仅会闪烁 LED D[3](作为 Arduino 心跳信号的视觉指示),还会通过 R[6] 将电容器 C[1] 短接到地(GND),从而放电。电容器 C[1] 然后重新开始充电,重置 555 重新设置 Arduino 的时间。
如果 Arduino 持续发送心跳脉冲,555 定时器将无法有机会将输出状态改变为低电平(LOW)并重置 Arduino,如前所述。然而,如果 Arduino 停止发送心跳信号,电容 C[1]将继续充电,直到 555 的阈值引脚电压达到 5V 的三分之二。此时,555 的输出将短暂改变为低电平(由 R[2]决定)。这会重置 Arduino 并使 LED D[2]闪烁(这表示复位电路已被激活)。当 555 的输出引脚为低电平时,电流可以从 5V 流过电阻 R[3]和 LED,进入 555 的输出引脚。当输出引脚为低电平时,电流可以流入该引脚,而当其为高电平时,电流则流出。输出随后会恢复为高电平,过程重新开始。
电阻 R[3]和 R[4]限制 LED 的电流,而电阻 R[5]在 N-MOSFET 未激活时将其拉低,避免了不必要的激活。电阻 R[6]在电容 C[1]放电时保护电容,避免接收到心跳信号时发生短路。电容 C[2]在电路中平滑电源。最后,555 定时器需要电容 C[3]以确保正常运行。
电路组装
按照通常的方式使用图 4-5 所示的 PCB 组装项目:从最矮的元件开始,如电阻,然后逐步安装更高的元件(如电容和直插头针)。

图 4-5:项目#14 的 PCB
别忘了给 555 使用一个 IC 插座,并将 IC 端的缺口与 PCB 上的缺口对准。你可能需要修剪直插头针,使其变成一条四针排。
一旦组装完成,你的看门狗定时器应该类似于图 4-6 中所示的样子。

图 4-6:完成的看门狗定时器板
将 5V、RESET 和 GND 引脚连接到 Arduino 的相应引脚,并将心跳引脚连接到 Arduino 上未使用的数字引脚。为了使你的草图发送一个比看门狗定时器操作更快的脉冲频率,请参考以下使用看门狗定时器的示例。在将看门狗定时器连接到 Arduino 后,上传草图。
// Project #14 - Watchdog timer
❶ #define heartBeatPin 5
❷ void heartBeat()
{
digitalWrite(heartBeatPin, HIGH);
delay(100);
digitalWrite(heartBeatPin, LOW);
}
❸ void setup()
{
pinMode(heartBeatPin, OUTPUT);
heartBeat(); // Send heartbeat signal to watchdog timer
}
void loop()
{
// Do things
❹ heartBeat(); // Send heartbeat signal to watchdog timer
}
这段代码将数字引脚 5 定义为与看门狗定时器电路的心跳连接 ❶。heartBeat() 函数生成心跳脉冲 ❷,它只是瞬间将引脚切换开和关——足够长时间以复位看门狗定时器电路。void setup() 函数 ❸ 在 Arduino 复位或上电后尽快发送心跳信号以复位看门狗。最后,主循环 ❹ 定期发送心跳信号。
将 heartbeat() 函数放入 void loop() 是一个理想的位置,前提是循环中单次代码执行的时间小于看门狗的超时周期。如果你想增加看门狗触发的复位次数,你也可以在其他函数中插入更多对 heartBeat() 的调用。
继续前进
本章讲解了为什么你需要使用看门狗定时器,以及如何操作和构建自己的电路的理论。你学会了如何使用看门狗定时器方法,以保持未来基于 Arduino 的项目的可靠性,同时也学会了如何使用其他微控制器,如 ATtiny,或者与其他具有输出和低电平输入复位功能的电子设备一起使用。
在下一章,你将学习如何用最少的数字输出引脚控制多个 LED 灯。
第五章:5 使用 CHARLIEPLEXING 控制 LED

使用一块 Arduino 板控制多个 LED 非常简单。然而,你也可以在 Arduino 环境中使用Charlieplexing方法一次控制多个 LED,而无需外部显示驱动 IC。这是减少项目中数字或字符显示所需组件数量(和费用)的一种好方法。
在本章中,你将学习:
-
使用 Arduino Uno 或兼容板,或 ATtiny85 微控制器进行 Charlieplexing
-
使用 6 个和 12 个 LED 进行 Charlieplexing
-
使用逻辑表格简化 Charlieplexing 电路的规划
你还将构建一个 30 个 LED 的矩阵显示器,用于显示字母数字数据,可以用于你自己的项目
Charlieplexing 简介
Charlieplexing一词来源于 Charlie Allen,他在 Maxim Integrated(现为 Analog Devices 一部分)工作时最早提出了这一概念,该公司生产了如流行的 MAX7219 LED 显示驱动器等集成电路。这个词是由“Charlie”和“multiplexing”(多路复用)两个词组合而成,后者是一种通过微控制器的少量 I/O 引脚控制多个 LED 的方式。
Charlieplexing 是一种多路复用方式,依赖于微控制器 I/O 引脚的三态能力来控制激活多个 LED 所需的电流。你可以将每个引脚设置为三种状态之一:
高 电流从 I/O 引脚流出,例如使用digitalWrite()函数时。
低 当 I/O 引脚也可以接收电流时,电流不会从该引脚流出。
输入状态(或高阻抗) 引脚被设置为数字输入。在此状态下,几乎没有电流流动。这也被称为高阻抗或高-Z状态。
你可以设置这些 I/O 引脚为不同的状态,以便在电路中导电并创建一个可控的 LED 矩阵。每次只能点亮一个 LED,并且在同一电路中使用的所有 LED 必须具有相同的规格——即,它们的正向电压和工作电流必须相同。
为了帮助理解 Charlieplexing,让我们通过一些小例子来演示。首先,考虑图 5-1 中的电路图。

图 5-1:连接到 Arduino 输出的两个 LED 的电路图
如果 D13 引脚为 HIGH 且 D12 引脚为 LOW,电流将从 D13 引脚流经 R[1],通过 D[2],然后流经 R[2],最后进入 D12,引起 LED D[2] 点亮。如果你反转输出,使 D12 引脚为 HIGH 而 D13 引脚为 LOW,电流将从 D12 引脚流经 D[1],通过 R[1],并进入 D13,引起 LED D[1] 点亮。电流始终会流经两个电阻器,因此它们的总值应适合通过数字输出驱动 LED。使用 5V 输出时,270 Ω 到约 510 Ω 的电阻值将提供足够的亮度。
为了测试 Charlieplexing,构建图 5-1 中的电路;然后运行清单 5-1 中的草图。
void setup()
{
pinMode(13, OUTPUT);
pinMode(12, OUTPUT);
}
void loop()
{
digitalWrite(13, HIGH);
digitalWrite(12, LOW);
delay(500);
digitalWrite(13, LOW);
digitalWrite(12, HIGH);
delay(500);
}
清单 5-1:两个 LED 的演示
这个草图展示了如何通过电流控制两个 LED 中的一个,而不需要 GND 引脚。它仅作为介绍 Charlieplexing 概念的基础示例;将两个 LED 进行 Charlieplexing 并没有实际好处,因为它并没有减少所使用的 I/O 引脚数量。然而,一旦你在电路中增加另一个 I/O 引脚,Charlieplexing 的潜力将变得非常明显,如下一个项目所示。
项目 #15:使用六个 LED 的 Charlieplexing
在这个项目中,你将使用仅三个数字输出引脚控制六个 LED,展示 Charlieplexing 的优势。
你将需要以下部件:
-
一块 Arduino Uno 或兼容板和 USB 电缆
-
三个 270 Ω,0.25 W,1% 的电阻器
-
六个 LED
-
两块无焊面包板
-
公对公跳线
按照图 5-2 所示组装电路。

图 5-2:项目 #15 的电路图
为了便于组装,使用两块并排的无焊面包板,如图 5-3 所示。

图 5-3:已组装的项目 #15
现在输入并上传以下草图到你的 Arduino。上传草图后不久,每个 LED 从 1 到 6 将依次点亮。
// Project #15 - Charlieplexing six LEDs
❶ #define d 1000
❷ void LED1()
{
❸ DDRB = B00110000;
❹ PORTB = B00010000;
}
void LED2()
{
DDRB = B00110000;
PORTB = B00100000;
}
void LED3()
{
DDRB = B00011000;
PORTB = B00001000;
}
void LED4()
{
DDRB = B00011000;
PORTB = B00010000;
}
void LED5()
{
❺ DDRB = B00101000;
PORTB = B00001000;
}
void LED6()
{
DDRB = B00101000;
PORTB = B00100000;
}
void setup() {}
void loop()
❻ {
LED1(); delay(d); LED2(); delay(d); LED3(); delay(d);
LED4(); delay(d); LED5(); delay(d); LED6(); delay(d);
}
如果你习惯于为每个数字输出引脚使用一个 LED,操作的前几秒钟可能看起来像是魔术,但你可以通过小心地引导电流通过合适的引脚和电阻来实现这些效果。
以原理图中的第一个 LED,D[1]为例,它由函数LED1() ❷控制。按照原理图的路径追踪:电流需要从 D12 引脚流经 R[2]、LED 本身,再流过 R[1],最后进入 D13 引脚。你不希望电流流经 D[4],因此将 D11 引脚设置为输入,阻止电流流动。虽然 D[4]和 D[5]在电流路径中,但它们不会被激活,因为电阻和它们的正向工作电压极大地降低了电流的电压。
这段草图使用端口操作(详见第二章)来控制数字引脚,而不是使用大量的pinMode()和digitalWrite()函数。这大大减少了所需的代码量,同时也为项目的其余部分释放了更多内存。
要在代码中开启 D[1],该草图通过端口操作开启 LED D[1],并将 D13 引脚设置为 HIGH,D12 引脚设置为 LOW。草图首先激活 PORTB 引脚类型 D13 到 D8 ❸,将 D13 设置为输出,将 D12 设置为输出,将 D11 设置为输入,未使用的引脚保持为 0。接着,草图激活输出引脚 ❹,将 D13 设置为 LOW,将 D12 设置为 HIGH,输入保持为 0。这就点亮了 LED D[1]。
在项目中的另一个例子中,考虑 LED D[5]。要点亮它,电流必须从 D11 引脚流经 R[3]、D[4]和 R[1],然后进入 D13 引脚。草图将 D11 引脚设置为 HIGH,将 D13 引脚设置为 LOW,将 D12 引脚设置为输入 ❺,以阻止电流向该方向流动。
草图为每个 LED 依次设置电流流动方式,使用从❶开始的每个 LED 的函数。与使用传统的digitalWrite()函数不同,后者需要先使用pinMode()函数将引脚配置为输出或输入,在void setup()中无需任何操作,因此代码可以依次点亮每个 LED,从❻开始。
你可以通过更改定义的值d ❶,来调整每个 LED 开启之间的时间间隔。尽管草图没有使用这个选项,你也可以在代码的任何位置一次性关闭所有 LED,方法是使用以下代码行:
DDRB = B00000000; // Set all pins to inputs (high-Z)
将该项目的硬件保留,以便用于下一个项目。在下一部分,我将向你展示一种更系统化的方式,来确定哪些引脚需要设置为 HIGH 或 LOW。
更大的 Charlieplexing 显示器
在构建更大的 Charlieplexing 电路时,创建一个包含 LED 及其所需引脚类型和输出的逻辑表,可以帮助你规划如何编写代码。这使得创建控制 LED 所需的 DDRB 和 PORTB 函数变得更加容易。为此,追踪每个 LED 所需的电流路径,然后确定每个 LED 的引脚状态。例如,表 5-1 是项目 #15 的逻辑表,其中 L 代表 LOW,H 代表 HIGH,Z 代表输入。
表 5-1: 项目 #15 的逻辑表
| LED# | 引脚 D13 | 引脚 D12 | 引脚 D11 |
|---|---|---|---|
| 1 | L | H | Z |
| 2 | H | L | Z |
| 3 | Z | L | H |
| 4 | Z | H | L |
| 5 | L | Z | H |
| 6 | H | Z | L |
创建这样的表格可以简化确定端口操作命令的过程,因为你可以将每个字母与其相应的 1、0 和输入对应起来。要制作你自己的表格,首先追踪每个 LED 所需的电流流向,然后标注每个 I/O 引脚的状态。例如,要在项目 #15 中激活 LED 4,电流必须从引脚 D12(因此它是 HIGH)流出,经过 R[2],通过 LED D[4],然后经过 R[3],返回 D11(因此它是 LOW)。你不希望电流流入或流出 D13,因此它会是 Z。你可以使用这些 HIGH、LOW 和 Z 信息来创建代码中所需的 DDR 和 PORTB 函数。
一旦你使用的 I/O 引脚超过两个,你就可以将一对 LED 连接到每一条 I/O 线上。要确定你可以用 Arduino 上的任意数量的 I/O 引脚控制多少个 LED,可以使用公式 L = n² − n,其中 L 是 LED 的数量,n 是 I/O 引脚的数量。例如,如果你有 4 个引脚,你可以使用 12 个 LED(4² − 4 = 12)。
让我们在下一个项目中尝试这个。
项目#16:使用 12 个 LED 的 Charlieplexing
在这个项目中,你将练习使用 Charlieplexing 技术,仅通过四个数字输出引脚控制一个更大的 LED 显示器。你将需要以下零件:
-
一块 Arduino Uno 或兼容的开发板和 USB 电缆
-
四个 270 Ω,0.25 W,1% 的电阻器
-
12 个 LED
-
两块无焊接面包板
-
公对公跳线
按照图 5-4 中的示意图组装电路。

图 5-4:项目#16 的电路图
你可以通过扩展项目#15 的硬件设置来构建这个项目,使用两块并排放置的无焊接面包板,如图 5-5 所示。

图 5-5:已组装的项目#16
表 5-2 是项目#16 的逻辑表。你可以将此与后续草图中的端口操作进行比较,以加深你对控制 LED 的理解。为了练习,可以沿着电路图追踪不同 LED 的电流,并与表格进行对比。
表 5-2: 项目#16 的逻辑表
| LED# | 引脚 D13 | 引脚 D12 | 引脚 D11 | 引脚 D10 |
|---|---|---|---|---|
| 1 | L | H | Z | Z |
| 2 | H | L | Z | Z |
| 3 | Z | L | H | Z |
| 4 | Z | H | L | Z |
| 5 | L | Z | H | Z |
| 6 | H | Z | L | Z |
| 7 | Z | Z | L | H |
| 8 | Z | Z | H | L |
| 9 | Z | L | Z | H |
| 10 | Z | H | Z | L |
| 11 | L | Z | Z | H |
| 12 | H | Z | Z | L |
将项目#16 的草图上传到你的 Arduino 板。上传草图后一两秒钟,每个 LED 应该依次亮起。
让我们看看这个是如何工作的:
// Project #16 - Charlieplexing with 12 LEDs
#define d 250 ❶
void LED1()
{
DDRB = B00110000; ❷
PORTB = B00010000;
}
void LED2() {DDRB = B00110000; PORTB = B00100000;}
void LED3() {DDRB = B00011000; PORTB = B00001000;}
void LED4() {DDRB = B00011000; PORTB = B00010000;}
void LED5() {DDRB = B00101000; PORTB = B00001000;}
void LED6() {DDRB = B00101000; PORTB = B00100000;}
void LED7() {DDRB = B00001100; PORTB = B00000100;}
void LED8() {DDRB = B00001100; PORTB = B00001000;}
void LED9() {DDRB = B00010100; PORTB = B00000100;}
void LED10(){DDRB = B00010100; PORTB = B00010000;}
void LED11(){DDRB = B00100100; PORTB = B00000100;}
void LED12(){DDRB = B00100100; PORTB = B00100000;}
void noLED() ❸
{
DDRB = B00000000; // All to inputs, high-impedance
}
void setup() {}
void loop()
{
LED1(); delay(d); LED2(); delay(d); LED3(); delay(d); LED4(); delay(d);
LED5(); delay(d); LED6(); delay(d); LED7(); delay(d); LED8(); delay(d);
LED9(); delay(d); LED10(); delay(d); LED11(); delay(d); LED12(); delay(d); ❹
}
与项目#15 一样,每个 LED 由从❷开始的单独函数激活。在void LED1()之后,为了节省空间,我将每个函数压缩成一行代码。再次强调,端口操作大大提高了草图的效率。
你可以改变每个 LED 开关之间的延迟时间❶。该草图包含了一个函数,可以在需要时关闭所有 LED,以便用于其他未来的项目❸。为了节省空间,我把多个函数放在了一行上❹,每个 LED 在预设的延迟时间后被点亮。
该项目中 LED 的布局看起来可能是随机的,但你现在已经掌握了使用 Charlieplexing 创建更有用、更复杂显示的技能,这将在项目 #18 中进行。不过,首先,我将向你展示如何使用 ATtiny85 进行 Charlieplexing。
项目 #17:使用 ATtiny85 进行 Charlieplexing
使用 ATtiny85 微控制器进行 Charlieplexing 可以为你提供一个更便宜、更小巧的电路方案,用于控制大量的 LED 而无需外部设备。这个项目展示了如何使用 ATtiny85 控制 12 个 LED。
你将需要项目 #16 中的 LED 电路,以及项目 #13 在第三章中的 ATtiny85 编程扩展板,或者以下部件:
-
一块 Arduino Uno 或兼容板以及 USB 电缆
-
一个 ATtiny85 微控制器
-
一个 10 微法的电解电容
-
两块无焊面包板
-
公对公跳线
首先,按照第三章中的说明(如果你完成了项目 #13)组装 ATtiny85 编程设置,使用编程扩展板或为该项目列出的部件,然后上传代码。接着,将 ATtiny85 连接到 LED 电路,如图 5-6 所示。LED 电路与项目 #16 中的引脚连接不同,但其他方面是相同的。

图 5-6:项目 #17 的电路图
接下来,上传代码:
// Project 17 - Charlieplexing with ATtiny85
#define d 250
void LED1() {DDRB = B00001100; PORTB = B00000100;}
void LED2() {DDRB = B00001100; PORTB = B00001000;}
void LED3() {DDRB = B00000110; PORTB = B00000010;}
void LED4() {DDRB = B00000110; PORTB = B00000100;}
void LED5() {DDRB = B00001010; PORTB = B00000010;}
void LED6() {DDRB = B00001010; PORTB = B00001000;}
void LED7() {DDRB = B00000011; PORTB = B00000001;}
void LED8() {DDRB = B00000011; PORTB = B00000010;}
void LED9() {DDRB = B00000101; PORTB = B00000001;}
void LED10(){DDRB = B00000101; PORTB = B00000100;}
void LED11(){DDRB = B00001001; PORTB = B00000001;}
void LED12(){DDRB = B00001001; PORTB = B00001000;}
void noLED(){DDRB = B00000000;} // All to inputs, high-impedance
void setup() {}
void loop()
{
LED1(); delay(d); LED2(); delay(d); LED3(); delay(d); LED4(); delay(d);
LED5(); delay(d); LED6(); delay(d); LED7(); delay(d); LED8(); delay(d);
LED9(); delay(d); LED10(); delay(d); LED11(); delay(d); LED12(); delay(d);
}
上传后,该代码应该与项目 #16 的操作方式相同,12 个 LED 将依次亮起。不过,代码对端口的操作方式有所不同。由于你使用的是 ATtiny85 的 PORTB 的 3、2、1 和 0 引脚(数字 D3 至 D0),因此你的 DDRB 和 PORTB 函数需要操作位 0 至 3。
项目 #18:构建一个 30 LED 矩阵显示器
本项目将 Charlieplexing 技术提升到下一个层次,使用一个更大的 30 LED 电路,你可以用它来显示数字或你自己设计的字符。
从理论上讲,虽然可能实现,但在两块无焊面包板上搭建这个项目的电路极其困难,因此我强烈建议你下载该项目的 PCB 文件并自行制作 PCB。你将需要以下部件:
-
一块 Arduino Uno 或兼容板以及 USB 电缆
-
六个 270 Ω,0.25 W,1% 的电阻
-
三十个 5 毫米 LED
-
两块无焊面包板或项目 #18 的 PCB
-
1 个 40 针 2.54 毫米排针(如果使用 PCB)
-
各种跳线
按照图 5-7 所示组装电路。

图 5-7:项目 #18 的电路图
如果你已经订购了 PCB,组装起来很简单。从板上的布局可以看出,所有 LED 的正极引脚指向顶部,负极(平面的一侧)指向底部,如 图 5-8 所示。

图 5-8:项目 #18 的 PCB
先安装电阻器,再安装 LED,最后安装内联插头引脚。将引脚放置在后面,确保它们从背面突出出来,然后通过跳线将 PCB 支撑连接到 Arduino,或者直接将 PCB 放入 Arduino 中,如 图 5-9 所示。PCB 上的引脚标有与 Arduino 引脚号匹配的标签。

图 5-9:完成的项目 #18 电路
表 5-3 是项目 #18 的逻辑表,你可以将其与 清单 5-2 中的端口操作进行比较,或者与稍后列出的项目 #18 的草图或 图 5-7 中的原理图进行比较。同样,为了练习,可以通过原理图追踪不同 LED 的电流,并与表格对比,以增加你对控制 LED 的熟悉度。

在继续之前,请确保你的 LED 正常工作,通过输入并上传 清单 5-2 中的草图,应该能让每个 LED 依次点亮和熄灭。
#define d 250 ❶
void LED1() {DDRB = B00110000; PORTB = B00010000;} ❷
void LED2() {DDRB = B00011000; PORTB = B00001000;}
void LED3() {DDRB = B00001100; PORTB = B00000100;}
void LED4() {DDRB = B00000110; PORTB = B00000010;}
void LED5() {DDRB = B00000011; PORTB = B00000001;}
void LED6() {DDRB = B00110000; PORTB = B00100000;}
void LED7() {DDRB = B00011000; PORTB = B00010000;}
void LED8() {DDRB = B00001100; PORTB = B00001000;}
void LED9() {DDRB = B00000110; PORTB = B00000100;}
void LED10(){DDRB = B00000011; PORTB = B00000010;}
void LED11(){DDRB = B00101000; PORTB = B00001000;}
void LED12(){DDRB = B00101000; PORTB = B00100000;}
void LED13(){DDRB = B00100100; PORTB = B00000100;}
void LED14(){DDRB = B00100100; PORTB = B00100000;}
void LED15(){DDRB = B00100010; PORTB = B00000010;}
void LED16(){DDRB = B00100010; PORTB = B00100000;}
void LED17(){DDRB = B00100001; PORTB = B00000001;}
void LED18(){DDRB = B00100001; PORTB = B00100000;}
void LED19(){DDRB = B00010100; PORTB = B00000100;}
void LED20(){DDRB = B00010100; PORTB = B00010000;}
void LED21(){DDRB = B00010010; PORTB = B00000010;}
void LED22(){DDRB = B00010010; PORTB = B00010000;}
void LED23(){DDRB = B00010001; PORTB = B00000001;}
void LED24(){DDRB = B00010001; PORTB = B00010000;}
void LED25(){DDRB = B00000101; PORTB = B00000001;}
void LED26(){DDRB = B00000101; PORTB = B00000100;}
void LED27(){DDRB = B00001010; PORTB = B00000010;}
void LED28(){DDRB = B00001010; PORTB = B00001000;}
void LED29(){DDRB = B00001001; PORTB = B00000001;}
void LED30(){DDRB = B00001001; PORTB = B00001000;}
void noLED(){DDRB = B00000000;} // All LEDs to input, high impedance ❸
void setup() {}
void loop()
{
LED1(); delay(d); LED2(); delay(d); LED3(); delay(d); LED4(); delay(d);
LED5(); delay(d); LED6(); delay(d); LED7(); delay(d); LED8(); delay(d);
LED9(); delay(d); LED10(); delay(d); LED11(); delay(d); LED12(); delay(d);
LED13(); delay(d); LED14(); delay(d); LED15(); delay(d); LED16(); delay(d);
LED17(); delay(d); LED18(); delay(d); LED19(); delay(d); LED20(); delay(d);
LED21(); delay(d); LED22(); delay(d); LED23(); delay(d); LED24(); delay(d);
LED25(); delay(d); LED26(); delay(d); LED27(); delay(d); LED28(); delay(d);
LED29(); delay(d); LED30(); delay(d);
}
清单 5-2:测试 30 个 LED 矩阵显示
草图通过定义 d ❶ 来设置每个 LED 点亮和熄灭之间的延迟。从 ❷ 开始,函数设置 GPIO 引脚端口,以依次控制每个所需的引脚激活 LED。函数 ❸ 将所有 LED 关闭。之后,主循环会无限次地依次激活每个 LED。我已将代码的空格最小化,以提高显示效率。
现在你已经测试了 LED,接下来可以进入并上传项目的草图了。(如果你还没有,建议从书籍网页 https://
// Project #18 - Using the 30-LED matrix display
int pixels[10][30] = ❶
{ {0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0},
{0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0},
{1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0},
{1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0},
{1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0},
{0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0},
{1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0},
{1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0},
{1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0}};
void displayDigits(int i, int duration) ❷
{
for (int a = 0; a < duration; a++)
{
for (int b = 0; b < 30; b++)
{
if (pixels[i][b] == 1)
{
turnOnLED(b + 1); ❸
delay(1); // Adjust as required ❹
}
}
}
noLED();
}
void LED1() {DDRB = B00110000; PORTB = B00010000;} ❺
void LED2() {DDRB = B00011000; PORTB = B00001000;}
void LED3() {DDRB = B00001100; PORTB = B00000100;}
void LED4() {DDRB = B00000110; PORTB = B00000010;}
void LED5() {DDRB = B00000011; PORTB = B00000001;}
void LED6() {DDRB = B00110000; PORTB = B00100000;}
void LED7() {DDRB = B00011000; PORTB = B00010000;}
void LED8() {DDRB = B00001100; PORTB = B00001000;}
void LED9() {DDRB = B00000110; PORTB = B00000100;}
void LED10(){DDRB = B00000011; PORTB = B00000010;}
void LED11(){DDRB = B00101000; PORTB = B00001000;}
void LED12(){DDRB = B00101000; PORTB = B00100000;}
void LED13(){DDRB = B00100100; PORTB = B00000100;}
void LED14(){DDRB = B00100100; PORTB = B00100000;}
void LED15(){DDRB = B00100010; PORTB = B00000010;}
void LED16(){DDRB = B00100010; PORTB = B00100000;}
void LED17(){DDRB = B00100001; PORTB = B00000001;}
void LED18(){DDRB = B00100001; PORTB = B00100000;}
void LED19(){DDRB = B00010100; PORTB = B00000100;}
void LED20(){DDRB = B00010100; PORTB = B00010000;}
void LED21(){DDRB = B00010010; PORTB = B00000010;}
void LED22(){DDRB = B00010010; PORTB = B00010000;}
void LED23(){DDRB = B00010001; PORTB = B00000001;}
void LED24(){DDRB = B00010001; PORTB = B00010000;}
void LED25(){DDRB = B00000101; PORTB = B00000001;}
void LED26(){DDRB = B00000101; PORTB = B00000100;}
void LED27(){DDRB = B00001010; PORTB = B00000010;}
void LED28(){DDRB = B00001010; PORTB = B00001000;}
void LED29(){DDRB = B00001001; PORTB = B00000001;}
void LED30(){DDRB = B00001001; PORTB = B00001000;}
void noLED(){DDRB = B00000000;} // all LEDs to input, high impedance
void turnOnLED(int l)
{
switch (l)
{❻
case 1 : LED1(); break;
case 2 : LED2(); break;
case 3 : LED3(); break;
case 4 : LED4(); break;
case 5 : LED5(); break;
case 6 : LED6(); break;
case 7 : LED7(); break;
case 8 : LED8(); break;
case 9 : LED9(); break;
case 10 : LED10(); break;
case 11 : LED11(); break;
case 12 : LED12(); break;
case 13 : LED13(); break;
case 14 : LED14(); break;
case 15 : LED15(); break;
case 16 : LED16(); break;
case 17 : LED17(); break;
case 18 : LED18(); break;
case 19 : LED19(); break;
case 20 : LED20(); break;
case 21 : LED21(); break;
case 22 : LED22(); break;
case 23 : LED23(); break;
case 24 : LED24(); break;
case 25 : LED25(); break;
case 26 : LED26(); break;
case 27 : LED27(); break;
case 28 : LED28(); break;
case 29 : LED29(); break;
case 30 : LED30(); break;
}
}
void setup(){}
void loop()
{
for (int q = 0; q < 10; q++) ❼
{
displayDigits(q, 50); ❽
delay(250);
}
}
二维数组 ❶ 包含 10 个数组,分别对应数字 0 到 9。每个数字数组有 30 个元素,与显示板上的 30 个 LED 对应。显示板上的 LED 从左上角的 1 到右下角的 30 排列。在数组中,每个 1 表示需要快速依次点亮的 LED,以显示每个数字。
自定义的 displayDigits() 函数 ❷ 接受你要显示的数字以及该函数显示所需 LED 的次数。第二个参数中的数字越大,数字在显示器上的显示时间就越长。displayDigits() 函数使用另一个自定义函数 turnOnLED() ❸ 来点亮所需的 LED。turnOnLED() 函数又使用 switch…case ❻ 来调用列表 ❺ 中的适当函数,并点亮每个单独的 LED。
delay() 函数 ❹ 在 displayDigits() 内被设置为使每个 LED 在下一个点亮之前保持点亮 1 毫秒。由于 LED 亮灭非常快,人眼将所需的所有 LED 视为在同一时间亮起,如图 5-10 所示。你可以改变延迟的长度,以调整显示效果以适应个人喜好。
最后,在 void loop() 中,数字 0 到 9 会依次显示 ❼,并在每个数字之间有短暂的延迟。每个数字通过 50 次刷新来显示 ❽。

图 5-10:项目 #18 的示例
该草图使用 Monomin 字体显示数字,如图 5-11 所示。

图 5-11:Monomin 6×5 字体数字
你可以通过访问 https://
制作自定义字符显示
除了项目 #18 中显示的数字外,你还可以创建其他显示类型。要创建你自己的字符,首先使用一些方格纸或电子表格绘制你想要的显示样式。例如,要创建一个边界矩形,绘制图 5-12 所示的图示。

图 5-12:规划要显示的字符
左上角的 X 代表 LED 1,右下角的 X 代表 LED 30。然后,你创建自己的数组,表示每个 LED 的状态。对于这个示例,数组看起来会是这样的:
{1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1}
然后,你可以像在项目 #18 中对待其他数字一样,将这个数组添加或替换到草图中,并使用 displayDigits() 函数在显示器上显示自定义字符,如 图 5-13 所示。

图 5-13:自定义字符
在设计你自己的自定义字符时,使用电子表格或图纸来规划你想要开启和关闭的 LED 布局会有所帮助。
继续
使用 Charlieplexing 技术,你现在可以用最少的 I/O 引脚控制多个 LED,而无需使用外部集成电路。你已经学会了如何通过最少的数字输出引脚控制 2、6、12 或 30 个 LED,并且了解了如何使用 Charlieplexing 控制紧凑型 ATtiny 微控制器。
在下一章,你将学习如何使你的 Arduino 能够自动关闭电源。
第六章:6 添加专业电源控制

你可以使用软电源控制而不是通过开关或 USB 线来控制 Arduino 的电源:通过按钮打开和关闭电源,或者让 Arduino 通过程序或外部传感器自动关闭电源。软电源提高了项目的专业性。例如,如果你正在设计一个精美的高保真放大器,一对开关按钮看起来比你在工业设备上看到的那种开关要好得多。
本章介绍了多种软电源控制方法。你将学到:
-
使用 MOSFET 控制更大的电流
-
使用 555 定时器 IC 的双稳态模式
-
使用外部设备打开 Arduino 电路
-
通过构建仅在需要时开启的项目来节省电力
-
使用 DS3231 实时时钟(RTC)库进行 Arduino 开发
你还将为 Arduino 创建一个软开关,并构建一个低功耗事件记录器。
无物理开关供电 Arduino
为 Arduino 项目供电通常是通过 USB 插口、电池组或外部 AC 适配器。此时打开和关闭项目意味着需要拔掉电缆或 DC 插头,这对于仅做实验或为个人使用而构建的项目来说是可行的。然而,软电源控制使你能够通过按钮或来自其他设备的信号控制 Arduino 项目的电源,或者让 Arduino 项目完全关机。这不需要连接或切断电流流动的“硬”物理开关。
软电源控制减少了硬件的磨损,因为没有用于电源控制的活动部件,并且使接口更加简洁易用。通过按钮开启和关闭电源,或者进一步自动化设备控制,能够创造更好的用户体验。
本节进一步讨论了实现这些软电源控制电路所需的两种组件:用于开关电流的 MOSFET;以及 555 定时器 IC,在本例中作为软电源控制中的开关信号接口。
操作 MOSFET
MOSFET 允许你通过小信号(例如来自 Arduino 数字输出引脚的信号)开关大电压和电流。它们有多种大小可供选择,如图 6-1 所示。

图 6-1:各种 MOSFET
图 6-1 左下角显示的是来自第四章项目#14 的小型 2N7000 N-MOSFET,我们将在本章中继续使用它。看 2N7000 的正面(平面一侧),三个引脚从左到右分别是:
-
电源
-
门极
-
排水
图 6-2 显示了 2N7000 MOSFET 的原理符号。

图 6-2:2N7000 MOSFET 的原理符号
操作 MOSFET 非常简单。当你向栅极引脚施加一个小电流时,较大的电流就可以通过漏极引脚流入,并从源极引脚流出。你还可以使用 PWM 控制 MOSFET,从而实现对灯光、电机等设备的多种控制。你的 2N7000 MOSFET 可以连续处理高达 60 V DC 和 200 mA,或在突发情况下处理 500 mA。在为其他项目选择 MOSFET 时,一定要检查最大电压和电流是否符合你想要切换的信号要求。
你可以使用更大的 MOSFET 来控制更大的电流,例如 图 6-3 中显示的 IRF520。

图 6-3:IRF520 N-MOSFET
使用 MOSFET 时,务必查看数据表以确认引脚定义,因为不同型号的引脚定义可能不同。你可以找到更大的 MOSFET 扩展板形式,方便原型制作。图 6-4 显示了两个例子:左侧是 Freetronics 的 N-MOSFET 模块,右侧是 PMD Way(零件号 759300)的 IRF520 扩展板。

图 6-4:两块示例 MOSFET 扩展板
在本章中,你将使用 2N7000 来控制 Arduino 的电源供应。你还会在栅极和源极引脚之间连接一个 10 kΩ 的电阻,以确保当没有电流作用于栅极时,栅极保持关闭状态,防止 MOSFET 在随机情况下微弱开关。
在双稳态模式下使用 555 定时器 IC
要创建开/关开关,你可以使用 555 定时器 IC(最初用于 第四章)在 双稳态模式 下,配置电路使两个按钮切换输出引脚的电平为高或低。然后,输出将用来控制 MOSFET,进而开关 Arduino 的电源。
图 6-5 显示了双稳态 555 电路。

图 6-5:555 定时器 IC 在双稳态模式下的原理图
将引脚 2(触发引脚)通过 SW[1] 拉至 GND 将开启输出,而将引脚 4(复位引脚)通过 SW[2] 拉至 GND 将关闭输出。这就是你所需要的软开关!该电路可以在 5 V 到 12 V DC 的电压范围内工作。自己动手制作并测试,或许可以使用 LED 和 1 kΩ 电阻作为输出指示器。
然而,在本章的项目中,你需要切换整个 Arduino 的电源。在接下来的项目中,你将不再使用输出引脚作为电源,而是通过双稳态电路开关 2N7000 N-MOSFET 的开关,允许你控制更大的电流。
项目#19:创建一个软开关
在这个项目中,你将为你的 Arduino 创建一个软开关。你可以使用 9 到 12 V 直流电源来驱动 555 定时器;Arduino 将调节电压至其所需的 5 V。
你可以使用无焊面包板搭建此电路进行临时实验,或者使用可下载的项目文件创建一个定制的 PCB,如果你希望制作一个更持久的版本。你将使用这里列出的零件来完成本章剩余项目:
-
一个 Arduino Uno 或兼容板和 USB 电缆
-
一个 9 到 12 V 直流、1A 电源,墙壁适配器或插头式电源
-
一个无焊面包板或项目#19 的 PCB
-
三个 10 kΩ、0.25 W、1%的电阻
-
一个 2N7000 N-MOSFET
-
一个 555 定时器 IC(如果使用 PCB 的话,还需要一个 8 针 IC 插座)
-
两个触觉按钮
-
1 × 40 2.54 mm 内联连接器针脚(如果使用 PCB)
-
一个 PCB 安装的直流插座(如果使用 PCB 的话)
-
公对公跳线
如果你使用的是带有直流插头的电源,例如墙壁适配器,你可能希望使用像 PMD Way 的 51201299 直流插座模块,以避免需要切断电源线插头,如图 6-6 所示。

图 6-6:直流插座模块
按照图 6-7 所示组装电路。

图 6-7:项目#19 的原理图
如果你使用的是无焊面包板,直流插座只是将所需的 9 到 12 V 直流电源送到电路。原理图右上方的 Vin 和 GND 标签分别是 Arduino 的 Vin 和 GND 引脚的连接。
如果你使用的是图 6-8 中所示的 PCB,组装过程非常简单。首先安装电阻器,然后是按钮,接着是 IC 插座、直流插座和内联连接器针脚。(这个项目中有一些额外的连接没有列出,稍后在项目#20 中你会使用到它们。)

图 6-8:项目#19 的 PCB
一旦组装完成,你的 PCB 应该与图 6-9 所示相似。

图 6-9:项目#19 完成的 PCB
将 Arduino 连接到 PCB 上,然后连接外部电源,电压范围为 6 到 12 V 直流电。你可以通过 SW[1]和 SW[2]分别打开和关闭 Arduino。Arduino 的电源指示 LED 将立即告诉你 Arduino 是否开机。
如前所述,当你按下 SW[2]时,555 的输出应该变为 HIGH。在这个电路中,电流流向 N-MOSFET 的栅极(G)引脚,进而开启 MOSFET,使得电流通过漏极(D)引脚流向源极(S)引脚。(即使 Arduino 关闭,电路本身仍会在 9 V 直流电下消耗约 5 mA 的电流。)当 MOSFET 开启时,电流可以通过 Vin 引脚从外部电源流入 Arduino,再通过 GND 引脚流出,通过 MOSFET,最后回到 GND,完成电力回路。当你按下 SW[1]时,555 的输出变为 LOW,MOSFET 被关闭。这意味着没有电流可以通过 Arduino,从而将其关闭。
如果你使用的是无焊面包板,请将电路保持在整个章节中不变。如果它们满足相同的电力需求,你也可以将此电路与其他设备一起使用。现在,让我们使用这个电路来赋予 Arduino 关闭自身的能力。
项目#20:自动关闭 Arduino
作为项目#19 中软开关电路的扩展,你可以通过将 555 的引脚 4 拉至 LOW 来让 Arduino 自动关闭(如同在前一个项目中通过 SW[1]实现的)。这对于创建不需要持续运行的应用非常有用,适用于从简单的游戏到需要仅在事件发生时才记录数据的更复杂数据记录项目。
为了修改项目#19 的电路以实现这个目的,首先将数字输出引脚 D12 连接到 555 引脚 4 和电阻 R[1]的交点,成为关闭信号连接,如果使用无焊面包板,如图 6-10 所示,或者连接到 PCB 上标记为 A OFF 的引脚。

图 6-10:带有 Arduino 自关接点的项目#20 电路
还需连接 Vin 和 GND。打开 Arduino(这应该会点亮板载电源 LED),D13 LED 也应点亮。五秒钟后,D13 LED 应该熄灭;然后,一秒钟后,Arduino 应自动关闭。
让我们看看这个是如何工作的:
// Project #20 - Arduino self power off
❶ void turnOff()
{
digitalWrite(12, LOW);
}
void setup()
{
pinMode(12, OUTPUT);
pinMode(13, OUTPUT);
digitalWrite(12, HIGH);
}
void loop()
{
❷ digitalWrite(13, HIGH);
delay(5000);
❸ digitalWrite(13, LOW);
delay(1000);
❹ turnOff();
}
程序首先确保在void setup()中 D12 为 HIGH。接下来,一个简单的自定义函数turnOff() ❶ 将 D12 置为 LOW,触发 555 关闭 MOSFET,从而关闭 Arduino 的电源。程序将点亮 D13 LED ❷ 并熄灭 ❸ 作为活动示例;然后,Arduino 将自动关闭 ❹。
从外部设备激活 Arduino
到目前为止,你已经了解了 Arduino 如何自我关闭。我现在将解释如何编程一个外部设备或传感器来激活 Arduino 的电源,它完成任务后再关闭电源,为下一个事件做好准备。
常开触点设备
在第 18 和第 19 个项目中使用的开关关闭了 555 引脚 2 和 GND 之间的电路。这意味着你可以用(或与)具有常开(NO)触点的设备替代该开关——换句话说,就是用一种与按键或开关关闭电路的方式相同的设备。这可以是任何东西,例如简单的门铃按钮、当踩在上面时起作用的压力垫、带继电器输出触点的被动红外(PIR)运动传感器、常开簧片开关、可以检测门是否打开的门磁等。
任何可以闭合电路中两个触点的设备都会激活你的 Arduino。只需将设备的两根线分别连接到 555 引脚 2 和 GND,如图 6-11 所示。

图 6-11:带外部触发连接的软开/关电路原理图
这两个触点在原理图中显示为 ON_SIGNAL 点。如果你使用的是本章的项目 PCB,那么这些点就是 ON+和 ON−引脚。
输出逻辑设备
你可能希望通过一些在激活时发出电信号的设备来开启 Arduino,例如逻辑电平 5V 输出。这些设备包括一些被动红外(PIR)运动探测器、温控器、一些工业可编程逻辑控制器(PLC)设备输出等。然而,这些设备需要稍微复杂一点的电路来连接激活点,因为它们的输出信号与前一部分描述的常开方法不同。
为了利用这些设备,你将使用它们的逻辑输出来驱动另一个 2N7000 MOSFET,从而桥接电路并作为开关为你开启电源。这些设备的输出电压必须足够高,才能开启 MOSFET,所以在开始之前一定要向设备供应商或查阅数据手册确认这一点。你将在下一个项目中使用的 MOSFET 需要 3 到 5V DC 来激活。
图 6-12 显示了一个示例电路,左侧包含了必要的额外 MOSFET 和输入。TRIGGER 点连接到设备的输出,而 TR_GND 连接到设备的负极或地线。

图 6-12:带逻辑电平触发的软开/关电路原理图
在进入下一个项目之前,我将向你展示一个更简单的方法,通过安装一个有用的 Arduino 库来使用 DS1307 和 DS3231 实时时钟 IC。
DS3231 实时时钟库
下一个项目使用 DS1307 或 DS3231 实时时钟 IC 来为数据记录项目保持时间。为了简化这个过程,你现在将学习如何使用匹配的 DS3231 RTC 库来节省编码工作和空间。该库的工作方式与 DS1307 的库相似。
首先,从https://
包含库
添加.ZIP 库。导航到下载的文件,然后点击确定来安装库。你可以通过在 IDE 中选择文件
示例来检查库是否已安装;一个新的 DS3231 选项应该会出现。
输入列表 6-1 中的草图来测试库以及设置和获取时间和日期信息的基本功能,但现在不要上传它:
❶ #include <Wire.h>
#include <DS3231.h>
DS3231 RTC;
❷ bool century = false;
bool h12Flag = false;
bool pmFlag = false;
// Change the following for your own time
byte year = `22`;
byte month = `9`;
byte date = `2`;
byte hour = `11`;
byte minute = `8`;
byte second = `0`;
void setTimeData()
{
RTC.setYear(year);
RTC.setMonth(month);
RTC.setDate(date);
RTC.setHour(hour);
RTC.setMinute(minute);
RTC.setSecond(second);
RTC.setClockMode(false); // Set clock to 24 hour
}
void setup()
{
Wire.begin();
❸ //setTimeData(); // Set time and date
Serial.begin(9600);
}
void loop()
{
// Display data on Serial Monitor
Serial.print(RTC.getDate());
Serial.print("/");
Serial.print(RTC.getMonth(century));
Serial.print("/20");
Serial.print(RTC.getYear());
Serial.print(" - ");
Serial.print(RTC.getHour(h12Flag, pmFlag));
Serial.print(":");
❹ if (RTC.getMinute() < 10)
{
Serial.print("0");
}
Serial.print(RTC.getMinute());
Serial.print(":");
❺ if (RTC.getSecond() < 10)
{
Serial.print("0");
}
Serial.println(RTC.getSecond());
delay(1000);
}
列表 6-1:测试 DS3231 RTC 库
该草图首先包含 I²C 和 RTC 库,然后创建一个实时时钟实例以进行引用 ❶。然后,它声明了所需的变量来保存时间和日期信息 ❷。其中包括三个布尔变量,默认情况下设置为 false,因为你使用的是 24 小时制。
要设置时间,自定义函数setTimeData()包含设置所有时间和日期参数的功能。之前声明的变量被放入每个匹配的函数中。此函数只需调用一次 ❸,因为你将首先设置时间和日期,然后在重新上传草图之前注释掉该函数。否则,时钟将重置为变量的值 ❷。
要检索时间,草图使用一系列以RTC开头的函数,如RTC.getdate(),它们返回相应的数据。为了让分钟和秒数的显示更自然,草图在小于 10 的分钟值 ❹ 和秒数值 ❺ 前显示 0。
要将时间和日期值更改为匹配你自己的时区,输入你自己的时间和日期值 ❷,然后取消注释函数setTimeData()❸,最后上传草图。现在重新注释掉setTimeData()函数,保存并重新上传草图。完成后,以 9,600 bps 打开串口监视器,你应该看到日期和时间每秒更新一次。
让我们将这些触发 Arduino 电源并记录事件的方法结合在一个最终项目中。
项目#21:构建事件记录器
对于这个项目,假设有人需要定期证明自己到达了某个地点。这可能是一个孩子到达学校或参加活动,或者是一个保安检查上班时间,或是一个必须在早上某个特定时间开店的员工。为了创建一个记录这些事件时间和日期的设备,本项目使用了带有 Arduino 和 DS1307 或 DS3231 实时时钟 IC 以及 SD 卡插槽的开关电路来将数据记录到存储卡中。触发器将是一个简单的按钮,布置在离 Arduino 一定距离的地方,就像有线门铃中的按钮一样。
作为一名经验丰富的 Arduino 用户,你可能会用与本项目中略有不同的产品来构建类似的电路。例如,你可以使用 SD 卡模块或 SD 卡扩展板(例如 PMD Way 的 668046 部件,如图 6-13 所示)来构建一个类似的电路。你可以将本项目中学到的框架应用于任何希望触发 Arduino 做某件事一次并且在下一个事件发生之前保持关闭的情况。

图 6-13:Arduino 的 SD 和 RTC 扩展板
你将使用以下硬件:
-
一块 Arduino Uno 或兼容板和 USB 电缆
-
一款 9 至 12 伏直流电源,1 安培的电源适配器或插头电源
-
项目#20 的完整 PCB 或硬件
-
一块 DS1307 或 DS3231 实时时钟模块和 SD 或 microSD 卡模块,或者 Arduino 的 SD 卡和 RTC 扩展板
-
一张 microSD 或 SD 存储卡
-
一个远程按钮和合适的双核线
按照图 6-14 中的示意图组装电路。如果你使用的是无焊接面包板,并且不需要本地控制,可以省略触摸开关。

图 6-14:项目#21 的原理图
别忘了正确格式化你的存储卡。
注意
如果你不熟悉使用 SD 卡或实时时钟模块,请参考《Arduino 工作坊》第七章和第二十章(第二版)。
一旦你组装好硬件,就在项目#21 的草图中使用函数setDS3231time()在void setup()中设置当前时间和日期。为了保持实时时钟中的时间和日期,上传草图后注释掉该行并重新上传草图,就像在上一节中做的那样。
让我们看看它是如何工作的:
// Project #21 - Arduino event logger
❶ #include <Wire.h>
#include <DS3231.h>
DS3231 RTC;
#include <SD.h>
#define offPin 9
❷ bool century = false;
bool h12Flag = false;
bool pmFlag = false;
byte year = `22`;
byte month = `9`;
byte date = `2`;
byte hour = `11`;
byte minute = `8`;
byte second = `0`;
void setTimeData()
{
RTC.setYear(year);
RTC.setMonth(month);
RTC.setDate(date);
RTC.setHour(hour);
RTC.setMinute(minute);
RTC.setSecond(second);
RTC.setClockMode(false); // Set clock to 24 hour
}
void turnOff()
{
digitalWrite(offPin, LOW);
}
void logEvent()
{
// Create the file for writing
File dataFile = SD.open("DATA.TXT", FILE_WRITE);
// If the file is ready, write to it:
if (dataFile)
{
dataFile.print("Event occurred on: ");
dataFile.print(RTC.getDate());
dataFile.print("/");
dataFile.print(RTC.getMonth(century));
dataFile.print("/20");
dataFile.print(RTC.getYear());
dataFile.print(" - ");
dataFile.print(RTC.getHour(h12Flag, pmFlag));
dataFile.print(":");
if (RTC.getMinute() < 10) {
dataFile.print("0");
}
dataFile.print(RTC.getMinute());
dataFile.print(":");
if (RTC.getSecond() < 10) {
dataFile.print("0");
}
dataFile.println(RTC.getSecond());
dataFile.close(); // Close the file once the system has finished with it
}
}
void setup()
{
Wire.begin();
//setTimeData(); // Set time and date
pinMode(offPin, OUTPUT);
digitalWrite(offPin, HIGH);
pinMode(10, OUTPUT);
// Check that the memory card exists and is usable
if (!SD.begin(10))
{
// Stop sketch:
return;
}
}
❸ void loop()
{
logEvent(); // Log event to SD card
delay(1000); // Wait a moment
turnOff();
}
该草图结合了基本的实时时钟和 SD 卡写入功能,以及一旦事件记录到存储卡后自我关闭的功能。它首先包含所需的库,然后设置 RTC I²C 总线地址和触发 555 定时器关闭电路的引脚编号 ❶。接着,它设置所需的日期和时间变量,以及写入数据的函数 ❷。
当调用时,turnOff() 函数将连接到 555 定时器的数字引脚设置为 LOW,关闭电路。logEvent() 函数打开 SD 卡上的文本文件,从实时时钟获取时间和日期,并将其整齐地写入一行。void setup() 函数初始化实时时钟并允许设置时间和日期。它还初始化触发外部电路和使用 SD 卡所需的数字引脚,并检查 SD 卡是否准备好。
最终的循环 ❸ 在外部设备电路触发 Arduino 并将其打开时开始运行。logEvent() 函数记录事件,将时间和日期写入 SD 卡。之后有一个短暂的延迟,确保数据文件已关闭,接着 turnOff() 关闭 Arduino。
要查看项目记录的数据,请断开 Arduino 的电源并将 SD 卡插入计算机。打开 DATA.TXT 文件查看按顺序列出的事件。
项目为每个事件记录了包含日期和时间的新文本行。
在 9 V 直流电压下运行时,项目关闭时电流约为 5 mA,工作时最大电流为 70 mA。如果使用六个 AA 电池进行便携式使用,并且每小时发生一到两个事件,则该项目可以轻松运行超过七天。
继续前进
本章向您展示了如何为您的项目添加专业的电源控制,通过让外部动作打开电源并让 Arduino 自动关闭电源,从而降低功耗。
下一章演示了另一种先进的开关控制方式:使用无线遥控插座安全控制交流电流。
第七章:7 控制交流电源插座

在本章中,你将学会如何通过各种自动化和远程控制方式安全地控制主电源交流插座。此技术让你能够轻松地从远程操作设备,如灯具、风扇和水泵,而无需与主电源线路直接连接。
你将学到:
-
使用光耦合器来隔离电气信号
-
改装无线交流电源远程控制发射器,并将其连接到 Arduino 电路
-
创建一个定时控制的无线交流电源插座
-
使用短信控制远程交流电源插座
光耦合器
光耦合器是一种小型设备,它可以在电路的不同部分之间传输信号,同时保持这两部分电气隔离。在典型的光耦合器内部,有一个 LED 和一个光电晶体管,后者通过响应光线放大电流。当电流通过 LED 时,它会打开;光电晶体管检测到光线后,允许另一个电流通过。当 LED 关闭时,电流无法通过光电晶体管。整个过程中,这两个电流是完全电气隔离的。
图 7-1 展示了光耦合器的原理符号。

图 7-1:典型光耦合器的原理符号
在原理图中,LED 连接到 1 引脚(阳极)和 2 引脚(阴极)。4、5、6 引脚是光电晶体管,而 3 引脚没有使用。你将使用的光耦合器型号 4N28 是一个六脚双排直插(DIL)封装,如图 7-2 所示。

图 7-2:插入无焊面包板中的 4N28 光耦合器
使用光耦合器时,你需要为 LED 添加一个限流电阻。在本章中,你将使用 1 kΩ的电阻。将电路连接到 4 和 5 引脚之间的开关,以控制开关的开启或关闭。如果你想实验光耦合器,可以构建图 7-3 中显示的电路。

图 7-3:光耦合器演示电路
当 SW[1]闭合时,电流通过电阻 R[1],光耦合器内的 LED 发光。接着,光电晶体管被触发,允许电流从 5 引脚流向 4 引脚,从而点亮 LED D[1]。光耦合器的 3 引脚没有电气连接。这使得光耦合器像一个开关一样工作,但两侧之间没有任何电气或机械接触。
在接下来的项目中,你将使用光耦合器与无线遥控交流电插座的发射器进行接口连接。
遥控交流电插座
像图 7-4 所示的廉价无线遥控交流电插座,能够安全地控制市电。在图 7-4 中,发射器位于右侧,接收器插座位于左侧。

图 7-4:典型的无线遥控交流电插座
发射器需要独立的开关按钮进行控制。尽量找一个按钮之间有较大间距的型号;按钮间距越大,在第 22 个项目中破解发射器时就越容易,这样可以将其连接到 Arduino 电路,以控制电源插座。你可以从常见零售商那里购买这些设备,如亚马逊、沃尔玛等。
警告
无论如何,你绝对不应打开或修改包含市电插座的接收器单元,因为暴露在市电线路中可能会致命。
为了顺利完成本章内容,我建议购买两套控制设备:一套用于实验(这样你就不用太担心弄坏它),另一套用于项目。
破解插座发射器
本节将解释如何破解无线发射器,并通过 Arduino 进行控制。我将通过一系列照片展示如何破解我的遥控器,你可以参考这些指导方针,对你的遥控器进行实验,达到相同的目的。此过程需要一定的焊接和拆焊操作,因此请确保你准备了一个 20 到 50 瓦的爱好者级焊接铁、一些手工工具(如侧切钳),以及一些拆焊丝。
发射器在形状、大小等方面会有所不同,但请确保你购买的发射器使用小型 12V A23 型电池,并且有独立的开关按钮,如前所述。在对设备进行任何损坏之前,插入电池并进行测试,以了解发射器与接收器之间的有效距离,从而确定你可以使用该插座的距离。图 7-5 展示了我发射器中的电池。

图 7-5:无线遥控发射器中的电池
检查是否有任何螺丝或卡扣可以用来打开发射器外壳,并按照图 7-6 所示将其打开。

图 7-6:无线遥控发射器内部接线
这样应该能看到电路板和控制输出的按钮。小心地取下电路板,检查是否有可能去除某些按钮的焊接;如果不行,就重新组装设备并将其退还给零售商。对于符合本节之前描述规格的任何发射器,你应该能去除按钮,但最好确认这些按钮是通过孔脚的类型。
你还需要连接电源。如果电池架到电路板(PCB)之间已经有电线连接,如图 7-7 所示,仔细将其切断或使用吸锡带将电池架两端的电线去除。

图 7-7: 电路板中的电源电线
接下来,通过定位按钮,并找到 PCB 另一侧与按钮匹配的引脚,识别系统中第一个(或唯一)插座的开关按钮的焊盘。从电路板底部查看按钮(即有铜轨的一面),将按钮引脚与电路板的焊盘匹配。
在我们的示例中,电路板使用了标准的四脚触觉按钮,每个触点有两根引脚。在图 7-8 中,我用线条标记了触觉开关两侧的焊盘,以标注出我需要去除焊接的焊盘。

图 7-8: 发射器电路板,按钮引脚用黑色标记笔分开
一旦找到了开关按钮的焊盘,使用吸锡带去除引脚,最好使用一些吸锡带,如图 7-9 所示。小心不要过热并损坏焊盘。

图 7-9: 从发射器上去除按钮的焊接
现在,轻轻地从电路板上撬出按钮。这样,你将得到一些空白的按钮位置,如图 7-10 所示。

图 7-10: 移除控制按钮的发射器电路板
在这些空的孔位中,焊接一些足够长的跳线,以便将电路板与无焊面包板连接。我已将一些预制跳线的两端剪掉,以便轻松连接到外部电路,如图 7-11 所示。

图 7-11:修改后的发射器电路板接线
现在是检查发射器电路板操作的时候了。将 12V 电源连接到电源引线,然后依次短接开关的开和关导线对,检查接收器是否仍能开关。如果不能,可能是(已拆除的)按钮形成了电路的一部分——也就是说,每个按钮的两边接触点都作为 PCB 轨道连接使用。我遇到的情况是这样的,因此我将一根短导线横跨按钮的一侧并将其焊接到电路板上,如图 7-12 所示。

图 7-12:为发射器按钮焊盘添加连接
如有必要,再次测试系统。如果测试成功,将连接线的焊点用热熔胶覆盖,以防止它们以后意外脱落,如图 7-13 所示。

图 7-13:用热熔胶保护额外焊接的连接
现在你有了一个可以通过 Arduino 和外部光耦电路控制的发射器电路板。让我们开始使用它吧。
项目 #22:控制发射器电路板
这个项目简要演示了如何控制一个被破解的发射器电路板,这为你在自己的项目中控制插座提供了框架。你将需要以下部件:
-
一块 Arduino Uno 或兼容板和 USB 数据线
-
一个 12V DC、1A 电源适配器,墙插电源适配器,或插头电源(如果需要,你可以使用第六章中为项目 #19 描述的 DC 插座插座扩展模块)
-
来自上一节的被破解的无线插座发射器和接收器
-
两个 1 kΩ、0.25 W、1% 的电阻器
-
两个 4N28 光耦合器
-
一块无焊面包板
-
各种跳线
按照图 7-14 所示组装电路。

图 7-14:项目 #22 的电路图
电路图中的 TX_On 和 TX_Off 对应发射器电路板控制按钮的接线,而 TX_12VDC+ 和 – 则表示连接到发射器电路板的 12V 电源引线。由于 12V DC 为整个电路提供电源,因此 Arduino 的电源通过 Vin 引脚供电。Arduino 的 D2 和 D3 引脚用来控制发射器的开关。
现在上传项目 #22 的草图。几秒钟后,主电源插座应每隔大约五秒钟开关一次。
让我们看看这是如何工作的:
// Project #22 - Mains outlet control with Arduino
❶ void mainsOff()
{
digitalWrite(3, HIGH);
delay(1000);
digitalWrite(3, LOW);
}
❷ void mainsOn()
{
digitalWrite(2, HIGH);
delay(1000);
digitalWrite(2, LOW);
}
void setup()
{
delay(1000);
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
}
void loop()
{
❸ mainsOn(); // AC mains on
delay(5000);
❹ mainsOff(); // AC mains off
delay(5000);
}
为了控制发射器,草图只需关闭开关的触点,这些触点现在由光耦合器表示,使用 ❶ 和 ❷ 中的函数。这些函数中的延迟确保发射器已激活足够长的时间以触发接收单元。
在 void setup() 中的延迟有助于测试电路,否则发射器在为 Arduino 通电时可能会立即开启。你应该在自己的项目中保留这个延迟,以避免不必要或不希望的插座激活。
最后,代码通过慢慢打开和关闭交流电来演示控制 ❸ ❹。你可以将灯或小风扇连接到交流电源插座,以查看效果。
将本项目的硬件保留好,因为你将在下一个项目中扩展使用它。
项目 #23:用定时器控制交流电源插座
在这个项目中,你将使用 DS1307 或 DS3231 实时时钟 IC 来构建一个交流电源插座,可以在所需的时间点打开或关闭。这是控制小型灌溉泵、夜灯、警报器或类似设备的绝佳方法。
要构建这个项目,你需要与项目 #22 中使用的相同硬件设置,以及 DS3231 或 DS1307 RTC 模块,或者是项目 #21 中使用的日志盾牌,详见第六章。按照图 7-15 中的示意图组装电路。

图 7-15:项目 #23 的原理图
现在上传项目 #23 草图(它应该适用于你使用的任何 RTC IC)。别忘了使用 setDS3231time() 函数在 void setup() 中设置当前时间数据。一旦你更新了时间值,取消注释该函数,上传草图,然后重新注释该函数并再次上传草图。我使用了 DS3231 库,如在第六章的项目 #21 中所示。
让我们看看它是如何工作的:
// Project #23 - Timer-controlled mains outlet
#include <Wire.h> ❶
#include <DS3231.h>
DS3231 RTC;
bool century = false; ❷
bool h12Flag = false;
bool pmFlag = false;
byte year = `22`;
byte month = `9`;
byte date = `19`;
byte hour = `21`;
byte minute = `53`;
byte second = `0`;
void setTimeData()
{
RTC.setYear(year);
RTC.setMonth(month);
RTC.setDate(date);
RTC.setHour(hour);
RTC.setMinute(minute);
RTC.setSecond(second);
RTC.setClockMode(false); // Set clock to 24 hour
}
void mainsOff()
{
digitalWrite(3, HIGH);
delay(1000);
digitalWrite(3, LOW);
digitalWrite(13, LOW);
}
void mainsOn()
{
digitalWrite(2, HIGH);
delay(1000);
digitalWrite(2, LOW);
digitalWrite(13, HIGH);
}
void setup()
{
mainsOff();
//setTimeData(); ❸ // Set time and date
Wire.begin();
pinMode(2, OUTPUT);
pinMode(3, OUTPUT);
pinMode(13, OUTPUT);
digitalWrite(13, LOW);
}
void turnOn(int onHour, int onMinute)
{
if ((RTC.getHour(h12Flag, pmFlag) == hour) && (onMinute == RTC.getMinute()))
{
mainsOn();
delay(59100);
}
}
void turnOff(int offHour, int offMinute)
{
if ((RTC.getHour(h12Flag, pmFlag) == hour) && (offMinute == RTC.getMinute()))
{
mainsOff();
delay(59100);
}
}
void loop()
{
turnOn(17, 02);
turnOff(17, 03);
turnOn(17, 04);
turnOff(17, 05);
turnOn(17, 06);
turnOff(17, 07);
}
草图首先包含 I²C 和 RTC 库,然后创建一个实时时钟实例以进行引用 ❶。接着,它声明所需的变量来保存时间和数据 ❷。这些变量包括三个布尔值变量,默认设置为 false,因为草图使用的是 24 小时制时间。
自定义 setTimeData() 函数包含设置所有时间和日期参数的函数。之前声明的变量被放置到每个匹配的函数中。这个函数只需要调用一次 ❸,因为你最初设置了时间和日期,然后在重新上传草图之前注释掉该函数;否则,时钟将重置为变量的值。
mainsOff() 和 mainsOn() 函数还使用 Arduino 板上 D13 的 LED 指示系统的开关状态。对于在特定时间控制插座,两个 turnOn() 和 turnoff() 函数各自接受一个小时和分钟参数,并将其与当前时间进行比较。
如果时间匹配,插座就会被打开或关闭,如在 void loop() 中的示例所示。你可以根据需要添加任意多的开关函数;代码将不断循环,检查是否需要开关插座。在 turnOn() 和 turnOff() 函数中有一个较长的延迟,目的是防止在开关时间匹配时发生多次触发。
如果你想增加一些挑战性,可以修改代码,让用户可以选择日期或星期几,并结合时间来控制开关。
项目 #24:通过短信控制主电源插座
这个项目将无线控制提升到了一个新水平,结合使用项目 #22 的硬件和一个 3G 蜂窝盾牌,创建一个可以通过短信控制的交流主电源插座(只要有手机信号覆盖)。这样做出奇的简单。
注意
你可能在《Arduino 工作坊》第 2 版的第二十二章中构建过类似的项目。
要使此项目正常运行,你需要接入一个支持 UMTS(3G)850 MHz、900 MHz、1900 MHz 或 2100 MHz 频段的蜂窝网络,并且允许使用非网络提供商提供的设备。你的蜂窝服务提供商应该能提供此类信息。此外,你还需要为该盾牌准备一个 SIM 卡(可为预付费卡或其他类型的卡),并确保 SIM 卡的 PIN 码设置已关闭。(你可以通过将 SIM 卡插入普通手机并在安全菜单中修改设置来实现这一点。)
本项目使用的是 SIM5320 型 3G GSM 盾牌和天线,具体如图 7-16 所示。该盾牌可以通过 TinySine(https://

图 7-16:带天线的 3G shield
由于这些 shields 需要 12V 外部电源,首先通过 Vin 和 GND 引脚连接你在本章早期项目中使用的电源。为了配置 D2 和 D3 引脚,shield 使用这些引脚通过 SoftwareSerial 与 Arduino 通信,请按照图 7-17 所示,连接跳线到 RX 3 和 TX 2 引脚。

图 7-17:Shield 串行配置跳线
接下来,将 shield 翻转过来,并将你的运营商 SIM 卡插入卡槽,如图 7-18 所示。

图 7-18:SIM 卡及其卡槽
轻轻地将 3G shield 插入 Arduino。连接外部电源和 USB 电缆到 Arduino 和 PC 之间,然后拧上外部天线。最后,按下 shield 左上角的电源按钮开启 SIM 模块,如图 7-19 所示。按下按钮 2 秒钟,然后松开。

图 7-19:3G shield 电源按钮和状态 LED,当电源、状态和网络活动时,LED 会亮起
P(电源)和 S(状态)LED 应该会亮起。蓝色的 W(网络活动)LED 应该会开始闪烁,一旦 3G shield 注册到蜂窝网络,表示你已经准备好使用这个 shield,一切正常。
要构建这个项目,你需要刚刚组装好的 3G shield,以及在项目 #22 中搭建的硬件设置。按照图 7-20 所示组装电路。请注意,光耦合器输入引脚已从项目 #22 中使用的引脚更改为 Arduino 的 D4 和 D5 引脚,因为我们将 D2 和 D3 用于 3G shield 的串行通信。

图 7-20:项目 #24 的原理图
输入并上传以下代码。一旦 shield 开机并且蓝色 LED 开始闪烁,向 SIM 卡的蜂窝号码发送 #1 作为短信。主电源插座应该打开;发送 #0 则可以关闭它。我希望这能激发你短暂的好奇心——这些时刻让学习和构建项目变得非常愉快。
让我们看看这个是如何工作的:
// Project #24 - Setting up an SMS remote control
#include <SoftwareSerial.h>
❶ SoftwareSerial cell(2, 3);
❷ void mainsOff()
{
digitalWrite(5, HIGH);
delay(1000);
digitalWrite(5, LOW);
digitalWrite(13, LOW);
}
❸ void mainsOn()
{
digitalWrite(4, HIGH);
delay(1000);
digitalWrite(4, LOW);
digitalWrite(13, HIGH);
}
void setup()
{
pinMode(5, OUTPUT);
pinMode(4, OUTPUT);
pinMode(13, OUTPUT);
❹ pinMode(8, OUTPUT);
mainsOff();
❺ digitalWrite(8, HIGH);
delay(2000);
digitalWrite(8, LOW);
cell.begin(4800);
delay(30000);
❻ cell.println("AT+CMGF=1");
delay(200);
❼ cell.println("AT+CNMI=3,3,0,0");
delay(200);
}
void loop()
{
char inchar;
if (cell.available() > 0)
{
inchar = cell.read();
❽ if (inchar == '#')
{
delay(10);
inchar = cell.read();
if (inchar == '0')
{
❾ mainsOff();
}
else if (inchar == '1')
{
❿ mainsOn();
}
delay(10);
cell.println("AT+CMGD=1,4"); // Delete all SMS
}
}
}
这段代码初始化了软件串口,用于与 3G 扩展板进行通信❶,然后声明了用于查询来自扩展板的传入数据的变量。发射器控制功能出现在❷和❸位置,代码在void setup()中配置了数字引脚❹。
从❺开始,代码启动并配置 3G 扩展板以供使用,使用 AT 命令AT+CMGF=1将传入的短信转换为文本,并将其发送到软件串口❻。每当 3G 扩展板接收到短信时,详细信息会通过软件串口逐个字符发送到 Arduino❼。代码测试每个传入的字符,看看它是否是#❽;如果是,它会检查是否为0或1,然后分别关闭❾或打开❿发射器。
扩展板的电源按钮连接到数字引脚 8,因此你也可以通过项目中的代码控制电源,而不必手动开关按钮。
继续前进
在本章中,你学习了如何使用各种形式的自动化和远程控制安全地控制主电源插座。你可以将所学知识应用于自己的项目中,通过各种传感器、开关或其他输入设备或代码来控制交流电源插座。只要你的输入设备能够控制 LED,它也能控制交流电源插座。
在下一章中,你将使用高功率移位寄存器构建更加有趣的控制应用。
第八章:8 控制高功率设备

许多 Arduino 用户依赖 74HC595 移位寄存器来进行项目,因为它流行且易于使用。然而,74HC595 只能处理相对较小的电流,特别是当所有引脚都处于活动状态时:虽然每个输出可以连续提供 20 mA 电流,但整个 IC 的 V[CC]或 GND 引脚的最大电流仅为 70 mA。
如果你需要每个输出提供 20 mA 电流来驱动,例如八个独立的 LED,你只能在推荐的工作条件下使用八个引脚中的三个。虽然可能超出制造商的建议,但良好的电子设计应该考虑安全性和可靠性。设计为处理更高电流的移位寄存器是更好的选择。
本章向你展示如何使用 Arduino 和 TPIC6B595 移位寄存器集成电路控制高功率设备。你将学习:
-
实验二进制数字显示
-
使用多个 TPIC6B595 来控制超过八个高功率输出
-
使用比普通 LED 更强大的亮度 Pirhana 风格 LED
你还将构建一个 PC 控制的八继电器板,并控制巨大的七段数字显示。
TPIC6B595
TPIC6B595 的控制方式与 74HC595 相同,但每个输出可提供最高 150 mA 的电流,整个 IC 的总电流为 500 mA——当所有引脚都使用时,每个引脚的电流约为 60 mA。它还能够切换最高 50 V DC 的电压。这使得可以控制八个高电流设备,如强力 LED、继电器线圈或机械开关设备。
TPIC6B595 是一个锁存移位寄存器,这意味着只要电源连接,它将保持输出状态。例如,如果你上传一个新的草图,输出不会受到影响。如果你从电源而非 Arduino 供电,你可以在不改变输出的情况下重置 Arduino。
图 8-1 显示了一个 TPIC6B595 双列直插封装,通过孔格式的无焊接面包板。

图 8-1:TPIC6B595 移位寄存器
图 8-2 显示了 TPIC6B595 的原理图符号。

图 8-2:TPIC6B595 的原理图符号
电路图中的八个输出引脚标记为 DRAINx,因为 TPIC6B595 是低侧输出。与前几章使用的 2N7000 N-MOSFET 一样,这些输出控制电流流入引脚(与 74HC595 的高侧输出不同,后者是电流从控制引脚流出)。这意味着受控设备连接在电源和 TPIC6B595 的控制引脚之间,TPIC6B595 则切换电流是否从设备流向地面(GND)。
查看 图 8-3 中的电路图。电流从 5V 电源流经电阻和 LED,再进入 TPIC6B595 的输出引脚。当该引脚被激活时,电流继续流向地面(GND),完成电路。

图 8-3:TPIC6B595 控制 LED 的示例
由于 TPIC6B595 控制电流的方式,它控制的设备电压可高达 50V,而移位寄存器仍然以 5V 工作。方便的是,这意味着你可以控制 12V 或更高电压的设备,而无需担心将电平转换回 Arduino。
让我们通过一个简单的项目来测试 TPIC6B595,演示移位寄存器的操作。
项目 #25:创建一个 TPIC6B595 二进制数字显示
本项目演示了 TPIC6B595 输出的使用,同时复习了二进制数字及其如何与移位寄存器输出控制相关。你将需要以下组件:
-
一块 Arduino Uno 或兼容板和 USB 电缆
-
一块免焊接面包板
-
各种跳线
-
一颗 TPIC6B595 移位寄存器 IC
-
一个 0.1 µF 电容器
-
八个 LED
-
八个 1 kΩ,0.25 W,1% 的电阻器
按照 图 8-4 所示组装电路。

图 8-4: 项目 #25 的电路图
输入并上传项目 #25 的草图。在 IDE 中打开串行监视器,输入一个介于 0 到 255 之间的数字(包括 0 和 255),然后按下 CTRL-ENTER。Arduino 应该会通过使用 LED 在二进制中显示该数字,并在串行监视器中显示出来,如 图 8-5 所示。LED 1 将是该数字的最低有效位,表示 1,而 LED 8 将是最高有效位,表示 255。

图 8-5:项目 #25 的示例输出
让我们看看这个是如何工作的:
// Project #25 - TPIC6B595 binary number display
❶ #define latch 8 // Latch RCK pin
#define clock 9 // Clock SRCK pin
#define data 10 // Data SERIN pin
void displayBinary(int displayNumber)
{
digitalWrite(latch, LOW);
shiftOut(data, clock, MSBFIRST, displayNumber);
digitalWrite(latch, HIGH);
}
void setup()
{
❷ Serial.begin(9600);
pinMode(latch, OUTPUT);
pinMode(clock, OUTPUT);
pinMode(data, OUTPUT);
}
voi`d` loop()
{
long number = 0;
long entry = 0;
❸ Serial.flush();
while (Serial.available() == 0) {}
❹ while (Serial.available() > 0)
{
number = number * 10;
entry = Serial.read() - '0';
number = number + entry;
delay(5);
}
displayBinary(number);
Serial.print("You entered: ");
Serial.print(number);
Serial.print(", which is ");
Serial.print(number, BIN);
Serial.println(" in binary.");
}
草图开始时定义了 Arduino 数字引脚,用于连接到移位寄存器的锁存器、时钟和数据引脚,分别是 ❶。自定义的 displayBinary() 函数接受一个整数,并将其发送到移位寄存器进行输出控制,使用的方法与之前提到的 74HC595 移位寄存器相同。为了以二进制形式将表示数字的位发送到移位寄存器并激活移位寄存器中的引脚,以控制将与要显示的二进制数字匹配的 LED,该函数使用 MSBFIRST(最高有效位优先)。
你可以通过发送到移位寄存器的 8 位数字来控制移位寄存器的输出开关,每一位与一个输出和状态(1 为高电平,0 为低电平)对应。你还可以将 MSBFIRST 改为 LSBFIRST,即“最低有效位优先”,这样可以看到数字在二进制中“反转”。
该草图初始化了串口监视器和数字输出引脚 ❷,然后刷新串口输入并等待用户在串口监视器中输入一个数字 ❸。接着它将串口监视器中输入的数字合并成最终要显示的数字 ❹。自定义的 displayBinary() 函数将该数字发送到移位寄存器和串口监视器。
你将在下一个项目中使用这种移位寄存器控制框架。
项目 #26:构建 PC 控制的继电器板
在这个项目中,你将构建一个带有 8 个单刀双掷(SPDT)继电器的继电器控制板,你可以通过 PC 或其他带有 Arduino 兼容 UART 的设备进行控制。未来,你可能会使用这个项目中介绍的技术来控制低压照明、电动门锁、开关扬声器等。
本项目中的每个继电器能够控制最高 30 V 的直流电压,电流为 2 A,前提是使用项目的 PCB。如果使用的是免焊面包板,则继电器的控制电流应仅限于 100 毫安左右。
尽管使用免焊面包板也能构建这个项目,但你需要将跳线焊接到继电器的引脚上,以便将继电器远程接回电路,如图 8-6 所示,因为继电器的引脚在面包板上不太稳定。我强烈建议你使用 PCB。

图 8-6:具有远程接线的继电器,用于面包板
本项目需要以下部件:
-
一块 Arduino Uno 或兼容板及 USB 电缆
-
各种跳线
-
一个 12 V 的电源或带有直流插头的电源适配器
-
一个 TPIC6B595 移位寄存器
-
一个 20 引脚 IC 插座
-
一个 0.1 µF 电容
-
八个 LED
-
八个 1 kΩ, 0.25 W, 1% 的电阻
-
八个 1N4001 电源二极管
-
八个 SRD-12VDC-SL-C SPDT 继电器
-
一个无焊面包板或项目 #26 的 PCB
如果你使用的是 PCB,还需要以下组件:
-
十个三路 5.08 mm 端子块
-
一个 20 引脚 IC 插座
-
一个 PCB 安装的 DC 插座
按照图 8-7 所示组装电路。

图 8-7: 项目 #26 的原理图
如果你使用的是 PCB,布局非常简单,如图 8-8 所示。

图 8-8: 项目 #26 的 PCB
从插入电阻开始;然后插入二极管、LED、IC 和 DC 插座、端子块,最后插入继电器。确保正确插入移位寄存器——PCB 上有标记的引脚 1。通过移位寄存器旁的两个端子块连接到 Arduino,如原理图和图 8-9 所示。Arduino 通过继电器板的 12V 电源供电,并返回 5V 给板子供电以驱动移位寄存器。

图 8-9: 项目 #26 的完成硬件
一旦硬件搭建完成,输入并上传项目 #26 的草图,控制继电器执行各种命令。使用串口监视器或终端软件输入 0 到 7,会依次打开继电器 0 到 7;输入 8 到 F 会关闭继电器 0 到 7;输入 G 会打开所有继电器;输入 H 会关闭所有继电器。输入 ? 可以检查哪些继电器已开或未开。返回的结果是一个二进制数,匹配继电器的顺序。如果输入了未识别的字符,Arduino 会返回一个有效命令列表。
例如,图 8-10 显示了命令 G、然后是 Q(这导致显示错误命令信息),然后是 H,再然后是 0、3、5、7 和 ? 的输出。

图 8-10:串行监视器中的操作示例
如果你和其他人共享继电器板和 Arduino,他们不需要运行 Arduino IDE 来进行控制;相反,他们可以使用任何支持 USB 串行的终端软件,如 PC、Mac 或其他计算机。例如,使用 Roger Meier 的 CoolTerm 应用程序进行相同的操作,该程序可以从 http://

图 8-11:使用 CoolTerm 应用程序控制继电器
控制 Arduino 仅期望从主机计算机(或其他 UART)接收单个字符,因此你可以在多种环境中为你的计算机编写软件来控制继电器。在你喜欢的环境中搜索 “plaintext serial over USB” 或类似的资源,以了解更多信息。
让我们来看一下这是如何工作的:
// Project #26 - PC-controlled relay board
#define latch 8 // Latch RCK pin
#define clock 9 // Clock SRCK pin
#define data 10 // Data SERIN pin
int relayStatus;
void showStatus()
{
Serial.print("Relay status 7 to 0 is: ");
Serial.println(relayStatus,BIN);
}
void waveHello()
{
int d = 250;
Serial.println("Hello!");
allOff();
for (int a = 0; a < 8; a++)
{
relayOn(a);
delay(d);
relayOff(a);
}
for (int a = 6; a >= 0; a--)
{
relayOn(a);
delay(d);
relayOff(a);
}
}
void relayOn(int a)
{
relayStatus = relayStatus|(1<<a);
sendStatus(relayStatus);
Serial.print("Relay "); Serial.print(a); Serial.println(" On");
}
void relayOff(int a)
{
relayStatus = relayStatus^(1<<a);
sendStatus(relayStatus);
Serial.print("Relay "); Serial.print(a); Serial.println(" Off");
}
void allOn()
{
relayStatus = 255;
sendStatus(relayStatus);
Serial.println("All relays turned on");
}
void allOff()
{
relayStatus = 0;
sendStatus(relayStatus);
Serial.println("All relays turned off");
}
void sendStatus(int a)
{
digitalWrite(latch, LOW);
shiftOut(data, clock, MSBFIRST, a);
digitalWrite(latch, HIGH);
}
void setup()
{
Serial.begin(9600);
pinMode(latch, OUTPUT);
pinMode(clock, OUTPUT);
pinMode(data, OUTPUT);
}
void loop()
{
char a = 0;
Serial.flush();
while (Serial.available() == 0) {}
while (Serial.available() > 0) ❶
{
a = Serial.read();
}
switch (a) ❷
{
case '0' : relayOn(0); break;
case '1' : relayOn(1); break;
case '2' : relayOn(2); break;
case '3' : relayOn(3); break;
case '4' : relayOn(4); break;
case '5' : relayOn(5); break;
case '6' : relayOn(6); break;
case '7' : relayOn(7); break;
case '8' : relayOff(0); break;
case '9' : relayOff(1); break;
case 'A' : relayOff(2); break;
case 'B' : relayOff(3); break;
case 'C' : relayOff(4); break;
case 'D' : relayOff(5); break;
case 'E' : relayOff(6); break;
case 'F' : relayOff(7); break;
case 'G' : allOn(); break;
case 'H' : allOff(); break;
case 'Z' : waveHello(); break; ❸
case '?' : showStatus(); break;
default : Serial.print("Incorrect command - 0 1 2 3 4 5 6 7 turns 0~7 on, "); ❹
Serial.println("8 9 A B C D E F turns 0~7 off, G - all on, H - all off");
}
}
该草图首先声明了整数 relayStatus,用于保存继电器的状态。可以将这个数字看作二进制数,其中最低有效位表示继电器 0:如果该位为 1,表示继电器开启;如果该位为 0,表示继电器关闭。自定义的 showStatus() 函数将 relayStatus 的二进制值发送回串行接口,接收方可以在他们的终端、串行监视器或其他软件中查看哪些继电器是开启的,哪些是关闭的。所有控制继电器的函数还会通过串行端口向用户发送反馈,描述已完成的操作。
relayOn(int a) 函数用于打开继电器,使用位运算符 OR(|)来激活指定的继电器,同时不影响其他继电器。该函数接收值 a,并与 relayStatus 变量执行按位 OR 操作,然后使用新的 relayStatus 值更新继电器。例如,如果继电器 0、1、2 和 3 已经打开,则当前 relayStatus 的二进制值为 B00001111。如果用户在串口监视器(或终端软件)中输入 5 来打开继电器 5,程序将会把第 5 位切换为 1,如下所示:
B00001111 | // Current value of relayStatus
B00100000 = // Perform OR with 1 << 5—received by the function in a
B00101111 // New value of relayStatus
sendStatus() 函数通过更新移位寄存器的输出来相应地改变继电器状态。
relayOff(int a) 函数用于关闭继电器,使用位运算符 XOR(^)来停用指定的继电器,同时不影响其他继电器。例如,如果继电器 0、1、2、3 和 5 已经开启,当前 relayStatus 的二进制值为 B00101111。如果在串口监视器或终端软件中输入 A 来关闭继电器 3,程序应该将第 3 位切换为 0,如下所示:
B00101111 ^ // Current value of relayStatus
B00001000 = // Perform XOR with 1 << 3
B00100111 // New value of relayStatus
同样,sendStatus() 函数通过更新移位寄存器的输出相应地改变继电器状态。两个额外的函数,allOn() 和 allOff(),分别通过向移位寄存器发送 255(即二进制 B11111111)和 0(即二进制 B0000000)来打开或关闭所有继电器。
一般操作很简单。Arduino 等待串口线接收到一个字符 ❶。接收到字符后,它会与一个命令进行匹配 ❷。用户可以通过按下 Z 来激活简单的 waveHello() 函数 ❸,从而依次打开和关闭继电器,用于测试和娱乐。最后,如果接收到的字符不是命令,程序会向串口发送快速参考 ❹,让用户了解可用的命令集。
为了增加挑战,你可以修改草图,使得继电器状态在更改时保存到内部 EEPROM,并且系统在重置后从 EEPROM 数据中设置继电器状态。
现在你已经知道如何使用单个 TPIC6B595 控制 8 个高电流设备,接下来我将向你展示如何同时使用两个或更多的 TPIC6B595。
使用多个 TPIC6B595
你可以轻松地同时使用两个或更多的 TPIC6B595 移位寄存器,以与使用 74HC595 时相同的方式控制 16 个或更多的设备,但具有处理更高电流的能力。首先将每个 TPIC6B595 的时钟线连接在一起,将它们的锁存线连接在一起,然后将第一个移位寄存器的串行输出连接到第二个移位寄存器的串行输入,按需要重复此操作。例如,图 8-12 中的原理图展示了由项目#25 控制的双倍数量的 LED。

图 8-12:使用移位寄存器控制 16 个 LED 的原理图
接下来,在锁存线为低电平时,发送 2 字节的数据,而不是 1 字节。你需要首先发送链中最后一个移位寄存器的字节。例如,要向图 8-12 中的移位寄存器发送 2 字节数据,你将使用以下函数:
void sendData(int a, int b)
{
digitalWrite(latch, LOW);
shiftOut(data, clock, MSBFIRST, b); // For TPIC #2
shiftOut(data, clock, MSBFIRST, a); // For TPIC #1
digitalWrite(latch, HIGH);
}
要添加一个或更多的移位寄存器,只需添加更多的参数和每个额外寄存器的shiftOut()函数。你将在接下来的项目中使用多个 TPIC6B595,并结合使用一种新的 LED 类型。
食人鱼风格 LED
市场上有着种类繁多的 LED,从微小的表面贴装 LED 到足够大的 LED,能够作为汽车前大灯的一部分。介于这两者之间的一个例子是紧凑且非常亮的食人鱼风格 LED;它的通孔封装使得使用起来非常方便。图 8-13 展示了一对食人鱼 LED。

图 8-13:两颗食人鱼 LED
每个 LED 有四根引脚,其中两个是阳极,两个是阴极。两个阳极引脚相互连接,两个阴极引脚也相互连接。阳极侧的底部左角被切去,并且与阴极侧相比,阳极侧有更大的金属表面。图 8-14 展示了食人鱼 LED 的原理图符号。

图 8-14:食人鱼风格 LED 的原理图符号
当单独使用时,这些 LED 在大约 2 到 2.2 伏直流电压下,以 20 毫安的电流安全运行。接下来的项目中,你将以每组四个的方式使用它们,并将它们串联(而不是并联)。在这种配置中,每组四颗 LED 将需要 9 伏直流电源和 47 欧姆的电阻,以维持所需的电流和高亮度。
要计算与串联 LED 一起使用的所需电阻,使用公式 R = (V[s] − V[f]) / I[f],其中 V[s]是电源电压,V[f]是 LED 的正向电压(推荐的工作电压),I[f]是 LED 的推荐工作电流。对于以下项目,你将使用 8V 的 LED 正向电压和 20mA 的工作电流,电源为 9V。根据公式,R = (9V − 8V) / 0.02A,结果为 50Ω。由于没有 50Ω电阻,47Ω是最接近的选择。
项目#27:创建一个巨型七段 LED 显示器
多个 TPIC6B595 芯片可以很好地驱动大量 LED。在这个项目中,你将构建一个或多个七段 LED 显示器,创建可以用于各种用途的大型数字显示。例如,你可以用它们来显示通过 Arduino 项目生成的各种数据,如温度、事件计数或时间。
要构建一个单数字显示器,你需要以下部件。对于更大的显示器,将除了 Arduino 以外的所有部件数量乘以你希望创建的数字数量。如果你想按照示例完全操作,那么就乘以四,因为这个项目展示了如何同时使用四个数字。对于这个项目,请使用 PCB 而不是面包板。
-
一个 Arduino Uno 或兼容板以及 USB 电缆
-
各种跳线
-
一个 9V 电源或带 DC 插头的电源适配器
-
一个 TPIC6B595 移位寄存器
-
一个 20 引脚 IC 插座
-
一个 0.1µF 电容
-
三十二个 5 毫米 Piranha 风格 LED
-
八个 47Ω、0.25W、1%的电阻
-
四个 3 接触 5.08 毫米接线端子
-
一个 PCB 安装式 DC 插座
-
项目#27 的 PCB
图 8-15 显示了项目的原理图。

图 8-15:项目#27 的原理图
如你所见,TPIC6B595 具有“低端”输出。当输出被激活时,电流从电源开始,经过待控制的元件,然后通过移位寄存器的 DRAIN 引脚,最后通过 GND 输出。
单位显示
图 8-16 显示了简单的 PCB 布局。

图 8-16:一个未装配的单数字 PCB
总是从连接最薄的元件开始:电阻器、IC 插座、LED,然后最后连接 DC 插座和接线端子。不要一开始就把所有的 LED 插好,再翻转 PCB,因为一些 LED 可能会松动或掉出来。相反,应该逐个焊接。所有的 LED 都应当按照阳极在左,阴极在右的方式摆放(PCB 的左侧是带有 DC 插座的那一边)。完成后,您的电路板应该与图 8-17 中的示例相似。

图 8-17:一个数字的完整 PCB
在设置好硬件后,上传下面的项目 #27a 草图,它展示了一个数字(你可以选择在“四色显示”部分中构建另外三个数字)。上传草图后,你需要将 Arduino 和电源连接到 PCB。将 PCB 朝上,DC 插座在左侧,按照表 8-1 中描述的方式进行连接。
表 8-1: PCB 与 Arduino 连接
| PCB 左侧 | Arduino |
|---|---|
| 时钟 | D9 |
| 锁存器 | D8 |
| 地 | GND |
| 9V | Vin |
| 5V | 5V |
| 串行输入 | D10 |
最后,将 9 V DC 电源连接到 PCB 上的 DC 插座。Arduino 通过 Vin 引脚从 9 V 电源供电,并通过反馈的 5 V 为显示板供电,驱动移位寄存器。显示屏应从零数到九,然后每个数字旁边都有一个小数点,如图 8-18 所示,最后重复此过程。

图 8-18:示例显示板输出
由于 LED 的亮度非常高,我在拍摄图 8-18 之前将电源电压设置为 7 V DC,以获得更清晰的照片。当工作在其指定的 9 V DC 电压下时,显示屏应当会更亮。
每个数字的每个段由 28 个 Piranha LED 组成,再加上 4 个用于小数点,由 TPIC6B595 的输出驱动。因此,可以将每个数字视为由七个 LED 组成,再加上一个小数点的 LED。这些 LED 通过 TPIC6B595 的输出以与第 26 项目中继板相同的方式进行控制。
让我们看看它是如何工作的:
// Project #27a - Single giant seven-segment LED displays
❶ #define latch 8 // Latch RCK pin
#define clock 9 // Clock SRCK pin
#define data 10 // Data SERIN pin
int digits[] = {B00111111, // 0
❷ B00000110, // 1
B01011011, // 2
B01001111, // 3
B01100110, // 4
B01101101, // 5
B01111101, // 6
B00000111, // 7
B01111111, // 8
B01100111}; // 9
void sendNumber(int a, boolean point)
{
if (point == false)
{
digitalWrite(latch, LOW);
shiftOut(data, clock, MSBFIRST, digits[a]);
digitalWrite(latch, HIGH);
} else if (point == true)
{
digitalWrite(latch, LOW);
❸ shiftOut(data, clock, MSBFIRST, digits[a]|B10000000);
digitalWrite(latch, HIGH);
}
}
void setup()
{
pinMode(latch, OUTPUT);
pinMode(clock, OUTPUT);
pinMode(data, OUTPUT);
}
void loop()
{
❹ for (int a = 0; a < 10; a++)
{
sendNumber(a,false);
delay(1000);
}
❺ for (int a = 0; a < 10; a++)
{
sendNumber(a,true);
delay(1000);
}
}
示例首先定义了用于连接到移位寄存器的锁存、时钟和数据引脚的 Arduino 数字引脚 ❶。对于 8 字节的数组 ❷,每个字节代表用于控制显示上段的八个输出。例如,数字 1 在二进制中表示为 B00000110,因为你想要点亮显示器的第二和第三段来显示数字 1。
自定义的 sendNumber() 函数将所需的数据发送到移位寄存器,以显示每个数字,然后设置相应的输出。此函数接受要显示的数字,并接受 true 或 false 作为第二个参数,用于打开或关闭小数点。如果需要小数点,示例使用 OR 函数 | ❸(如第二章的第 7 项目中所示)来开启移位寄存器的最高位(位 7),该位控制 DRAIN7。
void setup() 函数设置所需的数字输出引脚。最后,示例演示了通过从零计数到九并反向计数 ❹,然后再次显示带有小数点的数字 ❺。
现在你有了一个大型且引人注目的数字 LED 显示屏,可以从很远的地方看到。如果 LED 的亮度太强,你可以增加电阻的阻值,可能设为 180 或 270 Ω。请确保使用 0.25 W(1/4 W)类型的电阻。
四位数字显示器
你可以为更大的数字项目构建和使用多个显示板。在这个示例中,你将使用四个板来显示最多四位的数字。你可以用它来显示数据或作为一个大型、明亮的时钟。
按照上一节的方式构建第二、第三和第四个板,唯一的区别是:这些新板不需要直流插座,因为第一个显示板将充当电源。一旦附加板组装完成,通过将每侧的接线端子连接起来来将它们连接在一起。显示板背面上的匹配标签,如图 8-19 所示,将帮助你完成这一操作。

图 8-19:两个显示板的背面
接下来,输入并上传项目#27b 演示程序。一旦上传完成,将 Arduino 连接到最左边的显示板,并连接 9V 电源。显示板应该显示随机的四位数字,并且小数点位置随机,正如图 8-20 所示。

图 8-20:四个显示板在 7V 直流电压下工作
让我们看看这是如何工作的:
// Project #27b - Four giant seven-segment LED displays
#define latch 8 // Latch RCK pin ❶
#define clock 9 // Clock SRCK pin
#define data 10 // Data SERIN pin
int digits[] = {B00111111, // 0 ❷
B00000110, // 1
B01011011, // 2
B01001111, // 3
B01100110, // 4
B01101101, // 5
B01111101, // 6
B00000111, // 7
B01111111, // 8
B01100111}; // 9
void sendNumbers(int numbers[], int dp)
{
digitalWrite(latch, LOW);
for (int i = 0; i < 4; i++)
{
int dig_idx = numbers[i]; ❸
if (dp == i) {
// Display the digit
shiftOut(data, clock, MSBFIRST, digits[dig_idx] | B10000000); ❹
} else {
shiftOut(data, clock, MSBFIRST, digits[dig_idx]);
}
}
digitalWrite(latch, HIGH);
}
void setup()
{
randomSeed(analogRead(0)); ❺
pinMode(latch, OUTPUT);
pinMode(clock, OUTPUT);
pinMode(data, OUTPUT);
}
void loop()
{
int numbers[4] = {random(0, 10), random(0, 10), random(0, 10), random(0, 10)}; ❻
sendNumbers(numbers, random(0, 3)); ❼
delay(1000);
}
四个显示板的程序与项目#27a 的程序有一些不同,以便更容易使用多个显示器。它再次定义了连接到移位寄存器的锁存器、时钟和数据引脚的 Arduino 数字引脚❶,并定义了一个包含 8 个字节的数组❷,这些字节用于控制显示板上各个段的输出。
void sendNumbers()函数接受两个参数:一个包含四个数字的数组(每个显示板一个数字)和一个整数,表示在哪些显示板上显示小数点。程序会检查这个值❸。如果参数是 1,则需要的小数点显示位将包含在发送到移位寄存器的字节中❹。
在你显示随机数字时,生成器会使用模拟输入数据❺进行初始化,然后数字显示数组也会被填充为随机数字❻,并发送到显示板❼。最后,程序包含一个延迟,之后会显示更多的随机数字。
你可以使用这些显示板显示各种数字数据。你还可以通过向数字数组中添加更多元素来添加自己的字符。例如,表示温度的度符号如下所示:
B01100011 // Binary representation of °
对于一个挑战,将四个显示板与 Arduino 和实时时钟模块配对,制作一个大型明亮的时钟。你也可以加入一个温度计。希望你能像我设计这些显示板时那样享受使用它们的乐趣。
继续前进
在本章中,你学习了如何使用 TPIC6B595 移位寄存器控制电流和电压,这是一种比流行的 74HC595 更强大的替代方案。你现在可以控制比 Arduino 的数字 I/O 引脚安全处理的电流更大的设备。你还学会了如何使用明亮的 Piranha 风格 LED 作为出色的指示灯。
在下一章中,你将使用 MP3 播放器模块来创建数字音乐播放器和各种用途的声音板。
第九章:9 构建数字音乐播放器和音效板

数字音频播放器在 2000 年代流行起来,但随着智能手机开始主导市场,它们逐渐消失。不过,离散的 MP3 播放器仍然有很多用途,比如玩具中的音频播放、公告系统或简单的音频播放器。由于裸 MP3 播放器模块的成本较低,你可以轻松构建有趣的 MP3 播放器项目。
在本章中,你将学习:
-
构建一个简单的 MP3 播放器,仅用于听音乐,没有智能手机带来的干扰。
-
制作一个 MP3 音频音效板,用于直接播放控制。
你可以使用本章中构建的设备播放任何你喜欢的音频,并可以将它们修改为前面提到的应用,应用到你未来的项目中。
YX6300 MP3 模块
对于本章中的 MP3 项目,你将使用 YX6300 型紧凑模块,如图 9-1 所示的 PMD Way 部件 725600。

图 9-1:MP3 播放器模块(正面)
模块的背面是一个 microSD 闪存卡插槽,可以使用最大 32GB 的卡,如图 9-2 所示。

图 9-2:MP3 播放器模块背面
在购买用于 MP3 模块的闪存卡时,请务必购买一个适配器,这样你就可以将其插入 PC 上的普通 SD 内存卡插槽进行文件传输。
我选择基于 YX6300 型 MP3 模块来编写本章,原因有几点。首先,你可以通过模块的 3.5 毫米立体声音频插孔连接耳机、放大器或扬声器,无需额外的电路。其次,命令和信息通过串行数据(UART)使用 Arduino 软件串口发送,因此我们只需要将四个引脚连接到 Arduino 电路。最后,该单元价格低廉,且广泛可用。
在你开始第一个项目之前,先进行一个测试,使用模块播放一些音频。首先,准备三到四个你选择的 MP3 音频文件,并将它们复制到 microSD 卡中。接下来,按照图 9-3 中的原理图将 MP3 模块连接到 Arduino。

图 9-3:MP3 播放器测试电路
小心地将内存卡插入模块,标签面朝上,如图 9-4 所示。卡会滑入并碰到一个弹簧锁—再稍微推一下,直到锁扣住。(要取出卡片,请轻轻推卡片,卡片会弹回来一些,然后弹出。)

图 9-4:将内存卡插入 MP3 播放器模块
最后,将耳机或放大音响插入 MP3 模块,然后输入并上传列表 9-1 中的草图。几秒钟后,模块应该会播放内存卡中第一个音频文件的前 10 秒,然后重复跳到下一个文件并播放 10 秒。模块后面的 LED 应该在内存卡插入时保持亮起,在播放音频时闪烁。如果您的设备无法正常工作,请检查接线,包括 TX/RX 是否连接回 Arduino。
列表 9-1 显示了如何实现这一过程。
❶ #define HEADER_START 0x7E
#define HEADER_VERSION 0xFF
#define TAIL_END 0xEF
#define CMD_PLAY_NEXT 0x01
#define CMD_PLAY_PREV 0x02
#define CMD_PLAY_IDX 0x03
int8_t commands[8] = {0, 0, 0, 0, 0, 0, 0, 0};
// Holds command set to send to MP3
#include <SoftwareSerial.h>
SoftwareSerial MP3(2, 3); // Module TX to D2; module RX to D3
void controlMP3(int8_t command, int16_t data)
{
commands[0] = HEADER_START; // Header byte 0 (start)
commands[1] = HEADER_VERSION; // Header byte 1 (version)
commands[2] = 0x06; // Header byte 2 (length cmd + data)
commands[3] = command; // Command byte
commands[4] = 0x00; // Feedback (0x00 = no, 0x01 = yes)
commands[5] = (int8_t)(data >> 8); // Data byte 0
commands[6] = (int8_t)(data); // Data byte 1
commands[7] = 0xEF; // End byte
❷ for (int i = 0; i < 8; i++)
{
MP3.write(commands[i]);
}
}
void setup()
{
MP3.begin(9600);
❸ controlMP3(0x09, 0x02); // Select flash card for operation
delay(500);
❹ controlMP3(0x0D, 0); // Resume playback, no feedback
delay(500);
}
void loop()
{
❺ controlMP3(0x01, 0); // Next track, zero data
❻ delay(10000);
}
列表 9-1:测试串行 MP3 播放器
该草图通过与 Arduino 的串行 UART 连接,将命令和数据发送到 MP3 模块。每个数据包包含命令和相关数据,共有 8 个十六进制字节的数据。
为了节省时间,草图将一些有用的命令值定义为变量❶。这些命令被组装到commands[]数组中,并通过软件串口发送出去❷。这使用表 9-1 中列出的第一个命令来初始化 MP3 播放器,并选择 microSD 卡插槽作为音频文件的来源❸,然后开始播放音频文件❹。该草图会播放 microSD 卡中找到的下一个音频文件❺,持续播放 10 秒钟❻,然后过程重复。
表 9-1 描述了您可以在播放器上使用的命令。
表 9-1: MP3 播放器命令
| 命令集 | 目的 |
|---|---|
| 7E FF 06 09 00 00 02 EF | 初始化 MP3 播放器,使用 microSD 卡 |
| 7E FF 06 0D 00 00 00 EF | 恢复/开始播放 |
| 7E FF 06 0E 00 00 00 EF | 暂停播放 |
| 7E FF 06 16 00 00 00 EF | 停止播放,重置为第一首曲目 |
| 7E FF 06 02 00 00 00 EF | 播放上一首曲目 |
| 7E FF 06 01 00 00 00 EF | 播放下一首曲目 |
| 7E FF 06 05 00 00 00 EF | 调低音量 |
| 7E FF 06 04 00 00 00 EF | 调高音量 |
在每个命令组中的两个粗体字节中,第一个是命令字节,第二个是数据字节。这些是草图中 controlMP3() 函数用来指示 MP3 播放器的两个参数。对于基本播放使用,你无需发送数据字节。
在下一个项目中,你将通过构建一个具有控制功能的 MP3 播放器来测试这些命令。
项目 #28:构建一个简单的 MP3 播放器
在这个项目中,你将创建一个带有典型播放、暂停、音量和曲目前进/后退控制的 MP3 播放器。你将需要以下零件:
-
一块 Arduino Uno 或兼容板和 USB 电缆
-
一个带有 MP3 音频文件的 microSD 卡的 MP3 播放器模块
-
一个无焊接面包板
-
各种跳线
-
耳机或音频放大器和扬声器的连接线
-
来自第一章的模拟 6 或 12 按钮键盘电路
为了构建这个项目,你可以重用在项目 #3 中构建的 12 按钮键盘,只使用其中的 6 个按钮,或者使用项目 #2 中的键盘。如果你使用 12 按钮键盘,别忘了更新模拟键盘库中的模拟值。按照 图 9-5 所示组装电路。

图 9-5:项目 #28 的原理图
输入并上传项目 #28 草图。几秒钟后,按下按钮 7;你在存储卡上安排的 MP3 文件中的音乐应该开始播放。测试其他按钮以确保它们的功能正常。如果某些或所有按钮不起作用,请检查按钮的模拟值是否与实际的按键匹配,按照项目 #2 中描述的过程检查,并按照项目 #3 中的说明更新模拟键盘库中的值。
让我们来看一下这个是如何工作的:
// Project #28 - Simple MP3 player
❶ #define HEADER_START 0x7E
#define HEADER_VERSION 0xFF
#define TAIL_END 0xEF
#define CMD_PLAY_NEXT 0x01
#define CMD_PLAY_PREV 0x02
#define CMD_PLAY_IDX 0x03
❷ int8_t commands[8] = {0, 0, 0, 0, 0, 0, 0, 0};
// Holds command set to send to MP3
#include <SoftwareSerial.h>
SoftwareSerial MP3(2, 3); // module TX to D2; module RX to D3
#include <analogkeypad.h>
analogkeypad keypad;
void controlMP3(int8_t command, int16_t data)
{
commands[0] = HEADER_START;
commands[1] = HEADER_VERSION;
commands[2] = 0x06;
❸ commands[3] = command;8
commands[4] = 0x00;
❹ commands[5] = (int8_t)(data >> 8);
commands[6] = (int8_t)(data);
commands[7] = TAIL_END;
for (int i = 0; i < 8; i++)
{
❺ MP3.write(commands[i]);
}
delay(100);
}
void setup()
{
❻ MP3.begin(9600);
controlMP3(0x09, 0x02); // Select uSD card for operation
delay(500);
}
void loop()
{
switch (keypad.readKeypad()) // Read button status
{
case 7 : controlMP3(0x0D, 0); break; // Play
case 8 : controlMP3(0x0E, 0); break; // Pause
case 9 : controlMP3(0x02, 0); break; // Previous track
case 10 : controlMP3(0x01, 0); break; // Next track
case 11 : controlMP3(0x05, 0); break; // Volume down
case 12 : controlMP3(0x04, 0); break; // Volume up
}
delay(250);
}
该草图首先声明了用于存储控制 MP3 模块命令的数组❶,然后初始化了用于 Arduino 与 MP3 播放器控制的软件串口,以及模拟按钮键盘的库❷。自定义的 void controlMP3() 函数接收控制 MP3 播放器所需的命令和数据参数,将它们插入数组❸ ❹,并通过软件串口发送命令给 MP3 播放器❺。然后,草图启动软件串口并发送初始化命令,指示 MP3 播放器使用 microSD 卡❻。
一旦操作开始,草图将循环等待来自模拟按钮电路的响应,在 void loop() 中,每个按钮都被分配一个 controlMP3() 函数,并与来自 表 9-1 的适当命令数据一起使用。
你现在拥有一个可操作的 MP3 音频播放器,可以嵌入、修改或将其做成便携式设备,作为你自己的无干扰音频源(与智能手机不同,智能手机的游戏、信息和其他内容可能会分散注意力)。连接到 Arduino 的 USB 电缆为该项目供电,但你也可以使用移动电源或交流转 USB 适配器,并将所有组件放置在你自己设计的外壳内。如果你要向朋友或家人演示这个项目,你还可以绘制一张小地图,说明哪些按钮对应哪些功能,正如图 9-6 所示。

图 9-6:项目 #28 的示例,配有 12 键键盘和用户参考播放控制
在下一个项目中,你将使用刚刚制作的 MP3 播放器模块来创建一个声音板。
项目 #29:构建一个 MP3 播放器声音板
在本项目中,你将使用项目 #28 中的硬件,结合 12 键板,创建一个 声音板,这是一个可以通过按下按钮播放预设音频的设备。声音板常用于广播、玩具或博物馆展览中,为视障人士提供信息。本项目使用按钮来启动音频播放,但你可以将其作为框架,用于在 Arduino 检测到其他动作时播放音频,比如代码中的触发器或传感器、开关等的输出。
在上传项目 #29 的草图之前,按照图 9-7 所示的编号规范,重命名 microSD 卡上的 MP3 音频文件,以便 MP3 播放器可以在按下按钮时搜索并播放准确的音频文件。例如,当播放器搜索音频文件 1 时,MP3 会播放 001001.mp3 文件。因此,你应该将文件 4 命名为 004004.mp3,文件 12 命名为 012012.mp3,依此类推。将这些文件存储在名为 01 的文件夹中。

图 9-7:MP3 文件在 01 文件夹中的示例,文件名结构适用于声音板使用
这些 MP3 文件可以包含你喜欢的任何音频。为了好玩,你可以尝试从网站如 https://
一旦你收集并整理好了 MP3 文件,并将 microSD 卡插入播放器,输入并上传以下草图:
// Project #29 - MP3 player sound board
#define HEADER_START 0x7E
#define HEADER_VERSION 0xFF
#define TAIL_END 0xEF
#define CMD_PLAY_NEXT 0x01
#define CMD_PLAY_PREV 0x02
#define CMD_PLAY_IDX 0x03
int8_t commands[8] = {0, 0, 0, 0, 0, 0, 0, 0};
// Holds command set to send to MP3
#include <SoftwareSerial.h>
SoftwareSerial MP3(2, 3); // Module TX to D2; module RX to D3
#include <analogkeypad.h>
analogkeypad keypad;
void controlMP3(int8_t command, int16_t data)
{
commands[0] = 0x7E;
commands[1] = 0xFF;
commands[2] = 0x06;
commands[3] = command;
commands[4] = 0x00;
commands[5] = 0x01;
commands[6] = (int8_t)(data);
commands[7] = 0xEF;
for (int i = 0; i < 8; i++)
{
MP3.write(commands[i]);
}
delay(100);
}
void setup()
{
MP3.begin(9600);
controlMP3(0x09, 0x02); // Select uSD card for operation
delay(500);
}
void loop()
{
switch (keypad.readKeypad()) // Read button status
{
❶ case 1 : controlMP3(0x0F, 1); break; // Play 01/001001.mp3
case 2 : controlMP3(0x0F, 2); break; // Play 01/002002.mp3
case 3 : controlMP3(0x0F, 3); break; // Play 01/003003.mp3
case 4 : controlMP3(0x0F, 4); break; // And so on…
case 5 : controlMP3(0x0F, 5); break;
case 6 : controlMP3(0x0F, 6); break;
case 7 : controlMP3(0x0F, 7); break;
case 8 : controlMP3(0x0F, 8); break;
case 9 : controlMP3(0x0F, 9); break;
case 10 : controlMP3(0x0F, 10); break;
case 11 : controlMP3(0x0F, 11); break;
case 12 : controlMP3(0x0F, 12); break;
}
delay(250);
}
这个草图的工作方式与前一个项目相同,唯一不同的是,当你按下按钮时,会发送不同类型的命令和数据到播放器。这些新命令播放与数据值匹配的轨道编号。例如,要播放文件001001.mp3,草图会发送
7E FF 06 0F 00 01 01 EF
使用controlMP3(0x0F, 1) ❶。直接播放轨道的命令是0x0F,数据值(1)是轨道文件名的编号(001001.mp3)。其他按钮遵循相同的命令格式,数据值与其他 MP3 文件名匹配。
Moving On
在本章中,你学习了如何构建自己的 MP3 播放器,并用它来听音乐和播放音效。还有许多其他有趣的方式可以使用你的 MP3 模块,例如为项目创建可听见的输出,而不是使用 LED 或显示器。作为最后的挑战,你可能会编写自己的 MP3 播放器 Arduino 库。
在下一章,你将学习如何使用一种新的温度传感器、OLED 显示屏以及多个 I²C 总线与 Arduino 配合使用。
第十章:10 使用具有相同地址的多个 I2C 设备

在您的 Arduino 旅程中,您可能遇到过通过 I²C 总线接口的设备,I²C 是一个简单的双线数据总线,在这个总线上 Arduino 被视为主设备,而总线上的每个附加设备或 IC 则是从设备。每个从设备都有自己的地址,允许 Arduino 与其通信。然而,如果两个或更多设备具有相同的总线地址,您将无法将它们一起使用在同一总线上。
本章将向您展示如何通过 TCA9548A I²C 开关 IC 使用两个或更多具有相同总线地址的设备。您将学习如何:
-
构建一个 I²C 地址扫描仪,以确定设备的总线地址
-
使用紧凑型图形有机发光二极管(OLED)显示屏
-
使用 BMP180 温度和气压传感器
-
将 BMP180 和 OLED 设备结合使用
为了让您能同时使用多个 I²C 设备,我将首先介绍一个简单的工具,它可以报告连接到 Arduino I²C 总线的设备的总线地址:I²C 总线扫描仪。
注意
如果您不熟悉 I ²**C 总线,可以查看 《Arduino Workshop》第十九章,第二版。
项目 #30:使用 I²C 总线扫描仪
在本项目中,您将学习如何使用 I²C 总线扫描仪。这个工具帮助您收集无法找到文档的零件或设备的信息,例如零件的总线地址。它对于在将设备连接到总线后进行快速的 I²C 总线测试也非常理想。
您需要以下零件:
-
一块 Arduino Uno 或兼容板及 USB 数据线
-
一个无焊接面包板或 I²C 设备的安装架
-
各种跳线
硬件连接将取决于待测 I²C 设备。例如,我将使用 BMP180 传感器来测量温度、大气压力和海拔,如图 10-1 所示。

图 10-1:BMP180 传感器板
您还将在本章及本书的其他部分使用 BMP180,但在本项目中,您可以使用任何兼容 5V 的 I²C 设备。如果您的设备不是模块,请不要忘记像平时一样使用上拉电阻。
本项目的草图已随 Arduino IDE 提供。通过选择 文件
示例
Wire
i2c_scanner,打开并上传随 Wire 库示例一起提供的 i2c_scanner 草图。
该示意图应该查询总线并在串口监视器中报告找到的地址(或地址),如图 10-2 所示。

图 10-2:I²C 扫描器示意图的结果2C 扫描器示意图
如果总线上有两个或更多设备,扫描器将报告两个地址,但你无法知道哪个设备属于哪个地址。例如,图 10-3 显示了连接到总线的两个设备的总线地址。当你同时使用两个或更多设备时,即使电源开启,移除一个或多个设备也是可以的,因为 I²C 总线支持“热插拔”。也就是说,它设计成允许设备在运行时连接或移除。

图 10-3:带有两个设备的 I²C 扫描器示意图的结果
该示意图非常简单。让我们来看它是如何工作的:
// Project #30 - I2C bus address scanner
void loop()
{
int nDevices = 0;
Serial.println("Scanning…");
❶ for (byte address = 1; address < 127; ++address)
{
Wire.beginTransmission(address);
byte error = Wire.endTransmission();
❷ if (error == 0)
{
Serial.print("I2C device found at address 0x");
if (address < 16)
{
Serial.print("0");
}
Serial.print(address, HEX);
Serial.println(" !");
++nDevices;
}
❸ else if (error == 4)
{
Serial.print("Unknown error at address 0x");
if (address < 16)
{
Serial.print("0");
}
Serial.println(address, HEX);
}
}
if (nDevices == 0)
{
❹ Serial.println("No I2C devices found\n");
} else
{
Serial.println("done\n");
}
delay(5000); // Wait 5 seconds for next scan
}
该示意图开始并完成对每个可能的 I²C 地址位置(0 到 127)进行 I²C 连接 ❶。如果在测试的地址上存在设备,示意图会返回错误代码 0 ❷;如果发生错误,它会返回 4 ❸。如果没有找到任何设备,它会打印信息 未找到 I2C 设备 ❹。
现在你已经了解了如何将一个或多个 I²C 设备连接到总线,让我们开始使用 TCA9548A。
TCA9548A 扩展板
TCA9548A,如图 10-4 所示,允许你将 Arduino 的单个 I²C 总线连接到从 TCA9548A 发出的八个独立总线中的任何一个,每个总线的工作方式与 Arduino 原始的 I²C 总线相同。

图 10-4:TCA9548A 扩展板
TCA9548A 还负责电压转换,使你可以使用与主机 Arduino 工作在不同电压的设备——这是一个方便的优点,因为越来越多的设备只在 3.3 V 或 1.8 V 下工作。不幸的是,TCA9548A 仅以表面贴装形式提供,因此无法与无焊接面包板或原型板一起使用。相反,你需要使用模块。
TCA9548A 插件板应包括一对尚未焊接到板上的内联插针。现在我们来焊接它们。为了使插针正确对齐,你可以将它们插入无焊接面包板,如 图 10-5 所示。

图 10-5:未焊接内联插针的 TCA9548A 插件板
在你焊接好插针后,板子就可以在无焊接面包板和其他硬件解决方案中使用,如 图 10-6 所示。

图 10-6:带有内联插针并已焊接到 PCB 焊盘的 TCA9548A 插件板
板上的引脚排列与 图 10-7 中显示的原理图相匹配,原理图中还包括了你应该连接到 Arduino Uno 或兼容板的连接。

图 10-7:TCA9548A 原理图
TCA9548A 需要在 I²C 总线与正电源之间连接上拉电阻;这些电阻已经包含在插件板上。
原理图中标记为 A0 到 A2 的引脚用于定义 TCA9548A 的总线地址。正如你所看到的,原理图将它们都设置为 GND。如果你运行端口扫描器,它将返回地址 0x70。如果需要,你可以通过将 A0 到 A2 引脚连接到 GND 或 5V 的组合,来更改地址,如 表 10-1 所列。
表 10-1: TCA9548A 地址配置
| A0 | A1 | A2 | 地址 |
|---|---|---|---|
| GND | GND | GND | 0x70 |
| GND | GND | 5V | 0x71 |
| GND | 5V | GND | 0x72 |
| GND | 5V | 5V | 0x73 |
| 5V | GND | GND | 0x74 |
| 5V | GND | 5V | 0x75 |
| 5V | 5V | GND | 0x76 |
| 5V | 5V | 5V | 0x77 |
原理图中的八个 I²C 总线对标记为 SC0/SD0 到 SC7/SD7(分别是时钟和数据)。TCA9548A 将它们每个都视为物理上独立的 I²C 总线。
在您的草图中使用 TCA9548A 只需要在正常使用 I²C 设备之前进行一个额外的步骤:指示 TCA9548A 使用它控制的八个总线之一。为此,您需要向 TCA9548A 的 I²C 总线寄存器发送一个字节的数据,以确定您想使用的总线是哪一条。该字节的每一位用于打开或关闭总线,其中最高有效位(MSB)对应总线 7,最低有效位(LSB)对应总线 0。例如,发送 B00000001(二进制)或 0(十进制)将激活总线 0,而发送 B00010000 将激活总线 5。然后,TCA9548A 会将所有数据在所选总线上进出 Arduino;TCA9548A 对 Arduino 是透明的。
仅在您想要更改总线时发送总线选择数据。为了简化此过程,您可以使用以下函数来选择所需的总线:
void TCA9548A(uint8_t bus)
{
Wire.beginTransmission(0x70); // TCA9548A address is 0x70
Wire.write(1 << bus); // Send byte to select bus
Wire.endTransmission();
}
该函数接受一个总线编号,并在 TCA9548A 的总线寄存器中放置一个 1,以匹配我们的需求。在需要访问特定 I²C 总线上的设备之前,插入此函数。例如,您可以使用此函数访问总线 0 上的设备:
TCA9548A(0);
这是访问总线 7 上设备的函数:
TCA9548A(7);
现在您知道如何选择所需的总线,我将介绍一个有趣的显示器,您将在项目中使用它。
图形 OLED 显示器
您可能熟悉经典的 16 × 2 字符 LCD 显示器,因为它们非常流行且价格便宜。然而,它们也有点笨重且过时。OLED 显示器是一种更时尚、更现代的选择,能够显示文本和图形,且尺寸可调。
本章中的项目将使用一款紧凑型 128 × 32 像素 OLED 显示器,显示尺寸对角线为 0.91 英寸,例如 PMD Way 的 35998841A,见 图 10-8。

图 10-8:一款带内联连接引脚的紧凑型 OLED 显示器
使用无焊面包板的内联连接头随 PMD Way 示例板一起提供,通常其他供应商的产品也包括这个,但请与供应商确认。你可以通过将引脚和 OLED 安装到面包板上,轻松地将它们焊接到面包板上,正如 图 10-9 所示。用其他内联引脚支撑 OLED 的另一端,保持显示器水平,正如 图 10-9 中所示,可以帮助确保引脚保持笔直。

图 10-9:一款紧凑型 OLED 显示器放置在内联连接引脚上,准备焊接
一旦准备好使用 OLED,按 图 10-10 所示,通过拉动蓝色标签移除塑料屏幕保护膜。

图 10-10:即插即用 OLED
要使用 OLED,你需要安装 Arduino 库。在库管理器中搜索 u8g2,然后点击库描述底部的 安装。
安装库之后,运行 I²C 扫描器草图来测试 OLED。它应该返回地址 0x3C,但如果你得到不同的结果,记下它以备后用。接下来,使用 表 10-2 中列出的连接方式,将 OLED 连接到你的 Arduino Uno 或兼容板。
表 10-2: OLED 与 Arduino 连接
| OLED | Arduino Uno |
|---|---|
| SDA | A4 |
| SCL | A5 |
| VCC | 5V |
| GND | GND |
现在,输入并上传清单 10-1 的草图,你可以在本书网页上的可下载代码文件中找到它。片刻后,显示屏应当在显示“Count!”和一个整数之间滚动,如图 10-11 所示。如果你的显示屏的总线地址与 0x3C 不同,请将新地址插入到行 u8g2_2.setI2CAddress(0x3D); 的 “address” 字段中,在 void setup() 中,然后取消注释此行,保存并重新上传草图。

图 10-11:来自 清单 10-1 的示例显示
清单 10-1 展示了如何实现这一点。
❶ #include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
void setup()
{
Wire.begin();
❷ u8g2.begin();
//u8g2_2.setI2CAddress(address);
❸ u8g2.setFont(`u8g2_font_logisoso32_tr`); // Select font
}
void loop()
{
for (int a = 29999; a < 32767; a++)
{
u8g2.clearBuffer(); // Clear OLED memory
❹ u8g2.drawStr(0, 32, "Count!"); // Write text to memory
u8g2.sendBuffer(); // Display contents of memory
delay(1000);
u8g2.clearBuffer(); // Clear OLED memory
u8g2.setCursor(0, 32); // Position cursor in memory
u8g2.print(a); // Write integer to memory
u8g2.sendBuffer(); // Display contents of memory
delay(1000);
}
}
清单 10-1:使用图形 OLED 显示屏
首先,草图包含了 u8g2 库,并包含了 OLED 库 ❶,然后创建了 OLED 的实例。OLED 的规格在此文件中定义,以便库知道你正在使用哪种类型的 OLED。
草图首先初始化 OLED ❷ 并为显示屏选择字体 ❸。草图使用的字体为 32 像素高,这对于这个 OLED 显示屏效果很好,但你可以用https://
草图通过在其内存中绘制预期的显示内容,然后将内存中的内容发送到显示屏,来控制 OLED。OLED 的内存清空后,函数 ❹ 接受 OLED 显示屏的较低 x 和 y 坐标,然后开始绘制文本本身。草图随后将 OLED 内存中的内容打印到显示屏上。
草图在短暂的延迟后再次清空内存,并通过首先设置光标位置,再将数字打印到内存中,然后通过另一个 sendBuffer() 函数来显示内存中的内容。
现在你已经掌握了 OLED 显示屏,我将介绍一个你将在最终项目中与 OLED 一起使用的传感器。
BMP180 传感器
BMP180 传感器是一种廉价、易于使用的 I²C 设备,用于测量环境温度和气压。本章的最终项目使用了一个紧凑型的 BMP180 模块,带有上拉电阻,类似于第 30 个项目中使用的那种,例如 PMD Way 部件 18000001,如图 10-12 所示。

图 10-12:BMP180 传感器模块
与 OLED 显示屏一样,首先需要将 BMP180 的引脚焊接到电路板上。接着,在 Arduino 库管理器中找到Adafruit BMP180库并安装它。
在安装 BMP180 库的过程中,可能会提示你安装一些缺失的依赖项。如果出现提示,请点击安装全部。
现在,输入并上传清单 10-2 的草图。上传完成后,打开 IDE 中的串口监视器,你将看到当前的温度(单位为摄氏度)和气压(单位为百帕)。在一些地区,气压单位是毫巴。一百帕等于一毫巴。图 10-13 显示了这个输出的示例。

图 10-13:清单 10-2 的示例输出
清单 10-2 展示了这一过程是如何工作的。
❶ #include "Adafruit_BMP085.h"
Adafruit_BMP085 bmp;
int temperature;
int pressure;
void setup()
{
Serial.begin(9600);
❷ bmp.begin();
}
void loop()
{
Serial.print("Temperature = ");
❸ temperature = bmp.readTemperature();
Serial.print(temperature);
Serial.println(" degrees C");
Serial.print("Pressure = ");
❹ Serial.print(int(bmp.readPressure() / 100));
Serial.println(" hPa");
Serial.println();
delay(500);
}
清单 10-2:使用 BMP180 温度和气压传感器
要设置 BMP180,草图首先包含所需的库,并创建一个传感器实例 ❶。两个变量,temperature 和 pressure 存储传感器数据。接下来是初始化传感器 ❷。
然后,草图可以使用read Temperature() ❸和readPressure() ❹函数从传感器获取数据。温度和气压读数(后者除以 100 转换为百帕)将被发送到串口监视器。
项目 #31:创建一个温度和气压显示
这个项目结合了本章到目前为止学到的所有技巧,通过使用多个 I²C 总线将 BMP180 的温度和气压读数与 OLED 显示器进行显示。你将需要以下组件:
-
一块 Arduino Uno 或兼容板及 USB 电缆
-
一个 128 × 32 像素,0.91 英寸的 OLED 显示屏
-
一块 BMP180 温度和气压传感器板
-
一个 TCA9548A 扩展板
-
一块无焊面包板
-
各种跳线
按照图 10-14 所示组装电路。

图 10-14:项目 #31 的电路图
接下来,输入并上传项目 #31 的草图。温度和气压应交替显示在 OLED 上,如图 10-15 所示。

图 10-15:项目 #31 的示例输出
让我们看看这个是如何工作的:
// Project #31 - Temperature display
❶ #include <U8g2lib.h>
#include <Adafruit_BMP085.h>
#include <Wire.h>
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
Adafruit_BMP085 bmp;
float temperature;
float pressure;
❷ void TCA9548A(uint8_t bus)
{
Wire.beginTransmission(0x70); // TCA9548A address is 0x70
Wire.write(1 << bus); // Send byte to select bus
Wire.endTransmission();
}
void setup()
{
❸ Wire.begin();
u8g2.begin();
❹ u8g2.setFont(u8g2_font_logisoso32_tr); // Select font
TCA9548A(0); // Select I2C bus 0 for the BMP180
❺ bmp.begin();
}
void loop()
{
// First, get the temperature from the BMP180
❻ TCA9548A(0); // Select I2C bus 0 for the BMP180
temperature = bmp.readTemperature();
pressure = bmp.readPressure();
// Next, display the temperature on the OLED
❼ TCA9548A(1); // Select I2C bus 1 for the OLED
u8g2.clearBuffer(); // Clear OLED memory
u8g2.setCursor(0, 32);
u8g2.print(int(temperature));
u8g2.drawStr(50, 32, "C");
❽ u8g2.sendBuffer();
delay(1000);
u8g2.clearBuffer(); // Clear OLED memory
u8g2.setCursor(0, 32);
pressure = pressure / 100; // Convert to hectopascals
u8g2.print(int(pressure));
u8g2.drawStr(80, 32, "h");
❾ u8g2.sendBuffer();
delay(1000);
}
程序首先引入所需的库 ❶,并配置要使用的 OLED 类型、温度传感器和所需的变量,正如之前所讲。然后它定义了一个自定义函数,用于从 TCA9548A 选择 I²C 总线 ❷。接着,它启动 I²C 和 OLED 功能 ❸,选择 OLED 字体 ❹,并启动温度传感器 ❺。
程序接着将 TCA9548A 切换到其第一个 I²C 总线,以获取来自 BMP180 的数据 ❻,然后切换到第二个 I²C 总线 ❼,以便将温度和压力数据显示在 OLED 屏幕上 ❽ ❾。
继续前进
在这一章,你学会了如何在 I²C 总线上控制多个设备,同时将新的传感器和显示器加入你的工具包。你可以运用这些技能制作一个大型的多时区时钟,使用多个 OLED 屏幕和实时时钟芯片,或者你也可以在一个区域使用多个温度传感器来检测冰箱内部温度的变化。无论是什么项目,你再也不受限于只有一个 I²C 总线地址的廉价设备。
在下一章,你将使用 Arduino Leonardo 或兼容板来模拟 USB 鼠标和键盘,这为许多有趣且实用的应用打开了可能性。
第十一章:11 使用 LEONARDO 模拟 USB 鼠标和键盘

本章将展示如何将 Arduino 收集到的信息转换为按键或鼠标移动,然后将其发送到计算机,模拟 USB 键盘上的输入或模拟操作外部鼠标。为此,你将使用 Arduino Leonardo 开发板,它与通常的 Uno 或兼容板有一些细微的不同。
除了微型 USB 连接器和所有主要组件的完整表面贴装设计外,Leonardo 还使用了 Microchip Technology 的 ATmega32U4 微控制器。该微控制器内置 USB 通信,因此开发板无需 USB 接口芯片,这使得 Leonardo 可以作为鼠标或键盘出现在连接的计算机上。你可以使用这种键盘模拟技术来构建自己的游戏控制器、快捷键键盘、快速数据捕获和输入系统、为不同能力的用户设计的输入设备等。
在本章中,你将学到:
-
模拟 USB 键盘的按键输入和打字操作,以及 USB 鼠标的移动和按钮控制
-
构建一个直接写入电子表格的 USB 数据记录器
-
使用键盘使其成为快捷键键盘
USB 键盘
要使用 Leonardo 模拟键盘(如图 11-1 所示),除了足够长的 USB 电缆将开发板与主机 PC 连接外,不需要任何额外的硬件。如果你购买更长的电缆,请记得你需要一根 USB-A 到 micro-USB 的电缆。

图 11-1:Arduino Leonardo 开发板
你可以通过包含 Arduino 键盘库来激活键盘模拟,使用以下两行:
#include "Keyboard.h" // Entered at start of sketch
Keyboard.begin(); // Entered in void setup()
若要让 Leonardo 通过 USB 键盘模拟用户的按键操作,使用以下功能:
Keyboard.print()
Keyboard.println()
这些功能的操作方式类似于Serial.print()和Serial.println():前者将要“输入”的文本作为按键发送,后者则将相同的内容发送并添加一个回车符(或按下 RETURN 或 ENTER 键)以换行。使用这两个函数,你可以发送任何通常通过 USB 键盘发送的文本。
若要停止模拟 USB 键盘,请使用以下功能:
Keyboard.end() // Ends emulation
该功能仅在你期望用户在 Leonardo 仍然连接时,继续通过正常键盘使用 PC 时才需要。
让我们试试看。首先,将 Arduino Leonardo 连接到计算机。如果这是你第一次使用 Leonardo,你的 PC 可能会花几秒钟自动安装所需的 USB 驱动程序。接下来,打开 Arduino IDE,并使用下拉菜单将板卡类型更改为 Arduino Leonardo,如图 11-2 所示(请注意,你的 COM 端口可能与图中的不同)。

图 11-2:在 IDE 中选择 Leonardo
在使用包含 Keyboard 库的草图时,务必确保板卡类型设置为 Leonardo;否则,如果你在测试错误时,它们将无法编译。
如果你的 PC 上有其他软件正在运行,关闭它们或确保它们的窗口在你进行键盘和鼠标仿真时不会处于活动状态。否则,Leonardo 发送的“按键”可能会干扰其他软件。例如,在我自己机器上上传以下草图之前,我打开的唯一软件是 Arduino IDE 和记事本。
现在输入并上传列表 11-1 中的草图;然后立即切换到简单的文本编辑器,如记事本。
#include "Keyboard.h"
void setup()
{
delay(5000);
Keyboard.begin();
}
void loop()
{
❶ Keyboard.print("Hello, ");
❷ Keyboard.println("world. ");
delay(1000);
}
列表 11-1:USB 键盘仿真
上传后,Leonardo 应该每秒打出一次“Hello, world.”,如图 11-3 所示。

图 11-3:列表 11-1 的示例输出
首先,草图包括 Keyboard 库。五秒的延迟为用户提供了一些时间以准备好 PC 接收来自 Arduino 的输入。然后初始化 Keyboard 库。接下来,草图发送一些没有换行符的文本 ❶,然后是一些带有换行符的文本 ❷。
当你完成使用 Leonardo 进行任何 USB 键盘或鼠标仿真项目时,上传一个简单的草图,例如 IDE 示例中的 blink,以防下次连接 Leonardo 到 PC 时,它不会尝试接管控制。如果你在上传新草图时遇到控制问题,可以使用 CTRL-U 快捷键上传草图。
模拟键盘修饰键和特殊键
除了为字母、数字和符号创建按键输入外,你还可以通过一些额外的函数模拟按钮,如光标键、TAB 键、功能键等。
要发送单个按键(即按下按钮并释放它),使用
Keyboard.write(`x`)
其中 x 是要按下的字母、数字或符号的 ASCII 代码,或特殊修饰键的代码,列在表 11-1 中。
表 11-1: 修饰键及其代码
| 修饰键 | 代码 | 修饰键 | 代码 |
|---|---|---|---|
| KEY_LEFT_CTRL | 128 | KEY_F1 | 194 |
| KEY_LEFT_SHIFT | 129 | KEY_F2 | 195 |
| KEY_LEFT_ALT | 130 | KEY_F3 | 196 |
| KEY_LEFT_GUI | 131 | KEY_F4 | 197 |
| KEY_RIGHT_CTRL | 132 | KEY_F5 | 198 |
| KEY_RIGHT_SHIFT | 133 | KEY_F6 | 199 |
| KEY_RIGHT_ALT | 134 | KEY_F7 | 200 |
| KEY_RIGHT_GUI | 135 | KEY_F8 | 201 |
| KEY_UP_ARROW | 218 | KEY_F9 | 202 |
| KEY_DOWN_ARROW | 217 | KEY_F10 | 203 |
| KEY_LEFT_ARROW | 216 | KEY_F11 | 204 |
| KEY_RIGHT_ARROW | 215 | KEY_F12 | 205 |
| KEY_BACKSPACE | 178 | KEY_F13 | 240 |
| KEY_TAB | 179 | KEY_F14 | 241 |
| KEY_RETURN | 176 | KEY_F15 | 242 |
| KEY_ESC | 177 | KEY_F16 | 243 |
| KEY_INSERT | 209 | KEY_F17 | 244 |
| KEY_DELETE | 212 | KEY_F18 | 245 |
| KEY_PAGE_UP | 211 | KEY_F19 | 246 |
| KEY_PAGE_DOWN | 214 | KEY_F20 | 247 |
| KEY_HOME | 210 | KEY_F21 | 248 |
| KEY_END | 213 | KEY_F22 | 249 |
| KEY_CAPS_LOCK | 193 | KEY_F23 | 250 |
| KEY_F24 | 251 |
除了表中列出的修饰符外,您还可以查看完整的 ASCII 码列表,包括字母、数字和符号的编码,网址为 https://
为了简化操作,你可以为想要模拟的键定义所需的代码和匹配的描述。例如,CAPS LOCK 的代码是 193,因此在草图开始时加入以下内容:
#define CAPS_LOCK 193
要使用 Leonardo 按下 CAPS LOCK,你可以使用以下函数:
Keyboard.write(CAPS_LOCK);
要测试此功能,你可以使用清单 11-2 中的草图,每秒按下 CAPS LOCK。
#define CAPS_LOCK 193
#include "Keyboard.h"
void setup()
{
Keyboard.begin();
}
void loop()
{
❶ Keyboard.write(CAPS_LOCK);
delay(1000);
}
清单 11-2:每秒闪烁 CAPS LOCK 每秒
该草图告诉主机 PC 已按下 CAPS LOCK ❶,并将反映在连接到 PC 的 USB 键盘上。(如果此时你打算在朋友的电脑上恶作剧,请自行承担风险!)
按下和松开一个或多个键
你还可以模拟按下键盘上的一个按钮,并在设定的时间后松开它。为此,使用
Keyboard.press(`x`)
按住修饰符或 ASCII 代码为 x 的按钮。然后,要释放该按钮,请使用以下代码:
Keyboard.release(`x`)
你可以使用清单 11-3 进行测试。
#include "Keyboard.h"
void setup()
{
Keyboard.begin();
}
void loop()
{
❶ Keyboard.press(122); // Hold down the "z" key
delay(1000);
❷ Keyboard.release(122);
Keyboard.println(); // Press ENTER for new line
Delay(5000);
}
清单 11-3:测试按下和松开
这个草图展示了按住 Z 键 ❶,等待片刻后松开该键 ❷。图 11-4 展示了该草图的结果。如你所见,打印到屏幕上的前两行文字比后两行短,因为键盘响应时间略有差异,取决于主机 PC 的操作速度。

图 11-4:来自清单 11-3 的示例输出
最后,你可以编程让 Leonardo 同时按下多个键,然后使用多个 Keyboard.press() 函数将它们一一松开,或者一次性全部松开。这使得你能够模拟操作系统中常用的多个按键快捷方式。
例如,你可以通过按下 CTRL-ALT-DELETE 键组合,然后按下 ALT-S 来退出 Windows PC。以下函数会同时按下左侧 CTRL 键、左侧 ALT 键和 DELETE 键:
Keyboard.press(128); // CTRL
Keyboard.press(130); // ALT
Keyboard.press(212); // DEL
然后你可以使用以下代码一次性释放所有按钮:
Keyboard.releaseAll(); // Let go of all buttons
你还可以通过使用 Keyboard.release(x) 来松开单个按钮,并保持其他按钮按下,其中 x 是所需的修饰符或 ASCII 代码。
你将在下一个项目中通过将 Leonardo 的数据直接记录到 PC 上来运用这一切。
项目#32:使用 USB 键盘仿真记录数据
键盘仿真功能的一个非常有用的好处是能够直接将数据从 Leonardo 记录到 PC 中。在这个项目中,你将把来自第十章的 BMP180 传感器的温度和气压数据直接记录到电子表格中。如果你能为数据记录专门配备一台 PC——例如,在实验室或办公室——这是一个快速且经济的数据捕获方法。
你将需要以下零件来完成此项目:
-
一块 Arduino Leonardo 或兼容板和 USB 电缆
-
一块 BMP180 温度和气压传感器板
-
一块无焊接面包板
-
各种跳线
按照图 11-5 所示组装电路。

图 11-5:项目#32 的原理图
要开始,打开你喜欢的电子表格,例如 Microsoft Excel 或 Google Sheets。然后切换回 Arduino IDE 并上传项目#32 的草图。接着迅速切换回电子表格软件,点击左上角的单元格并等待。重置 Leonardo 后的十秒钟,日期、时间、温度和以帕斯卡为单位的气压应会自动“输入”到单元格中,如图 11-6 所示。

图 11-6:项目#32 在 Excel 电子表格中的示例输出数据
一旦开始记录数据,你可以断开 PC 上的鼠标和键盘,以防其他人中断数据采集。此外,通过使用基于云的工具,如 Google Sheets,你可以从任何支持网络的设备实时监控和查看结果,如图 11-7 所示。

图 11-7:Google Sheets 中的示例结果
让我们来看一下这如何工作:
// Project #32 - Data logging using USB keyboard emulation
#define Ctrl 128
#define Semicolon 59
#define Shift 129
#define Tab 179
#define Enter 176
❶ #include "Keyboard.h"
#include "Wire.h"
#include "Adafruit_BMP085.h"
Adafruit_BMP085 bmp;
❷ void spreadsheetDate()
{
Keyboard.press(Ctrl);
Keyboard.press(Semicolon);
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
}
❸ void spreadsheetTime()
{
Keyboard.press(Ctrl);
Keyboard.press(Shift);
Keyboard.press(Semicolon);
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
}
void pressTab()
{
Keyboard.write(Tab);
}
void pressEnter()
{
Keyboard.write(Enter);
}
void setup()
{
Keyboard.begin();
bmp.begin();
❹ delay(10000);
}
void loop()
{
spreadsheetDate();
pressTab();
spreadsheetTime();
pressTab();
Keyboard.print(bmp.readTemperature());
pressTab();
Keyboard.print(bmp.readPressure());
pressEnter();
delay(5000);
}
草图首先初始化传感器和键盘库 ❶。自定义函数 ❷ 模拟按下 CTRL-+,这是许多电子表格中用来插入当前单元格日期的快捷键。释放键之前有一个小延迟。接下来的自定义函数 ❸ 模拟按下 CTRL-SHIFT-+,这是插入当前单元格时间的快捷键。接着是一些按下 TAB 和 ENTER 的函数,方便你省去使用原始命令的步骤。
键盘控制基于 Windows 操作系统的控制方式。如果你使用的是 macOS 或其他操作系统,可能需要更改屏幕上移动光标或电子表格软件中的快捷键。
通常的初始化发生在 void setup() 中,并且有一个长时间的延迟 ❹,这给用户足够的时间将光标导航到电子表格中的起始单元格,之后 Leonardo 才开始打字。最后,在 void loop() 中,日期、时间、温度和气压使用所需的按键组合“输入”到电子表格中,之后会有一个延迟,才会记录下一个样本。
现在你已经对键盘仿真有了扎实的理解,让我们再构建一个你可以定期使用的工具:快捷键键盘。
项目 #33:构建一个 USB 快捷键键盘
那些每天使用 PC 工作或娱乐的人,随着时间的推移,需要反复输入各种内容,比如密码、键盘快捷键或常用的文本行。使用 Arduino Leonardo 和项目 #3 中描述的 12 键键盘(见 第一章),你可以构建一个方便的快捷键键盘,具有 12 个不同的选项。
为了演示目的,项目将会将各种示例分配给键盘。不过,除了有用的文本外,你可以研究你电脑操作系统或最喜欢的软件的快捷方式,并创建你自己的。表 11-2 列出了各种键盘和打字快捷键。
表 11-2: 示例快捷键
| 按钮 | 快捷键 | 所需的仿真按键 |
|---|---|---|
| 1 | 输入 PIN 456700。 | 456700 |
| 2 | 锁定 Windows 11 屏幕。 | CTRL-ALT-DEL,然后按 ENTER |
| 3 | 显示 Windows 任务管理器。 | CTRL-ESC-SHIFT |
| 4 | 打印到默认打印机。 | CTRL-P,然后按 ENTER |
| 5 | 在应用之间切换。 | ALT-TAB |
| 6 | 在 Edge 浏览器中将打开的页面保存为书签。 | CTRL-SHIFT-D |
| 7 | 在 Excel 中切换到页面布局视图。 | 按 ALT-W,然后按 P |
| 8 | 在 Excel 中切换到正常布局视图。 | 按 ALT-W,然后按 L |
| 9 | 您可以通过电子邮件联系我……或在下午 3 点后拨打电话…… | 输入的文字 |
| 10 | 我们无法通过社交媒体解决您的问题。请通过电子邮件联系……以获得技术支持。 | 输入的文字 |
| 11 | 感谢您的邮件。我会在找到所需信息后尽快详细回复。 | 输入的文字 |
| 12 | 如果不亲自检查产品,我们无法确定故障原因。请将其邮寄至…… | 输入的文字 |
您将需要以下部件来完成本项目:
-
一块 Arduino Leonardo 或兼容板和 USB 数据线
-
如第一章项目#3 中所描述的键盘硬件
装配与项目#3 相同,只是将 Arduino Leonardo 替换为 Uno,如图 11-8 所示。您可能需要使用项目#1 中的草图检查每个键盘按钮报告的 ADC 值,然后更新我们 analogkeypad 库中的范围。

图 11-8:项目#33 的电路图
现在输入并上传以下草图:
// Project #33 - USB shortcut keyboard
#define Ctrl 128
#define Alt 130
#define Escape 177
#define Shift 129
#define Tab 179
#define Enter 176
#define Delete 212
❶ #include "Keyboard.h"
#include "analogkeypad.h"
analogkeypad keypad;
void button1()
{
Keyboard.print("456700");
}
void button2()
{
Keyboard.press(Ctrl);
Keyboard.press(Alt);
Keyboard.press(212);
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
delay(100);
Keyboard.write(Enter);
delay(100);
}
void button3()
{
Keyboard.press(Ctrl);
Keyboard.press(Shift);
Keyboard.press(Escape);
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
}
void button4()
{
Keyboard.press(Ctrl);
Keyboard.press(112); // Press P
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
delay(100);
Keyboard.write(Enter);
delay(100);
}
void button5()
{
Keyboard.press(Alt);
Keyboard.press(Tab);
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
}
void button6()
{
Keyboard.press(Ctrl);
Keyboard.press(Shift);
Keyboard.press(100); // Press d
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
}
void button7()
{
Keyboard.press(130);
Keyboard.press(119); // Press w
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
Keyboard.write(112); // Press p
}
void button8()
{
Keyboard.press(Alt);
Keyboard.press(119); // Press w
delay(100);
Keyboard.releaseAll(); // Let go of all buttons
Keyboard.write(108); // Press l
}
void button9()
{
Keyboard.println("You can contact me via email at email@address.com
or telephone 212-555-1213 after 3 p.m. ");
}
void button10()
{
Keyboard.println("We cannot solve your problem via social media.
Please email support@company.com for technical support. ");
}
void button11()
{
Keyboard.println("Thank you for your email, I will reply in more detail
once I can locate the required information. ");
}
void button12()
{
Keyboard.println("Without examining the product in person, we cannot
determine the fault. Please post it to: ");
Keyboard.println("129 West 81st Street");
Keyboard.println("Apt 5B");
Keyboard.println("New York NY 10024");
}
void setup()
{
❷ Keyboard.begin();
}
void loop()
{
❸ switch (keypad.readKeypad()) // Read button status
{
case 1: button1(); break;
case 2: button2(); break;
case 3: button3(); break;
case 4: button4(); break;
case 5: button5(); break;
case 6: button6(); break;
case 7: button7(); break;
case 8: button8(); break;
case 9: button9(); break;
case 10: button10(); break;
case 11: button11(); break;
case 12: button12(); break;
}
delay(250);
}
该草图利用了 analogkeypad 库来控制硬件,利用 Keyboard 库来模拟 USB 键盘。这些库在❶处包含,然后在❷处初始化。草图在循环中等待按钮按下,操作发生在❸处。之后,只需监控键盘并调用与每个按钮匹配的自定义函数,范围从button1()到button12()。每个自定义按钮函数随后执行所需的模拟任务,您可以根据自己的需求进行更改。
一旦草图上传完毕,你的键盘应该会作为快捷键键盘工作。例如,在收到关于故障产品的邮件时,我们的示例用户可以点击回复,按下第 12 个按钮,然后发送邮件。图 11-9 显示了一个示例邮件交换。

图 11-9:项目 #33 示例输出邮件
对于处理客户支持邮件的人来说,这将节省大量时间。根据你自己的日常计算机使用习惯,你可以用自己的快捷键替换草图中的快捷键。
USB 鼠标
你还可以通过在草图中包含以下两行代码来使用 Leonardo 激活鼠标仿真功能:
#include "Mouse.h" // Entered at start of sketch
Mouse.begin(); // Entered in void setup()
要让 Leonardo 像 USB 鼠标一样移动鼠标指针,使用这个功能:
Mouse.move(`x`,`y`,`z`);
参数如下:
x 在 x 轴上的移动量。使用正数向右移动,使用负数向左移动,0 则保持在当前 x 轴位置。
y 在 y 轴上的移动量。使用正数向下移动,负数向上移动,0 则保持在当前 y 轴位置。
z 模拟鼠标滚轮的移动。使用正数向“你”滚动,使用负数向“远离你”滚动。如果你更改了计算机操作系统中默认的鼠标操作,你需要交换正数和负数。
要结束草图中的鼠标仿真,使用:
Mouse.end();
在使用鼠标仿真时,每个鼠标功能后需要一个 2 毫秒的小延迟,以便计算机有时间跟上。你可以使用 清单 11-4 来测试控制指针移动。
#include <Mouse.h>
void setup()
{
Mouse.begin();
}
void loop()
{
❶ Mouse.move(−10, −5, 0);
delay(500);
}
清单 11-4:控制指针移动
草图首先初始化库,然后鼠标指针向左移动 10 像素,向上移动 5 像素❶。之后会有一个短暂的延迟,以稍微减慢操作速度。
上传草图后,你的鼠标指针应该会向屏幕左上方漂移。你也可以随时用 PC 鼠标改变其位置。完成后,上传一个非鼠标的草图以停止移动。
现在,你将通过一个简单的项目来使用这些鼠标仿真功能。
项目 #34:自动保持电脑唤醒
一些带有 USB 鼠标输入的 PC 或其他设备可能会进入“睡眠模式”,或定期需要鼠标移动以证明用户在场。在这个项目中,你将编程 Leonardo 每隔 30 秒左右“晃动”一次鼠标,以保持 PC 唤醒或让老板满意。
唯一需要的硬件是你的 Arduino Leonardo 和匹配的 USB 电缆。输入并上传以下草图:
// Project #34 - PC "Keep Awake"
#include <Mouse.h>
void wiggleMouse()
{
❶ Mouse.move(−50, 0, 0);
delay(500);
❷ Mouse.move(50, 0, 0);
delay(500);
}
void setup()
{
Mouse.begin();
}
void loop()
{
wiggleMouse();
delay(30000);
}
鼠标指针应该向左移动 ❶,稍等片刻,向右移动 ❷,然后再等片刻,正如wiggleMouse()自定义函数中所指示的那样。函数和主循环中的延迟完全是任意的;你可以根据需要调整它们。
USB 鼠标按钮
要让你的 Leonardo 模拟鼠标按钮“点击”(按下然后释放),可以使用
Mouse.click(); // Clicks the left mouse button by default
或者
Mouse.click(`x`);
其中 x 可以是 MOUSE_LEFT、MOUSE_RIGHT 或 MOUSE_MIDDLE,分别对应各个按钮。
要按下并保持鼠标按钮,可以使用以下函数:
Mouse.press(); // Presses and holds down the left mouse button by default
要释放已按下的鼠标按钮,可以使用以下函数:
Mouse.release(); // Defaults to releasing the left mouse button
你还可以使用与Mouse.click()相同的参数来控制中键和右键。请注意,鼠标按钮的左键和右键的定义由操作系统中的设置决定。例如,如果你交换了按钮(即“右键”是物理的左键),你需要考虑这一点。
让我们通过创建一个有趣的绘画项目来演示鼠标按钮的控制。
项目 #35:创建一个 PC 随机绘画工具
这个项目将随机移动鼠标指针并随机按下左键。通过将此草图与 PC 绘图程序结合运行,你可以创作一些前卫的 Arduino 生成的艺术作品。
再次提醒,所需的唯一硬件是你的 Arduino Leonardo 和匹配的 USB 线。输入但不要上传项目 #35 草图。现在打开一个 PC 绘图程序,如 Microsoft Paint,然后切换到 Arduino IDE 并上传草图。上传后切回绘图软件,选择画笔和颜色后将光标放在绘图区域。草图应该开始根据随机鼠标移动进行绘制,创作出类似图 11-10 中的画作。

图 11-10:随机 PC 绘画工具的示例结果
让我们看看这个是如何工作的:
// Project #35 - PC Random Painter
#include <Mouse.h>
void setup()
{
Mouse.begin();
randomSeed(analogRead(0));
❶ delay(10000);
}
void loop()
{
❷ Mouse.press();
delay(2);
switch (random(8))
❸ {
case 0: Mouse.move(−10, 0, 0); break;
case 1: Mouse.move(10, 0, 0); break;
case 2: Mouse.move(0, −10, 0); break;
case 3: Mouse.move(0, 10, 0); break;
case 4: Mouse.move(10, 10, 0); break;
case 5: Mouse.move(10, −10, 0); break;
case 6: Mouse.move(−10, 10, 0); break;
case 7: Mouse.move(−10, 10, 0); break;
}
delay(1000);
❹ Mouse.release();
delay(2);
switch (random(8))
❺ {
case 0: Mouse.move(−10, 0, 0); break;
case 1: Mouse.move(10, 0, 0); break;
case 2: Mouse.move(0, −10, 0); break;
case 3: Mouse.move(0, 10, 0); break;
case 4: Mouse.move(10, 10, 0); break;
case 5: Mouse.move(10, −10, 0); break;
case 6: Mouse.move(−10, 10, 0); break;
case 7: Mouse.move(−10, 10, 0); break;
}
delay(1000);
}
为了创作这幅杰作,草图首先按下鼠标按钮 ❷,执行八种随机鼠标移动之一 ❸,然后释放鼠标按钮 ❹。接着,它再做一次随机鼠标移动 ❺,然后重复此过程。大的延迟 ❶ 让你有时间在上传草图后从 Arduino IDE 切换到绘图软件。
作为最后的挑战,你可以为你的项目添加一个启动按钮,这样 Arduino 就不会在你准备好之前接管键盘或鼠标。你还可以尝试制作一个 Etch A Sketch 模拟器,使用两个电位器来控制鼠标的 x 轴和 y 轴。
继续前进
到此为止,你已经具备了实施 USB 键盘和鼠标仿真所需的技能和实践。你学会了如何指示 Arduino Leonardo 或兼容板作为键盘和鼠标进行操作。每当你这么做时,确保在项目开始时有足够的延迟,以便在 PC 上进行任何必要的准备。这也给你时间轻松地上传新的草图到板子上,如果你完成了仿真实验。如果你完全失去了对板子的控制,可以通过 ICSP 引脚使用 USBasp 硬件上传新的草图。
在下一章,你将学习如何使用 Arduino 读取和写入 USB 闪存驱动器的数据。
第十二章:12 数据的传输与接收:USB 闪存驱动器

USB 闪存驱动器是将数据传输进出 Arduino 项目的便捷工具。本章将向你展示如何使用 Arduino 和一个廉价的接口模块,将数据传输进出这些便携式存储设备。
你可以使用这些记录和检索数据的方法,创建更用户友好的信息交互方式。例如,你可以通过让 Arduino 控制的机器人读取 USB 闪存驱动器上的控制值,改变机器人的动作;或者,你可以改变自动洒水器控制器的时间,而不需要物理用户界面。
本章中,你将学到:
-
安装并测试 USB 闪存驱动器模块
-
将数据从 Arduino 写入并追加到 USB 闪存驱动器
-
将传感器数据记录到 USB 闪存驱动器
-
从 USB 闪存驱动器读取数字和文本到 Arduino
-
构建一个项目 USB 安全密钥
-
使用外部数据从 USB 闪存驱动器配置 Arduino 项目
选择和准备 USB 闪存驱动器
市面上有许多品牌和类型的 USB 闪存驱动器。由于接口模块的独特性,某些驱动器可能完全无法工作。经过测试,我发现 Lexar 和 Toshiba 品牌的闪存驱动器是成功的。确保它们是标明为 USB 2.0,而不是 USB 3.0 的。
USB 闪存驱动器应该出厂时已格式化。然而,如果你发现它们无法可靠地工作,你可能需要自己重新格式化它们。在这种情况下,请使用 FAT32 文件系统,并设置 16KB 的分配单元大小。
每当需要从电脑中移除 USB 闪存驱动器时,请始终使用操作系统提供的安全移除选项。不要在你认为 PC 完成操作后直接拔出闪存驱动器,因为这通常会导致驱动器无法被 USB 接口模块读取。
USB 接口模块
为了与 USB 闪存驱动器配合使用,你需要一个基于 CH376S USB 文件管理器和控制 IC 的接口模块——例如,图 12-1 中显示的 PMD Way 566592 部件。

图 12-1:USB 闪存驱动器接口模块
你可以使用多种接口将模块与微控制器连接起来。为了本章的目的,我们将使用 9,600 bps 的串行 UART 连接模块与 Arduino 上的软件串口。
由于本章所有项目的模块设置相同,我们首先来测试您的模块并确保其配置正确。在 USB 插座下方的 2 × 3 引脚组中找到跳线引脚,并将跳线放置在顶部行的第二和第三个引脚之间,如图 12-2 所示。这将模块设置为串口接口模式。如果您的模块有一个 1 × 3 的跳线引脚组,您可能需要尝试两种选项(跳线连接 1 和 2,或 2 和 3,若引脚未标记)。

图 12-2: 设置 USB 闪存驱动接口模块的跳线
接下来,使用一些公对母跳线将模块与您的 Arduino Uno 或兼容板连接,并按照图 12-3 中所示进行连接。您刚刚放置的跳线下方的三排引脚被称为 S1、S2 和 S3。

图 12-3: USB 模块与 Arduino 之间连接的示意图
模块需要您安装一个 Arduino 库。在库管理器中搜索CH376,然后点击库描述底部的安装按钮。
安装完库后,将 Arduino 与接口模块连接到您的 PC,并上传包含在 CH376 库示例中的 basicUsageSoftSerial 示例代码,如图 12-4 所示。

图 12-4: 在 Arduino IDE 中定位 CH376 示例代码
运行此示例代码的目的是在将其集成到您自己的项目之前,测试 USB 使用情况。现在打开串口监视器,将速率更改为 115,200 bps,并重置您的 Arduino。示例代码应显示一系列选项,并显示闪存驱动器的存在,如图 12-5 所示。

图 12-5: CH376 演示示例的输出
如果在监视器中看不到消息 Flash drive attached!,请轻轻拔出并重新插入 USB 闪存驱动器。模块上的一个 LED 灯应该会在插入 USB 闪存驱动器时亮起。如果这仍然不成功,尝试按照前一节的描述重新格式化闪存驱动器。如果仍然无效,可以尝试更换一个闪存驱动器。
如果 Arduino 报告闪存驱动器已连接,则执行串口监视器上显示菜单中描述的各种操作。首先,在串口监视器中发送 1 来在闪存驱动器上创建一个文本文件,然后发送 2 来将数据附加到该文件(这会打开原文件并向其中添加更多文本),接着发送 7 来显示闪存驱动器的内容,如 图 12-6 所示。

图 12-6:闪存驱动器操作结果
草图会创建文件并将其命名为 TEST1.TXT。如果你有文件名超过经典 8 + 3 文件格式的文件,它们将被截断,并且文件名中会插入波浪符(~),就像 图 12-6 中显示的前两个文件一样。
一旦你创建并附加了文件 TEST1.TXT,你就可以确认硬件和 USB 闪存驱动器组合正常工作。现在是时候将数据写入闪存驱动器上的文本文件了。
写入数据
在本节中,我将向你展示如何初始化并将各种数值和文本数据写入闪存驱动器上的文本文件,然后通过一个示例草图演示这一过程。这项技术非常适合数据捕获和日志记录应用。首先,我将向你展示所需的函数及其工作原理,随后将在接下来的项目中进行演示。
无论你想写入什么类型的文本数据,你首先需要在草图的开头加入以下四行代码,以初始化接口模块和所需的软件串口:
#include <Ch376msc.h>
#include <SoftwareSerial.h>
SoftwareSerial USBserial (7, 6);
Ch376msc USBdrive(Serial1);
这包括 USB 模块(CH376)和软件串口的库,然后初始化软件串口,最后通过软件串口创建 USB 模块的实例。
接下来,你将在 void setup() 中添加一些代码行:
Serial.begin(115200); // For Serial Monitor
USBserial.begin(9600); // For USB module
USBdrive.init();
这会启动串口监视器与 USB 模块之间的串口通信,并开始 USB 模块的通信。你必须按照所示的顺序放置这三个函数。
在草图的 void loop() 部分,使用以下代码来检查 USB 模块的状态。除了检查 USB 闪存驱动器是否存在外,它对于故障排除也很有用;如果闪存驱动器没有连接或无法工作,这段代码将停止草图并通过串口监视器通知你:
if (USBdrive.checkIntMessage())
{
if (USBdrive.getDeviceStatus())
{
Serial.println(F("Flash drive attached!"));
} else {
Serial.println(F("Flash drive detached!"));
}
}
Serial.println()中的文本被包裹在F()中,以节省 Arduino 的 SRAM(可以把它想象成用于操作的内存,就像电脑中的 RAM),并改为使用闪存(微控制器中存储草图的地方)。
接下来,设置文件名并打开文件:
USBdrive.setFileName("TEST1.TXT");
USBdrive.openFile();
文件名必须遵循经典的 8 + 3 格式。如果您打算写入纯文本,请使用.txt文件扩展名;如果打算在电子表格中打开该文件,请使用.csv。如果您使用相同的文件名并多次写入该文件,文件中的数据将从头开始被覆盖。
接下来,写入一些数据。要写入字节、整数、双精度或长整型变量,请使用
USBdrive.writeNum(`x`)
USBdrive.writeNumln(`x`)
其中,x是您要写入的变量的名称。第二个函数会在数字后写入一个新行。
要写入一段文本字符或以字符形式发送命令(例如换行符\n),请使用此方法:
USBdrive.writeChar('`x`');
这将把字符x写入文件。
要写入一行文本,您必须首先将其存储在不超过 254 个字符的字符数组中:
char text1[] = "This is a line of text. \n";
USBdrive.writeFile(text1, strlen(text1));
这将把数组text1中的文本写入已打开的文件,并添加一个换行符。writeFile()函数接受字符数组以及字符数组的长度作为参数。
要写入浮点变量,您必须先将数字转换为字符数组,然后写入该数组。首先创建一个字符数组以存放该数字:
char floatChar[8] = " ";
确保数组足够长,可以包含所有数字、小数点以及必要时的负号。然后为浮点变量赋一个值:
float f = 123.456;
现在,使用以下方法将浮动数转换为字符数组:
dtostrf(`a`,`b`,`c`,`d`);
这里,a是要转换的浮动数,b是整数部分的位数,c是小数部分的位数,d是存放结果数字的数组。例如,要将f转换为floatChar,请使用以下代码:
dtostrf(f,3,3,floatChar);
您现在可以使用以下方法写入字符数组(就像您写入文本行时一样):
USBdrive.writeFile(floatChar, strlen(floatChar));
最后,当您完成数据写入文本文件后,关闭文件:
USBdrive.closeFile();
此时,您可以将闪存驱动器从模块中取出并插入到 PC 中以检索文本文件。
你可以使用以下草图来测试这些函数。在将接口模块连接到你的 Arduino 并插入 USB 闪存驱动器后,输入并上传 列表 12-1。
char text1[] = "This is a line of text in the form of a character
array, followed by a new line function. \n"; ❶
char text2[] = "This is another line of text. I hope you're enjoying the book. John. \n";
char floatChar[8] = " ";
float f = 12.34567;
#include <Ch376msc.h> ❷
#include <SoftwareSerial.h>
SoftwareSerial USBserial(7, 6);
Ch376msc USBdrive(USBserial);
void fileWrite()
{
Serial.println("fileWrite()");
USBdrive.setFileName("TEST1.TXT"); // Set the filename ❸
USBdrive.openFile(); // Open the file
// Write some integers:
for (int a = 0; a < 20; a++)
{
USBdrive.writeNum(a); ❹
USBdrive.writeChar(',');
}
// Write new line:
USBdrive.writeChar('\n'); ❺
// Write lines of text:
USBdrive.writeFile(text1, strlen(text1)); ❻
USBdrive.writeFile(text2, strlen(text2));
USBdrive.writeChar('\n');
// Write floating-point number - first convert to char:
dtostrf(f,2,5,floatChar); // 2 whole digits, 5 decimals ❼
USBdrive.writeFile(floatChar, strlen(floatChar));
USBdrive.closeFile(); // Close the file ❽
Serial.println("Finished.");
}
void setup()
{
Serial.begin(115200); // For Serial Monitor ❾
USBserial .begin(9600); // For USB module
USBdrive.init();
delay(5000);
}
void loop()
{
if (USBdrive.checkIntMessage()) ❿
{
if (USBdrive.getDeviceStatus())
{
Serial.println("Flash drive attached!");
} else {
Serial.println("Flash drive detached!");
}
}
fileWrite();
delay(5000);
}
列表 12-1: 测试写入 USB 闪存驱动器
该草图声明了文本使用所需的变量和用于演示的浮动变量 ❶。它初始化了所需的库 ❷,然后命名并打开文件 ❸。接着,它将整数写入文件 ❹,并加入字符(逗号用于分隔写入的数字)。程序向文件写入换行命令 ❺,然后写入文本 ❻,将浮动变量转换为字符数组,并将其写入文件 ❼。
一旦文件关闭 ❽,就可以拔出闪存驱动器。串行通信和 USB 驱动器在 ❾ 开始。然后,草图插入用于 USB 闪存驱动器检测的代码 ❿,最后通过调用 fileWrite() 函数来写入数据。
你可以通过监视写入进度,并在串行监视器中显示 “Finished” 后拔出闪存驱动器。通过在 PC 上查看文本文件来检查结果,如 图 12-7 所示。

图 12-7: 列表 12-1 的结果
所有这些都允许你以一次性爆发的方式写入数据。然而,如果你希望随着时间的推移向文件添加数据,你需要附加数据。
向文件附加数据
将数字和文本数据写入存储在闪存驱动器上的文本文件非常适合一次性使用的情况。然而,如果你需要向文件添加数据,就不能像前一节那样直接写入。相反,在第一次写入文件后,你必须将数据附加到文件中,以确保所有数据都能安全存储。在本节中,我将解释所需的函数,然后在接下来的项目中演示它们。
附加数据到文件的代码与写入代码相似,主要区别是在打开文件之后。我会先解释需要做什么,然后通过一个项目来演示操作。首先,你检查文件是否存在,如果存在,就使用以下命令将光标(即 USB 接口开始写入的位置)移到文件的末尾:
if (USBdrive.openFile() == ANSW_USB_INT_SUCCESS)
{
USBdrive.moveCursor(CURSOREND);
}
如果闪存驱动器上有足够的空闲空间,你可以使用之前描述的写入函数将数据附加到文件中:
if (USBdrive.getFreeSectors())
{
❶ // Write data to flash drive here
} else
{
Serial.println("Disk full");
}
插入所需的代码以将数据写入文件 ❶;然后像往常一样使用 USBdrive.closeFile() 关闭文件。
现在让我们测试追加数据,参考清单 12-2,该清单会向由清单 12-1 生成的TEST1.TXT文件中追加一个随机数。
#include <Ch376msc.h>
#include <SoftwareSerial.h>
SoftwareSerial USBserial (7, 6);
Ch376msc USBdrive(USBserial);
void fileAppend()
{
Serial.println("fileAppend()");
USBdrive.setFileName("TEST1.TXT"); // Set the filename
❶ if (USBdrive.openFile() == ANSW_USB_INT_SUCCESS)
{
USBdrive.moveCursor(CURSOREND);
}
❷ if (USBdrive.getFreeSectors())
{
❸ USBdrive.writeNumln(random(1000));
} else
{
Serial.println("Disk full");
}
USBdrive.closeFile();
Serial.println("Finished");
delay(1000);
}
void setup()
{
Serial.begin(115200); // For Serial Monitor
USBserial .begin(9600); // For USB module
USBdrive.init();
delay(5000);
❹ randomSeed(analogRead(0));
}
void loop()
{
if (USBdrive.checkIntMessage())
{
if (USBdrive.getDeviceStatus())
{
Serial.println(F("Flash drive attached!"));
} else {
Serial.println(F("Flash drive detached!"));
}
}
fileAppend();
❺ do {} while (1);
}
清单 12-2:向 .txt 文件追加一个随机数
初始化所需的库并完成void setup()后,草图调用自定义函数fileAppend(),此时只有在要追加的文件存在时,草图才会继续执行❶。如果文件存在,光标(即新文本被追加到文件中的位置)将被移动到文件末尾。
会检查是否有足够的空闲空间❷。如果有空闲空间,草图将在文本文件中新的一行写入一个随机数❸。然后,它像往常一样关闭文件。由于项目使用了随机数,草图会对随机数生成器进行初始化❹。“永远什么都不做”的代码行❺防止数据被无限制地追加。
如同清单 12-1 一样,你可以监控写入进度,并在串口监视器显示Finished后移除闪存驱动器。通过在 PC 上查看文本文件来检查结果。例如,图 12-8 展示了草图运行六次后的结果。

图 12-8:清单 12-2 的结果
在下一个项目中,你将使用刚刚学到的框架,将温度传感器的数据追加到文本文件中。
项目#36:传感器数据记录
这个简单的数据记录演示程序记录了温度、气压和由 BMP180 传感器检测到的海拔高度,示例参见第十章。项目#36 的草图从传感器获取数据,并在固定间隔时间内将数据写入 USB 闪存驱动器,你可以根据自己的需要修改这些间隔时间。
你将需要以下部件来完成此项目:
-
一块 Arduino Uno 或兼容板和 USB 数据线
-
一个 USB 闪存驱动器接口模块
-
配备插针的 BMP180 传感器模块
-
无焊接面包板
-
各种跳线
对于这个项目,你将使用面包板作为桥梁,帮助为两个模块和 Arduino 创建更多的 5V 和 GND 连接。如果你之前没有使用过 BMP180 传感器,请参考第十章中的“BMP180 传感器”部分。按照图 12-9 所示组装电路。

图 12-9:项目#36 的原理图
输入并上传项目 #36 的示例代码。代码运行几分钟后,移除 USB 闪存驱动器,并使用电脑查看文件,如 图 12-10 所示。

图 12-10:由项目 #36 捕获的示例数据
由于使用逗号分隔符作为数据和描述之间的分隔符,你可以将文件导入到电子表格程序中,例如 Microsoft Excel,这样就可以轻松分析或分发捕获的数据,正如 图 12-11 所示。

图 12-11:项目 #36 在 Excel 中捕获的示例数据
让我们看看这个是如何工作的:
// Project #36 - Log sensor data to USB flash drive
❶ #include <Wire.h>
#include <Adafruit_BMP085.h>
#include <Ch376msc.h>
#include <SoftwareSerial.h>
Adafruit_BMP085 bmp;
SoftwareSerial USBserial (7, 6);
Ch376msc USBdrive(USBserial);
❷ char floatChar[8] = " ";
void fileAppend()
{
Serial.println("fileAppend()");
USBdrive.setFileName("TEMPDATA.TXT"); // Set the filename
if (USBdrive.openFile() == ANSW_USB_INT_SUCCESS)
{
USBdrive.moveCursor(CURSOREND);
}
if (USBdrive.getFreeSectors())
{
❸ dtostrf(bmp.readTemperature(),2,2,floatChar);
USBdrive.writeFile(floatChar, strlen(floatChar));
USBdrive.writeChar(',');
USBdrive.writeChar('C');
USBdrive.writeChar(',');
❹ USBdrive.writeNum(bmp.readPressure());
USBdrive.writeChar(',');
USBdrive.writeChar('P');
USBdrive.writeChar('a');
USBdrive.writeChar(',');
❺ dtostrf(bmp.readAltitude(),5,1,floatChar);
USBdrive.writeFile(floatChar, strlen(floatChar));
USBdrive.writeChar(',');
USBdrive.writeChar('m');
USBdrive.writeChar('\n');
} else
{
Serial.println("Disk full");
}
USBdrive.closeFile();
Serial.println("Finished");
}
void setup()
{
bmp.begin();
Serial.begin(115200); // For Serial Monitor
USBserial.begin(9600); // For USB module
USBdrive.init();
delay(5000);
}
void loop()
{
if (USBdrive.checkIntMessage())
{
if (USBdrive.getDeviceStatus()) {
Serial.println(F("Flash drive attached!"));
}
else
{
Serial.println(F("Flash drive detached!"));
return;
}
}
fileAppend();
❻ delay(5000); // Arbitrary delay
9
这个 Listing 12-2 示例的实用扩展首先包含并设置了 BMP180、USB 模块和软件串口所需的库,从 ❶ 开始。然后,示例声明了一个字符数组 ❷,用于将浮动小数点数字转换为字符以写入闪存驱动器。温度(摄氏度)、气压(帕斯卡)和海拔(米)从 BMP180 获取,并在 ❸、❹ 和 ❺ 处分别写入闪存驱动器,数据之间有简单的分隔符。你可以根据需要更改任意延迟 ❻。
你可以使用这个项目将任何 Arduino 能够捕获或生成的数据记录到 USB 闪存驱动器上。为了增加挑战性,你可以加入一个实时时钟,记录时间和日期以及天气数据。
读取数字数据
从 USB 闪存驱动器读取数据是将设置、参数和其他信息导入 Arduino 项目的有用方法。在本节中,我将向你展示如何从 USB 读取数字数据;稍后你将学习如何读取文本数据。
你的 Arduino 可以查询存储在闪存驱动器上的文件,该文件仅包含整数并逐个读取它们。每个整数必须单独存储在文件的每一行,并且后面必须跟随一个换行符。你可以使用记事本风格的文本编辑器创建文本文件,确保使用 8 + 3 的文件名格式,如 FILENAME.TXT。
代码初始化与写入或附加数据到 USB 闪存驱动器时相同。你需要在代码开始时使用以下四行来初始化接口模块和所需的软件串口:
#include <Ch376msc.h>
#include <SoftwareSerial.h>
SoftwareSerial Serial1(7, 6);
Ch376msc USBdrive(Serial1);
接下来,设置你想要打开的文件的名称,并使用以下两个函数打开文件:
USBdrive.setFileName("`FILENAME.TXT`");
USBdrive.openFile();
然后,你可以使用以下代码依次读取文件中的每个数字,从文件的开始到结束:
while(!USBdrive.getEOF())
{
number = USBdrive.readLong();
}
该示例中的整数将存储在变量 number 中。
最后,关闭文件:
USBdrive.closeFile();
让我们通过一个示例来演示,草图将 10 个整数写入闪存驱动器上的文件,然后检索并显示这些数字。你需要将 USB 闪存驱动器模块连接到 Arduino,并且像往常一样准备闪存驱动器。
上传 清单 12-3 草图。
❶ #include <Ch376msc.h>
#include <SoftwareSerial.h>
SoftwareSerial Serial1(7, 6);
Ch376msc USBdrive(Serial1);
void writeIntegers()
{
int number;
if (USBdrive.driveReady())
{
USBdrive.setFileName("NUMBERS.TXT");
USBdrive.openFile();
Serial.println(F("Writing integers:"));
for (int a = 0; a < 10; a++)
{
number = random(−1000, 1000);
Serial.print(number); Serial.print(" ");
USBdrive.writeNumln(number);
}
USBdrive.closeFile();
Serial.println(F("Finished writing integers."));
Serial.println();
}
}
void readIntegers()
{
❷ USBdrive.setFileName("NUMBERS.TXT");
USBdrive.openFile();
Serial.println(F("Reading integer numbers…"));
❸ while(!USBdrive.getEOF())
{
❹ Serial.print(USBdrive.readLong());
Serial.print(" ");
}
❺ USBdrive.closeFile();
Serial.println(F("Finished reading integers."));
Serial.println();
}
void setup()
{
❻ randomSeed(analogRead(0));
Serial.begin(115200);
Serial1.begin(9600);
USBdrive.init();
delay(5000);
}
void loop()
{
❼ if (USBdrive.checkIntMessage())
{
if (USBdrive.getDeviceStatus()) {
Serial.println(F("Flash drive attached!"));
} else {
Serial.println(F("Flash drive detached!"));
return;
}
}
❽ writeIntegers();
delay(2000);
❾ readIntegers();
delay(30000);
}
清单 12-3:在 USB 闪存驱动器上写入和显示整数
在短暂的延迟后,草图应在串口监视器中显示写入到闪存驱动器上的数字,然后从闪存驱动器中读取这些数字,并再次在串口监视器中显示相同的数字,如 图 12-12 所示。

图 12-12:清单 12-3 示例输出
草图首先包含并设置 USB 模块和软件串口所需的库 ❶。自定义的 writeIntegers() 函数将一些整数写入 USB 闪存驱动器以供演示,USBdrive.setFileName() 函数 ❷ 设置要存储的文件名。在设置要打开的文件名并打开文件后,草图从文件的起始位置 ❸ 开始循环,直到文件结束,读取每个整数 ❹ 并在串口监视器上显示它们。
草图接着关闭文件 ❺,初始化随机数生成器 ❻,并启动串口和 USB 模块。它检查模块中是否存在 USB 闪存驱动器 ❼,然后分别在 ❽ 和 ❾ 运行写入和读取演示功能。
你已准备好在接下来的两个项目中运用你在整数读写方面的知识,通过创建 USB 安全密钥,然后将参数加载到 Arduino 项目中。
项目 #37:使用 USB 安全密钥
该项目展示了如何通过强制 Arduino 在操作主代码之前读取存储在 USB 闪存驱动器上的一个秘密数字文本文件来锁定或解锁 Arduino 的操作能力。如果文本文件中的数字与草图中加载的数字匹配,代码就可以运行;否则,Arduino 将停止并且无法运行。如果没有 USB 闪存驱动器,它也无法操作。
你将需要以下零件来完成此项目:
-
一个 Arduino Uno 或兼容板和 USB 电缆
-
一个 USB 闪存驱动器接口模块
-
各种跳线
按照项目 #36 的描述,将 USB 闪存驱动器模块连接到 Arduino,然后使用你的 PC 创建一个名为 SECURITY.TXT 的文本文件,文件内容仅包含一行数字 12345。将该文件复制到 USB 闪存驱动器上。
接下来,将闪存驱动器插入 USB 模块中,然后输入并上传以下草图。
// Project #37 - USB security key
#include <Ch376msc.h>
#include <SoftwareSerial.h>
SoftwareSerial Serial1(7, 6);
Ch376msc USBdrive(Serial1);
boolean readKey()
{
boolean keyStatus=false;
❶ int securityKey=12345;
int testKey=0;
❷ USBdrive.setFileName("SECURITY.TXT");
USBdrive.openFile();
while(!USBdrive.getEOF())
{
❸ testKey=USBdrive.readLong();
}
USBdrive.closeFile();
if (securityKey==testKey)
{
❹ keyStatus=true;
}
❺ return keyStatus;
}
void setup()
{
pinMode(13, OUTPUT);
Serial.begin(115200);
Serial1.begin(9600);
USBdrive.init();
delay(5000);
❻ if (USBdrive.checkIntMessage())
{
if (USBdrive.getDeviceStatus())
{
Serial.println(F("Flash drive attached!"));
} else {
Serial.println(F("Flash drive detached!"));
return;
}
}
❼ if (readKey()==false)
{
Serial.println("Incorrect key!");
❽ do {} while (1); // Halt
}
}
void loop()
{
❾ digitalWrite(13, HIGH);
delay(500);
digitalWrite(13, LOW);
delay(500);
}
本质上,这个草图打开一个文本文件,读取五位数字,并将其与草图中存储的数字进行比较。如果匹配,则执行 void loop() 中的代码;如果不匹配,草图将停止在 void setup() 中。
自定义的 boolean readKey() 函数检查安全文件,该文件中存储了安全密钥值 ❶。草图打开 USB 闪存驱动器上的文件 ❷,然后读取文件中的密钥值 ❸。如果 Arduino 密钥值和文件中的密钥值匹配,当草图进行比较时,函数的值将设置为 true ❹。否则,函数将默认返回 false。无论哪种结果,都将在 ❺ 返回。
检查键值从 void setup() 开始。该草图首先检查 USB 闪存驱动器 ❻ 的存在,然后检查文件键值 ❼。如果没有匹配,或者未插入 USB 闪存驱动器,草图会停止 ❽,并且 Arduino 必须重置才能重试。这些结果将显示在串行监视器中。例如,在 图 12-13 中,监视器告诉我闪存驱动器已连接,但我的键值不正确。

图 12-13:当安全密钥不匹配时,项目 #37 的示例输出
然而,如果密钥匹配,草图可以继续执行到 void loop() 并像往常一样运行任何代码 ❾。在这种情况下,它只是简单地闪烁 Arduino 的板载 LED。
你可以使用这个草图为你自己的基于 Arduino 的项目添加安全密钥。在下一个项目中,你将使用类似的硬件将大量数据导入 Arduino,从而可以做出相应的决策。
项目 #38:通过 USB 闪存驱动器配置项目
你还可以使用 USB 闪存驱动器功能,将数据或参数从 USB 闪存驱动器导入到 Arduino 中。例如,你可以导入机器人预设的指令,设置计时器控制项目的开始和结束时间,或为由项目控制的灯光添加开关模式。你可以通过读取 USB 上的文件,将这些值保存到 Arduino 的内部 EEPROM 中,然后检索并处理这些值。这对于更持久的项目配置非常有用。
为了演示这一过程,本项目向你展示了如何从 USB 闪存驱动器上的文件中读取介于 0 到 255 之间的整数,存储这些值到 EEPROM 中,然后再检索这些值并在八个 LED 上以二进制形式显示它们。完成后,你可以将这些函数作为框架用于你自己的项目。
你将需要以下零件来完成本项目:
-
一块 Arduino Uno 或兼容板及 USB 线
-
一个 USB 闪存驱动器接口模块
-
各种跳线
-
八个 1 kΩ,0.25 W,1% 的电阻器
-
八个 LED
-
一块无焊接面包板
按照图 12-14 中的示意图组装电路。

图 12-14:项目 #38 的电路图
接下来,在 USB 闪存驱动器上创建一个名为SETTINGS.TXT的文本文件,并在每一行上放置一个 0 到 255 之间的数字(包括 0 和 255)。这些值将从闪存驱动器复制到 EEPROM 中。(你可以在《Arduino 工作坊》第二版的第十八章中查看如何编写和安装你自己的 Arduino 库。)
接下来,将闪存驱动器插入 USB 模块,输入并上传项目 #38 的草图,打开串行监视器。LED 应该会显示存储在 EEPROM 中的每个数字的二进制等效值。(值 0 到 3 可能无法正确显示,因为这些 LED 共享了用于 USB 串行连接到 PC 的数字引脚。)
让我们看看它是如何工作的:
// Project #38 - Project configuration via USB flash drive
#include <EEPROM.h>
#include <Ch376msc.h>
#include <SoftwareSerial.h>
SoftwareSerial Serial1(9, 8);
Ch376msc USBdrive(Serial1);
void loadData()
{
int _data;
int pointer = 0;
❶ USBdrive.setFileName("SETTINGS.TXT");
USBdrive.openFile();
Serial.println(F("Reading data from USB:"));
while(!USBdrive.getEOF())
{
❷ data = USBdrive.readLong();
EEPROM.write(_data,pointer);
Serial.print(pointer); Serial.print(" "); Serial.println(_data);
pointer++;
}
❸ USBdrive.closeFile();
Serial.println(F("Finished."));
Serial.println();
}
void useData()
{
int a;
int _data;
Serial.println("Reading from EEPROM…");
for (a = 0; a < 100; a++)
{
❹ _data = EEPROM.read(a);
PORTD = _data;
Serial.print(a); Serial.print(" "); Serial.println(_data);
delay(100);
}
}
void setup()
{
DDRD = B11111111;
Serial.begin(115200);
Serial1.begin(9600);
USBdrive.init();
delay(5000);
}
void loop()
{
if (USBdrive.checkIntMessage())
{
if (USBdrive.getDeviceStatus()) {
Serial.println(F("Flash drive attached!"));
} else
{
Serial.println(F("Flash drive detached!"));
return;
}
}
loadData();
delay(1000);
useData();
delay(1000);
do {} while (1); // Halt
}
草图首先包含并设置了 USB 模块、EEPROM 和软件串行线路所需的库。自定义函数 loadData() 从 USB 闪存驱动器中的SETTINGS.TXT文件中检索数据,并将其存储到 EEPROM 中。草图打开闪存驱动器上的文件 ❶ ,然后从文件中读取每个整数 ❷ 并将其存储到 EEPROM 中。pointer 变量跟踪 EEPROM 的位置,并在每次读取后增加 1。草图读取完文件后会关闭文件 ❸。
自定义的 useData() 函数依次读取每个值并将其存储在 PORTD 寄存器中,以控制 LED。(关于端口操作的复习,请参阅第二章)。for 循环依次读取 EEPROM 中的每个位置的值 ❹ ,然后将该值发送到 PORTD 寄存器。
该草图在 void setup() 中设置了常见的配置,并在 void loop() 中检查闪存驱动器。然后它调用了自定义函数 loadData() 和 useData()。由于“什么也不做”的那一行,草图仅运行一次。
当该项目运行时,你将看到数据从闪存驱动器复制到 EEPROM 中,随后你将看到相同的数据从 EEPROM 中显示出来。我在 loadData() 和 useData() 函数中插入了几行 Serial.print(),以便监控过程;图 12-15 显示了这个过程的示例。

图 12-15:项目 #38 的示例输出
不过,如果你不需要调试信息,可以移除 Serial.print() 语句。
这个简单的示例演示了如何从 USB 闪存驱动器读取数字数据并将其存储到 Arduino 中。接下来的任务就是你自己从 EEPROM 获取所需的数据,应用到你的项目中。
现在你已经学会了如何读取数字数据,我将向你展示如何读取文本。
读取文本
你的 Arduino 还可以读取来自简单文件(与整数使用的文本文件类型相同)的文本文件,这些文件只包含文本和换行符。库将检测到换行符,但由文字处理软件创建的任何其他格式代码将仅显示为 ASCII 表中的字符。
同样,最好使用记事本风格的文本编辑器创建此类文件,采用 8 + 3 的文件名格式。读取文本与读取整数类似;首先设置你想要打开的文件,然后打开它:
USBdrive.setFileName("TESTTEXT.TXT");
USBdrive.openFile();
接下来,依次读取文件中的每个字符,从文件的开始到结束,使用
moreText = USBdrive.readFile(_buffer, sizeof(_buffer));
其中,moreText 是一个布尔变量(true 或 false),_buffer 是一个字符数组,长度在 2 到 255 个字符之间,用于存放文本。你需要重复执行前述语句,直到变量 moreText 返回 false(这表示文件读取完毕)。
最后,关闭文件:
USBdrive.closeFile();
让我们通过一个草图来演示,展示名为 TESTTEXT.TXT 的文本文件内容,该文件存储在 USB 闪存驱动器上。按照图 12-3 的方式连接 USB 模块,将 USB 闪存驱动器插入模块,上传并输入 清单 12-4 草图:
#include <Ch376msc.h>
#include <SoftwareSerial.h>
SoftwareSerial Serial1(7, 6);
Ch376msc USBdrive(Serial1);
void readText()
{
char _buffer[254];
boolean moreText;
❶ USBdrive.setFileName("TESTTEXT.TXT");
USBdrive.openFile();
❷ moreText = true;
while(moreText)
{
❸ moreText = USBdrive.readFile(_buffer, sizeof(_buffer));
❹ Serial.print(_buffer);
}
❺ USBdrive.closeFile();
}
void setup()
{
Serial.begin(115200);
Serial1.begin(9600);
USBdrive.init();
delay(5000);
}
void loop()
{
if (USBdrive.checkIntMessage())
{
if (USBdrive.getDeviceStatus())
{
Serial.println(F("Flash drive attached!"));
} else
{
Serial.println(F("Flash drive detached!"));
}
}
❻ readText();
delay(5000);
}
清单 12-4:从闪存驱动器读取文本
打开串口监视器,应该会显示文本文件的内容,如图 12-16 所示。

图 12-16:来自清单 12-4 的示例输出
自定义的 readtext() 函数负责打开文本文件、读取内容并使用这些文本。所需的变量包括字符数组 _buffer[254],它一次从 USB 闪存驱动器存储 254 个字符的数据,以及布尔变量 moreText,它监控读取状态。在函数设置并打开文件进行使用 ❶ 之后,它将布尔变量 moreText 设置为 true ❷,以启动随后的 while 循环。它从文件中读取 254 个字符,并将它们放入数组 _buffer[254] ❸。如果还有更多文本需要读取,函数会将 true 返回到 moreText,使得 while 循环重复执行。然后,函数将文本发送到串口监视器 ❹。当读取到文件末尾时,moreText 被设置为 false,并关闭文件 ❺。最后,草图调用整个 readtext() 函数 ❻。
现在,你已经有了基本的框架,可以从 USB 闪存驱动器读取文本,并在自己的项目中使用它。如同前一节中读取整数的演示,你可以使用字符或单词作为加载参数来配置项目,或者使用收集到的字符数组通过其他方法进行显示。如果你使用较小的缓冲区数组,可能是 10 个字符长,这将使你能够读取 Arduino 可以识别并执行的命令词。
继续前进
在这一章中,你学习了如何将数据写入 USB 闪存驱动器,并从 USB 闪存驱动器中检索和使用数据与 Arduino 进行交互。这是一种简单易用的将数据从 Arduino 传输到外部设备的方法。
在下一章中,你将通过学习如何将 PS/2 PC 键盘与 Arduino 连接,进一步扩展你的输入方法知识。
第十三章:13 与 PS/2 键盘的接口

一些 Arduino 项目,比如电机控制器、照明系统,甚至是游戏,需要通过一种熟悉且用户友好的方式输入文本或数值数据。本章将展示如何通过标准的 PS/2 风格 PC 键盘接受这些数据,然后通过 I²C 接口 LCD 显示出来。
你将学习:
-
通过 Arduino 接收来自个人系统/2(PS/2)键盘的数据
-
设置 I²C 接口 LCD,以便方便地显示数据
-
从 PS/2 键盘捕获文本并在 LCD 上显示
你还将构建一个带有键盘输入的 RGB LED 测试器,以及一个无干扰的文本文件输入设备。
PS/2 键盘
本章使用的键盘具有 PS/2 风格的接口,这种接口最初由 IBM 于 1987 年与其新的 PS/2 系列个人计算机一起推出。这种接口在 1997 年左右流行,但 USB 标准的普及让其逐渐被取代。然而,由于接口简单,PS/2 键盘仍然可用并被各种 PC 和工业设备使用。PS/2 键盘的实际按键布局几乎与今天使用的键盘完全相同。
PS/2 键盘连接器采用六针迷你 DIN 格式,如图 13-1 所示。

图 13-1:PS/2 键盘连接器
我们将使用一个简单的 PS/2 插座分线板(例如 PMD Way 的 694804 部件)将键盘与 Arduino 连接,如图 13-2 所示。插座分线板使用四个连接:5V、GND、时钟和数据。

图 13-2:PS/2 插座分线板
每个键盘上的按键都有一个数字代码表示。当按下键盘上的一个按键时,按键码数据通过一个双向半双工数据线传输到 PC。这意味着数据可以在 PC 和键盘之间双向传输,但每次只能在一个方向上传输。
注意
PS/2 键盘是 5V 设备。如果你使用的是工作电压为 3.3V 的 Arduino 或兼容设备,你需要在 Arduino 与 PS/2 分线板之间使用双向电平转换板。
对于每一次按键,键盘会发送一个低电平的起始位,接着是 8 个比特表示按下的键,然后是一个校验位,通过数据线传输,并且有一个匹配的时钟线来保持时序和数据的准确性。例如,图 13-3 显示了按下 P 键时时钟线(1)和数据线(2)。

图 13-3: 在数字存储示波器上显示的 PS/2 键盘按键时序图
字母 P 的键码是 16 进制的 0x4D,或二进制的 01001101——你可以在图 13-3 中看到标记在数据线上的位。键码的完整列表可从https://
要在 Arduino 上使用键盘,您需要安装一个库。打开 IDE 的库管理器并搜索ps/2,然后安装结果库。
要测试键盘,请将 PS/2 突破板连接到 Arduino,如图 13-4 中的原理图所示。

图 13-4:键盘连接的原理图
接下来,输入并上传列表 13-1 示例。
❶ #include <PS2Keyboard.h>
PS2Keyboard keyboard;
void setup()
{
delay(1000);
❷ keyboard.begin(4, 3);
Serial.begin(9600);
Serial.println("Keyboard Test:");
}
void loop()
{
if (keyboard.available())
❸ {
// Read the next key:
❹ char c = keyboard.read();
// Check for some of the special keys:
switch (c)
{
case PS2_ENTER: Serial.println(); break;
case PS2_TAB: Serial.print("[Tab]"); break;
case PS2_ESC: Serial.print("[ESC]"); break;
case PS2_PAGEDOWN: Serial.print("[PgDn]"); break;
case PS2_PAGEUP: Serial.print("[PgUp]"); break;
case PS2_LEFTARROW: Serial.print("[Left]"); break;
case PS2_RIGHTARROW: Serial.print("[Right]"); break;
case PS2_UPARROW: Serial.print("[Up]"); break;
case PS2_DOWNARROW: Serial.print("[Down]"); break;
case PS2_DELETE: Serial.print("[Del]"); break;
// Otherwise, just print all normal characters
❺ default: Serial.print(c); break;
}
}
}
列表 13-1:测试 PS/2 键盘
上传后几秒钟,打开串口监视器并在 PS/2 键盘上随便输入任何内容。图 13-5 显示了项目运行时的示例输出,其中我在键盘上输入的内容显示在串口监视器中。

图 13-5:键盘测试示例输出
该示例初始化库 ❶,然后激活连接到数字引脚 4 和 3 的键盘 ❷;keyboard.begin()的参数分别是时钟线和数据线的数字引脚号。该示例检查是否按下了一个按钮 ❸,并将结果分配给字符变量c ❹。在使用switch…case语句检查功能键后,它将结果发送到串口监视器 ❺。
如果你发现键盘返回乱码,检查键盘的电源是否正好或接近 5V 直流电压;键盘需要稳定的 5V 电压才能正常工作。一些 Arduino 板可以提供接近 5V 的电压,但有些可能会低一些,大约是 4.7V 或更低。如果电压太低,可以为电路和以后使用 PS/2 键盘的项目使用外部 5V 电源。
现在你已经了解了如何将键盘输入集成到项目中,让我们来设置一个输出显示,使用兼容 I²C 总线的 LCD。
PCF8574 LCD 模块
在 Arduino 世界中,使用 LCD 作为显示输出非常流行。通常,这涉及使用 LCD 扩展板或将与 HD44780 兼容的 LCD 用大约八根线连接回 Arduino。然而,近年来,一种新的使用这些 LCD 的方法变得越来越流行:将 PCF8574 I²C 背包单元焊接到显示屏上,这样只需四根线就可以将 LCD 连接到你的项目中。
PCF8574 是一个基于 I²C 的端口扩展器,它可以将 LCD 的控制引脚连接到流行的串行数据总线。这避免了使用数字 I/O 引脚的需求,并有助于简化整体布线。你可以单独购买模块,或者购买带有预先附加模块的 LCD,如 图 13-6 所示。

图 13-6:PCF8574 I2C LCD 模块的背面
要将模块连接到 Arduino,请使用 5V/V[CC]、GND、时钟和数据线,就像连接其他任何 I²C 设备一样。你可以使用模块上的可调电位器调节显示对比度。你还可以在电位器下方的三个焊接桥接垫(A0、A1 或 A2)之间桥接,以将 I²C 总线地址从默认的 0 x 27 更改为其他七个选项之一。要在 Arduino 上使用 LCD,你必须安装一个库。打开 IDE 的库管理器并搜索 PCF8574,然后安装该库的最新版本。
要测试显示,按照 图 13-7 中的原理图连接到 Arduino。

图 13-7:LCD 连接的原理图
接下来,输入并上传 列表 13-2 草图。
❶ #include <Wire.h>
#include <LiquidCrystal_PCF8574.h>
LiquidCrystal_PCF8574 lcd(0x27);
void setup()
{
Wire.begin();
❷ Wire.beginTransmission(0x27);
lcd.begin(16, 2);
❸ lcd.setBacklight(255);
}
void loop()
{
lcd.home();
lcd.clear();
lcd.setCursor(2, 0);
lcd.print("Hello, world!");
delay(1000);
lcd.setCursor(2,1);
lcd.print("* Arduino *");
delay(1000);
}
列表 13-2:测试 PCF8574 LCD
在草图上传后,你应该能看到 Hello, world! 和 * Arduino * 在 LCD 上闪烁,如 图 13-8 所示。

图 13-8:I2C LCD 在工作中的表现
一旦 LCD 初始化完成,基本操作与常规 Arduino LCD 库相同。该草图包含了用于 I²C 的 Wire 库和 I²C LCD 库 ❶,然后通过总线地址 0x27 创建 LCD 的实例。它在 I²C 总线上启动通信,地址为 0x27(即 LCD) ❷,接着用 lcd.begin() 配置 LCD 大小(16 个字符,2 行),最后使用 lcd.setBacklight() 打开背光。你可以通过使用 255 来开启背光,0 来关闭背光;在此草图中,背光设置为开启状态 ❸。
该草图使用了常规的 LCD 函数:lcd.home() 将光标重置到左上角,lcd.clear() 清除显示屏,等等。如果你在同一个项目中使用其他 I²C 总线设备,记得在发送显示命令之前,使用另一个 Wire.beginTransmission(0x27) 来重新启动 LCD 的总线。
如果显示器无法工作,可能是显示器的总线地址与草图中使用的地址不同。你可以使用第十章中描述的项目 #30 I²C 扫描器草图来检查设备的总线地址,并将其替换为草图中的 0x27。
测试 I2C LCD 和 PS/2 键盘
既然你已经分别实验了 I²C LCD 和键盘,本节将展示它们如何作为一个综合硬件进行协作。我希望这能激发你自己独立项目的灵感,比如使用 LCD 的文本编辑器,或许是你自己设计的基于文本的游戏。
按照前面的章节连接键盘和 LCD,然后输入并上传清单 13-3 草图。
❶ #include <LiquidCrystal_PCF8574.h>
#include <Wire.h>
LiquidCrystal_PCF8574 lcd(0x27);
#include <PS2Keyboard.h>
PS2Keyboard keyboard;
int xPosition = 0;
int yPosition = 0;
void setup()
{
Wire.begin();
Wire.beginTransmission(0x27);
lcd.begin(16, 2);
lcd.setBacklight(255);
lcd.blink();
keyboard.begin(4, 3);
}
void loop()
{
❷ if (keyboard.available())
{
// Read the next key:
char c = keyboard.read();
// Check for some of the special keys:
❸ if (c == PS2_ENTER)
{
xPosition = 0;
if (yPosition == 0)
{
yPosition = 1;
} else if (yPosition == 1)
{
yPosition = 0;
}
lcd.setCursor(xPosition, yPosition);
❹ }
else if (c == PS2_TAB)
{
lcd.print("[Tab]");
❺ }
else if (c == PS2_ESC)
{
lcd.home();
lcd.clear();
xPosition = 0;
yPosition = 0;
❻ }
else if (c == PS2_PAGEDOWN)
{
lcd.print("[PgDn]");
}
else if (c == PS2_PAGEUP)
{
lcd.print("[PgUp]");
}
else if (c == PS2_LEFTARROW)
{
lcd.print("[Left]");
}
else if (c == PS2_RIGHTARROW)
{
lcd.print("[Right]");
}
else if (c == PS2_UPARROW)
{
lcd.print("[Up]");
}
else if (c == PS2_DOWNARROW)
{
lcd.print("[Down]");
}
else if (c == PS2_DELETE)
{
lcd.print("[Del]");
}
else
{
// Otherwise, just print all normal characters
❼ lcd.print(c);
xPosition++;
❽ if (xPosition > 15)
{
xPosition = 0;
❾ if (yPosition == 0)
{
yPosition = 1;
}
else if (yPosition == 1)
{
yPosition = 0;
}
❿ lcd.setCursor(xPosition, yPosition);
}
}
}
}
清单 13-3:LCD 和键盘协同工作
几秒钟后,LCD 背光应该会亮起,块状光标应开始在 LCD 的左上角闪烁。现在开始输入。你在键盘上输入的任何内容——包括文本、符号和大多数功能键——都会显示出来,并且在屏幕上循环,就像 LCD 是一个小型文本编辑器一样,如图 13-9 所示。(为了更清晰的照片,我已关闭背光。)

图 13-9:LCD 显示通过 PS/2 键盘输入的文本
你可以按 ESC 键清除屏幕并将光标移回左上角。程序会从键盘(除了 ESC 键)获取输入,并将每次按键发送到显示器。
程序首先初始化 LCD 和键盘所需的库,并创建两个变量来存储光标的位置 ❶。然后,它启动键盘和 LCD,打开 LCD 背光和闪烁的光标。
程序的主循环首先通过检查键盘上的按键来启动,并根据按键判断接下来要执行的操作 ❷。首先,项目检查用户是否按下了回车键。如果是,LCD 上的光标会移动到第二行的起始位置,如果光标已经在第二行,则返回第一行的起始位置。程序同样检查是否按下了 TAB 键 ❹,如果按下,就在 LCD 上显示。或者,如果用户按下 ESC ❺,屏幕会清空,光标会移回 LCD 的左上角。
程序会继续检查其他按键并在 LCD 上显示相应按键的名称 ❻。这包括按下任何其他不属于前述检查的按键,包括字母、数字和符号 ❼。每次按键后,LCD 上的光标位置会增加,使其在列 ❽ 和行 ❾ 上移动,并在必要时移动到下一行或 LCD 的左上角 ❿。
现在你已经知道如何读取 PS/2 键盘并显示按下的按键,你将在以下项目中使用这些技能,通过从键盘读取整数并与其一起工作,控制 RGB LED 的亮度和可用颜色。
项目 #39:创建 RGB LED 测试仪
在未来的项目中,你可能需要测试可以通过红、绿、蓝(RGB)LED 显示的各种颜色,以便校准设置,以达到所需的 LED 亮度和颜色。在这个项目中,你将构建一个设备,该设备接受每种颜色的脉宽调制(PWM)值,并根据定义的时间控制每个 LED。如果你没有 RGB LED,仍然可以使用三个独立的 LED 来享受和理解这个项目。
注意
你可以在《Arduino Workshop》(第二版,No Starch Press,2021 年)第十九章了解更多关于 PWM 的内容。
本项目需要以下部件:
-
一个 Arduino Uno 或兼容板和 USB 电缆
-
一个 PS/2 分线板
-
一个 PS/2 键盘
-
一个 PCF8574 LCD 模块
-
一个共阴极 RGB LED 或各自一个 RGB LED
-
三个 1 kΩ、0.25 W、1%精度的电阻器
-
一个无焊接面包板
-
各种跳线
按照图 13-10 中的电路图组装电路。在这个项目中,面包板被用作桥接,帮助为两个模块与 Arduino 之间提供更多的 5V 和 GND 连接。

图 13-10:项目#39 的电路图
现在输入并上传项目 #39 的草图。LCD 应显示 R:。使用 PS/2 键盘,输入一个介于 000 和 255 之间的三位数字值,用来设置红色 LED 的 PWM 值。数字越大,LED 中的颜色越强。对绿色和蓝色 LED 重复这个过程,以响应 G: 和 B: 提示。最后,LCD 应显示 T: 提示。输入你要求的 LED 激活持续时间,单位为 000 到 255 秒。LED 将根据你的指示激活。
图 13-11 显示了输入所有三个 LED 的亮度值以及激活时长(秒)的结果。

图 13-11:项目 #39 操作中的用户界面
让我们看看这是如何工作的:
// Project #39 - RGB LED tester
❶ #include <Wire.h>
#include <LiquidCrystal_PCF8574.h>
LiquidCrystal_PCF8574 lcd(0x27);
#include <PS2Keyboard.h>
PS2Keyboard keyboard;
#define red 11
#define green 10
#define blue 9
❷ int readDigit()
{
int _digit=−1;
❸ do
{
if (keyboard.available())
{
// Read the next key:
char c = keyboard.read();
switch (c)
{
case '1' : _digit=_digit+2; break;
case '2' : _digit=_digit+3; break;
case '3' : _digit=_digit+4; break;
case '4' : _digit=_digit+5; break;
case '5' : _digit=_digit+6; break;
case '6' : _digit=_digit+7; break;
case '7' : _digit=_digit+8; break;
case '8' : _digit=_digit+9; break;
case '9' : _digit=_digit+10; break;
case '0' : _digit=0; break;
default: _digit=0; break;
}
❹ lcd.print(c);
}
❺ }
while(_digit==−1);
return _digit;
}
❻ int get3digit()
{
int z;
z = readDigit()*100;
z = z + (readDigit()*10);
z = z + readDigit();
return z;
}
void setup()
{
❼ Wire.begin();
Wire.beginTransmission(0x27);
lcd.begin(16, 2);
lcd.setBacklight(255);
keyboard.begin(4, 3);
DDRB = B111111; // Set LED pins to output
}
void loop()
{
❽ int _red;
int _green;
int _blue;
int _delay;
lcd.home();
lcd.clear();
lcd.setCursor(1, 0);
lcd.print("R:");
lcd.setCursor(4, 0);
_red = get3digit();
lcd.setCursor(8, 0);
lcd.print("G:");
lcd.setCursor(11, 0);
_green = get3digit();
lcd.setCursor(1, 1);
lcd.print("B:");
lcd.setCursor(4, 1);
_blue = get3digit();
lcd.setCursor(8, 1);
lcd.print("T:");
lcd.setCursor(11, 1);
❾ _delay = get3digit();
analogWrite(red, _red);
analogWrite(green, _green);
analogWrite(blue, _blue);
delay(_delay*1000);
analogWrite(red, 0);
analogWrite(green, 0);
analogWrite(blue, 0);
}
草图首先像往常一样包含并配置 I²C LCD 和键盘库 ❶,然后将 LED 的 Arduino 数字引脚号定义为颜色名称,方便参考。自定义函数 readDigit() ❷ 返回一个整数,该整数用于返回按下的 PS/2 键盘数字。此函数使用一个变量 _digit,初始化时设置为 –1,并保持该值直到按下数字键。然后,它返回用户按下的任何按钮的值,作为函数的返回值。
由于 readDigit() 函数需要等待直到键盘按下数字,因此 do…while 函数 ❸ 会持续循环,直到通过测试 _digit 的值来等待按键。当草图检测到按键时,函数会通过 switch…case 函数来查询按键的值。然后,_digit 的值被设置为按下的数字键的值,等于数字加 1。例如,如果用户按下数字 5 键,草图将向 _digit 添加 6 来反映这一点。如果用户没有按下任何数字,则 _digit 返回 0。
一旦_digit的值发生变化,do…while函数中的测试❺就会失败。因此,代码可以继续在 LCD 上显示适当的数字❹,并返回按下的数字值。
该项目需要用户输入三个数字值来设置 PWM 和时间值。因此,用户自定义函数get3digit()❻通过从键盘读取三次数字并将其组合成变量z,作为该函数的返回结果。
草图启动 I²C 总线、LCD 和键盘❼,然后通过端口操作设置 LED 的数字引脚为输出(详见第二章)。我设置了变量来保存每个 LED 的 PWM 值和时间延迟❽。接着,项目接受用户输入的红色 PWM、绿色 PWM、蓝色 PWM 和 LED 点亮的秒数。这些操作会在❾执行,延迟时间转换为毫秒。一旦延迟结束,LED 会熄灭,系统重新开始。
现在,您已经掌握了从 PS/2 键盘获取和处理数字输入的示例,接下来您将构建一个设备,用于捕获文本并将其保存到 SD 卡,以便与 PC 一起使用。
项目#40:构建文本捕获设备
本项目中描述的文本捕获设备让您可以记录笔记或其他文本,供以后参考,不会被互联网或手机通知等干扰,避免分心。这一切您在项目中的 PS/2 键盘上输入的内容都会被记录到 SD 存储卡上的文本文件中,您可以在 PC 上打开该文件进行最终编辑。
注意
您可以在《Arduino 工作坊》第二版(No Starch Press, 2021)第七章中了解更多关于使用 SD 存储卡的内容。
本项目需要以下零件:
-
一块 Arduino Uno 或兼容板及 USB 线
-
一个 PS/2 接线板
-
一台 PS/2 键盘
-
一个 PCF8574 LCD 模块
-
一个 SD 卡模块或 Arduino 的 SD 卡扩展板
-
一张空白 SD 存储卡
-
一块无焊接面包板
-
各种跳线
按照图 13-12 所示组装电路。如果使用 SD 卡扩展板,请将其插入 Arduino,然后连接 LCD 和 PS/2 模块。再次提醒,本项目使用面包板作为桥梁,帮助为这两个模块提供更多的 5V 和 GND 连接到 Arduino。

图 13-12:项目#40 的原理图
现在,输入并上传项目#40 的草图,然后将 SD 卡插入模块。当 LCD 提示时开始输入,如图 13-13 所示。

图 13-13:项目 #40 的 LCD 显示从键盘捕获的文本
当您继续输入时,光标应在第二行滚动,然后返回到第一行,方式与 图 13-14 所示相同。

图 13-14:文本在 LCD 上滚动跨越两行
当您输入时,Arduino 应该自动将每 100 个字符保存到 SD 卡上的文件中。当您输入完毕并且想取出 SD 卡时,按下键盘上的 ESC 键。Arduino 应该保存剩余的字符,然后提示您取出 SD 卡,如 图 13-15 所示。

图 13-15:LCD 显示可以移除 SD 卡的消息
如果在任何尝试保存时 SD 卡出现问题,显示器应显示信息 SD card fail,如 图 13-16 所示。

图 13-16:LCD 显示 SD 卡出现问题
在这种情况下,您的 SD 卡可能没有正确格式化,写保护开关可能被设置为开启,或者 SD 卡可能没有插入模块中。修复问题并取出 SD 卡后,您可以通过将 SD 卡插入 PC 来查看和编辑使用您的 PC 创建的文本文件。
让我们看看它是如何工作的:
// Project #40 - Text capture device
❶ char _text[100];
int _counter = 0;
int xPosition = 0;
int yPosition = 1;
#include <SD.h>
#include <LiquidCrystal_PCF8574.h>
#include <Wire.h>
LiquidCrystal_PCF8574 lcd(0x27);
#include <PS2Keyboard.h>
PS2Keyboard keyboard;
❷ void halt()
{
do {} while (1);
}
❸ void saveText(boolean _halt)
{
if (!SD.begin(10))
{
lcd.setCursor(1, 1);
lcd.print("SD card fail");
halt();
}
❹ File dataFile = SD.open("WRITING.TXT", FILE_WRITE);
if (dataFile)
{
// Save and clear array
for (int i = 0; i < 100; i++)
{
dataFile.print(_text[i]);
_text[i] = " ";
}
dataFile.close();
}
❺ if (_halt == true)
{
File dataFile = SD.open("WRITING.TXT", FILE_WRITE);
if (dataFile)
{
dataFile.println();
dataFile.println("-------------------------");
dataFile.close();
}
lcd.setCursor(1, 1);
lcd.print("OK remove card");
halt();
}
}
void setup()
{
❻ Wire.begin();
Wire.beginTransmission(0x27);
lcd.begin(16, 2);
lcd.setBacklight(255);
lcd.blink();
lcd.clear();
lcd.home();
lcd.print("Start typing:");
lcd.setCursor(xPosition, yPosition);
keyboard.begin(4, 3);
pinMode(10, OUTPUT);
}
void loop()
{
❼ if (keyboard.available())
{
char c = keyboard.read();
if (c == PS2_ENTER)
{
❽ _text[_counter] = '\n';
_counter = _counter + 1;
xPosition = 0;
if (yPosition == 0)
{
yPosition = 1;
}
else if (yPosition == 1)
{
yPosition = 0;
}
lcd.setCursor(xPosition, yPosition);
}
else if (c == PS2_ESC)
{
lcd.home();
lcd.clear();
saveText(true);
}
else
{
_text[_counter] = c;
❾ lcd.print(_text[_counter]);
_counter = _counter + 1;
if (_counter >= 99)
{
❿ saveText(false);
_counter = 0;
}
xPosition++;
if (xPosition > 15)
{
xPosition = 0;
if (yPosition == 0)
{
yPosition = 1;
}
else if (yPosition == 1)
{
yPosition = 0;
}
lcd.setCursor(xPosition, yPosition);
}
}
}
}
本草图基于 列表 13-3 构建,加入了写入 SD 卡的额外代码。首先,它声明了全局变量 ❶,包括用于在写入 SD 卡之前保存用户输入的 array_text[] 缓冲区,跟踪缓冲区中存储字符数的 _counter 变量,以及 LCD 上的光标位置。
接下来,程序包含并初始化 SD 卡模块、LCD 和键盘所需的库。自定义函数 halt() ❷ 用于停止程序的操作,并在文本保存到 SD 卡后调用,以便用户可以安全地移除 SD 卡。它仅在 do…while 函数中永久循环,因为 1 始终为真。
自定义函数 saveText(boolean _halt) ❸ 将数据保存到 SD 卡,具体是根据布尔参数 false 或 true 来决定是保存文本并返回用户输入,还是保存文本并结束操作。该函数会检查 SD 卡是否可用,如果出现问题,会停止并返回错误,具体如图 13-16 所示。如果没有错误,程序将打开此函数中设置的文件并向其写入数据;如果文件不存在,则会创建该文件 ❹。然后,程序会将文本缓冲区写入 SD 卡,并清空缓冲区,以准备接收更多数据。
如果参数 true 被传递给 saveText(boolean _halt) 函数 ❺,代码会打开 SD 卡上的文件,写入一行破折号来结束文本文件,然后关闭该文件并向用户指示,具体如图 13-15 所示。从 ❻ 开始,通常需要激活 I²C 总线、LCD 和键盘,并设置 D10 为 SD 卡模块 SPI 接口的输出。
然后,程序设置项目的主要操作:从键盘捕获文本,显示在 LCD 上,并将其写入 SD 卡。在 ❼ 处,程序准备接收来自 PS/2 键盘的按键。如果用户按下 ENTER,程序将在文本数组中插入一个换行符 | ❽,将数组的大小计数器增加 1,并根据需要将光标移动到 LCD 的另一行。如果用户按下 ESC,程序会清除 LCD,将数据保存到 SD 卡,并通过调用 saveText(true); 停止操作。所有其他按键都会作为普通文本插入到文本数组 ❾ 中,然后显示在 LCD 上,同时程序将大小计数器加 1。
然后,程序会检查文本数组的大小。如果数组的大小大于或等于 99 个字符,程序会通过调用 saveText(false); ❿ 将文本数组写入文本文件,写入后返回正常的文本捕获状态,并将计数器重置为 0。最后,程序会更新光标位置,检查文本是否超出边距,并在必要时重新定位光标。
作为最终挑战,你可以为第 40 号项目添加一个实时钟表,并在文件关闭时加上数据时间戳。
继续前进
许多项目的用户界面表现不佳;正如你在这一章中看到的,使用 PS/2 键盘简化了数据输入的过程。在这一章中,你学习了如何将键盘数据捕获到 Arduino 中,并如何保存键盘输入的数据。希望本章能为你改善自己项目的配置或操作提供一些灵感。
在下一章中,我将向你展示如何利用蓝牙模块,让智能手机与其他设备通过无线通信与 Arduino 项目进行连接。
第十四章:14 通过蓝牙控制 Arduino

大多数现代智能手机、个人电脑或平板电脑可以通过蓝牙与其他设备无线通信。在本章中,你将使用廉价的蓝牙模块,并配合 Windows、Android 或 Apple 设备来控制你的 Arduino 项目。
你将学习如何:
-
使用电平转换板与串口蓝牙接口模块进行电压转换
-
将蓝牙模块与基于 Android、Apple 和 Windows 的设备配对
-
编辑串口蓝牙模块的参数
你还将通过蓝牙向 Arduino 项目发送和接收数据,然后使用 MIT App Inventor 构建自己的 Android 应用程序来远程控制你的项目。
蓝牙模块
市面上有许多类型的蓝牙模块。本章依赖于 HC-05 型蓝牙模块,例如 PMD Way 部件 590526,如图 14-1 所示。

图 14-1:一个 HC-05 蓝牙模块
市面上其他模块,如 HC-06,不适用于本章中的示例和项目,因为它们只能发送数据,无法接收数据。这些模块使用 TX 和 RX 线进行串口通信,可以像往常一样与 SoftwareSerial 库一起使用。数据传输速率默认为 9,600 bps。稍后我会在本章中解释如何设置。
将模块物理连接到 Arduino 项目非常简单,这得益于模块末端的标准针脚间距,而且针脚通常会在模块背面标注。它们都使用 5V DC 电源供应。然而,通信针脚使用较低的逻辑电压 3.3V DC。将 5V 转换为 3.3V 的最佳方法是使用电平转换模块,我将在接下来讲解。
电平转换模块
许多设备的通信引脚设计为工作在 3.3V 电压下,而不是通常的 5V DC。这可能用于串口通信,比如蓝牙模块,或者当使用其他数据总线如 SPI 或 I²C 时。然而,这些引脚与 5V 设备(如 Arduino Uno 或兼容设备)不兼容,因此你需要使用电压转换板在 3.3V 和 5V 之间转换信号。
为此,你可以使用一个电平转换模块,例如 图 14-2 中所示的 PMD Way 部件 441079。

图 14-2:一个电平转换模块
大多数电平转换模块都包括内联针脚,您需要将其焊接到电路板上。不同的电平转换器有不同数量的通道;我们需要至少两个通道来连接蓝牙设备,一个用于串行 TX,另一个用于串行 RX。
所有电平转换器都需要连接到 5V 和 3.3V 电源才能工作。只需将低电压设备连接到电平转换器的 LV 通道端,然后将 Arduino 连接到匹配的 HV 通道端。我将在本章的项目中解释如何使用电平转换器。
在将蓝牙模块与 PC 或其他设备一起使用之前,您需要将蓝牙模块与该设备配对,方法与配对蓝牙耳机或扬声器相同。为了以后的参考,并在本章进行操作时,以下部分提供了与 Windows PC、Android 设备和 macOS 计算机配对模块的说明,您可以根据项目需要进行配对。请注意,当成功与这些设备中的任何一个配对时,蓝牙模块上的 LED 会慢慢闪烁。
与 Windows 设备配对
将蓝牙模块与 Windows 机器配对的步骤与其他蓝牙设备(如耳机或扬声器)相似。首先,确保模块已连接到 5V 和 GND 引脚以供电。一旦供电连接,模块上的 LED 将快速闪烁,表示模块与设备之间没有蓝牙连接。
在使用基于 Windows 的 PC 时,只需从设置中的“蓝牙和设备”页面搜索新的蓝牙设备。选择列表中的 HC-05 设备,系统会提示您输入 PIN 码。输入1234,点击连接,然后就可以开始使用了。
然后,您可以通过 COM 端口地址向蓝牙模块发送和接收数据。这些地址可以在 Windows 设备管理器中找到。
要继续,请跳到第 228 页的“通过蓝牙发送数据”部分。
与 macOS 设备配对
从 Apple 菜单打开“系统偏好设置”窗口,然后选择蓝牙。确保蓝牙已打开,这样计算机就会开始搜索附近的设备。
在连接选项中找到 HC-05 并点击连接。片刻之后,您应该会看到一个密码错误提示;这没关系。点击选项,输入 PIN 码1234,然后点击连接。
片刻之后,您的 Mac 应该会与蓝牙模块配对。
要继续,请跳到第 228 页的“通过蓝牙发送数据”部分。
与 Android 设备配对
将蓝牙模块与 Android 配对的步骤与任何其他蓝牙设备(如耳机或扬声器)的步骤相似。打开 Android 设备上的蓝牙,搜索新设备,然后点击HC-05的列表。系统会提示你输入 PIN 码,输入1234以配对模块。
现在你已经将模块与 Android 或计算机配对,我将向你展示如何通过蓝牙从 Arduino 发送数据。
通过蓝牙发送数据
蓝牙模块是一个串行设备,你可以使用 SoftwareSerial 库轻松地发送数据。让我们现在进行测试。
你将需要以下部件来完成这个项目:
-
一个 Arduino Uno 或兼容板和 USB 电缆
-
一个无焊面包板
-
一个电平转换模块
-
各种跳线
-
一个 HC-05 蓝牙模块
按照图 14-3 中的示意图组装电路。

图 14-3:通过电平转换器将蓝牙模块连接到 Arduino 的原理图
现在输入并上传列表 14-1。
❶ #define PIN_BT_TX 3
#define PIN_BT_RX 2
#include <SoftwareSerial.h>
SoftwareSerial BT(PIN_BT_RX, PIN_BT_TX);
void setup()
{
// Set the data rate for the SoftwareSerial port:
❷ BT.begin(9600);
// Send test message to other device:
}
void loop()
{
BT.print("millis = ");
BT.println(millis());
delay(500);
}
列表 14-1:测试蓝牙输出
Sketch 操作很简单。由于蓝牙模块是串行数据设备,因此 Sketch 将其配置为使用在 D2 和 D3 引脚上定义的软件串行端口❶。接着,Sketch 启动软件串行端口❷,并使用 print() 和 println() 函数通过模块发送数据。当模块与另一设备建立数据连接时,LED 每五秒闪烁两次。
现在你需要选择一种接收通过蓝牙从 Arduino 发送的数据的方法。我将展示如何在 PC、macOS 计算机和 Android 设备上进行此操作。请根据你可用的平台按照说明操作。
连接 Windows 设备
要在支持蓝牙的 PC 上接收 Arduino 数据,你可以使用任何可以通过 COM: 端口进行通信的终端软件。在本章中,我建议使用 Roger Meier 开发的 CoolTerm 软件,可以从http://
安装 CoolTerm 后,使用“选项”菜单选择合适的 COM: 端口并点击连接。几秒钟后,来自 Arduino 的数据应当会滚动显示在窗口中,如图 14-4 所示。

图 14-4:PC 终端上列表 14-1 的示例输出
现在你可以在 Windows 设备上接收 Arduino 数据,跳到项目 #41,开始使用此功能。
到 macOS 设备
要在支持蓝牙的 Mac 上接收数据,你可以使用任何能够通过 COM 端口进行通信的终端软件。在本章中,我建议使用由 Roger Meier 提供的 CoolTerm 软件,可以从http://
安装 CoolTerm 后,在尝试运行软件时,可能会出现 macOS 安全错误。为了解决这个问题,找到 CoolTerm 图标,右键点击并选择打开;你可能需要在 macOS 系统偏好设置的隐私与安全选项卡中点击仍然打开。之后,每次使用时,CoolTerm 都应该能够正常运行。
一旦解决了错误,来自 Arduino 的数据应开始在终端窗口中滚动,如图 14-5 所示。

图 14-5:在 macOS 计算机上显示列表 14-1 的示例输出
接下来,进入项目 #41,开始使用此功能。
到 Android 设备
在你的 Android 设备上,找到 Google Play 商店中的 Bluetooth Terminal HC-05 应用并下载。安装并打开终端应用后,它应该会提示你选择已配对的蓝牙设备,选择HC-05。连接完成后,终端应用窗口应显示来自 Arduino 的millis()值。图 14-6 展示了这个过程。

图 14-6:在 Android 设备上显示列表 14-1 的示例输出
现在你可以在 Android 设备上接收 Arduino 数据,继续进入项目 #41,开始使用此功能。
在接下来的项目中,你将使用你的新能力,在设备上查看来自 Arduino 的数据。
项目 #41:实时监控数据采集
在这个项目中,使用在第十章中介绍的 BMP180 温度和气压传感器,你将构建一个系统,通过你刚设置的 Arduino 和蓝牙模块来监控数据。如果你需要在移动中保持实时监控,可以使用智能手机进行此类数据采集,或者使用运行 CoolTerm 的 PC 进行记录和分析。如果你有这两种设备,也可以尝试同时使用。
你将需要以下部件来完成此项目:
-
一个 Arduino Uno 或兼容的开发板和 USB 线
-
一个无焊面包板
-
一个电平转换模块
-
各种跳线
-
一个 HC-05 蓝牙模块
-
一个 BMP180 温度和气压传感器板
接下来,按照图 14-7 所示组装电路。

图 14-7:项目 #41 的原理图
现在输入并上传项目 #41 草图。接下来,在你的 Android 设备或 Windows 或 Mac 机器上打开终端应用,并按照上一节的说明连接到蓝牙模块。你应该开始看到显示的温度和气压,如图 14-8 所示。

图 14-8:项目 #41 的示例输出
让我们看看这个是如何工作的:
// Project #41 - Capturing Data via Bluetooth
❶ #define PIN_BT_TX 3
#define PIN_BT_RX 2
#include <SoftwareSerial.h>
#include "Wire.h"
#include "Adafruit_BMP085.h"
SoftwareSerial BT(PIN_BT_RX, PIN_BT_TX);
Adafruit_BMP085 bmp;
❷ int temperature;
int pressure;
void setup()
{
// Set the data rate for the SoftwareSerial port:
❸ BT.begin(9600);
bmp.begin();
}
void loop()
{
❹ BT.print("Temperature = ");
temperature = bmp.readTemperature();
BT.print(temperature);
BT.print(" degrees C");
BT.print(" Pressure = ");
❺ BT.print(int(bmp.readPressure() / 100));
BT.println(" hPa");
delay(1000);
}
这个草图结合了从 BMP180 读取数据、整齐地格式化数据并将其发送到蓝牙模块。草图包含并配置了所需的库 ❶,然后声明了两个变量 ❷ 来存储温度和气压数据。它启动了软件串行连接和 BMP180 传感器 ❸,并发送温度数据 ❹。来自 BMP180 的压力读数被从帕斯卡转换为百帕 ❺,然后也发送出去。
现在数据通过蓝牙发送,你可以使用 CoolTerm 软件将其捕获到文本文件中。如果你的设备已连接到项目,请断开连接,然后按照上一节的说明连接你的 macOS 或 Windows 电脑。打开 CoolTerm 并连接。数据应显示在终端窗口中,如图 14-9 所示。

图 14-9:PC 上显示的示例输出
按下 CTRL-R 打开“捕获到文本文件”对话框,输入文件名,选择保存位置,然后点击保存。Arduino 的所有数据应该会写入文本文件中。完成后,按下 CTRL-SHIFT-R 停止并最终保存文本文件。
现在,你可以在像 Microsoft Excel 这样的电子表格软件中打开文本文件,如图 14-10 所示,在导入过程中使用空格作为分隔符。你的数据现在已经准备好进行分析。

图 14-10:Excel 中捕获的数据
使用从 Arduino 发送的任何数据的便利性将取决于它如何格式化以进行传输。不过,现在你有了无线捕获任何 Arduino 项目生成的数据并将其转换为用户友好格式的选项。
此外,借助智能手机和终端应用或配备蓝牙的笔记本电脑,你可以创建一个步进式监控系统来监视各种设备,而无需花费资金构建本地数据的显示系统。只需将你的计算机或 Android 手机带入蓝牙 Arduino 项目的范围内并连接到该项目,即可监控数据读数。通过这种技术,你可以创建只自己知晓的安全数据传输,通过自己的设备监控数据,而不是让任何人都能看到显示内容。
在下一个项目中,你将学习如何通过创建一个蓝牙远程控制项目,将数据发送到 Arduino 和其他设备之间。
项目 #42:使用蓝牙进行数字 I/O 控制
像任何外部串口设备一样,蓝牙模块可以同时发送和接收数据,允许你将数据发送到 Arduino 用于各种目的。通过这个项目,你将制作一个简单的远程控制器,用于控制连接到四个数字引脚的 LED,这将为你自己的无线远程控制项目提供框架。
你将需要以下零件来完成此项目:
-
一块 Arduino Uno 或兼容板和 USB 数据线
-
一个无焊接面包板
-
一个电平转换模块
-
各种跳线
-
一个 HC-05 蓝牙模块
-
四个 LED
-
四个 1 kΩ、0.25 W、1% 的电阻器
按照 图 14-11 所示组装电路。完成此项目后,请保持电路组装好,因为接下来的项目中你将再次使用它。

图 14-11:项目 #42 的原理图
输入并上传项目 #42 的草图,然后在你的 Android 设备或计算机上打开终端应用并连接到蓝牙模块。通过终端应用发送一个问号 (?),Arduino 应该会快速响应一个控制概述。
你还可以发送 0、1、2 或 3 来分别开启数字引脚 D8 到 D11。要关闭它们,分别发送 4、5、6 或 7。每个命令发送后,Arduino 会返回确认信息。图 14-12 显示了在 Android 终端应用中输出的内容。

图 14-12:从安卓手机控制项目 #42
和之前的项目一样,你可以使用配备蓝牙的计算机进行相同的控制方法,如 图 14-13 所示。

图 14-13:从 PC 控制项目 #42
让我们看看这是如何工作的:
// Project 42 - Digital I/O control with Android and Bluetooth
❶ #define PIN_BT_TX 3
#define PIN_BT_RX 2
#include <SoftwareSerial.h>
SoftwareSerial BT(PIN_BT_RX, PIN_BT_TX);
void sendHelp()
{
BT.println("Send 0~3 to turn on D8~11");
BT.println("Send 4~7 to turn off D8~11");
}
void setup()
{
// Set digital pin to control as an output:
❷ DDRB = B111111;
// Set the data rate for the SoftwareSerial port:
❸ BT.begin(9600);
// Send test message to other device:
BT.println("Hello from Arduino");
}
void loop()
{
❹ char a; // Stores incoming character from other device
if (BT.available())
{
❺ a = (BT.read());
if (a == '0') {digitalWrite(8, HIGH); BT.println("D8 on"); }
if (a == '1') {digitalWrite(9, HIGH); BT.println("D9 on"); }
if (a == '2') {digitalWrite(10, HIGH); BT.println("D10 on");}
if (a == '3') {digitalWrite(11, HIGH); BT.println("D11 on");}
if (a == '4') {digitalWrite(8, LOW); BT.println("D8 off"); }
if (a == '5') {digitalWrite(9, LOW); BT.println("D9 off"); }
if (a == '6') {digitalWrite(10, LOW); BT.println("D10 off");}
if (a == '7') {digitalWrite(11, LOW); BT.println("D11 off");}
❻ if (a == '?') {sendHelp();}
}
}
蓝牙模块的软件串口首先进行设置 ❶。自定义函数 sendHelp() 在用户发送问号给 Arduino 时发送指令。草图设置了 D13 到 D8 的数字输出引脚为输出 ❷,并启动软件串口连接 ❸。它还声明了一个字符变量,用于存储从蓝牙接收到的命令 ❹。
当从蓝牙模块接收到一个字符时,它会被存储到字符变量 a 中 ❺;一系列的 if 函数将字符转换为所需的操作。除了打开或关闭数字引脚,还会发送一条消息确认命令已执行。如果 Arduino 收到一个问号,草图会调用 sendHelp() 函数 ❻,该函数告诉用户如何操作项目。
你可以在自己未来的工作中使用本项目的远程控制框架,通过使用 Android 手机或计算机上的蓝牙而不是控制面板,将控制保持在公众视野之外。你还可以修改本项目,以控制 第八章 中描述的继电器板,具体见项目 #26。
你现在将创建自己的智能手机应用来控制这个项目,以提高其用户友好性。
项目 #43:使用 Android 应用进行蓝牙控制
用 Android 设备上的终端应用控制 Arduino 的数字输出对你来说很简单,作为创建者。然而,当其他人需要控制输出时,定制的 Android 智能手机应用更为理想,因为它可以防止最终用户发生误操作。为了避免从零开始编写这样的应用代码,在本项目中,我将向你展示如何使用免费的 MIT App Inventor —— 由 Google 和麻省理工学院提供的在线服务 —— 创建你自己的 Android 应用来控制项目 #42 中的硬件。
注意
你可以在 Windows、macOS 或 Linux 上使用 MIT App Inventor 网站,但生成的应用仅能在 Android 设备上运行。
图 14-14 显示了完整应用界面的样式,展示了连接 Android 手机或平板电脑到项目的蓝牙按钮,并且可以打开或关闭 Arduino 的 D8 到 D11 的数字输出。

图 14-14:完成的 Android 应用,控制项目 #42
构建自己的应用程序过程分为两个主要阶段。第一阶段涉及应用程序的设计,例如按钮和文本在显示屏上的位置。第二阶段则是确定应用程序在响应用户输入或与硬件交互时执行的操作。使用 MIT App Inventor,每个阶段都包括将项目拖放到适当位置以构建应用界面和操作指令的过程。我将向你展示如何导航这个过程来创建你的应用。
该项目的硬件与项目#42 相同。设置完该项目的电路后,请访问 MIT App Inventor 网站:https://
一旦登录并同意服务条款后,欢迎屏幕应该会出现。点击开始一个空白项目并在提示时输入项目名称。我把它命名为 Project42,但你可以选择任何你喜欢的名字。这也将是你稍后下载到 Android 设备上的应用名称。请注意,你可以像使用其他编辑器一样,通过屏幕顶部的“项目”菜单保存和加载进度。
现在你的屏幕应该像图 14-15 中的那样,显示一个空白的手机屏幕和各种菜单。

图 14-15:控制项目#42 的 Android 应用
你将从上到下开始构建界面,从标题文本开始。显示屏的左侧是调色板。点击用户界面,它会显示各种界面选项。将鼠标指针悬停在标签上,然后点击并将其拖动到手机显示屏中,如图 14-16 所示。

图 14-16:构建标签
要编辑标签,点击手机右侧组件窗口中的Label1条目。这将允许你使用屏幕右侧的属性窗口编辑标签的属性,如图 14-17 所示。

图 14-17:标签属性
打开FontBold,将字体大小设置为28,并在文本框中输入Bluetooth GPIO Control。这些更改应该立即生效,并反映在屏幕中央的手机显示中,如图 14-18 所示。

图 14-18:完成的标签
请注意,当你向手机显示屏中添加一个项目时,它会出现在右侧的组件列表中。点击该列表中的项目,你可以在最右侧的属性框中编辑其属性。稍后你会用到这个功能。
接下来,你将开始将按钮添加到应用程序中。第一个按钮将激活手机上的蓝牙连接菜单。为了添加这个按钮,从网页左侧的用户界面列表中点击并拖动ListPicker到手机显示屏中。然后点击组件列表中的ListPicker,并在右侧编辑其属性。将文本字段更改为Connect to BT。此时你的按钮应该与图 14-19 中显示的按钮类似。

图 14-19:完成的按钮
现在是时候确保所有内容在显示屏中居中。点击组件列表中的Screen1,并将 AlignHorizontal 设置更改为Center。显示屏上的项目现在应该已经居中对齐,如图 14-20 所示。

图 14-20:项目现在居中显示。
接下来,你将创建一个由八个按钮组成的表格,每个按钮控制一个数字输出引脚的开关。点击调色板中的Layout列表,然后将TableArrangement拖动到显示屏中。这将在连接按钮下方留下一个框,如图 14-21 所示。同时将 TableArrangement 属性中的行数改为四。

图 14-21:表格已经插入。
现在来处理按钮:从左侧的用户界面列表中点击并拖动八个按钮到手机显示屏中的灰色表格中。借助 TableArrangement,你可以轻松地将它们排列成四行两列,如图 14-22 所示。

图 14-22:正在操作按钮
点击组件列表中的Button2,并将文本更改为D8 On。逐个修改其他七个按钮的文本属性,直到它们与图 14-23 中的按钮一致。

图 14-23:按钮已经完成。
为了使按钮在后续的应用开发步骤中更易于操作,请在组件窗口中重命名按钮:依次点击每个按钮,点击 重命名,并将按钮的名称更改为其功能。例如,将 Button2 的名称更改为 D8 On,如图 14-24 所示。

图 14-24:编辑按钮引用
接下来,我们需要添加蓝牙引用。这是应用和手机蓝牙硬件之间的虚拟链接。在调色板中点击 连接性 标签,将 BluetoothClient 拖动到主窗口,并将其放置在手机上方。蓝牙图标应出现在手机下方,如图 14-25 所示。

图 14-25:蓝牙已添加到应用设计中
最后,你可以通过编辑组件菜单中的 Screen1 项来编辑应用顶部的文本,或者删除它。要将应用名称更改为更有用或更有趣的名称,请编辑属性窗口底部出现的标题字段,或者如果不想显示应用的标题,请取消选中 TitleVisible 选项框。
到目前为止,你已经在设计器窗口中创建了应用的设计和界面。现在,你将开始应用设计的第二阶段:确定应用在用户交互时将执行的操作。首先,点击显示界面右上角的 块 按钮,如图 14-26 所示。

图 14-26:切换到块编辑器
在 MIT App Inventor 中,设计器界面用于构建应用的用户界面,而块页面则用于构建定义应用工作方式的代码。你可以使用块编辑器页面,通过拖放各种“块”来形成指令序列或基于用户输入的活动,而不需要编写代码。每个块代表一种指令类型、一个函数或一项活动。你可以通过在 设计器 和 块 按钮之间切换,来在这两个设计界面之间移动。
现在,我将向你展示如何创建所需的块结构,以实现应用的操作。在块编辑器页面中,点击 ListPicker1,在块菜单中将出现一系列块,其中一些会在图 14-27 中显示。

图 14-27:各种代码块
从左侧的Blocks菜单中拖放when ListPicker1 BeforePicking块到查看窗口。再次点击 Blocks 菜单中的ListPicker1,然后将set ListPicker1 elements to块拖放到前一步中的when块中。它应该会点击到when块中。
在这个过程中,若要删除不需要的块,只需将其拖动到查看窗口右下角的垃圾桶图标上。
接下来,点击 Blocks 菜单中的BluetoothClient1项目,并将BluetoothClient1 AddressesAndNames项目拖到并点击到最后添加的项目。
你现在需要创建一组指令来连接到用户所选的蓝牙设备。将块拼接在一起,你可以在 Blocks 菜单中的 Control 部分找到if then块。
现在你需要为每个按钮配置操作。为了与项目#42 控制 LED 的方式一致,你希望应用程序发送 0 到 3 来依次打开 D8 到 D11,并发送 4 到 7 来关闭它们。点击D8按钮,在 Blocks 菜单中的 TableArrangement1 列表里,然后将when D8_On.click块拖放到查看窗口中。
从 BluetoothClient1 列表中拖放call BluetoothClient1.SendText text块到when块中。
要完成按钮操作组,点击 Blocks 菜单中的Math项目,然后将第一个块(仅包含一个 0)拖入text旁边的凹槽中。
这一组块会在用户按下按钮 D8 时激活;这将通过蓝牙发送文本字符 0。
你需要创建另外七个这样的组,确保总共创建八个,以匹配按钮。为了节省时间,你可以右键点击一个组中的第一个块并选择Duplicate来重复你正在编辑的组。其他组必须引用不同的按钮。点击D8_ON,应该会显示所有可用按钮的下拉列表,如图 14-28 所示。

图 14-28:按钮组选择
确保你已在每个八个按钮组中更改了设置,使其与八个按钮匹配,并随后更改其发送的文本字符的值。也就是说,按下 D8 关闭时应发送 4,按下 D9 开启时应发送 1,依此类推。你只需点击每个组中的 0 并输入新的值。完成后,你的屏幕应与图 14-29 中显示的内容相似。

图 14-29:完成的控制块组
您的应用现在已完成,并准备安装到 Android 手机或平板上。使用项目菜单保存您的项目(以防以后需要更改),然后在构建菜单中选择Android 应用(.apk)。App Inventor 将编译指令,经过一两分钟后,提供下载按钮和二维码,如图 14-30 所示。

图 14-30:下载您的应用!
然后,您可以下载并保存应用的.apk文件,将其加载到您的设备上,或者直接使用设备的摄像头扫描二维码。出现提示时,访问从图像中解码的 URL 并按照下载和安装提示操作。点击是或同意,接受 Android 可能弹出的任何对话框。
最终,您的应用应该会弹出。将其拖到主屏幕上,如图 14-31 所示。

图 14-31:您的应用已安装在手机上
恭喜!您已经制作了一个 Android 应用。现在进行测试:确保您的第 42 号项目硬件正确连接,然后打开应用。开启设备的蓝牙功能,点击应用中的连接到 BT按钮。您应该会看到一份已配对蓝牙设备的列表。选择 HC-05 设备,应用将返回到主页面。
现在,您应该能够按下每个按钮来打开或关闭 Arduino 上的数字输出引脚,LED 灯将反映 Arduino 的状态。如果应用无法正常工作,首先返回第 42 号项目,确保硬件可以通过终端应用或支持蓝牙的 PC 控制。如果还是不行,检查 App Inventor 中的应用设置。您还可以在书籍网页上的可下载文件中,下载项目#43文件夹中的 AI 项目文件,https://
这个简单的示例展示了如何使用 Android App Inventor 创建具有专业用户界面的远程控制项目。如果您愿意,可以花更多时间在 App Inventor 上进行实验。我相信您会自己想出一些有趣的使用案例。
让我们通过修改蓝牙模块学习另一种保持项目用户友好的方式。
更新模块名称和 PIN
现在你已经构建了一个 Android 应用,最后一步是更新模块的名称和 PIN/密码以增强安全性,因为每个设备通常从工厂出厂时 PIN 为 1234。为了实现这一点,你必须将模块的操作模式更改为 AT 命令模式,这样你就可以发送 AT 命令来更新和查看各种参数(就像你在 Arduino 的蜂窝模块上所做的那样)。
要切换到 AT 命令模式,将模块按 图 14-3 所示连接到 Arduino,然后将 3.3 V 接到蓝牙模块的 EN 引脚。此额外连接仅在修改参数时需要——正常使用时,请移除或断开此连接。
接下来,输入并上传 Listing 14-2 中的代码,它可以实现 Arduino 串口监视器与蓝牙模块之间的通信。
#define PIN_BT_TX 3
#define PIN_BT_RX 2
#include <SoftwareSerial.h>
❶ SoftwareSerial BT(PIN_BT_RX, PIN_BT_TX);
void setup()
{
❷ BT.begin(38400);
❸ Serial.begin(9600);
}
void loop()
{
if (BT.available())
Serial.write(BT.read());
if (Serial.available())
BT.write(Serial.read());
}
Listing 14-2: 配置 HC-05 蓝牙模块
这段代码首先建立了 Arduino 和蓝牙模块之间的串口连接 ❶。蓝牙模块在 AT 模式下的串口速度设置为 38,400 波特率 ❷。PC 与 Arduino 之间的串口连接保持在 9,600 波特率 ❸。模块的 LED 应该缓慢闪烁,表示 AT 命令模式正在工作。
打开串口监视器,并确保在速度旁边的菜单中选择了 NL(表示“新行”)和 CR(表示“回车”)。输入 AT 并按 发送。蓝牙模块应在串口监视器中回复 OK。如果没有回复,请仔细检查线路连接,并确保 LED 正在缓慢闪烁。
就像网络设备一样,每个蓝牙模块都有一个唯一的媒体访问控制(MAC)地址。在串口监视器中发送 AT+ADDR?,它应返回模块的 MAC 地址。
我的设备返回的 MAC 地址是 00:21:06:08:30:BF。要更改四位数的配对 PIN,发送 AT+PSWD =xxxx,其中 xxxx 是新的四位 PIN,例如 8675。然后,你可以通过在蓝牙菜单中忘记该设备并重新搜索它来测试新的 PIN。
要将蓝牙名称从 HC-05 更改为更具描述性的名称,请发送 AT+NAME =NewName,其中 NewName 是您希望模块在操作时传输的名称。例如,如果您正在构建一个使用继电器在车库中操作设备的项目,您可以通过发送 AT+NAME =GarageRelay 将其名称更改为 GarageRelay。新名称应该会在配对时出现在设备的蓝牙屏幕上,如图 14-32 所示。

图 14-32:更改模块名称的结果
当在同一地区处理多个蓝牙设备项目时,更改模块名称非常有用,这样可以轻松地跟踪您希望配对的设备。如果您希望将模块的名称和 PIN 重置为 HC-05 和 1234,请发送 AT + ORGL 来恢复出厂默认设置。您可能会在项目临时使用或者将模块转交给他人时执行此操作。
继续前进
在本章中,您学习了几种将串行蓝牙模块与 Arduino 项目结合使用的方法。您现在可以使用蓝牙进行无线遥控,捕获 Arduino 数据并通过蓝牙传输到计算机,以及为 Android 设备构建自己的遥控应用程序。在未来,您可以将这些技能应用于许多领域,例如遥控继电器板、在难以触及的地方记录传感器数据,或者制作定制的遥控圣诞灯。
在下一章中,您将学习如何减少 Arduino 的功耗。
第十五章:15 便携项目的能效

基于 Arduino 的项目通常由交流电适配器或 USB 电源供电,而不太考虑功耗。然而,对于便携式项目来说,减少能量消耗以延长电池寿命非常重要,从而获得更长的运行时间。这包括便携游戏或没有外部电源的传感器和数据记录项目。
本章介绍了提高你 Arduino 项目电源效率的各种方法。你将学习如何:
-
测量你的 Arduino 项目的功耗
-
估算项目电池寿命
-
通过软件和硬件方法减少 Arduino 功耗
-
构建低功耗的数据记录设备
你还将学习一些与 Arduino 配合使用的有用工具和方法,如适用于 DS3231 型时钟 IC 模块的 Arduino 库、声音传感器模块以及用于将程序上传到 Arduino 板和兼容电路的 USBasp 设备。
电学基本原理
便携项目通常都以某种形式由电池供电。你使用的电量越少,项目在电池充电或更换之间的持续时间就越长。本章旨在教你多种方法,以使你的项目在没有有线电源的情况下能更长时间运行。在继续之前,让我们回顾一些基本的电学原理,确保你具备测量功耗和确定电池寿命的概念基础,便于后续章节的学习。
简单来说,电力是一种我们可以利用并转化为热能、光能、运动和功率的能量形式。电力有三个主要特性:电流、电压和功率。
电流
电能通过电路的流动被称为电流。电流从电源的正极流向负极,例如从电池的正极到负极。这被称为直流 (DC)。在本书中,我们不会涉及交流电 (AC)。
在一些电路中,负极称为接地 (GND)。电流以安培 (A)为单位测量,也叫做安。较小的电流以毫安 (mA)为单位测量,1,000 毫安等于 1 安培。
电压
电压是电路正负端之间潜在能量差的度量。它以伏特 (V)为单位进行测量。电压越大,电流通过电路的速度就越快。
功率
功率是电气设备将能量从一种形式转换为另一种形式的速率的度量。功率以瓦特 (W)为单位进行测量。例如,100 瓦的白炽灯比 60 瓦的灯更亮,因为更高瓦数的灯泡将更多的电能转化为光能。
电压、电流和功率之间存在简单的数学关系:
功率 (W) = 电压 (V) × 电流 (A)
您将在本章稍后的计算中使用此公式。接下来,我将向您介绍功耗的类型及如何测量这些功率。
测量功耗
确定您的 Arduino 项目在特定时刻使用多少功率的方法取决于电源如何连接到您的 Arduino。为了计算本章后面示例和项目中的功耗,您需要能够选择一种适合当前项目的功率测量方法。在本节中,我将向您展示各种测量 Arduino 项目功耗的方法。
USB 电源
如果您的项目通过 USB 电源供电,最简单的测量功耗的方法是使用诸如 USB Doctor 这样的在线设备,如图 15-1 所示。USB Doctor 是一个小型设备,可以测量通过 USB 电缆传输的电压和电流。

图 15-1:USB Doctor,一种在线电源测量设备
USB Doctor 设备在线连接在 Arduino 与电源(例如 PC、墙壁适配器或电池组)之间,它会在数字显示屏上交替显示当前使用的电流和电压。主要关注的值是电流,因为您将使用它来计算电池寿命。
有线电源
如果一个项目有有线功耗,那意味着它通过 Arduino 的 DC 插座或直接连接到电路板上的 5V 和 GND 引脚来连接电源。要测量此类项目的功耗,可以使用具有电流设置的万用表。
与测量电压不同,测量电流需要电源通过电流插孔进入万用表,并通过 COM 插孔退出。要快速确定您的万用表是否具有此功能(大多数都有),请检查它是否有标有 mA 或 A 的插孔。
例如,考虑图 15-2 中的万用表。从最左边的万用表开始,依次使用 A mA 和 COM 插孔;10A 和 COM 插孔;20A 和 COM 插孔;以及 10A mA 和 COM 插孔。

图 15-2:具有电流测量能力的万用表
如果你看到 A 型插座和 mA 型插座,首先使用安培(A)插座。如果显示的值在毫安(mA)设置范围内,你可以改用那个插座和范围来获得更精确的测量。
别忘了将万用表设置为与你使用的插座相匹配的电流模式。如果有疑问,请查看万用表的说明书。一旦将万用表设置为电流范围,探针需要连接到电源电路中。
如果你使用的是直流插座电源,测量的简单方法是使用一对直流端子连接器,例如 PMD Way 部件 116101M 和 116101F,图示见图 15-3。然后,你可以在两个单元之间接入电线,并用你的表笔进行测量。如果万用表返回负值,交换表笔以改变极性。

图 15-3:直流端子连接器
然而,如果你直接连接到 Arduino 的 5V 和 GND 引脚,你可以很容易地在电源正端和 Arduino 的 5V 引脚之间进行探测。
Arduino 电源消耗
现在你已经了解了可用的测量工具类型,我将给你一个关于消耗的基准概念。我们来看看我用万用表从标准的 Arduino Uno R3 板上测量的电流消耗值,板上 D13 LED 已开启。然后你可以跟着做,确定你自己项目的电流消耗。
表 15-1 列出了以毫安为单位的电流及用于为 Arduino 提供电力的电源。当你使用前面描述的方法测量你自己项目的电流消耗时,根据你使用的 Arduino 或兼容板的品牌,测量值可能会与你的略有不同。
表 15-1: 不同供电电压下的 Arduino 电流消耗
| 电源 | 电流消耗(毫安) |
|---|---|
| 12 V 通过直流插座 | 47.9 |
| 9 V 通过直流插座 | 47.9 |
| 5 V 通过 5V 引脚或 USB 插座 | 44.5 |
| 4.8 V 通过 5V 引脚 | 42.5 |
请记住,这些数值仅适用于 Arduino 本身。无论你在项目中添加什么,都将增加电流消耗。我在这个示例中测量的是 4.8V 下的电流,因为这是使用四个 AA 充电电池(它们是 1.2V,而不是 1.5V)或四个几乎耗尽寿命的 AA 一次性电池时常见的电压。
Arduino 可以在这些较低的电压下正常工作。然而,如果你使用微控制器的内置 ADC 来测量模拟信号,你需要考虑降低的工作电压,因为 ADC 的参考电压假设为 5V,而不是实际的工作电压。相反,使用外部参考电压引脚和analogReference()函数。如果你不熟悉这个过程,你可以在《Arduino 工作坊》第二版的第四章中学习更多。
现在你对 Arduino 在没有任何外部部件情况下的常规功耗有所了解,我将向你展示如何根据电池容量和项目电流消耗来估算电池寿命。
估算电池寿命
要确定你项目的电池寿命,你需要确定项目在一段时间内(以毫安时为单位)消耗的电流。首先,测量你的项目从连接的电源吸取的电流。一旦你得到了电流消耗值,例如 65 mA,你的项目应该在一小时内消耗这个电流。这样,你的消耗就是毫安时(mAh)。然后,你可以认为消耗是 65 mAh。
如果 Arduino 激活外部设备,例如电机或伺服电机,别忘了考虑额外的电流消耗。例如,如果你的项目每小时运行 1 分钟的电机,测量电机开启和关闭时的电流,然后确定一小时内总共消耗的电流。例如,如果项目以恒定速度运行电机 10 分钟,电流测量为 100 mA,而项目运行了 50 分钟,电流为 60 mAh,那么消耗量为 100 mA × (10 / 60 分钟) + 60 mA × (50 / 60 分钟) = 66.6 mAh。
接下来,考虑你的项目使用的电池类型。例如,如果你有一个 20 Ah(安培时,通常标示为 20,000 mAh)的 USB 电池银行,如图 15-4 所示,你的项目应该能持续大约 300 小时,因为 20,000 / 66.6 = 300.3003。如果你的电池的额定值是 Ah,将该值乘以 1000 即可转换为 mAh。

图 15-4:典型的 USB 电池银行
有些电池组在电流消耗不足时会关闭,以节省电能。选择那些具有常开模式的电池组,这样它们就不会在 Arduino 的功耗降到非常低时自动关闭。
除了电池组,另一种常见且简单的为便携项目供电的方法是使用镍氢 (NiMH) AA 可充电电池,通常是四颗一组。这提供了 4.8 V DC 的电压,同样可以很好地为 Arduino 供电。你可以使用四节 AA 电池座来固定这些电池,如图 Figure 15-5 所示,然后将电池座直接接线到 Arduino 的 5V 和 GND 引脚。
每个 NiMH 电池应标明其容量,如 2000 mAh。使用四颗全新并充满电的电池,在 66.6 mA 的功耗下,你的项目大约可以运行 30 小时。
如果你只能使用一次性 AA 电池,可以将一个像常见的 1N4001 二极管串联在电池组的正极和 Arduino 的 5V 引脚之间。阳极连接到电池组的正极输出,阴极连接到 Arduino 的 5V 引脚。这样应该能将新电池组的电压从约 6 V DC 降到约 5.3 V DC,这是 Arduino 可以正常使用的电压。

Figure 15-5:四节 AA 电池座
像 NiMH 可充电电池和一次性电池这样的电池在整个生命周期内并不提供恒定的电压——相反,电压会在放电周期的末期下降。为了让事情更复杂,一次性电池的使用寿命可能会根据每次所用的电流量有所不同。例如,图 Figure 15-6 显示了从 Energizer 品牌的碱性 AA 电池放出的电流越多,其可用容量就越小。

Figure 15-6:一次性 Energizer 品牌碱性 AA 电池的容量与放电率关系图
为了获得最准确的电池寿命估算,找到并查看你感兴趣的电池类型的数据表。这些数据表提供了放电率图、不同温度下的表现以及更多有趣的信息,帮助你做出明智的决策。
现在你已经知道如何测量项目的功耗,并能在多种便携电源选项中做出选择,我将解释一些减少功耗的方法。你将在接下来的项目中运用这些方法。
基于软件的方法来减少功耗
要在你的 Arduino 上使用低功耗功能,你需要安装一个库。打开 IDE 的库管理器并搜索 low-power rocket,然后安装 Rocket Scream Electronics 提供的 Low-Power 库,如 图 15-7 所示。

图 15-7:低功耗库安装
在接下来的部分中,我将向你展示两种使用该库来减少功耗的方法。你将首先在以下作业中练习编程电源关闭/唤醒模式,然后继续测试中断唤醒模式。本章中的示例和项目使用的是 Arduino Uno 或兼容的 ATmega328 微控制器板。
电源关闭/唤醒周期模式
电源关闭/唤醒周期模式在你需要让 Arduino 定期执行某个任务,并在任务完成后停止耗电时非常有用——例如,唤醒并检查某个状态,或记录数据然后返回休眠状态。要使用此模式,请在你的草图中包含以下库:
#include "LowPower.h"
你可以使用此函数通过 ADC_OFF 和 BOD_OFF 参数来关闭 ADC 和微控制器的欠压检测,如下所示。当你希望 Arduino 在启用所有省电功能的情况下进入睡眠状态时,请在你的草图中使用以下函数:
LowPower.powerDown (SLEEP_8S, ADC_OFF, BOD_OFF);
该函数有几个参数,第一个是空闲的持续时间。在这个示例中,我将持续时间设置为八秒,使用了 SLEEP_8S。然而,你也可以使用其他延迟选项之一:
SLEEP_15MS 15 毫秒
SLEEP_30MS 30 毫秒
SLEEP_60MS 60 毫秒
SLEEP_120MS 120 毫秒
SLEEP_250MS 250 毫秒
SLEEP_500MS 500 毫秒
SLEEP_1S 1 秒
SLEEP_2S 2 秒
SLEEP_4S 4 秒
SLEEP_8S 8 秒
以下简单的草图使用此函数使 Arduino 休眠 8 秒,然后重复再次休眠,然后以满功率运行 16 秒。这让你能够快速检查在满功率和省电模式下的功耗,使用万用表进行测量。
将你的 Arduino Uno 连接到 PC,并上传列表 15-1。上传完成后,如前面章节所述,连接你的 USB 电源监视器或万用表,以观察功耗变化。
❶ #include "LowPower.h"
void setup() {}
void loop()
{
// Idle for 16 seconds
❷ LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
// Operate at normal power for 16 seconds
❸ delay(16000);
}
列表 15-1:测试断电/唤醒周期模式
草图包含了库❶,然后运行空闲函数两次,所有参数设置为OFF,并有八秒的延迟❷。接下来,草图引入另一个延迟,在此期间,Arduino 以全功率运行❸。
如果你测量这个电路的电流,功耗应该从全功率下的大约 42 mA 下降到省电模式下的大约 30 mA,功耗下降约 12 mA。这意味着,当你的项目处于省电模式时,功耗减少了 28%。
我在这个例子中引用这些数值为“大约 42 mA”等等,因为实际功耗会在引用的数值上下波动 1 到 2 mA,具体取决于你使用的 Arduino 或兼容板的品牌。它也可能因你使用的测试设备不同而有所变化。为了简化,我使用了整数和平均功耗。
在实际示例中使用省电功能之前,我将介绍一个方便的库用于 DS3231 实时钟 IC,后续项目中你将会使用到它。
DS3231 实时钟库
DS3231 实时钟模块提供了一种方便且准确的方式来追踪时间和日期信息,尽管使用它们确实需要编写大量代码。在本节中,我将向你展示如何通过使用 DS3231 库以更简单的方式读写时间和日期数据。
首先,打开 Arduino IDE,进入库管理器,搜索Andrew Wickert 的 DS3231库。
将实时钟模块连接到你的 Arduino Uno,如图 15-8 所示,然后使用 USB 线将 Uno 连接到 PC。

图 15-8:DS3231 与 Arduino Uno 之间的连接
接下来,输入但不要上传列表 15-2。
❶ #include <DS3231.h>
#include <Wire.h>
DS3231 RTCmodule;
❷ bool century = false;
bool h12Flag = false;
bool pmFlag = false;
int hh, mm, ss, dd, dow, mo, yy;
void setTime()
{
❸ RTCmodule.setClockMode(false); // Set to 24-hour time
RTCmodule.setYear(`22`);
RTCmodule.setMonth(`1`);
RTCmodule.setDate(`28`);
RTCmodule.setDoW(`6`); // Sunday = 1
RTCmodule.setHour(`14`);
RTCmodule.setMinute(`5`);
RTCmodule.setSecond(`30`);
}
void showTime()
{
// Get data from RTC.
❹ dd = RTCmodule.getDate();
dow = RTCmodule.getDoW();
mo = RTCmodule.getMonth(century);
yy = RTCmodule.getYear();
hh = RTCmodule.getHour(h12Flag, pmFlag);
mm = RTCmodule.getMinute();
ss = RTCmodule.getSecond();
// Send information to Serial Monitor
❺ switch(dow)
{
case 1: Serial.print("Sunday "); break;
case 2: Serial.print("Monday "); break;
case 3: Serial.print("Tuesday "); break;
case 4: Serial.print("Wednesday "); break;
case 5: Serial.print("Thursday "); break;
case 6: Serial.print("Friday "); break;
case 7: Serial.print("Saturday "); break;
}
Serial.print(dd, DEC);
Serial.print("/");
Serial.print(mo, DEC);
Serial.print("/");
Serial.print(yy, DEC);
Serial.print(" | ");
Serial.print(hh, DEC);
Serial.print(":");
if (mm < 10) // Check for leading 0 on minutes
{
Serial.print("0");
}
Serial.print(mm, DEC);
Serial.print(":");
if (ss < 10) // Check for leading 0 on seconds
{
Serial.print("0");
}
Serial.println(ss, DEC);
}
void setup()
{
Serial.begin(9600);
Wire.begin();
❻ // setTime();
}
void loop()
{
showTime();
delay(1000);
}
列表 15-2:测试 DS3231 库
该草图包含并初始化了所需的库 ❶,然后声明了所需的全局变量 ❷。这些变量用于存储 RTC 数据和设置。setTime() 函数用于设置时间和日期到 RTC。该草图使用 false ❸ 表示 24 小时制,但你也可以输入 true 来使用 12 小时制。否则,输入时间和日期数据时使用一位或两位数字格式。setDoW 的值代表星期几的数字,1 表示星期日,7 表示星期六。
为了从 RTC 获取数据并在串口监视器上显示,showTime() 函数依次将 RTC 中的数据存入变量 ❹,然后开始在串口监视器上显示这些数据 ❺,首先使用 switch…case 函数来显示星期几。
通过取消注释 ❻ 处的函数并更新 setTime() 函数中的数据,以匹配你上传草图时的当前时间和日期来设置时间。上传草图后,再次注释掉这一行,以免每次重置 Arduino 时都重置时间。
最后,… 常规的时钟显示。串口监视器将输出你当前的时间和日期。
在接下来的项目中,你将使用 DS3231 库。
项目 #44:创建低功耗周期性数据记录器
在这个项目中,你将创建一个数据记录器,它每分钟左右将温度、湿度以及时间和日期记录到 SD 卡中。在这些时间段之间,Arduino 将进入空闲模式以节省电源。这种类型的项目适用于长期数据记录,例如,记录一季或几个月的天气数据。
对于此项目,你将需要以下硬件:
-
一块 Arduino Uno 或兼容的开发板和 USB 数据线
-
一块免焊面包板
-
各种跳线
-
一块 BMP180 温度和气压传感器模块
-
一块 DS3231 实时时钟模块
-
一块 SD 或 microSD 卡模块和匹配的内存卡
按照 图 15-9 中的示意图组装电路。

图 15-9:项目 #44 的电路原理图
接下来,输入并上传以下草图:
// Project #44 - Periodic low-power data logging
❶ #include <SD.h>
#include <LowPower.h>
#include <DS3231.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>
Adafruit_BMP085 bmp;
DS3231 RTCmodule;
❷ bool century = false;
bool h12Flag = false;
bool pmFlag = false;
int hh, mm, ss, dd, mo, yy, temperature, pressure;
void setup()
{
delay(3000); // Allow time for SD card to be inserted
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");
Wire.begin();
bmp.begin();
}
void setTime()
{
// Set to 24-hour time
RTCmodule.setClockMode(false);
RTCmodule.setYear(`2022`);
RTCmodule.setMonth(`1`);
RTCmodule.setDate(`11`);
RTCmodule.setHour(`17`);
RTCmodule.setMinute(`11`);
RTCmodule.setSecond(`8`);
}
void logData()
{
// Create the file for writing:
File dataFile = SD.open("DATA.TXT", FILE_WRITE);
// If the file is ready, write to it:
if (dataFile)
{
// Get data from RTC:
dd = RTCmodule.getDate();
mo = RTCmodule.getMonth(century);
yy = RTCmodule.getYear();
hh = RTCmodule.getHour(h12Flag, pmFlag);
mm = RTCmodule.getMinute();
ss = RTCmodule.getSecond();
// Write time and date to SD card:
dataFile.print(dd, DEC);
dataFile.print("/");
dataFile.print(mo, DEC);
dataFile.print("/");
dataFile.print(yy, DEC);
dataFile.print(" | ");
dataFile.print(hh, DEC);
dataFile.print(":");
if (mm < 10) // Check for leading 0 on minutes
{
dataFile.print("0");
}
dataFile.print(mm, DEC);
dataFile.print(":");
if (ss < 10) // Check for leading 0 on seconds
{
dataFile.print("0");
}
dataFile.print(ss, DEC);
dataFile.print(" | ");
// Write temperature data:
temperature = bmp.readTemperature();
pressure = int(bmp.readPressure() / 100);
dataFile.print(temperature, DEC);
dataFile.print(" C - ");
dataFile.print(pressure, DEC);
dataFile.println(" hPa");
dataFile.close();
}
}
void loop()
{
❹ // setTime(); // Used to set time in RTC if necessary
for (int snooze = 0; snooze<8; snooze++)
// Idle for 64 seconds
{
❺ LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);
}
logData();
}
一如既往,草图包含并初始化了所需的库 ❶,并声明了用于存储 RTC 数据和设置的全局变量 ❷。它初始化并测试了 SD 存储卡读取器 ❸,然后使用 setTime() 函数将时间和日期设置到 RTC 中。如同 Listing 15-2 所示,草图使用 false 来表示 24 小时制,但你可以将其更改为 true 来使用 12 小时制。务必在 RTCmodule 函数中替换为当前你所在位置的时间。
logData() 函数在调用时会将日期、时间、温度和压力写入 SD 存储卡。通过取消注释❹处的函数来设置 DS3231 中的时间和日期,以便你在 setTime() 函数中所做的更改生效。上传草图后,再次注释这一行,以避免每次 Arduino 重启时时间被重置。
最后,for 循环将执行 8 秒关机操作 8 次,总计 64 秒;然后 Arduino 会醒来并记录数据 ❺。此时,草图会再次循环,将 Arduino 置于关机状态。
图 15-10 展示了输出文本文件的示例。你的日期和时间当然会有所不同。

图 15-10:项目#44 的示例输出
如果你测量这个电路的电流,功率全开时消耗应为大约 51 mA,而当项目关闭电源时,消耗大约降至 38 mA。在 Arduino 写入数据到 SD 卡时,消耗会出现短暂的跳跃。撇开这一点不谈,整个项目的功率消耗大约减少了 10 mA。(Arduino 本身的功率消耗减少,但不会影响其连接的设备。)
这可能看起来节省不多,但考虑到在关机模式下,这相当于节省了 20%的功耗,而关机模式占据了 97%到 98%的工作时间。此外,当你从电池供电时,任何节省都是值得的。
中断唤醒模式
还有一种基于软件的节能方式:中断唤醒模式。使用这种模式会让 Arduino 进入睡眠状态,直到触发中断,导致 Arduino 醒来并继续运行草图。完成必要任务后,它会再次进入睡眠状态,等待另一个中断。
注意
有关中断的更多信息,请查看《Arduino 工作坊》第二版的第七章。
要使用此模式,在草图中包含以下代码行来引用库:
#include "LowPower.h"
由于每个 Arduino 中断都会调用一个函数,因此你需要定义一个函数以使草图能够编译。然而,由于在中断触发时不需要调用任何特定代码,可以将该函数留空:
void wakeUp() {}
你还必须在void setup()中将硬件中断引脚声明为输入,使用 D2 或 D3 引脚。
在草图的主部分,使用以下代码附加中断:
attachInterrupt(pinNumber, interruptFunction, `mode`);
将pinNumber替换为 0 以连接到数字引脚 D2,或者替换为 1 以连接到数字引脚 D3。
在attachInterrupt()函数中,将mode替换为以下四种中断类型之一:
LOW 没有电流施加到中断引脚。
CHANGE 电流发生变化,无论是从打开到关闭,还是从关闭到打开。
RISING 电流从关闭变为打开,电压为 5V。
FALLING 电流从 5V 的打开状态变为关闭状态。
Arduino 中断函数需要一个在中断触发后调用的函数,但直接从附加到中断的函数中运行代码会导致递归循环。为了避免这种情况,我们使用一个空白的(也叫做占位)自定义函数——即一个没有任何代码的函数。例如,你可以使用以下代码:
void interruptFunction() {}
接下来,当你想让 Arduino 进入睡眠状态时,使用以下代码行:
LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
在中断触发 Arduino 唤醒后,使用以下函数将中断从数字引脚分离:
detachInterrupt(0);
接下来,运行代码或调用所需的函数进行操作。此操作完成后,Arduino 将返回睡眠状态。
我将在以下简单示例中演示这一点,其中 D2 引脚的信号从 HIGH 变为 LOW,Arduino 的板载 LED 闪烁一次,然后 Arduino 返回睡眠状态。将你的 Arduino Uno 连接到 PC,从 5V 引脚跳线连接到 D2 引脚,并上传清单 15-3。连接你的 USB 功率监视器或万用表观察功耗变化。
❶ #include <LowPower.h>
❷ void interruptFunction(){}
void setup()
{
❸ pinMode(2, INPUT);
❹ pinMode(13, OUTPUT); // For demo LED
}
void loop()
{
// start using interrupt pin
❺ attachInterrupt(0, interruptFunction, FALLING);
// Enter power down state with ADC and BOD module disabled.
// Wake up when D2 is low.
❻ LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
// Disable external pin interrupt on wake up pin
// Not using interrupt anymore
❼ detachInterrupt(0);
// Do something here when interrupt triggered
❽ digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
}
清单 15-3:在中断模式下唤醒
草图包含了低功耗库❶,接着是一个空的函数,正如中断函数❷所要求的。然后,它将用于中断的数字引脚设置为输入❸,并配置了 Arduino 的内建 LED❹。
程序还将中断类型❺配置到 D2 引脚,并定义空白中断函数和触发中断的事件类型。在这种情况下,事件类型为FALLING,意味着 D2 引脚的信号从 5V 变化为 0V。
一旦中断被配置,程序就会让 Arduino 进入休眠状态❻。在中断被触发之前,什么也不会发生,直到 Arduino 进入全功率模式。这时,程序会禁用中断,以避免在程序其余部分运行时再次触发中断❼。
在此时,程序会打开和关闭板载 LED,演示 Arduino 正常运行❽,但如果你愿意,也可以在这里替换为其他功能。一旦代码执行完毕,void loop()会返回到起始位置,重新激活中断❺,并将 Arduino 重新置于休眠状态❻。
你可以通过将跳线从 5V 引脚移除到 D2 来测试这个项目,这将触发中断并将 Arduino 恢复到全功率模式。然后恢复跳线,LED 激活并关闭后,Arduino 应该会重新进入休眠模式。在这个例子中,功耗从 Arduino 唤醒时约 42 mA 降到休眠模式时的 28 mA,减少了大约 59%。
在下一个项目中,你将使用这种技术进行更严肃的应用,通过记录由声音传感器检测到的噪声事件。
廉价的声音传感器
当你构建一个必须检测响声的项目时,可以使用可调声音级传感器,例如 PMD Way 部件 596692,如图 15-11 所示。

图 15-11:一个可调声音传感器
这些传感器可以在大多数零售商处找到,具有两种输出类型:模拟和数字。在下面的项目中,你将使用该传感器的数字模式。将 5V 和 GND 分别连接到传感器的+和−(或 G)引脚时,当环境声音达到某一水平时,数字输出(DO)引脚会发送 5V 信号。当声音达到阈值时,LED 会亮起,你可以使用传感器上的可调电位器来调整此阈值。
项目#45:记录中断触发
本项目的目标是记录声音事件的日期和时间,例如嘈杂的邻居或周围环境中的强烈震动。它使用声音传感器模块触发中断,将 Arduino 从低功耗模式唤醒,记录日期和时间到 SD 卡中,然后再次让 Arduino 休眠,直到下一个事件。如果你不想记录声音事件,你可以使用任何具有数字输出的传感器或开关。
对于这个项目,你将需要以下硬件:
-
一个 Arduino Uno 或兼容板以及 USB 数据线
-
一个无焊接面包板
-
各种跳线
-
一个声音传感器模块
-
一个 DS3231 实时时钟模块
-
一个 SD 或 microSD 卡模块和匹配的存储卡
按照 图 15-12 中的示意图组装电路。

图 15-12:项目 #45 的电路原理图
接下来,输入并上传以下草图:
// Project #45 - Interrupt-triggered logging with reduced power consumption
#include <SD.h>
#include <LowPower.h>
#include <DS3231.h>
#include <Wire.h>
DS3231 RTCmodule;
bool century = false;
bool h12Flag = false;
bool pmFlag = false;
int hh, mm, ss, dd, mo, yy;
void setup()
{
delay(3000); // Allow time for SD card to be inserted:
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!");
Wire.begin();
pinMode(13, OUTPUT);
}
void interruptFunction() {}
void setTimeData()
// Use once to set the time and date in the RTC module:
{
RTCmodule.setClockMode(false); // Set to 24-hour time
RTCmodule.setYear(2022);
RTCmodule.setMonth(1);
RTCmodule.setDate(11);
RTCmodule.setDoW(2);
RTCmodule.setHour(17);
RTCmodule.setMinute(11);
RTCmodule.setSecond(8);
}
void logData()
{
// Create the file for writing:
File dataFile = SD.open("DATA.TXT", FILE_WRITE);
// If the file is ready, write to it:
if (dataFile)
{
// Get data from RTC:
dd = RTCmodule.getDate();
mo = RTCmodule.getMonth(century);
yy = RTCmodule.getYear();
hh = RTCmodule.getHour(h12Flag, pmFlag);
mm = RTCmodule.getMinute();
ss = RTCmodule.getSecond();
// Write time and date to SD card:
dataFile.print(dd, DEC);
dataFile.print("/");
dataFile.print(mo, DEC);
dataFile.print("/");
dataFile.print(yy, DEC);
dataFile.print(" | ");
dataFile.print(hh, DEC);
dataFile.print(":");
if (mm < 10) // Check for leading 0 on minutes
{
dataFile.print("0");
}
dataFile.print(mm, DEC);
dataFile.print(":");
if (ss < 10) // Check for leading 0 on seconds
{
dataFile.print("0");
}
dataFile.print(ss, DEC);
dataFile.println(".");
// Close the file once the system has finished with it
dataFile.close();
}
}
void loop()
{
❶ attachInterrupt(0, interruptFunction, RISING);
❷ LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);
❸ detachInterrupt(0);
❹ logData();
}
到现在为止,你应该理解了这个草图中的初始代码,这些代码用于设置低功耗库、实时时钟和 SD 存储卡。代码的最后几行配置了中断为 RISING ❶,因此当 Arduino 的 D2 引脚的电压从 0 V 升高到 5 V(声音传感器的数字输出)时,草图会告诉 Arduino 关闭电源 ❷。一旦传感器被触发,草图就会取消低功耗模式并禁用中断 ❸,以防止递归中断。最后,草图将声音事件的时间和日期记录到 SD 卡 ❹,然后草图会再次激活中断 ❶ 并关闭 Arduino 的电源,为下一轮做准备 ❷。
该项目的功耗大约是当 Arduino 处于休眠状态时为 28 mA,而在项目向 SD 卡记录数据的短暂期间,功耗大约为 60 mA。这意味着该项目在休眠模式下节省了约 50% 的功耗。
基于硬件的方法来减少功耗
你还可以通过对 Arduino 进行硬件修改来减少功耗。Arduino 开发平台最初设计时考虑到了易用性。虽然标准的 Arduino Uno 型电路很好地实现了这个目的,但更高级的用户可以通过移除许多对于许多项目来说不必要的组件来提高功效。
如果以下部分的元件在你的项目中不会被使用,可以将其移除:
-
四个 LED(电源、D13、TX 和 RX)
-
用于板载 5 V 和 3.3 V 电源的线性电压调节器电路(如果你用 5 V DC 给 Arduino 供电)
-
一个比较器电路,用于决定是否使用 USB 或 DC 插座电源(如果你用 5 V DC 给 Arduino 供电)
-
一个 USB 到串口接口的微控制器和电路
-
如果不需要,可以移除重置按钮
如果你准备制作自己的 Arduino 兼容电路并提供合适的电源,那么让你的项目比标准的 Arduino Uno 消耗更少的能量是很容易的。为了演示这一点,我将向你展示如何制作两种版本的极简主义 Arduino 兼容电路,一种在 5 V 下工作,另一种在 3.3 V 下工作。你可以在无焊接的面包板或原型板上构建这些电路,然后围绕它们构建项目。
要构建这些项目,你首先需要知道如何使用 ICSP 引脚(而非 USB 接口)将程序上传到 Arduino 和极简电路。
使用 ICSP 引脚上传程序
到目前为止,你很可能一直在使用 Arduino 或兼容板上的 USB 连接上传程序。然而,你可以通过使用内电路串行编程器(ICSP)连接到微控制器来减少电路的使用,从而使你的项目更节能。
ICSP 是你 Arduino 板右侧的六针连接器,如图 15-13 所示。在不上传程序时,你还可以将其作为连接 SPI 数据总线的另一种方式。

图 15-13: Arduino Uno 上的 ICSP 接头引脚
这六个引脚按固定的配置排列,如图 15-14 所示。

图 15-14: ICSP 接头引脚布局
引脚 1 通常在 PCB 上标记为数字 1 或靠近左上角引脚的小点,正如你在图 15-13 中看到的那样。要将程序上传到电路中,你需要一个外部编程器,它连接到这些引脚,称为 USBasp 设备。这些开源设备由 Thomas Fischl 创建,仅在与 Windows 系统的 PC 一起使用时才需要 USB 驱动程序。除了帮助你降低功耗外,USBasp 还允许你在 Arduino Uno 或兼容板的 USB 接口损坏或被破坏的情况下上传程序。
注意
如果你有兴趣在没有 Arduino 环境的情况下编程 AVR 微控制器,USBasp 也很有用,正如我在我的书《AVR 工作坊》(No Starch Press, 2022)中所描述的那样。
USBasp 在外观上有所不同,但功能相同。图 15-15 展示了两个例子。图中上方的编程器是来自供应商 PMD Way 的通用设备,位于书中的配件清单上,而下方的编程器则来自澳大利亚的 Freetronics。

图 15-15: USBasp 编程器示例
在购买 USBasp 编程器时,确保购买带有 6 针(而非 10 针)排线的型号。Windows 用户应咨询供应商,了解如何安装编程器。Linux 和 Mac 用户只需将其插入 USB 插口,USBasp 很快就能准备好使用。
快速测试你的 USBasp,确保它工作正常,然后再继续构建下一节中你将搭建的简约电路。首先将它连接到你的 Arduino Uno 或兼容板,如图 15-16 所示。

图 15-16:USBasp 编程器连接到 Arduino Uno
USBasp 为 Arduino 提供电源,因此你不需要连接其他电源。接下来,通过附带的 USB 数据线将 USBasp 连接到你的 PCB。打开 Arduino IDE 中的 Blink 示例程序,然后选择 工具
编程器
USBasp。
最后,要上传程序,选择 程序
通过编程器上传。
代码应该上传完成,板载 LED 应该按预期闪烁,确认你的 USBasp 工作正常。如果是这样,你就准备好构建一个简约的低功耗 5 V Arduino 电路了。
项目 #46:构建一个简约的 5 V Arduino 电路
在这个项目中,你将构建一个尽可能简约的 Arduino 兼容电路,能够以大约 5 V DC 的电压运行。你可以在此模板的基础上扩展,构建你未来的低功耗 Arduino 兼容项目。
你需要以下硬件:
-
一个 USBasp 编程器和匹配的 USB 数据线
-
一个无焊接面包板
-
各种跳线
-
一个 ATmega328P-PU 微控制器
-
一个 0.1 µF 聚酯电容器(C1)
-
两个 22 pF 陶瓷电容(C2,C3)
-
一个 16 MHz 的 HC49S 晶体
-
一个 ICSP 面包板适配器
-
一个 560 Ω,0.25 W,1% 的电阻
-
一个 LED
按照图 15-17 所示的电路进行组装。尽量将晶体、C2 和 C3 放得尽可能靠近微控制器。

图 15-17:项目 #46 的原理图
现在上传以下代码:
// Project #46 - Blink LED on D7
void setup()
{
pinMode(7, OUTPUT);
}
void loop()
{
digitalWrite(7, HIGH);
delay(1000);
digitalWrite(7, LOW);
delay(1000);
}
连接到微控制器第 13 引脚的 LED 应该按预期每秒闪烁一次,开与关交替。你现在已经有了一个工作正常的简约 Arduino 电路示例,可以用于实验;如果你想将其永久纳入项目中,可以将其焊接到条形板或自定义的 PCB 上。电阻和 LED 仅用于演示目的,因此如果未来的项目中不需要它们,当然可以将其去除。
这个电路的目的是减少功耗。在去除 LED 和电阻后,我测得电流大约为 22 mA,比标准的 Uno 板要好得多,后者通常需要大约 44 mA。
这是一个很好的开始,但你可以通过前面章节中学到的基于软件的方法进一步降低功耗。你需要一台能够测量微安级电流的万用表来测量项目运行时的功耗。当我将这个电路与 Listing 15-1 中的 Power Down/Wake Periodic 模式草图配对使用时,当 Arduino 关闭电源时,电流大约为 3 μA(微安,每个微安等于 1,000 毫安)。在使用这个电路试验 Listing 15-3 时,也返回了大约 3 μA 的测量值。结合硬件和软件方法的电池供电项目可以持续非常长的时间。
对于将来使用这种简约电路的项目,你需要知道微控制器的哪个引脚与 Arduino 草图中的哪个引脚相关联,因为 Arduino 板上的引脚编号与微控制器上的引脚编号并不匹配。正常的 Arduino 板上的所有模拟、数字及其他引脚在面包板版本中也可用;你只需直接连接到微控制器。
R2 和 LED2 在你的面包板 Arduino 的数字引脚 13 上。表 15-2 列出了左侧为 Arduino 引脚,右侧为匹配的 ATmega328P-PU 引脚。
表 15-2: ATmega328P-PU 引脚
| Arduino 引脚名称 | ATmega328P-PU 引脚 |
|---|---|
| RST | 1 |
| RX/D0 | 2 |
| TX/D1 | 3 |
| D2 | 4 |
| D3 | 5 |
| D4 | 6 |
| (仅 5V) | 7 |
| GND | 8 |
| D5 | 11 |
| D6 | 12 |
| D7 | 13 |
| D8 | 14 |
| D9 | 15 |
| D10 | 16 |
| D11 | 17 |
| D12 | 18 |
| D13 | 19 |
| (仅限 5 V) | 20 |
| AREF | 21 |
| GND | 22 |
| A0 | 23 |
| A1 | 24 |
| A2 | 25 |
| A3 | 26 |
| A4 | 27 |
| A5 | 28 |
为避免混淆,像 Freetronics 这样的零售商提供可粘贴标签,可以贴在微控制器上,如图 15-18 所示(访问 Freetronics 首页并搜索 labels)。

图 15-18:Arduino 引脚标签,适用于快速原型制作
使用这些标签是确保在匆忙时不会搞错引脚布局的好方法。
继续前进
在本章中,你学习了如何测量你的 Arduino 使用多少电量,以及如何使用各种硬件和软件方法来最小化功耗,从而使得电池供电的项目比原本可以持续的时间更长。你还学习了如何使用一些新的传感器和定时模块,以及 USBasp 编程器。你现在可以创建一些可以长时间从电池中获取电力的项目,例如在远程地区监测季节性天气数据,或者在没有市电的区域检测附近的运动。
在下一章中,我将向你展示如何使用汽车的 CAN 数据总线将你的汽车与 Arduino 连接,以监控和记录车辆引擎数据等信息。
第十六章:16 通过 CAN 总线监控汽车电子设备

汽车使用复杂的电子网络来监控操作并控制各种功能,包括发动机管理、温度测量以及窗户和锁的控制。这些电子设备通过控制器局域网络数据总线(CAN 总线)相互通信。
在本章中,你将学习如何将 Arduino 与 CAN 总线接口,监控来自某些汽车电子设备的数据。你将设置并测试 Arduino 与 CAN 总线的数据连接,然后监控并记录实时的发动机统计数据以便后续分析。
CAN 总线
你可能熟悉 SPI 和 I²C 数据总线。CAN 总线是一种用于将发动机管理计算机与现代汽车中其他设备连接的数据总线。它减少了设备之间的物理布线,并允许你将外部设备(如计算机、扫描工具或 Arduino)连接到汽车进行操作监控。
例如,汽车的发动机管理计算机会读取发动机和制动系统中的传感器数据。通过 CAN 总线,它会将速度、发动机转速、温度和其他数据广播到控制仪表盘显示的计算机系统。当你按下遥控器上的按钮锁车门时,无线电数据收发器接收到遥控器的信号,然后通过 CAN 总线指示中央锁定控制单元锁住所有车门。
用于 CAN 总线通信的协议称为车载诊断 II(OBDII),这是 OBD 协议的第二代。你可以使用 Arduino 和匹配的 CAN 总线扩展板与总线进行通信,以查看汽车统计数据。
在进一步操作之前,如果你打算制作本章的项目,请检查你的汽车是否具备 CAN 总线和 OBDII 连接器,因为一些较老的汽车可能没有。为此,你可以在车内仪表盘下方查看连接器的位置;例如,我的三菱欧蓝德运动版/ASX 的连接器位于踏板上方,如图 16-1 所示。如果你最初找不到 OBDII 连接器,你可能需要查阅制造商提供的手册或维修手册。

图 16-1:OBDII 连接器插槽
当你准备将电缆连接到 OBDII 连接器时,一只手稳稳地握住插槽,另一只手将插头插入。插槽通常没有太多支撑,如果不小心,可能会破坏支架。
所需的 CAN 总线硬件
本章的项目需要一个 Arduino 扩展板,如 图 16-2 所示的 PMD Way 部件 13479987。无论选择哪个扩展板,它必须使用 Microchip MCP2515 CAN 控制器和 MCP2551 CAN 收发器芯片。

图 16-2: 一款 Arduino 用的 CAN 总线扩展板
接下来,你需要一根适配的电缆,将车辆的 OBDII 端口与扩展板上的九针插口连接,例如 图 16-3 所示的 PMD Way 部件 717211。

图 16-3: 一根 OBDII 到扩展板的电缆
最后,你需要一种方式为 Arduino 提供电源。如果你没有一台电池续航足够支持一个小时左右使用的笔记本电脑,考虑购买一个 USB 电源银行,如 图 16-4 所示。

图 16-4: 一款 USB 电源银行
你也可以使用 USB 电缆和汽车 USB 适配器为你的项目供电,如 图 16-5 所示。

图 16-5: 一款汽车 USB 电源适配器
OBDII 连接可能因品牌和车型不同而有所差异,因此在购买本章所需硬件之前,建议查阅爱好者论坛,获取更多关于你车辆的信息。例如,三菱车主可以访问 https://
一旦你有了所需的硬件,下载 CAN 总线库的 ZIP 文件,链接为 https://
Include Library
Add ZIP Library 来安装这个库。
现在你准备好用一个简单的 CAN 总线数据检索程序来测试你的硬件了。
项目 #47: 监控发动机数据
在这个项目中,你将使用 CAN 总线扩展板从汽车的发动机控制单元(ECU)接收基本的发动机数据和速度,并在 Arduino IDE 的串口监视器中显示这些数据。这是检查硬件是否正常运行并增加对汽车操作统计数据熟悉度的好方法。
你的项目将监控以下内容:
-
汽车速度
-
发动机转速(RPM)
-
油门位置
-
油门应用百分比
-
发动机冷却液温度
-
氧气传感器电压
-
质量空气流量(MAF)传感器值
ECU 使用氧传感器的电压来确定发动机排气气体中氧气的含量,这能指示发动机的燃油效率。一旦你的车的 MAF 传感器测量了进入发动机的空气量,ECU 会使用这个数值来确定发动机正常运行所需的正确燃油量。
你需要以下部件来完成这个项目:
-
一块 Arduino Uno 或兼容的开发板以及 USB 电缆
-
一块适用于 Arduino 的 CAN 总线扩展板
-
一根 OBDII 到扩展板的电缆
-
一台笔记本电脑(最好,但不是必需的)
如果你有一台运行 Arduino IDE 的笔记本电脑,你可以使用它来监控项目的输出。否则,你可以在车库中设置一台台式电脑。如果两者都不行,可以跳过这个项目。
要组装这个项目,将 CAN 总线扩展板连接到 Arduino,将 Arduino 连接到计算机,并将 CAN 总线扩展板连接到汽车中的 OBDII 连接器。如果你的 Arduino 开发板有全尺寸的 USB 插座,如 图 16-6 所示,它可以靠在 CAN 总线扩展板的焊接点上。如果是这种情况,请在两者之间放一些纸张,以将 Arduino 与扩展板隔离。

图 16-6:Arduino USB 端口连接到 CAN 总线扩展板
输入并上传草图,然后确保在启动发动机时汽车不会移动。启动发动机,在 Arduino IDE 中打开串口监视器,将数据速率设置为 115,200 波特率。几秒钟后,发动机数据应该会显示出来,如 图 16-7 所示。

图 16-7:项目 #47 的示例输出
在图中,速度大于 0 公里/小时。这是因为我开车带着一个乘客,乘客用笔记本电脑截取了屏幕截图。这个测试还展示了不同车辆之间 OBDII 数据的潜在差异:在 图 16-7 中的结果可以看到,我没有从 ECU 获取氧传感器电压的任何读数。
让我们看看这个是如何工作的:
// Project #47 - Monitoring engine data
❶ #include <Canbus.h>
int requestDelay = 100;
char buffer[456];
void setup()
{
❷ Serial.begin(115200);
// Initialize CAN bus shield with the required speed
❸ if (Canbus.init(CANSPEED_500))
{
Serial.println("CAN initialization ok");
} else
{
Serial.println("CAN initialization error");
while (1) {}
}
delay(2000);
}
void loop()
{
❹ Canbus.ecu_req(VEHICLE_SPEED, buffer);
Serial.print("Speed: ");
Serial.print(buffer);
delay(requestDelay);
Canbus.ecu_req(ENGINE_RPM, buffer);
Serial.print(", RPM: ");
Serial.print(buffer);
delay(requestDelay);
Canbus.ecu_req(THROTTLE, buffer);
Serial.print(", Throttle: ");
Serial.print(buffer);
delay(requestDelay);
Canbus.ecu_req(ENGINE_COOLANT_TEMP, buffer);
Serial.print(", Coolant temp: ");
Serial.print(buffer);
delay(requestDelay);
Canbus.ecu_req(O2_VOLTAGE, buffer);
Serial.print(", o2 Voltage: ");
Serial.print(buffer);
delay(requestDelay);
Canbus.ecu_req(MAF_SENSOR, buffer);
Serial.print(" , MAF sensor: ");
Serial.println(buffer);
delay(requestDelay);
}
该示例代码首先引入 CAN 总线库 ❶,然后创建两个变量。第一个是 requestDelay,用于在从 CAN 总线读取数据后创建延迟。根据你使用的汽车,可能需要调整这个变量的值,但先尝试 100。如果返回的数据类型不匹配,或者你根本看不到数据,你需要尝试更高或更低的值。第二个变量是较大的字符数组 buffer,用于存储通过 CAN 总线接收到的数据,然后显示在串口监视器中。
在 void setup() 中,示例代码初始化串口输出 ❷。然后开始 CAN 总线通信 ❸,同时检查 Arduino 是否能够与 CAN 总线进行通信,使用 if (Canbus.init (CANSPEED_500)) 函数。参数 CANSPEED_500 设置了 CAN 总线与数据之间的传输速率。所需的速度取决于汽车类型;对于我的车辆,合适的速度是 500 kbps。
该示例代码通过 Canbus.ecu_req() 函数在 void loop() 中获取数据。这些函数有两个参数:数据类型和存储数据的变量。例如,示例代码获取并将第一个数据项——速度,存储到 buffer 变量中,并在串口监视器上显示该数据❹。之后会有一个短暂的延迟,接着获取下一个参数并显示,过程会重复。
如果项目无法正常工作,你可能需要更改数据传输速率。你可以将参数更改为 CANSPEED_125 或 CANSPEED_250,分别对应 125 kbps 或 250 kbps,尝试不同的速度,看哪个有效。
使用这个示例代码,你可以学到很多关于你汽车的知识。例如,我发现我的三菱汽车的 ECU 在变速杆处于停车档或空档时,将油门限制在 18%,防止驾驶员不必要地让引擎过度转速。
现在你已经有了一个基本框架来获取 OBDII 数据,接下来你可以学习如何将这些数据保存到 microSD 卡上,供以后查看。请将这个项目的硬件设备保存好,因为你将在下一个项目中继续使用它。
项目 #48:记录 OBDII 汽车数据
在本项目中,您将把从 CAN 总线获取的数据记录到 microSD 卡上,这样可以随着时间的推移分析数据,无论汽车是静止还是行驶中。所需的硬件和组装与上一个项目相同,只是您还需要一张 microSD 卡——使用第九章中所述的类型即可——以及为您的 Arduino 提供电源,如本章开始时所描述的。
输入并上传程序草图,将 microSD 卡插入 CAN 总线扩展板,然后确认如果启动发动机,汽车不会移动。将 Arduino 连接到电源并启动发动机。程序草图应该会将数据提取并记录到 microSD 卡上的名为CANBUS.TXT的文本文件中,每秒生成一个新的数据条目。
如果可能,开车出行一段时间。完成后,断开 Arduino 的电源,将 microSD 卡插入您的 PC 中并打开查看。它应该包含记录的所有数据,存储在名为 CANBUS.TXT 的文件中,如图 16-8 所示。

图 16-8:来自 microSD 卡的示例数据文件内容
在电子表格中打开 CANBUS.TXT,在打开过程中,使用逗号和空格作为分隔符。例如,如果您使用的是 Microsoft Excel,您可以通过在打开文本文件时出现的文本导入向导来完成此操作,如图 16-9 所示。

图 16-9:在 Excel 中打开 CANBUS.TXT 文件
现在您的数据已经是标准的电子表格文件格式,您可以在电子表格软件中打开它,进行进一步的分析,例如查找速度与转速(RPM)或转速与油门位置之间的关系,如图 16-10 所示。

图 16-10:在电子表格中处理示例数据
让我们看看这个程序是如何工作的:
// Project #48 - Logging OBDII car data
❶ #include <Canbus.h>
int requestDelay = 1000;
char buffer[456];
#include <SPI.h>
#include <SD.h>
int chipselect = 9;
void setup()
{
Serial.begin(115200);
// Check uSD card
❷ if (!SD.begin(chipselect))
{
Serial.println("Card failed, or not present");
// Stop
while (1);
}
Serial.println("Micro SD card initialized.");
❸ if (Canbus.init(CANSPEED_500))
{
Serial.println("CAN initialization ok");
} else
{
Serial.println("CAN initialization error");
while(1) {}
}
delay(2000);
}
void loop()
{
// Open file on memory card
❹ File dataFile = SD.open("CANBUS.TXT", FILE_WRITE);
❺ if (dataFile)
{
❻ Canbus.ecu_req(VEHICLE_SPEED, buffer);
❼ dataFile.print(buffer);
delay(requestDelay);
Canbus.ecu_req(ENGINE_RPM, buffer);
dataFile.print(",");
dataFile.print(buffer);
delay(requestDelay);
Canbus.ecu_req(THROTTLE, buffer);
dataFile.print(",");
dataFile.print(buffer);
delay(requestDelay);
Canbus.ecu_req(ENGINE_COOLANT_TEMP, buffer);
dataFile.print(",");
dataFile.print(buffer);
delay(requestDelay);
Canbus.ecu_req(O2_VOLTAGE, buffer);
dataFile.print(",");
dataFile.print(buffer);
delay(requestDelay);
Canbus.ecu_req(MAF_SENSOR, buffer);
dataFile.print(",");
dataFile.println(buffer);
// Close file on memory card
❽ dataFile.close();
delay(1000); // Around 1-second delay between logging data
}
}
程序草图包含所需的 CAN 总线库和变量 ❶,接着是用于读取 microSD 卡的库以及 chipselect 变量。此变量指示 microSD 卡插槽使用的 CS 引脚,在本例中,Arduino 数字引脚 9 的值为 9。在 void setup() 中,程序会检查 microSD 卡子系统是否正常工作 ❷,然后初始化 CAN 总线接口并设置所需的总线速度 ❸。与之前的项目一样,如果程序没有记录任何数据,请尝试更改 requestDelay 的值。
如果 microSD 卡和 CAN 总线初始化成功,程序将进入主循环,并打开 microSD 卡上的文件进行写入❹。如果存在需要写入的文件❺,程序将继续从 ECU 获取第一条数据❻;然后,程序不是将数据发送到串口监视器,而是将其写入 microSD 卡❼。这个过程会对来自 ECU 的其他五条数据重复执行。一旦完成,程序将关闭文件进行写入❽。你可以使用delay()来调整数据记录的时间间隔。
作为挑战,你可以添加一个实时时钟 IC,如流行的 DS3231 系列,用来记录时间和日期信息,以及发动机参数。你还可以添加一个 GPS 模块,记录来自 ECU 和 GPS 的车速,从而比较你车速表的准确性。
继续前进
本章为你提供了监控和记录车辆系统提供的各种信息参数的工具,如车速、发动机转速、油门位置、冷却液温度等,这些信息可以帮助你更深入地了解发动机及其传感器的工作原理。每种车辆都有不同,因此,通过使用你在本章中构建的项目,你可以找到适合自己车辆的数据。
在下一章,你将学习如何使用 RS232 数据总线让 Arduino 与其他设备进行通信。
第十七章:17 ARDUINO 与 RS232 通信

RS232 数据总线是一种常用的有线方式,用于电子设备之间的通信。它最初是在 1960 年代末期发明的,至今仍广泛应用于工业可编程逻辑控制器(PLC)和需要可靠有线点对点数据传输的应用中。它也常用于将计算机与旧设备连接。
本章将向你展示如何将 Arduino 与 RS232 总线进行数据传输和远程控制应用的接口。你将学到:
-
使用开发板或扩展板为你的 Arduino 添加 RS232 接口
-
构建一个通过 RS232 工作的 PC 到 Arduino 的远程控制
-
在两块 Arduino 板之间设置 RS232 通信
你可以将本章中的项目作为框架,用于未来 Arduino 与 RS232 通信的需求。
RS232 总线
RS232 总线最初是为了实现数据终端与调制解调器之间的连接,调制解调器再连接到远程主机和小型计算机。该标准随着时间的推移不断发展,每个版本通过字母后缀进行标注,如 RS232-C。为了本章的目的,我将所有版本的标准统称为 RS232。
尽管 RS232 总线是一项较老的技术,但仍然有许多使用它的理由,包括与旧硬件接口以及在设备之间建立长距离、可靠的有线数据连接,如 Arduino 与 PC 之间。RS232 数据线的长度可以延伸到 15 米,超出此长度时,可靠性可能会成为问题,尽管你可以通过降低数据传输速率来延长超过 15 米的距离。本章中的示例使用 9600 bps,但在你自己的项目中,如果需要,可以使用 Arduino 支持的更低速度。
RS232 是一种串行数据总线,其工作方式与 Arduino 的串行端口(通常位于 D0 和 D1 引脚)类似。基本的 RS232 连接有 TX(发送)和 RX(接收)线路,以及一个共同地线。完整的 RS232 标准还包括用于电话、控制数据开始/停止和设备间发送状态的额外线路,但在本章中你不需要使用这些线路。
尽管 RS232 总线按顺序传送表示 1 和 0 的数据位,但其信号类型不同于 Arduino 使用的 TTL 串行。例如,图 17-1 展示了从 Arduino 串口发送的数据。你可以看到,1(在 RS232 术语中称为标记)是 5V,而 0(空格)是 0V。

图 17-1:来自 Arduino 串口的生存时间(TTL)逻辑数据
现在考虑沿 RS232 发送的相同数据,如图 17-2 所示。RS232 要求标记是负电压,空格是正电压。

图 17-2:来自 RS232 端口的 RS232 逻辑数据
使用相反的电压表示标记和空格可以清晰地定义 2 位数据,避免潜在的混淆,从而提高数据的准确性。每发送一个字节的数据,RS232 会在每 8 位数据的两端包含一个起始位和一个停止位。(在本章后面,我会向你展示如何更改每次传输时发送的位数。)
标记的电压范围可以从 3 V 到 15 V DC,而空格的电压范围则在 −3 V 到 −15 V DC 之间。虽然较长的电缆意味着信号电压会有更大的波动——电缆越长,电压降越大,因为电缆的电阻——但标记和空格之间的宽电压范围意味着 RS232 电缆的长度对信号完整性的影响较小。如果周围的电气环境噪声较大,标记与空格之间发生混淆的几率较小。
RS232 信号所需的增加电压由 TTL 至 RS232 串行转换器 IC(MAX3232)生成,该 IC 存在于 Arduino 的 RS232 扩展板和模块上。该 IC 使用电荷泵电路将 5 V DC 转换为 +/−10 V DC 或更高电压,但电流较小。因此,使用 RS232 和 Arduino 时无需单独的双轨电源。
连接 RS232
有几种简便的方法可以将 Arduino 与设备或 PC 上的 RS232 连接进行接口。无论你使用哪种接口设备,它都会有一个九针连接器用于 RS232,并且有一种方法可以将板载的 MAX3232 IC 与 Arduino 的 TX/RX 串行引脚电气连接起来。
最方便的接口方式是使用 Arduino 的 RS232 扩展板(PMD Way 部件 954042),如图 17-3 所示。该扩展板具有一个方便的开关,用于选择 RS232 使用和 Arduino 与 PC 之间的通信;上传程序时关闭开关,使用 RS232 时打开开关。

图 17-3:Arduino 的 RS232 扩展板
对于 Arduino 的 RS232 扩展板的更紧凑替代方案,特别适用于制作较小的 Arduino 基础电路,是 RS232 至 TTL 模块,如图 17-4 所示。

图 17-4:RS232 至 TTL 模块
本模块通过内联针脚连接到 Arduino:将 V[CC]和 GND 连接到 Arduino 的 5V 和 GND,将模块 TXD 连接到 Arduino 的 RX(D0),将模块 RXD 连接到 Arduino 的 TX(D1)。本章的项目使用 Arduino 的 RS232 扩展板,但你也可以使用本模块来完成相同的项目。
本章中,你还需要一根 RS232 电缆,将 Arduino 与 RS232 硬件连接到设备或计算机。基于 RS232 扩展板和模块上的标准连接器以及标准 PC 的 RS232 插座(如图 17-5 顶部所示),你将需要一根双头九针母头电缆。

图 17-5:显示 RS232 插座的台式电脑背面
如果你使用的是 Mac 或笔记本电脑,或者 PC 没有 RS232 端口,你将需要一根 USB 到 RS232 电缆,比如 PMD Way 的 514539A,如图 17-6 所示。

图 17-6:USB 到 RS232 电缆
你 PC 或 Arduino 扩展板上的 RS232 连接器是九针型,有两排针脚,无论是公头还是母头配置。当使用带有 25 针连接器的旧设备时,你应该能从其他 RS232 设备供应商处购买到像图 17-7 所示的转换插头或电缆。

图 17-7:DB9 到 DB25 串口电缆
一旦你收集了所需的硬件,你就可以开始设置并测试通过 RS232 进行的 PC 到 Arduino 的连接。
通过 USB 测试 Arduino 与 PC 的连接
要测试 Arduino 与 PC 的连接,请将清单 17-1 上传到你的 Arduino 板。
void setup()
{
Serial.begin(9600);
}
void loop()
{
Serial.print("Hello ");
delay(1000);
}
清单 17-1:测试 Arduino 与 PC 的连接
接下来,拔下 Arduino 上的 USB 电缆,然后按照上一节描述连接 RS232 扩展板或模块。重新将 Arduino 连接到 USB 或外部电源,然后打开终端软件。与之前的章节一样,本章的项目使用的是 Roger Meier 提供的免费终端模拟器 CoolTerm,你可以在http://
打开终端软件后,点击选项。你应该会看到一个串口选项菜单,如图 17-8 所示。

图 17-8:CoolTerm 选项菜单
从端口下拉菜单中选择您的串口(例如 COM1),并确保其他串口设置与图中所示相匹配。点击确定关闭菜单,然后点击连接开始通过 RS232 从 Arduino 向 PC 传输数据。PC 应该会不断接收到“Hello”这个词,并在终端窗口中显示,如图 17-9 所示。

图 17-9:列出 17-1 在 CoolTerm 窗口中的结果
您可以使用这个简单的设置,通过 RS232 连接在比使用 USB 电缆更远的距离上捕获来自 Arduino 的数据。在 CoolTerm 中,按 CTRL-R 开始记录接收到的任何输出到文本文件,按 CTRL-SHIFT-R 停止记录。
保持硬件连接;您将在接下来的项目中使用它来实现 PC 到 Arduino 的远程控制,使用 RS232 进行通信。
项目 #49:创建一个 PC 到 Arduino 的远程控制
本项目演示了一个基本框架,通过 RS232 远程控制 Arduino 来控制连接到数字输出的设备、请求传感器的信息,甚至使用 PC 软件通过串口写入的方式将计算机控制加入到 Arduino 项目中。
您将需要以下零件:
-
一块 Arduino Uno 或兼容的板子和 USB 电缆
-
一个用于 Arduino 的 RS232 扩展板或模块
-
一个用于 PC 与 RS232 连接的九针电缆
为了组装这个项目,请按照上一节所述将 Arduino 连接到 RS232 硬件和 PC。上传项目 #49 的草图,打开 CoolTerm 软件,然后点击连接开始通过 RS232 在 Arduino 和 PC 之间进行数据通信。
按0或1键请求 Arduino 的模拟输入 0 和 1 的值,分别按2和3键打开和关闭板载 LED。如果按下其他键,终端应显示错误信息未识别的命令,如图 17-10 所示。

图 17-10:项目 #49 的示例输出
让我们看看它是如何工作的:
// Project #49 - PC-to-Arduino remote control
❶ void doSomething0()
{
Serial.print("A0 value is: ");
Serial.println(analogRead(0));
}
void doSomething1()
{
Serial.print("A1 value is: ");
Serial.println(analogRead(1));
}
void doSomething2()
{
digitalWrite(13, HIGH);
Serial.println("LED On");
}
void doSomething3()
{
digitalWrite(13, LOW);
Serial.println("LED Off");
}
void setup()
{
❷ Serial.begin(9600);
pinMode(13, HIGH);
digitalWrite(13,LOW);
}
void loop()
{
char command;
❸ if(Serial.available()) // If data received by Arduino serial
{
command = Serial.read(); // Save the data
❹ switch (command)
// Act on the received data:
{
case '0': doSomething0(); break;
case '1': doSomething1(); break;
case '2': doSomething2(); break;
case '3': doSomething3(); break;
❺ default: Serial.println("Unrecognized command.");
}
}
}
从❶开始,草图创建了四个自定义函数,用于在接收到匹配命令时调用。然后草图初始化 Arduino 串口 ❷,并将板载 LED 引脚设置为输出并将其关闭。
在主循环中,Arduino 等待通过串口连接从 RS232 接收到一个字符❸,然后使用switch…case函数❹对该字符进行处理。如果该字符不在选择范围内,程序会将错误信息发送回 PC❺。这个示例中的操作仅用于演示,你可以根据自己的需求替换其中的内容。
本项目演示了你可以通过 RS232 连接使用 PC 控制或监视任何 Arduino 操作。你也可以编写自己的定制 PC 软件进行远程控制。任何可以写入 COM:或串口的代码都可以控制 Arduino,而任何能够从串口接收文本的代码都可以对其进行操作。
在下一个项目中,我将展示如何通过 RS232 设置两个 Arduino 之间的通信。由于 RS232 是 Arduino 串行通信的扩展,两个 Arduino 可以通过 RS232 互相通信。然而,所需的 RS232 电缆与普通电缆稍有不同:TX 和 RX 数据线需要交叉连接。也就是说,一端的 TX 针脚需要连接到另一端的 RX 针脚,反之亦然。否则,你将试图连接两个 Arduino 的 TX 针脚,这是不行的。
解决这个问题的方法是使用交叉电缆,或无调制解调器电缆,它具有直接 RS232 到 RS232 设备通信所需的内部接线,示例中使用的是 PMD Way 部件 6045480L15,如图 17-11 所示。

图 17-11:RS232 交叉电缆
如果你只是进行短距离实验,可以直接使用跳线在两个 RS232 保护板或 Arduino 模块之间连接:只需将一侧的 TX 和 RX 分别连接到另一侧的 RX 和 TX,再将 GND 连接起来即可。
项目#50:启用 Arduino 到 Arduino 的通信
本项目演示了一个 Arduino 如何通过 RS232 与另一个 Arduino 进行通信。一块带有 BMP180 温度传感器的 Arduino 板会通过 RS232 将当前温度发送到另一块 Arduino 板,接收 Arduino 上的 I²C LCD 会显示该值。
你将需要以下部件来完成这个项目:
-
两块 Arduino Uno 或兼容的开发板和 USB 线
-
两个 RS232 保护板或模块用于 Arduino
-
一根九针 RS232 交叉电缆
-
一个免焊面包板
-
各种跳线
-
一个 BMP180 温度和气压传感器板
-
一个 PCF8574 LCD 模块
一块 Arduino 将充当发射器板,另一块将充当接收器板。要组装硬件,将 RS232 扩展板或模块连接到每块 Arduino,然后通过交叉电缆或跳线将它们连接起来。将 BMP180 传感器添加到发射器板,如 图 17-12 中的原理图所示。

图 17-12:BMP180 传感器与发射器 Arduino 的连接图
接下来,将 LCD 模块添加到接收器板,如 图 17-13 所示。

图 17-13:LCD 与接收器 Arduino 的连接图
如果你之前没有使用过 BMP180 传感器,请按照 第十章 中的说明安装库,并按照 第十三章 中的说明安装 I²C LCD。
现在,Arduino 已连接,如果你使用的是 RS232 扩展板,请确保它们已经开启。为了为项目供电,如果 PC 和 Arduino 之间有距离,可以使用 USB 电缆或外部电源。
接下来,将发射器草图输入并上传到发射器 Arduino,然后将接收器草图上传到接收器 Arduino。稍等片刻,LCD 屏幕应该会显示当前温度,显示内容的示例如 图 17-14 所示。

图 17-14:项目 #50 的示例输出
让我们先从发射器草图开始,看看它是如何工作的:
// Project #50 - Arduino-to-Arduino communication with RS232C - transmitter
#include "Adafruit_BMP085.h"
Adafruit_BMP085 bmp;
❶ int temperature;
void setup()
{
❷ Serial.begin(9600);
bmp.begin();
}
void loop()
{
❸ temperature = bmp.readTemperature();
Serial.print("Temp (C)= ");
Serial.print(temperature);
Serial.print("\n");
delay(1000);
}
发射器草图包含并初始化了 BMP180 库,并创建一个变量来存储温度 ❶。它启动了串口和 BMP180 ❷。循环从 BMP180 获取温度读取 ❸,然后将文本发送到接收 Arduino,文本以描述 Temp (C) = 开头,后面跟着温度的数值。接着是 \n,它告诉接收 Arduino 添加一个新行。然后稍作延迟,过程重复。
现在让我们来看一下接收器的草图:
// Project #50 - Arduino-to-Arduino communication with RS232 - receiver
#include <LiquidCrystal_PCF8574.h>
#include <Wire.h>
LiquidCrystal_PCF8574 lcd(0x27);
int length = 16;
char newLine = '\n';
char buffer[16];
void setup()
{
Serial.begin(9600);
Wire.begin();
Wire.beginTransmission(0x27);
lcd.begin(16, 2);
lcd.setBacklight(255);
lcd.clear();
}
void loop()
{
if (Serial.available())
{
lcd.home();
lcd.setCursor(0, 0);
Serial.readBytesUntil(newLine, buffer, length);
lcd.print(buffer);
}
}
接收器草图的任务是将通过 RS232 接收到的串行数据行显示在 LCD 上。它包含并初始化了 LCD 库和 I²C 总线,然后创建了三个必要的变量:length,表示 LCD 每行可显示的字符数;endOfLine,保存换行符;以及字符数组 buffer,用于保存来自 RS232 总线的接收数据。
在void setup()中,草图设置了串行通信,激活了 I²C 总线,并初始化了 LCD 的使用,通过开启背光并将光标设置到显示屏的左上角来实现。在主循环中,如果检测到传入的字符,LCD 光标会通过lcd.home();和lcd.setCursor(0, 0);重置到左上角。Serial.readBytesUntil()函数会捕获最多 16 个来自串行的数据字符,直到接收到换行符。草图将这些字符存储在数组缓冲区中。最后,它会在 LCD 上显示缓冲区数组的字符。每当从发射端 Arduino 接收到新数据时,这个过程会重复。
对于涉及两个 Arduino 板在彼此之间的距离的更长期项目,本项目中的简单演示为可靠的数据传输提供了理想的框架。作为挑战,您可以创建一个项目,在其中两个 Arduino 通过发送随机数据或传感器值“对话”。
其他 RS232 数据配置
到目前为止,在本章中,您使用了默认的 9,600 bps 数据速率,每个数据单元为 8 位,无校验和 1 个停止位。(在设备规格表或用户手册中,这通常写作“8, None, 1”在速率后面。)然而,在未来的项目中,您可能需要使用不同数据速率或发送接收数据的方式。例如,一台旧终端可能使用 7 位而非 8 位,使用 2 个停止位而不是 1 个。
为了解决这个问题,您可以修改本章中项目中使用的Serial.begin()函数的参数。将第一个参数设置为您需要的数据速率,将第二个参数设置为新的数据配置:
Serial.begin(`speed`, SERIAL_`XYZ`)
在第二个参数中,X代表所需的数据位数,可以在 5 到 8 之间;Y代表奇偶校验,可以是N表示无,E表示偶数,或O表示奇数;Z代表停止位的数量,可以是 1 或 2。
例如,要以 4,800 bps 的速度启动串行通信(即 RS232),使用 7 位数据,无校验和 1 个停止位,请在您的草图中使用以下函数:
Serial.begin(4800, SERIAL_7N1)
如果你决定将任何复古设备与 Arduino 连接,这可能会派上用场。为了体验一些复古计算乐趣,可以留意 eBay 或计算机市场,寻找旧的计算机终端或打印机。例如,看看这个 Arduino 控制的 RS232 到并行打印机接口:https://
继续前进
你已经学会了如何通过 RS232 在 PC 和 Arduino 之间进行通信,以及在多个 Arduino 之间通过 RS232 进行通信。现在你具备了利用 RS232 总线进行远程控制、Arduino 间的数据通信、以及将数据捕捉到 PC 中进行分析的工具,同时还可以通过 Arduino 与基于 RS232 的设备进行通信。
在下一章中,你将学习如何使用 Arduino 通过另一种数据总线——RS485 数据总线,与其他设备进行通信。
第十八章:18 ARDUINO-TO-RS485 COMMUNICATION

在上一章中,你使用了 RS232 总线创建了 Arduino 微控制器与其他设备之间的长距离可靠有线数据连接。本章介绍了 RS485 总线,你可以使用它实现更长的有线连接。
RS485 数据总线广泛应用于各种设备,从供暖、通风和空调(HVAC)系统的设备控制器到远程传感器通信和安全系统网络。它需要使用双绞屏蔽线。根据数据传输速度,你的电缆长度可以超过 800 米,并且可以在单一总线上连接多达 256 台设备。这种设备之间的安全、无干扰通信是无线数据模块无法实现的。RS485 模块价格非常低廉,使得你可以轻松地在自己的项目中使用它们。
本章将介绍如何将 RS485 接口模块与 Arduino 连接。你将学习:
-
通过电路将数据从远程 Arduino 发送到你的 PC。
-
创建一个带 LCD 显示的远程温度计
-
构建一个远程控制的 Arduino 项目,包含一个主要控制器和两个或更多的远程控制 Arduino。
你可以将这些项目作为未来 Arduino 到 RS485 通信需求的框架。
RS485 总线
RS485 数据总线旨在直接连接两个或多个数据终端设备,而无需通过调制解调器,连接距离比 RS232 等其他数据总线更长。它非常适合连接需要相互通信的两个基于微控制器的设备,因为它比无线数据链接更可靠,并且在设备之间的通信距离更长。
与 RS232 类似,RS485 是一种串行数据总线,其工作方式与 Arduino 的串口(通常位于 D0 和 D1 引脚)类似。然而,数据并不会简单地从 Arduino 通过 RS485 接口传输到另一端的 RS485 设备。Arduino 必须在数据传输前告诉 RS485 设备是发送数据还是接收数据,然后数据才能按照需要在总线上传输。数据随后按照要求沿 RS485 总线传输到另一个 RS485 设备,再传输到连接的 Arduino。
虽然 RS485 总线发送的是表示 1 和 0 的顺序数据位,但其信号类型与 Arduino 中常见的 TTL 串行通信不同。为了比较,图 18-1 显示了从 Arduino 串口发送的一些数据,捕捉到的波形图。正如你所看到的,1 的电平是 5 V,0 的电平是 0 V。

图 18-1:从 Arduino 串口发送的数据
现在考虑通过 RS485 发送的相同数据,如 图 18-2 所示。RS485 使用两根线,A 和 B。当发送 1 时,A 线的电压高于 B 线,当发送 0 时,A 线的电压低于 B 线。所使用的电压范围可以在 −7 V 到 12 V DC 或更高之间变化,具体取决于所使用的 RS485 接口硬件。

图 18-2:通过 RS485 接口发送的数据
使用不同的电压表示 1 和 0,通过两根导线可以清晰地区分这两个比特,消除了混淆的可能性,从而提高了数据的准确性。由于电压范围较宽,RS485 电缆的长度对信号完整性的影响较小,因为信号电压可以在电缆长度变化的范围内变化(电缆越长,因电缆电阻而导致的电压降越大)。结合使用屏蔽电缆,这个电压范围意味着你可以在比其他数据总线(如 RS232)更长的距离上使用 RS485。
连接到 RS485
基本的 RS485 接口板或模块具有一个发送端子(称为 RO)和一个接收端子(称为 DI),用于 Arduino 之间的通信,还有电源(通常为 5V)和地线(GND)。从微控制器到 RS485 接口的一个额外信号,通常标记为 DE/RE,用于指示数据是接收还是发送。在某些模块上,DE 和 RE 是两个独立的引脚,必须将它们连接在一起。最后,正如上一节所描述,RS485 接口之间的连接使用两根线,A 和 B。这允许进行 半双工通信,即数据一次只能在一个方向上传输。
注意
RS485 总线还支持通过四根线进行全双工通信,但详细描述此内容超出了本书的范围。
你可以在廉价的 RS485 模块上清晰地看到所有引脚的标注,例如 图 18-3 中显示的 PMD Way 零件号 600197。这些模块可以方便地放置在无焊接面包板上进行实验或快速原型设计。在本章的项目中,你将使用这些模块。

图 18-3:一款廉价的 RS485 接口模块
图 18-4 显示了这些模块使用的原理图符号。
原理图中的 5V 和 GND 引脚连接到电源。接口模块的 A 和 B 引脚连接到其他接口模块;A 引脚连接到 A 引脚,B 引脚连接到 B 引脚。DI(数据输入)引脚接收来自 Arduino 的数据,并通过 RS485 发送出去。R0 引脚将通过 RS485 接收到的数据发送到 Arduino。最后,在电路中使用 DE 和 RE 引脚设置接口模块为发送或接收模式,将它们设置为 HIGH 以发送数据,设置为 LOW 以接收数据,就像你将在接下来的项目中所做的那样。

图 18-4:RS485 接口模块原理图符号
项目 #51:创建 Arduino 到 PC 的数据链路
本项目展示了如何通过 RS485 总线轻松地将数据从一个 Arduino 发送到另一个 Arduino,再到 PC。你也可以用它从远程 Arduino 捕获数据到 PC。
你将需要以下部件:
-
两个 Arduino Uno 或兼容板和一根 USB 电缆
-
两个 RS485 接口模块
-
两个免焊接面包板
-
各种跳线
-
适合 Arduino Uno 或兼容板的电源
-
一根用于长距离通信的双绞线(可选)
与本章中的所有项目一样,你将创建两个 Arduino 到 RS485 的电路:一个接收器电路和一个发送器电路。发送器的 Arduino 将通过 RS485 向接收器的 Arduino 发送数据,这些数据然后通过常规的 USB 连接传输到 PC。你可能会发现,在构建电路之前先将接收器和发送器的草图上传到各自的 Arduino 会更容易,因为那样在构建电路时会更加方便,特别是当电路之间有较长距离时。
如果你想使用长的 RS485 电缆,请使用屏蔽双绞线,并利用电缆的屏蔽层将发送端和接收端的 GND 连接起来。不过,你也可以使用短接线来测试此项目。
图 18-5 显示了接收器电路的原理图。

图 18-5:项目 #51 接收器电路原理图
图 18-6 显示了发送器电路的原理图。

图 18-6:项目 #51 发送器电路原理图
一旦构建了电路并上传了草图,通过 USB 将接收单元连接到 PC,并打开 Arduino 的串行监视器或终端软件(如 CoolTerm,第一次在 第十四章 中解释)。终端应该显示发送器电路的 Arduino 上模拟引脚 0 的值,如 图 18-7 所示。

图 18-7:项目 #51 的示例输出
让我们看看它是如何工作的,首先仔细查看发射器电路的草图:
// Project #51 - Transmitter
❶ #include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
❷ #define TXRX 2 // Using D2 for RS485 DE/RE
void setup()
{
RS485.begin(9600);
pinMode(TXRX, OUTPUT);
digitalWrite(TXRX, HIGH); // RS485 transmit
}
void loop()
{
RS485.print("A0 measures: ");
RS485.println(analogRead(0));
}
该草图配置软件串行以使用数字引脚 D4 和 D3 ❶与 RS485 接口模块进行通信。它使用 Arduino 数字引脚 D2 来控制 DE/RE 引脚,这些引脚决定 RS485 总线上的数据方向 ❷。在void setup()中,草图启动将与 RS485 模块通信的软件串行,然后设置 D2 引脚为输出,以控制 RS485 模块上的数据方向。然后,它将方向设置为发送。最后,在void loop()中,草图通过软件串行不断地将模拟输入 0 的值作为示例数据沿 RS485 总线发送。
现在考虑接收电路的草图:
// Project #51 - Receiver
#include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
#define TXRX 2 // Using D2 for RS485 DE/RE
void setup()
{
Serial.begin(9600);
pinMode(TXRX, OUTPUT);
❶ digitalWrite(TXRX, LOW); // RS485 receive
}
void loop()
{
if (RS485.available())
{
❷ Serial.write(RS485.read());
}
}
接收器草图的配置与发射器草图相同,唯一不同的是程序通过将数字引脚 D2 设置为低电平 ❶来将 RS485 模块设置为接收模式。然后,Arduino 等待通过软件串行从 RS485 总线接收到一个字符。当一个字符到达时,Arduino 通过硬件串行和 USB 将其发送到连接的计算机 ❷。您可以使用此项目通过 RS485 数据连接将由连接到 Arduino 的设备生成的任何类型的数据发送到 PC 进行记录。
下一个项目演示了一种查看通过 RS485 数据连接接收的数据的方法,且不依赖于计算机进行操作。
项目 #52:创建 Arduino 与 Arduino 的数据连接
在此项目中,您将建立一个 RS485 数据连接,通过总线发送来自 BMP180 传感器的温度数据,并使用 I²C PCF8574 LCD 模块进行显示。除了作为使用 RS485 数据总线的另一个良好示范外,该项目还作为构建远程实时监控系统的框架,这些系统在接收端无需主机计算机,例如操作温度、机器速度或其他传感器数据。
注意
如果您不熟悉 I²C PCF8574 模块 LCD,请参考第十三章中描述的用法。我还在第十章中讲解了 BMP180 传感器。
您需要以下零件来完成此项目:
-
两个 Arduino Uno 或兼容板和一根 USB 电缆
-
两个 RS485 接口模块
-
两个无焊面包板
-
各种跳线
-
两个适合 Arduino Uno 或兼容板的电源供应器
-
一个 PCF8574 LCD 模块
-
一个 BMP180 温度和气压传感器板
-
一根双芯电缆用于远距离通信(可选)
你将再次构建两个 Arduino 至 RS485 电路,一个接收器和一个发送器。在这个项目中,发送器的 Arduino 将通过 RS485 向接收器的 Arduino 发送温度数据,以在 LCD 上显示。同样,你可能会发现,在构建电路之前先将两个草图上传到各自的 Arduino 上会更容易。
图 18-8 显示了接收器电路的原理图。

图 18-8: 项目 #52 接收器电路原理图
图 18-9 显示了发送器电路的原理图。

图 18-9: 项目 #52 发送器电路原理图
一旦你将草图上传到各自的 Arduino 板并完成电路搭建,启动两个电路。当前的环境温度应该会显示在 LCD 上,如 图 18-10 所示。

图 18-10: 项目 #52 示例输出
让我们看看这个是如何工作的,首先仔细观察发送器的草图:
// Project #52 - Transmitter
❶ #include <Adafruit_BMP085.h>
Adafruit_BMP085 bmp;
❷ #include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
❸ #define TXRX 2 // Using D2 for RS485 DE/RE
void setup()
{
RS485.begin(9600);
pinMode(TXRX, OUTPUT);
digitalWrite(TXRX, HIGH); // RS485 transmit
bmp.begin();
}
void loop()
{
RS485.print(bmp.readTemperature());
delay(500);
}
草图首先包含了使用 BMP180 温度传感器所需的库,并创建了实例 ❶。然后,草图配置软件串行通信,使用数字引脚 D4 和 D3 与 RS485 接口模块进行通信 ❷。草图使用 Arduino 的数字引脚 D2 来控制 DE/RE 引脚,从而决定 RS485 总线上的数据方向 ❸。
在 void setup() 中,程序启动软件串行通信,与 RS485 模块进行通信,然后设置 D2 引脚为输出,以控制 RS485 模块上的数据方向。接着,它将方向设置为发送模式,并启动 BMP180 传感器。最后,在 void loop() 中,它通过软件串行通信每半秒将 BMP180 传感器的温度数据通过 RS485 总线发送出去。
现在,来看一下接收器电路的草图:
// Project #52 - Receiver
❶ #include <Wire.h>
#include <LiquidCrystal_PCF8574.h>
LiquidCrystal_PCF8574 lcd(0x27);
#include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
#define TXRX 2 // Using D2 for RS485 DE/RE
float temperature = 0;
void setup()
{
Wire.begin();
Wire.beginTransmission(0x27);
❷ lcd.begin(16, 2);
❸ lcd.setBacklight(255);
❹ lcd.clear();
RS485.begin(9600);
pinMode(TXRX, OUTPUT);
digitalWrite(TXRX, LOW); // RS485 receive
}
void loop()
{
byte incoming = RS485.available();
❺ if (incoming != 0)
{
temperature = RS485.parseFloat();
❻ lcd.home();
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Temperature:");
lcd.setCursor(0, 1);
lcd.print(temperature);
lcd.print(" Celsius");
}
}
由于接收器使用 PCF8574 I²C LCD,草图包含了 I²C 总线所需的库、LCD 显示和实例 ❶。在 void setup() 中,它启动了 I²C 总线并开始与 LCD 通信,以配置 LCD 显示的尺寸 ❷,打开背光 ❸,并清空显示 ❹。然后,草图包含了接收器草图的 RS485 配置。这与发送器草图完全相同,只是草图通过将数字引脚 D2 设置为 LOW,将 RS485 模块设置为接收模式。
Arduino 通过软件串行口❺等待来自 RS485 总线的文本。一旦文本到达,示意图会使用串口库中的.parsefloat()函数将其转换为浮点数。然后,温度信息会显示在 LCD 屏幕❻上。这个过程在接收到更多来自发射电路的温度数据时会重复。
项目#53:远程控制操作
由于 RS485 支持更长的电缆连接,它也非常适合远程控制应用。这个项目展示了通过 RS485 进行远程控制操作,其中一个 Arduino 电路(主电路)通过 RS485 向另一个 Arduino 电路(次级电路)发送信号,以执行各种操作。
你将需要以下零件来完成这个项目:
-
两个 Arduino Uno 或兼容板和一根 USB 电缆
-
两个 RS485 接口模块
-
两块无焊接面包板
-
各种跳线
-
两个适用于 Arduino Uno 或兼容板的电源
-
一根双芯电缆,用于更长距离的通信(可选)
在这个项目中,主 Arduino 通过 RS485 向次级 Arduino 发送单个字符。你将编程次级 Arduino,根据接收到的字符执行各种操作;如果接收到未知命令,将执行默认操作。同样,你可能会发现,在构建电路之前,将示意图上传到每个 Arduino 会更容易一些。
图 18-11 展示了主电路的原理图。

图 18-11:项目#53 主电路原理图
图 18-12 展示了次级电路的原理图。

图 18-12:项目#53 次级电路原理图
一旦你将项目示意图上传到各自的 Arduino 板并搭建好电路,启动电源。主 Arduino 应开始向次级 Arduino 发送命令,次级 Arduino 将通过各种顺序闪烁其板载 LED 灯来响应这些命令。
让我们来看看这个是如何工作的,首先查看主电路的示意图:
// Project #53 - Primary
#include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
#define TXRX 2 // Using D2 for RS485 DE/RE
void setup()
{
RS485.begin(9600);
pinMode(TXRX, OUTPUT);
digitalWrite(TXRX, HIGH); // RS485 transmit
}
void loop()
{
RS485.print("0");
delay(1000);
RS485.print("1");
delay(1000);
RS485.print("2");
delay(1000);
RS485.print("3");
delay(1000);
RS485.print("8");
delay(5000);
}
此示意图配置了主电路,通过 RS485 进行传输,就像以前的项目一样。不同之处在于在void loop()中发送的示例命令。次级 Arduino 被配置为响应命令 0、1、2 和 3,因此主示意图会按顺序发送这些命令作为演示。它还发送数字 8 来展示接收方处理未知命令的功能。每个命令依次发送,每个命令之间有延迟。
这是次级电路的示意图:
// Project #53 - Secondary
#include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
#define TXRX 2 // Using D2 for RS485 DE/RE
void blinkLED(int i)
{
for (int j = 0; j < i; j++)
{
digitalWrite(13, HIGH);
delay(100);
digitalWrite(13, LOW);
delay(100);
}
}
void setup()
{
RS485.begin(9600);
pinMode(TXRX, OUTPUT);
digitalWrite(TXRX, LOW); // RS485 receive
pinMode(13, OUTPUT);
}
void loop()
{
❶ if (RS485.available() > 0)
{
char receivedChar = RS485.read();
❷ switch (receivedChar)
{
case '0': blinkLED(5); break;
case '1': blinkLED(10); break;
case '2': blinkLED(15); break;
case '3': blinkLED(20); break;
❸ default: blinkLED(2); break;
}
}
}
从属电路配置为通过 RS485 接收命令,然后根据这些命令执行操作。为了演示这一点,草图使用了blinkLED()函数,该函数接受一个整数并使板载 LED 闪烁该次数。从属电路通过软件串口 ❶等待来自 RS485 总线的一个文本字符(命令)。从属单元使用switch…case函数 ❷来确定根据从主单元接收到的命令调用哪个函数。如果它收到的命令不在switch…case函数中,它会让 LED 闪烁两次 ❸。
你可以通过将对blinkLED()函数的调用替换为你自己的可操作需求,修改此项目以满足你自己的远程控制需求。
控制两个或更多从属 RS485 设备
到目前为止,你已经学会了如何在两块 RS485 连接的 Arduino 板之间进行通信。然而,如前所述,你可以通过 RS485 总线控制多个从设备,最多可以连接 256 个设备。在本节以及接下来的项目中,我将向你展示如何做到这一点。
当使用三个或更多设备时,RS485 总线的布线稍有不同。你一直在使用的模块上的收发器 IC 还需要在 RS485 总线的每个端点,A 和 B 线之间添加一个 120 Ω的终端电阻。如果你只使用两个模块,那么终端电阻已经装配好,你不需要做任何更改。然而,如果你使用本章中介绍的三个或更多 RS485 模块,你需要从模块上拆下标有 R7 的终端电阻(电阻值为 121,解释为 12 Ω,末尾加上 0 变为 120 Ω)。这个电阻在图 18-13 中已圈出。

图 18-13:一个 RS485 终端电阻
当你的设备连接距离超过演示中工作台上的距离时,确保 A/B 对的线缆以“菊花链”配置从一个设备延伸到另一个设备,如图 18-14 所示。不要为每个设备创建一条从设备到主 A/B 对的单独线路。

图 18-14:三个设备的 RS485 布线配置示例
你将在下一个项目中实际应用这个技术。
项目 #54:控制两个或更多从属 Arduino 板
这个项目演示了如何通过 RS485 从一个主板控制两个或更多的从 Arduino 板。一个 Arduino 电路(主设备)会通过 RS485 向其他 Arduino(从设备)发送命令,指示它们执行各种操作。
这个项目的硬件与第 53 号项目中使用的相同,只是你将使用两个或更多的从设备。首先搭建一个主设备和两个从设备,通过 RS485 总线将它们连接起来,正如上一节中图 18-14 所示。同样,在搭建电路之前,最好先将草图上传到每个 Arduino。如果你喜欢,可以在三设备设置成功后,稍后再向项目中添加更多的从设备。
如果你只是测试这个项目在工作台上的表现,RS485 总线的距离足够短,你可能不需要从 RS485 模块中移除终端电阻,因此在对硬件做出任何更改之前,先测试代码。
在这个项目中,主板通过 RS485 总线发送一个三位数字,所有从板都会接收到这个数字。命令的第一位表示要控制的从设备的编号(从设备 1 到 9),最后两位表示发送给特定从设备的命令。例如,要向从设备 2 发送命令 12,主设备会通过 RS485 总线发送 212,因为加载到第二个从设备的草图被编程为根据接收 12 来执行动作。如果你添加了 10 个或更多从设备,你可以更改草图中的命令,以便它们发送四位数字,或根据自己的需求修改命令编码方式。
这个项目只是一个通过 RS485 控制多个从设备的演示。在当前形式下,从主设备发送的每个命令都会触发对应从设备的 LED 闪烁特定次数。然而,在你未来的项目中,你可以用你自己的需求替换从设备中的简单示例动作。
让我们来看看这个在草图中是如何工作的,从主设备的草图开始:
// Project #54 - Primary
#include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
#define TXRX 2 // Using D2 for RS485 DE/RE
void setup()
{
RS485.begin(9600);
pinMode(TXRX, OUTPUT);
digitalWrite(TXRX, HIGH); // RS485 transmit
}
void loop()
{
❶ RS485.println(105); // Command for secondary 1
delay(5000);
❷ RS485.println(205); // Command for secondary 2
delay(5000);
}
在你现在应该已经熟悉的序列中,草图首先配置主设备以将数据发送到 RS485 总线。在 void loop() 中,它发送两个新的示例命令:105 ❶,这是给从设备 1 的命令 5,以及 205 ❷,这是给从设备 2 的命令 5。记住,命令是三位数字,第一位表示目标从设备的编号,后两位(00 到 99)表示实际的命令。
以下是从设备 1 的草图:
// Project #54 - Secondary device 1
#include <SoftwareSerial.h>
SoftwareSerial RS485(4, 3);
#define TXRX 2 // Using D2 for RS485 DE/RE
void blinkLED(int i)
{
for (int j = 0; j < i; j++)
{
digitalWrite(13, HIGH);
delay(250);
digitalWrite(13, LOW);
delay(250);
}
}
void setup()
{
RS485.begin(9600);
pinMode(TXRX, OUTPUT);
digitalWrite(TXRX, LOW); // RS485 receive
pinMode(13, OUTPUT);
}
void loop()
{
while (RS485.available() == 0) {}
❶ int commandType = RS485.parseInt();
❷ if (commandType >= 100 && commandType < 200)
{
switch (commandType)
{
❸ case 105: blinkLED(5); break;
case 110: blinkLED(10); break;
case 115: blinkLED(15); break;
case 120: blinkLED(20); break;
❹ default: blinkLED(2); break;
}
}
}
该草图配置为,当接收到来自主单元的命令时,会调用blinkLED()函数,并传递一个变化的参数来使 LED 闪烁不同次数。在void loop()中,次级设备等待来自 RS485 总线的整数输入❶。然后,它检查接收到的整数是否在 100 到 199 之间(包括 100 和 199)❷。如果是,表示该命令是为该单元发出的,草图会使用switch…case函数将该整数与预设的命令进行比较。例如,如果收到命令 105,调用blinkLED()函数使板载 LED 闪烁五次❸。如果接收到的命令无法识别❹,LED 闪烁两次。
次级设备 2 的草图的第一部分与次级设备 1 相同。只有草图中的void loop()部分有所不同:
void loop()
{
while (RS485.available() == 0) {}
int commandType = RS485.parseInt();
❶ if (commandType >= 200 && commandType < 300)
{
switch (commandType)
{
❷ case 205: blinkLED(5); break;
case 210: blinkLED(10); break;
case 215: blinkLED(15); break;
case 220: blinkLED(20); break;
❸ default: blinkLED(2); break;
}
}
}
该草图测试接收到的命令,检查它是否在 200 到 299 之间(包括 200 和 299)❶,因为命令的第一位数字(2)表示该命令是为该次级单元发出的。如果该命令是为此单元发出的,草图会使用switch…case函数将其与预设的命令进行比较。例如,如果设备收到命令 205,草图会调用blinkLED()函数使板载 LED 闪烁五次❷。如果接收到的命令无法识别❸,LED 闪烁两次。
要添加额外的次级单元,您只需修改测试行❶,以便新单元能够检查主设备发出的命令是否在其要求的数字范围内。您还需要在switch…case函数中添加相应的命令和所需的操作。
为了增加挑战,您可以尝试创建一个系统,将来自次级设备的数据返回给主设备。
继续前进
本章已为您提供了使用 Arduino 实现 RS485 数据总线的基本构建模块。您可以利用这些知识,制作自己的数据传输和控制应用,支持远距离操作,例如监控传感器或控制办公室或工厂内其他房间的设备。
在下一章中,您将开始使用流行的支持 Wi-Fi 的 Arduino 兼容板,制作可以从手机或任何网络设备远程控制的设备。
第十九章:19 ESP32 微控制器平台与物联网

物联网(IoT)这一术语描述了通过互联网通信或被控制的各种设备。过去,使用 Wi-Fi 通信的物联网 Arduino 项目既复杂又昂贵,因为 Arduino 的 Wi-Fi 扩展板通常体积庞大,且与 Arduino 微控制器结合时功能有限。然而,随着 Espressif 的 ESP32 微控制器平台的出现,情况已经发生了改变,ESP32 内置了 Wi-Fi,并且与 Arduino 兼容。
本书的最后六章将使用基于 ESP32 的开发板。为了获得最佳的学习体验,建议按顺序阅读这些章节;不过,如果你愿意,也可以随意跳过某些章节。
在这一章中,我将向你介绍基于 ESP32 的 Arduino 兼容开发板。你将学习如何在 Arduino IDE 中安装新开发板配置文件,测试 ESP32 的 Wi-Fi 连接,并在 ESP32 开发板上使用 PWM。你将学习:
-
创建一个带按钮的简单网页
-
构建一个带状态显示的四路输出遥控器
-
构建一个可遥控调整的 PWM 输出
你将在本章中掌握的技能将帮助你在本书后续章节中与其他联网设备进行互动。
ESP32
ESP32 是一款内置 Wi-Fi 和蓝牙的微控制器,其运行速度比典型的 Arduino 或兼容开发板更快。它并不是一个简单的单片配置;相反,它需要外部闪存芯片和无线操作所需的天线。芯片和天线通常紧密放置在一起,如图 19-1 所示。

图 19-1:示例 ESP32 芯片组,去除顶部金属外壳后的模块
为了避免信号干扰,所需的电路被集成在一个模块中,该模块上方有金属外壳。这个模块被安装到开发板上,电路用于与其他设备连接,或者被添加到开发板上以实现 Arduino 兼容性,如图 19-2 所示。

图 19-2:ESP32 开发板示例
像 PMD Way part 51045204 这样的开发板,见图 19-2,通常被称为ESP32 开发板(或 开发板)。你将在本章及第二十四章的项目中使用此开发板。如果购买的开发板没有附带 USB 电缆,请确保单独订购所需的电缆。
Arduino Uno 和 ESP32 开发板之间有四个主要的区别。第一个是较大的外部闪存 IC;ESP32 提供的空间比 8 位 Arduino 更多,可以存储更多的 Arduino 代码。第二个区别是 CPU 的速度更快,最高可达 240 MHz。
第三个区别是,ESP32 是 3.3 V 设备,因此所有通过输入或输出引脚连接的外部设备,必须在 3.3 V 下工作,或者具有 3.3 V 容忍性。如果默认连接电压不是 3.3 V,你需要使用电平转换器,例如 图 19-3 中显示的 PMD Way 441079。将转换器连接在 5 V 设备的线路和 HV 垫之间,以及连接 ESP32 的 LV 垫上的匹配低电压线路,最后将两侧的 GND 连接到 ESP32 和 5 V 设备。

图 19-3:四通道电平转换器板
在购买电平转换器时,选择一个四通道的设备,比如图 19-3 中的设备。这样,你可以同时使用 SPI 和 I²C 数据总线,而不会用完转换器的引脚。
Arduino Uno 和 ESP32 之间的最后一个区别是,尽管 ESP32 和 Uno 具有相同的物理外形尺寸,但它们的引脚排布是不同的,具体如下所示 表 19-1。这也意味着某些 Arduino 扩展板可能无法与 ESP32 开发板兼容,因此在购买用于 ESP32 的扩展板之前,需先了解其所需的连接方式。
表 19-1: ESP32 和 Arduino 引脚排布比较
| Arduino 标签 | Uno R3 | ESP32 | Arduino 标签 | Uno R3 | ESP32 |
|---|---|---|---|---|---|
| I2C 时钟 | SCL | SCL | 数字 I/O | D3 | IO25 |
| I2C 数据 | SDA | SDA | 数字 I/O | D2 | IO26 |
| 模拟参考 | AREF | RESET | 数字 I/O | D1 | 仅 TX |
| GND | GND | GND | 数字 I/O | D0 | 仅 RX |
| 数字 I/O | D13 | IO18 | 模拟输入 | A5 | IO39 |
| 数字 I/O | D12 | IO19 | 模拟输入 | A4 | IO36 |
| 数字 I/O | D11 | IO23 | 模拟输入 | A3 | IO34 |
| 数字 I/O | D10 | IO5 | 模拟输入 | A2 | IO35 |
| 数字 I/O | D9 | IO13 | 模拟输入 | A1 | IO4 |
| 数字 I/O | D8 | IO12 | 模拟输入 | A0 | IO2 |
| 数字 I/O | D7 | IO14 | 板载电压 | IOREF | IO0 |
| 数字 I/O | D6 | IO27 | 重置 MCU | RESET | RESET |
| 数字 I/O | D5 | IO16 | 3.3 V 输出 | 3.3V | 3.3V |
| 数字 I/O | D4 | IO17 | 5 V 输出 | 5V | 5V |
| GND | GND | GND | |||
| GND | GND | GND | |||
| 原始电压输入 | Vin | Vin |
ESP32 上标记为 IOxx 的引脚是(3.3 V)数字输入和输出引脚。在 Arduino 草图中引用这些引脚时,不需要包含 IO,只需使用数字编号。我很快会演示这一点。
注意
端口操作在 ESP32 板上无法使用。引脚可能还具有表格中未显示的其他功能,但对于本章中的项目,你只会使用表格中显示的功能。
为 ESP32 配置 Arduino IDE
Arduino IDE 默认没有预装 ESP32 板,因此你需要安装所需的文件。拿起你的 ESP32,打开 Arduino IDE,选择 文件
首选项。当首选项对话框出现时,点击附加板管理器 URL 字段旁的小按钮,附加板管理器 URL 对话框应该会出现,如 图 19-4 所示。

图 19-4:Arduino IDE 附加板管理器 URL 对话框
在对话框中输入以下 URL:
**https://dl.espressif.com/dl/package_esp32_index.json**
如果对话框中已经有其他 URL,在最后一个 URL 后添加逗号,并将新 URL 粘贴到逗号后面。
点击确定关闭首选项对话框。现在关闭并重新打开 IDE。如果你的电脑未连接到互联网,请现在连接。选择工具
板管理器。打开板管理器后,在搜索框中输入ESP32。ESP32 软件包应显示在列表中。请始终使用最新版本。
点击安装并等待片刻,直到进度条显示过程完成,如“板管理器”对话框底部所示。
由于 ESP32 使用的是与 Arduino Uno 或兼容板不同的 CH340 USB 到串口接口,你需要为计算机安装相应的驱动程序。为此,请按照 SparkFun 提供的指南,访问 https://
如果你的电脑上还没有安装最新版本的 Python,你还需要下载并安装它,因为 Arduino IDE 需要它作为工具链的一部分,以便将代码上传到 ESP32 板。如果需要,请访问 https://
安装完 Python 后,macOS 用户可能需要打开终端并运行以下命令,然后再重新打开 Arduino IDE:
**Open -a Arduino**
现在打开 Arduino IDE,选择工具,并将所有设置从板开始更改为与图 19-5 中所示的设置相匹配。

图 19-5:Arduino IDE 中的 ESP32 板设置
唯一可能因你的计算机而有所不同的参数是 USB 端口;请选择显示的适当端口。
测试 ESP32
在开始本章第一个项目之前,先测试开发板,确保你可以上传草图,操作 GPIO 引脚,并连接 Wi-Fi。
GPIO 引脚
首先,尝试通过构建经典的测试电路来控制一些 GPIO 引脚,电路会闪烁一些 LED 灯。你需要以下材料:
-
一块 ESP32 开发板和匹配的 USB 电缆
-
一块无焊面包板
-
各种跳线
-
四个 LED 灯
-
一个 560 Ω,0.25 W,1% 的电阻器
按照图 19-6 中的示意图组装电路。

图 19-6:一个用于测试目的的基本四 LED 电路。
现在输入并上传列出 19-1 到你的 ESP32 开发板。上传草图后,片刻之内,每个 LED 灯应该依次打开和关闭。
void setup()
{
❶ pinMode(16, OUTPUT);
pinMode(17, OUTPUT);
pinMode(18, OUTPUT);
pinMode(19, OUTPUT);
}
void blinkLEDs()
{
int i;
int d = 250;
for (i = 16; i < 20; i++)
{
❷ digitalWrite(i, HIGH);
delay(d);
❸ digitalWrite(i, LOW);
}
}
void loop()
{
blinkLEDs();
}
清单 19-1:测试来自 ESP32 开发板的输出
尽管 GPIO 引脚的硬件描述是 IOxx,如表 19-1 所示,但草图在设置 GPIO 引脚状态❶并控制它们的开关❷❸时,仅通过其数字标识符来引用引脚。
如果你的草图没有上传,检查之前章节中描述的板卡类型和参数设置。完成此测试后,请保持电路组装,因为你将在接下来的项目中使用它。
Wi-Fi 连接性
你现在将测试 ESP32 的 Wi-Fi 连接性,使用 ESP32 开发板配置文件中包含的 WiFiScan 草图,该草图会扫描你周围的 Wi-Fi 网络。
你无需额外的硬件,除了开发板、USB 线和 Wi-Fi 网络。通过 IDE 的示例上传草图,路径为文件
示例
WiFi
WiFiScan,然后在 Arduino IDE 中打开串行监视器。过一会儿,开发板应该会搜索 Wi-Fi 接入点的存在,并列出它们以及信号强度,如图 19-7 所示。

图 19-7:扫描 Wi-Fi 接入点的示例结果
串行监视器仅显示接入点的名称,而不显示连接是否开放。如果你想了解信号强度的含义,可以访问https://
端口转发
若要从未连接到本地网络的设备(如通过蜂窝连接的平板电脑或手机)控制你的项目,并且这些设备没有静态 IP 地址,你需要使用一种叫做端口转发的技术,这需要在你的网络路由器或调制解调器中设置。端口转发可以从外部提供商获取静态 IP 地址,然后将数据直接转发到你的设备。
端口转发服务由一些组织提供,如 No-IP(https://
一旦你设置了端口转发或以其他方式确认了静态 IP 地址,你就可以开始构建远程控制应用程序的框架了。不过,如果你无法设置端口转发,你仍然可以通过本地 Wi-Fi 网络控制你的项目。
项目 #55:远程控制单个 GPIO 引脚
在这个项目中,你将通过任何带有网页浏览器的设备远程控制你的 ESP32 开发板上的 GPIO 引脚。这包括使用 HTML 和 CSS 构建一个由 ESP32 托管的简单网页。然后,你可以使用这个网页控制任何通常用 Arduino 3.3 V(或通过电平转换器转换为 5 V)数字输出控制的设备,例如继电器、MOSFET、LED 等。
对于硬件,使用前一节中描述的 GPIO 引脚测试电路。输入并上传项目 #55 的草图到你的 ESP32 开发板。在上传之前,别忘了在草图中更新你的 Wi-Fi 网络凭证。上传后,稍等片刻,打开 Arduino IDE 中的串口监视器,观察 ESP32 连接 Wi-Fi 网络的过程。草图应该显示你项目的 IP 地址,如图 19-8 所示。

图 19-8:串口监视器显示 ESP32 已连接到 Wi-Fi 网络
接下来,使用带有网页浏览器的设备,访问串口监视器中显示的 IP 地址。你应该能看到由 ESP32 开发板托管的简单网页,如图 19-9 所示。

图 19-9:远程控制网页
当你点击开(On)或关(Off)按钮时,连接到 GPIO 16 的 LED 应该会开关。在这个项目中,ESP32 开发板有两个任务:一是作为网页服务器托管用户界面的简单网页(包括开/关按钮),二是响应网页浏览器的 HTML 请求(打开或关闭 GPIO 引脚)。
让我们更详细地了解一下它是如何工作的:
// Project #55 - Digital output control via web page
#include <WiFi.h> ❶
WiFiServer server(80); // Set web server port to 80
// Store your Wi-Fi network credentials:
char* ssid = "`name`"; // Insert name of Wi-Fi access point
char* password = "`password`"; // Insert password
String request; // Stores HTTP request from client (web browser)
int outputPin = 16; // Using GPIO pin 16 on the ESP32 board
unsigned long currentTime = millis(); ❷
unsigned long previousTime = 0;
const long timeoutTime = 2000; // Allowed client connection time
void setup()
{
Serial.begin(115200);
// Set up digital pin to control
pinMode(outputPin, OUTPUT);
digitalWrite(outputPin, LOW);
// Connect to Wi-Fi network:
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
// Show indicator while waiting for connection:
{
delay(500);
Serial.print("~");
}
Serial.println(); // Display local IP address on Serial Monitor
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
// Start web server
server.begin();
}
void loop()
{
// Listen for incoming clients from web browser: ❸
WiFiClient client = server.available();
if (client) // If a request received
{
currentTime = millis();
previousTime = currentTime;
Serial.println("New client connected");
String currentLine = " ";
while (client.connected() && currentTime - previousTime <= timeoutTime)
{// Stay connected for timeoutTime
currentTime = millis();
if (client.available())
{
// Display data from client on Serial Monitor:
char c = client.read(); ❹
Serial.write(c);
request += c;
if (c == '\n') // Client request has finished
{
if (currentLine.length() == 0)
{
// Send HTTP response to client:
client.println("HTTP/1.1 200"OK");
client.println("Content-type:text/h"ml");
client.println("Connection: cl"se");
client.println();
// Now do things based on the client request:
if (request.indexOf("GET /16/on") >= 0) ❺
{
// If request was IP address/16/on:
Serial.println("output on");
digitalWrite(outputPin, HIGH);
} else if (request.indexOf("GET /16/off") >= 0) ❻
// If request was IP address/16/off:
{
Serial.println("output off");
digitalWrite(outputPin, LOW);
}
// Build web page for display on browser:
client.println("<!DOCTYPE html><html>");
client.print("<head><meta name=\"viewport\");
client.println(" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\ ">");
// Build buttons using CSS:
client.print("<style>html {font-family: Helvetica; display:");
client.print("inline-block; margin: 0px auto; text-align: center;}");
client.println();
client.print (".button {background-color: #ff0000; border: ");
client.println("none; color: white; padding: 8px 20px;");
client.print ("text-decoration: none; font-size: 20px; margin: ");
client.println("2px; cursor: pointer; border-radius: 12px;}");
client.println("</style></head>");
// Now HTML for layout:
client.println("<body><h3>Remote ESP32 Output Control</h3>");
// Display "On" button with matching HTML "/16/on":
client.print ("<p><a href=\"/16/on\"><button ");
client.println("class=\"button\">On</button></a>");
// Display "Off" button with matching HTML "/16/off":
client.print ("<a href=\"/16/off\"><button class");
client.println("=\"button\">Off</button></a></p>");
client.println("</body></html>");
// End the response to client request:
client.println();
break;
} else
{// If you got a newline, then clear currentLine
currentLine = " ";
}
} else if (c != '\r')
{
currentLine += c;
}
}
}
request = " "; ❼
// Close connection to client:
client.stop();
Serial.println("Disconnected.");
}
}
首先,草图包含了 Arduino Wi-Fi 库 ❶,并创建了一个端口为 80 的服务器实例。接下来,它将 SSID(Wi-Fi 接入点名称)和密码存储在相应的字段中。它声明了一个字符串变量,用来存储来自客户端(显示项目网页的网页浏览器)的 HTTP 请求文本,然后设置它将控制的 GPIO 引脚。
草图接着声明了变量来处理每个客户端与服务器(ESP32 开发板)连接时允许的时间 ❷。在 void setup() 中,它配置了串口监视器,然后将 GPIO16 设置为输出并拉低。ESP32 使用之前输入的凭据尝试连接 Wi-Fi 网络,代码会每半秒循环一次并显示波浪线 (~),直到网络连接成功。一旦 ESP32 成功连接到 Wi-Fi,草图将在串口监视器中显示 IP 地址,项目也开始作为一个网页服务器运行,这都得益于 server.begin()。
ESP32 接下来会运行 void loop() 中的代码,直到重置或断电。每次循环开始时,代码会检查是否有客户端(使用网页浏览器的人)尝试连接到项目 ❸。如果是,代码会记录连接开始的时间点,使用 millis() 函数,以确保每个客户端最多连接到 ESP32 两秒钟(如 timeoutTime 设置的时间)。这是因为其他用户也可能想要访问,你不希望某一个人占用项目的访问权限。代码还会清空字符串变量 currentLine,等待客户端发送的文本。
如果客户端连接时间少于 2000 毫秒并已发出请求,ESP32 会接收到该请求。代码将最新的 millis() 值赋给 currentTime 变量。字符串请求按一个字符接收客户端请求 ❹。该请求会显示在串口监视器中,以便你如果好奇的话,可以查看客户端发送的信息(浏览器类型、设备操作系统等)。一旦客户端完成发送请求,并以换行符 (\n) 标记,ESP32 就会以 HTML 格式向客户端浏览器发送响应。为此,它使用 client.println() 函数,通过 Wi-Fi 连接发送文本,就像你在其他项目中使用 Serial.println() 一样。
ESP32 最终可以响应客户端请求并执行操作。客户端请求以 /16/on 或 /16/off 的形式接收,因此该字符串请求会依次搜索开启命令 ❺ 并执行,通过将其发送到串口监视器并打开 GPIO16;如果收到关闭命令,则会发生相反的操作 ❻。这是你可以根据自己的需要修改操作的地方。这个项目只是调用了一个 digitalWrite() 函数,你可以用自己的函数替换它,让 ESP32 执行你自己选择的操作。
代码的最后一部分使 ESP32 托管了简单的网页,即用户界面,如 图 19-10 所示。通过多个 client.println() 函数,代码生成 HTML,创建网页的框架,使用 CSS 定义要显示的按钮,定位按钮,并发送 HTML 来定义网页的结束。一旦客户端请求完成,草图会从变量中刷新请求 ❼ 并关闭连接,为下一个用户做好准备。
让我们仔细看看 client.println() 函数中包含的 HTML:
<!DOCTYPE html><html>
<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<link rel=\"icon\" href=\"data:,\">
<style>html {font-family: Helvetica; display: inline-block;
margin: 0px auto; text-align: center;}
.button {background-color: #ff0000; border: none; color: white; padding: 8px 20px;
text-decoration: none; font-size: 20px; margin: 2px; cursor: pointer; border-radius: 12px;} ❶
</style></head>
<body><h3>Remote ESP32 Output Control</h3> ❷
<p><a href=\"/16/on\"><button class=\"button\">On</button></a> ❸
<a href=\"/16/off\"><button class=\"button\">Off</button></a></p> ❹
</body></html>
这段代码使用 CSS 来选择网页界面按钮的颜色、大小、字体大小和圆角 ❶,并将标题显示为更大的字体 ❷。然后,它将两个按钮添加到网页中。每个按钮都附有一个指向其各自操作的超链接:第一个链接是 /16/on ❸,第二个是 /16/off ❹。当用户点击其中任何一个按钮时,客户端请求将包括 ESP32 的 IP 地址,后跟 /16/on——例如 http://
如果你将鼠标悬停在网页上的按钮上,链接应该会出现在屏幕底部。例如,图 19-10 显示了当鼠标悬停在开启按钮上时的情况。(浏览器标签页顶部的 URL 是我上次访问页面的地址,点击关闭按钮后的结果。)

图 19-10:在浏览器中显示的示例超链接
当在较为永久的基础上安装项目时,最终用户也可以为每个功能添加完整的 URL 书签。例如,如果项目返回的 IP 地址是 192.168.20.28,则需要打开的书签为 http://
请注意,任何拥有此类型项目的 URL 地址的人都可以操作控制按钮。为了提高安全性,你可以创建一个安全的网站或门户,设置密码保护以限制对控制按钮的访问。
项目#56:远程控制四个 GPIO 引脚
这个项目基于之前的项目,允许你远程控制四个 GPIO 引脚,并在 ESP32 主机托管的网页上显示其状态。用户可以快速查看被控制设备的状态,这对于真正的远程控制项目非常理想。
你将使用与“GPIO 引脚”一节中描述的相同演示电路(这就是为什么它包括四个 LED)。设置好电路后,输入并上传项目#56 的草图到你的 ESP32 开发板。上传后不久,打开 Arduino IDE 中的串口监视器,观察 ESP32 如何连接到 Wi-Fi 网络。
一旦 ESP32 连接,项目的 IP 地址应出现在串口监视器中。将该 IP 地址输入到网页浏览器中,项目的界面应显示出来,如图 19-11 所示。

图 19-11:项目#56 的用户界面
你应该能够通过点击相应按钮控制所有四个 LED。每次点击按钮后,网页应显示已连接 LED 的状态。例如,图 19-12 显示输出 2 和 4 已开启。

图 19-12:输出 2 和 4 已开启,而 1 和 3 关闭。
你可以想象,拥有四组按钮,能够控制更多的继电器、输出、由 MOSFET 控制的设备等,这将变得多么有用。
让我们仔细看看它是如何工作的:
// Project #56 - Digital output controls via web page with status update
#include <WiFi.h>
WiFiServer server(80); // Set web server port to 80
// Store your Wi-Fi network credentials:
char* ssid = "`name`"; // Insert name of Wi-Fi access point
char* password = "`password`"; // Insert password
String request; // Stores HTTP request from client (web browser)
String outputPin1State = "Off"; ❶
String outputPin2State = "Off";
String outputPin3State = "Off";
String outputPin4State = "Off";
int outputPin1 = 16; // Using GPIO pins 16~19 on the ESP32 board ❷
int outputPin2 = 17;
int outputPin3 = 18;
int outputPin4 = 19;
unsigned long currentTime = millis();
unsigned long previousTime = 0;
const long timeoutTime = 2000; // Allowed client connection time
void setup()
{
Serial.begin(115200);
// Set up digital pins to control:
pinMode(outputPin1, OUTPUT); ❸
pinMode(outputPin2, OUTPUT);
pinMode(outputPin3, OUTPUT);
pinMode(outputPin4, OUTPUT);
digitalWrite(outputPin1, LOW);
digitalWrite(outputPin2, LOW);
digitalWrite(outputPin3, LOW);
digitalWrite(outputPin4, LOW);
// Connect to Wi-Fi network:
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
// Show indicator while waiting for connection:
{
delay(500);
Serial.print("~");
}
Serial.println(); // Display local IP address on Serial Monitor
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
// Start web server:
server.begin();
}
void loop()
{
// Listen for incoming clients from web browser:
WiFiClient client = server.available();
if (client) // If a request received
{
currentTime = millis();
previousTime = currentTime;
Serial.println("New client connected");
String currentLine = " ";
while (client.connected() && currentTime—previousTime <= timeoutTime)
{// Stay connected for timeoutTime
currentTime = millis();
if (client.available())
{
char c = client.read(); // Display data from client on Serial Monitor
Serial.write(c);
request += c;
if (c == '\n')
{
// Client request has finished
if (currentLine.length() == 0)
{
// Send HTTP response back to client
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// Now do things based on the client request:
if (request.indexOf("GET /16/on") >= 0) ❹
{
// If request was IP address/16/on:
Serial.println("output 1 on");
outputPin1State = "On"; ❺
digitalWrite(outputPin1, HIGH);
} else if (request.indexOf("GET /16/off") >= 0)
{
// If request was IP address/16/off:
Serial.println("output 1 off");
outputPin1State = "Off";
digitalWrite(outputPin1, LOW);
} else if (request.indexOf("GET /17/on") >= 0)
{
// If request was IP address/17/on:
Serial.println("output 2 on");
outputPin2State = "On";
digitalWrite(outputPin2, HIGH);
} else if (request.indexOf("GET /17/off") >= 0)
// If request was IP address/17/off:
{
Serial.println("output 2 off");
outputPin2State = "Off";
digitalWrite(outputPin2, LOW);
}
else if (request.indexOf("GET /18/on") >= 0)
{
// If request was IP address/18/on:
Serial.println("output 3 on");
outputPin3State = "On";
digitalWrite(outputPin3, HIGH);
} else if (request.indexOf("GET /18/off") >= 0)
// If request was IP address/18/off:
{
Serial.println("output 3 off");
outputPin3State = "Off";
digitalWrite(outputPin3, LOW);
}
else if (request.indexOf("GET /19/on") >= 0)
{
// If request was IP address/19/on:
Serial.println("output 4 on");
outputPin4State = "On";
digitalWrite(outputPin4, HIGH);
} else if (request.indexOf("GET /19/off") >= 0)
// If request was IP address/19/off:
{
outputPin4State = "Off";
Serial.println("output 4 off");
digitalWrite(outputPin4, LOW);
}
// Build web page for display on browser:
client.println("<!DOCTYPE html><html>");
client.print("<head><meta name=\"viewport\" content=\"width=");
client.println("device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
// Build buttons using CSS:
client.print("<style>html {font-family: Helvetica; display: inline-block;");
client.print("margin: 0px auto; text-align: center;} ");
client.print(".button {background-color: #ff0000; border: none; color: ");
client.print("white; padding: 8px 20px; ");
client.print("text-decoration: none; font-size: 20px; margin: 2px; ");
client.println("cursor: pointer; border-radius: 12px;} ");
client.println("</style></head>");
// Now HTML for layout:
client.println("<body><h3>Remote ESP32 Output Controls</h3>");
client.println("<p><b>Output 1 is ");
client.println(outputPin1State); ❻
client.print(" - </b><a href=\"/16/on\"><button);
client.println("class=\"button\">On</button></a>");
client.print("<a href=\"/16/off\"><button");
client.println("class=\"button\">Off</button></a></p>");
client.println("<p><b>Output 2 is ");
client.println(outputPin2State);
client.print(" - </b><a href=\"/17/on\"><button ");
client.println("class=\"button\">On</button></a>");
client.print("<a href=\"/17/off\"><button");
client.println("class=\"button\">Off</button></a></p>");
client.println("<p><b>Output 3 is ");
client.println(outputPin3State);
client.print(" - </b><a href=\"/18/on\"><button ");
client.println("class=\"button\">On</button></a>");
client.print ("<a href=\"/18/off\"><button ");
client.println("class=\"button\">Off</button></a></p>");
client.println("<p><b>Output 4 is ");
client.println(outputPin4State);
client.print(" - </b><a href=\"/19/on\"><button");
client.println("class=\"button\">On</button></a>");
client.print ("<a href=\"/19/off\"><button");
client.println(" class=\"button\">Off</button></a></p>");
client.println("</body></html>");
// End the response to client request:
client.println();
break;
} else
{// If you got a newline, then clear currentLine
currentLine = " ";
}
} else if (c != '\r')
{
currentLine += c;
}
}
}
request = " ";
// Close connection to client:
client.stop();
Serial.println("Disconnected.");
}
}
这个项目的框架与之前的项目相同,唯一不同的是它控制四个 GPIO 引脚和八个按钮,并跟踪 GPIO 的状态。这个草图使用了四个字符串变量来存储四个 GPIO 引脚的状态,以文本形式表示“开”或“关”,并声明它们的默认值为“关”❶。为了避免混淆,草图还声明了变量,为每个 GPIO 引脚指定一个名称,而不是在代码中使用数字❷。然后,它将引脚设置为输出,并设定为 LOW❸。
由于现在有八个按钮和八个相应的操作(每个 GPIO 的开与关),需要注意八个客户端请求❹。每次操作完成后,匹配 GPIO 的状态将在其相应的状态变量中更新❺。这对所有八种客户端请求类型都适用。
再次强调,代码中使用了许多 client.println() 函数来生成用户界面的 HTML。项目代码中的 ❻ 位置的函数生成了显示每个 GPIO 引脚状态所需的 HTML。草图还为每个按钮附加了超链接(使用 <a href=),例如 /16/on 用于开启 GPIO 16,等等。
这是草图中包含的完整 HTML 代码:
<!DOCTYPE html><html>
<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<link rel=\"icon\" href=\"data:,\">
<style>html {font-family: Helvetica; display: inline-block;
margin: 0px auto; text-align: center;}
".button {background-color: #ff0000; border: none; color: white; padding: 8px 20px;"
"text-decoration: none; font-size: 20px; margin: 2px; cursor: pointer; border-radius: 12px;}"
</style></head>
<body><h3>Remote ESP32 Output Controls</h3>
<p><b>Output 1 is "
// Insert outputPin1State:
" - </b><a href=\"/16/on\"><button class=\"button\">On</button></a>
<a href=\"/16/off\"><button class=\"button\">Off</button></a></p>
<p><b>Output 2 is "
// Insert outputPin2State:
" - </b><a href=\"/17/on\"><button class=\"button\">On</button></a>
<a href=\"/17/off\"><button class=\"button\">Off</button></a></p>
<p><b>Output 3 is "
// Insert outputPin3State:
" - </b><a href=\"/18/on\"><button class=\"button\">On</button></a>"
"<a href=\"/18/off\"><button class=\"button\">Off</button></a></p>
<p><b>Output 4 is ");
// Insert outputPin4State:
" - </b><a href=\"/19/on\"><button class=\"button\">On</button></a>
<a href=\"/19/off\"><button class=\"button\">Off</button></a></p>
</body></html>
我鼓励你尝试修改按钮的大小、形状、链接等内容,以帮助你掌握构建界面的过程。
保持该项目的电路已组装好。你将在下一部分使用它,学习如何在 ESP32 开发板上使用脉冲宽度调制。
脉冲宽度调制
就像通常的 Arduino 和兼容板一样,ESP32 开发板提供了所有 GPIO 引脚的脉冲宽度调制(PWM)。PWM 使你能够控制 LED 的感知亮度,而不仅仅是开关 LED。LED 的亮度由占空比决定,即 GPIO 引脚处于开启状态的时间(LED 点亮)与关闭状态的时间(LED 熄灭)的比例。
占空比是指引脚在每个 周期 中处于开启状态的时间百分比,周期是指引脚可以开或关的固定时间段。占空比越大,连接到引脚的 LED 的感知亮度就越高。此外,PWM 信号的频率越高,即信号开关的速度越快,视觉效果越平滑。如果你在控制电机,更高的 PWM 频率使得转速更加接近所需的实际速度。
图 19-13 展示了四种可能的 PWM 占空比。填充的灰色区域表示 LED 点亮的时间;如你所见,随着占空比的增加,这一时间也在增加。

图 19-13:各种 PWM 占空比
ESP32 模块有 16 个 PWM 通道(编号 0 到 15)。你可以将每个通道指向一个 GPIO 引脚。为了做到这一点,你需要为每个要使用的引脚在 void setup() 中添加两行代码。第一行是
ledcSetup(`channel`, `frequency`, `resolution`);
其中,channel是要使用的 PWM 通道(0 到 15),frequency是 PWM 频率,resolution是占空比的精度。对于frequency,我建议使用 5,000,即 5 kHz。对于resolution,使用 8 位分辨率,得到一个 0 到 255 之间的值用于占空比(就像你在使用 Arduino 或兼容板时使用analogWrite()一样)。
第二行需要的是
ledcAttachPin(`GPIO`, `channel`);
其中,GPIO是要使用的 GPIO 引脚的编号,channel是要使用的 PWM 通道,正如前一行代码中所定义的。
最后,若要在 GPIO 上启用 PWM,使用
ledcWrite(`channel`, `dutyCycle`);
其中,channel是要控制的 PWM 通道,dutyCycle当然是占空比。
要停止在特定 GPIO 引脚上使用 PWM,并改为将其用作数字输入或输出,使用
ledcDetachPin(`GPIO`);
其中,GPIO是 GPIO 引脚的编号(而非 PWM 通道)。
你可以使用以下示例代码测试 PWM,并尝试这些参数,该代码使用了你在本章中一直使用的相同四个 LED 电路。在你输入并上传清单 19-2 后,所有四个 LED 应该会使用 PWM 展现“呼吸”效果,通过增加和减少占空比值来实现。
void setup()
{
❶ ledcSetup(0, 5000, 8); // LED channel, frequency, resolution (8-bit)
ledcSetup(1, 5000, 8);
ledcSetup(2, 5000, 8);
ledcSetup(3, 5000, 8);
❷ ledcAttachPin(16, 0); // GPIO, LED channel
ledcAttachPin(17, 1);
ledcAttachPin(18, 2);
ledcAttachPin(19, 3);
}
void loop()
{
int dutyCycle;
for (dutyCycle = 0; dutyCycle <= 255; dutyCycle++)
{
❸ ledcWrite(0, dutyCycle);
ledcWrite(1, dutyCycle);
ledcWrite(2, dutyCycle);
ledcWrite(3, dutyCycle);
delay(10);
}
for (dutyCycle = 255; dutyCycle > −0; dutyCycle--)
{
❹ ledcWrite(0, dutyCycle);
ledcWrite(1, dutyCycle);
ledcWrite(2, dutyCycle);
ledcWrite(3, dutyCycle);
delay(10);
}
}
清单 19-2:测试 PWM 输出
这个示例演示了使用 PWM 的三行相关代码,设置所需的 PWM 通道❶,然后将这些通道连接到 GPIO 引脚❷。这允许为每个引脚设置占空比,并按递增❸和递减❹的方式调整。
你将在下一个项目中使用这一技巧。
项目 #57:构建用于用户界面的托管网页
本项目综合了你在本章中学到的所有内容:使用 ESP32 开发板通过 Wi-Fi 控制 GPIO,使用 PWM 控制不同的输出电平,并构建一个用于用户界面的托管网页。你可以将其作为框架,构建需要 PWM 控制的项目,如 LED 灯光效果、通过 MOSFET 控制直流电机,或远程实验声音效果。
这个项目使用与之前项目相同的电路,仅控制连接到 GPIO16 的一个 LED。当电路准备好并且 ESP32 开发板已连接到 PC 时,上传 Project #57 的草图。稍等片刻,像往常一样通过串口监视器确定项目的 IP 地址,然后在浏览器中输入该地址。浏览器应该会显示项目的界面,如图 19-14 所示。

图 19-14:Project #57 的用户界面
你应该能够像往常一样通过适当的按钮打开或关闭 LED。不过这次,你还可以通过 DC−和 DC+按钮逐步减少或增加占空比。你可以点击第二排的四个附加按钮为占空比设置预设值,下面的文本行显示当前的占空比值。例如,图 19-14 显示 LED 关闭(因为占空比为 0),而在图 19-15 中,LED 亮起,占空比为 72/255。

图 19-15:占空比设置为 72/255 的用户界面
让我们看看这是如何工作的:
// Project #57 - ESP32 Remote Control PWM Output
#include <WiFi.h>
WiFiServer server(80); // Set webserver port to 80
// Store your Wi-Fi network credentials:
char* ssid = "`name`"; // Insert name of Wi-Fi access point
char* password = "`password`"; // Insert password
int outputPin1 = 16;
int dutyCycle = 0; ❶
String request; // Stores HTTP request from client (web browser)
unsigned long currentTime = millis();
unsigned long previousTime = 0;
const long timeoutTime = 2000; // Allowed client connection time
void pinOff()
{
ledcDetachPin(16); // Disconnect from PWM
digitalWrite(16, LOW); // Pin off
}
void pinOn()
{
ledcDetachPin(16); // Disconnect from PWM
digitalWrite(16, HIGH); // Usual turn pin on
}
void pinPWM(int dc)
{
// Set up digital pin for PWM:
ledcSetup(0, 5000, 8); // LED channel, frequency, resolution (8-bit)
ledcAttachPin(16, 0); // Attach PWM — GPIO, LED channel
ledcWrite(0, dc); // Set duty cycle
}
void setup()
{
Serial.begin(115200);
pinMode(16, OUTPUT);
pinOff(); ❷
// Connect to Wi-Fi network:
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
// Show indicator while waiting for connection:
{
delay(500);
Serial.print("~");
}
Serial.println(); // Display local IP address on Serial Monitor
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
// Start web server:
server.begin();
}
void loop()
{
// Listen for incoming clients from web browser:
WiFiClient client = server.available();
if (client) // If a request received
{
currentTime = millis();
previousTime = currentTime;
Serial.println("New client connected");
String currentLine = " ";
while (client.connected() && currentTime—previousTime <= timeoutTime)
{// Stay connected for timeoutTime
currentTime = millis();
if (client.available())
{
char c = client.read(); // Display data from client on Serial Monitor
Serial.write(c);
request += c;
if (c == '\n') // Client request has finished
{
if (currentLine.length() == 0)
{
// Send HTTP response back to client:
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// Now do things based on the client request:
if (request.indexOf("GET /off") >= 0)
{
// If request was IP address/off:
pinOff(); ❸
dutyCycle = 0;
} else if (request.indexOf("GET /dcdown") >= 0)
// If request was reduced duty cycle:
{
if (dutyCycle >= 2) ❹
{
pinPWM(dutyCycle − 1);
dutyCycle−−;
} else if (dutyCycle <= 1)
{
pinOff();
dutyCycle = 0;
}
} else if (request.indexOf("GET /dcup") >= 0) ❺
{
// If request was increased duty cycle:
if (dutyCycle > 253)
{
pinOn();
dutyCycle = 255;
}
else if (dutyCycle <= 253)
{
pinPWM(dutyCycle + 1);
dutyCycle++;
}
} else if (request.indexOf("GET /on") >= 0) ❻
// If request was IP address/on:
{
pinOn();
dutyCycle = 255;
}
else if (request.indexOf("GET /35") >= 0) ❼
{
// If request was IP address/35:
pinPWM(35);
dutyCycle = 35;
} else if (request.indexOf("GET /50") >= 0)
// If request was IP address/50:
{
pinPWM(50);
dutyCycle = 50;
}
else if (request.indexOf("GET /65") >= 0)
{
// If request was IP address/65:
pinPWM(65);
dutyCycle = 65;
} else if (request.indexOf("GET /75") >= 0)
// If request was IP address/75:
{
pinPWM(75);
dutyCycle = 75;
}
// Build web page for display on browser:
client.println("<!DOCTYPE html><html>");
client.print("<head><meta name=\"viewport\" content=\"width=device-width");
client.println(", initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
// Build buttons using CSS:
client.print("<style>html {font-family: Helvetica; display: inline-block; ");
client.println("margin: 0px auto; text-align: center;}");
client.print(".button {background-color: #ff0000; border: none; color: ");
client.println("white; padding: 8px 20px;");
client.print("text-decoration: none; font-size: 20px; margin: 2px; cursor: ");
client.println("pointer; border-radius: 12px;}");
client.println("</style></head>");
// Now HTML for layout:
client.println("<body><h3>Remote ESP32 PWM Control</h3>");
client.println("<p><a href=\"/off\"><button class=\"button\">Off</button></a>");
client.print("<a href=\"/dcdown\"><button class=\"button\"> ");
client.println("DC -</button></a>");
client.println(" <a href=\"/dcup\"><button class=\"button\">DC +</button></a>");
client.println(" <a href=\"/on\"><button class=\"button\">On</button></a></p>");
client.println("<p><a href=\"/35\"><button class=\"button\">35</button></a>");
client.println(" <a href=\"/50\"><button class=\"button\">50</button></a>");
client.println(" <a href=\"/60\"><button class=\"button\">65</button></a>");
client.println(" <a href=\"/75\"><button class=\"button\">75</button></a></p>");
client.println(" Duty cycle is set to ");
client.println(dutyCycle);
client.println("/255</p>");
client.println("</body></html>");
// End the response to client request
client.println();
break;
} else {// If you got a newline, then clear currentLine
currentLine = " ";
}
} else if (c != '\r')
{
currentLine += c;
}
}
}
request = " ";
// Close connection to client:
client.stop();
Serial.println("Disconnected.");
}
}
该项目的操作类似于之前的项目,但按钮更多,状态显示方式也不同。
当用户更改 PWM 输出的占空比时,该值会存储在一个整数变量❶中。用户点击“Off”按钮时会调用自定义的pinOff()函数。由于无法通过将 PWM 设置为 0 占空比来关闭 GPIO 输出,因此该函数将 PWM 通道从 GPIO 引脚上分离,然后使用digitalWrite()关闭 GPIO 引脚。pinOn()函数则执行相反的操作;由于无法将 PWM 设置为完全开启,该函数将 PWM 通道分离,然后使用digitalWrite()打开 GPIO 引脚。
该草图使用自定义的PWM(int dc)函数来激活 PWM,将 PWM 通道连接到 GPIO 引脚,并设置所需的占空比,通过参数dc传入。当你第一次启动项目或重置它时,草图会关闭 GPIO 引脚❷。
接下来,草图回顾客户端请求。如果用户按下关闭按钮以关闭 GPIO 引脚,代码会调用pinOff()函数❸,并将dutyCycle变量设置为 0,以便在界面上显示。如果用户按下 DC 按钮❹,占空比会减少 1。如果这样会导致占空比低于 1,则使用pinOff()函数关闭 GPIO。同样,如果用户按下 DC+按钮❺,占空比增加 1。如果这样会导致占空比超过 254,则草图会使用pinOn()函数打开 GPIO。按下开启按钮❻会调用pinOn()函数,并将dutyCycle变量设置为 255,以便在界面上显示。
最后的四个按钮❼将占空比设置为预设值。它们仅作为示例,给你一个了解可能实现的功能的概念。每个请求只是通过pinPWM()设置 PWM 值,然后相应地更新dutyCycle变量。
继续前进
本章演示了如何通过专门设计的网页远程控制 ESP32 开发板,而不是控制操作,你也可以简单地在网页上显示通过 ESP32 生成的数据进行远程查看,例如来自传感器的数据。
在接下来的章节中,你将继续使用 ESP32。在下一章,你将学习如何通过与社交媒体互动来控制它。
第二十章:20 通过 TELEGRAM 远程控制

从远程控制 Arduino,超出本地网络的范围,通常需要付费的物联网服务,例如 Blynk 或 Microsoft Azure,或者一些复杂的编码和时间。为了更简单且成本更低的远程控制方式,你可以使用 Telegram 即时消息服务和基于 ESP32 的 Arduino 兼容板。
Telegram 是一款免费的全球可访问的跨平台加密消息服务,允许用户以相对隐秘的方式进行一对一或群组聊天。得益于 Telegram 的自动化功能,你可以让 Arduino 控制一个 Telegram 用户账户,通过该服务发送和接收数据。这使你能够构建远程控制的设备,并使用 Telegram 从连接到互联网的 Arduino 远程请求数据。
在本章中,我将介绍 Telegram 应用和网页版界面。你将学习如何将 Arduino 程序与 Telegram 库连接。你将学到:
-
创建一个简单的遥控器来控制数字输出引脚
-
构建一个项目,远程检索 BMP180 传感器板生成的数据
-
配置一个你可以从互联网监控的自动化数据传输器
配置你的 Telegram 账户
你可以通过 Telegram 应用或者几乎任何连接到互联网的设备上的网页浏览器进行通信。然而,在你可以在电脑上使用该服务之前,你需要先通过智能手机或平板设备创建一个 Telegram 账户。如果你还没有账户,请访问网站 https://
要通过 Telegram 与你的 Arduino 兼容的 ESP32 板进行通信,你需要创建一个 Telegram “机器人”,这是你自己的自动化 Telegram 用户,用于接收和发送消息到 ESP32。为此,打开 Telegram,登录你的设备账户,搜索用户 BotFather,如图 20-1 所示(Android)。

图 20-1:在 Android 上搜索 Telegram 用户 BotFather 的截图
选择带有蓝色勾选标记的 BotFather 账户,你应该会看到开始界面。
点击 START 继续。Telegram 应该会显示一个选项列表。点击、触摸或在消息中发送 /newbot 来提示您输入一个机器人帐户名称。输入您喜欢的机器人名称后,系统会提示您为机器人设置用户名。输入您喜欢的用户名后,您将获得一个 令牌,这是机器人独有的标识符,您的 Arduino 草图中需要使用它,如图 20-2 所示。

图 20-2: 在 Telegram 中创建机器人帐户
将令牌记下来以供以后参考。如果您在计算机上使用 Telegram,可以将其复制并粘贴到文件中,或者在移动设备的笔记应用中保存。
接下来,您需要确定您的 Telegram 聊天 ID,这是一个唯一的数字,用于验证您 Arduino 向 Telegram 发送的消息。搜索 Telegram 用户 IDBot,并选择带有指纹图标的结果,如图 20-3 所示。

图 20-3: 查找 IDBot Telegram 帐户
打开其帐户后,向 IDBot 发送 /start 消息以初始化通信,然后发送 /getid,如图 20-4 所示。当 IDBot 回复您的聊天 ID 后,请记下该号码,您将需要它和令牌一起使用。

图 20-4: 查找帐户的聊天 ID
现在您已经设置好 Telegram 帐户和机器人,是时候配置您的 Arduino IDE 了。
配置 Arduino IDE
如果您还没有完成,请按照第十九章中“为 ESP32 配置 Arduino IDE”和“测试 ESP32”的说明进行操作。一旦您设置好 ESP32,您需要安装两个库。在库管理器中搜索 UniversalTelegramBot,然后点击 安装。
安装好 UniversalTelegramBot 后,在库管理器中搜索 ArduinoJson,然后点击库描述底部的 安装(除非该库已经与 UniversalTelegramBot 一起安装)。
现在,您准备好创建您的第一个由 Telegram 遥控的项目了。
项目 #58: 遥控四个 LED
本项目演示了一种通过 Telegram 控制四个 LED 的快速简便方法,使用的是你的 Arduino 兼容板上的四个数字输出引脚。凭借你现有的电子学和 Arduino 知识,你应该能够轻松地从这个基本框架中推导出如何控制其他可以通过数字输出引脚触发的设备。
你将需要以下硬件:
-
一块 ESP32 开发板和匹配的 USB 电缆
-
一块无焊面包板
-
各种跳线
-
四个 LED
-
一个 560 Ω,0.25 W,1%精度的电阻器
按照图 20-5 所示组装电路。

图 20-5:项目#58 的原理图
接下来,输入并上传项目#58 的草图,适当添加你的 Wi-Fi 网络凭证到前几行,正如在项目#55 的第十九章中所解释的那样。上传草图后,ESP32 应该会尝试连接到你的 Wi-Fi 网络并与之前创建的 Telegram 机器人进行通信。几秒钟后,打开你设备上的 Telegram 或网页浏览器,搜索你的机器人。
选择你的机器人。你应该会看到机器人的聊天历史页面。你可以通过这个聊天历史来控制你的 ESP32 板。当用户发送/start命令时,任何 Telegram 机器人都应该将其发送回 ESP32,ESP32 会通过一组基本的指令来回应该命令。始终在你的项目中包含这样的指令,以确保用户不会不知道如何控制系统。
在 Telegram 显示启动消息后,你可以发送指令集中包含的命令,并观察 LED 如何响应你的命令。图 20-6 展示了启动机器人并控制 LED 的示例。

图 20-6:通过 Telegram 向 ESP32 板发送各种命令
你应该会收到所有发送命令的响应;同样,良好的编程习惯是写出回应用户的代码,以确保操作已经发生。草图也会在 Arduino IDE 的串口监视器中确认回应,如图 20-7 所示。

图 20-7:项目#58 的示例串口监视器输出
让我们看看这个是如何工作的:
// Project #58 - ESP32 GPIO remote control over Telegram
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
// Enter your Wi-Fi network SSID and password:
const char* ssid = "`wifiname`";
const char* password = "`password`";
// Enter your Telegram bot token and chat ID:
#define botToken "token"
#define chatID "chatID"
❶ #define LED1 17
#define LED2 16
#define LED3 27
#define LED4 14
WiFiClientSecure client;
UniversalTelegramBot bot(botToken, client);
❷ int bot_delay = 1000;
unsigned long lastRun=0;
void processMessages(int numNewMessages)
{
Serial.println("Handling New Message");
Serial.println(String(numNewMessages));
for (int i = 0; i < numNewMessages; i++)
{
// Chat ID of the requester:
❸ String chat_id = String(bot.messages[i].chat_id);
if (chat_id != chatID)
{
bot.sendMessage(chat_id, "Unauthorized user", " ");
continue;
}
// Print the received message:
String user_text = bot.messages[i].text;
Serial.println(user_text);
❹ String your_name = bot.messages[i].from_name;
❺ if (user_text == "/start")
{
String startMessage = "Hello, " + your_name + ".\n";
startMessage += "Choose from the following commands:\n";
startMessage += "(replace x with LED number 1~4)\n";
startMessage += "Send /xon to turn LEDx ON \n";
startMessage += "Send /xoff to turn LEDx ON \n";
startMessage += "Send /alloff to turn all LEDs off \n";
startMessage += "Send /status to check LED states \n";
❻ bot.sendMessage(chat_id, startMessage, " ");
}
❼ if (user_text == "/1on")
{
bot.sendMessage(chat_id, "LED 1 turned on", " ");
digitalWrite(LED1, HIGH);
}
if (user_text == "/1off")
{
bot.sendMessage(chat_id, "LED 1 turned off", " ");
digitalWrite(LED1, LOW);
}
if (user_text == "/2on")
{
bot.sendMessage(chat_id, "LED 2 turned on", " ");
digitalWrite(LED2, HIGH);
}
if (user_text == "/2off")
{
bot.sendMessage(chat_id, "LED 2 turned off", " ");
digitalWrite(LED2, LOW);
}
if (user_text == "/3on")
{
bot.sendMessage(chat_id, "LED 3 turned on", " ");
digitalWrite(LED3, HIGH);
}
if (user_text == "/3off")
{
bot.sendMessage(chat_id, "LED 3 turned off", " ");
digitalWrite(LED3, LOW);
}
if (user_text == "/4on")
{
bot.sendMessage(chat_id, "LED 4 turned on", " ");
digitalWrite(LED4, HIGH);
}
if (user_text == "/4off")
{
bot.sendMessage(chat_id, "LED 4 turned off", " ");
digitalWrite(LED4, LOW);
}
if (user_text == "/alloff")
{
bot.sendMessage(chat_id, "Turning all LEDs off", " ");
digitalWrite(LED1, LOW);
digitalWrite(LED2, LOW);
digitalWrite(LED3, LOW);
digitalWrite(LED4, LOW);
}
if (user_text == "/status")
{
if (digitalRead(LED1))
{
bot.sendMessage(chat_id, "LED1 is on", " ");
} else
{
bot.sendMessage(chat_id, "LED1 is off", " ");
}
if (digitalRead(LED2))
{
bot.sendMessage(chat_id, "LED2 is on", " ");
} else
{
bot.sendMessage(chat_id, "LED2 is off", " ");
}
if (digitalRead(LED3))
{
bot.sendMessage(chat_id, "LED3 is on", " ");
} else
{
bot.sendMessage(chat_id, "LED3 is off", " ");
}
if (digitalRead(LED4))
{
bot.sendMessage(chat_id, "LED4 is on", " ");
} else
{
bot.sendMessage(chat_id, "LED4 is off", " ");
}
}
}
}
void setup()
{
Serial.begin(115200);
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(LED3, OUTPUT);
pinMode(LED4, OUTPUT);
digitalWrite(LED1, LOW);
digitalWrite(LED2, LOW);
digitalWrite(LED3, LOW);
digitalWrite(LED4, LOW);
// Connect to Wi-Fi
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// Add root certificate for api.telegram.org
client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.println("Connecting to Wi-Fi…");
}
Serial.println(WiFi.localIP()); // Display IP address used by ESP32
}
void loop()
{
if (millis() > lastRun + bot_delay)
{
int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
while (numNewMessages)
{
Serial.println("Received message");
processMessages(numNewMessages);
numNewMessages = bot.getUpdates(bot.last_message_received + 1);
}
lastRun = millis();
}
}
草图首先包含所需的库文件,接着是输入 Wi-Fi 网络 ID 和密码的字段,然后是输入 Telegram token 和聊天 ID 的字段。它定义了 LED 的引脚编号,以便在草图的其余部分中方便引用 ❶,然后启动安全 Wi-Fi 连接客户端和 Telegram 机器人接口的实例。接下来,草图设置等待检查是否有新的 Telegram 消息的周期长度为 1,000 毫秒 ❷。
该草图使用自定义函数 processMessages() 来检索 Telegram 机器人接收到的消息,然后根据消息的内容采取相应的操作。整个过程会根据传入的参数 numNewMessages 重复执行。草图首先通过比较你的 Telegram 字母数字聊天 ID 与 Telegram 发送的消息中的聊天 ID 来进行安全检查 ❸。这可以防止黑客试图控制你的 Telegram 机器人。如果两者不匹配,草图会使用 continue 函数跳出循环并忽略接收到的消息。如果匹配,草图将继续将接收到的消息发送到串口监视器进行调试,然后检索附加到你的 Telegram 帐户的 Telegram 用户名 ❹。这用于增加一些个性化的友好设置。
为了决定如何处理接收到的消息,草图将接收到的消息与多个 if 函数进行比较,第一个是 /start ❺。当激活时,草图的这一部分会将多行文本,包括 Telegram 用户名,合并成一个字符串变量,然后使用 bot.sendMessage() ❻ 发送该字符串以在 Telegram 聊天中显示。草图已设置为接受来自用户的各种其他聊天消息,所有消息都以斜杠(/)开头。例如,打开 LED 1 的消息(/1on)在 ❼ 处被检测到。当收到用户的消息时,bot.sendMessage() 函数会向 Telegram 聊天发送一条确认消息,然后打开 LED。草图以类似的方式接收并执行其他可能的消息。
在 void setup() 中,草图配置了 LED 的数字引脚,并将其默认设置为 LOW,初始化了 Wi-Fi 库,并建立了与 Telegram 的安全连接。最后,它将 ESP32 板连接到 Wi-Fi 并将 IP 地址发送到串口监视器。
在 void loop() 中,示例代码每秒检查一次 Telegram 是否有新的聊天消息,使用 if 函数中的比较。每当收到一条消息时,lastRun 的值会被更新为 millis() 返回的值。如果 millis() 的值大于 lastRun 加上 bot_delay(即检查消息之间的延迟时间),那么就该再次检查消息了。然后,从 Telegram 获取新的消息数量,并通过之前描述的 processMessages() 函数逐一处理这些消息。
本示例提供了一个简单的框架,用于处理从 Telegram 接收到的消息,并提供了一个简单的数字输出远程控制。下一个项目将在此框架基础上进行拓展,教你如何通过 Telegram 从远程的 ESP32 板获取数据。
项目 #59:获取远程数据
本项目使你能够按需获取由 BMP180 传感器板(首次在第十章中使用)生成的数据,包括温度、气压或海拔高度。你也可以将此项目作为一个通用框架,通过 Telegram 远程监控参数。
你需要以下硬件:
-
一块 ESP32 开发板和匹配的 USB 电缆
-
一块无焊接面包板
-
各种跳线
-
一块 BMP180 传感器板
按照图 20-8 中的示意图组装电路。BMP180 可以在 3.3 V 和 5 V 下工作,因此在这种情况下不需要电平转换板。

图 20-8:项目 #59 的电路图
一旦你搭建好电路并上传了示例代码,打开 Telegram,进入你的 Telegram 机器人聊天历史,就像之前的项目一样。在 Telegram 中输入 /start。你应该会收到一条消息,列出你可以用来获取传感器数据的命令,如图 20-9 所示。为了增加变化,我使用了 Telegram Windows 应用程序,而不是 Android 应用,来为这个项目截图。

图 20-9:项目 #59 的示例输出
让我们看看这个是如何工作的:
// Project #59 - ESP32 remote data retrieval via Telegram
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
❶ #include <Adafruit_BMP085.h>
Adafruit_BMP085 bmp;
// Enter your Wi-Fi network SSID and password:
const char* ssid = "`wifiname`";
const char* password = "`password`";
// Enter your Telegram bot token and chatID:
#define botToken "token"
#define chatID "chatID"
WiFiClientSecure client;
UniversalTelegramBot bot(botToken, client);
// Checks for new messages every 1 second:
int bot_delay = 1000;
unsigned long lastRun=0;
void processMessages(int numNewMessages)
{
String welcome; // Used for assembling messages to send
Serial.println("Handling New Message");
Serial.println(String(numNewMessages));
for (int i = 0; i < numNewMessages; i++)
{
// Chat ID of the requester:
String chat_id = String(bot.messages[i].chat_id);
if (chat_id != chatID)
{
bot.sendMessage(chat_id, "Unauthorized user", " ");
continue;
}
// Print the received message:
String user_text = bot.messages[i].text;
Serial.println(user_text);
String your_name = bot.messages[i].from_name;
❷ if (user_text == "/start")
{
welcome = "Hello, " + your_name + ".\n";
welcome += "Choose from the following commands:\n\n";
welcome += "Send /temp for temperature\n";
welcome += "Send /pressure for air pressure\n";
welcome += "Send /altitude for altitude\n";
bot.sendMessage(chat_id, welcome, " ");
}
❸ if (user_text == "/temp")
{
// get BMP180 temperature
welcome = "Temperature (C): ";
welcome += String(bmp.readTemperature());
bot.sendMessage(chat_id, welcome, " ");
}
❹ if (user_text == "/pressure")
{
// Get BMP180 air pressure, convert to hPa:
welcome = "Air pressure (hPa): ";
welcome += String(bmp.readSealevelPressure() / 100);
bot.sendMessage(chat_id, welcome, " ");
}
❺ if (user_text == "/altitude")
{
// Get BMP180 altitude:
welcome = "Altitude (m): ";
welcome += String(bmp.readAltitude());
bot.sendMessage(chat_id, welcome, " ");
}
}
}
void setup()
{
❻ bmp.begin();
Serial.begin(115200);
// Connect to Wi-Fi:
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
// Add root certificate for api.telegram.org:
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.println("Connecting to Wi-Fi…");
}
// Print ESP32 Local IP Address:
Serial.println(WiFi.localIP());
}
void loop()
{
if (millis() > lastRun + bot_delay)
{
int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
while (numNewMessages)
{
Serial.println("Received message");
processMessages(numNewMessages);
numNewMessages = bot.getUpdates(bot.last_message_received + 1);
}
lastRun = millis();
}
}
该草图使用 Telegram 的框架与项目 #58 相同。唯一的区别是为了新硬件的使用所做的更改——在此案例中为 BMP180 传感器——以及用于 Telegram 的通信消息。草图包括 BMP180 传感器的所需库和初始化 ❶ ❻。它修改了之前的 Telegram 启动消息,以适应本项目的目的 ❷,并编写了温度 ❸、气压 ❹ 和高度 ❺ 消息请求的响应程序。你可以轻松修改本项目的草图,以便替换为你自己的操作,用于通过 Telegram 远程控制 ESP32 兼容的 Arduino 开发板。
保持该项目的硬件连接。你将在下一个项目中使用它,在该项目中你将学习如何通过 Telegram 自动化数据传输。
项目 #60:自动化数据传输
该项目会自动定期将 BMP180 传感器板生成的数据发送到你的 Telegram 帐户。与通过消息请求数据不同,你只需打开 Telegram 聊天即可查看最新的项目更新,几乎实时地观看数据的变化。
所需硬件与项目 #59 相同。一旦设置完成,输入并上传项目 #60 的草图,像往常一样在 #define 字段中添加你的 Wi-Fi 网络凭证。稍等片刻,打开 Telegram 机器人聊天窗口。你应该能看到来自 BMP180 的温度,每隔五秒钟更新一次,如 图 20-10 所示。

图 20-10:项目 #60 的示例输出
让我们看看这是如何工作的:
// Project #60 - Automating data transmission over Telegram
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>
#include <ArduinoJson.h>
#include <Adafruit_BMP085.h>
Adafruit_BMP085 bmp;
// Enter your Wi-Fi network SSID and password:
const char* ssid = "`wifiname`";
const char* password = "`password`";
// Enter your Telegram bot token and chatID:
#define botToken "token"
#define chatID "chat ID"
WiFiClientSecure client;
UniversalTelegramBot bot(botToken, client);
void setup()
{
bmp.begin();
Serial.begin(115200);
// Connect to Wi-Fi:
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
client.setCACert(TELEGRAM_CERTIFICATE_ROOT);
// Add root certificate for api.telegram.org:
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.println("Connecting to Wi-Fi…");
}
// Print ESP32 Local IP Address:
Serial.println(WiFi.localIP());
}
void loop()
{
String messageOut; // Used for assembling messages to send
❶ messageOut = "Temperature (C): ";
❷ messageOut += String(bmp.readTemperature());
bot.sendMessage(chatID, messageOut, " ");
delay(5000);
}
由于该项目是单向消息传递形式,所需的代码比之前的项目要短得多。尽管仍然需要为 BMP180、Wi-Fi 和 Telegram 设置配置,但不需要处理接收和响应 Telegram 聊天消息的自定义函数。草图仅通过 void loop() 定期发送一条消息。它声明了一个字符串变量 messageOut 用于存储要发送的文本,然后通过说明文本构建消息,显示数据是温度 ❶ 以及来自 BMP180 的温度值 ❷。然后,它将消息发送到 Telegram 聊天中。经过五秒钟的延迟后,过程会重复进行。
继续前进
在本章中,你学习了如何利用 Telegram 消息应用程序进行远程项目控制或数据检索,无需额外的云服务费用或特殊的智能手机应用程序。你可以利用这些项目通过数字输出引脚控制自己的设备,或者从任何可以连接到 Arduino 或兼容板的传感器中检索数据。
下一章将介绍如何从互联网时间服务器获取当前时间。
第二十一章:21 从互联网时间服务器获取当前时间

当构建依赖于当前时间跟踪的项目(例如数据记录、创建计划事件或仅仅是制作自己的时钟)时,通常使用实时钟表 IC。然而,为了提高时间和日期的准确性,你可以使用 ESP32 兼容 Arduino 的板子从互联网时间服务器获取当前时间和日期。使用 ESP32 还可以节省 RTC IC 模块或独立 Arduino 板的成本,并且可以在代码中设置你所需的时区,而不是在单独的界面中设置。
在本章中,你将学习网络时间协议(NTP),安装并测试 Arduino 的 NTP 客户端库,并提取时间和日期组件用于你自己的项目。你将学会:
-
构建一个带 OLED 显示屏的紧凑型时钟
-
构建一个双时区时钟,显示家里和另一个地点的时间
-
构建一个巨型超亮数字时钟
网络时间协议
NTP 最初在 1980 年代中期标准化,是一种将计算机与协调世界时(正式称为格林威治标准时间,或 GMT)同步的方法。NTP 可以达到 1 毫秒或更好的精度。一般情况下,误差范围约为 100 毫秒,但对于与时间相关的 Arduino 项目来说,十分之一秒的差距通常已经足够准确。
你可以从 NTP 服务器中以通常的 24 小时制时间和标准日期格式或以纪元时间格式获取时间。纪元时间是自 1970 年 1 月 1 日起的秒数,你可以基于此时间进行自己的计算,以确定当前的日期和时间。有些人使用纪元时间来计算自己的时间类型,例如公制时间。
世界各地有许多 NTP 服务器,每个服务器都有自己的地址。通常,离你的位置最近的服务器应该能提供最快的连接,但你可能希望根据互联网路由使用不同的服务器。请访问https://
你还需要知道你的时区偏移:即相对于格林威治标准时间(GMT)或更现代的协调世界时(UTC)的小时数。例如,如果你住在伦敦,将时区偏移设置为 0,因为伦敦时区属于 UTC(或 GMT)0。如果你住在洛杉矶,加利福尼亚州,时区为 UTC −8。这意味着你应该使用−28,800 的值,因为洛杉矶比 UTC 晚 8 小时,将其乘以 3,600 来转换为秒。最后,更新间隔值 60,000 是以毫秒为单位的更新间隔。这是设置为默认值,等于一分钟,因为你不需要每秒钟更新时间超过一次。
为了准备本章的项目,直接从我的 GitHub 页面下载并安装 Arduino 的 NTP 客户端库。访问https://

图 21-1:下载 NTP 客户端库
打开 Arduino IDE,并通过选择草图
包含库
添加 .ZIP 库来安装库。安装完成后,重新启动 Arduino IDE 以继续操作。
现在,你可以开始在项目中使用互联网时间了。
项目#61:获取时间和日期
该项目作为一个框架,用于从 NTP 服务器获取时间和日期,然后提取日期和时间的各个元素(小时、分钟等),以便你在自己的项目中使用。
该项目使用串口监视器显示时间和日期,因此在硬件方面,你只需要 ESP32 开发板和与前几章相同的 USB 电缆。如果你还没有完成,请转到第十九章,按照“为 ESP32 配置 Arduino IDE”到“测试 ESP32”的步骤安装 Arduino IDE 所需的板卡配置。
输入并上传项目#61 的草图,在草图顶部附近的 SSID 和密码字段中添加你的 Wi-Fi 网络凭证(如在第十九章中项目#55 部分所述)。上传草图后,ESP32 应该尝试连接到你的 Wi-Fi 网络,并在串口监视器中检索并显示时间和日期信息,如图 21-2 所示。

图 21-2:项目 #61 的示例输出
让我们更仔细地看看它是如何工作的:
// Project #61 - Retrieving Internet time
❶ #include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
WiFiUDP ntpUDP;
❷ // Enter your Wi-Fi network SSID and password:
const char* ssid = "`SSID`";
const char* password = "`password`";
// ntpUDP, time server pool, offset in seconds, update interval (mS):
NTPClient timeClient(ntpUDP, "`0.north-america.pool.ntp.org`", `36000`, 60000);
void setup()
{
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi ");
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
timeClient.begin();
}
void loop()
{
String dateString;
String date, month, year;
❸ timeClient.update();
Serial.println(timeClient.getEpochTime());
Serial.println(timeClient.getFormattedDate());
Serial.println(timeClient.getFormattedTime());
// Retrieve and display day of week
❹ Serial.print("Today is ");
switch (timeClient.getDay())
{
case 0: Serial.println("Sunday"); break;
case 1: Serial.println("Monday"); break;
case 2: Serial.println("Tuesday"); break;
case 3: Serial.println("Wednesday"); break;
case 4: Serial.println("Thursday"); break;
case 5: Serial.println("Friday"); break;
case 6: Serial.println("Saturday"); break;
case 7: Serial.println("Sunday"); break;
}
// Retrieve hours, minutes, and seconds separately and display:
if (timeClient.getHours() < 10)
{
Serial.print("0");
}
Serial.print(timeClient.getHours());
Serial.print("-");
if (timeClient.getMinutes() < 10)
{
Serial.print("0");
}
Serial.print(timeClient.getMinutes());
Serial.print("-");
if (timeClient.getSeconds() < 10)
{
Serial.print("0");
}
Serial.println(timeClient.getSeconds());
❺ // Extract data from formatted date result and display:
dateString = timeClient.getFormattedDate();
date = dateString.substring(8, 10);
Serial.print(date);
Serial.print("/");
month = dateString.substring(5, 7);
Serial.print(month);
Serial.print("/");
year = dateString.substring(0, 4);
Serial.println(year);
Serial.println();
delay(1000);
}
首先,草图包含了 Wi-Fi 和 NTP 客户端所需的库,并创建了一个定时器服务器实例 ntp ❶。接下来,它存储了 Wi-Fi 网络的名称和密码,以供 Wi-Fi 库使用 ❷。
NTPClient timeClient() 函数接受要使用的 NTP 服务器地址、所需的时区偏移量和更新时间间隔。如前节所述,尝试使用代码中提供的默认时间服务器地址,或查找一个离您位置更近的服务器地址。对于时区偏移,请将代码中的值替换为适合您所在位置的偏移量。
在 void loop() 中,声明了多个字符串变量用于存储时间和日期信息,接着强制更新时间客户端 ❸ 以从服务器获取最新的时间和日期。接下来的三个函数演示了如何以不同的格式检索时间和日期信息,并将其值显示在串口监视器上。第一个是 timeClient.getEpochTime(),它获取纪元时间。接着,timeClient.getFormattedDate() 显示设置时区后的完整时间和日期,日期格式为 yyyy-mm-dd,后跟字母 T(表示“时间”),然后是当前时间的 24 小时制。最后,timeClient.getFormattedTime() 显示当前时间,格式为 hh:mm:ss。
接下来,草图演示了如何提取单独的时间和日期信息。timeClient.getDay() 函数返回一个介于 0 和 6 之间的整数,表示星期几,分别是从星期天到星期六。这个值用于 switch…case 函数 ❹ 来确定当前是星期几,并在串口监视器上显示对应的名称。草图通过函数 timeClient.getHours()、timeClient.getMinutes() 和 timeClient.getSeconds() 显示小时、分钟和秒的单独值,这些函数都返回对应的整数值。代码还包括一个测试,检查分钟和秒是否小于 10,并在必要时添加前导零,以确保正确的时间格式。例如,防止显示为 9:5:00,表示早上 9 点 5 分。
获取日期、月份和年份的方式稍有不同。程序首先使用 timeClient.getFormattedDate()❺ 获取完整的时间和日期,然后使用 .substring 函数将日期、月份和年份的值存储在字符串变量中。例如,年份位于字符串 dateString 的前四个字符中,因此函数 dateString.substring(0,4) 将返回这四个字符。最后,程序会延迟 1 秒后重复该过程。
如果你的项目与 Wi-Fi 网络断开连接,时间将保持不变,直到 ESP32 能够重新连接到 NTP 服务器,届时时间和日期将自动更新。
以下项目使用此代码框架来获取时间和日期,并在不同的设备上显示结果。
项目 #62:在 OLED 上显示时间和日期
本项目展示了如何在你在第十章中首次使用的廉价 OLED 显示屏上显示来自 NTP 服务器的时间和日期。如果愿意,你可以修改本项目来控制其他类型的显示器,比如 LCD 或者甚至是尼克管显示器。
你将需要以下硬件:
-
一块 ESP32 Arduino 兼容板和 USB 电缆
-
一个 128 × 32 像素、0.91 英寸的 OLED 显示屏
-
一个无焊接面包板
-
各种跳线
如果这是你第一次使用 OLED 显示屏,请转到第十章,按照第 149 页的“图形 OLED 显示屏”部分中的说明,测试 OLED 显示屏,然后按照图 21-3 中的电路图组装电路。
注意
尽管你的 OLED 可能标明为 5 V 设备,但指定型号在 3.3 V 下也能正常工作,无需电平转换器。

图 21-3:项目 #62 的电路图
输入并上传项目 #62 的代码,适当更新你的 Wi-Fi 网络详细信息。OLED 应该显示一条信息,告诉你 ESP32 正在尝试连接 Wi-Fi,如图 21-4 所示。

图 21-4:项目连接到 Wi-Fi
一旦项目连接到 NTP 服务器,OLED 显示屏应该显示当前时间和日期,以及星期几,如图 21-5 所示。

图 21-5:项目 #62 的示例输出
让我们更仔细地看看它是如何工作的:
// Project #62 - OLED NTP clock
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// Enter your Wi-Fi network SSID and password
const char* ssid = "`SSID`";
const char* password = "`Password`";
WiFiUDP ntpUDP;
❶ // ntpUDP, time server pool, offset in seconds, update interval (mS)
NTPClient timeClient(ntpUDP, "`0.us.pool.ntp.org`", `36000`, 60000);
void setup()
{
Wire.begin();
u8g2.begin();
u8g2.setFont(u8g2_font_9x18_tr);
Serial.begin(115200);
❷ WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi ");
while (WiFi.status() != WL_CONNECTED)
{
u8g2.clearBuffer();
u8g2.drawStr(0, 16, "Connecting");
u8g2.drawStr(0, 31, "to Wi-Fi"…");
u8g2.sendBuffer();
}
timeClient.begin();
}
void loop()
{
int hours, minutes, seconds;
int date, month, year, dayOfWeek;
String hh, mm, ss, dateString;
String OLED1, OLED2;
timeClient.update();
// Assemble time:
❸ hours = timeClient.getHours();
minutes = timeClient.getMinutes();
seconds = timeClient.getSeconds();
if (hours < 10) {hh = hh + "0";}
if (minutes < 10) {mm = mm + "0";}
if (seconds < 10) {ss = ss + "0";}
hh = hh + String(hours);
mm = mm + String(minutes);
ss = ss + String(seconds);
❹ OLED1 = hh + ":" + mm + ":" + ss;
// Assemble date:
❺ dateString = timeClient.getFormattedDate();
switch (timeClient.getDay())
{
case 0: OLED2 = OLED2 + "Sun "; break;
case 1: OLED2 = OLED2 + "Mon "; break;
case 2: OLED2 = OLED2 + "Tue "; break;
case 3: OLED2 = OLED2 + "Wed "; break;
case 4: OLED2 = OLED2 + "Thu "; break;
case 5: OLED2 = OLED2 + "Fri "; break;
case 6: OLED2 = OLED2 + "Sat "; break;
case 7: OLED2 = OLED2 + "Sun "; break;
}
❻ OLED2 = OLED2 + dateString.substring(8, 10); // Date
OLED2 = OLED2 + "/";
OLED2 = OLED2 + dateString.substring(5, 7); // Month
OLED2 = OLED2 + "/";
OLED2 = OLED2 + dateString.substring(0, 4); // Year
// Show time and date on OLED:
❼ u8g2.clearBuffer();
u8g2.drawStr(0, 16, OLED1.c_str());
u8g2.drawStr(0, 31, OLED2.c_str());
u8g2.sendBuffer();
delay(1000);
}
这个草图包括并初始化了 Wi-Fi、NTP 客户端、I²C 总线和 OLED 所需的库。像往常一样,设置你的 Wi-Fi 网络详细信息。
然后,草图初始化了 NTP 客户端的一个实例,并设置了池服务器、时区偏移和更新时间间隔❶。在void setup()中,它启动了 I²C 总线、OLED 显示库和用于调试的串口监视器,然后初始化了 Wi-Fi。接下来的代码块会使“OLED 正在连接...”消息在连接过程中显示❷。草图接着启动了 NTP 客户端库。
void loop()中的代码旨在组合时间信息并将其显示在 OLED 的顶部行,然后组合日期信息并将其显示在底部行。首先通过获取时间❸,然后将一个 0 添加到字符串OLED1中,如果小时、分钟或秒数小于 10,字符串将包含这些数据。接着,小时、分钟和秒数被添加到各自的字符串变量中,最终它们会被拼接成一个字符串进行显示❹。
获取日期字符串❺之后,代码获取星期几并将其放入字符串OLED2中,接着利用switch…case函数显示完整的日期。然后,它获取日期、月份和年份,并将这些数据添加到主日期字符串中,同时使用分隔符进行整洁的显示❻。
最后,草图将两行数据OLED1和OLED2发送到 OLED 显示器进行显示❼。如代码所示,必须在变量名后添加后缀.c_str,以告诉 OLED 库将变量从字符串转换为可用的数据。
保持这个项目的硬件组件已组装好。在下一个项目中,你将使用它制作一个双时区时钟,利用 NTP 库中的偏移功能。
项目 #63:在 OLED 上显示两个时区
这个项目展示了如何在前一个项目使用的 OLED 显示器上同时显示两个时区的时间。如果你是 HAM 无线电爱好者,或者你经常与不同时间区的朋友、家人或同事沟通,这可能会很有用。
所需的硬件和组装与项目#62 相同。硬件准备好后,进入并上传项目#63 的草图。别忘了将你的 Wi-Fi 网络凭证添加到这个草图中。ESP32 连接到 Wi-Fi 网络后,OLED 应显示澳大利亚东海岸的“家庭时区”时间(我所在的地方),并显示美国旧金山的“离开时区”时间(No Starch Press 的总部),如图 21-6 所示。

图 21-6:项目#63 的示例显示
为了学习如何更改时区,让我们仔细看看这个草图:
// Project #63 - OLED dual-zone NTP clock
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <U8g2lib.h>
#include <Wire.h>
U8G2_SSD1306_128X32_UNIVISION_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
// Enter your Wi-Fi network SSID and password:
const char* ssid = "`SSID`";
const char* password = "`Password`";
WiFiUDP ntpUDP;
// ntpUDP, time server pool, offset in seconds, update interval (mS)
// Starts with home offset (e.g. 36000 for UTC + 10)
NTPClient timeClient(ntpUDP, "`0.us.pool.ntp.org`", `36000`, 60000);
void setup()
{
Wire.begin();
u8g2.begin();
u8g2.setFont(u8g2_font_9x18_tr);
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi ");
while (WiFi.status() != WL_CONNECTED)
{
u8g2.clearBuffer();
u8g2.drawStr(0, 16, "Connecting");
u8g2.drawStr(0, 31, "to Wi-Fi…");
u8g2.sendBuffer();
}
timeClient.begin();
}
void loop()
{
int hours, minutes, seconds;
String hh, mm, ss, dateString;
String OLED1, OLED2;
timeClient.update();
// Assemble home time (BNE, UTC + 10):
❶ timeClient.setTimeOffset(`36000`);
hours = timeClient.getHours();
minutes = timeClient.getMinutes();
seconds = timeClient.getSeconds();
if (hours < 10) {hh = hh + "0";}
if (minutes < 10) {mm = mm + "0";}
if (seconds < 10) {ss = ss + "0";}
hh = hh + String(hours);
mm = mm + String(minutes);
ss = ss + String(seconds);
❷ OLED1 = "Home " + hh + ":" + mm + ":" + ss;
❸ hh = " "; mm = " "; ss = " ";
// Assemble away time (SFO, UTC - 8):
timeClient.setTimeOffset(−`28800`);
hours = timeClient.getHours();
minutes = timeClient.getMinutes();
seconds = timeClient.getSeconds();
if (hours < 10) {hh = hh + "0";}
if (minutes < 10) {mm = mm + "0";}
if (seconds < 10) {ss = ss + "0";}
hh = hh + String(hours);
mm = mm + String(minutes);
ss = ss + String(seconds);
❹ OLED2 = "SFO " + hh + ":" + mm + ":" + ss;
// Show time and date on OLED:
❺ u8g2.clearBuffer();
u8g2.drawStr(0, 16, OLED1.c_str());
u8g2.drawStr(0, 31, OLED2.c_str());
u8g2.sendBuffer();
delay(1000);
}
这个草图与项目#62 的草图类似,同样将两行数据组装并显示到 OLED 上。为了创建一个双时区时钟,代码首先设置第一个时区的时区偏移量❶。草图中的示例本地时区是布里斯班,布里斯班的时区是 UTC+10,因此timeClient.setTimeOffset()函数的偏移值是 36,000(3,600 乘以+10 小时),但是你可以将其更改为任何你喜欢的时区。接着,草图获取时间数据并将其组装到字符串变量OLED1 ❷中,在前面加上标签Home。草图然后清空临时存储获取时间数据的字符串变量,供以后重用❸。
相同的过程会对第二个时区重复。我使用了旧金山的例子,旧金山的时区是 UTC-8,这意味着timeClient.setTimeOffset()函数的偏移值为-28,800(3,600 乘以-8 小时)。草图将获取并组装时间数据到字符串变量OLED2 ❹中,以“离开时区”的标签SFO开头。最后,它将这两组数据发送到 OLED 显示❺,然后稍作延迟后整个过程再次重复。
如果你想挑战自己,可以尝试修改这个项目,在按下按钮时切换显示不同的时区。
项目#64:构建一个巨型数字时钟
本章的最终项目,让我们来点乐趣,使用项目#27 中来自第八章的七段 LED 模块构建一个巨型数字时钟。组装完成后,你可以将这个项目用作车间里的时间工具,展示你的技术实力。
如果你还没有设置七段 LED 模块,回顾一下项目 #27 并制作四个需要的显示板。(如果你想挑战自己,可以做六个显示板,这样就可以同时显示秒数、分钟和小时了。)除了这个项目中涉及的显示板、布线和 9 V DC 1A 电源,你还需要本章节中使用的 ESP32 板以及常规的跳线和 USB 电缆。
注意
LED 显示板中使用的 TPIC6B595 移位寄存器 IC 既支持 3.3 V 也支持 5 V,所以你不需要在它们与 ESP32 板之间连接电平转换器。
一旦连接好四个显示板,上传项目 #64 的草图。按照 图 21-7 中的连接图,连接 ESP32 到第一个显示板(图中左侧的那个显示板)。

图 21-7:ESP32 与第一个显示板之间的连接布局
将电源连接到第一个显示板。ESP32 应该连接到 Wi-Fi 网络并获取时间。稍微退后,你就能看到四个数字同时亮起,显示当前时间,如 图 21-8 所示。

图 21-8:项目 #64 的运行示例
让我们看看这个草图是如何工作的:
// Project #64 - Giant LED NTP Clock
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
// Enter your Wi-Fi network SSID and password:
const char* ssid = "`SSID`";
const char* password = "`Password`";
WiFiUDP ntpUDP;
// ntpUDP, time server pool, offset in seconds, update interval (mS)
// Starts with home offset (e.g. 36000 for UTC + 10)
NTPClient timeClient(ntpUDP, "`0.us.pool.ntp.org`", `36000`, 60000);
❶ #define LATCH 5 // Latch RCK pin
#define CLOCK 18 // Clock SRCK pin
#define DATA 23 // Data SERIN pin
int digits[] = { B00111111, // 0
B00000110, // 1
B01011011, // 2
B01001111, // 3
B01100110, // 4
B01101101, // 5
B01111101, // 6
B00000111, // 7
B01111111, // 8
B01100111}; // 9
void sendNumbers(int numbers[], int dp)
{
digitalWrite(LATCH, LOW);
for (int i = 0; i < 4; i++)
{
int dig_idx = numbers[i];
if (dp == i)
{
// Display the digit:
shiftOut(DATA, CLOCK, MSBFIRST, digits[dig_idx] | B10000000);
} else
{
shiftOut(DATA, CLOCK, MSBFIRST, digits[dig_idx]);
}
}
digitalWrite(LATCH, HIGH);
}
void setup()
{
pinMode(LATCH, OUTPUT);
pinMode(CLOCK, OUTPUT);
pinMode(DATA, OUTPUT);
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi…");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
❷ timeClient.begin();
}
void loop()
{
int numbers[4];
int hours, minutes;
❸ timeClient.update();
hours = timeClient.getHours();
minutes = timeClient.getMinutes();
❹ numbers[3] = hours / 10;
numbers[2] = hours % 10;
numbers[1] = minutes / 10;
numbers[0] = minutes % 10;
❺ sendNumbers(numbers, 2);
delay(1000);
}
这个草图中的操作现在应该对你来说已经很熟悉了。代码包含了所需的库和 Wi-Fi 网络详情。接着,它创建了一个 NTP 客户端实例,包含了池服务器、时区偏移和更新时间间隔的细节。为了便于参考,草图还定义了用于向显示板上的移位寄存器输出的引脚编号 ❶。
数组 digits[] 存储了用于定义数字在 LED 显示板上如何显示的 10 个字节数据,其中每一位代表组成数字的七个段之一。自定义的 sendNumbers() 函数接受一个包含四个整数的数组,以在四个 LED 显示板上显示,并且还可以接受另一个整数,如果使用这个整数,就会在相应的 LED 显示板上点亮小数点。
在 void setup() 中,代码初始化了所需的数字引脚,设置了串口监视器进行调试,启动了 Wi-Fi 连接和 NTP 客户端 ❷。在 void loop() 中,代码更新 NTP 客户端以获取最新的时间信息 ❸,然后检索并存储小时和分钟的值。时间在发送到 LED 显示板之前必须拆解成单个数字,因此对小时和分钟值进行取模和除法操作 ❹,返回第一个和第二个数字,然后将其存储在数组 numbers[] 中。最后,代码通过 sendNumbers() 函数将这些数字发送到显示屏,且在第二个数字处放置小数点作为小时和分钟之间的分隔符 ❺。
作为最终挑战,你可以尝试修改本章任何项目中的代码,将时间格式从 24 小时制改为 12 小时制。为此,你需要将任何大于或等于 13 的小时值减去 12,然后再显示该小时数。当然,你也可以增加一个 AM 和 PM 的测试。
继续前进
在这一章中,你学习了如何从互联网获取准确的时间和日期信息,并使用各种显示类型展示这些信息。掌握了这些技能后,你可以尝试构建其他类型的时钟,作为练习,为需要时间和日期的其他项目做准备。
下一章介绍了另一种使用 ESP32 的方法,演示了如何将开发板的数据捕获到 Google Sheets 应用中进行分析。
第二十二章:22 将数据捕获并记录到 Google Sheets

你可以使用像 ESP32 这样的联网开发板来捕捉数据并将其存储在在线电子表格中,进行实时监控和后期分析。在本章中,你将使用这种方法构建一个项目,将时间和温度数据通过 Google Workspace 中的 Google Sheets 电子表格工具发送到 Google Sheets,Google Workspace 是一个云计算协作工具集合,几乎所有支持网络的设备都能访问。
你可以在未来的项目中使用这个框架,记录任何你能够通过兼容 Arduino 的项目收集到的数据。例如,你可能想与其他同事共享远程地点的天气数据,或者在度假时,通过手机监控实验室实验中的传感器值。
项目 #65:记录时间和温度数据
本项目演示了如何通过 ESP32 开发板将来自 NTP 服务器的时间和日期,以及来自 BMP180 传感器板的温度和气压数据,发送到 Google Workspace 的 Google Sheets 工具。
你将需要以下硬件:
-
一块 ESP32 开发板及匹配的 USB 电缆
-
无焊接面包板
-
各种跳线
-
一块 BMP180 温度和压力传感器板
如果你还没有这样做,请转到 第十九章,完成“为 ESP32 配置 Arduino IDE”到“测试 ESP32”的说明,以确保 ESP32 工作正常。同时,确保你已经完成了 第十章 中的“BMP180 传感器”部分,并阅读了 第二十一章 来熟悉 NTP 服务器。
为了构建这个项目,你将首先配置一个 Google Workspace 账户来接收来自 ESP32 的数据,然后设置 ESP32 开发板和传感器进行数据传输。
准备 Google Sheets 文档
Google Sheets 是 Google Workspace 云计算产品的一部分,提供基于云的电子表格。如果你还没有账户,可以在 https://
登录你的账户后,访问 https://

图 22-1:Google Sheets 起始页
通过点击空白图标,在“开始新电子表格”下创建一个新的空白电子表格。该电子表格的新标签将会出现,如图 22-2 所示。

图 22-2:一个空白电子表格
点击无标题电子表格,并为用于跟踪您所在位置的温度和气压数据的电子表格输入一个合适的名称,例如OfficeWeather,然后按回车键。接下来,将页面底部标签从 Sheet1 重命名为office。
现在,从 A1 单元格开始,按照图 22-3 所示,给第 1 行的列命名,分别为date, month, year, hour, minute, second, temperature, airpressure。这些标题将与 ESP32 板发送的数据匹配,稍后您将看到。

图 22-3:电子表格设置,带有名称和数据标题
请注意,Google Sheets 中的列标题必须始终小写,并且不能包含任何空格或符号。
等待 Google 自动保存电子表格,然后检索并保存电子表格 ID。该 ID 是电子表格 URL 中/d和/edit之间的一长串字符,如图 22-4 所示。复制该电子表格 ID 并将其粘贴到文本文件中,以便以后轻松获取。

图 22-4:电子表格 ID 在 URL 中的示例
现在,您的电子表格已准备好接收数据。暂时将其保持在网页浏览器中打开。
Google Apps 脚本
您的 ESP32 硬件将通过在 HTTP 地址末尾附加数据来向 Google 电子表格发送数据。为了启用这一过程,您必须将一小段 JavaScript 代码上传到 Google 服务器,该代码解码来自 ESP32 板的 HTTP 调用,将数据分开并放入电子表格的正确列中。
通过在 Google Sheets 中选择扩展
Apps 脚本来打开 Google Apps 脚本工具。
一个新的 Apps 脚本编辑器标签将在网页浏览器中打开,如图 22-5 所示。

图 22-5:打开一个新标签
点击无标题项目,并为脚本输入一个名称,例如OfficeWeatherScript。复制以下代码并将其粘贴到编辑器的第 1 行文本中:
var sheet_id = "`sheet_ID`";
var sheet_name = "office";
function doGet(e)
{
var ss = SpreadsheetApp.openById(sheet_id);
var sheet = ss.getSheetByName(sheet_name);
var date = Number(e.parameter.date);
var month = Number(e.parameter.month);
var year = Number(e.parameter.year);
var hour = Number(e.parameter.hour);
var minute = Number(e.parameter.minute);
var second = Number(e.parameter.second);
var temperature = Number(e.parameter.temperature);
var airpressure = Number(e.parameter.airpressure);
sheet.appendRow([date,month,year,hour,minute,second,temperature,airpressure]);
}
将您之前获取的表格 ID 插入代码第一行的引号之间。在接下来的行中,表格标签名称office出现在引号之间。每一列数据的存储都通过var声明表示,按它们在电子表格第 1 行中的顺序排列(从date到airpressure)。
最后,sheet.appendrow() 函数将接收到的数据写入电子表格中的新行。函数中变量出现的顺序决定了数据写入表格的顺序。每一组接收到的数据都会保存到下一个空行,直到达到 Google 表格的最大行数 40,000 行。之后,您将无法再记录更多数据。
现在,您的编辑器页面应该与图 22-6 类似。脚本编辑器会高亮显示任何语法错误,因此在继续之前请检查是否有错误。

图 22-6:完成的应用脚本
若要部署脚本,选择部署
新部署。当新部署窗口出现时,点击齿轮图标选择部署类型。在出现的下拉菜单中,点击Web 应用以打开配置对话框,其中有三个字段。
系统将显示配置对话框,其中有三个字段。在第一个字段“新描述”中,输入OfficeWeather(或您为表格命名的名称)。在 Web 应用字段中选择我,在谁可以访问字段中选择任何人,如图 22-7 所示。点击部署。

图 22-7:部署配置页面
一个弹出窗口应该会提示您授权访问帐户。点击授权访问。
如果系统提示您再次登录 Google 帐号,请进行登录。接下来,您会看到一个新窗口,显示您的电子表格部署 ID 和 Web 应用 URL。
使用窗口中的复制链接功能,将每项内容复制并保存在包含表格 ID 的文档中。您现在可以关闭 Web 浏览器中的 Apps Script 标签。
在设置此项目的硬件之前,测试您的脚本是否正常工作。使用文本编辑器,在新的一行输入 Web 应用的 URL,然后在 URL 的末尾添加以下文本:
**?date=12&month=1&year=2023&hour=12&minute=37&second=30&temperature=28.5&airpressure=9999**
附加到 URL 的文本描述了以 column =data 格式发送到电子表格的数据,每对列和数据之间由问号分隔。这行代码中的数据仅用于测试脚本——稍后你将编程 ESP32 开发板来发送自己的数据。
将整个 URL 复制到网页浏览器的地址栏并按 ENTER 键。应该会发生两件事。首先,网页浏览器应返回一条消息,告诉你脚本已完成但没有返回任何内容,如 图 22-8 所示。

图 22-8:由 Google Apps 脚本返回的消息
该消息意味着脚本已成功执行,但没有将数据发送回网页浏览器。没关系——数据已经发送到电子表格中了。检查你的表格,你应该能看到数据出现在新的一行中,如 图 22-9 所示。如果你愿意,可以返回并编辑输入 URL 末尾的数据,重新提交 URL,并查看另一个数据行被输入到电子表格中。

图 22-9:成功执行 HTML 调用后的电子表格
这个过程是 ESP32 数据记录的基础:草图将传感器捕获的数据与时间和日期一起安排到一个 URL 中,然后执行一个 HTTP 调用,由你之前创建的 Google Apps 脚本处理,将数据输入到电子表格中。如果这个测试没有对你有效,请检查你是否正确按照项目中到目前为止提供的所有指示进行操作。
准备硬件
按照 图 22-10 中所示的方式组装你的硬件。请注意,尽管你的 BMP180 板可能标注为 5 V 设备,但它也可以在 3.3 V 和 ESP32 板上正常工作,无需电平转换器。

图 22-10:项目 #65 的电路图
输入并上传草图,包括在程序开头部分输入你的 Wi-Fi 网络名称(SSID)和密码,正如在 第十九章中的项目 #55 原始解释中所述。别忘了更改你的时区偏移量,正如在 第二十一章“网络时间协议”中所描述的那样,并将你的 Web 应用 URL 插入到相应的字段中。ESP32 应该连接到 Wi-Fi 网络,然后从 NTP 服务器获取时间和日期,从 BMP180 获取温度和气压。它应该将数据合并为一个包含 Web 应用 URL 和数据的字符串,并随后完成对 Google 服务器的 HTTP 请求。
稍等片刻后,你应该能在电子表格中看到数据出现。保持该表格打开,以便看到其几乎实时更新,如 图 22-11 所示。

图 22-11:项目 #65 的示例输出
让我们看看这如何工作:
// Project #65 - Logging time and temperature data to Google Sheets
#include <WiFi.h>
#include <HTTPClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Adafruit_BMP085.h>
WiFiUDP ntpUDP;
Adafruit_BMP085 bmp;
const char* ssid = "`SSID`";
const char* password = "`password`";
// ntpUDP, time server pool, offset in seconds, update interval (mS)
NTPClient timeClient(ntpUDP, "0.au.pool.ntp.org", 36000, 60000);
❶ String webAppURL = "`webapp_URL`";
unsigned long lastUpdate = 0;
❷ unsigned long updatePeriod = 20000; // 20 seconds
void setup()
{
❸ bmp.begin();
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi ");
while (WiFi.status() != WL_CONNECTED)
{
Serial.print(".");
delay(500);
}
timeClient.begin();
}
void loop()
{
String dateString, finalAppURL;
String date, month, year;
String hh, mm, ss;
String temperature, pressure;
❹ if ((millis() - lastUpdate) > updatePeriod)
{
if (WiFi.status() == WL_CONNECTED)
{
HTTPClient http;
timeClient.update();
// Extract date from NTP server:
dateString = timeClient.getFormattedDate();
date = dateString.substring(8, 10);
month = dateString.substring(5, 7);
year = dateString.substring(0, 4);
// Extract time from NTP server:
hh = String(timeClient.getHours());
mm = String(timeClient.getMinutes());
ss = String(timeClient.getSeconds());
// Add time and date to web app URL:
❺ finalAppURL = webAppURL + "?date=" + date + "&month=";
finalAppURL += month + "&year=" + year + "&hour=" + hh;
finalAppURL += "&minute=" + mm + "&second=" + ss;
❻ // Get weather data from BMP180:
temperature = String(bmp.readTemperature());
pressure = String(bmp.readSealevelPressure());
// Add weather data to web app URL:
finalAppURL = finalAppURL + "&temperature=";
finalAppURL += temperature + "&airpressure=" + pressure;
❼ http.begin(finalAppURL.c_str());
❽ int httpCode = http.GET(); // Get response code from server
if (httpCode > 0)
{
Serial.print("HTTP Response code: ");
Serial.println(httpCode);
String httpMessage = http.getString();
Serial.println(httpMessage);
} else
{
Serial.print("HTTP error: ");
Serial.println(httpCode);
}
http.end(
} else
{
Serial.println("Wi-Fi problem.");
}
❾ lastUpdate = millis();
}
}
该草图包含并初始化了所有所需的库,接着是 Wi-Fi 网络名称和密码的字段,然后是网络时间服务器客户端的初始化 ❶。
接着,草图设置了发送不同数据读数到电子表格之间的周期 ❷,以毫秒为单位。这是更新之间的最小时间,但实际的更新频率由 Google 服务器的速度和你的互联网连接决定。例如,参考 图 22-12 中显示的值——尽管演示设置为 20 秒,实际记录的时间为 22 秒或更长。10 秒或更短的值可能无法被记录,因为这不足以让过程完成接收和在 Google 服务器上保存数据的操作。
在 void setup() 中,进行常规的声明,并启动所需的库和串口监视器。草图连接到你的 Wi-Fi 网络并启动网络时间客户端。void loop() 中的代码声明了所需的字符串变量,用于分别存储来自网络时间服务器和 BMP180 传感器的时间和温度数据。
该程序包含了一个计算,以确定在发送更多数据之前是否已经过去了所需的时间 ❸。这允许草图在从上次更新时间的 millis() 计数大于设定周期时继续执行 ❹。一旦周期过去,就该从 NTP 服务器提取日期和时间信息,并将其存储在字符串变量中,然后将这些数据附加到 Web 应用 URL 中 ❺。
接下来,从 BMP180 传感器获取温度和气压数据,并将这些值存储到字符串变量中 ❻。这些变量随后会被附加到已经包含 web 应用 URL 以及日期和时间的巨大字符串 finalAppURL 中。
现在,web 应用 URL 已完全构建,HTTP 调用将数据发送到 Google 服务器 ❼。最后的代码行获取来自 Google 服务器的 HTTP 响应代码 ❽,并将其发送到串口监视器。这对于调试很有帮助,因为它确认了过程是否成功,但一旦草图按要求运行,你可以将其移除。最后,草图记录了自上次更新后的经过时间,这个时间用于计算下一次更新前的延迟 ❾。
未来 Google Sheets 项目的提示
现在你已经有了一个工作机制,将现实世界的数据记录到 Google Sheets 电子表格中,以下是一些构建此类项目的最终提示。首先,由于电子表格存储在 Google 服务器上,因此你只能通过浏览器查看你的项目。你还可以通过在移动设备或平板电脑上使用 Google Sheets 应用来监控项目的进展,如图 22-12 所示。

图 22-12:通过 Android 上的 Google Sheets 应用显示的项目 #65 示例输出
如前所述,Google Sheets 电子表格的最大行数为 40,000。为了同时监控两个或更多的 ESP32 开发板,给它们各自分配独立的电子表格;然后,一旦所需数据被捕获,你可以在桌面上合并并分析这些数据。如果你想编辑现有项目——例如更改电子表格中的变量名或位置——别忘了更新应用脚本,然后创建一个新的部署,使用新的 web 应用 URL,并将其插入到草图中。
继续前进
本章为你提供了一个廉价的框架,用于捕捉任何类型的由 Arduino 兼容板收集的数据,并将其直接发送到电子表格中。这使你能够从世界上几乎任何地方监控实时数据,并以公开或私人方式与他人分享这些数据。你可以使用自己的传感器,改变电子表格中的变量,并根据自己的需要更新时间。
在下一章,你将继续与联网设备合作,使用 ESP32 开发板创建自己的 web 服务器,以只读格式共享信息,供他人查看。
第二十三章:23 构建迷你网页服务器

网页服务器这个术语可能会让你联想到一个满是计算机服务器和各种电缆的房间。然而,任何拥有 IP 地址的设备都可以充当网页服务器,包括你的 ESP32 开发板。得益于它的板载 Wi-Fi 连接和软件库,这块开发板可以响应来自网页浏览器客户端的 HTTP 请求,以显示你所需的数据。
在本章中,你将创建一个迷你网页服务器,用于实时监控 ESP32 开发板收集或生成的任何信息,允许任何有权限访问服务器的人查看捕获的数据,无需额外的软件或工具。你将使用 HTML 设置一个简单的网页,以测试并展示本章中的框架,然后构建一个网页服务器来显示 ESP32 模拟输入和数字输入状态,接着再构建一个网页来显示日期、时间和温度信息。
项目 #66:创建一个基本的文本网页服务器
在本项目中,你将创建一个基本的文本网页服务器。你可以将其作为一个框架,用来展示用 HTML 构建的文本或其他信息。
如果你还没有完成,请转到第十九章,并按照“为 ESP32 配置 Arduino IDE”到“测试 ESP32”部分的说明,确保 ESP32 正常工作。如果需要,务必按照说明设置端口转发。你还应该完成了第十章中的“BMP180 传感器”部分。
对于硬件,你只需要 ESP32 开发板和相应的 USB 数据线。连接好 ESP32 和计算机后,上传项目 #66 的草图,并在适当的字段中填写你的 Wi-Fi 网络和名称,这些内容在第十九章的项目 #55 中已有说明。
打开 Arduino IDE 中的串口监视器。你应该能够看到 ESP32 连接到的网络名称(SSID),以及其在本地网络中的 IP 地址。图 23-1 显示了连接到 IP 地址 192.168.20.10 的情况。

图 23-1:项目 #66 的串口监视器输出
现在,打开一个网页浏览器,在 URL 栏中输入你项目的 IP 地址,如图 23-2 所示。

图 23-2:基本网页服务器的运行示例
让我们来看看这是如何工作的:
// Project #66 - Basic web page server
#include <WiFi.h>
WiFiServer server(80); // Set web server port to 80
char* ssid = "`name`";
char* password = "`password`";
String request; // Stores HTTP request from client (web browser)
unsigned long currentTime = millis();
unsigned long previousTime = 0;
const long timeoutTime = 2000; // Allowed client connection time
void setup()
{
Serial.begin(115200);
// Connect to Wi-Fi network:
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
// Show indicator while waiting for connection:
{
delay(500);
Serial.print(".");
}
Serial.println(); // Display local IP address on Serial Monitor
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
// Start web server:
server.begin();
}
void loop()
{
String millisValue;
String textLine = " ";
// Listen for incoming clients from web browser
WiFiClient client = server.available(); ❶
if (client) // If a request received
{
currentTime = millis();
previousTime = currentTime;
Serial.println("New client connected");
String currentLine = " ";
while (client.connected() && (currentTime - previousTime <= timeoutTime))
{// Stay connected for timeoutTime
currentTime = millis();
if (client.available())
{
char c = client.read();
Serial.write(c);
request += c;
if (c == '\n') // Client request has finished
{
if (currentLine.length() == 0)
{
// Send HTTP response back to client: ❷
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// Build web page for display on browser:
client.println("<!DOCTYPE html><html><body>");
// Now HTML for your page:
client.println("<h1>millis() server</h1>"); ❸
millisValue = String(millis());
textLine += "<p>ESP32 board uptime is : ";
textLine += millisValue;
textLine += " milliseconds</p>";
client.println(textLine);
client.println("</body></html>"); ❹
// End the response to client request:
client.println();
break;
} else
{// If you received a newline, clear currentLine:
currentLine = " ";
}
} else if (c != '\r')
{
currentLine += c;
}
}
}
request = " ";
// Close connection to client:
client.stop(); ❺
Serial.println("Disconnected.");
}
}
草图首先包含 Wi-Fi 库并初始化一个端口号为 80 的 Web 服务器实例。客户端计算机会使用此端口号与 Web 服务器进行连接。也就是说,如果你使用的是静态 IP 地址,你需要在浏览器的 URL 字段中输入 IP 地址,并在后面加上:80,以访问 Web 服务器。
草图接着声明所需的变量,包括timeoutTime,它是客户端可以连接到 Web 服务器的最大时长(以毫秒为单位)。如果你计划允许多个客户端使用 Web 服务器,可能需要减少此值,因为服务器一次只能处理一个客户端请求。
在void setup()中,代码启动了串口监视器,并像往常一样连接到 Wi-Fi 网络。连接成功后,IP 地址会在串口监视器中显示,并且 Web 服务器开始启动。在void loop()中,ESP32 等待来自 HTTP 客户端(远程设备上的 Web 浏览器)的请求 ❶。当接收到请求时,草图记录由millis()返回的当前值,以确定客户端连接的时长。只要客户端连接的时间少于之前设置的限制,Web 服务器将接收客户端请求,并将其发送到串口监视器,用于调试和观察。
当客户端请求结束时,服务器发送以下的 HTTP 响应,格式为 HTML ❷:
HTTP/1.1 200 OK
Content-type:text/html
Connection: close
为了将你的网页 HTML 代码发送回客户端 Web 浏览器,草图使用client.print()将文本发送到 Web 客户端,然后使用.println()将文本及换行符发送到客户端,开始时包括网页初始化。每次形成网页时,都需要从这个页面初始化的 HTML 行开始。
接下来,草图以以下 HTML 代码的形式发送网页内容:
<h1>millis() server</h1>
<p>ESP32 board uptime is : `millis` </p>
草图将 HTML 中的millis替换为millis()的值,通过在
行之后构建一行文本,将其转换为字符串,然后使用client.println(textLine)函数发送该字符串。
草图发送结束网页的行 ❹。与网页初始化代码一样,你将在类似的项目中始终使用这一行来结束网页。最后,草图关闭与客户端的连接 ❺,ESP32 等待新的请求。
在下一个项目中,你将基于刚学到的技能,展示与 ESP32 的 I/O 端口相关的数据在网页上。
项目 #67:创建一个 ESP32 I/O 端口监控器
本项目展示了 ESP32 开发板上四个数字输入引脚和四个模拟输入引脚的状态。这为你创建一个 Web 服务器框架提供了基础,服务器能够显示来自具有模拟输出的设备(例如光传感器或可调电阻)的数据,以及来自具有简单数字输出的设备(例如运动传感器或门控开关)的数据。在这个项目中,你将通过按钮和可调电阻来模拟这些类型的传感器。
你将需要:
-
一块 ESP32 开发板和匹配的 USB 数据线
-
一块无焊面包板
-
各种跳线
-
四个触摸按钮
-
四个 10 kΩ,0.25W,1% 精度的电阻
-
四个 10 kΩ的面包板兼容型可调电阻
按照图 23-3 所示组装电路。

图 23-3:项目 #67 的原理图
上传草图,填写草图顶部的 Wi-Fi 网络和名称,然后在 Arduino IDE 中打开串行监视器。串行监视器应该显示 ESP32 连接的 Wi-Fi 网络的名称(SSID),以及 ESP32 在本地网络上的 IP 地址。
使用本地网络中的设备在网页浏览器中打开这个 IP 地址。你应该能看到由 ESP32 提供的状态网站。调整可调电阻到随机位置,按下电路中的一个或多个按钮,然后刷新网页浏览器。下一个显示应该展示 ADC 引脚测量的值以及四个数字输入的状态,如图 23-4 所示。

图 23-4:项目 #67 的示例输出
与 Arduino Uno 及兼容板不同,后者的 ADC 范围是 0 到 1,023(10 位分辨率),ESP32 开发板的 ADC 范围是 0 到 4,095,因为它使用的是 12 位分辨率(2¹² = 4,096)。你还可以在网页上查看每个模拟引脚状态数据的最后一项,显示 ADC 输入的计算电压。
让我们看看它是如何工作的:
// Project #67 - ESP32 I/O monitor
#include <WiFi.h>
WiFiServer server(80); // Set web server port to 80
char* ssid = "`SSID`";
char* password = "`password`";
String request; // Stores HTTP request from client (web browser)
unsigned long currentTime = millis();
unsigned long previousTime = 0;
const long timeoutTime = 2000; // Allowed client connection time
void setup()
{
Serial.begin(115200);
pinMode(18, INPUT);
pinMode(19, INPUT);
pinMode(23, INPUT);
pinMode(5, INPUT);
// Connect to Wi-Fi network:
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
// Show indicator while waiting for connection:
{
delay(500);
Serial.print(".");
}
Serial.println(); // Display local IP address on Serial Monitor
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
// Start web server
server.begin();
}
void loop()
{
String textLine = " "; ❶
int IO18, IO19, IO23, IO5;
float IO2, IO4, IO35, IO34;
float IO2A, IO4A, IO35A, IO34A;
String IO2S, IO4S, IO35S, IO34S;
// Listen for incoming clients from web browser:
WiFiClient client = server.available();
if (client) // If a request received
{
currentTime = millis();
previousTime = currentTime;
Serial.println("New client connected");
String currentLine = " ";
while (client.connected() && (currentTime - previousTime <= timeoutTime))
{// Stay connected for timeoutTime:
currentTime = millis();
if (client.available())
{
char c = client.read();
Serial.write(c);
request += c;
if (c == '\n') // Client request has finished
{
if (currentLine.length() == 0)
{
// Send HTTP response back to client:
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// Build web page for display on browser:
client.println("<!DOCTYPE html><html><body>");
// Now HTML for your page:
client.println("<h1>ESP32 I/O Monitor</h1>"); ❷
client.print ("<p> Status as of last update.");
client.println("Refresh browser for latest values.</p>");
client.println("<hr>");
IO18 = digitalRead(18); ❸
IO19 = digitalRead(19);
IO23 = digitalRead(23);
IO5 = digitalRead(5);
IO2 = analogRead(2);
IO4 = analogRead(4);
IO35 = analogRead(35);
IO34 = analogRead(34);
IO2A = ((IO2 / 4096) * 3.3); ❹
IO4A = ((IO4 / 4096) * 3.3);
IO35A = ((IO35 / 4096) * 3.3);
IO34A = ((IO34 / 4096) * 3.3);
client.println("<p><b>Digital Input Status:</b></p>"); ❺
textLine += "<p>Digital pin IO18 is ";
if (IO18 == 1)
{
textLine += "HIGH";
} else
{
textLine += "LOW</p>";
}
client.println(textLine);
textLine = " ";
textLine += "<p>Digital pin IO19 is ";
if (IO19 == 1)
{
textLine += "HIGH";
} else
{
textLine += "LOW</p>";
}
client.println(textLine);
textLine = " ";
textLine += "<p>Digital pin IO23 is ";
if (IO23 == 1)
{
textLine += "HIGH";
} else
{
textLine += "LOW</p>";
}
client.println(textLine);
textLine = " ";
textLine += "<p>Digital pin IO5 is ";
if (IO5 == 1)
{
textLine += "HIGH";
} else
{
textLine += "LOW</p>";
}
client.println(textLine);
textLine = " ";
client.println(" ");
client.println("<p><b>Analog Input Status:</b></p>"); ❻
textLine += "Analog pin IO2 raw value: ";
textLine += String(IO2);
textLine += "; Voltage (V): ";
client.println(textLine);
client.println(IO2A);
client.println("</p>");
textLine = " ";
textLine += "Analog pin IO4 raw value: ";
textLine += String(IO4);
textLine += "; Voltage (V): ";
client.println(textLine);
client.println(IO4A);
client.println("</p>");
textLine = " ";
textLine += "Analog pin IO35 raw value: ";
textLine += String(IO35);
textLine += "; Voltage (V): ";
client.println(textLine);
client.println(IO35A);
client.println("</p>");
textLine = " ";
textLine += "Analog pin IO34 raw value: ";
textLine += String(IO34);
textLine += "; Voltage (V): ";
client.println(textLine);
client.println(IO34A);
client.println("</p>");
textLine = " ";
client.println("<hr>");
client.println("</body></html>"); ❼
// End the response to client request:
client.println();
break;
} else
{// If you received a newline, clear currentLine:
currentLine = " ";
}
} else if (c != '\r')
{
currentLine += c;
}
}
}
request = " ";
// Close connection to client:
client.stop();
Serial.println("Disconnected.");
}
}
与之前的项目类似,这个草图在接收到客户端请求后会提供一个 HTML 网页。唯一的不同是需要处理 I/O 值并在 HTML 中显示结果的代码块。
草图声明了变量 ❶,用于存储每个输入的状态——数字输入的整型变量,以及模拟输入的浮点型变量。然后,它使用字符串变量存储来自 ADC 的值,这些值稍后将与 HTML 结合,用于网页显示。网页的主要构建从 ❷ 开始,包含一个大的标题和一些用户文本,后面跟着一条水平线。草图存储数字输入的值(1 代表 HIGH,0 代表 LOW) ❸,然后是四个 ADC 的模拟值,将后者转换为电压 ❹。
为了在网页 ❺ 上显示输入数据,草图测试每个数字引脚的状态,将结果作为文本添加到字符串 textLine 中。它使用 client.print() 显示该文本,并对其他三个数字输入引脚重复此过程。接下来,它显示每个模拟输入的值以及相应的电压 ❻。它将每行所需的 HTML 文本连接到字符串 textLin,然后跟随一个 HTML
命令来结束段落。在草图发送该行以结束网页 ❼ 后,ESP32 关闭连接并等待新的客户端请求。在整个代码中,草图使用函数 String() 将整数或浮点值转换为文本,以将该值添加到更大的字符串变量中。这是将草图生成的数据显示在 HTML 网页中的关键。
为了更多地练习在网页上显示传感器数据,你将在下一个项目中在服务器网页上显示更多类型的数据。
项目 #68:构建时间和天气服务器
本项目基于你现有的知识,创建一个能够根据请求显示当前时间、日期、温度和海平面气压的网页服务器。这是一个很好的示例,展示了如何在一个网页上显示来自多个源的数据。
你将需要:
-
一块 ESP32 开发板和匹配的 USB 数据线
-
一块无焊接面包板
-
各种跳线
-
一块 BMP180 温度和气压传感器板
按照 图 23-5 所示组装电路。

图 23-5:项目 #68 的原理图
上传项目 #68 的草图,像往常一样在代码顶部的字段中添加你的 Wi-Fi 网络和名称,然后在 Arduino IDE 中打开串口监视器。串口监视器应该显示 ESP32 连接的网络名称(SSID),以及它在本地网络中的 IP 地址。
如第二十一章中“网络时间协议”部分所讨论的,你可能需要根据自己的位置更改网络时间服务器的 IP 地址和时区偏移量。
使用本地网络中的设备在网页浏览器中打开 IP 地址。你应该能看到由 ESP32 提供的状态网站,如图 23-6 所示。刷新浏览器以随时查看最新信息。

图 23-6:项目 #68 的示例输出
让我们看看它是如何工作的:
// Project #68 - ESP32 time and weather server
#include <Adafruit_BMP085.h>
Adafruit_BMP085 bmp;
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
WiFiUDP ntpUDP;
WiFiServer server(80); // Set web server port to 80
char* ssid = "`SSID`";
char* password = "`password`";
// ntpUDP, time server pool, offset in seconds, update interval (mS)
NTPClient timeClient(ntpUDP, "`0.au.pool.ntp.org`", 36000, 60000);
String request; // Stores HTTP request from client (web browser)
unsigned long currentTime = millis();
unsigned long previousTime = 0;
const long timeoutTime = 2000; // Allowed client connection time
void setup()
{
Serial.begin(115200);
bmp.begin();
// Connect to Wi-Fi network
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println(); // Display local IP address on Serial Monitor
Serial.print("Connected, IP address: ");
Serial.println(WiFi.localIP());
// Start web server
server.begin();
}
void loop()
{
String dateString;
String date, month, year;
// Listen for incoming clients from web browser:
WiFiClient client = server.available();
if (client) // If a request received
{
currentTime = millis();
previousTime = currentTime;
Serial.println("New client connected");
String currentLine = " ";
while (client.connected() && (currentTime - previousTime <= timeoutTime))
{// Stay connected for timeoutTime
currentTime = millis();
if (client.available())
{
char c = client.read(); // Display data from client on Serial Monitor
Serial.write(c);
request += c;
if (c == '\n') // Client request has finished
{
if (currentLine.length() == 0)
{
// Send HTTP response back to client
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// Build web page for display on browser:
client.println("<!DOCTYPE html><html><body>");
client.println("<h1>ESP32 Time and Weather Server</h1>");
client.println("<p>Refresh browser for latest information.</p>");
client.println("<hr>");
client.print("<p>Time: ");
timeClient.update();
client.print(timeClient.getFormattedTime()); ❶
client.print("</p>");
client.print("<p>Today is ");
switch (timeClient.getDay()) ❷
{
case 0: client.print("Sunday "); break;
case 1: client.print("Monday "); break;
case 2: client.print("Tuesday "); break;
case 3: client.print("Wednesday "); break;
case 4: client.print("Thursday "); break;
case 5: client.print("Friday "); break;
case 6: client.print("Saturday "); break;
case 7: client.print("Sunday "); break;
}
// Retrieve and format date display:
dateString = timeClient.getFormattedDate(); ❸
date = dateString.substring(8, 10);
client.print(date);
client.print("/");
month = dateString.substring(5, 7);
client.print(month);
client.print("/");
year = dateString.substring(0, 4);
client.print(year);
client.print("</p>");
client.println("<hr>"); ❹
client.print("<p>Current temperature (C): ");
client.print(bmp.readTemperature()); ❺
client.print("</p>");
client.print("<p>Current air pressure at ");
client.print("sea level (calculated) (Pa): ");
client.print(bmp.readSealevelPressure()); ❻
client.print("</p>");
client.println("<hr>");
client.println("</body></html>"); ❼
// End the response to client request
client.println();
break;
} else
{// If you received a newline, clear currentLine:
currentLine = " ";
}
} else if (c != '\r')
{
currentLine += c;
}
}
}
request = " ";
// Close connection to client:
client.stop();
Serial.println("Disconnected.");
}
}
提供网页的要求与之前的项目相同;只不过所需的代码不同,用于记录数据和显示的部分不同。草图包括了所需的库,并启动了 Wi-Fi 服务器和网络时间协议客户端实例。
与之前的项目一样,草图连接到本地局域网 Wi-Fi,并在串口监视器上显示连接详情。当收到客户端请求时,ESP32 板发送网页 HTML 给客户端,并请求网络时间服务器中的第一个数据(当前时间) ❶,然后通过 client.print() 函数将时间发送到网页。接下来,草图检索星期几 ❷,通过 switch…case 函数确定星期几的名称,并将所有这些信息发送给客户端。
草图将完整的日期存储在字符串 dateString ❸ 中。日期值(日期、月份和年份)被提取到各自的字符串变量中,以便草图可以使用 client.print() 将它们作为 HTML 的一部分发送出去。
代码添加了一条水平线以便于更整洁的显示 ❹,然后检索并显示海平面的温度 ❺ 和气压 ❻。最后,完成网页 ❼,客户端请求的响应也完成了。
继续前进
在本章中,你学会了创建网页,可以显示任何你能通过与 ESP32 一起使用的设备收集或计算的数据。从简单的天气报告到实验室传感器数据,你可以将这些数据放到网页上,供自己或他人使用。
下一章将介绍一个最终的 IoT 技能集,展示如何使用 ESP32 控制的数码相机进行监控或娱乐。
第二十四章:24 ESP32 摄像头板

过去,要在 Arduino 上使用数码相机,你必须破解相机的远程控制系统,并通过 Arduino 的数字输出引脚控制它。如今,得益于价格便宜的 ESP32 开发板的发布(也称为 ESP32 摄像头板),你可以轻松地在项目中控制小型数码相机,并保存图像以供回顾或通过 Wi-Fi 流媒体传输。
ESP32 摄像头板使用与 第十九章 到 第二十四章 中使用的 ESP32 开发板相同的带 Wi-Fi 的 ESP32 微控制器。然而,摄像头板是一个更加紧凑的单元,并且包括一个连接小型但实用的摄像头模块,通常在购买 ESP32 摄像头板时会随附该模块。它可能也是本书中你用 Arduino 兼容板控制的最复杂设备。
本章中,你将学习如何:
-
为 Arduino 配置 ESP32 摄像头板
-
构建一个简单的视频流设备
-
控制 ESP32 摄像头板以按命令捕捉静态图像
选择 ESP32 摄像头
本章基于 AI-Thinker ESP32 CAM 模块,这是一块小型电路板,包含 ESP32 硬件、microSD 存储卡插槽、小型电源调节器和一个连接器,用于连接小型摄像头模块。包装中通常还会包含摄像头模块。
一些 ESP32 摄像头板,如 PMD Way 的 97263257 部件(如 图 24-1 所示),没有 USB 接口,以节省空间。

图 24-1:一块 ESP32 摄像头板
如果你购买的模块是这种情况,你将需要一根 USB 到串行电缆,如 图 24-2 所示的 PL2303TA 电缆(PMD Way 部件 727859)。你将使用这根电缆为 ESP32 摄像头供电,以便在 Arduino IDE 的串行监视器中上传草图并监控串行输出。

图 24-2:一根 USB 到 TTL 电缆
或者,你可以选择带有匹配 USB 接口板的 ESP32 摄像头,例如 PMD Way 的 26409000 部件,如 图 24-3 所示。

图 24-3:一块带有 USB 接口板的 ESP32 摄像头板
我选择了如图 图 24-1 所示的 ESP32 摄像头板,不仅因为它是最受欢迎的型号,还因为它允许你使用外部 Wi-Fi 天线,从而使你可以与 Wi-Fi 接入点保持更大的操作距离。通常,出厂时模块已配置为使用外部天线。你仍然可以在没有天线的情况下使用它,但 Wi-Fi 信号范围将不理想。
在初步实验后,你可能希望用不同类型的镜头替换默认的摄像头镜头,用于变焦或广角摄影,或者使用更长的电缆将摄像头与 ESP32 摄像头板连接。你可以从 PMD Way 购买这些镜头(例如,查看 26401000 部件)或从你的 ESP32 摄像头板供应商处购买。然而,这些额外的镜头类型并不是本章项目的必要条件。
设置 ESP32 摄像头
一旦你购买了 ESP32 摄像头板,你需要进行设置以便接收草图并正常运行。
如果你还没有完成,转到 第十九章,并完成“为 ESP32 配置 Arduino IDE”到“测试 ESP32”的步骤,以使你的 ESP32 正常工作。接下来,将 ESP32 摄像头连接到电脑。如果你有如图 图 24-2 所示的 USB 接口板,按常规通过 USB 电缆连接。如果你使用的是替代的 USB 到 TTL 电缆连接 USB 接口,请按照 表 24-1 所示的方式连接 ESP32 摄像头的引脚到电缆。
表 24-1: ESP32 摄像头与 USB-TTL 电缆的连接
| ESP32 摄像头引脚 | 电缆连接器 |
|---|---|
| 5V | 5V(红色) |
| GND | GND(黑色) |
| U0T | RX(白色) |
| U0R | TX(绿色) |
如果你使用的是 USB 到 TTL 电缆,在上传草图之前,你还必须将跳线连接到 GND 引脚和 IO0 引脚之间。上传草图后,你必须断开并重新连接 USB 电缆到电脑,然后移除 IO0 到 GND 的跳线,并按下开发板上的重置按钮以启动草图。每次上传草图时,你都需要执行这些步骤。
连接 ESP32 相机到您的 PC 后,打开 Arduino IDE 并将开发板设置为 AI Thinker ESP-32 CAM,方法是选择工具
开发板
esp32。您可能还需要通过选择工具
端口来设置 USB 端口,以便与您的 USB 接口匹配。
打开 Arduino Wi-Fi 库中附带的 WiFiScan 示例草图。将此草图上传到 ESP32 相机板,并打开串口监视器。您应该能看到附近可用的 Wi-Fi 网络列出,如图 24-4 所示。

图 24-4:运行 WiFiScan 示例草图的输出示例
在图 24-4 中显示的接收信号强度指示器(RSSI)值是一个测量 ESP32 相机板接收到您 Wi-Fi 接入点信号强度的指标。数值越接近 0,信号越好。CH 值是您 Wi-Fi 接入点的 Wi-Fi 频道;大多数接入点有 16 个频道。最后,加密类型显示的是所列 Wi-Fi 接入点使用的加密协议类型。如果 WiFiScan 示例草图工作正常,那么您的 ESP32 相机板已准备好使用。
如果您的相机在送达时没有预先连接到板上,您现在需要进行连接。请小心慢慢地操作,撬开黑色塑料铰链条带,如图 24-5 所示。使用塑料镊子可能会比用手指更有帮助。

图 24-5:打开 ESP32 相机板上的相机电缆连接器
将相机的接口电缆插入 ESP32 相机板上的连接器。在此过程中,相机镜头应朝上,电缆应滑入几毫米。不要用力过大;如果遇到阻力,电缆已经插入足够深,如图 24-6 所示。

图 24-6:将相机电缆连接器插入 ESP32 相机板
最后,将黑色塑料铰链条带沿连接器方向推下,直到听到“咔嚓”一声,表示已经锁定到位,这样也能将相机连接器电缆固定到 ESP32 相机板上,如图 24-7 所示。

图 24-7:相机安装到 ESP32 相机板上
如果镜头上有保护塑料膜,请小心地拉起保护膜一侧的标签将其取下。现在你可以通过它在 Wi-Fi 网络中进行视频流测试了。
项目#69:从基本相机服务器流式传输视频
在这个项目中,你将通过 Wi-Fi 局域网远程控制 ESP32 相机模块,使用网页浏览器界面,尝试调整相机界面上的各种设置来调整相机的图像或视频流。
对于硬件,你只需要按照前一节的描述准备 ESP32 相机模块和 USB 接口。对于草图,你需要像往常一样从书籍网站下载项目文件,https://

图 24-8:Arduino IDE 中显示的所有项目文件标签
额外的文件(app_httpd.cpp、camera_index.h和camera_pins.h)包含用于定义 ESP32 相机板 GPIO 引脚的数据,并包含帮助显示相机网页的数据。该项目操作的是一个预定义的界面,因此你无法修改太多内容。你只需要关注.ino草图文件。
在项目#69 的草图中,按照之前在第十九章项目#55 中解释的那样,添加你的 Wi-Fi 网络和名称到 SSID 和密码字段。当你将 ESP32 相机连接到计算机时,上传草图并打开 Arduino IDE 中的串口监视器。如果你的 USB 接口通过外部电缆连接,请记得从 IO0 移除与 GND 的连接,然后按下RESET按钮重启相机模块。
你应该能在串口监视器中看到 ESP32 相机正在使用的 IP 地址,如图 24-9 所示。

图 24-9:串口监视器输出示例,显示相机的 IP 地址
打开连接到同一 Wi-Fi 网络的 PC 或移动设备上的网页浏览器,访问串口监视器中显示的 IP 地址。ESP32 相机应该会提供一个包含各种控件和设置的网页。点击页面底部的开始流式传输按钮。来自相机的实时视频流应该会出现在屏幕上,如图 24-10 所示。(图中的视频是我在厨房桌子上拍摄的。)

图 24-10:来自相机流媒体页面的示例输出
花些时间尝试调整设置,调整亮度、对比度等。进行视频流传输时,图像质量越高,网络上传输的数据量越大。如果你发现视频更新速度或帧率太慢或卡顿,可以通过下拉菜单更改分辨率。你也可以切换为黑白流媒体,因为这比彩色图像传输需要的数据显示量要少得多。如果你对操作时的帧率感到好奇,可以在串口监视器中查看相机流媒体时的帧率,如图 24-11 所示。

图 24-11:来自 ESP32 相机的帧率示例输出
如果你对视频或图像的质量或亮度不满意,可能需要更好的外部照明,或者重新调整相机的位置,以便让更多的环境光照射到镜头上。
让我们来看一下.ino 草图文件:
// Project #69 - Camera web server
#include "esp_camera.h"
#include <WiFi.h>
#define CAMERA_MODEL_AI_THINKER
#include "camera_pins.h"
const char* ssid = "`SSID`";
const char* password = "`password`";
void startCameraServer();
void setup()
{
❶ Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
❷ camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
❸ config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
❹ esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
❺ Serial.printf("Camera init failed with error 0x%x", err);
return;
}
❻ s->set_framesize(s, FRAMESIZE_QVGA); // Set default frame size
❼ WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.println(" ");
Serial.println("WiFi connected");
startCameraServer();
❽ Serial.print("Camera Ready! Use 'http://");
Serial.print(WiFi.localIP());
Serial.println(" ' to connect");
}
void loop() {}
草图包含了所需的库,然后定义了 ESP32 相机类型,允许将正确的引脚标签与 Arduino IDE 使用的引脚进行关联。请确保在适当的字段中插入您的 Wi-Fi 网络名称和密码。
在 void setup() 中,代码会启用串口监视器以进行调试 ❶。它为相机接口引脚进行 Arduino GPIO 关联 ❷,然后定义默认的图像大小和质量 ❸。如果初始化相机时出现问题,会进行相应的提示 ❹。以下的 printf() 函数仅在使用 ESP32 兼容的 Arduino 时可用,而不是 Arduino 板本身。它允许在文本字符串中显示变量的值。例如,❺ 处的代码将显示存储在变量 err 中的错误代码,该错误代码以十六进制格式显示。
默认的图像帧大小设置为四分之一 VGA(320 × 240 像素,或 QVGA),以提高速度 ❻。由于 QVGA 分辨率较小,每帧所需的数据较少,这意味着帧率可以更高,从而实现更流畅的视频。
在建立 Wi-Fi 连接后 ❼,草图会在串口监视器 ❽ 中显示 ESP32 相机的网页 IP 地址。尽情尝试相机吧。有很多方法可以使用这款廉价硬件,例如监控房产入口、从另一个房间观察孩子,或者查看鸟巢。现在是学习如何使用外部天线最大化 Wi-Fi 范围的好时机。
外部 Wi-Fi 天线
本章推荐的 ESP32 摄像头板支持使用外部 Wi-Fi 天线,这可以使 Wi-Fi 接入点的距离更远。在购买天线时,请确保选择一个包含从天线到 ESP32 摄像头板电缆的天线。板上的插座称为 uFL 或 mini ipex 连接器。外部天线和电缆通常作为一套出售,但通常不包括在摄像头中,这意味着你需要单独购买它们。
图 24-12 展示了一个外部天线和电缆的示例,PMD Way 部件 70202023。

图 24-12:外部天线和电缆
要检查模块天线配置,请将模块翻转过来,以便看到天线插座,PCB 上天线下方的微小圆形铜环,如图 24-13 所示。

图 24-13:ESP32 摄像头板天线插座
插座的左侧应有三个微小的 PCB 焊盘——上、下和左。两个焊盘应通过一个表面贴装电阻桥接。如果下部和左部焊盘按这种方式桥接,如图 24-14 所示,那么你就可以准备连接一个外部天线到插座,如图 24-13 所示。

图 24-14:ESP32 摄像头板桥接电阻
然而,如果你的板子上电阻跨接在顶部和左侧焊盘之间,你需要将其拆除并重新焊接到正确的位置。为此,你需要一把细尖焊枪或适用于表面贴装元件回流工作的热风枪,以及少量的焊锡。如果这对你来说是个问题,最好在订购 ESP32 摄像头板之前与板子的供应商确认。
使用天线可以大大提高 ESP32 摄像头板的无线电范围。一旦连接上外部天线和电缆,你可以轻松地通过运行本章前面使用过的 WiFiScan 演示示例来展示无线电范围的改善。例如,图 24-15 展示了未连接天线和连接天线后的 WiFiScan 结果。

图 24-15:带和不带外部天线的 WiFiScan 结果
未连接天线时,输出显示 RSSI 值为 −92;但是,连接天线后,RSSI 值为 −57。
现在你已经知道如何测试 ESP32 相机模块的 Wi-Fi 并设置视频流,我将向你展示如何按命令拍照并将其保存到 microSD 卡,同时在每张图像的文件名中存储时间和日期,便于参考。
项目 #70:拍照并保存到存储卡
本项目演示了如何控制板载相机按你的命令拍照。你可以在自己的草图中使用本项目的代码,制作延时摄影相机、由传感器或开关触发的相机,或者只是一个简单的便携数字相机。
对于硬件,你需要 ESP32 相机模块(带有匹配的 USB 接口)和一张 microSD 存储卡,用于存储拍摄的图像。如果你还没有这么做,格式化 microSD 卡以便与相机配合使用,将文件系统类型设置为 FAT32。任何在 PC、Mac 或 Linux 机器上的正常格式化程序都提供设置文件系统类型的选项。
将格式化后的卡插入 ESP32 相机,然后将 ESP32 相机连接到计算机并上传项目 #70 的草图。在 Arduino IDE 中打开串口监视器。如果你的 USB 接口是通过外部电缆连接的,请记得移除 IO0 到 GND 的连线,然后按RESET按钮重启相机模块。
串口监视器应显示 ESP32 相机拍摄的图像文件名,如图 24-16 所示。

图 24-16:图像文件名的串口监视器输出
要停止相机并查看结果,断开 USB 电缆与计算机的连接,并将 microSD 卡插入计算机。打开计算机的文件管理器,找到代表 microSD 卡的驱动器,你应该能找到 ESP32 相机拍摄的图像。例如,图 24-17 展示了我办公室窗外的照片。

图 24-17:项目 #70 的示例输出
让我们看看这个是如何工作的:
// Project #70 - Save images to microSD card
#include "esp_camera.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "driver/rtc_io.h"
#include "FS.h"
#include "SD_MMC.h"
// Pin definitions for CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM −1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
❶ int imageCounter = 0;
void configESPCamera()
{
// Object for camera configuration parameters:
camera_config_t config;
❷ config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
// Set camera quality parameters:
sensor_t *s = esp_camera_sensor_get();
❸ // Brightness (−2 to 2)
s->set_brightness(s, 0);
// Contrast (−2 to 2)
s->set_contrast(s, 0);
// Saturation (−2 to 2)
s->set_saturation(s, 0);
// Special effects (0 - none, 1 - negative, 2 - grayscale,
// 3 - red tint, 4 - green tint, 5 - blue tint, 6 - sepia)
s->set_special_effect(s, 0);
// Auto white balance (0 = disable , 1 = enable)
s->set_whitebal(s, 1);
// Auto white balance gain (0 = disable , 1 = enable)
s->set_awb_gain(s, 1);
// WB MODES (0 - automatic, 1 - sunny, 2 - cloudy,
// 3 - office, 4 - home)
s->set_wb_mode(s, 0);
// EXPOSURE CONTROLS (0 = disable , 1 = enable)
s->set_exposure_ctrl(s, 1);
// AEC2 (0 = disable , 1 = enable)
s->set_aec2(s, 0);
// AE LEVELS (−2 to 2)
s->set_ae_level(s, 0);
// AEC VALUES (0 to 1200)
s->set_aec_value(s, 300);
// GAIN CONTROLS (0 = disable , 1 = enable)
s->set_gain_ctrl(s, 1);
// AGC GAIN (0 to 30)
s->set_agc_gain(s, 0);
// GAIN CEILING (0 to 6)
s->set_gainceiling(s, (gainceiling_t)0);
// BPC (0 = disable , 1 = enable)
s->set_bpc(s, 0);
// WPC (0 = disable , 1 = enable)
s->set_wpc(s, 1);
// RAW GMA (0 = disable , 1 = enable)
s->set_raw_gma(s, 1);
// LENC (0 = disable , 1 = enable)
s->set_lenc(s, 1);
// HORIZ MIRROR (0 = disable , 1 = enable)
s->set_hmirror(s, 0);
// VERT FLIP (0 = disable , 1 = enable)
s->set_vflip(s, 0);
// DCW (0 = disable , 1 = enable)
s->set_dcw(s, 1);
// COLOR BAR PATTERN (0 = disable , 1 = enable)
s->set_colorbar(s, 0);
}
void initMicroSDCard()
{
if (!SD_MMC.begin())
{
Serial.println("microSD card failed");
return;
}
uint8_t cardType = SD_MMC.cardType();
if (cardType == CARD_NONE)
{
Serial.println("microSD card not found");
return;
}
}
void takeNewPhoto(String path)
{
// Set up frame buffer:
camera_fb_t *fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Camera capture failed");
return;
}
// Save picture to microSD card:
fs::FS &fs = SD_MMC;
File file = fs.open(path.c_str(), FILE_WRITE);
if (!file)
{
Serial.println("Couldn't open file in write mode!");
} else
{
file.write(fb->buf, fb->len);
Serial.printf("File saved as: %s\n", path.c_str());
}
file.close();
esp_camera_fb_return(fb);
}
void captureImage()
{
imageCounter += 1;
// Build image path and filename:
String path = "/ESP32CAM_" + String(imageCounter) + ".jpg";
Serial.printf("Picture file name: %s\n", path.c_str());
takeNewPhoto(path);
}
void setup()
{
Serial.begin(115200);
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
configESPCamera();
initMicroSDCard();
}
void loop()
{
captureImage();
delay(5000);
}
该草图包括所有必需的库,然后给出 AI-Thinker ESP32 相机模块的引脚定义。每次相机拍照时,程序会使用imageCounter整型变量❶跟踪照片的数量,图像的文件名将包含此数字(如图 24-17 所示)。
该草图还定义了用于连接相机❷的引脚编号,保存了图像类型,并根据config.pixel_format定义了图像的大小和质量。此设置为.jpg文件类型,图像大小为 UXGA(1,600 × 1,200 像素),最大质量为 10(数字越大,图像质量越低)。其他相机设置❸包括白平衡控制、亮度、对比度等。草图中每个参数上方的注释描述了它们的值范围,因此你可以调整这些值,以便为你的相机安装获取理想的效果。
initMicroSDCard()函数包含了 microSD 卡电路的初始化,而takeNewPhoto()函数使相机能够拍摄并保存图像;其参数是用于存储图像的文件名。草图的核心是captureImage(),程序调用该函数来捕获图像。此函数增加imageCounter变量,并将其插入到以/ESP32CAM_开头(你可以根据需要更改)的字符串中,最后添加.jpg文件扩展名。请注意,如果你在从存储卡中获取图像之前重置了 ESP32 板,这些图像将被覆盖。
在void setup()中,草图初始化了串行监视器,接着是WRITE_PERI_REG()函数,该函数关闭了相机的欠压检测。此函数允许相机在电源电压暂时下降时继续工作(尽管质量较低)。草图初始化了相机和 microSD 卡电路。最后,它通过调用captureImage()捕获一张新图像,然后等待五秒钟。
你可以围绕这个项目的草图构建自己的数字摄影项目,它包含了控制相机所需的一切——只需根据你的喜好调整相机设置,并在需要拍照时调用captureImage()函数。捕获图像并将其保存到 microSD 卡的过程大约需要两秒钟,因此你不能强迫它更快地工作。
现在你已经可以拍摄自己的照片了,是时候了解 ESP32 相机板上的引脚分配,这样你就可以与外部设备交互,扩展你的项目了。
ESP32 相机引脚分配
与 ESP32 开发板一样,ESP32 摄像头板也具有一系列可以用于非摄像头操作的 GPIO 引脚。了解这些引脚图后,你可以根据需要在自己的项目中使用它们。表 24-2 显示了引脚标签及其对应的 Arduino 用法。
表 24-2: ESP32 摄像头引脚图
| 引脚标签 | 引脚用途 | 备注 |
|---|---|---|
| 5V | 5 V 电源输入 | 无 |
| GND | 地 | 无 |
| IO12 | GPIO 引脚 | 无 |
| IO13 | GPIO 引脚 | 无 |
| IO15 | GPIO 引脚 | 无 |
| IO14 | GPIO 引脚 | 无 |
| IO2 | GPIO 引脚 | 无 |
| IO4 | GPIO 引脚 | 控制板载明亮 LED |
| 3V3 | 3.3 V 电源输入 | 建议使用 5 V |
| IO16 | GPIO 引脚 | 无 |
| IO0 | 代码上传/GPIO 引脚 | 连接到 GND 以上传代码 |
| GND | 地 | 无 |
| VCC | 电源输出 | 输出,不是输入! |
| UOR | 串口 RX(接收) | 无 |
| UOT | 串行 TX(传输) | n/a |
| GND | GND | n/a |
如果你在使用相机,最好使用 5 V 为 ESP32 相机供电;否则,3.3 V 也可以。使用较低电压可能会导致图像轻微退化。当你在构建包含比板子更多硬件的项目时,建议使用至少 500 mA 电流的 5 V 直流电源。
明亮的白色“闪光灯”LED 连接到 GPIO 引脚 IO4,可以像任何数字输出引脚一样打开和关闭。它还连接到 microSD 卡插槽电路,当卡被访问时会点亮。这款 LED 产生大量热量,因此在控制它用于自己的项目时,不要让它持续运行超过一秒钟,而应给予它足够的时间冷却。
在相机模块的另一侧有一个小的红色 LED,它通过 GPIO 引脚 33 内部连接。它的接线方式是反向的,设置该引脚为低电平时会点亮 LED,高电平时则关闭。总体来说,GPIO 引脚可以用作输入或输出。请记住,逻辑电压是 3.3 V,而不是 5 V。
继续前进
在本章中,你学习了如何利用廉价且实用的 ESP32 相机板进行视频流传输或拍照,并将其保存到存储卡。这为六个基于 ESP32 的章节增加了最后一项技能集,后者展示了远程控制项目、记录和显示数据的各种方式。
这标志着Arduino for Arduinians的最后一章!要继续你的 Arduino 之旅,请查看结语,了解接下来的步骤。
第二十五章:后记

到此为止,阅读(并且我希望你已经制作了)书中的 70 个项目后,你应该对电子学和 Arduino 环境有了更深刻、更广泛的理解,并且掌握了创造更复杂和更有用的 Arduino 项目所需的知识和信心。
你已经学会了通过数据总线和不同形式的无线通信(包括蓝牙和 Telegram 即时通讯平台)与 Arduino 生态系统之外的各种设备进行接口。凭借新获得的技能,你可以构建通过互联网控制电源插座的设备,创建用于监控传感器数据的网页界面,为你的项目添加 MP3 质量的音频以生成用户通知,使用看门狗定时器提高项目运行的可靠性,等等。
我希望这本书能够激发你继续探索电气工程的世界。你在这条路上不会孤单。在互联网上,有一个活跃的 Arduino 用户社区,你可以在以下网站找到:
官方 Arduino 论坛 https://
hackaday.io 网站 https://
Instructables https://
Reddit https://
你也可以在社交媒体上搜索#arduino 标签,获取项目创意和灵感,或者寻找本地的黑客空间或俱乐部,无论是线上还是线下。
我很高兴通过出版商网页上的联系方式接收关于本书的反馈:https://
一如既往,不要只是坐在那里——做点什么!
附录
压缩并安装 Arduino 库

创建新的 Arduino 库后,你可以通过将库文件保存为 ZIP 文件来存储和分发它。未来获取 ZIP 文件的用户可以轻松安装该库。
本附录将教你如何在 Windows、macOS 和 Linux 上创建 ZIP 文件,这些步骤是将 Arduino 库文件整理为可安装文件的过程的一部分。之后,你将学习如何将作为 ZIP 文件收到的 Arduino 库安装到 Arduino IDE 中。
压缩文件为 ZIP 文件
本节内容将教你如何将自定义的 Arduino 库文件压缩成 ZIP 文件,这不仅方便分发,还将它们转换成适合快速安装到 Arduino IDE 中的格式。请跳转到适合你操作系统的部分。
本节基于名为blinko的示例 Arduino 库。但是,在你自己的项目中,你应当将其替换为你自己的库名称。
Windows 7 及更高版本
要在 Windows 上为 Arduino 库创建 ZIP 文件,首先将你想压缩的自定义 Arduino 库文件和示例草图(草图存放在它自己的文件夹中,所有草图都应如此)放置在一个位置。图 A-1 展示了库文件在一起的示例。

图 A-1:下载文件夹中的 Arduino 库文件
选择所有文件,右键点击选中的文件,选择压缩为 ZIP 文件,如图 A-2 所示。

图 A-2:压缩库文件
文件夹中会出现一个新文件,允许修改文件名。将库文件名更改为blinko.zip,然后按 ENTER 键,如图 A-3 所示。

图 A-3:更改库 ZIP 文件名
现在你有了一个库的 ZIP 文件,可以轻松分发给他人或自行安装。
macOS 10 或更高版本
要在 macOS 上创建 ZIP 文件,首先将你想压缩的自定义 Arduino 库文件和示例草图(草图存放在它自己的文件夹中,所有草图都应如此)放置在一个位置。图 A-4 展示了库文件在一起的示例。

图 A-4:下载文件夹中的 Arduino 库文件
选择所有文件,右键点击文件,选择压缩 4 个项目,如图 A-5 所示。

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

图 A-6: Archive.zip 文件在下载文件夹中
点击Archive.zip文件夹并将其重命名为blinko.zip,如图 A-7 所示。

图 A-7:重命名 Archive.zip 为 blinko.zip
现在你有了一个库 ZIP 文件,可以轻松地分发给他人或自己安装。
Ubuntu 20.04 LTS 及更高版本
要在 Ubuntu Linux 上创建 ZIP 文件,首先将你要压缩的自定义 Arduino 库文件与示例草图(存储在自己的文件夹中,所有草图都是如此)放置在一个位置。图 A-8 展示了库文件的一个例子。

图 A-8:Blinko Arduino 库文件
选择所有文件,右键点击文件,选择压缩,如图 A-9 所示。

图 A-9:压缩库文件
一个新窗口将出现,提示你输入归档名称。输入blinko作为文件名,并点击.zip 单选按钮,然后点击创建,如图 A-10 所示。

图 A-10:设置 ZIP 文件名
一旦压缩完成,你将在同一文件夹中找到你的新 ZIP 文件以及库文件。现在你有了一个库 ZIP 文件,可以轻松地分发给他人或自己安装。
安装你的新库
现在,你可以使用 ZIP 文件方法安装库。打开 Arduino IDE,选择 草图 ➧ 包含库
添加 .ZIP 库。文件对话框将会打开。导航到你的 ZIP 文件并选择它进行安装。
一旦库安装完成并且你重新启动了 Arduino IDE,当你选择 草图
包含库 时,你会看到你的库在列表中,如 图 A-11 所示。

图 A-11:Arduino 库,现已在 IDE 中可用
现在,你可以轻松访问你已安装的草图,例如 blinko 库,方法是选择 文件
示例
blinko,如 图 A-12 所示。

图 A-12:Arduino IDE 的库示例草图菜单,显示 blinko 示例草图


浙公网安备 33010602011771号