通信时序解析


所谓“时序”从字面意义上来理解,一是“时间问题”,二是“顺序问题”。

先说“顺序问题”,这个相对简单一些。我们在学 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 所示。

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


我们用三个箭头来表示 7、8、9 这三次的采集位置,大家可以注意到,当采集到 D7 的时候,已经有一次采集偏出去了,但是我们采集到的数据还是不会错,因为有 2 次采集正确。至于这个偏差允许多大,大家自己可以详细算一下。实际上 UART 通信的波特率是允许一定范围内误差存在的,但是不能过大,否则就会采集错误。大家在计算波特率的时候,发现没有整除,有小数部分的时候,就要特别小心了,因为小数部分是一概被舍掉的,于是计算误差就产生了。我们用 11.0592M 晶振计算的过程中,11059200/12/32/9600 得到的是一个整数,如果用 12M 晶振计算 12000000/12/32/9600 就会得到一个小数,大家可以算一下误差多少,是否在误差范围内。

1602 的时序问题,大家要学会通过 LCD1602 的数据手册提供的时序图和时序参数表格来进行研究,而且看懂时序图是学习单片机所必须掌握的一项技能,如图 13-2 所示。

图 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

大家要善于把手册中的这个表格和时序图结合起来看。表 13-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 的时序参数表已经解析完成了,看完之后,是不是感觉比你想象的要简单,没有你想的那么困难。大家自己也得慢慢学会看这种时序图和表格,在今后的学习中,这方面的能力尤为重要。如果以后换用了其它型号的单片机,那么就根据单片机的执行速度来评估你的程序是否满足时序要求,整体上来说器件都是有一个最快速度的限制,而没有最慢限制,所以当换用高速的单片机后通常都是靠在各步骤间插入软件延时来满足较慢的时序要求。


1602液晶整屏移动程序

