前言

來替 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 的通訊正常無誤。

 

操作影片

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Frank Lin(@ohiyooo)分享的貼文

 

再來就是來跟 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 來列印跟測試結果。結果正確運作無誤。

 

操作影片

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Frank Lin(@ohiyooo)分享的貼文

 

 

 

最後,原始程式碼列表

 


\  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    

 

 

文章標籤
全站熱搜
創作者介紹
創作者 ohiyooo2 的頭像
ohiyooo2

早安,苦命工程師的胡言亂語

ohiyooo2 發表在 痞客邦 留言(0) 人氣(877)