STM32版CCD线性摄像头寻线寻迹小车

基于STM32F103C8T6的CCD线性摄像头寻线寻迹小车


前言

目前大多数的小车寻线寻迹都是用红外对管寻线,这是比较简单也比较成熟的技术方案,且成本也低。本文将介绍使用CCD线性摄像头寻线寻迹。


一、模块介绍

TSL1401 线性传感器由一个 1x128 的光电二极管阵列、相关的电荷放大电路以及一个内部像素数据保功能组成。内部像素数据保功能可以为所有像素点提供同时积分的开始和停止时间。该阵列由 128 个像素组成,每个像素的感光面积为 3,524.3 平方微米。 像素之间的间隔为 8μm。内部控制逻辑简化了操作,该模块需要串行输入(SI)信号和时钟信号(CLK)。

在这里插入图片描述

二、使用说明

1.引脚说明

在这里插入图片描述
通过查看数据手册模块的管脚介绍如上

  1. AO: 信号输出(供单片机进行信号采集读取)
  2. CLK: 时钟信号
  3. GND: 地线
  4. SI: 信号输入(单片机向 CCD 发送指令)
  5. VDD: 电源(模块引脚标识为 VCC,接单片机逻辑电平,支持 3~5V)

2.其他

CCD 传感器是光学传感器,会受到环境光线的影响;程序中已经运用了动态阈值算法,尽量减小环境光线的影响,但是太暗或者太亮的环境下是不能正常工作的(一般室内正常光线可以运行)。

三、调试过程

在CCD调试助手中可以看出,当CCD摄像头扫描到黑线时,会出现一个凹槽,左右移动,凹槽也移动,脱离黑线凹槽消失。从这条线大概可以看出中值为多少。
在这里插入图片描述
通过串口助手打印出的中值结果如下:
在这里插入图片描述

四、寻线原理

小车寻线原理是通过 CCD 线性摄像头扫描黑线,摄像头扫描到 128 的像素点,中值为 64,扫描到黑线会得到一个二值化数据,用这个二值化数据减去中值64再除以 2,就得到小车偏离黑线的值(有正有负,如果为正,小车左转,如果为负,小车右转),舵机初始角度的值加上偏差的值就可以控小车沿着黑线跑了(数据是实时获取的,所以小车沿着黑线行驶不会卡方向)。

五、部分代码

1.CCD摄像头相关代码

首先对CCD摄像头初始化所用到的管脚初始化:

void Ccd_Init(void)
{ 
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	 //使能PA端口时钟
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;				 //PA.2 端口配置
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);					 //根据设定参数初始化GPIOA.2
	GPIO_SetBits(GPIOA,GPIO_Pin_2);						 //PA.2 输出高

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;	    		 //PA.3 端口配置, 推挽输出
	GPIO_Init(GPIOA, &GPIO_InitStructure);	  				 //推挽输出 ,IO口速度为50MHz
	GPIO_SetBits(GPIOA,GPIO_Pin_3); 						 //PA.7	输出高
}

获取中值:

void RD_TSL(void) 
{
	u8 i=0,tslp=0;
		
	static u8 j,Left,Right,Last_CCD_Zhongzhi;
	static u16 value1_max,value1_min;
		
	TSL_CLK=1;     //CLK引脚设为高电平          
	TSL_SI=0; 
	Dly_us(TIME_us);
				
	TSL_SI=1; 
	TSL_CLK=0;
	Dly_us(TIME_us);
				
	TSL_CLK=1;
	TSL_SI=0;
	Dly_us(TIME_us); 
	for(i=0;i<128;i++)
	{ 
		TSL_CLK=0; 
		Dly_us(TIME_us);  //调节曝光时间
		ccd_adc[tslp]=(u8)((float)Get_Adc(ADC_Channel_1)/4096*255);  //将读取到的电压值存入数组中
		++tslp;
		TSL_CLK=1;
		Dly_us(TIME_us);
	} 

	value1_max=ccd_adc[0];  //动态阈值算法,读取最大和最小值
	for(i=5;i<123;i++)   //两边各去掉5个点
	{
		if(value1_max<=ccd_adc[i])
		value1_max=ccd_adc[i];
	}
	value1_min=ccd_adc[0];  //最小值
	for(i=5;i<123;i++) 
	{
		if(value1_min>=ccd_adc[i])
		{
			value1_min=ccd_adc[i];				
		}
	 }
	 CCD_Yuzhi=(value1_max+value1_min)/2;	  //计算出本次中线提取的阈值
	 for(i = 5;i<118; i++)   //寻找左边跳变沿
	 {
		if(ccd_adc[i]>CCD_Yuzhi&&ccd_adc[i+1]>CCD_Yuzhi&&ccd_adc[i+2]>CCD_Yuzhi&&ccd_adc[i+3]<CCD_Yuzhi&&ccd_adc[i+4]<CCD_Yuzhi&&ccd_adc[i+5]<CCD_Yuzhi)
		{	
			Left=i;
			break;	
		}
	 }
	 for(j = 118;j>5; j--)//寻找右边跳变沿
	 {
		if(ccd_adc[j]<CCD_Yuzhi&&ccd_adc[j+1]<CCD_Yuzhi&&ccd_adc[j+2]<CCD_Yuzhi&&ccd_adc[j+3]>CCD_Yuzhi&&ccd_adc[j+4]>CCD_Yuzhi&&ccd_adc[j+5]>CCD_Yuzhi)
		{	
			Right=j;
			break;	
		}
	 }
	CCD_Zhongzhi=(Right+Left)/2;//计算中线位置
	if(myabs(CCD_Zhongzhi-Last_CCD_Zhongzhi)>70)   //计算中线的偏差,如果太大
	CCD_Zhongzhi=Last_CCD_Zhongzhi;    //则取上一次的值
	Last_CCD_Zhongzhi=CCD_Zhongzhi;  //保存上一次的偏差	
		
}

发送至上位机调试:

void sendToPc(void)
{ 
	int i;
	slove_data();
	printf("*");
	printf("LD");
	for(i=2;i<134;i++)
	{ 
		printf("%c",binToHex_high(SciBuf[i])); //以字符形式发送高4位对应的16进制
		printf("%c",binToHex_low(SciBuf[i]));  //以字符形式发送低?位对应的16进制
	}
	printf("00");   //通信协议要求
	printf("#");    //通信协议要求
}

2.ADC采集相关代码

ADC定义初始化:

void  Adc_Init(void)
{ 	
	ADC_InitTypeDef ADC_InitStructure; 
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1	, ENABLE );	  //使能ADC1通道时钟
 

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);   //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M

	//PA1 作为模拟通道输入引脚                         
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;		//模拟输入引脚
	GPIO_Init(GPIOA, &GPIO_InitStructure);	

	ADC_DeInit(ADC1);  //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值

	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//ADC工作模式:ADC1和ADC2工作在独立模式
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;	//模数转换工作在单通道模式
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;	//模数转换工作在单次转换模式
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//转换由软件而不是外部触发启动
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//ADC数据右对齐
	ADC_InitStructure.ADC_NbrOfChannel = 1;	//顺序进行规则转换的ADC通道的数目
	ADC_Init(ADC1, &ADC_InitStructure);	//根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器   

  
	ADC_Cmd(ADC1, ENABLE);	//使能指定的ADC1
	
	ADC_ResetCalibration(ADC1);	//使能复位校准  
	 
	while(ADC_GetResetCalibrationStatus(ADC1));	//等待复位校准结束
	
	ADC_StartCalibration(ADC1);	 //开启AD校准
 
	while(ADC_GetCalibrationStatus(ADC1));	 //等待校准结束
 
//	ADC_SoftwareStartConvCmd(ADC1, ENABLE);		//使能指定的ADC1的软件转换启动功能

}

获取ADC值:

//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)   
{
  	//设置指定ADC的规则组通道,一个序列,采样时间
	ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 );	//ADC1,ADC通道,采样时间为239.5周期	  			    
  
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);		//使能指定的ADC1的软件转换启动功能	
	 
	while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束

	return ADC_GetConversionValue(ADC1);	//返回最近一次ADC1规则组的转换结果
}