我们前边学第 7 章点阵 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 flag500ms = 0; //500ms 定时标志

  • 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 InitLcd1602();

  • void LcdShowStr(unsigned char x, unsigned char y,

  • unsigned char *str, unsigned char len);


  • void main(){

  •     unsigned char i;

  •     unsigned char index = 0; //移动索引

  •     unsigned char pdata bufMove1[16+sizeof(str1)+16]; //移动显示缓冲区 1

  •     unsigned char pdata bufMove2[16+sizeof(str2)+16]; //移动显示缓冲区 2


  •     EA = 1; //开总中断

  •     ConfigTimer0(10); //配置 T0 定时 10ms

  •     InitLcd1602(); //初始化液晶


  •     //缓冲区开头一段填充为空格

  •     for (i=0; i<16; i++){

  •         bufMove1
    = ' ';

  •         bufMove2
    = ' ';

  •     }

  •     //待显示字符串拷贝到缓冲区中间位置

  •     for (i=0; i<(sizeof(str1)-1); i++){

  •         bufMove1[16+i] = str1
    ;

  •         bufMove2[16+i] = str2
    ;

  •     }

  •     //缓冲区结尾一段也填充为空格

  •     for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++){

  •         bufMove1
    = ' ';

  •         bufMove2
    = ' ';

  •     }


  •     while (1){

  •         if (flag500ms){ //每 500ms 移动一次屏幕

  •             flag500ms = 0;

  •             //从缓冲区抽出需显示的一段字符显示到液晶上

  •             LcdShowStr(0, 0, bufMove1+index, 16);

  •             LcdShowStr(0, 1, bufMove2+index, 16);

  •             //移动索引递增,实现左移

  •             index++;

  •             if (index >= (16+sizeof(str1)-1)){

  •                 //起始位置达到字符串尾部后即返回从头开始

  •                 index = 0;

  •             }

  •         }

  •     }

  • }

  • /* 配置并启动 T0,ms-T0 定时时间 */

  • void ConfigTimer0(unsigned int ms){

  •     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;

  •     //bit7 等于 1 表示液晶正忙,重复检测直到其等于 0 为止

  •     }while (sta & 0x80);

  • }

  • /* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */

  • void LcdWriteCmd(unsigned char cmd){

  •     LcdWaitReady();

  •     LCD1602_RS = 0;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = cmd;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */

  • void LcdWriteDat(unsigned char dat){

  •     LcdWaitReady();

  •     LCD1602_RS = 1;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = dat;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */

  • void LcdSetCursor(unsigned char x, unsigned char y){

  •     unsigned char addr;

  •     if (y == 0){ //由输入的屏幕坐标计算显示 RAM 的地址

  •         addr = 0x00 + x; //第一行字符地址从 0x00 起始

  •     }else{

  •         addr = 0x40 + x; //第二行字符地址从 0x40 起始

  •     }

  •     LcdWriteCmd(addr | 0x80); //设置 RAM 地址

  • }

  • /* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,

  • str-字符串指针,len-需显示的字符长度 */

  • void LcdShowStr(unsigned char x, unsigned char y,

  •         unsigned char *str, unsigned char len){

  •     LcdSetCursor(x, y); //设置起始地址

  •     while (len--){ //连续写入 len 个字符数据

  •         LcdWriteDat(*str++); //先取 str 指向的数据,然后 str 自加 1

  •     }

  • }

  • /* 初始化 1602 液晶 */

  • void InitLcd1602(){

  •     LcdWriteCmd(0x38); //16*2 显示,5*7 点阵,8 位数据接口

  •     LcdWriteCmd(0x0C); //显示器开,光标关闭

  •     LcdWriteCmd(0x06); //文字不动,地址自动+1

  •     LcdWriteCmd(0x01); //清屏

  • }

  • /* T0 中断服务函数,定时 500ms */

  • void InterruptTimer0() interrupt 1{

  •     static unsigned char tmr500ms = 0;


  •     TH0 = T0RH; //重新加载重载值

  •     TL0 = T0RL;

  •     tmr500ms++;

  •     if (tmr500ms >= 50){

  •         tmr500ms = 0;

  •         flag500ms = 1;

  •     }

  • }




通过这个程序,大家首先要学会 for 语句在数组中的灵活应用,这个其实在数码管显示有效位的例程中已经有所体现了。其次,随着我们后边程序量的增大,大家得学会多个函数之间相互调用的灵活应用,体会其中的奥妙。


多个.c文件的初步认识

我们上一节的这个液晶滚屏移动程序,大概有 160 行左右。随着我们硬件模块使用的增多,程序量的增大,我们往往要把程序写到多个文件里,方便代码的编写、维护和移植。

比如这个液晶滚屏程序,我们就可以把 1602 底层的功能函数专门写到一个.c 文件内,如LcdWaitReady、LcdWriteCmd、LcdWriteDat、LcdShowStr、LcdSetCursor、InitLcd1602 这些函数,都是属于液晶底层驱动的程序代码,我们要使用液晶功能的时候,只有两个函数对我们实际功能实现部分有用,一个是 InitLcd1602 这个函数,因为需要先初始化液晶,另外一个就是 LcdShowStr 这个函数,我们只需要把要显示的内容通过参数传递给这个函数,这个函数就可以实现我们想要的显示效果,所以我们把这几个底层的液晶驱动程序都放到另外一个文件 Lcd1602.c 文件中,而我们想实现的一些比如滚动实现、中断等上层功能程序全部都放到 main.c 中,但是 main.c 文件如何调用 Lcd1602.c 文件中的函数呢?

C 语言中,有一个 extern 关键字,它有两个基本作用。

1、当一个变量的声明不在文件的开头,在它声明之前的函数想要引用的话,则应该用extern 进行“外部变量”声明。用一个简单的程序给大家介绍一下,知道这么回事,能看懂别人写的就行,自己写就别这么用了。

  • #include <reg52.h>

  • sbit LED = P0^0;

  • 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、InitLcd1602,然后编写 main.c 文件中的功能程序,在编写 main.c文件中程序时,又有对 Lcd1602.c 底层程序的综合调用,这个时候需要 Lcd1602.c 文件提供一个被调用的函数比如 LcdShowStr,我们就可以再到 Lcd1602.c 中把这个函数完成。当然了,这仅仅是一个说明例子而已,顺序完全是没有一个标准的,实际应用中如果对程序逻辑需求了解透彻,根据自己的理解去写程序即可。那我们把 1602 整屏移动的程序改造成为多文件的程序,大家先初步认识一下。

  • /***************************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;

  •     //bit7 等于 1 表示液晶正忙,重复检测直到其等于 0 为止

  •     }while (sta & 0x80);

  • }

  • /* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */

  • void LcdWriteCmd(unsigned char cmd){

  •     LcdWaitReady();

  •     LCD1602_RS = 0;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = cmd;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */

  • void LcdWriteDat(unsigned char dat){

  •     LcdWaitReady();

  •     LCD1602_RS = 1;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = dat;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */

  • void LcdSetCursor(unsigned char x, unsigned char y){

  •     unsigned char addr;

  •     if (y == 0){ //由输入的屏幕坐标计算显示 RAM 的地址

  •         addr = 0x00 + x; //第一行字符地址从 0x00 起始

  •     }else{

  •         addr = 0x40 + x; //第二行字符地址从 0x40 起始

  •     }

  •     LcdWriteCmd(addr | 0x80); //设置 RAM 地址

  • }

  • /* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,

  •     str-字符串指针,len-需显示的字符长度 */

  • void LcdShowStr(unsigned char x, unsigned char y,

  •         unsigned char *str, unsigned char len){

  •     LcdSetCursor(x, y); //设置起始地址

  •     while (len--){ //连续写入 len 个字符数据

  •         LcdWriteDat(*str++);

  •     }

  • }

  • /* 初始化 1602 液晶 */

  • void InitLcd1602(){

  •     LcdWriteCmd(0x38); //16*2 显示,5*7 点阵,8 位数据接口

  •     LcdWriteCmd(0x0C); //显示器开,光标关闭

  •     LcdWriteCmd(0x06); //文字不动,地址自动+1

  •     LcdWriteCmd(0x01); //清屏

  • }






纯文本
复制



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

  • #include <reg52.h>

  • bit flag500ms = 0; //500ms 定时标志

  • 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 InitLcd1602();

  • extern void LcdShowStr(unsigned char x, unsigned char y,

  • unsigned char *str, unsigned char len);


  • void main(){

  •     unsigned char i;

  •     unsigned char index = 0; //移动索引

  •     unsigned char pdata bufMove1[16+sizeof(str1)+16]; //移动显示缓冲区 1

  •     unsigned char pdata bufMove2[16+sizeof(str2)+16]; //移动显示缓冲区 2

  •     EA = 1; //开总中断

  •     ConfigTimer0(10); //配置 T0 定时 10ms

  •     InitLcd1602(); //初始化液晶


  •     //缓冲区开头一段填充为空格

  •     for (i=0; i<16; i++){

  •         bufMove1
    = ' ';

  •         bufMove2
    = ' ';

  •     }

  •     //待显示字符串拷贝到缓冲区中间位置

  •     for (i=0; i<(sizeof(str1)-1); i++){

  •         bufMove1[16+i] = str1
    ;

  •         bufMove2[16+i] = str2
    ;

  •     }

  •     //缓冲区结尾一段也填充为空格

  •     for (i=(16+sizeof(str1)-1); i<sizeof(bufMove1); i++){

  •         bufMove1
    = ' ';

  •         bufMove2
    = ' ';

  •     }


  •     while (1){

  •         if (flag500ms){ //每 500ms 移动一次屏幕

  •             flag500ms = 0;

  •             //从缓冲区抽出需显示的一段字符显示到液晶上

  •             LcdShowStr(0, 0, bufMove1+index, 16);

  •             LcdShowStr(0, 1, bufMove2+index, 16);

  •             //移动索引递增,实现左移

  •             index++;

  •             if (index >= (16+sizeof(str1)-1)){ //起始位置达到字符串尾部后即返回从头开始

  •                 index = 0;

  •             }

  •         }

  •     }

  • }

  • /* 配置并启动 T0,ms-T0 定时时间 */

  • void ConfigTimer0(unsigned int ms){

  •     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

  • }

  • /* T0 中断服务函数,定时 500ms */

  • void InterruptTimer0() interrupt 1{

  •     static unsigned char tmr500ms = 0;

  •     TH0 = T0RH; //重新加载重载值

  •     TL0 = T0RL;

  •     tmr500ms++;

  •     if (tmr500ms >= 50){

  •         tmr500ms = 0;

  •         flag500ms = 1;

  •     }

  • }




我们在 main.c 中要调用 Lcd1602.c 文件中的 InitLcd1602()和 LcdShowStr 这两个函数,只需要在 main.c 中进行 extern 声明即可。大家用 Keil 软件编程试试,真正的感觉一下多.c 源文件的好处。如果这个程序给你的感觉还不深刻,那下面我们来做一个稍微大点的程序来体会一下。


单片机计算器程序设计

按键和液晶,可以组成我们最简易的计算器。下面我们来写一个简易整数计算器提供给大家学习。为了让程序不过于复杂,我们这个计算器不考虑连加,连减等连续计算,不考虑小数情况。加减乘除分别用上下左右来替代,回车表示等于,ESC 表示归 0。程序共分为三部分,一部分是 1602 液晶显示,一部分是按键动作和扫描,一部分是主函数功能。

  • /***************************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;

  •     //bit7 等于 1 表示液晶正忙,重复检测直到其等于 0 为止

  •     }while (sta & 0x80);

  • }

  • /* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */

  • void LcdWriteCmd(unsigned char cmd){

  •     LcdWaitReady();

  •     LCD1602_RS = 0;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = cmd;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */

  • void LcdWriteDat(unsigned char dat){

  •     LcdWaitReady();

  •     LCD1602_RS = 1;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = dat;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */

  • void LcdSetCursor(unsigned char x, unsigned char y){

  •     unsigned char addr;

  •     if (y == 0){ //由输入的屏幕坐标计算显示 RAM 的地址

  •         addr = 0x00 + x; //第一行字符地址从 0x00 起始

  •     }else{

  •         addr = 0x40 + x; //第二行字符地址从 0x40 起始

  •     }

  •     LcdWriteCmd(addr | 0x80); //设置 RAM 地址

  • }

  • /* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */

  • void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str){

  •     LcdSetCursor(x, y); //设置起始地址

  •     while (*str != '\0'){ //连续写入字符串数据,直到检测到结束符

  •         LcdWriteDat(*str++);

  •     }

  • }

  • /* 区域清除,清除从(x,y)坐标起始的 len 个字符位 */

  • void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len){

  •     LcdSetCursor(x, y); //设置起始地址

  •     while (len--){ //连续写入空格

  •         LcdWriteDat(' ');

  •     }

  • }

  • /* 整屏清除 */

  • void LcdFullClear(){

  •     LcdWriteCmd(0x01);

  • }

  • /* 初始化 1602 液晶 */

  • void InitLcd1602(){

  •     LcdWriteCmd(0x38); //16*2 显示,5*7 点阵,8 位数据接口

  •     LcdWriteCmd(0x0C); //显示器开,光标关闭

  •     LcdWriteCmd(0x06); //文字不动,地址自动+1

  •     LcdWriteCmd(0x01); //清屏

  • }




Lcd1602.c 文件中根据上层应用的需要增加了 2 个清屏函数:区域清屏——LcdAreaClear,整屏清屏——LcdFullClear。


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

  • #include <reg52.h>

  • sbit KEY_IN_1 = P2^4;

  • sbit KEY_IN_2 = P2^5;

  • sbit KEY_IN_3 = P2^6;

  • sbit KEY_IN_4 = P2^7;

  • sbit KEY_OUT_1 = P2^3;

  • sbit KEY_OUT_2 = P2^2;

  • sbit KEY_OUT_3 = P2^1;

  • sbit KEY_OUT_4 = P2^0;


  • unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到标准键盘键码的映射表

  •     { '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}

  • };


  • extern void KeyAction(unsigned char keycode);


  • /* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */

  • void KeyDriver(){

  •     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
    [j] != KeySta
    [j]){ //检测按键动作

  •                 if (backup
    [j] != 0){ //按键按下时执行动作

  •                     KeyAction(KeyCodeMap
    [j]); //调用按键动作函数

  •                 }

  •                 backup
    [j] = KeySta
    [j]; //刷新前一次的备份值

  •             }

  •         }

  •     }

  • }

  • /* 按键扫描函数,需在定时中断中调用,推荐调用间隔 1ms */

  • 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]
    & 0x0F) == 0x00){

  •             //连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下

  •             KeySta[keyout]
    = 0;

  •         }else if ((keybuf[keyout]
    & 0x0F) == 0x0F){

  •             //连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起

  •             KeySta[keyout]
    = 1;

  •         }

  •     }


  •     //执行下一次的扫描输出

  •     keyout++; //输出索引递增

  •     keyout &= 0x03; //索引值加到 4 即归零

  •     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;

  •     }

  • }




