一文学会制作11路数字量红外巡线小车!!!

目录

导言

一、小车概述

二、基础部分硬件概述

2.1、小车的眼睛——TCRT5000循迹传感器

2.2、小车的方向盘——舵机模块SG90/MG90

2.3、小车的腿——直流减速电机(TT马达)

2.4、小车的大脑——STM32F103C8T6最小系统板

2.5、小车的动力保障——L298N驱动模块

2.6、小车的能源——电源模块以及升降压模块

2.7、小车的血管——导线和开关

2.8、推荐基础器件清单 

2.9、推荐STM32F103C8T6最小系统板引脚资源分配

2.10、推荐接线图

三、基础部分软件概述

3.1、巡线原理概述

3.2、代码实现步骤概述

3.3、初始化系统时钟

3.2、设置中断分组

3.3、初始化延时函数

3.4、初始化板载LED灯

3.5、初始化电机PWM输出引脚

3.6、初始化红外传感器模块

3.7、初始化舵机模块

3.8、初始化中断

3.9、小车逻辑功能实现

3.9.1、实现红外扫描读取函数的封装和处理

3.9.2、 舵机转向控制

3.9.3、电机的差速控制辅助转向

四、调试部分

五、代码模板下载

导言

大家好,本期为大家带来校园比赛的一个项目分享:11路数字量红外巡线小车

本分享的元器件仅推荐使用,在调试过程中可以更换更好的元器件模块。

本文仅写到入门处理,过程中有一些知识如PWM等需要自行学习,更多功能可以自己添加完善。

本文以学习为主,积极探索。

希望大家可以给我多提提意见,感谢每个人的支持!

一、小车概述

这是我初始做的9路数字量红外巡线小车

图1 9路数字量红外巡线小车样例图

二、基础部分硬件概述

2.1、小车的眼睛——TCRT5000循迹传感器

实现巡线的必不可少的部分,通过单片机读取模块的OUT引脚,我们可以得到高低电平信号,根据检测到的信号进行分情况处理,就能实现我们的循迹处理。

这里推荐淘宝的如下图的模块

 

 图2 TCRT5000循迹传感器样例图

2.2、小车的方向盘——舵机模块SG90/MG90

舵机能有助于小车转弯,能让小车调节更加简单,这里推荐SG90进行入门,因为其价格更加便宜,不用担心一下烧坏损失太多。

注意!:

1.舵机需要外部5V的供电,直接使用STM32F103C8T6最小系统板上的5V供电可能不够用。

2.舵机有一个死区电压,一般使用PWM驱动时需要注意这个电压。

  图3 SG90舵机模块样例图

2.3、小车的腿——直流减速电机(TT马达)

没有好的腿怎么走得快呢,在装电机前请记得检测电机的转向,不然一条腿向东一条腿向西边跑了,跑不快的哦。

电机在高速转动时电流较大,这个时候我们可以更换电机自带的两根线,换更粗的上去,增大载流量。

  图4 TT马达模块样例图

2.4、小车的大脑——STM32F103C8T6最小系统板

这个是我们用来实现全部思想的核心,开发时推荐使用STM32F103C8T6最小系统板,其单价便宜,可拆卸方便,开发前需要准备和了解其资源分布,引脚映射功能等,并且在使用时需要配备st-link等的烧录器进行代码的烧写。

使用时需要注意:

1、接线的正负极,

2、注意使用的IO口的电压电流,防止烧坏芯片或者烧坏IO引脚。

图5 STM32F103C8T6最小系统板样例图和资源分配图

图6 st-link烧录器模块样例

2.5、小车的动力保障——L298N驱动模块

L298N能实现两个电机的正反转控制和速度控制,自身也配备了5V降压模块,但是其需要12V的电源输入,也需要和其他模块共地。

由于我们的小车一般不用倒车功能,可以将其4个接口中的两个接口直接给低电平,另外两个给PWM输入,即可通过程序控制电机固定方向和给定速度前进。

 

 图7 L298N电机驱动模块示意图

2.6、小车的能源——电源模块以及升降压模块

2.6.1、电源模块

小车在使用时需要消耗的电量比较大,可以考虑使用三节18650的电池盒,但是如果考虑车的整体重量,使用两节18650电池盒就足够了。电池盒的自带的线比较细,可以自行更换较大的线。

 图8 18650电池模块示意图

