[野火]电机应用开发实战指南¶
关于野火¶
开源共享,共同进步¶
野火在发布第一块STM32开发板之初,就喊出 开源共享,共同进步 的口号, 把代码和文档教程都免费提供给用户下载,而我们也一直把这个理念贯穿至今。
目前我们的产品已经包括 STM32、i.MX RT系列、GD32V、FPGA、Linux、emXGUI、 操作系统、网络、下载器 等分支,覆盖电子工程应用领域的各种常用技术, 其中教学类产品的代码和文档一直保持着开源的姿态发布到网络上, 为电子工程师排忧解难,让嵌入式没有难用的技术是我们最大的愿望。
联系方式¶
- 官网: http://www.embedfire.com
- 论坛: http://www.firebbs.cn
- github主页:https://github.com/Embdefire
- gitee主页: https://gitee.com/wildfireteam
- 淘宝: https://yehuosm.tmall.com
- 邮箱: embedfire@embedfire.com
- 电话: 0769-33894118
快速参与本项目(提交bug或文档修改)¶
开源代码和文档最重要的初衷是让大家参与进来,一起来找茬。
无论你是觉得代码写得不够漂亮、有bug,或是文档小到有错别字,大到有原理性的描述错误, 都可以直接通过项目的github提交给我们,我们会视具体的情况进行审核处理加入到主线或特性分支。
野火的每个项目的《 ../README 》文件中都包含有项目的git仓库地址, 通过git提交 issue 、pull request 的方式可参与到项目。
我们主要使用github仓库进行维护和更新,gitee仓库用于方便用户快速下载, 我们一般只在gitee上同步github的master分支,所以参与项目时请尽量使用github。
轻度参与,提交issue¶
如果你发现了一些bug或小问题,而自己暂时没时间帮助我们修改的话, 可以直接在项目的github中提交issue告诉我们,我们会适时进行处理。
深度参与,提交pull request¶
如果你想深度参与本项目,可以先克隆或fork项目的仓库到本地, 修改编辑后向我们提交pull request,我们会审核处理。
TODO和悬赏任务¶
本页记录待改进的事项,以后会在此页发布任务清单,完成部分任务可获得 奖励 ,敬请期待!
如果您想参与到项目,可按《 快速参与本项目(提交bug或文档修改) 》的说明操作。
代码开发任务¶
此处写具体的任务列表
前言¶
关于本书¶
电动机(Motor)是用来把电能转换为机械能的电气设备,俗称马达,其主要作用是产生驱动力矩, 作为用电器或小型机械的动力能源。随着技术的发展电机在速度、转矩和位置的得以精确的控制, 使得电动机在实际应用中非常广泛,生活中随处可见,在我们的打印机、雕刻机、智能家居和控制机械等等都有电机的身影, 电动机也是他们不可或缺的一部分。本书分为两个部分来从原理到驱动方法再到应用的为大家讲解电机的控制。
第一部分为基础部分,该部分对于各种电机的组成原理、驱动方法以及什么是编码器、 驱动器都进行了详细的讲解,以及如何实现简单的开环控制;电机会分为直流、步进以及舵机单独进行讲解, 每种电机会先讲解其工作原理和驱动方法,在了解其工作原理和驱动方法后,我们再通过编程来让电机转起来。
本书第二部分为提高部分 ,主要讲解高级的电机控制使用方法和源码的讲解,这部分从实践的角度出发, 不仅有理论的推导,还有从理论到实践的全过程,将理论知识用到电机应用开发,对于什么是闭环控制系统, PID参数怎样整定,步进电机的高级应用,这部分都会讲解,从而真正意义上的玩转电机应用。
本书的参考资料¶
本书的配套硬件¶
为什么学习电机应用开发?¶
本章主要从电机的应用场景,来讨论为什么需要学习电机的应用开发。
电机的应用场景可以说是遍布在生活中无处不在,只要涉及到可以旋转的场景,百分之90以上都会用到电机, 工业上的打印机、雕刻机、机床;汽车上的空调、雨刷、电动车门安全气囊的驱动; 生活中常见的电动自行车、共享单车、平衡车、电动滑板车; 就连我们每天不离手的手机振动都是有电机振动产生的,以上的例子足以证明电机的广泛市场。
电机可以简单的分为步进电机、直流电机、以及舵机。电机的应用场景主要是由于电机的自身特点决定的, 步进电机的高精度、脉冲数与步进角度成正比、脉冲频率决定速度的特点,决定了它在数控机床制造领域使用极其广泛; 直流电机的启动和调速性能较好,广泛用于平衡车、电动滑板车、智能车、无人机、录像机、吹风机等适用于高速的场景中; 舵机的角度可以根据脉冲宽度来进行调制,广泛用于航空、航海的模型中,并且航天方面,导弹姿态变换的俯仰、偏航、 滚转运动都是靠舵机相互配合完成的。
电机的应用市场极其庞大,有市场就会有需求,有了需求那么也就自然有学习的必要了。
如何学习电机应用开发?¶
需要掌握的技能¶
希望经过前面介绍的电机开发的应用场景对学习电机应用开发产生浓厚的兴趣, 并且能让你更加坚定地开始学习电机应用开发。那么该如何学习呢? 对于电机驱动开发工程师,会有如下基本要求:
- 学好基本的数学知识
- 熟悉使用C语言,要求会使用指针、结构体及各种C库的使用
- 熟悉在Keil5下编写stm32程序并掌握高级定时器的使用
- 熟悉各种常用的设备通信协议,如UART、I2C及SPI等
- 了解步进电机、直流有刷电机、直流无刷电机、永磁同步电机及舵机的工作原理
- 熟悉并掌握PID闭环控制系统以及参数的整定
- 熟悉使用控制算法
- 熟悉使用RTOS,例如:FreeRTOS、RT-Thread等
总而言之,电机应用开发涉及领域极其广泛,不仅需要有编程功底,还需要具有数学功底, 不要看到数学两个字就拒之门外,当你可以控制直流电机转速时,你会发现原来PID闭环控制也不过如此。 电机控制是一个真正由理论到实践的过程,只有通过大量的公式推导和动手实践以及耐心的调整参数,才能够深入的理解并掌握。
学习要培养兴趣,要有耐心。只有带着兴趣出发,才不会枯燥乏味。学习是一个螺旋上升的过程, 学习到后面的章节,再回过头来再看一遍本书,会发现刚开始学习时以为自己了解的东西又深入了一遍。
推荐书单¶
- 《电机和电源控制中的最新微控制器技术》
- 野火《emWin实战指南》
- 野火《STM32库开发实战指南》,快速上手MCU开发并巩固C语言相关的知识。
- 野火《RT-Thread内核实现与应用开发实战指南》和《FreeRTOS内核实现与应用开发实战指南》。
- 《自动控制原理》
电机的分类介绍¶
电机的简介¶
电机是一种可以在电能和机械能的之间相互转换的设备,其中发电机是将机械能转换为电能, 电动机是将电能转换为机械能。发电机的主要用于产生电能,用途单一, 但是电动机主要用于产生机械能,用途极其广泛。
电机的分类¶
电机种类多种多样,自然分类也是多种多样的,可以按照工作电源种类划分、按照结构和工作原理划分、 按照启动与运行方式划分、按照用途划分、按照运转速度划分等等。可以说是分类五花八门,但是在实际应用中, 工程师会根据电机的特性来分,例如:对速度要求高的会选择直流电机,对精度要求高的会选择步进电机等等; 接下来我们将会从众多类型中的电机中选择几个具有代表性、普遍性的常用电机。
直流电机¶
在直流电机中还分为普通的直流电机、直流减速电机,有刷和无刷,共分为:
- 直流有刷电机
- 直流有刷减速电机
- 直流无刷电机
- 直流无刷减速电机
这四种电机。 从字面意思上就可以想象的到,普通的直流电机和直流减速电机相差的只有“减速”,它们在构造上相差的是一个减速齿轮组。 普通的直流电机当空载时,电机的转速由电压决定;直流减速电机的转速由齿轮组和电压决定; 齿轮组的作用是,提供较低的转速,较大的力矩;同时不同的减速比会提供不同的转速和力矩。这样就大大提高了减速电机的使用率。

图 1-1 减速电机齿轮箱(图片来自网络)
接下来就是有刷与无刷的区别:有刷和无刷的字面意思是有无碳刷;有刷电机电机工作时需要线圈和换向器旋转, 磁钢和碳刷不转,线圈电流方向的交替变化是随电机转动的换相器和电刷来完成的。 无刷直流电动机是采用半导体开关器件来实现电子换向的,使用电子开关器件代替传统的接触式换向器和电刷。

图 1-2 左侧为有刷电机、右侧为无刷电机(图片来自网络)
步进电机¶
步进电机是一种可以将脉冲信号转换为角位移或线位移的开环控制电机,在空载低频的情况下,一个脉冲就是一步, 可以精准的控制旋转角度;步进电机按照构造方式分类,分为三类分别是反应式、永磁式和混合式。
构造方式 | 特性说明 |
---|---|
反应式 | 结构简单、成本低、步距角小,可达1.2°、但动态性能差、效率低、发热大,可靠性难保证。 |
永磁式 | 其特点是动态性能好、输出力矩大,但这种电机精度差,步矩角大(一般为7.5°或15°)。 |
混合式 | 其特点是输出力矩大、动态性能好,步距角小,但结构复杂、成本相对较高。 |
按照定子上绕组可分为二相、三相和五相等系列,但是最受欢迎的是两相混合式步进电机,约占97%上的市场份额, 基本上是占据了整个市场;所以后面也会主要针对两相混合式步进电机进行讲解。你一定听过42步进电机, 那么“42”指的是什么呢?下面这张图会告诉你“42”到底是什么意思。

图 1-3 42步进电机尺寸图
42步进电机是指步进的尺寸,混合式步进电机一般都是正方形,外框尺寸为42mm*42mm; 除42之外还有57步进电机,86步进电机,也都是因外框尺寸而得名的。
伺服电机¶
什么叫伺服电机?伺服电机长什么样子?“伺服”一词是来源于希腊语“奴隶”的意思, 那么伺服电机也可以理解为绝对服从控制信号指挥电机,所以伺服电机是指在伺服系统中被控制的电机。 如果单指一个电机的话,那只能算一个被控的机械元件,但是加上闭环控制系统就可以称之为伺服系统中的电机。

图 1-4 伺服电机图
伺服电机分为直流和交流伺服电动机两大类,其主要特点是,当信号电压为零时无自转现象,转速随着转矩的增加而匀速下降。
- 直流伺服电机特性
- 机械特性 在输入的电枢电压保持不变时,电机的转速随电磁转矩变化而变化的规律
- 调节特性 直流电机在一定的电磁转矩(或负载转矩)下电机的稳态转速随电枢的控制电压变化而变化
- 动态特性 从原来的稳定状态到新的稳定状态,存在一个过渡过程,这就是直流电机的动态特性
- 交流伺服电机特性
- 无电刷和换向器,因此工作可靠,对维护和保养要求低
- 定子绕组散热比较方便
- 惯量小,易于提高系统的快速性
- 适应于高速大力矩工作状态
舵机¶
舵机是一种常见的伺服电机,由小型直流电机、控制电路板、电位计和齿轮组构成,舵机的用途广泛; 舵机可按照信号类型划分、按照齿轮划分、按照用途划分;舵机分90°、180°、270°和360°舵机,其中180°的舵机最为常见。

图 1-5 模拟信号舵机(MG995)图
按照信号类型划分为 模拟信号舵机和数字信号舵机
- 模拟舵机:无MCU微控制器,电路为模拟电路,同样的舵机之间会有性能差异
- 数字舵机:有MCU控制器,一般内部采用算法优化,性能比模拟舵机好
按照齿轮划分为 金属齿轮舵机和塑料齿轮舵机
- 金属齿轮舵机:适用于大扭力和高速场合
- 塑料齿轮舵机:成本低适用于中低扭矩场合
stm32定时器详解¶
定时器与电机的关系¶
对于电机而言用什么去控制至关重要,具体的控制方法这与电机的内部构造和原理有关;
一般电机控制可以分成两种:电压控制和电流控制;小的时候应该都玩过四驱车并且拆过,四驱车里面的小马达是上电就可以转的, 那么什么情况可以使得这个小马达转速变快或者变慢呢?答案很简单,两节干电池串联接在一起与一节干电池相比,一定是两节干电池的 马达转速快;还有一节干电池,满电与没电时的马达的转速差距也是很大的,这就是典型的 电压控制,通过改变电压的大小来改变电机的特性; 四驱车中的电机是直流电机,拆过小电机的都知道电机外壳的内测有两块磁铁,那么如果说我可以控制两块磁铁的磁性是不是也能控制电机转动呢? 还记得小时候做过的将一根铁钉外缠慢带有绝缘皮的铜线,通电后可以吸引起缝衣针的实验么,这就是典型的利用电流流向产生磁性,如果电机定子两侧 换成这种结构那就可以通过控制电流来控制旋转了。这种就是步进电机的大致原理(具体在步进电机基础章节会详细介绍),也是典型的 电流控制。
那么怎么样才能更好的控制电压和电流呢?
手动控制?数字电路控制?貌似都不是一个有效并且高效的办法,最有效且性价比较高的就是MCU控制了,也就是单片机控制。单片机具有定时器这一外设, 其实最主要的就是利用MCU可以精准定时计时这一特性,但是MCU多种多样,有AT89C51、AVR、stm32、等等,这些MCU都有定时器,但是功能却不同, 低端的51单片机只有定时功能,如果需要使用PWM或者脉冲,只能使用模拟的方式输出;高端的51、AVR单片机可以直接输出PWM, 可定时器的主频不是很高并且定时器的功能也不是很丰富,所以使用stm32就是一个非常好的选择了,它不仅拥有强大的定时器外设,而且还有高级定时器可 直接在硬件处理电机死区和刹车等问题,不仅减轻了MCU的压力而且可以精确地控制。
驱动电机需要定时器输出的信号满足电机的最小频率或者最小电压值,但是MCU功能再强也是不能够直接驱动电机的,需要使用驱动器来驱动,关系图如下:

stm32主要是利用定时器来输出控制信号来控制驱动器,然后由驱动器将信号放大或转换后驱动电机。所以说如何配置定时器就显得尤为重要了, 接下来的章节将着重讲解定时器相关内容,为之后的提高做好基础。
TIM-基本定时器¶
TIM简介¶
定时器(Timer)最基本的功能就是定时了,比如定时发送USART数据、定时采集AD数据等等。 如果把定时器与GPIO结合起来使用的话可以实现非常丰富的功能,可以测量输入信号的脉冲宽度,可以生产输出波形。 定时器生产PWM控制电机状态是工业控制普遍方法,这方面知识非常有必要深入了解。
STM32F4xx系列控制器有2个高级控制定时器、10个通用定时器和2个基本定时器,还有2个看门狗定时器。 看门狗定时器不在本章讨论范围,有专门讲解的章节。控制器上所有定时器都是彼此独立的,不共享任何资源。定时器功能参考下表:

定时器功能表
其中最大定时器时钟可通过 RCC_DCKCFGR 寄存器配置为 84MHz 或者 168MHz。定时器功能强大, 这一点透过《STM32F4xx 中文参考手册》讲解定时器内容就有 160多页就显而易见了。 定时器篇幅长,内容多,对于新手想完全掌握确实有些难度,特别参考手册是先介绍高级控制定时器, 然后介绍通用定时器,最后才介绍基本定时器。实际上,就功能上来说通用定时器包含所有基本定时器功能, 而高级控制定时器包含通用定时器所有功能。所以高级控制定时器功能繁多,但也是最难理解的, 本章我们先选择最简单的基本定时器进行讲解。
基本定时器¶
基本定时器比高级控制定时器和通用定时器功能少,结构简单,理解起来更容易,我们就开始先讲解基本定时器内容。 基本定时器主要两个功能,第一就是基本定时功能,生成时基,第二就是专门用于驱动数模转换器(DAC)。 关于驱动 DAC 具体应用参考 DAC 章节。
控制器有两个基本定时器 TIM6 和 TIM7,功能完全一样,但所用资源彼此都完全独立, 可以同时使用。在本章内容中,以 TIMx 统称基本定时器。
基本上定时器 TIM6 和 TIM7 是一个 16 位向上递增的定时器, 当我在自动重载寄存器(TIMx_ARR)添加一个计数值后并使能 TIMx,计数寄存器(TIMx_CNT)就会从 0 开始递增, 当 TIMx_CNT 的数值与 TIMx_ARR 值相同时就会生成事件并把 TIMx_CNT 寄存器清 0,完成一次循环过程。 如果没有停止定时器就循环执行上述过程。这些只是大概的流程,希望大家有个感性认识,下面细讲整个过程。
基本定时器功能框图¶
基本定时器的功能框图包含了基本定时器最核心内容,掌握了功能框图,对基本定时器就有一个整体的把握, 在编程时思路就非常清晰。首先先看图中绿色框内容,第一个是带有阴影的方框,方框内容一般是一个寄存器名称, 比如图中主体部分的自动重载寄存器(TIMx_ARR)或 PSC 预分频器(TIMx_PSC),这里要特别突出的是阴影这个标志的作用, 它表示这个寄存器还自带有影子寄存器,在硬件结构上实际是有两个寄存器,源寄存器是我们可以进行读写操作, 而影子寄存器我们是完全无法操作的,有内部硬件使用。影子寄存器是在程序运行时真正起到作用的, 源寄存器只是给我们读写用的,只有在特定时候(特定事件发生时)才把源寄存器的值拷贝给它的影子寄存器。 多个影子寄存器一起使用可以到达同步更新多个寄存器内容的目的。接下来是一个指向右下角的图标,它表示一个事件, 而一个指向右上角的图标表示中断和 DMA 输出。这个我们把它放在图中主体更好理解。图中的自动重载寄存器有影子寄存器, 它左边有一个带有“U”字母的事件图标,表示在更新事件生成时就把自动重载寄存器内容拷贝到影子寄存器内,这个与上面分析是一致。 寄存器右边的事件图标、中断和DMA 输出图标表示在自动重载寄存器值与计数器寄存器值相等时生成事件、中断和 DMA输出。

基本定时器功能框图
时钟源¶
定时器要实现计数必须有个时钟源,基本定时器时钟只能来自内部时钟, 高级控制定时器和通用定时器还可以选择外部时钟源或者直接来自其他定时器等待模式。 我们可以通过 RCC 专用时钟配置寄存器(RCC_DCKCFGR)的 TIMPRE 位设置所有定时器的时钟频率, 我们一般设置该位为默认值 0,即 TIMxCLK 为总线时钟的两倍,使得表 32-1 中可选的最大定时器时钟为 84MHz, 即基本定时器的内部时钟(CK_INT)频率为 84MHz。 基本定时器只能使用内部时钟,当 TIM6 和 TIM7 控制寄存器 1(TIMx_CR1)的 CEN 位置 1 时,启动基本定时器, 并且预分频器的时钟来源就是 CK_INT。对于高级控制定时器和通用定时器的时钟源可以来找控制器外部时钟、其他定时器等等模式, 较为复杂,我们在相关章节会详细介绍。
控制器¶
定时器控制器控制实现定时器功能,控制定时器复位、使能、计数是其基础功能,基本定时器还专门用于 DAC 转换触发。
计数器¶
基本定时器计数过程主要涉及到三个寄存器内容,分别是计数器寄存器(TIMx_CNT)、 预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR),这三个寄存器都是 16 位有效数字,即可设置值为 0 至 65535。
首先我们来看图中预分频器 PSC,它有一个输入时钟 CK_PSC 和一个输出时钟CK_CNT。 输入时钟 CK_PSC 来源于控制器部分,基本定时器只有内部时钟源所以CK_PSC 实际等于 CK_INT,即 90MHz。 在不同应用场所,经常需要不同的定时频率,通过设置预分频器 PSC CK_CNT,实际计算为:fCK_CNT 等于 fCK_PSC/(PSC[15:0]+1)。
图中是将预分频器 PSC 的值从 1 改为 4 时计数器时钟变化过程。原来是 1 分频, CK_PSC 和 CK_CNT 频率相同。向 TIMx_PSC 寄存器写入新值时,并不会马上更新CK_CNT 输出频率, 而是等到更新事件发生时,把 TIMx_PSC 寄存器值更新到影子寄存器中,使其真正产生效果。 更新为 4 分频后,在 CK_PSC 连续出现 4 个脉冲后 CK_CNT 才产生一个脉冲。

基本定时器时钟源分频
在定时器使能(CEN 置 1)时,计数器 COUNTER 根据 CK_CNT 频率向上计数,即每来一个 CK_CNT 脉冲, TIMx_CNT 值就加 1。当 TIMx_CNT 值与 TIMx_ARR 的设定值相等时就自动生成事件并 TIMx_CNT 自动清零, 然后自动重新开始计数,如此重复以上过程。为此可见,我们只要设置 CK_PSC 和 TIMx_ARR 这两个寄存器的值就可以控制事件生成的时间,而我们一般的应用程序就是在事件生成的回调函数中运行的。 在 TIMx_CNT 递增至与 TIMx_ARR 值相等,我们叫做为定时器上溢。
自动重载寄存器 TIMx_ARR 用来存放于计数器值比较的数值,如果两个数值相等就生成事件, 将相关事件标志位置位,生成 DMA 和中断输出。 TIMx_ARR 有影子寄存器, 可以通过 TIMx_CR1 寄存器的 ARPE 位控制影子寄存器功能,如果 ARPE 位置 1,影子寄存器有效, 只有在事件更新时才把 TIMx_ARR 值赋给影子寄存器。如果 ARPE 位为 0,修改TIMx_ARR 值马上有效。
定时器周期计算¶
经过上面分析,我们知道定时事件生成时间主要由 TIMx_PSC 和 TIMx_ARR 两个寄存器值决定, 这个也就是定时器的周期。比如我们需要一个 1s 周期的定时器,具体这两个寄存器值该如何设置内。 假设,我们先设置 TIMx_ARR 寄存器值为 9999,即当 TIMx_CNT从 0 开始计算,刚好等于 9999 时生成事件, 总共计数 10000 次,那么如果此时时钟源周期为 100us 即可得到刚好 1s 的定时周期。
接下来问题就是设置 TIMx_PSC 寄存器值使得 CK_CNT 输出为 100us 周期(10000Hz)的时钟。 预分频器的输入时钟 CK_PSC 为 84MHz,所以设置预分频器值为(8400-1)即可满足。
定时器初始化接头体讲解¶
HAL 库函数对定时器外设建立了四个初始化结构体,基本定时器只用到其中一个即TIM_TimeBaseInitTypeDef, 该结构体成员用于设置定时器基本工作参数,并由定时器基本初始化配置函数 TIM_TimeBaseInit 调用, 这些设定参数将会设置定时器相应的寄存器,达到配置定时器工作环境的目的。 这一章我们只介绍 TIM_TimeBaseInitTypeDef 结构体,其他结构体将在相关章节介绍。
初始化结构体和初始化库函数配合使用是 HAL 库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如了。 初始化结构体定义在 stm32f4xx_hal_tim.h 文件中, 初始化库函数定义在 stm32f4xx_hal_tim.c 文件中,编程时我们可以结合这两个文件内注释使用。
1 2 3 4 5 6 7 | typedef struct {
uint16_t Prescaler; // 预分频器
uint16_t CounterMode; // 计数模式
uint32_t Period; // 定时器周期
uint16_t ClockDivision; // 时钟分频
uint8_t RepetitionCounter; // 重复计算器
} TIM_Base_InitTypeDef;
|
- Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定TIMx_PSC 寄存器的值。 可设置范围为 0 至 65535,实现 1 至 65536 分频。
- CounterMode:定时器计数方式,可是在为向上计数、向下计数以及三种中心对齐模式。 基本定时器只能是向上计数,即 TIMx_CNT 只能从 0 开始递增,并且无需初始化。
- Period:定时器周期,实际就是设定自动重载寄存器的值,在事件生成时更新到影子寄存器。 可设置范围为 0 至 65535。
- ClockDivision:时钟分频,设置定时器时钟 CK_INT 频率与数字滤波器采样时钟频率分频比, 基本定时器没有此功能,不用设置。
- RepetitionCounter:重复计数器,属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数。 这里不用设置。
虽然定时器基本初始化结构体有 5 个成员,但对于基本定时器只需设置其中两个就可以,想想使用基本定时器就是简单。
基本定时器定时实验¶
在 DAC 转换中几乎都用到基本定时器,使用有关基本定时器触发 DAC 转换内容在 DAC 章节讲解即可, 这里就利用基本定时器实现简单的定时功能。
我们使用基本定时器循环定时 0.5s 并使能定时器中断,每到 0.5s 就在定时器中断服务函数翻转 RGB 彩灯, 使得最终效果 RGB 彩灯暗 0.5s,亮 0.5s,如此循环。
硬件设计¶
基本定时器没有相关 GPIO,这里我们只用定时器的定时功能,无效其他外部引脚, 至于 RGB 彩灯硬件可参考 野火《stm32HAL库开发实战指南》的GPIO 章节。
宏定义
1 2 3 4 | #define BASIC_TIM TIM6
#define BASIC_TIM_CLK_ENABLE() __TIM6_CLK_ENABLE()
#define BASIC_TIM_IRQn TIM6_DAC_IRQn
#define BASIC_TIM_IRQHandler TIM6_DAC_IRQHandler
|
使用宏定义非常方便程序升级、移植。
NVIC配置
1 2 3 4 5 6 7 8 9 10 11 12 | /**
* @brief 基本定时器 TIMx,x[6,7]中断优先级配置
* @param 无
* @retval 无
*/
static void TIMx_NVIC_Configuration(void)
{
//设置抢占优先级,子优先级
HAL_NVIC_SetPriority(BASIC_TIM_IRQn, 0, 3);
// 设置中断来源
HAL_NVIC_EnableIRQ(BASIC_TIM_IRQn);
}
|
实验用到定时器更新中断,需要配置 NVIC,实验只有一个中断,对 NVIC 配置没什么具体要求。
基本定时器模式配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | static void TIM_Mode_Config(void)
{
// 开启 TIMx_CLK,x[6,7]
BASIC_TIM_CLK_ENABLE();
TIM_TimeBaseStructure.Instance = BASIC_TIM;
/* 累计 TIM_Period 个后产生一个更新或者中断*/
//当定时器从 0 计数到 4999,即为 5000 次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = 5000-1;
//定时器时钟源 TIMxCLK = 2 * PCLK1
// PCLK1 = HCLK / 4
// => TIMxCLK=HCLK/2=SystemCoreClock/2=84MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=10000Hz
TIM_TimeBaseStructure.Init.Prescaler = 8400-1;
// 初始化定时器 TIMx, x[2,3,4,5]
HAL_TIM_Base_Init(&TIM_TimeBaseStructure);
// 开启定时器更新中断
HAL_TIM_Base_Start_IT(&TIM_TimeBaseStructure);
}
|
使用定时器之前都必须开启定时器时钟,基本定时器属于 APB1 总线外设。
接下来设置定时器周期数为 4999,即计数 5000 次生成事件。设置定时器预分频器为(8400-1), 基本定时器使能内部时钟,频率为 84MHz,经过预分频器后得到 10KHz 的频率。 然后就是调用 TIM HAL_TIM_Base_Init 函数完成定时器配置。 接下来设置定时器周期数为 4999,即计数 5000 次生成事件。设置定时器预分频器为 (8400-1),基本定时器使能内部时钟,频率为 84MHz,经过预分频器后得到 10KHz 的频率。 然后就是调用 TIM_HAL_TIM_Base_Init 函数完成定时器配置。
最后使用 HAL_TIM_Base_Start_IT 函数开启定时器和更新中断。
定时器中断服务函数
1 2 3 4 5 6 7 8 9 10 11 | void BASIC_TIM_IRQHandler (void)
{
HAL_TIM_IRQHandler(&TIM_TimeBaseStructure);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim==(&TIM_TimeBaseStructure)) {
LED1_TOGGLE; //红灯周期闪烁
}
}
|
我们在 TIM_Mode_Config 函数启动了定时器更新中断,在发生中断时,中断服务函数就得到运行。 在服务函数内直接调用库函数 HAL_TIM_IRQHandler 函数,它会产生一个中断回调函数 HAL_TIM_PeriodElapsedCallback,用来添加用户代码,确定是 TIM6 产生中断后才运行 RGB 彩灯翻转动作。
主函数
1 2 3 4 5 6 7 8 9 10 11 12 | int main(void)
{
/* 初始化系统时钟为 168MHz */
SystemClock_Config();
/* 初始化 LED */
LED_GPIO_Config();
/* 初始化基本定时器定时, 1s 产生一次中断 */
TIMx_Configuration();
while (1) {
}
}
|
实验中先初始化系统时钟,用到 RGB 彩灯,需要对其初始化配置。 LED_GPIO_Config 函数是定义在 bsp_led.c 文件的完成 RGB 彩灯 GPIO 初始化配置的程序。
TIMx_Configuration 函数是定义在 bsp_basic_tim.c 文件的一个函数,它只是简单的先后调用 TIMx_NVIC_Configuration 和 TIM_Mode_Config 两个函数完成 NVIC 配置和基本定时器模式配置。
下载验证¶
保证开发板相关硬件连接正确,把编译好的程序下载到开发板。开始 RGB 彩灯是暗的, 等一会 RGB 彩灯变为红色,再等一会又暗了,如此反复。如果我们使用表钟与 RGB 彩灯闪烁对比, 可以发现它是每 0.5s 改变一次 RGB 彩灯状态的。
TIM-高级定时器¶
特别说明,定时器的内容是以 STM32F40xx 系列控制器资源讲解。
上一章我们讲解了基本定时器功能,基本定时器功能简单,理解起来也容易。 高级控制定时器包含了通用定时器的功能,再加上已经有了基本定时器基础的基础, 如果再把通用定时器单独拿出来讲那内容有很多重复,实际效果不是很好,所以通用定时器不作为独立章节讲解, 可以在理解了高级定时器后参考《STM32F4xx 中文参考手册》通用定时器章节内容理解即可。
高级控制定时器¶
高级控制定时器(TIM1 和 TIM8)和通用定时器在基本定时器的基础上引入了外部引脚,可以输入捕获和输出比较功能。 高级控制定时器比通用定时器增加了可编程死区互补输出、重复计数器、带刹车(断路)功能,这些功能都是针对工业电机控制方面。 这几个功能在本书不做详细的介绍,主要介绍常用的输入捕获和输出比较功能。
高级控制定时器时基单元包含一个 16 位自动重载计数器 ARR,一个 16 位的计数器 CNT,可向上/下计数, 一个 16 位可编程预分频器 PSC,预分频器时钟源有多种可选,有内部的时钟、外部时钟。 还有一个 8 位的重复计数器 RCR,这样最高可实现 40 位的可编程定时。
STM32F407ZGT6 的高级/通用定时器的 IO 分配具体见下图。 配套开发板因为 IO 资源紧缺,定时器的 IO 很多已经复用它途,故下表中的 IO 只有部分可用于定时器的实验。
TIM1 | TIM8 | TIM2 | TIM5 | TIM3 | TIM4 | TIM9 | TIM10 | TIM11 | TIM12 | TIM13 | TIM14 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
CH1 | PA8/PE9 | PC6 | PA0/PA5 | PA0 | PA6/PC6/PB4 | PD12/PB6 | PE5/PA2 | PF6/PB8 | PF7/PB9 | PB14 | PF8/PA6 | PF9/PA7 |
CH1N | PA7/PE8/PB13 | PA5/PA7 | ||||||||||
CH2 | PE11/PA9 | PC7 | PA1/PB3 | PA1 | PA7/PC7/PB5 | PD13/PB7 | PE6/PA3 | PB15 | ||||
CH2N | PB0/PE10/PB14 | PB0/PB14 | ||||||||||
CH3 | PE13/PA10 | PC8 | PA2/PB10 | PA2 | PB0/PC8 | PD14/PB8 | ||||||
CH3N | PB1/PE12/PB15 | PB1/PB15 | ||||||||||
CH4 | PE14/PA11 | PC9 | PA3/PB11 | PA3 | PB1/PC9 | PD15/PB9 | ||||||
ETR | PE7/PA12 | PA0/PI3 | PA0/PA5/PA15 | PD2 | PE0 | |||||||
BKIN | PA6/PE15/PB12 | PA6/PI4 |
高级控制定时器功能框图¶
高级控制定时器功能框图包含了高级控制定时器最核心内容,掌握了功能框图, 对高级控制定时器就有一个整体的把握,在编程时思路就非常清晰,见下图。

高级控制定时器功能框图
关于图中带阴影的寄存器,即带有影子寄存器, 指向左下角的事件更新图标以及指向右上角的中断和DMA输出标志在上一章已经做了解释,这里就不再介绍。
①时钟源¶
高级控制定时器有四个时钟源可选:
- 内部时钟源CK_INT
- 外部时钟模式1:外部输入引脚TIx(x=1,2,3,4)
- 外部时钟模式2:外部触发输入ETR
- 内部触发输入
内部时钟源(CK_INT)
内部时钟CK_INT即来自于芯片内部,等于168M,一般情况下,我们都是使用内部时钟。 当从模式控制寄存器TIMx_SMCR的SMS位等于000时,则使用内部时钟。
外部时钟模式1

外部时钟模式1框图
①:时钟信号输入引脚
当使用外部时钟模式1的时候,时钟信号来自于定时器的输入通道,总共有4个,分别为TI1/2/3/4, 即TIMx_CH1/2/3/4。具体使用哪一路信号,由TIM_CCMx的位CCxS[1:0]配置,其中CCM1控制TI1/2,CCM2控制TI3/4。
②:滤波器
如果来自外部的时钟信号的频率过高或者混杂有高频干扰信号的话,我们就需要使用滤波器对ETRP信号重新采样, 来达到降频或者去除高频干扰的目的,具体的由TIMx_CCMx的位ICxF[3:0]配置。
③:边沿检测
边沿检测的信号来自于滤波器的输出,在成为触发信号之前,需要进行边沿检测,决定是上升沿有效还是下降沿有效, 具体的由TIMx_CCER的位CCxP和CCxNP配置。
④:触发选择
当使用外部时钟模式1时,触发源有两个,一个是滤波后的定时器输入1(TI1FP1)和滤波后的定时器输入2(TI2FP2), 具体的由TIMxSMCR的位TS[2:0]配置。
⑤:从模式选择
选定了触发源信号后,最后我们需把信号连接到TRGI引脚,让触发信号成为外部时钟模式1的输入,最终等于CK_PSC, 然后驱动计数器CNT计数。具体的配置TIMx_SMCR的位SMS[2:0]为000即可选择外部时钟模式1。
⑥:使能计数器
经过上面的5个步骤之后,最后我们只需使能计数器开始计数,外部时钟模式1的配置就算完成。使能计数器由TIMx_CR1的位CEN配置。
外部时钟模式2

外部时钟模式2框图
①:时钟信号输入引脚
当使用外部时钟模式2的时候,时钟信号来自于定时器的特定输入通道TIMx_ETR,只有1个。
②:外部触发极性
来自ETR引脚输入的信号可以选择为上升沿或者下降沿有效,具体的由TIMx_SMCR的位ETP配置。
③:外部触发预分频器
由于ETRP的信号的频率不能超过TIMx_CLK(168M)的1/4,当触发信号的频率很高的情况下, 就必须使用分频器来降频,具体的由 TIMx_SMCR的位ETPS[1:0]配置。
④:滤波器
如果ETRP的信号的频率过高或者混杂有高频干扰信号的话,我们就需要使用滤波器对ETRP信号重新采样, 来达到降频或者去除高频干扰的目的。具体的由TIMx_SMCR的位ETF[3:0]配置,其中的fDTS是由内部时钟CK_INT分频得到,具体的由TIMx_CR1的位CKD[1:0]配置。
⑤:从模式选择
经过滤波器滤波的信号连接到ETRF引脚后,触发信号成为外部时钟模式2的输入,最终等于CK_PSC,然后驱动计数器CNT计数。 具体的配置TIMx_SMCR的位ECE为1即可选择外部时钟模式2。
⑥:使能计数器
经过上面的5个步骤之后,最后我们只需使能计数器开始计数,外部时钟模式2的配置就算完成。使能计数器由TIMx_CR1的位CEN配置。
内部触发输入
内部触发输入是使用一个定时器作为另一个定时器的预分频器。硬件上高级控制定时器和通用定时器在内部连接在一起, 可以实现定时器同步或级联。主模式的定时器可以对从模式定时器执行复位、启动、停止或提供时钟。 高级控制定时器和部分通用定时器(TIM2至TIM5)可以设置为主模式或从模式,TIM9和TIM10可设置为从模式。
下图为主模式定时器(TIM1)为从模式定时器(TIM2)提供时钟,即TIM1用作TIM2的预分频器。

TIM1用作TIM2的预分频器
②控制器¶
高级控制定时器控制器部分包括触发控制器、从模式控制器以及编码器接口。触发控制器用来针对片内外设输出触发信号, 比如为其它定时器提供时钟和触发DAC/ADC转换。
编码器接口专门针对编码器计数而设计。从模式控制器可以控制计数器复位、启动、递增/递减、计数。
③时基单元¶

