读《Arduino程序设计基础》

概述

这篇文章主要记录了读陈吕洲著的《Arduino程序设计基础》之后,记录的笔记,包含使用arduino开发硬件的一些基础知识点。

关于本书

《Arduino程序设计基础(第二版)》,涵盖了 Arduino 基础知识和高级应用,中间穿插简单的项目,同时列举了常用的 API 参考。本书主要针对本科生及研究生阶段的 Arduino 教学实验进行编写,也适用于相关开发人员及入门者学习。

作者

陈吕洲:Arduino 中文社区创始人,曾今的机器人竞赛选手。现从事3D打印机设计与开发,业余从事开源硬件开发与推广。

笔记

基础篇

Arduino 语言

通常所说的 Arduino 语言是指 Arduino 核心库文件提供的各种程序接口 (API) 的集合。这些API 是对更底层单片机支持库进行二次封装形成的,为我们屏蔽了底层复杂的实现机制,使得更多的人能够掌握,同时也让程序更容易阅读,也提升了开发效率。在 Arduino 中的核心库采用 C/C++ 混合编写,我们写Arduino 程序时也使用 C/C++ 完成。

Arduino 程序结构

在 Arduino 程序中由两个基本函数构成,setup() 函数只会在程序运行之初运行一次,一般用于初始化设置,如配置 I/O 口状态和初始化串口等操作;loop() 函数在 setup() 函数执行完成后开始执行,随后重复运行。

C/C++ 语言基础
数据类型
  • 常量与变量

    • 常量 在程序运行过程中,值不能改变的量称为常量,常量可以是字符,也可以是数字,常量通常使用 #define 常量名 常量值 ,常量名一般习惯用大写字母表示。

    • 变量 在程序运行过程中可变的值称为变量。定义方法是 类型 变量名 ,可以在定义变量时为其赋值,也可以再定义之后赋值,例如:

      1
      int i = 1;

      1
      2
      int i;
      i = 1;

      是等价的。

  • 数据类型

    • 整形 整数类型。

    • 浮点型 即为平时说的实数,小数。在 arduino 中有 float 和 double 两种浮点类型,但在使用 AVR 作为控制核心的 Arduino (UNO、MEGA等)上,两者精度是一样的,都占 4 字节(32位)存储空间。在 Arduino Due 中,double 类型占 8 字节(64位)存储空间。

      浮点型数据计算较慢且有一定误差,因此,通常将浮点型转换成整形来处理,如9.8cm,通常会转换成 98mm来计算。

    • 字符型 即 char 类型,占 1 字节存储空间,主要用于存储字符变量。在存储字符时需要用单引号引用,如 char col = 'C'

      字符都是以数字形式存储在 char 类型变量中的,数字与字符的对应关系可以参照 ASCII 码表。

    • 布尔型 即 boolean 类型,占 1 个字节存储空间,值只有两个:false(假)和 true(真)。

运算符
  • 算术运算符

    运算符 说明
    = 赋值
    +
    -
    *
    /
    % 取模
  • 比较运算符

    运算符 说明
    == 等于
    != 不等于
    < 小于
    > 大于
    <= 小于等于
    >= 大于等于
  • 逻辑运算符

    运算符 说明
    && 逻辑与
    || 逻辑或
    ! 逻辑非
  • 复合运算符

    运算符 说明
    ++ 自加
    自减
    += 复合加
    -= 复合减
表达式

用运算符将运算对象连接起来的式子称为表达式,如 1 + 2a - b5 > 3 等。

数组

数组是一组相同类型的数据构成的集合。定义方式为 数据类型 数组名称[元素个数]; ,如定义一个有 5 个元素的整形数组:int a[5];

访问数组中的元素时可以使用 数组名称[下标] ,需要注意的是数组下标是从 0 开始的,因此访问数组中的第一个元素时,下标是 0 。如将数组第一个元素赋值为 1 的代码:a[0] = 1;

可以使用下标给数组元素进行赋值,也可以在定义数组时进行赋值,如:

1
int a[5] = {1, 2, 3, 4, 5};
字符串

字符串的定义有两种方式,一种是字符数组,另一种是使用 String 来定义。

以字符数组方式定义的语句为 char 字符串名称[字符个数]; ,用法与数组的用法一致,有多少个字符就用多少字节的存储空间。

大多数情况下使用 String 类型来定义字符串,该类型提供了一些操作字符串的成员函数,是的字符串使用起来更加灵活。定义语句为:String 字符串名称; ,可以在定义时进行赋值,也可以在定义后进行赋值。如

1
2
3
String str = "abc";
String str2;
str2 = "abc";

相较于数组形式的定义方法,使用 String 类型定义字符串会占用更多的存储空间,毕竟 String 定义的是一个对象。

注释

// 之间的内容以及 // 之后的内容都是程序注释,不会被编译到程序中,因此不影响程序运行。

I/O 口的简单应用
数字 I/O 口的使用

数字信号 是以 0 和 1 表示的不连续信号,也就是二进制形式的信号。在 Arduino 中数字信号以高低电平来表示,高电平为数字 1 ,低电平为数字 0 。

Arduino 上每个带有数字编号的引脚都是数字引脚,包括写有 A 编号的模拟输入引脚。用这些引脚可以完成输入/输出数字信号的功能。

引脚模式 使用数字引脚时,需要先设置引脚的模式,使用 pinMode 函数设置指定引脚的工作模式:pinMode(pin,mode); 参数 pin 为引脚编号,参数 mode 为引脚工作模式,有如下 3 种工作模式:

模式名称 说明
INPUT 0 输入模式
OUTPUT 1 输出模式
INPUT_PULLUP 2 输入上拉模式

可以用模式的值,代替模式名称,如 pinMode(1, OUTPUT);pinMode(1, 1); 是等价的。

引脚状态输出 当设置引脚为输出模式(OUTPUT)后,可以使用 **digitalWrite **函数使该引脚输出高电平或低电平:digitalWrite(pin, value); ,参数 pin 为引脚编号,value 为要指定的输出电平,使用 HIGH 指定输出高电平,使用 LOW 指定输出低电平。

状态
HIGH 1
LOW 0

Arduino 中输出的低电平为 0V,高电平为工作电压,如 Arduino UNO 的工作电压为 5V,所以其高电平输出为 5V。

引脚状态读取 当设置引脚为输入模式(INPUT)后,可以使用 digitalRead 函数读取引脚输入信号:digitalRead(pin); ,参数 pin 为要读取状态的引脚编号。

当 Arduino 以 5V 供电时,会将范围为 -0.5~1.5V 的输入电压作为低电平识别,将范围在 3~5.5V 的输入电压作为高电平识别。所以即使电压不太准确,Arduino 也可以正确识别,但需要注意太高的电压可能会损坏 Arduino。

LED引脚 大多数 Arduino 控制板上,13 号引脚都连接了一个标有 L 的 LED 灯,在没有外部的 LED 灯时,可以使用内嵌(LED_BUILTIN)的 LED 做实验。

Arduino 按键控制 LED 当未按下按键时,2 号引脚检测到的输入电压为低电平,当按下按键时,会导通 2 号引脚与 VCC ,此时检测到的输入电压为高电平,程序以此来判断按键是否被按下,从而控制 LED 是否点亮。

"Arduino按键控制LED电路图"

限流电阻 一般的 LED 最大能承受的电流为 25mA,直接接入 5V 的 Arduino 电路中容易烧坏,因此需要在 LED 一端串联一个电阻 R2(220Ω),这样做可以减小流过 LED 的电流,防止 LED 损坏,这个电阻称为限流电阻。

下拉电阻 在 Arduino 控制器的 2 号引脚到 GND 之前,连接了一个阻值很大(10kΩ)的电阻 R1。如果没有该电阻,当未按下按键时,2 号引脚一直处于悬空状态,此时使用 digitalRead() 函数读取引脚状态会得到一个不稳定的值(可能是高,也可能是低),添加这个电阻到 GND 就是为了稳定引脚的电平,当该引脚悬空时,就会识别成低电平。这种将某节点通过电阻接地的做法叫做下拉,这个电阻称为下拉电阻。

