ArduinoPlus开发板-编程手册

ArduinoPlus = 基于 Arduino UNO 二次开发的教学主板,板载集成了双路直流电机驱动。 本手册可用于教师备课,也可用于学生自学使用:语法点给出结构与对照,案例章提供完整、可直接复制上传的程序。 适用对象:中小学信息技术 / 通用技术、高校低年级嵌入式入门、创客社团等。


arduino1.jpg

目录


1. 认识 ArduinoPlus

1.1 它是什么

项目说明
名称ArduinoPlus
基础基于 Arduino UNO(ATmega328P)二次开发,指令、语法、IDE 与标准 UNO 完全一致
主频 / 电平16 MHz / 5 V 逻辑电平
增强部分板载双路直流电机驱动,可直接驱动两个直流减速电机
编程环境Arduino IDE(C/C++ 风格语法)

因为内核就是 UNO,所以网上所有 Arduino UNO 的教程、库、语法都能直接用,不必担心兼容问题。

1.2 引脚功能定义

电机相关引脚(ArduinoPlus 核心特性)

Arduino 引脚作用信号类型
D4电机 M1 方向数字量(HIGH / LOW)
D5电机 M1 转速模拟量(PWM,analogWrite)
D6电机 M2 转速模拟量(PWM,analogWrite)
D7电机 M2 方向数字量(HIGH / LOW)

其余引脚(与标准 UNO 相同)

引脚可用功能
D0 / D1串口 RX / TX(下载程序时使用,外接模块会干扰下载)
D2 / D3数字 IO + 外部中断引脚
D8 / D12数字 IO
D9 D10 D11数字 IO + PWM(~)
D13数字 IO + 板载 LED(常量 LED_BUILTIN)
A0 – A5模拟输入(也可当数字 IO 用)

速记表

功能可用引脚(ArduinoPlus)
PWM 输出 analogWrite3、5、6、9、10、11(电机模式下 5、6 归电机)
外部中断2、3
模拟输入 analogReadA0 – A5
板载 LED13

1.3 四位拨码开关

ArduinoPlus 板上有一个 4 位拨码开关,决定 D4 D5 D6 / D7 这四根脚"通向哪里"。

开关状态引脚去向使用场景
4 位全部向上(ON)接通板载电机驱动使用电机时必须全部拨上去
4 位全部向下(OFF)接通排针当普通 IO 用(读写传感器、点灯等)
有的上、有的下——⛔ 禁止,信号会被切断或冲突

拨码开关选择流程

预览
源码

驱动电机

普通 IO

一部分上、一部分下

开始接线

先断开电源

这个项目中
D4-D7 要做什么?

四位全部向上 ON

四位全部向下 OFF

写代码使用 analogWrite / digitalWrite 控制电机

写代码当作普通数字引脚使用

上电运行

禁止此状态

graph TD
  A[开始接线] --> B[先断开电源]
  B --> C{这个项目中<br/>D4-D7 要做什么?}
  C -->|驱动电机| D[四位全部向上 ON]
  C -->|普通 IO| E[四位全部向下 OFF]
  D --> F[写代码使用 analogWrite / digitalWrite 控制电机]
  E --> G[写代码当作普通数字引脚使用]
  F --> H[上电运行]
  G --> H
  C -->|一部分上、一部分下| I[禁止此状态]
  I --> B

使用口诀

用电机 → 四个全往上
当 IO → 四个全往下
切开关 → 一定先断电

⚠️ 在电机模式下,不要再写 digitalRead(D4) 之类的代码去读这四根脚,读到的已经不是外部信号了。

1.4 安全提醒

  1. 切换拨码开关前先断电;

  2. 下载程序时留意 D0 / D1 不要接着会发数据的模块;

  3. 电机必须由板上/外部电机电源供电,不要指望 USB 那点电流;

  4. 程序里养成习惯:setup() 开头先把电机速度置 0。


2. 十分钟跑起来

2.1 下载与安装 Arduino IDE

官方下载地址:https://www.arduino.cc/en/software

系统选哪一项说明
WindowsWindows Win 10 or newer (64-bit)常规安装包,一路"下一步"即可
WindowsWindows ZIP file免安装版,解压即用,适合机房统一部署
macOSmacOS拖入 Applications
LinuxLinux AppImage / 64-bit ZIP加执行权限后运行
临时用 / 机房Arduino Cloud Editor网页版,联网即可写程序,但不能离线
  • 当前稳定版为 2.3.x(本手册按 2.x 界面编写)。旧版 1.8.19 仅供老电脑使用,教学建议统一用 2.x——有自动补全、报错能直接点跳转。

  • ArduinoPlus 内核就是 UNO,不需要额外安装开发板支持包,装好 IDE 选 "Arduino Uno" 即可。

  • 首次启动会下载索引文件,等它跑完再写代码。

2.2 界面速览(5 分钟认识界面)

┌──────────────────────────────────────────────────────┐
│ 文件 编辑 项目 工具 帮助          [板卡 ▾] [端口 ▾]   │ ← 菜单栏 + 板卡/端口
├────┬─────────────────────────────────────────────────┤
│ 侧 │  ✓ 验证      → 上传      ⌨ 串口监视器            │ ← 顶部工具栏
│ 边 │─────────────────────────────────────────────────│
│ 栏 │                                                 │
│ 📁 │        代码编辑区(有行号、自动补全)             │
│ 📚 │                                                 │
│ 🔍 │                                                 │
├────┴─────────────────────────────────────────────────┤
│ 输出区:编译结果 / 上传进度 / 报错信息(红色)          │
└──────────────────────────────────────────────────────┘
区域作用
顶部工具栏 ✓验证(编译):只检查语法,不上传
顶部工具栏 →上传:编译并烧写到板子
顶部工具栏 放大镜打开串口监视器
左侧栏(书本图标)库管理器:搜索安装第三方库
左侧栏(芯片图标)开发板管理器:安装其他型号的支持包
中间代码区写程序的地方,报错行会标红
底部输出区看编译是否通过、报错在第几行

两个必做设置

  1. 工具 → 开发板 → Arduino AVR Boards → Arduino Uno

  2. 工具 → 端口 → COMx(Windows 是 COM3、COM4……;拔掉 USB 再插一次,看多了哪个口)

想换中文界面:文件 → 首选项 → Language → 简体中文,重启 IDE 生效。 程序默认保存在 文档\Arduino 目录下(sketchbook 路径),可在"首选项"里查看或修改。

2.3 外部库导入(简单了解)

库就是别人写好的功能包,装好后用 #include <库名.h> 引入,可以直接调用现成函数(比如舵机库 Servo、超声波库 NewPing)。

方式一:库管理器(推荐,最简单)

  1. 点左侧书本图标(或 Ctrl + Shift + I)

  2. 搜索关键词,例如 Servo

  3. 点 INSTALL,装完即可

方式二:导入 .ZIP 库

  1. 先把下载的库压缩包放在电脑里(不要解压)

  2. 项目 → 加载库 → 添加 .ZIP 库…,选中该 zip

  3. 装完重启 IDE

方式三:手动拷贝

把解压后的库文件夹整个复制到 文档\Arduino\libraries 下,重启 IDE。

使用示例(以舵机库为例)

#include <Servo.h>          // ① 引入库

Servo myservo;              // ② 创建一个舵机对象

void setup() {
  myservo.attach(9);        // ③ 告诉库:舵机信号线接在 D9
}

void loop() {
  myservo.write(90);        // ④ 调用库提供的函数:转到 90 度
  delay(1000);
  myservo.write(0);         //     转到 0 度
  delay(1000);
}

常见问题

现象处理
No such file or directory库没装成功,或 #include 的名字写错了
装完看不到重启 IDE;确认装的是当前用户目录
同名库冲突只保留一个版本,删掉 libraries 里的旧文件夹
装了库但示例没出现文件 → 示例 拉到最下面找该库的名字

2.4 上手流程

流程图

预览
源码

编译通过

有报错

正常

不正常

插 USB 线

选板卡 Arduino Uno

选端口 COMx

写代码

Ctrl + R 验证编译

Ctrl + U 上传

看报错信息
对照第 16 章修改

打开串口监视器
波特率 9600

现象正常?

完成

加 Serial.println 定位

graph TD
  A[插 USB 线] --> B[选板卡 Arduino Uno]
  B --> C[选端口 COMx]
  C --> D[写代码]
  D --> E["Ctrl + R 验证编译"]
  E -->|编译通过| H["Ctrl + U 上传"]
  E -->|有报错| F["看报错信息<br/>对照第 16 章修改"]
  F --> D
  H --> I[打开串口监视器<br/>波特率 9600]
  I --> J{现象正常?}
  J -->|正常| L[完成]
  J -->|不正常| K[加 Serial.println 定位]
  K --> D

常用快捷键

操作快捷键作用
验证(编译)Ctrl + R检查有没有语法错误
上传Ctrl + U编译并烧写到板子
串口监视器Ctrl + Shift + M看程序打印的信息
自动格式化Ctrl + T一键整理缩进(交作业前按一下)

2.5 上传失败排查

现象处理
找不到端口换 USB 线 / 换口;确认板子通电
programmer is not responding板卡型号选错;D0/D1 被外设占用
Board at COMx is not available串口监视器或其他软件占用了端口,先关掉

3. 程序的基本骨架

3.1 只有两个必需函数

void setup() {
  // 上电(或复位)后【只执行一次】,写初始化内容
}

void loop() {
  // setup 结束后【反复执行】,写主逻辑
}

执行顺序图

预览
源码

循环往复,直到断电

上电 / 复位

执行 setup 一次
初始化引脚与串口

进入 loop 第 1 次
从上到下执行语句

loop 第 2 次

loop 第 3 次 ……

graph TD
  A[上电 / 复位] --> B["执行 setup 一次<br/>初始化引脚与串口"]
  B --> C["进入 loop 第 1 次<br/>从上到下执行语句"]
  C --> D[loop 第 2 次]
  D --> E[loop 第 3 次 ……]
  E -->|循环往复,直到断电| C

3.2 语法案例:第一段可以照抄的程序

const byte LED_PIN = LED_BUILTIN;   // 常量:板载 LED 引脚 = 13

void setup() {
  pinMode(LED_PIN, OUTPUT);         // 把这个脚设成"输出"
}

void loop() {
  digitalWrite(LED_PIN, HIGH);      // 输出高电平 → 亮
  delay(1000);                      // 等 1000 毫秒 = 1 秒
  digitalWrite(LED_PIN, LOW);       // 输出低电平 → 灭
  delay(1000);
}

3.3 写代码的三条纪律

纪律说明
每条语句以 ; 结尾忘了就是编译报错第一名
成对的花括号 { }写 { 立刻补 },再填中间的内容
大小写敏感PinMode ≠ pinMode,IDE 不会帮你改

注释

// 单行注释:解释这一行在干嘛

/*
   多行注释:
   用在函数开头说明作者、功能、参数
*/

4. 变量、常量与数据类型

4.1 变量的声明与赋值

int  score   = 0;        // 声明的同时赋值(推荐)
char grade;              // 先声明
grade = 'A';             // 后赋值
bool isReady = true;     // true / false

4.2 数据类型表

类型取值范围典型用途
booltrue / false开关状态、标志位
byte0 – 255引脚号、PWM 值(省内存)
int-32 768 – 32 767最常用的整数
unsigned int0 – 65 535非负整数
long±21 亿大整数
unsigned long0 – 42 亿millis() 的时间必须用它
float小数(6–7 位有效)温度、电压(运算较慢)
char单个字符'A'、'1'(单引号)
char[]字符串"hello"(双引号)

选类型原则:够用就好。引脚和 0–255 的值一律用 byte;时间一律用 unsigned long。

4.3 命名规范(建议从一开始就养成)

const byte LED_PIN = 13;      // 常量:全大写 + 下划线
int  buttonCount   = 0;       // 变量:小驼峰,见名知意
bool isRunning     = false;   // 布尔:用 is/has 开头

int a, b, c;                  // ❌ 不要用这种名字

4.4 作用域

int globalVal = 10;            // 【全局变量】:任何函数都能用

void setup() {
  int temp = 5;                // 【局部变量】:只在 setup 里有效
  Serial.println(globalVal);   // OK
}

void loop() {
  Serial.println(globalVal);   // OK
  // Serial.println(temp);     // ❌ 编译错误:temp 在 loop 里不存在
}
修饰含义
局部函数内定义,出了函数失效
全局函数外定义,全程序可见(少用)
static函数调用结束后值仍保留
volatile中断函数里会修改的变量必须加它

4.5 常量

const byte LED_PIN = LED_BUILTIN;   // ✅ 推荐:有类型检查
#define LED_PIN 13                  // 也可以:编译前文本替换

// 内置常用常量
HIGH / LOW                     // 电平
INPUT / OUTPUT / INPUT_PULLUP  // 引脚模式
true / false
LED_BUILTIN                    // 板载 LED(ArduinoPlus 上 = 13)
PI                             // 3.14159…

5. 运算符

5.1 算术运算符

符号含义例子结果
+加3 + 47
-减7 - 25
*乘3 * 412
/除5 / 2(两个整数相除)2(小数被丢掉)
%取余(求余数)7 % 31
int a = 5 / 2;         // 2,不是 2.5
float b = 5.0 / 2;     // 2.5 ✅ 至少写成 5.0
int c = 7 % 3;         // 1 → "7 除以 3 余 1"

% 的常用场景:每隔 N 次做一件事。if (count % 5 == 0) 就是每 5 次执行一次。

5.2 比较运算符(结果只有真 / 假)

符号含义例子
==等于a == 5
!=不等于a != 5
<小于a < 10
>大于a > 0
<=小于等于a <= 255
>=大于等于a >= 0

⚠️ 判断相等是两个等号 ==,一个等号 = 是赋值。这是初学者第一大坑,详见 5.6。

5.3 逻辑运算符(把多个条件连起来)

符号含义写法什么时候结果为真    
&&与(and)A && BA 和 B 都成立    
``或(or)`AB`A、B 任意一个成立
!非(not)!AA 不成立时为真    

真值表(背下来比记文字快)

ABA && B`AB`!A
falsefalsefalsefalsetrue  
falsetruefalsetruetrue  
truefalsefalsetruefalse  
truetruetruetruefalse  
if (x >= 0 && x <= 255) { }     // ✅ 判断区间
if (0 <= x <= 255)       { }    // ❌ 错误写法!必须拆成上面那种
if (!isRunning)          { }    // "没有在跑"

5.4 赋值运算符

符号含义简写完整写法
=直接赋值a = 5—
+=加完再存回a += 3a = a + 3
-=减完再存回a -= 3a = a - 3
*=乘完再存回a *= 2a = a * 2
/=除完再存回a /= 2a = a / 2
%=取余后存回a %= 10a = a % 10

初学阶段先写完整形式(a = a + 3),看懂了再用简写,不容易出错。

5.5 自增 ++ 与自减 --

一句话:i++ 就是"把 i 加 1",等价于 i = i + 1;i-- 等价于 i = i - 1。

int i = 0;
i++;        // 执行后 i 变成 1
i++;        // 再执行,i 变成 2
i--;        // i 变回 1

两种写法:放前面还是放后面

写法名称规则举例(假设 i 原来是 5)
i++后置自增先用旧值,再加 1int a = i++; → a = 5,i 变 6
++i前置自增先加 1,再用新值int a = ++i; → a = 6,i 变 6
i--后置自减先用旧值,再减 1int a = i--; → a = 5,i 变 4
--i前置自减先减 1,再用新值int a = --i; → a = 4,i 变 4
int i = 5;
int a = i++;     // a = 5(旧值),i = 6
int b = ++i;     // i 先变 7,b = 7(新值)

记住三条就够

  1. 单独写成一句时(比如 for 里的 i++),i++ 和 ++i 效果完全一样,随便用哪个。

  2. 只有把它塞进别的表达式里(如 int a = i++)才有区别——初学阶段建议避开这种写法,拆成两行写。

  3. 一条语句里不要对同一个变量用两次 ++(如 i = i++ + ++i),结果没有意义,属于典型错误示范。

for (int i = 0; i < 10; i++) { }    // ✅ 最常见用法
                                    // 也可以写 ++i,结果相同

5.6 三个高频陷阱

陷阱一:把 == 写成 =

int x = 5;
if (x == 5) { }     // ✅ 判断:x 是不是等于 5
if (x = 5)  { }     // ❌ 赋值:x 被改成 5,条件永远为真,程序逻辑全乱

陷阱二:整数除法会丢掉小数

int  a = 5 / 2;        // 结果是 2(不是 2.5!)
float b = 5.0 / 2;     // 结果是 2.5 ✅ 必须写成 5.0
float c = (float)5 / 2;// 结果 2.5 ✅ 或强制类型转换

陷阱三:% 取余的经典用法

for (int i = 0; i < 10; i++) {
  if (i % 2 == 0) Serial.println(i);   // 打印偶数
}
if (n % 5 == 0) { /* 每 5 次做一件事 */ }

6. 条件判断 if else switch

6.1 基本形式

if (条件) {
  // 条件成立时执行
}

语法案例:读取引脚

if (digitalRead(2) == LOW) {      // 按键按下(上拉输入时按下为 LOW)
  digitalWrite(LED_BUILTIN, HIGH);
}

6.2 if - else(二选一)

if (条件) {
  // 成立走这里
} else {
  // 不成立走这里
}
if (temperature > 30) {
  Serial.println("太热了");
} else {
  Serial.println("还行");
}

6.3 else if(多选一)

if (score >= 90) {
  Serial.println("优秀");
} else if (score >= 60) {
  Serial.println("及格");
} else {
  Serial.println("加油");
}

判断是从上往下逐个比较,命中第一个就跳出,所以顺序很重要。

6.4 嵌套(分层判断)

if (isRunning) {
  if (speed > 200) {
    Serial.println("高速运行中");
  } else {
    Serial.println("低速运行中");
  }
} else {
  Serial.println("已停止");
}

嵌套超过 3 层就该考虑拆成函数了。

6.5 多条件组合

if (btnA == LOW && btnB == LOW) { }        // 与:两个都按下
if (btnA == LOW || btnB == LOW) { }        // 或:任意一个按下
if (!isRunning) { }                        // 非:没在运行
if (x >= 0 && x <= 255) { }                // 区间(不能写成 0 <= x <= 255)

写 (A) && (B) 时加括号更保险,运算顺序一目了然。

6.6 三元运算符(简化版 if-else)

// 格式:  条件 ? 成立时的值 : 不成立时的值
int absSpeed = (speed < 0) ? -speed : speed;

digitalWrite(dirPin, forward ? HIGH : LOW);   // 根据布尔值选电平

6.7 switch(离散值分支)

char cmd = Serial.read();

switch (cmd) {
  case 'f':
    Serial.println("前进");
    break;                       // 每个 case 记得 break
  case 'b':
    Serial.println("后退");
    break;
  case 's':
    Serial.println("停止");
    break;
  default:                       // 都不匹配时执行(建议保留)
    Serial.println("未知指令");
    break;
}
对比适合场景
if - else if判断范围(>60、<=100)
switch判断固定的离散值(字符、数字编号)

6.8 if 常见错误

错误写法问题正确写法
if x > 5少了括号if (x > 5)
if (x = 5)赋值而非比较if (x == 5)
if (x > 5);分号让判断"空转"去掉 ;
if (0 < x < 100)数学写法无效if (x > 0 && x < 100)
if (x && y == 1)优先级混乱if ((x != 0) && (y == 1))

7. 计数循环 for

7.1 结构

for (初始化; 循环条件; 每次结束后更新) {
  // 循环体
}

执行顺序流程图

预览
源码

假 false

真 true

开始

① 初始化
int i = 0(只做一次)

② 判断条件
i < 10 ?

退出循环
继续往下执行

③ 执行循环体
花括号里的语句

④ 执行更新
i++

graph TD
  A[开始] --> B["① 初始化<br/>int i = 0(只做一次)"]
  B --> C{"② 判断条件<br/>i < 10 ?"}
  C -->|假 false| D[退出循环<br/>继续往下执行]
  C -->|真 true| E["③ 执行循环体<br/>花括号里的语句"]
  E --> F["④ 执行更新<br/>i++"]
  F --> C

7.2 语法案例

// 从 0 数到 9
for (int i = 0; i < 10; i++) {
  Serial.println(i);
}
// 倒着数:10 → 1
for (int i = 10; i > 0; i--) {
  Serial.println(i);
}
// 步长不为 1:每次加 5
for (int duty = 0; duty <= 255; duty += 5) {
  analogWrite(9, duty);      // 逐档提高 LED / 电机速度
  delay(50);
}
// 重复做固定次数的事
for (int t = 0; t < 3; t++) {
  digitalWrite(LED_BUILTIN, HIGH);
  delay(200);
  digitalWrite(LED_BUILTIN, LOW);
  delay(200);
}

7.3 遍历数组

int pins[] = {8, 9, 10, 11};         // 4 个引脚

for (int i = 0; i < 4; i++) {
  pinMode(pins[i], OUTPUT);          // 一次性初始化全部
}

7.4 嵌套循环(打印图形 / 控制点阵)

for (int row = 0; row < 3; row++) {        // 外层:行
  for (int col = 0; col < 5; col++) {      // 内层:列
    Serial.print("*");
  }
  Serial.println();                        // 一行结束换行
}

7.5 for 循环常见陷阱

陷阱说明处理
循环次数不对i < 10 跑 10 次(0–9),i <= 10 跑 11 次想清楚要不要等号
数组越界int a[5] 的下标是 0–4,a[5] 会踩内存循环条件写 i < 5
改了循环变量在循环体内又写 i = 0 会变成死循环循环体内别动 i
条件永远为真for (int i = 0; i >= 0; i++) 永不停止检查条件的方向

8. 条件循环 while / do-while

8.1 结构

while (条件) {
  // 条件为真就一直执行
}

语法案例

int count = 0;
while (count < 5) {
  Serial.println(count);
  count++;                       // ❗忘了这句就是死循环
}

8.2 for 还是 while?

用法场景例子
for知道要跑多少次(计数)跑 10 次、遍历 8 个引脚
while只知道什么时候停(等待某个条件)等按键松开、等到天黑、等到串口来数据
// 等待型用法:一直等按键被按下
while (digitalRead(2) == HIGH) {
  // 还没按下,空转等待
}
Serial.println("按下啦!");
// 等待串口数据到来
while (Serial.available() == 0) { }
char c = Serial.read();

等待型 while 里至少要留一件事(哪怕 delay(1))让系统喘口气,避免某些环境下卡死。

8.3 do-while(先做一次再判断)

int v;
do {
  v = analogRead(A0);            // 先读一次
  delay(100);
} while (v < 500);               // 没到阈值就继续读
对比最少执行次数
while0 次(条件一开始就不成立)
do-while1 次

8.4 break 与 continue

for (int i = 0; i < 100; i++) {
  if (digitalRead(2) == LOW) break;      // break:立刻跳出整个循环
  if (i % 2 == 1) continue;              // continue:跳过本次,进入下一轮
  Serial.println(i);
}

死循环(条件永远为真)是新手最常见的"程序卡住"原因:看到板子没反应,先检查每个 while 的退出条件。


9. 等待与时间控制 delay / millis

9.1 delay:最简单的等待

delay(1000);           // 等 1000 毫秒 = 1 秒
delay(500);            // 等 0.5 秒
delayMicroseconds(10); // 等 10 微秒

语法案例:延时执行序列

digitalWrite(LED_BUILTIN, HIGH);
delay(1000);                          // 亮 1 秒
digitalWrite(LED_BUILTIN, LOW);
delay(1000);                          // 灭 1 秒

9.2 delay 的致命缺点:阻塞

delay() 执行期间,单片机什么都不干:

delay(1000) 期间:按键?不响应。串口数据?不读。传感器?不看。

所以:一次只做一件事的练习用 delay 没问题;想同时做多件事(比如一边闪灯一边读按键),就必须用 millis()。

9.3 millis():定时任务的标准写法(重点)

millis() 返回"开机到现在的毫秒数",类型是 unsigned long。

unsigned long lastTime = 0;                  // 记录上次做事的时刻
const unsigned long INTERVAL = 1000;         // 每隔多久做一次

void loop() {
  unsigned long now = millis();

  if (now - lastTime >= INTERVAL) {          // 到点了
    lastTime = now;                          // 更新时间戳
    // 这里写"到点要做的事"
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }

  // loop 继续往下跑,还能同时做别的事
}

两种写法对比流程图

delay 写法(阻塞)

预览
源码

做事 A

delay 1000
原地空等

做事 B

delay 1000
期间按键、串口全不响应

graph TD
  A1[做事 A] --> A2["delay 1000<br/>原地空等"]
  A2 --> A3[做事 B]
  A3 --> A4["delay 1000<br/>期间按键、串口全不响应"]
  A4 --> A1

millis 写法(并行)

预览
源码

是

否

是

否

loop 开始

millis - lastA
≥ 间隔 ?

做任务 A
lastA = millis

millis - lastB
≥ 间隔 ?

做任务 B
lastB = millis

读按键 / 串口

loop 结束,立刻重来

graph TD
  B1[loop 开始] --> B2{"millis - lastA<br/>≥ 间隔 ?"}
  B2 -->|是| B3["做任务 A<br/>lastA = millis"]
  B2 -->|否| B4{"millis - lastB<br/>≥ 间隔 ?"}
  B3 --> B4
  B4 -->|是| B5["做任务 B<br/>lastB = millis"]
  B4 -->|否| B6[读按键 / 串口]
  B5 --> B6
  B6 --> B7["loop 结束,立刻重来"]
  B7 --> B1

9.4 语法案例:两个任务同时进行

unsigned long lastBlink = 0, lastPrint = 0;

void loop() {
  unsigned long now = millis();

  if (now - lastBlink >= 500) {              // 任务1:每 500ms 翻转 LED
    lastBlink = now;
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
  }

  if (now - lastPrint >= 2000) {             // 任务2:每 2000ms 打印一次
    lastPrint = now;
    Serial.print("A0 = ");
    Serial.println(analogRead(A0));
  }
}

9.5 millis 注意事项

要点说明
变量类型必须是 unsigned long,用 int 会在 32 秒后出错
比较写法用 now - lastTime >= 间隔,不要写成 now >= lastTime + 间隔(溢出时会出错)
溢出millis() 约 50 天回到 0,用上面的减法写法可自动兼容
长时间需要计时超过 50 天就得自己扩展,一般作业用不到

9.6 时间函数总表

函数单位说明
delay(ms)毫秒阻塞等待
delayMicroseconds(us)微秒精短等待
millis()毫秒开机至今(约 50 天循环)
micros()微秒开机至今(约 70 分钟循环)

10. 函数

10.1 为什么要用函数

重复的代码抽成函数 → 改一处就够了、主程序读起来像"说明书"。

不用函数:loop() 里 200 行,看不出干了啥
用函数:  loop() 里 5 行,每行一个动作,逻辑清楚

10.2 定义与调用

// 返回值类型  函数名(参数类型 参数名)
int add(int a, int b) {
  return a + b;                  // 有返回值
}

void blinkTimes(int times) {     // void = 不返回值
  for (int i = 0; i < times; i++) {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(200);
    digitalWrite(LED_BUILTIN, LOW);
    delay(200);
  }
}

void loop() {
  Serial.println(add(3, 5));     // 调用 → 打印 8
  blinkTimes(3);                 // 调用 → 闪 3 下
}

10.3 语法案例:把电机控制封装成函数

#define M1_DIR 4
#define M1_PWM 5

void motor1(int speed) {          // speed: -255 ~ +255,负数反转
  if (speed >= 0) {
    digitalWrite(M1_DIR, HIGH);
    analogWrite(M1_PWM, speed);
  } else {
    digitalWrite(M1_DIR, LOW);
    analogWrite(M1_PWM, -speed);
  }
}

void loop() {
  motor1(200);    delay(2000);
  motor1(-150);   delay(2000);
  motor1(0);      delay(2000);
}

10.4 写函数的习惯

建议说明
一个函数只做一件事名字就能说明做什么,如 readButton()、motorForward()
函数别太长超过一屏就再拆
善用参数不要直接依赖全局变量,方便复用
写在 loop() 后面也可以Arduino IDE 会自动补函数声明,但自己动手写声明是更好的习惯

11. 数组与字符串

11.1 数组

int nums[5];                        // 声明 5 个元素,下标 0–4
int scores[] = {90, 85, 78, 92};    // 声明并赋值

nums[0] = 10;                       // 赋值
Serial.println(nums[0]);            // 读取

for (int i = 0; i < 4; i++) {       // 遍历
  Serial.println(scores[i]);
}

下标从 0 开始,scores[4] 是越界访问,不会报错但会破坏别的变量 —— 这是最隐蔽的 bug。

二维数组的简单用法

int matrix[2][3] = { {1,2,3}, {4,5,6} };
Serial.println(matrix[1][2]);      // 6

11.2 字符串

char msg[]  = "Hello";       // ✅ UNO 上推荐:字符数组,省内存
String s    = "Hello";       // 也可以用(方便但更耗内存)

Serial.println(msg);
// 拼接:
String line = "Value = " + String(analogRead(A0));
对比char[]String
内存占用小较大
拼接操作麻烦(strcat)方便(+)
建议短提示语用它做串口解析时用它

12. 常用函数速查表

12.1 引脚数字量 IO

函数作用例
pinMode(pin, mode)设置方向(OUTPUT/INPUT/INPUT_PULLUP)pinMode(2, INPUT_PULLUP);
digitalWrite(pin, val)输出 HIGH / LOWdigitalWrite(13, HIGH);
digitalRead(pin)读取,返回 HIGH / LOWif (digitalRead(2) == LOW)

上拉输入口诀:INPUT_PULLUP 时,按键按下读到 LOW,松开读到 HIGH。

12.2 模拟量

函数作用范围
analogRead(pin)读 A0–A5 的电压0 – 1023
analogWrite(pin, val)PWM 输出(3/5/6/9/10/11)0 – 255
map(v, a, b, c, d)区间映射map(512,0,1023,0,255) → 127
constrain(v, lo, hi)限幅constrain(300,0,255) → 255
int  raw  = analogRead(A0);
float volt = raw * (5.0 / 1023.0);          // 转成电压值(注意 5.0)
int  duty = constrain(map(raw, 0, 1023, 0, 255), 0, 255);
analogWrite(9, duty);

12.3 数学

函数作用
min(a,b) / max(a,b)较小 / 较大值
abs(x)绝对值
constrain(x, lo, hi)限幅
map(...)区间映射
pow(b,e) / sqrt(x)幂 / 平方根
sin() cos() tan()三角函数(参数是弧度)
random(min,max)随机数(random(min,max),max 取不到)
randomSeed(seed)播种,randomSeed(analogRead(A0))

12.4 串口(调试必备)

函数作用
Serial.begin(9600)初始化波特率
Serial.print(x)打印不换行
Serial.println(x)打印并换行
Serial.print(x, HEX)按进制打印
Serial.available()缓冲区里有几个字节
Serial.read()读一个字符
Serial.parseInt()读一个整数
Serial.readStringUntil('\n')读一整行

12.5 外部中断(D2、D3)

volatile long pulse = 0;

void count() { pulse++; }     // 中断函数:短、快、无参数无返回值

void setup() {
  pinMode(2, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(2), count, FALLING);
}
触发模式时机
RISING低 → 高
FALLING高 → 低(上拉按键按下)
CHANGE任意翻转
LOW持续低电平期间

中断函数内不要用 delay() 和 Serial 打印。

12.6 类型转换

int(sensor / 10);              // 转 int
float x = float(a) / b;        // 转 float
String(42);                    // 数字转字符串
someString.toInt();            // 字符串转整数

13. ArduinoPlus 专属:电机控制写法

13.1 记住这张表

想做的事写法引脚
设定 M1 方向digitalWrite(4, HIGH);D4
设定 M1 转速analogWrite(5, 200);D5
设定 M2 方向digitalWrite(7, HIGH);D7
设定 M2 转速analogWrite(6, 200);D6
HIGH/LOW → 决定往哪边转     0–255 → 决定转多快

一个电机怎么转起来

预览
源码

是

否

给定目标速度 speed
-255 ~ +255

speed >= 0 ?

digitalWrite(方向脚, HIGH)
正转

digitalWrite(方向脚, LOW)
反转

analogWrite(速度脚, speed)

analogWrite(速度脚, -speed)

电机转动

speed == 0

analogWrite(速度脚, 0)
停止

graph TD
  A["给定目标速度 speed<br/>-255 ~ +255"] --> B{"speed >= 0 ?"}
  B -->|是| C["digitalWrite(方向脚, HIGH)<br/>正转"]
  B -->|否| D["digitalWrite(方向脚, LOW)<br/>反转"]
  C --> E["analogWrite(速度脚, speed)"]
  D --> F["analogWrite(速度脚, -speed)"]
  E --> G[电机转动]
  F --> G
  H["speed == 0"] --> I["analogWrite(速度脚, 0)<br/>停止"]

13.2 最简控制(三段式)

#define M1_DIR 4
#define M1_PWM 5
#define M2_DIR 7
#define M2_PWM 6

void setup() {
  pinMode(M1_DIR, OUTPUT);  pinMode(M1_PWM, OUTPUT);
  pinMode(M2_DIR, OUTPUT);  pinMode(M2_PWM, OUTPUT);
  analogWrite(M1_PWM, 0);   analogWrite(M2_PWM, 0);   // 开局先停,好习惯
}

void loop() {
  // 前进:两台同向同速
  digitalWrite(M1_DIR, HIGH);  analogWrite(M1_PWM, 200);
  digitalWrite(M2_DIR, HIGH);  analogWrite(M2_PWM, 200);
  delay(2000);

  // 停止
  analogWrite(M1_PWM, 0);  analogWrite(M2_PWM, 0);
  delay(1000);
}

13.3 常用需求的写法模板

① 用函数封装(推荐,自己写一遍)

void motorRun(byte dirPin, byte pwmPin, bool forward, byte spd) {
  digitalWrite(dirPin, forward ? HIGH : LOW);
  analogWrite(pwmPin, spd);
}
// 调用:motorRun(M1_DIR, M1_PWM, true, 180);
//      motorRun(M1_DIR, M1_PWM, false, 180);   // 反转

② 软启动(慢慢加速)

for (int spd = 60; spd <= 220; spd += 10) {
  digitalWrite(M1_DIR, HIGH);
  analogWrite(M1_PWM, spd);
  delay(50);
}

③ 小车基本动作

动作M1M2
前进正转 v正转 v
后退反转 v反转 v
左转反转 v正转 v
右转正转 v反转 v
停止00
// 用参数化函数表达上面这张表(留给你自己实现 ✍️)
// void move(int leftSpeed, int rightSpeed) { ... }

13.4 电机调试要点

现象原因处理
完全不转拨码没全上拨 没供电 速度为 0逐项检查
只响不转PWM 给得太小(启动死区)起步给 120 以上再降速
转向反了方向电平约定不一致把 HIGH 换成 LOW,或换接线
两边不一样快电机个体差异给慢的一边加一点补偿

14. 综合案例程序(完整可复制)

本章每个案例都是完整程序,复制粘贴即可编译上传。语法讲解章节(运算符、if / for 等)里的对照片段仍然保持精简,方便老师板书。

本章每个案例都给出功能说明、接线表、思路、完整程序、知识点、课堂扩展。程序可直接复制粘贴上传;接线表只列引脚对应关系,不涉及电路原理。

案例 A:按键控制 LED(按一次亮,再按一次灭)

功能:每按一次按键,LED 状态翻转一次(不是"按住才亮")。

接线

元件接到
按键一端D2
按键另一端GND
LED用板载 LED(D13),无需外接

思路:用 INPUT_PULLUP 省掉外部电阻;松手时读到 HIGH,按下读到 LOW。比较"上一次"和"这一次",出现下降沿说明刚按下,此时翻转灯的状态。

/* ===== 案例 A:按键控制 LED ===== */
const byte BTN_PIN  = 2;            // 按键:一端接 D2,另一端接 GND
const byte LED_PIN  = LED_BUILTIN;  // 板载 LED(D13)

bool ledOn   = false;   // 记录灯当前是否亮着
bool lastBtn = HIGH;    // 记录上一次读到的按键状态(INPUT_PULLUP:松手 = HIGH)

void setup() {
  pinMode(BTN_PIN, INPUT_PULLUP);   // 启用内部上拉电阻,按键另一端直接接 GND 即可
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);       // 开局先灭
}

void loop() {
  bool now = digitalRead(BTN_PIN);  // 读当前状态

  if (lastBtn == HIGH && now == LOW) {   // 下降沿:说明"刚按下"
    ledOn = !ledOn;                      // 状态翻转:亮→灭,灭→亮
    digitalWrite(LED_PIN, ledOn);
    delay(200);                          // 简易去抖:跳过按下瞬间的抖动
  }

  lastBtn = now;   // 把这一次记下来,供下一轮比较
}

知识点:INPUT_PULLUP、边沿检测、"上一次 vs 这一次"的状态比较、布尔取反 !、去抖。

课堂扩展

  1. 把 delay(200) 改成 50 和 500,体会去抖时长对"按一次是否被识别成多次"的影响。

  2. 改成"按住才亮、松开就灭":把整个 if 换成 digitalWrite(LED_PIN, !now);。


案例 B:PWM 呼吸灯(亮度渐亮渐灭)

功能:LED 亮度从 0 平滑升到最亮,再平滑降回 0,循环往复。

接线

元件接到
LED 长脚(正极)D9(必须是带 ~ 的 PWM 脚)
LED 短脚(负极)GND

思路:亮度值每次加 5,到 0 或 255 的边界时把步长取反,形成往复。注意不能用 D13——它不是 PWM 脚,只能亮/灭两档。

/* ===== 案例 B:PWM 呼吸灯 ===== */
const byte LED_PIN = 9;      // 必须是 PWM 脚(D3/D5/D6/D9/D10/D11 之一;别用 D13)

int brightness = 0;          // 当前亮度 0–255
int stepSize   = 5;          // 每步变化量(必须是 int,因为它要变成负数)

void setup() {
  pinMode(LED_PIN, OUTPUT);  // 规范写法:用之前先声明模式
  Serial.begin(9600);
}

void loop() {
  brightness += stepSize;                          // ① 累加(+5 或 -5)
  brightness = constrain(brightness, 0, 255);      // ② 夹住,保证不越界
  analogWrite(LED_PIN, brightness);                // ③ 输出 PWM

  if (brightness <= 0 || brightness >= 255) {      // ④ 到端点就反向
    stepSize = -stepSize;                          //    取相反数:+5 ↔ -5
  }

  Serial.println(brightness);                      // 打开串口监视器看数值变化
  delay(20);
}

知识点:analogWrite、constrain、"往复累加"思想、为什么 D13 不能渐变。

执行推演(让学生填这张表,是验证程序最快的方法)

轮次进入时 brightness+= stepSizeconstrain 后是否到边界翻转后 stepSize
100+55否+5
255+510否+5
…………否+5
51250255255是(≥255)−5
52255250250否−5
…………否−5
102500是(≤0)+5

为什么这样写是对的

  1. constrain(brightness, 0, 255) 是安全阀:无论 stepSize 取多少(哪怕 7、13 这种不能整除 255 的值),analogWrite() 都不会收到越界的数,因此不会卡在边界。

  2. 翻转条件必须带等号(>= 255、<= 0)。若写成 == 255,步长不能整除 255 时会漏掉,变成"只增不减"。

  3. stepSize = -stepSize; 一行实现"到顶往回、到底再往前",不需要两个 if。注意它必须声明成 int——byte 是无符号类型,存不了负数。

呼吸周期:255 ÷ 5 = 51 步,每步 delay(20) → 单程约 1020 ms,一个完整呼吸约 2 秒。想变慢就调小 stepSize 或调大 delay。

课堂扩展

  1. 把 stepSize 改成 1 和 15,观察渐变细腻度与速度的取舍。

  2. 加一个电位器,用 analogRead 的值控制 stepSize,做成"可调速呼吸灯"。


案例 C:多个 LED 流水灯

功能:4 个 LED 依次点亮再熄灭,像跑马灯一样循环。

接线

LED接到
LED1D8
LED2D9
LED3D10
LED4D11

(4 个 LED 的负极都接 GND;D8–D11 在电机模式下也不与电机冲突。)

思路:把引脚号放进数组,用 for 依次取出,避免写 4 遍重复代码。

/* ===== 案例 C:4 路 LED 流水灯 ===== */
const byte leds[] = {8, 9, 10, 11};        // 4 个 LED 的引脚
const byte LED_COUNT = 4;                  // LED 数量
const int  DELAY_MS  = 150;                // 每个灯亮多久

void setup() {
  for (int i = 0; i < LED_COUNT; i++) {    // 用 for 一次性完成初始化
    pinMode(leds[i], OUTPUT);
  }
}

void loop() {
  for (int i = 0; i < LED_COUNT; i++) {    // 从第一个走到最后一个
    digitalWrite(leds[i], HIGH);
    delay(DELAY_MS);
    digitalWrite(leds[i], LOW);
  }
}

知识点:数组 + for 遍历、sizeof 的替代写法(这里用常量 LED_COUNT)、批量初始化。

课堂扩展

  1. 加一段"倒着回来"的 for,做成来回流动:for (int i = LED_COUNT - 1; i >= 0; i--)。

  2. 用 analogWrite 代替 digitalWrite,让每个灯"渐亮渐灭"地流动(提示 LED 必须接 PWM 脚)。


案例 D:串口指令控制(发送字符控制设备)

功能:在串口监视器里输入字符,控制板载 LED 开关并回传状态。

接线:无需外接,用板载 LED(D13)。

思路:Serial.available() 判断有没有数据进来,有就读一个字符,用 switch 分发到不同动作。

/* ===== 案例 D:串口指令控制 ===== */
const byte LED_PIN = LED_BUILTIN;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  Serial.begin(9600);                 // 波特率必须与串口监视器一致
  Serial.println("输入 1 开灯 / 0 关灯 / ? 查询");
}

void loop() {
  if (Serial.available() == 0) return;    // 没数据就直接返回,不往下走

  char cmd = Serial.read();               // 读一个字符

  switch (cmd) {
    case '1':
      digitalWrite(LED_PIN, HIGH);
      Serial.println("LED ON");
      break;
    case '0':
      digitalWrite(LED_PIN, LOW);
      Serial.println("LED OFF");
      break;
    case '?':
      Serial.print("当前状态: ");
      Serial.println(digitalRead(LED_PIN) == HIGH ? "ON" : "OFF");
      break;
    default:
      Serial.println("未知指令(只认 1 / 0 / ?)");
      break;
  }
}

知识点:Serial.begin/available/read/print、switch 分支、早返回写法、三元运算符。

串口监视器右下角的波特率要选 9600,行结束符选"没有行结束符"(否则会多收到换行)。

课堂扩展

  1. 把 case 里的动作换成电机控制(f 前进 b 后退 s 停止),做成串口遥控小车。

  2. 加一个 case 'v',实现"先发 v 再发数字"调节速度(提示:用 Serial.parseInt())。


案例 E:电位器调节亮度 / 转速

功能:旋转电位器,LED 亮度(或电机转速)随之平滑变化,并在串口显示数值。

接线元件接到
电位器中间脚A0 
电位器两端5V 与 GND 
LEDD9(PWM 脚);若控电机则用 D5 / D6 

思路:analogRead() 得到 0–1023(10 位),而 analogWrite() 需要 0–255(8 位),中间用 map() 做量程换算,constrain() 兜底。

/* ===== 案例 E:电位器调节亮度 ===== */
const byte POT_PIN = A0;     // 电位器
const byte LED_PIN = 9;      // LED(PWM 脚)

void setup() {
  Serial.begin(9600);
  pinMode(LED_PIN, OUTPUT);
  // 模拟口 A0 用 analogRead 时不需要 pinMode,写了也不影响
}

void loop() {
  int raw  = analogRead(POT_PIN);              // 0–1023
  int duty = map(raw, 0, 1023, 0, 255);        // 量程换算:10 位 → 8 位
  duty     = constrain(duty, 0, 255);          // 保险:确保不越界

  analogWrite(LED_PIN, duty);

  Serial.print("原始值: ");
  Serial.print(raw);
  Serial.print("  →  PWM: ");
  Serial.println(duty);

  delay(50);                                   // 打印太快会刷屏
}

知识点:analogRead 的 10 位精度、map 映射、constrain 兜底、Serial.print 组合输出。

课堂扩展

  1. 把 LED_PIN 换成 D5 或 D6,直接用电位器调电机转速(记得先把四位拨码开关全部向上)。

  2. 观察"电机在小占空比时不转"的现象,加一行 if (duty > 0 && duty < 60) duty = 60; 处理死区。


案例 F:状态机(红绿灯自动切换)

功能:红 → 绿 → 黄 → 红……按固定时长自动切换,且不阻塞(期间还可以响应其他逻辑)。

接线

灯接到
红灯D8
绿灯D9
黄灯D10

思路:用一个变量记住"当前是第几步";用 millis() 判断时间到了没有,到了就切到下一步。用数组存每一步的时长和引脚,加一个灯只需改数组。

/* ===== 案例 F:红绿灯状态机(非阻塞) ===== */
const byte RED_PIN    = 8;
const byte GREEN_PIN  = 9;
const byte YELLOW_PIN = 10;

const byte pins[]   = {RED_PIN, GREEN_PIN, YELLOW_PIN};  // 0=红 1=绿 2=黄
const unsigned long durations[] = {5000, 5000, 2000};    // 每一步持续多少毫秒
const byte STEP_COUNT = 3;

byte         currentStep = 0;      // 当前走到第几步
unsigned long lastChange  = 0;     // 上一次切换的时刻

void setup() {
  Serial.begin(9600);
  for (int i = 0; i < STEP_COUNT; i++) {
    pinMode(pins[i], OUTPUT);
  }
  digitalWrite(pins[0], HIGH);     // 从红灯开始
  Serial.println("step 0: RED");
}

void loop() {
  // 时间到了就换下一步
  if (millis() - lastChange >= durations[currentStep]) {
    lastChange = millis();

    digitalWrite(pins[currentStep], LOW);        // 灭掉当前灯
    currentStep = (currentStep + 1) % STEP_COUNT;  // 0→1→2→0 循环
    digitalWrite(pins[currentStep], HIGH);       // 点亮下一盏

    Serial.print("step ");
    Serial.println(currentStep);
  }

  // 这里还可以继续写其他逻辑(读按键、读传感器……),不会被卡住
}

知识点:状态变量、millis() 定时、取余实现循环、并行数组(引脚数组 + 时长数组)的写法。

课堂扩展

  1. 把黄灯时长改成 500 ms,观察通行节奏变化。

  2. 加一个按键:按下时强制切到红灯(提示:在 loop 里读按键,命中就把 currentStep 设成 0 并刷新 lastChange)。


案例 G:光控灯(读光敏传感器)

功能:环境变暗自动开灯,变亮自动关灯;串口实时显示光照数值,方便标定阈值。

接线

元件接到
光敏传感器信号脚A0
LEDD9(PWM 脚)

思路:analogRead() 读光照值(0–1023),与阈值比较决定开关。第一步永远是"看清数据"——不同传感器暗/亮时的数值方向可能相反。

预览
源码

是 天黑

否 天亮

analogRead(A0)
得到光照值 light

light < 阈值 ?

digitalWrite(LED_PIN, HIGH)
开灯

digitalWrite(LED_PIN, LOW)
关灯

delay 100
稍等再测

graph TD
  A["analogRead(A0)<br/>得到光照值 light"] --> B{"light < 阈值 ?"}
  B -->|是 天黑| C["digitalWrite(LED_PIN, HIGH)<br/>开灯"]
  B -->|否 天亮| D["digitalWrite(LED_PIN, LOW)<br/>关灯"]
  C --> E[delay 100<br/>稍等再测]
  D --> E
  E --> A
/* ===== 案例 G:光控灯 ===== */
const byte LIGHT_PIN = A0;     // 光敏传感器
const byte LED_PIN   = 9;      // LED(PWM 脚)
const int  THRESHOLD = 500;    // 阈值:先用串口观察实际数值后再定

void setup() {
  Serial.begin(9600);
  pinMode(LED_PIN, OUTPUT);
  Serial.println("用手遮住传感器,观察数值变化……");
}

void loop() {
  int light = analogRead(LIGHT_PIN);   // 0–1023
  Serial.println(light);               // 标定时看这一行

  if (light < THRESHOLD) {             // 数值越小表示越暗(以你手上的传感器为准)
    digitalWrite(LED_PIN, HIGH);       // 天黑 → 开灯
  } else {
    digitalWrite(LED_PIN, LOW);        // 天亮 → 关灯
  }

  delay(100);
}

标定三步:① 上传后打开串口监视器;② 用手完全遮住传感器,记下数值 A;③ 用手电照射,记下数值 B;④ 阈值取 (A+B)/2。若发现"遮住时数值反而变大",把判断条件改成 light > THRESHOLD。

进阶一:亮度渐变(越暗灯越亮)

int duty = map(light, 0, 1023, 255, 0);   // 注意两端"反着写"
duty = constrain(duty, 0, 255);
analogWrite(LED_PIN, duty);

进阶二:加迟滞,防止临界闪烁(只开一个阈值时,光在临界点会让灯疯狂抖动)

bool lampOn = false;

if (!lampOn && light < 450) lampOn = true;    // 暗到 450 才开灯
if ( lampOn && light > 550) lampOn = false;   // 亮过 550 才关灯
digitalWrite(LED_PIN, lampOn);

知识点:analogRead、阈值判断、map 反向映射、迟滞(hysteresis)思想。

课堂扩展

  1. 让学生各自标定自己的传感器,比较阈值为何不同——引出"硬件差异需要软件标定"。

  2. 改成"光控风扇":亮度越低电机转得越快(提示:反向 map 后接到 D5)。


案例 H:超声波测距(HC-SR04)

功能:实时测量前方障碍物距离并通过串口显示;距离过近时自动停车。

接线

HC-SR04接到
VCC5V
GNDGND
TrigD8
EchoD2

(电机模式下 D4–D7 已被电机占用,Trig/Echo 要避开。)

思路:给 Trig 发一个 10 微秒的高脉冲,模块发射超声波;用 pulseIn() 量出 Echo 高电平持续了多少微秒,再按声速换算距离。

预览
源码

Trig 发 10 微秒高脉冲

模块发射超声波