高级定时器时基单元
高级控制定时器时基单元包括四个寄存器,分别是计数器寄存器(CNT)、预分频器寄存器(PSC)、自动重载寄存器(ARR)和重复计数器寄存器(RCR)。 其中重复计数器RCR是高级定时器独有,通用和基本定时器没有。前面三个寄存器都是16位有效,TIMx_RCR寄存器是8位有效。
预分频器PSC
预分频器PSC,有一个输入时钟CK_PSC和一个输出时钟CK_CNT。输入时钟CK_PSC就是上面时钟源的输出,输出CK_CNT则用来驱动计数器CNT计数。 通过设置预分频器PSC的值可以得到不同的CK_CNT, 实际计算为:fCK_CNT等于f:sub:CK_PSC/(PSC[15:0]+1),可以实现1至65536分频。
计数器CNT
高级控制定时器的计数器有三种计数模式,分别为递增计数模式、递减计数模式和递增/递减(中心对齐)计数模式。
(1) 递增计数模式下,计数器从0开始计数,每来一个CK_CNT脉冲计数器就增加1, 直到计数器的值与自动重载寄存器ARR值相等,然后计数器又从0开始计数并生成计数器上溢事件,计数器总是如此循环计数。 如果禁用重复计数器,在计数器生成上溢事件就马上生成更新事件(UEV); 如果使能重复计数器,每生成一次上溢事件重复计数器内容就减1,直到重复计数器内容为0时才会生成更新事件。
(2) 递减计数模式下,计数器从自动重载寄存器ARR值开始计数,每来一个CK_CNT脉冲计数器就减1,直到计数器值为0, 然后计数器又从自动重载寄存器ARR值开始递减计数并生成计数器下溢事件,计数器总是如此循环计数。 如果禁用重复计数器,在计数器生成下溢事件就马上生成更新事件; 如果使能重复计数器,每生成一次下溢事件重复计数器内容就减1,直到重复计数器内容为0时才会生成更新事件。
(3) 中心对齐模式下,计数器从0开始递增计数,直到计数值等于(ARR-1)值生成计数器上溢事件, 然后从ARR值开始递减计数直到1生成计数器下溢事件。然后又从0开始计数,如此循环。 每次发生计数器上溢和下溢事件都会生成更新事件。
自动重载寄存器ARR
自动重载寄存器ARR用来存放与计数器CNT比较的值,如果两个值相等就递减重复计数器。 可以通过TIMx_CR1寄存器的ARPE位控制自动重载影子寄存器功能, 如果ARPE位置1,自动重载影子寄存器有效,只有在事件更新时才把TIMx_ARR值赋给影子寄存器。 如果ARPE位为0,则修改TIMx_ARR值马上有效。
重复计数器RCR
在基本/通用定时器发生上/下溢事件时直接就生成更新事件,但对于高级控制定时器却不是这样, 高级控制定时器在硬件结构上多出了重复计数器,在定时器发生上溢或下溢事件是递减重复计数器的值, 只有当重复计数器为0时才会生成更新事件。在发生N+1个上溢或下溢事件(N为RCR的值)时产生更新事件。
④输入捕获¶

输入捕获功能框图
输入捕获可以对输入的信号的上升沿,下降沿或者双边沿进行捕获, 常用的有测量输入信号的脉宽和测量PWM输入信号的频率和占空比这两种。
输入捕获的大概的原理就是,当捕获到信号的跳变沿的时候, 把计数器CNT的值锁存到捕获寄存器CCR中,把前后两次捕获到的CCR寄存器中的值相减,就可以算出脉宽或者频率。 如果捕获的脉宽的时间长度超过你的捕获定时器的周期,就会发生溢出,这个我们需要做额外的处理。
①输入通道
需要被测量的信号从定时器的外部引脚TIMx_CH1/2/3/4进入,通常叫TI1/2/3/4, 在后面的捕获讲解中对于要被测量的信号我们都以TIx为标准叫法。
②输入滤波器和边沿检测器
当输入的信号存在高频干扰的时候,我们需要对输入信号进行滤波,即进行重新采样,根据采样定律, 采样的频率必须大于等于两倍的输入信号。比如输入的信号为1M,又存在高频的信号干扰,那么此时就很有必要进行滤波, 我们可以设置采样频率为2M,这样可以在保证采样到有效信号的基础上把高于2M的高频干扰信号过滤掉。
滤波器的配置由CR1寄存器的位CKD[1:0]和CCMR1/2的位ICxF[3:0]控制。从ICxF位的描述可知, 采样频率fSAMPLE可以由fCK_INT和fDTS分频后的时钟提供, 其中是fCK_INT内部时钟,fDTS是fCK_INT经过分频后得到的频率, 分频因子由CKD[1:0]决定,可以是不分频,2分频或者是4分频。
边沿检测器用来设置信号在捕获的时候是什么边沿有效,可以是上升沿,下降沿,或者是双边沿,具体的由CCER寄存器的位CCxP和CCxNP决定。
③捕获通道
捕获通道就是图中的IC1/2/3/4,每个捕获通道都有相对应的捕获寄存器CCR1/2/3/4, 当发生捕获的时候,计数器CNT的值就会被锁存到捕获寄存器中。
这里我们要搞清楚输入通道和捕获通道的区别,输入通道是用来输入信号的, 捕获通道是用来捕获输入信号的通道,一个输入通道的信号可以同时输入给两个捕获通道。 比如输入通道TI1的信号经过滤波边沿检测器之后的TI1FP1和TI1FP2可以进入到捕获通道IC1和IC2, 其实这就是我们后面要讲的PWM输入捕获,只有一路输入信号(TI1)却占用了两个捕获通道(IC1和IC2)。 当只需要测量输入信号的脉宽时候,用一个捕获通道即可。输入通道和捕获通道的映射关系具体由寄存器CCMRx的位CCxS[1:0]配置。
④的预分频器
ICx的输出信号会经过一个预分频器,用于决定发生多少个事件时进行一次捕获。 具体的由寄存器CCMRx的位ICxPSC配置,如果希望捕获信号的每一个边沿,则不分频。
⑤捕获寄存器
经过预分频器的信号ICxPS是最终被捕获的信号,当发生捕获时(第一次), 计数器CNT的值会被锁存到捕获寄存器CCR中,还会产生CCxI中断,相应的中断位CCxIF(在SR寄存器中)会被置位, 通过软件或者读取CCR中的值可以将CCxIF清0。如果发生第二次捕获(即重复捕获:CCR寄存器中已捕获到计数器值且 CCxIF 标志已置 1), 则捕获溢出标志位CCxOF(在SR寄存器中)会被置位,CCxOF只能通过软件清零。
⑤输出比较¶

输出比较功能框图
输出比较就是通过定时器的外部引脚对外输出控制信号,有冻结、将通道X(x=1,2,3,4)设置为匹配时输出有效电平、 将通道X设置为匹配时输出无效电平、翻转、强制变为无效电平、强制变为有效电平、PWM1和PWM2这八种模式, 具体使用哪种模式由寄存器CCMRx的位OCxM[2:0]配置。其中PWM模式是输出比较中的特例,使用的也最多。
①比较寄存器
当计数器CNT的值跟比较寄存器CCR的值相等的时候,输出参考信号OCxREF的信号的极性就会改变, 其中OCxREF=1(高电平)称之为有效电平,OCxREF=0(低电平)称之为无效电平,并且会产生比较中断CCxI, 相应的标志位CCxIF(SR寄存器中)会置位。然后OCxREF再经过一系列的控制之后就成为真正的输出信号OCx/OCxN。
②死区发生器
在生成的参考波形OCxREF的基础上,可以插入死区时间,用于生成两路互补的输出信号OCx和OCxN, 死区时间的大小具体由BDTR寄存器的位DTG[7:0]配置。死区时间的大小必须根据与输出信号相连接的器件及其特性来调整。 下面我们简单举例说明下带死区的PWM信号的应用,我们以一个板桥驱动电路为例。

半桥驱动电路
在这个半桥驱动电路中,Q1导通,Q2截止,此时我想让Q1截止Q2导通,肯定是要先让Q1截止一段时间之后, 再等一段时间才让Q2导通,那么这段等待的时间就称为死区时间,因为Q1关闭需要时间(由MOS管的工艺决定)。 如果Q1关闭之后,马上打开Q2,那么此时一段时间内相当于Q1和Q2都导通了,这样电路会短路。
上图是针对上面的半桥驱动电路而画的带死区插入的PWM信号,图中的死区时间要根据MOS管的工艺来调节。

带死区插入的互补输出
③输出控制

输出比较(通道1~3)的输出控制框图
在输出比较的输出控制中,参考信号OCxREF在经过死区发生器之后会产生两路带死区的互补信号OCx_DT和OCxN_DT(通道1~3才有互补信号, 通道4没有,其余跟通道1~3一样),这两路带死区的互补信号然后就进入输出控制电路,如果没有加入死区控制, 那么进入输出控制电路的信号就直接是OCxREF。
进入输出控制电路的信号会被分成两路,一路是原始信号,一路是被反向的信号,具体的由寄存器CCER的位CCxP和CCxNP控制。 经过极性选择的信号是否由OCx引脚输出到外部引脚CHx/CHxN则由寄存器CCER的位CxE/CxNE配置。
如果加入了断路(刹车)功能,则断路和死区寄存器BDTR的MOE、OSSI和OSSR这三个位会共同影响输出的信号。
④输出引脚
输出比较的输出信号最终是通过定时器的外部IO来输出的,分别为CH1/2/3/4, 其中前面三个通道还有互补的输出通道CH1/2/3N。更加详细的IO说明还请查阅相关的数据手册。
⑥断路功能¶
断路功能就是电机控制的刹车功能,使能断路功能时,根据相关控制位状态修改输出信号电平。 在任何情况下,OCx和OCxN输出都不能同时为有效电平,这关系到电机控制常用的H桥电路结构原因。
断路源可以是时钟故障事件,由内部复位时钟控制器中的时钟安全系统(CSS)生成,也可以是外部断路输入IO,两者是或运算关系。
系统复位启动都默认关闭断路功能,将断路和死区寄存器(TIMx_BDTR)的BKE为置1,使能断路功能。 可通过TIMx_BDTR 寄存器的BKP位设置设置断路输入引脚的有效电平,设置为1时输入BRK为高电平有效,否则低电平有效。
发送断路时,将产生以下效果:
- TIMx_BDTR 寄存器中主输出模式使能(MOE)位被清零,输出处于无效、空闲或复位状态;
- 根据相关控制位状态控制输出通道引脚电平;当使能通道互补输出时,会根据情况自动控制输出通道电平;
- 将TIMx_SR 寄存器中的 BIF位置 1,并可产生中断和DMA传输请求。
- 如果 TIMx_BDTR 寄存器中的 自动输出使能(AOE)位置 1,则MOE位会在发生下一个UEV事件时自动再次置 1。
输入捕获应用¶
输入捕获一般应用在两个方面,一个方面是脉冲跳变沿时间测量,另一方面是PWM输入测量。
测量脉宽或者频率¶

脉宽/频率测量示意图
测量频率¶
当捕获通道TIx上出现上升沿时,发生第一次捕获,计数器CNT的值会被锁存到捕获寄存器CCR中,而且还会进入捕获中断, 在中断服务程序中记录一次捕获(可以用一个标志变量来记录),并把捕获寄存器中的值读取到value1中。 当出现第二次上升沿时,发生第二次捕获,计数器CNT的值会再次被锁存到捕获寄存器CCR中,并再次进入捕获中断, 在捕获中断中,把捕获寄存器的值读取到value3中,并清除捕获记录标志。利用value3和value1的差值我们就可以算出信号的周期(频率)。
测量脉宽¶
当捕获通道TIx上出现上升沿时,发生第一次捕获,计数器CNT的值会被锁存到捕获寄存器CCR中,而且还会进入捕获中断, 在中断服务程序中记录一次捕获(可以用一个标志变量来记录),并把捕获寄存器中的值读取到value1中。然后把捕获边沿改变为下降沿捕获, 目的是捕获后面的下降沿。当下降沿到来的时候,发生第二次捕获,计数器CNT的值会再次被锁存到捕获寄存器CCR中,并再次进入捕获中断, 在捕获中断中,把捕获寄存器的值读取到value3中,并清除捕获记录标志。然后把捕获边沿设置为上升沿捕获。
在测量脉宽过程中需要来回的切换捕获边沿的极性,如果测量的脉宽时间比较长,定时器就会发生溢出, 溢出的时候会产生更新中断,我们可以在中断里面对溢出进行记录处理。
PWM输入模式¶
测量脉宽和频率还有一个更简便的方法就是使用PWM输入模式。 与上面那种只使用一个捕获寄存器测量脉宽和频率的方法相比,PWM输入模式需要占用两个捕获寄存器。

输入通道和捕获通道的关系映射图
当使用PWM输入模式的时候,因为一个输入通道(TIx)会占用两个捕获通道(ICx), 所以一个定时器在使用PWM输入的时候最多只能使用两个输入通道(TIx)。
我们以输入通道TI1工作在PWM输入模式为例来讲解下具体的工作原理,其他通道以此类推即可。
PWM信号由输入通道TI1进入,因为是PWM输入模式的缘故,信号会被分为两路,一路是TI1FP1,另外一路是TI2FP2。 其中一路是周期,另一路是占空比,具体哪一路信号对应周期还是占空比,得从程序上设置哪一路信号作为触发输入, 作为触发输入的哪一路信号对应的就是周期,另一路就是对应占空比。作为触发输入的那一路信号还需要设置极性, 是上升沿还是下降沿捕获,一旦设置好触发输入的极性,另外一路硬件就会自动配置为相反的极性捕获,无需软件配置。 一句话概括就是:选定输入通道,确定触发信号,然后设置触发信号的极性即可,因为是PWM输入的缘故,另一路信号则由硬件配置,无需软件配置。
当使用PWM输入模式的时候必须将从模式控制器配置为复位模式(配置寄存器SMCR的位SMS[2:0]来实现), 即当我们启动触发信号开始进行捕获的时候,同时把计数器CNT复位清零。
下面我们以一个更加具体的时序图来分析下PWM输入模式。

PWM输入模式时序
PWM信号由输入通道TI1进入,配置TI1FP1为触发信号,上升沿捕获。 当上升沿的时候IC1和IC2同时捕获,计数器CNT清零,到了下降沿的时候,IC2捕获,此时计数器CNT的值被锁存到捕获寄存器CCR2中, 到了下一个上升沿的时候,IC1捕获,计数器CNT的值被锁存到捕获寄存器CCR1中。其中CCR2测量的是脉宽,CCR1测量的是周期。
从软件上来说,用PWM输入模式测量脉宽和周期更容易,付出的代价是需要占用两个捕获寄存器。
输出比较应用¶
输出比较模式总共有8种,具体的由寄存器CCMRx的位OCxM[2:0]配置。我们这里只讲解最常用的PWM模式,其他几种模式具体的看数据手册即可。
PWM输出模式¶
PWM输出就是对外输出脉宽(即占空比)可调的方波信号,信号频率由自动重装寄存器ARR的值决定,占空比由比较寄存器CCR的值决定。
PWM模式分为两种,PWM1和PWM2,总得来说是差不多,就看你怎么用而已,具体的区别下表PWM1与PWM2模式的区别。
模式 | 计数器CNT计算方式 | 说明 |
---|---|---|
PWM1 | 递增 | CNT<CCR,通道CH为有效,否则为无效 |
递减 | CNT>CCR,通道CH为无效,否则为有效 | |
PWM2 | 递增 | CNT<CCR,通道CH为无效,否则为有效 |
递减 | CNT>CCR,通道CH为有效,否则为无效 |
PWM1与PWM2模式的区别
下面我们以PWM1模式来讲解,以计数器CNT计数的方向不同还分为边沿对齐模式和中心对齐模式。 PWM信号主要都是用来控制电机,一般的电机控制用的都是边沿对齐模式,FOC电机一般用中心对齐模式。 我们这里只分析这两种模式在信号感官上(即信号波形)的区别,具体在电机控制中的区别不做讨论,到了你真正需要使用的时候就会知道了。
PWM边沿对齐模式¶
在递增计数模式下,计数器从 0 计数到自动重载值( TIMx_ARR寄存器的内容),然后重新从 0 开始计数并生成计数器上溢事件

PWM1模式的边沿对齐波形
在边沿对齐模式下,计数器CNT只工作在一种模式,递增或者递减模式。这里我们以CNT工作在递增模式为例, 在中,ARR=8,CCR=4,CNT从0开始计数,当CNT<CCR的值时,OCxREF为有效的高电平,于此同时,比较中断寄存器CCxIF置位。 当CCR=<CNT<=ARR时,OCxREF为无效的低电平。然后CNT又从0开始计数并生成计数器上溢事件,以此循环往复。
PWM中心对齐模式¶

PWM1模式的中心对齐波形
在中心对齐模式下,计数器CNT是工作做递增/递减模式下。开始的时候,计数器CNT从 0 开始计数到自动重载值减1(ARR-1), 生成计数器上溢事件;然后从自动重载值开始向下计数到 1 并生成计数器下溢事件。之后从0 开始重新计数。
图中是PWM1模式的中心对齐波形,ARR=8,CCR=4。 第一阶段计数器CNT工作在递增模式下,从0开始计数,当CNT<CCR的值时,OCxREF为有效的高电平,当CCR=<CNT<<ARR时,OCxREF为无效的低电平。 第二阶段计数器CNT工作在递减模式,从ARR的值开始递减,当CNT>CCR时,OCxREF为无效的低电平,当CCR=>CNT>=1时,OCxREF为有效的高电平。
在波形图上我们把波形分为两个阶段,第一个阶段是计数器CNT工作在递增模式的波形,这个阶段我们又分为①和②两个阶段, 第二个阶段是计数器CNT工作在递减模式的波形,这个阶段我们又分为③和④两个阶段。 要说中心对齐模式下的波形有什么特征的话,那就是①和③阶段的时间相等,②和④阶段的时间相等。
中心对齐模式又分为中心对齐模式1/2/3 三种,具体由寄存器CR1位CMS[1:0]配置。具体的区别就是比较中断中断标志位CCxIF在何时置1: 中心模式1在CNT递减计数的时候置1,中心对齐模式2在CNT递增计数时置1,中心模式3在CNT递增和递减计数时都置1。
定时器初始化结构体详解¶
HAL库函数对定时器外设建立了多个初始化结构体,分别为时基初始化结构体TIM_Base_InitTypeDef、 输出比较初始化结构体TIM_OC_InitTypeDef、输入捕获初始化结构体TIM_IC_InitTypeDef、 单脉冲初始化结构体TIM_OnePulse_InitTypeDef、编码器模式配置初始化结构体TIM_Encoder_InitTypeDef、 断路和死区初始化结构体TIM_BreakDeadTimeConfigTypeDef。高级控制定时器可以用到所有初始化结构体, 通用定时器不能使用TIM_BreakDeadTimeConfigTypeDef结构体,基本定时器只能使用时基结构体。 初始化结构体成员用于设置定时器工作环境参数,并由定时器相应初始化配置函数调用,最终这些参数将会写入到定时器相应的寄存器中。
初始化结构体和初始化库函数配合使用是HAL库精髓所在,理解了初始化结构体每个成员意义基本上就可以对该外设运用自如。 初始化结构体定义在stm32f4xx_hal_tim.h和stm32f4xx_hal_tim_ex.h文件中, 初始化库函数定义在stm32f4xx_hal_tim.c和stm32f4xx_hal_tim_ex.c文件中,编程时我们可以结合这四个文件内注释使用。
TIM_Base_InitTypeDef¶
时基结构体TIM_Base_InitTypeDef用于定时器基础参数设置,与TIM_TimeBaseInit函数配合使用完成配置。
1 2 3 4 5 6 7 | typedef struct {
uint16_t Prescaler; // 预分频器
uint16_t CounterMode; // 计数模式
uint32_t Period; // 定时器周期
uint16_t ClockDivision; // 时钟分频
uint8_t RepetitionCounter; // 重复计算器
} Time_Base_InitTypeDef;
|
- Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器计数时钟CK_CNT,它设定PSC寄存器的值。计算公式为: 计数器时钟频率 (f:sub:CK_CNT) 等于 fCK_PSC / (PSC[15:0] + 1),可实现1至65536分频。
- CounterMode:定时器计数方式,可设置为向上计数、向下计数以及中心对齐。高级控制定时器允许选择任意一种。
- Period:定时器周期,实际就是设定自动重载寄存器ARR的值,ARR 为要装载到实际自动重载寄存器(即影子寄存器)的值,可设置范围为0至65535。
- ClockDivision:时钟分频,设置定时器时钟CK_INT频率与死区发生器以及数字滤波器采样时钟频率分频比。可以选择1、2、4分频。
- RepetitionCounter:重复计数器,只有8位,只存在于高级定时器。
TIM_OCInitTypeDef¶
输出比较结构体TIM_OCInitTypeDef用于输出比较模式,与TIM_OCx_SetConfig函数配合使用完成指定定时器输出通道初始化配置。 高级控制定时器有四个定时器通道,使用时都必须单独设置。
1 2 3 4 5 6 7 8 9 | typedef struct {
uint32_t OCMode; // 比较输出模式
uint32_t Pulse; // 脉冲宽度
uint32_t OCPolarity; // 输出极性
uint32_t OCNPolarity; // 互补输出极性
uint32_t OCFastMode; // 比较输出模式快速使能
uint32_t OCIdleState; // 空闲状态下比较输出状态
uint32_t OCNIdleState; // 空闲状态下比较互补输出状态
}TIM_OCInitTypeDef;
|
- OCMode:比较输出模式选择,总共有八种,常用的为PWM1/PWM2。它设定CCMRx寄存器OCxM[2:0]位的值。
- Pulse:比较输出脉冲宽度,实际设定比较寄存器CCR的值,决定脉冲宽度。可设置范围为0至65535。
- OCPolarity:比较输出极性,可选OCx为高电平有效或低电平有效。它决定着定时器通道有效电平。它设定CCER寄存器的CCxP位的值。
- OCNPolarity:比较互补输出极性,可选OCxN为高电平有效或低电平有效。它设定TIMx_CCER寄存器的CCxNP位的值。
- OCFastMode:比较输出模式快速使能。它设定TIMx_CCMR寄存器的,OCxFE位的值可以快速使能或者禁能输出。
- OCIdleState:空闲状态时通道输出电平设置,可选输出1或输出0,即在空闲状态(BDTR_MOE位为0)时, 经过死区时间后定时器通道输出高电平或低电平。它设定CR2寄存器的OISx位的值。
(7) OCNIdleState:空闲状态时互补通道输出电平设置,可选输出1或输出0,即在空闲状态(BDTR_MOE位为0)时, 经过死区时间后定时器互补通道输出高电平或低电平,设定值必须与OCIdleState相反。它设定是CR2寄存器的OISxN位的值。
TIM_IC_InitTypeDef¶
输入捕获结构体TIM_IC_InitTypeDef用于输入捕获模式,与HAL_TIM_IC_ConfigChannel函数配合使用完成定时器输入通道初始化配置。 如果使用PWM输入模式需要与HAL_TIM_PWM_ConfigChannel函数配合使用完成定时器输入通道初始化配置。
1 2 3 4 5 6 | typedef struct {
uint32_t ICPolarity; // 输入捕获触发选择
uint32_t ICSelection; // 输入捕获选择
uint32_t ICPrescaler; // 输入捕获预分频器
uint32_t ICFilter; // 输入捕获滤波器
} TIM_IC_InitTypeDef;
|
- ICPolarity:输入捕获边沿触发选择,可选上升沿触发、下降沿触发或边沿跳变触发。它设定CCER寄存器CCxP位和CCxNP位的值。
- ICSelection:输入通道选择,捕获通道ICx的信号可来自三个输入通道, 分别为TIM_ICSELECTION_DIRECTTI、TIM_ICSELECTION_INDIRECTTI或TIM_ICSELECTION_TRC,具体的区别见下图。 它设定CCRMx寄存器的CCxS[1:0]位的值。

输入通道与捕获通道IC的映射图
- ICPrescaler:输入捕获通道预分频器,可设置1、2、4、8分频,它设定CCMRx寄存器的ICxPSC[1:0]位的值。 如果需要捕获输入信号的每个有效边沿,则设置1分频即可。
- ICFilter:输入捕获滤波器设置,可选设置0x0至0x0F。它设定CCMRx寄存器ICxF[3:0]位的值。一般我们不使用滤波器,即设置为0。
TIM_BreakDeadTimeConfigTypeDef¶
断路和死区结构体TIM_BreakDeadTimeConfigTypeDef用于断路和死区参数的设置,属于高级定时器专用,用于配置断路时通道输出状态,以及死区时间。 它与HAL_TIMEx_ConfigBreakDeadTime函数配置使用完成参数配置。这个结构体的成员只对应BDTR这个寄存器,有关成员的具体使用配置请参考手册BDTR寄存器的详细描述。
1 2 3 4 5 6 7 8 9 10 11 12 13 | typedef struct {
uint32_t OffStateRunMode; // 运行模式下的关闭状态选择
uint32_t OffStateIDLEMode; // 空闲模式下的关闭状态选择
uint32_t LockLevel; // 锁定配置
uint32_t DeadTime; // 死区时间
uint32_t BreakState; // 断路输入使能控制
uint32_t BreakPolarity; // 断路输入极性
uint32_t BreakFilter; // 断路输入滤波器
uint32_t Break2State; // 断路2输入使能控制
uint32_t Break2Polarity; // 断路2输入极性
uint32_t Break2Filter; // 断路2输入滤波器
uint32_t AutomaticOutput; // 自动输出使能
} TIM_BreakDeadTimeConfigTypeDef;
|
- OffStateRunMode:运行模式下的关闭状态选择,它设定BDTR寄存器OSSR位的值。
- OffStateIDLEMode:空闲模式下的关闭状态选择,它设定BDTR寄存器OSSI位的值。
- LockLevel:锁定级别配置, BDTR寄存器LOCK[1:0]位的值。
- DeadTime:配置死区发生器,定义死区持续时间,可选设置范围为0x0至0xFF。它设定BDTR寄存器DTG[7:0]位的值。
- BreakState:断路输入功能选择,可选使能或禁止。它设定BDTR寄存器BKE位的值。
- BreakPolarity:断路输入通道BRK极性选择,可选高电平有效或低电平有效。它设定BDTR寄存器BKP位的值。
- BreakFilter:断路输入滤波器,定义BRK 输入的采样频率和适用于 BRK的数字滤波器带宽。它设定BDTR寄存器BKF[3:0]位的值。
- Break2State:断路2输入功能选择,可选使能或禁止。它设定BDTR寄存器BK2E位的值。
- Break2Polarity:断路2输入通道BRK2极性选择,可选高电平有效或低电平有效。它设定BDTR寄存器BK2P位的值。
<<<<<<< .mine (10) Break2Filter:断路2输入滤波器,定义BRK2 输入的采样频率和适用于 BRK2的数字滤波器带宽。
它设定BDTR寄存器BK2F[3:0]位的值。
>>>>>>> .theirs
- AutomaticOutput:自动输出使能,可选使能或禁止,它设定BDTR寄存器AOE位的值。
PWM互补输出实验¶
输出比较模式比较多,这里我们以PWM输出为例讲解,并通过示波器来观察波形。 实验中不仅在主输出通道输出波形,还在互补通道输出与主通道互补的的波形,并且添加了断路和死区功能。
硬件设计¶
根据开发板引脚使用情况,并且参考定时器引脚信息 ,使用TIM8的通道1及其互补通道作为本实验的波形输出通道, 对应选择PC6和PA5引脚。将示波器的两个输入通道分别与PC6和PA5引脚短接,用于观察波形,还有注意共地。
为增加断路功能,需要用到TIM8_BKIN引脚,这里选择PA6引脚。程序我们设置该引脚为低电平有效, 所以先使用杜邦线将该引脚与开发板上3.3V短接。
另外,实验用到两个按键用于调节PWM的占空比大小,直接使用开发板上独立按键即可,电路参考独立按键相关章节。
软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们创建了两个文件:bsp_advance_tim.c和bsp_advance_tim.h文件用来存定时器驱动程序及相关宏定义。
编程要点¶
- 定时器 IO 配置
- 定时器时基结构体TIM_TimeBaseInitTypeDef配置
- 定时器输出比较结构体TIM_OCInitTypeDef配置
- 定时器断路和死区结构体TIM_BDTRInitTypeDef配置
软件分析¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* 定时器 */
#define ADVANCE_TIM TIM8
#define ADVANCE_TIM_CLK_ENABLE() \__TIM8_CLK_ENABLE()
/* TIM8通道1输出引脚 */
#define ADVANCE_OCPWM_PIN GPIO_PIN_6
#define ADVANCE_OCPWM_GPIO_PORT GPIOC
#define ADVANCE_OCPWM_GPIO_CLK_ENABLE() \__GPIOC_CLK_ENABLE()
#define ADVANCE_OCPWM_AF GPIO_AF3_TIM8
/* TIM8通道1互补输出引脚 */
#define ADVANCE_OCNPWM_PIN GPIO_PIN_5
#define ADVANCE_OCNPWM_GPIO_PORT GPIOA
#define ADVANCE_OCNPWM_GPIO_CLK_ENABLE() \__GPIOA_CLK_ENABLE()
#define ADVANCE_OCNPWM_AF GPIO_AF3_TIM8
/* TIM8断路输入引脚 */
#define ADVANCE_BKIN_PIN GPIO_PIN_6
#define ADVANCE_BKIN_GPIO_PORT GPIOA
#define ADVANCE_BKIN_GPIO_CLK_ENABLE() \__GPIOA_CLK_ENABLE()
#define ADVANCE_BKIN_AF GPIO_AF3_TIM8
|
使用宏定义非常方便程序升级、移植。如果使用不同的定时器IO,修改这些宏即可。
定时器复用功能引脚初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | static void TIMx_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启定时器相关的GPIO外设时钟*/
ADVANCE_OCPWM_GPIO_CLK_ENABLE();
ADVANCE_OCNPWM_GPIO_CLK_ENABLE();
ADVANCE_BKIN_GPIO_CLK_ENABLE();
/* 定时器功能引脚初始化 */
GPIO_InitStructure.Pin = ADVANCE_OCPWM_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
GPIO_InitStructure.Alternate = ADVANCE_OCPWM_AF;
HAL_GPIO_Init(ADVANCE_OCPWM_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = ADVANCE_OCNPWM_PIN;
GPIO_InitStructure.Alternate = ADVANCE_OCNPWM_AF;
HAL_GPIO_Init(ADVANCE_OCNPWM_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = ADVANCE_BKIN_PIN;
GPIO_InitStructure.Alternate = ADVANCE_BKIN_AF;
HAL_GPIO_Init(ADVANCE_BKIN_GPIO_PORT, &GPIO_InitStructure);
}
|
定时器通道引脚使用之前必须设定相关参数,这选择复用功能,并指定到对应的定时器。使用GPIO之前都必须开启相应端口时钟。
定时器模式配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | static void TIM_Mode_Config(void)
{
TIM_BreakDeadTimeConfigTypeDef TIM_BDTRInitStructure;
// 开启TIMx_CLK,x[1,8]
ADVANCE_TIM_CLK_ENABLE();
/* 定义定时器的句柄即确定定时器寄存器的基地址*/
TIM_TimeBaseStructure.Instance = ADVANCE_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到999,即为1000次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = 1000-1;
// 高级控制定时器时钟源TIMxCLK = HCLK=168MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=1MHz
TIM_TimeBaseStructure.Init.Prescaler = 168-1;
// 采样时钟分频
TIM_TimeBaseStructure.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
// 计数方式
TIM_TimeBaseStructure.Init.CounterMode=TIM_COUNTERMODE_UP;
// 重复计数器
TIM_TimeBaseStructure.Init.RepetitionCounter=0;
// 初始化定时器TIMx, x[1,8]
HAL_TIM_PWM_Init(&TIM_TimeBaseStructure);
/*PWM模式配置*/
//配置为PWM模式1
TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.Pulse = ChannelPulse;
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
TIM_OCInitStructure.OCNPolarity = TIM_OCNPOLARITY_HIGH;
TIM_OCInitStructure.OCIdleState = TIM_OCIDLESTATE_SET;
TIM_OCInitStructure.OCNIdleState = TIM_OCNIDLESTATE_RESET;
//初始化通道1输出PWM
HAL_TIM_PWM_ConfigChannel(&TIM_TimeBaseStructure,&TIM_OCInitStructure,TIM_CHANNEL_1);
/* 自动输出使能,断路、死区时间和锁定配置 */
TIM_BDTRInitStructure.OffStateRunMode = TIM_OSSR_ENABLE;
TIM_BDTRInitStructure.OffStateIDLEMode = TIM_OSSI_ENABLE;
TIM_BDTRInitStructure.LockLevel = TIM_LOCKLEVEL_1;
TIM_BDTRInitStructure.DeadTime = 11;
TIM_BDTRInitStructure.BreakState = TIM_BREAK_ENABLE;
TIM_BDTRInitStructure.BreakPolarity = TIM_BREAKPOLARITY_LOW;
TIM_BDTRInitStructure.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;
HAL_TIMEx_ConfigBreakDeadTime(&TIM_TimeBaseStructure,
&TIM_BDTRInitStructure);
/* 定时器通道1输出PWM */
HAL_TIM_PWM_Start(&TIM_TimeBaseStructure,TIM_CHANNEL_1);
/* 定时器通道1互补输出PWM */
HAL_TIMEx_PWMN_Start(&TIM_TimeBaseStructure,TIM_CHANNEL_1);
}
|
首先定义三个定时器初始化结构体,定时器模式配置函数主要就是对这三个结构体的成员进行初始化, 然后通过相应的初始化函数把这些参数写入定时器的寄存器中。有关结构体的成员介绍请参考定时器初始化结构体详解小节。
不同的定时器可能对应不同的APB总线,在使能定时器时钟是必须特别注意。高级控制定时器属于APB2,定时器内部时钟是168MHz。
在时基结构体中我们设置定时器周期参数为1000,频率为1MHz,使用向上计数方式。 因为我们使用的是内部时钟,所以外部时钟采样分频成员不需要设置,重复计数器我们没用到,也不需要设置。
在输出比较结构体中,设置输出模式为PWM1模式,主通道和互补通道输出高电平有效, 设置脉宽为ChannelPulse,ChannelPulse是我们定义的一个无符号16位整形的全局变量,用来指定占空比大小, 实际上脉宽就是设定比较寄存器CCR的值,用于跟计数器CNT的值比较。
断路和死区结构体中,使能断路功能,设定断路信号的有效极性,设定死区时间。
最后使用HAL_TIM_PWM_Start函数和HAL_TIMEx_PWMN_Start函数让计数器开始计数和通道输出。
主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | int main(void)
{
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/* 初始化按键GPIO */
Key_GPIO_Config();
/* 初始化基本定时器定时,1s产生一次中断 */
TIMx_Configuration();
while (1) {
/* 扫描KEY1 */
if ( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON ) {
/* 增大占空比 */
if (ChannelPulse<950)
ChannelPulse+=50;
else
ChannelPulse=1000;
__HAL_TIM_SetCompare(&TIM_TimeBaseStructure,M_CHANNEL_1,ChannelPulse);
}
/* 扫描KEY2 */
if ( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON ) {
/* 减小占空比 */
if (ChannelPulse>=50)
ChannelPulse-=50;
else
ChannelPulse=0;
__HAL_TIM_SetCompare(&TIM_TimeBaseStructure,M_CHANNEL_1,ChannelPulse);
}
}
}
|
首先,调用初始化系统时钟,Key_GPIO_Config函数完成按键引脚初始化配置,该函数定义在bsp_key.c文件中。
接下来,调用TIMx_Configuration函数完成定时器参数配置,包括定时器复用引脚配置和定时器模式配置, 该函数定义在bsp_advance_tim.c文件中它实际上只是简单的调用TIMx_GPIO_Config函数和TIM_Mode_Config函数。 运行完该函数后通道引脚就已经有PWM波形输出,通过示波器可直观观察到。
最后,在无限循环函数中检测按键状态, 如果是KEY1被按下,就增加ChannelPulse变量值,并调用TIM_SetCompare1函数完成增加占空比设置; 如果是KEY2被按下,就减小ChannelPulse变量值,并调用TIM_SetCompare1函数完成减少占空比设置。TIM_SetCompare1函数实际是设定TIMx_CCR1寄存器值。
下载验证¶
根据实验的硬件设计内容接好示波器输入通道和开发板引脚连接,并把断路输入引脚拉高。 编译实验程序并下载到开发板上,调整示波器到合适参数,在示波器显示屏和看到一路互补的PWM波形,参考下图。 此时,按下开发板上KEY1或KEY2可改变波形的占空比。

PWM互补波形输出示波器图
PWM输入捕获实验¶
实验中,我们用通用定时器产生已知频率和占空比的PWM信号,然后用高级定时器的PWM输入模式来测量这个已知的PWM信号的频率和占空比, 通过两者的对比即可知道测量是否准确。
硬件设计¶
实验中用到两个引脚,一个是通用定时器通道用于波形输出,另一个是高级控制定时器通道用于输入捕获,实验中直接使用一根杜邦线短接即可。
软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。我们创建了两个文件: bsp_advance_tim.c和bsp_advance_tim.h文件用来存定时器驱动程序及相关宏定义。
编程要点¶
- 通用定时器产生PWM配置
- 高级定时器PWM输入配置
- 计算测量的频率和占空比,并打印出来比较
软件分析¶
宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /* 通用定时器 */
#define GENERAL_TIM TIM2
#define GENERAL_TIM_CLK_ENABLE() \__TIM2_CLK_ENABLE()
/* 通用定时器PWM输出 */
/* PWM输出引脚 */
#define GENERAL_OCPWM_PIN GPIO_PIN_5
#define GENERAL_OCPWM_GPIO_PORT GPIOA
#define GENERAL_OCPWM_GPIO_CLK_ENABLE() \__GPIOA_CLK_ENABLE()
#define GENERAL_OCPWM_AF GPIO_AF1_TIM2
/* 高级控制定时器 */
#define ADVANCE_TIM TIM8
#define ADVANCE_TIM_CLK_ENABLE() \__TIM8_CLK_ENABLE()
/* 捕获/比较中断 */
#define ADVANCE_TIM_IRQn TIM8_CC_IRQn
#define ADVANCE_TIM_IRQHandler TIM8_CC_IRQHandler
/* 高级控制定时器PWM输入捕获 */
/* PWM输入捕获引脚 */
#define ADVANCE_ICPWM_PIN GPIO_PIN_6
#define ADVANCE_ICPWM_GPIO_PORT GPIOC
#define ADVANCE_ICPWM_GPIO_CLK_ENABLE() \__GPIOC_CLK_ENABLE()
#define ADVANCE_ICPWM_AF GPIO_AF3_TIM8
#define ADVANCE_IC1PWM_CHANNEL TIM_CHANNEL_1
#define ADVANCE_IC2PWM_CHANNEL TIM_CHANNEL_2
|
使用宏定义非常方便程序升级、移植。如果使用不同的定时器IO,修改这些宏即可。
定时器复用功能引脚初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | static void TIMx_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启定时器相关的GPIO外设时钟*/
GENERAL_OCPWM_GPIO_CLK_ENABLE();
ADVANCE_ICPWM_GPIO_CLK_ENABLE();
/* 定时器功能引脚初始化 */
/* 通用定时器PWM输出引脚*/
GPIO_InitStructure.Pin = GENERAL_OCPWM_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
GPIO_InitStructure.Alternate = GENERAL_OCPWM_AF;
HAL_GPIO_Init(GENERAL_OCPWM_GPIO_PORT, &GPIO_InitStructure);
/* 高级定时器输入捕获引脚 */
GPIO_InitStructure.Pin = ADVANCE_ICPWM_PIN;
GPIO_InitStructure.Alternate = ADVANCE_ICPWM_AF;
HAL_GPIO_Init(ADVANCE_ICPWM_GPIO_PORT, &GPIO_InitStructure);
}
|
定时器通道引脚使用之前必须设定相关参数,这选择复用功能,并指定到对应的定时器。使用GPIO之前都必须开启相应端口时钟。
嵌套向量中断控制器组配置
1 2 3 4 5 6 7 | static void TIMx_NVIC_Configuration(void)
{
//设置抢占优先级,子优先级
HAL_NVIC_SetPriority(ADVANCE_TIM_IRQn, 0, 3);
// 设置中断来源
HAL_NVIC_EnableIRQ(ADVANCE_TIM_IRQn);
}
|
实验用到高级控制定时器捕获/比较中断,需要配置中断优先级,因为实验只用到一个中断, 所以这里对优先级配置没具体要求,只要符合中断组参数要求即可。
通用定时器PWM输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | static void TIM_PWMOUTPUT_Config(void)
{
TIM_OC_InitTypeDef TIM_OCInitStructure;
// 开启TIMx_CLK,x[2,3,4,5,12,13,14]
GENERAL_TIM_CLK_ENABLE();
/* 定义定时器的句柄即确定定时器寄存器的基地址*/
TIM_PWMOUTPUT_Handle.Instance = GENERAL_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到9999,即为10000次,为一个定时周期
TIM_PWMOUTPUT_Handle.Init.Period = 10000-1;
// 高级控制定时器时钟源TIMxCLK = HCLK=84MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=100KHz
TIM_PWMOUTPUT_Handle.Init.Prescaler = 84-1;
// 采样时钟分频
TIM_PWMOUTPUT_Handle.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
// 计数方式
TIM_PWMOUTPUT_Handle.Init.CounterMode=TIM_COUNTERMODE_UP;
// 重复计数器
TIM_PWMOUTPUT_Handle.Init.RepetitionCounter=0;
// 初始化定时器TIMx, x[1,8]
HAL_TIM_PWM_Init(&TIM_PWMOUTPUT_Handle);
/*PWM模式配置*/
//配置为PWM模式1
TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.Pulse = 5000;
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
TIM_OCInitStructure.OCNPolarity = TIM_OCNPOLARITY_HIGH;
TIM_OCInitStructure.OCIdleState = TIM_OCIDLESTATE_SET;
TIM_OCInitStructure.OCNIdleState = TIM_OCNIDLESTATE_RESET;
//初始化通道1输出PWM
HAL_TIM_PWM_ConfigChannel(&TIM_PWMOUTPUT_Handle,
&TIM_OCInitStructure,TIM_CHANNEL_1);
/* 定时器通道1输出PWM */
HAL_TIM_PWM_Start(&TIM_PWMOUTPUT_Handle,TIM_CHANNEL_1);
}
|
定时器PWM输出模式配置函数很简单,看代码注释即可。这里我们设置了PWM的频率为100Hz, 即周期为10ms,占空比为:(Pulse+1)/(Period+1) = 50%。
高级控制定时PWM输入模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | static void TIM_PWMINPUT_Config(void)
{
TIM_IC_InitTypeDef TIM_ICInitStructure;
TIM_SlaveConfigTypeDef TIM_SlaveConfigStructure;
// 开启TIMx_CLK,x[1,8]
ADVANCE_TIM_CLK_ENABLE();
/* 定义定时器的句柄即确定定时器寄存器的基地址*/
TIM_PWMINPUT_Handle.Instance = ADVANCE_TIM;
TIM_PWMINPUT_Handle.Init.Period = 0xFFFF;
// 高级控制定时器时钟源TIMxCLK = HCLK=168MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=1MHz
TIM_PWMINPUT_Handle.Init.Prescaler = 168-1;
// 采样时钟分频
TIM_PWMINPUT_Handle.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
// 计数方式
TIM_PWMINPUT_Handle.Init.CounterMode=TIM_COUNTERMODE_UP;
// 初始化定时器TIMx, x[1,8]
HAL_TIM_IC_Init(&TIM_PWMINPUT_Handle);
/* IC1捕获:上升沿触发 TI1FP1 */
TIM_ICInitStructure.ICPolarity = TIM_ICPOLARITY_RISING;
TIM_ICInitStructure.ICSelection = TIM_ICSELECTION_DIRECTTI;
TIM_ICInitStructure.ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.ICFilter = 0x0;
HAL_TIM_IC_ConfigChannel(&TIM_PWMINPUT_Handle,
&TIM_ICInitStructure,ADVANCE_IC1PWM_CHANNEL);
/* IC2捕获:下降沿触发 TI1FP2 */
TIM_ICInitStructure.ICPolarity = TIM_ICPOLARITY_FALLING;
TIM_ICInitStructure.ICSelection = TIM_ICSELECTION_INDIRECTTI;
TIM_ICInitStructure.ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.ICFilter = 0x0;
HAL_TIM_IC_ConfigChannel(&TIM_PWMINPUT_Handle,
&TIM_ICInitStructure,ADVANCE_IC2PWM_CHANNEL);
/* 选择从模式: 复位模式 */
TIM_SlaveConfigStructure.SlaveMode = TIM_SLAVEMODE_RESET;
/* 选择定时器输入触发: TI1FP1 */
TIM_SlaveConfigStructure.InputTrigger = TIM_TS_TI1FP1;
HAL_TIM_SlaveConfigSynchronization(&TIM_PWMINPUT_Handle,
&TIM_SlaveConfigStructure);
/* 使能捕获/比较2中断请求 */
HAL_TIM_IC_Start_IT(&TIM_PWMINPUT_Handle,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&TIM_PWMINPUT_Handle,TIM_CHANNEL_2);
}
|
输入捕获配置中,主要初始化三个结构体,时基结构体部分很简单,看注释理解即可。关键部分是输入捕获结构体和从模式结构体的初始化。
首先,我们要选定捕获通道,这里我们用IC1,然后设置捕获信号的极性,这里我们配置为上升沿, 我们需要对捕获信号的每个有效边沿(即我们设置的上升沿)都捕获,所以我们不分频,滤波器我们也不需要用。 那么捕获通道的信号来源于哪里呢?IC1的信号可以是TI1输入的TI1FP1,也可以是从TI2输入的TI2FP1, 我们这里选择直连(DIRECTTI),即IC1映射到TI1FP1,即PWM信号从TI1输入。
我们知道,PWM输入模式,需要使用两个捕获通道,占用两个捕获寄存器。由输入通道TI1输入的信号会分成TI1FP1和TI1FP2, 具体选择哪一路信号作为捕获触发信号决定着哪个捕获通道测量的是周期。 这里我们选择TI1FP1作为捕获的触发信号,那PWM信号的周期则存储在CCR1寄存器中, 剩下的另外一路信号TI1FP2则进入IC2,CCR2寄存器存储的是脉冲宽度。
测量脉冲宽度我们选择捕获通道2,即IC2,设置捕获信号的极性,这里我们配置为下降沿, 我们需要对捕获信号的每个有效边沿(即我们设置的下降沿)都捕获,所以我们不分频,滤波器我们也不需要用。 那么捕获通道的信号来源于TI2输入的TI2FP1,这里选择间接(INDIRECTTI),PWM信号从IC1输入再进入IC2.
I2C作为间接输入模式,我们需要配置他的从模式,即从模式复位模式,定时器触发源为TIM_TS_TI1FP1, 最后使用函数HAL_TIM_SlaveConfigSynchronization进行配置。
最后启动定时器的两个通道捕获。
高级控制定时器中断服务函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
/* 获取输入捕获值 */
IC1Value = HAL_TIM_ReadCapturedValue(&TIM_PWMINPUT_Handle,ADVANCE_IC1PWM_CHANNEL);
IC2Value = HAL_TIM_ReadCapturedValue(&TIM_PWMINPUT_Handle,ADVANCE_IC2PWM_CHANNEL);
if (IC1Value != 0)
{
/* 占空比计算 */
DutyCycle = (float)((IC2Value+1) * 100) / (IC1Value+1);
/* 频率计算 */
Frequency = 168000000/168/(float)(IC1Value+1);
}
else
{
DutyCycle = 0;
Frequency = 0;
}
}
}
|
中断服务函数的回调函数中,我们获取CCR1和CCR2寄存器中的值,当CCR1的值不为0时,说明有效捕获到了一个周期,然后计算出频率和占空比。
如果是第一个上升沿中断,计数器会被复位,锁存到CCR1寄存器的值是0,CCR2寄存器的值也是0,无法计算频率和占空比。 当第二次上升沿到来的时候,CCR1和CCR2捕获到的才是有效的值。
主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | int main(void)
{
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/* 初始化串口 */
UARTx_Config();
/* 初始化基本定时器定时,1s产生一次中断 */
TIMx_Configuration();
while (1) {
HAL_Delay(500);
printf("IC1Value = %d IC2Value = %d ",IC1Value,IC2Value);
printf("占空比:%0.2f%% 频率:%0.2fHz\n",DutyCycle,Frequency);
}
}
|
主函数内容非常简单,首先初始化系统时钟、调用UARTx_Config函数完成串口初始化配置,该函定义在bsp _usart.c文件内。
接下来就是调用TIMx_Configuration函数完成定时器配置,该函数定义在bsp_advance_tim.c文件内, 它只是简单的分别调用TIMx_GPIO_Config()、TIMx_NVIC_Configuration()、TIM_PWMOUTPUT_Config()和TIM_PWMINPUT_Config()四个函数, 完成定时器引脚初始化配置,NVIC配置,通用定时器输出PWM以及高级控制定时器PWM输入模式配置。
主函数的无限循环每隔500ms输出一次捕获结果。
下载验证¶
把编译好的程序烧写到开发板,用杜邦线把通用定时器的PWM输出引脚(PA5)连接到高级定时器的PWM输入引脚(PC6)。然后用USB线连接电脑与开发板的USB转串口,打开串口调试助手,即可看到捕获到的PWM信号的频率和占空比, 具体见下图于此同时,可用示波器监控通用定时器输出的PWM信号,看下捕获到的信号是否正确,具体见下图。

