在GPRS模块(GA6)和STM32F103C8T6单片机上移植MQTT协议

在GPRS模块(GA6)和STM32F103C8T6单片机上移植MQTT协议

最近几天折腾了一下,在GPRS模块(GA6)和STM32F103C8T6单片机上移植MQTT协议,网上找了一些资料,不是很全面,下面结合自己的移植过程记录一下。

下载MQTT的库

我移植MQTT的库:Paho,这个库支持非常多的平台,当然也包括了嵌入式平台:GitHub – paho.mqtt.embedded-c
这里写图片描述
将该库中的MQTTPacket文件夹下载下来,MQTTPacket文件夹下面主要有三个文件夹,如上图所示,我们使用的文件主要在src文件夹和samples文件夹中。
src文件夹中存放着MQTT核心功能的代码,而samples中存放着五个例子:ping.c,ping_nb.c,pub0sub1.c,pub0sub1_nc.c,qos0pub.c和网络驱动文件(transport.c和transport.h),如下图所示:
这里写图片描述

移植步骤

1, 准备工作

因为我板子都是淘宝上买的,给过来很多配套的资料,熟悉下怎么通过STM32F103C8T6单片机串口连接GPRS模块(GA6)来发送TCP消息,当然也熟悉下keil 5这个工具,因为我不是做单片机开发的,第一次玩这个,着实也是折腾了一下,才勉强用Keil 5完成我要的任务,这里的STM32F103C8T6单片机串口连接GPRS模块(GA6)来发送TCP消息在Keil 5中建一个工程试试,一般资料中都会有,直接用就可以,后面的移植就基于这个工程来弄。

2,移植

直接把前面我们下载下来的MQTTPacket/src目录中的所有文件,如下图所示导入到上面的工程中。
这里写图片描述
我导入之后的结果如下图所示:
这里写图片描述
这里只是截取了一个片段,后面我会把整个完成的工程代码放出。当然上面用于cmake编译的CMakeLists.txt文件,我们不需要添加进来。导入之后重新编译是没有错误的,如果前面的工程本来就有错误的话,请先修改的没错误。

这样其实就已经把MQTT库移植完成了,是不是很简单,应该说这个的支持比较好,下面我们来把库samples目录文件夹中的例子移植进去,我是基于pub0sub1.c这个例子来做的,我先把这个文件的原内容贴出来,方便对比:

/*******************************************************************************
 * Copyright (c) 2014 IBM Corp.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Eclipse Distribution License v1.0 which accompany this distribution.
 *
 * The Eclipse Public License is available at
 *    http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 *   http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *    Ian Craggs - initial API and implementation and/or initial documentation
 *    Sergio R. Caprile - clarifications and/or documentation extension
 *******************************************************************************/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "MQTTPacket.h"
#include "transport.h"

/* This is in order to get an asynchronous signal to stop the sample,
as the code loops waiting for msgs on the subscribed topic.
Your actual code will depend on your hw and approach*/
#include <signal.h>

int toStop = 0;

void cfinish(int sig)
{
    signal(SIGINT, NULL);
    toStop = 1;
}

void stop_init(void)
{
    signal(SIGINT, cfinish);
    signal(SIGTERM, cfinish);
}
/* */

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;
}

可以看到,其内容比较简单,其中关于中断的处理,STM32F103C8T6单片机应该是用不了的,我们可以直接去掉,我是用按键来替代的,然后除了下面这个四个接口:

int transport_sendPacketBuffer(unsigned char* buf, int buflen);
int transport_getdata(unsigned char* buf, int count);
int transport_open(char* host, int port);
int transport_close();

