題外話:這幾天天氣突然轉冷了。今天已是11月23日了,查查黃歷,昨天(11月22日)剛好是小雪,一夜溫度驟降,果然老祖先的經驗有靈驗!冬天來了,還是多加加衣服,注意保暖!
1.Abstract
前些天借用他人的一塊MCS-51開發板來做實驗,不想這塊板子與我剛開始接觸MCS-51的板子一樣,實在是太親切了!現在回過來看這塊板子,功能算不上是太強大,麻雀雖小五臟俱全,該有的功能都有。於是又忍不住搗騰這塊板子,倒不是寫小程序一塊,看着電路圖,到處連線測試一下功能,從中體會下最初的學習興奮感覺。
最初板子里邊最難學會的有兩處,一處是由I2C上掛上的一些器件,另外一處是基於DS18B20的一線傳輸協議,當初花了好大氣力去學習,仍舊一頭霧水的情景仍記憶猶新。現在回過來看,覺得當時最大的問題是不注重對時序的分析,不能完全理解總線傳輸協議,畢竟理解是隨着時間的增加而逐步深刻的。看到這兩大部分,雖然是幾個小小的芯片,但心底有不少的辛酸感。現在的理解也不能算完全,我想盡可能的用現在的所學將這兩大部分分別寫下來,權當是一個鞏固學習的過程。
2.Content
2.1 協議分析
先想想兩個陌生人是怎么進行溝通的,為了顯得更有主次關系,選取老板和新員工進行溝通的場景,老板一般占有主動權,而且手中有新員工的基本信息,比如姓名,年齡,性別等。溝通開始:
老板:“XXX,歡迎你加入我們公司,為公司注入新的血液!”伸出握手姿勢 —— 主握手
新員工:“承蒙厚愛,有幸加入我們公司,我覺得是一種光榮!”握手 —— 握手成功
老板:“想必已經讀過員工手冊了吧,說說你對前兩條的理解。” —— 執行溝通
新員工:“第一條……,第二條……” —— 從應答
……
老板:“回答的很好,確實是我們迫切想招募的人,以后看你的精彩表現了!” —— 主要求溝通結束
新員工:“一定不負厚望,必努力工作!” —— 從做好結束准備
老板離開 —— 溝通結束
為了跟后邊的描述更貼切,在實景對話的后邊做了一定的注釋,將溝通的過程抽象出來。從上面的對話可以看出,溝通分為四大步驟,握手、數據交換、准備結束、正式結束。I2C的通信也氛圍這樣幾大步驟,值得注意的是,在通信的時候,總線上必須要有一個為主器件,其他的為從器件。有些器件它既可以是主器件,又可以是從器件,但是它們在總線上的特定時候只有一種模式,要么是主器件模式,要么是從器件模式,否則就混亂了。
把那些在特定時候器件作為主器件,即在總線上表現為主控的器件叫做主發送器/接收器(MASTER TRANSMITTER/RECEIVER),有些主器件只執行發送,不執行接收的,命名為主發送器(MASTER TRANSMITTER);那些在同時候作為從器件,即在總線上表現為被控的器件叫做從發送器/接收器(SLAVE RECEIVE/RTRANSMITTER),有些從器件只執行接收,不執行發送的,命名為從接受器(SLAVE RECEIVER)。它們在同一個總線上的連接可以如FIG2.1所示。
值得強調的一下是 同一時刻,總線上只有一個器件配置為主器件,而其他的只能作為從器件!
I2C協議是典型的二線通信,確切的講,應該是三根(SDA串行數據線,SCL串行時鍾線,GND共地線),如上圖所示。
那么,一條I2C總線最多能掛多少個器件呢?一般來說,串行數據都是以一個字節一個字節的方式來衡量的,前幾位用來表示地址(上述對話中的員工名字),地址的最后一位為讀寫操作位(READ/WRITTE位,簡寫為R/W,邏輯1表示讀,邏輯0表示寫),以最開始的一個字節作為地址來算的話,那么除去字節的最后讀寫標志位,就剩 8 – 1 = 7bit了,所以理論上以1個字節為地址來算的話,就可以分配2的7次方128個地址,就可以掛上128個器件(極端情況,假設這128個器件都是從機的話,就還需要掛一個主器件,合計就是128 + 1 = 129個器件了)。要是想掛更多的器件,那么就須得將地址位擴展,比如將前兩個字節作為地址,去掉最后的一位讀寫標志位,就剩下 8 + 8 – 1 = 15 bit了,所以以2個字節為地址來算的話,就可以分配2的15次方32768個器件(當然,極端情況下可以多掛一個主器件,合計就是32768 + 1 = 32769個器件了)。要是還想掛更多的器件,方法就如上述了,將前幾個字節作為地址,最后一位作為讀寫標志位,具體的算就不展開了。
以上是理論的算法,在實際的器件中,都是以第一個字節作為地址的,而且大部分的器件的地址高四位已經被根據不同功能的芯片分配了不同的編碼(例如,AD/DA轉換芯片PCF8591的前四位為1001,E2PROM芯片AT24C02的前四位為1010,具體的芯片就得查查手冊了,這里只說明原理),那么同一種功能芯片(地址前4位都相同)最多只有 8 – 4 – 1 = 3位用來分配地址了,也就是最多可以掛2的3次方8個同種功能芯片。用一個問題來深化理解一下。
一條I2C總線上最多可以分別掛多少個PCF8591芯片和多少個AT24C02芯片呢?它們能同時掛在總線上嗎?若能,請將它們的地址全部列出來。
算算,1)查手冊得知,PCF8591的前四位固定編碼為1001,除去最后一位讀寫標志位,則剩下8 – 4 – 1 = 3位地址編碼,由數學邏輯可知,3位二進制碼可以分配2的3次方8個地址,故一條I2C總線上最多可以掛8個PCF8591;同理,AT24C02的前四位固定編碼為1010,其他的跟PCF8591一樣,故一條I2C總線上最多可以掛8個AT24C02。2)它們是可以同時掛在一條總線上的,因為它們對應的地址都不相同,主器件可以全部訪問到它們。地址如下表
表2.1 器件地址表
器件名稱 | 器件地址 |
PCF8591 | 1001 000X |
PCF8591 | 1001 001X |
PCF8591 | 1001 010X |
PCF8591 | 1001 011X |
PCF8591 | 1001 100X |
PCF8591 | 1001 101X |
PCF8591 | 1001 110X |
PCF8591 | 1001 111X |
AT24C02 | 1010 000X |
AT24C02 | 1010 001X |
AT24C02 | 1010 010X |
AT24C02 | 1010 011X |
AT24C02 | 1010 100X |
AT24C02 | 1010 101X |
AT24C02 | 1010 110X |
AT24C02 | 1010 111X |
關於總線掛器件的問題,應該是明晰許多了。下面開始看看它的四大通信步驟。
2.1.1 握手與結束
握手與結束它們都是屬於控制總線一塊兒的,抽象出來說,它是做通信的控制的,跟數據是如何交換的區分開來。
I2C協議有規定,在SCL和SDA均為高電平的前提下,檢測到SDA有下降沿信號,則建立I2C的通信開始;同樣的,在SCL為高電平,SDA為低電平的前提下,檢測到SDA有上升沿信號,則I2C通信正式結束。
對上面的一段話還真需要有深刻的理解,甚至有必要將它背下來,熟記於心。首先看看總線閑,也就是總線上沒有通信,這兩根信號線的電平狀態;從圖中可以看到,通信的開始之前,SCL和SDA均為高電平;再看看通信正式結束后,SCL和SDA信號均為高電平;也就是說,總線閑的時候,兩根信號線都是高電平的。再看看通信的建立和正式結束的時候,這兩根信號線的電平變化特點。由圖中虛線框中引出的,不管是通信建立和通信結束階段,SCL都是高電平,SDA的變化控制着通信的建立與結束;這一點尤為重要,或許換一種說法更為適合,在SCL為高電平的情況下,SDA信號的轉變就對通信起着強制性作用,要么通信建立,要么通信正式結束,有且僅有這兩種情況!也就是說,在數據交換的過程中,要對SCL這根信號線尤為注意,在數據變化的時候,一定要保證SCL是為低電平!讓數據的變化在SCL的“安全”狀態下進行,所以需要牢記一點,數據變化,時鍾線低電平先行,如下圖所示。
2.1.2 數據交換
為了更好地理解I2C數據交換的過程,真是花了不少的功夫,或許真正難懂的地方就在這里,需要准確無誤的理解。
I2C總線上時常是掛着許多器件的,但是某個通信時刻下,只存在一個主器件,一個從器件的一對一通信機制;這也是要分配地址編碼的真正原因了,主器件將數據傳送到總線上,雖然所有的器件都能接收到數據,但是只有對應地址的器件能做出應答反應。理解這一點就好辦了;從主器件這一端看過去,需要和某個從機打交道,那么它應該具備的信息有:1) 從器件的地址 2)從器件內部的控制信息;對應於人的邏輯思維應該是 對方的姓名和要求他做特定的事兒。每次通信開始前,主器件都需要將這兩個信息交代清楚。
還是站在主器件的角度來看;綜合一下,主器件主要做哪些事兒呢?是的,無非兩件,一、寫數據,二、讀數據,除此二者之外,別無其他。這對應人的思維應該就是說和聽了。引出了主器件寫數據和讀數據的概念,再就看它們是如何工作的了。
主器件向從器件寫數據。如上所述,主器件向要向從器件寫數據,首先得知道從器件的兩個基本信息,從器件地址和從器件內部控制信息。寫這些信息有特定的順序,第一個字節是從器件的地址,第二個字節是從器件內部控制信息,第三個字節是所要寫的第一個數據,第四個字節是所要寫的第二個數據……完整寫的通信過程就應該是如下圖所示了。
紫色部分表示通信控制,橙色表示寫必要的通信信息,綠色部分表示要寫的數據。
實際的I2C通信大概流程就是如此了,但是它做了一定的優化,即在每次寫數據的時候,每寫一個字節數據(不論是必要的從器件地址數據、從器件控制數據,還是需要寫到從機里的數據),從器件都有一個應答(從器件的應答實現是將SDA線拉成低電平,所以在寫完第8位數據以后,切記要把SCL的電平拉成低電平,等一小段時間以后,在將數據線拉高去讀SDA的數據,至於原因,2.1.1節描述得很清晰了),用下面的情景模式解釋一下或許更容易理解。
對話已經開始 —— 通信開始
老板:“XXX,你來一下!” —— 寫從器件地址
員工:“是!” —— 從器件應答
老板:“你管業務這一塊的吧?” —— 寫從器件控制信息
員工:“是!” —— 從器件應答
老板:“分配兩個任務,其一,今天下午2:30你們組開會!” —— 寫數據1
員工:“是!” —— 從器件應答
老板:“其二,下班后大家一起出去吃飯!” —— 寫數據2
員工:“是!” —— 從器件應答
老板:“好了,就這么多了,去忙吧!” —— 寫數據3
員工:“是!” —— 從器件應答
對話結束 —— 通信正式結束
也就是說,老板每說一段話,員工都必須做出是的回答。轉換成I2C協議里邊,就是主器件每發送一個字節數據,從機都要做一個應答(用ACK表示,簡寫為A)。
這個是以PCF8591為例子的,其他的器件類似。從左往右看,最開始的是通信建立S;然后再寫從器件地址(注意,器件地址的最后位為0,表示的是寫操作);緊接着器件回復了一個應答信號A;然后主器件寫從器件的控制信息CONTROL BYTE;從器件回復一個應答信號A;之后主器件開始寫數據DATA BYTE;寫完一個數據以后,從器件回復一個應答信號A;當然,可以寫多個數據,每寫完一個數據,從器件都回復一個應答信號A;寫完所有數據之后,可以選擇停止通信操作P,也可以建立新的開始通信S。主器件的寫就講述完了,再來看主器件的讀。
主器件向從器件讀數據。主器件向從器件讀數據的過程和上述主器件向從器件寫數據的過程類似,也得首先知道從器件的地址信息,對於控制信息,有些器件需要,有些器件則不需要,這得查看手冊信息了。具備這些信息以后,就可以開始讀器件的數據了。現在以一個最基本讀流程開始,后邊再介紹一個稍微復雜的讀流程。
以PCF8591為例,對照它的數據手冊,可知它的功能是一個8位4路A/D轉換和1路D/A轉換的集成芯片。在做D/A轉換時,主要用到的是對它進行寫的操作;而用到讀取它的數據,則是A/D轉換的操作;因為每次A/D采樣完成以后,就會把數據放到指定的地方,而且只有一個字節數據,所以對它的讀操作就不需要控制信息了,主器件只需要有它的器件地址就可以完成讀的操作了。這樣就簡化了流程,主器件只需要發送從器件的地址信息加上讀操作位,就可以對信息的讀取了。完整的流程如下。
主器件首先要建立通信,然后向總線發送 器件地址+ 讀標志位,從器件就可以將數據一位一位將數據發到總線上,要注意的是,雖然主器件不能控制SDA信號了,但是它可以控制SCL信號,主機可以通過改變SCL的電平,從而來接收來自於SDA的信息。和寫操作一樣,在每發送1個字節數據以后,需要有一個應答信號;這下應答信號的產生就來自於主器件了。可以這樣來看,發送方式從器件,而接收方是主器件,發送方要確認接收方已經接收到數據了,這種發送方和接收方的理解也適用於上述的讀操作。
比較特殊的是,主器件的應答方式有兩種。一種是將SDA線成低電平,然后輸出一個脈沖,從器件根據接收到這個低電平信號,可以判斷主器件正常地接收到了數據,而且准備好了接收下一個數據,這與上述的寫操作從機應答是一樣的;還有一種就是主器件將SDA釋放為高電平,然后輸出一個脈沖,從器件根據接收到的這個高電平信號,可以判斷主器件正常地接收到了數據,但是不准備接收下下一個數據,意味着數據通信即將結束。這種應答方式可以用下面的情景模式輔助理解一下。
對話開始 —— 建立通信
老板:“XXX,你過來一下,把今天的完成的進度匯報一下!” —— 發送從機地址並加讀信號標志位
員工:“今天完成的任務有四項,第一項是……” —— 從器件發送數據1
老板:“嗯,繼續” —— 主器件確定應答
員工:“第二項是……” —— 從器件發送數據2
老板:“好,就這樣吧,還匯報一個任務你就去忙吧!” —— 主器件取消應答
員工:“第三項是……” —— 從器件發送數據3
對話結束 —— 通信正式結束
也就是說,員工匯報完每項任務以后,老板都需要做一個應答,應答的方式有兩種,一種是做好准備聽下下一個任務,另外一種是結束聽下下一個任務,從而結束對話。轉換到I2C協議中來,就是從器件自接收地址數據要求做數據輸出時,從器件開始向總線逐個的發送數據;主器件也需要對從器件發送的數據進行應答;應答的方式有兩種,一種是發送應答信號(ACK),表示主器件已經准備好接收下下一個數據,另外一種就是非應答信號(NO ACK),表示主器件不打算接收下下一個數據,要求通信結束。PCF8591完整的讀操作過程如下所示。
還是從左往右看,最開始是建立通信開始S;然后主器件向總線上發送從器件地址,並將讀標志位置1(地址的最后一位置1,表示讀操作);隨后,從器件將第一個數據發送到I2C總線上;如果主器件准備再讀取下下一個數據,則置應答位為0(ACK),如果主器件不想再讀下下一個數據,就置應答位為1(NO ACK),表示通信即將結束;最后就是通信結束S,表示通信正式結束。
PCF8591的讀操作流程還是非常簡單的,因為所要讀的數據不多,數據位置已經固定好了,所以沒有控制信息。再來看一下一個具有控制信息的器件該怎么讀,以E2PROM存儲器AT24C02為例。
針對存儲器,首先要知道的就是它的器件地址,其次就是要讀的地址信息(也就是控制信息)。所以,在讀數據之前就要先將地址信息寫過去,然后才能准確地讀數據。例子就不舉了,跟PCF8591大同小異,多了一個在讀之前要寫控制信息這部分。直接看看完整的讀操作流程。
FIG 2.8 針對AT24C02芯片主器件完整的讀操作流程
還是從左往右看,要想讀取特定地址的數據,首先要將地址寫入到指定器件。按照操作流程,最開始是建立通信S,然后寫從器件地址,並將讀寫操作位置0(地址的最后1位為0,表示寫操作);緊接着是從器件回復應答信號(ACK);然后寫入要讀出的數據信息位置(控制信息);從器件回復應答信號(ACK);然后再開始進行讀操作,讀操作之前,也先將從器件的地址發送到I2C總線上,並將讀寫標志位置1(地址最后一位置1,表示讀操作);然后從器件回復一個應答信號(ACK);緊接着從器件將指定位置的數據發送到I2C總線上,主器件接收到數據以后,發送一個非應答信號(NO ACK),表示不再准備讀取下下一個數據,准備通信結束;最后就是通信結束S,表示通信正式結束。
上述描述了主從器件之間如何握手與通信結束,如何向從機寫數據,如何從從機讀數據等基本協議分析。下面就是進行用程序來實現這個通信協議。
2.2 協議的程序實現
協議的程序實現部分,我自己就不再寫代碼了,上述的協議圖已經分析的很透徹了,這里我參考別人寫好的程序,其實實現的方式也是大同小異,因為通信的格式已經固定了,具體的代碼都得按照這個格式來書寫。
我的這份代碼摘自於doflye網站:www.doflye.net 它是以PCF8591為例,其他的類推就可以了。
2.2.1 基本配置和宏定義
#include "i2c.h" #include "delay.h" #define _Nop() _nop_() //定義空指令 bit ack; //應答標志位 sbit SDA=P2^1; sbit SCL=P2^0;
2.2.1 I2C通信建立
void Start_I2c() { SDA=1; //發送起始條件的數據信號 _Nop(); SCL=1; _Nop(); //起始條件建立時間大於4.7us,延時 _Nop(); _Nop(); _Nop(); _Nop(); SDA=0; //發送起始信號 _Nop(); //起始條件鎖定時間大於4μ _Nop(); _Nop(); _Nop(); _Nop(); SCL=0; //鉗住I2C總線,准備發送或接收數據 _Nop(); _Nop(); }
這里有幾個位置需要說明一下,就是時序上的沖突問題。
FIG2.9 和 FIG2.10是需要合起來一起看,而且一般是要找出幾種極限的參數,比如時序建立時間、保持時間、有效數據時間等。對照圖和表,查出時序建立時間Tsu至少為4.7us,保持時間Thd至少為4.0us,數據有效時間建立不低於3.4us。
若MCS-51選取12M的晶振,每一個指令周期的時間就是 12 / ( 6 * 12) = 1us,故達到了器件工作頻率的上限值,所以需要采取一定的空機器周期用來解決時序沖突。下面也有這樣的做法,就不再贅述了。
2.2.2 I2C通信結束
void Stop_I2c() { SDA=0; //發送結束條件的數據信號 _Nop(); //發送結束條件的時鍾信號 SCL=1; //結束條件建立時間大於4μ _Nop(); _Nop(); _Nop(); _Nop(); _Nop(); SDA=1; //發送I2C總線結束信號 _Nop(); _Nop(); _Nop(); _Nop(); }
2.2.3 主器件向從器件寫一個字節數據(帶應答信號檢驗)
void SendByte(unsigned char c) { unsigned char BitCnt; for(BitCnt=0;BitCnt<8;BitCnt++) //要傳送的數據長度為8位 { if((c<<BitCnt)&0x80)SDA=1; //判斷發送位 else SDA=0; _Nop(); SCL=1; //置時鍾線為高,通知被控器開始接收數據位 _Nop(); _Nop(); //保證時鍾高電平周期大於4μ _Nop(); _Nop(); _Nop(); SCL=0; } _Nop(); _Nop(); SDA=1; //8位發送完后釋放數據線,准備接收應答位 _Nop(); _Nop(); SCL=1; _Nop(); _Nop(); _Nop(); if(SDA==1)ack=0; else ack=1; //判斷是否接收到應答信號 SCL=0; _Nop(); _Nop(); }
2.2.4 主器件從從器件接收一個數據
unsigned char RcvByte() { unsigned char retc; unsigned char BitCnt; retc=0; SDA=1; //置數據線為輸入方式 for(BitCnt=0;BitCnt<8;BitCnt++) { _Nop(); SCL=0; //置時鍾線為低,准備接收數據位 _Nop(); _Nop(); //時鍾低電平周期大於4.7us _Nop(); _Nop(); _Nop(); SCL=1; //置時鍾線為高使數據線上數據有效 _Nop(); _Nop(); retc=retc<<1; if(SDA==1)retc=retc+1; //讀數據位,接收的數據位放入retc中 _Nop(); _Nop(); } SCL=0; _Nop(); _Nop(); return(retc); }
2.2.5 主器件回復應答信號0(ACK)
void Ack_I2c(void) { SDA=0; _Nop(); _Nop(); _Nop(); SCL=1; _Nop(); _Nop(); //時鍾低電平周期大於4μ _Nop(); _Nop(); _Nop(); SCL=0; //清時鍾線,鉗住I2C總線以便繼續接收 _Nop(); _Nop(); }
2.2.6 主器件回復應答信號1(NO ACK)
void NoAck_I2c(void) { SDA=1; _Nop(); _Nop(); _Nop(); SCL=1; _Nop(); _Nop(); //時鍾低電平周期大於4μ _Nop(); _Nop(); _Nop(); SCL=0; //清時鍾線,鉗住I2C總線以便繼續接收 _Nop(); _Nop(); }
2.2.7 主器件向從器件寫入數據
bit WriteDAC(unsigned char dat) { Start_I2c(); //啟動總線 SendByte(AddWr); //發送器件地址 if(ack==0)return(0); SendByte(0x40); //發送控制信息if(ack==0)return(0); SendByte(dat); //發送數據 if(ack==0)return(0); Stop_I2c(); }
2.2.8 主器件從從器件讀入數據
unsigned char ReadADC(unsigned char Chl) { unsigned char Val; Start_I2c(); //啟動總線 SendByte(AddWr); //發送器件地址 if(ack==0)return(0); SendByte(0x00|Chl); //發送控制信息 AD采樣通道 if(ack==0)return(0); Start_I2c(); SendByte(AddWr+1); // 將最后一位讀寫標志位設置為1,表示讀 if(ack==0)return(0); Val=RcvByte(); NoAck_I2c(); //發送非應位 Stop_I2c(); //結束總線 return(Val); }
2.3 實際程序驗證
最后加入一些液晶顯示模塊和AD轉換數據處理,將信息可以顯示在液晶屏幕上。因為這套板子的程序已經完全做好了,在出廠之前就已經測試好了;為了保險起見,我也將程序燒錄其中做了一下測試,實際上是可以通得過的。這里就不再插圖看了,直接將測量結果以表格的方式給出來,避免打廣告的嫌疑。
表2.2 數據測量表
測量次數 | 萬用表測試值(V) | AD轉換后對應的值(V) |
1 | 0.00 | 0.00 |
2 | 1.56 | 1.60 |
3 | 2.45 | 2.52 |
4 | 4.87 | 4.98 |
萬用表的型號是LINI-T UT51。從上述的測試值對比,轉換的值還是相差挺大的,雖然A/D轉換器存在量化誤差,但是不至於這么大。分析了下原因,其實是受參考電壓的影響,A/D轉換計算是采用5.00V的參考電壓,而實際的參考電壓為4.87V,根據轉換的公式,可以知道計算出來的電壓與實際電壓有一個比值系數 k = 5.00/4.87,有了這個關系,把AD轉換后的值除以這個系數,就可以得到實際的電壓了,以第二次測試結果為例說明一下。
1.60 / k = 1.60 * 4.87 / 5.00 = 1.5584,取小數后最后兩位,第三位小數位進行四舍五入處理,那么實際的轉換值為1.56,這與萬用表的測試值是相吻合的。
關於器件的量化誤差,與本節的內容就不大相關了,這里就不分析這部分了。上述實驗證明,通過MCU與PCF8591的I2C通信是成功的。
3.Conclusion
總體來說,I2C的協議還是不復雜的,不過學好它確實有點不容易,需要從多個角度去思考和體會。現在規范的協議不算太多,能夠很好的掌握I2C協議就相當於上了一個新的台階。
就個人來說,對I2C的協議理解還是不夠深刻的,以前學了一門課程,計算機接口與技術,這門課是專門講接口的,I2C就是一個很常用的串行接口。不過這本書的主要目的是講硬件方面,即這些接口是怎樣用物理的方法實現的。對於使用者,確確實實只要了解這個接口怎么用就可以了,但想從電路設計的角度去深刻理解它,我想要學的還有很多。
4.Reference
1). PCF8591 datasheet
2). AT24C02 datasheet