现在来详细看一下寄存器,我们直接查看单片机手册。

SCON寄存器

先来说说SCON寄存器。

image.png

前一节我们提过,我们一般使用串口用的是模式1,即8位UART,这样我们就用不到校验位。从手册中可以看到,寄存器SCON中的SM0和SM1配置成01即可。

image.png

SM2寄存器明显用不到,因为我们没有用模式2和3.

REN寄存器控制接收串行,发送数据时候置0,接收数据时置1。

TB8和RB8同SM2,一样用不到。

TI就比较关键了。我们肯定会用到。从串口结构图中可以看到,TI是一个标志位,来判断发送是否结束。举个例子,发送数据就是全自动步枪,TI寄存器就是我们的枪栓。我们发送结束后,TI的值会自动置1,我们需要手动写程序在软件层面给TI置0,让它能够再次使用。

这里插一句,软件层面指的就是我们的代码工程,硬件层面就是我们的板子和电路。

image.png

那么RI和TI的原理是类似的,就不详细说了。我们在初始化配置的时候,一般两位都置0,让它能够直接使用。

image.png

发送数据时,代码如下:

void UART_Init(){
	SCON = 0x40;//0100 0000
}

接收数据时,代码如下:

void UART_Init(){
	SCON = 0x50;//010 0000
}

PCON寄存器

再来说说PCON寄存器。

image.png

SMOD选择波特率是否加倍。我们对晶振频率进行过分频,所以波特率是需要加倍的。

定时器寄存器

除了串口寄存器,我们还需要用到定时器,打开我们上一章定时器的.c文件(不用打开工程,用记事本方式以文本文件方式打开即可)

image.png

复制出定时器的初始化代码。

void Timer0Init(void)		//1毫秒@12.000MHz
{
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;		//设置定时器模式
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
        ET0 = 1;
	EA = 1;
	PT0 = 0;
}

然而串口所用到的定时器是Timer1,和我们之前用的Timer0是不同的寄存器,所以我们需要修改定时器寄存器的代码。串口用的定时器模式也不是我们之前的16位定时器模式,而是8位自动重装载模式。(相比16位定时器模式,这个模式在计数满后,高八位的初值数据会自动转入低八位)

这里还是详细说一下吧,想简单说两句发现说不清楚16位定时器高八位和低八位都用来计时,所以计数范围会比较大,每次计数高八位和低八位都置初始值,然后开始计数,计数满了我们再手动赋初值。八位重装载模式只用低八位寄存器来计时,高八位里面存放的是我们的定时器初值,在低八位加满之后高八位存的数据会流入低八位,这样就不需要我们手动给计时器置初值了

image.png

首先在TMOD中我们需要打开定时器1,我们使用逻辑语言 &= 给TMOD赋值。这样就不会影响我们上一次对TMOD的操作了。

image.png

代码如下:

void Timer0Init(void)		//1毫秒@12.000MHz{
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x20;		//设置定时器模式
	TL0 = ;		//设置定时初值
	TH0 = ;		//设置定时初值
}

软件生成初始化代码

初始值的计算太复杂了,我们还是用STC-ISP软件生成代码,不用再用手算了。 别的寄存器也通过软件来生成吧。

image.png

这里提一嘴,波特率倍速得勾选,上面讲PCON的时候说了原因了。不过实在懒得重新截图了。具体分析看下面小标题里的内容!!!

那么初始化函数里面的内容如下

void UartInit(void)		//4800bps@12.000MHz
{
	PCON &= 0x80;		//波特率不倍速
	SCON = 0x40;		//8位数据,可变波特率
//	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
//	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xF3;		//设定定时初值
	TH1 = 0xF3;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}

波特率计算

现在来说一下如何计算TL和TH。

0xF3就是十进制的243,而定时器每隔256溢出一次。我们定时器重装值设为243,那么计数13次后定时器就溢出了(定时器那里讲过,忘记了可以翻看以前的博客)。

12M的晶振,在12T模式下,每隔1us计数一次(因为12分频了嘛,很好理解的对不对)。

image.png

