找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1631

积分

0

好友

215

主题
发表于 2026-2-12 16:30:55 | 查看: 29| 回复: 0

基于Arduino控制器搭建的风扇频闪系统,可通过简单硬件组合实现静态固定与动态循环两种频闪效果,兼具低成本、易调试、实用性强等特点,对于物理、光电、仪器、测控等专业是一个非常好的闭环综合实验。本文将从硬件构成、工作原理、模式特性及调试要点等方面,系统拆解该系统的实现逻辑。

实验原理

物体在快速运动时,当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1~0.4秒左右,这种现象被称为视觉暂留现象。频闪效应就是通过视觉暂留现象实现的,通过周期性闪烁的光源照射运动的物体,物体每次运动到某一固定位置时恰好被照亮,被照亮的物体的影像会在眼中停留0.1~0.4秒,所以会看到运动的物体“静止”在某处的现象。频闪现象对光源闪烁的稳定性要求较高,如果光源闪烁的频率不固定,会有多个“物体”叠加在一起的现象,不能观察到清晰的像。

核心硬件构成(双模式通用)

  • 主控单元Arduino UNO,承担信号采集、算法运算及执行器控制核心任务,保障系统时序精度。
  • 位置检测单元:风扇切割扇叶后的红色激光器,光电探测器用于采集叶片位置信号和风扇转速信息,通过模拟量变化反馈叶片是否经过检测点。
  • 执行单元:白光LED模块,采用低电平触发模式,响应控制器指令实现精准亮灭,作为频闪视觉输出载体。
  • 载体单元:9叶电脑风扇,叶片数量为相位定位提供基准,旋转过程中形成周期性位置触发信号,保障频闪效果稳定性。
  • 辅助:杜邦线若干、USB拓展坞一个,BNC转鳄鱼夹线,SMA转鳄鱼夹线,不同型号固定支撑座若干,立柱若干,光学平台。

整体硬件是多轴笼式结构搭建而成,结构如下图所示。

风扇频闪系统硬件搭建实物图

系统核心工作原理

两种模式的底层工作逻辑一致,均通过“叶片位置检测-信号滤波处理-微秒级时序控光”的闭环机制实现频闪效果,核心依托位置识别精度与时序控制稳定性,具体流程如下:

  1. 位置信号采集:风扇旋转时,叶片切割激光触发光电探测器模拟信号突变,当信号值超过预设阈值,初步判定叶片到位。
  2. 信号滤波防抖:通过“连续2次有效信号确认+2毫秒时间间隔滤波”算法,排除环境光干扰、传感器抖动导致的误触发,确保叶片位置识别精准。
  3. 时序控光执行:控制器根据预设程序逻辑触发光源点亮,采用450微秒固定点亮时长(可按需微调),避免视觉残影,随后自动关闭光源,完成单次频闪动作。
  4. 循环执行:风扇持续旋转带动叶片依次经过检测点,系统重复上述流程,形成连续、稳定的频闪效果,频闪相位由程序逻辑精准控制。通过内部控制程序调整照明频率,可以看到扇叶上一个字母或者多个字母的频闪效果,这是一个简单的闭环系统。

频闪系统框图如下:

风扇频闪实验系统框图

双模式功能特性与实现

模式一:手动固定频闪(静态相位模式)

功能特性:采用相位锁定机制,通过串口指令标定叶片基准后,手动指定目标叶片编号,光源仅在该叶片经过检测点时触发频闪,频闪相位固定不变,适用于静态标识展示、定点亮化等场景。

调试流程

  1. 将程序烧录至Arduino控制器,打开串口监视器并设置波特率为115200,等待系统初始化完成。
  2. 缓慢转动风扇,将待标定为“1号”的叶片对准光电探测器检测点,通过串口发送指令 s 完成叶片基准同步。
  3. 发送1-9范围内的数字指令,设定目标频闪叶片,系统自动锁定该叶片相位,实现定点频闪,相位变更需重新发送数字指令。

核心优势:相位锁定精度高,无漂移现象,调试流程简洁,对硬件时序稳定性要求较低。

模式二:自动循环频闪(动态相位模式)