keyboard.c 是对之前已经用过多次的矩阵按键驱动的封装,具体到某个按键要执行的动作函数都放到上层的 main.c 中实现,在这个按键驱动文件中只负责调用上层实现的按键动作函数即可。
纯文本
复制



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

  • #include <reg52.h>

  • unsigned char step = 0; //操作步骤

  • unsigned char oprt = 0; //运算类型

  • signed long num1 = 0; //操作数 1

  • signed long num2 = 0; //操作数 2

  • signed long result = 0; //运算结果

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

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


  • void ConfigTimer0(unsigned int ms);

  • extern void KeyScan();

  • extern void KeyDriver();

  • extern void InitLcd1602();

  • extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);

  • extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);

  • extern void LcdFullClear();


  • void main(){

  •     EA = 1; //开总中断

  •     ConfigTimer0(1); //配置 T0 定时 1ms

  •     InitLcd1602(); //初始化液晶

  •     LcdShowStr(15, 1, "0"); //初始显示一个数字 0

  •     while (1){

  •         KeyDriver(); //调用按键驱动

  •     }

  • }

  • /* 长整型数转换为字符串,str-字符串指针,dat-待转换数,返回值-字符串长度 */

  • unsigned char LongToString(unsigned char *str, signed long dat){

  •     signed char i = 0;

  •     unsigned char len = 0;

  •     unsigned char buf[12];


  •     if (dat < 0){ //如果为负数,首先取绝对值,并在指针上添加负号

  •         dat = -dat;

  •         *str++ = '-';

  •         len++;

  •     }


  •     do { //先转换为低位在前的十进制数组

  •         buf[i++] = dat % 10;

  •         dat /= 10;

  •     } while (dat > 0);

  •     len += i; //i 最后的值就是有效字符的个数

  •     while (i-- > 0){ //将数组值转换为 ASCII 码反向拷贝到接收指针上

  •         *str++ = buf
    + '0';

  •     }

  •     *str = '\0'; //添加字符串结束符

  •     return len; //返回字符串长度

  • }

  • /* 显示运算符,显示位置 y,运算符类型 type */

  • void ShowOprt(unsigned char y, unsigned char type){

  •     switch (type){

  •         case 0: LcdShowStr(0, y, "+"); break; //0 代表+

  •         case 1: LcdShowStr(0, y, "-"); break; //1 代表-

  •         case 2: LcdShowStr(0, y, "*"); break; //2 代表*

  •         case 3: LcdShowStr(0, y, "/"); break; //3 代表/

  •         default: break;

  •     }

  • }

  • /* 计算器复位,清零变量值,清除屏幕显示 */

  • void Reset(){

  •     num1 = 0;

  •     num2 = 0;

  •     step = 0;

  •     LcdFullClear();

  • }

  • /* 数字键动作函数,n-按键输入的数值 */

  • void NumKeyAction(unsigned char n){

  •     unsigned char len;

  •     unsigned char str[12];


  •     if (step > 1){ //如计算已完成,则重新开始新的计算

  •         Reset();

  •     }

  •     if (step == 0){ //输入第一操作数

  •         num1 = num1*10 + n; //输入数值累加到原操作数上

  •         len = LongToString(str, num1); //新数值转换为字符串

  •         LcdShowStr(16-len, 1, str); //显示到液晶第二行上

  •     }else{ //输入第二操作数

  •         num2 = num2*10 + n; //输入数值累加到原操作数上

  •         len = LongToString(str, num2); //新数值转换为字符串

  •         LcdShowStr(16-len, 1, str); //显示到液晶第二行上

  •     }

  • }

  • /* 运算符按键动作函数,运算符类型 type */

  • void OprtKeyAction(unsigned char type){

  •     unsigned char len;

  •     unsigned char str[12];


  •     if (step == 0){ //第二操作数尚未输入时响应,即不支持连续操作

  •         len = LongToString(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 = LongToString(str, num2); //原第二操作数和运算符显示到第一行

  •         ShowOprt(0, oprt);

  •         LcdAreaClear(1, 0, 16-1-len);

  •         LcdShowStr(16-len, 0, str);

  •         len = LongToString(str, result); //计算结果和等号显示在第二行

  •         LcdShowStr(0, 1, "=");

  •         LcdAreaClear(1, 1, 16-1-len);

  •         LcdShowStr(16-len, 1, str);

  •     }

  • }

  • /* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */

  • 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");

  •     }

  • }

  • /* 配置并启动 T0,ms-T0 定时时间 */

  • void ConfigTimer0(unsigned int ms){

  •     unsigned long tmp; //临时变量

  •     tmp = 11059200 / 12; //定时器计数频率

  •     tmp = (tmp * ms) / 1000; //计算所需的计数值

  •     tmp = 65536 - tmp; //计算定时器重载值

  •     tmp = tmp + 28; //补偿中断响应延时造成的误差

  •     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

  • }

  • /* T0 中断服务函数,执行按键扫描 */

  • void InterruptTimer0() interrupt 1{

  •     TH0 = T0RH; //重新加载重载值

  •     TL0 = T0RL;

  •     KeyScan(); //按键扫描

  • }




