Arduino C 编程指南(二)

原文:zh.annas-archive.org/md5/84c5bfddb3707fe8d398d438af660eab

译者:飞龙

协议:CC BY-NC-SA 4.0

第四章。使用函数、数学和计时提高编程能力

作为一位数字艺术家,我需要特殊的条件才能工作。我们都需要自己的环境和氛围来提高生产力。即使我们每个人都有自己的方式,也有很多共同之处。

在这一章中,我想给你一些元素,这将使你更容易编写易于阅读、重用,尽可能美观的源代码。就像阴阳一样,对我来说,我的艺术和编程方面始终有一种禅意。这里我可以提供一些编程智慧之珠,以给你的创造性带来平静。

我们将要学习一些我们之前已经稍微使用过的一些内容:函数。它们同时有助于提高可读性和效率。在这个过程中,我们将涉及到许多项目中常用的数学和三角学。我们还将讨论一些计算优化的方法,并以 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);。当然,ii+1 的技巧很有趣,但在这里要认识到的是在 loop() 函数开始处声明的变量 currentResult 的使用。

在编程中,重要的是要认识到右边的所有内容(内容)都进入左边(新容器)。根据优先级规则,函数调用相对于 = 赋值运算符的优先级为 2 对 16。这意味着调用首先进行,函数返回 + 操作的结果,正如我们设计的那样。从这个角度来看,你刚刚学到了一些非常重要的东西:函数返回值的调用语句是一个值

你可以查看 附录 B,C 和 C++中的运算符优先级 以获取完整的优先级列表。与变量内的所有值一样,我们可以将其存储到另一个变量中,这里是在整数变量 result 中。

使用函数的好处

编程是关于为一般和特定目的编写代码片段。使用函数是分割你的代码的最佳方式之一。

更容易的编码和调试

函数真的可以帮助我们更好地组织。在设计程序时,我们经常使用伪代码,这也是我们注意到有很多常见语句的步骤。这些常见语句通常可以放在函数中。

函数/调用模式也更容易调试。函数所在的部分只有一段代码。如果有问题,我们只需调试一次函数本身,然后所有的调用都会被修复,而不是修改整个代码部分。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_001.jpg

函数使你的代码更容易调试

更好的模块化有助于重用性

你的一些代码将是高级和通用的。例如,在某个时候,你可能需要一系列可以切割数组并按照基本规则重新组合所有值的语句。这个系列可以是函数的主体。另一种方式是编写一个将华氏单位转换为摄氏单位的函数可能对你感兴趣。这两个例子都是通用函数。

相反,你也可以有一个特定功能的唯一目的是将美元转换为法国法郎。你可能不会经常调用它,但如果偶尔需要,它总是准备好处理这个任务。

在这两种情况下,函数都可以使用,当然也可以重用。背后的想法是节省时间。这也意味着你可以抓取一些已经存在的函数并重用它们。当然,这必须遵循一些原则,例如:

  • 代码许可

  • 尊重可以作为库一部分的函数的 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是两个距离之间的比率,并以弧度单位表示。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_002.jpg

弧度定义

是一个完整旋转的 1/360(完整圆)。考虑到这两个定义以及一个完整旋转等于 2π的事实,我们可以将一个转换为另一个:

注意

angleradian = angledegree x π/180

angledegree = angleradian x 180/π

余弦、正弦和正切

让我们看看三角形的例子:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_003.jpg

考虑到以弧度为角度 A,我们可以定义余弦、正弦和正切如下:

  • cos(A) = b/h

  • sin(A) = a/h

  • tan(A) = sin(A)/cos(A) = a/b

余弦和正弦在弧度角度的值为-1 到 1 之间演变,而正切有一些特殊点,在这些点上它没有定义,然后周期性地从-∞演变到+∞。我们可以如下在同一张图上表示它们:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_004.jpg

图形余弦、正弦和正切表示

当然,这些函数会振荡,无限地复制相同的演变。记住它们不仅可以用于纯计算,还可以通过用更平滑的振荡代替线性值演变来避免时间上的过度线性演变。我们稍后会看到这一点。

当我们有一个角度时,我们知道如何计算余弦/正弦/正切,但当我们已经有了余弦/正弦/正切时,如何计算角度呢?

反余弦、反正弦和反正切

反余弦、反正弦和反正切被称为反三角函数。这些函数用于计算角度,当你已经有了之前提到的距离比时。

它们被称为反函数,因为这是之前看到的三角函数的逆/倒数过程。基本上,这些函数为你提供一个角度,但考虑到周期性,它们提供了很多角度。如果 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); 返回 xy 次幂

  • 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 为例。它可以表示如下:

位置012345
10101

然后,我可以写出这个乘积之和,它等于我的 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 常量,它有助于将度转换为弧度。我们声明了 cosinePrecisioncosinePeriod 以执行这些计算一次。

然后,我们定义了一个 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()提供一个微秒数。

程序在延迟期间做什么?

什么也不做。它等待。这个子子节并不是玩笑。我希望你能专注于这个特定的点,因为稍后它将非常重要。

注意

当你在程序中调用delaydelayMicroseconds时,它会停止执行一段时间。

这里有一个小图解,说明了当我们打开 Arduino 时会发生什么:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_012.jpg

Arduino 固件的一个生命周期

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_005.jpg

固件生命周期中的主要部分循环

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_006.jpg

当调用delay()时,固件的主要部分循环,并中断

当调用 delay() 时,整个程序会中断。中断的长度取决于传递给 delay() 的参数。

我们可以注意到,所有事情都是按顺序和按时完成的。如果一条语句执行需要很长时间,Arduino 的芯片会先执行它,然后继续下一个任务。

在那种非常常见和普遍的情况下,如果某个特定任务(语句、函数调用或任何其他)需要很长时间,整个程序可能会挂起并产生中断;考虑用户体验。

想象一下这样一个具体案例,你需要在同一时间读取传感器,切换一些开关,并将信息写入显示屏。如果你按顺序这样做,并且你有大量的传感器,这是相当常见的,那么在 loop() 中执行这个任务会在其他任务之后,你可能会在信息显示上出现一些延迟和减速。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_007.jpg

一个忙于许多输入和输出的 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,现在,我们将引入线程。请稍等!

线程是什么?

线程是处理器执行一系列任务(通常循环,但不一定)的运行程序流程。

只有一个处理器时,通常是通过时分复用来完成的,这意味着处理器根据时间在不同的线程之间切换,即上下文切换。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_008.jpg

时分复用提供了多任务处理

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_009.jpg

真正的多线程提供了同时发生的任务

由于我们现在没有处理计算机处理器,所以不深入探讨计算机处理器,我可以告诉你,线程是在编程中用来使任务同时运行的不错的技术。

不幸的是,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文件夹中,否则创建这个特殊的文件夹:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_010.jpg

Marcello Romani 编写的 SimpleTimer 的头文件和源代码

下次你启动 Arduino IDE 时,如果你去草图 | 导入库,你会在底部看到一个新库。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_04_011.jpg

为了包含一个库,你可以点击菜单中的它,它将在你的代码中写入#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 能够感知世界,例如温度,我们就需要连接一个传感器。

一些类型的传感器

我们可以找到各种类型的传感器。当我们使用传感器这个词时,我们经常想到环境传感器。

我将首先引用一些环境量:

  • 温度

  • 湿度

  • 压力

  • 气体传感器(特定气体或非特定气体,烟雾)

  • 电磁场

  • 风速计(风速)

  • 光线

  • 距离

  • 电容

  • 运动

这是一个不完整的列表。对于几乎每个数量,我们都可以找到一个传感器。实际上,对于每个可量化的物理或化学现象,都有一种方法可以测量和跟踪它。每个都提供了与测量的数量相关的数据。

数量被转换为数据

当我们使用传感器时,原因是我们需要从物理现象(如温度或运动)中获得一个数值。如果我们能够直接用我们皮肤的热传感器测量温度,我们就能够理解化学成分的体积与温度本身之间的关系。因为我们从其他物理测量或计算中知道了这种关系,所以我们能够设计温度计。

事实上,温度计是将与温度相关的数量(在这里是体积)转换为温度计刻度上可读的值的转换。实际上,我们在这里有一个双重转换。体积是温度的函数。温度计内液体的液位是液体积的函数。因此,我们可以理解高度和温度是相关的。这是双重转换。

不管怎样,温度计是一个很好的模块,它集成了所有这些数学和物理的奇妙之处,以提供数据,一个值:温度。如图所示,体积被用来提供温度:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_01.jpg

所有传感器都像这样工作。它们是测量物理现象并提供值的模块。我们稍后会看到这些值可以非常不同,最终也可以编码。

数据必须被感知

传感器提供的数据,如果被读取,就会更有意义。这可能是显而易见的,但想象一下,读者不是一个人类,而是一台仪器、一台机器,或者在我们的例子中,是一块 Arduino 板。

事实上,让我们以一个电子热传感器为例。首先,这个传感器必须供电才能工作。然后,如果我们能够供电但无法从其引脚物理测量它产生的电势,我们就无法欣赏它试图为我们提供的主要价值:温度。

在我们的例子中,Arduino 将是能够将电势转换为可读或至少对我们人类来说更容易理解的设备的装置。这又是一个转换。从我们想要翻译的物理现象,到显示解释物理现象的值的设备,有转换和感知。

我可以将这个过程简化,如下面的图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_02.jpg

数字意味着什么?

让我们精确地定义这里的数字术语。

数字和模拟概念

在计算机和电子领域,数字意味着离散的,这是与模拟/连续相反的。它也是一个数学定义。我们经常谈论域来定义数字和模拟的使用情况。

通常,模拟域是与物理测量相关的域。我们的温度可以具有所有可能和存在的值,即使我们的测量设备没有无限分辨率。

数字域是计算机的域。由于编码和有限的内存大小,计算机将模拟/连续值转换为数字表示。

在图表上,这可以表示如下:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_03.jpg

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 文件,并使用图标运行生成的文件:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_004.jpg

Processing 图标

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_005.jpg

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_006.jpg

Processing 的 IDE 看起来与其他 IDE 相似

一个非常熟悉的 IDE

事实上,Processing IDE 看起来就像 Arduino IDE。Processing IDE 就像是 Arduino IDE 的父亲。

这完全正常,因为 Arduino IDE 是从 Processing IDE 分叉出来的。现在,我们将检查我们是否也会非常熟悉 Processing IDE。

让我们探索它并运行一个小示例:

  1. 前往文件 | 示例 | 基础 | 数组 | 数组对象

  2. 然后,点击第一个图标(播放符号箭头)。您应该看到以下截图:https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_007.jpg

    在 Processing 中运行 ArrayObjects 原生示例

  3. 现在点击小方块(停止符号)。是的,这个新的游乐场非常熟悉。https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_008.jpg

    打开包含 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]);
  }
}

你可以运行这段代码。然后,你可以将鼠标移入画布中,享受正在发生的事情。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_010.jpg

processingMultipleEasing 代码正在运行并显示一系列奇怪的粒子,这些粒子跟随鼠标移动

首先,检查代码。基本上,这是 Java。我想你不会太惊讶,对吧?确实,Java 源自 C。

你可以在你的代码中看到三个主要部分:

  • 变量声明/定义

  • setup()函数只在开始时运行一次

  • draw()函数会无限运行,直到你按下停止键

好的。你可以看到 Arduino 核心和 Processing 中的setup()函数具有类似的作用,loop()draw()也是如此。

这段代码展示了 Processing 中的一些常用设计模式。我首先初始化一个变量来存储全局粒子数,然后为我想创建的每个粒子初始化一些数组。请注意,所有这些数组在这个步骤都是空的!

这种模式很常见,因为它提供了良好的可读性,并且工作得很好。我本可以使用类或甚至是多维数组,但在后一种情况下,除了代码更短(但可读性更差)之外,我甚至不会得到任何好处。在这些数组中,第N个索引值代表第N个粒子。为了存储/检索粒子N的参数,我必须操纵每个数组中的第N个值。参数分布在每个数组中,但存储和检索都很方便,不是吗?

setup()中,我定义并实例化了画布及其 600 x 600 的大小。然后,我定义在所有我的绘画中都不会有线条。例如,圆的线条是其边界。

然后,我使用for循环结构填充easingradii数组。这是一个非常常见的模式,我们可以使用setup()在开始时初始化一系列参数。然后我们可以检查draw()循环。我定义了一个背景颜色。这个函数也会擦除画布并填充参数中的颜色。查看参考页面上的背景函数,以了解我们如何使用它。这种擦除/填充是一种很好的方式来擦除每一帧并重置画布。

在这次擦除/填充之后,我将鼠标的当前位置存储在每个坐标的局部变量targetXtargetY中。

程序的核心位于for循环中。这个循环遍历每个粒子,并为每个粒子生成一些内容。代码相当直观。我还可以在这里补充说,我正在检查鼠标和每个粒子之间的距离,这是在每一帧(每次draw()的运行)中进行的,并且我会根据其缓动效果移动每个粒子来绘制它们。

这是一个非常简单的例子,但也是一个很好的例子,我用来展示 Processing 的强大功能。

Processing 和 Arduino

Processing 和 Arduino 是非常好的朋友。

首先,它们都是开源的。这是一个非常友好的特性,带来了许多优势,如代码源共享和庞大的社区等。它们适用于所有操作系统:Windows、OS X 和 Linux。我们还可以免费下载它们,并点击几下即可运行。

我最初是用 Processing 编程的,并且我经常用它来做一些自己的数据可视化项目和艺术作品。然后,我们可以在屏幕上通过平滑和原始的形状来展示复杂和抽象的数据流。

我们现在要一起做的是在 Processing 画布上显示 Arduino 的活动。实际上,这是 Processing 作为 Arduino 友好的软件的常见用途。

我们将设计一个非常简单且便宜的硬件和软件之间的通信协议。这将展示我们在本书下一章将进一步深入探讨的路径。确实,如果你想让你的 Arduino 与另一个软件框架(我想到了 Max 6、openFrameworks、Cinder 以及许多其他框架)通信,你必须遵循相同的设计方法。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_011.jpg

Arduino 和一些软件朋友

我经常说 Arduino 可以作为一个非常智能的器官来工作。如果你想将一些软件连接到真实的物理世界,Arduino 就是你的选择。确实,通过这种方式,软件可以感知世界,为你的电脑提供新的功能。让我们通过在电脑上显示一些物理世界事件来继续前进。

按下按钮

我们将会有趣。是的,这就是我们将物理世界与虚拟世界连接的特殊时刻。Arduino 正是关于这一点。

按钮和开关是什么?

开关是一种能够断开电路的电气元件。有很多不同类型的开关。

不同类型的开关

一些开关被称为切换开关。切换开关也被称为连续开关。为了对电路进行操作,切换开关可以每次按下并释放,以便进行操作,当你释放它时,操作会继续。

一些被称为瞬态开关。瞬态开关也被称为动作按钮。为了对电路进行操作,你必须按下并保持开关按下以继续操作。如果你释放它,操作就会停止。

通常,我们家里的所有开关都是切换开关。除了你必须按下以切断并释放以停止的混音器开关,这意味着它是一个瞬态开关。

基本电路

这里有一个带有 Arduino、一个瞬态开关和一个电阻的基本电路。

我们想在按下瞬态开关时打开板上的内置 LED,并在释放它时关闭 LED。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_12.jpg

小电路

我现在向您展示的是我们即将要工作的电路。这也是一个很好的理由,让你更熟悉电路图。

电缆

每条线代表两个组件之间的连接。根据定义,一条线是电缆,从一侧到另一侧没有电势。它也可以定义为以下内容:电缆的电阻为 0 欧姆。然后我们可以这样说,通过电缆连接的两个点具有相同的电势。

现实世界的电路

当然,我不想直接展示下一个图表。现在我们必须构建真实的电路,所以请拿一些电线、你的面包板和瞬态开关,并按照下一个图表所示连接整个电路。

你可以取一个大约 10 千欧姆的电阻。我们将在下一页解释电阻的作用。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_014.jpg

真实电路中的瞬态开关

让我们更详细地解释一下。

让我们记住面包板布线;我在面包板顶部使用冷线和热线(冷线是蓝色,表示地线,热线是红色,表示+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 点亮。我们首先编写伪代码。

伪代码

下面是一个可能的伪代码。以下是我们希望固件遵循的步骤:

  1. 定义引脚。

  2. 定义一个变量来表示当前开关状态。

  3. 将 LED 引脚设置为输出。

  4. 将开关引脚设置为输入。

  5. 设置一个无限循环。在无限循环中执行以下操作:

    1. 读取输入状态并存储它。

    2. 如果输入状态是 HIGH,则点亮 LED。

    3. 否则关闭 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 已经内置了一个非常有用的库集。具体来说,我们将使用串行库。

让我们先画一个伪代码,就像往常一样。

绘制伪代码

我们希望程序做什么?

我建议有一个大圆圈。它的颜色将代表开关的状态。深色表示未释放,而绿色表示按下。

可以如下创建伪代码:

  1. 定义并实例化串行端口。

  2. 定义一个当前绘图颜色为深色。

  3. 在无限循环中,执行以下操作:

    1. 检查串行端口和抓取数据是否已接收。

    2. 如果数据指示状态是关闭的,将当前绘图颜色从颜色更改为深色。

    3. 否则,将当前绘图颜色更改为绿色。

    4. 使用当前绘图颜色绘制圆圈。

让我们写下这段代码

让我们打开一个新的 Processing 画布。

因为 Processing IDE 的工作方式类似于 Arduino IDE,需要在一个文件夹中创建所有保存的项目文件,所以我建议您直接在磁盘上的正确位置保存画布,即使它是空的。命名为processingOneButtonDisplay

您可以在 Packt 网站上找到代码,位于Chapter05/processingOneButtonDisplay/文件夹中,可供下载,以及其他代码文件。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_015.jpg

在您的代码中包含库

要从 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;
    }
  }
}
变量定义

theSerialPortSerial库的对象。我必须首先创建它。

serialBytesArray是一个包含两个整数的数组,用于存储来自 Arduino 的消息。您还记得吗?当我们设计协议时,我们谈到了 2 字节消息。

switchStateswitchID是全局但临时变量,用于存储来自板子的开关状态和开关 ID。开关 ID 被放置在那里,以便(接近)未来的实现,以便在我们要使用多个开关的情况下区分不同的开关。

bytesCount是一个有用的变量,用于跟踪我们的消息读取中的当前位置。

init在开始时定义为false,当第一次接收到来自 Arduino 的第一个字节(以及一个特殊的字节,Z)时变为true。这是一种首次接触的目的。

然后,我们跟踪填充颜色和初始颜色是4040只是一个整数,并将稍后用作函数fill()的参数。

setup()

我们定义画布(大小、背景颜色和没有轮廓)。

我们打印出计算机上可用的所有串行端口的列表。这是下一个语句的调试信息,我们将第一个串行端口的名称存储到一个 String 中。实际上,您可能需要根据打印列表中 Arduino 端口的定位将数组元素从 0 更改为正确的位置。

这个字符串随后被用于一个非常重要的语句,该语句在 9600 波特率下实例化串行通信。

当然,这个 setup() 函数只运行一次。

draw()

在这里,draw 函数非常简单。

我们将变量 fillColor 传递给 fill() 函数,设置所有后续形状填充的颜色。

