3.1 工程概述

创建工程

与上一章节类似,我们在一个工作空间中创建每一节课对应的工程:
第3章:定时器实验 - 图1

在code目录下创建相关的目录与文件,其中的common文件夹是用来存放大家共用的文件:
第3章:定时器实验 - 图2

common中的文件(把延时函数和引脚通用配置我们单独出来为公共文件使用):
第3章:定时器实验 - 图3

工程配置

但是使用公共目录这个工程,必须先在工程中进行设置,这样编译器才能找到文件所在的目录。

(1)打开工程选项进行设置。
第3章:定时器实验 - 图4

(2)进入 C/C++ Compiler 选项卡,选择 Preprocessor 选项,然后点击如下按钮。
第3章:定时器实验 - 图5

(3)点击 Click to add。
第3章:定时器实验 - 图6

(4)在弹出的对话框中找到common文件夹所在的目录:
第3章:定时器实验 - 图7

(5)同时还可以把绝对路径转换为相对路径:
第3章:定时器实验 - 图8
提示:
本课程配套的工程中已经设置好了,这里主要讲解如何设置文件夹路径的方法。

3.2 定时器T1实验——查询触发

本节将使用CC2530内部的定时器1对LED等进行定时的开关控制,从而实现闪烁LED的效果。

定时器基础理论

(1)系统时钟频率
时钟发生器会以恒定的时间间隔产生脉冲,这个间歇性的脉冲可以形象理解为芯片的心跳,时钟频率则是用来描述这个心跳的速率。大家通常用1s内时钟发生器产生的脉冲数量来描述时钟频率,例如“时钟频率10 MHz”表示1s内的心跳次数为10 000 000次。CC2530有两种时钟频率可供开发者使用:32MHz和16MHz。

(2)分频系数
分频是指将时钟频率降低为原来的1/N,也称为N分频。比如当时钟频率是16MHz的时候,那么2分频是8MHz。分频系数则是用1/N来表示,比如2分频的分频系数为:1/2。

(3)系统时钟周期
周期和频率的关系可以用公式表示: T = 1 / f,其中T为时钟周期,f为时钟频率。时钟周期和时钟频率成倒数关系。举个例子说明一下,时钟频率为16MHz时表示在1s内时钟发生器可以产生16 000 000个脉冲,而时钟周期则可以表示产生一个脉冲所需要的时间,即1 / 160 000 00s。

(4)计数器
计数器是定时器的核心,用于记录时钟发生器产生的脉冲数量。由于脉冲的时钟周期是恒定的,因此计算定时时间的公式是:t=nT,其中t为定时时间,n为计数次数,T为时钟周期

(5)溢出
由于计数器的范围是有限的,当计数次数超过最大值时就会产生溢出。例如当计数器的大小是16位时,那么计数范围是0~65535,因此计数次数超过65535后计数器就会产生溢出。在产生溢出后,计算器的值会从最大值变为0。

定时原理

利用公式对频率和周期的关系作进一步的解释。我们利用f表示时钟频率,T来表示时钟周期,那么可以用此关系式来表示它们的关系:

  • T = 1 / f (1)

我们计时t秒后,假设此时计数器从0开始计数了N次(假设此时计数器没有溢出)。前面已经讲解过,时钟周期T表示心跳1次所需要时间,因此t与N的关系如下:

  • t = N × T (2)

接着,我们可以推导出:

  • N = t / T (3)

CC2530的默认系统时钟频率是16MHz(16000000Hz),其定时器1使用128分频,因此定时器的时钟频率是 16000000 / 128 Hz。

  • 根据公式(1)T = 1 / f 可以算出定时器1时钟周期为T = 128/16000000 秒。
  • 在定时5秒的情况下(即t=5秒),根据公式(2)N = t / T,计数器的计数值N = 5 / (128/16000000) = 625000。

处理溢出

当定时器溢出时会发生中断,此时寄存器IRCON的Bit1位会由原先的0被设置为1,因此我们只需要检测这个标志位即可判断是否发生了溢出。
具体的相关寄存器说明,请阅读下文的说明

定时器1是一个16位定时器,每溢出一次计数65536次,所以定时5秒后将会溢出: 625000 / 65536 = 9.54,取整数,即9次。反过来,如果溢出了9次,我们可以大约第认为时间过了5秒。

相关寄存器

第3章:定时器实验 - 图9
第3章:定时器实验 - 图10

寄存器配置

  • 定时器1是一个16位的定时器,也就是说计数器能从0~65535进行计数。
  • 定时器1支持5个通道
  1. 1.T1CTL = 0x0D; // 0000 1101:128分频,自由计数(从0~65535)
  2. 2.T1STAT= 0x21; // bit0写1,清空通道0中断状态位
  3. 3. // bit5写1,清空计数器中断状态位

