目录
1 接线图
2 工程文件
3 代码
3.1 旋转编码器模块
3.2 初始化函数
3.3 中断函数
3.4 功能代码
3.5 本节课代码
1 接线图
第二个程序,先看一下接线图。打开接线图的文件夹(STM32入门\程序源码\STM32Project-有注释版\1-1 接线图),打开【5-2 旋转编码器计次.jpg】的图片。
右边还是OLED的电路,左边这里插了一个旋转编码器模块。上面VCC和GND接正负极,下面这两个A,B相的输出引脚,分别接到STM32的PB0和PB1两个引脚,这就是硬件电路。
我们在面包板上接一下。首先把旋转编码器模块插到面包板上,因为这个模块的引脚间距并不是标准的距离,所以要稍微别一下引脚,才能插到面包板上。然后用面包板跳线接一下电源,vcc接电源正极,GND接电源负极。下面的两个零件用这个面包板飞线接一下,A相输出接PB0,B相输出接PB1,这样硬件电路就完成了。
2 工程文件
找到工程文件夹,复制一下【4-1 OLED显示屏】的工程,改下名字,叫:【5-2 旋转编码器计次】。
打开Project.uvprojx工程。
先关掉无用的窗口。
把这多余的代码删掉。
编译试一下,没有问题。
下载,看一下oled显示【HelloWorld!】,有没有问题。
3 代码
3.1 旋转编码器模块
在Keil中,先把旋转编码器建一个模块。Hardware处右键,添加新的文件。
选择c文件,起个名字叫:【Encoder】,路径加上反斜杠Hardware,Add。
在Hardware处右键,添加新文件。选择.h文件,也叫:【Encoder】,路径加上反斜杠Hardware,Add。
.h文件中,写固定框架。
然后.c文件,右键,添加一下头文件。
3.2 初始化函数
写一个初始化函数。
初始化一下PB0和PB1,两个GPIO口的外部中断。
当然这里只初始化一个外部中断,其实也是可以完成功能的。因为对于这个编码器而言,正向旋转时,A、B相输出的是这样的波形。(上A,下B。后同)
反向旋转时,输出的是这样的波形。
如果把一相的下降沿用作触发中断,在中断时刻读取另一相的电平。正转就是高电平,反转就是低电平,这样就能区分旋转方向了。
只不过这样在操作上有一些小瑕疵。比如你正转的时候,由于A相先出现下降沿,所以你刚开始动,就进中断了。而反转是A相后出现下降沿,所以就是你转到位了,才进入终端。这样实际上也没问题,就是有点不爽。
所以我准备的就是A、B相都触发中断,只有:
在B相下降沿和A相低电平时,才判断为正转。
在A相下降沿和B相低电平时,才判断为反转。
这样就能保证正转、反转都是转到位了,才执行数字加减的操作。同时这样也可以给大家演示一下两个中断的初始化代码。
那我们开始写代码。我们先把上个代码的初始化部分复制过来,我们打开上一个工程(Project——5-1 对射式红外传感器计次)。
然后把这些CountSensor_Init初始化代码复制一下。
再回来。
粘贴放在这里。
然后改一下,首先初始化时钟RCC_APB2Periph_GPIOB和RCC_APB2Periph_AFIO这些不用改。接着初始化GPIO我们用的是pb 0和pb 1,所以这里改一下,变成GPIO_Pin_0或上GPIO_Pin_1。
然后是AFIO中断引脚选择,我们把这个14改成0,将第0个线路拨到GPIOB上,然后复制粘贴一下这个代码,再将第1个线路拨到GPIOB上,这样AFIO就完成了。
然后是EXTI部分,我们把这个指定的中断线改成EXTI_Line0或者EXTI_Line1。EXTI_Trigger改为:【EXTI_Trigger_Falling】。
这样就能同时把第0条线路和第1条线路都初始化为中断模式、下降沿触发了。
接着中断分组不用改。下面中断优先级,要对两个通道分别设置优先级。我们复制一下下面的这部分代码。
定义结构体变量的就不用复制了,这个变量可以重复使用。
然后改一下上面的改成:【EXTI0_IRQn】,优先级抢占和响应都还是1。把下面的改成:【EXTI1_IRQn】,就把这个响应优先级改低一些,设置为2。
3.3 中断函数
这样同时初始化EXTI0和EXTI01的代码就改好了。然后我们把这两个中断的中断函数都写一下。双击打开启动文件Start——startup_stm32f10x_md.s。
看到这里有这两个中断的中断函数。
这里EXTI0和EXTI01是分别独占一个函数的,所以先复制一下这个EXTI0_IRQHandler,回到这里。写上:void EXTI0_IRQHandler(void),在中断里我们也固定的检查一下中断标志位,虽然这个函数只有EXTI0可以触发,但是作为固定模式的代码,我们还是写一下。在这里就写:if (EXTI_GetITStatus(EXTI_Line0) == SET)。如果是的话,执行中断程序。最后清除中断标志位。
然后复制一下这个函数,改一下函数名,这里是EXTI1_IRQHandler,里面判断标志位改成EXTI_Line1,清除标志位也是EXTI_Line1,这样中断就完成了。
这里这两个中断是进的两个函数。如果你使用的是9_5和15_10的这些中断,那只能写一个中断函数,这样就只需要把这两个if并列的放在一个函数里就行了。
3.4 功能代码
写实现功能的代码,先在最上面定义一个变量。因为这里需要正反转,所以就定义一个带符号的变量。
接着在中断函数里,EXTI0_IRQHandler里,我们先判断一下另一个引脚的电平,是不是等于等于0。如果是,那就是反转,就Encoder_Count --。当然这个是正转还是反转,你可以自己定义,正和反都是相对的。
然后在EXTI1_IRQHandler里,我们就if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0),如果是,那就是正转,就Encoder_Count ++。这样中断对这个变量的加减就完成了。
编译一下,没有问题。
然后我们需要再定义一个函数,把这个变量返回回去,所以在这个位置写上:int16_t Encoder_Get(void),在这里面我就不直接返回【Encoder_Count】这个变量了。我打算返回每次调用这个Get函数之后,返回count的变化值,用于外部加减一个变量。所以在这里我需要返回count,然后把count清零。
那因为返回count之后函数就结束了,没法清零了。所以我先定义一个临时变量Temp,把Count先赋值给Temp,再把Count清零,最后返回Temp。这样就完成了。
然后我们把这个Encoder_Get函数放在头文件声明一下,上面这个Encoder_Init函数也声明一下。
编译看看,没有问题。
在main.c里,先#include "Encoder.h",然后Encoder_Init();
假设这里有一个变Num,需要调用Encoder_Get模块,对它进行加减。那就可以在主循环里写:Num += Encoder_Get();
因为这个Encoder_Get函数,返回的是调用这个函数的间隔里旋转编码器产生的正负脉冲数,所以这个返回值直接+=给Num,就能对Num进行加减操作了。
加一下OLED显示,在这里调用OLED_ShowSignedNum,在一行五列显示Num,长度为5。上面的显示字符串也改一下,一行一列显示Num:,这样代码就完成了。
编译,看一下。
下载。
现在我们向右转编码器,数字就增加。向左转数字减小。
3.5 本节课代码
Encoder.c
#include "stm32f10x.h" // Device header int16_t Encoder_Count; void Encoder_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB,&GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);//AFIO外设中断引脚选择配置 GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);//AFIO外设中断引脚选择配置 EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//中断模式 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿都触发 EXTI_Init(&EXTI_InitStructure); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_Init(&NVIC_InitStructure); } int16_t Encoder_Get(void) { int16_t Temp; Temp = Encoder_Count; Encoder_Count = 0; return Temp; } void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) == SET) { if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)//判断另一个引脚的电平 { Encoder_Count --;//反转 } EXTI_ClearITPendingBit(EXTI_Line0); } } void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) == SET) { if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0)//判断另一个引脚的电平 { Encoder_Count ++;//正转 } EXTI_ClearITPendingBit(EXTI_Line1); } }Encoder.h
#ifndef __ENCODER_H #define __ENCODER_H void Encoder_Init(void); int16_t Encoder_Get(void); #endifmain.c
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Encoder.h" int16_t Num; int main(void) { OLED_Init(); Encoder_Init(); OLED_ShowString(1,1,"Num:"); while(1) { Num += Encoder_Get(); OLED_ShowSignedNum(1,5,Num,5); } }这就是第二个代码的全部内容了。
最后再给大家提几个中断编程的建议,第一个就是在这个中断函数里,最好不要执行耗时过长的代码,中断函数要简短快速,别刚进中段就执行一个Delay多少毫秒这样的代码,因为中断是处理突发的事情,如果你为了处理一个突发的事情,待在中断里不出来了,那主程序就会受到严重的阻塞。
另外就是最好不要在中断函数和主函数调用相同的函数,或者操作同一个硬件,尤其是硬件相关的函数。比如Oled显示函数。如果你既在主函数里调用Oled,又在中断里调用Oled,Oled就会显示错误。为什么呢?因为你想在主程序里Oled刚显示一半,他进中断了,结果中断,你还是Oled显示函数,Oled就挪到其他地方显示了。这时还没有问题,但当中断结束之后,需要继续原来的显示,这时候就出问题了,因为硬件的显示位置被挪到其他地方,所以再回来的时候继续显示的内容就会跟着跑到其他地方去,这就会造成问题。
虽然在中断进入和退出的时候会有保护现场和恢复现场,但这只能保证cpu程序能正常返回,不出问题。对于外部硬件的话,并没有在进入中断时进行现场保护,所以中断返回后就出问题了。那为了避免这样的问题,就最好不要在主程序和中断程序里操作可能产生冲突的硬件。
在实现功能的时候,可以像我这样在中断里操作变量或者标志位。当中断返回时,我在对这个变量进行显示和操作,这样既能保证中断函数的简短快速,又能保证不产生冲突的硬件操作。这就是中断程序设计的注意事项,那在其他地方大家也都可以多用用变量或者标志位,来减少代码之间的耦合性。让各部分代码相互独立,仅使用变量标志位或者函数作为接口,这样能让程序更加清晰,代码更加强健。
那本节外部中断的部分到这里就结束了,我们下节再见。