前言
在学习4G拨号前,首先要了解清楚串口通信的工作原理。搞明白了之后,在现有一个Rx链接了Tx的一个串口上调试实现自收自发的串口通信程序。
一、要掌握的知识
1.串口通信
在上一篇博客中写了关于串口通信方面的的知识。总的来说,我们在Linux下编写串口通信程序时,必须对起始位、数据位、奇偶校验位、停止位、波特率进行初始化。特别是波特率,输入和输出要保持一致。
2.struct termios 结构体及相关函数
termios 函数族提供了一个常规的终端接口,用于控制非同步通信端口。简单来说就是通过这个结构体来对串口进行新的配置。
先来看一下struct termios结构体成员:
struct termios{
unsigned short c_iflag; /* 输入模式标志*/
unsigned short c_oflag; /* 输出模式标志*/
unsigned short c_cflag; /* 控制模式标志*/
unsigned short c_lflag; /*区域模式标志或本地模式标志或局部模式*/
unsigned char c_line; /*行控制line discipline */
unsigned char c_cc[NCC]; /* 控制字符特性*/
};
2.1 c_iflag 输入模式标志
c_iflag:输入模式标志,控制终端输入方式,具体参数如表1所示。

2.2 c_oflag 输出模式标志
c_oflag:输出模式标志,控制终端输出方式,具体参数如表2所示。

2.3 c_cflag 控制模式标志
c_cflag:控制模式标志,指定终端硬件控制信息,具体参数如表3所示

2.4 c_lflag 区域模式标志或本地模式标志或局部模式
c_lflag:本地模式标志,控制终端编辑功能,具体参数如表4所示。

2.4 c_cc[NCC] 控制字符特性
c_cc[NCCS]:控制字符,用于保存终端驱动程序中的特殊字符,如输入结束符等。c_cc中定义了如表5所示的控制字符。