程序说明

main函数代码如下:

  1. void main()
  2. {
  3. uint8_t Counter = 0;
  4. initLed();
  5. initTimer1();
  6. while(1) {
  7. if (!(IRCON & 0x02)) continue; // Timer1 interrupt not pending
  8. IRCON &= ~(0x02); // Clear timer1 interrupt flag
  9. if (++Counter < 9) continue; // ~5 second
  10. else Counter = 0;
  11. DEBUG_LOG("~5 Second.\r\n");
  12. LED = (LED == LED_ON)?LED_OFF : LED_ON;
  13. } /* while */
  14. }

主函数定义了一个计数器,用来记录定时器溢出的次数。前面我们已经算出了定时5秒后会溢出9次。每当定时器溢出后都需要清除溢出标志位。溢出9次后我们需要把计数器归0,重新计数。这样,我们就实现了每隔5秒中打印输出相应提示,并翻转LED灯的亮灭状态。

仿真调试

(1)把开发板连接仿真器,进入仿真模式:
第3章:定时器实验 - 图11

(2)可以看到,每隔大概5秒打印出信息,并且LED灯翻转。

3.3 定时器T3实验——中断触发

定时器3 简介

定时器3是一个8位的定时器,计数范围是0~255。在上节课中,我们采用了查询的方式来定时,本节课带领读者会采用中断的方式。

相关寄存器

第3章:定时器实验 - 图12
第3章:定时器实验 - 图13

寄存器配置

  1. 1.T3CTL = 0xE8; // Bit[7:5] : 111 -> 128分频;
  2. 2. // Bit3 : 1 -> 打开溢出中断
  3. 3. // Bit[1:0] : 00 -> 自由计数,反复从0到255
  4. 4.T3IE = 1; // 使能定时器3中断
  5. 5.EA = 1; // 开启中断总开关
  6. 6.T3CTL |= 0x10; // 启动定时器

定时原理

定时器3的系统时钟频率为:16 MHz的128分频,即16000000/128Hz

  • 由上节课的公式1可以得出,其时钟周期T=128/16000000
  • 在计时5秒的情况下,由上节课的公式2可以得出,计数值 N=5/(128/16000000) = 625000。

由此可以得出,定时器3在系统时钟频率的16MHz的128分频状态下,计数625000次所需的时间5秒。

处理溢出

定时器3是8位计数器,每溢出1次计数256次,因此其在定时5秒后的溢出次数为:625000 / 256 = 2441.4,即2441次。

源码分析

  1. void main()
  2. {
  3. initLed();
  4. initTimer3();
  5. while(1) { }
  6. }

定时器3初始化函数的工作内容

  • 128分频
  • 开溢出中断
  • 自由计数
  • 中断使能
  • 最后启动定时器
  1. static void initTimer3(void)
  2. {
  3. T3CTL = 0xE8; // Tick frequency/128
  4. // Overflow interrupt is enabled
  5. // Free running, repeatedly count from 0x00 to 0xFF
  6. T3IE = 1; // Enable timer3 interrupt
  7. EA = 1; // Enable Interrupts
  8. T3CTL |= 0x10; // Start timer
  9. }

定时器3中断服务函数

  • 声明中断向量T3_VECTOR
  • 溢出计数2441次,溢出2441次时长约等于5秒
  • 每隔5秒调试输出并反转LED
  1. /*
  2. * Timer3 interrupt service function
  3. */
  4. #pragma vector = T3_VECTOR
  5. __interrupt void Timer3_ISR(void)
  6. {
  7. // ~5s
  8. if (++counter_g == 2441) {
  9. counter_g = 0;
  10. DEBUG_LOG("Timer3 timeout -> 5-seconds!\r\n");
  11. LED = (LED == LED_ON)?LED_OFF : LED_ON;
  12. }
  13. }

仿真调试

打开本节课配套的工程并且编译通过后,进行仿真调试,运行结果如下图所示:
第3章:定时器实验 - 图14

3.4 看门狗定时器实验

理论基础

(1)看门狗定时器
看门狗定时器也是定时器,相对对于定时器T1和T3,其特殊性在于:时间到了后会复位芯片。

(2)喂狗
为阻止看门狗定时器复位芯片,我们可以在看门狗定时器计时结束之前,让其重新开始计时(即清零定时器)。这个清零定时器的动作我们称之为”喂狗”。

