在刚开始学习单片机写程序的时候,大多数人都比较喜欢使用全局变量。因为这样写程序写起来比较简单,也容易理解。但是看官方例程的时候,大多数都喜欢使用结构体和指针。感觉指针和结构体看起来麻烦,写起来更麻烦,往往都是一长串字母。但是为什么官方都爱这样用呢?这样用的好处是什么,自己写程序怎么才能写成这种方式。
下面通过一个实际的工程例子来说明,如何一步一步将全局变量改为指针和结构体的方式。
系统需求是,通过一个控制板控制系统的输出功率、工作时间、启动/暂停。控制板上有4个按键,第一个按键用来设置系统的输出功率,第二个按键增加系统工作时间,第三个按键减小系统工作时间,第四个按键设置系统的启动和停止。
接下里使用全局变量开始编写程序
u8 level; //功率等级
u8 count_down; //倒计时计数
u8 work; //是否工作
首先定义三个全局变量来存储系统的输出功率等级,系统工作时间,系统处于启动状态还是停止状态。
接下来初始化这个三个全局变量。‘
void sys_init( void )
{
level = LEVEL_DEF; //功率等级 0--10 <---> 0---1000W 一个档位100W
count_down = COUNT_DOWN_DEF; //倒计时1---15分钟
work = 1; //0 工作 1 停止
}
给三个变量分别设置初始值,设置功率等级为5,也就是半功率,工作时间15分钟,工作模式为停止。
下面就需要在按键程序中判断按键,根据不同的按键来改变这三个值。
void read_key( void )
{
u8 key;
key = KEY_Sacn( 1 );
switch( key )
{
case KEY1_PRES: //功率等级
set_value( powerAdd );
break;
case KEY2_PRES: //时间加
set_value( timeAdd );
break;
case KEY3_PRES: //时间减
set_value( timeSub );
break;
case KEY4_PRES: //启动/停止
set_value( start_pause );
break;
default:
break;
}
}
//通过按键值设置参数
void set_value( u8 value )
{
switch( value ) //只有在工作状态下才能设置功率和时间
{
case powerAdd: //功率加
if( work == 0 )
{
if( level < LEVEL_MAX )
{
level++;
}
else
level = LEVEL_MIN;
}
break;
case timeAdd: //时间加
if( work == 0 )
{
if( count_down < COUNT_DOWN_MAX )
{
count_down++;
}
}
break;
case timeSub: //时间减
if( work == 0 )
{
if( count_down > 1 )
{
count_down--;
}
}
break;
case start_pause: //启动
work = ! work;
if( work == 1 )
{
timer.ms = 0;
timer.second = 0;
timer.minute = 0;
}
break;
default:
break;
}
}
在按键读取函数read_key()
中判断哪个按键按下,如果有按键按下就调用按键处理函数set_value()
对系统的这三个全局变量进行设置。比如功率等级按键按下一次,记录功率等级的全局变量level
的值就加1,当level
值超过最大值之后,它的值就设置为最小值1,这样通过一个按键循环设置系统的输出功率。
这样通过全局变量就可以实现需要的功能,但是现在观察这三个全局变量。
u8 level;
u8 count_down;
u8 work;
单纯从名字上来看,很难发现这三个变量之间的关系,他们是不同系统的变量还是同一个系统的变量?这三个变量是描述了一个系统还是三个系统?如果代码中没有注释,理解起来就比较费劲。 还有另外一种情况,假如现在系统需要升级,一个控制板需要控制两个系统,两个系统除了名字不一样之外,其他的参数都是一样的。那么这时候这三个全局变量就需要修改为下面这种。
u8 level1,level2;
u8 count_down1,count_down2;
u8 work1,work2;
此时程序中以前使用的三个全局变量,全部需要修改了。同样在处理按键的时候,还需要判断是修改系统1的变量还是修改系统2的变量。这样程序修改起来修改量就会比较大。一个按键处理函数还得分成两个按键处理函数,分别处理系统1和系统2的全局变量。
那么有没有什么办法,可以把这三个独立的变量组合成一个,这时就需要用到结构体了。使用结构体可以参考库函数的用法。
将GPIO的端口的三个属性组成一个结构体,这样在初始化端口的时候,就可以直接通过结构体来操作了。
不管是哪个端口在初始化的时候,只需要定义一个结构体,这个结构体里面的成员就会是相同的,不管是GPIOA还是GPIOC,在初始化的时候,都需要设置引脚,模式,速度这三个属性。
下面就模仿库函数,将全局变量修改为结构体形式。
首先将全局变量修改为结构体形式。
typedef struct
{
u8 level; //功率等级
u8 count_down; //倒计时计数
u8 work; //是否工作
} Sys_TypeDef;
Sys_TypeDef sys_info;
按照库函数的那种方式,重新定义一个结构体类型Sys_TypeDef
,它里面有三个成员,这三个成员的名字还是和全局变量的名字一样。这时这个结构体就成了一个数据类型了,不能直接使用。相当于 int
类型一样了,是一个新的数据类型。要使用它,必须用这种类型,声明一个新的变量,像 int a;
一样。此时用这个新的结构体类型,定义一个变量结构体变量 sys_info
,这个变量里面就有三个成员了,这三个成员和上面的全局变量一样,代表了系统的三个属性。
下来初始化系统。
//系统参数初始化
void sys_init( void )
{
sys_info.level = LEVEL_DEF; //电源功率等级 0--10 <-> 0---1000W 一个档位100W
sys_info.count_down = COUNT_DOWN_DEF; //倒计时1---15分钟
sys_info.work = 1; //0 工作 1 停止
}
要设置系统属性的时候,通过结构体变量名后面加一个点来访问系统的属性。同样通过按键设置的时候,也是这种方式来访问。
//通过按键值设置参数
void set_value( u8 value )
{
switch( value ) //只有在工作状态下才能设置功率和时间
{
case powerAdd: //功率加
if( sys_info.work == 0 )
{
if( sys_info.level < LEVEL_MAX )
{
sys_info.level++;
}
else
sys_info.level = LEVEL_MIN;
}
break;
case timeAdd: //时间加
if( sys_info.work == 0 )
{
if( sys_info.count_down < COUNT_DOWN_MAX )
{
sys_info.count_down++;
}
}
break;
case timeSub: //时间减
if( sys_info.work == 0 )
{
if( sys_info.count_down > 1 )
{
sys_info.count_down--;
}
}
break;
case start_pause: //启动
sys_info.work = ! sys_info.work;
if( sys_info.work == 1 )
{
timer.ms = 0;
timer.second = 0;
timer.minute = 0;
}
break;
default:
break;
}
}
void read_key( void )
{
u8 key;
key = KEY_Sacn( 1 );
switch( key )
{
case KEY1_PRES: //功率等级
set_value( powerAdd );
break;
case KEY2_PRES: //时间加
set_value( timeAdd );
break;
case KEY3_PRES: //时间减
set_value( timeSub );
break;
case KEY4_PRES: //启动/停止
set_value( start_pause );
break;
default:
break;
}
}
通过这种方式,把原来的三个全局变量封装到了一个盒子中的。这样直接通过名字就可以清晰的看出,这个三个变量是一起的,他们共同来描述了这个系统的属性。相当于单打独斗的三个全局变量,现在组成了一个小团队。此时如果一个控制板需要控制两个系统,那么定义变量的时候就可以通过结构体来直接定义。
Sys_TypeDef sys_info1,sys_info2;
通过这种方式定义之后,表面上看起来是两个变量,但是每个变量中还包含了三个成员。这就相当于将描述系统属性的三个全局变量全部拷贝的一份。在访问不同系统的内部属性时,可以按照下面的方式操作。
sys_info1.level = 6;
sys_info2.level = 8;
结构体变量的名字不一样,但是变量内部成员的名字都是一样的。通过这个就可以很简单的看出来第一条语句是设置系统1的功率等级为6,第二天语句是设置系统功率等级为8。
这样通过结构体来设置,代码上看起来更加的简洁明了。这时虽然可以通过再定义一个结构体变量来控制系统2,但是按键处理函数,依然得写两个,因为在按键处理函数中,直接调用的是结构体的全局变量。每次处理的都是指定系统的按键。如果有两个系统,那么就得写两个按键处理函数。
有没有什么办法,可以只写一个按键处理函数,就可以处理两个系统的按键。这时就需要用到指针了。
观察系统函数可以发现,初始化不同的IO口时,调用的都是同一个初始化函数。
这个函数的原型如下:
这个函数的参数是两个指针,将端口和端口属性的初始化都通过指针传递进来。这样传递的是GPIOA的指针,就初始化GPIOA口,传递的是GPIOC的指针,就初始化GPIOC口。
下面就模仿库函数这种方法,将结构体全局变量修改为通过指针传递。
typedef struct
{
u8 level; //功率等级
u8 count_down; //倒计时计数
u8 work; //是否工作
} Sys_TypeDef;
Sys_TypeDef sys_info;
//系统参数初始化
void sys_init( void )
{
sys_info.level = LEVEL_DEF; //电源功率等级 0--10 <-> 0---1000W 一个档位100W
sys_info.count_down = COUNT_DOWN_DEF; //倒计时1---15分钟
sys_info.work = 1; //0 工作 1 停止
}
结构体的定义和初始化不变,需要修改按键设置函数。
//通过按键值设置参数
void set_value( u8 value,Sys_TypeDef *Sys_InitStruct)
{
switch( value ) //只有在工作状态下才能设置功率和时间
{
case powerAdd: //功率加
if( Sys_InitStruct->work == 0 )
{
if( Sys_InitStruct->level < LEVEL_MAX )
{
Sys_InitStruct->level++;
}
else
Sys_InitStruct->level = LEVEL_MIN;
}
break;
case timeAdd: //时间加
if( Sys_InitStruct->work == 0 )
{
if( Sys_InitStruct->count_down < COUNT_DOWN_MAX )
{
Sys_InitStruct->count_down++;
}
}
break;
case timeSub: //时间减
if( Sys_InitStruct->work == 0 )
{
if( Sys_InitStruct->count_down > 1 )
{
Sys_InitStruct->count_down--;
}
}
break;
case start_pause: //启动
Sys_InitStruct->work = ! Sys_InitStruct->work;
if( Sys_InitStruct->work == 1 )
{
timer.ms = 0;
timer.second = 0;
timer.minute = 0;
}
break;
default:
break;
}
}
原来是在按键处理函数中直接调用结构体的全局变量,现在要将结构体作为一个参数传递到按键处理函数中。由于结构体的类型为Sys_TypeDef
,那么就在按键处理函数的参数中定义 一个 Sys_TypeDef
类型的指针,将结构体的地址直接传递到函数中。在函数中访问结构体的成员时,不能向以前一样 直接通过 .
符号来访问,而是需要通过->
符号来访问。当调用这个函数的时候,就需要将结构体的地址传递进来。
void read_key( void )
{
u8 key;
key = KEY_Sacn( 1 );
switch( key )
{
case KEY1_PRES: //功率等级
set_value( powerAdd ,&sys_info);
break;
case KEY2_PRES: //时间加
set_value( timeAdd ,&sys_info);
break;
case KEY3_PRES: //时间减
set_value( timeSub,&sys_info );
break;
case KEY4_PRES: //启动/停止
set_value( start_pause,&sys_info );
break;
default:
break;
}
}
在调用按键处理函数的时候,需要将结构体的地址传递到函数中去,所以要在结构体名称前面添加上&
符号。此时系统参数都在sys_info
这个结构体中包含着,在传递参数的时候在这个结构体名称前加上&
符号就行。就相当于将这个结构体的地址传递了过去。在按键处理函数中,直接通过地址去操作这个结构体。此时如果需要同时处理两个系统,那么系统的结构体定义就改为:
Power_TypeDef sys_info1;
Power_TypeDef sys_info2;
在按键处理的时候,这两个结构体就可以共用一个按键处理函数了,假如此时系统1的功率等级按键按下,就将系统1的结构体地址传递过去。
set_value( powerAdd ,&sys_info1);
假如此时系统2的功率等级按键按下,就将系统2的结构体地址传递过去。
set_value( powerAdd ,&sys_info2);
这样两个不同的系统就可以共享一个按键处理函数了。当哪个系统需要处理按键的时候,只需要将系统结构体的地址传递到按键处理函数中就行,而按键处理函数在处理的过程中就不需要关心当前处理的是哪个系统的参数。相当于按键处理函数就完全脱离了对系统的依赖,只有系统的框架一样,这个函数就成了一个通用的函数。
通过上面三个例子可以看到,从按键处理函数直接调用全局变量,到调用结构体,最后到调用结构体的地址。按键处理函数对具体系统的依赖逐渐减少。按键处理函数从处理一个具体的系统参数抽象为了处理一类相似系统的参数。函数看起来更加抽象了,同时函数的功能也更加强大了。
通过上面的例子就可以明白,为什么官方系统使用的接口函数全部是这种抽象的函数了,函数的参数都是各种结构体和指针,这样不同型号的单片机,只要是处理方法类似,那么就可以统一调用这一个函数。不同的单片机不同的GPIO口,处理的方法基本都是一样的。这样在写单片机的程序时,只要学会了一种单片机的使用方法,那么其他类似的单片机就全部会使用了,极大的降低了单片机的学习成本。使用起来也更加方便了。