应用场景:设备的硬件由一个MCU加上一个通信模组构成,设备的应用逻辑运行在MCU上,通信模组支持MQTT功能并提供AT指令给MCU使用, MCU控制模组连接云端服务以及收发数据。

本示例中:示例app + SDK + 模组对接代码一起的RAM消耗为6KB。

对于这样的场景,设备厂商需要将Link SDK集成并运行在MCU上, 让Link SDK通过通信模组连接到阿里云物联网平台。

文档目标

下面的文档关注于讲解用户如何把SDK移植到MCU, 并与通信模组协作来与阿里云物联网平台通信。为了简化移植过程, 下面的文档在MCU上以开发一个基础版产品作为案例进行讲解,如果用户需要在MCU上使用SDK的其它功能,可以在MCU上将基础版的example正确运行之后,再重新配置SDK,选中其它功能再进行产品功能开发。

设备端开发过程

设备端的开发过程如下所示。

SDK配置与代码抽取

SDK中有各种功能模块,用户需要决定。

  • SDK获取
  • 需要使用哪些功能(SDK配置)

    SDK提供了配置工具用于配置需要使能哪些功能,每个功能的配置选项名称类似FEATURE_MQTT_XXX,下面的章节中会讲解具体有哪些功能可供配置。

  • SDK如何与外部模组进行数据交互

    上图中的三根红色虚线代表SDK可以与MQTT模组进行数据交互的三种方式。

  • MQTT Wrapper

    MQTT Wrapper提供了接口函数定义用于与MQTT Client交互,当MCU外接MQTT模组时可以通过实现相关接口函数来驱动MQTT模组中的MQTT Client与阿里云物联网平台上的MQTT Broker/Server建连/收发MQTT消息。开发者可以实现相关的wrapper函数来代码来驱动MQTT模组进行MQTT的连接,无需使能ATM/AT MQTT/AT Parser等功能。

  • AT MQTT

    当MQTT模组发送MQTT消息给MCU时,如果模组发送给MCU的数据的速度超过了MCU上处理MQTT消息的速度,可能导致丢包,因此SDK中实现了一个AT MQTT模块用于对收到的MQTT消息进行缓存。开发者如果使能本模块,本模块将提供MQTT Wrapper函数的实现,开发者需要实现的函数将是AT MQTT HAL中定义的函数,在这些函数中驱动MQTT模组。

  • AT Parser

    MCU与模组之间通常使用UART进行连接,因此开发者需要开发代码对UART进行初始化,通过UART接收来自模组的数据。由于UART是一个字符一个字符的接收数据,因此开发者还需要对收到的数据组装并判断AT指令是否承载MQTT数据,如果是才能将MQTT数据发送给AT MQTT模块。SDK中提供了AT Parser模块用于完成这些功能,如果开发者尚未实现在UART上的数据收发/解析等功能, 可以使能AT Parser功能来减少开发工作量。

    当开发者使能AT Parser后,AT Parser将会提供AT MQTT HAL的实现,因此开发者需要实现的函数是AT Parser HAL中定义的函数。

配置SDK

SDK包含的功能较多,为了节约对MCU RAM/Flash资源的消耗,用户需要根据自己的产品功能定义需要SDK中的哪些功能。

运行配置命令

  • Linux系统

进入SDK的根目录下,运行命令。

make menuconfig            
  • Windows系统

运行SDK根目录下的config.bat。

config.bat          

使能需要的SDK功能

运行上面的命令之后, 将会跳出下面的功能配置界面。按下空格键可以选中或者失效某个功能,使用小键盘的上下键来在不同功能之间切换,如果想知道每个选项的具体含义,先用方向键将高亮光条移到那个选项上,再按键盘上的h按键,将出现帮助文本,说明选项是什么含义,打开了和关闭了意味着什么。

如果编译环境有自带标准头文件<stdint.h>,请使能选项。

  • PLATFORM_HAS_STDINT

