admin 管理员组

文章数量: 887021

目录

前言

1.注释

2.变量

3.条件语句

1.if语句

2.switch语句

4.循环语句

1.for循环

2.while循环

5.数组

6.函数

7.常用函数介绍

8.库控制函数

1.LEDC外设输出PWM

2.数模转换器(ADC)

3.IIC驱动LCD1602液晶屏幕

1.在屏幕上显示 Hello,world

2. 读取串口输入内容

4.SPI 驱动 OLED 液晶屏幕

1. Adafruit_SSD1306 控制 OLED 屏幕

2.U8G2 库控制 OLED

5.外部中断

6.定时器中断

1.硬件定时器

2.软件定时器

7.Wi-Fi连接

1.硬件电路设计

2.软件电路设计

8.发送网络请求

1.实验原理

2.HTTP 请求与 API

3.JSON 数据

4.软件程序设计


前言

本人不太擅长美化内容,代码块的装饰等都挺普通,望大家谅解,在这里先感谢大家的支持。

1.注释

当你编写代码时,注释(comments)是非常重要的一部分。注释是对代码的解释和说明,而且对于其他开发者或者自己日后需要修改代码的时候,都非常有帮助。注释可以提高代码的可读性和可维护性,并且可以帮助你自己更好地理解代码。

注释是由双斜线 // 或者斜线星号 /*...*/ 来表示的。单行注释以两个斜线开头,多行注释则以斜线星号开头,以星号斜线结尾。例如:

​
// 这是一个单行注释
​
/*
这是一个多行注释
它可以跨越多行
*/

注释可以用来解释代码的功能,算法或者实现细节。例如,以下是一些常见的注释用法:

  • 函数或者方法的用途

  • 参数的说明

  • 返回值的说明

  • 代码实现的说明

  • 代码的限制或者假设条件

  • 作者信息、创建时间、修改时间等等

注释应该尽可能的清晰、简洁和明了,同时避免使用无用的注释,以免给代码带来混淆和干扰。注释应该随着代码一起更新,以确保注释和代码的一致性。

2.变量

当我们编写程序时,变量是一个非常基本的概念。一个变量可以存储一个值,这个值可以是数字、字符串、布尔值、对象等。

在 Arduino 编程语言中,变量需要在使用前声明,声明语法为:

  • 数据类型 变量名;

数据类型指定变量可以存储的数据类型,常见的数据类型有:

  • int:整数类型,占用2个字节,可以表示范围为 -32768 到 32767 之间的整数。

  • float:浮点数类型,占用4个字节,可以表示小数。

  • char:字符类型,占用1个字节,可以表示一个字符。

  • bool:布尔类型,占用1个字节,只有两个值:true 或 false。

变量名是标识符,命名规则为字母、数字、下划线的组合,第一个字符不能是数字。

下面是一些变量的示例:

int a; // 声明一个名为 a 的整型变量
float b = 3.14; // 声明一个名为 b 的浮点型变量,并初始化为 3.14
char c = 'A'; // 声明一个名为 c 的字符型变量,并初始化为字符 'A'
bool d = true; // 声明一个名为 d 的布尔型变量,并初始化为 true

变量在程序中可以被赋值或者修改:

int a = 10; // 初始化 a 为 10
a = 20; // 修改 a 的值为 20

除了上述基本数据类型外,Arduino 还支持其他的数据类型,如字符串类型 String,数组类型等。在使用变量时,需要根据需求选择合适的数据类型,避免浪费内存。

3.条件语句

当我们需要根据某个条件来执行不同的代码时,就需要使用条件语句。在 Arduino 编程语言中,常见的条件语句有 if 语句和 switch 语句。

1.if语句

if 语句是最基本的条件语句,其语法如下

if (condition) {
  // if 条件成立时要执行的代码
}

其中,condition 是一个条件表达式,如果这个条件表达式的值为真,则执行花括号中的代码块。

如果需要在条件不成立时执行代码,则可以添加 else 语句:

if (condition) {
  // if 条件成立时要执行的代码
} else {
  // if 条件不成立时要执行的代码
}

当然,也可以在 else 语句后面添加一个 if 语句,从而实现多个条件判断。这个语法结构被称为 else if

if (condition1) {
  // if 条件 1 成立时要执行的代码
} else if (condition2) {
  // if 条件 2 成立时要执行的代码
} else {
  // 如果以上条件都不成立,则执行这里的代码
}

2.switch语句

switch 语句也是一种条件语句,通常用于比较一个变量与一系列常量值。其语法如下:

switch (variable) {
  case value1:
    // 如果 variable 的值等于 value1,则执行这里的代码
    break;
  case value2:
    // 如果 variable 的值等于 value2,则执行这里的代码
    break;
  default:
    // 如果 variable 的值不等于任何一个 case 的值,则执行这里的代码
    break;
}

switch 语句中,variable 是要进行比较的变量,而 case 是常量值。如果 variable 的值等于某个 case 的值,则执行该 case 对应的代码块,并且在代码块末尾添加 break 语句,以防止执行其他的 case。如果 variable 的值不等于任何一个 case 的值,则执行 default 中的代码块。需要注意的是,在 switch 语句中,每个 case 的值必须是常量,且不可重复。

总的来说,if 语句和 switch 语句都是用于控制程序执行流程的条件语句,开发者可以根据具体情况选择使用哪种语句。

4.循环语句

当我们需要重复执行一段代码时,就需要用到循环语句。在 Arduino 中,有两种主要的循环语句:for 循环和 while 循环。

1.for循环

for 循环是一个控制结构,它允许你重复执行一系列语句,具体次数由循环次数确定。for 循环的语法如下:

for (初始化表达式; 布尔表达式; 更新表达式) {
  // 代码块
}

for 循环由三部分组成:

  • 初始化表达式:在循环开始时执行一次,通常用于初始化计数器。

  • 布尔表达式:在每次迭代开始前计算,如果结果为 true,则执行循环体语句,否则退出循环。

  • 更新表达式:在每次迭代结束后执行,通常用于更新计数器。

下面是一个简单的 for 循环的例子,它输出数字 0 到 9:

for (int i = 0; i < 10; i++) {
  Serial.println(i);
}

在这个例子中,初始化表达式初始化了计数器 i 为 0,布尔表达式检查 i 是否小于 10,更新表达式将 i 增加 1。在每次迭代中,计数器 i 的值都会被输出。

2.while循环

while 循环是另一种重复执行语句块的方法。它会在条件为 true 时重复执行代码块。while 循环的语法如下:

while (布尔表达式) {
  // 代码块
}

while 循环只由一个条件表达式组成,当这个表达式为 true 时,执行循环体语句。在每次循环执行后,条件表达式都会被重新计算。如果条件表达式为 false,则跳过循环体语句,直接执行循环后面的代码。

下面是一个使用 while 循环输出数字 0 到 9 的例子:

int i = 0;
while (i < 10) {
  Serial.println(i);
  i++;
}

在这个例子中,初始化变量 i 的值为 0。while 循环的条件表达式检查变量 i 是否小于 10。只要条件为 true,循环就会一直执行,每次将变量 i 的值增加 1。在每次循环中,变量 i 的值都会被输出。

