STM32實戰九 編碼器

這一章編寫編碼器程序,通過定時器連接編碼器,原理和細器節這裏不多說,參考代碼段中的網頁,有兩個注意事項,一是所有網上的參考代碼都沒有設置第二個通道,默認沒有濾波,雖然能用,但是通道2抗干擾能力差,容易造成誤計數。二是volatile u8 m_bInterrupt,說明在別處(計時器)會改變這個變量,不優化,因爲優化後把很重要的代碼刪除了,詳見setData函數說明。可用5個定時TIM1、TIM3-5、TIM8,最多可連接5個編碼器。

特別提示,以上測試中,CPU始終接5V電壓,把開發板上的5V和3.3V短接了,約二個月時間,沒有出現問題,估計能長期使用,這樣就可以方便直接連接其他的5V設備了。

Encode.h

#ifndef __ENCODER__
#define __ENCODER__

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"

#pragma diag_default 368	// 恢復368號警告
}

#include "Timer.h"
#include "IO.h"

class Encoder : public Timer	// 編碼器對象從Timer繼承
{
// Construction
public:
	Encoder(TIM_TypeDef * pTIMx);

// Properties
public:
	s32 m_nCount;		// 有符號32位計數值
	volatile u8 m_bInterrupt;	// 讀取或設置數據過程被中斷

protected:
	
private:

// Methods
public:
	s32 getData();				// 取計數
	void setData(s32 nData);	// 設置計數值
		
// Overwrite
public:
	virtual void onTimer(void);	// 中斷
};

#endif

 Encode.cpp

/**
  ******************************************************************************
  * @file		Encode.cpp
  * @author		Mr. Hu
  * @version	V1.0.0 STM32F103VET6
  * @date		05/22/2019
  * @brief		編碼輸入
  * @IO
  *		定時器	編碼器A	編碼器B
  *		TIM1	PE9 	PE11
  *		TIM3	PB4		PB5
  *		TIM4	PB6		PB7
  *		TIM5	PA0		PA1
  *		TIM8	PC6		PC7
  ******************************************************************************
  * @remarks
  *		通過定時器連接編碼器,可選TIM1、TIM2-5、TIM8共5個。在中斷函數onTimer中把無符
  * 號16位數擴展到有符號32位數,適用範圍廣。最大計數頻率140KHz,對刻度360的編碼器,可
  * 記錄轉速達23400轉/分。
  *
  * 	特別注意:這個文件的編譯優化級別要設置成0,不優化,因爲優化程序會把setData和
  * getData中的重要代碼刪除。設置方法是右鍵點擊左邊的文件名Encoder.cpp|Options for 
  * file 'Encoder.cpp"...|C/C++|Optimization|Level0'
  *
  *	參考資料
  * https://blog.csdn.net/wang328452854/article/details/50579832	貼子中的TIM_ICPolarity_BothEdge未定義
  * https://www.cnblogs.com/ChYQ/p/6247567.html
  * 按以下參數,用兩個PWM做輸入,24kHz以下比較保險,計數正常 72M/3000
  * http://bbs.21ic.com/icview-335440-1-1.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 "Encoder.h"

// 取32位數的16位
#define GET16(num, i) (((s16*)&num)[i])

/**
  * @date	05/22/2019
  * @brief  編碼器類,佔用端口見前面的IO表
  * @param	pTIMx,定時器,可選TIM1、TIM2-5、TIM8共5個
  * @retval None
  */