如果目标系统上运行有嵌入式操作系统,请使能选项。

  • PLATFORM_HAS_OS

请务必使能:

  • FEATURE_MQTT_COMM_ENABLED,用于让SDK提供MQTT API供应用程序调用,并关闭。
  • FEATURE_MQTT_DEFAULT_IMPL,该选项用于包含阿里提供的MQTT Client实现,因为模组支持MQTT Client,所以关闭该选项。

SDK连接MQTT模组有几种不同的对接方法,为了简化对接,本文档中使能。

  • FEATURE_ATM_ENABLED

该选项使能之后具有下面的子选项可供选择,需要使能。

  • FEATURE_AT_MQTT_ENABLED

如果用户没有用于AT命令收发/解析的框架,可以选择(非必须)使用at_parser框架。

  • FEATURE_AT_PARSER_ENABLED

SDK基于at_parser提供了已对接示例, 如果模组是支持MQTT的sim800 2G模组或者支持ICA MQTT的WiFi模组,可以进行进一步选择相应选项, 这样开发的工作量将进一步减少。 如果不需要对接示例, 请忽略该步骤。

完整的配置开关说明表格如下, 但最终解释应以上面提到的h按键触发文本为准。

配置开关说明
PLATFORM_HAS_STDINT告诉SDK当前要移植的嵌入式平台是否有自带标准头文件<stdint.h>
PLATFORM_HAS_OS目标系统是否运行某个操作系统FEATURE_MQTT_COMM_ENABLEDMQTT长连接功能, 打开后将使SDK提供MQTT网络收发的能力和接口
FEATURE_MQTT_DEFAULT_IMPLSDK内包含的MQTT Client实现, 打开则表示使用SDK内置的MQTT客户端实现
FEATURE_ASYNC_PROTOCOL_STACK对于使用SDK内置的MQTT客户端实现的时候,需要用户实现TCP相关的HAL,这些HAL的TCP发送数据/接收数据的定义是同步机制的,如果目标系统的TCP基于异步机制,可以使能该开关实现SDK从同步到异步机制的转换
FEATURE_DYNAMIC_REGISTER动态注册能力,即设备端只保存了设备的ProductKey和ProductSecret和设备的唯一标识,通过该功能从物联网平台换取DeviceSecret
FEATURE_DEVICE_MODEL_ENABLE使能设备物模型编程相关的接口以及实现FEATURE_DEVICE_MODEL_GATEWAY网关的功能以及相应接口
FEATURE_THREAD_COST_INTERNAL为收包启动一个独立线程FEATURE_SUPPORT_TLS标准TLS连接, 打开后SDK将使用标准的TLS1.2安全协议连接服务器
FEATURE_SUPPORT_ITLS阿里iTLS连接, 打开后SDK将使用阿里自研的iTLS代替TLS建立安全连接
FEATURE_ATM_ENABLED如果系统是使用MCU+外接模组的架构, 并且SDK运行在MCU上, 必须打开该选项, 然后进行配置
FEATURE_AT_MQTT_ENABLED如果MCU连接的通信模组支持MQTT AT, 则使用该选项
FEATURE_AT_PARSER_ENABLED如果用户需要使用SDK提供的AT收发/解析的框架, 则可以使用该选项
FEATURE_AT_MQTT_HAL_ICA基于at_parser的ICA MQTT AT对接示例
FEATURE_AT_MQTT_HAL_SIM800基于at_parser的SIM800 MQTT对接示例

使能需要的SDK配置后,保持配置并退出SDK配置工具。

抽取选中功能的源代码

运行SDK根目录下的extract.bat,客户选中的功能所对应的代码将会被放置到文件夹output。

实现HAL对接函数

Link SDK被设计为可以在不同的操作系统上运行,或者甚至在不支持操作系统的MCU上运行,因此与系统相关的操作被定义成一些HAL函数,需要客户进行实现。另外, 由于不同的通信模组支持的AT指令集不一样,所以与通信模组上TCP相关的操作也被定义成HAL函数需要设备开发者进行实现。

