放学时段,中山路门口的信号灯可能会遇到大量的穿行人群,信号灯的设计逻辑需要改变,能否设计一款可以人工干预的信号灯呢?即在高峰时段,即使中山路本身是绿灯,也可以被人工中断,从而让行人过街。

按钮开关

按键一共有4个引脚,下图分别显示了正面与背面。下图也说明了按键的工作原理。一旦按下后,左右两侧就被导通了,而上下两端始终导通。
image.pngimage.png
使用按钮开关的目的是为Arduino板提供一个高电压的信号或低电压的信号。但是我们可以直接将按钮开关加在Arduino板的IO口和电源之间吗?
下拉电阻.jpg
可以从上面两张图看到,第一张是未接电阻的电路,按键没被按下时, input引脚就处于一个悬空状态。空气会使该引脚电压产生浮动,不能确保是0V。然而第二张是接了下拉电阻的电路,当没被按下时,输入引脚通过电阻接地,确保为0V,不会产生电压浮动现象。

参考资料:开关的类型

开关是指一个可以使电路开路、使电流中断或使其流到其他电路的电子元件。最常见的开关是让人操作机器装置或下达命令的手动操作开关,其中有一个或数个电子接点。接点的“闭合”(close)表示电子接点导通,允许电流流过;开关的“开路”(open)表示电子接点不导通形成开路,不允许电流流过。
image.png
三种常见的按钮式开关
最简单的开关有二片名叫“接点”的金属,二接点接触时使电流形成回路,二接点不接触时电流开路。选用接点金属时需考虑其对抗腐蚀的程度,因为大多数金属氧化后会形成绝缘的氧化物,使接点无法正常工作。选用接点金属也需考虑其电导率、硬度、机械强度、成本及是否有毒等因素。有时会在接点上电镀抗腐蚀金属。一般会镀在接点的接触面,以避免因氧化物而影响其性能。有时接触面也会使用非金属的导电材料,如导电塑胶。
开关中除了接点之外,也会有可动件使接点导通或不导通,开关可依可动件的不同为分为杠杆开关(toggle switch)、按键开关、船型开关(rocker switch)、旋转开关、侧拨开关等,而可动件也可以是其他型式的机械连杆。另有非机械式做线路导通的例如电容式触摸开关、电阻式开关、压电开关、光遮断开关、磁簧开关、霍尔开关等。

image.png image.png image.png
船型开关 拨动开关 圆形船型开关

实例7:人工干预信号灯

【实验说明】
使用面包板和杜邦线,利用程序代码,点亮一个普通LED。
【材料准备】
已经通电的UNO板,470电阻一只,发光二极管一只,杜邦线两根。
【马上行动】

1.硬件连接

image.png

2.程序设计

  1. /*可人工干预的信号灯
  2. */
  3. int carRed = 12; //设置汽车灯
  4. int carYellow = 11;
  5. int carGreen = 10;
  6. int button = 9; //按钮引脚
  7. int pedRed = 8; //设置行人灯,ped是pedestrian(行人)的头三个字母
  8. int pedGreen = 7;
  9. int crossTime = 5000; //允许行人通过的时间
  10. unsigned long changeTime; //按钮按下后的时间
  11. void setup() {
  12. //所有LED设置为输出模式
  13. pinMode(carRed, OUTPUT);
  14. pinMode(carYellow, OUTPUT);
  15. pinMode(carGreen, OUTPUT);
  16. pinMode(pedRed, OUTPUT);
  17. pinMode(pedGreen, OUTPUT);
  18. pinMode(button, INPUT); //按钮设置为输入模式
  19. digitalWrite(carGreen, HIGH); //开始时,汽车灯绿灯
  20. digitalWrite(pedRed, LOW); //行人灯为红灯
  21. }
  22. void loop() {
  23. int state = digitalRead(button);
  24. //检测按钮是否被按下,并且是否距上次按下后有5秒的等待时间
  25. if(state == HIGH && (millis() - changeTime)> 5000){
  26. //调用变灯函数
  27. changeLights();
  28. }
  29. }
  30. void changeLights() {
  31. digitalWrite(carGreen, HIGH); //汽车绿灯灭
  32. digitalWrite(carYellow, LOW); //汽车黄灯亮
  33. delay(2000); //等待2秒
  34. digitalWrite(carYellow, HIGH); //汽车黄灯灭
  35. digitalWrite(carRed, LOW); //汽车红灯亮
  36. delay(1000); //为安全考虑等待1秒
  37. digitalWrite(pedRed, HIGH); //行人红灯灭
  38. digitalWrite(pedGreen, LOW); //行人绿灯亮
  39. delay(crossTime); //等待一个通过时间
  40. //闪烁行人灯绿灯,提示可过马路时间快到
  41. for (int x=0; x<10; x++) {
  42. digitalWrite(pedGreen, LOW);
  43. delay(250);
  44. digitalWrite(pedGreen, HIGH);
  45. delay(250);
  46. }
  47. digitalWrite(pedRed, LOW); //行人红灯亮
  48. delay(500);
  49. digitalWrite(carRed, HIGH); //汽车红灯灭
  50. digitalWrite(carYellow, LOW); //汽车黄灯亮
  51. delay(1000);
  52. digitalWrite(carYellow, HIGH); //汽车黄灯灭
  53. digitalWrite(carGreen, LOW); //汽车绿灯亮
  54. changeTime = millis(); //记录自上一次灯变化的时间
  55. //返回到主函数循环中
  56. }

