嵌入式-stm32-江科大-EXTI外部中断

news2024/9/30 17:32:12

一:EXTI外部中断(external interrupt)

1.1 STM32 中断系统

在这里插入图片描述

中断是指在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续执行,当中断发生时是由硬件自动调用中断函数执行的,期间编译器会保护现场最后还原现场。使得中断系统极大程度地提高程序的效率,就像是给自己定闹钟,可以不用担心错过时间而可以安心睡觉,在这个过程中,有如下概念:

  • 中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。
  • 中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。

stm32的F1系列总共有68个可屏蔽中断通道(中断源),包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设。所有的中断使用嵌套向量中断控制器NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。具体到某一个型号的芯片可能不会有这多中断,具体需要查看的芯片手册。下面是手册中的中断向量表节选:
在这里插入图片描述

  • 地址(最后一列):存储中断地址,这个地址列表也称为中断向量表。因为程序中的中断函数地址由编译器来分配,所以中断函数地址不固定。但由于硬件的限制,中断跳转只能跳转到固定的地址执行程序。所以为了让硬件能跳转到(一个地址不固定的)中断函数里,就需要在内存中定义一个固定的地址列表。当中断发生后,首先跳转到这个固定的地址列表,编译器会在这个固定的位置加上一条跳转到中断函数的代码,于是中断跳转就可以跳转到任意位置了。中断地址列表就是中断向量表,相当于是中断跳转的跳板。C语言编程无需关注中断向量表,汇编语言需要

在这里插入图片描述

上图给出了嵌套向量中断控制器NVIC的基本结构示意图。在stm32中,NVIC用于统一管理中断和分配中断优先级,属于内核外设,是CPU的小助手,可以让CPU专注于运算。从上图可以看出:

  • NVIC有很多输入口,每个都代表一个中断线路,如EXTI、TIM、ADC等。
  • 每个中断线路上的斜杠n表示n条线,因为一个外设可能会同时占用多个中断通道。
  • NVIC只有一个输出口,通过中断优先级确定中断执行的顺序。

NVIC的中断优先级由优先级寄存器的4位二进制(十进制0~15)决定,这4位可以进行切分,分为高n位的抢占优先级和低(4-n)位的响应优先级。
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队。这个中断号就是指中断向量表的第二列“优先级”。

用医院的叫号系统来举例子。假设医生正在给某个病人看病,外面还有很多病人排队:

  1. 新来的病人 抢占优先级高就相当于直接进屋打断医生,给自己看病。
  2. 新来的病人 响应优先级高就相当于不打扰医生,但直接插队,排在队伍的第一个。

下表是NVIC优先级的分组方式

分组方式抢占优先级响应优先级
分组0(n = 0)0位,取值为04位,取值为0~15
分组1(n = 1)1位,取值为0~13位,取值为0~7
分组2(n = 2)2位,取值为0~32位,取值为0~3
分组3(n = 3)3位,取值0~ 71位,取值为0~ 1
分组4(n = 4)4位,取值为0~150位,取值为0

注:NVIC是内核外设,更多关于NVIC的介绍参考“STM32F10xxx Cortex-M3编程手册”。 NVIC

  • 中断分组的配置寄存器,在SCB_AIRCR中,PRIGROUP这三位就是用于配置中断分组的。

1.2 STM32外部中断EXTI

下图是外部中断向量表片选
在这里插入图片描述

中断系统是管理和执行中断的逻辑结构,外部中断是众多能产生中断的外设之一,而EXTI就是其中之一(比如USART、I2C,参考上面的NVIC基本结构),上图给出了外部中断向量表。EXTI(external interrupt)外部中断可以监测指定的GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使得CPU执行EXTI对应的中断程序。

  • 支持的触发方式:上升沿/下降沿/双边沿/软件触发(上升沿就是低电平到高电平触发)
  • 支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断(比如gpioa_Pin1与gpiob_pin1不能同时触发中断)。
  • 通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒,共20个。

