一、串口基本知识
1、什么是串口通信
串口通信(Serial Communication),是指外设和计算机间,通过数据信号线、地线等,按位进行传输数据的一种通讯方式。
串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。串口是计算机上一种非常通用设备通信的协议。大多数计算机包含两个基于RS232的串口。串口同时也是仪器仪表设备通用的通信协议;很多GPIB兼容的设备也带有RS-232口。同时,串口通信协议也可以用于获取远程采集设备的数据。串口通信的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且能够实现远距离通信。
串口接头
各个引脚功能说明
注:一般我们需要的就是2,3,5接口,
典型地,串口用于ASCII码字符的传输。通信使用3根线完成:(1)地线,(2)发送,(3)接收。由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但是不是必须的。连接时是TXD接RXD,RXD接TXD,GND接GND。自己的TXD口接RXD口,自发自收,测试串口是否正常。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通行的端口,这些参数必须匹配:
a,波特率:这是一个衡量通信速度的参数。它表示每秒钟传送的bit的个数。例如300波特表示每秒钟发送300个bit。当我们提到时钟周期时,我们就是指波特率例如如果协议需要4800波特率,那么时钟是4800Hz。这意味着串口通信在数据线上的采样率为4800Hz。通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信。
b,数据位:这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据不会是8位的,标准的值是5、7和8位。如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7位)。扩展的ASCII码是0~255(8位)。如果数据使用简单的文本(标准 ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语“包”指任何通信的情况。
c,停止位:用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
d,奇偶校验位:在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位位1,这样就有3个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步.
2、串口通信协议
在串口通信中,常用的协议包括RS-232、RS-422和RS-485。
RS-232:标准串口,最常用的一种串行通讯接口。有三种类型(A,B和C),它们分别采用不同的电压来表示on和off。最被广泛使用的是RS-232C,传送距离最大为约15米,最高速率为20kb/s。RS-232是为点对点(即只用一对收、发设备)通讯而设计的,其驱动器负载为3~7kΩ。所以RS-232适合本地设备之间的通信。RS232标准是按负逻辑定义的,他的“1”电平在-5~-15 V之间,“0”电平在+5~+15 V之间。虽然RS232应用很广,但由于数据传输速率慢,通讯距离短,特别是在100 m以上的远程通讯中难以让人满意,因此通常采用RS422,RS449,RS423及RS485等接口标准来实现远程通讯。
RS-422:最大传输距离为1219米,最大传输速率为10Mb/s。其平衡双绞线的长度与传输速率成反比,在100kb/s速率以下,才可能达到最大传输距离。只有在很短的距离下才能获得最高速率传输。一般100米长的双绞线上所能获得的最大传输速率仅为1Mb/s。
RS-485:从RS-422基础上发展而来的,最大传输距离约为1219米,最大传输速率为10Mb/s。平衡双绞线的长度与传输速率成反比,在100kb/s速率以下,才可能使用规定最长的电缆长度。只有在很短的距离下才能获得最高速率传输。一般100米长双绞线最大传输速率仅为1Mb/s。
3、同步通信和异步通信
同步通信:是一种比特同步通信技术,要求发收双方具有同频同相的同步时钟信号,只需在传送报文的最前面附加特定的同步字符,使发收双方建立同步,此后便在同步时钟的控制下逐位发送/接收。如:SPI总线。
异步通信:指两个互不同步的设备通过计时机制或其他技术进行数据传输。也就是说,双方不需要共同的时钟。发送方可以随时传输数据,而接收方必须在信息到达时准备好接收。如:串口(UART)。
UART和USART,实际上,从字面意思即可理解:
UART:universal asynchronous receiver and transmitter(通用异步收/发器)。
USART:universal synchronous asynchronous receiver and transmitter(通用同步/异步收/发器)。
USART在UART基础上增加了同步功能,即USART是UART的增强型。
3、通信方式
单工模式(Simplex Communication):单向的数据传输。通信双方中,一方为发送端,一方则为接收端。信息只能沿一个方向传输,使用一根传输线。双方是固定的。
半双工模式(Half Duplex):通信使用同一根传输线,既可以发送数据又可以接收数据,但不能同时进行发送和接收。数据传输允许数据在两个方向上传输,但是,在任何时刻只能由其中的一方发送数据,另一方接收数据。
全双工模式(Full Duplex)通信允许数据同时在两个方向上传输。因此,全双工通信是两个单工通信方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。在全双工模式中,每一端都有发送器和接收器,有两条传输线,信息传输效率高。
二、linux 下串口编程
Linux对所有设备的访问是通过设备文件来进行的,串口也是这样,为了访问串口,只需打开其设备文件即可操作串口设备。在linux系统下面,每一个串口设备都有设备文件与其关联,设备文件位于系统的/dev目录下面。如linux下的/ttyS0,/ttyS1分别表示的是串口1和串口2。下面来详细介绍linux下是如何使用串口的。
下面通过写一个串口类来说明linux下的串口编程,并通过虚拟串口来测试代码。
//SerialPort.h 这是串口类的头文件,定义了一个串口了
#ifndef SERIALPORT_H
#define SERIALPORT_H
/*linux下串口需要使用到的头文件*/
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<termios.h>
#include<errno.h>
#include<string.h>
#include<pthread.h>
class SerialPort
{
public:
SerialPort(int WhichCom);
~SerialPort();
bool InitSerialPort(int BaudRate,int DataBits,int StopBits,int ParityBit); // 初始化串口
bool CloseSerialPort();// 关闭串口
int Write(char *Buff, const int Len); // 向串口写入数据
int Read(char *Buff, const int Len); // 从串口中读取数据
void StartRead(); // 开启一个线程来循环读取
void StartWrite(); // 开启一个 线程来循环写入
private:
static int m_BaudRateArr[]; // 波特率数组
static int m_SpeedArr[]; // 波特率数组
static char *m_DevName; // 串口设备名称
struct termios m_Setting; // 串口配置的结构体
int fd; // 打开串口设备后返回的文件描述符
};
#endif
//SerialPort.cpp 实现具体的串口类
#include "SerialPort.h"
#ifdef VITRUALPROT // 加上一个宏编译,如果使用虚拟串口来调试就定义这个宏,如果使用硬件来调试就不用
const char *COM_NAME = "/dev/pts/"; // 这个是我设备中虚拟串口的名称
#else
const char *COM_NAME = "/dev/ttymxc" ; // 这个是我硬件中串口的名称
#endif
// 波特率数组
int SerialPort::m_BaudRateArr[] = {B115200, B57600, B9600,B38400, B19200, B4800, B2400, B1200, B300 };
int SerialPort::m_SpeedArr[] = {115200, 57600,9600,38400, 19200, 4800, 2400, 1200, 300 };
/*
构造函数,打开串口。参数WhichCom:第几个串口
*/
SerialPort::SerialPort(int WhichCom)
{
char devName[100];
sprintf(devName, "%s%d", COM_NAME, WhichCom);
/*open函数打开串口
O_RDWR :串口可读写
O_NOCTTY:可以告诉Linux这个程序不会成为这个端口上的“控制终端”.如果不这样做的话,所有的输入,比如键盘上过来的Ctrl+C中止信号等等,会影响到你的进程。
O_NDELAY:标志则是告诉Linux,这个程序并不关心DCD信号线的状态——也就是不关心端口另一端是否已经连接(不阻塞)。
*/
fd = open( devName, O_RDWR | O_NOCTTY |O_NDELAY);
if(fd < 0)
{
fd = -1;
printf("Can't Open the %s device.\n", devName);
return;
}
bzero(&m_Setting, sizeof(m_Setting));
/*重新将串口设置为阻塞模式,即执行read函数时,如果没有数据就会阻塞等待,不往下执行,
如果设置为非阻塞模式为fcntl(fd, F_SETFL, O_NDELAY),此时执行read函数时,如果没有数据,
则返回-1,程序继续往下执行*/
fcntl(fd, F_SETFL, 0);
}
/*
初始化串口,配置串口的各种参数。
参数:BaudRate:波特率
DataBits:数据位
StopBits:停止位
ParityBit:校验位
*/
bool SerialPort ::InitSerialPort(int BaudRate,int DataBits,int StopBits,int ParityBit)
{
if( -1 == fd)
return false;
if( 0!= tcgetattr (fd,&m_Setting))
{
printf("InitSerialPort tcgetattr() line:%d failed\n",__LINE__);
return false;
}
// 设置波特率
for(int i = 0 ; i<sizeof(m_SpeedArr)/sizeof(int);i++)
{
if( BaudRate == m_SpeedArr[i])
{
tcflush(fd, TCIOFLUSH); // 清空发送接收缓冲区
cfsetispeed(&m_Setting,m_BaudRateArr[i]); // 设置输入波特率
cfsetospeed(&m_Setting,m_BaudRateArr[i]); // 设置输出波特率
break;
}
if(i == sizeof(m_SpeedArr) / sizeof(int))
return false;
}
m_Setting.c_cflag |= CLOCAL;//控制模式, 保证程序不会成为端口的占有者
m_Setting.c_cflag |= CREAD; //控制模式, 使能端口读取输入的数据
// 设置数据位
m_Setting.c_cflag &= ~CSIZE;
switch(DataBits)
{
case 6:m_Setting.c_cflag |= CS6 ; break; //6位数据位
case 7:m_Setting.c_cflag |= CS7 ; break; //7位数据位
case 8:m_Setting.c_cflag |= CS8 ; break; //8位数据位
default:
fprintf(stderr,"unsupported dataBits\n");
return false;
}
// 设置停止位
switch(StopBits)
{
case 1: m_Setting.c_cflag &= ~CSTOPB;break; //1位停止位
case 2: m_Setting.c_cflag |= CSTOPB; break; //2位停止位
default:
return false;
}
// 设置奇偶校验位
switch(ParityBit)
{
case 'n':
case 'N':
m_Setting.c_cflag &= ~PARENB; // 关闭c_cflag中的校验位使能标志PARENB)
m_Setting.c_iflag &= ~INPCK; // 关闭输入奇偶检测
break;
case 'o':
case 'O':
m_Setting.c_cflag |= (PARODD | PARENB);//激活c_cflag中的校验位使能标志PARENB,同时进行奇校验
m_Setting.c_iflag |= INPCK; // 开启输入奇偶检测
break;
case 'e':
case 'E':
m_Setting.c_cflag |= PARENB;//激活c_cflag中的校验位使能标志PARENB
m_Setting.c_cflag &= ~PARODD;// 使用偶校验
m_Setting.c_iflag |= INPCK;// 开启输入奇偶检测
break;
case 's':
case 'S':
m_Setting.c_cflag &= ~PARENB; // 关闭c_cflag中的校验位使能标志PARENB)
m_Setting.c_cflag &= ~CSTOPB; // 设置停止位位一位
break;
default:
fprintf(stderr,"unsupported parityBit\n");
return false;
}
m_Setting.c_oflag &= ~OPOST;// 设置为原始输出模式
m_Setting.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 设置为原始输入模式
/*所谓标准输入模式是指输入是以行为单位的,可以这样理解,输入的数据最开始存储在一个缓冲区里面(但并未真正发送出去),
可以使用Backspace或者Delete键来删除输入的字符,从而达到修改字符的目的,当按下回车键时,输入才真正的发送出去,这样终端程序才能接收到。通常情况下我们都是使用的是原始输入模式,也就是说输入的数据并不组成行。在标准输入模式下,系统每次返回的是一行数据,在原始输入模式下,系统又是怎样返回数据的呢?如果读一次就返回一个字节,那么系统开销就会很大,但在读数据的时候,我们也并不知道一次要读多少字节的数据,
解决办法是使用c_cc数组中的VMIN和VTIME,如果已经读到了VMIN个字节的数据或者已经超过VTIME时间,系统立即返回。*/
m_Setting.c_cc[VTIME] = 1;
m_Setting.c_cc[VMIN] = 1;
/*刷新串口数据
TCIFLUSH:刷新收到的数据但是不读
TCOFLUSH:刷新写入的数据但是不传送
TCIOFLUSH:同时刷新收到的数据但是不读,并且刷新写入的数据但是不传送。 */
tcflush(fd, TCIFLUSH);
// 激活配置
if( 0 != tcsetattr(fd,TCSANOW,&m_Setting))
{
printf("InitSerialPort tecsetattr() %d failed\n",__LINE__);
return false;
}
return true;
}
// 关闭串口
bool SerialPort::CloseSerialPort()
{
if( -1 == fd)
return false;
close(fd);
fd = -1;
return true;
}
//从串口读取数据
int SerialPort::Read(char *readBuffer,const int bufferSize)
{
if( -1 == fd)
return -1;
return read(fd,readBuffer,bufferSize);
}
// 往串口写入数据
int SerialPort::Write(char *writeBuffer,const int bufferSize)
{
if( -1 == fd)
return -1;
return write(fd,writeBuffer,bufferSize);
}
// 线程体,不断地读取数据
void *ReadFunction(void *arg)
{
SerialPort *serialPort = (SerialPort *)arg;
char buffer[100];
static int readSize = 99;
while(1)
{
int len = serialPort->Read(buffer,99);
if(len > 0)
{
buffer[len] = '\0';
printf("receive data:%s, len = %d\n",buffer,len);
}
else
{
printf("cannot receive data\n");
}
sleep(1);
}
}
// 线程体,不断地写入数据
void *WriteFunction(void *arg)
{
SerialPort *serialPort = (SerialPort *)arg;
char buffer[] = "test";
while(1)
{
if(!serialPort->Write(buffer,strlen(buffer)))
{
printf("write failed\n");
}
printf("write: %s\n",buffer);
sleep(1);
}
}
// 开启读线程
void SerialPort::StartRead()
{
if(fd == -1) return;
pthread_t readThread;
if ( pthread_create( &readThread, NULL, ReadFunction, this) )
{
printf("Error creating readThread.\n");
}
}
// 开启写线程
void SerialPort::StartWrite()
{
if(fd == -1) return;
pthread_t writeThread;
if ( pthread_create( &writeThread, NULL, WriteFunction, this) )
{
printf("Error creating writeThread.\n");
}
}
//main.cpp
#include "SerialPort.h"
int main(int argc ,char *argv[])
{
SerialPort *serialPort = new SerialPort(1);
serialPort->InitSerialPort(9600,8,1,'N');
#ifdef WRITE
serialPort->StartWrite();
#endif
#ifdef READ
serialPort->StartRead();
#endif
while(1);
return 0;
}
下面来编译程序:
g++ main.cpp SerialPort.cpp -o read -lpthread -D READ // 读写串口数据的程序
g++ main.cpp SerialPort.cpp -o write-lpthread -D WRITE // 往串口写入数据的程序
到此为止,程序就编译完成了 。
三、测试串口类程序
如果要测试程序,需要用到开发板和串口助手。如果没有硬件环境,那么也可以用虚拟串口来调试。下面的python程序可以建立两个虚拟串口:
#! /usr/bin/env python
#coding=utf-8
import pty
import os
import select
def mkpty():
master1, slave = pty.openpty()
slaveName1 = os.ttyname(slave)
master2, slave = pty.openpty()
slaveName2 = os.ttyname(slave)
print ('Virtual serial port : ', slaveName1, slaveName2)
return master1, master2
if __name__ == "__main__":
master1, master2 = mkpty()
while True:
rl, wl, el = select.select([master1,master2], [], [], 1)
for master in rl:
data = os.read(master, 128)
print ("read %d data." % len(data))
if master==master1:
os.write(master2, data)
else:
os.write(master1, data)
如果是使用虚拟串口,那么程序的编译命令如下:
g++ main.cpp SerialPort.cpp -o read -lpthread -D VITRUALPROT -D READ
g++ main.cpp SerialPort.cpp -o write -lpthread -D VITRUALPROT -D WRITE
运行这个虚拟串口的程序,前提是你的linux 下已经安装好了python。
如果搭好了python的环境,在linux下键入命令
python3 virtualPort.py //virtualPort.py 为我上面虚拟串口程序的文件名
那么就会出现以下信息
图中的,/dev/pts/5 和 /dev/pts8 就是我们建立起来的两个虚拟串口,我们就可以利用这两个虚拟串口来模拟串口之间的通信了。
接着另外打开两个linux 终端,在其中一个终端上运行read程序,在另外一个终端上运行write程序,运行结果如下
可以看到,write中写入的字符串:test,已经成功被read程序中接收。程序测试正常。
————————————END——————————————————
参考:
https://blog.csdn.net/baweiyaoji/article/details/72885633
http://www.21ic.com/jichuzhishi/datasheet/RS232/jiekou/187596.html