条件判断语句

image.png
if语句是一种条件判断的语句,判断是否满足括号内的条件,如满足则执行花括号内的语句,如不满足则跳出if语句。表达式是指我们的判断条件,通常为一些关系式或逻辑式,也可是直接表示某一数值。如果if表达式条件为真则执行if中的语句。表达式条件为假,则跳出if语句。我们代码中,第一个条件是state变量为H I GH。如果按键被按下,state就会变为HIGH。第二个条件是millis()的值减changeTime的值大于5000。这两个条件之间有个“&&”符号。这是一种逻辑运算符,表示的含义是两者同时满足。

millis()函数

该函数是Arduino语言自有的函数,它返回值是一个时间,Arduino开始运行到执行到当前的时间,也称之为机
器时间,就像一个隐形时钟,从控制器开始运行的那一刻起开始计时, 以毫秒为单位。变量changeTime初始化时,不存储任何数值, 只有在Arduino运行之后,将millis()值赋给它,它才开始有数值,并且随着millis()值变化而变化。通过millis()函数不断记录时间,判断两次按键之间的时间是不是大于5秒,如果在5秒之内不予反
应。这样做的目的是,防止重复按键而导致的运行错误。

参考资料:时间函数

除了mills()函数和之前学习过的delay()函数外,Arduino还提供2种不同的时间操作函数,一共四种。它们是:

序号 函数和描述
1 delay()函数
delay()函数的工作方式非常简单。它接受单个整数(或数字)参数。此数字表示时间(以毫秒为单位)。
2 delayMicroseconds()函数
delayMicroseconds()函数接受单个整数(或数字)参数。一毫秒内有一千微秒,一秒内有一百万微秒。
3 millis()函数
此函数用于返回Arduino板开始运行当前程序时的毫秒数。
4 micros()函数
micros()函数返回Arduino板开始运行当前程序时的微秒数。该数字在大约70分钟后溢出,即回到零。

逻辑运算符

前面说到的&&是一个逻辑运算符,常用的逻辑运算符有:
image.png

参考资料:运算符

运算符是一个符号,它告诉编译器执行特定的数学或逻辑函数。C语言具有丰富的内置运算符,并提供以下类型的运算符:

  • Arithmetic Operators 算术运算符
  • Comparison Operators 比较运算符
  • Boolean Operators 布尔运算符
  • Bitwise Operators 位运算符
  • Compound Operators 复合运算符

    算术运算符

    假设变量A为10,变量B为20,则:
运算符名称 运算符简写 描述 例子
赋值运算符

| = | 将等号右侧的值存储在等号左边的变量中。 | A = B | | 加号 | + | 两个操作数相加 | A + B将得出30

| | 减号 | - | 从第一个操作数中减去第二个操作数 | A - B将得出-10

| | 乘号 | | 将两个操作数相乘 | A B将得出200

| | 除号 | / | 用分母除分子 | B / A将得出2

| | 模数 | % | 模数运算符和整数除后的余数 | B % A将得出0 |


比较运算符

假设变量A为10,变量B为20,则:

| 运算符名称 | 运算符简写

描述 例子
等于 == 检查两个操作数的值是否相等,如果相等,则条件为真(true)。 (A == B)不为真
不等于 != 检查两个操作数的值是否相等,如果值不相等,则条件为真。 (A != B)为真
小于 < 检查左操作数的值是否小于右操作数的值,如果是,则条件为真。 (A < B)为真
大于 > 检查左操作数的值是否大于右操作数的值,如果是,则条件为真。 (A > B)不为真
小于或等于 <= 检查左操作数的值是否小于或等于右操作数的值,如果是,则条件为真。 (A <= B)为真
大于或等于 >= 检查左操作数的值是否大于或等于右操作数的值,如果是,则条件为真。 (A >= B)不为真

