为什么要使用模块化程序设计?
大家可能在在学习嵌入式过程中经常会碰到动辄就是在一个文件里面实现一个功能几百上千行代码,然后花了大量的时间去理清代码设计的思路,当分析清楚后,发现原来这个程序其实可以写的更好理解,我在工作中碰到过很多类似的工程师大牛,都是十几年行业经验,不可否认有些确实写的很优秀,但是写的代码跳来跳去,需要你花费大量的时间来阅读,我们技术工程师的时间很宝贵,行业竞争也很激烈,如何用高效易于沟通的程序框架写程序那就显得尤为重要,腾出来的时间完全可以学习更多有意义的东西,同时也能得到同事老板的认可!!为这一步加油⛽
我们先来看下常用的嵌入式控制系统基本框架和两个用的非常广泛的控制器的拆解图片
我们看了上面的嵌入式系统基本框架,结合我们平常学习51单片机了解到的知识,单片机本身需要处理的基本就是一些输入量(数字输入和模拟输入)、输出量(显示器,数字输出和模拟输出)以及通信。
比如我们用按键控制LED灯亮灭:我们检测按键的输入状态,然后根据应用程序逻辑来改变LED灯的输出状态。
再比如我们的洗衣机:洗衣机面板上有按键输入量、门限位开关量、离散输出电机正反转控制、进水电磁阀控制、排水牵引电机控制、LED显示状态、数码管显示当前洗涤剩余时间。
总结起来就是:这些输入输出量可以做成标准化模块,而我们的应用程序则根据我们具体的需求去实现相应的逻辑,这样我们每次就可以把这些标准模块组装起来然后直接开始我们的应用程序,像搭乐高积木一样。
模块化程序变形记
我们先来看这个按键控制LED灯状态翻转单个源文件程序
/*************************************
** 文件名称:main.c
** 描 述:LED灯闪烁
** 实验平台:STC12C5A60S2最小系统板 + 面包板 + 分立元器件
** 日 期:2020/01/23
** 作 者:wllis
** 历史记录:
LED->P0.0引脚
KEY->P0.2引脚
**************************************/
#include
#include
// 宏定义
#define ON 0
#define OFF 1
#define KEY_UP 1
#define KEY_DOWN 0
/****** 变量类型定义 defines ******/
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
/******** 引脚定义 ***************/
sbit LED = P0^0;
sbit KEY = P0^2;
/********* 函数定义 **************/
void delay( uint16_t time );
void Delay1ms( void );
void Key_Init( void );
uint8_t Key_Scan( void );
void LED_Togle( void );
/*************************************
** 函 数 名:main
** 输入参数:none
** 返 回 值:none
** 说 明:主函数
**************************************/
void main ( void )
{
uint8_t key_value = 0;
Key_Init();
while(1)
{
key_value = Key_Scan(); // 按键扫描
if(1==key_value) // 按键按下翻转下LED灯状态
{
LED_Togle();
key_value = 0;
}
}
}
/*************************************
** 函 数 名:delay
** 输入参数:需要延时ms数,16位无符号整数
** 返 回 值:none
** 说 明:延时函数
**************************************/
void delay( uint16_t time )
{
while(--time){ Delay1ms(); }
}
/*************************************
** 函 数 名:Delay1ms
** 输入参数:none
** 返 回 值:none
** 说 明:1ms延时函数
**************************************/
void Delay1ms( void ) //@11.0592MHz
{
uint8_t i, j;
_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
/*************************************
** 函 数 名:LED_Blink
** 输入参数:none
** 返 回 值:none
** 说 明:LED翻转函数
**************************************/
void LED_Togle( void )
{
LED = ~LED;
}
/*************************************
** 函 数 名:Key_Init
** 输入参数:none
** 返 回 值:none
** 说 明:按键初始化程序
**************************************/
void Key_Init( void )
{
KEY = 1; // 按键端口置1,用于输入读取
}
/*************************************
** 函 数 名:Key_Scan
** 输入参数:none
** 返 回 值:uint8_t型按键值
** 说 明:按键扫描程序
**************************************/
uint8_t Key_Scan( void )
{
if( KEY_DOWN==KEY )
{
delay(20);
if( KEY_DOWN==KEY )
{
while(KEY_DOWN==KEY);
return 1;
}
else{ return 0; }
}
else{ return 0; }
}
/************* end of file **********/
程序模块拆解
接下来我们来将这个程序进行拆分,拆成模块化的程序,我们将其拆成3个模块,分别是LED模块,按键模块,和主函数模块;包涵的文件分别是:LED.c, LED.h KEY.c, KEY.h, main.c, main.h,
第一步:我们把文件先准备好
第二步 :我们来对这些文件添加到工程当中
第三步: 我们先来对main.h文件进行改造
/*************************************
** 文件名称:main.h
** 描 述:公共头文件
** 实验平台:STC12C5A60S2最小系统板 + 面包板 + 分立元器件
** 日 期:2020/01/24
** 作 者:wllis
** 历史记录:
LED->P0.0引脚
KEY->P0.2引脚
**************************************/
#ifndef __MAIN_H
#define __MAIN_H
#include
#include
/****** 变量类型定义 defines ******/
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
void delay( uint16_t time );
void Delay1ms( void );
#endif
/************* end of file **********/
编译,下载进去,OK没有问题
我这里给大家讲讲头文件包涵的内容,以及主要用来干什么
我们一般会在头文件中看到如下头文件代码样式 ,这个主要是用来防止一个源文件多次包含同一个头文件
#ifndef __XX_H
#define __XX_H
// 里面放置头文件要包涵的内容
#endif
1、头文件中主要放置源文件中可能用到的其它头文件包含,比如说我们这里用到了#include
2、头文件中会放置这种类型定义以及宏定义也是可以的,主要是在源文件中会多次使用到的宏定义和类型定义,我们暂时只了解这么多,后面我们碰到新的情况,再补充。
typedef unsignedchar uint8_t;
typedef unsignedint uint16_t;
3、函数原型定义
void delay( uint16_t time );
void Delay1ms( void );
我们的.c文件里面则主要放置函数的具体实现,以及用到的一些变量或数组,我们同样等后面遇到再具体讲一讲
有了上面的基础我们继续对其它的两个文件进行模块化改造
首先来改造KEY.c和KEY.h
KEY.c源文件代码
/*************************************
** 文件名称:KEY.c
** 描 述:按键扫描源文件
** 实验平台:STC12C5A60S2最小系统板 + 面包板 + 分立元器件
** 日 期:2020/01/23
** 作 者:wllis
** 历史记录:
KEY->P0.2引脚
**************************************/
#include "KEY.h"
/*************************************
** 函 数 名:Key_Init
** 输入参数:none
** 返 回 值:none
** 说 明:按键初始化程序
**************************************/
void Key_Init( void )
{
KEY = 1; // 按键端口置1,用于输入读取
}
/*************************************
** 函 数 名:Key_Scan
** 输入参数:none
** 返 回 值:uint8_t型按键值
** 说 明:按键扫描程序
**************************************/
uint8_t Key_Scan( void )
{
if( KEY_DOWN==KEY )
{
delay(20);
if( KEY_DOWN==KEY )
{
while(KEY_DOWN==KEY);
return 1;
}
else{ return 0; }
}
else{ return 0; }
}
/************* end of file **********/
KEY.h头文件代码
/*************************************
** 文件名称:KEY.h
** 描 述:按键处理头文件
** 实验平台:STC12C5A60S2最小系统板 + 面包板 + 分立元器件
** 日 期:2020/01/24
** 作 者:wllis
** 历史记录:
KEY->P0.2引脚
**************************************/
#ifndef __KEY_H
#define __KEY_H
#include "main.h"
// 宏定义
#define KEY_UP 1
#define KEY_DOWN 0
/******** 引脚定义 ***************/
sbit KEY = P0^2;
void Key_Init( void );
uint8_t Key_Scan( void );
#endif
/************* end of file **********/
main.c文件代码
/*************************************
** 文件名称:main.c
** 描 述:LED灯闪烁
** 实验平台:STC12C5A60S2最小系统板 + 面包板 + 分立元器件
** 日 期:2020/01/23
** 作 者:wllis
** 历史记录:
LED->P0.0引脚
KEY->P0.2引脚
**************************************/
#include "main.h"
#include "KEY.h"
// 宏定义
#define ON 0
#define OFF 1
/******** 引脚定义 ***************/
sbit LED = P0^0;
/********* 函数定义 **************/
void LED_Togle( void );
/*************************************
** 函 数 名:main
** 输入参数:none
** 返 回 值:none
** 说 明:主函数
**************************************/
void main ( void )
{
uint8_t key_value = 0;
Key_Init();
while(1)
{
key_value = Key_Scan(); // 按键扫描
if(1==key_value) // 按键按下翻转下LED灯状态
{
LED_Togle();
key_value = 0;
}
}
}
/*************************************
** 函 数 名:delay
** 输入参数:需要延时ms数,16位无符号整数
** 返 回 值:none
** 说 明:延时函数
**************************************/
void delay( uint16_t time )
{
while(--time){ Delay1ms(); }
}
/*************************************
** 函 数 名:Delay1ms
** 输入参数:none
** 返 回 值:none
** 说 明:1ms延时函数
**************************************/
void Delay1ms( void ) //@11.0592MHz
{
uint8_t i, j;
_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
/*************************************
** 函 数 名:LED_Blink
** 输入参数:none
** 返 回 值:none
** 说 明:LED翻转函数
**************************************/
void LED_Togle( void )
{
LED = ~LED;
}
/************* end of file **********/
main.h文件代码
/*************************************
** 文件名称:main.h
** 描 述:公共头文件
** 实验平台:STC12C5A60S2最小系统板 + 面包板 + 分立元器件
** 日 期:2020/01/24
** 作 者:wllis
** 历史记录:
LED->P0.0引脚
KEY->P0.2引脚
**************************************/
#ifndef __MAIN_H
#define __MAIN_H
#include
#include
/****** 变量类型定义 defines ******/
typedef unsigned char uint8_t;
typedef unsigned int uint16_t;
void delay( uint16_t time );
void Delay1ms( void );
#endif
/************* end of file **********/
编译下载进去,OK,没有问题
细心的朋友应该有发现,我们又往头文件中加入了新的东西sbit KEY = P0^2;引脚定义,我们后面使用多少个输入端口都可以在这里定义,这样可以做到跟其他模块无关,应用程序中只需要调用函数即可。
在main.h文件中放置其他源文件模块都需要用到的类型定义,公共头文件以及一些公共函数等;其他模块头文件中则主要包含跟模块相关的一些定义和函数原型,比如说按键模块头文件中则放置了按键引脚定义,按键初始化函数、按键扫描函数以及按键的一些宏变量;我们拆分模块的一些基本原则是在以后的定义修改中不能牵一发动全身,就跟我们以前学C语言讲的高内敛、低耦合一样,就是模块内部的所有东西尽量保持相对独立,确保可移植性,不能说这改个参数,其它所有的模块都要改一遍,那是完全违背我们拆解模块的一个初衷
总结:
看到这里大家可能觉得这不是脱裤子放屁,多此一举么,就为了实现这么个破玩意,写这么多东西!!如果只是为了实现一个简单的按键控制LED灯亮灭,那做这么多事情当然是不值的,这里主要是提供一种新的程序设计方式,为写大型的、复杂的程序做准备,相信很多朋友看过STM32的程序,那玩意工程一上来就是几十个xx.c,xx.h文件,很多人没有基础直接去看,感觉很费劲,但是看的懂的人又觉得那玩意写的非常好,写的非常优秀。
另外,大家看到我还有个LED.c,LED.h文件还没往里面填内容,大家可以自行补充下,有疑问可以在评论区提出来,也可以去网上寻找答案,看《嵌入式系统构件》这本书也是一个不错的选择。
这种模块化程序程序设计无论是在arduino, STM32,还是windows应用程序开发中都应用非常广泛,大家要熟练掌握,勤动手,多思考。