太阳能追光路灯实现上下追光

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

316.11、太阳能追光路灯实现上下追光

#include <Servo.h>                                // 引入 Arduino Servo 库,使程序可以创建并控制舵机对象

#define right 15                                  // 将右侧光敏模块的 AO 接到 Uno 的 A1;在本课程使用的 Uno 核心中,A1 的数字别名是 15
#define left 16                                   // 将左侧光敏模块的 AO 接到 Uno 的 A2;在本课程使用的 Uno 核心中,A2 的数字别名是 16
#define up 17                                     // 将上侧光敏模块的 AO 接到 Uno 的 A3;在本课程使用的 Uno 核心中,A3 的数字别名是 17
#define down 14                                   // 将下侧光敏模块的 AO 接到 Uno 的 A0;在本课程使用的 Uno 核心中,A0 的数字别名是 14

#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 读数,初始值为 0
int left_size=0;                                 // 保存左侧光敏模块的 ADC 读数,初始值为 0
int up_size=0;                                   // 保存上侧光敏模块的 ADC 读数,初始值为 0
int down_size=0;                                 // 保存下侧光敏模块的 ADC 读数,初始值为 0

int angle1=90;                                   // 保存左右轴的目标角度,开机初值为 90°
int range1=180;                                  // 保存左右轴名义上限 180°;原程序保留了该变量,但后文实际直接写常量 180

int angle2=0;                                    // 保存上下轴的目标角度,开机初值为 0°
int range2=160;                                  // 保存本样机上下轴的安全上限 160°,为灯具和机构预留避让空间

void setup() {                                   // 定义只在上电或复位后执行一次的初始化函数
  pinMode(right,INPUT);                          // 把右侧光敏模块所接引脚设为输入
  pinMode(left,INPUT);                           // 把左侧光敏模块所接引脚设为输入
  pinMode(up,INPUT);                             // 把上侧光敏模块所接引脚设为输入
  pinMode(down,INPUT);                           // 把下侧光敏模块所接引脚设为输入
  right_left_servo.attach(right_left_servopin);  // 将左右轴 Servo 对象关联到 D9 信号引脚
  up_down_servo.attach(up_down_servopin);        // 将上下轴 Servo 对象关联到 D10 信号引脚

  right_left_servo.write(angle1);                // 向左右轴舵机发送 90°的初始目标位置
  up_down_servo.write(angle2);                   // 向上下轴舵机发送 0°的初始目标位置
}                                                // setup() 初始化结束

