三天让车跑起来!stm32循迹车 —— 第二天!如何控制舵机/电机?

声明在前:本系列以程序设计为主,适用于刚学会32,想完成一个基本项目却不知道怎么上手的小伙伴。想学习硬件方面如:电路、画板等内容的朋友请不要在本系列耽误您的时间,关闭即可。

在上一期32循迹第一天.的内容里,主要讲述了与GPIO有关的理论知识和具体在各种模块上的应用以及程序写法。

在这里插入图片描述
那么,从理论上来讲,就只剩下定时器的内容没有讲了,(当然,在应用层面是另一回事)。今天就把定时器讲完,这样下一期就可以毫无顾虑地讲方案了/doge,接下来,直入正文:
在这里插入图片描述
想看程序的从“实际应用"开始

理论知识

  • 按照上一期的习惯,在明确用法之前,我们首先来了解一下定时器的知识:

学51的时候我们知道,51有两个定时器,并且这两个定时器都是中断源。在使用51的定时器之前,我们都要初始化定时器——配置各寄存器的参数。在32中,也是同理:

初始化

第一个要初始化的定时器以 TIM4 为例,我们首先在Basic文件夹里新建tim4文件夹,再新建TIM4.c和TIM4.h文件
接下来我们打开 TIM4.c

定时器初始化:arr、psc

在初始化之前,32定时器有两点要注意:

  • 设置好定时器之后要明确定时器中断的优先级:用NVIC来配置
  • 每个定时器都有一定数量的通道,用于输出pwm、输入捕获等等(TIM1/8是高级定时器,TIM2345是通用定时器,TIM67是基本定时器,具体有什么区别,就等以后有机会再讲了)

先看程序:(以有四个通道的通用定时器TIM4为例)

void TIM4_Init(u16 arr,u16 psc){
  TIM_TimeBaseInitTypeDef    TIM_TimeBaseStructure;  
  TIM_OCInitTypeDef          TIM_OCInitStructure; 
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);	
 
  TIM_TimeBaseStructure.TIM_Period = arr;
  TIM_TimeBaseStructure.TIM_Prescaler = psc;
    
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;     
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; 
  TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);  
}

这里面只有一个要注意的点:就是这个初始化函数输入的参数:arrpsc
如果不想了解这两个参数有什么用的话,记住下面这个公式:
在这里插入图片描述
接下来我们来明确一下arr和psc的含义:

  • arr:自动重装值

自动重装值的意思很好理解:如图

如果不设置arr的话,在每个周期都会走向100%,而这和持续的“1”没有区别。在加了arr以后我们才能够控制这个输出的具体值。
arr并非不可修改,即使在初始化配置完了以后,仍然可以通过TIMx -> ARR =____控制寄存器来直击修改它的值(这一点稍后在讲舵机的时候会用到)

  • psc:预分频值

我们知道,stm32f103的时钟频率是 ,72MHz 而预分频的作用就是把这个 72M 再分一下:假如我让 psc + 1 = 72,那么分频完以后的时钟频率就是 1M。而在上述的公式中,常用来计算频率的除数(1/时间),用72可以理解为便于计算,
明确了arr和psc以后,我们再来看看其它几句的程序

——————接下来回到程序:

  • RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4 , ENABLE );

对一般的小程序,见RCC便知:时钟初始化。

  • TIM_TimeBaseStructure.TIM_ClockDivision = 0 ;

别看这句话很多定时器初始化程序都带,还都等于0,但是实际上,这玩意跟pwm没关系:)它跟输入捕获有关。

  • TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up

这是向上计数模式,即从0开始一直到 ARR 然后再自动重装。一般情况下用这个就好,我们Go to一下这个TIM_CounterMode_Up,可以看见除了它还有有其他指令,以及他们各自的含义就不多说了,我相信买板子的附赠教程里会说的:)

  • TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);

这是库里自带的初始化程序,类似之前GPIO那个库自带初始化。

NVIC

    TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE );
  	NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure); 
  • TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE );

同样是自带函数,使能中断,允许中断更新

  • NVIC_InitStructure.NVIC_IRQChannel = TIM4_IRQn ;

指定定时器4的中断

  • NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  • NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;

这两句是用来设置优先级的,第一句是先占优先级的意思,第二句是从优先级的意思,在比较不同中断源时,先比较先占优先级,若相同,再比较从优先级,二者都是数值越小,优先级越高。

  • NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

使能IRQ通道

  • NVIC_Init(&NVIC_InitStructure);

同样是自带函数,初始化NVIC

中断服务函数

void TIM4_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) {
		TIM_ClearITPendingBit(TIM4, TIM_IT_Update  );  
		函数体
		TIM_ClearFlag(TIM4, TIM_FLAG_Update); 
		}
}

