前言

來替 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 是無法運作的!

i2c hardware.png

SCL 和 SDA 訊號

SCL 是時脈訊號,規定一律都是 Master 所產生,網路上眾多 Slave 傾聽接收。

SDA 是資料訊號,這個是雙向的,可以是 Master 發出,網路上眾多 Master 接收。或是其中一個 Slave 發出,由 Master 及其他的 Slave 傾聽接收。

i2c SDA SCL.png

 

透過巧妙的安排 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

Data Write.png

這裡用我們的符號翻譯一下就是

<Start|  |Addr-WR|  |Tx|  |Tx| ... |Tx|  |Stop>

Master 先送個 Start 訊號,然後 位址訊號-寫入 來點名 Slave 存在否並要求接收資料,然後 Master 依序地送出要寫入的資料:第一個是要寫入DS1307暫存器的起始位址,第二個之後是依序要寫入暫存器的資料,最後送出 Stop 訊號結束所有 I2C 交談。

 

2. Slave 讀取資料,回傳給 Master

Data Read.png

用我們的符號翻譯一下就是

<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

Write Then Read.png

用我們的符號翻譯一下就是

<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 這個即時時鐘的規格書,所有暫存器如下表所示

Registers.png

位址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 匯流排囉。所以立即可以使用。

IMG_6250D.png

IMG_6247.png

IMG_6248.png

 

 

執行結果

先是用 scanner 這個指令來掃描一下,這個 DS1307 RTC 的 I2C 位址吧。掃描的結果,位址 0x68,且 I2C 的通訊正常無誤。

IMG_6274.png

 

操作影片

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

IMG_6273.png

 

操作影片

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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    

 

 

arrow
arrow

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