注:后面这四个功能是为了实现一些特殊的功能,比如想实现某个时间让stm32退出停止模式,由于外部中断可以在低功耗模式的停止模式下唤醒stm32,就可以在GPIO口上连接一个RTC时钟作为外部中断。

  • 触发响应方式:中断响应/事件响应。
  • 注意:
    中断响应就是正常的中断流程,申请中断让CPU执行中断函数;
  • 事件响应就是外部中断发生时,不把外部信号给CPU,而是选择触发一个事件,将这个信号通向其他外设,来触发其他外设的操作,可以实现外设之间的联合工作(中介分包)。

下图是EXTI的基本结构
在这里插入图片描述Alternate Fuction I/O:数据选择器(交替作用)
记形状:梯形(多个输入一个输出)

  • 最左侧:GPIO口的外设,每个外设都有16个引脚。
  • AFIO中断引脚选择:本质上就是数据选择器,从前面16*n 个引脚中选择16根端口号不重复的引脚出来,连接到后面的EXTI通道中。在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择。下面是中断引脚选择的AFIO示意图:
    在这里插入图片描述
  • PVD、RTC、USB、ETH:四个特殊功能的外设。
  • EXTI 边沿检测及控制:20个输入通道、两类输出。一类输出到NVIC中,并且将这20路输出的9、10 路外部中断合并在一起以节省通道;另一类输出到其他外设,直接就是20路输出。
    注:上面这个EXTI的基本结构也是编写代码时的主要参考图!

下图是EXTI 框图(方向为从左到右)-stm32F10系列参考手册
在这里插入图片描述

上图给出了参考手册中的EXTI框图。基本逻辑与“EXTI的基本结构”中所述相同,另外还有一些细节:

  • 边沿检测电路+软件中断事件寄存器:这个几个进行或门输出,便可以实现“上升沿/下降沿/双边沿/软件触发”这四种触发方式。
  • 请求挂起寄存器:相当于一个中断标志位,通过读取该寄存器可以判断是哪个通道触发的中断。
  • 中断屏蔽寄存器/事件屏蔽寄存器:相当于开关,只有置1,中断信号才能继续向左走。
  • 脉冲发生器:产生一个电平脉冲,用于触发其他外设工作。

在这里插入图片描述

最后一个问题,到底什么样的设备需要用到外部中段呢?

答:对于stm32来说,若想获取一个由外部驱动的很快的突发信号,就需要外部中断。

  • 如旋转编码器,平常不会有什么变化,但是一旦拧动时,会产生一段时间变化很快的突发信号,就需要stm32能在短时间内快速读取并处理掉这个数据。

  • 再如红外遥控接收头,平常也不会有什么变化,但是一旦接收到信号时,这个信号也是转瞬即逝的。

  • 但是不推荐按键使用外部中断。因为外部中断不能很好的处理按键抖动和松手检测的问题,所以要求不高时,还是建议在主函数内部循环读取。

1.2 旋转编码器介绍
对射式红外传感器就是一种通用传感器模块,已经在第三节“GPIO通用输入输出口”中介绍过,不再赘述。本实验只介绍旋转编码器。

旋转编码器是一种用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向。
在这里插入图片描述

如上图,旋转编码器主要有三种类型:光栅式/机械触点式/霍尔传感器式。下面是这三种形式的介绍:

  1. 光栅式(老款鼠标):配合对射式红外传感器使用,在旋转过程中光栅式编码盘会不断地阻挡/透过红外射线,于是模块便会输出高低电平交替的方波,方波的频率便代表了旋转速度。缺点是只有一路输出,无法判断转动方向。
  2. 机械触点式:内部使用机械触点检测通断,A口和B口输出的方波正交,具体看下面的介绍。当然,也有机械触点式编码器可以一个引脚输出速度信息,一一个引脚输出旋转方向信息。
  3. 霍尔传感器式:直接附在电机后面的编码器,中间是一个圆形磁铁,旋转时两侧的霍尔传感器便可输出正交的方波信号。
  4. 独立的编码器元件:输入轴转动时,输出便有波形。

注:触点式不适合高速旋转的场景,常用于音量调节。非接触式形式的电机可以用于电机测速。
在这里插入图片描述

