打印
[51单片机]

从业将近十年!手把手教你单片机程序框架(连载)

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
561
jianhong_wu 发表于 2014-10-25 13:27
我06年毕业的时候英语四级对毕业生找工作是很重要的,那时我大二的时候英语就过了四级。对于搞电子行业的 ...

我就没过四级,但不妨碍我看英文资料。其实看资料最重要的不就是看数据表,看时序图,看波形图吗?
至于其它的,遇到不懂的单词,查一下字典,有些单词经常遇到,自然也就认识了,一句英文,单词都认识了,管它什么时态,大致意思总是差不了的。
要我说,过了四级的,个个都能跟老外直接交流吗?我看不见得。
我用的软件,都是英文版的,我不喜欢用中文版或汉化版的,感觉有些翻译不到位,还不如原版的。倒是看到很多过了四级的大学生,用的软件都喜欢用中文版的,真搞不懂。

使用特权

评论回复
562
骄傲的番茄| | 2014-10-26 00:40 | 只看该作者
前不久浏览这个网站时无意见看到这个帖子的,一直收藏着,只要一有时间就看一点,一开始看的时候觉得觉得楼主前面的定义好多觉得这样写好吗?不过看的多了也渐渐发觉这样写的好处了(我也是刚学习单片机没多久,因为兴趣所以一直在**着,发现你写程序的思路和框架对像我这种 刚入门的很有帮助),我想问一下这个帖子结束了吗?(非常感谢楼主的帮助:handshake)

使用特权

评论回复
563
jianhong_wu|  楼主 | 2014-10-26 09:26 | 只看该作者
cjseng 发表于 2014-10-25 22:31
我就没过四级,但不妨碍我看英文资料。其实看资料最重要的不就是看数据表,看时序图,看波形图吗?
至于 ...

我很认同你的观点。只要能看懂技术资料就可以了,不一定非要过英语4级。

使用特权

评论回复
564
jianhong_wu|  楼主 | 2014-10-26 09:33 | 只看该作者
骄傲的番茄 发表于 2014-10-26 00:40
前不久浏览这个网站时无意见看到这个帖子的,一直收藏着,只要一有时间就看一点,一开始看的时候觉得觉得楼 ...

这个帖子远远还没有结束,我还有大量的内容没来得及分享,应该还能写两年左右吧。不过,对于初学者,我建议看完我前面80节内容就差不多了。80节以后我分享的主要是某个具体外围采集芯片的驱动源代码,方便同行做到类似项目时用来参考,减少工作量。另外,我准备下个月开始会新开一个技术贴,是讲零基础的,具体到keil软件的安装和应用,C语言的语法等基础内容。

使用特权

评论回复
评分
参与人数 1威望 +1 收起 理由
lcz735822869 + 1 牛人一个而且还可以无私奉献
565
jianhong_wu|  楼主 | 2014-10-26 09:37 | 只看该作者
第七十七节:在1个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

开场白:
    这一节要教会大家两个知识点:
第一个知识点:我在前面讲数码管显示的时候就提出了一个 “一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候才需要更新的,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要刷新显示的内容,这种内容放在局部更新显示的括号里。
第二个知识点:按键如何跟液晶屏显示有机的结合起来?只要遵循鸿哥总结出来的一个规律“在不同的窗口下,根据不同的局部变量来操作不同的参数”,这样再复杂的人机交互程序都会显得很简单清晰。

具体内容,请看源代码讲解。

(1)硬件平台:基于朱兆祺51单片机学习板。加按键对应S1键,减按键对应S5键,切换“光标”移动按键对应S9键,设置参数按键对应S13键。

(2)实现功能:
     通过按键设置4个不同的参数。
    有1个窗口。每个窗口显示4个参数。每个参数的范围是从0到99。
   有4个按键:
(a)        一个是设置参数S13按键,按下此按键,液晶屏的第一行会出现反显的光标,表示进入设置参数模式,再次按下此按键,反显光标会消失,表示退出设置参数模式。
(b)        一个是移动光标S9按键,在进入设置参数的模式下,依次按下此按键,液晶屏上的光标会从上往下移动,表示选中不同的参数。
(c)        一个是减数S5按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐减小。
(d)        一个是加数S1按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐加大。

(3)源代码讲解如下:
第七十七节源代码讲解.rar (6.42 KB)
总结陈词:
这一节讲了在一个窗口里设置不同的参数,如果有几个窗口的情况下,该如何编程?欲知详情,请听下回分解-----在多个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

(未完待续,下节更精彩,不要走开哦)

使用特权

