5

本次实战项目所使用的设备是“智学云科技有限公司”出品的“xLabGreenHouse物联网智能应用实验箱”。

实战内容

在xLabGreenHouse实验平台的传感网模块上有一组包含4个LED的模块,通过编写C语言代码控制这4个LED循环执行“LED1亮→LED1灭→LED2亮→LED2灭→LED3亮→LED3灭→LED4亮→LED4灭”来实现“流水灯”的效果。

理论分析

1.功能分析

要让xLabGreenHouse实验平台中的4个LED循环执行“LED1亮→LED1灭→LED2亮→LED2灭→LED3亮→LED3灭→LED4亮→LED4灭”的功能,首先要明确知道这4个LED都接在了CC2530 GPIO的哪些pin上,然后需要设置这些pin的工作模式为“通用IO”,还要编写代码来实现LED亮和灭状态的切换。

2.LED电路原理图

4-LED板块电路示意图

上图中这4个LED的其中一端已经接在了VCC为3.3V的电源上,如果要让LED“亮起来”,那么需要在LED两端形成电压差才能实现。在单片机中数字信号“0”就是“低电平”(一般电压值为0~0.5V),数字信号“1”就是“高电平”(一般电压值为2.4~3V)。很显然,在LED1端写入“1”时,因为同样是高电平,LED左右两边无电压差,因而LED是“灭”的状态;相反,如果写入“0”时,因为是低电平,LED左右两边形成电压差,LED就被点“亮”了。
4-LED与CC2530模块连接电路示意图

从上图可以看出,LED1连接在了P0_0上,LED2连接在P0_1,LED3连接在P0_2,LED4连接在P0_4上。如果想要控制这些LED的亮灭,我们需要把P0_0、P0_1、P0_2和P0_4同时设置为“通用IO的output”。
PxSEL和PxDIR这两类SFR的作用是用来设置GPIO的,那么要想向GPIO的pin写入数据,同样也需要操作相应的SFR,这里针对GPIO三个端口的SFR的名字就是P0,P1和P2。但因为我们一般都只操作某个或某几个pin,因而这三个SFR中每个二进制位也有对应的名字供编码时使用,这些位的命名规则就是:

端口号_位编号

例如“P0_0”就代表“P0端口的0号pin”。如果要让LED2亮,C语言代码是:

P0_1 = 0;

就代码的健壮性来说,上面那句肯定是语法正确,逻辑正确的。但是从代码的可读性来讲,就显得不那么友好了。通常的做法是先定一个C语言的头文件,把这个pin所代表的LED2用宏定义的方式来表述,这样在源码中使用LED2来代替P0_1,会增强代码的可读性。例如:

#define LED2    P0_1

3.程序流程图

流水灯程序流程图

程序流程图作为代码编写前的程序功能逻辑梳理的手段,在程序开发中占据了十分重要的地位。这里强烈建议程序设计人员无论在编写哪种语言的程序代码,都最好先把要编写的功能用程序流程图的方式描述一下。程序流程图可以帮助开发人员对程序功能进行结构化分析,在流程图中会清楚地展示程序的运行是“顺序进行”的,还是“有条件执行”的,还是“循环执行”的。

通过对实战内容的分析,可以画出上图所示的流水灯整体功能流程图。在图中看到了一个“开始”符号,这表明CPU在执行代码时从这里开始运行。IAR工程中所编写的代码是C语言的,所有C语言代码都是从主函数,也就是main函数的第一句代码开始执行。流程图中所有的“箭头”都是单向的,这表明程序代码的执行顺序是按照箭头的指引来进行。

在“开始”符号下面看到了“设置GPIO”功能,这个功能符号用矩形表示一句代码或者一组功能相对独立的代码。“设置GPIO”这里表示连接4个LED的pin进行配置,也可以称之为“初始化配置”。可以把对GPIO的初始化配置代码单独写在一个函数中,然后在main函数中进行调用。

这个流程图中有8处“延迟”功能,而且4个LED都执行了“亮”和“灭”两个功能。考虑到代码编写原则中很重要的一条——尽量不写重复功能,这里可把“延迟”作为一个函数来编写,而4个LED的“亮”和“灭”也通过函数以及参数来控制实现。

为什么需要在LED亮和灭的功能后都添加“延迟”功能?