2.6.2、升压模块

为给L298N驱动模块稳定供电,我们需要配备一个升压模块,升压模块有输入端int和输出端out,注意的是正负级不能接反,要先将电池盒的输出接入到升压模块输入端,然后将电压表调节到直流电压档(注意量程),检测输出端电压,然后旋转电位器,将输出电压调节到12V。

 图9 XL6009 DC-DC升压电源模块样例图

2.6.3、降压模块

小车的芯片、舵机等模块都需要稳定的供电,电池盒输出出来的电压太大,我们需要进行降压稳压操作,同样,降压模块有输入端int和输出端out。

注意:

正负级不能接反,要先将电池盒的输出接入到降压模块输入端,然后将电压表调节到直流电压档(注意量程),检测输出端电压,然后旋转电位器,将输出电压调节到5V。

 图10 LM2596S DC-DC降压电源模块示意图

2.7、小车的血管——导线和开关

2.7.1、导线

        可以买一些杜邦线和一些口径相对原来的大一些的导线。

2.7.2、开关

        开关能方便我们启动车辆,节约电量,保护电路等操作。

2.8、推荐基础器件清单 

模块名称数量
TCRT5000循迹传感器11
舵机模块SG901
直流减速电机(TT马达+轮子)2
STM32F103C8T6最小系统板1
st-link烧录器1
L298N驱动模块1
18650电池2
18650两节电池盒1
XL6009 DC-DC升压电源模块

1

LM2596S DC-DC降压电源模块

1

杜邦线、导线

若干
开关1
1寸定向轮1

2.9、推荐STM32F103C8T6最小系统板引脚资源分配

模块名称引脚号
舵机PB6
红外传感器PA0,PA1,PA2,PA3,PA4,PA5,PA6,PA7,PB9,PB10,PB11
左电机PB0
右电机PB1

2.10、推荐接线图

画工不精,请忍住别笑!

 图11 推荐接线样例图

三、基础部分软件概述

3.1、巡线原理概述

我们知道,舵机可以根据输出不同的PWM值进行不同位置的转向,在使用时我们先调试让舵机带的定向轮方向跟黑线方向平行,记录这个时候的PWM值为DUOJIZHONGZHI。

在这个中值的基础上进行加减,就能实现舵机的转向,那如何知道舵机什么时候加减呢,这个时候就要依靠我们的数字红外传感器了,我们可以像如图一样将下面红外检测到黑线时给我们的error变量进行赋值(这个赋值可以根据车模具体情况来使用)。

图12 红外传感器对应的误差赋值图

当黑线在对应传感器位置下时,对error变量进行赋值,但是这个error的值比较小,直接使用不足以让舵机转动,所以我们可以再定义一个变量为DUOJI=100,(这个100的值需要根据具体车模和舵机的情况进行调节),我们可以得到舵机转向的PWM计算公式为

DjPwm = DUOJIZHONGZHI + DUOJI * error;

注意:如果发现舵机转向不对,只需要更改 “+” 号变为 “-” 号

电机的差速也有利于转弯,可以模仿舵机的PWM计算方法运用到电机上,会使转弯更加丝滑。

3.2、代码实现步骤概述

初始化系统时钟

设置中断分组

初始化延时函数

初始化板载LED灯

初始化TTL电机PWM输出引脚

初始化红外传感器模块

初始化舵机模块

初始化中断

小车逻辑功能实现

3.3、初始化系统时钟

初始化时钟是代码必要的初始化步骤,直接调用库函数自带的SystemInit();函数即可

代码示例如下:

	/* 系统时钟的初始化 */
	SystemInit();

3.2、设置中断分组

设置中断分组的目的是为了管理优先级,把一些检测代码和控制代码放进中断服务函数里能够使数据等更加精确。

我们直接调用库函数里面的NVIC_PriorityGroupConfig();函数即可

代码示例如下:

	/* 设置中断分组,也就是程序先后执行的优先级 */
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

3.3、初始化延时函数

延时函数是我们进行代码调试的一个有利工具,这个模块是我们自己添加的模块

添加模块的delay.c代码如下