评论回复
566
duanduanr| | 2014-10-27 15:42 | 只看该作者
好贴就要留下爪印。

使用特权

评论回复
567
jianhong_wu|  楼主 | 2014-10-28 14:42 | 只看该作者
第七十八节:在多个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。

开场白:
上一节讲了1个窗口下如何设置参数的菜单程序,这一节多增加1个窗口变成2个窗口,看看它们两个窗口之间是如何通过按键程序进行切换的。继续巩固上一节教给大家的两个知识点:
   第一个知识点:我在前面讲数码管显示的时候就提出了一个 “一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候才需要更新的,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要刷新显示的内容,这种内容放在局部更新显示的括号里。
    第二个知识点:按键如何跟液晶屏显示有机的结合起来?只要遵循鸿哥总结出来的一个规律“在不同的窗口下,根据不同的局部变量来操作不同的参数”,这样再复杂的人机交互程序都会显得很简单清晰。

具体内容,请看源代码讲解。

(1)硬件平台:基于朱兆祺51单片机学习板。加按键对应S1键,减按键对应S5键,切换“光标”移动按键对应S9键,设置参数按键对应S13键。

(2)实现功能:
     通过按键设置8个不同的参数。
    有2个窗口。每个窗口显示4个参数。每个参数的范围是从0到99。
    有4个按键:
(a)        一个是设置参数S13按键,按下此按键,液晶屏的第1个窗口第一行会出现反显的光标,表示进入设置参数模式,再次按下此按键,反显光标会消失,并且强行切换到第1个窗口,表示退出设置参数模式。
(b)        一个是移动光标S9按键,在进入设置参数的模式下,依次按下此按键,液晶屏上的光标会从上往下移动,表示选中不同的参数。当移动到每个窗口最下边那一行时,再按下此按键会进行切换窗口的操作。
(c)        一个是减数S5按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐减小。
(d)        一个是加数S1按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐加大。
(3)源代码讲解如下:
第七十八节源代码讲解.rar (7.07 KB)
总结陈词:
这一节讲了在多个窗口里设置不同的参数。还有一种常见的项目,要求把第1窗口是用来作为主菜单,主菜单里面有3个子菜单,可以通过移动光标进入不同的子菜单窗口进行参数设置,这类项目该如何编程?欲知详情,请听下回分解-----通过主菜单移动光标来进入子菜单窗口的液晶屏程序。

(未完待续,下节更精彩,不要走开哦)

使用特权

评论回复
568
yaoweipeng| | 2014-10-28 22:16 | 只看该作者
不错,学习学习。

使用特权

评论回复
569
armxu| | 2014-10-29 14:55 | 只看该作者
吴工,谢谢你写这么好帖子!希望你能继续发帖!关于KEIL的帖子什么时候发?

使用特权

评论回复
570
jianhong_wu|  楼主 | 2014-10-29 15:31 | 只看该作者
armxu 发表于 2014-10-29 14:55
吴工,谢谢你写这么好帖子!希望你能继续发帖!关于KEIL的帖子什么时候发? ...

应该还有一个月左右。讲完了液晶屏,我就会打算另外开一个新的技术贴讲零基础入门的。

使用特权

评论回复
571
菜鸟很懒| | 2014-10-29 16:33 | 只看该作者

使用特权

评论回复
572
solarddd| | 2014-10-30 07:57 | 只看该作者
吴工  来一些综合应用的例子!

使用特权

评论回复
573
jianhong_wu|  楼主 | 2014-10-30 11:25 | 只看该作者
本帖最后由 jianhong_wu 于 2014-10-30 11:31 编辑
solarddd 发表于 2014-10-30 07:57
吴工  来一些综合应用的例子!

其实我前面分享的例程都已经是综合应用的例子了。那些跑马灯,数码管,按键,串口已经代表了我个人当前的最高技术水平。我平时做项目也是那样做的。我觉得我这个帖子最重要最有价值的就是用switch语句外加一个定时中断的状态机程序框架。这个程序思路是我想教会初学者的。也是我自己认为学单片机最核心最重要的东西。

使用特权

评论回复
574
szjlj168| | 2014-10-30 14:34 | 只看该作者
这楼主给力,我必须来支持一下:P

使用特权

评论回复
575
jianhong_wu|  楼主 | 2014-10-30 15:46 | 只看该作者
第七十九节:通过主菜单移动光标来进入子菜单窗口的液晶屏程序。

