定时器Timer
64-Bits Timer Plus
1、TIMER
64位通用定时器控制核心板LED循环点亮,周期1秒
2、TIMER_Dual_32-bit_Chained
双32位关联定时器,定时器12为通用定时器,定时器34作为预定标器,周期5秒
3、TIMER_Dual_32-bit_UnChained
双32位独立定时器,定时器12周期1秒,定时器34周期2秒
4、TIMER_Dual_32-bit_UnChained_4-bit_Prescaler
双32位独立定时器,同时定时器34带有4位预定标定时器12周期1秒,定时器34,周期2秒,输入时钟2分频
5、WatchDog
看门狗定时器,程序异常时复位(上电复位Power On Reset)
配置串口软件secureCRT演示
如果没有这秒内没有字符输入,程序复位。
6、BUZZER
蜂鸣器
7、DCMOTOR
直流电机
8、STEPPERMOTOR
步进电机
在这一期视频教程中为大家讲解TMS320C6748当中的定时器模块,几乎所有的嵌入式CPU当中都有定时器这一个外设。定时器的主要功能是帮助我们周期性的执行一些特定的程序功能或者来进行一些计数的操作。比如说我们要闪烁一个LED灯,需要每一秒闪烁一次,所以就需要定时器来为我们实现精确的定时,当然如果对时间要求不是很敏感,我们也可以使用简单的指令延时的方式进行延时,而使用定时器的好处是我们可以获得一个比较精确的时间操作。定时器还有一个功能就是计数功能,它可以对我们外部输入的脉冲进行一些计数,比如说常见的有一些传感器,它的输入就是以一些脉冲波的形式输出的,这时候我们就需要有捕获的模块来进行捕获,当然我们也可以使用专门的捕获模块,比如说在C6748中有eCAP模块(增强捕获模块)来捕获脉冲,如果脉冲波不是很复杂,我们也可以使用定时器来完成操作。
在这一期视频教程中主要讲解以下3个方面内容:
1、特性
首先是特性,也就是我们TMS320C6748当中的定时器模块有哪些特点。
2、时钟
第二部分讲解定时器模块的输入时钟,既然我们要将定时器来作为定时功能使用的时候,我们必须要知道定时器的时钟是多少,这样的话我们才能够计算出一个比较精确的定时时间。
3、模式
第三部分内容讲解C6748中的定时器模块有哪几种工作模式,在这里,定时器跟我们前面所讲过的GPIO通用输入输出口是一样的,它也可以产生中断事件,也可以产生EDMA3事件,当然对于C6000系列DSP或者对于C6748来说,中断和EDMA的配置其实也没什么太大的不同,所以在这里就不详细介绍了,在后面讲解代码时也主要讲解定时器配置部分的内容。
首先是定时器的结构,在C6748的文档中对于定时器的命名是64-Bit Timer Plus,在Timer后面还有一个Plus这个词,说明这个定时器的功能比我们之前所遇到的或者与其他某些做控制用的CPU来说,它的功能更为强大一些。而且定时器应该算是C6748所有的外设中第二个比较简单的外设,从这个框图中就可以明显的看出来。第一个简单的外设是上一集讲到的GPIO通用输入输出口。
可以看到在这张图中,定时器的结构就是几部分内容:首先就是时钟,对于定时器的时钟,它既可以来源于内部时钟,也就是C6748的内部锁相环来提供时钟,也可以由外部输入时钟,注意这个外部输入的时钟既可以是外部的时钟,也可以是外部的事件,外部事件输入以后就触发相应的中断或者是EDMA3的操作。如果输入的是外部时钟,我们就可以把这个定时器当做一个计数器来使用,比如说我们在使用的某些传感器,它的输出可能是以类似于PWM波形输出的模拟信号,这个时候我们就需要有方法来捕获这个脉冲,通过计算占空比来计算这个传感器所要传回来的一些参数。我们在捕获的时候既可以使用eCAP模块,因为C6748中海油3个eCAP模块(增强型捕获模块),如果它的输出比较简单我们也可以使用定时器来完成这个操作。在C6748定时器支持对脉冲的上下沿进行捕获。
在时钟后就是定时器模式的选择,就是选择我们定时器所要工作的模式。
之后就是关于定时器在运行过程中的一些操作,首先就是计数寄存器,一旦我们对定时器初始化完毕并且使能以后,这个定时器当中的计数寄存器每当一个时钟脉冲来临的时候,它的计数值就加1,当这个计数值达到设定的周期值的时候,通过比较模块就产生一个脉冲,而这个脉冲同时可以生成以下4个事件:
1、Timer interrupt to CPU interrupt controller,产生一个CPU中断到CPU中断控制器。
2、Timer event to DMA controller,也可以同时产生一个DMA事件到DMA控制器。
还有两个就是定时器比较特殊的地方
3、Output event to device reset,复位我们的CPU,这里需要注意的是,这个复位执行的是POR复位(Power On Reset),这个跟我们前面讲CCS的Debug操作的system reset的功能是一样的,它是将我们整个CPU当中的所有的设备包括CPU核心,外设,锁相环等等一切全部复位,然后将整个CPU内部全部模块置于一个我们全部所已知的状态下。这里需要注意的是,这个事件只能是看门狗定时器来产生的,而且在C6748中我们只能将定时器1来作为看门狗定时器。当我们把定时器1定义为看门狗定时器的时候,它就来进行一个计数,以一定的时钟周期,当计数值达到我们所设定的周期时候,就向CPU发送一个复位信号,这样的话就把我们的CPU进行复位了。你如果接触到其他的微控制器呢,可能对看门狗定时器比较了解,因为我们的微控制器在通常情况下都是应用于工业控制这样的场合,在这样的场合下我们对我们系统可靠性要求和稳定性要求都是非常高的,我们都必须让我们的程序在运行过程中的任何状态我们都必须是可知的,但是在我们的程序编写过程中可能会遇到各种各样的问题,比如说当时所处的硬件环境比较恶劣,或者我们的程序本身书写有问题,导致我们的程序在运行过程中进行了一次意外,也就是说我们常说的程序跑飞,而程序跑飞以后就并没有按我们之前所预设定的流程来进行,这个时候看门狗定时器的作用就发挥出来了,它将我们整个CPU复位,从而将我们的程序带到一个完全已知的状态再重新运行,在C6748当中的看门狗定时器也是这个功能。
4、Output event to TM64P_OUT12,还有一个输出就是输出事件到相应的输出管脚,在C6748中每一个定时器都有这样一个输出管脚,在我们定时器设置相应的模式下,达到某种触发条件就会向外输出一个事件,也就是脉冲信号。这里还需要讲一下,在这个周期寄存器旁边还有一个周期重载寄存器,它的作用是什么呢?它的作用就是改变我们定时器每次计数达到最大值的时候所要重载的那个值。
定时器工作的流程一般是这样的,在我们设定好周期后,每当脉冲来临时,它的计数值就加1,一直加到周期的时候就会产生脉冲信号,然后就会将这个计数的变量清零再重新计数,而这个时候就会将周期重载寄存器的值复制到周期寄存器当中,那么这个功能由什么用呢,比如说如果我们想用定时器的输出管脚来输出PWM波形来控制LED或者电机的时候,这个就比较有用,通过改变重载的值我们就可以输出更为丰富的PWM波形。
这里还有两个比较特殊的寄存器是控制定时器34,在C6748定时器因为它是64位定时器,所以使用当中我们既可以把它作为64位寄存器来使用也可以当作两个32位寄存器来使用。一般情况下我们把低32位寄存器叫做定时器12,把高32位定时器叫做定时器34,这两个寄存器就是给定时器34使用,它可以为定时器34指定一个分频系数。,
在C6748中还有几个比较寄存器,当我们的定时器的计数值达到比较寄存器的值的时候也会产生相应的事件。
下面看一下C6748当中定时器的一些特性。
首先在C6748中一共有4个64位的通用定时器,也就是说这4个定时器都可以作为通用的64位定时器来使用,同时每个定时器均可配置为两个32位定时器,当它们配置为两个32位定时器时就有以下3中关系:
1、两个32位独立定时器,这两个定时器完全没有关系,而且功能是对称的,可以独立来使用,只不过计数器的最大计数值或者说定时器的周期值从64位变为32位,
2、两个32位独立定时器,定时器34带有4位的预定标,也就是带有4位的分频功能,简单来说对于定时器34来说,它的输入时钟会被我们所设定4位的分频系数进行分频以后再输入到定时器当中。
3、同时这两个32位定时器也可以配置为关联模式,在关联模式下,定时器34作为定时器12的预定标器,也就是说定时器34就相当于定时器12的预分频。
定时器1还可以作为看门狗定时器,当我们程序异常的时候复位整个系统,而且是执行Power On Reset完全复位,将CPU内部的所有模块全部进行复位。
定时器的输入输出支持内部/外部的时钟输入方式。
定时器工作模式支持单次运行:也就是定时器在我们初始化启动之后只运行一次,当我们达到某个特定事件,比如说计数值达到我们所预设的周期值。
连续运行:当我们计数值达到我们所预设的周期值就在产生相应事件后将计数值清0,重新开始计数。
连续运行+周期重载:不仅要重新开始计数,而且每次要将周期值重载,也就是将reload寄存器的值复制到period寄存器当中。
对于事件触发,支持中断触发,EDMA3事件、还支持事件的输出。
当我们的定时器用于捕获的时候它有8个比较寄存器,当我们的计数值达到8个比较寄存器跟任意值相等的时候就产生相应的事件,而且在捕获的时候支持上升沿捕获、下降沿捕获和双边沿捕获脉冲。所以C6748定时器的功能还是比较强大的,这也就是为什么在数据手册对它的描述不仅仅标志为定时器Timer,还加了一个plus。
下面讲解一下时钟。
首先时钟有内部输入和外部输入,如果将定时器作为定时功能来使用的时候,使用最多的就是内部输入了,而外部输入是从定时器相应的管脚输入脉冲信号,这种情况常用的是事件触发和脉冲捕获的时候会用到。
在这里详细讲解使用内部时钟的情况。
首先看这张图,是系统锁相环控制器输出时钟。
这里我们需要关注我们定时器所使用的时钟,Timer64 P0/P1也就是C6748中的定时器0和定时器1,使用的是PLL0_AUXCLK,也就是PLL bypass clock(PLL旁路时钟)。在时钟那一部分我们讲过,PLL旁路时钟就相当于把晶体加CPU内部振荡器之后的时钟直接输入到模块中,在我们的核心板中使用的24MHz的晶体,所以PLL旁路时钟晶振的值是24MHz,也就是说我们定时器Timer64 P0/P1这两个定时器如果使用内部时钟就是24MHz的时钟,这点必须要注意,在我们计算周期值的时候时钟是很关键的。然后就是定时器Timer64 P2/P3,这两个时钟虽然是写在PLL1_SYSCLK2这里,但是需要注意的是:所有这些外设默认使用PLL0_SYSCLK2(all the modules use PLL0_SYSCLK2 by default)这个时钟,当然也可以通过修改寄存器的值来让它使用PLL1的时钟。
在创龙提供的程序当中,所有的外设使用的都是PLL0_SYSCLK2的时钟,所以定时器Timer64 P2/P3也是使用PLL0_SYSCLK2的时钟。这里需要注意的是这个时钟默认的频率是CPU的2分频,而且这个分频系数是固定的不可以更改,当我们的CPU运行在456MHz主频的时候,我们的定时器2和3它的输入时钟就是228MHz。
这里顺带提一下为什么这些外设的时钟是可选的:我们都知道C6748是一款低功耗的CPU,也就是说我们在CPU负载低的时候是可以通过降低CPU频率、降低CPU电压来降低功耗的。但是我们有一些外设比如说串口、定时器这样的外设,如果CPU频率在不停的变化,在运行的时候就会出现一些问题。比如说串口,如果CPU频率发生变化,那么我们在计算波特率的时候就要随着CPU频率的变化而变化,这样在实际运行中就会很麻烦。还有定时器也是一样的,如果频率在随意改变,那么这个定时器的值变得不可靠,就不知道定时是多少。这样就通过修改一个寄存器的值来将它的这些外设的值从跟CPU相关的锁相环转移到PLL1这个锁相环跟CPU的无关的,这样的话不管我们的CPU频率如何变化,这些外设都使用一个固定的频率在运行,这样的话当CPU频率发生变化的时候就不会影响比如说串口的输出、定时器的功能。
在创龙提供的程序中,程序都是跑在CPU默认的主频456MHz下,而所有的外设都是使用的PLL0_SYSCLK2,也就是说这些外设的输入时钟都是CPU的2分频也就是228MHz。对于我们的定时器来说,定时器0、1所使用的时候是旁路时钟也就是24MHz的时钟,而定时器2、3使用的是CPU2分频228MHz的输入时钟。
下面将一下时钟的输入输出管脚,这几张图节选自C6748详细的计数参考手持管脚复用那一节,SYSTEM CONFIGURATION。
可以看到我们的定时器都有相应的输入管脚和输出管脚,可以看到左边这张图是定时器23的输入管脚,右边的是定时器23的输出管脚,这里需要注意的是在管脚复用配置中,0是寄存器的默认值,也就是说默认就是作为0这个功能来使用的。当然为了稳妥起见,如果我们要使用定时器的输入,我们最好要手动配置一下定时器的管脚复用,来让它确定的工作在输入模式下,在输入模式下我们就可以通过外部来输入事件或者输入时钟脉冲。而输出管脚也是需要我们配置的,在我们配置完管脚复用以后,相应定时器的两个管脚就可以作为输出来使用就可以作为输出来使用,可以输出事件或者时钟脉冲。
下面这个图就是定时器01输入输出管脚,这里需要注意的是,定时器01来说输入输出管脚是同一个管脚,这样就有一个问题,也就是它的输入输出是不能同时来使用。或者说你可能会想分时复用行不行,理论上来说可以,当然这样用起来不如定时器23方便。这里需要提醒的是如果需要时钟输入输出功能一定要记得配置管脚复用。
在这一部分给大家讲解一下C6748中定时器工作的几种模式:
1、64位定时器
2、2个32位独立定时器
3、2个32位独立定时器,定时器34带有4位预定标,也就是4位的分频
4、2个32位关联定时器,也就是说定时器34作为定时器12的预定标器,也就是一个预分频
5、看门狗定时器
在这里我只以定时器工作在定时模式来给大家讲解,当然定时器也可以工作在计数模式,配置方法类似,这里就不再赘述了。
在我们的定时器当做64位的通用定时器来说,它的结构比较简单。首先是时钟,可以内部外部,这里我们使用的内部时钟。
在时钟这里多说一句,在所有相关程序都写了注释,提醒大家在使用这些外设的时候要关注这些外设的时钟,对于定时器也不例外。
// 注意:DSP ports, Shared RAM, UART0, EDMA, SPI0, MMC/SDs,
// VPIF, LCDC, SATA, uPP, DDR2/mDDR (bus ports), USB2.0, HPI, PRU
// 这些外设使用的时钟来源为 PLL0_SYSCLK2 默认频率为 CPU 频率的二分之一
// 但是,ECAPs, UART1/2, Timer64P2/3, eHRPWMs,McBSPs, McASP0, SPI1
// 这些外设的时钟来源可以在 PLL0_SYSCLK2 和 PLL1_SYSCLK2 中选择
// 通过修改 System Configuration (SYSCFG) Module
// 寄存器 Chip Configuration 3 Register (CFGCHIP3) 第四位 ASYNC3_CLKSRC
// 配置时钟来源
// (默认值) 0 来源于 PLL0_SYSCLK2
// 1 来源于 PLL1_SYSCLK2
// 如果不是为了降低功耗,不建议修改这个值,它会影响所有相关外设的时钟频率
在Timer这个例程中使用的定时器2,在默认情况下使用的PLL0_SYSCLK2,也就是CPU的2分频。也就是说定时器2的输入频率是228MHz,写这段话的目的就是着重告诉大家这个地方比较关键,希望大家不要搞错。
定时器时钟完了以后主要就是计数,一个计数寄存器,因为我们是当做64位的定时器来使用的,所以计数位数是64位,最后是周期,周期这里涉及到一个比较模块,当我们的计数值和周期值相等的时候就会产生一个事件,或为中断事件或为EDMA事件,或者输出一个脉冲等等。同时我们可以配置周期重载,在作为64位通用定时器相对比较简单,有一个比较重要的是周期值的计算,可能好奇这个值是怎么得来的。
/****************************************************************************/
/* */
/* 宏定义 */
/* */
/****************************************************************************/
// 软件断点
#define SW_BREAKPOINT asm(" SWBP 0 ");
// 64位 定时器 / 计数器周期
// 定时时间 1 秒
// 低32位
#define TMR_PERIOD_LSB32 (0x0D970100)
// 高32位 0
#define TMR_PERIOD_MSB32 (0)
打开Windows系统自带的计算器功能,切换到程序员模式,将这个值以16进制输入0x0D970100转换成10进制就是228000000。定时时间是1s,1s的时间是怎么算的呢?实际定时器是在做一个周期性的计数,也就是说当这个计数变量的值数够228000000的时候,就产生一此事件,而我们定这个周期就是要设置这样一个值,让它数够这么多次数的时候它所花费的时间正好是1s。大致上可以使用这样一个公式得到计数器的计数周期值,因为我们计数器模块工作的时钟频率是228MHz,也就是说我们定时器内部的计数模块每增加1所需要的时间是 1 228000000 s \frac{1}{228000000}s 2280000001s,那么我们如果想要定时器的定时时间是1s,我们只需要在1s中数够228000000个时钟周期就可以了。
T = N ∗ 1 228 ∗ 1 0 6 T = N * \frac{1}{228*10^{6}} T=N∗228∗1061
下面简单看一下关于定时器的一些配置功能。按照惯例,我们看一个程序首先从main函数看起:
/****************************************************************************/
/* */
/* 主函数 */
/* */
/****************************************************************************/
int main(void)
{
// 外设使能配置
PSCInit();
// GPIO 管脚复用配置
GPIOBankPinMuxSet();
// GPIO 管脚初始化
GPIOBankPinInit();
// 定时器 / 计数器初始化
TimerInit();
// DSP 中断初始化
InterruptInit();
// 定时器 / 计数器中断初始化
TimerInterruptInit();
// 主循环
for(;;)
{
}
}
这里需要说一下PSC,由于我们的定时器比较特殊,属于CPU系统内部相关控制模块,准确意义讲它并不是一个挂在总线上的外设,所以定时器不需要使能,在PSCInit函数内使能了一个GPIO口,目的是用定时器控制GPIO口的LED闪烁。
/****************************************************************************/
/* */
/* PSC 初始化 */
/* */
/****************************************************************************/
void PSCInit(void)
{
// 使能 GPIO 模块
// 对相应外设模块的使能也可以在 BootLoader 中完成
PSCModuleControl(SOC_PSC_1_REGS, HW_PSC_GPIO, PSC_POWERDOMAIN_ALWAYS_ON, PSC_MDCTL_NEXT_ENABLE);
}
管脚复用和管脚初始化都是针对控制LED来说的。
// GPIO 管脚复用配置
GPIOBankPinMuxSet();
// GPIO 管脚初始化
GPIOBankPinInit();
下面的三个是跟定时器有关的:
// 定时器 / 计数器初始化
TimerInit();
// DSP 中断初始化
InterruptInit();
// 定时器 / 计数器中断初始化
TimerInterruptInit();
定时器 / 计数器初始化:
/****************************************************************************/
/* */
/* 定时器 / 计数器初始化 */
/* */
/****************************************************************************/
void TimerInit(void)
{
// 配置 定时器 / 计数器 2 为 64 位模式
TimerConfigure(SOC_TMR_2_REGS, TMR_CFG_64BIT_CLK_INT);
// 设置周期
TimerPeriodSet(SOC_TMR_2_REGS, TMR_TIMER12, TMR_PERIOD_LSB32);
TimerPeriodSet(SOC_TMR_2_REGS, TMR_TIMER34, TMR_PERIOD_MSB32);
// 使能 定时器 / 计数器 2
TimerEnable(SOC_TMR_2_REGS, TMR_TIMER12, TMR_ENABLE_CONT);
}
先配置定时器/计数器2为64位模式,同时输入的时钟为内部时钟。
然后配置时钟周期,先配置低32位再配置高32位,当然如果这个计数值没有超过32位数所表示的最大值的话,也没有必要配置高32位。
然后使能定时器2。
之后是DSP中断初始化,主要是初始化DSP中断控制器和使能全局中断。
/****************************************************************************/
/* */
/* DSP 中断初始化 */
/* */
/****************************************************************************/
void InterruptInit(void)
{
// 初始化 DSP 中断控制器
IntDSPINTCInit();
// 使能 DSP 全局中断
IntGlobalEnable();
}
然后就是定时器的中断初始化,主要是映射中断服务函数,映射中断事件,这个跟前边中断是一样的。
/****************************************************************************/
/* */
/* 定时器 / 计数器中断初始化 */
/* */
/****************************************************************************/
void TimerInterruptInit(void)
{
// 注册中断服务函数
IntRegister(C674X_MASK_INT4, TimerIsr);
// 映射中断到 DSP 可屏蔽中断
IntEventMap(C674X_MASK_INT4, SYS_INT_T64P2_TINTALL);
// 使能 DSP 可屏蔽中断
IntEnable(C674X_MASK_INT4);
// 使能 定时器 / 计数器 中断
TimerIntEnable(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
}
主要实现的功能就是在定时器的中断服务函数,在这里是让核心板的两个LED循环点亮的。中断服务函数执行这些操作相对来说比较严谨的。
/****************************************************************************/
/* */
/* 中断服务函数 */
/* */
/****************************************************************************/
void TimerIsr(void)
{
// 禁用定时器 / 计数器中断
TimerIntDisable(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
// 清除中断标志
IntEventClear(SYS_INT_T64P2_TINTALL);
TimerIntStatusClear(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
// 改变 LED 状态
GPIOPinWrite(SOC_GPIO_0_REGS, 109, Flag);
Flag=!Flag;
GPIOPinWrite(SOC_GPIO_0_REGS, 110, Flag);
// 使能 定时器 / 计数器 中断
TimerIntEnable(SOC_TMR_2_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
}
先进入中断,再清除标志,然后执行我们需要执行的操作,改变LED的状态,然后再使能。
下面这个例程是将定时器1作为两个独立的32位定时器来使用的,首先时钟这里需要注意,定时器1使用的时钟是PLL旁路时钟,也就是说是24MHz的输入时钟,这个在设定周期的时候要注意。
// 注意:DSP ports, Shared RAM, UART0, EDMA, SPI0, MMC/SDs,
// VPIF, LCDC, SATA, uPP, DDR2/mDDR (bus ports), USB2.0, HPI, PRU
// 这些外设使用的时钟来源为 PLL0_SYSCLK2 默认频率为 CPU 频率的二分之一
// 但是,ECAPs, UART1/2, Timer64P2/3, eHRPWMs,McBSPs, McASP0, SPI1
// 这些外设的时钟来源可以在 PLL0_SYSCLK2 和 PLL1_SYSCLK2 中选择
// 通过修改 System Configuration (SYSCFG) Module
// 寄存器 Chip Configuration 3 Register (CFGCHIP3) 第四位 ASYNC3_CLKSRC
// 配置时钟来源
// (默认值) 0 来源于 PLL0_SYSCLK2
// 1 来源于 PLL1_SYSCLK2
// 如果不是为了降低功耗,不建议修改这个值,它会影响所有相关外设的时钟频率
由于是使用两个定时器,所以分别为它配置周期,宏写成了表达式的形式,帮助大家更好理解。
/****************************************************************************/
/* */
/* 宏定义 */
/* */
/****************************************************************************/
// 软件断点
#define SW_BREAKPOINT asm(" SWBP 0 ");
// 双 32位 定时器 / 计数器周期
// 定时器 12 定时时间 1 秒
#define TMR_PERIOD12 (1 * 24 * 1000 * 1000)
// 定时器 34 定时时间 2 秒
#define TMR_PERIOD34 (2 * 24 * 1000 * 1000)
定时时间是1秒,而它的时钟是24MHz,所以当我们定时器中的计数模块计数(1 * 24 * 1000 * 1000)这么多次的时候,时间正好是1s。配置上也没有什么不同,这个工程没有LED了,所以就省去了外设初始化,GPIO管脚复用配置等过程。
配置的一些函数主要是对定时器的初始化。
/****************************************************************************/
/* */
/* 定时器 / 计数器初始化 */
/* */
/****************************************************************************/
void TimerInit(void)
{
// 配置 定时器 / 计数器 1 为 32 位模式
TimerConfigure(SOC_TMR_1_REGS, TMR_CFG_32BIT_UNCH_CLK_BOTH_INT);
// 设置周期
TimerPeriodSet(SOC_TMR_1_REGS, TMR_TIMER12, TMR_PERIOD12);
TimerPeriodSet(SOC_TMR_1_REGS, TMR_TIMER34, TMR_PERIOD34);
// 使能 定时器 / 计数器 1
TimerEnable(SOC_TMR_1_REGS, TMR_TIMER_BOTH, TMR_ENABLE_CONT);
}
初始化调用TimerConfigure函数,配置定时器1为两个32位非关联定时器,时钟来源于内部时钟。使用宏的好处是更好理解。
然后分别配置周期,使能定时器。
中断初始化:
/****************************************************************************/
/* */
/* 定时器 / 计数器中断初始化 */
/* */
/****************************************************************************/
void TimerInterruptInit(void)
{
// 注册中断服务函数
IntRegister(C674X_MASK_INT4, Timer12Isr);
IntRegister(C674X_MASK_INT5, Timer34Isr);
// 映射中断到 DSP 可屏蔽中断
IntEventMap(C674X_MASK_INT4, SYS_INT_T64P1_TINT12);
IntEventMap(C674X_MASK_INT5, SYS_INT_T64P1_TINT34);
// 使能 DSP 可屏蔽中断
IntEnable(C674X_MASK_INT4);
IntEnable(C674X_MASK_INT5);
// 使能 定时器 / 计数器 中断
TimerIntEnable(SOC_TMR_1_REGS, TMR_INT_TMR12_NON_CAPT_MODE | TMR_INT_TMR34_NON_CAPT_MODE);
}
因为我们是将它作为两个32位独立的定时器来操作的,这样就需要两个中断服务函数。这里需要注意的是,并不是所有的定时器都支持每个32位定时器都能产生中断事件的。当然,定时器1是可以的,定时器2就不可以,定时器2只能产生一个总的中断事件,所以将定时器2配置为独立的2个32位定时器的时候,两个中断服务函数当中还要集成一个小的判断,判断具体是哪个32位的定时器模块产生的中断事件。在这里因为我们使用的是定时器1,它本来就支持两个独立的定时器分别产生中断事件,所以我们分别配置就好了。最后,中断配置完要使能中断。
剩下就是两个中断服务函数:
/****************************************************************************/
/* */
/* 中断服务函数 */
/* */
/****************************************************************************/
void Timer12Isr(void)
{
// 禁用定时器 / 计数器中断
TimerIntDisable(SOC_TMR_1_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
// 清除中断标志
IntEventClear(SYS_INT_T64P1_TINT12);
TimerIntStatusClear(SOC_TMR_1_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
Time12++;
// 使能 定时器 / 计数器 中断
TimerIntEnable(SOC_TMR_1_REGS, TMR_INT_TMR12_NON_CAPT_MODE);
}
void Timer34Isr(void)
{
// 禁用定时器 / 计数器中断
TimerIntDisable(SOC_TMR_1_REGS, TMR_INT_TMR34_NON_CAPT_MODE);
// 清除中断标志
IntEventClear(SYS_INT_T64P1_TINT34);
TimerIntStatusClear(SOC_TMR_1_REGS, TMR_INT_TMR34_NON_CAPT_MODE);
Time34++;
// 使能 定时器 / 计数器 中断
TimerIntEnable(SOC_TMR_1_REGS, TMR_INT_TMR34_NON_CAPT_MODE);
}
中断服务函数中将两个全局变量自加的操作,例程演示的部分演示了效果。
下面这个例程当中,定时器依然是工作在2个独立32位定时器模式,与上一个例程不同的是,定时器34带有了额外的4分频。
也就是进入定时器34的时钟首先经过4分频,然后才输入到定时器34当中,这里跟刚才的配置也没什么不同,主要就是有一个预定标分频计数的配置。
/****************************************************************************/
/* */
/* 宏定义 */
/* */
/****************************************************************************/
// 软件断点
#define SW_BREAKPOINT asm(" SWBP 0 ");
// 双 32位 定时器 / 计数器周期
// 定时器 12 定时时间 1 秒
#define TMR_PERIOD12 (1 * 24 * 1000 * 1000)
// 定时器 34 定时时间 2 秒
#define TMR_PERIOD34 (2 * 24 * 1000 * 1000)
// 预定标分频计数
#define TMR_Prescale (1)
预定标分频计数在这里配置为1,也就是说输入信号输入到定时器34的时钟要经过2分频。剩下就是在初始化的时候稍有不同,需要初始化预分频的数值。
/****************************************************************************/
/* */
/* 定时器 / 计数器初始化 */
/* */
/****************************************************************************/
void TimerInit(void)
{
// 配置 定时器 / 计数器 1 为 32 位模式
TimerConfigure(SOC_TMR_1_REGS, TMR_CFG_32BIT_UNCH_CLK_BOTH_INT);
// 设置周期
TimerPeriodSet(SOC_TMR_1_REGS, TMR_TIMER12, TMR_PERIOD12);
TimerPeriodSet(SOC_TMR_1_REGS, TMR_TIMER34, TMR_PERIOD34);
TimerPreScalarCount34Set(SOC_TMR_1_REGS, TMR_Prescale);
// 使能 定时器 / 计数器 1
TimerEnable(SOC_TMR_1_REGS, TMR_TIMER_BOTH, TMR_ENABLE_CONT);
}
从这点我们可以看出,C6748定时器使用比较灵活。当然,就像刚刚例程所说的,虽然这两个32位独立定时器功能基本完全一致,但是某些细节还是不一样的,比如定时器12支持事件输出,定时器34才有额外的分频。
在这个例程中,将定时器1配置成2个关联的32位定时器。
我们前面也说过,在这种模式下,定时器34是作为定时器12的预分频器,也就是说我们输入到定时器12的时钟先经过定时器34这个分频器,然后分频后的时钟再输入到定时器12当中,在这里配置主要就是配置定时器12的周期和定时器34,TMR_Prescale在这里当做分频的计数值。
/****************************************************************************/
/* */
/* 宏定义 */
/* */
/****************************************************************************/
// 软件断点
#define SW_BREAKPOINT asm(" SWBP 0 ");
// 双 32位 定时器 / 计数器周期
// 定时器 12 定时时间 5 秒
#define TMR_PERIOD12 (5 * 1000 * 1000)
// 预定标分频计数
#define TMR_Prescale (24)
因为定时器1使用的是PLL旁路时钟也就是24MHz时钟,我们先使用定时器34作为预分频,经过24分频变为1MHz,然后将它的周期设置成(5 * 1000 * 1000)
,这样它的定时时间就是5秒钟。在初始化配置也基本类似,只不过我们在定时器初始化的时候依然是配置定时器34的周期值,所以跟我们前面使用非关联模式的时候或者说独立定时器的时候基本上是类似的。
从图中也可以看出我们时钟的关系,在我们使用内部时钟的时候,内部时钟先经过定时器34进行分频,然后才输入到定时器12当中。从下面时序图也可以看出,当我们定时器34的计数值达到一定值的时候才会向定时器12输出一个脉冲。
将定时器1配置成看门狗定时器,看门狗定时器的配置就更为简单了,因为我们看门狗定时器不需要中断服务函数,在它满足它的中断事件时候产生的是一个复位信号。
对于它的配置最主要的就是定时器周期的配置,这个周期跟定时器的配置也是一样的,而这个值就是(5 * 24 * 1000 * 1000)
十六进制0x07270E00。
/****************************************************************************/
/* */
/* 宏定义 */
/* */
/****************************************************************************/
// 软件断点
#define SW_BREAKPOINT asm(" SWBP 0 ");
// 看门狗定时器周期
// 定时时间 5秒
// 低32位
#define TMR_PERIOD_LSB32 (0x07270E00)
// 高32位
#define TMR_PERIOD_MSB32 (0x0)
数字可以使用十进制的形式、表达式的形式也可以使用十六进制的形式,仅仅是个宏定义而已。对于它的配置主要集中在看门狗定时器初始化这个函数。
/****************************************************************************/
/* */
/* 定时器 / 计数器初始化 */
/* */
/****************************************************************************/
void TimerWatchDogInit(void)
{
// 配置 定时器 / 计数器 1 为 看门狗模式
TimerConfigure(SOC_TMR_1_REGS, TMR_CFG_64BIT_WATCHDOG);
// 设置周期 64位
TimerPeriodSet(SOC_TMR_1_REGS, TMR_TIMER12, TMR_PERIOD_LSB32);
TimerPeriodSet(SOC_TMR_1_REGS, TMR_TIMER34, TMR_PERIOD_MSB32);
// 使能看门狗定时器
TimerWatchdogActivate(SOC_TMR_1_REGS);
}
首先将它配置为64位的看门狗定时器,使用TimerConfigure这个函数,然后设置周期是64位,最后使能看门狗定时器。
然后需要看一下主函数执行的操作。
/****************************************************************************/
/* */
/* 主函数 */
/* */
/****************************************************************************/
int main(void)
{
// 初始化串口终端 使用串口2
UARTStdioInit();
// 定时器 / 计数器初始化
TimerWatchDogInit();
// 打印串口终端信息
UARTPuts("\r\n\r\nTronlong WatchDog Application......\r\n\r\n", -2);
UARTPuts("System is Reset!\r\n\r\n", -2);
UARTPuts("If not any character inputs in every 5 seconds, I will reset......\r\n", -2);
// 主循环
for(;;)
{
// 等待输入字符
UARTPutc(UARTGetc());
// 复位看门狗定时器 “喂狗”
TimerWatchdogReactivate(SOC_TMR_1_REGS);
}
}
// 初始化串口终端 使用串口2
UARTStdioInit();
串口初始化函数是一个非常有用的函数,不管是我们调试算法还是调试外设的时候都可以通过串口终端的函数输出一些状态信息。调用也非常简单,只需要执行UARTStdioInit();
初始化操作,然后使用UARTPuts
、UARTGets
、UARTprintf
、UARTGetHexNum
等等一系列函数,简单看一下,可以看到为我们提供了很多函数:
extern unsigned int UARTPuts(char *pTxBuffer, int numBytesToWrite);
extern unsigned int UARTGets(char *pRxBuffer, int numBytesToRead);
extern unsigned int UARTwrite(const char *pcBuf, unsigned int len);
extern void UARTprintf(const char *pcString, ...);
extern void UARTPutHexNum(unsigned int hexValue);
extern void UARTPutc(unsigned char byteTx);
extern unsigned int UARTGetHexNum(void);
extern unsigned char UARTGetc(void);
extern void UARTPutNum(int value);
extern void UARTStdioInit(void);
extern int UARTGetNum(void);
UARTprintf
函数是一个非常有用的函数,因为它支持一些格式化的操作,可以输出一些类型比如说十进制输入字符串等等。你可能会问,如果我要输入一个浮点数怎么办?这个时候需要一个小技巧,在C语言中提供了一个叫sprintf
的函数,它的功能就是格式化字符串,将我们输入的一些变量值转化为字符串的形式,如果需要输出浮点数或者其他类型,也就是说如果输出的类型是UARTprintf
这个函数不支持的话,就可以先使用sprintf
函数将你需要输出的类型比如说浮点数,转化成字符串的形式,然后再通过UARTprintf
这个函数将这个字符串输出就可以了,这样的话就可以将任意的数据输出到串口中断,极大的方便了我们的调试操作。
当然如果使用CCS仿真调试的话,你也可以简单的使用c语言中标准的printf
、scanf
这样的函数来输出到CCS的终端,但是如果你想脱离CCS来调试程序的话,使用串口终端是非常推荐使用的方式。
这个程序主要是做一个演示功能,如果你在5秒内不输入任何字符的话就执行复位操作。那么是如何实现的呢?
// 主循环
for(;;)
{
// 等待输入字符
UARTPutc(UARTGetc());
// 复位看门狗定时器 “喂狗”
TimerWatchdogReactivate(SOC_TMR_1_REGS);
}
主循环中也只是有两个语句,一个是接收从串口终端输入的字符,然后再将这个字符回显到终端,也就是将接收到的数据再发送回去,然后一句就是复位看门狗定时器。如果你之前用过看门狗定时器你可能会了解,或者说通过刚才的讲解你也快理解它的使用,它就是一个计数器,只要我们启动它,它就一直计数,当计数值达到设定周期时,就发送一个复位信号,但是如果我们程序正常运行时,我们不需要它复位,所以我们就要定时复位看门狗定时器的计数值,也就是俗称的“喂狗”的操作,也就是你不喂它,它就复位。如果当我们程序跑飞的时候就没有办法执行复位操作(喂狗),那么我们的CPU就会执行复位,我们整个系统处于一个已知的确定状态。这里需要注意的是:这两个函数UARTPutc(UARTGetc());
它是一个blocking函数(阻塞函数),它必须要等待你输入字符才会执行下面的语句,这也就是为什么我们只有在不停的输入字符的时候CPU才不会复位,如果我们不输入字符,整个程序就会一直阻塞在UARTGetc()
这个函数当中,也就没有办法执行下面复位看门狗定时器的函数,这样的话我们的CPU就会复位。看门狗定时器实际应用比较广泛,它极大提高了我们的可靠性。这可能也不是一个很好的解决办法,如果发现程序经常复位的话就需要寻找原因,到底是什么原因导致程序循环跑飞。