布尔运算符

假设变量A为10,变量B为20,则:

| 运算符名称

| 运算符简写

描述 例子
and(与) && 称为逻辑运算符与。如果两个操作数都是非零,那么条件为真。 (A && B)为真
or(或) || 称为逻辑运算符或。如果两个操作数中的任何一个是非零,则条件为真。 (A || B)为真
not(非) ! 称为逻辑运算符非。用于反转其操作数的逻辑状态。如果条件为真,则逻辑运算符非将为假。 !(A && B)为假


位运算符

假设变量A为60,变量B为13,则:

| 运算符名称

| 运算符简写

描述 例子
and(与) & 如果同时存在于两个操作数中,二进制AND运算符复制一位到结果中。 (A & B)将得出12,即0000 1100
or(或) | 如果存在于任一操作数中,二进制OR运算符复制一位到结果中。 (A | B)将得出61,即0011 1101
xor(异或) ^ 如果存在于其中一个操作数中但不同时存在于两个操作数中,二进制XOR运算符复制一位到结果中。 (A ^ B)将得出49,即0011 0001
not(非) ~ 二进制NOT运算符是一元运算符,具有”翻转”位效果。 (〜A)将得出-60,即1100 0011
shift left(左移)

| << | 二进制左移运算符。左操作数的值向左移动右操作数指定的位数。 | A<< 2将得出240,即1111 0000 | | shift right(右移)

| >> | 二进制右移运算符。左操作数的值向右移动右操作数指定的位数。 | A>> 2将得出15,即0000 1111 |

复合运算符

假设变量A为10,变量B为20,则:

| 运算符名称

| 运算符简写

描述 例子
自增 ++ 自增运算符,将整数值增加1 A++ 将得出11

| | 自减 | — | 自减运算符,将整数值减1 | A— 将得出9 | | 复合加

| += | 加且赋值运算符。把右边操作数加上左边操作数的结果赋值给左边操作数。 | B += A 等效于 B = B + A

| | 复合减

| -= | 减且赋值运算符。把左边操作数减去右边操作数的结果赋值给左边操作数。 | B -= A等效于B = B - A | | 复合乘

| = | 乘且赋值运算符。把右边操作数乘以左边操作数的结果赋值给左边操作数。 | B = A等价于B = B * A | | 复合除

| /= | 除且赋值运算符。把左边操作数除以右边操作数的结果赋值给左边操作数。 | B /= A等效于B = B / A | | 复合模数

| %= | 求模且赋值运算符。 求两个操作数的模赋值给左边操作数 | B %= A等效于B = B % A | | 复合按位或 | |= | 按位或且赋值运算符 | A |= 2与A = A | 2相同 | | 复合按位与

| &= | 按位与且赋值运算符 | A &= 2与A = A & 2相同 |

Serial.print()函数

除了闪烁之外,如何通过读秒的方式提醒行人注意绿灯的剩余时间呢?
image.png
请你试着在以下区域中,添加以下函数,观察效果。

  1. /*此处为局部代码,请参考实例5的人工干预信号灯代码运行*/
  2. //闪烁行人灯绿灯,提示可过马路时间快到
  3. for (int x=0; x<10; x++) {
  4. digitalWrite(pedGreen, LOW);
  5. delay(250);
  6. digitalWrite(pedGreen, HIGH);
  7. delay(250);
  8. Serial.print(x);//新加入的内容
  9. }

使用Serial.print()函数,可以将信号发送到串口上,如果串口连接着电脑的USB接口,我们可以通过Arduino IDE上面的串口监视器,监视电脑的串口接收到了什么信号。
image.png
【注意】如果显示“串口不可用”,多半是忘记连接USB线了,请检查USB线或者驱动程序是否安装。
Serial.print()函数不仅可以输出x这个局部变量的值,也可以输出形如“请不要闯红灯”、“Watch Out!”之类的字符串,但输出字符串时,请务必加引号,如下方程序代码所示:

  1. /*此处为局部代码,请参考实例5的人工干预信号灯代码运行*/
  2. //闪烁行人灯绿灯,提示可过马路时间快到
  3. Serial.print("道路千万条,安全第一条!");
  4. for (int x=0; x<10; x++) {
  5. digitalWrite(pedGreen, LOW);
  6. delay(250);
  7. digitalWrite(pedGreen, HIGH);
  8. delay(250);
  9. Serial.print("剩余时间:");
  10. Serial.print(x);
  11. }