那么定时器Timer1的溢出率就是1/(13us),即0.07692MHz,假如我们倍速波特率,那么SMOD的开关会走上面的路,上面的频率需要再除以16,进入接收控制器,即0.07692MHz/16=4807.69Hz,这就是我们所设置的波特率4800。、

image.png

同理,假如我们不勾选倍速波特率,SMOD开关会走下面除以2的路,相当于频率除以32。软件帮我们计算出来的TH和TL值是F9,即十进制249,即7us溢出一次,溢出率1/(7us)=0.14285MHz,再除以32,进入接收控制器,即0.14285MHz/32=4464.28Hz,此时误差就比较大了。

image.png

发送数据

我们需要将数据写进SBUF寄存器里,这样在配置好寄存器后,数据会直接被发送出。

image.png

void UART_SendByte(unsigned char Byte){
	SBUF = Byte;//将数据写进SUBF中
	while(TI==0);//发送完成标志位一旦变为1,说明数据发送成功了。接下来软件复位。
	TI = 0;
}

主文件里面的内容如下(其实Delay和Timer0没必要导入,因为没用到):

#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"
void UartInit(void)		//4800bps@12.000MHz
{
	PCON |= 0x80;		//使能波特率倍速位SMOD
	SCON = 0x40;		//8位数据,可变波特率
//	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
//	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xF3;		//设定定时初值
	TH1 = 0xF3;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}
void UART_SendByte(unsigned char Byte)
{
	SBUF = Byte;//将数据写进SUBF中
	while(TI==0);//发送完成标志位一旦变为1,说明数据发送成功了。接下来软件复位。
	TI = 0;
}
void main()
{
	UartInit();
	UART_SendByte(0x11);
	while(1)
	{
		
	}
}

烧录程序后,连续按下复位键,可以看到,串口不断接收到字符11。

image.png

当然,我们也可以把UART_SendByte函数写在while(1)里面,这样就能通过串口持续收到11。但是此时我们需要再发送数据之后加上一定的延时,不然因为晶振频率和波特率的误差,会导致接受数据发生错误。

串口模块化

现在我们通过实验验证了串口发送接收数据的可行性,那么按照前面学习的内容,就可以对串口进行模块化了。具体操作不细说,直接上内容(别忘了加上注释):

