基于Arduino控制器搭建的风扇频闪系统,可通过简单硬件组合实现静态固定与动态循环两种频闪效果,兼具低成本、易调试、实用性强等特点,对于物理、光电、仪器、测控等专业是一个非常好的闭环综合实验。本文将从硬件构成、工作原理、模式特性及调试要点等方面,系统拆解该系统的实现逻辑。
实验原理
物体在快速运动时,当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1~0.4秒左右,这种现象被称为视觉暂留现象。频闪效应就是通过视觉暂留现象实现的,通过周期性闪烁的光源照射运动的物体,物体每次运动到某一固定位置时恰好被照亮,被照亮的物体的影像会在眼中停留0.1~0.4秒,所以会看到运动的物体“静止”在某处的现象。频闪现象对光源闪烁的稳定性要求较高,如果光源闪烁的频率不固定,会有多个“物体”叠加在一起的现象,不能观察到清晰的像。
核心硬件构成(双模式通用)
- 主控单元:Arduino UNO,承担信号采集、算法运算及执行器控制核心任务,保障系统时序精度。
- 位置检测单元:风扇切割扇叶后的红色激光器,光电探测器用于采集叶片位置信号和风扇转速信息,通过模拟量变化反馈叶片是否经过检测点。
- 执行单元:白光LED模块,采用低电平触发模式,响应控制器指令实现精准亮灭,作为频闪视觉输出载体。
- 载体单元:9叶电脑风扇,叶片数量为相位定位提供基准,旋转过程中形成周期性位置触发信号,保障频闪效果稳定性。
- 辅助:杜邦线若干、USB拓展坞一个,BNC转鳄鱼夹线,SMA转鳄鱼夹线,不同型号固定支撑座若干,立柱若干,光学平台。
整体硬件是多轴笼式结构搭建而成,结构如下图所示。

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

双模式功能特性与实现
模式一:手动固定频闪(静态相位模式)
功能特性:采用相位锁定机制,通过串口指令标定叶片基准后,手动指定目标叶片编号,光源仅在该叶片经过检测点时触发频闪,频闪相位固定不变,适用于静态标识展示、定点亮化等场景。
调试流程:
- 将程序烧录至Arduino控制器,打开串口监视器并设置波特率为115200,等待系统初始化完成。
- 缓慢转动风扇,将待标定为“1号”的叶片对准光电探测器检测点,通过串口发送指令
s 完成叶片基准同步。
- 发送1-9范围内的数字指令,设定目标频闪叶片,系统自动锁定该叶片相位,实现定点频闪,相位变更需重新发送数字指令。
核心优势:相位锁定精度高,无漂移现象,调试流程简洁,对硬件时序稳定性要求较低。
模式二:自动循环频闪(动态相位模式)
功能特性:在静态模式基础上引入非阻塞式时序调度机制,叶片基准同步完成后,系统按1秒固定间隔(可自定义)自动切换频闪相位,相位递进方向与风扇旋转方向自适应匹配,呈现动态流动光效,适用于氛围亮化、动态演示等场景。
功能亮点:
- 手动干预兼容:支持通过串口数字指令直接重置频闪相位,重置后自动延续循环逻辑,兼顾手动控制与自动运行需求。
- 时序稳定性:采用
millis()函数实现非阻塞计时,避免计时逻辑阻塞叶片检测与防抖流程,保障频闪与相位切换精准度。
- 方向自适应:通过程序参数配置(
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;
}
}
|