总体来说,for 循环适用于知道循环次数的情况,而 while 循环适用于不知道循环次数的情况。

5.数组

接下来我们来看看如何使用 Arduino 的数组。

数组是一种用于存储多个值的数据类型。数组的每个元素都有一个唯一的索引,可以使用这个索引来访问数组中的元素。

定义数组的语法如下:

type arrayName[arraySize];

其中,type 是数组中元素的数据类型,arrayName 是数组的名称,arraySize 是数组的大小,下面是一个例子:

int myArray[5];

这个代码定义了一个包含 5 个整数的数组,可以使用 myArray[0] 到 myArray[4] 访问这些元素。

可以在定义数组时初始化数组。例如:

int myArray[5] = {1, 2, 3, 4, 5};

这个代码定义了一个包含 5 个整数的数组,并将其初始化为 1、2、3、4、5。

也可以使用以下方式初始化数组:

int myArray[] = {1, 2, 3, 4, 5};

这个代码定义了一个包含 5 个整数的数组,并将其初始化为 1、2、3、4、5。

你可以使用下标访问数组元素。下标从 0 开始,例如:

int myArray[5] = {1, 2, 3, 4, 5};
int x = myArray[2];  // 将 x 的值设置为数组中下标为 2 的元素,即 3。

可以使用循环遍历数组中的所有元素。例如:

int myArray[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
  // 打印数组中的每个元素
  Serial.println(myArray[i]);
}

Arduino 中还支持多维数组。例如:

int myArray[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};

这个代码定义了一个 3 行 3 列的二维数组,并将其初始化为:

1 2 3
4 5 6
7 8 9

可以使用两个下标来访问数组中的元素。例如:

int myArray[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int x = myArray[1][2];  // 将 x 的值设置为数组中第 2 行第 3 列的元素,即 6。

6.函数

当程序中需要执行某个特定任务时,函数是非常有用的。函数可以包含一些代码块,这些代码块可以在程序的其他地方多次调用。在 Arduino 编程语言中,函数包含了一个函数头和一个函数体。函数头包含了函数名称和参数列表,函数体包含了一些要执行的代码。

以下是一个简单的函数示例:

int add(int a, int b) {    //函数头
  int sum = a + b;         //函数体
  return sum;
}

这个函数的名称是 add,它有两个参数,分别是 ab,函数体包含了将 ab 相加的操作,最后通过 return 返回结果。

在程序中调用函数时,可以像这样使用:

int x = 3;
int y = 5;
int z = add(x, y);    //调用函数

在这个示例中,我们将 xy 作为参数传递给 add 函数,该函数返回它们的和,最后将结果存储在 z 变量中。

函数的参数也可以是其他类型的数据,例如字符串、浮点数等等。在 Arduino 编程语言中,函数也可以没有参数,也可以没有返回值。以下是一个没有参数和返回值的函数示例:

void sayHello() {    //函数头
  Serial.println("Hello World!");    //函数体
}

这个函数的名称是 sayHello,它没有参数和返回值。函数体包含了一条输出语句,它将字符串 "Hello World!" 输出到串行监视器中。

函数的使用可以让代码更加清晰、易读和易于维护。通过将代码块封装到函数中,可以使代码更加模块化,也可以避免在多个地方重复编写相同的代码。在编写代码时,应该尽可能地使用函数,以便使代码更加可读、易于维护和可扩展。

7.常用函数介绍

下面列出了一些常用的 Arduino 函数:

当使用 Arduino 进行编程时,有许多内置函数可用。这些函数可以帮助我们更轻松地编写程序,处理输入和输出,控制逻辑流和实现其他功能。下面是一些常用的 Arduino 函数:

  1. pinMode(pin, mode): 用于配置数字引脚的输入或输出模式。pin 是数字引脚的编号,mode 是要设置的模式(输入或输出)。

  2. digitalWrite(pin, value): 用于在数字引脚上写入数字值(HIGH 或 LOW)。pin 是数字引脚的编号,value 是要写入的值。

  3. digitalRead(pin): 用于读取数字引脚上的数字值(HIGH 或 LOW)。pin 是数字引脚的编号。

  4. analogRead(pin): 用于读取模拟引脚上的模拟值(0-1023)。pin 是模拟引脚的编号。

  5. analogWrite(pin, value): 用于在支持 PWM 输出的数字引脚上输出模拟值(0-255)。pin 是数字引脚的编号,value 是要输出的值。

  6. delay(ms): 用于在程序中创建暂停(延迟)时间。ms 是要延迟的毫秒数。

  7. millis(): 返回自启动以来的毫秒数,可以用于时间跟踪和计时器。

  8. Serial.begin(baud): 用于初始化串口通信,其中 baud 是波特率。

  9. Serial.print(data): 用于将数据打印到串口监视器。data 可以是数字,字符串或其他数据类型。

  10. Serial.available(): 用于检查是否有数据可以从串口读取。

这些函数只是 Arduino 可用的众多函数中的一部分。熟悉这些常用函数可以帮助我们更轻松地编写程序,并为实现特定功能提供了有用的工具。

下面是一个简单的实例代码,演示了如何控制一个 LED 灯的亮灭:

// 设置 LED 引脚
int led_pin = 2;
​
void setup() {
  // 设定引脚为输出模式
  pinMode(led_pin, OUTPUT);
}
​
void loop() {
  // 点亮 LED
  digitalWrite(led_pin, HIGH);
  // 等待一段时间
  delay(1000);
  // 关闭 LED
  digitalWrite(led_pin, LOW);
  // 等待一段时间
  delay(1000);
}

这段代码中,我们首先定义了一个整型变量 led_pin,表示连接 LED 灯的引脚。在 setup() 函数中,我们将该引脚设定为输出模式,然后在 loop() 函数中交替点亮和关闭 LED 灯,并在两次操作之间等待 1 秒钟的时间。

8.库控制函数

1.LEDC外设输出PWM

以呼吸灯为例,使用 ESP32 的 LEDC 外设,在 ESP32 上有一个 LEDC 外设模块专用于输出 PWM 波形。

LED PWM 控制器可以生成 16 路通道(0 ~ 15),波形的周期和占空比可配置。分为高低速两组,高速通道(0 ~ 7)由 80MHz 时钟驱动,低速通道(8 ~ 15)由 1MHz 时钟驱动。另外,每路 LED PWM 支持自动步进式地增加或减少占空比,可以用于 LED RGB 彩色梯度发生器。

作为刚入门的学习者,上面这段概念不理解也不影响我们后续的学习,我们需要了解的是 LEDC 的控制函数以及 PWM 信号的产生流程。

以下为 LEDC 的所有控制函数:

// 设置 LEDC 通道对应的频率和计数位数(占空比分辨率),返回最终频率
// 分辨率的意思就是把一个周期分成 2 的 resolution_bits 份。
uint32_t    ledcSetup(uint8_t channel, uint32_t freq, uint8_t resolution_bits);
​
// 指定通道输出一定占空比波形
void        ledcWrite(uint8_t channel, uint32_t duty);
​
// 类似于 arduino 的 tone ,当外接无源蜂鸣器的时候可以发出某个声音(根据频率不同而不同)
uint32_t    ledcWriteTone(uint8_t channel, uint32_t freq);
​
//  该方法是上面方法的进一步封装,可以直接输出指定调式和音阶声音的信号
uint32_t    ledcWriteNote(uint8_t channel, note_t note, uint8_t octave);
​
// 返回指定通道占空比的值
uint32_t    ledcRead(uint8_t channel);
​
// 返回指定通道当前频率(如果当前占空比为0 则该方法返回0)
uint32_t    ledcReadFreq(uint8_t channel);
​
// 将 LEDC 通道绑定到指定 IO 口上以实现输出
void        ledcAttachPin(uint8_t pin, uint8_t channel);
​
// 解除 IO 口的 LEDC 功能
void        ledcDetachPin(uint8_t pin);

使用 LEDC 外设的时候需要遵循以下步骤:

  1. 使用 ledcSetup() 函数建立 LEDC 通道;

  2. 通过 ledcAttachPin() 将 GPIO 口与 LEDC 通道关联;

  3. 通过 ledcWrite()、ledcWriteTone()、ledcWriteNote()设置频率、设置蜂鸣器音调等等

  4. 通过 ledcDetachPin() 解除 GPIO 口与 LEDC 通道的关联

所有我们可以通过以下代码,实现呼吸灯效果:

#define FREQ        2000    // 频率
#define CHANNEL     0       // 通道
#define RESOLUTION  8       // 分辨率
#define LED         12      // LED 引脚
​
​
void setup()
{
  ledcSetup(CHANNEL, FREQ, RESOLUTION); // 设置通道
  ledcAttachPin(LED, CHANNEL);          // 将通道与对应的引脚连接
}
​
void loop()
{
  // 逐渐变亮
  for (int i=0;i<pow(2, RESOLUTION); i++)
  {
    ledcWrite(CHANNEL, i); // 输出PWM
    delay(5);
  }
​
  // 逐渐变暗
  for (int i=pow(2, RESOLUTION)-1;i>=0;i--)
  {
    ledcWrite(CHANNEL, i); // 输出PWM
    delay(5);
  }
}

2.数模转换器(ADC)

使用 ADC 模拟通道输入,以下为ADC 的所有控制函数:

  • analogReadResolution(resolution):设置样本位和分辨率。它可以是一个介于 9(0 - 511)和 12 位(0 - 4095)之间的值。默认是 12 位分辨率。

  • analogSetWidth(width):设置样本位和分辨率。它可以是一个介于 9(0 - 511)和 12 位(0 - 4095)之间的值。默认是 12 位分辨率。

  • analogSetCycles(cycles):设置每个样本的循环次数。默认是 8。取值范围:1 ~ 255。

  • analogSetSamples(samples):设置范围内的样本数量。默认为 1 个样本。它有增加灵敏度的作用。

  • analogSetClockDiv(attenuation):设置ADC时钟的分压器。默认值为1。取值范围:1 ~ 255。

  • adcAttachPin(pin):附加一个引脚到 ADC(也清除任何其他模拟模式可能是 on)。返回TRUE或FALSE结果。

  • analogSetAttenuation(attenuation)

    :设置所有 ADC 引脚的输入衰减。默认是 ADC_11db。其他取值:

    1. ADC_0db: 集没有衰减。ADC 可以测量大约 800mv (1V 输入 = ADC 读数 1088)。

    2. ADC_2_5db: ADC 的输入电压将被衰减,扩展测量范围至约。1100 mV。(1V 输入 = ADC 读数 3722)。

    3. ADC_6db: ADC 的输入电压将被衰减,扩展测量范围至约。1350 mV。(1V 输入= ADC 读数 3033)。

    4. ADC_11db: ADC 的输入电压将被衰减,扩展测量范围至约。2600 mV。(1V 输入= ADC 读数 1575)。

  • analogSetPinAttenuation(pin, attenuation):设置指定引脚的输入衰减。默认是 ADC_11db。衰减值与前一个函数相同。

因此,以呼吸灯为例,代码可以这么写:

#define POT       26
#define LED       13
#define CHANNEL   0
​
​
// 初始化模拟输入值
int pot_value;
​
 
void setup() {
  Serial.begin(9600);
  // 设置 ADC 分辨率
  analogReadResolution(12);
  // 配置衰减器
  analogSetAttenuation(ADC_11db);
  // 建立 LEDC 通道,配置 LEDC 分辨率
  ledcSetup(CHANNEL, 1000, 12);
  // 关联 GPIO 口与 LEDC 通道
  ledcAttachPin(LED, CHANNEL);
}
​
​
void loop() {
  // 获取模拟输入值
  pot_value = analogRead(POT);
  // 输出 PWM
  ledcWrite(CHANNEL, pot_value);
  delay(50);
}

3.IIC驱动LCD1602液晶屏幕

在 Arduino 中使用 I2C 控制 LCD1602 需要下载第三方代码 LiquidCrystal_I2C,其中包括 LiquidCrystal_I2C.hLiquidCrystal_I2C.cpp 两个文件,我们只需要把这两个文件放在项目的文件夹中即可。

Arduino 项目的创建非常简单,就是打开 Arduino IDE 后保存,它会在你选择的位置新建一个文件夹,这就是项目的文件夹。

.h.cpp 文件到底是什么?在 Arduino 开发中,.h.cpp 文件同样是用于代码组织和模块化的文件类型,但在 Arduino 环境中有些特殊的用法和约定。

  1. .h 文件(头文件):在 Arduino 中,头文件通常包含库、类、函数和变量的声明。头文件的目的是为了让多个源代码文件可以共享相同的声明,以便在程序中使用这些声明而无需重复编写。头文件通常使用预处理器指令 #include 引入到源文件中。在 Arduino 库中,.h 文件中通常包含类的声明、常量定义、函数原型等。

  2. .cpp 文件(源文件):在 Arduino 中,.cpp 文件是用于存放函数和类的实现代码的文件。.cpp 文件中包含了类成员函数的具体实现和其他函数的定义。通常,Arduino 项目中的 .cpp 文件中会包含 .h 文件,以便使用其中的声明。

需要注意的是,Arduino 开发中的 .h.cpp 文件的约定与传统的 C++ 开发并不完全相同。Arduino IDE 会在编译过程中将 .ino 文件转换为 .cpp 文件,并将其中的代码放置在全局范围。因此,在 Arduino 项目中,.ino 文件也可以包含全局变量和函数,而不仅限于 .cpp 文件。

总结起来,.h 文件是用于声明库、类、函数和变量的头文件,而 .cpp 文件是用于实现函数和类的源文件。这样的组织方式有助于提高代码的可读性、可维护性和重用性。

接着,我们就可以了解 LiquidCrystal_I2C 库的使用:

  1. LiquidCrystal_I2C(uint8_t addr, uint8_t cols, uint8_t rows):构造函数,用于构造 LCD I2C 对象,参数:addr 是地址,默认的是 0x27,cols 是 LCD 显示的列数,rows 是 LCD 显示的行数;

  2. void init():初始化显示屏;

  3. void clear():清除 LCD 屏幕上内容,并将光标置于左上角;

  4. void home():将光标在定位在屏幕左上角;

  5. void noBacklight()void backlight():是否开启背光;

  6. print():显示内容;

  7. void leftToRight()void rightToLeft():控制文字显示的方向,默认是从左向右;

  8. void noDisplay()void display():关闭显示或恢复显示(内容不会丢失);

  9. void setCursor(uint8_t col, uint8_t row):设置光标的位置,列,行,基于 0;

  10. void noCursor()void cursor:显示与不显示光标,默认不显示;

  11. void noBlink()void blink():光标是否闪烁,默认不闪烁。

现在,我们就可以写代码了。

1.在屏幕上显示 Hello,world

了解了第三方库之后,我们先写一个最简单的程序,比如,在屏幕上显示 Hello, world,代码如下:

#include "LiquidCrystal_I2C.h"
​
​
// 设置 LCD1602 的地址,列数,行数
LiquidCrystal_I2C lcd(0x27, 16, 2);
​
void setup()
{
  // 初始化 LCD 对象
  lcd.init();
 
  // 打印内容
  lcd.backlight();
  lcd.print("Hello, world!");
}
​
void loop()
{
}
2. 读取串口输入内容

在这个程序中,我们需要用到串口的另外两个方法 Serial.available()Serial.read():

  1. Serial.available():返回串口缓冲区中当前剩余的字符个数。一般用这个函数来判断串口的缓冲区有无数据,当 Serial.available()>0 时,说明串口接收到了数据,可以读取;

  2. Serial.read() 指从串口的缓冲区取出并读取一个 Byte 的数据,比如有设备通过串口向 Arduino 发送数据了,我们就可以用 Serial.read() 来读取发送的数据。

#include "LiquidCrystal_I2C.h"
​
​
// 设置 LCD1602 的地址,列数,行数
LiquidCrystal_I2C lcd(0x27,16,2);  
​
void setup()
{
  // 初始化 LCD 对象
  lcd.init(); 
  // 开启背光
  lcd.backlight();
  // 开启串口通信
  Serial.begin(9600);
}
​
void loop()
{
  // 检测是否有串口输入
  if (Serial.available()) {
    // 延时以等待所有数据传输完成
    delay(100);
    // 清屏
    lcd.clear();
    // 反复读取串口的数据并在 LCD1602 屏幕上显示,直到数据读完
    while (Serial.available() > 0) {
      lcd.write(Serial.read());
    }
  }
}

4.SPI 驱动 OLED 液晶屏幕

1. Adafruit_SSD1306 控制 OLED 屏幕

想要使用 Adafruit_SSD1306,还需要安装 Adafruit_GFX 第三方库。Arduino 的 Adafruit_GFX 库为我们所有的 LCD 和 OLED 显示器提供了通用语法和图形功能集,也就是说这是一个通用图形库,并不针对特定的显示器型号。

  • Adafruit_GFX 定义了一系列的绘画方法(线,矩形,圆等等),属于基础类,并且最重要的一点,drawPixel 方法由子类来实现;

  • Adafruit_SSD1306 定义了一系列跟 SSD1306 有关的方法,并且重写了 drawPixel 方法,属于扩展类。

首先,我们就需要先下载这两个第三方库,PlatformIO 已经为我们提供了方便的下载途径,我们可以直接在 PlatformIO 的 PIO HOME 页面中选择 Libraries 中分别搜索 Adafruit GFX LibraryAdafruit_SSD1306,然后添加到项目中即可。

接下来的方法(函数)无论是 I2C 还是 SPI 总线构建的,用法都是一致的:

  • clearDisplay:清除显示,该方法仅清除 Arduino 缓存,不会立即显示在屏幕上,可以通过调用 display 来立即清除;

  • display:显示内容,这个方法才是真正把绘制内容画在 OLED 屏幕上(非常重要);

  • drawCircle:绘制空心圆;

  • fillCircle:绘制实心圆;

  • drawTriangle:绘制空心三角形;

  • fillTriangle:绘制实心三角形;

  • drawRoundRect:绘制空心圆角方形;

  • fillRoundRect:绘制实心圆角方形;

  • drawBitmap:绘制 Bitmap 图形;

  • drawXBitmap:绘制 XBitmap 图形;

  • drawChar:绘制单个字符;

  • getTextBounds:计算字符串在当前字体大小下的像素大小,返回左上角坐标以及宽度高度像素值;

  • setTextSize:设置字体大小;

  • setFont:设置字体;

  • setCursor:设置光标位置;

  • setTextColor:设置字体颜色;

  • setTextWrap:设置是否自动换行;

  • drawPixel:绘制像素点;

  • drawFastHLine:绘制水平线;

  • drawFastVLine:绘制垂直线;

  • startscrollright:滚动到右边;

  • startscrollleft:滚动到左边;

  • startscrolldiagright:沿着对角线滚动到右边;

  • startscrolldiagleft:沿着对角线滚动到左边;

  • stopscroll:停止滚动:

使用 Adafruit_SSD1306 库分为三个步骤:

  1. 初始化 OLED,调用构造函数,调用 begin 方法;

  2. 初始化成功后,调用绘制类函数,当然可以设置颜色、字体等

  3. 绘制完毕,调用显示类函数 display。

了解完基本原理之后,我们就可以写一个简单的程序了,比如我们可以在屏幕上显示一些图形和字符,代码如下:

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED 显示屏宽度
#define SCREEN_HEIGHT 64 // OLED 显示屏高度
​
// 软件SPI总线
#define OLED_MOSI 13
#define OLED_CLK 18
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
                      OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
​
void setup()
{
  oled.begin();
  oled.clearDisplay();                          // 清除显示
  oled.drawFastHLine(32, 5, 48, SSD1306_WHITE); // 绘制水平线
  oled.drawLine(32, 5, 48, 30, SSD1306_WHITE);  // 绘制线
  oled.drawRect(5, 5, 10, 25, SSD1306_WHITE);   // 绘制矩形
  oled.fillRect(75, 5, 10, 30, SSD1306_WHITE);  // 绘制实心矩形
  oled.setCursor(5, 50);                        // 设置光标位置
  oled.setTextSize(2);                          // 设置字体大小
  oled.setTextColor(WHITE);                     // 设置文本颜色
  oled.println("Hello, world!");                // 显示文字
  oled.display();                               // 显示内容
}
​
void loop()
{
}

在 OLED 上显示进度条

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED 显示屏宽度
#define SCREEN_HEIGHT 64 // OLED 显示屏高度
​
// 软件SPI总线
#define OLED_MOSI 13
#define OLED_CLK 18
#define OLED_DC 2
#define OLED_CS 4
#define OLED_RESET 15
Adafruit_SSD1306 oled(SCREEN_WIDTH, SCREEN_HEIGHT,
                      OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, OLED_CS);
​
// 初始化进度条变量
int progress = 0;
​
void setup()
{
  oled.begin();
  oled.setTextSize(2);              // 设置字体大小
  oled.setTextColor(SSD1306_WHITE); // 设置文本颜色
  oled.display();                   // 显示内容
}
​
void loop()
{
  // 清空屏幕
  oled.clearDisplay();
​
  // 设置光标位置
  oled.setCursor(25, 40);
  // 显示文字
  oled.println("Process");
​
  // 显示进度条边框
  oled.drawRoundRect(0, 10, 128, 20, 5, SSD1306_WHITE);
  // 显示进度
  oled.fillRoundRect(5, 15, progress, 10, 2, SSD1306_WHITE);
​
  // 进度递增
  if (progress < 118)
  {
    progress++;
  }
  else
  {
    progress = 0;
  }
​
  // 刷新屏幕
  oled.display();
​
  delay(50); // 延迟一段时间后更新显示
}
2.U8G2 库控制 OLED

学会使用 Adafruit_SSD1306 库之后,我们再学习另一个并且是 Arduino 平台上使用最广泛的 OLED 库 - U8G2 库open in new window。U8g2 是嵌入式设备的单色图形库,一句话简单明了。主要应用于嵌入式设备,包括我们常见的单片机。

为什么要运用 U8g2 库?也就是说 U8g2 库能带给我们什么样的开发便利,主要考虑几个方面:

  • 平台支持性好,兼容多款开发板如 ESP32、ESP8266、Arduino Uno 等;

  • 显示控制器支持性好,基本上市面上的 OLED 都完美支持;

  • API 众多,特别支持了中文,支持了不同字体,这是一个对于开发者来说不小的福利。

因为 U8G2 库兼容很多版本的驱动以及不同尺寸的 OLED,所以 U8G2 构造方法有很多,但是我们需要根据我们自己的 OLED 的型号,选择适合我们的构造方法。打开 U8g2lib.h 文件,找到构造器的位置。

我们可以看到这些构造方法的名字有一定的规律:U8G2_驱动芯片_屏幕尺寸_缓存大小_总线,而我们的 OLED 尺寸是 128x64,SPI 总线,SSD1306 驱动,因此,我们可以搜索 U8G2_SSD1306_128X64

HW 表示硬件(hardware),SW 表示软件(software),4W 表示 4 线,因此,我们就找到了最适合我们的构造器

U8G2_SSD1306_128X64_NONAME_1_4W_SW_SPI
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI

这里的 1、2、F 表示不同的缓存大小:

  • 1;只有一页的缓冲区,需要使用 firstPage/nextPage 方法来循环更新屏幕,使用 128 字节的内存;

  • 2:保持两页的缓冲区,使用 256 字节的内存;

  • F:保存有完整的显示的缓存,可以使用所有的函数,但是 ram 消耗大,一般用在 ram 空间比较大的开发板;

所有的软件模拟总线构造函数的第一个参数都是 rotation,这个参数表示显示内容是否旋转,U8G2 提供了以下几个选项:

  • U8G2_R0:不旋转;

  • U8G2_R1:顺时针转 90°;

  • U8G2_R2:顺时针转 180°;

  • U8G2_R3:顺时针转 270°;

  • U8G2_MIRROR:镜像翻转;

构造完对象之后,我们就可以学习 U8G2 的方法了,方法可以分为四大类(这里我们只列举了部分,详细内容可以查阅 u8g2 库):

  1. 基本函数

    • begin():初始化方法;

    • initDisplay():初始化显示控制器,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,会在里面针对具体的 OLED 进行配置;;

    • clearDisplay():清除屏幕内容,这个方法不需要我们单独调用,会在 begin 函数主动调用一次,我们主要理解即可,并且不要在 firstPage 和 nextPage 函数之间调用该方法;

    • clear():清除操作;

    • clearBuffer():清除缓冲区;

    • enableUTF8Print():开启 Arduino 平台下支持输出 UTF8 字符集,我们的中文字符就是UTF8;

    • home():重置显示光标的位置,回到原点(0,0);

  2. 绘制相关函数

    • drawPixel():绘制像素点;

    • drawHLine():绘制水平线;

    • drawLine():两点之间绘制线

    • drawBox():画实心方形;

    • drawFrame():画空心方形

    • drawCircle():画空心圆;

    • drawDisc():画实心圆;

    • drawStr():绘制字符串,需要先设置字体,调用 setFont 方法;

    • drawXBM()/drawXBMP():绘制图像;

    • firstPage()/nextPage():绘制命令,firstPage 方法会把当前页码位置变成 0,修改内容处于 firstPage 和 nextPage 之间,每次都是重新渲染所有内容;

    • print():绘制内容;

  3. 显示配置相关函数

    • getDisplayHeight():获取显示器的高度;

    • getDisplayWidth():获取显示器的宽度;

    • setCursor():设置绘制光标位置;

    • setDisplayRotation():设置显示器的旋转角度;

    • setFont():设置字体集(字体集用于字符串绘制方法或者glyph绘制方法);

  4. 缓存相关函数

    • getBufferPtr():获取缓存空间的地址;

    • getBufferTileHeight():获取缓冲区的Tile高度,一个tile等于8个像素点;

    • getBufferTileWidth():获取缓冲区的Tile宽度;

    • getBufferCurrTileRow():获取缓冲区的当前Tile row;

    • clearBuffer():清除内部缓存区;

    • sendBuffer():发送缓冲区的内容到显示器。

U8g2 支持以下两种绘制模式:

  1. Full screen buffer mode,全屏缓存模式;

  2. Page mode,分页模式;

全屏缓存模式使用步骤:

  1. 构造对象,根据 OLED 的型号选择对应的构造器,构造器必须带 F,因此,需要使用 U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI

  2. 初始化对象,使用 begin() 方法,清除缓冲区内容,使用 u8g2.clearBuffer();

  3. 绘制内容,使用绘制函数或者设置字体等;

  4. 发送缓冲区的内容到显示器 u8g2.sendBuffer()。

了解完构造方法与使用方法之后,我们就可以来在程序中使用 U8G2 库了,代码如下:

#include <Arduino.h>
#include <U8g2lib.h>
​
// 构造对象
U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13, 
                                             /* cs=*/4, /* dc=*/2, /* reset=*/15);
