今天咱来聊聊串口,其实串口这个词.含义实在是太广泛了,前面所提到的IIC、SPI其实都可以归结为串口通信.确切滴说是串行通信,那么今天的串口,到底和前面有啥区别呢?
IIC、SPI其实有一个非常有意思的共同点,就是一定会留出一个线路作为时钟SCL/SCLK.那是因为无论是发送方还是接收方.都会以这个时钟信号为参考,进行数据的传输.从数字逻辑的角度来讲,叫做是同步时序电路,所以,IIC和SPI本质上都属于同步串行通信.而今天所提到的的串口,实质上应该是指LVTTL/CMOS电平的UART接口.UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)其实应该属于串口的一种,是一种异步串行接口,为毛叫异步,和同步又有什么区别呢?
咱先看看硬件结构,常见的UART串口会有8个功能端口
序号 | 管脚名称 | 功能 |
1 | DCD | 载波检测。主要用于Modem通知计算机其处于在线状态,即Modem检测到拨号音, 处于在线状态。 |
2 | DTR | 数据终端就绪;当此引脚高电平时,通知Modem可以进行数据传输,计算机已经准备好。 |
3 | DSR | 此引脚高电平时,通知计算机Modem已经准备好,可以进行数据通讯了。 |
4 | RTS | 请求发送;此脚有计算机来控制,用以通知Modem马上传送数据至计算机;否则,Modem将收到的数据暂时放入缓冲区中。 |
5 | CTS | 清除发送;此脚由Modem控制,用以通知计算机将欲传的数据送至Modem。 |
6 | RI | Modem通知计算机有呼叫进来,是否接听呼叫由计算机决定 |
7 | RXD | 用于接收外部设备送来的数据 |
8 | TXD | 用于将数据发送给外部设备 |
9 | GND | …… |
从上面的管脚说明可以看出来,1-6号管脚其实是和调制解调器(或者一些其它设备)的功能有关.这几个管脚更多的作用是进行控制.而真正完成首发数据的管脚,便是RXD和TXD两个.实际情况是一般的UART串口,RXD、TXD加上共地.就能够完成数据传输的功能了,不知道大家发现没有.所有的管脚端口中,木有时钟的影子啊!没错,和SPI、IIC等同步串行通信依赖于同一个时钟不同.UART串口的接收方和发送方.所需要的参考时钟都由自己本地产生,相互间的时钟并不同步.也就是使用不同的时钟,所以,UART也叫异步串行通信接口.硬件连接上有个值得一提的地方.
两个设备的TXD和RXD其实是交叉的,一台设备的TX,另外一台对应就是RX.一台是RX,另外一台对应就是TX,不少第一次接触的朋友,在这上面会出点小差错.咱这里讨论的主要是LVTTL/CMOS电平的UART串口,电平范围大约是0-3.3v或者是0-5v.当然,UART只是一种通信规范,如果在电平上做做文章,进行一次电平转换,就能衍生出.譬如RS232、RS485等不同电平的串行接口,再配合内部的一些控制电路,又能完成红外功能.举个栗子吧
这就是一个单片机和RS232调制解调器之间,RS232通信的连接.中间夹了一片电平转换器,注意一下哟.对电器特性、逻辑电平和各种信号线功能都作了规定。在TxD和RxD上:
逻辑1(MARK)=-3V~-15V
逻辑0(SPACE)= 3~ 15V
在RTS、CTS、DSR、DTR和DCD等控制线上:
信号有效(接通,ON状态,正电压)= 3V~ 15V
信号无效(断开,OFF状态,负电压)=-3V~-15V
所以,UART其实有着非常广泛用法,咱拿来发送个字符数据,其实是最基本的用法.
Ok,没有统一的参考时钟源(异步通信),设备之间怎么开始通信呢?其实异步通信每个数据之间的时间间隔是不固定的.但是,同一个数据中相邻bit间的时间间隔是固定的.
下面来看看UART的协议
上图中bit发送的顺序是从右往左依次发送,传输的速率叫波特率,也就是每秒传输的bit位数.譬如一秒传输1000个字符.每个字符1个起始位、1个校验位、1个停止位和7个数据位.共10位,那么1000个字符就有1000x10=10000bits.所以,仪表传输10000个字符对应的比特率就是10000bps(10000 bits per second),数据线空闲时为高.数据线被拉低,则表示一个起始位,数据开始传输.接收设备在接收到起始信号后,进行发送和接收的时钟同步.也就是最早就得商量好,传输的波特率是多少.双方知道波特率后,才能不用同一个时钟,而正常通信.但是由于不同设备的时钟差异,总是会出现小小的传输错误.所以,UART串口有一个很重要的指标就是某个波特率下的误码率.下面简单总结一下各个位的作用
起始位 | 先发出一个逻辑”0”信号,表示传输字符的开始。 |
数据位 | 可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输,低位在前。 |
校验位 | 数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验)。 |
停止位 | 它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 |
空闲位 | 处于逻辑“1”状态,表示当前线路上没有资料传送。 |
看一个上位机的串口调试工具
嗯,应该知道这些是干嘛的了吧.特别注意流控位,和其他功能端口有关,现在基本上所有的单片机都集成了硬件UART.这里还是给一个51 IO口模拟UART串口的代码吧
#include
sbit BT_SND =P1^0; //TXD口
sbit LED =P1^2;
sbit BT_REC =P1^1; //RXD口
#define MODE_QUICK
#define F_TM F0
#define TIMER0_ENABLE TL0=TH0; TR0=1;
#define TIMER0_DISABLE TR0=0;
sbit ACC0= ACC^0;
sbit ACC1= ACC^1;
sbit ACC2= ACC^2;
sbit ACC3= ACC^3;
sbit ACC4= ACC^4;
sbit ACC5= ACC^5;
sbit ACC6= ACC^6;
sbit ACC7= ACC^7;
void IntTimer0() interrupt 1
{
F_TM=1;
}
//发送一个字符
void PSendChar(unsigned char inch)
{
#ifdef MODE_QUICK
ACC=inch;
F_TM=0;
BT_SND=0; //start bit
TIMER0_ENABLE; //启动
while(!F_TM);
BT_SND=ACC0; //先送出低位
F_TM=0;
while(!F_TM);
BT_SND=ACC1;
F_TM=0;
while(!F_TM);
BT_SND=ACC2;
F_TM=0;
while(!F_TM);
BT_SND=ACC3;
F_TM=0;
while(!F_TM);
BT_SND=ACC4;
F_TM=0;
while(!F_TM);
BT_SND=ACC5;
F_TM=0;
while(!F_TM);
BT_SND=ACC6;
F_TM=0;
while(!F_TM);
BT_SND=ACC7;
F_TM=0;
while(!F_TM);
BT_SND=1;
F_TM=0;
while(!F_TM);
TIMER0_DISABLE; //停止timer
#else
unsigned char ii;
ii=0;
F_TM=0;
BT_SND=0; //start bit
TIMER0_ENABLE; //启动
while(!F_TM);
while(ii<8)
{
if(inch&1)
{
BT_SND=1;
}
else
{
BT_SND=0;
}
F_TM=0;
while(!F_TM);
ii ;
inch>>=1;
}
BT_SND=1;
F_TM=0;
while(!F_TM);
#endif
TIMER0_DISABLE; //停止timer
}
//接收一个字符
unsigned char PGetChar()
{
#ifdef MODE_QUICK
TIMER0_ENABLE;
F_TM=0;
while(!F_TM); //等过起始位
ACC0=BT_REC;
TL0=TH0;
F_TM=0;
while(!F_TM);
ACC1=BT_REC;
F_TM=0;
while(!F_TM);
ACC2=BT_REC;
F_TM=0;
while(!F_TM);
ACC3=BT_REC;
F_TM=0;
while(!F_TM);
ACC4=BT_REC;
F_TM=0;
while(!F_TM);
ACC5=BT_REC;
F_TM=0;
while(!F_TM);
ACC6=BT_REC;
F_TM=0;
while(!F_TM);
ACC7=BT_REC;
F_TM=0;
while(!F_TM)
{
if(BT_REC)
{
break;
}
}
TIMER0_DISABLE; //停止timer
return ACC;
#else
unsigned char rch,ii;
TIMER0_ENABLE;
F_TM=0;
ii=0;
rch=0;
while(!F_TM); //等过起始位
while(ii<8)
{
rch>>=1;
if(BT_REC)
{
rch|=0x80;
}
ii ;
F_TM=0;
while(!F_TM);
}
F_TM=0;
while(!F_TM)
{
if(BT_REC)
{
break;
}
}
TIMER0_DISABLE; //停止timer
return rch;
#endif
}
//检查是不是有起始位
bit StartBitOn()
{
return (BT_REC==0);
}
void main()
{
unsigned char gch;
TMOD=0x22; /*定时器1为工作模式2(8位自动重装),0为模式2(8位自动重装) */
PCON=00;
TR0=0; //在发送或接收才开始使用
TF0=0;
TH0=(256-96); //9600bps 就是 1000000/9600=104.167微秒
TL0=TH0;
ET0=1;
EA=1;
PSendChar(0x55);
PSendChar(0xaa);
PSendChar(0x00);
PSendChar(0xff);
while(1)
{
if(StartBitOn())
{
gch=PGetChar();
PSendChar(gch);
if(gch==\'p\')
{LED = 0;}
else if(gch==\'q\')
{LED = 1;}
else
{LED = LED;}
}
}
}
嗯,有点儿长.不过仔细看代码的运行过程,其实基本上就是遵循上面提到的时序.用定时器作为波特率发生器.也就是数据发送接收的本地时钟.然后每次接收一个字符.
如果接收到的是字母p,则点亮LED
如果接收到的是字母q,则点亮LED
上gif
其实UART的时序很简单,难点是如何将接收的数据进行简单的处理.转化为控制相关的数据.说白了,就是字符串或者数据流的处理.这个是使用串口必须要做的一件事情.下面的代码进行了一些简单的字符串处理.可直接调用printf函数(这个是个比较牛的函数,不过消耗很多资源).使用STC89C52RC的硬件UART配置.做了一个简单的人机交互
看注释吧
#include
#include
#include
typedef unsigned char BYTE;
typedef unsigned int WORD;
#define uint unsigned int
#define uchar unsigned char
#define FOSC 11059200L //System frequency
#define BAUD 9600 //UART baudrate
/*Define UART parity mode*/
#define NONE_PARITY 0 //None parity
#define ODD_PARITY 1 //Odd parity
#define EVEN_PARITY 2 //Even parity
#define MARK_PARITY 3 //Mark parity
#define SPACE_PARITY 4 //Space parity
#define Vref 5.0
#define PARITYBIT EVEN_PARITY //Testing even parity
char rx_buff[16],rx_buff_count=0; //接收缓存,16字节
uchar GUI_flag=0;
sbit bit9 = P2^2; //P2.2 show UART data bit9
bit busy;
void SendData(BYTE dat);
void SendString(char *s);
void delayms(uint xms)
{
uint i,j;
for(i=0; i");
}
void sys_init(void) //串口初始化,9600bps
{
#if (PARITYBIT == NONE_PARITY)
SCON = 0x50; //8-bit variable UART
#elif (PARITYBIT == ODD_PARITY) || (PARITYBIT == EVEN_PARITY) || (PARITYBIT == MARK_PARITY)
SCON = 0xda; //9-bit variable UART, parity bit initial to 1
#elif (PARITYBIT == SPACE_PARITY)
SCON = 0xd2; //9-bit variable UART, parity bit initial to 0
#endif
TMOD = 0x20; //Set Timer1 as 8-bit auto reload mode
TH1 = TL1 = -(FOSC/12/32/BAUD); //Set auto-reload vaule
TR1 = 1; //Timer1 start run
ES = 1; //Enable UART interrupt
EA = 1; //Open master interrupt switch
printf("rnSTC89C52RC Uart GUI Test start!rn");
printf("Welcome to GUI TESTrn");
printf(">");
}
void main()
{
sys_init();
while (1)
{
if(GUI_flag)
{
input_ASK();
GUI_flag = 0;
}
}
}
/*----------------------------
UART interrupt service routine
---------------------------- */
void Uart_Isr() interrupt 4 using 1 //串口中断服务函数
{
if (RI)
{
RI = 0; //Clear receive interrupt flag
if(SBUF==0x0d)
{GUI_flag = 1;}
else{
rx_buff[rx_buff_count]= SBUF;
rx_buff_count ;
SendData(SBUF);
GUI_flag = 0;
}
bit9 = RB8; //P2.2 show parity bit
}
if (TI)
{
TI = 0; //Clear transmit interrupt flag
busy = 0; //Clear transmit busy flag
}
}
/*----------------------------
Send a byte data to UART
Input: dat (data to be sent)
Output:None
---------------------------- */
void SendData(BYTE dat) //发送一个字节
{
while (busy); //Wait for the completion of the previous data is sent
ACC = dat; //Calculate the even parity bit P (PSW.0)
if (P) //Set the parity bit according to P
{
#if (PARITYBIT == ODD_PARITY)
TB8 = 0; //Set parity bit to 0
#elif (PARITYBIT == EVEN_PARITY)
TB8 = 1; //Set parity bit to 1
#endif
}
else
{
#if (PARITYBIT == ODD_PARITY)
TB8 = 1; //Set parity bit to 1
#elif (PARITYBIT == EVEN_PARITY)
TB8 = 0; //Set parity bit to 0
#endif
}
busy = 1;
SBUF = ACC; //Send data to UART buffer
}
/*----------------------------
Send a string to UART
Input: s (address of string)
Output:None
---------------------------- */
void SendString(char *s) //发送一个字符串
{
while (*s) //Check the end of the string
{
SendData(*s ); //Send current char and increment string ptr
}
}
char putchar (char c) //修正标准库putchar函数,将SBUF作为输出源,使用printf
{
ES=0;
SBUF = c;
while(TI==0);
TI=0;
ES=1;
return 0;
}
视频中演示了,UART串口接收到字符串后。51进行对应的比对输出,达到交互的效果、像不像小霸王学习机?(一不小心暴露年龄啊)!!
了解更多51系列教程,请关注“云汉电子社区”官方微信公众号ickeybbs,或者登录云汉电子社区官方网站(bbs.ickey.cn)
,