3.1 工程概述
创建工程
与上一章节类似,我们在一个工作空间中创建每一节课对应的工程:
在code目录下创建相关的目录与文件,其中的common文件夹是用来存放大家共用的文件:
common中的文件(把延时函数和引脚通用配置我们单独出来为公共文件使用):
工程配置
但是使用公共目录这个工程,必须先在工程中进行设置,这样编译器才能找到文件所在的目录。
(1)打开工程选项进行设置。
(2)进入 C/C++ Compiler 选项卡,选择 Preprocessor 选项,然后点击如下按钮。
(3)点击 Click to add。
(4)在弹出的对话框中找到common文件夹所在的目录:
(5)同时还可以把绝对路径转换为相对路径:
提示:
本课程配套的工程中已经设置好了,这里主要讲解如何设置文件夹路径的方法。
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秒。
相关寄存器
寄存器配置
- 定时器1是一个16位的定时器,也就是说计数器能从0~65535进行计数。
- 定时器1支持5个通道
1.T1CTL = 0x0D; // 0000 1101:128分频,自由计数(从0~65535)
2.T1STAT= 0x21; // bit0写1,清空通道0中断状态位
3. // bit5写1,清空计数器中断状态位
程序说明
main函数代码如下:
void main()
{
uint8_t Counter = 0;
initLed();
initTimer1();
while(1) {
if (!(IRCON & 0x02)) continue; // Timer1 interrupt not pending
IRCON &= ~(0x02); // Clear timer1 interrupt flag
if (++Counter < 9) continue; // ~5 second
else Counter = 0;
DEBUG_LOG("~5 Second.\r\n");
LED = (LED == LED_ON)?LED_OFF : LED_ON;
} /* while */
}
主函数定义了一个计数器,用来记录定时器溢出的次数。前面我们已经算出了定时5秒后会溢出9次。每当定时器溢出后都需要清除溢出标志位。溢出9次后我们需要把计数器归0,重新计数。这样,我们就实现了每隔5秒中打印输出相应提示,并翻转LED灯的亮灭状态。
仿真调试
(1)把开发板连接仿真器,进入仿真模式:
(2)可以看到,每隔大概5秒打印出信息,并且LED灯翻转。
3.3 定时器T3实验——中断触发
定时器3 简介
定时器3是一个8位的定时器,计数范围是0~255。在上节课中,我们采用了查询的方式来定时,本节课带领读者会采用中断的方式。
相关寄存器
寄存器配置
1.T3CTL = 0xE8; // Bit[7:5] : 111 -> 128分频;
2. // Bit3 : 1 -> 打开溢出中断
3. // Bit[1:0] : 00 -> 自由计数,反复从0到255
4.T3IE = 1; // 使能定时器3中断
5.EA = 1; // 开启中断总开关
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次。
源码分析
void main()
{
initLed();
initTimer3();
while(1) { }
}
定时器3初始化函数的工作内容
- 128分频
- 开溢出中断
- 自由计数
- 中断使能
- 最后启动定时器
static void initTimer3(void)
{
T3CTL = 0xE8; // Tick frequency/128
// Overflow interrupt is enabled
// Free running, repeatedly count from 0x00 to 0xFF
T3IE = 1; // Enable timer3 interrupt
EA = 1; // Enable Interrupts
T3CTL |= 0x10; // Start timer
}
定时器3中断服务函数
- 声明中断向量T3_VECTOR
- 溢出计数2441次,溢出2441次时长约等于5秒
- 每隔5秒调试输出并反转LED
/*
* Timer3 interrupt service function
*/
#pragma vector = T3_VECTOR
__interrupt void Timer3_ISR(void)
{
// ~5s
if (++counter_g == 2441) {
counter_g = 0;
DEBUG_LOG("Timer3 timeout -> 5-seconds!\r\n");
LED = (LED == LED_ON)?LED_OFF : LED_ON;
}
}
仿真调试
打开本节课配套的工程并且编译通过后,进行仿真调试,运行结果如下图所示:
3.4 看门狗定时器实验
理论基础
(1)看门狗定时器
看门狗定时器也是定时器,相对对于定时器T1和T3,其特殊性在于:时间到了后会复位芯片。
(2)喂狗
为阻止看门狗定时器复位芯片,我们可以在看门狗定时器计时结束之前,让其重新开始计时(即清零定时器)。这个清零定时器的动作我们称之为”喂狗”。
(3)程序跑飞
程序在正常运行的过程中,有时候可能会突然遇到意外(例如静电干扰)而不正常,没有按正常的流程运行,这个现象我们称之为“程序跑飞”。在遇到程序跑飞现象时,看门狗没有及时被“喂”,时间一到就会去复位芯片。复位芯片后,我们可以通过技术手段让程序恢复正常。(这也是看门狗定时器的意义所在)
(4)应用场景
看门狗定时器可用在电噪声大、电源故障率高或静电放电等恶劣环境中,或对可靠性有更高要求的场合中。如果系统不需要用到看门狗,则可配置成间隔定时器,在指定的时间间隔内发生中断。
相关寄存器
寄存器初始化配置代码如下:
1.WDCTL = 0x00; // 打开 IDLE模式才能设置看门狗
2.WDCTL = 0x08; // 看门狗模式、定时1秒
喂狗配置代码如下:
1.WDCTL |= (0xA << 4); // 在Bit[7:4]依次写入0xA和0x5,定时器被清除
2.WDCTL |= (0x5 << 4);
程序分析
main函数代码如下:
void main()
{
initLed();
initWatchDogTimer();
while(1) {
#if 0 //0 or 1
delayMs(SYSCLK_16MHZ, 1500);
#else
delayMs(SYSCLK_16MHZ, 500);
watchDogFeet();
delayMs(SYSCLK_16MHZ, 500);
watchDogFeet();
delayMs(SYSCLK_16MHZ, 500);
watchDogFeet();
#endif
LED = (LED == LED_ON)?LED_OFF : LED_ON;
}
}
代码中把看门狗定时器设置成了定时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或者外部中断唤醒,此模式功耗最低
相关寄存器
寄存器配置
休眠定时器初始化配置代码如下:
1.ST2 = 0; ST1 = 0; ST0 = 0; // 清零休眠定时器计数器
2.STIE = 1; // 开启休眠定时器中断
3.STIF = 0; // 清零休眠定时器中断标志
4.EA = 1; // 打开中断总开关
配置休眠时间方法如下:
(1)由于休眠定时器工作的时钟频率是32768HZ,也就是计数32768次为1秒!
(2)配置休眠时间时,只需要把当前的计数器数值读出来,加上我们需要定时的时间计数值再重新设置到寄存器中即可。
^
读取当前休眠定时器的计数值:
1.uint32_t sleepTimer = 0;
2.sleepTimer = (uint32_t)ST0;
3.sleepTimer |= (uint32_t)ST1 << 8;
4.sleepTimer |= (uint32_t)ST2 << 16;
加上我们需要定时的时间对应的计数次数,因为计数32768次为1秒,假设我们需要定时sec秒,那就是计数 sec * 32768次:
1.// 更新睡眠时间
2.sleepTimer += (uint32_t)sec * 32768;
把计数值重新配置到寄存器中
1.ST2 = (uint8_t)(sleepTimer >> 16);
2.ST1 = (uint8_t)(sleepTimer >> 8);
3.ST0 = (uint8_t)(sleepTimer);
配置休眠模式:
1.SLEEPCMD |= mode; // 设置该定时器Bit[1:0],mode: 0~3
2.PCON = 0x01; // 进入睡眠模式
退出休眠模式:
1.// 退出休眠
2.PCON = 0x00;
源码分析
main函数代码如下:
void main()
{
initLed();
initSleepTimer();
while(1)
{
uint8_t i;
for (i = 0; i < 6; i++) {
LED = (LED == LED_ON)?LED_OFF : LED_ON;
delayMs(SYSCLK_32MHZ ,250);
}
DEBUG_LOG("Sleeping...\r\n");
setSleepPeriod(5);
setPowerMode(POWER_MODE_PM2);
}
}
主函数初始化后做了两件事情,闪烁3次LED灯,然后设置休眠定时器的定时时间为5秒,接着设置电源管理模式为PM2,然后就开始启动进入休眠状态了(CPU不工作)。
睡眠定时器中断服务函数(休眠定时器到时间后会中断,在中断处理程序中,CPU重新工作起来):
/*
* Sleep timer ISR
*/
#pragma vector = ST_VECTOR
__interrupt void SleepTimer_ISR(void)
{
STIF = 0; // Clear interrupt flag
setPowerMode(POWER_MODE_ACTIVE); // Entry active power mode
DEBUG_LOG("Activing...\r\n");
}
初始化休眠定时器函数:
static void initSleepTimer(void)
{
ST2 = 0;
ST1 = 0;
ST0 = 0;
STIE = 1;
STIF = 0;
EA = 1;
}
设置休眠定时器定时时间,即读出当前计数值、加入新的数值、重新设置计数器:
static void setSleepPeriod(uint8_t nS)
{
uint32_t sleepTimer = 0;
sleepTimer = (uint32_t)ST0;
sleepTimer |= (uint32_t)ST1 << 8;
sleepTimer |= (uint32_t)ST2 << 16;
sleepTimer += (uint32_t)nS * 32768;
ST2 = (uint8_t)(sleepTimer >> 16);
ST1 = (uint8_t)(sleepTimer >> 8);
ST0 = (uint8_t)(sleepTimer);
}
设置电源模式(低功耗模式):
static void setPowerMode(PowerMode_t mode)
{
if (mode > _POWER_MODE_MAX) {
DEBUG_LOG("Power mode not found: %d\r\n", (int)mode);
return;
}
if (mode == POWER_MODE_ACTIVE) {
PCON = 0x00;
return; // Don't sleep
}
SLEEPCMD |= mode; // Set power mode
PCON = 0x01; // Entering sleep mode
}
休眠定时器有4种电源模式,我们把这几种模式定义为类型PowerMode_t:
/*
* Power modes: Active mode, PM1, PM2, PM3
*/
typedef enum
{
POWER_MODE_IDLE = 0,
POWER_MODE_PM1 = 1,
POWER_MODE_PM2 = 2,
POWER_MODE_PM3 = 3,
POWER_MODE_ACTIVE = 4,
_POWER_MODE_MAX = POWER_MODE_ACTIVE,
}PowerMode_t;
调试仿真
编译完成后,通过仿真器连接开发板和电脑,进入仿真模式:
可以看到,开发板在LED闪烁3次后就打印Sleep…,这个时候CC2530进入了休眠状态。休眠定时器在定时时间到了后会触发中断,中断服务函数中会让芯片重新进入正常工作状态。