找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 28768|回复: 23
收起左侧

第13章 1602液晶与串口实用例程

  [复制链接]
ID:1 发表于 2013-9-28 15:11 | 显示全部楼层 |阅读模式
  本教材现以连载的方式由网络发布,并将于2014年由清华大学出版社出版最终完整版,版权归作者和清华大学出版社所有。本着开源、分享的理念,本教材可以自由传播及学习使用,但是务必请注明出处来自金沙滩工作室
        理论上的内容要想逐步消化掌握,必须得通过大量的实践进行巩固,否则时间一长,极容易忘掉。尤其是一些编程的算法相关的技巧,就是靠不停的写程序,不停的参考别人的程序慢慢积累成长起来的。这节课带着大家学习一下1602的例程和实际开发中比较实用的串口程序。
13.1 通信时序解析        随着我们对通信技术的深入学习,大家要逐渐在头脑中建立起时序这种概念。所谓“时序”从字面意义上来理解,一是“时间问题”,二是“顺序问题”。
        先说“顺序问题”,这个相对简单一些。我们在学UART串口通信的时候,先1位起始位,再8位数据位,最后1位停止位,这个先后顺序不能错。我们在学 1602液晶的时候,比如写指令,RS=L,R/W=L,D0~D7=指令码,这三者的顺序是无所谓的,但是最终的E=高脉冲,必须是在这三条程序之后,这个顺序一旦错误,写的数据也可能出错。
        “时间问题”内容相对复杂。比如UART通信,每一位的时间宽度是1/baud。我们初中就学过一个概念,世界上没有绝对的准确。那我们这个每一位的时间宽度1/baud要求精确到什么范围内呢?
前边教程我提到过,单片机读取UART的RXD引脚数据的时候,一位数据,单片机平均分成了16份,取其中的7、8、9三次读到的结果,这三次中有2次是高电平那这一位就是1,有2次是低电平,那这一次就是0。如果我们的波特率稍微有些偏差,只要累计下来到最后一位停止位,这7、8、9还在范围内即可。如图 13-1所示。

2.JPG

        图13-1 UART信号采集时序图


     我用三个箭头来表示7、8、9这三次的采集位置,大家可以注意到,当采集到D7的时候,已经有一次采集偏差出去了,但是我们采集到的数据还是不会错,因为有 2次采集正确。至于这个偏差允许多大,大家自己可以详细算一下。实际上UART通信的波特率是允许一定范围内误差存在的,但是不能过大,否则就会采集错误。大家在计算波特率的时候,发现没有整除,有小数部分的时候,就要特别小心了,因为小数部分是一概被舍掉的,于是计算误差就产生了。 我们用 11.0592M晶振计算的过程中,11059200/12/32/9600得到的是一个整数,如果用12M晶振计算12000000/12/32 /9600就会得到一个小数,大家可以算一下误差多少,是否在误差范围内。
        1602的时序问题,大家要学会通过LCD1602的数据手册提供的时序图和时序参数表格来进行研究,而且看懂时序图是学习单片机必须学会的一项技能,如图12-2所示。
3.JPG


        图13-2 1602时序图


        大家看到这种图的时候,不要觉得害怕。说句不过分的话,单片机这些逻辑上的问题,只要小学毕业就可以理解的,很多时候是因为大家把问题想象的太难才学不下去的。
        我们先来看一下读操作时序的RS引脚和R/W引脚,这两个引脚先进行变化,因为是读操作,所以R/W引脚首先要置为高电平,而不管他原来是什么。读指令还是读数据,都是读操作,而且都有可能,所以RS引脚既有可能是置为高电平,也有可能是置为低电平,大家注意图上的画法。而RS和R/W变化了经过 Tsp1这么长时间后,使能引脚E才能从低电平到高电平发生变化。
而使能引脚E拉高了经过了tD这么长时间后,LCD1602输出DB的数据就是有效数据了,我们就可以来读取DB的数据了。读完了之后,我们要先把使能E拉低,经过一段时间后RS、R/W和DB才可以变化继续为下一次读写做准备了。
        而写操作时序和读操作时序的差别,就是写操作时序,DB的改变是我们单片机来完成的,因此要放到使能引脚E的变化之前进行操作,其他区别大家可以自行对比一下。
        细心的同学会发现,这个时序图上还有很多时间标签。比如E的上升时间tR,下降时间时间tF,使能引脚E从一个上升沿到下一个上升沿之间的长度周期 tC,使能E下降沿后,R/W和RS变化时间间隔tHD1等等很多时间要求,这些要求怎么看呢?放心,只要是正规的数据手册,都会把这些时间要求给大家标记出来的。我们来看一下表13-1所示。

        表13-1 1602时序参数


时序参数
符号
极限值
单位
测试条件
最小值
典型值
最大值
E信号周期
tC
400
--
--
ns
引脚E
E脉冲宽度
tPW
150
--
--
ns
E上升沿/下降沿时间
tR, tF
--
--
25
ns
地址建立时间
tSP1
30
--
--
ns
引脚E、RS、R/W
地址保持时间
tHD1
10
--
--
ns
数据建立时间(读)
tD
--
--
100
ns
引脚DB0~DB7
数据保持时间(读)
tHD2
20
--
--
ns
数据建立时间(写)
tSP2
40
--
--
ns
数据保持时间(写)
tHD2
10
--
--
ns
        大家要善于把手册中的这个表格和时序图结合起来看。表12-1中的数据,都是时序参数,本节课的所有的时序参数,我都一点点的给大家讲出来,以后遇到同类时序图,我就不再讲了,只是提一下,但是大家务必要学会自己看时序图,这个很重要,此外,看以下解释需要结合图13-2来看。
        tC:指的是使能引脚E从本次上升沿到下次上升沿的最短时间是400ns,而我们单片机因为速度较慢,一个机器周期就是1us多,而一条C语言指令肯定是一个或者几个机器周期的,所以这个条件完全满足。
        tPW:指的是使能引脚E高电平的持续时间最短是150ns,由于我们的单片机比较慢,这个条件也完全满足。
        tR, tF:指的是使能引脚E的上升沿时间和下降沿时间,不能超过25ns,这个时间限值空间很大,我们用示波器测了一下我们开发板的这个引脚上升沿和下降沿时间大概是10ns到15ns之间,完全满足。
        tSP1:指的是RS和R/W引脚使能后至少保持30ns,使能引脚E才可以变成高电平,这个条件完全满足。
        tHD1:指的是使能引脚E变成低电平后,至少保持10ns之后,RS和R/W才能进行变化,这个条件完全满足。
        tD:指的是我们的使能引脚E变成高电平后,最多100ns后,1602就把数据送出来了,那么我们就可以正常去读取状态或者数据了。
        tHD2:指的是读操作过程中,使能引脚E变成低电平后,至少保持20ns,DB数据总线才可以进行变化,这个条件完全满足。
        tSP2:指的是DB数据总线准备好后,至少保持40ns,使能引脚E才可以从低到高进行使能变化,这个条件完全满足。
        tHD2:指的是写操作过程中,只能引脚E变成低电平后,至少保持10ns,DB数据总线才可以变化,这个条件完全满足。
        好了,表13-1这个LCD1602的时序参数表已经解析完成了,看完之后,是不是感觉比你想象的要简单,没有你想的那么困难。大家自己也得慢慢学会看这种时序图和表格,在今后的学习中,这方面的能力尤为重要。如果以后换用了其它型号的单片机,那么就根据单片机的执行速度来评估你的程序是否满足时序要求,整体上来说器件都是有一个最快速度的限制,而没有最慢限制,所以当换用高速的单片机后通常都是靠在各步骤间插入软件延时来满足较慢的时序要求。