由于不同的用户使能的SDK的功能可能不一样,因此需要对接的HAL函数会不一样,设备开发者只需要实现位于文件output/eng/wrappers/wrapper.c中的HAL函数。下面对可能出现在文件wrapper.c的HAL函数进行讲解。

MCU系统相关HAL

必须实现函数:

序号函数名说明
1HAL_Malloc对应标准C库中的malloc(), 按入参长度开辟一片可用内存, 并返回首地址。
2HAL_Free对应标准C库中的free(),将入参指针所指向的内存空间释放。
3HAL_Printf对应标准C库中的printf(),根据入参格式字符串将字符文本显示到终端,如果用户无需在串口上进行调试,该函数可以为空。
4HAL_Snprintf类似printf,但输出的结果不再是显示到终端,而是存入指定的缓冲区内存。
5HAL_UptimeMs返回一个uint64_t类型的数值,表达设备启动后到当前时间点过去的毫秒数。
6HAL_SleepMs按照指定入参的数值,睡眠相应的毫秒,比如参数是10,那么就会睡眠10毫秒。

对以上函数若需了解更多细节, 可访问SDK官方文档页面

OS相关可选函数

如果MCU没有运行OS,或者SDK的MQTT API并没有在多个线程中被调用,以下函数可以不用修改wrapper.c中相关的函数实现。在有OS场景下并且MQTT API被APP在多个线程中调用,则需要用户对接以下函数。

序号函数名说明
1HAL_MutexCreate创建一个互斥锁,返回值可以传递给HAL_MutexLock/Unlock。
2HAL_MutexDestroy销毁一个互斥锁,这个锁由入参标识。
3HAL_MutexLock申请互斥锁,如果当前该锁由其它线程持有,则当前线程睡眠, 否则继续。
4HAL_MutexUnlock释放互斥锁,此后当前在该锁上睡眠的其它线程将取得锁并往下执行。
5HAL_SemaphoreCreate创建一个信号量,返回值可以传递给HAL_SemaphorePost/Wait。
6HAL_SemaphoreDestroy销毁一个信号量,这个信号量由入参标识。
7HAL_SemaphorePost在指定的计数信号量上做自增操作,解除其它线程的等待。
8HAL_SemaphoreWait在指定的计数信号量上等待并做自减操作。
9HAL_ThreadCreate根据配置参数创建thread。

AT MQTT相关HAL

AT MQTT相关HAL函数位于抽取出来的文件wrapper.c中, 客户需要在这些函数中调用模组提供的AT指令和模组进行数据交互. 函数说明如下。

序号函数名说明
1HAL_AT_MQTT_Init初始化MQTT参数配置。比如初始化MCU与通信模组之间的UART串口设置,初始化MQTT配置参数:clientID/clean session/user name/password/timeout/MQTT Broker的地址和端口等数值。返回值类型为iotx_err_t,其定义位于文件infra_defs.h。
2HAL_AT_MQTT_Deinit如果在HAL_AT_MQTT_Init创建了一些资源,可以在本函数中相关资源释放掉。
3HAL_AT_MQTT_Connect连接MQTT服务器。入参:
proKey:产品密码
devName:设备名
devSecret:设备密码
注:只有通信模组集成了阿里的SDK的时候会使用到该函数的这几个入参,如果模组上并没有集成阿里的SDK,那么略过这几个参数。该函数的入参并没有指定服务器的地址/端口,这两个参数需要在HAL_AT_MQTT_Init()中记录下来。
4HAL_AT_MQTT_Disconnect断开MQTT服务器。
5HAL_AT_MQTT_Subscribe向服务器订阅指定的TOPIC。入参:
topic:主题
qos:服务器质量
mqtt_packet_id: 数据包的ID
mqtt_status: mqtt状态
timeout_ms:超时时间
6HAL_AT_MQTT_Unsubscribe向服务器取消对指定topoic的订阅。入参:
topic:主题
mqtt_packet_id:数据包的ID
mqtt_status:mqtt状态
7HAL_AT_MQTT_Publish向服务器指定的Topic发送消息。
8HAL_AT_MQTT_State返回MQTT的状态,状态值定义在文件mal.h的数据结构iotx_mc_state_t中。

