玩转 Arduino Uno、Mega、ESP 开源硬件

Arduino 是一款非常流行与成熟的电子原型评估套件,其 PCB 硬件IDE 集成开发环境板级支持包 全部基于开源共享协议,其中,Eagle 原理图以及 PCB 布线遵循 CC BY-SA 共享协议,而 IDE 集成开发环境的源代码基于 GPL 开源协议,微控制器 MCU 的 C/C++ 板级支持包则是基于 LGPL 开源协议。自从 2005 年第一款 Arduino 开发板面世以来,官方已经推出了琳琅满目的各类硬件以及软件包,叠加各大芯片厂商的助力,整个开源社区的氛围日趋活跃与丰富。

本文首先从入门级的 Arduino Uno 入手,然后逐步过渡至片上资源更为丰富的 Mega 2560,两款都是由 Arduino 官方所推出的 5V 供电电压的开发板。最后引入了携带有 Wifi/Bluetooth 无线接入能力的 ESP8266ESP32,而它们则是采用了国产 3.3V 芯片的开发板,由于开源社区或者芯片原厂提供了兼容 Arduino API 的板级支持包,在较为丰富功能的基础上提供了相对低廉的价格,叠加 Arduino 较为成熟的开发环境,更是加速了 Arduino 的应用与普及。

UNO R3

Arduino Uno 是整个 Arduino 家族当中应用最为广泛、文档最为齐全的一款硬件产品,基于 Microchip 微芯公司的 8AVR 单片机 ATmega328P,工作电压为5V,采用16MHz的无源谐振器,并拥有32 KBFlash 存储器(其中0.5KB用于Bootloader程序),以及2KBSRAM1KBEEPROM,同时还具备 14 个数字输入/输出引脚(其中 6 个可以用作 PWM 输出)和 6 个模拟输入接口,本文采用的是 2011 年发布的 Arduino Uno R3 版本开发板。

通常情况下,所有基于 Atmel 公司(现已被 Microchip 收购)的 AVR 架构的微控制器,都具备有如下 3 种类型的片上存储空间

  1. Flash:程序存储空间,即保存 Arduino Sketch 程序的位置;
  2. SRAM:静态随机读取存储器,Arduino Sketch 程序运行时建立和操作变量的地方;
  3. EEPROM:供开发人员通过调用EEPROM 库,从而持久保存数据的空间。

注意:Flash 和 EEPROM 存储器中的数据可以断电保存,而 SRAM 内的数据则是掉电丢失的。

Arduino Uno板载的ATmega328P微控制器拥有32k BytesFlash 存储器(其中0.5k Bytes用于Bootloader),以及2k BytesSRAM1k BytesEEPROM。相比于Arduino Mega 2560板载ATmega2560微控制器拥有的256k BytesFlash 存储器(其中8KB用于Bootloader),以及8k BytesSRAM4k BytesEEPROMArduino Uno 的存储资源相对要逊色不少,特别是 SRAM 过低将会引发程序执行错误,例如下面的代码:

1
char name[] = "I am Hank";

这段代码执行时,由 9 个字母组成的字符数组将会被放入 SRAM,由于每个字母的 ASCII 码会占用一个Byte,加上最后的\0结束符,那么name[]总共会占用9 + 1 = 10个字节,貌似看起来不多,但是在需要大量显示文本或者进行查表操作的时候,虽然程序能够正常的编译并且上传,但是执行的时候 SRAM 空间将会很快发生溢出,从而导致程序以不可预料的方式执行失败。

综上所述,为了节约宝贵的存储空间,除了尽量采用较小的数据类型以外,还可以将代码中不需要频繁修改的数据通过PROGMEM关键字存放至 Flash 存储器:

1
2
3
4
const PROGMEM dataType variableName[] = {}; // 优先使用这种格式
const dataType variableName[] PROGMEM = {}; // 或者使用这种格式

const dataType PROGMEM variableName[] = {}; // 但是这种格式不被允许

将上面字符数组name[]的定义修改为如下格式,就可以确保数据保存至Flash存储器。

1
const PROGMEM char name[] = "I am Hank";

Arduino Uno R3 开发板的推荐输出电压为7-12V,极限输出电压为6-20V。每个 IO 引脚的输出电流为20mA,而3.3V引脚的输出电流为50mA,其详细的 IO 引脚资源分配示意图如下图所示:

Arduino 开发板的 ISCP(电路内串行编程,In-Circuit Serial Programming)引脚,它们直接与 ATmega328P 微控制器连接,分别用于VCCMISOMOSISCKGNDRESET功能,其主要用途是通过串行接口向微控制器烧写程序。

注意:由于 Arduino Uno R3 已经内置CH340串口 USB 转换芯片,所以通常直接通过 USB 接口完成烧录,因而 ICSP 引脚较少被使用。

上述示意图当中各种颜色和图案所表达的意义如下表所示:

安装串口驱动

笔者手中的 Arduino Uno R3 属于国产版本,将官方电路设计中用作 USB 转串口用途的ATMEGA16U2芯片替换为了江苏沁恒的CH340G芯片。因而不能再使用 Arduino 官方推荐的驱动程序,而应当安装CH340G 驱动程序,从而确保计算机与开发板的正确连接。

安装完成之后,用USB cable type A/B数据线连接开发板与计算机,如果计算机的【设备管理器】当中显示USB-SERIAL CH340 (COMx),就表示 Arduino 开发板已经连接成功。

Arduino IDE

从官方网站下载Arduino IDE并且安装,打开后依次选择【文件 > 示例 > 01.Basics > Blink】

然后再依次点击【工具 > 开发板 > Arduino UNO】选择当前所使用的 Arduino 开发板型号:

最后点击【工具 > 端口 > COMx】选择当前开发板所使用的串行端口,由于COM1COM2通常被计算机保留给内部硬件串行设备值使用,因此外接串行设备的端口号通常会大于或等于COM3

注意:如果无法确定当前 Arduino 硬件占用的串行端口号码,可以先拔出 USB 连接线,然后再重新插入,此时 IDE 端口设置中新增加的就是 Arduino 硬件的串口号码。

接下来,验证当前程序是否存在语法错误,鼠标点击 Arduino IDE 右上角的编译验证按钮,或者依次选择菜单栏上的【项目 > 验证/编译】,当然也可以直接按下快捷键【Ctrl + R】

最后,编译并且上传程序至 Arduino 开发板,鼠标点击 Arduino IDE 右上角的上传按钮,或者依次选择菜单栏上的【项目 > 上传】,当然也可以直接按下快捷键【Ctrl + U】

注意:使用快捷键【Ctrl + U】编译和下载程序,有时候会导致 Arduino IDE 报出 Java 空指针异常,所以建议尽量采用鼠标点击按钮进行编译上传。

程序上传完成之后将会被 Arduino 开发板自动运行,此时网络标号为L的 LED 将会间隔1秒钟不断闪烁,下面就是本示例里所运行的 Blink 的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 设置函数,只在按下重置或者电源按钮以后被运行一次。
void setup() {
pinMode(LED_BUILTIN, OUTPUT); // 初始化数字引脚 LED_BUILTIN 为输出模式
}

// 循环函数,永远循环执行里面的功能代码。
void loop() {
digitalWrite(LED_BUILTIN, HIGH); // 打开 LED,其中的 HIGH 表示高电平
delay(1000); // 等待 1 秒钟
digitalWrite(LED_BUILTIN, LOW); // 关闭 LED,其中的 LOW 表示低电平
delay(1000); // 等待 1 秒钟
}

注意:由于 Arduno 官方提供的 IDE 开发体验并不友好,所以可以尝试使用安装有 PlatformIO 插件的 VSCode,PlatformIO 是一款可以方便应用于各类嵌入式、物联网产品开发的插件,具备跨平台的构建系统和库管理器,并提供了对 ArduinoARM mbedESP8266/ESP32AVRRISC-VSTM32PIC32nRF51/nRF52MSP430MCS-51(8051)FPGAFreeRTOSESP-IDFCMSISSPL 等众多微控制器的支持。

安装第三方类库

Arduino 提供了大量由工程师或者爱好者编写的第三方库,选择 Arduino IDE 菜单栏上的【项目 > 加载库 > 管理库...】或是按下【Ctrl + Shift + I】快捷键:

打开【库管理器】界面,搜索并且选择指定的第三库点击【安装】按钮即可:

此外,还可以离线直接安装ZIP格式的第三方库,压缩包的名称即为库的名称,压缩包当中包含 1 份.cpp源文件和 1 份.h头文件,以及一些其它的相关文件。此时选择菜单栏上的【项目 > 加载库 > 添加.zip库...】,然后在弹出的对话框中选择.zip文件即可完成安装:

安装板级支持包

自从 Arduino IDE 1.6.2 版本开始,默认会安装所有 AVR 微控制器的核心板级支持包。但是某些第三方厂商生产的 Arduino 开发板需要额外进行安装,此时可以选择菜单栏上的【工具 > 开发板 > 开发板管理器...】:

打开【开发板管理器】界面以后,检索需要的板级支持包名称就可以进行添加操作:

如果第三方厂商的开发板不在 Arduino 官方的支持列表里面,则再需要选择菜单栏上的【文件 > 首选项 > 附加开发板管理器网址】,然后输入一个指向 JSON 格式配置文件的 URL 地址(如果存在多个可以用逗号,进行分隔),再重新进入【开发板管理器】更新安装所需的支持包即可。

如果输入框当中存在多款板级支持包的 URL 地址,那么就可以使用逗号,进行分隔;或者输入框右侧的按钮,在弹出的文本框当中通过换行进行添加:

引脚工作模式

数字引脚 Digital

Arduino 的引脚可以分别配置为输入或者输出,采用 Atmega 芯片的数字、模拟引脚都可以采用基本相同的方式进行配置。

INPUT 输入

Atmega 芯片的引脚默认为输入模式,此时引脚处于high-impedance state高阻抗状态,类似于输入引脚前面串联了一枚100MΩ电阻。这意味着当引脚入处于默认状态或者被配置为pinMode(pin, INPUT)的时候,极易遭到外部干扰。因此 Arduino 官方建议将没有使用的引脚,通过上拉电阻(连接+5V)或者下拉电阻(连接 GND),设置为一个已知的确定状态,这里的电阻建议选取10KΩ阻值。

1
2
3
4
5
6
7
8
9
10
11
12
int pin_test = 5;

void setup() {
Serial.begin(9600); // 初始化串口,设置波特率为 9600
pinMode(pin_test, INPUT); // 初始化引脚为输入 INPUT
}

void loop() {
int status = digitalRead(pin_test); // 读取目标引脚的状态
Serial.println(status); // 串口打印状态值
delay(1000); // 循环读取引脚状态的间隔时间为 1 秒
}

上面代码将数字引脚 D5 设置为INPUT输入模式,然后将 D5 用杜邦线分别短接至 Arduino Uno 的3.3V5VGND引脚,然后观察串口打印的 D5 引脚电平状态,会发现短接至3.3V5V接口的时候输入状态皆为高电平1,而短接至GND引脚的时候则输入状态为低电平0

INPUT_PULLUP 上拉输入

Atmega 芯片引脚上内置有20KΩ上拉电阻,调用pinMode(pin, INPUT_PULLUP)函数即可以设置为使能。一旦使能则该引脚行为就会与普通INPUT模式相反,例如:当外接有传感器时,将传感器连接到配置为INPUT_PULLUP模式的引脚,而另一端接入GND,那么当开关断开时该引脚会读取到高电平(由内部上拉电阻拉高),而当开关按下时引脚则会读取到低电平(由于另一端接入了GND),即高电平HIGH关闭传感器,低电平LOW打开传感器。

注意:由于上拉电阻的存在,Arduino 引脚可能无法为点亮 LED 提供足够的电流。

无论当前引脚状态为HIGH或者LOW,上拉电阻的控制都是由微控制器内部的相同寄存器来完成的。因此,如果某个引脚被配置为INPUT_PULLUP上拉电阻接通,如果该引脚随后通过pinMode()切换至OUTPUT,此时该引脚的状态将被设置为HIGH,而再通过pinMode()切换至INPUT,则处于HIGH状态的OUTPUT引脚同样将会使能上拉电阻。这也正是 Arduino 1.0.1 之前版本里,可以通过如下代码使能上拉电阻的原因:

1
2
pinMode(pin, INPUT);           // 设置引脚为输入模式
digitalWrite(pin, HIGH); // 开启上拉电阻

下面来编写一段测试代码,将数字引脚 D13 设置为上拉输入模式INPUT_PULLUP,此时由于内部上拉电阻的作用,串口打印出的该引脚状态将会总是高电平1

1
2
3
4
5
6
7
8
9
10
11
12
int pin_test = 13;

void setup() {
Serial.begin(9600); // 初始化串口,设置波特率为 9600
pinMode(pin_test, INPUT_PULLUP); // 初始化引脚为上拉输入 INPUT_PULLUP
}

