hww5408
发表于 2014-9-16 08:09
mark
jianhong_wu
发表于 2014-9-17 14:21
第六十八节:单片机C语言的多文件编程技巧。 开场白:很多人也把多文件编程称作模块化编程,其实我觉得叫多文件编程会更加符合实际一些。多文件编程有两个最大的好处,一个是给我们的程序增加了目录,方便我们查找。另外一个好处是方便移植别人已经做好的功能程序模块,利用这个特点,特别适合团队一起做大型项目。很多初学者刚开始学多文件编程时,会经常遇到重复定义等问题,想知道怎么解决这些问题吗?只要按照以下鸿哥教的规则来做,这些问题就不存在了。第一个:每个文件保持成双成对出现。每个.c源文件必须有一个.h头文件跟它对应,每个.h头文件必须有一个.c源文件跟它对应。比如:main.c与main.h,delay.c与 delay.h。 第二个:.c源文件只负责函数的定义和变量的定义,但是不负责函数的声明和变量的声明。比如:unsigned char ucLedStep=0; //这个是全局变量的定义void led_flicker() //这个是函数的定义{ //…里面是具体代码内容} 第三个:.h头文件只负责函数的声明和变量的声明,以及常量和IO口的宏定义,但是不负责函数的定义和变量的定义。比如:#define const_time_level 200//这个是常量的宏定义sbit led_dr=P3^5; //这个是IO口的宏定义 void led_flicker(); //这个是函数的声明extern unsigned char ucLedStep; //这个是全局变量的声明,不能赋初始值 第四个:每个.h头文件都必须固定以#ifndef,#define,#endif语句为模板,此模板是用来避免编译时由于重复包含头文件里面的内容而导致出错。其中标志变量_XXX_鸿哥建议用它本身的文件名称加前后下划线_。比如:#ifndef _LED_ //标志变量_LED_是用它本身的文件名称命名#define _LED_ //标志变量_LED_是用它本身的文件名称命名 #define const_time_level 200//这个是常量的宏定义sbit led_dr=P3^5; //这个是IO口的宏定义 void led_flicker(); //这个是函数的声明extern unsigned char ucLedStep; //这个是全局变量的声明,不能赋初始值#endif 第五个:每个.h头文件里都必须声明它对应的.c源文件里的所有定义函数和全局变量,注意:.c源文件里所有的全局变量都要在它所对应的.h头文件里声明一次,不仅仅是函数,这个地方很容易被人忽略。比如:在led.h头文件中:void led_flicker(); //这个是函数的声明,因为在这个函数在led.c文件里定义了。 extern unsigned char ucLedStep; //这个是全局变量的声明,不许赋初值 第六个:每个.c源文件里都必须包含两个文件,一个是单片机的系统头文件REG52.H,另外一个是它自己本身的头文件比如initial.h.剩下其它的头文件看实际情况来决定是否调用,我们用到了哪些文件的函数,全局变量或者宏定义,就需要调用对应的头文件。比如:在initial.c源文件中:#include"REG52.H"//必须包含的单片机系统头文件#include"initial.h"//必须包含它本身的头文件/* 注释: 由于本源文件中用到了led_dr的语句,而led_dr是在led.h文件里宏定义的,所以必须把led.h也包含进来*/#include"led.h"//由于本源文件中用到了led_dr的语句,所以必须把led.h也包含进来void initial_myself()//这个是函数定义{led_dr=0;//led_dr是在led文件里定义和声明的} 第七个:声明一个全局变量必须加extern关键字,同时千万不能在声明全局变量的时候赋初始值,比如:extern unsigned char ucLedStep=0; //这样是绝对错误的。extern unsigned char ucLedStep; //这个是全局变量的声明,这个才是正确的 第八个:对于函数与全局变量的声明,编译器都不分配内存空间。对于函数与全局变量的定义,编译器都分配内存空间。函数与全局变量的定义只能在一个.c源文件中出现一次,而函数与全局变量的声明可以在多个.h文件中出现。
具体内容,请看源代码讲解,本程序例程是直接把前面第四节一个源文件更改成多文件编程方式。
(1)硬件平台:
基于朱兆祺51单片机学习板。把前面第四节一个源文件更改成多文件编程方式。
(2)实现功能:跟前面第四节的功能一模一样,让一个LED闪烁。
(3)keil多文件编程的截图预览: (4)整个源代码讲解工程文件下载:
总结陈词: 下一节开始讲液晶屏显示方面的内容。欲知详情,请听下回分解----带字库12864液晶屏的常用点阵字体程序。 (未完待续,下节更精彩,不要走开哦)
ILOVE电子
发表于 2014-9-17 16:52
顶一个再看
lvyunhua
发表于 2014-9-17 22:12
学习了,楼主辛苦了。
jianhong_wu
发表于 2014-9-18 12:59
第六十九节:使用static关键字可以减少全局变量的使用。
开场白:
本来这一节打算开始讲液晶屏的,但是昨天经过网友“任军”的点拨,我发现了一个惊天秘密,原来static关键字是这么好的东西我却错过了那么多年。以前就有一些网友抱怨,鸿哥的程序好是好,就是全局变量满天飞,当时我觉得我也没招呀,C语言就全局变量和局部变量,单单靠局部变量肯定不行,局部变量每次进入函数内部数值都会被初始化改变,所以我在很多场合也只能靠全局变量了。但是自从昨天知道了static关键字的秘密后,我恍然大悟,很多场合只要在局部变量前面加上static关键字,就可以大大减少全局变量的使用了。
这一节要教会大家一个知识点:
大家都知道,普通的局部变量在每次程序执行到函数内部的时候,数值都会被重新初始化,数值会发生变化,不能保持之前的数值。但是在局部变量加上static关键字后,系统在刚上电的时候就已经把带static的局部变量赋初始值了,从此程序每次进入函数内部,都不会初始化带static关键字的局部变量,它会保持最近一次被程序执行更改的数值不变,像全局变量一样。跟全局变量唯一的差别是,带static关键字的局部变量的作用域仅仅在函数内部,而普通全局变量的作用域是整个工程。
本程序例程是直接在第八节程序上修改,大大减少了全局变量的使用。具体内容,请看源代码讲解。
(1)硬件平台:
基于朱兆祺51单片机学习板。用矩阵键盘中的S1和S5号键作为独立按键,记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。
(2)实现功能:跟前面第八节的功能一模一样,有两个独立按键,每按一个独立按键,蜂鸣器发出“滴”的一声后就停。
(3)源代码讲解如下:#include "REG52.H"
#define const_voice_short40
#define const_key_time120
#define const_key_time220
void initial_myself();
void initial_peripheral();
void delay_long(unsigned int uiDelaylong);
void T0_time();
void key_service();
void key_scan();
sbit key_sr1=P0^0;
sbit key_sr2=P0^1;
sbit key_gnd_dr=P0^4;
sbit beep_dr=P2^7;
unsigned char ucKeySec=0; //一些需要在不同函数之间使用的核心变量,只能用全局变量
unsigned intuiVoiceCnt=0;//一些需要在不同函数之间使用的核心变量,只能用全局变量
void main()
{
initial_myself();
delay_long(100);
initial_peripheral();
while(1)
{
key_service();
}
}
void key_scan()
{
/* 注释一:
(1)大家都知道,普通的局部变量在每次程序执行到函数内部的时候,数值都会被重新初始化,
数值会发生变化,不能保持之前的数值。
(2)但是在局部变量加上static关键字后,系统在刚上电的时候就已经把带static的局部变量
赋初始值了,从此程序每次进入函数内部,都不会初始化带static关键字的局部变量,它会保持
最近一次被程序执行更改的数值不变,像全局变量一样。跟全局变量唯一的差别是,带static关键字
的局部变量的作用域仅仅在函数内部,而普通全局变量的作用域是整个工程。
(3)以下这些变量我原来在第八节是用普通全局变量的,现在改成用static的局部变量了,减少了全局变量
的使用,让程序阅读起来更加简洁。大家也可以试试把以下变量的static去掉试试,结果会发现去掉了static后,
按键就不会被触发了。
*/
static unsigned intuiKeyTimeCnt1=0; //带static的局部变量
static unsigned char ucKeyLock1=0;
static unsigned intuiKeyTimeCnt2=0;
static unsigned char ucKeyLock2=0;
if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
{
ucKeyLock1=0; //按键自锁标志清零
uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。
}
else if(ucKeyLock1==0)//有按键按下,且是第一次被按下
{
uiKeyTimeCnt1++; //累加定时中断次数
if(uiKeyTimeCnt1>const_key_time1)
{
uiKeyTimeCnt1=0;
ucKeyLock1=1;//自锁按键置位,避免一直触发
ucKeySec=1; //触发1号键
}
}
if(key_sr2==1)
{
ucKeyLock2=0;
uiKeyTimeCnt2=0;
}
else if(ucKeyLock2==0)
{
uiKeyTimeCnt2++;
if(uiKeyTimeCnt2>const_key_time2)
{
uiKeyTimeCnt2=0;
ucKeyLock2=1;
ucKeySec=2;
}
}
}
void key_service()
{
switch(ucKeySec)
{
case 1:
uiVoiceCnt=const_voice_short;
ucKeySec=0;
break;
case 2:
uiVoiceCnt=const_voice_short;
ucKeySec=0;
break;
}
}
void T0_time() interrupt 1
{
TF0=0;
TR0=0;
key_scan();
if(uiVoiceCnt!=0)
{
uiVoiceCnt--;
beep_dr=0;
}
else
{
;
beep_dr=1;
}
TH0=0xf8;
TL0=0x2f;
TR0=1;
}
void delay_long(unsigned int uiDelayLong)
{
unsigned int i;
unsigned int j;
for(i=0;i<uiDelayLong;i++)
{
for(j=0;j<500;j++)
{
;
}
}
}
void initial_myself()
{
key_gnd_dr=0;
beep_dr=1;
TMOD=0x01;
TH0=0xf8;
TL0=0x2f;
}
void initial_peripheral()
{
EA=1;
ET0=1;
TR0=1;
}总结陈词:
下一节开始讲液晶屏显示方面的内容。欲知详情,请听下回分解----带字库12864液晶屏的常用点阵字体程序。
(未完待续,下节更精彩,不要走开哦)
hww5408
发表于 2014-9-18 14:58
鸿哥,你讲得好基础,学生受益非浅,俺再实充一下,可以吗?:lol
static还有一个作用,在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。 这对模块化编程有很大的用处。
jianhong_wu
发表于 2014-9-18 15:30
本帖最后由 jianhong_wu 于 2014-9-18 15:33 编辑
hww5408 发表于 2014-9-18 14:58 static/image/common/back.gif
鸿哥,你讲得好基础,学生受益非浅,俺再实充一下,可以吗?
static还有一个作用,在模块内(但在 ...
是的。这个功能昨天网友“任军”也告诉我了,用在多文件编程时,可以避免命名冲突等问题。但是相比之下,我觉得在单片机领域,用在函数内的static更加有使用价值,毕竟大部分的单片机程序往往都是一个人完成的,多人合作搞一个单片机软件的机会并不多。
hww5408
发表于 2014-9-18 16:01
赞同鸿哥观点。
我公司就我一个码农,因为历史原因,很多项目由不同工程师做的,已经离职了,MCU用了好几种,比如:PIC、中颖、合泰、仪隆、松翰、AVR,源程序有的用汇编,有的用C,有的连源程序也没有,只有烧录码,还要用以的MCU写程序,空间也只有1K,有时C写不完还要考虑用汇编,维护起来那个累呀!
a948523778
发表于 2014-9-19 12:45
买个51单片机开发板例程带指导说明都有的。。
alphal
发表于 2014-9-19 21:26
很好的总结
hyseyic
发表于 2014-9-22 16:52
吴老师,推荐一款51单片机开发板吧。
jay510132012
发表于 2014-9-22 17:18
MARK
jianhong_wu
发表于 2014-9-22 22:49
hyseyic 发表于 2014-9-22 16:52 static/image/common/back.gif
吴老师,推荐一款51单片机开发板吧。
我推荐朱兆祺的51学习板,你可以在淘宝上搜索到。
hyseyic
发表于 2014-9-23 17:17
jianhong_wu 发表于 2014-9-22 22:49 static/image/common/back.gif
我推荐朱兆祺的51学习板,你可以在淘宝上搜索到。
好的。谢谢。
hyseyic
发表于 2014-9-23 17:22
吴老师,深圳学林电子的51开发板咋样啊?
jianhong_wu
发表于 2014-9-23 23:11
hyseyic 发表于 2014-9-23 17:22 static/image/common/back.gif
吴老师,深圳学林电子的51开发板咋样啊?
我跟朱兆祺本人合伙成立了一个公司,朱兆祺的51学习板是我们公司做的。其它同行的产品我不方便评价,望谅解。
jlgcumt
发表于 2014-9-23 23:39
jianhong_wu 发表于 2014-9-23 23:11 static/image/common/back.gif
我跟朱兆祺本人合伙成立了一个公司,朱兆祺的51学习板是我们公司做的。其它同行的产品我不方便评价,望谅 ...
讲的都比较基础,有没有高大上的东西呀?都是按键,数码管,这些在**中的东西!
比方说 软件的组织架构,类似于编程大全之类的
硬件的可靠性设计,自检功能,比方说一个FLASH的自检什么的,电源的可靠性设计什么的的
还有就是EMC什么的,这些才是有价值的东西
zhangyudong
发表于 2014-9-24 10:28
感谢鸿哥多的不说感激涕零
jianhong_wu
发表于 2014-9-24 14:25
本帖最后由 jianhong_wu 于 2014-9-24 14:27 编辑
jlgcumt 发表于 2014-9-23 23:39 static/image/common/back.gif
讲的都比较基础,有没有高大上的东西呀?都是按键,数码管,这些在**中的东西!
比方说 软件的组织架 ...
感谢你的建议。我现在讲的确实是很基础的东西,同时也是代表了我本人最高水平的东西,主要针对初学者入门,可能我水平有限,望各位牛人前辈谅解。
jianhong_wu
发表于 2014-9-24 14:29
第七十节:深入讲解液晶屏的构字过程。
开场白:
液晶屏模块本身带控制芯片,驱动液晶屏的本质就是单片机通过串行或者并行方式,根据芯片资料指定的协议跟液晶芯片进行通讯的过程。这个详细的通讯协议驱动程序厂家都会免费提供的,也可以在网上找到大量的示范程序。那么我们最应该关注的核心是什么?我认为最核心的是要理清楚程序坐标与实际显示坐标之间的关系规律。本程序不使用模块自带的字库,而是使用自己构造的字库,目的就是为了让读者理解更底层的字模显示。
这一节要教会大家三个知识点:
第一个:对于驱动芯片是st7920的12864液晶屏,它的真实坐标体系的本质是256x32的点阵液晶屏。
第二个:鸿哥刻意在驱动显示函数里增加了大延时函数,目的是通过慢镜头回放,让大家观察到横向取模的字是如何一个字节一个字节构建而成的。
第三个:数组带const关键字,表示数据常量存放在ROM程序区,不占用RAM的变量。
具体内容,请看源代码讲解。
(1)硬件平台:
基于朱兆祺51单片机学习板。
(2)实现功能:开机上电后,可以观察到0x01,0x02,0x03,0x04这4个显示数字在不同的排列方式下,出现在不同的液晶屏显示位置。也可以观察到“馒头”这两个字是如何一个字节一个字节构建而成的,加深理解字模数组跟显示现象的关系。
(3)源代码讲解如下:#include "REG52.H"
sbitLCDCS_dr= P1^6;//片选线
sbitLCDSID_dr = P1^7;//串行数据线
sbitLCDCLK_dr = P3^2;//串行时钟线
sbitLCDRST_dr = P3^4;//复位线
void SendByteToLcd(unsigned char ucData);//发送一个字节数据到液晶模块
void SPIWrite(unsigned char ucWData, unsigned char ucWRS); //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
void WriteCommand(unsigned char ucCommand); //发送一个字节的命令给液晶模块
void LCDWriteData(unsigned char ucData); //发送一个字节的数据给液晶模块
void LCDInit(void);//初始化函数内部包括液晶模块的复位
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount); //显示任意点阵函数
void display_clear(void); // 清屏
void delay_short(unsigned int uiDelayshort); //延时
/* 注释一:
* 数组带const关键字,表示数据常量存放在ROM程序区,不占用RAM的变量
*/
const unsigned char Hz1616_man[]= /*馒 横向取模16X16点阵 网上有很多免费的字模软件生成字模数组 */
{
0x21,0xF8,0x21,0x08,0x21,0xF8,0x3D,0x08,0x45,0xF8,0x48,0x00,0x83,0xFC,0x22,0x94,
0x23,0xFC,0x20,0x00,0x21,0xF8,0x20,0x90,0x28,0x60,0x30,0x90,0x23,0x0E,0x00,0x00,
};
const unsigned char Hz1616_tou[]= /*头 横向取模16X16点阵 网上有很多免费的字模软件生成字模数组 */
{
0x00,0x80,0x10,0x80,0x0C,0x80,0x04,0x80,0x10,0x80,0x0C,0x80,0x08,0x80,0x00,0x80,
0xFF,0xFE,0x00,0x80,0x01,0x40,0x02,0x20,0x04,0x30,0x08,0x18,0x10,0x0C,0x20,0x08,
};
/* 注释二:
* 为了方便观察字模的数字与显示的关系,以下3个数组的本质是完全一样的,只是排列不一样而已。
*/
const unsigned char Byte_1[]=//4横,1列
{
0x01,0x02,0x03,0x04,
};
const unsigned char Byte_2[]= //2横,2列
{
0x01,0x02,
0x03,0x04,
};
const unsigned char Byte_3[]= //1横,4列
{
0x01,
0x02,
0x03,
0x04,
};
void main()
{
LCDInit(); //初始化12864 内部包含液晶模块的复位
display_clear(); // 清屏
display_lattice(0,0,Byte_1,0,4,1); //显示<4横,1列>的数组数字
display_lattice(0,16,Byte_1,1,4,1); //显示<4横,1列>的数组数字 反显
display_lattice(7,0,Byte_2,0,2,2); //显示<2横,2列>的数组数字
display_lattice(7,16,Byte_2,1,2,2);//显示<2横,2列>的数组数字 反显
display_lattice(8,0,Byte_3,0,1,4);//显示<1横,4列>的数组数字
display_lattice(8,16,Byte_3,1,1,4); //显示<1横,4列>的数组数字 反显
display_lattice(14,0,Hz1616_man,0,2,16);//显示<馒>字
display_lattice(15,0,Hz1616_tou,0,2,16);//显示<头>字
display_lattice(14,16,Hz1616_man,1,2,16); //显示<馒>字 反显
display_lattice(15,16,Hz1616_tou,1,2,16); //显示<头>字 反显
while(1)
{
;
}
}
/* 注释三:真实坐标体系的本质。
* 从坐标体系的角度来看,本液晶屏表面上是128x64的液晶屏,实际上可以看做是256x32的液晶屏。
* 把256x32的液晶屏分左右两半,把左半屏128x32放在上面,把右半屏128x32放下面,就合并成了
* 一个128x64的液晶屏。由于液晶模块内部控制器的原因,虽然横向有256个点阵,但是我们的x轴
* 坐标没办法精确到每个点,只能以16个点(2个字节)为一个单位,因此256个点的x轴坐标范围是0至15。
* 而y轴的坐标可以精确到每个点为一行,所以32个点的y轴坐标范围是0至31.
*/
void display_clear(void) // 清屏
{
unsigned char x,y;
//WriteCommand(0x34);//关显示缓冲指令
WriteCommand(0x36); //这次为了观察每个数字在显示屏上的关系,所以把这个显示缓冲的命令提前打开,下一节放到本函数最后
y=0;
while(y<32)//y轴的范围0至31
{
WriteCommand(y+0x80); //垂直地址
WriteCommand(0x80); //水平地址
for(x=0;x<32;x++)//256个横向点,有32个字节
{
LCDWriteData(0x00);
}
y++;
}
}
/* 注释四:本节的核心函数,读者尤其要搞懂x_amount和y_amount对应的显示关系。
* 第1,2个参数x,y是坐标体系。x的范围是0至15,y的范围是0至31.
* 第3个参数*ucArray是字模的数组。
* 第4个参数ucFbFlag是反白显示标志。0代表正常显示,1代表反白显示。
* 第5,6个参数x_amount,y_amount分别代表字模数组的横向有多少个字节,纵向有几横。
* 本函数后面故意增加一个长延时delay_short(30000),是为了方便读者观察横向取模的
* 字是如何一个字节一个字节构建而成的。
*/
void display_lattice(unsigned int x,unsigned int y,const unsigned char*ucArray,unsigned char ucFbFlag,unsigned int x_amount,unsigned int y_amount)
{
unsigned int j=0;
unsigned int i=0;
unsigned char ucTemp;
//WriteCommand(0x34); //关显示缓冲指令
WriteCommand(0x36);//这次为了观察每个数字在显示屏上的关系,所以把这个显示缓冲的命令提前打开,下一节放到本函数最后
for(j=0;j<y_amount;j++) //y_amount代表y轴有多少横
{
WriteCommand(y+j+0x80); //垂直地址
WriteCommand(x+0x80); //水平地址
for(i=0;i<x_amount;i++) //x_amount代表x轴有多少列
{
ucTemp=ucArray;
if(ucFbFlag==1)//反白显示
{
ucTemp=~ucTemp;
}
LCDWriteData(ucTemp);
delay_short(30000);//本函数故意增加这个长延时,是为了方便读者观察横向取模的字是如何一个字节一个字节构建而成的。
}
}
}
/* 注释五:
* 以下是液晶屏模块的驱动程序,我觉得没有什么好讲的,因为我是直接在网上寻找现成的驱动时序修改而成。
* 它的本质就是单片机跟这个液晶模块芯片进行串行通信。
*/
void SendByteToLcd(unsigned char ucData)//发送一个字节数据到液晶模块
{
unsigned char i;
for ( i = 0; i < 8; i++ )
{
if ( (ucData << i) & 0x80 )
{
LCDSID_dr = 1;
}
else
{
LCDSID_dr = 0;
}
LCDCLK_dr = 0;
LCDCLK_dr = 1;
}
}
void SPIWrite(unsigned char ucWData, unsigned char ucWRS) //模拟SPI发送一个字节的命令或者数据给液晶模块的底层驱动
{
SendByteToLcd( 0xf8 + (ucWRS << 1) );
SendByteToLcd( ucWData & 0xf0 );
SendByteToLcd( (ucWData << 4) & 0xf0);
}
void WriteCommand(unsigned char ucCommand) //发送一个字节的命令给液晶模块
{
LCDCS_dr = 0;
LCDCS_dr = 1;
SPIWrite(ucCommand, 0);
delay_short(90);
}
void LCDWriteData(unsigned char ucData)//发送一个字节的数据给液晶模块
{
LCDCS_dr = 0;
LCDCS_dr = 1;
SPIWrite(ucData, 1);
}
void LCDInit(void) //初始化函数内部包括液晶模块的复位
{
LCDRST_dr = 1;//复位
LCDRST_dr = 0;
LCDRST_dr = 1;
}
void delay_short(unsigned int uiDelayShort) //延时函数
{
unsigned int i;
for(i=0;i<uiDelayShort;i++)
{
;
}
}
总结陈词:
这节重点讲了液晶屏的构字过程,下节将会在本节的基础上,略作修改,显示常用的不同点阵字模。欲知详情,请听下回分解-----液晶屏的字符,16点阵,24点阵和32点阵的显示程序。
(未完待续,下节更精彩,不要走开哦)