上图是机械触点式旋转编码器-实物拆解

  • 图片右侧是旋转编码器的旋钮,可以看到下面是一圈可以导电的金属片。
  • 中间有一个大的按键开关结构,也可以检测通断,但是该旋转编码器模块没有使用到该功能。
  • 左右两组金属触点。内部实际的连接如红线标注,C口接地,于是旋钮在旋转过程中就可以使A口、B口输出高低交替的方波。方波频率表示旋转速度。
  • A口、B口配合旋钮,可以产生相位相差90°的方波,称为正交信号。顺时针旋转A口相位超前,逆时针旋转B口相位超前。

在这里插入图片描述

  • R1、R2:上拉电阻。
  • R3、R4:输出限流电阻,防止引脚电流过大。
  • C1、C2:滤波电容,滤除高频不稳定纹波。
    注:C口已经默认接地,只需关心A口、B口的高低变化及相位差即可。

1.3 实验:对射式红外传感器计次

需求:利用stm32的外部中断,对 对射式红外传感器 产生的下降沿进行计次。

在这里插入图片描述上图是对射式红外传感器计次-接线图
下图是对射式红外传感器计次-代码调用
在这里插入图片描述代码展示:OLED.h、OLED.c、OLED_Font.h代码见第四节“OLED调试工具”,本节省略。
文章在这《嵌入式-stm32-江科大-OLED调试工具》

main.c

#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"

int main(void)
{
	//模块初始化
	OLED_Init();       //OLED初始化
	CountSensor_Init(); //计数传感器初始化
	
	//OLED显示,显示静态字符串
	OLED_ShowString(1,1,"Count:");  //一行一列显示字符串
	Delay_ms(500);
	
	
	while(1)
	{
	OLED_ShowNum(2,1,CountSensor_Get(),5);
	}

}

CountSensor.h

#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_H

void CountSensor_Init(void);
uint16_t CountSensor_Get(void);


#endif

CountSensor.c

#include "stm32f10x.h"

uint16_t  CountSensor_Count;     //全局变量,中断触发次数,用于计数
//默认初始化为0

/**
  * 函    数:对射式红外传感器-计数传感器初始化 PB14
  * 参    数:无
  * 返 回 值:无
  */
void CountSensor_Init(void)
{
	//1.开启时钟
	//2.GPIO初始化
	//3.AFIO选择中断引脚
	//4.EXTI初始化
	//5.//NVIC配置,NVIC中断分组
	
	
	//EXTI初始化
	//1.开启GPIO&AFIO的外设时钟(EXTI和NVIC的时钟是一直打开的)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	//2.配置GPIO—PB14上拉输入
	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);
	
	//3.配置AFIO(库函数在GPIO中)中断引脚选择
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);//将外部中断的14号线映射到GPIOB,即选择PB14为外部中断引脚
	//4.配置EXTI初始化
	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);
	
	//5.NVIC配置(库函数在misc.h文件中,杂项)
	//配置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);
}	
/**
  * 函    数:获取计数传感器的计数值,输出中断触发的次数
  * 参    数:无
  * 返 回 值:计数值,范围:0~65535
  */
uint16_t CountSensor_Get(void)
{
	return CountSensor_Count;
}	
   
