双通道点光源追踪系统

1.项目简介

本系统基于 FreeRTOS 实时操作系统架构构建。在感知层,利用 STM32F411 的 ADC 多路分时复用技术,结合 TIM2 定时器触发(100kHz) 与 DMA 直接存储器访问,实现了对双路光敏传感器信号的高频、低延迟同步采集。 在控制层,核心任务实时计算两路电压的差分信号作为误差反馈,通过带死区的比例控制算法动态调制 TIM3 的 PWM 占空比,驱动舵机执行机构实时修正姿态,从而构建了一个高响应速度的闭环光源追踪系统,实现对点光源的自动捕获与跟随。

**硬件环境:**stm32f411re单片机、嘉立创设计pcb板、SG90 舵机。

**核心技术点:**STM32F411 + FreeRTOS 系统调度、ADC 多通道 DMA 采集(100kHz 触发)、舵机 PWM 闭环控制、自定义 PCB 设计

核心原理

**物理感知:**利用光敏电阻在电路中的分压随光强变化的特性搭建检测前端。通过 STM32 内部 ADC 对传感器阵列(双路)进行高频电压采样,将光强信号转换为数字信号。

差分逻辑:采用差分电压比较算法。系统实时监测两路传感器的电压差值,当光源正对检测板时电压差为0;当光源移动导致照射角度偏移时,两路电压产生压差,该压差作为系统的误差反馈信号。

**闭环追踪:**基于误差信号驱动舵机执行机构。控制算法根据电压差的极性和大小,动态计算 PWM 脉宽,驱动舵机调整检测板角度,直至消除电压差,从而实现检测板始终正对光源的闭环追踪效果。

项目展示

2.系统整体设计

3.硬件设计

3.1原理图展示

3.2Pcb展示

3.3 PCB 设计细节与考量

(1)在电源输入端并联了 1000uF 电解电容100nF 陶瓷电容

1000uF 电解电容 用于滤除低频纹波和进行储能。由于项目中包含舵机(感性负载),舵机启动瞬间电流需求极大,会导致电源电压瞬间跌落。1000uF 电容充当"蓄水池",在电压跌落时迅速放电补充电流,维持电压稳定,防止单片机复位

100nF 陶瓷电容 用于滤除高频噪声。大电容由于卷绕工艺,存在较大的等效串联电感(ESL) ,对高频噪声(如 MCU 内部时钟切换产生的开关噪声)阻抗很大,无法过滤。而 100nF 陶瓷电容 ESL 很小,能为高频干扰提供一条通往地(GND)的低阻抗路径。在 PCB 布局时,这个小电容必须尽可能靠近 MCU 的电源引脚放置,否则线路寄生电感会使其失效。

(2)传感器对称布局

算法核心是"差分比较"(Delta V = V_{left} - V_{right})。对称布局保证了两个传感器的物理基准一致 ****。****避免因物理位置偏差导致差分算法出现静态误差。

(3)双面铺铜接地

PCB 上的铜皮也是有电阻的。舵机工作时电流很大,如果地线很细(阻抗大),会在地线上产生电压降(地弹,Ground Bounce)。ADC 采集是参考地电压的。如果地电压因为舵机动作而跳变,采集到的光强数据就会剧烈抖动。大面积铺铜可以将地线阻抗降到最低,保证 0V 参考电位的纯净。同时,双面铜皮构成的屏蔽层也增强了系统抗环境电磁干扰的能力。

(4)顶层(Top)放传感器,底层(Bottom)放元件

光敏电阻独立放置于 Top 层,其余阻容元件与连接器放置于 Bottom 层。这种设计不仅避免了元件阴影对光路的遮挡,也便于后续安装遮光筒以提高追踪灵敏度

4.软件设计 ------ 核心代码与思路

4.1软件逻辑

4.2核心代码

(1)左转右转代码

cpp 复制代码
void MotorTurnRight(uint16_t step)//应该 加
{
	
	if(pulsewide <= 500 - step) 
		
		{
        pulsewide += step;
    } 
	else 
		{
        pulsewide = 500; // 到达右极限
    }
		
		
		//******************************************************重置占空比
		//__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,pulsewide);    不需要这个,因为我已经在定时器设置的地方把那个常量改成pulsewide了
		
		__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulsewide);
		
		/*MX_TIM3_Init();
    HAL_TIM_Base_Start(&htim3);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);*/
		
}	


void MotorTurnLeft(uint16_t step)//应该减
{
	
	
		// 逻辑:左转需要减小 pulsewide
    // 边界检查:当前值是否比 (最小值 + 步长) 大
    // 如果是,可以安全减去 step;否则直接设为最小值,防止越界
	
	
	if(pulsewide >= 300 + step) 
		{
        pulsewide -= step; 
    } 
	
		else 
		{
        pulsewide = 300; // 到达左极限
    }
		
		__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, pulsewide);
		
		/*
		MX_TIM3_Init() 会重新配置定时器硬件、重置计数器,这会导致 PWM 波形中断、频率重置,舵机会剧烈抽搐甚至不动作。

正确的做法: 只需要修改定时器的**比较寄存器(CCR)**的值即可。
		
		
		MX_TIM3_Init();
    HAL_TIM_Base_Start(&htim3);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);*/
	
	
}	

