太阳能追光路灯实现左右追光

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

315.10、太阳能追光路灯实现左右追光

#include <Servo.h>                                // 引入 Arduino Servo 库,使草图可以创建并控制舵机对象
#define right 15                                  // 右侧光敏模块的 AO 接 Uno 的 A1;在本板核心中 A1 的数字别名是 15
#define left 16                                   // 左侧光敏模块的 AO 接 Uno 的 A2;在本板核心中 A2 的数字别名是 16
#define up 17                                     // 上侧光敏模块的 AO 接 Uno 的 A3;本节只采样,尚未参与俯仰判断
#define down 14                                   // 下侧光敏模块的 AO 接 Uno 的 A0;本节只采样,尚未参与俯仰判断
#define right_left_servopin 9                     // 左右旋转舵机的信号线接数字引脚 D9
#define up_down_servopin 10                       // 上下俯仰舵机的信号线接数字引脚 D10
Servo right_left_servo;                           // 创建控制左右旋转轴的 Servo 对象
Servo up_down_servo;                              // 创建控制上下俯仰轴的 Servo 对象
int right_size=0;                                 // 保存右侧光敏模块本轮得到的 ADC 数值
int left_size=0;                                  // 保存左侧光敏模块本轮得到的 ADC 数值
int up_size=0;                                    // 保存上侧光敏模块本轮得到的 ADC 数值
int down_size=0;                                  // 保存下侧光敏模块本轮得到的 ADC 数值
int angle1=90;                                    // 左右轴目标角度初值为 90°,即本项目校准后的中间位置
int range1=180;                                   // 保存左右轴名义范围 180°;视频最终代码中该变量尚未被实际引用
int angle2=0;                                     // 上下轴目标角度初值为 0°,即本项目约定的朝上位置
int range2=160;                                   // 保存上下轴计划上限 160°;本节尚未编写上下追光判断
void setup() {                                    // Arduino Uno 上电或复位后执行一次初始化
  pinMode(right,INPUT);                           // 把右侧模块对应的 A1 明确设置为输入
  pinMode(left,INPUT);                            // 把左侧模块对应的 A2 明确设置为输入
  pinMode(up,INPUT);                              // 把上侧模块对应的 A3 明确设置为输入
  pinMode(down,INPUT);                            // 把下侧模块对应的 A0 明确设置为输入
  right_left_servo.attach(right_left_servopin);   // 将左右轴舵机对象关联到 D9 信号引脚
  up_down_servo.attach(up_down_servopin);         // 将上下轴舵机对象关联到 D10 信号引脚
  right_left_servo.write(angle1);                 // 初始化时命令左右轴转到 angle1 所保存的 90°
  up_down_servo.write(angle2);                    // 初始化时命令上下轴转到 angle2 所保存的 0°
}                                                 // setup() 初始化结束
void loop() {                                     // 主循环会不断重复采样、判断和控制
  right_size=analogRead(right);                   // 读取右侧 A1 的 10 位 ADC 数值
  left_size=analogRead(left);                     // 读取左侧 A2 的 10 位 ADC 数值
  up_size=analogRead(up);                         // 读取上侧 A3 的 ADC 数值,为下一节上下追光预留
  down_size=analogRead(down);                     // 读取下侧 A0 的 ADC 数值,为下一节上下追光预留
  if(right_size-left_size > 150)                  // 若右侧读数比左侧大 150 以上,说明左侧明显更亮
  {                                               // 进入向左调整的分支
    angle1=angle1 - 10;                           // 把左右轴目标角度减小 10°
    angle1= constrain(angle1,0,180);              // 将计算结果限制在 0°~180°,并写回 angle1
    right_left_servo.write(angle1);               // 向左右轴舵机发送更新后的目标角度
  }else                                           // 若左右差值没有满足本次向左调整条件
  {                                               // 进入保持分支
    right_left_servo.write(angle1);               // 再次发送当前目标角度,使左右轴保持原位置
  }                                               // 第一个方向判断结束
  if(left_size-right_size > 150)                  // 若左侧读数比右侧大 150 以上,说明右侧明显更亮
  {                                               // 进入向右调整的分支
    angle1=angle1 + 10;                           // 把左右轴目标角度增加 10°
    angle1= constrain(angle1,0,180);              // 将计算结果限制在 0°~180°,并写回 angle1
    right_left_servo.write(angle1);               // 向左右轴舵机发送更新后的目标角度
  }else                                           // 若左右差值没有满足本次向右调整条件
  {                                               // 进入保持分支
    right_left_servo.write(angle1);               // 再次发送当前目标角度,使左右轴保持原位置
  }                                               // 第二个方向判断结束
  delay(500);                                     // 阻塞等待 500 ms,再开始下一轮采样和判断
}                                                 // 本轮 loop() 结束,随后自动从 loop() 开头继续