串口调试助手打印的捕获信息

示波器监控的波形
从上面两个图中我们可以看到,程序捕获计算出的频率和占空比和示波器监控到的波形的频率和占空比跟一致,所以我们的程序是正确的。
直流有刷电机¶
直流有刷电机(Brushed DC motor)具有结构简单、易于控制、成本低等特点, 在一些功能简单的应用场合,或者说在能够满足必要的性能、低成本和足够的可靠性的前提下, 直流有刷电机往往是一个很好的选择。例如便宜的电子玩具、各种风扇和汽车的电动座椅等。 基本的直流有刷电机在电源和电机之间只需要两根电缆,这样就可以节省配线和连接器所需的空间, 并降低电缆和连接器的成本。此外,还可以使用MOSFET/IGBT开关对直流有刷电机进行控制, 给电机提供足够好的性能的同时,整个电机控制系统也会比较便宜。
直流有刷电机转速快、扭矩小,在某些应用中可能无法满足要求。这种情况就需要做一些改进来降低转速,并提高力矩。 直流减速电机就是这样一种电机,实物图如下图所示。

这种电机通常也叫齿轮减速电机,它是在普通直流有刷电机的基础上增加了一套齿轮减速箱, 用来提供更大的力矩和更低的转速。齿轮减速箱可以通过配置不同的减速比,提供各种不同的转速和力矩。 在实际使用中减速电机使用的最为广泛,所以本章节将主要介绍直流有刷减速电机。
本章节将介绍直流有刷电机的工作原理、电机参数和驱动电路,最后通过实验来实现电机运动的简单控制。
直流有刷电机工作原理¶
在分析原理前我们先复习一下左手定则。
左手定则是判断通电导体处于磁场中时,所受安培力 F (或运动)的方向、 磁感应强度B的方向以及通电导体棒的电流 I 三者方向之间的关系的定律。 通过左手定则可以知道通电导体在磁场中的受力方向,如下图所示。

判断方法是:伸开左手,使拇指与其他四指垂直且在一个平面内,让磁感线从手心流入, 四指指向电流方向,大拇指指向的就是安培力方向(即导体受力方向)。
有刷直流电机在其电枢上绕有大量的线圈,所产生强大的磁场与外部磁场相互作用产生旋转运动。 磁极方向的跳转是通过移动固定位置的接触点来完成的,该接触点在电机转子上与电触点相对连接。 这种固定触点通常由石墨制成,与铜或其他金属相比,在大电流短路或断路/起动过程中石墨不会熔断或者与旋转触点焊接到一起, 并且这个触点通常是弹簧承载的,所以能够获得持续的接触压力,保证向线圈供应电力。 在这里我们将通过其中一组线圈和一对磁极来分析其工作原理,如下图所示。

图中C和D两片半圆周的铜片构成换向器,两个弹性铜片靠在换向器两侧的A和B是电刷,电源通过电刷向导线框供电, 线框中就有电流通过,在线框两侧放一对磁极N和S,形成磁场,磁力线由N到S。线框通有电流时, 两侧导线就会受到磁场的作用力,方向依左手定则判断,红色和蓝色线框部分分别会受到力F1和F2, 这两个力的方向相反,这使得线框会转动,当线框转过90°时,换向器改变了线框电流的方向,产生的安培力方向不变, 于是导线框会连续旋转下去,这就是直流电动机的工作原理。
直流有刷减速电机几个重要参数¶
- 空载转速:正常工作电压下电机不带任何负载的转速(单位为r/min(转/分))。 空载转速由于没有反向力矩,所以输出功率和堵转情况不一样,该参数只是提供一个电机在规定电压下最大转速的参考。
- 空载电流:正常工作电压下电机不带任何负载的工作电流(单位mA(毫安))。越好的电机,在空载时,该值越小。
- 负载转速:正常工作电压下电机带负载的转速。
- 负载力矩:正常工作电压下电机带负载的力矩 (N·m(牛米))。
- 负载电流:负载电流是指电机拖动负载时实际检测到的定子电流数值。
- 堵转力矩:在电机受反向外力使其停止转动时的力矩。如果电机堵转现象经常出现, 则会损坏电机,或烧坏驱动芯片。所以大家选电机时,这是除转速外要考虑的参数。 堵转时间一长,电机温度上升的很快,这个值也会下降的很厉害。
- 堵转电流:在电机受反向外力使其停止转动时的电流,此时电流非常大,时间稍微就可能会烧毁电机, 在实际使用时应尽量避免。
- 减速比:是指没有减速齿轮时转速与有减速齿轮时转速之比。
- 功率:般指的是它的额定功率(单位W(瓦)),即在额定电压下能够长期正常运转的最大功率, 也是指电动机在制造厂所规定的额定情况下运行时, 其输出端的机械功率。
直流有刷电机驱动设计与分析¶
我们先来想一个问题,假设你手里现在有一个直流电机和一节电池, 当你把电机的两根电源线和电池的电源连接在一起时,这时电机可以正常旋转, 当想要电机反向旋转时,只需要把两根电源线交换一下就可以了。 但是当在实际应用中要实现正转和反转的控制也通过交换电源线实现吗? 显然这样的方法是不可行的。这时候我们可以用一个叫做“H桥电路”来驱动电机。
控制电路原理设计与分析¶
如下图所示,是使用4个三极管搭建的H桥电路。

上图中,H桥式电机驱动电路包括4个三极管和一个电机。要使电机运转,必须导通对角线上的一对三极管。 根据不同三极管对的导通情况,电流可能会从左至右或从右至左流过电机,从而控制电机的转向。

上图中,当Q1和Q4导通时,电流将经过Q1从左往右流过电机, 在经过Q4流到电源负极,这时图中电机可以顺时针转动。

上图中,当Q3和Q2导通时,电流将经过Q3从右往左流过电机, 在经过Q2流到电源负极,这时图中电机可以逆时针转动。
特别地,当同一侧的Q1和2同时导通时,电流将从电源先后经过Q1和Q2,然后直接流到电源负极, 在这个回路中除了三极管以外就没有其他负载(没有经过电机),这时电流可能会达到最大值,此时可能会烧毁三极管, 同理,当Q3和4同时导通时,也会出现相同的状况。这样的情况肯定是不能发生的, 但是我们写程序又是三分写代码七分在调试,这就难免会有写错代码将同一测得三极管导通的情况, 为此我们就需要从硬件上来避免这个问题。下面电路图是改进后的驱动电路图。

与改进前的电路相比,在上面的改进电路中新增加了两个非门和4个与门,经过这样的组合就可以实现一个信号控制两个同一侧的三极管, 并且可以保证在同一侧中两个三级管不会同时导通,在同一时刻只会有一个三极管是导通的。
我们来分析一下电信号的变化:在ENABLE脚接入高电平,在IN1脚接入高电平,在经过第一个非门后,AND1的2脚就是低电平, 此时与门AND1的输出(3脚)就是低电平,所以Q1截止。而AND2的1脚和2脚都是高电平,所以AND2的3脚也是高电平, 这样Q2就导通了。在IN2接入低电平,同理分析可得,Q3导通Q4截止。在IN1和IN2处分别接入低电平和高电平, 则Q1和Q4导通,Q3和Q2截止。当IN1和IN2都接入高电平或者高电平时都只会同时导通上面或者下面的两个三极管, 不会出现同一侧的三极管同时导通的情况。
驱动芯片分析¶
通常在驱动电机的时候我们会选择集成H桥的IC,因为H桥使用分立元件搭建比较麻烦,增加了硬件设计难度, 当然如果集成IC无法满足我们的功率要求时,还是需要我们自己使用MOS管、三极管等元件来搭建H桥电路, 这样的分立元件搭建的H桥一般驱动能力都会比集成IC要高。当我们在选择集成IC时, 我们需要考虑集成IC是否能满足电机的驱动电压要求,是否能承受电机工作时的电流等情况。
L298N驱动芯片¶
L298N是ST公司的产品,内部包含4通道逻辑驱动电路,是一种二相和四相电机的专门驱动芯片, 即内含两个H桥的高电压大电流双桥式驱动器,接收标准的TTL逻辑电平信号,可驱动4.5V~46V、 2A以下的电机,电流峰值输出可达3A,其内部结构如下图所示。

其工作原理与上面的讲解的H桥原理一样,这里不再赘述。L298N引脚图如下图所示。

L298N逻辑功能表。
IN1 | IN2 | ENA | 电机状态 |
---|---|---|---|
× | × | 0 | 电机停止 |
1 | 0 | 1 | 电机正转 |
0 | 1 | 1 | 电机反转 |
0 | 0 | 1 | 电机停止 |
1 | 1 | 1 | 电机停止 |
IN3,IN4的逻辑图与上表相同。由上表可知ENA为低电平时,INx输入电平对电机控制不起作用, 当ENA为高电平,输入电平为一高一低,电机正或反转。同为低电平电机停止,同为高电平电机停止。 L298N的应用电路图将在后面硬件设计小节讲解。
直流有刷减速电机控制实现¶
速度控制原理¶
脉冲宽度调制(Pulse width modulation,PWM)信号,即PWM是一种按一定的规则对各脉冲的宽度进行调制, 既可改变电路输出电压的大小,也可改变输出频率。PWM通过一定的频率来改变通电和断电的时间, 从而控制电路输出功率,在电机的控制周期中,通电时间决定了它的转速。其中, 通电时间/(通断时间+断电时间)=占空比,即,高电平占整个周期的百分比,如下图所示:

上图中:T1为高电平时间,T2为低电平时间,T是周期。
D(占空比) = T1/T*100%
设电机的速度为V,最大速度为Vmax。
则:V=Vmax*D
当占空比D(0≤D≤1)的大小改变时,速度V也会改变,所以只要改变占空比就能达到控制的目的。
硬件设计¶
主控有刷电机接口原理图如下图所示,有刷电机接口与无刷接口使用的是同一个接口,舍去了其中一些多余的接口, 用到了两个定时器通道,编码器、两路ADC采集通道(后续章节讲解)。本节实验只用到了TIM1的CH1和CH2, 即PA8和PA9来输出PWM信号来控制电机,注意主控板需要和电机驱动板供地。

L298N¶
野火直流有刷电机驱动板¶
野火有刷电机驱动板是使用MOS管搭建的大功率H桥电机驱动板,实物图如下图所示。

驱动板可支持12V~70V的宽电压输入,10A过电流保护电路,超过10A可自动禁用电机控制信号,最高功率支持700W。 同时还具有电流采样电路、编码器接口和电源电压检测电路等等,本小节主要讲解电机驱动部分电路, 其他功能将在后续章节中讲解。
PWM控制信号使用了TLP2362高速光耦进行了隔离,SD控制信号使用了EL357N光耦进行了隔离,如下图所示。

需要注意的是TLP2362的输出信号与输入信号是反向的,真值表如下表所示。即输入高电平时,LED灯打开,输出为低电平; 输入低电平时,LED灯关闭,输出为高电平;这需要我们在初始化定时器的时候注意这个问题。其中SD的信号并没有反向, 输入为高电平时输出也为高电平,输入为低电平时输出也为低电平。
Input | LED | Output |
---|---|---|
H | ON | L |
L | OFF | H |
下图是使用MOS管搭建的H桥电路,使用两个EG2104驱动四个MOS管。

EG2104S主要功能有逻辑信号输入处理、死区时间控制、电平转换功能、悬浮自举电源结构和上下桥图腾柱式输出。 逻辑信号输入端高电平阀值为 2.5V 以上,低电平阀值为 1.0V 以下,要求逻辑信号的输出电流小, 可以使MCU输出逻辑信号直接连接到EG2104S的输入通道上。EG2104S芯片有一个shutdown引脚, 逻辑输入控制信号低电平有效,控制强行使LO、HO输出低电平。这样可以直接使用这个引脚做软件控制电机的旋转与停止, 还可以实现硬件的限流保护(后续章节分析保护电路),输入信号和输出信号逻辑真值表如下表所示。
IN(引脚2) | SD(引脚3) | HO(引脚7) | LO(引脚5) |
---|---|---|---|
L | L | L | L |
H | L | L | L |
L | H | L | H |
H | H | H | L |
从真值表可知,在输入逻辑信号SD为“L”时,不管IN为“H”或者“L”情况下,驱动器控制输出HO、LO同时为“L”, 上、下功率管同时关断;当输入逻辑信号SD为“H”、IN为“L”时,HO输出为“L”,LO输出为“H”; 当输入逻辑信号SD为“H”、IN 为“H”时,HO输出为“H”,LO输出为“L”。
EG2104S内部集成了死区时控制电路,死区时间波形图如下图所示,其中死区时间DT的典型值为640ns。

EG2104S采用自举悬浮驱动电源结构大大简化了驱动电源设计, 只用一路电源电压VCC即可完成高端N沟道MOS管和低端N沟道MOS管两个功率开关器件的驱动,给实际应用带来极大的方便。 EG2104S自举电路结构如下图所示,EG2104S可以使用外接一个自举二极管和一个自举电容自动完成自举升压功能, 假定在下管开通、上管关断期间VC自举电容已充到足够的电压(Vc=VCC),当HO输出高电平时上管开通、下管关断时, VC自举电容上的电压将等效一个电压源作为内部驱动器VB和VS的电源,完成高端N沟道MOS管的驱动。

软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们创建了四个文件:bsp_general_tim.c、bsp_general_tim.h、bsp_motor_control.c和bsp_motor_control.h 文件用来存定时器驱动和电机控制程序及相关宏定义。
编程要点¶
- 定时器 IO 配置
- 定时器时基结构体TIM_TimeBaseInitTypeDef配置
- 定时器输出比较结构体TIM_OCInitTypeDef配置
- 根据定时器定义电机控制相关函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /*宏定义*/
#define PWM_TIM TIM1
#define PWM_TIM_GPIO_AF GPIO_AF1_TIM1
#define PWM_TIM_CLK_ENABLE() __TIM1_CLK_ENABLE()
#define PWM_CHANNEL_1 TIM_CHANNEL_1
#define PWM_CHANNEL_2 TIM_CHANNEL_2
/* 累计 TIM_Period个后产生一个更新或者中断*/
/* 当定时器从0计数到PWM_PERIOD_COUNT,即为PWM_PERIOD_COUNT+1次,为一个定时周期 */
#define PWM_PERIOD_COUNT (1000)
/* 通用控制定时器时钟源TIMxCLK = HCLK=168MHz */
/* 设定定时器频率为=TIMxCLK/(PWM_PRESCALER_COUNT+1) */
#define PWM_PRESCALER_COUNT (9)
/*PWM引脚*/
#define PWM_TIM_CH1_GPIO_PORT GPIOA
#define PWM_TIM_CH1_PIN GPIO_PIN_8
#define PWM_TIM_CH2_GPIO_PORT GPIOA
#define PWM_TIM_CH2_PIN GPIO_PIN_9
#define PWM_TIM_CH3_GPIO_PORT GPIOA
#define PWM_TIM_CH3_PIN GPIO_PIN_10
|
使用宏定义非常方便程序升级、移植。如果使用不同的定时器IO,修改这些宏即可。
定时器复用功能引脚初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static void TIMx_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/* 定时器通道功能引脚端口时钟使能 */
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 定时器通道1功能引脚IO初始化 */
/*设置输出类型*/
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
/*设置引脚速率 */
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/*设置复用*/
GPIO_InitStruct.Alternate = PWM_TIM_GPIO_AF;
/*选择要控制的GPIO引脚*/
GPIO_InitStruct.Pin = PWM_TIM_CH1_PIN;
/*调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO*/
HAL_GPIO_Init(PWM_TIM_CH1_GPIO_PORT, &GPIO_InitStruct);
GPIO_InitStruct.Pin = PWM_TIM_CH2_PIN;
HAL_GPIO_Init(PWM_TIM_CH2_GPIO_PORT, &GPIO_InitStruct);
}
|
定时器通道引脚使用之前必须设定相关参数,这选择复用功能,并指定到对应的定时器。 使用GPIO之前都必须开启相应端口时钟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | TIM_HandleTypeDef TIM_TimeBaseStructure;
static void TIM_PWMOUTPUT_Config(void)
{
TIM_OC_InitTypeDef TIM_OCInitStructure;
/*使能定时器*/
PWM_TIM_CLK_ENABLE();
TIM_TimeBaseStructure.Instance = PWM_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到PWM_PERIOD_COUNT,即为PWM_PERIOD_COUNT+1次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = PWM_PERIOD_COUNT - 1;
// 通用控制定时器时钟源TIMxCLK = HCLK/2=84MHz
// 设定定时器频率为=TIMxCLK/(PWM_PRESCALER_COUNT+1)
TIM_TimeBaseStructure.Init.Prescaler = PWM_PRESCALER_COUNT - 1;
/*计数方式*/
TIM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP;
/*采样时钟分频*/
TIM_TimeBaseStructure.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
/*初始化定时器*/
HAL_TIM_PWM_Init(&TIM_TimeBaseStructure);
/*PWM模式配置*/
TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.Pulse = 0;
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_LOW;
TIM_OCInitStructure.OCNPolarity = TIM_OCPOLARITY_LOW;
TIM_OCInitStructure.OCIdleState = TIM_OCIDLESTATE_SET;
TIM_OCInitStructure.OCNIdleState = TIM_OCNIDLESTATE_RESET;
TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1;//配置为PWM模式1
TIM_OCInitStructure.Pulse = PWM_PERIOD_COUNT/2;//默认占空比为50%
TIM_OCInitStructure.OCFastMode = TIM_OCFAST_DISABLE;
/*当定时器计数值小于CCR1_Val时为高电平*/
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
/*配置PWM通道*/
HAL_TIM_PWM_ConfigChannel(&TIM_TimeBaseStructure, &TIM_OCInitStructure, PWM_CHANNEL_1);
/*开始输出PWM*/
HAL_TIM_PWM_Start(&TIM_TimeBaseStructure,PWM_CHANNEL_1);
/*配置脉宽*/
TIM_OCInitStructure.Pulse = PWM_PERIOD_COUNT/2; // 默认占空比为50%
/*配置PWM通道*/
HAL_TIM_PWM_ConfigChannel(&TIM_TimeBaseStructure, &TIM_OCInitStructure, PWM_CHANNEL_2);
/*开始输出PWM*/
HAL_TIM_PWM_Start(&TIM_TimeBaseStructure,PWM_CHANNEL_2);
}
|
首先定义两个定时器初始化结构体,定时器模式配置函数主要就是对这两个结构体的成员进行初始化, 然后通过相应的初始化函数把这些参数写入定时器的寄存器中。有关结构体的成员介绍请参考定时器详解章节。
不同的定时器可能对应不同的APB总线,在使能定时器时钟是必须特别注意。通用控制定时器属于APB1, 定时器内部时钟是84MHz。
在时基结构体中我们设置定时器周期参数为PWM_PERIOD_COUNT(5599),频率为15KHz,使用向上计数方式。 因为我们使用的是内部时钟,所以外部时钟采样分频成员不需要设置,重复计数器我们没用到,也不需要设置。
在输出比较结构体中,设置输出模式为PWM1模式,通道输出高电平有效,设置脉宽为ChannelPulse, ChannelPulse是我们定义的一个无符号16位整形的全局变量,用来指定占空比大小, 实际上脉宽就是设定比较寄存器CCR的值,用于跟计数器CNT的值比较。
最后使用HAL_TIM_PWM_Start函数让计数器开始计数和通道输出。
1 2 3 4 5 6 | /* 电机方向控制枚举 */
typedef enum
{
MOTOR_FWD = 0,
MOTOR_REV,
}motor_dir_t;
|
在这里枚举了两个变量,用于控制电机的正转与反转。注意:在这里并不规定什么方向是正转与反转, 这个是你自己定义的。
1 2 3 | /* 私有变量 */
static motor_dir_t direction = MOTOR_FWD; // 记录方向
static uint16_t dutyfactor = 0; // 记录占空比
|
定义两个私有变量,direction用于记录电机旋转方向,dutyfactor用于记录当前设置的占空比。
1 2 3 4 5 6 7 8 9 10 11 | /* 设置速度(占空比) */
#define SET_FWD_COMPAER(ChannelPulse) TIM1_SetPWM_pulse(PWM_CHANNEL_1,ChannelPulse) // 设置比较寄存器的值
#define SET_REV_COMPAER(ChannelPulse) TIM1_SetPWM_pulse(PWM_CHANNEL_2,ChannelPulse) // 设置比较寄存器的值
/* 使能输出 */
#define MOTOR_FWD_ENABLE() HAL_TIM_PWM_Start(&TIM_TimeBaseStructure,PWM_CHANNEL_1); // 使能 PWM 通道 1
#define MOTOR_REV_ENABLE() HAL_TIM_PWM_Start(&TIM_TimeBaseStructure,PWM_CHANNEL_2); // 使能 PWM 通道 2
/* 禁用输出 */
#define MOTOR_FWD_DISABLE() HAL_TIM_PWM_Stop(&TIM_TimeBaseStructure,PWM_CHANNEL_1); // 禁用 PWM 通道 1
#define MOTOR_REV_DISABLE() HAL_TIM_PWM_Stop(&TIM_TimeBaseStructure,PWM_CHANNEL_2); // 禁用 PWM 通道 2
|
使用宏定义非常方便程序升级、移植。如果使用不同的定时器IO,修改这些宏即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 | void set_motor_speed(uint16_t v)
{
dutyfactor = v;
if (direction == MOTOR_FWD)
{
SET_FWD_COMPAER(dutyfactor); // 设置速度
}
else
{
SET_REV_COMPAER(dutyfactor); // 设置速度
}
}
|
根据电机的旋转方向来设置电机的速度(占空比),并记录下设置的占空比,方便在切换旋转 方向时设置另一路为相同的占空比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void set_motor_direction(motor_dir_t dir)
{
direction = dir;
if (direction == MOTOR_FWD)
{
SET_FWD_COMPAER(dutyfactor); // 设置速度
SET_REV_COMPAER(0); // 设置占空比为 0
}
else
{
SET_FWD_COMPAER(0); // 设置速度
SET_REV_COMPAER(dutyfactor); // 设置占空比为 0
}
}
|
将一路PWM的占空比设置为0,另一路用于设置速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | int main(void)
{
__IO uint16_t ChannelPulse = 0;
uint8_t i = 0;
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/* 初始化按键GPIO */
Key_GPIO_Config();
/* 通用定时器初始化并配置PWM输出功能 */
TIMx_Configuration();
TIM1_SetPWM_pulse(PWM_CHANNEL_1,0);
TIM1_SetPWM_pulse(PWM_CHANNEL_2,0);
while(1)
{
/* 扫描KEY1 */
if( Key_Scan(KEY1_GPIO_PORT, KEY1_PIN) == KEY_ON)
{
/* 增大占空比 */
ChannelPulse += 50;
if(ChannelPulse > PWM_PERIOD_COUNT)
ChannelPulse = PWM_PERIOD_COUNT;
set_motor_speed(ChannelPulse);
}
/* 扫描KEY2 */
if( Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON)
{
if(ChannelPulse < 50)
ChannelPulse = 0;
else
ChannelPulse -= 50;
set_motor_speed(ChannelPulse);
}
/* 扫描KEY3 */
if( Key_Scan(KEY3_GPIO_PORT, KEY3_PIN) == KEY_ON)
{
/* 转换方向 */
set_motor_direction( (++i % 2) ? MOTOR_FWD : MOTOR_REV);
}
}
}
|
首先初始化系统时钟,然后初始化定时器和按键,将占空比设置为0,即电机默认不转动。 在死循环里面扫描按键,KEY1按键按下增加速度(占空比),KEY2按键按下减少速度(占空比), KEY3按键按下切换电机旋转方向。
下载验证¶
如果有条件的话,这里我们先不连接电机,先通过示波器连接到开发板的PWM输出引脚上,通过示波器来观察PWM 的变化情况:
- 使用DAP连接开发板到电脑;
- 使用示波器的CH1连接到PA15,CH2连接到PB3,注意示波器要与开发板共地;
- 给开发板供电,编译下载配套源码,复位开发板。
上电后我们通过示波器可以观察到两个通道都是低电平,当按下KEY1时,可以增加CH1通道的占空比,如下图所示。

在上图中黄色波形为CH1通道,蓝色波形为CH2通道,按下一次KEY1后,周期设置为500,所以CH1的占空比为 500/5600*100%=9%。通过波形计算也与理论相符,这说明我们的PWM的配置是正确的,其中CH2通道的波形 一直为低电平。当CH1和CH2都为低电平时,电机停止转动。当CH1上的平均电压大于电机的启动电压后电机就 可以转动了,电源电压为12V,占空比为D,则平均电压为:12V*D。当按下KEY3后两通道输出相反,CH1一直为 低电平,CH2为PWM波,电机反向转动。
在确定PWM输出正确后我们就可以接上电机进行验证我们的程序了,实物连接如下图所示。

舵机控制¶
舵机是一种位置(角度)伺服的驱动器,适用于那些需要角度不断变化并可以保持的控制系统。 舵机是一种俗称,其实是一种伺服马达。伺服马达内部包括了一个小型直流马达;一组变速齿轮组; 一个反馈可调电位器;及一块电子控制板。
舵机工作原理¶
舵机内部的电子控制板对外接收控制信号,控制板处理控制信号后获得直流偏置电压。在控制板内部又有一个基准电路,它产生周期为 20ms,宽度为1.5ms的基准信号,将获得的直流偏置电压与电位器的电压比较,获得电压差输出。最后, 电压差的正负输出到电机驱动芯片决定电机的正反转。当电机转速一定时,通过级联减速齿轮带动电位器旋转, 使得电压差为0,电机停止转动。

舵机控制原理¶
舵机的输入有三根线,一般的中间的红色线为电源正极,咖啡色线的为电源负极,黄色色线为控制线号线。 如下图所示。

舵机的控制通常采用PWM信号,例如需要一个周期为20ms的脉冲宽度调制(PWM), 脉冲宽度部分一般为0.5ms-2.5ms范围内的角度控制脉冲部分,总间隔为2ms。 当脉冲宽度为1.5ms时,舵机旋转至中间角度,大于1.5ms时 舵机旋转角度增大,小于1.5ms时舵机旋转角度减小。舵机分90°、180°、270°和360°舵机, 以180°的舵机为例来看看脉冲宽度与角度的关系,见下图所示。

上图中脉冲宽度与舵机旋转角度为线性关系,其他舵机控制脉冲也类似,0.5ms对应0度,2.5ms对应最大 旋转角度,脉冲宽度与旋转角度也是线性关系。
舵机几个参数介绍¶
舵机速度的单位是sec/60°,已就是舵机转过60°需要的时间,如果控制脉冲变化宽度大,变化速度快, 舵机就有可能在一次脉冲的变化过程中还没有转到目标角度时,而脉冲就再次发生了变化, 舵机的转动速度一般有0.16sec/60°、0.12sec/60°等,0.16sec/60°就是舵机转动60°需要0.16秒的时间。 舵机的速度还有工作电压有关,在允许的电压范围内,电压越大速度越快,反之亦然。
舵机扭矩的单位是KG*CM,这是一个扭矩的单位,可以理解为在舵盘上距离舵机轴中心水平距离1CM处, 舵机能够带动的物体重量,如下图所示。