#include "delay.h"
// 	 
//如果需要使用OS,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h"					//ucos 使用	  
#endif

 

static u8  fac_us=0;							//us延时倍乘数			   
static u16 fac_ms=0;							//ms延时倍乘数,在ucos下,代表每个节拍的ms数
	
	
#if SYSTEM_SUPPORT_OS							//如果SYSTEM_SUPPORT_OS定义了,说明要支持OS了(不限于UCOS).
//当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持
//首先是3个宏定义:
//    delay_osrunning:用于表示OS当前是否正在运行,以决定是否可以使用相关函数
//delay_ostickspersec:用于表示OS设定的时钟节拍,delay_init将根据这个参数来初始哈systick
// delay_osintnesting:用于表示OS中断嵌套级别,因为中断里面不可以调度,delay_ms使用该参数来决定如何运行
//然后是3个函数:
//  delay_osschedlock:用于锁定OS任务调度,禁止调度
//delay_osschedunlock:用于解锁OS任务调度,重新开启调度
//    delay_ostimedly:用于OS延时,可以引起任务调度.

//本例程仅作UCOSII和UCOSIII的支持,其他OS,请自行参考着移植
//支持UCOSII
#ifdef 	OS_CRITICAL_METHOD						//OS_CRITICAL_METHOD定义了,说明要支持UCOSII				
#define delay_osrunning		OSRunning			//OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec	OS_TICKS_PER_SEC	//OS时钟节拍,即每秒调度次数
#define delay_osintnesting 	OSIntNesting		//中断嵌套级别,即中断嵌套次数
#endif

//支持UCOSIII
#ifdef 	CPU_CFG_CRITICAL_METHOD					//CPU_CFG_CRITICAL_METHOD定义了,说明要支持UCOSIII	
#define delay_osrunning		OSRunning			//OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec	OSCfg_TickRate_Hz	//OS时钟节拍,即每秒调度次数
#define delay_osintnesting 	OSIntNestingCtr		//中断嵌套级别,即中断嵌套次数
#endif


//us级延时时,关闭任务调度(防止打断us级延迟)
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD   				//使用UCOSIII
	OS_ERR err; 
	OSSchedLock(&err);							//UCOSIII的方式,禁止调度,防止打断us延时
#else											//否则UCOSII
	OSSchedLock();								//UCOSII的方式,禁止调度,防止打断us延时
#endif
}

//us级延时时,恢复任务调度
void delay_osschedunlock(void)
{	
#ifdef CPU_CFG_CRITICAL_METHOD   				//使用UCOSIII
	OS_ERR err; 
	OSSchedUnlock(&err);						//UCOSIII的方式,恢复调度
#else											//否则UCOSII
	OSSchedUnlock();							//UCOSII的方式,恢复调度
#endif
}

//调用OS自带的延时函数延时
//ticks:延时的节拍数
void delay_ostimedly(u32 ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
	OS_ERR err; 
	OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err);	//UCOSIII延时采用周期模式
#else
	OSTimeDly(ticks);							//UCOSII延时
#endif 
}
 
//systick中断服务函数,使用ucos时用到
void SysTick_Handler(void)
{	
	if(delay_osrunning==1)						//OS开始跑了,才执行正常的调度处理
	{
		OSIntEnter();							//进入中断
		OSTimeTick();       					//调用ucos的时钟服务程序               
		OSIntExit();       	 					//触发任务切换软中断
	}
}
#endif

			   
//初始化延迟函数
//当使用OS的时候,此函数会初始化OS的时钟节拍
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init()
{
#if SYSTEM_SUPPORT_OS  							//如果需要支持OS.
	u32 reload;
#endif
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);	//选择外部时钟  HCLK/8
	fac_us=SystemCoreClock/8000000;				//为系统时钟的1/8  
#if SYSTEM_SUPPORT_OS  							//如果需要支持OS.
	reload=SystemCoreClock/8000000;				//每秒钟的计数次数 单位为M  
	reload*=1000000/delay_ostickspersec;		//根据delay_ostickspersec设定溢出时间
												//reload为24位寄存器,最大值:16777216,在72M下,约合1.86s左右	
	fac_ms=1000/delay_ostickspersec;			//代表OS可以延时的最少单位	   

	SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;   	//开启SYSTICK中断
	SysTick->LOAD=reload; 						//每1/delay_ostickspersec秒中断一次	
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;   	//开启SYSTICK    