(3)程序跑飞
程序在正常运行的过程中,有时候可能会突然遇到意外(例如静电干扰)而不正常,没有按正常的流程运行,这个现象我们称之为“程序跑飞”。在遇到程序跑飞现象时,看门狗没有及时被“喂”,时间一到就会去复位芯片。复位芯片后,我们可以通过技术手段让程序恢复正常。(这也是看门狗定时器的意义所在)

(4)应用场景
看门狗定时器可用在电噪声大、电源故障率高或静电放电等恶劣环境中,或对可靠性有更高要求的场合中。如果系统不需要用到看门狗,则可配置成间隔定时器,在指定的时间间隔内发生中断。

相关寄存器

第3章:定时器实验 - 图15

寄存器初始化配置代码如下:

  1. 1.WDCTL = 0x00; // 打开 IDLE模式才能设置看门狗
  2. 2.WDCTL = 0x08; // 看门狗模式、定时1秒

喂狗配置代码如下:

  1. 1.WDCTL |= (0xA << 4); // 在Bit[7:4]依次写入0xA和0x5,定时器被清除
  2. 2.WDCTL |= (0x5 << 4);

程序分析

main函数代码如下:

  1. void main()
  2. {
  3. initLed();
  4. initWatchDogTimer();
  5. while(1) {
  6. #if 0 //0 or 1
  7. delayMs(SYSCLK_16MHZ, 1500);
  8. #else
  9. delayMs(SYSCLK_16MHZ, 500);
  10. watchDogFeet();
  11. delayMs(SYSCLK_16MHZ, 500);
  12. watchDogFeet();
  13. delayMs(SYSCLK_16MHZ, 500);
  14. watchDogFeet();
  15. #endif
  16. LED = (LED == LED_ON)?LED_OFF : LED_ON;
  17. }
  18. }

代码中把看门狗定时器设置成了定时1秒,所以必须在1秒内进行喂狗。

第一段程序直接延时了1.5秒,然后翻转LED灯。由于没有及时喂狗,所以系统不断地复位,而且看不到LED灯闪烁过程

第二段程序虽然也延时了1.5秒,但是是分三次延时且每隔500ms就喂狗一次,所以看门狗不会导致复位,程序正常运行,LED灯闪烁。

调试仿真

(1)打开第一段程序预编译:#if 1,编译下载到开发板,LED灯一直是灭的!

(2)打开第二段程序预编译:#if 0,编译下载到开发板,LED灯闪烁!

3.5 低功耗定时器实验

理论基础

(1)休眠及唤醒
CC2530内部有很多模块,例如定时器模块、收发器模块和51内核等等,这些数字模块不工作时就不耗电,我们把模块这种状态称为“休眠”。这些模块由“休眠”状态进入工作状态时,这个过程称为“唤醒”。

(2)低功耗
低功耗是指芯片耗电量非常少。一般地,耗电量的大小取决于芯片内部有多少模块处于休眠状态,有多少模块处于工作状态。我们也可以给低功耗分等级:芯片内部的越多模块处于休眠状态,其功耗就越低。

(3)低功耗定时器
也称为休眠定时器,可以定时一段时间,在这段时间内会让相关模块进入休眠状态,时间到了再将其唤醒。
CC2530中的休眠定时器是一个24 位的定时器,其时钟频率为:32768kHz

电源管理模式

CC2530有以下集中电源管理模式:
(1)全功能模式
高频晶振(16M或者32M)和低频晶振(32.768K RCOSC/XOSC)全部工作,数字处理器模块正常工作

(2)空闲模式
除了CPU内核停止运行之外,其他和全功能模式一样

(3)PM1
高频晶振关闭,低频晶振正常工作,数字核心模块正常工作

(4)PM2
低频晶振工作,数字核心模块关闭,系统通过RESET,外部中断或者休眠计数器溢出唤醒

(5)PM3
晶振全部关闭,数字处理器核心模块关闭,系统只能通过RESET或者外部中断唤醒,此模式功耗最低

相关寄存器

第3章:定时器实验 - 图16

寄存器配置

休眠定时器初始化配置代码如下:

  1. 1.ST2 = 0; ST1 = 0; ST0 = 0; // 清零休眠定时器计数器
  2. 2.STIE = 1; // 开启休眠定时器中断
  3. 3.STIF = 0; // 清零休眠定时器中断标志
  4. 4.EA = 1; // 打开中断总开关

配置休眠时间方法如下:

(1)由于休眠定时器工作的时钟频率是32768HZ,也就是计数32768次为1秒!

(2)配置休眠时间时,只需要把当前的计数器数值读出来,加上我们需要定时的时间计数值再重新设置到寄存器中即可。
^
读取当前休眠定时器的计数值:

  1. 1.uint32_t sleepTimer = 0;
  2. 2.sleepTimer = (uint32_t)ST0;
  3. 3.sleepTimer |= (uint32_t)ST1 << 8;
  4. 4.sleepTimer |= (uint32_t)ST2 << 16;

