声明在前:本系列以程序设计为主,适用于刚学会32,想完成一个基本项目却不知道怎么上手的小伙伴。想学习硬件方面如:电路、画板等内容的朋友请不要在本系列耽误您的时间,关闭即可。
在上一期32循迹第一天.的内容里,主要讲述了与GPIO有关的理论知识和具体在各种模块上的应用以及程序写法。
那么,从理论上来讲,就只剩下定时器的内容没有讲了,(当然,在应用层面是另一回事)。今天就把定时器讲完,这样下一期就可以毫无顾虑地讲方案了/doge,接下来,直入正文:
想看程序的从“实际应用"开始
学51的时候我们知道,51有两个定时器,并且这两个定时器都是中断源。在使用51的定时器之前,我们都要初始化定时器——配置各寄存器的参数。在32中,也是同理:
第一个要初始化的定时器以 TIM4 为例,我们首先在Basic文件夹里新建tim4文件夹,再新建TIM4.c和TIM4.h文件
接下来我们打开 TIM4.c
在初始化之前,32定时器有两点要注意:
先看程序:(以有四个通道的通用定时器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);
}
这里面只有一个要注意的点:就是这个初始化函数输入的参数:arr 和 psc
如果不想了解这两个参数有什么用的话,记住下面这个公式:
接下来我们来明确一下arr和psc的含义:
自动重装值的意思很好理解:如图
如果不设置arr的话,在每个周期都会走向100%,而这和持续的“1”没有区别。在加了arr以后我们才能够控制这个输出的具体值。
arr并非不可修改,即使在初始化配置完了以后,仍然可以通过TIMx -> ARR =____控制寄存器来直击修改它的值(这一点稍后在讲舵机的时候会用到)
我们知道,stm32f103的时钟频率是 ,72MHz 而预分频的作用就是把这个 72M 再分一下:假如我让 psc + 1 = 72,那么分频完以后的时钟频率就是 1M。而在上述的公式中,常用来计算频率的除数(1/时间),用72可以理解为便于计算,
明确了arr和psc以后,我们再来看看其它几句的程序
——————接下来回到程序:
对一般的小程序,见RCC便知:时钟初始化。
别看这句话很多定时器初始化程序都带,还都等于0,但是实际上,这玩意跟pwm没关系:)它跟输入捕获有关。
这是向上计数模式,即从0开始一直到 ARR 然后再自动重装。一般情况下用这个就好,我们Go to一下这个TIM_CounterMode_Up,可以看见除了它还有有其他指令,以及他们各自的含义就不多说了,我相信买板子的附赠教程里会说的:)
这是库里自带的初始化程序,类似之前GPIO那个库自带初始化。
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);
同样是自带函数,使能中断,允许中断更新
指定定时器4的中断
这两句是用来设置优先级的,第一句是先占优先级的意思,第二句是从优先级的意思,在比较不同中断源时,先比较先占优先级,若相同,再比较从优先级,二者都是数值越小,优先级越高。
使能IRQ通道
同样是自带函数,初始化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);
}
}
中断服务函数有一点要注意就是:清除标志位:
注意这两个函数,我们分别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)
二者一个是中断位,一个是标志位,不是所有的标志位都能产生中断,但是中断位可以。而以上两个函数一个清除了标志位,一个清除了中断位。然鹅实际在使用的时候二者几乎没有区别,都能起到同样的作用。而一定要分两个函数写的原因,可能是为了保证官方库的科学性吧:)
而我们把这两个函数都放上去的话也应该是无可厚非
(看来我学的还是不够啊,还要再努力才行,希望各位读者也是,不要停止自己的学习)
——————————————回到正文——————————————
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);
}
PWM模式1,我们同样Go to以后可以看见有其他模式可选,PWM模式和之前的向上/向下计数共同决定该通道在什么时候为有效电平。
看见Enable肯定都明白啥意思了:)
我们Go to这个TIM_Pulse,看见它的描述:
划重点:0——FFFF,即十进制的 0——65535初始化的时候让他等于一个在0-65535之间的值,(参数m)即可设为这个通道的初始PWM
这个High是说,当计数器小于CCRx的数值的时候,这个通道的电平为高。Goto一下发现还有一个Low的选项,意思也不难理解。
分别是通道一的初始化和使能。
这样,我们就完成了TIM4的通道一(CCR1)PWM初始化配置,剩下的CCR2-4也是同理,为了练习请自行把剩下的通道按照已有程序修改好,再写上。
PWM初始化就写完了,而具体的赋值会在下一部分里讲
——————————————————————————
在写完上述函数后,再回到TIM4.h里声明一下就好了
——————————————————————————
首先来回顾一下昨天尚未完成的Run函数:
可见,我们需要控制的有电机,还有舵机。先来说电机:
我们再来看一下TB6612的背面:
很明显,PWMA 和 PWMB就是分别控制两个轮子的转速的。
怎么赋值PWM呢?
第一种,也是最直接的方法,就是控制这个寄存器,以达到输出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
这几个函数在前文和昨天都已经写过了,就不再这里说了
其实原理非常简单:
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 3、4一组5、6一组这么一直写下去,然后
控制舵机的总周期就是 2.5ms * 舵机数
例:10000 = 2500 * 4,就是控制了四个舵机,然后case总共有4组,从1到8
}
然后把这个函数放到定时器中断服务函数内部,并且在执行完以后让 i++,若是只有一个舵机的话,当i++完以后,进入case 2时让 i = 0,以此使得控制 i 始终在一定范围内
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完结后,根据时间安排会再开机器学习的系列,欢迎各位与我共同学习,一起进步!
点我进入下一期:最后一天!如何让车跑得更好?.