功能特性:在静态模式基础上引入非阻塞式时序调度机制,叶片基准同步完成后,系统按1秒固定间隔(可自定义)自动切换频闪相位,相位递进方向与风扇旋转方向自适应匹配,呈现动态流动光效,适用于氛围亮化、动态演示等场景。

功能亮点

  1. 手动干预兼容:支持通过串口数字指令直接重置频闪相位,重置后自动延续循环逻辑,兼顾手动控制与自动运行需求。
  2. 时序稳定性:采用millis()函数实现非阻塞计时,避免计时逻辑阻塞叶片检测与防抖流程,保障频闪与相位切换精准度。
  3. 方向自适应:通过程序参数配置(isClockwiseRotation)匹配风扇实际旋转方向(顺时针/逆时针),确保相位递进与风扇运动逻辑一致。

两种频闪模式都可以通过改变代码里displayBladeCount的值,设定扇叶上的成像数量,效果对比如下图。

手动固定模式与自动循环模式成像效果对比图

总结

本文设计的基于Arduino的双模式风扇频闪系统,以光电探测器为位置检测核心、白光LED为执行载体,构建了“检测-处理-执行-反馈”的闭环控制架构,实现了静态固定与动态循环两种频闪模式。系统兼具低成本、高稳定性、易调试的优势,硬件选型通用且无需复杂改装,软件逻辑采用防抖滤波与非阻塞计时算法,保障了频闪相位精准度与时序稳定性。两款模式均支持串口手动干预,兼顾灵活性与实用性。如果你想获取更多类似的开源实战项目代码或进行技术交流,可以到云栈社区逛逛。

附录1:手动调节相位代码

// 核心配置参数
const int lightPin = 11;
const int detectorPin = A0;
const unsigned long lightOnUs = 450;
const int analogThreshold = 90;

const int bladesPerRevolution = 9;
int targetBlade = 1;          // 默认目标
int displayBladeCount = 1;//设定扇叶上成像数量
unsigned long triggerDelayUs = 0;

// 防抖参数
const unsigned long debounceUs = 2000;
const int pulseConfirmCount = 2;

// 状态变量
bool isLightOn = false;
unsigned long lightOnStartTime = 0;
unsigned long lastTriggerTime = 0;
int pulseCount = 0;
int currentBlade = 0;
unsigned long lastPrintTime = 0;

const bool isClockwiseRotation = false; //风扇转动方向 true=顺时针, false=逆时针

// 同步状态
bool syncCompleted = false;   // 是否已完成同步

void setup(){
  pinMode(lightPin, OUTPUT);
  digitalWrite(lightPin, HIGH); // 光源初始关闭
  Serial.begin(115200);
  delay(500); // 等待串口稳定

  Serial.println("=== 风扇屏显 - 相位可控模式 ===");
  Serial.println("📌 操作步骤:");
  Serial.println("1. 缓慢转动风扇,将【你想作为1号的叶片】对准传感器");
  Serial.println("2. 在串口监视器输入 's' 并发送");
  Serial.println("3. 系统会将下一个经过的叶片设为1号");
  Serial.println("4. 输入数字(1-9)设置目标显示位置");
  Serial.print("风扇旋转方向: ");
  Serial.println(isClockwiseRotation ? "顺时针" : "逆时针");
  Serial.println("----------------------------------");
}

bool detectValidFanPulse(){
  int analogValue = analogRead(detectorPin);
  unsigned long currentTime = micros();

  // 调试打印(每200ms)
  if (currentTime - lastPrintTime >= 200000) {
    Serial.print("ADC=");
    Serial.print(analogValue);
    Serial.print(" | Blade=");
    Serial.print(currentBlade);
    Serial.print(" | Target=");
    Serial.print(targetBlade);
    Serial.print(" | Light=");
    Serial.println(isLightOn ? "ON" : "OFF");
    lastPrintTime = currentTime;
  }

  if (analogValue > analogThreshold) {
    pulseCount++;
    if (pulseCount >= pulseConfirmCount && currentTime - lastTriggerTime > debounceUs) {
      pulseCount = 0;
      lastTriggerTime = currentTime;
      return true;
    }
  } else {
    pulseCount = 0;
  }
  return false;
}