void loop() {
int status = digitalRead(pin_test); // 读取目标引脚的状态
Serial.println(status); // 串口打印状态值
delay(1000); // 循环读取引脚状态的间隔时间为 1 秒
}

注意:谨慎考虑将数字引脚 D13 用作输入模式,由于该引脚还连接了电阻和 LED 元件,如果使能内置20KΩ上拉电阻,则其电压将会徘徊在1.7V左右,而非预期的5V,这意味着该引脚会始终返回状态LOW。如果必须将该引脚用作数字输入,则需要将其pinMode()设置为INPUT然后再使用一个外置的下拉电阻。

OUTPUT 输出

Arduino Uno 可以通过pinMode()将 Atmega 芯片的引脚配置为OUTPUT模式,此时引脚处于低阻抗状态(low-impedance state),这意味着引脚可以为外置电路提供较大的电流(最高可达40mA)。下面的测试代码,用于循环间隔 1 秒输出数字引脚 D2 的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int pin_test = 2;

void setup() {
Serial.begin(9600); // 初始化串口,设置波特率为 9600
pinMode(pin_test, OUTPUT); // 初始化引脚
}

void loop() {
digitalWrite(pin_test, HIGH); // 输出高电平 HIGH
delay(1000); // 延时 1 秒
printSerial(); // 调用串口打印函数
digitalWrite(pin_test, LOW); // 输出低电平 LOW
delay(1000); // 等待 1 秒
printSerial(); // 调用串口打印函数
}

/* 串口打印函数,用于输出当前引脚状态 */
void printSerial() {
int status = digitalRead(pin_test); // 读取目标引脚的状态
Serial.println(status); // 串口打印状态值
delay(1000); // 循环读取状态的间隔时间为 1 秒
}

注意:将 Arduino 的引脚进行短路或者试图通过其负载大电流设备,都有可能会损坏引脚上的晶体管,甚至烧毁整个 Atmega 芯片。因此为了避免不必要的损失,最好将输出引脚连接至具备470Ω1kΩ电阻的设备上面。

模拟引脚 Analog

Arduino 板载的 ATmega 微控制器包含 1 个 6 通道模数转换器 ADC,该转换器具备 10 位分辨率,可以返回0 ~ 1023之间的整数。除此之外,模拟引脚还具备数字 GPIO 引脚的所有功能,其用作数字用途时功能与数字引脚D0 ~ D13一致。代码当中可以通过别名A0 ~ A5引用模拟引脚,当然也可以将模拟引脚作为数字引脚那样使用。例如,下面代码将模拟引脚A0设置为输出模式OUTPUT,并间隔 1 秒切换其电平状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int pin_test = A0;                      // 模拟引脚 A0 ~ A5

void setup() {
Serial.begin(9600); // 初始化串口,设置波特率为 9600
pinMode(pin_test, OUTPUT); // 初始化引脚
}

void loop() {
digitalWrite(pin_test, HIGH); // 输出高电平 HIGH
delay(1000); // 延时 1 秒
printSerial(); // 调用串口打印函数
digitalWrite(pin_test, LOW); // 输出低电平 LOW
delay(1000); // 等待 1 秒
printSerial(); // 调用串口打印函数
}

/* 串口打印函数,用于输出当前引脚状态 */
void printSerial() {
int status = digitalRead(pin_test); // 读取目标引脚的状态
Serial.println(status); // 串口打印状态值
delay(1000); // 循环读取状态的间隔时间为 1 秒
}

模拟引脚也具有上拉电阻特性,其作用与数字引脚的上拉电阻相同,可以通过如下代码进行启用:

1
pinMode(A0,INPUT_PULLUP); // 将模拟引脚 0 设置为输入上拉

注意:打开上拉电阻会影响analogRead()函数的返回值。

如果之前已经将引脚设置为OUTPUT输出模式,那么AnalogRead()函数将会无法正常工作,因此,调用该函数之前需要将其设置回INPUT输入模式。同样的,如果该引脚已经被设置为高电平作为输出状态,那么当切换回输入状态时,则需要使能上拉电阻。

ATmega 数据手册警告不要在相近的时间内,让 A/D 通过analogRead()函数访问其它的模拟引脚,否则可能会引起信号的抖动与噪声。正确的做法是在操作模拟引脚(数字模式下)之后,调用analogRead()读取模拟引脚之前,进行一次短暂的延时操作。

脉冲宽度调制 PWM

Arduino IDE 上附带的 文件 > 示例 > 03.Analog > Fade 示例,展示了如何通过analogWrite()函数,以 PWM 模拟输出的方式控制 LED 的淡入淡出(渐亮与渐暗)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int ledPin = 9;    // LED 正极连接至数字 D9 引脚,而负极连接到 GND

void setup() {
// 不需要进行任何设置
}

void loop() {
/* 淡入,从最暗到最亮,每次增加 5 点亮度 */
for (int fadeValue = 0 ; fadeValue <= 255; fadeValue += 5) {
analogWrite(ledPin, fadeValue); // 设置值,范围从 0 ~ 255
delay(30); // 延时 30 毫秒以展示效果
}

/* 淡出,从最亮到最暗,每次减少 5 点亮度 */
for (int fadeValue = 255 ; fadeValue >= 0; fadeValue -= 5) {
analogWrite(ledPin, fadeValue); // 设置值,范围从 0 ~ 255
delay(30); // 延时 30 毫秒以展示效果
}
}

PWM 是指脉冲宽度调制(Pulse Width Modulation),是一种通过数字方式获得模拟结果的技术。通过创建数字方波,控制(调制)信号开关状态(5V或者0V)的持续时间(即脉冲宽度),从而获得近似的模拟值,实现 LED 淡入淡出的效果。

上面是Fade示例代码的波形示意图,其中绿线代表固定的时间间隔,这个时间是 PWM 频率的倒数。换而言之,当 PWM 频率为500Hz的时候,每格绿线间隔就代表2ms毫秒。调用analogWrite()的时间范围为0 ~ 255,那么analogWrite(255)表示占空比为100%2ms时间内始终打开),而analogWrite(127)则表示占空比为50%(打开约一半时间)。

上图为笔者采用数字示波器对Fade示例中的 D9GND 引脚采样所获得的动态波形图,从动图当中可以看到电平信号的宽度始终处于线性的变化之中。

Arduino C++

草图

Arduino IDE 采用 C++ 作为开发语言,但是裁剪了面向对象以及一些较为复杂的语法特性,然后增加了一些特性关键字,所有源代码都通过称为Sketch [sketʃ] 草图的工程项目进行管理,每份 Sketch 包含的代码主要由如下 2 部分组成:

  • setup():主要用于初始化变量与第三方库以及引脚的工作模式,仅会在 Sketch 程序启动的时候执行一次;
  • loop():放置在setup()函数之后,主要用于放置实际控制 Arduino 开发板的代码。
1
2
3
4
5
6
7
void setup() {
// 用于放置设置代码,仅运行一次。
}

void loop() {
// 放置功能代码,无限循环执行。
}

常量

整型常量

整型常量就是Sketch代码中直接声明的各种整型数据,默认情况下为 10 进制,但是通过特殊的标识符与格式,可以分别转换为二、八、十六进制。

基数 示例 格式 可用字符
10进制(Decimal) 123 -
2进制(Binary) B1111011 字母B前缀 01
8进制(Octal) 0173 数字0前缀 0 - 7
16进制(Hexadecimal) 0x7B 字母数字0x前缀 0 ~ 9A ~ Fa ~ f

注意ATmega328P 属于 8 位 AVR 单片机,所以最多只能表达 8 位二进制数据,即0 ~ 255范围的整型二进制形式。

格式 描述
后缀uU 将常量强制转换为unsigned数据格式 33u
后缀lL 将常量强制转换为long数据格式 100000L
后缀ulUL 将常量强制转换为unsigned long数据格式 32767ul

浮点常量

浮点常量除了使用小数点.表示以外,还可以采用科学计数法Ee进行表达。

整型常量 渲染结果
10.0 \(10\)
2.34E5 \(2.34 \times 10^5 = 234000\)
67e-12 \(67.0 \times 10^{-12} = 0.000000000067\)

布尔常量 true 与 false

Arduino 分别采用truefalse来表达

  • false:通常被定义为0
  • true:通常被定义为1,但是任意非零的整数(包括负数)也被认为等效于true

电平状态 HIGH LOW

常量HIGHLOW分别用于设置数字引脚的电平状态,其作用与当前引脚的输入INPUT输出OUTPUT状态密切相关:

  • HIGH:当引脚处于INPUT模式,调用digitalRead()读取到HIGH意味着当前电平状态高于3.0V,即处于高电平状态。如果调用digitalWrite()将设置为INPUT模式的引脚置为高电平HIGH,这将会使能内部的20KΩ上拉电阻,从而持续向引脚输入高电平状态。当然,使能引脚内部上拉电阻的正确做法是将引用的工作模式设置为INPUT_PULLUP。当引脚处于OUTPUT模式时,调用digitalWrite()将引脚状态置为HIGH,此时引脚向外输出的电压为5V
  • LOW:当引脚处于INPUT模式,如果此时引脚通过digitalRead()读取的电压低于1.5V就会认为处于低电平LOW。当引脚处于OUTPUT模式,调用digitalWrite()将其状态置为LOW,此时引脚输出电压为0V,同时输出的电流也会变得极小。

输入状态 INPUT OUTPUT INPUT_PULLUP

如前所述,常量INPUTOUTPUTINPUT_PULLUP用于pinMode()函数设置引脚的工作模式。

板载 LED_BUILTIN

大部份 Arduino 开发板都会存在一个引脚通过一个电阻连接到一枚板载 LED,常量LED_BUILTIN就表示这枚 LED 所连接的引脚编号,大部分 LED 都会连接到第 13 号数字引脚,所以通过串口打印该常量将会输出13

数据类型

数据类型 占用存储空间 说明
array \(\frac{sizeof(array)}{sizeof(*array)}\) 数组,包含一系列相同数据类型的元素,可以采用索引进行访问。
bool 1 Byte(8bit) 布尔型,拥有truefalse两个值。
boolean 1 Byte(8bit) 布尔型别名,官方建议代码中使用bool关键字作为数据类型。
byte 1 Byte(8bit) 字节型,用于存储0 ~ 255的 8 位无符号整数
word 2 Byte(16bit) 单词型,用于存储 0 ~ 65535无符号整数
char 1 Byte(8bit) 字符型,用于存储单引号'h'字符,实际保存是其 ASCII 编码。
unsigned char 1 Byte(8bit) 无符号字符型,其编码为0 ~ 255的数字。
char string[] sizeof() 字符串,本质是字符数组,可以使用双引号进行表示char myString[] = "Hank",结尾会自动添加 ASCII 编码为 \0 的空字符。
String 类 length() 字符串对象,可以通过 String() 构造函数创建一个 String 类,该类拥有诸多便捷的字符串处理函数。
float 4 Byte(32bit) 浮点型,用于保存小数,注意数据类型转换时会损失精度。
double 4 Byte(32bit) 双精度浮点型,在 Arduino Due 上面占用 8 Byte(64bit)。
int 2 Byte(16bit) 整型,在 Arduino Due 上面占用 4 Byte(32bit)。
unsigned int 2 Byte(16bit) 无符号整型,只存储正整数,与普通整型区别在于符号位的处理,在 Arduino Due 上占用 4 Byte(32bit)。
short 2 Byte(16bit) 短整型
long 4 Byte(32bit) 长整型
unsigned long 4 Byte(32bit) 无符号长整型,用于存储范围更大的正整数。
size_t - 一种以字节(Byte)来表示任意对象大小的数据类型,也是sizeof()Serial.print()的返回值类型。
void - 仅仅用于函数声明,表示当前函数没有任何返回信息。

数据类型转换

可以对下面表格当中指定的数据类型,执行强制数据类型转换操作:

Arduino 风格 标准 C 风格 描述
(unsigned int)x - 强制转换为无符号普通整型
(unsigned long)x - 强制转换为无符号长整型
byte(x) (byte)x 强制转换为字节型
char(x) (char)x 强制转换为字符型
float(x) (float)x 强制转换为浮点型
int(x) (int)x 强制转换为普通整型
long(x) (long)x 强制转换为长整型
word(x, h/l) (word)x 当第 2 个参数为h表示取word的最位字节,为l则是取最侧的位字节。

注意:通过原生的String()函数还能够将不同类型的数据构造为一个String类实例,具体请参考本文【核心库函数】章节下的字符串小节:

下面总结了一下 Arduino 开发实践当中一些常用的数据类型转换技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 将 String 对象转换为字符 Char 数组 */
String object = "www.uinika.com";
static char buffer[100]; // 防止局部变量被销毁
strcpy(buffer, object.c_str()); // 将转换之后的字符数组拷贝至 buffer

/* 将参数 2022 按照 format 转换成格式为 %ld 长度为 SIZE 的字符数组 string */
#define SIZE (100)
char string[SIZE];
snprintf(string, SIZE, "%ld", 2022);