是在我们所说网络驱动文件(transport.c和transport.h)中实现,其他MQTT相关的接口,都在MQTTPacket.h文件中可以找到定义,也就是说我们上面移植的MQTT库中,下面我们剩下的工作就是完成对这上面4个接口来进行移植。
上面4个接口实现的主要功能是:
1、transport_open的作用是初始化模块连网的信息、transport_close作用是关闭链接。
2、transport_sendPacketBuffer用于发送数据、transport_getdata用于接收数据。
然后用transport.c来实现transport.h中声明的4个函数。
其实transport_open和transport_close这两个接口我在移植中也没用到,最后主要就集中在transport_sendPacketBuffer和transport_getdata这两个接口上了,为了方便,我甚至把上面transport.c和transport.h这两个文件都给删了,直接把上面两个要用的接口实现放在我们自己工程的main函数前面进行定义,具体细节我就不说,直接贴我移植之后的代码:

transport_sendPacketBuffer函数:
int transport_sendPacketBuffer(const char*server_ip_and_port,unsigned char* buf, int buflen)
{
    u8 ret;
    char end_char[2];
    char connect_server_ip_port_cmd[56];

    // 因为每次读取消息之前都要设置这个值,所以干脆放到这个函数中,以便下次读取
    // 尽量不去动MQTT中的代码
    first_index_to_read = 21;

    end_char[0] = 0x1A;//结束字符
    end_char[1] = '\0';

    ret = UART2_Send_AT_Command("AT+CIPSTATUS","CONNECT OK",3,1);//查询连接状态
    //UART1_Printf("0000 ret %d\n", ret);
    if(ret == 1)//说明服务器处于连接状态
    {
        ret = UART2_Send_AT_Command("AT+CIPSEND",">",3,1);//开发发送数据
        //UART1_Printf("1111 ret %d\n", ret);
        if(ret == 0)
        {
            return AT_CIPSEND_ERROR;
        }

        UART2_SendU8Array(buf,buflen);
        delays(2);
        ret = UART2_Send_AT_Command_End(end_char,"SEND OK",4,1);//发送结束符,等待返回ok,等待5S发一次,因为发送数据时间可能较长
        //UART1_Printf("2222 ret %d\n", ret);
        if(ret == 0)
        {
            return END_CHAR_ERROR;
        }
        //clear_uart2_recv_buf();
        return 1;
    }

    ret = UART2_Send_AT_Command("AT+CGATT=1","OK",2,1);//附着网络
    //UART1_Printf("3333 ret %d\n", ret);
    if(ret == 0)
    {
        return AT_CGATT_ERROR;
    }

    ret = UART2_Send_AT_Command("AT+CGACT=1,1","OK",2,1);//激活网络
    //UART1_Printf("0000 ret %d\n", ret);
    if(ret == 0)
    {
        return AT_CGATT1_ERROR;
    }

    memset(connect_server_ip_port_cmd,'\0',56);
    strcpy(connect_server_ip_port_cmd,"AT+CIPSTART=\"TCP\",");
    strcat(connect_server_ip_port_cmd,server_ip_and_port);

    ret = UART2_Send_AT_Command(connect_server_ip_port_cmd,"CONNECT OK",4,1);//连接服务器
    //UART1_Printf("0000 ret %d\n", ret);
    if(ret == 0)
    {
        return AT_CIPSTART_ERROR;
    }

    ret = UART2_Send_AT_Command("AT+CIPSEND",">",3,1);//开发发送数据
    //UART1_Printf("0000 ret %d\n", ret);
    if(ret == 0)
    {
        return AT_CIPSEND_ERROR;
    }

    UART2_SendU8Array(buf,buflen);
    delays(2);
    ret = UART2_Send_AT_Command_End(end_char,"SEND OK",4,1);//发送结束符,等待返回ok,等待5S发一次,因为发送数据时间可能较长
  if(ret == 0)
    {
         return END_CHAR_ERROR;
    }

    return 1;
}

主要是GPRS模块(GA6)通过AT指令来发送TCP数据,每个指令之间的延时,然后怎么确认每条指令已经完成,具体细节大家在移植的时候要结合自己的芯片进行调试。