/**
  * 函    数:EXTI15_10外部中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void EXTI15_10_IRQHandler(void)
{
	//中断标志位判断
	if(EXTI_GetITStatus(EXTI_Line14) == SET)  //判断是否是外部中断14号线触发的中断
	{
		//如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动
		if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_14) == 0 )
		{
			CountSensor_Count ++;  //计数值自增一次
		
		}
		EXTI_ClearITPendingBit(EXTI_Line4);//清除外部中断14号线的中断标志位
											//中断标志位必须清除
											//否则中断将连续不断地触发,导致主程序卡死

	}
}










1.31 编程感想
  1. 对射式传感器采用下降沿触发(移除遮挡触发,硬件设计)。
    传感器无遮挡时,DO输出低电平;传感器有遮挡时,DO输出高电平。所以放入遮挡物意味着触发上升沿,移除遮挡相当于下降沿。采用上升沿触发计数可能不准确,下降沿触发计数准确(实测),若传感器采用上升沿触发那么结论和上面相反,移除遮挡物的时候比较准确。

  2. 中断函数的名字从启动文件“stratup_stm32f10x_md”中来,并且中断函数都是无参无返回值的。

  3. 在CountSensor.c里面GPIO使用结构体来初始化函数、外部中断、ADC、串口都是一样的操作。

  4. 学会使用keil调试,博主第一次烧录程序发现计数传感器没有返回值,灯会闪计数,但是不会反馈到显示屏上。于是我先把老师的代码烧录运行成功,说明硬件没问题,就是自己代码问题,然后一直排除,通过用源码替换自己写的代码过程中,发现是上一个实验的OLED.c问题,CV工程师。

OLED.c(可以跳过)

#include "stm32f10x.h"
#include "OLED_Font.h"

/*引脚配置*/
#define OLED_W_SCL(x)		GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA(x)		GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))

/*引脚初始化*/
void OLED_I2C_Init(void)
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
 	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
 	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
  * @brief  I2C开始
  * @param  无
  * @retval 无
  */
void OLED_I2C_Start(void)
{
	OLED_W_SDA(1);
	OLED_W_SCL(1);
	OLED_W_SDA(0);
	OLED_W_SCL(0);
}

/**
  * @brief  I2C停止
  * @param  无
  * @retval 无
  */
void OLED_I2C_Stop(void)
{
	OLED_W_SDA(0);
	OLED_W_SCL(1);
	OLED_W_SDA(1);
}

/**
  * @brief  I2C发送一个字节
  * @param  Byte 要发送的一个字节
  * @retval 无
  */
void OLED_I2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i++)
	{
		OLED_W_SDA(Byte & (0x80 >> i));
		OLED_W_SCL(1);
		OLED_W_SCL(0);
	}
	OLED_W_SCL(1);	//额外的一个时钟,不处理应答信号
	OLED_W_SCL(0);
}

/**
  * @brief  OLED写命令
  * @param  Command 要写入的命令
  * @retval 无
  */
void OLED_WriteCommand(uint8_t Command)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);		//从机地址
	OLED_I2C_SendByte(0x00);		//写命令
	OLED_I2C_SendByte(Command); 
	OLED_I2C_Stop();
}

/**
  * @brief  OLED写数据
  * @param  Data 要写入的数据
  * @retval 无
  */
void OLED_WriteData(uint8_t Data)
{
	OLED_I2C_Start();
	OLED_I2C_SendByte(0x78);		//从机地址
	OLED_I2C_SendByte(0x40);		//写数据
	OLED_I2C_SendByte(Data);
	OLED_I2C_Stop();
}

/**
  * @brief  OLED设置光标位置
  * @param  Y 以左上角为原点,向下方向的坐标,范围:0~7
  * @param  X 以左上角为原点,向右方向的坐标,范围:0~127
  * @retval 无
  */
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
	OLED_WriteCommand(0xB0 | Y);					//设置Y位置
	OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4));	//设置X位置高4位
	OLED_WriteCommand(0x00 | (X & 0x0F));			//设置X位置低4位
}

/**
  * @brief  OLED清屏
  * @param  无
  * @retval 无
  */
void OLED_Clear(void)
{  
	uint8_t i, j;
	for (j = 0; j < 8; j++)
	{
		OLED_SetCursor(j, 0);
		for(i = 0; i < 128; i++)
		{
			OLED_WriteData(0x00);
		}
	}
}

/**
  * @brief  OLED显示一个字符
  * @param  Line 行位置,范围:1~4
  * @param  Column 列位置,范围:1~16
  * @param  Char 要显示的一个字符,范围:ASCII可见字符
  * @retval 无
  */
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{      	
	uint8_t i;
	OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8);		//设置光标位置在上半部分
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i]);			//显示上半部分内容
	}
	OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8);	//设置光标位置在下半部分
	for (i = 0; i < 8; i++)
	{
		OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]);		//显示下半部分内容
	}
}