/* 首先将字符串转换为字符数组 characters,然后再将其强制转换为无符号字符数组 result */
char characters[100];
strcpy(characters, String("www.uinika.cn").c_str());
unsigned char *result = (unsigned char *)characters;

运算符 & 操作符

sizeof()

sizeof(variable) 操作符用于返回参数变量所占据的字节数,返回值的数据类型为 size_t。由于可以方便的调整数组大小,该操作符在处理数组或者字符串时非常有用,下面的示例程序将会逐次向串口打印一个字符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char myString[] = "www.uinika.com";

void setup() {
Serial.begin(9600);
}

void loop() {
for (byte i = 0; i < sizeof(myString) - 1; i++) {
Serial.print(i, DEC);
Serial.print(" = ");
Serial.write(myString[i]);
Serial.println();
}
delay(5000); // 放慢程序的运行速度
}

由于 sizeof() 返回的是数组或者字符串的总字节数,对于诸如 int 这样更大数据类型的数组,可以采用下面这种方式来处理 for 循环:

1
2
3
4
5
6
int years[] = {2019, 2020, 2021};

/* 对于任何类型或大小的数组,这个for循环都可以正常工作 */
for (byte i = 0; i < (sizeof(years) / sizeof(years[0])); i++) {
// 针对 years[i] 进行一些操作
}

注释

注释 功能描述 注释 功能描述
/* ... */ 块注释; // ... 单行注释;

流程控制

流程控制 功能描述
while 连续进行循环,直至参数当中的表达式执行结果变为 false 为止。
do...while 工作方式与 while 循环相同,但会在循环结束的时候进行一次判断,所以这种循环至少会被执行一次。
if 检查判断条件,如果为 true 就执行内部的语句,否则就会绕开这些语句。
else 通常与 if 语句搭配使用,如果 if 语句的条件为 false,就会执行 else 子句,从而实现对多个判断条件的分组;
for 用于循环执行花括号 {} 当中的循环体语句。
switch...case switch 语句的变量值与 case 语句中指定的值进行比较,如果匹配就会执行这个 case 语句当中的代码。
break 退出 forwhiledo…while 循环, 以及 switch case 语句。
continue 跳出本次 forwhiledo…while 循环。
return 终止函数的执行,并且返回函数的执行结果。
goto 跳转程序的运行流程到指定的标记点。

预编译

预编译 功能描述
#define() 在编译之前赋予常量一个引用,编译器会在编译时用常量值替换对这些引用。
#include() 用于在 Arduino 草图当中包含外部的库文件。

数学运算符

数学运算符 功能描述 数学运算符 功能描述
A % B 取余 A - B 减法
A * B 乘法 A / B 除法
A + B 加法 A = B 赋值

比较运算符

比较运算符 功能描述 比较运算符 功能描述
A != B 不等于 A == B 等于
A < B 小于 A > B 大于
A <= B 小于或等于 A >= B 大于或等于

布尔运算符

布尔运算符 功能描述 布尔运算符 功能描述 布尔运算符 功能描述
! A 逻辑非 A && B 逻辑与 A ∥ B 逻辑或

指针运算符

指针运算符 功能描述 指针运算符 功能描述
& A 获取变量 A 的地址 * A 获取指针变量 A 所指向的地址当中包含的值

位运算符

位运算符 功能描述 位运算符 功能描述
A & B 按位与 A ^ B 按位异或
A << n 左移 n 位,空位用二进制 0 填充 A 〡 B 按位或
A >> n 右移 n 位,空位用二进制 0 填充 ~ A 按位取反

复合运算符

复合运算符 功能描述 复合运算符 功能描述
A++ A 进行递增 A *= B 首先 AB 相乘,然后再赋值给 A
A-- A 进行递减 A -= B 首先 AB 相减,然后再赋值给 A
A %= B 首先 AB 取余,然后再赋值给 A A /= B 首先 AB 相除,然后再赋值给 A
A &= B 首先 AB 按位与,然后再赋值给 A A ^= B 首先 AB 按位异或,然后再赋值给 A
A += B 首先 AB 相加,然后再赋值给 A A 〡= B 首先 AB 按位或,然后再赋值给 A

变量限定符

Arduino C++ 依然允许声明全局变量(代码文件全局可见)或者局部变量(仅在声明它们的函数中可见)。

1
2
3
4
5
6
7
8
9
10
11
12
int global = 2020;                 // 全局变量,对于所有函数可见

void setup() {
Serial.begin(9600); // 初始化串口,设置波特率为 9600
}

void loop() {
int local = 2019; // 局部变量,仅在函数内部可见
Serial.println(global); // 串口输出全局变量
Serial.println(local); // 串口输出局部变量
delay(1000);
}

const

关键字const用于声明一个只读的常量,修改其值将会引发编译器错误。

1
2
3
const float pi = 3.14;

pi = 7; // 非法,不能修改一个常量

static

关键字static用于声明一个静态局部变量,其值在函数调用结束后并不会释放存储单元;下一次函数调用时,该静态局部变量依然继续保留原来的值。

1
2
3
4
5
6
7
8
9
10
11
12
void setup() {
Serial.begin(9600);
test(); // 静态局部变量index的值为 1
test(); // 静态局部变量index的值为 2
}

void loop() {}

void test() {
static int index = 0;
Serial.println(++index);
}

volatile

关键字volatile [ˈvɒlətaɪl] 不稳定物质用于指示 Arduino 编译器从 RAM 加载变量,而非从存储寄存器(存储与操作程序变量的临时内存区域)当中。因为在某些情况下,存储在寄存器中的变量值可能不准确。当一个变量可以被其所处代码块之外的代码修改时,就应该将其声明为volatile,Arduino 里出现这种情况的地方,主要是在中断服务程序当中。

如果声明为volatile的变量超过一个字节(例如是一个 16 位的整型或者 32 位的长整型数据),那么 Arduino Uno 的 8 位微控制器将无法一次性完成读取。所以程序的主代码(例如loop()函数)读取volatile变量的第一个 8 位的时候,此时中断服务函数可能已经对第二个 8 位进行了修改,从而产生一个错误的结果。解决这个问题,需要在主代码读取volatile变量时,通过noInterrupts()函数或者ATOMIC_BLOCK宏禁用中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 当中断引脚状态改变的时候切换 LED 的亮灭 */
int pin = 13;
volatile byte state = LOW;

void setup() {
pinMode(pin, OUTPUT); // 设置第 13 引脚为输出模式

/* 添加数字引脚 2 的中断事件,设置 blink 为中断服务函数,引脚状态发生改变时触发 */
attachInterrupt(digitalPinToInterrupt(2), blink, CHANGE);
}

void loop() {
digitalWrite(pin, state); // 向第 13 引脚写入状态
}

void blink() {
state = !state; // 状态值取反
}
1
2
3
4
5
6
7
8
/* 这个库包含了 ATOMIC_BLOCK 宏 */
#include <util/atomic.h>

volatile int input_from_interrupt; // 声明 volatile 变量

ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
int result = input_from_interrupt; // 中断被阻塞的代码,连续的原子操作不会发生中断
}

PROGMEM

如前所述,关键字PROGMEM用于将变量保存在 Flash 存储器,而非 SRAM 存储器当中。

1
2
3
4
5
6
7
void setup() {
Serial.begin(9600);
const PROGMEM char name[] = "I am Hank"; // 将字符串保存至 Flash 存储器
Serial.println(name); // 串口输出保存在 Flash 里的字符串
}

void loop() {}

数组处理

Arduino C++ 的数组语法与标准 C 语言完全相同。

1
2
3
4
int test[6];
int test[] = {2, 4, 8, 3, 6};
int test[6] = {2, 4, -8, 3, 2};
char test[7] = "uinika";

注意:声明数组长度时,需要增加 1 个结束符。

数组的遍历与标准 C 语言也是一样的,下面代码将会循环向串口输出"uinika"字符串:

1
2
3
4
5
6
7
8
9
10
void setup() {
Serial.begin(9600);
char name[7] = "uinika"; // 声明字符串或字符数组

for (int i = 0; i < 7; i++) {
Serial.println(name[i]); // 串口遍历输出字符串上的每个字符
}
}

void loop() {}

注意,采用双引号""声明字符数组时,系统会自动在最后为其分配一个空操作符\0作为结束标志。

此外,数组的长度可以调用原生提供的sizeof()函数进行返回:

1
2
3
4
5
6
7
void setup() {
Serial.begin(9600);
char name[] = "uinika"; // 声明字符串或者字符数组
Serial.println(sizeof(name)); // 串口输出字符数组长度
}

void loop() {}

核心库函数

数字 I/O

数字 I/O 功能描述
pinMode(pin, mode) 配置引脚的工作模式。参数pin是引脚数。
▶ 参数mode:可以选择INPUT(输入)、OUTPUT(输出)、INPUT_PULLUP(输入上拉);
▶ 返回值:无;
digitalRead(pin) 从指定数字引脚读取当前的电平状态,可以是HIGH或者LOW
▶ 参数pin:需要读取状态的引脚编号;
▶ 返回值:HIGH或者LOW
digitalWrite(pin, value) HIGHLOW状态写入指定的数字引脚。
▶ 参数pin:需要写入的引脚编号;
▶ 参数valueHIGH或者LOW
▶ 返回值:无;

模拟 I/O

模拟 I/O 功能描述
analogRead(pin) 从指定的模拟引脚读取数值,Arduino Uno 包含一个多通道的 10 位模数转换器,能够将0V ~ 5V映射为0 ~ 1023的整数值,其中每个单元所表示的电压为 \(\frac{5V}{1024} \approx 0.0049V = 4.9 mV\);读取一次模拟输入需要消耗100微秒(即0.0001秒),因此读取模拟输入的最大频率为10000次/秒。
▶ 参数pin:需要进行读取操作的模拟引脚编号;
▶ 返回值:0 ~ 1023之间的整数值;
▶ 当模拟输入引脚没有任何连接时,调用analogRead()获得的返回值是一个随机数值,该数值受到多种因素影响,例如将手指靠近该引脚也会引发变化。
analogWrite(pin, value) 向指定引脚写入模拟数值,以产生固定频率的 PWM 波形。Arduino Uno 提供35691011为 PWM 引脚,可输出频率为490 Hz(其中 5 和 6 引脚为 980Hz)的 PWM 波形。
▶ 参数pin:需要进行读取操作的模拟引脚编号;
▶ 参数value:为0(关断)到255(打开)之间的 整型占空比;
▶ 返回值:无;
analogReference(type) 配置模拟输入参考电压的范围与解析度,对于 Arduino Uno 可以选择如下参数:
▶ 参数typeDEFAULT(默认模拟参考电压为5V),INTERNALATmega328P 内建参考电压1.1V);
▶ 返回值:无;

高级 I/O

