SCON寄存器
前一节我们提过,我们一般使用串口用的是模式1,即8位UART,这样我们就用不到校验位。从手册中可以看到,寄存器SCON中的SM0和SM1配置成01即可。
SM2寄存器明显用不到,因为我们没有用模式2和3.
REN寄存器控制接收串行,发送数据时候置0,接收数据时置1。
TB8和RB8同SM2,一样用不到。
TI就比较关键了。我们肯定会用到。从串口结构图中可以看到,TI是一个标志位,来判断发送是否结束。举个例子,发送数据就是全自动步枪,TI寄存器就是我们的枪栓。我们发送结束后,TI的值会自动置1,我们需要手动写程序在软件层面给TI置0,让它能够再次使用。
这里插一句,软件层面指的就是我们的代码工程,硬件层面就是我们的板子和电路。
void UART_Init(){ SCON = 0x40;//0100 0000 }
void UART_Init(){ SCON = 0x50;//010 0000 }
PCON寄存器
定时器寄存器
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位定时器高八位和低八位都用来计时,所以计数范围会比较大,每次计数高八位和低八位都置初始值,然后开始计数,计数满了我们再手动赋初值。八位重装载模式只用低八位寄存器来计时,高八位里面存放的是我们的定时器初值,在低八位加满之后高八位存的数据会流入低八位,这样就不需要我们手动给计时器置初值了
void Timer0Init(void) //1毫秒@12.000MHz{ TMOD &= 0x0F; //设置定时器模式 TMOD |= 0x20; //设置定时器模式 TL0 = ; //设置定时初值 TH0 = ; //设置定时初值 }
软件生成初始化代码
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分频了嘛,很好理解的对不对)。
那么定时器Timer1的溢出率就是1/(13us),即0.07692MHz,假如我们倍速波特率,那么SMOD的开关会走上面的路,上面的频率需要再除以16,进入接收控制器,即0.07692MHz/16=4807.69Hz,这就是我们所设置的波特率4800。、
同理,假如我们不勾选倍速波特率,SMOD开关会走下面除以2的路,相当于频率除以32。软件帮我们计算出来的TH和TL值是F9,即十进制249,即7us溢出一次,溢出率1/(7us)=0.14285MHz,再除以32,进入接收控制器,即0.14285MHz/32=4464.28Hz,此时误差就比较大了。
发送数据
void UART_SendByte(unsigned char Byte){ SBUF = Byte;//将数据写进SUBF中 while(TI==0);//发送完成标志位一旦变为1,说明数据发送成功了。接下来软件复位。 TI = 0; }
#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) { } }
当然,我们也可以把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
串口接收数据
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; }
查询串口的中断次序号
void UART_Routine() interrupt 4{ P2 = 0x00; }
#include <REGX52.H> #include "Timer0.h" #include "Delay.h" #include "UART.h" void main() { UartInit(); while(1) { } } void UART_Routine() interrupt 4 { P2 = 0x00; }
电脑发送数据控制LED灯
void UART_Routine() interrupt 4{ if(RI == 1) { P2 = SBUF; RI = 0; } }
因为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语言大家应该了解过,可以对照图来验证一下。
同样也需要留意,SendByte函数里面的参数,如果是0x开头的十六进制,用文本模式和HEX模式发送和接收,结果是不一样的。挺简单的,各位自行验证。
随便扯两句
其实写完上一篇笔记,就已经马不停蹄地开始写这一篇笔记。中间鸽了近两个月,当时以为处理完手头的事情,就可以继续用空闲时间学点东西,结果学了一半的内容就被课内的任务缠身,忙的不可开交,而且串口这一块知识点环环相扣,时隔两个月,很多东西都忘记了。处理完课内大作业,删无用文件时太激动了小手一抖把之前的keil工程文件都给删了,还好有之前的笔记,让我不至于从头开始去写以前的模块内容。这两天还发着低烧,不过还是硬挺着完成中断了这么久的内容。不能再拖了,越拖越难重新拾起来。
————————————————
版权声明:本文为CSDN博主「孤心亦暖」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Destiny_Di/article/details/128432781