(2)获取电压代码

cpp 复制代码
float Get_Channel1_ADC_Value()
{
      flag=0;
			//开启时钟tim2 和adc采样
			HAL_TIM_Base_Start(&htim2);
			HAL_ADC_Start_DMA(&hadc1,ADC_V,200);
		
	
       while(flag==0)
       {
			  osDelay(1);
			 }
				ADC_1_ave=0;
				ADC_2_ave=0;
			 for(int i=0;i<100;i++)
				{
					ADC_1_ave=(ADC_V[i*2]+ADC_1_ave);
					ADC_2_ave=(ADC_V[i*2+1]+ADC_2_ave);
				}
	        ADC_1_ave=ADC_1_ave/100;
					ADC_2_ave=ADC_2_ave/100;
				 // HAL_ADC_Stop(&hadc1);	
				return ((float)ADC_1_ave*3.3f/4095.0f);

}	

float Get_Channel2_ADC_Value()
{
return ((float)ADC_2_ave*3.3f/4095.0f);
}	

(3)中控代码

cpp 复制代码
void StartCoreTask(void *argument)
{
  /* USER CODE BEGIN StartCoreTask */
  /* Infinite loop */
  for(;;)
  {

		
		adc_chanel1_current=Get_Channel1_ADC_Value();//采样第1通道电压
		adc_chanel2_current=Get_Channel2_ADC_Value();//采样第2通道电压
		delta_v_currtnt=adc_chanel2_current-adc_chanel1_current;
	
	
		printf("adc_chanel1_current: %.3f \n",adc_chanel1_current);   //传回第1通道电压
		printf("adc_chanel2_current: %.3f \n",adc_chanel2_current);   //传回第2通道电压
		printf("delta_v: %.3f \n",delta_v_currtnt);  //电压差目前
		
		//*********************************************动态循环**********************************************************
		
		if (fabs(delta_v_currtnt) > 0.01) 
      {
          // 动态计算步长:电压差越大,转得越快
          // 限制步长最大值,防止一次转动过大导致超调
          step_temp = (uint16_t)(fabs(delta_v_currtnt) * 200); 
          if(step_temp > 50) step_temp = 50; // 增加一个限幅保护
          if(step_temp < 1) step_temp = 1;   // 保证至少动一下

          // 判断方向
          if (delta_v_currtnt > 0) 
          {
               // 如果通道2电压 > 通道1电压,往左转(假设逻辑)
               // 具体方向如果反了,把Left改成Right即可
               MotorTurnLeft(step_temp);
               printf("Turning Left, Step: %d\n", step_temp);
          }
          else 
          {
               MotorTurnRight(step_temp);
               printf("Turning Right, Step: %d\n", step_temp);
          }
      }
      
      osDelay(20); // 给控制循环一个时间间隔,不要太快,伺服电机响应需要时间
		
		
  }
  /* USER CODE END StartCoreTask */
}

frtos代码作为后续开发开源---可利用二值信号量作为一个开锁,添加OLED任务,或者同步开启上下两个维度的光源追踪,实现三维追踪光源的效果

关键代码解析

(1)舵机驱动逻辑

SG90 模拟舵机通过 PWM(脉冲宽度调制) 信号控制角度。它对信号有两个硬性要求:

频率固定:PWM 信号的周期必须是 20ms(即频率 50Hz)。

占空比决定角度:高电平持续的时间(脉宽)决定了舵机的转角。

首先,Prescaler (预分频系数) = 500 - 1;Counter Period (自动重装载值 ARR) = 4000;使得信号周期达到20ms从。随后通过改变ccr的值去改变高电平时间从而达到控制舵机旋转的功效。

0.5ms-> 0度(或者 -90度,视厂家定义);1.5ms ->90度(中位);2.5ms-> 180度(或者 +90度)

(2)电压线性对应代码: (float)ADC_1_ave*3.3f/4095.0f

ADC 的分辨率(Resolution)参考电压(Reference Voltage)的线性对应

STM32F411 内部集成的 ADC 是12位 逐次逼近型 ADC。能表示的数值范围是 0到 2^{12} - 1 = 4095。当输入电压为 0V 时,ADC 读数为 0;当输入电压为 3.3V 时,ADC 读数为 4095。

ADC 的工作本质是一个线性比例映射。假设当前采集到的数值为 X(即代码中的 ADC_1_ave),对应的实际电压为 V_{in}。加上 f 后缀(如 3.3f)强制编译器将其作为 float 类型(单精度,4字节)处理。对于 STM32F4 这类带有 FPU(浮点运算单元)的 MCU,使用 float 可以利用硬件加速,运算速度比 double 快几十倍。

(3)比例计算的舵机控制器----防止"帕金森抖动"