Encoder::Encoder(TIM_TypeDef * pTIMx)
: Timer(pTIMx)
, m_nCount(0)
, m_bInterrupt(0)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		// 使能複用輸出,不映射端口時可以不用這一句
	
	switch( (u32)pTIMx )
	{
	case (u32)TIM1:
		GPIO_PinRemapConfig(GPIO_FullRemap_TIM1, ENABLE);		// 把TIM1第1/2通道重映射到PC9/11。如果不映射,不要這一句
		IO(GPIOE, GPIO_Pin_9 | GPIO_Pin_11, GPIO_Mode_IPU, 2);	// GPIOx, nPin, GPIO_Mode_IPU 上拉, 2 輸入時無效
		break;
	case (u32)TIM3:
		GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);	// 把TIM3第1/2通道重映射到P4/5,只用PC6-7。如果不映射,不要這一句
		IO(GPIOB, GPIO_Pin_4 | GPIO_Pin_5, GPIO_Mode_IPU, 2);	// GPIOx, nPin, GPIO_Mode_IPU 上拉, 2 輸入時無效
		break;
	case (u32)TIM4:
		IO(GPIOB, GPIO_Pin_6 | GPIO_Pin_7, GPIO_Mode_IPU, 2);	// GPIOx, nPin, GPIO_Mode_IPU 上拉, 2 輸入時無效
		break;
	case (u32)TIM5:
		IO(GPIOA, GPIO_Pin_0 | GPIO_Pin_1, GPIO_Mode_IPU, 2);	// GPIOx, nPin, GPIO_Mode_IPU 上拉, 2 輸入時無效
		break;
	case (u32)TIM8:
		IO(GPIOC, GPIO_Pin_6 | GPIO_Pin_7, GPIO_Mode_IPU, 2);	// GPIOx, nPin, GPIO_Mode_IPU 上拉, 2 輸入時無效
		break;
	default:
		return;	// ?? 異常
	}
	
	TIM_TimeBaseStructure.TIM_Period = 0xffff;				// 設定計數器重裝值,在中斷函數中進位或借位
	TIM_TimeBaseStructure.TIM_Prescaler = 0;				// 時鐘預分頻值,好象是對輸入進行分頻
	TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;	// 採樣分頻倍數1,未明該語句作用。
	TIM_TimeBaseInit(m_pTIMx, &TIM_TimeBaseStructure);

	// 要放到後面兩個TIM_ICInit的後面
	TIM_EncoderInterfaceConfig(m_pTIMx, TIM_EncoderMode_TI12, TIM_ICPolarity_Falling, TIM_ICPolarity_Falling);//下降計數,實測是4分頻,即1個週期有4個計數

	// 設置通道1,TIM_ICFilter=15時最高計數頻率約140KHz,36000000/32/8 = 140625,詳見操作手冊:ETF[3:0]:外部觸發濾波 (External trigger filter) 
	TIM_ICInitTypeDef TIM_ICInitStructure;
	TIM_ICStructInit(&TIM_ICInitStructure);					// 將結構體中的內容缺省輸入
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;		// 通道1
	TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;	// 配置輸入分頻,不分頻, (檢測到幾次算一次捕獲)
	TIM_ICInitStructure.TIM_ICFilter = 15; 					// 選擇輸入比較濾波器,實測這個參數最有用,TIM_ClockDivision和TIM_ICPrescaler不明顯,還影響計數頻率,高速時可以用排線
	TIM_ICInit(m_pTIMx, &TIM_ICInitStructure);				// 將TIM_ICInitStructure中的指定參數初始化

	// 設置通道2,這個很重要,網上的參考代碼都沒有這一段,雖然能用,但是通道2抗干擾能力差,造成誤計數。
	TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;	// 通道2
	TIM_ICInit(m_pTIMx, &TIM_ICInitStructure);			// 將TIM_ICInitStructure中的指定參數初始化
	
	m_pTIMx->CNT = 0;	// 初始值
	
	enableInterrupt();	// 最後開中斷
}

/**
  * @date	05/22/2019
  * @brief  獲取編碼器數據,把定時器無符號16位數轉化爲有符號32位數,其中m_bInterrupt是重點。
  * @param	None
  * @retval 有符號32位編碼器數據
  */
s32 Encoder::getData()
{
	// 中斷標誌清零
	m_bInterrupt = 0;
	
	// 轉換成32位數
	s32 v = m_nCount | m_pTIMx->CNT;
	
	// 這兩句是重點,表面上看m_bInterrupt在上面清零,這裏也是零,沒有意義,優化編譯也會把這兩行刪除,
	// 但是實際上,上面賦值的計算過程中,可能產生溢出中斷,執行進位或借位操作,然後繼續合併高低16位,
	// 造成很大的誤差(65535),測試時發現正確數據應該是0xffffffff,讀出是0xffff0000,推理過程是:運
	// 到上一步時,m_nCount和m_pTIMx->CNT都是零,先m_pTIMx->CNT讀入寄存器,產生下溢出中斷,進入中斷
	// 程序onTimer,m_pTIMx->CNT減1,並從m_nCount借位,結果是:
	//			m_nCount = 0xffff0000,m_pTIMx->CNT
	// 回到這段程序再取m_nCount與前面程序獲取的0合併得到錯誤結果0xffff0000,解決問題的方法是添加中斷
	// 標誌m_bInterrupt,先清零,在中斷程序onTimer中將m_bInterrupt置1,返回前如果m_bInterrupt是1,
	// 再取一次,就能返回正確的值。遺憾的是編譯優化時會刪除這兩行程序,只能把這個文件Encode.cpp的優化
	// 級別設成0,不優化,以後如再發現類似的問題,把這些代碼集中到一個文件,不影響其它代碼的優化。
	if(m_bInterrupt)
		return getData();
	
	return v;
}