高级 I/O 功能描述
tone(pin, frequency,
duration)
用于生成指定频率(以及50%占空比)的方波信号,可以驱动压电式蜂鸣器或者扬声器播放指定频率音调。注意,Arduino Uno 只能同时通过tone()从一个引脚产生信号,此时其它引脚无法通过调用tone()输出信号。
▶ 参数pin:需要输出信号的引脚编号;
▶ 参数frequency:输出信号的频率,数据类型为unsigned int
▶ 参数duration:信号持续时间,以毫秒作为单位,数据类型为unsigned long
▶ 返回值:无;
noTone(pin) 停止生成由tone()函数触发的方波信号,如果需要使用多个引脚输出信号,必须在每个引脚输出信号前调用noTone()函数停止其它引脚输出信号。
▶ 参数pin:需要停止信号输出的引脚编号;
▶ 返回值:无;
pulseIn(pin, value,
timeout)
获取指定引脚的脉冲状态持续时间, 例如当前想检测脉冲信号HIGH,那么 Arduino 会在引脚变为HIGH时开始计时,引脚变为LOW之后停止记时,最后返回以毫秒为单位的脉冲持续时间,如果在超时时间内没有读取到脉冲信号,那么就会返回0
▶ 参数pin:需要获取脉冲状态的引脚编号;
▶ 参数value:需要读取的脉冲类型,值为HIGH或者LOW两者之一,数据类型为unsigned int
▶ 参数timeout:可选,超时时间,以毫秒为单位,默认为1秒,数据类型为unsigned long
▶ 返回值:脉冲持续的时间长度,如果在超时时间内没有读到脉冲信号则返回0,数据类型为unsigned long
pulseInLong(pin, value,
timeout)
pulseIn()的替代方法,更适合处理长脉冲和受中断影响的场景,其功能和参数与pulseIn()基本一致。但是由于该函数依赖于micros(),因此不能被用于noInterrupts()函数的上下文。
▶ 参数pin:需要获取脉冲状态的引脚编号;
▶ 参数value:需要读取的脉冲类型,值为HIGH或者LOW两者之一,数据类型为unsigned int
▶ 参数timeout:可选,超时时间,以毫秒为单位,默认为1秒,数据类型为unsigned long
▶ 返回值:脉冲持续的时间长度,如果在超时时间内没有读到脉冲信号则返回0,数据类型为unsigned long
shiftIn(dataPin, clockPin,
bitOrder)
将 1 个字节的数据通过移位方式逐位进行输入,数据可以从最高位(最左侧)或者最低位(最右侧)输入。Arduino 会首先在时钟引脚输出HIGH,然后通过数据引脚读取 1 位数据,读取完成之后时钟引脚会被设置为LOW
▶ 参数dataPin:输入位数据的引脚编号,允许的数据类型为int
▶ 参数clockPin:时钟信号引脚编号,允许的数据类型为int
▶ 参数bitOrder:位传送的顺序,MSBFIRST(最高有效位优先)或者LSBFIRST(最低有效位优先)二选一;
▶ 返回值:读取到的数据,允许的数据类型为byte
✸ 如果与 Arduino 通信的设备是在时钟引脚脉冲信号的上升沿发送数据,那么需要确保在调用shiftIn()之前,首先调用digitalWrite(clockPin, LOW)将时钟引脚设置为LOW,从而确保数据读取准确无误。
shiftOut(dataPin,
clockPin, bitOrder,
value)
将 1 个字节的数据通过移位方式逐位进行输出,数据可以从最高位(最左侧)或者最低位(最右侧)输出。Arduino 会首先在时钟引脚输出HIGH,然后通过数据引脚读取 1 位数据,读取完成之后时钟引脚会被设置为LOW
▶ 参数dataPin:输出位数据的引脚编号,允许的数据类型为int
▶ 参数clockPin:时钟信号引脚编号,允许的数据类型为int
▶ 参数bitOrder:位传送的顺序,MSBFIRST(最高有效位优先)或者LSBFIRST(最低有效位优先)二选一;
▶ 参数value:需要传送出的数据,允许的数据类型为byte
▶ 返回值:无;
✸ 如果目标设备是在时钟引脚脉冲信号的上升沿读取 Arduino 上的数据,那么就需要确保调用shiftOut()之前,首先调用digitalWrite(clockPin, LOW)将时钟引脚设置为LOW,从而确保数据读取准确无误。

注意:下面表格当中的两个函数是 Zero、Due、MKR 的专属 I/O 函数:

Zero、Due、MKR 专属 I/O 功能描述
analogReadResolution(bits) 以位bit为单位设置analogRead()返回值大小,由于 Zero、Due、MKR 系列开发板拥有 12 位 ADC 功能,其分辨率最高可以调整至12,可以让analogRead()函数的返回值介于0 ~ 4095之间。所以为了向后兼容 AVR 开发板,其默认值限定为10bit,返回值介于0 ~ 1023之间。
▶ 参数bits:决定analogRead()函数返回值的位分辨率,取值范围介于1 ~ 32之间,如果取值超出开发板提供的范围,则按照该开发板能够处理的最高分辨率处理。
▶ 返回值:无;
analogWriteResolution(bits) 以位bit为单位设置analogWrite()函数的输出分辨率,同样为了向后兼容 AVR 开发板,其默认值被限定为8bit,返回值介于0 ~ 255之间。
▶ 参数bits:决定analogWrite()函数所使用的位分辨率,取值范围介于1 ~ 32之间,如果取值超出了开发板提供的范围,则按照该开发板能够处理的最高分辨率处理。
▶ 返回值:无;

定时

定时 功能描述
delay(ms) 让程序的运行停顿参数指定的时间(以毫秒为单位),1000毫秒 = 1秒。
▶ 参数ms:延时时间,单位为毫秒,允许的数据类型为unsigned long
▶ 返回值:无;
delayMicroseconds(us) 让程序的运行停顿参数指定的时间(以微秒为单位),1000微秒 = 1毫秒。
▶ 参数ms:延时时间,单位为微秒,允许的数据类型为unsigned long
▶ 返回值:无;
micros() 记录 Arduino 启动后运行当前程序的时间(单位为微秒),最长可以记录接近70分钟。如果超出记录时间上限,记录将会从0重新开始。在16MHz的 Arduino Uno 开发板上,该函数的分辨率为4微秒,因此每次返回的时间值总是4的倍数。
▶ 返回值:Arduino 启动后运行当前程序的时间,以微秒作为单位 ,允许的数据类型为unsigned long
millis() 记录 Arduino 启动后运行当前程序的毫秒数,最长可以记录至50天,然后归0
▶ 返回值:Arduino 启动后运行当前程序的毫秒数 ,允许的数据类型为unsigned long

数学

数学 功能描述
abs(x) 计算一个数值的绝对值。
▶ 参数x:待计算绝对值的数值;
▶ 返回值:如果参数 x 大于或者等于 0 返回 +x;如果参数 x 小于 0 则返回 -x
constrain(x, a, b) 将一个数值限制在一个特定范围以内。
▶ 参数x:需要约束范围的数值,允许所有数据类型;
▶ 参数a:范围的下限,允许所有数据类型;
▶ 参数b:范围的上限,允许所有数据类型;
▶ 返回值:如果 a<x<b 返回 x;如果 x<a 返回 a;如果 x>b 返回 b
map(value, fromLow,
fromHigh, toLow,
toHigh)
将一个数值从一个区间等比例的映射至另外一个区间,即将 fromLow 的值映射至 toLowfromHigh 的值映射至 toHigh,中间值映射至中间值。
▶ 参数value:需要进行映射的数值;
▶ 参数fromLow:映射之前,区间的最小边界值;
▶ 参数fromHigh:映射之前,区间的最大边界值;
▶ 参数toLow:映射之后,区间的最小边界值;
▶ 参数toHigh:映射之后,区间的最大边界值;
▶ 返回值:被映射后的值;
max(x, y) 计算两个数值的最大值。
▶ 参数x:待比较的第 1 个值;
▶ 参数y:待比较的第 2 个值;
▶ 返回值:两个值当中的最大值;
min(x, y) 计算两个数值的最小值。
▶ 参数x:待比较的第 1 个值;
▶ 参数y:待比较的第 2 个值;
▶ 返回值:两个值当中的最小值;
pow(base, exponent) 计算一个幂函数的值。
▶ 参数base:底数,允许的数据类型为 float
▶ 参数exponent:指数,允许的数据类型为 float
▶ 返回值:求幂运算的结果,允许的数据类型为 double
sq(x) 计算数值的平方结果,即数字乘以自身的值。
▶ 参数x:待计算的数值,允许为任意数据类型;
▶ 返回值:数值的乘方结果,允许的数据类型为 double
sqrt(x) 计算数字的√ ̄平方根结果,即开方运算。
▶ 参数x:待计算的数值,允许为任意数据类型;
▶ 返回值:数值的平方根,允许的数据类型为 double
cos(rad) 以弧度为单位,计算角度的余弦值
▶ 参数rad:角度的弧度值,允许的数据类型为 float
▶ 返回值:角度的余弦值,允许的数据类型为 double
sin(rad) 以弧度为单位,计算角度的正弦值
▶ 参数rad:角度的弧度值,允许的数据类型为 float
▶ 返回值:角度的正弦值,允许的数据类型为 double
tan(rad) 以弧度为单位,计算角度的正切值
▶ 参数rad:角度的弧度值,允许的数据类型为 float
▶ 返回值:角度的正切值,允许的数据类型为 double
random(min, max) 用于生成伪随机数。
▶ 参数max:随机值的上界;
▶ 参数min:可选,随机值的下界;
▶ 返回值:一个介于 min ~ max-1 之间的伪随机数,允许的数据类型为 long
randomSeed(seed) 用于初始化一个伪随机数生成器,虽然伪随机数会从一个随机序列的任意一点开始生成,但是这个序列总是相同的。采用analogRead()这样随机的输入结果作为该函数参数,就可以确保该序列总是不同的。
▶ 参数seed:用于初始化伪随机数序列的种子值,允许的数据类型为 unsigned long
▶ 返回值:无;

字符

字符 功能描述
isAlpha(thisChar) 判断一个字符是否是字母,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isAlphaNumeric(thisChar) 判断一个字符是否为字母或者数字,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isAscii(thisChar) 判断一个字符是否为 ASCII,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isControl(thisChar) 判断一个字符是否为控制字符,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isDigit(thisChar) 判断一个字符是否为数字,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isGraph(thisChar) 判断一个字符是否可以打印内容(包含能够产生输出的任意字符,但是不包含无内容的空格),如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isHexadecimalDigit(thisChar) 判断一个字符是否是十六进制字符0 ~ 9a ~ f),如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isLowerCase(thisChar) 判断一个字符是否为小写格式,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isPrintable(thisChar) 判断一个字符是否可以打印(能够产生输出的任意字符,也包含空格),如果是则返回true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isPunct(thisChar) 判断一个字符是否是标点符号(即逗号、分号、感叹号等),如果是则返回true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isSpace(thisChar) 判断一个字符是否是空白字符(包括空格、分页符\f、换行符\n、回车\r、水平制表符\t、垂直制表符\v),如果是则返回true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isUpperCase(thisChar) 判断一个字符是否为大写格式,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true
isWhitespace(thisChar) 判断一个字符是否是空格或者水平制表符 \t,如果是则返回 true
▶ 参数thisChar:字符变量,允许的数据类型为 char
▶ 返回值:true

字符串

通过 String() 构造函数,可以构造出一个 String 类型的实例,该构造函数具有如下 3 种重载形式:

1
2
3
String(val)
String(val, base)
String(val, decimalPlaces)

构造函数 String() 当中的 valbasedecimalPlaces 三个参数分别具有如下意义:

  • val:需要转换为 String 实例的数据,可以是 stringcharbyteintlongunsigned intunsigned longfloatdouble 类型。
  • base:用于格式化整数值的基数(进制)。
  • decimalPlaces:指定小数点之后有几位,仅用于参数 val 为浮点类型 floatdouble 的时候。

下面的代码是一个关于 String() 构造函数应用的综合实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 字符与字符串处理 */
String demo = "Hello String"; // 使用常量字符串
String demo = String('a'); // 常量字符 char
String demo = String("This is a string"); // 常量字符串 sting
String demo = String(stringTwo + " with more"); // 连接两个字符串

/* 数值处理 */
String demo = String(13); // 整型常量
String demo = String(analogRead(0), DEC); // 十进制整型
String demo = String(45, HEX); // 十六进制整型
String demo = String(255, BIN); // 二进制整型
String demo = String(millis(), DEC); // 十进制长整型
String demo = String(5.698, 3); // 使用浮点数,并且指定小数点的位置

注意:使用 String() 构造函数将整型转换为字符串时,其结果总是为该整型数据的 ASCII 形式。