上拉电阻 同下拉电阻一样,上拉电阻也可以稳定 I/O 口的电平,不同的是上拉电阻连接到 VCC 上,并将引脚稳定在高电位,这种电阻称为上拉电阻。在 Arduino 中可以使用内部上拉电阻 pinMode(pin, INPUT_PULLUP); ,以此替代外部的上拉电阻。

稳定悬空引脚电平所用的电阻应该尽量选择阻值较大的,一般使用 10kΩ 的电阻。

模拟 I/O 口的使用

模拟信号 生活中接触到的大多数信号都是模拟信号,如声音和温度的变化等。模拟信号是用连续变化的物理量来表示信息的,信号随时间做连续变化。在 arduino 中常用 0~5V 电压来表示模拟信号。

模拟输入引脚 在 Arduino 控制器上,编号前带有 A 的引脚是模拟输入引脚,可以读取这些引脚上输入的模拟值,即读取引脚上输入的电压大小。模拟输入引脚是带有 ADC(Analog-to-Digital Converter 模/数转换器)功能的引脚,可以将外部输入的模拟信号转换成芯片运行时可以识别得数字信号,从而实现读入模拟值的功能。

使用 AVR 芯片作为主控器的 Arduino 模拟输入功能有 10 位精度,即可将 0~5V 的电压转换成 1~1023 的整数形式表示。

读取模拟输入信号 使用 analogRead() 函数读取模拟输入信号:analogRead(pin); ,参数 pin 是要读取模拟值的引脚,被指定的引脚必须是模拟输入引脚,如:analogRead(A0); 为读取 A0 引脚上的模拟值。

输出模拟信号 使用 analogWrite() 函数实现模拟输出功能,但该函数并不是输出真正意义上的模拟值,二是以一种特殊的方式来达到输出模拟值的效果,这种方式叫做 PWM

当使用 analogWrite() 函数时,指定引脚会通过高低电平不断转换来输出一个周期固定(约 490Hz)的方波,通过改变高低电平在每个周期中所占比例(占空比),而得到近似输出不同电压的效果。需要注意,这里只是得到近似模拟值输出的效果,如果要输出真正的模拟值,还需要加上外围滤波电路。

analogWrite 函数的用法是:analogWrite(pin, value); ,其中参数 pin 是要输出 PWM 波的引脚,value 是 PWM 的脉冲宽度,范围是 0~255。

PWM引脚 大多数 Arduino 控制器的 PWM 引脚都会用 ~ 标识,不同型号的 Arduino 对应不同位置和不同数量的 PWM 引脚。

串口

在 Arduino 控制器上,串口都是位于 0(RX)和 1(TX)的两个引脚,Arduino 的 USB 口通过一个转换芯片(通常是 ATmega16u2)与这两个串口引脚连接。该转换芯片会通过 USB 接口在计算机上虚拟出一个用于与 Arduino 通信的串口。

初始化串口 要使串口与计算机通信,需要先使用 Serial.begin(speed) 函数初始化 Arduino 的串口通信功能,其中参数 speed 为串口通信波特率,是设定串口通信速率的参数。串口通信双方必须使用同样的波特率才能进行正常通信。

波特率 是一个衡量通信速度的参数,表示每秒传送的 bit 的个数。例如 9600 波特率表示每秒发送 9600 bit 的数据。Arduino 常用以下波特率:300、600、1200、2400、4800、9600、14400、19200、28800、38400、57600、115200。

串口输出 初始化串口通信后,可以使用 Serial.print(val)Serial.println(val) 函数向计算机发送信息,其中参数 val 是要输出的数据,各种类型数据均可,第二个函数输出完指定数据后,会再输出一组回车换行符。

串口输入 初始化串口通信后,可以使用 Serial.read() 函数读取串口数据。调用该语句,每次都会返回 1 字节的数据,返回值就是当前串口读到的数据。当串口缓冲区没有数据时,Serial.read() 函数会返回 int 型值 -1 ,对应的 char 型数据是乱码。

在使用串口时,Arduino 会在 SRAM 中开辟一段大小为 64B 的空间,串口接收到的数据都会被暂存在该空间中,被称为缓冲区。当调用 Serial.read() 函数时,Arduino 就会从缓冲区中取出 1B 的数据。

通常在使用串口读取数据时,需要搭配使用 Serial.available() 函数,返回值是当前缓冲区中接收到的数据字节数。可以搭配 if 或者 while 语句来使用,先检测缓冲区中是否有可读数据,如果有数据再读取,如果没有数据则跳过或等待。如:

1
2
3
4
5
6
7
if(Serial.available() > 0){
// 如果缓冲区有数据...
}
// 或者
while(Serial.available() > 0){

}

在进行串口通信时,Arduino 控制器上标有 RX 和 TX 的 2 个 LED 灯会闪烁提示。当接收数据时,RX 灯会点亮;当发送数据时,TX 灯会点亮。

时间控制函数

运行时间函数 使用 millis()micros() 能够获取 Arduino 从通电(或复位)到现在的时间。millis() 函数返回值是 unsigned long 类型,单位是毫秒,大概 50 天会溢出一次。micros() 函数返回值为 unsigned long 类型,单位是微秒,大概 70 分钟会溢出一次。

时间精度 在使用 16MHz 晶振的 Arduino 上,精度为 4 微秒;在使用 8MHz 晶振的 Arduino 上,精度为 8 微秒。

延时函数 使用 delay()delayMicroseconds() 函数可以暂停程序,并可通过参数来设定延时时间。delay(time) 函数是毫秒级延迟,参数 time 类型是 unsigned long。delayMicroseconds(time) 函数是微秒级延迟,参数类型是 unsigned int。

I/O 口高级应用

调声函数

调声函数 tone() 主要用于 Arduino 连接蜂鸣器或扬声器发声的场合,其实质是输出一个评率可调的方波,以此驱动蜂鸣器或扬声器震动发声。

tone()

能够让指定引脚产生一个占空比为 50% 的指定频率的方波。

语法:

1
2
tone(pin, frequency);
tone(pin, frequency, duration);

参数:

  • pin 需要输出方波的引脚
  • frequency 输出的频率,为 unsigned int 类型
  • duration 频率持续时间,单位为毫秒。如果没有这个参数,Arduino 将持续发出设定的音调,直到改变了发声频率或使用 noTone() 函数停止发声

返回值:无

tone()analogWrite() 函数都可以输出方波,不同的是:tone() 函数输出的方波占空比固定(50%),所调节的是方波的频率;而 analogWrite() 函数输出的频率固定(约为 490Hz),所调节的是方波的占空比。

使用 tone() 函数会干扰 3 号和 11 号引脚的 PWM 输出功能(Arduino MEGA 控制器除外),并且同一时间的 tone() 函数仅能作用于一个引脚,如果有多个引脚需要使用 tone() ,则必须先使用 noTone() 函数停止之前已经使用了 tone() 函数的引脚,再使用 tone() 函数开启下一个指定引脚的方波输出。

noTone()

停止指定引脚上的方波输出。

语法:noTone(pin)

参数:pin 需要停止方波输出的引脚

返回值:无

无源蜂鸣器

无源蜂鸣器需要外部震荡源,即一定频率的方波,不同频率的方波输入,会产生不同的音调。

脉冲宽度测量函数
pulseIn()

检测指定引脚上的脉冲信号宽度。

当要检测高电平脉冲时,pulseIn() 函数会等待指定引脚输入的电平变高,在变高后开始计时,直到输入电平变低时,计时停止。

pulseIn() 函数会返回此脉冲信号的持续时间,即该脉冲的宽度。

pulseIn() 函数还可以设定超时时间,如果超过指定时间仍未检测到脉冲,则会退出 pulseIn() 函数并返回 0。如果没有设置超时时间,默认超时时间为 1 秒。

语法:

1
2
pulseIn(pin, value);
pulseIn(pin, value, timeout);