2.5 tcsetattr() 与 tcgetattr()函数
2.5.1 tcsetattr()函数
是用于设置终端参数的函数。成功的时候返回0,失败的时候返回-1,并设置errno的值。
函数原型 :int tcsetattr(int fd, int optional_actions, conststruct termios *termios_p);
fd:打开的终端文件描述符;
optional_actions: 控制修改起作用的时间;
optional_actions可以取如下的值:
TCSANOW: 不等数据传输完毕就立即改变属性。
TCSADRAIN: 等待所有数据传输结束才改变属性。
TCSAFLUSH: 等待所有数据传输结束,清空输入输出缓冲区才改变属性。
termios_p: 保存了要修改的参数;
错误信息:
EBADF: 非法的文件描述符。
EINTR: tcsetattr函数调用被信号中断。
EINVAL: 参数optional_actions使用了非法值,或参数termios中使用了非法值。
ENOTTY: 非终端的文件描述符。
2.5.2 tcgetattr()函数
tcgetattr函数用于获取与终端相关的参数。返回的结果保存在termios 结构体中。
函数原型:int tcgetattr(int fd, struct termios *termios_p);
fd:打开的终端文件描述符;
termios *termios_p: 用来保存从终端获取到的相关参数。
2.6 cfsetispeed() 与 cfsetospeed()
cfsetispeed() 与 cfsetospeed()函数分别用于设置输入和输出的波特率
函数原型
int cfsetispeed(struct termios *termios_p, speed_t speed); int
cfsetospeed(struct termios *termios_p, speed_t speed);
参数说明:
struct termios*termptr:指向termios结构的指针;
speed_t speed: 需要设置的输出波特率;
示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。
二、绘制流程图和设计代码
对于串口模块来说,可以把程序分别封装为下面几个函数:
1、Linux下一切皆文件,所以又开就有关。设计两个函数用于串口的开关,在打开串口时候我们就因该对串口进行初始化所以设计如下两个函数用于串口的开关
1、int serial_open(serial_t *serial, char *devname, long baudrate, char *conf );
2、int serial_close(serial_t *serial);
2、串口间要实现通信,就得有发和接收两个函数。
3、int serial_send(serial_t *serial, char *sbuf, int sbuf_len);
4、int serial_recv(int fd, char *rbuf, int rbuf_len, int timeout);
由于串口是一个模块所以所有的函数应该封装为一个.c和一个.h文件里
serial.h
#ifndef _SERIAL_H
#define _SERIAL_H
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
#define SERIALNAME_LEN 128
typedef struct serial_s{
int fd; //串口文件描述符
long Baudrate; //波特率
int Databits; //数据位
char Parity; //奇偶校验位
int Stopbits; //停止位
int mSend; //一次发送的最大数据
char SerialName[SERIALNAME_LEN]; //端口名字
struct termios OldTermios; //保存原来的端口配置
}serial_t;
int serial_open(serial_t *serial, char *devname, long baudrate, char *conf /*"8N1N"*/);
int serial_close(serial_t *serial);
int serial_send(serial_t *serial, char *sbuf, int sbuf_len);
int serial_recv(int fd, char *rbuf, int rbuf_len, int timeout);
#endif
1.串口的打开和关闭
1.1打开串口
serial_open()函数
int serial_open(serial_t *serial, char *devname, long baudrate, char *conf)
{
int retval;
char Baudrate[32];
struct termios Newtermios;
memset(Baudrate, 0, sizeof(baudrate));
memset(&Newtermios, 0, sizeof(struct termios));
memset(&(serial->OldTermios), 0, sizeof(struct termios));
if(strlen(serial->SerialName) == 0 || devname == NULL)
{
printf("open_serial Invalid parameter\n");
return -1;
}
if(devname != NULL)
{
strncpy(serial->SerialName, devname, SERIALNAME_LEN);
}
else
{
return -2;
}
if(baudrate)
{
serial->Baudrate = baudrate;
}
else
{
serial->Baudrate = 9600;
}
if(conf == NULL)
{
serial->Databits = 8;
serial->Parity = 'N';
serial->Stopbits = 1;
}
else
{
serial->Databits = atoi(&conf[0]);
serial->Parity = conf[1];
serial->Stopbits = atoi(&conf[2]);
}
/*
*
* O_RDWR 读写
* O_NOCTTY 如果打开的文件为终端机设备时, 则不会将该终端机当成进程控制终端机.
* O_NONBLOCK 以不可阻断的方式打开文件, 也就是无论有无数据读取或等待, 都会立即返回进程之中.
*
*/
serial->fd = open(serial->SerialName, O_RDWR | O_NOCTTY | O_NONBLOCK);
if(serial->fd < 0)
{
printf("%s serail open failed: %s\n", serial->SerialName, strerror(errno));
return -3;
}
if((retval = fcntl(serial->fd,F_SETFL,0)) < 0)
{
printf("%s,Fcntl check faile. \n", serial->SerialName);
return -4;
}
/* isattr(int fd) 检测设备fd是否是终端,返回1是,0就不是 */
if(isatty(serial->fd) == 0)
{
printf("%s isn`t a device type\n", serial->SerialName);
return -5;
}
printf("%s is a device type \n", serial->SerialName);
if(serial == NULL)
{
printf("Invalid parameter.\n");
return -6;
}
serial->mSend = 128;
/*Serial port configuration backup*/
if(tcgetattr(serial->fd, &(serial->OldTermios)) != 0)
{
printf("Serial port configuration backup errno\n");
return -7;
}
if(tcgetattr(serial->fd,&Newtermios))
{
printf("Get termios to Newtermios failure:%s\n", strerror(errno));
return -8;
}
/* 修改控制模式,保证程序不会占用串口 */
Newtermios.c_cflag |= CLOCAL;
/* 启动接收器,能够从串口中读取输入数据 */
Newtermios.c_cflag |= CREAD;
/* CSIZE字符大小掩码,将与设置databits相关的标致位置零 */
Newtermios.c_cflag &= ~CSIZE;
Newtermios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
/*
* ICANON: 标准模式
* ECHO: 回显所输入的字符
* ECHOE: 如果同时设置了ICANON标志,ERASE字符删除前一个所输入的字符,WERASE删除前一个输入的单词
* ISIG: 当接收到INTR/QUIT/SUSP/DSUSP字符,生成一个相应的信号
*/
Newtermios.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/*
* BRKINT: BREAK将会丢弃输入和输出队列中的数据(flush),并且如果终端为前台进程组的控制终端,则BREAK将会产生一个
SIGINT信号发送到这个前台进程组
* ICRNL: 将输入中的CR转换为NL
* INPCK: 允许奇偶校验
* ISTRIP: 剥离第8个bits
* IXON: 允许输出端的XON/XOF流控
*/
/* OPOST: 表示处理后输出,按照原始数据输出 */
Newtermios.c_oflag &= ~(OPOST);
/* 波特率的设置 */
if(serial->Baudrate != 0)
{
snprintf(Baudrate, sizeof(Baudrate), "B%d", serial->Baudrate);
cfsetispeed(&Newtermios, (int)baudrate); //输入
cfsetospeed(&Newtermios, (int)baudrate); //输出
}
else
{
cfsetispeed(&Newtermios, B9600);
cfsetospeed(&Newtermios, B9600);
}
/* 设置数据位位数 */
//字符长度,取值范围为CS5、CS6、CS7或CS8
switch(serial->Databits)
{
case 5:
Newtermios.c_cflag |= CS5;
break;
case 6:
Newtermios.c_cflag |= CS6;
break;
case 7:
Newtermios.c_cflag |= CS7;
break;
case 8:
Newtermios.c_cflag |= CS8;
break;
default:
Newtermios.c_cflag |= CS8;
break;
}
/* 设置校验方式 */
switch(serial->Parity)
{
/* 无校验 */
case 'n':
case 'N':
Newtermios.c_cflag &= ~PARENB;
Newtermios.c_iflag &= ~INPCK;
break;
/* 偶校验 */
case 'e':
case 'E':
Newtermios.c_cflag |= PARENB;
Newtermios.c_cflag &= ~PARODD;
Newtermios.c_iflag |= INPCK;
break;
/* 奇校验 */
case 'o':
case 'O':
Newtermios.c_cflag |= PARENB;
Newtermios.c_cflag |= PARODD;
Newtermios.c_iflag |= INPCK;
break;
/* 默认无校验 */
default:
Newtermios.c_cflag &= ~PARENB;
Newtermios.c_iflag &= ~INPCK;
break;
}
/* 设置停止位 */
switch(serial->Stopbits)
{
case 2:
Newtermios.c_cflag |= CSTOPB; //设置两个停止位
break;
default:
Newtermios.c_cflag &= ~CSTOPB; //停止位为1,则要清楚CSTOPB
break;
}
Newtermios.c_cc[VTIME] = 0; //最长等待时间
Newtermios.c_cc[VMIN] = 0; //最小接收字符
if(tcflush(serial->fd, TCIFLUSH))
{
printf("Failed to clear the cache: %s\n", strerror(errno));
return -9;
}
if(tcsetattr(serial->fd, TCSANOW, &Newtermios) != 0)
{
printf("Serial configuration failure:%s\n", strerror(errno));
return -1;
}
printf("Serial Init Successdully...\n");
return serial->fd;
}
注意:输入和输出的波特率必须时相同的。
1.2 关闭串口