开场白:
    其实主菜单窗口与子菜单窗口本质都是多窗口菜单程序,只不过我在按键服务程序里面建立起来了一条主窗口与子窗口的关系链。这个关系链还是用switch语句搭建起来的,在某个窗口某个局部显示上,操作某个按键就会切换到不同的窗口显示。
继续巩固上一节教给大家的两个知识点:
   第一个知识点:我在前面讲数码管显示的时候就提出了一个 “一二级菜单显示理论”:凡是人机界面显示,不管是数码管还是液晶屏,都可以把显示的内容分成不同的窗口来显示,每个显示的窗口中又可以分成不同的局部显示。其中窗口就是一级菜单,用ucWd变量表示。局部就是二级菜单,用ucWdxPart来表示。不同的窗口,会有不同的更新显示变量ucWdXUpdate来对应,表示整屏全部更新显示。不同的局部,也会有不同的更新显示变量ucWdXPartYUpdate来对应,表示局部更新显示。把每一个窗口的内容分为两种类型,一种类型是那些不用经常刷新显示的内容,只有在切换窗口的时候才需要更新的,这种内容放在整屏更新显示的括号里,比如清屏操作等内容。另外一种是那些经常需要刷新显示的内容,这种内容放在局部更新显示的括号里。
    第二个知识点:按键如何跟液晶屏显示有机的结合起来?只要遵循鸿哥总结出来的一个规律“在不同的窗口下,根据不同的局部变量来操作不同的参数”,这样再复杂的人机交互程序都会显得很简单清晰。

具体内容,请看源代码讲解。

(1)硬件平台:基于朱兆祺51单片机学习板。加按键对应S1键,减按键对应S5键,切换“光标”移动按键对应S9键,设置参数按键对应S13键。

(2)实现功能:
     通过按键设置6个不同的参数。
    有4个窗口。第1个窗口是主菜单界面,通过光标切换可以进去设置不同参数的子菜单界面。第2个窗口是设置时间范围界面。第3个窗口是设置速度范围界面。第4个窗口是设置频率范围界面。每个设置界面显示2个参数。每个参数的范围是从0到99。
    有4个按键:
(a)        一个是进入和退出S13按键,按一次进入选中的子菜单。再按一次退出子菜单。
(b)        一个是移动光标S9按键,依次按下此按键,液晶屏上的光标会从上往下移动,表示选中不同的参数。当移动到每个窗口最下边那一行时,再按下此按键会把光标移动到第一个参数。
(c)        一个是减数S5按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐减小。
(d)        一个是加数S1按键,在设置参数模式下,依次按下此按键,被选中的参数会逐渐加大。

(3)源代码讲解如下:
第七十九节源代码讲解.rar (7.95 KB)
总结陈词:
    我前面几节液晶屏程序的字模都是通过外围工具软件生成的,其实这款12864液晶模块本身就是自带字库,编程的时候只要在源代码里直接写入所需要的汉字或者字符,就可以自动调用相对应的字库了。但是细心的网友一定会问,为什么在源代码上直接写入某个汉字就可以调用到这个汉字的字库?在这个过程中,C51编译器到底还干了哪些鲜为人知的好事?欲知详情,请听下回分解-----液晶屏自带字库跟汉字机内码的关系。

(未完待续,下节更精彩,不要走开哦)

使用特权

评论回复
576
jianhong_wu|  楼主 | 2014-11-3 15:20 | 只看该作者
第八十节:调用液晶屏内部字库来显示汉字或字符的坐标体系和本质。

开场白:
前面章节讲的内容全部都是用自构字库的,相当于使用液晶屏的图像模式。其实这个款12864液晶屏的驱动芯片是st7920,它内部是自带16x16字库的,可以显示16x16的汉字或者8x16的字符。这一节开始就跟大家讲讲这方面的内容。要教会大家四个知识点:
第一个:内部字库的真实坐标体系的本质。当我们用内部字库的时候,它的坐标体系跟前面讲的自造字库坐标不一样,不再是256x32的液晶屏。它还原成为128x64的液晶屏,横坐标x轴坐标没办法精确到每个点,只能以16个点(2个字节)为一个单位,因此128个点的x轴坐标范围是0至8。而y轴的坐标也是以16个点(2个字节)为一个单位,因此64个点的x轴坐标范围是0至3。把12864液晶屏分成4行8列,每个数代表一个坐标点。
第二个:在使用内部字库时,C51编译器暗地里干了啥?如果使用液晶屏内部自带字库,编程的时候只要在源代码里直接写入所需要的汉字或者字符,就可以自动调用相对应的字库了。但是细心的网友一定会问,为什么在源代码上直接写入某个汉字就可以调用到这个汉字的字库?其实,表面上我们写下具体的某个汉字或者字符,但是C51编译器会自动对数组内的汉字翻译成 机内码(2字节),会自动对数组内的字符翻译成 ASCII码(1字节)。
第三个:12864的控制芯片st7920内部有两套驱动显示指令方式,一种是前面章节讲的自构字库模式,也是图像模式。另外一种就是本节讲的用内部字库模式。在切换模式的时候,发送命令字0x0c表示用内部字库模式,发送命令字0x36表示用自构字库模式。
第四个:12864整屏有4行8列,一共32个坐标点,每个坐标点可以显示一个16x16的汉字,但是在显示8x16字符时候,必须一次显示2个字符筹够16x16的点阵。例如,只想达到显示一个字符的时候,应该在另外一个空位置上显示空字符来填充。