main.c 文件实现所有应用层的操作函数,即计算器功能所需要信息显示、按键动作响应等,另外还包括主循环和定时中断的调度。

通过这样一个程序,大家一方面学习如何进行多个.c 文件的编程,另外一个方面学会多个函数之间的灵活调用。可以把这个程序看成是一个简单的小项目,学习一下项目编程都是如何进行和布局的。不要把项目想象的太难,再复杂的项目也是这种简单程序的组合和扩展而已。


单片机串口通信原理和控制程序

我们前边学串口通信的时候,比较注重的是串口底层时序上的操作过程,所以例程都是简单的收发字符或者字符串。在实际应用中,往往串口还要和电脑上的上位机软件进行交互,实现电脑软件发送不同的指令,单片机对应执行不同操作的功能,这就要求我们组织一个比较合理的通信机制和逻辑关系,用来实现我们想要的结果。

本节所提供程序的功能是,通过电脑串口调试助手下发三个不同的命令,第一条指令:buzz on 可以让蜂鸣器响;第二条指令:buzz off 可以让蜂鸣器不响;第三条指令:showstr ,这个命令空格后边,可以添加任何字符串,让后边的字符串在 1602 液晶上显示出来,同时不管发送什么命令,单片机收到后把命令原封不动的再通过串口发送给电脑,以表示“我收到了„„你可以检查下对不对”。这样的感觉是不是更像是一个小项目了呢?