​
void setup(void)
{
  // 初始化 oled 对象
  u8g2.begin();
  // 开启中文字符集支持
  u8g2.enableUTF8Print();
}
​
void loop(void)
{
  // 设置字体
  u8g2.setFont(u8g2_font_unifont_t_chinese2);
  // 设置字体方向
  u8g2.setFontDirection(0);
  // 
  u8g2.clearBuffer();
  u8g2.setCursor(0, 15);
  u8g2.print("Hello GeeksMan!");
  u8g2.setCursor(0, 40);
  u8g2.print("你好, ESP32!");
  u8g2.sendBuffer();
​
  delay(1000);
}

U8G2 库的分页模式实现进度条效果

分页模式的使用步骤:

  1. 构造对象,根据 OLED 的型号选择对应的构造器;

  2. 初始化对象,使用 begin() 方法,调用 firstPage() 进入第一页

  3. 开始一个 do while 循环,循环条件是 nextPage(),作用是进入下一页,如果还有下一页则返回 true;

  4. 在循环内部 操作一些绘制方法。

#include <Arduino.h>
#include <U8g2lib.h>
​
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
                                             /* cs=*/4, /* dc=*/2, /* reset=*/15);
​
int progress = 0;
​
void setup()
{
  // 初始化 OLED 对象
  u8g2.begin();
}
​
void loop()
{
  // 进入第一页
  u8g2.firstPage();
  do
  {
    // 显示进度条边框
    u8g2.drawFrame(0, 10, 128, 20);
    // 显示进度
    u8g2.drawBox(5, 15, progress, 10);
​
  } while (u8g2.nextPage());  // 进入下一页,如果还有下一页则返回true
​
  // 进度递增
  if (progress < 118)
  {
    progress++;
  }
  else
  {
    progress = 0;
  }
}

