这一章编写定时器,包括定时器基类 Timer 和派生的通用定时器 GeneralTimer。基类对定时器参数进行封装,通用定时器封装一些定时应用,对应PLC的一些功能,包括:
- 1ms定时中断
- 100个32位数字时间继电器,最小1ms,最大0xffffffff,大约50天。
- 一个高精度回调函数,微秒级误差,最小定时间隔1ms。
- 按键抖动和干扰过滤,并产生按键上升沿和下降沿。
代码中有详细的说明,这里只解释几个知识点,其它文档介绍按键防抖和延时的时候一般都是死循环,官方文档也是这么用,如果有很多按键和延时就会一个一个等,效率很低。我这里用了另外一种高效的方法,就是模仿时间继电器,100个计数器同时工作,直到计数为0时执行对应操作,这样主循环没有等待,循环周期只有几十微秒,能进行高精度实时控制,具体方法下一章中介绍,这里先做好基础。
按键滤波后动作会有一定的延时,大约4ms加主循环周期,屏蔽了高频信号,对高速信号不适用。
GeneralTimer中的100个定时器u32 m_t[100]占用很多栈空间,导至程序不运行,要增加栈空间,方法是增大 startup_stm32f10x_hd.s 文件中的 Stack_Size 项,我这里加到了 0x0800,也就是2k字节,原来是0x0200,512字节。其它不多说了,看代码:
Timer.h
#ifndef __TIMER__
#define __TIMER__
extern "C" { // 兼容C,按C语言编译,Keil5中的包含文件已经加入了C++兼容,不用再加这一段
#pragma diag_remark 368 //消除 warning: #368-D: class "<unnamed>" defines no constructor to initialize the following:
#include "stm32f10x.h"
#include "stm32f10x_tim.h"
#pragma diag_default 368 // 恢复368号警告
}
// TIMx 1ms定时
class Timer
{
// Construction
public:
Timer(TIM_TypeDef * TIMx); // tim:TIM1-8
// Properties
public:
TIM_TypeDef * m_pTIMx; // 定时器指针
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
private:
// Methods
public:
inline void timInit(void); // 初始化定时
inline void nvicInit(void); // 初始化优先级
void enableInterrupt(void); // 使能中断
virtual void onTimer(void); // 中断,返回1溢出,0不溢出
// Overwrite
public:
};
// 中断处理
extern "C" { // 兼容C
void TIM2_IRQHandler(void);
void TIM3_IRQHandler(void);
void TIM4_IRQHandler(void);
void TIM5_IRQHandler(void);
void TIM6_IRQHandler(void);
void TIM7_IRQHandler(void);
void TIM8_UP_IRQHandler(void);
void TIM1_UP_IRQHandler(void);
}
#endif
Timer.cpp
/**
******************************************************************************
* @file Timer.cpp
* @author Mr. Hu
* @version V1.0.0 STM32F103VET6
* @date 05/19/2019
* @brief 定时器基类
*
******************************************************************************
* @remarks
* 定时器基类,默认不开启中断,可用类方法enableInterrupt使能中断
* 默认定时周期1ms
*
* 参考资料:
* STM32 Keil C++编写单片机程序
* https://www.cnblogs.com/yeshuimaowei/p/6949642.html
* 利用基本定时器进行精确延时
* https://blog.csdn.net/sahpah/article/details/38545637
* TIM1和8是高级定时器,配置方法不同
* https://www.cnblogs.com/pertor/p/9488813.html
*/
/* Includes ------------------------------------------------------------------*/
extern "C" { // 兼容C,按C语言编译,Keil5中的包含文件已经加入了C++兼容,不用再加这一段
#pragma diag_remark 368 //消除 warning: #368-D: class "<unnamed>" defines no constructor to initialize the following:
#include "stm32f10x_tim.h"
#include "misc.h"
#pragma diag_default 368 // 恢复368号警告
}
#include "Timer.h"
// 全局指针,每个定时器对应一个指针,不能合并
Timer * pTim2 = 0;
Timer * pTim3 = 0;
Timer * pTim4 = 0;
Timer * pTim5 = 0;
Timer * pTim6 = 0;
Timer * pTim7 = 0;
Timer * pTim8 = 0;
Timer * pTim1 = 0;
/**
* @date 05/19/2019
* @brief 初始化定时器基类.
* @param m_pTIMx:TIM1-8
* @retval None
*/
Timer::Timer(TIM_TypeDef * pTIMx)
{
assert_param(IS_TIM_ALL_PERIPH(TIMx));
// 保存变量
m_pTIMx = pTIMx;
if(pTIMx == TIM2) pTim2 = this;
else if(pTIMx == TIM3) pTim3 = this;
else if(pTIMx == TIM4) pTim4 = this;
else if(pTIMx == TIM5) pTim5 = this;
else if(pTIMx == TIM6) pTim6 = this;
else if(pTIMx == TIM7) pTim7 = this;
else if(pTIMx == TIM8) pTim8 = this;
else if(pTIMx == TIM1) pTim1 = this;
//else // ?? 异常
nvicInit();
timInit();
}
/**
* @date 05/19/2019
* @brief 初始化定时器,周期1ms.
* @param None
* @retval None
*/
void Timer::timInit(void)
{
if(m_pTIMx == TIM2) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // TIM2-7 时钟使能
else if(m_pTIMx == TIM3) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
else if(m_pTIMx == TIM4) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
else if(m_pTIMx == TIM5) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);
else if(m_pTIMx == TIM6) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
else if(m_pTIMx == TIM7) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM7, ENABLE);
else if(m_pTIMx == TIM8) RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE); // TIM1和8 时钟使能
else if(m_pTIMx == TIM1) RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
TIM_DeInit(m_pTIMx); // 使用缺省值初始化TIM外设寄存器
//TIM_TimeBaseStructInit( &TIM_TimeBaseStructure ); // 5项都独立初始化了,不用这个
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 自动重装载寄存器值,软件仿真用1000-1,软件仿真很慢,可用100-1
TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1; // 时钟预分频数为72-1,定时周期计算方法 72M/72/1000 = 1ms
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //采样分频倍数1,未明该语句作用。
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //上升模式
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; //高级定时器1和8是用定时器功能配置这个才可以是正常的计数频率一开始的72Mhz 值得注意的地方
TIM_TimeBaseInit(m_pTIMx, &TIM_TimeBaseStructure);
TIM_ClearFlag(m_pTIMx, TIM_FLAG_Update); //清除更新标志位
//TIM_ITConfig(m_pTIMx, TIM_IT_Update, ENABLE); //使能中断, //在这里使能中断不好,会产生很多没用的中断,所有初始化完成后
TIM_Cmd(m_pTIMx, ENABLE); //使能TIM定时器
}
/**
* @date 05/19/2019
* @brief 初始化优先级 // ?? 需要完善
* @param None
* @retval None
*/
void Timer::nvicInit(void)
{
NVIC_SetPriorityGrouping(NVIC_PriorityGroup_0);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = //TIMx 中断 // ?? 越界
m_pTIMx != TIM2 ? m_pTIMx != TIM3 ? m_pTIMx != TIM4 ? m_pTIMx != TIM5 ? m_pTIMx != TIM6 ? m_pTIMx != TIM7 ? m_pTIMx != TIM8 ? m_pTIMx != TIM1
? 0 : TIM1_UP_IRQn : TIM8_UP_IRQn : TIM7_IRQn : TIM6_IRQn : TIM5_IRQn : TIM4_IRQn : TIM3_IRQn : TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级 1 级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级 3 级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能
NVIC_Init(&NVIC_InitStructure); //初始化 NVIC 寄存器
}
/**
* @date 05/19/2019
* @brief 中断
* @param None
* @retval None
*/
void Timer::onTimer(void)
{
// ?? 下面的判断在大部分示例中都有,实测无用,先注释
//if(TIM_GetITStatus(m_tim,TIM_IT_Update) == RESET) //溢出中断
// return;
TIM_ClearITPendingBit(m_pTIMx, TIM_IT_Update);//清除溢出标志
}
/**
* @date 05/19/2019
* @brief 使能中断,放到初始化和配制以后,否则会产生好多没用的中断
* @param None
* @retval None
*/
void Timer::enableInterrupt(void)
{
TIM_ClearITPendingBit(m_pTIMx, TIM_IT_Update);//清除溢出标志,否则会产生一个中断
TIM_ITConfig(m_pTIMx, TIM_IT_Update, ENABLE);//使能中断, 不能先使能中断,会产生很多没用的中断
}
// 各定时器中断入口,调用各自的onTimer();
void TIM2_IRQHandler(void)
{
pTim2->onTimer();
}
void TIM3_IRQHandler(void)
{
pTim3->onTimer();
}
void TIM4_IRQHandler(void)
{
pTim4->onTimer();
}
void TIM5_IRQHandler(void)
{
pTim5->onTimer();
}
void TIM6_IRQHandler(void)
{
pTim6->onTimer();
}
void TIM7_IRQHandler(void)
{
pTim7->onTimer();
}
// TIM1和8是高级定时器
void TIM8_UP_IRQHandler(void)
{
pTim8->onTimer();
}
void TIM1_UP_IRQHandler(void)
{
pTim1->onTimer();
}
GeneralTimer.h
#ifndef __GENERALTIMER__
#define __GENERALTIMER__
#pragma anon_unions // 允许使用匿名结构
#include "Timer.h"
union INx // 记录输入口状态,按位分割
{
u16 state; // 状态字
struct
{
u8 count : 8; // 转换计数,低8位
u8 enable : 1; // 前次电平,第8位,从0开始
u8 level : 1; // 当前电平,第9位
u8 up : 1; // 上升沿,第10位
u8 down : 1; // 下降沿,第11位
u8 reserve: 4; // 预留
};
};
// 通用定时器
class GeneralTimer : public Timer
{
// Construction
public:
GeneralTimer(TIM_TypeDef * m_pTIMx);
// Properties
public:
u32 m_t[100]; // 100个定时器,要修改栈空间,startup_stm32f10x_hd.s Stack_Size EQU 0x400 ;//0x00000200 // 栈大小http://m.elecfans.com/article/588038.html
INx ina[16], inb[16], inc[16], ind[16], ine[16];
private:
u16 m_nFT1;
u16 m_nFT2;
void (*m_pBack)(void);
// Methods
public:
virtual void onTimer(void); // 中断,返回1溢出,0不溢出
void setCallback(void (*pBack)(void), u16 nCount);
void loop(void); // 主循环中调用,必需在循环的最开始
inline void addCount( GPIO_TypeDef* GPIOx, u16 nPin, INx & inx );
inline void upDown( INx & inx );
// Overwrite
public:
};
#endif
GeneralTimer.cpp
/**
******************************************************************************
* @file GeneralTimer.cpp
* @author Mr. Hu
* @version V1.0.0 STM32F103VET6
* @date 06/62/2019
* @brief 通用定时器
*
******************************************************************************
* @remarks
* 通用定时器,启用中断,包括3大功能中断方式
* 1. 100个时间继电器,单位ms,最大0xffffffff,大约50天。
* 2. 输入信号数字滤波,方法是,连续4ms(四次采样),信号反向不变,输入信号改变,并产
* 生一个主循环(main中的循环)周期的上升或下降沿信号。作用是防止抖动和干扰,高
* 速信号不能用这个滤波。
* 3. 高精度回调函数,回调周期最小1ms,最大65535ms,精度us
*
* 参考资料:
* STM32 Keil C++编写单片机程序
* https://www.cnblogs.com/yeshuimaowei/p/6949642.html
* 利用基本定时器进行精确延时
* https://blog.csdn.net/sahpah/article/details/38545637
* TIM1和8是高级定时器,配置方法不同
* https://www.cnblogs.com/pertor/p/9488813.html
*/
/* Includes ------------------------------------------------------------------*/
extern "C" { // 兼容C,按C语言编译,Keil5中的包含文件已经加入了C++兼容,不用再加这一段
#pragma diag_remark 368 //消除 warning: #368-D: class "<unnamed>" defines no constructor to initialize the following:
#include "stm32f10x_tim.h"
#pragma diag_default 368 // 恢复368号警告
}
#include "GeneralTimer.h"
#define COUNT 0x4 // 二进制1位
/**
* @date 06/02/2019
* @brief 通用定时器,从Timer继承
* @param m_pTIMx:TIM1-8
* @retval None
*/
GeneralTimer::GeneralTimer(TIM_TypeDef * m_pTIMx)
: Timer(m_pTIMx)
, m_nFT1(0)
, m_nFT2(0)
, m_pBack(0)
{
// 初始化100个时间继电器
for(int i = 0; i < sizeof(m_t)/sizeof(m_t[0]); i++)
m_t[i] = 0;
// 按键滤波,防止抖动和干扰
for( u8 i = 0; i < 16; i++ )
{
ina[i].state = 0x0200; // 0x0200表示ina[i].level = 1,默认上拉
inb[i].state = 0x0200;
inc[i].state = 0x0200;
ind[i].state = 0x0200;
ine[i].state = 0x0200;
}
enableInterrupt(); // 允许中断
}
/**
* @date 06/02/2019
* @brief 重写定时中断,加入时间继电器、回调计时和按键滤波
* @param None
* @retval None
*/
void GeneralTimer::onTimer(void)
{
Timer::onTimer();
// 时间继电器递减计数,到0停止,由外部程序重设32位无符号整数后再次开始计数,最大0xffffffff
for(int i = 0; i < sizeof(m_t)/sizeof(m_t[0]); i++)
{
if(m_t[i])
m_t[i]--;
}
// 回调计时
if(m_pBack)
{
if(!m_nFT2)
{
m_nFT2 = m_nFT1;
m_pBack();
}
else
m_nFT2--;
}
// 按键滤波,防止抖动和干扰
u16 nPin = GPIO_Pin_0;
for( u8 i = 0; i < 16; i++ )
{
addCount( GPIOA, nPin, ina[i] );
addCount( GPIOB, nPin, inb[i] );
addCount( GPIOC, nPin, inc[i] );
addCount( GPIOD, nPin, ind[i] );
addCount( GPIOE, nPin, ine[i] );
nPin <<= 1;
}
return;
}
/**
* @date 06/02/2019
* @brief 铵键抖动或干扰计数,nPin中只能是一个键
* @param GPIOx 端口号,x取A到E
* @retval None
*/
void GeneralTimer::addCount( GPIO_TypeDef* GPIOx, u16 nPin, INx & inx )
{
if(!inx.enable)
return;
assert_param(IS_GET_GPIO_PIN(nPin));
u8 v = GPIO_ReadInputDataBit( GPIOx, nPin );
if( v ^ inx.level ) // 异或
{
if( !(inx.count & COUNT) )
inx.count++; // 如果不同,计数加1,upDown( INx & inx )中,计到4ms时,电平变换
}
else if( inx.count )
inx.count = 0; // 如果相同清零,只有连续4个周期一至才跳变
}
/**
* @date 06/02/2019
* @brief 接入主循环,必需放在主循环的第一条,计算上升沿和下降沿
* @param None
* @retval None
*/
void GeneralTimer::loop(void)
{
for( u8 i = 0; i < 16; i++ )
{
upDown( ina[i] );
upDown( inb[i] );
upDown( inc[i] );
upDown( ind[i] );
upDown( ine[i] );
}
}
/**
* @date 06/02/2019
* @brief 计算指定IO口的上升沿和下降沿,在主循环的最开始计算,保证其它循环体上升沿和下降沿
* 不能在中断函数中计算上升沿和下降,因为中断函数和主循环不同步。
* @param inx INx 结构
* @retval None
*/
void GeneralTimer::upDown( INx & inx )
{
if(!inx.enable)
return; // 没有激活,不计算
if(inx.up) // 先清零上升沿和下降沿
inx.up = 0;
if(inx.down)
inx.down = 0;
if( !(inx.count & COUNT) )
return; // 连续计数不到4,不反向
inx.count = 0; // 连续计数完成
inx.level = ! inx.level;
if( inx.level )
inx.up = 1;
else
inx.down = 1;
}
/**
* @date 06/02/2019
* @brief 设置回调函数,高精度中断方式
* @param pBack 回调函数指针
* @param nCount 回调周期ms
* @retval None
*/
void GeneralTimer::setCallback(void (*pBack)(void), u16 nCount)
{
m_nFT1 = nCount;
m_nFT2 = nCount;
m_pBack = pBack;
}
下一章介绍LED灯,显示Morse code
STM32实战系列源码,按键/定时器/PWM/ADC/DAC/DMA/滤波
STM32实战一 初识单片机
STM32实战二 新建工程
STM32实战三 C++ IO.cpp
STM32实战四 定时器和按键
STM32实战五 板载LED显示数据
STM32实战六 PWM加移相正交
STM32实战七 数字滤波
STM32实战八 DAC/ADC
STM32实战九 编码器
STM32开发过程的常见问题