1. 介绍
随着物联网的全面普及,作为终端的单片机也需要联网,本文讲述的是一种低成本的物联网方案,硬件使用GPRS模块和STM32单片机,网络基于MQTT报文协议,相比于WIFI局域网,GPRS(最新LTE)具有更大的适用性。同时MQTT协议具有使用方便,资源消耗少和成本低的优点,非常适合物联网设备使用。
硬件:STM32F103RBT6 SIM800C
软件:stm32_hal Keil MDK V5 pahoMQTT
pahoMQTT 选择Embedded C版本,主要考虑单片机平台的资源有限,嵌入式版本不使用动态内存分配,基本与平台无关,使用起来相对比较困难。
选择Embedded C/C++版本,github下载地址
2. 拷贝需要移植的文件
pahoMQTT实现的文件,在目录 paho.mqtt.embedded-c-master\MQTTPacket\src 下(10个C文件及头文件)
接口和主函数调文件 在目录 paho.mqtt.embedded-c-master\MQTTPacket\samples下(只需要2个C文件和对应头文件)
移植后的工程目录如下所示
3.修改接口和主测试文件
以上文件都找齐后,移植只需要修改transport.c下的几个接口函数,paho例程如下:
/*原文件使用的Socket接口作为例子,包含
transport_sendPacketBuffer 透传一串字符
transport_getdata 接收一串字符
transport_open 打开和连接MQTT
transport_close 关闭连接 (可省略为空)
移植时,只需要重写以上上个函数
*/
static int mysock = INVALID_SOCKET;
int transport_sendPacketBuffer(int sock, unsigned char* buf, int buflen)
{
int rc = 0;
rc = write(sock, buf, buflen);
return rc;
}
int transport_getdata(unsigned char* buf, int count)
{
int rc = recv(mysock, buf, count, 0);
//printf("received %d bytes count %d\n", rc, (int)count);
return rc;
}
/*不懂,移植时同上*/
int transport_getdatanb(void *sck, unsigned char* buf, int count)
{
int sock = *((int *)sck); /* sck: pointer to whatever the system may use to identify the transport */
/* this call will return after the timeout set on initialization if no bytes;
in your system you will use whatever you use to get whichever outstanding
bytes your socket equivalent has ready to be extracted right now, if any,
or return immediately */
int rc = recv(sock, buf, count, 0);
if (rc == -1) {
/* check error conditions from your system here, and return -1 */
return 0;
}
return rc;
}
/**
return >=0 for a socket descriptor, <0 for an error code
@todo Basically moved from the sample without changes, should accomodate same usage for 'sock' for clarity,
removing indirections
*/
int transport_open(char* addr, int port)
{
int* sock = &mysock;
int type = SOCK_STREAM;
struct sockaddr_in address;
#if defined(AF_INET6)
struct sockaddr_in6 address6;
#endif
int rc = -1;
#if defined(WIN32)
short family;
#else
sa_family_t family = AF_INET;
#endif
struct addrinfo *result = NULL;
struct addrinfo hints = {0, AF_UNSPEC, SOCK_STREAM, IPPROTO_TCP, 0, NULL, NULL, NULL};
static struct timeval tv;
*sock = -1;
if (addr[0] == '[')
++addr;
if ((rc = getaddrinfo(addr, NULL, &hints, &result)) == 0)
{
struct addrinfo* res = result;
/* prefer ip4 addresses */
while (res)
{
if (res->ai_family == AF_INET)
{
result = res;
break;
}
res = res->ai_next;
}
#if defined(AF_INET6)
if (result->ai_family == AF_INET6)
{
address6.sin6_port = htons(port);
address6.sin6_family = family = AF_INET6;
address6.sin6_addr = ((struct sockaddr_in6*)(result->ai_addr))->sin6_addr;
}
else
#endif
if (result->ai_family == AF_INET)
{
address.sin_port = htons(port);
address.sin_family = family = AF_INET;
address.sin_addr = ((struct sockaddr_in*)(result->ai_addr))->sin_addr;
}
else
rc = -1;
freeaddrinfo(result);
}
if (rc == 0)
{
*sock = socket(family, type, 0);
if (*sock != -1)
{
#if defined(NOSIGPIPE)
int opt = 1;
if (setsockopt(*sock, SOL_SOCKET, SO_NOSIGPIPE, (void*)&opt, sizeof(opt)) != 0)
Log(TRACE_MIN, -1, "Could not set SO_NOSIGPIPE for socket %d", *sock);
#endif
if (family == AF_INET)
rc = connect(*sock, (struct sockaddr*)&address, sizeof(address));
#if defined(AF_INET6)
else
rc = connect(*sock, (struct sockaddr*)&address6, sizeof(address6));
#endif
}
}
if (mysock == INVALID_SOCKET)
return rc;
tv.tv_sec = 1; /* 1 second Timeout */
tv.tv_usec = 0;
setsockopt(mysock, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv,sizeof(struct timeval));
return mysock;
}
int transport_close(int sock)
{
int rc;
rc = shutdown(sock, SHUT_WR);
rc = recv(sock, NULL, (size_t)0, 0);
rc = close(sock);
return rc;
}
网络连接使用的是SIM800C模块,使用AT指令实现网络数据的透传功能
一般启动步骤包含:上电->注册网络-> 附着GPRS->设置单链接->设置透传->激活网络->连接MQTT服务器 ,SIM800C初始化函数如下
........................一般函数略过........................
/*-------------------------------------------------*/
/*函数名:800C连接物联网云服务器 */
/*参 数:data:数据 */
/*返回值:无 */
/*-------------------------------------------------*/
char SIM800C_Connect_IoTServer(char *addr,int port)
{
char i; //定义一个变量,用于for循环
/*********SIM800C初始化**********/
SIM800C_GPIO_Init(); //控制800C的IO初始化
if(SIM800C_Power()) //控制800C开机或重启,如果返回1,说明开机重启失败,准备重启
{
return 1; //返回1
}
/*********SIM800C注册网络**********/
u1_printf("请等待注册上网络... ...\r\n"); //串口输出信息
if(SIM800C_CREG(30)) //等待注册上网络,超时单位1s,超时时间30s
{
u1_printf("注册网络超时,准备重启\r\n"); //串口输出信息
return 2; //返回2
}
else
{
u1_printf("注册上网络\r\n"); //串口输出信息
}
/*********SIM800C查询信号**********/
if(SIM800C_CSQ(60)) //查询信号强度,超时单位100ms,超时时间6s
{
u1_printf("查询信号强度超时,准备重启\r\n"); //串口输出信息
return 3; //返回3
}
/*********SIM800C附着GPRS**********/
u1_printf("请等待附着上GPRS... ...\r\n"); //串口输出信息
if(SIM800C_CGATT(30)){ //查询附着GPRS,超时单位1s,超时时间30s
u1_printf("查询附着GPRS超时,准备重启\r\n"); //串口输出信息
return 4; //返回4
}else u1_printf("附着上GPRS\r\n"); //串口输出信息
/*********SIM800C设置单链接**********/
u1_printf("请等待设置单链接... ...\r\n"); //串口输出信息
if(SIM800C_SendCmd("AT+CIPMUX=0",60)){ //设置单链接模式,超时单位100ms,超时时间6s
u1_printf("设置单链接失败,准备重启\r\n"); //串口输出信息
return 5; //返回5
}else u1_printf("设置单链接模式成功\r\n"); //串口输出信息
/*********SIM800C设置透传**********/
u1_printf("请等待设置透传... ...\r\n"); //串口输出信息
if(SIM800C_SendCmd("AT+CIPMODE=1",60)){ //设置透传模式,超时单位100ms,超时时间6s
u1_printf("设置透传失败,准备重启\r\n"); //串口输出信息
return 6; //返回6
}else u1_printf("设置透传成功\r\n"); //串口输出信息
/*********SIM800C激活网络**********/
if(SIM800C_ActivateNetwork()){ //准备激活网络
u1_printf("激活网络失败,准备重启\r\n"); //串口输出信息
return 7; //返回7
}
Delay_Ms(500); //适当延时
for(i=0;i<3;i++) //重试3次
{
u1_printf("请等待连接上服务器... ...\r\n"); //串口输出信息
if(SIM800C_TCPConnect(addr,port,10)){ //同服务器建立TCP连接,超时单位1s,超时时间10s
u1_printf("连接失败,准备再次连接\r\n"); //串口输出信息
Delay_Ms(500); //适当延时
if(SIM800C_TCPClose(10)){ //准备再次连接前要先发送关闭指令,超时单位1s,超时时间10s
u1_printf("连接异常,准备重启\r\n"); //串口输出信息
Delay_Ms(500); //适当延时
return 8; //返回7
}
}else return 0; //正确,返回0
}
u1_printf("连接不上服务器,准备重启\r\n"); //串口输出信息
return 9;
}
修改后的transport.c的程序如下
#include "transport.h"
#include "sim800c.h"
#include "usart.h"
#include "Array.h"
/**********传输发送缓冲数据包*************/
int transport_sendPacketBuffer(unsigned char* buf, int buflen)
{
int rc = 0;
HAL_StatusTypeDef sta;
sta = HAL_UART_Transmit(&huart2, buf, buflen, 1000);
if(sta == 0)
rc = buflen;
return rc;
}
/*计算两次的间隔时间,单位 ms*/
uint32_t Diff_Tim(uint32_t pastim)
{
uint32_t ntim;
ntim = HAL_GetTick();
if(ntim > pastim)
return (ntim - pastim);
else
return (0xFFFFFFFF - pastim) + ntim;
}
/**********接收数据*************/
int transport_getdata(unsigned char* buf, int count)
{
int rc = 0;
uint32_t tim = HAL_GetTick(); //获得当前时间
while(rc != count) /*添加 1秒超时判断 如果超过1秒,跳出*/
{
rc = getQueueBuff(&u2_rx,(int8_t *)buf,count);
if(Diff_Tim(tim) > 500)
{
break;
}
osDelay(5);
}
return rc;
}
int transport_getdatanb(unsigned char* buf, int count)
{
int rc = 0;
rc = getQueueBuff(&u2_rx,(int8_t *)buf,count);
return rc;
}
/**********连接服务器*************/
int transport_open(char* addr, int port)
{
return SIM800C_Connect_IoTServer(addr,port);
}
/**********断开服务器*************/
int transport_close(void)
{
SIM800C_TCPClose(1);
printf("exit。。。\r\n");
}
/**********清空循环队列*************/
void RecClear(void)
{
initQueue(&u2_rx);
}
4.主程序测试(pahoMQTT例程)
int main(int argc, char *argv[])
{
MQTTPacket_connectData data = MQTTPacket_connectData_initializer;
int rc = 0;
int mysock = 0;
unsigned char buf[200];
int buflen = sizeof(buf);
int msgid = 1;
MQTTString topicString = MQTTString_initializer;
int req_qos = 0;
char* payload = "mypayload";
int payloadlen = strlen(payload);
int len = 0;
char *host = "m2m.eclipse.org";
int port = 1883;
stop_init();
if (argc > 1)
host = argv[1];
if (argc > 2)
port = atoi(argv[2]);
mysock = transport_open(host, port);
if(mysock < 0)
return mysock;
printf("Sending to hostname %s port %d\n", host, port);
data.clientID.cstring = "me";
data.keepAliveInterval = 20;
data.cleansession = 1;
data.username.cstring = "testuser";
data.password.cstring = "testpassword";
len = MQTTSerialize_connect(buf, buflen, &data);
rc = transport_sendPacketBuffer(mysock, buf, len);
/* wait for connack */
if (MQTTPacket_read(buf, buflen, transport_getdata) == CONNACK)
{
unsigned char sessionPresent, connack_rc;
if (MQTTDeserialize_connack(&sessionPresent, &connack_rc, buf, buflen) != 1 || connack_rc != 0)
{
printf("Unable to connect, return code %d\n", connack_rc);
goto exit;
}
}
else
goto exit;
/* subscribe */
topicString.cstring = "substopic";
len = MQTTSerialize_subscribe(buf, buflen, 0, msgid, 1, &topicString, &req_qos);
rc = transport_sendPacketBuffer(mysock, buf, len);
if (MQTTPacket_read(buf, buflen, transport_getdata) == SUBACK) /* wait for suback */
{
unsigned short submsgid;
int subcount;
int granted_qos;
rc = MQTTDeserialize_suback(&submsgid, 1, &subcount, &granted_qos, buf, buflen);
if (granted_qos != 0)
{
printf("granted qos != 0, %d\n", granted_qos);
goto exit;
}
}
else
goto exit;
/* loop getting msgs on subscribed topic */
topicString.cstring = "pubtopic";
while (!toStop)
{
/* transport_getdata() has a built-in 1 second timeout,
your mileage will vary */
if (MQTTPacket_read(buf, buflen, transport_getdata) == PUBLISH)
{
unsigned char dup;
int qos;
unsigned char retained;
unsigned short msgid;
int payloadlen_in;
unsigned char* payload_in;
int rc;
MQTTString receivedTopic;
/*接收到消息 */
rc = MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic,
&payload_in, &payloadlen_in, buf, buflen);
printf("message arrived %.*s\n", payloadlen_in, payload_in);
}
printf("publishing reading\n");
/*发布消息*/
len = MQTTSerialize_publish(buf, buflen, 0, 0, 0, 0, topicString, (unsigned char*)payload, payloadlen);
rc = transport_sendPacketBuffer(mysock, buf, len);
}
printf("disconnecting\n");
len = MQTTSerialize_disconnect(buf, buflen);
rc = transport_sendPacketBuffer(mysock, buf, len);
exit:
transport_close(mysock);
return 0;
}
5. 结束
pahoMQTT embedded 移植相对简单,MQTT embedded提供的发布和订阅功能函数一般由两部分组成,先序列化在发送一串数据,使用起来困难一点点,根据MQTT的通信协议和机制,还是能够容易理解。主要难点在于SIM800C的驱动稳定可靠,需要完整的驱动程序朋友,欢迎留言。