因为在执行LED闪烁功能时,我们向连接LED的pin传送数字信号“0”可以让它亮,传送数字信号“1”可以让它灭。如果传送完“0”之后紧接着传递“1”,由于CC2530的工作频率比较快,而人的视觉残留导致我们还没来得及分辨出“亮”的状态它就“灭”了。因此需要在“亮”和“灭”的代码执行之后分别增加一定时间的延迟,让我们能够看到它的状态。本次实战中所使用的延迟函数是TI官方举例中给出的软件延迟源码,我们不需要知道该函数的具体定义,只要把它写在正确的位置然后调用即可。

4.根据流程图写代码

既然要编写程序代码,就一定要建立程序文件。IAR for 8051的工程都是从“main.c”这个文件的“main函数”的第一句代码开始执行的,首先要强调的是“main.c”这个文件名不能任意命名,如果工程中没有“main.c”这个文件,那CPU就找不到程序的入口,也就无法“开始”。

虽然流程图描述的是代码在main函数中的运行情况,但不代表所有的源码都要写到“main.c”同一个文件中,因为那样做会使“main.c”整个文件看起来很“臃肿”,而且一些功能相对独立的代码被“独占”在了一个文件中,不方便代码重用。为了使代码结构看起来更加简洁,同时为了增强代码的可重用性,这里将xLabGreenHouse上Node3 CC2530模块中与LED有关的功能代码写在单独的文件“led.h”和“led.c”中,而流水灯实现功能写在“main.c”文件中。

C语言中使用“.h”类型的头文件来指定所需要的其他重要头文件,指定宏定义以及声明函数原型;使用同名的“.c”文件对函数原型进行定义。

※文件名:led.h

/****************************************
*头文件
****************************************/
#include <ioCC2530.h>        //引入CC2530头文件,其中包含各个SFR定义

/***************************************
*宏定义
****************************************/
#define LED1    P0_0            //将P0_0管脚定义为LED1
#define LED2    P0_1            //将P0_1管脚定义为LED2
#define LED3    P0_2            //将P0_2管脚定义为LED3
#define LED4    P0_4            //将P0_4管脚定义为LED4

/***************************************
*功能函数声明
****************************************/
void led_init(void);                //LED初始化函数原型
void led_on(char led);                //打开LED函数原型
void led_off(char led);                //关闭LED函数原型

※文件名:led.c

/***************************************
*头文件
****************************************/
#include “led.h”                    //添加同名的头文件引用

/**************************************
*名  称:led_init
*功  能:LED初始化
*参  数:无
*返回值:无
****************************************/
void led_init(void){
    P0SEL &= ~0x17;             //将P0_0,P0_1,P0_2和P0_4设为通用IO
    P0DIR |= 0x17;                 //设置P0_0,P0_1,P0_2和P0_4的方向为输出

    LED1 = 1;                    //向LED1输出高电平,将LED1熄灭
    LED2 = 1;                    //向LED2输出高电平,将LED2熄灭
    LED3 = 1;                     //向LED3输出高电平,将LED3熄灭
    LED4 = 1;                    //向LED4输出高电平,将LED4熄灭
}
/***************************************
*名  称:led_on
*功  能:打开LED
*参  数:char led
*返回值:无
****************************************/
void led_on(char led){
    switch(led){         //根据参数led的值,使用switch分支语句实现LED判断
        case 0x01:LED1 = 0; 
          break;
        case 0x02:LED2 = 0; 
          break;
        case 0x04:LED3 = 0; 
          break;
        case 0x10:LED4 = 0; 
          break;
        default:    
          break;
    }
}
/***************************************
*名  称:led_off
*功  能:关闭LED
*参  数:char led
*返回值:无
****************************************/
void led_off(char led){
    switch(led){         //根据参数led的值,使用switch分支语句实现LED判断
        case 0x01:LED1 = 1; 
          break;
        case 0x02:LED2 = 1; 
          break;
        case 0x04:LED3 = 1; 
          break;
        case 0x10:LED4 = 1; 
          break;
        default:    
          break;
    }
}

在“led.h”文件中,引用ioCC2530.h文件的代码中使用了“尖括号”,而在“led.c”文件中引用其同名的头文件时使用了“双引号”,两种不同的引用符号所代表的功能也不一样:

  • “尖括号”——代表所引用的文件属于“标准库”头文件,这些头文件是在安装IAR这类具有C语言编译功能的IDE时被拷贝在指定目录的,只需要使用“尖括号”来包裹这些标准库头文件就可以被编译器正确找到。其实再说的更加通俗些的话,就是在“尖括号”中所包裹的头文件是开发环境自带的,不是我们编写的。
  • “双引号”——在C语言代码开发过程中,开发人员也会涉及到编写自己的“头文件”,而且还会有与之同名配对的源码文件。例如上面提到的“led.h”和“led.c”。这两个文件一般都会同时创建并存放在同一个目录下,“头文件”中规定了宏定义和声明函数原型,“源码”文件中对函数原型进行定义,因此必须在“源码”文件中对“头文件”进行引用。在“.c”源码文件中使用“#include”引用同名的“.h”文件时,在“双引号”中写的是“.h”文件与“.c”文件的“相对路径”。编译器在编译过程中定位文件的方式有两种:相对路径或者绝对路径。
