上次写了一个按键的程序,那个比较简单,但是有时我们可以用另一种方法写按键程序,由于这个学习板不是机械按键,所以消抖的问题就不存在了,所以在这个开发板中,写这个键盘程序的目的是描述一个写程序的思想。这个思想会在之后说,在此之前先介绍一下基础知识,STM32定时器的应用。
在说定时器之前首先得说一下中断的概念,中断就是异常,怎么理解中断呢?中断字面意思就是打断,把一个程序打断去执行中断服务的程序,形象的比喻一下:CPU使用很势利的服务者但是他的能力又十分的强,强到什么程度呢,所有的事没有他就做不成,普通的程序是他最普通的客户,中断服务程序是他的VIP用户,VIP也是分等级的(中断优先级)。当VIP用户发出服务请求(中断请求)的时候,他就马上停止对普通客户的服务去执行VIP客户的服务(中断响应),但是如果一个VIP等级更高的客户(比如SVIP)也发出服务请求,他就转向对更高的客户的服务,之后才再会对VIP用户的服务(这个过程就叫中断嵌套)。最后才会去执行普通客户的服务。这就是整个中断执行的过程。
接下来再说一下定时器。这个东西从名字上就知道他是干什么的了,就是提供一个定时的作用,顺便还可以记个时。现在说一下stm32f103的定时器,STM32中除了互联型的产品,共有8个定时器,分别是基本定时器,通用定时器和高级定时器。基本定时器tiM6、7是一个16位的只能向上计数的定时器只能定时没有外部IO。通用定时器TIM2/3/4/5是一个16位的可以向上或向下计数的定时器可以定时,可以输出比较,可以输入捕捉,每个定时器有四个外部 IO。高级定时器 TIM1/8是一个 16 位的可以向上/下计数的定时器,可以定时,可以输出比较,可以输入捕捉,还可以有三相电机互补输出信号,每个定时器有 8 个外部 IO。具体如图
注意小容量的只有TIM1~4;就是汇编为是_md(FLASH=64 or 128)的那个;这个学习板是hd,大容量可以使用基本定时器。
接下来就是代码的编写,标准库编程,首先我要知道怎么设置,在stm32f10x_tim.h这个文件里有个定时器结构体
这个结构体告诉了我们怎么设置
(1) TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定TIM分频时钟。可设置范围为 0 至 65535,实现 1 至 65536 分频。
(2) TIM_CounterMode:定时器计数方式,可是在为向上计数、向下计数以及三种中心对齐模式。基本定时器只能是向上计数
(3) TIM_Period:定时器周期,实际就是设定自动重载寄存器的值,在事件生成时更新到影子寄存器。可设置范围为 0 至 65535。
(4) TIM_ClockDivision:时钟分频,设置定时器时钟频率与数字滤波器采样时钟频率分频比,基本定时器没有此功能,不用设置。
(5) TIM_RepetitionCounter:重复计数器,属于高级控制寄存器专用寄存器位,利用它可以非常容易控制输出 PWM 的个数。这里不用设置。
那些值怎么设,怎么给后面的注释都会告诉你。
配置中断服务函数在 misc.h这个文件有
英文看不懂???有道翻译了解一下。
- /*
- 因为这个开发板用的是触摸按键而且触摸按键
- 的信号是由触摸按键芯片处理的,所以就没有
- 抖动这个概念,但是,我还得再在用另一种方
- 法写一下按键,就是将按键的扫描程序放到中
- 断里,做一个定时查询,就是一个简单的前后
- 台系统,就是比轮询牛逼一丢丢的程序执行方
- 式。
- */
- #include "stm32f10x.h"
- #include "stm32f10x_gpio.h"
- #include "stm32f10x_rcc.h"
- uint32_t key1_flag; //定义两个标志位的原因是按键IO的没有
- uint32_t key2_flag; //定义到一个端口上→_→
- void Led_GPIO_init(void)
- {
- GPIO_InitTypeDef GPIO_InitStruct; //定义一个gpio结构体
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能时钟(为了节省功耗,时钟默认关)
-
- GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //设置为上拉推挽输出
- GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0| GPIO_Pin_1|GPIO_Pin_2; //定义0引脚
- GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //管脚速度
- GPIO_Init(GPIOA, &GPIO_InitStruct); //初始化gpio
- GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET); //先把这些端口置高
- GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET); //防止这些灯乱闪
- GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_SET);
- }
- void key_GPIO_int(void)
- {
- GPIO_InitTypeDef key_GPIO_InitStruct; //定义一个gpio结构体
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); //使能时钟(为了节省功耗,时钟默认关)
-
- key_GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; //设置为浮空输入按键比较常用
- key_GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5| GPIO_Pin_4; //其中的两个在C端口的4号5号
- GPIO_Init(GPIOC, &key_GPIO_InitStruct); //初始化C端口的输入引脚
-
- GPIO_InitTypeDef key1_GPIO_InitStruct; //定义一个gpio结构体
- RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); //使能时钟(为了节省功耗,时钟默认关)
- GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE); //由于B3,4占用了JTAG的调试接口,所以需要复用一下
- key_GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; //同样设置为浮空输入
- key_GPIO_InitStruct.GPIO_Pin = GPIO_Pin_3| GPIO_Pin_4; //定义0引脚
- GPIO_Init(GPIOB, &key1_GPIO_InitStruct); //初始化gpio
- }
- /*配置的是定时器6*/
- /**设置时钟的分频值是71,在定时器分到的时钟就是72M/(71+1) = 1M
- 那么计数一次的时间就是1/(1M) = 1US,计1000次就是1ms
- **/
- void key_TIM_config()
- {
- TIM_TimeBaseInitTypeDef key_TimeBaseStructure; //定义一个定时器结构体
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE); //定时器2时钟使能
-
- key_TimeBaseStructure.TIM_Period = 1000; //自动重装载值,计数值
- key_TimeBaseStructure.TIM_Prescaler = 71; //时钟分频
- // key_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数方式,基本定时器默认向上,这个就不用管
- // key_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //分频因子,基本定时器没有,不用管
- // key_TimeBaseStructure.TIM_RepetitionCounter = 0; //重复定时器的值,基本定时器没有,不用管
- TIM_TimeBaseInit(TIM6, &key_TimeBaseStructure); //初始化定时器
- TIM_ClearFlag(TIM6, TIM_FLAG_Update); //清除定时器计数标志位
- TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE); //开启定时器中断
- TIM_Cmd(TIM6, ENABLE); //使能定时器
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, DISABLE); //暂时关闭定时器,以后开启
- }
- void key_NVIC_config()
- {
- NVIC_InitTypeDef key_NVIC_IRQStructure; //定义中断结构体
- NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); //设置中断组0
- key_NVIC_IRQStructure.NVIC_IRQChannel = TIM6_IRQn; //设置中断源
- key_NVIC_IRQStructure.NVIC_IRQChannelPreemptionPriority = 0; //设置主优先级为0
- key_NVIC_IRQStructure.NVIC_IRQChannelSubPriority = 3; //设置抢占优先级为3
- key_NVIC_IRQStructure.NVIC_IRQChannelCmd = ENABLE; //中断使能
- NVIC_Init(&key_NVIC_IRQStructure); //初始化中断服务
-
- }
- /*******定时中断服务*********/
- /******注意!!这个函数名是不能随便改的******/
- /*这个函数名已经在汇编中定义过,而且那个还是个弱定义,
- 但定时器启动的时候,程序自动查找中断向量表的名称,如果查不到
- 就陷入无限循环,所以这个名称是固定的,除非改汇编文件*/
- void TIM6_IRQHandler()
- {
- if(TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET) //如果检测定时器中断的标志位是置位(也就是不是置位)
- {
- key1_flag = GPIO_ReadInputData(GPIOB) & 0X0018; //B组的按键是在3、4口所以要检测低3、4口的值,就要清除其他位
- key2_flag = GPIO_ReadInputData(GPIOC) & 0X0030; //B组的按键是在4、5口
- TIM_ClearITPendingBit(TIM6, TIM_FLAG_Update); //清除中断标志为,如果不清除,就会一直在中断里
- }
- }
- int main(void)
- {
- Led_GPIO_init(); //初始化GPIO
- key_GPIO_int(); //按键初始化
- key_TIM_config();
- key_NVIC_config();
- RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
- while(1)
- {
- if(key1_flag == 0x08)
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
- }
- else
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
- }
-
- if(key1_flag == 0x10)
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET);
- }
- else
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET);
- }
-
- if(key2_flag == 0x20)
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_RESET);
- }
- else
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_SET);
- }
-
- if(key2_flag == 0x10)
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
- GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET);
- GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_RESET);
- }
- else
- {
- GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
- GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET);
- GPIO_WriteBit(GPIOA, GPIO_Pin_2, Bit_SET);
- }
- }
- }
复制代码
我费了这么大劲儿就为写了个按键扫描?当然不是,是介绍一下一种嵌入式开发思想。
单片机开发大致有两个方向在任务执行,不待操作系统的裸机系统和带操作系统的多任务操作系统。
1、轮询系统:轮询系统即是在裸机编程的时候, 先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环, 顺序地做各种事情。就是一个大while,里面的程序挨个儿执行。这是一种非常简单的软件结构,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动的就能完成的事情。如果只是实现 LED 翻转,串口输出,液晶显示等这些操作,那么使用轮询系统将会非常完美。但是,如果加入了按键操作等需要检测外部信号的事件,用来模拟紧急报警,那么整个系统的实时响应能力就不会那么好了。假设task3 是按键扫描,当外部按键被按下,相当于一个警报,这个时候,需要立马响应,并做紧急处理,而这个时候程序刚好执行到 task1,要命的是 task1需要执行的时间比较久,久到按键释放之后都没有执行完毕,那么当执行到 task3的时候就会丢失掉一次事件,最要命的是如果task1中有延时,那就完蛋了,得按多少次按键系统才反应过来啊。足见,轮询系统只适合顺序执行的功能代码,当有外部事件驱动时,实时性就会降低。
- void main()
- {
- INIT();
- while(1)
- {
- task1();
- task2();
- task3();
- }
- }
复制代码
2、前后台系统:相比轮询系统,前后台系统是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里我们称为前台, main 函数里面的无限循环我们称为后台。大概长这样子
- int flag1 = 0;
- int flag2 = 0;
- int flag3 = 0;
- int main(void)
- {
- INIT();
- while(1)
- {
- if(flag1)
- {
- task1();
- }
- if(flag2)
- {
- task2();
- }
- if(flag3)
- {
- task3();
- }
- }
- }
复制代码
在顺序执行后台程序的时候,如果有中断来临,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序里面标记事件,如果事件要处理的事情很简短,则可在中断服务程序里面处理,如果事件要处理的事情比较多,则返回到后台程序里面处理。虽然事件的响应和处理是分开了,但是事件的处理还是在后台里面顺序执行的,但相比轮询系统,前后台系统确保了事件不会丢失,再加上中断具有可嵌套的功能,这可以大大的提高程序的实时响应能力。 在大多数的中小型项目中,前后台系统运用的好,堪称有操作系统的效果。
3、多任务系统:相比前后台系统,多任务系统的事件响应也是在中断中完成的,但是事件的处理是在任务中完成的。在多任务系统中,任务跟中断一样,也具有优先级,优先级高的任务会被优先执行。当一个紧急的事件在中断被标记之后,如果事件对应的任务的优先级足够高,就会立马得到响应。相比前后台系统,多任务系统的实时性又被提高了。 它大概长这样子的
- int flag1 = 0;
- int flag2 = 0;
- int flag3 = 0;
- int main(void)
- {
- INIT();
- RTOSInit(); //系统初始化
- RTOSStart(); //系统启动
- }
- void ISR1(void)
- {
- flag1 = 1; //标志位置位
- }
- void ISR1(void)
- {
- flag2 = 1; //标志位置位
- }
- void ISR1(void)
- {
- flag3 = 1; //标志位置位
- }
复制代码
相比前后台系统中后台顺序执行的程序主体,在多任务系统中,根据程序的功能,我们把这个程序主体分割成一个个独立的,无限循环且不能返回的小程序,这个小程序我们称之为任务。 每个任务都是独立的,互不干扰的,且具备自身的优先级,它由操作系统调度管理。 加入操作系统后, 我们在编程的时候不需要精心地去设计程序的执行流,不用担心每个功能模块之间是否存在干扰。加入了操作系统,我们的编程反而变得简单了。
上述方式没有孰优孰劣只有合适与否。
|