加上我们需要定时的时间对应的计数次数,因为计数32768次为1秒,假设我们需要定时sec秒,那就是计数 sec * 32768次:

  1. 1.// 更新睡眠时间
  2. 2.sleepTimer += (uint32_t)sec * 32768;
  3. 把计数值重新配置到寄存器中
  4. 1.ST2 = (uint8_t)(sleepTimer >> 16);
  5. 2.ST1 = (uint8_t)(sleepTimer >> 8);
  6. 3.ST0 = (uint8_t)(sleepTimer);

配置休眠模式:

  1. 1.SLEEPCMD |= mode; // 设置该定时器Bit[1:0],mode: 0~3
  2. 2.PCON = 0x01; // 进入睡眠模式

退出休眠模式:

  1. 1.// 退出休眠
  2. 2.PCON = 0x00;

源码分析

main函数代码如下:

  1. void main()
  2. {
  3. initLed();
  4. initSleepTimer();
  5. while(1)
  6. {
  7. uint8_t i;
  8. for (i = 0; i < 6; i++) {
  9. LED = (LED == LED_ON)?LED_OFF : LED_ON;
  10. delayMs(SYSCLK_32MHZ ,250);
  11. }
  12. DEBUG_LOG("Sleeping...\r\n");
  13. setSleepPeriod(5);
  14. setPowerMode(POWER_MODE_PM2);
  15. }
  16. }

主函数初始化后做了两件事情,闪烁3次LED灯,然后设置休眠定时器的定时时间为5秒,接着设置电源管理模式为PM2,然后就开始启动进入休眠状态了(CPU不工作)。

睡眠定时器中断服务函数(休眠定时器到时间后会中断,在中断处理程序中,CPU重新工作起来):

  1. /*
  2. * Sleep timer ISR
  3. */
  4. #pragma vector = ST_VECTOR
  5. __interrupt void SleepTimer_ISR(void)
  6. {
  7. STIF = 0; // Clear interrupt flag
  8. setPowerMode(POWER_MODE_ACTIVE); // Entry active power mode
  9. DEBUG_LOG("Activing...\r\n");
  10. }

初始化休眠定时器函数:

  1. static void initSleepTimer(void)
  2. {
  3. ST2 = 0;
  4. ST1 = 0;
  5. ST0 = 0;
  6. STIE = 1;
  7. STIF = 0;
  8. EA = 1;
  9. }

设置休眠定时器定时时间,即读出当前计数值、加入新的数值、重新设置计数器:

  1. static void setSleepPeriod(uint8_t nS)
  2. {
  3. uint32_t sleepTimer = 0;
  4. sleepTimer = (uint32_t)ST0;
  5. sleepTimer |= (uint32_t)ST1 << 8;
  6. sleepTimer |= (uint32_t)ST2 << 16;
  7. sleepTimer += (uint32_t)nS * 32768;
  8. ST2 = (uint8_t)(sleepTimer >> 16);
  9. ST1 = (uint8_t)(sleepTimer >> 8);
  10. ST0 = (uint8_t)(sleepTimer);
  11. }

设置电源模式(低功耗模式):

  1. static void setPowerMode(PowerMode_t mode)
  2. {
  3. if (mode > _POWER_MODE_MAX) {
  4. DEBUG_LOG("Power mode not found: %d\r\n", (int)mode);
  5. return;
  6. }
  7. if (mode == POWER_MODE_ACTIVE) {
  8. PCON = 0x00;
  9. return; // Don't sleep
  10. }
  11. SLEEPCMD |= mode; // Set power mode
  12. PCON = 0x01; // Entering sleep mode
  13. }

休眠定时器有4种电源模式,我们把这几种模式定义为类型PowerMode_t:

  1. /*
  2. * Power modes: Active mode, PM1, PM2, PM3
  3. */
  4. typedef enum
  5. {
  6. POWER_MODE_IDLE = 0,
  7. POWER_MODE_PM1 = 1,
  8. POWER_MODE_PM2 = 2,
  9. POWER_MODE_PM3 = 3,
  10. POWER_MODE_ACTIVE = 4,
  11. _POWER_MODE_MAX = POWER_MODE_ACTIVE,
  12. }PowerMode_t;

调试仿真

编译完成后,通过仿真器连接开发板和电脑,进入仿真模式:
第3章:定时器实验 - 图17

可以看到,开发板在LED闪烁3次后就打印Sleep…,这个时候CC2530进入了休眠状态。休眠定时器在定时时间到了后会触发中断,中断服务函数中会让芯片重新进入正常工作状态。