作者:楊俊益
根據IoT應用場景不同,物聯網的需求可以分為高速率、中速率、低功耗高覆蓋率等三層,其中LPWAN(Low Power WideArea Network;低功耗廣域網絡)即是專為低功耗、遠距離、多數量連接的物聯網應用而生的網路技術,常見的有LoRa、Sigfox、NB-IoT等,目前以NB-IoT的市佔率最高。
LPWAN 技術可又被分為授權頻段( NB-IoT)的廣域網技術及非授權頻段( Sigfox、LoRa)的廣域網技術兩類,不同的LPWAN技術在接入網絡、部署方式、 技術特點、功耗性能及服務模式上都有所差異。
Sigfox和LoRa 屬於非授權頻段,應用時需要單獨建構網絡,而且使用的頻段沒有授權,在安全性上也可能存在缺陷。NB-IoT是3GPP推出的標準技術,已成為了目前被全球廣泛接受的全新窄帶物聯網技術標準。
從接入網絡上看,由於 NB-IoT 是在 LTE 基礎上發展起來的, 其主要採用了 LTE 的相關技術,並針對自身特點做了相應的修改。從技術特點上看,NB-IoT 的部署方式較為快捷、靈活,支持 3 種部署場景。此外, NB-IoT 也可以部署在 2G/3G 網絡。
NB-IoT與WiFi之差異可參考下表:
DSI2598+開發板介紹
DSI2598+使用聯發科技NB-IoT晶片 – MT2625模組與意法半導體的微控制器 - STM32F103C8T6,提供PWM、I2C、SPI、ADC、UART等多種腳位功能,簡單但完整,可讓使用者無縫接軌任何Arduino程式庫,進行各項功能程式開發,是改善上一代DSI2598速度及記憶體空間不足的第三代 NB-IoT開發板。
DSI 2598開發板已推出了三代,功能持續優化,比較表如下:
第三代的DSI 2598+的腳位定義可參照下圖:
開發環境設定
接下來實際動手設定 DSI2598+開發板的Arduino環境 (for Windows 10 作業系統)。
1.安裝DFU Windows 的Driver :
自https://github.com/rogerclarkmelbourne/Arduino_STM32下載原廠驅動程式Arduino_STM32-master.zip:
解開檔案之後在目錄下用系統管理者執行 Arduino_STM32-master\drivers\win\install_drivers.bat,會出現下列畫面:
2.安裝Arduino IDE for 1.8.13:
下載區:https://www.arduino.cc/en/software
3. 設定 STM32 所需的管理員網址:
在”Additional Boards Manager URLs:” 輸入http://dan.drown.org/stm32duino/package_STM32duino_index.json
DSI2598+ 實作運用 — 落塵檢測器(Particle Measuring System)
在有些 GMP 與科技工廠需要隨時監測落塵數量與狀況,之前都會透過手持或是固定式檢測器收集落塵相關資料,然後在透過傳輸線傳回電腦做收集統計,但由於收集範圍過大,手持式需要由人員在不同地方做收集並且傳回後台系統,而固定式的檢測器則需要有插電處提供電源做收集。但固定式機台成本過高,不適合到處擺設此裝置收集落塵資訊。
為解決現況問題,特別運用DSI 2598+來自造一款落塵檢測器,介紹如下:
材料:
- DSI2598+
- UPS鋰電池充電電路模組 – USB接頭
- SSD1306 1.3吋
- PMS 5003T 落塵感測器
- 小麵包板 *2
- 杜邦線 公對公 與 公對母 些許
- 18650 鋰電池
接線圖:
實作圖:
透過NB-IoT低耗電特性,此作品使用一節18650鋰電池,操作時間上幾乎能達到3天以上。且透過MQTT傳輸機制,可以立即傳輸收集到的落塵資料到後台,且在後台透過Node-RED便能輕鬆建置dashboard與儲存資料,管理者也可以透過手機或平板端來檢視收集到的落塵資訊,非常便利。
新增功能
希望藉由 NB-IoT省電特性,希望能將落塵收集器的工作使用時間能延長,才能符合經濟效益。系統調整方式如下:
1.SSD1306藉由按鈕控制是否顯示資訊,在需要時再開啟即可。
2.增設LED的鋰電電量顯示器,一樣可藉由按鈕來呈現目前電量狀態。
3.PMS5003讀出落塵資料後,便把收集落塵風扇關閉。
4.BC26(NB-IoT)透過MQTT傳回資料後,便進入休眠模式,已節省電量。
5.再讓STM32進入休眠模式,待下次(60分鐘後)傳回落塵資料再重新啟動。
程式說明:
PMS5003T_to_MQTT.ino(主程式)
#include "BC26Init.h"
#include
#include
#include
#define DELAYTIME 3600
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
String MQTT_Server = "broker.emqx.io"; //MQTT Server
String MQTT_Port = "1883"; //MQTT Port
String MQTT_user = "user1"; //user
String MQTT_pass = "123456"; //password
String MQTTtopic_Temp = "DSI2598Test/Temp"; //Temp
String MQTTtopic_Humi = "DSI2598Test/Humi"; //Humi
String MQTTtopic_PM25 = "DSI2598Test/PM25"; //PM25
String MQTTtopic_PM10 = "DSI2598Test/PM10"; //PM10
String MQTTtopic_PM1 = "DSI2598Test/PM1"; //PM1
String MQTTmessage = "";
int errorFlag = 0 ;
long pmat10 = 0;
long pmat25 = 0;
long pmat100 = 0;
unsigned int temperature = 0;
unsigned int humandity = 0;
unsigned long startTime;
RTClock rt (RTCSEL_LSI); // initialise
int buttonPin = PB9; //觸發喚醒 OLED的 Pin
int ledToggle;
int previousState = HIGH;
unsigned int previousPress = 0;
unsigned int buttonFlag = 1;
int buttonDebounce = 20;
void setup() {
Serial.begin(115200);
Serial1.begin(115200); //與 BC26 溝通的 UART
Serial2.begin(9600); //與 PMS5003T 溝通的 UART
pinMode(buttonPin, INPUT); //宣告 觸發 OLED 顯示的 Pin 為 中斷服務
attachInterrupt(digitalPinToInterrupt(buttonPin), button_ISR, FALLING);
PMSpassiveMode(); //宣告 PMS5003T 為 passiveMode
u8g2.begin(); //啟動 u8g2 library
u8g2.enableUTF8Print();
u8g2.setDisplayRotation(U8G2_R2); //OLED 旋轉 180度
u8g2.setFont(u8g2_font_Born2bSportyV2_tr); //使用我們做好的字型
u8g2.clearBuffer();
u8g2.setCursor(5, 30);
u8g2.print("Connecting...");
u8g2.drawFrame(0, 0, 126, 64);
u8g2.sendBuffer();
if (!BC26init()) //初始化 BC26 , 若連線失敗等待5秒鐘再重新啟動
{
u8g2.clearBuffer();
u8g2.setCursor(5, 30);
u8g2.print("Failed");
u8g2.drawFrame(0, 0, 126, 64);
u8g2.sendBuffer();
delay (5000);
nvic_sys_reset();
}
u8g2.setFont(u8g2_font_Born2bSportyV2_tr);
u8g2.clearBuffer();
u8g2.setCursor(5, 30);
u8g2.print("Successfully");
u8g2.sendBuffer();
delay(1000);
u8g2.setCursor(5, 30);
u8g2.print("initialization");
u8g2.clearBuffer();
u8g2.setCursor(13, 18);
u8g2.setFont(u8g2_font_Born2bSportyV2_tr);
u8g2.print("IoT Service Hub");
u8g2.drawFrame(0, 0, 126, 64);
u8g2.setFont( u8g2_font_5x8_tf );
u8g2.setCursor(5, 35);
u8g2.print("PM1: ");
u8g2.setCursor(64, 35);
u8g2.print("PM2.5: ");
u8g2.setCursor(5, 50);
u8g2.print("Temp: ");
u8g2.setCursor(64, 50);
u8g2.print("Humi: ");
u8g2.drawLine(0, 51, 128, 51);
u8g2.sendBuffer();
Serial.println("initialization OK ....");
Serial.println("Start Loop Program ...");
delay(5000);
startTime = millis();
}
void loop() {
PMSwake(); //開啟 PMS5003T 風扇
delay(20000);
retrievePmValue(); //抓取 落塵數值與溫溼度
while ( errorFlag ) //若抓回數值有誤,延遲2秒重新抓取
{
retrievePmValue();
delay(2000);
}
Serial.print("Fan turn off");
PMSsleep(); //關閉 PMS5003T 的風扇,節省電能
connect_MQTT(MQTT_Server, MQTT_Port, MQTT_user, MQTT_pass); //開啟 MQTT 連線
String Buff ;
displayValue();
Buff += temperature ; //數值轉為 String
Publish_MQTT(MQTTtopic_Temp, Buff); //MQTT 傳回溫度
Buff = "";
Buff += humandity ;
Publish_MQTT(MQTTtopic_Humi, Buff); //MQTT 傳回濕度
Buff = "";
Buff += pmat10 ;
Publish_MQTT(MQTTtopic_PM1, Buff); //MQTT 傳回PM1
Buff = "";
Buff += pmat25 ;
Publish_MQTT(MQTTtopic_PM25, Buff); //MQTT 傳回PM2.5
Buff = "";
Buff += pmat100 ;
Publish_MQTT(MQTTtopic_PM10, Buff); //MQTT 傳回PM10
Close_MQTT(); //關閉 MQTT 連結
//Send_ATcommand("AT+QPOWD=0", 1); //關閉 BC26 電源
Send_ATcommand("AT+CFUN=0",1); //讓 BC26 進入省電模式
//disableAllPeripheralClocks();
sleepAndWakeUp(STOP, &rt, 30); //讓 STM32 進入 STOP 模式
u8g2.setPowerSave(buttonFlag);
for (int i = 1 ; i <= DELAYTIME ; i++ ) // 控制 3600 秒傳回一次數據; 因為進入 STOP 狀況, 事件與中斷會再喚醒 { // 透過喚醒時間延遲 char temp[5]; sprintf(temp, "%04d", i); u8g2.setPowerSave(buttonFlag); u8g2.setCursor(90, 62); u8g2.print(temp); u8g2.sendBuffer(); delay(90); u8g2.setCursor(90, 62); u8g2.print(" "); u8g2.setCursor(5, 62); u8g2.print(" "); u8g2.sendBuffer(); } nvic_sys_reset(); //STM32 MCU 重新啟動 } void button_ISR() { delay(20); buttonFlag = !buttonFlag; Serial.print("Button ="); Serial.println(buttonFlag); delay(200); } void displayValue() //在 OLED 顯示相關數值 { u8g2.setFont( u8g2_font_amstrad_cpc_extended_8f ); u8g2.setCursor(35, 35); u8g2.print(pmat10); u8g2.setCursor(100, 35); u8g2.print(pmat25); u8g2.setCursor(35, 50); u8g2.print(temperature); u8g2.setCursor(100, 50); u8g2.print(humandity); u8g2.sendBuffer(); } void retrievePmValue() { //取出 PMS5003T 相關數值 函式 int count = 0; unsigned char c; unsigned char high; while (Serial2.available()) { c = Serial2.read(); if ((count == 0 && c != 0x42) || (count == 1 && c != 0x4d)) { Serial.println("check failed"); errorFlag = 1 ; break; } if (count > 27) {
Serial.println("complete");
errorFlag = 0 ;
break;
}
else if (count == 10 || count == 12 || count == 14 || count == 24 || count == 26) {
high = c;
}
else if (count == 11) {
pmat10 = 256 * high + c;
Serial.print("PM1.0=");
Serial.print(pmat10);
Serial.println(" ug/m3");
}
else if (count == 13) {
pmat25 = 256 * high + c;
Serial.print("PM2.5=");
Serial.print(pmat25);
Serial.println(" ug/m3");
}
else if (count == 15) {
pmat100 = 256 * high + c;
Serial.print("PM10=");
Serial.print(pmat100);
Serial.println(" ug/m3");
}
else if (count == 25) {
temperature = (256 * high + c) / 10;
Serial.print("Temp=");
Serial.print(temperature);
Serial.println(" (C)");
}
else if (count == 27) {
humandity = (256 * high + c) / 10;
Serial.print("Humidity=");
Serial.print(humandity);
Serial.println(" (%)");
}
count++;
}
while (Serial2.available()) Serial2.read();
Serial.println();
}
void PMSpassiveMode() //PMS5003T passiveMode
{
uint8_t command[] = { 0x42, 0x4D, 0xE1, 0x00, 0x01, 0x01, 0x70 };
Serial2.write(command, sizeof(command));
}
void PMSwake() //打開 PMS5003T 風扇
{
uint8_t command[] = { 0x42, 0x4D, 0xE4, 0x00, 0x01, 0x01, 0x74 };
Serial2.write(command, sizeof(command));
}
void PMSsleep() //關閉 PMS5003T 風扇
{
uint8_t command[] = { 0x42, 0x4D, 0xE4, 0x00, 0x00, 0x01, 0x73 };
Serial2.write(command, sizeof(command));
}
BC26Init.h(NB-IoT function)
#include
#include
void(* resetFunc) (void) = 0;
byte Rset_Count=0;
int waitingTime = 30000;
String Check_RevData()
{
String data= "";
char c;
while (Serial1.available())
{
delay(50);
c = Serial1.read(); //Conduct a serial read
data+=c; //Shorthand for data = data + c
if (c=='\n') break;
}
data.trim();
return data;
}
byte Send_ATcommand(String msg,byte stepnum)
{
String Showmsg,C_temp;
Serial.println(msg);
Serial1.println(msg);
Showmsg=Check_RevData();
//Serial.println(Showmsg);
long StartTime=millis();
switch (stepnum)
{
case 0: // Reset BC26
C_temp="+IP:";
break;
case 1: // Other Data
C_temp="OK";
break;
case 2: // Check IPAddress
C_temp="+CGPADDR:";
break;
case 10: // build MQTT Server
C_temp="+QMTOPEN: 0,0";
break;
case 11: // Connect to MQTT server by username and password
C_temp="+QMTCONN: 0,0,0";
break;
case 12: // Publisher MQTT Data
C_temp="+QMTPUB: 0,0,0";
break;
case 13: // Sub MQTT Data
C_temp="+QMTSUB: 0,1,0,0";
break;
}
while (!Showmsg.startsWith(C_temp))
{
Showmsg=Check_RevData();
if (Showmsg.startsWith("+")) Serial.println(Showmsg);
if ((StartTime+waitingTime) < millis()) return stepnum;
}
return 99;
}
bool BC26init() // initialization BC26
{
Send_ATcommand("AT+QGACT=1,1,"apn","internet.iot"",1);
Send_ATcommand("AT+QCGDEFCONT="IP","internet.iot"",1);
Send_ATcommand("AT+QBAND=1,8",1);
Send_ATcommand("AT+QRST=1",0);
if (Send_ATcommand("ATE0",1)==99)
if (Send_ATcommand("AT+CGPADDR=1",2)==99) return true;
return false;
}
bool connect_MQTT(String Broker,String port,String user,String pass) // connect MQTT Broker
{
String tempBuff;
tempBuff = """ + Broker + """ + "," + port;
tempBuff="AT+QMTOPEN=0," + tempBuff;
if (Send_ATcommand(tempBuff,10)!=99)
return false;
tempBuff= """ + user + """ + "," + """ + pass + """;
tempBuff="AT+QMTCONN=0,0," + tempBuff;
if (Send_ATcommand(tempBuff,11)!=99)
return false;
return true;
}
bool Publish_MQTT(String topic, String message) {
String tempBuff;
tempBuff = """ + topic + """ + "," + message ;
tempBuff = "AT+QMTPUB=0,0,0,0," + tempBuff ;
if (Send_ATcommand(tempBuff,12)!=99)
return false;
return true;
}
bool Sub_MQTT(String topic) // Subscribe Topic
{
String tempBuff;
tempBuff = """ + topic + """ + "," + "0";
tempBuff = "AT+QMTSUB=0,1," + tempBuff;
if (Send_ATcommand(tempBuff,13)!= 99)
return false;
return true;
}
bool Close_MQTT() // Close MQTT connect
{
String tempBuff;
tempBuff="AT+QMTCLOSE=0";
if (Send_ATcommand(tempBuff,1)!=99)
return false;
return true;
}
String JSON_DEC_data (String input,String findData)
{
int index = input.indexOf(',');
int x = input.substring(0, index).toInt();
String json = input.substring(index + 1, input.length());
//Serial.println(json);
index = json.indexOf(':');
x = json.substring(0, index).toInt();
json = json.substring(index + 1, json.length());
//Serial.println(json);
DynamicJsonDocument doc(1024);
deserializeJson(doc, json);
JsonObject obj = doc.as();
return obj[findData];
}
bool Sub_Ideaschain(String attrestopic)
{
String S_temp;
S_temp="""+ attrestopic + """ + "," + "0";
S_temp="AT+QMTSUB=0,1," + S_temp; // Qos 0
Serial.println(S_temp);
Serial1.println(S_temp);
delay (2000);
return true;
}
String Get_Publish_MQTT(byte mode,String attreqtopic , String message) {
String Showmsg;
String S_temp,T_temp;
//delay (1000);
if (mode==0) T_temp="sharedKeys";
if (mode==1) T_temp="clientKeys";
S_temp=""" + attreqtopic + """ + "," + ""{"" + T_temp + "":"" + message + ""}"";
S_temp="AT+QMTPUB=0,0,0,0," + S_temp;
Serial.println(S_temp);
Serial1.println(S_temp);
Showmsg=Check_RevData();
long StartTime=millis();
while (!Showmsg.startsWith("+QMTRECV:"))
{
delay(100);
Showmsg=Check_RevData();
if (Showmsg.length()>30) break;
//Serial.println(Showmsg);
if ((StartTime+waitingTime) < millis()) return "error";
}
//Serial.println(Showmsg);
return JSON_DEC_data (Showmsg,message);
}
所需的額外函式庫:
#include<U8g2lib.h>
下載處 : https://github.com/olikraus/u8g2
#include<TimeLib.h>
下載處 : https://github.com/PaulStoffregen/Time
結語
以NB-IoT設計上的概念完全不能以wifi的模式來思考,要盡可能的用最低的電能來發揮他最大的效益。而DSI2598+是以STM32F103C+BC26為架構所設計的板子,對於高效能與低耗電且會去計較一分一毫電量的需求者,與在一些運用場合上無法提供wifi時,DSI2598+無非是個最佳的運用選擇。
(責任編輯:謝涵如)
「經濟部工業局廣告」
- 【防疫居家學習】用DSI5168自製上、下課鐘聲播放器 - 2021/07/26
- 【NB-IoT】用DSI2598+開發板自造落塵檢測器 - 2021/06/28