STM32F103RCT6智能小车用CCD模块的循迹测评_ccd摄像头循迹小车-程序员宅基地

技术标签: stm32  嵌入式硬件  单片机  

前言:

之前在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演示

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

智能推荐

Docker 快速上手学习入门教程_docker菜鸟教程-程序员宅基地

文章浏览阅读2.5w次,点赞6次,收藏50次。官方解释是,docker 容器是机器上的沙盒进程,它与主机上的所有其他进程隔离。所以容器只是操作系统中被隔离开来的一个进程,所谓的容器化,其实也只是对操作系统进行欺骗的一种语法糖。_docker菜鸟教程

电脑技巧:Windows系统原版纯净软件必备的两个网站_msdn我告诉你-程序员宅基地

文章浏览阅读5.7k次,点赞3次,收藏14次。该如何避免的,今天小编给大家推荐两个下载Windows系统官方软件的资源网站,可以杜绝软件捆绑等行为。该站提供了丰富的Windows官方技术资源,比较重要的有MSDN技术资源文档库、官方工具和资源、应用程序、开发人员工具(Visual Studio 、SQLServer等等)、系统镜像、设计人员工具等。总的来说,这两个都是非常优秀的Windows系统镜像资源站,提供了丰富的Windows系统镜像资源,并且保证了资源的纯净和安全性,有需要的朋友可以去了解一下。这个非常实用的资源网站的创建者是国内的一个网友。_msdn我告诉你

vue2封装对话框el-dialog组件_<el-dialog 封装成组件 vue2-程序员宅基地

文章浏览阅读1.2k次。vue2封装对话框el-dialog组件_

MFC 文本框换行_c++ mfc同一框内输入二行怎么换行-程序员宅基地

文章浏览阅读4.7k次,点赞5次,收藏6次。MFC 文本框换行 标签: it mfc 文本框1.将Multiline属性设置为True2.换行是使用"\r\n" (宽字符串为L"\r\n")3.如果需要编辑并且按Enter键换行,还要将 Want Return 设置为 True4.如果需要垂直滚动条的话将Vertical Scroll属性设置为True,需要水平滚动条的话将Horizontal Scroll属性设_c++ mfc同一框内输入二行怎么换行

redis-desktop-manager无法连接redis-server的解决方法_redis-server doesn't support auth command or ismis-程序员宅基地

文章浏览阅读832次。检查Linux是否是否开启所需端口,默认为6379,若未打开,将其开启:以root用户执行iptables -I INPUT -p tcp --dport 6379 -j ACCEPT如果还是未能解决,修改redis.conf,修改主机地址:bind 192.168.85.**;然后使用该配置文件,重新启动Redis服务./redis-server redis.conf..._redis-server doesn't support auth command or ismisconfigured. try

实验四 数据选择器及其应用-程序员宅基地

文章浏览阅读4.9k次。济大数电实验报告_数据选择器及其应用

随便推点

灰色预测模型matlab_MATLAB实战|基于灰色预测河南省社会消费品零售总额预测-程序员宅基地

文章浏览阅读236次。1研究内容消费在生产中占据十分重要的地位,是生产的最终目的和动力,是保持省内经济稳定快速发展的核心要素。预测河南省社会消费品零售总额,是进行宏观经济调控和消费体制改变创新的基础,是河南省内人民对美好的全面和谐社会的追求的要求,保持河南省经济稳定和可持续发展具有重要意义。本文建立灰色预测模型,利用MATLAB软件,预测出2019年~2023年河南省社会消费品零售总额预测值分别为21881...._灰色预测模型用什么软件

log4qt-程序员宅基地

文章浏览阅读1.2k次。12.4-在Qt中使用Log4Qt输出Log文件,看这一篇就足够了一、为啥要使用第三方Log库,而不用平台自带的Log库二、Log4j系列库的功能介绍与基本概念三、Log4Qt库的基本介绍四、将Log4qt组装成为一个单独模块五、使用配置文件的方式配置Log4Qt六、使用代码的方式配置Log4Qt七、在Qt工程中引入Log4Qt库模块的方法八、获取示例中的源代码一、为啥要使用第三方Log库,而不用平台自带的Log库首先要说明的是,在平时开发和调试中开发平台自带的“打印输出”已经足够了。但_log4qt

100种思维模型之全局观思维模型-67_计算机中对于全局观的-程序员宅基地

文章浏览阅读786次。全局观思维模型,一个教我们由点到线,由线到面,再由面到体,不断的放大格局去思考问题的思维模型。_计算机中对于全局观的

线程间控制之CountDownLatch和CyclicBarrier使用介绍_countdownluach于cyclicbarrier的用法-程序员宅基地

文章浏览阅读330次。一、CountDownLatch介绍CountDownLatch采用减法计算;是一个同步辅助工具类和CyclicBarrier类功能类似,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。二、CountDownLatch俩种应用场景: 场景一:所有线程在等待开始信号(startSignal.await()),主流程发出开始信号通知,既执行startSignal.countDown()方法后;所有线程才开始执行;每个线程执行完发出做完信号,既执行do..._countdownluach于cyclicbarrier的用法

自动化监控系统Prometheus&Grafana_-自动化监控系统prometheus&grafana实战-程序员宅基地

文章浏览阅读508次。Prometheus 算是一个全能型选手,原生支持容器监控,当然监控传统应用也不是吃干饭的,所以就是容器和非容器他都支持,所有的监控系统都具备这个流程,_-自动化监控系统prometheus&grafana实战

React 组件封装之 Search 搜索_react search-程序员宅基地

文章浏览阅读4.7k次。输入关键字,可以通过键盘的搜索按钮完成搜索功能。_react search