注:笔记主要参考B站 江科大自化协 教学视频“STM32入门教程-2023持续更新中”。
注:工程及代码文件放在了本人的Github仓库。
定时器是STM32中功能最强大、结构最复杂的一个外设。定时器将包括四部分8小节:
TIM(Timer)定时器 最基本的功能 是对输入的时钟进行计数,并在计数值达到设定值时触发中断。若这个输入是一个可靠的基准时钟,那么对这个基准时钟技术就实现了计时的功能。
如STM32中主频一般是72MHz,那么计数值设定为72,就是每1us触发一次中断;计数值设定为72000,就是每1ms触发一次中断。
STM32定时器拥有由16位 计数器、预分频器、自动重装寄存器 组成的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时。TIM不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、主从触发模式、输出比较、编码器接口等多种功能,并且根据复杂度和应用场景分为了高级定时器、通用定时器、基本定时器三种类型。
类型 | 编号 | 总线 | 功能 |
---|---|---|---|
高级定时器【最复杂】 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器【最常用】 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器【最简单】 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能 |
- 只有高级定时器连接的是性能最高的APB2总线,剩下的通用定时器和基本定时器都是APB1总线。
- 高级定时器额外多出的功能主要是为了三相无刷电机的驱动设计的,本课程不会涉及到。
- 不同型号芯片的定时器数量不同,stm32f103c8t6定时器资源:TIM1、TIM2、TIM3、TIM4。没有基本定时器。
下面依次介绍上面三种类型的定时器。
- 内部时钟(CK_CNT):一般就是系统的主频72MHz,通向时基单元的输入。
- 时基单元:16位预分频器 + 16位计数器 + 16位自动重装载寄存器。
- 预分频器:对输入的72MHz时钟进行预分频,寄存器内存储的值是实际的分频系数减一。写0就是不分频,写1就是2分频,写2就是3分频……
- 计数器:对预分频后的计数时钟进行计数,每遇到上升沿就加一。
- 自动重装载寄存器:存储计数的最大值,到达此值后触发中断并清零计数器。
- 折线UI:向上的折线箭头表示该位置会产生中断信号——“更新中断”(由计数值等于自动重装值产生的中断),这个中断信号会通向NVIC。
- 折线U:向下的折线箭头表示该位置会产生事件——“更新事件”,这个更新事件不会触发中断,但可以触发内部其他电路的工作。
主模式触发DAC:
- stm32的一大特色就是主从触发模式,可以让内部的硬件在不受程序的控制下自动运行,可以极大地减轻CPU的负担。
- 驱动DAC正常思路及其问题:每隔一段时间就产生一个定时中断,手动更新DAC的值。但这样子会频繁的产生中断,会影响主程序的运行和其他中断的响应。
- 解决方法:定时器设计了一个主模式,使用主模式可以将定时器的“更新事件”映射到“触发输出TRGO”,然后TRGO直接接到DAC的触发转换引脚上,于是定时器的更新就不需要中断来实现了。整个过程无需软件参与,实现了硬件的自动化。
注:除了主模式外,还有更多的硬件自动化设计。
- 时基单元:中间的PSC预分频器、自动重装载寄存器、CNT计数器。通用定时器和高级定时器新增两个功能——“向下计数模式”、“中央计数模式”。
- 向上计数模式【常用】:从0开始累加,到自动重装载值触发中断。
- 向下计数模式:从自动重装载值递减,到0触发中断。
- 中央对齐模式:从0开始累加,到自动重装载值触发中断,然后递减,到0再次触发中断。常用于电机控制的SVPWM算法中。
- 内外时钟源选择和主从触发模式结构:上面的一大块。下面介绍各种各样的内外时钟源:
- 内部时钟CK_INT【常用】:通常为72MHz,基本定时器只能选择CK_INT,通用定时器和高级定时器则新增了下面的时钟源。
- 外部TIMx_ETR【常用】:引脚的位置可以参考引脚定义表,如stm32f103c8t6的PA0引脚复用了TIM2_CH1_ETR等。外部输入了方波时钟,然后通过极性选择、滤波等电路进行整形,然后兵分两路,一路ETRF进入触发控制器,紧跟着就可以选择成为时基单元的时钟(外部时钟模式2);一路进入选择器等待成为TRGI。
- TRGI主要用作触发输入来使用,可以触发定时器的从模式,本小节仅用做外部时钟(外部时钟模式1),其他功能后续再讲。
- 外部ITR信号:包括ITR0~ITR4(引脚定义见参考手册“表78 TIMx内部触发连接”),来自其他定时器,实现了定时器的级联。这个ITR信号从上一级定时器的主模式TRGO引脚来(图片右上方)。
- 外部TI1F-ED:来自于输入捕获单元的TIMx_CH1引脚,后缀“ED”意为边沿,也就是说该路时钟的上升沿和下降沿均有效,也就是CH1引脚的边沿。
- 外部TI1FP1:来自CH1引脚时钟。
- 外部TI2FP2:来自CH2引脚时钟。
- 编码器接口:可以读取正交编码器的输出波形,后续会再介绍。
- TRGO引脚:定时器的主模式输出,可以将内部的一些事件映射到TRGO上,相比基本定时器这些事件的范围显然更广。
注:最后三种外部时钟用于输入捕获和测频率,后续介绍。
- 输出比较电路:下面右侧的一大堆,总共有4个通道,可以用于输出PWM波形驱动电机。
- 输入捕获电路:下面左侧的一大推,也是有4个通道,可以用于测量输入方波的频率。
注:输入捕获电路和输出比较电路不能同时使用,所以共用中间的“捕获/比较寄存器”以及输入/输出的引脚。后续再介绍。
相比于通用定时器,高级定时器主要增加了以下功能:对输出比较模块的升级
- 重复次数计数器:可以实现每个几个计数周期,才发生一次更新事件和更新中断,相当于自带一级的定时器级联。
- DTG(Dead Time Generate):死区生成电路。将输出引脚由原来的一个变为两个互补的输出,可以输出一对互补的有死区的PWM波(防止出现短暂的直通现象),可以驱动三相无刷电机(如四轴飞行器、电动车后轮、电钻等)。
- BRK刹车输入:为了给电机驱动提供安全保障,如果外部引脚BKIN(Break IN)产生了刹车信号或者内部时钟失效,这个电路就会自动切断电机的输出,防止意外的发生。
上图是UP主自己画的定时中断基本结构图,去掉了一些无关的东西并加了一些定时器框图中没有体现的模块,后续在配置TIM时可以直接参考本图。
- 时基单元:中间的粉色部分。
- 运行控制:控制寄存器的一些位,如启动停止、向上或向下计数等,操作这些寄存器就可以控制时基单元的运行了。
- 内部时钟模式、外部时钟模式2、外部时钟模式1:外部时钟源选择。这个选择器的输出就是为时基单元提供时钟。
- 编码器模式:编码器独有的模式,一般用不到。
- 中断申请控制:由于定时器内部有很多地方要申请中断,“中断申请控制”就用来使能控制这些中断是否使能。比如,中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。
上面三个框图中,有阴影的部分都是包含缓冲寄存器(影子寄存器)的,具体作用见下面时基单元运行时的一些细节:
- CK_PSC:预分频器的输入时钟,选择内部时钟源就是72MHz。
- CNT_EN:计数器使能。高电平计数器正常运行,低电平计数器停止。
- CK_CNT:计数器时钟,既是预分频器的时钟输出,也是计数器的时钟输入。
- 计数器寄存器:对CK_CNT进行自增计数,到达自动重装载值清零。
- 更新事件:计数器寄存器到达自动重装载值时产生一个脉冲。
下面三行时序体现了预分频计数器的一种缓冲机制:
- 预分频控制寄存器:供用户读写使用,实时响应用户控制,但并不直接决定分频系数。
- 预分频缓冲器:也称为影子寄存器,真正起分频作用的寄存器。只有在更新事件到达时,才从“预分频控制寄存器”更新预分频参数。以确保更新事件的稳定性。
- 预分频计数器:按照预分频参数进行计数,以产生对应的CK_CNT脉冲。
正常的计数器时序没啥好说的,就是根据CK_CNT计数,到达自动重装载值产生中断,所以只需看一下更新自动重装载值的过程:
- 无预装时序(禁用缓冲寄存器):有溢出问题。若将自动重装载值变小,且此时计数器寄存器已经超过这个新的重装载值,那么计数器寄存器就会一直计数到FFFF才清零。这可能会造成一些问题。
- 有预装时序(启用缓冲寄存器):比较稳定。只有更新事件来临时才更新自动重装载值。
时钟是所有外设的基础,所以是需要最先配置的。ST公司写好了
SystemInit
函数来配置时钟树,下面具体介绍:
左侧是时钟产生电路,右侧时钟分配电路,中间的SYSCLK就是72MHz的系统时钟。
时钟产生电路:
- 四个振荡源:
- 内部的8MHz高速RC振荡器。
- 外部的4-16MHz高速石英晶体振荡器:一般8MHz,相比于内部的RC高速振荡器更加稳定。
- 外部的32.768kHz低速晶振:一般给RTC提供时钟。
- 内部的40kHz低速RC振荡器:给看门狗提供时钟。
上面的两个高速晶振用于给系统提供时钟,如AGB、APB1、APB2的时钟。
- SystemInit函数配置时钟的过程:首先启动内部8MHz时钟为系统时钟,然后配置外部8MHz时钟到PLLMUL模块进行9倍频到72MHz,等到这个72MHz时钟稳定后,再将其作为系统时钟。于是就实现了系统时钟从8MHz切换到72MHz。
- CSS:监测外部时钟的运行状态,一旦外部时钟失效,就会自动把外部时钟切换成内部时钟,保障系统时钟的运行,防止程序卡死造成事故。如果外部晶振出问题,那么就会导致程序的时钟变为8MHz,也就是比预期的时钟慢了9倍。
- 时钟分配电路
- AHB总线:有预分频器,SysytemInit配置分频系数为1,于是AHB时钟输出就是72MHz。
- APB1总线:SysytemInit配置分频系数为2,于是APB1时钟输出就是36MHz。
- APB2总线:SysytemInit配置分频系数为1,于是APB2时钟输出就是72MHz。
- 外设时钟使能:就是库函数
RCC_APB2PeriphClockCmd
开启的地方,可以控制相应的外设时钟开启。- 定时器的时钟:从图中可以看出,按照SystemInit的默认配置,所有的定时器时钟都是72MHz。
需求:在OLED显示屏上显示数字,每秒自动加一。
下面是代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Timer.h"
uint16_t TimerCount = 0;
int main(void){
//配置中断的优先级分组,每个工程只能出现一次!!
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"TIM_Interrupt:");
OLED_ShowNum(2,1,0,5);
//定时器初始化
Timer_Init();
while(1){
OLED_ShowNum(2,1,TimerCount,5);
};
}
//TIM2定时中断后的中断函数
void TIM2_IRQHandler(void){
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET){
TimerCount++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
- Timer.h
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
#endif
- Timer.c
#include "stm32f10x.h" // Device header
// 定时器初始化-TIM2
void Timer_Init(void){
//1.初始化RCC内部时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//2.选择时基单元的时钟
TIM_InternalClockConfig(TIM2);//默认使用内部时钟,也可以不写
//3.配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//外部时钟源的输入滤波器采样频率,内部时钟无所谓
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//加计数
TIM_TimeBaseInitStructure.TIM_Period = 10000-1;//ARR自动重装器的值10000
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200-1;//PSC预分频的值7200
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);//消除上一行TIM_TimeBaseInit立刻产生更新事件影响
//4.配置中断输出控制
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//5.配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2 ;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//6.配置运行控制
TIM_Cmd(TIM2, ENABLE);
}
/*
//TIM2定时中断后的中断函数
void TIM2_IRQHandler(void){
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET){
??
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
*/
编程感想:
- 由于TIM中断与其他外部硬件没有关系,所以就直接放在了System文件夹。
- 复位后计数器的值从1开始而不是从0开始,说明上电初始化后TIM2就立刻中断了一次。这是因为时基单元初始化函数
TIM_TimeBaseInit
,在函数的最后生成了一个更新事件,来保证可以立刻重新装载预分频器和重复计数器的值。要消除这个影响,就在TIM_TimeBaseInit
后面加一句TIM_ClearFlag
来清除相应的中断标志位。
需求:对外部输入的方波(对射式红外传感器)进行计次,每出现9个方波就自动加一。
“定时器定时中断-外部时钟”与“定时器定时中断-内部时钟”的 代码调用关系相同。下面是代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Timer.h"
uint16_t TimerCount = 0;
int main(void){
//配置中断的优先级分组,每个工程只能出现一次!!
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"TimerCount:");
OLED_ShowNum(2,1,0,5);
OLED_ShowString(3,1,"CNT:");
OLED_ShowNum(3,5,0,5);
//定时器初始化
Timer_Init();
while(1){
OLED_ShowNum(2,1,TimerCount,5);
OLED_ShowNum(3,5,TIM_GetCounter(TIM2),5);
};
}
//TIM2定时中断后的中断函数
void TIM2_IRQHandler(void){
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET){
TimerCount++;
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
- Timer.h
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
#endif
- Timer.c
#include "stm32f10x.h" // Device header
// 定时器初始化-TIM2
void Timer_Init(void){
//1.初始化RCC内部时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.配置GPIO-PA0
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//虽然器件手册推荐浮空输入,但上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//3.选择时基单元的时钟-ETR外部时钟模式2
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_Inverted, 0x0F);
//4.配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;//外部时钟源的输入滤波器采样频率,内部时钟无所谓
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//加计数
TIM_TimeBaseInitStructure.TIM_Period = 10-1;//ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 1-1;//PSC预分频的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;//重复计数器的值(高级定时器才有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_FLAG_Update);//消除上一行TIM_TimeBaseInit立刻产生更新事件影响
//5.配置中断输出控制
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//6.配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2 ;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//7.配置运行控制
TIM_Cmd(TIM2, ENABLE);
}
/*
//TIM2定时中断后的中断函数
void TIM2_IRQHandler(void){
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET){
??
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
*/
编程感想:
- PA0配置成上拉输入:虽然手册里写TIM2的ETR时钟配置成浮空输入,但UP主说不喜欢浮空输入,因为这会导致输入电平跳个没完。只有当外部输入信号的功率很小,内部的上拉的电阻可能会影响到输入信号,此时采用浮空输入防止影响外部输入的电平。其他情况一律上拉输入。
- 关于时钟源选择函数
TIM_ETRClockMode2Config
:第三个参数设置极性,其实就是规定在外部时钟的 上升沿/下降沿 计数;第四个参数滤波器,就是设置对于外部时钟的采样情况,具体的含义可以参考器件手册“14.4.3 从模式控制寄存器(TIMx_SMCR)”中的位11:8。
TIM的 OC(Output Compare)输出比较 主要用于输出PWM波形,PWM又是驱动电机的必要条件(智能车、机器人等),所以应用广泛。输出比较功能 可以通过比较 CNT计数器 与 CCR捕获/比较寄存器 (见图6-2“通用定时器框图”)的大小,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形。
- 每个高级定时器和通用定时器都拥有4个输出比较通道。
- 高级定时器的前3个通道额外拥有死区生成和互补输出的功能。
简单介绍一下PWM,即PWM(Pulse Width Modulation)脉冲宽度调制。在 具有惯性的系统 中,可以通过对一系列脉冲的宽度进行调制,来等效地输出的模拟参量,常应用于电机控速等领域。PWM参数:
- 频率 = 1 / TS,一般在 几kHz~几十kHz。
- 占空比 = TON / TS
- 分辨率 = 占空比变化步距,也就是占空比变化的精细程度。一般1%足够使用。
注:定时中断的频率就是PWM波的频率,只不过占空比的变化范围由自动重装载值决定。
有关“通用定时器框图”已经在6.1节“TIM定时中断原理”介绍过,下面来具体介绍其中的输出比较通道的电路结构。
相比于“通用定时器”,“高级定时器”的输出比较电路增加了“死区生成器”和“互补输出电路”。
- 死区生成器:消除上下两路可能同时导通的短暂状态,以防止电路发热、功率损耗。
- 互补输出OC1N:与OC1反相,用于驱动三相无刷电机。
上图与“定时器中断”的区别在于最后输出的时候不需要“更新事件”的中断申请,而是走输出比较电路。
下面是一些参数计算:
- PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
- PWM占空比: Duty = CCR / (ARR + 1)
- PWM分辨率: Reso = 1 / (ARR + 1)
舵机是一种根据输入PWM信号 占空比来控制输出角度 的装置。
- 型号:SG90。
- 三根输入线:棕色是电源负、红色是电源正、橙色是PWM信号线。注意这个颜色因型号不同可能不同,需要查看说明手册。
- 输入PWM信号要求:周期为20ms(50Hz),高电平宽度为0.5ms~2.5ms(占空比2.5%~12.5%),脉冲控制精度为2us(0.18°,占空比精度0.01%)。
- 内部电板的基本思路:根据输入占空比得到期望角度,然后检测当前角度,若当前角度较小则顺时针转;反之则逆时针转,直到与期望角度相同。
注:这里实际上是将PWM当作一种通信协议,而不是模拟输出。
更多细节可以参考CSDN文章 “SG90舵机的使用”。
- GND:stm32地。
- 5V电源线:舵机属于大功率设备,驱动电源也期望是大功率的输出设备,驱动电源要注意和stm32开发板共地。对于本实验来说,可以使用STLINK的5V输出引脚进行供电,属于USB供电符合功率要求,另外要求不严格也可以不共地。
- PWM:作为通信线无需大功率,可以连接到stm32的某个引脚,如PA0。
直流电机是一种将电能转换为机械能的装置。
- 型号:130直流电机。
- 直流电机两个引脚:当电极正接时,电机正转;当电极反接时,电机反转。
直流电机属于大功率器件,GPIO口无法直接驱动,需要配合 电机驱动电路 来操作,如TB6612、DRV8833、L9110、L298N等。另外还有一些用分离元件(如MOS)做的驱动电路,支持更大的驱动功率。本实验采用的 TB6612 是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向。而有些芯片(如ULN2003)一路就只有一个开关管,所以只能控制电机在一个方向转,选择电机的时候注意区分。
- VM:电机电源的正极,范围4.5V~10V。要接一个可以输出大电流的电源,且电压一般与电机的额定电压保持一致。
- VCC:逻辑电平输入端,范围2.7V~5.5V。这个要与控制控制器的电源保持一致,所以采用stm32就是3.3V、采用51单片机就是5V。
- 三个GND:都是电源负极,随便选一个接地即可。
- STBY:Stand by,待机控制引脚。接地,芯片处于待机状态不工作;接逻辑电源VCC,芯片正常工作。
- PWMA、AIN1、AIN2:接在单片机GPIO引脚上,用于控制电机,控制逻辑如上图。PWMA接PWM波,AIN1、AIN2可以任意接普通的GPIO口。
- AO1、AO2:按照控制逻辑,从VM汲取电流来驱动电机。
- PWMB、BIN1、BIN2与BO1、BO2:同上述,控制另一个电机的转动。
注:根据逻辑控制真值表,这里的PWM就是用来等效成一个模拟量。
需求:实现0.5s逐渐亮、0.5s逐渐灭的呼吸灯,PA0高电平驱动。
代码展示:OLED和Delay相关代码见前面,本节略。
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "PWM.h"
#include "Delay.h"
int main(void){
uint16_t pwm_duty = 0;//PWM波的占空比
uint8_t pwm_flag = 1;//占空比变化控制信号,1升0降
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"BreathLED:");
OLED_ShowString(2,1,"Init");
//PWM初始化
PWM_Init();
while(1){
Delay_ms(5);//0.5s完成100个占空比变化
//调整占空比
if(pwm_flag==1){
if(pwm_duty<100){
pwm_duty++;
OLED_ShowString(2,1,"Inhale");
}else if(pwm_duty==100){
pwm_flag = 0;
}else{
pwm_duty = 0;
pwm_flag = 1;
}
}else if(pwm_flag == 0){
if(pwm_duty >0 && pwm_duty<=100){
pwm_duty--;
OLED_ShowString(2,1,"Exhale");
}else if(pwm_duty==0){
pwm_flag = 1;
}else{
pwm_duty = 0;
pwm_flag = 1;
}
}else{
pwm_duty = 0;
pwm_flag = 1;
}
//改变占空比
PWM_SetDuty(pwm_duty);
};
}
- PWM.h
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetDuty(uint16_t pwm_duty);
#endif
- PWM.c
#include "stm32f10x.h" // Device header
//TIM输出比较模式-PWM初始化
void PWM_Init(void){
//1.配置RCC
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// //隐:引脚重映射,将PA0映射到PA15
// RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//引脚重映射会使用AFIO
// GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);//参考手册“8.3.7定时器复用功能重映射”
// GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//PA15、PB3、PB4变成普通IO口
//2.选择时基单元时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元-10kHz
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100-1;//自动重装载值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72-1;//预分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0x0000;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//4.配置运行控制
TIM_Cmd(TIM2, ENABLE);
//5.配置输出捕获电路
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//后续即使用到高级定时器初始化,也不会出错
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0x0000; //占空比
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
//7.配置GPIO-PA0
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //GPIO_Pin_15-引脚重映射
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
//设置PWM波的占空比
//范围是0~100
void PWM_SetDuty(uint16_t pwm_duty){
TIM_SetCompare1(TIM2, pwm_duty);
}
编程感想:
- 在Hardware文件夹创建了
PWM.h
和PWM.c
。- 输出比较电路的OC1输出怎么映射到GPIO引脚上呢?其实在引脚定义表上就已经固定好了,OC1输出固定是PA0。但是配置GPIO时注意配置成“复用推挽输出”模式,因为GPIO输出框图中显示,只有复用输出模式才能使信号来自片上外设。
- 引脚重映射。进行引脚重映射时,需要不断对照参考手册及引脚定义表。若对 调试端口 进行引脚重映射,需要三步:开启AFIO时钟,进行引脚重映射,解除调试端口复用。若想对 定时器和其他外设的复用引脚 进行重映射,那就只需要前两步即可。
需求:按下按键开关,舵机角度就变化一次,并在OLED显示屏上显示舵机当前的角度。
注1:按键开关PB1,舵机PWM接PA1(输出比较电路通道2)、电源接STLINK上的5V引脚、GND接面包板GND。
注2:输入PWM信号要求周期为20ms(50Hz),高电平宽度为0.5ms~2.5ms(占空比2.5%~12.5%),脉冲控制精度为2us(0.18°,占空比精度0.01%)。
注3:舵机的三根输入线,棕色是电源负、红色是电源正、橙色是PWM信号线。
代码展示:首先是在UP主提供的OLED.c
中添加一个显示单精度浮点数的函数,然后OLED其他代码和Key相关代码略,仅展示新增函数。
- OLED_ShowFloat
函数:(按照UP主的风格复制编写)
/**
* @brief OLED显示单精度浮点数(十进制,带符号数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-3.4E38~3.4E38
* @param Len1 要显示整数长度,范围:1~10
* @param Len2 要显示小数长度,范围:1~10
* @retval 无
*/
void OLED_ShowFloat(uint8_t Line, uint8_t Column, float Number, uint8_t Len1, uint8_t Len2)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
Number = -Number;
}
for (i = 0; i < Len1; i++)
{
OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Len1 - i - 1) % 10 + '0');
}
OLED_ShowChar(Line, Column + 1 + Len1, '.');
Number1 = (uint32_t)((Number - (uint32_t)Number)*OLED_Pow(10,Len2));
for (i = 0; i < Len2; i++)
{
OLED_ShowChar(Line, Column + i + 2 + Len1, Number1 / OLED_Pow(10, Len2 - i - 1) % 10 + '0');
}
}
//别忘了在OLED.h文件中声明
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Key.h"
#include "SteerEngine.h"
int main(void){
float sg90_degree = 0; //舵机的角度,范围-90~90,精度为1
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"SG90-angle:");
OLED_ShowString(2,1,"-90.00 degree");
//按键初始化
Key_Init();
//舵机初始化
SteerEngine_Init();
sg90_degree = -90;
SteerEngine_SetDegree(sg90_degree);
while(1){
if(Key_GetNum()==1){
//改变舵机的角度
if(sg90_degree<90){
sg90_degree += 8.8;
}else{
sg90_degree = -90;
}
SteerEngine_SetDegree(sg90_degree);
//显示舵机当前角度
OLED_ShowFloat(2,1,sg90_degree,2,2);
}
};
}
- SteerEngine.h
#ifndef __STEERENGINE_H
#define __STEERENGINE_H
void SteerEngine_Init(void);
void SteerEngine_SetDegree(float SteerEngine_Degree);
#endif
- SteerEngine.c
#include "stm32f10x.h" // Device header
//舵机驱动初始化-TIM2输出比较通道2-PA1
void SteerEngine_Init(void){
//1.配置RCC
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.选择时基单元时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元-50Hz-总计分频720000*2
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStructure.TIM_Period = 10000-1;//自动重装载值-占空比精度0.01%
TIM_TimeBaseInitStructure.TIM_Prescaler = 144-1;//预分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0x0000;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//4.配置运行控制
TIM_Cmd(TIM2, ENABLE);
//5.配置输出捕获电路
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//后续即使用到高级定时器初始化,也不会出错
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0x0000; //占空比
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
//7.配置GPIO-PA1
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
//设置舵机的旋转角度
//-90~90度-->占空比250~1250
void SteerEngine_SetDegree(float SteerEngine_Degree){
if(SteerEngine_Degree>=-90 && SteerEngine_Degree<=90){
TIM_SetCompare2(TIM2, (SteerEngine_Degree+90)*1000/180+250);
}else{
//期待报错,但不知道怎么写代码
}
}
编程感想:
- 本实验用的舵机旋转范围固定为-90度~90度,无法实现360度旋转。这与舵机的内置电路板有关系,舵机电机本身是有360旋转的潜力的。
- 舵机角度不对。程序下载后,拆下旋转片,按照当前舵机的设定角度重新安装旋转片即可,但是注意这个旋转片带齿,所以角度还是会有轻微的误差。不要旋转片安装好且上电的情况下,强行拨动旋转片,有可能会烧坏舵机的内置电路板!
- 使用多个输出比较通道可以相位同步!由于四个通道共用一个时基单元,所以只能做到相位同步,只是 捕获/比较寄存器 的值不一样而已。
需求:按下按键开关,直流电机依次改变转速:+20、+40、+60、+80、+100、-100、-80、-60、-40、-20、0。
代码展示:OLED和Key的相关代码和上一小节“PWM驱动舵机”一样。下面仅展示新增代码。
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Key.h"
#include "DC_Motor.h"
int main(void){
//直流电机的参数,正负代表转向,数值代表占空比(范围-100~100)
int16_t DCmotor_para = 0;
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"DC_Motor:");
OLED_ShowString(2,1,"+000%");
//按键初始化
Key_Init();
//直流电机初始化
DC_Motor_Init();
while(1){
if(Key_GetNum()==1){
if(DCmotor_para<100){
DCmotor_para += 20;
}else if(DCmotor_para>=100){
DCmotor_para = -100;
}
DC_Motor_SetRotateSpeed(DCmotor_para);
}
OLED_ShowSignedNum(2,1,DCmotor_para,3);
};
}
- DC_Motor.h
#ifndef __DC_MOTOR_H
#define __DC_MOTOR_H
void DC_Motor_Init(void);
void DC_Motor_SetRotateSpeed(int16_t DC_Motor_para);
#endif
- DC_Motor.c
#include "stm32f10x.h" // Device header
//直流电机初始化-PWM-TIM2输出比较3(PA2);AIN1-A4;AIN2-A5
void DC_Motor_Init(void){
//1.配置RCC
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.选择时基单元时钟
TIM_InternalClockConfig(TIM2);
//3.配置时基单元-20kHz-总计分频3600
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;//向上计数
TIM_TimeBaseInitStructure.TIM_Period = 100-1;//自动重装载值-占空比精度1%
TIM_TimeBaseInitStructure.TIM_Prescaler = 36-1;//预分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0x0000;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//4.配置运行控制
TIM_Cmd(TIM2, ENABLE);
//5.配置输出捕获电路
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);//后续即使用到高级定时器初始化,也不会出错
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0x0000; //占空比
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
//7.配置GPIO-PA2
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//8.配置AIN1-A4、AIN2-A5
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4 | GPIO_Pin_5);//直流电机初始化为停止状态
}
//调制直流电机转向及转速
//占空比范围-100~100,精度1
void DC_Motor_SetRotateSpeed(int16_t DC_Motor_para){
//参数正-顺时针;参数负-逆时针
if(DC_Motor_para>=0){
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
}else{
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
GPIO_SetBits(GPIOA, GPIO_Pin_5);
DC_Motor_para = -DC_Motor_para;
}
//调节输出PWM波的占空比
TIM_SetCompare3(TIM2, DC_Motor_para);
}
编程感想:
- 关于直流电机的转向。安装好风扇叶后上电进行测试,发现黑色线接AO1红色线接AO2,可以保证直流电机正转时向前吹风,反转向后吸风。
- 关于GPIO口的模式。查阅参考手册“8.1.11外设的GPIO配置”,发现TIM2的输出通道要配置成 复用推挽输出 模式,而AIN1、AIN2所对应的配置成普通的 推挽输出 即可。
- 关于直流电机的声音。使用1kHz的PWM波驱动直流电机时,旋转速度较低时,用手捏住电机轴不让它转,直流电机可能会发出类似蜂鸣器的声音,这是因为1kHz在人耳能听到的频率范围(20Hz~20kHz)内。将频率改成20kHz以上就不会听到了。
需求:将旋转编码器顺时针转则舵机正转,逆时针转则舵机反转,分辨率为10°。
- 接线图参考“5-2旋转编码器计次”、“6-4PWM驱动舵机”两个实验的接线。
- 旋转编码器:A口接PB0、B口接PB1。VCC和GND直连面包板。
- 舵机:舵机的三根输入线,棕色是电源负、红色是电源正、橙色是PWM信号线接TIM2输出比较通道2(PA1)。
代码展示:SteerEngine模块和RotaryEncode模块与之前相同,不予赘述。
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "RotaryEncoder.h"
#include "SteerEngine.h"
int main(void){
int16_t RE_count=0, RE_change=0;//旋转编码器参数
float sg90_degree = 0;//舵机角度
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"SG90_degree:");
OLED_ShowString(2,1,"+00 degree");
//旋转编码器初始化
RotaryEncoder_Init();
//舵机初始化
SteerEngine_Init();
SteerEngine_SetDegree(0);
while(1){
//旋转编码器顺时针转-->舵机顺时针转(角度减)
//旋转编码器逆时针转-->舵机逆时针转(角度加)
if(RE_count != RotaryEncoder_GetCount()){
RE_count = RotaryEncoder_GetCount();
if(RotaryEncoder_GetChange() == 1){
if(sg90_degree>-90 && sg90_degree<=90){
sg90_degree -= 18;
}else if(sg90_degree != -90){
sg90_degree = 0;
}
}else if(RotaryEncoder_GetChange() == -1){
if(sg90_degree>=-90 && sg90_degree<90){
sg90_degree += 18;
}else if(sg90_degree != 90){
sg90_degree = 0;
}
}
SteerEngine_SetDegree(sg90_degree);
OLED_ShowSignedNum(2,1,sg90_degree,2);
}
};
}
需求:将旋转编码器顺时针转则加速,逆时针转则减速,分辨率为20%,边界为 [-100, +100]。
- 接线图参考“5-2旋转编码器计次”、“6-5PWM驱动直流电机”两个实验的接线。
- 旋转编码器:A口接PB0、B口接PB1。VCC和GND直连面包板。
- 直流电机:PWMA接PA2、AIN1接PA4、PA5。VM连STLINK的5V。AO1、AO2接直流电机。
代码展示:DC_Motor模块和RotaryEncode模块与之前相同,不予赘述。
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "RotaryEncoder.h"
#include "DC_Motor.h"
int main(void){
//直流电机的参数,正负代表转向,数值代表占空比(范围-100~100)
int16_t DCmotor_para = 0;
//旋转编码器数值及转向
int16_t RE_rotate;
int16_t RE_num = 0;
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"DC_Motor:");
OLED_ShowString(2,1,"+000%");
//旋转编码器初始化
RotaryEncoder_Init();
//直流电机初始化
DC_Motor_Init();
while(1){
if(RE_num != RotaryEncoder_GetCount()){
RE_num = RotaryEncoder_GetCount();
RE_rotate = RotaryEncoder_GetChange();
// OLED_ShowSignedNum(3,1,RE_rotate,1);
// OLED_ShowSignedNum(4,1,RotaryEncoder_GetCount(),5);
//改变转速
if(RE_rotate == 1){//旋转编码器顺时针转,增大转速
if(DCmotor_para>=-100 && DCmotor_para<100){
DCmotor_para += 20;
}else if(DCmotor_para!=100){
DCmotor_para = 0;
}
//更新显示及转速
OLED_ShowSignedNum(2,1,DCmotor_para,3);
DC_Motor_SetRotateSpeed(DCmotor_para);
}else if(RE_rotate == -1){//旋转编码器逆时针转,减小转速
if(DCmotor_para>-100 && DCmotor_para<=100){
DCmotor_para -= 20;
}else if(DCmotor_para!=-100){
DCmotor_para = 0;
}
//更新显示及转速
OLED_ShowSignedNum(2,1,DCmotor_para,3);
DC_Motor_SetRotateSpeed(DCmotor_para);
}
}
};
}
编程感想:
- 关于外设的调用。旋转编码器的原理是外部中断EXTI,随便指定两个GPIO端口即可。直流电机需要用到定时器的输出比较电路,需要占用一个固定的引脚输出PWM,另外再随便占用两个GPIO端口控制正反转。
- 接上直流电机后,旋转编码器控制的不精准。这个问题以目前的知识水平,没有解决。故障现象具体大致可以参考B站视频“旋转编码器控制直流电机正反转及转速”。
本节的功能描述对应参考手册内容为“14.3.5 输入捕获模式”、“14.3.6 PWM输入模式”、“14.3.15 定时器同步”。
IC(Input Capture)输入捕获模式 下,当通道 输入引脚 出现指定电平跳变时,当前CNT的值将被锁存到CCR中,读取CCR的值就可以测量PWM波形的脉冲间隔(频率)、电平持续时间(占空比)等参数。
- 每个高级定时器和通用定时器都拥有4个输入捕获通道,基本定时器没有输入捕获/输出比较的功能。
- 可配置为 PWMI模式,同时测量频率和占空比。
- 可配合 主从触发模式(UP主自己取的名字),实现硬件全自动测量。
注:输入捕获和输出比较共用CCR寄存器和引脚,所以对于同一个定时器不能同时使用。
注:PWMI模式和主从触发模式的硬件设计巧妙,可以极大的减轻软件的压力。
6.5.1 频率测量原理
stm32只能测量数字信号(高电平3.3V,低电平0V)的频率,对于正弦波则需要使用运放比较、电压隔离等模块进行预处理,转换成数字信号之后才能进行测量。
- 测频法:在闸门时间T内,对上升沿计次,得到N,则频率为 f x = N / T f_x = N/T fx=N/T。如测量1s内有多少个上升沿就是多少Hz。适合测量高频信号,数据更新慢。
- 测周法:两个上升沿内,以标准频率 f c f_c fc 计次,得到N ,则频率 f x = f c / N f_x = f_c / N fx=fc/N。本质上就是直接测量一个周期的时间,适合测量低频信号,数据更新速度取决于待测信号频率,更新速度相对更快。
- 中界频率:测频法与测周法误差相等的频率点 f m = f c / T f_m = \sqrt{f_c / T} fm=fc/T。待测信号频率大于中介频率,更适合用测频法;反之则适合用测周法。
注:测频法可以参考之前的 外部中断计次 的代码;而测周法则需要用到TIM的 输入捕获模式。
6.5.2 TIM的输入捕获电路
- TIMx_CH1 ~ TIMx_CH4:4个输入引脚,参考引脚定义表可以知道引脚复用在哪个位置。
- 三输入异或门:数据选择器可以使其不起作用。起作用时主要是为三相无刷电机服务,此时三个输入接三相无刷电机的霍尔传感器(检测转子位置),然后这个定时器就可以作为换相电路的接口定时器,驱动换相电路工作。本节不涉及。
- 输入滤波器:对输入信号进行滤波,避免一些高频的毛刺信号误触发。
- 边沿检测器:与外部中断类似,可以选择高电平触发、低电平触发等。
- TI1FP1、TI1FP2:是两套独立的信号,都经过各自的滤波器、极性选择,进而输出到后续电路。之所以设计成交叉电路,主要有两个好处:可以灵活切换后续捕获电路的输入,而不需要重新初始化;将一个引脚输入同时映射到两个捕获单元,是PWMI模式的经典结构。下面三路的信号同理。
- TRC1:来源于时钟源的选择,也可以作为输入捕获信号的输入。
- 预分频器:对信号进行分频,其输出的信号触发捕获电路进行工作:每来一个触发信号,CCR就会读取一次CNT的值,同时发生一个捕获事件ICxPS,这个事件会在状态寄存器置标志位,同时也可以产生中断(捕获中断)。
例(测周法):比如可以配置上升沿触发捕获,没来一次上升沿,CNT就将值转运到CCR,由于CNT由内部时钟源驱动,此时CNT的值实际上就是两次上升沿之间的时间间隔,于是就实现了测周法。
注:要想在一次捕获后将CNT清零,可以使用 主从触发模式 配置硬件自动完成。
上面对于输入捕获电路的基本结构做了相应的介绍,下面来介绍更加细节的电路结构:
上图给出了输入捕获通道的实际电路,但其实TI1FP2也有一路单独的滤波器和边沿检测器,这部分电路并没有显示在图中。
- TI1:实际上就是CH1引脚。
- FDTS:滤波器的采样时钟来源。
- ICF[3:0]:控制滤波器参数,可以对消除高频毛刺——参考手册“14.4.7 捕获/比较模式寄存器(TIMx_CCMR1)”。这个滤波器的基本原理就是以采样频率对输入信号进行采样,当连续N个值都为高电平才输出高电平、连续N个值都为低电平才输出低电平,若N个值产生了高频抖动,那输出就不变化。采样频率越低、采样个数N越大,采样效果越好。
- 边沿检测器:捕获TI1F的上升沿/下降沿。
- CC1P位:属于TIMx_CCER寄存器,控制极性选择。
- CC1S[1:0]:对输入的TI1FP1、TI2FP1、TRC进行选择。
- ICPS[1:0]:控制分频器,可以选择不分频、2分频、4分频、8分频。
- CC1E:控制输出使能或失能。进而就可以在输入TI1的边沿,将CNT转运到CCR。
- 从模式控制器:实现硬件自动化操作的利器,可以来自于TI1F_ED、TI1FP1。后续可以配置相应的硬件,在TI1FP1的上升沿对CNT自动清零。下面介绍主从触发模式。
6.5.3 主从触发模式
主从触发模式实际上是UP主自己取的名字,参考手册中只有主模式、从模式的介绍。
主从触发模式就是 主模式、从模式、触发源选择 三个功能的简称,因为对于单个定时器来说,既可以配置成主模式,输出触发源;也可以配置成从模式,受其他触发源的控制。
- 主模式:将定时器内部的信号映射到TRGO引脚,用于触发别的外设。见“14.4.2 控制寄存器2(TIMx_CR2)”:
- 从模式:接收其他外设/自身外设的信号(TRGI),来控制自身定时器的运行。可以执行的操作见上图。参考手册见“14.4.3 从模式控制寄存器(TIMx_SMCR)”:
- 触发源选择:选择从模式的触发源,可以认为是从模式的一部分。可以选择的触发源见上图,注意触发源不包含TI3、TI4信号,所以进行 从模式自动清零CNT,只能使用通道1和通道2。参考手册见“14.4.3 从模式控制寄存器(TIMx_SMCR)”:
例:若想让TI1FP1信号自动触发CNT清零。那么触发源选择TIFP1,从模式选择
Reset
,即可实现硬件自动化。
不用担心,看似复杂,但实际操作过程中,也是在库函数中调用相应的函数即可快速的完成配置。
6.5.4 输入捕获基本结构和PWMI基本结构
本结构只使用了一个通道,所以只能测量频率。
- 时基单元:与之前的相同,CNT就是就是测周法用于计时的东西。注意测频率的标准频率是预分频之后的频率。
- TI1FP1:兵分两路,一路用于触发转运CNT到CCR,一路用于触发清零CNT。硬件执行时肯定是先转运再清零,或者是两者同时非阻塞进行。
- 读取CCR值:需要测频率时就读取CCR值,不需要测频率时整个电路自动运行,也不占用软件资源。
- 关于CNT:计数最大值是65535,所以待测频率不能太低。
使用两个通道同时捕获一个引脚,可以同时测量周期和占空比。
- TI1FP1:上升沿触发,和之前相同。通道1的捕获寄存器CCR1表示整个周期的时间。
- TI1FP2:配置为下降沿触发,通过交叉通道触发通道2的捕获单元。于是通道2的捕获寄存器CCR2就表示高电平周期。
需求:PA0产生频率可调的PWM波,然后在PA6端口进行测量,并在OLED上显示相应的频率。
代码展示:OLED代码见第四章“4OLED调试工具”、PWM相关代码见“PWM驱动呼吸灯实验”。
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "PWM.h"
#include "InputCapture.h"
int main(void){
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"Frequency:");
OLED_ShowString(2,1,"00000 Hz");
//初始化PWM波-PA0输出
PWM_Init();
PWM_SetDuty(50);//占空比(单位:%)
PWM_SetFreq(10000); //PWM频率(单位:Hz)
//初始化输入捕获
InputCapture_Init();
while(1){
OLED_ShowNum(2,1, InputCapture_GetFreq(),5);
};
}
- InputCapture.h
#ifndef __INPUTCAPTURE_H
#define __INPUTCAPTURE_H
void InputCapture_Init(void);
uint32_t InputCapture_GetFreq(void);
#endif
- InputCapture.c
#include "stm32f10x.h" // Device header
//输入捕获初始化-TIM3输入捕获通道1-PA6
void InputCapture_Init(void){
//1.开启外设时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.GPIO配置
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//3.时基单元配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 0xffff;
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0x00;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_Cmd(TIM3, ENABLE);
//4.输入捕获单元配置
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//输入捕获通道1
TIM_ICInitStructure.TIM_ICFilter = 0x3;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//上升沿计数
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//不选择交叉通道
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//4.从模式配置
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
}
//根据输入捕获的值计算频率
//计算范围:16HZ~500kHz,较为精准的范围:16Hz~10kHz,高频分辨率约为15Hz
uint32_t InputCapture_GetFreq(void){
return (uint32_t)1000000/(uint32_t)(TIM_GetCapture1(TIM3)+1);
}
编程感想:
- 调节PWM波的频率。影响PWM频率的两个参数是自动重装载值ARR、预分频系数PSC,但是注意如果改变ARR会同步影响占空比,所以为了简单,就调节预分频系数来改变PSC即可。
- 运行控制。注意配置时基单元的时候,一定不要完了时基单元的运行控制函数
TIM_Cmd
。- 计算频率。最后获取输入捕获值计算频率时,由于CNT会在上升沿清零计数器,所以单周期的最后一个小周期可能会由于上升沿的触发而少记一次,所以记得将输入捕获值+1后,再计算频率。这个是测周法固有的误差。
需求:PA0产生频率和占空比可调的PWM波,然后在PA6端口采用PWMI模式进行测量,并在OLED上显示相应的频率和占空比。
接线图、代码调用与上一小节完全相同,所不同的是在原来的InputCapture.c
基础上,增加了两个函数。下面是代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "PWM.h"
#include "InputCapture.h"
int main(void){
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"Freq:00000 Hz");
OLED_ShowString(2,1,"Duty:000%");
//初始化PWM波-PA0输出
PWM_Init();
PWM_SetDuty(79);//占空比(单位:%)
PWM_SetFreq(10000); //PWM频率(单位:Hz)
//初始化输入捕获
InputCapture_PWMIInit();
while(1){
OLED_ShowNum(1,6, InputCapture_GetFreq(),5);
OLED_ShowNum(2,6, InputCapture_GetDuty(),3);
// OLED_ShowNum(3,1,TIM_GetCapture2(TIM3),5);
// OLED_ShowNum(4,1,TIM_GetCapture1(TIM3),5);
};
}
- InputCapture.c
#include "stm32f10x.h" // Device header
//PWMI模式初始化-TIM3输入捕获通道1-PA6
void InputCapture_PWMIInit(void){
//1.开启外设时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.GPIO配置
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//3.时基单元配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 0xffff;
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0x00;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_Cmd(TIM3, ENABLE);
//4.输入捕获单元配置
//输入捕获通道1
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//输入捕获通道1
TIM_ICInitStructure.TIM_ICFilter = 0x3;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;//上升沿计数
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;//不选择交叉通道
TIM_PWMIConfig(TIM3, &TIM_ICInitStructure);//注意只是这里换了!!!使用该函数不需要再初始化通道2
//4.从模式配置
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
}
//根据输入捕获的值计算占空比-PWMI模式初始化才能调用
//计算范围:1%~100%,占空比分辨率1%
uint16_t InputCapture_GetDuty(void){
// float duty;
// duty = ((float)(TIM_GetCapture2(TIM3)+1))/((float)(TIM_GetCapture1(TIM3)+1));
// return (uint16_t)(duty*100);
return (TIM_GetCapture2(TIM3)+1)*100/(TIM_GetCapture1(TIM3)+1);
}
//别忘了将这两个函数在InputCapture.h头文件中声明
Encoder Interface 编码器接口 是定时器的时钟源之一,可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减(不消耗软件资源),从而指示编码器的位置、旋转方向和旋转速度。当然上面这个功能也可以使用外部中断+软件代码来手动实现,但是stm32开辟出专用的硬件资源,可以减轻软件的消耗。
- 每个高级定时器和通用定时器都拥有1个编码器接口。注意定时器配置成编码器接口模式后,就干不了别的活了。
- 两个输入引脚借用了输入捕获的通道1和通道2。
- 典型应用场景:一般应用在电机控制的项目上。使用PWM驱动电机,再使用定时器的编码器接口测量电机的速度,然后再用PID算法进行闭环控制。一般电机的转速比较高,会使用无接触式的霍尔传感器或光栅来输出包含转速信息的信号(如正交编码器信号)。
在要求不严格的场景下,正交信号的某一路信号便可以传递转速信息,但不能说明旋转方向;要指明旋转方向,可以再添加一路专门用于指示旋转方向的信号,但是这样就不是正交信号了。正交信号使用两路相位相差90°的信号,可以同时说明转速、转向,并且由于其特殊的正交性质,还额外增加了抗干扰的能力。
为了消除某些毛刺噪声,stm32处理正交信号的基本逻辑:
在两路的所有的边沿部分都进行判断,按照上表指示的逻辑对CNT进行增/减。下面是两个例子,演示了正交信号的抗噪声能力:
上面这两种模式的应用:如果发现计次方向和预期方向相反,那么就调整一下极性即可。
- 两个输入:编码器接口的输入固定为输入捕获通道1的TI1FP1(CH1引脚)和输入捕获通道2的TI2FP2(CH2引脚)。CH3和CH4与编码器接口无关。
- 极性选择:极性选择实际上就是选择是否对输入信号进行反相。在输入捕获模式的作用效果是选择上升沿有效还是下降沿有效;在编码器接口模式的作用效果是影响判断的高低电平。
注:进一步详细介绍参考“14.3.12 编码器接口模式”。
需求:使用定时器的编码器接口,对旋转编码器进行计次、测速。
- 旋转编码器一圈共有20个分割点,单个分割点为18°。
- 注:用编码器接口对旋转编码器进行测速,显然太奢侈了。但是目前手头上的器件,只有旋转编码器能输出两路正交的编码器信号,所以就对旋转编码器进行测速了。
- 基本思路:两个定时器,一个定时中断,一个编码器接口。每次定时中断读取一次计数器CNT的值并清零,就可以不断地得到相应的速度。
代码展示:定时器Timer相关代码参考“6.2.1 定时器定时中断-内部时钟”,只不过设定定时器的中短间隔为1s。
- main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "EncoderInterface.h"
#include "Timer.h"
int16_t timer_cnt = 0;//定时器的计数器
int main(void){
//定义旋转编码器的转速(单位:转/分)
float RE_RPM = 0;
//OLED显示屏初始化
OLED_Init();
OLED_ShowString(1,1,"RE_Speed:");
OLED_ShowString(2,1,"+000.00 RPM");
OLED_ShowString(3,1,"CNT:+00000");
//编码器接口初始化
EncoderInterface_Init();
//定时器初始化
Timer_Init();
while(1){
RE_RPM = (float)timer_cnt/4/20*60;//定时器的闸门时间是1s
OLED_ShowFloat(2,1,RE_RPM,3,2);
OLED_ShowSignedNum(3,5,timer_cnt,5);
};
}
//TIM2定时中断后的中断函数-获取计数器的值并清零
void TIM2_IRQHandler(void){
if(TIM_GetITStatus(TIM2,TIM_IT_Update)==SET){
timer_cnt = EncoderInterface_Get();
TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
}
}
- EncoderInterface.h
#ifndef __ENCODERINTERFACE_H
#define __ENCODERINTERFACE_H
void EncoderInterface_Init(void);
uint16_t EncoderInterface_Get(void);
#endif
- EncoderInterface.c
#include "stm32f10x.h" // Device header
//定时器的编码器接口初始化-TIM3-PA6、PA7
void EncoderInterface_Init(void){
//1.开启外设时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//3.配置时基单元-编码器接口托管时钟
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 0xffff;
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;//默认不分频
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0x00;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
//4.配置输入捕获通道
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure);//防止没定义的参数影响程序正常运行
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;//输入捕获通道1
TIM_ICInitStructure.TIM_ICFilter = 0x3;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICStructInit(&TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;//输入捕获通道2
TIM_ICInitStructure.TIM_ICFilter = 0x3;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//5.选择编码器接口
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//6.启动定时器
TIM_Cmd(TIM3, ENABLE);
}
//读取计数器的值并清零
uint16_t EncoderInterface_Get(void){
uint16_t temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0x0000);
return temp;
}
编程感想:
- GPIO输入模式。在配置成上拉输入、下拉输入的时候,记得要与外部引脚的默认电平保持一致,防止默认电平打架。不过默认高电平是一种行业习惯。若实在不确定外部引脚默认电平,就选择浮空输入,但缺点就是容易受到噪声的干扰。
- 极性的配置。
TIM_EncoderInterfaceConfig
编码器配置中对于极性的配置和输入捕获通道对于极性的配置重复,所以若将编码器配置放在后面,那么在输入捕获通道配置时就无需特别指明极性选择的配置了。- 旋转编码器的段落感。转动一下,计数器会变动4次。这是因为在双边沿都会进行判断计数。
- 初始化!!!把定时器的代码加进来后,库库一顿操作在中断函数里取CNT值、主函数计算转速,一运行啥都没有,反复看代码也没看出啥逻辑上的毛病,一筹莫展。忽然发现,原来是忘了加定时器的初始化函数。