void Encoder::setData(s32 nData)
{
	// 中斷標誌清零
	m_bInterrupt = 0;
	
	// 分別設置高16位和低16位
	GET16(m_nCount, 1) = GET16(nData, 1);
	m_pTIMx->CNT = nData;
	
	// 這兩句是重點,如果執行過程中被中斷,再執行一次,參看setData()中的說明
	if(m_bInterrupt)
		setData(nData);
}

/**
  * @date	05/22/2019
  * @brief  計數中斷,設置高16位值
  * @param	None
  * @retval None
  */
void Encoder::onTimer(void)
{
	// 調用基類程序,清TIM中斷位
	Timer::onTimer();
	
	// 設置中斷標誌,非常重要,參看setData()中的說明
	m_bInterrupt = 1;
	
	// 計數溢出中斷,把16位無符號計數擴展到32位有符號計數
	// 只修改m_nCount的高16位
	if(TIM_CR1_DIR & m_pTIMx->CR1)
		GET16(m_nCount, 1)--;	// 向下溢出,高16位減1
	else
		GET16(m_nCount, 1)++;	// 向上溢出,高16位加1
}

Main.h


#ifndef __MAIN__
#define __MAIN__

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"

#pragma diag_default 368	// 恢復368號警告
}

s32 m_nCPUTemperate;		// CPU溫度 x 100

#endif

 Main.cpp

/**
  ******************************************************************************
  * @file		Main.cpp
  * @author		Mr. Hu
  * @version	V1.0.0 STM32F103VET6
  * @date		05/18/2019
  * @brief		程序入口
  *	@io
  *		TIM1	Encode
  *		TIM2	PWM
  *		TIM3	Encode
  *		TIM4	Encode
  *		TIM5	Encode
  *		TIM7	通用定時器
  *		TIM8	Encode
  *		ADC1	ADC
  *		DAC1
  *		DAC2
  *
  *		PA0		TIM5 Encode A
  *		PA1		TIM5 Encode B
  *		PA2		PWM
  *		PA3		PWM
  *		PA4		DAC1輸出,ADC1 數據4
  *		PA5		DAC2輸出,ADC1 數據5
  *		PA6 	ADC1 數據6
  *		PA7 	ADC1 數據7
  *		PA9 	板載串口
  *		PA10 	板載串口
  *		PA13	板載JLINK佔用
  *		PA14	板載JLINK佔用
  *		PA15	板載JLINK佔用
  *
  *		PB1		板載SW2
  *		PB3		板載JLINK佔用
  *		PB4		板載JLINK佔用,TIM3 Encode A
  *		PB5		TIM3 Encode B
  *		PB6		TIM4 Encode A
  *		PB7		TIM4 Encode B
  *		PB8		板載CAN
  *		PB9		板載CAN
  *		PB10	板載RS485
  *		PB11	板載RS485
  *		PB13	板載LED2
  *		PB14	板載LED3
  *		PB15	板載SW3
  *
  *		PC0-3	ADC1 數據0-3
  *		PC4		板載RS485
  *		PC5		板載RS485
  *		PC6		TIM8 Encode A
  *		PC7		TIM8 Encode B
  *
  *		PE9		TIM8 Encode A
  *		PE11	TIM8 Encode B
  ******************************************************************************
  * @remarks
  *
  */ 

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 "stm32f10x_dac.h"

#pragma diag_default 368	// 恢復368號警告
}

#include "stm32f10x_adc.h"
#include "IO.h"
#include "Timer.h"
#include "GeneralTimer.h"
#include "BoardLED.h"
#include "PWM.h"
#include "MedianFilter.h"
#include "AverageFilter.h"
#include "ADDA.h"
#include "Encoder.h"
#include "Main.h" 

/**
  * @date	05/18/2019
  * @brief  主入口,主循環
  *			如果不正常運行,可能是棧設置不夠 startup_stm32f10x_hd.s Stack_Size EQU 0x600
  * @param	None
  * @retval None
  */
