串口通信是最简单的通信方式。即使在USB 非常流行的今天,依然保留了串行通信的方式。网络上已经有大量关于Linux下 C++ 串口编程的文章,但是我依然要写这篇博文。因为网络上的资料不是内容太多,就是过于简短。我想写的恰到好处。
文章中的部分内容来自于:
Linux 下串口编程入门。
http://digilander.libero.it/robang/rubrica/serial.htm
我将这些内容封装成为一个C++类。 serialPort类的代码和serialTest程序是我自己编写的,在ubuntu 下运行正确。它使用clang++编译。
最后,我使用笔记本电脑通过USB 线和一个STM32F429 Mbed 系统modular 2 相连,进行了测试。
#include /*标准输入输出定义*/
#include /*标准函数库定义*/
#include /*Unix 标准函数定义*/
#include
#include
#include /*文件控制定义*/
#include /*PPSIX 终端控制定义*/
#include /*错误号定义*/
在 Linux 下串口文件是位于 /dev 下。可以
ll /dev/ 查看。
在windows subsystem for linux 中,如果window 下为COM36 的话,对应的就是“/dev/ttyS36”。
另一方面,需要键入
chmod 666 /dev/ttyS36
打开串口是通过使用标准的文件打开函数操作:
int fd;
/*以读写方式打开串口*/
fd = open( "/dev/ttyS0", O_RDWR);
if (-1 == fd){
/* 不能打开串口一*/
perror(" 提示错误!");
}
最基本的设置串口包括波特率设置,效验位和停止位设置。
串口的设置主要是设置 struct termios 结构体的各成员值。
struct termio
{ unsigned short c_iflag; /* 输入模式标志 */
unsigned short c_oflag; /* 输出模式标志 */
unsigned short c_cflag; /* 控制模式标志*/
unsigned short c_lflag; /* local mode flags */
unsigned char c_line; /* line discipline */
unsigned char c_cc[NCC]; /* control characters */
};
设置这个结构体很复杂,我这里就只说说常见的一些设置:
波特率设置
下面是修改波特率的代码
struct termios Opt;
tcgetattr(fd, &Opt);
cfsetispeed(&Opt,B19200); /*设置为19200Bps*/
cfsetospeed(&Opt,B19200);
tcsetattr(fd,TCANOW,&Opt);
设置波特率的例子函数:
/**
*@brief 设置串口通信速率
*@param fd 类型 int 打开串口的文件句柄
*@param speed 类型 int 串口速度
*@return void
*/
int speed_arr[] = { B38400, B19200, B9600, B4800, B2400, B1200, B300,
B38400, B19200, B9600, B4800, B2400, B1200, B300, };
int name_arr[] = {38400, 19200, 9600, 4800, 2400, 1200, 300, 38400,
19200, 9600, 4800, 2400, 1200, 300, };
void set_speed(int fd, int speed){
int i;
int status;
struct termios Opt;
tcgetattr(fd, &Opt);
for ( i= 0; i < sizeof(speed_arr) / sizeof(int); i++) {
if (speed == name_arr[i]) {
tcflush(fd, TCIOFLUSH);
cfsetispeed(&Opt, speed_arr[i]);
cfsetospeed(&Opt, speed_arr[i]);
status = tcsetattr(fd, TCSANOW, &Opt);
if (status != 0) {
perror("tcsetattr fd");
return;
}
tcflush(fd,TCIOFLUSH);
}
}
}
效验位和停止位的设置:
设置效验位的函数:
/**
*@brief 设置串口数据位,停止位和效验位
*@param fd 类型 int 打开的串口文件句柄
*@param databits 类型 int 数据位 取值 为 7 或者8
*@param stopbits 类型 int 停止位 取值为 1 或者2
*@param parity 类型 int 效验类型 取值为N,E,O,,S
*/
int set_Parity(int fd,int databits,int stopbits,int parity)
{
struct termios options;
if ( tcgetattr( fd,&options) != 0) {
perror("SetupSerial 1");
return(FALSE);
}
options.c_cflag &= ~CSIZE;
switch (databits) /*设置数据位数*/
{
case 7:
options.c_cflag |= CS7;
break;
case 8:
options.c_cflag |= CS8;
break;
default:
fprintf(stderr,"Unsupported data size\n"); return (FALSE);
}
switch (parity)
{
case 'n':
case 'N':
options.c_cflag &= ~PARENB; /* Clear parity enable */
options.c_iflag &= ~INPCK; /* Enable parity checking */
break;
case 'o':
case 'O':
options.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'e':
case 'E':
options.c_cflag |= PARENB; /* Enable parity */
options.c_cflag &= ~PARODD; /* 转换为偶效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'S':
case 's': /*as no parity*/
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;break;
default:
fprintf(stderr,"Unsupported parity\n");
return (FALSE);
}
/* 设置停止位*/
switch (stopbits)
{
case 1:
options.c_cflag &= ~CSTOPB;
break;
case 2:
options.c_cflag |= CSTOPB;
break;
default:
fprintf(stderr,"Unsupported stop bits\n");
return (FALSE);
}
/* Set input parity option */
if (parity != 'n')
options.c_iflag |= INPCK;
tcflush(fd,TCIFLUSH);
options.c_cc[VTIME] = 150; /* 设置超时15 seconds*/
options.c_cc[VMIN] = 0; /* Update the options and do it NOW */
if (tcsetattr(fd,TCSANOW,&options) != 0)
{
perror("SetupSerial 3");
return (FALSE);
}
return (TRUE);
}
注意
如果不是开发终端之类的,只是串口传输数据,而不需要串口来处理,那么使用原始模式(Raw Mode)方式来通讯,设置方式如下
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); /*Input*/
options.c_oflag &= ~OPOST; /*Output*/
设置好串口之后,读写串口就很容易了,把串口当作文件读写就是。
发送数据
char buffer[1024];int Length;int nByte;nByte = write(fd, buffer ,Length)
读取串口数据
使用文件操作read函数读取,如果设置为原始模式(Raw Mode)传输数据,那么read函数返回的字符数是实际串口收到的字符数。
可以使用操作文件的函数来实现异步读取,如fcntl,或者select等来操作。
char buff[1024];int Len;int readByte = read(fd,buff,Len);
关闭串口就是关闭文件。
close(fd);
我将上面的内容封装在了一个serialPort 类中,以便应用程序调用。
serialPort.hpp
#include /*标准输入输出定义*/
#include /*标准函数库定义*/
#include /*Unix 标准函数定义*/
#include
#include
#include /*文件控制定义*/
#include /*PPSIX 终端控制定义*/
#include /*错误号定义*/
#include
using namespace std;
class serialPort
{
private:
int fd;
struct termios Opt;
int speed_arr[14] = { B38400, B19200, B9600, B4800, B2400, B1200, B300,
B38400, B19200, B9600, B4800, B2400, B1200, B300, };
int name_arr[14] = {38400, 19200, 9600, 4800, 2400, 1200, 300, 38400,
19200, 9600, 4800, 2400, 1200, 300, };
public:
serialPort();
bool OpenPort(const char * dev);
int setup(int speed,int flow_ctrl,int databits,int stopbits,int parity) ;
void set_speed(int speed);
int set_Parity(int databits,int stopbits,int parity);
int readBuffer(uint8_t * buffer,int size);
int writeBuffer(uint8_t * buffer,int size);
uint8_t getchar();
void ClosePort();
};
serialPort.cpp
#include "serialPort.hpp"
#include
#include
using namespace std;
serialPort::serialPort()
{
fd=-1;
}
bool serialPort::OpenPort(const char * dev)
{
char* _dev=new char[32];
strcpy(_dev,dev);
fd = open(_dev, O_RDWR); //| O_NOCTTY | O_NDELAY
if (-1 == fd)
{
perror("Can't Open Serial Port");
return false;
}
int DTR_flag;
DTR_flag = TIOCM_DTR;
ioctl(fd,TIOCMBIS,&DTR_flag);//Set RTS pin
return true;
}
int serialPort::setup(int speed,int flow_ctrl,int databits,int stopbits,int parity)
{
int i;
int status;
struct termios options;
/*tcgetattr(fd,&options)得到与fd指向对象的相关参数,并将它们保存于options,该函数还可以测试配置是否正确,该串口是否可用等。若调用成功,函数返回值为0,若调用失败,函数返回值为1.
*/
if( tcgetattr( fd,&options) != 0)
{
perror("SetupSerial 1");
return(false);
}
//设置串口输入波特率和输出波特率
for ( i= 0; i < sizeof(speed_arr) / sizeof(int); i++)
{
if (speed == name_arr[i])
{
cfsetispeed(&options, speed_arr[i]);
cfsetospeed(&options, speed_arr[i]);
}
}
//修改控制模式,保证程序不会占用串口
options.c_cflag |= CLOCAL;
//修改控制模式,使得能够从串口中读取输入数据
options.c_cflag |= CREAD;
//设置数据流控制
switch(flow_ctrl)
{
case 0 ://不使用流控制
options.c_cflag &= ~CRTSCTS;
break;
case 1 ://使用硬件流控制
options.c_cflag |= CRTSCTS;
break;
case 2 ://使用软件流控制
options.c_cflag |= IXON | IXOFF | IXANY;
break;
}
//设置数据位
//屏蔽其他标志位
options.c_cflag &= ~CSIZE;
switch (databits)
{
case 5 :
options.c_cflag |= CS5;
break;
case 6 :
options.c_cflag |= CS6;
break;
case 7 :
options.c_cflag |= CS7;
break;
case 8:
options.c_cflag |= CS8;
break;
default:
fprintf(stderr,"Unsupported data size\n");
return (false);
}
//设置校验位
switch (parity)
{
case 'n':
case 'N': //无奇偶校验位。
options.c_cflag &= ~PARENB;
options.c_iflag &= ~INPCK;
break;
case 'o':
case 'O'://设置为奇校验
options.c_cflag |= (PARODD | PARENB);
options.c_iflag |= INPCK;
break;
case 'e':
case 'E'://设置为偶校验
options.c_cflag |= PARENB;
options.c_cflag &= ~PARODD;
options.c_iflag |= INPCK;
break;
case 's':
case 'S': //设置为空格
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
break;
default:
fprintf(stderr,"Unsupported parity\n");
return (false);
}
// 设置停止位
switch (stopbits)
{
case 1:
options.c_cflag &= ~CSTOPB; break;
case 2:
options.c_cflag |= CSTOPB; break;
default:
fprintf(stderr,"Unsupported stop bits\n");
return (false);
}
//修改输出模式,原始数据输出
options.c_oflag &= ~OPOST;
options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
//options.c_lflag &= ~(ISIG | ICANON);
//设置等待时间和最小接收字符
options.c_cc[VTIME] = 1; /* 读取一个字符等待1*(1/10)s */
options.c_cc[VMIN] = 1; /* 读取字符的最少个数为1 */
//如果发生数据溢出,接收数据,但是不再读取 刷新收到的数据但是不读
tcflush(fd,TCIFLUSH);
//激活配置 (将修改后的termios数据设置到串口中)
if (tcsetattr(fd,TCSANOW,&options) != 0)
{
perror("com set error!\n");
return (false);
}
return (true);
}
int serialPort::readBuffer(uint8_t * buffer,int size)
{
return read(fd, buffer, size);
}
int serialPort::writeBuffer(uint8_t * buffer,int size)
{
return write(fd, buffer ,size);
}
uint8_t serialPort::getchar()
{
uint8_t t;
read(fd, &t, 1);
return t;
}
void serialPort::ClosePort()
{
close(fd);
}
#include "serialPort.hpp"
#include
char buff[512];
const char *dev = "/dev/ttyS36";
int main()
{ serialPort myserial;
int i,nread,nwrite;
cout<<"serialPort Test"<
Mbed OS 的程序
#include "mbed.h"
#include "USBSerial.h"
DigitalOut led1(PC_6);
USBSerial serial;
char buf[32];
void readBuffer(char * buf,int length)
{
int i;
for (i=0;i
在windows wsl 系统中,如果在window 中看到 串口是COM36 的话,在linux 中就为 /dev/ttyS36.
在联调中需要输入如下命令
chmod 666 /dev/ttyS36
./serialTest
还遇到一个怪现象,USB 串口需要先在window 下用串口调试助手联一下COM36 后,在Linux 中才可以返还。这个问题困扰了我一个周末。后来发现 当主机端设置了DTR 之后,Mbed OS 下的USBserial 来检测到 Ready(我使用 wait Ready 函数检测)
于是在linux 端Open 中加入了DTR enable 的程序
int DTR_flag;
DTR_flag = TIOCM_DTR;
ioctl(fd,TIOCMBIS,&DTR_flag);//Set RTS pin
老外的那篇文章写的比较详细。