transport_getdata函数:
int transport_getdata(unsigned char*buf, int count)
{
    memcpy(buf,&uart2_recv_buf[first_index_to_read],count);
    // 更新读取的序号,以便下次读取
    first_index_to_read += count;
    return count;
}

这个函数实现的比较奇怪,因为GPRS模块(GA6)收到服务器发回的消息之后,是通过串口发送给STM32F103C8T6单片机的,然后STM32F103C8T6单片机串口是通过串口中断把GPRS模块(GA6)发送过来的数据,存放在uart2_recv_buf这个用于接收串口数据的数组中的,然后我们解析服务器发来的数据时,MQTT库中的这个函数MQTTPacket_read有个参数是以transport_getdata函数的函数指针作为形参:

/**
 * Helper function to read packet data from some source into a buffer
 * @param buf the buffer into which the packet will be serialized
 * @param buflen the length in bytes of the supplied buffer
 * @param getfn pointer to a function which will read any number of bytes from the needed source
 * @return integer MQTT packet type, or -1 on error
 * @note  the whole message must fit into the caller's buffer
 */
int MQTTPacket_read(unsigned char* buf, int buflen, int (*getfn)(unsigned char*, int))

为了不改变MQTT库,所以按照上面的办法实现了这个接口,虽然不是很严谨—把参数count直接返回,但是在我们后面测试的时候,没发现问题,(有点啰嗦,哈哈,我主要当作自己的学习笔记吧,以后自己看到的时候,能明白自己为啥当初这么做。)

最后我们来看看移植完之后的main函数:

int main(void)
{
    u16 key_value= 0, retry_count = 5;
    unsigned short submsgid;
    unsigned char buf[MQTT_MAX_BUF_SIZE];
    unsigned char sessionPresent, connack_rc;
    const char* payload = "mypayload";
    int payloadlen = strlen(payload);
    int ret = 0,len = 0,req_qos = 0,msgid = 1,loop = 1,granted_qos,subcount;

    MQTTString topicString = MQTTString_initializer;
    MQTTPacket_connectData data = MQTTPacket_connectData_initializer;

    SysTick_Init_Config();   //系统滴答时钟初始化
    GPIO_Config();           //GPIO初始化
    Key_GPIO_Config();
    USART2_Init_Config(115200);  //串口2初始化
    Timer2_Init_Config();        //定时器2初始化

    USART1_Init_Config(115200);//UART1用作串口调试信息

    UART1_SendString("系统启动.......................\r\n");

    //等待系统上电后的稳定
    delays(STABLE_TIMES);

    data.clientID.cstring = "me";
    data.keepAliveInterval = 20;
    data.cleansession = 1;
    data.username.cstring = "testuser";
    data.password.cstring = "testpassword";

    len = MQTTSerialize_connect(buf, MQTT_MAX_BUF_SIZE, &data);
    UART1_Printf("1111 len %d\n", len);
    ret = transport_sendPacketBuffer(MQTT_SERVER_IP_AND_PORT, buf, len);
    if( ret != 1 ){
        UART1_Printf("1111 transport_sendPacketBuffer Error %d\n", ret);
        return -1;
    }
    /* wait for connack */
    if (MQTTPacket_read(buf, MQTT_MAX_BUF_SIZE, transport_getdata) != CONNACK)
    {
        UART1_Printf("MQTTPacket_read != CONNACK\n");
        return -1;
    }
    if (MQTTDeserialize_connack(&sessionPresent, &connack_rc, buf, MQTT_MAX_BUF_SIZE) != 1 || connack_rc != 0)
    {
        UART1_Printf("Unable to connect, return code %d\n", connack_rc);
        return -1;
    }
    topicString.cstring = "substopic";
    len = MQTTSerialize_subscribe(buf, MQTT_MAX_BUF_SIZE, 0, msgid, 1, &topicString, &req_qos);
    UART1_Printf("2222 len %d\n", len);
    ret = transport_sendPacketBuffer(MQTT_SERVER_IP_AND_PORT, buf, len);
    if( ret != 1 ){
        UART1_Printf("2222 transport_sendPacketBuffer Error %d\n", ret);
        return -1;
    }

    if (MQTTPacket_read(buf, MQTT_MAX_BUF_SIZE, transport_getdata) != SUBACK)   /* wait for suback */
    {
        UART1_Printf("MQTTPacket_read != SUBACK\n");
        return -1;
    }

    MQTTDeserialize_suback(&submsgid, 1, &subcount, &granted_qos, buf, MQTT_MAX_BUF_SIZE);
    if (granted_qos != 0)
    {
        UART1_Printf("granted qos != 0, %d\n", granted_qos);
        return -1;
    }

    /* loop getting msgs on subscribed topic */
    topicString.cstring = "pubtopic";

    while( loop ){ 

        key_value = Key_Down_Scan();
        // add by james_xie 20171221 ,for debug
        UART1_Printf("key_value %d\r\n",key_value);
        // 按键KEY1即PA5被按下则跳出循环
        if( key_value == 0x0001 << 5 ){
            UART1_Printf("Key1 is pressed down , exit \r\n");
            loop = 0;
        }
        /* transport_getdata() has a built-in 1 second timeout,
        your mileage will vary */
        if (MQTTPacket_read(buf, MQTT_MAX_BUF_SIZE, transport_getdata) == PUBLISH)
        {
            int qos,payloadlen_in;
            unsigned char dup,retained;
            unsigned short msgid;
            unsigned char* payload_in;
            MQTTString receivedTopic;

            MQTTDeserialize_publish(&dup, &qos, &retained, &msgid, &receivedTopic,
                    &payload_in, &payloadlen_in, buf, MQTT_MAX_BUF_SIZE);
            UART1_Printf("message arrived %.*s\n", payloadlen_in, payload_in);
        }

        UART1_Printf("publishing reading\n");
        len = MQTTSerialize_publish(buf, MQTT_MAX_BUF_SIZE, 0, 0, 0, 0, topicString, (unsigned char*)payload, payloadlen);
        UART1_Printf("33333 len %d\n", len);
        // 重复发送5次,如果5次都不成功,则退出程序
        while( retry_count > 0 ){
            ret = transport_sendPacketBuffer(MQTT_SERVER_IP_AND_PORT, buf, len);
            // 如果发送成功则跳出循环
            if( ret == 1 ){
                break;
            }
            delays(1);
        }
        if( !retry_count && ret != 1 ){
             UART1_Printf("3333 transport_sendPacketBuffer Error %d\n", ret);
             break;
        }
        // 每次发送之后,等两秒
        delays(2);
    }

    UART1_Printf("disconnecting\n");
    len = MQTTSerialize_disconnect(buf, MQTT_MAX_BUF_SIZE);
    ret = transport_sendPacketBuffer(MQTT_SERVER_IP_AND_PORT, buf, len);
    if( ret != 1 ){
        UART1_Printf("4444 transport_sendPacketBuffer Error %d\n", ret);
        return -1;
    }
}

其实整个流程完全没动,直接把例程中的main函数内容copy过来,然后进行了一下必要的修改,主要是模块的初始化相关的东西。
到此整个移植过程就已经完成,相对比较简单。

3,测试

因为GPRS模块(GA6)的IP地址是公网IP,所以我们测试得找个公网服务器来进行测试,至于Mosquitto环境搭建,不知道的可以看我前面的一篇文章—-MQTT学习——Centos7上安装Mosquitto和使用
下面可以看看服务器的打印:
这里写图片描述
这里写图片描述
这是服务器那边的打印,可以看到已经工作正常,后续有时间深入研究下MQTT协议的过程。

源码我上传到这里—http://download.csdn.net/download/xj178926426/10172099
大家有兴趣可以下载下来看看,模块间串口怎么连接在注释中写的比较详细,应该下下来就直接可用。

发布了114 篇原创文章 · 获赞 52 · 访问量 28万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章