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

智能推荐

机器学习模型评分总结(sklearn)_model.score-程序员宅基地

文章浏览阅读1.5w次,点赞10次,收藏129次。文章目录目录模型评估评价指标1.分类评价指标acc、recall、F1、混淆矩阵、分类综合报告1.准确率方式一:accuracy_score方式二:metrics2.召回率3.F1分数4.混淆矩阵5.分类报告6.kappa scoreROC1.ROC计算2.ROC曲线3.具体实例2.回归评价指标3.聚类评价指标1.Adjusted Rand index 调整兰德系数2.Mutual Informa..._model.score

Apache虚拟主机配置mod_jk_apache mod_jk 虚拟-程序员宅基地

文章浏览阅读344次。因工作需要,在Apache上使用,重新学习配置mod_jk1. 分别安装Apache和Tomcat:2. 编辑httpd-vhosts.conf: LoadModule jk_module modules/mod_jk.so #加载mod_jk模块 JkWorkersFile conf/workers.properties #添加worker信息 JkLogFil_apache mod_jk 虚拟

Android ConstraintLayout2.0 过度动画MotionLayout MotionScene3_android onoffsetchanged-程序员宅基地

文章浏览阅读335次。待老夫kotlin大成,扩展:MotionLayout 与 CoordinatorLayout,DrawerLayout,ViewPager 的 交互众所周知,MotionLayout 的 动画是有完成度的 即Progress ,他在0-1之间变化,一.CoordinatorLayout 与AppBarLayout 交互时,其实就是监听 offsetliner 这个 偏移量的变化 同样..._android onoffsetchanged

【转】多核处理器的工作原理及优缺点_多核处理器怎么工作-程序员宅基地

文章浏览阅读8.3k次,点赞3次,收藏19次。【转】多核处理器的工作原理及优缺点《处理器关于多核概念与区别 多核处理器工作原理及优缺点》原文传送门  摘要:目前关于处理器的单核、双核和多核已经得到了普遍的运用,今天我们主要说说关于多核处理器的一些相关概念,它的工作与那里以及优缺点而展开的分析。1、多核处理器  多核处理器是指在一枚处理器中集成两个或多个完整的计算引擎(内核),此时处理器能支持系统总线上的多个处理器,由总..._多核处理器怎么工作

个人小结---eclipse/myeclipse配置lombok_eclispe每次运行个新项目都需要重新配置lombok吗-程序员宅基地

文章浏览阅读306次。1. eclipse配置lombok 拷贝lombok.jar到eclipse.ini同级文件夹下,编辑eclipse.ini文件,添加: -javaagent:lombok.jar2. myeclipse配置lombok myeclipse像eclipse配置后,定义对象后,直接访问方法,可能会出现飘红的报错。 如果出现报错,可按照以下方式解决。 ..._eclispe每次运行个新项目都需要重新配置lombok吗

【最新实用版】Python批量将pdf文本提取并存储到txt文件中_python批量读取文字并批量保存-程序员宅基地

文章浏览阅读1.2w次,点赞31次,收藏126次。#注意:笔者在2021/11/11当天调试过这个代码是可用的,由于pdfminer版本的更新,网络上大多数的语法没有更新,我也是找了好久的文章才修正了我的代码,仅供学习参考。1、把pdf文件移动到本代码文件的同一个目录下,笔者是在pycharm里面运行的项目,下图中的x1文件夹存储了我需要转换成文本文件的所有pdf文件。然后要在此目录下创建一个存放转换后的txt文件的文件夹,如图中的txt文件夹。2、编写代码 (1)导入所需库# coding:utf-8import ..._python批量读取文字并批量保存

随便推点

Scala:访问修饰符、运算符和循环_scala ===运算符-程序员宅基地

文章浏览阅读1.4k次。http://blog.csdn.net/pipisorry/article/details/52902234Scala 访问修饰符Scala 访问修饰符基本和Java的一样,分别有:private,protected,public。如果没有指定访问修饰符符,默认情况下,Scala对象的访问级别都是 public。Scala 中的 private 限定符,比 Java 更严格,在嵌套类情况下,外层_scala ===运算符

MySQL导出ER图为图片或PDF_数据库怎么导出er图-程序员宅基地

文章浏览阅读2.6k次,点赞7次,收藏19次。ER图导出为PDF或图片格式_数据库怎么导出er图

oracle触发器修改同一张表,oracle触发器中对同一张表进行更新再查询时,需加自制事务...-程序员宅基地

文章浏览阅读655次。CREATE OR REPLACE TRIGGER Trg_ReimFactBEFORE UPDATEON BP_OrderFOR EACH ROWDECLAREPRAGMA AUTONOMOUS_TRANSACTION;--自制事务fc varchar2(255);BEGINIF ( :NEW.orderstate = 2AND :NEW.TransState = 1 ) THENBEG..._oracle触发器更新同一张表

debounce与throttle区别及其应用场景_throttle和debounce应用在哪些场景-程序员宅基地

文章浏览阅读513次。目录概念debouncethrottle实现debouncethrottle应用场景debouncethrottle场景举例debouncethrottle概念debounce字面理解是“防抖”,何谓“防抖”,就是连续操作结束后再执行,以网页滚动为例,debounce要等到用户停止滚动后才执行,将连续多次执行合并为一次执行。throttle字面理解是“节流”,何谓“节流”,就是确保一段时..._throttle和debounce应用在哪些场景

java操作mongdb【超详细】_java 操作mongodb-程序员宅基地

文章浏览阅读526次。regex() $regex 正则表达式用于模式匹配,基本上是用于文档中的发现字符串 (下面有例子)注意:若未加 @Field("名称") ,则识别mongdb集合中的key名为实体类属性名。也可以对数组进行索引,如果被索引的列是数组时,MongoDB会索引这个数组中的每一个元素。也可以对整个Document进行索引,排序是预定义的按插入BSON数据的先后升序排列。save: 若新增数据的主键已经存在,则会对当前已经存在的数据进行修改操作。_java 操作mongodb

github push 推送代码失败. 使用ssh rsa key. remote: Support for password authentication was removed._git push remote: support for password authenticati-程序员宅基地

文章浏览阅读1k次。今天push代码到github仓库时出现这个报错TACKCHEN-MB0:tc-image tackchen$ git pushremote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.remote: Please see https://github.blog/2020-12-15-token-authentication_git push remote: support for password authentication was removed on august 1