13.2 1602整屏移动       我们前边学第七章点阵LED的时候,可以实现上下移动,左右移动等。而对于1602液晶来说,也可以进行屏幕移动,实现我们想要的一些效果,那我们来用一个例程实现字符串在1602液晶上的左移。每个人都不要只瞪着眼看,一定要认真抄下来,甚至抄几遍,边抄遍理解,要想真正学好,一定要根据我的方法来做。

#include <reg52.h>

#define LCD1602_DB   P0     

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E  = P1^5;

bit flagT0 = 0;            //T0中断产生标志
unsigned char T0RH = 0;  //T0重载值的高字节
unsigned char T0RL = 0;  //T0重载值的低字节

unsigned char code str1[] = "Kingst Studio"; //待显示的第一行字符串
unsigned char code str2[] = "Let's move..."; //待显示的第二行字符串,需保持与第一行字符串等长,较短的行可用空格补齐

void ConfigTimer0(unsigned int ms);
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len);
void LcdInit();

void main ()
{
    unsigned char i;
    unsigned char iMove = 0; //移动索引
    unsigned int  tmrMove = 0; //屏幕移动定时器
    unsigned char pdata bufMove1[16 + sizeof(str1) + 16]; //移动显示的缓冲区
    unsigned char pdata bufMove2[16 + sizeof(str1) + 16]; //移动显示的缓冲区

    EA = 1;            //开总中断
    ConfigTimer0(10);  //配置T0定时10ms
    LcdInit();         //初始化液晶

    for (i=0; i<16; i++)  //缓冲区开头一段填充为空格
    {
        bufMove1[ i] = ' ';
        bufMove2[ i] = ' ';
    }
    for (i=0; i<(sizeof(str1)-1); i++) //待显示字符串拷贝到缓冲区中间位置
    {
        bufMove1[16+i] = str1[ i];
        bufMove2[16+i] = str2[ i];
    }
    for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++)  //缓冲区结尾一段也填充为空格
    {
        bufMove1[ i] = ' ';
        bufMove2[ i] = ' ';
    }

    while(1)
    {
        if (flagT0)
        {
            flagT0 = 0;
            tmrMove += 10;
            if (tmrMove >= 500) //每500ms移动一次屏幕
            {
                tmrMove = 0;
                LcdShowStr(0, 0, bufMove1+iMove, 16); //从缓冲区抽出需显示的一段字符显示到液晶上
                LcdShowStr(0, 1, bufMove2+iMove, 16);
                iMove++;                              //移动索引递增,实现左移
                if (iMove >= (16+sizeof(str1)-1))     //起始位置达到字符串尾部后即返回从头开始
                {
                    iMove = 0;
                }
            }
        }
    }
}

