应用场景:设备的硬件由一个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
必须实现函数:
序号 | 函数名 | 说明 |
---|---|---|
1 | HAL_Malloc | 对应标准C库中的malloc(), 按入参长度开辟一片可用内存, 并返回首地址。 |
2 | HAL_Free | 对应标准C库中的free(),将入参指针所指向的内存空间释放。 |
3 | HAL_Printf | 对应标准C库中的printf(),根据入参格式字符串将字符文本显示到终端,如果用户无需在串口上进行调试,该函数可以为空。 |
4 | HAL_Snprintf | 类似printf,但输出的结果不再是显示到终端,而是存入指定的缓冲区内存。 |
5 | HAL_UptimeMs | 返回一个uint64_t类型的数值,表达设备启动后到当前时间点过去的毫秒数。 |
6 | HAL_SleepMs | 按照指定入参的数值,睡眠相应的毫秒,比如参数是10,那么就会睡眠10毫秒。 |
对以上函数若需了解更多细节, 可访问SDK官方文档页面。
OS相关可选函数
如果MCU没有运行OS,或者SDK的MQTT API并没有在多个线程中被调用,以下函数可以不用修改wrapper.c中相关的函数实现。在有OS场景下并且MQTT API被APP在多个线程中调用,则需要用户对接以下函数。
序号 | 函数名 | 说明 |
---|---|---|
1 | HAL_MutexCreate | 创建一个互斥锁,返回值可以传递给HAL_MutexLock/Unlock。 |
2 | HAL_MutexDestroy | 销毁一个互斥锁,这个锁由入参标识。 |
3 | HAL_MutexLock | 申请互斥锁,如果当前该锁由其它线程持有,则当前线程睡眠, 否则继续。 |
4 | HAL_MutexUnlock | 释放互斥锁,此后当前在该锁上睡眠的其它线程将取得锁并往下执行。 |
5 | HAL_SemaphoreCreate | 创建一个信号量,返回值可以传递给HAL_SemaphorePost/Wait。 |
6 | HAL_SemaphoreDestroy | 销毁一个信号量,这个信号量由入参标识。 |
7 | HAL_SemaphorePost | 在指定的计数信号量上做自增操作,解除其它线程的等待。 |
8 | HAL_SemaphoreWait | 在指定的计数信号量上等待并做自减操作。 |
9 | HAL_ThreadCreate | 根据配置参数创建thread。 |
AT MQTT相关HAL
AT MQTT相关HAL函数位于抽取出来的文件wrapper.c中, 客户需要在这些函数中调用模组提供的AT指令和模组进行数据交互. 函数说明如下。
序号 | 函数名 | 说明 |
---|---|---|
1 | HAL_AT_MQTT_Init | 初始化MQTT参数配置。比如初始化MCU与通信模组之间的UART串口设置,初始化MQTT配置参数:clientID/clean session/user name/password/timeout/MQTT Broker的地址和端口等数值。返回值类型为iotx_err_t,其定义位于文件infra_defs.h。 |
2 | HAL_AT_MQTT_Deinit | 如果在HAL_AT_MQTT_Init创建了一些资源,可以在本函数中相关资源释放掉。 |
3 | HAL_AT_MQTT_Connect | 连接MQTT服务器。入参: 注:只有通信模组集成了阿里的SDK的时候会使用到该函数的这几个入参,如果模组上并没有集成阿里的SDK,那么略过这几个参数。该函数的入参并没有指定服务器的地址/端口,这两个参数需要在HAL_AT_MQTT_Init()中记录下来。 |
4 | HAL_AT_MQTT_Disconnect | 断开MQTT服务器。 |
5 | HAL_AT_MQTT_Subscribe | 向服务器订阅指定的TOPIC。入参:
|
6 | HAL_AT_MQTT_Unsubscribe | 向服务器取消对指定topoic的订阅。入参:
|
7 | HAL_AT_MQTT_Publish | 向服务器指定的Topic发送消息。 |
8 | HAL_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(¶m) != 0) {
mal_err("hand data to uplayer fail!\n");
}
}
AT Parser相关HAL
如果选择了at_parser框架, 则需要对接以下四个UART HAL函数, 函数声明见at_wrapper.h. 如果用户不使用at_parser框架请忽略该步。
序号 | 函数名 | 说明 |
---|---|---|
1 | HAL_AT_Uart_Init | 该接口对UART进行配置(波特率/停止位等)并初始化。 |
2 | HAL_AT_Uart_Deinit | 该接口对UART去初始化。 |
3 | HAL_AT_Uart_Send | 该接口用于向指定的UART口发送数据。 |
4 | HAL_AT_Uart_Recv | 该接口用于从底层UART buffer接收数据。 |
产品相关HAL
下面的HAL用于获取产品的身份认证信息, 设备厂商需要设计如何在设备上烧写设备身份信息, 并通过下面的HAL函数将其读出后提供给SDK。
序号 | 函数名 | 说明 |
---|---|---|
1 | HAL_GetProductKey | 获取设备的ProductKey, 用于标识设备的产品型号。 |
2 | HAL_GetDeviceName | 获取设备的DeviceName,用于唯一标识单个设备。 |
3 | HAL_GetDeviceSecret | 获取设备的DeviceSecret,用于标识单个设备的密钥。 |
代码集成
如果设备商的开发环境使用makefile编译代码,可以将SDK抽取出来的代码加入其编译环境进行编译。如果设备商使用KEIL/IAR这样的开发工具, 可以将SDK抽取出来的代码文件加入到IDE的工程中进行编译。
参照example实现产品功能
如果要使用MQTT连云,可参考抽取文件夹中的 eng/examples/mqtt_example_at.c。设备厂商可以将该文件复制到产品工程中,对其进行修改后使用。
该example将连接设备到阿里云,订阅一个指定的topic并发送数据给该topic,即设备上报的消息会被物联网平台发送给设备,下面是example的大概过程说明。
注意:需要在云端将该topic从默认的权限从"订阅"修改为"发布和订阅",如下图所示。
从程序入口的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的输出内容,其中使用橙色圈选的信息表明设备已成功连接到阿里云物联网平台。
如何判断设备已成功发送数据到云端
登录阿里网物联网平台的商家后台,选中指定的设备,可以查看是否收到来自设备的消息,如下图所示。
注:上图中的内容只能看见消息发送到了哪个topic,消息的内容并不会显示出来。
如何判断设备已可成功接收来自云端数据
在商家后台的"下行消息分析"分析中可以看见由物联网平台发送给设备的消息。
也可在设备端查看是否已收到来自云端的数据,exmaple代码中收到云端发送的数据的打印信息如下所示。
至此,SDK在MCU与模组之间的适配开发已结束,用户可以进行产品业务功能的实现。