中断服务函数有一点要注意就是:清除标志位:

  • TIM_ClearITPendingBit(TIM4, TIM_IT_Update );
  • TIM_ClearFlag(TIM2, TIM_FLAG_Update);

注意这两个函数,我们分别Go to一下:

void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT)
{
  /* Check the parameters */
  assert_param(IS_TIM_ALL_PERIPH(TIMx));
  assert_param(IS_TIM_IT(TIM_IT));
  /* Clear the IT pending Bit */
  TIMx->SR = (uint16_t)~TIM_IT;
}
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG)
{  
  /* Check the parameters */
  assert_param(IS_TIM_ALL_PERIPH(TIMx));
  assert_param(IS_TIM_CLEAR_FLAG(TIM_FLAG));
   
  /* Clear the flags */
  TIMx->SR = (uint16_t)~TIM_FLAG;
}

乍一看二者没有什么区别,仔细看的话会发现,这里面的两个参数不同:前者是TIM_IT ,后者是TIM_FLAG

—————以下为我的主观猜测,仅供参考——————————————

我们打开stm32自带的库文件:stm32f10x_tim.h
在601行开始:(实际行数可能不一样,这里以我装的库为参考)

#define TIM_IT_Update                      ((uint16_t)0x0001)
#define TIM_IT_CC1                         ((uint16_t)0x0002)
#define TIM_IT_CC2                         ((uint16_t)0x0004)
#define TIM_IT_CC3                         ((uint16_t)0x0008)
#define TIM_IT_CC4                         ((uint16_t)0x0010)
#define TIM_IT_COM                         ((uint16_t)0x0020)
#define TIM_IT_Trigger                     ((uint16_t)0x0040)
#define TIM_IT_Break                       ((uint16_t)0x0080)

再看963行

#define TIM_FLAG_Update                    ((uint16_t)0x0001)
#define TIM_FLAG_CC1                       ((uint16_t)0x0002)
#define TIM_FLAG_CC2                       ((uint16_t)0x0004)
#define TIM_FLAG_CC3                       ((uint16_t)0x0008)
#define TIM_FLAG_CC4                       ((uint16_t)0x0010)
#define TIM_FLAG_COM                       ((uint16_t)0x0020)
#define TIM_FLAG_Trigger                   ((uint16_t)0x0040)
#define TIM_FLAG_Break                     ((uint16_t)0x0080)
#define TIM_FLAG_CC1OF                     ((uint16_t)0x0200)
#define TIM_FLAG_CC2OF                     ((uint16_t)0x0400)
#define TIM_FLAG_CC3OF                     ((uint16_t)0x0800)
#define TIM_FLAG_CC4OF                     ((uint16_t)0x1000)

二者一个是中断位,一个是标志位,不是所有的标志位都能产生中断,但是中断位可以。而以上两个函数一个清除了标志位,一个清除了中断位。然鹅实际在使用的时候二者几乎没有区别,都能起到同样的作用。而一定要分两个函数写的原因,可能是为了保证官方库的科学性吧:)
而我们把这两个函数都放上去的话也应该是无可厚非
看来我学的还是不够啊,还要再努力才行,希望各位读者也是,不要停止自己的学习
——————————————回到正文——————————————

pwm通道初始化:CCRx

TIM4有4个通道:分别对应了引脚PB.6.7.8.9,对应的寄存器分别是TIM4 -> CCR1 ~ CCR4(稍后说),先看代码:

void TIM4_PWM_Init(u16 m,u16 n,u16 p,u16 q) 
这里我设了4个参数,分别给四个通道设置PWM初始值
{
	/* CCR1:通道一初始化 */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; 
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    TIM_OCInitStructure.TIM_Pulse = m;
    TIM_OCInitStructure.TIM_OCPolarity =TIM_OCPolarity_High;
	TIM_OC1Init(TIM4, &TIM_OCInitStructure);  
    TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); 
}
  • TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1

PWM模式1,我们同样Go to以后可以看见有其他模式可选,PWM模式和之前的向上/向下计数共同决定该通道在什么时候为有效电平。

  • TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;

看见Enable肯定都明白啥意思了:)

  • TIM_OCInitStructure.TIM_Pulse = m;

我们Go to这个TIM_Pulse,看见它的描述:
在这里插入图片描述
划重点:0——FFFF,即十进制的 0——65535初始化的时候让他等于一个在0-65535之间的值,(参数m)即可设为这个通道的初始PWM

  • TIM_OCInitStructure.TIM_OCPolarity =TIM_OCPolarity_High ;

