重温STM32基础:定时器(6)
文章目录TIM_OCxInit配置通道选择TIM_Output_Compare_and_PWM_modesTIM_OutputStateTIM_Pulse带N的域名补assert到底在防御什么未附初始值的坑否要调用TIM_CCxCmd引脚重映射的使用调试器disable的注意点关于调试器调试接口的哪些pin可以被释放什么时候要操作disable引脚的main function摘要本文是 STM32F103 标准外设库定时器输出比较与 PWM 配置的实战笔记。前半部分围绕TIM_OCxInit结构体配置梳理了输出比较模式、TIM_OutputState使能位、TIM_Pulse与TIM_SetCompare1的对应关系并分析了局部结构体未初始化导致的随机值坑。后半部分聚焦引脚重映射说明将 TIM2_CH1 重映射到 PA15 时需先解除 JTDI 调试端口复用并总结了 JTAG/SWD 调试引脚的区别与释放策略。这主要是一个用呼吸灯实验。配置定时器输出比较产生占空比不断变化的PWM波来控制LED产生呼吸灯效果。配置步骤为要用到的外设开启RCC时钟GPIO, TIM配置时基单元为TIM选择时钟时基单元配置配置输出比较单元配置GPIO复用启动TIM计数器启动TIM的运行控制TIM_OCxInit这是一个单通道配置函数。每个通用定时器有4个OC通道x在这里表示1~4。这里依旧用结构体来配置。配置通道选择实验中用的是PA0它的默认AF里包含TIM2_CH1。所以后面就会配置TIM2以及TIM2的通道1.TIM_Output_Compare_and_PWM_modes初始化函数中需要配置输出的模式。之前看过理论上有8种。但是库函数代码里有这样两个入参判断的宏绿框而且上面只定义了6个值而不是8个。4和5没了当然不是漏定义了而是定义在了别的地方。可以发现库里面给4和5的组起名为“forced action”和其他模式不太一样它们不依赖CCR和CNT的比较结果强制一个输出值。虽然0模式也时无视比较结果但它没有强制输出。所以按照这样来理解IS_TIM_OC_MODE是用来检查入参是不是用到了比较结果。它被用在TIM_OCxInit中。也就是说如果我在TIM_OCxInit想设置“强制输出高/低”的模式这是会报错的。IS_TIM_OCM用来检查入参是不是8种模式之一。用在TIM_SelectOCxM的入参检测。这是一个模式切换函数自然需要接受所有模式。总之TIM_OCxInit中不可以用两个forced mode.猜测ST的设计意图可能他们认为系统初始化的时候调用TIM_OCxInit的时候使用者的意图肯定是要用CNT和CCR的比较结果做一些什么事情而不是上来就强制输出。他们把8中模式做了这两种区分可能是为了明确“初始化 VS 运行时配置”的职责划分。TIM_OutputState这里合法值只有0x0001和0x0000并且写入的是CCER寄存器它的bit 0是配置OC1的使能。可以发现4个TIM_OCxInit函数的用户输入都是一样的。难道2,3,4通道也只是配置了CC1当然不是不同通道的配置函数里面会对输入做移位处理保证配置在寄存器的对应通道bits。这是库函数的封装。课程中老师有感叹“定时器的库函数是海量”。现在看来海量的一部分原因是大部分TIM有4个通道每个通道都有各自的配置函数而各种功能的配置参数又非常多几乎每个配置参数都有自己独立的函数来进行单独配置。。。“海量”不在话下啊。。。TIM_Pulse这是结构体里输出比较值的域名字。在LED呼吸灯的实验中想到肯定要改变PWM值那就一定会有对这个参数的修改的接口。在海量的函数中并没有找到“Pulse”关键字。。。然后发现修改它的函数名字叫 TIM_SetCompare1()。带N的域名所有名字里带有“N”的域名都是高级定时器要用到的参数主要是互补输出的配置需要。当然也有一些不带N的也是高级定时器相关配置。如果使用通用定时器这些高级定时器的参数就可以不去配置。至于到底有哪些需要配置一个是对通用定时器非常熟悉能够精确定位所有需要的参数另一个方法个人觉得可以参考库函数的实现比如TIM_OC1Init中当配置到高级定时器相关参数时才会对它们进行检查也就是说这些参数是通用定时器不会用的。但这只是参考和个人观点而且不能认为“只有检查的才用到”。比如这个函数中没有对“TIM_Pulse”进行检查因为它是一个0x0~0xFFFF的值。TIM_Pulse会被写入一个寄存器的前16bits但本身就被定义成uint16_t所以即使我们写入一个超过16位的值也会被C语言的类型系统截断。输入寄存器的是合法值。补assert到底在防御什么看起来虽然TIM_Pulse“全域合法”但是是不是还是加上assert会好一些比如检查它是不是在0~0xFFFF范围因为如果真的有人写了一个0x12345进去如果不报错那他会期望达到写入0x12345的效果但实际不会这种情况看起来很难debug如果有assert来检查那用户直接可以定位参数超限。或许还是不加的好。首先在TIM_OCxInit中加入对这个值的检查其实无法防御超限值。因为截断发生在赋值的时候“TIM_OCInitStruct.TIM_Pulse 0x12345”运行了这句这个时候TIM_Pulse就已经是uin16_t范围内的了assert不会报错。如果对这个uint16_t的入参检查那逻辑上要对所有函数的所有数值参数都要加检查这是一笔很大的代码体积和运行时开销。assert的目的不是为了“防呆”而是为了防御运行时出现非法状态而非数值检查。所以库函数中一般只对枚举/有限状态参数进行检查而不会检查全域合法的参数。所以如何有效防御上面的问题这大部分就取决于库使用者了打开编译器的类型告警比如-Wconversion以及对外设的功能和寄存器有清晰的认识。未附初始值的坑老师在课程中分享了一个因为没有初始化一个局部变量导致后面程序运行诡异的坑甚至发现修改这行代码的位置居然会对程序造成不同的结果。这种诡异现象我也遇到过由于OC初始化所用的结构体包含很多参数其中有些参数只在高级定时器中会被使用。如下写法其实比较危险。TIM_OCInitTypeDef TIM_OCInitStruct;// no initializationTIM_OCInitStruct.TIM_OCModeTIM_OCMode_PWM1;TIM_OCInitStruct.TIM_OCPolarityTIM_OCPolarity_High;TIM_OCInitStruct.TIM_OutputStateTIM_OutputState_Enable;TIM_OCInitStruct.TIM_Pulse0xFF;TIM_OCInitStruct其实还包含TIM_OCIdleState等域当我们在函数中声明一个局部变量的时候就是开辟了一块栈空间但是只写“TIM_OCInitTypeDef TIM_OCInitStruct;”并没有给这块内存空间赋值这块地方是一些随机值可能之前某些出栈的时候遗留下来的东西。这个时候没有被显式赋值的域的空间是这些随机值如果后面在代码更新迭代的时候开始用了这些域但是又没有去显式赋值或者认为这些值是默认0那么后面的程序大概率不会按照我们预想的来跑总之声明一个局部变量它所占空间不会被默认初始化为0。保证局部变量整体有确定性的值。比较好的做法是给一个确定的初始值然后根据我们的需求修改相应的部分。初始化的两种写法一种写法是直接给0另一种当然是更加合理安全的写法是调用库提供的结构体初始化函数这里面的值都是合理合法的域的默认值// method 1TIM_OCInitTypeDef TIM_OCInitStruct{0};// method 2TIM_OCInitTypeDef TIM_OCInitStruct;TIM_OCStructInit(TIM_OCInitStruct);否要调用TIM_CCxCmd不用。。。虽然调用了也不会错实际上它确实是使能某个TIM的某个通道。但是这个使能的事情已经在TIM_OC1Init中做了就是通过结构体里的这个域TIM_OCInitStructure.TIM_OutputStateTIM_OutputState_Enable;外设在被初始化时会用一个结构体来接受所有参数然后再初始化函数中操作写入各种寄存器。这个结构体包含很多参数。同时库函数又提供了单独操作各个参数的函数。我以为这些函数名字会和参数名字对应。但是不是比如TIM_CCxCmd居然时操作TIM_OutputState我以为TIM_CCxCmd是另外一个使能还有结构体里面的“Pulse”其实它对应的是CC值现有的单独控制这个参数的函数是TIM_SetCompare1这和”Pulse“毫无关系。咨询了一下AI它把这种参数名和修改这些参数的函数名“货不对板”的情况做了如下解释标准库中对函数或者变量命名方式有时候从寄存器角度有时候从功能角度。而且这个struct和配置独立参数的函数用在程序的不同阶段初始化和运行时没有硬性规定说必须一致。AI说的好像这么一回事但是大家也不是不知道它一般可以把黑的说成白的然后还很让人类信服。关于这个解释没有出处。算是AI从混沌当中提炼的结论吧。我尝试找一下规律发现不太有。。。但是我们可以记住的是初始化外设一般都是 xxx_Init(struct)这样一个函数入参为一个拥有这个外设所有parameters的struct。这是系统初始化的时候用到。而同时库函数还会为我们提供对每个parameters的set/get 函数。这主要用于运行时配置。有了这样一个“信念”当我们要修改某个parameter的时候就可以刻意去按照寄存器/功能去找相应的函数更方便的当然是让AI来找。。。引脚重映射的使用关于重映射的内容这在之前的定时器4中已经有提到过。但是当时自己没有意识到很重要的一个点。在这个实验中老师过了一下AF的使用提到了这点。TIM2_CH1默认映射在PA0上或者说PA0默认映射TIM2_CH1但是TIM_CH1可以重映射到PA15上。这就要通过AF的配置。需要注意的是TIM2的通道重映射是一组一起切换的部分重映射和全部重映射只有几个“档位”这在之前的文章中有描述。这里就要注意了PA15的main function不是一个普通的GPIO它是JTDI。如果要使用PA15的重映射的功能要把JTDI给disable。调试器disable的注意点PA15上电默认是用作调试端口的JTDI的复用如果要把它用作AF remap中的任何一个功能都需要先解除调试端口复用。解除的方式还是用GPIO_PinRemapConfig 它的入参里面有/* * arg GPIO_Remap_SWJ_NoJTRST : Full SWJ Enabled (JTAG-DP SW-DP) but without JTRST * arg GPIO_Remap_SWJ_JTAGDisable : JTAG-DP Disabled and SW-DP Enabled * arg GPIO_Remap_SWJ_Disable : Full SWJ Disabled (JTAG-DP SW-DP) * /注意不要随便用GPIO_Remap_SWJ_Disable 这样就没有调试端口了STLink就无法下载程序了关于调试器和调试功能有关的有下面5个pin脚大家都知道JTAG调试需要4个引脚JTCK, JTMS, JTDI, JTDOSWD调试只需要2个引脚SWCLKSWDIO但是这里和JTAG有关的还有一个复位引脚。它的作用复位JTAG的TAP(Test Access Port在MCU里面)的状态和相关调试电路而不是复位整个MCU。这个pin在JTAG的协议中是可选的。在大部分现代的MCU中会有上电复位电路把TAG复位到正确状态所以可以不用特地留这个Pin用作复位。SWD是ARM自己定义的更精简的调试协议目的是为了省引脚。。。不会经过TAP所以不会用到这个复位pin脚。它有自己的复位机制。另外从这5个pin脚定义可以看出SWD的SWCLK对应于JTAG的JTCK都是时钟信号SWD的SWDIO对应于JTAG的JTMS为什么不是对应于JTDI和JTDOJTAG中JTMS(JTAG Test Mode Selection)是TAP的控制信号控制TAP中的状态机真正数据传输确实是通过JTDI和JTDO它是一个全双工通信。而SWD的SWDIO其实把这三条线的功能都“合并”了它有的时候发命令有的时候传输数据是一个半双工的通信。关于调试器方面的信息可以参考这篇文章特别的第4章是关于JTAG和SWD的介绍。调试接口的哪些pin可以被释放RM中AFIO章节这个表格有一个清晰的展示其实从这5个引脚定义还可以看出不存在“JTAG能用但SWD不能用”的情况因为SWD的引脚是JTAG的“子集”。所以只有上面代码的三个选项。如果我们要选择PA15作为TIM2_CH1那么就要配置GPIO_Remap_SWJ_JTAGDisable。这是可以的因为课程用的STLink默认用的就是SWD移除JTAG不影响。如果我们调试程序不用JTAGPA15PB3和PB4都是可以被重映射作为他用的。什么时候要操作disable引脚的main function这波操作也带来一点疑问当把一个外设的引脚重映射到另外一个引脚的时候我们是不是都应该在代码里操作移除这个“另外一个引脚”的默认功能首先要明确的是这个“默认功能”指的就是pin definition表格中的“main”即上电复位后这个引脚的功能。是需要考虑移除的但是移除的方法取决于“默认功能”是什么。在STM32F103上有这样几种情况默认功能就是GPIO那么只要在配置GPIO mode的时候配置成“AF的两个模式如果默认功能是一些外设比如调试接口、晶振等它们可以被关闭。对于调试接口用GPIO_PinRemapConfig来关闭对于晶振用RCC关闭等。