//UART.c
#include <REGX52.H>
/**
  * @brief	串口初始化4800bps,@12.000MHz
  * @param	无
  * @retval 无
*/
void UartInit(void)		
{
	PCON |= 0x80;		//使能波特率倍速位SMOD
	SCON = 0x40;		//8位数据,可变波特率
//	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
//	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xF3;		//设定定时初值
	TH1 = 0xF3;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
}
/**
  * @brief	串口发送一个字节数据
  * @param	Byte 要发送的一个字节数据
  * @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
	SBUF = Byte;//将数据写进SUBF中
	while(TI==0);//发送完成标志位一旦变为1,说明数据发送成功了。接下来软件复位。
	TI = 0;
}
#ifndef __UART_H__
#define __UART_H__
void UartInit(void);
void UART_SendByte(unsigned char Byte);
#endif

此时在主文件内直接引用函数就可以正常使用了。

串口接收数据

刚才所讲的内容是关于串口向电脑发送数据。现在再来说一说串口如何接收电脑数据。电脑发送数据被串口收到时,我们不能直接处理,必须进入中断函数进行处理。否则会影响单片机正常工作。

image.png

先打开中断使能EA,然后ES置1,相当于打开中断。

image.png

void UartInit(void)		//4800bps@12.000MHz
{
	PCON &= 0x80;		//波特率不倍速
	SCON = 0x50;		//8位数据,可变波特率
//	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
//	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xF3;		//设定定时初值
	TH1 = 0xF3;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
    EA = 1;
    ES = 1;
}

查询串口的中断次序号

image.png

void UART_Routine() interrupt 4{
    P2 = 0x00;
}

写好之后,烧录程序,在STC-ISP上发送任意一个数据,可以看到,单片机上的所有LED都亮了。

#include <REGX52.H>
#include "Timer0.h"
#include "Delay.h"
#include "UART.h"

void main()
{
	UartInit();
	while(1)
	{
		
	}
}
void UART_Routine() interrupt 4
{
    P2 = 0x00;
}

image.png

说明串口接收数据可行,现在我们可以发挥想象力,大展拳脚了。

电脑发送数据控制LED灯

这样写:

void UART_Routine() interrupt 4{
    if(RI == 1)
	{
		P2 = SBUF;
		RI =  0;
	}
}

image.png

因为TI和RI都有可能使程序进入中断,所以我们要用判断语句来确认是串口收到了数据。

SUBF中保存的是接收和发送的数据,我们可以直接提取出来进行处理。

同时类似于TI,我们在接收时也需要进行软件复位,在确认每次收到数据(即RI=1)后,让RI归零(RI=0)。

那么上面程序的效果,就是前四个LED灭,后四个LED亮。

注意,在中断函数里面的调用的函数,不能在主函数里面使用。因为假如主函数正在调用函数,进入中断后你再调用一次,主函数中的调用就会被打断,那么程序就会出错。

我们可以让单片机再通过串口,把接收到的数据返回给电脑,只需要用之前写好的语句,很简单。

void UART_Routine() interrupt 4
{
    if(RI == 1)
	{
		P2 = SBUF;
		UART_SendByte(SBUF);
		RI =  0;
	}
}
#include <REGX52.H>
/**
  * @brief	串口初始化4800bps,@12.000MHz
  * @param	无
  * @retval 无
*/
void UartInit(void)		
{
	PCON |= 0x80;		//使能波特率倍速位SMOD
	SCON = 0x50;		//8位数据,可变波特率
//	AUXR &= 0xBF;		//定时器1时钟为Fosc/12,即12T
//	AUXR &= 0xFE;		//串口1选择定时器1为波特率发生器
	TMOD &= 0x0F;		//清除定时器1模式位
	TMOD |= 0x20;		//设定定时器1为8位自动重装方式
	TL1 = 0xF3;		//设定定时初值
	TH1 = 0xF3;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1
	EA = 1;
    ES = 1;
}
/**
  * @brief	串口发送一个字节数据
  * @param	Byte 要发送的一个字节数据
  * @retval 无
*/
void UART_SendByte(unsigned char Byte)
{
	SBUF = Byte;//将数据写进SUBF中
	while(TI==0);//发送完成标志位一旦变为1,说明数据发送成功了。接下来软件复位。
	TI = 0;
}
/*串口中断函数模板
void UART_Routine() interrupt 4
{
    if(RI == 1)
	{
	
		RI =  0;
	}
}
*/

以模板的形式加入到我们的模块里,需要使用的时候,直接挪到主函数下面就可以了。它和主函数耦合性还是比较高的,可以直接使用。

数据显示模式

HEX模式/十六进制模式/二进制模式

以原始数据的形式显示

文本模式/字符模式

以原始数据编码后的形式显示

说人话

HEX模式就是进行ASCI编码后的数据,而文本模式,就是ASCI译码之后的结果,C语言大家应该了解过,可以对照图来验证一下。

image.png

同样也需要留意,SendByte函数里面的参数,如果是0x开头的十六进制,用文本模式和HEX模式发送和接收,结果是不一样的。挺简单的,各位自行验证。

随便扯两句

其实写完上一篇笔记,就已经马不停蹄地开始写这一篇笔记。中间鸽了近两个月,当时以为处理完手头的事情,就可以继续用空闲时间学点东西,结果学了一半的内容就被课内的任务缠身,忙的不可开交,而且串口这一块知识点环环相扣,时隔两个月,很多东西都忘记了。处理完课内大作业,删无用文件时太激动了小手一抖把之前的keil工程文件都给删了,还好有之前的笔记,让我不至于从头开始去写以前的模块内容。这两天还发着低烧,不过还是硬挺着完成中断了这么久的内容。不能再拖了,越拖越难重新拾起来。

————————————————

版权声明:本文为CSDN博主「孤心亦暖」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/Destiny_Di/article/details/128432781