字符串 功能描述
charAt(n) 访问字符串的特定字符。
▶ 参数n:一个数据类型为 unsigned int 的变量。
▶ 返回值:字符串的第 n 个字符;
compareTo(myString) 逐个比较两个字符串当中字符的 ASCII 编码,这意味着 ab 之前,但是在大写字母 A 之后,而数字会在字母之前;
▶ 参数myString:另一个 String 类型的变量。
▶ 返回值:返回负数表示调用 String 出现在参数 String 之前,而返回正数则表示调用 String 出现在参数 String 之后,返回表示两个字符串相等;
concat(parameter) 将一个参数附加到 String 对象上面。
▶ 参数parameter:允许的数据类型有 Stringstringcharbyteintunsigned intlongunsigned longfloatdouble
▶ 返回值:成功返回 true,失败返回 false
c_str() 将字符串的内容转换为 C 语言风格的以空字符结尾的字符串。
▶ 返回值:转换之后的 C 语言风格字符串指针;
endsWith(myString) 判断 String 类型字符串是否以指定的 String 类型字符串结尾。
▶ 参数myString:另一个 String 类型的变量;
▶ 返回值:如果调用 String 以参数 String 结尾返回 true,否则返回 false
equals(myString) 比较两个字符串是否相等,这种比较是区分大小写的,这意味着 hello 不等于 HELLO
▶ 参数myString:另一个 String 类型的变量;
▶ 返回值:如果调用 String 与参数 String 相等返回 true,否则返回 false
equalsIgnoreCase(myString) 比较两个字符串是否相等,但是这种比较并不会区分大小写的,因此 hello 就会等于 HELLO
▶ 参数myString:另一个 String 类型的变量;
▶ 返回值:如果调用 String 与参数 String 在忽略大小写的情况下相等返回 true,否则返回 false
getBytes(buf, len) 将调用 String 的字符复制到指定大小 len 的缓冲区 buf
▶ 参数buf:目标缓冲区,允许数据类型是 byte 类型的数组;
▶ 参数len:缓冲区大小,允许 unsigned int 类型的数据;
▶ 返回值:无;
indexOf(val, from) 从调用 String 的开始位置查找一个指定的字符或者字符串
▶ 参数val:数据类型为 charString,待搜索的字符或者字符串;
▶ 参数from:开始搜索的字符串索引位置;
▶ 返回值:参数 val 在字符串当中的索引,没有如果找到则返回 -1
lastIndexOf(val, from) 从调用 String 的结束位置查找一个指定的字符或者字符串
▶ 参数val:数据类型为 charString,待搜索的字符或者字符串;
▶ 参数from:开始搜索的字符串索引位置;
▶ 返回值:参数 val 在字符串当中的索引,没有如果找到则返回 -1
length() 以字符作为单位,返回字符串的长度,注意返回结果不会包含字符串末尾的结束字符
▶ 参数buf:目标缓冲区,允许数据类型是 byte 类型的数组;
▶ 参数len:缓冲区大小,允许 unsigned int 类型的数据;
▶ 返回值:无;
remove(index, count) 删除调用字符串当中,指定索引位置 index 的指定数量字符 count
▶ 参数index:数据类型为 unsigned int,开始进行删除操作的字符索引;
▶ 参数count:数据类型为 unsigned int,需要删除的字符数量;
▶ 返回值:无;
replace(substring1, substring2) 将字符串当中的指定部分 substring1 替换为 substring2
▶ 参数substring1:数据类型为 String,希望替换的字符串;
▶ 参数substring2:数据类型为 String,替换之后的字符串;
▶ 返回值:无;
reserve(size) 在内存当中分配一个缓冲区来处理 String 对象。
▶ 参数size:数据类型为 unsigned int,内存缓冲区占用的字节数;
▶ 返回值:无;
setCharAt(index, c) 设置字符串指定索引位置 index 为字符 c,如果索引超出字符串长度,则不会产生任何影响。
▶ 参数index:设置字符的索引;
▶ 参数c:等待设置的字符;
▶ 返回值:无;
startsWith(myString) 测试调用 String 是否以参数 String 作为开始。
▶ 参数myString:数据类型为 String 的变量;
▶ 返回值:无;
substring(from, to) 从调用字符串截取子字符串,如果省略结束索引 to,该子字符串将会截取至调用字符串的末尾。
▶ 参数from:待截取子字符串的起始索引;
▶ 参数to:可选参数,待截取子字符串的结束索引;
▶ 返回值:无;
toCharArray(buf, len) 将调用 String 当中的字符复制到指定长度为 len 的缓冲区 buf
▶ 参数buf:数据类型为 char 字符数组,复制操作的目标缓冲区;
▶ 参数len:数据类型为 unsigned int,缓冲区的大小;
▶ 返回值:无;
toDouble() 将调用字符串 String 转换为双精度浮点数
▶ 返回值:转换后的 double 数据类型,如果调用字符串不是以数值开头,导致转换不能有效进行,则返回 0
toInt() 将调用字符串 String 转换为整型数
▶ 返回值:转换后的 long 数据类型,如果调用字符串并非整型数据,致使转换不能有效进行,则返回 0
toFloat() 将调用字符串 String 转换为浮点数
▶ 返回值:转换后的 float 数据类型,如果调用字符串不是以数值开头,致使转换不能有效进行,则返回 0
toLowerCase() 将调用字符串 String 全部就地转换为小写格式
▶ 返回值:无;
toUpperCase() 将调用字符串 String 全部就地转换为大写格式
▶ 返回值:无;
trim() 就地去除调用字符串 String 前后的空格。
▶ 返回值:无;

位操作

位操作 功能描述
bit(n) 计算指定位的值(第 0 位是 1,第 1 位是 2,第 2 位是 4 等等)。
▶ 参数n:需要计算的位;
▶ 返回值:位的值;
bitClear(x, n) 清除一个数值变量的指定位。
▶ 参数x:待进行位清除操作的数字变量;
▶ 参数n:待清除的位;
▶ 返回值:无;
bitRead(x, n) 读取指定数值变量的 1 位。
▶ 参数x:待进行位读取操作的数值;
▶ 参数n:需要读取哪一位,对于最右侧的最低有效位,从第 0 位开始;
▶ 返回值:无;
bitSet(x, n) 将某个数值变量的指定位设置为 1
▶ 参数x:待进行位设置的数值;
▶ 参数n:需要设置的位,对于最右侧的最低有效位,从第 0 位开始;
▶ 返回值:无;
bitWrite(x, n, b) 对数值变量的指定位进行写入操作。
▶ 参数x:待进行位写入操作的数值;
▶ 参数n:需要写入的位,对于最右侧的最低有效位,从第0位开始;
▶ 参数b:需要写入的二进制值(0或者1);
▶ 返回值:无;
highByte(x) 提取 1 个 word 字的最左侧高位字节(或者对于较大的数据类型是第 2 个最低字节)。
▶ 参数x:一个任意类型的值;
▶ 返回值:提取的字节,允许的数据类型为byte
lowByte(x) 提取 1 个 word 字的最右侧低位字节
▶ 参数x:一个任意类型的值;
▶ 返回值:提取的字节,允许的数据类型为 byte

中断

中断 功能描述
attachInterrupt(digitalPinToInterrupt(pin), ISR, mode) 用于添加外部中断,可用于中断的数字引脚:Uno23 号引脚,Mega25602318192021 号引脚,以及 Due 的所有数字引脚;
▶ 参数interrupt:中断编号,通常使用 digitalPinToInterrupt(pin) 函数将实际数字引脚转换为特定中断编号,允许的数据类型为 int
▶ 参数pin:发生中断的引脚编号;
▶ 参数ISR:中断服务函数,中断发生时自动调用,该函数必须没有参数与返回值;
▶ 参数modeLOW:引脚为低电平时触发中断;CHANGE:引脚电平发生变化时触发中断;RISING:引脚电平由低电平变为高电平时触发中断;FALLING:引脚电平由高电平变为低电平时触发中断;此外,Arduino Due、Zero 和 MKR1000 还允许为 HIGH,即引脚为高电平时触发中断;
▶ 返回值:无;
✸ 中断服务程序(ISR,Interrupt Service Routine)应当尽可能短小快速。如果当前 Sketch 设置有多个中断服务,那么每次只能运行 1 个中断,其它中断将会在当前中断执行完成以后,按照优先级顺序进行执行。由 millis() 依赖于中断进行计数,而 delay() 需要中断才能生效,因此在中断服务程序内部调用它们将会失效;micros() 最初可以正常工作,但是在 1 ~ 2 毫秒以后发生异常;最后,由于 delayMicroseconds() 不使用任何计数器,因此将会得到正常运行。
✸ 通常采用声明为 volatile 的全局变量在主程序与中断服务程序之间传递数据,从而确保变量在它们之间得到正确的更新;
detachInterrupt(digitalPinToInterrupt(pin)) 关闭指定的外部中断
▶ 参数interrupt:需要禁用的中断编号;
▶ 参数pin:待禁用外部中断的引脚编号;
▶ 返回值:无;
interrupts() 重新使能中断(在 nointerrupts() 禁用中断之后),中断用于让某些任务运行在后台,其默认情况下是使能的,禁用中断以后会导致一些内置函数无法正常工作。
▶ 参数:无;
▶ 返回值:无;
noInterrupts() 禁用中断(可以调用 interrupts() 重新使能中断),禁用以后会影响到一些内置函数的工作。
▶ 参数:无;
▶ 返回值:无;

I/O 工具类

stream

stream 类 是所有字符流二进制流的基类,通常不会直接进行调用,主要在继承于它的 SerialWireEthernetSD 等相关工具函数当中使用。

stream 字符与二进制流 功能描述
stream.available() 获取已经接收到的数据流当中可用的字节数,该函数是 Stream 类的一部分,可以被继承它的类(WireSerial 等)调用;
▶ 参数:无;
▶ 返回值:无;
stream.read() 将输入流当中的字符读取至缓冲区。
▶ 参数:无;
▶ 返回值:输入数据的第 1 个有效字节,如果没有数据,则返回 -1。;
stream.flush() 在所有字符发送完毕之后,清除缓冲区。
▶ 参数:无;
▶ 返回值:无;
stream.find(target, length) 从流当中读取数据,直至查询到目标;。
▶ 参数target:待检索的字符串,数据类型为 char
▶ 参数length:目标的长度,数据类型为 size_t
▶ 返回值:如果找到目标返回 true,如果超时则返回 false,数据类型为 bool
stream.findUntil(target, terminal) 从流当中读取数据,直至找到指定长度的目标字符串/结束符,或者发生超时退出。
▶ 参数target:待检索的字符串,数据类型为 char
▶ 参数terminal:搜索当中的结束字符串,数据类型 char
▶ 返回值:如果检索到目标字符串返回 true,如果超时则返回 false,数据类型为 bool
stream.peek() 从文件中读取 1 个字节,但是并不跳转至下 1 个字节,连续调用该函数会返回与下一次调用 read() 相同的值。
▶ 参数:无;
▶ 返回值:下一个字节或者字符,如果没有则返回 -1
stream.readBytes(buffer, length) 将数据流当中的字符读入缓冲区,读取完成或者超时就会终止。
▶ 参数buffer:用来存储字节的缓冲区,数据类型为 char 或者 byte 数组;
▶ 参数length:待读取的字节数,数据类型为 int
▶ 返回值:缓冲区中的字节数,数据类型为 size_t
stream.readBytesUntil(character,
buffer, length)
从流中读取字符到缓冲区,如果检测到终止符字符,或者读取完成以及超时的时候,就会终止。
▶ 参数character:待检索的字符,数据类型为 char
▶ 参数buffer:用来存储字节的缓冲区,数据类型为 char 或者 byte数组;
▶ 参数length:待读取的字节数,数据类型为 int
▶ 返回值:放置在缓冲区当中的字节数;
stream.readString() 将数据流当中的字符读取到字符串,如果超时则函数终止。
▶ 参数:无;
▶ 返回值:继承自 Stream 类的实例;
stream.readStringUntil(terminator) 从流中读取字符到字符串,如果检测到终止符或者超时则函数就会终止。
▶ 参数terminator:待检索的字符,数据类型为 char
▶ 返回值:从数据流当中读取到的全部字符串(以结束符为止);
stream.parseInt(lookahead, ignore) 返回当前位置第 1 个有效的整型 int 或者长整型 long 数据;
▶ 参数lookahead:设定在数据流当中查找整型数据的模式,可以选择 SKIP_ALL(默认模式,忽略除负号、小数点或数字之外的所有字符),SKIP_NONE(没有任何内容被忽略),SKIP_WHITESPACE(忽略制表符、空格、换行符、回车符)当中的一种模式;
▶ 参数ignore:数据类型为 char,用于在搜索当中跳过指定的字符;
▶ 返回值:返回的数据类型为 long
stream.parseFloat(lookahead, ignore) 返回当前位置的第 1 个有效的浮点型数据,然后以第 1 个非浮点数的字符结束,如果发生超时则函数将会终止;
▶ 参数lookahead:设定在数据流当中查找浮点数的模式,可以选择 SKIP_ALL(默认模式,忽略除负号、小数点或数字之外的所有字符),SKIP_NONE(没有任何内容被忽略),SKIP_WHITESPACE(忽略制表符、空格、换行符、回车符)当中的一种模式;
▶ 参数ignore:数据类型为 char,用于在搜索当中跳过指定的字符;
▶ 返回值:返回的数据类型为 float
stream.setTimeout(time) 设置数据流等待的最大毫秒数,默认值为 1000 毫秒。
▶ 参数time:单位为毫秒的超时时间,数据类型为 long
▶ 返回值:无;

Serial

Serial 类用于 Arduino 开发板与其它设备进行串行通信,所有 Arduino 开发板都至少拥有一组 UART(通用异步收发器)或者 USART(通用同步/异步收发器)串行通讯接口。

Arduino 型号 SERIAL 引脚 SERIAL1 引脚 SERIAL2 引脚 SERIAL3 引脚
Uno - 0(RX) / 1(TX) - -
Mega 0(RX) / 1(TX) 19(RX) / 18(TX) 17(RX) / 16(TX) 15(RX) / 14(TX)
ESP8266 - GPIO1(TX) / GPIO3(RX) GPIO2 (TX) -
ESP32 - GPIO1(TX) / GPIO3(RX) - -

Arduino 与 Uno 和 Mega 与计算机通信的引脚分别为 01,连接到这些引脚会干扰通信,导致固件上传失败。

