重温STM32基础:定时器(7)

发布时间:2026/10/19 7:16:46
重温STM32基础:定时器(7)
文章目录通用定时器4个通道的关系防御性编程-入参检查入参检查常用方法assert vs. if-else如何处理NaN如果写成 0.0f 500.0 会怎么样生成反汇编类型转换和浮点计算对比浮点计算单元FPUfloat和int16_t转换没有warning 遇到问题identifier uint16_t not defined...摘要本文总结了 STM32 开发中几个易踩的坑① 定时器 4 通道共用计数器频率一致、占空比独立、相位对齐② 入参检查与 NaN 处理③500.0f与500.0在无 FPU 的 STM32F103 上生成不同反汇编双精度更耗时④ 编译器将常量以 IEEE754 存入文字池float转uint16_t无 warning⑤ 模块化编程应包含stdint.h而非stm32f10x.h降低芯片耦合。通用定时器4个通道的关系结合之前PWM的三个参数公式之前文章提到过频率F r e q C K _ P S C ( 1 P S C ) ( A R R 1 ) Freq \frac{CK\_PSC}{(1 PSC)(ARR 1)}Freq(1PSC)(ARR1)CK_PSC​PWM占空比D u t y C C R A R R 1 Duty \frac{CCR}{ARR1}DutyARR1CCR​PWM分辨率基于上面对分辨率得定义R e s o l u t i o n 1 A R R 1 Resolution \frac{1}{ARR 1}ResolutionARR11​并且从框图可以看出4个通道共用一个计数器CNT共用ARR和prescalar-- 频率必须一样有各自的CCR – 4个通道的占空比可以不一样。有各自的输出极性所以4个通道的频率一样4个通道的占空比可以不一样它们的相位对齐对于课程中说的“相位对齐”做了一些理解。这需要一些前提假设定时器向上计数不是中央对齐计数并且配置PWM模式1.这个情况的“相位对齐”就是4个波形都有同一个起点/每个周期都从同一时刻开始。这听上去是4个通道“相互耦合、制约”但是其实不是。相位相同给与了我们精确控制相位的能力。在实际应用中要求几个通道有相同相位是常见的应用需求而另一方面如果真的要求几个通道之间有一定相位差可以用PWM模式2来任意控制相位。防御性编程-入参检查课程主要是介绍STM32的外设所以在代码上有一些写的比较简单。比如在这里的代码中对于Servo_SetAngle课程里面写的很简单voidServo_SetAngle(floatAngle){PWM_setCompare2(Angle/180*2000500);}这么写的话是不严谨的如果Angle是负数或者Angle大于180度可能会超过设备的物理极限。float 类型的数据是可能为NaN的not a number比如0/0的结果。程序接收到NaN会有不确定行为。入参检查常用方法可能可以有两个地方来检查进入函数之后立刻检查。比如在进入Servo_SetAngle之后第一步就检查。调用者检查。但是这就要求调用者知道函数内部的机制或者实现这违反软件设计的规则。通常使用第一种。而检查入参的方法用if-else检查入参值得范围。比如根据实际情况进行钳位把入参限制在合理值让程序继续运行。加上断言。调试期暴露bug。但是在release的时候断言可能会被去掉。还是不安全。返回错误码。这需要这个系统有一套错误处理机制。对于简单项目设计全套处理过于复杂。发现是不合理的值直接return;。有时候也这么用。对于简单项目或者简单的模块比较安全和适合的做法是把数值钳位在符合直觉的最大或最小值。几种处理方法根据实际情况来选择。assert vs. if-else类型assertif-else目的检查“程序员的错误”是为了保证最基本的函数使用的正确性比如不要传一个不存在的pin脚。。。检查运行时的错误处理“外部不按期望来”的情况结果直接停止程序通常要返回错误码有错误处理系统处理走降级逻辑等。一般保持程序运行。发布版本建议去掉减少代码体积必须在如何处理NaN下面的代码做了钳位但是还是有危险的voidtest(floata){floatvalid_a0.0f;if(a180.0f){valid_a180.0f;}elseif(a0.0f){valid_a0.0f;}else{// NaN会到这里valid_aa;}}NaN与任何有理数值的比较大于小于和等于都会return false也就是说如果这个输入的a是一个NaN它会进入最后一个else然后被认为是合法值进入底层。NaN只有不等于某个有理数会返回true包括和它自己比较。所以如果一个数和它自己不等就可以认为它是NaN。也就是可以用if(a!a){// a is an NaN}来判断NaN。但是这个项目代码中比较好的写法是这样的if(angle180.0f){validated_angle180.0f;}elseif(angle0.0f){validated_angleangle;// Valid range: 0.0f to 180.0f}else{validated_angle0.0f;// Catches negative numbers AND NaN}一个小点可以注意的是嵌入式系统中如果是两个互斥条件尽量考虑用“if…else (if)…”而不是“if… if…”。对于后者CPU对两个条件都要检查而有时满足了第一个条件就不会满足第二个第二个条件的检查就浪费了CPU的周期所以前者可以减少浪费加快执行速度。如果写成 0.0f 500.0 会怎么样生成反汇编在说具体区别之前需要准备一下反汇编文件。使用Keil IDE来编译没有在它的配置里找到自动生成反汇编文件虽然debug窗口可以给我们看到需要自己手动执行去生成。首先找到Keil的安装目录。找到我们使用的相应的编译器文件夹。课程中用的是ARM Compiler V5.06所以是这个文件夹中“ARM\Compiler_v5\bin”。里面有一个fromelf.exe这个编译器会编译出来axf文件在项目的Ojects文件夹中文件夹路径根据项目配置不同会变化用如下指令来生成disassembly代码。.\fromelf.exe-c--text E:\xx\xxx\6-4_PWM_Servo\Objects\Project.axfProject.asm“”指定输出文件名字和路径。如上写法就是在ARM\Compiler_v5\bin执行fromelf.exe生成在asm在这个文件夹下。.axf 文件是ARMCC生成的文件在.elf基础包含了更多的信息。编译器会生成多种文件。这篇文章中开头部分就有相应的对比 文件对比类型转换和浮点计算对比需要注意的是500.0f是一个单精度浮点类型float占4个字节500.0是一个双精度浮点类型double占8个字节一个运算式子中如果几个数的数据类型不同C语言会做一些相应的类型转换和提升。这也是坑很多的地方。因为很多时候我们没有意识到这点。这里做了一个500.0f和500.0所生成的反汇编得对比从两个红框可以看出两个的区别500.0的写法比500.0f的多了一个类型转换指令500.0跳转到了__aeabi_dadd而500.0f跳转到了__aeabi_fadd由于STM32F103没有浮点单元FPU所以所有的单精度浮点和双精度浮点计算都是用软件模拟比如图中所示的加法单精度浮点加法__aeabi_fadd双精度浮点加法__aeabi_dadd这两条指令之前都是BL.W,说明是跳转到另一个function也就是相应的实现。在反汇编文件中可以看到。两个function都非常长意味着软件模拟浮点计算需要很多的CPU 周期(cycles)。而__aeabi_dadd比__aeabi_fadd更长一个~ 100行一个~ 60行也就是双精度比单精度计算更耗时。这些指令数量的不同在一些简单软件中可能我们感受不到或者在一些稍微高端点的MCU中已经有了浮点计算单元会让人更“无感”但是如果完全不在意这些类型显然软件的一些行为可能和我们期望的不一样并且会让CPU做很多无谓的操作。如果这是一个持续的项目不知道是幸运还是不幸运大部分情况都会演变为shi山代码堆起来肯定会使性能降低。所以任何一个运算最好能够让类型一致让软件行为更加确定。数据类型够用就行不要动不动用double。当然对于那些没有FPU的设备提升运算的另一个方法就是用定点计算这也是一门很大的学问。根据我短暂的接触和肤浅的认知定点运算中饱和和缩放是要重点考虑的内容。浮点计算单元FPU刚才提到了浮点计算单元。它就是把一些浮点运算用硬件来做。向刚才单精度浮点加法可能要用六十多个周期用FPU只要一个周期就可以完成。但是这里也有一些点要明确一下。FPU不一定支持双精度浮点计算。比如STM32F401中有FPU但是只支持单精度浮点计算而STM32H743则有一个double-precision FPU。对于单精度FPU如果让它做双精度浮点计算也是需要耗费很多周期的。有了FPU也不意味着所有浮点运算都可以一个周期干完。一般的FPU对加减乘可以做到单周期吞吐但是除法运算或者开根号还是需要很多周期float和int16_t转换没有warning 老师写的代码是voidServo_set_angle(floatangle){PWM_set_compare(angle/180*2000500);}其中PWM_set_compare的入参是uin16_t。这里按照理解似乎至少涉及两次类型转换除法中的整型变成float以及float的结果变成uint16_t。但是在反汇编中并没有看到类似于float 到double转换的指令。这是因为编译器已经把这些常值以IEEE754编码规则进行转换并且存在了flash里。这里面有至少3个类似于“[pc,#40] ”的部分这些都是从文字池literal pool中加载相应的常量后面的4334000044fa0000和43fa000016进制表达就是180,2000和500的IEEE754的二进制表达。所以这里编译器做了优化没有浪费指令在运行时进行整型和float型的转换。可以发现按照课程中的这个写法即使有这样的类型转换也不会有warning。我搜索了相关打开warning的编译flag都看不到warning。或许ARM Compiler v5.06直接认为这没有风险遇到问题identifier uint16_t not defined…课程中老师会在每个c文件开头都先包含stm32f10x.h。这节课中需要写一个Servo模块。项目中涉及了一些模块个人认为是这样一个层级关系PWM是对定时器TIMER的某些功能的封装Servo会调用PWM所以在Servo模块中我认为已经不需要去接触stm32f10x.h这些底层的外设接口所以没有像课程中那样去包含它。然后编译的时候报错了“identifier uint16_t not defined…”uint16_t是标准库stdint.h中定义的而stm32f10x.h中包含了这个标准库文件。之前的情况没有发生这个错误包括老师的代码没有错误是因为每个moduleC文件都包含了stm32f10x.h。把stm32f10x.h加到Servo.c中当然可行但是我认为比较好的写法是直接在Servo.c中包含stdint.h而不是stm32f10x.h。因为如果包含了stm32f10x.h那就和芯片有不必要的耦合这个模块只能和这个芯片一起用。不方便移植虽然我也没想过移植。不过这显然是不合适的。而且从可读性上如果包含了stm32f10x.h别人会以为Servo操作了底层外设。但其实它只是要一个int16_t。。。.c文件中包含标准库stdint.h是一个常规操作