然后,我们使用椭圆函数绘制圆形。这个函数接受四个参数:

  • 椭圆中心的 x 坐标(这里 width/2

  • 椭圆中心的 y 坐标(这里 height/2

  • 椭圆的宽度(这里 230

  • 椭圆的高度(这里 230

在 Processing IDE 中用蓝色标注的 widthheight 是画布的当前宽度和高度。使用它们非常有用,因为如果你通过选择新的画布大小更改 setup() 语句,你的代码中所有的 widthheight 都会自动更新,而无需手动更改它们。

请记住,宽度和高度相同的椭圆是一个圆(!)!好的。但是这里的魔法在哪里?它只会绘制一个圆形,每次都是同一个(大小和位置)。fillColordraw() 函数的唯一变量。让我们看看那个奇怪的回调 serialEvent()

serialEvent() 回调

我们在 第四章 中讨论了回调,通过函数、数学和计时改进编程

这里,我们在 Processing 中有一个纯回调方法。这是一个事件驱动的回调。在这种情况下,不必每次轮询我们的串行端口是否需要读取数据是有用且高效的。确实,与用户界面相关的事件数量远少于 Arduino 板的处理器周期数。在这种情况下实现回调更聪明;一旦发生串行事件(即接收到消息),我们就执行一系列语句。

myPort.read() 首先读取接收到的字节。然后我们使用 init 变量进行测试。实际上,如果是第一条消息,我们想检查通信是否已经启动。

在第一次“你好”(init == false)的情况下,如果来自 Arduino 板的消息是 Z,Processing 程序会清除自己的串行端口,存储通信刚刚开始的事实,并将 Z 重新发送回 Arduino 板。这并不复杂。

这可以如下说明:

假设我们只有通过先互相说“你好”才能开始交谈。我们并没有互相观察(没有事件)。然后我开始说话。你转向我(串行事件发生)并倾听。我是不是在对你说“你好”?(消息是否为 Z?)。如果不是,你只需转回你的头(没有 else 语句)。如果是,你回答“你好”(发送回 Z),通信就开始了。

那么,接下来会发生什么呢?

如果通信已经开始,我们必须将读取的字节存储到serialBytesArray中,并增加bytesCount。当字节正在接收且bytesCount小于或等于 1 时,这意味着我们没有完整的消息(一个完整的消息是两个字节),因此我们在数组中存储更多的字节。

当字节计数等于2时,我们就有了完整的消息,我们可以将其“分割”成变量switchIDswitchState。我们是这样做的:

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 输入上复用大量输入,而是一种基本的点对点连接,其中每个开关都连接到一个输入。我们将在稍后学习复用(在下一章中)。

电路

下面是与多个开关一起工作所需的电路图:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_16.jpg

将三个瞬态开关连接到 Arduino 板

电路图是对之前只显示一个开关的电路图的扩展。我们可以看到三个开关位于+5V 和三个下拉电阻之间。然后我们还可以看到连接到数字输入 2 到 4 的三根线。

这里是一个小的记忆刷新:为什么我没有使用数字引脚 0 或 1?

因为我在 Arduino 中使用串行通信,所以我们不能使用数字引脚 0 和 1(每个分别对应于串行通信中使用的 RX 和 TX)。即使我们使用 USB 链路作为我们串行消息的物理支持,Arduino 板也是这样设计的,我们必须非常小心。

这是带有面包板的电路视图。我故意没有对齐每一根线。为什么?你不记得我想要你在阅读这本书后完全自主吗?是的,你会在现实世界中找到很多这样的电路图。你也必须熟悉它们。这可能是一个(简单的)家庭作业。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_017.jpg

之前的电路显示了三个开关、三个下拉电阻和 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。这个数字可以从 112 中的任何其他数字更改。这个数字代表当前连接到板上的开关数量,从数字引脚 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;
    }
  }
}

下面是使用五个开关并按下第四个按钮时此代码渲染的截图:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_020.jpg

那么,我改变了什么?

与 Arduino 代码中的概念相同,我添加了一个变量(不是一个常量),命名为 switchesNumber。一个很好的进化可能是向协议中添加有关开关数量的内容。例如,Arduino 板可以根据 Arduino 固件中定义的一个常量来通知 Processing 开关的编号。这将节省我们在更改此数字时手动更新 processing 代码的时间。

我还将变量 switchState 转换成了一个整数数组 switchesStates。这个数组存储了所有开关的状态。我添加了两个与显示相关的变量:distanceCirclesradii。这些用于根据开关的数量动态显示圆的位置。实际上,我们希望每个开关对应一个圆。

setup() 函数几乎和之前一样。

我在这里通过将画布宽度除以圆的数量来计算两个圆之间的距离。然后,我通过将它们之间的距离除以 2 来计算每个圆的半径。这些数字可以更改。你可以有非常不同的审美选择。

这里的大不同之处也是for循环。我在整个switchesStates数组中填充零以初始化它。一开始,没有任何开关被按下。现在的draw()函数也包括一个for循环。请注意这里。我移除了fillColor方法,因为我将填充颜色的选择移到了draw中。这是一个替代方案,向你展示代码的灵活性。

在同一个for循环中,我在绘制圆号i。我将让你自己检查我是如何放置这些圆圈的。serialEvent()方法也没有太多变化。正如我之前写的,我移除了填充颜色的变化。我还使用了switchesStates数组,以及存储在switchID中的消息的第一个字节的索引。

现在,你在 Arduino 板上上传了固件后,可以在每一侧运行代码。

魔法?我想你现在知道这根本不是魔法,而是美丽的,也许吧。

让我们进一步讨论关于开关的一些重要内容,同时也与其他开关相关。

理解去抖动概念

现在有一个小节,与模拟输入相比,它相当酷且轻巧,我们将在下一章深入探讨。

我们将讨论当有人按下按钮时发生的事情。

什么?谁在弹跳?

现在,我们必须用我们的微观生物控制论眼睛来放大开关的结构。

开关是由金属和塑料制成的。当你按下盖子时,一块金属移动并接触到另一块金属,闭合电路。在微观层面和非常短的时间间隔内,事情并不那么干净。实际上,移动的金属片会弹跳到另一部分。通过使用示波器测量 Arduino 数字引脚上的电势,我们可以在按下后大约 1 毫秒的电压曲线上看到一些噪声。

这些振荡可能会在某些程序中生成错误的输入。想象一下,你想要按顺序计数状态转换,例如,当用户按下开关七次时运行某些操作。如果你有一个弹跳系统,通过只按一次,程序可能会计数很多转换,即使用户只按了一次开关。

查看下一个图表。它表示电压与时间的关系。时间轴上的小箭头显示了开关被按下的时刻:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_05_18.jpg

我们如何处理这些振荡?

如何去抖动

我们有两个不同的元素,我们可以对其施加作用:

  • 电路本身

  • 固件

电路本身可以进行修改。我可以引用一些解决方案,例如添加二极管、电容器和一些施密特触发器反相器。我不会详细解释这个解决方案,因为我们将在软件中实现它,但我可以解释全局概念。在这种情况下,电容器将在开关弹跳时充电和放电,从而平滑噪声的峰值。当然,需要进行一些测试,以找到适合您精确需求的完美组件。

固件也可以进行修改。

基本上,我们可以使用基于时间的过滤器,因为弹跳发生在特定的时间段内。

下面是代码,然后是解释:

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 板上模拟输入概念的最基本电路:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_01.jpg

一个连接到 Arduino 板上的电位器

检查相应的电气图以了解连接:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_02.jpg

模拟输入 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 补丁:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_03.jpg

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。顺便提一下,其他图形化编程框架也使用这个术语。

如果一开始将这种范式理解为一种简化的方式,那么它并非首要目的,我的意思是,这不仅更容易,而且也为程序员和非程序员提供了全新的方法。它还提供了一种新的支持任务类型。实际上,如果我们编程的方式与修补不同,那么我们解决问题的方式也会不同。

我可以引用我们领域内的一些其他主要图形化编程软件:

我想特别提一下 Usine。这是一个非常有趣且强大的框架,它提供了图形化编程来设计可在 Usine 软件内部使用或作为独立二进制文件使用的补丁。但其中一个特别强大的功能是,你可以将你的补丁导出为功能齐全且经过优化的 VST 插件。VST虚拟工作室技术)是由 Steinberg 公司创建的一个强大的标准。它提供了一长串规范,并在几乎所有数字音频工作站中得到实现。Usine 提供了一个只需一键即可导出的功能,将你的图形化编程补丁打包成标准 VST 插件,这对于甚至没有听说过 Usine 或补丁风格的用户来说非常方便。Usine 独特的多点触控功能也使其成为一个非常强大的框架。然后,你甚至可以使用他们的 C++ SDK软件开发工具包)来编写自己的模块。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_04.jpg

Usine 大补丁连接现实世界与许多虚拟对象

Max,用于游乐场

Max 是游乐场和核心结构,所有内容都将放置在其中,进行调试和展示。这是放置对象、将它们连接起来、创建用户界面(UI)以及进行一些视觉渲染的地方。

这里有一个截图,展示了一个非常基本的补丁设计,旨在帮助你理解事物所在的位置:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_05.jpg

一个使用 Max 6 的小型简单计算系统补丁

正如我描述的那样,使用图形编程框架,我们不需要输入代码来让事情发生。这里,我只是触发了一个计算。

内部带有数字17的盒子是一个 numbox。它包含一个整数,它也是一个 UI 对象,提供了一种通过拖放鼠标来改变值的方式。然后你将一个对象的输出连接到另一个对象的输入。现在当你改变值时,它将通过电线发送到连接到 numboxes 的对象。魔法!

你看到了另外两个对象。一个带有:

  • +符号后面跟着数字5

  • -符号后面跟着数字3

每个对象都接收发送给它们的数字,并分别进行+ 5 和- 3 的计算。

