應用場景:設備的硬件由一個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與模組之間的適配開發已結束,用戶可以進行產品業務功能的實現。