调用接收函数

MCU从模组收到MQTT消息之后,需要调用SDK提供的函数IOT_ATM_Input()(见atm/at_api.h)将MQTT 消息交付给SDK。下面的示例代码演示当MCU从模组收到MQTT消息后,如何调用IOT_ATM_Input函数。

void handle_recv_data()
{
    struct at_mqtt_input param;
    ...

    param.topic = topic_ptr;
    param.topic_len = strlen(topic_ptr);
    param.message = msg_ptr;
    param.msg_len = strlen(msg_ptr);

    if (IOT_ATM_Input(&param) != 0) {
        mal_err("hand data to uplayer fail!\n");
    }
}
            

AT Parser相关HAL

如果选择了at_parser框架, 则需要对接以下四个UART HAL函数, 函数声明见at_wrapper.h. 如果用户不使用at_parser框架请忽略该步。

序号函数名说明
1HAL_AT_Uart_Init该接口对UART进行配置(波特率/停止位等)并初始化。
2HAL_AT_Uart_Deinit该接口对UART去初始化。
3HAL_AT_Uart_Send该接口用于向指定的UART口发送数据。
4HAL_AT_Uart_Recv该接口用于从底层UART buffer接收数据。

产品相关HAL

下面的HAL用于获取产品的身份认证信息, 设备厂商需要设计如何在设备上烧写设备身份信息, 并通过下面的HAL函数将其读出后提供给SDK。

序号函数名说明
1HAL_GetProductKey获取设备的ProductKey, 用于标识设备的产品型号。
2HAL_GetDeviceName获取设备的DeviceName,用于唯一标识单个设备。
3HAL_GetDeviceSecret获取设备的DeviceSecret,用于标识单个设备的密钥。

代码集成

如果设备商的开发环境使用makefile编译代码,可以将SDK抽取出来的代码加入其编译环境进行编译。如果设备商使用KEIL/IAR这样的开发工具, 可以将SDK抽取出来的代码文件加入到IDE的工程中进行编译。

参照example实现产品功能

如果要使用MQTT连云,可参考抽取文件夹中的 eng/examples/mqtt_example_at.c。设备厂商可以将该文件复制到产品工程中,对其进行修改后使用。

该example将连接设备到阿里云,订阅一个指定的topic并发送数据给该topic,即设备上报的消息会被物联网平台发送给设备,下面是example的大概过程说明。

注意:需要在云端将该topic从默认的权限从"订阅"修改为"发布和订阅",如下图所示。

f

从程序入口的main()函数看起,第一步是调用AT模块初始化函数IoT_ATM_Init(),使模组处于ready状态,第二步是调用用户提供的HAL函数获取产品信息。