按键控制菜单

按键控制菜单的原理其实很简单 -,当我检测到按键按下的时候,就切换屏幕状态,因为只有部分区域发生了改变,让你产生立箭头移动的错觉,代码如下:

#include <Arduino.h>
#include <U8g2lib.h>
​
// PlatformIO 中 自己编写的函数如果处于末尾,需要在文件顶部显式声明
void display_menu(unsigned int index);
​
U8G2_SSD1306_128X64_NONAME_2_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/18, /* data=*/13,
                                            /* cs=*/4, /* dc=*/2, /* reset=*/15);
​
#define MENU_SIZE 4
char *menu[MENU_SIZE] = {"Item 1", "Item 2", "Item 3", "Item 4"};
​
#define BUTTON_UP 12
#define BUTTON_DOWN 14
​
// 定义当前选项
unsigned int  order = 0;
​
void setup()
{
  // 初始化 OLED 对象
  u8g2.begin();
  u8g2.setFont(u8g2_font_6x12_tr);
  
  // 配置输入按键
  pinMode(BUTTON_UP, INPUT_PULLUP);
  pinMode(BUTTON_DOWN, INPUT_PULLUP);
}
​
void loop()
{
  // 判断按键是否按下,并记录当前箭头位置
  if(!digitalRead(BUTTON_UP)) 
  {
    order = (order - 1) % 4;
  }else if (!digitalRead(BUTTON_DOWN))
  {
    order = (order + 1) % 4;
  }
  
  // 显示菜单
  display_menu(order);
​
  // 延时
  delay(100);
}
​
void display_menu(unsigned int index)
{
  // 进入第一页
  u8g2.firstPage();
  do
  {
    // 绘制页面内容
    u8g2.drawStr(0, 12, "Menu");
    u8g2.drawHLine(0, 14, 128);
    for (int i = 0; i < MENU_SIZE; i++)
    {
      if (i == index)
      {
        u8g2.drawStr(5, (i + 2) * 12 + 2, ">");
        u8g2.drawStr(20, (i + 2) * 12 + 2, menu[i]);
      }
      else
      {
        u8g2.drawStr(5, (i + 2) * 12 + 2, menu[i]);
      }
    }
  } while (u8g2.nextPage()); // 进入下一页,如果还有下一页则返回 True.
}