/**
  * @brief  OLED显示字符串
  * @param  Line 起始行位置,范围:1~4
  * @param  Column 起始列位置,范围:1~16
  * @param  String 要显示的字符串,范围:ASCII可见字符
  * @retval 无
  */
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i++)
	{
		OLED_ShowChar(Line, Column + i, String[i]);
	}
}

/**
  * @brief  OLED次方函数
  * @retval 返回值等于X的Y次方
  */
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;
	while (Y--)
	{
		Result *= X;
	}
	return Result;
}

/**
  * @brief  OLED显示数字(十进制,正数)
  * @param  Line 起始行位置,范围:1~4
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~4294967295
  * @param  Length 要显示数字的长度,范围:1~10
  * @retval 无
  */
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)							
	{
		OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
	}
}

/**
  * @brief  OLED显示数字(十进制,带符号数)
  * @param  Line 起始行位置,范围:1~4
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:-2147483648~2147483647
  * @param  Length 要显示数字的长度,范围:1~10
  * @retval 无
  */
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
	uint8_t i;
	uint32_t Number1;
	if (Number >= 0)
	{
		OLED_ShowChar(Line, Column, '+');
		Number1 = Number;
	}
	else
	{
		OLED_ShowChar(Line, Column, '-');
		Number1 = -Number;
	}
	for (i = 0; i < Length; i++)							
	{
		OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
	}
}

/**
  * @brief  OLED显示数字(十六进制,正数)
  * @param  Line 起始行位置,范围:1~4
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~0xFFFFFFFF
  * @param  Length 要显示数字的长度,范围:1~8
  * @retval 无
  */
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i, SingleNumber;
	for (i = 0; i < Length; i++)							
	{
		SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
		if (SingleNumber < 10)
		{
			OLED_ShowChar(Line, Column + i, SingleNumber + '0');
		}
		else
		{
			OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
		}
	}
}

/**
  * @brief  OLED显示数字(二进制,正数)
  * @param  Line 起始行位置,范围:1~4
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
  * @param  Length 要显示数字的长度,范围:1~16
  * @retval 无
  */
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i++)							
	{
		OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
	}
}

/**
  * @brief  OLED初始化
  * @param  无
  * @retval 无
  */
void OLED_Init(void)
{
	uint32_t i, j;
	
	for (i = 0; i < 1000; i++)			//上电延时
	{
		for (j = 0; j < 1000; j++);
	}
	
	OLED_I2C_Init();			//端口初始化
	
	OLED_WriteCommand(0xAE);	//关闭显示
	
	OLED_WriteCommand(0xD5);	//设置显示时钟分频比/振荡器频率
	OLED_WriteCommand(0x80);
	
	OLED_WriteCommand(0xA8);	//设置多路复用率
	OLED_WriteCommand(0x3F);
	
	OLED_WriteCommand(0xD3);	//设置显示偏移
	OLED_WriteCommand(0x00);
	
	OLED_WriteCommand(0x40);	//设置显示开始行
	
	OLED_WriteCommand(0xA1);	//设置左右方向,0xA1正常 0xA0左右反置
	
	OLED_WriteCommand(0xC8);	//设置上下方向,0xC8正常 0xC0上下反置

	OLED_WriteCommand(0xDA);	//设置COM引脚硬件配置
	OLED_WriteCommand(0x12);
	
	OLED_WriteCommand(0x81);	//设置对比度控制
	OLED_WriteCommand(0xCF);

	OLED_WriteCommand(0xD9);	//设置预充电周期
	OLED_WriteCommand(0xF1);

	OLED_WriteCommand(0xDB);	//设置VCOMH取消选择级别
	OLED_WriteCommand(0x30);

	OLED_WriteCommand(0xA4);	//设置整个显示打开/关闭

	OLED_WriteCommand(0xA6);	//设置正常/倒转显示

	OLED_WriteCommand(0x8D);	//设置充电泵
	OLED_WriteCommand(0x14);

	OLED_WriteCommand(0xAF);	//开启显示
		
	OLED_Clear();				//OLED清屏
}

1.4 实验:旋转编码器计次