通常说的55g舵机、9g舵机等,这里的55g和9g指的是舵机本身的重量。
舵机控制实现¶
硬件设计¶
介绍电路和舵机
软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请 参考本章配套的工程。我们创建了四个文件:bsp_general_tim.c、bsp_general_tim.h文件用来 存定时器驱动和舵机控制程序及相关宏定义
编程要点¶
- 定时器 IO 配置
- 定时器时基结构体TIM_TimeBaseInitTypeDef配置
- 定时器输出比较结构体TIM_OCInitTypeDef配置
- 封装一个舵机角度控制函数
- 在main函数中编写按键舵机控制代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /*宏定义*/
#define GENERAL_TIM TIM4
#define GENERAL_TIM_GPIO_AF GPIO_AF2_TIM4
#define GENERAL_TIM_CLK_ENABLE() __TIM4_CLK_ENABLE()
#define PWM_CHANNEL_1 TIM_CHANNEL_1
//#define PWM_CHANNEL_2 TIM_CHANNEL_2
//#define PWM_CHANNEL_3 TIM_CHANNEL_3
//#define PWM_CHANNEL_4 TIM_CHANNEL_4
/* 累计 TIM_Period个后产生一个更新或者中断*/
/* 当定时器从0计数到PWM_PERIOD_COUNT,即为PWM_PERIOD_COUNT+1次,为一个定时周期 */
#define PWM_PERIOD_COUNT 999
/* 通用控制定时器时钟源TIMxCLK = HCLK/2=84MHz */
/* 设定定时器频率为=TIMxCLK/(PWM_PRESCALER_COUNT+1) */
#define PWM_PRESCALER_COUNT 1679
/*PWM引脚*/
#define GENERAL_TIM_CH1_GPIO_PORT GPIOD
#define GENERAL_TIM_CH1_PIN GPIO_PIN_12
//#define GENERAL_TIM_CH2_GPIO_PORT GPIOD
//#define GENERAL_TIM_CH2_PIN GPIO_PIN_13
|
使用宏定义非常方便程序升级、移植。如果使用不同的定时器IO,修改这些宏即可。
定时器复用功能引脚初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | static void TIMx_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/* 定时器通道功能引脚端口时钟使能 */
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 定时器通道1功能引脚IO初始化 */
/*设置输出类型*/
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
/*设置引脚速率 */
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/*设置复用*/
GPIO_InitStruct.Alternate = GENERAL_TIM_GPIO_AF;
/*选择要控制的GPIO引脚*/
GPIO_InitStruct.Pin = GENERAL_TIM_CH1_PIN;
/*调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO*/
HAL_GPIO_Init(GENERAL_TIM_CH1_GPIO_PORT, &GPIO_InitStruct);
}
|
定时器通道引脚使用之前必须设定相关参数,这选择复用功能,并指定到对应的定时器。 使用GPIO之前都必须开启相应端口时钟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | TIM_HandleTypeDef TIM_TimeBaseStructure;
static void TIM_PWMOUTPUT_Config(void)
{
TIM_OC_InitTypeDef TIM_OCInitStructure;
/*使能定时器*/
GENERAL_TIM_CLK_ENABLE();
TIM_TimeBaseStructure.Instance = GENERAL_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到PWM_PERIOD_COUNT,即为PWM_PERIOD_COUNT+1次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = PWM_PERIOD_COUNT;
// 通用控制定时器时钟源TIMxCLK = HCLK/2=84MHz
// 设定定时器频率为=TIMxCLK/(PWM_PRESCALER_COUNT+1)
TIM_TimeBaseStructure.Init.Prescaler = PWM_PRESCALER_COUNT;
/*计数方式*/
TIM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP;
/*采样时钟分频*/
TIM_TimeBaseStructure.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
/*初始化定时器*/
HAL_TIM_Base_Init(&TIM_TimeBaseStructure);
/*PWM模式配置*/
TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1; // 配置为PWM模式1
TIM_OCInitStructure.Pulse = 0.5/20.0*PWM_PERIOD_COUNT; // 默认占空比
TIM_OCInitStructure.OCFastMode = TIM_OCFAST_DISABLE;
/*当定时器计数值小于CCR1_Val时为高电平*/
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
/*配置PWM通道*/
HAL_TIM_PWM_ConfigChannel(&TIM_TimeBaseStructure, &TIM_OCInitStructure, PWM_CHANNEL_1);
/*开始输出PWM*/
HAL_TIM_PWM_Start(&TIM_TimeBaseStructure,PWM_CHANNEL_1);
}
|
首先定义两个定时器初始化结构体,定时器模式配置函数主要就是对这两个结构体的成员进行初始化,然后通过相 应的初始化函数把这些参数写入定时器的寄存器中。有关结构体的成员介绍请参考定时器详解章节。
不同的定时器可能对应不同的APB总线,在使能定时器时钟是必须特别注意。通用控制定时器属于APB1, 定时器内部时钟是84MHz。
在时基结构体中我们设置定时器周期参数为PWM_PERIOD_COUNT(999),频率为50Hz,使用向上计数方式。 因为我们使用的是内部时钟,所以外部时钟采样分频成员不需要设置,重复计数器我们没用到,也不需要设置, 然后调用HAL_TIM_Base_Init初始化定时器。
在输出比较结构体中,设置输出模式为PWM1模式,通道输出高电平有效,设置默认脉宽为PWM_PERIOD_COUNT, PWM_PERIOD_COUNT是我们定义的一个宏,用来指定占空比大小,实际上脉宽就是设定比较寄存器CCR的值, 用于跟计数器CNT的值比较。然后调用HAL_TIM_PWM_ConfigChannel初始化PWM输出。
最后使用HAL_TIM_PWM_Start函数让计数器开始计数和通道输出。
1 2 3 4 5 6 7 8 9 10 11 12 | void set_steering_gear_dutyfactor(uint16_t dutyfactor)
{
#if 1
{
/* 对超过范围的占空比进行边界处理 */
dutyfactor = 0.5/20.0*PWM_PERIOD_COUNT > dutyfactor ? 0.5/20.0*PWM_PERIOD_COUNT : dutyfactor;
dutyfactor = 2.5/20.0*PWM_PERIOD_COUNT < dutyfactor ? 2.5/20.0*PWM_PERIOD_COUNT : dutyfactor;
}
#endif
TIM2_SetPWM_pulse(PWM_CHANNEL_1, dutyfactor);
}
|
封装一个舵机占空比设置函数,接收一个参数用于设置PWM的占空比,并对输入的参数进行合法性检查,将脉冲宽度限制 在0.5~2.5ms之间。
1 2 3 4 5 6 | void set_steering_gear_angle(uint16_t angle_temp)
{
angle_temp = (0.5 + angle_temp / 180.0 * (2.5 - 0.5)) / 20.0 * PWM_PERIOD_COUNT; // 计算角度对应的占空比
set_steering_gear_dutyfactor(angle_temp); // 设置占空比
}
|
该函数用于设置舵机角度,传入角度值然后计算占空比,最后条用set_steering_gear_dutyfactor()来设置占空比。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | void deal_serial_data(void)
{
int angle_temp=0;
//接收到正确的指令才为1
char okCmd = 0;
//检查是否接收到指令
if(receive_cmd == 1)
{
if(UART_RxBuffer[0] == 'a' || UART_RxBuffer[0] == 'A')
{
//设置速度
if(UART_RxBuffer[1] == ' ')
{
angle_temp = atoi((char const *)UART_RxBuffer+2);
if(angle_temp>=0 && angle_temp <= 180)
{
printf("\n\r角度: %d\n\r", angle_temp);
angle_temp = (0.5 + angle_temp / 180.0 * (2.5 - 0.5)) / 20.0 * PWM_PERIOD_COUNT;
ChannelPulse = angle_temp; // 同步按键控制的比较值
set_steering_gear_angle(angle_temp);
okCmd = 1;
}
}
}
else if(UART_RxBuffer[0] == '?')
{
//打印帮助命令
show_help();
okCmd = 1;
}
//如果指令有无则打印帮助命令
if(okCmd != 1)
{
printf("\n\r 输入有误,请重新输入...\n\r");
show_help();
}
//清空串口接收缓冲数组
receive_cmd = 0;
uart_FlushRxBuffer();
}
}
|
以上为串口接收处理函数,接收正确的指令后将字符串计算出正确的角度值,判断角度值是否是在有效范围内, 同步按键调节的占空比防止按钮调节时转动范围过大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | int main(void)
{
/* HAL 库初始化 */
HAL_Init();
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/* 初始化按键GPIO */
Key_GPIO_Config();
/* 初始化串口 */
DEBUG_USART_Config();
/* 通用定时器初始化并配置PWM输出功能 */
TIMx_Configuration();
printf("野火舵机控制实验\r\n");
show_help();
while(1)
{
/* 处理数据 */
if (Key_Scan(KEY1_GPIO_PORT, KEY1_PIN) == KEY_ON)
{
ChannelPulse -= 10; // 减少占空比
ChannelPulse = 0.5/20.0*PWM_PERIOD_COUNT > ChannelPulse ? 0.5/20.0*PWM_PERIOD_COUNT : ChannelPulse; // 检查占空比的合法性
set_steering_gear_dutyfactor(ChannelPulse); // 设置占空比
}
/* 处理数据 */
if (Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON)
{
ChannelPulse += 10; // 增加占空比
ChannelPulse = (2.5/20.0*PWM_PERIOD_COUNT) < ChannelPulse ? (2.5/20.0*PWM_PERIOD_COUNT) : ChannelPulse; // 检查占空比的合法性
set_steering_gear_dutyfactor(ChannelPulse); // 设置占空比
}
/* 串口处理 */
deal_serial_data();
}
}
|
初始化串口、定时器输出PWM和按键等外设,最后在循环里面处理按键和串口接收的数据。当KEY1按下后, 减少占空比,并检查占空比是否在有效范围内,然后设置占空比,当KEY2按下后,增加占空比,并检查占空比 是否在有效范围内,然后设置占空比。最后调用deal_serial_data()来处理串口接收的函数。
下载验证¶
如果有条件的话,这里我们先不连接舵机,先通过示波器连接到开发板的PWM输出引脚上,通过示波器来观察PWM 的变化情况:
- 使用DAP连接开发板到电脑;
- 使用示波器的CH1连接到PA15,注意示波器要与开发板共地;
- 给开发板供电,编译下载配套源码,复位开发板。
上电后我们通过示波器可以观察到CH1通道的PWM波形,当按下KEY1或者KEY2时,可以改变CH1通道的占空比, 如下图所示。

经过验证可以知道我们的PWM脉冲宽度是在0.5~2.5ms之间变化。这正是我们想要的结果,这说明我们的代码是 正确的,这时我们就可以接上舵机来测试了。
通过按键KEY1和KEY2来调整舵机角度,或者通过串口来控制舵机角度。
步进电机¶
介绍¶
步进电机又称为脉冲电机,基于最基本的电磁铁原理,它是一种可以自由回转的电磁铁,其动作原理是依靠气隙磁导的变化来产生电磁转矩。 由于步进电机是一个可以把电脉冲转换成机械运动的装置,具有很好的数据控制特性,因此,计算机成为步进电机的理想驱动源, 随着微电子和计算机技术的发展,软硬件结合的控制方式成为了主流,即通过程序产生控制脉冲,驱动硬件电路。单片机通过软件来控制步进电机, 更好地挖掘出了电机的潜力。在不超载的情况下电机的转速和停止位置只取决于脉冲信号的频率和数量; 并且步进电机的脉冲与步进旋转的角度成正比,脉冲的频率与步进的转速成正比,所以可以很好的从源头控制信号的输出; 且步进电机只有周期性的误差,使得在速度、位置等控制领域用步进电机来控制变的非常的简单。话虽如此,想要玩转步进也不是件容易的事情。

混合式步进电机拆解图
工作原理¶
通常步进电机的转子为永磁体,当电流流过定子绕组时,定子绕组产生一矢量磁场。磁场会带动转子旋转一定的角度, 使得转子的一对磁场方向与定子的磁场方向一致。当定子的矢量磁场旋转一个角度。转子也随着该磁场转步距角。 每输入一个电脉冲,电动机转动一个角度前进一步。它输出的角位移与输入的脉冲数成正比、转速与脉冲频率成正比。 改变绕组通电的顺序,电机就会反转。所以可用控制脉冲数量、频率及电动机各相绕组的通电顺序来控制步进电机的转动。具体看下图:

步进电机横截图
步进电机极性区分¶
步进电机又分为单极性的步进电机和双极性的步进电机;具体简易图如下图所示:

其中左侧为单极性步进电机,右侧为双极性的步进电机,从上图中不难看出区别是什么。单双极性是指一个步进电机里面有几种电流的流向, 左侧的五线四相步进电机就是单极性的步进电机,图中的红色箭头为电流的走向,四根线的电流走向汇总到公共线,所以称之为单极性电机; 但是右侧则不同,电机中有两个电流的回路,两个电流的回路自然就是双极性,所以称之为双极性电机。
单极性绕组
单极性步进电机使用的是单极性绕组。其一个电极上有两个绕组,这种联接方式为当一个绕组通电时,产生一个北极磁场; 另一个绕组通电,则产生一个南极磁场。因为从驱动器到线圈的电流不会反向,所以可称其为单极绕组。
双极性绕组
双极性步进电机使用的是双极性绕组。每相用一个绕组,通过将绕组中电流反向,电磁极性被反向。 典型的两相双极驱动的输出步骤在电气原理图和下图中的步进顺序中进一步阐述。 按图所示,转换只利用绕组简单地改变电流的方向,就能改变该组的极性。

永磁步进电机包括一个永磁转子、线圈绕组和导磁定子,只要将线圈通电根据电磁铁的原理就会产生磁场,分为南北极,见上图所示; 通过改变步进电机定子的磁场,转子就会因磁场的变化而发生转动,同理,依次改变通电的顺序就可以使得电机转动起来。
双极性步进电机驱动原理¶
下图是一个双极性的步进电机整步,步进顺序。 在第一步中:将A相通电,根据电磁铁原理,产生磁性,并且因异性相吸,所以磁场将转子固定在第一步的位置; 第二步:当A相关闭,B相通电时,转子会旋转90°; 第三步:B相关闭、A相通电,但极性与第1步相反,这促使转子再次旋转90°。 在第四步中:A相关闭、B相通电,极性与第2步相反。重复该顺序促使转子按90°的步距角顺时针旋转。

上图中显示的步进顺序是单相激励步进,也可以理解为每次通电产生磁性的相只有一个,要么A相,要么B相; 但是更常用的是双相激励,但是在转换时,一次只能换相一次,具体详见下图:

上图是两相同时通电的旋转顺序,与单相激励不同的是,单相通电后被固定在了与定子正对着的绕组极性, 但是双相同时激励时转子却被固定在两个绕阻的极性中间;此时通电顺序就变成了AB相同时通电即可。
在双相激励的过程中,也可以在装换相位时加一个关闭相位的状态而产生走半步的现象,这将步进电机的整个步距角一分为二, 例如,一个90°的步进电机将每半步移动45°,具体见下图。

- A相通电,B相不通电
- A、B相全部通电,且电流相同,产生相同磁性
- B相通电,A断电
- B相通电,A相通电,且电流相等,产生相同磁性
- A相通电,B断电
- A、B相全部通电,且电流相同,产生相同磁性
- B相通电,A断电
- B相通电,A相通电,且电流相等,产生相同磁性
其中1~4步与5~8步的电流方向相反(电流相反,电磁的极性就相反)这样就产生了顺时针旋转,同理逆时针是 将通电顺序反过来即可。
单极性步进电机驱动原理¶
单极性与双极性步进电机驱动类似,都可以分为整步与半步的驱动方式,不同的是,双极性的步进电机可以通过改变 电流的方向来改变每相的磁场方向,但是单极性的就不可以了,它有一个公共端,这就直接决定了,电流方向。具体旋转顺序详见下图:

上图是单极性步进电机整步旋转的过程,其中,在图示中分为5根线,分别为A、B、C、D和公共端(+),公共端需要一直通电, 剩下ABCD相中只要有一个相通电,即可形成回路产生磁场,图中的通电顺序为A->B->C->D,即可完成上图中的顺时针旋转, 如果想要逆时针旋转只需要将其倒序即可。
以上是单相通电产生的整步旋转,两相通电也可以产生,两个相邻的相通电,这样相邻的两个相就都产生了回路,也就产生了磁场, 图中的通电顺序为AB->BC->CD->DA,同理逆时针旋转的顺序为逆序。具体看下图:

上面两张图清晰的描述了单极性步进电机的通电顺序与旋转的过程,综合这两张图就是单极性步进电机半步的通电顺序,具体看下图:

上图兼容了前两张图的所有特点,也可以说前两张图是这张图的子集,图中的通电顺序为:A->AB->B->BC->C->CD->D->DA 转子每次只走半步45度,所以这也被称为半步驱动,与整步相比半步的旋转方式旋转起来更加的顺滑。
细分器驱动原理¶
对于细分驱动的原理,不分单双极步进电机,下图以单极为例:

在上图中均为双相激励;其中图(a)为A相电流很大,B相的电流极其微弱,接近0; 图(C)为A相和B相的电流相同,电流决定磁场,所以说A相和B相的磁场也是相同的,(a)和(c)可以是极限特殊的情况, 再看图(b)和图(d)这两个是由于A相和B相的电流不同产生位置情况;由此可以得出改变定子的电流比例就可以使得转子在任意角度停住。 细分的原理就是:通过改变定子的电流比例,改变转子在一个整步中的不同位置,可以将一个整步分成多个小步来运行。
在上图中就是一个整步分成了4步来跑,从(a)~(d)是A相的电流逐渐减小,B相电流逐渐增大的过程,如果驱动器的细分能力很强, 可以将其分成32细分、64细分等;这不仅提高了步进电机旋转的顺畅度而且提高了每步的精度。
技术指标术语¶
静态指标术语¶
- 相数:产生不同对极N、S磁场的激磁线圈对数,也可以理解为步进电机中线圈的组数,其中两相步进电机步距角为1.8°, 三相的步进电机步距角为1.5°,相数越多的步进电机,其步距角就越小。
- 拍数:完成一个磁场周期性变化所需脉冲数或导电状态用n表示,或指电机转过一个齿距角所需脉冲数,以四相电机为例, 有四相四拍运行方式即AB-BC-CD-DA-AB,四相八拍运行方式即 A-AB-B-BC-C-CD-D-DA-A。
- 步距角:一个脉冲信号所对应的电机转动的角度,可以简单理解为一个脉冲信号驱动的角度,电机上都有写,一般42步进电机的步距角为1.8°
- 定位转矩:电机在不通电状态下,电机转子自身的锁定力矩(由磁场齿形的谐波以及机械误差造成的)。
- 静转矩:电机在额定静态电压作用下,电机不作旋转运动时,电机转轴的锁定力矩。此力矩是衡量电机体积的标准,与驱动电压及驱动电源等无关。
动态指标术语¶
- 步距角精度:步进电机转动一个步距角度的理论值与实际值的误差。用百分比表示:误差/步距角*100%。
- 失步:电机运转时运转的步数,不等于理论上的步数。也可以叫做丢步,一般都是因负载太大或者是频率过快。
- 失调角:转子齿轴线偏移定子齿轴线的角度,电机运转必存在失调角,由失调角产生的误差,采用细分驱动是不能解决的。
- 最大空载起动频率:在不加负载的情况下,能够直接起动的最大频率。
- 最大空载的运行频率:电机不带负载的最高转速频率。
- 运行转矩特性:电机的动态力矩取决于电机运行时的平均电流(而非静态电流),平均电流越大,电机输出力矩越大,即电机的频率特性越硬。
- 电机正反转控制:通过改变通电顺序而改变电机的正反转。
主要特点¶
- 步进电机的精度大概为步距角的3-5%,且不会积累
- 步进电机的外表允许的最高温度:一般步进电机会因外表温度过高而产生磁性减小,从而会导致力矩较小, 一般来说磁性材料的退磁点都在摄氏130度以上,有的甚至高达摄氏200度以上,所以步进电机外表温度在摄氏80-90度完全正常。
- 步进电机的转矩与速度成反比,速度越快力矩越小。
- 低速时步进电机可以正常启动,高速时不会启动,并伴有啸叫声。步进电机的空载启动频率是固定的, 如果高于这个频率电机不能被启动并且会产生丢步或者堵转。
驱动器简介¶
步进电机必须要有控制器和驱动器才可以使电机正常工作,控制器是stm32或者其它型号的MCU了,驱动器就是步进电机驱动器了。 为什么要使用驱动器呢?驱动器起到将控制器信号放大或者转换的作用,如下图所示,控制器输出方向信号和脉冲信号来控制步进电机驱动器, 驱动器将其功率放大然后作用到步进电机上。

野火步进电机细分器介绍¶
BHMSD4805是野火科技推出的一款智能步进电机驱动器。它是一款以双极恒流PWM驱动输出控制电机的驱动器,驱动电压范围DC12V~48V, 适合外径为42mm、 57mm、86mm系列,驱动电流在5A以下的所有两相混合式步进电机。 根据驱动器提供的8位拨码开关可以轻松的实现对不同电机电流及不同细分步数的精确控制。带有自动半流技术, 可以大大降低电机的功耗及发热量,输入信号都经过光耦隔离,具有很强的抗干扰能力,能适应恶劣的工作环境,下图为产品实物图。

驱动器性能表
参数 | 说明 | |
---|---|---|
额定电压 | 直流: | 12V~48V |
额定电流 | 0.75A~5.0A | |
驱动方式 | 双极恒流PWM驱动输出 | |
工作温度 | 0℃~80℃ | |
结构尺寸 | 118*75.5*33 | 单位mm |
应用领域 | 数控设备、雕刻机等设备 |
模块引脚说明¶
驱动器右侧分别是电源及故障指示灯、控制信号接口、参数设定拨码开关、电源驱动接口,在其端子的正,上方是对应引脚名称的丝印。
控制信号引脚如下表所示:
序号 | 引脚名称 | 引脚定义 |
---|---|---|
1 | ENA-(ENA) | 输出使能负端 |
2 | ENA+(5V) | 输出使能正端 |
3 | DIR-(DIR) | 方向控制负端 |
4 | DIR+(5V) | 方向控制正端 |
5 | PUL-(PLU) | 脉冲控制负端 |
6 | PUL+(5V) | 脉冲控制正端 |
- ENA功能说明:控制器的输出是通过该组信号使能,又称脱机信号。当此信号有效时,输出关闭,电机绕组电流为零, 电机为无力矩状态,可以自由转动电机,适合需要手动调整电机的场合。
- DIR功能说明:电机的方向控制信号,当此信号有效时,电机顺时针转动,当此信号无效时,电机逆时针旋转。
- PUL功能说明:电机的转动控制信号,驱动器接收到的脉冲信号电机就会按照既定的方向旋转。电机的角位移与脉冲的数量成正比, 速度与脉冲的频率成正比。通常脉冲的有效宽度>=5us,频率<=125KHz。
拨码开关引脚如下表所示:
序号 | 引脚名称 | 引脚定义 |
---|---|---|
1 | SW1~SW4 | 细分设定 |
2 | SW5~SW8 | 电流设定 |
细分参数设置
驱动器的细分设置由拨码开关的SW1~SW4来设定,默认为100细分,一般的两相四线制步进电机的步进角都是1.8°, 因此电机旋转一圈需要360° /1.8° =200个脉冲,这里100细分转一圈 需要的脉冲数为200*100=20000个。具体详见下表:
细分 | 脉冲 | SW1 | SW2 | SW3 | SW4 |
---|---|---|---|---|---|
2 | 400 | ON | ON | ON | ON |
4 | 800 | OFF | ON | ON | ON |
8 | 1600 | ON | OFF | ON | ON |
16 | 3200 | OFF | OFF | ON | ON |
32 | 6400 | ON | ON | OFF | ON |
64 | 12800 | OFF | ON | OFF | ON |
128 | 25600 | ON | OFF | OFF | ON |
3 | 600 | OFF | OFF | OFF | ON |
6 | 1200 | ON | ON | ON | OFF |
12 | 2400 | OFF | ON | ON | OFF |
36 | 7200 | ON | OFF | ON | OFF |
5 | 1000 | OFF | OFF | ON | OFF |
10 | 2000 | ON | ON | OFF | OFF |
20 | 4000 | OFF | ON | OFF | OFF |
50 | 10000 | ON | OFF | OFF | OFF |
100 | 20000 | OFF | OFF | OFF | OFF |
电流参数设置
驱动器的电流设置由拨码开关的SW5~SW8来设定,默认为1.5A。这个电流值需要根据步进电机的额定电流来设定。 一般建议驱动器的输出电流设定和电机额定电流差不多或者小一点,详细设定见下表:
电流 | SW5 | SW6 | SW7 | SW8 |
---|---|---|---|---|
0.75A | OFF | OFF | OFF | ON |
1.00A | ON | OFF | OFF | ON |
1.25A | OFF | ON | OFF | ON |
1.50A | OFF | OFF | OFF | OFF |
1.75A | OFF | OFF | ON | ON |
2.00A | ON | OFF | OFF | OFF |
2.25A | OFF | ON | ON | ON |
2.50A | OFF | ON | OFF | OFF |
3.00A | ON | ON | OFF | OFF |
3.50A | OFF | OFF | ON | OFF |
4.00A | ON | OFF | ON | OFF |
4.50A | OFF | ON | ON | OFF |
5.00A | ON | ON | ON | OFF |
接线方式
驱动器与控制器共有两种接线方式,分别为共阴极接法和供阳极接法:
共阴极接法如图所示:

共阳极接法如图所示:

驱动器引脚 | 电机绕组接线 |
---|---|
A+ | 蓝色 |
A- | 红色 |
B+ | 绿色 |
B- | 黑色 |
当输入信号高于5V时,可根据需要外接限流电阻。
步进电机基础旋转控制¶
在本章前几个小节对步进电机的工作原理、特点以及驱动器的进行了详细的讲解, 本小节将对最基本的控制方法进行例举和讲解;
硬件设计¶
介绍步进电机的电路与接线方法
隔离电路
步进电机光耦隔离部分电路

上图为原理图中的隔离电路,其中主要用到的是高速的光耦进行隔离,在这里隔离不仅可以防止外部电流倒灌, 损坏芯片,还有增强驱动能力的作用;并且在开发板这端已经默认为共阳极接法了,可以将步进电机的所有线按照对应的顺序接在端子上, 也可以在驱动器一端实现共阴或者共阳的接法。
接线方法
接线的方法可以查看 模块引脚说明 章节,里面有详细的介绍。
软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 对于步进电机的基础控制部分,共使用了四种方式进行控制,层层递巩固基础。分别为:使用GPIO延时模拟脉冲控制、 使用GPIO中断模拟脉冲控制、使用PWM比较输出和使用PWM控制匀速旋转。
第一种方式:使用GPIO延时模拟脉冲控制¶
编程要点
- 通用GPIO配置
- GPIO结构体GPIO_InitTypeDef配置
- 封装stepper_turn()函数用于控制步进电机旋转
- 在main函数中编写按键控制步进电机旋转的代码
宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //引脚定义
/*******************************************************/
//Motor 方向
#define MOTOR_DIR_PIN GPIO_PIN_1
#define MOTOR_DIR_GPIO_PORT GPIOE
#define MOTOR_DIR_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 使能
#define MOTOR_EN_PIN GPIO_PIN_0
#define MOTOR_EN_GPIO_PORT GPIOE
#define MOTOR_EN_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 脉冲
#define MOTOR_PUL_PIN GPIO_PIN_5
#define MOTOR_PUL_GPIO_PORT GPIOI
#define MOTOR_PUL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOI_CLK_ENABLE()
/************************************************************/
#define HIGH 1 //高电平
#define LOW 0 //低电平
#define ON 0 //开
#define OFF !0 //关
#define CLOCKWISE 1//顺时针
#define ANTI_CLOCKWISE 0//逆时针
//控制使能引脚
/* 带参宏,可以像内联函数一样使用 */
#define MOTOR_EN(x) HAL_GPIO_WritePin(MOTOR_EN_GPIO_PORT,MOTOR_EN_PIN,x)
#define MOTOR_PLU(x) HAL_GPIO_WritePin(MOTOR_PUL_GPIO_PORT,MOTOR_PUL_PIN,x)
#define MOTOR_DIR(x) HAL_GPIO_WritePin(MOTOR_DIR_GPIO_PORT,MOTOR_DIR_PIN,x)
|
使用宏定义非常方便程序升级、移植。如果使用不同的GPIO,修改这些宏即可。
步进电机引脚初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /**
* @brief 引脚初始化
* @retval 无
*/
void stepper_Init()
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStruct;
/*开启Motor相关的GPIO外设时钟*/
MOTOR_DIR_GPIO_CLK_ENABLE();
MOTOR_PUL_GPIO_CLK_ENABLE();
MOTOR_EN_GPIO_CLK_ENABLE();
/*选择要控制的GPIO引脚*/
GPIO_InitStruct.Pin = MOTOR_DIR_PIN;
/*设置引脚的输出类型为推挽输出*/
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull =GPIO_PULLUP;// GPIO_PULLDOWN GPIO_PULLUP
/*设置引脚速率为高速 */
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/*Motor 方向引脚 初始化*/
HAL_GPIO_Init(MOTOR_DIR_GPIO_PORT, &GPIO_InitStruct);
/*Motor 脉冲引脚 初始化*/
GPIO_InitStruct.Pin = MOTOR_PUL_PIN;
HAL_GPIO_Init(MOTOR_PUL_GPIO_PORT, &GPIO_InitStruct);
/*Motor 使能引脚 初始化*/
GPIO_InitStruct.Pin = MOTOR_EN_PIN;
HAL_GPIO_Init(MOTOR_EN_GPIO_PORT, &GPIO_InitStruct);
/*关掉使能*/
MOTOR_EN(OFF);
}
|
步进电机引脚使用必须选择相应的模式和设置对应的参数,使用GPIO之前都必须开启相应端口时钟。 初始化结束后可以先将步进电机驱动器的使能先关掉,需要旋转的时候,再将其打开即可。
封装步进电机旋转函数
由于脉冲为模拟产生的所以必须使用模拟的方式来产生所需的特定脉冲
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /**
* @brief 步进电机旋转
* @param tim 方波周期 单位MS 周期越短频率越高,转速越快 细分为1时最少10ms
* @param angle 需要转动的角度值
* @param dir 选择正反转(取值范围:0,1)
* @param subdivide 细分值
* @note 无
* @retval 无
*/
void stepper_turn(int tim,float angle,float subdivide,uint8_t dir)
{
int n,i;
/*根据细分数求得步距角被分成多少个方波*/
n=(int)(angle/(1.8/subdivide));
if(dir==CLOCKWISE) //顺时针
{
MOTOR_DIR(CLOCKWISE);
}
else if(dir==ANTI_CLOCKWISE)//逆时针
{
MOTOR_DIR(ANTI_CLOCKWISE);
}
/*开使能*/
MOTOR_EN(ON);
/*模拟方波*/
for(i=0;i<n;i++)
{
MOTOR_PLU(HIGH);
delay_us(tim/2);
MOTOR_PLU(LOW);
delay_us(tim/2);
}
/*关使能*/
MOTOR_EN(OFF);
}
|
此函数封装的功能为步进电机选装特定的角度,stepper_turn()函数共四个参数,这四个参数几乎是决定了步进电机的旋转的所有特性
- tim: tim用于控制脉冲的产生周期,周期越短频率越高,速度也就越快
- angle:angle用于控制步进电机旋转的角度,如果需要旋转一周,输入360即可
- subdivide:subdivide用于控制软件上的细分数,这个细分参数必须与硬件的细分参数保持一致
- dir:dir用于控制方向,dir为1时顺时针方向旋转,dir为0时逆时针方向旋转
在函数中 n=(int)(angle/(1.8/subdivide)); 根据函数传入的角度参数和步进电机的步角1.8°, 就可以算出在细分参数为1的情况下需要模拟的脉冲数,以此类推, 细分数为2、4、8、16时代入公式计算即可。
主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
int key_val=0;
int i=0;
int dir_val=0;
int angle_val=90;
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
DEBUG_USART_Config();
printf("欢迎使用野火 电机开发板 步进电机 IO口模拟控制 例程\r\n");
printf("按下按键1、2可修改旋转方向和角度\r\n");
/*按键初始化*/
Key_GPIO_Config();
/*步进电机初始化*/
stepper_Init();
/*开启步进电机使能*/
while(1)
{
/*获取键值*/
key_val=ret_key_num();
/*有按键按下*/
if(key_val)
{
if(key_val==1)
{
/*改变方向*/
dir_val=(++i % 2) ? CLOCKWISE : ANTI_CLOCKWISE;
}
else if(key_val==2)
{
/*增加旋转角度*/
angle_val=angle_val+90;
}
stepper_turn(1000,angle_val,32,dir_val);
/*打印状态*/
if(dir_val)
printf("顺时针旋转 %d 度\r\n",angle_val);
else
printf("逆时针旋转 %d 度\r\n",angle_val);
}
}
}
|
初始化系统时钟、串口、按键和步进电机IO等外设,最后在循环里面处理键值。当KEY1按下后, 改变旋转方向,当KEY2按下后,增加旋转角度,并打印旋转的状态与角度。
第二种方式:使用GPIO中断模拟脉冲控制¶
编程要点
- 通用GPIO配置
- 按键及其中断配置
- 步进电机、定时器中断初始化
- 在定时器中断翻转IO引脚
- 在按键中断中编写按键控制步进电机旋转的代码
宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | #define GENERAL_TIM TIM2
#define GENERAL_TIM_CLK_ENABLE() __TIM2_CLK_ENABLE()
#define GENERAL_TIM_IRQ TIM2_IRQn
#define GENERAL_TIM_INT_IRQHandler TIM2_IRQHandler
//引脚定义
/*******************************************************/
//Motor 方向
#define MOTOR_DIR_PIN GPIO_PIN_1
#define MOTOR_DIR_GPIO_PORT GPIOE
#define MOTOR_DIR_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 使能
#define MOTOR_EN_PIN GPIO_PIN_0
#define MOTOR_EN_GPIO_PORT GPIOE
#define MOTOR_EN_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 脉冲
#define MOTOR_PUL_PIN GPIO_PIN_15
#define MOTOR_PUL_GPIO_PORT GPIOA
#define MOTOR_PUL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE()
|
使用宏定义非常方便程序升级、移植。如果使用不同的GPIO,定时器更换对应修改这些宏即可。
按键初始化配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /**
* @brief 配置 key ,并设置中断优先级
* @param 无
* @retval 无
*/
void EXTI_Key_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启按键GPIO口的时钟*/
KEY1_INT_GPIO_CLK_ENABLE();
KEY2_INT_GPIO_CLK_ENABLE();
/* 选择按键1的引脚 */
GPIO_InitStructure.Pin = KEY1_INT_GPIO_PIN;
/* 设置引脚为输入模式 */
GPIO_InitStructure.Mode = GPIO_MODE_IT_RISING;
/* 设置引脚不上拉也不下拉 */
GPIO_InitStructure.Pull = GPIO_NOPULL;
/* 使用上面的结构体初始化按键 */
HAL_GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure);
/* 配置 EXTI 中断源 到key1 引脚、配置中断优先级*/
HAL_NVIC_SetPriority(KEY1_INT_EXTI_IRQ, 0, 0);
/* 使能中断 */
HAL_NVIC_EnableIRQ(KEY1_INT_EXTI_IRQ);
/* 选择按键2的引脚 */
GPIO_InitStructure.Pin = KEY2_INT_GPIO_PIN;
/* 其他配置与上面相同 */
HAL_GPIO_Init(KEY2_INT_GPIO_PORT, &GPIO_InitStructure);
/* 配置 EXTI 中断源 到key1 引脚、配置中断优先级*/
HAL_NVIC_SetPriority(KEY2_INT_EXTI_IRQ, 0, 0);
/* 使能中断 */
HAL_NVIC_EnableIRQ(KEY2_INT_EXTI_IRQ);
}
|
开启按键IO对应的时钟,配置中断源到引脚上,配置中断优先级并使能中断。当按键按下时,会自动进入中断函数并且执行相应代码。
定时器初始化配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /*
* 注意:TIM_TimeBaseInitTypeDef结构体里面有5个成员,TIM6和TIM7的寄存器里面只有
* TIM_Prescaler和TIM_Period,所以使用TIM6和TIM7的时候只需初始化这两个成员即可,
* 另外三个成员是通用定时器和高级定时器才有.
*-----------------------------------------------------------------------------
* TIM_Prescaler 都有
* TIM_CounterMode TIMx,x[6,7]没有,其他都有(通用定时器)
* TIM_Period 都有
* TIM_ClockDivision TIMx,x[6,7]没有,其他都有(通用定时器)
* TIM_RepetitionCounter TIMx,x[1,8]才有(高级定时器)
*-----------------------------------------------------------------------------
*/
static void TIM_Mode_Config(void)
{
GENERAL_TIM_CLK_ENABLE();
TIM_TimeBaseStructure.Instance = GENERAL_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到4999,即为5000次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = 300-1;
// 通用控制定时器时钟源TIMxCLK = HCLK/2=84MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=1MHz
TIM_TimeBaseStructure.Init.Prescaler = 84-1;
// 计数方式
TIM_TimeBaseStructure.Init.CounterMode=TIM_COUNTERMODE_UP;
// 采样时钟分频
TIM_TimeBaseStructure.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
// 初始化定时器TIMx, x[2,5] [9,14]
HAL_TIM_Base_Init(&TIM_TimeBaseStructure);
// 开启定时器更新中断
HAL_TIM_Base_Start_IT(&TIM_TimeBaseStructure);
}
|
首先对定时器进行初始化,定时器模式配置函数主要就是对这结构体的成员进行初始化,然后通过相 应的初始化函数把这些参数写入定时器的寄存器中。有关结构体的成员介绍请参考定时器详解章节。
由于定时器坐在的APB总线不完全一致,所以说,定时器的时钟是不同的,在使能定时器时钟时必须特别注意, 在这里使用的是定时器2,通用定时器的总线频率为84MHZ,分频参数选择为(84-1),也就是当计数器计数到1M时为一个周期, 计数累计到(300-1)时产生一个中断,使用向上计数方式。产生中断后翻转IO口电平即可。 因为我们使用的是内部时钟,所以外部时钟采样分频成员不需要设置,重复计数器我们没用到,也不需要设置, 然后调用HAL_TIM_Base_Init初始化定时器并开启定时器更新中断。
步进电机初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /**
* @brief 引脚初始化
* @retval 无
*/
void stepper_Init()
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStruct;
/*开启Motor相关的GPIO外设时钟*/
MOTOR_DIR_GPIO_CLK_ENABLE();
MOTOR_PUL_GPIO_CLK_ENABLE();
MOTOR_EN_GPIO_CLK_ENABLE();
/*选择要控制的GPIO引脚*/
GPIO_InitStruct.Pin = MOTOR_DIR_PIN;
/*设置引脚的输出类型为推挽输出*/
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull =GPIO_PULLUP;
/*设置引脚速率为高速 */
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/*Motor 方向引脚 初始化*/
HAL_GPIO_Init(MOTOR_DIR_GPIO_PORT, &GPIO_InitStruct);
/*Motor 脉冲引脚 初始化*/
GPIO_InitStruct.Pin = MOTOR_PUL_PIN;
HAL_GPIO_Init(MOTOR_PUL_GPIO_PORT, &GPIO_InitStruct);
/*Motor 使能引脚 初始化*/
GPIO_InitStruct.Pin = MOTOR_EN_PIN;
HAL_GPIO_Init(MOTOR_EN_GPIO_PORT, &GPIO_InitStruct);
/*关掉使能*/
MOTOR_EN(OFF);
/*初始化定时器*/
TIMx_Configuration();
}
|
步进电机引脚使用必须选择相应的模式和设置对应的参数,使用GPIO之前都必须开启相应端口时钟。 初始化结束后可以先将步进电机驱动器的使能先关掉,需要旋转的时候,再将其打开即可。 最后需要初始化定时器,来反转引脚电平以达到模拟脉冲的目的。
按键中服务函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | /**
* @brief KEY1中断服务函数
* @param 无
* @retval 无
*/
void KEY1_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if(__HAL_GPIO_EXTI_GET_IT(KEY1_INT_GPIO_PIN) != RESET)
{
// LED2 取反
LED2_TOGGLE;
/*改变方向*/
dir_val=(++i % 2) ? CLOCKWISE : ANTI_CLOCKWISE;
MOTOR_DIR(dir_val);
//清除中断标志位
__HAL_GPIO_EXTI_CLEAR_IT(KEY1_INT_GPIO_PIN);
}
}
/**
* @brief KEY2中断服务函数
* @param 无
* @retval 无
*/
void KEY2_IRQHandler(void)
{
//确保是否产生了EXTI Line中断
if(__HAL_GPIO_EXTI_GET_IT(KEY2_INT_GPIO_PIN) != RESET)
{
// LED1 取反
LED1_TOGGLE;
/*改变使能*/
en_val=(++j % 2) ? ON : OFF;
MOTOR_EN(en_val);
//清除中断标志位
__HAL_GPIO_EXTI_CLEAR_IT(KEY2_INT_GPIO_PIN);
}
}
|
这是两个中断服务函数,主要对使能开关和方向的改变,在中断里可以实时的改变步进电机的状态。
主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
DEBUG_USART_Config();
printf("欢迎使用野火 电机开发板 步进电机 IO口模拟控制 例程\r\n");
printf("按下按键1、2可修改旋转方向和使能\r\n");
/*按键中断初始化*/
EXTI_Key_Config();
/*步进电机初始化*/
stepper_Init();
MOTOR_EN(0);
while(1)
{
}
}
|
主函数中只有对系统和外设的初始化,部分代码已在中断函数中实现,则不需要在while里面提及到。
与方式一不同的是,从延时模拟脉冲变成了中断翻转电平增加了脉冲的准确性。
第三种方式:使用PWM比较输出¶
方式二与方式三中的相同的部分,不再重复讲解,这里只讲解不同的部分。
编程要点
- 按键及其中断配置
- 步进电机定时器配置
- 在按键中断中编写按键控制步进电机旋转的代码
宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /*宏定义*/
/*******************************************************/
//Motor 方向
#define MOTOR_DIR_PIN GPIO_PIN_1
#define MOTOR_DIR_GPIO_PORT GPIOE
#define MOTOR_DIR_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 使能
#define MOTOR_EN_PIN GPIO_PIN_0
#define MOTOR_EN_GPIO_PORT GPIOE
#define MOTOR_EN_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 脉冲
#define MOTOR_PUL_IRQn TIM8_CC_IRQn
#define MOTOR_PUL_IRQHandler TIM8_CC_IRQHandler
#define MOTOR_PUL_TIM TIM8
#define MOTOR_PUL_CLK_ENABLE() __TIM8_CLK_ENABLE()
#define MOTOR_PUL_PORT GPIOI
#define MOTOR_PUL_PIN GPIO_PIN_5
#define MOTOR_PUL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOI_CLK_ENABLE()
#define MOTOR_PUL_GPIO_AF GPIO_AF3_TIM8
#define MOTOR_PUL_CHANNEL_x TIM_CHANNEL_1
|
使用宏定义非常方便程序升级、移植。如果使用不同的GPIO,定时器更换对应修改这些宏即可。
PWM输出配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | /*
* 注意:TIM_TimeBaseInitTypeDef结构体里面有5个成员,TIM6和TIM7的寄存器里面只有
* TIM_Prescaler和TIM_Period,所以使用TIM6和TIM7的时候只需初始化这两个成员即可,
* 另外三个成员是通用定时器和高级定时器才有.
*-----------------------------------------------------------------------------
* TIM_Prescaler 都有
* TIM_CounterMode TIMx,x[6,7]没有,其他都有(基本定时器)
* TIM_Period 都有
* TIM_ClockDivision TIMx,x[6,7]没有,其他都有(基本定时器)
* TIM_RepetitionCounter TIMx,x[1,8]才有(高级定时器)
*-----------------------------------------------------------------------------
*/
void TIM_PWMOUTPUT_Config(void)
{
TIM_OC_InitTypeDef TIM_OCInitStructure;
/*使能定时器*/
MOTOR_PUL_CLK_ENABLE();
TIM_TimeBaseStructure.Instance = MOTOR_PUL_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到10000,即为10000次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = TIM_PERIOD;
// 通用控制定时器时钟源TIMxCLK = HCLK/2=84MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=1MHz
TIM_TimeBaseStructure.Init.Prescaler = 84-1;
/*计数方式*/
TIM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP;
/*采样时钟分频*/
TIM_TimeBaseStructure.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
TIM_TimeBaseStructure.Init.RepetitionCounter = 0 ;
/*初始化定时器*/
HAL_TIM_OC_Init(&TIM_TimeBaseStructure);
/*PWM模式配置--这里配置为输出比较模式*/
TIM_OCInitStructure.OCMode = TIM_OCMODE_TOGGLE;
/*比较输出的计数值*/
TIM_OCInitStructure.Pulse = OC_Pulse_num;
/*当定时器计数值小于CCR1_Val时为高电平*/
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
/*设置互补通道输出的极性*/
TIM_OCInitStructure.OCNPolarity = TIM_OCNPOLARITY_LOW;
/*快速模式设置*/
TIM_OCInitStructure.OCFastMode = TIM_OCFAST_DISABLE;
/*空闲电平*/
TIM_OCInitStructure.OCIdleState = TIM_OCIDLESTATE_RESET;
/*互补通道设置*/
TIM_OCInitStructure.OCNIdleState = TIM_OCNIDLESTATE_RESET;
HAL_TIM_OC_ConfigChannel(&TIM_TimeBaseStructure, &TIM_OCInitStructure, MOTOR_PUL_CHANNEL_x);
/* 确定定时器 */
HAL_TIM_Base_Start(&TIM_TimeBaseStructure);
/* 启动比较输出并使能中断 */
HAL_TIM_OC_Start_IT(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x);
/*使能比较通道*/
TIM_CCxChannelCmd(MOTOR_PUL_TIM,MOTOR_PUL_CHANNEL_x,TIM_CCx_ENABLE);
}
|
首先定义两个定时器初始化结构体,定时器模式配置函数主要就是对这两个结构体的成员进行初始化,然后通过相 应的初始化函数把这些参数写入定时器的寄存器中。有关结构体的成员介绍请参考定时器详解章节。
不同的定时器可能对应不同的APB总线,在使能定时器时钟是必须特别注意。通用控制定时器属于APB1, 定时器内部时钟是84MHz。
配置结构体后,则需要调用HAL_TIM_Base_Init初始化定时器并且启用比较输出通道和使能比较通道即可。
在输出比较结构体中,设置输出模式为TOGGLE模式,通道输出高电平有效,设置默认脉宽为OC_Pulse_num, OC_Pulse_num是我们定义的一个全局参数,用来指定占空比大小,实际上脉宽就是设定比较寄存器CCR的值, 用于跟计数器CNT的值比较。然后调用HAL_TIM_PWM_ConfigChannel初始化PWM输出。
最后使用HAL_TIM_PWM_Start函数让计数器开始计数和通道输出。
定时器比较中断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /**
* @brief 定时器比较中断
* @param htim:定时器句柄指针
* @note 无
* @retval 无
*/
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim)
{
uint32_t count;
__IO uint32_t temp_val;
/*获取当前计数*/
count=__HAL_TIM_GET_COUNTER(&TIM_TimeBaseStructure);
/*计算比较数值*/
temp_val = TIM_PERIOD & (count+OC_Pulse_num);
/*设置比较数值*/
__HAL_TIM_SET_COMPARE(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x,temp_val);
}
|
当定时器的比较数值达到后,就会产生中断,进入到这个定时器比较中断,中断中主要用于获取当前的计数值与设定下一次进入中断的时间。
主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/*初始化USART 配置模式为 115200 8-N-1,中断接收*/
DEBUG_USART_Config();
printf("欢迎使用野火 电机开发板 步进电机 PWM控制旋转 例程\r\n");
printf("按下按键1、2可修改旋转方向和使能\r\n");
/*按键中断初始化*/
EXTI_Key_Config();
/*led初始化*/
LED_GPIO_Config();
/*步进电机初始化*/
stepper_Init();
while(1)
{
}
}
|
主函数只做一些初始化外设的配置,具体的脉冲产生已经在定时器中实现了,并且控制步进电机旋转的代码已经在按键中断中实现了。
第四种方式:使用PWM控制匀速旋转¶
与比较输出的PWM相比,普通的PWM模式就有些略显简单了,虽然简单但控制步进电机匀速旋转还是绰绰有余。
与上述有相同的部分,不再重复讲解。
编程要点
(1)按键及其中断配置
(2)步进电机、定时器初始化
(3)在按键中断中编写按键控制步进电机旋转的代码
步进电机定时器初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | /*
* 注意:TIM_TimeBaseInitTypeDef结构体里面有5个成员,TIM6和TIM7的寄存器里面只有
* TIM_Prescaler和TIM_Period,所以使用TIM6和TIM7的时候只需初始化这两个成员即可,
* 另外三个成员是通用定时器和高级定时器才有.
*-----------------------------------------------------------------------------
* TIM_Prescaler 都有
* TIM_CounterMode TIMx,x[6,7]没有,其他都有(基本定时器)
* TIM_Period 都有
* TIM_ClockDivision TIMx,x[6,7]没有,其他都有(基本定时器)
* TIM_RepetitionCounter TIMx,x[1,8]才有(高级定时器)
*-----------------------------------------------------------------------------
*/
TIM_HandleTypeDef TIM_TimeBaseStructure;
static void TIM_PWMOUTPUT_Config(void)
{
TIM_OC_InitTypeDef TIM_OCInitStructure;
int tim_per=50;//定时器周期
/*使能定时器*/
MOTOR_PUL_CLK_ENABLE();
TIM_TimeBaseStructure.Instance = MOTOR_PUL_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到10000,即为10000次,为一个定时周期
TIM_TimeBaseStructure.Init.Period = tim_per;
// 通用控制定时器时钟源TIMxCLK = HCLK/2=84MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=1MHz
TIM_TimeBaseStructure.Init.Prescaler = (84)-1;
/*计数方式*/
TIM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP;
/*采样时钟分频*/
TIM_TimeBaseStructure.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
/*初始化定时器*/
HAL_TIM_Base_Init(&TIM_TimeBaseStructure);
/*PWM模式配置*/
TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1;//配置为PWM模式1
TIM_OCInitStructure.Pulse = tim_per/2;//默认占空比为50%
TIM_OCInitStructure.OCFastMode = TIM_OCFAST_DISABLE;
/*当定时器计数值小于CCR1_Val时为高电平*/
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
/*配置PWM通道*/
HAL_TIM_PWM_ConfigChannel(&TIM_TimeBaseStructure, &TIM_OCInitStructure, MOTOR_PUL_CHANNEL_x);
/*开始输出PWM*/
HAL_TIM_PWM_Start(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x);
}
|
首先对定时器进行初始化,定时器模式配置函数主要就是对这结构体的成员进行初始化,然后通过相 应的初始化函数把这些参数写入定时器的寄存器中。有关结构体的成员介绍请参考定时器详解章节。
由于定时器坐在的APB总线不完全一致,所以说,定时器的时钟是不同的,在使能定时器时钟时必须特别注意, 在这里使用的是定时器2,通用定时器的总线频率为84MHZ,分频参数选择为(84-1),也就是当计数器计数到1M时为一个周期, 计数累计到tim_per时使能的通道就会产生一个脉冲,并且使用向上计数方式。 因为我们使用的是内部时钟,所以外部时钟采样分频成员不需要设置,重复计数器我们没用到,也不需要设置, 然后调用HAL_TIM_PWM_ConfigChannel()来配置所需的定时器通道,并且开始输出PWM。
其它相同的函数不在这详细讲解。
上面虽然说是四种方式去控制步进电机,但其实原理大同小异,最终的目的都是产生脉冲,所谓条条大道通罗马, 也许产生脉冲且控制步进电机的不止这四种,但相信经过上述的方式你一定对步进电机的基础控制了解的足够深刻了。
下载验证¶
- 将电机、驱动连接好;
- 使用野火DAP连接开发板到电脑;
- 给开发板供电,编译下载配套源码,复位开发板。
上电后复位后即可串口打印相应的提示消息。