具体内容,请看源代码讲解。


(1)硬件平台:基于朱兆祺51单片机学习板。

(2)实现功能:
     开机上电后,液晶屏第一行调用直接汉字书写方式的数组来显示(馒头V5)的内容。第四行调用机内码和ASCII码的数组来显示(馒头V5)的内容。

(3)源代码讲解如下:
#include "REG52.H"

sbit  LCDCS_dr  = P1^6;  //片选线
sbit  LCDSID_dr = P1^7;  //串行数据线
sbit  LCDCLK_dr = P3^2;  //串行时钟线
sbit  LCDRST_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_clear(void); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。

void display_hz1616(unsigned int x,unsigned int y,const unsigned char  *ucArray);
void display_double_zf816(unsigned int x,unsigned int y,const unsigned char  *ucArray1,const unsigned char  *ucArray2);

void delay_short(unsigned int uiDelayshort); //延时

/* 注释一:内部字库的真实坐标体系的本质。
* 当我们用内部字库的时候,它的坐标体系跟前面讲的自造字库坐标不一样,不再是256x32的液晶屏。
* 它还原成为128x64的液晶屏,横坐标x轴坐标没办法精确到每个点,只能以16个点(2个字节)为一个单位,
* 因此128个点的x轴坐标范围是0至8。而y轴的坐标也是以16个点(2个字节)为一个单位,因此64个点的x轴
* 坐标范围是0至3。以下是坐标地址的位置编码。把12864液晶屏分成4行8列,每个数代表一个坐标点,
* 用深究具体含义,液晶驱动芯片ST7920的手册上有提到。
*/
code unsigned char  ucAddrTable[]=  //调用内部字库时,液晶屏的坐标体系,位置编码,是驱动内容,读者可以不用深究它的含义。
{     
0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,
0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,
0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,
0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f,
};


/* 注释二:在使用内部字库时,C51编译器暗地里干了啥?
* 如果使用液晶屏内部自带字库,以下编程的时候只要在源代码里直接写入所需要的汉字或者字符,
* 就可以自动调用相对应的字库了。但是细心的网友一定会问,为什么在源代码上直接写入某个汉字
* 就可以调用到这个汉字的字库?其实,表面上我们写下具体的某个汉字或者字符,但是C51编译器
* 会自动对数组内的汉字翻译成 机内码(2字节),会自动对数组内的字符翻译成 ASCII码(1字节)。
* 本节程序会做这个实验来验证它。以下两种书写方式不一样,但本质是一样的。
*/

code unsigned char Hz1616_man[]="馒"; //对于数组内的汉字,编译会自动翻译成 机内码(2字节)
code unsigned char JN1616_man[]=  //机内码  馒  网上有很多把汉字或者字符转换成相关编码的工具软件
{
0xC2,
0xF8,
};

code unsigned char Hz1616_tou[]="头"; //对于数组内的汉字,编译会自动翻译成 机内码(2字节)
code unsigned char JN1616_tou[]=  //机内码  头  网上有很多把汉字或者字符转换成相关编码的工具软件
{
0xCD,
0xB7,
};

code unsigned char Zf816_V[]="V";     //对于数组内的字符,编译会自动翻译成 ASCII码(1字节)
code unsigned char ASCII816_V[]= //ASCII码  V  网上有很多把汉字或者字符转换成相关编码的工具软件
{
0x56,
};

code unsigned char Zf816_5[]="5";     //对于数组内的字符,编译会自动翻译成 ASCII码(1字节)
code unsigned char ASCII816_5[]= //ASCII码  5  网上有很多把汉字或者字符转换成相关编码的工具软件
{
0x35,
};


