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 "" 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 "" 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 "" 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 "" 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

STM32实战九 编码器_第1张图片

	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,调用了大部分功能,便于示波器测试。波形不太规整,说明干扰比较严重,使用时要注意。

STM32实战九 编码器_第2张图片

全部源程序上传到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开发过程的常见问题

你可能感兴趣的:(STM32实战九 编码器)