对于串口通信部分来说,单片机给电脑发字符串好说,有多大的数组,我们就发送多少个字节即可,但是单片机接收数据,接收多少个才应该是一帧完整的数据呢?数据接收起始头在哪里,结束在哪里?这些我们在接收到数据前都是无从得知的。那怎么办呢?

我们的编程思路基于这样一种通常的事实:当需要发送一帧(多个字节)数据时,这些数据都是连续不断的发送的,即发送完一个字节后会紧接着发送下一个字节,期间没有间隔或间隔很短,而当这一帧数据都发送完毕后,就会间隔很长一段时间(相对于连续发送时的间隔来讲)不再发送数据,也就是通信总线上会空闲一段较长的时间。于是我们就建立这样一种程序机制:设置一个软件的总线空闲定时器,这个定时器在有数据传输时(从单片机接收角度来说就是接收到数据时)清零,而在总线空闲时(也就是没有接收到数据时)时累加,当它累加到一定时间(例程里是 30ms)后,我们就可以认定一帧完整的数据已经传输完毕了,于是告诉其它程序可以来处理数据了,本次的数据处理完后就恢复到初始状态,再准备下一次的接收。那么这个用于判定一帧结束的空闲时间取多少合适呢?它取决于多个条件,并没有一个固定值,我们这里介绍几个需要考虑的原则:第一,这个时间必须大于波特率周期,很明显我们的单片机接收中断产生是在一个字节接收完毕后,也就是一个时刻点,而其接收过程我们的程序是无从知晓的,因此在至少一个波特率周期内你绝不能认为空闲已经时间达到了。第二,要考虑发送方的系统延时,因为不是所有的发送方都能让数据严格无间隔的发送,因为软件响应、关中断、系统临界区等等操作都会引起延时,所以还得再附加几个到十几个 ms 的时间。我们选取的 30ms 是一个折中的经验值,它能适应大部分的波特率(大于1200)和大部分的系统延时(PC 机或其它单片机系统)情况。

