Arduino-C-编程指南-全-
Arduino C 编程指南(全)
原文:
zh.annas-archive.org/md5/84c5bfddb3707fe8d398d438af660eab译者:飞龙
前言
我们的未来世界充满了智能和互联的设备。DIY 社区一直着迷于这样一个事实:每个人都可以设计和构建自己的智能系统,无论是专门的还是非专门的,用于特定任务。从小型控制器在检测到有人时打开灯光,到智能沙发在我们坐下时发送电子邮件,廉价的电子产品项目变得越来越容易创建,为此,我们都要感谢那些在 2005 年左右在意大利伊夫雷亚发起 Arduino 项目的团队。
Arduino 平台是世界上使用最广泛的开源硬件之一。它在一个小型印刷电路板上提供了一个功能强大的微控制器,具有非常小的外形尺寸。Arduino 用户可以下载 Arduino 集成开发环境(IDE),并使用 C/C++语言和 Arduino 核心库编写自己的程序,该库提供了许多有用的功能和特性。
通过Arduino C 编程,用户将学习足够的 C/C++知识,能够基于 Arduino 设计自己的硬件。这是一本包含所有必需理论并辅以具体实例的全能书籍。读者还将了解一些主要交互设计和实时多媒体框架,如 Processing 和 Max 6 图形编程框架。
Arduino C 编程将教授您著名的“通过制作学习”的工作方式,这是我在所有课程中(从 Max 6 到 Processing 和 Ableton Live)努力遵循的方式。
最后,通过探讨输入/输出概念、通信和网络、声音合成以及反应式系统设计,Arduino C 编程将开启新的知识领域。读者将学习到必要的技能,通过以不同的方式看待现代世界,不仅作为用户,也作为真正的创造者,继续他们的旅程。
如需更多详细信息,您可以访问我的网站cprogrammingforarduino.com/。
本书涵盖的内容
第一章, 让我们连接事物,是您与 Arduino 和微控制器编程的第一次接触。我们将学习如何在我们的计算机上安装 Arduino 集成开发环境,以及如何布线和测试开发工具链,为后续学习做准备。
第二章, C 语言初探,涵盖了软件与硬件之间的关系。我们将介绍 C 语言,了解如何编译它,然后学习如何将我们的程序上传到 Arduino 板。我们还将学习将一个纯粹的想法转化为 Arduino 固件所需的所有步骤。
第三章, C 基础——让你变得更强大,直接进入 C 语言的学习。通过学习基础知识,我们学习如何阅读和编写 C 程序,发现数据类型、基本结构和编程块。
第四章,使用函数、数学和定时改进编程,提供了改进我们的 C 代码的第一把钥匙,特别是通过使用函数。我们学习如何产生可重用和高效的编程结构。
第五章,使用数字输入进行感应,介绍了 Arduino 的数字输入。我们将学习如何使用它们,并理解它们的输入和输出。我们还将看到 Arduino 如何使用电和脉冲与一切进行通信。
第六章,通过模拟输入感知世界——用模拟输入感受,通过不同的具体例子描述了 Arduino 的模拟输入,并将它们与数字引脚进行比较。本章介绍了 Max 6 框架,作为 Arduino 的理想伴侣之一。
第七章,通过串行通信进行交流,介绍了通信概念,特别是通过教授关于串行通信的内容。我们将学习如何使用串行通信控制台作为一个强大的调试工具。
第八章,设计视觉输出反馈,讨论了 Arduino 的输出,以及我们如何使用它们通过使用 LED 及其系统来设计视觉反馈系统。它介绍了强大的 PWM 概念,并也讨论了 LCD 显示屏。
第九章,使事物移动和创造声音,展示了我们如何使用 Arduino 的输出进行与运动相关的项目。我们讨论了电机和运动,以及空气振动和声音设计。我们描述了一些关于数字声音、MIDI 和 OSC 协议的基础知识,并使用一个功能强大的 PCM 库,提供从 Arduino 本身读取数字编码声音文件的功能。
第十章,一些高级技术,介绍了许多高级概念,从 EEPROM 单元上的数据存储,到多个 Arduino 板之间的通信,以及 GPS 模块的使用。我们还将学习如何使用 Arduino 板与电池一起使用,玩转 LCD 显示屏,以及使用 VGA 屏蔽卡将微控制器连接到典型的计算机屏幕。
第十一章,网络,介绍了我们需要理解的网络概念,以便在以太网、有线或无线网络上使用我们的 Arduino。我们还将使用一个强大的库,它为我们提供了一种通过在 Arduino 上按按钮直接发送推文的方法,而不需要使用任何计算机。
第十二章, 玩转 Max 6 框架,介绍了一些我们可以用于 Max 6 图形编程框架的技巧和技术。我们将完全描述 Serial 对象的使用以及如何解析和选择从 Arduino 传到计算机的数据。我们将设计一个小型声音电平计,使用真实的 LED 和 Max 6,并以设计由我们自己的手和距离传感器控制的声音音调转换效果结束。
第十三章, 提高 C 编程技能和创建库,是本书最先进的章节。它描述了一些高级的 C 概念,通过一些精彩且有趣的现实世界示例,我们可以使用这些概念使我们的代码可重用、更高效和优化。
附录为我们提供了 C 编程语言中数据类型的详细信息,C 和 C++中的运算符优先级,重要的数学函数,用于计算优化的泰勒级数,ASCII 表,安装库的说明,以及组件分销商的列表。
附录可以从www.packtpub.com/sites/default/files/downloads/7584OS_Appendix.pdf下载。
阅读本书所需的条件
如果你想利用本书中的每个示例,以下软件是必需的:
-
Arduino 环境(免费,
arduino.cc/en/main/software)。这是所有与 Arduino 编程相关的操作所必需的。 -
Fritzing(免费,
fritzing.org/download)。这是一个开源环境,帮助我们设计电路。 -
Processing(免费,
processing.org/download)。这是一个使用 Java 进行快速原型设计的开源框架。一些示例将其用作 Arduino 板的通信伙伴。 -
Max 6 框架(30 天试用期,
cycling74.com/downloads)。这个框架是一个巨大的环境,本书中也使用了它。
本书还使用了其他一些库。每次需要它们时,示例描述都会解释从哪里下载它们以及如何在我们的计算机上安装它们。
本书面向的对象
本书面向那些想要掌握使用 Arduino 板进行 DIY 电子硬件制作的人。它教授了我们使用 C 编程语言编程固件以及如何将 Arduino 连接到物理世界所需的一切,深入浅出。从互动设计艺术学校的学生到纯粹爱好者,从互动装置设计师到想要通过加入一个庞大且不断增长的物理计算程序员社区来学习电子的人,本书将帮助所有对学习新方法设计智能对象、会说话的对象、高效设备以及自主或连接的反应装置感兴趣的人。
本书开启了通过实践学习的全新视野,这将改变读者的生活。
惯例
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词将以如下方式显示:“我们可以通过使用include指令来包含其他上下文。”
代码块将以如下方式设置:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
[default]
exten => s,1,Dial(Zap/1|30)
exten => s,2,Voicemail(u100)
exten => s,102,Voicemail(b100)
exten => i,1,Voicemail(s0)
任何命令行输入或输出将以如下方式编写:
# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
/etc/asterisk/cdr_mysql.conf
新术语和重要词汇将以粗体显示。你在屏幕上看到的,例如在菜单或对话框中的文字,将以如下方式显示:“点击下一个按钮将您带到下一屏幕。”
注意
警告或重要注意事项将以如下框中显示。
小贴士
技巧和窍门将以如下方式显示。
读者反馈
我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大价值的标题非常重要。
要发送给我们一般性的反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在您的邮件主题中提及书籍标题。如果您在某个主题上有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在www.packtpub.com的账户下载所有已购买的 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问www.packtpub.com/submit-errata来报告它们,选择您的书籍,点击错误清单提交表单链接,并输入您的错误清单详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的错误清单部分。任何现有的错误清单都可以通过从www.packtpub.com/support中选择您的标题来查看。
侵权
在互联网上,版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢你在保护我们作者和提供有价值内容的能力方面的帮助。
问题
如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。
第一章。让我们连接东西
Arduino 的一切都是关于连接东西。在我们对微控制器的一般知识以及特别是庞大而惊人的 Arduino 大家族有了更多了解之后,我们将在这几分钟内做到这一点。本章将教会你如何完全准备好用你的新硬件朋友进行编码、布线和测试。是的,这很快就会发生,非常快;现在让我们深入探讨吧!
什么是微控制器?
微控制器是一个集成电路(IC),包含典型计算机的所有主要部分,如下所述:
-
处理器
-
存储器
-
外设
-
输入和输出
处理器是大脑,是所有决策被做出并可以进行计算的部分。
存储器通常是核心内部程序和用户元素运行的空间(通常称为只读存储器(ROM)和随机存取存储器(RAM))。
我将全局板上的自包含外设定义为外设;这些是具有主要目的的非常不同类型的集成电路:支持处理器并扩展其功能。
输入和输出是世界(围绕微控制器)与微控制器本身之间通信的方式。
第一款单片处理器是在 1971 年由英特尔公司建造并提出的,名为Intel 4004。它是一个 4 位的中央处理单元(CPU)。
自从 70 年代以来,事物已经发生了很大的变化,我们周围有很多处理器。看看四周,你会看到你的手机、你的电脑和你的屏幕。处理器或微处理器几乎驱动着一切。
与微处理器相比,微控制器提供了一种减少功耗、尺寸和成本的方法。确实,即使微处理器的速度比微控制器中嵌入的处理器快,它们也需要大量的外围设备才能工作。微控制器提供的高度集成使其成为汽车引擎控制器、电视遥控器、包括您漂亮的打印机在内的桌面设备、家用电器、儿童游戏、移动电话等嵌入式系统的朋友。我可以继续……
我无法在这本书中介绍许多微控制器系列,不提PICs (en.wikipedia.org/wiki/PIC_microcontroller) 和 Parallax SX 微控制器系列。我还想提及一个特定的音乐硬件开发开源项目:MIDIbox(基于 PIC,然后基于 STM32,查看 www.ucapps.de)。这是一个非常强大且稳健的框架,非常可调整。Protodeck 控制器 (julienbayle.net/protodeck) 基于 MIDIbox。
现在你已经明白你手中有一台完整的电脑,让我们具体描述 Arduino 板!
介绍 Arduino 大家族
Arduino 是一个开源(en.wikipedia.org/wiki/Open_source)的单板式微控制器。它是一个非常流行的平台,源自Wiring平台(www.wiring.org.co/),最初设计用于推广在交互式设计大学生的项目中使用电子技术。

我手中的 Arduino MEGA
它基于 Atmel AVR 处理器(www.atmel.com/products/microcontrollers/avr/default.aspx),并且在一块自给自足的硬件上提供了许多输入和输出。该项目的官方网站是www.arduino.cc。
该项目始于 2005 年的意大利,由创始人 Massimo Banzi 和 David Cuartielles 发起。如今,它是开源概念在硬件世界中最美丽的例子之一,经常只在软件世界中使用。
我们谈论 Arduino 家族,因为今天我们可以数出大约 15 款基于 Arduino 的板子,这是一个有趣的元术语,用来定义所有使用 Atmel AVR 处理器制作的不同类型的板子设计。这些板子之间的主要区别是:
-
处理器类型
-
输入和输出的数量
-
外形尺寸
一些 Arduino 板在计算速度上更强大,一些有更多的内存,一些有大量的输入/输出(检查巨大的 Arduino Mega),一些旨在集成到更复杂的项目中,具有非常小的外形尺寸和很少的输入/输出……就像我经常对我的学生说,每个人都能在 Arduino 家族中找到他的朋友。还有一些板子包括像以太网连接器或甚至蓝牙模块这样的外围设备,包括天线。
这个家族背后的魔法事实是我们可以在我们的计算机上使用相同的集成开发环境(IDE)来使用任何这些板子(en.wikipedia.org/wiki/Integrated_development_environment)。一些设置需要正确配置,但这是我们将会使用的相同软件和语言:

一些值得注意的 Arduino 家族成员:Uno R3、LilyPad、Arduino Ethernet、Arduino Mega、Arduino Nano、Arduino Pro 和原型板
你可以在arduino.cc/en/Main/Hardware找到关于此的非常棒但并不详尽的参考页面。
我特别想让你检查以下型号:
-
Arduino Uno是基本型号,具有可更换的芯片组
-
Arduino Mega,2560 提供了大量的输入和输出
-
Arduino LilyPad,可以像衣服一样穿戴
-
Arduino Nano,非常小巧
在这本书中,我会使用 Arduino Mega 和 Arduino Uno;但不要害怕,当你掌握了 Arduino 编程,你将能够使用它们中的任何一个!
关于硬件原型
我们今天可以很容易地使用大量的开源框架来编程和构建软件,你可以在网上找到很多有帮助的社区。我想到的是Processing(基于 Java,请查看processing.org),和openFrameworks(基于 C++,请查看www.openframeworks.cc),但还有很多其他的使用非常不同的范例,比如图形编程语言,如Pure Data (puredata.info),Max 6 (cycling74.com/products/max/),或vvvv (vvvv.org)。
因为作为制造者,我们完全沉浸在 DIY 实践中,我们都想并且需要构建和设计我们自己的工具,这通常意味着硬件和电子工具。我们希望扩展我们的计算机,添加传感器、闪烁的灯光,甚至创建独立的设备。
即使是测试非常基本的事情,比如闪烁一个发光二极管(LED),也涉及到从供电到芯片组低级编程,从电阻值计算到电压驱动石英钟设置等多个元素。所有这些步骤都让学生们头疼,甚至那些有动力的学生也可能因为第一次测试而放弃。
Arduino 的出现改变了整个领域的格局,它提出了一个价格低廉且功能齐全的解决方案(我们不得不为 Arduino Uno R3 支付 30 美元),一个跨平台的工具链,在 Windows、OS X 和 Linux 上运行,一个非常容易使用的高级 C 语言和库,它也可以调整低级位,以及一个完全可扩展的开源框架。
事实上,有了这个包含所有功能的可爱的小板,一根 USB 线,和你的电脑,你可以学习电子学,使用 C 语言编程嵌入式硬件,并使 LED 闪烁。
由于整个框架提供的软件和硬件之间的高度集成,硬件原型变得(几乎)和软件原型一样容易。
在这里要理解的最重要的事情之一是原型周期。

一个简单的硬件原型步骤列表
从我们的想法到我们的最终渲染,我们通常必须遵循这些步骤。
如果我们想让那个 LED 闪烁,我们必须定义几个闪烁特性。这将有助于精确地定义项目,这是成功的关键。
然后,我们需要用我们的 Arduino 板和 LED 绘制一个原理图;这将解决“它们是如何连接在一起”的问题。
使用 C 语言进行固件编程可以直接在我们绘制电路图后开始,因为,正如我们稍后将会看到的,它与硬件直接相关。这是 Arduino 开发的强大功能之一。你还记得吗?板的设计只是为了让我们思考我们的项目,而不是用非常低级的抽象学习片段来混淆我们。
上传步骤非常重要。它可以为我们提供大量信息,尤其是在进一步故障排除的情况下。一旦将板正确连接到我们的计算机,我们将了解到这一步骤不需要超过几个点击。
然后,将进行子周期测试和修复。我们将通过制作、测试和失败来学习。这是过程的一个重要部分,它将教会你很多。我必须承认一个重要的事情:在我第一次开始我的bonome项目(julienbayle.net/bonome),一个 RGB monome 克隆设备时,我花了两个小时修复一个反向焊接的 LED 矩阵。现在,我非常了解它们,因为我有一天失败了。
最后一步是最酷的。我之所以提到它,是因为我们必须牢记最终目标,那就是最终让我们感到快乐的那个目标;这是成功的一个秘密!
理解 Arduino 软件架构
为了理解如何让我们的 Arduino 板正好按照我们的意愿工作,我们必须了解全局软件架构以及我们将很快使用的工具链。
拿起你的 Arduino 板。你会看到一个矩形形状的 IC,上面写着 ATMEL;这就是处理器。
这个处理器是包含我们将要编写的整个程序并使事物发生的地方。
当我们购买 Arduino(请参阅附录 G,组件分销商列表以及此链接:arduino.cc/en/Main/Buy)时,处理器,也称为芯片组,已经预先烧录。它已经被细心的人编程,以便让我们的生活更轻松。芯片组中已经包含的程序称为引导加载程序(en.wikipedia.org/wiki/Booting)。基本上,它负责处理当您给它供电时的处理器生命的第一个时刻。但它的主要作用是加载我们的固件(en.wikipedia.org/wiki/Firmware),我的意思是,我们宝贵的编译程序。
让我们看看一个小图解,以便更好地理解:

我喜欢通过以下方式来定义它:引导加载程序是硬件的软件,而固件是用户的软件。确实,它也有一些重要性,因为芯片组中的内存空间在写操作上并不相等(我们将在本书的后续章节中讨论特定的硬件)。使用编程器,我们无法覆盖引导加载程序(在阅读的这一点上更安全),但只能覆盖固件。即使对于高级用途,这也将绰绰有余,正如你将在整本书中看到的那样。
并非所有 Arduino 板上的引导加载程序都是等效的。实际上,它们被设计得非常具体,针对硬件部分,这为我们提供了更多的硬件抽象;我们可以专注于更高层次的设计,因为引导加载程序为我们提供了诸如通过 USB 和串行监控上传固件等服务。
现在我们下载一些必需的软件:
-
FTDI USB 驱动程序:
www.ftdichip.com/Drivers/VCP.htm -
Arduino IDE:
arduino.cc/en/Main/Software -
Processing:
processing.org/download/
Processing 在本书中被使用,但不是编程和使用 Arduino 板所必需的。
小贴士
Arduino 的工具链是什么?
通常,我们将 Arduino 的工具链称为一组软件工具,用于处理从我们在计算机上的 Arduino IDE 中输入的 C 代码到上传到板上的固件的所有步骤。确实,你输入的 C 代码在编译步骤之前必须由 avr-gcc 和 avr-g++编译器准备。一旦结果的目标文件通过工具链中的其他程序链接,通常只有一个文件,你就完成了。这可以稍后上传到板子上。还有其他使用 Arduino 板的方法,我们将在本书的最后一章中介绍。
安装 Arduino 开发环境(IDE)
让我们在上一部分下载的arduino.cc/en/Main/Software中找到压缩文件,并在我们的计算机上解压缩它。
无论平台如何,IDE 都同样工作,即使我将描述三个不同平台的一些特定部分,我也只会描述 IDE 的使用,并展示从 OS X 的截图。
安装 IDE
由于它运行在Java 虚拟机上,因此没有典型的 IDE 安装。这意味着你只需要下载它,将其解压缩到系统中的某个位置,然后启动它,JAVA 就会执行程序。你可以只使用CLI(命令行界面,著名的 g33ks 窗口,你可以在其中直接向系统输入命令)来构建你的二进制文件,而不是图形界面,但在这个阶段,我不推荐这样做。
通常,Windows 和 OS X 都预装了 Java。如果没有,请从java.com网站页面www.java.com/en/download/安装它。
在 Linux 中,这个过程实际上取决于您使用的发行版,因此我建议您查看页面www.arduino.cc/playground/Learning/Linux,如果您想从源代码检查和安装所有环境和依赖项,您也可以查看页面www.arduino.cc/playground/Linux/All。
如何启动环境?
在 Windows 中,请点击解压缩文件夹中包含的.exe文件。在 OS X 中,请点击带有漂亮 Arduino 标志的全局自包含包。在 Linux 中,您必须从 GUI 或通过在 CLI 中输入来启动 Arduino 脚本。
您必须知道,使用 IDE,您可以完成本书中我们将要做的所有事情。
IDE 长什么样?
IDE 提供了一个图形界面,您可以在其中编写代码、调试它、编译它,并上传它,基本上。

在 Arduino IDE 中打开的著名 Blink 代码示例
从左到右有六个图标,我们必须非常熟悉,因为我们每次都会使用它们:
-
验证(检查符号):这提供了代码错误检查
-
上传(右侧箭头):这编译并将我们的代码上传到 Arduino 板
-
新建(小空白页):这创建了一个新的空白草图
-
打开(向上箭头):这打开了一个列表,列出了我们草图簿中所有已存在的草图
-
保存(向下箭头):这将在我们的草图簿中保存我们的草图
-
串行监视器(小放大镜):这提供了串行监控功能
顶部栏中的每个菜单项都提供了我们将逐步在本书中发现更多选项。
然而,工具菜单值得更仔细的关注:
-
自动格式化: 这提供了带有正确和标准缩进的代码格式化
-
存档草图: 这将压缩整个当前草图及其所有文件
-
板: 这提供了所有支持板的列表
-
串行端口: 这提供了系统上所有串行设备的列表
-
编程器: 这提供了所有支持并用于 AVR 芯片组完全重新编程的编程设备的列表
-
烧录引导加载程序: 当您想在您的板上覆盖(甚至写入)新的引导加载程序时,请选择此选项。
![IDE 长什么样?]()
工具菜单
预设对话框也是我们现在必须了解的部分。像往常一样,预设对话框是一个我们实际上并不需要经常去的地方,但只是为了更改 IDE 的全局参数。您可以在该对话框中选择草图簿位置和编辑器语言。您还可以更改一些设置,例如在启动时自动检查 IDE 更新或编辑器字体大小。
草图簿的概念会使我们的生活更简单。实际上,草图簿是一个文件夹,基本上,你所有的草图都会放在里面。从我的个人观点来看,这样使用它非常宝贵,因为它真的为你组织了东西,你可以更容易地检索你的代码片段。跟我来吧;你以后会感谢我的。
当我们从零开始创建草图时,我们基本上是输入代码,验证它,上传它,并保存它。通过第一次保存,IDE 会创建一个文件夹,它将把所有与当前草图相关的文件都放在里面。点击这个文件夹内的草图文件,Arduino IDE 将打开,并且相关的代码将在窗口的编辑/输入部分显示。
我们几乎完成了!
让我们在系统上安装 Arduino USB 接口的驱动程序。
安装 Arduino 驱动程序
Arduino 板子提供了一个 USB 接口。在我们插入 USB 电缆并将板子连接到计算机之前,我们必须在计算机上安装特定的驱动程序。
在这里,Windows 和 OS X 之间有很大的区别;基本上,OS X 不需要为 Arduino Uno 或甚至 Mega 2560 安装任何特定的驱动程序。如果你使用的是较老的板子,你将不得不在 FTDI 网站上下载最新版本的驱动程序,双击包,然后按照说明操作,最后,重新启动你的计算机。
让我们描述一下它在基于 Windows 的系统上是如何工作的,我的意思是 Windows 7、Vista 和 XP。
安装 Arduino Uno R3 的驱动程序
重要的是要遵循下面提到的步骤,才能使用 Arduino Uno R3 和一些其他板子。请访问 Arduino 网站获取最新的参考资料。
-
插入你的板子,等待 Windows 开始驱动程序安装过程。几分钟后,过程失败。
-
点击开始菜单,打开控制面板。
-
在控制面板中,导航到系统和安全。然后点击系统。一旦系统窗口打开,打开设备管理器。
-
在端口(COM & LPT)下查看。检查名为Arduino UNO (COMxx)的已打开端口。
-
右键点击Arduino UNO (COMxx)端口,并选择更新驱动程序软件选项。
-
接下来,选择浏览计算机以查找驱动程序软件选项。
-
最后,导航并选择位于 Arduino 软件下载的
Drivers文件夹中的名为ArduinoUNO.inf的驱动文件,注意:不是FTDI USB Drivers子目录。 -
Windows 将从那里完成驱动程序的安装,一切都会正常。
安装 Arduino Duemilanove、Nano 或 Diecimilla 的驱动程序
当你连接板子时,Windows 应该会启动驱动程序安装过程(如果你之前没有使用过 Arduino 板子的话)。
在 Windows Vista 上,驱动程序应该会自动下载并安装。(真的,它工作得很好!)
在 Windows XP 上,将打开添加新硬件向导:
-
当被问及Windows 是否可以连接到 Windows 更新以搜索软件?时,选择不,这次不行。点击下一步。
-
选择从列表或指定位置安装(高级),然后点击下一步。
-
确保已勾选在这些位置搜索最佳驱动程序,取消勾选搜索可移动媒体,勾选将此位置包含在搜索中,并浏览到 Arduino 发行版的drivers/FTDI USB Drivers目录。(驱动程序的最新版本可以在FTDI网站上找到。)点击下一步。
-
向导将搜索驱动程序,然后告诉你找到了USB 串行转换器。点击完成。
-
新硬件向导将再次出现。按照相同的步骤进行,选择相同选项和位置进行搜索。这次,将找到USB 串行端口。
你可以通过打开Windows 设备管理器(在系统控制面板的硬件选项卡中)来检查驱动程序是否已安装。在端口部分查找USB 串行端口;那就是 Arduino 板。
现在,我们的电脑可以识别我们的 Arduino 板。让我们稍微转向物理世界,将有形和无形的世界结合起来。
电力是什么?
Arduino 全关于电子,而电子指的是电。这可能是你第一次进入这个由电线和电压组成的神奇宇宙,包括闪烁的 LED 和信号。我在这部分定义了几个非常有用的概念;你可以考虑将此页的角落向下折起,并在需要时随时回来。
在这里,我使用的是通常的水的类比。基本上,电线是管道,水是电本身。
电压
电压是电位差。基本上,这种差异是由发电机产生并维持的。这个值以伏特单位(符号为 V)表示。
与液压系统的直接类比是将电压比作管道两点之间水的压力差。压力越高,水流动得越快,当然是在管道直径恒定的情况下。
在整本书中,我们将处理低电压,这意味着不超过 5 V。很快,我们将使用 12 V 为电机供电,我会每次都明确指出。
当你打开闭合电路的发电机时,它会产生并保持这种电位差。电压是一个差异,必须在电路上的两点之间进行测量。我们使用电压表来测量电压。
电流和功率
电流可以与液压体积流量率相比较,这是在时间间隔内流动水的体积量。
电流值以安培(符号为 A)表示。电流越高,移动的电量就越多。
流量不需要测量两点之间的压力差作为差异;我们只需要电路的一个点,就可以使用名为安培计的设备进行测量。
在我们所有的应用中,我们将处理直流(DC),这与交流(AC)不同。
功率是一个特定的概念,用瓦特(符号为 W)表示。
以下是电压、电流和功率之间的数学关系:
P = V x I
其中,P 是瓦特的功率,V 是伏特的电压,I 是安培的电流。
你已经感觉好多了吗?这个类比必须作为一个恰当的类比来理解,但它确实有助于理解我们稍后会做什么。
那么,电阻器、电容器等等是什么呢?
按照同样的类比,电阻器是减缓电流流动的小型组件。它们的电阻性比任何你可以使用的电线都要强;它们通常将其作为热量耗散。它们是两个无源端子组件,并且不是极化的,这意味着你可以以两个方向布线。
电阻器由其表示为欧姆(符号为 Ω)的电阻定义。
电阻器两侧测量的电压、电流和电阻之间存在直接的数学关系,称为欧姆定律:
R = V / I
其中 R 是欧姆的电阻,V 是伏特的电压,I 是安培的电流。
对于恒定的电压值,如果电阻高,电流就低,反之亦然。这一点很重要。
每个电阻器上都有一个颜色代码,显示其电阻值。
电阻器有很多种类型。有些具有恒定的电阻,有些则可以根据物理参数(例如温度或光强度)提供不同的电阻值。
电位器是一种可变电阻器。你移动滑块或旋转旋钮,电阻就会改变。我想你开始理解我的观点了…
电容器(或电容器)是另一种非常常用的组件。直接的类比是将橡胶膜放入管道中:水不能通过它,但水可以通过拉伸它移动。
它们也是无源双端子组件,但可以是极化的。通常,小电容器不是。
我们通常说电容器通过充电来储存电能。确实,当你拉伸橡胶膜时,橡胶膜本身会储存能量;试着释放拉伸的膜,它会回到最初的位置。
电容是定义每个电容器的值。它以法拉(符号为 F)表示。
我们在这里就电容计算停止,因为它涉及到高级数学,而这本书的目的不是这个。顺便说一句,记住电容越大,电容器可以储存的电能就越多。
二极管又是一个双端子无源组件,但它是极化的。它只允许电流单向通过它,并在另一方向阻止电流。我们将看到,即使在直流的情况下,它也可以在某些情况下帮助并使我们的电路更安全。
LED 是一种特殊的二极管。当电流以正确的方向通过它们时,它们会发光。这是一个很好的特性,我们将在几分钟内用它来检查我们的电路是否正确闭合。
晶体管是我在这里描述的最后一个项目,因为它稍微复杂一些,但我们不能不引用它来谈论电子学。
晶体管是半导体器件,可以根据其使用方式放大和切换电子信号和电力。它们是三端组件。这是我们周围几乎所有现代电子设备的关键活性组件。微处理器由晶体管制成,甚至可以包含超过 10 亿个晶体管。
在 Arduino 世界中,晶体管通常用于驱动高电流,如果没有烧毁 Arduino 板,这些电流无法通过 Arduino 板本身。在这种情况下,我们基本上将它们用作模拟开关。当我们需要它们关闭高电流电路以驱动电机等设备时,我们只需用来自 Arduino 的 5V 电压驱动它们的一个三端,高电流就会通过它,就像它已经闭合了电路一样。在这种情况下,它扩展了 Arduino 板的功能,使我们能够用我们的小块硬件驱动更高的电流。
连接事物和 Fritzing
通过前面的类比,我们可以很好地理解电路需要闭合才能让电流流动。
电路是由导线制成的,导线基本上是导体。导体是一种电阻接近于零的物质;它使电流容易流动。金属通常是良好的导体。我们通常使用铜线。
为了使我们的布线操作简单,我们经常使用引脚和引线。这是一种不需要每次都使用烙铁就能连接事物的不错方式!
顺便说一下,有几种不同的方法可以将不同的组件连接在一起。为了我们的原型设计目的,我们不会设计印刷电路板或甚至使用我们的烙铁;我们将使用面板!

一个总线(bus)带有蓝色和红色的总线,以及其众多的穿孔。
面板(breadboard)是快速原型设计的方式,这也是我们在这里要采取的方式。
基本上,面板由一块塑料制成,上面有许多穿孔,其中包含小块导体,允许在面板内部连接电线和组件引脚。
两个穿孔之间的距离是 2.54 毫米(等于 0.1 英寸),这是一个标准;例如,双列直插式集成电路的引脚都是这个特定的距离,因此,你甚至可以将 IC 放在面板上。
正如我们在上一张截图中所看到的,有总线(bus)和端子条(terminal strips)。
总线是中央部分的一系列五个穿孔,并按列放置,其下方的导体是连接的。我用绿色线条包围了一个总线。
终端是通常用于为电路供电的特殊总线,它们出现在蓝色和红色线路之间。通常,我们用蓝色表示地线,红色表示电压源(在某些情况下为 5 V 或 3.3 V)。一整行的终端都有其穿孔全部相连,这样在所有面包板上就可以轻松地获得电压源和地线,而无需使用大量的连接到 Arduino。我围绕了 4 个终端中的 2 个用红色和蓝色线条标出。
面包板提供了一种无需焊接的简单原型制作方法。这也意味着你可以多年使用和重复使用你的面包板!
什么是 Fritzing?
当我需要制作关于 2010 年我制作的 Protodeck 控制器(julienbayle.net/protodeck)的第一次大师班幻灯片原理图工具时,我发现了开源的Fritzing项目(fritzing.org)。
Fritzing 被定义为一个开源倡议,旨在支持设计师、艺术家、研究人员和爱好者以创造性的方式与交互式电子设备一起工作。听起来就像是为我们量身定做的,不是吗?
你可以在fritzing.org/download/找到 Fritzing 的最新版本。
基本上,使用 Fritzing,你可以设计和绘制电子电路。因为电子电路有许多表示方式,这个宝贵的工具提供了两种经典的表示方式,以及一个 PCB 设计工具。
考虑到我们将要做的第一个实际工作,你必须拿出你的面包板、Arduino,并将引线和电阻连接得与下一张截图所示完全一致:

显示我们第一个电路的面包板视图
面包板视图看起来最像我们在桌子上面前的东西。你代表所有电线,并将虚拟面包板连接到你的 Arduino,并直接插入组件。
魔法在于,当你正在面包板视图中绘制草图时,原理图会自动构建。而且它是双向的!你可以制作一个原理图,Fritzing 会在面包板视图中连接组件。当然,你可能需要将部件放置在更方便或美观的位置,但它工作得非常完美。特别是,自动布线器可以帮助你使所有电线更加线性简单。
在下一张截图,你可以看到与之前相同的电路,但以原理图视图显示:

表示电路图的原理图视图
已经有许多为 Fritzing 特别设计的组件,你甚至可以非常容易地创建自己的组件。访问此目的的页面是fritzing.org/parts/。
本地库包含本书中所有 Arduino 板、任何离散元件和 IC 所需的所有部件。确实,本书的所有原理图都是使用 Fritzing 制作的!
现在你已经知道如何在不使用烙铁的情况下接线,以及如何在将实物连接到桌面之前,在你的电脑上安静地绘制和检查事物,让我们学习一些关于电源供应的知识。
电源供应基础
我们之前学了一些关于电的知识,但如何在现实生活中为所有电路供电呢?
Arduino 板可以通过三种不同的方式供电:
-
通过电脑的 USB 线缆(提供 5 V 电压)
-
通过电池或直接的外部电源供应单元(PSU)/适配器
-
通过将稳压 5 V 连接到+5 V 引脚
USB 线缆包含四根线:两根用于数据通信,两根用于电源供应。后两根主要用于当你通过 USB 将 Arduino 连接到电脑时为 Arduino 供电。
USB 是一种特殊的通信总线,提供 5 V 电压,但不超过 500 mA(0.5 A)。这意味着在需要大量 LED、电机和其他大量电流驱动设备的特殊项目中,我们必须使用另一个电源来源。
提示
我可以使用什么适配器与我的 Arduino 一起使用?
Arduino Uno 和 Mega 可以直接通过直流适配器供电,但这个适配器必须符合一些特性:
-
输出电压应在 9 V 到 12 V 之间。
-
它应该能够驱动至少 250 mA 的电流。
-
它必须有一个 2.1 毫米的电源插头,中心为正极。
通常,如果你在考虑是否使用适配器,这意味着你需要比 USB 的 500 mA 更多的电流(实际上,问自己这个问题是否需要大约 400 mA)。
使用 USB 或 2.1 毫米电源插头加适配器是使用 Arduino 板最安全的方式,原因有很多。最主要的是这两个来源(希望)是干净的,这意味着它们提供的是稳压电压。
然而,如果你想使用其中一个来源,你必须在板上做一些更改:跳线必须移动到正确的位置:

在左侧,跳线设置为 USB 电源供电,在右侧,它设置为外部电源供电。
通常,一个空闲的 Arduino 板大约消耗 100 mA 的电流,除非在特定情况下(见第九章,使事物移动和创造声音),我们将使用 USB 供电方式。现在你必须这样做:将 USB 线缆同时插入 Arduino 和你的电脑。
启动 Arduino IDE,然后让我们进一步了解我们系统的硬件Hello World,我称之为Hello LED!
嘿,LED!
如果你的 Arduino 没有固件,LED 可能不会做任何事。如果你检查 Arduino 板上的内置 LED,那个应该会闪烁。
让我们控制一下现在插在面包板上的外部可爱 LED。
我们到底想做什么?
如果你记得正确,这是我们首先要问的第一个问题。当然,我们绕过了这个步骤,特别是关于硬件的部分,因为我不得不在你接线的时候解释事情,但让我们继续通过检查代码和上传来继续原型制作过程。
我们想让我们的 LED 闪烁。但是闪烁速度是多少?多长时间?让我们说我们想让它在每 250ms 闪烁一次,闪烁之间有 1 秒的暂停。并且我们想无限期地这样做。
如果你检查了电路图,你可以理解 LED 被放置在地线和数字输出引脚 8 之间。
有一个电阻,你现在知道它可以通过抵抗流向 LED 的电流来消耗一点能量。我们可以说电阻保护了我们的 LED。
为了让 LED 发光,我们必须创建一个电流流。将+5V 发送到数字输出 8 可以实现这一点。这样,LED 的两个引脚之间将会有电位差,使其发光。但是数字输出不应该每次都处于+5V。我们必须控制它提供这种电压的时刻。还正常吗?
让我们总结一下我们需要做什么:
-
在 250ms 内将 5V 加到数字输出 8 上。
-
停止驱动数字输出 8 1 秒。
-
每次 Arduino 上电时都重新启动
我该如何使用 C 代码做到这一点?
如果你正确地遵循了上一页的说明,你已经在你的 Arduino 板上通过 USB 线连接到电脑的一侧,并通过面包板连接到另一侧。
现在,启动你的 Arduino IDE。
从一个新的空白页面开始
如果你已经通过加载一些示例或编写一些代码测试了你的 IDE,你必须点击新建图标来加载一个空白页面,准备容纳我们的Blink250ms代码:

一个漂亮且吸引人的空白页面
根据我们使用的板设置环境
IDE 必须知道它将使用哪个板进行通信。我们将在以下步骤中完成:
-
前往工具菜单并选择正确的板。第一个是Arduino Uno:
![根据我们使用的板设置环境]()
选择你正在使用的板
-
一旦我们完成了这个步骤,我们就必须选择正确的串行端口。再次转到工具菜单并选择正确的串行端口:
-
在 OS X 上,正确的端口对于 Uno 和 Mega 2560 开始于/dev/tty.usbmodem,对于较老的板则是/dev/tty.usbserial。
-
在 Windows 上,正确的端口通常是COM3(COM1和COM2通常被操作系统保留)。顺便说一句,它也可以是COM4、COM5或任何其他端口。为了确保,请检查设备管理器。
-
在 Linux 上,端口通常是/dev/ttyUSB0:
![根据我们使用的板设置环境]()
选择与你的板对应的串行端口
-
现在,我们的 IDE 可以与我们的板通信。让我们现在推送代码。
让我们编写代码
以下为完整代码。你可以在Chapter01/Blink250ms/文件夹中的 zip 文件中找到它:
小贴士
下载示例代码
你可以从你购买的所有 Packt 书籍的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
/*
Blink250ms Program
Turns a LED connected to digital pin 8 on for 250ms, then off for 1s, infinitely.
Written by Julien Bayle, this example code is Creative Commons CC-BY-SA
*/
// Pin 8 is the one connected to our LED
int ledPin = 8; // ledPin is an integer variable initialized at 8
// --------- the setup routine runs once when you power up the board or push the reset switch
void setup() {
pinMode(ledPin, OUTPUT); // initialize the digital pin as an output because we want it to source a current
}
// --------- the loop routine runs forever
void loop() {
digitalWrite(ledPin, HIGH); // turn the LED on (HIGH is a constant meaning a 5V voltage)
delay(250); // wait for 250ms in the current state
digitalWrite(ledPin, LOW); // turn the LED off (LOW is a constant meaning a 5V voltage)
delay(1000); // wait for 1s in the current state
}
让我们给它加一点注释。确实,我们将在下一章学习如何编写自己的 C 代码,然后我将只描述这个,并给你一些小贴士。
首先,所有在/*和*/之间的内容,以及所有在//之后的内容,都只是注释。第一种形式用于一次注释多行,而另一种形式用于单行注释。你可以写任何这样的注释,并且它们不会被编译器考虑。我强烈建议你注释你的代码;这是成功的另一个关键。
然后,代码的第一部分包含一个变量声明和初始化:
int ledPin = 8;
然后,我们可以在花括号之间看到两个特定的结构:
void setup() {
pinMode(ledPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
delay(250);
digitalWrite(ledPin, LOW);
delay(1000);
}
第一个(setup())是一个函数,当 Arduino 板启动(或重置)时只执行一次;这是我们告诉板子连接 LED 的引脚是输出引脚的地方,也就是说,当激活时,这个引脚将必须驱动电流。
第二个(loop())是一个函数,当 Arduino 板供电时无限执行。这是我们代码的主要部分,其中我们可以找到我们想要点亮 LED 250 毫秒并关闭 LED 1 秒的步骤,重复进行。
最后,让我们上传代码!
如果你正确地按照之前解释的方式操作了硬件和 IDE,我们现在已经准备好将代码上传到板上。
只需在 IDE 中点击上传按钮。你会看到 TX 和 RX LED 灯微微闪烁……你的面包板上的 LED 灯应该按照预期闪烁。这是我们非常第一个 HELLO LED! 示例,希望你喜欢。
如果你想要稍微调整一下代码,你可以替换以下行:
delay(1000);
例如,用以下行:
delay(100);
现在再次上传这段新代码,看看会发生什么。
摘要
在本章中,我们了解了一些关于 Arduino 和微控制器,以及关于电的知识。这将在我们将在下一章中大量讨论电路的章节中帮助我们。
我们还安装了我们将每次编程 Arduino 板时都会使用的 IDE,甚至测试了第一段代码。我们现在可以通过学习更多关于 C 语言本身的内容来继续我们的旅程。
第二章。与 C 的第一次接触
在我的程序员生涯中,我遇到了很多基于编译器和脚本语言的情况。其中最低的共同点一直是 C 语言。
在我们的情况下,这是 嵌入式系统编程,这是 硬件编程 的另一个名称;这个说法也是正确的。
让我们来看看 C 编程究竟是什么,让我们进入一个新的世界,那就是 Arduino 编程的领域。我们还将使用一个非常必要的功能,称为 串行监控。这将在我们的 C 学习中帮我们很多,你也会理解这个功能在现实生活中的项目中也被使用。
编程简介
第一个问题,什么是程序?
程序 是你使用编程语言编写的文本,其中包含你需要处理器获取的行为。它基本上创建了一种处理输入和根据这些行为产生输出的方式。
根据维基百科 (en.wikipedia.org/wiki/Computer_programming):
编程是设计、编写、测试、调试和维护计算机程序源代码的过程。
当然,这个定义非常简单,它也适用于微控制器,因为我们已经知道后者基本上是一种计算机。
设计一个程序 是你在开始编码之前必须首先思考的事实。这通常涉及到编写、绘制以及制作你希望处理器为你执行的所有动作的示意图。有时,这也意味着编写我们所说的 伪代码。我希望你能记住,这是我们上章在想要精确定义我们期望的 LED 行为的所有步骤时创建的内容。
我不同意很多人称它为 伪代码,因为它实际上更像是 真实代码。
我们所说的 伪代码 是非常有帮助的东西,因为它易于阅读,由清晰的句子组成,并且用于更好地思考和说明我们的目的,这是成功的关键。
我固件 伪代码 的定义示例可能如下:
-
测量热敏传感器的当前值。
-
检查温度是否大于 30°C,如果是,则发出声音。
-
如果不是,点亮蓝色 LED。
-
并且将这些之前的步骤永久地放在一个循环中。
编写一个程序 通常是将伪代码转换为真实且格式良好的代码的过程。这涉及到对编程语言的了解,因为这是你真正编写程序的时候。这就是我们接下来要学习的。
测试 是在你对代码进行了一些修改后运行程序时的明显步骤。当你也是程序员时,这是一个既令人兴奋又有点害怕的时刻,因为你害怕虫子,那些让你的程序运行结果与最初预期完全不同的事情。
调试 是当你试图找出为什么程序没有按预期工作得很好时的一个非常重要的步骤。你正在追踪打字错误、逻辑差异和全局程序架构问题。你需要监控事物,并且经常需要稍微修改你的程序,以便精确地追踪它是如何工作的。
维护源代码 是程序生命周期中帮助避免过时的部分。
程序正在运行,并且你逐步改进它;你根据硬件的进化更新它,有时,你需要调试它,因为用户仍然发现了这个未知的错误。这一步骤增加了你程序的使用寿命。
不同的编程范式
范式是描述某物的方式。它可以是某物的表示或理论模型。
将编程应用于,编程范式是计算机编程的基本风格。
以下是有四种主要的编程范式:
-
面向对象
-
命令式
-
函数式
-
逻辑编程
一些语言遵循的不是一种而是多种范式。
这本书的目的不是围绕这些进行辩论,但我可以增加一个,它可以是这些的组合,同时也描述了一个特定的概念:可视化编程。我们将在第六章中了解到最强大的框架之一,即感知世界 – 使用模拟输入感受,也就是Max 6框架(之前称为Max/MSP)。
编程风格
没有科学或普遍的方法来定义什么是绝对最佳的编程风格。然而,我可以引用六个项目,这些项目可以帮助我们理解在这本书中我们将尝试一起做什么,以编写出优秀的程序。我们的目标是以下内容:
-
可靠性:这使代码能够在运行时处理其生成的错误
-
稳定性:这提供了一个框架来预测用户端的问题(错误的输入)
-
人体工程学:这有助于直观地轻松使用它
-
可移植性:这是为广泛的平台设计程序
-
可维护性:这是即使你没有亲自编写代码也能轻松修改它的程度
-
效率:这表明程序运行得非常顺畅,而不消耗大量资源
当然,我们将在本书的例子中回到它们,我相信你将逐步改进你的风格。
C 和 C++?
丹尼斯·里奇 en.wikipedia.org/wiki/Dennis_Ritchie) 在贝尔实验室从 1969 年到 1973 年开发了 C 编程语言。它通常被定义为一门通用编程语言,并且确实是所有时代中最常用的语言之一。它最初被用来设计具有众多要求的 Unix 操作系统(en.wikipedia.org/wiki/Unix),特别是高性能。
它影响了很多非常知名且广泛使用的语言,如 C++、Objective-C、Java、JavaScript、Perl、PHP 等。
C 语言既适用于命令式也适用于结构化。它非常适合 8 位和 64 位处理器,对于既有几字节内存也有太字节内存的系统,以及涉及庞大团队的大型项目,以及只有单个开发者的最小项目。
是的,我们将学习一种语言,它将打开你的思路,让你接触到全球和通用的编程概念!
C 语言无处不在
事实上,C 语言提供了很多优点。它们如下:
-
它体积小,易于学习。
-
它是处理器无关的,因为几乎世界上所有的处理器都有相应的编译器。这种无关性为程序员提供了非常有用的东西:他们可以专注于算法和工作的应用层面,而不是在每一行代码中都要考虑硬件层面。
-
它是一种非常“底层”的高级语言。
这是它的主要优势。Dennis M. Ritchie 在他的与 Brian W. Kernighan 合著的《C 程序设计语言》一书中对 C 语言评论道:
C 是一种相对“底层”的语言。这种描述并不是贬义的;它仅仅意味着 C 处理的是大多数计算机都处理的对象。这些对象可以用真实机器实现的算术和逻辑运算符组合和移动。
今天,这是唯一一种可以如此容易地与底层硬件引擎交互的语言,这也是为什么 Arduino 工具链基于 C 语言的原因。
Arduino 是用 C 和 C++编写的
C++可以被认为是 C 的超集。这意味着 C++为 C 带来了新的概念和元素。基本上,C++可以定义为具有面向对象实现的 C(en.wikipedia.org/wiki/Object-oriented_programming),这是一个高级特性。这是一个非常好的特性,它带来了新的设计方式。
我们将在本书稍后更深入地探讨这个概念,但基本上,在面向对象的程序中,你定义称为类的结构,它们是一种模型,然后你创建这些类的实例,这些实例在运行时拥有自己的生命周期,并且尊重和继承它们所属类的结构。
面向对象编程(OOP)提供了四个非常有用且有趣的特点:
-
继承(类可以从其父类继承属性和行为)
-
数据封装(每个实例保留其数据和函数)
-
对象标识(每个实例都是独立的)
-
多态(每个行为都可以依赖于上下文)
在 OOP 中,我们首先定义类,然后使用称为构造函数的特定函数来创建这些类的实例。想象一下,一个类是一类房子的地图,而实例则是根据这个地图建造的所有房子。
几乎所有的 Arduino 库都是使用 C++编写的,以便易于重用,这是编程中最重要的品质之一。
Arduino 原生库和其他库
编程库是一组可供程序使用的资源。
它们可以包括不同类型的东西,如下所示:
-
配置数据
-
帮助和文档资源
-
子程序和可重用代码部分
-
类
-
类型定义
我喜欢说图书馆提供了一种行为封装;您不需要知道如何实现该行为,只需使用它即可。
图书馆可以是非常具体的,也可以具有全局目的。
例如,如果您打算设计固件,将 Arduino 连接到互联网以从邮件服务器获取一些信息,并根据邮件服务器响应的内容以某种方式使 LED 矩阵闪烁,您有以下两种解决方案:
-
从头开始编写整个固件
-
使用库
即使我们都喜欢编写代码,但如果我们能专注于我们设计的全局目的,我们会更快乐,不是吗?
在这种情况下,我们将尝试找到已经为所需行为专门设计的库。例如,可能有一个专门用于 LED 矩阵控制的库,还有一个具有服务器连接目的的库。
发现 Arduino 原生库
原生库是为了一个非常基础和全局的目的而设计的。这意味着它可能不够用,但也意味着您将在所有固件设计中每次都使用它。
您可以在arduino.cc/en/Reference/HomePage找到它。现在您对这个页面应该很熟悉了!
它分为以下三个部分:
-
结构(从全局条件控制结构到更具体的结构)
-
变量(与类型和类型之间的转换相关)
-
函数(从 I/O 函数到数学计算函数等)
以下步骤可用于在 IDE 中直接查找帮助:
-
打开您的 IDE。
-
前往文件 | 示例;您将看到以下屏幕截图:
![发现 Arduino 原生库]()
在菜单的第一部分(在先前的屏幕截图中),您有与原生库相关的许多示例。
-
选择02.Digital按钮。
-
将显示一个新窗口。右键单击代码中的彩色关键字,如图所示:
![发现 Arduino 原生库]()
在 Arduino IDE 中直接查找所有保留关键字的参考信息
-
您可以在此上下文菜单的底部看到在参考中查找*。这是一个您现在就会理解的非常有用的工具;点击它!
您的 IDE 直接调用了您的默认浏览器,并显示了一个与您点击的关键字帮助页面相对应的 HTML 页面。您可以在 IDE 内部保持专注并获取帮助。

可用的有用本地帮助文件
包含但未直接提供的其他库
Arduino 库逐步包括了必要的和有用的其他库。我们在前面的章节中看到,现在使用的库已经集成到 Arduino 分发的 核心 中,这有点滥用,但很好地总结了它们在您仅安装 Arduino IDE 包时即可使用的事实。
一些非常有用的包含库
-
EEPROM提供在硬件存储组件中读取/写的函数和类。这对于存储超出 Arduino 电源状态的内容非常有用,也就是说,即使在断电时。
-
Ethernet帮助在以太网网络上进行第 2 层和第 3 层通信。
-
Firmata用于串行通信。
-
SD提供了一种读取/写入 SD 卡的简单方法;它是对 EEPROM 解决方案更用户友好的替代方案。
-
伺服帮助控制伺服电机。
核心分发中还有一些其他库。有时,还会包含新的库。
一些外部库
我建议您检查链接 arduino.cc/en/Reference/Libraries 上同一页引用的库。
我特别使用了以下很多库:
-
TLC5940:用于平滑地控制 16 通道、12 位 LED 控制器
-
MsTimer2:用于触发一个必须非常快且甚至每 1 毫秒就要执行一次的动作(这个库也是芯片组中包含的硬件定时器的一个很好的黑客技巧)
-
Tone*:用于生成可听见的方波
您可以使用 Google 搜索更多库。您会发现很多库,但并非所有都同等文档化和维护。我们将在本书的最后一章中看到如何创建我们自己的库,当然也包括如何为其他用户和 ourselves 优雅地文档化。
检查所有基本开发步骤
我们在这里聚在一起不是为了理解代码编译的整个细节。但我想要提供一个全局的解释,这将帮助您更好地理解它的工作原理。这也有助于您理解如何调试源代码以及为什么在某些随机情况下某些东西不会工作。
让我们从一张流程图开始,展示整个流程。

从源代码到二进制可执行代码
以下步骤用于将代码从源代码转换为可执行的生产阶段:
-
C 和 C++源代码正是您在第一章 让我们连接事物 中为
Blink250ms项目编写的代码类型。 -
头文件通常包含在代码的开头,并引用其他具有
.h扩展名的文件,其中包含一些定义和类声明。这种设计,其中你有源代码(你目前正在编写的程序)和头文件(已制作元素)的单独文件,为你已经编写的代码的复用提供了一个很好的方式。 -
预处理器是一种基本替换代码中文本元素的例程,考虑到头文件和其他常量的定义。
-
解析器准备一个将被翻译的文件,该文件将被汇编以生成多个目标文件。
-
目标文件包含机器代码,这些代码不能直接由任何硬件处理器执行。
-
最后一个重要步骤是由链接器程序执行的链接。链接器将之前编译步骤产生的所有目标文件组合成一个单一的名为程序的可执行文件。
-
从源代码到目标文件,所有过程都概括为
编译。 -
通常,库提供目标文件,供链接器链接。有时,特别是在开源世界中,库还附带源代码。这使得对库的任何更改都更容易。在这种情况下,库本身必须编译以生成在全局代码编译中使用的所需目标文件。
-
因此,我们将编译定义为从源代码到程序的整体过程。
我甚至应该使用并介绍另一个术语:交叉编译。实际上,我们是在我们的计算机上编译源代码,但最终目标处理器是我们生成的程序(固件)的 Arduino 处理器。
通常,我们将交叉编译定义为使用一个处理器编译源代码以制作针对另一个处理器的程序的过程。
现在,让我们进一步学习如何精确地使用 IDE 控制台测试我们的初始 C 代码片段。
使用串行监视器
Arduino 板本身可以使用基本的串行通信协议轻松通信。
基本上,串行通信是发送数据元素到通道的过程,通常称为总线。通常,数据元素是字节,但这完全取决于串行通信的实现。
在串行通信中,数据是顺序地发送的,一个接一个。这与并行通信相反,在并行通信中,数据通过多个通道同时发送。
波特率
因为想要使用串行通信进行通信的两个实体必须对“嘿,一个单词是什么?”这个问题有相同的答案,所以我们必须在双方使用相同的传输速度。实际上,如果我发送001010101010,这是一个完整的单词,还是有多个单词?我们必须定义,例如,一个单词是四位数长。然后,我们可以理解前面的例子包含三个单词:0010、1010和1010。这涉及到一个时钟。
那个时钟定义是通过在特定的波特率下初始化串行通信来实现的,也称为波特率。
1 波特表示每秒传输 1 个符号。一个符号可以超过一个比特。
这就是为什么我们不必在 bps(每秒比特数)、波特率之间产生混淆!
使用 Arduino 进行串行通信
每块 Arduino 板至少有一个串行端口。你可以通过使用数字引脚 0 和 1,或者直接使用 USB 连接来使用它,当你想要通过串行通信与电脑通信时。
你可以检查arduino.cc/en/Reference/serial。
在 Arduino 板上,你可以分别在数字引脚 0 和 1 上读取 RX 和 TX。TX代表发送,RX代表接收;实际上,最基本的串行通信只需要两根线。
我们将在第十章第十章。一些高级技巧的使用 I2C 和 SPI 进行 LCD、LED 和其他有趣游戏部分稍后描述许多其他类型的串行通信总线。
注意
如果你要在 Arduino 板上使用串行通信,你不能使用数字引脚 0 和 1。

检查数字引脚 1 和 0 上的 TX 和 RX
Arduino IDE 提供了一个很好的串行监控器,它通过 USB 接口显示板通过 USB 接口发送给电脑的所有符号。它提供了从 300 波特到 115,200 波特的多种波特率。我们将在接下来的章节中检查如何使用它。
串行监控
串行监控是创建与我们的板非常基本和简单通信的方式!这意味着我们可以通过串行监控来编程它,让它对我们说话。
如果你必须调试某些东西,并且板的行为与你的预期不同,你想“验证问题是否源于固件”,你可以创建一些将消息写入你的例程。这些消息被称为跟踪。跟踪对于调试源代码可能是完全必要的。
跟踪将在下一章中详细介绍。
让 Arduino 与我们对话
想象一下,你已经仔细地跟随了Blink250ms项目,一切连接都正确无误,你也再次检查过,代码看起来也没有问题,但它就是不起作用。
我们的 LED 根本就没有闪烁。如何确保你的代码中的loop()结构正确运行呢?我们将稍微修改一下代码,以便追踪其步骤。
将串行通信添加到 Blink250ms
在下面的代码中,我们将为 LED 添加串行通信,使其每 250 毫秒闪烁一次:
-
打开您之前的代码。
-
使用 另存为 创建一个名为
TalkingAndBlink250ms的另一个项目。注意
从一个已经存在的代码开始,将其保存为另一个名称,并根据您的需求进行修改,这是一个好的实践。
-
通过添加以下以
Serial开头的所有行来修改当前代码:/* TalkingAndBlink250ms Program Turns a LED connected to digital pin 8 on for 250ms, then off for 1s, infinitely. In both steps, the Arduino Board send data to the console of the IDE for information purpose. Written by Julien Bayle, this example code is under Creative Commons CC-BY-SA */ // Pin 8 is the one connected to our pretty LED int ledPin = 8; // ledPin is an integer variable initialized at 8 // --------- setup routine void setup() { pinMode(ledPin, OUTPUT); // initialize the digital pin as an output Serial.begin(9600); // Serial communication setup at 9600 baud }// --------- loop routine void loop() { digitalWrite(ledPin, HIGH); // turn the LED on Serial.print("the pin "); // print "the pin " Serial.print(ledPin); // print ledPin's value (currently 8) Serial.println(" is on"); // print " is on" delay(250); // wait for 250ms in the current state digitalWrite(ledPin, LOW); // turn the LED off Serial.print("the pin "); // print "the pin " Serial.print(ledPin); // print ledPin's value (still 8) Serial.println(" is off"); // print " is off delay(1000); // wait for 1s in the current state }注意
请注意,我每次都会稍微突出显示注释代码,以便使内容更易于阅读。例如,在以下步骤中,我不会写出以下注释:
// ---------- 循环例程
您也可以在文件夹
Chapter02/TalkingAndBlink250ms/中的 zip 文件中找到整个代码。 -
点击 Arduino IDE 中的串行监视器按钮:
![向 Blink250ms 添加串行通信]()
点击右上角的玻璃图标以激活串行监视器
-
选择与代码中相同的波特率,该波特率位于串行监视窗口的右下角菜单中,并观察正在发生的事情。
![向 Blink250ms 添加串行通信]()
您的 Arduino 板似乎正在与您对话!
您将注意到一些消息出现在串行监视器窗口中,与 LED 闪烁状态同步。
现在,我们可以确信我们的代码是正确的,因为每个消息都已发送,并且所有行都是顺序处理的;这意味着 digitalWrite() 函数也被正确调用(没有阻塞)。这些信息可以作为一个线索,例如,再次检查我们的电路,以尝试在那里而不是在代码中找到错误。
当然这是一个简单的例子,但我相信您理解了目标和追踪代码的力量!
更详细地了解串行函数
让我们检查我们在代码中添加了什么。
Serial.begin()
一切都从 Serial.begin() 函数开始。这个函数在 setup() 例程中只执行一次,即当 Arduino 启动时。
在代码中,我设置了板子以 9,600 波特率启动串行通信。
Serial.print() 和 Serial.println()
Serial.print() 和 Serial.println() 几乎相同:它们将某些内容写入串行输出,但 ln 版本还会添加回车和换行符。
这个函数的语法是 Serial.print(val) 或 Serial.print(val,format)。
您可以传递一个或两个参数。
基本上,如果 Serial.print(5) 将数字 5 打印为 ASCII 编码的十进制符号,那么 Serial.print(5,OCT) 将数字 5 打印为 ASCII 编码的八进制符号。
深入挖掘…
如果您仔细检查了代码(我相信您确实检查了),您会注意到我们放置了两组三行:一组在 digitalWrite(ledPin,HIGH) 函数之后,该函数点亮了 LED,另一组在关闭 LED 的行之后。
明白了?
我们要求 Arduino 板根据传递给数字引脚 8(LED 仍然连接的引脚)的最后一个命令发送消息。当请求引脚传递电流(LED 开启时),板会发送消息;当引脚不传递电流(LED 关闭时),板会发送另一条消息。
你刚刚编写了你的第一个跟踪例程。
从计算机与板通信
你可能注意到了串行监视器窗口中的文本字段和发送按钮:

我们可以通过串行通信将符号发送到我们的 Arduino 板
这意味着我们也可以使用这个工具从我们的计算机向板发送数据。然而,固件板必须实现一些其他功能,以便能够理解我们想要发送的内容。
在本书的后面部分,我们将看到如何使用串行监视器窗口、天才的 Processing 框架和 Max 6 框架轻松地向 Arduino 板发送消息。
摘要
在本章中,我们学习了使用 C 语言进行编程。我们还学习了如何使用 Arduino IDE 的串行监视功能,以便更了解我们的 Arduino 处理器在实时中使用跟踪时发生的情况。
我提到了串行通信,因为它非常有用,并且在许多需要计算机和 Arduino 板之间进行通信的实际情况项目中也被使用。它也可以用于两个 Arduino 板之间,或者 Arduino 板与其他电路之间。
在下一章中,我们将通过使用串行监视器窗口输入 C 代码,以便使事情更加具体化。
第三章。C 基础——让你变得更强大
C 语言编程并不那么难。但需要在开始时投入足够的工作。幸运的是,我和你在一起,我们有一个非常好的朋友,从第三章开始——我们的 Arduino 板。现在,我们将深入探讨 C 语言,我会尽我所能使其更加具体,而不是抽象。
本章和下一章真正是 C 语言导向的,因为 Arduino 程序设计需要编程逻辑语句的知识。在这两章之后,你将能够阅读这本书中的任何代码;这些坚实的基础也将帮助你进行进一步的项目,甚至那些与 Arduino 无关的项目。
我还将逐步介绍我们以后会用到的新概念,例如函数。如果你不太理解,不要害怕,我有时喜欢我的学生逐步听到一些单词,甚至在没有适当定义的情况下,因为这有助于进一步的解释。
所以,如果我没有定义它但提到了它,请放心,解释将在后面进行。让我们深入探讨。
接近变量和数据类型
我们已经在之前的章节示例中使用了变量。现在,让我们更好地理解这个概念。
什么是变量?
变量 是一个与符号名称相关的内存存储位置。这个预留的内存区域可以被填充或留空。基本上,它用于存储不同类型的值。我们在之前的例子中使用了变量 ledPin,使用了关键字 int。
变量非常有用的一点是我们可以在运行时更改它们的内容(值);这也是为什么它们被称为变量,与常量相比,常量也存储值,但在程序运行时不能更改。
什么是类型?
变量(和常量)与类型相关联。类型,也称为 数据类型,定义了数据的可能性质。它还提供了一种直接在内存中预留定义大小的空间的好方法。C 大约有 10 种主要的数据类型,我们可以像下面将要看到的那样进行扩展。
我故意只解释我们在 Arduino 编程中会大量使用的类型。这大约符合 80%的其他常用 C 数据类型,在这里将绰绰有余。
基本上,当我们像这里所示那样声明变量时,我们正在使用一个类型:
int ledPin; // declare a variable of the type int and named "ledPin"
为特定大小(与 int 类型相关的大小)的内存空间被预留,正如你所见,如果你只写这一行,该变量中仍然没有存储任何数据。但请记住,已经预留了内存空间,准备用来存储值。
| 类型 | 定义 | 内存大小 |
|---|---|---|
void |
这种特定类型仅在 函数 声明和定义未知类型的指针时使用。我们将在下一章中看到这一点。 | |
boolean |
它存储 false 或 true。 |
1 字节(8 位) |
char |
它以 数字 存储单引号字符,如 'a',遵循 ASCII 表(en.wikipedia.org/wiki/ASCII_chart)。它是一个 有符号 类型,存储从 -128 到 127 的数字;它可以是无符号的,然后存储从 0 到 255 的数字。 |
1 字节 |
byte |
它以 8 位无符号 数据存储数字,这意味着从 0 到 255。 | 8 位 |
int |
它以 2 字节有符号 数据存储数字,这意味着从 -32,768 到 32,767。它也可以是无符号的,然后存储从 0 到 65,535 的数字。 | 2 字节 (16 位) |
word |
它以 2 字节无符号 数据存储数字,这与 无符号 int 相同。 |
2 字节 (16 位) |
long |
它以 4 字节有符号 数据存储数字,这意味着从 -2,147,483,648 到 2,147,483,647,并且可以是无符号的,然后存储从 0 到 4,294,967,295 的数字。 | 4 字节 (32 位) |
float |
它基本上以 4 字节有符号 数据存储带有小数点的数字,范围从 -3.4028235E + 38 到 3.4028235E + 38。请注意所需的精度;它们只有六到七位小数,有时可能会给出奇怪的舍入结果。 | 4 字节 (32 位) |
double |
它通常存储比 float 值精度高两倍的 float 值。请注意,在 Arduino IDE 和板上,double 的实现与 float 完全相同;这意味着只有六到七位小数的精度。 |
4 字节 (32 位) |
| 数组 | 数组是有序的连续元素结构,这些元素类型相同,可以通过索引号访问。 | 元素数量 x 元素类型大小 |
string |
它在 char 数组中存储文本字符串,其中最后一个元素是 null,即一个特定的字符(ASCII 码 0)。请注意 string 开头的 "s" 小写字母。 |
元素数量 * 1 字节 |
String |
它是一种特定的数据结构,即类,提供了一种方便的方式来使用和操作文本字符串。它带有方法/函数,可以轻松地连接字符串、分割字符串等。请注意 String 开头的 "S" 大写字母。 |
每次都可以使用 length() 方法获取 |
滚动/包装概念
如果您超出类型的可能范围,变量将滚动到边界另一侧。
以下是一个示例:
int myInt = 32767; //the maximum int value
myInt = myInt + 1; // myInt is now -32768
这在两个方向上都可能发生,从存储 -32768 的 int 变量中减去 1 会得到 32767。请记住这一点。
声明和定义变量
我们将描述如何声明和定义变量,并学习如何同时进行这两者。
声明变量
变量的声明是一个语句,其中您指定一个 标识符、一个 类型,以及最终变量的维度。
标识符是我们所说的 变量名。您也知道类型是什么。维度对于数组很有用,例如,但对于 String(它们在内部作为数组处理)也是如此。
在 C 和所有其他强类型语言,如 Java 和 Python 中,我们 必须 在使用变量之前声明它们。无论如何,如果你忘记了声明,编译器会报错。
定义变量
下表包含了一些变量定义的示例:
| 类型 | 示例 |
|---|---|
boolean |
bool myVariable; // declaration of the variable
myVariable = true; // definition of the variable by assigning it a value
bool myOtherVariable = false; // declaration and definition inside the same statement !
|
char |
|---|
char myChar = 'U'; // declaration and definition using the ASCII value of 'U' (i.e 85)
char myOtherChar = 85; // equals the previous statement
char myDefaultChar = 128; // this gives an ERROR because char are signed from -128 to 127
unsigned char myUnsignedChar = 128; // this is correct !
|
byte |
|---|
byte myByte = B10111; // 23 in binary notation with the B notation
byte myOtherByte = 23; // equals the previous statement
|
int |
|---|
int ledPin = 8; // classic for us, now :)
unsigned myUint = 32768; // very okay with the prefix unsigned !
|
word |
|---|
word myWord = 12345;
|
long |
|---|
long myLong = -123; // don't forget that we can use negative numbers too!
long myOtherLong = 345;
unsigned myUlong = 2147483648; // correct because of the unsigned prefix
|
float |
|---|
float myFloat = -123456.1; // they can be negative.
float myOtherFloat = 1.234567; //
float myNoDecimalPointedFloat = 1234; // they can have a decimal part equaling zero
|
double |
|---|
double myDouble = 1.234567; // Arduino implementation of double is same as float
|
| 数组 |
|---|
int myIntTable[5]; // declaration of a table that can contain 5 integers
boolean myOtherTab[] = { false, true, true}; // declaration and definition of a 3 boolean arrays
myIntTable[5]; // considering the previous definition, this gives an array bound ERROR (index starts from 0 and thus the last one here is myIntTable[4])
myOtherTab[1]; // this elements can be manipulated as a boolean, it IS a boolean with the value true
|
string |
|---|
char mystring[3]; // a string of 3 characters
char mystring2[4] = {'b','y','t','e'}; // declaration & definition
char mystring3[4] = "byte"; // equals to mystring2;
char mystring4[ ] = "byte"; // equals to mystring3;
|
定义一个变量是将值赋给之前为该变量预留的内存区域的行为。
让我们声明并定义每种类型的变量。我在代码注释中添加了一些解释。
这里有一些你可以使用的示例,但你会在本书给出的每一块代码中看到,使用了不同类型的声明和定义。一旦我们连接了电路板,你就可以接受这一点。
让我们更深入地了解 String 类型。
String
String 类型值得一个单独的小节,因为它不仅仅是一个类型。实际上,它是一个对象(在面向对象编程的意义上)。
对象具有特殊的属性和函数。属性和函数是原生可用的,因为 String 现在是 Arduino 核心的一部分,即使你的 IDE 中没有一行代码,也可以将其视为一个预存在的实体。
再次强调,框架会为你处理这些事情,提供一个具有强大且已编码函数的类型/对象,这些函数可以直接使用。
在 Arduino 网站上查看 arduino.cc/en/Reference/StringObject。
String 定义是一种构造
我们讨论了变量的定义,但对象有一个类似的概念,称为 构造。
对于 String 对象,我在这里谈论的是 构造 而不是 定义,但你可以将这两个术语视为等同。在 Arduino 核心中声明 String 类型涉及对象构造函数,这是一个面向对象编程的概念;幸运的是,我们在这个阶段不必处理它。
String myString01 = "Hello my friend"; // usual constant string to construct it
String myString02 = String('U'); // convert U char into a String object
// concatenating 2 String together and put the result into another
String myString03 = String(myString01 + ", we are trying to play with String(s));
// converting the current value of integer into a String object
int myNiceInt = 8; // define an integer
String myString04 = String(myNiceInt); // convert to a String object
// converting the current value of an integer w/ a base into a String object
int myNiceInt = 47; // define an integer
String myString05 = String(myNiceInt, DEC);
String myString06 = String(myNiceInt, HEX);
String myString07 = String(myNiceInt, BIN);
使用索引和字符串内的搜索
String 是 char 元素的数组。这意味着我们可以通过它们的索引访问 String 类型的任何元素。
请记住,索引从 0 开始,而不是从 1 开始。String 对象实现了针对此特定目的的一些函数。
charAt()
考虑到 String 类型如下声明和定义:
String myString = "Hello World !!";
语句 myString.charAt(3) 返回字符串中的第四个元素,即:l。注意这里使用的特定记法:我们有 String 变量的名称,然后是一个点,接着是函数的名称(这是 String 对象的方法),然后是传递给函数的参数 3。
注意
charAt() 函数返回字符串中特定位置的字符。
语法:string.charAt(int);
int 是表示 String 值索引的整数。
返回类型:char
让我们了解其他类似的功能。你会非常频繁地使用它们,因为我们已经看到,在非常低级别的观点中,通信包括解析和处理数据,这通常可以是字符串。
indexOf() 和 lastIndexOf()
让我们考虑相同的声明/定义:
String myString = "Hello World !!";
myString.indexOf('r') 等于 8。确实,r 在字符串 myString 的值中的第九位。indexOf(val) 和 lastIndexOf(val) 都是在查找值 val 的第一个出现位置。
如果你想要从特定的点开始搜索,你可以指定一个起点,例如:indexOf(val,start),其中 start 是函数开始搜索字符串中字符 val 的索引。正如你可能已经理解的,这个函数的第二个参数(start)可以省略,默认情况下搜索从字符串的第一个元素开始,即 0。
注意
indexOf() 函数返回字符串中字符串或字符的第一个出现位置。
语法: string.indexOf(val, from);
val 是要搜索的值,可以是字符串或字符。from 是搜索的起始索引,它是一个 int 类型。此参数可以省略。搜索是向前的。
返回类型: int
类似地,lastIndexOf(val,start) 从 start 或从最后一个元素(如果你省略 start)开始向后查找 val 的最后一个出现位置。
lastIndexOf() 函数返回字符串中字符串或字符的最后一个出现位置。
注意
语法: string.lastIndexOf(val, from);
val 是要搜索的值,它是一个字符串或字符。from 是搜索的起始索引,它是一个 int 类型。此参数可以省略。搜索是向后的。
返回类型: int
startsWith() 和 endsWith()
startsWith() 和 endsWith() 函数分别检查字符串是否以作为函数参数传递的另一个字符串开始或结束。
String myString = "Hello World !!";
String anotherString ="Hell" ;
myString.startsWith(anotherString); // this returns true
myString.startsWith("World"); // this returns false
注意
如果一个字符串以与另一个字符串相同的字符开始,则 startsWith() 函数返回 true。
语法: string.startsWith(string2);
string2 是你想要测试的字符串的模式。
返回类型: boolean
我想,你现在已经开始理解了。endsWith() 也是这样工作的,但它比较字符串模式与测试字符串的末尾。
注意
endsWith() 函数如果字符串以与另一个字符串相同的字符结尾,则返回 true。
语法: string.endsWith(string2);
string2 是你想要测试的字符串的模式。
返回类型: boolean
连接、提取和替换
前面的操作也引入了新的 C 运算符。我在这里用字符串来使用它们,但你将在更广泛的环境中了解更多关于它们的信息。
连接
字符串连接是一种操作,你取两个字符串并将它们粘合在一起。它产生一个新的字符串,由前两个字符串组成。顺序很重要;你必须管理你想要附加到另一个字符串末尾的字符串。
Concat()
Arduino 核心提供了 string.concat() 函数,它专门为此目的设计。
String firstString = "Hello ";
String secondString ="World!";
// appending the second to the first and put the result in the first
firstString.concat(secondString);
注意
concat() 函数将一个字符串附加到另一个字符串(即按定义的顺序连接)。
语法:string.concat(string2);
string2 是一个字符串,并将其附加到字符串的末尾。记住,由于连接,字符串的先前内容被覆盖。
返回类型:int(如果连接发生正确,函数返回 1)。
在字符串上使用 + 操作符
另一种连接两个字符串的方法。这不是使用函数,而是使用操作符:+。
String firstString = "Hello ";
String secondString ="World!";
// appending the second to the first and putting the result in the first
firstString = firstString + secondString;
这段代码与之前的一样。+ 是一个稍后我会更好地描述的操作符。我在这里给你提供了一些东西:+ 操作符的缩写表示法:
firstString = firstString + secondString;
这也可以写成:
firstString += secondString;
尝试一下。你会理解的。
提取和替换
可以使用一些非常有用的函数进行字符串操作和修改,提取和替换字符串中的元素。
substring() 是提取器
你想提取字符串的一部分。想象一下,如果 Arduino 板通过特定的和定义良好的通信协议发送消息:
<output number>.<value>
输出数字每次用两个字符编码,值用三个字符(45 必须写成 045)。我经常这样工作,并在需要时从我的电脑的串行端口通过 USB 弹出这类消息;例如,发送一个命令以特定强度点亮特定的 LED。如果我想在第四个输出上以 100/127 的强度点亮 LED,我会发送:
04.100
Arduino 需要 理解这条消息。在不进一步讨论通信协议设计的情况下,因为这将涵盖在第七章——通过串行通信,我想向你介绍一个新功能——拆分字符串。
String receivedMessage = "04.100";
String currentOutputNumber;
String currentValueNumber;
// extracting a part of receivedMessage from index 0 (included) to 1 (excluded)
currentOutputNumber = receivedMessage.substring(0,2);
// extracting a part of receivedMessage from index 3 (included) to the end
currentValueNumber = receivedMessage.substring(3);
这段代码将 Arduino 接收到的消息分成两部分。
注意
substring() 函数从起始索引(包含)到另一个索引(不包含)提取字符串的一部分。
语法:string.substring(from, to);
from 是起始索引。结果包括 from 字符串元素的内容。to 是结束索引。结果不包括 end 字符串元素的内容,它可以省略。
返回类型:String
让我们进一步探讨字符串提取的概念并对其进行拆分。
使用分隔符拆分字符串
让我们稍微挑战一下自己。想象一下,我不知道或者我不确定消息格式(两个字符,一个点,然后是三个字符,这是我们刚刚看到的)。这是一个真实生活中的案例;在学习制作东西的过程中,我们经常会遇到那些东西没有按预期表现的情况。
假设我想使用点作为分隔符,因为我非常确定。我该如何使用我们已经学过的东西来实现这一点?我需要提取字符。好吧,我现在知道了substring()函数!
但我还需要一个索引来提取特定位置的内容。我也知道如何使用indexOf()在字符串中找到一个字符的索引。
我们就是这样做的:
String receivedMessage = "04.100";
String currentOutputNumber;
String currentValueNumber;
int splitPointIndex;
// storing the index of the separator in the String
splitPointIndex = receivedMessage.indexOf('.');
// extracting my two elements
currentOutputNumber = receivedMessage.substring(0, splitPointIndex);
currentValueNumber = receivedMessage.substring(splitPointIndex + 1);
首先,我找到分割点索引(字符串中点所在的位置)。其次,我将这个结果用作提取子字符串的最后一个元素。别担心,最后一个元素不包括在内,这意味着currentOutputNumber不包含点。
最后,我再次使用splitPointIndex作为需要提取的字符串第二部分的起始位置。然后呢?我将整数1加到它上面,因为,正如你现在掌握的substring()函数所知,与起始索引对应的元素总是被substring()操作包括。我们不想那个点,因为它只是一个分隔符。对吧?
如果你现在有点困惑,别担心。在下一章和特别是当我们开始用 Arduino 处理事情时,事情会变得更加清晰,这在书中稍后会出现。
替换
当我们想要将通信协议转换为另一个协议时,通常会使用替换操作。例如,我们需要替换字符串的一部分,以便为后续处理做准备。
让我们以之前的例子为例。现在我们想要将点替换为另一个字符,因为我们想将结果发送到另一个只理解空格字符作为分隔符的进程。
String receivedMessage = "04.100";
String originalMessage;
// keeping a trace of the previous message by putting it into another variable
originalMessage = receivedMessage;
// replacing dot by space character in receivedMessage
receivedMessage.replace('.',' ');
首先,我将receivedMessage变量的内容放入另一个名为originalMessage的变量中,因为我知道replace()函数肯定会修改处理后的字符串。然后我用replace()函数处理receivedMessage。
注意
replace()函数用另一个字符串替换字符串的一部分。
语法:string.replace(substringToReplace, replacingSubstring);
from是起始索引。结果包括from字符串元素的内容。to是结束索引。结果不包括end字符串元素的内容,它可以省略。记住,替换操作会覆盖字符串的先前内容(如果你想要保留它,请将其复制到另一个字符串变量中)。
返回类型:int(如果连接操作正确发生,函数返回1)。
这个函数显然可以替换一个字符为另一个字符。字符串是字符数组。一个字符可以作为一个只有一个元素的字符串进行处理并不奇怪。让我们稍微思考一下。
其他字符串函数
我还想快速引用一些其他字符串处理函数。
toCharArray()
此函数将字符串的所有字符复制到一个名为“缓冲区”的“真实”字符数组中,出于内部原因,也被称为缓冲区。您可以查看arduino.cc/en/Reference/StringToCharArray。
toLowerCase() 和 toUpperCase()
这些函数将它们处理的字符串替换为相同但所有字符均为小写和大写的字符串。您可以查看arduino.cc/en/Reference/StringToLower 和 arduino.cc/en/Reference/StringToUpperCase。请注意,因为它会覆盖经过此过程处理的字符串。
trim()
此函数会移除字符串中的所有空白字符。您可以查看arduino.cc/en/Reference/StringTrim。再次提醒,请注意,因为它会覆盖经过此过程处理的字符串。
length()
我想以这个函数结束。这是您将大量使用的函数。它以整数形式提供字符串的长度。您可以查看arduino.cc/en/Reference/StringLength。
在板上测试变量
以下是一段您也可以在文件夹 Chapter03/VariablesVariations/ 中找到的代码:
/*
Variables Variations Program
This firmware pops out messages over Serial to better understand variables' use.
Switch on the Serial Monitoring window and reset the board after that.
Observe and check the code :)
Written by Julien Bayle, this example code is under Creative Commons CC-BY-SA
*/
// declaring variables before having fun !
boolean myBoolean;
char myChar;
int myInt;
float myFloat;
String myString;
void setup(){
Serial.begin(9600);
myBoolean = false;
myChar = 'A';
myInt = 1;
myFloat = 5.6789 ;
myString = "Hello Human!!";
}
void loop(){
// checking the boolean
if (myBoolean) {
Serial.println("myBoolean is true");
}
else {
Serial.println("myBoolean is false");
}
// playing with char & int
Serial.print("myChar is currently ");
Serial.write(myChar);
Serial.println();
Serial.print("myInt is currently ");
Serial.print(myInt);
Serial.println();
Serial.print("Then, here is myChar + myInt : ");
Serial.write(myChar + myInt);
Serial.println();
// playing with float & int
Serial.print("myFloat is : ");
Serial.print(myFloat);
Serial.println();
// putting the content of myFloat into myInt
myInt = myFloat;
Serial.print("I put myFloat into myInt, and here is myInt now : ");
Serial.println(myInt);
// playing with String
Serial.print("myString is currently: ");
Serial.println(myString);
myString += myChar; // concatening myString with myChar
Serial.print("myString has a length of ");
Serial.print(myString.length());// printing the myString length
Serial.print(" and equals now: ");
Serial.println(myString);
// myString becomes too long, more than 15, removing the last 3 elements
if (myString.length() >= 15){
Serial.println("myString too long ... come on, let's clean it up! ");
myInt = myString.lastIndexOf('!'); // finding the place of the '!'
myString = myString.substring(0,myInt+1); // removing characters
Serial.print("myString is now cleaner: ");
Serial.println(myString);
// putting true into myBoolean
}
else {
myBoolean = false; // resetting myBoolean to false
}
delay(5000); // let's make a pause
// let's put 2 blank lines to have a clear read
Serial.println();
Serial.println();
}
将此代码上传到您的板子,然后打开串行监视器。最后,通过按下复位按钮来复位板子并观察。板子会直接将内容写入您的串行监视器,如下面的截图所示:

显示您板子所说内容的串行监视器
一些解释
所有解释将逐步进行,但这里是对当前发生情况的小结。
我首先声明了我的变量,然后在 setup() 中定义了一些。我本可以在同一时间声明和定义它们。
通过刷新记忆,setup() 只在板子启动时执行一次。然后,loop() 函数无限次地执行,依次运行每一行语句。
在 loop() 中,我首先测试 myBoolean,引入 if() 条件语句。我们将在本章学习这一点。
然后,我会对 char、int 和 String 类型进行一些操作,打印一些变量,然后修改它们并重新打印。
这里需要注意的主要点是 if() 和 else 结构。看看它,然后放松,答案很快就会到来。
范围概念
范围可以定义为变量的特定属性(以及函数,正如我们将看到的)。考虑到源代码,变量的范围是代码中该变量可见和可用的部分。
一个变量可以是全局的,然后在整个源代码中可见并可使用。但变量也可以是局部的,例如在函数内部声明,那么它只在这个特定函数内部可见。
作用域属性是通过变量声明在代码中的位置隐式设置的。你可能刚刚了解到每个变量都可以全局声明。通常,我遵循自己的数字俳句。
注意
让代码的每一部分只知道它必须知道的变量,不再多。
尽量缩小变量的作用域绝对是一个获胜的方法。查看以下示例:
// this variable is declared at the highest level, making it visible everywhere
int globalString;
void setup(){
// … some code
}
void loop(){
int a; // a is visible inside the loop function only
anotherFunction(); // calling the global function anotherFunction
// … some other code
}
void anotherFunction() {
// … yet another code
int veryLocalVar; // veryLocalVar is visible only in anotherFunction function
}
我们可以将代码的作用域表示为一个或多或少嵌套的框。

代码的作用域被视为框
外部框代表源代码的最高作用域级别。在这个级别声明的所有声明都可以被所有函数看到和使用;它是全局级别。
每个其他框代表一个特定的作用域。在一个作用域中声明的每个变量都不能在更高作用域中看到和使用,也不能在同一级别的其他作用域中。
这种表示对我那些总是需要更多视觉的学生非常有用。当我们谈论库时,我们也会使用这个隐喻,特别是。库中声明的声明可以在我们的代码中使用,如果我们当然在代码开头包含一些特定的头文件。
静态、易变和 const 限定符
限定符是用于根据限定变量改变处理器行为的关键字。实际上,编译器将使用这些限定符来改变生成的二进制固件中考虑的变量的特性。我们将学习三个限定符:static、volatile和const。
静态
当你在函数内部使用static限定符时,这会使变量在两次函数调用之间保持持久。在函数内部声明变量使变量隐式地成为函数的局部变量,正如我们刚刚学到的。这意味着只有函数才能知道和使用该变量。例如:
int myGlobalVariable;
void setup(){
}
void loop(){
myFunction(digitalPinValue);
}
void myFunction(argument){
int aLocalVariable;
aLocalVariable = aLocalVariable + argument;
// playing with aLocalVariable
}
这个变量仅在myFunction函数中可见。但是第一次循环之后会发生什么?之前的值丢失了,一旦执行了int aLocalVariable;,就会设置一个新的变量,其值为零。查看以下新代码块:
int myGlobalVariable;
void setup(){
}
void loop(){
myFunction(digitalPinValue);
}
void myFunction(argument){
static int aStaticVariable;
aStaticVariable = aStaticVariable + argument;
// playing with aStaticVariable
}
这个变量仅在myFunction函数中可见,并且在添加参数后修改了它,我们可以玩弄其新值。
在这种情况下,变量被限定为static。这意味着变量仅在第一次声明时存在。这提供了一种跟踪事物的有用方式,同时使包含此跟踪的变量局部化。
易变的
当你在变量声明语句中使用volatile修饰符时,此变量将从 RAM 中加载,而不是从板上的存储寄存器内存空间中加载。这种差异很微妙,并且此修饰符在特定情况下使用,在这些情况下,你的代码本身无法控制处理器上执行的其他操作。其中一个例子,在其他例子中,是使用中断。我们稍后会看到这一点。
基本上,你的代码正常运行,一些指令不是由这段代码触发的,而是由另一个过程,如外部事件触发的。确实,我们的代码不知道何时以及中断服务例程(ISR)做什么,但它在发生类似情况时停止,让 CPU 运行 ISR,然后继续。从 RAM 中加载变量可以防止变量值的一些可能的不一致性。
const
const修饰符表示常量。使用const修饰变量使其不可变,这听起来可能有些奇怪。
如果你在声明/定义语句之后尝试向const变量写入值,编译器会给出错误。作用域的概念也适用于这里;我们可以在函数内部或全局范围内修饰变量。此语句定义并声明了masterMidiChannel变量为常量:
const int masterMidiChannel = 10;
这相当于:
#define masterMidiChannel 10
注意
在#define语句之后没有分号。
#define似乎比const用得少一些,可能是因为它不能用于常量数组。无论情况如何,const总是可以使用的。现在,让我们继续学习一些新的运算符。
运算符、运算符结构和优先级
我们已经遇到了很多运算符。让我们首先检查一下算术运算符。
算术运算符和类型
算术运算符包括:
-
+(加号) -
-(减法) -
*(星号) -
/(斜杠) -
%(百分号) -
=(等于)
我从最后一个开始讲:= . 它是赋值运算符。我们已经多次使用它来定义变量,这仅仅意味着给它赋值。例如:
int oscillatorFrequency = 440;
对于其他运算符,我将在以下内容中区分两种不同的情况:字符类型,包括char和String,以及数值类型。运算符可以根据变量的类型改变其效果。
字符类型
char和String只能通过+来处理。正如你可能已经猜到的,+是连接运算符:
String myString = "Hello ";
String myString2 = "World";
String myResultString = myString + myString2;
myString.concat(myString2);
在此代码中,myResultString和myString的连接生成了Hello World字符串。
数值类型
对于所有数值类型(int、word、long、float、double),你可以使用以下运算符:
-
+(加法) -
-(减号) -
*(乘法) -
/(除法) -
%(取模)
以下是一个乘法的基本示例:
float OutputOscillatorAmplitude = 5.5;
int multiplier = 3;
OutputOscillatorAmplitude = OutputOscillatorAmplitude * multiplier
注意
一旦你使用float或double类型作为操作数之一,就会使用浮点计算过程。
在之前的代码中,OutputOscillatorAmplitude * multiplier 的结果是 float 类型的值。当然,除以零是 禁止的;原因在于数学而不是 C 或 Arduino。
取模是简单地将一个整数除以另一个整数的余数。我们将大量使用它来保持变量在受控和选择的范围内。如果您让一个变量无限增长,但通过例如 7 来操作它的模,结果将始终在 0(当增长变量是 7 的倍数时)和 6 之间,从而约束增长变量。
简化表示和优先级
如您所注意到的,使用之前解释过的运算符有一种简化的写法。让我们看看两种等价的表达式并解释这一点。
示例 1:
int myInt1 = 1;
int myInt2 = 2;
myInt1 = myInt1 + myInt2;
示例 2:
int myInt1 = 1;
int myInt2 = 2;
myInt1 += myInt2;
这两段代码是等价的。第一段向您介绍了运算符的优先级。在 附录 B,C 和 C++运算符优先级 中给出了所有优先级。让我们现在学习一些。
+, -, *, /, 和 % 的优先级高于 =。这意味着 myInt1 + myInt2 在赋值运算符之前计算,然后,结果被赋值给 myInt1。
第二个是简化版本。它与第一个版本等价,因此这里也适用优先级。以下是一个有点棘手的示例:
int myInt1 = 1;
int myInt2 = 2;
myInt1 += myInt2 + myInt2;
您需要知道+的优先级高于+=。这意味着运算顺序是:首先,myInt2 + myInt2,然后是myInt1 +刚刚计算出的结果myInt2 + myInt2。然后,第二个运算的结果被赋值给myInt1。这意味着它等价于:
int myInt1 = 1;
int myInt2 = 2;
myInt1 = myInt1 + myInt2 + myInt2;
增量和减量运算符
我想提醒您注意您经常会遇到的另一种简化表示:双重运算符。
int myInt1 = 1;
myInt++; // myInt1 now contains 2
myInt--; // myInt1 now contains 1
++ 等价于 +=1,-- 等价于 -=1。这些被称为 后缀增量 (++) 和 后缀减量 (--)。它们也可以用作 前缀。作为前缀的 ++ 和 -- 的优先级低于它们作为后缀使用时的等效运算符,但在两种情况下,优先级都远高于 +, -, /, *,甚至 = 和 +=。
以下是一个我可以为您提供的最常用情况的简化表格。在每组中,运算符具有相同的优先级。它使得表达式 myInt++ + 3 变得模糊不清。在这里,括号的使用有助于确定哪个计算将首先进行。
| 优先级组 | 运算符 | 名称 |
|---|---|---|
| 2 | ++``--``()``[] |
后缀增量后缀减量函数调用数组元素访问 |
| 3 | ++``-- |
前缀增量前缀减量 |
| 5 | *``/``% |
乘法除法取模 |
| 6 | +``- |
加法减法 |
| 16 | =``+=``-=``*=``/=``%= |
赋值加法赋值减法赋值乘法赋值除法赋值取模 |
我想您现在对运算符的感觉好一些了吧?让我们继续进行一个非常重要的步骤:类型转换。
类型操作
当你设计一个程序时,有一个重要的步骤,就是为每个变量选择正确的类型。
选择正确的类型
有时候,选择会受到外部因素的影响。例如,当你使用 Arduino 与一个能够以 10 位整数编码数据的外部传感器时,这种情况就会发生(2¹⁰ = 1024 个分辨率步骤)。你知道byte类型只能存储从 0 到 255 的数字吗?你可能不会选择它!你会选择int。
有时候你必须自己做出选择。想象一下,你从电脑上的 Max 6 框架补丁通过串行连接(使用 USB)将数据发送到板上。因为这是最方便的,既然你设计了它这样,补丁就会弹出封装在字符串消息中的float数字到板上。在解析后,将这些消息切割成片段以提取你需要的信息(即float部分),你会选择将其存储到int中吗?
这个问题有点难以回答。它涉及到一个转换过程。
隐式和显式类型转换
类型转换是将实体数据类型转换为另一个的过程。请注意,我没有提到变量,而是提到实体。
这是 C 语言设计的结果,我们只能转换存储在变量中的值,其他值会保持它们的类型直到它们的生命周期结束,也就是程序执行结束的时候。
类型转换可以是隐式地执行或显式地进行。为了确保每个人都跟我在一起,我会声明隐式地意味着不是明显和有意识地写出来的,与显式地意味着在代码中明确写出来的相对。
隐式类型转换
有时候,这也被称为强制转换。这发生在你没有为编译器指定任何内容,编译器必须根据它自己的基本(但通常足够智能)规则自动进行转换时。一个经典的例子是将float值转换为int值。
float myFloat = 12345.6789 ;
int myInt;
myInt = myFloat;
println(myInt); // displays 12345
我使用赋值运算符(=)将myFloat的内容放入myInt。这会导致float值的截断,即去除小数部分。如果你继续只使用myInt变量而不是myFloat,你肯定已经丢失了一些东西。这可能没问题,但你必须记住这一点。
另一个不太经典的例子是int类型到float类型的隐式转换。int没有小数部分。隐式转换为float不会产生除了等于零的小数部分之外的其他东西。这是简单的一部分。
但要小心,你可能会对int到float的隐式转换感到惊讶。整数是使用 32 位编码的,但即使float也是 32 位,它们的尾数(也称为尾数)是使用 23 位编码的。如果你没有精确地记住这一点,没关系。但我希望你能记住这个例子:
float myFloat;
long int myInt = 123456789;
void setup(){
Serial.begin(9600);
myFloat = myInt;
}
void loop(){
Serial.println(myFloat); // displays a very strange result
}
代码的输出如下所示:

从int到float隐式转换的奇怪结果
我将123456789存储到long int类型中,这是完全合法的(long int是 32 位有符号整数,能够存储从-2147483648到2147483647的整数)。赋值后,我显示的结果是:123456792.00。我们当然期望的是123456789.00。
注意
隐式类型转换规则:
-
long int到float可能会导致错误的结果 -
float到int会移除小数部分 -
double到float会四舍五入double的数字 -
long int到int会丢弃编码的高位比特
显式类型转换
如果你想要有可预测的结果,每次你都可以显式地转换类型。Arduino 核心包含了六个转换函数:
-
char()
-
int()
-
float()
-
word()
-
byte()
-
long()
我们可以通过将你想转换的变量作为函数的参数来使用它们。例如,myFloat = float(myInt);其中myFloat是float类型,myInt是int类型。不用担心使用,我们会在我们的固件中稍后使用它们。
注意
我的转换规则:注意你进行的每一个类型转换。对于你来说,它们都不应该是显而易见的,并且即使语法完全正确,它们也可能导致你的逻辑错误。
比较值和布尔运算符
我们现在知道了如何将实体存储到变量中,转换值,并选择正确的转换方法。我们现在将学习如何比较变量的值。
比较表达式
有六个比较运算符:
-
==(等于) -
!=(不等于) -
<(小于) -
>(大于) -
<=(小于或等于) -
>=(大于或等于)
以下是一个代码中的比较表达式:
int myInt1 = 4;
float myFloat = 5.76;
(myInt1 > myFloat) ;
这样的表达式没有任何作用,但它合法。比较两个元素会产生一个结果,在这个小例子中,它没有被用来触发或执行任何操作。myInt1 > myFloat是一个比较表达式。结果显然是true或false,我的意思是它是一个boolean值。这里它是false,因为4不大于5.76。我们也可以将比较表达式组合起来,以创建更复杂的表达式。
使用布尔运算符组合比较
有三个布尔运算符:
-
&&(和) -
||(或) -
!(非)
是时候回忆一些使用三个小表的逻辑运算了。你可以像阅读列元素 + 比较运算符 + 行元素那样阅读这些表;运算的结果位于列和行的交叉点。
二元运算符 AND,也写作&&:
&& |
true | false |
|---|---|---|
| true | true | false |
| false | false | false |
然后是二元运算符 OR,也写作||:
|| |
true | false |
|---|---|---|
| true | true | true |
| false | true | false |
最后,一元运算符 NOT,也写作!:
| true | false | |
|---|---|---|
! |
false | true |
例如,true && false = false,false || true = true。&&和||是二元运算符,它们可以比较两个表达式。
! 是一个 一元运算符,只能与一个表达式一起使用,逻辑上取反。&& 是逻辑与。当比较的两个表达式都为真时为真,在其他所有情况下为假。|| 是逻辑或。当至少有一个表达式为真时为真,当它们都为假时为假。它是包含性或。! 是否定运算符,即 NOT。它基本上将假和真反转成真和假。
当你想要在代码中执行一些测试时,这些不同的操作非常有用且必要。例如,如果你想比较一个变量与特定值。
结合否定和比较
考虑两个表达式 A 和 B:
-
NOT(A
&&B) = (NOT A||NOT B) -
NOT (A
||B) = (NOT A&&NOT B)
这在你创建代码中的条件时可能非常有用。例如,让我们考虑两个包含四个变量 a、b、c 和 d 的表达式:
-
a
<b -
c
>=d
!(a < b) 的意义是什么?它是表达式的否定,其中:
!(a < b) 等于 (a >= b)
a 严格小于 b 的相反面是 a 大于或等于 b。同样地:
!(c >= d) 等于 (c < d)
现在,让我们结合一下。让我们取全局表达式的反:
(a < b) && (c >= d) 和 !((a < b) && (c >= d)) 等于 (!(a < b) || !(c >= d)) 等于 (a >= b) || (c < d)
这里是另一个组合示例,引入了 运算符优先级 的概念:
int myInt1 = 4;
float myFloat = 5.76;
int myInt2 = 16;
(myInt1 > myFloat && myInt2 < myFloat) ;
( (myInt1 > myFloat) && (myInt2 < myFloat) ) ;
我的两个陈述是等价的。这里发生优先级,我们现在可以将这些运算符添加到之前的优先级表中(检查 附录 B,C 和 C++中的运算符优先级)。我添加了比较运算符:
| 优先级组 | 运算符 | 名称 |
|---|---|---|
| 2 | ++``--``()``[] |
后缀增量后缀减量函数调用数组元素访问 |
| 3 | ++``-- |
前缀增量前缀减量 |
| 5 | *``/``% |
乘除取模 |
| 6 | +``- |
加减 |
| 8 | <``<=``>``>= |
小于小于等于大于大于等于 |
| 9 | ==``!= |
等于不等于 |
| 13 | && |
逻辑与 |
| 14 | || |
逻辑或 |
| 15 | ?: |
三元条件运算符 |
| 16 | =``+=``-=``*=``/=``%= |
赋值赋值加赋值减赋值乘赋值除赋值取余 |
如同往常,我稍微作弊了一下,添加了包含唯一运算符的优先级组 15,即稍后我们将看到的三元条件运算符。让我们转到条件结构。
在代码中添加条件
因为我在生物学方面有研究,并且有硕士学位,所以我熟悉有机和生物行为。我喜欢告诉我的学生,代码,尤其是在交互设计领域的工作中,必须是有生命的。使用 Arduino,我们经常构建能够“感受”真实世界并通过行动与之交互的机器。没有条件语句是无法做到这一点的。这种语句被称为控制结构。当我们测试包括变量显示等的大代码时,我们使用了一个条件结构。
if 和 else 条件结构
这是我们没有解释的一个例子。你已经学会了耐心和禅。事情开始出现,对吧?现在,让我们来解释它。这个结构非常直观,因为它非常类似于任何条件伪代码。这里有一个:
如果变量a的值小于变量b的值,则打开 LED。否则关闭它。
现在是真正的 C 代码,我在其中通过给出 1 或 0 的状态来简化 LED 的部分,这取决于我想要在代码中进一步做什么:
int a;
int b;
int ledState;
if (a < b) {
ledState = 1;
}
else {
ledState = 0;
}
我认为这已经很清楚了。以下是这种结构的通用语法:
If (expression) {
// code executed only if expression is true
}
else {
// code executed only if expression is false
}
表达式评估通常会产生布尔值。但在这个结构中,表达式的数值结果也可以是正确的,即使它不太明确,我个人不喜欢。在 Arduino 核心中,表达式的数值结果为0等于false,而对于任何其他值,都等于true。
注意
隐式通常意味着使你的代码更短更简洁。根据我个人的看法,这也意味着几个月后当你不得不支持和维护包含大量隐式内容的代码时,你会非常不高兴。
我鼓励我的学生要明确和详细。我们不是在这里为了将代码压缩到极小的内存中,相信我。我们不是在谈论将 3 兆字节的代码减少到 500 千字节,而是更多地减少到 198 千字节。
将 if…else 结构链接到另一个 if…else 结构
以下是一个修改后的示例:
int a;
int b;
int ledState;
if (a < b) {
ledState = 1;
}
else if (b > 0) {
ledState = 0;
}
else {
ledState = 1;
}
第一个if测试是:如果a小于b。如果是true,我们将值1放入变量ledState中。如果是false,我们进入下一个语句else。
这个else包含对b的另一个测试:b是否大于0?如果是,我们将值0放入变量ledState中。如果是false,我们可以进入最后一个情况,最后一个else,并将值1放入变量ledState中。
小贴士
一个常见的错误 - 缺少一些情况
有时,if…else链非常复杂和长,我们可能会错过一些情况,并且没有任何情况被验证。要明确,并尝试检查整个情况宇宙,并根据它编写条件。
一个不错的建议是尝试将所有情况都写在纸上,并尝试找到漏洞。我的意思是,变量的值部分没有通过测试。
带有组合比较表达式的 if…else 结构
以下是我之前例子中注释更多的一些代码:
int a;
int b;
int ledState;
if (a < b) { // a < b
ledState = 1;
}
else if (b > 0) { // a >= b and b > 0
ledState = 0;
}
else { // a >= b and b < 0
ledState = 1;
}
我们也可以按照我之前在代码中写的注释来这样写:
int a;
int b;
int ledState;
if (a < b || (a >= b && b < 0) ) {
ledState = 1;
}
else if (a >= b && b > 0) {
ledState = 0;
}
这可以被认为是一个更紧凑的版本,其中所有关于 LED 开关的语句都在一个地方,同样适用于关闭它。
寻找条件结构的所有情况
假设你想测试一个温度值。你有两个特定的限制/点,你希望 Arduino 做出反应,例如通过点亮 LED 或任何与真实世界交互的事件。例如,这两个限制是:15 摄氏度和 30 摄氏度。如何确保我涵盖了所有情况?最好的方法是拿一支笔,一张纸,画一画。

检查所有可能的 T 值
我们有三个部分:
-
T < 15
-
T > 15 但 T < 30
-
T > 30
因此,我们有三个情况:
-
T < 15
-
T > 15 且 T < 30
-
T > 30
当 T = 30 或 T = 15 时会发生什么?这些都是我们逻辑中的漏洞。根据我们如何设计我们的代码,这可能会发生。匹配所有情况意味着:包括 T = 15 和 T = 30 的情况。我们可以这样做:
float T ; // my temperature variable
if (T < 15) {
colorizeLed(blue);
}
else if (T >= 15 && T < 30) {
colorizeLed(white);
}
else if (T >= 30) {
colorizeLed(red);
}
我将这两个情况包含在我的比较中。15 摄氏度包含在第二个温度区间内,30 摄氏度在最后一个区间内。这是一个我们如何做到的例子。
我希望你在这种情况下记得使用笔和纸。这有助于你设计和特别是在设计步骤中从 IDE 中跳出,这实际上是非常好的。现在让我们探索一个新的条件结构。
switch…case…break 条件结构
在这里,我们将看到一个新的条件结构。标准语法如下所示:
switch (var) {
case label:
// statements
break;
case label:
// statements
break;
default:
// statements
}
var 与每个情况标签进行比较。如果 var 等于特定的 label 值,则执行此情况中的语句,直到下一个 break。如果没有匹配,并且你使用了可选的 default: 情况,则执行此情况的语句。如果没有 default: 情况,则不执行任何操作。label 必须是一个值,而不是一个字符或字符串。让我们举一个更具体的例子:
float midiCCMessage;
switch (midiCCMessage) {
case 7:
changeVolume();
break;
case 10:
changePan();
break;
default:
LedOff();
}
这段代码等同于:
float midiCCMessage;
if (midiCCMessage == 7) changeVolume();
else if (midiCCMessage == 10) changePan ();
else LedOff();
你好吗?
注意
我想要说的是,当你想要将一个变量与许多唯一值进行比较时,使用 switch…case…break,否则使用 if…else。
当你有比较区间时,if…else 更方便,因为你可以使用 < 和 >,而在 switch…case…break 中则不能。当然,我们可以结合两者。但请记住,尽量保持你的代码尽可能简单。
三元运算符
这种奇怪的符号通常对我的学生来说完全陌生。我过去常说,“嘿!这比 Arduino 更像是 C 语言”当他们回答“这就是我们忘记它的原因”时。淘气的学生!这个三元运算符接受三个输入元素。语法是 (expression) ? val1 : val2。
表达式被测试。如果是 true,则整个语句返回(或等于)val1,如果是 false,则等于 val2。
再次想象我们的 Arduino、温度传感器和唯一的限制是 20 摄氏度。我想如果T小于限制,就使 LED 变蓝,如果T大于或等于 20 摄氏度,就变红。以下是我们会如何使用两个三元运算符:
Int T;
Int ledColor; // 0 means blue, 1 means red
ledColor = (T < 20) ? 0 : 1;
这可以是一种很好的表示法,特别是如果你不需要在每个情况下执行语句,而只需要变量赋值的话。
为重复性任务创建智能循环
循环是一系列在时间上重复的事件。基本上,计算机最初被设计出来是为了进行大量的重复计算以节省人类的时间。设计一个循环来重复必须重复的任务似乎是一个自然的想法。C 语言原生实现了一些设计循环的方法。Arduino 核心自然包括三种循环结构:
-
for -
while -
do…while
for循环结构
for循环语句相当容易使用。它基于至少一个从你定义的特定值开始的计数器,并增加或减少它,直到另一个定义的值。其语法如下:
for (declaration & definition ; condition ; increment) {
// statements
}
计数器也被称为index。我在这里给你一个真实示例:
for (int i = 0 ; i < 100 ; i++) {
println(i);
}
这个基本示例定义了一个循环,它打印从0到99的所有整数。整数类型变量i的声明/定义是for结构的第一个元素。然后,条件描述了在哪种情况下必须执行此循环中包含的语句。最后,执行i++增量。
注意增量元素。它是以增量作为后缀定义的。这意味着在这里,增量发生在考虑的i值执行语句之后。
让我们中断前两个和最后两个i值的循环,看看会发生什么。第一个和第二次迭代的整数变量i的声明如下所示:
-
i = 0,i小于100吗?是的,println(0),增加i -
i = 1,i小于100吗?是的,println(1),增加i
对于最后两次迭代,i的值如下所示:
-
i = 99,i小于100吗?是的,println(99),增加i -
i = 100,i小于100吗?不,停止循环
当然,索引可以在for结构之前声明,并在for结构内部定义。我们也可以在声明和定义变量之前进行,我们会有:
int i = 0;
for ( ; i < 100 ; i++) {
println(i);
}
这看起来有点奇怪,但在 C 语言和 Arduino 核心中都是合法的。
小贴士
索引的作用域
如果索引已经在for循环括号内声明,其作用域仅限于for循环。这意味着这个变量在循环外部是不可知或不可用的。
对于在for循环语句中声明的任何变量,通常都是这样工作的。这并不是什么需要做的事情,即使在 C 语言中这是完全合法的。为什么不是呢?因为这意味着你每次循环运行时都会声明一个变量,这并不真的聪明。最好是在循环外部声明一次,然后在循环内部使用它,无论目的(索引或要在语句内部使用的变量)是什么。
玩转增量
增量可以比仅使用增量运算符更复杂。
更复杂的增量
首先,我们可以在不写i++的情况下写i = i + 1。我们还可以使用其他类型的操作,如减法、乘法、除法、取模,或者它们的组合。想象一下,你只想打印奇数。奇数都是形如2n + 1的形式,其中n是整数。以下是打印从1到99的奇数的代码:
for (int i = 0 ; i < 50 ; i = 2 * i + 1) {
println(i);
}
i的初始值是:1、3、5,依此类推。
减量是负的增量
我只是想将之前的代码重新组合成其他的东西,以激发你对增量和减量的思考。下面是另一段代码,做同样的事情,但打印从99到1的奇数:
for (int i = 50 ; i > 0 ; i = 2 * i - 1) {
println(i);
}
好吧?让我们稍微复杂化一下。
使用嵌套for循环或两个索引
在for结构中也可以使用多个索引。想象一下,我们想要计算一个 10 x 10 的乘法表。我们必须定义两个从1到10的整数变量(0 是平凡的)。这两个索引必须从1到10变化。我们可以从一个带有索引x的循环开始:
for (int x = 1 ; x <= 10 ; x++) {
}
这是第一个索引的情况。第二个索引完全相同:
for (int y = 1 ; y <= 10 ; y++) {
}
我该如何混合这些?答案是和回答“乘法表是什么?”一样:我必须保持一个索引不变,然后将其与另一个从1到10的索引相乘。然后,我必须增加第一个索引,并继续用另一个索引重复同样的操作,依此类推。下面是如何做到这一点:
for (int x = 1 ; x <= 10 ; x++) {
for (int y = 1 ; y <= 10 ; y++) {
println(x*y);
}
}
这段代码打印了所有x*y的结果,其中x和y是从1到10的整数,每个结果占一行。以下是前几个步骤:
-
x = 1,y = 1… 打印结果 -
x = 1,y = 2… 打印结果 -
x = 1,y = 3… 打印结果
x每次内部for循环(带有y的那个)结束时都会增加至2,然后x固定为2,而y会增长直到x = 10和y = 10,此时for循环结束。
让我们稍微改进一下,只是为了美观。这也是一个调整和玩弄代码的理由,让你更熟悉它。通常,乘法表是这样绘制的:

乘法表的经典视图
每当其中一个索引(只有一个)达到限制值10时,我们需要转到下一行。
for (int x = 1 ; x <= 10 ; x++) {
for (int y = 1 ; y <= 10 ; y++) {
print(x*y);
}
println(); // add a carriage return & a new line
}
检查代码,每次y达到10时,就会创建一个新的行。for循环是一个强大的重复任务的结构。让我们检查另一个结构。
while循环结构
while循环结构稍微简单一些。以下是语法:
While (expression) {
// statements
}
表达式被评估为布尔值,true或false。当表达式为true时,执行语句,然后一旦它变为false,循环就会结束。显然,通常需要在while结构之外声明和定义。以下是一个例子,它产生了与我们的第一个for循环相同的结果,打印从 0 到 99 的所有整数:
int i = 0;
while (i < 100) {
println(i);
i++;
}
事实上,你必须在你的语句中明确处理增量或减量;我稍后会谈谈无限循环。我们可以通过这样做来进一步压缩代码:
int i = 0;
while (i < 100) {
println(i++); // print the current I value, then increment i
}
while循环结构在执行第一条语句之前测试表达式。让我们检查一个以不同方式执行类似结构的例子。
do…while 循环结构
do…while循环结构与while结构非常相似,但它在循环的末尾评估表达式,这意味着在执行语句之后。以下是语法:
do {
// statements
} while (expression);
这里是一个相同模型的例子:
int i = 0;
do {
println(i);
i++;
} while (i < 100);
这意味着即使表达式评估的第一个结果是false,语句也会按时执行。这与while结构不同。
打破循环
我们学习了如何创建由索引驱动的循环,这些索引精确地定义了这些循环将如何存在。但当我们遇到一个外部事件时,我们如何停止循环?外部是指循环本身及其索引之外。在这种情况下,循环的条件本身不会包含外部元素。
想象一下,我们在正常条件下运行一个过程 100 次。但我们要中断它,或者根据另一个具有更大作用域的变量(至少在循环外部声明)来修改它。
多亏了break语句,才使得这一点对我们来说成为可能。break;是基本语法。当执行break时,它会根据do、for和while退出当前循环。当我们在讨论switch条件结构时,你已经看到了break。让我们来举例说明。
想象一个 LED。我们希望它的强度从 0 增长到 100%,然后回到 0,每次都这样。但我们还希望使用一个很好的距离传感器,每次用户与传感器之间的距离大于某个值时,都会重置这个循环。
注意
这基于我为一个博物馆安装的真实系统,该系统必须使用户远离时 LED 平滑闪烁,当用户靠近时关闭 LED,就像一个活生生的系统在召唤用户来见面。
我设计得非常简单,如下所示:
for ( intensity = 0 ; intensity < 100 ; intensity++ ){
ledIntensity (intensity);
if (distance > maxDistance) { // if the user is far
intensity = 0; // switch off the LED
break; // exits the loop
}
}
整个循环都包含在 Arduino 板上的全局loop()函数中,并且每次loop()函数执行时,都会执行关于距离的完整测试,等待用户。
无限循环不是你的朋友
注意无限循环。问题其实不在于循环的无限状态,而在于一个系统,无论它是 Arduino 还是其他什么,如果运行了无限循环,就只能做那件事!循环之后的任何代码都无法执行,因为程序不会跳出循环。
如果你正确理解了我的意思,loop()——这是 Arduino 核心的基本函数——是一个无限循环。但它是一个设计良好的、基于 Arduino 核心的受控循环。当调用函数或发生其他特殊事件时,它可以(并且确实可以)被中断,让我们用户在这个循环内部设计我们需要的功能。我过去把它称为“事件驱动器和监听器”,因为这是我们的主程序运行的地方。
有很多方法可以创建无限循环的过程。你可以在 setup() 中定义一个变量,使其在 loop() 中增长,并在每次 loop() 运行时测试它,以便将其重置到初始值,例如。它利用了已经存在的 loop() 循环。以下是一个 Arduino 的 C 语言示例:
int i;
void setup(){
i = 0;
}
void loop(){
if (i < threshold) i +=1 ;
else i = 0;
// some statements
}
这个 i 从 0 增长到 threshold – 1,然后回到 0,再次增长,无限循环,利用了 loop() 函数。
也有其他方法可以在受控方式下无限运行循环,我们将在本书的更高级部分稍后看到,但你要注意:小心那些无限循环。
摘要
在这个重要的章节中,我们学到了很多抽象的概念。从类型到运算符的优先级,再到条件结构,现在我们将学习新的结构和语法,这将帮助我们编写更高效的代码块,尤其是更可重用的代码块。我们现在可以学习函数了。让我们深入下一章 C/C++知识,然后我们将在之后测试我们的 Arduino。
第四章。使用函数、数学和计时提高编程能力
作为一位数字艺术家,我需要特殊的条件才能工作。我们都需要自己的环境和氛围来提高生产力。即使我们每个人都有自己的方式,也有很多共同之处。
在这一章中,我想给你一些元素,这将使你更容易编写易于阅读、重用,尽可能美观的源代码。就像阴阳一样,对我来说,我的艺术和编程方面始终有一种禅意。这里我可以提供一些编程智慧之珠,以给你的创造性带来平静。
我们将要学习一些我们之前已经稍微使用过的一些内容:函数。它们同时有助于提高可读性和效率。在这个过程中,我们将涉及到许多项目中常用的数学和三角学。我们还将讨论一些计算优化的方法,并以 Arduino 固件中的与计时相关的事件或动作结束这一章。
在真正深入 Arduino 纯项目之前,这将是一个非常有趣的章节!
介绍函数
函数是一段通过名称定义的代码,可以在 C 程序中的许多不同点重用/执行。函数的名称在 C 程序中必须是唯一的。它也是全局的,这意味着,正如你之前已经读到的关于变量的内容,它可以在包含函数声明/定义的作用域内的 C 程序中的任何地方使用(参见第三章中的作用域概念部分,C 基础 - 使你更强壮)。
函数可能需要传递给它一些特殊元素;这些被称为参数。函数也可以产生并返回结果。
函数的结构
函数是一块代码,它有一个标题和一个主体。在标准 C 中,函数的声明和定义是分开进行的。函数的声明特别被称为函数原型的声明,并且必须在头文件中完成(参见第二章,C 语言初探)。
使用 Arduino IDE 创建函数原型
Arduino IDE 使我们的生活变得更简单;它为我们创建函数原型。但在特殊情况下,如果你需要声明函数原型,你可以在代码文件的开头进行声明。这提供了一种很好的源代码集中化的方式。
让我们用一个简单的例子来说明,我们想要创建一个函数,该函数将两个整数相加并产生/返回结果。有两个参数是整数类型的变量。在这种情况下,这两个int(整数)值的加法结果是另一个int值。这不必是,但在这个例子中是这样的。在这种情况下,原型将是:
int mySum(int m, int n);
函数的标题和名称
了解原型看起来像什么很有趣,因为它与我们所说的头相似。函数的头是其第一个语句定义。让我们通过编写我们的函数 mySum 的全局结构来进一步了解:
int mySum(int m, int n) // this row is the header
{
// between curly brackets sits the body
}
函数头具有以下全局形式:
returnType functionName(arguments)
returnType 是一个变量类型。到现在为止,我猜你更好地理解了 void 类型。在我们的函数不返回任何内容的情况下,我们必须通过选择 returnType 等于 void 来指定它。
functionName 必须选择易于记忆,并且应尽可能具有自描述性。想象一下支持其他人编写的代码。寻找 myNiceAndCoolMathFunction 需要研究。另一方面,mySum 是自解释的。你更愿意支持哪个代码示例?
Arduino 核心库(甚至 C 语言)遵循一个称为驼峰式命名的命名约定。两个单词之间的区别,因为我们不能在函数名中使用空白/空格字符,是通过将单词的首字母大写来实现的。这不是必需的,但如果你想在以后节省时间,则推荐这样做。它更容易阅读,并且使函数具有自解释性。
mysum 的可读性不如 mySum,对吧?参数是一系列变量声明。在我们的 mySum 示例中,我们创建了两个函数参数。但我们也可以有一个没有参数的函数。想象一下,你需要调用一个函数来执行一个始终相同的动作,不依赖于变量。你会这样写:
int myAction()
{
// between curly brackets sits the body
}
注意
在函数内部声明的变量只对其包含它们的函数是已知的。这就是所谓的“作用域”。在函数内部声明的此类变量在其他任何地方都无法访问,但它们可以被“传递”。可以传递的变量被称为参数。
函数的体和语句
如你可能直觉理解的那样,函数体是所有事情发生的地方;它是所有函数指令步骤构建的地方。
将函数体想象成一个真实、纯净且全新的代码块。你可以声明和定义变量,添加条件,并玩转循环。将函数体(指令)想象成雕塑家的粘土被塑形和塑造,最终以期望的效果呈现出来;可能是一块或几块,可能是相同的副本,等等。这是对现有事物的操作,但请记住:垃圾输入,垃圾输出!
你也可以,就像我们刚才介绍的,返回一个变量的值。让我们创建我们的 mySum 示例的函数体:
int mySum(int m, int n) // this row is the header
{
int result; // this is the variable to store the result
result = m + n; // it makes the sum and store it in result
return result; // it returns the value of result variable
}
int result; 声明了一个变量,并将其命名为 result。它的作用域与参数的作用域相同。result = m + n; 包含两个运算符,你已经知道 + 的优先级高于 =,这很好,因为数学运算首先进行,然后将结果存储在 result 变量中。这就是魔法发生的地方;取两个运算符,将它们中的一个变成函数。记住,在多个数学运算的组合中,不要忘记优先级顺序;这是至关重要的,以免得到意外的结果。
最后,return result; 是使函数调用返回值的语句。让我们通过一个实际的 Arduino 代码示例来更好地理解这一点:
void setup() {
Serial.begin(9600); Let's check an actual example of Arduino code to understand this better.
}
Void loop() {
// let's sum all integers from 0 to 99, 2 by 2 and display
int currentResult;
for (int i = 0 ; i < 100 ; i++)
{
currentResult = mySum(i,i+1); // sum and store
Serial.println(currentResult); // display to serial monitor
}
delay(2000); make a 2 second pause
}
int mySum(int m, int n) // this row is the header
{
int result; // this is the variable to store the result
result = m + n; // it makes the sum and store it in result
return result; // it returns the value of result variable
}
正如你所看到的,mySum 函数在示例中已经被定义并调用。最重要的语句是 currentResult = mySum(i,i+1);。当然,i 和 i+1 的技巧很有趣,但在这里要认识到的是在 loop() 函数开始处声明的变量 currentResult 的使用。
在编程中,重要的是要认识到右边的所有内容(内容)都进入左边(新容器)。根据优先级规则,函数调用相对于 = 赋值运算符的优先级为 2 对 16。这意味着调用首先进行,函数返回 + 操作的结果,正如我们设计的那样。从这个角度来看,你刚刚学到了一些非常重要的东西:函数返回值的调用语句是一个值。
你可以查看 附录 B,C 和 C++中的运算符优先级 以获取完整的优先级列表。与变量内的所有值一样,我们可以将其存储到另一个变量中,这里是在整数变量 result 中。
使用函数的好处
编程是关于为一般和特定目的编写代码片段。使用函数是分割你的代码的最佳方式之一。
更容易的编码和调试
函数真的可以帮助我们更好地组织。在设计程序时,我们经常使用伪代码,这也是我们注意到有很多常见语句的步骤。这些常见语句通常可以放在函数中。
函数/调用模式也更容易调试。函数所在的部分只有一段代码。如果有问题,我们只需调试一次函数本身,然后所有的调用都会被修复,而不是修改整个代码部分。

函数使你的代码更容易调试
更好的模块化有助于重用性
你的一些代码将是高级和通用的。例如,在某个时候,你可能需要一系列可以切割数组并按照基本规则重新组合所有值的语句。这个系列可以是函数的主体。另一种方式是编写一个将华氏单位转换为摄氏单位的函数可能对你感兴趣。这两个例子都是通用函数。
相反,你也可以有一个特定功能的唯一目的是将美元转换为法国法郎。你可能不会经常调用它,但如果偶尔需要,它总是准备好处理这个任务。
在这两种情况下,函数都可以使用,当然也可以重用。背后的想法是节省时间。这也意味着你可以抓取一些已经存在的函数并重用它们。当然,这必须遵循一些原则,例如:
-
代码许可
-
尊重可以作为库一部分的函数的 API
-
与你的目的相匹配
代码许可问题是一个重要点。我们习惯于抓取、测试和复制粘贴东西,但你找到的代码并不总是属于公共领域。你必须注意通常包含在代码发布存档中的许可文件,以及在代码的第一行,其中注释可以帮助你理解尊重其再使用的条件。
应用程序编程接口(API)意味着在使用与该 API 相关的材料之前,你必须遵守一些文档。我理解纯粹主义者可能会认为这是一种轻微的滥用,但这是一种相当实用主义的定义。
基本上,一个 API 定义了可以在其他程序内部重用的例程、数据结构和其他代码实体的规范。API 规范可以是库的文档。在这种情况下,它将精确地定义你可以做什么,不可以做什么。
良好的匹配原则可能看起来很明显,但有时出于方便,我们会找到一个现有的库并选择使用它而不是编写自己的解决方案。不幸的是,有时最终我们只是增加了比最初打算更多的复杂性。自己来做可能满足简单的需求,并且肯定会避免更全面解决方案的复杂性和特殊性。还有避免潜在的性能损失;当你真正需要的是走到街角的超市时,你不会买一辆豪华轿车。
更好的可读性
这是其他益处的结果,但我希望让你明白这一点比注释你的代码更为重要。更好的可读性意味着节省时间来专注于其他事情。这也意味着更容易进行代码升级和改进步骤。
C 标准数学函数和 Arduino
正如我们已经看到的,几乎所有的由编译器avr-g++支持的 C 和 C++标准实体都应该与 Arduino 兼容。这也适用于 C 数学函数。
这组函数是(著名的)C 标准库的一部分。这个组中的许多函数在 C++中被继承。C 和 C++在复数的使用上存在一些差异。C++不从这个库中提供复数处理,而是通过其自己的 C++标准库,使用类模板 std::complex 来提供。
几乎所有这些函数都是为了与浮点数一起工作并对其进行操作而设计的。在标准 C 中,这个库被称为 math.h(一个文件名),我们在 C 程序的头部提到它,这样我们就可以使用它的函数。
Arduino 核心中的三角函数
我们经常需要进行一些三角计算,从确定物体移动的距离,到角速度,以及许多其他现实世界的属性。有时,你需要在 Arduino 本身内部做这些计算,因为你会将其用作一个没有附近任何计算机的自主智能单元。
Arduino 核心提供了经典的三角函数,可以通过编写它们的原型来总结。这些函数中的大部分以弧度返回结果。让我们先简要回顾一下我们的三角学!
一些先决条件
我保证,我会快速且简洁。但以下这些文本将节省你寻找你那本旧且破旧的学校课本的时间。当我从特定领域学习知识时,我特别喜欢把所有需要的东西都放在手边。
弧度和度数之间的区别
弧度是许多三角函数使用的单位。然后,我们必须清楚弧度和度数,尤其是如何将一个转换为另一个。以下是官方的弧度定义:Alpha是两个距离之间的比率,并以弧度单位表示。

弧度定义
度是一个完整旋转的 1/360(完整圆)。考虑到这两个定义以及一个完整旋转等于 2π的事实,我们可以将一个转换为另一个:
注意
angleradian = angledegree x π/180
angledegree = angleradian x 180/π
余弦、正弦和正切
让我们看看三角形的例子:

考虑到以弧度为角度 A,我们可以定义余弦、正弦和正切如下:
-
cos(A) = b/h
-
sin(A) = a/h
-
tan(A) = sin(A)/cos(A) = a/b
余弦和正弦在弧度角度的值为-1 到 1 之间演变,而正切有一些特殊点,在这些点上它没有定义,然后周期性地从-∞演变到+∞。我们可以如下在同一张图上表示它们:

图形余弦、正弦和正切表示
当然,这些函数会振荡,无限地复制相同的演变。记住它们不仅可以用于纯计算,还可以通过用更平滑的振荡代替线性值演变来避免时间上的过度线性演变。我们稍后会看到这一点。
当我们有一个角度时,我们知道如何计算余弦/正弦/正切,但当我们已经有了余弦/正弦/正切时,如何计算角度呢?
反余弦、反正弦和反正切
反余弦、反正弦和反正切被称为反三角函数。这些函数用于计算角度,当你已经有了之前提到的距离比时。
它们被称为反函数,因为这是之前看到的三角函数的逆/倒数过程。基本上,这些函数为你提供一个角度,但考虑到周期性,它们提供了很多角度。如果 k 是整数,我们可以写成:
-
sin (A) = x ó A = arcsin(x) + 2kπ 或 y = π – arcsin(x) + 2kπ
-
cos (A) = x ó A = arccos(x) + 2kπ 或 y = 2π – arccos (x) + 2kπ
-
tan (A) = x ó A = arctan(x) + kπ
这些是正确的数学关系。实际上,在通常情况下,我们可以忽略完整的旋转情况,并忘记余弦和正弦函数的 2kπ 以及正切函数的 kπ。
三角函数
Math.h 包含三角函数的原型,Arduino 核心也是如此:
-
double cos (double x);返回x弧度的余弦值 -
double sin (double x);返回x弧度的正弦值 -
double tan (double x);返回x弧度的正切值 -
double acos (double x);返回 A,对应于 cos (A) =x的角度 -
double asin (double x);返回 A,对应于 sin (A) =x的角度 -
double atan (double x);返回 A,对应于 tan (A) =x的角度 -
double atan2 (double y, double x);返回 arctan (y/x)
指数函数和一些其他函数
进行计算,即使是基本的计算,也涉及其他类型的数学函数,例如幂、绝对值等。Arduino 核心随后实现了这些函数。以下是一些数学函数的示例:
-
double pow (double x, double y);返回x的y次幂 -
double exp (double x);返回x的指数值 -
double log (double x);返回x的自然对数,其中x> 0 -
double log10 (double x);返回x以 10 为底的对数,其中x> 0 -
double square (double x);返回x的平方 -
double sqrt (double x);返回x的平方根,其中x>= 0 -
double fabs (double x);返回x的绝对值
当然,数学规则,特别是考虑到值的范围,必须得到尊重。这就是为什么我在列表中添加了一些关于 x 的条件。
所有这些函数都非常有用,即使是解决小问题。有一天,我在一个研讨会上教某人,不得不解释如何使用传感器测量温度。这位学生相当有动力,但她不了解这些函数,因为她只是玩输入和输出,没有进行任何转换(因为她基本上不需要那样做)。然后我们学习了这些函数,她甚至优化了自己的固件,这让我为她感到非常自豪!
现在,让我们探讨一些优化方法。
接近计算优化
这一部分是一个方法。这意味着它不包含所有高级编程优化的技巧,但包含纯计算的优化。
通常,我们设计一个想法,编写一个程序,然后优化它。对于大型程序来说,这很正常。对于较小的程序,我们可以在编码时进行优化。
注意
通常,我们的固件很小,所以我建议你考虑以下新规则:编写每个语句时都要考虑到优化。
我现在可以添加一些其他内容:不要用太多的神秘优化方案破坏你代码的可读性;我在写那的时候想到了指针。我会添加几行关于它们的介绍,以便让你至少熟悉这个概念。
位移运算的幂
如果我考虑一个数组来存储东西,我几乎总是选择 2 的幂作为大小。为什么?因为编译器,而不是通过使用 CPU 密集型的乘法操作来进行数组索引,可以使用更高效的位移操作。
位操作是什么?
你们中的一些人可能已经理解了我的工作方式;我使用了很多借口来教你们新东西。位运算符是针对位的具体运算符。有些情况下需要这种计算。我可以引用两个我们将在本书下一部分学习的情况:
-
使用移位寄存器进行复用
-
执行涉及乘法和除法运算符的 2 的幂的算术运算
有四个运算符和两个位移运算符。在我们深入之前,让我们更多地了解二进制数制。
二进制数制
我们习惯于使用十进制系统进行计数,也称为十进制数制或基-10 数制。在这个系统中,我们可以这样计数:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12...
二进制数制是计算机和数字电子设备底层使用的系统。它也被称为基-2 系统。在这个系统中,我们这样计数:
0, 1, 10, 11, 100, 101, 110, 111...
将二进制数轻松转换为十进制数
将二进制转换为十进制的一个小技巧,从索引 0 开始,先计算 0 和 1 的位置。
让我们以 110101 为例。它可以表示如下:
| 位置 | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 1 | 0 | 1 | 0 | 1 |
然后,我可以写出这个乘积之和,它等于我的 110101 数字的十进制版本:
1 x 20 + 0 x 21 + 1 x 22 + 0 x 23 + 1 x 24 + 1 x 25 = 1 + 4 + 16 + 32 = 53
每一位 决定 我们是否需要考虑 2 的幂,考虑到其位置。
与(AND)、或(OR)、异或(XOR)和非(NOT)运算符
让我们来看看这四个运算符。
与(AND)
按位与运算符用单个与号(&)表示。这个运算符根据以下规则独立地对每个位位置进行操作:
-
0
&0 == 0 -
0
&1 == 0 -
1
&0 == 0 -
1
&1 == 1
让我们用一个实际的整数例子来说明,整数是一个 16 位的值:
int a = 35; // in binary: 00000000 00100011
int b = 49; // in binary: 00000000 00110001
int c = a & b; // in binary: 00000000 00100001 and in decimal 33
为了容易找到结果,我们必须遵循前面的规则,逐位比较每个位置的每个位。
OR
按位或运算符用单个竖线表示:|。在 OSX 上,可以通过按 Alt + Shift + l(字母 L)来实现,在其他 PC 键盘上则是 Shift + **。这个运算符根据以下规则独立地对每个位位置进行操作:
-
0
|0 == 0 -
0
|1 == 1 -
1
|0 == 1 -
1
|1 == 1
XOR
按位异或运算符用单个撇号符号表示:^。这个运算符根据以下规则独立地对每个位位置进行操作:
-
0
^0 == 0 -
0
^1 == 1 -
1
^0 == 1 -
1
^1 == 0
这是 XOR 的排他版本,因此得名 XOR。
NOT
按位异或运算符用波浪线符号表示:~。它是一个一元运算符,也就是说,如果你正确地记住这个术语,它只能应用于一个数字。我在我的研讨会上称它为位变换器。它将每个位变换为其相反数:
-
~0 == 1 -
~1 == 0
让我们用一个实际的整数例子来说明,正如你所知,整数是 16 位值:
int a = 35; // in binary: 00000000 00100011
int b = ~a ; // in binary: 11111111 11011100 and in decimal -36
如你所知,C 语言中的 int 类型是一个有符号类型(第三章
{
if (valueToTest == 1)
{
int temporaryVariable;
// some calculations with temporaryVariable
return temporaryVariable;
}
else {
return -1;
}
}
`temporaryVariable`只在一种情况下需要,即`valueToTest`等于`1`。如果我在`if`语句之外声明`temporaryVariable`,无论`valueToTest`的值如何,`temporaryVariable`都会被创建。
在我引用的例子中,我们节省了内存和处理时间;在所有`valueToTest`不等于`1`的情况下,变量`temporaryVariable`甚至没有被创建。
### 注意
为所有变量使用可能的最小作用域。
## 返回的道
函数通常是根据特定的想法设计的,它们是能够通过包含的语句执行特定操作的代码模块,并且也能够返回一个结果。这个概念提供了一个很好的方法,在我们不在函数内部时忘记函数内部执行的所有特定操作。我们知道函数被设计成当我们给它提供参数时提供结果。
再次强调,这是一种关注程序核心的好方法。
### 直接返回的概念
正如你可能已经理解的,声明一个变量会在内存中创建一个位置。当然,这个位置不能被其他东西使用。创建变量的过程会消耗处理器时间。让我们更详细地看看之前的例子:
```cpp
int myFunction( int valueToTest )
{
if (valueToTest == 1)
{
int temporaryVariable;
temporaryVariable += globalVariable;
temporaryVariable *= 7;
return temporaryVariable;
}
else {
return -1;
}
}
我能做些什么来尝试避免使用temporaryVariable?我可以进行如下直接的返回:
int myFunction( int valueToTest )
{
if (valueToTest == 1)
{
return ( (globalVariable + 1)*7 );
}
else {
return -1;
}
}
在更长的版本中:
-
我们在
valueToTest == 1的案例中,因此valueToTest等于1 -
我直接在
return语句中放入计算
在那种情况下,不再需要创建临时变量。有些情况下,写很多临时变量可能更易于阅读。但现在,你已经意识到在可读性和效率之间找到折衷是值得的。
注意
使用直接返回而不是很多临时变量。
如果不需要返回值,请使用 void
我经常阅读包含没有返回值的函数返回类型的代码。编译器可能会警告你这一点。但如果没有警告,你必须注意这个问题。调用一个提供返回类型的函数时,总是会传递返回值,即使函数体内实际上没有返回任何内容。这会产生 CPU 开销。
注意
如果你的函数不返回任何内容,请使用void作为返回类型。
查找表的秘密
查找表是编程宇宙中最强大的技巧之一。它们是包含预先计算值的数组,因此通过简单的数组索引操作替换了复杂的运行时计算。例如,想象一下你想通过读取来自一组距离传感器的距离来跟踪某个东西的位置。你将需要进行三角和可能幂的计算。由于这些计算可能会消耗处理器的时间,使用数组内容读取而不是这些计算会更聪明、更经济。这是查找表使用的通常说明。
这些查找表可以预先计算并存储在静态程序的存储内存中,或者计算在程序的初始化阶段(在这种情况下,我们称它们为预取查找表)。
有些函数在 CPU 工作方面特别昂贵。三角函数就是这样一种函数,在嵌入式系统中,由于存储空间和内存有限,它们可能会产生不良后果。它们通常在代码中被预取。让我们看看我们如何做到这一点。
表初始化
我们必须预先计算余弦查找表(LUT)。我们需要创建一个小的精度系统。在调用 cos(x)时,我们可以拥有我们想要的任何 x 值。但如果我们想在具有设计上有限大小的数组中预取值,我们必须计算有限数量的值。然后,我们不能为所有浮点值计算余弦(x)的结果,而只能为计算出的那些值。
我认为精度是 0.5 度的角度。这意味着,例如,在我们系统中,45 度的余弦值将等于 45 度 4 分钟的余弦值。这是合理的。
让我们考虑 Arduino 代码。你可以在Chapter04/CosLUT/文件夹中找到这段代码:
float cosLUT[(int) (360.0 * 1 / 0.5)] ;
const float DEG2RAD = 180 / PI ;
const float cosinePrecision = 0.5;
const int cosinePeriod = (int) (360.0 * 1 / cosinePrecision);
void setup()
{
initCosineLUT();
}
void loop()
{
// nothing for now!
}
void initCosineLUT(){
for (int i = 0 ; i < cosinePeriod ; i++)
{
cosLUT[i] = (float) cos(i * DEG2RAD * cosinePrecision);
}
}
cosLUT被声明为一个特殊大小的float类型数组。360 * 1/(精度,以度为单位)就是我们数组中需要的元素数量。在这里,精度是 0.5 度,当然,声明可以简化如下:
float cosLUT[720];
我们还声明并定义了一个 DEG2RAD 常量,它有助于将度转换为弧度。我们声明了 cosinePrecision 和 cosinePeriod 以执行这些计算一次。
然后,我们定义了一个 initCosineLUT() 函数,它在 setup() 函数内部执行预计算。在其主体中,我们可以看到一个从 i=0 到数组大小的循环。这个循环预先计算了从 0 到 2π 的所有 x 值的余弦(x)值。我明确地将 x 写成 i * DEG2RAD * precision,以便保持精度可见。
在板初始化时,它计算所有查找表值一次,并通过简单的数组索引操作提供这些值以供进一步计算。
用数组索引操作替换纯计算
现在,让我们检索我们的余弦值。我们可以通过访问另一个函数来轻松检索我们的值,如下所示:
float myFastCosine(float angle){
return cosLUT[(int) (angle * 1 / cosinePrecision) % cosinePeriod];
}
angle * 1 / cosinePrecision 给出了考虑给定精度的 LUT 的角度。我们应用一个考虑 cosinePeriod 值的模运算,将更高角度的值包装到 LUT 的限制内,我们得到了索引。我们直接返回与我们的索引对应的数组值。
我们也可以使用这种技术进行平方根预取。这是我使用另一种语言在编写我的第一个名为 digital collisions 的 iOS 应用程序时使用的方法(julienbayle.net/blog/2012/04/07/digital-collisions-1-1-new-features)。如果你没有测试过,这是一个基于物理碰撞算法的生成音乐和视觉应用程序。我需要进行大量的距离和旋转计算。相信我,这种技术将第一个缓慢的原型变成了一个快速的应用程序。
泰勒级数展开技巧
有一种节省 CPU 工作量的好方法,这需要一些数学知识。我的意思是,稍微高级一点的数学。以下内容非常简化。但确实,我们需要关注事情中的 C 部分,而不是完全关注数学。
泰勒级数展开是一种通过使用多项式表达式来近似特定点(及其周围)的几乎每个数学表达式的方法。
注意
多项式表达式类似于以下表达式:
P(x) = a + bx + cx² + dx³
P(x)是一个三次多项式函数。a, b, c 和 d 是浮点数。
泰勒级数的理念是,我们可以通过使用表示该表达式的理论无限和的第一个项来近似一个表达式。让我们举一些例子。
例如,考虑 x 从 -π 到 π 的变化;我们可以将正弦函数写成以下形式:
sin(x) ≈ x - x³/6 + x⁵/120 - x⁷/5040
符号 ≈ 表示“约等于”。在合理范围内,我们可以用 x - x³/6 + x⁵/120 - x⁷/5040 替换 sin(x)。这没有魔法,只是数学定理。我们也可以将 x 从 -2 到 3 的变化写成以下形式:
ex ≈ 1 + x + x²/2 + x³/6 + x⁴/24
我可以在这里添加一些其他示例,但你将在 附录 D,一些有用的泰勒级数用于计算优化 中找到这些。这些技术是一些节省 CPU 时间的强大技巧。
Arduino 核心甚至提供了指针
指针是 C 编程初学者的更复杂的技术,但我希望你能理解这个概念。它们不是数据,而是指向数据起始点的指针。至少有两种方法可以将数据传递给一个函数或其他东西:
-
复制并传递它
-
向它传递一个指针
在第一种情况下,如果数据量太大,我们的内存堆栈就会爆炸,因为整个数据都会复制在堆栈中。我们除了指针传递外别无选择。
在这种情况下,我们有数据存储在内存中的位置的引用。我们可以按照我们想要的任何方式操作,但只能通过使用指针。指针是处理任何类型数据,尤其是数组的智能方式。
时间测量
时间总是测量和处理的有意思的东西,尤其是在嵌入式软件中,这显然是我们在这里的主要目的。Arduino 核心包括几个我将要讨论的时间函数。
还有一个命名得非常巧妙的库,名为 SimpleTimer Library,由 Marcello Romani 设计,作为一个 GNU LGPL 2.1 + 库。这是一个基于 millis() 核心函数的好库,这意味着最大分辨率是 1 毫秒。这对你未来 99% 的项目来说将绰绰有余。Marcello 甚至为这本书制作了一个基于 micros() 的特殊版本库。
Arduino 核心库现在也包括一个能够达到 8 微秒分辨率的本地函数,这意味着你可以测量 1/8,000,000 秒的时间差;非常精确,不是吗?
我还会在书的最后一章描述一个更高分辨率的库 FlexiTimer2。它将提供一个高分辨率、可定制的计时器。
Arduino 板子有自己的手表吗?
Arduino 板芯片提供其 运行时间。运行时间 是自板子启动以来的时间。这意味着你无法在不保持板子开启和供电的情况下,以原生方式存储绝对时间和日期。此外,它将要求你设置一次绝对时间,然后保持 Arduino 板供电。可以自主地为板子供电。我会在本书的后面部分讨论这一点。
millis() 函数
核心函数 millis() 返回自板子上次启动以来的毫秒数。为了你的信息,1 毫秒等于 1/1000 秒。
Arduino 核心文档还提供,这个数值在大约 50 天后会回到零(这被称为计时器溢出)。现在你可以笑了,但想象一下你的最新安装艺术性地在纽约市的 MoMA 中阐释时间概念,50 天后会完全混乱。你肯定会对这个信息感兴趣,不是吗?millis()的返回格式是unsigned long。
这里是一个你将在接下来的几分钟内上传到板上的示例。你还可以在Chapter04/measuringUptime/文件夹中找到这段代码:
/*
measuringTime is a small program measuring the uptime and printing it
to the serial monitor each 250ms in order not to be too verbose.
Written by Julien Bayle, this example code is under Creative Commons CC-BY-SA
This code is related to the book "C programming for Arduino" written by Julien Bayle
and published by Packt Publishing.
http://cprogrammingforarduino.com
*/
unsigned long measuredTime; // store the uptime
void setup(){
Serial.begin(9600);
}
void loop(){
Serial.print("Time: ");
measuredTime = millis();
Serial.println(measuredTime); // prints the current uptime
delay(250); // pausing the program 250ms
}
你能优化这个(仅出于教学目的,因为这个程序非常小)吗?是的,我们确实可以避免使用measuredTime变量。它看起来会更像这样:
/*
measuringTime is a small program measuring the uptime and printing it
to the serial monitor each 250ms in order not to be too verbose.
Written by Julien Bayle, this example code is under Creative Commons CC-BY-SA
This code is related to the book "C programming for Arduino" written by Julien Bayle
and published by Packt Publishing.
http://cprogrammingforarduino.com
*/
void setup(){
Serial.begin(9600);
}
void loop(){
Serial.print("Time: ");
Serial.println(millis()); // prints the current uptime
delay(250); // pausing the program 250ms
}
它的简单性也很美,不是吗?我相信你会同意的。所以将这段代码上传到你的板上,启动串行监视器,看看它。
micros()函数
如果你需要更高的精度,可以使用micros()函数。它提供与之前所述的 8 微秒精度相同的工作时间,但大约有 70 分钟的溢出(远小于 50 天,对吧?)。我们获得了精度,但失去了溢出时间范围。你还可以在Chapter04/measuringUptimeMicros/文件夹中找到以下代码:
/*
measuringTimeMicros is a small program measuring the uptime in ms and
µs and printing it to the serial monitor each 250ms in order not to be too verbose.
Written by Julien Bayle, this example code is under Creative Commons CC-BY-SA
This code is related to the book «C programming for Arduino» written by Julien Bayle
and published by Packt Publishing.
http://cprogrammingforarduino.com
*/
void setup(){
Serial.begin(9600);
}
void loop(){
Serial.print(«Time in ms: «);
Serial.println(millis()); // prints the current uptime in ms
Serial.print(«Time in µs: «);
Serial.println(micros()); // prints the current uptime in µs
delay(250); // pausing the program 250ms
}
上传并检查串行监视器。
延迟概念和程序流程
就像那位连自己说话都是散文的布尔乔亚绅士一样,你已经使用了delay()核心函数,却并未意识到。在loop()函数中,可以通过直接使用delay()和delayMicroseconds()函数来延迟 Arduino 程序。
这两个函数都会使程序暂停。唯一的区别是,你必须为delay()提供一个毫秒数,为delayMicroseconds()提供一个微秒数。
程序在延迟期间做什么?
什么也不做。它等待。这个子子节并不是玩笑。我希望你能专注于这个特定的点,因为稍后它将非常重要。
注意
当你在程序中调用delay或delayMicroseconds时,它会停止执行一段时间。
这里有一个小图解,说明了当我们打开 Arduino 时会发生什么:

Arduino 固件的一个生命周期
现在是一个固件执行的图解,这是我们将在下一行中工作的部分:

固件生命周期中的主要部分循环
接受这样一个事实:当setup()停止时,loop()函数开始循环,loop()中的所有内容都是连续的。现在看看当出现延迟时的情况:

当调用delay()时,固件的主要部分循环,并中断
当调用 delay() 时,整个程序会中断。中断的长度取决于传递给 delay() 的参数。
我们可以注意到,所有事情都是按顺序和按时完成的。如果一条语句执行需要很长时间,Arduino 的芯片会先执行它,然后继续下一个任务。
在那种非常常见和普遍的情况下,如果某个特定任务(语句、函数调用或任何其他)需要很长时间,整个程序可能会挂起并产生中断;考虑用户体验。
想象一下这样一个具体案例,你需要在同一时间读取传感器,切换一些开关,并将信息写入显示屏。如果你按顺序这样做,并且你有大量的传感器,这是相当常见的,那么在 loop() 中执行这个任务会在其他任务之后,你可能会在信息显示上出现一些延迟和减速。

一个忙于许多输入和输出的 Arduino 板
我通常至少向我的学生传授两个处理这种单一任务属性(可能会感觉像是一种限制)的概念:
-
线程
-
中断处理程序(以及随后的中断服务例程概念)
我显然还教授另一个:轮询。轮询是一种特殊的中断情况,我们将从这里开始。
投票概念 – 一种特殊的中断情况
你知道轮询这个词。我可以将其总结为“询问,等待答案,并将其保存在某处”。
如果我想创建一个读取输入并当这些输入的值满足特定条件时执行某些操作的代码,我会编写以下伪代码:
setup()
- initialize things
loop()
- ask an input value and wait for it until it is available
- test this input according to something else
- if the test is true perform something else, loop to the beginning
这里可能有什么令人烦恼的地方?我循环地轮询新信息,并必须等待它。
在这个步骤中,没有做更多的事情,但想象一下输入值在很长时间内保持不变。我会在循环中周期性地请求这个值,将其他任务约束为等待。
这听起来像是一种浪费时间的行为。通常,轮询是完全足够的。它必须在这里写,而不是其他原始程序员可能会告诉你的内容。
我们是创造者,我们需要让事物相互沟通和运作,我们可以并且喜欢测试,不是吗?那么,你在这里就学到了一些重要的东西。
注意
在测试基本解决方案之前,不要设计复杂的程序解决方案。
有一天,我要求一些人设计基本代码。当然,像往常一样,他们连接到了互联网,我只是同意了,因为我们几乎所有人今天都在这样做,对吧?有些人比其他人先完成。
为什么呢?很多后来完成的人试图使用消息系统和外部库构建一个漂亮的多线程解决方案。他们的意图是好的,但在我们拥有的时间里,他们没有完成,只有一块漂亮的 Arduino 板,一些有线组件,以及一些在桌子上无法工作的代码。
你想知道其他人桌面上的内容吗?一个基于投票的例行程序,它完美地驱动着他们的电路!考虑到电路,这种基于投票的固件浪费的时间完全不重要。
注意
考虑到核心优化,但首先测试你的基本代码。
中断处理程序的概念
轮询很好,但有点耗时,正如我们刚才发现的。最好的方法是有能力以更智能的方式控制处理器何时需要处理输入或输出。
想象一下我们之前绘制的具有许多输入和输出的例子。也许,这是一个必须根据用户操作做出反应的系统。通常,我们可以认为用户输入的速度比系统的响应能力慢得多。
这意味着我们可以创建一个系统,当特定事件发生时,如用户输入,就会中断显示。这个概念被称为基于事件的中断系统。
中断是一个信号。当特定事件发生时,会向处理器发送一个中断消息。有时它被发送到处理器外部(硬件中断),有时是内部(软件中断)。
这就是磁盘控制器或任何外部外围设备如何通知主单元处理器在正确的时间提供这个或那个信息。
中断处理程序是一种通过执行某些操作来处理中断的例程。例如,当鼠标移动时,计算机操作系统(通常称为 OS)必须在另一个位置重新绘制光标。让处理器本身每毫秒都测试鼠标是否移动,这将是疯狂的,因为 CPU 将运行在 100%的利用率。似乎有一个专门的硬件部分来做这件事更明智。当鼠标移动发生时,它会向处理器发送一个中断,然后处理器会重新绘制鼠标。
在我们安装了大量的输入和输出的情况下,我们可以考虑使用中断来处理用户输入。我们不得不实现所谓的中断服务例程(ISR),这是一个仅在物理世界事件发生时调用的例程,即当传感器值改变或类似情况发生时。
Arduino 现在提供了一种将中断附加到函数的好方法,现在设计 ISR(即使我们稍后会学习如何做)变得容易。例如,我们现在可以使用 ISR 来响应模拟热传感器的值变化。在这种情况下,我们不会永久性地轮询模拟输入,而是让我们的低级 Arduino 部分来做。只有当值根据我们如何附加中断而变化(上升或下降)时,这才会作为触发器,并执行一个特殊函数(例如,LCD 显示屏更新为新值)。
轮询、ISR,现在,我们将引入线程。请稍等!
线程是什么?
线程是处理器执行一系列任务(通常循环,但不一定)的运行程序流程。
只有一个处理器时,通常是通过时分复用来完成的,这意味着处理器根据时间在不同的线程之间切换,即上下文切换。

时分复用提供了多任务处理
更先进的处理器提供了多线程功能。它们表现得好像它们不仅仅是单个处理器,每个部分同时处理一个任务。

真正的多线程提供了同时发生的任务
由于我们现在没有处理计算机处理器,所以不深入探讨计算机处理器,我可以告诉你,线程是在编程中用来使任务同时运行的不错的技术。
不幸的是,Arduino 核心不提供多线程,其他任何微控制器也不提供。因为 Arduino 是一个开源硬件项目,一些黑客已经设计了一种 Arduino 板的变体,并创建了一些 Freeduino 变体,提供并发,一个开源编程语言,以及一个特别为多线程设计的环境。这超出了我们的话题,但至少,如果你对此感兴趣,你现在有一些线索。
如果需要,让我们转向第二个解决方案,以超越一次只处理一个任务的限制。
一个现实生活中的轮询库示例
如本节第一行所述,Marcello 的库是一个非常不错的库。它提供了一种基于轮询的方式来启动定时动作。
这些动作通常是函数调用。表现像这样的函数有时被称为回调函数。这些函数通常作为另一个代码片段的参数被调用。
假设我想让 Arduino 板上的宝贵 LED 每 120 毫秒闪烁一次。我可以使用延迟,但这将完全停止程序。不够聪明。
我可以在板上黑客一个硬件定时器,但这将是过度杀鸡用牛刀。一个更实用的解决方案是我会使用 Marcello 的SimpleTimer库中的回调函数。轮询提供了一种简单且经济的方式(从计算的角度来看)来处理非定时器依赖的应用程序,同时避免了使用中断,这会引发更复杂的问题,如硬件定时器过度消耗(劫持),这会导致其他复杂因素。
然而,如果你想要每 5 毫秒调用一个函数,而这个函数需要 9 毫秒才能完成,它将每 9 毫秒被调用一次。在我们的例子中,需要 120 毫秒来产生一个既美观又对眼睛友好的可见闪烁,我们非常安全。
仅供参考,你不需要在板子和你的电脑之间连接任何超过 USB 电缆的东西。Arduino 板上焊接的 LED 连接到了数字引脚 13。让我们使用它。
但首先,让我们下载SimpleTimer库,以便你第一次使用外部库。
安装外部库
从playground.arduino.cc/Code/SimpleTimer下载它,并在你的电脑上的某个位置解压。你通常会看到一个包含至少两个文件的文件夹:
-
头文件(
.h扩展名) -
源代码文件(
.cpp扩展名)
现在,你可以亲自看看它们是什么。在这些文件中,你有源代码。打开你的草图簿文件夹(见第一章, 让我们连接东西),如果存在,将库文件夹移动到libraries文件夹中,否则创建这个特殊的文件夹:

Marcello Romani 编写的 SimpleTimer 的头文件和源代码
下次你启动 Arduino IDE 时,如果你去草图 | 导入库,你会在底部看到一个新库。

为了包含一个库,你可以点击菜单中的它,它将在你的代码中写入#include <libname.h>。你也可以自己输入这个。
让我们来测试一下代码
上传此代码并重新启动 Arduino;我将解释它是如何工作的。你也可以在Chapter04/simpleTimerBlinker/文件夹中找到此代码:
#include <SimpleTimer.h> // include the Marcello's library
SimpleTimer timer ; // the timer object construction
boolean currentLEDState ;
int ledPin = 13 ;
void setup() {
currentLEDState = false ;
pinMode(ledPin, OUTPUT) ;
timer.setInterval(120, blink) ;
}
void loop() {
timer.run() ;
}
// a function to be executed periodically
void blink() {
if (!currentLEDState) digitalWrite(ledPin, HIGH);
else digitalWrite(ledPin, LOW);
currentLEDState = !currentLEDState ; // invert the boolean
}
在我们的案例中,这个库很容易使用。当然,你首先必须包含它。然后,你必须通过声明来创建SimpleTimer的实例,这是一个对象构造。
然后,我使用一个currentLEDState布尔值来显式存储 LED 的当前状态。最后,我声明/定义ledPin为所需的引脚号(在这种情况下,13)以使 LED 闪烁。setup()基本上是一些初始化。这里最重要的是timer.setInterval()函数。
也许,这是你的第一次方法调用。对象 timer 包含一些我们可以使用的方法。其中之一是setInterval,它接受两个变量:
-
一个时间间隔
-
回调函数
我们在这里传递一个函数名(一段代码)给另一段代码。这是典型回调系统的结构。
loop()是通过在每次运行时调用计时器对象的run()方法来设计的。这是使用它的必要条件。至少,回调函数blink()在最后使用了一个小技巧。
比较很明显。我测试 LED 的当前状态,如果它已经开启,我就将其关闭,否则将其开启。然后,我反转状态,这就是技巧。我正在使用!(非)一元运算符在这个布尔变量上以翻转其值,并将反转后的值赋给布尔变量本身。我本可以这样写:
void blink() {
if (!currentLEDState) {
digitalWrite(ledPin, HIGH);
currentLEDState = true ;
}
else {
digitalWrite(ledPin, LOW);
currentLEDState = false;
}
}
实际上,无论是哪种方式,都没有性能提升。这只是一个个人决定;使用你喜欢的任何一种。
我个人认为翻转是一个必须每次都做的通用动作,与状态无关。这就是为什么我建议你将其放在测试结构之外的原因。
摘要
这完成了本书的第一部分。我希望你已经能够吸收并享受这些(庞大)的第一步。如果还没有,你可能想要花些时间去回顾一下你可能不太清楚的地方;更好地理解你所做的事情总是值得的。
我们对 C 和 C++编程了解得更多一些,至少足够让我们安全地通过接下来的两部分。我们现在可以理解 Arduino 的基本任务,我们可以上传我们的固件,并且可以用基本的接线来测试它们。
现在,我们将进一步深入到一个更加实用、理论较少的领域。准备好去探索新的物理世界,在那里你可以让事物发声,相互交流,你的电脑将能够对你的感受和反应做出回应,有时甚至不需要电线!再次提醒,你可能想要花一点时间去回顾一下你可能仍然有些模糊的地方;知识就是力量。
未来已经到来!
第五章. 使用数字输入进行感知
Arduino 板具有输入和输出。实际上,这也是这个平台的一个优势:直接提供连接 ATMega 芯片组引脚的引脚。然后我们可以直接将输入或输出连接到任何其他外部组件或电路,而无需焊接。
如果你需要,我在这里提醒你一些要点:
-
Arduino 具有数字和模拟输入
-
Arduino 具有也可以用于模拟输出的数字输出
我们将在本章中讨论数字输入。
我们将学习关于感知世界的全局概念。我们将遇到一个名为Processing的新伙伴,因为它以图形化的方式可视化和说明我们将要做的一切,这是一个很好的方式。它也是一个展示这个非常强大且开源工具的先导。然后,它将引导我们设计板与软件之间的第一个串行通信协议。
我们将特别与开关进行互动,但也会涵盖一些有用的硬件设计模式。
感知世界
在我们过度连接的世界中,许多系统甚至没有传感器。我们人类在我们的身体内部和外部拥有大量的生物传感器。我们能够通过皮肤感受温度,通过眼睛感受光线,通过鼻子和嘴巴感受化学成分,以及通过耳朵感受空气流动。从我们世界的特性中,我们能够感知、整合这种感觉,并最终做出反应。
如果我进一步思考,我可以回忆起我在大学早期生理学课程中学到的一个关于感官的定义(你还记得,我前生是一名生物学家):
“感官是提供感知数据的生理能力”
这个基本的生理模型是理解我们如何与 Arduino 板合作使其感知世界的一个好方法。
事实上,它引入了我们需要的三个要素:
-
容量
-
一些数据
-
一种感知
传感器提供了新的能力
传感器是一种物理转换器,能够测量一个物理量并将其转换为人类或机器可以直接或间接理解的信号。
例如,温度计是一种传感器。它能够测量局部温度并将其转换为信号。基于酒精或汞的温度计提供了刻度,根据温度的化学物质的收缩/膨胀使得它们易于读取。
为了让我们的 Arduino 能够感知世界,例如温度,我们就需要连接一个传感器。
一些类型的传感器
我们可以找到各种类型的传感器。当我们使用传感器这个词时,我们经常想到环境传感器。
我将首先引用一些环境量:
-
温度
-
湿度
-
压力
-
气体传感器(特定气体或非特定气体,烟雾)
-
电磁场
-
风速计(风速)
-
光线
-
距离
-
电容
-
运动
这是一个不完整的列表。对于几乎每个数量,我们都可以找到一个传感器。实际上,对于每个可量化的物理或化学现象,都有一种方法可以测量和跟踪它。每个都提供了与测量的数量相关的数据。
数量被转换为数据
当我们使用传感器时,原因是我们需要从物理现象(如温度或运动)中获得一个数值。如果我们能够直接用我们皮肤的热传感器测量温度,我们就能够理解化学成分的体积与温度本身之间的关系。因为我们从其他物理测量或计算中知道了这种关系,所以我们能够设计温度计。
事实上,温度计是将与温度相关的数量(在这里是体积)转换为温度计刻度上可读的值的转换。实际上,我们在这里有一个双重转换。体积是温度的函数。温度计内液体的液位是液体积的函数。因此,我们可以理解高度和温度是相关的。这是双重转换。
不管怎样,温度计是一个很好的模块,它集成了所有这些数学和物理的奇妙之处,以提供数据,一个值:温度。如图所示,体积被用来提供温度:

所有传感器都像这样工作。它们是测量物理现象并提供值的模块。我们稍后会看到这些值可以非常不同,最终也可以编码。
数据必须被感知
传感器提供的数据,如果被读取,就会更有意义。这可能是显而易见的,但想象一下,读者不是一个人类,而是一台仪器、一台机器,或者在我们的例子中,是一块 Arduino 板。
事实上,让我们以一个电子热传感器为例。首先,这个传感器必须供电才能工作。然后,如果我们能够供电但无法从其引脚物理测量它产生的电势,我们就无法欣赏它试图为我们提供的主要价值:温度。
在我们的例子中,Arduino 将是能够将电势转换为可读或至少对我们人类来说更容易理解的设备的装置。这又是一个转换。从我们想要翻译的物理现象,到显示解释物理现象的值的设备,有转换和感知。
我可以将这个过程简化,如下面的图所示:

数字意味着什么?
让我们精确地定义这里的数字术语。
数字和模拟概念
在计算机和电子领域,数字意味着离散的,这是与模拟/连续相反的。它也是一个数学定义。我们经常谈论域来定义数字和模拟的使用情况。
通常,模拟域是与物理测量相关的域。我们的温度可以具有所有可能和存在的值,即使我们的测量设备没有无限分辨率。
数字域是计算机的域。由于编码和有限的内存大小,计算机将模拟/连续值转换为数字表示。
在图表上,这可以表示如下:

Arduino 的输入和输出
Arduino 拥有输入和输出。我们还可以区分模拟和数字引脚。
你必须记住以下要点:
-
Arduino 提供了既可以作为输入也可以作为输出的数字引脚。
-
Arduino 只提供模拟输入,不提供输出。
输入和输出是板子提供的引脚,用于与外部外围设备通信。
注意
输入提供了感知世界的能力。
输出提供了改变世界的能力。
我们经常谈论“读取引脚”作为输入和“写入引脚”作为输出。确实,从 Arduino 板的角度来看,我们是从世界读取并写入世界,对吧?
数字输入是一个设置为输入的数字引脚,它提供了读取电势和将其转换为 0 或 1 到 Arduino 板的能力。我们将很快使用开关来展示这一点。
但在直接操作之前,让我介绍一位新朋友,名叫Processing。我们将使用它来在本书中轻松地展示我们的 Arduino 测试。
介绍一位新朋友——Processing
Processing 是一种开源编程语言和集成开发环境(IDE),它为想要创建图像、动画和交互的人提供支持。
这个主要的开源项目始于 2001 年,由本·弗瑞(Ben Fry)和凯西·瑞斯(Casey Reas)发起,他们是麻省理工学院媒体实验室美学与计算小组约翰·梅达(John Maeda)的前学生和大师。
这是一个大多数非程序员使用的编程框架。确实,它主要是为此目的而设计的。Processing 的第一个目标之一就是通过即时满足视觉反馈的快感,为非程序员提供一种简单的编程方式。确实,正如我们所知,编程可以非常抽象。Processing 原生提供了一块画布,我们可以在上面绘制、书写和做更多的事情。它还提供了一个非常用户友好的 IDE,我们将在官方网站processing.org上看到它。
你可能还会发现 Processing 这个术语被写成Proce55ing,因为在它的诞生时期,域名processing.org已经被占用。
Processing 是一种语言吗?
处理(Processing)在严格意义上来说不是一种语言。它是 Java 的一个子集,包含一些外部库和自定义的 IDE。
使用 Processing 进行编程通常是通过下载时附带的原生 IDE 来完成的,正如我们将在本节中看到的。
Processing 使用 Java 语言,但提供了简化的语法和图形编程。它还将所有编译步骤简化为一个一键操作,就像 Arduino IDE 一样。
就像 Arduino 核心一样,它提供了一组庞大的现成函数。您可以在processing.org/reference找到所有参考。
现在使用 Processing 的方式不止一种。实际上,由于集成在网页浏览器中的 JavaScript 运行时变得越来越强大,我们可以使用一个基于 JavaScript 的项目。您仍然可以使用 Java 继续编码,将此代码包含在您的网页中,正如官方网站所说,“Processing.js 会完成剩余的工作。这不是魔法,但几乎是的。”网站是processingjs.org。
还有非常有趣的一点:您可以使用 Processing 为 Android 移动操作系统打包应用程序。如果您感兴趣,可以阅读processing.org/learning/android。
我将避免在 JS 和 Android 应用程序上跑题,但我认为这些用法很重要,值得提及。
让我们安装并启动它
就像 Arduino 框架一样,Processing 框架不包含安装程序。您只需将其放在某个位置,然后从那里运行即可。
下载链接是:processing.org/download。
首先,下载与您的操作系统对应的软件包。请参考网站了解您特定操作系统的安装过程。
在 OS X 上,您需要解压 zip 文件,并使用图标运行生成的文件:

Processing 图标
双击图标,您将看到一个相当漂亮的启动画面:

然后,您将看到如下所示的 Processing IDE:

Processing 的 IDE 看起来与其他 IDE 相似
一个非常熟悉的 IDE
事实上,Processing IDE 看起来就像 Arduino IDE。Processing IDE 就像是 Arduino IDE 的父亲。
这完全正常,因为 Arduino IDE 是从 Processing IDE 分叉出来的。现在,我们将检查我们是否也会非常熟悉 Processing IDE。
让我们探索它并运行一个小示例:
-
前往文件 | 示例 | 基础 | 数组 | 数组对象。
-
然后,点击第一个图标(播放符号箭头)。您应该看到以下截图:
![一个非常熟悉的 IDE]()
在 Processing 中运行 ArrayObjects 原生示例
-
现在点击小方块(停止符号)。是的,这个新的游乐场非常熟悉。
![一个非常熟悉的 IDE]()
打开包含 ArrayObjects 示例的 Processing IDE
在顶部,我们可以看到一些熟悉的图标。
从左到右,它们如下所示:
-
运行(小箭头):用于编译和运行您的程序
-
停止(小方块):当程序运行时,用于停止程序
-
新建项目(小页面):这是用来打开空白画布的
-
打开项目(顶部箭头):这是用来打开现有项目的
-
保存项目(向下箭头):这是用来保存项目的
-
导出应用程序(向右箭头):这是用来创建应用程序的
当然,这里没有上传按钮。在这里,你不需要上传任何东西;我们在这里使用电脑,我们只想编写应用程序、编译它们并运行它们。
使用 Processing,你可以轻松地编写、编译和运行代码。
如果你在一个项目中使用多个文件(特别是如果你使用一些独立的 Java 类),你可以有一些标签页。
在这个标签区域下,你有文本区域,你可以在这里输入你的代码。代码的颜色与 Arduino IDE 中的颜色相同,这非常有用。
最后,在底部,你有日志控制台区域,所有消息都可以在这里输出,从错误到我们自己的跟踪消息。
替代 IDE 和版本控制
如果你感兴趣,想挖掘一些 IDE 替代品,我建议你使用通用的开源软件开发环境 Eclipse。我向所有想进一步在纯开发领域发展的学生推荐这个强大的 IDE。它可以轻松设置以支持版本控制。
版本控制是一个非常好的概念,它提供了一种轻松跟踪代码版本的方法。例如,你可以编写一些代码,测试它,然后在版本控制系统中备份,然后继续你的代码设计。如果你运行它,并在某个时刻出现一个漂亮而可爱的崩溃,你可以轻松地检查你的工作代码和新不工作的代码之间的差异,从而使故障排除变得容易得多!我不会详细描述版本控制系统,但我想向你介绍两个广泛使用的系统。
检查一个示例
这里有一小段代码展示了几个简单易行的设计模式。你还可以在代码包中的Chapter05 /p rocessingMultipleEasing/ 文件夹中找到这段代码:
// some declarations / definitions
int particlesNumber = 80; // particles number
float[] positionsX = new float[particlesNumber]; // store particles X-coordinates float[] positionsY = new float[particlesNumber]; // store particles Y-coordinates
float[] radii = new float[particlesNumber]; // store particles radii
float[] easings = new float[particlesNumber]; // store particles easing amount
// setup is run one time at the beginning
void setup() {
size(600, 600); // define the playground
noStroke(); // define no stroke for all shapes drawn
// for loop initializing easings & radii for all particles
for (int i=0 ; i < particlesNumber ; i++)
{
easings[i] = 0.04 * i / particlesNumber; // filling the easing array
radii[i] = 30 * i / particlesNumber ; // filling the radii array
}
}
// draw is run infinitely
void draw() {
background(34); // define the background color of the playground
// let's store the current mouse position
float targetX = mouseX;
float targetY = mouseY;
// for loop across all particles
for (int i=0 ; i < particlesNumber ; i++)
{
float dx = targetX - positionsX[i]; // calculate X distance mouse / particle
if (abs(dx) > 1) { // if distance > 1, update position
positionsX[i] += dx * easings[i];
}
float dy = targetY - positionsY[i]; // same for Y
if (abs(dy) > 1) {
positionsY[i] += dy * easings[i];
}
// change the color of the pencil for the particle i
fill(255 * i / particlesNumber);
// draw the particle i
ellipse(positionsX[i], positionsY[i], radii[i], radii[i]);
}
}
你可以运行这段代码。然后,你可以将鼠标移入画布中,享受正在发生的事情。

processingMultipleEasing 代码正在运行并显示一系列奇怪的粒子,这些粒子跟随鼠标移动
首先,检查代码。基本上,这是 Java。我想你不会太惊讶,对吧?确实,Java 源自 C。
你可以在你的代码中看到三个主要部分:
-
变量声明/定义
-
setup()函数只在开始时运行一次 -
draw()函数会无限运行,直到你按下停止键
好的。你可以看到 Arduino 核心和 Processing 中的setup()函数具有类似的作用,loop()和draw()也是如此。
这段代码展示了 Processing 中的一些常用设计模式。我首先初始化一个变量来存储全局粒子数,然后为我想创建的每个粒子初始化一些数组。请注意,所有这些数组在这个步骤都是空的!
这种模式很常见,因为它提供了良好的可读性,并且工作得很好。我本可以使用类或甚至是多维数组,但在后一种情况下,除了代码更短(但可读性更差)之外,我甚至不会得到任何好处。在这些数组中,第N个索引值代表第N个粒子。为了存储/检索粒子N的参数,我必须操纵每个数组中的第N个值。参数分布在每个数组中,但存储和检索都很方便,不是吗?
在setup()中,我定义并实例化了画布及其 600 x 600 的大小。然后,我定义在所有我的绘画中都不会有线条。例如,圆的线条是其边界。
然后,我使用for循环结构填充easing和radii数组。这是一个非常常见的模式,我们可以使用setup()在开始时初始化一系列参数。然后我们可以检查draw()循环。我定义了一个背景颜色。这个函数也会擦除画布并填充参数中的颜色。查看参考页面上的背景函数,以了解我们如何使用它。这种擦除/填充是一种很好的方式来擦除每一帧并重置画布。
在这次擦除/填充之后,我将鼠标的当前位置存储在每个坐标的局部变量targetX和targetY中。
程序的核心位于for循环中。这个循环遍历每个粒子,并为每个粒子生成一些内容。代码相当直观。我还可以在这里补充说,我正在检查鼠标和每个粒子之间的距离,这是在每一帧(每次draw()的运行)中进行的,并且我会根据其缓动效果移动每个粒子来绘制它们。
这是一个非常简单的例子,但也是一个很好的例子,我用来展示 Processing 的强大功能。
Processing 和 Arduino
Processing 和 Arduino 是非常好的朋友。
首先,它们都是开源的。这是一个非常友好的特性,带来了许多优势,如代码源共享和庞大的社区等。它们适用于所有操作系统:Windows、OS X 和 Linux。我们还可以免费下载它们,并点击几下即可运行。
我最初是用 Processing 编程的,并且我经常用它来做一些自己的数据可视化项目和艺术作品。然后,我们可以在屏幕上通过平滑和原始的形状来展示复杂和抽象的数据流。
我们现在要一起做的是在 Processing 画布上显示 Arduino 的活动。实际上,这是 Processing 作为 Arduino 友好的软件的常见用途。
我们将设计一个非常简单且便宜的硬件和软件之间的通信协议。这将展示我们在本书下一章将进一步深入探讨的路径。确实,如果你想让你的 Arduino 与另一个软件框架(我想到了 Max 6、openFrameworks、Cinder 以及许多其他框架)通信,你必须遵循相同的设计方法。

Arduino 和一些软件朋友
我经常说 Arduino 可以作为一个非常智能的器官来工作。如果你想将一些软件连接到真实的物理世界,Arduino 就是你的选择。确实,通过这种方式,软件可以感知世界,为你的电脑提供新的功能。让我们通过在电脑上显示一些物理世界事件来继续前进。
按下按钮
我们将会有趣。是的,这就是我们将物理世界与虚拟世界连接的特殊时刻。Arduino 正是关于这一点。
按钮和开关是什么?
开关是一种能够断开电路的电气元件。有很多不同类型的开关。
不同类型的开关
一些开关被称为切换开关。切换开关也被称为连续开关。为了对电路进行操作,切换开关可以每次按下并释放,以便进行操作,当你释放它时,操作会继续。
一些被称为瞬态开关。瞬态开关也被称为动作按钮。为了对电路进行操作,你必须按下并保持开关按下以继续操作。如果你释放它,操作就会停止。
通常,我们家里的所有开关都是切换开关。除了你必须按下以切断并释放以停止的混音器开关,这意味着它是一个瞬态开关。
基本电路
这里有一个带有 Arduino、一个瞬态开关和一个电阻的基本电路。
我们想在按下瞬态开关时打开板上的内置 LED,并在释放它时关闭 LED。

小电路
我现在向您展示的是我们即将要工作的电路。这也是一个很好的理由,让你更熟悉电路图。
电缆
每条线代表两个组件之间的连接。根据定义,一条线是电缆,从一侧到另一侧没有电势。它也可以定义为以下内容:电缆的电阻为 0 欧姆。然后我们可以这样说,通过电缆连接的两个点具有相同的电势。
现实世界的电路
当然,我不想直接展示下一个图表。现在我们必须构建真实的电路,所以请拿一些电线、你的面包板和瞬态开关,并按照下一个图表所示连接整个电路。
你可以取一个大约 10 千欧姆的电阻。我们将在下一页解释电阻的作用。

真实电路中的瞬态开关
让我们更详细地解释一下。
让我们记住面包板布线;我在面包板顶部使用冷线和热线(冷线是蓝色,表示地线,热线是红色,表示+5 V)。在我将地线和+5 V 从 Arduino 连接到总线之后,我使用总线来布线板的其他部分;这更容易,并且需要更短的电缆。
地线和数字引脚 2 之间有一个电阻。+5 V 线和引脚 2 之间有一个瞬态开关。引脚 2 将被设置为输入,这意味着它能够吸收电流。
通常,开关是按下开。按下它们闭合电路并允许电流流动。所以,在这种情况下,如果我不按下开关,就没有从+5 V 到引脚 2 的电流。
在按下期间,电路闭合。然后,电流从+5 V 流向引脚 2。这有点比喻和滥用,我应该说我在这+5 V 和引脚 2 之间创建了一个电势,但我需要更简洁地说明这一点。
那么这个电阻,为什么在这里?
上拉和下拉的概念
如果全局电路很简单,那么电阻部分一开始可能会有些棘手。
将数字引脚设置为输入提供了吸收电流的能力。这意味着它表现得像地线。实际上,内部工作方式确实就像相关的引脚连接到地线一样。
使用正确编码的固件,我们就有能力检查引脚 2。这意味着我们可以测试它并读取电势值。因为这是一个数字输入,接近+5 V 的电势会被解释为高值,而接近 0 V 则会被解释为低值。这两个值都是在 Arduino 核心内部定义的常量。但即使在一个完美的数字世界中一切看起来都完美无缺,这也并不真实。
实际上,输入信号噪声可能会被误读为按钮按下。
为了确保安全,我们使用所谓的下拉电阻。这通常是一个高阻抗电阻,为考虑的数字引脚提供电流吸收,如果开关未按下,则使其在 0 V 值时更安全。下拉以更一致地识别为低值,上拉以更一致地识别为高值。
当然,全球能源消耗略有增加。在我们的情况下,这在这里并不重要,但你必须知道这一点。关于这个相同的概念,一个上拉电阻可以用来将+5 V 连接到数字输出。一般来说,你应该知道芯片组的 I/O 不应该悬空。
这里是你必须记住的:
| 数字引脚类型 | 输入 | 输出 |
|---|---|---|
| 上拉电阻 | 下拉电阻 | 上拉电阻 |
我们想要按下开关,特别是这个动作必须使 LED 点亮。我们首先编写伪代码。
伪代码
下面是一个可能的伪代码。以下是我们希望固件遵循的步骤:
-
定义引脚。
-
定义一个变量来表示当前开关状态。
-
将 LED 引脚设置为输出。
-
将开关引脚设置为输入。
-
设置一个无限循环。在无限循环中执行以下操作:
-
读取输入状态并存储它。
-
如果输入状态是 HIGH,则点亮 LED。
-
否则关闭 LED。
-
代码
这里是将此伪代码翻译成有效 C 代码的示例:
const int switchPin = 2; // pin of the digital input related to the switch
const int ledPin = 13; // pin of the board built-in LED
int switchState = 0; // storage variable for current switch state
void setup() {
pinMode(ledPin, OUTPUT); // the led pin is setup as an output
pinMode(switchPin, INPUT); // the switch pin is setup as an input
}
void loop(){
switchState = digitalRead(switchPin); // read the state of the digital pin 2
if (switchState == HIGH) { // test if the switch is pushed or not
digitalWrite(ledPin, HIGH); // turn the LED ON if it is currently pushed
}
else {
digitalWrite(ledPin, LOW); // turn the LED OFF if it is currently pushed
}
}
如往常一样,你还可以在 Packt Publishing 网站上找到代码,在Chapter05/MonoSwitch/文件夹中,以及其他可下载的代码文件。
上传它并看看会发生什么。你应该有一个很好的系统,你可以按下一个开关并点亮一个 LED。太棒了!
现在我们让 Arduino 板和 Processing 相互通信。
让 Arduino 和 Processing 进行对话
假设我们想在计算机上可视化我们的开关操作。
我们必须在 Arduino 和 Processing 之间定义一个小型的通信协议。当然,我们会使用串行通信协议,因为它设置起来相当简单,而且很轻量。
我们可以将协议设计为一个通信库。目前,我们只使用本地的 Arduino 核心来设计协议。然后,在本书的后面部分,我们将设计一个库。
通信协议
通信协议是一套规则和格式,用于在两个实体之间交换消息。这些实体可以是人类、计算机,也许还有更多。
事实上,我会用一个基本的类比来解释我们的语言。为了相互理解,我们必须遵循一些规则:
-
语法和语法规则(我必须使用你知道的单词)
-
物理规则(我必须说得足够大声)
-
社交规则(我不应该在向你询问时间之前侮辱你)
我可以引用许多其他规则,比如说话的速度、两个实体之间的距离等等。如果每个规则都被同意并验证,我们就可以一起交流。在设计协议之前,我们必须定义我们的要求。
协议要求
我们想要做什么?
我们需要在计算机内部的 Arduino 和 Processing 之间建立一个通信协议。对!这些要求对于你将要设计的许多通信协议通常是相同的。
这里是一个非常重要的简短列表:
-
协议必须能够在不重写一切的情况下扩展,每次我想添加新的消息类型时。
-
协议必须能够快速发送足够的数据
-
协议必须易于理解,并且有良好的注释,特别是对于开源和协作项目。
协议设计
每条消息的大小为 2 字节。这是一个常见的数据包大小,我建议这样组织数据:
-
字节 1:开关编号
-
字节 2:开关状态
我将字节 1 定义为开关编号的表示,通常是因为扩展性的要求。对于一个开关,数字将是 0。
我可以轻松地在板和计算机之间实例化串行通信。实际上,当我们使用串行监控时,至少在 Arduino 一侧我们已经做到了这一点。
我们如何使用 Processing 来实现这一点?
The Processing code
Processing 已经内置了一个非常有用的库集。具体来说,我们将使用串行库。
让我们先画一个伪代码,就像往常一样。
绘制伪代码
我们希望程序做什么?
我建议有一个大圆圈。它的颜色将代表开关的状态。深色表示未释放,而绿色表示按下。
可以如下创建伪代码:
-
定义并实例化串行端口。
-
定义一个当前绘图颜色为深色。
-
在无限循环中,执行以下操作:
-
检查串行端口和抓取数据是否已接收。
-
如果数据指示状态是关闭的,将当前绘图颜色从颜色更改为深色。
-
否则,将当前绘图颜色更改为绿色。
-
使用当前绘图颜色绘制圆圈。
-
让我们写下这段代码
让我们打开一个新的 Processing 画布。
因为 Processing IDE 的工作方式类似于 Arduino IDE,需要在一个文件夹中创建所有保存的项目文件,所以我建议您直接在磁盘上的正确位置保存画布,即使它是空的。命名为processingOneButtonDisplay。
您可以在 Packt 网站上找到代码,位于Chapter05/processingOneButtonDisplay/文件夹中,可供下载,以及其他代码文件。

在您的代码中包含库
要从 Processing 核心包含串行库,您可以转到草图 | 导入库… | 串行。这将在您的代码中添加这一行:processing.serial.*;
您也可以自己输入这个语句。
以下是一段带有许多注释的代码:
import processing.serial.*;
Serial theSerialPort; // create the serial port object
int[] serialBytesArray = new int[2]; // array storing current message
int switchState; // current switch state
int switchID; // index of the switch
int bytesCount = 0; // current number of bytes relative to messages
boolean init = false; // init state
int fillColor = 40; // defining the initial fill color
void setup(){
// define some canvas and drawing parameters
size(500,500);
background(70);
noStroke();
// printing the list of all serial devices (debug purpose)
println(Serial.list());
// On osx, the Arduino port is the first into the list
String thePortName = Serial.list()[0];
// Instantate the Serial Communication
theSerialPort = new Serial(this, thePortName, 9600);
}
void draw(){
// set the fill color
fill(fillColor);
// draw a circle in the middle of the screen
ellipse(width/2, height/2, 230, 230);
}
void serialEvent(Serial myPort) {
// read a byte from the serial port
int inByte = myPort.read();
if (init == false) { // if there wasn't the first hello
if (inByte == 'Z') { // if the byte read is Z
myPort.clear(); // clear the serial port buffer
init = true; // store the fact we had the first hello
myPort.write('Z'); // tell the Arduino to send more !
}
}
else { // if there already was the first hello
// Add the latest byte from the serial port to array
serialBytesArray[bytesCount] = inByte;
bytesCount++;
// if the messages is 2 bytes length
if (bytesCount > 1 ) {
switchID = serialBytesArray[0]; // store the ID of the switch
switchState = serialBytesArray[1]; // store the state of the switch
// print the values (for debugging purposes):
println(switchID + "\t" + switchState);
// alter the fill color according to the message received from Arduino
if (switchState == 0) fillColor = 40;
else fillColor = 255;
// Send a capital Z to request new sensor readings
myPort.write('Z');
// Reset bytesCount:
bytesCount = 0;
}
}
}
变量定义
theSerialPort是Serial库的对象。我必须首先创建它。
serialBytesArray是一个包含两个整数的数组,用于存储来自 Arduino 的消息。您还记得吗?当我们设计协议时,我们谈到了 2 字节消息。
switchState和switchID是全局但临时变量,用于存储来自板子的开关状态和开关 ID。开关 ID 被放置在那里,以便(接近)未来的实现,以便在我们要使用多个开关的情况下区分不同的开关。
bytesCount是一个有用的变量,用于跟踪我们的消息读取中的当前位置。
init在开始时定义为false,当第一次接收到来自 Arduino 的第一个字节(以及一个特殊的字节,Z)时变为true。这是一种首次接触的目的。
然后,我们跟踪填充颜色和初始颜色是40。40只是一个整数,并将稍后用作函数fill()的参数。
setup()
我们定义画布(大小、背景颜色和没有轮廓)。
我们打印出计算机上可用的所有串行端口的列表。这是下一个语句的调试信息,我们将第一个串行端口的名称存储到一个 String 中。实际上,您可能需要根据打印列表中 Arduino 端口的定位将数组元素从 0 更改为正确的位置。
这个字符串随后被用于一个非常重要的语句,该语句在 9600 波特率下实例化串行通信。
当然,这个 setup() 函数只运行一次。
draw()
在这里,draw 函数非常简单。
我们将变量 fillColor 传递给 fill() 函数,设置所有后续形状填充的颜色。
然后,我们使用椭圆函数绘制圆形。这个函数接受四个参数:
-
椭圆中心的 x 坐标(这里
width/2) -
椭圆中心的 y 坐标(这里
height/2) -
椭圆的宽度(这里
230) -
椭圆的高度(这里
230)
在 Processing IDE 中用蓝色标注的 width 和 height 是画布的当前宽度和高度。使用它们非常有用,因为如果你通过选择新的画布大小更改 setup() 语句,你的代码中所有的 width 和 height 都会自动更新,而无需手动更改它们。
请记住,宽度和高度相同的椭圆是一个圆(!)!好的。但是这里的魔法在哪里?它只会绘制一个圆形,每次都是同一个(大小和位置)。fillColor 是 draw() 函数的唯一变量。让我们看看那个奇怪的回调 serialEvent()。
serialEvent() 回调
我们在 第四章 中讨论了回调,通过函数、数学和计时改进编程。
这里,我们在 Processing 中有一个纯回调方法。这是一个事件驱动的回调。在这种情况下,不必每次轮询我们的串行端口是否需要读取数据是有用且高效的。确实,与用户界面相关的事件数量远少于 Arduino 板的处理器周期数。在这种情况下实现回调更聪明;一旦发生串行事件(即接收到消息),我们就执行一系列语句。
myPort.read() 首先读取接收到的字节。然后我们使用 init 变量进行测试。实际上,如果是第一条消息,我们想检查通信是否已经启动。
在第一次“你好”(init == false)的情况下,如果来自 Arduino 板的消息是 Z,Processing 程序会清除自己的串行端口,存储通信刚刚开始的事实,并将 Z 重新发送回 Arduino 板。这并不复杂。
这可以如下说明:
假设我们只有通过先互相说“你好”才能开始交谈。我们并没有互相观察(没有事件)。然后我开始说话。你转向我(串行事件发生)并倾听。我是不是在对你说“你好”?(消息是否为 Z?)。如果不是,你只需转回你的头(没有 else 语句)。如果是,你回答“你好”(发送回 Z),通信就开始了。
那么,接下来会发生什么呢?
如果通信已经开始,我们必须将读取的字节存储到serialBytesArray中,并增加bytesCount。当字节正在接收且bytesCount小于或等于 1 时,这意味着我们没有完整的消息(一个完整的消息是两个字节),因此我们在数组中存储更多的字节。
当字节计数等于2时,我们就有了完整的消息,我们可以将其“分割”成变量switchID和switchState。我们是这样做的:
switchID = serialBytesArray[0];
switchState = serialBytesArray[1];
下一个语句是一个调试语句:我们打印每个变量。然后,方法的核心是测试switchState变量。如果它是0,这意味着开关被释放,我们将fillColor修改为40(深色,40表示每个 RGB 组件的值 40;请查看 Processing 参考中的color()方法processing.org/reference/color_.html)。如果不是0,我们将fillColor修改为255,这意味着白色。我们可以通过不只用else,而是用else if (switchState ==1)来更安全一些。
为什么?因为我们不确定可以发送的所有消息(缺乏文档或其他使我们不确定的原因),只有当switchState等于1时,我们才能将颜色修改为白色。这个概念也可以在优化状态下完成,但在这里,它相当简单,所以我们可以保持原样。
好的。这是一件既好又重的东西,对吧?现在,让我们看看我们如何修改 Arduino 代码。你还记得吗?它还没有准备好通信。
新的 Arduino 固件已准备好进行通信
因为我们现在有了一种很好的方式来显示开关状态,所以我将移除与板载内置 LED 相关的一切,以下是结果:
const int switchPin = 2; // pin of the digital input related to the switch
int switchState = 0; // storage variable for current switch state
void setup() {
pinMode(switchPin, INPUT); // the switch pin is setup as an input
}
void loop(){
switchState = digitalRead(switchPin);
}
我们需要添加什么?所有的Serial相关内容。我还想添加一个专门用于第一个“hello”的小函数。
这是结果,然后我们将看到解释:
const int switchPin = 2; // pin of the digital input related to the switch
int switchState = 0; // storage variable for current switch state
int inByte = 0;
void setup() {
Serial.begin(9600);
pinMode(switchPin, INPUT); // the switch pin is setup as an input
sayHello();
}
void loop(){
// if a valid byte is received from processing, read the digital in.
if (Serial.available() > 0) {
// get incoming byte:
inByte = Serial.read();
switchState = digitalRead(switchPin);
// send switch state to Arduino
Serial.write("0");
Serial.write(switchState);
}
}
void sayHello() {
while (Serial.available() <= 0) {
Serial.print('Z'); // send a capital Z to Arduino to say "HELLO!"
delay(200);
}
}
我首先定义了一个新变量:inByte。它存储读取的字节。然后,在setup()方法中,我实例化了串行通信,就像我们之前学习的那样使用 Arduino。然后,我设置了开关引脚的pinMode方法,然后调用sayHello()。
这个函数只是等待某事发生。请集中注意这一点。
我在setup()中调用这个函数。这是一个简单的调用,不是一个回调或其他任何东西。这个函数包含一个while循环,当Serial.available()小于或等于零时。这意味着什么?这意味着这个函数在第一个字节到达 Arduino 板的串行端口时暂停setup()方法。loop()在setup()完成之前不会运行,所以这是一个很好的技巧来等待第一个外部事件;在这种情况下,第一次通信。实际上,当 Processing 没有回答时,板正在发送消息Z(即,“hello”)。
结果是,当你插入你的板时,它会在你运行 Processing 程序时连续发送Z。然后开始通信,你可以按开关并查看发生了什么。实际上,一旦通信开始,loop()就开始它的无限循环。首先在每个周期进行测试,我们只测试是否有字节被接收。无论接收到的字节是什么(Processing 只向板发送Z),我们都读取开关的数字引脚并发送两个字节。请注意:每个字节都是使用Serial.write()写入串行端口的。你必须发送 2 个字节,所以你需要堆叠两个Serial.write()。第一个字节是按下的/释放的开关的编号(ID);在这里,它不是一个变量,因为我们只有一个开关,所以它是一个整数 0。第二个字节是开关状态。我们刚才看到了一个很好的设计模式,涉及板、在计算机上运行的外部程序以及两者之间的通信。
现在,让我们更进一步,玩转一个以上的开关。
玩转多个按钮
我们可以用一个以上的开关来扩展我们之前设计的逻辑。
使用多个开关的方法有很多,通常在 Arduino 上也有多个输入。我们现在将看到一种既便宜又简单的方法。这种方法不涉及在仅几个 Arduino 输入上复用大量输入,而是一种基本的点对点连接,其中每个开关都连接到一个输入。我们将在稍后学习复用(在下一章中)。
电路
下面是与多个开关一起工作所需的电路图:

将三个瞬态开关连接到 Arduino 板
电路图是对之前只显示一个开关的电路图的扩展。我们可以看到三个开关位于+5V 和三个下拉电阻之间。然后我们还可以看到连接到数字输入 2 到 4 的三根线。
这里是一个小的记忆刷新:为什么我没有使用数字引脚 0 或 1?
因为我在 Arduino 中使用串行通信,所以我们不能使用数字引脚 0 和 1(每个分别对应于串行通信中使用的 RX 和 TX)。即使我们使用 USB 链路作为我们串行消息的物理支持,Arduino 板也是这样设计的,我们必须非常小心。
这是带有面包板的电路视图。我故意没有对齐每一根线。为什么?你不记得我想要你在阅读这本书后完全自主吗?是的,你会在现实世界中找到很多这样的电路图。你也必须熟悉它们。这可能是一个(简单的)家庭作业。

之前的电路显示了三个开关、三个下拉电阻和 Arduino 板。
必须修改两个源代码以提供对新电路的支持。
让我们在那里添加一些内容。
Arduino 代码
下面是新的代码;当然,你可以在 Packt 网站上找到它,在 Chapter05/MultipleSwitchesWithProcessing/ 文件夹中,与其他代码文件一起可供下载:
#define switchesNumber 3 // define the number of switches as a constant
int switchesStates[switchesNumber] ; // array storing current switches states
int inByte = 0;
void setup() {
Serial.begin(9600);
// initiating each pins as input and filling switchesStates with zeroes
for(int i = 0 ; i < switchesNumber ; i++)
{
// BE CAREFUL TO THAT INDEX
pinMode(i + 2, INPUT); // the switch pin is setup as an input
switchesStates[i] = 0 ;
}
sayHello(); // waiting for the processing program hello answer
}
void loop(){
// if a valid byte is received from processing, read all digital inputs.
if (Serial.available() > 0) {
// get incoming byte
inByte = Serial.read();
for(int i = 0 ; i < switchesNumber ; i++)
{
switchesStates[i] = digitalRead(i+2); // BE CAREFUL TO THAT INDEX
// WE ARE STARTING FROM PIN 2 !
Serial.write(i); // 1st byte = switch number (0 to 2)
Serial.write(switchesStates[i]); // 2nd byte = the switch i state
}
}
}
void sayHello() {
while (Serial.available() <= 0) {
Serial.print('Z'); // send a capital Z to Arduino to say "HELLO!"
delay(200);
}
}
让我们来解释这段代码。
首先,我将一个常量 switchesNumber 定义为数字 3。这个数字可以从 1 到 12 中的任何其他数字更改。这个数字代表当前连接到板上的开关数量,从数字引脚 2 到数字引脚 14。所有开关都必须相互连接,中间不能有空引脚。
然后,我定义了一个数组来存储开关的状态。我使用 switchesNumber 常量作为长度来声明它。我必须在 setup() 方法中用 for 循环填充这个数组,我创建了一个 for 循环。这提供了一种安全的方式,确保代码中所有开关都有一个释放状态。
我仍然使用 sayHello() 函数来设置与 Processing 的通信开始。
的确,我必须填充数组 switchesStates 中的每个开关状态,所以我添加了 for 循环。请注意每个 for 循环中的索引技巧。实际上,因为从 0 开始似乎更方便,而且在现实世界中,在使用串行通信时我们绝对不能使用数字引脚 0 和 1,所以我一处理实际的数字引脚数量,也就是使用 pinMode() 和 digitalRead() 这两个函数时,就立即添加了 2。
现在,让我们也升级一下 Processing 代码。
Processing 代码
下面是新的代码;你可以在 Packt 网站的 Chapter05/MultipleSwitchesWithProcessing/ 文件夹中找到它,与其他代码文件一起可供下载:
import processing.serial.*;
int switchesNumber = 2;
Serial theSerialPort; // create the serial port object
int[] serialBytesArray = new int[2]; // array storing current message
int switchID; // index of the switch
int[] switchesStates = new int[switchesNumber]; // current switch state
int bytesCount = 0; // current number of bytes relative to messages
boolean init = false; // init state
int fillColor = 40; // defining the initial fill color
// circles display stuff
int distanceCircles ;
int radii;
void setup() {
// define some canvas and drawing parameters
size(500, 500);
background(70);
noStroke();
distanceCircles = width / switchesNumber;
radii = distanceCircles/2;
// printing the list of all serial devices (debug purpose)
println(Serial.list());
// On osx, the Arduino port is the first into the list
String thePortName = Serial.list()[0];
// Instantate the Serial Communication
theSerialPort = new Serial(this, thePortName, 9600);
for (int i = 0 ; i < switchesNumber ; i++)
{
switchesStates[i] = 0;
}
}
void draw() {
for (int i = 0 ; i < switchesNumber ; i++)
{
if (switchesStates[i] == 0) fill(0);
else fill(255);
// draw a circle in the middle of the screen
ellipse(distanceCircles * (i + 1) - radii, height/2, radii, radii);
}
}
void serialEvent(Serial myPort) {
// read a byte from the serial port
int inByte = myPort.read();
if (init == false) { // if this is the first hello
if (inByte == 'Z') { // if the byte read is Z
myPort.clear(); // clear the serial port buffer
init = true; // store the fact we had the first hello
myPort.write('Z'); // tell the Arduino to send more !
}
}
else { // if there already was the first hello
// Add the latest byte from the serial port to array
serialBytesArray[bytesCount] = inByte;
bytesCount++;
// if the messages is 2 bytes length
if (bytesCount > 1 ) {
switchID = serialBytesArray[0]; // store the ID of the switch
switchesStates[switchID] = serialBytesArray[1]; // store state of the switch
// print the values (for debugging purposes):
println(switchID + "\t" + switchesStates[switchID]);
// Send a capital Z to request new sensor readings
myPort.write('Z');
// Reset bytesCount:
bytesCount = 0;
}
}
}
下面是使用五个开关并按下第四个按钮时此代码渲染的截图:

那么,我改变了什么?
与 Arduino 代码中的概念相同,我添加了一个变量(不是一个常量),命名为 switchesNumber。一个很好的进化可能是向协议中添加有关开关数量的内容。例如,Arduino 板可以根据 Arduino 固件中定义的一个常量来通知 Processing 开关的编号。这将节省我们在更改此数字时手动更新 processing 代码的时间。
我还将变量 switchState 转换成了一个整数数组 switchesStates。这个数组存储了所有开关的状态。我添加了两个与显示相关的变量:distanceCircles 和 radii。这些用于根据开关的数量动态显示圆的位置。实际上,我们希望每个开关对应一个圆。
setup() 函数几乎和之前一样。
我在这里通过将画布宽度除以圆的数量来计算两个圆之间的距离。然后,我通过将它们之间的距离除以 2 来计算每个圆的半径。这些数字可以更改。你可以有非常不同的审美选择。
这里的大不同之处也是for循环。我在整个switchesStates数组中填充零以初始化它。一开始,没有任何开关被按下。现在的draw()函数也包括一个for循环。请注意这里。我移除了fillColor方法,因为我将填充颜色的选择移到了draw中。这是一个替代方案,向你展示代码的灵活性。
在同一个for循环中,我在绘制圆号i。我将让你自己检查我是如何放置这些圆圈的。serialEvent()方法也没有太多变化。正如我之前写的,我移除了填充颜色的变化。我还使用了switchesStates数组,以及存储在switchID中的消息的第一个字节的索引。
现在,你在 Arduino 板上上传了固件后,可以在每一侧运行代码。
魔法?我想你现在知道这根本不是魔法,而是美丽的,也许吧。
让我们进一步讨论关于开关的一些重要内容,同时也与其他开关相关。
理解去抖动概念
现在有一个小节,与模拟输入相比,它相当酷且轻巧,我们将在下一章深入探讨。
我们将讨论当有人按下按钮时发生的事情。
什么?谁在弹跳?
现在,我们必须用我们的微观生物控制论眼睛来放大开关的结构。
开关是由金属和塑料制成的。当你按下盖子时,一块金属移动并接触到另一块金属,闭合电路。在微观层面和非常短的时间间隔内,事情并不那么干净。实际上,移动的金属片会弹跳到另一部分。通过使用示波器测量 Arduino 数字引脚上的电势,我们可以在按下后大约 1 毫秒的电压曲线上看到一些噪声。
这些振荡可能会在某些程序中生成错误的输入。想象一下,你想要按顺序计数状态转换,例如,当用户按下开关七次时运行某些操作。如果你有一个弹跳系统,通过只按一次,程序可能会计数很多转换,即使用户只按了一次开关。
查看下一个图表。它表示电压与时间的关系。时间轴上的小箭头显示了开关被按下的时刻:

我们如何处理这些振荡?
如何去抖动
我们有两个不同的元素,我们可以对其施加作用:
-
电路本身
-
固件
电路本身可以进行修改。我可以引用一些解决方案,例如添加二极管、电容器和一些施密特触发器反相器。我不会详细解释这个解决方案,因为我们将在软件中实现它,但我可以解释全局概念。在这种情况下,电容器将在开关弹跳时充电和放电,从而平滑噪声的峰值。当然,需要进行一些测试,以找到适合您精确需求的完美组件。
固件也可以进行修改。
基本上,我们可以使用基于时间的过滤器,因为弹跳发生在特定的时间段内。
下面是代码,然后是解释:
const int switchPin = 2; // pin of the digital input related to the switch
const int ledPin = 13; // pin of the board built-in LED
int switchState = 0; // storage variable for current switch state
int lastSwitchState= LOW;
// variables related to the debouncing system
long lastDebounceTime = 0;
long debounceDelay = 50;
void setup() {
pinMode(ledPin, OUTPUT); // the led pin is setup as an output
pinMode(switchPin, INPUT); // the switch pin is setup as an input
}
void loop(){
// read the state of the digital pin
int readInput = digitalRead(switchPin);
// if freshly read state is different than the last debounced value
if (readInput != lastSwitchState){
// reset the debounce counter by storing the current uptime ms
lastDebounceTime = millis();
}
// if the time since the last debounce is greater than the debounce delay
if ( (millis() - lastDebounceTime) > debounceDelay ){
// store the value because it is a debounced one and we are safe
switchState = readInput;
}
// store the last read state for the next loop comparison purpose
lastSwitchState = readInput;
// modify the LED state according to the switch state
if (switchState == HIGH)
{ // test if the switch is pushed or not
digitalWrite(ledPin, HIGH); // turn the LED ON if it is currently pushed
}
else
{
digitalWrite(ledPin, LOW); // turn the LED OFF if it is currently pushed
}
}
下面是去抖动周期的示例。
在开始时,我定义了一些变量:
-
lastSwitchState:此变量存储最后一个读取的状态 -
lastDebounceTime:此变量存储上次去抖动发生的时间 -
debounceDelay:这是在此期间被视为安全值的值
我们在这里使用millis()来测量时间。我们已经在第四章中讨论了此时间函数,使用函数、数学和定时改进编程。
然后,在每次loop()周期中,我读取输入,但基本上我不将其存储在用于测试 LED 开关的switchState变量中。基本上,我过去常说switchState是官方变量,我不希望在去抖动过程之前修改它。用其他话说,我只有在确定状态时才将东西存储在switchState中,而不是在此之前。
因此,我在每个周期读取输入并将其存储在readInput中。我将readInput与最后一个读取的值lastSwitchState变量进行比较。如果这两个变量都不同,这意味着什么?这意味着发生了变化,但可能是弹跳(不希望的事件)或真实的推动。无论如何,在这种情况下,我们将通过将millis()提供的当前时间放入lastDebounceTime来重置计数器。
然后,我们检查自上次去抖动以来经过的时间是否大于我们的延迟。如果是,那么我们可以考虑在这个周期中的最后一个readInput作为实际的开关状态,并将其存储到相应的变量中。在另一种情况下,我们将最后一个读取的值存储到lastSwitchState中,以供下一个周期的比较。
此方法是一个用于平滑输入的一般概念。
我们可以在各个地方找到一些软件去抖动的例子,这些例子不仅用于开关,也用于噪声输入。在所有与用户驱动事件相关的内容中,我都会建议使用这种去抖动器。但对于所有与系统通信相关的内容,去抖动可能非常无用,甚至可能成为问题,因为我们可能会忽略一些重要的消息和数据。为什么?因为通信系统比任何用户都要快得多,如果我们可以将 50 毫秒作为用户认为真实推动或释放的时间,那么我们无法将这个时间应用于非常快速的芯片信号和其他系统之间可能发生的事件。
摘要
我们对数字输入的了解又深入了一些。数字输入可以直接使用,就像我们刚才做的那样,也可以间接使用。我使用这个术语是因为确实,我们可以在将数据发送到数字输入之前使用其他外围设备对数据进行编码。我使用了一些类似这样的距离传感器,使用数字输入而不是模拟输入。它们通过 I2C 协议编码距离并将数据输出。提取和使用距离需要一些特定的操作。这样,我们就是在间接使用数字输入。
另一种感知世界的好方法是使用模拟输入。确实,这开启了一个连续值的新世界。让我们继续前进。
第六章:感知世界——用模拟输入感觉
真实的世界并不是数字的。基于数字艺术的我的视野让我看到了事物背后的矩阵以及事物之间巨大的数字瀑布。然而,在这一章中,我需要向你传达数字和模拟之间的关系,并且我们需要很好地理解它。
这章很好,但很大。不要害怕。我们还将在设计纯 C++代码的同时讨论很多新概念。
我们将一起描述什么是模拟输入。我还会向你介绍一个值得尊重的新朋友,Max 6 框架。确实,它将帮助我们像 Processing 一样与 Arduino 板通信。你会意识到这对计算机来说有多重要,尤其是当它们需要感知世界时。拥有 Max 6 框架的计算机非常强大,但拥有 Max 6 框架和 Arduino 插件的计算机可以感受到物理世界的许多特性,如压力、温度、光、颜色等等。正如我们之前看到的,Arduino 表现得有点像一个能够…感觉的非常强大的器官。
如果你喜欢这种感知事物的概念,尤其是让其他事物对这些感觉做出反应的概念,你将喜欢这一章。
感知模拟输入和连续值
没有比将其与数字比较更好的方法来定义模拟了。我们刚刚在上一章中讨论了数字输入,你现在很清楚这类输入可以读取的唯一两个值。写起来有点累人,我为此道歉,因为这确实更多的是处理器限制,而不是纯输入限制。顺便说一句,结果是数字输入只能向我们执行的二进制固件提供 0 或 1。
模拟的工作方式完全不同。确实,模拟输入可以通过测量从 0V 到 5V 的电压来连续提供可变值。这意味着 1.4V 和 4.9V 的值将被解释为完全不同的值。这与数字输入将它们解释为…1 的情况非常不同。确实,正如我们之前看到的,电压值大于 0 通常被数字输入理解为 1。0 被理解为 0,但 1.4 会被理解为 1;我们可以将其理解为 HIGH,即开启值,相对于来自 0V 测量的 OFF。
在这个连续的模拟输入世界中,我们可以感受到不同值之间的流动,而数字输入只能提供步骤。这就是我总是使用“感觉”这个术语的原因之一。是的,当你能测量很多值时,这几乎就是感觉和感知。这是对电子硬件的一点点人性化,我完全相信这一点。
我们可以区分多少个值?
“很多”这个术语并不精确。即使我们处于一个新的连续测量领域,我们仍然处于数字世界,即计算机的世界。那么 Arduino 的模拟输入可以区分多少个值呢?1024。
为什么是 1024?如果你理解了 Arduino 如何感知连续值,这个原因很容易理解。
因为 Arduino 的芯片在数字域进行所有计算,我们必须将 0V 到 5V 的模拟值转换为数字。内置芯片组中的模数转换器的目的正是如此。这个设备也被称为 ADC 的缩写。
Arduino 的 ADC 具有 10 位分辨率。这意味着每个模拟值都被编码并映射到一个 10 位的编码整数。使用这种编码系统可编码的最大数字是二进制的 1111111111,即十进制的 1023。如果我把第一个数字视为 0,我们就有 1024 个值表示。1024 值的分辨率提供了一个非常舒适的感知范围,正如我们将在下一页看到的那样。
让我们看看我们如何使用这些宝贵的输入与 Arduino 一起使用。
读取模拟输入
因为我们现在对电路和代码更熟悉了,我们可以在解释概念的同时进行一个小项目。我将描述一个仅使用电位器的简单电路和代码示例。
电位器的真正目的
首先,让我们拿一个电位器。如果你记得这本书的第一章,电位器是一个可变电阻。
考虑到欧姆定律,它将电压、电流和电阻值联系起来,我们可以理解,对于恒定电流,我们可以通过改变电位器的电阻值来改变电压。实际上,因为有些人多年没有翻阅我们的基础电子课程教科书,我们不妨复习一下?以下是欧姆定律:
V = R * I
在这里,V 是电压(伏特),R 是电阻(欧姆),I 是电流(安培)。
因此,现在,为了定义电位器的目的:
注意
电位器是你在运行代码中从物理世界连续改变变量的方法。
提示
始终记住:
使用 10 位分辨率,你将成为模拟输入的大师!
使用电位器改变 LED 的闪烁延迟
下图是说明 Arduino 板上模拟输入概念的最基本电路:

一个连接到 Arduino 板上的电位器
检查相应的电气图以了解连接:

模拟输入 0 正在测量电压
现在让我们看看我们必须使用的代码。
就像digitalRead()函数可以读取 Arduino 上的数字输入值一样,还有analogRead()用于读取模拟输入。
这里的目的是将值作为程序中的暂停值来读取,以控制 LED 的闪烁速率。在代码中,我们将使用delay()函数。
这里有一个例子:
int potPin = 0; // pin number where the potentiometer is connected
int ledPin = 13 ; // pin number of the on-board LED
int potValue = 0 ; // variable storing the voltage value measured at potPin pin
void setup() {
pinMode(ledPin, OUTPUT); // define ledPin pin as an output
}
void loop(){
potValue = analogRead(potPin); // read and store the read value at potPin pin
digitalWrite(ledPin, HIGH); // turn on the LED
delay(potValue); // pause the program during potValue millisecond
digitalWrite(ledPin, LOW); // turn off the LED
delay(potValue); // pause the program during potValue millisecond
}
上传代码。然后转动电位器一点,观察输出。
变量定义之后,我在setup()函数中将ledPin引脚定义为输出,以便能够驱动电流到这个引脚。实际上,我正在使用引脚 13 来简化我们的测试。别忘了引脚 13 是 Arduino 板上的表面贴装 LED。
然后,在loop()函数中发生神奇的事情。
我首先读取potPin引脚的值。正如我们之前讨论的,这个函数返回的值是一个介于 0 和 1023 之间的整数。我将它存储在potValue变量中,以保持 LED 开启,但也以保持 LED 关闭。
然后,我通过在状态变化之间设置一些延迟来打开和关闭 LED。这里聪明的地方是使用potValue作为延迟。完全打开一边时,电位计提供一个值为 0。完全打开另一边时,它提供一个 1023,这是一个合理且用户友好的毫秒延迟值。
值越高,延迟越长。
为了确保你理解了物理部分,我想再解释一下电压。
Arduino 的+5V 和地引脚为电位计提供电压。它的第三条腿提供了一种通过改变电阻来改变电压的方法。Arduino 的模拟输入能够读取这个电压。请注意,Arduino 上的模拟引脚仅是输入。这也是为什么,与数字引脚相比,我们不需要在代码中担心精度。
因此,让我们修改一下代码,以便读取电压值。
如何将 Arduino 变成低电压电压表?
测量电压需要一个电路上的两个不同点。确实,电压是一种电势。在这里,我们只有那个参与我们电路测量电压的模拟引脚。那是什么?!
简单!我们正在使用 Vcc 的+5V 电源作为参考。我们控制电位计提供的电阻,并从 Vcc 引脚供电,以便有所展示。
如果我们想将其用作真正的电位计,我们必须给电路的另一个部分也提供 Vcc,然后将我们的 A0 引脚连接到电路的另一个点。
正如我们所见,analogRead()函数只提供从 0 到 1023 的整数。我们如何将实际的电测量显示在某个地方?
这是它的工作原理:
范围 0 到 1023 映射到 0 到 5V。这是 Arduino 内置的。然后我们可以按照以下方式计算电压:
V = 5 * (analogRead()值 / 1023)
让我们实现它,并通过使用 Arduino IDE 的串行监视器将其显示在我们的计算机上:
int potPin = 0; // pin number where the potentiometer is connected
int ledPin = 13 ; // pin number of the on-board LED
int potValue = 0 ; // variable storing the voltage value measured at potPin pin
float voltageValue = 0.; // variable storing the voltage calculated
void setup() {
Serial.begin(9600);
pinMode(ledPin, OUTPUT); // define ledPin pin as an output
}
void loop(){
potValue = analogRead(potPin); // read and store the read value at potPin pin
digitalWrite(ledPin, HIGH); // turn on the LED
delay(potValue); // pause the program during potValue millisecond
digitalWrite(ledPin, LOW); // turn off the LED
delay(potValue); // pause the program during potValue millisecond
voltageValue = 5\. * (potValue / 1023.) ; // calculate the voltage
Serial.println(voltageValue); // write the voltage value an a carriage return
}
代码几乎与之前的代码相同。
我添加了一个变量来存储计算出的电压。我还添加了串行通信的内容,你总是能看到:Serial.begin(9600)实例化串行通信,Serial.println()将当前计算出的电压值写入串行通信端口,后面跟着一个换行符。
为了在你的电脑上看到结果,你必须当然打开串行监视器。然后,你可以读取电压值。
计算精度
请注意,我们在这里使用 ADC 是为了将模拟值转换为数字;然后,我们对这个数字值进行小计算,以获得电压值。与基本模拟电压控制器相比,这是一个非常昂贵的方法。
这意味着我们的精度取决于 ADC 本身,它具有 10 位的分辨率。这意味着我们只能在 0 V 和 5 V 之间有 1024 个值。5 除以 1024 等于 0.00488,这是一个近似值。
这基本上意味着我们无法区分像 2.01 V 和 2.01487 V 这样的值。然而,对于我们的学习目的来说,这应该足够精确。
再次强调,这是一个例子,因为我想向你指出精度/分辨率的概念。你必须了解并考虑它。它在某些情况下可能会证明非常重要,并可能产生奇怪的结果。至少,你已经得到了警告。
让我们探索另一种与 Arduino 板交互的巧妙方式。
介绍 Max 6,图形编程框架
现在,让我向你介绍一个名为 Max 6 的框架。这本身就是一个宇宙,但我想在本书中写一些关于它的内容,因为你在未来的项目中可能会遇到它;也许有一天你将成为像我一样的 Max 6 开发者,或者你可能需要将你的智能物理对象与基于 Max 6 的系统进行接口。
以下是我 3D 宇宙项目中的一个 Max 6 补丁:

Max/MSP 简史
Max 是一种用于多媒体目的的视觉编程语言。它实际上由 Cycling '74 开发和维护。为什么叫 Max?它是以 Max Matthews 的名字命名的(en.wikipedia.org/wiki/Max_Mathews),他是计算机音乐的大先驱之一。
Max 的原始版本是由 Miller Puckette 编写的;最初是一个名为 Patcher 的 Macintosh 编辑器。他在欧洲声学/音乐研究协调院(IRCAM)编写了它,这是一个位于法国巴黎蓬皮杜中心附近的前卫科学研究所。
1989 年,该软件由 IRCAM 许可给了一家私营公司 Opcode Systems,从那时起,它就由 David Zicarelli 开发和扩展。在 20 世纪 90 年代中期,Opcode Systems 停止了所有对该软件的开发。
Puckette 发布了一个完全免费和开源的 Max 版本,名为 Pure Data(通常简称为 Pd)。这个版本实际上被广泛使用,并由使用它的社区维护。
大约在 1997 年,一个专门用于声音处理和生成的模块被添加进来,命名为MSP,代表Max Signal Processing,显然也是为了纪念 Miller S. Puckette。
自 1999 年以来,通常被称为 Max/MSP 的框架由 Cycling '74 公司开发和发行,这是 Zicarelli 先生的公司的产品。
由于框架架构非常灵活,一些扩展逐渐被添加,例如 Jitter(一个巨大且高效的视觉合成)、Processing、实时矩阵计算模块,以及 3D 引擎。这发生在 2003 年左右。当时,Jitter 被发布并可以单独获取,但当然需要 Max。
2008 年,发布了名为 Max 5 的重大更新。这个版本也没有原生包含 Jitter,但作为一个附加模块。
在我谦卑的意见中,最大的升级,也就是 2011 年 11 月发布的 Max 6,它原生地包含了 Jitter,并提供了巨大的改进,例如:
-
重新设计的用户界面
-
兼容 64 位操作系统的新的音频引擎
-
高质量声音滤波器设计功能
-
新的数据结构
-
新的 3D 模型运动处理
-
新的 3D 材料处理
-
Gen 扩展
Max 4 已经完全可用且高效,但我在这里必须谈谈我对 Max 6 的看法。无论你需要构建什么,接口、复杂或简单的通信协议,包括基于 HID(HID=人机界面设备)的 USB 设备,如 Kinect、MIDI、OSC、串行、HTTP,以及其他任何东西,基于 3D 的声音引擎或 Windows 或 OS X 平台的基本独立应用程序,你都可以用 Max 6 来制作,而且这是一种安全的方式来构建。
这里是我自己与 Max 的简短历史:我亲自开始尝试 Max 4。我为我的第一个硬件 MIDI 控制器特别构建了一些宏 MIDI 接口,以便以非常具体的方式控制我的软件工具。它教会了我很多,并开阔了我的思路。我一直在使用它,几乎用于我艺术创作的每一个部分。
现在,让我们更深入地了解一下 Max 是什么。
全局概念
当然,我在上一节中犹豫是否开始介绍 Max 6 的部分。但我想这个小故事是描述框架本身的良好起点。
什么是图形编程框架?
图形编程框架是一种编程语言,它为用户提供了一种通过图形操作元素而不是通过键入文本来创建程序的方法。
通常,图形编程语言也被称为可视化编程语言,但我会使用“图形”,因为对许多人来说,“可视化”用于框架渲染的产品;我的意思是,例如 3D 场景。图形更相关于GUI,即图形用户界面,从开发者的角度来看,是我们的编辑器界面(我的意思是 IDE 部分)。
使用这种强大图形范式的框架包括许多编程方式,我们可以从中找到数据、数据类型、操作符和函数、输入和输出,以及连接硬件的方式。
你不是键入长源代码,而是添加对象并将它们连接起来以构建软件架构。想想 Tinker Toys 或乐高积木。
在 Max 的世界里,一个全球软件架构,即我们在 2D 屏幕上连接和相关的对象系统,被称为Patch。顺便提一下,其他图形化编程框架也使用这个术语。
如果一开始将这种范式理解为一种简化的方式,那么它并非首要目的,我的意思是,这不仅更容易,而且也为程序员和非程序员提供了全新的方法。它还提供了一种新的支持任务类型。实际上,如果我们编程的方式与修补不同,那么我们解决问题的方式也会不同。
我可以引用我们领域内的一些其他主要图形化编程软件:
-
Quartz Composer:这是一个针对 OS X 的图形渲染框架,可在
developer.apple.com/technologies/mac/graphics-and-animation.html找到。 -
Reaktor:这是由 Native Instruments 开发的一个 DSP 和 MIDI 处理框架,可在
www.native-instruments.com/#/en/products/producer/reaktor-5找到。 -
Usine:这是一个适用于现场和录音棚录音的通用音频软件,可在
www.sensomusic.com/usine找到。 -
vvvv:这是一个 Windows 的实时视频合成工具,可在
vvvv.org找到。 -
SynthMa****ker:这是一个为 Windows 设计的 VST 设备,可在
synthmaker.co.uk找到。
我想特别提一下 Usine。这是一个非常有趣且强大的框架,它提供了图形化编程来设计可在 Usine 软件内部使用或作为独立二进制文件使用的补丁。但其中一个特别强大的功能是,你可以将你的补丁导出为功能齐全且经过优化的 VST 插件。VST(虚拟工作室技术)是由 Steinberg 公司创建的一个强大的标准。它提供了一长串规范,并在几乎所有数字音频工作站中得到实现。Usine 提供了一个只需一键即可导出的功能,将你的图形化编程补丁打包成标准 VST 插件,这对于甚至没有听说过 Usine 或补丁风格的用户来说非常方便。Usine 独特的多点触控功能也使其成为一个非常强大的框架。然后,你甚至可以使用他们的 C++ SDK(软件开发工具包)来编写自己的模块。

Usine 大补丁连接现实世界与许多虚拟对象
Max,用于游乐场
Max 是游乐场和核心结构,所有内容都将放置在其中,进行调试和展示。这是放置对象、将它们连接起来、创建用户界面(UI)以及进行一些视觉渲染的地方。
这里有一个截图,展示了一个非常基本的补丁设计,旨在帮助你理解事物所在的位置:

一个使用 Max 6 的小型简单计算系统补丁
正如我描述的那样,使用图形编程框架,我们不需要输入代码来让事情发生。这里,我只是触发了一个计算。
内部带有数字17的盒子是一个 numbox。它包含一个整数,它也是一个 UI 对象,提供了一种通过拖放鼠标来改变值的方式。然后你将一个对象的输出连接到另一个对象的输入。现在当你改变值时,它将通过电线发送到连接到 numboxes 的对象。魔法!
你看到了另外两个对象。一个带有:
-
+符号后面跟着数字5
-
-符号后面跟着数字3
每个对象都接收发送给它们的数字,并分别进行+ 5 和- 3 的计算。
你可以看到另外两个 numboxes,它们基本上显示了带有+和–符号的对象发送的结果数字。
你还在吗?我猜是的。Max 6 提供了一个非常完善的帮助系统,其中包含了每个对象的全部引用,并且可以直接在 playground 中直接访问。当你教授这个框架时,告诉学生这一点是很好的,因为它真的有助于学生自学。确实,他们几乎可以自主地寻找答案,无论是关于小问题还是他们已经忘记但不敢问的事情。
Max 部分提供了一个相当高级的任务调度器,一些对象甚至可以修改优先级,例如,将defer和deferlow用于在您的补丁中实现优先级的精细粒度,例如,对于 UI 方面和计算核心方面,每个方面都需要非常不同的调度。
Max 还提供了一个方便的调试系统,它有一个类似于控制台的窗口,称为Max 窗口。

显示 expr 对象错误调试信息的 Max 窗口
Max 驱动很多事情。实际上,是 Max 拥有并领导了对所有模块的访问,无论是激活的还是未激活的,当你创建新对象时提供自动完成,还提供了访问许多可以扩展 Max 功能的东西,例如:
-
JavaScript API 用于 Max 本身以及特定部分,例如 Jitter
-
通过 mxj 对象在 Max 6 中直接实例化 Java 类
-
MSP 核心引擎用于与信号速率相关的一切,包括音频
-
Jitter 核心引擎用于与矩阵处理相关的一切,以及更多,例如视觉和视频
-
Gen 引擎用于从补丁中直接进行高效和即时的代码编译
这不是一个详尽的列表,但它让你了解了 Max 提供了什么。
让我们检查其他模块。
MSP,用于声音
在 Max 对象通过用户或调度器本身触发的消息进行通信时,MSP 是核心引擎,它在任何特定时刻计算信号,正如文档中所写。
即使我们可以像纯 Max 对象一样连接 MSP 对象,但背后的概念是不同的。在每一个时刻,都会计算一个信号元素,通过我们所说的信号网络形成一个几乎连续的数据流。信号网络在补丁窗口中很容易识别;线缆是不同的。
这里有一张非常简单的补丁图,在你的耳朵里产生基于余弦的音频波:

事实上,甚至补丁线也有不同的外观,展现出酷炫的条纹状黄黑色,类似蜜蜂的颜色,MSP 对象的名称后面包含一个波浪线 ~ 作为后缀,象征着……当然是一波!
信号速率由音频采样率和 MSP 核心设置窗口中的某些暗参数驱动。我不会描述这些,但你需要知道,Max 通常默认提供与你的声卡相关的参数,包括采样率(44110 Hz,音频 CD 的标准采样率,意味着每个音频通道每秒以 44100 次的速度进行快速处理)。

音频状态窗口是设置一些重要 MSP 参数的地方
Jitter,用于视觉效果
Jitter 是 Max 6 中与视觉处理和合成相关的所有事物的核心引擎。
它提供了一个非常高效的矩阵处理框架,最初是为快速像素值计算而设计的,用于显示图片,无论是有动画的还是没有的。
我们在谈论与 Jitter 处理矩阵相关的一切的矩阵计算。实际上,如果你需要在 Max 6 中触发快速计算大量数组,即使你不需要显示任何视觉效果,你也可以使用 Jitter 来做这件事。
Jitter 提供的不仅仅是矩阵计算。它提供了对 OpenGL (en.wikipedia.org/wiki/OpenGL) 实现的完全访问,该实现以光速运行。它还提供了一种设计和处理粒子系统、3D 世界、OpenGL 材质和基于物理的动画的方法。像素处理也是它提供的许多专为像素处理本身设计和优化的对象所具有的强大功能之一。

基于 Jitter 核心的基本补丁生成一个分辨率良好的 400x400 噪声像素图
为了总结这大量信息,Max 安排事件或等待用户触发某些操作,一旦激活,MSP(用于音频信号处理)——在它的信号网络中的每一个瞬间计算信号元素,而 Jitter 在 Jitter 对象被bangs触发时处理计算。
事实上,Jitter 对象需要被触发才能执行它们的工作,这些工作可能非常不同,例如弹出包含像素颜色值的矩阵,对矩阵的每个单元格进行矩阵处理,然后弹出结果矩阵,例如。
触发信号是特殊消息,用来对对象说“嘿,让我们开始你的工作!”。Max 中的对象可以有不同的行为,但几乎每个对象都可以理解触发信号。
在 Patch003(如图中所示的前一个屏幕截图),Max 对象 qmetro 每隔 20 毫秒从低优先级调度队列向名为 jit.noise 的 Jitter 对象发送一个触发信号。这个后者的对象计算出一个矩阵,每个单元格中填充随机值。然后,结果通过一条新的绿色和黑色条纹的补丁线到一个 UI 对象,我们可以看到一个名称,jit.pwindow,这是一种可以包含在我们的补丁中的显示方式。
通过强大的 Java 和 JavaScript API,可以控制抖动,这对于需要在代码中编写大循环的任务来说,使用代码设计起来很容易。
还在这里吗?
对于最勇敢的勇士们,关于 Gen 的其他一些信息,这是 Max 6 中最新且最有效的模块。
Gen,对于代码生成的新方法
如果你理解在我们的补丁背后存在一种编译/执行过程,那么我会让你失望地说,它实际上并不像那样工作。即使一切都可以实时工作,也没有真正的编译。
顺便说一下,有许多方法可以使用代码设计补丁位,例如使用 JavaScript。直接在 Max 补丁器内部,你可以创建一个 .js 对象,并将你的 JavaScript 代码放入其中;它确实是即时编译的(它被称为 JS JIT 编译器,即 JavaScript 即时编译器)。它真的很快。相信我,我测试了很多,并与许多其他框架进行了比较。所以,正如文档所说,“我们不仅限于用 C 语言编写 Max 外部插件”,即使使用 Max 6 SDK 完全可能(cycling74.com/products/sdk)。
Gen 是一个全新的概念。
Gen 提供了一种在补丁上即时编译补丁位的方法,这是从你的补丁中进行的真正编译。它提供了一种具有特定对象的新的补丁类型,与 Max 对象非常相似。
它适用于 MSP,使用 gen~ Max 对象,提供了一种设计与音频补丁架构相关的信号速率的整洁方式。你可以设计这样的 DSP 和声音发生器。gen~ 补丁就像是对时间的放大;你必须把它们视为样本处理器。每个样本都在 gen~ 补丁器内部由这些补丁处理。当然,有智能对象可以随时间累积事物,以便拥有信号处理的时间窗口。
它也适用于 Jitter,有三个主要的 Max 对象:
-
jit.gen是快速矩阵处理器,在每个循环中处理矩阵的每个单元格。 -
jit.pix是基于 CPU 的像素处理器,处理像素图中的每个像素。 -
jit.gl.pix是jit.pix的基于 GPU 的版本。
GPU(图形处理器单元),基本上是你显卡上的一个专用图形处理器。通常,这是一个完全不同的领域,OpenGL 管道提供了从软件定义到屏幕显示之前修改像素的简单方法。这被称为着色器过程。
你可能已经知道这个术语与游戏世界有关。这些是那些在我们的游戏中也是改善图形和视觉渲染的最后一步的着色器。
着色器基本上是可以在 GPU 本身处理的参数传递中即时修改的小程序。这些小程序使用特定的语言,并在我们的显卡上的专用处理器上运行得非常快。
Max 6 + Gen 通过仅补丁即可直接访问管道的这一部分;如果我们不想基于 OpenGL GLSL (www.opengl.org/documentation/glsl)、Microsoft DirectX HLSL (msdn.microsoft.com/en-us/library/bb509635(v=VS.).aspx) 或 Nvidia Cg (http.developer.nvidia.com/CgTutorial/cg_tutorial_chapter01.html) 编写着色器,Gen 就是你的朋友。
所有基于 jit.gl.pix 的补丁都是专门编译并用于基于 GPU 的执行的。
你可以通过补丁来设计自己的片段着色器(或像素着色器),甚至可以抓取 GLSL 或 WebGL 语言中的源代码,以便在其他框架中使用。
使用 Gen 无法使用几何着色器,但与其他 Jitter 对象一起,它们已经存在。
我猜我可能让一些人感到困惑了。放松,我不会在 Arduino 考试中问你关于 Gen 的问题!
将所有内容总结在一个表格中
与 Max 6 相关的一切信息都可以在 Cycling 74 的网站上找到,网址是 cycling74.com。此外,几乎 99% 的文档也是在线的,可以在 cycling74.com/docs/max6/dynamic/c74_docs.html#docintro 找到。
以下表格总结了到目前为止我们所做的一切:
| 部分 | 是什么? | 电缆颜色 | 特征标志 |
|---|---|---|---|
| Max | 操场 | 默认为灰色,没有条纹 | 基本名称 |
| MSP | 与音频和信号速率相关的一切 | 黄色和黑色条纹 | 命名后缀为 ~,表示信号速率处理 |
| Jitter | 与视觉和矩阵相关的一切 | 矩阵电缆为绿色和黑色条纹 | 命名前缀为 jit. |
| Gen | 在线编译的特定补丁(与 DSP 相关以及矩阵和纹理处理) | 类似于 MSP 的 gen~ 和 Jitter 的 jit.pix、jit.gl.pix |
非常非常快! |
安装 Max 6
Max 6 作为一个 30 天的试用版可用。安装 Max 6 相当简单,因为它提供了 Windows 和 OS X 平台的安装程序,可在cycling74.com/downloads下载。下载并安装它。然后,启动它。就这样。(以下示例只有在安装了 Max 之后才会工作。)
你应该看到一个空白的游乐场

Max 6 的空白页面焦虑可能现在就会发生,不是吗?
第一个补丁
这里有一个基本的补丁,你可以在Chapter06/文件夹下以Patcher004_Arduino.maxpat的名称找到它。通常,如果你双击它,它会被 Max 6 直接打开。
这个补丁是一个非常基本的补丁,但实际上并不那么简单!
这是一个基本的基于噪声的序列发生器,它实时地定期修改振荡器的频率。这会产生一系列奇怪的声音,或多或少有点漂亮,频率的改变是由随机控制的。所以,打开你的扬声器,补丁将会产生声音。

基于噪声的序列发生器
基本上,补丁是存储在文件中的。你可以非常容易地与其他朋友分享补丁。当然,更大的项目可能会涉及一些依赖性问题;如果你向 Max 6 框架中添加了一些库,如果你在补丁中使用它们,或者如果你基本上将补丁文件发送给一个没有安装这些库的朋友,你的朋友在 Max 窗口中将会出现一些错误。我不会在这里描述这类问题,但我想要提醒你。
在 Max 6 的世界中,分享补丁的其他整洁方式是复制/粘贴和复制压缩功能。确实,如果你在补丁器中选择对象(无论层次,包括子补丁器,子补丁器内的子补丁器,等等),然后转到编辑 | 复制,基于文本的内容就会被放入你的剪贴板。然后你可以将其粘贴到另一个补丁器或文本文件中。
最聪明的办法是使用复制压缩功能,正如其名字所暗示的,它复制并压缩 JSON 代码,使其变得更加紧凑,更容易复制到论坛上的文本区域,例如。
等一下,让我给你看看它是什么样子。
我只是选择了补丁中的所有对象,然后转到编辑 | 复制压缩。

复制压缩功能
以下图是直接粘贴到文本文件中的结果。
熟悉 HTML 的人可能会注意到一些有趣的地方;Cycling '74 的开发者在 HTML 标签(pre和code)中包含了两项,以便直接提供可以在(任何)网络论坛上的文本字段中粘贴的代码。

复制压缩代码
因此,你也可以将那段代码复制到你的剪贴板,并将其粘贴到一个新的补丁中。你可以通过访问文件 | 新建(或者在 Windows 上按Ctrl + N,在 OS X 上按command + N)来创建一个新的空补丁。
使用补丁播放声音
如你所见,我在补丁中添加了一些注释。你可以按照它们来产生一些来自你电脑的电子声音。
在开始之前,请确保通过点击左下角的锁形图标锁定补丁。要听到补丁的结果,你还需要点击扬声器图标。要缩小视图,请转到 视图 菜单并点击 缩小。
首先,注意并检查顶部的 toggle。它将发送值 1 到连接的对象 metro。
Metro 是一个纯 Max 对象,每 n 毫秒发送一个 bang 信号。这里,我硬编码了一个参数:100。一旦 metro 收到来自 toggle 的消息 1,它就开始活跃,并遵循 Max 定时调度器,每隔 100 毫秒向下一个连接的对象发送 bang 信号。
当 random 对象接收到一个 bang 信号时,它会从指定范围内弹出一个随机整数。这里,我设置了 128,这意味着 random 将发送 0 到 127 的值。紧接着 random,我放置了一个 zmap 对象,它像一个缩放器。我硬编码了四个参数,即输入的最小值和最大值以及输出的最小值和最大值。
基本上,在这里,zmap 将 random 发送的值 0 到 127 映射到 20 到 100 的另一个值。它产生了一种隐式的拉伸和分辨率损失,这是我喜欢的。
然后,这个结果数值被发送到著名的且重要的 mtof 对象。它将 MIDI 音高标准转换为根据 MIDI 标准的频率。它通常用于从 MIDI 世界进入真实声音世界。你还可以在显示频率为浮点数(赫兹,频率的度量单位)的 UI 对象 flonum 中读取频率。
最后,这个频率被发送到 cycle~ 对象,产生一个信号(检查黄色和黑色条纹的线)。向这个对象发送数字会使其改变产生的信号的频率。这个信号乘以一个信号乘法运算符 *~,产生另一个信号,但幅度更低,以保护我们宝贵的耳朵。
该信号的最后一个目的地是你必须点击一次才能听到或听不到由上面的信号网络产生的声音的大灰色框。
现在,你可以准备检查复选框了。通过点击灰色框激活扬声器图标,然后你可以开始跳舞。实际上,产生的电子声音在频率(即音符)上有些混乱,但可能会很有趣。
当然,使用 Arduino 控制这个便宜的补丁,以便不使用鼠标/光标,将会非常棒。
让我们使用之前设计的相同电路来做这件事。
使用硬件控制软件
来自纯数字领域,其中一切都可以封装到软件和虚拟世界中,我们经常需要物理接口。这听起来可能像是一个悖论;我们希望一切都在一个地方,但那个地方对于与纯创造和情感相关的一切来说都太小,不够友好,因此我们需要更多或更少的大的外部(物理)接口。我喜欢这个悖论。
但是,为什么我们需要这样的接口呢?有时,旧鼠标和 QWERTY 键盘就不够用了。我们的电脑很快,但这些控制我们程序的接口却很慢,很笨拙。
我们需要在现实世界和虚拟世界之间建立接口。无论它们是什么,我们都需要它们专注于我们的最终目的,这通常不是接口,甚至不是软件本身。
亲自来说,我写书并教授与艺术相关的技术课程,但作为一个现场表演者,我需要专注于最终的渲染。在表演时,我希望尽可能地黑盒化底下的技术。我想要感受,而不是计算。我需要一个控制器接口来帮助我在速度和灵活性上操作,以便进行我想要的类型的变化。
正如我在这本书中已经说过的,我需要一个巨大的 MIDI 控制器,沉重、坚固且复杂,才能控制我电脑上的一个软件。因此,我建造了 Protodeck (julienbayle.net/protodeck)). 这就是我的接口。
那么,我们如何使用 Arduino 来控制软件呢?我想你已经有了一部分答案,因为我们已经通过旋转电位器将数据发送到我们的电脑。
让我们改进我们的 Max 6 补丁,使其在旋转电位器时接收 Arduino 的数据。
改进序列器和连接 Arduino
我们将创建一个非常便宜和基础的项目,该项目将涉及我们的 Arduino 板作为一个小型声音控制器。实际上,我们将直接使用我们刚刚设计的带有电位器的固件,然后我们将修改我们的补丁。这对于你继续构建事物甚至创建更大的控制器机器非常有用。
让我们连接 Arduino 到 Max 6
Arduino 可以使用串行协议进行通信。我们已经做到了。我们的最新固件已经做到了,发送电压值。
让我们稍作修改,使其只发送读取的模拟值,范围在 0 到 1023 之间。以下是代码,可在 Chapter06/maxController 中找到:
int potPin = 0; // pin number where the potentiometer is connected
int potValue = 0 ; // variable storing the voltage value measured at potPin pin
void setup() {
Serial.begin(9600);
}
void loop(){
potValue = analogRead(potPin); // read and store the read value at potPin pin
Serial.println(potValue); // write the voltage value an a carriage return
delay(2); // this small break waits for the ADC to stabilize is often used
}
我移除了所有不必要的部分,并在循环末尾(在循环重新开始之前)添加了 2 毫秒的延迟。这通常与模拟输入和特别是 ADC 一起使用。它提供了一个中断,让它稳定一会儿。我在之前的涉及模拟读取的代码中没有这样做,因为那里已经有两个 delay() 方法涉及 LED 闪烁。
这个基本版本发送连接到电位器的模拟输入引脚上读取的值。不多,也不少。
现在,让我们学习如何在除了我们宝贵的 IDE 的串行监视器之外的某个地方接收这些数据。
Max 6 中的串行对象
Max 中有一个名为 serial 的对象。它提供了一种使用串行端口与其他任何使用串行通信的设备进行通信的方式。
下一个图描述了新的 Max 6 补丁,包括与我们的小型硬件控制器通信所需的部件。
现在,如果还没有这样做,请将 Arduino 插入,并上传 maxController 固件。
注意
注意关闭 IDE 的串行监控。
否则,你的电脑上会有冲突;一个端口上只能实例化一个串行通信。
然后这里还有一个你可以找到的补丁,也在 Chapter06/ 文件夹中,名为 Patcher005_Arduino.maxpat。

包含 Arduino 通信模块的 Max 补丁
双击文件,你会看到这个补丁。
让我们稍微描述一下。我添加了所有绿色和橙色的内容。
理解 Arduino 消息并将其转换为我们的序列器补丁易于理解的所有必要内容都在绿色部分。一些非常有用的辅助工具,能够在数据流中的每个步骤写入 Max 窗口,从原始数据到转换后的数据,都在橙色部分。
让我们描述这两部分,从辅助部分开始。
在 Max 6 中轻松追踪和调试
Max 6 提供了许多调试和追踪的方法。我不会在这本 Arduino 书中描述所有这些,但其中一些需要几句话说明。
检查你的补丁,特别是橙色部分的对象。
print 对象是直接向 Max 窗口发送消息的方式。一旦收到,发送给它们的任何内容都会立即写入 Max 窗口。你可以传递给这些对象的参数也非常有用;它有助于在您使用多个 print 对象的情况下区分哪个 print 对象发送了什么。这里就是这种情况,检查一下:我根据消息来源的对象命名所有的 print 对象:
-
fromSerial:这是针对来自serial对象自身的所有消息 -
fromZl:这是针对来自zl对象的所有消息 -
fromitoa:这是针对来自itoa对象的所有消息 -
fromLastStep:这是针对来自fromsymbol对象的所有消息
gate 对象只是小门,我们可以通过发送 1 或 0 到最左侧的输入来启用或禁用它们。toggle 对象是很好的 UI 对象,可以通过点击来实现这一点。一旦你勾选了 toggle,相关的 gate 对象将允许发送到右侧输入的消息通过它们传递到唯一的输出。
我们将在几分钟内使用这个追踪系统。
在 Max 6 中理解 Arduino 消息
需要理解的是,之前的切换现在也连接到了一个新的 qmetro 对象。这是低优先级的 metro 对应物。实际上,这个对象将每 20 毫秒轮询 serial 对象,考虑到我们的 Arduino 固件当前通过在循环的每次迭代中发送读取的模拟值来工作,即使轮询有点延迟,也不会有问题;下一次迭代,更新将会发生。
serial 对象在这里非常重要。
我硬编码了一些与 Arduino 串行通信相关的参数:
-
9600设置时钟为 9600 波特 -
8设置字长为 8 位 -
1表示有一个停止位 -
0表示没有奇偶校验(奇偶校验有时在错误检查中很有用)
这个对象需要被 bang 以提供串行端口缓冲区的当前内容。这就是为什么我用 qmetro 对象给它提供数据的原因。
serial 对象会弹出一系列原始值。在读取发送的模拟值之前,这些值需要被稍微解析和组织。这就是 select、zl、itoa 和 fromsymbol 对象的作用。
注意
通过按键盘上的 Alt 键然后点击对象,直接读取 Max 6 中任何对象的帮助信息。

串行对象的帮助补丁
每 20 毫秒,如果串行通信已经实例化,serial 对象将提供 Arduino 将要发送的内容,即连接到电位器的引脚上当前和最近读取的模拟值。这个值从 0 到 1023,我使用 scale 对象,就像我在补丁的序列/声音部分使用 zmap 对象一样。这个 scale 对象将输入的 0 到 1023 的值范围重新映射为 300 到 20 的反转范围,使范围反向(请注意,当前和未来的 Max 补丁,zmap 不像这样)。我这样做是为了定义每分钟音符的最大范围。expr 对象计算这个值。qmetro 需要两个 bang 之间的间隔。当我转动电位器时,我让这个间隔在 400 毫秒和 20 毫秒之间变化。然后,我计算每分钟音符速率,并在另一个 flonum UI 对象中显示它。
然后,我还添加了这个奇怪的 loadbang 对象和 print 对象。loadbang 是一个特定的对象,当 Max 6 打开补丁时,它会立即发送一个 bang。它通常用于初始化我们补丁内部的一些变量,有点像我们在 Arduino 脚本的第一行中进行的声明。
print 是在名为 message 的对象内的文本。通常,每个 Max 6 对象都可以理解特定的消息。你可以在补丁的任何地方键入 m 来创建一个新的空消息。然后,通过选择它并再次点击它,你可以使用自动完成功能填充文本。
在这里,一旦补丁加载并开始运行,serial对象就会接收到由loadbang触发的打印消息。serial对象能够将所有串行端口消息列表发送到运行补丁的计算机的终端(即 Max 窗口)。这发生在我们向它发送打印消息时。检查显示Patcher005_Arduino.maxpat补丁的 Max 窗口。
我们可以看到一系列事物。serial弹出一个串行端口字母缩写列表,对应的串行端口通常表示硬件名称。在这里,正如我们在 Arduino IDE 中已经看到的,对应于 Arduino 的是usbmodemfa131。
Max 中对应的引用是我电脑上的字母c。这仅是一个内部引用。

发送到串行对象的打印消息的结果:端口字母/串行端口的名称列表
让我们更改在补丁中作为serial对象参数的硬编码字母。
选择serial对象。然后,在内部重新单击并交换a与您计算机上 Arduino 串行端口的对应字母。一旦您按下Enter,对象就会以新的参数重新实例化。

将序列对象中的参考字母更改为与 Arduino 的串行端口对应的字母
现在,一切准备就绪。检查切换,启用带有扬声器的灰色框,并转动您的电位器。您将听到来自序列器的奇怪噪音,现在您可以更改音符速率(我的意思是每个声音之间的间隔),因为我滥用术语音符以更好地适应序列器的通常定义。
究竟在电线上发送了什么?
您可能已经注意到,像往常一样,我提到了一系列对象:select、zl、itoa和fromsymbol。现在是时候解释它们了。
当您在 Arduino 固件源代码中使用Serial.println()函数时,Arduino 不仅发送函数传递的参数值。检查一系列切换/门系统顶部的第一个橙色切换。

串行对象弹出一系列奇怪的数字
您可以在名为对象的第一列中看到打印对象的名称,在消息列中,可以看到相关对象发送的消息。我们还可以看到serial对象以重复的方式弹出一系列奇怪的数字:51、53、48、13、10,等等。
注意
Arduino 以 ASCII 码的形式发送其值,就像我们在计算机上键入它们一样。
这非常重要。让我们检查附录 E,ASCII 表,以找到相应的字符:
-
51 表示字符 3
-
53 表示 5
-
48 表示 0
-
13 表示回车
-
10 表示换行,它本身意味着新的一行
当然,我在排序序列时有点作弊。我知道 10 13 这一对数字。这是一个常用的标记,意味着 一个回车符后跟一个换行符。
因此,我的 Arduino 发送了一条类似这样的消息:
350<CR><LF>
这里,<CR> 和 <LF> 分别代表回车符和换行符。
如果我使用了 Serial.print() 函数而不是 Serial.println(),我就不会得到相同的结果。实际上,Serial.print() 版本不会在消息末尾添加 <CR> 和 <NL> 字符。如果没有结束标记,我怎么知道 3、5 或 0 将会是第一个字符呢?
需要记住的设计模式如下:
-
构建消息
-
在消息完全构建后发送消息(使用
Serial.println()函数)。
如果你想在构建过程中发送它,这里是你可以使用的方法:
-
使用
Serial.print()发送第一个字节 -
使用
Serial.print()发送第二个字节 -
继续发送直到结束
-
使用不带参数的
Serial.println()在末尾发送<CR><LF>
仅提取有效载荷?
在许多与通信相关的领域,我们谈论有效载荷。这是消息,通信本身的目的。其他所有东西都非常重要,但可以理解为载体;没有这些信号和信号量,消息无法传播。然而,我们感兴趣的是消息本身。
我们需要解析来自串行对象的消息。
我们必须将每个 ASCII 码累积到同一个消息中,当我们检测到 <CR><LF> 序列时,我们必须弹出消息块,然后重新开始这个过程。
这是通过 select 和 zl 对象完成的。
select 能够检测与其参数相等的消息。当 select 10 13 接收到一个 10 时,它将向第一个输出发送一个 bang。如果是 13,它将向第二个输出发送一个 bang。然后,如果收到其他任何消息,它将只从最后一个输出传递到右边。
zl 是一个如此强大的列表处理器,具有如此多的使用场景,以至于它可以单独构成一本书!使用参数运算符,我们甚至可以用它来解析数据,将列表切割成片段,等等。在这里,使用组 4 参数,zl 接收一个初始消息并将其存储;当它接收到第二个消息时,它存储该消息,依此类推,直到第四个消息。在接收到这个消息的精确时刻,它将发送一个由接收并存储的四个消息组成的大消息。然后,它清除其内存。
在这里,如果我们检查相应的切换并观察 Max 窗口,我们可以看到 51 53 48 被重复几次,并由 zl 对象发送。
zl 对象做得很好;它传递所有 ASCII 字符,除了 <CR> 和 <LF>,并且一旦它接收到 <LF>,zl 就发送一个 bang。我们刚刚构建了一个消息处理器,每次它接收到 <LF> 时都会 重置 zl 缓冲区,也就是说,当一条新消息即将发送时。

zl 列表处理器会弹出一系列整数
ASCII 转换和符号
我们现在有一系列三个整数,它们直接等于 Arduino 发送的 ASCII 消息,在我的情况下,是51 53 48。
如果你旋转电位器,当然会改变这个系列。
但是看看这个,我们期望的 0 到 1023 之间的值在哪里?我们必须将 ASCII 整数消息转换为实际的字符。这可以通过使用itoa对象(表示整数到 ASCII)来完成。
检查相关的切换,并观察 Max 窗口。

这里是我们的重要值
这个值是重要的;它是 Arduino 通过电线发送的消息,并以符号的形式传输。你无法在 Max 窗口中区分符号和其他类型的消息,如整数或浮点数。
我在补丁中放置了两个空消息。这些对于调试目的也非常有用。我将它们连接到右侧的itoa和fromsymbol对象。每次你向右侧输入的消息发送消息时,目标消息的值就会通过另一个消息的内容而改变。然后我们可以显示itoa和fromsymbol实际发送的消息。

"350"并不完全等于 350
fromsymbol将每个符号转换为它的组成部分,在这里它组成一个整数,350。
这个最终值是我们可以用任何能够理解和处理数字的对象使用的。这个值通过比例对象进行缩放,最后发送到 metro 对象。旋转电位器会改变发送的值,根据这个值,metro 会更快或更慢地发送 bang。
这个长例子教会了你两件主要的事情:
-
你必须仔细了解发送和接收的内容
-
Arduino 的通信方式
现在,让我们继续探讨一些与模拟输入相关的一些其他示例。
与传感器玩耍
我不想在这本书中写一个大的目录。相反,我想给你提供钥匙和所有概念的感觉。当然,我们必须精确,并了解你没有发明过的特定技术,但我特别想让你学习最佳实践,自己思考大型项目,并能够有一个全局的视角。
我在这里给你举一些例子,但不会涵盖之前提到的所有类型的传感器。
测量距离
当我为他人或自己设计安装时,我经常有测量移动物体与固定点之间距离的想法。想象一下,你想要创建一个系统,其光线强度根据一些访客的接近程度而变化。
我曾经玩过一个 Sharp GP2Y0A02YK 红外长距离传感器。

红外 Sharp GP2Y0A 系列传感器
这个酷炫的模拟传感器对于 20 到 150 厘米的距离提供了良好的结果。市场上还有其他类型的传感器,但我喜欢这个,因为它很稳定。
与任何距离传感器一样,目标/主题理论上必须垂直于红外光束的方向,以获得最大精度,但在现实世界中,即使不是这样也能正常工作。
数据表是首先要关注的对象。
阅读数据表?
首先,你必须找到数据表。搜索引擎可以帮上大忙。这个传感器的数据表在sharp-world.com/products/devvice/lineup/data/pdf/datasheet/gp2y0a02_e.pdf。
你不必理解一切。我知道有些人会在这里责怪我没有解释数据表,但我想让我的学生对此放松。你必须过滤信息。
准备好了吗?让我们开始吧!
通常,在第一页上,你可以看到所有功能的总结。
在这里,我们可以看到这个传感器似乎在目标颜色方面相当独立。好的,很好。距离输出类型在这里非常重要。实际上,这意味着它直接输出距离,不需要额外的电路来利用其模拟数据输出。
常常有一些传感器所有尺寸的轮廓图。如果你想在订购之前确保传感器适合你的盒子或安装,这可能会非常有用。
在下一张图中,我们可以看到一个图表。这是一条曲线,说明了输出电压如何根据目标距离变化。

传感器距离与模拟输出电压之间的数学关系
这些信息非常宝贵。确实,正如我们在上一章讨论的那样,传感器将一个物理参数转换成 Arduino(或任何其他类型的设备)可测量的东西。在这里,距离被转换成电压。
因为我们要用 Arduino 板上的模拟输入来测量电压,所以我们需要了解转换是如何工作的。在这里,我将使用一个捷径,因为我已经为你做了计算。
基本上,我使用了另一个与我们看到的类似的图表,但它是通过数学生成的。我们需要一个公式来编写我们的固件。
如果输出电压增加,距离会按照一种指数函数减少。我曾在某个时候与一些夏普工程师联系过,他们证实了我的关于公式的想法,并给了我这个:

在这里,D 是厘米距离,V 是测量的电压;a = 0.008271,b = 939.65,c = -3.398,d = 17.339
这个公式将被包含在 Arduino 的逻辑中,以便它可以直接向任何想知道它的人提供距离。我们也可以在通信链的另一方进行这个计算,例如在 Max 6 补丁中,或者在 Processing 中。无论如何,你想要确保你的距离参数数据在比较传感器输出和将使用该数据输入时能够很好地缩放。
让我们连接东西
下一个电路会让你想起之前的那个。实际上,范围传感器替换了电位器,但它是以完全相同的方式连接的:
-
Arduino 板上的 Vcc 和地分别连接到+5 V 和地
-
连接到模拟输入 0 的信号引脚

连接到 Arduino 板上的 Sharp 传感器
电路图如下:

Arduino 本身提供的传感器范围和发送电压到模拟输入 0
编写固件
以下是我设计的固件代码:
int sensorPin = 0; // pin number where the SHARP GP2Y0A02YK is connected
int sensorValue = 0
int distanceCalculated = 0; // variable storing the distance calculated
int v = 0; // variable storing the calculated voltage
// our formula's constants
const int a = 0.008271;
const int b = 939.65;
const int c = -3.398;
const int d = 17.339;
void setup() {
Serial.begin(9600);
}
void loop(){
sensorValue = analogRead(sensorPin);
v = 5\. * (sensorValue / 1023.) ; // calculate the voltage
distanceCalculated = ((a + b * v) / (1\. + c * v + d * v * v) );
Serial.println(distanceCalculated);
delay(2);
}
知道你理解了每一行代码,是不是很令人欣慰?不过,以防万一,我将提供一个简短的说明。
我需要一些变量来存储从 ADC 来的传感器值(即从 0 到 1023 的值)。然后,我需要存储从传感器值计算出的电压,当然,还有从电压值计算出的距离。
我只在 setup() 函数中初始化串行通信。然后,我在 loop() 方法中进行所有计算。
我首先读取从传感器引脚测量的当前 ADC 值和编码值。我使用这个值来计算电压,使用我们在之前的固件中已经使用过的公式。然后,我将这个电压值注入到 Sharp 传感器的公式中,我就得到了距离。
最后,我通过串行通信使用 Serial.println() 函数发送计算出的距离。
在 Max 6 中读取距离
Patcher006_Arduino.maxpat 是与这个距离测量项目相关的补丁。这里就是它:

距离读取补丁
正如我们之前学到的,这个补丁包含了读取来自 Arduino 板的消息的整个设计模式。
这里唯一的新奇之处是底部的奇怪 UI 元素。它被称为滑块。通常,滑块用于控制事物。确实,当你点击并拖动滑块对象时,它会弹出值。它看起来像调音台或调光器的滑块,可以控制某些参数。
显然,因为我想要在这里传输大量数据,所以我使用这个滑块对象作为显示设备,而不是控制设备。实际上,滑块对象也有一个输入端口。如果你向滑块发送一个数字,滑块会接受它并更新其内部当前值;它也会传输接收到的值。我这里只使用它作为显示。
Max 6 中的每个对象都有其自己的参数。当然,很多参数对所有对象都是通用的,但也有一些不是。为了检查这些参数:
-
选择对象
-
通过选择检查器选项卡或在 Windows 上按Ctrl + I或在 OS X 上按command + I来检查检查器
![在 Max 6 中读取距离]()
显示所选滑块对象属性和属性的检查器窗口
我不会描述所有参数,只描述底部的两个。为了产生相关结果,我必须将来自fromsymbol对象的值进行缩放。我知道 Arduino 传输的值范围(尽管这可能需要一些个人验证),我已经从 Sharp 数据表中计算了它们。我将这个范围视为 20 到 150 厘米。我的意思是 20 到 150 之间的一个数字。
我将这个范围进行了压缩和转换,使用scale对象将其转换为浮点数的0-to-100范围。我为我的滑块对象选择了相同的范围。这样做,滑块显示的结果是一致的,并代表真实值。
我没有在滑块上写任何增量标记,只做了两个注释:“近”和“远”。在这个数字的世界里,这有点诗意。
让我们看看其他一些能够弹出连续电压变化的传感器的例子。
测量弯曲
柔性传感器也非常有用。在距离传感器能够将测量的距离转换为电压的地方,柔性传感器测量弯曲并提供电压。
基本上,设备的弯曲与一个能够根据弯曲量使电压变化的可变电阻相关。

只有两个连接器的标准柔性传感器
柔性传感器可用于许多用途。
我喜欢用它通过 Arduino 通知计算机我设计的数字安装中的门位置。最初人们只想知道门是打开还是关闭,但我提出使用柔性传感器,并获得了关于开启角度的非常准确的信息。
下图说明了传感器的工作原理:

现在,我将直接给你看用 Fritzing 再次制作的接线图:

柔性传感器连接到 Arduino 板上的下拉电阻
我添加了一个下拉电阻。如果你还没有阅读关于上拉和下拉电阻的第五章,使用数字输入进行感应,我建议你现在去阅读。
通常,我使用大约 10K Ω的电阻,它们工作得很好。
电路图如下所示:

柔性传感器及其下拉电阻连接到 Arduino
电阻计算
对于这个项目,我不会给你代码,因为它与上一个项目非常相似,只是计算公式不同。我想在这里讨论的是这些电阻计算公式。
如果我们没有 Sharp 公司慷慨提供的红外传感器图,我们该怎么办?我们必须求助于一些计算。
通常,柔性传感器文档提供了当它未弯曲和当它弯曲到 90 度时的电阻值。让我们假设一些常见的值,如 10K Ω和 20K Ω,分别。
对于这些电阻值,包括下拉电阻,我们可以期望的电压值是什么?
考虑到电气原理图,模拟引脚 0 的电压是:

如果我们选择与未弯曲时的柔性电阻相同的下拉电阻,我们可以期望电压按照这个公式变化:

显然,通过在未弯曲时使用相同的公式,我们可以期望:

这意味着我们找到了我们的电压值范围。
我们现在可以将这些数据转换为数字 10 位,编码值,我的意思是 Arduino 的 ADC 著名的 0 到 1023 的范围。
一个小的简单计算为我们提供了以下值:
-
当电压为 2.5 时(当柔性未弯曲时),电压为
511 -
当电压为 1.7 时(当柔性弯曲在约 90 度角时),电压为
347
因为 Arduino 引脚上的电压取决于电阻的倒数,所以我们没有完美的线性变化。
经验告诉我,我可以几乎将其近似为线性变化,我在 Arduino 固件中使用了缩放函数,将[347,511]映射到更简单的范围[0,90]。map(value, fromLow, fromHigh, toLow, toHigh)是这里要使用的函数。
你还记得 Max 6 中的scale对象吗?map()基本上以相同的方式工作,但针对 Arduino。这里的语句将是map(347,511,90,0)。这将给出一个相当近似的物理弯曲角度值。
map函数在两个方向上都可以工作,可以将相反方向的数字段进行映射。我想你开始看到当你需要在 Arduino 上处理模拟输入时应该遵循的步骤。
现在,我们将遇到一些其他传感器。
几乎可以感知一切
无论你想测量哪个物理参数,都有相应的传感器。
这里有一个小列表:
-
颜色和亮度
-
声音音量
-
放射性强度
-
湿度
-
压力
-
弯曲
-
液位
-
罗盘和与磁北相关的方向
-
气体特定检测
-
振动强度
-
三轴(x,y,z)加速度
-
温度
-
距离
-
重量(纯弯曲传感器不同)
这不是一个详尽的列表,但相当完整。
价格变化很大,从几美元到 50 或 60 美元。我找到了一个价格较低的盖革计数器,大约 100 美元。你可以在附录 G,组件分销商列表中找到大量可以在互联网上购买传感器的公司。
现在,让我们更进一步。我们如何处理多个模拟传感器?第一个答案是,将所有东西都连接到 Arduino 的多个模拟输入。让我们看看我们是否可以比这更聪明。
使用 CD4051 复用器/解复用器进行复用
我们将要探索一种称为复用的技术。这是一个重要的子章节,因为我们将要学习如何使我们的实际项目更加具体、更加真实。
在现实世界中,我们经常有许多限制。其中一个可能是可用的 Arduino 数量。这种限制也可能来自于只有单个 USB 端口的计算机。是的,这种情况在现实生活中确实会发生,如果我说我可以在你想要的任何时候,在你想要的预算内拥有你想要的每一个连接器,那我就是在撒谎。
假设你不得不将超过八个传感器连接到 Arduino 的模拟输入。你会怎么做?
我们将学习如何复用信号。
复用概念
复用在电信世界中相当常见。复用定义了提供有效方式让多个信号共享单一介质的技巧。

基本复用概念展示了共享介质
这种技术提供了一个非常有帮助的概念,其中你只需要一个共享介质来带来许多信息通道,正如我们在前面的图中可以看到的那样。
当然,这涉及到复用(在图中称为 mux)和解复用(demux)过程。
让我们深入探讨一下这些过程。
多种复用/解复用技术
当我们需要复用/解复用信号时,我们基本上需要找到一种方法,通过我们可以控制的物理量来分离它们。
我至少可以列出三种复用技术类型:
-
空分复用
-
频分复用
-
时分复用
空分复用
这是最容易理解的。

空分复用将所有电线物理地聚集到同一个地方
例如,这个概念是您公寓中的基本电话网络复用。
你的电话线,就像你邻居的电话线一样,所有这些线都被连接到一个屏蔽的大多对电缆中,例如,包含你居住的整个建筑中的所有电话线。这条巨大的多对电缆进入街道,将其作为一个全局电缆捕获比捕获来自你邻居的每根电缆加上你自己的电缆要容易。
这个概念很容易转化为 Wi-Fi 通信。确实,今天一些 Wi-Fi 路由器提供了不止一个 Wi-Fi 天线。例如,每个天线都能够处理一个 Wi-Fi 连接。每一次通信都会使用相同的介质:空气传输电磁波。
频分复用
这种复用技术在所有与 DSL 和有线电视连接相关的事物中都非常常见。
服务提供商可以使用这种技术通过同一根电缆提供多个服务。

频分复用与传输频率和带宽玩游戏
想象一下图中的1、2和3频率波段是三种不同的服务。1 可能是语音,2 可能是互联网,3 是电视。现实与这并不太远。
当然,我们在一端复用的东西,我们必须在另一端解复用,以便正确地处理我们的信号。我不会尝试将电视调制的信号转换为语音,但我猜这不会是一次很有成效的经历。
时分复用
这是我们将要深入挖掘的情况,因为这是我们将在 Arduino 上用于多路复用的信号。

用一个四步周期的例子说明时分复用
依次,多路复用器和多路分解器之间只有一条通道被完全用于第一个信号,然后是第二个,以此类推,直到最后一个。
这种系统通常涉及一个时钟。这有助于为每个参与者设置正确的周期,以便他们知道我们在通信的哪个步骤。保持通信的安全性和完整性至关重要。
串行通信就是这样工作的,并且由于许多原因——即使你在前面的章节中认为你已经了解了很多——我们将在下一章中更深入地探讨它们。
让我们来检查一下如何处理我们的 Arduino 板上的八个传感器和仅有一个模拟输入。
CD4051B 模拟多路复用器
CD4051B 模拟多路复用器非常便宜且非常有用。它基本上是一个模拟和数字多路复用器和多路分解器。这并不意味着你可以同时将其用作多路复用器和多路分解器。你必须确定你处于哪种情况,并为此情况布线和设计代码。但总是拥有几台 CD4051B 设备是有用的。
用作多路复用器时,你可以将八个电位器连接到 CD4051B,并只有一个 Arduino 模拟输入,然后通过代码读取所有 8 个值。
用作多路分解器时,你可以通过只从 Arduino 的一个引脚写入来写入八个模拟输出。我们将在本书稍后讨论这一点,当我们接近输出引脚,特别是与 LED 的脉冲宽度调制(PWM)技巧时。
集成电路是什么?
集成电路(IC)是一个微型化并全部包含在一个小塑料盒中的电子电路。这是最简单的定义。
基本上,我们无法谈论集成电路而不想到它们的小尺寸。这是集成电路的一个更有趣的特点。
另一个是我称之为黑盒抽象的东西。我也像硬件世界的编程类一样定义它。为什么?因为你不必确切知道它是如何工作的,只需知道如何使用它。这意味着如果外部引脚对你自己的目的有意义,那么内部的电路实际上并不重要。
这里是几种 IC 封装类型中的两种:
-
双列直插封装(DIP,也称为DIL)
-
小外形(SO)
你可以在how-to.wikia.com/wiki/Guide_to_IC_packages找到一份有用的指南。
两种 IC 中更常用的是 DIP 封装。它们也被称为通孔封装。我们可以轻松地操作并将它们插入到面包板或印刷电路板(PCB)上。
SO 需要更多的灵巧和更精细的工具。
如何布线 CD4051B IC?
第一个问题关于它看起来像什么?在这种情况下,答案是它看起来像 DIP 封装。

CD4051B DIP 封装版本
这是这个小巧的集成电路的正面。数据表在互联网上很容易找到。这里有一个来自德州仪器的:
www.ti.com/lit/ds/symlink/cd4051b.pdf
我在下一张图中重新绘制了全局封装。

带有所有引脚描述的 CD4051B 原理图
识别引脚编号 1
很容易找出哪个是引脚编号 1。按照标准,其中一个角落引脚前面刻有一个小圆圈。这就是引脚编号 1。
也有一个半圆形的小孔。当你将 IC 放置在这个半圆形在顶部(如图中所示)时,你就知道哪个是引脚编号 1;紧挨着引脚 1 的第一个引脚是引脚 2,以此类推,直到左列的最后一个引脚,在我们的例子中是引脚 8。然后,继续与左列最后一个引脚相对的引脚;这是引脚 9,下一个引脚是引脚 10,以此类推,直到右列的顶部。

IC 引脚编号
当然,如果第一个输入是引脚 1,那就太简单了。唯一真正能确定的方法是查看规格。
为 IC 供电
IC 本身必须供电。这是为了使其激活,在某些情况下,还可以驱动电流。
-
Vdd 是正电源电压引脚。它必须连接到 5V 电源。
-
Vee 是负电源电压引脚。在这里,我们将它连接到地。
-
Vss 是地引脚,也连接到地。
模拟 I/O 系列和常见的 O/I
检查这个标题中 I 和 O 的顺序。
如果你选择使用 CD4051B 作为多路复用器,你将有多路模拟输入和一个公共输出。
另一方面,如果你选择将其用作解复用器,你将有一个公共输入和多个模拟输出。
选择/切换是如何工作的?让我们检查选择器的数字引脚,A、B 和 C。
选择数字引脚
现在是最重要的部分。
有三个引脚,命名为 A(引脚 11)、B(引脚 10)和 C(引脚 9),必须由 Arduino 的数字引脚驱动。什么?我们不是在模拟输入部分吗?我们完全是在,但我们将使用这三个选定的引脚介绍一种新的控制方法。
内置的多路复用引擎并不难理解。
基本上,我们发送一些信号来使 CD4051B 将输入切换到公共输出。如果我们想将其用作解复用器,三个选定的引脚必须以完全相同的方式控制。
在数据表中,我发现了一个真值表。那是什么?它只是一个表格,我们可以检查哪些 A、B 和 C 组合将输入切换到公共输出。
下表描述了组合:

CD4051B 的真值表
换句话说,这意味着如果我们向 Arduino 上对应于 A 的数字输出写入 1,对应于 B 的写入 1,对应于 C 的写入 0,则切换的输入将是第三个通道。
当然,这里有一些好处。如果你读取对应于 C、B 和 A(按此顺序)的输入的二进制数,你会有一个惊喜;它将等同于公共输出切换的输入引脚的十进制数。
的确,二进制的 0 0 0 等于十进制的 0。参考表格以获取十进制数的二进制值:
| 0 0 0 | 0 |
|---|---|
| 0 0 1 | 1 |
| 0 1 0 | 2 |
| 0 1 1 | 3 |
| 1 0 0 | 4 |
| 1 0 1 | 5 |
| 1 1 0 | 6 |
| 1 1 1 | 7 |
这里是我们如何连接东西的方法:

包括 CD4051B 多路复用器和其公共输出连接到模拟引脚 0 的电路
以下图是电气图:

电气图
我们希望用这个系统读取的所有设备都应该连接到 CD4051B 上的 I/O 0、1、2 等端口。
考虑到我们对真值表的了解以及设备的工作方式,如果我们想顺序读取从 0 到 7 的所有引脚,我们必须在循环中包含两种类型的语句:
-
一个用于切换多路复用器
-
一个用于读取 Arduino 模拟输入 0
源代码看起来像这样(你可以在Chapter6/analogMuxReader文件夹中找到它):
int muxOutputPin = 0 ; // pin connected to the common output of the CD4051B
int devicesNumber = 8 ; // number of device // BE CAREFUL, plug them from 0
int controlPinA = 2 ; // pin connected to the select pin A of the CD4051B
int controlPinB = 3 ; // pin connected to the select pin B of the CD4051B
int controlPinC = 4 ; // pin connected to the select pin C of the CD4051B
int currentInput = 0 ; // hold the current analog input commuted o the common output of the CD4051B
void setup() {
Serial.begin(9600);
// setting up all 3 digital pins related to selectors A, B and C as outputs
pinMode(controlPinA, OUTPUT);
pinMode(controlPinB, OUTPUT);
pinMode(controlPinC, OUTPUT);
}
void loop(){
for (currentInput = 0 ; currentInput < devicesNumber - 1 ; currentInput++)
{
// selecting the inputs that is commuted to the common output of the CD4051B
digitalWrite(controlPinA, bitRead(currentInput,0));
digitalWrite(controlPinB, bitRead(currentInput,1));
digitalWrite(controlPinC, bitRead(currentInput,2));
// reading and storing the value of the currentInput
Serial.println(analogRead(muxOutputPin)) ;
}
}
在定义了所有变量之后,我们在setup()中设置串行端口,并将与 CD4051B 选择引脚相关的三个引脚作为输出。然后,在每个周期中,我首先通过驱动或不对 CD4051B 的 A、B 和 C 引脚供电来选择切换的输入。我在我的语句中使用嵌套函数来节省一些行。
bitRead(number,n)是一个新函数,能够返回一个数的第 n位。在我们的情况下,这是一个完美的函数。
我们对从 0 到 7 的输入进行循环,更确切地说,到devicesNumber - 1。
通过将这些位写入 CD4051B 设备的引脚 A、B 和 C,它每次选择模拟输入,并将串行端口读取的值弹出,以便在 Processing 或 Max 6 或您想使用的任何软件中进行进一步处理。
摘要
在本章中,我们至少学会了如何接近一个名为 Max 6 的非常强大的图形框架环境。随着我们继续使用 Processing,我们将在本书的几个后续示例中使用它。
当我们想要处理为 Arduino 模拟输入提供连续电压变化的传感器时,我们学到了一些反射技巧。
然后,我们还发现了一个非常重要的技术,即复用/解复用技术。
我们将在下一章关于串行通信的章节中讨论它。既然我们已经花费了很多时间,现在我们将更深入地探讨这种通信类型。
第七章. 通过串行通信
我们已经看到,使用 Arduino 就是关于通信和共享信号。确实,从 Arduino 最基本的部分,通过改变其环境来响应一些物理世界的值,并将这种变化作为基本消息传播给其邻居,到现在的经典串行通信,电子实体之间以及与我们之间都在进行通信。
就像这本书中的许多概念一样,我们已经在几个地方使用了串行通信及其底层串行协议作为一个黑盒工具,也就是说,我介绍了一个工具但没有解释它。
我们将在本章中深入探讨这个问题。我们将发现,串行通信不仅用于人机通信,还用于机器内部的“组件到组件”的讨论。在这里,我所说的组件是指小型系统,我可以用外设这个词来描述它们。
串行通信
在计算机科学和电信中,串行通信通常是一种通信方式,其中数据逐位通过通信总线发送。
现在,我们到处都能看到串行通信,而且我们往往甚至没有意识到这一点。USB缩写中的“S”代表串行(USB 是通用串行总线),代表了每个更高协议使用的底层串行通信总线。
让我们立刻深入探讨这个问题。
串行和并行通信
串行通信通常通过其相反的通信形式,即并行通信来定义,在这种通信中,多个数据位同时通过由多个并行通道组成的链路发送。看看下面的图:

发言者和听众之间基本、单向的串行通信
现在让我们来比较一下并行的情况:

发言者和听众之间基本、单向的并行通信
在这两个图中,一个扬声器正在发送以下数据字节:0 1 1 0 0 0 1 1。在这些情况下,当使用串行通信时,这八个数据位是按顺序通过一个通道发送的,而当使用并行通信时,则是同时通过八个不同的通道发送。
从短距离到长距离通信,尽管从表面上看并行方法似乎更快,因为在时钟周期内同时发送多个数据位,但串行通信在逐渐超越其他通信形式。
这种情况下的第一个原因是涉及的线缆数量。例如,在我们的小例子中使用的并行方法需要八个通道来同时驱动我们的八个数据位,而串行通信只需要一个。我们很快就会讨论什么是通道,但如果我们使用串行通信,仅一根线缆的 1:8 比例就能节省我们很多钱。
第二个主要原因是,我们最终实现了使串行通信非常快速。这是由于以下原因:
-
首先,传播时间在较少的线缆中更容易处理
-
其次,串扰在通道较少的情况下比在通道密度较高的并行链路中要少
-
第三,由于涉及的线缆较少,我们可以节省空间(和金钱),并且经常利用这些节省的空间来更好地屏蔽我们的线缆
现在,串行通信的带宽从每秒几兆比特到超过 1 太比特(即 1,000 千兆比特),可以使用多种媒体,从有线光纤到无线,从铜缆到光纤。正如你可能猜到的,有许多串行协议被使用。
串行通信的类型和特性
无论是对称性、双工模式、总线还是对等,串行通信都可以有不同的定义,我们必须深入探讨这一点。
同步或异步
串行通信可以是同步的,也可以不是。
同步通信涉及一个时钟,我们可以称之为主时钟,为通信的所有参与者保持参考时间。第一个想到的例子是电话通信。
异步通信不需要时钟数据通过串行通道(s)发送;这使得通信更容易,但有时可能导致一些理解上的问题。邮寄和短信是异步通信类型。
双工模式
双工模式是通信通道的一个特定特性。它可以:
-
单工:单向(数据只在两个点之间单向传递)
-
半双工:双向,但同一时间只能在一个方向上
-
全双工:双向同时
半双工显然比单工更有用,但它必须运行碰撞检测和重传过程。确实,当你和朋友交谈时,你也在共享相同的媒体(房间和房间内的空气,它携带从你的嘴到他的耳朵的振动),如果你同时说话,通常一个人会检查这一点,然后停下来告诉另一个人重复。
全双工需要更多的通道。这样就不会发生碰撞,并且可以取消所有碰撞检测和重传过程。其他错误的检测和修复仍然涉及,但通常要容易得多。
对等和总线
在对等系统中,说话者通过物理或逻辑方式连接到听众。没有主控,这类接口通常是异步的。
在总线中,它们最终会在某个物理位置连接起来,并且会发生一些逻辑交换。

一个多总线系统的例子
主从总线
在主/从总线上,一个设备是主设备,其他的是从设备,这通常涉及到同步,其中主参与者生成定时时钟。
串行通信的主要困难是避免冲突和误解。
有很多解决方案可以实施来解决这些问题,例如使用多种物理链路类型和特定的现有通信协议。让我们检查一些这些,特别是那些我们可以与 Arduino 一起使用的。
数据编码
当我们使用串行协议进行通信时,需要定义的最重要的事情如下:
-
字长(以比特为单位)
-
是否存在停止位(定义了时间上的空白时刻)
-
是否存在奇偶校验位(定义了最简单、基于错误检测的代码解决方案)
事实上,特别是在异步通信中,没有这些属性,一个听者如何知道一个字开始或结束在哪里呢?通常,我们会在参与者的脑海中硬编码这种行为,以确保我们有有效的通信协议。
在本章的第一幅图中,我通过该通道发送了 8 位数据。这等于 1 字节。
我们通常将串行通信的类型写成 <word length><parity><stop>。例如,8 位无奇偶校验但一个停止位的写法为 8N1。
我不会完全描述奇偶校验位,但你应该知道它基本上是一个校验和。使用这个概念,我们发送一个字和一个校验和,然后我们验证接收到的字中所有位的二进制和。这样,听者可以非常容易地检查接收到的字的完整性,但方式非常原始。可能会发生错误,但这是最便宜的方法;它可以避免许多错误,并且从统计上来说是正确的。
使用 8N1 类型串行通信的全局数据帧包含 10 位:
-
一个起始位
-
每个字符 8 位
-
一个停止位
事实上,发送的数据中只有 80%是真实的有效负载。我们一直在尝试减少发送的流量控制数据的数量,因为这可以节省带宽并最终节省时间。
多个串行接口
我不会描述所有的串行协议,但我想要谈谈一些重要的,并将它们分类。
强大的摩尔斯电码电报祖先
我给你介绍一种最古老的串行协议:摩尔斯电码电报协议。电信运营商自 19 世纪下半叶以来一直在使用这个协议。
我必须说,Samuel F. B. Morse 不仅是一位发明家,而且是一位技艺高超的艺术家和画家。在这里提到这一点很重要,因为我真的相信艺术和技术最终是同一件事,我们曾经用两种不同的观点来看待。我可以引用更多的艺术家/发明家,但我猜这会有些离题。
通过发送长脉冲和短脉冲,并在空白处分隔,莫尔斯电码的操作员可以发送单词、句子和信息。这可以通过多种类型的媒体发生,例如:
-
电缆(电脉冲)
-
空气(电磁波载体、光、声音)
它可以被分类为对等、半双工和异步通信系统。
关于脉冲持续时间有一些规则,从长到短再到空白,但这仍然是异步的,因为双方之间并没有真正共享的时钟。
著名的 RS-232
RS-232 是一种常见的接口,你将在所有个人计算机上找到它。它定义了一个完整的电气到物理(以及电气到机械)特性的标准,例如连接硬件、引脚和信号名称。RS-232 于 1962 年推出,至今仍被广泛使用。这种点对点接口可以在适中的距离上驱动高达 20 Kbps(每秒千比特=每秒 20,000 比特)的数据。尽管标准中没有规定,但通常我们会在短而屏蔽的电缆上找到速度超过 115.2 Kbps 的实例。
我自己使用 20 米长的电缆,传感器通过串行传输数据到 Arduino,用于不同的安装。有些朋友使用 50 米长的电缆,但我没有这么做,更倾向于其他解决方案,如以太网。
从 25 根线到 3 根
如果标准定义了 25 针连接器和链路,我们可以将所需的多个硬件流控制、错误检测等大量电线/信号减少到只有三根:
-
传输数据(通常写作 TX)
-
接收数据(通常写作 RX)
-
地线
有 25 个引脚/线的连接器被称为 DB25,并且被广泛用于打印机等外围设备。还有一种名为 DB9 的连接器,只有 9 个引脚/线。这是比 DB25 省去更多线的变体。这种 DB9 被广泛用于连接鼠标设备。
但我们如何省去大量电线/信号,同时保持串行通信良好工作?基本上,就像许多标准一样,它已经被设计来适应许多用例。例如,在 DB25 的全版本中,有 8 号和 22 号引脚是专门用于电话线的:第一个是数据载波检测,第二个是振铃指示器。通过 4 号和 5 号引脚发送的信号用于参与者之间的握手。
在本标准中,引脚 7 是公共地,2 和 3 分别代表 TX 和 RX。仅使用这三个引脚,我们就可以正确地进行串行异步通信。

DB25 连接器

DB9 连接器
我们宝贵的 Arduino 提供了这种三线串行替代方案。当然,每种类型的板子提供的串行接口数量并不相同,但原理是一样的:基于三线的串行接口是可用的。
Arduino Uno 和 Leonardo 提供了 TX、RX 和地三个引脚,而新发布的 Arduino Mega 2560 和 Arduino Due (arduino.cc/en/Main/ArduinoBoardDue)则提供了从 RX0 和 TX0 到 RX3 和 TX3 的四个不同的串行通信接口名称。
我们将描述另一种串行接口标准,然后我们将回到使用 FTDI 制造的著名集成电路的 RS-232,它提供了一种非常有效的方法将 RS-232 转换为 USB。
优雅的 I2C
由 Philips 设计的 I2C 多主串行单端计算机总线需要任何硬件实现都获得许可。
其中的一个优点是它只使用两根线:SDA(串行数据线)具有 7 位寻址系统和SCL(串行时钟线)。
考虑到其寻址系统,这个接口真的很不错。为了使用它,我们必须从 Arduino 构建两个基于引脚的总线,在这里 Arduino 是主设备。

将 BlinkM 模块作为 I2C 总线连接到 Arduino Uno R3
为了知道每个 Arduino 板需要使用哪些引脚,你可以直接查看www.arduino.cc/en/Reference/Wire上的信息。
BlinkM模块(thingm.com/products/blinkm)是具有小型尺寸的 RGB LED 模块,在 I2C 总线上操作起来相当简单。我也用它大量控制大型的 LCD 屏幕,使用 Arduino。
这也是 Arduino 的Wire库页面。如今,这个库已经包含在 Arduino 核心中。考虑到标准的复杂性,当总线上有大量元素时,成本会增加。由于其两根引脚和数据完整性的精度,这仍然是在同一盒子内进行短距离和间歇性通信的一种优雅解决方案。双线接口(TWI)原则上与 I2C 相同标准。当 I2C 的专利仍在运行时,它以另一个名称为人所知。
I2C 已经成为许多其他接口协议的基础,如 VESA DDC(屏幕和图形卡之间的数字连接)、Intel 的 SMBus 以及一些其他协议。
同步 SPI
SPI代表串行外围接口,由 Motorola 开发。它使用以下四根线:
-
SCLK:这是由主设备驱动的串行时钟
-
MOSI:这是由主设备驱动的主输出/从输入
-
MISO:这是由主设备驱动的主输入/从输出
-
SS:这是从机选择线
在点对点通信中,即使我们在 SPI 总线上发现许多具有多个从机的应用,这种用法仍然非常有用,其中只有一个主设备和一个从设备。
由于 SPI 是一种基于模式的全双工接口,我们可以实现比 I2C 更高的数据传输速率。它通常用于编码器/解码器和数字信号处理器之间的通信;这种通信包括同时发送和接收样本。SPI 缺少设备寻址也是一个巨大的优势,因为它使得系统更加轻量级,因此在不需要此功能的情况下,速度更快。确实,I2C 和 SPI 根据您想要实现的目标,彼此之间是非常互补的。
在 Arduino 板上有关 SPI 的信息可在网上找到(arduino.cc/en/Reference/SPI),但您必须知道我们可以轻松地将任何数字引脚用作 SPI 中包含的四根线之一。
我个人在涉及大量移位寄存器的项目中经常使用它,这些寄存器都通过 Arduino Uno 和甚至 Arduino Mega 串联起来,后者提供了更多的原生输出和输入。
在下一章中,我将向您介绍如何使用移位寄存器,届时我会向您展示如何通过 SPI 轻松地使用一些智能且最终非常简单的集成电路来复用输出,这些集成电路通过 SPI 连接到 Arduino。
普及的 USB
USB 是通用串行总线标准。这可能是您使用得最多的一个。
该标准的主要优势是 USB 设备的即插即用功能。您可以在不重新启动计算机的情况下插入和拔出设备。
USB 被设计成标准化连接各种计算机外围设备,包括以下设备:
-
音频(扬声器,麦克风,声卡,MIDI)
-
通信(调制解调器,Wi-Fi 和以太网)
-
人类界面设备(HID,键盘,鼠标,游戏手柄)
-
图像和视频(网络摄像头、扫描仪)
-
打印机
-
大容量存储(闪存驱动器,存储卡,驱动器)
-
无线(红外)
还有许多其他类型。该标准实际上是 3.0 版本。USB 总线可以包含多达 127 个外围设备,并为通用设备提供最大 500 到 900 毫安的电流。
USB 系统设计
USB 的架构是不对称拓扑,由一个主机和多个下游 USB 端口以及多个以分层星状拓扑连接的外围设备组成。
USB 集线器可以包含在层级中,允许分支到五级。这导致树状拓扑。这就是为什么您可以在集线器上堆叠集线器。
设备类别提供了一种方式,即具有适应性和设备无关的主机来支持新设备。主机可以识别的 ID 定义了每个类别。您可以在官方 USB 标准网站上找到所有批准的类别www.usb.org/developers/devclass。
USB 连接器和电缆
一个 USB 标准插头包含四根线(en.wikipedia.org/wiki/Universal_Serial_Bus):
-
Vcc (+5 V)
-
数据-
-
数据+
-
地线
![USB 连接器和电缆]()
USB 标准 A 型和 B 型插头
电缆是屏蔽的;它们的通常最大长度在两到五米左右。我已经使用了一根 12 米的电缆连接 USB 端口。使用我自己在一个电磁安全环境中焊接的电缆,也就是说,在一个我的电缆独自位于墙后且没有与其他许多电缆混合的地方,特别是那些供电的电缆,它完全正常工作。
有一些其他类型的插头稍微大一些,但至少需要四根线的需求保持不变。
FTDI IC 将 RS-232 转换为 USB
除了某些版本,如 Arduino Pro Mini 之外,Arduino 板提供 USB 连接器,正如你所知并已使用的那样。
这为计算机或连接到计算机的集线器提供了基本的电源功能,同时也用于通信。
FTDI 集成电路 EEPROM 名为 FT232,提供了一种将 USB 转换为 RS-232 接口的方法。这就是为什么我们可以在 USB 上使用 Arduino 板的串行通信功能,而无需从与串行通信相关的 Arduino 引脚(即 TX 和 RX)的外部串行端口接口。新板包括一个提供串行通信功能的 Atmega16U2。
的确,当你将 Arduino 板连接到计算机时,你将有一个可用的串行通信功能。我们已经在以下情况下使用过它:
-
Arduino IDE(串行监视器)
-
Processing(带有串行库)
-
最大 6(带有串行对象)
我想你也记得,当我们使用 Max 6 的串行对象轮询功能时,我们无法使用串行监视器。
你现在明白为什么了吗?在电线和计算机的虚拟世界中,同一时间只能有一个点对点链接是活跃的。物理链接也是如此。我警告过你,当你需要使用 Arduino 板进行串行通信时,不要使用数字引脚 0 和 1,尤其是 Diecimilla 版本。这些引脚直接连接到 FTDI USB 到 TTL 串行芯片的相应 RX 和 TX 引脚。
注意
如果你使用 USB 功能进行串行通信,你必须避免使用数字引脚 0 和 1。
摘要
在本章中,我们讨论了串行通信。这是电子设备内部和之间非常常见的一种通信方式。本章也是对其他通用通信协议的良好介绍,我相信你现在已经准备好理解更高级的功能。
在下一章中,我们将使用这里介绍的一些不同类型的串行协议。特别是,我们将讨论 Arduino 输出;这意味着我们不仅能够为我们的 Arduino 板添加反馈和反应,考虑到行为模式设计,如刺激和响应的确定性方式,我们还将看到更多混沌的行为,例如包括受限制的随机性等。
第八章:设计视觉输出反馈
交互是关于控制和反馈的一切。你通过对其执行操作来控制一个系统。你甚至可以修改它。系统通过提供有关其修改后所做事情的有用信息来给你反馈。
在上一章中,我们更多地学习了如何控制 Arduino,而不是 Arduino 给我们反馈。例如,我们使用了按钮和旋钮向 Arduino 发送数据,使其为我们工作。当然,有很多观点,我们很容易考虑控制 LED 并向 Arduino 提供反馈。但通常,当我们想要对系统向我们返回的信息进行定性时,我们谈论反馈。
阿尔卡卢格·拉马普拉萨德,伊利诺伊大学芝加哥分校商学院信息与决策科学系教授,将反馈定义为如下:
“关于系统参数实际水平与参考水平之间差距的信息,该参数用于以某种方式改变该差距。”
我们已经在第五章数字输入检测中讨论了一些视觉输出,当时我们试图可视化按钮点击事件的结果。这种由我们的点击事件产生的视觉渲染是反馈。
现在,我们将讨论基于 Arduino 板驱动的 LED 的视觉反馈系统的设计。LED 是提供视觉反馈的最简单的系统之一。
我们将要学习以下内容:
-
如何使用基本单色 LED
-
如何制作 LED 矩阵以及如何多路复用 LED
-
如何使用 RGB LED
我们将通过介绍液晶显示器设备来结束本章。
使用 LED
LED 可以是单色的或多色的。实际上,有许多类型的 LED。在通过一些示例之前,让我们发现一些这些 LED 类型。
不同类型的 LED
通常,LED 既用于阻止来自线路的电流流向其阴极引脚,也用于当电流流入其阳极时提供光反馈:

我们可以找到的不同型号如下:
-
基本 LED
-
OLED(有机 LED通过层叠有机半导体部分制成)
-
AMOLED(有源矩阵 OLED提供大尺寸屏幕的高像素密度)
-
FOLED(柔性****OLED)
我们在这里只讨论基本 LED。通过“基本”一词,我指的是像前面图像中那样的具有离散组件的 LED。
封装可能从顶部带有注塑环氧树脂状透镜的两脚组件,到提供许多连接器的表面组件不等,如下面的截图所示:

我们还可以根据它们的光的颜色特性对它们进行分类:
-
单色 LED
-
多色 LED
在每种情况下,LED 的可见颜色由注塑环氧树脂盖的颜色决定;LED 本身发射相同的波长。
单色 LED
单色 LED 只发出一种颜色。
最常见的单色 LED 在每个电压下都会发出恒定的颜色。
彩色 LED
彩色 LED 可以根据几个参数发出多种颜色,例如电压,也可以根据多脚 LED 中提供电流的脚来决定。
这里最重要的特性是可控性。彩色 LED 必须易于控制。这意味着我们应该能够通过打开或关闭来控制每种颜色。
下面是一个经典的共阴极 RGB LED,有三个不同的阳极:

这种 LED 是我们 Arduino 设备的首选。考虑到我们可以轻松控制它们并使用它们产生非常广泛的颜色,它们并不昂贵(大约每 100 个 LED 1.2 欧元)。
在接下来的几页中,我们将了解如何处理多个 LED,以及彩色 RGB LED。
记住 Hello LED 示例
在 Hello LED 中,我们使 LED 在每 1000 毫秒中闪烁 250 毫秒。让我们再次看看其电路图,以保持阅读的连贯性:

Hello LED 的代码如下:
// Pin 8 is the one connected to our pretty LED
int ledPin = 8; // ledPin is an integer variable initialized at 8
void setup() {
pinMode(ledPin, OUTPUT); // initialize the digital pin as an output
}
// --------- the loop routine runs forever
void loop() {
digitalWrite(ledPin, HIGH); // turn the LED on
delay(250); // wait for 250ms in the current state
digitalWrite(ledPin, LOW); // turn the LED off
delay(1000); // wait for 1s in the current state
}
直观地,在接下来的示例中,我们将尝试使用多个 LED,同时玩单色和彩色 LED。
多个单色 LED
由于我们在这里讨论的是反馈,而不仅仅是纯输出,我们将构建一个小示例来向您展示如何处理多个按钮和多个 LED。如果您现在完全无法理解,请不要担心;只需继续阅读。
两个按钮和两个 LED
我们已经在第五章中讨论了玩多个按钮,使用数字输入进行感应。现在让我们构建一个新的电路。
下面是电路图:

继续绘制与每个电路图相关的电原理图是更好的选择。
基本上,这是来自第五章的多个按钮示例,使用数字输入进行感应;然而,我们移除了一个按钮,并添加了两个 LED。

如您所知,Arduino 的数字引脚可以用作输入或输出。我们可以看到,两个开关连接到一侧的 5V Arduino 引脚,另一侧连接到数字引脚 2 和 3,每个后者引脚都有一个相关的下拉电阻,将电流吸收到 Arduino 的地线引脚。
我们还可以看到,一个 LED 连接到一侧的数字引脚 8 和 9;它们都连接到 Arduino 的地线引脚。
这并没有什么真正令人难以置信的。
在您设计专用固件之前,您需要简要介绍一些非常重要的事情:耦合。对于任何接口设计来说,这是必须知道的;更广泛地说,对于交互设计。
交互设计中的控制和反馈耦合
这一节被视为子章节有两个主要原因:
-
首先,它听起来很棒,并且是保持动机流畅的关键。
-
其次,这部分是您未来所有人机界面设计的关键。
如您所知,Arduino(得益于其固件)连接了控制和反馈两侧。这一点非常重要,需要牢记在心。
无论外部系统的类型如何,从 Arduino 的角度来看,它通常被视为人类。一旦您想要设计一个交互系统,您就必须处理这一点。
我们可以用一个非常简单的示意图来总结这个概念,以便在脑海中固定下来。
事实上,您必须理解,我们即将设计的固件将创建一个控制-反馈耦合。
控制/反馈耦合是一组规则,定义了系统在接收到我们的命令时如何表现,以及它如何通过给我们(或不给我们)反馈来做出反应。
这组硬编码的规则非常重要,需要理解。

但是,想象一下,您想用 Arduino 控制另一个系统。在这种情况下,您可能希望将耦合放在 Arduino 本身之外。
看第二个图外部系统 2,我把耦合放在 Arduino 之外。通常,外部系统 1是我们,外部系统 2是计算机:

现在,我们可以引用一个现实生活中的例子。就像许多界面和遥控器的用户一样,我喜欢并且需要用简约的硬件设备控制我电脑上的复杂软件。
我喜欢由 Brian Crabtree 设计的简约且开源的Monome 界面(monome.org)。我经常使用它,有时还在使用。它基本上是一个 LED 和按钮的矩阵。令人惊叹的技巧是,在内部没有任何耦合。

上一张图片是 Brian Crabtree 设计的 Monome 256 及其非常精美的木质外壳。
如果所有文档中都没有直接这样写,我希望能够这样定义给我的朋友和学生:“Monome 概念是您需要的最简约的界面,因为它只提供控制 LEDs 的方式;除此之外,您有很多按钮,但按钮和 LEDs 之间没有逻辑或物理连接。”
如果 Monome 不提供按钮和 LEDs 之间的真实、现成的耦合,那是因为这将非常限制性,甚至可能消除所有创造力!
由于有一个非常原始且高效的协议设计(monome.org/data/monome256_protocol.txt),专门用于控制 LED 和读取按钮的按下,我们能够自己创建和设计我们的耦合。Monome 还提供了Monome Serial Router,这是一个非常小的应用程序,基本上将原始协议转换为OSC(archive.cnmat.berkeley.edu/OpenSoundControl/)或MIDI(www.midi.org/)。我们将在本章的后续部分讨论它们。这些在多媒体交互设计中非常常见;OSC 可以在网络上传输,而 MIDI 非常适合连接音乐相关设备,如序列器和合成器。
如果不附上关于 Monome 的另一个原理图,这次简短的离题就不会完整。
检查一下,然后我们再深入了解:

智能简约的 Monome 界面在其通常的基于计算机的设置中
这里是 Monome 64 界面的原理图,在通常的基于计算机的设置中,耦合就在其中发生。这是我多次在音乐表演中使用过的实际设置(vimeo.com/20110773)。
我在 Max 6 中设计了一个特定的耦合,将特定的消息从/到 Monome 本身,以及从/到软件转换,特别是 Ableton Live(www.ableton.com)。
这是一个非常强大的系统,它可以控制事物并提供反馈,你可以基本上从头开始构建你的耦合,并将你的原始简约界面转变为你需要的样子。
这只是关于交互设计的一个更广泛独白的一部分。
让我们现在构建这个耦合固件,看看我们如何可以将控制和反馈耦合到基本的示例代码中。
耦合固件
这里,我们只使用了 Arduino 的开关和 LED,实际上没有使用电脑。
让我们设计一个基本的固件,包括耦合,基于这个伪代码:
-
如果我按下开关 1,LED 1 将被打开,如果我释放它,LED 1 将被关闭
-
如果我按下开关 2,LED 2 将被打开,如果我释放它,LED 2 将被关闭
为了操作新的元素和想法,我们将使用一个名为Bounce的库。它提供了一个简单的方法来去抖动数字引脚输入。我们已经在第五章的理解去抖概念部分中讨论过去抖动,感应数字输入。提醒一下:如果你按下按钮时没有按钮完全吸收抖动,我们可以通过软件平滑事物并过滤掉不希望的非理想值跳跃。
你可以在arduino.cc/playground/Code/Bounce找到关于Bounce库的说明。
让我们检查一下这段代码:
#include <Bounce.h> // include the (magic) Bounce library
#define BUTTON01 2 // pin of the button #1
#define BUTTON02 3 // pin of the button #2
#define LED01 8 // pin of the button #1
#define LED02 9 // pin of the button #2
// let's instantiate the 2 debouncers with a debounce time of 7 ms
Bounce bouncer_button01 = Bounce (BUTTON01, 7);
Bounce bouncer_button02 = Bounce (BUTTON02, 7);
void setup() {
pinMode(BUTTON01, INPUT); // the switch pin 2 is setup as an input
pinMode(BUTTON02, INPUT); // the switch pin 3 is setup as an input
pinMode(LED01, OUTPUT); // the switch pin 8 is setup as an output
pinMode(LED02, OUTPUT); // the switch pin 9 is setup as an output
}
void loop(){
// let's update the two debouncers
bouncer_button01.update();
bouncer_button02.update();
// let's read each button state, debounced!
int button01_state = bouncer_button01.read();
int button02_state = bouncer_button02.read();
// let's test each button state and switch leds on or off
if ( button01_state == HIGH ) digitalWrite(LED01, HIGH);
else digitalWrite(LED01, LOW);
if ( button02_state == HIGH ) digitalWrite(LED02, HIGH);
else digitalWrite(LED02, LOW);
}
你可以在Chapter08/feedbacks_2x2/文件夹中找到它。
此代码在开头包含了 Bounce 头文件,即 Bounce 库。
然后,我根据数字输入和输出引脚定义了四个常量,其中我们在电路中放置开关和 LED。
Bounce 库要求实例化每个去抖动器,如下所示:
Bounce bouncer_button01 = Bounce (BUTTON01, 7);
Bounce bouncer_button02 = Bounce (BUTTON02, 7);
我选择了 7 毫秒的去抖动时间。这意味着,如果你记得正确的话,在小于 7 毫秒的时间间隔内发生的两次值变化(自愿或非自愿)不会被系统考虑,从而避免了奇怪和不寻常的抖动结果。
setup()块并不复杂,它只定义了数字引脚作为按钮的输入和 LED 的输出(请记住,数字引脚可以是输入也可以是输出,你必须在某个时候做出选择)。
loop()函数首先更新两个去抖动器,之后我们读取每个去抖动按钮状态值。
最后,我们处理 LED 控制,这取决于按钮状态。耦合发生在哪里?当然是在这个最后的步骤。我们在该固件中将我们的控制(按钮按下)与我们的反馈(LED 灯)耦合起来。让我们上传并测试它。
更多 LED?
我们基本上只是看到了如何将多个 LED 连接到我们的 Arduino 上。当然,我们也可以用相同的方式连接超过两个 LED。你可以在Chapter05/feedbacks_6x6/文件夹中找到处理六个 LED 和六个开关的代码。
但是,我有一个问题要问你:你将如何用 Arduino Uno 处理更多的 LED?请不要回答说“我会买一个 Arduino MEGA”,因为那样我会问你如何处理超过 50 个 LED。
正确的答案是多路复用。让我们看看我们如何处理大量的 LED。
多路复用 LED
多路复用的概念既有趣又高效。它是将许多外围设备连接到我们的 Arduino 板的关键。
多路复用提供了一种方法,在板上使用很少的 I/O 引脚,同时使用大量的外部组件。Arduino 与这些外部组件之间的连接是通过使用多路复用器/解复用器(也简称为 mux/demux)来实现的。
我们在第六章中讨论了输入多路复用,使用模拟输入进行游戏。
我们将在这里使用 74HC595 组件。其数据表可以在www.nxp.com/documents/data_sheet/74HC_HCT595.pdf找到。
此组件是一个 8 位串行输入/串行或并行输出。这意味着它通过串行接口控制,基本上使用 Arduino 的三个引脚,并且可以用其八个引脚驱动。
我将向您展示如何使用 Arduino 的仅三个引脚来控制八个 LED。由于 Arduino Uno 包含 12 个可用的数字引脚(我通常不使用 0 和 1),我们可以轻松地想象使用 4 x 75HC595 来控制 4 x 8 = 32 个单色 LED。我还会提供相应的代码。
将 75HC595 连接到 Arduino 和 LED
正如我们与 CD4051 和模拟输入多路复用一起学习的那样,我们将芯片连接到 75HC595 移位寄存器,以便多路复用/解复用八个数字输出引脚。让我们检查接线:

我们让 Arduino 为面包板供电。每个电阻提供 220 欧姆的电阻。
75HC595 从 GND 和 5V 电位获取其自身的电源和配置。
基本上,74HC595 需要通过引脚 11、12 和 14 连接,以便通过 Arduino 处理的串行协议进行控制。
让我们来检查 74HC595 本身:

-
引脚 8 和 16 用于内部电源。
-
引脚 10 被命名为主复位,为了激活它,你必须将这个引脚连接到地。这就是为什么在正常工作状态下,我们将其驱动到 5V 的原因。
-
引脚 13 是输出使能输入引脚,必须保持激活状态才能使整个设备输出电流。将其连接到地即可实现这一点。
-
引脚 11 是移位寄存器时钟输入。
-
引脚 12 是存储寄存器时钟输入,也称为锁存。
-
引脚 14 是串行数据输入。
-
引脚 15 和引脚 1 到 7 是输出引脚。
我们的小型且经济的串行链路连接到 Arduino,由引脚 11、12 和 14 处理,提供了一种简单的方法来控制和基本将八个位加载到设备中。我们可以循环遍历八个位,并将它们以串行方式发送到存储它们的寄存器的设备。
这些类型的设备通常被称为移位寄存器,我们在加载它们的同时从 0 到 7 移动位。
然后,每个状态都从 Q0 到 Q7 正确输出,将之前通过串行传输的状态转换。
这是我们在上一章中提到的串行到并行转换的直接说明。我们有一个数据流按顺序流动,直到寄存器全局加载,然后将其推送到许多输出引脚。
现在,让我们可视化接线图:

一个带有电阻连接到 74HC595 移位寄存器的 8 个 LED 阵列
串行寄存器处理固件
我们将学习如何设计专门用于这类移位寄存器的固件。这个固件基本上是为 595 设计的,但与其他集成电路一起使用时不需要太多修改。你特别需要注意三个串行引脚,即锁存、时钟和数据。
因为我想每次都教给你比每个章节标题所激发的精确内容更多一点,所以我为你创造了一台非常便宜且小巧的随机凹槽机。它的目的是生成随机字节。然后,这些字节将被发送到移位寄存器,以便为每个 LED 供电或不供电。这样,你将得到一个整洁的随机 LED 图案。
你可以在Chapter08/Multiplexing_8Leds/文件夹中找到这个代码。
让我们检查一下:
// 595 clock pin connecting to pin 4
int CLOCK_595 = 4;
// 595 latch pin connecting to pin 3
int LATCH_595 = 3;
// 595 serial data input pin connecting to pin 2
int DATA_595 = 2;
// random groove machine variables
int counter = 0;
byte LED_states = B00000000 ;
void setup() {
// Let's set all serial related pins as outputs
pinMode(LATCH_595, OUTPUT);
pinMode(CLOCK_595, OUTPUT);
pinMode(DATA_595, OUTPUT);
// use a seed coming from the electronic noise of the ADC
randomSeed(analogRead(0));
}
void loop(){
// generate a random byte
for (int i = 0 ; i < 8 ; i++)
{
bitWrite(LED_states, i, random(2));
}
// Put latch pin to LOW (ground) while transmitting data to 595
digitalWrite(LATCH_595, LOW);
// Shifting Out bits i.e. using the random byte for LEDs states
shiftOut(DATA_595, CLOCK_595, MSBFIRST, LED_states);
// Put latch pin to HIGH (5V) & all data are pushed to outputs
digitalWrite(LATCH_595, HIGH);
// each 5000 loop() execution, grab a new seed for the random function
if (counter < 5000) counter++;
else
{
randomSeed(analogRead(0)); // read a new value from analog pin 0
counter = 0; // reset the counter
}
// make a short pause before changing LEDs states
delay(45);
}
全局移位寄存器编程模式
首先,让我们检查全局结构。
我首先定义了 595 移位寄存器的 3 个引脚。然后,我在setup()块中将它们每个都设置为输出。
然后,我有一个看起来类似的模式:
digitalWrite(latch-pin, LOW)
shiftOut(data-pin, clock-pin, MSBFIRST, my_states)
digitalWrite(latch-pin, HIGH)
这通常是移位寄存器操作的常规模式。正如之前所解释的,“锁存引脚”是提供给我们一种方式来通知集成电路我们想要将其加载数据,然后我们希望它将这些数据应用到其输出。
这有点像说:
-
锁存引脚低电平 = “嗨,让我们存储我即将发送给你的内容。”
-
锁存引脚高电平 = “好的,现在使用我刚刚发送的数据来转换到你的输出或不输出。”
然后,我们有这个shiftOut()函数。这个函数提供了一个简单的方法,通过特定的时钟/速率速度,将整个字节数据包发送到特定的引脚(数据引脚),并且给定一个传输顺序(MSBFIRST 或 LSBFIRST)。
尽管我们在这里不会描述底层的细节,但你必须理解 MSB 和 LSB 的概念。
让我们考虑一个字节:1 0 1 0 0 1 1 0。
MSB是最高有效位的缩写。这个位位于最左侧位置(具有最大值的位)。在这里,它的值是1。
LSB代表最低有效位。这个位位于最右侧位置(最小值的位)。它是位于最右侧的位(具有最小值的位)。在这里,它的值是0。
通过在shiftOut()函数中固定这个参数,我们提供了有关传输方向的特殊信息。实际上,我们可以通过发送这些位来发送前一个字节:1然后,0,然后1 0 0 1 1 0(MSBFIRST),或者通过发送这些位:0 1 1 0 0 1 0 1(LSBFIRST)。
玩转机会和随机种子
我想要提供一个关于我个人编程方式的例子。在这里,我将描述一个便宜且小巧的系统,它可以生成随机字节。然后,这些字节将被发送到 595,我们的 8 个 LED 数组将处于一个非常随机的状态。
在计算机中,随机并不是真正的随机。实际上,random()函数是一个伪随机数生成器。它也可以被称为确定性随机比特生成器(DRBG)。确实,序列是由一组小的初始值(包括种子)完全确定的。
对于特定的种子,伪随机数生成器每次都会生成相同的数字序列。
但是,你可以在这里使用一个技巧来稍微增加一些确定性。
想象一下你有时让种子变化。你还可以将外部随机因素引入你的系统。正如我们在本书之前解释的那样,即使没有连接到模拟输入,ADC 也会有电子噪声。你可以通过读取模拟输入 0 来使用这种外部/物理噪声。
如我们所知,模拟analogRead()提供了从 0 到 1023 的数字。这对于我们的目的来说是一个巨大的分辨率。
这就是我放在固件中的内容。
我定义了一个计数器变量和一个字节。我在setup()函数中首先读取来自模拟引脚 0 的 ADC 值。然后,我使用for()循环和bitWrite()函数生成随机字节。
我正在使用由random(2)数字函数生成的数字来编写字节LED_states的每个位,该函数随机地给出 0 或 1。然后,我将使用伪随机生成的字节到之前描述的结构中。
我通过读取模拟引脚 0 的 ADC 来重新定义每次 5000 次loop()执行的种子。
注意
如果您想在计算机上使用random()函数,包括 Arduino 和嵌入式系统,请获取一些物理和外部噪声。
现在,让我们继续前进。
我们可以使用许多 74HC595 移位寄存器来处理 LED,但想象一下你需要保留更多的数字引脚。好吧,我们已经看到我们可以通过移位寄存器节省很多。一个移位寄存器需要三个数字引脚并驱动八个 LED。这意味着我们通过每个移位寄存器节省了五个引脚,考虑到我们布线了八个 LED。
如果你需要更多呢?如果你需要为开关处理等保留所有其他引脚怎么办?
让我们现在进行菊花链连接!
菊花链多个 74HC595 移位寄存器
菊花链是一种布线方案,用于按顺序或甚至环形连接多个设备。
事实上,因为我们已经对移位寄存器的工作原理有了更多的了解,我们可以考虑将其扩展到一起布线的多个移位寄存器,不是吗?
我将通过使用 Juan Hernandez 的ShiftOutX库来向您展示如何做到这一点。我在版本 1.0 中得到了非常好的结果,并建议您使用这个版本。
您可以在此处下载:arduino.cc/playground/Main/ShiftOutX。您可以通过附录中解释的程序进行安装。
连接多个移位寄存器
每个移位寄存器需要了解什么?
串行时钟、锁存和数据是必须在整个设备链中传输的信息点。让我们检查一下原理图:

使用 Arduino 上的三个数字引脚驱动 16 个单色 LED 的串联移位寄存器
我使用了与之前电路相同的颜色来表示时钟(蓝色)、锁存器(绿色)和串行数据(橙色)。
串行时钟和锁存器在移位寄存器之间共享。来自 Arduino 的命令/指令必须与时钟同步,并告诉移位寄存器存储或应用接收到的数据到它们的输出,这必须是一致的。
来自 Arduino 的串行数据首先进入第一个移位寄存器,然后将其串行数据发送到第二个。这是级联概念的核心。
让我们检查电路图来记住这一点:

驱动 16 个单色 LED 的两个级联移位寄存器的电路图
处理两个移位寄存器和 16 个 LED 的固件
固件包括之前提到的ShiftOutX库 ShiftOutX。它为移位寄存器的级联提供了非常简单和流畅的处理。
这里是固件的代码。
你可以在Chapter08/Multiplexing_WithDaisyChain/文件夹中找到它:
#include <ShiftOutX.h>
#include <ShiftPinNo.h>
int CLOCK_595 = 4; // first 595 clock pin connecting to pin 4
int LATCH_595 = 3; // first 595 latch pin connecting to pin 3
int DATA_595 = 2; // first 595 serial data input pin connecting to pin 2
int SR_Number = 2; // number of shift registers in the chain
// instantiate and enabling the shiftOutX library with our circuit parameters
shiftOutX regGroupOne(LATCH_595, DATA_595, CLOCK_595, MSBFIRST, SR_Number);
// random groove machine variables
int counter = 0;
byte LED0to7_states = B00000000 ;
byte LED8to15_states = B00000000 ;
void setup() {
// NO MORE setup for each digital pin of the Arduino
// EVERYTHING is made by the library :-)
// use a seed coming from the electronic noise of the ADC
randomSeed(analogRead(0));
}
void loop(){
// generate a 2 random bytes
for (int i = 0 ; i < 8 ; i++)
{
bitWrite(LED0to7_states, i, random(2));
bitWrite(LED8to15_states, i, random(2));
}
unsigned long int data; // declaring the data container as a very local variable
data = LED0to7_states | (LED8to15_states << 8); // aggregating the 2 random bytes
shiftOut_16(DATA_595, CLOCK_595, MSBFIRST, data); // pushing the whole data to SRs
// each 5000 loop() execution, grab a new seed for the random function
if (counter < 5000) counter++;
else
{
randomSeed(analogRead(0)); // read a new value from analog pin 0
counter = 0; // reset the counter
}
// make a short pause before changing LEDs states
delay(45);
}
ShiftOutX 库可以用多种方式使用。我们在这里使用它的方式与ShiftOut相同,它是核心库的一部分,适用于仅使用一个移位寄存器。
首先,我们必须使用草图 | 导入库 | ShiftOutX来包含库。
它在开头包含两个头文件,即ShiftOutX.h和ShiftPinNo.h。
然后,我们定义一个新的变量来存储链中的移位寄存器数量。
最后,我们使用以下代码实例化 ShiftOutX 库:
shiftOutX regGroupOne(LATCH_595, DATA_595, CLOCK_595, MSBFIRST, SR_Number);
setup()中的代码有所改变。实际上,不再有数字引脚的设置语句。这部分由库处理,这看起来可能有些奇怪,但却是非常常见的。实际上,在你实例化库之前,你传递了三个 Arduino 引脚作为参数,而这个语句实际上也设置了引脚为输出。
loop()块几乎和之前一样。实际上,我又加入了带有模拟读取技巧的小随机槽道机。但这次我创建了两个随机字节。确实,这是因为我需要 16 个值,并且我想使用shiftOut_16函数在同一个语句中发送所有我的数据。生成字节然后通过位运算符将它们聚合到unsigned short int数据类型中是非常简单和常见的。
让我们详细说明这个操作。
当我们生成随机字节时,我们有两个 8 位的序列。让我们看以下例子:
0 1 1 1 0 1 0 0
1 1 0 1 0 0 0 1
如果我们想将它们存储在一个地方,我们能做什么呢?我们可以先移位一个,然后将移位后的结果加到另一个上,不是吗?
0 1 1 1 0 1 0 0 << 8 = 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0
然后,如果我们使用位运算符(|)添加一个字节,我们得到:
0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0
| 1 1 0 1 0 0 0 1
= 0 1 1 1 0 1 0 0 1 1 0 1 0 0 0 1
结果似乎是将所有位连接在一起。
这就是我们在这部分代码中所做的事情。然后我们使用shiftOut_16()将所有数据发送到两个移位寄存器。嘿,我们应该如何处理这四个移位寄存器呢?以同样的方式,同样地处理!
可能我们不得不使用 << 32、<< 16 和再次 << 8 来移位,以便将所有字节存储到一个变量中,我们可以使用 shiftOut_32() 函数发送这个变量。
通过使用这个库,你可以有两个组,每个组包含八个移位寄存器。
这意味着什么?
这意味着你可以使用仅四个引脚(两个锁存器,但共用串行时钟和数据)来驱动 2 x 8 x 8 = 128 个输出。这听起来很疯狂,不是吗?
在现实生活中,完全可能只使用一个 Arduino 来构建这种架构,但我们必须注意一个非常重要的事情,那就是电流量。在这个 128 个 LED 的特定情况下,我们应该想象最坏的情况,即所有 LED 都被打开。驱动的电流量甚至可能烧毁 Arduino 板,它有时会通过复位来保护自己。但就我个人而言,我甚至不会尝试。
当前简要考虑
Arduino 板,使用 USB 电源供电,不能驱动超过 500 mA。所有组合引脚不能驱动超过 200 mA,且没有任何引脚可以驱动超过 40 mA。这些可能因板型而异,但这些是真实、绝对的最大额定值。
我们没有进行这些考虑和以下计算,因为在我们的例子中,我们只使用了少数设备和组件,但有时你可能会被诱惑去构建一个巨大的设备,比如我有时做的,例如,使用 Protodeck 控制器。
让我们通过一个例子来更仔细地看看一些电流计算。
想象一下,你有一个 LED,它需要大约 10 mA 才能正确地发亮(第二次闪烁时不会烧毁!!)
这意味着如果你要同时打开所有 LED,一个包含八个 LED 的数组将会有 8 x 10 mA,由一个 595 移位寄存器驱动。
80 mA 将是来自 Arduino Vcc 源的一个 595 移位寄存器驱动的全局电流。
如果你有多于 595 移位寄存器,电流的幅度会增加。你必须知道所有集成电路也会消耗电流。它们的消耗通常不被考虑,因为非常小。例如,595 移位寄存器电路本身只消耗大约 80 微安培,这意味着 0.008 mA。与我们的 LED 相比,这是微不足道的。电阻器也会消耗电流,尽管它们经常被用来保护 LED,但它们非常有用。
无论如何,我们即将学习另一个非常巧妙且实用的技巧,它可以用于单色或 RGB LED。
让我们进入一个充满色彩的世界。
使用 RGB LED
RGB 代表红色、绿色和蓝色,正如你可能猜到的。
我不谈论那些可以根据你施加的电压改变颜色的 LED。这种类型的 LED 存在,但据我所实验,这些并不是学习步骤时的最佳选择,尤其是在学习初期。
我在谈论共阴极和共阳极 RGB LED。
一些控制概念
你需要什么来控制一个 LED?
你需要能够向其引脚施加电流。更准确地说,你需要能够在其引脚之间产生电位差。
这个原理的直接应用就是我们已经在本章的第一部分测试过的,这让我们想起了我们如何开关一个 LED:你需要使用 Arduino 的数字输出引脚来控制电流,知道你想要控制的 LED 的节点连接到输出引脚,其阴极连接到地,线上还有一个电阻。
我们可以讨论不同的控制方式,你将会通过下一张图片很快理解这一点。
为了使数字输出能够提供电流,我们需要使用digitalWrite写入一个HIGH值。在这种情况下,所考虑的数字输出将内部连接到 5V 电池,并产生 5V 电压。这意味着连接到它和地之间的 LED 将通过电流供电。
在另一种情况下,如果我们给 LED 施加 5V 电压,并且想要将其开启,我们需要将一个LOW值写入与之相连的数字引脚。在这种情况下,数字引脚将内部连接到地,并吸收电流。
这是控制电流的两种方式。
检查以下图示:

不同类型的 RGB LED
让我们检查两种常见的 RGB LED:

基本上,一个封装中包含三个 LED,内部有不同的接线方式。这个封装的制作方式并不是关于内部的接线,但在这里我不会争论这一点。
如果你正确地跟随我,你可能已经猜到我们需要更多的数字输出来连接 RGB LED。确实,上一节讨论了节省数字引脚。我想你明白为什么节省引脚和仔细规划电路架构可能很重要。
点亮 RGB LED
检查这个基本电路:

将 RGB LED 连接到 Arduino
现在检查一下代码。你可以在Chapter08/One_RGB_LED/文件夹中找到它。
int pinR = 4; // pin related to Red of RGB LED
int pinG = 3; // pin related to Green of RGB LED
int pinB = 2; // pin related to Blue of RGB LED
void setup() {
pinMode(pinR, OUTPUT);
pinMode(pinG, OUTPUT);
pinMode(pinB, OUTPUT);
}
void loop() {
for (int r = 0 ; r < 2 ; r++)
{
for (int g = 0 ; g < 2 ; g++)
{
for (int b = 0 ; b < 2 ; b++)
{
digitalWrite(pinR,r); // turning red pin to value r
digitalWrite(pinG,g); // turning green pin to value g
digitalWrite(pinB,b); // turning blue pin to value b
delay(150); // pausing a bit
}
}
}
}
再次,代码中包含了一些提示。
红色、绿色和蓝色光组件和颜色
首先,这里的关键点是什么?我想让 RGB LED 循环通过所有可能的状态。一些数学可以帮助列出所有状态。
我们有一个包含三个元素的有序列表,每个元素可以是开启或关闭。因此,总共有 2³ 个状态,即总共 8 个状态:
| R | G | B | 结果颜色 |
|---|---|---|---|
| 关闭 | 关闭 | 关闭 | 关闭 |
| 关闭 | 关闭 | 开启 | 蓝色 |
| 关闭 | 开启 | 关闭 | 绿色 |
| 关闭 | 开启 | 开启 | 青色 |
| 开启 | 关闭 | 关闭 | 红色 |
| 开启 | 关闭 | 开启 | 紫色 |
| 开启 | 开启 | 关闭 | 橙色 |
| 开启 | 开启 | 开启 | 白色 |
只有通过开关每个颜色组件的开启或关闭,我们才能改变全局 RGB LED 的状态。
不要忘记,系统的工作方式与我们通过 Arduino 的三个数字输出控制三个单色 LED 完全一样。
首先,我们定义了三个变量来存储不同颜色的 LED 连接器。
然后,在setup()中,我们将这三个引脚设置为输出。
多重嵌套的 for()循环
最后,loop()块包含三重嵌套的for()循环。这是什么?这是一个很好的高效方法,可以确保匹配所有可能的情况。这也是循环每个可能数字的简单方法。让我们检查第一步,以便更好地理解嵌套循环的概念:
-
第 1 步:r = 0, g = 0, 和 b = 0表示所有东西都是关闭的,然后在那个状态下暂停 150ms
-
第 2 步:r = 0, g = 0, 和 b = 1表示只有蓝色被打开,然后在那个状态下暂停 150ms
-
第 3 步:r = 0, g = 1, 和 b = 0表示只有绿色被打开,然后在那个状态下暂停 150ms
最内层的循环总是执行次数最多的。
这可以吗?当然可以!
你也可能已经注意到我没有将HIGH或LOW作为digitalWrite()函数的参数。事实上,HIGH和LOW是在 Arduino 核心库中定义的常量,并且分别替换 1 和 0 的值。
为了证明这一点,特别是为了第一次向您展示 Arduino 核心文件的位置,这里要检查的重要文件是Arduino.h。
在 Windows 系统上,它可以在 IDE 版本的一些子目录中的Arduino文件夹中找到。
在 OS X 上,它位于Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/Arduino.h。我们可以通过右键单击包本身来查看应用程序包的内容。
在这个文件中,我们可以读取一大串常量,以及其他许多定义。
最后,我们可以检索以下内容:
#define HIGH 0x1
#define LOW 0x0
是的,HIGH和LOW关键字只是 1 和 0 的常量。
这就是我为什么直接通过嵌套循环将digitalWrite()的0和1传递给digitalWrite()的原因,循环遍历每个 LED 的所有可能状态,从而遍历 RGB LED 的所有状态。
使用这个概念,我们将进一步挖掘,通过制作一个 LED 数组。
构建 LED 数组
LED 数组基本上是将 LED 作为矩阵连接起来的。
我们将一起构建一个 3x3 的 LED 矩阵。这并不难,我们将以一个非常棒、整洁和智能的概念来处理这个任务,这个概念可以真正优化您的硬件设计。
让我们检查这本书中最简单的电路图:

当电流通过 LED 时,当电压施加到其引脚上时,LED 可以闪烁
为了关闭前一张截图显示的 LED,我们可以停止在该节点创建 5V 电流。没有电压意味着没有电流供应。我们也可以切断电路本身来关闭 LED。最后,我们可以通过添加一个 5V 源电流来改变接地。
这意味着一旦电位差被消除,LED 就会关闭。
LED 数组基于这些可能的双重控制。
我们将在这里介绍一个新的组件,晶体管。
一个新朋友,晶体管
晶体管是我们在这本书的第一部分稍微介绍过的一种特殊组件。

带有三条腿的普通 NPN 晶体管
此组件通常在以下三种主要情况下使用:
-
作为逻辑电路中的数字开关
-
作为信号放大器
-
作为与其他组件结合的电压稳定器
晶体管是世界上最广泛使用的组件。它们不仅用作离散组件(独立的),而且还与其他许多组件结合成一个高密度系统,例如在处理器中。
达尔林顿晶体管阵列,ULN2003
我们将在这里使用晶体管,因为它包含在一个名为 ULN2003 的集成电路中。多么漂亮的名字!一个更明确的名称是高电流****达尔林顿晶体管阵列。好吧,我知道这没有帮助!

其数据表可以在以下位置找到
www.ti.com/lit/ds/symlink/uln2003a.pdf.
它包含七个输入引脚和七个输出引脚。我们还可以看到 0 V 引脚(编号 8)和 COM 引脚 9。
原理简单而神奇:
-
必须将 0 V 连接到地
-
如果你对输入n施加 5 V,输出n将转换到地
如果你将 0 V 施加到输入n,输出n将断开连接。
这可以很容易地用作开关的电流吸收阵列。
与 74HC595 结合,我们现在将驱动我们的 3 x 3 LED 矩阵:

一种情况是输入 1 和 2 被供电,导致输出 1 和 2(引脚 16 和 14)的转换
LED 矩阵
让我们看看我们如何布线我们的矩阵,记住我们必须能够独立控制每个 LED,当然。
这种设计非常常见。你可以很容易地找到这种方式的现成 LED 矩阵,它们以带有与行和列相关的连接器的包装形式出售。
LED 矩阵基本上是一个数组,其中:
-
每行都突出一个与该行所有阳极相关的连接器
-
每列都突出一个与该列所有阴极相关的连接器
这不是法律,我发现有些矩阵完全相反地布线,有时相当奇怪。所以,小心并检查数据表。在这里,我们将研究一个非常基本的 LED 矩阵,以便深入了解这个概念:

一个基本的 3 x 3 LED 矩阵
让我们看看 LED 矩阵架构的概念。
我们如何控制它?在这里,我指的是将好的 LED 指向好的行为,从开启或关闭。
让我们想象一下,如果我们想点亮LED 2,我们必须:
-
将行 1连接到 5 V
-
将列 2连接到地
太好了!我们可以点亮那个LED 2。
让我们进一步。让我们想象一下,如果我们想点亮LED 2和LED 4,我们必须:
-
将ROW 1连接到 5 V
-
将COLUMN 2连接到地
-
将ROW 2连接到 5 V
-
将COLUMN 1连接到地
你注意到什么了吗?
如果你仔细遵循步骤,你应该在你的矩阵上看到一些奇怪的东西:
LED 1、LED 2、LED 4和LED 5将被点亮
出现了问题:如果我们给ROW 1接上 5 V,如何区分COLUMN 1和COLUMN 2?
我们将看到这并不难,而且这只是一个与我们视觉持久性相关的小技巧。
自行车和视角
我们可以通过快速循环我们的矩阵来处理上一节遇到的问题。
诀窍是每次只打开一列。当然,也可以通过每次只打开一行来实现。
让我们回顾一下之前的问题:如果我们想点亮LED 2和LED 4,我们必须:
-
将ROW 1连接到 5 V 和COLUMN 1仅连接到 5 V
-
然后,将ROW 2连接到 5 V,仅将COLUMN 2连接到 5 V
如果我们非常快地这样做,我们的眼睛不会看到每次只有一个 LED 被点亮。
伪代码将是:
For each column
Switch On the column
For each row
Switch on the row if the corresponding LED has to be switched On
电路
首先,电路必须被设计。它看起来是这样的:

将 Arduino 连接到 595 移位寄存器,通过 ULN2003 驱动每一行和每一列
现在我们来检查电路图:

显示矩阵行和列处理的电路图
我们现在已知的是 74HC595 移位寄存器。
这个是连接到 ULN2003 移位寄存器和矩阵的行,ULN2003 连接到矩阵的列。
那是什么设计模式?
移位寄存器从 Arduino 通过其数字引脚 2 发送的基于串行协议的消息中获取数据。正如我们之前测试的那样,移位寄存器被时钟到 Arduino,一旦其锁存引脚连接到高电平(等于 5 V),它就会根据 Arduino 发送给它的数据驱动输出到 5V 或不驱动。因此,我们可以通过发送到移位寄存器的数据来控制矩阵的每一行,通过是否提供 5V 来控制它们。
为了点亮 LED,我们必须关闭它们所插的电路,即电线路。我们可以给ROW 1提供 5V 电流,但如果我们不将这个或那个列接地,电路就不会闭合,并且没有 LED 会被点亮。对吧?
ULN2003 正是为了地面换流而制造的,正如我们之前看到的。如果我们给其一个输入提供 5V,它就会将相应的输出n引脚换流到地。因此,通过我们的 595 移位寄存器,我们可以控制行的 5V 换流和列的接地换流。我们现在完全控制了我们的矩阵。
尤其是我们将检查代码,包括之前解释的列的电源周期。
3 x 3 LED 矩阵代码
你可以在Chapter08/LedMatrix3x3/文件夹中找到以下 3x3 LED 矩阵代码:
int CLOCK_595 = 4; // first 595 clock pin connecting to pin 4
int LATCH_595 = 3; // first 595 latch pin connecting to pin 3
int DATA_595 = 2; // first 595 serial data pin connecting to pin 2
// random groove machine variables
int counter = 0;
boolean LED_states[9] ;
void setup() {
pinMode(LATCH_595, OUTPUT);
pinMode(CLOCK_595, OUTPUT);
pinMode(DATA_595, OUTPUT);
// use a seed coming from the electronic noise of the ADC
randomSeed(analogRead(0));
}
void loop() {
// generate random state for each 9 LEDs
for (int i = 0 ; i < 9 ; i++)
{
LED_states[i] = random(2) ;
}
// initialize data at each loop()
byte data = 0;
byte dataRow = 0;
byte dataColumn = 0;
int currentLed = 0;
// cycling columns
for (int c = 0 ; c < 3 ; c++)
{
// write the 1 at the correct bit place (= current column)
dataColumn = 1 << (4 - c);
// cycling rows
for (int r = 0 ; r < 3 ; r++)
{
// IF that LED has to be up, according to LED_states array
// write the 1 at the correct bit place (= current row)
if (LED_states[currentLed]) dataRow = 1 << (4 - c);
// sum the two half-bytes results in the data to be sent
data = dataRow | dataColumn;
// Put latch pin to LOW (ground) while transmitting data to 595
digitalWrite(LATCH_595, LOW);
// Shifting Out bits
shiftOut(DATA_595, CLOCK_595, MSBFIRST, data);
// Put latch pin to HIGH (5V) & all data are pushed to outputs
digitalWrite(LATCH_595, HIGH);
dataRow = 0; // resetting row bits for next turn
currentLed++;// incrementing to next LED to process
}
dataColumn = 0;// resetting column bits for next turn
}
// each 5000 loop() execution, grab a new seed for the random function
if (counter < 5000) counter++;
else
{
randomSeed(analogRead(0)); // read a new value from analog pin 0
counter = 0; // reset the counter
}
// pause a bit to provide a cuter fx
delay(150);
}
这段代码带有注释,相当自解释,但让我们更详细地检查一下。
全局结构让人联想到 Multiplexing_8Leds 中的结构。
我们有一个名为 LED_states 的整数数组。我们在其中存储每个 LED 状态的值。setup()块相当简单,定义用于与 595 移位寄存器通信的每个数字引脚,然后从 ADC 获取一个随机种子。loop()函数稍微复杂一些。首先,我们生成九个随机值并将它们存储在 LED_states 数组中。然后,我们初始化/定义一些值:
-
data是发送到移位寄存器的字节。 -
dataRow是处理行状态的字节部分(是否转换为 5V)。 -
dataColumn是处理列状态的字节部分(是否转换为地)。 -
currentLed保留当前由 LED 处理的跟踪。
然后,那些嵌套的循环发生。
对于每一列(第一个 for()循环),我们通过使用一个小巧、便宜且快速的位运算符来激活循环:
dataColumn = 1 << (4 – c);
(4 – c) 从4到2,在整个第一个loop()函数中;然后,dataColumn从0 0 0 1 0 0 0 0变为0 0 0 0 1 0 0 0,最后变为0 0 0 0 0 1 0 0。
这里发生了什么?一切都是关于编码。
前三位(从左边开始,最高位 MSB)处理矩阵的行。确实,三行连接到 595 移位寄存器的Q0、Q1和Q2引脚。
第二个三位组处理 ULN2003,它本身处理列。
通过从 595 的Q0、Q1和Q2提供 5V,我们处理行。通过从 595 的Q3、Q4和Q5提供 5V,我们通过 ULN2003 处理列。
好的!
我们仍然有两个未使用的位在这里,最后两个。
让我们再次看看我们的代码。
在 for()循环的每次列转换中,我们将对应于列的位向右移动,将每个列循环性地转换为地。
然后,对于每一列,我们以相同的模式循环行,测试我们必须要推送到 595 的相应 LED 的状态。如果 LED 需要打开,我们使用相同的位运算技巧将相应的位存储在dataRow变量中。
然后,我们将这两部分相加,得到数据变量。
例如,如果我们处于第二行和第二列,并且需要打开 LED,那么存储的数据将是:
0 1 0 0 1 0 0 0。
如果我们处于(1,3),那么存储的数据将是:
1 0 0 0 0 1 0 0.
然后,我们有一个模式,将锁存器设置为低电平,将存储在数据中的位移出到移位寄存器,然后通过将锁存器设置为高电平将数据提交到 Q0 到 Q7 输出,为电路中的正确元素提供能量。
在处理完每一行后,我们重置对应于前三个行的三位,并增加currentLed变量。
在处理完每一列的末尾,我们重置与下一列对应的三个位。
这种全局嵌套结构确保我们一次只能有一个 LED 开启。
电流消耗会有什么后果?
我们只有一个 LED 供电,这意味着我们的最大功耗可能被九等分。是的,听起来很棒!
然后,我们有模式抓取,每次 5000 次 loop()循环抓取一个新的种子。
我们刚刚学会了如何轻松地处理 LED 矩阵,同时减少功耗。
但是,我不满意。通常,创造者和艺术家通常永远不会完全满意,但在这里,请相信我,情况不同;我们可以做得比仅仅开关 LED 更好。我们还可以调节亮度,从非常低的强度切换到非常高的强度,产生不同的光色。
使用 PWM 模拟模拟输出
如我们所知,开关 LED 是没问题的,而且正如我们将在下一章中看到的,使用 Arduino 的数字引脚作为输出开关许多东西也是可以的。
我们也知道如何从设置为输入的数字引脚读取状态,甚至从 ADC 中的模拟输入读取 0 到 1023 的值。
就我们所知,Arduino 上没有模拟输出。
模拟输出会添加什么?它会提供一种写入除了只有 0 和 1 之外的其他值的方法,我的意思是 0V 和 5V。这会很棒,但需要昂贵的 DAC。
事实上,Arduino 板上没有 DAC。
脉宽调制概念
脉宽调制是一种非常常见的用于模拟输出行为的模拟技术。
让我们换一种说法。
我们的数字输出只能处于 0V 或 5V。但在特定的时间间隔内,如果我们快速开关它们,那么我们可以根据在 0V 或 5V 下经过的时间计算平均值。这个平均值可以很容易地用作一个值。
查看以下电路图以了解更多关于占空比的概念:

占空比和 PWM 的概念
在 5V 下花费的平均时间定义了占空比。这个值是引脚在 5V 时的平均时间,并以百分比给出。
analogWrite()是一个特殊函数,可以在特定的占空比下生成稳定的方波,直到下一次调用。
根据 Arduino 核心文档,PWM 信号以 490Hz 的频率脉冲。我还没有(现在)验证这一点,但使用示波器等工具才能真正实现。
备注
注意:不是你板上的每个引脚都支持 PWM!
例如,Arduino Uno 和 Leonardo 在数字引脚 3、5、6、9、10 和 11 上提供 PWM。
在尝试任何操作之前,你必须知道这一点。
调暗 LED
让我们检查一个基本电路来测试 PWM:

让我们看看电路图,即使它很明显:

我们将使用 David A. Mellis 的 Fading 示例,并由 Tom Igoe 修改。在文件 | 示例 | 03.模拟 | Fading中检查它。我们将把ledPin值从9改为11以适应我们的电路。
这里是修改后的样子:
int ledPin = 11; // LED connected to digital pin 11 (!!)
void setup() {
// nothing happens in setup
}
void loop() {
// fade in from min to max in increments of 5 points:
for(int fadeValue = 0 ; fadeValue <= 255; fadeValue +=5) {
// sets the value (range from 0 to 255):
analogWrite(ledPin, fadeValue);
// wait for 30 milliseconds to see the dimming effect
delay(30);
}
// fade out from max to min in increments of 5 points:
for(int fadeValue = 255 ; fadeValue >= 0; fadeValue -=5) {
// sets the value (range from 0 to 255):
analogWrite(ledPin, fadeValue);
// wait for 30 milliseconds to see the dimming effect
delay(30);
}
}
上传它,测试它,并爱上它!
更高分辨率的 PWM 驱动组件
当然,有提供更高 PWM 分辨率的组件。在这里,使用原生的 Arduino 板,我们有 8 位分辨率(256 个值)。我想指出的是德州仪器的 TLC5940。您可以在以下位置找到其数据表:www.ti.com/lit/ds/symlink/tlc5940.pdf。

TLC5950,一个提供 PWM 控制的 16 通道 LED 驱动器
小心,它是一个恒流源驱动器。这意味着它会吸收电流而不是提供电流。例如,您需要将 LED 的阴极连接到OUT0和OUT15引脚,而不是阳极。如果您想使用这样的特定驱动器,当然不会使用analogWrite()。为什么?因为这个驱动器作为一个移位寄存器,通过串行连接与我们的 Arduino 相连。
我建议使用一个名为 tlc5940arduino 的库,它可在 Google 代码上找到
code.google.com/p/tlc5940arduino/
在本书的第三部分,我们将看到如何在 LED 矩阵上写消息。但是,也有一种使用最高分辨率显示器的不错方法:LCD。
LCD 快速入门
LCD代表液晶显示器。我们在日常生活中使用 LCD 技术,如手表、数字码显示等。环顾四周,检查这些小或大的 LCD。
存在两种主要的 LCD 显示器系列:
-
字符 LCD 基于字符矩阵(列 x 行)
-
图形 LCD,基于像素矩阵
现在可以以便宜的价格找到许多包含 LCD 和连接器,用于将它们与 Arduino 和其他系统接口的印刷电路板。
现在 Arduino 核心中包含了一个库,使用起来非常简单。它的名字是LiquidCrystal,它与所有兼容 Hitachi HD44780 驱动器的 LCD 显示器一起工作。这个驱动器非常常见。
日立将其开发为一个非常专用的驱动器,它本身包含一个微控制器,专门用于驱动字符 LCD 并轻松连接到外部世界,这可以通过一个特定的链接完成,通常使用 16 个连接器,包括为外部电路本身和背光供电:

16 x 2 字符 LCD
我们将对其进行布线并在其上显示一些消息。
兼容 HD44780 的 LCD 显示电路
这里是 HD44780 兼容 LCD 显示电路的基本电路:

一个连接到 Arduino 和电位器的 16 x 2 字符 LCD,用于控制其对比度
对应的电路图如下:

字符 LCD、电位器和 Arduino 板电路图
如果你已经有足够的光线,LED+和 LED-不是必需的。使用电位器,你还可以设置 LCD 的对比度,以便有足够的可读性。
顺便说一句,LED+和 LED-分别是内部 LED 的背光阳极和背光阴极。你可以从 Arduino 驱动这些,但这可能会导致更多的功耗。请仔细阅读 LCD 说明和数据表。
显示一些随机消息
这里有一些整洁的固件。你可以在Chapter08/basicLCD/文件夹中找到它:
#include <LiquidCrystal.h>
String manyMessages[4];
int counter = 0;
// Initialize the library with pins number of the circuit
// 4-bit mode here without RW
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
void setup() {
// set up the number of column and row of the LCD
lcd.begin(16, 2);
manyMessages[0] = "I am the Arduino";
manyMessages[1] = "I can talk";
manyMessages[2] = "I can feel";
manyMessages[3] = "I can react";
// shaking the dice!
randomSeed(analogRead(0);
}
void loop() {
// set the cursor to column 0 and row 0
lcd.setCursor(0, 0);
// each 5s
if (millis() - counter > 5000)
{
lcd.clear(); // clear the whole LCD
lcd.print(manyMessages[random(4)]); // display a random message
counter = millis(); // store the current time
}
// set the cursor to column 0 and row 1
lcd.setCursor(0, 1);
// print the value of millis() at each loop() execution
lcd.print("up since: " + millis() + "ms");
}
首先,我们必须包含LiquidCrystal库。然后,我们定义两个变量:
-
manyMessages是一个用于存储消息的 String 数组 -
counter是一个用于时间追踪的变量
然后,我们通过向其构造函数传递一些变量来初始化LiquidCrystal库,这些变量对应于连接 LCD 到 Arduino 所使用的每个引脚。当然,引脚的顺序很重要。它是:rs、enable、d4、d5、d6和d7。
在setup()中,我们根据硬件定义 LCD 的大小,这里将是 16 列和两行。
然后,我们在 String 数组的每个元素中静态存储一些消息。
在loop()块中,我们首先将光标放置在 LCD 的第一位置。
我们测试表达式(millis() – counter > 5000),如果为真,则清除整个 LCD。然后,我打印一个随机定义的消息。实际上,random(4)生成一个介于 0 和 3 之间的伪随机数,由于索引是随机的,我们在setup()中定义的四个消息之一随机打印到 LCD 的第一行。
然后,我们存储当前时间,以便能够测量自上次显示随机消息以来经过的时间。
然后,我们将光标置于第二行的第一列,然后打印一个由常数和变量部分组成的 String,显示自 Arduino 板上次重置以来的毫秒数。
摘要
在这个漫长的章节中,我们学习了如何处理许多事情,包括单色 LED 到 RGB LED,使用移位寄存器和晶体管阵列,甚至介绍了 LCD 显示。我们深入研究了在不使用电脑的情况下从 Arduino 显示视觉反馈。
在许多实际设计案例中,我们可以找到完全独立使用 Arduino 板的项目,无需电脑。通过使用特殊库和特定组件,我们现在知道我们可以让我们的 Arduino 感觉、表达和反应。
在下一章中,我们将解释并深入研究一些其他概念,例如让 Arduino 移动,最终生成声音。
第九章. 使物体移动和创造声音
如果 Arduino 板可以通过传感器来监听和感受,它也可以通过使物体移动来做出反应。
通过运动概念,我指的是以下两个方面:
-
物体运动
-
产生声音的空气运动
我们将学习如何控制名为伺服的小型电机,以及如何通过使用晶体管来处理高电流控制。
然后,我们将开始谈论声音产生的基础知识。这是尝试产生任何声音,即使是简单的声音之前的一个要求。这是我们将描述模拟和数字概念的部分。
最后,我们将设计一个非常基本的随机合成器,可以通过 MIDI 进行控制。我们还将介绍一个非常棒的库,称为PCM,它提供了一种简单的方法,可以将样本播放功能添加到你的 8 位微控制器中。
使物体振动
我们可以在这里介绍的最简单的项目之一就是使用一个小型的压电传感器。
这是我们在设计中设计的第一个基本可感知的动作。当然,我们之前已经设计了许多视觉反馈,但这是我们第一个真正影响固件的现实世界对象。
这种类型的反馈在非视觉环境中非常有用。我为一个人设计了一个小项目,他想在他的反应式装置中向参观者发送反馈。参观者必须穿上一件包含一些电子组件的 T 恤,例如 LilyPad 和一些压电传感器。LED 反馈不是我们之前用来向穿戴者发送反馈的解决方案,我们决定发送振动。这些压电传感器分布在 T 恤的两侧,以产生不同的反馈,以响应不同的交互。
但我不是在谈论传感器振动时犯了一个错误吗?
压电传感器
马达式传感器是一种利用压电效应的组件。
这种效应被定义为某些特定材料中机械状态和电状态之间的线性机电相互作用。
基本上,这个设备上的机械动作会产生电能,使其可用于运动和振动检测。但这里的好之处在于效果是相互的——如果你给它施加电流,它就会振动。
这就是为什么我们在这里使用压电传感器的原因。我们将其用作振动发生器。
压电传感器也常被用作音调发生器。我们将在稍后更深入地探讨空气振动和声音之间的关系,但在这里提一下也很重要。
连接振动电机
压电传感器通常消耗大约 10 mA 到 15 mA 的电流,这非常小。
当然,你需要检查你将要使用的设备的正确数据表。我使用Sparkfun(www.sparkfun.com/products/10293)的设备取得了很好的效果。布线非常简单——只有两个引脚。以下图像显示了压电传感器/振动器如何通过具有 PWM 功能的数字引脚连接到 Arduino:

请注意,我已经将压电器件连接到一个具有 PWM 功能的数字引脚。我在上一章中解释了 PWM。
这里是电路原理图。这个压电元件显示为一个小的蜂鸣器/扬声器:

当然,由于我们将使用 PWM,这意味着我们将模拟模拟输出电流。考虑到占空比的概念,我们可以使用analogWrite()函数给压电器件供电,然后以不同的电压供电。
生成振动的固件
检查固件。它也位于Chapter09/vibrations/文件夹中。
int piezoPin = 9;
int value = 0; // stores the current feed value
int incdec = 1; // stores the direction of the variation
void setup() {
}
void loop() {
// test current value and change the direction if required
if (value == 0 || value == 255) incdec *= -1;
analogWrite(piezoPin, value + incdec);
delay(30);
}
我们在这里再次使用analogWrite()函数。这个函数将数字引脚作为参数和值。这个值从 0 到 255 是占空比。它基本上模拟了模拟输出。
我们使用incdec(表示递增-递减)参数的常规方式。我们在每次loop()执行时存储我们想要使用的增量值。
当值达到边界,即 0 或 255 时,这个增量会改变,并发生反转,提供了一种从 0 到 255,然后到 0,然后到 255,依此类推的循环的廉价方法。
这个固件使压电器件从低频率到高频率循环振动。
现在让我们控制更大的电机。
高电流驱动和晶体管
我们在上一章讨论了晶体管。我们将其用作数字开关。它们也可以用作放大器、电压稳定器和许多其他相关应用。
你几乎可以在任何地方找到晶体管,而且它们相当便宜。你可以在www.fairchildsemi.com/ds/BC/BC547.pdf找到完整的数据表。
以下是一个基本图解,解释了晶体管的工作原理:

在逻辑电路中用作数字开关的晶体管
晶体管有以下引脚:
-
收集极
-
基极
-
发射极
如果我们通过向其施加 5 V 电源来饱和基极,那么来自收集器的所有电流都将通过发射极传输。
当这样使用时,NPN 晶体管是一种很好的开关高电流的方法,Arduino 本身无法驱动。顺便说一句,这个开关可以用 Arduino 控制,因为它只需要向晶体管的基极提供非常小的电流。
注意
向晶体管基极发送 5 V 电压会闭合电路。将晶体管基极接地会打开电路。
在任何需要外部电源来驱动电机的情况下,我们使用这种设计模式。
让我们现在学习小型电流伺服电机,然后进一步使用晶体管。
控制伺服电机
伺服电机也被定义为一种允许对角位置进行非常精细控制的旋转执行器。
许多伺服电机都很常见且价格低廉。我使用 Spring Model Electronics 的 43 R 伺服电机取得了很好的效果。你可以在www.sparkfun.com/datasheets/Robotics/servo-360_e.pdf找到数据表。
伺服电机可以驱动大量的电流。这意味着在没有使用外部电源的情况下,你无法在 Arduino 板上使用超过一个或两个伺服电机。
我们什么时候需要伺服电机?
每当我们需要一种控制与旋转角度相关的位置的方法时,我们都可以使用伺服电机。
伺服电机不仅可以用来移动小部件和使物体旋转,还可以用来移动包括在内的物体。机器人就是这样工作的,网上有很多有趣的 Arduino 相关机器人项目。
在机器人的情况下,伺服电机外壳固定在机械臂的一部分,而机械臂的另一部分则固定在伺服电机的旋转部分。
如何使用 Arduino 控制伺服电机
有一个非常好的库应该首先使用,名为Servo。
这个库在大多数 Arduino 板上支持多达 12 个电机,在 Arduino Mega 上支持多达 48 个。
通过使用 Mega 以外的其他 Arduino 板,我们可以找出一些软件限制。例如,引脚 9 和 10 不能用于 PWM 的analogWrite()方法(arduino.cc/en/Reference/analogWrite)。
伺服电机提供的是三引脚封装:
-
5 V
-
地线
-
脉冲;即控制引脚
基本上,电源可以很容易地由外部电池提供,而脉冲仍然保持在 Arduino 板上。
让我们来检查基本的接线。
接线一个伺服电机
以下是一个将伺服电机连接到 Arduino 板以供电和控制的双向电路图:

相应的电路图如下:

一个伺服电机和 Arduino
我们基本上处于一个非常常见的基于数字输出的控制模式。
让我们现在检查代码。
使用 Servo 库控制一个伺服电机的固件
这里是一个提供从 0 度到 180 度循环运动的固件。它也位于Chapter09/OneServo/文件夹中。
#include <Servo.h>
Servo myServo; // instantiate the Servo object
int angle = 0; // store the current angle
void setup()
{
// pin 9 to Servo object myServo
myServo.attach(9);
}
void loop()
{
for(angle = 0; angle < 180; angle += 1)
{
myServo.write(angle);
delay(20);
}
for(angle = 180; angle >= 1; angle -=1)
{
myServo.write(angle);
delay(20);
}
}
我们首先包含Servo库头文件。
然后我们实例化一个名为myServo的Servo对象实例。
在setup()块中,我们必须做一些特别的事情。我们将引脚 9 连接到myServo对象。这明确地将引脚定义为Servo实例myServo的控制引脚。
在loop()块中,我们有两个for()循环,看起来和之前的压电设备示例类似。我们定义一个循环,逐步增加角度变量从 0 到 180,然后从 180 递减到 0,每次我们暂停 20 毫秒。
这里还有一个未使用的函数,我想提一下,Servo.read()。
这个函数读取伺服电机的当前角度(即传递给write()的最后一个调用值)。如果我们不希望在每个循环中存储动态内容,这可能很有用。
使用外部电源的多伺服电机
让我们想象我们需要三个伺服电机。正如之前解释的,伺服电机是电机,电机将电流转换为运动,驱动比 LED 或传感器等其他设备更多的电流。
如果你的 Arduino 项目需要电脑,你可以通过 USB 为其供电,只要不超过 500 mA 的限制。超过这个限制,你需要为电路的某些部分或全部使用外部电源。
让我们看看用三个伺服电机的情况。
三个伺服电机和外部电源
外部电源可以是电池或墙壁适配器电源。
我们将在这里使用基本的 AA 电池。这也是在没有电脑的情况下为 Arduino 供电的一种方式,如果你不需要电脑,想让 Arduino 独立运行。我们将在本书关于更高级概念的第三部分考虑这个选项。
现在我们先检查一下接线:

连接到 Arduino 的三个伺服电机,并由两节 AA 电池供电
在这种情况下,我们必须将地线连接在一起。当然,伺服电机只有一个电流源供应——两节 AA 电池。
让我们检查一下电路图:

三个伺服电机,两节 AA 电池,和一个 Arduino
使用固件驱动三个伺服电机
这里是驱动三个伺服电机的固件示例:
#include <Servo.h>
Servo servo01;
Servo servo02;
Servo servo03;
int angle;
void setup()
{
servo01.attach(9);
servo02.attach(10);
servo03.attach(11);
}
void loop()
{
for(angle = 0; angle < 180; angle += 1)
{
servo01.write(angle);
servo02.write(135-angle/2);
servo03.write(180-angle);
delay(15);
}
}
这个非常基础的固件也位于Chapter09/Servos/文件夹中。
我们首先实例化我们的三个伺服电机,并在setup()块中为每个电机连接一个引脚。
在loop()函数中,我们玩转角度。作为一种新的生成性创作方法,我只为角度定义了一个变量。这个变量在每个loop()循环中循环地从 0 到 180。
连接到 9 号引脚的伺服电机使用角度值本身驱动。
连接到 10 号引脚的伺服电机使用[135-(角度/2)]的值驱动,其值从 135 变化到 45。
然后,连接到 11 号引脚的伺服电机使用[180-角度]的值驱动,这是连接到 9 号引脚的伺服电机的相反运动。
这也是一个示例,展示了我们如何轻松地只控制一个变量,并在每次编程时围绕这个变量进行变化;在这里,我们使角度变化,并将角度变量组合到不同的表达式中。
当然,我们可以通过使用外部参数来控制伺服位置,例如电位器位置或测量的距离。这将结合在这里教授的概念与第五章中教授的概念,使用数字输入进行感应,以及第六章中教授的概念,通过模拟输入感知世界。
让我们更深入地了解步进电机。
控制步进电机
步进电机是步进电机的常用名称。它们是可以使用小步进行控制的电机。
完整的旋转被分成多个相等的步,电机的位置可以很容易地控制,在其中一个步骤移动并保持,具有高精度,无需任何反馈机制。
有一系列电磁线圈,可以在特定顺序中充电为正或负。控制顺序提供了对运动,向前或向后以小步的控制。
当然,我们可以使用 Arduino 板来做这件事。
我们将在这里检查单极步进电机。
将单极步进电机连接到 Arduino
单极步进电机通常由一个中心轴部分和四个电磁线圈组成。我们称其为单极,因为电源通过一个极进入。我们可以如下绘制:

一个六引脚单极步进电机
让我们看看它如何连接到我们的 Arduino。
我们需要从外部电源为步进电机供电。这里的一个最佳实践是使用电源适配器。引脚 5 和 6 必须提供电流源。
然后,我们需要使用 Arduino 控制从 1 到 4 的每个引脚。这将通过 ULN2004 吸收电流系统来完成,它与我们在上一章中用于 LED 矩阵的 ULN2003 非常相似。ULN2004 适用于 6 V 至 15 V 的电压。当 ULN2003 为 5 V 时,步进电机数据表显示我们必须使用此系统而不是 ULN2003。

通过达林顿晶体管阵列 ULN2004 连接到 Arduino 的单极步进电机
让我们检查相应的电路图:

一个电路图,显示了 Arduino、ULN2004 达林顿晶体管阵列和步进电机
我们在这里再次使用外部电源。所有地线也都连接在一起。
请注意,COM引脚(引脚编号 9)必须连接到电源源(+V)。
如果你能正确回忆起上一章的内容,当我们向 ULN200x 达林顿晶体管阵列输入时,相应的输出将电流吸收到地。
在我们这里,Arduino 连接到 ULN2004 移位寄存器的每个引脚都可以使步进电机的每个引脚通向地。
让我们为步进电机控制设计固件。
控制步进电机的固件
有一个非常棒的库可以避免我们提供想要驱动的运动的 HIGH 和 LOW 引脚的序列。
为了控制精确的运动,我们通常必须处理特定的序列。这些序列通常在数据表中描述。
让我们检查一个可用的 www.sparkfun.com/datasheets/Robotics/StepperMotor.pdf。
Sparkfun 电子公司为机器人设计的模型提供了它。
我们可以看到一个类似于以下表格的表格,命名为 驱动序列模型:
| STEP | A | B | C | D |
|---|---|---|---|---|
| 1 | HIGH | HIGH | LOW | LOW |
| 2 | LOW | HIGH | HIGH | LOW |
| 3 | LOW | LOW | HIGH | HIGH |
| 4 | HIGH | LOW | LOW | HIGH |
如果你想进行顺时针旋转,你应该生成从 1 到 4 的序列,然后是 1,并循环。逆时针旋转需要生成从 4 到 1 的序列等。
为了避免编写大量这样的序列,我们可以使用一个函数,直接使用名为 Stepper 的库,该库现在包含在 Arduino 核心中。
这里是代码,后面是讨论。它也位于 Chapter09/StepperMotor/ 文件夹中。
#include <Stepper.h>
#define STEPS 200
// create an instance of stepper class
Stepper stepper(STEPS, 8, 9, 10, 11);
int counter = 0; // store steps number since last change of direction
int multiplier = 1; // a basic multiplier
void setup()
{
stepper.setSpeed(30); // set the speed at 30 RPM
}
void loop()
{
// move randomly from at least 1 step
stepper.step(multiplier);
// counting how many steps already moved
// then if we reach a whole turn, reset counter and go backward
if (counter < STEPS) counter++ ;
else {
counter = 0;
multiplier *= -1;
}
}
我们首先包含 Stepper 库。
然后我们定义相当于一整圈的步数。在我们的数据表中,我们可以看到第一步的角度是 1.8 度,有 5% 的误差范围。我们不会考虑这个误差;我们将采用 1.8 度。这意味着我们需要 200 步(200 * 1.8 = 360°)才能完成一整圈。
然后,我们通过推送五个参数实例化一个 Stepper 对象,这些参数是一个整圈的步数,以及连接到步进电机的 Arduino 的四个引脚。
然后,我们声明两个辅助变量用于跟踪和有时改变旋转方向。
在 setup() 块中,我们通常定义当前实例处理步进电机的速度。这里,我设置为 30(代表每分钟 30 圈)。这也可以在 loop() 块中根据特定条件或任何其他情况更改。
最后,在 loop() 块中,我们将步进电机移动到乘数值相等的量,这个值最初是 1。这意味着在每次 loop() 方法的运行中,步进电机从顺时针方向的步 1(即,1.8 度)开始旋转。
我添加了一个逻辑测试,每次检查计数器是否完成了完成一整圈所需的步数。如果没有,我增加它;否则,一旦它达到限制(即,电机从程序执行开始以来完成了一整圈),我重置计数器并反转乘数,以便步进电机继续行走,但方向相反。
这是你应该记住的另一个模式。这些都是小模式,将为你提供很多便宜且高效的想法,可以在你未来的每个项目中使用。
通过伺服电机和步进电机,我们现在可以使物体移动。
在我的某些项目中,我使用了两个步进电机,每个电机连接一根线,这两根线都连接到一个悬挂的铅笔上。我们可以通过控制每边的线悬挂量在墙上作画。
空气运动和声音
使空气移动可以产生美妙的声音,我们将在接下来的几节中了解更多关于这方面的内容。
如果你能够用 Arduino 控制物体移动,你很可能也能让空气移动。
实际上,我们已经这样做了,但我们可能没有移动得足够多以产生声音。
这部分只是对一些定义的简要介绍,而不是关于声音合成的完整课程。这些是我们将在本书的下一部分使用的基本元素,尽可能提供网站或书籍的参考,如果你对这部分内容感兴趣,可以进一步学习。
声音实际上是什么?
声音可以被定义为一种机械波。这种波是压力的振荡,可以通过固体、液体或气体传播。通过扩展,我们可以将声音定义为这些振荡在我们耳朵中可听到的结果。
我们的耳朵,结合进一步的脑部处理,是一个惊人的空气压力传感器。它能够评估以下内容:
-
声音的幅度(与空气移动量相关)
-
声音的频率(与空气振荡量相关)
当然,所有这些过程都是实时的,假设更高或更低的频率在此时混合。
我真的建议你阅读由 Max 6 框架的制作者 cycling 74 提供的令人惊叹且高效的介绍数字音频是如何工作的?,你可以在线阅读www.cycling74.com/docs/max6/dynamic/c74_docs.html#mspdigitalaudio。
一个声音可以包含多个频率,通常是由频率内容以及每个频率振幅的总体感知组合,给我们带来我们称之为声音音色的感觉。心理声学研究声音的感知。
如何描述声音
我们可以用许多方式描述声音。
通常,声音有两种表示:
-
随时间变化的幅度。这种描述可以放在图表上,并定义为声音的时间域表示。
-
幅度随频率内容的变化。这被称为声音的频域表示。
存在一种数学运算,提供了一种从一种表示转换到另一种表示的简单方法,称为傅里叶变换(en.wikipedia.org/wiki/Fast_Fourier_transform)。计算机上有许多这种运算的实现,形式为快速傅里叶变换(FFT),这是一种高效的方法,可以提供快速的近似计算。
让我们考虑空气压力的正弦变化。这是最简单的声波之一。
这里是两个域中的两种表示:

由空气压力的正弦变化产生的相同基本声音的两种表示。
让我们描述前一张图像的两个图表。
在时域表示中,我们可以看到一个具有周期的周期性变化。周期是空间波长的等效时间。
周期是完成一个完整的振动周期所需的时间。基本上,如果你能描述一个周期内的变化,你就能完全绘制出声音在时间上的表示。在这里,这一点很明显,因为我们正在观察一个基于纯正弦的声音。
如果你绘制并观察由一个源产生的声音,时间上的振幅变化将直接对应于空气压力的变化。
考虑到轴的方向,我们首先有我们称之为高压前沿的部分。这是曲线在零点以上的部分(由时间轴表示)。这意味着压力高,我们的鼓膜在我们的耳朵内部被推得更多一些。
然后,在半周期之后,曲线穿过零点并下降,这意味着空气压力低于正常大气压力。我们的鼓膜也感受到了这种变化。它被稍微拉扯了一下。
在频域表示中,只有一条垂直线。前图中这种脉冲状的图形代表了基于正弦波的声音中包含的唯一频率。它通过一个数学方程与周期直接相关,如下所示:

这里,T 是秒内的周期,f 是赫兹内的频率。
频率越高,声音听起来越尖锐。频率越低,声音听起来越低沉。
当然,高频意味着短周期和随时间更快地振荡。
这些是理解声音如何被表示和感知的基本步骤。
麦克风和扬声器
麦克风是敏感于空气压力微妙变化的设备。是的,它们是传感器。它们可以将空气压力的变化转换为电压的变化。
扬声器是实施可以移动的部分的设备,推动和拉动空气的质量,使其振动并产生声音。这种运动是由电压变化引起的。
在这两种情况下,我们有:
-
膜
-
电气传感器系统
在麦克风的情况下,我们改变空气压力,这会产生一个电信号。
在扬声器的情况下,我们改变电信号,这会产生空气压力的变化。
在每种情况下,我们都有模拟信号。
数字和模拟域
声音源可以非常不同。如果你敲击桌子,你会听到声音。这是一种基于模拟和物理的声音。在这里,你使桌子稍微振动一下,推动和拉动周围的空气;因为你靠近它,你的鼓膜会感受到这些细微的变化。
一提到数字设备,我们就必须考虑到存储和内存的限制。即使现在这些设备很大且足够,它们也不是无限的。
那在这种情况下我们如何描述模拟的东西呢?当我们描述 Arduino 的模拟和数字输入输出引脚时,我们已经讨论了这种情况。
如何数字化声音
想象一个可以定期采样麦克风电压变化的系统。通常使用的采样概念是采样保持。
系统能够在固定的时间间隔内读取模拟值。它取一个值,将其保持为常数,直到下一个值,依此类推。
我们在谈论采样率来定义采样频率。如果采样率低,我们将对模拟信号的近似将低于如果采样率高的话。
一个数学定理为我们提供了一个我们必须记住的限制——奈奎斯特频率。
为了确保我们的采样系统处理由系统本身引起的最小安全文物,我们必须以至少是我们原始模拟信号中最高频率的两倍进行采样。

采样正弦波时的采样率示例
更高的采样率不仅意味着对原始模拟波的更高精度和保真度,而且意味着在数字系统中存储更多的点。结果将是文件更重,从磁盘和文件系统来看。
在采样时,还需要注意的一个要素是位深度。
我在之前的图中自愿省略了它,以免使绘图过载。
事实上,我们在一段时间内采样了一个值,但如何表示这个值本身,我的意思是振幅呢?我们通常使用基于位的编码系统,使用数字设备。
位深度是幅度值的分辨率,从-1(可能的最小值)到1(可能的最大值)。
位深度越高,我们能够编码和记录到数字系统中的细微变化就越多。相反,如果我们有一个位深度非常低的采样器,并且我们进行逐渐减少的幅度变化,声音将显著减小,类似于多普勒效应。例如,我们无法区分0.5到0.6的值;一切都将只有0.5或0.7,但永远不会是0.6。声音将失去细微之处。
常用的采样率和位深度取决于最终渲染的目的。
这里有两个常用的质量标准:
-
CD 质量为 44.1 kHz 和 16 位
-
DAT 质量为 48 kHz 和 16 位
一些录音和母带工作室使用 96 kHz 和 24 位的音频接口和内部处理。一些热爱复古音效引擎的人仍然使用低保真系统,以 16 kHz 和 8 位产生自己的声音和音乐。
从模拟到数字转换的过程由模拟到数字转换器(ADC)处理。其质量是实现良好转换的关键。这个过程与我们在 Arduino 中使用模拟输入时涉及的过程类似。它的 ADC 是 10 位,它可以每 111 微秒读取一个值,这相当于 9 kHz 的采样率频率。
缓冲区用于平滑处理时间,使事物在时间上更加平滑。
如何将数字比特作为声音播放
我们还可以将数字编码的声音转换为模拟声音。这个过程是通过数字到模拟转换器(DAC)实现的。
如果处理器将编码声音的比特数据以连续的离散值流的形式发送到 DAC,那么 DAC 就会接收所有这些值并将它们转换为模拟电信号。它会在每个数字值之间插值,这通常涉及一些过程(例如,低通滤波),以消除一些如奈奎斯特频率以上的谐波等伪影。
在数字音频的世界里,DAC 的功率和质量是我们音频工作站最重要的方面之一。它们必须提供高分辨率、高采样率、小的总谐波失真和噪声,以及极大的动态范围。
Arduino 如何帮助产生声音
让我们回到 Arduino。
Arduino 可以读取和写入数字信号。它还可以读取模拟信号,并通过 PWM 模拟模拟输出信号。
它难道不能产生甚至听到声音吗?当然可以。
我们甚至可以使用一些专用组件来改善事情。例如,我们可以使用采样率更高的 ADC 来存储声音,如果需要的话,还可以使用高质量的 DAC。今天,我们经常使用电子硬件设备来控制软件。例如,我们可以基于 Arduino 构建一个设备,里面充满了旋钮和按钮,并将其与计算机上的软件接口。这一点必须在此提及。
我们还可以将 Arduino 用作声音触发器。实际上,将其转变为一个小型序列器相当简单,可以向外部合成器等设备发送特定的 MIDI 或 OSC 消息。让我们进一步深入,具体探讨 Arduino 板的音频概念。
播放基本声音片段
播放声音需要一个声音源和扬声器。当然,还需要一个能够听到声音的听众。
Arduino 本地能够在小型电脑扬声器上产生 8 kHz 和 8 位音频回放声音。
我们将使用 Arduino 内置的tone()函数。正如arduino.cc/en/Reference/Tone中所述,在使用此函数时,我们必须注意所使用的引脚,因为它将干扰引脚 3 和 11 上的 PWM 输出(Arduino MEGA 除外)。
这种技术也被称为位打点。它基于特定频率的 I/O 引脚切换。
连接最经济的音响电路
我们将设计一个使用小型 8 欧姆扬声器、电阻和 Arduino 板的史上最经济的声音发生器。

一个小型的声音发生器
这里所做的连接确保了声音的可听性。现在让我们编程芯片。
对应的电路图如下:

声音发生器的示意图
播放随机音调
作为一位数字艺术家,尤其是电子音乐家,我喜欢摆脱音符的束缚。我经常使用频率而不是音符;如果你感兴趣,可以阅读有关微音概念的资料en.wikipedia.org/wiki/Microtonal_music。
在这个例子中,我们不使用音符,而是使用频率来定义和触发我们的电子音乐。
代码也位于Chapter09/ ToneGenerator/文件夹中。
void setup() {
// initiate the pseudo-random number generator
randomSeed(analogRead(0));
}
void loop() {
// generate random pitch & duration
int pitch = random(30,5000);
int duration = 1000 / (random(1000) + 1);
// play a tone to the digital pin PWM number 8
tone(8, pitch, duration);
// make a pause
delay(duration * 1.30);
// stop the tone playing
noTone(8);
}
我们首先通过读取模拟输入0来初始化伪随机数生成器。
在循环中,我们生成两个数字:
-
音高是一个从 30 到 4,999 的数字;这是声音的频率
-
持续时间是 1 毫秒到 1 秒之间的数字;这是声音的持续时间
这两个参数是tone()函数所必需的。
然后,我们调用tone()。第一个参数是你给扬声器供电的引脚。
tone()函数在引脚上生成指定频率的方波,如其在arduino.cc/en/Reference/Tone的参考页面中所述。
如果我们不提供持续时间,声音将继续播放,直到调用noTone()函数。后者接受一个与引脚相同的参数。
现在,请聆听并享受从你的 8 位芯片传来的微音伪随机旋律。
使用 Mozzi 改进声音引擎
位打点技术非常经济,学习它是件好事。然而,我这里可以引用一些令人烦恼的事情:
-
没有纯音:方波是基频下所有奇次谐波的总和
-
没有幅度控制可用:每个音符都以相同的音量播放
我们将使用一个名为 Mozzi 的非常棒的库,由 Tim Barrass 编写。官方网站直接托管在 GitHub 上sensorium.github.com/Mozzi/。它包括TimerOne库,一个非常快速的定时器处理器。
Mozzi 提供了一个非常棒的 16,384 kHz,8 位音频输出。它还包含一个很好的基本音频工具包,包括振荡器、样本、线条和包络,以及滤波器。
所有的内容都可以在没有外部硬件的情况下,仅使用 Arduino 的两个引脚获得。
我们将基于它设计一个小型声音引擎。
设置电路和 Mozzi 库
设置电路很简单;它与最新的电路相同,只是需要使用引脚 9。
Mozzi 的文档说明如下:
要听 Mozzi,将 3.5 毫米音频插头的中线连接到 Arduino 数字引脚 9*上的 PWM 输出,将黑色地线连接到 Arduino 的地线。将其用作线路输出,您可以将其插入电脑并使用 Audacity 等声音程序进行收听。
硬件设置非常简单。您可以在互联网上找到许多类似的 3.5 毫米音频插头连接器。在下面的电路图中,我使用了一个扬声器而不是插头连接器,但使用插头连接器时效果完全相同,后者有 2 个引脚,一个地线和与信号相关的引脚。地线必须连接到 Arduino 的地线,另一个引脚连接到 Arduino 的数字引脚 9。
然后我们必须安装库本身。
从他们的网站下载它:
sensorium.github.com/Mozzi解压它并将文件夹重命名为 Mozzi。
然后将它放在通常放置库的位置;在我的情况下是:
/Users/julien/Documents/Arduino/libraries/
重新启动或仅启动 Arduino IDE,您将能够在 IDE 中看到库。
它提供了一系列示例。
我们将使用正弦波相关的示例。
这就是 Mozzi 库的外观:

展示大量示例的 Mozzi 安装
一个正弦波示例
就像任何库一样,我们必须学习如何使用正弦波。
有很多示例,这些示例有助于我们逐步学习如何设计自己的固件。显然,我不会描述所有这些示例,但只会描述那些我将从中提取元素以制作您自己的声音发生器的示例。
让我们检查一下正弦波示例。它也位于Chapter09/MozziSoundGenerator/文件夹中。
#include <MozziGuts.h>
#include <Oscil.h> // oscillator template
#include <tables/sin2048_int8.h> // sine table for oscillator
// use: Oscil <table_size, update_rate> oscilName (wavetable)
Oscil <SIN2048_NUM_CELLS, AUDIO_RATE> aSin(SIN2048_DATA);
// use #define for CONTROL_RATE, not a constant
#define CONTROL_RATE 64 // powers of 2 please
void setup(){
startMozzi(CONTROL_RATE); // set a control rate of 64 (powers of 2 please)
aSin.setFreq(440u); // set the frequency with an unsigned int or a float
}
void updateControl(){
// put changing controls in here
}
int updateAudio(){
return aSin.next(); // return an int signal centered around 0
}
void loop(){
audioHook(); // required here
}
首先,进行一些包含操作。
MozziGuts.h 是在任何情况下都应该包含的基本头文件。
Oscil.h 是需要使用振荡器时应该使用的头文件。
然后我们包含一个波表(正弦波)。
振荡器
在声音合成领域,振荡器是一个能够产生振荡的基本单元。它不仅常用于直接生成频率从 20 Hz 到 20 kHz(可听频谱)的声音,而且还作为调制器(通常频率低于 50 Hz)。在本例中,它被用作后者。振荡器通常被称为低频振荡器(LFO)。
波表
波表是一种非常不错且高效的存储整个声音片段的方法,通常是循环或循环的声音。
我们基本上将其用作查找表。你还记得使用它吗?
我们不是在实时中计算正弦值,而是基本上预先计算整个周期的每个值,然后将结果添加到表中;每次需要时,我们只需从表头扫描到表尾以检索每个值。
当然,这确实是一个近似值。但它节省了很多 CPU 工作。
波表由其大小、相关的采样率和当然整个值定义。
让我们检查sin2048_int8.h文件中我们可以找到什么:

我们确实可以找到单元格的数量:2048(即表中包含 2048 个值)。然后,采样率被定义为 2048。
让我们回到例子。
然后我们定义 Oscil 对象,它创建一个振荡器。
在与变量更新频率相关的第二个define关键字之后,我们有setup()和loop()的常规结构。
我们还有updateControl()和updateAudio(),这些在代码中未定义。实际上,它们与 Mozzi 相关,并在库文件中定义。
setup()块在之前定义的特定控制率下启动 Mozzi 库。然后,我们将之前定义的振荡器设置为 440 Hz 的频率。440 Hz 是通用 A 音符的频率。在这个上下文中,它可以被认为是音频的 Hello World 示例。
关于updateControl()这里没有更多内容。
在updateAudio()中返回aSin.next()。它读取并返回下一个样本,这被理解为下一个元素,也就是下一个声音片段。
在loop()中,我们调用audioHook()函数。
全局模式是常规的。即使你使用与声音相关的另一个库,无论是 Arduino 世界内部还是外部,你也必须以四个步骤(通常如此,但可能有所不同)来处理这种模式:
-
在标题中定义的带有一些包含的定义
-
音频引擎的开始
-
钩子的永久循环
-
在提交前更新渲染事物的函数,然后在钩子中
如果你上传这个,你会听到一个很棒的 A440 音符,这可能会让你哼唱起来。
正弦波的频率调制
现在我们合并一些概念——正弦波生成、调制和输入读取。
我们将使用两个振荡器,一个调制另一个的频率。
使用电位器,我们可以控制调制振荡器的频率。
让我们先通过添加电位器来改进电路。
添加一个电位器
在以下电路图中,我们在声音发生器电路中添加了一个电位器:

电路图如下:

改进声音发生器
升级固件以处理输入
此代码也位于Chapter09/MozziFMOnePot/文件夹中。
#include <MozziGuts.h>
#include <Oscil.h>
#include <tables/cos8192_int8.h> // table for Oscils to play
#include <utils.h> // for mtof
#define CONTROL_RATE 64 // powers of 2 please
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aCos(COS8192_DATA);
Oscil<COS8192_NUM_CELLS, AUDIO_RATE> aVibrato(COS8192_DATA);
const long intensityMax = 500;
int potPin = A0;
int potValue = 0;
void setup(){
startMozzi(CONTROL_RATE);
aCos.setFreq(mtof(random(21,80)));
aVibrato.setFreq((float) map(potValue, 0, 1024, 0, intensityMax));
}
void loop(){
audioHook();
}
void updateControl(){
potValue = analogRead(potPin);
}
int updateAudio(){
long vibrato = map(potValue, 0, 1024, 0, intensityMax) * aVibrato.next();
return (int)aCos.phMod(vibrato);
}
在这个例子中,我们使用两个振荡器,它们都基于余弦波表:
-
aCos代表声音本身 -
aVibrato是调制器
由于我们这里有一个电位器,我们需要稍微调整一下。
intensityMax是调制效果的强度最大值。我在测试后选择了 500。
我们经常使用以下技术来缩放事物:使用一个常数(甚至是一个“真实”变量),然后乘以你可以改变的价值。这可以通过使用map()函数在一遍中完成。我们已经在第六章,感知世界 – 使用模拟输入感受中为了同样的目的使用过它——缩放模拟输入值。
在那种情况下,在最大值时,你的电位器(更普遍地说,你的输入)将你想要改变的参数改变到最大值。
让我们继续审查代码。
我们定义了电位器引脚 n 和变量potPin。我们还定义了potValue为0。
在setup()块中,我们启动 Mozzi。我们将振荡器的频率定义为aCos。频率本身是mtof()函数的结果。mtof代表MIDI to Frequency。
正如我们稍后将要描述的,MIDI 协议编码了许多字节的值,包括它用于从序列器传输到乐器所使用的音符音高。每个 MIDI 音符与真实世界中的实际音符值相对应,每个音符对应一个特定的频率。有一些表格显示了每个 MIDI 音符的频率,Mozzi 为我们提供了这些信息。
我们可以将 MIDI 音符的音高作为mtof()函数的参数传递,它将返回正确的频率。在这里,我们使用random(21,80)函数生成一个从 21 到 79 的 MIDI 音符音高,这意味着从 A0 到 A5。
当然,这个用例是开始介绍 MIDI 的一个前奏。我们本可以直接使用random()函数来生成频率。
然后,我们读取模拟输入 A0 的当前值,并使用它来计算调制振荡器频率的缩放值,即aVibrato。这只是为了提供更多的随机性和奇特感。实际上,如果你每次重启 Arduino 时电位器不在相同的位置,你将会有不同的调制频率。
然后,loop()块会持续执行audioHook()方法以产生音频。
而这里聪明的地方在于updateControl()方法。我们添加了analogRead()函数来读取模拟输入的值。考虑到这个函数的目的,这样做更好。实际上,Mozzi 框架将音频渲染时间关键任务与控制(特别是人类控制)代码部分分开。
在许多框架中,你经常会遇到这种情况,第一次可能会让你感到困惑。这全部关乎任务及其调度。在这里不逆向工程 Mozzi 的概念,我想说的是,时间关键事件必须比人类行为更加小心地处理。
事实上,即使我们看起来可以非常快地转动旋钮,与 Mozzi 的采样率(16,384 kHz)相比,这实际上是非常慢的。这意味着我们不能只为测试和检查而停止整个过程,如果不断改变这个电位计的值。事情是分开的;请记住这一点并小心使用框架。
在这里,我们在updateControl()中读取值并将其存储在potValue变量中。
然后,在updateAudio()中,我们计算颤音值,它是potValue的值从0到intensityMax的值缩放,乘以振荡器在其波形表中的下一个值。
这个值随后被用于一个名为phMod的新方法。此方法对其调用的振荡器应用相位调制。这种调制是产生频率调制效果的好方法。
现在,上传固件,添加耳机,转动电位计。你应该能够听到效果并用电位计控制它。
使用包络和 MIDI 控制声音
我们现在可以设计使用 Mozzi 的小部分声音引擎。还有其他库,我们学到的知识将用于这两个库。确实,这些是模式。
让我们来检查如何使用来自计算机或其他设备的标准协议来控制基于 Arduino 的声音引擎。确实,能够使用计算机触发音符以改变声音参数将是非常有趣的,例如。
这两个都是用于音乐和新媒体相关项目和作品中的协议。
MIDI 概述
MIDI代表Musical Instrument Digital Interface。这是一个规范标准,它使数字音乐乐器、计算机和所有必需的设备能够相互连接和通信。它在 1983 年推出,在撰写本文时刚刚庆祝了它的 30 周年。参考网站是www.midi.org。
MIDI 可以通过基本串行链路传输以下数据:
-
音符(开启/关闭、后触)
-
参数更改(控制更改、程序更改)
-
实时消息(时钟、传输状态如开始/停止/继续)
-
系统专用,允许制造商创建他们的消息
一个新的协议出现了,并且现在被广泛使用:OSC。顺便说一下,它不是一个真正的协议。
OSC代表Open Sound Control,是由加州伯克利大学新音乐与音频技术中心(CNMAT)的两个人开发的内容格式。它最初是为了在音乐表演期间共享手势、参数和音符序列而设计的。它现在非常广泛地用作 MIDI 的替代品,提供更高的分辨率和更快的传输。其主要特点是本地的网络传输能力。OSC 可以在 IP 环境中通过 UDP 或 TCP 传输,这使得它很容易在 Wi-Fi 网络上甚至通过互联网使用。
MIDI 和 OSC 库用于 Arduino
我建议在这里使用两个库。我自己测试过它们,它们是稳定且高效的。您可以在sourceforge.net/projects/arduinomidilib上查看关于 MIDI 的库。您可以在github.com/recotana/ArdOSC上查看关于 OSC 的库。现在安装它们应该不会太难。让我们至少安装 MIDI,并重新启动 IDE。
生成包络
在音频领域,包络是用来修改某物形状的一种。例如,想象一个振幅包络塑造波形。
您首先有一个波形。我在 Ableton Live 的 Operator 合成器中生成这个正弦波(www.ableton.com),这是著名的数字音频工作站。以下是截图:

由 Ableton Live 的 Operator FM 合成器中的运算符生成的基本正弦波
由于混叠,正弦波显示得不是很好;这里还有另一个截图,这是相同的波形但放大了:

一个正弦波
这个正弦波具有全局恒定的振幅。当然,空气压力的推拉是不断变化的,但全局的最大值和最小值随时间保持恒定。
音乐家总是希望他们的声音随时间演变,无论是微妙还是强烈。
让我们将一个包络应用到这个相同的波形上,使其全局音量逐渐增加,然后稍微减少,然后迅速减少到零:

由具有长攻击时间的包络改变的正弦波
这里是使用另一个包络的结果:

由具有非常短攻击时间的包络改变的正弦波
基本上,包络是一系列时间点。在每一个时刻,我们将原始信号的值乘以包络的值。
这产生了随时间变化的声音演变。
我们可以在许多情况下使用包络,因为它们可以调制振幅,正如我们刚刚学到的。我们还可以使用它们来改变声音的音高(即频率)。
通常,包络是在声音触发(即应用于声音)的同时触发的,但当然我们可以使用偏移重新触发功能在同一个触发声音期间重新触发包络,并做更多的事情。
这里有一个最后的例子,展示了音高包络。包络使声音的频率降低。如您所见,左边的波比右边的波更紧密。声音从高音变为低音。

调制声音音高的包络
实现包络和 MIDI
我们将要设计一个非常便宜的声音合成器,当它接收到 MIDI 音符消息时能够触发音符,并在接收到特定的 MIDI 控制更改消息时改变声音。
MIDI 部分将由库处理,包络将被详细说明并编码。
你可以检查以下代码。此代码也位于 Chapter09/MozziMIDI/ 文件夹中。
#include <MIDI.h>
#include <MozziGuts.h>
#include <Oscil.h> // oscillator template
#include <Line.h> // for envelope
#include <utils.h> // for mtof
#include <tables/sin2048_int8.h> // sine table for oscillator
// use #define for CONTROL_RATE, not a constant
#define CONTROL_RATE 128 // powers of 2 please
// declare an oscillator using a sine tone wavetable
// use: Oscil <table_size, update_rate> oscilName (wavetable)
Oscil <SIN2048_NUM_CELLS, AUDIO_RATE> aSin(SIN2048_DATA);
// for envelope
Line <unsigned int> aGain;
unsigned int release_control_steps = CONTROL_RATE; // 1 second of control
unsigned int release_audio_steps = 16384; // 1 second of audio
int fade_counter;
float vol= 1\. ; // store the master output volume
unsigned int freq; // to convey control info from MIDI handler to updateControl()
void HandleControlChange(byte channel, byte CCnumber, byte value) {
switch(CCnumber){
case 100:
vol = map(value,0, 127, 0., 1.);
break;
}
}
void HandleNoteOn(byte channel, byte pitch, byte velocity) {
// scale velocity for high resolution linear fade on Note-off later
freq = mtof(pitch);
aGain.set(velocity<<8); // might need a fade-in to avoid clicks
}
void HandleNoteOff(byte channel, byte pitch, byte velocity) {
// scale velocity for high resolution linear fade on Note-off later
aGain.set(0,release_audio_steps);
fade_counter = release_control_steps;
}
void setup() {
// Initiate MIDI communications, listen to all channels
MIDI.begin(MIDI_CHANNEL_OMNI);
// Connect the HandleControlChange function to the library, so it is called upon reception of a NoteOn.
MIDI.setHandleControlChange(HandleControlChange); // Put only the name of the function
// Connect the HandleNoteOn function to the library, so it is called upon reception of a NoteOn.
MIDI.setHandleNoteOn(HandleNoteOn); // Put only the name of the function
// Connect the HandleNoteOn function to the library, so it is called upon reception of a NoteOn.
MIDI.setHandleNoteOff(HandleNoteOff); // Put only the name of the function
aSin.setFreq(440u); // default frequency
startMozzi(CONTROL_RATE);
}
void updateControl(){
// Ideally, call MIDI.read the fastest you can for real-time performance.
// In practice, there is a balance required between real-time
// audio generation and a responsive midi control rate.
MIDI.read();
if (fade_counter-- <=0) aGain.set(0,0,2); // a line along 0
}
int updateAudio(){
// aGain is scaled down to usable range
return (int) ((aGain.next()>>8) * aSin.next() * vol )>>8; // >>8 shifts the multiplied result back to usable output range
}
void loop() {
audioHook(); // required here
}
首先,我们包含 MIDI 库。然后我们包含 Mozzi 库。
当然,每个项目中需要包含的 Mozzi 的正确位略有不同。研究示例有助于理解它们的位置。在这里,我们不仅需要 Oscil 来提供振荡器的基本功能,还需要 Line。Line 与 Mozzi 中的插值函数相关。生成包络处理这个问题。基本上,我们选择两个值和一个时间持续时间,然后从第一个值开始,在所选的时间内达到第二个值。
我们还包含了与正弦波相关的 wavetable。
我们定义一个比之前更高的控制速率,为 128。这意味着 updateControl() 函数每秒被调用 128 次。
然后我们将振荡器定义为 aSin。
在这些位之后,我们通过声明 Line 对象的实例来定义一个包络。
我们定义了两个变量来存储包络持续时间的释放部分,一个用于一秒内的控制部分(即步骤数将是 CONTROL_RATE 的值),另一个用于一秒内的音频部分(即 16,384 步)。最后,定义了一个名为 fade_counter 的变量。
HandleControlChange() 是一个在向 Arduino 发送 MIDI 控制更改消息时被调用的函数。消息包含以下字节:
-
MIDI 通道
-
CC 编号
-
值
这些参数传递给 HandleControlChange() 函数,你可以在你的代码中直接访问它们。
这是一种非常常见的使用事件处理程序的方式。几乎所有的事件监听器框架都是这样构建的。你有一些函数,你可以使用它们,并在其中放入你想要的任何内容。框架本身处理必须调用的函数,以尽可能节省 CPU 时间。
在这里,我们在 CCNumber 变量上添加一个只有一个情况的 switch 语句。
这意味着如果你发送一个 MIDI 控制更改 100 消息,这个情况匹配,CC 的值将被处理,vol 变量将被更改和修改。这个控制更改将控制合成器的主输出音量。
同样地,HandleNoteOn() 和 HandleNoteOff() 处理 MIDI 音符消息。
基本上,当你按下 MIDI 键盘上的键时,会发送一个 MIDI 音符开启消息。当你释放那个键时,就会弹出一个 MIDI 音符关闭消息。
在这里,我们有两个函数处理这些消息。
HandleNoteOn() 解析消息,获取速度部分,将其左移 8 位,并通过 set() 方法传递给 aGain。当接收到 MIDI 音符开启消息时,包络 aGain 被触发到最大值。当接收到 MIDI 音符关闭消息时,包络被触发在之前讨论的音频步骤数内达到 0,耗时一秒。当键释放时,fade 计数器也会重置到最大值。
这样,我们就有一个响应 MIDI Note On 和 MIDI Note Off 消息的系统。当我们按下键时,会产生声音,直到我们释放键。当我们释放它时,声音会线性衰减到 0,耗时一秒。
setup()方法包括 MIDI 库的设置:
-
MIDI.begin()实例化通信 -
MIDI.setHandleControlChange()允许您定义当控制变化消息到来时调用的函数名称 -
MIDI.setHandleNoteOn()允许您定义当 Note On 消息到来时调用的函数名称 -
MIDI.setHandleNoteOff()允许您定义当 Note Off 消息到来时调用的函数名称
它还包括 Mozzi 的设置。
loop()函数现在相当熟悉了。
updateControl()函数不包含声音生成器的关键部分。这并不意味着这个函数很少被调用;它调用次数少于updateAudio()——每秒控制 128 次,音频每秒 16,384 次,正如我们之前看到的。
这是阅读我们的 MIDI 流程的完美地方,使用MIDI.read()函数。
这是我们可以在fade计数器达到 0 时立即触发我们的衰减包络到 0 的地方,而不是在此之前,这样声音在一秒内就会像我们之前检查的那样。
最后,updateAudio()函数返回振荡器乘以包络值的值。这就是包络的目的。然后,vol乘以第一个结果,以便添加一个键来控制主输出音量。
这里的<<8和>>8表达式用于在 Note Off 时设置高分辨率线性淡入,这是 Tim Barrass 自己提供的一个好技巧。
将 MIDI 连接器连接到 Arduino
这个原理图基于MIDI 电气规范图。

基于 Arduino 的具有 MIDI 功能的音效生成器
对应的电路图如下:

连接到基于 Arduino 的音效生成器的 MIDI 连接器
如您所见,数字引脚 0(串行输入)被涉及。这意味着我们无法使用 USB 上的串行通信。实际上,我们想使用我们的 MIDI 接口。
让我们上传代码,并在 Max 6 中启动这个小序列发生器。

芯片上的廉价序列发生器可以触发 MIDI 音符和 MIDI 控制变化
序列发生器相当直观。在左上角切换开切换按钮,它就会启动序列发生器,读取 multislider 对象中的每个步骤。滑块越高,这个音符进入该步骤的音高就越高。
您可以点击左侧 multislider 下的按钮,它将生成一个包含 16 个元素的随机序列。
从右上角列表菜单中选择正确的 MIDI 输出总线。
使用 MIDI 线将您的 Arduino 电路和 MIDI 接口连接起来,并聆听音乐。更改 multislider 内容以及播放的序列。如果您转动旋钮,音量将改变。
这里的一切都是通过 MIDI 传输的。计算机是序列器和远程控制器,Arduino 是合成器。
使用 PCM 库播放音频文件
另一种播放声音的方法是读取已经数字化的声音。
音频样本定义了数字内容,通常存储在可以读取和转换为可听声音的文件系统上。
从内存大小来看,样本可能非常庞大。
我们将使用由麻省理工学院的大卫·A·梅利斯(David A. Mellis)设置的 PCM 库。像其他合作者一样,他很高兴成为本书的一部分。
参考页面是hlt.media.mit.edu/?p=1963。
下载库并安装它。
假设我们在 Arduino 内存空间中有足够的空间。如果我们想在磁盘上以 C 兼容的结构转换样本,我们应该如何进行安装?
PCM 库
检查以下代码。它也位于Chapter09/PCMreader/文件夹中。

我们的 PCM 读取器
声明了一个unsigned char数据类型的数组,被命名为const,特别是带有PROGMEM关键字的sample。
PROGMEM强制将此常量放入程序空间而不是 RAM 中,因为后者要小得多。基本上,这就是样本。startPlayback()函数能够从数组中播放样本。sizeof()方法计算数组内存的大小。
WAV2C – 转换您的样本
由于我们已经玩过 wavetable,并且这是我们接下来要做的,我们可以直接在 Arduino 代码中存储我们的样本波形。
即使从 SD 卡动态读取音频文件看起来更智能,PCM 提供了一种更简单的方法来处理——直接读取数组的模拟转换,并将波形存储到声音中。
我们首先必须将样本转换为 C 数据。
大卫·埃利斯(David Ellis)开发了一个开源的小型基于 Processing 的程序,提供了一种实现此功能的方法;它可以在github.com/damellis/EncodeAudio找到。
您可以直接从针对您的操作系统编译的参考项目页面下载它。
启动它,选择一个 WAV 文件(基于 PCM 编码的样本),然后它将在您的剪贴板中复制大量内容。
然后,您只需将此内容复制粘贴到之前定义的数组中。
注意正确地将它粘贴在花括号之间。
这里是从我制作的wav样本转换后复制到剪贴板的内容:

要粘贴到 C 数组中的大量数据
在同一文件夹中,我放置了一个我设计的.wav文件。它是一个 16 位记录的简短节奏。
布线电路
电路与“播放基本声音片段”部分中的电路类似,但在这里我们必须使用数字引脚 11。而且我们不能在引脚 3、9 和 10 上使用 PWM,因为库中涉及的定时器消耗了这些引脚。

连接我们的 PCM 读取器
电路图也很简单。

不要忘记使用 PCM 库中的引脚 11
现在,让我们播放音乐。
其他读取库
还有其他库提供了读取和解码 MP3 格式或其他格式的途径。
你可以在互联网上找到很多;但请注意,其中一些需要一些保护罩,比如 Sparkfun 网站上的www.sparkfun.com/products/10628。
这提供了一个带有 SD 卡读取器、3.5 毫米立体声耳机插孔、VS1053 移位寄存器和非常通用的解码器芯片(用于 MP3、WMA、AAC 和其他格式)的保护罩。
这是一个非常专业的解决方案,我们只需要将保护罩与 Arduino 连接即可。
Arduino 只从保护罩发送和接收位,保护罩负责解码编码文件、转换为模拟信号等。
我真的建议你测试一下。Sparkfun 网站上有很多示例。
摘要
我们在这里学习了如何使用 Arduino 让物体移动。特别是,我们学习了以下内容:
-
使用电机移动固体物体
-
使用声音发生器移动空气
当然,遗憾的是,我无法更多地描述如何让物体移动。
如果你需要关于声音的帮助,请通过电子邮件 book@cprogrammingforarduino.com 联系我。我很乐意帮助你处理声音输入,例如。
这是本书第二部分的结束。我们一起发现了许多概念。现在我们将深入研究一些更高级的主题。
我们能够理解固件设计和输入输出,所以让我们继续前进。
我们将更深入地探讨使用 I2C/SPI 通信的精确示例,以使用 GPS 模块、7 段 LED 系统等。我们还将深入研究 Max 6,特别是如何使用 Arduino 控制计算机上的某些 OpenGL 视觉。我们将发现网络协议,以及如何使用 Wi-Fi 在没有网络线的情况下使用 Arduino。最后,我们将一起设计一个小型库,并检查一些不错的技巧和窍门来改进我们的 C 代码。
第十章。一些高级技术
在本章中,我们将学习可以一起使用或独立使用的技术。在这里开发的每个技术都是你未来或当前项目的新工具。我们将使用 EEPROM 为 Arduino 板提供一个可读可写的内存系统。
我们还将测试 Arduino 板之间的通信,使用 GPS 模块,使我们的板子实现自主化,以及更多。
使用 EEPROM 进行数据存储
到目前为止,我们学习和使用 Arduino 板作为完全依赖电力的设备。确实,它们需要电流来执行我们固件中编译的任务。
正如我们所注意到的,当我们关闭它们时,每个活着的变量和数据都会丢失。幸运的是,固件不会。
Arduino 板上的三个原生内存池
基于 ATmega168 芯片组的 Arduino 板拥有三个不同的内存池:
-
闪存内存
-
SRAM
-
EEPROM
闪存也被称为程序空间。这是我们的固件存储的地方。
SRAM代表静态随机存取存储器,是运行中的固件存储、读取和操作变量的地方。
EEPROM代表电擦除可编程只读存储器。这是程序员可以存储长期数据的地方。这是我们的固件所在的地方,如果板子关闭,EEPROM 中的任何内容都不会被擦除。
ATmega168 具有:
-
16000 字节的闪存(其中 2000 字节用于引导加载程序)
-
1024 字节的 SRAM
-
512 字节的 EEPROM
在这里,我们不会讨论在编程时必须注意内存的事实;我们将在本书的最后一章第十三章中这样做,提高你的 C 编程和创建库。
这里有趣的部分是 EEPROM 空间。它允许我们在 Arduino 上存储数据,而我们直到现在甚至都不知道这一点。让我们测试 EEPROM 原生库。
使用 EEPROM 核心库进行读写
基本上,这个示例不需要任何接线。我们将使用 512 字节的内部 EEPROM。以下是一些读取 EEPROM 所有字节并将其打印到计算机串行监视器的代码:
#include <EEPROM.h>
// start reading from the first byte (address 0) of the EEPROM
int address = 0;
byte value;
void setup()
{
// initialize serial and wait for port to open:
Serial.begin(9600);
}
void loop()
{
// read a byte from the current address of the EEPROM
value = EEPROM.read(address);
Serial.print(address);
Serial.print("\t");
Serial.print(value, DEC);
Serial.println();
// advance to the next address of the EEPROM
address = address + 1;
// there are only 512 bytes of EEPROM, from 0 to 511, so if we're
// on address 512, wrap around to address 0
if (address == 512)
address = 0;
delay(500);
}
这段代码属于公共领域,并作为 EEPROM 库的示例提供。你可以在 Arduino IDE 的文件菜单下的示例文件夹中找到它,在示例 | EEPROM文件夹中。
首先,我们包含库本身。然后我们定义一个用于存储当前读取地址的变量。我们将其初始化为 0,即内存寄存器的开始。我们还定义了一个字节类型的变量。
在setup()函数中,我们初始化串行通信。在loop()中,我们读取当前地址的字节并将其存储在变量value中。然后我们将结果打印到串行端口。注意第二个Serial.print()语句中的\t值。这代表制表符(就像电脑键盘上的Tab键)。这将在打印的当前地址和值本身之间写入制表符,以便使内容更易读。
我们前进到下一个地址。我们检查地址是否等于 512,如果是,我们将地址计数器重置为 0,依此类推。
我们添加了一个小的延迟。我们可以使用EEPROM.write(addr, val);以相同的方式写入字节,其中addr是你想写入值val的地址。
小心,这些都是字节(8 比特=256 个可能值)。在内部 EEPROM 上读写操作相当简单,所以让我们看看通过 I2C 连接的外部 EEPROM 会怎样。
外部 EEPROM 布线
电子市场上有很多廉价的 EEPROM 组件。我们将使用经典的 24LC256,这是一个实现 I2C 读写操作并提供 256 千比特(32 千字节)内存空间的 EEPROM。
你可以在 Sparkfun 找到它:www.sparkfun.com/products/525。以下是使用 I2C 布线其更大的兄弟 24LC1025(1024k 字节)的方法:

通过 I2C 通信连接到 Arduino 的 24LC256 EEPROM
对应的图如下所示:

通过 I2C 通信连接到 Arduino 的 24LC256 EEPROM
让我们描述一下 EEPROM。
A0、A1和A2是芯片地址输入。+V和0V是5V和地。WP 是写保护引脚。如果它连接到地,我们可以写入 EEPROM。如果它连接到 5V,则不能。
SCL 和 SDA 是参与 I2C 通信的两个引脚,并连接到SDA / SCL。SDA代表串行 数据 线,SCL代表串行 时钟 线。注意 SDA/SCL 引脚。以下取决于你的板:
-
Arduino UNO R3 之前的 I2C 引脚是 A4(SDA)和 A5(SCL)
-
Mega2560,20 号引脚(SDA)和 21 号引脚(SCL)
-
Leonardo,2 号引脚(SDA)和 3 号引脚(SCL)
-
Due 引脚,20 号引脚(SDA)和 21 号引脚(SCL),还有一个 SDA1 和 SCL1
读写 EEPROM
我们可以用于 I2C 目的的底层库是Wire。你可以在 Arduino 核心库中直接找到它。这个库负责处理原始比特,但我们需要更仔细地查看它。
Wire库为我们处理了很多事情。让我们检查文件夹Chapter10/readWriteI2C中的代码:
#include <Wire.h>
void eepromWrite(byte address, byte source_addr, byte data) {
Wire.beginTransmission(address);
Wire.write(source_addr);
Wire.write(data);
Wire.endTransmission();
}
byte eepromRead(int address, int source_addr) {
Wire.beginTransmission(address);
Wire.write(source_addr);
Wire.endTransmission();
Wire.requestFrom(address, 1);
if(Wire.available())
return Wire.read();
else
return 0xFF;
}
void setup() {
Wire.begin();
Serial.begin(9600);
for(int i = 0; i < 10; i++) {
eepromWrite(B01010000, i, 'a'+i);
delay(100);
}
Serial.println("Bytes written to external EEPROM !");
}
void loop() {
for(int i = 0; i < 10; i++) {
byte val = eepromRead(B01010000, i);
Serial.print(i);
Serial.print("\t");
Serial.print(val);
Serial.print("\n");
delay(1000);
}
}
我们首先包含Wire库。然后我们定义 2 个函数:
-
eepromWrite() -
eepromRead()
这些函数使用Wire库将字节写入和读取到外部 EEPROM。
Setup() 函数实例化了 Wire 和 Serial 通信。然后使用 for 循环,我们将数据写入特定的地址。这些数据基本上是一个字符 'a' 加上一个数字。这种结构从 'a' 写到 'a' + 9,即 'j'。这是一个展示我们如何快速存储东西的例子,但当然我们可以写入更有意义的数据。
然后,我们向串行监视器打印一条消息,以告知用户 Arduino 已完成对 EEPROM 的写入。
在 loop() 函数中,我们读取 EEPROM。它与 EEPROM 库非常相似。
显然,我们还没有讨论地址。以下是一个 I2C 消息格式:

一个 I2C 消息
Wire 库负责起始位和确认位。控制码是固定的,你可以通过将芯片选择位(A0、A1 和 A2 引脚)连接到地或 +V 来更改。这意味着有 8 种地址的可能性,从 0 到 7。
1010000 1010001… 直到 1010111。1010000 二进制表示十六进制的 0x50,而 1010111 表示 0x57。
在我们的情况下,我们将 A0、A1 和 A2 连接到地,然后 EEPROM 在 I2C 总线上的地址是 0x50。我们可以在 I2C 总线上使用多个地址,但只有当我们需要更多的存储容量时。实际上,我们可能需要在固件中为不同的设备分配地址。
我们现在可以想象在 EEPROM 空间中存储很多东西,从播放 PCM 音频的样本到,最终,巨大的查找表或任何需要比 Arduino 本身更多内存的东西。
使用 GPS 模块
GPS 代表 全球定位系统。该系统基于卫星星座。
基本上,一个至少接收来自 4 颗嵌入特殊原子钟的卫星的接收器,通过计算这些信号之间的传播时间以及与自身的传播时间,可以精确地计算出其三维位置。这听起来很神奇;其实只是三角学的应用。
我们不会深入探讨这个过程的细节;相反,我们关注来自 GPS 模块的数据解析。你可以在维基百科上获取更多信息:en.wikipedia.org/wiki/Global_Positioning_System。
连接 Parallax GPS 接收器模块
Parallax GPS 接收器基于 PMB-248 规范,以其小巧的尺寸和低廉的成本,为 Arduino 添加位置检测提供了非常简单的方法。

Parallax GPS 接收器:小巧尺寸和精确
它提供标准的原始 NMEA01823 字符串,甚至可以通过串行命令接口提供特定用户请求的数据。它可以跟踪 12 颗卫星,甚至 WAAS(仅在 USA 和 Hawaii 可用的系统,用于帮助 GPS 信号计算)。
NMEA0183 是一种结合了硬件和逻辑规范,用于海洋电子设备(如声纳、风速计等)之间的通信,包括 GPS。关于此协议的详细描述可以在这里找到:aprs.gids.nl/nmea/。
该模块提供当前时间、日期、纬度、经度、海拔、速度以及航向/航向,以及其他数据。
我们可以向 GPS 模块写入数据以请求特定的字符串。然而,如果我们将 /RAW 引脚拉低,模块会自动传输一些字符串。这些字符串是:
-
$GPGGA: 全球定位系统定位数据
-
$GPGSV: 视野中的 GPS 卫星
-
$GPGSA: GPS DOP 和活动卫星
-
$GPRMC: 推荐的最小特定 GPS/Transit 数据
这些数据必须由 Arduino 捕获并最终使用。让我们先检查一下接线:

通过将 /RAW 引脚拉低,自动模式下连接到 Arduino 的 Parallax GPS 接收器
接线相当简单。
是的,Parallax GPS 接收器只消耗一个数据引脚:数字引脚 0。让我们在这里停顿两秒钟。我们不是讨论过在 Arduino 上我们不能同时使用 USB 端口进行串行监控,以及引脚 0 和 1 用于其他串行功能吗?
使用 Rx/Tx 2 根线进行串行通信,串行软件实现可以是全双工的。
在我们的例子中,GPS 设备将数据发送到 Arduino 的 Rx 引脚。这个引脚(数字引脚 0)连接到 USB Rx 引脚。同时,Arduino 使用连接到数字引脚 1 的 USB Tx 引脚将数据发送到计算机。
在我们的情况下这里有问题吗?没有。我们只需要注意干扰。我们绝对不能通过 USB 从计算机发送数据到 Arduino,因为它已经从 GPS 设备接收了串行引脚 0 的数据。这是我们唯一需要注意的事情。
Serial.write() 函数将写入数字引脚 1,而 USB Tx 数字引脚 1 没有连接到任何东西。因此,没有问题,数据将被发送到 USB。Serial.read() 函数从数字引脚 0 和 USB 读取,我们没有从计算机发送任何数据到 USB,所以它可以无任何问题地读取数字引脚 0。
我们将 /RAW 引脚拉低。在这种模式下,设备会自动将数据推送到 Arduino;我的意思是,不需要请求它。
解析 GPS 位置数据
在构建任何能够使用 GPS 数据的固件之前,我们必须更多地了解设备能够传输的内容。
我们可以在以下位置查看 GPS 设备的数据表:www.rcc.ryerson.ca/media/2008HCLParallaxGPSReceiverModuledatasheet.pdf。
这里是可传输数据的一个示例:
$GPRMC,220516,A,5133.82,N,00042.24,W,173.8,231.8,130694,004.2,W*70
$GPRMC 定义了发送的信息序列的类型。逗号是一个分隔符,用于分隔每个数据字段。
下面是每个字段的含义:
-
定位的 UTC 时间
-
数据状态(
A表示有效位置,V表示警告) -
定位的纬度
-
北或南纬度
-
定位的经度
-
东或西经度
-
地面速度(以节为单位)
-
航迹向度(以度为单位)
-
定位的 UTC 日期
-
磁差(以度为单位)
-
东或西磁差
-
校验和
一旦我们知道发送了什么数据,我们就可以在我们的固件中编写一个解析器。以下是一个可能的固件示例。您可以在文件夹 Chapter10/locateMe 中找到它:
int rxPin = 0; // RX PIN
int byteGPS = -1; // Current read byte
char line[300] = ""; // Buffer
char commandGPR[7] = "$GPRMC"; // String related to messages
int counter=0;
int correctness=0;
int lineCounter=0;
int index[13];
void setup() {
pinMode(rxPin, INPUT);
Serial.begin(4800);
// Clear buffer
for (int i=0;i<300;i++){
line[i]=' ';
}
}
void loop() {
byteGPS = Serial.read(); // Read a byte from the serial port
// Test if the port is empty
if (byteGPS == -1) {
delay(100);
}
else { // if it isn't empty
line[lineCounter] = byteGPS; // put data read in the buffer
lineCounter++;
Serial.print(byteGPS); // print data read to the serial monitor
// Test if the transmission is finished
// if it is finished, we begin to parse !
if (byteGPS==13){
counter=0;
correctness=0;
// Test if the received command starts by $GPR
// If it does, increase correctness counter
for (int i=1;i<7;i++){
if (line[i]==commandGPR[i-1]){
correctness++;
}
}
if(correctness==6){
// We are sure command is okay here.
//
for (int i=0;i<300;i++){
// store position of "," separators
if (line[i]==','){
index[counter]=i;
counter++;
}
// store position of "*" separator meaning the last byte
if (line[i]=='*'){ // ... and the "*"
index[12]=i;
counter++;
}
}
// Write data to serial monitor on the computer
Serial.println("");
Serial.println("");
Serial.println("---------------");
for (int i=0;i<12;i++){
switch(i){
case 0 :
Serial.print("Time in UTC (HhMmSs): ");
break;
case 1 :
Serial.print("Status (A=OK,V=KO): ");
break;
case 2 :
Serial.print("Latitude: ");
break;
case 3 :
Serial.print("Direction (N/S): ");
break;
case 4 :
Serial.print("Longitude: ");
break;
case 5 :
Serial.print("Direction (E/W): ");
break;
case 6 :
Serial.print("Velocity in knots: ");
break;
case 7 :
Serial.print("Heading in degrees: ");
break;
case 8 :
Serial.print("Date UTC (DdMmAa): ");
break;
case 9 :
Serial.print("Magnetic degrees: ");
break;
case 10 :
Serial.print("(E/W): ");
break;
case 11 :
Serial.print("Mode: ");
break;
case 12 :
Serial.print("Checksum: ");
break;
}
for (int j=index[i];j<(index[i+1]-1);j++){
Serial.print(line[j+1]);
}
Serial.println("");
}
Serial.println("---------------");
}
// Reset the buffer
lineCounter=0;
for (int i=0;i<300;i++){
line[i]=' ';
}
}
}
}
让我们解释一下代码。首先,我定义了几个变量:
-
rxPin是 GPS 设备连接的数字输入 -
byteGPS是通过串行通信从 GPS 读取的最新字节 -
line是一个缓冲区数组 -
commandGPR是与我们要解析的消息相关的字符串 -
counter是索引数组的索引 -
correctness存储消息的有效性 -
lineCounter是跟踪数据缓冲区位置的计数器 -
index存储 GPS 数据字符串中每个分隔符的位置(",")
在 setup() 函数中,我们首先将数字引脚 0 定义为输入,然后以串行接口所需的 4800 波特率开始串行通信(请记住始终检查您的数据表)。然后,我们通过填充空格字符来清除我们的 line 数组缓冲区。
在 loop() 函数中,我们首先从串行输入读取字节,数字引脚为 0。如果端口不为空,我们进入由 else 块定义的 if 条件测试的第二部分。如果它是空的,我们只需等待 100 毫秒然后再次尝试读取。
首先,解析开始于将读取的数据放入行缓冲区中数组的特定索引:lineCounter。然后,我们增加后者以便存储接收到的数据。
我们然后将读取的数据作为原始行打印到 USB 端口。就在这个时候,串行监视器可以接收并显示我们之前引用的示例中的原始数据行。
然后,我们测试数据本身,将其与 13 进行比较。如果它等于 13,这意味着数据通信已完成,我们可以开始解析。
我们重置 counter 和 correctness 变量,并检查缓冲区中的前 6 个字符是否等于 $GPRMC。对于每个匹配项,我们增加 correctness 变量。
这是一个经典的模式。实际上,如果所有测试都为真,那么最终 correctness 等于 6。然后我们只需检查 correctness 是否等于 6,以查看是否所有测试都为真,以及前 6 个字符是否等于 $GPRMC。
如果是这样,我们可以确信我们有一个正确的 NMEA 原始序列类型 $GPRMC,然后我们可以开始实际解析数据的负载部分。
首先,我们通过存储字符串中每个逗号分隔符的位置来分割我们的原始字符串。然后,我们用最后一个部分分隔符,即"*"字符,做同样的事情。在这个时候,我们能够区分哪个字符属于字符串的哪个部分,我的意思是,哪个部分属于原始消息。
这是一个在原始消息的每个值之间的循环,我们使用 switch/case 结构测试每个值,以便显示介绍 GPS 数据消息每个值的正确句子。
最后,最棘手的部分是最后的for()循环。我们并不像通常那样开始。实际上,我们在循环中使用数组index在特定位置i来开始j索引。
这里是一个显示原始消息周围索引的小型电路图:

根据每个分隔符逐步解析消息的每一部分
我们根据每个分隔符的位置逐步增加,并显示每个值。这是使用 GPS 模块解析和使用位置数据的一种方法。这些数据可以根据你的目的以多种方式使用。我喜欢数据可视化,我为学生制作了小项目,使用 GPS 模块每隔 30 秒在街上抓取位置并写入 EEPROM。然后,我使用这些数据制作了一些图表。我最喜欢的一个是以下这个:

使用由 GPS Arduino 模块提供的数据集设计的 Processing 数据可视化
每一行都是一个时间戳。行的长度代表我在 Arduino GPS 模块两次测量之间花费的时间。行越长,我在这个旅行步骤上花费的时间就越长。
你的问题可能是:你在街上行走时是如何给你的 Arduino + GPS 模块供电的?
现在,让我们看看如何使用电池使 Arduino 实现自主性。
Arduino、电池和自主性
Arduino 板可以通过两种方式供电:
-
来自电脑的 USB 线
-
外部电源
从本节开始,我们就已经使用 USB 为 Arduino 供电。这是一种相当好的开始方式(甚至可以做出一个很棒的项目)。这很简单,适用于许多用途。
当我们需要更多的自主性和移动性时,我们也可以使用外部电源为 Arduino 设备供电。
在任何情况下,我们都要记住,我们的 Arduino 及其连接的电路都需要供电。通常,Arduino 的功耗不超过 50mA。添加一些 LED,你会发现功耗增加。
让我们检查一些实际应用的案例。
经典的 USB 供电案例
我们为什么和什么时候会使用 USB 电源?
显然,如果我们需要我们的电脑连接到 Arduino 进行数据通信,我们可以自然地通过 USB 为 Arduino 供电。
这就是使用 USB 电源的主要原因。
也有一些情况,我们无法拥有很多电源插座。有时,在安装设计项目中存在许多限制,我们没有很多电源插座。这也是使用 USB 供电的一个例子。
基本上,在使用 USB 端口供电之前,首先要考虑的是我们电路的全球功耗。
的确,正如我们已经学到的,USB 端口可以提供的最大电流大约是 500mA。确保不要超过这个值。超过这个功耗限制,事情变得完全不可预测,有些电脑甚至可能重新启动,而有些电脑可能禁用所有 USB 端口。我们必须记住这一点。
外部电源供电
有两种不同的方式为基于 Arduino 的系统供电。我们可以将两种主要的电源供应方式表述为:
-
电池
-
电源适配器
使用电池供电
如果我们记得正确的话,Arduino Uno 和 Mega 等实例可以在 6 V 到 20 V 的外部电源下运行。为了稳定使用,建议的范围是 7 V 到 12 V。9 V 是一个理想的电压。
为了将板设置为外部电源供电,你必须注意电源跳线。我们必须将其放在外部电源侧,称为 EXT。这种设置适用于 Arduino Diecimilla 和较老的 Arduino 板:

将电源跳线放在 EXT 侧,意味着设置为外部电源
让我们用 9 V 电池检查基本接线:

一个连接到 Arduino 板 UNO R3 的 9V 电池
这种简单的接线提供了一种为 Arduino 板供电的方法。如果你将其他电路连接到 Arduino 上,通过 Arduino 的电池将为它们供电。
我们还可以使用其他类型的电池。纽扣电池是一种在外部供电时节省空间的好方法:

一个经典的纽扣电池
有许多类型的纽扣电池座,可以在我们的电路中使用这种电池。通常,纽扣电池提供 3.6 V,110 mAh。如果这不能为 Arduino Uno 供电,它可以轻松地为工作在 3.3 V 电压下的 Arduino Pro Mini 供电:

Arduino Pro Mono
Arduino Pro Mini 板非常有趣,因为它可以嵌入许多需要离散和有时隐藏在墙内的电路中,用于数字艺术安装,或者当它们作为移动工具使用时,可以放入可以放入口袋的小塑料盒中。
我们还可以使用聚合物锂离子电池。我曾在几个自主设备项目中使用过它们。
然而,我们可能会有一些需要更多电力的项目。
Arduino 电源适配器
对于需要更多电力的项目,我们必须使用外部电源。Arduino 的设置与使用电池时相同。现成的 Arduino 适配器必须满足一些要求:
-
直流适配器(这里没有交流适配器!)
-
输出电压为 9V 至 12V 直流电
-
至少能输出 250mA 的最低电流,但目标是 500mA 或更佳,最好是 1A
-
必须有一个中心正极 2.1mm 电源插头
在插入 Arduino 之前,你必须在适配器上寻找以下图案。
首先,连接器的中心必须是正极部分;查看以下图解。你应该能看到在 Arduino 兼容适配器上:

表示中心正极插头的符号
然后,电压和电流特性。这必须显示类似以下内容:输出:12 VDC 1 A。这是一个例子;12 VDC 和 5 A 也是可以的。别忘了电流只由电路中的内容驱动。输出更高电流的电源适配器不会损害你的电路,因为电路只会吸取它需要的。
市面上有很多适配器可供使用,并且可以与我们的 Arduino 板一起使用。
如何计算电流消耗
为了计算电路中的电流,你必须使用本书第一章中描述的欧姆定律。
当你检查一个组件的数据表,比如 LED,你可以看到通过它的电流。
让我们用这份数据表检查 RGB 共阴极 LED:www.sparkfun.com/datasheets/Components/YSL-R596CR3G4B5C-C10.pdf
我们可以看到正向电流为 20 mA,峰值正向电流为 30 mA。如果我们有五个这样的 LED 以最大亮度开启(即红色、蓝色和绿色点亮),我们就有:5 x (20 + 20 + 20) = 300 mA 的正常使用电流,甚至峰值也会消耗 5 x (30 + 30 + 30) = 450 mA。
在这种情况下,所有 LED 都同时完全开启。
你必须已经理解了我们已经在电源循环中使用的策略,即依次快速开启每个 LED。这提供了一种减少功耗的方法,同时也允许一些项目使用大量 LED 而不需要外部电源适配器。
我不会在这里描述每种情况的计算,但你必须参考电学规则来精确计算消耗。
根据经验,没有比你的电压表和安培表更好的工具了,前者测量两点之间的电压,后者测量电路中某些点的电流。
我建议你做一些计算以确保:
-
不要超过 Arduino 每引脚的容量
-
不要超过 USB 450mA 的限制,以防你使用 USB 电源
然后,之后,同时使用电压表和安培表进行布线和测量。
最后,大多数 Arduino 板的一个经典参考可以在本页找到:playground.arduino.cc/Main/ArduinoPinCurrentLimitations。
我们可以找到 Arduino 每个部分的电流消耗限制。
在 gLCD 上绘图
绘图总是很有趣。与 LED 矩阵相比,绘制和处理 LCD 显示也很有趣,因为我们有可以轻松开关的高密度点设备。
LCD 存在许多类型。两种主要类型是字符和图形类型。
我们在这里讨论的是图形类型,特别是基于在许多常规 gLCD 设备中使用的 KS0108 图形控制器。
我们将使用一个在 Google 上可用的优秀库。它包含 Michael Margolis 和 Bill Perry 的代码,命名为glcd-arduino。此库根据 GNU Lesser GPL 许可。
让我们在这里下载它:code.google.com/p/glcd-arduino/downloads/list。下载最新版本。
解压它,将其放在所有库所在的目录中,然后重新启动或启动你的 Arduino IDE。
你现在应该看到很多与 gLCD 库相关的示例。
我们不会检查这个库提供的所有优秀功能和功能,但您可以在 Arduino 网站上查看这个页面:playground.arduino.cc/Code/GLCDks0108。
连接设备
我们将检查基于 KS0108 的 gLCD 类型面板 B 的连接:

将许多线连接到 Arduino 和电位器以调整 LCD 对比度
对应的电气图如下:

基于 KS0108 的 gLCD 类型面板 B 连接到 Arduino Uno R3
这有很多线。当然,我们可以乘以东西。我们还可以使用 Arduino MEGA 并继续使用其他数字引脚用于其他目的,但这不是重点。让我们检查这个强大库的一些功能。
演示库
查看名为GLCDdemo的示例。它展示了库中几乎所有的功能。
库中提供了非常好的 PDF 文档。它解释了每个可用的方法。您可以在library文件夹中的doc子文件夹中找到它:

gLCD-Arduino 文档显示屏幕坐标系系统
首先,我们必须包含glcd.h以使用库。然后,我们必须包含一些其他头文件,在这个例子中,字体和位图,以便使用字体排版方法和位图对象。
一些有用的方法家族
我建议将学习方法分为三个部分:
-
全局 GLCD 方法
-
绘图方法
-
文本方法
全局 GLCD 方法
第一项是init()函数。这个函数初始化库,必须在调用任何其他 gLCD 方法之前调用。
SetDisplayMode() 函数很有用,因为它设置 LCD 的使用为正常(在白色背景上用黑色书写)或反转。白色只是意味着不是黑色。真正的颜色当然取决于背光颜色。
ClearScreen() 函数擦除屏幕,在正常模式下填充白色背景,或在反转模式下填充黑色。
ReadData() 和 WriteData() 函数是真正原始的方法,它们获取和设置特定坐标处的字节数据。
绘图方法
这些是一组专门用于在屏幕上绘制的函数。
常量集合如下:
-
GLCD.Width是显示宽度(以像素为单位) -
GLCD.Height是显示高度(以像素为单位) -
GLCD.Right是最右侧的最后一行像素(等于 GLCD.Width – 1) -
GLCD.Bottom是底部最后一行像素(等于 GLCD.Height – 1) -
GLCD.CenterX和GLCD.CenterY是像素中心的坐标
基本上,你可以通过移动图形光标和绘制原始形状来绘图:
| 函数 | 描述 |
|---|---|
GotoXY() |
将光标移动到特定坐标 |
DrawVLine() |
在同一像素列中从一点绘制到另一点,但位于初始点的上方或下方 |
DrawHLine() |
与 DrawVLine() 工作方式相同,但在同一像素行上 |
DrawLine() |
在两个坐标之间绘制线条 |
还可以绘制一些更复杂的形状:
| 函数 | 描述 |
|---|---|
DrawRect() |
当提供宽度和高度时,从一点绘制矩形。 |
FillRect() |
与 DrawRect() 工作方式相同,但通过用黑色(或白色)像素填充矩形形状。 |
DrawRoundRect() |
绘制具有圆角的矩形。 |
DrawCircle() 和 FillCircle() |
从坐标和半径绘制圆,以及用黑色(或白色)像素填充的圆。 |
DrawBitmap() |
在屏幕上的特定位置绘制整个位图。它使用指向该位图的内存中的指针。 |
使用这组函数,你可以基本上绘制任何你想要的东西。
文本方法
这些是一组专门用于屏幕排版的函数:
| 函数 | 描述 |
|---|---|
SelectFont() |
首先,这选择在后续函数调用中使用的字体。 |
SetFontColor() |
选择颜色。 |
SetTextMode() |
选择滚动方向。 |
CursorTo() |
将光标移动到特定的列和行。列的计算使用最宽字符的宽度。 |
CursorToXY() |
将光标移动到特定的像素坐标。 |
有一个重要的特性需要了解,那就是 Arduino 的打印函数可以与 gLCD 库一起使用;例如,GLCD.print() 可以正常工作。官方网站上还有其他一些函数可供使用。
最后,我建议您测试名为 life 的示例。这是基于约翰·康威的生命游戏。这是一个很好的例子,展示了您可以做什么,并实现一些不错且有用的逻辑。
在 gLCD 上绘图很棒,但我们也可以使用一个小型处理 VGA 的模块。
使用 Gameduino 扩展板通过 VGA 输出
Gameduino 是一个 Arduino 扩展板。这是我们在这本书中首次使用的一个。基本上,扩展板是一个可以插入到另一个 PCB(印刷电路板)上的 PCB,这里指的是我们的 Arduino。
Arduino 扩展板是预制的电路,包括组件,有时还包括处理器。它们通过处理一些特定任务来为我们的 Arduino 板添加功能。
在这里,Gameduino 将为我们的 Arduino 添加无法自行完成的 VGA 绘图功能。
Gameduino 添加了一个 VGA 端口、一个用于声音的迷你插孔,并且还包含了一个 FPGA Xilling Spartan3A。FPGA Xilling Spartan3A 可以比 Arduino 本身更快地处理图形数据。Arduino 可以通过 SPI 接口控制这个图形硬件驱动程序。
让我们看看它是如何工作的:

Gameduino 控制器 Arduino 扩展板
Arduino 扩展板可以直接插入 Arduino 板。请查看以下截图:

Gameduino 插入 Arduino 板
这里是 Gameduino 的一些特点:
-
视频输出为 400 x 300 像素,512 种颜色
-
所有颜色都以内置 15 位精度处理
-
兼容任何标准 VGA 显示器(800 x 600 @ 72 Hz)
-
背景图形(512 x 512 像素字符,256 个字符)
-
前景图形(16 x 16 像素精灵能力,透明度,旋转/翻转,精灵碰撞检测)
-
音频输出为立体声;12 位频率合成器
-
64 个独立的 10 到 8000 Hz 语音
-
样本回放通道
基本概念是将它插入 Arduino,并使用我们的 Arduino 固件来控制它,库负责处理 Arduino 和 Gameduino 之间的所有 SPI 通信。
我们不能在这里描述所有示例,但我希望您能找到正确的方向。首先,官方网站:excamera.com/sphinx/gameduino/。
您可以在以下位置找到库:excamera.com/files/gameduino/synth/sketches/Gameduino.zip。
您还可以在此处查看和使用快速参考海报:excamera.com/files/gameduino/synth/doc/gen/poster.pdf。
为了您的信息,我目前正在设计一个基于这个扩展板的数字艺术装置。我打算在我的个人网站上julienbayle.net描述它,并且还会提供整个电路图。
摘要
在本章的第一个、高级章节中,我们了解了一些关于如何处理新具体概念的方法,例如在非易失性存储器(内部和外部 EEPROM)上存储数据,使用 GPS 模块接收器,在图形 LCD 上绘图,以及使用一个名为 Gameduino 的 Arduino Shield 来添加新功能和增强我们的 Arduino。这使得它能够显示 VGA 信号,并且还能产生音频。我们还学习了 Arduino 作为一个非常便携和移动设备的用途,从电源供应的角度来看是自给自足的。
在下一章中,我们将讨论网络概念。创建和使用网络是当今常见的通信方式。在下一章中,我们将描述使用 Arduino 项目进行有线和无线网络的使用。
第十一章:网络通信
在本章中,我们将讨论通过创建通信网络来连接对象并使它们通过通信进行交流。我们将学习如何通过网络链路和协议使多个 Arduino 和计算机进行通信。
在定义了什么是网络(特别是数据网络)之后,我们将描述如何在 Arduino 和计算机之间使用有线以太网链路。这将使 Arduino 世界通向互联网。然后,我们将探讨如何创建蓝牙通信。
我们将学习如何使用以太网 Wi-Fi 将 Arduino 连接到计算机或其他 Arduino,而无需被网络电缆所束缚。
最后,我们将研究几个例子,从向微博服务 Twitter 发送消息的例子,到解析和响应从互联网接收到的数据的例子。
我们还将介绍广泛用于与交互设计、音乐和多媒体相关的一切的 OSC 交换协议。
网络概述
网络是由相互连接的元素组成的系统。我们周围有许多网络,如公路系统、电网和数据网络。数据网络包围着我们。它们与视频服务网络、电话和全球电信网络、计算机网络等相关。我们将通过讨论如何通过不同类型的媒体(如传输电脉冲的电线或促进无线通信的电磁波)共享数据来关注这些类型的网络。
在我们深入 Arduino 板网络实现细节之前,我们将描述一个名为 OSI 模型(开放系统互连模型)的模型。这是一个非常有用的表示,说明了数据网络是什么以及它涉及的内容。
OSI 模型概述
开放 系统 互连模型(OSI模型)于 1977 年由国际标准化组织发起,旨在定义关于通信系统功能的抽象层的规定和要求。
基本上,这是一个基于层的模型,描述了设计通信系统所需的功能。以下是具有七层的 OSI 模型:

描述通信系统要求的七层抽象的 OSI 模型
协议和通信
通信协议是一组消息格式和规则,提供了一种至少两个参与者之间通信的方式。在每一层中,一个或多个实体实现其功能,每个实体直接且仅与下一层交互,同时为上层提供使用设施。协议使一个主机中的一个实体能够与另一个主机中同一层的相应实体进行交互。这可以通过以下图表表示:

协议帮助主机层之间进行通信
数据封装和解封装
如果一个主机的应用程序需要将数据发送到另一个主机的应用程序,有效数据,也称为有效载荷,将直接传递到其下的一层。为了使应用程序能够检索其数据,根据每一层使用的协议,将添加头部和尾部到这些数据。这被称为封装,并且一直发生到最低层,即物理层。在这一点,一个比特流被调制到介质上以供接收器使用。
接收器必须使数据逐步爬升层堆栈,将数据从一层传递到更高一层,并使用之前添加的头部和尾部将其地址指向每一层的正确实体。这些头部和尾部在整个路径上都被移除;这被称为解封装。
在旅程结束时,接收器的应用程序接收其数据并可以处理它。整个过程可以用以下图表表示:

在层堆栈中沿层进行封装和解封装
我们也可以将这些过程表示如下图所示。小灰色矩形是层 N+1 的数据有效载荷。

根据使用的协议添加和移除特定的头部和尾部
在每一级,两个主机使用传输的协议进行交互,我们称之为协议数据单元或PDU。我们还将从一层传递到下一层且尚未封装的特定数据单元称为服务数据单元或SDU。
每一层都将接收到的数据视为自己的数据,并根据所使用的协议添加/移除头部和尾部。
我们现在将通过示例来阐述每一层和协议。
每一层的角色
我们将在这里描述每一层的用途和角色。
物理层
物理层定义了通信所需的电气和物理规范。
引脚布局、电压和线路阻抗、信号时序、网络适配器或主机总线适配器在此层定义。基本上,这一层执行三个主要功能/服务:
-
初始化和终止与通信介质的连接
-
参与共享资源控制过程
-
通信数据与携带它们的电气信号之间的转换
我们可以引用一些已知的标准,它们位于这一物理层:
-
ADSL 和 ISDN(网络和模拟服务提供商)
-
蓝牙
-
IEEE 1394(FireWire)
-
USB
-
IrDA(通过红外链路的数据传输)
-
SONET、SDH(由提供商运营的广域光纤网络)
数据链路层
这一层由两个子层组成:
-
逻辑链路控制 (LLC)
-
媒体访问控制 (MAC)
它们都负责在网络实体之间传输数据,并检测物理层可能发生的错误,最终修复它们。基本上,这一层提供以下功能/服务:
-
封装
-
物理寻址
-
流控制
-
错误控制
-
访问控制
-
媒体访问控制
我们可以引用该数据链路层的一些已知标准:
-
以太网
-
Wi-Fi
-
PPP
-
I2C
我们必须记住,第二层也是局域网的领域,只有物理地址。它可以通过局域网交换机进行联邦。
顺便说一下,我们经常需要分段网络并更广泛地通信,因此我们需要另一个寻址概念;这引入了网络层。
网络层
这一层提供了在不同网络中的主机之间传输数据序列的方法。它提供以下功能/服务:
-
路由
-
分片和重组
-
报告交付错误
路由提供了一种使不同网络上的主机能够通过使用网络寻址系统进行通信的方法。
分片和重组也发生在这一级。这些提供了一种将数据流切割成片段并在传输后重新组装部分的方法。我们可以引用这一层的一些已知标准:
-
ARP(解析和将物理 MAC 地址转换为网络地址)
-
BOOTP(为主机通过网络启动提供一种方式)
-
BGP、OSPF、RIP 和其他路由协议
-
IPv4 和 IPv6(互联网协议)
路由器通常是路由发生的地方。它们连接到多个网络,使数据从一个网络传输到另一个网络。这也是我们可以放置一些访问列表以根据 IP 地址控制访问的地方。
传输层
这一层负责在终端用户之间进行数据传输,位于网络层和应用层的交汇处。这一层提供以下功能/服务:
-
流控制以确保链路的可靠性
-
数据单元的分割/解分割
-
错误控制
通常,我们将协议分为两类:
-
面向状态
-
面向连接
这意味着这一层可以跟踪发出的段,并在之前传输失败的情况下最终重新传输它们。
在这一层,我们可以引用 IP 套件的两个著名标准:
-
TCP
-
UDP
TCP 是面向连接的。它通过在每个传输或每个 x 个传输的段中检查许多元素来保持通信的可靠性。
UDP 更简单且无状态。它不提供通信状态控制,因此更轻量。它更适合于面向事务的查询/响应协议,如 DNS(域名系统)或 NTP(网络时间协议)。如果有问题,例如一个分段没有很好地传输,上面的层必须负责重新发送请求,例如。
应用/主机层
我将最高三层归类为应用和主机。
事实上,它们不被视为网络层,但它们是 OSI 模型的一部分,因为它们通常是任何网络通信的最终目的。
我们在那里发现了许多客户端/服务器应用程序:
-
FTP 用于基本和轻量级文件传输
-
POP3、IMAP 和 SMTP 用于邮件服务
-
SSH 用于安全的远程 shell 通信
-
HTTP 用于网页浏览和下载(以及如今更多)
我们还发现了许多与加密和安全相关的标准,例如 TLS(传输层安全性)。我们的固件,一个正在执行的 Processing 代码,Max 6 运行补丁都在这一层。
如果我们想让它们通过广泛的网络进行通信,我们需要一些 OSI 栈。我的意思是,我们需要一个传输和网络协议以及一个传输数据的中介。
如果我们的现代计算机拥有整个网络栈并准备好使用,那么如果我们想让它们能够与世界通信,我们就必须在 Arduino 的固件中稍后构建这个功能。这就是我们在下一小节将要做的。
一些关于 IP 地址和端口的方面
我们每天倾向于使用的协议栈之一是 TCP/IP。TCP 是第 4 层传输协议,IP 是第 3 层网络。
这是世界上使用最广泛的网络协议,无论是对于终端用户还是对于公司。
我们将更详细地解释 IP 寻址系统、子网掩码和通信端口。我不会描述一个完整的网络课程。
IP 地址
IP 地址是任何想要通过 IP 网络通信的设备引用的数值地址。IP 目前使用 2 个版本:IPv4 和 IPv6。在这里我们考虑 IPv4,因为它目前是终端用户唯一使用的版本。IPv4 地址由 32 位编码。它们通常被写成由点分隔的 4 个字节的易读集合。192.168.1.222 是我的计算机当前的 IP 地址。有 2³²个可能的唯一地址,并且并不是所有都可以在互联网上路由。一些被保留用于私有用途。一些公司分配可路由互联网地址。实际上,我们不能使用这两个地址,因为这是由全球组织处理的。每个国家都有为自身目的分配的地址集合。
子网
子网是一种将我们的网络分割成多个更小网络的方法。设备网络的配置通常包含地址、子网掩码和网关。
地址和子网掩码定义了网络范围。了解发送器是否可以直接与接收器通信是必要的。实际上,如果后者在同一网络内,通信可以直接发生;如果它在另一个网络中,发送器必须将其数据发送到网关,网关将数据路由到正确的下一个节点,以便尽可能到达接收器。
网关了解它所连接的网络。它可以跨不同网络路由数据,并最终根据某些规则过滤一些数据。
通常,子网掩码也以人类可读的 4 字节集合的形式编写。显然,有一个位表示法,对于那些不习惯于操作数字的人来说更难。
我的计算机的子网掩码是 255.255.255.0。这些信息和我的 IP 地址定义了我的家庭网络从 192.168.1.0(这是基本网络地址)开始,到 192.168.1.255(这是广播地址)结束。我不能使用这些地址为我的设备分配,而只能使用从 192.168.1.1 到 192.168.1.254 的地址。
通信端口
通信端口是定义并相关于第 4 层,即传输层的某个东西。
假设你想向特定应用的主机发送一条消息。接收者必须处于监听模式,以便接收他想要接收的消息。
这意味着它必须为连接打开并保留一个特定的套接字,这就是通信端口。通常,应用程序为它们自己的目的打开特定的端口,一旦一个端口被一个应用程序打开并保留,在第一个应用程序打开期间,它就不能被另一个应用程序使用。
这提供了一种强大的数据交换系统。实际上,如果我们想向一个主机发送超过一个应用的数据,我们可以将我们的消息特别指向这个主机上的不同端口,以到达不同的应用。
当然,为了全球通信,必须定义标准。
TCP 端口 80 用于与 Web 服务器数据交换相关的 HTTP 协议。
UDP 端口 53 用于与 DNS 相关的任何事物。
如果你好奇,你可以阅读以下包含所有声明和保留端口及其相关服务的巨大官方文本文件:www.ietf.org/assignments/service-names-port-numbers/service-names-port-numbers.txt。
这些是惯例。有人可以很容易地在非 80 端口的端口上运行 Web 服务器。然后,这个 Web 服务器的特定客户端必须知道使用的端口。这就是为什么惯例和标准是有用的。
将 Arduino 连接到有线以太网
以太网是现在最常用的局域网。
常规的 Arduino 板不提供以太网功能。有一个名为 Arduino Ethernet 的板提供了本地的以太网和网络功能。顺便说一下,它不提供任何 USB 原生功能。
你可以在这里找到参考页面:arduino.cc/en/Main/ArduinoBoardEthernet。

带有以太网连接器的 Arduino 以太网板
我们将使用 Arduino 以太网盾和一根 100BASE-T 电缆与 Arduino UNO R3 连接。它保留了 USB 功能,并增加了以太网网络连接性,通过比 USB 更长的电缆,为我们提供了一个将计算机与 Arduino 连接的便捷方式。

Arduino 以太网盾
如果你寻找 Arduino 以太网模块,你必须知道它们是带 PoE 模块或不带 PoE 模块销售的。
PoE代表以太网供电,是一种通过以太网连接为设备供电的方式。这需要两个部分:
-
设备上必须供电的模块
-
一台能够提供 PoE 支持的网路设备
在我们这里,我们不会使用 PoE。
通过以太网使 Processing 和 Arduino 通信
让我们设计一个基本系统,展示如何设置 Arduino 板和 processing 小程序之间的以太网通信。
这里,我们将使用一个通过以太网连接到我们电脑的 Arduino 板。我们按下一个按钮,触发 Arduino 通过 UDP 向电脑上的 Processing 小程序发送消息。小程序通过绘制某些内容并发送回消息给 Arduino,Arduino 内置的 LED 灯就会亮起。
基本接线
这里,我们连接一个开关并使用内置的 LED 板。我们必须使用以太网线将我们的 Arduino 板连接到电脑。
这种接线与第五章中 MonoSwitch 项目的接线非常相似,除了我们在这里使用的是 Arduino 以太网屏蔽板而不是 Arduino 板本身。

连接到 Arduino 以太网屏蔽板的开关和下拉电阻
对应的电路图如下:

连接到 Arduino 以太网屏蔽板的开关和下拉电阻
在 Arduino 中编码网络连接实现
正如我们描述的,如果我们想让我们的 Arduino 能够通过以太网线(更普遍地说,通过以太网网络)进行通信,我们必须在固件中实现所需的标准。
有一个名为Ethernet的库可以提供大量的功能。
如同往常,我们必须包含这个本地库本身。你可以通过导航到草图 | 导入库来选择这样做,这几乎包含了你需要的一切。
然而,由于 Arduino 版本 0018 中 SPI 的实现,以及 Arduino 以太网屏蔽板通过 SPI 与 Arduino 板通信,我们必须包含一些额外的内容。请注意这一点。
对于这个代码,你需要:
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
这是一段 Arduino 代码的示例,后面将进行解释。
你可以在Chapter11/WiredEthernet找到完整的 Arduino 代码。
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
// Switch & LED stuff
const int switchPin = 2; // switch pin
const int ledPin = 13; // built-in LED pin
int switchState = 0; // storage variable for current switch state
int lastSwitchState = LOW;
long lastDebounceTime = 0;
long debounceDelay = 50;
// Network related stuff
// a MAC address, an IP address and a port for the Arduino
byte mac[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ipArduino(192, 168, 1, 123);
unsigned int ArduinoPort = 9999;
// an IP address and a UDP port for the Computer
// modify these according to your configuration
IPAddress ipComputer(192, 168, 1, 222);
unsigned int ComputerPort = 10000;
// Send/receive buffer
char packetBuffer[UDP_TX_PACKET_MAX_SIZE]; //buffer for incoming packets
// Instantiate EthernetUDP instance to send/receive packets over UDP
EthernetUDP Udp;
void setup() {
pinMode(ledPin, OUTPUT); // the led pin is setup as an output
pinMode(switchPin, INPUT); // the switch pin is setup as an input
// start Ethernet and UDP:
Ethernet.begin(mac,ipArduino);
Udp.begin(ArduinoPort);
}
void loop(){
// if a packet has been received read a packet into packetBufffer
if (Udp.parsePacket()) Udp.read(packetBuffer,UDP_TX_PACKET_MAX_SIZE);
if (packetBuffer == "Light") digitalWrite(ledPin, HIGH);
else if (packetBuffer == "Dark") digitalWrite(ledPin, LOW);
// read the state of the digital pin
int readInput = digitalRead(switchPin);
if (readInput != lastSwitchState)
{
lastDebounceTime = millis();
}
if ( (millis() - lastDebounceTime) > debounceDelay )
{
switchState = readInput;
}
lastSwitchState = readInput;
if (switchState == HIGH)
{
// If switch is pushed, a packet is sent to Processing
Udp.beginPacket(ipComputer, ComputerPort);
Udp.write('Pushed');
Udp.endPacket();
}
else
{
// If switch is pushed, a packet is sent to Processing
Udp.beginPacket(ipComputer, ComputerPort);
Udp.write('Released');
Udp.endPacket();
}
delay(10);
}
在之前的代码块中,首先我们包含Ethernet库。然后我们声明与开关去抖动和 LED 处理相关的完整变量集。在这些语句之后,我们定义了一些与网络功能相关的变量。
首先,我们必须设置与我们自己的屏蔽板相关的 MAC 地址。这个唯一的标识符通常标示在你的以太网屏蔽板上的标签上。请务必在代码中放入你的 MAC 地址。
然后,我们设置 Arduino 的 IP 地址。只要它遵守 IP 地址方案,并且我们的计算机可以访问,我们就可以使用任何地址。这意味着在同一网络或另一网络,但两者之间有一个路由器。然而,请注意,您选择的 IP 地址必须在本地网络段中是唯一的。
我们还为我们的通信选择一个 UDP 端口。我们使用与我们的计算机相关的网络参数相同的定义,这是通信中的第二组参与者。
我们声明一个缓冲区来存储每次接收到的当前消息。注意常量UDP_TX_PACKET_MAX_SIZE。它在 Ethernet 库中定义。基本上,它被定义为 24 字节,以节省内存。我们可以更改它。然后,我们实例化EthernetUDP对象,以便通过 UDP 接收和发送数据报。setup()函数块包含开关和 LED 的语句,然后是 Ethernet 本身的语句。
我们使用 MAC 和 IP 地址开始以太网通信。然后我们打开并监听定义中指定的 UDP 端口,在我们的例子中是 9999。loop()函数看起来有点复杂,但我们可以将其分为两部分。
在第一部分,我们检查 Arduino 是否已收到数据包。如果收到了,它将通过调用 Ethernet 库的parsePacket()函数并检查它是否返回一个非零的数据包大小来检查。我们读取数据并将其存储在packetBuffer变量中。
然后我们检查这个变量是否等于Light或Dark,并相应地通过在 Arduino 板上打开或关闭 LED 来采取行动。
在第二部分,我们可以看到与我们在第五章中看到的相同的防抖结构。在这一部分的末尾,我们检查开关是否被按下或释放,并根据状态向计算机发送 UDP 消息。
现在我们来检查 Processing/计算机部分。
编写一个通过以太网通信的 Processing Applet
让我们检查Chapter11/WiredEthernetProcessing中的代码。
我们需要超媒体库。我们可以在ubaa.net/shared/processing/udp找到它。
import hypermedia.net.*;
UDP udp; // define the UDP object
String currentMessage;
String ip = "192.168.1.123"; // the Arduino IP address
int port = 9999; // the Arduino UDP port
void setup() {
size(700, 700);
noStroke();
fill(0);
udp = new UDP( this, 10000 ); // create UDP socket
udp.listen( true ); // wait for incoming message
}
void draw()
{
ellipse(width/2, height/2, 230, 230);
}
void receive( byte[] data ) {
// if the message could be "Pushed" or "Released"
if ( data.length == 6 || data.length == 8 )
{
for (int i=0; i < data.length; i++)
{
currentMessage += data[i];
}
// if the message is really Pushed
// then answer back by sending "Light"
if (currentMessage == "Pushed")
{
udp.send("Light", ip, port );
fill(255);
}
else if (currentMessage == "Released")
{
udp.send("Dark", ip, port );
fill(0);
}
}
}
我们首先导入库。然后我们定义 UDP 对象和用于当前接收消息的 String 变量。
在这里,我们也必须定义远程参与者,即 Arduino 的 IP 地址。我们还要定义在 Arduino 侧打开并可用于通信的端口,这里为 9999。
当然,这必须与在 Arduino 固件中定义的相匹配。在setup()函数中,我们定义了一些绘图参数,然后实例化 UDP 端口 10000 上的 UDP 套接字,并将其设置为监听模式,等待传入的消息。
在draw()函数中,我们画一个圆。receive()函数是代码在接收到数据包时调用的回调函数。我们测试数据包的字节数长度,因为我们只想对两种不同的消息做出反应(Pushed或Released),所以我们检查长度是否为 6 或 8 字节。所有其他数据包都不会被处理。我们可以实现一个更好的检查机制,但这个方法已经足够好。
一旦这些长度中的任何一个匹配,我们就将每个字节连接到 String 变量currentMessage中。这提供了一种方便的方法来比较内容与任何其他字符串。
然后,我们将它与Pushed和Released进行比较,并相应地通过向 Arduino 发送消息Light来填充我们绘制的圆圈为白色,或者通过向 Arduino 发送消息Dark来填充我们绘制的圆圈为黑色。
我们刚刚使用以太网和 UDP 设计了我们第一个基本的通信协议。
关于 TCP 的一些话
在我的设计中,我经常使用 UDP 在系统之间进行通信。它比 TCP 轻得多,并且对我们的目的来说已经足够。
在某些情况下,你可能需要 TCP 提供的流控制。我们刚刚使用的以太网库也提供了 TCP 功能。你可以在arduino.cc/en/Reference/Ethernet找到参考页面。
Server和Client类可以特别用于此目的,实现功能测试,例如检查是否已打开连接,是否仍然有效等。
在本章的结尾,我们将学习如何将我们的 Arduino 连接到互联网上的某个实时服务器。
蓝牙通信
蓝牙是一种无线技术标准。它提供了一种使用 2,400 到 2,480 MHz 频段内的短波无线电传输在短距离内交换数据的方法。
它允许创建具有“正确”安全级别的 PANs(个人区域网络)。它被应用于各种类型的设备上,例如计算机、智能手机、音响系统等,这些设备可以从远程源读取数字音频。
Arduino BT 板原生实现了这项技术。它现在配备了 ATmega328 和 Bluegiga WT11 蓝牙模块。参考页面是http://www.arduino.cc/en/Main/ArduinoBoardBluetooth。
在我看来,在许多项目中,最好的做法是将通用板放在设计的核心,并通过添加外部模块仅添加我们需要的功能。因此,我们将在这里使用 Arduino UNO R3 和一个外部蓝牙模块。
我们将再次使用 Processing 制作一个小项目。你可以在 Processing 画布上点击某个位置,Processing 应用程序将通过蓝牙向 Arduino 发送消息,Arduino 会通过切换其内置 LED 的开或关来做出反应。
连接蓝牙模块
检查以下图示:

RN41 蓝牙模块通过串行链路连接到 Arduino
对应的电路图如下:

将 Roving Networks RN41 模块连接到 Arduino 板
有一个 Roving Networks RN41 蓝牙模块连接到 Arduino 板。
您可以在www.sparkfun.com/products/10559找到它。
这里我们使用 Arduino 本身和蓝牙模块之间的基本串行链路通信。
我们假设我们的计算机具有蓝牙功能,并且这些功能已被激活。
编写固件和 Processing 小程序
固件如下。您可以在Chapter11/Bluetooth中找到它。
// LED stuff
const int ledPin = 13; // pin of the board built-in LED
void setup() {
pinMode(ledPin, OUTPUT); // the led pin is setup as an output
Serial.begin(9600); // start serial communication at 9600bps
}
void loop()
{
if (Serial.available() > 0) {
incomingByte = Serial.read();
if (incomingByte == 1) digitalWrite(ledPin, HIGH);
else if (incomingByte == 0) digitalWrite(ledPin, LOW);
}
}
我们基本上使用蓝牙模块实例化Serial通信,然后检查是否有任何字节从其中可用并解析它们。如果有一个消息可用并且等于 1,我们打开 LED;如果它等于 0,我们关闭 LED。
处理代码如下:
import processing.serial.*;
Serial port;
int bgcolor, fgcolor;
void setup() {
size(700, 700);
background(0);
stroke(255);
bgcolor = 0;
fgcolor = 255;
println(Serial.list());
port = new Serial(this, Serial.list()[2], 9600);
}
void draw() {
background(bgcolor);
stroke(fgcolor);
fill(fgcolor);
rect(100, 100, 500, 500);
}
void mousePressed() {
if (mouseX > 100 && mouseX < 600 && mouseY > 100 && mouseY < 600)
{
bgcolor = 255;
fgcolor = 0;
port.write('1');
}
}
void mouseReleased() {
bgcolor = 0;
fgcolor = 255;
port.write('0');
}
我们首先包含串行库。在setup()函数中,我们定义了一些绘图位,然后我们将串行设备列表打印到 Processing 日志区域。这显示了一个列表,我们必须找到我们计算机的正确蓝牙模块。在我的情况下,这是第三个,我使用这个在setup()函数的最后一条语句中实例化Serial通信:
port = new Serial(this, Serial.list()[2], 9600);
draw()函数只设置:
-
背景颜色根据变量
bgcolor -
轮廓颜色根据变量
fgcolor -
填充颜色根据变量
fgcolor
然后我们画一个正方形。
mousePressed()和mouseReleased()函数是 Processing 回调函数,分别在鼠标事件发生时调用,当你按下鼠标按钮并释放它时。
当鼠标按下时,我们检查按下时的光标位置。在我的情况下,我定义了正方形内的区域。
如果我们按下正方形中的按钮,会出现视觉反馈,以告诉我们已收到命令,但当然最重要的是digitalWrite('1')函数。
我们将值 1 写入蓝牙模块。
同样,当我们释放鼠标按钮时,一个“0”被写入计算机的蓝牙模块。当然,这些消息被发送到 Arduino,后者打开或关闭 LED。
我们刚刚检查了一个外部模块提供无线蓝牙通信功能的 Arduino 的示例。
正如我们所注意到的,我们不需要为此目的使用特定的库,因为模块本身只有当我们向它发送串行数据时,才能自行连接和发送/接收数据。确实,Arduino 和模块之间的通信是一种基本的串行通信。
让我们通过以太网 Wi-Fi 改进我们的空中数据通信。
玩转 Wi-Fi
我们之前学习了如何使用以太网库。然后,我们测试了蓝牙进行短距离网络通信。现在,让我们测试 Wi-Fi 进行中等距离通信,仍然没有任何线缆。
什么是 Wi-Fi?
Wi-Fi 是一套由 IEEE 802.11 标准驱动的无线通信协议。这些标准描述了无线局域网(WLAN)的特性。
基本上,拥有 Wi-Fi 模块的多个主机可以通过它们的 IP 堆栈无线通信。Wi-Fi 使用了多种网络模式。
基础设施模式
在这种模式下,Wi-Fi 主机可以通过接入点相互通信。
这个接入点和主机必须使用相同的服务集标识符(SSID),这是一个用作参考的网络名称。
这种模式很有趣,因为它通过每个主机必须通过接入点才能访问全局网络的事实来提供安全性。我们可以配置一些访问列表来控制哪些主机可以连接,哪些不能。

在基础设施模式下通过接入点交换数据的宿主
临时模式
在这种模式下,每个主机可以直接连接到另一个主机,而不需要接入点。这对于快速连接两个主机以共享文档和交换数据非常有用。

在临时模式下直接连接的两个主机
其他模式
还有两种其他模式。桥接模式是一种连接多个接入点的方式。我们可以想象一个分散在两座建筑中的工作组;我们可以使用两个不同的接入点,并通过桥接模式将它们连接起来。
还有一个名为 范围扩展模式 的简单模式。它用于重复信号,并在两个主机、两个接入点或主机和接入点之间提供连接,当它们距离太远时。
Arduino Wi-Fi 扩展板
这个扩展板为 Arduino 板增加了无线网络功能。官方扩展板还包含一个 SD 卡槽,提供存储功能。它提供:
-
通过 802.11b/g 网络进行连接
-
使用 WEP 或 WPA2 个人加密
-
用于扩展板本身串行调试的 FTDI 连接
-
Mini-USB 用于更新 Wi-Fi 扩展板的固件

Arduino Wi-Fi 扩展板
它基于 HDG104 无线局域网 802.11b/g 系统封装。适当的 Atmega 32 UC3 提供了网络 IP 堆栈。
一个名为 WiFi 库 的专用本地库提供了我们将板子无线连接到任何网络所需的所有功能。参考信息可在arduino.cc/en/Reference/WiFi找到。
这个扩展板可以从许多分销商以及 Arduino 商店购买:store.arduino.cc/ww/index.php?main_page=product_info&cPath=11_5&products_id=237。
让我们尝试将我们的 Arduino 连接到 Wi-Fi 网络。
无加密的基本 Wi-Fi 连接
这里,我们不需要绘制任何原理图。基本上,我们将盾牌连接到 Arduino 并上传我们的代码。我们首先将测试一个不进行加密的基本连接。
接受点必须提供 DHCP 服务器;后者将为我们的基于 Arduino 的系统提供一个 IP 地址。
让我们检查WiFi库提供的示例ConnectNoEncryption。
#include <WiFi.h>
char ssid[] = "yourNetwork"; // the name of your network
int status = WL_IDLE_STATUS; // the Wifi radio's status
void setup() {
//Initialize serial and wait for port to open:
Serial.begin(9600);
// check for the presence of the shield:
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
// don't continue:
while(true)
delay(30) ;
}
// attempt to connect to Wifi network:
while ( status != WL_CONNECTED) {
Serial.print("Attempting to connect to open SSID: ");
Serial.println(ssid);
status = WiFi.begin(ssid);
// wait 10 seconds for connection:
delay(10000);
}
// you're connected now, so print out the data:
Serial.print("You're connected to the network");
printCurrentNet();
printWifiData();
}
void loop() {
// check the network connection once every 10 seconds:
delay(10000);
printCurrentNet();
}
void printWifiData() {
// print your WiFi shield's IP address:
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
Serial.println(ip);
// print your MAC address:
byte mac[6];
WiFi.macAddress(mac);
Serial.print("MAC address: ");
Serial.print(mac[5],HEX);
Serial.print(":");
Serial.print(mac[4],HEX);
Serial.print(":");
Serial.print(mac[3],HEX);
Serial.print(":");
Serial.print(mac[2],HEX);
Serial.print(":");
Serial.print(mac[1],HEX);
Serial.print(":");
Serial.println(mac[0],HEX);
// print your subnet mask:
IPAddress subnet = WiFi.subnetMask();
Serial.print("NetMask: ");
Serial.println(subnet);
// print your gateway address:
IPAddress gateway = WiFi.gatewayIP();
Serial.print("Gateway: ");
Serial.println(gateway);
}
void printCurrentNet() {
// print the SSID of the network you're attached to:
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
// print the MAC address of the router you're attached to:
byte bssid[6];
WiFi.BSSID(bssid);
Serial.print("BSSID: ");
Serial.print(bssid[5],HEX);
Serial.print(":");
Serial.print(bssid[4],HEX);
Serial.print(":");
Serial.print(bssid[3],HEX);
Serial.print(":");
Serial.print(bssid[2],HEX);
Serial.print(":");
Serial.print(bssid[1],HEX);
Serial.print(":");
Serial.println(bssid[0],HEX);
// print the received signal strength:
long rssi = WiFi.RSSI();
Serial.print("signal strength (RSSI):");
Serial.println(rssi);
// print the encryption type:
byte encryption = WiFi.encryptionType();
Serial.print("Encryption Type:");
Serial.println(encryption,HEX);
}
首先,我们包含WiFi库。然后,我们设置我们网络的名称,即 SSID。请务必将其更改为您自己的 SSID。
在setup()函数中,我们实例化Serial连接。然后,我们通过调用函数WiFi.status()来检查盾牌的存在。
如果后者返回的值是WL_NO_SHIELD(这是在 WiFi 库内部定义的一个常量),这意味着没有盾牌。在这种情况下,将执行一个无限循环,使用while(true)语句而没有break关键字。
如果它返回的值不同于WL_CONNECTED,那么我们将打印一条语句来通知它正在尝试连接。然后,WiFi.begin()尝试连接。这是一个常见的结构,提供了一种在不连接时不断尝试连接的方法,并且每 10 秒调用一次delay()函数。
然后,如果连接成功,状态变为WL_CONNECTED,我们退出while循环并继续。
同时也会在串行中打印一些信息,表示板子已经达到连接状态。
我们还调用了两个函数。这些函数会打印与网络参数和状态相关的许多元素。我将让您通过之前引用的arduino.cc/en/Reference/WiFi参考来发现每个函数。
在此连接之后,我们可以开始交换数据。正如您可能知道的,使用 Wi-Fi(尤其是没有安全措施的情况下)可能会导致问题。事实上,从未受保护的 Wi-Fi 网络捕获数据包非常容易。
让我们使用更安全的WiFi库。
使用 WEP 或 WPA2 的 Arduino Wi-Fi 连接
如果您打开ConnectWithWEP和ConnectWithWPA这两个代码,与前面的例子相比只有一些细微的差别。
使用 WiFi 库中的 WEP
如果我们使用 40 位 WEP,我们需要一个包含 10 个字符的密钥,这些字符必须是十六进制的。如果我们使用 128 位 WEP,我们需要一个包含 26 个字符的密钥,这些字符也必须是十六进制的。这个密钥必须在代码中指定。
我们用两个与 WEP 加密相关的新参数替换了只带一个参数的WiFi.begin()调用。这是唯一的区别。
由于许多我们在这里不会讨论的原因,WEP 在安全性方面被认为太弱,因此大多数人和组织已经转向更安全的 WPA2 替代方案。
使用 WiFi 库中的 WPA2
按照相同的方案,这里我们只需要一个密码。然后,我们用两个参数调用WiFi.begin():SSID 和密码。
在我们检查的两种情况下,我们只需要在WiFi.begin()中传递一些额外的参数,以便使事情更加安全。
Arduino 有一个(轻量级)网络服务器
在这里,我们使用库中提供的WifiWebServer代码。
在这个例子中,Arduino 在连接到 WEP 或 WPA Wi-Fi 网络后充当一个网络服务器。
#include <WiFi.h>
char ssid[] = "yourNetwork"; // your network SSID (name)
char pass[] = "secretPassword"; // your network password
int keyIndex = 0; // your network key Index number (needed only for WEP)
int status = WL_IDLE_STATUS;
WiFiServer server(80);
void setup() {
//Initialize serial and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for Leonardo only
}
// check for the presence of the shield:
if (WiFi.status() == WL_NO_SHIELD) {
Serial.println("WiFi shield not present");
// don't continue:
while(true);
}
// attempt to connect to Wifi network:
while ( status != WL_CONNECTED) {
Serial.print("Attempting to connect to SSID: ");
Serial.println(ssid);
// Connect to WPA/WPA2 network. Change this line if using open or WEP network:
status = WiFi.begin(ssid, pass);
// wait 10 seconds for connection:
delay(10000);
}
server.begin();
// you're connected now, so print out the status:
printWifiStatus();
}
void loop() {
// listen for incoming clients
WiFiClient client = server.available();
if (client) {
Serial.println("new client");
// an http request ends with a blank line
boolean currentLineIsBlank = true;
while (client.connected()) {
if (client.available()) {
char c = client.read();
Serial.write(c);
// if you've gotten to the end of the line (received a newline
// character) and the line is blank, the http request has ended,
// so you can send a reply
if (c == '\n' && currentLineIsBlank) {
// send a standard http response header
client.println("HTTP/1.1 200 OK");
client.println("Content-Type: text/html");
client.println("Connnection: close");
client.println();
client.println("<!DOCTYPE HTML>");
client.println("<html>");
// add a meta refresh tag, so the browser pulls again every 5 seconds:
client.println("<meta http-equiv=\"refresh\" content=\"5\">");
// output the value of each analog input pin
for (int analogChannel = 0; analogChannel < 6; analogChannel++) {
int sensorReading = analogRead(analogChannel);
client.print("analog input ");
client.print(analogChannel);
client.print(" is ");
client.print(sensorReading);
client.println("<br />");
}
client.println("</html>");
break;
}
if (c == '\n') {
// you're starting a new line
currentLineIsBlank = true;
}
else if (c != '\r') {
// you've gotten a character on the current line
currentLineIsBlank = false;
}
}
}
// give the web browser time to receive the data
delay(1);
// close the connection:
client.stop();
Serial.println("client disonnected");
}
}
void printWifiStatus() {
// print the SSID of the network you're attached to:
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
// print your WiFi shield's IP address:
IPAddress ip = WiFi.localIP();
Serial.print("IP Address: ");
Serial.println(ip);
// print the received signal strength:
long rssi = WiFi.RSSI();
Serial.print("signal strength (RSSI):");
Serial.print(rssi);
Serial.println(" dBm");
}
让我们解释这些语句背后的概念。
我们只解释代码的新部分,而不是自动连接和加密语句,因为我们之前已经做过这些。
WiFiServer server(80)语句在特定端口上实例化一个服务器。在这里,选择的 TCP 端口是 80,这是标准的 HTTP 服务器 TCP 端口。
在setup()函数中,我们自动将 Arduino 连接到 Wi-Fi 网络,然后启动服务器。基本上,它会在 TCP 端口 80 上打开一个套接字并开始监听该端口。
在loop()函数中,我们检查是否有客户端连接到 Arduino 上嵌入的网络服务器。这是通过WiFiClient client = server.available();来完成的。
然后,我们对客户端实例有一个条件。如果没有客户端,我们基本上什么都不做,并再次执行循环,直到我们有客户端。
一旦我们有了连接,我们就将其打印到串行端口以提供反馈。然后,我们检查客户端是否真正连接,以及读取缓冲区中是否有数据。如果有数据,我们就将其打印出来,并通过发送标准的 HTTP 响应头来回答客户端。这基本上是通过将字节打印到客户端实例本身来完成的。
代码包括一些动态特性,并发送一些从板上读取的值,如来自每个模拟输入的 ADC 值。
我们可以尝试连接一些传感器,并通过 Arduino 直接处理的一个网页直接提供它们的值。我会让你检查代码的其他部分。这部分处理标准的 HTTP 消息。
通过按开关来发推文
将 Arduino 连接到网络显然让人联想到互联网。我们可以尝试创建一个可以发送互联网消息的小系统。我选择使用微博服务 Twitter,因为它提供了一个很好的通信 API。
我们将使用与“将 Arduino 连接到以太网”部分相同的电路,但在这里我们使用的是与一些内存约束相关的 Arduino MEGA,板子更小。
API 概述
API代表应用程序 编程 接口。基本上,它定义了与考虑的系统交换数据的方式。我们可以在我们的系统中定义 API,以便它们可以与其他系统通信。
例如,我们可以在我们的 Arduino 固件中定义一个 API,说明如何以及发送什么数据来使板上的 LED 开关。我们不会描述整个固件,但我们会向世界提供一个基本文档,精确地说明从互联网发送的格式和数据,例如,用于远程使用。那将是一个 API。
Twitter 的 API
Twitter,就像互联网上许多其他与社交网络相关的系统一样,提供了一个 API。其他程序员可以使用它来获取数据,也可以发送数据。与 Twitter API 相关的所有数据规范都可以在dev.twitter.com找到。
为了使用 API,我们必须在 Twitter 开发者网站上创建一个应用程序。有一些特殊的设置安全参数,我们必须同意一些使用规则,这些规则尊重数据请求速率和其他技术规范。
我们可以通过访问dev.twitter.com/apps/new来创建一个应用程序。
这将为我们提供一些凭证信息,特别是访问令牌和令牌密钥。这些是必须按照某些协议使用才能访问 API 的字符字符串。
使用具有 OAuth 支持的 Twitter 库
马克库·罗西创建了一个非常强大且可靠的库,它嵌入 OAuth 支持,并旨在直接从 Arduino 发送推文。官方库网站是www.markkurossi.com/ArduinoTwitter。
这个库需要与具有比通常更多内存的板子一起使用。Arduino MEGA 可以完美运行它。
OAuth 是一种开放协议,允许以简单和标准的方法从 Web、移动和桌面应用程序进行安全授权。这定义在oauth.net。
基本上,这是一种使第三方应用程序能够获得对 HTTP 服务的有限访问的方法。通过发送一些特定的字符字符串,我们可以授予对主机的访问权限,并使其与 API 通信。
这就是我们将要一起作为一个很好的示例来做的,你可以将其用于 Web 上的其他 API。
从 Twitter 获取凭证
马克库的库实现了 OAuth 请求签名,但没有实现 OAuth 访问令牌检索流程。我们可以通过使用我们在创建应用程序的 Twitter 网站上提供的此指南来检索我们的令牌:dev.twitter.com/docs/auth/tokens-devtwittercom。
你需要随身携带访问令牌和访问令牌密钥,因为我们将它们包含在我们的固件中。
编写连接到 Twitter 的固件
马克库的库易于使用。以下是将 Arduino 连接到你的以太网网络以便直接发送推文的可能代码。
你可以在Chapter11/tweetingButton/中找到它。
#include <SPI.h>
#include <Ethernet.h>
#include <sha1.h>
#include <Time.h>
#include <EEPROM.h>
#include <Twitter.h>
// Switch
const int switchPin = 2;
int switchState = 0;
int lastSwitchState = LOW;
long lastDebounceTime = 0;
long debounceDelay = 50;
// Local network configuration
uint8_t mac[6] = {
0xc4, 0x2c, 0x03, 0x0a, 0x3b, 0xb5}; // USE YOUR MAC ADDRESS
IPAddress ip(192, 168, 1, 43); // USE IP ON YOUR NETWORK
IPAddress gateway(192, 168, 1, 1); // USE YOUR GATWEWAY IP ADDRESS
IPAddress subnet(255, 255, 255, 0); // USE YOUR SUBNET MASK
// IP address to Twitter
IPAddress twitter_ip(199, 59, 149, 232);
uint16_t twitter_port = 80;
unsigned long last_tweet = 0;
#define TWEET_DELTA (60L * 60L)
// Store the credentials
const static char consumer_key[] PROGMEM = "xxxxxxxxxxxxx";
const static char consumer_secret[] PROGMEM
= "yyyyyyyyyyyyy";
#DEFINE ALREADY_TOKENS 0 ; // Change it at 1 when you put your tokens
char buffer[512];
Twitter twitter(buffer, sizeof(buffer));
void setup() {
Serial.begin(9600);
Serial.println("Arduino Twitter demo");
// the switch pin is setup as an input
pinMode(switchPin, INPUT);
// start the network connection
Ethernet.begin(mac, ip, dns, gateway, subnet);
// define twitter entry point
twitter.set_twitter_endpoint(PSTR("api.twitter.com"),
PSTR("/1/statuses/update.json"),
twitter_ip, twitter_port, false);
twitter.set_client_id(consumer_key, consumer_secret);
// Store or read credentials in EEPROM part of the board
#if ALREADY_TOKENS
/* Read OAuth account identification from EEPROM. */
twitter.set_account_id(256, 384);
#else
/* Set OAuth account identification from program memory. */
twitter.set_account_id(PSTR("*** set account access token here ***"),
PSTR("*** set account token secret here ***"));
#endif
delay(500);
}
void loop() {
if (twitter.is_ready()) // if the twitter connection is okay
{
unsigned long now = twitter.get_time();
if (last_tweet == 0) last_tweet = now - TWEET_DELTA + 15L;
// read the state of the digital pin
int readInput = digitalRead(switchPin);
if (readInput != lastSwitchState)
{
lastDebounceTime = millis();
}
if ( (millis() - lastDebounceTime) > debounceDelay )
{
switchState = readInput;
}
lastSwitchState = readInput;
if (switchState == HIGH) // if you push the button
{
if (now > last_tweet + TWEET_DELTA) // if you didn't tweet for a while
{
char msg[32];
sprintf(msg, "Tweeting from #arduino by pushing a button is cool, thanks to @julienbayle");
// feedback to serial monitor
Serial.print("Posting to Twitter: ");
Serial.println(msg);
last_tweet = now;
if (twitter.post_status(msg))
Serial.println("Status updated");
else
Serial.println("Update failed");
}
else Serial.println("Wait a bit before pushing it again!");
}
}
delay(5000); // waiting a bit, just in case
}
让我们在这里解释一下。请注意,这是一个包含我们已共同发现和学习的许多内容的代码:
-
带有去抖动系统的按钮按下
-
使用 Arduino 以太网盾片的以太网连接
-
Twitter 库示例
我们首先包含大量的库头文件:
-
用于网络连接的 SPI 和以太网
-
Sha1 用于凭证加密
-
Twitter 库中用于时间和日期特定功能的时间
-
使用 EEPROM 在板子的 EEPROM 中存储凭证
-
Twitter 库本身
然后,我们包括与按钮本身和防抖系统相关的变量。
我们配置网络参数。请注意,您必须根据您的网络和以太网屏蔽器在此处放置自己的元素。然后,我们定义 Twitter 的 IP 地址。
我们定义 TWEET_DELTA 常量以供以后使用,考虑到 Twitter API 使用禁止我们一次性发送过多推文。然后,我们存储我们的凭据。请使用与您在 Twitter 网站上创建的应用程序相关的凭据。最后,我们创建对象 twitter。
在 setup() 函数中,我们启动 Serial 连接以便向我们发送一些反馈。我们配置开关的数字引脚并启动以太网连接。然后,我们有了关于 Twitter 的所有魔法。我们首先选择由 Twitter API 文档本身定义的入口点。我们还需要在这里放置我们的访问令牌和令牌密钥。然后,我们有一个编译条件:#if TOKEN_IN_MEMORY。
TOKEN_IN_MEMORY 之前定义为 0 或 1。根据其值,编译以某种方式或另一种方式进行。
为了将凭据存储到板的 EEPROM 中,我们首先必须将值设置为 0。我们编译它并在板上运行。固件运行并将令牌写入内存。然后,我们将值更改为 1(因为令牌现在在内存中),我们编译它并在板上运行。从现在起,固件将读取 EEPROM 中的凭据。
然后,考虑到我们之前学到的内容,loop() 函数相当简单。
我们首先测试与 API 的 Twitter 连接是否正常。如果一切正常,我们将时间和最后一条推文的最后时间存储在一个初始值中。我们读取数字输入的防抖值。
如果我们按下按钮,我们会测试是否在 TWEET_DELTA 时间内完成。如果是这样,我们就符合 Twitter API 规则,可以发推文。
最后,我们在字符数组 msg 中存储一条消息。我们通过使用 twitter.post_status() 函数来发推文。在使用它时,我们还测试它返回的内容。如果它返回 1,这意味着推文已成功。通过串行监视器向用户提供此信息。
所有 API 提供商都以相同的方式工作。在这里,我们得到了我们使用的 Twitter 库的很大帮助,但还有其他库也适用于互联网上的其他服务。每个服务都提供了使用其 API 的完整文档。Facebook API 资源在此处可用:developers.facebook.com/。Google+ API 资源在此处可用:developers.google.com/+/api/。Instagram API 资源在此处可用:instagram.com/developer。我们还可以找到很多其他资源。
摘要
在本章中,我们学习了如何扩展我们的 Arduino 板通信范围。我们以前习惯于进行非常局部的连接;现在我们能够将我们的板连接到互联网,并且有可能与整个地球进行通信。
我们描述了有线以太网、Wi-Fi、蓝牙连接,以及如何使用 Twitter 的 API。
我们本可以描述使用无线电频率的 Xbee 板,但我更倾向于描述与 IP 相关的内容,因为我认为这是传输数据最安全的方式。当然,Xbee 的屏蔽解决方案也是一个非常好的选择,我自己在许多项目中都使用过它。
在下一章中,我们将描述并深入研究 Max 6 框架。这是一个非常强大的编程工具,可以生成和解析数据,我们将解释如何将其与 Arduino 结合使用。
第十二章. 玩转 Max 6 框架
本章将教会我们一些可以在 Max 6 图形编程框架和 Arduino 板上使用的技巧和技术。
我们在第六章中介绍了这个令人惊叹的框架,当我们学习 Arduino 模拟输入处理时。阅读上一章是更好地理解和学习本章中开发的技术的要求。我甚至建议你再次阅读 Max 6 简介部分。
在本章中,我们将学习如何从 Max 6 向 Arduino 发送数据。我们还将描述如何处理和解析从 Arduino 接收到的数据。
Arduino 为你的 Max 6 程序添加了许多功能。实际上,它提供了一种将 Max 6 插入真实物理世界的方法。通过两个示例,我们将了解一种与 Arduino、计算机和最先进的编程框架一起工作的好方法。
让我们开始吧。
与 Max 6 轻松通信 – [serial]对象
正如我们在第六章中已经讨论过的,感知世界 – 使用模拟输入感受,在运行 Max 6 补丁的计算机和 Arduino 板之间交换数据的最简单方法是使用串行端口。我们的 Arduino 板上的 USB 连接器包括 FTDI 集成电路 EEPROM FT-232,它将 RS-232 标准串行转换为 USB。
我们将再次使用 Arduino 和我们的计算机之间的基本 USB 连接来在此处交换数据。
[serial]对象
我们必须记住[serial]对象的特点。它提供了一种从串行端口发送和接收数据的方式。为此,有一个包括基本模块的基本补丁。我们将在这个子章节中逐步改进它。
[serial]对象就像一个我们需要频繁轮询的缓冲区。如果从 Arduino 向计算机的串行端口发送消息,我们必须要求[serial]对象将它们弹出。我们将在接下来的页面中这样做。
![The [serial] object](https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_12_001.jpg)
你可以在Chapter12文件夹中找到它;补丁文件名为serialObject.maxpat。
当然,本章也是我向您提供一些我在 Max 6 本身的一些技巧和窍门的序言。接受并使用它们;它们会让你的补丁生活变得更轻松。
选择正确的串行端口
在第六章中,感知世界 – 使用模拟输入感受,我们使用发送到[serial]的消息(print)来列出计算机上可用的所有串行端口。然后我们检查了 Max 窗口。这并不是最聪明的解决方案。在这里,我们将设计一个更好的方案。
我们必须记住 [loadbang] 对象。它在补丁加载时触发一个撞击,即一个 (print) 消息发送到后续对象。这有助于设置和初始化一些值,就像我们在 Arduino 板的固件中的 setup() 块内做的那样。
这里,我们这样做是为了填充串行端口选择菜单。当 [serial] 对象接收到 (print) 消息时,它会从其右侧出口弹出计算机上所有可用的串行端口列表,并在前面加上单词 port。然后我们通过使用 [route port] 处理结果,该对象只解析以单词 port 开头的列表。
[t] 对象是 [trigger] 的缩写。根据文档所述,如果假设使用以下参数:
-
b表示撞击 -
f表示浮点数 -
i表示整数 -
s表示符号 -
l表示列表(即至少有一个元素)
我们也可以使用常量作为参数,一旦接收到输入,常量就会以原样发送。
最后,以特定顺序输出 [trigger] 的消息:从最右侧出口到最左侧出口。
因此,我们取从 [route] 对象接收到的串行端口列表;我们将 clear 消息发送到 [umenu] 对象(左侧的列表菜单),以清除整个列表。然后,将串行端口列表作为列表(由于第一个参数)发送到 [iter]。[iter] 将列表分割成其各个元素。
[prepend] 在传入的输入消息前添加一个消息。
这意味着全局过程将消息发送到 [umenu] 对象,类似于以下内容:
-
添加
xxxxxx -
添加
yyyyyy
这里 xxxxxx 和 yyyyyy 是可用的串行端口。
这通过填充串行端口的名称来创建串行端口选择菜单。这是在补丁中使用 UI 元素创建一些辅助工具(在这种情况下是菜单)的典型方法之一。
一旦加载此补丁,菜单就会填充,你只需选择你想要使用的正确串行端口。一旦在菜单中选择了一个元素,列表中该元素的编号就会触发到其最左侧出口。我们在这个数字前加上 port 并将其发送到 [serial],将其设置为右侧的串行端口。
轮询系统
Max 6 中最常使用的对象之一 [metro] 用于发送定期的撞击以触发事物或计时。
我们至少需要使用一个参数;这是两个撞击之间的时间间隔(以毫秒为单位)。
撞击 [serial] 对象使其弹出其缓冲区中的值。
如果我们想从 Arduino 连续发送数据并使用 Max 6 处理它们,则需要激活 [metro] 对象。然后我们发送一个定期的撞击,并可以在 Max 6 补丁中更新所有由 Arduino 读取的输入。
选择 15 ms 到 150 ms 之间的值是好的,但取决于你的需求。
让我们看看我们如何读取、解析和选择从 Arduino 接收的有用数据。
解析和选择来自 Arduino 的数据
首先,我想向您介绍一个辅助固件,它灵感来源于 Arduino 网站上的 Arduino2Max 页面,但经过我的更新和优化。它提供了一种读取 Arduino 上所有输入的方法,打包读取的所有数据,并通过 [serial] 对象将它们发送到我们的 Max 6 补丁。
ReadAll 固件
以下代码是固件。您可以在 Chapter12/ReadAll 中找到它:
int val = 0;
void setup()
{
Serial.begin(9600);
pinMode(13,INPUT);
}
void loop()
{
// Check serial buffer for characters incoming
if (Serial.available() > 0){
// If an 'r' is received then read all the pins
if (Serial.read() == 'r') {
// Read and send analog pins 0-5 values
for (int pin= 0; pin<=5; pin++){
val = analogRead(pin);
sendValue (val);
}
// Read and send digital pins 2-13 values
for (int pin= 2; pin<=13; pin++){
val = digitalRead(pin);
sendValue (val);
}
Serial.println();// Carriage return to mark end of data flow.
delay (5); // prevent buffer overload
}
}
}
void sendValue (int val){
Serial.print(val);
Serial.write(32); // add a space character after each value sent
}
首先,我们在 setup() 块中以 9600 波特率开始串行通信。
与处理串行通信一样,我们首先使用 Serial.available() 函数检查 Arduino 的串行缓冲区中是否有内容。如果有内容,我们检查它是否是字符 r。当然,我们可以使用任何其他字符。这里的 r 代表读取,这是基本的。如果收到一个 r,它将触发读取模拟和数字端口。每个值(val 变量)都传递给 sendValue() 函数;这基本上将值打印到串行端口,并添加一个空格字符以提供一些格式化,以便 Max 6 更容易解析。我们可以轻松地修改此代码以只读取一些输入而不是所有输入。我们还可以删除 sendValue() 函数并找到另一种打包数据的方法。
最后,我们使用 Serial.println() 将回车符推送到串行端口。这会在发送的每个数据包之间创建分隔符。
现在,让我们改进我们的 Max 6 补丁,以处理从 Arduino 接收到的数据包。
ReadAll Max 6 补丁
以下截图是 ReadAll Max 补丁,它提供了一种与我们的 Arduino 通信的方法:

您可以在 Chapter12 文件夹中找到此补丁。它命名为 ReadAll.maxpat。
我们在之前的补丁中添加了一些基本构建块。
从 Arduino 请求数据
首先,我们将看到一个 [t b b] 对象。它也是一个触发器,由 [metro] 对象提供的 bang 信号。每个接收到的 bang 信号都会触发另一个 bang 信号发送到另一个 [trigger] 对象,然后发送到 [serial] 对象本身。
[t 13 r] 对象可能看起来有些复杂。它只是触发一个字符 r 然后整数 13。字符 r 被发送到 [spell],它将其转换为 ASCII 码,然后将结果发送到 [serial]。13 是回车符的 ASCII 码。
这种结构提供了一种将字符 r 发送到 [serial] 对象的方法,这意味着每次 metro 发出信号时,都会发送给 Arduino。正如我们在固件中已经看到的,它触发 Arduino 读取所有输入,然后打包数据,最后将数据包发送到 Max 6 补丁的串行端口。
为了总结 metro 在每个 bang 信号时触发的操作,我们可以写出以下序列:
-
将字符
r发送到 Arduino。 -
向 Arduino 发送回车符。
-
触发
[serial]对象。
这会触发 Arduino 将所有数据发送回 Max 补丁。
解析接收到的数据
在 [serial] 对象下,我们可以看到一个以 [sel 10 13] 对象开始的新结构。这是 [select] 对象的缩写。此对象选择一个传入的消息,并在消息等于对应输出位置的参数时向特定输出发送一个 bang 信号。基本上,这里我们选择了 10 或 13。最后一个输出如果传入的消息不等于任何参数,就会弹出该消息。
在这里,我们不希望考虑新的换行符(ASCII 代码 10)。这就是为什么我们将其作为参数,但如果选中的是这个,我们就不做任何事情。这是一个很好的技巧,可以避免这个消息触发任何事情,甚至从 [select] 的右侧输出中消除它。
在这里,我们将从 Arduino 收到的所有消息(除了 10 或 13)发送到 [zl group 78] 对象。后者是一个强大的列表,用于处理许多功能。group 参数使得将接收到的消息分组到列表中变得容易。最后一个参数是为了确保列表中元素的数量不会太多。一旦 [zl group] 被一个 bang 信号或列表长度达到长度参数值触发,它就会从其左侧出口弹出整个列表。
在这里,我们“累积”从 Arduino 收到的所有消息,一旦发送了回车符(记住我们在固件 loop() 块的最后几行中做这件事),就会发送一个 bang 信号,并将所有数据传递到下一个对象。
我们目前有一个包含所有数据的列表,每个值之间由一个空格字符(我们在固件中添加的著名的 ASCII 代码 32)分隔。
这个列表被传递到 [itoa] 对象。itoa 代表 整数到 ASCII。此对象将整数转换为 ASCII 字符。
[fromsymbol] 对象将符号转换为消息列表。
最后,在这个 [fromsymbol] 对象之后,我们得到了一个由空格分隔的、完全可读的大值列表。
然后,我们必须解包列表。[unpack] 是一个非常有用的对象,它提供了一种将消息列表切割成单个消息的方法。我们可以注意到,在这里,我们在 Arduino 固件中实现了与打包每个值到一个大消息相反的过程。
[unpack] 可以接受我们想要的任意数量的参数。它需要知道发送给它的列表中确切元素的数量。这里我们发送了来自 Arduino 的 12 个值,所以我们放置了 12 个 i 参数。i 代表 整数。如果我们发送一个浮点数,[unpack] 会将其转换为整数。了解这一点很重要。太多学生在这个问题上卡住了,特别是当他们在调试时。
我们在这里只处理整数。实际上,Arduino 的 ADC 提供从 0 到 1023 的数据,而数字输入只提供 0 或 1。
我们将数字框连接到 [unpack] 对象的每个输出,以便显示每个值。
然后,我们使用了 [change] 对象。这个对象很棒。当它接收到一个值时,只有当它与之前接收到的值不同时,它才会将其传递到其输出。它提供了一种有效的方法,以避免在不需要时每次都发送相同的值。
在这里,我选择了参数 -1,因为这不是 Arduino 固件发送的值,并且我确信发送的第一个元素将被解析。
因此,我们现在可以访问所有我们的值。我们可以用它们来完成不同的任务。
但是,我建议使用一种更智能的方法,这也会引入一个新概念。
分发接收到的数据和其它技巧
让我们在这里介绍一些其他技巧来改进我们的补丁风格。
无线技巧
我们经常需要在我们的补丁中使用一些数据。相同的数据需要为多个对象提供。
避免到处都是电线和电缆的杂乱无章的好方法是用 [send] 和 [receive] 对象。这些对象可以用 [s] 和 [r] 简写,并且它们生成通信总线,为我们提供在补丁内部无线通信的方式。

这三个结构是等价的。
第一个是基本的线缆。一旦我们从上面的数字框发送数据,它就会传输到线缆另一端。
第二个生成一个名为 busA 的数据总线。一旦你将数据发送到 [send busA],你的补丁中每个 [receive busA] 对象都会弹出该数据。
第三个例子与第二个例子相同,但它生成另一个名为 busB 的总线。
这是一种分发数据的好方法。
我经常用它来作为我的主时钟,例如。我有一个且仅有一个主时钟在 [send masterClock] 上敲击时钟,无论我需要在哪里有那个时钟,我都使用 [receive masterClock],它为我提供所需的数据。
如果你检查全局补丁,你可以看到我们将数据分发到补丁底部的结构。但这些结构也可以位于其他地方。实际上,任何视觉编程框架(如 Max 6)的一个优势是,你可以在补丁器中直观地组织代码的每一部分,就像你想要的那样。请尽可能这样做。这将帮助你支持并维护你的补丁,在整个开发过程中。

检查前面的截图。我本可以将左上角的 [r A1] 对象直接链接到 [p process03] 对象。但也许如果我将处理链保持分离,这将更容易阅读。我经常以这种方式使用 Max 6。
这是我 Max 6 课程中教授的多个技巧之一。当然,我也介绍了 [p] 对象,它是 [patcher] 的缩写。
在我们继续一些涉及 Max 6 和 Arduino 的好例子之前,让我们检查一些小贴士。
封装和子补丁
当你打开 Max 6 并转到 文件 | 新建补丁器 时,它会打开一个空白补丁器。如果你还记得,那里是你放置所有对象的地方。还有一个名为 子补丁 的优秀功能。使用这个功能,你可以在补丁器内部创建新的补丁器,也可以在补丁器内部嵌入补丁器。
包含在其他补丁器内部的补丁器也被称为子补丁器。
让我们看看名为 ReadAllCutest.maxpat 的补丁是如何工作的。

有四个新的对象取代了我们之前设计的整个结构。
这些对象是子补丁器。如果你在 补丁锁定模式 中双击它们,或者如果你按住 命令 键(或 Windows 中的 Ctrl),在 补丁编辑模式 中双击它们,你就可以打开它们。让我们看看里面有什么。

[requester] 子补丁器包含我们之前设计的相同架构,但你可以看到棕色 1 和 2 对象以及另一个蓝色 1 对象。这些是输入口和输出口。实际上,如果你想让你的子补丁器能够与包含它的补丁器通信,这些是必需的。当然,我们也可以使用 [send] 和 [receive] 对象来达到这个目的。我们将在接下来的页面中看到这一点。
这些输入口和输出口在你子补丁器中的位置很重要。实际上,如果你将 1 对象移动到 2 对象的右边,数字就会交换!上补丁中的不同输入口也会交换。你必须小心这一点。但再次强调,你可以按照你想要的和需要的组织它们。
检查下一张截图:

现在,检查包含此子补丁器的根补丁器。它会自动反转输入口,保持相关。

现在我们来看看其他子补丁器:

[p portHandler] 子补丁器

[p dataHandler] 子补丁器

[p dataDispatcher] 子补丁器
在最后一张图中,我们可以看到一个输入口而没有输出口。实际上,我们只是在子补丁器内部封装了全局数据分发系统。这个系统使用 [send] 对象生成其数据总线。这是一个我们不需要甚至不想使用输出口的例子。使用输出口会变得混乱,因为我们不得不使用很多电线将每个请求这个或那个值的 Arduino 元素连接起来。
为了创建一个子补丁器,你只需输入 n 来创建一个新的对象,然后输入 p,一个空格,以及你的子补丁器名称。
当我设计这些示例时,我使用了一些比创建子补丁器、复制粘贴内部结构、从外部移除结构并添加输入口和输出口更快的方法。
此功能名为封装,是 Max 6 的 编辑 菜单的一部分。
您必须选择要封装在子模块内的模块部分,然后点击 封装,哇!您已经创建了一个包含与输入和输出正确连接的结构子模块。

封装和解封装功能
您还可以解封装一个子模块。这将遵循与移除子模块和直接弹出内部整个结构相反的过程。
子模块化有助于保持事物井然有序且易于阅读。
我们可以想象,我们必须设计一个包含许多魔法和技巧的整个模块。这个是一个处理单元,一旦我们知道它做什么,完成之后,我们不想知道它是如何做到的,只想 使用它。
这通过将一些处理单元保持在盒子内并不过度混乱主模块,提供了一个很好的抽象级别。
您可以复制和粘贴子模块。如果您需要快速复制处理单元,这是一个强大的方法。但每个子模块与其他子模块完全独立。这意味着,如果您需要修改一个以更新它,您必须在每个模块的子模块中单独进行。
这可能真的很难。
在我进一步介绍 Arduino 之前,让我先向您介绍最后一个纯 Max 6 概念,现在命名为抽象。
抽象和可重用性
任何创建并保存的模块都可以用作另一个模块中的新对象。我们可以通过在模块中键入 n 来创建一个新对象;然后我们只需键入之前创建并保存的模块的名称。
以这种方式使用的模块称为 抽象。
为了在模块中调用模块作为抽象,该模块必须位于 Max 6 的 路径 中,以便被找到。您可以通过转到 选项 | 文件首选项 来检查 Max 6 所知的路径。通常,如果您将主模块放在一个文件夹中,并将您想要用作抽象的其他模块放在同一个文件夹中,Max 6 就能找到它们。
Max 6 自身的抽象概念非常强大,因为它提供了 可重用性。
事实上,想象一下,您需要并且每天都在使用很多(或很大)的模块结构,几乎在每个项目中都会用到。您可以将它们放入您的磁盘上的一个特定文件夹中,该文件夹包含在您的 Max 6 路径中,然后您可以在您设计的每个模块中调用(我们称之为 实例化)它们。
由于每个使用它的模块只引用了实例化的那个模块,因此您只需改进您的抽象;每次您加载使用它的模块时,该模块将包含最新的抽象。
这在整个开发月份或年份中维护起来非常容易。
当然,如果您完全改变抽象以适应一个专用项目/补丁,您在使用其他补丁时可能会遇到一些问题。您必须小心,即使是非常简短的文档也要保持您的抽象。
让我们继续描述一些与 Arduino 相关的良好示例。
使用 LED 创建声音水平计
这个小型项目是 Max 6/Arduino 硬件和软件协作的典型例子。
Max 可以轻松监听声音并将它们从模拟域转换为数字域。
我们将使用 Arduino、一些 LED 和 Max 6 构建一个小型声音水平可视化器。
电路
我们将使用我们在第八章中设计的相同电路,设计视觉输出反馈,同时我们使用 595 类型移位寄存器的菊花链来多路复用 LED。
下图显示了电路:

我们的八 LED 双串

我们的八 LED 双串
基本想法是:
-
使用每个声音通道(左和右)的每串八个 LED
-
在 LED 串中显示声音水平
对于每个通道,开着的 LED 数量越多,声音水平就越高。
让我们先看看如何在 Max 6 中处理这个问题。
计算声音水平的 Max 6 补丁
看看下面的图,显示了 SoundLevelMeters 补丁:

生成声音和测量声音水平
我们在这里使用 Max 6 框架的 MSP 部分,这部分与声音信号相关。在补丁中,我们有两个源(命名为 source 1 和 source 2)。每个源生成两个信号。我将每个源连接到一个 [selector~ ] 对象。
后者是用于信号的开关。左上角的源选择器提供了一个在 source 1 和 source 2 之间切换的方法。
我不会描述声音源的低成本魔法;这将涉及对合成技术的了解,而这超出了本主题的范围。
然后,我们有一个连接到每个 [selector~ ] 输出和一个小符号,比如一个扬声器的连接。这与您音频接口的声音输出有关。
我还使用了 [meter~] 对象来显示每个通道的级别。
最后,我添加了一个 [flonum] 对象来显示每次的级别当前值。
这些是我们将要发送给 Arduino 的数字。
让我们添加我们之前描述的串行通信构建块。

向 Arduino 发送数据
我们已经准备好了串行通信设置。
我们还有 [zmap 0\. 1\. 0 255] 对象。这些对象接受一个值,该值旨在在 0\. 1 之间,正如在参数中设置的那样,并将其缩放到 0 255 的范围。这为每个通道提供了一个字节数据。
我们使用两个数据总线将每个通道的值发送到 [pak] 对象。后者收集传入的消息并创建一个包含它们的列表。[pak] 与 [pack] 的区别在于 [pak] 在其任一输入接收到消息时就会发送数据,而不仅仅是当它接收到其左侧输入的消息时,就像 [pack] 一样。
因此,我们有了当电平值改变时,从计算机弹出到 Arduino 的消息列表。
用于读取字节的固件
让我们看看如何在 Arduino 中处理这个问题:
#include <ShiftOutX.h>
#include <ShiftPinNo.h>
int CLOCK_595 = 4; // first 595 clock pin connecting to pin 4
int LATCH_595 = 3; // first 595 latch pin connecting to pin 3
int DATA_595 = 2; // first 595 serial data input pin connecting to pin 2
int SR_Number = 2; // number of shift registers in the chain
// instantiate and enabling the shiftOutX library with our circuit parameters
shiftOutX regGroupOne(LATCH_595, DATA_595, CLOCK_595, MSBFIRST, SR_Number);
// random groove machine variables
int counter = 0;
byte LeftChannel = B00000000 ; // store left channel Leds infos
byte RightChannel = B00000000 ; // store right channel Leds infos
void setup() {
// NO MORE setup for each digital pin of the Arduino
// EVERYTHING is made by the library :-)
}
void loop(){
if (Serial.available() > 0) {
LeftChannel = (byte)Serial.parseInt();
RightChannel = (byte)Serial.parseInt();
unsigned short int data; // declaring the data container as a very local variable
data = ( LeftChannel << 8 ) | RightChannel; // aggregating the 2 read bytes
shiftOut_16(DATA_595, CLOCK_595, MSBFIRST, data); // pushing the whole data to SRs
// make a short pause before changing LEDs states
delay(2);
}
}
这与第八章中的固件相同,设计视觉输出反馈,但在这里我们正在读取真实值而不是生成随机值。
我们使用 Serial.parseInt() 在 Serial.available() 测试中做这件事。
这意味着一旦数据进入 Arduino 串行缓冲区,我们就会读取它。实际上,我们正在读取两个值,并在字节转换后,将它们存储在LeftChannel和RightChannel中。
然后,我们将数据处理到移位寄存器中,根据 Max 6 补丁发送的值点亮 LED。
让我们再举一个与声音文件和距离传感器玩耍的例子。
手部控制的音调转换效果
音调转换是所有与声音处理相关的领域都熟知的效果。它改变传入声音的音调。在这里,我们将使用 Max 6 实现一个非常便宜的音调转换器,但我们将关注如何控制这个声音效果。我们将通过在距离传感器上移动我们的手来控制它。
我们将使用与第六章中相同的电路,即感知世界 – 使用模拟输入进行感知。
带有传感器和固件的电路
以下电路显示了 Arduino 板连接到传感器:

连接到 Arduino 的 Sharp 距离传感器
固件几乎也是相同的。我移除了关于距离计算的部分,因为我们确实不关心距离本身。
Arduino 的 ADC 提供 10 位的分辨率,这将给出从 0 到 1023 的数字。我们将使用这个值来校准我们的系统。
以下代码是固件。您可以在Chapter12/PitchShift文件夹中找到它:
int sensorPin = 0; // pin number where the SHARP GP2Y0A02YK is connected
int sensorValue = 0 ; // storing the value measured from 0 to 1023
void setup() {
Serial.begin(9600);
}
void loop(){
sensorValue = analogRead(sensorPin); // read/store the value from sensor
Serial.println(sensorValue);
delay(20);
}
一旦 Arduino 运行此固件,它就会向串行端口发送值。
用于改变声音和解析 Arduino 消息的补丁
我无法描述整个音调转换器本身。顺便说一句,您可以打开相关的子补丁来查看它是如何设计的。一切都是开放的。

通过距离传感器控制的手部音调转换
正如我们之前描述的,我们必须选择正确的串行端口,然后敲击 [serial] 对象,以便使其弹出其缓冲区中的值。
在这里,我们使用了 [scale] 对象。它与我们已经使用过的 [zmap] 类似,因为它可以将一个范围映射到另一个范围,但它也可以与反转范围一起工作,并且不会截断值。
在这里,我将从 Arduino 的 ADC 接收到的值从 0 到 1023 映射到适合我们需求的 12.0 到 0.5。
如果我们将手靠近传感器,距离就小,如果我们把手移远,距离就会改变,效果也会被调制。
摘要
本章教我们如何使用 Max 6 处理 Arduino。
我们对 Max 6 中的一些常用技术有了更多的了解,并且我们练习了一些在这本书中学到的概念。显然,Max 6 中还有更多东西可以学习,我很乐意给你一些更好的学习指南。
首先,我建议你阅读 所有 的教程,从 Max 的教程开始,然后是 MSP,然后是数字声音,最后是如果你对视觉效果和 OpenGL 感兴趣的话,是 Jitter。听起来很明显,但我每天还是有两三个人问我从哪里开始学习 Max 6。答案是:教程。
然后,我建议你设计一个小型系统。少即是多。小型系统提供了易于维护、修改和支持的便捷方式。使用注释也是一种快速记住你在这一部分或那一部分尝试做什么的好方法。
最后,每天稍微修补一下是成功的关键。这需要时间,但我们不是都想成为大师吗?
第十三章。提高你的 C 编程技能和创建库
这是本书的最后一章,也是最先进的,但不是最复杂的。你将通过几个典型的示例学习 C 代码优化,这些示例将使你更进一步,并使你在使用 Arduino 的未来项目中更有能力。我将讨论库以及它们如何提高你代码的可重用性,以节省未来的时间。我将描述一些使用位操作而不是常规操作符以及使用一些内存管理技术来提高代码性能的技巧。然后,我将讨论重新编程 Arduino 芯片本身以及使用外部硬件编程器调试我们的代码。
让我们开始吧。
编程库
我已经在第二章中提到了库,与 C 语言的初次接触。我们可以将其定义为一组使用特定语言编写的已实现的行为,该语言通过一些接口提供了一些方法,可以通过这些方法调用所有可用的行为。
基本上,库是一些已经编写好并且可以重复使用的代码,我们可以在自己的代码中通过遵循一些规范来使用它们。例如,我们可以引用 Arduino 核心中包含的一些库。从历史上看,其中一些库是独立编写的,随着时间的推移,Arduino 团队以及整个 Arduino 社区将它们纳入不断增长的核心库中,作为原生可用的库。
让我们以 EEPROM 库为例。为了检查与之相关的文件,我们必须在我们的计算机上找到正确的文件夹。例如,在 OS X 上,我们可以浏览Arduino.app文件本身的内容。我们可以进入Contents/Resources/Java/libraries/中的EEPROM文件夹。在这个文件夹中,我们有三个文件和一个名为examples的文件夹,它包含所有与 EEPROM 库相关的示例:

我们计算机上的 EEPROM 库(一个 OS X 系统)
我们有以下文件:
-
EEPROM.h,包含库的头文件 -
EEPROM.cpp,包含实际的代码 -
keywords.txt,包含一些参数来着色库的关键字
由于这些文件在文件夹层次结构中的位置,它们可以作为核心 EEPROM 库的一部分使用。这意味着我们一旦在我们的计算机上安装了 Arduino 环境,就可以包含这个库,而无需下载其他任何东西。
简单的语句include <EEPROM.h>将库包含到我们的代码中,并使这个库的所有功能都可以进一步使用。
让我们在这些文件中编写代码。
头文件
让我们打开EEPROM.h:

在 Xcode IDE 中显示的 EEPROM.h
在这个文件中,我们可以看到一些以#字符开头的预处理器指令。这是我们用来在 Arduino 代码中包含库的同一个指令。在这里,这是一种很好的方法,可以避免重复包含相同的头文件。有时,在编码过程中,我们会包含很多库,在编译时,我们必须检查我们没有重复包含相同的代码。这些指令,尤其是ifndef指令意味着:“如果EEPROM_h常量尚未定义,则执行以下语句”。
这是一个众所周知的技术,称为包含保护。在这项测试之后,我们首先定义EEPROM_h常量。如果在我们的代码中我们或某些其他库包含了 EEPROM 库,预处理器就不会在第二次看到这个指令时重新处理以下语句。
我们必须使用#endif指令完成#ifndef指令。这是头文件中的一个常见块,如果您打开其他库头文件,您会看到它很多次。这个块里包含什么?我们还有一个与 C 整数类型相关的包含:#include <inttypes.h>。
Arduino IDE 包含库中所有必需的 C 头文件。正如我们之前提到的,我们可以在固件中使用纯 C 和 C++代码。我们之前没有这样做,因为我们一直在使用的函数和类型已经编码到 Arduino 核心中。但请记住,您可以选择在固件中包含其他纯 C 代码,在本章的最后,我们还将讨论您也可以遵循纯 AVR 处理器类型代码的事实。
现在我们有一个类定义。这是一个 C++特性。在这个类内部,我们声明了两个函数原型:
-
uint8_t read(int) -
void write(int, uint8_t)
有一个函数用于读取,它接受一个整数作为参数,并返回一个 8 位无符号整数(即字节)。然后,还有一个函数用于写入,它接受一个整数和一个字节,并返回空值。这些原型指的是在其他EEPROM.cpp文件中这些函数的定义。
源文件
让我们打开EEPROM.cpp文件:

EEPROM 库的源文件在 Xcode IDE 中显示
文件开始时包含了一些头文件。avr/eeprom.h指的是 AVR 类型处理器的 EEPROM 库本身。在这个库示例中,我们只是有一个库,它引用并为我们提供了比原始纯 AVR 代码更好的 Arduino 编程风格接口。这就是为什么我选择了这个库示例。这是最短但最明确的示例,它教会了我们很多。
然后我们包含Arduino.h头文件,以便访问 Arduino 语言本身的标凈类型和常量。最后,当然,我们还要包含 EEPROM 库本身的头文件。
在以下语句中,我们定义了这两个函数。它们在其块定义中调用其他函数:
-
eeprom_read_byte() -
eeprom_write_byte()
这些函数直接来自 AVR EEPROM 库本身。EEPROM Arduino 库只是 AVR EEPROM 库的一个接口。我们为什么不尝试自己创建一个库呢?
创建自己的 LED 数组库
我们将创建一个非常小的库,并用一个包括六个非复用 LED 的基本电路来测试它。
将六个 LED 连接到板子上
下面是电路图。它基本上包含六个连接到 Arduino 的 LED:

六个 LED 连接到板子上
电路图如下所示:

另一个将六个 LED 直接连接到 Arduino 的电路图
我不会讨论电路本身,只是提一下我放入了一个 1 kΩ的电阻。我考虑了最坏的情况,即所有 LED 同时点亮。这将驱动大量的电流,因此这为我们的 Arduino 提供了安全保障。一些作者可能不会使用它。我更愿意让一些 LED 稍微暗一些,以保护我的 Arduino。
创建一些漂亮的灯光图案
下面是按照某些模式点亮 LED 的代码,所有这些都是硬编码的。每个图案显示之间都有一个暂停:
void setup() {
for (int i = 2 ; i <= 7 ; i++)
{
pinMode(i, OUTPUT);
}
}
void loop(){
// switch on everything progressively
for (int i = 2 ; i <= 7 ; i++)
{
digitalWrite(i, HIGH);
delay(100);
}
delay(3000);
// switch off everything progressively
for (int i = 7 ; i >=2 ; i--)
{
digitalWrite(i, LOW);
delay(100);
}
delay(3000);
// switch on even LEDS
for (int i = 2 ; i <= 7 ; i++)
{
if ( i % 2 == 0 ) digitalWrite(i, HIGH);
else digitalWrite(i, LOW);
}
delay(3000);
// switch on odd LEDS
for (int i = 2 ; i <= 7 ; i++)
{
if ( i % 2 != 0 ) digitalWrite(i, HIGH);
else digitalWrite(i, LOW);
}
delay(3000);
}
这段代码运行正确。但我们如何让它更优雅,尤其是更易于重用呢?我们可以将for()循环块嵌入到函数中。但它们只在这个代码中可用。我们必须通过记住我们设计它们的那个项目来复制和粘贴它们,以便在另一个项目中重用它们。
通过创建一个我们可以反复使用的库,我们可以在未来的编码和数据处理中节省时间。通过一些定期的修改,我们可以达到为特定任务设计的完美模块,它将越来越好,直到不需要再触碰它,因为它比其他任何东西都表现得更好。至少这是我们希望看到的。
设计一个小型的 LED 图案库
首先,我们可以在头文件中设计我们函数的原型。让我们把这个库叫做LEDpatterns。
编写 LEDpatterns.h 头文件
下面是一个可能的头文件示例:
/*
LEDpatterns - Library for making cute LEDs Pattern.
Created by Julien Bayle, February 10, 2013.
*/
#ifndef LEDpatterns_h
#define LEDpatterns_h
#include "Arduino.h"
class LEDpatterns
{
public:
LEDpatterns(int firstPin, int ledsNumber);
void switchOnAll();
void switchOffAll();
void switchEven();
void switchOdd();
private:
int _firstPin;
int _ledsNumber;
};
#endif
我们首先编写我们的 include guards。然后包含 Arduino 库。然后,我们定义一个名为LEDpatterns的类,其中包含与类本身同名的构造函数等public函数。
我们还有两个与第一个连接 LED 的引脚和与连接 LED 总数相关的内部(private)变量。在示例中,LED 必须连续连接。
编写 LEDpatterns.cpp 源文件
这是 C++库的源代码:
/*
LEDpatterns.cpp - Library for making cute LEDs Pattern.
Created by Julien Bayle, February 10, 2013.
*/
#include "Arduino.h"
#include "LEDpatterns.h"
LEDpatterns::LEDpatterns(int firstPin, int ledsNumber)
{
for (int i = firstPin ; i < ledsNumber + firstPin ; i++)
{
pinMode(i, OUTPUT);
}
_ledsNumber = ledsNumber;
_firstPin = firstPin;
}
void LEDpatterns::switchOnAll()
{
for (int i = _firstPin ; i < _ledsNumber + _firstPin ; i++)
{
digitalWrite(i, HIGH);
delay(100);
}
}
void LEDpatterns::switchOffAll()
{
for (int i = _ledsNumber + _firstPin -1 ; i >= _firstPin ; i--)
{
digitalWrite(i, LOW);
delay(100);
}
}
void LEDpatterns::switchEven()
{
for (int i = _firstPin ; i < _ledsNumber + _firstPin ; i++)
{
if ( i % 2 == 0 ) digitalWrite(i, HIGH);
else digitalWrite(i, LOW);
}
}
void LEDpatterns::switchOdd()
{
for (int i = _firstPin ; i < _ledsNumber + _firstPin ; i++)
{
if ( i % 2 != 0 ) digitalWrite(i, HIGH);
else digitalWrite(i, LOW);
}
}
在开始时,我们检索所有include库。然后我们有构造函数,这是一个与库同名的特殊方法。这是这里的重要点。它接受两个参数。在其主体内部,我们将从第一个到最后的所有引脚(将 LED 视为数字输出)放入其中。然后,我们将构造函数的参数存储在之前在头文件LEDpatterns.h中定义的private变量中。
我们可以声明所有与第一个示例中创建的函数相关的函数,而不需要库。注意每个函数的LEDpatterns::前缀。我不会在这里讨论这种纯类相关语法,但请记住结构。
编写 keyword.txt 文件
当我们查看我们的源代码时,如果某些内容能够跳出来而不是融入背景,那就非常有帮助。为了正确地着色与我们新创建的库相关的不同关键字,我们必须使用keyword.txt文件。让我们检查一下这个文件:
#######################################
# Syntax Coloring Map For Messenger
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
LEDpatterns KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
switchOnAll KEYWORD2
switchOffAll KEYWORD2
switchEven KEYWORD2
switchOdd KEYWORD2
#######################################
# Instances (KEYWORD2)
#######################################
#######################################
# Constants (LITERAL1)
#######################################
在前面的代码中,我们可以看到以下内容:
-
所有跟随
KEYWORD1的内容都将被染成橙色,通常用于类 -
所有跟随
KEYWORD2的内容都将被染成棕色,用于函数 -
所有跟随
LITERAL1的内容都将被染成蓝色,用于常量
使用这些来着色你的代码并使其更易于阅读是非常有用的。
使用 LEDpatterns 库
该库位于Chapter13的LEDpatterns文件夹中,你必须将它放在与其他库相同的正确文件夹中,我们已经这样做了。我们必须重新启动 Arduino IDE 以使库可用。完成之后,你应该能够在菜单Sketch | Import Library中检查它。现在LEDpatterns已经出现在列表中:

该库是一个贡献的库,因为它不是 Arduino 核心的一部分
现在我们使用这个库来检查新的代码。你可以在Chapter13/LEDLib文件夹中找到它:
#include <LEDpatterns.h>
LEDpatterns ledpattern(2,6);
void setup() {
}
void loop(){
ledpattern.switchOnAll();
delay(3000);
ledpattern.switchOffAll();
delay(3000);
ledpattern.switchEven();
delay(3000);
ledpattern.switchOdd();
delay(3000);
}
在第一步中,我们包含LEDpatterns库。然后,我们创建名为ledpattern的LEDpatterns实例。我们使用之前设计的带有两个参数的构造函数:
-
第一个 LED 的第一个引脚
-
LED 的总数
ledpattern是LEDpatterns类的一个实例。它在我们的代码中被引用,如果没有#include,它将无法工作。我们已调用这个实例的每个方法。
如果代码看起来更干净,这种设计的真正好处是我们可以在我们的任何项目中重用这个库。如果我们想修改和改进库,我们只需要修改库的头文件和源文件。
内存管理
这个部分非常短,但绝对不是不重要。我们必须记住,我们在 Arduino 上有以下三个内存池:
-
闪存(程序空间),其中存储固件
-
静态随机存取存储器(SRAM),其中草图在运行时创建和操作变量
-
EEPROM 是一个用于存储长期信息的内存空间
与 SRAM 相比,Flash 和 EEPROM 是非易失性的,这意味着即使断电后数据也会持续存在。每个不同的 Arduino 板都有不同数量的内存:
-
ATMega328 (UNO) 具有:
-
Flash 32k 字节(引导程序占用 0.5k 字节)
-
SRAM 2k 字节
-
EEPROM 1k 字节
-
-
ATMega2560 (MEGA) 具有:
-
Flash 256k 字节(引导程序占用 8k 字节)
-
SRAM 8k 字节
-
EEPROM 4k 字节
-
一个经典的例子是引用一个字符串的基本声明:
char text[] = "I love Arduino because it rocks.";
这将占用 32 字节到 SRAM 中。这似乎并不多,但 UNO 只提供了 2048 字节。想象一下,如果你使用了一个大的查找表或大量的文本。以下是一些节省内存的技巧:
-
如果你的项目同时使用 Arduino 和计算机,你可以尝试将一些计算步骤从 Arduino 移动到计算机本身,使 Arduino 只在计算机上触发计算并请求结果,例如。
-
总是使用可能的最小数据类型来存储你需要的数据。例如,如果你需要存储介于 0 和 255 之间的数据,不要使用占用 2 字节的
int类型,而应使用byte类型 -
如果你使用了一些不会更改的查找表或数据,你可以将它们存储在 Flash 内存中而不是 SRAM 中。你必须使用
PROGMEM关键字来完成此操作。 -
你可以使用 Arduino 板的原生 EEPROM,这将需要编写两个小程序:第一个用于将信息存储在 EEPROM 中,第二个用于使用它。我们在第九章 Making Things Move and Creating Sounds 中使用 PCM 库做到了这一点,使事物移动和创造声音。
掌握位移操作
C++ 中有两个位移操作符:
-
<<是左移操作符 -
>>是右移操作符
这些在 SRAM 内存中非常有用,并且通常可以优化你的代码。<< 可以理解为左操作数乘以 2 的右操作数次幂。
>> 与之相同,但类似于除法。位操作的能力通常非常有用,并且可以在许多情况下使你的代码更快。
用 2 的倍数进行乘除
让我们使用位移来乘以一个变量。
int a = 4;
int b = a << 3;
第二行将变量 a 乘以 2 的三次方,因此 b 现在包含 32。同样,除法可以按以下方式进行:
int a = 12 ;
int b = a >> 2;
b 包含 3,因为 >> 2 等于除以 4。使用这些操作符可以使代码更快,因为它们是直接访问二进制操作,而不需要使用 Arduino 核心的任何函数,如 pow() 或其他操作符。
将多个数据项打包到字节中
例如,而不是使用一个大型的二维表来存储,比如以下显示的位图:
const prog_uint8_t BitMap[5][7] = {
// store in program memory to save RAM
{1,1,0,0,0,1,1},
{0,0,1,0,1,0,0},
{0,0,0,1,0,0,0},
{0,0,1,0,1,0,0},
{1,1,0,0,0,1,1} };
我们可以使用以下代码:
const prog_uint8_t BitMap[5] = {
// store in program memory to save RAM
B1100011,
B0010100,
B0001000,
B0010100,
B1100011 };
在第一种情况下,每个位图需要 7 x 5 = 35 字节。在第二种情况下,只需要 5 字节。我想你已经刚刚发现了一些重大的事情,不是吗?
在控制和端口寄存器中打开/关闭单个位
以下是前一条技巧的直接后果。如果我们想将引脚 8 到 13 设置为输出,我们可以这样做:
void setup() {
int pin;
for (pin=8; pin <= 13; ++pin) {
pinMode (pin, LOW);
}
}
但这样做会更好:
void setup() {
DDRB = B00111111 ; // DDRB are pins from 8 to 15
}
在一次遍历中,我们直接在内存中将整个包配置到一个变量中,并且不需要编译 pinMode 函数、结构或变量名。
重新编程 Arduino 板
Arduino 本地使用著名的引导加载程序。这为我们通过 USB 上的虚拟串行端口上传固件提供了一种很好的方式。但我们也可能对在没有引导加载程序的情况下继续前进感兴趣。如何以及为什么?首先,这将节省一些闪存内存。它还提供了一种避免在我们打开或重置板子并使其活跃并开始运行之前的小延迟的方法。这需要一个外部编程器。
我可以引用 AVR-ISP、STK500,甚至并行编程器(并行编程器在 arduino.cc/en/Hacking/ParallelProgrammer 中有描述)。你可以在 Sparkfun 电子找到 AVR-ISP。
我在 2013 年的一个名为 The Village 的连接城市的项目中,用这个编程器编程了 Arduino FIO 型板,用于特定的无线应用。

Sparkfun 电子的口袋 AVR 编程器
这个编程器可以通过 2 x 5 连接器连接到 Arduino 板上的 ICSP 端口。

Arduino 的 ICSP 连接器
为了重新编程 Arduino 的处理器,我们首先必须关闭 Arduino IDE,然后检查首选项文件(Mac 上的 preferences.txt,位于 Arduino.app 包内的 Contents/Resources/Java/lib 中)。在 Windows 7 PC 和更高版本上,此文件位于:c:\Users\<USERNAME>\AppData\Local\Arduino\preferences.txt。在 Linux 上位于:~/arduino/preferences.ard。
我们必须将最初设置为引导加载程序的 upload.using 值更改为适合你的编程器的正确标识符。这可以在 OS X 的 Arduino 应用程序包内容中找到,或者在 Windows 的 Arduino 文件夹中。例如,如果你显示 Arduino.app 内容,你可以找到这个文件:Arduino.app/Contents/Resources/Java/hardware/arduino/programmers.txt。
然后,我们可以启动 Arduino IDE,使用我们的编程器上传草图。要恢复到正常的引导加载程序行为,我们首先必须重新上传适合我们硬件的引导加载程序。然后,我们必须更改 preferences.txt 文件,它将像初始板一样工作。
摘要
在这一章中,我们学习了更多关于设计库的知识,我们现在能够以不同的方式设计我们的项目,考虑到未来项目中代码或代码部分的复用性。这可以节省时间,同时也提高了可读性。
我们也可以通过使用现有的库,通过修改它们,使它们符合我们的需求,来探索开源世界。这是一个真正开放的世界,我们刚刚迈出了第一步。
结论
我们已经到了这本书的结尾。你可能已经读完了所有的内容,也用你自己的硬件测试了一些代码片段,我相信你现在能够想象出你未来的高级项目了。
我想感谢你如此专注和感兴趣。我知道你现在几乎和我处于同一条船上,你想要学习更多,测试更多,检查和使用新技术来实现你疯狂的项目。我想说最后一件事:去做,现在就去做!
在大多数情况下,人们在开始之前,仅仅想象到将要面对的大量工作就会感到害怕。但你要相信我,不要过多地考虑细节或优化。试着做一些简单的东西,一些能工作起来的东西。然后你会有方法去优化和改进它。
最后一条建议给你:不要想太多,多做一些。我见过太多的人因为想要先想、想、想,而不是先开始做,结果项目没有完成。
保重,继续探索!













浙公网安备 33010602011771号