CC2530 + LED流水灯
前言
在搭建ZigBee定位系统前,先通过几个基础案例熟悉CC2530的一些外设和寄存器编程方式。CC2530基础篇由LED流水灯(按键控制启停、定时器中断方式)、定时器与Delay_ms延时函数、Uart收发信息三章组成。
按键控制启停–通用I/O中断
硬件电路说明
单片机程序的编写,需要考虑到开发板实际的电路设计。由于市面上的ZigBee开发板种类丰富,作者仅根据自己手中开发板进行讲解,所写代码仅供读者参考,但能保证在作者开发板上成功运行。本篇需要了解开发板上的LED电路和按键电路
LED电路
如下图所示,有D1
、D2
、D3
、POW
四个LED灯,分别接在P1
端口的1、2号引脚、P0
端口的4号引脚和GND
。
其中POW
灯一端接VDD3V3
,另一端接GND
,因此当板子上电即自动导通。D1
、D2
、D3
仅当对应引脚设置输出低电平时导通。
KEY电路
如下图所示,有S1
、S2
、S3
三个按键,分别与CC2530的P0_1
(P0端口1号引脚)、P2_0
以及RESET
连接。当按键按下时,引脚与GND
连接,会产生一个下降沿(高电平—>低电平)。
I/O端口
CC2530有21个数字输入/输出引脚,可以配置为通用数字I/O或外设I/O信号,配置为连接到ADC、定时器或 USART 外设。这些 I/O 口的用途可以通过一系列寄存器配置,由用户软件加以实现。
通用I/O
用作通用 I/O 时,引脚可以组成 3 个 8 位端口,端口 0、端口 1 和端口 2,表示为 P0、P1 和 P2。其中,P0 和 P1 是完全的 8 位端口。
要使用通用I/O功能,需要用到以下寄存器:
- 所有的端口均可以通过 SFR(特殊功能寄存器): P0、P1 和 P2 直接寻址和字节寻址
- 寄存器 PxSEL,其中 x 为端口的标号 0~2,用来设置端口的每个引脚为通用 I/O 或者是外部设备 I/O 信号。
- 寄存器 PxDIR 来设置每个端口引脚为输入或输出
- 用作输入时,通用 I/O 端口引脚可以设置为上拉、下拉或三态操作模式。复位之后,所有的端口均设置为带上拉的输入。要取消输入的上拉或下拉功能,就要将 PxINP 中的对应位设置为 1
下面是P0端口相关的寄存器: (直观了解)
通用I/O中断
通用 I/O 引脚设置为输入后,可以用于产生中断。中断可以设置在外部信号的上升或下降沿触发。P0、P1 或 P2 端口都有中断使能位。控制端口中断使能的寄存器如下:
-
IEN1.P0IE: P0中断使能
-
IEN2.P1IE: P1中断使能
-
IEN2.P2IE: P2中断使能
控制位中断使能寄存器如下:
-
PxIEN: Px中断使能, x为0-2
此外还有用于保存中断标志和触发沿设置的寄存器:
-
PICTL: P0、P1和P2触发沿设置
-
PxIFG: Px中断标志, x为0-2
了解完通用I/O中断,还不足以成功使用配置中断。事实上,在CC2530中,配置中断有一个基本的固定流程,将在下一小节进行介绍。
中断屏蔽
根据CC2530数据手册,为了使能任一中断,有以上基本流程。
具体代码讲解
LED.h
#ifndef __LED_H
#define __LED_H
#define LED1 P1_0 // 定义LED1 为P1_0
#define LED2 P1_1 // 定义LED2 为P1_1
#define LED3 P0_4 // 定义LED3 为P0_4
// 函数声明
void LED_Init(void);
void LED_Flow(void);
void LED_OFF(void);
#endif
LED.c
#include <ioCC2530.h> // CC2530头文件 里头是各种寄存器的定义
#include "LED.h"
#include "delay.h"
/**
* @brief 初始化LED
*/
void LED_Init(void)
{
P1SEL &= ~0x03;
P0SEL &= ~0x10;
P1DIR |= 0x03; // P1_0 P1_1 设置为输出
P0DIR |= 0x10; // P0_4 设置为输出
LED1 = 1;
LED2 = 1;
LED3 = 1;
}
/**
* @brief LED流水灯
*/
void LED_Flow(void)
{
LED3 = !LED3; // 转换状态
Delay_ms(5000); // 延时
LED2 = !LED2;
Delay_ms(5000);
LED1 = !LED1;
Delay_ms(5000);
}
/**
* @brief 关闭所有LED
*/
void LED_OFF(void)
{
LED1 = 1;
LED2 = 1;
LED3 = 1;
}
LED_Init()
函数详解:
P1SEL &= ~0x03; // &= 11111100(B) 实际是将P1的0号引脚和1号引脚的功能选择位置0,即选择通用I/O功能
P0SEL &= ~0x10; // &= 11101111(B) 将P0的4号引脚的功能选择位置零, 同上
P1DIR |= 0x03; // |= 00000011 将P1的0号和1号引脚设置为输出模式
P0DIR |= 0x10; // |= 00010000 将P0的4号引脚设置为输出模式,同上不放寄存器的表了
LED1 = 1; // 设置为高电平 关灯 由于硬件是低电平驱动
LED2 = 1; // 默认关灯
LED3 = 1; // 默认关灯
- Note: 要向用CC2530的外设功能,首先需要根据其数据手册,找到相应寄存器,并正确配置。以后的章节中,将不一一对照代码找到寄存器表,仅说明配置的流程以及相应的寄存器。因为寄存器数量太多,全部找出来放上去不如直接翻数据手册。此次找出是为了让读者直观体会到裸机开发中寄存器的重要性,以及LED驱动代码所涉及的寄存器较少,比较适合用于教程。值得注意的是,在以后所有的CC2530开发中,基本流程都与LED流水灯类似,无非就是外设不同用到的寄存器不同,功能越复杂的外设,其需要配置的寄存器更多。
delay.c
#include "delay.h"
void Delay_ms(unsigned int time) // 实际上不精确
{
unsigned char n;
while(time > 0)
{
for(n=0; n<187;++n)
{
asm("nop");
}
time--;
}
}
Delay_ms()
函数,延时1ms,通过循环执行空指令达到延时目的,并不准确,后续在定时器与Delay函数的章节中,会有准确的Delay_ms()
函数实现。
delay.h
#ifndef __DELAY_H
#define __DELAY_H
void Delay_ms(unsigned int time);
#endif
Key.c
#include <ioCC2530.h>
#include "Key.h"
#include "delay.h"
unsigned int key_flag = 0;
/**
* @brief 按键初始化函数 开启P0_1 外部中断
*/
void Key_Init(void)
{
P0IF = 0; // 1. 清除中断标志
P0IE = 1; // 2. 使能P0中断 IEN1寄存器
P0IEN |= 0x02; // 3. 使能P0_1 中断 SFR寄存器
EA = 1; // 4. 使能全局中断
PICTL |= 0x02; // 配置P0 下降沿触发中断
return;
}
/**
* @brief P0 中断服务函数 5.
*/
#pragma vector = P0INT_VECTOR
__interrupt void P0_ISR(void)
{
if(P0IFG > 0) // P0IFG是端口0 的标志寄存器 共八位 对应引脚 7-0
{
P0IFG = 0x00; // 清除标志位
Delay_ms(50); // 延时用于按键消抖
key_flag = ~key_flag; // 按下按钮后状态 按键状态改变
}
P0IF = 0; // 清除 标志位 IRCON寄存器中 这个寄存器汇总了外设 端口0 定时器1-4 以及DMA中断标志等
}
从按键初始化代码中可以看出,要使能一个中断,应遵循上一节的五个基本步骤(具体看中断屏蔽)。
- Note: 值得一提的是 对于CC2530的中断服务函数有其独特的写法
#pragma vector = <中断向量>
__interrupt void <函数名>(void)
{
函数体
}
其中的中断向量,定义在<ioCC2530.h>
文件中,如下图所示
Key.h
#ifndef __KEY_H
#define __KEY_H
#define KEY1 P0_1
extern unsigned int key_flag;
void Key_Init(void);
#endif
main.c
#include <ioCC2530.h>
#include "LED.h"
#include "key.h"
void main(void)
{
LED_Init(); // LED初始化
Key_Init(); // 按键初始化
while(1)
{
if(key_flag == 0)
LED_OFF(); // 关灯
else
LED_Flow(); // 流水灯
}
}
主函数中,先初始化LED和按键, 然后根据按键状态,开始流水灯或者关掉所有LED灯。
定时器中断方式
由于单纯用循环执行空指令的延时函数并不精确,为了更加精确地控制流水灯闪烁,方法之二是利用定时器功能实现延时函数或直接利用定时器中断改变LED灯的状态
定时器T1
定时器T1包括一个16位计数器,当32MHz晶振用作系统时钟源时,定时器T1可以使用的最低时钟频率是1953.125Hz,最高是32 MHz。当16MHz RC 振荡器用作系统时钟源时,定时器T1可以使用的最高时钟频率是16MHz。
可以通过两个8位的SFR读取16位的计数器值:
- T1CNTH: 计数器值高位字节
- T1CNTL: 计数器值低位字节
- 值得注意的是: 当读取T1CNTL时,计数器的高位字节在那时被缓冲到T1CNTH,以便高位字节可以从T1CNTH中读出。因此T1CNTL 必须总是在读取T1CNTH之前首先读取。
模模式
当定时器运行在模模式,16位计数器从0x0000开始,每个活动时钟边沿增加 1。当计数器达到 T1CC0(溢出),寄存器 T1CC0H:T1 CC0L 保存的最终计数值,计数器将复位到 0x0000,并继续递增。如果设置了相应的中断屏蔽位 TIMIF.OVFIM 以及 IEN1.T1EN,将产生一个中断请求。
使用定时器T1的模模式,需要配置以下寄存器:
- T1CTL: 定时器控制寄存器
- T1CC0H和T1CC0L: 保存最终的计数值
- T1CCTL0 : 定时器1通道0 捕获/比较控制,使用定时器1模计数时,必须使能 T1CCTL0.MODE =1
- TIMIF: 定时器 1 /3/4 联合中断屏蔽/标志
- T1EN: 定时器1中断使能
时钟配置
定时器的工作需要时钟源,系统时钟是从所选的主系统时钟源获得的,主系统时钟源可以是32MHz XOSC或16MHz RCOSC。 在确定系统时钟频率、定时器预分频值以及计数器最终计数值(重装值)后,即可确定每发生一次定时器溢出中断函数经过的时间。
-
CLKCONCMD.OSC 位选择主系统时钟的源。
-
CLKCONSTA: 时钟控制状态
具体代码讲解
工程结构
Clock.c
#include <ioCC2530.h>
#include "Clock.h"
/**
* @brief 设置系统时钟为32Mhz晶振
*/
void Oclk32_Init(void)
{
CLKCONCMD &= ~0x40; // 设置系统时钟源为 32MHZ晶振
while(CLKCONSTA & 0x40); // 等待晶振稳定
CLKCONCMD &= ~0x3f; // 定时器tick输出设置 设置系统主时钟频率为 32MHZ
}
/**
* @brief 设置系统时钟为16Mhz晶振
*/
void Oclk16_Init(void)
{
CLKCONCMD |= 0x40;
while(!(CLKCONSTA & 0x40));
CLKCONCMD |= 0x09;
CLKCONCMD &= 0xC9;
}
Clock.h
#ifndef __CLOCK_H
#define __CLOCK_H
void Oclk32_Init(void);
void Oclk16_Init(void);
#endif
timer1.c
#include "timer1.h"
#include "LED.h"
#include <ioCC2530.h>
unsigned int time = 0;
/**
* @brief 定时器1初始化函数 定时器中断
*/
void Timer1_Init(void)
{
// 开启定时器1中断
T1IF = 0; // 清除中断标志
T1STAT = 0x00; // 清除中断标志
T1IE = 1; // 定时器1中断使能 IEN1
T1OVFIM = 1; // 定时器1 溢出中断屏蔽位
EA = 1; // 使能全局中断
// 定时器1参数配置 当系统时钟为32Mhz时1ms中断1次 32 000 000/ 32 / 1000 = 1000Hz = 1ms
T1CTL |= 0x0A; // 预分频 32 模模式
T1CCTL0 |=0X04; //使用模计数时,必须使能 T1CCTL0.MODE =1
T1CC0H = 0x03; // 最终计数值 设为1000 高位寄存器
T1CC0L = 0xE8; // 低位寄存器
}
// 定时器T1中断服务函数
#pragma vector = T1_VECTOR
__interrupt void T1_ISR(void)
{
IRCON = 0x00; // 清除中断标志位
if(time++ >= 1000) // 每进入中断1次 time加1 当time为1000时(约等于1000ms=1s)为真
{
time = 0; // 清除 time
if(LED_Flag == 0)
{
LED1 = ~LED1;
}
else if(LED_Flag == 1)
{
LED2 = ~LED2;
}
else if(LED_Flag == 2)
{
LED3 = ~LED3;
}
LED_Flag = (LED_Flag+1)%3; // 切换灯光
}
}
main.c
#include <ioCC2530.h>
#include "LED.h"
#include "timer1.h"
#include "Clock.h"
void main(void)
{
Oclk32_Init(); // 系统时钟初始化 32MHZ
LED_Init(); // LED初始化
Timer1_Init(); // 定时器1初始化
while(1)
{
// 由于灯光切换在中断函数中实现,主函数初始化后死循环即可
}
}
- Note: LED相关代码与按键控制启停小节一致
结语
本章介绍了CC2530的通用I/O、通用I/O中断以及定时中断的应用案例。简单使用了I/O端口和定时器两大基本外设,并演示了寄存器编程方式(见LED_Init()
函数详解),值得注意的是在LED_Init()
函数讲解中,为了讲解清楚,作者按照的逻辑是根据代码讲解对应寄存器,但实际编程时,应按照根据所用寄存器写代码的逻辑。