int main(int argc, char *argv[])
{
    void *      pclient = NULL;
    int         res = 0;
    int         loop_cnt = 0;
    iotx_mqtt_region_types_t    region = IOTX_CLOUD_REGION_SHANGHAI;
    iotx_sign_mqtt_t            sign_mqtt;
    iotx_dev_meta_info_t        meta;
    iotx_mqtt_param_t           mqtt_params;
#ifdef ATM_ENABLED
    if (IOT_ATM_Init() < 0) {
        HAL_Printf("IOT ATM init failed!\n");
        return -1;
    }
#endif
    HAL_Printf("mqtt example\n");
    memset(&meta, 0, sizeof(iotx_dev_meta_info_t));
    HAL_GetProductKey(meta.product_key);
    HAL_GetDeviceName(meta.device_name);
    HAL_GetDeviceSecret(meta.device_secret);
            

注:

  • 上面的三个HAL_GetXXX函数是获取设备的设备证书(ProductKey、DeviceName、DeviceSecret)信息,设备厂商需要自己设计设备的设备证书(ProductKey、DeviceName、DeviceSecret)存放的位置/并将其从指定位置读取出来。
  • 由于设备的唯一标识DeviceName/设备密钥DeviceSecret都是机密信息,设备厂商在设计时可以把相关信息加密后存放到Flash上,在HAL函数里面将其解密后提供给SDK,以避免黑客直接从Flash里面读取设备的身份信息。

接下来对MQTT连接参数进行指定,客户可以根据自己的需要对参数进行修改。

/* Initialize MQTT parameter */
    memset(&mqtt_params, 0x0, sizeof(mqtt_params));
    mqtt_params.port = sign_mqtt.port;
    mqtt_params.host = sign_mqtt.hostname;
    mqtt_params.client_id = sign_mqtt.clientid;
    mqtt_params.username = sign_mqtt.username;
    mqtt_params.password = sign_mqtt.password;
    mqtt_params.request_timeout_ms = 2000;
    mqtt_params.clean_session = 0;
    mqtt_params.keepalive_interval_ms = 60000;
    mqtt_params.read_buf_size = 1024;
    mqtt_params.write_buf_size = 1024;
    mqtt_params.handle_event.h_fp = example_event_handle;
    mqtt_params.handle_event.pcontext = NULL;
    pclient = IOT_MQTT_Construct(&mqtt_params);
            

通过调用接口 IOT_MQTT_Construct() 触发SDK连接云平台,若接口返回值非NULL,则连云成功之后调用example_subscribe对一个指定的topic进行数据订阅。

res = example_subscribe(pclient);
            

example_subscribe的函数内容如下。

注:

  • 设备商需要根据自己的产品设计,订阅自己希望订阅的TOPIC,以及注册相应的处理函数。
  • 订阅的topic的格式需要指定产品型号(product_key)以及设备标识(device_name),如上图中第一个橙色框中的格式。
  • 上图的第二个框展示了如何订阅一个指定的topic以及其处理函数。

以下段落演示MQTT的发布功能,即将业务报文上报到云平台。

 while (1) {
        if (0 == loop_cnt % 20) {
            example_publish(pclient);
        }

        IOT_MQTT_Yield(pclient, 200);

        loop_cnt += 1;
    }
            

下面是example_publish函数体的部分内容。

注:

  • 上面的代码是周期性的将固定的消息发送给云端,设备商需要根据自己的产品功能,在必要的时候才上传数据给物联网平台。
  • 客户可以删除main函数中example_publish(pclient)语句,避免周期发送无效数据给到云端。
  • IOT_MQTT_Yield是让SDK去接收来自MQTT Broker的数据,其中200毫秒是等待时间,如果用户的消息数量比较大/或者实时性要求较高,可以将时间改小。

功能调试

下面的信息截图以mqtt_example_at.c为例编写。

如何判断设备已连接到阿里云

下面的打印是HAL_Printf函数将信息打印到串口后运行example的输出内容,其中使用橙色圈选的信息表明设备已成功连接到阿里云物联网平台。

d

如何判断设备已成功发送数据到云端

登录阿里网物联网平台的商家后台,选中指定的设备,可以查看是否收到来自设备的消息,如下图所示。

gg

注:上图中的内容只能看见消息发送到了哪个topic,消息的内容并不会显示出来。

如何判断设备已可成功接收来自云端数据

在商家后台的"下行消息分析"分析中可以看见由物联网平台发送给设备的消息。

也可在设备端查看是否已收到来自云端的数据,exmaple代码中收到云端发送的数据的打印信息如下所示。

至此,SDK在MCU与模组之间的适配开发已结束,用户可以进行产品业务功能的实现。