目录
前言
在用C语言实现bc28上报数据之前,需要了解bc28NB模组上报阿里云的流程。具体可参考之前的博客:BC28上报数据到电信云平台
为了具有良好的移植性,我将整个项目封装了4层:
1.串口层:这一层封装了端口的打开、初始化、关闭、发送数据和接收数据函数。
2.指令操作层:这一层封装了两个函数,一个是AT指令的发送与回调,另一个是字符串切割获取有用信息。
3.BC28指令集层:这一层封装了移远BC28相关的有用的AT指令。
4.NB应用层:这一层封装了nb模组附着网络、查询硬件信息、设置IOT平台ip和端口、上报数据函数。
通过这种封装,整个项目一层调用一层,使用者最后只需要调用最外层的nbiot层函数即可。在开始编写程序时,首先需要学习一些基本的串口通信属性设置:
一、串口层
串口层的主要功能就是实现串口的打开、关闭、初始化、发送和接收信息操作。串口的这些操作都是通过对串口文件描述符的open、close、read、write实现的,这一部分与socket操作类似。而不同的也是最麻烦的就是串口属性的初始化操作,需要学习termios结构体、以及各种串口属性设置函数大家可以参考其他博主的介绍:termios结构体及相关函数介绍。
为了方便对串口属性的操作,于是我自己定义了一个结构体,用来保存和设置串口属性。相关操作会在下面的串口初始化中阐明。
typedef struct comport_s
{
int fd; //串口文件描述符
int baudrate; //波特率
int databits; //数据位
int mSend_Len;//单次最大发送长度
char parity; //奇偶校验位
int stopbits; //停止位
char tty_name[SERLALNAME];//串口名称:"ttyUSB0"
char modelname[SERLALNAME];//型号:“BC28”
struct termios oldtermios; //串口原始属性
}comport_t;
1.comport_open()
这里有一个重要的点就是串口读取(read)数据的阻塞与非阻塞模式,有以下两种方式来控制:
1、open打开串口时,默认是阻塞模式,如果想要设置非阻塞模式那么参数就要加上O_NDELAY/O_NONBLOCK,二者的区别是:在read时,如果读不到数据,O_NDELAY会返回0,由于正常读取到文件末尾时,也会返回0,这样就无法区分是否是遗产隔离,所以就引入了O_NONBLOCK,在读不到数据时,返回-1,并且设置errno为EAGAIN,而读到结尾处,正常返回0。
2、打开串口后,用fcntl()函数进行设置。
fcntl( fd, F_SETFL, O_NONBLOCK ); //设为非阻塞
fcntl( fd, F_SETFL, 0 ); //设为阻塞,阻塞情况下,可以采用 c_cc[VTIME] 与 c_cc[VMIN] 进行限定。
本次串口编程采用阻塞模式,利用I/O多路复用select实现程序的软中断。
int comport_open(comport_t *comport,char *devname)
{
int rv=-1; //return value返回值
/* 参数合法性检测 */
if( !comport || !devname)
{
printf("%s,Invalid parameter\n",__func__);
return -1;
}
/*
* O_NOCTTY:不想成为“控制终端”控制的程序,不说明这个标志的话,任何输入都会影响你的程序。
* O_NDELAY:这个程序不关心DCD信号线状态,即其他端口是否运行,不说明这个标志的话,该程序就会在DCD信号线为低电平时停止。
* O_NDELAY/O_NONBLOCK:以非阻塞模式打开
*/
comport->fd = open(devname,O_RDWR | O_NOCTTY);
if( comport->fd < 0 )
{
printf("%s,Open %s failed:%s\n",__func__,comport->tty_name,strerror(errno));
return -1;
}
/* 检查串口是否处于阻塞态 */
if( (rv = fcntl(comport->fd,F_SETFL,0)) < 0 )
{
printf("%s,Fcntl check faile.\n",__func__);
return -2;
}
/* 是否为终端设备 */
if( 0 == isatty(comport->fd) )
{
printf("%s:[%d] is not a Terminal equipment.\n",comport->tty_name,comport->fd);
return -3;
}
printf("Open %s successfully.\n",comport->tty_name);
return 0;
}
2.comport_conf()
int comport_conf(comport_t *comport)
{
char baudrate[BAUDRATE]={0};
struct termios newtermios;
if( !comport )
{
printf("invalid parameter.\n");
return -1;
}
memset(&newtermios,0,sizeof(struct termios));
memset(&(comport->oldtermios),0,sizeof(struct termios));
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;
/* 启动接收器,能够从串口中读取输入数据 */
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( comport->baudrate )
{
sprintf(baudrate,"B%d",comport->baudrate);
/* 设置输入输出波特率 */
cfsetispeed(&newtermios,(speed_t)baudrate);
cfsetospeed(&newtermios,(speed_t)baudrate);
}
else
{
cfsetispeed(&newtermios,B9600);
cfsetospeed(&newtermios,B9600);
}
/* 设置数据位 */
switch( comport->databits )
{
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(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;
break;
/* 设置为空格 */
case 's':
case 'S':
newtermios.c_cflag &= ~PARENB;
newtermios.c_cflag &= ~CSTOPB;
break;
/* 默认无校验 */
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;
break;
}
/* 设置超时 */
newtermios.c_cc[VTIME] = 0; //最长等待时间
newtermios.c_cc[VMIN] = 0; //最小接收字符
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;
}
if( tcsetattr(comport->fd,TCSANOW,&newtermios) )
{
printf("%s,tcsetcomport failure:%s\n",__func__,strerror(errno));
return -5;
}
printf("Comport Conf Successfully......\n");
return 0;
}
3.comport_close()
int comport_close(comport_t *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);
return 0;
}
4.int comport_send()
int comport_send(comport_t *comport,char *sbuf,int sbuf_len)
{
char *ptr = NULL;
char *end = NULL;
int rv;
if( !comport || !sbuf || sbuf_len <= 0 )
{
printf("%s,Invalid parameter.\n",__func__);
return -1;
}
if( comport->fd < 0 )
{
printf("Serail not connected.\n");
return -2;
}
if( comport->mSend_Len < sbuf_len )
{
ptr = sbuf;
end = sbuf + sbuf_len;
do
{
if( comport->mSend_Len < (end - ptr) )
{
rv = write(comport->fd,ptr,comport->mSend_Len);
if( rv <= 0 || rv != comport->mSend_Len )
{
printf("Write to com port[%d] failed:%s\n",comport->fd,strerror(errno));
return -3;
}
ptr += comport->mSend_Len;
}
else
{
rv = write(comport->fd,ptr,(end - ptr));
if( rv <= 0 || rv != (end - ptr) )
{
printf("Write to com port[%d] failed:%s\n",comport->fd,strerror(errno));
return -4;
}
ptr += (end - ptr);
}
}while(end > ptr);
}
else
{
printf("write:%s\b",sbuf);
rv = write(comport->fd,sbuf,sbuf_len);
if( rv <= 0 || rv != sbuf_len )
{
printf("Write to com port[[%d] failed:%s\n",comport->fd,strerror(errno));
return -5;
}
}
printf("comport_secd Successfully\n");
return rv;
}
二、指令操作层
这一层主要实现了AT指令的发送和接收、对接收的字符串关键字进行切割。
1.send_atcmd():
在实现AT指令发送的函数前,首先需要了两个重要的串口收发数据的特性:
1、发送给串口的指令要以"\r\n"结尾表示发送完毕。
2、我们使用read接收串口发送的信息时,实际是从串口芯片缓存中读取数据,因此一次能读取多少数据取决于缓存大小,通常在信息稍大的时候我们需要多次从缓存中读取消息。
所以在读取串口消息的时候采用I/O多路复用,将read放在死循环中,通过select的超时设置(串口收发数据会有延迟大概200ms-2000ms)来实现程序中断。
int send_atcmd(comport_t *comport,char *atcmd,char *buf,int buf_size,int timeout)
{
int rv = -1;
fd_set rfds;
struct timeval time_out;
struct timeval *time;
char *ptr;
int bytes = 0;
if(!comport || !atcmd || !buf || buf_size <= 0)
{
printf("%s,invalid parameter...\n",__func__);
return -1;
}
rv = comport_send(comport,atcmd,strlen(atcmd));
if( rv < 0 )
{
printf("comport_send is failed:%s...\n",strerror(errno));
return -2;
}
memset(buf,0,buf_size);
FD_ZERO(&rfds);
FD_SET(comport->fd,&rfds);
time_out.tv_sec = 0;
time_out.tv_usec = timeout * 1000;
ptr = buf;
while(1)
{
rv = select(comport->fd + 1, &rfds, NULL, NULL, &time_out);
if( rv < 0 )
{
printf("select open failed:%s.\n ",strerror(errno));
break;
}
else if( 0 == rv )
{
rv = -4;
break;
}
else
{
rv = read( comport->fd, ptr, buf_size - bytes );
if( rv < 0 )
{
printf("%s,read is failure.\n",__func__);
return -5;
}
ptr += rv;
bytes += rv;
if( strstr(buf,"OK\r\n") )
{
rv = 0;
break;
}
if( strstr(buf,"ERROR\r\n") )
{
rv = -6;
}
}
}
return rv;
}
2.at_fetch():
这个函数的主要功能就是用给出的首尾字符来对字符串进行切割,保存有用信息。
int at_fetch(char *atreply, char *start_key, char *end_key, char *rbuf, int size)
{
char *rbuf_start;
char *rbuf_end;
int rv =-1;
if( !atreply || !start_key || size < 0 )
{
printf("%s,invalid parameter...\n",__func__);
return -1;
}
memset(rbuf,0,size);
rbuf_start = strstr(atreply,start_key);
if( rbuf_start == NULL )
{
printf("strstr start_key :%s failure\n", start_key);
return -2;
}
rbuf_start += strlen(start_key);
if( end_key != NULL )
{
rbuf_end = strstr(rbuf_start,end_key);
if( rbuf_end == NULL )
{
printf("strstr end_key %s failure\n", rbuf_end);
return -3;
}
}
if( rbuf != NULL && ((rbuf_end-rbuf_start) <= size) )
{
strncpy(rbuf, rbuf_start, rbuf_end-rbuf_start);
}
return 0;
}
三、BC28指令集层
这一层主要封装了各种移远BC28的相关AT指令。自己定义了一个nbiot_hwinfo_t的结构体类型,用来保存硬件信息。
typedef struct nbiot_hwinfo_s {
char manufacturer[MAX_NFRMTN_T];//制造商
char modules_name[MAX_NFRMTN_T];//模块名
char num[MAX_NFRMTN_T];//产品序列号
char imei[MAX_NFRMTN_T];
char cimi[MAX_NFRMTN_T];
char version[MAX_NFRMTN_T];
}nbiot_hwinfo_t;
int atcmd_at(comport_t *comport); //查询是否连接成功
int atcmd_csq(comport_t *comport); //查询信号强度
int atcmd_cereg(comport_t *comport); //查询网络注册状态
int atcmd_cgatt(comport_t *comport); //查询网络是否被激活(附着上基站
int atcmd_cgatt1(comport_t *comport); //触发网络连接
int atcmd_cgpaddr(comport_t *comport); //查询模块的IP地址
int atcmd_ncdp(comport_t *comport , char *ip , char *port); //设置IOT平台的ip和端口
int atcmd_cgsn(comport_t *comport , char *imei , int size); //查询设备唯一标识码
int atcmd_cimi(comport_t *comport , char *cimi , int size); //查询国际移动用户标识码
int atcmd_cgmm(comport_t *comport , char *nmae , int size); //请求厂商模型标识
int atcmd_cgmr(comport_t *comport , char *version , int size); //请求厂商软件版本
int atcmd_ati(comport_t *comport , nbiot_hwinfo_t *ctx); //显示产品ID
int atcmd_qlwuladtaex(comport_t *comport, int temp); //上报数据
int atcmd_set_echo(comport_t *comport,int flag); //设置回显
我以指令AT为例,来展示AT的函数:
向串口发送“AT\r\n”后,串口返回的消息保存到wbuf中,使用strstr()函数对“OK”进行字符串匹配。
int atcmd_at(comport_t *comport)
{
char wbuf[INFORMATION];
int rv;
if( !comport )
{
printf("open atcmd_at:“at”failure...\n");
return -1;
}
memset(wbuf , 0 , sizeof(wbuf));
memset(rbuf , 0 , sizeof(rbuf));
rv = send_atcmd(comport,"AT\r\n",wbuf,sizeof(wbuf),TIME_OUT);
if( rv < 0 )
{
printf("send_atcmd_AT failure\n");
return -2;
}
if( !strstr(wbuf,"OK") )
{
printf("NBiot is not connection...\n");
return -3;
}
printf("AT:%s\n",wbuf);
return 0;
}
四、NB应用层
1.find_nbiot_dev():
这个函数的功能就是查询NB模组对应的串口名称,原理就是使用函数opendir()打开“/dev”文件夹,readdir()遍历所有文件名,找到文件名开头为“ttyUSB”的文件,挨个打开发送“AT+CGMM”查看对应的型号进行字符串匹配。
int find_nbiot_dev( comport_t *comport)//查找BC28对应的串口
{
DIR *pDir = NULL;
struct dirent *pEnt = NULL;
int i = 0;
int rv = 0;
char buf[BUF_MAX];
char devname[BUF_MAX];
memset(buf, 0, sizeof(buf));
memset(devname, 0, sizeof(devname));
pDir = opendir(PATH);
if( NULL == pDir )
{
printf("open dir failure");
return -1;
}
while( pEnt = readdir(pDir) )
{
if( strstr(pEnt->d_name,"ttyUSB") )
{
snprintf(devname, sizeof(devname), "%s/%s", PATH, pEnt->d_name);
if( comport_open( comport, devname ) != 0 )
{
continue;
}
printf("open is %s\n",devname);
if( comport_conf(comport) != 0 )
{
printf("config failure\n");
continue;
}
rv = send_atcmd(comport, AT_MODEL, buf, sizeof(buf), TIME_OUT);
if( rv < 0 )
{
comport_close(comport);
continue;
}
else
{
if( strstr(buf,comport->modelname) )
{
strncpy(comport->tty_name,devname,sizeof(comport->tty_name));
closedir(pDir);
return 0;
}
}
comport_close(comport);
}
}
closedir(pDir);
return -2;
}
2.check_nbiot_attach():
这个函数是用来检查NB模块的网络附着情况。
int check_nbiot_attach(comport_t *comport, nbiot_hwinfo_t *nbiot)
{
int rv;
rv = atcmd_at(comport);
if( rv < 0 )
{
printf("nbiot_at is failure rv :%d\n",rv);
return -1;
}
rv = nbiot_module(comport, nbiot);
if( rv < 0 )
{
printf("nbiot_module is failed..rv :%d\n",rv);
return -2;
}
rv = atcmd_csq(comport);
if( rv < 0 )
{
printf("nbiot_csq is failure rv :%d\n",rv);
return -3;
}
rv = atcmd_cgatt1(comport);
if( rv < 0 )
{
printf("nbiot_cgatt1? is failure rv :%d\n" , rv);
return -4;
}
rv = atcmd_cereg(comport);
if( rv < 0 )
{
printf("nbiot_cereg is failure rv :%d\n" , rv);
return -5;
}
rv = atcmd_cgatt(comport);
if( rv < 0 )
{
printf(" nbiot_cgatt is failure rv :%d\n", rv);
return -6;
}
rv = atcmd_cgpaddr(comport);
if( rv < 0 )
{
printf(" nbiot_cgpaddr id failure rv :%d\n");
return -7;
}
return 0;
}
3.set_nbiot_register():
/* 设置注册信息(ip,端口) */
int set_nbiot_register( comport_t *comport , char *ip , char *port)
{
int rv;
rv = atcmd_ncdp(comport, ip, port);
if( rv < 0 )
{
printf("atcmd ncdp is failure ...\n");
return -1;
}
return 0;
}
4.nbiot_report_cloud():
/* 数据上报 */
int nbiot_report_cloud(comport_t *comport, int temp)
{
int rv;
rv = atcmd_qlwuladtaex(comport , temp);
if( rv < 0 )
{
printf("atcmd_qlwuladteax is failure rv :%d...\n",rv);
return -1;
}
return 0;
}
总结
main程序流程图: