ArduinoPlus开发板-编程手册 ArduinoPlus = 基于 Arduino UNO 二次开发的教学主板,板载集成了双路直流电机驱动 。
本手册可用于教师备课 ,也可用于学生自学使用 :语法点给出结构与对照,案例章提供完整、可直接复制上传的程序 。
适用对象:中小学信息技术 / 通用技术、高校低年级嵌入式入门、创客社团等。
目录 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 输出 analogWrite 3、5、6、9、10、11(电机模式下 5、6 归电机 ) 外部中断 2、3 模拟输入 analogRead A0 – A5 板载 LED 13
1.3 四位拨码开关ArduinoPlus 板上有一个 4 位拨码开关 ,决定 D4 D5 D6 / D7 这四根脚"通向哪里"。
开关状态 引脚去向 使用场景 4 位全部向上(ON) 接通板载电机驱动 使用电机时必须全部拨上去 4 位全部向下(OFF) 接通排针 当普通 IO 用(读写传感器、点灯等) 有的上、有的下 —— ⛔ 禁止 ,信号会被切断或冲突
拨码开关选择流程
使用口诀
用电机 → 四个全往上
当 IO → 四个全往下
切开关 → 一定先断电
⚠️ 在电机模式下,不要 再写 digitalRead(D4) 之类的代码去读这四根脚,读到的已经不是外部信号了。
1.4 安全提醒切换拨码开关前先断电 ;
下载程序时留意 D0 / D1 不要接着会发数据的模块 ;
电机必须由板上/外部电机电源 供电,不要指望 USB 那点电流;
程序里养成习惯:setup() 开头先把电机速度置 0。
2. 十分钟跑起来 2.1 下载与安装 Arduino IDE官方下载地址 :https://www.arduino.cc/en/software
系统 选哪一项 说明 Windows Windows Win 10 or newer (64-bit) 常规安装包,一路"下一步"即可 Windows Windows ZIP file 免安装版,解压即用,适合机房统一部署 macOS macOS 拖入 Applications Linux Linux AppImage / 64-bit ZIP 加执行权限后运行 临时用 / 机房 Arduino Cloud Editor 网页版,联网即可写程序,但不能离线
2.2 界面速览(5 分钟认识界面)
┌──────────────────────────────────────────────────────┐
│ 文件 编辑 项目 工具 帮助 [板卡 ▾] [端口 ▾] │ ← 菜单栏 + 板卡/端口
├────┬─────────────────────────────────────────────────┤
│ 侧 │ ✓ 验证 → 上传 ⌨ 串口监视器 │ ← 顶部工具栏
│ 边 │─────────────────────────────────────────────────│
│ 栏 │ │
│ 📁 │ 代码编辑区(有行号、自动补全) │
│ 📚 │ │
│ 🔍 │ │
├────┴─────────────────────────────────────────────────┤
│ 输出区:编译结果 / 上传进度 / 报错信息(红色) │
└──────────────────────────────────────────────────────┘
区域 作用 顶部工具栏 ✓ 验证 (编译):只检查语法,不上传顶部工具栏 → 上传 :编译并烧写到板子顶部工具栏 放大镜 打开串口监视器 左侧栏(书本图标) 库管理器 :搜索安装第三方库左侧栏(芯片图标) 开发板管理器 :安装其他型号的支持包中间代码区 写程序的地方,报错行会标红 底部输出区 看编译是否通过、报错在第几行
两个必做设置
工具 → 开发板 → Arduino AVR Boards → Arduino Uno
工具 → 端口 → COMx(Windows 是 COM3、COM4……;拔掉 USB 再插一次,看多了哪个口)
想换中文界面:文件 → 首选项 → Language → 简体中文,重启 IDE 生效。
程序默认保存在 文档\Arduino 目录下(sketchbook 路径),可在"首选项"里查看或修改。
2.3 外部库导入(简单了解)库 就是别人写好的功能包,装好后用 #include <库名.h> 引入,可以直接调用现成函数(比如舵机库 Servo、超声波库 NewPing)。
方式一:库管理器(推荐,最简单)
点左侧书本图标 (或 Ctrl + Shift + I)
搜索关键词,例如 Servo
点 INSTALL ,装完即可
方式二:导入 .ZIP 库
先把下载的库压缩包放在电脑里(不要解压 )
项目 → 加载库 → 添加 .ZIP 库…,选中该 zip
装完重启 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 上手流程流程图
常用快捷键
操作 快捷键 作用 验证(编译) 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 结束后【反复执行】,写主逻辑
}
执行顺序图
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) `A B` A、B 任意一个 成立 !非(not) !AA 不成立 时为真
真值表 (背下来比记文字快)
A B A && B`A B` !Afalse false false false true false true false true true true false false true false true true true true false
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++后置自增 先用旧值,再加 1 int a = i++; → a = 5 ,i 变 6++i前置自增 先加 1,再用新值 int a = ++i; → a = 6 ,i 变 6i--后置自减 先用旧值,再减 1 int 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(新值)
记住三条就够
单独写成一句时(比如 for 里的 i++),i++ 和 ++i 效果完全一样 ,随便用哪个。
只有把它塞进别的表达式里 (如 int a = i++)才有区别——初学阶段建议避开这种写法 ,拆成两行写。
一条语句里不要对同一个变量用两次 ++(如 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 (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 (初始化; 循环条件; 每次结束后更新) {
// 循环体
}
执行顺序流程图
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 写法(阻塞)
millis 写法(并行)
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 / LOW digitalWrite(13, HIGH);digitalRead(pin)读取,返回 HIGH / LOW if (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) → 127constrain(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 → 决定转多快
一个电机怎么转起来
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);
}
③ 小车基本动作
动作 M1 M2 前进 正转 v 正转 v 后退 反转 v 反转 v 左转 反转 v 正转 v 右转 正转 v 反转 v 停止 0 0
// 用参数化函数表达上面这张表(留给你自己实现 ✍️)
// 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 这一次"的状态比较、布尔取反 !、去抖。
课堂扩展
把 delay(200) 改成 50 和 500,体会去抖时长对"按一次是否被识别成多次"的影响。
改成"按住才亮、松开就灭":把整个 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 1 0 0+5 5 否 +5 2 5 5+5 10 否 +5 … … … … 否 +5 51 250 255 255 是(≥255) −5 52 255 250 250 否 −5 … … … … 否 −5 102 5 0 0 是(≤0) +5
为什么这样写是对的
constrain(brightness, 0, 255) 是安全阀 :无论 stepSize 取多少(哪怕 7、13 这种不能整除 255 的值),analogWrite() 都不会收到越界的数,因此不会卡在边界。
翻转条件必须带等号(>= 255、<= 0)。若写成 == 255,步长不能整除 255 时会漏掉,变成"只增不减"。
stepSize = -stepSize; 一行实现"到顶往回、到底再往前",不需要两个 if。注意它必须声明成 int——byte 是无符号类型,存不了负数。
呼吸周期 :255 ÷ 5 = 51 步,每步 delay(20) → 单程约 1020 ms,一个完整呼吸约 2 秒 。想变慢就调小 stepSize 或调大 delay。
课堂扩展
把 stepSize 改成 1 和 15,观察渐变细腻度与速度的取舍。
加一个电位器,用 analogRead 的值控制 stepSize,做成"可调速呼吸灯"。
案例 C:多个 LED 流水灯功能 :4 个 LED 依次点亮再熄灭,像跑马灯一样循环。
接线
LED 接到 LED1 D8 LED2 D9 LED3 D10 LED4 D11
(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)、批量初始化。
课堂扩展
加一段"倒着回来"的 for,做成来回流动:for (int i = LED_COUNT - 1; i >= 0; i--)。
用 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 ,行结束符选"没有行结束符"(否则会多收到换行)。
课堂扩展
把 case 里的动作换成电机控制(f 前进 b 后退 s 停止),做成串口遥控小车。
加一个 case 'v',实现"先发 v 再发数字"调节速度(提示:用 Serial.parseInt())。
案例 E:电位器调节亮度 / 转速功能 :旋转电位器,LED 亮度(或电机转速)随之平滑变化,并在串口显示数值。
接线 元件 接到 电位器中间脚 A0 电位器两端 5V 与 GND LED D9(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 组合输出。
课堂扩展
把 LED_PIN 换成 D5 或 D6,直接用电位器调电机转速(记得先把四位拨码开关全部向上 )。
观察"电机在小占空比时不转"的现象,加一行 if (duty > 0 && duty < 60) duty = 60; 处理死区。
案例 F:状态机(红绿灯自动切换)功能 :红 → 绿 → 黄 → 红……按固定时长自动切换,且不阻塞 (期间还可以响应其他逻辑)。
接线
思路 :用一个变量记住"当前是第几步";用 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() 定时、取余实现循环、并行数组(引脚数组 + 时长数组)的写法。
课堂扩展
把黄灯时长改成 500 ms,观察通行节奏变化。
加一个按键:按下时强制切到红灯(提示:在 loop 里读按键,命中就把 currentStep 设成 0 并刷新 lastChange)。
案例 G:光控灯(读光敏传感器)功能 :环境变暗自动开灯,变亮自动关灯;串口实时显示光照数值,方便标定阈值。
接线
元件 接到 光敏传感器信号脚 A0 LED D9(PWM 脚)
思路 :analogRead() 读光照值(0–1023),与阈值比较决定开关。第一步永远是"看清数据" ——不同传感器暗/亮时的数值方向可能相反。
/* ===== 案例 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)思想。
课堂扩展
让学生各自标定自己的传感器,比较阈值为何不同——引出"硬件差异需要软件标定"。
改成"光控风扇":亮度越低电机转得越快(提示:反向 map 后接到 D5)。
案例 H:超声波测距(HC-SR04)功能 :实时测量前方障碍物距离并通过串口显示;距离过近时自动停车。
接线
HC-SR04 接到 VCC 5V GND GND Trig D8 Echo D2
(电机模式下 D4–D7 已被电机占用,Trig/Echo 要避开。)
思路 :给 Trig 发一个 10 微秒的高脉冲,模块发射超声波;用 pulseIn() 量出 Echo 高电平持续了多少微秒,再按声速换算距离。
/* ===== 案例 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() 是阻塞 的——等回波的这段时间里程序什么也干不了。想同时做别的事,需要用中断或专用库。
课堂扩展
把测距结果做成"倒车雷达":距离越近,蜂鸣器响得越急促(提示:用 tone())。
讨论:为什么测斜面、柔软物体时数据会跳变?(声波反射角度问题)
案例 I:双路寻线小车(模拟量 + PWM)功能 :车底左右两个反射式光电传感器,让小车沿黑色轨迹自动行驶。
接线
元件 接到 左传感器信号 A0 右传感器信号 A1 电机 M1 D4(方向)/ D5(速度) 电机 M2 D7(方向)/ D6(速度)
⚠️ 必须把四位拨码开关全部向上(ON) ,进入电机模式,D4–D7 才会接到电机驱动。
思路 :黑线反射弱 → 读数越大 (你的传感器若相反,把 l - r 改成 r - l)。用左右读数之差当作"偏离量",哪边压线就让哪边轮子加速,把车拉回线上。
版本一:阈值法(最简单,先跑通这个)
/* ===== 案例 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);
}
调试顺序(照做少走弯路)
先只打印 l、r、error,用手在传感器下方晃动,确认数值变化方向与预期一致;
把车抬起来(轮子悬空),看两轮转速是否随偏差反向 变化;
落地先给 BASE 一个很小的值(如 100),能慢慢跟线走再提速;
抖得厉害 → 调小 KP;转不过弯 → 调大 KP 或提高 BASE;
某一侧总是跑偏 → 给慢的一侧加固定补偿量(例如 leftSpeed + 10)。
关于 PID(只提一句,本手册不展开) :版本二其实就是 P(比例) 控制。加 D(微分) 可用"偏差的变化速度"提前刹车、抑制过冲;加 I(积分) 可消除长期的微小偏移——这就是常说的 PID。先跑通 P,等车能沿线走了,再自己查资料加 D。
知识点 :多路模拟量读取、偏差(error)思想、constrain 限幅、死区处理、比例控制雏形。
课堂扩展
举办"沿线竞速":固定赛道,比谁的车不脱线且用时最短——自然引出调 KP 与 BASE 的取舍。
加第三个传感器(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 时怎么办排查流程
四步口诀
① 先确认程序真的烧进去了(加一句 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 '' 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. 练习题 由易到难,不提供答案 ,请对照本手册自己写。
入门(掌握结构)
让板载 LED 按"亮 0.2 秒、灭 0.2 秒"闪烁。
用 for 循环让 LED 闪 5 下后停下。
用串口每秒打印一次 Hello ArduinoPlus。
基础(掌握 IO)
用 D13 和 D9 两个 LED,做一个"交替亮"的效果。
用 INPUT_PULLUP 读按键,按下时打印一条信息。
把 analogRead(A0) 的值(0–1023)打印出来,串口绘图器里看曲线。
进阶(掌握控制结构)
用 if - else if 实现三档:读到数值 < 300 打印"暗",300–700 打印"一般",> 700 打印"亮"。
用 map + constrain 把 0–1023 映射成 0–255,输出 PWM 调光。
用 while 实现"按住按键才亮,松开就灭"。
用 switch 写一个串口菜单:输入 1/2/3 切换三档亮度。
综合(函数 + millis + 电机)
写一个 void motor1(int speed) 函数,实现 -255~+255 的速度控制,自己定义正负对应的转向。
用 millis() 做成"闪灯 + 每 2 秒打印一次 A0"的双任务。
用 for 实现电机软启动(60 → 220),观察与直接给满速的区别。
写一个 void moveOne(int left, int right, unsigned long ms),让车按参数动一段再停。
挑战:用状态机写一个"前进 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 的同学:语法几乎是一比一的,注意两点差别花括号/缩进 和行尾分号 。
概念 Python Arduino 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 二次开发,板载双路电机驱动)