你可以看到另外两个 numboxes,它们基本上显示了带有**+–**符号的对象发送的结果数字。

你还在吗?我猜是的。Max 6 提供了一个非常完善的帮助系统,其中包含了每个对象的全部引用,并且可以直接在 playground 中直接访问。当你教授这个框架时,告诉学生这一点是很好的,因为它真的有助于学生自学。确实,他们几乎可以自主地寻找答案,无论是关于小问题还是他们已经忘记但不敢问的事情。

Max 部分提供了一个相当高级的任务调度器,一些对象甚至可以修改优先级,例如,将deferdeferlow用于在您的补丁中实现优先级的精细粒度,例如,对于 UI 方面和计算核心方面,每个方面都需要非常不同的调度。

Max 还提供了一个方便的调试系统,它有一个类似于控制台的窗口,称为Max 窗口

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_06.jpg

显示 expr 对象错误调试信息的 Max 窗口

Max 驱动很多事情。实际上,是 Max 拥有并领导了对所有模块的访问,无论是激活的还是未激活的,当你创建新对象时提供自动完成,还提供了访问许多可以扩展 Max 功能的东西,例如:

  • JavaScript API 用于 Max 本身以及特定部分,例如 Jitter

  • 通过 mxj 对象在 Max 6 中直接实例化 Java 类

  • MSP 核心引擎用于与信号速率相关的一切,包括音频

  • Jitter 核心引擎用于与矩阵处理相关的一切,以及更多,例如视觉和视频

  • Gen 引擎用于从补丁中直接进行高效和即时的代码编译

这不是一个详尽的列表,但它让你了解了 Max 提供了什么。

让我们检查其他模块。

MSP,用于声音

在 Max 对象通过用户或调度器本身触发的消息进行通信时,MSP 是核心引擎,它在任何特定时刻计算信号,正如文档中所写。

即使我们可以像纯 Max 对象一样连接 MSP 对象,但背后的概念是不同的。在每一个时刻,都会计算一个信号元素,通过我们所说的信号网络形成一个几乎连续的数据流。信号网络在补丁窗口中很容易识别;线缆是不同的。

这里有一张非常简单的补丁图,在你的耳朵里产生基于余弦的音频波:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_07.jpg

事实上,甚至补丁线也有不同的外观,展现出酷炫的条纹状黄黑色,类似蜜蜂的颜色,MSP 对象的名称后面包含一个波浪线 ~ 作为后缀,象征着……当然是一波!

信号速率由音频采样率和 MSP 核心设置窗口中的某些暗参数驱动。我不会描述这些,但你需要知道,Max 通常默认提供与你的声卡相关的参数,包括采样率(44110 Hz,音频 CD 的标准采样率,意味着每个音频通道每秒以 44100 次的速度进行快速处理)。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_08.jpg

音频状态窗口是设置一些重要 MSP 参数的地方

Jitter,用于视觉效果

Jitter 是 Max 6 中与视觉处理和合成相关的所有事物的核心引擎。

它提供了一个非常高效的矩阵处理框架,最初是为快速像素值计算而设计的,用于显示图片,无论是有动画的还是没有的。

我们在谈论与 Jitter 处理矩阵相关的一切的矩阵计算。实际上,如果你需要在 Max 6 中触发快速计算大量数组,即使你不需要显示任何视觉效果,你也可以使用 Jitter 来做这件事。

Jitter 提供的不仅仅是矩阵计算。它提供了对 OpenGL (en.wikipedia.org/wiki/OpenGL) 实现的完全访问,该实现以光速运行。它还提供了一种设计和处理粒子系统、3D 世界、OpenGL 材质和基于物理的动画的方法。像素处理也是它提供的许多专为像素处理本身设计和优化的对象所具有的强大功能之一。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_09.jpg

基于 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.pixjit.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.pixjit.gl.pix非常非常快!

安装 Max 6

Max 6 作为一个 30 天的试用版可用。安装 Max 6 相当简单,因为它提供了 Windows 和 OS X 平台的安装程序,可在cycling74.com/downloads下载。下载并安装它。然后,启动它。就这样。(以下示例只有在安装了 Max 之后才会工作。)

你应该看到一个空白的游乐场

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_10.jpg

Max 6 的空白页面焦虑可能现在就会发生,不是吗?

第一个补丁

这里有一个基本的补丁,你可以在Chapter06/文件夹下以Patcher004_Arduino.maxpat的名称找到它。通常,如果你双击它,它会被 Max 6 直接打开。

这个补丁是一个非常基本的补丁,但实际上并不那么简单!

这是一个基本的基于噪声的序列发生器,它实时地定期修改振荡器的频率。这会产生一系列奇怪的声音,或多或少有点漂亮,频率的改变是由随机控制的。所以,打开你的扬声器,补丁将会产生声音。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_11.jpg

基于噪声的序列发生器

基本上,补丁是存储在文件中的。你可以非常容易地与其他朋友分享补丁。当然,更大的项目可能会涉及一些依赖性问题;如果你向 Max 6 框架中添加了一些库,如果你在补丁中使用它们,或者如果你基本上将补丁文件发送给一个没有安装这些库的朋友,你的朋友在 Max 窗口中将会出现一些错误。我不会在这里描述这类问题,但我想要提醒你。

在 Max 6 的世界中,分享补丁的其他整洁方式是复制/粘贴和复制压缩功能。确实,如果你在补丁器中选择对象(无论层次,包括子补丁器,子补丁器内的子补丁器,等等),然后转到编辑 | 复制,基于文本的内容就会被放入你的剪贴板。然后你可以将其粘贴到另一个补丁器或文本文件中。

最聪明的办法是使用复制压缩功能,正如其名字所暗示的,它复制并压缩 JSON 代码,使其变得更加紧凑,更容易复制到论坛上的文本区域,例如。

等一下,让我给你看看它是什么样子。

我只是选择了补丁中的所有对象,然后转到编辑 | 复制压缩

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_12.jpg

复制压缩功能

以下图是直接粘贴到文本文件中的结果。

熟悉 HTML 的人可能会注意到一些有趣的地方;Cycling '74 的开发者在 HTML 标签(precode)中包含了两项,以便直接提供可以在(任何)网络论坛上的文本字段中粘贴的代码。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_41.jpg

复制压缩代码

