STM32F103RCT6智能小车用CCD模块的循迹测评

前言:

之前在B站冲浪,看到了有智能小车采用CCD线性模块进行循迹功能。那它和红外循迹模块TCRT5000以及灰度循迹模块有什么不同呢?为了满足好奇心,也本着实践是唯一真理,我决定买回来试一下。


目录

前言:

硬件的安装:

软件部分:

最后总结:

演示视频:


硬件的安装:

模块介绍:

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

引脚说明:

        阵线性CCD模块(以下简称CCD模块),有5个引脚,模块使用的IIC通信,单片机通过采集AO引脚就可以接收到模块传来的数据。

引脚分配:

SI→PC3

CLK→PB3

AO→PA4

分配好引脚后,把模块安装上,就可以开始我们的循迹功能了。


软件部分:

我们要创建ccd.c和ccd.h,adc.c和adc.h四个页面,先初始化PC3和PB3引脚,去控制两个引脚对模块发送时序与它通信,最重要是获取中值,代码示例:

ccd.c

#include "ccd.h"
#include "adc.h"	
#include "string.h"
u8 ccd_adc[128]={0};
u8 SciBuf[200];  //存储上传到上位机的信息
int TIME_us=20; //曝光时间
void Ccd_Init(void)
{ 
	GPIO_InitTypeDef  GPIO_InitStructure;
 	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);	 //使能PA端口时钟
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;      //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;     //2M
	GPIO_Init(GPIOB, &GPIO_InitStructure);					      //根据设定参数初始化GPIOB 
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;      //推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;     //2M
	GPIO_Init(GPIOC, &GPIO_InitStructure);					      //根据设定参数初始化GPIOC
	
}


/******************************************************************************
***
* FUNCTION NAME: void Dly_us(int a) *
* CREATE DATE : 20170707 *
* CREATED BY : XJU *
* FUNCTION : 延时函数,控制曝光时间 *
* MODIFY DATE : NONE *
* INPUT : int *
* OUTPUT : NONE *
* RETURN : NONE *
*******************************************************************************
**/
void Dly_us(int a)
{
   int ii;    
   for(ii=0;ii<a;ii++);      
}

/******************************************************************************
***
* FUNCTION NAME: RD_TSL(void) *
* CREATE DATE : 20170707 *
* CREATED BY : XJU *
* FUNCTION : 按照时序依次读取CCD输出的模拟电压值 *
* MODIFY DATE : NONE *
* INPUT : void *
* OUTPUT : NONE *
* RETURN : NONE *
*******************************************************************************
**/
  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_4)/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;  //保存上一次的偏差	
		
}
	
int myabs(int a)
{ 		   
	  int temp;
		if(a<0) temp=-a;  
	  else temp=a;
	  return temp;
}


ccd.h中声明了定义的函数以及引脚

#ifndef __CCD_H
#define __CCD_H	 
#include "sys.h"
#include "delay.h"

#define TSL_SI    PCout(3)   //SI  C3
#define TSL_CLK   PBout(3)   //CLK B3
extern u8 CCD_Zhongzhi,CCD_Yuzhi;                 //线性CCD相关
extern u8 ccd_adc[128];
extern int TIME_us;
void Ccd_Init(void);
void Dly_us(int a);
void RD_TSL(void);
int myabs(int a);

		 				    
#endif

接下来定义ADC引脚

adc.c

#include "adc.h"
										   
void  Adc_Init(void)
{ 	
	ADC_InitTypeDef ADC_InitStructure; 
	GPIO_InitTypeDef GPIO_InitStructure;

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

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

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

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;			//12V电压检测
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;		//模拟输入引脚
	GPIO_Init(GPIOB, &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值
//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规则组的转换结果
}

/**************************************************************************
函数功能:读取电池电压 
入口参数:无
返回  值:电池电压 单位MV
**************************************************************************/
int Get_battery_volt(void)   
{  
	int Volt;//电池电压
	Volt=Get_Adc(Battery_Ch)*3.3*11*100/4096;	//电阻分压,具体根据原理图简单分析可以得到	
	return Volt;
}

adc.h

#ifndef __ADC_H
#define __ADC_H	 
#include "sys.h"
#include "delay.h"

#define Battery_Ch 8

														   
void  Adc_Init(void);
	  
//获得ADC值
//ch:通道值 0~3
u16 Get_Adc(u8 ch);
int Get_battery_volt(void); 
		 				    
#endif

初始化好了之后,在main.c主函数中调用RD_TSL()函数,单片机就会往模块中发送时序了,接下来我们再读取CCD_Zhongzhi  CCD中值就可以了。

因为线性CCD模块是128个光电二极管,但二极管扫描到黑线,黑线会把红外光吸收掉,越靠近黑线吸收的光就越多,所以就得到了我们的模拟量值,根据算法判断,我们就可以去除中值了,黑线在模块正中央时,CCD_Zhongzhi就等于64,黑线越往左就大于64,往右就小于64。知道原理后,我们只要简单用if判断,CCD_Zhongzhi>64我们就左转,CCD_Zhongzhi<64我们就右转,总结:线在那边,我们就往那边转(这么看如果运用PID和CCD结合起来的话,小车确实会丝滑无比)。


main.c

#include "stm32f10x.h"
#include "delay.h"
#include "motor.h"
#include "ccd.h"
#include "adc.h"

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

int main()
{
	delay_Init();
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
	JTAG_Set(JTAG_SWD_DISABLE);     //关闭JTAG接口
	JTAG_Set(SWD_ENABLE);           //打开SWD接口 可以利用主板的SWD接口调试
	
	delay_ms(500);					//=====延时等待系统稳定
	
	Adc_Init();  //ADC初始化
	Ccd_Init();   //CCD初始化
	TIME_us=20;    //设置曝光时间
		
	Motor_Init();
	Motor_PWM_Init(7199,0);
	
	while(1)
	{	
		RD_TSL();
		if(CCD_Zhongzhi == 64)
		{
			go();
		}
		if(CCD_Zhongzhi > 64)
		{
			turnleft();
		}
		if(CCD_Zhongzhi < 64)
		{
			turnright();
		}
	}
}

最后总结:

        通过对比CCD模块以及红外模块,行驶的效果其实区别不是很大。当然从简单角度来说,CCD线性模块确实更加简单易懂,再加入PID算法的话,小车会更加丝滑。当然还有一个就是省个IO口,但是我觉得没必要,不如8路红外。总之CCD模块更像是一把轮椅,给不会玩智能小车的人降低了门槛(但是这价格是真的降低了?)。红外循迹模块还是目前循迹最好,最低成本的通解(前提是 寻黑线 好像CCD也只能寻黑线?)。玩智能小车还是学K210视觉模块吧,或者上OPENMV也可以。


演示视频:

智能小车CCD循迹无PID演示

  • 16
    点赞
  • 141
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值