STM32實戰四 定時器和按鍵

這一章編寫定時器,包括定時器基類 Timer 和派生的通用定時器 GeneralTimer。基類對定時器參數進行封裝,通用定時器封裝一些定時應用,對應PLC的一些功能,包括:

  1. 1ms定時中斷
  2. 100個32位數字時間繼電器,最小1ms,最大0xffffffff,大約50天。
  3. 一個高精度回調函數,微秒級誤差,最小定時間隔1ms。
  4. 按鍵抖動和干擾過濾,併產生按鍵上升沿和下降沿。

代碼中有詳細的說明,這裏只解釋幾個知識點,其它文檔介紹按鍵防抖和延時的時候一般都是死循環,官方文檔也是這麼用,如果有很多按鍵和延時就會一個一個等,效率很低。我這裏用了另外一種高效的方法,就是模仿時間繼電器,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開發過程的常見問題

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章