一、太阳能追光路灯编程控制

上一节完成机械组装后,本节把第 313 节的四路 ADC 采样框架与前面课程的双舵机控制代码合并,先实现水平方向的左右追光。视频中最终完成编译、上传并用手电筒验证的就是页首草图;它已经读取上、下两路传感器并初始化俯仰舵机,但还没有根据 up_size、down_size 控制上下转动。

课程配套最终 6.ino 在本节代码后面还有上下追光判断,那是后续课程才添加的内容,因此没有提前放进页首源码。页首只去除了教师原注释并增加逐行解释,变量名、数值、判断顺序和两个独立的 if...else 块均按视频最终上传状态保留,没有擅自优化。

本节程序形成的闭环可以先概括为:

左、右光敏模块的 AO
          ↓ analogRead()
比较 right_size 与 left_size 的差值
          ↓ 差值超过 150 才调整
angle1 每次增加或减少 10°
          ↓ constrain() 限制数值范围
D9 上的左右轴舵机改变太阳能板朝向
          ↓
传感器在新朝向下再次采样

1.1 AD 采样

1.1.1 从已有示例集成本节草图

视频开始时,教师分别打开了前面写过的 ADC 采样草图和舵机示例,并依次移植:

  1. #include <Servo.h>;
  2. 两个 Servo 对象及其 attach() 初始化;
  3. 四只光敏模块的引脚、采样变量和 analogRead();
  4. 两只舵机的目标角度变量及初始 write();
  5. 左右差值判断、角度增减、constrain() 和 500 ms 延时。

这不是把两个程序机械拼在一起:复制后还需要把单舵机示例中的 myservo、pos 等占位名称改成当前项目的 right_left_servo、up_down_servo、angle1 和 angle2。视频编辑过程中曾短暂出现名称未改、分号未写、四路都读取 right 等中间状态;这些都没有被当作最终源码收录。

教师还删除了原单路采样代码中的 Serial.begin() 和 Serial.println(),所以本节运行时不会在串口监视器打印四路数值。程序会读取 ADC,但没有把结果发送给电脑;调试时若要知道具体数值,需要另写串口输出代码,不能误以为 analogRead() 会自动显示结果。

1.1.2 四路采样的含义

每轮 loop() 都执行四次 analogRead():

变量物理方向Uno 插孔视频本节是否参与动作判断
right_size右A1是,与左侧比较
left_size左A2是,与右侧比较
up_size上A3否,只采样
down_size下A0否,只采样

Uno R3 的 ADC 默认返回 0~1023 的 10 位结果。变量名中的 _size 只是教师采用的名称,保存的不是光敏电阻尺寸,也不是欧姆值或 lux,而是模块 AO 电压经 ADC 转换后的数字码。

第 313 节已用本项目模块实测:遮挡后读数约升到 500~600,强光近照时可降到 20~30。因此,本节特定模块和接法表现为越亮读数越小,越暗读数越大。这不是所有光敏模块都必然具有的极性;换模块或改变分压接法后,应先重新做遮挡/照射测试。

1.2 定义光敏电阻引脚

1.2.1 实际定义与 Uno 模拟口名称

原始笔记所说的“四个引脚都定义为 16 号”不正确。视频最终定义为:

宏名源码数值Uno R3 等价名称接线图方向
down14A0下
right15A1右
left16A2左
up17A3上

在课程所选的 Arduino Uno AVR 核心中,A0~A3 同时具有数字别名 14~17,所以 analogRead(15) 等价于在这块板上读取 A1。这个数字别名属于具体开发板核心的引脚映射,不是所有 Arduino 板都保证相同;若代码改到其他板型,直接写 A0、A1、A2、A3 通常更直观。不过,README 要求保留课程源码,本节仍原样使用 14~17。

配套接线图和最终 6.ino 在这里是一致的,都是“下 A0、右 A1、左 A2、上 A3”。后续查线时仍建议逐只遮挡确认物理方向,因为标签贴错或杜邦线交叉同样会让变量名与实物不一致。

1.2.2 宏名不能与对象名重复

#define right 15 是预处理宏:正式编译前,源码中的独立标识符 right 会被替换为 15。因此不能再写 Servo right;,否则预处理后会变成无效声明。视频中教师发现名称可能重合后,最终使用了更具体的对象名:

  • right_left_servo:左右轴舵机对象;
  • up_down_servo:上下轴舵机对象;
  • right_left_servopin:左右轴信号引脚常量;
  • up_down_servopin:上下轴信号引脚常量。

原始笔记把这段概括为“删除 pin”,容易造成误解。真正需要避免的是同一作用域内的标识符冲突和宏替换冲突,并不是单词 pin 本身不能使用。