#else
	fac_ms=(u16)fac_us*1000;					//非OS下,代表每个ms需要的systick时钟数   
#endif
}								    

#if SYSTEM_SUPPORT_OS  							//如果需要支持OS.
//延时nus
//nus为要延时的us数.		    								   
void delay_us(u32 nus)
{		
	u32 ticks;
	u32 told,tnow,tcnt=0;
	u32 reload=SysTick->LOAD;					//LOAD的值	    	 
	ticks=nus*fac_us; 							//需要的节拍数	  		 
	tcnt=0;
	delay_osschedlock();						//阻止OS调度,防止打断us延时
	told=SysTick->VAL;        					//刚进入时的计数器值
	while(1)
	{
		tnow=SysTick->VAL;	
		if(tnow!=told)
		{	    
			if(tnow<told)tcnt+=told-tnow;		//这里注意一下SYSTICK是一个递减的计数器就可以了.
			else tcnt+=reload-tnow+told;	    
			told=tnow;
			if(tcnt>=ticks)break;				//时间超过/等于要延迟的时间,则退出.
		}  
	};
	delay_osschedunlock();						//恢复OS调度									    
}
//延时nms
//nms:要延时的ms数
void delay_ms(u16 nms)
{	
	if(delay_osrunning&&delay_osintnesting==0)	//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)	    
	{		 
		if(nms>=fac_ms)							//延时的时间大于OS的最少时间周期 
		{ 
   			delay_ostimedly(nms/fac_ms);		//OS延时
		}
		nms%=fac_ms;							//OS已经无法提供这么小的延时了,采用普通方式延时    
	}
	delay_us((u32)(nms*1000));					//普通方式延时  
}
#else //不用OS时
//延时nus
//nus为要延时的us数.		    								   
void delay_us(u32 nus)
{		
	u32 temp;	    	 
	SysTick->LOAD=nus*fac_us; 					//时间加载	  		 
	SysTick->VAL=0x00;        					//清空计数器
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;	//开始倒数	  
	do
	{
		temp=SysTick->CTRL;
	}while((temp&0x01)&&!(temp&(1<<16)));		//等待时间到达   
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;      					 //清空计数器	 
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864 
void delay_ms(u16 nms)
{	 		  	  
	u32 temp;		   
	SysTick->LOAD=(u32)nms*fac_ms;				//时间加载(SysTick->LOAD为24bit)
	SysTick->VAL =0x00;							//清空计数器
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;	//开始倒数  
	do
	{
		temp=SysTick->CTRL;
	}while((temp&0x01)&&!(temp&(1<<16)));		//等待时间到达   
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;       					//清空计数器	  	    
} 
#endif 

 添加模块的delay.h代码如下

#ifndef __DELAY_H
#define __DELAY_H 			   
#include "sys.h"  
//	 
//本程序只供学习使用,未经作者许可,不得用于其它任何用途
//ALIENTEK STM32开发板
//使用SysTick的普通计数模式对延迟进行管理(适合STM32F10x系列)
//包括delay_us,delay_ms
//正点原子@ALIENTEK
//技术论坛:www.openedv.com
//创建日期:2010/1/1
//版本:V1.8
//版权所有,盗版必究。
//Copyright(C) 广州市星翼电子科技有限公司 2009-2019
//All rights reserved
//********************************************************************************
//V1.2修改说明
//修正了中断中调用出现死循环的错误
//防止延时不准确,采用do while结构!
//V1.3修改说明
//增加了对UCOSII延时的支持.
//如果使用ucosII,delay_init会自动设置SYSTICK的值,使之与ucos的TICKS_PER_SEC对应.
//delay_ms和delay_us也进行了针对ucos的改造.
//delay_us可以在ucos下使用,而且准确度很高,更重要的是没有占用额外的定时器.
//delay_ms在ucos下,可以当成OSTimeDly来用,在未启动ucos时,它采用delay_us实现,从而准确延时
//可以用来初始化外设,在启动了ucos之后delay_ms根据延时的长短,选择OSTimeDly实现或者delay_us实现.
//V1.4修改说明 20110929
//修改了使用ucos,但是ucos未启动的时候,delay_ms中中断无法响应的bug.
//V1.5修改说明 20120902
//在delay_us加入ucos上锁,防止由于ucos打断delay_us的执行,可能导致的延时不准。
//V1.6修改说明 20150109
//在delay_ms加入OSLockNesting判断。
//V1.7修改说明 20150319
//修改OS支持方式,以支持任意OS(不限于UCOSII和UCOSIII,理论上任意OS都可以支持)
//添加:delay_osrunning/delay_ostickspersec/delay_osintnesting三个宏定义
//添加:delay_osschedlock/delay_osschedunlock/delay_ostimedly三个函数
//V1.8修改说明 20150519
//修正UCOSIII支持时的2个bug:
//delay_tickspersec改为:delay_ostickspersec
//delay_intnesting改为:delay_osintnesting
// 
	 
void delay_init(void);
void delay_ms(u16 nms);
void delay_us(u32 nus);

#endif

需要调用的初始化函数是delay_init();

代码示例如下:

	/* 延时函数的初始化 */
	delay_init();

3.4、初始化板载LED灯

板载led灯的初始化为普通gpio的推挽输出模式初始化即可。

我们需要先封装如下函数,再调用此函数进行初始化。

封装函数如下:

/*
	板载LED初始化
*/
void my_LED_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;  //GPIO管脚的结构体定义
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);//使能GPIO的时钟C

	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;//IO管脚输出模式为推挽输出,此模式既可以输出高电平或低电平
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_13;//13号管脚
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;//IO管脚的传输速度
	GPIO_Init(GPIOC,&GPIO_InitStructure);//PC13管脚配置
	
	GPIO_SetBits(GPIOC,GPIO_Pin_13);	//PC13管脚输出高电平
}

