鱼鹰在做一个项目时,曾经遇到一个问题,8 路 PWM 输出,有一个高级定时器死活无法输出PWM,另一个高级定时器却可以顺利输出,初始化配置完全是一样的。

 

根据鱼鹰的经验,定时器没有输出有几个方面:

 

1、如果通过中断翻转电平输出PWM,那么需要检查是否进入中断(检查中断控制器是否开启中断,外设相应中断是否开启)。

 

2、如果使用 PWM模式,一般问题出在 IO复用功能和重映射上。如果没有使用 IO的复用功能,那么它是不可能被定时器外设所驱动的。而如果你的 IO不是该定时器默认的输出 IO,那么就需要进行重映射。而STM32F1 和 STM32F4 的重映射机制是不一样的。

 

 

可以看到 F4的重映射比较简单,直接和 GPIO绑定,不需要另外开启时钟,并且我们可以从该函数的参数表中直接找到我们想要的对应 IO复用功能(上面的代码代表 GPIOD_12的 TIM4 复用功能)。

 

 

但是 F1 分为部分映射,全映射,还可能有部分映射2……

 

 

没有参考手册,你根本不知道这些映射对应的到底是哪个引脚。此时,我们打开相应参考手册,找到 GPIO那一章节,AFIO 小节,找到定时器部分,你可以看到所有定时器的IO映射关系。

 

 

从这张图,我们可以得到以下几点信息:

 

1)如果不开启重映射(没有重映射),定时器2默认输出 IO为:PA0、PA1、PA2、PA3。2)部分重映射1 :PA15、PB3、PA2、PA3。3)部分重映射2:PA0、PA1、PB10、PB11。4)完全重映射:PA15、PB3、PB10、PB11。


从中我们也可以看到另一个坑,那就是可能端口换了,而你相应的时钟并没有打开,导致初始化配置失败,最终导致无法输出。而使用 PB3 时必须禁用部分 JTAG引脚才行。

 

而在重映射上,鱼鹰也确实踩了一个坑,到现在我也没明白为什么。当时在程序开始关闭了部分 JTAG引脚功能,然后再初始化定时器,并且开启相应重映射,最终还是没有任何输出,鱼鹰甚至直接查看最终的 AFIO寄存器的值,但情况就是配置的值一样,但还是无法输出,所幸的是,当我把复用功能在禁用部分 JTAG引脚后再开启所有定时器的重映射功能,发现能用了……

 

3、GPIO 的时钟没有打开,或者打开错误。

RCC_APB1PeriphClockCmd(RCC_APB2Periph_GPIOA ENABLE);

 

比如像这样,用 APB1的函数打开 APB2外设的时钟,当然会出现问题,当然这种坑还是蛮容易检查出来的


4、高级定时器的坑:

要想高级定时器输出脉冲,必须在初始化后增加一条语句:TIM_CtrlPWMOutputs(TIM8, ENABLE);没有这个函数,定时器是不可能输出脉冲的。毕竟是高级定时器,很傲娇,也很任性。

 

而因为它特殊性,鱼鹰也是在这里载了一个大跟头,这也是鱼鹰为什么要写这篇笔记的原因了。

 

定时器1 和定时器 8 同属于高级定时器,但定时器 8 可以正常输出,而定时器 1 却没法输出,这是怎么回事?为了解决这个问题,鱼鹰专门把定时器初始化部分封装成了一个函数,只留一个定时器作为参数部分:

 

 

但最终的结果还是没法输出,这不应该的啊,都是一样的函数,一样的配置,怎么一个成功,一个失败。认真检查了初始化函数的每一条语句,参数并没有任何问题。

 

那到底是什么问题导致的?

 

最终鱼鹰只能上网查找相关问题了,有可能就有前辈踩过这种坑呢。经过多方查找,鱼鹰尝试了各种办法也没有效果,最终死马当活马医的试了一个完全没有道理的可能:把串口初始化函数屏蔽了。

 

 

试了之后,发现定时器真的有输出了。那么这是怎么一回事?为什么串口还能干扰到定时器的输出。这个时候就要说一说我们的栈了(关于栈,鱼鹰写过一篇笔记《今天,你的栈溢出了吗?》)。

 

很多关于栈的话题,基本都是栈溢出,但事实上,还有一个容易忽略的话题是,栈的值不确定。我们都知道,函数进入时会进行压栈操作(用于保持寄存器的值),同时如果函数有局部变量,也可能会从栈中申请空间。

 

 

因为局部变量用完即毁,不会占用我们宝贵的 RAM资源,所以很多时候我们会选择使用局部变量。而大部分网上参考例程在使用局部变量时并不规范,导致问题的发生。

 

现在鱼鹰解释一下为什么串口函数会影响高级定时器的输出,而其他普通定时器并没有受影响。当串口函数执行时,使用的栈比较大,而在定时器函数执行时,刚好使用了这部分已被修改的栈空间,并且使用时没有初始化它,导致出现了问题。

 

 

那为什么屏蔽了串口就没有问题呢?那是因为单片机开始运行时,__main 函数会将栈空间全部清零(在运行到main前完成该工作),如果不运行串口函数,那么栈中的脏数据就不会很多,那么定时器函数的局部变量即使不初始化,也可认为就是 0,而这正是定时器需要的默认值。

 

 

那么为什么普通定时器不受影响呢?拿 TIM_TimeBaseInitTypeDef 结构体来举例。

 

 

基本定时器一般会初始化这几个变量,但是你查看该结构体的定义时你会发现:

 

 

该结构体还有一个变量是专为高级定时器准备的,你并没有对它进行初始化,此时它可能是任何值,并会直接赋值给高级定时器的寄存器中。而关于输出的结构体 TIM_OCInitTypeDef 更是如此,很多变量在基本定时器中不会使用,却会在高级定时器中影响它的输出功能。


所以为了解决这个问题,有两个办法:1、使用库函数提前初始化局部变量:

 

 

这样后续就不用关心不需要的变量了。2、直接在初始化时,将所有的结构体成员变量都初始化一遍,确定没有任何一个变量遗漏:

 

 

鱼鹰推荐第二种办法,这样高级定时器和普通定时器的代码可以统一,也能更直观的看出函数提供的功能。当然,两种方法同时使用也是没有任何问题的。

 

以上就是鱼鹰踩的坑,希望对各位道友有所帮助。咱们下期再见!