void loop() {                                    // 定义持续重复执行的追光控制循环

  right_size=analogRead(right);                  // 读取右侧模块 AO 的模拟量并保存到 right_size
  left_size=analogRead(left);                    // 读取左侧模块 AO 的模拟量并保存到 left_size
  up_size=analogRead(up);                        // 读取上侧模块 AO 的模拟量并保存到 up_size
  down_size=analogRead(down);                    // 读取下侧模块 AO 的模拟量并保存到 down_size

  if(right_size-left_size > 150)                 // 若右侧读数比左侧大 150 以上,表示本样机右侧更暗、左侧更亮
  {                                              // 进入向左侧光源逐步靠近的处理分支
    angle1=angle1 - 10;                          // 将左右轴目标角度减小 10°,在本样机上对应向左转
    angle1= constrain(angle1,0,180);             // 把左右轴目标角度限制在 0°~180°之间
    right_left_servo.write(angle1);              // 将受限后的目标角度发送给 D9 上的左右轴舵机
  }else                                          // 当右侧读数没有比左侧大 150 以上时进入此分支
  {                                              // 开始执行保持当前左右轴目标角度的分支
    right_left_servo.write(angle1);              // 再次发送原角度,使左右轴保持当前目标位置
  }                                              // 结束第一组左右差值判断

  if(left_size-right_size > 150)                 // 若左侧读数比右侧大 150 以上,表示本样机左侧更暗、右侧更亮
  {                                              // 进入向右侧光源逐步靠近的处理分支
    angle1=angle1 + 10;                          // 将左右轴目标角度增加 10°,在本样机上对应向右转
    angle1= constrain(angle1,0,180);             // 把左右轴目标角度限制在 0°~180°之间
    right_left_servo.write(angle1);              // 将受限后的目标角度发送给 D9 上的左右轴舵机
  }else                                          // 当左侧读数没有比右侧大 150 以上时进入此分支
  {                                              // 开始执行保持当前左右轴目标角度的分支
    right_left_servo.write(angle1);              // 再次发送原角度,使左右轴保持当前目标位置
  }                                              // 结束第二组左右差值判断

  if(up_size-down_size > 150)                    // 若上侧读数比下侧大 150 以上,表示本样机上侧更暗、下侧更亮
  {                                              // 进入向下侧光源逐步靠近的处理分支
    angle2=angle2 + 10;                          // 将上下轴目标角度增加 10°,在本样机上对应向下俯
    angle2= constrain(angle2,0,range2);           // 把上下轴目标角度限制在 0°~range2(160°)之间
    up_down_servo.write(angle2);                 // 将受限后的目标角度发送给 D10 上的上下轴舵机
  }else                                          // 当上侧读数没有比下侧大 150 以上时进入此分支
  {                                              // 开始执行保持当前上下轴目标角度的分支
    up_down_servo.write(angle2);                 // 再次发送原角度,使上下轴保持当前目标位置
  }                                              // 结束第一组上下差值判断
  if(down_size-up_size > 150)                    // 若下侧读数比上侧大 150 以上,表示本样机下侧更暗、上侧更亮
  {                                              // 进入向上侧光源逐步靠近的处理分支
    angle2=angle2 - 10;                          // 将上下轴目标角度减小 10°,在本样机上对应向上仰
    angle2= constrain(angle2,0,range2);           // 把上下轴目标角度限制在 0°~range2(160°)之间
    up_down_servo.write(angle2);                 // 将受限后的目标角度发送给 D10 上的上下轴舵机
  }else                                          // 当下侧读数没有比上侧大 150 以上时进入此分支
  {                                              // 开始执行保持当前上下轴目标角度的分支
    up_down_servo.write(angle2);                 // 再次发送原角度,使上下轴保持当前目标位置
  }                                              // 结束第二组上下差值判断

  delay(500);                                    // 暂停 500 ms,再开始下一轮采样和两轴判断


}                                                // loop() 本轮结束,随后由 Arduino 运行框架再次调用

一、太阳能追光路灯上下追光

本节在第 315 节“左右追光”程序上增加上下轴判断,得到课程项目的最终二维追光草图。页首代码依据视频最后成功编译、上传的画面和配套“最终代码/6.ino”逐项核对:程序仍保留教师原来的变量名、数值、语句顺序、两个独立 if...else 的写法以及冗余的保持命令,只去除了原注释并补上逐行说明,没有擅自优化。

视频编辑过程中曾短暂出现复制后尚未改完的代码,例如上下判断里仍向 right_left_servo 写入 angle1。那只是中间状态;最终版本已经统一改成 angle2、range2 和 up_down_servo,因此不能把中途错误抄入成品源码。最终草图在视频所用环境中编译后显示:程序存储空间使用 2666 字节,全局变量使用 65 字节,并上传到当时识别出的 COM5。这些数字和端口号只能说明该次课程演示的结果,不是每台电脑都会相同。

完整控制关系可以概括为:

四只光敏模块的 AO → Uno 的 A0~A3 → analogRead() 取得四路 ADC 读数
                                      ↓
                      左右差值判断 + 上下差值判断
                                      ↓
                每轴差值超过 150 时,将目标角度改变 10°
                                      ↓
                   constrain() 限幅 → D9/D10 控制两只舵机
                                      ↓
                    太阳能板转向 → 下一轮重新感知光照

1.1 从左右追光扩展为二维追光

教师的实现方法是复制已有的两组左右判断,再把复制出的变量和舵机对象改为上下轴对应名称:

控制轴两只比较传感器目标角度变量限制范围Servo 对象信号引脚
左右旋转轴right_size、left_sizeangle1,初值 90°0°~180°right_left_servoD9
上下俯仰轴up_size、down_sizeangle2,初值 0°0°~range2,即 160°up_down_servoD10

原笔记把上下轴角度变量识别成了“h2”,应纠正为源码中的 angle2。只改比较变量还不够:角度变量、限幅变量、舵机写入对象也必须属于同一根轴,否则传感器判断与执行机构会错配。