参数:

  • pin 需要读取脉冲的引脚
  • value 需要读取的脉冲类型,为 HIGHLOW
  • timeout 超时时间,单位为微秒,数据类型为 unsigned long

返回值:脉冲宽度,单位为微秒,数据类型 unsigned long。如果测量超时返回 0。

超声波测距

超声波是频率超过 20000Hz 的声波,它指向性强,能量消耗缓慢,在介质中传播距离较远,因而经常用于测量距离。

时间差测距法原理:超声波发射器向某一方向发射超声波,在发射时开始计时;超声波在空气中传播,途中碰到障碍物则立即返回,超声波接收器收到反射波则立即停止计时。声波在空气中的传播速度为 340m/s ,根据计时器记录的时间 t,即可计算出发射点与障碍物之间的距离 s,即 s= 340 * t / 2

SR04 超声波传感器

SR04 超声波模块有 4 个引脚:

引脚名称 说明
Vcc 电源 5V
Trig 触发引脚
Echo 回馈引脚
Gnd

使用说明:

  • 使用 Arduino 的数字引脚给 SR04 模块的 Trig 引脚至少 10μs 的高电平信号,触发 SR04 模块的测距功能。
  • 触发测距功能后,模块会自动发送 8 个 40kHz 的超声波脉冲,并自动检测是否有信号返回,这一步由模块内部自动完成。
  • 如有信号返回,则 Echo 引脚会输出高电平,高电平持续时间就是超声波从发射到返回的时间。此时可以用 pulseIn() 函数获取测距的结果,并计算出与被测物体的实际距离。
设置 ADC 参考电压

使用 analogRead() 函数读取模拟输入口的电压时,函数返回值的计算方式为:

analogRead(pin)y=V1V0×1023analogRead(pin)y = \frac{V_1}{V_0}\times1023

  • y 为函数返回值
  • V1 为检测电压
  • V0 为参考电压

当用户没有设置参考电压时,Arduino 会默认使用工作电压为参考电压。大多数 Arduino 控制器工作电压为 5V,所以默认参考电压也是 5V。

当要测量的电压较小时或对测量精度要求较高时,可以通过降低参考电压来使测量结果更精准。Arduino 提供了内部参考电压,但内部参考电压并不准确,如果使用的话反而会使精度降低。在实际应用中,一般通过输入高精度的外部参考电压来提高检测精度。

在 Arduino 控制器上有一个 AREF 引脚,可以从该引脚给 Arduino 输入外部参考电压,同时需要使用 analogReference() 函数来设置 Arduino 使用外部参考电压。

语法:annalogReference(type)

参数:type 为参考电压类型,可选值如下:

选项 说明
DEFAULT 默认当前 Arduino 工作电压为参考电压
INTERNAL 使用内部参考电压(当使用 UNO 时为 1.1V,当使用 Atmega8 时为 2.56V),该设置并不适用于 Arduino MEGA
INTERNAL1V1 使用内部 1.1V参考电压
INTERNAL2V56 使用内部 2.56V 参考电压
EXTERNAL 使用从 AREF 引脚输入的外部参考电压

外部输入电压必须大于 0,且小于当前工作电压,否则可能会损坏 Arduino 控制器。

外部中断

程序运行过程中时常需要监控一些事件的发生,如对某一传感器的检测结果做出反应。使用轮询的方式进行检测时效率较低,等待时间较长,而使用中断方式进行检测则可以达到实时检测的效果。

中断程序可以看做是一段独立于主程序之外的程序,当中断被触发时,控制器会暂停当前正在运行的主程序,而跳转去运行中断程序;当中断程序运行完后,会再回到之前主程序暂停的位置,继续运行主程序。如此便可收到实时响应处理事件的效果。

外部中断是由外部设备发起请求的中断,要使用外部中断,就需要了解中断引脚的位置,根据外部设备选择中断模式,以及编写一个中断被触发后需要执行的中断函数。

中断引脚与中断编号

不同型号的 Arduino 控制器上,中断引脚的位置也不相同,只有中断信号发生在带有外部中断功能的引脚上,Arduino 才能捕获到该中断信号并作出响应。

型号 int0 int1 int2 int3 int4 int5
UNO 2 3 - - - -
MEGA 2 3 21 20 19 18
Leonardo 3 2 0 1 - -

表中的 int0 、int1 等都为外部中断的编号。

Arduino Due 所有引脚都可以使用外部中断,其中断编号就是引脚编号。

中断模式

为了设置中断模式,还需要了解设备触发外部中断的输入信号类型。中断模式也就是中断触发的方式。

模式名称 说明
LOW 低电平触发
CHANGE 电平变化触发,即高电平变低电平、低电平变高电平
RISING 上升沿触发,即低电平变高电平
FALLING 下降沿触发,即高电平变低电平

在 Arduino Due 中,还可以使用高电平(HIGH)来触发中断。

中断函数

除了设置中断模式,还需要编写一个响应中断的处理程序——中断函数,当中断被触发后,可以让 Arduino 运行该中断函数。中断函数就是当中断触发后要去执行的函数,该函数不能带有任何参数,且返回类型为空。如:

1
2
3
void Hello(){
Serial.println("hello");
}
配置中断引脚

需要在 setup() 中使用 attachInterrupt() 函数对中断引脚进行初始化配置,以开启 Arduino 的外部中断功能。

1
attachInterrupt(interrupt, function, mode);

参数:

  • interrupt 中断编号,注意这里的中断编号不是引脚编号
  • function 中断函数名,当中断发生后会运行此函数名称所代表的中断函数
  • mode 中断模式

例如:attachInterrupt(0, Hello, FALLING);

关闭中断功能

detachInterrupt(interrupt)

禁用外部中断

参数:interrupt 要禁用的中断编号

使用和编写类库

使用 Arduino 类库

要想提高代码编写效率及程序可读性,可以使用他人编写好的类库。可以在网上下载类库文件,将其解压后放到 Arduino IDE 所在文件夹中的 libraries 文件夹内,就可以在编写程序时调用它们。

比较好的类库中还附带了示例代码,可以直接在 Arduino IDE 的 “文件” → “示例” 菜单中找到示例代码。

使用他人编写的类库时,首先需要声明包含的类库,然后调用类库中的函数。如 :

1
2
3
4
5
6
// 声明程序会使用 SR04 类库
#include "SR04.h"
// 调用 SR04 构造函数创建一个 SR04 类型的对象,赋值给 sr04 变量
SR04 sr04 = SR04(2, 3);
// 调用对象的成员函数
float distance = sr04.Get();

Arduino 还有很多第三方的类库可以使用,在 Github.comArduino.cc 等开源社区可以找到更多的类库。

编写 Arduino 类库

以下以 SR04 为例,编写一个 SR04 类库,首先创建一个文件夹,为 SR04

编写头文件(接口)

首先需要建立一个名叫 SR04.h 的头文件,在 SR04.h 文件中需要声明一个 SR04 超声波类。

类的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 防止重复包含文件
#ifndef SR04_H
#define SR04_H

// 使编译器自动判断正在使用的 IDE 版本,从而调用正确的头文件
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif

// 类定义
class SR04{
// 公共
// 需要外部访问的函数与变量放到public中定义
public:
SR04(int TrigPin, int EchoPin); // 构造函数,需要与类同名且不能有返回类型,一般放到 public 中,用于初始化对象
float Get(); // 用于处理超声波传感器返回的信息

// 私有
// 一些程序运行过程中使用到的函数或变量,用户在使用时并不会接触它们,因此可以放到 private 中定义
private:
int Trig_pin; // 触发引脚
int Echo_pin; // 回馈引脚
float distance; // 距离
};

通常一个类包含两个部分,public 中声明的函数和变量可以被外部程序访问,private 中声明的函数和变量只能在这个类的内部访问。

预处理命令

# 开头的语句称为预处理命令,包含文件使用的 #include 及在常量定义时使用的 #define 均为预处理命令。

预处理命令并不是 C/C++ 语言的组成部分,编译器不会直接对齐进行编译,而是在编译之前,系统会预先处理这些命令。

宏定义