3.主函数代码

u8 CCD_Zhongzhi=64,CCD_Yuzhi;                 //线性CCD相关

int main(void)
{	 
	int piancha=0,jiaodu;
	delay_init();	    	 //延时函数初始化	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
	uart_init(115200);	 //串口初始化为115200 
		
	Adc_Init();	//ADC初始化
	Ccd_Init(); //CCD初始化
	TIME_us=1;  //设置曝光时间
	 
	TIM1_PWM_Init(7199,0);   	//=====初始化PWM 10KHZ,用于驱动电机。 
	Motor_Init();				//=====初始化与电机连接的硬件IO接口
	SERVO_Init();               //=====舵机PWM定时器3, 初始占空比7.5%	 
		
	while(1)
	{  
//		sendToPc();   //发送信息至上位机
		RD_TSL();		//获取中值
		printf("%d\n",CCD_Zhongzhi);
		piancha = (CCD_Zhongzhi-64)/3;
		jiaodu = 75+piancha;
		SERVO_Angle_Control(jiaodu);
		go();
	}
}

六、小车硬件

主控:STM32F103C8T6
摄像头:线性CCD( TSL1401CL)
电机驱动:TB6612FNG
舵机一个
电机两个
LM2596S稳压模块
7.4V航模锂电池

七、演示视频

CCD摄像头寻线寻迹小车


总结

用CCD线性摄像头寻迹寻线小车跑起来更稳更丝滑,跑黑线的类型也多一点。参考:线性CCD基础学习

版权声明:本文为CSDN博主「Gxust_Veneno」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Gxust_Veneno/article/details/119797411

基于STM32F103C8T6的CCD线性摄像头寻线寻迹小车


前言

目前大多数的小车寻线寻迹都是用红外对管寻线,这是比较简单也比较成熟的技术方案,且成本也低。本文将介绍使用CCD线性摄像头寻线寻迹。


一、模块介绍

TSL1401 线性传感器由一个 1x128 的光电二极管阵列、相关的电荷放大电路以及一个内部像素数据保功能组成。内部像素数据保功能可以为所有像素点提供同时积分的开始和停止时间。该阵列由 128 个像素组成,每个像素的感光面积为 3,524.3 平方微米。 像素之间的间隔为 8μm。内部控制逻辑简化了操作,该模块需要串行输入(SI)信号和时钟信号(CLK)。

在这里插入图片描述

二、使用说明

1.引脚说明

在这里插入图片描述
通过查看数据手册模块的管脚介绍如上

  1. AO: 信号输出(供单片机进行信号采集读取)
  2. CLK: 时钟信号
  3. GND: 地线
  4. SI: 信号输入(单片机向 CCD 发送指令)
  5. VDD: 电源(模块引脚标识为 VCC,接单片机逻辑电平,支持 3~5V)

2.其他

CCD 传感器是光学传感器,会受到环境光线的影响;程序中已经运用了动态阈值算法,尽量减小环境光线的影响,但是太暗或者太亮的环境下是不能正常工作的(一般室内正常光线可以运行)。

三、调试过程

在CCD调试助手中可以看出,当CCD摄像头扫描到黑线时,会出现一个凹槽,左右移动,凹槽也移动,脱离黑线凹槽消失。从这条线大概可以看出中值为多少。
在这里插入图片描述
通过串口助手打印出的中值结果如下:
在这里插入图片描述

四、寻线原理

小车寻线原理是通过 CCD 线性摄像头扫描黑线,摄像头扫描到 128 的像素点,中值为 64,扫描到黑线会得到一个二值化数据,用这个二值化数据减去中值64再除以 2,就得到小车偏离黑线的值(有正有负,如果为正,小车左转,如果为负,小车右转),舵机初始角度的值加上偏差的值就可以控小车沿着黑线跑了(数据是实时获取的,所以小车沿着黑线行驶不会卡方向)。

五、部分代码

1.CCD摄像头相关代码

首先对CCD摄像头初始化所用到的管脚初始化:

void Ccd_Init(void)
{ 
	GPIO_InitTypeDef  GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	 //使能PA端口时钟
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;				 //PA.2 端口配置
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 //IO口速度为50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);					 //根据设定参数初始化GPIOA.2
	GPIO_SetBits(GPIOA,GPIO_Pin_2);						 //PA.2 输出高

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;	    		 //PA.3 端口配置, 推挽输出
	GPIO_Init(GPIOA, &GPIO_InitStructure);	  				 //推挽输出 ,IO口速度为50MHz
	GPIO_SetBits(GPIOA,GPIO_Pin_3); 						 //PA.7	输出高
}

获取中值:

void RD_TSL(void) 
{
	u8 i=0,tslp=0;
		
	static u8 j,Left,Right,Last_CCD_Zhongzhi;
	static u16 value1_max,value1_min;
		
	TSL_CLK=1;     //CLK引脚设为高电平          
	TSL_SI=0; 
	Dly_us(TIME_us);
				
	TSL_SI=1; 
	TSL_CLK=0;
	Dly_us(TIME_us);
				
	TSL_CLK=1;
	TSL_SI=0;
	Dly_us(TIME_us); 
	for(i=0;i<128;i++)
	{ 
		TSL_CLK=0; 
		Dly_us(TIME_us);  //调节曝光时间
		ccd_adc[tslp]=(u8)((float)Get_Adc(ADC_Channel_1)/4096*255);  //将读取到的电压值存入数组中
		++tslp;
		TSL_CLK=1;
		Dly_us(TIME_us);
	} 

	value1_max=ccd_adc[0];  //动态阈值算法,读取最大和最小值
	for(i=5;i<123;i++)   //两边各去掉5个点
	{
		if(value1_max<=ccd_adc[i])
		value1_max=ccd_adc[i];
	}
	value1_min=ccd_adc[0];  //最小值
	for(i=5;i<123;i++) 
	{
		if(value1_min>=ccd_adc[i])
		{
			value1_min=ccd_adc[i];				
		}
	 }
	 CCD_Yuzhi=(value1_max+value1_min)/2;	  //计算出本次中线提取的阈值
	 for(i = 5;i<118; i++)   //寻找左边跳变沿
	 {
		if(ccd_adc[i]>CCD_Yuzhi&&ccd_adc[i+1]>CCD_Yuzhi&&ccd_adc[i+2]>CCD_Yuzhi&&ccd_adc[i+3]<CCD_Yuzhi&&ccd_adc[i+4]<CCD_Yuzhi&&ccd_adc[i+5]<CCD_Yuzhi)
		{	
			Left=i;
			break;	
		}
	 }
	 for(j = 118;j>5; j--)//寻找右边跳变沿
	 {
		if(ccd_adc[j]<CCD_Yuzhi&&ccd_adc[j+1]<CCD_Yuzhi&&ccd_adc[j+2]<CCD_Yuzhi&&ccd_adc[j+3]>CCD_Yuzhi&&ccd_adc[j+4]>CCD_Yuzhi&&ccd_adc[j+5]>CCD_Yuzhi)
		{	
			Right=j;
			break;	
		}
	 }
	CCD_Zhongzhi=(Right+Left)/2;//计算中线位置
	if(myabs(CCD_Zhongzhi-Last_CCD_Zhongzhi)>70)   //计算中线的偏差,如果太大
	CCD_Zhongzhi=Last_CCD_Zhongzhi;    //则取上一次的值
	Last_CCD_Zhongzhi=CCD_Zhongzhi;  //保存上一次的偏差	
		
}

发送至上位机调试:

void sendToPc(void)
{ 
	int i;
	slove_data();
	printf("*");
	printf("LD");
	for(i=2;i<134;i++)
	{ 
		printf("%c",binToHex_high(SciBuf[i])); //以字符形式发送高4位对应的16进制
		printf("%c",binToHex_low(SciBuf[i]));  //以字符形式发送低?位对应的16进制
	}
	printf("00");   //通信协议要求
	printf("#");    //通信协议要求
}

2.ADC采集相关代码

ADC定义初始化:

void  Adc_Init(void)
{ 	
	ADC_InitTypeDef ADC_InitStructure; 
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1	, ENABLE );	  //使能ADC1通道时钟
 

	RCC_ADCCLKConfig(RCC_PCLK2_Div6);   //设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M

	//PA1 作为模拟通道输入引脚                         
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;		//模拟输入引脚
	GPIO_Init(GPIOA, &GPIO_InitStructure);	

	ADC_DeInit(ADC1);  //复位ADC1,将外设 ADC1 的全部寄存器重设为缺省值

	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;	//ADC工作模式:ADC1和ADC2工作在独立模式
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;	//模数转换工作在单通道模式
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;	//模数转换工作在单次转换模式
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//转换由软件而不是外部触发启动
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//ADC数据右对齐
	ADC_InitStructure.ADC_NbrOfChannel = 1;	//顺序进行规则转换的ADC通道的数目
	ADC_Init(ADC1, &ADC_InitStructure);	//根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器   

  
	ADC_Cmd(ADC1, ENABLE);	//使能指定的ADC1
	
	ADC_ResetCalibration(ADC1);	//使能复位校准  
	 
	while(ADC_GetResetCalibrationStatus(ADC1));	//等待复位校准结束
	
	ADC_StartCalibration(ADC1);	 //开启AD校准
 
	while(ADC_GetCalibrationStatus(ADC1));	 //等待校准结束
 
//	ADC_SoftwareStartConvCmd(ADC1, ENABLE);		//使能指定的ADC1的软件转换启动功能

}

获取ADC值:

//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch)   
{
  	//设置指定ADC的规则组通道,一个序列,采样时间
	ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 );	//ADC1,ADC通道,采样时间为239.5周期	  			    
  
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);		//使能指定的ADC1的软件转换启动功能	
	 
	while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束

	return ADC_GetConversionValue(ADC1);	//返回最近一次ADC1规则组的转换结果
}

3.主函数代码

u8 CCD_Zhongzhi=64,CCD_Yuzhi;                 //线性CCD相关

int main(void)
{	 
	int piancha=0,jiaodu;
	delay_init();	    	 //延时函数初始化	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
	uart_init(115200);	 //串口初始化为115200 
		
	Adc_Init();	//ADC初始化
	Ccd_Init(); //CCD初始化
	TIME_us=1;  //设置曝光时间
	 
	TIM1_PWM_Init(7199,0);   	//=====初始化PWM 10KHZ,用于驱动电机。 
	Motor_Init();				//=====初始化与电机连接的硬件IO接口
	SERVO_Init();               //=====舵机PWM定时器3, 初始占空比7.5%	 
		
	while(1)
	{  
//		sendToPc();   //发送信息至上位机
		RD_TSL();		//获取中值
		printf("%d\n",CCD_Zhongzhi);
		piancha = (CCD_Zhongzhi-64)/3;
		jiaodu = 75+piancha;
		SERVO_Angle_Control(jiaodu);
		go();
	}
}

六、小车硬件

主控:STM32F103C8T6
摄像头:线性CCD( TSL1401CL)
电机驱动:TB6612FNG
舵机一个
电机两个
LM2596S稳压模块
7.4V航模锂电池

七、演示视频

CCD摄像头寻线寻迹小车


总结

用CCD线性摄像头寻迹寻线小车跑起来更稳更丝滑,跑黑线的类型也多一点。参考:线性CCD基础学习

版权声明:本文为CSDN博主「Gxust_Veneno」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Gxust_Veneno/article/details/119797411

生成海报
点赞 0

Gxust_Veneno

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

OV7670摄像头模块资料

OV7670摄像头模块资料 一、实物图和原理图 二、模块简介 OV7670 是 OV( OmniVision)公司生产的 CMOS VGA 图像传感器。该传感器体积小、工作电压低,提供单片 VGA

TCRT5000循迹模块原理及应用

前言 本文将讲述TCRT5000循迹模块的原理及应用。本文应用于STM32,对于使用循迹模块的你有一定的帮助。 以下是本篇文章的正文内容 一、TCRT5000循迹模块介绍 TCRT5000就是一个红外发射和接收器&#xff0