這一章編寫編碼器程序,通過定時器連接編碼器,原理和細器節這裏不多說,參考代碼段中的網頁,有兩個注意事項,一是所有網上的參考代碼都沒有設置第二個通道,默認沒有濾波,雖然能用,但是通道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開發過程的常見問題