1.项目简介
硬件平台:基于某STM32F103开发板。
软件平台:KEIL5 MDK V5.38,基于LCD屏幕库函数例程库为模板,添加了外部中断,定时器中断相关代码,稍加修改而成。
功能简介:有三个功能按键,启动,暂停,归零;一块LCD屏幕上显示数据数据,理论精度0.01秒
2.硬件简介
按键模块硬件接线图如图所示。
硬件接线图
需要注意的是三个按键中,KEY0,KEY1一端接的是GND。我们在初始化时应设置上拉输入,即没有按下按下按键,IO口读取电压为高,当按键按下,IO口读取的电压会变低,可正确完成按键按下判断;WK_UP按键同理,应设置下拉输入。
3.软件简介
3.1软件总体代码
#include "delay.h"
#include "usart.h"
#include "timer.h"
#include "exti.h"
#include "lcd.h"
#include "led.h"
#include "sys.h"
#include "key.h"
volatile u8 m=0; //分钟
volatile u8 s=0; //秒位
volatile u8 ms=0;//毫秒位,1个ms代表10毫秒,如果显示毫秒,屏幕刷不过来
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 设置中断优先级分组2
LED_Init(); //LED初始化
delay_init(); //延时函数初始化
uart_init(115200);//LCD与主控通过串口通信进行初始化,不可省略
LCD_Init(); //LCD初始化
EXTIX_Init(); //外部中断初始化
POINT_COLOR=RED; //设置颜色
LCD_ShowString(65,25,12*9,24,24,"stopwatch");//显示标题
LCD_ShowChar(60+3*12,80,'m',24,0);//字体大小设置为12像素宽,24像素长
LCD_ShowChar(60+6*12,80,'s',24,0);
TIM3_Int_Init(100,7199);//时钟周期72M/(7199+1)=10k,10Khz的计数频率,计数到计数到100,为10ms
while(1)
{
LCD_ShowxNum(60+0*12,80,m,2,24,0);//显示
LCD_ShowxNum(60+4*12,80,s,2,24,0);
LCD_ShowxNum(60+7*12,80,ms,2,24,0);
}
return 0;
}
初始化以及main函数代码
下面我们按照顺序依次讲解。
3.2按键外部中断
//外部中断初始化函数
void EXTIX_Init(void)
{
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);//外部中断,需要使能AFIO时钟
KEY_Init();//初始化按键对应io模式
//GPIOC.5 中断线以及中断初始化配置
GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource5);
EXTI_InitStructure.EXTI_Line=EXTI_Line5;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOA.15 中断线以及中断初始化配置
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource15);
EXTI_InitStructure.EXTI_Line=EXTI_Line15;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
//GPIOA.0 中断线以及中断初始化配置
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line=EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; //使能按键所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //使能按键所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);
}
我们设置的是按键触发是外部中断,所以先进行按键的初始化,在初始化时注意按键的硬件连接,如果按键另一端连接的3.3V,那就设置为下拉输入,按键连接的GND那就设置上拉输入。
对于外部中断,STM32的中断主控制支持19个外部中断请求:
线0~15:对应外部IO口的输入中断。
线16:连接到PVD输出。
线17:连接到RTC闹钟事件。
线18:连接到USB唤醒事件。
每个外部中断线可以独立的配置触发方式(上升沿,下降沿或者双边沿触发),触发/屏蔽,专用的状态位。
我们使用的是第一类中断。STM32的IO每组最多只有16个(如PA0~PA15),所以根据IO口序号选择对应的外部中断线即可。例如:我们的按键的硬件IO口别是分PA0,PA15,PC5,我就需要配置为
PA0 -EXTI_Line0
PA15-EXTI_Line15
PC5 -EXTI_Line5
(根据IO口序号选择即可)
(如果选择PA0与PB0来触发外部中断,将无法正常工作,因为他们共用一条中断线)
再配置触发方式(上升沿,下降沿,双边沿)
选择对应的中断向量,对应的选择如下图所示
同样根据IO口序号选择即可,如果是0~4号口,选择EXTI0~4即可,如果是5~9号口,那么只能选择EXTI9_5这个中断向量,10~15号口同理只能选择EXTI15_10。
(这就意味着如果同时选择PC9,PB7作为外部中断触发,那么他们将无法同时正常工作,因为他们位于同样的中断向量EXTI9_5)
//io口对应外部中断关系如下
io口 外部中断线 中断向量 中断服务函数
PA0 -EXTI_Line0 -EXTI0_IRQn -EXTI0_IRQHandler
PA15-EXTI_Line15-EXTI15_10_IRQn-EXTI15_10_IRQHandler
PC5 -EXTI_Line5 -EXTI9_5_IRQn -EXTI9_5_IRQHandler
选好了中断向量,就在对应的中断服务函数中去按照要求编写代码即可。
外部中断服务函数代码如下。
volatile u8 suspend_flag=0;//暂停标
volatile u8 start_flag=0; //开始标
volatile u8 zero_flag=0; //清零
void EXTI0_IRQHandler(void)//开始
{
if(WK_UP==1)
{
//delay_ms(10); //消抖,会提高按键识别率,但是会影响秒表精度,自行选择
start_flag=1;//开始
zero_flag=0;
suspend_flag=0;
LED0=!LED0;
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除EXTI0线路挂起位
}
void EXTI15_10_IRQHandler(void)//暂停
{
if(KEY1==0)
{
//delay_ms(10); //消抖,会提高按键识别率,但是会影响秒表精度,自行选择
if(start_flag==1&&suspend_flag==0)//正常运行情况下,才暂停
{
suspend_flag=1;
}
LED0=!LED0;
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line15);//清除LINE15线路挂起位
}
void EXTI9_5_IRQHandler(void)//清零
{
//delay_ms(10); //消抖,会提高按键识别率,但是会影响秒表精度,自行选择
if(KEY0==0)
{
if(suspend_flag==1)//只暂停状态下清零
{
zero_flag=1; //清零
}
LED0=!LED0;
LED1=!LED1;
}
EXTI_ClearITPendingBit(EXTI_Line5); //清除LINE5上的中断标志位
}
在进入中断后可以选择打开10ms消抖,减少误判率,但是本设计是秒表,为了提高计时精准性,选择将它关闭,所以实际测试时,会有误判现象发生,如误判较多,开启10ms消抖即可,但会降低计时精度。
进入到每个按键的中断服务函数后,可以根据情况,选择将标志置位或者清除,在定时器中断函数中读取相关标志位并做出相应操作。
3.3定时器中断
定时器初始化代码如下
TIM3_Int_Init(100,7199);//时钟周期72M/(7199+1)=10k,10Khz的计数频率,计数到计数到100,为10ms
void TIM3_Int_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能
TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为TIMx时钟频率除数的预分频值 10Khz的计数频率
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
//使能或者失能指定的TIM中断//TIM3//使能
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级0级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级3级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器
TIM_Cmd(TIM3, ENABLE); //使能TIMx外设
}
定时器中断中需注意分频系数与装载值,这两个参数决定了计数频率,具体公式在注释中有写。
将定时器中断初始化好,就可在定时器中断服务函数中编写代码了。
(注意:一般情况下,定时器初始化都在最后配置,因为一旦配置完成,定时器自动执行中断,可能会影响其他外设的初始化,造成初始化失败)
定时器中断服务函数如下
extern volatile u8 start_flag; //开始标
extern volatile u8 suspend_flag;//暂停标
extern volatile u8 zero_flag; //清零标
extern volatile u8 m; //分钟
extern volatile u8 s; //秒位
extern volatile u8 ms;//毫秒位
void TIM3_IRQHandler(void) //TIM3中断
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查指定的TIM中断发生与否:TIM 中断源
{
if(zero_flag==1)//数值清零
{
s=0;//清零
m=0;
ms=0;
zero_flag=0;//清零后把自己置零,保证清一次,不然数值永远是0
}
if(start_flag==1)//只要在开始模式下
{
if(suspend_flag==1)
{
;//暂停按下,数值不再自加
}
else//正常计时
{
ms++; //毫秒位
if(ms>=100)
{
s++;//进位操作
ms=0;
}
if(s>=60)
{
m++;//进位操作
s=0;
}
}
}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除TIMx的中断待处理位:TIM 中断源
LED1=!LED1;
}
}
根据在定时器中断设置的几个标志位,来决定时间是自增,还是空跑。
3.4main函数
while(1)
{
LCD_ShowxNum(60+0*12,80,m,2,24,0);//显示
LCD_ShowxNum(60+4*12,80,s,2,24,0);
LCD_ShowxNum(60+7*12,80,ms,2,24,0);
}
在main函数中只有显示这一功能,注意显示区域,不要重叠即可。
4.总结
设计了一个简易秒表,通过按键外部中断来控制开始,暂停,清零等功能,计时功能通过10ms的定时器中断,60一进位,完成时分秒的显示,在main函数调用屏幕显示函数将将关数据显示出来。
计时精度其实不高,估计STM32的72M的晶振产生的10ms中断未必是真的10ms,或者屏幕显示占用了一定的时间,这些误差随着时间是积累,误差会越来越大,实测每一分钟大概会慢0.5秒左右,所以仅供学习参考,不建议实际使用。
欢迎支持,有问题欢迎交流讨论。
qq:2296449414