绝对路径就是从“根”出发找到该文件的路径
相对路径就是从“当前文件”所在位置出发找到另一个文件的路径。

如果在“led.c”中使用以下的代码引用“led.h”,表明“led.h”与“led.c”在同一个目录下。

#include "led.h"

※文件名:main.c

****************************************
*头文件
****************************************/
#include <ioCC2530.h>
#include “led.h”

/***************************************
*名  称:halWait
*功  能:延迟函数
*参  数:unsigned char wait
*返回值:无
****************************************/
void halWait(unsigned char wait){
    unsigned long largeWait;
    if(wait == 0)return;
    largeWait = ((unsigned short) (wait << 7));
    largeWait += 114 * wait;
    largeWait = (largeWait >> ( CLKCONCMD & 0x07));
    while(largeWait--);
    return;
}

/***************************************
*名  称:delay_ms
*功  能:毫秒延迟函数
*参  数:unsigned short t
*返回值:无
****************************************/
void delay_ms(unsigned short t){
    while(t--){
        halWait(1);
    }
}

/***************************************
*名  称:led_twinkle
*功  能:led闪烁
*参  数:char led
*返回值:无
****************************************/
void led_twinkle(char led){
    led_on(led);
    delay_ms(500);
    led_off(led);
    delay_ms(500);
}

/****************************************
*主函数
****************************************/
void main(void){
    led_init();                     //初始化led
    while(1){
        led_twinkle(0x01);        //LED1闪烁一次
        led_twinkle(0x02);        //LED2闪烁一次
        led_twinkle(0x04);        //LED3闪烁一次
        led_twinkle(0x10);        //LED4闪烁一次
    }
}

在main.c文件中,首先对需要的头文件进行了引用,接着定义了三个功能函数(halWait、delay_ms和led_twinkle),最后才是main函数。C语言代码编写规则中针对函数的操作有一条是:先定义,后调用。也就是说如果要对某个函数进行调用,那么一定要把调用的代码写在函数定义代码的后面。除了这种编写方式外,还有一种方式使代码结构看起来更加清晰,就是在main函数代码之前对需要调用的函数进行声明,就像在头文件中声明那样,只写函数的原型,而在main函数代码之后再编写函数的定义。按照这种方式来修改一下上面的main.c的话,代码如下:

※修改后的main.c

****************************************
*头文件
****************************************/
#include <ioCC2530.h>
#include “led.h”

****************************************
*函数声明
****************************************/
void halWait(unsigned char wait);
void delay_ms(unsigned short t);
void led_twinkle(char led);

/****************************************
*主函数
****************************************/
void main(void){
    led_init();                     //初始化led
    while(1){
        led_twinkle(0x01);        //LED1闪烁一次
        led_twinkle(0x02);        //LED2闪烁一次
        led_twinkle(0x04);        //LED3闪烁一次
        led_twinkle(0x10);        //LED4闪烁一次
    }
}
/***************************************
*名  称:halWait
*功  能:延迟函数
*参  数:unsigned char wait
*返回值:无
****************************************/
void halWait(unsigned char wait){
    unsigned long largeWait;
    if(wait == 0)return;
    largeWait = ((unsigned short) (wait << 7));
    largeWait += 114 * wait;
    largeWait = (largeWait >> ( CLKCONCMD & 0x07));
    while(largeWait--);
    return;
}

/***************************************
*名  称:delay_ms
*功  能:毫秒延迟函数
*参  数:unsigned short t
*返回值:无
****************************************/
void delay_ms(unsigned short t){
    while(t--){
        halWait(1);
    }
}

/***************************************
*名  称:led_twinkle
*功  能:led闪烁
*参  数:char led
*返回值:无
****************************************/
void led_twinkle(char led){
    led_on(led);
    delay_ms(500);
    led_off(led);
    delay_ms(500);
}

ursaminor
12 声望9 粉丝