因此,你也可以将那段代码复制到你的剪贴板,并将其粘贴到一个新的补丁中。你可以通过访问文件 | 新建(或者在 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 将发送 0127 的值。紧接着 random,我放置了一个 zmap 对象,它像一个缩放器。我硬编码了四个参数,即输入的最小值和最大值以及输出的最小值和最大值。

基本上,在这里,zmaprandom 发送的值 0127 映射到 20100 的另一个值。它产生了一种隐式的拉伸和分辨率损失,这是我喜欢的。

然后,这个结果数值被发送到著名的且重要的 mtof 对象。它将 MIDI 音高标准转换为根据 MIDI 标准的频率。它通常用于从 MIDI 世界进入真实声音世界。你还可以在显示频率为浮点数(赫兹,频率的度量单位)的 UI 对象 flonum 中读取频率。

最后,这个频率被发送到 cycle~ 对象,产生一个信号(检查黄色和黑色条纹的线)。向这个对象发送数字会使其改变产生的信号的频率。这个信号乘以一个信号乘法运算符 *~,产生另一个信号,但幅度更低,以保护我们宝贵的耳朵。

该信号的最后一个目的地是你必须点击一次才能听到或听不到由上面的信号网络产生的声音的大灰色框。

现在,你可以准备检查复选框了。通过点击灰色框激活扬声器图标,然后你可以开始跳舞。实际上,产生的电子声音在频率(即音符)上有些混乱,但可能会很有趣。

当然,使用 Arduino 控制这个便宜的补丁,以便不使用鼠标/光标,将会非常棒。

让我们使用之前设计的相同电路来做这件事。

使用硬件控制软件

来自纯数字领域,其中一切都可以封装到软件和虚拟世界中,我们经常需要物理接口。这听起来可能像是一个悖论;我们希望一切都在一个地方,但那个地方对于与纯创造和情感相关的一切来说都太小,不够友好,因此我们需要更多或更少的大的外部(物理)接口。我喜欢这个悖论。

但是,为什么我们需要这样的接口呢?有时,旧鼠标和 QWERTY 键盘就不够用了。我们的电脑很快,但这些控制我们程序的接口却很慢,很笨拙。

我们需要在现实世界和虚拟世界之间建立接口。无论它们是什么,我们都需要它们专注于我们的最终目的,这通常不是接口,甚至不是软件本身。

亲自来说,我写书并教授与艺术相关的技术课程,但作为一个现场表演者,我需要专注于最终的渲染。在表演时,我希望尽可能地黑盒化底下的技术。我想要感受,而不是计算。我需要一个控制器接口来帮助我在速度和灵活性上操作,以便进行我想要的类型的变化。

正如我在这本书中已经说过的,我需要一个巨大的 MIDI 控制器,沉重、坚固且复杂,才能控制我电脑上的一个软件。因此,我建造了 Protodeck (julienbayle.net/protodeck)). 这就是我的接口。

那么,我们如何使用 Arduino 来控制软件呢?我想你已经有了一部分答案,因为我们已经通过旋转电位器将数据发送到我们的电脑。

让我们改进我们的 Max 6 补丁,使其在旋转电位器时接收 Arduino 的数据。

改进序列器和连接 Arduino

我们将创建一个非常便宜和基础的项目,该项目将涉及我们的 Arduino 板作为一个小型声音控制器。实际上,我们将直接使用我们刚刚设计的带有电位器的固件,然后我们将修改我们的补丁。这对于你继续构建事物甚至创建更大的控制器机器非常有用。

让我们连接 Arduino 到 Max 6

Arduino 可以使用串行协议进行通信。我们已经做到了。我们的最新固件已经做到了,发送电压值。

让我们稍作修改,使其只发送读取的模拟值,范围在 01023 之间。以下是代码,可在 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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_13.jpg

包含 Arduino 通信模块的 Max 补丁

双击文件,你会看到这个补丁。

让我们稍微描述一下。我添加了所有绿色和橙色的内容。

理解 Arduino 消息并将其转换为我们的序列器补丁易于理解的所有必要内容都在绿色部分。一些非常有用的辅助工具,能够在数据流中的每个步骤写入 Max 窗口,从原始数据到转换后的数据,都在橙色部分。

让我们描述这两部分,从辅助部分开始。

在 Max 6 中轻松追踪和调试

Max 6 提供了许多调试和追踪的方法。我不会在这本 Arduino 书中描述所有这些,但其中一些需要几句话说明。

检查你的补丁,特别是橙色部分的对象。

print 对象是直接向 Max 窗口发送消息的方式。一旦收到,发送给它们的任何内容都会立即写入 Max 窗口。你可以传递给这些对象的参数也非常有用;它有助于在您使用多个 print 对象的情况下区分哪个 print 对象发送了什么。这里就是这种情况,检查一下:我根据消息来源的对象命名所有的 print 对象:

  • fromSerial:这是针对来自 serial 对象自身的所有消息

  • fromZl:这是针对来自 zl 对象的所有消息

  • fromitoa:这是针对来自 itoa 对象的所有消息

  • fromLastStep:这是针对来自 fromsymbol 对象的所有消息

gate 对象只是小门,我们可以通过发送 10 到最左侧的输入来启用或禁用它们。toggle 对象是很好的 UI 对象,可以通过点击来实现这一点。一旦你勾选了 toggle,相关的 gate 对象将允许发送到右侧输入的消息通过它们传递到唯一的输出。

我们将在几分钟内使用这个追踪系统。

在 Max 6 中理解 Arduino 消息

需要理解的是,之前的切换现在也连接到了一个新的 qmetro 对象。这是低优先级的 metro 对应物。实际上,这个对象将每 20 毫秒轮询 serial 对象,考虑到我们的 Arduino 固件当前通过在循环的每次迭代中发送读取的模拟值来工作,即使轮询有点延迟,也不会有问题;下一次迭代,更新将会发生。

serial 对象在这里非常重要。

我硬编码了一些与 Arduino 串行通信相关的参数:

  • 9600 设置时钟为 9600 波特

  • 8 设置字长为 8 位

  • 1 表示有一个停止位

  • 0 表示没有奇偶校验(奇偶校验有时在错误检查中很有用)

这个对象需要被 bang 以提供串行端口缓冲区的当前内容。这就是为什么我用 qmetro 对象给它提供数据的原因。

serial 对象会弹出一系列原始值。在读取发送的模拟值之前,这些值需要被稍微解析和组织。这就是 selectzlitoafromsymbol 对象的作用。

注意

通过按键盘上的 Alt 键然后点击对象,直接读取 Max 6 中任何对象的帮助信息。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_14.jpg

串行对象的帮助补丁

每 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。这仅是一个内部引用。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_15.jpg

发送到串行对象的打印消息的结果:端口字母/串行端口的名称列表

让我们更改在补丁中作为serial对象参数的硬编码字母。

选择serial对象。然后,在内部重新单击并交换a与您计算机上 Arduino 串行端口的对应字母。一旦您按下Enter,对象就会以新的参数重新实例化。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_16.jpg

将序列对象中的参考字母更改为与 Arduino 的串行端口对应的字母

现在,一切准备就绪。检查切换,启用带有扬声器的灰色框,并转动您的电位器。您将听到来自序列器的奇怪噪音,现在您可以更改音符速率(我的意思是每个声音之间的间隔),因为我滥用术语音符以更好地适应序列器的通常定义。

究竟在电线上发送了什么?

您可能已经注意到,像往常一样,我提到了一系列对象:selectzlitoafromsymbol。现在是时候解释它们了。

当您在 Arduino 固件源代码中使用Serial.println()函数时,Arduino 不仅发送函数传递的参数值。检查一系列切换/门系统顶部的第一个橙色切换。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_17.jpg

串行对象弹出一系列奇怪的数字

您可以在名为对象的第一列中看到打印对象的名称,在消息列中,可以看到相关对象发送的消息。我们还可以看到serial对象以重复的方式弹出一系列奇怪的数字:5153481310,等等。

注意

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> 字符。如果没有结束标记,我怎么知道 350 将会是第一个字符呢?

需要记住的设计模式如下:

  • 构建消息

  • 在消息完全构建后发送消息(使用 Serial.println() 函数)。

如果你想在构建过程中发送它,这里是你可以使用的方法:

  • 使用 Serial.print() 发送第一个字节

  • 使用 Serial.print() 发送第二个字节

  • 继续发送直到结束

  • 使用不带参数的 Serial.println() 在末尾发送 <CR><LF>