程序每轮先连续读取四路 ADC,之后依次处理左右轴、上下轴,最后统一 delay(500)。因此,一次循环里如果左右差值和上下差值都超过阈值,两根轴都可以各改变 10°,太阳能板会以连续的小步动作逼近斜向光源,而不是必须等一根轴完全调完再调另一根轴。

这里的算法虽然由传感器读数影响舵机、舵机又改变下一轮读数,形成了反馈过程,但它没有位置传感器、比例控制或真正的太阳方位计算。angle1、angle2 只是程序最近一次发出的目标角度,Servo 库的 write() 也只是下达目标命令,并不会返回舵机轴的实际角度。

1.2 上下光敏差值与转动方向

up_size、down_size 保存的是模块 AO 电压经 ADC 转换后的数字读数,不是电阻阻值、照度 lux 或元件尺寸。第 313 节和本项目演示已经确认:在课程使用的这批模块及接法中,光越强,ADC 读数越小;光越弱,读数越大。所以原笔记的“上光敏大于下光敏”应理解为“上侧 ADC 读数更大”,而不是“上侧更亮”。

上下轴最终逻辑如下:

源码条件当前光照含义angle2 变化本样机动作
up_size-down_size > 150上侧更暗、下侧更亮+10°向下俯
down_size-up_size > 150下侧更暗、上侧更亮-10°向上仰
两个条件都不成立上下差异处于死区不变保持当前目标位置

若定义

$ d_v=up_size-down_size $

则可写成:$d_v>150$ 时向下,$d_v<-150$ 时向上,$-150\le d_v\le150$ 时保持。源码使用严格的大于号,所以差值恰好等于 150 时也不会调整。这个 ±150 区间是死区:它可以减少微小噪声、环境波动和两只模块固有误差引发的来回抖动,但 150 是本课程样机的经验阈值,不是通用物理常数。

两条方向判断是两个独立的 if...else,不是一条 if...else if...else。同一组固定采样值不可能同时满足“上减下大于 150”和“下减上大于 150”,所以本轮最多只有一个上下调整分支生效;两个 else 中重复写入当前 angle2 是冗余但无害的保持命令。精校稿按配套源码原样保留,没有替教师重构。

更换光敏模块、改变分压接法或接错 AO 方向后,亮暗与读数的极性可能不同。首次调试应先固定舵机,分别遮挡和照亮每只模块,确认“物理位置—模拟引脚—变量名—读数变化方向”四者一致,再允许机构运动。若动作反而远离光源,应先断电查方向标签、接线和舵盘安装,不能直接用手顶住通电舵机。

1.3 angle2、range2 与机械限位

课程样机约定上下轴 0°为向上的端点,目标角度增大时太阳能板向下俯。教师把 range2 设置为 160,并用:

angle2= constrain(angle2,0,range2);

把上下轴的软件命令限制在 0°~160°。设置 160°而不是 180°,是为了给样机的灯具、支架和线束留出避让空间,降低太阳能板或云台撞到路灯结构的风险;它是这台样机的机械标定结果,不能直接照搬到不同支架。

constrain(x,a,b) 的作用只是返回被夹在 [a,b] 内的数值:小于 a 时返回 a,大于 b 时返回 b,区间内则返回原值。它没有读取限位开关,也不知道太阳能板是否真的撞到了物体。因此:

  • 当 angle2=160 后仍持续满足向下条件时,程序每轮先算出 170,再把它限制回 160,并继续发送 160°;软件变量不会越界,但传感器失衡可能一直存在。
  • 当 angle2=0 后仍持续满足向上条件时,同理会先得到 -10,再被限制回 0。
  • 舵盘安装角度、舵机个体行程、支架尺寸和线束余量若不合适,即使命令在 0°~160°内仍可能发生机械干涉。

因此,软件限幅不能替代断电手动检查、低风险分段试转以及必要的实体限位。若舵机持续嗡鸣、明显发热、机构顶住不动、开发板反复复位或 USB 断连,应立即断电并缩小可用角度范围。

还要区分两个范围变量:range2 确实参与了上下轴的 constrain();range1=180 虽然被定义,但左右轴代码直接写了 constrain(angle1,0,180),所以 range1 在原程序中并未真正使用。