按照按键提示按key1、key2即可达到相应的旋转效果。
无刷直流电机¶
无刷直流电机(Brushless Direct Current Motor,简称BLDCM)由电动机主体和驱动器组成, 是一种典型的机电一体化产品。 无刷电机是指无电刷和换向器 (或集电环)的电机,又称无换向器电机。这是模型中除了有刷电机以外用的最多的一种电机, 无刷直流电机不使用机械的电刷装置,采用方波自控式永磁同步电机,与有刷电机相比,它将转子和定子交换, 即无刷电机中使用电枢绕组作为定子,使用钕铁硼的永磁材料作为转子,以霍尔传感器取代碳刷换向器, 性能上相较一般的传统直流电机有很大优势。具有高效率、低能耗、低噪音、超长寿命、高可靠性、 可伺服控制、无级变频调速等优点,而缺点则是比有刷的贵、不好维护,广泛应用于航模、高速车模和船模。
不过,单个的无刷电机不是一套完整的动力系统,无刷电机基本必须通过无刷控制器才能实现连续不断的运转。 普通的碳刷电机旋转的是绕组,而无刷电机不论是外转子结构还是内转子结构旋转的都是磁铁。
无刷电机的定子是产生旋转磁场的部分,能够支撑转子进行旋转,主要由硅钢片、漆包线、轴承、 支撑件构成;而转子则是黏贴了钕铁硼磁铁、在定子旋转磁场的作用进行旋转的部件,主要由转轴、 磁铁、支持件构成。除此之外,定子与转子组成的磁极对数还影响着电机的转速与扭力。
直流无刷减速电机几个重要参数¶
直流无刷电机工作原理¶
在学习工作原理前我们先来学习一下安培定则,安培定则,也叫右手螺旋定则,是表示电流和电流激发 磁场的磁感线方向间关系的定则。通电直导线中的安培定则(安培定则一):用右手握住通电直导线, 让大拇指指向电流的方向,那么四指指向就是磁感线的环绕方向;通电螺线管中的安培定则(安培定则二): 用右手握住通电螺线管,让四指指向电流的方向,那么大拇指所指的那一端是通电螺线管的N极,如下图所示。

我们知道在磁极中同名相吸,异名相斥,及N极与S极相互吸引,N极与N极和S极与S极相互排斥, 下面我们来看看一个直流模型,如下图所示。

当两边的线圈通上电后,由右手螺旋定则可知两个线圈中将会产生方向向右的磁场,而中间的转子会尽量使 自己内部的磁感线方向与外磁感线方向保持一致,以形成一个最短闭合磁力线回路,N极与S极相互吸引, 这样内转子就会按顺时针方向旋转了。当转子旋转到如图所示的水平位置时转子将不会受到作用力。

但是由于惯性的作用转子将会继续旋转,当转子旋转至水平位置时,交换两个线圈中的电流方向, 这时转子就会继续向顺时针方向转动了。当转子再次旋转至水平位置时,再次交换两个线圈中的电流方向, 这样转子就可以一直旋转了。
有了上面的基础,我们再来看下面的“三相星形联结的二二导通方式”。

在A端上电源正极,在B端接电源负极,那么可以在线圈A和B中可以产生如图所示的磁场,因为磁场强度是矢量, 所以由磁场BB和BA可以得到合成磁场B。此时转子就会保持在图中方向。

想要转子转动就需要接入不同的电压,我们来分析一下图中的6个过程。
- 在A端接入正电压,B端接入负电压,C端悬空,转子将会旋转至图中1的位置。
- 在1的基础上,C端接入正电压,B端接入负电压,A端悬空,转子将会从1的位置旋转至图中2的位置。
- 在2的基础上,C端接入正电压,A端接入负电压,B端悬空,转子将会从2的位置旋转至图中3的位置。
- 在3的基础上,B端接入正电压,A端接入负电压,C端悬空,转子将会从3的位置旋转至图中4的位置。
- 在4的基础上,B端接入正电压,C端接入负电压,A端悬空,转子将会从4的位置旋转至图中5的位置。
- 在5的基础上,A端接入正电压,C端接入负电压,B端悬空,转子将会从5的位置旋转至图中6的位置。
当转子旋转到位置6时,在重复1的供电状态,转子将会从6的位置旋转到1的位置。 在经过上面的6个过程后转子正好转了一圈,我们将这种驱动方法称为6拍工作方式, 每次电压的变化称为换相。想要电机持续的旋转我们只要按上面转子相应的位置接入相应的电压即可。
直流无刷电机驱动设计与分析¶
控制电路原理设计与分析¶
有了上面的原理分析,我们知道了怎么导通就可以让无刷电机转起来,因为单片机的引脚驱动能力有限, 所以在这里我们使用一个叫做三相六臂全桥驱动电路来驱动无刷电机,原理图如下图所示。

在上图中导通Q1和Q4,其他都不导通,那么电流将从Q1流经U相绕组, 再从V相绕组流到Q4。这样也就完成了上一节中的第一步,同理,依次导通Q5Q4、 Q5Q2、Q3Q2、Q3Q6和Q1Q6, 这也就完成了6拍工作方式。但是,单片机的引脚直接驱动MOS管还是不行的,所以这里需要使用专用的IC来驱动MOS管。
我们再来思考一个问题,在上面的MOS管导通时,是需要知道上一步导通的是哪两个MOS管, 而且第一步中MOS管导通时转子的位置是我们自己规定,但是在实际使用中启动时转子的位置是未知的, 因此,我们并不知道第一步应该导通哪两个MOS管,所以这里我们需要知道转子的位置信息。 但并不需要连续的位置信息,值需要知道换相点的位置即可。 获取转子位置一般有两种方法,一种是使用霍尔传感器,一种是不使用传感器。
霍尔传感器模式¶
霍尔传感器是根据霍尔效应制作的一种磁场传感器。霍尔效应:当电流垂直于外磁场通过半导体时, 载流子发生偏转,垂直于电流和磁场的方向会产生一附加电场,从而在半导体的两端产生电势差, 这一现象就是霍尔效应,这个电势差也被称为霍尔电势差。
在BLDC中一般采用3个开关型霍尔传感器测量转子的位置,由其输出的3位二进制编码去控制三相六臂全桥中的6 个MOS管的导通实现换相。如果将一只霍尔传感器安装在靠近转子的位置,当N极逐渐靠近霍尔传感器即磁感器达到一定值时, 其输出是导通状态;当N极逐渐离开霍尔传感器、磁感应逐渐小时,其输出仍然保持导通状态; 只有磁场转变为S极便达到一定值时,其输出才翻转为截止状态。在S和N交替变化下传感器输出波形占高、 低电平各占50%。如果转子是一对极,则电机旋转一周霍尔传感器输出一个周期的电压波形,如果转子是两对极, 则输出两个周期的波形。
在直流无刷电机中一般把3个霍尔传感器按间隔120度或60度的圆周分布来安装,如果按间隔120度来安装, 则3个霍尔传感器输出波形相差120度电度角,输出信号中高、低电平各占180度电度角。 如果规定输出信号高电平用“1”表示,低电平用“0”表示,则输出的三个信号可以用三位二进制码表示, 如下图所示。

转子每旋转一周可以输出6个不同的信号,这样正好可以满足我们条件。只要我们根据霍尔传感器输出的值来导通MOS管即可。 通常厂家也会给出真值表。假设某厂家给出的真值表如下。
霍尔a | 霍尔b | 霍尔c | A+ | A- | B+ | B- | C+ | C- |
---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 导通 | × | × | 导通 | × | × |
1 | 0 | 0 | × | × | × | 导通 | 导通 | × |
1 | 0 | 1 | × | 导通 | × | × | 导通 | × |
0 | 0 | 1 | × | 导通 | 导通 | × | × | × |
0 | 1 | 1 | × | × | 导通 | × | × | 导通 |
0 | 1 | 0 | 导通 | × | × | × | × | 导通 |
上表的意思是:当检测到的3个霍尔传感器的值,则导通对应值的MOS管。例如,检测到霍尔a、 霍尔b和霍尔c分别为1、1和0,则导通A+和B-对应的MOS管,其他MOS管都要处于截止状态。 当导通对应的MOS管后电机就会旋转一个角度,旋转到下一个霍尔值改变为100,这时在关闭A+和B-, 导通C+和B-,这样电机有将会旋转一个角度直到下一个霍尔值改变, 只要我们按表中的霍尔值导通对应的MOS管电机就可按一定的方向旋转。电机的真值表一个般有两个, 一个是对应顺时针旋转,另一个对应的是逆时针旋转。
在对MOS管的控制有中两个特殊情况需要注意一下:
- 当按真值表中对应霍尔值导通MOS管后,就保持导通状态不变时,此时电机就会旋转到对应位置保持不变, 此时电路中的电能将只能转换为热能,不能转换为机械能,而我们的电机绕组时候的是漆包铜线, 其内阻非常的小,电流就会非常的大,这将会产生大量的热而导致电源或者电机被烧毁。
- 在上面的三相六臂全桥驱动电路原理图中如果同时导通Q1和Q2,或者导通 Q3和Q4,或者导通Q5和Q6,只要导通以上对应的两个MOS管, 都会导致电路中的电机不能正常工作,而MOS管直接将电源的正负极接通,这无疑将会烧毁电源。
以上两个情况是我们电路设计和编程控制需要特别注意的,必须要避免以上情况的发生。
驱动芯片与驱动电机设计与分析¶
根据我们配套驱动器来讲解
直流无刷减速电机控制实现¶
速度控制原理¶
通常我们使用电机不仅仅只是让电机旋转这么简单,更多的时候需要对速度进行控制, 按照以下无刷直流电机转速计算公式可知,影响电机转速的三个参量分别是电枢回路的总电阻Ra, 调整电枢绕组的供电电压Ua或者调整励磁磁通φ。也就是说,想要改变电机的转速, 必须对以上三个参量进行调整。
V=(Ua-IaRa)/CEφ
- Ua——电机定子绕组的实际电压大小
- Ia——电机绕组内通过的实际电流大小
- Ra——电路系统中包含电机的回路电阻大小
- CE——电势系数
- φ——励磁磁通
在现实情况下,在已确定无刷直流电机选型及电机参数的情况下,改变系统总的电阻值Ra和电机的励磁磁通值 难度是比较大的,因此,在一般情况下,我们可以对无刷直流电机的供电电压所处适当调整, 从而降低线圈绕组通过电流大小,以期达到控制电机转速的目的,同前面讲到的直流有刷减速电机一样, 直流无刷电机也可以使用脉宽调制信号(PWM)来进行速度控制,通常使用的PWM频率为十几或者几十千赫兹 (不得超过MOS管的开关频率),这样把需要通电的MOS管使用PWM来控制就可以实现速度的控制。
使用PWM控制直流无刷电机的策略包括PWM-ON、ON-PWM、H_PWM-L_ON、H_ON-L_PWM和H_PWM-L_PWM。 这5种控制策略,均是电机处于120°运行方式下进行的。如下图所示。

