zoukankan      html  css  js  c++  java
  • SAM4E单片机之旅——3、LED闪烁之定时器中断

    让一个LED灯闪烁不过瘾,我们应该让这块开发板完成一点更高难度的任务:比如让两个LED灯闪烁。

    ……

    当然了,以我们的现在使用的空循环技术,还是可以实现这点的。但是这样显得略为低端。所以我们使用一个高端点的技术:中断。还有就是会介绍一下在CMSIS里怎么使用中断。

    一、电路

    二、实现思路

    第一个LED的闪烁还是用之前使用的空循环吧,别把世界弄得太复杂了。

    第二个LED的闪烁就稍微自动化一点了:使用一个定时器,让它在到了需要切换引脚电平的时候通知我们一下。这样做的好处就是我们只需在定时器通知时关注第二个LED灯,而在其他的时候就可以忙别的事了。(比如让第一个LED闪烁。)

    使用的中断源还是之前用到的RTT。RTT可以在计数器达到特定值时产生中断,这个特定的值(Alarm Value)可以通过访问RTT报警寄存器(RTT_AR)设定。然后在RTT的中断处理函数中切换LED引脚的电平,同时设定好下一次中断的条件就好了。

    三、中断

    在中断时,处理器会根据中断号在中断向量表查询中断服务函数(ISR)相关的信息。为此,我们需要知道RTT的中端号(3),还有中断向量表的位置,然后修改中断向量表。系统控制块(SCB)中有个“向量表偏移寄存器”(SCB_VTOR),在这个地址指向的区域里储存着一系列的向量,包括外部中断向量表。然后我们需要知道ISR相关信息在这个向量表的位置。接着修改中断向量表时需要知道它储存的只有ISR的地址,还是直接跳转至ISR的指令……(先别忙着动手)

    四、main函数之前发生的事

    实际上,入口点——即整个程序开始运行的入口,并不是main函数。这个入口是链接器指定的,默认情况下是_start函数。而在Atmel Studio生成的项目中,默认情况下链接器的参数有“--entry=Reset_Handler”的这么一项,意思就是指定程序入口为Reset_Handler

    这个函数的实现在以下文件中:

    srcASFsamutilscmsissam4esource emplatesgccstartup_sam4e.c

    这个是函数也是重置时的中断处理函数。在这个函数中,进行了一系列的初始化工作,其中包括中断向量表的配置。然后在初始化C库之后,就调用main函数了。最后在main函数返回后执行一个死循环。

    五、定义中断处理函数

    CMSIS已经为定义好了各种ISR的函数原型,同时做好了默认的函数实现。这些函数在以下文件中实现:

    srcASFsamutilscmsissam4esource emplatesexceptions.c

    不过默认的函数实现是“弱定义”为Dummy_Handler的别名,这个函数的实现只是一个简单的死循环。弱定义意味着我们可以很方便地在链接时覆盖默认的实现。方法就是重新定义一个具有相同签名的函数。因为默认情况下是“强定义”的,所以就会覆盖掉默认的实现。

    四、CMSIS默默完成的工作

    其实CMSIS已经做好很多事了。

    1. 在运行C语言编译后产生的代码时,需要堆栈来追踪函数调用情况,以及储存一些临时变量。我们编写的main函数得以成功运行的原因之一就是CMSIS已经帮我们准备好了堆栈。
    2. 然后CMSIS完成的事就是配置向量表了。CMSIS已经为定义好了各种ISR的函数原型,同时做好了默认的函数实现。不过默认的函数实现是“弱定义”的,这意味着我们可以很方便地覆盖默认的实现。然后CMSIS就会根据这些信息配置向量表,而在编写自定义的中断处理函数只需根据原型做出实现即可。
    3. 接着CMSIS会进行一系列系统初始化工作(调用SystemInit函数)。包括定义好读写Flash芯片时需要等待的时间,初始化震荡器、锁相环,设定系统主时钟等。
    4. “最后”,就是执行main函数了。

    六、准备工作

    现在程序已经略为复杂了,需要做些准备工作。

    宏定义:

    /* LED 使用的GPIO引脚 */
    #define LED0_GPIO PIO_PA0
    #define LED1_GPIO PIO_PD20
    
    /* LED 闪烁的周期 */
    #define LED0_OFF_MS 500
    #define LED0_ON_MS 1000
    #define LED1_OFF_MS 500
    #define LED1_ON_MS 200

    辅助函数CalcRTTNeedInc。之前为了计算经过指定时间后RTT记数器增加的值,写了几行代码。因为有多个地方要用到这个计算,所以把它抽象出来了:

    inline uint32_t CalcRTTNeedInc(unsigned int ms)
    {
        /* 计数器加一的频率 */
        const uint32_t freq = CHIP_FREQ_SLCK_RC / PRESCALE;
        /* 计算延迟后,计数器需要增加的值
         *  need_inc = ms /1000 / (1/freq) */
        return (ms * freq / 1000);
    
    }

    六、RTT的中断处理

    在理论上,本程序在RTT中断时切换第二个LED的引脚电平,并设置下一次中断的条件。

    在文件sam4e16e.h中,已经定义好了RTT中断处理函数的原型了,只需实现即可。

    需要注意的是,在中断处理函数中,需要通过读取一次RTT_SR以清除RTT的Alarm状态,否则该中断一直会被触发。

    void RTT_Handler(void)
    {
    	/* 通过读取状态寄存器清除Alarm */
    	uint32_t _ = RTT->RTT_SR;
    	uint32_t begin_rttv = ReadRTT_CRTV();
    	uint32_t int_gap_ms ;
    	uint32_t need_inc;
    	
    	if ((PIOD->PIO_ODSR & LED1_GPIO) == 0)
    	{
    		/* 现在引脚电平为低,LED是亮的 */
    		/* 灭灯 */
    		PIOD->PIO_SODR = LED1_GPIO;
    		/* 设置下次中断唤醒间隔的时间 */
    		int_gap_ms = LED1_OFF_MS;
    	}
    	else
    	{
    		/* 现在引脚电平为高,LED是灭的 */
    		/* 亮灯 */
    		PIOD->PIO_CODR = LED1_GPIO;
    		/* 设置下次中断唤醒间隔的时间 */
    		int_gap_ms = LED1_ON_MS;
    	}
    
    	/* 计算并设置下一次中断的条件 */
    	need_inc = CalcRTTNeedInc(int_gap_ms);
    	RTT->RTT_AR = RTT_AR_ALMV(begin_rttv + need_inc - 1);
    
    	return;
    }

    七、RTT初始化中断启用

    如果需要启用中断,需要配置NVIC_ISERx寄存器,而且需要进行一定的计算。而CMSIS也做了相应的工作:

    /* 启用中断 */
    NVIC_ClearPendingIRQ(RTT_IRQn);
    NVIC_EnableIRQ(RTT_IRQn);

    对于RTT,配置时只需使能中断,同时设置第一次中断的条件即可。

    /* 初始化 RTT */	
    RTT->RTT_MR = RTT_MR_RTPRES(PRESCALE)
    				| RTT_MR_RTTRST
    				| RTT_MR_ALMIEN
    				;
    /* 计算第一次中断的时间 
     * 现在灯是亮的,第一次中断即在需要灯灭时
     */
    RTT->RTT_AR = RTT_AR_ALMV(
    	ReadRTT_CRTV() + CalcRTTNeedInc(LED1_ON_MS) -1);

    八、禁用看门狗

    程序在运行若干秒之后,可能会看到一些不和谐的状况,比如某个LED灯不按照我们的设想快速闪动一两下。这是因为看门狗默认是开启的,而我们却从来没有“喂狗”,从而导致系统重置。现在我只需禁用看门狗即可:

    WDT->WDT_MR = WDT_MR_WDDIS;

    PS,完整程序代码:

    这一部分完整代码放在下面,以后大概不会再在这个基础上修改了吧。

    #include <sam.h>
    //#include <sam4e_ek.h>
    
    /* LED 使用的GPIO引脚 */
    #define LED0_GPIO PIO_PA0
    #define LED1_GPIO PIO_PD20
    
    /* LED 闪烁的周期 */
    #define LED0_OFF_MS 500
    #define LED0_ON_MS  200
    #define LED1_OFF_MS 200
    #define LED1_ON_MS  300
    
    #define PRESCALE (1u<<10)
    
    
    inline uint32_t ReadRTT_CRTV(void)
    {
    	//return (RTT->RTT_VR) & RTT_VR_CRTV_Msk;
    	uint32_t v1;
    	uint32_t v2;
    	while(1)
    	{
    		v1 = (RTT->RTT_VR) & RTT_VR_CRTV_Msk;
    		v2 = (RTT->RTT_VR) & RTT_VR_CRTV_Msk;
    		/* 通过连续读取两次RTT_VR的值以增加准备性 */
    		if (v1 == v2)
    		{
    			return v1;
    		}
    	}
    }
    
    inline uint32_t CalcRTTNeedInc(unsigned int ms)
    {
        /* 计数器加一的频率 */
        const uint32_t freq = CHIP_FREQ_SLCK_RC / PRESCALE;
        /* 计算延迟后,计数器需要增加的值
         * need_inc = ms /1000 / (1/freq) */
        return (ms * freq / 1000);
    }
    
    void Delay(unsigned int ms)
    {
        uint32_t begin_rttv = ReadRTT_CRTV();
    	uint32_t need_inc = CalcRTTNeedInc(ms);
        uint32_t end_rttv = begin_rttv + need_inc;
     
        /* 等待*/
        while(ReadRTT_CRTV() < end_rttv)
            ;
    }
    
    /* RTT 中断处理函数
     * 在这里主要就进行LED1引脚电平的切换了
     */
    void RTT_Handler(void)
    {
    	/* 通过读取状态寄存器清除Alarm */
    	uint32_t _ = RTT->RTT_SR;
    	uint32_t begin_rttv = ReadRTT_CRTV();
    	uint32_t int_gap_ms ;
    	uint32_t need_inc;
    	
    	if ((PIOD->PIO_ODSR & LED1_GPIO) == 0)
    	{
    		/* 现在引脚电平为低,LED是亮的 */
    		/* 灭灯 */
    		PIOD->PIO_SODR = LED1_GPIO;
    		/* 设置下次中断唤醒间隔的时间 */
    		int_gap_ms = LED1_OFF_MS;
    	}
    	else
    	{
    		/* 现在引脚电平为高,LED是灭的 */
    		/* 亮灯 */
    		PIOD->PIO_CODR = LED1_GPIO;
    		/* 设置下次中断唤醒间隔的时间 */
    		int_gap_ms = LED1_ON_MS;
    	}
    
    	/* 计算并设置下一次中断的条件 */
        need_inc = CalcRTTNeedInc(int_gap_ms);
    	RTT->RTT_AR = RTT_AR_ALMV(begin_rttv + need_inc - 1);
    
    	return;
    }
    
    int main(void)
    {
    	/* 关闭看门狗 */
    	WDT->WDT_MR = WDT_MR_WDDIS;
    	/* 初始化PIO */
    	/* 让PIO控制器直接控制引脚 */
    	PIOA->PIO_PER = LED0_GPIO;
    	PIOD->PIO_PER = LED1_GPIO;
    	/* 引脚输出使能 */
    	PIOA->PIO_OER = LED0_GPIO;
    	PIOD->PIO_OER = LED1_GPIO;
    	/* 引脚输出写使能 */
    	PIOA->PIO_OWER = LED0_GPIO;
    	PIOD->PIO_OWER = LED1_GPIO;
    
    	/* 初始化 RTT */
    	/* 启用中断 */
    	NVIC_ClearPendingIRQ(RTT_IRQn);
    	NVIC_EnableIRQ(RTT_IRQn);
    	RTT->RTT_MR = RTT_MR_RTPRES(PRESCALE)
    					| RTT_MR_RTTRST
    					| RTT_MR_ALMIEN
    					;
    	/* 计算第一次中断的时间 
    	 * 现在灯是亮的,第一次中断即在需要灯灭时
    	 */
    	RTT->RTT_AR = RTT_AR_ALMV(ReadRTT_CRTV() + CalcRTTNeedInc(LED1_ON_MS) -1);	
    	
    	while (1) {
    		/* 设置PA0引脚为高电平,灯灭 */
    		PIOA->PIO_SODR = LED0_GPIO;
    		/* 延迟 */
    		Delay(LED0_OFF_MS);
    		
    		/* 设置PA0引脚为高电平,灯亮 */
    		PIOA->PIO_CODR = LED0_GPIO;
    		Delay(LED0_ON_MS);
    	}
    	return 0;
    }
  • 相关阅读:
    PHP基本的语法以及和Java的差别
    Linux 性能測试工具
    【Oracle 集群】Linux下Oracle RAC集群搭建之Oracle DataBase安装(八)
    【Oracle 集群】Oracle 11G RAC教程之集群安装(七)
    【Oracle 集群】11G RAC 知识图文详细教程之RAC在LINUX上使用NFS安装前准备(六)
    【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之RAC 特殊问题和实战经验(五)
    【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之缓存融合技术和主要后台进程(四)
    【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之RAC 工作原理和相关组件(三)
    Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之ORACLE集群概念和原理(二)
    【Oracle 集群】ORACLE DATABASE 11G RAC 知识图文详细教程之集群概念介绍(一)
  • 原文地址:https://www.cnblogs.com/h46incon/p/3405850.html
Copyright © 2011-2022 走看看