5.外部中断

Arduino 中的外部中断配置函数 attachInterrupt(digitalPinToInterrupt(pin), ISR, mode) 包括 3 个参数:

  1. pin:GPIO 端口号;

  2. ISR:中断服务程序,没有参数与返回值的函数;

  3. mode

    :中断触发的方式,支持以下触发方式:

    • LOW 低电平触发

    • HIGH 高电平触发

    • RISING 上升沿触发

    • FALLING 下降沿触发

    • CHANGE 电平变化触发

在 Arduino 中使用中断需要注意一下几点:

  1. 尽量保证中断程序内容少

  2. 避免在中断处理函数中使用阻塞函数(如 delay()),使用非阻塞的延迟方法来处理需要延迟的操作(micros() 函数),以保证中断的正常执行和系统的稳定性。这是因为 delay() 函数会阻塞整个系统,包括中断的正常执行。当中断触发时,处理函数应该尽快执行完毕,以确保及时响应并避免中断积压;

  3. 与主程序共享的变量要加上 volatile 关键字;

  4. 在 Arduino 中使用中断时,应尽量避免在中断处理函数中使用 Serial 对象的打印函数。当在中断处理函数中使用 Serial 打印函数时,会导致以下问题:

    • 时间延迟:Serial 打印函数通常是比较耗时的操作,它会阻塞中断的执行时间,导致中断响应的延迟。这可能会导致在中断期间丢失其他重要的中断事件或导致系统不稳定。

    • 缓冲区溢出:Serial 对象在内部使用一个缓冲区来存储要发送的数据。如果在中断处理函数中频繁调用 Serial 打印函数,可能会导致缓冲区溢出,造成数据丢失或不可预测的行为。

为了避免这些问题,建议在中断处理函数中尽量避免使用 Serial 打印函数。如果需要在中断处理函数中输出调试信息,可以使用其他方式,如设置标志位,在主循环中检查标志位并进行打印,代码如下:

#define BUTTON  14
​
​
// 定义可以在外部中断函数中使用的变量
volatile bool flag = false;
​
​
// 定义外部中断函数
void handle_interrupt() {
  flag = true;
  number += 10000;
}
​
void setup() {
  Serial.begin(9600);
  pinMode(BUTTON, INPUT_PULLDOWN);
  // 配置中断引脚
  attachInterrupt(digitalPinToInterrupt(BUTTON), handle_interrupt, FALLING);
}
​
void loop() {
  if (flag) {
    Serial.println("外部中断触发");
    flag = false;
  }
​
}

6.定时器中断

1.硬件定时器

在 ESP32 Arduino 开发环境中,可以使用以下几个库函数来配置和操作硬件定时器(Timer):

  1. void timerBegin(timer_num_t timer_num, uint32_t divider, bool count_up):初始化硬件定时器,参数说明:

  • timer_num:定时器编号,可选值为 0-3 等。

  • divider:定时器的分频系数,用于设置定时器的时钟频率。较大的分频系数将降低定时器的时钟频率。可以根据需要选择合适的值,一般设置为 80 即可;

  • count_up:指定定时器是否为向上计数模式。设置为 true 表示向上计数,设置为 false 表示向下计数。

  1. timerAttachInterrupt(hw_timer_t *timer, void (*isr)(void *), void *arg, int intr_type):用于将中断处理函数与特定的定时器关联起来,参数含义如下:

  • timer;定时器指针;

  • isr: 中断处理函数。

  • arg: 传递给中断处理函数的参数。

  • intr_type: 中断类型,可选值为 ture(边沿触发)或 false(电平触发)。

  1. timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload):用于设置定时器的计数值,即定时器触发的时间间隔,参数含义如下:

  • timer:定时器指针;

  • alarm_value: 定时器的计数值,即触发时间间隔;

  • autoreload: 是否自动重载计数值,可选值为 true(自动重载)或 false(单次触发)。

  1. timerAlarmEnable(hw_timer_t *timer):用于启动定时器,使其开始计数;

  2. timerAlarmDisable(hw_timer_t *timer):用于禁用定时器,停止计数;

  3. timerGetAutoReload(hw_timer_t *timer):获取定时器是否自动重新加载;

  4. timerAlarmRead(hw_timer_t *timer):获取定时器计数器报警值;

  5. timerStart(hw_timer_t *timer):计数器开始计数;

  6. timerStop(hw_timer_t *timer):计数器停止计数;

  7. timerRestart(hw_timer_t *timer):计数器重新开始计数,从 0 开始;

  8. timerStarted(hw_timer_t *timer):计数器是否开始计数。

以上是一些常用的硬件定时器相关的库函数,你可以根据自己的需求和定时器的特性,调用适当的函数来配置和操作硬件定时器。请参考 ESP32 的官方文档和相关库的文档,以获取更详细的信息。

使用硬件定时器的基本步骤如下:

  1. 初始化定时器:使用 timerBegin() 函数初始化所需的硬件定时器;

  2. 注册中断处理函数:使用 timerAttachInterrupt() 函数将中断处理函数与定时器关联起来;

  3. 设置定时器模式:使用 timerAlarmWrite(),设置触发一次,还是周期性触发;

  4. 启动定时器:使用 timerAlarmEnable() 函数启动定时器,使其开始计数。

因此,我们的代码可以这么写:

#define LED   2
#define LED_ONCE  4
​
hw_timer_t *timer = NULL;
hw_timer_t *timer_once=NULL;
​
​
// 定时器中断处理函数
void timer_interrupt(){
  digitalWrite(LED, !digitalRead(LED));
}
​
void timer_once_interrupt() {
  digitalWrite(LED_ONCE, !digitalRead(LED_ONCE));
}
 
void setup() {
  pinMode(LED, OUTPUT);
  pinMode(LED_ONCE, OUTPUT);
​
  // 初始化定时器
  timer = timerBegin(0,80,true);
  timer_once = timerBegin(1, 80, true);
​
  // 配置定时器
  timerAttachInterrupt(timer,timer_interrupt,true);
  timerAttachInterrupt(timer_once, timer_once_interrupt, true);
​
  // 定时模式,单位us,只触发一次
  timerAlarmWrite(timer,1000000,true); 
  timerAlarmWrite(timer_once, 3000000, false);
  // 启动定时器
  timerAlarmEnable(timer); 
  timerAlarmEnable(timer_once);
 
}
 
void loop() {
}
2.软件定时器

使用软件计时器的时候,我们需要用到 ESP32 内置的库 TickerTicker 是 ESP32 Arduino 内置的一个定时器库,这个库用于规定时间后调用函数。

接着我们来看看 Ticker 库的一些方法

  • detach():停止 Ticker;

  • active():Ticker 是否激活状态,True 表示启用;

  • once(n, callback,arg):n 秒后只执行一次 callback 函数,arg 表示回调函数的参数(不写表示没有);

  • once_ms(n, callback,arg):n 秒后只执行一次 callback 函数,arg 表示回调函数的参数(不写表示没有);

  • attach(n, callback, arg):每隔 n 秒周期性执行;

  • attach_ms(n, callback, arg):每隔 n 毫秒周期性执行

​
#include <Ticker.h>
 
#define LED 4
#define LED_ONCE 2
​
​
// 定义定时器对象
Ticker timer;
Ticker timer_once;
​
​
// 定义定时器中断回调函数
void toggle(int pin) {
  digitalWrite(pin, !digitalRead(pin));
}
 
​
void setup() {
  pinMode(LED, OUTPUT);
  pinMode(LED_ONCE, OUTPUT);
​
  // 配置周期性定时器
  timer.attach(0.5, toggle, LED);
  // 配置一次性定时器
  timer_once.once(3, toggle, LED_ONCE);
}
 
void loop() {
  
}

7.Wi-Fi连接

1.硬件电路设计

连接无线路由器,将 ESP32 的 IP 地址等信息通过 Shell 控制台输出显示。由于 ESP32 内置 WIFI 功能,所以直接在开发板上使用即可,无需额外连接。

2.软件电路设计

Arduino 已经集成了 Wi-Fi 模块,因此我们可以直接使用该模块。

模块包含热点 AP 模式和客户端 STA 模式,热点 AP 是指电脑或手机端直接连接 ESP32 发出的热点实现连接,如果电脑连接模块 AP 热点,这样电脑就不能上网,因此在使用电脑端和模块进行网络通信时,一般情况下都是使用 STA 模式。也就是电脑和设备同时连接到相同网段的路由器上。

下面是一些 ESP32 Arduino 库中常用的 Wi-Fi 相关函数的介绍:

  1. WiFi.begin(ssid, password):该函数用于连接到 Wi-Fi 网络。需要提供要连接的网络的 SSID 和密码作为参数。

  2. WiFi.disconnect():该函数用于断开当前的 Wi-Fi 连接。

  3. WiFi.status()

    :该函数返回当前 Wi-Fi 连接的状态。返回值可能是以下之一:

    • WL_CONNECTED:已连接到 Wi-Fi 网络。

    • WL_DISCONNECTED:未连接到 Wi-Fi 网络。

    • WL_IDLE_STATUS:Wi-Fi 处于空闲状态。

    • WL_NO_SSID_AVAIL:未找到指定的 Wi-Fi 网络。

  4. WiFi.localIP():该函数返回 ESP32 设备在 Wi-Fi 网络中分配的本地 IP 地址。

  5. WiFi.macAddress():该函数返回 ESP32 设备的 MAC 地址。

  6. WiFi.scanNetworks():该函数用于扫描周围可用的 Wi-Fi 网络。它返回一个整数,表示扫描到的网络数量。可以使用其他函数(如 WiFi.SSID()WiFi.RSSI())来获取每个网络的详细信息。

  7. WiFi.SSID(networkIndex):该函数返回指定索引的扫描到的 Wi-Fi 网络的 SSID。

  8. WiFi.RSSI(networkIndex):该函数返回指定索引的扫描到的 Wi-Fi 网络的信号强度(RSSI)。

  9. WiFi.softAPIP():该函数用于获取设备在ESP32 Wi-Fi网络中分配的IP地址

#include <WiFi.h>
​
​
#define LED   2
​
​
// 定义 Wi-Fi 名与密码
const char * ssid = "WiFi名";
const char * password = "WiFi密码";
​
void setup() {
  Serial.begin(9600);
​
  // 断开之前的连接
  WiFi.disconnect(true);
  // 连接 Wi-Fi
  WiFi.begin(ssid, password);
​
  Serial.print("正在连接 Wi-Fi");
​
  // 检测是否链接成功
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    }
  Serial.println("连接成功");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
​
  // 使用板载 LED 反馈连接成功
  pinMode(LED, OUTPUT);
  
  digitalWrite(LED, HIGH);
  delay(100);
  digitalWrite(LED, LOW);
  delay(100);
​
  digitalWrite(LED, HIGH);
  delay(100);
  digitalWrite(LED, LOW);
  delay(100);
​
  digitalWrite(LED, HIGH);
  delay(1500);
  digitalWrite(LED, LOW);
}
​
void loop() {
}

创建热点示例:

#include <WiFi.h>
​
// 设置要创建的热点名与密码
const char * ssid = "ESP32_AP";
const char * password = "12345678";
​
​
void setup() {
  Serial.begin(9600);
​
  // 创建热点
  WiFi.softAP(ssid, password);
​
  //  打印热点 IP
  Serial.print("Wi-Fi 接入的 IP:");
  Serial.println(WiFi.softAPIP());
​
}
​
void loop() {
​
}

8.发送网络请求

1.实验原理

ESP32 支持 2.4G 网络,那我们可以通过发送 HTTP 请求来获取实时天气数据。一般来说,天气数据是由一些公共 API 接口提供的,这些接口需要向它们发送 HTTP 请求以获取数据。

2.HTTP 请求与 API