这5种调制方式为:
- PWM-ON型。在120°导通区间,各开关管前60°采用PWM调制,后60°则恒通。
- ON-PWM型。在120°导通区间,各开关管前60°恒通,后60°则采用PWM调制。
- H_PWM-L_ON型。在120°导通区间,上桥臂开关管采用PWM调制,下桥臂恒通。
- H_ON-L_PWM型。在120°导通区间,上桥臂开关管恒通,下桥臂采用PWM调制。
- H_PWM-L_PWM型。在120°导通区间,上、下桥臂均采用PWM调制。
那么我们选择那种控制方式更好呢?其实并没有那种方式是最好的,因为的不同的应用场所下各种控制的效果是不同的, 所以在实际应用中我们可以尝试多种方式,然后再选择控制效果最佳的方式。
硬件设计¶
软件设计¶
这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请 参考本章配套的工程。我们创建了四个文件:bsp_motor_tim.c、bsp_motor_tim.h、 bsp_bldcm_control.c和bsp_bldcmr_control.h文件用来存定时器驱动和电机控制程序及相关宏定义
编程要点¶
- 高级定时器 IO 配置
- 定时器时基结构体TIM_HandleTypeDef配置
- 定时器输出比较结构体TIM_OC_InitTypeDef配置
- 根据电机的换相表编写换相中断回调函数
- 根据定时器定义电机控制相关函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | /* 电机控制定时器 */
#define MOTOR_TIM TIM8
#define MOTOR_TIM_CLK_ENABLE() __TIM8_CLK_ENABLE()
extern TIM_HandleTypeDef htimx_bldcm;
/* 累计 TIM_Period个后产生一个更新或者中断
当定时器从0计数到5599,即为5600次,为一个定时周期 */
#define PWM_PERIOD_COUNT (5600)
/* 高级控制定时器时钟源TIMxCLK = HCLK=168MHz
设定定时器频率为=TIMxCLK/(PWM_PRESCALER_COUNT+1)/PWM_PERIOD_COUNT = 15KHz*/
#define PWM_PRESCALER_COUNT (2)
/* TIM8通道1输出引脚 */
#define MOTOR_OCPWM1_PIN GPIO_PIN_5
#define MOTOR_OCPWM1_GPIO_PORT GPIOI
#define MOTOR_OCPWM1_GPIO_CLK_ENABLE() __GPIOI_CLK_ENABLE()
#define MOTOR_OCPWM1_AF GPIO_AF3_TIM8
/* TIM8通道2输出引脚 */
#define MOTOR_OCPWM2_PIN GPIO_PIN_6
#define MOTOR_OCPWM2_GPIO_PORT GPIOI
#define MOTOR_OCPWM2_GPIO_CLK_ENABLE() __GPIOI_CLK_ENABLE()
#define MOTOR_OCPWM2_AF GPIO_AF3_TIM8
/* TIM8通道3输出引脚 */
#define MOTOR_OCPWM3_PIN GPIO_PIN_7
#define MOTOR_OCPWM3_GPIO_PORT GPIOI
#define MOTOR_OCPWM3_GPIO_CLK_ENABLE() __GPIOI_CLK_ENABLE()
#define MOTOR_OCPWM3_AF GPIO_AF3_TIM8
/* TIM8通道1互补输出引脚 */
#define MOTOR_OCNPWM1_PIN GPIO_PIN_13
#define MOTOR_OCNPWM1_GPIO_PORT GPIOH
#define MOTOR_OCNPWM1_GPIO_CLK_ENABLE() __GPIOH_CLK_ENABLE()
#define MOTOR_OCNPWM1_AF GPIO_AF3_TIM8
/* TIM8通道2互补输出引脚 */
#define MOTOR_OCNPWM2_PIN GPIO_PIN_14
#define MOTOR_OCNPWM2_GPIO_PORT GPIOH
#define MOTOR_OCNPWM2_GPIO_CLK_ENABLE() __GPIOH_CLK_ENABLE()
#define MOTOR_OCNPWM2_AF GPIO_AF3_TIM8
/* TIM8通道3互补输出引脚 */
#define MOTOR_OCNPWM3_PIN GPIO_PIN_15
#define MOTOR_OCNPWM3_GPIO_PORT GPIOH
#define MOTOR_OCNPWM3_GPIO_CLK_ENABLE() __GPIOH_CLK_ENABLE()
#define MOTOR_OCNPWM3_AF GPIO_AF3_TIM8
/* 霍尔传感器定时器 */
#define HALL_TIM TIM5
#define HALL_TIM_CLK_ENABLE() __TIM5_CLK_ENABLE()
extern TIM_HandleTypeDef htimx_hall;
/* 累计 TIM_Period个后产生一个更新或者中断
当定时器从0计数到4999,即为5000次,为一个定时周期 */
#define HALL_PERIOD_COUNT (0xFFFF)
/* 高级控制定时器时钟源TIMxCLK = HCLK / 2 = 84MHz
设定定时器频率为 = TIMxCLK / (PWM_PRESCALER_COUNT + 1) / PWM_PERIOD_COUNT = 10.01Hz
周期 T = 100ms */
#define HALL_PRESCALER_COUNT (128-1)
/* TIM5 通道 1 引脚 */
#define HALL_INPUT1_PIN GPIO_PIN_10
#define HALL_INPUT1_GPIO_PORT GPIOH
#define HALL_INPUT1_GPIO_CLK_ENABLE() __GPIOH_CLK_ENABLE()
#define HALL_INPUT1_AF GPIO_AF2_TIM5
/* TIM5 通道 2 引脚 */
#define HALL_INPUT2_PIN GPIO_PIN_11
#define HALL_INPUT2_GPIO_PORT GPIOH
#define HALL_INPUT2_GPIO_CLK_ENABLE() __GPIOH_CLK_ENABLE()
#define HALL_INPUT2_AF GPIO_AF2_TIM5
/* TIM5 通道 3 引脚 */
#define HALL_INPUT3_PIN GPIO_PIN_12
#define HALL_INPUT3_GPIO_PORT GPIOH
#define HALL_INPUT3_GPIO_CLK_ENABLE() __GPIOH_CLK_ENABLE()
#define HALL_INPUT3_AF GPIO_AF2_TIM5
#define HALL_TIM_IRQn TIM5_IRQn
#define HALL_TIM_IRQHandler TIM5_IRQHandler
|
使用宏定义非常方便程序升级、移植。如果使用不同的定时器IO,修改这些宏即可。
定时器复用功能引脚初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | static void TIMx_GPIO_Config(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启定时器相关的GPIO外设时钟*/
MOTOR_OCPWM1_GPIO_CLK_ENABLE();
MOTOR_OCNPWM1_GPIO_CLK_ENABLE();
MOTOR_OCPWM2_GPIO_CLK_ENABLE();
MOTOR_OCNPWM2_GPIO_CLK_ENABLE();
MOTOR_OCPWM3_GPIO_CLK_ENABLE();
MOTOR_OCNPWM3_GPIO_CLK_ENABLE();
/* 定时器功能引脚初始化 */
GPIO_InitStructure.Pull = GPIO_NOPULL;
GPIO_InitStructure.Speed = GPIO_SPEED_HIGH;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出模式
GPIO_InitStructure.Pin = MOTOR_OCNPWM1_PIN;
HAL_GPIO_Init(MOTOR_OCNPWM1_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = MOTOR_OCNPWM2_PIN;
HAL_GPIO_Init(MOTOR_OCNPWM2_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = MOTOR_OCNPWM3_PIN;
HAL_GPIO_Init(MOTOR_OCNPWM3_GPIO_PORT, &GPIO_InitStructure);
/* 通道 2 */
GPIO_InitStructure.Mode = GPIO_MODE_AF_PP;
GPIO_InitStructure.Pin = MOTOR_OCPWM1_PIN;
GPIO_InitStructure.Alternate = MOTOR_OCPWM1_AF;
HAL_GPIO_Init(MOTOR_OCPWM1_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = MOTOR_OCPWM2_PIN;
GPIO_InitStructure.Alternate = MOTOR_OCPWM2_AF;
HAL_GPIO_Init(MOTOR_OCPWM2_GPIO_PORT, &GPIO_InitStructure);
/* 通道 3 */
GPIO_InitStructure.Pin = MOTOR_OCPWM3_PIN;
GPIO_InitStructure.Alternate = MOTOR_OCPWM3_AF;
HAL_GPIO_Init(MOTOR_OCPWM3_GPIO_PORT, &GPIO_InitStructure);
}
|
定时器通道引脚使用之前必须设定相关参数,这选择复用功能,并指定到对应的定时器。 使用GPIO之前都必须开启相应端口时钟。在上面我们将TIM1的CH1、CH2和CH3配置为PWM模式, 我对其对应的互补输出通道配置为推挽输出模式,所以在三相六臂驱动电路中,对于下桥臂是始终开启的, 即这里我们使用的是H_PWM-L_ON调制方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | static void TIM_Mode_Config(void)
{
// 开启TIMx_CLK,x[1,8]
MOTOR_TIM_CLK_ENABLE();
/* 定义定时器的句柄即确定定时器寄存器的基地址*/
htimx_bldcm.Instance = MOTOR_TIM;
/* 累计 TIM_Period个后产生一个更新或者中断*/
//当定时器从0计数到999,即为1000次,为一个定时周期
htimx_bldcm.Init.Period = PWM_PERIOD_COUNT - 1;
// 高级控制定时器时钟源TIMxCLK = HCLK=216MHz
// 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=1MHz
htimx_bldcm.Init.Prescaler = PWM_PRESCALER_COUNT - 1;
// 采样时钟分频
htimx_bldcm.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
// 计数方式
htimx_bldcm.Init.CounterMode=TIM_COUNTERMODE_UP;
// 重复计数器
htimx_bldcm.Init.RepetitionCounter=0;
// 初始化定时器TIMx, x[1,8]
HAL_TIM_PWM_Init(&htimx_bldcm);
/*PWM模式配置*/
//配置为PWM模式1
TIM_OCInitStructure.OCMode = TIM_OCMODE_PWM1;
TIM_OCInitStructure.Pulse = 200;
TIM_OCInitStructure.OCPolarity = TIM_OCPOLARITY_HIGH;
TIM_OCInitStructure.OCNPolarity = TIM_OCNPOLARITY_HIGH;
TIM_OCInitStructure.OCIdleState = TIM_OCIDLESTATE_SET;
TIM_OCInitStructure.OCNIdleState = TIM_OCNIDLESTATE_RESET;
HAL_TIM_PWM_ConfigChannel(&htimx_bldcm,&TIM_OCInitStructure,TIM_CHANNEL_1); // 初始化通道 1 输出 PWM
HAL_TIM_PWM_ConfigChannel(&htimx_bldcm,&TIM_OCInitStructure,TIM_CHANNEL_2); // 初始化通道 2 输出 PWM
HAL_TIM_PWM_ConfigChannel(&htimx_bldcm,&TIM_OCInitStructure,TIM_CHANNEL_3); // 初始化通道 3 输出 PWM
/* 关闭定时器通道1输出PWM */
HAL_TIM_PWM_Stop(&htimx_bldcm,TIM_CHANNEL_1);
/* 关闭定时器通道2输出PWM */
HAL_TIM_PWM_Stop(&htimx_bldcm,TIM_CHANNEL_2);
/* 关闭定时器通道3输出PWM */
HAL_TIM_PWM_Stop(&htimx_bldcm,TIM_CHANNEL_3);
}
|
首先定义两个定时器初始化结构体,定时器模式配置函数主要就是对这两个结构体的成员进行初始化,然后通过相 应的初始化函数把这些参数写入定时器的寄存器中。有关结构体的成员介绍请参考定时器详解章节。
不同的定时器可能对应不同的APB总线,在使能定时器时钟是必须特别注意。高级控制定时器属于APB2, 定时器内部时钟是168MHz。
在时基结构体中我们设置定时器周期参数为PWM_PERIOD_COUNT(5600)-1,时钟预分频器设置为 PWM_PRESCALER_COUNT(2) - 1,频率为:168MHz/PWM_PERIOD_COUNT/PWM_PRESCALER_COUNT=15KHz, 使用向上计数方式。
在输出比较结构体中,设置输出模式为PWM1模式,通道输出高电平有效,设置脉宽为0。
最后使用HAL_TIM_PWM_Stop函数确保计数器不开始计数和通道不输出PWM,这需要我们手动开启,默认不开启。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | static void hall_tim_init(void)
{
TIM_HallSensor_InitTypeDef hall_sensor_onfig;
/* 基本定时器外设时钟使能 */
HALL_TIM_CLK_ENABLE();
/* 定时器基本功能配置 */
htimx_hall.Instance = HALL_TIM;
htimx_hall.Init.Prescaler = HALL_PRESCALER_COUNT - 1; // 预分频
htimx_hall.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
htimx_hall.Init.Period = HALL_PERIOD_COUNT - 1; // 计数周期
htimx_hall.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频
hall_sensor_onfig.IC1Prescaler = TIM_ICPSC_DIV1; // 输入捕获分频
hall_sensor_onfig.IC1Polarity = TIM_ICPOLARITY_BOTHEDGE; // 输入捕获极性
hall_sensor_onfig.IC1Filter = 10; // 输入滤波
hall_sensor_onfig.Commutation_Delay = 0U; // 不使用延迟触发
HAL_TIMEx_HallSensor_Init(&htimx_hall,&hall_sensor_onfig);
HAL_NVIC_SetPriority(HALL_TIM_IRQn, 0, 0); // 设置中断优先级
HAL_NVIC_EnableIRQ(HALL_TIM_IRQn); // 使能中断
}
|
关于霍尔传感器引脚的初始化代码这里不在讲解,具体代码请参考配套工程代码。 高级控制定时器属于APB1,定时器内部时钟是84MHz。 在时基结构体中我们设置定时器周期参数为PWM_PERIOD_COUNT(0xFFFF)-1,时钟预分频器设置为 PWM_PRESCALER_COUNT(128) - 1,频率为:84MHz/PWM_PERIOD_COUNT/PWM_PRESCALER_COUNT≈10Hz, ,计数器的溢出周期为100毫秒,这个时间要设置到电机正常旋转时足够一路霍尔传感器产生变化, 这样能方便后续速度控制时的计时功能,使用向上计数方式。因为任何一相霍尔传感器发生变化都需要换相, 所以输入捕获极性设置为双边沿触发。借助 TIMx_CR2 寄存器中的 TI1S 位, 可将通道1的输入滤波器连接到异或门的输出,从而将CH1、CH2和CH3这三个输入引脚组合在一起,如下图所示。 因此霍尔传感器必须使用定时器的CH1、CH2和CH3这3个通道,这样只要任意一相霍尔传感器状态发生变化都可以触发中断进行换相。 配置定时器的中断优先级,并使能全局定时器中断。

1 2 3 4 5 6 7 8 9 10 11 12 | void hall_enable(void)
{
/* 使能霍尔传感器接口 */
__HAL_TIM_ENABLE_IT(&htimx_hall, TIM_IT_TRIGGER);
__HAL_TIM_ENABLE_IT(&htimx_hall, TIM_IT_UPDATE);
HAL_TIMEx_HallSensor_Start(&htimx_hall);
LED1_OFF;
HAL_TIM_TriggerCallback(&htimx_hall); // 执行一次换相
}
|
开启触发中断,开启更新中断,启动霍尔传感器,关闭LED1,LED1将用于电机堵转超时的指示灯, 所以在开启电机前好确保该指示灯是灭的。最后执行了一次换相,在HAL_TIM_TriggerCallback这个函数里面执行一次换相, 这是因为需要根据当前霍尔传感器的位置让电机旋转到下一个位置,同时时霍尔传感器状态也发生了变化, 这时才会到HAL_TIM_TriggerCallback中断回调函数里面执行换相,否则电机将有可能不能正常启动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | void HAL_TIM_TriggerCallback(TIM_HandleTypeDef *htim)
{
/* 获取霍尔传感器引脚状态,作为换相的依据 */
uint8_t step = 0;
step = get_hall_state();
if(get_bldcm_direction() == MOTOR_FWD)
{
step = 7 - step; // 根据顺序表的规律可知: CW = 7 - CCW;
}
switch(step)
{
case 1://W+ U-
/* Channe2 configuration */
HAL_TIM_PWM_Stop(&htimx_bldcm, TIM_CHANNEL_2); // 停止上桥臂 PWM 输出
HAL_GPIO_WritePin(MOTOR_OCNPWM2_GPIO_PORT, MOTOR_OCNPWM2_PIN, GPIO_PIN_RESET); // 关闭下桥臂
/* Channe3 configuration */
HAL_TIM_PWM_Start(&htimx_bldcm, TIM_CHANNEL_3); // 开始上桥臂 PWM 输出
HAL_GPIO_WritePin(MOTOR_OCNPWM1_GPIO_PORT, MOTOR_OCNPWM1_PIN, GPIO_PIN_SET); // 开启下桥臂
break;
case 2: //U+ V-
/* Channe3 configuration */
HAL_TIM_PWM_Stop(&htimx_bldcm, TIM_CHANNEL_3);
HAL_GPIO_WritePin(MOTOR_OCNPWM3_GPIO_PORT, MOTOR_OCNPWM3_PIN, GPIO_PIN_RESET);
/* Channel configuration */
HAL_TIM_PWM_Start(&htimx_bldcm, TIM_CHANNEL_1);
HAL_GPIO_WritePin(MOTOR_OCNPWM2_GPIO_PORT, MOTOR_OCNPWM2_PIN, GPIO_PIN_SET);
break;
case 3:// W+ V-
/* Channel configuration */
HAL_TIM_PWM_Stop(&htimx_bldcm, TIM_CHANNEL_1);
HAL_GPIO_WritePin(MOTOR_OCNPWM1_GPIO_PORT, MOTOR_OCNPWM1_PIN, GPIO_PIN_RESET);
/* Channe3 configuration */
HAL_TIM_PWM_Start(&htimx_bldcm, TIM_CHANNEL_3);
HAL_GPIO_WritePin(MOTOR_OCNPWM2_GPIO_PORT, MOTOR_OCNPWM2_PIN, GPIO_PIN_SET);
break;
case 4:// V+ W-
/* Channel configuration */
HAL_TIM_PWM_Stop(&htimx_bldcm, TIM_CHANNEL_1);
HAL_GPIO_WritePin(MOTOR_OCNPWM1_GPIO_PORT, MOTOR_OCNPWM1_PIN, GPIO_PIN_RESET);
/* Channe2 configuration */
HAL_TIM_PWM_Start(&htimx_bldcm, TIM_CHANNEL_2);
HAL_GPIO_WritePin(MOTOR_OCNPWM3_GPIO_PORT, MOTOR_OCNPWM3_PIN, GPIO_PIN_SET);
break;
case 5: // V+ U-
/* Channe3 configuration */
HAL_TIM_PWM_Stop(&htimx_bldcm, TIM_CHANNEL_3);
HAL_GPIO_WritePin(MOTOR_OCNPWM3_GPIO_PORT, MOTOR_OCNPWM3_PIN, GPIO_PIN_RESET);
/* Channe2 configuration */
HAL_TIM_PWM_Start(&htimx_bldcm, TIM_CHANNEL_2);
HAL_GPIO_WritePin(MOTOR_OCNPWM1_GPIO_PORT, MOTOR_OCNPWM1_PIN, GPIO_PIN_SET);
break;
case 6: // U+ W-
/* Channe2 configuration */
HAL_TIM_PWM_Stop(&htimx_bldcm, TIM_CHANNEL_2);
HAL_GPIO_WritePin(MOTOR_OCNPWM2_GPIO_PORT, MOTOR_OCNPWM2_PIN, GPIO_PIN_RESET);
/* Channel configuration */
HAL_TIM_PWM_Start(&htimx_bldcm, TIM_CHANNEL_1);
HAL_GPIO_WritePin(MOTOR_OCNPWM3_GPIO_PORT, MOTOR_OCNPWM3_PIN, GPIO_PIN_SET);
break;
}
update = 0;
}
|
获取霍尔传感器引脚状态,根据厂家给出的真值表进行换相。将上桥臂采用PWM输出,下桥臂直接输出高电平。 即为H_PWM-L_ON模式。将变量update设置为0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (update++ > 1) // 有一次在产生更新中断前霍尔传感器没有捕获到值
{
printf("堵转超时\r\n");
update = 0;
LED1_ON; // 点亮LED1表示堵转超时停止
/* 堵转超时停止 PWM 输出 */
hall_disable(); // 禁用霍尔传感器接口
stop_pwm_output(); // 停止 PWM 输出
}
}
|
因为霍尔传感器没变化一次都会进HAL_TIM_TriggerCallback函数将update设置为0,并且会产生更新中断进入 HAL_TIM_PeriodElapsedCallback函数将update加一,那么如果update大于1就说明没有进入HAL_TIM_TriggerCallback 函数,直接进入HAL_TIM_PeriodElapsedCallback函数,这就说明电机是堵转了,并且已经至少堵转了100毫秒, 这里我们认为堵转超时停止PWM的输出和禁用霍尔传感器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | int main(void)
{
__IO uint16_t ChannelPulse = 200;
uint8_t i = 0;
/* 此处省略各种初始化函数 */
/* 电机初始化 */
bldcm_init();
while(1)
{
/* 扫描KEY1 */
if( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON )
{
/* 使能电机 */
set_bldcm_speed(ChannelPulse);
set_bldcm_enable();
}
/* 扫描KEY2 */
if( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON )
{
/* 增大占空比 */
ChannelPulse+=50;
if(ChannelPulse>PWM_PERIOD_COUNT)
ChannelPulse=PWM_PERIOD_COUNT;
set_bldcm_speed(ChannelPulse);
}
/* 扫描KEY3 */
if( Key_Scan(KEY3_GPIO_PORT,KEY3_PIN) == KEY_ON )
{
if(ChannelPulse<50)
ChannelPulse=0;
else
ChannelPulse-=50;
set_bldcm_speed(ChannelPulse);
}
/* 扫描KEY4 */
if( Key_Scan(KEY4_GPIO_PORT,KEY4_PIN) == KEY_ON )
{
/* 转换方向 */
set_bldcm_direction( (++i % 2) ? MOTOR_FWD : MOTOR_REV);
}
/* 扫描KEY4 */
if( Key_Scan(KEY5_GPIO_PORT,KEY5_PIN) == KEY_ON )
{
/* 停止电机 */
set_bldcm_disable();
}
}
}
|
在main函数中首先初始化了各种外设,在死循环中检测按键的变化,按KEY1可以启动电机;按KEY2可以增大PWM占空比,增加电机旋转速度; 按KEY3可以减小PWM占空比,减小电机旋转速度;按KEY4可以使电机旋转方向改变;按KEY4可以停止电机旋转;
下载验证¶
按照要求电机和控制板连接好,可以按下KEY1、2、3、4、5对电机进行控制,当PWM减小到一定值时,电机会停止旋转, 当堵转超时后LED1会亮起,并且停止PWM的输出,关闭电机防止长时间的大电流烧毁电机。
在确定PWM输出正确后我们就可以接上电机进行验证我们的程序了,实物连接如下图所示。

编码器详解¶
编码器介绍¶
编码器,是一种用来测量机械旋转或位移的传感器。这种传感器能够测量机械部件在旋转或直线运动时的位移位置或速度等信息, 并将其转换成一系列电信号。编码器是工业中常用的传感器之一,广泛应用于工业生产当中需要对机械系统进行监视或控制的场景, 包括工业控制、机器人、照相机镜头、雷达平台以及部分计算机输入设备例如轨迹球和鼠标滚轮等等。
编码器可以根据不同的方式分出很多种类型。例如根据检测原理,可分为光学式、磁式、感应式和电容式。 根据内部机械结构的运动方式,可分为线性编码器和旋转编码器。根据其刻度实现方法及信号输出形式, 又可分为增量式、绝对式以及混合式三种。编码器种类繁多,本章主要讲解旋转编码器,如下图所示,外形很像一个电机。

图7-1 旋转编码器
增量式编码器¶
增量式旋转编码器是将设备运动时的位移信息变成连续的脉冲信号,脉冲个数表示位移量的大小。只有当设备运动的时候增量式编码器才会输出信号。 编码器一般会把这些信号分为通道A和通道B两组输出,并且这两组信号间有90°的相位差。同时采集这两组信号就可以知道设备的运动和方向。 除了通道A、通道B以外,很多增量式编码器还会设置一个额外的通道Z输出信号,用来表示编码器特定的参考位置,传感器转一圈Z轴信号才会输出一个脉冲。 增量式编码器只输出设备的位置变化和运动方向,不会输出设备的绝对位置。
绝对式编码器¶
绝对式旋转编码器是将设备运动时的位移信息通过二进制编码的方式变成数字量直接输出。 这种编码器与增量式编码器的区别主要在内部的码盘。绝对式编码器的码盘利用若干透光和不透光的线槽组成一套二进制编码, 这些二进制码与编码器转轴的每一个不同角度是唯一对应的,读取这些二进制码就能知道设备的绝对位置,所以叫它绝对式编码器。 绝对式编码器一般常用自然二进制、格雷码或者BCD码等编码方式。
混合式绝对式编码器¶
混合式绝对式编码器,它输出两组信息:一组信息用于检测磁极位置,带有绝对信息功能;另一组则和增量式编码器的输出信息完全相同。
旋转编码器原理¶
旋转编码器的原理示意图如下图所示。旋转编码器内部大都由码盘、光电检测装置和信号处理电路等部分构成。码盘上刻了若干圈线槽, 线槽等距并且可透光,当码盘旋转时就会周期性的透过和遮挡来自光电检测装置的光线,这样检测装置就会周期性的生成若干电信号。 但是这些电信号通常比较微弱,需要加入一套处理电路对信号进行放大和整形,最后把信号整形为脉冲信号并向外输出。

图7-2 编码器原理示意图
虽然旋转编码器的原理在总体上差不多,但是对于这些原理的具体实现方法却有很大不同。
增量式编码器原理¶
首先来看增量式编码器。上节提到过,增量式编码器都有A、B两通道信号输出,这是因为增量式编码器的码盘上有两圈线槽, 两圈线槽的之间会错开一定的角度,这个角度会使得光电检测装置输出的两相信号相差 1/4 周期(90°)。码盘的具体工作方式如下图所示。 图中黑色代表透光,白色代表遮光。当码盘转动时,内圈和外圈的线槽会依次透过光线,光电检测装置检测到光线通断的变化, 就会相应的输出脉冲信号,因为内外圈遮光和透光时候存在时间差,所以也就有了A、B两通道信号的相位差。

图7-3 增量式编码器码盘运作方式1

图7-4 增量式编码器码盘运作方式2

图7-5 增量式编码器码盘运作方式3
根据两相信号变化的先后顺序就可以判断运动方向,记录输出的脉冲个数可以知道位移量的大小,同时通过输出信号的频率就能得到速度。
一些增量式编码器上会有4圈线槽,分别对应A、B、-A、-B四相信号,相邻两相信号间也是差1/4周期,只不过这种编码器会把-A和-B两相信号反相, 然后叠加到A、通道B,用来增强信号。除了通道A、通道B以外,很多增量式编码器还会设置一个额外的通道Z输出信号。通道Z信号也在码盘上有对应的线槽, 不过只有一条,码盘转一圈才会经过一次。通道Z信号一般用做参考零位,指示设备位置或者清除积累量。
另一种较为常用的增量式编码器是霍尔编码器。霍尔增量式编码器在结构上和光电式几乎相同,只不过检测原理变成了霍尔效应。 内部元件也稍有不同,霍尔编码器的码盘上不是线槽,而是不同的磁极,或者有些直接把电机的旋转磁场当作码盘, 然后检测装置换成了霍尔传感器。输出和光电式相同,仍然是相位差1/4周期的A、B两通道信号。
增量式编码器计数起点任意设定,可实现多圈无限累加和测量。需要提高分辨率时,可触发A、B两通道信号的上升沿和下降沿对原脉冲数进行倍频。 但是当接收设备停机重启后,增量式编码器需要重新寻找参考零点。
绝对式编码器原理¶
接着是绝对式编码器。绝对式编码器在总体结构上与增量式比较类似,都是由码盘、检测装置和放大整形电路构成,但是具体的码盘结构和输出信号含义不同。 绝对式编码器的码盘上有很多圈线槽,被称为码道,每一条码道内部线槽数量和长度都不同。它们共同组成一套二进制编码, 一条码道对应二进制数的其中一个位,通常是码盘最外侧的码道表示最低位,最内侧的码道表示最高位。码道的数量决定了二进制编码的位数, 一个绝对式编码器有 N 条码道,它就能输出 N 位二进制数,且输出二进制数的总个数是 2N 个。 这些二进制数与转轴的机械位置是固定的,和编码器外部因素无关,所以叫做绝对式编码器。在接收设备断电重启后绝对式编码器无需寻找参考零点。
下图是一个简化版的绝对式编码器码盘,其中白色块透光表示0,黑色块不透光表示1。码盘上的二进制数逆时针依次增大。
.png)
图7-6 绝对式编码器码盘(自然二进制)
图中码盘有3条码道,一共可表示23=8个二进制数,所以整个码盘被分成了8个扇区,每个扇区表示一个3位二进制数, 每个二进制数对应一个转轴的位置信息。码盘采用自然二进制编码,自然二进制编码的优点是很方便直观,但是受编码器制造和安装精度的影响, 实际应用中二进制数的每一位不可能同时改变,或者出现码盘停在两个扇区中间,这些情况都很容易造成读数错误。
为了避免出现读数错误,可以使用格雷码来解决。下图是一个使用格雷码的码盘,同样的,白色块透光表示0,黑色块不透光表示1。码盘上的二进制数逆时针依次增大。
.png)
图7-7 绝对式编码器码盘(格雷码)
图中码盘的码道数与上面的自然二进制码盘完全一致,也能表示8个3位二进制数,只不过将编码方式换成了格雷码。 利用任意相邻的二进制格雷码数都只有一位不同的特性,采用这种编码的码盘在一定程度上克服了自然二进制码盘容易产生读数错误的问题。
绝对式编码器还分为单圈绝对式编码器和多圈绝对式编码器,上面举的两个例子都是针对单圈也就是360°以内的情况,当码盘转动超过360°, 输出的编码会重复,这样不符合绝对式编码器数据唯一的要求,所以就出现了多圈绝对式编码器。多圈绝对式编码器的量程可以超过360°,并且通常超出很多, 其内部结构也比单圈的复杂,但是基本原理都是一样的。
编码器基本参数¶
- 分辨率:指编码器能够分辨的最小单位。对于增量式编码器,其分辨率表示为编码器转轴旋转一圈所产生的脉冲数, 即脉冲数/转(Pulse Per Rotation或PPR)。码盘上透光线槽的数目其实就等于分辨率,也叫多少线,较为常见的有5-6000线。 对于绝对式编码器,内部码盘所用的位数就是它的分辨率,单位是位(bit),具体还分单圈分辨率和多圈分辨率。
- 精度:首先明确一点,精度与分辨率是两个不同的概念。精度是指编码器每个读数与转轴实际位置间的最大误差,通常用角度、角分和角秒来表示。 例如有些绝对式编码器参数表里会写±20′′,这个就表示编码器输出的读数与转轴实际位置之间存在20角秒的误差,精度由码盘刻线加工精度、 转轴同心度、材料的温度特性、电路的响应时间等各方面因素共同决定。
- 最大响应频率:指编码器每秒输出的脉冲数,单位是Hz。计算公式:最大响应频率 = 分辨率 * 轴转速/60。
- 信号输出形式:对于增量式编码器,每个通道的信号独立输出,输出电路形式通常有集电极开路输出、推挽输出、差分输出等。 对于绝对式编码器,由于是直接输出几十位的二进制数,为了确保传输速率和信号质量,一般采用串行输出或总线型输出, 例如同步串行接口(SSI)、RS485、CANopen或EtherCAT等,也有一部分是并行输出,输出电路形式与增量式编码器相同。
驱动器的分类¶
有刷电机驱动器¶
直流有刷电机的驱动方法在之前已经详细的讲解过,这里就不再赘述了。其实本质上是使用H桥电路进行驱动,核心电路H桥加上一些必要的外围电路, 共同组成直流有刷电机的驱动器。H桥本身可作为集成电路使用,也可由分立元件构成。集成电路形式的H桥一般用于中小功率需求的应用, 或者是对电路面积有要求的场合。分立元件形式的H桥通常用于大功率或者超大功率需求的应用,主要由MOSFET或IGBT晶体管组成。 不过MCU的引脚是无法直接驱动MOS管等元件的,需要加上专用的MOS管驱动芯片。下图是一款经典的直流有刷电机驱动芯片L298N,其内部集成了两个H桥。

图8-1 经典直流有刷电机驱动芯片L298N
无刷电机驱动器¶
无刷电机也是使用H桥电路进行驱动的,只不过是电机的每一相都用一个半桥电路驱动,一个三相无刷电机总共需要三个半桥,而不像直流有刷电机驱动那种使用全桥电路。 跟直流有刷电机电机一样,无刷电机驱动器也分集成电路形式和分立元件形式,但因为无刷电机需要换相操作,就算是分立元件形式也只是把半桥电路给独立了出来。 类似于下图这样的,就是一款无刷电机驱动器。

图8-2 直流无刷电机驱动器
步进电机驱动器¶
步进电机不能直接接到直流或交流电源上工作,必须接入专用的驱动器才能正常使用。控制器将步进脉冲和方向信号发送到步进电机驱动器, 驱动器将控制器发来的步进脉冲信号转换为激励步进电动机旋转所需的功率信号。步进电机驱动器通常都带有细分功能,可以对步距角和电流进行细分, 从而实现更请准的控制和更低的噪声震动。

图8-3 步进电机驱动器
伺服电机驱动器¶
伺服电机驱动器(servo drives),是一种用来驱动和控制伺服电机的控制器,属于伺服系统的一部分。伺服电机驱动器接收和放大来自控制系统的命令信号, 并将电流传输给伺服电机,以产生与命令信号成比例的运动。这些命令信号通常对伺服电机的位置、速度和力矩等参数进行控制,实现高精度的传动系统定位。 附在伺服电机上的传感器将电机的实际状态反馈给伺服驱动器,驱动器将实际电机状态与来自控制系统的命令状态进行比较。然后驱动器改变传给电机的电压、 频率或脉冲宽度,以纠正任何偏离命令的状态。下图是一款伺服电器驱动器,在实际应用中通常把伺服电机和驱动器作为一个整体使用。

图8-4 伺服电机驱动器和伺服电机
控制系统与电机的关系¶
什么是控制系统?¶
控制系统是指由控制主体、控制客体和控制媒体组成的具有自身目标和功能的管理系统。也可以理解为:为了使控制对象达到预期的稳定状态。 例如一个水箱的温度控制,可以通过控制加热设备输出的功率进而来改变水温达到目标温度,这个水箱的温度控制可以称之为一个简单的控制系统。
自动控制系统的工作原理是什么?¶
原理:对生产中某些关键性参数进行自动控制,使它们在受到外界干扰(扰动) 的影响而偏离正常状态时,能够被自动地调节而回到工艺所要求的数值范围内。 自动控制系统分为开环和闭环,具体为:
闭环自动控制系统原理:¶
闭环控制也就是(负)反馈控制,原理与人和动物的目的性行为相似,系统组成包括传感器(相当于感官),控制装置(相当于脑和神经), 执行机构(相当于手腿和肌肉)。传感器检测被控对象的状态信息(输出量),并将其转变成物理(电)信号传给控制装置。 控制装置比较被控对象当前状态(输出量)对希望状态(给定量)的偏差,产生一个控制信号,通过执行机构驱动被控对象运动, 使其运动状态接近希望状态。具体可见一下框图:

闭环控制系统框图
以热水器自动控温为例,首先我们将热水器设置一个温度,此时这个温度就是这个系统的给定值,也是控制系统想要达到的目标值; 热水器的调节器为整个系统的控制器,调节器经过计算处理输出给执行机构,通过热水器的加热装置,进而控制水的温度, 然后检测实际的温度,将温度反馈给控制系统,此时实际的温度和目标温度产生偏差,通过加热装置给水加热来达到理想的温度。闭环控制框图如下:

热水器闭环控制系统框图
开环自动控制系统原理:¶
开环控制不能够检测误差,不能够校正误差,只能够按照事先确定好的程序和产生信号的条件,依次去控制对象并且无抑制的干扰能力。

开环控制系统框图
以生活中的电吹风为例,电吹风是一个常见开环控制系统,通过设置吹风机的档位可以改变风扇的转速和电热丝的温度,进而调节输出的温度和风速。 电吹风的开环控制框图如下:

电吹风开环控制系统框图
控制系统与电机有什么关系?¶
上一小节讲解了开环与闭环控制系统,并给出了系统框图和实际的应用举例;在控制系统中有一个角色十分的重要, 它根据控制装置的信号改变着被控对象的状态——执行器。执行器的种类多种多样,可以是改变温度的电热丝、改变转速的电机等等, 只要是符合可以改变被控对象的状态都可以称之为执行器。
就目前来说电机加上相应的传输结构占执行器的一大部分,所以说控制电机就变成了十分重要的内容。 电机控制的好坏直接决定于被控对象状态改变的是否准确。电机的控制是对电机的启动、加速、运转、减速以及停止进行控制, 通过对其控制达到快速启动、快速响应、高效率、高扭矩输出和高过载能力的目的。
具体的电机应该如何应用,将在应用章节集中讲解。
PID算法的通俗解说¶
为什么使用PID?¶
PID算法是控制领域非常常见的算法,小到控制温度,大到控制飞机的飞行姿态和速度等等,都会涉及到PID控制, 在控制领域可以算是万能的算法,如果你能够掌握PID算法的控制与实现,那么已经足以应对控制领域的一般问题了。 并且在众多控制算法中PID是最能体现反馈思想的算法;可以算上是经典之作,那么如此好用的算法是不是很复杂呢? 并不是,经典不等同于复杂,往往经典的东西是都是简单的。所以放心学习就好了!
以小车速度为例,你一定会发现这样一个问题,当你刚把充满的12V电池装在小车上时,然后在程序上给了一个固定50%的占空比, 此时小车跑的很快动力很足,但是跑着跑着就慢了下来,因为电池电压的影响小车速度变慢了,在刚充满的时候12V电池50%的占空比 相当于直接作用在电机两端的电压是12V x 50% = 6V ,当使用一段时间后电池的电压变为9V,虽然程序占空比没有变,但是由于电池电压降低了, 所以作用在电机两端的电压也就变了,所以小车变慢了。那么怎么才能够使小车按照恒定速度行驶呢?其思想就是当小车速度慢了,就增加占空比。 那么速度慢多少开始增加占空比呢?怎么增加?增加多少呢?
此时,PID算法就是一个非常好的选择,对于增加多少的问题,一定要通过PID算法,因为速度和占空比到底是个什么关系,谁也不知道。 但是此时使用PID算法,通过编码器的速度反馈,可以实时的知道小车的速度是否慢了,然后利用目标速度与实际速度的误差带入算法, 即可获得当前占空比,达到控制速度的效果。
PID算法介绍¶
PID是Proportional(比例)、Integral(积分)、Differential(微分)的首字母缩写; 是一种结合比例、积分和微分三种环节于一体的闭环控制算法,它是目前为止在连续控制系统中计数最为成熟的一种控制算法; 在工业控制、机器人、无人机、机械臂和平衡车等领域有着极为重要的作用;该控制算法出现于20世纪30至40年代,至今为止经久不衰, 适用于对被控对象模型了解不清楚的场合。实际运行的经验和理论的分析都表明,运用这种控制规律对许多工业过程进行控制时, 都能得到比较满意的效果。PID控制的实质是对目标值和实际值误差进行比例、积分、微分运算后的结果用来作用在输出上。
连续控制的理想PID控制规律:

- Kp——比例增益,Kp与比例度成倒数关系
- Tt——积分时间常数
- TD——微分时间常数
- u(t)——PID控制器的输出信号
- e(t)——给定值r(t)与测量值误差
比例(P)
比例控制是最简单的一种控制方式,成比例的反应控制系统中输入与输出的偏差信号,只要偏差一旦产生,就立即产生控制的作用来减小产生的误差。 比例控制器的输出与输入成正比关系,能够迅速的反应偏差,偏差减小的速度取决于比例系数Kp,Kp越大偏差减小的就越快,但是极易引起震荡; Kp减小发生震荡的可能性减小,但是调节的速度变慢,单纯的比例控制存在不能消除的静态误差,这里就需要积分来控制。
积分(I)
在比例控制环节产生了静态误差,在积分环节中,主要用于就是消除静态误差提高系统的无差度。积分作用的强弱,取决于积分时间常数Ti, Ti越大积分作用越弱,反之则越强。积分控制作用的存在与偏差e(t)的存在时间有关,只要系统存在着偏差,积分环节就会不断起作用,对输入偏差进行积分, 使控制器的输出及执行器的开度不断变化,产生控制作用以减小偏差。在积分时间足够的情况下,可以完全消除静差,这时积分控制作用将维持不变。 Ti越小,积分速度越快,积分作用越强。积分作用太强会使系统超调加大,甚至使系统出现振荡。
微分(D)
微分环节的作用是反应系统偏差的一个变化趋势,也可以说是变化率,可以在误差来临之前提前引入一个有效的修正信号, 有利于提高输出响应的快速性,减小被控量的超调和增加系统的稳定性,虽然积分环节可以消除静态误差但是降低了系统的响应速度, 所以引入微分控制器就显得很有必要,尤其是具有较大惯性的被控对象使用PI控制器很难得到很好的动态调节品质,系统会产生较大的超调和振荡, 这时可以引入微分作用。在偏差刚出现或变化的瞬间,不仅根据偏差量作出及时反应(即比例控制作用), 还可以根据偏差量的变化趋势(速度)提前给出较大的控制作用(即微分控制作用),将偏差消灭在萌芽状态, 这样可以大大减小系统的动态偏差和调节时问,使系统的动态调节品质得以改善。微分环节有助于系统减小超调,克服振荡, 加快系统的响应速度,减小调节时间,从而改善了系统的动态性能,但微分时间常数过大,会使系统出现不稳定。 微分控制作用一个很大的缺陷是容易引入高频噪声,所有在干扰信号比较严重的流量控制系统中不宜引入微分控制作用。
举例分析
(1)假设一个水箱注水的实例,水位高度可以实时观测,并且要从空箱开始注入水达到某个位置,然而你可以控制的就是注水龙头的的开关大小。
那么想要将水住满只需要观察水位的实际情况与目标位置的距离,如果距离较大的话那就将水龙头开大点,如果距离较小的话就将水龙头开的小点; 随着距离越来越小,直到关闭水龙头,就可以达到将水注满的目的,对于这个简单的系统来说,只需要 比例调节 即可;

Kp代表水龙头放水的粗细,水龙头越粗调节的就越快,也就是增大比例系数可以加快系统的响应。

(2)在空箱注水的基础上,这时不仅仅需要注水,而且还需要给用户持续供水,那么在原来的数学模型上就需要添加一个常数项,具体如下:

这时候如果控制器只有一个比例环节进行调节的话,当系统处于稳态时,也就是放水的速度与注入水的速度即 dx = 0 时,可以推导出 e 的关系式,e 在系统稳定时不为0,液位的高度就一直会有差那么一点点,这就是系统的 静态误差。
当 c 是固定常数时,kp 越大就会使得 e 越小,即 在有静差的情况下,增大比例系数有助于较小静差;
具体如下图:

但是如果客户用水量不是一个常数,那么静态误差就不是很好控制了,所以需要增加注水的动态性,那么怎么增加其动态性呢?只有比例控制 是不可能了,这是需要增加积分调节。

此时相当于多加了一个水龙头,这个水龙头的使用规则是,当水位低于目标高度时就将其一直往大拧,当水位高于目标高度时就一直往小了拧, 如果用户用水的速度不变,那么数次之后便可以消除系统的静差;这就是积分环节可以消除系统的静差。
增加了积分调节环节,那么还有一个重要的参数,积分时间 ;具体看下图:

从上式可以看出,积分时间越大会导致积分环节调节的就越小,相当于降低了积分调节的敏感度,在实例中可以理解为将水龙头的水管换成细水管了。 在没有到达预定的高度之前,第二个龙头会按照最大量向水箱内注入水,当达到预期的高度时,水龙头正好是拧到最大的输出,自然而然就会出现注水注多的现象, 所以多出来的这部分就是 超调;所以说第二个水龙头越粗,他达到预期高度也就越快,但是波折也就越多。具体如下图:

所以可以得出结论:增大积分时间有利于减小超调,是系统稳定性增加,但是会增长消除静差的时间;
(3)如果用户用水量不是一个常数,是一个会变化的量,那么此时第二个水龙头根据预期高度来控制的话就显得有些滞后了,因为你很难知道下一个时间段 用户用多少水量;此时仅仅使用比例调节和积分调节就不是那么奏效了,所以我们需要引入微分调节。相当于在水箱又加了一个水龙头和一个可以漏水可控的阀门; 现在就需要观察水位的变化快慢,根据水位的快慢来决定 放水/注水 的速度;根据水位实际高度与预期高度差值的变化率来反应阀门的状态来达到更好的 控制水位的效果。效果如下图:

PID算法的离散化¶
公式推导
先看一下PID算法的一般形式:

PID框图
通过以上框图不难看出,PID控制其实就是对偏差的控制过程;如果偏差为0,则比例环节不起作用,只有存在偏差时,比例环节才起作用; 积分环节主要是用来消除静差,所谓静差,就是系统稳定后输出值和设定值之间的差值,积分环节实际上就是偏差累计的过程, 把累计的误差加到原有系统上以抵消系统造成的静差;而微分信号则反应了偏差信号的变化规律,也可以说是变化趋势,根据偏差信号的变化趋势来进行超前调节, 从而增加了系统的预知性;
接下来对上述PID系统进行离散化,离散化后方便在程序上进行数字处理,把连续状态的公式整理得:

- 假设采集数据的间隔时间为T,则在第 k T 时刻有:
- 误差等于第k个周期时刻的误差等于输入(目标)值减输出(实际)值,则有: err(k)=rin(k)-rout(k)
- 积分环节为所有时刻的误差和,则有: err(k)+err(k+1)+err(k+2)+...
- 微分环节为第k时刻误差的变化率,则有:[err(k)-err(k-1)]/T
从而获得如下PID离散形式:

则u(k)可表示为:

到此为止,PID的基本离散表达形式就推导出来了,有点经验人一定会有疑问,PID的公式不应该P*A(x)+I*B(x)+D*C(x)的形式么? 不错,以上的形式是没有化简的形式,接着推导则有:

其中:
- k为采样的序号
- err(k)为第k次的误差
- u(k)为输出量
- Kp不变
- Ki=Kp*T/Ti
- Kd=Kp*Td/T
这样就相对方便记忆了;目前这种表达形式为 位置式 ,也叫作全量式PID。
接下来只需两步即可推导出 增量式PID:
第一步,将 k-1 带入到 k 得:

第二步,由△u=u(k)-u(k-1)得:

到此 增量式PID 表达方式就推导完了,从公式可以看出 增量式PID 的输出与近三次的偏差有很大关系; 需要注意的是我们推导的是对于上一次来说的调节量,也就是说当前的输出等于上一次加增加的调节量, 公式如下:

对比区别
- 增量式算法 不需要对积分项累加,控制量增量只与近几次的误差有关,计算误差对控制量计算的影响较小。 而 位置式算法 要对近几次的偏差的进行积分累加,容易产生较大的累加误差;
- 增量式算法 得出的是控制量的增量,例如在阀门控制中,只输出阀门开度的变化部分,误动作影响小,必要时还可通过逻辑判断限制或禁止本次输出, 不会严重影响系统的工作; 而位置式的输出直接对应对象的输出,因此对系统影响较大;
- 增量式算法 控制输出的是控制量增量,并无积分作用,因此该方法适用于执行机构带积分部件的对象,如步进电机等, 而 位置式算法 适用于执行机构不带积分部件的对象,如电液伺服阀;
- 在进行PID控制时,位置式PID 需要有积分限幅和输出限幅,而 增量式PID 只需输出限幅。
位置式PID优缺点:
优点::位置式PID是一种非递推式算法,可直接控制执行机构(如平衡小车),u(k)的值和执行机构的实际位置(如小车当前角度)是一一对应的, 因此在执行机构不带积分部件的对象中可以很好应用;
缺点::每次输出均与过去的状态有关,计算时要对e(k)进行累加,运算工作量大。
增量式PID优缺点:
优点::
- 误动作时影响小,必要时可用逻辑判断的方法去掉出错数据。
- 手动/自动切换时冲击小,便于实现无扰动切换。
- 算式中不需要累加。控制增量Δu(k)的确定仅与最近3次的采样值有关。在速度闭环控制中有很好的实时性。
缺点:
- 积分截断效应大,有稳态误差;
- 溢出的影响大。有的被控对象用增量式则不太好;
位置式PID的C语言实现¶
在上一小节已经推导出位置式PID;这节主要讲解位置式PID的实现方法,以及C语言的算法实现举例说明。 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们创建了两个文件:bsp_pid.c和bsp_pid.h文件用来存放PID的程序及相关宏定义。
编程要点¶
- 定时器中断配置
- 串口初始化
- PID_realize()函数算法实现
- PID_param_init()参数整定
软件分析¶
1 2 3 4 5 6 7 8 9 10 | /*pid*/
typedef struct
{
float target_val; //目标值
float actual_val; //实际值
float err; //定义偏差值
float err_last; //定义上一个偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
float integral; //定义积分值
}_pid;
|
用于在使用PID时方便调用每个结构体成员,不同的PID算法只需要使用_pid重新定义即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 初始化参数 */
printf("PID_init begin \n");
pid.target_val=0.0;
pid.actual_val=0.0;
pid.err=0.0;
pid.err_last=0.0;
pid.integral=0.0;
pid.Kp = 0.31;
pid.Ki = 0.070;
pid.Kd = 0.3;
printf("PID_init end \n");
}
|
在这个函数中主要对PID的所有参数进行初始化,并且要初始化好Kp、Ki、Kd这三个参数, 因为这三个参数直接影响算法到达目标值的时间和状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /**
* @brief PID算法实现
* @param val 目标值
* @note 无
* @retval 通过PID计算后的输出
*/
float PID_realize(float temp_val)
{
/*传入目标值*/
pid.target_val=temp_val;
/*计算目标值与实际值的误差*/
pid.err=pid.target_val-pid.actual_val;
/*误差累积*/
pid.integral+=pid.err;
/*PID算法实现*/
pid.actual_val=pid.Kp*pid.err+pid.Ki*pid.integral+pid.Kd*(pid.err-pid.err_last);
/*误差传递*/
pid.err_last=pid.err;
/*返回当前实际值*/
return pid.actual_val;
}
|
这个函数是整个工程的核心,不算注释,10行左右的代码,就实现了位置式PID的算法; 在PID_realize(float temp_val)函数中以传参的形式将目标值传入函数中,然后所有的计算数值都是pid结构体成员的运算; 为了更好地理解从公式到算法的实现,可以仔细观察以下公式:

这个公式就是代码第16行中的公式形式,公式和代码的计算方式基本一致,只不过在公式中第二项的Ki是使用的对误差积分, 在代码中变成了对误差的累加,虽然表达形式不一样,但是达到的效果和目的是一样的。 计算过后将误差传递用于下一次使用,并将实际值返回。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /**
* @brief 定时器周期调用函数
* @param 无
* @note 无
* @retval 无
*/
void time_period_fun()
{
static int num=0;
static int run_i=0;
if(!pid_status)
{
float val=PID_realize(set_point);
int temp = val;
// 给通道 1 发送实际值
set_computer_value(SEED_FACT_CMD, CURVES_CH1, &temp, 1);
}
}
|
这个函数主要在定时器中断中调用,定时器配置为每20ms中断一次,PID算法每20ms执行一次,这也就是算法的周期。
将程序下载到开发板,就会看到目标值与实际值的变化,为了方便观看,我将串口打印信息复制到了下面:

观察数据可以面明显看到一开始相邻两个数据相差很多,震荡的比较严重,但是随着算法一直运行,目标值(val)与实际值(act)的误差越来越小,到最后,实际值的相邻两个数值在目标值上下跳动 ,这里数值的微小振动就是稳态误差了,也叫作静态误差。
位置式参数验证
调节参数并观察曲线变化,对于不同的PID参数,输出调节一定是不一样的,具体如下图:

以上的曲线图是相同的代码,但是带来的效果却是大不相同,左侧的曲线明显是震荡了很多次后才趋于稳定, 但是只修改了一个参数Kp,将原来的0.31,修改为0.21曲线调节次数就明显减少了,这足以证明参数的重要性。
增量式PID的C语言实现¶
看过上一节的讲解后,对于位置式的PID的算法实现应该有一个深度的认识了,在这节将对增量式PID的算法进行解析。 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整的代码请参考本章配套的工程。 我们创建了两个文件:bsp_pid.c和bsp_pid.h文件用来存放PID的程序及相关宏定义。
编程要点¶
- 定时器中断配置
- 串口初始化
- PID_realize()函数算法实现
- PID_param_init()参数整定
软件分析¶
1 2 3 4 5 6 7 8 9 10 | /*pid*/
typedef struct
{
float target_val; //目标值
float actual_val; //实际值
float err; //定义当前偏差值
float err_next; //定义下一个偏差值
float err_last; //定义最后一个偏差值
float Kp, Ki, Kd; //定义比例、积分、微分系数
}_pid;
|
用于在使用PID时方便调用每个结构体成员,不同的PID算法只需要使用_pid重新定义即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /**
* @brief PID参数初始化
* @note 无
* @retval 无
*/
void PID_param_init()
{
/* 初始化参数 */
printf("PID_init begin \n");
pid.target_val=0.0;
pid.actual_val=0.0;
pid.err = 0.0;
pid.err_last = 0.0;
pid.err_next = 0.0;
// pid.Kp = 0.21;
// pid.Ki = 0.070;
// pid.Kd = 0.32;
pid.Kp = 0.21;
pid.Ki = 0.80;
pid.Kd = 0.01;
printf("PID_init end \n");
}
|
在这个函数中主要对PID的所有参数进行初始化,并且要初始化好Kp、Ki、Kd这三个参数, 因为这三个参数直接影响算法到达目标值的时间和状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /**
* @brief PID算法实现
* @param val 目标值
* @note 无
* @retval 通过PID计算后的输出
*/
float PID_realize(float temp_val)
{
/*传入目标值*/
pid.target_val = temp_val;
/*计算目标值与实际值的误差*/
pid.err=pid.target_val-pid.actual_val;
/*PID算法实现*/
float increment_val = pid.Kp*(pid.err - pid.err_next) + pid.Ki*pid.err + pid.Kd*(pid.err - 2 * pid.err_next + pid.err_last);
/*累加*/
pid.actual_val += increment_val;
/*传递误差*/
pid.err_last = pid.err_next;
pid.err_next = pid.err;
/*返回当前实际值*/
return pid.actual_val;
}
|
这个函数是整个工程的核心,不算注释,10行左右的代码,就实现了位置式PID的算法; 在PID_realize(float temp_val)函数中以传参的形式将目标值传入函数中,然后所有的计算数值都是pid结构体成员的运算; 为了更好地理解从公式到算法的实现,可以仔细观察以下两个公式:


这两个公式就是代码第14、16行中的公式形式,公式和代码的计算方式基本一致,可以看出增量式的PID是与近三次的误差有关; 虽然代码与公式的表达形式不一样,但是达到的效果和目的是一样的。计算过后将误差传递用于下一次使用,并将实际值返回。
增量式参数验证
将代码下载到开发板,调节参数并观察曲线变化,对于不同的PID参数,输出调节一定是不一样的,具体如下图:

以上是修改参数后的调节输出;通过数据看到PID调节的次数更少了,次数越少说明调节的效果越好,当然也要根据具体应用来决定需要什么样的曲线。
PID控制器参数整定¶
算法固然重要,但是参数重要性一点也不低于算法本身,同样的算法如果PID的参数调的不好,实际的效果就是天壤之别了,所以这个章节主要对参数的整定进行讲解。

参数整定曲线图
以上是四组不同的参数对实际值的影响,其中蓝色的线为目标值绿色的线为实际值,但是在不同参数下却表现的大不相同,在曲线上已经变现的很明显了; 如果曲线的走势就表示电机的速度变化,可想而知,哪个参数好哪个参数不好,那么这个么重要的参数应该怎样整定呢?整定参数都与什么有关系呢?
它是根据被控过程的特性确定 PID 控制器的比例系数、积分时间和微分时间的大小;PID控制器参数整定的方法有很多, 主要分为两大类理论计算整定方法和工程整定法也叫作经验法;其中第一种是理论计算整定法主要是建立数学模型然后根据数学模型, 经过理论的计算来确定最终的控制器参数,这种方法是在所有的情况都是理想的,经过这种方法调出来的参数是不可以直接使用的, 毕竟理想化的参数考虑的太少,只能当做一个参考还需要根据实际情况修改;
第二种是工程整定方法,它主要是依赖于工程中的经验,直接在实际的控制系统的实验中进行,方法简单容易掌握,被广泛使用;
在实际的应用中用到的最多的应该就要属经验法(工程整定法),因为不同的控制系统的实际情况都是不一样的,除了理论公式一致以外其它的完全不同。 所以就需要懂得理论并付诸于实践,才能使控制系统达到好的效果。

先看上图中PID曲线动态图,这个图只描述了算法中的一部分参数,可以做来参考。
试凑法
采样周期的选择,要根据所设计的系统的具体情况,用试凑的方法,在试凑过程中根据各种合理的建议来预选采样周期, 多次试凑,选择性能较好的一个作为最后的采样周期。早整定参数时必须要认真的观察系统的相应情况,根据系统的响应情况来调整参数。 在调节参数时应该知道各种参数调节的特点,才能有的放矢;
- 比例调节作用特点:调节作用快,系统一出现偏差,调节器立即将偏差放大输出;
- 积分调节作用特点:积分调节作用的输出变化与输入偏差的积分成正比,积分调节作用的输出不仅取决于偏差的大小,还取决于偏差存在的时间,只要有偏差存在, 尽管偏差可能很小,但它存在的时间越长,输出信号就越大,只有消除偏差,输出才停止变化;
- 微分调节作用特点:微分调节的输出是与被调量的变化率成正比,微分调节越大,越能提前响应,但是也会将不必要的偏差放大;
- 先是比例(P),再积分(I),最后是微分(D)
- 调试时,将PID参数置于影响最小的位置,即P最大,I最大,D最小;
- 按纯比例系统整定比例度,使其得到比较理想的调节过程曲线,然后再把比例度放大1.2倍左右,将积分时间从大到小改变,使其得到较好的调节过程曲线;
- 最后在这个积分时间下重新改变比例度,再看调节过程曲线有无改善;
- 如有改善,可将原整定的比例度减少,改变积分时间,这样多次的反复,就可得到合适的比例度和积分时间;
- 如果在外界的干扰下系统稳定性不好,可把比例度和积分时间适当增加一些,使系统足够稳定;
- 将整定好的比例度和积分时间适当减小,加入微分作用,以得到超调量最小、调节作用时间最短的调节过程。
临界比例法
临界比例法:适用于闭环控制系统里将调节器置于纯比例的作用下,从大到小逐渐改变调节器的比例度,并且得到等幅度的震荡过程就叫做临界比例度;
- 将调节器的积分置于最大,微分置于0,比例度系数适当即可平衡一段时间,把系统投放到自动运行中。
- 然后将比例逐渐增大,增大到产生等幅现象,并记录下等幅时的临界比例系数和两个波峰的时间间隔。
- 根据记下的比例系数和周期,采用经验公式,计算调节器的参数。
控制方法 | Kp | Ki | Kd |
---|---|---|---|
P控制 | δK / 2 | ||
PI控制 | δK / 2.2 | Kp / (0.833 × TK) | |
PID控制 | δK / 1.7 | Kp / (0.5 × TK) | 0.125 × TK × Kp |
一般调节法
这种方法针对一般的PID控制系统所以称之为一般调节法;其中Kp是加快系统响应速度,提高系统的调节精度;Ki用于消除稳态误差;Kd改善系统的稳态性能。
- 在输出不振荡时,增大比例增益P。
- 在输出不振荡时,减小积分时间常数Ti。
- 在输出不振荡时,增大微分时间常数Td。
(它们三个任何谁过大都会造成系统的震荡。)
一般步骤为:
- 确定比例增益P :确定比例增益P 时,首先去掉PID的积分项和微分项,一般是令Ti=0、Td=0(具体见PID的参数设定说明),使PID为纯比例调节。 输入设定为系统允许的最大值的60%~70%,由0逐渐加大比例增益P,直至系统出现振荡;再反过来,从此时的比例增益P逐渐减小,直至系统振荡消失, 记录此时的比例增益P,设定PID的比例增益P为当前值的60%~70%。比例增益P调试完成。
- 确定积分时间常数Ti比例增益P确定后,设定一个较大的积分时间常数Ti的初值,然后逐渐减小Ti,直至系统出现振荡,之后在反过来,逐渐加大Ti, 直至系统振荡消失。记录此时的Ti,设定PID的积分时间常数Ti为当前值的150%~180%。积分时间常数Ti调试完成。
- 确定积分时间常数Td 积分时间常数Td一般不用设定,为0即可。若要设定,与确定 P和Ti的方法相同,取不振荡时的30%。
- 系统空载、带载联调,再对PID参数进行微调,直至满足要求:理想时间两个波,前高后低4比1。
采样周期选择
采样周期该怎么选择?采样周期越短控制的效果越接近于连续,对于大多数算法缩短采样周期可使控制回路性能改善,但采样周期缩短时, 频繁的采样必然会占用较多的计算工作时间,同时也会增加计算的负担,而对有些变化缓慢的受控对象无需很高的采样频率即可满意地进行跟踪, 过多的采样反而没有多少实际意义。
以一个轮子的转动为例,根据耐奎斯特采样定理可知:假设这个伦斯以每秒45度来转动,那么每个轴返回原位需要8秒(采样周期), 那如果我们在8、16、24秒时用相机拍照是不是拍到的照片都是静止不动的?这是因为在采样的周期内,车轮旋转的证书周期都会回到原位, 不论旋转方向如何都会回到原位;如果现在减少拍照时间,每4秒钟拍一张照片则会在照片中发现轮子正在旋转,但是不能区分旋转方向。 如果3秒钟拍一张照片那么无论是顺时针还是逆时针都可以从照片中观察到轮子的相位变化。这就是Nyquist-Shannon采样定理, 我们希望同时看到轮子的旋转和相位变化,采样周期要小于整数周期的1/2,采样的频率应该大于原始频率的2倍。

以上的调参方法只是一些工程上一些普遍的方法,但是调参时更注重的应该是原理和影响因素,不同的控制系统因素不同。 例如电机控速系统,影响参数的有电源电压电流还有反馈的精度,具体参数要根据实际具体调整,才能更有效。
编码器的使用¶
本章参考资料:《STM32 HAL库开发指南——基于F407》、《STM32F4xx参考手册》、 HAL库帮助文档《STM32F417xx_User_Manual.chm》。 学习本章时,配合《STM32F4xx 参考手册》通用定时器章节一起阅读,效果会更佳,特别是涉及到寄存器说明的部分。
在基础部分的编码器详解章节中,已经详细介绍了旋转编码器的结构、原理和参数,这一章节我们将介绍如何使用编码器对电机的速度和位置进行测量。
增量式编码器倍频技术¶
首先来看一下增量式编码器的输出信号和它的信号倍频技术。增量式编码器输出的脉冲波形信号形式常见的有两种:
- 一种是占空比50%的方波,通道A和B相位差为90°;
- 另一种则是正弦波这类模拟信号,通道A和B相位差同样为90°。
对于第1种形式的方波信号,如果把两个通道组合起来看的话,可以发现A和B各自的上升沿和下降沿都能计数,至少在1/2个原始方波周期内就可以计数一次, 最多1/4个原始方波周期。这样计数频率就是原始方波信号的2倍或4倍,换句话说就是,将编码器的分辨率提高了2到4倍,具体如下图所示。

图中的方波信号如果只看其中一个通道的上升沿,那计数频率就等于这个通道信号的频率。如果在通道A的上升沿和下降沿都进行计数,计数频率就是通道A的两倍,即2倍频。 如果同时对两个通道的上升沿和下降沿都计数,那计数频率就变成了原始信号的4倍,即4倍频。
假设有个增量式编码器它的分辨率是600PPR,能分辨的最小角度是0.6°,对它进行4倍频之后就相当于把分辨率提高到了600*4=2400PPR,此时编码器能够分辨的最小角度为0.15°。 编码器倍频技术还可用来扩展一些测速方法的速度适用范围。例如电机测速通常会使用M法进行测量(M法在下节介绍), 通过对编码器4倍频可以扩展M法的速度下限。
以上就是方波信号的编码器倍频技术,其实输出模拟信号的增量式编码器同样也可以倍频,不过这种倍频原理与方波完全不同,教程当中就不讲解了。
STM32的编码器接口简介¶
STM32芯片内部有专门用来采集增量式编码器方波信号的接口,这些接口实际上是STM32定时器的其中一种功能。 不过编码器接口功能只有高级定时器TIM1、TIM8和通用定时器TIM2到TIM5才有。编码器接口用到了定时器的输入捕获部分, 功能框图如下图所示。输入捕获功能在《STM32 HAL库开发指南》中已有详细讲解,所以这部分内容在此就不再赘述了。
我们重点关注编码器接口是如何实现信号采集和倍频的。《STM32F4xx参考手册》给出了的编码器信号与计数器方向和计数位置之间的关系,如下表所示。

这个表格将编码器接口所有可能出现的工作情况全都列了出来,包括它是如何实现方向检测和倍频的。虽然信息很全面但是乍看上去却不容易看懂。 首先需要解释一下,表中的TI1和TI2对应编码器的通道A和通道B,而TI1FP1和TI2FP2则对应反相以后的TI1、TI2。STM32的编码器接口在计数的时候, 并不是单纯采集某一通道信号的上升沿或下降沿,而是需要综合另一个通道信号的电平。表中“相反信号的电平”指的就是在计数的时候所参考的另一个通道信号的电平, 这些电平决定了计数器的计数方向。
为了便于大家理解STM32编码器接口的计数原理,我们将表中的信息提出转换成一系列图像。首先看下图,下图所展示的信息对应表格中“仅在TI1处计数”。 图中包含TI1、TI2两通道的信号,以及计数器的计数方向,其中TI1比TI2 提前 1/4个周期,以TI1的信号边沿作为有效边沿。 当检测到TI1的上升沿时,TI2为低电平,此时计数器向上计数1次,下一时刻检测到TI1的下降沿时,TI2为高电平,此时计数器仍然向上计数一次,以此类推。 这样就能把TI1的上升沿和下降沿都用来计数,即实现了对原始信号的2倍频。

接下来看如下图像,图中同样包含TI1、TI2两通道的信号,以及计数器的计数方向,其中TI1比TI2 滞后 1/4个周期,以TI1的信号边沿作为有效边沿。 当检测到TI1的上升沿时,TI2为高电平,此时计数器向下计数1次,下一时刻检测到TI1的下降沿时,TI2为低电平,此时计数器仍然向下计数一次,以此类推。 这样同样是把TI1的上升沿和下降沿都用来计数,同样实现了对原始信号的2倍频,只不过变成向下计数了。

以上两幅图像都是只以TI1的信号边沿作为有效边沿,并且根据TI2的电平决定各自的计数方向,然后判断计数方向就能得到编码器的旋转方向,向上计数正向,向下计数反向。 “仅在TI2处计数”也是同样的原理,在这里就不重复讲了。
最后如下图所示,下图所展示的信息对应表格中“在TI1和TI2处均计数”。这种采样方式可以把两个通道的上升沿和下降沿都用来计数,计数方向也是两个通道同时参考, 相当于原来仅在一个通道处计数的2倍,所以这种就能实现对原始信号的4倍频。

编码器接口初始化结构体详解¶
HAL库函数对定时器外设建立了多个初始化结构体,其中编码器接口用到的有时基初始化结构体 TIM_Base_InitTypeDef ,和编码器初始化配置结构体 TIM_Encoder_InitTypeDef 。初始化结构体成员用于设置定时器工作环境参数,并由定时器相应初始化配置函数调用, 最终这些参数将会写入到定时器相应的寄存器中。
TIM_Base_InitTypeDef¶
时基结构体 TIM_Base_InitTypeDef 用于定时器基础参数设置,与 HAL_TIM_Base_Init 函数配合使用完成配置。 这个结构体在《STM32 HAL库开发指南》的定时器章节有详细的讲解,这里我们只简单的提一下。
1 2 3 4 5 6 7 8 9 | typedef struct
{
uint32_t Prescaler; //预分频器
uint32_t CounterMode; //计数模式
uint32_t Period; //定时器周期
uint32_t ClockDivision; //时钟分频
uint32_t RepetitionCounter; //重复计算器
uint32_t AutoReloadPreload; //自动重载值
}TIM_Base_InitTypeDef;
|
- Prescaler:定时器预分频器设置;
- CounterMode:定时器计数方式;
- Period:定时器周期;
- ClockDivision:时钟分频;
- RepetitionCounter:重复计数器;
- AutoReloadPreload:自动重载预装载值。
TIM_Encoder_InitTypeDef¶
编码器初始化配置结构体 TIM_Encoder_InitTypeDef 用于定时器的编码器接口模式,与 HAL_TIM_Encoder_Init 函数配合使用完成初始化配置操作。高级定时器TIM1和TIM8以及通用定时器TIM2到TIM5都带有编码器接口,使用时都必须单独设置。
1 2 3 4 5 6 7 8 9 10 11 12 | typedef struct
{
uint32_t EncoderMode; //编码器模式
uint32_t IC1Polarity; //输入信号极性
uint32_t IC1Selection; //输入通道
uint32_t IC1Prescaler; //输入捕获预分频器
uint32_t IC1Filter; //输入捕获滤波器
uint32_t IC2Polarity; //输入信号极性
uint32_t IC2Selection; //输入通道
uint32_t IC2Prescaler; //输入捕获预分频器
uint32_t IC2Filter; //输入捕获滤波器
}TIM_Encoder_InitTypeDef;
|
- EncoderMode:编码器模式选择,用来设置计数器采集编码器信号的方式,可选通道A计数、通道B计数和双通道计数。 它设定TIMx_DIER寄存器的SMS[2:0]位。这个成员实际是用来设置编码器接口的倍频数的,当选择通道A或B计数时为2倍频,双通道计数时为4倍频。
- ICxPolarity:输入捕获信号极性选择,用于设置定时器通道在编码器模式下的输入信号是否反相。 它设定TIMx_CCER寄存器的CCxNP位和CCxP位。
- ICxSelection:输入通道选择,ICx的信号可来自三个输入通道,分别为 TIM_ICSELECTION_DIRECTTI、 TIM_ICSELECTION_INDIRECTTI 或 IM_ICSELECTION_TRC。它设定TIMx_CCMRx寄存器的CCxS[1:0]位的值。 定时器在编码器接口模式下,此成员只能设置为TIM_ICSELECTION_DIRECTTI。
- ICxPrescaler:输入捕获通道预分频器,可设置1、2、4、8分频。它设定TIMx_CCMRx寄存器的ICxPSC[1:0]位的值。
- ICxFilter:输入捕获滤波器设置,可选设置0x0至0x0F。它设定TIMx_CCMRx寄存器ICxF[3:0]位的值。
减速电机编码器测速实验¶
本实验讲解如何使用STM32的编码器接口,并利用编码器接口对减速电机进行测速。学习本小节内容时,请打开配套的“减速电机编码器测速”工程配合阅读。
硬件设计¶
本实验用到的减速电机与减速电机按键控制例程的相同,所以电机、开发板和驱动板的硬件连接也完全相同,只加上了编码器的连线。

上图是我们电机开发板使用的编码器接口原理图,通过连接器与STM32的GPIO相连,一共4个通道,可以同时接入两个编码器。本实验使用PC6和PC7两个引脚,对应TIM3的CH1和CH2。
软件设计¶
本编码器测速例程是在减速电机按键控制例程的基础上编写的,这里只讲解跟编码器有关的部分核心代码,有些变量的设置,头文件的包含以及如何驱动电机等并没有涉及到, 完整的代码请参考本章配套的工程。我们创建了两个文件:bsp_encoder.c 和 bsp_encoder.h 文件用来存放编码器接口驱动程序及相关宏定义。
编程要点¶
- 定时器 IO 配置
- 定时器时基结构体TIM_TimeBaseInitTypeDef配置
- 编码器接口结构体TIM_Encoder_InitTypeDef配置
- 通过编码器接口测量到的数值计算减速电机转速
软件分析¶
- 宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /* 定时器选择 */
#define ENCODER_TIM TIM3
#define ENCODER_TIM_CLK_ENABLE() __HAL_RCC_TIM3_CLK_ENABLE()
/* 定时器溢出值 */
#define ENCODER_TIM_PERIOD 65535
/* 定时器预分频值 */
#define ENCODER_TIM_PRESCALER 0
/* 定时器中断 */
#define ENCODER_TIM_IRQn TIM3_IRQn
#define ENCODER_TIM_IRQHandler TIM3_IRQHandler
/* 编码器接口引脚 */
#define ENCODER_TIM_CH1_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define ENCODER_TIM_CH1_GPIO_PORT GPIOC
#define ENCODER_TIM_CH1_PIN GPIO_PIN_6
#define ENCODER_TIM_CH1_GPIO_AF GPIO_AF2_TIM3
#define ENCODER_TIM_CH2_GPIO_CLK_ENABLE() __HAL_RCC_GPIOC_CLK_ENABLE()
#define ENCODER_TIM_CH2_GPIO_PORT GPIOC
#define ENCODER_TIM_CH2_PIN GPIO_PIN_7
#define ENCODER_TIM_CH2_GPIO_AF GPIO_AF2_TIM3
/* 编码器接口倍频数 */
#define ENCODER_MODE TIM_ENCODERMODE_TI12
/* 编码器接口输入捕获通道相位设置 */
#define ENCODER_IC1_POLARITY TIM_ICPOLARITY_RISING
#define ENCODER_IC2_POLARITY TIM_ICPOLARITY_RISING
/* 编码器物理分辨率 */
#define ENCODER_RESOLUTION 15
/* 经过倍频之后的总分辨率 */
#if ((ENCODER_MODE == TIM_ENCODERMODE_TI1) || (ENCODER_MODE == TIM_ENCODERMODE_TI2))
#define ENCODER_TOTAL_RESOLUTION (ENCODER_RESOLUTION * 2) /* 2倍频后的总分辨率 */
#else
#define ENCODER_TOTAL_RESOLUTION (ENCODER_RESOLUTION * 4) /* 4倍频后的总分辨率 */
#endif
/* 减速电机减速比 */
#define REDUCTION_RATIO 34
|
使用宏定义非常方便程序升级、移植。如果使用不同的定时器、编码器倍频数、编码器分辨率等,修改这些宏即可。 开发板使用的是TIM3的CH1和CH2,分别连接到编码器的通道A和通道B,对应的引脚为PC6、PC7。
- 定时器复用功能引脚初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /**
* @brief 编码器接口引脚初始化
* @param 无
* @retval 无
*/
static void Encoder_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 定时器通道引脚端口时钟使能 */
ENCODER_TIM_CH1_GPIO_CLK_ENABLE();
ENCODER_TIM_CH2_GPIO_CLK_ENABLE();
/**TIM3 GPIO Configuration
PC6 ------> TIM3_CH1
PC7 ------> TIM3_CH2
*/
/* 设置输入类型 */
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
/* 设置上拉 */
GPIO_InitStruct.Pull = GPIO_PULLUP;
/* 设置引脚速率 */
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
/* 选择要控制的GPIO引脚 */
GPIO_InitStruct.Pin = ENCODER_TIM_CH1_PIN;
/* 设置复用 */
GPIO_InitStruct.Alternate = ENCODER_TIM_CH1_GPIO_AF;
/* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
HAL_GPIO_Init(ENCODER_TIM_CH1_GPIO_PORT, &GPIO_InitStruct);
/* 选择要控制的GPIO引脚 */
GPIO_InitStruct.Pin = ENCODER_TIM_CH2_PIN;
/* 设置复用 */
GPIO_InitStruct.Alternate = ENCODER_TIM_CH2_GPIO_AF;
/* 调用库函数,使用上面配置的GPIO_InitStructure初始化GPIO */
HAL_GPIO_Init(ENCODER_TIM_CH2_GPIO_PORT, &GPIO_InitStruct);
}
|
定时器通道引脚使用之前必须设定相关参数,这里选择复用功能,并指定到对应的定时器。使用GPIO之前都必须开启相应端口时钟,这个没什么好说的。 唯一要注意的一点,有些编码器的输出电路是不带上拉电阻的,需要在板子上或者芯片GPIO设置中加上上拉电阻。
- 编码器接口配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | /**
* @brief 配置TIMx编码器模式
* @param 无
* @retval 无
*/
static void TIM_Encoder_Init(void)
{
TIM_Encoder_InitTypeDef Encoder_ConfigStructure;
/* 使能编码器接口时钟 */
ENCODER_TIM_CLK_ENABLE();
/* 定时器初始化设置 */
TIM_EncoderHandle.Instance = ENCODER_TIM;
TIM_EncoderHandle.Init.Prescaler = ENCODER_TIM_PRESCALER;
TIM_EncoderHandle.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM_EncoderHandle.Init.Period = ENCODER_TIM_PERIOD;
TIM_EncoderHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
TIM_EncoderHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
/* 设置编码器倍频数 */
Encoder_ConfigStructure.EncoderMode = ENCODER_MODE;
/* 编码器接口通道1设置 */
Encoder_ConfigStructure.IC1Polarity = ENCODER_IC1_POLARITY;
Encoder_ConfigStructure.IC1Selection = TIM_ICSELECTION_DIRECTTI;
Encoder_ConfigStructure.IC1Prescaler = TIM_ICPSC_DIV1;
Encoder_ConfigStructure.IC1Filter = 0;
/* 编码器接口通道2设置 */
Encoder_ConfigStructure.IC2Polarity = ENCODER_IC2_POLARITY;
Encoder_ConfigStructure.IC2Selection = TIM_ICSELECTION_DIRECTTI;
Encoder_ConfigStructure.IC2Prescaler = TIM_ICPSC_DIV1;
Encoder_ConfigStructure.IC2Filter = 0;
/* 初始化编码器接口 */
HAL_TIM_Encoder_Init(&TIM_EncoderHandle, &Encoder_ConfigStructure);
/* 清零计数器 */
__HAL_TIM_SET_COUNTER(&TIM_EncoderHandle, 0);
/* 清零中断标志位 */
__HAL_TIM_CLEAR_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
/* 使能定时器的更新事件中断 */
__HAL_TIM_ENABLE_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);
/* 设置更新事件请求源为:定时器溢出 */
__HAL_TIM_URS_ENABLE(&TIM_EncoderHandle);
/* 设置中断优先级 */
HAL_NVIC_SetPriority(ENCODER_TIM_IRQn, 5, 1);
/* 使能定时器中断 */
HAL_NVIC_EnableIRQ(ENCODER_TIM_IRQn);
/* 使能编码器接口 */
HAL_TIM_Encoder_Start(&TIM_EncoderHandle, TIM_CHANNEL_ALL);
}
|
编码器接口配置中,主要初始化两个结构体,其中时基初始化结构体TIM_HandleTypeDef很简单,而且在其他应用中都用涉及到,直接看注释理解即可。
重点是编码器接口结构体TIM_Encoder_InitTypeDef的初始化。对于STM32定时器的编码器接口,我们首先需要设置编码器的倍频数,即成员EncoderMode, 它可把编码器接口设置为2倍频或4倍频,根据bsp_encoder.h的宏定义我们将其设置为4倍频,倍频原理在上面已有讲解这里不再赘述。
对于编码器接口输入通道的配置,我们只讲解通道1的配置情况,通道2是一样的。首先是输入信号极性,成员IC1Polarity在输入捕获模式中是用来设置触发边沿的, 但在编码器模式中是用来设置输入信号是否反相的。设置为RISING表示不反相,FALLING表示反相。此成员与编码器的计数触发边沿无关, 只用来匹配编码器和电机的方向,当设定的电机正方向与编码器正方向不一致时不必更改硬件连接,直接在程序中修改IC1Polarity即可。
接下来是成员IC1Selection,这个成员用于选择输入通道,IC1可以是TI1输入的TI1FP1,也可以是从TI2输入的TI2FP1,我们这里选择直连(DIRECTTI),即TI1FP1映射到IC1, 在编码器模式下这个成员只能设置为DIRECTTI,其他可选值都是不起作用的。
最后是成员IC1Prescaler和成员IC1Filter,我们需要对编码器的每个脉冲信号都进行捕获,所以设置成不分频。根据STM32编码器接口2倍频或4倍频的原理, 接口在倍频采样的过程中也会对信号抖动进行补偿,所以输入滤波器也很少会用到。
配置完编码器接口结构体后清零计数器,然后开启定时器的更新事件中断,并把更新事件中断源配置为定时器溢出,也就是仅当定时器溢出时才触发更新事件中断。 然后配置定时器的中断优先级并开启中断,最后启动编码器接口。
- 定时器溢出次数记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /**
* @brief 定时器更新事件回调函数
* @param 无
* @retval 无
*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
/* 判断当前计数器计数方向 */
if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle))
/* 下溢 */
Encoder_Overflow_Count--;
else
/* 上溢 */
Encoder_Overflow_Count++;
}
|
在TIM_Encoder_Init函数中我们配置了仅当定时器计数溢出时才触发更新事件中断,然后在中断回调函数中记录定时器溢出了多少次。首先定义一个全局变量Encoder_Overflow_Count, 用来记录计数器的溢出次数。在定时器更新事件中断回调函数中,使用__HAL_TIM_IS_TIM_COUNTING_DOWN函数判断当前的计数方向,是向上计数还是向下计数, 如果向下计数,Encoder_Overflow_Count减1,反之则加1。这样在计算电机转速和位置的时候就可以把溢出次数也参与在内。
- 主函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | /**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
__IO uint16_t ChannelPulse = 0;
uint8_t i = 0;
/* HAL库初始化*/
HAL_Init();
/* 初始化系统时钟为168MHz */
SystemClock_Config();
/* 配置1ms时基为SysTick */
HAL_InitTick(5);
/* 初始化按键GPIO */
Key_GPIO_Config();
/* 初始化USART */
DEBUG_USART_Config();
printf("\r\n——————————野火减速电机编码器测速演示程序——————————\r\n");
/* 通用定时器初始化并配置PWM输出功能 */
TIMx_Configuration();
TIM1_SetPWM_pulse(PWM_CHANNEL_1,0);
TIM1_SetPWM_pulse(PWM_CHANNEL_2,0);
/* 编码器接口初始化 */
Encoder_Init();
while(1)
{
/* 扫描KEY1 */
if( Key_Scan(KEY1_GPIO_PORT, KEY1_PIN) == KEY_ON)
{
/* 增大占空比 */
ChannelPulse += 50;
if(ChannelPulse > PWM_PERIOD_COUNT)
ChannelPulse = PWM_PERIOD_COUNT;
set_motor_speed(ChannelPulse);
}
/* 扫描KEY2 */
if( Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON)
{
if(ChannelPulse < 50)
ChannelPulse = 0;
else
ChannelPulse -= 50;
set_motor_speed(ChannelPulse);
}
/* 扫描KEY3 */
if( Key_Scan(KEY3_GPIO_PORT, KEY3_PIN) == KEY_ON)
{
/* 转换方向 */
set_motor_direction( (++i % 2) ? MOTOR_FWD : MOTOR_REV);
}
}
}
|
本实验的主函数与减速电机按键调速基本相同,只是在一开始初始化了HAL库和配置了SysTick嘀嗒定时器为1ms中断一次, 当然最重要的还是调用Encoder_Init函数,初始化和配置STM32的编码器接口。while循环内容相同,为了不影响到在while循环中调整电机速度, 我们将使用中断进行编码器数据采集和计算。
- 数据计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /* 电机旋转方向 */
__IO int8_t Motor_Direction = 0;
/* 当前时刻总计数值 */
__IO int32_t Capture_Count = 0;
/* 上一时刻总计数值 */
__IO int32_t Last_Count = 0;
/* 电机转轴转速 */
__IO float Shaft_Speed = 0.0f;
/**
* @brief SysTick中断回调函数
* @param 无
* @retval 无
*/
void HAL_SYSTICK_Callback(void)
{
static uint16_t i = 0;
i++;
if(i == 100)/* 100ms计算一次 */
{
/* 电机旋转方向 = 计数器计数方向 */
Motor_Direction = __HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle);
/* 当前时刻总计数值 = 计数器值 + 计数溢出次数 * 计数器溢出值 */
Capture_Count =__HAL_TIM_GET_COUNTER(&TIM_EncoderHandle) + (Encoder_Overflow_Count * ENCODER_TIM_PERIOD);
/* 转轴转速 = 单位时间内的计数值 / 编码器总分辨率 * 时间系数 */
Shaft_Speed = (float)(Capture_Count - Last_Count) / ENCODER_TOTAL_RESOLUTION * 10 ;
printf("电机方向:%d\r\n", Motor_Direction);
printf("单位时间内有效计数值:%d\r\n", Capture_Count - Last_Count);/* 单位时间计数值 = 当前时刻总计数值 - 上一时刻总计数值 */
printf("电机转轴处转速:%.2f 转/秒 \r\n", Shaft_Speed);
printf("电机输出轴转速:%.2f 转/秒 \r\n", Shaft_Speed/REDUCTION_RATIO);/* 输出轴转速 = 转轴转速 / 减速比 */
/* 记录当前总计数值,供下一时刻计算使用 */
Last_Count = Capture_Count;
i = 0;
}
}
|
如上代码所示,首先定义了一些全局变量,用来保存计算数据和供其他函数使用。在SysTick中断回调函数中每100ms执行一次采集和计算, 先检测电机旋转方向,直接读取当前时刻的计数器计数方向就可获得方向,向上计数为正向,向下计数为反向。
接着是测量当前时刻的总计数值,根据总计数值计算电机转速,在本例程中我们使用M法进行测速,单位时间内的计数值除以编码器总分辨率即可得到单位时间内的电机转速, 代码中单位时间为100ms,单位时间内的计数值由当前时刻总计数值Capture_Count减上一时刻总计数值Last_Count得到,编码器总分辨率由编码器物理分辨率乘倍频数得到, 这里算出来的电机转速单位是转/百毫秒,转到常用的单位还需要乘上一个时间系数,比如转/秒就乘10。不过此时得到的是电机转轴处的转速,并不是减速电机输出轴的转速, 把转轴转速除以减速比即可得到输出轴的转速。
所有数据全部采集和计算完毕后,将电机方向、单位时间内的计数值、电机转轴转速和电机输出轴转速等数据全部通过串口打印到窗口调试助手上, 并将当前的总计数值记录下来方便下次计算使用。
下载验证¶
保证开发板相关硬件连接正确,用USB线连接开发板“USB转串口”接口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板,串口调试助手会显示程序输出的信息。 我们通过开发板上的三个按键控制电机加减速和方向,在串口调试助手的接收区即可看到电机转速等信息。

直流电机速度环控制实现¶
前面我们学习了直流电机简单的PWM控制。但是我们在实际使用中并不是只是简单的PWM控制就能满足应用要求, 通常我们还需要对速度进行控制控制,如前面章节中讲到的为什么使用PID一节中列举的小车控制一样, 如果不对速度进行控制可能系统运行效果会不如预期那么好,本章节中我们就通过速度环的PID控制来实现直流电机的速度控制。
本章通过我们前面学习的位置式PID和增量式PID两种控制方式分别来实现速度环的控制, 如果还不知道什么是位置式PID和增量式PID,请务必先学习前面PID算法的通俗解说这一章节。
硬件设计¶
本章的硬件设计与编码器的使用一章节中的硬件设计完全一样,所以这里不在赘述。
直流电机速度环控制-位置式PID实现¶
直流电机速度环控制-增量式PID实现¶
直流电机电流环控制实现¶
直流电机位置环控制实现¶
舵机位置环控制实现¶
步进电机位置环控制实现¶
步进电机梯形加减速实现¶
在基础章节已经对步进电机的基础旋转进行了详细的讲解和多种方式的实验。相信你对步进电机的基础旋转已经得心应手了, 但是这并不够;还需要对步进电机的加减速进行学习,所以在这一章节主要对步进电机的梯形加减速进行讲解。
一定会有人疑问为什么要使用加减速、加减速有什么好处呢?
加减速使用的场景有那些呢?
为什么要使用加减速呢?如果你在基础部分学习,硬件驱动细分器与软件的细分参数或定时器分频参数设置不当时启动电机时, 会遇见步进电机有啸叫声但是不会转动,这是因为软件产生脉冲的频率大于步进电机的启动频率,步进电机有一个很重要的技术参数: 空载启动频率,也就是在没有负载的情况下能够正常启动的最大脉冲频率,如果脉冲频率大于该值,步进电机则不能够正常启动, 发生丢步或者堵转的情况;或者也可以理解为由于步进脉冲变化过快,转子由于惯性的作用跟不上电信号的变化。 所以要使用加减速来解决启动频率低的问题,在启动时使用较低的脉冲频率,然后逐渐的加快频率。
步进电机加减速使用的场景有有哪些呢?步进电机加减速使用的场景可以说是多种多样,但是大部分在一些工业上,例如CNC雕刻机、 3D打印机、车床等。只要是在工业上使用到步进电机的都会涉及到加减速,可见加减速算法的重要性。
梯形加减速算法原理详解¶
梯形加减速的实现是基于基础旋转章节的内容, 所以对于基础旋转章节不理解的可以参考,基础部分步进电机基础旋转控制 章节。参考书目《AVR446_Linear speed control of stepper motor.pdf》
算法特点¶
为了使得不出现丢步或者超步现象并且提高效率,需要使得步进电机先以固定的加速度达到目标速度,然后以这个速度运行, 快到达目标步数时再减到最低速;整个过程是一个梯形的模型,所以以它的数学模型命名的加减速算法。

从模型中即可反映出算法的特点,数学模型中一共分为三个阶段,OA加速部分、AB匀速部分和BC减速部分。
- 在OA加速过程中,由低于步进电机的启动频率开始启动(模型中由0启动),以固定的加速度增加速度到目标值;
- 在AB匀速过程中,以最大速度匀速运动;
- 在BC减速部分中,以加速度不变的速度递减到0;
这种算法是一种在加速过程和减速过程中加速度不变的匀变速控制算法, 由于速度变化的曲线有折点,所以在启动、停止、匀速段中很容易产生冲击和振动。
算法基础概念及方程¶
步进电机的转动需要控制器发送脉冲,如果控制器以恒定速度发送脉冲,那么步进电机就以恒定速度转动; 如果控制器以加速度运动,那么步进电机就以加速度运动;所以只要改变脉冲的频率就可以改变速度的变化, 也就是说调整脉冲之间的时间间隔就可以改变速度。

上图为步进电机与时间的示意图,其中
- t0 表示脉冲发送的起始时刻
- t1 表示脉冲发送的第二个时刻
- t2 表示脉冲发送的第三个时刻
- tt 表示定时器的计数周期
- c0 表示定时器从t0~t1时刻的定时器计数值
- c1 表示定时器从t1~t2时刻的定时器计数值
- δt 表示两个脉冲之间的间隔时间
以stm32的高级定时器8为例,定时器8的时钟频率为168MHZ,如果将分频值设置为5,那么定时器的时钟频率则为:ft=168/(5+1)=28MHZ, 相当于计数28M次正好为一秒,周期与频率为倒数关系,所以分频值为5的定时器8的计数周期为:1/ft;

其中 :
- n 为步进电机所转的脉冲数
- s 为时间单位 秒
- spr 为步进电机所转一圈的脉冲数
- rad 为弧度单位 (1rad = (180/π)° ≈ 57.3°)
- rad/sec 弧度每秒; 1圈(revolutions) = 2 * 3.1415弧度(rad); (1 rad/sec = 60*1/(2*3.1415) rev/min = 9.55 rpm); 1 rad/sec=9.55 rpm;
直线加减速模型解析¶
在本章的一开始就已经简单的介绍了一下加减速算法的模型
模型阶段分析:

要使得步进电机平稳的启动和停止,则需要控制好步进电机的加速度和减速度,控制好加速度和减速度就可得到好的曲线模型, 上图中一共是三个不同变量与时间的变化曲线,分别是 加速度与时间、速度与时间和位置与时间的曲线。
在三个模型中的跳变位置已经画上对应的虚线,分别为虚线a、虚线b、虚线c和虚线d
- 起始点~a:在这阶段中,速度、加速度、位置都没有变化;
- a~b阶段:从虚线a开始,在这阶段中加速度不变,速度与位置不断上升,并且速度是以恒定加速度上升,上升到最大速度也就是速度曲线与虚线b的交点;
- b~c阶段:在这阶段中,以不变的速度在运行,由于速度不变,则加速度为零,并且位置在这阶段呈现一个一次函数的上升阶段;
- c~d阶段:在这阶段中,速度开始呈匀减速的状态,所以加速度为负值,但是位置依旧上升,但上升曲线逐渐变慢;
进一步理解:
在已经对模型的几个阶段有所了解后,下面对加速部分进一步讲解;

上图中分别是对 ω-t和θ-t 的变化图:
在 ω-t 图中是梯形加减速模型中的加速部分中,红色竖线表示的是脉冲发生的位置,由加速的方向看(从左到右), 在图上两个相邻之间的脉冲之间的距离越来越近,根据 δt=ct 计数周期不变,计数值越来越小也就意味着脉冲之间的间隔时间变短了, 频率变快了,直接影响步进电机转的变快了;所以说脉冲之间的定时器计数值是影响脉冲频率变化的重要因素。
在 θ-t 图中,脉冲每产生一次就对应着θ轴上的一小格,ω-t 图中工产生6次脉冲就对应 θ-t 中的6次位置的变化。
脉冲时间间隔的精确计算¶
脉冲间隔决定速率变化,所以对于脉冲的时间间隔计算 就显得尤为重要。
时间间隔的计算由以下几个公式推导出来:

注:n表示脉冲个数;α表示步距角;
在第n个时刻的脉冲角度,所以 nα 就是n个脉冲实际旋转的角度;这里可以说是角度也可以说是位置,单纯说一个脉冲那就是一个步距角, 那么电机与丝杆滑台联系到一起那最终作用到的就是滑台的位置移动了。

对于这个公式应该都不陌生,物理学中的匀加速运动的距离公式,在梯形加减速中加速部分就是匀加速运动。 当v0=0,并且将相关变量带入得:

注: S表示位移;w表示加速度;tn表示时间点
将上述公式整理为:

可以将上述理解为两个时间点,那么相邻脉冲的时间点的差值就是脉冲的时间间隔; 所以计数器的时间间隔公式为:

将n与n+1带入公式4并且提出公因式即可得到公式6,将公式6左右两侧除以tt,即可得到公式7。

当n=0带入公式7,括号内的数值为1,并且算出第一次产生脉冲的计数值C0;仔细观察公式8与公式7,发现可以将公式8直接带入到公式7,即可得到公式9; 此时第n次的脉冲间隔的计数值只与第一次的计数值和次数有关。
由于计算的过程中需要进行开方运算,微控制器的计算能力有限,因此在此使用泰勒公式进行泰勒级数逐级逼近的方法。 在这里主要是用的是泰勒公式的特例—— 麦克劳林公式 ;具体如下图:

为构造与麦克劳林相同的公式将 n-1 ,并且与公式9做比值处理,并进行化简计算,具体如下图所示:

公式推导一共分为以下5个步骤推导:
- 步骤1是将 n 与 n-1 分别带入到公式9;
- 分子分母提出C0和根号n,并将其约掉;
- 整理化简根号下的内容;
- 将麦克劳林公式带入;
- 忽略无穷下余项,化简求得;
将其化简为关于Cn的式子如下:

这样就避免了开方两次的问题,由于在化简时舍弃了无穷小余项,所以验证下化简前后的误差:

当 n=1 时,分别带入以上两个式子,求得其结果,发现出现偏差,但是可以通过将化简后的C0乘以一个0.69的参数进行矫正这个误差。
加减速度与步数的关系¶
根据上一小节推导的公式可得:

- 这是初速度为0的匀加速运动的基础方程,只不过其中一些变量是与具体参数有关的,具体可以参考上一小节的公式;
- 步距角与步数的乘积相当于旋转角度,或者位置;
- 将(1)与(2) 的关系式联系起来就是公式(3);
- 根据V=V0+at,初始速度为0得V=at,再将其带入相关变量;
- 将公式(4)带入公式(5)
在上述公式中有相关变量分别为:步数、加速度、速度和步距角四个变量,由于步距角是一个固定值, 所以当速度设置为最大值时步数就与加速度成反比,也就是当加速度小的时候需要较多的步数,当加速度大的时候需要较少的步数就可以到达目标速度。
由于步进电机 加速到最大的时候速度与其刚开始减速时的速度一样 ,具体看下图:

根据上图,我们只要修改步数就可以修改加速度的数值, 所以有以下公式:

当初始速度和末速度都为0并且给定步数时,为了得到加速的步数,将公式12整理得:

- 将公式12写到这里;
- 将等式两端分别加上同一项,保证等式;
- 将等式两端分别提出公因式;
- 将左侧除n1外多余的项移到右侧;即可得到公式13;
算法理论实现¶
由于算法在计算过程中涉及到一些浮点型运算,大量的浮点型运算会使得效率大大降低为了使在计算浮点型的速度得到更好的优化, 所以这一小节主要讲解由算法到代码的一些变量参数的放大转换过程和一些相关算法的不同情况。

控制步进电机需要四个描述速度曲线的参数;速度曲线从零速度开始,加速到给定速度并持续到减速开始,并且最后减速至零给定步数的速度。
- step 需要移动的步数
- accel 加速度
- decel 减速度
- speed 最大速度
设置计算¶
最小间隔
根据前几小节可有一下公式:

在上图中最终得出的是间隔时间与速度的函数关系式;其中步距角与定时器的频率为定值,所以说速度与脉冲时间间隔成反比;

在这里将步距角与定时器的频率放大100倍,并将数值赋值给变量 A_T_x100 所以最小的时间间隔的公式就为 min_delay=A_T_x100/speed ;
C0
以下是加速度相关的参数:



以上两个关于C0的是带入参数后的式子和原始的式子,在 step_delay 中参数 T1_FREQ_148 矫正了误差并且将其缩 小10的两次方倍,将A_SQ放大10十次方倍,由于放大倍数是在根号下放大的10的10次方,开根号后就是5次方,加速度也放大10的二次方倍, 在除以100,正好就是与原始相等;(具体运算如下图)

- 步骤(1)中是原始式子
- 步骤(2)矫正误差并且将放大的倍数分解
- 步骤(3)整理分解的倍数
- 步骤(4)最后的结果只与原始有误差矫正的区别
以上的整理说明,即使放大或者缩小了部分参数的倍数只要保证结果不变,会给计算带来很大的便利。
加减速情况分析¶
对于加减速的情况来说,由于已经设定好了步进电机加速度、减速度、最大速度和步数,所以说一共分为两种情况:
第一种情况:持续加速到最大速度然后再减速到0
第二种情况:在没达到最大速度之前就需要开始减速到0
第一种情况¶

根据上图可以很明显的看到7个参数,其中
- speed: 算法设置的最大速度;
- accel:加速度;
- decel:加速度;
- step :总步数;
以上的参数都是程序里面直接给出的,不需要求解。
- max_s_lim:速度从0加速到speed所需的步数;
- accel_lim:在忽略虽大速度的情况下,开始减速之前的步数,也可以理解为加速度曲线与减速度曲线的交点;
- decel_val:实际减速的步数;
以上的参数都是需要根据前面的计算推导求解的。
max_s_lim:
根据速度与路程的物理公式,所以有以下公式:

并将其图中相关参数带入,具体如下图:

注:speed是扩大100倍后的数值,那么平方就是10000倍,所以分子需要乘以100,才能保证结果不变
accel_lim:
最大的加速步数公式推导可以参考 加减速度与步数的关系 章节;

如果 max_s_lim <accel_lim ,则通过达到所需速度来限制加速度;所以 减速度取决于此,在这种情况下,通过以下方法找到decal_val:
decel_val:
根据公式12可以直接推出decel_val的表达式;但是由于是减速度的步数,所以需要带上负号,具体公式如下图:

第二种情况¶

这种情况是在还未达到最大速度时就已经开始减速了;其中 accel_lim、max_s_lim 不需要重复计算了;
当 max_s_lim>accel_lim 时,如上图加速受减速开始的限制,所以 decel_val 表达式为:

中断状态区分¶

上图表现的是速度在数学模型中的几个阶段性速度,具体看上图。

上图是这几个状态机之间切换的的关系图:
- 第一种情况 :当步数为1时,毫无疑问直接进入到减速阶段然后到停止状态
- 第二种情况 :当步数大于1,并且会加到最大速度,会经过:加速状态->匀速状态->减速状态->停止状态
- 第三种情况 :当步数大于1,并且不会加到最大速度,会经过:加速状态->减速状态->停止状态
对于加减速的每一步来说,都需要重新计算下一步的时间,计算的过程中可能会出现除不尽的项式, 为了更有利的加减速,可以采用加速向上取整,减速向下取整的原则来做运算,也可以采用余数累计的方法, 在这里使用的是将余数累计的方法来提高间隔时间的精度和准确性。
根据公式11可有:

梯形加减速算法实现¶
硬件设计¶
提高部分的线路连接与基础部分的线路连接是完全一样的,所以硬件的部分可以直接参考: 基础部分-步进电机基础旋转控制-硬件设计
软件设计¶
编程要点
- 通用GPIO配置
- 步进电机、定时器中断初始化
- 在定时器中对速度和状态进行决策
- 通过对步进电机的步数、加减速度和最大速度的设置来决定步进电机的运动
梯形加减速算法是基于基础旋转的延伸控制方式,所以,相关的基础部分可以直接参考基础部分步进电机控制的教程;
宏定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | #ifndef __BSP_STEP_MOTOR_INIT_H
#define __BSP_STEP_MOTOR_INIT_H
#include "stm32f4xx.h"
#include "stm32f4xx_hal.h"
#include "./stepper/bsp_stepper_T_speed.h"
/*宏定义*/
/*******************************************************/
//宏定义对应开发板的接口 1 、2 、3 、4
#define CHANNEL_SW 1
#if(CHANNEL_SW == 1)
//Motor 方向
#define MOTOR_DIR_PIN GPIO_PIN_1
#define MOTOR_DIR_GPIO_PORT GPIOE
#define MOTOR_DIR_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 使能
#define MOTOR_EN_PIN GPIO_PIN_0
#define MOTOR_EN_GPIO_PORT GPIOE
#define MOTOR_EN_GPIO_CLK_ENABLE() __HAL_RCC_GPIOE_CLK_ENABLE()
//Motor 脉冲
#define MOTOR_PUL_IRQn TIM8_CC_IRQn
#define MOTOR_PUL_IRQHandler TIM8_CC_IRQHandler
#define MOTOR_PUL_TIM TIM8
#define MOTOR_PUL_CLK_ENABLE() __TIM8_CLK_ENABLE()
#define MOTOR_PUL_PORT GPIOI
#define MOTOR_PUL_PIN GPIO_PIN_5
#define MOTOR_PUL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOI_CLK_ENABLE()
#define MOTOR_PUL_GPIO_AF GPIO_AF3_TIM8
#define MOTOR_PUL_CHANNEL_x TIM_CHANNEL_1
#define MOTOR_TIM_IT_CCx TIM_IT_CC1
#define MOTOR_TIM_FLAG_CCx TIM_FLAG_CC1
#elif(CHANNEL_SW == 2)
... ...
#elif(CHANNEL_SW == 3)
... ...
#elif(CHANNEL_SW == 4)
... ...
#endif
#endif
|
以上是在板子上步进电机的四个接口,(由于篇幅有限,只写了一部分具体开源码)为了方便使用,在这里全都定义完,并且可以使用宏定义 CHANNEL_SW 直接修改数值为1、2、3、4就可以直接修改对应的开发板通道,然后对应接在上面即可。
对于加减速来说有两个部分的框架很重要,分别是中断函数里面的速度决策调用和 stepper_move_T() 函数相关数值计算。
速度决策
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | /**
* @brief 速度决策
* @note 在中断中使用,每进一次中断,决策一次
* @retval 无
*/
void speed_decision()
{
uint32_t tim_count=0;
uint32_t tmp = 0;
// 保存新(下)一个延时周期
uint16_t new_step_delay=0;
// 加速过程中最后一次延时(脉冲周期).
static uint16_t last_accel_delay=0;
// 总移动步数计数器
static uint32_t step_count = 0;
static int32_t rest = 0;
//定时器使用翻转模式,需要进入两次中断才输出一个完整脉冲
static uint8_t i=0;
if(__HAL_TIM_GET_IT_SOURCE(&TIM_TimeBaseStructure, MOTOR_TIM_IT_CCx) !=RESET)
{
// 清楚定时器中断
__HAL_TIM_CLEAR_IT(&TIM_TimeBaseStructure, MOTOR_TIM_IT_CCx);
// 设置比较值
tim_count=__HAL_TIM_GET_COUNTER(&TIM_TimeBaseStructure);
tmp = tim_count+srd.step_delay;
__HAL_TIM_SET_COMPARE(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x,tmp);
i++; // 定时器中断次数计数值
if(i==2) // 2次,说明已经输出一个完整脉冲
{
i=0; // 清零定时器中断次数计数值
switch(srd.run_state)
{
/*步进电机停止状态*/
case STOP:
step_count = 0; // 清零步数计数器
rest = 0; // 清零余值
// 关闭通道
TIM_CCxChannelCmd(MOTOR_PUL_TIM, MOTOR_PUL_CHANNEL_x, TIM_CCx_DISABLE);
__HAL_TIM_CLEAR_FLAG(&TIM_TimeBaseStructure, MOTOR_TIM_FLAG_CCx);
status.running = FALSE;
break;
/*步进电机加速状态*/
case ACCEL:
step_count++;
srd.accel_count++;
new_step_delay = srd.step_delay - (((2 *srd.step_delay) + rest)/(4 * srd.accel_count + 1));//计算新(下)一步脉冲周期(时间间隔)
rest = ((2 * srd.step_delay)+rest)%(4 * srd.accel_count + 1);// 计算余数,下次计算补上余数,减少误差
//检查是够应该开始减速
if(step_count >= srd.decel_start) {
srd.accel_count = srd.decel_val;
srd.run_state = DECEL;
}
//检查是否到达期望的最大速度
else if(new_step_delay <= srd.min_delay) {
last_accel_delay = new_step_delay;
new_step_delay = srd.min_delay;
rest = 0;
srd.run_state = RUN;
}
break;
/*步进电机最大速度运行状态*/
case RUN:
step_count++;
new_step_delay = srd.min_delay;
//检查是否需要开始减速
if(step_count >= srd.decel_start)
{
srd.accel_count = srd.decel_val;
//以最后一次加速的延时作为开始减速的延时
new_step_delay = last_accel_delay;
srd.run_state = DECEL;
}
break;
/*步进电机减速状态*/
case DECEL:
step_count++;
srd.accel_count++;
new_step_delay = srd.step_delay - (((2 * srd.step_delay) + rest)/(4 * srd.accel_count + 1)); //计算新(下)一步脉冲周期(时间间隔)
rest = ((2 * srd.step_delay)+rest)%(4 * srd.accel_count + 1);// 计算余数,下次计算补上余数,减少误差
//检查是否为最后一步
if(srd.accel_count >= 0)
{
srd.run_state = STOP;
}
break;
}
/*求得下一次间隔时间*/
srd.step_delay = new_step_delay;
}
}
}
|
- 8~18行 在函数内部定义临时变量和静态变量,用于中断内算法的相关计算推导;
- 20~23行 判断当前是否为 TIM_TimeBaseStructure 中断的通道1;
- 26~28行 在26行中使用HAL库函数 __HAL_TIM_GET_COUNTER() 来获取当前计数器的数值,并且将其返回给 tim_count 并且计算下一次需要的数值,使用 __HAL_TIM_SET_COMPARE() 设置比较值;
- 30~33行 由于进入中断两次才能输出一个完整脉冲,所以在这只对进入中断的次数进行一个偶数化;
- 37~45行 接下来这部分就是对步进电机的运行状态的分析,在37~45行是 STOP 状态,在停止状态主要是关闭当前步进电机的通道以及清除中断标志位;
- 47~65行 这部分是加速状态,在加速状态中需要时刻计算下一次的脉冲间隔时间,由于加减速分为两种情况,这两种情况可以参考 加减速情况分析 所以需要判断当前的步数是否到达了需要减速步数或者已经达到了设置的最大速度需要开始减速了,根据不同条件判断下一状态;
- 67~81行 这部分是以最大速度运行的状态;如果说在加速阶段判断下一阶段可以达到最大速度,那么就会跳转到这个状态中,那么这个状态的下一状态一定是减速, 所以说需要在这部分使用步数 step_count 的条件来判断是否到达了减速阶段;
- 83~94行 这部分是以减速度运行的状态,有可能是从匀速状态或者是加速状态跳转过来的,并且求得下一次的脉冲时间间隔;
- 97行 将新求得的间隔时间赋值给结构体成员,方便下一次调用;
如果有不懂的可以在详细看一下上一章节的 梯形加减速算法原理详解
stepper_move_T
stepper_move_T() 这个函数主要是对给定步数和加减速度等参数的计算,将加减速整个过程的最大速度位置最小速度位置以及到达加减速区域的步数等。 具体的代码实现,可以看以下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | /*! \brief 以给定的步数移动步进电机
* 通过计算加速到最大速度,以给定的步数开始减速
* 如果加速度和减速度很小,步进电机会移动很慢,还没达到最大速度就要开始减速
* \param step 移动的步数 (正数为顺时针,负数为逆时针).
* \param accel 加速度,如果取值为100,实际值为100*0.01*rad/sec^2=1rad/sec^2
* \param decel 减速度,如果取值为100,实际值为100*0.01*rad/sec^2=1rad/sec^2
* \param speed 最大速度,如果取值为100,实际值为100*0.01*rad/sec=1rad/sec
*/
void stepper_move_T( int32_t step, uint32_t accel, uint32_t decel, uint32_t speed)
{
//达到最大速度时的步数.
unsigned int max_s_lim;
//必须开始减速的步数(如果还没加速到达最大速度时)。
unsigned int accel_lim;
/*根据步数和正负判断*/
if(step == 0)
{
return ;
}
else if(step < 0)//逆时针
{
srd.dir = CCW;
step = -step;
}
else//顺时针
{
srd.dir = CW;
} // 输出电机方向
MOTOR_DIR(srd.dir);
// 如果只移动一步
if(step == 1)
{
// 只移动一步
srd.accel_count = -1;
// 减速状态
srd.run_state = DECEL;
// 短延时
srd.step_delay = 1000;
// 配置电机为运行状态
status.running = TRUE;
}
// 步数不为零才移动
else if(step != 0)
{
// 设置最大速度极限, 计算得到min_delay用于定时器的计数器的值。
// min_delay = (alpha / tt)/ w
srd.min_delay = (int32_t)(A_T_x10/speed);
// 通过计算第一个(c0) 的步进延时来设定加速度,其中accel单位为0.1rad/sec^2
// step_delay = 1/tt * sqrt(2*alpha/accel)
// step_delay = ( tfreq*0.676/10 )*10 * sqrt( (2*alpha*100000) / (accel*10) )/100
srd.step_delay = (int32_t)((T1_FREQ_148 * sqrt(A_SQ / accel))/10);
// 计算多少步之后达到最大速度的限制
// max_s_lim = speed^2 / (2*alpha*accel)
max_s_lim = (uint32_t)(speed*speed/(A_x200*accel/10));
// 如果达到最大速度小于0.5步,我们将四舍五入为0
// 但实际我们必须移动至少一步才能达到想要的速度
if(max_s_lim == 0)
{
max_s_lim = 1;
}
// 计算多少步之后我们必须开始减速
// n1 = (n1+n2)decel / (accel + decel)
accel_lim = (uint32_t)(step*decel/(accel+decel));
// 我们必须加速至少1步才能才能开始减速.
if(accel_lim == 0)
{
accel_lim = 1;
}
// 使用限制条件我们可以计算出第一次开始减速的位置
//srd.decel_val为负数
if(accel_lim <= max_s_lim)
{
srd.decel_val = accel_lim - step;
}
else{
srd.decel_val = -(max_s_lim*accel/decel);
}
// 当只剩下一步我们必须减速
if(srd.decel_val == 0)
{
srd.decel_val = -1;
}
// 计算开始减速时的步数
srd.decel_start = step + srd.decel_val;
// 如果最大速度很慢,我们就不需要进行加速运动
if(srd.step_delay <= srd.min_delay)
{
srd.step_delay = srd.min_delay;
srd.run_state = RUN;
}
else
{
srd.run_state = ACCEL;
}
// 复位加速度计数值
srd.accel_count = 0;
status.running = TRUE;
}
/*获取当前计数值*/
int tim_count=__HAL_TIM_GET_COUNTER(&TIM_TimeBaseStructure);
/*在当前计数值基础上设置定时器比较值*/
__HAL_TIM_SET_COMPARE(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x,tim_count+srd.step_delay);
/*使能定时器通道*/
TIM_CCxChannelCmd(MOTOR_PUL_TIM, MOTOR_PUL_CHANNEL_x, TIM_CCx_ENABLE);
MOTOR_EN(ON);
}
|
- 11~15行 定义最大速度需要的步数和开始减速的步数变量;
- 17~30行 这部分是对步数的判断和方向的设置;
- 46~106行 这部分是针对加减速模型所需要的的一些计算;具体推导过程可以参考 算法基础概念及方程 章节中的内容;
- 108~113行 获取当前计数值,根据计算设置第一次的比较值并开启使能驱动器;
下载验证¶
保证开发板相关硬件连接正确,并且将代码下载到开发板中,会发现电机正转2圈后反转两圈。
步进电机S形加减速实现¶
在上一章节已经对步进电机的梯形加减速进行了非常详细的推导和讲解,接下来会对另一种加减速进行讲解—S形加减速。
S形加减速可以理解为在加减速的变化过程中速度曲线呈现一个英文字母“S”形的加减速算法。那么已经有了梯形加减速算法了, 为什么还需要S形加减速算法呢?答案很明显,由于不同的加减速算法的特点是不一样的,所以带来的效果自然而然的就不同了。
梯形加减速 在启动、停止和高速运动的过程中会产生很大的冲击力振动和噪声,所以多数会应用于简单的定长送料的应用场合中, 例如常见的3D打印机使用的就是梯形加减速算法;但是相比较 S形加减速 在启动停止以及高速运动时的速度变化的比较慢, 导致冲击力噪音就很小,但这也决定了他在启动停止时需要较长的时间,所以多数适用于精密的工件搬运与建造。
S形加减速原理分析¶
"S"模型解析¶
S形加减速算法对于曲线并没有具体的限定和轨迹,可以是指数函数、正弦函数只要满足于速度的变化曲线是一个“S”形即可;

具体的曲线情况如上图。在上图中一共有两幅曲线图像,其中红色的是速度的曲线,可以看出整体都属于速度的上升阶段, 在加速的过程中一共可以分为两个阶段,分别为前半部分和后半部分,前半部分是加速度匀速递增的曲线,称为: 加加速阶段 曲线,后半部分是加速度匀速递减的曲线超,所以称为: 减加速度阶段 曲线。
上图中蓝色的曲线是加速度的变化曲线,按照速度变化的规律共分成前半段加速度匀速递增和后半部分加速度匀速递减, 也可以简单理解为一次函数,前半段一次函数的斜率是大于0的,后半部分的斜率是小于0的;加速度从0开始变化, 到了最大值开始减小,最后为0,由于加速度的斜率是相同的,所以斜率大于0和小于0两段曲线是关于加速度最大值所对应的速度中心对称的。
算法理论实现¶
下图共有三条曲线分别是红色、青色和蓝色,其中红色速度曲线、蓝色加速度曲线,青色为梯形加减速模型的加速部分曲线。

图中是梯形加速度部分(青色曲线)和S形加速部分(红色曲线)比较,梯形加减速是按照一个固定的斜率增加速度到达Vt, 到达Vt后加速部分结束,开始进入匀速部分,梯形加减速由匀加速上升的趋势突然变成匀速,由于惯性会产生较大的冲击力和噪声; S形加减速则很好的避免了这一问题,在加速到Vt后进入匀速阶段曲线上非常的顺滑,阶段衔接的相对完美。
主要的目的是通过初速度、末速度和时间三个变量计算出控制步进电机的每一步速度;
基础公式推导
加速度 a 从0变化到最大值有

其中 k 是图中加速度的斜率,t 为时间变化,所以有:

对加速度积分就可以得到速度,所以有:

根据公式三可得:

如上图所示:当 加速度a 随着时间变化到最大值时速度 V=Vm,并且初速度 V0=0,将这两个条件带入公式4, 可求得 斜率k:

对速度积分就可以得到位移,所以有:

其中:
- 对速度进行积分;
- 将 速度v 的表达式带入;
- 求解速度积分的计算结果,并得到位移与时间的公式,即 公式6;
根据 公式六 可以很清楚的看到只要给定一个合适的t值, 比如1秒内加速1000次,也就是 t1=1/1000 那就可以得到一个比较平滑的速度曲线。
速度推导
步进电机的速度是由定时器脉冲的频率决定的,频率越快也就是步进电机的速度越快, 所以可以得出 脉冲输出的频率=速度,所以有:

所以说只要计算出第一步的周期时间就可以计算出下一步的速度;并且根据时间的变化每计算一次就会累加一次时间变化量 Sumt。
在上图中:
- T 代表但脉冲周期
- f 代表频率
- speed 代表速度
每一步的速度都是在初始速度的基础上计算的,所以说 公式4 计算的是当 初始速度为0时 的增量,具体公式如下:

- t时刻的速度等于初始速度与增量速度的和;
- 求解增量速度,并且将累计时间变量带入 公式4;
- 带入求解t时刻的速度;
S形加减速算法实现¶
硬件设计¶
提高部分的线路连接与基础部分的线路连接是完全一样的,所以硬件的部分可以直接参考: 基础部分-步进电机基础旋转控制-硬件设计
软件设计¶
编程要点
- 通用GPIO配置
- 步进电机、定时器中断初始化
- 计算S加减速的速度表
- 在定时器中对状态进行决策
- 通过对步进电机的初始速度、末速度和加速时间的设置来决定步进电机的运动
S形加减速算法是基于基础旋转延伸的控制方式,所以,相关的基础部分可以直接参考基础部分步进电机控制的教程; 在这里不再重复讲解。
计算加减速速度表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /**
* @brief 初始化状态并且设置第一步的速度
* @param 无
* @param 无
* @note 无
* @retval 无
*/
void stepper_start_run()
{
/*初始化结构体*/
memset(&Stepper,0,sizeof(Stepper_Typedef));
/*初始电机状态*/
Stepper.status=ACCEL;
/*初始电机位置*/
Stepper.pos=0;
/*计算第一次脉冲间隔*/
if(Speed.Form[0] == 0) //排除分母为0的情况
Stepper.pluse_time = 0xFFFF;
else //分母不为0的情况
Stepper.pluse_time = (uint32_t)(T1_FREQ/Speed.Form[0]);
/*获取当前计数值*/
uint32_t temp=__HAL_TIM_GET_COUNTER(&TIM_TimeBaseStructure);
/*在当前计数值基础上设置定时器比较值*/
__HAL_TIM_SET_COMPARE(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x,temp +Stepper.pluse_time);
/*开启中断输出比较*/
HAL_TIM_OC_Start_IT(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x);
/*使能定时器通道*/
TIM_CCxChannelCmd(MOTOR_PUL_TIM, MOTOR_PUL_CHANNEL_x, TIM_CCx_ENABLE);
}
|
- 第12-16行:初始化 Stepper 结构体,初始化电机状态以及电机位置;
- 第18-23行:计算第一次的脉冲时间间隔,也就是利用 定时器分频后的主频与Speed.Form[0] 做比值,但是这里需要考虑分母为0的情况 如果分母为0,那么直接将脉冲时间赋值为0xffff;
- 第25-31行:获取当前定时器的计数值并且与脉冲时间累加,然后作为参数传入 __HAL_TIM_SET_COMPARE 设置下一次进入中断的时间。 并开启中断比较输出。
计算加减速速度表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | /**
* @brief CalcSpeed
* @param Vo 初始速度
* @param Vt 末速度
* @param T 时间
* @note 无
* @retval 无
*/
void CalcSpeed(int32_t Vo, int32_t Vt, float T)
{
uint8_t Is_Dec = FALSE;
int32_t i = 0;
int32_t Vm =0; // 中间点速度
float K = 0; // 加加速度
float Ti = 0; // 时间间隔 dt
float Sumt = 0; // 时间累加量
float DeltaV = 0; // 速度的增量dv
/***************************第一部分************************************************/
/*判断初速度和末速度的关系,来决定加减速*/
if(Vo > Vt )
{
Is_Dec = TRUE;
Speed.Vo = CONVER(Vt);
Speed.Vt = CONVER(Vo);
}
else
{
Is_Dec = FALSE;
Speed.Vo = CONVER(Vo);
Speed.Vt = CONVER(Vt);
}
/****************************第二部分***********************************************/
/*计算初始参数*/
T = T / 2; //加加速段的时间(加速度斜率>0的时间)
Vm = (Speed.Vo + Speed.Vt) / 2; //计算中点的速度
K = fabs(( 2 * ((Vm) - (Speed.Vo)) ) / pow((T),2));// 根据中点速度计算加加速度
Speed.INC_AccelTotalStep = (int32_t)( ( (K) * pow( (T) ,3) ) / 6 );// 加加速需要的步数
Speed.Dec_AccelTotalStep = (int32_t)(Speed.Vt * T - Speed.INC_AccelTotalStep); // 减加速需要的步数 S = Vt * Time - S1
/***************************第三部分************************************************/
/*计算共需要的步数,并校检内存大小,申请内存空间存放速度表*/
Speed.AccelTotalStep = Speed.Dec_AccelTotalStep + Speed.INC_AccelTotalStep; // 加速需要的步数
if( Speed.AccelTotalStep % 2 != 0) // 由于浮点型数据转换成整形数据带来了误差,所以这里加1
Speed.AccelTotalStep += 1;
/*判断内存长度*/
if(FORM_LEN<Speed.AccelTotalStep)
{
printf("FORM_LEN 缓存长度不足\r\n,请将 FORM_LEN 修改为 %d \r\n",Speed.AccelTotalStep);
return ;
}
/***************************第四部分************************************************/
/* 计算第一步的时间 */
/*根据第一步的时间计算,第一步的速度和脉冲时间间隔*/
/*根据位移为0的时候的情况,计算时间的关系式 -> 根据位移和时间的公式S = 1/2 * K * Ti^3 可得 Ti=6 * 1 / K开1/3次方 */
Ti = pow((6.0f * 1.0f / K),(1 / 3.0f) ); //开方求解 Ti 时间常数
Sumt += Ti;//累计时间常数
/*根据V=1/2*K*T^2,可以计算第一步的速度*/
DeltaV = 0.5f * K * pow(Sumt,2);
/*在初始速度的基础上增加速度*/
Speed.Form[0] = Speed.Vo + DeltaV;
/****************************第五部分***********************************************/
/*最小速度限幅*/
if( Speed.Form[0] <= MIN_SPEED )//以当前定时器频率所能达到的最低速度
Speed.Form[0] = MIN_SPEED;
/****************************第六部分***********************************************/
/*计算S形速度表*/
for(i = 1; i < Speed.AccelTotalStep; i++)
{
/*根据时间周期与频率成反比的关系,可以计算出Ti,在这里每次计算上一步时间,用于积累到当前时间*/
Ti = 1.0f / Speed.Form[i-1];
/* 加加速度计算 */
if( i < Speed.INC_AccelTotalStep)
{
/*累积时间*/
Sumt += Ti;
/*速度的变化量 dV = 1/2 * K * Ti^2 */
DeltaV = 0.5f * K * pow(Sumt,2);
/*根据初始速度和变化量求得速度表*/
Speed.Form[i] = Speed.Vo + DeltaV;
/*为了保证在最后一步可以使得时间严谨的与预期计算的时间一致,在最后一步进行处理*/
if(i == Speed.INC_AccelTotalStep - 1)
Sumt = fabs(Sumt - T );
}
/* 减加速度计算 */
else
{
/*时间累积*/
Sumt += Ti;
/*计算速度*/
DeltaV = 0.5f * K * pow(fabs( T - Sumt),2);
Speed.Form[i] = Speed.Vt - DeltaV;
}
}
/**************************第七部分*************************************************/
/*减速运动,倒序排列*/
if(Is_Dec == TRUE)
{
float tmp_Speed = 0;
/* 倒序排序 */
for(i = 0; i< (Speed.AccelTotalStep / 2); i++)
{
tmp_Speed = Speed.Form[i];
Speed.Form[i] = Speed.Form[Speed.AccelTotalStep-1 - i];
Speed.Form[Speed.AccelTotalStep-1 - i] = tmp_Speed;
}
}
}
|
- 第一部分: 根据传入的参数判断,是加速还是减速;如果初始速度小于末速度那么就是加速运动,如果初始速度大于末速度那么就是 减速度运动,并将状态变量 Is_Dec 对应修改。
- 第二部分: 计算初始算法相关的基础公式,其中 T 计算的是斜率大于0的时间;Vm 是根据初末速度计算的中点速度; K 计算的是斜率;Speed.INC_AccelTotalStep 和 Speed.Dec_AccelTotalStep 计算的分别是加加速度时的所需步数和总步数; 具体的公式推导过程可以参考 算法理论实现 的基础公式推到部分。
- 第三部分:根据加加速运动和减加速运动可以计算出一共所需的步数,并判断数组是否可以装下这些数据,如果不可以,提示修改。
- 第四部分:计算 Speed.Form[0] :根据公式6可直接推导出时间 Ti 的数值,并且累计时间常数带入到 v-t 的关系式中,求得△t的数值。 并根据初始量与变化量的关系即可求出 Speed.Form[0] 。
- 第五部分:判断 Speed.Form[0] 最小速度的数值大小,不能小于 定时器的频率与最大计数值的比值。
- 第六部分:根据前面的总步数和算出每一步的 Speed.Form[i] 数值
- 第七部分:由于中心对称的关系,如果为减速运动只需将之前计算的数值倒序排列即可。
速度决策
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | /**
* @brief 速度决策
* @note 在中断中使用,每进一次中断,决策一次
* @retval 无
*/
void speed_decision(void)
{
/*计数临时变量*/
float temp_p = 0;
/*脉冲计数*/
static uint8_t i = 0;
if(__HAL_TIM_GET_IT_SOURCE(&TIM_TimeBaseStructure, MOTOR_TIM_IT_CCx) !=RESET)
{
/*清除定时器中断*/
__HAL_TIM_CLEAR_IT(&TIM_TimeBaseStructure, MOTOR_TIM_IT_CCx);
/******************************************************************/
/*两次为一个脉冲周期*/
i++;
if(i == 2)
{
/*脉冲周期完整后清零*/
i = 0;
/*判断当前的状态,*/
if(Stepper.status == ACCEL || Stepper.status == DECEL)
{
/*步数位置索引递增*/
Stepper.pos++;
if(Stepper.pos < Speed.AccelTotalStep )
{
/*获取每一步的定时器计数值*/
temp_p = T1_FREQ / Speed.Form[Stepper.pos];
if((temp_p / 2) >= 0xFFFF)
temp_p = 0xFFFF;
Stepper.pluse_time = (uint16_t) (temp_p / 2);
}
else
{
/*加速部分结束后接下来就是匀速状态或者停止状态*/
if(Stepper.status == ACCEL)
{
Stepper.status = AVESPEED;
}
else
{
/*停止状态,清空速度表并且关闭通道*/
Stepper.status = STOP;
memset(Speed.Form,0,sizeof(float)*FORM_LEN);
TIM_CCxChannelCmd(MOTOR_PUL_TIM, MOTOR_PUL_CHANNEL_x, TIM_CCx_DISABLE);// 使能定时器通道
}
}
}
}
/**********************************************************************/
// 获取当前计数器数值
uint32_t tim_count=__HAL_TIM_GET_COUNTER(&TIM_TimeBaseStructure);
/*计算下一次时间*/
uint32_t tmp = tim_count+Stepper.pluse_time;
/*设置比较值*/
__HAL_TIM_SET_COMPARE(&TIM_TimeBaseStructure,MOTOR_PUL_CHANNEL_x,tmp);
}
}
|
- 第8~12行:定义了脉冲计数以及临时变量;
- 第13~17行:判断是否有新的中断到来,如果有便清楚定时器的中断标志;
- 第20~24行:进入两次中断才能获得一个完整的脉冲周期,所以在这里进行一个脉冲偶数化清零;
- 第29~37行:在 Speed.Form[] 表里按照顺序获取数值,并与定时器分频后的时钟主频做比值求得脉冲时间;
- 第40~52行:加速结束后共有两个状态分别是匀速状态和停止状态,匀速状态即保持当前速度不变继续运行,停止状态即清空速度表并关闭通道;
- 第57~62行:获取当前定时器的计数值并且与上文计算的脉冲时间累加,然后作为参数传入 __HAL_TIM_SET_COMPARE 设置下一次进入中断的时间。
下载验证¶
保证开发板相关硬件连接正确,并且将代码下载到开发板中,按下 KEY2 按键会发现步进电机先加速运动再减速运动。
步进电机直线插补实现¶
步进电机圆弧插补实现¶
无刷电机速度环控制(BLDC)¶
永磁同步电机(PMSM)的闭环控制详解¶
常见问题¶
版权说明¶
野火电子保留本项目的所有版权。
公司组织有生存的压力,因为开源而导致生存不下去,是有GEEK精神的人不愿看到的事情。 由于我们还不熟悉各种版权条例,所以关于版权的问题还在选择中。
目前我们保留本项目的所有版权,但我们秉承开源的心是不变的。 如果你使用本项目不是用于商业目的,基本不需要考虑版权问题。
我们主要担忧的商业目的如下,列出来的意思是如果用于以下目的,我们极有可能会追究版权。
- 同业竞争,目前开发板是我们主要的盈利来源,禁止把本项目用于其它开发板项目。
- 文档出版,项目中包含的文档我们随时会提交至出版社出版,所以我们保留文档出版的权利。 如果你只是在自己的文章中使用了本项目文档中的内容,需要注明来源。