完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
这一章编写编码器程序,通过定时器连接编码器,原理和细器节这里不多说,参考代码段中的网页,有两个注意事项,一是所有网上的参考代码都没有设置第二个通道,默认没有滤波,虽然能用,但是通道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,设为正交模式,模拟编码器。 // 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调节、通讯、显示、多任务,就是一套完整的控制程序了。 |
|
|
|
只有小组成员才能发言,加入小组>>
调试STM32H750的FMC总线读写PSRAM遇到的问题求解?
1627 浏览 1 评论
X-NUCLEO-IHM08M1板文档中输出电流为15Arms,15Arms是怎么得出来的呢?
1550 浏览 1 评论
984 浏览 2 评论
STM32F030F4 HSI时钟温度测试过不去是怎么回事?
688 浏览 2 评论
ST25R3916能否对ISO15693的标签芯片进行分区域写密码?
1601 浏览 2 评论
1867浏览 9评论
STM32仿真器是选择ST-LINK还是选择J-LINK?各有什么优势啊?
650浏览 4评论
STM32F0_TIM2输出pwm2后OLED变暗或者系统重启是怎么回事?
518浏览 3评论
536浏览 3评论
stm32cubemx生成mdk-arm v4项目文件无法打开是什么原因导致的?
506浏览 3评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-11-24 21:23 , Processed in 0.804337 second(s), Total 80, Slave 63 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号