需求
需要在 linux 中配置好串口,并且通过串口收发数据
解决
打开串口
打开串口直接使用 open
函数即可,需要注意的是, flag
中的参数
O_NOCTTY
, O_NDELAY
.
- O_NOCTTY:告诉Unix这个程序不想成为“控制终端”控制的程序,不说明这个标志的话,任何输入都会影响你的程序。
- O_NDELAY:告诉Unix这个程序不关心DCD信号线状态,即其他端口是否运行,不说明这个标志的话,该程序就会在DCD信号线为低电平时停止。但是在
man
中,是把O_NDELAY
和O_NONBLOCK
放在一起,并没有说明两者的区别,所以这这个参数的作用存疑。 open
时需要指定串口的串口号,比如/dev/ttyS0
int OpenDevice(const char * dev)
{
int fd = 0;
fd = open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (fd < 0) {
fprintf(stderr, "Can not open serial port %s", dev);
return -1;
}
cout << "open " << dev << " Ok!" << endl;
return fd;
}
if ((serial_fd = OpenDevice(kSerialName.c_str())) < 0) {
return -1;
}
串口配置
- 对于任何硬件的配置,都应该先保存原来的旧配置,然后配置新的配置,等到程序退出的时候,把硬件恢复到原来的旧配置,防止影响其他程序的运行。
- 串口的配置主要是:波特率,数据位,校验位,停止位,流控这些。
波特率
static void SetSpeed(struct termios * opt, int speed)
{
switch (speed) {
case 9600:
cfsetispeed(opt, B9600);
cfsetospeed(opt, B9600);
break;
case 115200:
cfsetispeed(opt, B115200);
cfsetospeed(opt, B115200);
break;
default:
cfsetispeed(opt, B9600);
cfsetospeed(opt, B9600);
break;
}
}
数据位
static void SetBits(struct termios * opt, int bits)
{
opt->c_cflag &= ~CSIZE;
switch (bits) {
case 8:
opt->c_cflag |= CS8;
break;
case 7:
opt->c_cflag |= CS7;
break;
case 6:
opt->c_cflag |= CS6;
break;
case 5:
opt->c_cflag |= CS5;
break;
default:
opt->c_cflag |= CS8;
break;
}
}
校验位
static void SetParity(struct termios * opt, char parity)
{
switch (parity) {
case 'N':
opt->c_cflag &= ~PARENB;
break;
case 'E':
opt->c_cflag |= PARENB;
opt->c_cflag &= ~PARODD;
break;
case 'O':
opt->c_cflag |= PARENB;
opt->c_cflag |= PARODD;
break;
default:
opt->c_cflag &= ~PARENB;
break;
}
}
停止位
static void SetStop(struct termios * opt, int stop)
{
switch (stop) {
case 1:
opt->c_cflag &= ~CSTOPB;
break;
case 2:
opt->c_cflag |= CSTOPB;
break;
default:
opt->c_cflag &= ~CSTOPB;
break;
}
}
阻塞
static void SetWait(struct termios * opt, int min, int time)
{
opt->c_cc[VMIN] = min;
opt->c_cc[VTIME] = time;
}
其他配置
CLOCAL
表示忽略调制解调器的相关状态行。
static void SetControl(struct termios * opt)
{
opt->c_cflag |= CLOCAL | CREAD;
}
总的配置
int ConfigDevice(int fd, struct termios * orig, struct termios * opt,
int speed, int bits, char parity, int stop,
int min, int time)
{
//struct termios new_term, old_term;
int err = 0;
if ((err = tcgetattr(fd, orig))) {
fprintf(stderr, "get old terminal config error, %d\n", err);
return err;
}
//memset(opt, 0, sizeof(struct termios));
*opt = {};
SetControl(opt);
SetSpeed(opt, speed);
SetBits(opt, bits);
SetParity(opt, parity);
SetStop(opt, stop);
SetWait(opt, min, time);
// clear flush: TCIFLUSH, TCOFLUSH, TCIOFLUSH
tcflush(fd, TCIOFLUSH);
if ((err = tcsetattr(fd, TCSANOW, opt))) {
fprintf(stderr, "set new terminal config error, %d\n", err);
return err;
}
cout << "serial config ok!" << endl;
return 0;
}
if (ConfigDevice(serial_fd, &old_term, &new_term, 115200, 8, 'N', 1, 0, 0) < 0) {
//return -1;
goto Error;
}
恢复硬件原来的配置
int ResetDevice(int fd, struct termios * orig)
{
int err = 0;
tcflush(fd, TCIOFLUSH);
if ((err = tcsetattr(fd, TCSANOW, orig))) {
fprintf(stderr, "reset terminal config error, %d\n", err);
return err;
}
cout << "reset serial ok!" << endl;
return 0;
}
if (ResetDevice(serial_fd, &old_term) < 0) {
//return -1;
goto Error;
}
串口读写
串口读写如果要求不高的话,简单的 read
, write
就可以解决问题。如果是希望实现稳定的功能,那么就需要使用 select
.
简单读写
while (1) {
cout << "try to read" << endl;
int len = read(serial_fd, buf, sizeof(buf));
if (len <= 0) {
sleep(1);
continue;
}
if (len + 1 <= sizeof(buf)) {
buf[len] = '\0';
} else {
buf[sizeof(buf) - 1] = '\0';
}
printf("len = %d, data: %s\n", len, buf);
write(serial_fd, buf, len);
sleep(1);
break;
}
使用 select 来进行读写
使用 select
有几个地方需要注意:
- 使用前,需要把监视的
fd
都放到集合中去 select
如果是因为文件改动而返回的话,那么集合会被修改成只有改动文件的位置位。所以每次使用之前,都需要重新配置集合。select
返回之后,再使用ioctl
获取文件可读的字节数。
memcpy(send_buf, s, send_len);
fd_set set_inputs, set_test;
struct timeval timeout;
FD_ZERO(&set_inputs);
FD_SET(serial_fd, &set_inputs);
while (1) {
printf("write count = %d, len = %d : %s\n",
send_count++, send_len, send_buf);
//cout << "write cout: " << send_count++ << endl;
write(serial_fd, send_buf, send_len);
set_test = set_inputs;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
int result = select(serial_fd + 1, &set_test, (fd_set *)nullptr,
(fd_set *)nullptr, &timeout);
switch (result) {
case 0:
cout << "timeout" << endl;
continue;
case -1:
fprintf(stderr, "select error\n");
goto Error;
default:
cout << "select default" << endl;
int nread = 0;
if (FD_ISSET(serial_fd, &set_test)) {
ioctl(serial_fd, FIONREAD, &nread);
if (nread == 0) {
cout << "ioctl nread = 0" << endl;
continue;
}
nread = read(serial_fd, receive_buf, nread);
nread + 1 > kReceiveBufSize ?
receive_buf[kReceiveBufSize - 1] = '\0' :
receive_buf[nread] = '\0';
printf("receive count = %d, len = %d : %s\n",
receive_count++, nread, receive_buf);
//cout << "receive: " << receive_buf << endl;
}
break;
}
}
错误处理
因为可能有多个地方会出错,所以使用统一的 goto
语句方便错误的集中处理,虽然说 goto
不提倡使用,但是当你明确知道你到底在干什么,会产生什么效果的时候, goto
也不是不可用。
Error:
close(serial_fd);
return -1;
强制退出
如果用户使用 C-c
来强行退出的话,还需要把串口配置回原来的配置。建议使用 sigaction
而不是旧的 SIGNAL
.
void SignalHandlerInt(int sig)
{
cout << endl << "user has press Ctrl + c" << endl;
if (ResetDevice(serial_fd, &old_term) < 0) {
}
if (serial_fd > 0) {
close(serial_fd);
}
exit(-1);
}
struct sigaction act;
act.sa_handler = SignalHandlerInt;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGINT, &act, nullptr);
参考
红皮书 Linux 程序设计第4版,第五章终端