串行通信 功能描述
Serial.if(Serial) 判断串行通信接口是否已经就绪。
▶ 参数:无;
▶ 返回值:如果指定的串行端口可用就返回 true,否则返回 false
Serial.available() 获取当前可以从串口读取的字节或者字符数量,即已经接收到并且保存在串行接收缓冲区当中的数据。
▶ 参数:无;
▶ 返回值:可供读取的字节数量;
Serial.availableForWrite() 非阻塞写入操作的情况下,获取当前串行缓冲区写入的字节或者字符数。
▶ 参数:无;
▶ 返回值:可供写入的字节数量;
Serial.begin(speed, config) 设置串行数据传输的速率(以比特/秒作为单位),以及数据位奇偶校验位停止位(默认为 8 个数据位,1 个停止位,无奇偶校验位)。
▶ 参数speed:串行通信的波特率,数据类型为长整型 long
▶ 参数config:设置数据位奇偶校验位停止位,可选择的值有 SERIAL_5N1SERIAL_6N1SERIAL_7N1SERIAL_8N1(默认)、SERIAL_5N2SERIAL_6N2SERIAL_7N2SERIAL_8N2SERIAL_5E1(偶数校验)、SERIAL_6E1SERIAL_7E1SERIAL_8E1SERIAL_5E2SERIAL_6E2SERIAL_7E2SERIAL_8E2SERIAL_5O1(奇数校验)、SERIAL_6O1SERIAL_7O1SERIAL_8O1SERIAL_5O2SERIAL_6O2SERIAL_7O2SERIAL_8O2
▶ 返回值:无;
Serial.end() 关闭串行通信,让RXTX引脚可以用作通用的输入输出(GPIO),调用Serial.begin()以后可以重新打开串行通信。
▶ 参数:无;
▶ 返回值:无;
Serial.find(target, length) 从串行缓冲区检索数据,如果找到目标数据,函数返回 true,如果超时则返回 false
▶ 参数target:待检索的字符串,数据类型为字符型 char
▶ 参数length:检索到的目标长度,数据类型为字符型 size_t
▶ 返回值:true 或者 false
Serial.findUntil(target, terminal) 从串行缓冲区读取数据,直至查询到指定长度的目标字符串或者终止符字符串(检索到目标返回 true,超时则返回 false)。
▶ 参数target:待检索的字符串,数据类型为字符型 char
▶ 参数terminal:检索过程当中的结束字符,数据类型为字符型 char
▶ 返回值:true 或者 false
Serial.flush() 等待发送出去的串行数据传输完成。
▶ 参数:无
▶ 返回值:无
Serial.parseFloat(lookahead, ignore) 从串行缓冲区返回第 1 个有效的浮点数据,超时以后将会自动退出。
▶ 参数lookahead:设定在数据流当中查找浮点数的模式,可以选择 SKIP_ALL(默认模式,忽略除负号、小数点或数字之外的所有字符),SKIP_NONE(没有任何内容被忽略),SKIP_WHITESPACE(忽略制表符、空格、换行符、回车符)当中的一种模式;
▶ 参数ignore:数据类型为 char,用于在搜索当中跳过指定的字符;
▶ 返回值:返回的数据类型为 float
Serial.parseInt(lookahead, ignore) 从输入的串行数据当中查找一个有效的整型数据,超时以后将会自动退出。
▶ 参数lookahead:设定在数据流当中查找整型数据的模式,可以选择 SKIP_ALL(默认模式,忽略除负号、小数点或数字之外的所有字符),SKIP_NONE(没有任何内容被忽略),SKIP_WHITESPACE(忽略制表符、空格、换行符、回车符)当中的一种模式;
▶ 参数ignore:数据类型为 char,用于在搜索当中跳过指定的字符;
▶ 返回值:返回的数据类型为 long
Serial.peek() 返回输入串行数据的下一个字节或者字符,而不将其从内部串行缓冲区移除,连续调用peek()将会返回相同的字符。
▶ 参数:无;
▶ 返回值:当前串行数据可用的第 1 个字节,返回 -1 则表示没有数据可用,数据类型为整型 int
Serial.print(val, format) 将数据以人类可读的 ASCII 文本打印至串行通信端口。
▶ 参数val:等待打印的值,可以是任意数据类型;
▶ 参数format:打印格式,可以设置为 BIN(二进制)、OCT(八进制)、DEC(十进制)、HEX(十六进制),对于浮点数,该参数用 于设定需要使用到的小数位数;
▶ 返回值:返回写入的字节数,数据类型为 size_t
Serial.println(val, format) 将数据以 ASCII 文本格式打印至串行端口,并且添加返回字符(ASCII 码13或者'\r')和换行字符 (ASCII 码10或者'\n')。
▶ 参数val:等待打印的值,可以是任意数据类型;
▶ 参数format:打印格式,可以设置为 BIN(二进制)、OCT(八进制)、DEC(十进制)、HEX(十六进制),对于浮点数,该参数用于设定需要使用到的小数位数;
▶ 返回值:返回写入的字节数,数据类型为 size_t
Serial.read() 读取输入的串行数据;
▶ 参数:无;
▶ 返回值:当前串行数据可用的第 1 个字节,返回 -1 则表示没有数据可用,数据类型为整型 int
Serial.readBytes(buffer, length) 从串行端口读取字符到缓冲区,如果读取完成或者超时则函数终止。
▶ 参数buffer:用来存储字节的缓冲区,可以使用 charbyte 数组数据类型;
▶ 参数length:需要读取的字节数,数据类型为 int
▶ 返回值:放置在缓冲区当中的字节数,数据类型为 size_t
Serial.readBytesUntil(character,
buffer, length)
将串行缓冲区的字符读取到数组里,如果检测到终止字符或者读取完成以及超时则函数终止。
▶ 参数character:待读取的字符串,数据类型为 char
▶ 参数buffer:用来存储字节的缓冲区,数据类型为 char 或者 byte 数组;
▶ 参数length:待读取的字节长度,数据类型为 int
▶ 返回值:数据类型为 size_t
Serial.readString() 将串行缓冲区的字符读取到一个字符串当中,如果超时则函数中止。
▶ 参数:无;
▶ 返回值:从串行缓冲区读取的字符串;
Serial.readStringUntil(terminator) 将串行缓冲区中的字符读取为字符串,如果检测到终止字符或者超时则函数终止。
▶ 参数terminator:待搜索的字符,数据类型为 char
▶ 返回值:从串行缓冲区读取的字符串(以结束符为止);
Serial.setTimeout(time) 设置串行数据等待的最大毫秒数,默认为1000毫秒。
▶ 参数time:单位为毫秒的超时时间,数据类型为 long
▶ 返回值:无;
Serial.write(val)
Serial.write(str)
Serial.write(buf, len)
将二进制数据写入串口,这些数据将会以字节字节序列的形式发送;要发送代表数字的字符,请使用print()函数。
▶ 参数val:希望以单个字节发送的值;
▶ 参数str:希望以字节序列形式发送的字符串;
▶ 参数buf:希望以字节序列形式发送的数组;
▶ 参数len:需要从上面 buf 数组发送的字节长度;
▶ 返回值:写入的字节长度,数据类型为 size_t
Serial.serialEvent() 当串行数据可用时调用该事件函数,然后就可以使用Serial.read()来读取这些数据。
▶ 参数statements:任意有效的 Arduino C++ 语句;
▶ 返回值:无;

Wire

Wire 库 用于设备之前的 I²C/TWI 协议通信,下面表格展示了 I²C 引脚在各种 Arduino 开发板上的默认位置:

Arduino 型号 I²C/TWI 功能引脚
Uno A4(SDA)、A5(SCL)
Mega2560 20(SDA)、21(SCL)
Due 20(SDA)、21(SCL)、SDA1SCL1
ESP8266 GPIO4/D2(SDA)、GPIO5/D1(SCL)
ESP32 GPIO21(SDA)、GPIO22(SCL)

使用该 Wire 库时,需要在代码当中包含 #include <Wire.h> 头文件,此外还需要注意如下两点:

  • I²C/TWI 地址拥有 7 位和 8 位两种格式,其中 7 位用于标识设备,而第 8 位则用于确定其读写状态,Wire 库至始至终使用 7 位地址。如果参考代码当中存在 8 位地址,那么就需要删除其最低位,即将值向右移动一位,从而变成一个位于 0 ~ 127 范围的地址。但是,其中 0 ~ 7 范围的地址为系统保留使用,因而可供开发人员使用的地址从 8 开始;
  • Wire 库的实现当中使用 32 字节的缓冲区,所有的通信操作都应当位于该范围以内,单次传输当中超出的字节都将会被丢弃;
I²C 总线通信 功能描述
Wire.begin(address) 初始化 Wire 库,以或者方式加入 I²C 总线,该函数通常只会被调用一次。
▶ 参数address:7 位从机(Slave)地址,如果不指定,则表明当前设备为主机(Master);
▶ 返回值:无;
Wire.requestFrom(address, quantity, stop) 主设备从设备请求字节数据,然后就可以调用 available()read() 读取这些字节数据。
▶ 参数address:被请求字节数据的从设备 7 位地址; ▶ 参数quantity:请求的字节(Bytes)数; ▶ 参数stop:布尔类型,如果为 true 就会在请求完成之后发送一个停止信息,从而释放总线资源;如果为 false,则会在请求完成之后,不断的重复发送请求,持续进行连接;
▶ 返回值:从设备返回的字节(Bytes)数据;
Wire.beginTransmission(address) 开始使用指定的地址向 I²C 从设备传输数据。
▶ 参数address:被传输字节数据的从设备 7 位地址;
▶ 返回值:无;
Wire.endTransmission(stop) 结束指定地址的 I²C 从设备数据传输。
▶ 参数stop:布尔类型,设置为 true 将发送一个停止消息,在传输结束之后释放 I²C 总线;设置为 false 将会重复进行发送,保持连接一直处于活动状态;
▶ 返回值:传输状态标识,0 表示成功,1 表示数据对于传输缓冲区而言过长,2 接收来自传输地址的否定应答,3 接收来自传输地址的否定数据,4 表示其它错误;
Wire.available() 返回可以使用 read() 进行读取的字节数量,可以在主设备执行 requestFrom() 之后调用,或者在从设备执行完 onReceive() 回调函数以后调用;
▶ 参数:无;
▶ 返回值:可用于进行读取的字节数量;
Wire.read() 读取执行 requestFrom() 之后,从设备传输到主设备的字节数据,或者从主设备传递给从设备的字节数据。
▶ 参数:无;
▶ 返回值:当前接收到的下一个字节数据;
Wire.write(value)
Wire.write(string)
Wire.write(data, length)
读取执行 requestFrom() 之后,从设备传输到主设备的字节数据,或者从主设备传递给从设备的字节数据。
▶ 参数 value:作为单个字节数据发送的值;
▶ 参数 string:作为一系列字节数据发送的字符串;
▶ 参数 data:将以字节形式发送的数据数组;
▶ 参数 length:需要传输的字节数量;
▶ 返回值:写入的字节数量;
Wire.setClock() 设置 I²C 总线工作的时钟频率。
▶ 参数:需要设置的时钟频率,单位为赫兹
▶ 返回值:无;
Wire.onReceive(handler) 注册一个回调函数,在从设备接收到主设备传输的数据时被调用。
▶ 参数:回调函数(参数为从主设备读取到的字节数量,无返回值),例如 void myHandler(int numBytes)
▶ 返回值:无;
Wire.onRequest(handler) 注册一个回调函数,在主设备从设备请求数据时被自动调用。
▶ 参数:回调函数(即没有参数,也没有返回值),例如 void myHandler();
▶ 返回值:无;

注意:从 Arduino 1.0 开始,Wire 库继承自 Stream 函数,因此其读写函数 send()receive() 已经被最新的 read()write() 代替。

使用一个 I²C 外围设备之前,通常需要查询该设备对应的 I²C 总线地址,在 ESP32ESP8266 当中执行如下代码,就可以在 Arduino 的串口监视器当中(波特率为 9600),查询当前连接到开发板上的设备 I²C 地址。

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
#include <Wire.h>

void setup() {
Wire.begin();
Serial.begin(9600); // 设置串口监视器的波特率为 9600
while (!Serial)
; // 等待串口监视器准备完成
Serial.println("\n===============优雅的分隔线===============");
Serial.println("【正在运行 Hank 的 I²C 地址扫描程序】");
}

void loop() {
byte error, address; // 错误以及 I²C 地址变量
int devicecount;

Serial.println("开始扫描...");

devicecount = 0;
for (address = 1; address < 127; address++) {
/* I²C 地址扫描程序通过 Wire.endTransmission() 的返回值来查询设备地址 */
Wire.beginTransmission(address);
error = Wire.endTransmission();

if (error == 0) {
Serial.print("发现 I²C 设备的地址 0x");
if (address < 16)
Serial.print("0");
Serial.print(address, HEX);
Serial.println(" !");
devicecount++;
} else if (error == 4) {
Serial.print("未知错误发生在 I²C 地址 0x");
if (address < 16)
Serial.print("0");
Serial.println(address, HEX);
}
}
if (devicecount == 0)
Serial.println("没有找到 I²C 设备!\n");
else
Serial.println("完成!\n");

delay(5000); // 5 秒之后开始下一次扫描
}

SPI

SPI 库 用于让 Arduino 以主机身份(Master)实现 SPI 总线通信,使用时需要在代码当中包含如下头文件:

1
#include <SPI.h>