当我们在浏览器中输入网址或者使用应用程序时,我们实际上是向服务器发出请求。HTTP 请求是客户端(如浏览器)与服务器之间通信的方式,用于获取或发送 Web 资源。这些资源可以是文本文件、图像、脚本等,客户端通过 HTTP 协议发起请求,服务器返回相应的响应。

HTTP 请求通常由以下几个部分组成:

1.请求行:包含请求方法、请求 URL 和 HTTP 协议版本,例如

GET https://www.baidu/content-search.xml HTTP/1.1

GET 是请求方法,https://www.baidu com/ 是 URL 地址,HTTP/1.1 指定了协议版本。

HTTP 协议版本一般都是 HTTP/1.1,URL 是你要访问的地址,而请求方法除了 GET 还有 POST、PUT、DELETE 经常使用的 4 个请求方式,以及一些其他的请求方法。

2.请求头:包含与请求相关的信息,例如浏览器类型、请求时间等。

3.请求体:包含请求所需的数据。

我们虽然可以对任意网址发送网络请求,但是这样毫无意义,比如,我想要获取某个地区的天气状况,就需要调用相对应的接口,也就是 API。

API(Application Programming Interface)是指应用程序编程接口,它定义了应用程序之间进行通信的方式和规范。API 允许不同的应用程序之间进行数据交换,使得应用程序可以共享资源和信息,从而提高应用程序的效率和可用性。

API 通常使用 HTTP 请求来提供服务,客户端通过发送 HTTP 请求访问 API,服务器则通过 HTTP 响应返回所需的数据。API 可以提供许多不同的服务,例如访问数据库、获取实时数据、处理图像等。

当我们使用别人提供的 API 的时候就需要遵守别人制定的规则,使用对应的链接、请求方法等等,我们需要查看 API 文档来获取这些信息。比如,我们今天使用聚合数据的 API 接口。

总之,HTTP 请求是客户端与服务器之间通信的方式,API 则是应用程序之间通信的方式。通过 HTTP 请求访问 API,我们可以实现不同应用程序之间的数据交换和共享。

3.JSON 数据

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,常用于 Web 应用程序之间的数据传输。它是一种文本格式,易于阅读和编写,并且可以被各种编程语言支持。

JSON 数据由键值对组成,其中键是字符串,值可以是字符串、数字、布尔值、数组、对象等数据类型。一个基本的 JSON 对象看起来像这样:

{
  "name": "老王",
  "age": 20,
  "isStudent": true,
  "hobbies": ["NBA", "吃"],
  "address": {
    "city": "普宁",
    "state": "广东"
  }
}

其中,nameageisStudenthobbiesaddress 都是键,而对应的值分别是字符串 "罗大富"、数字 29、布尔值 false、字符串数组 ["睡觉", "打游戏"] 和一个嵌套的 JSON 对象 {"city": "菏泽", "state": "山东"}

JSON 数据通常用于 Web 应用程序中,例如从后端服务器获取数据或向后端服务器发送数据。在前端 JavaScript 中,可以使用内置的 JSON 对象将 JSON 字符串转换为 JavaScript 对象,或将 JavaScript 对象转换为 JSON 字符串。

4.软件程序设计

想要发送 HTTP 请求,我们就需要用到 HTTPClient 库。

HTTPClient 库是一个用于 Arduino 的 HTTP 客户端库,它提供了一组函数来轻松地发送 HTTP 请求并处理服务器响应。HTTPClient 库基于 ESP-IDF 的 HTTP 客户端实现,并在Arduino框架下进行了封装,使其易于使用。

以下是 HTTPClient 库的一些常用功能和函数:

  1. HTTPClient http;:创建 HTTPClient 对象。

  2. http.begin(url):指定要发送请求的 URL。

  3. http.addHeader(name, value):添加 HTTP 头部。

  4. http.setAuthorization(username, password):设置 HTTP 基本身份验证的用户名和密码。

  5. http.setTimeout(timeout):设置请求超时时间(以毫秒为单位)。

  6. http.GET():发送 GET 请求,并返回一个 HTTP 状态码。

  7. http.POST(payload):发送 POST 请求,并将 payload 作为请求正文。

  8. http.responseStatusCode():获取响应的状态码。

  9. http.responseHeaders():获取响应的头部。

  10. http.responseBody():获取响应的正文。

  11. http.getString():获取响应正文作为字符串。

  12. http.getStream():获取响应正文作为流对象。

  13. http.end():关闭连接并释放资源。

我们从 Web 服务获取的是 JSON 数据,要想解析 JSON 数据,可以使用 Arduino 的 ArduinoJSON 库。ArduinoJSON 库使您能够解析和生成 JSON 数据,以及在 Arduino 上处理 JSON 格式的数据。

下面是使用 ArduinoJSON 库解析 JSON 数据的基本步骤:

  1. 引入 ArduinoJson.h 头文件;

  2. 创建一个 DynamicJsonDocument 对象来存储和处理 JSON 数据

DynamicJsonDocument doc(1024);  // 指定 JSON 文档的大小
  1. 使用 deserializeJson() 函数将 JSON 数据解析到 DynamicJsonDocument 对象中

deserializeJson(doc, json);
  • doc:DynamicJsonDocument 对象,用于存储解析后的 JSON 数据。

  • json:包含 JSON 数据的字符串或字符数组

  1. 通过使用 .as<type>() 方法从解析后的 JSON 文档中获取值:

int value = doc["key"].as<int>();
  • key:JSON 对象的键。

  • as<type>():将值转换为指定的类型,例如 int、float、String 等。

因此,我们的获取实时天气网络数据的代码可以这么写:

#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
​
const char* ssid = "GeeksMan";
const char* password = "123456qq.";
​
// 定义
String url = "http://apis.juhe/simpleWeather/query";
String city = "城市名";
String key = "你的请求Key";
​
void setup() {
  Serial.begin(9600);
​
  // 连接 WiFi
  WiFi.begin(ssid, password);
​
  Serial.print("正在连接 Wi-Fi");
  
  // 检测是否连接成功
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    }
​
  Serial.println("连接成功");
  Serial.print("IP 地址:");
  Serial.println(WiFi.localIP());
​
  // 创建 HTTPClient 对象
  HTTPClient http;
​
  // 发送GET请求
  http.begin(url+"?city="+city+"&key="+key);
​
  int httpCode = http.GET();
​
  // 获取响应状态码
  Serial.printf("HTTP 状态码: %d", httpCode);
​
  // 获取响应正文
  String response = http.getString();
  Serial.println("响应数据");
  Serial.println(response);
​
  http.end();
​
  // 创建 DynamicJsonDocument 对象
  DynamicJsonDocument doc(1024);
​
  // 解析 JSON 数据
  deserializeJson(doc, response);
​
  // 从解析后的 JSON 文档中获取值
  unsigned int temp = doc["result"]["realtime"]["temperature"].as<unsigned int>();
  String info = doc["result"]["realtime"]["info"].as<String>();
  int aqi = doc["result"]["realtime"]["aqi"].as<int>();
  
  Serial.printf("温度: %d\n", temp);
  Serial.printf("天气: %s\n", info);
  Serial.printf("空气指数: %d\n", aqi);
  
}
​
​
void loop() {
​
}

本文标签: 为例 一篇文章 编程语言 Arduino