code unsigned char Zf816_nc[]=" ";     //对于数组内的字符,编译会自动翻译成 ASCII码(1字节)
code unsigned char ASCII816_nc[]= //ASCII码  空字符  网上有很多把汉字或者字符转换成相关编码的工具软件
{
0x20,
};


void main()
  {
        LCDInit(); //初始化12864 内部包含液晶模块的复位

/* 注释三:
* 12864的控制芯片st7920内部有两套驱动显示指令方式,一种是前面章节讲的自构字库模式,也是图像模式。
* 另外一种就是本节讲的用内部字库模式。以下是切换模式的命令,命令字0x0c表示用内部字库模式。
* 命令字0x36表示用自构字库模式。
*/
        WriteCommand(0x0C); //命令字0x0c表示用内部字库模式。命令字0x36表示用自构字库模式。

        display_clear(); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。


        display_hz1616(0,0,Hz1616_man);  //第一行,调用直接汉字书写方式的数组来显示(馒头V5),
        display_hz1616(1,0,Hz1616_tou);
        display_double_zf816(2,0,Zf816_V,Zf816_5);

        display_hz1616(0,3,JN1616_man);  //第四行,调用机内码和ASCII码的数组来显示(馒头V5),
        display_hz1616(1,3,JN1616_tou);
        display_double_zf816(2,3,ASCII816_V,Zf816_5);


        while(1)  
        {
           ;
        }

}


/* 注释四:在一个坐标点显示1个内部字库汉字的函数
* 第1,2个参数x,y是坐标体系。x的范围是0至8,y的范围是0至3.
* 第3个参数*ucArray是汉字机内码,是有2个字节的数组。
*/
void display_hz1616(unsigned int x,unsigned int y,const unsigned char  *ucArray)
{
    WriteCommand(0x30);   //基本指令集
        WriteCommand(ucAddrTable[8*y+x]);        //起始位置
        LCDWriteData(ucArray[0]);
        LCDWriteData(ucArray[1]);
}

/* 注释五:在一个坐标点显示2个内部字库字符的函数
* 注意,由于一个坐标点是16x16点阵,而一个字符是8x16点阵的,所以务必要显示2个字符筹够1个坐标点。
* 第1,2个参数x,y是坐标体系。x的范围是0至8,y的范围是0至3.
* 第3个参数*ucArray1是左边第1个字符ASCII码,是有1个字节的数组。
* 第4个参数*ucArray2是右边第2个字符ASCII码,是有1个字节的数组。
*/
void display_double_zf816(unsigned int x,unsigned int y,const unsigned char *ucArray1,const unsigned char  *ucArray2)
{
    WriteCommand(0x30);   //基本指令集
        WriteCommand(ucAddrTable[8*y+x]);        //起始位置
        LCDWriteData(ucArray1[0]);
        LCDWriteData(ucArray2[0]);
}


void display_clear(void) // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
{   

    unsigned int i,j;
        for(i=0;i<4;i++)
        {
                for(j=0;j<8;j++)
                {
                   display_double_zf816(j,i,Zf816_nc,ASCII816_nc);  //Zf816_nc与ASCII816_nc本质是一样的,只是书写方式不一样。
                }
        }


}

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++)
   {
     ;  
   }
}
总结陈词:
    通过本节的实验,我们发现汉字的识别本质是机内码,字符的识别本质是ASCII码。不管是机内码还是ASCII码,这些都是16进制的数字,也就是我们手机平时接收和发送的信息本质都是这些数字编码,但是机内码是2个字节,ASCII码是1个字节,如果在一串随机的信息中,同时包含汉字和字符两种数字信息,我们的程序又该如何能筛选和识别它们,会不会把机内码和ASCII码搞混乱了?不会的。其实这两种编码都是有规律可以筛选识别的,欲知详情,请听下回分解-----液晶屏显示串口发送过来的任意汉字和字符。

(未完待续,下节更精彩,不要走开哦)

使用特权

评论回复
577
无影行者| | 2014-11-4 19:10 | 只看该作者
学习中

使用特权

评论回复
578
jianhong_wu|  楼主 | 2014-11-6 16:39 | 只看该作者
第八十一节:液晶屏显示串口发送过来的任意汉字和字符。

