6.1 并行和串行基本通信方式
随着单片机系统的广泛应用和计算机网络技术的普及,单片机的通信功能愈来愈显得重要。单片机通信是指单片机与计算机或单片机与单片机之间的信息交换。通信有并行和串行两种方式。在单片机系统以及现代单片机测控系统中,信息的交换多采用串行通信方式。
1. 并行通信方式
并行通信通常是将数据字节的各位用多条数据线同时进行传送,每一位数据都需要一条传输线,如下图所示,8位数据总线的通信系统,一次传送8位数据(1个字节),将需要8条数据线。此外,还需要一条信号线和若干控制信号线,这种方式仅适合于短距离的数据传输,如比较老式的打印机就是通过并口方式与计算机连接,现在都用传输速度非常快的USB(Universal Serial Bus,通用串行通信,关于USB参见https://zhuanlan.zhihu.com/p/356863749)2.0(现在已经到4.0了)接口通信了。
并行通信控制简单,相对传输速度快,但传输线较多,长距离传输时成本高且收发方的各位同时接受存在困难。
2. 串行通信方式
串行通信是将数据字节分成一位一位的形式在一条传输线上逐个地传送,此时只需要条数据线,外加一条公共信号地线和若干控制信号线。因为一次只能传送一位,所以对于一个字节的数据,至少要分8位才能传送完毕,如下图所示。
串行通信的必要过程是:发送时,要把并行数据变成串行数据发送到线路上去,接收时,要把串行信号再变成并行数据,这样才能被计算机及其他设备处理。
串行通信传输线少,长距离传送时成本低,且可以利用电话网等现成的设备,但数据的传送控制比并行通信复杂。
串行通信又有两种方式:异步串行通信和同步串行通信——
(1)异步串行通信方式
异步串行通信是指通信的发送与接收设备使用各自的时钟控制数据的发送和接收过程,为使双方收、发协调,要求发送和接收设备的时钟尽可能一致。
异步通信是以字符(构成的帧,1帧=10位)为单位进行传输,字符与字符之间的间隙(时间间隔)是任意的,但每个字符中的各位是以固定的时间传送的,即字符之间不一定有“位间隔”的整数倍关系,但同一字符内的各位之间的距离均为“位间隔”的整数倍。
异步通信一帧字符信息由4部分组成:起始位、数据位、奇偶校验位和停止位,如下图所示。有的字符信息也有带空闲位形式,即在字符之间有空闲字符。
异步通信的特点:不要求收发双方时钟严格一致,容易实现,设备开销少,但每个字符要附加2-3位用于起止位、校验位和停止位,各帧之间还有间隔,传输效率不高。在单片机与单片机之间,单片机与计算机之间通信时,通常采用异步串行通信方式。
(2)同步串行通信方式
同步通信时要建立发送方时钟对接收方时钟的直接控制,使双方达到完全同步。此时,传输数据的位之间的距离均为“位间隔”的整数倍,同时传送的字符间不留间隙,即保持位同步关系,也保持字符同步关系。发送方对接收方的同步可以通过外同步和自同步两种方法实现,分别下图所示。
面向字符的同步格式:
此时,传送的数据和控制信息都必须由规定的字符集(如 ASCII码)中的字符所组成。上图中帧头为1个或2个同步字符SYN(ASCII码为16H)。SOH为序始字符(ASCII码为 01H),表示标题的开始,标题中包含源地址、目标地址和路由指示等信息。STX为文始字符(ASCII码为02H),表示传送的数据块开始。数据块是传送的正文内容,由多个字符组成,数据块后面是组终字符ETB(ASCII码为17H)或文终字符ETX(ASCII码为03H),然后是校验码,典型的面向字符的同步规程如 IBM 的二进制同步规程 BSC。
面向位的同步格式:
此时,将数据块看做数据流,并用序列01111110作为开始和结束标志。为了避免在数据流中出现序列 01111110时引起的混乱,发送方总是在其发送的数据流中每出现5个连续的1就插入一个附加的0;接收方则每检测到5个连续的1并且其后有一个0时,就删除该 0。典型的面向位的同步协议如ISO的高级数据链路控制规程HDLC和IBM的同步数据链路控制规程 SDLC。
面向位的同步通信的特点是以特定的位组合 01111110作为帧的开始和结束标志,所传输的一帧数据可以是任意位。它传输的效率较高,但实现的硬件设备比异步通信复杂。
3. 串行通信的制式
(1)单工——传输仅沿一个方向,不可反向传输;
(2)半双工——传输可沿两个方向,但需分时进行
(3)全双工——可同时双向传输
4. 串行通信的错误校验
(1)奇偶校验
在发送数据时,数据位尾随的1位为奇偶校验位(1或0)。奇校验时,数据中1的个数与校验位1的个数之和应为奇数;偶校验时,数据中1的个数与校验位1的个数之和应为偶数。接收字符时,对1的个数进行校验,若发现不一致,则说明传输数据过程中出现了差错。对于计算机只需要对所有信息位进行异或操作就可以实现求偶校验位和进行偶校验,例如:
对于1001101的求偶校验位:所有位的依次异或得到0,故1001101的偶校验位为0;对01001101进行偶校验,若结果为1说明出错,01001101的异或结果为0,故没错。
值得注意的是,如果发生了2bit位的错误(偶数个位发生变化),奇偶校验码将没用了。
(2)代码和校验
代码和校验是发送方将所发数据块求和(或各字节异或),产生一个字节的校验字符(校验和)附加到数据块末尾。接收方接收数据时同时对数据块(除校验字节外)求和(或各字节异或),将所得的结果与发送方的“校验和”进行比较,相符则无差错,否则即认为传送过程中出现了差错。
(3)循环几余校验
这种校验是通过某种数学运算实现有效信息与校验位之间的循环校验,常用于对磁盘信息的传输、存储区的完整性校验等。这种校验方法纠错能力强,广泛应用于同步通信中。
6.2 RS-232电平与TTL电平的转换
计算机串口的RS232C电平,高电平-12V,低电平+12V(负逻辑电平)。这里主要说明的是计算机RS232电平与单片机TTL电平的转换方式。早期的 MC1488,75188 等芯片可实现 TTL电平到 RS-232 电平的转换;MC1489,75189等芯片可实现RS-232电平到TIL电平的转换。现在用的较多的是 MAX232,MAX202,HIN232等芯片,它们同时集成了RS-232电平和 TTL 电平之间的互转。下面介绍的是在没有 MAX232 这种现成电平转换芯片时,如何用二极管、三极管、电阻、电容等分立元件搭建一个简单的 RS-232 电平与TTL 电平之间的转换电路。
1. 分立元件实现RS232电平与TTL电平转换电路
MAX232 是把 TTL 电平从 0V 和 5V 转换到 3V~15V 或-3V~-15V 之间。分析下图,首先 TTL 电平 TXD 发送数据时,若发送低电平 0,这时Q3导通,PCRXD 由空闲时的低电平变高电平(如PC用中断接收的话会产生中断),满足条件。发送高电平1时,TXD为高电平,Q3截止,由于 PCRXD 内部高阻,而 PCTXD 平时是-3~-15V,通过D1和R7将其拉低PCRXD 至-3~-15V,此时计算机接收到的就是1。下面再反过来,PC发送信号,由单片机来接收信号。当 PCTXD 为低电平-3~-15V 时,Q4截止,单片机端的 RXD 被 R9,拉到5V高电平;当 PCTXD 变高时,Q4导通,RXD被Q拉到低电平,这样便实现的双向转换。
TXD= transmit exchange data;RXD=receive exchange data
2. MAX232芯片实现RS232电平与TTL电平转换
MAX232 芯片是 MAXIM 公司生产的、包含两路接收器和驱动器的IC 芯片,它的内部有一个电源电压变换器,可以把输入的+5V 电源电压变换成为 RS-232 输出电平所需的+10V电压。所以,采用此芯片接口的串行通信系统只需单一的+5V 电源就可以了。对于没有+12V电源的场合,其适应性更强,加之其价格适中,硬件接口简单,所以被广泛采用。
上图中上半部分电容 C1,C2,C3,C4及 V+,V-是电源变换电路部分。在实际应用中器件对电源噪声很敏感,因此Vcc必须要对地加去耦电容C5,其值为0.1μF。按芯片手册中介绍,电容C1,C2,C3,C4应取1.0μF/16V的电解电容,经大量实验及实际应用,这4个电容都可以选用0.1μF的非极性瓷片电容代替1.0μF/16V的电解电容,在具体设计电路时,这4个电容要尽量靠近 MAX232 芯片,以提高抗干扰能力。
图下半部分为发送和接收部分。实际应用中,T1IN,T2IN可直接连接TTL/CMOS电平的 51单片机串行发送端 TXD;R1OUT,R2OUT可直接连接TTL/CMOS 电平的51 单片机的串行接收端RXD;T1OUT,T2OUT可直接连接PC机的RS-232串口的接收端RXD;R1IN,R2IN可直接连接 PC 机的 RS-232 串口的发送端 TXD。
现从 MAX232 芯片中两路发送、接收中任选一路作为接口。要注意其发送、接收的引脚要对应。如使 T连接单片机的发送端 TXD,则 PC机的 RS-232 接收端 RXD 一定要对应接Tiour引脚。同时,Riout连接单片机的 RXD引脚,PC机的 RS-232发送端TXD对应接RIN引脚。
3. TX-1C实验班串口部分原理
数据传输过程如下——MAX232的11脚T接单片机TXD端P3.1,TTL电平从单片机的 TXD 端发出,经过MAX232转换为RS-232电平后从MAX232的14脚T1OUT发出,再连接到实验板上串口座的第3脚。经过随板配送的交叉串口线后,MAX232的13引脚连接至PC 机的串口座的第2 脚 RXD 端,至此计算机接收到数据。PC 机发送数据时从 PC 机串口座第3 脚 TXD 端发出数据,再逆流单片机的RXDP3.0接口接收数据。
需要注意的是,MAX232与串口座连接时,无论是数据输出端,还是数据输入端,连接串口座的第2引脚或第3引脚都可以,选用不同的连接方法时,单片机与计算机之间的串口线都要谨慎选择,是选择平行串口线还是交叉串口线、是选择母头对母头串口线还是母头对公头串口线这些都要非常注意,每种选择都有对应的电路,但无论哪种搭配方式,在单片机与计算机之间必须要有一条数据能互相传输的回路,只要把握好每个交接点就一定能通信成功。
6.3 波特率与定时器初值的关系
1. 波特率
单片机或计算机在串口通信时的速率用波特率表示,它定义为每秒传输二进制代码的位数,即1波特=1位/秒,单位是bps(位/秒)。如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位、1个停止位、8个数据位),这时的波特率为10位×240个/秒=2400 bps。串行接口或终端直接传送串行信息位流的最大距离与传输速率及传输线的电气特性也有关。当传输线使用每0.3m(约1英尺)有50pF电容的非平衡屏蔽双绞线时,传输距离随传输速率的增加而减小。当比特率超过 1000bps 时,最大传输距离迅速下降,如 9600bps 时最大距离下降到只有76m(约 250英尺)。因此我们在做串口通信实验选择较高速率传输数据时尽量缩短数据线的长度,为了能使数据安全传输,即使是在较低传输速率下也不要使用太长的数据线。
2. 波特率的计算
在串行通信中,收、发双方对发送或接收数据的速率要有约定。通过编程可以对单片机串口设定为4种工作方式,其中方式0和方式2的波特率是固定的,方式1和方式3的波特率是可变的,由定时器T1的溢出率来决定。
串行口的 4种工作方式对应三种波特率。由于输入的移位时钟的来源不同,所以各种方式的波特率计算公式也不相同,以下是4种方式波特率的计算公式:
方式0的波特率=fOSC/12;
方式1的波特率=(2SMOD/32)×(T1溢出率);
方式2的波特率=(2SMOD/64) × fOSC;
方式3的波特率=(2SMOD/32) ×(T1溢出率)。
式中, fOSC为系统晶振频率,通常为12MHz或11.0592MHz;SMOD是PCON寄存器的最高位;T1溢出率即定时器 T1溢出的频率。
3. 电源管理寄存器PCON
电源管理寄存器在特殊功能寄存器中,字节地址为87H,不能位寻址,PCON用来管理单片机的电源部分,包括上电复位检测、掉电模式、空闲模式等。单片机复位时 PCON 全部被清 0。
SMOD-仅该位与串口通信波特率有关。
SMOD=0:串口方式1,2,3时,波特率正常;
SMOD=1:串口方式1,2,3时,波特率加倍,
(SMOD0),(LVDF),(POF)——这三位是STC单片机特有的功能,请查看相关手册其他单片机保留未使用。
GF1,GF0-两个通用工作标志位,用户可以自由使用
PD-掉电模式设定位。
PD=0:单片机处于正常工作状态。
PD=1:单片机进入掉电(Power Down)模式,可由外部中断低电平触发或由下降沿触发或者硬件复位模式换醒,进入掉电模式后,外部晶振停振,CPU、定时器、串行口全部停止工作,只有外部中断继续工作。
IDL——空闲模式设定位。
IDL=0:单片机处于正常工作状态。IDL=1:单片机进入空闲(Idle)模式,除CPU 不工作外,其余仍继续工作,在空闲模式下可由任一个中断或硬件复位唤醒
T1溢出率就是T1定时器溢出的频率,只要算出T1定时器每溢出一次所需的时间T,那么T的倒数1/T就是它的溢出率。这个问题还是比较容易理解的,在第3章讲解过定时器T0和 T1方式1的操作方法,若我们设定定时器 T1每 50ms 溢出一次,那么其溢出率就为 20Hz,再将 20代入串口波特率计算公式中即可求出相应的波特率,当然也可根据波特率反推出定时器的溢出率,进而计算出定时器的初值。通常单片机在通信时,波特率都较高,因此T1溢出率也必定很高,如果我们使用定时器1的工作方式1在中断中装初值的方法来求T1溢出率的话,在进入中断、装值、出中断这个过程中很容易产生时间上微小的误差,当多次操作时微小的误差不断累积,终会产生错误。有效的解决办法是,使用T1定时器的工作方式2,8位初值自动重装的8位定时器/计数器,定时器方式2逻辑结构图如下图所示。
在方式1中,当定时器计满溢出时,自动进入中断服务程序,然后我们需要手动再次给定时器装初值,而在方式2中,当定时器计满溢出后,单片机会自动为其装初值,并且无须进入中断服务程序进行任何处理,这样定时器溢出的速率就会绝对稳定。方式2的工作过程是:先设定M0M1选择定时器方式2,在 TLX和 THX 中装入计算好的初值,启动定时器,然后TLX寄存器便在时钟的作用下开始加1计数,当 TLX 计满溢出后,CPU 会自动将 THX 中的数装入 TLX中,继续计数。因此我们在启动定时器之前必须先将TLX和THX中装好合适的数值,以让定时器输出产生的溢出率,这里 TLX和 THX 中装的数值必须是一样的,因为每次计数溢出后TLX中装入的新值是从 THX 中取出的。
根据已知波特率,计算定时器1以方式2计算寄存器初始值(TL1和TH1)方法——
以波特率9600bps,晶振11.0592MHz为例:设所求的数为X,则定时器每计 256-X个数溢出一次,每计一个数的时间为一个机器周期,一个机器周期等于 12个时钟周期,所以计一个数的时间为 12/11.0592MHz(s),那么定时器溢出一次的时间为[256-X]×12/11.0592MHz(s),T1的溢出率就是它的倒数,方式1的波特率 =(2SMOD/32) ×(T1溢出率),这里我们取 SMOD=0,则 2SMOD=1,将已知的数代入公式后得 9600=(1/32)x11059200/[256-X] ×12,求得X=253,转换成十六进制为 0xFD。上面若将SMOD置1的话,那么X的值就变成250了。可见,在不变化X值的状态下,SMOD由0变1后,波特率便增加一倍。
常用波特率通常按规范取为1200,2400,4800,9600,…,若采用晶振12MHz或 6MHz,计算得出的 T1 定时初值将不是一个整数,这样通信时便会产生积累误差,进而产生波特率误差,影响串行通信的同步性能。解决的方法只有调整单片机的时钟频率fc,通常采用11.0592MHz 晶振。因为用它能够非常准确地计算出 T1 定时初值,即使对于较高的波特率(19600,19200),不管多么古怪的值,只要是标准通信速率,使用11.0592MHz的晶振可以得到非常准确的数值。
下表列出了串口方式1定时器1方式2产生常用波特率时,TL0和TH0中所装入的值:
6.4 51单片机串行口结构描述
1. 串行口结构
51单片机的串行口是一个可编程全双工的通信接口,具有UART(通用异步收发器)的全部功能,能同时进行数据的发送和接收,也可作为同步移位寄存器使用。51单片机的串行口主要由两个独立的串行数据缓冲寄存器SBUF(一个发送缓冲寄存器一个接收缓冲寄存器)和发送控制器、接收控制器、输入移位寄存器及若干控制门电路组成。串行口基本结构如图所示。
51单片机可以通过特殊功能寄存器 SBUF 对串行接收或串行发送寄存器进行访问,两个寄存器共用一个地址 99H,但在物理上是两个独立的寄存器,由指令操作决定访问哪一个寄存器。执行写指令时,访问串行发送寄存器;执行读指令时,访问串行接收寄存器。接收器具有双缓冲结构,即在从接收寄存器中读出前一个已收到的字节之前,便能接收第二个字节,如果第二个字节已经接收完毕,第一个字节还没有读出,则将丢失其中一个字节,编程时应引起注意。对于发送器,因为数据是由CPU 控制和发送的,所以不需要考虑。
与串行口紧密相关的一个特殊功能寄存器是串行口控制寄存器SCON,它用来设定串行口的工作方式、接收/发送控制以及设置状态标志等。
串行口控制寄存器SCON:
串行口控制寄存器 SCON在特殊功能寄存器中,字节地址为98H,可位寻址,SCON 用以设定串行口的工作方式、接收/发送控制以及设置状态标志等。单片机复位时 SCON 全部被清 0。其各位的定义如表所示。
SM0,SM1——工作方式选择位。串行口有4种工作方式,它们由SM0,SM1设定,对应关系如下表示。
SM2——多机通信控制位。SM2主要用于方式2和方式3。当接收机的SM2=1 时,可以利用收到的 RB8 来控制是否激活 RI(RB8=0时不激活RI,收到的信息丢弃;RB8=1时收到的数据进入 SBUF,并激活RI,进而在中断服务中将数据从SBUF读走)。当SM2=0时,不论收到的RB8是0还是1,均可以使收到的数据进入 SBUF,并激活 RI(即此时 RB8 不具有控制 RI激活的功能)。通过控制 SM2,可以实现多机通信。在方式0时,SM2必须是0。在方式1时,若SM2=1,则只有接收到有效停止位时,RI才置1。
REN——允许串行接收位。REN=1:允许串行口接收数据;REN=0:禁止串行口接收数据
TB8——方式2,3中发送数据的第9位。在方式2或方式3中,是发送数据的第9位,可以用软件规定其作用。可以用做数据的奇偶校验位,或在多机通信中,作为地址帧1数据帧的标志位。在方式0和方式1中,该位未用。
RB8——方式2,3中接收数据的第9位。在方式2或方式3中,是接收数据的第9位,可作为奇偶校验位或地址帧1数据的标志在方式1时,若SM2=0,则RB8是接收到的停止位。
TI——发送中断标志位。在方式0时,当串行发送第8位数据结束时,或在其他方式,串行发送停止位的开始时由内部硬件使 TI置 1,向 CPU发出中断申请。在中断服务程序中,必须用软件将其清 0,取消此中断申请。
RI——接收中断标志位。在方式0时,当串行接收第8位数据结束时,或在其他方式,串行接收停止位的中间时由内部硬件使RI置1,向CPU发出中断申请。也必须在中断服务程序中,用软件将其清0,取消此中断申请。
2. 串口方式简介
在这里对串口4种方式仅做简单介绍,在下一节将重点介绍串口方式1,在后面的篇章对其他几种方式再做详细介绍。
(1)方式0
方式0时,串行口为同步移位寄存器的输入输出方式,主要用于扩展并行输入或输出口。数据由RXD(P3.0)引脚输入或输出,同步移位脉冲由TXD(P3.1)引脚输出。发送和接收均为8位数据,低位在先,高位在后,波特率固定为fOSC/12。
(2)方式1
方式1是10位数据的异步通信口,其中1位起始位,8位数据位,1位停止位。TXD(P3.1)为数据发送引脚,RXD(P3.0)为数据接收引脚。其传输波特率是可变的,对于51单片机,波特率由定时器1的溢出率决定。通常我们在做单片机与单片机串口通信、单片机与计算机串口通信、计算机与计算机串口通信时,基本都选择方式1。
(3)方式 2,3
方式2,3 时为11位数据的异步通信口。TXD(P3.1)为数据发送引脚RXD(P3.0)为数据接收引脚。这两种方式下,起始位1位,数据9位(含1位附加的第9位,发送时为SCON中的TB8,接收时为RB8),停止位1位,一数据为11位。方式2的波特率固定为晶振频率的1/64或1/32,方式3的波特率由定时器T1的溢出率决定。方式2和方式3的差别仅在于波特率的选取方式不同,在两种方式下,接收到的停止位与 SBUF,RB8 及RI都无关。
补充——单片机与单片机的通信:
一、点对点的通信
1. 硬件连接
2、通信协议
- 所有从机的SM2位置1,处于接收地址帧状态。
- 主机发送一地址帧,其中8位是地址,第9位为地址/数据的区分标志,该位置1表示该帧为地址帧。
- 所有从机收到地址帧后,都将接收的地址与本机的地址比较。对于地址相符的从机,使自己的SM2位置0(以接收主机随后发来的数据帧,并把本站地址发回主机作为应答;对于地址不符的从机,仍保持SM21=,对主机随后发来的数据帧不予理睬。
- 从机发送数据结束后,要发送一帧校验和,并置第9位(TB8)为1,作为从机数据传送结束的标志。
- 主机接收数据时先判断数据接收标志(RB8),若RB8=1,表示数据传送结束,并比较此帧校验和,若正确则回送正确信号00H,此信号命令该从机复位(即重新等待地址帧);若校验和出错,则发送OFFH,命令该从机重发数据。若接收帧的RB8=0则存数据到缓冲区,并准备接收下帧信息。
- 主机收到从机应答地址后,确认地址是否相符,如果地址不符,发复位信号(数据帧中TB8=1);如果地址相符,则清TB8,开始发送数据。
- 从机收到复位命令后回到监听地址状态(SM2=1)。否则开始接收数据和命令。
3、应用程序
- 主机发送的地址联络信号为:00H,01H,02H… …(即从机设备地址),地址FFH为命令各从机复位,即恢复SM2=1。
- S主机命令编码为:01H,主机命令从机接收数据;02H,主机命令从机发送数据。其它都按02H对待。
RRDY=1:表示从机准备好接收。
TRDY=1:表示从机准备好发送,
ERR=:表示从机接收的命令是非法的。
程序分为主机程序和从机程序。约定一次传递数据为16个字节,以01H地址的从机为例。
6.5 串行口方式1编程与实现
串行口方式1是最常用的通信方式,其传送一帧数据的格式如下图所示:
串行口方式1传送一帧数据共10位,1位起始位(0),8位数据位,最低位在前,高位在后,1位停止位(1),帧与帧之间可以有空闲,也可以无空闲。方式1数据输出时序图和数据输入时序图分别如上图和下图所示。
方式1数据输出时序图
当数据被写入 SBUF 寄存器后,单片机自动开始从起始位发送数据,发送到停止位的开始时,由内部硬件将TI置1,向CPU申请中断,接下来可在中断服务程序中做相应处理,也可选择不进入中断。
方式1数据输入时序图
用软件置REN为1时,接收器以所选择波特率的16倍速率采样RXD引脚电平,检测到RXD 引脚输入电平发生负跳变时,则说明起始位有效,将其移入输入移位寄存器,并开始接收这一帧信息的其余位。接收过程中,数据从输入移位寄存器右边移入,起始位移至输入移位寄存器最左边时,控制电路进行最后一次移位。当RI=0,且SM2=0(或接收到的停止位为1)时,将接收到的9位数据的前8位数据装入接收SBUF,第9位(停止位)进入RB8,并置 RI=1,向 CPU 请求中断。
在具体操作串行口之前,需要对单片机的一些与串口有关的特殊功能寄存器进行初始化设置,主要是设置产生波特率的定时器1、串行口控制和中断控制。
具体步骤:
①确定 T1的工作方式(编程 TMOD 寄存器);
②计算 T1的初值,装载 TH1,TL1;
③启动 T1(编程 TCON 中的 TR1位);
④确定串行口工作方式(编程 SCON 寄存器);
⑤ 串行口工作在中断方式时,要进行中断设置(编程,IP寄存器)。
一些示例:
例一:查询法点亮二极管
#include<reg52.h>
//查询法点亮二级管
void main()
{
while(1)
{
REN=1; //打开串口
SM0=0; //设置串口的工作方式
SM1=1;
TMOD=0x20; //设置定时器1为工作方式2
TH1=0xfd; //定时器装初值
TL1=0xfd;
TR1=1; //打开定时器1
if(RI == 1) // 接收到数据后RI被硬件置1
{
RI=0; //此处RI必须手动用软件清0
P1=SBUF; //收到数据点亮第一个二极管
}
}
}
例二:中断法点亮二极管
#include<reg52.h>
//中断法点亮二级管
void main()
{
while(1)
{
REN=1; //打开串口
SM0=0; //设置串口的工作方式
SM1=1;
TMOD=0x20; //设置定时器1为工作方式2
TH1=0xfd; //定时器装初值
TL1=0xfd;
TR1=1; //打开定时器1
EA=1;
ES=1;
}
}
void ser() interrupt 4
{
RI=0;
P1=SBUF;
}
例三:收发相同字符
#include<reg52.h>
unsigned char flag,a;
//发送什么回收什么字符
void main()
{
REN=1; //打开串口,允许接收
SM0=0; //设置串口的工作方式
SM1=1;
TMOD=0x20; //设置定时器1为工作方式2
TH1=0xfd; //定时器装初值
TL1=0xfd;
TR1=1; //打开定时器1
EA=1;
ES=1;
while(1)
{
while(flag)
{
ES=0;
flag=0;
SBUF=a;
while(!TI);
TI=0;
ES=1;
}
}
}
void ser() interrupt 4
{
RI=0;
P1=SBUF;
a=SBUF;
flag=1;
}
注意:
- 示例程序中“void ser() interrupt 4”为串口中断服务程序,在本程序中完成三件事:
- RI清0,因为程序既然产生了串口中断,那么肯定是收到或发送了数据,在开始时没有发送任何数据,那必然是收到了数据,此时 RI会被硬件置1,进入串口中断服务程序后必须由软件清0,这样才能产生下一次中断;
- 将 SBUF中的数据读走给a,这才是进入中断服务程序中最重要的目的;
- 将标志位 flag 置1,以方便在主程序中查询判断是否已经收到数据。
- 进入大循环 while( )语句后,一直在检测标志位 flag 是否为1,当检测到为1时,说明程序已经执行过串口中断服务程序,即收到了数据,否则始终检测 fag 的状态。当检测到 flag置1后:
- 首先是将ES清0,原因是接下来要发送数据,若不关闭串口中断,当发送完数据后,单片机同样会申请串口中断,便再次进入中断服务程序,flag又被置1,主程序检测到flag为1,又回到这里再次发送,如此重复下去,程序便成为死循环,造成错误的现象,因此需要在发送数据前把串口中断关闭,等发送完数据后再打开串口中断,这样即可安全地发送数据了。
- 之后由“SBUF=a”的方式写入并发送数据;
- 使用“while(!TI);”等待是否发送完毕,因为当发送完毕后TI会由硬件置1,然后才退出“while(!TI);”接下来再将 TI手动清 0,“TI=0”;
- 最后开启串口中断“ES=1”,退出本次循环。
- 在发送数据时,当发送前面书上例程中6个固定的字符时,使用了一个for 循环语句,将前面数组中的字符依次发送出去,后面再接着发送从中断服务程序中读回来的SBUF中的数据:
- 当接收数据时,我们写“a=SBUF;”语句,单片机便会自动将串口接收寄存器中的数据取走给 a;当发送数据时,我们写“SBUF=a;”语句,程序执行完这条语句便自动开始将串口发送寄存器中的数据一位位从串口发送出去。注意,SBUF是共用一个地址的两个独立的寄存器,单片机识别操作哪个寄存器的关键语句就是“a=SBUF”(接收)和“SBUF=a”(发送)。
【例6.5.1】
在上位机上用串口调试助手发送一个字符X,单片机收到字符后返回给上位机“I get X”,串口波特率设为9600bps。
#include<reg52.h>
#define uint unsigned int
#define uchar unsigned char
#define ul unsigned long
uchar flag,a,i;
uchar code message_table[]="I get";
void main()
{
void init_SCI(); //声明串口初始化设置函数
init_SCI(); //串口初始化设置
while(1)
{
if(flag==1)
{
ES=0;
for(i=0;i<6;i++)
{
SBUF=message_table[i];
while(!TI);
TI=0;
}
SBUF=a;
while(!TI);
TI=0;
ES=1;
flag=0;
}
}
}
void init_SCI() //串口初始化设置函数
//对单片机中与串口有关的特殊功能寄存器进行初始化设置
//主要是设置产生波特率的定时器1、串行口控制和中断控制
{
TMOD=0x20; //设定T1定时器工作方式2,即设置M1M0为10,8位数值自动重装的定时器/计数器
TH1=0xfd; //T1定时器装初值,对应波特率9600
TL1=0xfd; //T1定时器装初值,对应波特率9600,TH1与TL1相同,工作方式2自动重装
TR1=1; //启动T1定时器
REN=1; //允许串口接收
SM0=0; //设定串口工作方式1
SM1=1; //设定串口工作方式1
EA=1; //开总中断
ES=1; //开串口中断,隶属于中断允许寄存器IE(EA也是)。ES——串口中断允许控制位。ES=1时允许串行口接受、发送中断
//后续无开定时器1中断的语句,因为此时定时器1在方式2时为8为自动重装方式,无需打开也无需写程序
}
void ser() interrupt 4 //串口中断服务程序
{
RI=0;
a=SBUF;
flag=1;
}
注意,在串口调试助手中可能只有COM1~COM4的选择,而笔记本电脑上的COM口有COM5无法匹配,此时需要手动修改单片机此时连接的COM口的序号,解决办法:我的电脑->右键->属性->设备管理器->端口->右键-属性-端口设置-高级-COM端口号,即可自定义端口号。
6.6 串行口打印在调试程序中的应用
串行口打印功能通常用在程序调试中,用于输出给上位机一个关键数据,以获得道程序中某些变量的实时数值,进一步得知程序运行的状况。例如AD采样。
【例6.6.1】
单片机上电后等待从上位机串口发送来的命令,同时在数码管的前三位以十进制方式显示 A/D 采集的数值,在未收到上位机发送来的启动 AD 转换命令之前数码管始终显示 000。当收到上位机以十六进制发送来的 01后,向上位机发送字符串“Turn on ad!”,同时间隔一秒读取一次 AD 的值,然后把 A/D 采集回来的8位二进制数转换成十进制数表示的实际电压浮点数,并且从串口发送给上位机,形式如“The voltage is 3.398438V”,发送周期也是秒一次,同时在数码管上也要每秒刷新显示的数值。当收到上位机以十六进制发送来的02后,向上位机发送字符串“Turn off ad!”,然后停止发送电压值,数码管上显示上次结束时保持的值。当收到上位机发来的其他任何数时,向上位机发送字符串“Error!”。
#include <reg52.h>
#include <stdio.h> //需要用到printf()函数和puts()函数
#define uchar unsigned char
#define uint unsigned int
sbit dula = P2^6; //申请U1锁存器的锁存端
sbit wela = P2^7; //申请U1锁存器的锁存端
sbit adwr = P3^6; //定义AD的WR端口
sbit adrd = P3^7; //定义AD的RD端口
uchar flag, flag_uart, flag_time, flag_on, a, i, t0_num, ad_val;
float ad_vo;
uchar code dula_table[]={ //段选数码库
0x3f,0x06,0x5b,0x4f, // 0,1,2,3
0x66,0x6d,0x7d,0x07, //4,5,6,7
0x7f,0x6f,0x77,0x7c, //8,9,10,11
0x39,0x5e,0x79,0x71 //12,13,14,15
};
uchar code wela_table[]= //片选数码库
{
0xdf/*最低位*/,0xef,0xf7,0x7b,0x7d,0x7e/*最高位*/ //从右向左数第一到六位
/*特别地,这里最高位为0x7bde而非0xfbde是由于段选公用U2锁存器,而AD的CS端需要一直保持低电平不变*/
};
void main()
{
void delayxms(uint xms);
void init();
void display(uchar value);
uchar get_ad();
//初始化
init();
while (1)
{
//若检测到flag_uart为1,说明串口已经执行过串口中断服务程序, 即收到了数据
if (flag_uart == 1)
{
//手动将flag_uart清0,方便标志位检测
flag_uart = 0;
//检测到收到数据后,先将ES清0,原因是接下来要发送数据,若不关闭串口,
//发送完数据后,单片机会重新申请串口中断flag_uart又为1,又再次发送,一直反复
ES = 0;
//由于下面switch中需要调用puts,printf,在单片机的puts,printf中需要对TI是否为1
//进行检测,就是需要等待TI为1才会将字符发送出去,否则一直等待下去
//所以这里需要先将TI置为1
TI = 1;
//根据执行串口中断后获取的flag_on的值执行相应的动作
switch(flag_on)
{
case 0:puts("Turn on ad!\n");
TR0 = 1; //开启计数器0
break;
case 1:printf("Turn off ad!\n");
TR0 = 0; //关闭计数器0
break;
case 2:puts("Error!\n");
break;
}
//等待发送完毕,因为发送完毕后TI会由硬件清0
while (!TI);
//手动TI清0
TI = 0;
//重新开启串口中断
ES = 1;
}
//通过flag_time判断时间是否到1s, 从而去获取AD转换的值
if (flag_time == 1)
{
//手动将flag_time清0,方便下次检测
flag_time = 0;
//获取AD转换的值
ad_val = get_ad();
//获取浮点数表示的AD实际采集到的电压值
ad_vo = (float)ad_val*5.0/256.0;
//关闭串口中断
ES = 0;
//下面要调用printf,所以将TI置1, 原因见上面
TI = 1;
printf("The voltage is %fV\n", ad_vo);
//检测发送是否完成
while (!TI);
//手动TI清0
TI = 0;
//开启串口中断
ES = 1;
}
//数码管上显示AD转化的值
display(ad_val);
delayxms(2);
}
}
void delayxms(uint xms) //延时函数 ms为单位
{
uint x,y;
for(x=xms;x>0;x--)
for(y=124;y>0;y--);
}
void init()
{
//设定T1定时器工作方式2, T0定时器工作方式1
TMOD = 0x21;
//为T0定时器装入初值
TH0 = (65536 - 50000) / 256;
TL0 = (65536 - 50000) % 256;
//为T1定时器装入初值
TH1 = 0xfd;
TL1 = 0xfd;
//ET1 = 1; 这里不需要开启定时器1中断,因为定时器1工作在方式2,为8位自动重装方式,进入中断也无事可做
//启动T1定时器
TR1 = 1;
//开启定时器0中断
ET0 = 1;
//启动定时器0
//TR0 = 1; TR0的初始化放在主函数的while中,方便检测到串口发送数据后的1s延时,即延时1s从串口发送完数据开始
//设定串口工作方式
//10位异步收发,含8位数据,波特率可变,且由定时器1溢出率控制
SM0 = 0; //
SM1 = 1;
//容许串口中断
REN = 1;
//开启总中断
EA = 1;
//开启串口中断
ES = 1;
//为了节省IO端口,ADC0804的片选端CS连接U2锁存器的Q7输出端
//即位选时,P0的最高位
//于是通过位选时对P0赋值置CSAD为0,选通后,以后不必再管
wela = 1;
P0 = 0x7f; //置CSAD为0,选通ADCS以后不必再管ADCS
wela = 0;
ad_vo=0.0;
adwr = 0; //启动A/D转换
adrd = 0; //AD读使能
}
void display(uchar num)
{
void delayxms(uint xms);
void wedu(uchar dula_num,uint wela_num); //数码管位选段选函数
uchar bai, shi, ge;
uint wei; //位选数字
bai = num / 100;
shi = num % 100 / 10;
ge = num % 10;
for(wei=3;wei<6;wei++)
{
switch(wei)
{
case 3: wedu(ge,wei);break;
case 4: wedu(shi,wei);break;
case 5: wedu(bai,wei);break;
}
dula=1; //打开U1锁存端
P0=0x00; //防止最高位数码管过亮
dula=0; //关闭U1锁存端
}
}
void wedu(uchar dula_num,uint wela_num) //数码管位选段选函数
{
void delayxms(uint xms); //延时函数 ms为单位
wela=1; //打开U2锁存端
P0=wela_table[wela_num]; //送入U2锁存端
wela=0; //关闭U2锁存端
P0=0xff; //消影,防止P0残留电位信号干扰段选
dula=1; //打开U1锁存端
P0=dula_table[dula_num]; //送入段选信号
dula=0; //关闭U1锁存端
P0=0xff; //消影,防止P0残留电位信号干扰片选
delayxms(2);
}
uchar get_ad()
{
uchar adval;
adval=P1; //A/D数据读取赋予P1口
return adval;
}
void timer0() interrupt 1
{
TH0 = (65536 - 50000) / 256;
TL0 = (65536 - 50000) % 256;
t0_num++;
if (t0_num == 2)
{
t0_num = 0;
flag_time = 1; //flag_time置1,便于主程序检测是否到1s
}
}
void ser() interrupt 4
{
//RI为接收中断标志位, 在方式0时, 当串行接收第8位数据结束时, 或在其他方式, 串行接收停止位的
//中间时, 由内部硬件使RI置1, 向CPU发出中断申请, 也必须在中断服务程序中, 用软件将其清0,取消
//此中断申请, 以方便下一次中断申请检测, 即这样才能产生下一次中断.
//这里RI清0, 因为程序既然产生了串口中断, 肯定是收到或发送了数据, 在开始时没有发送任何数据
//那必然是收到了数据, 此时RI会被硬件置1, 所以进入串口中断服务程序后必须由软件清0, 这样才能
//产生下一次中断.
RI = 0;
//将SBUF中的数据读走给a, 这是此中断服务程序最重要的目的
a = SBUF;
//将串口中断标志位设置为1,便于主程序检测
flag_uart = 1;
if (a == 1) flag_on = 0;
else if (a == 2) flag_on = 1;
else flag_on = 2;
}
补充说明:
- 注意,串口调试助手一定要以十六进制发送数据,否则会出现问题(本人在此处卡住很久,因为否则a==1或a==2的判断将无法进行,程序无法正常给flag_on进行赋值,也就无法进一步进入switch语句),关于十六进制的发送和接收数据参考——串口网口16进制发送的和ASCII发送以及16进制接收和ASCII接收区别_串口通信16进制发送和接收-CSDN博客;
- AD采样程序部分相较于之前的方法要(详见(51单片机)第五章-A/D和D/A工作原理-A/D-CSDN博客)简单很多(按照书上),只在初始化函数后在get_ad()函数中对P1口采样得到实时的数据,这一块内容有待继续研究,;
-
先设置串口模式,再允许串口接收,可以避开串口方式0接收数据;
-
对SCON寄存器进行整体设置而不要位操作;
-
#include <stdio.h>头文件中包含有要使用的函数printf()和puts( )。在Keil\C51NNC 文件夹下打开 STDIO.H,可看到里面申明了一些外部函数,内容如下:
extern表示在这里申明的是一个外部函数,外部函数的函数体不在本文件中,而是在其他某个文件中写有这个函数的实现部分。这其中很多函数需要等待TI为1才将字符发送出去,否则一直等待下去,因此每次在调用 printf()和 puts()函数之前先要手动将 TI置1,同时必须把串口中断先关闭(否则每发送一个字节都会触发串口中断),每次调用 printf()和 puts()函数之后要把TI清0,否则程序也会出错。
参考资料:
[1] 郭天祥. 新概念51单片机C语言教程:入门、提高、开发、拓展全攻略[M]. 北京: 电子工业出版社, 2009.
[2] 串口网口16进制发送的和ASCII发送以及16进制接收和ASCII接收区别_串口通信16进制发送和接收-CSDN博客[3](51单片机)第五章-A/D和D/A工作原理-D/A-CSDN博客