仅提取有效载荷?

在许多与通信相关的领域,我们谈论有效载荷。这是消息,通信本身的目的。其他所有东西都非常重要,但可以理解为载体;没有这些信号和信号量,消息无法传播。然而,我们感兴趣的是消息本身。

我们需要解析来自串行对象的消息。

我们必须将每个 ASCII 码累积到同一个消息中,当我们检测到 <CR><LF> 序列时,我们必须弹出消息块,然后重新开始这个过程。

这是通过 selectzl 对象完成的。

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 缓冲区,也就是说,当一条新消息即将发送时。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_18.jpg

zl 列表处理器会弹出一系列整数

ASCII 转换和符号

我们现在有一系列三个整数,它们直接等于 Arduino 发送的 ASCII 消息,在我的情况下,是51 53 48

如果你旋转电位器,当然会改变这个系列。

但是看看这个,我们期望的 0 到 1023 之间的值在哪里?我们必须将 ASCII 整数消息转换为实际的字符。这可以通过使用itoa对象(表示整数到 ASCII)来完成。

检查相关的切换,并观察 Max 窗口。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_19.jpg

这里是我们的重要值

这个值是重要的;它是 Arduino 通过电线发送的消息,并以符号的形式传输。你无法在 Max 窗口中区分符号和其他类型的消息,如整数或浮点数。

我在补丁中放置了两个空消息。这些对于调试目的也非常有用。我将它们连接到右侧的itoafromsymbol对象。每次你向右侧输入的消息发送消息时,目标消息的值就会通过另一个消息的内容而改变。然后我们可以显示itoafromsymbol实际发送的消息。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_20.jpg

"350"并不完全等于 350

fromsymbol将每个符号转换为它的组成部分,在这里它组成一个整数,350

这个最终值是我们可以用任何能够理解和处理数字的对象使用的。这个值通过比例对象进行缩放,最后发送到 metro 对象。旋转电位器会改变发送的值,根据这个值,metro 会更快或更慢地发送 bang。

这个长例子教会了你两件主要的事情:

  • 你必须仔细了解发送和接收的内容

  • Arduino 的通信方式

现在,让我们继续探讨一些与模拟输入相关的一些其他示例。

与传感器玩耍

我不想在这本书中写一个大的目录。相反,我想给你提供钥匙和所有概念的感觉。当然,我们必须精确,并了解你没有发明过的特定技术,但我特别想让你学习最佳实践,自己思考大型项目,并能够有一个全局的视角。

我在这里给你举一些例子,但不会涵盖之前提到的所有类型的传感器。

测量距离

当我为他人或自己设计安装时,我经常有测量移动物体与固定点之间距离的想法。想象一下,你想要创建一个系统,其光线强度根据一些访客的接近程度而变化。

我曾经玩过一个 Sharp GP2Y0A02YK 红外长距离传感器。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_21.jpg

红外 Sharp GP2Y0A 系列传感器

这个酷炫的模拟传感器对于 20 到 150 厘米的距离提供了良好的结果。市场上还有其他类型的传感器,但我喜欢这个,因为它很稳定。

与任何距离传感器一样,目标/主题理论上必须垂直于红外光束的方向,以获得最大精度,但在现实世界中,即使不是这样也能正常工作。

数据表是首先要关注的对象。

阅读数据表?

首先,你必须找到数据表。搜索引擎可以帮上大忙。这个传感器的数据表在sharp-world.com/products/devvice/lineup/data/pdf/datasheet/gp2y0a02_e.pdf

你不必理解一切。我知道有些人会在这里责怪我没有解释数据表,但我想让我的学生对此放松。你必须过滤信息。

准备好了吗?让我们开始吧!

通常,在第一页上,你可以看到所有功能的总结。

在这里,我们可以看到这个传感器似乎在目标颜色方面相当独立。好的,很好。距离输出类型在这里非常重要。实际上,这意味着它直接输出距离,不需要额外的电路来利用其模拟数据输出。

常常有一些传感器所有尺寸的轮廓图。如果你想在订购之前确保传感器适合你的盒子或安装,这可能会非常有用。

在下一张图中,我们可以看到一个图表。这是一条曲线,说明了输出电压如何根据目标距离变化。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_22.jpg

传感器距离与模拟输出电压之间的数学关系

这些信息非常宝贵。确实,正如我们在上一章讨论的那样,传感器将一个物理参数转换成 Arduino(或任何其他类型的设备)可测量的东西。在这里,距离被转换成电压。

因为我们要用 Arduino 板上的模拟输入来测量电压,所以我们需要了解转换是如何工作的。在这里,我将使用一个捷径,因为我已经为你做了计算。

基本上,我使用了另一个与我们看到的类似的图表,但它是通过数学生成的。我们需要一个公式来编写我们的固件。

如果输出电压增加,距离会按照一种指数函数减少。我曾在某个时候与一些夏普工程师联系过,他们证实了我的关于公式的想法,并给了我这个:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_42.jpg

在这里,D 是厘米距离,V 是测量的电压;a = 0.008271,b = 939.65,c = -3.398,d = 17.339

这个公式将被包含在 Arduino 的逻辑中,以便它可以直接向任何想知道它的人提供距离。我们也可以在通信链的另一方进行这个计算,例如在 Max 6 补丁中,或者在 Processing 中。无论如何,你想要确保你的距离参数数据在比较传感器输出和将使用该数据输入时能够很好地缩放。

让我们连接东西

下一个电路会让你想起之前的那个。实际上,范围传感器替换了电位器,但它是以完全相同的方式连接的:

  • Arduino 板上的 Vcc 和地分别连接到+5 V 和地

  • 连接到模拟输入 0 的信号引脚

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_23.jpg

连接到 Arduino 板上的 Sharp 传感器

电路图如下:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_24.jpg

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 来的传感器值(即从 01023 的值)。然后,我需要存储从传感器值计算出的电压,当然,还有从电压值计算出的距离。

我只在 setup() 函数中初始化串行通信。然后,我在 loop() 方法中进行所有计算。

我首先读取从传感器引脚测量的当前 ADC 值和编码值。我使用这个值来计算电压,使用我们在之前的固件中已经使用过的公式。然后,我将这个电压值注入到 Sharp 传感器的公式中,我就得到了距离。

最后,我通过串行通信使用 Serial.println() 函数发送计算出的距离。

在 Max 6 中读取距离

Patcher006_Arduino.maxpat 是与这个距离测量项目相关的补丁。这里就是它:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_25.jpg

距离读取补丁

正如我们之前学到的,这个补丁包含了读取来自 Arduino 板的消息的整个设计模式。

这里唯一的新奇之处是底部的奇怪 UI 元素。它被称为滑块。通常,滑块用于控制事物。确实,当你点击并拖动滑块对象时,它会弹出值。它看起来像调音台或调光器的滑块,可以控制某些参数。

显然,因为我想要在这里传输大量数据,所以我使用这个滑块对象作为显示设备,而不是控制设备。实际上,滑块对象也有一个输入端口。如果你向滑块发送一个数字,滑块会接受它并更新其内部当前值;它也会传输接收到的值。我这里只使用它作为显示。

Max 6 中的每个对象都有其自己的参数。当然,很多参数对所有对象都是通用的,但也有一些不是。为了检查这些参数:

