一. 前言
前面关于串口通信,串口编程的文章也总结了许多,之前的有一篇文章,写的是编程实现busybox microcom 工具,将功能模块一一分开,其实有一点多此一举了,但是作为我学习Linux下串口编程的记录,我还是决定不修改这篇博客,但是,既然作为串口编程的c文件,应该将所有用于串口通信的功能模块归纳进一个c文件 comport.c ,头文件均放置在 comport.h 中,然后写一个main函数来进行调用,这样,才是一个更具有整体性和完整性的调试工具,后续也修改了不能发送短信的bug,如果需要源码的可以私聊.
二. 短信篇介绍
作为短信篇的开篇,有必要说明一下我想实现的是什么,需要掌握哪些知识…
无论是busybox 中的microcom还是我自己写出来的comport工具,都有一个头大问题,那就是,发送中文短信时,需要进行PDU的编码,我还记得刚拿到4g模块时,就遇到了很多问题,在ATD终于拨通我的手机那一瞬间,眼泪掉下来,紧接着又百度了AT发送短信的命令,看到发送短信还分为TEXT和PDU简直一脸懵逼,在尝试发送TEXT格式短信成功后,兴奋又准备去尝试PDU格式的短信,可是,当我的鼠标下移后,惊恐的发现,这是一个New World…
终于搞了半天才弄明白怎么发送PDU格式的短信,可是,这仅仅停留在别人全部把现成的编码方式告诉我,而且在UTF-8转Unicode 时,用的还是网页在线转码(有点丢人),在这么一个较为繁琐的编码和计算之下,难免会遇到很多错误,下面是我遇到的一些错误:
- 再输入PDU编码前一步是输入AT+CMGS=len,len是PDU编码时的一个中间产物,短信发送时,只有输入的PDU编码严格满足这个值,短信才可以发送.
- 网上找了很多方法来实现 UTF-8 转 Unicode ,有使用wchar_t类型借助wcseln函数实现的,有使用iconv函数实现的,但是最终我还是选择搞懂他们之间的转换规则,自己写代码来实现
- 编码时可能会漏东西,检测出错误还好,如果恰巧AT+CMGS的值是正确的,那接收到的短信就会是一些奇奇怪怪的文字
- 换卡以后,短信中心号码也变了,又要重新按照步骤处理短信中心号
- 电话号码也是需要处理的
总结,每发一次短信,都需要折腾很久,而且能一次成功的几率甚小,于是,我就打算写一个程序,只需要输入收件人号码,短信内容就可以发送短信了,就像手机一样,第一个版本需要手动输入短信中心号,不过后来优化了,下面就来看看我是怎么实现的吧~
三. 串口初始化
关于串口的初始化,我在下面这篇博客做了非常详细的讲解,这里,就总体的梳理梳理
传送门
3.1 流程图
打开串口
因为函数都是自己编写,所以可以统一返回值,我的函数如果不是特殊要求(后面的文章会提到),成功返回0,失败返回负数。
串口初始化
其他的就不过多赘述了,这些都是为发送短信而铺的基石,文章末尾的博客详细总结了这方面内容。
3.2 代码
串口的初始化,服务于后面的程序,只有先将串口的准备工作做完,才可以进行后面的工作
comport.h
/********************************************************************************
* Copyright: (C) 2020 LuXiaoyang<920916829@qq.com>
* All rights reserved.
*
* Filename: comport.h
* Description: This head file of comport.c
*
* Version: 2.0.0(11/07/20)
* Author: LuXiaoyang <920916829@qq.com>
* ChangeLog: 1, Release initial version on "11/07/20 16:09:33"
*
********************************************************************************/
#ifndef _COMPORT_H_
#define _COMPORT_H_
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#define SERIALNAME_LEN 64
typedef struct _st_ComportAttr {
int fd; //串口文件描述符
int BaudRate; //波特率
int DataBits; //数据位
char Parity; //奇偶校验位
int StopBits; //停止位
int mSend_Len; //单次最大发送长度
char SerialName[SERIALNAME_LEN]; //串口名称
struct termios OldTermios; //串口的原始属性
}ComportAttr;
/* Use the serial port name(comport->SerialName) to open and assign the returned file descriptor to comport->fd */
int comport_open(ComportAttr *comport);
/* Set the serial port's baud rate, data bits, parity and other attributes, and set the related Peugeot bits for serial communication */
int comport_init(ComportAttr *comport);
/* Send the serial port command in the parameter and can handle the command with too long length */
int comport_send(ComportAttr *comport,char *sbuf,int sbuf_len);
/* Receive serial data, you can use select multiplexing to specify the time of read according to the value of timeout */
int comport_recv(ComportAttr *comport,char *rbuf,int rbuf_len,int timeout);
/* Close the serial port, clear the serial port buffer, and release the memory */
int comport_close(ComportAttr *comport);
#endif /* ----- #ifndef _COMPORT_H_ ----- */
comport.c
/*********************************************************************************
* Copyright: (C) 2020 LuXiaoyang<920916829@qq.com>
* All rights reserved.
*
* Filename: comport.c
* Description: The file contains the functions of opening, closing,
* initializing, and communicating with the serial port.
*
* Version: 1.0.0(11/07/20)
* Author: LuXiaoyang <920916829@qq.com>
* ChangeLog: 1, Release initial version on "11/07/20 15:07:25"
*
********************************************************************************/
#include "comport.h"
int comport_open(ComportAttr *comport)
{
int retval = -1;
if(NULL == comport)
{
printf("%s,Invalid parameter\n",__func__);
return retval;
}
/*
* O_NOCTTY表示打开的是一个终端设备,程序不会成为该
* 端口的控制终端,O_NONBLOCK使得read处于非阻塞模式
*
* */
comport->fd = open(comport->SerialName,O_RDWR | O_NOCTTY | O_NONBLOCK);
if(comport->fd < 0)
{
printf("%s,Open %s failed:%s\n",__func__,comport->SerialName,strerror(errno));
return -1;
}
/* 检查串口是否处于阻塞态 */
if((retval = fcntl(comport->fd,F_SETFL,0)) < 0)
{
printf("%s,Fcntl check faile.\n",__func__);
return -2;
}
printf("Starting serial communication process ");
/* 检查该文件描述符是否对应了终端设备 */
if(0 == isatty(comport->fd))
{
printf("%s:[%d] is not a Terminal equipment.\n",comport->SerialName,comport->fd);
return -3;
}
printf("Open %s successfully.\n",comport->SerialName);
return 0;
}
int comport_close(ComportAttr *comport)
{
if(tcflush(comport->fd,TCIOFLUSH)) //清零用于串口通信的缓冲区
{
printf("%s,Tcflush faile:%s\n",__func__,strerror(errno));
return -1;
}
/* 将串口设置为原有属性 */
if(tcsetattr(comport->fd,TCSANOW,&(comport->OldTermios)))
{
printf("%s,Set old options failed:%s\n",__func__,strerror(errno));
return -2;
}
close(comport->fd);
free(comport);
return 0;
}
/* 初始化串口属性,设置串口用于通信 */
int comport_init(ComportAttr *comport)
{
char baudrate[32] = {0};
struct termios NewTermios;
memset(&NewTermios,0,sizeof(struct termios));
memset(&(comport->OldTermios),0,sizeof(struct termios));
if(!comport)
{
printf("Invalid parameter.\n");
return -1;
}
/* 获取串口原始属性,这部分是备份 */
if(tcgetattr(comport->fd,&(comport->OldTermios)))
{
printf("%s,Get termios to OldTermios failure:%s\n",__func__,strerror(errno));
return -2;
}
/* 获取串口原始属性,这部分用于设置新属性 */
if(tcgetattr(comport->fd,&NewTermios))
{
printf("%s,Get termios to NewTermios failure:%s\n",__func__,strerror(errno));
return -3;
}
/* 修改控制模式,保证程序不会占用串口 */
NewTermios.c_cflag |= CLOCAL;
/* For example:
*
* c_cflag: 0 0 0 0 1 0 0 0
* CLOCAL: | 0 0 0 1 0 0 0 0
* --------------------
* 0 0 0 1 1 0 0 0
*
* Finally:
*
* c_flag = 0 0 0 1 1 0 0 0;
*
* */
/* 启动接收器,能够从串口中读取输入数据 */
NewTermios.c_cflag |= CREAD;
/* CSIZE字符大小掩码,将与设置databits相关的标致位置零 */
NewTermios.c_cflag &= ~CSIZE;
/* For example:
*
* CSIZE = 0 1 1 1 0 0 0 0 ---> ~CSIZE = 1 0 0 0 1 1 1 1
*
* c_cflag: 0 0 1 0 1 1 0 0
* ~CSIZE: & 1 0 0 0 1 1 1 1
* -----------------------
* 0 0 0 0 1 1 0 0
*
* Finally:
*
* c_cflag = 0 0 0 0 1 1 0 0
*
* */
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(comport->BaudRate)
{
sprintf(baudrate,"B%d",comport->BaudRate);
cfsetispeed(&NewTermios,(int)baudrate); //设置输入输出波特率
cfsetospeed(&NewTermios,(int)baudrate);
}
else
{
cfsetispeed(&NewTermios,B115200);
cfsetospeed(&NewTermios,B115200);
}
/* 设置数据位 */
switch(comport->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; //默认数据位为8
break;
}
/* 设置校验方式 */
switch(comport->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;
/* 设置为空格 */
case 's':
case 'S':
NewTermios.c_cflag &= ~PARENB;
NewTermios.c_cflag &= ~CSTOPB;
/* 默认无校验 */
default:
NewTermios.c_cflag &= ~PARENB;
NewTermios.c_iflag &= ~INPCK;
break;
}
/* 设置停止位 */
switch(comport->StopBits)
{
case '1':
NewTermios.c_cflag &= ~CSTOPB;
break;
case '2':
NewTermios.c_cflag |= CSTOPB;
break;
default:
NewTermios.c_cflag &= ~CSTOPB; //默认使用1位作为停止位
break;
}
NewTermios.c_cc[VTIME] = 0; //最长等待时间
NewTermios.c_cc[VMIN] = 2; //最小接收字符
comport->mSend_Len = 128; //若命令长度大于mSend_Len,则每次最多发送为mSend_Len
/* 清空用于串口通信的输入输出缓存区 */
if(tcflush(comport->fd,TCIFLUSH))
{
printf("%s,Failed to clear the cache:%s\n",__func__,strerror(errno));
return -4;
}
/* 设置串口属性,除了查看波特率的函数,其余用于串口通信的函数成功均返回0 */
if(tcsetattr(comport->fd,TCSANOW,&NewTermios) != 0)
{
printf("%s,tcsetattr failure:%s\n",__func__,strerror(errno));
return -5;
}
printf("Comport Init Successfully......\n");
return 0;
}
/* 向串口发送相关指令 */
int comport_send(ComportAttr *comport,char *sbuf,int sbuf_len)
{
char *ptr,*end;
int retval;
if(!comport || !sbuf || sbuf_len <= 0)
{
printf("%s,Invalid parameter.\n",__func__);
return -1;
}
if(sbuf_len > comport->mSend_Len) //指令长度实际长度大于单次发送的最大长度,则每次发送单次发送的最大长度
{
ptr = sbuf;
end = sbuf + sbuf_len;
do
{
if(comport->mSend_Len < (end - ptr)) //剩余长度大于单次发送的最大长度
{
retval = write(comport->fd,ptr,comport->mSend_Len);
if(retval <= 0 || retval != comport->mSend_Len)
{
printf("Write to com port[%d] failed:%s\n",comport->fd,strerror(errno));
return -2;
}
ptr += comport->mSend_Len;
}
else
{
retval = write(comport->fd,ptr,(end - ptr)); //剩余长度可一次性发送
if(retval <= 0 || retval != (end - ptr))
{
printf("Write to com port[%d] failed:%s\n",comport->fd,strerror(errno));
return -3;
}
ptr += (end - ptr);
}
}while(end > ptr);
}
else
{
retval = write(comport->fd,sbuf,sbuf_len);
if(retval <= 0 || retval != sbuf_len)
{
printf("Write to com port[[%d] failed:%s\n",comport->fd,strerror(errno));
return -4;
}
}
return retval;
}
int comport_recv(ComportAttr *comport,char *rbuf,int rbuf_len,int timeout)
{
int retval;
fd_set rset;
struct timeval time_out;
if(!rbuf || rbuf_len <= 0)
{
printf("%s,Invalid parameter.\n",__func__);
return -1;
}
if(timeout) //如果传入该参数,则调用select指定读的阻塞时间
{
time_out.tv_sec = (time_t)timeout;
time_out.tv_usec = 0;
FD_ZERO(&rset);
FD_SET(comport->fd,&rset);
retval = select(comport->fd + 1,&rset,NULL,NULL,&time_out);
if(retval < 0)
{
printf("%s,Select failed:%s\n",__func__,strerror(errno));
return -2;
}
else if(0 == retval)
{
printf("Time Out.\n");
return 0;
}
}
/* 延时,避免数据还未到 */
usleep(1000);
retval = read(comport->fd,rbuf,rbuf_len);
if( retval <= 0)
{
printf("%s,Read failed:%s\n",__func__,strerror(errno));
return -3;
}
return retval;
}
具体标志位的设置,函数的使用,comport_send(),comport_recv() 的实现思路,请参考下方博客,要想完成这个程序,搞懂初始化的步骤很有必要: