写在前面

  这篇博客主要记录下单片机是如何通过TXD、RXD与上位机进行数据交换的。
  先介绍下51单片机中与串口通信有关的各种寄存器。
  首先,上位机如果要发送数据给单片机,单片机接收到数据之后,会存入到SBUF这个发送/接收寄存器,这个寄存器非常特殊,兼具发送和接收时存放数据的功能。如果是data = SBUF,则会把SBUF接收到上位机发送过来的数据存入到data中;如果是SBUF = data,则会把单片机想要发送的数据即data中的数据送入到SBUF中,然后再通过串口发送到上位机。
  在接收数据时,单片机会产生中断,不然单片机不知道什么时候接收完一位数据,这个中断叫做串口中断,服务程序是interrupt4,标志位是RI,所以进入串口中断服务程序时一定要记得把RI清零,不然程序就会一直进入串口中断服务程序。控制串口中断的寄存器叫SCON,它的每一位如下:
  
  SM0、SM1这两位与TMOD中控制定时器0、1的M0、M1类似,SM0和SM1是用来控制串口工作方式的。通过改变SM0、SM1的值可以让串行口工作在4种方式:

SM0 SM1 波特率
0 0 fosc/12(主振频率/12)
0 1 可变
1 0 fosc/32(主振频率/36)、fosc/64
1 1 可变

  这里关于4种工作方式如果展开了讲的话,实在太庞大,所以读者如果有疑惑可以自行百度。这里解释一下用定时器产生固定波特率的问题,我翻看了很多其他同学写的博客,发现他们有很多都不清楚方式1和3中为啥要给TH1、TL1(这里用定时器1举例)一个固定的初值。其实这个固定的初值是很多其他前辈算出来的初值,如果要自己计算也是完全可以的,公式如下(戴胜华教授《单片机原理与应用》):
  
  上式中出现到SMOD1,这一位是由电源寄存器PCON的第七位来控制的,假设我们规定好串行口工作在方式1或3的一个初值,原本波特率为9600,SMOD置1后,波特率翻倍,会变为19200,,很好理解。
  注意注意:这里的串行口工作方式0123跟定时器工作方式0123不是同一个东西,一定要分开。
  串行口工作方式为0123任意一种都能通过使用定时器1工作在方式2来产生相应的波特率。
  这里给出常用波特率相对应的定时器初值:
  
  举例假如我要让串口产生9600的波特率,我使用串口工作方式1、3,定时器1工作在方式2,那么公式就等于 波特率(9600)=2^0/32 * fosc/12 * 1/(2^k-初值) ,这里我们晶振频率为11.0592MHz,波特率为9600Hz,注意单位转换,K是计数器的计数位数,定时器方式2为八位自动重装,所以K=8,那么等式就变为了 9600 = 1/32 * 11059200/12 * 1/(256-初值) ,化简一下 1/3 = 1/256-初值,那么初值就要为253,十六进制则是0xFD,则定时器每次放进TL1的初值就要为0xFD,这样就能产生9600的波特率。
  再解释一下为什么定时器中的高八位和低八位相同,以定时器1举例,如果定时器1工作在方式2即八位自动重装模式,会用低八位TL1来计数,用高八位来保存计数初值。TL1计数回到0时自动将TH1中的初值送回TL1中,完成自动重装。
  回到正题,SCON中REN这一位为允许接收控制位,置0则禁止串口接收数据,置1则反之。TB8和RB8是用于方式2和3中发送和接收数据的第9位,我这里不在过多解释,需要用的时候,再仔细百度。TI是发送中断标志位,发送完毕会自动置1,在发送数据前一定要先清零TI,发送完后可根据TI来判断是否发送完毕。RI则是接收中断标志位,可以根据RI的值来判断单片机是否接受完上位机发送来的数据。
  总结一下,串口发送和接收涉及到的寄存器相应的位有:PCON中的SMOD,SCON中的SM0、SM1、REN、TI、RI,TH0、TL0(TH1、TL1),TMOD中的M1、M0(控制定时器的工作方式),IE中的EA、ES(允许总中断、允许串口中断),TCON中的TR0(TR1)。
  单片机接收上位机数据工作过程大致为:定时器产生一定波特率——单片机与上位机通过TXD、RXD开始通信——单片机允许串口中断,允许接收数据——单片机接收到数据,进入串口中断服务程序,并将RI置1,软件将RI清零,读取SBUF。
  单片机发送数据到上位机工作过程大致为:定时器产生一定波特率——单片机与上位机通过TXD、RXD开始通信——单片机赋值给TI——单片机发送数据给上位机——上位机接收到数据。
  下面这两段程序是在郭天祥《新概念51单片机C语言教程》以及参考其他同学的博客写的。
  郭天祥:

#include<reg52.h>
typedef unsigned char uint8;
typedef unsigned int uint16;uint8 flag,a,i;
uint8 code table[]="I get ";void init(){TMOD = 0x20;            //定时器1工作在方式2,八位自动重装TH1 = 0xfd;TL1 = 0xfd;TR1 = 1;             //开启定时器1SM0 = 0;               SM1 = 1;               //串口工作方式1REN = 1;              //接收允许EA = 1;                  //开总中断ES = 1;                  //开串口中断
}void main(){init();while(1){if(flag){ES = 0;              //暂时关闭串口中断,防止在处理数据时再次发生串口中断for(i=0;i<6;i++){SBUF=table[i];    //将I get放入发送寄存器while(!TI);      //检测是否发送完毕,发送完毕后自动置1TI=0;           //将发送完毕的标志位清零}SBUF=a;              //将接受到的值发回给主机while(!TI);            TI=0;ES=1;                //重新打开串口中断flag=0;}}
}
void ser()interrupt 4{          //串口中断服务程序RI = 0;                      //中断标志位a = SBUF;                   //将接收到的数据存入a中flag=1;
}

  结合按键,按一下发送一行字符:

#include <reg51.h>
typedef unsigned char uint8;
typedef unsigned int uint16;
#define key_state0 0
#define key_state1 1
#define key_state2 2
sbit key = P3^2;
uint8 key_value;
bit flag;uint8 Buf[]="hello world!\n";void delay(uint16 n)
{while (n--);
}/*波特率为9600*/
void UART_init(void)
{SCON = 0x50;        //串口方式1TMOD = 0x21;        //定时器1使用方式2自动重载,定时器0用作按键扫描TH1 = 0xFD;         //9600波特率对应的预设数,定时器方式2下,TH1=TL1TL1 = 0xFD;TH0 = 0x4C;          //50msTL0 = 0x00;TR1 = 1;         //开启定时器,开始产生波特率TR0 = 1;ET0 = 1;EA  = 1;
}/*发送一个字符*/
void UART_send_byte(uint8 dat)
{SBUF = dat;           //把数据放到SBUF中while (TI == 0);  //未发送完毕就等待TI = 0;              //发送完毕后,要把TI重新置0
}/*发送一个字符串*/
void UART_send_string(uint8 *buf)
{while (*buf != '\0'){UART_send_byte(*buf++);}
}void scankey(){static uint8 key_state;switch(key_state){case key_state0:if(!key) key_state = key_state1;break;case key_state1:if(!key){UART_send_string(Buf);delay(20000);key_state = key_state2;}else{key_state = key_state0;}break;case key_state2:if(key){key_state = key_state0;}break;default:break;      }
} void main()
{UART_init();while(1){if(flag){scankey();}}
}void timer0_isr() interrupt 1 using 0{TH0 = 0xDC;         //10msTL0 = 0x00;flag = 1;
}

  上面郭天祥那段代码中,只接收了上位机发送的一位数据,我又花了点时间改出了一段程序,分别是可以接受多位数据以及根据上位机送来的数据控制流水灯,两段代码就综合到一起了,注释部分是接收多位数据。

#include <reg52.h>
#define key_state0 0
#define key_state1 1
#define key_state2 2
typedef unsigned char uint8;
typedef unsigned int uint16;sbit key = P3^2;
// uint8 table[8];
uint8 key_value;
uint8 flag,i,dat;
bit flag1;                              //控制是否开始流水
//uint8 num;            void init(){            TMOD = 0x21;                       //定时器1工作在方式2,八位自动重装TH1 = 0xfd;          TL1 = 0xfd;            TR1 = 0xfd;                            //开启定时器1TH0 = 0x4C;                            //50msTL0 = 0x00;          TR0 = 1;           ET0 = 1;           SM0 = 0;                           SM1 = 1;                           //串口工作方式1EA = 1;                               //开总中断ES = 1;                              //开串口中断
}void scankey(){static uint8 key_state;switch(key_state){case key_state0:if(!key) key_state = key_state1;break;case key_state1:if(!key){REN = ~REN;               //允许/禁止接收上位机数据key_state = key_state2;}else{key_state = key_state0;}break;case key_state2:if(key){key_state = key_state0;}break;default:break;        }
}void main(){init();P1 = 0xff;while(1){if(!REN) P1 = 0xff;                //不接收上位机数据时,关闭所有灯if(flag){ES = 0;                       //暂时关闭串口中断,防止在处理数据时再次发生串口中断// for(i=0;i<8;i++){        //回传多位数据// SBUF=table[i];      //发送一位// while(!TI);            //检测是否发送完毕,发送完毕后自动置1// TI=0;                //将发送完毕的标志位清零// }SBUF = dat;while(!TI);TI = 0;ES=1;                      //重新打开串口中断flag=0;  //num=0;                   //清零接收计数}   }
}
void ser()interrupt 4{                  //串口中断服务程序if(RI){   RI = 0;                            //中断标志位//table[num++] = SBUF;    dat = SBUF;                        //将接收到的数据存入dat中P1 = SBUF;                      //将收到的16进制数赋给P1//if(num == 8)                     //收满8位数据,开始回传flag=1;}
}void timer0_isr() interrupt 1 using 0{TH0 = 0xDC;         //10msTL0 = 0x00;scankey();
}

  目前程序中我觉得不足的地方是发送代码中的while(!TI),这里会把单片机一直占用住,按照之前按键扫描延时尽量不用delay的惯例,这里的while等待我觉得也有不妥,但是不确定是不是我自己想多了,还需要以后深入学习才能得出结论。
  如有错误,欢迎评论指正,本人也是边学边总结,一方面检验自己是否真的理解,另一方面如有错误理解也能及时发现及时改正。

2020/3/19日补充:
  为什么串口的波特率与定时器有关?
  最近再次看回这篇博客不禁思考,这两者有什么联系吗?为什么要用定时器1来控制波特率为什么不能用定时器2,百度了一下发现原来51单片机串口的波特率是与定时器1的溢出率有关,这一点在上面计算波特率的时候的公式里面有体现。定时器的溢出率顾名思义就是与定时器的溢出速率有关,大概意思可能是定时器溢出一次的时间,那么晶振频率如果为11.0592MHz,时钟周期就是1/11.0592,机器周期为12/11.0592,则单片机定时器+1的时间为12/11.0592us,溢出率=溢出一次的时间=计数次数*机器周期,所以通过改变定时器的初值就能改变定时器的溢出率,也就改变了串口的波特率。
  波特率这里也顺带解释一下,就是串口每秒能接受的比特数bit,因为串口是一位一位数据按顺序发送,波特率9600就是串口每秒钟能接收的bit数为9600位,如果上位机的波特率大于9600,那么通信就会失败,因为单片机来不及接收这么多的数据量。所以串口通信要求上下位机的波特率要一致,才能保证数据传送不出错。

单片机与上位机的串行通信相关推荐

  1. proteus中使用虚拟串口实现单片机和上位机通讯

    祝大家身体健康哈,肺炎愈来愈多,希望看到这篇文章的旁友都能健健康康! 今天写一下proteus里如何使用虚拟串口仿真单片机和上位机通讯,所需要的软件有:(1)Virtual Serial Port D ...

  2. 单片机的上位机简单开发(1)

    单片机的上位机简单开发(1) 使用的上位机开发工具为Visual Studio 2019 1.界面设计 1.1创建应用 1.2 控件 在Form1.cs(设计)界面下,点右边点击工具箱,找到Label ...

  3. 单片机的上位机简单开发(4)

    单片机的上位机简单开发(4) 界面设计 增加了外部的自定义温度插件,chart图表插件 1.自定义温度插件 打开项目,右键单击工具箱中任意一个控件,弹出右键菜单如下: 单击"选择项" ...

  4. 【上位机与下位机通信】使用WIFI模块ESP8266连接单片机与上位机通信

    文章目录 前言 一.ESP8266模块与STM32连接 二.单片机代码 三.总结 前言 承接上文WIFI上位机部分:[上位机]通过WIFI上位机与网络调试助手通信绘制曲线,现阶段实现了STM32单片机 ...

  5. 安卓wifi调试助手(单片机wifi上位机)

    例程 在网上找例程 最好找个日期新一点的, 因为,太老的工程,不容易编译过. Android WIFI调试助手源码2.0 这个例程,可以很方便地改装成 wifi 开发板(或wifi相关产品) 的上位机 ...

  6. 编程(代码、软件)规范(适用嵌入式、单片机、上位机等)

    目录 前言 第1章 文件 1.1 头文件 1.2 定义文件 第2章 注释规范 2.1 共性注释规范 2.2 文档注释规范 2.3 C语言风格注释规范 第3章 排版规范 3.1 缩进与对齐 风格 3.2 ...

  7. 英飞凌TC264D单片机——匿名上位机蓝牙串口发送通信协议

    代码参照匿名通信协议b站教程完成 Blutetooth.h #ifndef SRC_APPSW_TRICORE_USER_BLUETOOTH_H_ #define SRC_APPSW_TRICORE_ ...

  8. 单板计算机作用上位机,SCB-1单板机的基本操作

    SCB-1单板机的基本操作 1.          键盘操作 SCB-1单板机键盘参见附录三.键盘操作参见附录五. 1)状态设置键  MON   与  USE 监控系统采用设置待命状态的方法实现一键两 ...

  9. c# 火狐浏览器怎么嵌入窗体中_「C#上位机必看」你们想要的练手项目来了

    最近有越来越多做电气的小伙伴开始学习C#来做上位机开发,很多人在学习一段时间后,都有这种感觉,似乎学到了很多知识,但是不知道怎么应用,因此我找了一个真实的上位机小项目,让大家来练练手.这篇文章主要对这 ...

最新文章

  1. 不可不看的干货——机器人自主系统的技术构建:感知、决策和执行
  2. python函数作为参数例题_笨办法学Python 习题 19: 函数和变量
  3. .net core HttpClient 使用之掉坑解析(一)
  4. node.js之文件读写模块,配合递归函数遍历文件夹和其中的文件
  5. freemarker使用说明_SpringBoot+Swagger2集成详细说明
  6. 12岁女孩自学成才考上亚利桑那大学,博士母亲的家庭教育造就「天才少女」...
  7. MongoDB(1)--简单介绍以及安装
  8. 二十年的编程,教会我的五件事!
  9. linux sudoers_Linux –将用户添加到Sudoers列表
  10. 什么是通讯作者?和第一作者的区别有哪些?
  11. lasted是什么意思_lasted是什么意思_lasted怎么读_lasted翻译_用法_发音_词组_同反义词_继续存在( last的过去式和过去分词 )-新东方在线英语词典...
  12. web工程引用其他java工程_并读取spring配置文件_SpringBoot项目实战(8):四种读取properties文件的方式...
  13. Error: Cannot find module ‘webpack‘
  14. 整除分块 B - Make Divisible
  15. SDN相关组织——ONF
  16. C++中switch用法的意义
  17. 【开发利器】中国国内可用API合集
  18. for update
  19. 浅析多元回归中的“三差”:离差(Deviation)、残差(Residual)与误差(Error)
  20. Python:variable in function(argument、function) name should be lowercase 处理方式

热门文章

  1. RoboCup仿真3D底层通信模块介绍(二)
  2. 时间序列平稳性检验(ADF)和白噪声检验(Ljung-Box)
  3. Harbor开源项目有奖征文活动开启
  4. azure 配置vpn_ASP.NET和Azure中配置中的私有配置数据和连接字符串的最佳做法
  5. QTableWidget实现复制粘贴
  6. 块存储、文件存储、对象存储这三者的差别
  7. kali安装flash player
  8. Excel删除指定列(VB)
  9. 小米重大变革:成立十个一级部门大量启用80后 向雷军汇报
  10. [练习]QQ/微信 表情收藏-测试用例的编写 [简洁思路]