int main(void)
{
	m_nCPUTemperate = 0;

	SystemInit();	// 配置系統時鐘爲72M

	GeneralTimer tim(TIM7);		// 通用定時器,實際用TIM7,不佔用IO,但軟件仿真只有1-4,所以選2

	ADDA adda;	// 定時器下緊跟啓動ADDA,因爲轉換需要時間
	//adda.daDMA(tim);	// DMA方式,按數據生成正弦波,使用這個功能時,註釋下面的三角波代碼
	s16 dainc = 1;
	u16 daval = 0;

	BoardLED boardLED( &tim );	// 板載LED
	
	// 板載按鍵,PB1 SW2, PB2 SW3,不同的板子不一樣。
	IO key(GPIOB, GPIO_Pin_1 | GPIO_Pin_15, GPIO_Mode_IPU, 2);	// GPIOx, nPin, GPIO_Mode_IPU 上拉, 2 輸入時無效
	
	// 使能按鍵濾波
	//tim.inb[1].level = 1;		// SW2 PB1 上拉
	tim.inb[1].enable = 1;		// SW2 PB1 使能
	//tim.inb[15].level = 1;	// SW3 PB15 上拉
	tim.inb[15].enable = 1;		// SW3 PB15
	
	u32 loopCount = 0;	// 主循環計數
	
	// PWML模擬編碼器輸出到PA2、PA3
	PWM pwm;
	pwm.orthogonal( 2 - 1, 128 - 1 );	// 140kHz 移相正交波形
	
	// 用杜邦線PA0-PA2、PA1-PA3,把信號傳到TIM5編碼器輸入PA0、PA1
	Encoder en( TIM5 );
	s32 nPrevious = en.getData();
	
	for(int i = 0; i < 3600; i++)	// 延時大約1ms,等待AD轉換後再往下接行,求平均時要以獲得比較準確的初值
	{
		i++;	// 加一句,不然優化編譯時會被刪掉
	}

	// 計算方法
	// 數據手冊 5.3.20 溫度傳感器特性
	// float v2 = d * 5.f / 0xfff;	// 把測量數d(0-ffff)轉換成電壓,單片機用了5V電源,所以用5.f,否則改用3.3f
	// (1.43f - v2) / 0.0043 + 25;	// 1.43f 25度時的電壓值,v2 測量值,0.0043 每度電壓變化
	// 下面是簡化後的公式,因爲沒有FPU,不能用浮點計算,結果單位爲1/100度
	#define CPUT ((s32)35756 - 1221 * adda.m_adData[8] / 43) /* adda.m_adData[8]是內部CPU溫度 */
	MedianFilter mfTemperate( CPUT, 2 );
	AverageFilter afTemperate( CPUT, 3 );
	
	while(1)
	{
		tim.loop();	// 必須放在主循環的第一行,按鍵濾波和上下沿微分。
		
		// PWM
		//pwm.setData(0, 300);	// PWM1 PC6 30%的佔空比
		//pwm.setData(1, 700);	// PWM2 PC7 70%的佔空比
		
		// LED
		// 測試時間
//		loopCount++;
//		if( !tim.m_t[2] )	// 定時器2
//		{
//			tim.m_t[2] = 1000;		// 延時1000ms
//			boardLED.m_nNum = 100 * 1000 / loopCount;	// 計算循環週期,1000*1000對應週期單位是1us,100*1000是10us,以此類推。
//			if( boardLED.m_nNum > 0xf )
//				boardLED.m_nNum = 0xf;		// 大於15時,顯示15
//			loopCount = 0;
//		}
//		boardLED.showNumber();	// 顯示四位二進制boardLED.m_nNum,用了m_t[0]
		
		// CPU溫度 https://blog.csdn.net/qq_27970103/article/details/81325418
		if(!tim.m_t[3])
		{
			s32 mf = mfTemperate.filter( CPUT );		// 中值濾波
			m_nCPUTemperate = afTemperate.filter( mf );	// 平均濾波
			tim.m_t[3] = 100;	// 100ms 計算一次
		}
		
		// 開關LED
		if( tim.inb[1].down | tim.inb[15].down ) 	// 兩個板載開關的下降沿
		{
			boardLED.showLED(GPIO_Pin_14, 1);		// 點亮LED3
		}
		else if( tim.inb[1].up | tim.inb[15].up )	// 兩個板載開關的上升沿
		{
			boardLED.showLED(GPIO_Pin_14, 0);		// 熄滅LED3
		}
				
		// DA-AD 測試,先設置數據,用DA轉換成電壓,再用AD轉換成數字,用示波器觀察,延後1ms
		// 產生三角波
//		SETDAC2( daval );
//		daval += dainc;
//		if(daval > 4095)	// daval是無符號數,減過0以後是很大的數,所以只用一個判斷
//		{
//			dainc = -dainc;		// 改變方向
//			daval += dainc;		// 調到範圍內
//		}
//		u16 test1 = adda.m_adData[5];		// adda.m_adData[5]是PA5電壓的轉換結果,而PA5的電壓是數字adda.m_daData.da2的轉換結果,用了同一個IO腳,不用接線測試
//		SETDAC1(test1);	// 再把結果送到DAC通道1(adda.m_daData.da1 = test1)PA4,再用示波器觀查,延後1ms,DA觸發是1ms
		
		// 這段程序測試兩次數據之間的差值,如果太大說明計數有問題,用此方法發現了溢出中斷會影響正常讀數
		s32 nCount = en.getData();
		if( (nCount - nPrevious) < -0x200 )
		{
			boardLED.m_nNum |= 0x4;
		}
		else if( (nCount - nPrevious) > 0x200 )
		{
			boardLED.m_nNum |= 0x8;
		}
		nPrevious = nCount;
		
		// 判斷計數是否超出,如果超出,限定在指定範圍內。
		nCount >>= 5;
		if( nCount < 0 )
		{
			boardLED.m_nNum |= 0x1;
			nCount = 0;
		}
		else if( nCount > 4095 )
		{
			boardLED.m_nNum |= 0x2;
			nCount = 4095;
		}
		
		boardLED.showNumber();	// 顯示四位二進制boardLED.m_nNum,用了m_t[0]
		
		// PWML模擬編碼器輸出到PA2、PA3
		// 用杜邦線PA0-PA2、PA1-PA3,把信號傳到編碼器輸入
		// 把編碼器數據轉換成電壓,輸出到PA5。
		SETDAC2( nCount );
		// 把PA5電壓轉換成數字,再轉換成電壓,輸出到PA4
		SETDAC1( adda.m_adData[5] );
		
		// 溢出時反向計數,產生三角波
		if( nCount >= 4095 )
			pwm.orthogonal2( 2 - 1, 128 - 1 );	// 到最大值後開始減計數
		else if( nCount <= 0 )
			pwm.orthogonal( 2 - 1, 128 - 1 );	// 到最小值後開始加計數	
	}
}

 註釋了一些程序,新加了一段程序,把LED指示燈改成了錯誤顯示,四短表示正常,其它表示錯誤。

	// PWML模擬編碼器輸出到PA2、PA3
	PWM pwm;
	pwm.orthogonal( 2 - 1, 128 - 1 );	// 140kHz 移相正交波形

 

 

 

 以上代碼,初始化兩路PWM,設爲正交模式,模擬編碼器。

	Encoder en( TIM5 );
	s32 nPrevious = en.getData();

 以上代碼啓動TIM5編碼器模式,用杜邦線連接PA0-PA2、PA1-PA3

	s32 nCount = en.getData();
	if( (nCount - nPrevious) < -0x200 )
	{
		boardLED.m_nNum |= 0x4;
	}
	else if( (nCount - nPrevious) > 0x200 )
	{
		boardLED.m_nNum |= 0x8;
	}
	nPrevious = nCount;

 這段程序測試兩次數據之間的差值,如果太大說明計數有問題,就是用此方法發現了溢出中斷會影響正常讀數,LED指示燈顯示錯誤,前兩次長明。

	// 把編碼器數據轉換成電壓,輸出到PA5。
	SETDAC2( nCount );
	// 把PA5電壓轉換成數字,再轉換成電壓,輸出到PA4
	SETDAC1( adda.m_adData[5] );

 PWM > Encode > DAC2 > ADC1[5] > DAC1,調用了大部分功能,便於示波器測試。波形不太規整,說明干擾比較嚴重,使用時要注意。