1.3 定义舵机引脚

1.3.1 D9、D10 与两个 Servo 对象

本项目的舵机信号分工为:

Servo 对象信号引脚机械轴本节动作
right_left_servoD9下层水平旋转轴根据左右光差自动转动
up_down_servoD10上层俯仰轴只在初始化时转到 0°,没有自动上下追光

attach() 的作用是把软件对象关联到指定信号引脚;write() 写入的是目标位置命令。D9、D10 在这里传送舵机控制脉冲,不是给舵机供电的电源脚,也不能因为 Uno 板上把它们标成 PWM 引脚,就把 Servo 库控制简单理解为普通 analogWrite() 占空比输出。

setup() 末尾先发送 angle1=90 和 angle2=0。因此上电或按复位键后,程序都会重新运行 setup(),命令左右轴回到 90°、上下轴回到 0°。这只是重新发送软件设定值,不是读取了舵机真实位置,也不是机械限位开关执行的“寻零”;若舵盘安装角度不对或机构被卡住,复位不会自动修正机械问题。

1.3.2 角度和范围变量

视频保留了四个变量:

  • angle1=90:左右轴当前命令值;本项目约定 0°为左端、90°为中间、180°为右端。
  • range1=180:记录左右轴名义上限,但本节 constrain(angle1,0,180) 直接写了常量 180,所以 range1 没有实际参与运行。
  • angle2=0:上下轴初始命令值;本项目约定 0°为朝上。
  • range2=160:为后续俯仰控制准备的上限,本节尚未引用。

代码里的 0°和 180°是 Servo API 的命令范围,不等于装好太阳能板后的机构一定能无碰撞走满全程。实际安全角度还受舵盘装配、支架、灯杆和线束限制;若舵机持续嗡鸣、发热或机构顶住,应立即断电并缩小软件范围,不能依靠 constrain() 解决机械干涉。

1.3.3 pinMode() 与初始化顺序

视频把四个模拟口显式设置为 INPUT,用于表达它们是采样输入。对 Uno 而言,复位后的引脚本来就是输入,analogRead() 也可直接读取模拟口;这四句并非 ADC 能工作的必要前提,但属于合法且清晰的课程写法。不要改成 INPUT_PULLUP,内部上拉会参与模块输出网络并可能改变 ADC 读数。

较安全的调试顺序是先断电核对机械空间和线序,再上电让 setup() 给出初始角度,最后测试光照变化。已通电时不要手扳云台,也不要在机构顶住时反复按复位。

1.4 约束函数介绍

1.4.1 constrain() 的准确作用

Arduino 的 constrain(x, a, b) 返回规则是:

输入关系返回值
x < aa
a <= x <= bx
x > bb

它返回受限后的值,不会凭空修改变量,所以代码必须写成 angle1 = constrain(angle1,0,180); 才会把结果保存回 angle1。本节在每次加减 10°后进行限制,使软件变量不会继续下降到负数或上升到 180 以上。

这里应纠正视频中的一个因果说法:超出软件角度范围不会直接让“程序死机”,delay(500) 也不是防死机机制。真正的风险是机构撞限位后舵机仍试图运动,可能持续大电流、发热并拖低供电电压,进而造成 Uno 复位或 USB 断连。软件限幅、机械范围校准和可靠供电需要同时具备。

1.4.2 左右追光判断

在本项目模块“亮时读数小”的前提下,代码逻辑如下:

条件光照解释angle1 变化机械动作
right_size-left_size > 150右侧更暗、左侧更亮-10°向左转
left_size-right_size > 150左侧更暗、右侧更亮+10°向右转
两个差值都不大于 150左右差异落在死区内不变保持当前位置

阈值使用严格的大于号,所以差值恰好为 150 时也保持不动。若令

$ d=right_size-left_size $

则这段控制可以读成:$d>150$ 时左转,$d<-150$ 时右转,$-150\le d\le150$ 时保持。这个 ±150 的区间就是死区,可减少传感器小幅波动导致的左右反复抖动;它是课程样机采用的经验值,不是所有环境都通用的物理常数。

源码使用两个独立的 if...else,没有写成 if...else if...else。由于同一次采样中不可能同时满足“右减左大于 150”和“左减右大于 150”,两个调整分支不会同时执行;每个 else 中再次写入当前 angle1 是冗余但无害的保持命令。按照源码原样保留,不在精校稿中替教师重构。

1.4.3 10°步进和 500 ms 延时

每次触发只改变 10°,随后 delay(500) 让主循环暂停 500 ms,因此新的追光判断最多约每秒两次;连续偏向一侧时,命令角度大约以每秒 20°的步幅变化。实际舵机轴的运动速度还取决于舵机型号、负载和供电,不能用这个数字断言机械速度恰好是 20°/s。