需求:利用stm32的外部中断,对旋转编码器的转动进行计次,顺时针加、逆时针减,并显示在OLED显示屏上。
在这里插入图片描述上图是旋转编码器计次的接线图,下图是代码调用(除库函数以外)
在这里插入图片描述
main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
#include "Encoder.h"

int16_t Num;			//定义待被旋转编码器调节的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();			//OLED初始化
	CountSensor_Init();		//计数传感器初始化
	Encoder_Init();            //旋转编码器初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Num:");	//1行1列显示字符串Count:
	OLED_ShowString(2, 1, "Num+=:");	//1行1列显示字符串Count:
	while (1)
	{	
		Num += Encoder_Get();
		OLED_ShowSignedNum(1, 7, CountSensor_Get(), 5);		//OLED不断刷新显示CountSensor_Get的返回值
		OLED_ShowSignedNum(2,8,Num,5);     //显示Num
	}
}

Encoder.h

#ifndef __ENCODER_H
#define __ENCODER_H

void Encoder_Init(void);
int16_t Encoder_Get(void);

#endif

Encoder.c

#include "stm32f10x.h"

int16_t Encoder_Count;  //全局变量,用于计数旋转编码器的增量值

void Encoder_Init(void)//旋转编码器初始化
{
	  /*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//开启GPIOB的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);		//开启AFIO的时钟,外部中断必须开启AFIO的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;//AB相分别接的是PB0和PB1
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);						//将PB0和PB1引脚初始化为上拉输入
	
	/*AFIO选择中断引脚*/
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);//将外部中断的0号线映射到GPIOB,即选择PB0为外部中断引脚
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);//将外部中断的1号线映射到GPIOB,即选择PB1为外部中断引脚
	
	/*EXTI初始化*/
	EXTI_InitTypeDef EXTI_InitStructure;						//定义结构体变量
	EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;		//选择配置外部中断的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_Init,配置EXTI外设
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;			//选择配置NVIC的EXTI0线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设

	NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;			//选择配置NVIC的EXTI1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;	//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;			//指定NVIC线路的响应优先级为2
	NVIC_Init(&NVIC_InitStructure);		

}


//打算每次调用get函数之后,返回count的变化值,用于外部加减一个变量
int16_t Encoder_Get(void)
{
	//使用Temp变量作为中继,目的是返回Encoder_Count后将其清零,目的是给count清零
	//在这里,也可以直接返回Encoder_Count,直接返回就退出函数了没办法给count清零
	//下面这样子又能获取到count变化值,又能给count清零 
	//直接返回count会导致你扭一下,count就有一个值,Num就会一直变化了
	
	int16_t Temp;
	 Temp = Encoder_Count;
	 Encoder_Count= 0;
	return Temp;
}

void EXTI0_IRQHandler(void)//A口下降沿中断函数
{
	if(EXTI_GetITStatus(EXTI_Line0) == SET)
	{
		
		if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0) //PB1的下降沿触发中断,此时检测另一相,等于1正转,等于0反转
		{
			Encoder_Count --;  //此方向定义为反转,计数变量自减
		}
		
		EXTI_ClearITPendingBit(EXTI_Line0); //清除标志中断0号线的中断标志位
	}

}

void EXTI1_IRQHandler(void)//B口下降沿中断函数
{
	if(EXTI_GetITStatus(EXTI_Line1) == SET)
	{
		if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0) //PB1的下降沿触发中断,此时检测另一相
		{
			Encoder_Count ++;  //此方向定义为正转,计数变量自增
		}
		
		EXTI_ClearITPendingBit(EXTI_Line1); //清除标志中断1号线的中断标志位
	}

}
1.41 编程感想
  1. 管理Hardware文件夹。本次实验继承的是“OLED显示屏”实验的代码,而非“对射式红外传感器计次”。
  2. 注意每个模块在使用的时候都要进行初始化。
  3. 注意进入中断函数的时候要检查中断标志位,,退出的时候清零中断标志位。
  4. 注意主函数和中断函数不要操控同一个硬件,避免不必要的硬件冲突。中断函数一般执行简短快速的代码,如操作中断标志位等。
  5. 在中断函数内部不要执行长时间函数,中断是快速的突发事件,必须要短。