需要调用的函数如下:

	/* STM32f103c8t6最小系统板上的LED灯的初始化 */
	my_LED_Init();

3.5、初始化电机PWM输出引脚

对于电机,我们只需要向前的转速即可,即只需要初始化两个PWM输出模式的端口,并对他们进行一个值运算的限幅,和频率的初始化。我们可以将自动重装载寄存器的值设置为7199,预分频值为0,改变这两个值会让电机有不同的响声,大家可以自己体验一下。

电机的两个PWM的映射引脚为 PB0 PB1 对应定时器资源3的3通道和4通道。

我们需要写一个初始化函数

代码示例如下:

/*
	电机初始化
	参数7199,0
	初始化PWM   定时器3  PB0  PB1
*/
void PWM_Init_TIM3(u16 Per,u16 Psc)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	TIM_OCInitTypeDef  TIM_OCInitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);// 
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);  //使能GPIO外设时钟使能                                                                    	

   //设置该(PB0和PB1)引脚为复用输出功能,输出TIM4 CH3和CH4的PWM脉冲波形
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; //TIM_CH3 CH4
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	TIM_TimeBaseStructure.TIM_Period = Per; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	 80K
	TIM_TimeBaseStructure.TIM_Prescaler =Psc; //设置用来作为TIMx时钟频率除数的预分频值  不分频
	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位

	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
	TIM_OCInitStructure.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
	TIM_OC3Init(TIM3, &TIM_OCInitStructure);  //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
	
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式2
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
	TIM_OCInitStructure.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
	TIM_OC4Init(TIM3, &TIM_OCInitStructure);  //根据TIM_OCInitStruct中指定的参数初始化外设TIMx

	
	TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);  //CH1预装载使能	 
	TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable);  //CH1预装载使能
	
	TIM_ARRPreloadConfig(TIM3, ENABLE); //使能TIMx在ARR上的预装载寄存器
	
	TIM_Cmd(TIM3, ENABLE);  //使能TIM3
}

然后我们需要在主函数中调用这个封装函数

代码示例如下:

	/* 电机引脚及PWM初始化 */
	PWM_Init_TIM3(7199,0);

3.6、初始化红外传感器模块

对于红外传感器模块,需要使用的引脚比较多,每个引脚只需要使用普通的GPIO模式,将GPIO模式设置为浮空输入即可。

红外模块占用的端口资源为 PA0~PA7        PB9~PB11

