串行通信協議用於芯片間的數據交互。常用的串行通信協議包括單總線、I2C、SPI、UART。最初,串行通信協議種類太多,給芯片設計者和使用方都造成了很大的不便。后來,經過飛利浦等公司的共同努力,才形成了I2C、SPI、UART等通用串行通信協議。而單總線因為其通信效率低,只在少數場合中有應用,並未形成通用的協議,仍需要根據具體的芯片手冊進行協議的軟件實現。
通用串行通信協議具有完善而靈活的通信功能,很多外圍芯片都根據通用串行通信協議設計其通信接口電路,所以大部分微控制器/微處理器也都集成了通用串行通信協議的硬件實現電路。在此基礎上,程序員通常只需要簡單配置微控制器/微處理器中的串行通信協議的工作方式就可以和外圍芯片進行數據交互了。而在一些情況下(比如微控制器/微處理器沒有集成所需的通用串行通信協議或外圍芯片的串行通信協議和通用串行通信協議有些出入),程序員就需要通過軟件去實現串行通信協議。
通用串行通信協議用於交互一個或多個字節,這些字節對於不同的外圍芯片具有不同的含義。因此,在設計程序時,需要設計通用驅動代碼和專用驅動代碼兩個部分。通用驅動代碼就是通用串行通信協議的代碼,專用驅動代碼就是利用具體芯片內部各種寄存器進行讀寫操作的代碼。
第一部分 單總線
參考資料:《DS18B20.pdf》《http://c.biancheng.net/cpp/html/1958.html》
單總線通信協議因芯片不同有明顯差異,這一部分以常見的DS18B20為例介紹其單總線通信協議的實現。
1.1 協議說明
1.1.1 初始化
和 I2C 的尋址類似,1-Wire 總線開始也需要檢測這條總線上是否存在 DS18B20這個器件。如果這條總線上存在 DS18B20,總線會根據時序要求返回一個低電平脈沖,如果不存在的話,也就不會返回脈沖,即總線保持為高電平,所以習慣上稱之為檢測存在脈沖。此外,獲取存在脈沖不僅僅是檢測是否存在 DS18B20,還要通過這個脈沖過程通知 DS18B20准備好,單片機要對它進行操作了。
存在脈沖檢測過程,首先單片機要拉低這個引腳,持續大概 480us 到 960us 之間的時間即可,我們的程序中持續了 500us。然后,單片機釋放總線,就是給高電平,DS18B20 等待大概 15 到 60us 后,會主動拉低這個引腳大概是 60 到 240us,而后 DS18B20 會主動釋放總線,這樣 IO 口會被上拉電阻自動拉高。
有的同學還是不能夠徹底理解,程序列出來逐句解釋。首先,由於 DS18B20 時序要求非常嚴格,所以在操作時序的時候,為了防止中斷干擾總線時序,先關閉總中斷。然后第一步,拉低 DS18B20 這個引腳,持續 500us;第二步,延時 60us;第三步,讀取存在脈沖,並且等待存在脈沖結束。
1.1.2 ROM操作指令
我們學 I2C 總線的時候就了解到,總線上可以掛多個器件,通過不同的器件地址來訪問不同的器件。同樣,1-Wire 總線也可以掛多個器件,但是它只有一條線,如何區分不同的器件呢?
在每個 DS18B20 內部都有一個唯一的 64 位長的序列號,這個序列號值就存在 DS18B20內部的 ROM 中。開始的 8 位是產品類型編碼(DS18B20 是 0x10),接着的 48 位是每個器件唯一的序號,最后的 8 位是 CRC 校驗碼。DS18B20 可以引出去很長的線,最長可以到幾十米,測不同位置的溫度。單片機可以通過和 DS18B20 之間的通信,獲取每個傳感器所采集到的溫度信息,也可以同時給所有的 DS18B20 發送一些指令。這些指令相對來說比較復雜,而且應用很少,所以這里大家有興趣的話就自己去查手冊完成吧,我們這里只講一條總線上只接一個器件的指令和程序。
Skip ROM(跳過 ROM):0xCC。當總線上只有一個器件的時候,可以跳過 ROM,不進行 ROM 檢測。
1.1.3 RAM操作指令
Read Scratchpad(讀暫存寄存器):0xBE。這里要注意的是,DS18B20 的溫度數據是 2 個字節,我們讀取數據的時候,先讀取到的是低字節的低位,讀完了第一個字節后,再讀高字節的低位,直到兩個字節全部讀取完畢。
Convert Temperature(啟動溫度轉換):0x44。當我們發送一個啟動溫度轉換的指令后,DS18B20 開始進行轉換。從轉換開始到獲取溫度,DS18B20 是需要時間的,而這個時間長短取決於 DS18B20 的精度。前邊說 DS18B20 最高可以用 12 位來存儲溫度,但是也可以用 11 位,10 位和 9 位一共四種格式。
1.1.4 位讀寫時序
當要給 DS18B20 寫入 0 的時候,單片機直接將引腳拉低,持續時間大於 60us 小於 120us就可以了。圖上顯示的意思是,單片機先拉低 15us 之后,DS18B20 會在從 15us 到 60us 之間的時間來讀取這一位,DS18B20 最早會在 15us 的時刻讀取,典型值是在 30us 的時刻讀取,最多不會超過 60us,DS18B20 必然讀取完畢,所以持續時間超過 60us 即可。
當要給 DS18B20 寫入 1 的時候,單片機先將這個引腳拉低,拉低時間大於 1us,然后馬上釋放總線,即拉高引腳,並且持續時間也要大於 60us。和寫 0 類似的是,DS18B20 會在15us 到 60us 之間來讀取這個 1。
當要讀取 DS18B20 的數據的時候,我們的單片機首先要拉低這個引腳,並且至少保持1us 的時間,然后釋放引腳,釋放完畢后要盡快讀取。從拉低這個引腳到讀取引腳狀態,不能超過 15us。主機采樣時間,也就是 MASTER SAMPLES,是在 15us 之內必須完成的,讀取一個字節數據的程序如下。
1.2 協議實現

