系列文章目录
STM32单片机系列专栏
C语言理论和实践总结专栏
文章目录
1. 旋转编码器
2. 中断代码编写
2.1 Interrupt.c
2.2 Interrupt.h
2.3 完整工程文件
1. 旋转编码器
旋转编码器主要用于测量轴的旋转位置、速度或者是角度的变化,它能够将转动的角度或者位置转换为电信号输出。通常被用于定位和速度反馈系统,旋转编码器有多种类型,包括增量式和绝对式两大类:
增量式旋转编码器(Incremental Encoder):
- 这种编码器在旋转时产生一系列脉冲,通常有两个输出信号,称为A相和B相,这些信号相互之间有90度的相位差(称为正交输出)。通过计算这些脉冲,你可以确定轴的旋转速度和方向。方向由A相和B相的相位关系确定,速度由脉冲的频率确定。
绝对式旋转编码器(Absolute Encoder):
- 绝对式编码器为每个角度位置提供一个唯一的代码,即使在断电后,设备也能记住轴的具体位置。当电源再次接通时,编码器可以立即提供当前的位置信息,而无需任何移动或寻找参考点。
连接到STM32单片机
- 当连接到STM32单片机时,增量式编码器的A和B输出可以连接到微控制器的两个输入引脚,通常会配置这些引脚为外部中断输入,或者是使用具备输入捕获功能的定时器。编码器的旋转会导致A和B输出产生脉冲,STM32通过捕获这些脉冲来计算位置和速度。
- 速度测量:通过计算一定时间内的脉冲数量,可以测量旋转的速度。
- 方向判定:通过比较A和B两个通道脉冲的相对时序,可以判断旋转的方向。
- 位置确定:对于增量式编码器,通常从一个已知的参考点开始计数脉冲,来确定当前位置。对于绝对式编码器,直接读取编码器的输出即可。
硬件电路
增量式编码器通常有两个输出,称为A和B,它们通常提供相位相差90度的两个方波信号。在这个电路中:
- R1 和 R2 是上拉电阻,确保在开关没有闭合时,A和B输出为高电平(VCC)。
- R3 和 R4 是输出限流电阻,为了防止模块引脚电流过大。
- C1 和 C2 是去抖动电容,用于滤除由于机械开关反跳造成的电气噪声。
- 当编码器旋转时,A和B的输出会在两个开关的作用下生成脉冲信号。
2. 中断代码编写
中断是微控制器程序设计中的一种机制,它允许微控制器在执行常规程序时,响应异步事件(如外部设备的信号变化)。当中断发生时,微控制器会停止当前的操作,保存当前状态,然后跳转到一个特定的中断服务例程(ISR)去处理这个事件。事件处理完成后,它会返回到之前中断的位置,继续执行原来的程序。
对于中断的概念,如果不理解可以看这篇文章:
STM32中断系统详解
对于中断程序的编写,需要两个文件,Interrupt.c 和 Interrupt.h.
2.1 Interrupt.c
首先是初始化:我们需要从GPIO到NVIC这一路的外设模块都配置好,流程如下:
- 配置RCC,将涉及的外设的时钟都打开
- 配置GPIO,选择用到的端口作为输入模式
- 配置AFIO,选择使用到的这一路GPIO,连接到后面的EXTI
- 配置EXTI,选择边沿触发方式,上升沿或者下降沿或者双边沿。选择触发响应方式,中断响应或者事件响应。
- 配置NVIC,给这个中断选择一个合适的优先级。
初始化中断源的硬件
首先需要初始化引脚或硬件模块,让它们能够产生中断信号。对于旋转编码器,通常使用两个引脚来接收编码器的两个输出通道A和B。
// 初始化GPIOB的引脚为输入上拉模式,用来接收编码器的信号
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; // 选择0和1号引脚
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
配置中断线和中断通道
对于STM32微控制器,需要使用AFIO(Alternate Function I/O)和EXTI(External Interrupt)配置特定的GPIO引脚作为外部中断的源。
// 选择PB0和PB1作为外部中断的源
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);
配置外部中断控制器(EXTI)
接下来,配置外部中断控制器EXTI以监听特定的中断线,并设置其触发条件(上升沿、下降沿或双边沿触发)。
// 配置EXTI线0和1,设置为中断模式并且是下降沿触发
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);
配置中断向量控制器(NVIC)
中断服务程序的执行优先级是通过NVIC设置的。这里要设置抢占优先级和子优先级,并使能特定的中断。
实现中断服务程序(ISR)
编写ISR函数以响应中断。ISR的名称在STM32的启动文件中已经预定义,因此必须与启动文件中声明的名称相匹配。
// 处理来自编码器通道A的中断请求
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET) {
// ... 处理中断 ...
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志位
}
}
// 处理来自编码器通道B的中断请求
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET) {
// ... 处理中断 ...
EXTI_ClearITPendingBit(EXTI_Line1); // 清除中断标志位
}
}
在ISR内部,通常会执行如下操作:
- 确认中断来源:通过检查中断标志位确定是否为期望的中断源。
- 处理中断:执行必要的处理,如读取硬件状态、更新变量、设置输出等。
- 清除中断标志位:在处理完中断之后,必须清除相应的中断标志位,否则中断请求会一直存在,导致中断服务程序被重复调用。
下面是Interrupt.c完成代码,和详细注释。
void Encoder_Init(void)
{
/* 开启时钟 */
// 对于GPIOB端口和AFIO(用于重新映射中断线的功能)需要使能它们的时钟。
// RCC_APB2Periph_GPIOB和RCC_APB2Periph_AFIO是在库文件中定义的宏,用来指定时钟线。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // GPIOB时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // AFIO时钟使能
/* GPIO初始化 */
// 初始化GPIOB的0号和1号引脚作为输入。
GPIO_InitTypeDef GPIO_InitStructure; // 声明一个结构体变量,用来初始化GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 设置管脚模式为输入上拉
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; // 选择管脚0和管脚1
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置IO速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); // 应用配置到GPIOB
/* AFIO选择中断引脚 */
// 为外部中断线EXTI0和EXTI1选择PB0和PB1作为中断源。
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0); // 将PB0映射到EXTI的0线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1); // 将PB1映射到EXTI的1线
/* EXTI初始化 */
// 设置EXTI的0线和1线,开启这两条线,并设置为中断模式和下降沿触发。
EXTI_InitTypeDef EXTI_InitStructure; // 声明一个结构体变量,用来初始化EXTI
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1; // 设置要配置的线路是EXTI的0线和1线
EXTI_InitStructure.EXTI_LineCmd = ENABLE; // 使能线路
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; // 设置为中断请求,而非事件请求
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 设置为下降沿触发
EXTI_Init(&EXTI_InitStructure); // 应用配置到EXTI
/* NVIC中断分组 */
// 设置NVIC的优先级分组。分组决定了抢占优先级和子优先级的位宽。
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置分组2,这是一个中间等级的分组设置
/* NVIC配置 */
// 配置NVIC以接收EXTI0和EXTI1的中断请求,并设置相应的优先级。
NVIC_InitTypeDef NVIC_InitStructure; // 声明一个结构体变量,用来初始化NVIC
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; // 设置NVIC的中断通道为EXTI0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能这个中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级设置为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 子优先级设置为1
NVIC_Init(&NVIC_InitStructure); // 应用配置到NVIC
// EXTI1线路设置相似的NVIC配置
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; // 设置NVIC的中断通道为EXTI1
// 其余配置与EXTI0相同,除了子优先级为2,这意味着如果EXTI0和EXTI1同时请求,EXTI0的处理会被优先考虑
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //指定NVIC线路的响应优先级为2
NVIC_Init(&NVIC_InitStructure); // 应用配置到NVIC
}
// 函数名:Encoder_Get
// 功能:读取自上次调用此函数以来编码器的增量值
// 返回值:增量值,类型为int16_t(16位有符号整型)
// 使用Temp变量作为中继,目的是返回Encoder_Count后将其清零
// 在这里,也可以直接返回Encoder_Count,但这样就不是获取增量值的操作方法了,也可以实现功能,只是思路不一样
int16_t Encoder_Get(void)
{
int16_t Temp; // 定义一个局部变量Temp,用于暂存编码器的增量值
Temp = Encoder_Count; // 将全局变量Encoder_Count的值赋给Temp
Encoder_Count = 0; // 将全局计数器清零,为下次计数做准备
return Temp; // 返回增量值
}
// 函数名:EXTI0_IRQHandler
// 功能:外部中断0的中断服务程序
// 当外部中断0触发时,由中断控制器自动调用此函数
// 无返回值,无参数
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET) // 检查EXTI_Line0的中断是否被触发
{
// 为防止机械抖动误触发,再次检查GPIO_Pin_0的电平,如果出现数据乱跳的现象,可再次判断引脚电平
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) // 如果PB0的电平为低(因为是下降沿触发)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) // PB0的下降沿触发中断,同时检查PB1的电平,以判断旋转的方向
{
Encoder_Count--; // 如果PB1也为低,假定为反方向旋转,计数减一
}
}
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志位,以防止中断再次触发
}
}
// 函数名:EXTI1_IRQHandler
// 功能:外部中断1的中断服务程序
// 当外部中断1触发时,由中断控制器自动调用此函数
// 无返回值,无参数
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET) // 检查EXTI_Line1的中断是否被触发
{
// 为防止机械抖动误触发,再次检查GPIO_Pin_1的电平
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) // 如果PB1的电平为低(因为是下降沿触发)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) // 同时检查PB0的电平,以判断旋转的方向
{
Encoder_Count++; // 如果PB0也为低,假定为正方向旋转,计数加一
}
}
EXTI_ClearITPendingBit(EXTI_Line1); // 清除中断标志位,以防止中断再次触发
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
2.2 Interrupt.h
这里只需要声明一些函数就可以了
#ifndef __ENCODER_H
#define __ENCODER_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
2.3 完整工程文件
基于STM32单片机使用中断实现旋转编码器计次