串行外设接口(SPI,Serial Peripheral Interface)用于微控制器与一个或多个外围设备,以同步串行方式进行短距离快速通信,该协议包含有 4 条数据线:

  1. MISO (Master In Slave Out):主机输入,从机输出;
  2. MOSI (Master Out Slave In):主机输出,从机输入;
  3. SCK (Serial Clock):同步主机时钟;
  4. SS (Slave Select):从机片选,即指定主机需要调用的从机

注意:当一个设备的从机片选引脚为低电平时,该设备就可以与主机进行通信;而为高电平时,则会忽略主机;这样就可以使得多个 SPI 设备共享相同的 MISOMOSICLK 网络。

在 Arduino 当中为 SPI 设备编写代码时,需要注意如下一些事项:

  • 设备最大可用的 SPI 速度,其值可以通过 SPISettings() 函数的第 1 个参数进行控制,例如额定频率为 15 MHz,则该参数就填写 15000000
  • 数据以最高有效位(MSB,Most Significant Bit)先移动,还是最低有效位(LSB,Least Significant Bit)先移动,由 SPISettings() 第 2 个参数设置;
  • 数据时钟空闲时的电平状态,在时钟脉冲信号的上升沿还是下降沿取样,该项由 SPISettings() 的第 3 个参数进行控制;

注意:SPI 协议相对比较松散,每一种设备的实现细节都会有所区别,所以编写代码时必须注意阅读芯片的数据手册。

通常情况下,SPI 总线可以划分为 4 种传输模式,这些模式控制着数据是在时钟信号的上升沿还是下降沿进行移入移出,称为时钟相位(CPOL,Clock Polarity);以及时钟是在高电平还是低电平时空闲,称为时钟极性(CPHA,Clock Phase),这 4 种模式的相位与极性如下表所示:

模式 时钟相位 时钟极性 输出沿 数据采集
SPI_MODE0 0 0 下降沿 上升沿
SPI_MODE1 0 1 上升沿 下降沿
SPI_MODE2 1 0 上升沿 下降沿
SPI_MODE3 1 1 下降沿 上升沿

当选定好参数之后,就可以调用 SPI.beginTransaction() 启动 SPI 端口,并且直接将 SPI.SPISettings() 作为函数参数,例如:

1
SPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0));

注意:如果其它库想要在中断当中使用 SPI,则必须调用 SPI.endtransaction() 结束当前连接。

对于大多数 SPI 设备而言,会在调用 SPI.begintransaction() 之后,输出低电平片选从设备;接着再调用 SPI.transfer() 开始传输数据,结束之后调用 SPI. endtransaction(),让片选端输出高电平,下面的表格,展示了 Arduino 开发板 SPI 引脚的默认分布情况:

Arduino 型号 MOSI MISO SCK SS 工作电平
Uno GPIO11ICSP4 GPIO12ICSP1 GPIO13ICSP3 GPIO10 5V
Mega2560 GPIO51ICSP4 GPIO50ICSP1 GPIO52ICSP3 GPIO53 5V
ESP8266 GPIO13 GPIO12 GPIO14 GPIO15 3.3V
ESP32 GPIO23 GPIO19 GPIO18 GPIO5 3.3V
SPI 总线通信 功能描述
SPISettings(speedMaximum, dataOrder, dataMode) 该对象用于配置设备的 SPI 通信端口。
▶ 参数 speedMaximum: 设置 SPI 通信的最大速度,例如 20 MHz 就设置为 20000000
▶ 参数 dataOrder: 最高有效位优先 MSBFIRST,最低有效位优先 LSBFIRST
▶ 参数 dataMode: 一共拥有 SPI_MODE0SPI_MODE1SPI_MODE2SPI_MODE3 四种模式;
▶ 返回值:无;
SPI.begin() 初始化 SPI 总线,即设置 SCKMOSISS 引脚为输出模式,并且将 SCKMOSI 的电平拉低,而 SS 的电平拉高。
▶ 参数: 无;
▶ 返回值:无;
SPI.end() 关闭 SPI 总线,注意此时引脚的工作模式不会发生改变。
▶ 参数: 无;
▶ 返回值:无;
SPI.beginTransaction(mySettings) 使用 SPISettings 对象初始化 SPI 总线。
▶ 参数 mySettings: 已经配置好的 SPISettings 对象
▶ 返回值:无;
SPI.endTransaction() 停止使用 SPI 总线,通常在取消片选之后再进行调用,以释放 SPI 总线资源。
▶ 参数: 无;
▶ 返回值:无;
SPI.setBitOrder(order) 该函数已经废弃,请使用 SPISettingsSPI.beginTransaction() 来配置 SPI 参数。
▶ 参数 order: 最高有效位优先 MSBFIRST,最低有效位优先 LSBFIRST
▶ 返回值:无;
SPI.setClockDivider(divider) 该函数已经废弃,请使用 SPISettingsSPI.beginTransaction() 来配置 SPI 参数。
▶ 参数 divider: 仅可用于采用 AVR 微控制器的 Arduino,可以选择将其设置为 SPI_CLOCK_DIV2SPI_CLOCK_DIV4SPI_CLOCK_DIV8SPI_CLOCK_DIV16SPI_CLOCK_DIV32SPI_CLOCK_DIV64SPI_CLOCK_DIV128 当中的一项;
▶ 参数 slaveSelectPin: 仅用于 Arduino Due,从设备选择引脚;
▶ 参数 divider: 仅用于 Arduino Due,取值范围为 1 ~ 255
▶ 返回值:无;
SPI.setDataMode(mode) 该函数已经废弃,请使用 SPISettingsSPI.beginTransaction() 来配置 SPI 参数。
▶ 参数 mode: 选择 SPI 总线的工作模式,可以设置为 SPI_MODE0SPI_MODE1SPI_MODE2SPI_MODE3
▶ 参数 slaveSelectPin: 仅用于 Arduino Due,从设备选择引脚;
▶ 返回值:无;
receivedVal = SPI.transfer(val)
receivedVal16 = SPI.transfer16(val16)
SPI.transfer(buffer, size)
SPI 总线可以同时收发数据,其中接收到的数据以 receivedVal 或者 receivedVal16 形式返回;如果接收到的数据被存储在一个缓冲区当中,那么旧的数据会被新接收到的数据替换掉。
▶ 参数 val: 需要通过 SPI 总线发送出去的字节数据;
▶ 参数 val16: 需要使用 SPI 总线发送的两字节变量;
▶ 参数 buffer: 待传输的数据的数组;
▶ 返回值:Arduino 主设备在 SPI 总线上接收到的数据;
SPI.usingInterrupt(interruptNumber) 如果 Arduino 程序需要在中断当中执行 SPI 操作,那么可以调用这个函数注册该中断编码,从而使得 SPI.beginTransaction() 能够防止冲突的发生,即在调用该函数时指定的中断会在调用 beginTransaction() 时被禁用,然后在执行 endTransaction() 以后被重新启用。
▶ 参数 interruptNumber: 关联的中断编号
▶ 返回值:无;

外设工具类

Mouse

Mouse 类可以让基于32u4SAMD微控制器的开发板(Leonardo、Esplora、Zero、Due、MKR)通过其 Micro USB 接口控制其所连接计算机上的鼠标。

Mouse 鼠标控制 功能描述
Mouse.begin() 用于模拟连接到计算机的鼠标,结束控制可以调用Mouse.end()
Mouse.click(button) 在当前鼠标所在的位置向计算机发送瞬时单击,作用与按下并立即释放鼠标按键相同。
Mouse.end() 用于停止模拟连接至计算机的鼠标。
Mouse.move(xVal, yVal, wheel) 移动当前连接计算机上的鼠标位置,屏幕上的运动始终相对于光标当前的位置,使用之前必须首先调用Mouse.begin()
Mouse.press(button) 将按下鼠标按键的操作发送至当前连接的计算机,类似于单击并且连续按住鼠标按键,可以调用Mouse.release()取消该操作。
Mouse.release(button) 松开之前按下的鼠标按键。
Mouse.isPressed(button) 检查当前指定鼠标按键的状态。

Keyboard

Keyboard 类可以让基于32u4SAMD微控制器的开发板(Leonardo、Esplora、Zero、Due、MKR)通过 Micro USB 控制其所连接计算机上的键盘。

Keyboard 键盘控制 功能描述
Keyboard.begin() 使用 Due 开发板时,表示开始模拟连接到计算机的键盘,调用Keyboard.end()可以结束控制。
Keyboard.end() 停止 Arduino 对当前连接计算机的键盘的仿真。
Keyboard.press(key) 模拟按下并且按住键盘的操作,主要用于组合按键,可以调用Keyboard.release或者Keyboard.releaseAll结束操作,使用前必须首先调用Keyboard.begin()
Keyboard.print(characters) 将击键操作发送至当前连接的计算机。
Keyboard.println(characters) 将击键操作发送至当前连接的计算机,然后发送换行符和回车符。
Keyboard.release(key) 松开指定的键盘按键。
Keyboard.releaseAll() 松开当前按下的所有按键。
Keyboard.write(character) 模拟按下并且释放键盘上的按键,可以直接用于发送 ASCII 字符或者其它的修饰符以及特殊按键。

MEGA 2560

Arduino Mega 2560开发板基于ATmega2560微控制器,与 Uno 上采用的 ATmega328P 同属 8 位 AVR 微控制器,其工作电压同为5V,采用的时钟频率为16MHz,拥有256KB容量的 Flash 存储器(其中8KB用于 Bootloader),8KB大小的 SRAM,4KB容量的 EEPROM。

板载 54 个数字输入/输出引脚(其中 15 个可用作 PWM 输出),16 个模拟输入引脚,4 个 UART(硬件串行端口)。推荐的输入电压在7 ~ 12V范围以内,而极限输入电压则在6 ~ 20V之间,每个 I/O 引脚可以输出20mA直流电流,其中3.3V引脚可以输出50mA直流电流。

Arduino Due 作为首款基于 32 位 ARM 内核微控制器的 Arduino 开发板,其引脚分布与 Arduino Mega 2560 基本保持一致,但是微控制器采用了 Atmel SAM3X8E ARM Cortex-M3 CPU,其工作电压为3.3V,时钟晶振频率为84 MHz,拥有512KB的 Flash 存储器,以及96KB的 SRAM(分为64KB32KB两个Bank),并且板载 54 个数字输入/输出引脚(其中 12 个可用于 PWM 输出),12 个模拟输出,4 个硬件串行接口。

ESP 8266

深圳安信可科技推出的NodeMCU是一款以上海乐鑫科技 ESP8266 模组作为核心的开发板,早期主要支持 Lua 脚本化编程,后经开源社区推出的《ESP8266 Arduino Core》整合了对于 Arduino 环境的支持,作为一款低成本的物联网开发板风靡于全球电子爱好者。

NodeMCU 开发板支持频率范围在 2400 ~ 2483.5MHz 之间的 802.11b/g/n 无线协议 WIFI,板载 32Mbit 的 Flash 外置存储空间,并且拥有 30 个接口引脚:

安装板级支持包

将开源社区提供的 ESP8266 Arduino Core 板级支持包 URL 地址添加至 Arduino IDE,鼠标依次选择【文件 > 首选项 > 附加开发板管理器网址】,然后填写如下地址:

1
https://arduino.esp8266.com/stable/package_esp8266com_index.json

然后,选择菜单栏的【工具 > 开发板 > 开发板管理器...】打开开发板管理器,安装 ESP8266 板级支持包:

最后,不要忘记回到 Arduino IDE 的工具栏当中,重新选择【工具 > 开发板 > ESP 8266 Boards】里面的开发板型号:

选择完成以后,Arduino IDE 的【工具】菜单栏下面会展示 ESP8266 支持包提供的一些配置选项,这里还需要勾选当前开发板连接的串行端口号:

硬件串口 HardwareSerial

ESP8266 当中 Serial 串行总线对象的工作方式与普通 Arduino 基本相同,除了硬件上针对 TXRX128 字节 FIFO 之外,还拥有额外的 256 字节 TXRX 缓冲区发送接收都是由中断驱动,当 FIFO/缓冲区发生溢出之后,读写函数将会阻塞程序的执行。用于操作 ESP8266 串行通信接口的 SerialSerial1对象,两者都是 HardwareSerial 类的实例,它们仅适用于采用 ESP8266 的核心板:

  • Serial 对象默认使用硬件 UART0 接口,它被映射到 GPIO1/TXGPIO3/RX 引脚,执行 Serial.begin() 之后再调用 Serial.swap() 就可以将 Serial 对象映射至 GPIO15/TXGPIO13/RX,然后再次调用 Serial.swap(),就可以将 UART0 恢复为默认的 GPIO1GPIO3
  • Serial1 对象使用硬件 UART1 接口, 其中 TX 引脚为 GPIO2,而 RX 引脚连接到了 Flash 芯片,因而不能再用于接收数据,通过调用 Serial1.begin(baudrate),即可开始使用 Serial1。如果 Serial1 没有被使用,并且 Serial 对象没有调用过 swap(),那么在调用 Serial.begin() 之后再执行 Serial.set_tx(2),或者直接调用 Serial.begin(baud, config, mode, 2),就可以将 UART0TX 映射至 GPIO2