开场白:
通过上一节的学习,我们发现汉字的识别本质是机内码,字符的识别本质是ASCII码。不管是机内码还是ASCII码,这些都是16进制的数字,也就是我们手机平时接收和发送的信息本质都是这些数字编码,但是机内码是2个字节,ASCII码是1个字节,如果在一串随机的信息中,同时包含汉字和字符两种数字信息,我们的程序又该如何能筛选和识别它们,会不会把机内码和ASCII码搞混乱了?这一节要教大家三个知识点:
第一个:ASCII码与汉字机内码不一样的规律是,ASCII码都是小于128(0x80)的,根据这个特点可以编程序把它们区分开来。
第二个:当任意一串信息中既包含汉字机内码,又包含字符ASCII码时,并且当ASCII码左右相邻个数是以奇数存在的时候,如何巧妙地插入填充空格字符0x20使它们能够符合一个坐标点显示2个字符的要求。
第三个:本节程序串口部分是在第39节内容基础上移植修改而成,本节程序中多添加了如何通过结束标志0x0D 0x0A来提取有效数据的内容,读者可以学习一下其中的框架。

具体内容,请看源代码讲解。


(1)硬件平台:基于朱兆祺51单片机学习板。

(2)实现功能:
     开机上电后,液晶屏第1行显示“请发送信息”。 任意时刻,从电脑“串口调试助手”根据以下协议要求,发送一串不超过24个汉字或者字符的信息,液晶屏就实时把这些信息显示在第2,3,4行。并且蜂鸣器会鸣叫一声表示数据接收正确。

波特率是:9600 。
通讯协议:EB 00 55  XX XX XX XX …XX XX 0D 0A
最前面3个字节EB 00 55 表示数据头。
最后面2个字节0D 0A表示信息的结束标志。
中间的XX是机内码和ASCII码信息。比如:要发送“曹健1人学习51单片机”的信息,它们对应的指令是:
EB 00 55 B2 DC BD A1 31 C8 CB D1 A7 CF B0 35 31 B5 A5 C6 AC BB FA 0D 0A

(3)源代码讲解如下:
#include "REG52.H"


/* 注释一:
* 本程序的串口那部分内容是从《第三十九节:判断数据头来接收一串数据的串口通用程序框架。》
* 移植过来的,但是以下要把接收缓冲区的数据从10改成60.同时,协议后面多增加了数据结束标志0x0d 0x0a。
*/

#define const_rc_size  60  //接收串口中断数据的缓冲区数组大小
#define const_receive_time  5  //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小

#define const_voice_short  40   //蜂鸣器短叫的持续时间

sbit  LCDCS_dr  = P1^6;  //片选线
sbit  LCDSID_dr = P1^7;  //串行数据线
sbit  LCDCLK_dr = P3^2;  //串行时钟线
sbit  LCDRST_dr = P3^4;  //复位线

sbit beep_dr=P2^7; //蜂鸣器的驱动IO口

void initial_myself(void);   
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);
void T0_time(void);  //定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);  //串口服务程序,在main函数里

void display_service(void); //显示服务程序,在main函数里
void empty_diaplay_buffer(void); //把显示缓冲区全部填充空格字符0x20
void diaplay_all_buffer(void); //显示第2,3,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_clear(void); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
void display_double_code(unsigned int x,unsigned int y,const unsigned char ucArray1,const unsigned char  ucArray2); //在一个坐标点显示1个汉字或者2个字符的函数
void delay_short(unsigned int uiDelayshort); //延时


code unsigned char  ucAddrTable[]=  //调用内部字库时,液晶屏的坐标体系,位置编码,是驱动内容,读者可以不用深究它的含义。
{     
0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,
0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,
0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f,
0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f,
};

code unsigned char JN1616_qing[]=  //机内码  请
{
0xC7,0xEB, //请
};

code unsigned char JN1616_fa[]=  //机内码  发
{
0xB7,0xA2,
};

code unsigned char JN1616_song[]=  //机内码  送
{
0xCB,0xCD,
};

code unsigned char JN1616_xin[]=  //机内码  信
{
0xD0,0xC5,
};

code unsigned char JN1616_xi[]=  //机内码  息
{
0xCF,0xA2,
};

unsigned int  uiSendCnt=0;     //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1;    //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned int  uiRcregTotal=0;  //代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf[const_rc_size]; //接收串口中断数据的缓冲区数组
unsigned int  uiRcMoveIndex=0;  //用来解析数据协议的中间变量

unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器

unsigned char ucWd1Update=1; //窗口1的整屏更新显示变量      1代表更新显示,响应函数内部会清零
unsigned char ucWd1Part1Update=0; //窗口1的第1个局部更新显示变量  1代表更新显示,响应函数内部会清零

unsigned char ucDispplayBuffer[48]; //第2,3,4行显示内容的缓冲区

void main()
  {
        initial_myself();  
        delay_long(100);   
        initial_peripheral();

        while(1)  
        {
            usart_service();  //串口服务程序
                        display_service(); //显示服务程序
        }

}



