赞
踩
在开始本阶段之前,我们需要了解串口通信的知识,常见的几要素:起始位、数据位、校验、停止位以及波特率。尚未了解的,也可以看我之前写的关于串口通信的博客:串口通信 。在前面我们实现了串口驱动,在/dev/目录下可以查看ttyUSB等多个设备文件,使用ifconfig命令之后也可以看到usb0网卡,也可以使用Linux下的软件busybox microcom实现AT指令的发送和接收。但是我们怎么接收来自EC200U模块的信息呢?难道要写个程序启动busybox microcom软件再读取运行AT指令返回的信息?并不是,现在我们需要自己用C代码写一个类似busybox microcom的软件,实现AT指令集的发送和接收,这样我们就可以在C代码里面接收返回的信息了,把它们存放到我们创建的buffer里面,就可以对数据进行操作了。
该结构体主要用于对串口属性的修改,常见的如起始位、数据位、校验、停止位以及波特率等,通过与或等位相应的标志常量就可以对成员的值进行修改,也就是对串口属性的修改。
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);
参数:
返回值:成功返回0,失败返回-1
函数功能: 获取文件描述符对应串口的原始属性,并保存在第二个参数中,通常获取的原始属性需要进行备份,在程序退出之前要将其修改回来,否则无法继续使用串口。
(2)tcsetattr()
函数原型
#include <termios.h>
#include <unistd.h>
int tcsetattr(int fd, int optional_actions,const struct termios *termios_p);
参数:
返回值:成功返回 0 ,失败返回-1
函数功能:设置打开的文件描述符对应的串口的属性
函数原型
int tcflush(int fd,int quene)
参数
返回值:成返回 0 ,失败返回 -1
函数功能:
在打开串口后,串口其实已经可以开始读取 数据了 ,这段时间用户如果没有读取,将保存在缓冲区里,如果用户不想要开始的一段数据,或者发现缓冲区数据有误,可以使用这个函数清空缓冲需要注意,如果是在任务中,需要不停地写入数据到串口设备,千万不能在每次写入数据到设备前,进行flush以前数据的操作,因为两次写入的间隔是业务控制的,内核不会保证在两次写入之间一定把数据发送成功。flush操作一般在打开或者复位串口设备时进行操作。
函数原型
#include <termios.h>
#include <unistd.h>
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
函数参数:
函数功能:设置输入和输出的波特率
其中有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…),不同的平台下串口的名称是不同的,且串口的名称也是可以更改的。
#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
Linux系统下一般用负数表示执行函数出错返回值,返回0表示正常,在这里我也是引用了这一种方式。
在对串口操作之前,先打开串口设备文件
//打开串口文件 #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; }
在使用串口完毕的时候,我们需要进行相应的处理,即清空输入输出的缓冲,并且恢复串口原来的属性。
//关闭串口文件 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; }
对串口进行初始化
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; }
向串口发送信息
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; }
在发送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; }
#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
#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 ; }
我将.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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。