串口的物理层协议规定了串口的电气特性,有RS232,RS485,RS422协议。
RS-232与RS-485的区别在于:
1、传输方式不同。 RS-232采取不平衡传输方式,即所谓单端通讯。而RS485则采用平衡传输,即差分传输方式。
2、传输距离不同。RS-232适合本地设备之间的通信,传输距离一般不超过20m。而RS-485的传输距离为几十米到上千米。
3、RS-232 只允许一对一通信,而RS-485 接口在总线上是允许连接多达128个收发器。
4、电平特性不同:RS232逻辑“1”为-3—-15V;逻辑“0”:+3—+15V。RS485逻辑“1”以两线间的电压差+2V ~ +6V表示,逻辑“0”以两线间的电压差-6V ~ 2V表示。
1.可以自定义内部的软件通信协议
2.可以用Modbus协议
在linux中一切皆文件。在串口编程中不管是RS232,RS485标准,软件代码一般都没有任何区别,也就是说,编程是可以不管物理层协议的,只要按照既定的应用层协议来收发数据即可。
串口编程就是对串口进行初始化,并进行读写操作。而串口的初始化包括设定波特率,停止位,奇偶校验位,数据位等参数。
1.首先需要打开串口,才能对串口进行操作
fd = open(Name, O_RDWR);
if(fd == -1)
{
printf("serialport open() error\n");
return -1;
}
else
{
//测试是否为终端设备
if(0 == isatty(STDIN_FILENO))
{
printf("this is not a terminal device\n");
return -1;
}
else
{
printf ("open ");
printf ("%s ", ttyname(fd)); //获取终端设备路径
printf ("succesfully \n");
}
}
2.对串口进行参数设置
//*******************************************************************************
//* 函数名: Set_Speed()
//* 功能: 设置串口的波特率
//* 参数: fd :串口的文件描述符
//* speed:波特率
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
int g_speed_arr[]={B1200,B2400,B4800,B9600,B19200,B38400,B57600,B115200,B230400,B230400,B460800,B921600};
int g_name_arr[] ={1200,2400,4800,9600,19200,38400,57600,115200,230400,230400,460800,921600};
int Set_Speed(int Fd, int Speed)
{
int i;
int ret;
struct termios opt;
ret = tcgetattr(Fd, &opt);
if(-1 == ret)
{
printf("tcgetattr() error\n");
return -1;
}
for (i = 0; i < sizeof(g_speed_arr)/sizeof(int); i++)
{
if (Speed == g_name_arr[i])
{
tcflush(Fd, TCIOFLUSH);
if(-1 == cfsetispeed(&opt, g_speed_arr[i]))
{
printf("cfsetispeed error\n");
return -1;
}
if(-1 == cfsetospeed(&opt, g_speed_arr[i]))
{
printf("cfsetospeed error\n");
return -1;
}
ret = tcsetattr (Fd, TCSANOW, &opt);
if (ret != 0)
{
printf("tcsetattr() error\n");
return -1;
}
tcflush(Fd, TCIOFLUSH);
}
}
return 0;
}
a)波特率数组里面的成员如:B115200,是在 头文件#include
b)通过tcgetattr函数获取原有串口的参数设置,保存到termios 结构体中,后续需要操作termios 结构体的成员来对参数进行设置,termios 这个结构体是设置串口参数的关键。
c)通过cfsetispeed和cfsetospeed来设置输入输出波特率
d)设置为波特率后需要通过tcsetattr函数来对参数写入。
//*******************************************************************************
//* 函数名: Set_Parameter()
//* 功能: 设置串口的其他参数,包括数据位、停止位和奇偶校验位
//* 参数: fd :串口的文件描述符
//* Data_Bits:数据位
//* Stop_Bits:数据位
//* Parity:数据位
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
int Set_Parameter(int Fd, int Data_Bits, int Stop_Bits, int Parity)
{
struct termios opt;
if(-1 == tcgetattr (Fd, &opt)) //获取串口参数
{
printf("tcgetattr() error\n");
return -1;
}
//1.根据屏蔽字标志清空数据位,并设置
opt.c_cflag &= ~CSIZE;
switch(Data_Bits)
{
case 7:
opt.c_cflag |= CS7;
break;
case 8:
opt.c_cflag |= CS8;
break;
default:
printf("Unsupported data size\n");
return -1;
}
//2.设置奇偶校验位
switch(Parity)
{
case 'n':
case 'N':
opt.c_cflag &= ~PARENB; //不校验
opt.c_iflag &= ~INPCK;
break;
case 'o':
case 'O':
opt.c_cflag |= (PARODD | PARENB); //奇校验
opt.c_iflag |= INPCK;
break;
case 'e':
case 'E':
opt.c_cflag |= PARENB; //偶校验
opt.c_cflag &= ~PARODD;
opt.c_iflag |= INPCK;
break;
case 's':
case 'S': //设置为空校验
opt.c_cflag &= ~PARENB;
opt.c_cflag &= ~CSTOPB;
break;
default:
printf("Unsupported parity\n");
return -1;
}
switch(Stop_Bits)
{
case 1:
opt.c_cflag &= ~CSTOPB;
break;
case 2:
opt.c_cflag |= CSTOPB;
break;
default:
printf("Unsupported stop bits\n");
return -1;
}
tcflush(Fd, TCIFLUSH); //清空缓冲区,包括输入缓冲区(驱动程序已经接收到的数据)和输出缓冲区(已经准备好的数据,但还没发送出去)
//设置超时时间和最小返回字节
opt.c_cc[VTIME] = 150; //这个是超时时间,时间为分秒数(分秒为秒的1/10)
opt.c_cc[VMIN] = 0;
if(-1 == tcsetattr(Fd, TCSANOW, &opt)) //修改好了结构体里面的数据后,开始设置串口
{
printf("tcsetattr error\n");
return -1;
}
tcflush(Fd, TCIFLUSH);
//设置完成后,再次读取,与设置好的是否一致
return 0;
}
a)、根据APUE(UNIN 环境高级编程)所言,即便tcsetattr该函数没有出错,返回成功,我们也有责任检查该函数是否执行了所有要求的动作。这就意味着,在调用tcsetattr设置所希望的属性后,需要再次调用tcgetattr,然后将实际串口属性与所希望的属性相比较,已检测两者是否有区别。上述代码还没有完成这一步。
3、串口读写函数
//*******************************************************************************
//* 函数名: Write_Serial_Data()
//* 功能: 读取串口数据
//* 参数: Fd:串口的文件描述符
//* Data:发送的数据帧
//* Data_Len:数据长度
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
int Write_Serial_Data(int Fd, char *Data,int Data_Len)
{
int len;
len = write(Fd,Data,Data_Len);
if(len == Data_Len)
{
printf("send data is %s\n",Data);
return len;
}
else
{
tcflush(Fd,TCOFLUSH);
return -1;
}
return -1;
}
//*******************************************************************************
//* 函数名: Read_Serial_Data()
//* 功能: 读取串口数据
//* 参数: 无
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
void Read_Serial_Data(const int Fd)
{
char buf[255]={0};
int nread = 0;
int fd = Fd;
fd_set rset;
//采用多路复用IO模型
while(1)
{
FD_ZERO(&rset);
FD_SET(fd, &rset);
select(fd+1, &rset, NULL,NULL,NULL);//将会阻塞等待,直到有数据来
if(FD_ISSET(fd, &rset)) //如果是Fd的数据来了
{
while((nread = read(fd, buf, sizeof(buf))) > 0)
{
printf("nread = %d,%s\n",nread, buf);
memset(buf, 0 , sizeof(buf));
//对数据进行解析
}
}
}
}
a)、串口的读写操作也就是对read/write函数的调用
b)、IO操作模型,主要有四类。阻塞IO、非阻塞IO,多路复用IO、异步IO。
阻塞IO:即一个死循环,包含着一个read函数,read函数会一直阻塞等待,直到到数据的来临(浪费资源,等待时机太久,CPU资源浪费)
非阻塞IO:人为得将IO口设置为非阻塞模式,即read函数有没有数据都立即返回,一直循环读取(浪费资源,CPU做了很多无用功,在没有数据来临事也进行读取操作,不断重复,知道数据来临)
多路复用IO:通过select函数,来等到一个或者多个IO口的数据,如果有其中一个IO口准备好了数据,那么select函数返回。本质上讲,多路复用IO也是一个阻塞IO模型,一样需要等待而浪费资源,但是它有一个明显的优点,就是可以在单线程中处理多个IO的输入输出,而不用多线程。因为select函数可以同时监听多个IO。
异步IO:类似于如果当前没有数据读取,则执行其他有用的任务,一旦有数据到来,才会通知去处理。
一个完整的demo代码如下
//*******************************************************************************
//* 函数名: Set_Speed()
//* 功能: 设置串口的波特率
//* 参数: fd :串口的文件描述符
//* speed:波特率
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
int Set_Speed(int Fd, int Speed)
{
int i;
int ret;
struct termios opt;
ret = tcgetattr(Fd, &opt);
if(-1 == ret)
{
printf("tcgetattr() error\n");
return -1;
}
for (i = 0; i < sizeof(g_speed_arr)/sizeof(int); i++)
{
if (Speed == g_name_arr[i])
{
tcflush(Fd, TCIOFLUSH);
if(-1 == cfsetispeed(&opt, g_speed_arr[i]))
{
printf("cfsetispeed error\n");
return -1;
}
if(-1 == cfsetospeed(&opt, g_speed_arr[i]))
{
printf("cfsetospeed error\n");
return -1;
}
ret = tcsetattr (Fd, TCSANOW, &opt);
if (ret != 0)
{
printf("tcsetattr() error\n");
return -1;
}
tcflush(Fd, TCIOFLUSH);
}
}
return 0;
}
//*******************************************************************************
//* 函数名: Set_Parameter()
//* 功能: 设置串口的其他参数,包括数据位、停止位和奇偶校验位
//* 参数: fd :串口的文件描述符
//* Data_Bits:数据位
//* Stop_Bits:数据位
//* Parity:数据位
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
int Set_Parameter(int Fd, int Data_Bits, int Stop_Bits, int Parity)
{
struct termios opt;
if(-1 == tcgetattr (Fd, &opt))
{
printf("tcgetattr() error\n");
return -1;
}
//1.根据屏蔽字标志清空数据位,并设置
opt.c_cflag &= ~CSIZE;
switch(Data_Bits)
{
case 7:
opt.c_cflag |= CS7;
break;
case 8:
opt.c_cflag |= CS8;
break;
default:
printf("Unsupported data size\n");
return -1;
}
//2.设置奇偶校验位
switch(Parity)
{
case 'n':
case 'N':
opt.c_cflag &= ~PARENB; //不校验
opt.c_iflag &= ~INPCK;
break;
case 'o':
case 'O':
opt.c_cflag |= (PARODD | PARENB); //奇校验
opt.c_iflag |= INPCK;
break;
case 'e':
case 'E':
opt.c_cflag |= PARENB; //偶校验
opt.c_cflag &= ~PARODD;
opt.c_iflag |= INPCK;
break;
case 's':
case 'S': //设置为空校验
opt.c_cflag &= ~PARENB;
opt.c_cflag &= ~CSTOPB;
break;
default:
printf("Unsupported parity\n");
return -1;
}
switch(Stop_Bits)
{
case 1:
opt.c_cflag &= ~CSTOPB;
break;
case 2:
opt.c_cflag |= CSTOPB;
break;
default:
printf("Unsupported stop bits\n");
return -1;
}
tcflush(Fd, TCIFLUSH);
//设置超时时间和最小返回字节
opt.c_cc[VTIME] = 150;
opt.c_cc[VMIN] = 0;
if(-1 == tcsetattr(Fd, TCSANOW, &opt))
{
printf("tcsetattr error\n");
return -1;
}
tcflush(Fd, TCIFLUSH);
//设置完成后,再次读取,与设置好的是否一致
return 0;
}
//*******************************************************************************
//* 函数名: Serial_Init()
//* 功能: 初始化串口
//* 参数: 无
//* 返回值: -1:失败 否则:串口的文件描述符
//*******************************************************************************
int Serial_Init(char *Name, int Speed, int Data_Bits, int Stop_Bits, char Parity)
{
int fd;
int ret;
pthread_t id;
fd = open(Name, O_RDWR);
if(fd == -1)
{
printf("serialport open() error\n");
return -1;
}
else
{
//测试是否为终端设备
if(0 == isatty(STDIN_FILENO))
{
printf("this is not a terminal device\n");
return -1;
}
else
{
printf ("open ");
printf ("%s ", ttyname(fd)); //获取终端设备路径
printf ("succesfully \n");
}
}
if(-1 == Set_Speed(fd, Speed))
{
printf("set_speed Error\n");
return -1;
}
if(-1 == Set_Parameter(fd, Data_Bits, Stop_Bits, Parity))
{
printf("Set Parity Error\n");
return -1;
}
//创建获取数据线程
ret= pthread_create(&id,NULL,(void*)Read_GM_Data,NULL );
if(ret)
{
printf("create Read_Data pthread error\n");
return -1;
}
return fd;
}
//*******************************************************************************
//* 函数名: Read_Serial_Data()
//* 功能: 读取串口数据
//* 参数: 无
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
void Read_Serial_Data(const int Fd)
{
char buf[255]={0};
int nread = 0;
int fd = Fd;
fd_set rset;
while(1)
{
FD_ZERO(&rset);
FD_SET(fd, &rset);
select(fd+1, &rset, NULL,NULL,NULL);//将会阻塞等待,直到有数据来
if(FD_ISSET(fd, &rset)) //如果是Fd的数据来了
{
while((nread = read(fd, buf, sizeof(buf))) > 0)
{
printf("nread = %d,%s\n",nread, buf);
memset(buf, 0 , sizeof(buf));
//解析数据
}
}
}
}
//*******************************************************************************
//* 函数名: Write_Serial_Data()
//* 功能: 读取串口数据
//* 参数: Fd:串口的文件描述符
//* Data:发送的数据帧
//* Data_Len:数据长度
//* 返回值: 0:成功 -1:失败
//*******************************************************************************
int Write_Serial_Data(int Fd, char *Data,int Data_Len)
{
int len;
len = write(Fd,Data,Data_Len);
if(len == Data_Len)
{
printf("send data is %s\n",Data);
return len;
}
else
{
tcflush(Fd,TCOFLUSH);
return -1;
}
return -1;
}
参考:
APUE 第 14 章 高级IO
APUE 第 18 章 高级IO