前言
最近整理電腦時,發現了一年前在 Arduino 上所寫的關於地震儀偵測的程式。當時有個很棒的想法,大家都知道台灣是個多地震的島嶼,每當地震的時候,大家都會在臉書,或是推特上狂推地震推。那時候想說是不是可以用個三軸加速度計,來用個 Arduino 做個地震儀啊。然後地震的時候可以自動感測到地震,當大到某個程度的時候就可以自動幫忙我們發地震推囉。
於是興致勃勃找起可以用的加速度計, 找到一個還不錯的 ADXL 345 三軸加速度計,已經做了一個簡單的原型出來了說。然後因為忙,沒空再繼續下去了。而且我原來的期待是很高的,總覺得沒有一定的完成度不應該把這些東西放到部落格裡面。所以最後這整個小勞作跟一些測試結果就束之高閣囉。
不過一年後再次看到這些之前寫的程式碼,突然覺得雖然只是很粗糙的半成品,但是裡面包含了很多用 Arduino 來做 i2c 溝通的程式碼。覺得假如以 i2c,跟如何控制ADXL345三軸加速度計的這個角度來看,這些經驗還是很值得參考的。
所以決定還是花點時間整理一下公開囉,雖然很粗糙,但對自己或是眾多的 Maker 們以後也是很好的參考資料。
所以這篇,重點會放在 Arduino I2C 的控制,和 ADXL 345 三軸加速度計的粗略溝通和控制。
地震規模的量測
要做地震儀,先了解一下加速度的定義吧。
高中物理, 1g 的加速度等於 9.8 m/s^2 (每公尺/秒平方), 也等於 980 cm/s^2 (每公分/秒平方)
地球科學對地動加速度喜歡用 gal 來表示, 1 gal = 1 cm/s^2 ,所以其實 1g 的加速度等於 980 gal
所以 1 gal = 1/980 g = 0.0010204 g = 1.0204 mg,約略等於 1 mg
底下列出一些常用的地震分級資料
震度分級 | 地動加速度範圍 | 人的感受 | 屋內情形 | 屋外情形 | |
---|---|---|---|---|---|
0 | 無感 | 0.8gal以下 | 人無感覺。 | ||
1 | 微震 | 0.8~2.5gal | 人靜止時可感覺微小搖晃。 | ||
2 | 輕震 | 2.5~8.0gal | 大多數的人可感到搖晃,睡眠中的人有部分會醒來。 | 電燈等懸掛物有小搖晃。 | 靜止的汽車輕輕搖晃,類似卡車經過,但歷時很短。 |
3 | 弱震 | 8~25gal | 幾乎所有的人都感覺搖晃,有的人會有恐懼感。 | 房屋震動,碗盤門窗發出聲音,懸掛物搖擺。 | 靜止的汽車明顯搖動,電線略有搖晃。 |
4 | 中震 | 25~80gal | 有相當程度的恐懼感,部分的人會尋求躲避的地方,睡眠中的人幾乎都會驚醒。 | 房屋搖動甚烈,底座不穩物品傾倒,較重傢俱移動,可能有輕微災害。 | 汽車駕駛人略微有感,電線明顯搖晃,步行中的人也感到搖晃。 |
5 | 強震 | 80~250gal | 大多數人會感到驚嚇恐慌。 | 部分牆壁產生裂痕,重傢俱可能翻倒。 | 汽車駕駛人明顯感覺地震,有些牌坊煙囪傾倒。 |
6 | 烈震 | 250~400gal | 搖晃劇烈以致站立困難。 | 部分建築物受損,重傢俱翻倒,門窗扭曲變形。 | 汽車駕駛人開車困難,出現噴沙噴泥現象。 |
7 | 劇震 | 400gal以上 | 搖晃劇烈以致無法依意志行動。 | 部分建築物受損嚴重或倒塌,幾乎所有傢俱都大幅移位或摔落地面。 | 山崩地裂,鐵軌彎曲,地下管線破壞。 |
由這裡可以看到,想要偵測到地震,加速度計還真的需要非常靈敏才是!
例如想要偵測震幅 2 級以上的地震,加速度計必須要有能力偵測並分辨 2.5 - 8 mg 的加速度的能力。
想要偵測震幅 3 級以上的地震,加速度計必須要有能力偵測並分辨 8 - 25 mg 的加速度的能力。
I2C 介面簡介
I2C 全名 Inter-Integrated Circuit ,簡稱 I square C,是一種串列式的通訊匯流排。由有名的飛利浦 Philips 公司在 1980年代所研發出來,主要鎖定在數位電路板上的數位積體電路跟裝置間,作為低速彼此溝通的一種硬體通訊裝置跟協定。因為這個技術飛利浦申請的專利,所以像 Atmel 等這些公司,為了避開這些專利問體,改稱這個介面叫做 Two Wires Interface 簡稱 TWI ,所以也被以 TWI 的簡稱來稱呼。
I2C 有下列特點:
1. 網路硬體簡單,只需要 串列資料 SDA 及 串列時脈 SCL 兩個訊號線,即可完成通訊。這也是又被稱作叫做 2-Wires Interface 的由來。採用 wire-and 上拉電阻的方式建構序列網路,透過串連的方式將所有裝置連接在一起。
2. 採用主從 (Master-Slave) 的架構來解決匯流排上多裝置同時傳送資料所產生的通訊碰撞問題:網路上只能有一個 Master,所有Slave的網路活動皆由Master號令指揮,Slave只能被動接受 Master 指揮後動作,這樣就不會產生通訊碰撞啦。每個Slave 使用一個 7 bits 的位址來辨識其網路訊號,所以實務上同一個序列網路線上可以同時有多達 127個 slave 來進行通訊。但因為保留了16個位址,所以最多可以有112的slave。
3. 但是和其他如 RS485 Modbus 之類的 Master-Slave 的架構略有不同,雖然說通訊時匯流排內主要只允許有單一Master 對所有的Slave進行指揮運作。但協定依舊透過 wire-and 的網路硬體特性,建立了萬一匯流排中同時出現兩個以上的 Master搶奪 Slave 控制權時的仲裁機制。所以比 Modbus更為先進些,是可以允許多個 Master 同時存在同一個匯流排內。
4. 網路速度分成 standard mode: 100 Kbits/s 及 fast mode: 400 Kbits/sec 兩種。
雖然I2C支援多個 Master,但因為實務上,很少用這樣的方式來運作。最普遍的方式還是像 RS485 Modbus 般,單一 Master 對上多個 Slave。所以後面就要完全忽略掉很少會這樣使用的多重 Master 下的 I2C 使用方式囉。這裏只討論單一 Master 對上多個 Slave。
網路硬體接線
只使用 SCL 和 SDA 兩條線,需接上上拉電阻 Rp 將電位上拉至 +3.3V 或是 +5V!所有網路上的I2C節點接上這兩條線來進行通訊需採用Open Drain的 Digital I/O電路,亦即只有高阻抗跟接地這兩種狀態。當線路上所有的節點都是高阻抗時,因為上拉電阻的關係,所以電位一定是 High。只要有任意節點將電位接地,整個訊號線立即變成 Low。這種利用線路來達到類似 AND Gate 的作用,任一為 Low,結果即為 Low 被稱之為 Wire-AND。
透過 Wire-AND 的方式,任何一個 Slave 只要發現線路變成 Low 了,就知道有人正在使用線路來進行通訊了。也因為如此,未送訊號時是高阻抗,所以如果沒接上拉電阻,電位訊號不正確下 I2C 是無法運作的!
SCL 和 SDA 訊號
SCL 是時脈訊號,規定一律都是 Master 所產生,網路上眾多 Slave 傾聽接收。
SDA 是資料訊號,這個是雙向的,可以是 Master 發出,網路上眾多 Master 接收。或是其中一個 Slave 發出,由 Master 及其他的 Slave 傾聽接收。
透過巧妙的安排 SCL及SDA的時序,可以得到下面四種訊號:
1. Start 訊號:
眾多 Slave,當發現 SCL 還是 High 的狀況下, SDA 突然變成 Low 了,就知道 Master 要開始傳送資料了 S 。這個 Start 訊號只有 Master 可以發送!
2. End 訊號:
眾多 Slave,當發現 SCL 還是 High 的狀況下, SDA 突然變成由 Low 變成 High 了,就知道 Master 要停止傳送資料了 P 。這個 End 訊號只有 Master 可以發送!
3. Data Low 訊號:
在每個 SCL 訊號的脈波為 High 的時候,此時 SDA 線上的 High/Low 訊號為資料。Low 的話為邏輯 Low。可以是 Master 或綠路上被點名到的 Slave 給的!
4. Data High 訊號:
在每個 SCL 訊號的脈波為 High 的時候,此時 SDA 線上的 High/Low 訊號為資料。High 的話為邏輯 Low。可以是 Master 或綠路上被點名到的 Slave 給的!
I2C 單一 Master 下的傳輸方式
先大概列出所有的動作,細節直接用 ADXL345 的規格書來示範
(0) 所有的匯流排動作,都由 Master 來控制。所以沒有 Master 來發號施令,這個I2C匯流排是不會動的。一開始,SCL 跟 SDA 因為上拉電阻的關係,所以都是 High
(1) Master 準備要開始跟某個 Slave 開始交談了,所以先將 SDA 接地拉 Low 製造出 Start 訊號。匯流排上的眾多 Slave 看到這個訊號後,大家開始傾聽SCL/SDA訊號線,等待Master進一步的點名動作!
(2) Start 訊號後, Master 開始照規定的時脈製造出固定的 SCL 脈波出來,讓匯流排上的 Slave 們大家有溝通的依據。然後開始在SDA上傳送 7 bits 的 Slave 位址進行點名的動作,加上 1bit R(1)/W(0) 指令,要求被點到名的 Slave 照這個指令進行讀或寫的動作。傳送完畢後 Master 的 SDA 變高組抗進入傾聽的狀態 (因為上拉電阻,所以SDA回到 High),看看匯流排上的眾多 Slave 有沒有人有回應。
(3) 匯流排上眾多的 Slave,大家收到 Master 來的點名訊號後,比對送來的 7bits 位址,看看是不是要給我的。如果是的話,把 SDA 接地拉 Low 製造 ACK 訊號 (0),同時遵照 1 bit R(1)/W(0) 的指示,準備讀取或寫入。 當 Master 聽到 SDA 訊號被拉 Low (ACK訊號) 時,知道點名成功了。被點名的 Slave 在匯流排上,且接受了 R(1)/W(0) 指令。 如果 Master 沒聽到 ACK 訊號,點名失敗,被點名的 Slave不在匯流排上。所以 Master 傳送 STOP 告知所有 Slave 停止 I2C 通訊。
(4) 點名成功後,看 1 bit R(1)//W(0) 指令是讀還是寫,所謂讀,對象其實是 Master,也就是要求 Slave 送資料給 Master。所謂寫,對象還是 Master,亦即 Master 要送資料給 Slave。
(5) 是寫的話, R/W = 0, Master 進入傳送模式,開啟控制 SDA 送出 8 bits 資料,然後關掉 SDA 進入傾聽模式。而被點到名的 Slave 進入傾聽 SDA 的資料接收模式,接收 SDA 所送過來的 8 bits 資料。 接收完畢後,Slave 覺得 OK 就會在把 SDA 拉 Low 拉 Low 回應 ACK。 Master 監聽到 ACK 後知道一切順利,被允許繼續下一 byte 的傳送。
(6) 是讀的話, R/W = 1, Master 剛剛在點名的時候,SDA早就在傾聽模式囉 (來傾聽ACK),所以持續傾聽被點到名的 Slave 所送來的資料。收到所有資料後 Master 回應 ACK (0) 或 NACK (0) 給 Slave ,告知是否繼續。而 Slave 在收到 R/W =1 時,進入 SDA 傳送模式送出 8 bits 資料,然後進入 SDA 傾聽模式,傾聽 Master 送來的 ACK/NACK 來決定是否繼續傳送下一個 8 bits 的資料。
(7) Master 不管是送完資料給 Slave,或是讀取資料從 Slave。有絕對的控制權來決定是否轉換讀寫方向,還是同方向繼續傳輸交談,還是結束交談。如要結束交談,就NACK傾聽跟發送 STOP 訊號。如要繼續就繼續 ACK傾聽或發送資料。如要轉變傳送方向則再發送一次 Start 加上 7bits 位址加 R/W 指令來要求 Slave 配合新的動作。
ADXL 345 I2C 協定
根據 ADXL 345 的規格書,列出下列這個程式所用到的三個協定
(1) 對單一 ADXL 345 暫存器做寫入的動作
a. 首先 Master 產生 Start 訊號提醒網路上所有的 Slave 注意,Master要來跟大家聯絡了!
b. 接下來是點名的動作,請問有 Slave Address (ADXL 345) 的這個人在嗎?有的話請回應,我 Master 要要求你接下做寫入(接收)資料的動作
c. 這麼多的 Slave 中, 剛好有位真的符合這個 Slave Address (ADXL 345) 的人存在,所以它回了 ACK 告訴 Master ,有,我在!因為是 Master 要求寫入,所以準備接收資料。
d. Master 收到 ACK 訊號,知道有這個 Slave Address (ADXL 345) 的人在,所以放心的傳送了 1 byte 的 Register 位址資料。
e. Slave 很成功地收到了 Register 位址資料,確定無誤後回了 ACK 告知 Master,資料已收到無誤,請繼續!
f. Master 收到 ACK 後知道一切順利,所以繼續傳送 Data 資料
g. Slave 很成功地收到了 Data 資料,確定無誤後回了 ACK 告知 Master,資料已收到無誤,請繼續!
h. Master 發出 STOP 訊號,告知網路上所有 Slave,傳輸結束!
這段對應到的程式碼如下
void reg_set(byte reg, byte data) { Wire.beginTransmission(ADXL345); Wire.write(reg); Wire.write(data); Wire.endTransmission(); }
Wire,beginTransmission(i2c addr) 這個 Arduino IDE 的 I2C 函式會自動幫你處理Master端 a. b. c. 的 Start 跟 Slave 的點名動作
Wire.write(reg). Wire.write(data). 這兩個 Arduino IDE 的 I2C 函式會自動幫你處理Master端 d. e. f. 送出 register 跟 data 的資料.
最後 Wire.endTransmission() 這個 Arduino IDE 的 I2C 函式做個結尾,Master端送出 STOP 訊號
(2) 對單一 ADXL 345 暫存器做讀取的動作
a. 首先 Master 產生 Start 訊號提醒網路上所有的 Slave 注意,Master要來跟大家聯絡了!
b. 接下來是點名的動作,請問有 Slave Address (ADXL 345) 的這個人在嗎?有的話請回應,我 Master 要要求你接下做寫入(接收)資料的動作
c. 這麼多的 Slave 中, 剛好有位真的符合這個 Slave Address (ADXL 345) 的人存在,所以它回了 ACK 告訴 Master ,有,我在!因為是 Master 要求寫入,所以準備接收資料。
d. Master 收到 ACK 訊號,知道有這個 Slave Address (ADXL 345) 的人在,所以放心的傳送了 1 byte 的 Register 位址資料。
e. Slave 很成功地收到了 Register 位址資料,確定無誤後回了 ACK 告知 Master,資料已收到無誤,請繼續!
f. Master 收到 ACK 後知道一切順利,此時再次傳送一次 Start 訊號,這個在 I2C 的術語上,沒有STOP訊號停止過的 Start 被稱之為 Restart,所以 Slave知道接下來會是 Slave Address 跟讀寫的命令。
g. 接下來是Restart 後面的動作要求,請 Slave Address (ADXL 345) 的這個人,我 Master 要求你接下做讀取(送出)資料的動作
h. Slave Address (ADXL 345) 的人回了 ACK 告訴 Master ,有,指令接受!因為是 Master 要求讀取,所以直接接著傳送資料。
i. Master 收到 ACK 訊號,所以也直接跟著接收了從 Slave 來的 1 byte 的 Data 資料。
j. Master 順利接收Data完畢後,發出 NACK 告知不用再送了。
k. Master 發出 STOP 訊號,告知網路上所有 Slave,傳輸結束!
這段對應到的程式碼如下
byte reg_read(byte reg) { Wire.beginTransmission(ADXL345); Wire.write(reg); Wire.endTransmission(false); // false = keep going Wire.requestFrom(ADXL345, 1); while (Wire.available() < 1) { } return Wire.read(); }
Wire,beginTransmission(i2c addr) 這個 Arduino IDE 的 I2C 函式會自動幫你處理Master端 a. b. c. 的 Start 跟 Slave 的點名動作
Wire.write(reg). Wire.write(data). 這兩個 Arduino IDE 的 I2C 函式會自動幫你處理Master端 d. e. 送出 register 的資料.
然後 Wire.endTransmission(false) 這個 Arduino IDE 的 I2C 函式做個結尾,但因為參數 false,所以 Master端繼續控制 I2C傳輸,不送出 STOP 訊號
Wire.requestFrom(ic2 addr, bytes) 是 Master 要求 Slave 讀取(送出)資料的動作,要給 i2c 的位址跟要求 Slave 所送出的 bytes 數目。這個會自動完成對應 f. g. 的動作。會送出 Restart 跟要求i2c addr的 slave,請你做讀取(送資料給 master)的動作。
Arduino IDE 這裡設計了 Available() 用來來檢查 Slave 已經送來多少 Byte 資料了。
所以這裡用個 while-loop 搭配 Wire.available() 來檢查是否資料已經進來了。當 available()>1 就是資料已經讀進來了,這時候透過 Wire.read() 將資料讀出。 對應 h. i. 的動作
Arduino IDE 的 Wire.requestFrom(ic2 addr, bytes) 似乎蠻聰明的,因為已經給了所要讀取的 byte 數目,所以最後它會自動完成對應 j, k 的 NACK 及 STOP 的動作
(3) 對多個 ADXL 345 暫存器做讀取的動作
a. 首先 Master 產生 Start 訊號提醒網路上所有的 Slave 注意,Master要來跟大家聯絡了!
b. 接下來是點名的動作,請問有 Slave Address (ADXL 345) 的這個人在嗎?有的話請回應,我 Master 要要求你接下做寫入(接收)資料的動作
c. 這麼多的 Slave 中, 剛好有位真的符合這個 Slave Address (ADXL 345) 的人存在,所以它回了 ACK 告訴 Master ,有,我在!因為是 Master 要求寫入,所以準備接收資料。
d Master 收到 ACK 訊號,知道有這個 Slave Address (ADXL 345) 的人在,所以放心的傳送了 1 byte 的 Register 位址資料。
e. Slave 很成功地收到了 Register 位址資料,確定無誤後回了 ACK 告知 Master,資料已收到無誤,請繼續!
f. Master 收到 ACK 後知道一切順利,此時再次傳送一次 Start 訊號,這個在 I2C 的術語上,沒有STOP訊號停止過的 Start 被稱之為 Restart,所以 Slave知道接下來會是 Slave Address 跟讀寫的命令。
g. 接下來是Restart 後面的動作要求,請 Slave Address (ADXL 345) 的這個人,我 Master 要求你接下做讀取(送出)資料的動作
h. Slave Address (ADXL 345) 的人回了 ACK 告訴 Master ,有,指令接受!因為是 Master 要求讀取,所以直接接著傳送資料。
i. Master 收到 ACK 訊號,所以也直接跟著接收了從 Slave 來的 1 byte 的 Data 資料。
j. Master 順利接收Data完畢後,發出 ACK 告知請繼續傳送下一個 Byte。
k. Slave 收到 Master 來的 ACK後知道可以繼續傳送,所以繼續送出下一Byte 的資料。在 ADXL345 這裡,就是下一個暫存器的資料。
l. Master 接收完畢後,如果覺得要繼續就送 ACK, 不繼續就送 NACK 準備結束。
m. Master 發出 STOP 訊號,告知網路上所有 Slave,傳輸結束!
這段對應到的程式碼如下
void acc_read() { Wire.beginTransmission(ADXL345); Wire.write(DATAX0); Wire.endTransmission(false); // false = keep going Wire.requestFrom(ADXL345, 6); // ask 6 bytes data while (Wire.available() < 6) { } X_acc = (float) (Wire.read() | Wire.read() << 8)/256; // right-justify Y_acc = (float) (Wire.read() | Wire.read() << 8)/256; // 1 word data Z_acc = (float) (Wire.read() | Wire.read() << 8)/256; }
Wire,beginTransmission(i2c addr) 這個 Arduino IDE 的 I2C 函式會自動幫你處理Master端 a. b. c. 的 Start 跟 Slave 的點名動作
Wire.write(reg). Wire.write(data). 這兩個 Arduino IDE 的 I2C 函式會自動幫你處理Master端 d. e. 送出 register 的資料.
然後 Wire.endTransmission(false) 這個 Arduino IDE 的 I2C 函式做個結尾,但因為參數 false,所以 Master端繼續控制 I2C傳輸,不送出 STOP 訊號
Wire.requestFrom(ic2 addr, bytes) 是 Master 要求 Slave 讀取(送出)資料的動作,要給 i2c 的位址跟要求 Slave 所送出的 bytes 數目。這個會自動完成對應 f. g. 的動作。會送出 Restart 跟要求i2c addr的 slave,請你做讀取(送資料給 master)的動作。
所以這裡用個 while-loop 搭配 Wire.available() 來檢查是否6個 bytes 的資料已經全部進來了。當 availabe()>=6 時就是6個 bytes 的資料已經都讀進來了,這時候透過 Wire.read() 將資料讀出。
這裏要抱怨一下, Arduino IDE 的文件在這個地方寫得很不清楚,這樣的程式碼是試出來的。猜測 IDE 的 Wire.requestFrom(ic2 addr, bytes) 指令會自動完成對應 h. i. 的動作來收資料放到自己的 buffer中,等待使用者用 Wire.read() 將資料收走。
最後 Wire.requestFrom(ic2 addr, bytes) 自動完成對應 j, k 的 NACK 及 STOP 的動作
ADXL 345 三軸加速度計
這是顆非常不錯由 Analog Device 所開發的低功耗三軸加速度計,其特點如下
- 體積小巧纖薄,非常適合行動裝備應用
- 高達 +/-16 g 的加速度高分辨率 (13位) 測量,數字採用 1 word (2 bytes) 帶正負符號的整數
- 提供 X, Y, Z 三軸加速度 Offset 補償功能,可以直接內部進行數值補償校正
- 高解析度, 4 mg/LSB ,利用重力能夠量測 0.25度 的傾角變化。可以用在量測運動或衝擊導致的動態加速度,也可用在傾斜檢測應用中量測靜態重力加速度
- 支援 SPI 3線或4線通訊,或是 I2C通訊
- 內建活動/非活動檢測,可直接檢測是否加速度計從靜止被移動,無需外部MCU的協助
- 內建自由落體檢測,可直接檢測是否加速度計從空中摔落,無需外部MCU的協助
- 內建可定義的單擊/雙擊動作檢測,可監測使用者的動作,無需外部MCU的協助
- 內建 32級 FIFO 緩衝暫存區,減少外部MCU來不及處理資料時的負擔
- 睡眠模式,偵測到活動時才開啟系統,實現進一步省電的目的。低功耗模式,透過降低取樣率來進一步省電。
我買到的是已經弄好的模組囉,照片
硬體接線
整個接線很簡單,如下所示。
因為 ADXL 345 有 SPI ,今天我們沒有要用 SPI , 所以 CS 要接 High (5V), SDO 接 GND
兩個中斷 INT1, INT2 沒有使用,所以不接。
Arduino 的 SCL (A5) 和 ADXL 345 的 SCL 接在一起,然後透過上拉電阻接到 Vcc (+5V)
Arduino 的 SDA (A4) 和 ADXL 345 的 SDA 接在一起,一樣透過上拉電阻接到 Vcc (+5V)
對 I2C 匯流排而言,上拉電阻的大小其實是蠻關鍵的。太大太小都會造成訊號失真而無法通訊,或是通訊不穩定。這裏其實試了好久,才找到 5.1K ohm 的這個阻值,可以穩定通訊。
實際照片,上面是空白 Arduino Uno Shield。下面是 Arduino UNO,很方便就組起來囉!
再一張正面照
暫存器功能解說
這顆真的是顆功能非常齊全的三軸加速度計,因為功能多,所以有點複雜。但這裡的需求是要做個地震偵測的地震儀,為了彈性,所以不使用太多功能,而是很單純的要求加速度計即時的將所有的X, Y, Z 的加速度透過 I2C 傳給 Arduino ,而所有的判斷由 Arduino 來決定,所以最後只用到下列少少的暫存器。
解說如下: (按我連接至官方 ADXL 345 規格書)
暫存器 0x2D - POWER_CTL
這個用來控制 ADXL345 的模式選擇
我們的應用這裡很單純的,就是 D3 設成 1 ,讓系統維持在 Measure 模式隨時量測所有的加速度。 所以就是 0x8 的資料。
暫存器 0x31 - DATA_FORMAT
這個暫存器用來控制加速度計數字輸出的格式,
這裡重要的是
D3: FULL_RES 設成1的話,會用最大解析度 4mg/LSB 來量測,然後 Scale 會手動固定由 D1.D0 的 Range 來決定。
D2: Justify, 設成0的話為 Right-Justify,經過測試,此時正負號會正常在 High Byte 的 MSB。
D1.D0 為手動 Range 選擇,
00 - +/-2g 的 Range 這裡希望最高解析度來偵測最小的震動,所以選用 +/-2g 最小的偵測範圍。
01 - +/-4g 的 Range
10 - +/-8g 的 Range
11 - +/-16g 的 Range
所以總結,這個暫存器需要設成 0x8 的資料。
暫存器 0x2E - INT_ENABLE
各種中斷訊號的致能設定,
而這裡沒有要使用任何的硬體中斷訊號來觸發,而是單純使用 I2C 來溝通。所以這個暫存器需要設成 0x0 的資料。
暫存器 0x2C - BW_RATE
重點是 D3.D2.D1.D0 這四個位元,這四個位元控制了加速度計其值取樣的速度。
在正常模式下,其控制的取樣頻率如下表, D3.D2.D1.D0 = Rate Code
這裡因為只是測試,所以先選擇 ADXL345 的預設值 Rate Code = %1010 = 0x0A 為 100Hz ,每秒100次的資料輸出標準設定。
接下來不用說,就是加速度 X, Y, Z 的資料暫存器了
0x32 X-Axis Data 0 (Low Byte)
0x33 X-Axis Data 1 (High Byte)
0x34 Y-Axis Data 0 (Low Byte)
0x35 Y-Axis Data 1 (High Byte)
0x36 Z-Axis Data 0 (Low Byte)
0x37 Z-Axis Data 1 (High Byte)
因為這幾個暫存器的位址是連續的,所以這裡為了減少資料傳輸的負擔,在 I2C 的傳輸時採用一次傳輸多 bytes 的 I2C協定。
所以傳送從 0x32 開始,Master 要求 Slave 依序傳送 6 bytes 過來,所有的資料就進來囉。
最後是
0x1E OFSX X-axis Offset
0x1F OFSY Y-axis Offset
0x20 OFSZ Z-axis Offset
這三個是加速度計 Offset 補償值的暫存器。是個 8位元有正負號的數值,可正負補償 +/-127 個單位,單位視 Range 而決定。
當是 +/-2g的 Range 時,其單位為 15.6 mg/LSB (0x7F = +2g)
程式解說
程式主要是 I2C 溝通取值,跟資料處理地震判斷這兩部分
I2C 溝通取值,主要靠三個函式
reg_set(byte reg, byte data) 給定 ADXL345 暫存器的位址 跟 資料,送給 ADXL345 要求設定
reg_read(byte reg) 給定 ADXL345 暫存器的位址,然後讀出它的內容
acc_read() 透過連續讀出 0x32 X加速度值後面的 6 byes,把三軸加速度全部讀出來,放到變數 X_acc, Y_acc, Z_acc 中
這三個函式,都是透過 I2C 協定運作的,程式碼前面有解說過了,這裏就不贅述囉。
唯一可以解說的是加速度數值轉換這段,
就先讀到的是 Low Byte, 後讀到的是 High Byte, High Byte 左移 8 bits 後透過 or 合併起來,最後 (float) 強迫轉型到浮點數方便後面的運算
X_acc = (float) (Wire.read() | Wire.read() << 8)/256; // right-justify Y_acc = (float) (Wire.read() | Wire.read() << 8)/256; // 1 word data Z_acc = (float) (Wire.read() | Wire.read() << 8)/256;
資料處理運算,主要是利用一個加權平均來取得一個穩定的加速度平均值。當最新的加速度值偏離此值到某一個程度後代表地震發生,對外發出已偵測到地震發生的訊號。
加速度平均值,每次用 1/20 的加權平均來加入最新的值。這樣只要加速度計靜止時,加速度的值就會逐漸收斂的一個值。只要最新的值偏離這個收斂的值太大,就知道有新的不穩定震動發生了(地震)。
判斷的方式採用 每一軸向的加速度最新的值跟平均值相減,超過 0.04 就判斷為地震。
這個方式有缺陷,因為只比較 X, Y, Z 軸方向的加速度值。真實地震除了上下震動是Z軸外,其他的X-Y平面震動是隨意方向的,所以應該比較的是加速度向量的大小 sqrt(accX^2 + accY^2 + accZ^2) ,這樣才是忠實感測所有的震動方向。
不過只是測試而已,可以動就好囉! 😜
void loop() { acc_read(); if (abs(X_avg-X_acc)>0.04 || abs(Y_avg-Y_acc)>0.04 || abs(Z_avg-Z_acc)>0.04) { Serial.println(" E A R T H Q U A R K Detected!"); } else { X_avg = (X_acc + 19*X_avg)/20; Y_avg = (Y_acc + 19*Y_avg)/20; Z_avg = (Z_acc + 19*Z_avg)/20; }
執行狀況
原始程式碼列表
// // ADXL345 Test // 2019.08.31 Frank Lin // // #include <Wire.h> #define ADXL345 0x53 // I2C address #define POWER_CTL 0x2D // Power-Saving features control #define DATA_FORMAT 0x31 // Data Format Control #define INT_ENABLE 0x2E // interrupt enable control #define BW_RATE 0x2C // Data Rate and power mode control #define DATAX0 0x32 // X-axis data0, all acceleration data start from here. #define OFSX 0x1E // X-axis offset #define OFSY 0x1F // Y-axis offset #define OFSZ 0x20 // Z-axis offset float X_acc, Y_acc, Z_acc; // X, Y, Z acceleration values float X_avg, Y_avg, Z_avg; // X, Y, Z acceleration values, moving average void reg_set(byte reg, byte data) { Wire.beginTransmission(ADXL345); Wire.write(reg); Wire.write(data); Wire.endTransmission(); } byte reg_read(byte reg) { Wire.beginTransmission(ADXL345); Wire.write(reg); Wire.endTransmission(false); // false = keep going Wire.requestFrom(ADXL345, 1); while (Wire.available() < 1) { } return Wire.read(); } void dump() { for (byte i=0x1D; i<= 0x39; i++) { Serial.print("reg: 0x"); Serial.print(i, HEX); Serial.print(" = 0x"); Serial.println(reg_read(i), HEX); } } void acc_read() { Wire.beginTransmission(ADXL345); Wire.write(DATAX0); Wire.endTransmission(false); // false = keep going Wire.requestFrom(ADXL345, 6); // ask 6 bytes data while (Wire.available() < 6) { } X_acc = (float) (Wire.read() | Wire.read() << 8)/256; // left-justify Y_acc = (float) (Wire.read() | Wire.read() << 8)/256; // 1 word data Z_acc = (float) (Wire.read() | Wire.read() << 8)/256; } void setup() { // Initialize Serial, I2C Serial.begin(9600); Wire.begin(); Serial.println(); Serial.println("== Registers Data after Powerup... =="); Serial.println(); dump(); // setup registers reg_set(POWER_CTL,0x8); // measurement mode reg_set(DATA_FORMAT,0x8); // set to Full-Resolution, Right-Justify, 2g reg_set(INT_ENABLE,0x0); // interrupts enable/disable reg_set(BW_RATE,0x0A); // normal power, 100Hz sampling reg_set(OFSX,0x0); // offsets for accel values reg_set(OFSY,0x0); reg_set(OFSZ,0x0); Serial.println(); Serial.println("== Registers Data after Setup... =="); Serial.println(); dump(); delay(1000); acc_read(); X_avg = X_acc; Y_avg = Y_acc; Z_avg = Z_acc; for (byte i=0; i<10; i++) { acc_read(); X_avg = (X_acc + 9*X_avg)/10; Y_avg = (Y_acc + 9*Y_avg)/10; Z_avg = (Z_acc + 9*Z_avg)/10; } Serial.println(" Monitoring..."); } void loop() { acc_read(); if (abs(X_avg-X_acc)>0.04 || abs(Y_avg-Y_acc)>0.04 || abs(Z_avg-Z_acc)>0.04) { Serial.println(" E A R T H Q U A R K Detected!"); } else { X_avg = (X_acc + 19*X_avg)/20; Y_avg = (Y_acc + 19*Y_avg)/20; Z_avg = (Z_acc + 19*Z_avg)/20; } Serial.println(); Serial.print("Xavg= "); Serial.print(X_avg); Serial.print(" Yavg= "); Serial.print(Y_avg); Serial.print(" Zavg= "); Serial.println(Z_avg); }
xxx
留言列表