全部源程序上傳到CSDN資源中,https://download.csdn.net/download/hhhh63/11289892,最終代碼和端口分配與之前的博文有些區別,不影響總體結構,沒有改過來,請諒解。開發環境Keil4.72,CPU型號STM32F103VET6,不同的開發板引腳可能不一樣,請注意。

寫到這裏,STM32實戰系列告一段落,所有以上程序都經過反覆測試,通過示波器、萬用表和在線模擬等方式驗證,工作正常。之所以叫實戰這個名稱,意思是可用到工業級控制的實用程序,不是簡單的試驗。程序中的各項配置說明不是很詳細,着重寫知識點,代碼中的參考網頁中有詳細描述。把這些程序貼出來,分享給大家,同時也是自己的一個工作總結。以後有時間再加上PID調節、通訊、顯示、多任務,就是一套完整的控制程序了。

STM32實戰系列源碼,按鍵/定時器/PWM/ADC/DAC/DMA/濾波
STM32實戰一 初識單片機
STM32實戰二 新建工程
STM32實戰三 C++ IO.cpp
STM32實戰四 定時器和按鍵
STM32實戰五 板載LED顯示數據
STM32實戰六 PWM加移相正交
STM32實戰七 數字濾波
STM32實戰八 DAC/ADC
STM32實戰九 編碼器
STM32開發過程的常見問題

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