/****************************************************** //主要內容:DS18B20驅動代碼 //補充說明:12位分辨率時的最大轉換時間為750ms,啟動轉換后不能立即讀取,因此啟動轉換和讀取兩個函數是分開寫的 ******************************************************/ #include <reg52.h> #include <intrins.h> typedef unsigned char uint8 typedef unsigned int uint16 sbit DQ = P3^2; //DS18B20 通信引腳 /****************************************************** //功 能:軟件延時,延時時間(t*10)us //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void DelayX10us(uint8 t) { do { _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); _nop_(); }while (--t); } /****************************************************** //功 能:復位總線,獲取存在脈沖,以啟動一次讀寫操作 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ bit Get18B20Ack(void) { bit ack; EA = 0; //禁止總中斷 DQ = 0; DelayX10us(50); //產生 500us 復位脈沖 DQ = 1; DelayX10us(6); //延時 60us ack = DQ; //讀取存在脈沖 while(!DQ); //等待存在脈沖結束 EA = 1; //使能總中斷 return ack; } /****************************************************** //功 能:向 DS18B20 寫入一個字節 //輸入參數:待寫入字節 //返 回 值:void //補充說明:無 ******************************************************/ void Write18B20(uint8 dat) { uint8 i ; EA = 0; //禁止總中斷 for(i=0;i<8;i++) { DQ = 0; //拉低總線至少1us,表示寫時序開始。 _nop_(); DQ = dat & 0x01; DelayX10us(6); //延時 60us DQ = 1; //寫完后釋放總線 dat >>= 1; _nop_(); _nop_(); } EA = 1; //使能總中斷 } /****************************************************** //功 能:從 DS18B20 讀取一個字節 //輸入參數:void //返 回 值:讀到的字節 //補充說明:無 ******************************************************/ uint8 Read18B20(void) { uint8 i; uint8 dat = 0; EA = 0; //禁止總中斷 for(i=0;i<8;i++) { DQ = 0; //拉低總線至少1us,表示讀時序開始。單片機在此下降沿內15us內讀取的數據是有效的。 _nop_(); dat >>= 1; DQ = 1; //釋放總線,由DS18B20控制總線並傳輸一位數據 _nop_(); _nop_(); if(DQ) dat |= 0x80; DelayX10us(6); //延時60us } EA = 1; //重新使能總中斷 return dat; } /****************************************************** //功 能:啟動一次 18B20 溫度轉換 //輸入參數:void //返 回 值:表示是否啟動成功 //補充說明:無 ******************************************************/ bit Start18B20(void) { bit ack; ack = Get18B20Ack(); //執行總線復位,並獲取 18B20 應答 if(ack == 0) //如 18B20 正確應答,則啟動一次轉換 { Write18B20(0xCC); //跳過 ROM 操作 Write18B20(0x44); //啟動一次溫度轉換 } return ~ack; //ack==0 表示操作成功,所以返回值對其取反 } /****************************************************** //功 能:讀取 DS18B20 轉換的溫度值 //輸入參數:void //返 回 值:表示是否讀取成功 //補充說明:溫度有正有負,以補碼的形式存儲 ******************************************************/ bit Get18B20Temp(int *temp) { bit ack; uint8 LSB, MSB; //16bit 溫度值的低字節和高字節 ack = Get18B20Ack(); //執行總線復位,並獲取 18B20 應答 if(ack == 0) //如 18B20 正確應答,則讀取溫度值 { Write18B20(0xCC); //跳過 ROM 操作 Write18B20(0xBE); //發送讀命令 LSB = Read18B20(); //讀溫度值的低字節 MSB = Read18B20(); //讀溫度值的高字節 *temp = ((int)MSB << 8) + LSB; //合成為 16bit 整型數 } return ~ack; //ack==0 表示操作應答,所以返回值為其取反值 }
第二部分 Inter-IC(Standard-mode)
參考資料:《I2C-bus specification and user manual V.6.pdf》《AT24C02.pdf》《http://c.biancheng.net/cpp/html/1939.html》《STM32英文參考手冊_V15.pdf》《STM32中文參考手冊_V10.pdf》
2.1 協議說明
飛利浦半導體(現為恩智浦半導體)開發了一種簡單的雙向 2 線總線,用於高效的 IC 間控制,稱為 Inter-IC 或 I2C 總線。I2C總線數據傳輸速率在標准模式下最高可達 100 kbit/s,在快速模式下最高可達 400 kbit/s,在快速模式 Plus 下最高可達 1 Mbit/s,或在高速模式下高達 3.4 Mbit/s。超快速模式是一種單向模式,數據傳輸速度高達 5 Mbit/s。
串行數據 (SDA) 和串行時鍾 (SCL) 這兩條線在連接到總線的設備之間傳輸信息。 每個設備都由一個唯一的地址識別(無論是微控制器、LCD 驅動程序、存儲器還是鍵盤接口),並且可以作為發送器或接收器運行,具體取決於設備的功能。 LCD 驅動器可能只是一個接收器,而存儲器既可以接收數據,也可以傳輸數據。 除了發送器和接收器之外,設備在執行數據傳輸時也可以被視為主機或從機(見表 1)。主機是在總線上啟動數據傳輸並生成時鍾信號以允許傳輸的設備,此時任何被尋址的設備都視為從機。
2.1.1 SDA and SCL signals
SDA 和 SCL 都是雙向線路,通過電流源或上拉電阻連接到正電源電壓。
- 當總線空閑時,SDA 和 SCL 都是高電平。
- SCL 為高電平時,SDA 線上的高電平到低電平轉換定義了啟動條件。SCL 為高電平時,SDA 線上的低電平到高電平轉換定義了停止條件。
- 當 SCL 為高電平時,SDA 上的數據必須是穩定的。只有 SCL 為低電平時,SDA上的數據才可以改變(見圖 4)。每傳輸一個數據位都會產生一個時鍾脈沖。

