文章目录
一、前言
在开始本阶段之前,我们需要了解串口通信的知识,常见的几要素:起始位、数据位、校验、停止位以及波特率。尚未了解的,也可以看我之前写的关于串口通信的博客:串口通信 。在前面我们实现了串口驱动,在/dev/目录下可以查看ttyUSB等多个设备文件,使用ifconfig命令之后也可以看到usb0网卡,也可以使用Linux下的软件busybox microcom实现AT指令的发送和接收。但是我们怎么接收来自EC200U模块的信息呢?难道要写个程序启动busybox microcom软件再读取运行AT指令返回的信息?并不是,现在我们需要自己用C代码写一个类似busybox microcom的软件,实现AT指令集的发送和接收,这样我们就可以在C代码里面接收返回的信息了,把它们存放到我们创建的buffer里面,就可以对数据进行操作了。
二、要了解的知识
2.1 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]; /* 控制字符特性*/
};
对于该结构体成员可以设置哪些标准常量,可以看这篇博客,总结很详细:termios 详解
于此结构体相关的函数:
(1)tcgetattr()
函数原型
#include <termios.h>
#include <unistd.h>
int tcgetattr(int fd, struct termios *termios_p);
参数:
- int fd:打开串口文件的文件描述符
- struct termios *termios_p:指向termios 的结构体指针
返回值:成功返回0,失败返回-1
函数功能: 获取文件描述符对应串口的原始属性,并保存在第二个参数中,通常获取的原始属性需要进行备份,在程序退出之前要将其修改回来,否则无法继续使用串口。
(2)tcsetattr()
函数原型
#include <termios.h>
#include <unistd.h>
int tcsetattr(int fd, int optional_actions,const struct termios *termios_p);
参数:
- 打开串口文件的文件描述符
- int actions: 设置属性时,可以控制属性生效的时刻,actions可以取下面几个值:
TCSANOW: 立即生效
TCADRAIN: 改变在所有写入fd 的输出都被传输后生效。这个函数应当用于修改影响输出的参数时使用。(当前输出完成时将值改变)
TCSAFLUSH :改变在所有写入fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃(同TCSADRAIN,但会舍弃当前所有值)。 - struct termios *termios_p:指向termios 的结构体指针
返回值:成功返回 0 ,失败返回-1
函数功能:设置打开的文件描述符对应的串口的属性
2.2 tcflush()
函数原型
int tcflush(int fd,int quene)
参数
- fd: 要操作的文件描述符
- quene: 操作位置,可以取下面三个值:
TCIFLUSH:清空输入队列
TCOFLUSH:清空输出队列
TCIOFLUSH:清空输入输出队列
返回值:成返回 0 ,失败返回 -1
函数功能:
在打开串口后,串口其实已经可以开始读取 数据了 ,这段时间用户如果没有读取,将保存在缓冲区里,如果用户不想要开始的一段数据,或者发现缓冲区数据有误,可以使用这个函数清空缓冲需要注意,如果是在任务中,需要不停地写入数据到串口设备,千万不能在每次写入数据到设备前,进行flush以前数据的操作,因为两次写入的间隔是业务控制的,内核不会保证在两次写入之间一定把数据发送成功。flush操作一般在打开或者复位串口设备时进行操作。
2.3 cfsetispeed()与cfsetospeed()
函数原型
#include <termios.h>
#include <unistd.h>
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
函数参数:
- struct termios *termios_p:指向termios结构体类型的指针
- speed_t speed:因为串口通信没有时钟线,是一种异步的通信,想要通信双方收发信息实现统一,就需要设置输入输出波特率相同,通过man命令就可以查看可以设置的波特率,常用的是B9600和B115200。
函数功能:设置输入和输出的波特率
三、流程图设计与代码实现
其中有5个代码文件:
serial_init.h:头文件,用来声明自己定义的函数,同时定义一个串口属性结构体,以存放解析命令参数的数据。
serial_init.c:源文件,实现对串口设备的IO操作,以及属性设置的函数封装,包括串口打开、串口初始化、读串口、写串口和关闭串口五个函数。
main.c:主函数,实现命令行解析、信号注册,以及采用多路复用select监听串口文件描述符获取标准输入的数据和接收来自串口的数据。
main.h:导入头文件
Makefile:实现自动编译,生成可执行代码
Linux有三类设备:字符设备,块设备,网络设备。那么串口设备属于字符设备。所以串口设备的命名一般为/dev/ttySn(n = 0、1、2…),如果该串口为USB转串口,可能名称为/dev/ttyUSBn(n = 0、1、2…),不同的平台下串口的名称是不同的,且串口的名称也是可以更改的。
serial_init.h
#ifndef _UART_INIT_H_
#define _UART_INIT_H_
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <termios.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#define SERIAL_NAME 128
typedef struct ttyusb_ctx_s{
int fd;//文件描述符
int baudrate;//波特率
int databits;//数据位
char parity;//奇偶校验位
int stopbits;//停止位
char serial_name[SERIAL_NAME];//设备文件名
int msend_len;//单次最大发送长度
int timeout;//读数据时的最长延时
struct termios old_termios;//保存串口初始属性
}ttyusb_ctx_t;
int tty_open(ttyusb_ctx_t *ttyusb_ctx);
int tty_close(ttyusb_ctx_t *ttyusb_ctx);
int tty_init(ttyusb_ctx_t *ttyusb_ctx);
int tty_send(ttyusb_ctx_t *ttyusb_ctx, char *send_buf, int sbuf_len);
int tty_recv(ttyusb_ctx_t *ttyusb_ctx, char *recv_buf, int rbuf_len);
#endif
serial_init.c
Linux系统下一般用负数表示执行函数出错返回值,返回0表示正常,在这里我也是引用了这一种方式。
(1) tty_open()
在对串口操作之前,先打开串口设备文件
//打开串口文件
#include "serial_init.h"
int tty_open(ttyusb_ctx_t *ttyusb_ctx)
{
if (!ttyusb_ctx)
{
printf("The argument invalid!\n");
return -1;
}
ttyusb_ctx->fd = open(ttyusb_ctx->serial_name, O_RDWR|O_NOCTTY|O_NONBLOCK);
if (ttyusb_ctx->fd < 0)
{
printf("Open serial file failure:%s\n", strerror(errno));
return -2;
}
if (!isatty(ttyusb_ctx->fd))
{
printf("%s is not a terminal equipment!\n", ttyusb_ctx->serial_name);
return -3;
}
printf("[%s]Open %s successfully!\n", __func__, ttyusb_ctx->serial_name);
return 0;
}
(2) tty_close()
在使用串口完毕的时候,我们需要进行相应的处理,即清空输入输出的缓冲,并且恢复串口原来的属性。
//关闭串口文件
int tty_close(ttyusb_ctx_t *ttyusb_ctx)
{
int retval = -1;
if (!ttyusb_ctx)
{
printf("The argument invalid!\n");
return -1;
}
retval = tcflush(ttyusb_ctx->fd, TCIOFLUSH);//清空输入输出
if (retval < 0)
{
printf("Failed to clear the input/output buffer:%s\n", strerror(errno));
return -2;
}
retval = tcsetattr(ttyusb_ctx->fd, TCSANOW, &(ttyusb_ctx->old_termios));//恢复串口原始属性,TCSANOW
if (retval < 0)
{
printf("Set old termios failure:%s\n", strerror(errno));
return -3;
}
close(ttyusb_ctx->fd);
printf("Excute tty_close() successfully!\n");
return 0;
}
(3) tty_init()
对串口进行初始化
int tty_init(ttyusb_ctx_t *ttyusb_ctx)
{
int retval = -1;
char baudrate_buf[32] = {0};
struct termios new_termios;
if (!ttyusb_ctx)
{
printf("The argument invalid!\n");
return -1;
}
memset(&new_termios, 0, sizeof(new_termios));
memset(&(ttyusb_ctx->old_termios), 0, sizeof(ttyusb_ctx->old_termios));
retval = tcgetattr(ttyusb_ctx->fd, &(ttyusb_ctx->old_termios));
if (retval < 0)
{
printf("Failed to obtain the current serial port properties!\n");
return -2;
}
retval = tcgetattr(ttyusb_ctx->fd, &new_termios);
if (retval < 0)
{
printf("Failed to obtain the current serial port properties!\n");
return -3;
}
new_termios.c_cflag |= CLOCAL;//忽略解调器线路状态
new_termios.c_cflag |= CREAD;
new_termios.c_cflag &= ~CSIZE;//启动接收器,能够从串口中读取输入数据
new_termios.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
/*
* ICANON: 标准模式
* ECHO: 回显所输入的字符
* ECHOE: 如果同时设置了ICANON标志,ERASE字符删除前一个所输入的字符,WERASE删除前一个输入的单词
* ISIG: 当接收到INTR/QUIT/SUSP/DSUSP字符,生成一个相应的信号
*
* */
new_termios.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/*
* BRKINT: BREAK将会丢弃输入和输出队列中的数据(flush),并且如果终端为前台进程组的控制终端,则BREAK将会产生一个SIGINT信号发送到这个前台进程组
* ICRNL: 将输入中的CR转换为NL
* INPCK: 允许奇偶校验
* ISTRIP: 剥离第8个bits
* IXON: 允许输出端的XON/XOF流控
*
* */
/* OPOST: 表示处理后输出,按照原始数据输出 */
new_termios.c_oflag &= ~(OPOST);
if (ttyusb_ctx->baudrate)
{
snprintf(baudrate_buf, sizeof(baudrate_buf), "B%d", ttyusb_ctx->baudrate);
cfsetispeed(&new_termios, (int)baudrate_buf);//输入波特率
cfsetospeed(&new_termios, (int)baudrate_buf);//输出波特率
}
else
{
cfsetispeed(&new_termios, B115200);//默认波特率
cfsetospeed(&new_termios, B115200);
}
switch (ttyusb_ctx->databits)//数据位
{
case 5:
{
new_termios.c_cflag |= CS5;//5位
break;
}
case 6:
{
new_termios.c_cflag |= CS6;//6位
break;
}
case 7:
{
new_termios.c_cflag |= CS7;//7位
break;
}
case 8:
{
new_termios.c_cflag |= CS8;//8位
break;
}
default:
{
new_termios.c_cflag |= CS8;//默认
break;
}
}
#if 1
switch(ttyusb_ctx->parity)//奇偶校验位
{
case 'n':
case 'N':
{
new_termios.c_cflag &= ~PARENB;//无校验
break;
}
case 'o':
case 'O':
{
new_termios.c_cflag |= (PARODD | PARENB);//奇校验
break;
}
case 'e':
case 'E':
{
new_termios.c_cflag &= ~PARENB;//偶校验
new_termios.c_cflag &= ~PARODD;
}
case 'b':
case 'B':
{
new_termios.c_cflag &= ~PARENB;
new_termios.c_cflag &= ~CSTOP;//空格
}
default:
{
new_termios.c_cflag &= ~PARENB;//默认无校验
break;
}
}
switch(ttyusb_ctx->stopbits)//停止位
{
case 1:
{
new_termios.c_cflag &= ~CSTOPB;//1位停止位
break;
}
case 2:
{
new_termios.c_cflag |= CSTOPB;//2位停止位
break;
}
default:
{
new_termios.c_cflag &= ~CSTOPB;//默认1位停止位
break;
}
}
#endif
//MIN =0 TIME =0 时,如果有数据可读,则read最多返回所要求的字节数,如果无数据可用,则read立即返回0;
new_termios.c_cc[VTIME] = 0;
new_termios.c_cc[VMIN] = 0;
ttyusb_ctx->msend_len = 128;//最长数据发送长度
retval = tcflush(ttyusb_ctx->fd, TCIOFLUSH);//清空输入输出
if (retval < 0)
{
printf("Failed to clear the input/output buffer:%s\n", strerror(errno));
return -3;
}
retval = tcsetattr(ttyusb_ctx->fd, TCSANOW, &new_termios);//启用新的串口文件属性
if(retval < 0)
{
printf("Failed to set new properties of the serial port:%s\n", strerror(errno));
return -4;
}
printf("[%s]Successfully set new properties of the serial port!\n", __func__);
return 0;
}
(4)tty_send()
向串口发送信息
int tty_send(ttyusb_ctx_t *ttyusb_ctx, char *send_buf, int sbuf_len)
{
int retval = -1;
int write_rv = 0;
char *ptr = NULL, *end = NULL;
if (!ttyusb_ctx || !send_buf || (sbuf_len < 0))
{
printf("[%s]The argument invalid!\n", __func__);
return -1;
}
//对能发送的最长的信息和需要发送信息的长度作比较
if (sbuf_len > ttyusb_ctx->msend_len)
{
ptr = send_buf;
end = send_buf + sbuf_len;
do
{
if(ttyusb_ctx->msend_len <(end-ptr))
{
retval = write(ttyusb_ctx->fd, ptr, ttyusb_ctx->msend_len);
if ((retval <= 0) || (retval != ttyusb_ctx->msend_len))
{
printf("[%s]Write data to fd[%d] failure:%s\n", __func__, ttyusb_ctx->fd, strerror(errno));
return -2;
}
write_rv += retval;
ptr += ttyusb_ctx->msend_len;
}
else
{
retval = write(ttyusb_ctx->fd, ptr, (end - ptr));
if ((retval <= 0) || (retval != (end - ptr)))
{
printf("[%s]Write data to fd[%d] failure:%s\n",__func__, ttyusb_ctx->fd, strerror(errno));
return -3;
}
write_rv += retval;
ptr += (end - ptr);
}
}while(ptr < end);
}
else
{
retval = write(ttyusb_ctx->fd, send_buf, sbuf_len);
if((retval <= 0) || (retval != sbuf_len))
{
printf("[%s]Write data to fd[%d] failure:%s\n", __func__, ttyusb_ctx->fd, strerror(errno));
return -4;
}
write_rv += retval;
}
printf("[%s]send_buf:%s\n", __func__ , send_buf);
printf("[%s]write_rv: %d\n", __func__ , write_rv);
return write_rv;
}
(5) tty_recv()
在发送AT指令给4G模块之后,4G模块需要时间进行一定的处理,才能返回数据给树莓派,所以我这里采用了select进行延时阻塞,在一定时间内没读到信息就超时退出,在规定时间内读到立即返回。
//这里不固定接收buffer的大小,因为不同的AT指令返回的字符串大小不一样,如果要查看所有SMS信息的话,就可能很大
int tty_recv(ttyusb_ctx_t *ttyusb_ctx, char *recv_buf, int rbuf_len)
{
int read_rv = -1;
int rv_fd = -1;
fd_set rdset;
struct timeval time_out;
if (!ttyusb_ctx || (rbuf_len < 0))
{
printf("[%s]The argument invalid!\n", __func__);
return -1;
}
if (ttyusb_ctx->timeout)
{
time_out.tv_sec = (time_t)ttyusb_ctx->timeout;
time_out.tv_usec = 0;
FD_ZERO(&rdset);
FD_SET(ttyusb_ctx->fd, &rdset);
rv_fd = select(ttyusb_ctx->fd + 1, &rdset, NULL, NULL, &time_out);
if(rv_fd < 0)
{
printf("[%s]Select() listening for file descriptor error!\n", __func__);
return -2;
}
else if(rv_fd == 0)
{
printf("[%s]Select() listening for file descriptor timeout!\n", __func__);
return -3;
}
}
usleep(1000);
read_rv = read(ttyusb_ctx->fd, recv_buf, rbuf_len);
if (read_rv <= 0)
{
printf("[%s]Read data from fd[%d] failure:%s\n", __func__, ttyusb_ctx->fd, strerror(errno));
return -4;
}
printf("[%s]recv_buf:%s\n", __func__, recv_buf);
return read_rv;
}
流程图
main.h
#ifndef _MAIN_H_
#define _MAIN_H_
#include <termios.h>
#include <unistd.h>
#include <getopt.h>
#include <signal.h>
#include "serial_init.h"
void print_usage(char *program_name);
void install_signal(void);
void handler(int sig);
#endif
main.c
#include "main.h"
int g_stop = 0;
int main(int argc, char *argv[])
{
int rv = - 1;
int rv_fd = -1;
char send_buf[128] = {0};
char recv_buf[128] = {0};
fd_set rdset;
ttyusb_ctx_t ttyusb_ctx;
ttyusb_ctx_t *ttyusb_ctx_ptr;
ttyusb_ctx_ptr = &ttyusb_ctx;
int ch;
int i;
struct option opts[] = {
{"baudrate", required_argument, NULL, 'b'},
{"databits", required_argument, NULL, 'd'},
{"parity", required_argument, NULL, 'p'},
{"stopbits", required_argument, NULL, 's'},
{"serial_name", required_argument, NULL, 'm'},
{"help", no_argument, NULL, 'h'},
{0,0,0,0}
};
while((ch = getopt_long(argc, argv, "b:d:p:s:m:h", opts, NULL)) != -1)
{
switch(ch)
{
case 'b':
{
ttyusb_ctx_ptr->baudrate = atoi(optarg);
break;
}
case 'd':
{
ttyusb_ctx_ptr->databits = atoi(optarg);
break;
}
case 'p':
{
ttyusb_ctx_ptr->parity = optarg[0];
break;
}
case 's':
{
ttyusb_ctx_ptr->stopbits = atoi(optarg);
break;
}
case 'm':
{
strncpy(ttyusb_ctx_ptr->serial_name, optarg, SERIAL_NAME);
break;
}
case 'h':
{
print_usage(argv[0]);
return 0;
}
default:
{
printf("%s input invalid argument!\n", __func__);
return -1;
}
}
}
if(0 == strlen(ttyusb_ctx_ptr->serial_name))
{
printf("Failed to obtain the device name!\n");
return -1;
}
install_signal();
if(tty_open(ttyusb_ctx_ptr) < 0)
{
printf("Failed to open the device file");
return -2;
}
if(tty_init(ttyusb_ctx_ptr) < 0)
{
printf("Failed to initialize the serial port\n");
return -3;
}
while(!g_stop)
{
FD_ZERO(&rdset);//清空文件描述符集合
FD_SET(ttyusb_ctx_ptr->fd, &rdset);//将串口文件fd加入集合
FD_SET(STDIN_FILENO, &rdset);//将标准输入文件fd加入集合
//select多路复用非阻塞监听文件描述符
rv_fd = select(ttyusb_ctx_ptr->fd + 1, &rdset, NULL, NULL, NULL);
if(rv_fd < 0)
{
printf("Select listening for file descriptor error!\n");
rv = -4;
goto CleanUp;
}
else if(0 == rv_fd)
{
printf("Select listening for file descriptor timeout!\n");
rv = -5;
goto CleanUp;
}
else
{
if(FD_ISSET(STDIN_FILENO, &rdset))//判断是否是标准输入响应
{
memset(send_buf, 0, sizeof(send_buf));//清空buffer
fgets(send_buf, sizeof(send_buf), stdin);
i = strlen(send_buf);
strcpy(&send_buf[i-1], "\r");//发送AT指令时,需要在指令后面加上\r
if(tty_send(ttyusb_ctx_ptr, send_buf, strlen(send_buf)) < 0)
{
printf("Failed to send data through the serial port\n");
rv = -6;
goto CleanUp;
}
printf("Succeeded in send data serial port data:%s\n", recv_buf);
fflush(stdin);//冲洗输入流
}
else if(FD_ISSET(ttyusb_ctx_ptr->fd, &rdset))//判断是否是串口文件描述符响应
{
memset(recv_buf, 0, sizeof(recv_buf));
//读串口发来的信息
if(tty_recv(ttyusb_ctx_ptr, recv_buf, sizeof(recv_buf), 0) < 0)
{
printf("Failed to receive serial port data!\n");
rv = -7;
goto CleanUp;
}
printf("Succeeded in receiving serial port data:%s\n", recv_buf);
fflush(stdout);//冲洗输出流
}
}
}
return 0;
CleanUp:
tty_close(ttyusb_ctx_ptr);
return rv;
}
//打印帮助信息
void print_usage(char *program_name)
{
printf("Usage:%s[OPTION]\n\n", program_name);
printf("-b[baudrate]:Select baud rate, for example 115200 and 9600.\n");
printf("-p[parity]:Select parity check, for example n N e E o O.\n");
printf("-s[stopbits]:Select stop bit, for example 1 and 2.\n");
printf("-m[serial_name]:Select device file, for example /dev/ttyUSB0.\n");
printf("-h[help]:Printing Help Information.\n");
printf("For example:./SMS -b 115200 -p n -s 1 -m /dev/ttyUSB0 \n\n");
}
//对信号进行处理
void handler(int sig)
{
switch(sig)
{
case SIGINT:
{
printf("Process captured SIGINT signal!\n");
g_stop = 1;
break;
}
case SIGTERM:
{
printf("Process captured SIGTERM signal!\n");
g_stop = 1;
break;
}
case SIGSEGV:
{
printf("Process captured SIGSEGV signal!\n");
g_stop = 1;
exit(0);
break;
}
case SIGPIPE:
{
printf("Process captured SIGPIPE signal!\n");
g_stop = 1;
break;
}
default:
break;
}
return ;
}
//注册信号
void install_signal(void)
{
struct sigaction sigact;
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigact.sa_handler = handler;
sigaction(SIGINT, &sigact, 0);
sigaction(SIGTERM, &sigact, 0);
sigaction(SIGPIPE, &sigact, 0);
sigaction(SIGSEGV, &sigact, 0);
return ;
}
Makefile
我将.h文件都放到了inc目录下,Makefile和.c文件都放到了src目录下,执行make编译将会生成bin目录,里面生成了可执行文件。
APPNAME = SMS
OBJDIR = `pwd`/../
APPPATH = ${OBJDIR}bin/${APPNAME}
CC = gcc
CFLAGS += -I ${OBJDIR}inc/
#LIBPATH = ${OBJDIR}lib/
#LDFLAGS += -L ${LIBPATH} -lsqlite3 -lmosquitto -ldl -lpthread
server:create_file
${CC} `pwd`/*.c -o ${APPPATH} ${CFLAGS}
create_file:
$(shell if [ ! -d $(OBJDIR)bin ]; then mkdir -p $(OBJDIR)bin; fi)
.PHONY:distclean
distclean:
rm -rf ${OBJDIR}bin
四、运行结果
五、遇到的问题及解决办法
1、上面的代码相当于模拟了一个串口调试助手,但是我们只能发送AT指令,而不能向特定的号码发送消息,这是因为我们需要对发送的信息进行UTF8到Unicode的转码。
2、如果不将输入输出缓存区清空的话,可能会造成一直在发送,所以要用发fflush()清空。
3、每一个AT指令后面都要以\r或者是\r\n结束。这是AT标准AT指令集 里面规定的。
4、fgets()和gets()都是从标准输入读取一行数据,但是不同的是fgets()在读完数据之后会自动加上换行,就是\n,而且其需要指定写入缓冲区的大小,更为安全
参考链接:
https://blog.csdn.net/morixinguan/article/details/80898172
https://blog.csdn.net/weixin_45121946/article/details/107130238