宏定义是使用一个特定的标识符来代表一个字符串,在实际编译前,系统会将代码中所有的宏定义进行字符串替换,在对替换后的代码进行编译。

宏定义的一般形式为:

1
#define 标识符字符串

在Arduino 中,经常使用到的 HIGHLOWINPUTOUTPUT 等参数以及圆周率 PI 等常量都是通过宏的方式定义的。

文件包含

若程序中使用 #include 语句包含一个文件,如 #include "EEPROM.h" ,那么在预处理时系统会将该语句替换成 EEPROM.h 文件中的实际内容,然后在对替换后的代码进行编译。

文件包含命令的一般形式为: #include <文件名>#include "文件名"

两种形式实际效果是一样的,只是使用 <文件名> 形式时,系统会优先在 Arduino 库文件中寻找目标文件,若没找到,再到当前 Arduino 项目的项目文件夹中查找;而使用 "文件名" 的形式时,系统会优先在 Arduino 项目文件夹中查找目标文件,若没有找到,再查找 Arduino 库文件。

条件编译

为了防止重复的包含某文件,避免程序出错,可以使用条件编译命令,判断文件是否在程序其它位置被 #define 定义过,没被定义过则定义该标识符。如:

1
2
3
#ifndef 标识符
程序段
#endif
版本兼容

为了增加类型在不同版本 Arduino IDE 中的兼容性,可以使用添加编译预处理命令。如

1
2
3
4
5
#if 表达式
程序段1
#else
程序段2
#endif

在 Arduino IDE 1.0 之前的版本中, Arduino 核心库文件使用的主要函数声明的头文件为 WProgram.h ,而在 Arduino IDE 1.0 之后的版本中,核心库文件使用的主要函数声明头文件为 Arduino.hARDUINO 为系统变量,其中保存了该 IDE 的版本号。

编写 .cpp 文件(实现类)

建立一个 SR04.cpp 文件,在这个文件中需要写出头文件中声明的成员函数的具体代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 使编译器自动判断正在使用的 IDE 版本,从而调用正确的头文件
#if defined(ARDUINO) && ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif

#include "SR04.h"

SR04::SR04(int TP, int EP){
pinMode(TP, OUTPUT);
pinMode(EP, INPUT);
Trig_pin=TP;
Echo_pin=EP;
}

float SR04::Get(){
digitalWrite(Trig_pin, LOW);
delayMicroseconds(2);
digitalWrite(Trig_pin, HIGH);
delayMicroseconds(10);
digitalWrite(Trig_pin, LOW);
float distance = pulseIn(Echo_pin, HIGH)/58.00;
return distance;
}

.cpp 文件中也必须包含需要用到的头文件。

在编写 SR04 类库时,在 SR04.h 文件中声明 SR04 类及其成员函数,在 SR04.cpp 文件中定义其成员函数的实现方法。当在类声明以外定义成员函数时,需要使用域操作符 :: 来说明该函数作用于 SR04 类。

关键字高亮显示

为了让 Arduino IDE 识别并能够高亮显示关键字,需要建立一个 keywork.txt 文件,并写入如下代码:

1
2
SR04	KEYWORD1
Get KEYWORD2

需要注意:SR04KEYWORD1GetKEYWORD2 之间的空格应该用键盘上的 Tab 键输入。

在 Arduino IDE 的关键字高亮中,会将 KEYWORD1 识别为 数据类型高亮方式,将 KEYWORD2 识别为函数高亮方式。

示例程序

为了方便其他人学习和使用你编写的类库,还可以在 SR04 文件夹中新建一个 examples 文件夹,并放入你提供的示例程序。

通信篇

Arduino 与外部设备的通信都是串行通信,因为 并行通信占用的 I/O 口较多,而 Arduino 的 I/O 口资源较少,所以更常用的是串行通信方式。Arduino 硬件集成了串口、IIC、SPI 三种常见的通信方式。

串口通信

串口,也称 UART 接口,通过将 Arduino 上的 USB 接口与计算机连接实现串口通信,还可以使用串口引脚连接其他的串口设备进行通信。需要注意,通常一个串口只能连接一个设备进行通信。

在进行串口通信时,两个串口设备间需要发送端(TX)与接收端(RX)交叉相连,并共用电源地(GND)。

串口工作原理

Arduino 与其他期间通信过程中,数据传输实际上都是以数字信号(即高低电平变化)的形式进行的,串口通信也是如此。当使用 Serial.print() 函数输出数据时,Arduino 发送端会输出一连串的数字信号,称为数据帧。

"Serial.print(A)"

  • 起始位 起始位总为低电平,是一组数据帧开始传输的信号。
  • 数据位 是一个数据包,其中承载了实际发送的数据段。当 Arduino 通过串口发送一个数据包时,实际数据可能不是 8 位的,比如 标准的 ASCII 码是 0~127 (7位),而扩展的 ASCII 码是 0~255(8位)。如果数据使用简单文本(标准 ASCII),那么每个数据包将使用 7 位数据。Arduino 默认使用 8 位数据位,即每次可以传输 1B 数据。
  • 校验位 是串口通信中一种简单的验错方式。可以设置为偶校验或奇校验。没有校验位也可以,Arduino 默认无校验位。
  • 停止位 每段数据帧的最后都有停止位,表示该段数据帧传输结束。停止位总为高电平,可以设置停止位为 1 位或 2 位。Arduino 默认是 1 位停止位。

当串口通信速率较高或外部干扰较大时,可能会出现数据丢失的情况。为了保证数据传输的稳定性,最简单的方式就是降低通信波特率或增加停止位和校验位。在 Arduino 中,可以通过 Serial.begin(speed, config) 语句配置串口通信的数据位、停止位、校验位参数。

HardwareSerial 类库成员函数

HardwareSerial 类库位于 Arduino 核心库中,默认包含了该类,因此不用 include 进行调用。

函数 功能 语法 参数 返回值
availab() 获取串口缓冲区中的字节数。最大为 64 Serial.abailable() 可读取的字节数
begin() 初始化串口。可配置串口的各项参数 Serial.begin(speed) , Serial.begin(speed, config) speed 波特率,config 数据位、校验位、停止位配置
end() 结束串口通信 Serial.end()
find() 从串口缓冲区读取数据,直到读到指定的字符串 Serial.find(target) target 要搜索的字符串或字符 boolean型,true 表示找到,false 表示未找到
findUntil() 从串口缓冲区读取数据,直到读到指定的字符串或指定的停止符 Serial.findUntil(target,terminal) target 要搜索的字符串或字符,terminal 停止符
flush() 等待正在发送的数据发送完成。注意 Serial.flush()
parseFloat() 从串口缓冲区返回第一个有效的 float 型数据 Serial.parseFloat() float 型数据
parseInt() 从串口流中查找第一个有效的整形数据 Serial.parseInt() int 型数据
peek 返回 1 字节的数据,但不会从缓冲区删除该数据 Serial.peek() 缓冲区中第 1 字节的数据;如果没有可读数据则返回 -1
print() 将数据输出到串口。 Serial.print(val) , Serial.print(val,format) val 要输出的数据,format 分两种情况,①输出进制形式:BIN (二进制)、DEC (十进制)、OCT (八进制)、HEX (十六进制);②指定输出的 float 型数据的小数位数(默认输出 2 位) 输出的字节数
println() 将数据输出到串口,并回车换行 Serial.println(val) , Serial.println(val,format) 同上 ↑ 输出的字节数
read() 从串口读取 1 字节数据,并删除该数据 Serial.read() 串口缓冲区第 1 个字节;如果没有可读数据则返回 -1
readBytes() 从缓冲区读取指定长度的数据并将其存入一个数组中。超时会自动退出该函数 Serial.readBytes(buffer,length) buffer 用于存储数据的数组(char[]byte[]),length 需要读取的字符长度 读到的字节数;没有有效数据则返回 0
readBytesUntil() 从缓冲区读取指定长度数据并存入数组。如果遇到停止符或超时则退出该函数 Serial.readBytesUntil(character,buffer,length) character 停止符,buffer 存数据的数组(char[]byte[]),length 要读取的字符长度 同上 ↑
setTimeout() 设置超时时间,用于设置 readBytesUntil()readBytes() 函数的超时时间 Serial.setTimeout(time) time 超时时间,单位:毫秒
write() 输出数据到串口,以字节形式输出 Serial.write(val) , Serial.write(str) , Serial.write(buf,len) val 发送的数据,str String类型数据,buf 数组类型数据,len 缓冲区长度 输出的字节数