参考:B站STM32江协自动化&【哈工大虎慕】

道友:没有永久的巅峰也没有永远的低谷,真正的强大不是忘记而是接受,接受世事无常、接受孤独挫败、接受突如其来的无力感、接受自己的不完美、接受困惑不安的焦虑和遗憾。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1407113.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

菜鸟初进stable diffusion

不知道是不是玩novelai被boss看到了&#xff0c;推荐了我学stable diffusion 扩散模型 DALL E Midjourney stable diffusion latent diffusion 说是改进点在于“给输入图片压缩降低维度&#xff0c;所以有个latent&#xff0c;从而减少计算量”&#xff0c;类似于下采样吧&…

消息队列RabbitMQ.01.安装部署与基本使用

目录 RabbitMQ的作用 Message queue 释义 问题思考 存在的问题 优化方案 案例分析 带来的好处 消息队列特点 Email邮件案例分析 Docker安装部署RabbitMQ 1.下拉镜像 2.运行RabbitMQ 3.打开防火墙端口号并重新运行防火墙 4.容器启动后,可以通过 docker logs 容器 查…

Servlet 与 MVC

主要内容 Servlet 重点 MVC 重点 Filter 重点 章节目标 掌握 Servlet 的作用 掌握 Servlet 的生命周期 掌握 JSP 的本质 掌握 MVC 的设计思想 掌握 Filter 的作用及使用场景 第一节 Servlet 1. Servlet 概念 Servlet 是在服务器上运行的能够对客户端请求进行处理&a…

leetcode 刷题2

二分查找的绝妙运用&#xff1a; 看到有序数列&#xff0c;算法复杂度 0033. 搜索旋转排序数组 class Solution { public:int search(vector<int>& nums, int target) {int left 0;int right nums.size() - 1;while (left < right) {int mid left (right - …

Debezium发布历史85

原文地址&#xff1a; https://debezium.io/blog/2020/03/05/db2-cdc-approaches/ 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. 运行 Db2 更改数据捕获的方法 2020 年 3 月 5 日 作者&#xff1a; Luis Garcs…

JanusGraph图数据库的应用以及知识图谱技术介绍

目录 JanusGraph介绍 JanusGraph 的主要优势 JanusGraph的应用&#xff1a; JanusGraph 的行业应用&#xff1a; 架构概览 分布式技术应用 横向扩展能力 程序与janus的交互 Janus与图数据库相关概念 结构化存储 图结构存储 实体关系存储 知识存储技术 JanusGraph介…

数据结构<1>——树状数组

树状数组&#xff0c;也叫Fenwick Tree和BIT(Binary Indexed Tree)&#xff0c;是一种支持单点修改和区间查询的&#xff0c;代码量小的数据结构。 那神马是单点修改和区间查询&#xff1f;我们来看一道题。 洛谷P3374(模板): 在本题中&#xff0c;单点修改就是将某一个数加上…

销售额稳居行业第二!苏州金龙2023年跑出高质量发展加速度

2023年&#xff0c;苏州金龙海格客车销量同比去年增25.75%&#xff0c;实现销售11453辆、销售额78亿元的业绩&#xff0c;稳居行业第二位&#xff0c;更跑赢行业大盘&#xff01; 聚焦主业&#xff0c;及时呼应客户需求&#xff1b;聚力新能源技术提升&#xff0c;抓住商用车价…

LabVIEW高级CAN通信系统

LabVIEW高级CAN通信系统 在现代卫星通信和数据处理领域&#xff0c;精确的数据管理和控制系统是至关重要的。设计了一个基于LabVIEW的CAN通信系统&#xff0c;它结合了FPGA技术和LabVIEW软件&#xff0c;主要应用于模拟卫星平台的数据交换。这个系统的设计不仅充分体现了FPGA在…

时间序列大模型:TimeGPT

论文&#xff1a;https://arxiv.org/pdf/2310.03589.pdf TimeGPT&#xff0c;这是第一个用于时间序列的基础模型&#xff0c;能够为训练期间未见过的多样化数据集生成准确的预测。 大规模时间序列模型通过利用当代深度学习进步的能力&#xff0c;使精确预测和减少不确定性成为…

