前言
本次作业六对应的是第七章的内容,第七章主要讲了各种定时器模块,以及通过这些模块衍生的各种好玩的操作,整个第七章分为了几个东西来讲,分别是SysTick,RTC,Timer,PWM以及信号捕捉,我们也会在作业之前分别根据这几个方面来学习一下。
学习
SysTick
我们学习所用的mcu大有三类计时器,分别是SysTick,RTC模块以及Timer模块,其中,SysTick是这其中最简单的一类计时器,又称为“滴答”定时器,是Cortex-M内核中的一个系统定时器,经常被用作系统中断控制器的基础或者作为操作系统的时间片轮转机制的基准。凡是使用该内核生产的MCU均含有SysTick,因此使用这个定时器的程序方便在MCU间移植。SysTick 功能简单,内部寄存器也较少。
本身这个SysTick所涉及到的寄存器就比较少,并且由于本身SysTick就属于内核,有比较简单,因此在数据手册,参考手册,里面的信息还是比较少的
例如在参考手册中就只有仅仅一段话
这段话的大概意思就是说明了在SysTick时钟源设置为10 MHz(即处理器的主时钟频率HCLK的最大值除以8)的情况下,这个校准值能够提供一个1毫秒的时间基准。这意味着当SysTick定时器以10 MHz的频率运行时,每计数到1毫秒,就会产生一个中断或者计数事件,通常用于在没有精确的外部时钟源的情况下,提供一个相对稳定的时基,从而可以在不需要用户干预的情况下,实现较为精确的计时功能。
又例如在数据手册中
也仅是简单的介绍了一下这个时钟的一些特性
那么我们最想要了解的地址呢?地址在哪里?显然,这个SysTick因为不是外设,是在处理器里头的,因此我们想了解其寄存器的地址,就要到内核手册里面看,打开M4手册,能够轻松的找到有关于SysTick寄存器组的信息,如下
下面来看看示例程序吧
我们看示例程序就仅关注SysTick这部分内容了,不再关心其它其它的东西了
如上图,我们能从core_cm4.h头文件能看到SysTick寄存器组结构体,这也就代表这我们在配置寄存器的时候能够通过这个结构体来设置各项参数。
上图则是SysTick的初始化,其中传入的参数为中断间隔,即多少时间发一次中断,参数的单位是ms。具体做了什么也不细说了,无非就是那两个问题,要用到什么寄存器,要怎么设置寄存器的值,至于这两个问题,答案也很简单,去找手册,看手册就完事了!
下面简单跑跑这个示例程序看看正不正常吧
可以看到,具体来说就是做了一个简单的计时功能,同时控制蓝灯亮一秒,灭一秒,这个样例也可以为我们接下来做的作业1提供参考吧。
额外的,实际上我们能在中断处理程序中看到使用了watchdog,原因是为了确保系统的稳定性和可靠性。在嵌入式系统中,看门狗定时器是一种常见的硬件机制,用于监控程序的运行状态。如果程序由于某种原因(如硬件故障、软件错误、外部干扰等)导致无法正常执行,看门狗定时器可以检测到这种情况,并采取措施重置微控制器,使其从初始状态重新开始执行。在中断处理程序中,每次中断触发时,都会调用wdog_feed()函数来“喂狗”。这意味着在正常的程序执行过程中,看门狗定时器会定期被重置,防止它超时。如果程序因为某种原因而无法继续执行,比如进入了一个死循环或者响应了一个异常事件,SysTick中断可能无法按时触发wdog_feed()函数,看门狗定时器最终会超时,导致微控制器复位。这个实际上就防止在中断处理中乱写循环导致处理器卡死的。
那么关于SysTick的学习先到此为止。
RTC
与SysTick不同,RTC是一种实时时钟,也就意味着其能在断电状况下继续运行,但请不要误会,我这里所说的是那种带电池供电的RTC,像我们的板子,一把数据线拔了,管你是什么TC,统统无了。好了,先不扯了,正常来说RTC,用于保持日期和时间信息,即便在系统关闭的情况下也能维持这些信息,说到这一点,当我们拆开电脑的时候其实能发现,除了内部的大电池之外,主板那里其实能找到一些的独立电源,当你的电脑断电了,或者大电池没电了,这些小的独立电源就能支持你主板内部一些小器件的工作,其中就包括了RTC,不然你以为电脑关机为什么再次开机即使不联网也能刷新一个相对精准的时间,这就是RTC的功劳,RTC通常包括时钟日历功能,可以提供时、分、秒、日、月、年等时间信息,于计时、提醒、数据记录等方面有所应用,特别是在需要长期运行和记录时间的场合,RTC就非常的实用。
以下是参考手册对于RTC时钟的介绍
相比于SysTick寄存器组,RTC的寄存器组就比较多了,比较重要的则是以下几个,RTC_TR,RTC_DR,RTC_DR,RTC_ISR等,不过这个寄存器组还是挺有趣的,有趣在什么地方呢?这玩意带锁,还有密码,简而言之,除了少部分寄存器没有,其寄存器都会有写入保护,不过倒也正常,毕竟要防止外部随意更改时余的间嘛,有关于写入保护的介绍同样也在参考手册中提及,如下
OK,关于这方面,就讲到这里了,关于代码,我们也不深入进行了解了,主要就是要设置的东西要比SysTick要多罢了,那么测试一下自带的程序吧
程序运行如上图所示,其实也是个计时,只不过能把年月日,时分秒,周几显示出来而已,于是我试着给了组参数,进去,比如2024年5月16日,23时00分00秒,星期四,这不给不知道,给了甚至还发现一个BUG,例如给参数如下
结果运行结果如下图
不是,我那么大个2024怎么变成72了?于是去翻了一下参考手册,发现对于年份,给了8bit来存储,而且是用8421存,每4bit存一个位,如下
原来如此?只给了两位,但是我想了想,不对劲儿啊,再怎么说2024怎么能变成72的,就算是位宽不够,2024只取低8位也是0b11101000,换成8421那就更无从说法了,因为前四位在8421编码中根本就是没有意义的,嗨,看来还是得看看代码的情况了。
经过一番时间的寻找,终于,在rtc.c文件中找到这样一个函数
原来如此,作者是写了一个这样的转换函数,也就是传进来的数据要经过转换才能写进RTC_DR寄存器的,但仔细一想,这个函数其实是有bug的(其实也不能这么说),首先,我们传进来的2024,8位存不下,砍掉了前面的位的数字了,留下了八位的结果是232,这个作为函数的输入了,接下来就是神奇的事情了,看代码逻辑,代码会不断的数数,分离十位和个位,但是由于我们输入的是232,经过分离之后,十位变成了23,个位是2,然后后续的操作是保留了十位的第四位,刚好这个23表示为0b10111了刚好把最高位的1舍弃掉了,最后变成了十位为7,个位为2,这两数的BCD表示为01110010,被送进DR寄存器,真的是太抽象了。关键是,你也不能说作者有错误,只是本身输入不能这样输入罢了,不过实际上这个工程也应该做一个输入检查什么的。因此,如果真想写2024进去,把千位百位跟十位个位分来处理就好了,本身RTC_DR寄存器就只是让你输入年份的十位个位而已嘛。OK,那么对于RTC的学习就到此为止。
Timer
Timer定时器是一种能够按照设定的时间间隔产生中断或触发事件的硬件模块。在微控制器中,Timer定时器可用于多种用途,换言之就是很能整活,比如精确的时间测量、事件计数、PWM信号生成(在本次作业就是干这事)、信号延迟等。定时器的精度和分辨率取决于其时钟源和计数器的位数。
Timer模块内含6个独立定时器,分别称为TIM1、TIM2.TIM6、TIM7、TIM15、TIM16,各定时器之间相互独立不共享任何资源。这些定时器的时钟源可以通过编程使用外部晶振,也可以使用内部时钟源。除TIM2为32位定时器外,其他均为16位定时器TIM6、TIM7只用于基本计时,TIM1、TIM2、TIM15、TIM16还具有PWM、输入捕捉、输出比较功能,当用于这些功能时,不能用于基本计时。
如果说把它当作定时器使用,那就跟SysTick差不多,示例程序也仅是简单计时而已,如下
因此我们在这里不过多深入了,我们在后面的两个地方就能看到Timer的身影了。这里先到此为止。
PWM
即脉冲宽度调制PWM是一种信号控制技术,通过一个高电平和低电平的方波信号,来控制电路的功率输出。通过改变方波的占空比(即高电平持续时间与整个周期时间的比例),可以精确控制功率或电压的大小。
先了解PWM的几个术语
- PWM周期:在微控制器编程产生PWM波的语境下,PWM信号的周期用其持续的时钟周期个数来度量。
- PWM占空比:PWM信号处于有效电平的时钟周期数与整个PWM周期内的时钟周期数之比,用百分比表示。
- PWM极性:极性决定了有效电平,正极性时,高电平为有效电平,负极性时,低电平为有效电平。
- 脉冲宽度:指一个PWM周期内,PWM波处于高电平的时间(用持续的时钟周期数表示),可以用占空比与周期计算出来。
- PWM分辨率:PWM分辨率是指脉冲宽度的最小时间增量,例如PWM是利用频率为48Mhz的时钟源产生的,即时钟源周期=(1/48)us=0.208us=20.8ns,那这个20.8ns就是PWM分辨率。
- PWM对齐方式:分为边沿对齐模式和中央对齐模式。
我们所用到的开发板中,能产生PWM信号的Timer模块有Tim1,Tim2,Tim15和Tim16,大致情况如下
当然了,这些东西肯定是要启动复用功能的。ok,看看测试代码吧,我们先看一下,PWM初始化的函数pwm_init的定义,由于代码太多,我就不妨上来了,放个定义看看吧
可以看到,接受6个参数,接收到参数之后,就是对对应的寄存器组进行操作了,我们再查看这个pwm初始化函数的时候,还能看到有一个tim_timer_init1的函数,显然这个是对timer模块进行初始化的,不过也正常,本身pwm就是timer生成的,其函数定义如下
简单的学习了一下pwm.c的函数之后,我们试着跑一下示例程序,如下
可以看到程序输出小灯的高低电平,但这个不是特殊的,特殊之处在于小灯的亮灭情况,通过PWM,我们能控制小灯一亮一灭的情况,这个灯的闪烁速度是快到慢的,然后往复循环。这个是通过控制那么关于PWM的学习就到此为止了。
捕捉
即信号捕捉,信号捕捉是指微控制器对输入信号进行监测和捕捉的功能。当输入信号的边沿(如上升沿或下降沿)被检测到时,微控制器可以记录此时的计数器值或时间戳。这一功能可用于测量信号的频率、周期、占空比等参数。
Timer模块说实话真的能玩出各种花样了,好多的端口都能复用成为Timer的PWM输出端口,我们可以通过输出比较和输入捕捉处理定时器和外部信号之间的交互。
先简单介绍一下输出比较和输入捕捉
- 输出比较:输出比较的功能是用程序的方法在规定的较精确时刻输出需要的电平,实现对外部电路的控制。MCU输出比较模块的基本工作原理是,当定时器的某一通道用作输出比较功能时,通道寄存器的值和计数寄存器的值每隔4个总线周期比较一次。当两个值相等时,输出比较功能做出相应动作,比如电平的翻转。输出比较的应用场合主要有产生一定间隔的脉冲,典型的应用实例就是实现软件的串行通讯。
-
输入捕捉:输入捕捉是用来监测外部开关量输入信号变化的时刻。当外部信号在指定的MCU输入捕捉引脚上发生一个沿跳变(上升沿或下降沿)时,定时器捕捉到沿跳变之后,把计数器当前值锁存到通道寄存器,同时产生输入捕捉中断利用中断处理程序可以得到沿跳变的时刻。输入捕捉的应用场合主要有测量脉冲信号的周期与波形。
接着我们学习一下示例程序,(简单过一下,因为初始化的东西太多了,不一一细说了)
首先是输出比较的初始化
然后是输入捕捉的初始化
值得留意的是,在这些工具定义中,似乎是配置了TIM15作为输入捕捉的中断源,可以从下面的代码看出
有时间的可以认真去看看,真的很有意思,我下面就简述一下配置了什么(不简述的话可能要把我写到呕吐),大致是五个东西
- 指定了Tim2为输出(GEC39)
- 指定了Tim2为输入(GEC10)
- 指定了Tim15为输入捕捉功能,使其能够捕获GEC10引脚上的信号边沿
- 指定了Tim15为输出比较功能,使其能够控制GEC39引脚的电平状态
- 指定了SysTick为时钟,用于记录发生事件的时间
OK,那么下面跑一下程序吧
首先我们要先用杜邦线把我们程序中设定的两个引脚接起来,这样才能检测,如下
然后运行程序
可以看到,成功的输出了在对应时刻的事件,而且能够发现,变化是越来越快的,在开发板的情况就是,蓝灯闪烁,然后越闪越快,然后又重新变慢,这个主要是通过isr.c中的一段代码实现的,说白了就是将参数慢慢减少,减少到某一个阈值然后重置而已,如下
OK,那么关于这捕捉部分的学习就先这样咯
作业
1、利用SysTick定时器编写倒计时程序,如初始设置为2分30秒,每秒在屏幕上输出一次时间,倒计时为0后,红灯亮,停止屏幕输出,并关闭SysTick定时器的中断。
SysTick的使用还是比较简单的,在这里我写的代码就比较随意了,不过最终能达到目的就是了
以下是main.c
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
//函数名:setArray
//作用:用于拆分秒到时间数组的内部函数
//输入:tt:倒计时的总时长/s
// p: 时分秒数组
//输出:无
void setArray(uint32_t tt, uint8_t *p);
int main(void)
{
//设定工具变量
uint32_t totalSec = 150; //设置倒计时总时间,单位s
uint8_t secTag = totalSec%60; //秒表标志位,用于判断gTime[2]有无发生变化
//关总中断
DISABLE_INTERRUPTS;
wdog_stop();
//拆分倒计时到gTime数组中
setArray(totalSec,gTime);
//用户外设模块初始化
gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF); //绿灯
gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF); //红灯
systick_init(10); //设置systick为10ms中断
//开总中断
ENABLE_INTERRUPTS;
//输出倒计时总时长
printf("倒计时启动,计时时长为:%d\n",totalSec);
//开始循环
for(;;)
{
//声明时分秒变量
uint8_t curhour,curminute,cursecond;
//倒计时结束处理逻辑
if(gTime[0]==0 && gTime[1]==0 && gTime[2]==0)
{
//关绿灯
gpio_set(LIGHT_GREEN,LIGHT_OFF);
//关闭定时器相关功能
SysTick->CTRL=0;
SCB->ICSR |= (1<<25);
//提示倒计时结束,使红灯变亮一会儿
printf("倒计时结束!wywwywwyw\n");
gpio_set(LIGHT_RED,LIGHT_ON);
for(volatile uint32_t lastTime = 0; lastTime < 50000000; lastTime++);
gpio_set(LIGHT_RED,LIGHT_OFF);
break;
}
//利用标志判断秒针是否动过,如果动过,则往下走,否则continue
if(secTag == gTime[2])
continue;
secTag = gTime[2];
//从gTime数组中获取时间
curhour = gTime[0];
curminute = gTime[1];
cursecond = gTime[2];
printf("倒计时进行中:%02d:%02d:%02d\twyw wyw wyw\n",curhour,curminute,cursecond);
}
}
//用于拆分秒到时间数组的内部函数
void setArray(uint32_t tt, uint8_t *p)
{
uint8_t hours,minutes,seconds;
//计算时分秒
hours = (uint8_t)(tt/3600); //小时
minutes = (uint8_t)((tt%3600)/60); //分钟
seconds = (uint8_t)(tt%60); //秒
//存储计算结果到数组
*p = hours; //小时
*(p + 1) = minutes; //分钟
*(p + 2) = seconds; //秒
}
以下是isr.c
#include "includes.h"
//声明使用到的内部函数
//isr.c使用的内部函数声明处
void SecAdd1(uint8_t *p);
void SecSub1(uint8_t *p);
//定时器中断处理程序
void SysTick_Handler()
{
static uint8_t SysTickCount = 0;
SysTickCount++; //Tick单元+1
wdog_feed(); //看门狗“喂狗”
gpio_set(LIGHT_GREEN,LIGHT_OFF);
if (SysTickCount >= 100)
{
SysTickCount = 0;
SecSub1(gTime);
gpio_set(LIGHT_GREEN,LIGHT_ON);
}
}
//===========================================================================
//函数名称:SecAdd1
//函数返回:无
//参数说明:*p:为指向一个时分秒数组p[3]
//功能概要:秒单元+1,并处理时分单元(00:00:00-23:59:59)
//===========================================================================
void SecAdd1(uint8_t *p)
{
*(p+2)+=1; //秒+1
if(*(p+2)>=60) //秒溢出
{
*(p+2)=0; //清秒
*(p+1)+=1; //分+1
if(*(p+1)>=60) //分溢出
{
*(p+1)=0; //清分
*p+=1; //时+1
if(*p>=24) //时溢出
{
*p=0; //清时
}
}
}
}
//===========================================================================
//函数名称:SecSub1
//函数返回:无
//参数说明:*p:为指向一个时分秒数组p[3]
//功能概要:秒单元-1,并处理时分单元(23:59:59-00:00:00)
//===========================================================================
void SecSub1(uint8_t *p)
{
if (*(p+2) > 0) {
// 如果秒大于0,则直接减1
*(p+2) -= 1;
} else {
// 如果秒为0,则需要借位
*(p+2) = 59; // 秒变为59
if (*(p+1) > 0) {
// 如果分大于0,则减1
*(p+1) -= 1;
} else {
// 如果分为0,则需要借位
*(p+1) = 59; // 分变为59
if (*p > 0) {
// 如果时大于0,则减1
*p -= 1;
} else {
// 如果时为0,则重置为23(因为减1后应该是23:59:59)
*p = 23;
}
}
}
}
运行结果为
可以看到倒计时技术后亮红灯,同时每一秒就闪烁一下绿灯。
2、利用RTC显示日期(年月日、时分秒),每秒更新。并设置某个时间的闹钟。闹钟时间到时,屏幕上显示有你的姓名的文字,并点亮绿灯。
得益于构件的方式,RTC时钟的使用实际上也简单,实际上要做的事情不少,接下来放代码
main.c
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
//声明外部变量
extern volatile uint8_t yearFrontISR;
//内部函数,拆分年份
void setYear(volatile uint32_t year,volatile uint8_t *yearFront,volatile uint8_t *yearLast2);
int main(void)
{
//声明变量赋值
volatile uint32_t year = (volatile uint32_t)2024UL; //年
volatile uint8_t month = (volatile uint8_t)5U; //月
volatile uint8_t date = (volatile uint8_t)15U; //日
volatile uint8_t week = (volatile uint8_t)3U; //星期
volatile uint8_t hour = (volatile uint8_t)23U; //时
volatile uint8_t minute = (volatile uint8_t)47U; //分
volatile uint8_t second = (volatile uint8_t)0U; //秒
volatile uint8_t yearLast2; //年份十位个位
//调用函数拆分
setYear(year,&yearFrontISR,&yearLast2);
//printf("%2d\n",yearFrontISR);
//printf("%2d\n",yearLast2);
//关总中断
DISABLE_INTERRUPTS;
//初始化设备
gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF); //初始化蓝灯
RTC_Init(); //RTC初始化
RTC_Set_Date(yearLast2,month,date,week); //设置日期
RTC_Set_Time(hour,minute,second); //设置时间
//使能模块中断
RTC_PeriodWKUP_Enable_Int(); //使能唤醒中断
//设置闹钟
RTC_Alarm_Enable_Int(A); //使能闹钟中断
RTC_Set_Alarm(A,3,23,47,10); //闹钟时间
//开总中断
ENABLE_INTERRUPTS;
//配置WAKE UP中断,每秒中断一次
RTC_Set_PeriodWakeUp(1);
//循环
for(;;)
{
}
}
//内部函数,拆分年份
void setYear(volatile uint32_t year, volatile uint8_t *yearFront, volatile uint8_t *yearLast2)
{
*yearFront = year/100;
*yearLast2 = year%100;
}
isr.c
#define GLOBLE_VAR
#include "includes.h"
//定义变量,存储年份除最后两位的其他位
//main函数进行处理
volatile uint8_t yearFrontISR;
//======================================================================
//程序名称:RTC_WKUP_IRQHandler
//函数参数:无
//中断类型:RTC闹钟唤醒中断处理函数
//======================================================================
void RTC_WKUP_IRQHandler(void)
{
uint8_t hour,min,sec;
uint8_t year,month,date,week;
if(RTC_PeriodWKUP_Get_Int()) //唤醒中断的标志
{
RTC_PeriodWKUP_Clear(); //清除唤醒中断标志
RTC_Get_Date(&year,&month,&date,&week); //获取RTC记录的日期
RTC_Get_Time(&hour,&min,&sec); //获取RTC记录的时间
printf("现在时间为 %02d%02d年%02d月%02d日 %02d:%02d:%02d 星期%d\n",yearFrontISR,year,month,date,hour,min,sec,week);
//printf("%2d\n",yearFrontISR);
}
}
//======================================================================
//程序名称:RTC_Alarm_IRQHandler
//中断类型:RTC闹钟中断处理函数
//======================================================================
void RTC_Alarm_IRQHandler(void)
{
//printf("进入闹钟中断\n");
if(RTC_Alarm_Get_Int(A)) //闹钟A的中断标志位
{
RTC_Alarm_Clear(A); //清闹钟A的中断标志位
gpio_set(LIGHT_GREEN,LIGHT_ON);
printf("闹钟A响啦! wyw wyw wyw!\n");
}
}
运行结果为
可以看到,设定的闹钟A响了,打印相关信息,绿灯亮起
3、利用PWM脉宽调制,交替显示红灯的5个短闪和5个长闪。
这个题目有意思,如果说要用PWM,那么就是要用PWM的跳变作为标志去操纵小灯的亮灭,相当于使小灯的波形图跟PWM输出的波形同步,但实际上作者在这个模板最后提供了一个内部函数Delay_ms(),这个其实也能做类似效果的呼吸灯,但是只有这个就用不上PWM了,因此我们这里还是按照题目要做的来做吧
main.c
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
int main(void)
{
//关总中断
DISABLE_INTERRUPTS;
//用户外设模块初始化
gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF); //初始化红灯
pwm_init(PWM_USER,1500,2250,90.0,PWM_CENTER,PWM_MINUS); //PWM输出初始化
//开总中断
ENABLE_INTERRUPTS;
//设置相关变量
uint8_t count; //统计亮灯次数
double duty; //占空比
uint8_t mflag; //标志灯的状态
uint8_t flag; //期望收到的PWM输出状态
uint8_t tag; //循环计数器
double ltime = 0.0; //计算亮灯时长
//随意写写,一般编程还是不要用goto语句为好
//短闪烁语句块
short_shine:
duty = 10.0; //设置一个较短的占空比
pwm_update(PWM_USER,duty); //更新
//设定变量初始值
count = 0; //统计亮灯次数
mflag = 0; //标志灯的状态
flag = 1; //期望收到的PWM输出状态
tag = 0; //循环计数器
ltime = duty/100.0*1.5; //计算亮灯时长
for(;;)
{
printf("程序运行到短闪,亮灯时长为:%.2lfs,一个亮灭循环为:1.5s\n",ltime);
tag = 0;
do
{
count = tag/2+1;
mflag = gpio_get(PWM_USER);
if((mflag==1)&&(flag==1)) //检测到PWM跳变
{
//统计循环次数,反转红灯
gpio_reverse(LIGHT_RED);
printf("红灯第%d次亮\twyw\n",count);
flag = 0;
tag++;
}
else if((mflag==0)&&(flag==0)) //检测到PWM跳变
{
//统计循环次数,反转红灯
gpio_reverse(LIGHT_RED);
printf("灭\twyw\n");
flag = 1;
tag++;
}
}while(tag < 10);
goto long_shine; //能到这个语句,说明已经亮灭5次了,跳到长闪语句块
}
//长闪烁语句块
long_shine:
duty = 80.0; //设置一个较长的占空比
pwm_update(PWM_USER,duty); //更新
//设定变量初始值
count = 0; //统计亮灯次数
mflag = 0; //标志灯的状态
flag = 1; //期望收到的PWM输出状态
tag = 0; //循环计数器
ltime = duty/100.0*1.5; //计算亮灯时长
for(;;)
{
printf("程序运行到长闪,亮灯时长为:%.2lfs,一个亮灭循环为:1.5s\n",ltime);
tag = 0;
do
{
count = tag/2+1;
mflag = gpio_get(PWM_USER);
if((mflag==1)&&(flag==1)) //检测到PWM跳变
{
//统计循环次数,反转红灯
gpio_reverse(LIGHT_RED);
printf("红灯第%d次亮\twyw\n",count);
flag = 0;
tag++;
}
else if((mflag==0)&&(flag==0)) //检测到PWM跳变
{
//统计循环次数,反转红灯
gpio_reverse(LIGHT_RED);
printf("灭\twyw\n");
flag = 1;
tag++;
}
}while(tag < 10);
goto short_shine; //能到这个语句,说明已经亮灭5次了,跳到短闪语句块
}
}
运行结果为
可以看到按照短闪5次,长闪5次的结果输出,运行结果用视频表示
test3runmov
4、GEC39定义为输出引脚,GEC10定义为输入引脚,用杜邦线将两个引脚相连,验证捕捉实验程序Incapture-Outcmp-20211110,观察输出的时间间隔。
这道题一开始没有留意到是用的是实验程序,自行写了一个之后才发现,原来是验证实验程序,绷不住了,验证实验程序的话其实在上面学习板块的捕捉里面,可以目录直接导航到那里,那么这里就放一下我自行写的吧
main.c
#define GLOBLE_VAR
#include "includes.h" //包含总头文件
int main(void)
{
//【不变】关总中断
DISABLE_INTERRUPTS;
//给全局变量赋初值
gTime[0] = 0; //分钟
gTime[1] = 0; //秒
gTime[2] = 0; //毫秒
period = 1000; //自动重装载寄存器初始值
//用户外设模块初始化
gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_ON); //初始化红灯
outcmp_init(OUTCMP_USER,3000,200,50.0,CMP_REV); //输出比较初始化
incapture_init(INCAP_USER,375,1000,CAP_DOUBLE); //上升沿捕捉初始化
systick_init(1); //设置systick为1ms中断,方面得到ms级别的精度
//使能模块中断
cap_enable_int(INCAP_USER); //使能输入捕捉中断
//开总中断
ENABLE_INTERRUPTS;
for(;;)
{
//原逻辑整合到了isr.c中
}
}
isr.c
#include "includes.h"
void SecAdd1(volatile uint16_t *p);
//=====================================================================
//函数名称:TIM1_BRK_TIM15_IRQHandler(输入捕捉中断处理程序)
//参数说明:无
//函数返回:无
//功能概要:(1)每次捕捉到上升沿或者下降沿触发该程序;
// (2)每次触发都会上传当前捕捉到的上位机程序
//=====================================================================
void TIM1_BRK_TIM15_IRQHandler(void)
{
static uint8_t flag = 0; //标志
DISABLE_INTERRUPTS; //关总中断
if(cap_get_flag(INCAP_USER)) //等待捕捉中断位的变化
{
//在捕获到上升沿之后,输出此刻捕获的是上升沿和时间
if(gpio_get(INCAP_USER)==1 && flag == 0)
{
gpio_set(LIGHT_RED,LIGHT_ON);//亮灯提示
printf("时刻%d分钟:%d.%d秒 此刻是上升沿 wyw\n",gTime[0],gTime[1],gTime[2]);
flag = 1;
//通过修改自动重装载寄存器的值控制电平翻转时刻
outcmp_init(OUTCMP_USER,3000,period,50.0,CMP_REV); //修改输出比较电平周期,会触发中断,进入INCAP_USER_Handler(void)函数
}
//在捕获到下降沿之后,输出此刻捕获的是下降沿和时间
else if(gpio_get(INCAP_USER)==0 && flag == 1)
{
gpio_set(LIGHT_RED,LIGHT_OFF);
printf("时刻%d分钟:%d.%d秒 此刻是下降沿 wyw\n",gTime[0],gTime[1],gTime[2]);
flag = 0;
//通过修改自动重装载寄存器的值控制电平翻转时刻
outcmp_init(OUTCMP_USER,3000,period,50.0,CMP_REV); //修改发输出比较电平周期,会触中断,进入INCAP_USER_Handler(void)函数
}
cap_clear_flag(INCAP_USER); //清中断
}
//------------------------------------------------------------------
ENABLE_INTERRUPTS; //关总中断
}
//=====================================================================
//函数名称:SYSTICK_USER_Handler(SysTick定时器中断处理程序)
//参数说明:无
//函数返回:无
//功能概要:(1)每10ms中断触发本程序一次;(2)达到一秒时,调用秒+1
// 程序,计算“时、分、秒”
//特别提示:(1)使用全局变量字节型数组gTime[3],分别存储“时、分、秒”
// (2)注意其中静态变量的使用
//=====================================================================
void SysTick_Handler()
{
//printf("***\n");
static uint8_t SysTickCount = 0;
SysTickCount++; //Tick单元+1
//wdog_feed(); //看门狗“喂狗”
if (SysTickCount >= 1)
{
SysTickCount = 0;
SecAdd1(gTime);
}
}
//===========================================================================
//函数名称:SecAdd1
//函数返回:无
//参数说明:*p:为指向一个时分秒数组p[3]
//功能概要:秒单元+1,并处理时分单元(00:00:00-23:59:59)
//===========================================================================
void SecAdd1(volatile uint16_t *p)
{
*(p+2)+=1; //秒+1
if(*(p+2)>=1000) //秒溢出
{
*(p+2)=0; //清秒
*(p+1)+=1; //分+1
if(*(p+1)>=60) //分溢出
{
*(p+1)=0; //清分
*p+=1; //时+1
if(*p>=60) //时溢出
{
*p=0; //清时
}
}
}
}
运行结果为
可以看到输出了检测结果,同时红灯会在上升沿亮起,在下降沿熄灭。
test4runmov
那么作业4就到这里为止。
总结
本次作业是有关于开发板内各种定时器模块的学习与理解,总体来说还是比较有意思的,学习了三类定时器,一个是内核时钟,一个是实时时钟,一个是一组定时模块,总的来讲,各类定时器都有其特色,SysTick通常用于操作系统的时基和简单的延时,RTC用于维持实际时间和日期,即使在系统断电时也是如此,而Timer则提供广泛的时间测量和控制功能,用于各种定时相关的应用。三种定时器都可以实现简单的计时功能,但是因为他们的不同特性,所以实际应用时,在选择使用哪种组件时,需要根据应用的具体需求来决定。那么本次的第六次作业就到此结束了,我们下次作业再见!