print()write() 的区别

当使用 Serial.print() 发送一个数据时,Arduino 发送的并不是数据本身,而是将数据转换成字符,再将字符对应的 ASCII 码发送出去,串口监视器收到 ASCII 码,则会显示对应的字符。因此使用 print() 函数是以 ASCII 码形式输出数据到串口。

使用 write() 函数时,Arduino 发送的数值本身,但串口监视器收到数据后,会将数组当做 ASCII 码而显示其对应的字符。因此使用 Serial.write(123) 时,显示的 ASCII 码中 123 对应的字符为 {

read()peek() 输入方式的差异

串口接收到数据都会放到缓冲区,使用 read()peek() 函数都是从缓冲区中读取数据。不同的是,当使用 read() 函数读取数据后,会将该数据从缓冲区移除;使用 peek() 读取数据时,不会移除缓冲区中的数据。

######串口通信可用的 config 配置

可用配置 数据位 校验位 停止位
SERIAL_5N1 5 1
SERIAL_6N1 6 1
SERIAL_7N1 7 1
SERIAL_8N1 8 1
SERIAL_5N2 5 2
SERIAL_6N2 6 2
SERIAL_7N2 7 2
SERIAL_8N2 8 2
SERIAL_5E1 5 1
SERIAL_6E1 6 1
SERIAL_7E1 7 1
SERIAL_8E1 8 1
SERIAL_5E2 5 2
SERIAL_6E2 6 2
SERIAL_7E2 7 2
SERIAL_8E2 8 2
SERIAL_5O1 5 1
SERIAL_6O1 6 1
SERIAL_7O1 7 1
SERIAL_8O1 8 1
SERIAL_5O2 5 2
SERIAL_6O2 6 2
SERIAL_7O2 7 2
SERIAL_8O2 8 2
串口读取字符串

当使用 read() 函数时,没吃只能读取一个字节数据,如果要读取一个字符串,可以使用 += 运算将字符依次添加到字符串中。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void setup(){
Serial.begin(9600);
}

void loop(){
String inString = "";
while(Serial.available()>0){
char inChar = Serial.read();
inString += (char) inChar;
delay(10); // 这个延迟至关重要
}

if(inString != ""){
Serial.print("Input String: ");
Serial.println(inString);
}
}

在上述程序中有一个延时语句 delay(10); 至关重要,删除之后受到的字符串会被拆成单个字符输出。原因是 Arduino 程序运行速度很快,当 Arduino 读完第一个字符,进入下一次循环时,输入的数据还没有完全传进 Arduino 串口缓冲区,串口还未受到下一个字符,此时 Serial.available() 返回值是 0 ,而 Arduino 将在下一次 loop() 循环中才检查到下一个字符。

串口事件

在 Arduino 1.0 版本中新增了 serialEvent() 事件,是从 Processing 串口通信库中提取的函数。在 Arduino 中,serialEvent() 并非真正意义上的事件,无法做到实时响应。

serialEvent() 事件的功能是:当串口接收缓冲区中有数据时,会触发该事件。用法是定义一个 void serialEvent(){} 函数,定义之后就启用了该事件。当串口缓冲区中存在数据时,该函数就会运行。

需要注意,这里的 serialEvent() 事件并不能立即做出响应,而仅仅是一个伪事件,其实是在两次 loop() 循环之间检测串口缓冲区中是否有数据,如果有数据则调用 serialEvent() 函数。

串口缓冲区

Arduino 串口缓冲区容量默认为 64 字节,当数据超过 64 字节后,Arduino 会将最早存入缓冲区的数据丢弃。

通过宏定义的方式可以增大串口读写缓冲区的空间,Arduino 核心库中串口发送缓冲区宏定义名为 SERIAL_TX_BUFFER_SIZE ,串口接收缓冲区宏名为 SERIAL_RX_BUFFER_SIZE 。通过如下方式可以设定串口发送/接收缓冲区各为 128 字节:

1
2
3
4
#define SERIAL_TX_BUFFER_SIZE 128
#define SERIAL_RX_BUFFER_SIZE 128
void setup(){}
void loop(){}

缓冲区实际上是在 Arduino 的 RAM 上开辟临时存储空间。因此在设定缓冲区时,大小不能超过 Arduino 本身 RAM 的大小。而且在 RAM 中还要存储其它数据,因此也不能将所有 RAM 空间都分配给串口缓冲区,应根据项目需要酌情修改。

软件模拟串口

除了 HardwareSerial 类库外,Arduino 还提供了 SoftwareSerial 类库,可将其它数字引脚通过程序来模拟成串口通信引脚。

通常将 Arduino 上自带的串口称为硬件串口,使用 SoftwareSerial 类库模拟的串口称为软件模拟串口(简称软串口)。

软串口是由程序模拟生成的,使用起来不如硬串口稳定,并且和硬串口一样,波特率越高越不稳定。

软串口通过 AVR 芯片的 PCINT 中断功能来实现,在 Arduino UNO 上,所有引脚都支持 PCINT 中断,因此所有引脚都可以设置为软串口的 RX 接收端。但在其他型号的 Arduino 上,并不是每个引脚都支持中断功能,所以只有特定的引脚可以设置为 RX 端。

SoftwareSerial 类库成员函数
函数 功能 语法 参数 返回值
SoftwareSerial() SoftwareSerial 类的构造函数,可以指定软串口的 RX 和 TX 引脚 SoftwareSerial mySerial = SoftwareSerial(rxPin,txPin) rxPin 软串口接收引脚,txPin 发送引脚 软串口对象
listen() 开启软串口监听状态 mySerial.listen() mySerial 自定义的软串口对象
isListening() 检测软串口是否在监听状态 mySerial.isListening() 同上 ↑ boolean型数据,true 表示正在监听,false 表示未监听
overflow() 检测缓冲区是否已经溢出,软串口缓冲区最多可保存 64B 数据 mySerial.overflow() 同上 ↑ boolean 型数据,true 表示溢出,false 表示未溢出
建立软串口通信

SoftwareSerial 类库是 Arduino IDE 默认提供的一个第三方类库,并未包含在核心库中,因此需要声明包含 SoftwareSerial.h 头文件,然后就可以使用类库中提供的构造函数初始化软串口,创建软串口对象后,就可以调用 listen() 函数开启该软串口的监听功能。如:

1
2
3
4
5
6
// 新建名为 mySerial 的软串口,2 号引脚作为 RX 端,3号引脚作为 TX 端
SoftwareSerial mySerial = SoftwareSerial(2, 3);
// 初始化软串口通信
mySerial.begin(9600);
// 开启软串口监听
mySerial.listen();

注意,当使用 0(RX)和 1(TX)串口连接外部串口设备时,这组串口将被所连接的设备占用,从而可能会造成无法下载程序和通信异常的情况。因此,通常在连接外部设备时,尽量避免使用 0(RX)和 1(TX)这组串口。

同时使用多个软串口

要连接多个串口设备时,可以建立多个软串口,但限于软串口的实现原理,使得 Arduino 只能监视一个软串口,因此当存在多个软串口时,需要使用 listen() 函数指定要监听的设备。用法:调用串口对象的 listen() 函数,如 portOne.listen() ,需要切换时,调用另一个窗口对象的 listen() 函数,如 protTwo.listen()

IIC 总线

IIC 总线类型是由飞利浦(Philips)半导体公司在 20 世纪 80 年代初设计出来的。使用 IIC 协议可以通过两根双向的总线(数据线 SDA 和时钟线 SCL)使 Arduino 连接最多 128 个从机设备。在实现这种总线连接时,唯一需要的外部器件是每根总线上的上拉电阻。在目前使用的大多数 Arduino 相关 IIC 模块上,通常已经添加了上拉电阻,因此只需要将 IIC 从机设备模块直接连接到 Arduino 的 IIC 接口上即可。

Arduino 控制器内部集成的这种两线串行接口,通常称为 TWI 接口,事实上, TWI 和 IIC 总线是一回事。

IIC 主、从机与引脚

与串口的一对一通信方式不同,总线通信通常有主机(Master)、从机(Slave)之分,通信时,主机负责启动和终止数据传送,同时还要输出时钟信号;从机会被主机寻址,而且响应主机的通信请求。

在 IIC 通信中,通信速率的控制由主机完成,主机会通过 SCL 引脚输出时钟信号供总线上的所有从机使用,不像串口通信中的通信速率由双方事先约定。

IIC 是一种半双工通信方式,即总线上的设备通过 SDA 引脚传输通信数据,数据的发送和接收由主机控制,切换进行。

IIC 上的所有通信都是由主机发起的,总线上的设备都应该有各自的地址,主机通过这些地址向总线上的任一设备发起连接,从机响应请求并建立连接后,就可以进行数据传输。

Wire 类库成员函数

对于 IIC 总线的使用,Arduino IDE 自带了一个第三方库 Wire。

函数 功能 语法 参数 返回值
begin() 初始化 IIC 连接,并作为主机或者从机加入 IIC 总线 begin() , begin(address) 没有参数时设备将作为主机加入IIC总线,address 可以设置为 0~127 中任意地址
requestFrom() 主机向从机发送数据请求信号,从机可以用注册一个 onRequest 注册一个事件响应请求 Wire.requestFrom(address,quantity) , Wire.requestFrom(address,quantity,stop) address 设备地址,quantity 请求的字节数,stoptrue 时将发送停止信息,释放 IIC 总线,为 false 时将发送重新开始信息,并继续保持 IIC 总线连接
beginTransmission() 设定传输数据到指定冲击设备。随后可以使用 write() 函数发送数据,使用 endTransmission() 函数结束传输 beginTransmission(address) address 从机地址(0~127)
endTransmission() 结束数据传输 Wire.endTransmission() , Wire.endTransmission(stop) stoptrue 时将发送停止信息,释放 IIC 总线,为 flase 时将发送重新开始信息,并保持连接 byte 型值,表示本次传输状态:0(成功)、1(数据过长,超出发送缓冲区)、2(在地址发送时接收到 NACK 信号)、3(在数据发送时收到NACK信号)、4(其它错误)
write() 主机将要发送的数据加入发送队列;从机发送数据至发起请求的主机 Wire.write(value) , Wire.write(string) , Wire.write(data,length) value 以单字节发送,string 以一些列字节发送,data 以字节形式发送数组,length 传输的字节数 byte 型值,返回输入的字节数
available() 返回接收到的字节数,在主机中一般用于发送数据请求后,在从机中一般用于数据接收事件中 Wire.available() 可读的字节数
read() 读取 1 字节数据 Wire.read() 读到的字节数据
onReceive() 可以在从机端注册一个事件,当从机收到主机发送的数据时即被触发 Wire.onReceive(handler) handler 当从机接收到数据时可被触发的事件。该事件带有一个 int型参数(从主机读到的字节数)且没有返回值
onRequest() 注册一个事件,当从机接收到主机数据请求时被触发 Wire.onRequest(handler) handler 可被触发的事件,该事件不带参数和返回值
IIC 连接方法

多个设备可以将每个设备的 SCL 引脚连接起来,把每个设备的 SDA 引脚连接起来,即可接入 IIC 总线。

SPI 总线

SPI 是一种高速通信接口,通过它可以连接使用具有相同接口的外部设备,如 SD 卡、图形液晶、网络芯片等。

SPI 也是一种总线通信方式, Arduino 可以通过 SPI 接口连接多个从设备,并通过程序来选择对某一设备的连接使用。

SPI 引脚
引脚 说明
MISO(Master In Slave Out) 主机数据输入,从机数据输出
MOSI(Master Out Slave In) 主机数据输出,从机数据输入
SCK(Serial Clock) 用于通信同步的时钟信号,该时钟信号由主机产生
SS(Slave Select)或 CS(Chip Select) 从机使能信号,由主机控制

在 SPI 总线中也有主、从机之分,主机负责输出时钟信号及选择通信的从设备。时钟信号会通过主机的 SCK 引脚输出,提供给通信从机使用。而对于通信从机的选择,由主机上的 CS 引脚决定,当 CS 引脚为低电平时,该从机被选中;当 CS 引脚为高电平时,该从机被断开。数据的收发通过 MISO 和 MOSI 进行。

大多数 Arduino 控制器都带有 6 针的 ICSP 引脚,可通过 ICSP 引脚来使用 SPI 总线。

大多数情况下 Arduino 都是作为主机使用,并且 Arduino 的 SPI 类库没有提供 Arduino 作为从机的 API。

如果一个 SPI 总线上连接了多个 SPI 设备,那么在使用某一从机设备时,需要该从设备的 CS 引脚拉低,以选中该设备;而且需要将其他设备的 CS 引脚拉高,以释放这些暂时未使用的设备。在每次切换连接不同的从设备时,都需要进行这样的操作来选择从设备。

需要注意,虽然 SS 引脚只有在作为从机时才会用到,但即使不使用 SS引脚,也需要将其保持为输出状态,否则会造成 SPI 无法使用的情况。

SPI 类库成员函数

Arduino 的 SPI 类库定义在 SPI.h 头文件中。

函数 功能 语法 参数 返回值
begin() 初始化 SPI 通信。调用该函数后,SCK、MOSI、SS 引脚将被设置为输出模式,且 SCK 和 MOSI 引脚被拉低,SS 引脚被拉高 SPI.begin()
end() 关闭 SPI 总线通信 SPI.end()
setBitOrder() 设置传输顺序 SPI.setBitOrder(divider) order 传输顺序:LSBFIRST(低位在前)、MSBFIRST(高位在前)
setClockDivider() 设置通信时钟。时钟信号由主机产生,从机不用配置。主机的 SPI 时钟频率应该在从机允许的处理速度范围内 SPI.setClockDivider(divider) divider SPI 通信的时钟是由系统时钟分频得到的:SPI_CLOCK_DIV2(2分频)、SPI_CLOCK_DIV4(4分频)、SPI_CLOCK_DIV8(8分频)、SPI_CLOCK_DIV16(16分频)、SPI_CLOCK_DIV32(32分频)、SPI_CLOCK_DIV64(64分频)、SPI_CLOCK_DIV128`(128分频)
setDataMode() 设置数据模式 SPI.setDataMode(mode) mode 数据模式:SPI_MODE0SPI_MODE1SPI_MODE2SPI_MODE3
transfer() 传输 1B 数据,参数为发送的数据,返回值为接收到的数据 SPI.transfor(val) val 要发送的字节数据 读到的字节数据

SPI 是双工通道,因此每发送 1B 的数据,也会接收到 1B 的数据。

数据发送与接收

SPI 总线是一种同步串行总线,其收/发数据可以同时进行。SPI 类库并没有像其他类库一样提供用于发送、接收操作的 write()read() 函数,而是用 transfer() 函数替代了两者的功能,其参数是发送的数据,返回值是接收到的数据。每发送一次数据,即会接收一次。

软件模拟 SPI 通信

使用模拟 SPI 通信可以指定 Arduino 上任意引脚为模拟 SPI 引脚,并与其他 SPI 器件连接进行通信。Arduino 提供了两个相关 API 用于实现模拟 SPI 通信功能:

  • shiftOut(dataPin, clockPin, bitOrder, value) 用于模拟串口输出,无返回值,参数如下:
    • dataPin 数据输出引脚
    • clockPin 时钟输出引脚
    • bitOrder 数据传输顺序
    • value 传输的数据
  • shiftIn(dataPin, clockPin, bitOrder) 用于模拟 SPI 串行输入,返回值为输入的串行数据,参数如下:
    • dataPin 数据输出引脚
    • clockPin 时钟输出引脚
    • bitOrder 数据传输顺序
扩展 I/O 口

在使用 Arduino UNO 时,可能会遇到数字引脚不够用的情况,可以使用 74HC595 芯片来实现扩展 数字I/O 的效果。74HC595 只能作为输出端口扩展,如果要扩展输入端口,则可以使用其他的并行输入/串行输入芯片,如 74HC165 等。

存储篇

EEPROM ——断电也能保存数据

EEPROM 电可擦可编程只读存储器是一种断电后数据不丢失的存储设备,常被用作记录设备的工作数据和保存配置参数。若想断电后 Arduino 仍记住数据,就可以使用 EEPROM 。

在使用 AVR 芯片的 Arduino 控制器上均带有 EEPROM ,也可以使用外接的 EEPROM 芯片。

在 Arduino EEPROM 类库中,EEPROM 的地址被设定为从 0 开始,如 Arduino UNO 中的 EEPROM 有 1KB 的存储空间,其对应的地址为 0~1023,每个地址可以存储 1B 数据。当数据大于 1B 时,需要逐字读/写。

EEPROM 类库成员函数

Arduino 已经准备好了 EEPROM,只需要先调用 EEPROM.h 就可以使用 write()read() 函数对 EEPROM 进行写/读操作。

  • EEPROM.write(address, value) 对指定地址写入数据,无返回值,参数:
    • address EEPROM 地址,起始值为 0
    • value 写入的数据,byte 型
  • EEPROM.read(address) 用于读取指定地址的数据。一次读/写 1B 数据。如果指定地址没有写入过数据,则读出值为 255 。函数返回值为读到的数据,byte 类型,参数 address 是 EEPROM 地址,起始值为 0。
EEPROM 写入操作

要向 EEPROM 中写入数据,只需要使用 EEPROM.write(address, value) 语句就可以将数据 value 写入 EEPROM 地址 address 中。

需要注意,EEPROM 有 100000 次的擦写寿命,一次 EEPROM.write() 语句会占用 3ms,如果程序不断地擦写 EEPROM,则很快会损坏 EEPROM。所以在 loop() 中使用 EEPROM.write() 时,应使用延时或其他操作,避免频繁擦写 EEPROM。

EEPROM 读取操作

从 EEPROM 中读取数据需要使用 EEPROM.read(address) 语句,读取地址为 address 的数据。

EEPROM 清除

清除 EEPROM 的内容,其实就是把 EEPROM 中每一个字节写入 0,因为只需要执行一次清零,所以在 setup 部分完成。

存储各类型数据到 EEPROM

在 Arduino 提供的 EEPROM API 中,只能写入字节型数据,如果需要存储其它类型数据,需要先转换成字节,然后逐字写入 EEPROM,这里可以使用共用体把其它类型数据拆分成字节

几个不同的变量共同占用一段内存的结构,在 C 语言中被称为共用体类型结构,简称共用体。

定义一个名为 data 的共用体结构,共用体中有两种类型不同的成员变量:

1
2
3
4
union data{
float a;
byte b[];
}

再声明一个 data 类型的变量 c

1
data c;

现在可以通过 c.a 访问该共用体中 float 型成员 a,通过 c.b 访问该共用体中 byte 型数组 bc.ac.b 共同占用 4B 的地址。给 c.a 赋值后,通过 c.b 中的几个元素即可实现拆分 float 型数据的目的。

SD ——保存大量数据

当需要使用或存储大量数据,可以选择外置的 EEPROM 和 Flash 芯片来扩展存储空间,推荐使用 SD 卡来存储大量的数据。

SD 卡 是一种基于半导体快闪记忆器的新一代存储设备,广泛用于便携式设备上,如手机、数码相机、平板电脑等。

SD 卡可以通过 SPI 总线进行相关操作。使用 SD 卡库可以让 Arduino 读/写 SD 卡中的数据。由于 SD 卡库支持 FAT16FAT32 文件系统的 SD 卡、SDHC 卡和 TF 卡,因此需要将 SD 卡以 FAT16FAT32 文件系统进行格式化

SD 卡类库成员函数

Arduino 读/写 SD 卡程序需要包含 SPI 库的头文件 SPI.h 和 SD 卡库的头文件 SD.h 。SD 卡类库中提供了两个类:SDClass 类和 File

SDClass 类提供了访问 SD 卡、操作文件及文件夹的功能。

函数 功能 语法 参数 返回值
begin() 初始化 SD卡库和 SD卡 SD.begin() , SD.begin(cspin) 不带参数时默认将 Arduino 的 SPI 的 SS 引脚连接到 SD 卡的 CS 使能选择端;cspin 指定连接 SD卡CS使能选择端的引脚,注意 boolean 型数据,true 初始化成功,false 初始化失败
exists() 检查文件或文件夹是否存在 SD.exists(filename) filename 要检测的文件名。可以包含路径,路径用 / 分隔 boolean 型数据,true 表示存在,false 表示不存在
open() 打开 SD 卡上的一个文件。注意 SD.open(filename) , SD.open(filename, mode) filename 要打开的文件名。可以包含路径,路径用 / 分隔。mode 打开文件的方式,默认使用只读方式打开,可选值:FILE_READ (只读方式)、FILE_WRITE (写入方式) 返回被打开的文件对应的对象;如果不能打开,返回 false
remove() 从 SD 卡移除一个文件。注意 SD.remove(filename) filename 要移除的文件名,可以包含路径,路径用 / 分隔 boolean 型数据,true 表示移除成功,false 表示移除失败
mkdir() 创建文件夹 SD.mkdir(filename) filename 要创建的文件夹名,可以包含路径,路径用 / 分隔 boolean 型数据,true 表示创建成功,false 表示创建失败
rmdir() 移除文件夹 SD.rmdir(filename) filename 要移除的文件夹名,可以包含路径,路径用 / 分隔 boolean 型数据,true 表示移除成功,false 表示移除失败

File 类提供了读/写文件的功能。

函数 功能 语法 参数 返回值
available() 检查当前文件中可读数据的字节数 file.available() file 一个 File 类型对象 可读字节数
close() 关闭文件,并确保数据被完全写入 SD 卡中 file.close() 同上
flush() 确保数据已经写入 SD 卡,当文件关闭时会自动运行这个函数 file.flush() 同上
peek() 读取当前所在字节,但不移动到下一个字节 file.peek() 同上 下一字节或字符。如果没有可读数据返回 -1
position() 获取当前在文件中的位置 file.position() 同上 在当前文件中的位置
print() 输出数据到文件,要写入的文件应该被打开,且等待写入 file.print(data) , file.print(data, BASE) file 一个 File 类型对象,data 要写入的数据(可以是 char、byte、int、long 或 String 类型),BASE 数据输出形式:BIN(二进制)、OCT(八进制)、DEC(十进制)、HEX(十六进制) 发送的字节数
println() 输出数据到文件,并回车换行 file.println(data) , file.println(data, BASE) 同上 同上
seek() 跳转到指定位置,该位置必须在 0 到 该文件大小之间 file.seek(pos) file 一个 File 类型对象,pos 需要查找的位置 boolean 型数据,true 表示跳转成功,false 表示跳转失败
size() 获取文件的大小 file.size() file 一个 File 类型对象 文件大小(以字节为单位)
read() 读取 1B 数据 file.read() 同上 下一个字节或字符,如果没有可读数据则返回 -1
write() 写入数据到文件 file.write(data) , file.write(buf, len) file 一个 File 类型对象,data 要写入的数据(类型可以是 byte、char、字符串),buf 一个字符数组或字节数据,len 要写入的数据长度 发送的字节数
isDirectory() 判断当前文件对象是否为目录 file.isDirectory() file 一个 File 类型对象 boolean 型数据,true 表示是目录,false 表示不是目录
openNextFile() 打开下一个文件 file.openNextFile() 同上 下一个文件对应的对象
rewindDirectory() 回到当前目录中的第一个文件 file.rewindDirectory() 同上
SD 卡读写模块引脚

常见的 SD 卡读写模块引脚:

Micro SD 卡模块 说明 连接 Arduino 引脚
CD 插入检测,无卡时输出高电平,有卡时输出低电平 可不使用
CS SD 卡片选择。低电平使能 示例连接中接 4 号引脚,可根据实际情况修改
MOSI 数据输入口 MOSI,UNO 的 11 号引脚
MISO 数据输出口 MISO,UNO 的 12 号引脚
SCK SPI 时钟 SCK,UNO 的 13 号引脚
VCC 电压供电正 3.3~5V
GND 电源供电地 GND
SD 卡创建文件示例

在 2 号引脚上连接一个开关,用于控制程序开始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <SPI.h>
#include <SD.h>

File myFile;

void setup() {
// 在 2 号引脚上连接一个按键,用于控制程序开始
pinMode(2, INPUT_PULLUP);
while(digitalRead(2)){}

// 开启串口通信
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}


Serial.print("Initializing SD card...");

// Arduino 的 SS 引脚(UNO 的 10 号引脚,MEGA 的 53号引脚)必须保持输出模式,否则 SD 卡库无法工作
pinMode(10, OUTPUT);
if (!SD.begin(4)) {
Serial.println("initialization failed!");
while (1);
}
Serial.println("initialization done.");

if (SD.exists("example.txt")) {
Serial.println("example.txt exists.");
} else {
Serial.println("example.txt doesn't exist.");
}

// 打开一个文件,并立即关闭
Serial.println("Creating example.txt...");
myFile = SD.open("example.txt", FILE_WRITE);
myFile.close();

// 检查文件是否存在
if (SD.exists("example.txt")) {
Serial.println("example.txt exists.");
} else {
Serial.println("example.txt doesn't exist.");
}

// 删除文件
Serial.println("Removing example.txt...");
SD.remove("example.txt");

if (SD.exists("example.txt")) {
Serial.println("example.txt exists.");
} else {
Serial.println("example.txt doesn't exist.");
}
}

void loop() {
// nothing happens after setup finishes.
}
DHT11 温湿度检测模块

DHT11 温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,可以用来测环境温度和相对湿度。

DHT11 相对湿度的检测精度为 1%Rh,温度检测精度为 1℃,两次读取传感器数据的时间间隔应大于 1s。

可以从 Arduino 的库管理器中下载 DHT11 类库,其中只有一个成员函数 read() ,可以读取 DHT11 传感器的数据,并将温湿度数值分别存入 temperaturehumidity 两个成员变量中。用法:

1
Dht11.read(pin); // Dht11 是一个 dht11 类型的对象;pin 是连接 DHT11 传感器的引脚号

函数返回值为 int 类型,0 对应宏 DHTLIB_OK ,表示接收到数据且校验正确;-1 对应宏 DHTLIB_ERROR_CHECKSUM ,表示接收到数据但校验错误;2 对应宏 DHTLIB_ERROR_TIMEOUT ,表示通信超时。

无线通信篇

Arduino 可用的无线通信方式众多,如 ZigBee、WiFi、蓝牙等。比较常见的是使用串口透传模块,这类模块在设置好以后连接到 Arduino 串口,即可采用串口通信方式进行通信,该过程相当于将串口的有线通信改成了无线通信方式,而程序不需要修改。

另一种常见方式是使用 SPI 接口的无线模块,这类模块通常都有配套的驱动库,如 Arduino WiFi 扩展板。这种方式驱动无线模块传输速率更快,可以完成更多高级操作。

红外遥控

红外通信是一种利用红外光编码进行数据传输的无线通信方式,是目前使用最广泛的一种通信和遥控手段。

生活中大多数红外通信都使用 38 kHz 的频率进行通信,这是使用的一体化接收头和遥控器也使用 38kHz 的频率收/发信号。

要想使用红外遥控功能,还需要使用一个第三方的红外遥控库:IRremote 库,可以从网上下载。这个类库中的 IRrecv 类可用于接收红外信号并对其解码,IRsend 类用于对红外信号编码并发送。

红外接收 要使用红外遥控器控制 Arduino ,需要先了解遥控器各按键对应的编码,不同遥控器、按键、协议都对应着不同的编码,可以使用 IRremote 的示例程序来获取遥控器发送的信号编码。

红外发射 使用 Arduino 发送红外信号,需要将红外发射管与 Arduino 连接,连接方式与普通 LED 类似,需要串联一个限流电阻。IRremote 库只能使用 3 号引脚作为红外信号输出引脚

LCD显示篇

1602LCD

1602 液晶显示器(1602 Liquid Crystal Display) 是一种常见的字符型液晶显示器,因其能够显示 16*2 和字符而得名。

通常使用的 1602 LCD 中集成了字库芯片,通过 LiquidCrystal 类库提供的 API 可以方便的使用 1602 LCD 显示英文字母和一些符号。

图形显示器

Arduino 支持众多的显示器,如果字符型液晶显示器不满足需求,可以使用图片液晶显示器。

使用 u8glib 是目前 Arduino 平台上最好的图形显示库,可支持多种图片显示器。u8glib 库可以在显示器上绘制文字,可以设置字体、显示位置,还可以绘制图形,如矩形、圆形、圆弧、直线、点,可以绘制位图。绘制位图图片需要将图片转换成 Arduino 可以识别得代码保存。可以使用字模提取软件完成取模。

12864 LCD

12864 LCD 是最常见的图形液晶显示器,因其分辨率为 128*64 像素而得名,使用 12864 LCD 可以显示图形、汉字,甚至更高级的动画。

12864 OLED

小巧的液晶显示模块,使用的通信接口为 IIC,使用的控制芯片为 SSD1306。

USB 类库使用

在一些新推出的 Arduino 控制器上均带有 USB 通信功能,Arduino 提供了 USB 类库,可以将控制器模拟成 USB 鼠标或键盘设备。

Arduino USB 类库是带有 USB 功能的 Arduino 控制器特有的库,仅支持 Arduino 的 Leonardo、Micro 和 Due 型号。

USB 类库是 Arduino 的核心类库,因此不需要重新声明包含该库。该库提供了 MouseKeyboard 两个类,用于模拟鼠标和键盘。

Ethernet 类库使用

Arduino 不仅可以和各种硬件通信,还可以接入互联网,进行网络通信。Arduino IDE 自带了 Ethernet 类库,可以轻松将 Arduino 接入互联网,完成各种网络项目。

支持 Ethernet 的硬件
Ethernet 扩展板

Ethernet 扩展板是集成 WIZnetW5100 网络芯片的扩展板,连接到 Arduino 后使 Arduino 具有 网络功能,同时还集成了 SD 卡槽,以配合 SD 卡库读/写 SD 卡。

Arduino Ethernet

Arduino Ethernet 是集成了 Ethernet 功能的 Arduino 控制器,使用单个控制器即可连接到网络上,同时集成了 SD 卡槽,并且可以通过外接 POE 模块来扩展 POE 的供电功能。但该控制器没有下载功能,每次下载时需要连接 USB 转串口模块来下载程序。

Zduino Ethernet

OpenJumper 推出的高度集成的 Arduino Ethernet 兼容控制器,集成了 USB 下载、POE 供电、SD 卡槽等功能,并且完全兼容 Arduino UNO 的引脚位置。使用它可以快速将控制器接入网络,从而搭建自己的网络应用。

W5100

W5100 是 WIZnet 公司推出的一款多功能单片网络接口芯片,内部集成有 10/100 以太网控制器,主要用于高集成、高稳定、高性能和低成本的嵌入式系统。

Ethernet 类库

在使用网络功能时需要包含 Ethernet.h 头文件,由于 Arduino 通过 SPI 总线连接 W5100 实现网络功能,所以需要包含 SPI.h 头文件。Ethernet 类库中定义了多个类,需要配合使用才能完成网络通信。

读后感

这本书非常适合初学者,涉及的内容很全面,而且讲解由浅入深很容易理解。看了这本书后,不仅学到了 arduino 的开发,还学到了一些硬件的原理。