光流估计概念和算法

什么是光流&#xff1f; 光流就是物体和观测者之间的互相运动&#xff0c;亮度变化的速度矢量&#xff0c;下图两张图片表示了光流的原理。 光流的算法有几个基本不变的假设&#xff1a; 1&#xff0c;光强不变假设&#xff1b; 一元的n阶泰勒公式&#xff1a; 在这里插入图…

Mysql复习1--理论基础+操作实践--更新中

Mysql 索引索引的分类 索引InnoDB引擎MyISAM引擎Memory引擎Btree索引支持支持支持hash索引不支持不支持支持R-tree索引不支持支持不支持Full-text索引5.6版本以后支持支持不支持 索引 解释说明: 索引指的是帮助mysql高效的获取数据的结构叫做索引(有序) 没有建立索引的时候–…

Shell 虚拟机基线配置脚本示例

这是一个配置虚拟机基线的示例&#xff0c;包含关闭防火墙、禁用SElinux、设置时区、安装基础软件等。 这只是一个简单的模板&#xff0c;基线配置方面有很多&#xff0c;后续可以按照这个模板去逐步添加 代码示例 [rootbogon ~]# cat bastic.sh #!/bin/bashRED\E[1;31m GRE…

微信万能表单源码系统:自定义表单内容+自由创建多表单 附带完整的代码包以及安装部署教程

在当今信息化社会&#xff0c;在线表单已经成为收集、处理数据的重要工具。无论是企业还是个人&#xff0c;都需要通过表单来进行信息的收集、调查、报名等操作。然而&#xff0c;传统的表单系统往往功能单一&#xff0c;无法满足复杂多变的需求。为了解决这一问题&#xff0c;…

Hadoop3完全分布式搭建

一、第一台的操作搭建 修改主机名 使用hostnamectl set-hostname 修改当前主机名 关闭防火墙和SELlinux 1&#xff0c;使用 systemctl stop firewalld systemctl disable firewalld 关闭防火墙 2&#xff0c;使用 vim /etc/selinux/config 修改为 SELINUXdisabled 使用N…

【操作系统】实验五 添加内核模块

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的很重要&…

《Linux设备驱动开发详解》读书笔记

《Linux设备驱动开发详解》读书笔记 本书主要介绍linux设备驱动开发的方法&#xff0c;共有21章&#xff1a; linux设备驱动概述及开发环境搭建驱动设计的硬件基础linux内核及内核编程linux内核模块linux文件系统与设备文件字符设备驱动linux设备驱动中的并发控制linux设备驱…

Stable Diffusion学习

参考 Stable Diffusion原理详解_stable diffusion csdn-CSDN博客 Stable Diffusion是stability.ai开源的图像生成模型&#xff0c;可以说Stable Diffusion的发布将AI图像生成提高到了全新高度&#xff0c;其效果和影响不亚于Open AI发布ChatGPT。 图像生成的发展 在Stable D…

代码随想录算法训练营第41天|343. 整数拆分、96.不同的二叉搜索树

文章目录 343. 整数拆分思路代码 96.不同的二叉搜索树思路代码 343. 整数拆分 题目链接&#xff1a;343. 整数拆分 文章讲解&#xff1a;代码随想录|343. 整数拆分 视频讲解&#xff1a;整数拆分 思路 1.dp[i]:整数i拆分成k个数的最大乘积 2.dp[i] max(dp[i], max((i - j) *…

DAY08_SpringBoot—整合Mybatis-Plus

目录 1 MybatisPlus1.1 MP介绍1.2 MP的特点1.3 MybatisPlus入门案例1.3.1 导入jar包1.3.2 编辑POJO对象1.3.3 编辑Mapper接口1.3.4 编译YML配置文件1.3.5 编辑测试案例 1.4 MP核心原理1.4.1 需求1.4.2 原理说明1.4.3 对象转化Sql原理 1.5 MP常规操作1.5.1 添加日志打印1.5.2 测…