Echo 变高,开始计时

声波碰到障碍物返回

Echo 变低,pulseIn 得到微秒数

距离 = 微秒数 × 0.034 ÷ 2

graph TD
  A["Trig 发 10 微秒高脉冲"] --> B[模块发射超声波]
  B --> C["Echo 变高,开始计时"]
  C --> D[声波碰到障碍物返回]
  D --> E["Echo 变低,pulseIn 得到微秒数"]
  E --> F["距离 = 微秒数 × 0.034 ÷ 2"]
/* ===== 案例 H:超声波测距 HC-SR04 ===== */
const byte TRIG_PIN = 8;
const byte ECHO_PIN = 2;

long  duration;      // 回波持续时长(微秒)
float distance;      // 距离(厘米)

void setup() {
  Serial.begin(9600);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
}

void loop() {
  // 1) 发一个 10 微秒的触发脉冲
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  // 2) 量回波持续多久(微秒);30 ms 收不到就超时,返回 0
  duration = pulseIn(ECHO_PIN, HIGH, 30000L);

  // 3) 换算:声速约 340 m/s = 0.034 cm/微秒,声波走的是往返,所以 ÷ 2
  distance = duration * 0.034 / 2;

  if (duration == 0) {
    Serial.println("超出量程 / 未收到回波");
  } else {
    Serial.print("距离: ");
    Serial.print(distance);
    Serial.println(" cm");
  }

  delay(100);   // 两次测量间隔建议 60 ms 以上
}

应用:距离太近就停车(把下面这段接到电机控制里,需要先拨码到电机模式)

if (distance > 0 && distance < 15) {
  analogWrite(5, 0);   analogWrite(6, 0);      // 太近:停
} else if (distance < 40) {
  analogWrite(5, 100); analogWrite(6, 100);    // 中等:减速
} else {
  analogWrite(5, 200); analogWrite(6, 200);    // 安全:正常前进
}

知识点:delayMicroseconds(微秒级延时)、pulseIn(量脉冲宽度)、单位换算、long 类型、超时保护。

坑:pulseIn() 是阻塞的——等回波的这段时间里程序什么也干不了。想同时做别的事,需要用中断或专用库。

课堂扩展

  1. 把测距结果做成"倒车雷达":距离越近,蜂鸣器响得越急促(提示:用 tone())。

  2. 讨论:为什么测斜面、柔软物体时数据会跳变?(声波反射角度问题)


案例 I:双路寻线小车(模拟量 + PWM)

功能:车底左右两个反射式光电传感器,让小车沿黑色轨迹自动行驶。

接线

元件接到
左传感器信号A0
右传感器信号A1
电机 M1D4(方向)/ D5(速度)
电机 M2D7(方向)/ D6(速度)

⚠️ 必须把四位拨码开关全部向上(ON),进入电机模式,D4–D7 才会接到电机驱动。

思路:黑线反射弱 → 读数越大(你的传感器若相反,把 l - r 改成 r - l)。用左右读数之差当作"偏离量",哪边压线就让哪边轮子加速,把车拉回线上。

预览
源码

是

error > 0(左压线)

error < 0(右压线)

读左右传感器
l = analogRead(A0)
r = analogRead(A1)

算偏差 error = l - r

error 接近 0 ?

左右同速 → 直行

左轮加速、右轮减速 → 往右修正

右轮加速、左轮减速 → 往左修正

constrain 限幅 0–255
analogWrite 输出到 D5 / D6

graph TD
  A["读左右传感器<br/>l = analogRead(A0)<br/>r = analogRead(A1)"] --> B["算偏差 error = l - r"]
  B --> C{"error 接近 0 ?"}
  C -->|是| D["左右同速 → 直行"]
  C -->|"error > 0(左压线)"| E["左轮加速、右轮减速 → 往右修正"]
  C -->|"error < 0(右压线)"| F["右轮加速、左轮减速 → 往左修正"]
  D --> G["constrain 限幅 0–255<br/>analogWrite 输出到 D5 / D6"]
  E --> G
  F --> G
  G --> A

版本一:阈值法(最简单,先跑通这个)

/* ===== 案例 I-1:双路寻线(阈值法) ===== */
const byte L_SENSOR = A0;      // 左传感器
const byte R_SENSOR = A1;      // 右传感器

const byte M1_DIR = 4;         // M1 方向
const byte M1_PWM = 5;         // M1 速度(PWM)
const byte M2_DIR = 7;         // M2 方向
const byte M2_PWM = 6;         // M2 速度(PWM)

const int LINE = 600;          // "看到黑线"的阈值,先打印观察后确定

void setup() {
  Serial.begin(9600);
  pinMode(M1_DIR, OUTPUT);  pinMode(M1_PWM, OUTPUT);
  pinMode(M2_DIR, OUTPUT);  pinMode(M2_PWM, OUTPUT);

  digitalWrite(M1_DIR, HIGH);       // 都设为正转
  digitalWrite(M2_DIR, HIGH);
  analogWrite(M1_PWM, 0);           // 开局先停
  analogWrite(M2_PWM, 0);
}

void loop() {
  int l = analogRead(L_SENSOR);
  int r = analogRead(R_SENSOR);

  Serial.print("L="); Serial.print(l);
  Serial.print("  R="); Serial.println(r);

  if (l > LINE && r > LINE) {          // 两边都看到线:直行(或十字路口)
    analogWrite(M1_PWM, 180);
    analogWrite(M2_PWM, 180);
  } else if (l > LINE) {               // 左边压线:车偏右了,左转回来
    analogWrite(M1_PWM, 80);
    analogWrite(M2_PWM, 200);
  } else if (r > LINE) {               // 右边压线:车偏左了,右转回来
    analogWrite(M1_PWM, 200);
    analogWrite(M2_PWM, 80);
  } else {                             // 两边都看不到:冲出线了,停车最安全
    analogWrite(M1_PWM, 0);
    analogWrite(M2_PWM, 0);
  }

  delay(20);
}

版本二:比例控制(差值越大修正越猛,走得更顺)

只需把 loop() 换成下面这段(引脚常量沿用版本一):

/* ===== 案例 I-2:双路寻线(比例法)—— 只替换 loop 部分 ===== */
const int BASE      = 150;   // 基础速度
const int KP        = 1;     // 比例系数:抖得厉害调小,转不过弯调大
const int MIN_SPEED = 60;    // 死区:低于这个值电机不转,给个最小值

void loop() {
  int l = analogRead(L_SENSOR);
  int r = analogRead(R_SENSOR);
  int error = l - r;                      // 偏差:正 = 左边压线,负 = 右边压线

  int leftSpeed  = BASE + KP * error;     // 左压线 → 左轮快、右轮慢
  int rightSpeed = BASE - KP * error;

  if (leftSpeed  > 0 && leftSpeed  < MIN_SPEED) leftSpeed  = MIN_SPEED;  // 处理死区
  if (rightSpeed > 0 && rightSpeed < MIN_SPEED) rightSpeed = MIN_SPEED;

  leftSpeed  = constrain(leftSpeed,  0, 255);   // 限幅
  rightSpeed = constrain(rightSpeed, 0, 255);

  analogWrite(M1_PWM, leftSpeed);
  analogWrite(M2_PWM, rightSpeed);
}

调试顺序(照做少走弯路)

  1. 先只打印 l、r、error,用手在传感器下方晃动,确认数值变化方向与预期一致;

  2. 把车抬起来(轮子悬空),看两轮转速是否随偏差反向变化;

  3. 落地先给 BASE 一个很小的值(如 100),能慢慢跟线走再提速;

  4. 抖得厉害 → 调小 KP;转不过弯 → 调大 KP 或提高 BASE;

  5. 某一侧总是跑偏 → 给慢的一侧加固定补偿量(例如 leftSpeed + 10)。

关于 PID(只提一句,本手册不展开):版本二其实就是 P(比例)控制。加 D(微分)可用"偏差的变化速度"提前刹车、抑制过冲;加 I(积分)可消除长期的微小偏移——这就是常说的 PID。先跑通 P,等车能沿线走了,再自己查资料加 D。

知识点:多路模拟量读取、偏差(error)思想、constrain 限幅、死区处理、比例控制雏形。

课堂扩展

  1. 举办"沿线竞速":固定赛道,比谁的车不脱线且用时最短——自然引出调 KP 与 BASE 的取舍。

  2. 加第三个传感器(A2)做"三路寻线",处理十字路口。


15. 调试:串口打印大法

15.1 三行固定套路

void setup() { Serial.begin(9600); }        // ① 初始化

Serial.print("x = ");                       // ② 打标签
Serial.println(x);                          // ③ 打数值(换行版)

