前言
其實是今天請假在家,因為好不容易預約到可以打武漢肺癌的疫苗囉。在家等待的過程有點無聊,所以坐在電腦桌前玩起 ADXL345 的這個三軸加速度計囉!照著之前這篇部落格的經驗,很快的就完成 Flash FORTH 的版本。測試了一下,運作非常良好,所以做個小小的記錄囉。
ADXL345 三軸加速度計
這是顆非常不錯由 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來不及處理資料時的負擔
- 睡眠模式,偵測到活動時才開啟系統,實現進一步省電的目的。低功耗模式,透過降低取樣率來進一步省電。
我買到的是已經弄好的模組囉,照片
硬體接線
最近對 I2C 學到一個小技巧。大家都知道, I2C 的兩條通訊線 SDA/SCL 一定要上拉電阻來連接的,否則不會動。
但是其實也未必,當網路上只有一台 Master 跟一台 Slave 的時候,是不需要上拉電阻的。只要將雙方的 SDA 對連, SCL對連,這樣就搞定囉。
所以 Ardunio Uno A4 (SDA) 接上 ADXL345 的 SDA, A5 (SCL) 接上 ADXL345 的 SCL, VCC 接上 5V, GND 接地,這樣就搞定啦。
暫存器解說
簡單簡介
暫存器 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)
FORTH 程式碼解說
程式的前半段是 Arduino 的 I2C 控制程式碼,關於 I2C 的程式碼的解說,請參照這篇我的部落格文章,這裡就不贅述囉。
FORTH 的特色,寫完的程式碼可以變成自己的函式庫,這樣再開發新的東西的時候就不用重新來過。這段程式碼已經驗證跟實戰很多次了,在工作上,利用 FORTH 交談性的開發過程。例如,我只要用 scanner 這個字,就可以立刻掃描所有的 I2C裝置,馬上可以驗證 I2C Slave 的程式碼有無錯誤。實戰驗證過,用來 debug 真的非常方便。
這裡,我把原來的 I2C 程式碼繼續延伸如下。在實際的經驗中,大部分 I2C Slave 裝置的溝通方式離不開對它們自己的暫存器做讀或寫的動作。訊息格式大概都是這樣的
1. 給定暫存器位址,讀取單一內容,訊息格式
<Start| <slaveID> |Addr-WR| <register-addr> |Tx| |Restart| <slaveID> |Addr-RD| |Rx-NACK| |Stop>
讓我們把它廣義化成程式碼 regRead ( reg-addr -- dada),這樣以後就可以直接使用囉。
先定義一個變數存 slaveid,所以要使用前記得要初始化這個變數
variable slaveid \ i2c address
剩下其實看圖說故事,照 I2C 的訊息格式寫程式碼,這樣就完成囉。 |Addr-WR| 因為是對 Slave 裝置做點名的動作,可能這個裝置是不存在的。所以後面用 ?Response 做個回應檢查,沒有回應的話它會執行 abort,跳出程式執行。
: regRead ( reg -- data)
<Start|
slaveid @ |Addr-WR| ?Response
|Tx| ( reg)
|Restart|
slaveid @ |Addr-RD|
|Rx-NACK|
|Stop>
;
2. 給定暫存器位址,寫入單一內容,訊息格式
<Start| <slaveID> |Addr-WR| <register-addr> |Tx| <data> |Tx| |Stop>
一樣,看圖說故事,程式碼如下。其實就是在 I2C上連送兩筆資料,第一筆是暫存器位址,第二筆是要給這個暫存器的資料。
: regWrite ( data reg --)
<Start|
slaveid @ |Addr-WR| ?Response
|Tx| ( reg)
|Tx| ( data)
|Stop>
;
3. 給定起始暫存器位址,依序讀取接續每一筆暫存器資料。訊息格式如下,
<Start| <slaveID> |Addr-WR| <register-addr> |Tx| |Restart| <slaveID> |Addr-RD| |Rx-ACK| |Rx-ACK| ... |Rx-ACK| |Rx-NACK| |Stop>
其實就是第一種例子,只是多了個 for-loop 用 |Rx-Ack| 來連續讀取資料,依序放到堆疊上
: regsRead ( reg n -- d1 d2 ... dn)
<Start|
slaveid @ |Addr-WR| ?Response
swap |Tx| ( reg)
|Restart|
slaveid @ |Addr-RD|
1-
for
|Rx-ACK|
next
|Rx-NACK|
|Stop>
;
4. 給定起始暫存器位址,依序接續每一筆暫存器,寫入特定資料。訊息格式如下,
<Start| <slaveID> |Addr-WR| <register-addr> |Tx| <data1> |Tx| ... <dataN> |Tx| |Stop>
其實就是第二種例子,只是多了個 for-loop 來連續將堆疊上的資料,依序傳出給 slave 寫入暫存器中
: regsRead ( reg n -- d1 d2 ... dn)
<Start|
slaveid @ |Addr-WR| ?Response
swap |Tx| ( reg)
|Restart|
slaveid @ |Addr-RD|
1-
for
|Rx-ACK|
next
|Rx-NACK|
|Stop>
;
我們的 ADXL345 是完全符合上面的四種情況的,所以直接可以拿 regRead, regsRead, regWrite, regsWrite 來使用。
剩下的,沒有難度
起始一下一些變數
: adxl345
$53 slaveid ! \ I2C slave address
45 scaleX ! \ factor for mg/LSB ~ (3.5, 4.3)
45 scaleY ! \ factor for mg/LSB ~ (3.5, 4.3)
45 scaleZ ! \ factor for mg/LSB ~ (3.5, 4.3)
;
slaveid 透過 scanner 掃描後是 0x53 ,存入 slaveid 提供 i2c 使用
scaleX, scaleY, scaleZ 是三軸加速度計的 ADC 單位,每個 LSB 約是 3.9 mg 的解析度 (在 2g 的度規下),規格書是說保證介於 3.5 跟 4.3 之間。
實測,在沒校正 offset 的情況下,以 Z軸的重力加速度 1g 的情況下來計算,我手上的這顆約在 4.5 mg/LSB 左右,所以先採用 45來測試。ADXL345 傳來的是 ADC 單位,乘上這個比例常數後才會是 mg 的重力加速度單位。
起始一下 ADXL345
: init345
adxl345 i2cInit
$8 $2d ( POWER_CTL) regWrite \ measure mode
$8 $31 ( DATA_FORMAT) regWrite \ full resolution, right-justify, 2g
$0 $2e ( INT_ENABLE) regWrite \ interrupt disable
$a $2c ( BW_RATE) regWrite \ normal power 100Hz Sampling
$0 $1e ( OFSX) regWrite \ offset for X
$0 $1f ( OFSY) regWrite \ offset for Y
$0 $20 ( OFSZ) regWrite \ offset for Z
;
設好 i2c slaveid,起始一下i2c的硬體。透過I2C傳輸設定一下 ADXL345的 量測模式,資料格式為全解析度,右齊,2g 的度規。關掉中斷模式,正常每秒100次的取樣模式。 X, Y, Z 加速度計的校正補償設為零。
跟 ADXL345 要資料囉
: accRead ( -- X Y Z)
$32 6 regsRead
8 lshift or scaleZ @ * >r
8 lshift or scaleY @ * >r
8 lshift or scaleX @ * r> r>
;
0x32 是加速度X Low Byte 的位址,後面六個 byte依序為 X-Low, X-High, Y-Low, Y-High, Z-Low, Z-High。所以透過 regsRead 一次搞定把全部6個三軸加速度計的所有值的弄到手。
High-byte 在後面,所以 lshift 左移8位元後 or 跟 Low-byte 合併,就是完整的加速度資料了。
因為是 ADC 的數值,所以需要乘上 scaleX, scaleY, scaleZ 後才是重力加速度 g 的單位。就這樣, X, Y, Z 的加速度 get!
再來是列印囉,
測試後 Z 軸的加速度是 226,乘上 scaleZ = 45, 216*45 = 10170 ,這個數值等於 1.0170 g 的重力加速度,所以要列印四位小數且需要正負號。來看看怎麼印啦,
: .acc ( acc ---)
dup s>d dabs <# # # # # [char] . hold #s rot sign #> type
;
先數字 dup 複製一下留個底,因為後面要利用它來看正負號。
然後 s>d 帶符號的單整數轉成雙整數,因為 <# # ... #s #> 這些都是為雙整數數字來設計的。
然後,很重要的因為 <# # #s #> 是針對不帶符號的雙整數來運作的,當數字有帶符號的話,當其是負數的時候會算錯。所以一定要 dabs 取個絕對值後才可以繼續轉換。
四位小數,就是 <# # # # # [char] . hold #s #> ,但是且慢,正負號還沒加上去呀! rot 可以把我們剛剛刻意留下來有符號的單整數翻進來, sign 則會根據堆疊上的單整數的符號,假如是負的,補上 "-"字串。
所以就這樣,搞定四位小數,有帶正負號數字的列印。
最後就是主程式,串連全部
: main
init345
begin
accRead
rot ." X=" .acc ." , "
swap ." Y=" .acc ." , "
." Z=" .acc cr
again
;
起始 345 跟 i2c 後, 無窮迴圈不斷透過 i2c 讀取 X, Y, Z 的加速度後列印。
實際測試影片
運作非常完美,預計程式再改一下,可以利用重力來做水平儀的應用,或是地震儀應該也不錯。
原始程式列表
\ ADXL 345 Test
\ 2021.7.19 Frank Lin
\
marker --i2c--
decimal
\
\ ARduino I2C
\
\ Registers
$b8 constant TWBR
$b9 constant TWSR
$bb constant TWDR
$bc constant TWCR
\ Bits in the Control Register
%10000000 constant mTWINT \ Interrupt Flag
%01000000 constant mTWEA \ Enable Acknowledge
%00100000 constant mTWSTA \ Start Condition
%00010000 constant mTWSTO \ Stop Condition
%00001000 constant mTWWC \ Write Collition
%00000100 constant mTWEN \ Enable
%00000001 constant mTWIE \ Interrupt Enable
variable ACK?
: i2cInit ( -- ) \ Set frequency to 100kHz
%11 TWSR mclr \ prescale = 1
[ Fcy #100 / #16 - 2/ ] literal TWBR c!
mTWEN TWCR mset
;
: DoneWait ( -- ) \ Wait for operation to complete
begin TWCR c@ mTWINT and until
;
: <Start| ( -- ) \ Send start condition
[ mTWINT mTWEN or mTWSTA or ] literal TWCR c!
DoneWait
;
: |Restart| ( -- ) \ Send repeated start
<Start|
;
: |Stop> ( -- ) \ Send stop condition
[ mTWINT mTWEN or mTWSTO or ] literal TWCR c!
;
: |Tx| ( c--) \ send 1 byte to bus
DoneWait
TWDR c!
[ mTWINT mTWEN or ] literal TWCR c!
DoneWait
TWSR c@ $f8 and $18 xor 0= \ SLA + W
if true ACK? ! exit then \ true if ACK
TWSR c@ $f8 and $28 xor 0= \ data byte
if true ACK? ! exit then \ true if ACK
TWSR c@ $f8 and $40 xor 0= \ SLA + R
if true ACK? ! exit then \ true if ACK
false ACK? ! \ false if NACK
;
: |Rx-ACK| ( -- c)
[ mTWINT mTWEN or mTWEA or ] literal TWCR c!
DoneWait TWDR c@
;
: |Rx-NACK| ( -- c)
[ mTWINT mTWEN or ] literal TWCR c!
DoneWait TWDR c@
;
: |Addr-WR| ( 7-bit-addr --) \ ask slave for writing
1 lshift 1 invert and
|Tx|
;
: |Addr-RD| ( 7-bit-addr --) \ ask slve for reading
1 lshift 1 or
|Tx|
;
: ?Response ( --) \ ACK check
ACK? @ abort" I2C: No response from Slave!"
;
: i2cPing? ( 7-bit-addr -- f ) \ ping i2c slave
<Start| |Addr-RD|
ACK? @
if |Rx-NACK| drop true
else false then
;
: scanner ( --) \ scan all i2c slave
base @ hex i2cInit
cr #5 spaces $10 for r@ 2 u.r next
$80
for r@ $0f and $f =
if cr r@ $f0 and #2 u.r [char] : emit space
then
r@ $7 $78 within
if r@ i2cPing?
if r@ #2 u.r
else ." -- " then
else #3 spaces then
next
cr base !
;
\
\ I2C extension
\
variable slaveid \ i2c address
: regWrite ( data reg --)
<Start|
slaveid @ |Addr-WR| ?Response
|Tx| ( reg)
|Tx| ( data)
|Stop>
;
: regRead ( reg -- data)
<Start|
slaveid @ |Addr-WR| ?Response
|Tx| ( reg)
|Restart|
slaveid @ |Addr-RD|
|Rx-NACK|
|Stop>
;
: regsWrite ( d1 d2 d3 ... dn reg n --)
<Start|
slaveid @ |Addr-WR| ?Response
swap |Tx| ( reg)
for
|Tx| ( data)
next
|Stop>
;
: regsRead ( reg n -- d1 d2 ... dn)
<Start|
slaveid @ |Addr-WR| ?Response
swap |Tx| ( reg)
|Restart|
slaveid @ |Addr-RD|
1-
for
|Rx-ACK|
next
|Rx-NACK|
|Stop>
;
marker --adxl345--
\
\ ADXL345 control
\
variable scaleX
variable scaleY
variable scaleZ
: adxl345
$53 slaveid ! \ I2C slave address
45 scaleX ! \ factor for mg/LSB ~ (3.5, 4.3)
45 scaleY ! \ factor for mg/LSB ~ (3.5, 4.3)
45 scaleZ ! \ factor for mg/LSB ~ (3.5, 4.3)
;
: init345
adxl345 i2cInit
$8 $2d ( POWER_CTL) regWrite \ measure mode
$8 $31 ( DATA_FORMAT) regWrite \ full resolution, right-justify, 2g
$0 $2e ( INT_ENABLE) regWrite \ interrupt disable
$a $2c ( BW_RATE) regWrite \ normal power 100Hz Sampling
$0 $1e ( OFSX) regWrite \ offset for X
$0 $1f ( OFSY) regWrite \ offset for Y
$0 $20 ( OFSZ) regWrite \ offset for Z
;
: accRead ( -- X Y Z)
$32 6 regsRead
8 lshift or scaleZ @ * >r
8 lshift or scaleY @ * >r
8 lshift or scaleX @ * r> r>
;
: .acc ( acc ---)
dup s>d dabs <# # # # # [char] . hold #s rot sign #> type
;
: main
init345
begin
accRead
rot ." X=" .acc ." , "
swap ." Y=" .acc ." , "
." Z=" .acc cr
again
;