1.4 步进、循环时序与复位

每次满足某个方向条件时,对应目标角度只改变 10°。循环末尾的 delay(500) 让下一次完整判断至少晚约 500 ms,因此忽略少量采样和语句执行时间后,程序每秒最多约判断两轮;持续偏向一侧时,命令角度的最大变化节奏约为 20°/s。这不等于舵机转轴必然以 20°/s 匀速运动,实际速度还取决于舵机型号、负载、供电电压和机械阻力。

delay(500) 会阻塞整个 loop():等待期间四路传感器不会重新采样,两根轴也不会响应新的方向。这个简单时序便于课堂观察,却会限制动态响应能力。

上电或按复位键后,setup() 会重新向左右轴发送 90°、向上下轴发送 0°。这不是读取真实位置后的“自动回零”,也没有缓慢归位过程;若当时上下轴停在较大的角度,复位可能立即要求它回到 0°。通电前要确认全行程没有障碍物,并把手、工具和松散导线移出运动范围。

1.5 接线、供电与上传检查

本节没有把传感器或舵机“切换到另一组引脚”。最终二维程序会同时使用四只光敏模块和两只舵机,应按项目总接线图保持如下连接:

部件信号连接供电连接程序用途
下侧光敏模块AO → A0VCC → 5V,GND → GND与上侧比较
右侧光敏模块AO → A1VCC → 5V,GND → GND与左侧比较
左侧光敏模块AO → A2VCC → 5V,GND → GND与右侧比较
上侧光敏模块AO → A3VCC → 5V,GND → GND与下侧比较
左右轴舵机信号线 → D9正极 → 5V,地线 → GND水平旋转
上下轴舵机信号线 → D10正极 → 5V,地线 → GND俯仰运动

四块光敏模块使用 AO,没有使用板载比较器输出 DO。所有模块、Uno 和舵机控制信号都必须有共同的地参考;采用外部舵机电源时,也要将外部电源负极与 Uno 的 GND 共地,但不要把外部电源正极反灌到 USB 5V 系统。

D9、D10 是舵机信号线,不是舵机电源。两只舵机同时启动或堵转时的电流可能明显大于静止电流;课程兼容板上的多组 5V/GND 排针只是把电源节点扩展成更多插孔,并不会凭空提高 USB 口、稳压器或细导线的供电能力。若出现电压跌落、复位、断连或发热,应使用容量和瞬时电流合适的独立稳压电源为舵机供电并保持共地,而不是继续让机构堵转。

建议按以下顺序检查:

  1. 断电检查 A0~A3 的物理方向标签、D9/D10 的舵机归属、正负极和公共地。
  2. 手动轻移机构,确认 0°~160°预计行程不会撞灯杆、拉扯导线;不要强扭已通电舵机。
  3. 在 Arduino IDE 中选择实际开发板与当前端口,编译并上传页首完整草图;视频中的 COM5 只是教师电脑当时的端口。
  4. 第一次上电时随时准备断电,先观察 90°/0°初始化动作,再用较小范围的光照变化逐轴验证方向。
  5. 若需要校准阈值,应临时加入串口输出观察四路原始读数;课程最终 6.ino 本身没有 Serial.begin() 或 Serial.println(),不会自动显示 ADC 数据。

二、最终效果测试

2.1 视频中的实际测试过程

教师上传程序并接好总线路后,用手机的补光灯作为可移动光源进行测试:

  1. 光源移到太阳能板下方,面板逐步向下俯。
  2. 光源移到上方,面板逐步向上仰,直到本样机约定的 0°端点。
  3. 光源移到右侧,左右轴带动面板向右转。
  4. 再把光源向下移动,上下轴继续向下俯,演示两个方向的控制可以连续配合。

这段实物演示能够证明:在当时的桌面环境、当前接线、传感器方向和手机近距离照明条件下,四路差值判断与两只舵机的转向相互匹配,太阳能板能逐步跟随光源。画面中光敏模块上的绿色小灯是模块指示灯,不应误认成路灯主体已经由太阳能供电点亮。

2.2 “追光”不等于已证明“最大功率”