serial_close()函数
int serial_close(serial_t *serial)
{
int tcs;
if(!serial)
{
printf("close_serial Invalid parameter\n");
return -1;
}
/* 清空终端未完成的数据输入/输出请求数据 */
if(tcflush(serial->fd, TCIOFLUSH != 0))
{
printf("The input and output queues have been flushed failure: %s\n", strerror(errno));
return -1;
}
printf("The input and output queues have been flushed\n");
tcs = tcsetattr(serial->fd, TCSAFLUSH, &(serial->OldTermios));
if(tcs != 0)
{
printf("%s serial set old options failed: %s\n", serial->SerialName, strerror(errno));
return -2;
}
close(serial->fd);
return 0;
}
再关闭串口前,我们应该恢复串口原来的配置避免影响到后续数据的传输。
2.数据的收发
Linux下串口间的通信本质上是对文件的读写。串口间数据的接收与发送相当于read和write。(这一模块代码较为简单,直接复制粘贴)
2.1 serial_send()函数
int serial_send(serial_t *serial, char *sbuf, int sbuf_len)
{
char *ptr,*end;
int retval;
if(!serial || !sbuf || sbuf_len <= 0)
{
printf("Invalid parameter.\n");
return -1;
}
if(sbuf_len > serial->mSend)
{
ptr = sbuf;
end = sbuf + sbuf_len;
do
{
if(serial->mSend < (end - ptr))
{
retval = write(serial->fd, ptr, serial->mSend);
if(retval <= 0 || retval != serial->mSend)
{
printf("Write to com port[%d] failed:%s\n", serial->fd, strerror(errno));
return -2;
}
ptr += serial->mSend;
}
else
{
retval = write(serial->fd, ptr, (end - ptr));
if(retval <= 0 || retval != (end - ptr))
{
printf("Write to com port[%d] failed:%s\n", serial->fd, strerror(errno));
return -3;
}
ptr += (end - ptr);
}
}while(end > ptr);
}
else
{
retval = write(serial->fd, sbuf, sbuf_len);
if(retval <= 0 || retval != sbuf_len)
{
printf("Write to com port[[%d] failed:%s\n", serial->fd, strerror(errno));
return -4;
}
}
return retval;
}
2.2 serial_recv()函数
int serial_recv(int fd,char *rbuf,int rbuf_len, int timeout)
{
int retval;
fd_set rset;
struct timeval time_out;
if(!rbuf || rbuf_len <= 0)
{
printf("serial_recv Invalid parameter.\n");
return -1;
}
if(timeout) //指定延时等待
{
time_out.tv_sec = (time_t)(timeout / 1000);
time_out.tv_usec = 0;
FD_ZERO(&rset);
FD_SET(fd,&rset);
retval = select(fd + 1,&rset,NULL,NULL,&time_out);
if(retval < 0)
{
printf("%s,Select failed:%s\n",strerror(errno));
return -2;
}
else if(0 == retval)
{
printf("Time Out.\n");
return 0;
}
}
usleep(1000);
retval = read(fd, rbuf, rbuf_len);
if( retval <= 0)
{
printf("Read failed:%s\n",strerror(errno));
return -3;
}
return retval;
}
以上代码是经过自己调试成功后的封装结果,在这个项目里做一个模块使用。以后会根据情况对其进行修改。
参考资料:
https://baike.so.com/doc/3377394-3555619.html
https://baike.so.com/doc/674428-713912.html
https://baike.so.com/doc/23849678-24406351.html
本文详细介绍了在Linux环境下使用C语言实现串口通信的过程,包括掌握串口通信的基本知识,理解struct termios结构体及其相关函数,如tcsetattr()、tcgetattr()、cfsetispeed()和cfsetospeed()。文章还阐述了如何封装串口的打开、关闭、数据发送和接收函数,以及在实际操作中的注意事项。
3万+

被折叠的 条评论
为什么被折叠?



