目录
1、EXTI基础知识补充
2、编程要点
3、对射式红外传感器计次完整代码(注释)
4、旋转编码器计次完整代码(注释)
参考江科大32单片机教学视频!
旋转编码器工作原理
https://www.bilibili.com/video/BV1JJ411s7p3/?spm_id_from=333.337.search-card.all.click&vd_source=a302e304b0aa60652c390b422ff81ab8
1、EXTI基础知识补充
对于旋转编码器,正向旋转时,A、B相输出的波形为下图(上),反向旋转时,A、B相输出的波形为下图(下)。
1、如果把一相的的下降沿用作触发中断,在中断时刻读取另一相的电平,此时正转就是高电平,反转就是低电平,这样就能区分旋转方向。
(小瑕疵:正转时,由于A相先出现下降沿,旋转编码器刚开始动,就进入中断,而反转时,A相后出现下降沿,只有转到位了,才能进入中断)
2、改进方案:只有在B相下降沿和A相低电平时,才判断为正转,在A相下降沿和B相低电平时,才判断为反转。这样能够保证正转和反转都是转到位了,才执行数字的加减操作。
上图对应的中断服务函数为中断0服务函数和中断1服务函数
//中断0服务函数
void EXTI0_IRQHandler(void)
{
//检查函数标志位
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
/* 如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动 */
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Encoder_Count --;
}
}
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
//中断1服务函数
void EXTI1_IRQHandler(void)
{
//检查函数标志位
if (EXTI_GetITStatus(EXTI_Line1) == SET)
{
/* 如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动 */
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
Encoder_Count ++;
}
}
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
2、编程要点
1)初始化用来产生中断的GPIO
2)初始化EXTI
3)配置NVIC
4)编写中断服务函数
3、对射式红外传感器计次完整代码(注释)
接线图
注意
如果你想在主程序力查看和清除标志位,就用(一般的读写标志位)
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);
void EXTI_ClearFlag(uint32_t EXTI_Line);
如果你想在中断函数里查看和清除标志位,就用
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);
CountSensor.c
#include "stm32f10x.h" // Device header
//我们想要一个数字来统计中断触发的次数
uint16_t CountSensor_Count;
//设计的外设包括RCC GPIO AFIO EXTI NVIC
void CountSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
//EXTI(原因不详) 和 NVIC(内核外设都不需要开启时钟)的时钟一直都打开,不需要我们开启时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入,默认为高电平
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* 与AFIO相关的重要的函数配置 */
//GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState); //作用是可用来进行引脚重映射,第一个参数可以选择重映射的方式 第二个参数是新的状态
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14); //配置AFIO的数据选择器,来选择我们想要的中断引脚
//将EXTI的第14个线路配置为中断模式,下降沿触发,开启中断,这样PB14的电平信号就能够通过EXTI通向下一级的NVIC
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line14; //指定我们需要配置的中断线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //指定选择的中断线的新状态
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //中断模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发
EXTI_Init(&EXTI_InitStructure);
//配置NVIC
//中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //这个分组方式整个芯片只能用一种,因此这个分组的代码整个工程只需要执行一次即可
//中断初始化
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //优先级是在多个中断源同时申请,产生拥挤时才有作用,这只有一个中断,随机匹配即可
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
//返回变量CountSensor_Count
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
//中断函数都是无参无返回值
//中断函数不需要调用,它是自动执行的
void EXTI15_10_IRQHandler(void)
{
//中断标志位的判断,确保是我们想要的中断源触发的这个函数,因为这个函数EXTI10到15都能进来,因此需要判断是否是EXTI14
if (EXTI_GetITStatus(EXTI_Line14) == SET)
{
//如果是,我们就执行中断程序
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14) == 0)
{
CountSensor_Count ++;
}
//中断程序结束后,一定要再调用一下清除中断标志位的函数,因为只要中断标志位置1,程序就会跳转到中断函数
//如果你不清除中断标志位,那他就会一直申请中断,程序就会不断响应中断,执行中断函数,程序就会卡死在中断函数里
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
CountSensor.h
#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_H
void CountSensor_Init(void);
uint16_t CountSensor_Get(void);
#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
int main(void)
{
OLED_Init();
CountSensor_Init();
OLED_ShowString(1, 1, "Count:");
while (1)
{
//显示计次的数据
OLED_ShowNum(1, 7, CountSensor_Get(), 5);
}
}
4、旋转编码器计次完整代码(注释)
接线图
Encoder.c
#include "stm32f10x.h" // Device header
//由于需要正反转,因此定义一个带符号的变量
int16_t Encoder_Count;
//初始化PB0和PB1两个GPIO口的外部中断
void Encoder_Init(void)
{ //初始化GPIO和AFIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
//初始化GPIO,PB0和PB1
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);
//AFIO中断引脚选择
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); //将第0个线路拨到GPIOB上
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1); //将第1个线路拨到GPIOB上
//指定中断线,同时将第0条线路和第1条线路初始化为中断模式,下降沿触发
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);
}
//定义函数,返回变量,在这里不直接返回Encoder_Count变量,而是返回每次调用这个get函数后的Count变化值
//因为用count返回的话,return直接跳出函数,count就无法清零
int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
//中断0服务函数
void EXTI0_IRQHandler(void)
{
//检查函数标志位
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
/* 如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动 */
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Encoder_Count --;
}
}
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
//中断1服务函数
void EXTI1_IRQHandler(void)
{
//检查函数标志位
if (EXTI_GetITStatus(EXTI_Line1) == SET)
{
/* 如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动 */
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
Encoder_Count ++;
}
}
//清除中断标志位
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
/* 中断编程的建议
1、在中断函数里,最好不要执行耗时过长的代码,中断函数要简短快速,别刚进中断就执行一个Delay多少毫秒的代码
因为中断时处理突发的事情,如果你为了一个突发的事情呆在中断里不出来,主程序就会收到严重的阻塞
2、最好不要在中断函数和主函数调用相同的函数或者操作同一个硬件,如OLED显示函数,如果既在主函数里调用OLED函数
又在中断里调用OLED,OLED就会显示错误
为了避免问题,最好不要在主程序和中断程序里操作可能产生冲突的硬件
在实现功能的时候,可以在中断里操作变量或者标志位
当中断返回时,我在对这个变量进行显示和操作
这样既能保证中断函数的简短快速,又能保证不产生冲突的硬件操作
EXTI_GetITStatus() 函数用于获取指定的中断标志位的状态,返回值为标志位的状态,即是否被触发。如果返回值为 SET,则表示对应的中断已经被触发;如果返回值为 RESET,则表示对应的中断还未被触发。
EXTI_ClearITPendingBit() 函数用于清除指定的中断标志位。当某个中断被触发时,对应的中断标志位会被置位。在处理完中断后,需要通过调用 EXTI_ClearITPendingBit() 函数来清除该标志位,否则会一直保持置位状态,导致下一次中断无法正常触发。
因此,这两个函数常常一起使用,以确保外部中断能够正常工作。
*/
Encoder.h
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
main.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)
{
//因为Get函数返回的是调用这个函数的间隔里旋转编码器产生的正负脉冲数,所以返回值直接+=给Num,就能够对Num进行加减操作
Num += Encoder_Get();
OLED_ShowSignedNum(1, 5, Num, 5);
}
}