【NB-IoT】用DSI2598+開發板自造落塵檢測器

作者:楊俊益

根據IoT應用場景不同,物聯網的需求可以分為高速率、中速率、低功耗高覆蓋率等三層,其中LPWAN(Low Power WideArea Network;低功耗廣域網絡)即是專為低功耗、遠距離、多數量連接的物聯網應用而生的網路技術,常見的有LoRa、Sigfox、NB-IoT等,目前以NB-IoT的市佔率最高。

LPWAN 技術可又被分為授權頻段( NB-IoT)的廣域網技術及非授權頻段( Sigfox、LoRa)的廣域網技術兩類,不同的LPWAN技術在接入網絡、部署方式、 技術特點、功耗性能及服務模式上都有所差異。

LPWAN在IoT市場的應用最廣

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已推出了三代,功能持續優化(資料來源:Ideas Hatch)

DSI 2598開發板已推出了三代,功能持續優化,比較表如下:

第三代的DSI 2598+的腳位定義可參照下圖:

DSI 2598+腳位圖(資料來源:Ideas Hatch)

開發環境設定

接下來實際動手設定 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+來自造一款落塵檢測器,介紹如下:

材料:

  1. DSI2598+
  2. UPS鋰電池充電電路模組 – USB接頭
  3. SSD1306 1.3吋
  4. PMS 5003T 落塵感測器
  5. 小麵包板 *2
  6. 杜邦線 公對公 與 公對母 些許
  7. 18650 鋰電池

接線圖:

實作圖:

透過NB-IoT低耗電特性,此作品使用一節18650鋰電池,操作時間上幾乎能達到3天以上。且透過MQTT傳輸機制,可以立即傳輸收集到的落塵資料到後台,且在後台透過Node-RED便能輕鬆建置dashboard與儲存資料,管理者也可以透過手機或平板端來檢視收集到的落塵資訊,非常便利。

手機或平板的App ( MQTT Dash / IoT OnOff )

Node-RED dashboard

IoT OnOff (App)

新增功能

希望藉由 NB-IoT省電特性,希望能將落塵收集器的工作使用時間能延長,才能符合經濟效益。系統調整方式如下:

1.SSD1306藉由按鈕控制是否顯示資訊,在需要時再開啟即可。
2.增設LED的鋰電電量顯示器,一樣可藉由按鈕來呈現目前電量狀態。
3.PMS5003讀出落塵資料後,便把收集落塵風扇關閉。
4.BC26(NB-IoT)透過MQTT傳回資料後,便進入休眠模式,已節省電量。
5.再讓STM32進入休眠模式,待下次(60分鐘後)傳回落塵資料再重新啟動。

程式說明:

PMS5003T_to_MQTT.ino(主程式)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#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)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#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+無非是個最佳的運用選擇。

(責任編輯:謝涵如)

 

工作坊資訊:

報名活動網址:https://ievents.iii.org.tw/EventS.aspx?t=0&id=1281

活動時間:7/9(五)14:00~16:00

Author: 楊俊益

《MQTT 與 IoT 整合運用》臉書社團版主,是一位熱血Maker ,專長包含IoT及MQTT等,曾擔任國產IC開發板DSI 5168、2598種子講師,開發過智能居家防護警示系統設計等。

Share This Post On

發表

跳至工具列