STM32
直播中

何夏庄

7年用户 901经验值
私信 关注
[问答]

如何去编写定时器接编码器的程序呢

怎样通过定时器去连接编码器呢?

如何去编写定时器接编码器的程序呢?

回帖(2)

张恒

2021-11-15 16:13:50
  这一章编写编码器程序,通过定时器连接编码器,原理和细器节这里不多说,参考代码段中的网页,有两个注意事项,一是所有网上的参考代码都没有设置第二个通道,默认没有滤波,虽然能用,但是通道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方式,按数据生成正弦波,使用这个功能时,注释下面的三角波代码
  
举报

王静

2021-11-15 16:14:08
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,设为正交模式,模拟编码器。
  // PWML模拟编码器输出到PA2、PA3
  PWM pwm;
  pwm.orthogonal( 2 - 1, 128 - 1 ); // 140kHz 移相正交波形
  以上代码启动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调节、通讯、显示、多任务,就是一套完整的控制程序了。
举报

更多回帖

发帖
×
20
完善资料,
赚取积分