延时降低了更新频率,便于观察并减少过快连续改命令,但它会阻塞整个 loop():等待期间既不重新采样,也不执行其他循环业务。固定阈值、固定步长和阻塞延时足够完成本节演示,却不代表是高精度太阳跟踪控制器。

1.5 接线图展示

1.5.1 四只光敏模块

配套总接线图对光敏模块线色的说明为:黄色是 AO 信号、黑色是 GND、红色是 5V。本节四路完整关系如下:

模块位置AO 接 Uno源码定义VCCGNDDO
下A0down = 145V 扩展排针GND 扩展排针不接
右A1right = 155V 扩展排针GND 扩展排针不接
左A2left = 165V 扩展排针GND 扩展排针不接
上A3up = 175V 扩展排针GND 扩展排针不接

程序读取的是每块模块的模拟输出 AO,不是数字比较输出 DO。四块模块必须与 Uno 共地,否则 A0~A3 没有共同电压参考。线色只是课程接线图的约定,实际接线仍应以模块 PCB 丝印为准,并在断电状态下插拔。

1.5.2 两只舵机和供电

配套图把舵机黄色线标为信号、红色线标为 5V、黑色线标为 GND,并给出:

左右旋转舵机:信号 → D9
上下俯仰舵机:信号 → D10
两只舵机电源正极 → 5V
两只舵机地线     → GND

课程兼容板加焊了多组并联的 5V、GND 扩展排针,作用只是提供更多插接位置,不会增加 USB 口或板载电源能够提供的总电流。两只舵机同时启动、负载过重或堵转时可能造成电压下降、开发板复位或导线发热。Arduino Servo 官方资料建议,驱动较多舵机时考虑规格合适的独立电源;采用外部舵机电源时,外部电源负极必须与 Uno GND 共地,且不能把舵机电源接到普通 GPIO。

视频的短时桌面演示按照课程图接在板上扩展供电口并成功运行,但这不能证明任意 USB 口、任意兼容板或长期运行都具有足够余量。首次通电应扶稳支架,观察是否出现舵机持续嗡鸣、开发板反复复位、USB 断连、线材或稳压器异常发热。

1.5.3 上传和左右追光实测

视频中的验证顺序为:

  1. 依据总接线图接好两只舵机和四只光敏模块;本节动作判断实际使用左右两只模块。
  2. 插入 USB 数据线,在 Arduino IDE 中选择当前 Uno 和实际串口。
  3. 编译并上传页首草图。
  4. 用手电筒从右侧照射,太阳能板向右转;从左侧照射,太阳能板向左转。
  5. 按下板上的红色复位按钮,setup() 重新执行,两个舵机收到 90°和 0°的初始命令。

演示证明当前样机的左右传感器方位、D9 舵机方向和两条差值判断能够配合工作。它没有演示自动上下追光,也没有测量太阳能板输出功率;因此本节结论应限定为“左右轴能够朝手电筒方向逐步跟随”,不能扩大成完整二维追光已经编程完成。

核对依据

  • 本地 videos/315.10、太阳能追光路灯实现左右追光.mp4:逐帧核对代码从 ADC 与舵机示例合并、变量更名、最终编译上传、总接线图、实物接线、手电筒左右测试和复位效果;确认本节最终代码只含左右判断。
  • 本地 transcripts/315.10、太阳能追光路灯实现左右追光.txt 与 .srt:核对讲解顺序、150 阈值、10°步进、500 ms 延时和现场接线口述,并结合画面纠正“阻值”“14 号等于 A2”“防止程序死机”等不准确表述。
  • 本地配套 太阳能追光路灯.pptx、接线图.jpg 与最终 6.ino:核对四只光敏模块和两只舵机的最终引脚关系;最终源码只用于确认本节已出现部分的最终拼写,不提前收录后续上下追光判断。
  • Arduino Uno R3 官方页面:核对 Uno R3 具有 6 路模拟输入以及板卡基本资源。
  • Arduino AVR Core 的 Uno 标准引脚映射:核对 A0~A3 在本课程 Uno 核心中分别对应数字别名 14~17。
  • Arduino 官方 API 概览:核对 Uno 上 analogRead() 返回 0~1023 的 10 位结果,以及 pinMode()、delay() 等接口的用途。
  • Arduino 官方 constrain() 参考:核对输入小于下限、位于区间内和大于上限时的三个返回规则。
  • Arduino Servo 库 API:核对 attach() 与 write() 的含义和目标角度范围。
  • Arduino Servo 库说明:核对舵机三线连接、舵机供电电流注意事项以及外部供电时必须共地。