OLED
OLED(Organic Light Emitting Diode):有机发光二极管
OLED显示屏:性能优异的新型显示屏,具有功耗低、相应速度快、宽视角、轻薄柔韧等特点
0.96寸OLED模块:小巧玲珑、占用接口少、简单易用,是电子设计中非常常见的显示屏模块
供电:3~5.5V,通信协议:I2C/SPI,分辨率:128*64
常用于调试。
通常是I2C协议 SPI协议
调式方法
串口调试:通过串口通信,将调试信息发送到电脑端,电脑使用串口助手显示调试信息
可以借助电脑来调试,可以更丰富的参数设置,只能以信息流的方式呈现(也就是一行一行打印),所以只能刷屏进行显示;
显示屏调试:直接将显示屏连接到单片机,将调试信息打印在显示屏上
对于不断变化的数据,可覆盖刷新显示,显示方式直接,但是显示屏幕有限
Keil调试模式:借助Keil软件的调试模式,可使用单步运行、设置断点、查看寄存器及变量等功能
电灯调试法:在不确定的地方进行加入电灯的代码,来测试是否有电流
注释调式法
硬件电路:
通信引脚需要放到单片机I2C通信的引脚上,在STM32上用江协科技的GPIO模拟库出来的通信引脚,所以可以任意接入。
OLED驱动参数
将屏幕分划成4X16的小模块
相关函数
接线图
4-1 OLED显示屏
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
//#include "OLED_Font.h"
//4-1 OLED显示屏
int main(void){
OLED_Init();
OLED_ShowChar(1,1,'a');
OLED_ShowString(1,3,"hello world!"); //如果超出16个字,会从头到尾重新输入
OLED_ShowNum(2,1,12345,5);//如果长度比输入的数字大,则会在输入的数字前自动补0;如果比其小,则会向最高位进行切割
OLED_ShowSignedNum(2,7,12345,6);
OLED_ShowHexNum(3,1,0xAA55,4);
OLED_ShowBinNum(4,1,0xAA55,16);
//OLED_Clear();
while(1){
}
}
调试程序:
外部中断和中断系统
简介
中断系统:管理和执行中断的逻辑结构
外部中断:是众多能产生中断的外设之一
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行
中断触发条件:对于外部中断来说,可以是引脚发生了电平跳变
对于定时器来说,可以是定时的时间到了
对于串口通信来说,可以是接受到了数据
若不做中断处理,当下一个电平跳变等来临时,没及时处理原数据被新数据覆盖或被忽略
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。由自己设定优先级。
中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。
中断执行流程
68个可屏蔽中断通道(中断源),包含EXTI(外部中断)、TIM(定时器)、ADC(模数转换器)、USART(串口)、SPI(通信)、I2C(通信)、RTC(实时时钟)等多个外设
使用NVIC(STM32中用来管理中断、分配优先级)统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。
STM32的中断资源
内核的中断
外设中断资源:
0:WWDG 窗口看门狗,用来检测程序运行状态的中断;如果没有及时喂狗,则看门狗会自动申请中断
补:看门狗之后会接触,现在只是方便理解
可以把 窗口看门狗(WWDG) 想象成一个 严格的闹钟,而 “喂狗” 就是按时按点去关掉它。如果不按它的规矩来,它就会 “发火”,直接重启你的系统!
1:PVD电源电压检测,如果供电电压不足,PVD电路就会申请中断
6~10 & 23 & 40:EXTI0~10为本节课的对应中断
地址部分:
中断的地址:在程序中的中断函数,它的地址是由编译器来分配的,是不固定的;但是中断跳转,由于硬件的限制,只能跳到固定的地址执行程序。为了能让硬件跳转到一个不固定的中断函数里,就需要在内存中定义一个地址的列表(中断向量表,相当于中断跳转的一个跳板),这个列表地址是固定的,中断发生后,就跳到这个固定位置,然后在这个固定位置,由编译器,再加上一条跳转到中断函数的代码,这样可以中断跳转跳转到任意位置了。
一般C语言的编译器会自动完成这个功能,所以一般不会考虑。
NVIC
NVIC(嵌套中断向量控制器)基本结构:用来统一分配中断优先级和管理中断的;
是一个内核外设,是CPU的小助手;
参考:Cortex-M3编程手册
:一个外设可能会同时占用多个中断通道,所以这里有n条线
NVIC只有一个输出口,NVIC根据每个中断的优先级分配中断的先后顺序,告诉CPU应该处理哪个中断。所以关于中断先后顺序分配的任务,cpu是不知道的
中断分组:
抢占优先级:插队+立马执行的优先级:当CPU中正在执行一个中断时,拥有抢占优先级的中断会直接占进来,让CPU停下对原先的中断执行,来执行此中断(中断嵌套);
响应优先级:插队的优先级;
NVIC的中断优先级由优先级寄存器的4位(0~15的数:值越小,优先级越高)决定,这4位可以进行切分,分为:1.高n位的抢占优先级和2.低4-n位的响应优先级
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队(越小,优先级越高)
EXTI(Extern Interrupt)外部中断
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序(引脚电平变化,申请中断),属于内核外设。
支持的触发方式:上升沿(低电平变到高电平时瞬间触发中断)/下降沿(反之)/双边沿(上升沿和下降沿都可以触发中断)/软件触发(引脚啥事没有,程序执行一句代码,就触发中断)
支持的GPIO口:所有GPIO口,但相同的Pin不能同时触发中断(如:PA0和PB0)
通道数:16个GPIO_Pin,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒(后四个属于蹭网:因为外部中断有个功能:从低功耗模式的停止模式下唤醒STM32;
对于PVD电源电压检测,当电源从电压过低恢复,就需要PVD外部中断退出停止模式;
对于RTC闹钟,为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒;)
触发响应方式:中断响应/事件响应(不会触发中断,而会触发其他外设事件)
外部中断的基本结构体
AFIO:数据选择器,会在前面的n个GPIO外设的16个引脚里选择其中一个连接到后面EXTI的通道里,所以相同的Pin不能同时触发
经过EXTI电路之后,分为两种输出:
1.指向NVIC:用来触发中断
其中EXTI9_5和EXTI15_10:指外部中断的9~5会触发同一个中断函数,同理;所以在中断函数里,需要再根据标志位来区分到底哪个中断进来的
2.事件响应
AFIO复用IO口:
AFIO主要用于引脚复用功能的选择和重定义
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射
和中断引脚选择
EXTI框架图:
或门:可以有多个输入,但只能有一个输出,执行或的逻辑,只要有一个高电平(1),输出就是高电平
与门:可以有多个输入,但只能有一个输出,执行与的逻辑,只要有一个低电平(0),输出就是低电平
非门:只有一个输入和一个输入,输入0就是1
数据选择器:侧边有选择控制端,根据控制端的数据,从输入选择一个接到输出
硬件模块:
使用中断模块的特性:对于STM32,想要获取的信号是外部驱动的很快的突发信号,STM32稍微晚一点,就会错过,所以需要中断模块:当有脉冲过来,STM32立马进入中断函数处理。
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向。
类型:机械触点式/霍尔传感器式/光栅式
光栅式旋转编码器工作:
对射式红外传感器用来测速,同时还需要用一个光栅编码盘:当编码盘转动时,红外传感器的红外光就会出现遮挡、透过这样的现象,对应模块输出的电平就是高低电平交替的方波
这个方波的个数表示转过的角度,方波的频率表示转速
通过外部中断来捕获这个方波的边沿,以此判断位置和速度,由于此设备正转反转输出波形无法区分,所以只有一路输出,只能用来测量位置和速度,不能测量旋转方向
机械触点式旋转编码器,
一般用于调节
这种波形就叫为正交波形,可以用来测方向
另一种测量方向的方式是一个触电来测量速度,另一个触电来测量方向
霍尔传感器式旋转编码器
用于机电
旋转编码器的硬件电路
接线图
5-1 对射式红外传感器计次
AO为什么不用:
AO口输出的是模拟信号,一般不用
GPIO中有关AFIO的函数:
void GPIO_AFIODeInit(void); //用来复位AFIO外设的
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); //锁定AFIO的配置,防止意外更改
//两个函数用来配置AFIO的事件输出功能的
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);
void GPIO_EventOutputCmd(FunctionalState NewState);
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState); //用来引脚重映射,第一个参数选择重映射的方式,第二个参数是表示是否启用或禁用指定的重映射功能
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource); //配置AFIO的数据选择器,来选择我们想要的中断引脚
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface); //与以太网有关
EXTI的库函数:
void EXTI_DeInit(void); //复位,默认上电
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct); //初始化
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct); //把结构体变量赋值
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line); //用来软件触发外部中断的,EXTI_Line选择中断线,就能软件触发一次外部中断
//与标志位有关的四个函数
//主程序中查看和清除
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line); //获取指定的标志位是否被置1了
void EXTI_ClearFlag(uint32_t EXTI_Line); //可以对置1的标志位进行清除
//中断函数中查看和清除,并做出判断
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line); //获取中断标志位是否被置1了
void EXTI_ClearITPendingBit(uint32_t EXTI_Line); //清除中断挂起标志位
NVIC的库函数:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); //用来中断分组,参数是中断分组方式
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct); //初始化
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset); //设置中断向量表
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState); //系统低功耗配置
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);
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,3,"Hello world!");
OLED_ShowString(2,1,"Count");
while(1){
OLED_ShowNum(2,7,Get_Count(),5);
}
}
CountSensor.c
#include "stm32f10x.h" // Device header
uint16_t Count;
void CountSensor_Init(void){
//EXTI基本结构:GPIOA——AFIO——EXTI——NVIC
//1.配置RCC
//2.配置GPIO,选择端口为输入模式
//3.配置AFIO,选择我们用的GPIO,连接后面的EXTI
//4.配置EXTI,选择边沿触发方式,比如上升沿、下降沿或双边沿
//5.配置NVIC,给我们中断选择一个合适的优先级
//最后,通过NVIC,外部中断进入CPU
//1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);//注意APB2与APB1的总线
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
//不需要配置EXTI,NVIC(内核的外设均不需要)的时钟
//2
GPIO_InitTypeDef GPIO_InitStructure;
//具体要什么模式,可以去参考手册中的GPIO的8.1.11 外设GPIO配置
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的输入断引脚选择,输出端固定到EXTI电路
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);
//4 配置EXTI
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line=EXTI_Line14; //配置EXTI的第14个线路为中断模式
EXTI_InitStructure.EXTI_LineCmd=ENABLE; //开启中断,使PB14的电平信号通过EXTI通向下一级NVIC
EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;
//EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising_Falling; //双边沿触发
EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure);
//5 配置NVIC,由于NVIC为内核外设,所以放到misc.h
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);
}
//在中断向量表中对应的函数内编写处理逻辑。
//清除中断标志位(防止重复触发)。
//要配置中断程序,中断程序在STM32中的固定的,需要去启动Start文件夹里的启动文件里找
//中断函数不需要声明,它自动会执行
void EXTI15_10_IRQHandler(void){
//要先这个中断点是不是我们想要的EXTI14
if(EXTI_GetFlagStatus(EXTI_Line14)==SET){//查看EXTI_Line14是不是为1,返回值是SET or RESET
//执行中断程序
Count++;
//在中断程序结束后,要再调用一下清除中断标志位的函数,不然程序会不断响应中断,执行函数,最后程序卡死在中断函数
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
//返回Count
uint16_t Get_Count(void){
return Count;
}
5-2 旋转编码器计次
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,3,"Hello world!");
OLED_ShowString(2,1,"Num:");
while(1){
Num+=Get_Count();
OLED_ShowSignedNum(2,5,Num,5);
}
}
Encoder.c
#include "stm32f10x.h" // Device header
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;
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_InitStructure.EXTI_Trigger=EXTI_Trigger_Rising_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); //将结构体变量交给NVIC_Init,配置NVIC外设
}
/**
* 函 数:旋转编码器获取增量值
* 参 数:无
* 返 回 值:自上此调用此函数后,旋转编码器的增量值
*/
int16_t Encoder_Get(void)
{
/*使用Temp变量作为中继,目的是返回Encoder_Count后将其清零*/
/*在这里,也可以直接返回Encoder_Count
但这样就不是获取增量值的操作方法了
也可以实现功能,只是思路不一样*/
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}
/**
* 函 数:EXTI0外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET) //判断是否是外部中断0号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0) //PB0的下降沿触发中断,此时检测另一相PB1的电平,目的是判断旋转方向
{
Encoder_Count --; //此方向定义为反转,计数变量自减
}
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除外部中断0号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
}
/**
* 函 数:EXTI1外部中断函数
* 参 数:无
* 返 回 值:无
* 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
* 函数名为预留的指定名称,可以从启动文件复制
* 请确保函数名正确,不能有任何差异,否则中断函数将不能进入
*/
void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET) //判断是否是外部中断1号线触发的中断
{
/*如果出现数据乱跳的现象,可再次判断引脚电平,以避免抖动*/
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0) //PB1的下降沿触发中断,此时检测另一相PB0的电平,目的是判断旋转方向
{
Encoder_Count ++; //此方向定义为正转,计数变量自增
}
}
EXTI_ClearITPendingBit(EXTI_Line1); //清除外部中断1号线的中断标志位
//中断标志位必须清除
//否则中断将连续不断地触发,导致主程序卡死
}
//解释:
//正转:A相下降沿时,B相为低 → Encoder_Count++
//反转:B相下降沿时,A相为低 → Encoder_Count--
//因为A与B是正交波形:
//正转: A相 _|‾|__|‾|__
// B相 ‾|__|‾|__|‾ (A下降沿时B为低)
//反转: A相 _|‾|__|‾|__
// B相 __|‾|__|‾|__ (B下降沿时A为低)
//
}
建议
中断函数里不要耗时过长的代码;
中断函数与主函数不要调用同一个硬件;