C语言模块化开发技巧
在前面的文章中有介绍单片机开发的几种编程思维,其中模块化整理代码是最基础的一种编程方式。点击链接可以回顾:51单片机进阶开发(一)之单片机编程思维。由于当时没有没有具体举例说明,部分初学者看完之后还是不太理解,今天这篇文章我将通过一个具体示例做一个演示。接下来在实例说明之前先介绍一些相关基础知识。
C语言中的源文件和头文件
我们刚学习C语言之时,见到的第一个C程序往往就是在窗口输出hello,world!
#include <stdio.h>
int main()
{
printf('Hello World!\n');
return 0;
}
第一次上机课老师也会告诉我们,在编译软件中新建一个工程把这段代码敲进去保存为·**.c
文件,运行,代码输入无误就会输出一个hello,world!
。
上面的**.c
就是我们所说的C语言源文件,至于#include <stdio.h>
是什么意思,老师可能会告诉我们,它是C语言标准输入输出库文件的头文件,说起来是不是很拗口,但我们至少知道了stdio.h
是一个叫头文件的文件,至于它们的作用老师也可能不会详见,你如果不去探究竟可能就一直都不知道里面有啥内容,所以编程的时候可能就一直傻乎乎的把这句copy上去了,你能想到它们有什么用吗?
本质上源文件和头文件中包含的内容都是C语言代码的源代码,既然如此为何还需要分为.c
和.h
来区分呢?实际上只要处理得恰当,你完全可以直接#include “**.c”
,不信你自己试试。至于为什么要分为.c
和.h
呢?一个原因是如前面我们说过的C语言编译器对程序的编译过程包含几个步骤,在预处理,编译、汇编,链接的一系列过程中对源代码是可以分开进行的,最后整合为我们需要的可执行文件。因为有这个特点,也正是我们对程序进行模块化的基础。学过其他语言的朋友应该都会想到,为何编程界的主流语言种只有C
和C++
有头文件呢?C#
,java
,js
,python
等模块化更加明显的语言咋就没有头文件这一说呢?这就是另外一个原因了,这里就涉及到C语言产生时的历史背景了,在那个硬件资源极其宝贵的条件下它要把程序尽量编译到最简,编译出来的目标代码(.o
,.lib
,.dll
等)中就不能带太多无关紧要的内容,这时其他人想要复用你的目标代码就得知道怎么在你目标代码中去找到需要的那一部分代码,这时就要发挥头文件的作用了。在头文件中什声明你写的东西加上你的目标代码就可以二次开发应用了。为何其他编程语言不需要提供类似的头文件,这当然不用想都能知道了吧,它们编译出来的东西包含的内容太多了,在不缺硬件的时代,who care那一丢丢空间呢,功能要多,内容要炫,那就全都给我带上,哈哈……所以现在我们一个应用程序轻轻松松上G甚至十几G呀?这有点像我们的父辈跟我们讲他们的青葱岁月时我们一脸不屑的样子。OK,扯得有点远了。
通常情况下为了提高代码的可读性和可移植性,我们都会讲代码进行模块化,把一个复杂的.c
源文件分解为多个模块化的.c
源文件。在.c
源文件中定义相应模块功能的实现代码,然后在.h
头文件中分别对相应的.c
源文件中的内容进行声明。之后在哪个.c
文件中用到了其他.c
文件的内容就在该.c
文件中#include
进去。
头文件格式
现在我们应该明白了,头文件相当于是模块的接口文件,接下来了解其中一般内容格式。
#ifndef _YOUR_HEADER_H
#define _YOUR_HEADER_H
//头文件中内容:
//宏定义
#define TEST_NUM ****
//变量声明
extern char test_var
//函数声明
(extern) void test_func(void)
#endif
前面两句#ifdef _YOUR_HEADER_H
和#define _YOUR_HEADER_H
和最后的#endif
是头文件的关键,这样处理可以防止头文件重名,即一个工程下头文件名称是唯一的,工程中一旦有使用重复命名的头文件编译时将会报错。
万年历示例程序
万年历是一个非常适合初学者练手的小项目,整体难度不大,可以很好的综合51单片机的常用功能。
项目中包含三个源文件分别是main.c
,ds1302.c
,lcd.c
。基本上把项目分成了3个部分main.c
为主要控制部分代码,ds1302.c
为时钟芯片ds1302模块功能代码,lcd.c
文件为LCD1602液晶显示模块功能代码。另外还包含ds1302.c
,lcd.c
两个模块功能的头文件ds1302.h
,lcd.h
这两个模块中都会使用到51单片机底层应用,所以都要需要#include<reg51.h>
来引用keil编译器定义的51单片机内部资源,这个文件我们最开始使用时介绍过,里面主要是一些对单片机引脚,寄存器等信息的宏定义。另外ds1302.c
模块中使用到了51单片机的逻辑运算功能,所以还要引入intrins.h
,这两个头文件都是keil官方给到我们的,平时使用时不要修改,它们是在软件安装目录中,你也可以打开文件查看内容!ds1302.h
,lcd.h
是对ds1302.c
,lcd.c
中定义的函数,变量的声明,所以在ds1302.c
,lcd.c
最开始的位置我们需要分别引入它们。main.c
文件中需要使用lcd.c
和ds1302.c
模块的功能所以在文件最前面引入这两个模块的头文件,即:#include'lcd.h'
和#include'ds1302.h'
,这样处理之后我们就可以在main.c
使用ds1302.c
,lcd.c
中定义的函数或变量而不会报错了。当然作者在main.c
还引入了reg51.h
,这里其实可以不用引入了,因为ds1302.h
,lcd.h
已经引入,当然这里重复引入一次也不会有问题。
这是一个可以学习借鉴的工程,初学者可以尝试把自己的代码进行模块化。当然代码中也还有值得优化的地方,比如main.c
中的main函数还是写得不够简洁,可以试试把按键程序分解为一个单独的模块。另外,在按下调节按键时可以添加一个指示动作,指示当前调节的是哪个数据。总之多去尝试,日后遇到更复杂的项目才会做起来得心应手。
附代码
main.c
/*******************************************************************************
* 实验名 : 万年历实验
* 使用的IO :
* 实验效果 :1602显示时钟,按K3进入时钟设置,按K1选择设置的时分秒日月,按K2选择
*选择设置加1。
* 注意 :
*******************************************************************************/
#include<reg51.h>
#include'lcd.h'
#include'ds1302.h'
sbit K1=P3^1;
sbit K2=P3^0;
sbit K3=P3^2;
sbit K4=P3^3;
void Int0Configuration();
void LcdDisplay();
unsigned char SetState,SetPlace;
void Delay10ms(void); //误差 0us
/*******************************************************************************
* 函数名 : main
* 函数功能 : 主函数
* 输入 : 无
* 输出 : 无
*******************************************************************************/
void main()
{
unsigned char i;
Int0Configuration();
LcdInit();
Ds1302Init();
while(1)
{
if(SetState==0)
{
Ds1302ReadTime();
}
else
{
if(K1==0) //检测按键K1是否按下
{
Delay10ms(); //消除抖动
if(K1==0)
{
SetPlace++;
if(SetPlace>=7)
SetPlace=0;
}
while((i<50)&&(K1==0)) //检测按键是否松开
{
Delay10ms();
i++;
}
i=0;
}
if(K2==0) //检测按键K2是否按下
{
Delay10ms(); //消除抖动
if(K2==0)
{
TIME[SetPlace]++;
if((TIME[SetPlace]&0x0f)>9) //换成BCD码。
{
TIME[SetPlace]=TIME[SetPlace]+6;
}
if((TIME[SetPlace]>=0x60)&&(SetPlace<2)) //分秒只能到59
{
TIME[SetPlace]=0;
}
if((TIME[SetPlace]>=0x24)&&(SetPlace==2)) //小时只能到23
{
TIME[SetPlace]=0;
}
if((TIME[SetPlace]>=0x32)&&(SetPlace==3)) //日只能到31
{
TIME[SetPlace]=0;
}
if((TIME[SetPlace]>=0x13)&&(SetPlace==4)) //月只能到12
{
TIME[SetPlace]=0;
}
if((TIME[SetPlace]>=0x7)&&(SetPlace==5)) //周只能到7
{
TIME[SetPlace]=1;
}
// if(SetPlace==5) //月只能到12
// {
// TIME[SetPlace]=;
// }
}
while((i<50)&&(K2==0)) //检测按键是否松开
{
Delay10ms();
i++;
}
i=0;
}
}
LcdDisplay();
}
}
/*******************************************************************************
* 函数名 : LcdDisplay()
* 函数功能 : 显示函数
* 输入 : 无
* 输出 : 无
*******************************************************************************/
void LcdDisplay()
{
LcdWriteCom(0x80+0X40);
LcdWriteData('0'+TIME[2]/16); //时
LcdWriteData('0'+(TIME[2]&0x0f));
LcdWriteData('-');
LcdWriteData('0'+TIME[1]/16); //分
LcdWriteData('0'+(TIME[1]&0x0f));
LcdWriteData('-');
LcdWriteData('0'+TIME[0]/16); //秒
LcdWriteData('0'+(TIME[0]&0x0f));
LcdWriteCom(0x80);
LcdWriteData('2');
LcdWriteData('0');
LcdWriteData('0'+TIME[6]/16); //年
LcdWriteData('0'+(TIME[6]&0x0f));
LcdWriteData('-');
LcdWriteData('0'+TIME[4]/16); //月
LcdWriteData('0'+(TIME[4]&0x0f));
LcdWriteData('-');
LcdWriteData('0'+TIME[3]/16); //日
LcdWriteData('0'+(TIME[3]&0x0f));
LcdWriteCom(0x8D);
LcdWriteData('0'+(TIME[5]&0x07)); //星期
}
/*******************************************************************************
* 函数名 : Int0Configuration()
* 函数功能 : 配置外部中断0
* 输入 : 无
* 输出 : 无
*******************************************************************************/
void Int0Configuration()
{
//设置INT0
IT0=1;//跳变沿出发方式(下降沿)
EX0=1;//打开INT0的中断允许。
EA=1;//打开总中断
}
/*******************************************************************************
* 函数名 : Int0()
* 函数功能 : 外部中断0 中断函数
* 输入 : 无
* 输出 : 无
*******************************************************************************/
void Int0() interrupt 0
{
Delay10ms();
if(K3==0)
{
SetState=~SetState;
SetPlace=0;
Ds1302Init();
}
}
/*******************************************************************************
* 函数名 : Delay10ms
* 函数功能 : 延时函数,延时10ms
* 输入 : 无
* 输出 : 无
*******************************************************************************/
void Delay10ms(void) //误差 0us
{
unsigned char a,b,c;
for(c=1; c>0; c--)
for(b=38; b>0; b--)
for(a=130; a>0; a--);
}
ds1302.c
#include'ds1302.h'
//---DS1302写入和读取时分秒的地址命令---//
//---秒分时日月周年 最低位读写位;-------//
uchar code READ_RTC_ADDR[7] = {0x81, 0x83, 0x85, 0x87, 0x89, 0x8b, 0x8d};
uchar code WRITE_RTC_ADDR[7] = {0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c};
//---DS1302时钟初始化2013年1月1日星期二12点00分00秒。---//
//---存储顺序是秒分时日月周年,存储格式是用BCD码---//
uchar TIME[7] = {0, 0, 0x12, 0x01, 0x01, 0x02, 0x13};
/*******************************************************************************
* 函 数 名 : Ds1302Write
* 函数功能 : 向DS1302命令(地址+数据)
* 输 入 : addr,dat
* 输 出 : 无
*******************************************************************************/
void Ds1302Write(uchar addr, uchar dat)
{
uchar n;
RST = 0;
_nop_();
SCLK = 0;//先将SCLK置低电平。
_nop_();
RST = 1; //然后将RST(CE)置高电平。
_nop_();
for (n=0; n<8; n++)//开始传送八位地址命令
{
DSIO = addr & 0x01;//数据从低位开始传送
addr >>= 1;
SCLK = 1;//数据在上升沿时,DS1302读取数据
_nop_();
SCLK = 0;
_nop_();
}
for (n=0; n<8; n++)//写入8位数据
{
DSIO = dat & 0x01;
dat >>= 1;
SCLK = 1;//数据在上升沿时,DS1302读取数据
_nop_();
SCLK = 0;
_nop_();
}
RST = 0;//传送数据结束
_nop_();
}
/*******************************************************************************
* 函 数 名 : Ds1302Read
* 函数功能 : 读取一个地址的数据
* 输 入 : addr
* 输 出 : dat
*******************************************************************************/
uchar Ds1302Read(uchar addr)
{
uchar n,dat,dat1;
RST = 0;
_nop_();
SCLK = 0;//先将SCLK置低电平。
_nop_();
RST = 1;//然后将RST(CE)置高电平。
_nop_();
for(n=0; n<8; n++)//开始传送八位地址命令
{
DSIO = addr & 0x01;//数据从低位开始传送
addr >>= 1;
SCLK = 1;//数据在上升沿时,DS1302读取数据
_nop_();
SCLK = 0;//DS1302下降沿时,放置数据
_nop_();
}
_nop_();
for(n=0; n<8; n++)//读取8位数据
{
dat1 = DSIO;//从最低位开始接收
dat = (dat>>1) | (dat1<<7);
SCLK = 1;
_nop_();
SCLK = 0;//DS1302下降沿时,放置数据
_nop_();
}
RST = 0;
_nop_(); //以下为DS1302复位的稳定时间,必须的。
SCLK = 1;
_nop_();
DSIO = 0;
_nop_();
DSIO = 1;
_nop_();
return dat;
}
/*******************************************************************************
* 函 数 名 : Ds1302Init
* 函数功能 : 初始化DS1302.
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void Ds1302Init()
{
uchar n;
Ds1302Write(0x8E,0X00); //禁止写保护,就是关闭写保护功能
for (n=0; n<7; n++)//写入7个字节的时钟信号:分秒时日月周年
{
Ds1302Write(WRITE_RTC_ADDR[n],TIME[n]);
}
Ds1302Write(0x8E,0x80); //打开写保护功能
}
/*******************************************************************************
* 函 数 名 : Ds1302ReadTime
* 函数功能 : 读取时钟信息
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
void Ds1302ReadTime()
{
uchar n;
for (n=0; n<7; n++)//读取7个字节的时钟信号:分秒时日月周年
{
TIME[n] = Ds1302Read(READ_RTC_ADDR[n]);
}
}
lcd.c
#include'lcd.h'
/*******************************************************************************
* 函 数 名 : Lcd1602_Delay1ms
* 函数功能 : 延时函数,延时1ms
* 输 入 : c
* 输 出 : 无
* 说 名 : 该函数是在12MHZ晶振下,12分频单片机的延时。
*******************************************************************************/
void Lcd1602_Delay1ms(uint c) //误差 0us
{
uchar a,b;
for (; c>0; c--)
{
for (b=199; b>0; b--)
{
for(a=1; a>0; a--);
}
}
}
/*******************************************************************************
* 函 数 名 : LcdWriteCom
* 函数功能 : 向LCD写入一个字节的命令
* 输 入 : com
* 输 出 : 无
*******************************************************************************/
#ifndef LCD1602_4PINS //当没有定义这个LCD1602_4PINS时
void LcdWriteCom(uchar com) //写入命令
{
LCD1602_E = 0; //使能
LCD1602_RS = 0; //选择发送命令
LCD1602_RW = 0; //选择写入
LCD1602_DATAPINS = com; //放入命令
Lcd1602_Delay1ms(1); //等待数据稳定
LCD1602_E = 1; //写入时序
Lcd1602_Delay1ms(5); //保持时间
LCD1602_E = 0;
}
#else
void LcdWriteCom(uchar com) //写入命令
{
LCD1602_E = 0; //使能清零
LCD1602_RS = 0; //选择写入命令
LCD1602_RW = 0; //选择写入
LCD1602_DATAPINS = com; //由于4位的接线是接到P0口的高四位,所以传送高四位不用改
Lcd1602_Delay1ms(1);
LCD1602_E = 1; //写入时序
Lcd1602_Delay1ms(5);
LCD1602_E = 0;
// Lcd1602_Delay1ms(1);
LCD1602_DATAPINS = com << 4; //发送低四位
Lcd1602_Delay1ms(1);
LCD1602_E = 1; //写入时序
Lcd1602_Delay1ms(5);
LCD1602_E = 0;
}
#endif
/*******************************************************************************
* 函 数 名 : LcdWriteData
* 函数功能 : 向LCD写入一个字节的数据
* 输 入 : dat
* 输 出 : 无
*******************************************************************************/
#ifndef LCD1602_4PINS
void LcdWriteData(uchar dat) //写入数据
{
LCD1602_E = 0; //使能清零
LCD1602_RS = 1; //选择输入数据
LCD1602_RW = 0; //选择写入
LCD1602_DATAPINS = dat; //写入数据
Lcd1602_Delay1ms(1);
LCD1602_E = 1; //写入时序
Lcd1602_Delay1ms(5); //保持时间
LCD1602_E = 0;
}
#else
void LcdWriteData(uchar dat) //写入数据
{
LCD1602_E = 0; //使能清零
LCD1602_RS = 1; //选择写入数据
LCD1602_RW = 0; //选择写入
LCD1602_DATAPINS = dat; //由于4位的接线是接到P0口的高四位,所以传送高四位不用改
Lcd1602_Delay1ms(1);
LCD1602_E = 1; //写入时序
Lcd1602_Delay1ms(5);
LCD1602_E = 0;
LCD1602_DATAPINS = dat << 4; //写入低四位
Lcd1602_Delay1ms(1);
LCD1602_E = 1; //写入时序
Lcd1602_Delay1ms(5);
LCD1602_E = 0;
}
#endif
/*******************************************************************************
* 函 数 名 : LcdInit()
* 函数功能 : 初始化LCD屏
* 输 入 : 无
* 输 出 : 无
*******************************************************************************/
#ifndef LCD1602_4PINS
void LcdInit() //LCD初始化子程序
{
LcdWriteCom(0x38); //开显示
LcdWriteCom(0x0c); //开显示不显示光标
LcdWriteCom(0x06); //写一个指针加1
LcdWriteCom(0x01); //清屏
LcdWriteCom(0x80); //设置数据指针起点
}
#else
void LcdInit() //LCD初始化子程序
{
LcdWriteCom(0x32); //将8位总线转为4位总线
LcdWriteCom(0x28); //在四位线下的初始化
LcdWriteCom(0x0c); //开显示不显示光标
LcdWriteCom(0x06); //写一个指针加1
LcdWriteCom(0x01); //清屏
LcdWriteCom(0x80); //设置数据指针起点
}
#endif
ds1302.h
#ifndef __LCD_H_
#define __LCD_H_
/**********************************
当使用的是4位数据传输的时候定义,
使用8位取消这个定义
**********************************/
//#define LCD1602_4PINS
/**********************************
包含头文件
**********************************/
#include<reg51.h>
//---重定义关键词---//
#ifndef uchar
#define uchar unsigned char
#endif
#ifndef uint
#define uint unsigned int
#endif
/**********************************
PIN口定义
**********************************/
#define LCD1602_DATAPINS P0
sbit LCD1602_E=P2^7;
sbit LCD1602_RW=P2^5;
sbit LCD1602_RS=P2^6;
/**********************************
函数声明
**********************************/
/*在51单片机12MHZ时钟下的延时函数*/
void Lcd1602_Delay1ms(uint c); //误差 0us
/*LCD1602写入8位命令子函数*/
void LcdWriteCom(uchar com);
/*LCD1602写入8位数据子函数*/
void LcdWriteData(uchar dat) ;
/*LCD1602初始化子程序*/
void LcdInit();
#endif
lcd.h
#ifndef __DS1302_H_
#define __DS1302_H_
//---包含头文件---//
#include<reg51.h>
#include<intrins.h>
//---重定义关键词---//
#ifndef uchar
#define uchar unsigned char
#endif
#ifndef uint
#define uint unsigned int
#endif
//---定义ds1302使用的IO口---//
sbit DSIO=P3^4;
sbit RST=P3^5;
sbit SCLK=P3^6;
//---定义全局函数---//
void Ds1302Write(uchar addr, uchar dat);
uchar Ds1302Read(uchar addr);
void Ds1302Init();
void Ds1302ReadTime();
//---加入全局变量--//
extern uchar TIME[7]; //加入全局变量
#endif