原笔记把测试目的写成“实现最大光能接收”,表述过强。光伏跟踪系统的目标确实是让组件朝向太阳,以改善入射角并争取更高能量产出;双轴机构还能同时调整方位角和俯仰角。但视频只观察了机构是否跟随手机光源,没有测量太阳能板的电压、电流、瞬时功率或一段时间内的能量,也没有与固定面板作对照,所以只能得出“完成二维方向跟随演示”,不能由此证明已经找到最大功率点或必然获得最大能量。

需要区分三个概念:

  • 方向追踪:本程序根据四只光敏模块的相对读数改变面板朝向。
  • 功率测量:至少要同时测量光伏板电压与电流,计算 $P=UI$,本节没有做。
  • 最大功率点跟踪(MPPT):这是对光伏组件电气工作点的控制,不等同于用舵机改变面板朝向,本节程序也没有实现 MPPT。

实际能量产出还会受到日照强度、温度、遮挡、灰尘、组件特性、驱动机构自身耗电以及天气等因素影响。若要验证“收益”,应在户外太阳光下记录固定面板和追踪面板在相同时间条件下的功率或累计能量,并扣除舵机与控制器耗电。

2.3 从手机灯演示到户外使用还需要什么

手机灯距离很近,移动后会在面板周围产生明显的局部亮度差,适合检查代码方向;太阳距离极远,落在尺寸接近的四只传感器上的直射光强可能更接近。公开的四象限 LDR 跟踪研究常在传感器之间设置遮光结构或圆柱,让光线偏斜时形成可测阴影差。课程视频中的样机没有清楚展示这种典型遮光隔板,因此,近距离手机灯测试成功并不能单独证明它在户外太阳光下也能获得同样灵敏、稳定的方向误差。

正式户外验证前至少应完成:

  • 校准四只模块在同一均匀光照下的基线,检查个体差异是否已接近或超过 150。
  • 核对上下左右标签和支架几何方向,逐轴确认角度增加、减少所对应的真实运动方向。
  • 在早晨、中午、傍晚及云层变化时记录四路读数,依据数据重新选择阈值,而不是默认 150 到处适用。
  • 检查传感器的安装位置与遮光结构是否能对太阳入射方向产生稳定差值,并防止太阳能板、灯体或支架本身造成误导性阴影。
  • 验证 0°~160°和 0°~180°的机械安全范围,给运动线束留余量,并检查风载下支架是否可靠。
  • 监测舵机电源电压、电流和温升;若传感器长期要求越过机械端点,程序会反复发送极限角度,不能依赖软件变量限幅来发现堵转。

核对依据

  • 本地 videos/316.11、太阳能追光路灯实现上下追光.mp4:逐帧核对从左右代码复制上下判断、变量替换、160°限幅、最终编译上传画面、总接线图以及手机灯上下/左右跟随测试。
  • 本地 transcripts/316.11、太阳能追光路灯实现上下追光.txt 与 .srt:核对教师对上下读数关系、10°增减、0°/160°方向和最终测试过程的口述,并结合画面纠正“h2”“切换引脚”等识别或概括错误。
  • 本地配套项目“项目4:基于Arduino的太阳能追光路灯/最终代码/6.ino”:核对页首完整最终源码的标识符、数值、语句顺序和两个轴的全部判断。
  • Arduino 官方编程参考:核对 Uno 常用 analogRead() 结果范围及 pinMode()、delay() 等接口的基本行为。
  • Arduino 官方 constrain() 参考:核对输入低于下限、处于区间内和高于上限时的返回规则。
  • Arduino Servo 库 API:核对 attach()、write() 的作用、角度命令范围,以及 Servo 库记录的是最近目标值而非轴的真实位置反馈。
  • 美国能源部:Solar Photovoltaic System Design Basics:核对单轴/双轴跟踪器的目的,以及增加发电量与增加成本、维护需求之间的关系。
  • 美国能源部:Photovoltaic System Design and Energy Yield:核对光伏能量产出还受温度、积尘、遮挡等因素影响,不能只依据转向动作判断。
  • Sensors:四象限 LDR 双轴太阳跟踪研究:用于核对四象限光敏元件借助遮光圆柱形成方向差值的实例;该文献只用于说明户外方向检测原理,不代表课程样机采用了相同结构。