前言
來替 Arduino Uno上面的 Flash Forth 打通 I2C 的任督二脈吧。這個 I2C Bus 真的很重要,一但打通後就會很方便用 Flash FORTH 來做跟任何 I2C 裝置的溝通跟控制的應用囉。所以把這些經驗跟程式碼完整放在這個部落文囉。
然後 I2C 裝置裡面,最容易操作的就是 DS1307 這個 RTC 即時時鐘模組囉。所以我們用它來測試我們的 I2C 程式碼,順便來實作一下 ANSI FORTH 裡面的一個用來取得系統日期跟時間的 FORTH 指令 time&date。
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 下跟 Slave 的傳輸方式
先大概列出所有IC2匯流排的動作,細節直接用 DS1307 來示範
(0) 所有的匯流排動作,都由 Master 來控制。所以沒有 Master 來發號施令,這個I2C匯流排是不會動的。一開始,SCL 跟 SDA 因為上拉電阻的關係,所以都是 High
(1) 當 Master 準備要開始跟某個 Slave 開始交談了,規定先將 SDA 接地拉 Low 製造出 Start 訊號。匯流排上的眾多 Slave 看到這個訊號後,大家開始傾聽SCL/SDA訊號線,等待Master進一步的點名動作!
這裡我們用 <Start| 來代表這個事件。
(2) Start 訊號後, Master 開始照規定的時脈製造出固定的 SCL 脈波出來,讓匯流排上的 Slave 們大家有溝通的依據。然後開始在SDA上傳送 7 bits 的 Slave 位址進行點名的動作,加上 1bit R(1)/W(0) 指令,要求被點到名的 Slave 照這個指令進行讀或寫的動作。
傳送完畢後 Master 的 SDA 變高組抗進入傾聽的狀態 (因為上拉電阻,所以SDA回到 High),看看匯流排上的眾多 Slave 有沒有人有回應。
這裡我們用 |Addr-WR| 來表示對 Slave 點名,且要求 Slave 寫入資料。
|Addr-RD| 來表示對 Slave 點名,且要求對 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 通訊。
這裡我們用 |Stop> 來代表 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 的傳送。
這裡我們用 |Tx| 來代表 Master 送資料給 Slave。
(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 的資料。
這裡我們用 |Rx-ACK| 來代表 Master 讀取了 Slave 所傳來的資料,並回應了ACK訊號要求 Slave 繼續送下一筆資料。
|Rx-NACK| 來代表 Master 讀取了 Slave 所傳來的資料,並回應了NACK訊號要求 Slave 可以不用再送資料囉。
(7) Master 不管是送完資料給 Slave,或是讀取資料從 Slave。有絕對的控制權來決定是否轉換讀寫方向,還是同方向繼續傳輸交談,還是結束交談。
如要結束交談,就NACK傾聽跟發送 STOP 訊號。如要繼續就繼續 ACK傾聽或發送資料。如要轉變傳送方向則再發送一次 Start 加上 7bits 位址加 R/W 指令來要求 Slave 配合新的動作。
這裡我們用 |Restart| 來代表 Master 要求 Slave 接受新的傳送指令。接續後的 |Addr-WR| 代表要求 Slave 接收 Master 的資料傳送。 |Addr-RD| 代表要求 Slave 將資料傳送給 Master。
RTC DS1307 I2C 協定
根據上面的解說後,翻開 DS1307 的規格書吧,來看看它的 I2C 通訊細節為何?
1. 資料從 Master 寫入 Slave
這裡用我們的符號翻譯一下就是
<Start| |Addr-WR| |Tx| |Tx| ... |Tx| |Stop>
Master 先送個 Start 訊號,然後 位址訊號-寫入 來點名 Slave 存在否並要求接收資料,然後 Master 依序地送出要寫入的資料:第一個是要寫入DS1307暫存器的起始位址,第二個之後是依序要寫入暫存器的資料,最後送出 Stop 訊號結束所有 I2C 交談。
2. Slave 讀取資料,回傳給 Master
用我們的符號翻譯一下就是
<Start| |Addr-RD| |Rx-ACK| |Rx-ACK| ... |Rx-ACK| |Rx-NACK| |Stop>
Master 先送個 Start 訊號,然後 位址訊號-讀取 來點名 Slave 存在否並要求發送資料,然後 Master 依序地讀取從 Slave 所送出來的資料,之後回應 ACK,要求 Slave 繼續送下一筆資料。 最後一筆的時候, Master 回應 NACK 告訴 Slave 可以不用送了,最後送出 Stop 訊號結束所有 I2C 交談。
3. Master 先指定Slave 暫存器的起始位址,然後 Slave 讀取暫存器裡的資料回傳給 Master
用我們的符號翻譯一下就是
<Start| |Addr-WR| |Tx| |Restart| |Addr-RD| |Rx-ACK| |Rx-ACK| ... |Rx-NACK| |Stop>
Master 先送個 Start 訊號,然後 位址訊號-寫入 來點名 Slave 存在否並要求接收資料,然後 Master 對 Slave 傳送暫存器位址。接下來 Master 傳送 Restart 訊號,要求 Slave 接受新的傳送指令, 位址訊號-讀取 來點名 Slave 並要求要求發送資料。 隨後 Master 依序地讀取從 Slave 所送出來的暫存器內部資料,之後回應 ACK,要求 Slave 繼續送下一筆資料。 最後一筆的時候, Master 回應 NACK 告訴 Slave 可以不用送了,最後送出 Stop 訊號結束所有 I2C 交談。
RTC DS1307 暫存器及控制
根據 DS1307 這個即時時鐘的規格書,所有暫存器如下表所示
位址0: 以 BCD-8421 的格式存放秒數資訊。 BIT 7 - CH 用來控制即時時鐘是否開始運作。 1 代表停止計數,0代表開始計數,出廠預設值為1。
位址1: 以 BCD-8421 的格式存放分鐘資訊。
位址2: 以 BCD-8421 的格式存放小時資訊。BIT 6 用來選擇 12小時制還是 24小時制。當BIT 6 = 1 的時候是 12小時制,這時候 BIT 5 拿來當作 AM/PM, BIT 5 = 1 的時候是 PM。當 BIT 6 = 0 的時候是24小時制,此時 BIT5 剛好當作 BCD 的十進位數。
位址3: 以 BCD-8421 的格式存放星期資訊。1=星期一,2=星期二,... 6=星期六,7=星期日。
位址4: 以 BCD-8421 的格式存放日期資訊。
位址5: 以 BCD-8421 的格式存放月份資訊。
位址6: 以 BCD-8421 的格式存放年份資訊。因為只能有兩位數: 0 -99,所以只能從 2000 - 2099 年。
位址7: 用來控制脈波輸出及其頻率,假如你的電路需要用到的話。
所謂 BCD-8421 格式,就是「十六進制的十進制」。我們一般知道所謂十六進制就是從0數到15,就是 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F。
「十六進制的十進制」就是犧牲 A, B, C, D, E, F 把四個 bit 當做一個10進制來用。所以一個 Byte 只能顯示 00 到 99 兩位數字。這種格式有趣的地方是,你把它轉換成16進制,但實際看到的卻是人類比較熟悉的十進制數字。
這幾個暫存器,當 位址0 的BIT 7 = 1 的時候,計時器就會開始運作。暫存器裡的資料就會以即時時鐘的方式不斷變化著。使用者只要透過 I2C 詢問這些暫存器內部的內容,即可得知當前精確的日期跟時間。
FORTH 語法設計
眾所皆知,FORTH語言主要是用堆疊來傳遞訊息的。這樣的方式在語法的設計上有很大的好處,因為數據的傳遞變成是隱含的,所以我們可以設計出可讀性非常高的語法。
我們可以直接套用我們前面所設計的速記符號,所以 I2C 指令設計如下:
i2cInit ( --) 起始一下 Arduino 的 I2C 介面,設定運作頻率為 100K
<Start| ( --) 於匯流排上發送 Start 訊號,要求所有 Slave 注意聆聽
|Addr-WR| ( i2c-addr --) 點名 i2c-addr 位址的 Slave 裝置,並要求準備接收從 Master 來的資料
|Addr-RD| ( i2c-addr --) 點名 i2c-addr 位址的 Slave 裝置,並要求傳送資料給 Master
|Tx| ( data --) Master 從匯流排上傳送 data 給 Slave
|Rx-ACK| ( -- data) Master 從匯流排上接收從 Slave傳來的資料 data,收到資料後回應 ACK 要求繼續傳送下一筆資料。
|Rx-NACK| ( -- data) Master 從匯流排上接收從 Slave傳來的資料 data,收到資料後回應 NACK 要求停止傳送。
|Restart| ( --) 於匯流排上再次發送 Start 訊號,用來改變 Slave 的傳輸模式。
|Stop> ( --) 於匯流排上發送 Stop 訊號,談話結束
?Response ( --) 用來檢查 ACK 是否有回送,沒有就 Abort 掉整個指令。通常用來做第一次的檢查,是否點名到的 Slave 有回應?
ACK? 變數 ,當 Slave 回傳 ACK 時會其結果存在這個變數中,使用者有需要的話可以檢查來決定要如何處理。
所以根據 DS1307 的 I2C 協定,要對一個暫存器寫值的程式碼如下
: regWrite ( data reg --) 堆疊上給定資料 data 跟 位址 reg
<Start| 傳送 Start訊號,要求大家注意
ds1307 |Addr-WR| ?Response 對ds1307點名並要求接收資料,點不到名就Abort
|Tx| 傳送暫存器位址 reg
|Tx| 傳送對暫存器寫入的資料 data
|Stop> 傳送 Stop訊號,結束交談。
;
對 DS1307 一個暫存器讀值的程式碼如下
: regRead ( reg -- data) 堆疊上給定暫存器位址 reg, 回傳 data
<Start| 傳送 Start訊號,要求大家注意
ds1307 |Addr-WR| ?Response 對ds1307點名並要求接收資料,點不到名就Abort
|Tx| 傳送暫存器位址 reg
|Restart| 傳送 Restart訊號,要求更改通訊方向
ds1307 |Addr-RD| 對ds1307點名並要求傳送資料,
|Rx-NACK| 接收1307所送的資料,並回應NACK要求結束
|Stop> 傳送 Stop訊號,結束交談。
;
對 DS1307 多個暫存器寫值的程式碼如下,簡單用個 For Loop 就可以達成
: regsWrite ( d1 d2 d3 ... dn reg n --) 給定資料d1..dn,暫存器位址reg,及資料數n
<Start| 傳送 Start訊號,要求大家注意
ds1307 |Addr-WR| ?Response 對ds1307點名並要求接收資料,點不到名就Abort
swap |Tx| 傳送暫存器位址 reg
for for-loop
|Tx| 傳送資料 dn
next
|Stop> 傳送 Stop訊號,結束交談。
;
對 DS1307 多個暫存器讀值的程式碼如下,簡單用個 For Loop 就可以達成
: regsRead ( reg n -- d1 d2 ... dn) 給定暫存器位址reg,及資料數n,回傳所有資料
<Start| 傳送 Start訊號,要求大家注意
ds1307 |Addr-WR| ?Response 對ds1307點名並要求接收資料,點不到名就Abort
swap |Tx| 傳送暫存器位址 reg
|Restart| 傳送 Restart訊號,要求更改通訊方向
ds1307 |Addr-RD| 對ds1307點名並要求傳送資料,
1- 最後一個單獨送,所以減1
for for-loop
|Rx-ACK| 接收1307所送的資料,並回應ACK要求繼續
next
|Rx-NACK| 接收1307所送的資料,並回應NACK要求結束
|Stop> 傳送 Stop訊號,結束交談。
;
同樣的,可以寫一個 I2C 的 Ping 指令,來點名是否有特定位址的 I2C裝置正掛在匯流排上
: i2cPing? ( 7-bit-addr -- f ) 給定Slave位址,回傳true代表存在
<Start| |Addr-RD| 傳送 Start訊號,要求大家注意,然後點名
ACK? @ 取出 ACK? 變數裡的回應
if |Rx-NACK| drop true true 代表存在,回應NACK結束傳送
else false then false 代表不存在
;
程式碼簡介
透過 i2cPing? 的這個指令,寫一個可以掃描整個 I2C 匯流排上,有哪些 Slave 存在的指令。這個會十分的方便用來測試 I2C 的通訊。
: scanner ( --) \ scan all i2c slave
base @ hex i2cInit 起始一下 Arduino Uno 的I2C匯流排,頻率 100K
cr #5 spaces $10 for r@ 2 u.r next
$80 位址掃描,從 0x7F (127) 到 0x0 (0)
for r@ $0f and $f = 判斷是不是邊界,是的話印一下邊邊
if cr r@ $f0 and #2 u.r [char] : emit space
then
r@ $7 $78 within
if r@ i2cPing? Ping 看看 Slave 存在否
if r@ #2 u.r 是就印出來
else ." -- " then
else #3 spaces then
next
cr base !
;
BCD 轉整數的指令,「16進制的十進制」,所以用16來取商數跟餘數,商數乘十跟餘數相加就是囉。
: bcd>int ( bcd -- int)
16 u/mod 10 * +
;
整數轉 BCD 的指令,倒過來,「十進制的16進制」,所以用10來取商數跟餘數,左移4位(乘上16),相加(or) 就是囉。
: int>bcd ( int -- bcd)
10 u/mod 4 lshift or
;
讀取 DS1307日期,星期,時間資料
: time&date&week ( -- sec min hr day date month year)
i2cInit 起始 Arduino I2C
0 7 regsRead 從暫存器0開始,一共讀7個暫存器內的資料
>r >r >r >r >r >r 推到返回堆疊暫存一下
bcd>int ( sec) 將秒的BCD格式轉成整數
r> bcd>int ( min) 將分的BCD格式轉成整數
r> bcd>int ( hr) 將小時的BCD格式轉成整數
r> bcd>int ( day) 將星期的BCD格式轉成整數
r> bcd>int ( date) 將日期的BCD格式轉成整數
r> bcd>int ( month)將月份的BCD格式轉成整數
r> bcd>int ( year) 將年份的BCD格式轉成整數
2000 + 將年份加上2000
;
設定 DS1307日期,星期,時間資料
: time&date! ( year month date day hr min sec --)
>r >r >r >r >r >r 推到返回堆疊暫存一下
2000 - int>bcd ( year) 將年份的格式轉成BCD
r> int>bcd ( month)將月份的格式轉成BCD
r> int>bcd ( date)將日期的格式轉成BCD
r> int>bcd ( day)將星期的格式轉成BCD
r> int>bcd ( hr)將小時的格式轉成BCD
%00111111 and 確保使用24小時制
r> int>bcd ( min)將分鐘的格式轉成BCD
r> int>bcd ( sec)將秒數的格式轉成BCD
%10000000 or 確保時鐘立即執行計數
i2cInit 起始 Arduino I2C
0 7 regsWrite 從暫存器0開始,將資料寫入7個暫存器
;
另外要說明的, 標準 I2C 的傳輸速率是 100 kHz。所以每傳送 1 bit 資料所需的時間是 1 sec / 100kHz = 10 uS
我們的 Arduino Uno 的時鐘頻率是 25MHz,算是蠻快速的。所以每個指令機械週期 Cycle 所需的時間是 1 sec / 25MHz = 0.04 uS.
所以 I2C 每傳送一個 bit , Atmega328 微處理器所容許執行的機械週期數目為 10uS / 0.04uS = 250 Cycles
大部分 Atmega328 的指令都是一兩個機械週期就可以行完畢的, 250 cycles 夠塞入很多指令囉。所以直接用程式碼來控制整個 I2C 的流程並包含一些計算,只要不太複雜,是還來得及的。
硬體接線
我是直接購買 Arduino SD/RTC 紀錄模組,所以配線都不需要囉。直接把這個 Shield 掛上 Arduino Uno 就可以囉。模組裡面的 RTC DS1307 當然已經直接接上 Arduino Uno 的 I2C 匯流排囉。所以立即可以使用。
執行結果
先是用 scanner 這個指令來掃描一下,這個 DS1307 RTC 的 I2C 位址吧。掃描的結果,位址 0x68,且 I2C 的通訊正常無誤。
操作影片
再來就是來跟 DS1307 溝通囉。
DS1307 出廠預設值,暫存器0 裡面的 CH BIT = 1,亦即停止計時運作。除非我們將這個 CH BIT 設成0,這樣計時才會正式開始。
來利用 time&date! 指令來設定目前時間並啟動計時器吧!
鍵入 2021 2 28 7 9 37 0 time&date! 來設定時鐘時間至 2021年2月28日, 星期7(日), 9點37分00秒,並開始計時
隨後只要隨時鍵入 time&date 指令,就立刻可以得到目前最正確的時間訊息囉。
這裡利用 .time&date 來列印跟測試結果。結果正確運作無誤。
操作影片
最後,原始程式碼列表
\
\ Arduino I2C
\ 2021.2.22 Frank Lin
\
--i2c--
marker --i2c--
decimal ram
\ 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 !
;
decimal
\
\ RTC DS1307 Control
\ 2021.2.22 Frank Lin
\
--ds1307--
marker --ds1307--
$68 constant ds1307 ( i2c address)
: regWrite ( data reg --)
<Start|
ds1307 |Addr-WR| ?Response
|Tx| ( reg)
|Tx| ( data)
|Stop>
;
: regRead ( reg -- data)
<Start|
ds1307 |Addr-WR| ?Response
|Tx| ( reg)
|Restart|
ds1307 |Addr-RD| |Rx-NACK| |Stop>
;
: regsWrite ( d1 d2 d3 ... dn reg n --)
<Start|
ds1307 |Addr-WR| ?Response
swap |Tx| ( reg)
for
|Tx| ( data)
next
|Stop>
;
: regsRead ( reg n -- d1 d2 ... dn)
<Start|
ds1307 |Addr-WR| ?Response
swap |Tx| ( reg)
|Restart| ds1307 |Addr-RD|
1-
for
|Rx-ACK|
next
|Rx-NACK| |Stop>
;
: bcd>int ( bcd -- int)
16 u/mod 10 * +
;
: int>bcd ( int -- bcd)
10 u/mod 4 lshift or
;
: time&date&week ( -- sec min hr day date month year)
i2cInit
0 7 regsRead
>r >r >r >r >r >r
bcd>int ( sec)
r> bcd>int ( min)
r> bcd>int ( hr)
r> bcd>int ( day)
r> bcd>int ( date)
r> bcd>int ( month)
r> bcd>int ( year)
2000 +
;
: time&date ( -- sec min hr date month year)
time&date&week
>r rot drop r>
;
: time&date! ( year month date day hr min sec --)
>r >r >r >r >r >r
2000 - int>bcd ( year)
r> int>bcd ( month)
r> int>bcd ( date)
r> int>bcd ( day)
r> int>bcd ( hr)
%00111111 and
r> int>bcd ( min)
r> int>bcd ( sec)
%10000000 or
i2cInit
0 7 regsWrite
;
: .time&date
time&date
cr 4 u.r [char] / emit 2 u.r
[char] / emit 2 u.r
." - "
2 u.r [char] : emit 2 u.r [char] : emit 2 u.r
;
留言列表