2.1.4 START and STOP conditions
所有事務都以 START (S) 開始,並以 STOP (P) 結束(見圖 5)。
START 和 STOP 條件始終由主機生成。在 START 條件之后,總線被認為是忙碌的。在 STOP 條件之后的某個時間,總線被認為再次空閑。
如果生成重復的 START (Sr) 而不是 STOP 條件,則總線保持忙碌。在這方面,START (S) 和重復 START (Sr) 條件在功能上是相同的。
因此,對於本文檔的其余部分,S 符號用作通用術語來表示 START 和重復的 START 條件,除非 Sr 特別相關。
2.1.5 Byte format
SDA 線上的每個字節都必須是 8 位長。 每次傳輸可傳輸的字節數不受限制。 每個字節后必須跟一個確認位。 數據首先以最高有效位 (MSB) 傳輸(參見圖 6)。 如果從機在執行其他功能(例如服務內部中斷)之前無法接收或發送另一個完整字節的數據,則它可以將時鍾線 SCL 保持為低電平以強制主機進入等待狀態。 當從設備准備好接收另一個字節的數據並釋放時鍾線 SCL 時,數據傳輸將繼續。
2.1.6 Acknowledge (ACK) and Not Acknowledge (NACK)
確認發生在每個字節之后。確認位使接收器能夠通知發送器該字節已成功接收並且可以發送另一個字節。主機生成所有時鍾脈沖,包括確認第九個時鍾脈沖。
確認信號定義如下:發送器在確認時鍾脈沖期間釋放 SDA 線,因此接收器可以將 SDA 線拉低,並在該時鍾脈沖的高電平期間保持穩定的低電平(見圖 4)。還必須考慮建立和保持時間(在第 6 節中指定)。
當 SDA 在第 9 個時鍾脈沖期間保持高電平時,這被定義為未確認信號。然后,主機可以生成一個停止條件來中止傳輸,或者生成一個重復的 START 條件來開始新的傳輸。有五個條件會導致 NACK 的產生:
1. 總線上的接收器的地址都不是發送器發送的地址,因此沒有設備響應確認。
2. 接收器無法接收或發送,因為它正在執行一些實時功能,還沒有准備好開始與主設備的通信。
3. 在傳輸過程中,接收方得到了它不理解的數據或命令。
4. 在傳輸過程中,接收方不能再接收任何數據字節。
5. 主接收器必須向從發送器發出傳輸結束信號。
2.1.7 The slave address and R/W bit
數據傳輸遵循圖 9 所示的格式。在 START 條件 (S) 之后,發送從地址。 該地址有 7 位長,后跟第 8 位,即數據方向位 (R/W)——“0”表示傳輸 (WRITE),“1”表示請求數據 (READ)(參見圖 10)。 數據傳輸總是由主機產生的停止條件 (P) 終止。 但是,如果主機仍然希望在總線上進行通信,它可以生成重復的 START 條件 (Sr) 並尋址另一個從機,而無需首先生成 STOP 條件。 在這樣的傳輸中,讀/寫格式的各種組合是可能的。
2.2 協議實現
I2C 通信分為低速模式 100kbit/s、快速模式 400kbit/s 和高速模式3.4Mbit/s。因為所有的 I2C 器件都支持低速,但卻未必支持另外兩種速度,所以作為通用的I2C 程序我們選擇 100k 這個速率來實現,也就是說實際程序產生的時序必須小於等於 100k的時序參數。很明顯也就是要求 SCL 的高低電平持續時間都不短於 5us,因此我們在時序函數中通過插入 I2CDelay()這個總線延時函數(它實際上就是 4 個 NOP 指令,用 define 在文件開頭做了定義),加上改變 SCL 值語句本身占用的至少一個周期,來達到這個速度限制。如果以后需要提高速度,那么只需要減小這里的總線延時時間即可。
/****************************************************** //主要內容:I2C協議實現 //補充說明:無 ******************************************************/ #include <reg52.h> #include <intrins.h> #define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
sbit I2C_SCL = P3^7; sbit I2C_SDA = P3^6; /****************************************************** //功 能:產生總線起始信號 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void I2CStart(void) { I2C_SDA = 1; //首先確保 SDA、SCL 都是高電平 I2C_SCL = 1; I2CDelay(); I2C_SDA = 0; //先拉低 SDA I2CDelay(); I2C_SCL = 0; //再拉低 SCL } /****************************************************** //功 能:產生總線停止信號 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void I2CStop(void) { I2C_SCL = 0; //首先確保 SDA、SCL 都是低電平 I2C_SDA = 0; I2CDelay(); I2C_SCL = 1; //先拉高 SCL I2CDelay(); I2C_SDA = 1; //再拉高 SDA I2CDelay(); } /****************************************************** //功 能:I2C 總線寫操作 //輸入參數:待寫入字節 //返 回 值:從機應答位的值 //補充說明:無 ******************************************************/ bit I2CWrite(uint8 dat) { bit ack; //用於暫存應答位的值 uint8 i; for(i=0;i<8;i++) { if ((dat & 0x80) == 0) I2C_SDA = 0; else I2C_SDA = 1; dat <<= 1; I2CDelay(); I2C_SCL = 1; //拉高 SCL I2CDelay(); I2C_SCL = 0; //再拉低 SCL,完成一個位周期 } I2C_SDA = 1; //8 位數據發送完后,主機釋放 SDA,以檢測從機應答 I2CDelay(); I2C_SCL = 1; //拉高 SCL ack = I2C_SDA; //讀取此時的 SDA 值,即為從機的應答值 I2CDelay(); I2C_SCL = 0; //再拉低 SCL 完成應答位,並保持住總線 return (~ack); //應答值取反以符合通常的邏輯 } /****************************************************** //功 能:I2C 總線讀操作,並發送非應答信號 //輸入參數:void //返 回 值:讀到的字節 //補充說明:無 ******************************************************/ uint8 I2CReadNAK(void) { uint8 i; uint8 dat = 0; I2C_SDA = 1; //首先確保主機釋放 SDA for(i=0;i<8;i++) { I2CDelay(); I2C_SCL = 1; //拉高 SCL dat <<= 1; if(I2C_SDA != 0) dat |= 0x80; I2CDelay(); I2C_SCL = 0; //再拉低 SCL,以使從機發送出下一位 } I2C_SDA = 1; //8 位數據發送完后,拉高 SDA,發送非應答信號 I2CDelay(); I2C_SCL = 1; //拉高 SCL I2CDelay(); I2C_SCL = 0; //再拉低 SCL 完成非應答位,並保持住總線 return dat; } /****************************************************** //功 能:I2C 總線讀操作,並發送應答信號 //輸入參數:void //返 回 值:讀到的字節 //補充說明:無 ******************************************************/ uint8 I2CReadACK(void) { uint8 i; uint8 dat; I2C_SDA = 1; //首先確保主機釋放 SDA for(i=0;i<8;i++) { I2CDelay(); I2C_SCL = 1; //拉高 SCL dat <<= 1; if(I2C_SDA != 0) dat |= 0x80; I2CDelay(); I2C_SCL = 0; //再拉低 SCL,以使從機發送出下一位 } I2C_SDA = 0; //8 位數據發送完后,拉低 SDA,發送應答信號 I2CDelay(); I2C_SCL = 1; //拉高 SCL I2CDelay(); I2C_SCL = 0; //再拉低 SCL 完成應答位,並保持住總線 return dat; }
2.3 芯片說明
2.3.1 EEPROM 寫數據流程
第一步,首先是 I2C 的起始信號,接着跟上首字節,也就是我們前邊講的 I2C 的器件地址,並且在讀寫方向上選擇“寫”操作。
第二步,發送數據的存儲地址。24C02 一共 256 個字節的存儲空間,地址從 0x00~0xFF,我們想把數據存儲在哪個位置,此刻寫的就是哪個地址。
第三步,發送要存儲的數據第一個字節、第二個字節„„注意在寫數據的過程中,EEPROM 每個字節都會回應一個“應答位 0”,來告訴我們寫 EEPROM 數據成功,如果沒有回應答位,說明寫入不成功。
在寫數據的過程中,每成功寫入一個字節,EEPROM 存儲空間的地址就會自動加 1,當加到 0xFF 后,再寫一個字節,地址會溢出又變成了 0x00。
2.3.2 EEPROM 讀數據流程
第一步,首先是 I2C 的起始信號,接着跟上首字節,也就是我們前邊講的 I2C 的器件地址,並且在讀寫方向上選擇“寫”操作。這個地方可能有同學會詫異,我們明明是讀數據為何方向也要選“寫”呢?剛才說過了,24C02 一共有 256 個地址,我們選擇寫操作,是為了把所要讀的數據的存儲地址先寫進去,告訴 EEPROM 我們要讀取哪個地址的數據。這就如同我們打電話,先撥總機號碼(EEPROM 器件地址),而后還要繼續撥分機號碼(數據地址),而撥分機號碼這個動作,主機仍然是發送方,方向依然是“寫”。
第二步,發送要讀取的數據的地址,注意是地址而非存在 EEPROM 中的數據,通知EEPROM 我要哪個分機的信息。
第三步,重新發送 I2C 起始信號和器件地址,並且在方向位選擇“讀”操作。
這三步當中,每一個字節實際上都是在“寫”,所以每一個字節 EEPROM 都會回應一個“應答位 0”。
第四步,讀取從器件發回的數據,讀一個字節,如果還想繼續讀下一個字節,就發送一個“應答位 ACK(0)”,如果不想讀了,告訴 EEPROM,我不想要數據了,別再發數據了,那就發送一個“非應答位 NAK(1)”。
和寫操作規則一樣,我們每讀一個字節,地址會自動加 1,那如果我們想繼續往下讀,給 EEPROM 一個 ACK(0)低電平,那再繼續給 SCL 完整的時序,EEPROM 會繼續往外送數據。如果我們不想讀了,要告訴 EEPROM 不要數據了,那我們直接給一個 NAK(1)高電平即可。這個地方大家要從邏輯上理解透徹,不能簡單的靠死記硬背了,一定要理解明白。梳理一下幾個要點:
A、在本例中單片機是主機,24C02 是從機;
B、無論是讀是寫,SCL 始終都是由主機控制的;
C、寫的時候應答信號由從機給出,表示從機是否正確接收了數據;
D、讀的時候應答信號則由主機給出,表示是否繼續讀下去。
2.3.3 EEPROM 多字節讀寫操作
我們讀取 EEPROM 的時候很簡單,EEPROM 根據我們所送的時序,直接就把數據送出來了,但是寫 EEPROM 卻沒有這么簡單了。給 EEPROM 發送數據后,先保存在了 EEPROM的緩存,EEPROM 必須要把緩存中的數據搬移到“非易失”的區域,才能達到掉電不丟失的效果。而往非易失區域寫需要一定的時間,每種器件不完全一樣,ATMEL 公司的 24C02 的這個寫入時間最高不超過 5ms。在往非易失區域寫的過程,EEPROM 是不會再響應我們的訪問的,不僅接收不到我們的數據,我們即使用 I2C 標准的尋址模式去尋址,EEPROM 都不會應答,就如同這個總線上沒有這個器件一樣。數據寫入非易失區域完畢后,EEPROM 再次恢復正常,可以正常讀寫了。
在向 EEPROM 連續寫入多個字節的數據時,如果每寫一個字節都要等待幾 ms 的話,整體上的寫入效率就太低了。因此 EEPROM 的廠商就想了一個辦法,把 EEPROM 分頁管理。24C01、24C02 這兩個型號是 8 個字節一個頁,而 24C04、24C08、24C16 是 16 個字節一頁。我們開發板上用的型號是 24C02,一共是 256 個字節,8 個字節一頁,那么就一共有 32 頁。
分配好頁之后,如果我們在同一個頁內連續寫入幾個字節后,最后再發送停止位的時序。EEPROM 檢測到這個停止位后,就會一次性把這一頁的數據寫到非易失區域,就不需要像上節課那樣寫一個字節檢測一次了,並且頁寫入的時間也不會超過 5ms。如果我們寫入的數據跨頁了,那么寫完了一頁之后,我們要發送一個停止位,然后等待並且檢測 EEPROM 的空閑模式,一直等到把上一頁數據完全寫到非易失區域后,再進行下一頁的寫入,這樣就可以在很大程度上提高數據的寫入效率。
2.4 驅動實現

/****************************************************** //版權說明:AT24C02驅動代碼 //主要內容: //補充說明:無 ******************************************************/ #include <reg52.h> /****************************************************** //功 能:AT24C02讀取函數 //輸入參數:buf-數據接收指針,addr-E2 中的起始地址,len-讀取長度 //返 回 值:void //補充說明:無 ******************************************************/ void E2Read(uint8 *buf, uint8 addr, uint8 len) { do //用尋址操作查詢當前是否可進行讀寫操作 { I2CStart(); if(I2CWrite(0x50<<1)) break; //應答則跳出循環,非應答則進行下一次查詢 I2CStop(); }while(1); I2CWrite(addr); //寫入起始地址 I2CStart(); //發送重復啟動信號 I2CWrite((0x50<<1)|0x01); //尋址器件,后續為讀操作 while (len > 1) //連續讀取 len-1 個字節 { *buf++ = I2CReadACK(); //最后字節之前為讀取操作+應答 len--; } *buf = I2CReadNAK(); //最后一個字節為讀取操作+非應答 I2CStop(); } /****************************************************** //功 能:AT24C02寫入函數 //輸入參數:buf-源數據指針,addr-E2 中的起始地址,len-寫入長度 //返 回 值:void //補充說明:無 ******************************************************/ void E2Write(uint8 *buf, uint8 addr, uint8 len) { while(len > 0) //等待上次寫入操作完成 { do //用尋址操作查詢當前是否可進行讀寫操作 { I2CStart(); if(I2CWrite(0x50<<1)) break; //應答則跳出循環,非應答則進行下一次查詢 I2CStop(); } while(1); //按頁寫模式連續寫入字節 I2CWrite(addr); //寫入起始地址 while(len > 0) { I2CWrite(*buf++); //寫入一個字節數據 len--; //待寫入長度計數遞減 addr++; //E2 地址遞增 if ((addr&0x07) == 0) break; //檢查地址是否到達頁邊界,24C02 每頁 8 字節,所以檢測低 3 位是否為零即可。到達頁邊界時,跳出循環,結束本次寫操作 } I2CStop(); } }
第三部分 SPI
參考資料:《SPI Block Guide V4.01.pdf》《W25Q64FV.pdf》《STM32英文參考手冊_V15.pdf》《STM32中文參考手冊_V10.pdf》
3.1 協議介紹
SPI 模塊允許在 MCU 和外圍設備之間進行雙工、同步、串行通信。 軟件可以輪詢 SPI 狀態標志或 SPI 操作可以由中斷驅動。
通過設置 SPI 控制寄存器 1 中的 SPI 啟用 (SPE) 位來啟用 SPI 系統。當 SPE 位被設置時,四個相關的 SPI 端口引腳專用於 SPI 功能。
SPI 系統的主要組成部分是 SPI 數據寄存器。 主機中的 8 位數據寄存器和從機中的 8 位數據寄存器通過 MOSI 和 MISO 引腳鏈接,形成一個分布式的 16 位寄存器。 當執行數據傳輸操作時,這個 16 位寄存器從主機的 S 時鍾串行移位 8 位位置,因此在主機和從機之間交換數據。 寫入主機SPI數據寄存器的數據成為輸出到從機的數據,傳輸操作后從主機SPI數據寄存器讀取的數據是來自從機的輸入數據。
讀取 SPTEF=1 的 SPISR,然后寫入 SPIDR 會將數據放入發送數據寄存器。 當傳輸完成且 SPIF 清零時,接收到的數據被移入接收數據寄存器。 該 8 位數據寄存器用作讀取的 SPI 接收數據寄存器和寫入的 SPI 發送數據寄存器。 單個 SPI 寄存器地址用於從讀取數據緩沖區讀取數據並將數據寫入發送數據寄存器。
SPI 控制寄存器 1 (SPICR1) 中的時鍾相位控制位 (CPHA) 和時鍾極性控制位 (CPOL) 選擇 SPI 系統使用的四種可能時鍾格式之一。 CPOL 位只是選擇一個非反相或反相時鍾。 CPHA 位用於通過在奇數 SCK 邊沿或偶數 SCK 邊沿上采樣數據來適應兩種根本不同的協議(請參閱傳輸格式)。
SPI 可配置為作為主機或從機運行。 當 SPI 控制寄存器 1 中的 MSTR 位置位時,選擇主機模式,當 MSTR 位清零時,選擇從機模式。
Master Mode
當 MSTR 位置位時,SPI 以主模式運行。 只有主 SPI 模塊可以啟動傳輸。 發送通過寫入主 SPI 數據寄存器開始。 如果移位寄存器為空,字節立即傳送到移位寄存器。 在串行時鍾的控制下,字節開始在 MOSI 引腳上移出。
S-clock:SPR2、SPR1和SPR0波特率選擇位與SPI波特率寄存器中的SPPR2、SPPR1和SPPR0波特率預選位一起控制波特率發生器並決定傳輸速度。 SCK 引腳是 SPI 時鍾輸出。 主機的波特率發生器通過 SCK 引腳控制從機外設的移位寄存器。
MOSI、MISO 引腳:在主模式下,串行數據輸出引腳(MOSI)和串行數據輸入引腳(MISO)的功能由 SPC0 和 BIDIROE 控制位決定。
SS 引腳:如果設置了 MODFEN 和 SSOE 位,則 SS 引腳被配置為從選擇輸出。 SS 輸出在每次傳輸期間變為低電平,而在 SPI 處於空閑狀態時為高電平。
當對主機中的 SPI 數據寄存器進行寫操作時,會有半個 SCK 周期延遲。 延遲后,SCK 在主設備內啟動。 傳輸操作的其余部分略有不同,具體取決於 SPI 控制寄存器 1 中的 SPI 時鍾相位位 CPHA 指定的時鍾格式(請參閱傳輸格式)。
Slave Mode
當 SPI 控制寄存器 1 中的 MSTR 位清零時,SPI 以從機模式運行。
SCK 時鍾:在從機模式下,SCK 是來自主機的 SPI 時鍾輸入。
MISO、MOSI 引腳:在從機模式下,串行數據輸出引腳(MISO)和串行數據輸入引腳(MOSI)的功能由 SPI 控制寄存器 2 中的 SPC0 位和 BIDIROE 位決定。
SS 引腳:SS 引腳是從選擇輸入。 在數據傳輸發生之前,從 SPI 的 SS 引腳必須為低電平。 SS 必須保持低電平直到傳輸完成。 如果 SS 變高,SPI 將被強制進入空閑狀態。
SS 輸入還控制串行數據輸出引腳,如果 SS 為高電平(未選擇),則串行數據輸出引腳為高阻抗,如果 SS 為低電平,則 SPI 數據寄存器中的第一位被驅動出串行數據 輸出引腳。 此外,如果未選擇從機(SS 為高電平),則 SCK 輸入將被忽略,並且不會發生 SPI 移位寄存器的內部移位。
盡管 SPI 能夠進行雙工操作,但某些 SPI 外設只能在從模式下接收 SPI 數據。 對於這些更簡單的設備,沒有串行數據輸出引腳。
只要不超過一個從設備驅動系統從設備的串行數據輸出線,幾個從設備就可以從一個主設備接收相同的傳輸,盡管主設備不會收到所有接收從設備的返回信息。
如果 SPI 控制寄存器 1 中的 CPHA 位清零,則 SCK 輸入上的奇數邊沿會導致串行數據輸入引腳上的數據被鎖存。 偶數邊緣導致先前從串行數據輸入引腳鎖存的值移入 SPI 移位寄存器的 LSB 或 MSB,具體取決於 LSBFE 位。
如果設置了 CPHA 位,SCK 輸入上的偶數邊沿會導致串行數據輸入引腳上的數據被鎖存。 奇數邊沿導致先前從串行數據輸入引腳鎖存的值移入 SPI 移位寄存器的 LSB 或 MSB,具體取決於 LSBFE 位。
當 CPHA 置位時,第一個邊沿用於將第一個數據位傳送到串行數據輸出引腳。 當 CPHA 清零且 SS 輸入為低電平(選擇從機)時,SPI 數據的第一位被驅動出串行數據輸出引腳。 在第 8 個移位之后,傳輸被認為完成,接收到的數據被傳輸到 SPI 數據寄存器。 為了指示傳輸完成,SPI 狀態寄存器中的 SPIF 標志被設置。
Transmission Formats
在 SPI 傳輸期間,數據同時傳輸(串行移出)和接收(串行移入)。 串行時鍾 (SCK) 同步兩條串行數據線上信息的移位和采樣。 從選擇線允許選擇單個從 SPI 設備,未選擇的從設備不會干擾 SPI 總線活動。 或者,在主 SPI 設備上,從選擇線可用於指示多主總線爭用。
主 SPI 設備和通信從設備的時鍾相位和極性應該相同。 在某些情況下,在傳輸之間改變相位和極性以允許主設備與具有不同要求的外圍從設備通信。
3.2 協議實現
/****************************************************** //主要內容:SPI協議實現 //補充說明:無 ******************************************************/ sbit SPI_SCK = P3^2; sbit SPI_SI = P3^3; sbit SPI_SO = P3^4; sbit SPI_CS = P3^5; /****************************************************** //功 能:SPI發送一個字節數據 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void SPI_SendOneByte(uint16 Sdata) { uint8 i; for(i=0;i<8;i++) { SPI_SCK = 0; //上升沿發送數據,提前拉低電平 _nop_(); if(Sdata&0x80) SPI_SO = 1; //判斷高位是否為1 else SPI_SO = 0; _nop_(); SPI_SCK = 1; Sdata <<= 1; } } /****************************************************** //功 能:SPI接收一個字節數據 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ uint8 SPI_ReadOneByte(void) { uint8 i; uint8 Temp_data; for(i=0;i<8;i++) { SPI_SCK = 1; //下降沿接收數據,提前拉高電平 _nop_(); if(SPI_SI) Temp_data |= 0x01; _nop_(); SPI_SCK = 0; Temp_data <<= 1; } return Temp_data; }
3.3 芯片說明
存儲型矩陣: 一共有65,536頁,每頁256字節。每次只能寫入一頁,也就是每次只能寫入256個字節。擦除時,只能以16頁為最小單位,也就是一個扇區:16*256bits,一次就要擦8KB。寫入時可以以字節為單位寫入,但是擦除時只能以扇區為單位擦除。芯片支持標准的串行外設接口,比如SPI,也支持雙IO-SPI或者四IO-SPI。
3.4 驅動實現

/****************************************************** //功 能:寫使能 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void Write_Enable(void) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x06); //發送寫使能指令 _nop_(); SPI_CS = 1; } /****************************************************** //功 能:寫禁止 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void Write_Disable(void) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x04); _nop_(); SPI_CS = 1; } /****************************************************** //功 能:字節寫 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void Byte_Write(uint8 Addr,uint16 Wdata) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x02);//發送寫數據止指令 SPI_SendOneByte(Addr); SPI_SendOneByte(Wdata); _nop_(); SPI_CS = 1; } /****************************************************** //功 能:頁寫 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void Page_Write(uint8 Addr,uint8 *s) { SPI_CS = 0; _nop_(); SPI_SendOneByte(0x02); //發送寫數據指令 SPI_SendOneByte(Addr); SPI_SendOneByte(*s++); _nop_(); SPI_CS = 1; } /****************************************************** //功 能:讀一個字節 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ uint8 Byte_Read(uint8 Addr) { uint8 Rdata; SPI_CS = 0; _nop_(); SPI_SendOneByte(0x03); //發送讀數據指令 SPI_SendOneByte(Addr); Rdata = Data_Rece(); _nop_(); SPI_CS = 1; return Rdata; }
第四部分 UART
參考資料:《http://c.biancheng.net/cpp/html/1921.html》《STM32英文參考手冊_V15.pdf》《STM32中文參考手冊_V10.pdf》
4.1 協議說明
首先是對通信的波特率的設定,在這里我們配置的波特率是 9600,那么串口調試助手也得是 9600。配置波特率的時候,我們用的是定時器 T0 的模式 2。模式 2 中,不再是 TH0 代表高 8 位,TL0 代表低 8 位了,而只有TL0 在進行計數,當 TL0 溢出后,不僅僅會讓 TF0 變 1,而且還會將 TH0 中的內容重新自動裝到 TL0 中。這樣有一個好處,就是我們可以把想要的定時器初值提前存在 TH0 中,當 TL0溢出后,TH0 自動把初值就重新送入 TL0 了,全自動的,不需要程序中再給 TL0 重新賦值了,配置方式很簡單,大家可以自己看下程序並且計算一下初值。
波特率設置好以后,打開中斷,然后等待接收串口調試助手下發的數據。接收數據的時候,首先要進行低電平檢測 while (PIN_RXD),若沒有低電平則說明沒有數據,一旦檢測到低電平,就進入啟動接收函數 StartRXD()。接收函數最開始啟動半個波特率周期,初學可能這里不是很明白。大家回頭看一下我們的圖 11-2 里邊的串口數據示意圖,如果在數據位電平變化的時候去讀取,因為時序上的誤差以及信號穩定性的問題很容易讀錯數據,所以我們希望在信號最穩定的時候去讀數據。除了信號變化的那個沿的位置外,其它位置都很穩定,那么我們現在就約定在信號中間位置去讀取電平狀態,這樣能夠保證我們讀的一定是正確的。
一旦讀到了起始信號,我們就把當前狀態設定成接收狀態,並且打開定時器中斷,第一次是半個周期進入中斷后,對起始位進行二次判斷一下,確認一下起始位是低電平,而不是一個干擾信號。以后每經過 1/9600 秒進入一次中斷,並且把這個引腳的狀態讀到 RxdBuf 里邊。等待接收完畢之后,我們再把這個 RxdBuf 加 1,再通過 TXD 引腳發送出去,同樣需要先發一位起始位,然后發 8 個數據位,再發結束位,發送完畢后,程序運行到 while (PIN_RXD),等待第二輪信號接收的開始。
4.2 協議實現
/****************************************************** //版權說明:UART協議實現 //主要內容: //補充說明:無 ******************************************************/ #include <reg52.h> sbit PIN_RXD = P3^0; //接收引腳定義 sbit PIN_TXD = P3^1; //發送引腳定義 bit RxdOrTxd = 0; //指示當前狀態為接收還是發送 bit RxdEnd = 0; //接收結束標志 bit TxdEnd = 0; //發送結束標志 uint8 RxdBuf = 0; //接收緩沖器 uint8 TxdBuf = 0; //發送緩沖器 void ConfigUART(uint16 baud); void StartTXD(uint8 dat); void StartRXD(); /****************************************************** //功 能:主函數,用來測試協議實現效果 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void main() { EA = 1; //開總中斷 ConfigUART(9600); //配置波特率為 9600 while(1) { while(PIN_RXD); //等待接收引腳出現低電平,即起始位 StartRXD(); //啟動接收 while(!RxdEnd); //等待接收完成 StartTXD(RxdBuf+1); //接收到的數據+1 后,發送回去 while(!TxdEnd); //等待發送完成 } } /****************************************************** //功 能:串口配置 //輸入參數:baud-通信波特率 //返 回 值:void //補充說明:無 ******************************************************/ void ConfigUART(uint16 baud) { TMOD &= 0xF0; //清零 T0 的控制位 TMOD |= 0x02; //配置 T0 為模式 2 TH0 = 256 - (11059200/12)/baud; //計算 T0 重載值 } /****************************************************** //功 能:啟動串行接收 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void StartRXD(void) { TL0 = 256 - ((256-TH0)>>1); //接收啟動時的 T0 定時為半個波特率周期 ET0 = 1; //使能 T0 中斷 TR0 = 1; //啟動 T0 RxdEnd = 0; //清零接收結束標志 RxdOrTxd = 0; //設置當前狀態為接收 } /****************************************************** //功 能:啟動串行發送 //輸入參數:dat-待發送字節 //返 回 值:void //補充說明:無 ******************************************************/ void StartTXD(uint8 dat) { TxdBuf = dat; //待發送數據保存到發送緩沖器 TL0 = TH0; //T0 計數初值為重載值 ET0 = 1; //使能 T0 中斷 TR0 = 1; //啟動 T0 PIN_TXD = 0; //發送起始位 TxdEnd = 0; //清零發送結束標志 RxdOrTxd = 1; //設置當前狀態為發送 } /****************************************************** //功 能:T0 中斷服務函數,處理串行發送和接收 //輸入參數:void //返 回 值:void //補充說明:無 ******************************************************/ void InterruptTimer0(void) interrupt 1 { static uint8 cnt = 0; //位接收或發送計數 if(RxdOrTxd) //串行發送處理 { cnt++; if(cnt <= 8) //低位在先依次發送 8bit 數據位 { PIN_TXD = TxdBuf & 0x01; TxdBuf >>= 1; } else if (cnt == 9) PIN_TXD = 1; //發送停止位 else //發送結束 { cnt = 0; //復位 bit 計數器 TR0 = 0; //關閉 T0 TxdEnd = 1; //置發送結束標志 } } else //串行接收處理 { if(cnt == 0) //處理起始位 { if(!PIN_RXD) //起始位為 0 時,清零接收緩沖器,准備接收數據位 { RxdBuf = 0; cnt++; } } else TR0 = 0; //起始位不為 0 時,中止接收。關閉 T0 else if (cnt <= 8) //處理 8 位數據位 { RxdBuf >>= 1; //低位在先,所以將之前接收的位向右移 if (PIN_RXD) RxdBuf |= 0x80; //接收腳為 1 時,緩沖器最高位置 1,而為 0 時不處理即仍保持移位后的 0 cnt++; } else //停止位處理 { cnt = 0; //復位 bit 計數器 TR0 = 0; //關閉 T0 if (PIN_RXD) RxdEnd = 1; //停止位為 1 時,方能認為數據有效,置接收結束標志 } } }
第五部分 串行通信協議匯總比較
| 單總線 | IIC | SPI | UART | |
| 總線數量(不含電源VDD和GND) | 1 | 2 | 4 | 2 |
| 數據同步方式 | 異步 | 同步 | 同步 | 異步 |
| 數據傳輸方向與時間的關系 | 半雙工 | 半雙工 | 全雙工 | 全雙工 |
| 應答與校驗 | 未知 | 有應答,無校驗 | 無應答,無校驗 | 無應答,有校驗 |
| 數據長度限制 | 未知 | 8 | 8 | 8 |
| 通信終端 | 未知 | 一主多從 | 一主多從 | 無主從之分 |
| 通信速率 | 未知 | MB/s | MB/s | KB/s |
| 特性 | 未知 | 有地址位,以及傳送方向位 | 片選信號 | 波特率 |
RS232 串口和 UART 串口,它們的協議類型是一樣的,只是電平標准不同而已,而 MAX232 這個芯片起到的就是中間人的作用,它把 UART 電平轉換成 RS232 電平,也把 RS232 電平轉換成 UART 電平,從而實現標准 RS232接口和單片機 UART 之間的通信連接。隨着技術的發展,工業上還有 RS232 串口通信的大量使用,但是商業技術的應用上,已經慢慢的使用 USB 轉 UART 技術取代了 RS232 串口,絕大多數筆記本電腦已經沒有串口這個東西了,那我們要實現單片機和電腦之間的通信該怎么辦呢?我們只需要在電路上添加一個 USB 轉串口芯片(如CH340),就可以成功實現 USB 通信協議和標准UART 串行通信協議的轉換。