在Arduino的程序设计中,诸如Serial.print()的函数还有很多,有的源于Arduino自带的库函数,也有的是爱好者们自己添加的库函数。如果想了解Arduino基础的库函数及其用法,可以参考Arduino官方网站中的参考资料:
https://www.arduino.cc/reference/en/language/functions/communication/serial/print/
尽管网站为英文,但是对于高一同学来说,并非不可阅读。因为关于函数的参考资料有规定的格式,这也是未来我们阅读程序开发者文档的一般模式。

|

Description

|

Syntax

|

Parameters

|

Returns

|

Example Code

| | —- | —- | —- | —- | —- | | 描述 | 语法 | 参数 | 返回值 | 样例代码 | | 在这里给出了Serial.print的基本描述 | Serial.print(val)

Serial.print(val, format) | Serial: 串行端口对象
val: 需要”打印”的值,允许任意类型的数据 | print() 返回已经发送的字节的值,数据类型: size_t. | 一个完整的样例代码 |

参考资料:字符串

字符串用于存储文本。它们可用在LCD或Arduino IDE串口监视器窗口中显示文本。字符串也可用于存储用户输入。例如,用户在连接到Arduino的键盘上键入的字符。
在Arduino编程中有两种类型的字符串:

  • 字符数组,与C编程中使用的字符串相同。
  • Arduino字符串,它允许我们在草图中使用字符串对象。

在本章中,我们将学习Arduino草图中的字符串,对象和字符串的使用。在本章末尾,你将学习在草图中使用哪种类型的字符串。
字符串字符数组
我们要学习的第一种类型的字符串是 char 类型的一系列字符。在前面的章节中,我们学习了一个数组是什么:存储器中存储的相同类型的变量的连续序列。一个字符串是一个char变量的数组。
字符串是一个特殊的数组,在字符串的末尾有一个额外的元素,其值总是为0(零)。这被称为“空终止字符串”。
字符串字符数组示例
此示例将显示如何创建字符串并将其打印到串口监视器窗口。

  1. void setup() {
  2. char my_str[6]; // an array big enough for a 5 character string
  3. Serial.begin(9600);
  4. my_str[0] = 'H'; // the string consists of 5 characters
  5. my_str[1] = 'e';
  6. my_str[2] = 'l';
  7. my_str[3] = 'l';
  8. my_str[4] = 'o';
  9. my_str[5] = 0; // 6th array element is a null terminator
  10. Serial.println(my_str);
  11. }
  12. void loop() {
  13. }

以下示例显示了字符串由什么组成。一个具有可打印字符的字符数组和0作为数组的最后一个元素,表示这是字符串结束的位置。通过使用 Serial.println()并传递字符串的名称,可以将字符串打印到Arduino IDE串口监视器窗口。
同样的例子可以用更方便的方式编写,如下所示:

  1. void setup() {
  2. char my_str[] = "Hello";
  3. Serial.begin(9600);
  4. Serial.println(my_str);
  5. }
  6. void loop() {
  7. }

在这个草图中,编译器计算字符串数组的大小,并自动使用空值0终止字符串。一个长度为六个元素长,由五个字符后跟一个零组成的数组,其创建方式与上一个草图完全相同。
操作字符串数组
我们可以在草图中更改字符串数组,如下图所示。

  1. void setup() {
  2. char like[] = "I like coffee and cake"; // create a string
  3. Serial.begin(9600);
  4. // (1) print the string
  5. Serial.println(like);
  6. // (2) delete part of the string
  7. like[13] = 0;
  8. Serial.println(like);
  9. // (3) substitute a word into the string
  10. like[13] = ' '; // replace the null terminator with a space
  11. like[18] = 't'; // insert the new word
  12. like[19] = 'e';
  13. like[20] = 'a';
  14. like[21] = 0; // terminate the string
  15. Serial.println(like);
  16. }
  17. void loop() {
  18. }

结果

  1. I like coffee and cake
  2. I like coffee
  3. I like coffee and tea
  • (1)创建和打印字符串

在上面给出的草图中,创建了一个新的字符串,然后打印出来显示在串口监视器窗口中。

  • (2)缩短字符串

通过用空终止0替换字符串中的第14个字符来缩短字符串。这是从0开始计算的字符串数组中的13号元素。
打印字符串时,所有字符都打印到新的空终止0。其他字符不消失;它们仍然存在于内存中,并且字符串数组仍然是相同的大小。唯一的区别是任何使用字符串的函数只能看到第一个空终止符前的字符串。

  • (3)更改字符串中的单词

最后,草图用“tea”代替“cake”一词。它首先必须用空格替换空终止符,如[13],以便将字符串恢复为原来的格式。
新字符用单词“tea”覆盖单词“cake”的“cak”。这是通过覆盖单个字符来完成的。“cake”的“e”被替换为新的空终止字符。结果是字符串实际上终止于两个空字符,即字符串末尾的原始字符,以及替换“cake”中的“e”的新字符。这在打印新字符串时没有区别,因为打印字符串的函数在遇到第一个空终止字符时将停止打印字符串字符。
操作字符串数组的函数
上一个草图通过访问字符串中的单个字符,以手动方式操作字符串。为了更方便操作字符串数组,你可以编写自己的函数来执行,也可以使用 C 语言库中的一些字符串函数。
下面显示了操作字符串数组的列表函数。
下一个草图使用了一些C字符串函数。

  1. void setup() {
  2. char str[] = "This is my string"; // create a string
  3. char out_str[40]; // output from string functions placed here
  4. int num; // general purpose integer
  5. Serial.begin(9600);
  6. // (1) print the string
  7. Serial.println(str);
  8. // (2) get the length of the string (excludes null terminator)
  9. num = strlen(str);
  10. Serial.print("String length is: ");
  11. Serial.println(num);
  12. // (3) get the length of the array (includes null terminator)
  13. num = sizeof(str); // sizeof() is not a C string function
  14. Serial.print("Size of the array: ");
  15. Serial.println(num);
  16. // (4) copy a string
  17. strcpy(out_str, str);
  18. Serial.println(out_str);
  19. // (5) add a string to the end of a string (append)
  20. strcat(out_str, " sketch.");
  21. Serial.println(out_str);
  22. num = strlen(out_str);
  23. Serial.print("String length is: ");
  24. Serial.println(num);
  25. num = sizeof(out_str);
  26. Serial.print("Size of the array out_str[]: ");
  27. Serial.println(num);
  28. }
  29. void loop() {
  30. }

结果

  1. This is my string
  2. String length is: 17
  3. Size of the array: 18
  4. This is my string
  5. This is my string sketch.
  6. String length is: 25
  7. Size of the array out_str[]: 40

以上草图按以下方式工作。

  • (1)打印字符串

最新创建的字符串将打印到串口监视器窗口,如之前的草图所完成的。

  • (2)获取字符串的长度

strlen()函数用于获取字符串的长度。字符串的长度仅对于可打印字符,不包括空终止符。
该字符串包含17个字符,因此我们在串口监视器窗口中看到17个字符。

  • (3)获取数组的长度

运算符sizeof()用于获取包含字符串的数组的长度。长度包括空终止符,因此长度比字符串的长度多1。
sizeof()看起来像一个函数,但技术上是一个运算符。它不是C字符串库的一部分,但在草图中用于显示数组大小和字符串大小(或字符串长度)之间的差异。

  • (4)复制字符串

strcpy()函数用于将str[]字符串复制到out_num[]数组。strcpy()函数将传递给它的第二个字符串复制到第一个字符串中。现在,字符串的副本存在于out_num[]数组中,但只占用了数组的18个元素,因此在数组中仍然有22个空闲的char元素。这些空闲元素在内存中的字符串的后面可以找到。
将字符串复制到数组中,以便我们在数组中有一些额外的空间用于草图的下一部分,即在字符串的末尾添加一个字符串。

  • (5)将字符串附加到字符串(连接)

草图将一个字符串加到另一个字符串,这称为串联。这是使用strcat()函数完成的。strcat()函数将传递给它的第二个字符串放到传递给它的第一个字符串的末尾。
串联后,打印字符串的长度以显示新的字符串长度。然后打印数组的长度,以显示在40个元素长的数组中有一个25个字符长度的字符串。
请记住,25个字符长度的字符串实际上占用了数组的26个字符,因为还有空终止0。
数组边界
使用字符串和数组时,在字符串或数组的边界内工作是非常重要的。在示例草图中,创建了一个长度为40个字符的数组,以分配可用于操作字符串的内存。
如果数组太小,而我们尝试复制比数组大的字符串,那么字符串将复制到超出数组的末尾。超出数组末尾的内存可能包含草图中使用的其他重要数据,然而它们将被字符串覆盖。如果超出字符串末尾的内存超出范围,则可能会导致草图崩溃或导致意外行为。

练习(LED综合操作)

image.png