这一章编写编码器程序,通过定时器连接编码器,原理和细器节这里不多说,参考代码段中的网页,有两个注意事项,一是所有网上的参考代码都没有设置第二个通道,默认没有滤波,虽然能用,但是通道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'
*
* 参考资料
* 贴子中的TIM_ICPolarity_BothEdge未定义
*
* 按以下参数,用两个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温度
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资源中,,最终代码和端口分配与之前的博文有些区别,不影响总体结构,没有改过来,请谅解。开发环境Keil4.72,CPU型号STM32F103VET6,不同的开发板引脚可能不一样,请注意。
写到这里,STM32实战系列告一段落,所有以上程序都经过反复测试,通过示波器、万用表和在线模拟等方式验证,工作正常。之所以叫实战这个名称,意思是可用到工业级控制的实用程序,不是简单的试验。程序中的各项配置说明不是很详细,着重写知识点,代码中的参考网页中有详细描述。把这些程序贴出来,分享给大家,同时也是自己的一个工作总结。以后有时间再加上PID调节、通讯、显示、多任务,就是一套完整的控制程序了。