/* 注释二:在一个坐标点显示1个汉字或者2个字符的函数
* 第1,2个参数x,y是坐标体系。x的范围是0至8,y的范围是0至3.
* 第3个参数ucArray1是第1个汉字机内码或者ASCII码。
* 第4个参数ucArray2是第2个汉字机内码或者ASCII码。
*/
void display_double_code(unsigned int x,unsigned int y,const unsigned char ucArray1,const unsigned char  ucArray2)
{
    WriteCommand(0x30);   //基本指令集
    WriteCommand(ucAddrTable[8*y+x]);        //起始位置
    LCDWriteData(ucArray1);
    LCDWriteData(ucArray2);
}


void display_clear(void) // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。
{   

    unsigned int i,j;
        for(i=0;i<4;i++)
        {
                for(j=0;j<8;j++)
                {
                   display_double_code(j,i,0x20,0x20);  //0x20是空格的ASCII码
                }
        }


}

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 empty_diaplay_buffer(void) //把显示缓冲区全部填充空格字符0x20
{
   unsigned int i;

   for(i=0;i<48;i++)
   {
      ucDispplayBuffer[i]=0x20; //第2,3,4行显示内容的缓冲区全部填充0x20空格字符
   }

}

void diaplay_all_buffer(void) //显示第2,3,4行全部缓冲区的内容
{
   unsigned int i,j;

   for(i=0;i<3;i++) //i代表行数
   {
      for(j=0;j<8;j++) //j代表某行的某个坐标在第几列
      {
         display_double_code(j,i+1,ucDispplayBuffer[i*16+j*2],ucDispplayBuffer[i*16+j*2+1]); //这里的16代表一行可以显示16个字符
      }
   }

}


void display_service(void) //显示服务程序,在main函数里
{
  if(ucWd1Update==1)  //窗口1整屏更新,里面只放那些不用经常刷新显示的内容
    {
        ucWd1Update=0;  //及时清零,避免一直更新

        ucWd1Part1Update=1; //激活窗口1的第1个局部更新显示变量

        display_clear(); // 清屏。4行8列的坐标点全部显示2个空字符相当于清屏了。

                //显示第一行固定的内容:请发送信息
        display_double_code(1,0,JN1616_qing[0],JN1616_qing[1]);      //请
        display_double_code(2,0,JN1616_fa[0],JN1616_fa[1]);          //发
        display_double_code(3,0,JN1616_song[0],JN1616_song[1]);      //送
        display_double_code(4,0,JN1616_xin[0],JN1616_xin[1]);        //信
        display_double_code(5,0,JN1616_xi[0],JN1616_xi[1]);          //息

    }

    if(ucWd1Part1Update==1) //窗口1的第1个局部更新显示变量,里面放一些经常需要刷新显示的内容
    {
        ucWd1Part1Update=0; //及时清零,避免一直更新

        diaplay_all_buffer(); //显示第2,3,4行全部缓冲区的内容
        }
}