SerialSerial1 在 Arduino 标准 API 的基础之上,新增加了下面的工具方法:

函数名称 功能描述
setdebugoutput(true) 调用 Serial.begin() 时内置 WiFi 库的调试输出默认禁用,如果需要启用调试输出,就需要手动调用该函数,例如:
1
2
Serial.setDebugOutput(true);  // 开启 Serial 的调试输出
Serial1.setdebugoutput(true); // 将调试输出重定向至 Serial1
begin(baudrate, config) SerialSerial1 对象上调用该方法可以配置串行通信相关属性,例如数据位5678)、奇偶校验方式(奇校验 O、偶校验 E、无校验 N)、停止位12),例如:
1
2
Serial.begin(baudrate, SERIAL_8N1);
Serial.begin(baudrate, SERIAL_6E2);
baudRate() 调用 SerialSerial1 对象上的 baudRate() 可以获取当前的波特率设置,返回一个表示当前波特率的整型数据,例如:
1
2
3
Serial.begin(115200);                  // 设置波特率为 115200
int br = Serial.baudRate(); // 获取当前的波特率
Serial.printf("Serial is %d bps", br); // 串口 1 将会打印 "Serial is 115200 bps"
Serial.begin(detectedBaudrate) 检测输入至 Serial 串行通信接口的未知波特率,返回值为当前检测到的波特率,返回 0 表示没有检测到有效的波特率。
setRxBufferSize(size_t size) 该函数用于设置接收缓冲区的大小,默认值为 256

软件串口 SoftwareSerial

通过使用第三方提供的 EspSoftwareSerial 库,可以让 ESP8266 支持高达 115200 波特率的多个 SoftwareSerial 实例。下面的例子当中,首先将一块 ESP 开发板的 1617 引脚分别作为串行数据的接收与发送引脚,并且发送一个字符数据 5

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <SoftwareSerial.h>

SoftwareSerial mySerial(16, 17); // (接收引脚 RX, 发送引脚 TX)

void setup() {
Serial.begin(115200); // 初始化硬件串口,用于 Arduino 串口监视器
mySerial.begin(9600); // 初始化软件串口,用于两块 ESP 开发板之前通信
}

void loop() {
int bytesSent = mySerial.write(5); // 发送字节数据
Serial.println("Begins writing...");
}

然后,把另一块 ESP 开发板的 1617 引脚也分别用于串行数据的收发,然后接收上面那块 ESP 开发板发送过来的串行数据,并且将其显示到 Arduino IDE 的串口监视器当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <SoftwareSerial.h>

SoftwareSerial mySerial(16, 17); // (接收引脚 RX, 发送引脚 TX)

void setup() {
Serial.begin(115200); // 初始化硬件串口,用于 Arduino 串口监视器
mySerial.begin(9600); // 初始化软件串口,用于两块 ESP 开发板之前通信
}

void loop() {
if (mySerial.available() > 0) {
Serial.println(mySerial.read()); // 接收字节数据
}
}

I²C 总线配置

ESP8266 Arduino Core 板级支持包所实现的 Wire 库,支持 ESP8266主设备模式下高达 450KHz 的 I²C 总线时钟频率,使用时可以通过调用如下函数来配置 SDA(串行数据)和 SCL(串行时钟)引脚:

1
Wire.begin(int sda, int scl)

例如 Wire.begin(0, 2),在常见的 ESP-01 模组当中 SDA 默认为第 4 引脚,SCL 默认为第 5 引脚。

注意:需要特别注意 ESP32Wire.begin(int sda, int scl))与 ESP8266Wire.setPins(sda_pin, scl_pin))在设置 I²C 引脚方面的区别。

SPI 总线配置

ESP8266 Arduino Core 比较完整的实现了 Arduino 的 SPI 总线 API,支持设置时钟极性(CPHA,Clock Phase),但是并不支持时钟相位(CPOL,Clock Polarity)的设置,并且 SPI_MODE2SPI_MODE3 也暂时还无法工作。

通常情况下,默认的 SPI 引脚为:

1
2
3
4
MOSI = GPIO13
MISO = GPIO12
SCLK = GPIO14
SS = GPIO15

但是通过扩展模式,可以将普通引脚切换为 SPI0 的硬件引脚,例如在执行 SPI.begin() 之前调用 SPI.pins(6, 7, 8, 0),SPI 引脚的映射状态就会变为:

1
2
3
4
MOSI = SD1
MISO = SD0
SCLK = CLK
HWCS = GPIO0

注意:这种模式通过微控制器读取 Flash 当中的程序代码来控制 SPI 引脚,这种控制由微控制器上的硬件仲裁单元来完成(Flash 总是拥有更高的优先级),这种情况下片选操作总是由硬件来进行控制,开发人员不能通过 GPIO 来操作片选线 (因为永远不会知道仲裁单元何时会访问总线,所以必须让其自动进行片选操作)。

ESP 32

上海乐鑫科技推出的ESP32-WROOM-32 是一款通用型 Wi-Fi 802.11 b/g/nBluetooth v4.2 BR/EDR BLE 微控制器模组,其核心为 ESP32-D0WDQ6 芯片,拥有两个可以单独控制的 MCU 内核,时钟频率调节范围为 80 MHz ~ 240 MHz。集成了电容式触摸霍尔低噪声放大等传感器,以及 SD 卡以太网高速 SDIO/SPIUARTI2SI2C 等接口。

深圳果云科技推出的 Goouuu-ESP32 则是一款基于 ESP32-WROOM-32 模组的 39Pin 开发板,由于乐鑫官方在 Github 上提供了兼容 Arduino 的板级支持包 arduino-esp32,因此该开发板可以良好工作于 Ardunio IDE 环境下,下面示意图展示了该开发板 GPIO 引脚的功能映射情况:

安装板级支持包

按照前面 添加板级支持包 小节介绍的方法,将乐鑫官方提供的《《ESP32 Arduino Core》》板级支持包地址添加至 Ardunio IDE 的【附加开发板管理器网址】当中:

1
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

接下来就可以依次选择【工具 -> 开发板管理器】检索并且安装 ESP32 开发板兼容的 Ardunio 板级支持包了,如果觉得安装速度过慢,那么还可以选择下面的方式离线进行安装:

  1. arduino-esp32 从官方乐鑫官方的 GitHub 仓库地址 https://github.com/espressif/arduino-esp32.git 克隆下来,或者直接下载仓库的 .zip 格式包。然后,拷贝或者解压至被称为 Sketchbook 的 Ardunio 默认代码工作区的目录 \hardware\espressif\esp32 下面,例如 C:\Users\hank\Documents\Arduino\hardware\espressif\esp32
  2. 通过命令行进入 \hardware\espressif\esp32 目录然后执行 git submodule update --init --recursive 初始化 Git 子仓库;接下来进入 \hardware\espressif\esp32\tools 文件夹,鼠标双击执行 get.exe 安全依赖包;
  3. 完成上述步骤之后,安装 ESP32 Ardunio 兼容开发板对应的 USB 转串口驱动,然后打开 Ardunio IDE 并插入开发板,选择相应的开发板端口,就可以编译并且上传 Ardunio Sketch 程序到板子上了;

如果需要更新 arduino-esp32 板级支持包,则可以按照如下步骤进行操作:

  1. 首先,命令进入 \hardware\espressif\esp32 目录执行 git pull 更新代码;
  2. 然后,执行 \hardware\espressif\esp32\tools\get.exe 更新依赖包;

GPIO 矩阵与复用

一些微控制器架构会将外设功能绑定到指定引脚,并且不能通过固件重新进行定义。而 ESP32 架构可以通过 IO MUXGPIO Matrix 管理和配置某些外设到不同的物理引脚,例如可以将任意的 GPIO 路由成为 I²CSDASCL

使用 IO MUXGPIO Matrix 相关功能的时候,需要注意如下 3 条事项:

  1. 某些引脚只允许设置为 INPUT,这类引脚不能用于需要配置输入/输出信号的外设;
  2. 由于一些外围设备存在输出信号,所以必须将 GPIO 配置为 OUTPUT
  3. 某些高速外围设备,例如 ADCDACTouchJTAG 必须使用指定的 GPIO 引脚;

注意:该功能优点在于无需完全依赖物理引脚,避免在硬件设计阶段出现的一些引脚功能定义错误。

下面的表格展示了 ESP32 基本外设的 GPIO 功能映射情况:

外设类型 GPIO 功能
ADC 指定 GPIO
DAC 指定 GPIO
触摸传感器 指定 GPIO
JTAG 指定 GPIO
SD/SDIO/MMC 主控制器 指定 GPIO
Motor PWM 全部 GPIO
SDIO/SPI 从控制器 指定 GPIO
UART 全部 GPIO
I²C 全部 GPIO
I²S 全部 GPIO
LED PWM 全部 GPIO
RMT 全部 GPIO
GPIO 全部 GPIO
Parallel QSPI 指定 GPIO
EMAC 指定 GPIO
脉冲计数器 全部 GPIO
TWAI 全部 GPIO

I²C 总线配置

前面关于 Arduino Uno 的内容当中,提到过其在硬件上将 A4A4 引脚定义为 I²C 总线的 SDASCL,而在接下来的 ESP32 实例当中,我们不再需要手动将这些引脚设置到 Wire.begin() 函数的参数当中,因为这些配置(SDA/GPIO 21SCL/GPIO 22),已经提前预置到了 ES32 的 Arduino 板级支持包当中:

1
2
3
void setup() {
Wire.begin(); // 开启 I²C 总线
}

当然开发人员也可以根据需要,通过在调用 Wire.begin() 之前执行 Wire.setPins(int sda, int scl) 来改变这些引脚的默认设置:

1
2
3
4
5
6
7
int sda_pin = 16; // 将 GPIO 16 指定为 I²C 总线的 SDA
int scl_pin = 17; // 将 GPIO 17 指定为 I²C 总线的 SCL

void setup() {
Wire.setPins(sda_pin, scl_pin); // 首先,设置好 I²C 引脚
Wire.begin(); // 然后,再打开 I²C 总线
}

注意:类似的方法也适用于其它的外围设备。

外设运用实例

PCF8574T 驱动 1602 液晶

ESP32 通过使用 I²C 地址为 0x27PCF8574T 液晶屏驱动模块,可以方便的驱动拥有 162 行显示范围的 1602 液晶屏幕。

注意:通过前面 Wire 小节提供的 I²C 地址扫描程序,可以通过串口监视器,获得连接到 ESP 开发板默认 I²C 引脚的设备地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <LiquidCrystal_I2C.h>

/* 使用 PCF8574T 液晶屏驱动模块的 I²C 地址建立一个 16 列 2 行的液晶对象 */
LiquidCrystal_I2C lcd (0x27, 16, 2); //

void setup () {
lcd. init (); // 初始化 1602 液晶连接
lcd. backlight (); // 开启 1602 液晶的背光
lcd. print ( "==[Our Status]==");
}

void loop () {
lcd. setCursor (0, 1); // 将光标放置到第 1 行的第 0 列
/* 动态显示当前秒数 */
lcd. print ( millis () / 1000);
lcd. print ( " Seconds" );
delay (100);
}

MFRC 522 读写卡

MIFARE恩智浦(NXP)半导体公司推出的接触/非接触式智能卡技术的商标,其中 RC522 是应用 13.56MHz 频率的非接触式读写卡芯片,支持 14443A 协议。而 RFID-RC522 模组则是一款采用了 MIFARE RC522 芯片的读写卡模组,采用 3.3V 电压进行供电,支持 SPI 总线与主控制芯片进行连接通信。

RFID是一款支持 RFID-RC522 模组的 Arduino 第三方开源库,其对应的引脚功能映射如下面的表格所示:

SPI 引脚 MFRC522 Uno Mega ESP8266
RST/Reset RST 9 5 D3
SPI SS SDA 10 53 D8
SPI MOSI MOSI 11/ICSP-4 51 D7
SPI MISO MISO 12/ICSP-1 50 D6
SPI SCK SCK 13/ICSP-3 52 D5

注意:可供 RC522 读写操作的接触式集成电路卡(PICC,Proximity Integrated Circuit Card)是一种使用 ISO/IEC 14443A 接口的射频卡/标签,其与 RFID-RC522 读写卡器之间涉及的通信协议,定义在《ISO/IEC 14443-3:2011 Part 3 Type A》规范的第 6 小节 Type A – Initialization and anticollision 当中,除此之外,具体的功能描述还可以参考 RC522 数据手册

安装 ESP 板级支持包的新方法

首先,向 Arduino IDE 添加如下的开发板 URL 地址:

1
2
https://www.arduino.cn/package_esp32_index.json
https://www.arduino.cn/package_esp8266com_index.json

然后,前往 https://cloud.codess-nas.top:5213/s/2Ocn?path=%2F 直接下载板级支持包文件进行安装即可。