他们对应的的传感器位置从左到右依次为 PA0 PA1 PA2 PA3 PA4 PA5 PA6 PA7 PB9 PB19 PB11

需要封装一个初始化函数

代码示例如下:

/*
	红外初始化
*/
void hongwai_Init(void)
{
		GPIO_InitTypeDef GPIO_InitStructure;
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能GPIOA的时钟
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能GPIOB的时钟
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//使能复用功能的时钟
	
		GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//IO管脚模式配置为浮空输入,这样就能获取传感器传回来的数字信号(高低电平)
		GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7;
		GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
		GPIO_Init(GPIOA,&GPIO_InitStructure);//PA4,PA5,PA6,PA7管脚的初始化
	
		GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;//浮空输入模式
		GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10 | GPIO_Pin_9 | GPIO_Pin_11;
		GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
		GPIO_Init(GPIOB,&GPIO_InitStructure);//PB0,PB1,PB3,PB4,PB5管脚的初始化
	  
	
		GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);//关闭STM32f103c8t6的JTAGD功能,使PB3和PB4用作普通的IO管脚//必须开启复用功能的时钟才能关闭该功能
}

然后在主函数中调用它

代码示例如下:

	/* 11个红外传感器连接最小系统板上的管脚IO口的初始化 */
	hongwai_Init();

3.7、初始化舵机模块

对于舵机模块的初始化,我们需要初始化一个PWM输出的GPIO口,并且这个GPIO口的PWM频率跟前面的电机的频率不一样,在库函数的写法下,我们需要选择一个新的定时器进行初始化。

关于sg90舵机的PWM频率,有如下图:

图13 舵机的角度与脉冲对应图

因此我们可以对舵机的PWM的自动重装载寄存器的值设置为19999,预分频值为71

使用定时器4的 通道1 PB6

根据图13可以得到舵机的角度映射公式为

Angle/180*2000+500

其中Angle是我们需要调整的舵机角度,值范围为0~180

需要封装一个初始化函数

代码示例如下:

/*
	舵机PWM初始化
	定时器4
	通道1
	PB6
	arr:自动重装值
	psc:时钟预分频数
*/
void TIM4_PWM_Init(u16 arr,u16 psc)//在主函数main中传入arr和psc的数值
{
	GPIO_InitTypeDef GPIO_InitStructure;
	TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;  //定时器TIM结构体定义
	TIM_OCInitTypeDef  TIM_OCInitStructure;  //定时器TIM通道结构体定义,每个定时器有四个通道

	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);// 使能定时器四的时钟
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//使能GPIO B的时钟 
	                                                    
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//PB6管脚的初始化,管脚对应复用功能:TIM4_CH1(定时器四的通道一)
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //因为要用到管脚的复用功能,所以这里是管脚模式是复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);//PB6
	
	
	TIM_TimeBaseStructure.TIM_Period = arr;//; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	
	TIM_TimeBaseStructure.TIM_Prescaler =psc;//设置用来作为TIMx时钟频率除数的预分频值
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;//设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的相应模式配置

  
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式1
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
	TIM_OCInitStructure.TIM_Pulse =0; //设置待装入捕获比较寄存器的脉冲值
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
	TIM_OC1Init(TIM4, &TIM_OCInitStructure);  //根据TIM_OCInitStruct中指定的参数初始化TIM4_CH1定时器四的通道一
         
	TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);//使能TIM4在 CCR1 上的预装载寄存器

	TIM_Cmd(TIM4, ENABLE);  //开启TIM4
}

需要封装一个舵机角度的改变函数

其代码示例如下:

void Srevo_SetAngle(float Angle)//舵机设置角度
{
	TIM_SetCompare1(TIM4,Angle/180*2000+500);//映射计算,
}

在主函数中需要调用初始化函数进行初始化

其代码示例如下:

	/* 舵机的PWM初始化,这里设置的频率为50hz  这样得出的周期T=1/f=20ms  因为MG90S舵机的PWM驱动信号为20ms */
	TIM4_PWM_Init(19999,71);

我们还需要对舵机一开始的角度进行调整,确保它连接的定向轮指向黑线,在车的中间

即先用宏定义一个变量为 DUOJIZHONGZHI

其代码示例如下:

#define DUOJIZHONGZHI 90

每台车的舵机安装都不一样,所以这个值需要自己进行调试。

调试的代码示例如下:

	/* 舵机归中 */
	Srevo_SetAngle(DUOJIZHONGZHI);

3.8、初始化中断

初始化中断的目的是中断能够更加准确的对小车进行调控,让小车的实时性响应更好

我们前面的使用占用了定时器3和定时器4,所以我们使用定时2作中断

初始化为10ms一次

所以其自动重装载寄存器的值设置为9999,预分频值为71

需要封装一个初始化函数

其代码示例如下:

/* 
	中断函数的定义
	定时器:TIM2
*/
void TIM2_Int_Init(u16 arr,u16 psc)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //时钟使能

	TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值	 计数到5000为500ms
	TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值  10Khz的计数频率  
	TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
 
	TIM_ITConfig(  //使能或者失能指定的TIM中断
		TIM2, //TIM2
		TIM_IT_Update ,
		ENABLE  //使能
		);
	
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;  //TIM3中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;  //先占优先级0级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;  //从优先级3级
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
	NVIC_Init(&NVIC_InitStructure);  //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器

	TIM_Cmd(TIM2, ENABLE);  //使能TIMx外设
							 
}

在主函数中调用其初始化函数

代码示例如下:

	/* 中断服务函数的初始化,也就是PWM波的初始化,这里的周期为10ms
	也就是每10ms会执行一次control.c文件里TIM2_IRQHandler函数里的操作  */
	TIM2_Int_Init(9999,71);

3.9、小车逻辑功能实现

建设好前面的初始化后,我们就要进行小车逻辑功能的实现了。

3.9.1、实现红外扫描读取函数的封装和处理

我们需要先定义一个数组存储扫描回来的值方便我们后续使用

数组定义为 : 

int sensor[11]={0,0,0,0,0,0,0,0,0,0,0};

对应的GPIO引脚号、小车传感器位置和数组位置映射如下

sensor[0]      [1]       [2]      [3]      [4]       [5]      [6]      [7]      [8]      [9]       [10] 

PA0    PA1    PA2      PA3      PA4      PA5     PA6      PA7      PB9      PB10        PB11

-5      -4        -3          -2          -1         0         1           2            3          4                5

代码示例如下

void read_sensor(void)//红外传感器识别到黑线返回数字信号低电平0,未识别到黑线返回高电平1
{
	      /*将位置从最左按顺序到最右的传感器返回的数字信号依次存入数组sensor[0~10]里*/
		sensor[0]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);//最左的传感器
		sensor[1]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1);
		sensor[2]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_2);
		sensor[3]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_3);
		sensor[4]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_4);
		sensor[5]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_5);//中央的传感器
		sensor[6]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
		sensor[7]=GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_7);
		sensor[8]=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_9);
		sensor[9]=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10);
		sensor[10]=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);//最右的传感器
	
	/* 处理部分 */
	/* 最左边的红外扫到黑线 */
	if(sensor[0]==0&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = -5;
	}
	/* 最左边旁边的左边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==0&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = -4;
	}
	/* 最左边的旁边的旁边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==0&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = -3;
	}
	/* 最左边的旁边的旁边的旁边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==0&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = -2;
	}
	/* 最左边的旁边的旁边的旁边的旁边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==0&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = -1;
	}
	/* 中间的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==0&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = 0;
	}
	/* 中间的右边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==0&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = 1;
	}
	/* 中间的右边的右边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==0&&sensor[8]==1&&sensor[9]==1&&sensor[10]==1)
	{
		error = 2;
	}
	/* 中间的右边的右边的右边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==0&&sensor[9]==1&&sensor[10]==1)
	{
		error = 3;
	}
	/* 中间的右边的右边的右边的右边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==0&&sensor[10]==1)
	{
		error = 4;
	}
	/* 最右边的红外扫到黑线 */
	else if(sensor[0]==1&&sensor[1]==1&&sensor[2]==1&&sensor[3]==1&&sensor[4]==1&&sensor[5]==1&&sensor[6]==1&&sensor[7]==1&&sensor[8]==1&&sensor[9]==1&&sensor[10]==0)
	{
		error = 5;
	}
	/* 如果都不满足,就将上一次的值传给error */
	else
	{
		error = lastError;
	}
	/* 记录上一次的值 */
	lastError = error;
}