我先把这个程序最重要的 UART.c 文件中的程序贴出来,一点点给大家解析,这个是实际项目开发常用的用法,大家一定要认真弄明白。

  • /*****************************Uart.c 文件程序源代码*****************************/

  • #include <reg52.h>


  • bit flagFrame = 0; //帧接收完成标志,即接收到一帧新数据

  • bit flagTxd = 0; //单字节发送完成标志,用来替代 TXD 中断标志位

  • unsigned char cntRxd = 0; //接收字节计数器

  • unsigned char pdata bufRxd[64]; //接收字节缓冲区


  • extern void UartAction(unsigned char *buf, unsigned char len);


  • /* 串口配置函数,baud-通信波特率 */

  • void ConfigUART(unsigned int 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

  • }

  • /* 串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定的发送长度 */

  • void UartWrite(unsigned char *buf, unsigned char len){

  •     while (len--){ //循环发送所有字节

  •         flagTxd = 0; //清零发送标志

  •         SBUF = *buf++; //发送一个字节数据

  •         while (!flagTxd); //等待该字节发送完成

  •     }

  • }

  • /* 串口数据读取函数,buf-接收指针,len-指定的读取长度,返回值-实际读到的长度 */

  • unsigned char UartRead(unsigned char *buf, unsigned char len){

  •     unsigned char i;

  •     //指定读取长度大于实际接收到的数据长度时,

  •     //读取长度设置为实际接收到的数据长度

  •     if (len > cntRxd){

  •         len = cntRxd;

  •     }

  •     for (i=0; i<len; i++){ //拷贝接收到的数据到接收指针上

  •         *buf++ = bufRxd
    ;

  •     }

  •     cntRxd = 0; //接收计数器清零

  •     return len; //返回实际读取长度

  • }

  • /* 串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔 */

  • 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){ //空闲计时小于 30ms 时,持续累加

  •                 idletmr += ms;

  •                 if (idletmr >= 30){ //空闲时间达到 30ms 时,即判定为一帧接收完毕

  •                     flagFrame = 1; //设置帧接收完成标志

  •                 }

  •             }

  •         }

  •     }else{

  •         cntbkp = 0;

  •     }

  • }

  • /* 串口驱动函数,监测数据帧的接收,调度功能函数,需在主循环中调用 */

  • void UartDriver(){

  •     unsigned char len;

  •     unsigned char pdata buf[40];


  •     if (flagFrame){ //有命令到达时,读取处理该命令

  •         flagFrame = 0;

  •         len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中

  •         UartAction(buf, len); //传递数据帧,调用动作执行函数

  •     }

  • }

  • /* 串口中断服务函数 */

  • void InterruptUART() interrupt 4{

  •     if (RI){ //接收到新字节

  •         RI = 0; //清零接收中断标志位

  •         //接收缓冲区尚未用完时,保存接收字节,并递增计数器

  •         if (cntRxd < sizeof(bufRxd)){{

  •             bufRxd[cntRxd++] = SBUF;

  •         }

  •     }

  •     if (TI){ //字节发送完毕

  •         TI = 0; //清零发送中断标志位

  •         flagTxd = 1; //设置字节发送完成标志

  •     }

  • }




大家可以对照注释和前面的讲解分析下这个 Uart.c 文件,在这里指出其中的两个要点希望大家多注意下。

1、接收数据的处理,在串口中断中,将接收到的字节都存入缓冲区 bufRxd 中,同时利用另外的定时器中断通过间隔调用 UartRxMonitor 来监控一帧数据是否接收完毕,判定的原则就是我们前面介绍的空闲时间,当判定一帧数据结束完毕时,设置 flagFrame 标志,主循环中可以通过调用 UartDriver 来检测该标志,并处理接收到的数据。当要处理接收到的数据时,先通过串口读取函数 UartRead 把接收缓冲区 bufRxd 中的数据读取出来,然后再对读到的数据进行判断处理。也许你会说,既然数据都已经接收到 bufRxd 中了,那我直接在这里面用不就行了嘛,何必还得再拷贝到另一个地方去呢?我们设计这种双缓冲的机制,主要是为了提高串口接收到响应效率:首先如果你在 bufRxd 中处理数据,那么这时侯就不能再接收任何数据,因为新接收的数据会破坏原来的数据,造成其不完整和混乱;其次,这个处理过程可能会耗费较长的时间,比如说上位机现在就给你发来一个延时显示的命令,那么在这个延时的过程中你都无法去接收新的命令,在上位机看来就是你暂时失去响应了。而使用这种双缓冲机制就可以大大改善这个问题,因为数据拷贝所需的时间是相当短的,而只要拷贝出去后,bufRxd 就可以马上准备去接收新数据了。

2、串口数据写入函数 UartWrite,它把数据指针 buf 指向的数据块连续的由串口发送出去。虽然我们的串口程序启用了中断,但这里的发送功能却没有在中断中完成,而是仍然靠查询发送中断标志 flagTxd(因中断函数内必须清零 TI,否则中断会重复进入执行,所以另置了一个 flagTxd 来代替 TI)来完成,当然也可以采用先把发送数据拷贝到一个缓冲区中,然后再在中断中发缓冲区数据发送出去的方式,但这样一是要耗费额外的内存,二是使程序更复杂。这里也还是想告诉大家,简单方式可以解决的问题就不要搞得更复杂。

  • /*****************************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 UartDriver();

  • extern void ConfigUART(unsigned int baud);

  • extern void UartRxMonitor(unsigned char ms);

  • extern void UartWrite(unsigned char *buf, unsigned char len);

  • extern void InitLcd1602();

  • extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);

  • extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);


  • void main(){

  •     EA = 1; //开总中断

  •     ConfigTimer0(1); //配置 T0 定时 1ms

  •     ConfigUART(9600); //配置波特率为 9600

  •     InitLcd1602(); //初始化液晶


  •     while (1){

  •         UartDriver(); //调用串口驱动

  •     }

  • }

  • /* 内存比较函数,比较两个指针所指向的内存数据是否相同,

  • ptr1-待比较指针 1,ptr2-待比较指针 2,len-待比较长度

  • 返回值-两段内存数据完全相同时返回 1,不同返回 0 */

  • bit CmpMemory(unsigned char *ptr1, unsigned char *ptr2, unsigned char len){

  •     while (len--){

  •         if (*ptr1++ != *ptr2++){ //遇到不相等数据时即刻返回 0

  •             return 0;

  •         }

  •     }

  •     return 1; //比较完全部长度数据都相等则返回 1

  • }

  • /* 串口动作函数,根据接收到的命令帧执行响应的动作

  • buf-接收到的命令帧指针,len-命令帧长度 */

  • void UartAction(unsigned char *buf, unsigned char len){

  •     unsigned char i;

  •     unsigned char code cmd0[] = "buzz on"; //开蜂鸣器命令

  •     unsigned char code cmd1[] = "buzz off"; //关蜂鸣器命令

  •     unsigned char code cmd2[] = "showstr "; //字符串显示命令

  •     unsigned char code cmdLen[] = { //命令长度汇总表

  •         sizeof(cmd0)-1, sizeof(cmd1)-1, sizeof(cmd2)-1,

  •     };


  •     unsigned char code *cmdPtr[] = { //命令指针汇总表

  •         &cmd0[0], &cmd1[0], &cmd2[0],

  •     };


  •     for (i=0; i<sizeof(cmdLen); i++){ //遍历命令列表,查找相同命令

  •         if (len >= cmdLen
    ){ //首先接收到的数据长度要不小于命令长度

  •             if (CmpMemory(buf, cmdPtr
    , cmdLen
    )){ //比较相同时退出循环

  •                 break;

  •             }

  •         }

  •     }

  •     switch (i){ //循环退出时 i 的值即是当前命令的索引值

  •         case 0:

  •             flagBuzzOn = 1; //开启蜂鸣器

  •             break;

  •         case 1:

  •             flagBuzzOn = 0; //关闭蜂鸣器

  •             break;

  •         case 2:

  •             buf[len] = '\0'; //为接收到的字符串添加结束符

  •             LcdShowStr(0, 0, buf+cmdLen[2]); //显示命令后的字符串

  •             i = len - cmdLen[2]; //计算有效字符个数

  •             if (i < 16){ //有效字符少于 16 时,清除液晶上的后续字符位

  •                 LcdAreaClear(i, 0, 16-i);

  •             }

  •             break;

  •         default: //未找到相符命令时,给上机发送“错误命令”的提示

  •             UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);

  •             return;

  •     }

  •     buf[len++] = '\r'; //有效命令被执行后,在原命令帧之后添加

  •     buf[len++] = '\n'; //回车换行符后返回给上位机,表示已执行

  •     UartWrite(buf, len);

  • }

  • /* 配置并启动 T0,ms-T0 定时时间 */

  • void ConfigTimer0(unsigned int ms){

  •     unsigned long tmp; //临时变量

  •     tmp = 11059200 / 12; //定时器计数频率

  •     tmp = (tmp * ms) / 1000; //计算所需的计数值

  •     tmp = 65536 - tmp; //计算定时器重载值

  •     tmp = tmp + 33; //补偿中断响应延时造成的误差

  •     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

  • }

  • /* T0 中断服务函数,执行串口接收监控和蜂鸣器驱动 */

  • void InterruptTimer0() interrupt 1{

  •     TH0 = T0RH; //重新加载重载值

  •     TL0 = T0RL;

  •     if (flagBuzzOn){ //执行蜂鸣器鸣叫或关闭

  •         BUZZ = ~BUZZ;

  •     }else{

  •         BUZZ = 1;

  •     }

  •     UartRxMonitor(1); //串口接收监控

  • }




main 函数和主循环的结构我们已经做过很多了,就不多说了,这里重点把串口接收数据的具体解析方法给大家分析一下,这种用法具有很强的普遍性,掌握并灵活运用它可以使你将来的开发工作事半功倍。

首先来看 CmpMemory 函数,这个函数很简单,就是比较两段内存数据,通常都是数组中的数据,函数接收两段数据的指针,然后逐个字节比较——if (*ptr1++ != *ptr2++),这行代码既完成了两个指针指向的数据的比较,又在比较完后把两个指针都各自+1,从这里是不是也能领略到一点 C 语言的简洁高效的魅力呢。这个函数的用处自然就是用来比较我们接收到的数据和事先放在程序里的命令字符串是否相同,从而找出相符的命令了。

接下来是 UartAction 函数对接收数据的解析和处理方法,先把接收的数据与所支持的命令字符串逐条比较,这个比较中首先要确保接收的长度大于命令字符串的长度,然后再用上述的 CmpMemory 函数逐字节比较,如果比较相同就立即退出循环,不同则继续对比下一条命令。当找到相符的命令字符串时,最终 i 的值就是该命令在其列表中的索引位置,当遍历完命令列表都没有找到相符的命令时,最终 i 的值将等于命令总数,那么接下来就用 switch语句根据 i 的值来执行具体的动作,这个就不需要再详细说明了。

  • /***************************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 为止

  • }

  • /* 向 LCD1602 液晶写入一字节命令,cmd-待写入命令值 */

  • void LcdWriteCmd(unsigned char cmd){

  •     LcdWaitReady();

  •     LCD1602_RS = 0;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = cmd;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 向 LCD1602 液晶写入一字节数据,dat-待写入数据值 */

  • void LcdWriteDat(unsigned char dat){

  •     LcdWaitReady();

  •     LCD1602_RS = 1;

  •     LCD1602_RW = 0;

  •     LCD1602_DB = dat;

  •     LCD1602_E = 1;

  •     LCD1602_E = 0;

  • }

  • /* 设置显示 RAM 起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标 */

  • void LcdSetCursor(unsigned char x, unsigned char y){

  •     unsigned char addr;

  •     if (y == 0){ //由输入的屏幕坐标计算显示 RAM 的地址

  •         addr = 0x00 + x; //第一行字符地址从 0x00 起始

  •     }else{

  •         addr = 0x40 + x; //第二行字符地址从 0x40 起始

  •     }

  •     LcdWriteCmd(addr | 0x80); //设置 RAM 地址

  • }

  • /* 在液晶上显示字符串,(x,y)-对应屏幕上的起始坐标,str-字符串指针 */

  • void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str){

  •     LcdSetCursor(x, y); //设置起始地址

  •     while (*str != '\0'){ //连续写入字符串数据,直到检测到结束符

  •         LcdWriteDat(*str++);

  •     }

  • }

  • /* 区域清除,清除从(x,y)坐标起始的 len 个字符位 */

  • void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len){

  •     LcdSetCursor(x, y); //设置起始地址

  •     while (len--){ //连续写入空格

  •         LcdWriteDat(' ');

  •     }

  • }

  • /* 初始化 1602 液晶 */

  • void InitLcd1602(){

  •     LcdWriteCmd(0x38); //16*2 显示,5*7 点阵,8 位数据接口

  •     LcdWriteCmd(0x0C); //显示器开,光标关闭

  •     LcdWriteCmd(0x06); //文字不动,地址自动+1

  •     LcdWriteCmd(0x01); //清屏

  • }




液晶文件与上一个例程的液晶文件基本是一样的,唯一的区别是删掉了一个本例中用不到的全屏清屏函数,其实留着这个函数也没关系,只是 Keil 会提示一个警告,告诉你有未被调用的函数而已,可以不理会它。

经过这几个多文件工程的练习后,大家是否发现,在采用多文件模块化编程后,不光是某些函数,甚至整个 c 文件,如有需要,我们都可以直接复制到其它的新工程中使用,非常方便功能程序的移植,这样随着实践积累的增加,你会发现工作效率变得越来越高了。