cpp 复制代码
if (fabs(delta_v_currtnt) > 0.01) 
      {
          // 动态计算步长:电压差越大,转得越快
          // 限制步长最大值,防止一次转动过大导致超调
          step_temp = (uint16_t)(fabs(delta_v_currtnt) * 200); 
          if(step_temp > 50) step_temp = 50; // 增加一个限幅保护
          if(step_temp < 1) step_temp = 1;   // 保证至少动一下

          // 判断方向
          if (delta_v_currtnt > 0) 
          {
               // 如果通道2电压 > 通道1电压,往左转(假设逻辑)
               // 具体方向如果反了,把Left改成Right即可
               MotorTurnLeft(step_temp);
               printf("Turning Left, Step: %d\n", step_temp);
          }
          else 
          {
               MotorTurnRight(step_temp);
               printf("Turning Right, Step: %d\n", step_temp);
          }
      }

只有当两个传感器的电压差绝对值超过 0.01V 时,才认为光照不平衡,开始调节。因为,ADC 采样总会有微小的底噪跳动(eg: 0.002V 的波动)。如果没有这行代码,舵机会因为这些噪点不停地微小抖动(像帕金森一样),不仅消耗电流,还会磨损齿轮。0.01V 就是系统的"容忍度"。在这个范围内,我们认为已经对准了。

步长 = 电压差 ×比例系数

delta_v 的单位是 伏特 (V) (通常在 0~3.3V 之间),而 step 的单位是 PWM 计数器数值 (Ticks)(需要 1~50 这样的整数)。

灵敏度调节200 是一个经验参数。它决定了系统的反应速度

eg :假设电压差是 0.1V。如果不乘 200:step = 0.1 -> 取整为 0 -> 舵机不动。乘了 200:step = 0.1 * 200 = 20 -> 舵机一次转动 20 个单位,反应迅速。

参数调试心得 :如果这个数太小(比如 50):追光很慢,迟钝;如果这个数太大(比如 1000):步长太大,稍微一点电压差舵机就猛转,容易转过头(超调),导致在光源附近左右摇摆(震荡)。200 是我经过调试找到的最佳平衡点。

5.踩坑记录与注意事项

(1)转动不是到300而是到平均点

在代码中,pulsewide = 300 对应 PWM 脉宽 1.5ms。对于 SG90 舵机来说,这是物理上的正中间(90度), 这是一个固定的、预设的 位置。不能误以为"复位"就是让舵机回到 300。但在寻光系统中,我们的目的不是 让舵机回正,而是让它追光 。是一个动态的、未知的位置。

**"平均点"**是指两个光敏电阻接收到的光照强度相等的那个瞬间。反映在电路里,就是 Voltage_Left == Voltage_Right,或者说 Delta_V ≈ 0。它完全取决于手电筒(光源)在哪里。

(2)舵机疯狂抖动

在控制循环中重复初始化定时器,每次调用 MX_TIM3_Init() 都会重置定时器的预分频器(Prescaler)和计数器(Counter),导致 PWM 波形输出中断。对于需要连续平滑信号的舵机来说,这是致命的。从而导致了舵机的抽动。初始化只能在 main 函数开头做一次。在循环中,应使用宏直接修改 CCR 寄存器。

(3)舵机安全保护---舵机初始值

SG90 舵机的物理转动范围是 0°~180°。在我们的定时器配置下(周期 20ms,ARR=4000),这个物理范围对应 PWM 寄存器(CCR)数值范围是 100 到 500

如果程序计算出的 CCR 值小于 100 或大于 500,舵机会试图转动到机械结构允许范围之外,导致电机堵转,电流激增,极易损坏舵机或烧毁单片机接口。

代码中实现了**"预判 + 钳位"**的双重保护逻辑:

当计算结果试图超出 [100, 500] 区间时,代码不仅阻止了越界,还执行了 else { pulsewide = Limit; }。这相当于在软件层面装了一堵墙。无论 PID 算法或差分算法算出多离谱的数值,最终输出给硬件的信号永远在安全区间内。

(4)pwm信号未开启

相关推荐
Coder_Boy_2 小时前
开源向量数据库比较(Chroma、Milvus、Faiss、Weaviate)
数据库·人工智能·spring boot·开源·milvus
贪玩成性2 小时前
TM1652驱动代码
单片机·mcu
GUET_一路向前2 小时前
STM32 MCU OTA升级办法1
stm32·单片机·嵌入式硬件
BingoGo2 小时前
前后端分离框架 CatchAdmin V5 beta.2 发布 插件化与开发效率的进一步提升
后端·开源·php
微风欲寻竹影2 小时前
STC89C52电子日历:12864 LCD+按键调时【附源码+Proteus仿真,免费】
单片机·嵌入式硬件·51单片机·proteus
恒锐丰小吕2 小时前
屹晶微 EG2113D 高压 600V 半桥 MOS 管驱动芯片技术解析
嵌入式硬件·硬件工程
半壶清水12 小时前
开源免费的在线考试系统online-exam-system部署方法
运维·ubuntu·docker·开源
一路往蓝-Anbo12 小时前
【第13期】中断机制详解 :从向量表到ISR
c语言·开发语言·stm32·单片机·嵌入式硬件
ArrebolJiuZhou12 小时前
00 arm开发环境的搭建
linux·arm开发·单片机·嵌入式硬件