串口监视器的波特率必须选 9600(与 Serial.begin(9600) 一致),否则全是乱码。

15.2 调试句式模板

Serial.print("[进入函数] ");                    // 看程序跑到哪一步了
Serial.print("raw="); Serial.print(raw);       // 看变量原始值
Serial.print(" duty="); Serial.println(duty);  // 看换算结果

15.3 找不到 bug 时怎么办

排查流程

预览
源码

没通过

通过了

不能

能

不对

对

不能

能

现象不对

编译通过了吗?

看 IDE 报错行号
对照 16.1 表修改

setup 里能打印出
hello 吗?

确认板卡 / 端口 / 波特率

变量的值对吗?
用 Serial.println 打印

一路往源头打印
找到第一个出错的位置

单独控制这个引脚的
最小程序能跑吗?

问题在硬件 / 接线 / 拨码开关

问题在业务逻辑
检查判断与循环条件

graph TD
  A[现象不对] --> B{编译通过了吗?}
  B -->|没通过| C["看 IDE 报错行号<br/>对照 16.1 表修改"]
  C --> A
  B -->|通过了| D{"setup 里能打印出<br/>hello 吗?"}
  D -->|不能| E[确认板卡 / 端口 / 波特率]
  E --> A
  D -->|能| F{"变量的值对吗?<br/>用 Serial.println 打印"}
  F -->|不对| G["一路往源头打印<br/>找到第一个出错的位置"]
  G --> A
  F -->|对| H{"单独控制这个引脚的<br/>最小程序能跑吗?"}
  H -->|不能| I[问题在硬件 / 接线 / 拨码开关]
  H -->|能| J[问题在业务逻辑<br/>检查判断与循环条件]

四步口诀

① 先确认程序真的烧进去了(加一句 Serial.println("hello") 在 setup 里)
② 在可疑位置前后各打一句日志,缩小范围
③ 怀疑硬件还是软件?写个最小程序只操作这一个引脚
④ 变量值不对就一路 print 到源头

16. 错误对照表

16.1 编译错误(IDE 会直接告诉你)

报错(英文)含义处理
expected ';' before ...上一行漏了分号补 ;
'xxx' was not declared in this scope变量没定义或拼错检查拼写、作用域
too few arguments to function参数个数不对对照手册补参数
redefinition of 'xxx'重复定义删掉多余的那份
invalid conversion from 'int' to 'byte'类型不匹配加强制转换 (byte)
stray '&#x27; in program混入了中文标点换成英文标点
expected initializer before 'if'上一个函数缺 }检查花括号配对
expected ')' before ';'if/for 的括号没配对检查 ( )

16.2 逻辑错误(编译能通过,但现象不对)

现象最可能的原因
判断永远成立把 == 写成了 =
循环次数多一次 / 少一次< 和 <= 没数清
程序像死机while 里忘了更新条件变量
LEDs/电机完全没反应引脚没 pinMode(OUTPUT)
按键反应很乱没去抖 / 忘了 INPUT_PULLUP
analogWrite 只有亮灭没有渐变用了非 PWM 引脚(如 D13)
串口全是乱码波特率不一致
变量值莫名其妙被改数组越界踩到别的变量
时间久了行为异常millis() 相关变量用了 int 而非 unsigned long

17. 练习题

由易到难,不提供答案,请对照本手册自己写。

入门(掌握结构)

  1. 让板载 LED 按"亮 0.2 秒、灭 0.2 秒"闪烁。

  2. 用 for 循环让 LED 闪 5 下后停下。

  3. 用串口每秒打印一次 Hello ArduinoPlus。

基础(掌握 IO)

  1. 用 D13 和 D9 两个 LED,做一个"交替亮"的效果。

  2. 用 INPUT_PULLUP 读按键,按下时打印一条信息。

  3. 把 analogRead(A0) 的值(0–1023)打印出来,串口绘图器里看曲线。

进阶(掌握控制结构)

  1. 用 if - else if 实现三档:读到数值 < 300 打印"暗",300–700 打印"一般",> 700 打印"亮"。

  2. 用 map + constrain 把 0–1023 映射成 0–255,输出 PWM 调光。

  3. 用 while 实现"按住按键才亮,松开就灭"。

  4. 用 switch 写一个串口菜单:输入 1/2/3 切换三档亮度。

综合(函数 + millis + 电机)

  1. 写一个 void motor1(int speed) 函数,实现 -255~+255 的速度控制,自己定义正负对应的转向。

  2. 用 millis() 做成"闪灯 + 每 2 秒打印一次 A0"的双任务。

  3. 用 for 实现电机软启动(60 → 220),观察与直接给满速的区别。

  4. 写一个 void moveOne(int left, int right, unsigned long ms),让车按参数动一段再停。

  5. 挑战:用状态机写一个"前进 3 秒 → 右转 1 秒 → 前进 3 秒 → 停"的循环动作序列。


18. 附录 A:一页纸语法卡

┌────────────────────── ArduinoPlus 语法卡 ──────────────────────┐
│ 骨架:void setup(){ } 初始化一次  /  void loop(){ } 反复执行     │
│                                                                 │
│ 引脚:pinMode(p, OUTPUT|INPUT|INPUT_PULLUP)                     │
│       digitalWrite(p, HIGH|LOW)      digitalRead(p)             │
│       analogWrite(p, 0-255)  [PWM: 3,5,6,9,10,11]               │
│       analogRead(A0-A5) → 0-1023                                │
│                                                                 │
│ if      if (条件) { } else if (条件) { } else { }               │
│ switch  switch(x){ case 1: ...; break; default: ...; }          │
│ for     for (int i=0; i<N; i++) { }          计数               │
│ while   while (条件) { }                      等待/未知次数      │
│ do      do { } while (条件);                  至少执行一次       │
│ 跳出    break(跳出循环) / continue(跳过本次)                │
│                                                                 │
│ delay   delay(1000);              阻塞 1 秒                     │
│ millis  if (millis()-last >= 间隔) { last = millis(); ... }     │
│         ★ 变量必须用 unsigned long                              │
│                                                                 │
│ 函数    int f(int a){ return a; }  /  void g(byte p){ }         │
│ 映射    map(v,0,1023,0,255)   限幅 constrain(v,0,255)           │
│ 串口    Serial.begin(9600)  print / println / available / read  │
│                                                                 │
│ 电机 M1:D4=方向(HIGH/LOW)   D5=转速(analogWrite 0-255)         │
│ 电机 M2:D7=方向(HIGH/LOW)   D6=转速(analogWrite 0-255)         │
│ ★ 用电机 = 四位拨码全部向上;当 IO = 四位全部向下               │
│                                                                 │
│ 调试三宝:Serial.println(标签) + Serial.println(变量值)          │
│           + 先确认 setup 里能不能打印出 hello                    │
│                                                                 │
│ 头号错误:if(x == 5) 别写成 if(x = 5)                           │
│           5/2 = 2,想要 2.5 要写 5.0/2                          │
└─────────────────────────────────────────────────────────────────┘

19. 附录 B:从 Python 迁移对照表

给学过 Python 的同学:语法几乎是一比一的,注意两点差别花括号/缩进和行尾分号。

概念PythonArduino C/C++  
代码块缩进{ } 花括号  
语句结束换行; 分号  
注释#// 或 /* */  
变量声明x = 5(无需声明类型)int x = 5;(必须声明类型)  
条件if x > 5:if (x > 5) {  
否则elif / else:else if ( ) { / else {  
计数循环for i in range(10):for (int i = 0; i < 10; i++) {  
条件循环while x < 5:while (x < 5) {  
自增i += 1i++; 或 i += 1;  
函数def add(a, b): return a+bint add(int a, int b) { return a+b; }  
打印print(x)Serial.println(x);  
等待time.sleep(1)delay(1000); 注意单位不同  
整除5 // 2 → 25 / 2 → 2(整数除法默认整除)  
真/假True / Falsetrue / false  
与或非and or not&& ` !`
数组list = [1,2,3]int arr[] = {1,2,3};  

最容易犯的三个迁移错误

① 把 for i in range(10): 直接写成 for (10)      → 记住三段式写法
② 忘了 int 声明:写成 x = 5;                    → 编译报错
③ print 保留了 Python 习惯                      → 要用 Serial.println

适用主板:ArduinoPlus(基于 Arduino UNO 二次开发,板载双路电机驱动)

硬件开发指南 / ArduinoPlus开发板-编程手册 0 字 0 行 jimmy
2026-09-14T17:14:33.378657153Z 2026-09-14T18:31:25.937937709Z