void ConfigTimer0(unsigned int ms)  //T0配置函数
{
    unsigned long tmp;

    tmp = 11059200 / 12;      //定时器计数频率
    tmp = (tmp * ms) / 1000;  //计算所需的计数值
    tmp = 65536 - tmp;        //计算定时器重载值
    tmp = tmp + 12;           //修正中断响应延时造成的误差

    T0RH = (unsigned char)(tmp >> 8);  //定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中断
    TR0 = 1;        //启动T0
}
void LcdWaitReady()  //等待液晶准备好
{
    unsigned char sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do
    {
        LCD1602_E = 1;
        sta = LCD1602_DB; //读取状态字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止
}
void LcdWriteCmd(unsigned char cmd)  //写入命令函数
{
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_E  = 1;
    LCD1602_DB = cmd;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  //写入数据函数
{
    LcdWaitReady();
    LCD1602_RS = 1;
    LCD1602_RW = 0;
    LCD1602_E  = 1;
    LCD1602_DB = dat;
    LCD1602_E  = 0;
}
void LcdInit()  //液晶初始化函数
{
    LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口
    LcdWriteCmd(0x0C);  //显示器开,光标关闭
    LcdWriteCmd(0x06);  //文字不动,地址自动+1
    LcdWriteCmd(0x01);  //清屏
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len)  // 显示字符串,屏幕起始坐标(x,y),字符串指针str,需显示长度len
{
    unsigned char addr;

    //由输入的显示坐标计算显示RAM的地址
    if (y == 0)
    {
        addr = 0x00 + x; //第一行字符地址从0x00起始
    }
    else
    {
        addr = 0x40 + x; //第二行字符地址从0x40起始
    }

    //由起始显示RAM地址连续写入字符串
    LcdWriteCmd(addr | 0x80); //写入起始地址
    while (len--)       //连续写入字符串数据
    {
        LcdWriteDat(*str);
        str++;
    }
}
void InterruptTimer0() interrupt 1  //T0中断服务函数
{
    TH0 = T0RH;  //定时器重新加载重载值
    TL0 = T0RL;
    flagT0 = 1;
}
        通过这个程序,大家首先要学会for语句在数组中的灵活应用,这个其实在数码管显示有效位的例程中已经有所体现了。其次,随着我们后边程序量的增大,大家得学会多个函数之间的相互调用的灵活应用,体会其中的奥妙。
13.3 多.c文件的初步认识        我们上一节的这个液晶滚屏移动程序,大概有150行左右。随着我们硬件模块使用的增多,程序量的增大,我们往往要把程序写到多个文件里,方便代码的编写、维护和移植。
        比如这个液晶滚屏程序,我们就可以把1602底层的功能函数专门写到一个.C文件内,如LcdWaitReady、LcdWriteCmd、 LcdWriteDat、LcdInit、LcdShowStr 这几个函数,都是属于液晶底层驱动的程序代码,我们要使用液晶功能的时候,只有两个函数对我们实际功能实现部分有用,一个是LcdInit这个函数,需要先初始化液晶,另外一个就是LcdShowStr这个函数,我们只需要把我们要显示的内容通过参数传递给这个函数,这个函数就可以实现我们想要的显示效果,所以我们把这几个底层的液晶驱动程序都放到另外一个文件Lcd1602.c文件中,而我们想实现的一些比如滚动实现、中断等上层功能程序全部都放到main.c中,但是main.c文件如何调用Lcd1602.c文件中的函数呢?
C语言中,有一个extern关键字,他有两个基本作用。
        1、当一个变量的声明不在文件的开头,在它声明之前的函数想要引用的话,则应该用extern进行“外部变量”声明。用一个简单的程序给大家介绍一下,知道这么回事,能看懂别人写的就行,自己写就别这么用了。
#include<reg52.h>               //包含寄存器的库文件                  
sbit  LED = P0 ^ 0;             //位地址声明  注意:sbit必须小写!
void  main()               
{   
    extern unsigned  int  i;  

    while(1)                  //程序死循环  
    {                                   
        LED = 0;                 //点亮小灯
        for(i=0;i<30000;i++);  //延时
        LED = 1;   //熄灭小灯
        for(i=0;i<30000;i++);  //延时
    }
}
unsigned  int  i = 0;
... ...

        我们变量的作用域,是从声明这个变量开始往后所有的程序,如果我们调用在前,声明在后,那么就是这么用。但是实际开发过程中,我们一般都不会这样做,所以仅仅是表达一下extern的这个用法,但它并不实用。
        2、在一个工程中,我们为了方便管理维护代码,所以用了多个.C源文件,如果其中一个main.c文件要调用Lcd1602.c文件里的变量或者函数的时候,我们就必须得在main.c里边进行以下外部声明,告诉编译器这个变量或者函数是在其他文件中定义的,可以直接在这个文件中进行调用,我们用上一节的程序代码试试看。
        多.c文件的编程方式,大家不要想象的太复杂。首先新建一个工程,一个工程代表一个完整的单片机程序,只能生成一个hex,但是一个工程可以有很多个.c源文件组成共同参与编译。工程建立好之后,新建文件并且保存称为main.c文件,再新建一个文件并且保存称为Lcd1602.c文件,下面我们就可以在两个不同文件中分别编写代码了。当然,在编写程序的过程中,不是说我们要先把main.c的文件全部写完,再进行1602.c程序的编写,而往往是交互的。比如我们先写Lcd1602.c文件中部分Lcd1602液晶的底层函数LcdWaitReady、LcdWriteCmd、 LcdWriteDat、LcdInit,然后编写main.c文件中的功能程序,在编写main.c文件中程序时,又有对Lcd1602.c底层程序的综合调用,这个时候需要Lcd1602.c文件提供一个被调用的函数比如LcdShowStr,我们就可以再到Lcd1602.c中把这个函数完成。当然了,这仅仅是一个说明例子而已,顺序完全是没有一个标准的,实际过程我们如果对程序逻辑需求了解透彻,根据我们自己的理解去写程序即可。那我们把1602 整屏移动的程序改造成为多文件的程序,大家先初步认识一下。
/*************************1602.c文件程序源代码**************************/

#include <reg52.h>

#define LCD1602_DB   P0

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E  = P1^5;

void LcdWaitReady()  //等待液晶准备好
{
    unsigned char sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do
    {
        LCD1602_E = 1;
        sta = LCD1602_DB; //读取状态字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止
}
void LcdWriteCmd(unsigned char cmd)  //写入命令函数
{
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_E  = 1;
    LCD1602_DB = cmd;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  //写入数据函数
{
    LcdWaitReady();
    LCD1602_RS = 1;
    LCD1602_RW = 0;
    LCD1602_DB = dat;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdInit()  //液晶初始化函数
{
    LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口
    LcdWriteCmd(0x0C);  //显示器开,光标关闭
    LcdWriteCmd(0x06);  //文字不动,地址自动+1
    LcdWriteCmd(0x01);  //清屏
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len)  // 显示字符串,屏幕起始坐标(x,y),字符串指针str,需显示长度len
{
    unsigned char addr;

    //由输入的显示坐标计算显示RAM的地址
    if (y == 0)
    {
        addr = 0x00 + x; //第一行字符地址从0x00起始
    }
    else
    {
        addr = 0x40 + x; //第二行字符地址从0x40起始
    }

    //由起始显示RAM地址连续写入字符串
    LcdWriteCmd(addr | 0x80); //写入起始地址
    while (len--)       //连续写入字符串数据
    {
        LcdWriteDat(*str);
        str++;
    }
}

/*************************main.c文件程序源代码**************************/

#include <reg52.h>

#define LCD1602_DB   P0

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E  = P1^5;

bit flagT0 = 0;
unsigned char T0RH = 0;  //T0重载值的高字节
unsigned char T0RL = 0;  //T0重载值的低字节

unsigned char code str1[] = "Kingst Studio"; //待显示的第一行字符串
unsigned char code str2[] = "Let's move..."; //待显示的第二行字符串,需保持与第一行字符串等长,较短的行可用空格补齐

void ConfigTimer0(unsigned int ms);
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str, unsigned char len);

void main ()
{
    unsigned char i;
    unsigned char iMove = 0; //移动索引
    unsigned int  tmrMove = 0; //屏幕移动定时器
    unsigned char pdata bufMove1[16 + sizeof(str1) + 16]; //移动显示的缓冲区
    unsigned char pdata bufMove2[16 + sizeof(str1) + 16]; //移动显示的缓冲区

    EA = 1;            //开总中断
    ConfigTimer0(10);  //配置T0定时10ms
    LcdInit();         //初始化液晶

    for (i=0; i<16; i++)  //缓冲区开头一段填充为空格
    {
        bufMove1[ i] = ' ';
        bufMove2[ i] = ' ';
    }
    for (i=0; i<(sizeof(str1)-1); i++) //待显示字符串拷贝到缓冲区中间位置
    {
        bufMove1[16+i] = str1[ i];
        bufMove2[16+i] = str2[ i];
    }
    for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++)  //缓冲区结尾一段也填充为空格
    {
        bufMove1[ i] = ' ';
        bufMove2[ i] = ' ';
    }

    while(1)
    {
        if (flagT0)
        {
            flagT0 = 0;
            tmrMove += 10;
            if (tmrMove >= 500) //每500ms移动一次屏幕
            {
                tmrMove = 0;
                LcdShowStr(0, 0, bufMove1+iMove, 16); //从缓冲区抽出需显示的一段字符显示到液晶上
                LcdShowStr(0, 1, bufMove2+iMove, 16);
                iMove++;                              //移动索引递增,实现左移
                if (iMove >= (16+sizeof(str1)-1))     //起始位置达到字符串尾部后即返回从头开始
                {
                    iMove = 0;
                }
            }
        }
    }
}

void ConfigTimer0(unsigned int ms)  //T0配置函数
{
    unsigned long tmp;

    tmp = 11059200 / 12;      //定时器计数频率
    tmp = (tmp * ms) / 1000;  //计算所需的计数值
    tmp = 65536 - tmp;        //计算定时器重载值
    tmp = tmp + 12;           //修正中断响应延时造成的误差

    T0RH = (unsigned char)(tmp >> 8);  //定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中断
    TR0 = 1;        //启动T0
}
void InterruptTimer0() interrupt 1  //T0中断服务函数
{
    TH0 = T0RH;  //定时器重新加载重载值
    TL0 = T0RL;
    flagT0 = 1;
}

        我们在main.c要调用Lcd1602.c文件中的LcdInit()和LcdShowStr这两个函数,只需要在main.c中进行extern 声明即可。大家用keil软件编程试试,真正的感觉一下多.c源文件的好处。如果这个你的感觉还不够深刻,那下面我们来做一个稍微大点的程序来体会一下。
13.4 计算器程序        按键和液晶,可以组成我们最简易的计算器。下面我们来写一个简易整数计算器提供给大家学习。为了让程序不过于复杂,我们这个计算器不考虑连加,连减等连续计算,不考虑小数情况。加减乘除分别用上下左右来替代,回车表示等于,ESC表示归0。程序共分为三部分,一部分是1602液晶显示,一部分是按键动作和扫描,一部分是主函数功能。
/*************************1602.c文件程序源代码**************************/

#include <reg52.h>

#define LCD1602_DB   P0

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E  = P1^5;

void LcdWaitReady()  //等待液晶准备好
{
    unsigned char sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do
    {
        LCD1602_E = 1;
        sta = LCD1602_DB; //读取状态字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止
}
void LcdWriteCmd(unsigned char cmd)  //写入命令函数
{
    LcdWaitReady();
    LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  //写入数据函数
{
    LcdWaitReady();
    LCD1602_RS = 1;
    LCD1602_RW = 0;
    LCD1602_DB = dat;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str)  //显示字符串,屏幕起始坐标(x,y),字符串指针str
{
    unsigned char addr;

    //由输入的显示坐标计算显示RAM的地址
    if (y == 0)
        addr = 0x00 + x; //第一行字符地址从0x00起始
    else
        addr = 0x40 + x; //第二行字符地址从0x40起始

    //由起始显示RAM地址连续写入字符串
    LcdWriteCmd(addr | 0x80); //写入起始地址
    while (*str != '\0')      //连续写入字符串数据,直到检测到结束符
    {
        LcdWriteDat(*str);
        str++;
    }
}
void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len)  //区域清除,清除从(x,y)坐标起始的len个字符位
{
    unsigned char addr;

    //由输入的显示坐标计算显示RAM的地址
    if (y == 0)
        addr = 0x00 + x; //第一行字符地址从0x00起始
    else
        addr = 0x40 + x; //第二行字符地址从0x40起始

    //由起始显示RAM地址连续写入字符串
    LcdWriteCmd(addr | 0x80); //写入起始地址
    while (len--)             //连续写入空格
    {
        LcdWriteDat(' ');
    }
}
void LcdFullClear()
{
    LcdWriteCmd(0x01);  //清屏
}
void LcdInit()  //液晶初始化函数
{
    LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口
    LcdWriteCmd(0x0C);  //显示器开,光标关闭
    LcdWriteCmd(0x06);  //文字不动,地址自动+1
    LcdWriteCmd(0x01);  //清屏
    LcdShowStr(15, 1, "0");
}

/***********************keyboard.c文件程序源代码************************/

#include <reg52.h>

sbit KEY_IN_1  = P2^4;  //矩阵按键的扫描输入引脚1
sbit KEY_IN_2  = P2^5;  //矩阵按键的扫描输入引脚2
sbit KEY_IN_3  = P2^6;  //矩阵按键的扫描输入引脚3
sbit KEY_IN_4  = P2^7;  //矩阵按键的扫描输入引脚4
sbit KEY_OUT_1 = P2^3;  //矩阵按键的扫描输出引脚1
sbit KEY_OUT_2 = P2^2;  //矩阵按键的扫描输出引脚2
sbit KEY_OUT_3 = P2^1;  //矩阵按键的扫描输出引脚3
sbit KEY_OUT_4 = P2^0;  //矩阵按键的扫描输出引脚4

const unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到PC标准键盘键码的映射表
    { '1',  '2',  '3', 0x26 }, //数字键1、数字键2、数字键3、向上键
    { '4',  '5',  '6', 0x25 }, //数字键4、数字键5、数字键6、向左键
    { '7',  '8',  '9', 0x28 }, //数字键7、数字键8、数字键9、向下键
    { '0', 0x1B, 0x0D, 0x27 }  //数字键0、ESC键、  回车键、 向右键
};
unsigned char pdata KeySta[4][4] = {  //全部矩阵按键的当前状态
    {1, 1, 1, 1},
    {1, 1, 1, 1},
    {1, 1, 1, 1},
    {1, 1, 1, 1}
};
unsigned char step = 0;  //操作步骤
unsigned char oprt = 0;  //运算类型
signed long num1 = 0;    //操作数1
signed long num2 = 0;    //操作数2
signed long result = 0;  //运算结果

extern void LcdFullClear();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);

unsigned char NumToString(unsigned char *str, signed long num) //整型数转换为字符串,字符串指针str,待转换数num,返回值为字符串长度
{
    unsigned char i, len;
    unsigned char buf[12];

    if (num < 0)  //如果为负数,则首先输出符号到指针上,并取其绝对值
    {
        *str = '-';
        str++;
        num = -num;
    }
    i = 0;        //先转换为低位在前的十进制数组
    do {
        buf[ i] = num % 10;
        num /= 10;
        i++;
    } while (num > 0);
    len = i;       //i最后的值就是有效字符的个数
    while (i > 0)  //然后将数组值转换为ASCII码反向拷贝到接收指针上
    {
        i--;
        *str = buf[ i] + '0';
        str++;
    }

    return len;   //返回转换后的字符串长度
}
void ShowOprt(unsigned char y, unsigned char type) //显示运算符,显示位置y,运算符类型type
{
    switch (type)
    {
        case 0: LcdShowStr(0, y, "+"); break;
        case 1: LcdShowStr(0, y, "-"); break;
        case 2: LcdShowStr(0, y, "*"); break;
        case 3: LcdShowStr(0, y, "/"); break;
        default: break;
    }
}
void Reset()  //计算器复位函数
{
    num1 = 0;
    num2 = 0;
    step = 0;
    LcdFullClear();
}
void NumKeyAction(unsigned char n) //数字键动作函数,按键输入的数值n
{
    unsigned char len;
    unsigned char str[12];

    if (step > 1)  //如计算已完成,则重新开始新的计算
    {
        Reset();
    }
    if (step == 0)  //输入第一操作数
    {
        num1 = num1*10 + n;           //输入数值累加到原操作数上
        len = NumToString(str, num1); //新数值转换为字符串
        LcdShowStr(16-len, 1, str);   //显示到液晶第二行上
    }
    else            //输入第二操作数
    {
        num2 = num2*10 + n;
        len = NumToString(str, num2);
        LcdShowStr(16-len, 1, str);
    }
}
void OprtKeyAction(unsigned char type) //运算符按键动作函数,运算符类型type
{
    unsigned char len;
    unsigned char str[12];

    if (step == 0)  //第二操作数尚未输入时响应,即不支持连续操作
    {
        len = NumToString(str, num1); //第一操作数转换为字符串
        LcdAreaClear(0, 0, 16-len);   //清除第一行左边的字符位
        LcdShowStr(16-len, 0, str);   //字符串靠右显示在第一行
        ShowOprt(1, type);            //在第二行显示操作符
        LcdAreaClear(1, 1, 14);       //清除第二行中间的字符位
        LcdShowStr(15, 1, "0");       //在第二行最右端显示0
        oprt = type;                  //记录操作类型
        step = 1;
    }
}
void GetResult() //计算结果
{
    unsigned char len;
    unsigned char str[12];

    if (step == 1) //第二操作数已输入时才执行计算
    {
        step = 2;
        switch (oprt)  //根据运算符类型计算结果,未考虑溢出问题
        {
            case 0: result = num1 + num2; break;
            case 1: result = num1 - num2; break;
            case 2: result = num1 * num2; break;
            case 3: result = num1 / num2; break;
            default: break;
        }
        len = NumToString(str, num2);   //原第二操作数和运算符显示在第一行
        ShowOprt(0, oprt);
        LcdAreaClear(1, 0, 16-1-len);
        LcdShowStr(16-len, 0, str);
        len = NumToString(str, result); //计算结果和等号显示在第二行
        LcdShowStr(0, 1, "=");
        LcdAreaClear(1, 1, 16-1-len);
        LcdShowStr(16-len, 1, str);
    }
}

void KeyAction(unsigned char keycode)  //按键动作函数,根据键码执行相应动作
{
    if  ((keycode>='0') && (keycode<='9'))  //显示输入的字符
    {
        NumKeyAction(keycode - '0');
    }
    else if (keycode == 0x26)  //向上键,+
    {
        OprtKeyAction(0);
    }
    else if (keycode == 0x28)  //向下键,-
    {
        OprtKeyAction(1);
    }
    else if (keycode == 0x25)  //向左键,*
    {
        OprtKeyAction(2);
    }
    else if (keycode == 0x27)  //向右键,÷
    {
        OprtKeyAction(3);
    }
    else if (keycode == 0x0D)  //回车键,计算结果
    {
        GetResult();
    }
    else if (keycode == 0x1B)  //Esc键,清除
    {
        Reset();
        LcdShowStr(15, 1, "0");
    }
}
void KeyDrive()  //按键动作驱动函数
{
    unsigned char i, j;
    static unsigned char pdata backup[4][4] = {  //按键值备份,保存前一次的值
        {1, 1, 1, 1},
        {1, 1, 1, 1},
        {1, 1, 1, 1},
        {1, 1, 1, 1}
    };

    for (i=0; i<4; i++)  //循环扫描4*4的矩阵按键
    {
        for (j=0; j<4; j++)
        {
            if (backup[ i][j] != KeySta[ i][j])  //检测按键动作
            {
                if (backup[ i][j] != 0)  //按键按下时执行动作
                {
                    KeyAction(KeyCodeMap[ i][j]);  //调用按键动作函数
                }
                backup[ i][j] = KeySta[ i][j];
            }
        }
    }
}
void KeyScan()  //按键扫描函数
{
    unsigned char i;
    static unsigned char keyout = 0;  //矩阵按键扫描输出计数器
    static unsigned char keybuf[4][4] = {  //按键扫描缓冲区,保存一段时间内的扫描值
        {0xFF, 0xFF, 0xFF, 0xFF},
        {0xFF, 0xFF, 0xFF, 0xFF},
        {0xFF, 0xFF, 0xFF, 0xFF},
        {0xFF, 0xFF, 0xFF, 0xFF}
    };

    //将一行的4个按键值移入缓冲区
    keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;
    keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;
    keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;
    keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;

    //消抖后更新按键状态
    for (i=0; i<4; i++)  //每行4个按键,所以循环4次
    {
        if ((keybuf[keyout][ i] & 0x0F) == 0x00)
        {   //连续4次扫描值为0,即16ms(4*4ms)内都只检测到按下状态时,可认为按键已按下
            KeySta[keyout][ i] = 0;
        }
        else if ((keybuf[keyout][ i] & 0x0F) == 0x0F)
        {   //连续4次扫描值为1,即16ms(4*4ms)内都只检测到弹起状态时,可认为按键已弹起
            KeySta[keyout][ i] = 1;
        }
    }

    //执行下一次的扫描输出
    keyout++;
    keyout &= 0x03;
    switch (keyout)
    {
        case 0: KEY_OUT_4 = 1; KEY_OUT_1 = 0; break;
        case 1: KEY_OUT_1 = 1; KEY_OUT_2 = 0; break;
        case 2: KEY_OUT_2 = 1; KEY_OUT_3 = 0; break;
        case 3: KEY_OUT_3 = 1; KEY_OUT_4 = 0; break;
        default: break;
    }
}

/*************************main.c文件程序源代码**************************/

#include <reg52.h>

void ConfigTimer0(unsigned int ms);
extern void KeyScan();
extern void KeyDrive();
extern void LcdInit();

unsigned char T0RH = 0;  //T0重载值的高字节
unsigned char T0RL = 0;  //T0重载值的低字节

void main(void)
{
    EA = 1;          //开总中断
    ConfigTimer0(1); //配置T0定时1ms
    LcdInit();       //初始化液晶

    while(1)
    {
        KeyDrive();
    }
}

void ConfigTimer0(unsigned int ms)  //T0配置函数
{
    unsigned long tmp;

    tmp = 11059200 / 12;      //定时器计数频率
    tmp = (tmp * ms) / 1000;  //计算所需的计数值
    tmp = 65536 - tmp;        //计算定时器重载值
    tmp = tmp + 18;           //修正中断响应延时造成的误差

    T0RH = (unsigned char)(tmp >> 8);  //定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中断
    TR0 = 1;        //启动T0
}

void InterruptTimer0() interrupt 1  //T0中断服务函数
{
    TH0 = T0RH;  //定时器重新加载重载值
    TL0 = T0RL;
    KeyScan();   //按键扫描
}
        通过这样一个程序,大家一方面学习如何进行多个.c文件编程,另外一个方面学会多个函数之间的灵活调用。可以把这个程序看成是一个简单的小项目,学习一下项目编程都是如何进行和布局的。不要把项目想象的太难,再复杂的项目也是这种简单程序的组合而已。
13.5 串口通信机制和实用的串口例程        我们前边学串口通信的时候,比较注重的是串口底层时序上的操作过程,所以例程都是简单的收发字符或者字符串。在我们实际应用中,往往串口还要和电脑上的上位机软件进行交互,实现电脑软件发送不同的指令,单片机可以对应执行不同的操作,这就要求我们组织一个比较合理的通信机制逻辑关系,用来实现我们想要的结果。
        程序的功能是,通过我们电脑的串口调试助手下发三个不同的命令,第一条指令:buzz on可以让蜂鸣器响;第二条指令:buzz off可以让蜂鸣器不响;第三条指令:showstr ,这个命令空格后边,可以添加任何字符串,让后边的字符串在1602液晶上显示出来,同时不管发送什么命令,单片机收到后把命令原封不动的再通过串口发送给电脑,以表示“我收到了……你可以检查下对不对”。这样的感觉是不是更像是一个小项目了呢?
        对于串口通信部分来说,单片机给电脑发字符串好说,有多大的数组,我们就发送多少个字节即可,但是单片机接收数据,接收多少个才应该是一帧数据呢?数据接收起始头在哪里,结束在哪里?这些我们在接收到数据前都是无从得知的。那怎么办呢?
        我们的编程思路基于这样一种通常的事实:当需要发送一帧(多个字节)数据时,这些数据都是连续不断的发送的,即发送完一个字节后会紧接着发送下一个字节,期间没有间隔或间隔很短,而当这一帧数据都发送完毕后,就会间隔很长一段时间(相对于连续发送时的间隔来讲)不再发送数据,也就是通信总线上会空闲一段较长的时间。于是我们就建立这样一种程序机制:设置一个软件的总线空闲定时器,这个定时器在有数据传输时(从单片机接收角度来说就是接收到数据时)清零,而在总线空闲时(也就是没有接收到数据时)时累加,当它累加到一定时间(例程里是30ms)后,我们就可以认定一帧完整的数据已经传输完毕了,于是告诉其它程序可以来处理数据了,本次的数据处理完后就恢复到初始状态,再准备下一次的接收。那么这个用于判定一帧结束的空闲时间取多少合适呢?它取决于多个条件,并没有一个固定值,我们这里介绍几个需要考虑的原则:第一,这个时间必须大于波特率周期,很明显我们的单片机接收中断产生是在一个字节接收完毕后,也就是一个时刻点,而其接收过程我们的程序是无从知晓的,因此在至少一个波特率周期内你觉不能认为空闲已经时间达到了。第二,要考虑发送方的系统延时,因为不是所有的发送方都能让数据严格无间隔的发送,因为软件响应、关中断、系统临界区等等操作都会引起延时,所以还得再附加几个到十几个ms的时间。我们选取的30ms是一个折中的经验值,它能适应大部分的波特率(大于1200)和大部分的系统延时(PC机或其它单片机系统)情况。
我先把这个程序最重要的UART.c文件中的程序贴出来,一点点给大家解析,这个是实际项目开发常用的用法,大家一定要认真弄明白。

#include <reg52.h>

bit flagOnceTxd = 0;  //单次发送完成标志,即发送完一个字节
bit cmdArrived = 0;  //命令到达标志,即接收到上位机下发的命令
unsigned char cntRxd = 0;
unsigned char pdata bufRxd[40]; //串口接收缓冲区

extern bit flagBuzzOn;

extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);

void ConfigUART(unsigned int baud)  //串口配置函数,baud为波特率
{
    SCON = 0x50;   //配置串口为模式1
    TMOD &= 0x0F;  //清零T1的控制位
    TMOD |= 0x20;  //配置T1为模式2
    TH1 = 256 - (11059200/12/32) / baud;  //计算T1重载值
    TL1 = TH1;     //初值等于重载值
    ET1 = 0;       //禁止T1中断
    ES  = 1;       //使能串口中断
    TR1 = 1;       //启动T1
}
unsigned char UartRead(unsigned char *buf, unsigned char len) //串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度
{
    unsigned char i;

    if (len > cntRxd) //读取长度大于接收到的数据长度时,
    {
        len = cntRxd; //读取长度设置为实际接收到的数据长度
    }
    for (i=0; i<len; i++) //拷贝接收到的数据
    {
        *buf = bufRxd[ i];
        buf++;
    }
    cntRxd = 0;  //清零接收计数器

    return len;  //返回实际读取长度
}
void UartWrite(unsigned char *buf, unsigned char len) //串口数据写入函数,即串口发送函数,待发送数据指针buf,数据长度len
{
    while (len--)
    {
        flagOnceTxd = 0;
        SBUF = *buf;
        buf++;
        while (!flagOnceTxd);
    }
}

bit CmdCompare(unsigned char *buf, const unsigned char *cmd) //命令比较函数,缓冲区数据与指定命令比较,相同返回1,不同返回0
{
    while (*cmd != '\0')
    {
        if (*cmd != *buf) //遇到不相同字符时即刻返回0
        {
            return 0;
        }
        else //当前字符相等时,指针递增准备比较下一字符
        {
            cmd++;
            buf++;
        }
    }
    return 1; //到命令字符串结束时字符都相等则返回1
}
void UartDriver() //串口驱动函数,检测接收到的命令并执行相应动作
{
    unsigned char i;
    unsigned char len;
    unsigned char buf[30];
    const unsigned char code cmd0[] = "buzz on";
    const unsigned char code cmd1[] = "buzz off";
    const unsigned char code cmd2[] = "showstr ";
    const unsigned char code *cmdList[] = {cmd0, cmd1, cmd2};

    if (cmdArrived) //有命令到达时,读取处理该命令
    {
        cmdArrived = 0;
        for (i=0; i<sizeof(buf); i++) //清零命令接收缓冲区
        {
            buf[ i] = 0;
        }
        len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中
        for (i=0; i<sizeof(cmdList)/sizeof(cmdList[0]); i++) //与所支持的命令列表逐一进行比较
        {
            if (CmdCompare(buf, cmdList[ i]) == 1) //检测到相符命令时退出循环,此时的i值就是该命令在列表中的下标值
            {
                break;
            }
        }
        switch (i) //根据比较结果执行相应命令
        {
            case 0:
                flagBuzzOn = 1; //开启蜂鸣器
                break;
            case 1:
                flagBuzzOn = 0; //关闭蜂鸣器
                break;
            case 2:
                buf[len] = '\0'; //为接收到的字符串添加结束符
                i = sizeof(cmd2) - 1;
                LcdShowStr(0, 0, buf+i);  //显示字符串
                i = len - i;              //计算有效字符个数
                if (i < 16) //有效字符少于16时,清楚液晶上的后续字符位
                {
                    LcdAreaClear(i, 0, 16-i);
                }
                break;
            default:  //i大于命令列表最大下标时,即表示没有相符的命令,给上机发送“错误命令”的提示
                UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
                return;
        }
        buf[len++] = '\r';  //有效命令被执行后,在原命令帧之后添加回车换行符后返回给上位机,表示已执行
        buf[len++] = '\n';
        UartWrite(buf, len);
    }
}

void UartRxMonitor(unsigned char ms)  //串口接收监控函数
{
    static unsigned char cntbkp = 0;
    static unsigned char idletmr = 0;

    if (cntRxd > 0)  //接收计数器大于零时,监控总线空闲时间
    {
        if (cntbkp != cntRxd)  //接收计数器改变,即刚接收到数据时,清零空闲计时
        {
            cntbkp = cntRxd;
            idletmr = 0;
        }
        else
        {
            if (idletmr < 30)  //接收计数器未改变,即总线空闲时,累积空闲时间
            {
                idletmr += ms;
                if (idletmr >= 30)  //空闲时间超过30ms即认为一帧命令接收完毕
                {
                    cmdArrived = 1; //设置命令到达标志
                }
            }
        }
    }
    else
    {
        cntbkp = 0;
    }
}
void InterruptUART() interrupt 4  //UART中断服务函数
{
if (RI)  //接收到字节
    {
        RI = 0;   //手动清零接收中断标志位
        if (cntRxd < sizeof(bufRxd)) //接收缓冲区尚未用完时,
        {
            bufRxd[cntRxd++] = SBUF; //保存接收字节,并递增计数器
        }
    }
if (TI)  //字节发送完毕
    {
        TI = 0;   //手动清零发送中断标志位
        flagOnceTxd = 1;  //设置单次发送完成标志
    }
}

        我对照着程序,把重点部分给大家分析一下。
        bit变量flagOnceTxd:单片机接收到串口下发命令后回发给调试助手程序中所用到的变量。
        bit变量cmdArrived:单片机接收完整一帧数据后,通过这个变量指示接收一个命令完毕。
        变量cntRxd:用来记录一帧数据中,实际接收了多少个字节。
        数组bufRxd[40]:用来存放接收到的数据。
        声明外部Bit变量flagBuzzOn:蜂鸣器响和不响的标志位,串口接收指令,让蜂鸣器响,那就flagBuzzOn=1,如果让它不响,那就flagBuzzOn=0。主程序main.c里进行判断,如果是1则蜂鸣器响,如果是0则蜂鸣器不响。
        声明外部函数LcdShowStr:串口接收到让液晶显示字符的程序,调用此函数。
        声明外部函数LcdAreaClear:串口接收到让液晶显示字符的程序后,显示有效字符,后面的液晶显示清空。
        下面对函数逐个进行分析。
        ConfigUART函数不需要多说,配置串口波特率的。
       函数unsigned char UartRead(unsigned char *buf, unsigned char len):
       串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度。当其他函数要调用这个函数的时候,调用之前是不知道串口读到字节长度,所以调用之前定义一个足够大的缓冲数组比如buf[30],调用这个函数后,通过这个函数把串口读到的数据全部复制到buf[30]这个数组中。这里有两个长度,第一个长度是调用UartRead函数的形参len,这个长度用的是buf数组的长度(实际上是30);第二个长度是接收到的字符串的实际长度,UartRead函数不再是void类型,而是unsigned char类型,因此有一个返回值,返回值就是实际接收到的字符串的长度。
        进入这个函数后,我们首先判断一下30是否比接收到的字符串大,如果大的话,则获取实际长度,如果不大于的话,则直接返回30。然后用一个for循环,通过数组元素的指针,把串口接收缓冲区的所有的数据全部传递到调用UartRead的那个函数的数组buf里。最后清掉UART接收数据个数的计数器,返回刚才这一帧的数据长度。
        也许你会说,既然数据都已经接收到bufRxd[40]中了,那我直接从这里面拿出来用不就行了嘛,何必还得再拷贝到另一个地方去呢?我们设计这种双缓冲的机制,主要是为了提高串口接收到响应效率:首先如果你在bufRxd[40]中处理数据,那么这时机不能再接收任何数据,因为新接收的数据会破坏原来的数据,造成其不完整和混乱;其次,你这个处理过程可能会耗费较长的时间,比如说上位机现在就给你发来一个延时显示的命令,那么在这个延时的过程中你都无法去接收新的命令,在上位机看来就是你暂时失去响应了。而使用这种双缓冲机制就可以大大改善这个问题,因为数据拷贝所需的时间是相当短的,而只要拷贝出去后,bufRxd[40]就可以马上准备去接收新数据了。
         函数void UartWrite(unsigned char *buf, unsigned char len) :
        串口数据写入函数,即串口发送函数,待发送数据指针buf,数据长度len。这个函数要和串口中断函数结合来看,每次进入串口中断后,把变量 flagOnceTxd置1,其实相当于把flagOnceTxd认为和TI一样的效果,只是TI是给串口中断用的,而flagOnceTxd是给 UartWrite函数作为标志位用的。当我们接收到电脑下发下来的指令后,我们通过这个函数把指令再返回到电脑串口调试助手。
       函数bit CmdCompare(unsigned char *buf, const unsigned char *cmd) :
       命令比较函数,缓冲区数据与指定命令比较,相同返回1,不同返回0。这是字符串比较函数,传递进来的是2个字符串的指针,因为我们单片机中的命令是完整的字符串格式,都拥有一个结束符——’\0’,而上位机下发的字符串是没有结束符的,所以我们以单片机中的cmd为基准,检测cmd直到遇到’\0’为止,对2个字符串进行比较,如果遇到不同的字符,则返回0,比较到最后都没有发现不同的,则认定相同就返回1。
        函数void UartDriver() :
        串口驱动函数,检测接收到的命令并执行相应动作。这个函数是放在主循环里检测扫描的函数,要讲这个函数之前,先来了解一下指针数组。
        因为指针(指针变量)本身也是个变量,因此用指向同一种数据类型的几个指针来构成一个数组,就形成了指针数组。我们程序之前,只讲了RAM里边的变量的地址,写到FLASH里边的数组以及所有的程序代码,实际上都是有地址的,都可以使用指针访问。那这个函数我们定义到FLASH里三个字符串数组,并且定义了一个指针数组,我写出来,大家重点再认识一下。
const unsigned char code cmd0[] = "buzz on";
const unsigned char code cmd1[] = "buzz off";
const unsigned char code cmd2[] = "showstr ";
const unsigned char code *cmdList[] = {cmd0, cmd1, cmd2};
        我们这个程序,字符串命令数越多,指针数组的优势会越明显,这就是我前边提到的,指针的意义体现在复杂程序上,而且越复杂,指针的优势越明显。
        这个函数判断到了命令到达标志位变1后,首先将命令从串口接收缓冲区中把接收到的数据全部读过来,然后和我们之前协议好的指令一一进行对比。这段程序的技巧,大家要学会。尤其这一句i<sizeof(cmdList)/sizeof(cmdList[0]),是我们求一个数组元素个数的一个常用方法。sizeof(cmdList)是数组所占的总空间大小,sizeof(cmdList[0])是第0个元素所占的空间大小,因为数组元素类型一致,所以每个元素所占空间大小肯定是一样的。我们程序现在用了3个命令,这个位置我们不写成3,而写成这样的表达式,后边如果你想添加更多的命令,只需要添加一个命令数组cmd3[],并把它同时添加到cmdList[]中去就行了,而程序主体中不需要做任何改动,这就是程序易维护性的一种体现!
        读到指令后,根据判断到的指令执行相应的动作,最后在字符串后加个尾巴,再发到串口调试助手,以便于调试助手把一条命令显示为一行,而不是连续的不分行的显示。
        函数:void UartRxMonitor(unsigned char ms):
       这是串口接收的监控程序,我们把它放在了1ms定时器中断里,即每隔1ms调用一次,如果你使用的定时不是1ms,那么只需要修改调用时的参数就行了。这个函数就用来完成我们上面说的通过总线空闲定时器来判定一帧数据接收完毕的任务,所以它必须被不停的固定的间隔来调用。
        剩下的程序相信大家都可以独立研究明白,不懂的多和同学讨论一下,我把剩下的程序贴出来。
/*************************main.c文件程序源代码**************************/

#include <reg52.h>

sbit BUZZ = P1^6;  //蜂鸣器控制引脚

bit flagBuzzOn = 0;   //蜂鸣器启动标志

unsigned char T0RH = 0;  //T0重载值的高字节
unsigned char T0RL = 0;  //T0重载值的低字节

void ConfigTimer0(unsigned int ms);
extern void LcdInit();
extern void ConfigUART(unsigned int baud);
extern void UartRxMonitor(unsigned char ms);
extern void UartDriver();

void main ()
{
    EA = 1;           //开总中断
    ConfigTimer0(1);  //配置T0定时1ms
    ConfigUART(9600); //配置波特率为9600
    LcdInit();        //初始化液晶

    while(1)
    {
        UartDriver();
    }
}

void ConfigTimer0(unsigned int ms)  //T0配置函数
{
    unsigned long tmp;

    tmp = 11059200 / 12;      //定时器计数频率
    tmp = (tmp * ms) / 1000;  //计算所需的计数值
    tmp = 65536 - tmp;        //计算定时器重载值
    tmp = tmp + 18;           //修正中断响应延时造成的误差

    T0RH = (unsigned char)(tmp >> 8);  //定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   //清零T0的控制位
    TMOD |= 0x01;   //配置T0为模式1
    TH0 = T0RH;     //加载T0重载值
    TL0 = T0RL;
    ET0 = 1;        //使能T0中断
    TR0 = 1;        //启动T0
}
void InterruptTimer0() interrupt 1  //T0中断服务函数
{
    TH0 = T0RH;  //定时器重新加载重载值
    TL0 = T0RL;
    if (flagBuzzOn)  //执行蜂鸣器鸣叫或关闭
        BUZZ = ~BUZZ;
    else
        BUZZ = 1;
    UartRxMonitor(1);  //串口接收监控
}
/***********************lcd1602.c文件程序源代码*************************/

#include <reg52.h>

#define LCD1602_DB   P0

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E  = P1^5;

void LcdWaitReady()  //等待液晶准备好
{
    unsigned char sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do
    {
        LCD1602_E = 1;
        sta = LCD1602_DB; //读取状态字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7等于1表示液晶正忙,重复检测直到其等于0为止
}
void LcdWriteCmd(unsigned char cmd)  //写入命令函数
{
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_E  = 1;
    LCD1602_DB = cmd;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  //写入数据函数
{
    LcdWaitReady();
    LCD1602_RS = 1;
    LCD1602_RW = 0;
    LCD1602_DB = dat;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str)  //显示字符串,屏幕起始坐标(x,y),字符串指针str
{
    unsigned char addr;

    //由输入的显示坐标计算显示RAM的地址
    if (y == 0)
        addr = 0x00 + x; //第一行字符地址从0x00起始
    else
        addr = 0x40 + x; //第二行字符地址从0x40起始

    //由起始显示RAM地址连续写入字符串
    LcdWriteCmd(addr | 0x80); //写入起始地址
    while (*str != '\0')      //连续写入字符串数据,直到检测到结束符
    {
        LcdWriteDat(*str);
        str++;
    }
}
void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len)  //区域清除,清除从(x,y)坐标起始的len个字符位
{
    unsigned char addr;

    //由输入的显示坐标计算显示RAM的地址
    if (y == 0)
        addr = 0x00 + x; //第一行字符地址从0x00起始
    else
        addr = 0x40 + x; //第二行字符地址从0x40起始

    //由起始显示RAM地址连续写入字符串
    LcdWriteCmd(addr | 0x80); //写入起始地址
    while (len--)             //连续写入空格
    {
        LcdWriteDat(' ');
    }
}
void LcdInit()  //液晶初始化函数
{
    LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口
    LcdWriteCmd(0x0C);  //显示器开,光标关闭
    LcdWriteCmd(0x06);  //文字不动,地址自动+1
    LcdWriteCmd(0x01);  //清屏
}
        大家是否发现,现在模块化编程后,很多函数,甚至.c文件,如果我们有需要,都可以直接复制添加到我们新的工程中来用,非常方便功能程序移植,这样随着实践积累的增加,你会发现工作效率变得越来越高了。
13.6 作业1、将通信时序的逻辑完全理解透彻,并且能够自己独立看懂其他器件的时序图。
2、根据1602整屏移动程序,改写成右移以及先左移后右移的程序。
3、掌握多.c源文件编写代码的方法以及调用其他文件中变量和函数的用法。
4、彻底理解比较实用的串口通信机制程序,能够完全解析明白实用串口通信例程,为今后自己独立编写类似程序打下基础。

评分

参与人数 1黑币 +3 收起 理由
++___aa + 3 赞一个!

查看全部评分

回复

使用道具 举报

ID:72292 发表于 2015-1-18 19:23 | 显示全部楼层
这么好的章节,竟然没有评论
回复

使用道具 举报

ID:73216 发表于 2015-2-3 18:33 | 显示全部楼层
受教了,51hei有你更精彩
回复

使用道具 举报

ID:43559 发表于 2015-3-4 23:00 | 显示全部楼层
学习了,感谢分享。
回复

使用道具 举报

ID:75246 发表于 2015-3-29 10:43 | 显示全部楼层
老师好,我按照您的例程做了实验,用串口助手没问题,回传的字符串正常显示,可是我自己用易语言写了个类似串口助手的实验程序,发送正常,LCD顺序显示,可是回传到我的软件里显示的字符顺序混款,每次都不一样,您能帮我解答一下吗?如何能让我的易语言程序正确显示您的程序回传的字符串?谢谢老师!
附件在这个网址里 http://www.51hei.com/bbs/dpj-32219-1.html
回复

使用道具 举报

ID:75246 发表于 2015-3-29 13:54 | 显示全部楼层
老师好:
貌似解决了!
去掉那个“处理事件”就没问题了。
谢谢老师!
回复

使用道具 举报

ID:80135 发表于 2015-5-16 07:37 | 显示全部楼层
很精彩,学习了,谢谢楼主!
回复

使用道具 举报

ID:82036 发表于 2015-10-18 16:13 | 显示全部楼层
很精彩,学习了,谢谢楼主!
回复

使用道具 举报

ID:102050 发表于 2016-1-6 11:25 | 显示全部楼层
想问一下 液晶屏上下移动当如何
回复

使用道具 举报

ID:102050 发表于 2016-1-6 11:28 | 显示全部楼层
1602液晶屏 上下移动当如何
回复

使用道具 举报

ID:162190 发表于 2017-3-9 10:01 | 显示全部楼层
本帖最后由 tlone51hei 于 2017-3-22 16:27 编辑

看明白了,需要duo看几遍
回复

使用道具 举报

ID:163931 发表于 2017-3-26 23:35 | 显示全部楼层
不错!
回复

使用道具 举报

ID:204609 发表于 2017-8-8 15:44 | 显示全部楼层
我在做整屏左移方针的时候显示的都是黑格啊
回复

使用道具 举报

ID:204609 发表于 2017-8-9 15:20 | 显示全部楼层
我按照你的实例做出的仿真为什么不显示字符啊 一直都是黑格啊  请老师看一下
回复

使用道具 举报

ID:204609 发表于 2017-8-9 16:06 | 显示全部楼层
不好意思图片不显示 我发一下我帖子的地址:http://www.51hei.com/bbs/dpj-92146-1.html
回复

使用道具 举报

ID:204609 发表于 2017-8-15 14:31 | 显示全部楼层
我发现我真怀疑自己的能力:第一个不显示字符显示黑方块;第二个计算器按键不起作用
回复

使用道具 举报

ID:68429 发表于 2017-10-9 13:58 来自手机 | 显示全部楼层
很好的资料
回复

使用道具 举报

ID:304089 发表于 2018-4-8 23:27 来自手机 | 显示全部楼层
学习了
回复

使用道具 举报

ID:351047 发表于 2018-6-13 13:33 | 显示全部楼层
谢谢楼主
回复

使用道具 举报

ID:93625 发表于 2018-7-16 10:53 | 显示全部楼层
继续学习,充分利用时间。还是要感谢楼主发的教程!
回复

使用道具 举报

ID:618030 发表于 2019-12-1 22:52 | 显示全部楼层
够详细..好样的
回复

使用道具 举报

ID:702974 发表于 2020-3-22 09:45 | 显示全部楼层
对实际工程实施很有用
回复

使用道具 举报

ID:837558 发表于 2020-11-2 23:47 | 显示全部楼层
请问怎么编写一个简单的时钟呢
回复

使用道具 举报

ID:1042173 发表于 2022-9-2 10:10 | 显示全部楼层
这是什么书来的???
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表