我不会描述所有参数,只描述底部的两个。为了产生相关结果,我必须将来自fromsymbol对象的值进行缩放。我知道 Arduino 传输的值范围(尽管这可能需要一些个人验证),我已经从 Sharp 数据表中计算了它们。我将这个范围视为 20 到 150 厘米。我的意思是 20 到 150 之间的一个数字。

我将这个范围进行了压缩和转换,使用scale对象将其转换为浮点数的0-to-100范围。我为我的滑块对象选择了相同的范围。这样做,滑块显示的结果是一致的,并代表真实值。

我没有在滑块上写任何增量标记,只做了两个注释:“近”和“远”。在这个数字的世界里,这有点诗意。

让我们看看其他一些能够弹出连续电压变化的传感器的例子。

测量弯曲

柔性传感器也非常有用。在距离传感器能够将测量的距离转换为电压的地方,柔性传感器测量弯曲并提供电压。

基本上,设备的弯曲与一个能够根据弯曲量使电压变化的可变电阻相关。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_27.jpg

只有两个连接器的标准柔性传感器

柔性传感器可用于许多用途。

我喜欢用它通过 Arduino 通知计算机我设计的数字安装中的门位置。最初人们只想知道门是打开还是关闭,但我提出使用柔性传感器,并获得了关于开启角度的非常准确的信息。

下图说明了传感器的工作原理:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_28.jpg

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_29.jpg

柔性传感器连接到 Arduino 板上的下拉电阻

我添加了一个下拉电阻。如果你还没有阅读关于上拉和下拉电阻的第五章,使用数字输入进行感应,我建议你现在去阅读。

通常,我使用大约 10K Ω的电阻,它们工作得很好。

电路图如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_30.jpg

柔性传感器及其下拉电阻连接到 Arduino

电阻计算

对于这个项目,我不会给你代码,因为它与上一个项目非常相似,只是计算公式不同。我想在这里讨论的是这些电阻计算公式。

如果我们没有 Sharp 公司慷慨提供的红外传感器图,我们该怎么办?我们必须求助于一些计算。

通常,柔性传感器文档提供了当它未弯曲和当它弯曲到 90 度时的电阻值。让我们假设一些常见的值,如 10K Ω和 20K Ω,分别。

对于这些电阻值,包括下拉电阻,我们可以期望的电压值是什么?

考虑到电气原理图,模拟引脚 0 的电压是:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_45.jpg

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_43.jpg

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

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_44.jpg

这意味着我们找到了我们的电压值范围。

我们现在可以将这些数据转换为数字 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 的模拟输入。你会怎么做?

我们将学习如何复用信号。

复用概念

复用在电信世界中相当常见。复用定义了提供有效方式让多个信号共享单一介质的技巧。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_31.jpg

基本复用概念展示了共享介质

这种技术提供了一个非常有帮助的概念,其中你只需要一个共享介质来带来许多信息通道,正如我们在前面的图中可以看到的那样。

当然,这涉及到复用(在图中称为 mux)和解复用(demux)过程。

让我们深入探讨一下这些过程。

多种复用/解复用技术

当我们需要复用/解复用信号时,我们基本上需要找到一种方法,通过我们可以控制的物理量来分离它们。

我至少可以列出三种复用技术类型:

  • 空分复用

  • 频分复用

  • 时分复用

空分复用

这是最容易理解的。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_32.jpg

空分复用将所有电线物理地聚集到同一个地方

例如,这个概念是您公寓中的基本电话网络复用。

你的电话线,就像你邻居的电话线一样,所有这些线都被连接到一个屏蔽的大多对电缆中,例如,包含你居住的整个建筑中的所有电话线。这条巨大的多对电缆进入街道,将其作为一个全局电缆捕获比捕获来自你邻居的每根电缆加上你自己的电缆要容易。

这个概念很容易转化为 Wi-Fi 通信。确实,今天一些 Wi-Fi 路由器提供了不止一个 Wi-Fi 天线。例如,每个天线都能够处理一个 Wi-Fi 连接。每一次通信都会使用相同的介质:空气传输电磁波。

频分复用

这种复用技术在所有与 DSL 和有线电视连接相关的事物中都非常常见。

服务提供商可以使用这种技术通过同一根电缆提供多个服务。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_33.jpg

频分复用与传输频率和带宽玩游戏

想象一下图中的123频率波段是三种不同的服务。1 可能是语音,2 可能是互联网,3 是电视。现实与这并不太远。

当然,我们在一端复用的东西,我们必须在另一端解复用,以便正确地处理我们的信号。我不会尝试将电视调制的信号转换为语音,但我猜这不会是一次很有成效的经历。

时分复用

这是我们将要深入挖掘的情况,因为这是我们将在 Arduino 上用于多路复用的信号。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_34.jpg

用一个四步周期的例子说明时分复用

依次,多路复用器和多路分解器之间只有一条通道被完全用于第一个信号,然后是第二个,以此类推,直到最后一个。

这种系统通常涉及一个时钟。这有助于为每个参与者设置正确的周期,以便他们知道我们在通信的哪个步骤。保持通信的安全性和完整性至关重要。

串行通信就是这样工作的,并且由于许多原因——即使你在前面的章节中认为你已经了解了很多——我们将在下一章中更深入地探讨它们。

让我们来检查一下如何处理我们的 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 封装。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_35.jpg

CD4051B DIP 封装版本

这是这个小巧的集成电路的正面。数据表在互联网上很容易找到。这里有一个来自德州仪器的:

www.ti.com/lit/ds/symlink/cd4051b.pdf

我在下一张图中重新绘制了全局封装。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_36.jpg

带有所有引脚描述的 CD4051B 原理图

识别引脚编号 1

很容易找出哪个是引脚编号 1。按照标准,其中一个角落引脚前面刻有一个小圆圈。这就是引脚编号 1。

也有一个半圆形的小孔。当你将 IC 放置在这个半圆形在顶部(如图中所示)时,你就知道哪个是引脚编号 1;紧挨着引脚 1 的第一个引脚是引脚 2,以此类推,直到左列的最后一个引脚,在我们的例子中是引脚 8。然后,继续与左列最后一个引脚相对的引脚;这是引脚 9,下一个引脚是引脚 10,以此类推,直到右列的顶部。

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_37.jpg

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 组合将输入切换到公共输出。

下表描述了组合:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_38.jpg

CD4051B 的真值表

换句话说,这意味着如果我们向 Arduino 上对应于 A 的数字输出写入 1,对应于 B 的写入 1,对应于 C 的写入 0,则切换的输入将是第三个通道。

当然,这里有一些好处。如果你读取对应于 C、B 和 A(按此顺序)的输入的二进制数,你会有一个惊喜;它将等同于公共输出切换的输入引脚的十进制数。

的确,二进制的 0 0 0 等于十进制的 0。参考表格以获取十进制数的二进制值:

0 0 00
0 0 11
0 1 02
0 1 13
1 0 04
1 0 15
1 1 06
1 1 17

这里是我们如何连接东西的方法:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_39.jpg

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

以下图是电气图:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/c-prog-ardn/img/7584_06_40.jpg

电气图

我们希望用这个系统读取的所有设备都应该连接到 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 模拟输入提供连续电压变化的传感器时,我们学到了一些反射技巧。

然后,我们还发现了一个非常重要的技术,即复用/解复用技术。

我们将在下一章关于串行通信的章节中讨论它。既然我们已经花费了很多时间,现在我们将更深入地探讨这种通信类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值