bool isBladeInDisplayRange(){
  int step = isClockwiseRotation ? 1 : -1;
  for (int i = 0; i < displayBladeCount; i++) {
    int bladeToCheck = targetBlade + i * step;
    if (bladeToCheck < 1) bladeToCheck += bladesPerRevolution;
    if (bladeToCheck > bladesPerRevolution) bladeToCheck -= bladesPerRevolution;
    if (currentBlade == bladeToCheck) {
      return true;
    }
  }
  return false;
}

void loop(){

  // 处理串口命令
  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    cmd.trim();

    if (cmd == "s") {
      // 触发同步:重置状态,等待下一个叶片作为1号
      syncCompleted = false;
      currentBlade = 0;
      Serial.println("🔄 等待叶片同步... 请让目标叶片通过传感器");
    }
    else if (cmd.length() == 1 && isDigit(cmd[0])) {
      int num = cmd.toInt();
      if (num >= 1 && num <= bladesPerRevolution) {
        targetBlade = num;
        Serial.print("🎯 目标叶片已设为: ");
        Serial.println(targetBlade);
      }
    }
  }

  // 仅在完成同步后才处理叶片计数和点亮
  if (detectValidFanPulse()) {
    if (!syncCompleted) {
      // 第一个有效脉冲 → 设为1号叶片
      currentBlade = 1;
      syncCompleted = true;
      Serial.println("✅ 同步成功!当前叶片 = 1");
    } else {
      currentBlade++;
      if (currentBlade > bladesPerRevolution) {
        currentBlade = 1;
      }
    }
    // 如果已同步且当前叶片在显示范围内,则点亮
    if (syncCompleted && isBladeInDisplayRange() && !isLightOn) {
      delayMicroseconds(triggerDelayUs);
      digitalWrite(lightPin, LOW);
      lightOnStartTime = micros();
      isLightOn = true;
    }
  }
  // 自动关灯
  if (isLightOn && (micros() - lightOnStartTime >= lightOnUs)) {
    digitalWrite(lightPin, HIGH);
    isLightOn = false;
  }
}

附录2:自动调节相位代码


// 核心配置参数
const int lightPin = 11;
const int detectorPin = A0;
const unsigned long lightOnUs = 450;
const int analogThreshold = 90;

const int bladesPerRevolution = 9;
int targetBlade = 1;          // 默认目标
int displayBladeCount = 1;    // 设定扇叶上成像数量
unsigned long triggerDelayUs = 0;

// 防抖参数
const unsigned long debounceUs = 2000;
const int pulseConfirmCount = 2;

// 状态变量
bool isLightOn = false;
unsigned long lightOnStartTime = 0;
unsigned long lastTriggerTime = 0;
int pulseCount = 0;
int currentBlade = 0;
unsigned long lastPrintTime = 0;

const bool isClockwiseRotation = false; // 风扇转动方向 true=顺时针, false=逆时针

// 同步状态
bool syncCompleted = false;   // 是否已完成同步

// 新增:自动相位循环参数
unsigned long lastPhaseChangeTime = 0;  // 记录上次相位切换的时间
const unsigned long phaseChangeInterval = 1000; // 相位切换间隔(1000毫秒=1秒)

void setup(){
  pinMode(lightPin, OUTPUT);
  digitalWrite(lightPin, HIGH); // 光源初始关闭
  Serial.begin(115200);
  delay(500); // 等待串口稳定

  Serial.println("=== 风扇屏显 - 自动相位循环模式 ===");
  Serial.println("📌 操作步骤:");
  Serial.println("1. 缓慢转动风扇,将【你想作为1号的叶片】对准传感器");
  Serial.println("2. 在串口监视器输入 's' 并发送");
  Serial.println("3. 系统会将下一个经过的叶片设为1号,同步后自动每隔1秒相位前进");
  Serial.print("风扇旋转方向: ");
  Serial.println(isClockwiseRotation ? "顺时针" : "逆时针");
  Serial.println("----------------------------------");
}

