前言
接觸過各式各樣的溫度感測器,從純類比 LM35 之類的,還是數位的 DHT 系列的,真的是百家爭萌,也讓人看得眼花撩亂的。而關於數位的溫度感測器中,其中有一款 DS18B20 ,因為價格非常便宜,使用非常簡易,溫度適用範圍大,溫度解析度跟準確度也不差,又支援多個溫度感測器透過 1-wire 匯流排所組合在一起的溫度陣列,所以是非常受歡迎的。
來利用 Arduino 來做個可以控制很多個 DS18B20 溫度感測器的控制器吧。目標是電腦掛上這個控制器後,可以透過序列埠來跟控制器溝通,讀取任一已經掛上控制器的 DS18B20 的溫度感測所傳進來的溫度。透過序列埠,也很容易進一步跟電腦上的 LabView 或 Python 之類的控制程式進行整合,提供更強大的功能喲。
DS18B20 溫度感測器
DS18B20溫度感測器,是DALLAS公司生產的數位溫度感測器。透過特殊的1-Wire 單匯流排來通訊,具有線路簡單,體積小的特點。因此非常容易用它來組成一個測溫系統,且在一根通信線,可以同時掛很多這樣的數字溫度計(最多八個),十分方便。
特點:
(1)獨特的單線接口方式,只要求一個 Pin 即可實現雙向通信。
(2) 測量溫度範圍在-55C 到+125C之間,非常廣泛。
(3)每個感測器上都有自己獨一無二的序號 ID。支援獨特的 1-wire匯流排,可在匯流排上面同時控制最多八個 DS18B20 所組成溫度感測陣列,進行多個溫度的讀取。
(4)配線簡單,實際應用中不需要外部任何元件即可實現測溫。
(5)測量結果以9~12位的數位解析度量方式串列傳送。
(6)多種包裝,甚至是具備不銹鋼保護管的包裝,可以直接放入液體中,方便各式不同環境下之應用。
硬體接線
整個硬體接線如下圖所示。非常的簡單,一隻腳接 +5V 的 Vcc,另一隻腳接地。中間那隻腳就是 1-wire 匯流排,全部接在一起然後接上 Arduino Uno 的 Digital I/O,這裡我們選擇 Pin6。
這種 1-wire 匯流排,大家用的原理跟方法都差不多,都是利用 Wire-AND 的方式來偵測。所以要加上一個上拉電阻到 Vcc,維持住無人通訊的時候必然是高電位。這裡上拉電阻的選擇是 47K 歐姆。
DS18B20 就看需求囉,可以接上一顆,或是依序接上最多八顆。
實際接線的照片,下面是有帶壓克力外殼的 Arduino UNO。上面是有個小麵包版的空白 Shield。只要電路很簡單的情況下,這樣的組合非常容易做實驗。我手上剛好有三顆 DS18B20,所以直接全部都給它掛上去測試囉。
另外一個角度所拍攝的照片
這裡只是實驗用,真的要實用化的話可以採用 Arduino Nano 來小型化,外殼上面放上八個類似像耳機的接頭。要用的話就可以把 DS18B20 視需求掛上所需的顆數。然後透過序列埠指令來跟電腦裡的主控程式溝通。而我們的 Arduino 有點像是控制這些溫度感測器陣列的 Hub。
程式碼簡介
這裡顯示出 Arduino 的優勢囉,主 DS18B20 控制程式碼人家都幫你寫好囉,我們只要把它們 include 進來,然後專心在我們所需要整合的事情上面就可以囉。
這裡我們主要是從 RS232 序列埠接收文字指令,然後送出所讀的溫度。所以我們只要專注在如何從 RS232 序列埠收指令,字串判斷處理,然後再將溫度透過文字從 RS232序列埠送出這一部分。大概1小時左右就開發完畢囉。
這裡提供兩個程式:
程式一用在單一感測器,只要從 RS232 輸入 "temp\r" 指令,就立刻會從 RS232序列埠回應文字格式的溫度,例如:"27.34\r"
程式二用在多感測器網路,只要從 RS232 輸入 "temp XXX\r" 指令,其中 XXX 爲感測器號碼,從0開始計數,就立刻會從 RS232序列埠回應此對應 XXX 感測器所感測到的文字格式的溫度,例如:"27.34\r"
例如 "temp 0 \r" 就會回傳 0號感測器的溫度, "temp 1 \r" 就會回傳 1號感測器的溫度, "temp 2 \r" 就會回傳 2號感測器的溫度... 其它以此類推。
DS18B20 溫度陣列,最多只能支援8顆在同一個 1-wire 匯流排。所以 XXX 只能 0 - 7。
如果所對應號碼的感測器不存在,溫度將會回傳 -127。
DS18B20 感測器驅動:
這裡需要 #include <OneWire.h>
這個是DS18B20 所使用特殊 One Wire 匯流排的驅動函式庫。從 IDE 的函式庫管理載入即可。
OneWire oneWire(OneWirePin); 用來產生1-wire控制物件並告知此 One Wire 所在的Pin腳位置
在這裏,我們是接在 Arduino Uno 的 Pin 6,所以
#define OneWirePin 6
然後就是 #include <DallasTemperature.h> 這個主要控制 DS18B20 的函式庫
這個函式庫,需要依賴 OneWire 的控制物件來對感測器利用 1-wire 匯流排做溝通,所以需要將剛剛所產生的 OneWire 控制物件的位址傳入。
DallasTemperature ds18b20(&oneWire); 用來產生DS18B20控制物件並告知1-wire控制物件的位址
前置工作做完後,溫度的讀取非常的簡單
ds18b20.requestTemperatures(); 用來要求匯流排上的所有感測器開始讀取並轉換溫度。
ds18b20.getTempCByIndex(XXX) 用來要求第 index XXX 號碼的感測器回傳所轉換好的溫度。其中 index XXX = 0 - 7
要注意的, 因為每個 DS18B20自己都有自己獨一無二的出廠識別碼。為了方便, index 的代碼是由 <DallasTemperature.h> 這個函式庫執行時自己自動指定的,實際上要稍微試一下才知道 0, 1, 2, 3 ... 會是誰。
一旦確定後只要不換感測器,它們的次序是不會改變的。
多工性:
DS18B20 當在工作,溫度的轉換是需要時間的。所以無法持續很短的時間內回應外界的溫度詢問。大概要給個 1秒的時間 Delay 一下。但是我們的程式碼是用在要能快速回應外界程式的溫度詢問的。所以不能使用 Arduino 的 delayMicroseconds(1000) 這個硬指令來做 delay 的動作。這個指令會讓程式對外界的反應整個停擺。
所以要改用 millis() 這個指令,取得現在的 tick 時間。透過計算 tick 的時間差來得知是否 1000 mS 已經時間到,可以繼續跟感測器要求傳出下一個溫度數值。
程式要能即時反應外界的溫度詢問,所以需要用一個變數 temperature,或是陣列 temperature[8] 來儲存目前最新鮮的溫度值。一旦外界透過 RS232 序列埠指令詢問,就立刻把這個最新鮮的溫度值送出去。
一旦 1000mS 計時的時間已到,就繼續要求感測器轉換最新的溫度值,然後更新 temperature 變數或陣列。
計時器計算:
prevTime = millis(); 一開始計時器的初值 tick
currTime = millis(); 取得現在 tick 時間
if(prevTime > currTime) 預防 prevTime 這個整數數字爆掉,爆掉就重頭來過囉
prevTime = currTime;
if(currTime - prevTime >= updateInterval) { 時間差大於 1000mS,時間到
ds18b20.requestTemperatures(); 要求轉換溫度
temperature = ds18b20.getTempCByIndex(0); 要求 index:0 送上溫度值
prevTime = currTime; 重置時間,啟動下次計時
}
RS232 序列指令處理:
仔細翻閱了 Arduino IDE 的文件,最後選擇了 Serial.readBytesUntil(char, char *, int) 這個函式來當作序列埠指令讀取的核心。
這個函式需要三個參數:
第一個參數: 資料型態 Char,即俗稱所謂的終端字元。當 RS232 序列埠接收到這個字元時, 即視為一個段落的結束。此時會回傳整個到終端字元間的所有字串資料回來。
第二個參數: 資料型態 Char *,位址指標變數,所以要傳入一個你所建立的字元緩衝區位址,當接受到終端字元時,函式會將整個到終端字元間的所有接收到的字串資料傳入這個你所指定的字元緩衝區位址。
第三個參數: 資料型態 int,一個代表接收長度的整數。當 RS232 序列埠已經接收了這個長度的字元後,依舊看不到終端字元時,依舊會認為段落結束。函式會將整個這個長度所有接收到的字串資料傳入第二個參數你所指定的字元緩衝區位址。
所以
Serial.readBytesUntil('\r',txtbuf,sizeOfBuf); 接收到換行間的字串資料,放入textbuf
字串處理的方式,
第一個程式因爲感測器只有一個,所以只要判斷有無接收到 "temp" 這個指令即可。用 strcmp() 這個函式來做字串的比較,有就做反應傳出溫度值。
txtbuf[0]='\0'; 清掉 txtbuf,避免未收到指令字串而誤判
Serial.readBytesUntil('\r',txtbuf,sizeOfBuf); 接收指令
if (strcmp(txtbuf,cmd)==0) 比較一下,是不是 "temp"
Serial.println(temperature); 是就送出溫度值
第二個程式比較複雜點,因為要判斷 "temp XXX" 的指令,除了關鍵字 "temp" 外,還要能取出 XXX 的感測器索引數字,藉此取出對應的溫度值。
這裡利用 strchar() 這個函式來找尋 "temp XXX" 之間的分隔空白,找到後填入 '\0' 終端字元讓它們切成兩個字串。如此一來就可以分別對兩個字串做處理囉。
處理完畢後因為我們的 txtbuf 文字緩衝區仍然殘留上次的資料,這會影響到下次的判讀。所以用完後利用個小函式 emptystr() 確實地將文字緩衝區填入 '\0' 做清空的動作。
void emptystr(char * strptr, byte n) { 給定 txtbuf 位址跟長度
for(byte i=0; i<n; i++) {
*(strptr+i) = '\0'; 逐一填入 '\0'
}
}
Serial.readBytesUntil('\r',txtbuf,sizeOfBuf);
trimptr = strchr(txtbuf,' '); 找尋空白字元的位置
if (trimptr!=0) { 找到空白字元囉
*trimptr = '\0'; 將空白字元位置填入 '\0'截斷
if (strcmp(txtbuf,cmd)==0) { 比較看看是不是 'temp' 指令
index = atoi(trimptr+1); 是,將空白後面的字串轉換成整數
if (index>=0 && index<MAX_Sensor) { 此整數的範圍檢查
Serial.println(temperature[index]); 都OK的話,送出對應溫度值
}
}
emptystr(txtbuf,sizeOfBuf); 將緩衝區清乾淨
}
測試結果
底下是程式2 測試結果的影片,
依序利用電腦上的序列埠終端機,傳送 "temp 0", "temp 1", "temp 2", "temp 3" 指令給 Arduino UNO。
UNO 根據指令回傳對應感測器的溫度值。當溫度回傳 -127,代表此感測器硬體不存在。
DS
最後,原始程式碼列表
程式1
//
// DS18b20 temperature sensor
// Frank Lin 2021.02.06
//
#include <OneWire.h>
#include <DallasTemperature.h>
#define OneWirePin 6 // one wire pin
#define sizeOfBuf 20 // size of text buf
#define updateInterval 1000 // mS to update temperature
#define cmd "temp" // command for querying temperature
OneWire oneWire(OneWirePin); // create OneWire Obj
DallasTemperature ds18b20(&oneWire); // create DS18B20 Obj
unsigned long prevTime; // prev temperature get time
unsigned long currTime; // current time
void setup() {
Serial.begin(9600);
ds18b20.begin(); // init DS18B20 Object
prevTime = millis(); // setup prevTime first
}
void loop() {
char txtbuf[sizeOfBuf]; // inputed text
float temperature; // current temperature
currTime = millis(); // get current time
if(prevTime > currTime) // if Time counter has blew up, reset it.
prevTime = currTime;
// if it has passed more than updateInterval, get temperature
if(currTime - prevTime >= updateInterval) {
ds18b20.requestTemperatures(); // request DS18B20 for temperature
temperature = ds18b20.getTempCByIndex(0); // get temperature
prevTime = currTime;
}
// check user input and reply result
txtbuf[0]='\0';
Serial.readBytesUntil('\r',txtbuf,sizeOfBuf);
if (strcmp(txtbuf,cmd)==0)
Serial.println(temperature);
}
程式2
//
// DS18b20 temperature sensor array control
// Frank Lin 2021.02.06
//
#include <OneWire.h>
#include <DallasTemperature.h>
#define OneWirePin 6 // 1wire Pin of DS18B20 sensor array
#define MAX_Sensor 8 // max # of DS18B20
#define sizeOfBuf 20 // size of text buf
#define updateInterval 1000 // mS to update temperature
#define cmd "temp" // command for querying temperature
//
// Serial Command format:
// "temp XXX" XXX is the index of Sensor DS18B20
//
OneWire oneWire(OneWirePin); // create 1wire obj
DallasTemperature ds18b20(&oneWire); // create DS18B20 obj
unsigned long prevTime; // prev temperature get time
unsigned long currTime; // current time
// fill '\0' into buffer
void emptystr(char * strptr, byte n) {
for(byte i=0; i<n; i++) {
*(strptr+i) = '\0';
}
}
void setup() {
Serial.begin(9600);
ds18b20.begin(); // init DS18B20 Object
prevTime = millis(); // setup prevTime first
}
void loop() {
float temperature[MAX_Sensor]; // temperature array
int index; // Sensor index number
char txtbuf[sizeOfBuf]; // buffer for inputed text
char * trimptr; // pointer to cut string
currTime = millis(); // get current time
if(prevTime > currTime) // if Time counter has blew up, reset it.
prevTime = currTime;
if(currTime - prevTime >= updateInterval) { // timer up, get temperature
ds18b20.requestTemperatures(); // request DS18B20 for temperature
for (byte i=0;i<MAX_Sensor;i++) { // get data from every DS18B20
temperature[i] = ds18b20.getTempCByIndex(i);
}
prevTime = currTime;
}
// check user input and reply result
txtbuf[0]='\0';
Serial.readBytesUntil('\r',txtbuf,sizeOfBuf);
trimptr = strchr(txtbuf,' '); // search 1st space char ' '
if (trimptr!=0) { // if the space be found
*trimptr = '\0'; // cut the original string
if (strcmp(txtbuf,cmd)==0) { // check command matched?
index = atoi(trimptr+1); // convert to number as index
if (index>=0 && index<MAX_Sensor) { // check index range
Serial.println(temperature[index]); // send out temperature result
}
}
emptystr(txtbuf,sizeOfBuf); // empty input buffer
}
}
Xxx