这个High是说,当计数器小于CCRx的数值的时候,这个通道的电平为高。Goto一下发现还有一个Low的选项,意思也不难理解。

  • TIM_OC1 Init(TIM4, &TIM_OCInitStructure);
  • TIM_OC1 PreloadConfig(TIM4, TIM_OCPreload_Enable);

分别是通道一的初始化和使能。

这样,我们就完成了TIM4的通道一(CCR1)PWM初始化配置,剩下的CCR2-4也是同理,为了练习请自行把剩下的通道按照已有程序修改好,再写上。
PWM初始化就写完了,而具体的赋值会在下一部分里讲
——————————————————————————
在写完上述函数后,再回到TIM4.h里声明一下就好了
——————————————————————————

实际应用

首先来回顾一下昨天尚未完成的Run函数:
在这里插入图片描述
可见,我们需要控制的有电机,还有舵机。先来说电机:

电机、电机驱动

我们再来看一下TB6612的背面:
在这里插入图片描述
很明显,PWMA 和 PWMB就是分别控制两个轮子的转速的。
怎么赋值PWM呢?

TIMx->CCRx

第一种,也是最直接的方法,就是控制这个寄存器,以达到输出PWM的效果

TIM4->CCR2 = 65535

就是说让PWM = 65535/65535 * 100%,要是让它等于48000就是 48000/65535 *100%。这种方法很直接但是也不精确
接下来回到Run函数里:以第一个为例:(假定TIM4->CCR1(PB.6) 接到了PWMA ,CCR2(PB.7)接到了PWMB)

直行
TIM4->CCR1 = 48000
TIM4->CCR2 = 48000
舵机角度:向前

这就是说让两个电机转速为:48000/65535 *100%,约等于 73%,即——电机满速的73%。而电机满速的值是根据总电源电压、小车负重等等因素一起决定的。

库函数

我们来到官方库stm32f10x_tim.c的第2292行,我们看见

void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1)
{
  /* Check the parameters */
  assert_param(IS_TIM_LIST8_PERIPH(TIMx));
  /* Set the Capture Compare1 Register value */
  TIMx->CCR1 = Compare1;
}
这是通道一的设置PWM函数,它下面就是通道二的

在程序中,我们可以直接用这个函数:

u16 PWM1,PWM2(在前面声明它)

直行
TIM_SetCompare1(TIM4,PWM1);
TIM_SetCompare1(TIM4,PWM2);
舵机角度:向前

二者其实没有本质的区别:)

中断

这种写法比较适合舵机控制,稍后会在舵机内容详细说

舵机

舵机是个很神奇的玩意,只有在2.5ms内收到的高电平才会引起舵机角度的变化,即:0~2.5ms内,给xms高电平,舵机会旋转x/2.5* 舵机总角度 的角度,而舵机总角度的值比较常见的有180、270等等,还有360的全向(这种舵机更像电机,输入会影响其方向和转速,然后会朝这个方向一直转下去)

——————————————

建议单独建立舵机文件,并且为了避免和电机PWM使用冲突,可以换一个定时器,这里以TIM2为例
在Hardware文件夹里新建舵机文件夹Sensor,加入Sensor.c和Sensor.h

  1. 选定引脚:假定为PB.2
  2. 初始化TIM2——arr = 2499,psc = 71——通过前文公式计算得知理论时间恰好为2.5ms
  3. NVIC配置
  4. 初始化PB.2

这几个函数在前文和昨天都已经写过了,就不再这里说了

舵机控制函数

其实原理非常简单:

u8 i;
Uint High_Set;

void 舵机控制(void{
	switch(i)
	{
	case 1:
		TIM2->ARR = High_Set;
		让 PB.2 输出1 
		 	这个赋给ARR的值代入公式计算可知:
			给高的时间为:High_Set/100 0000 s,再换算成毫秒
			(刚刚说过psc = 71了,所以psc在这里已经被消去了)
	case 2:
		TIM2->ARR = 控制舵机的总周期 - High_Set
		让 PB.2 输出0
			控制舵机的总周期可以理解为:
			我想每10ms让舵机输出一次角度,这里就写10000
			我想每20ms让舵机输出一次角度,这里就写20000
	}
	如果想控制多个舵机的话,就case 34一组56一组这么一直写下去,然后
	控制舵机的总周期就是 2.5ms * 舵机数
	例:10000 = 2500 * 4,就是控制了四个舵机,然后case总共有4组,从18
}

然后把这个函数放到定时器中断服务函数内部,并且在执行完以后让 i++,若是只有一个舵机的话,当i++完以后,进入case 2时让 i = 0,以此使得控制 i 始终在一定范围内

  • 我们知道,输入舵机的高电平要控制在 2.5 ms内,所以High_Set不能大于2500
    写成程序则是()
u8 i;————————这是个全局变量,用于循环处理PB2输出的问题
unsigned int High_Set;——————这也是个全局变量,起着改变舵机角度的关键作用

void Sensor(void)
{
	switch(i)
	{  					
		case 1: 
		TIM2->ARR=High_Set;
    	GPIO_SetBits(GPIOB,GPIO_Pin_2);
    	break;
		
		case 2:
		//我把周期设为20ms,原理在刚刚提到过
   	    TIM2->ARR=20000-High_Set; 		 
		GPIO_ResetBits(GPIOB,GPIO_Pin_2);
		i=0;
		break;	
		
		default:break;
	}	
}

然后呢,我们回到中断服务函数,把二者放入即可达到用定时器中断控制舵机的目的

extern unsigned int High_Set;
extern u8 i;
void TIM3_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
		TIM_ClearITPendingBit(TIM3, TIM_IT_Update  );  
		
		i++;
		Sensor();
		
		TIM_ClearFlag(TIM2, TIM_FLAG_Update); 
		}
}