bool detectValidFanPulse(){
  int analogValue = analogRead(detectorPin);
  unsigned long currentTime = micros();

  // 调试打印(每200ms)
  if (currentTime - lastPrintTime >= 200000) {
    Serial.print("ADC=");
    Serial.print(analogValue);
    Serial.print(" | Blade=");
    Serial.print(currentBlade);
    Serial.print(" | Target=");
    Serial.print(targetBlade);
    Serial.print(" | Light=");
    Serial.println(isLightOn ? "ON" : "OFF");
    lastPrintTime = currentTime;
  }

  if (analogValue > analogThreshold) {
    pulseCount++;
    if (pulseCount >= pulseConfirmCount && currentTime - lastTriggerTime > debounceUs) {
      pulseCount = 0;
      lastTriggerTime = currentTime;
      return true;
    }
  } else {
    pulseCount = 0;
  }
  return false;
}

bool isBladeInDisplayRange(){
  int step = isClockwiseRotation ? 1 : -1;
  for (int i = 0; i < displayBladeCount; i++) {
    int bladeToCheck = targetBlade + i * step;
    if (bladeToCheck < 1) bladeToCheck += bladesPerRevolution;
    if (bladeToCheck > bladesPerRevolution) bladeToCheck -= bladesPerRevolution;
    if (currentBlade == bladeToCheck) {
      return true;
    }
  }
  return false;
}

void loop(){
  // 处理串口命令(保留手动同步功能)
  if (Serial.available()) {
    String cmd = Serial.readStringUntil('\n');
    cmd.trim();

    if (cmd == "s") {
      // 触发同步:重置状态,等待下一个叶片作为1号
      syncCompleted = false;
      currentBlade = 0;
      lastPhaseChangeTime = millis(); // 重置相位切换计时
      Serial.println("🔄 等待叶片同步... 请让目标叶片通过传感器");
    }
    // 保留手动设置目标叶片的功能(可选)
    else if (cmd.length() == 1 && isDigit(cmd[0])) {
      int num = cmd.toInt();
      if (num >= 1 && num <= bladesPerRevolution) {
        targetBlade = num;
        lastPhaseChangeTime = millis(); // 重置计时,从新目标开始循环
        Serial.print("🎯 手动设置目标叶片为: ");
        Serial.println(targetBlade);
      }
    }
  }

  // 新增:自动相位循环逻辑(仅同步完成后生效)
  if (syncCompleted) {
    unsigned long currentTime = millis();
    // 判断是否达到1秒切换间隔
    if (currentTime - lastPhaseChangeTime >= phaseChangeInterval) {
      lastPhaseChangeTime = currentTime; // 更新上次切换时间

      // 根据风扇旋转方向,计算下一个目标叶片
      int step = isClockwiseRotation ? 1 : -1;
      targetBlade += step;

      // 处理叶片编号循环(1-9)
      if (targetBlade < 1) {
        targetBlade += bladesPerRevolution;
      } else if (targetBlade > bladesPerRevolution) {
        targetBlade -= bladesPerRevolution;
      }

      // 串口打印相位变化提示
      Serial.print("🔄 自动切换相位 → 目标叶片: ");
      Serial.println(targetBlade);
    }
  }

  // 仅在完成同步后才处理叶片计数和点亮
  if (detectValidFanPulse()) {
    if (!syncCompleted) {
      // 第一个有效脉冲 → 设为1号叶片
      currentBlade = 1;
      syncCompleted = true;
      lastPhaseChangeTime = millis(); // 同步成功后,初始化相位计时
      Serial.println("✅ 同步成功!当前叶片 = 1,开始自动相位循环");
    } else {
      currentBlade++;
      if (currentBlade > bladesPerRevolution) {
        currentBlade = 1;
      }
    }

    // 如果已同步且当前叶片在显示范围内,则点亮
    if (syncCompleted && isBladeInDisplayRange() && !isLightOn) {
      delayMicroseconds(triggerDelayUs);
      digitalWrite(lightPin, LOW);
      lightOnStartTime = micros();
      isLightOn = true;
    }
  }

  // 自动关灯
  if (isLightOn && (micros() - lightOnStartTime >= lightOnUs)) {
    digitalWrite(lightPin, HIGH);
    isLightOn = false;
  }
}



上一篇:从菲涅尔公式推导玻璃表面的4%反射率:正入射条件详解
下一篇:澜起科技赴港IPO融资63亿,专注内存互联芯片研发与数据中心应用
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 14:19 , Processed in 0.598007 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表