具体条件还可以根据实际更改。

3.9.2、 舵机转向控制

关于舵机转向,舵机是从0~180度的范围的,根据前面描述的舵机公式

DjPwm = DUOJIZHONGZHI + DUOJI * error;

可以写出我们的控制代码为

		/* 根据反馈的error值进行舵机的角度运算 */
		DjPwm = DUOJIZHONGZHI + DUOJI * error;//把这个算式中的“+”换成“-”可以改变舵机转动方向
		/* 对运算的数据进行限幅,防止溢出或者过大 */
		if(DjPwm > 180)
			DjPwm = 180;
		if(DjPwm < 0)
			DjPwm = 0;
		/* 将运算完后的值传输进入PWM输出 */
		Srevo_SetAngle(DjPwm);

3.9.3、电机的差速控制辅助转向

小车可以通过轮子的差速进行转向控制。

即简单来说,要使小车向左边转,需要给左轮小于右轮的速度,小车两个轮子不同的转速会推动小车往左边偏,同理,要使小车向右边转,需要给右轮小于左轮的速度。

那在程序中如何实现呢。

我们现在使用了驱动模块驱动电机,然后我们只需要设置单片机的电机资源的两个引脚的PWM比较值,即设置两个引脚的电平输出时间,即可给电机一个驱动的电压。

这个比较值的大小不一样,电机的转速就不一样,因此,我们需要先给电机一个初始的比较值,让它有一个能跑的速度先。

我们可以        #define DIANJI_PWM 4000

                       int DIANJI = 100;//这个值用来差速运算,改变这个值可以改变差速大小

如果需要改变电机的速度就改变这个DIANJI_PWM 的值

那如何实现电机的差速运算呢,由前面的红外模块的扫描读取,当小车在黑线右边时,小车的左边红外传感器扫到黑线。error赋值小于0,这个时候我们小车要向左边转动,即左边轮子转速小于右边轮子,

即左边轮子的PWM比较值公式为       DIANJI_PWM + error * DIANJI;

即右边轮子的PWM比较值公式为       DIANJI_PWM - error * DIANJI;

同理,分析小车在黑线左边的情况,发现公式一致。

所以我们可以写出控制代码

代码示例如下:

		/* 根据反馈的error值进行电机的差速运算 */
		DianJiPwm_L = DIANJI_PWM + DIANJI * error;
		DianJiPwm_R = DIANJI_PWM - DIANJI * error;
		/* 对运算的数据进行限幅,防止溢出或者过大 */
		DianJiPwm_L = DianJiPwm_L > 7200 ? DianJiPwm_L : 7200;
		DianJiPwm_L = DianJiPwm_L < 0 ? DianJiPwm_L : 500;
		DianJiPwm_R = DianJiPwm_R > 7200 ? DianJiPwm_R : 7200;
		DianJiPwm_R = DianJiPwm_R < 0 ? DianJiPwm_R : 500;
		/* 将运算完后的值传输进入PWM输出 */
		TIM_SetCompare3(TIM3,DianJiPwm_L);
		TIM_SetCompare4(TIM3,DianJiPwm_R);

如果发现转的太大或者太小,就可以调节DIANJI的值,时期达到平滑。

四、调试部分

完成以上的步骤之前,需要先对小车的各个模块进行检查,确保上电后能正常使用,随后,需要调整舵机的中值 DUOJIZHONGZHI ,让舵机下面的定向轮朝着黑线方向。随后,调整两个电机的接线等,让电机朝前走即可。

代码的调试,主要的调试有:中断的时间,error的赋值,舵机的DUOJI 转向值,电机的DIANJI转向值等。

调整完就能正常巡线了。

每个人的车的结构都不一样,代码参数也不一样,制作过程需要多理解,多实践,才能更加明白这样使用的原理。

五、代码模板下载

码云:

https://gitee.com/wgjwgj030430/learning

百度网盘:

链接:https://pan.baidu.com/s/1YxLXIUNtS8yhvzVPjmRH2Q?pwd=wugj 
提取码:wugj
如若下载不了也可私信我

欢迎大家多多指教!

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

跳河轻生的鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值