改变舵机角度

前文说过了改变舵机角度的原理,那么在主函数中,我们想要改变舵机角度就很简单了:

extern unsigned int High_Set;
...
主函数
...
直行
TIM4->CCR1 = 48000
TIM4->CCR2 = 48000
High_Set = 参数

这个参数就请各位在实践的时候自己试验出来合适的值了。(0 - 2500之间,常用的中值在一千四到一千七不等。)
至于其他的,像左小角度、左大角度、右等等等等,都是同样的原理控制相同的参数,就没必要赘述了。

这里要额外说一下舵机的事情:有些稍微贵一点的舵机是有锁死保护的,有些则没有。在装车的时候,我们往往会发现: 传动杆本身经常会阻挡舵机的旋转。这时候,如果舵机本身是不带有锁死保护的,在如此被阻碍了一定时间或者次数后,舵机就烧了。
当初我们用的舵机就不带有锁死保护(那时候不知道),结果初赛前一天凌晨,舵机被我调烧了:) 还好当时准备了plan B。后来加了几十块钱,买了个带保护的型号,现在还没坏 😃
如果舵机没有锁死保护,传动杆又挡住它旋转时,我们编程者能做的就是: 不让它转到最大角度,始终让它留有一点角度,尽可能避免其锁死。
——————————————————————

总结

控制函数也都说得差不多了,其实到现在为止,理论上我们就已经可以做到让小车实现简单的寻迹了。可是在实际调试的时候我们会发现,车跑得可能很不令人满意,最典型的问题就是:经常会出赛道。那么在下一期,我将简单介绍一下可能发生的问题以及处理方法,然后再把这个项目要求的:避障、检测磁铁、停车等方案一一说明。

今天就说这么多好了,主要就是把定时器在具体模块上的应用,我在整理内容的时候发现容量实在是有些大,而我又想说得简短一些,所以可能导致全文在读起来的时候有些混乱,还请读者海涵,(经验不足,还是要练啊=_=)

本系列还剩最后一期!感兴趣的小伙伴可以关注我的频道,除了这个系列,我目前正在持续更新Python学习的系列,在Python完结后,根据时间安排会再开机器学习的系列,欢迎各位与我共同学习,一起进步!
在这里插入图片描述
点我进入下一期:最后一天!如何让车跑得更好?.

  • 39
    点赞
  • 182
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
舵机和超声波测距模块是STM32蓝牙控制循迹避障小中常用的两个重要组件。 首先,舵机用于控制小的转向。舵机通过PWM信号控制电机的转动角度,通过改变PWM信号的占空比,可以实现不同的转向角度。通常情况下,舵机的控制信号需要与STM32的定时器模块相连,通过调整定时器的周期和占空比,可以控制舵机的转向角度。在源代码中,通常会包括一些函数用于控制舵机的转向,如设置舵机转动角度等。 其次,超声波测距模块用于检测小前方的障碍物距离。超声波测距模块通过发射超声波脉冲,然后检测超声波脉冲的回波时间,从而计算出前方障碍物的距离。在源代码中,通常会包括一些函数用于控制超声波测距模块的工作方式,如初始化超声波模块、发送超声波脉冲、接收回波信号等。 舵机和超声波测距模块是实现循迹避障功能的关键组件之一。在源代码中,我们需要同时处理舵机和超声波测距模块的数据,并根据测距结果调整舵机的转向角度。通过不断的测距和调整转向角度,小可以根据前方障碍物的距离来做出合适的转向动作,从而实现循迹和避障的功能。 在实际应用中,舵机和超声波测距模块的代码通常需要与其他组件的代码进行协调和集成,以实现整个小的控制逻辑。通过合理的编程和调试,我们可以使得小根据测距结果和算法判断,实现自动避障和循迹控制的功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值