/* 注释三:
* 以下有效信息截取和如何判断机内码与ASCII码是本程序的核心,请仔细看讲解。
* 凡是ASCII码都是小于0x80(128)的,根据这个特点可以把ASCII码和机内码分离出来,
* 同时,由于液晶屏的1个坐标必须显示2个编码,对于单个存在的ASCII码,我们要在
* 它的右边多插入一个空格字符0x20。至于如何插入空格0x20字符,请看以下代码。
*/
void usart_service(void)  //串口服务程序,在main函数里
{
     unsigned int i;
         unsigned int uiCodeCnt; //统计接收的有效编码数量
         unsigned int uiCodeYu;  //对uiCodeCnt求2的余数,方便识别是否是1个ASCII码相邻
     if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
     {


            ucSendLock=0;    //处理一次就锁起来,不用每次都进来,除非有新接收的数据
            uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动  这个变量是用来抗干扰处理的

            while(uiRcregTotal>=6&&uiRcMoveIndex<=(uiRcregTotal-6)) //这里的6表示有3个字节的数据头,至少1个有效数据,2个数据结束标志0x0d 0x0a
            {
               if(ucRcregBuf[uiRcMoveIndex+0]==0xeb&&ucRcregBuf[uiRcMoveIndex+1]==0x00&&ucRcregBuf[uiRcMoveIndex+2]==0x55)  //数据头eb 00 55的判断
               {

                              empty_diaplay_buffer(); //把显示缓冲区全部填充空格字符0x20
                                  uiCodeCnt=0; //统计接收的有效编码数量清零
                  for(i=0;i<(uiRcregTotal-uiRcMoveIndex-3)&&i<48;i++)//这里的3表示有3个字节的数据头。48表示最大只能接收24个汉字,一共48个字节的机内码.
                                  {
                      if(ucRcregBuf[uiRcMoveIndex+3+i]==0x0d&&ucRcregBuf[uiRcMoveIndex+4+i]==0x0a)  //结束标志0x0d 0x0a的判断
                      {
                           uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,表示数据接收正确完毕
                                                   ucWd1Part1Update=1; //及时更新显示第2,3,4行内容的信息
                                                   break; //退出for循环
                      }       
                                       else  //收集有效信息编码进入显示缓冲区
                                          {
                                              uiCodeYu=uiCodeCnt%2; //对2求余数,用来识别相信的2个是否是机内码,否则要进行插入填充0x20处理
                                                  if(uiCodeYu==1)
                                                  {
                                                     if(ucRcregBuf[uiRcMoveIndex+3+i]>=0x80&&ucRcregBuf[uiRcMoveIndex+3+i-1]<0x80) //如果当前的是机内码,而上一个不是机内码
                                                         {
                                                                 ucDispplayBuffer[uiCodeCnt]=0x20; //当前的先填充插入空格字符0x20
                                                                uiCodeCnt++;   //统计接收的有效编码数量
                                                         }
                                                  }
                                              ucDispplayBuffer[uiCodeCnt]=ucRcregBuf[uiRcMoveIndex+3+i]; //收集有效信息编码进入显示缓冲区
                                              uiCodeCnt++;   //统计接收的有效编码数量
                                          }
                                  }
                  break;   //退出while循环
               }
               uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
           }
                                         
           uiRcregTotal=0;  //清空缓冲的下标,方便下次重新从0下标开始接受新数据
  
     }
                        
}


void T0_time(void) interrupt 1    //定时中断
{
  TF0=0;  //清除中断标志
  TR0=0; //关中断


  if(uiSendCnt<const_receive_time)   //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
  {
          uiSendCnt++;    //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
      ucSendLock=1;     //开自锁标志
  }

  if(uiVoiceCnt!=0)
  {
     uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
     beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。

  }
  else
  {
     ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
     beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
  }


  TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
  TL0=0x0b;
  TR0=1;  //开中断
}


void usart_receive(void) interrupt 4                 //串口接收数据中断        
{        

   if(RI==1)  
   {
        RI = 0;

            ++uiRcregTotal;
        if(uiRcregTotal>const_rc_size)  //超过缓冲区
        {
           uiRcregTotal=const_rc_size;
        }
        ucRcregBuf[uiRcregTotal-1]=SBUF;   //将串口接收到的数据缓存到接收缓冲区里
        uiSendCnt=0;  //及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
   
   }
   else  //我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
   {
        TI = 0;
   }
                                                         
}                                


void delay_short(unsigned int uiDelayShort)
{
   unsigned int i;  
   for(i=0;i<uiDelayShort;i++)
   {
     ;  
   }
}

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(void)  //第一区 初始化单片机
{

  beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。

  //配置定时器
  TMOD=0x01;  //设置定时器0为工作方式1
  TH0=0xfe;   //重装初始值(65535-500)=65035=0xfe0b
  TL0=0x0b;


  //配置串口
  SCON=0x50;
  TMOD=0X21;
  IP =0x10;  //把串口中断设置为最高优先级,必须的。
  TH1=TL1=-(11059200L/12/32/9600);  //这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
  TR1=1;

}

void initial_peripheral(void) //第二区 初始化外围
{

   EA=1;     //开总中断
   ES=1;     //允许串口中断
   ET0=1;    //允许定时中断
   TR0=1;    //启动定时中断


   LCDInit(); //初始化12864 内部包含液晶模块的复位
   WriteCommand(0x0C); //命令字0x0c表示用内部字库模式。命令字0x36表示用自构字库模式。
   empty_diaplay_buffer(); //把显示缓冲区全部填充空格字符0x20
}




总结陈词:
我们现在是调用液晶屏内部字库来显示内容,如果要某行内容反显或者光标闪烁改怎么编程?欲知详情,请听下回分解-----如何在调用液晶屏内部字库时让某行内容反显或者光标闪烁。

(未完待续,下节更精彩,不要走开哦)

使用特权

评论回复
579
armxu| | 2014-11-12 07:50 | 只看该作者
期待下一节

使用特权

评论回复
580
乐逍遥808| | 2014-11-13 17:06 | 只看该作者
福贴:victory:

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则