【STM32 學習筆記】I2C通信協議

注:通信協議的設計背景 3:00~10:13

?? I2C 通訊協議(Inter-Integrated Circuit)是由Phiilps公司開發的,由于它引腳少,硬件實現簡單,可擴展性強, 不需要USART、CAN等通訊協議的外部收發設備,現在被廣泛地使用在系統內多個集成電路(IC)間的通訊。
??I2C總線是一種用于芯片之間進行通信的串行總線。它由兩條線組成:串行時鐘線(SCL)和串行數據線(SDA)。這種總線允許多個設備在同一條總線上進行通信。

物理層

I2C通訊設備之間的常用連接方式見圖
在這里插入圖片描述

I2C通信協議是一種通用的總線協議。I2C通信協議有以下特征:

  • (1) 它是一個支持設備的總線。“總線”指多個設備共用的信號線。在一個I2C通訊總線中, 可連接多個I2C通訊設備,支持多個通訊主機及多個通訊從機。
  • (2) 一個I2C總線只使用兩條總線線路,一條雙向串行數據線(SDA) , 一條串行時鐘線 (SCL)。數據線即用來表示數據,時鐘線用于數據收發同步。
  • (3) 每個連接到總線的設備都有一個獨立的地址, 主機可以利用這個地址進行不同設備之間的訪問。
  • (4) 總線通過上拉電阻接到電源。當I2C設備空閑時,會輸出高阻態, 而當所有設備都空閑,都輸出高阻態時,由上拉電阻把總線拉成高電平。
  • (5) 多個主機同時使用總線時,為了防止數據沖突, 會利用仲裁方式決定由哪個設備占用總線。
  • (6) 具有三種傳輸模式:標準模式傳輸速率為100kbit/s ,快速模式為400kbit/s , 高速模式下可達 3.4Mbit/s,但目前大多I2C設備尚不支持高速模式。
  • (7) 連接到相同總線的 IC 數量受到總線的最大電容 400pF 限制
  • SDA數據線在每個SCL的時鐘周期傳輸一位數據,SCL為高電平的時候SDA表示的數據有效。
  • 應答信號和非應答信號I2C的數據和地址傳輸都帶響應。
    在這里插入圖片描述
    ??一主多從是指單片機作為主機,主導I2C總線的運行。掛在I2C總線上的所有外部模塊都是從機,只有被主機點名后才能控制I2C總線,不能在未經允許的情況下訪問I2C總線,以防止沖突。這就像在課堂上,老師是主機,學生是從機。未經點名允許,學生不能發言,但可以被動地聽老師講課。
    ??另外,I2C還支持多主多從模型,即多個主機。在多主多從模型中,總線上任何一個模塊都可以主動跳出來說,接下來我就是主機,你們都得聽我的。這就像在教室里,老師正在講課,突然一個學生站起來說,打斷一下,接下來讓我來說,所有同學聽我指揮。但是,同一個時間只能有一個人說話,這時就相當于發生了總線沖突。在總線沖突時,I2C協議會進行仲裁,仲裁勝利的一方取得總線控制權,失敗的一方自動變回從機。由于時鐘線也由主機控制,所以在多主機的模型下還要進行時鐘同步。多主機的情況下,協議是比較復雜的。本課程僅使用一主多從模型。
    以上是有關I2C的設計背景和基本功能。接下來我們將詳細分析I2C如何實現這些功能。??作為一個通信協議,I2C必須在硬件和軟件上作出規定硬件上的規定包括電路的連接方式、端口的輸入輸出模式等;軟件上的規定包括時序的定義、字節的傳輸方式、高位先行還是低位先行等。這些硬件和軟件的規定結合起來構成了一個完整的通信協議。

協議層

??I2C的協議定義了通訊的起始和停止信號、數據有效性、響應、仲裁、時鐘同步和地址廣播等環節。

I2C基本讀寫過程

先看看I2C通訊過程的基本結構,它的通訊過程見圖
在這里插入圖片描述

接下來我們先看一下12C的硬件規定,也就是I2C的硬件電路

I2C的硬件電路

在這里插入圖片描述
在這里插入圖片描述

這個圖就是I2C的典型電路模型,這個模型采用了一主多從的結構。在左側,我們可以看到CPU作為主設備,控制著總線并擁有很大的權利。其中,主機對SCL線擁有完全的控制權,無論何時何地,主機都負責掌控SCL線。在空閑狀態下,主機還可以主動發起對SDA的控制。但是,從機發送數據或應答時,主機需要將SDA的控制權轉交給從機。
接下來,我們看到了一系列被控IC,它們是掛載在12C總線上的從機設備,如姿態傳感器、OLED、存儲器、時鐘模塊等。這些從機的權利相對較小。對于SCL時鐘線,它們在任何時刻都只能被動的讀取,不允許控制SCL線對于SDA數據線,從機也不允許主動發起控制,只有在主機發送讀取從機的命令后,或從機應答時,從機才能短暫地取得SDA的控制權。這就是一主多從模型中協議的規定。
然后我們來看接線部分。所有I2C設備的SCL和SDA都連接在一起。主機的SCL線拉出來,所有從機的SCL都接在這上面。主機的SDA線也是一樣,拉出來,所有從機的SDA接在這上面。這就是SCL和SDA的接線方式。
那到現在,我們先不繼續往后看了,先忽略這兩個電阻,那到現在,假設我們就這樣連接,那如何規定每個設備SCL和SDA的輸入輸出模式呢?
由于現在是一主多從結構,主機擁有SCL的絕對控制權,因此主機的SCL可以配置成推挽輸出,所有從機的SCL都配置成浮空輸入或上拉輸入。數據流向為主機發送、所有從機接收。但是到SDA線這里就比較復雜了,因為這是半雙工協議,所以主機的SDA在發送時是輸出,在接收時是輸入。同樣地,從機的SDA也會在輸入和輸出之間反復切換。如果能夠協調好輸入輸出的切換時機就沒有問題。但是這樣做的話,如果總線時序沒有協調好,就極有可能發生兩個引腳同時處于輸出的狀態。如果此時一個引腳輸出高電平,一個引腳輸出低電平,就會造成電源短路的情況,這是要極力避免的。
為了避免這種情況的發生,I2C的設計規定所有設備不輸出強上拉的高電平,而是采用外置弱上拉電阻加開漏輸出的電路結構。這兩點規定對應于前面提到的“設備的SCL和SDA均要配置成開漏輸出模式”以及“SCL和SDA各添加一個上拉電阻,阻值一般為4.7KΩ左右”。對應上面這個圖。
在這里插入圖片描述

所有的設備,包括CPU和被控IC,它們的引腳內部結構都如上圖所示。圖左側展示的是SCL的結構,其中SClk代表SCL;右側則是SDA的結構,其中DATA代表SDA。引腳的信號輸入都可以通過一個數據緩沖器或施密特觸發器進行,因為輸入對電路無影響,所以任何設備在任何時刻都可以輸入。然而,在輸出部分,采用的是開漏輸出的配置。
在這里插入圖片描述
正常的推挽輸出方式如下:上面一個開關管連接正極,下面一個開關管連接負極。當上面導通時,輸出高電平;下面導通時,輸出低電平。因為這是通過開關管直接連接到正負極的,所以這是強上拉和強下拉的模式。
而開漏輸出呢,就是去掉這個強上拉的開關管,輸出低電平時,下管導通,是強下拉,輸出高電平時,下管斷開,但是沒有上管了,此時引腳處于浮空的狀態,這就是開漏輸出。
在這里插入圖片描述
和這里圖示是一樣的,輸出低電平,這個開關管導通,引腳直接接地,是強下拉,輸出高電平,這個開關管斷開,引腳什么都不接,處于浮空狀態,這樣的話,所有的設備都只能輸出低電平而不能輸出高電平,為了避免高電平造成的引腳浮空,這時就需要在總線外面,SCL和SDA各外置一個上拉電阻,這是通過一個電阻拉到高電平的,所以這是一個弱上拉。
UP主用彈簧和桿子的模型解釋這一段硬件電路設計
這樣做的好處是:

  • 第一,完全杜絕了電源短路現象,保證電路的安全。你看所有人無論怎么拉桿子或者放手,桿子都不會處于一個被同時強拉和強推的狀態,即使有多個人同時往下拉桿子,也沒問題
  • 第二,避免了引腳模式的頻繁切換。開漏加弱上拉的模式,同時兼具了輸入和輸出的功能,你要是想輸出,就去拉桿子或放手,操作桿子變化就行了,你要是想輸入,就直接放手,然后觀察桿子高低就行了,因為開漏模式下,輸出高電平就相當于斷開引腳,所以在輸入之前,可以直接輸出高電平,不需要再切換成輸入模式了
  • 第三,就是這個模式會有一個“線與”的現象。就是只要有任意一個或多個設備輸出了低電平,總線就處于低電平,只有所有設備都輸出高電平,總線才處于高電平。I2C可以利用這個電路特性執行多主機模式下的時鐘同步和總線仲裁,所以這里SCL雖然在一主多從模式下可以用推挽輸出,但是它仍然采用了開漏加上拉輸出的模式,因為在多主機模式下會利用到這個特征

好,以上就是I2C的硬件電路設計,那接下來,我們就要來學習軟件,也就是時序的設計了。

I2C時序設計

首先我們來學習一下I2C規定的一些時序基本單元。

起始和終止條件

在這里插入圖片描述
起始條件是指SCL高電平期間,SDA從高電平切換到低電平。在I2C總線處于空閑狀態時,SCL和SDA都處于高電平狀態,由外掛的上拉電阻保持。當主機需要數據收發時,會首先產生一個起始條件。這個起始條件是,SCL保持高電平,然后把SDA拉低,產生一個下降沿。當從機捕獲到這個SCL高電平,SDA下降沿信號時,就會進行自身的復位,等待主機的召喚。之后,主機需要將SCL拉低。這樣做一方面是占用這個總線,另一方面也是為了方便這些基本單元的拼接。這樣,除了起始和終止條件,每個時序單元的SCL都是以低電平開始,低電平結束。

終止條件是,SCL高電平期間,SDA從低電平切換到高電平。SCL先放開并回彈到高電平,SDA再放開并回彈高電平,產生一個上升沿。這個上升沿觸發終止條件,同時終止條件之后,SCL和SDA都是高電平,回歸到最初的平靜狀態。這個起始條件和終止條件就類似串口時序里的起始位和停止位。一個完整的數據幀總是以起始條件開始、終止條件結束。另外,起始和終止都是由主機產生的。因此,從機必須始終保持雙手放開,不允許主動跳出來去碰總線。如果允許從機這樣做,那么就會變成多主機模型,不在本節的討論范圍之內。這就是起始條件和終止條件的含義。

發送一個字節

接著繼續看,在起始條件之后,這時就可以緊跟著一個發送一個字節的時序單元,如何發送一個字節呢
在這里插入圖片描述

就這樣的流程,主機拉低SCL,把數據放在SDA上,主機松開SCL,從機讀取SDA的數據,在SCL的同步下,依次進行主機發送和從機接收,循環8次,就發送了8位數據,也就是一個字節,另外注意,這里是高位先行,所以第一位是一個字節的最高位B7,然后依次是次高位B6…

接收一個字節

在這里插入圖片描述
那我們再繼續看最后兩個基本單元,就是應答機制的設計。

發送應答和接收應答

在這里插入圖片描述
發一字節收一位,收一字節發一位

應用:

I2C從機地址

12C的完整時序,主要有指定地址寫,當前地址讀和指定地址讀這3種。
首先注意的是,我們這個12C是一主多從的模型,主機可以訪問總線上的任何一個設備,那如何發出指令,來確定要訪問的是哪個設備呢?
為了解決這個問題,我們需要為每個從設備分配一個唯一的設備地址。這些地址就像是每個設備的名字,主機通過發送這些地址來確定要與哪個設備通信。

當主機發送一個地址時,所有的從設備都會收到這個地址。但是,只有與發送的地址匹配的設備會響應主機的讀寫操作。
在I2C總線中,每個掛載的設備的地址必須是唯一的,否則當主機發送一個地址時,多個設備響應,就會導致混亂。
在12C協議標準中,從機設備地址分為7位和10位兩種。我們今天主要討論7位地址,因為它們相對簡單且應用廣泛。
在這里插入圖片描述

每個I2C設備在出廠時都會被分配一個7位的地址。例如,MPU6050的7位地址是1101 000,而AT24C02的7位地址是1010 000。不同型號的芯片地址是不同的,但相同型號的芯片地址是相同的。
如果多個相同型號的芯片掛載在同一條總線上,我們可以通過調整地址的最后幾位來解決這個問題。例如,MPU6050的地址可以通過ADO引腳來改變,而AT24C02的地址可以通過A0、A1、A2引腳來改變。這樣,即使相同型號的芯片,掛載在同一個總線上,也可以通過切換地址低位的方式,保證每個設備的地址都是唯一的。這就是12C設備的從機地址。

下面時序講解詳情
注意:時序里面的RA是接收從機的應答位(Receive Ack, RA)

指定地址寫

在這里插入圖片描述
在這里插入圖片描述
(Sláve Address + R/W) 中最后一位 0=W(寫),根據協議規定,緊跟著的單元,就得是接收從機的應答位(Receive Ack, RA),在這個時刻,主機要釋放SDA,
所以如果單看主機的波形,應該是這樣,
在這里插入圖片描述

釋放SDA之后,引腳電平回彈到高電平,但是根據協議規定,從機要在這個位拉低SDA,所以單看從機的波形,應該是這樣(綠色線)
在這里插入圖片描述
該應答的時候,從機立刻拽住SDA,然后應答結束之后,從機再放開SDA,那現在綜合兩者的波形,結合線與的特性,在主機釋放SDA之后,由于SDA也被從機拽住了,所以主機松手后,SDA并沒有回彈高電平,這個過程,就代表從機產生了應答。最終高電平期間,主機讀取SDA,發現是0,就說明,我進行尋址,有人給我應答了。如果主機讀取SDA,發現是1,就說明,我進行尋址,應答位期間,我松手了,但是沒人拽住它,沒人給我應答,那就直接產生停止條件吧,并提示一些信息,這就是應答位。
然后這個上升沿,就是應答位結束后,從機釋放SDA產生的,從機交出了SDA的控制權,因為從機要在低電平盡快變換數據,所以這個上升沿和SCL的下降沿,幾乎是同時發生的。

當前地址讀

在這里插入圖片描述
在這里插入圖片描述

指定地址讀

指定地址讀=指定地址寫+當前地址讀
在這里插入圖片描述

在這里插入圖片描述
Sr (Start Repeat)的意思就是重復起始條件,因為指定讀寫標志位只能是跟著起始條件的第一個字節,所以如果想切換讀寫方向,只能再來個起始條件。然后起始條件后,重新尋址并且指定讀寫標志位

代碼實戰:10-1 軟件I2C讀寫MPU6050

在這里插入圖片描述
由于我們這個代碼使用的是軟件I2C,就是用普通的GPIO口,手動翻轉電平實現的協議,它并不需要STM32內部的外設資源支持,所以這里的端口(SDA,SCL),其實可以任意指定,不局限于這兩個端口,你也可以SCL接PAO,SDA接PB12,或者SCL接PA8,SDA接PA9看,等等等等,接在任意的兩個普通的GPIO口就可以。
軟件I2C,只需要用gpio的讀寫函數就行了,就不用I2C的庫函數了。

程序的整體框架:
在這里插入圖片描述
MyI2C.h

#ifndef __MYI2C_H
#define __MYI2C_Hvoid MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);#endif

MyI2C.C

#include "stm32f10x.h"                  // Device header
#include "Delay.h"/*引腳配置層*//*** 函    數:I2C寫SCL引腳電平* 參    數:BitValue 協議層傳入的當前需要寫入SCL的電平,范圍0~1* 返 回 值:無* 注意事項:此函數需要用戶實現內容,當BitValue為0時,需要置SCL為低電平,當BitValue為1時,需要置SCL為高電平*/
void MyI2C_W_SCL(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根據BitValue,設置SCL引腳的電平Delay_us(10);												//延時10us,防止時序頻率超過要求
}/*** 函    數:I2C寫SDA引腳電平* 參    數:BitValue 協議層傳入的當前需要寫入SDA的電平,范圍0~0xFF* 返 回 值:無* 注意事項:此函數需要用戶實現內容,當BitValue為0時,需要置SDA為低電平,當BitValue非0時,需要置SDA為高電平*/
void MyI2C_W_SDA(uint8_t BitValue)
{GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		//根據BitValue,設置SDA引腳的電平,BitValue要實現非0即1的特性Delay_us(10);												//延時10us,防止時序頻率超過要求
}/*** 函    數:I2C讀SDA引腳電平* 參    數:無* 返 回 值:協議層需要得到的當前SDA的電平,范圍0~1* 注意事項:此函數需要用戶實現內容,當前SDA為低電平時,返回0,當前SDA為高電平時,返回1*/
uint8_t MyI2C_R_SDA(void)
{uint8_t BitValue;BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);		//讀取SDA電平Delay_us(10);												//延時10us,防止時序頻率超過要求return BitValue;											//返回SDA電平
}/*** 函    數:I2C初始化* 參    數:無* 返 回 值:無* 注意事項:此函數需要用戶實現內容,實現SCL和SDA引腳的初始化*/
void MyI2C_Init(void)
{/*開啟時鐘*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//開啟GPIOB的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);					//將PB10和PB11引腳初始化為開漏輸出/*設置默認電平*/GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			//設置PB10和PB11引腳初始化后默認為高電平(釋放總線狀態)
}/*協議層*//*** 函    數:I2C起始* 參    數:無* 返 回 值:無*/
void MyI2C_Start(void)
{MyI2C_W_SDA(1);							//釋放SDA,確保SDA為高電平MyI2C_W_SCL(1);							//釋放SCL,確保SCL為高電平MyI2C_W_SDA(0);							//在SCL高電平期間,拉低SDA,產生起始信號MyI2C_W_SCL(0);							//起始后把SCL也拉低,即為了占用總線,也為了方便總線時序的拼接
}/*** 函    數:I2C終止* 參    數:無* 返 回 值:無*/
void MyI2C_Stop(void)
{MyI2C_W_SDA(0);							//拉低SDA,確保SDA為低電平MyI2C_W_SCL(1);							//釋放SCL,使SCL呈現高電平MyI2C_W_SDA(1);							//在SCL高電平期間,釋放SDA,產生終止信號
}/*** 函    數:I2C發送一個字節* 參    數:Byte 要發送的一個字節數據,范圍:0x00~0xFF* 返 回 值:無*/
void MyI2C_SendByte(uint8_t Byte)
{uint8_t i;for (i = 0; i < 8; i ++)				//循環8次,主機依次發送數據的每一位{MyI2C_W_SDA(Byte & (0x80 >> i));	//使用掩碼的方式取出Byte的指定一位數據并寫入到SDA線MyI2C_W_SCL(1);						//釋放SCL,從機在SCL高電平期間讀取SDAMyI2C_W_SCL(0);						//拉低SCL,主機開始發送下一位數據}
}/*** 函    數:I2C接收一個字節* 參    數:無* 返 回 值:接收到的一個字節數據,范圍:0x00~0xFF*/
uint8_t MyI2C_ReceiveByte(void)
{uint8_t i, Byte = 0x00;					//定義接收的數據,并賦初值0x00,此處必須賦初值0x00,后面會用到MyI2C_W_SDA(1);							//接收前,主機先確保釋放SDA,避免干擾從機的數據發送for (i = 0; i < 8; i ++)				//循環8次,主機依次接收數據的每一位{MyI2C_W_SCL(1);						//釋放SCL,主機機在SCL高電平期間讀取SDAif (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//讀取SDA數據,并存儲到Byte變量//當SDA為1時,置變量指定位為1,當SDA為0時,不做處理,指定位為默認的初值0MyI2C_W_SCL(0);						//拉低SCL,從機在SCL低電平期間寫入SDA}return Byte;							//返回接收到的一個字節數據
}/*** 函    數:I2C發送應答位* 參    數:Byte 要發送的應答位,范圍:0~1,0表示應答,1表示非應答* 返 回 值:無*/
void MyI2C_SendAck(uint8_t AckBit)
{MyI2C_W_SDA(AckBit);					//主機把應答位數據放到SDA線MyI2C_W_SCL(1);							//釋放SCL,從機在SCL高電平期間,讀取應答位MyI2C_W_SCL(0);							//拉低SCL,開始下一個時序模塊
}/*** 函    數:I2C接收應答位* 參    數:無* 返 回 值:接收到的應答位,范圍:0~1,0表示應答,1表示非應答*/
uint8_t MyI2C_ReceiveAck(void)
{uint8_t AckBit;							//定義應答位變量MyI2C_W_SDA(1);							//接收前,主機先確保釋放SDA,避免干擾從機的數據發送MyI2C_W_SCL(1);							//釋放SCL,主機機在SCL高電平期間讀取SDAAckBit = MyI2C_R_SDA();					//將應答位存儲到變量里MyI2C_W_SCL(0);							//拉低SCL,開始下一個時序模塊return AckBit;							//返回定義應答位變量
}

函數邏輯:

  • void MyI2C_Start(void)
    在這里插入圖片描述
    如果起始條件之前SCL和SDA已經是高電平了,那先釋放哪一個是一樣的效果,但是在指定地址讀中,為了改變讀寫標志位,我們這個Start還要兼容這里的重復起始條件Sr。
    在這里插入圖片描述
    Sr最開始,SCL是低電平,SDA電平不敢確定,所以保險起見,我們趁SCL是低電平,先確保釋放SDA,再釋放SCL,這時SDA和SCL都是高電平,然后再拉低SDA、拉低SCL,這樣這個Start就可以兼容起始條件和重復起始條件了。
    【如果先釋放SCL,在SCL高電平期間再釋放SDA會被誤以為是終止條件;這里Sr是需要重新生成一個開始條件即SCL高電平期間,SDA從高變低。如果不先拉低SDA,就容易造成。SCL高電平期間,SDA從低變高。變成結束信號了。】

  • void MyI2C_Stop(void)
    在這里插入圖片描述
    在這里插入圖片描述
    在這里,如果Stop開始時,那就先釋放SCL,再釋放SDA就行了,但是在這個時序單元開始時,SDA并不一定是低電平,所以為了確保之后釋放SDA能產生上升沿,我們要在時序單元開始時,先拉低SDA,然后再釋放SCL、釋放SDA。

  • void MyI2C_SendByte(uint8_t Byte)
    在這里插入圖片描述
    發送一個字節時序開始時,SCL是低電平,實際上,除了終止條件,SCL以高電平結束,所有的單元我們都會保證SCL以低電平結束,這樣方便各個單元的拼接。
    補充:
    Byte & 0x80 就是用按位與的方式,取出數據的某一位或某幾位,感覺這里準確的講是檢查位是否為1,而不是取出最高位在這里插入圖片描述

MPU6050_Reg.h

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H#define	MPU6050_SMPLRT_DIV		0x19
#define	MPU6050_CONFIG			0x1A
#define	MPU6050_GYRO_CONFIG		0x1B
#define	MPU6050_ACCEL_CONFIG	0x1C#define	MPU6050_ACCEL_XOUT_H	0x3B
#define	MPU6050_ACCEL_XOUT_L	0x3C
#define	MPU6050_ACCEL_YOUT_H	0x3D
#define	MPU6050_ACCEL_YOUT_L	0x3E
#define	MPU6050_ACCEL_ZOUT_H	0x3F
#define	MPU6050_ACCEL_ZOUT_L	0x40
#define	MPU6050_TEMP_OUT_H		0x41
#define	MPU6050_TEMP_OUT_L		0x42
#define	MPU6050_GYRO_XOUT_H		0x43
#define	MPU6050_GYRO_XOUT_L		0x44
#define	MPU6050_GYRO_YOUT_H		0x45
#define	MPU6050_GYRO_YOUT_L		0x46
#define	MPU6050_GYRO_ZOUT_H		0x47
#define	MPU6050_GYRO_ZOUT_L		0x48#define	MPU6050_PWR_MGMT_1		0x6B
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75#endif

MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_Hvoid MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);#endif

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"#define MPU6050_ADDRESS		0xD0		//MPU6050的I2C從機地址/*** 函    數:MPU6050寫寄存器* 參    數:RegAddress 寄存器地址,范圍:參考MPU6050手冊的寄存器描述* 參    數:Data 要寫入寄存器的數據,范圍:0x00~0xFF* 返 回 值:無*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{MyI2C_Start();						//I2C起始MyI2C_SendByte(MPU6050_ADDRESS);	//發送從機地址,讀寫位為0,表示即將寫入MyI2C_ReceiveAck();					//接收應答MyI2C_SendByte(RegAddress);			//發送寄存器地址MyI2C_ReceiveAck();					//接收應答MyI2C_SendByte(Data);				//發送要寫入寄存器的數據MyI2C_ReceiveAck();					//接收應答MyI2C_Stop();						//I2C終止
}/*** 函    數:MPU6050讀寄存器* 參    數:RegAddress 寄存器地址,范圍:參考MPU6050手冊的寄存器描述* 返 回 值:讀取寄存器的數據,范圍:0x00~0xFF*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data;MyI2C_Start();						//I2C起始MyI2C_SendByte(MPU6050_ADDRESS);	//發送從機地址,讀寫位為0,表示即將寫入MyI2C_ReceiveAck();					//接收應答MyI2C_SendByte(RegAddress);			//發送寄存器地址MyI2C_ReceiveAck();					//接收應答MyI2C_Start();						//I2C重復起始MyI2C_SendByte(MPU6050_ADDRESS | 0x01);	//發送從機地址,讀寫位為1,表示即將讀取MyI2C_ReceiveAck();					//接收應答Data = MyI2C_ReceiveByte();			//接收指定寄存器的數據MyI2C_SendAck(1);					//發送應答,給從機非應答,終止從機的數據輸出MyI2C_Stop();						//I2C終止return Data;
}/*** 函    數:MPU6050初始化* 參    數:無* 返 回 值:無*/
void MPU6050_Init(void)
{MyI2C_Init();									//先初始化底層的I2C/*MPU6050寄存器初始化,需要對照MPU6050手冊的寄存器描述配置,此處僅配置了部分重要的寄存器*/MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);		//電源管理寄存器1,取消睡眠模式,選擇時鐘源為X軸陀螺儀MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);		//電源管理寄存器2,保持默認值0,所有軸均不待機MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);		//采樣率分頻寄存器,配置采樣率MPU6050_WriteReg(MPU6050_CONFIG, 0x06);			//配置寄存器,配置DLPFMPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);	//陀螺儀配置寄存器,選擇滿量程為±2000°/sMPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);	//加速度計配置寄存器,選擇滿量程為±16g
}/*** 函    數:MPU6050獲取ID號* 參    數:無* 返 回 值:MPU6050的ID號*/
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}/*** 函    數:MPU6050獲取數據* 參    數:AccX AccY AccZ 加速度計X、Y、Z軸的數據,使用輸出參數的形式返回,范圍:-32768~32767* 參    數:GyroX GyroY GyroZ 陀螺儀X、Y、Z軸的數據,使用輸出參數的形式返回,范圍:-32768~32767* 返 回 值:無*/
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{uint8_t DataH, DataL;								//定義數據高8位和低8位的變量DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);		//讀取加速度計X軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);		//讀取加速度計X軸的低8位數據*AccX = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);		//讀取加速度計Y軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);		//讀取加速度計Y軸的低8位數據*AccY = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);		//讀取加速度計Z軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);		//讀取加速度計Z軸的低8位數據*AccZ = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);		//讀取陀螺儀X軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);		//讀取陀螺儀X軸的低8位數據*GyroX = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);		//讀取陀螺儀Y軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);		//讀取陀螺儀Y軸的低8位數據*GyroY = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);		//讀取陀螺儀Z軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);		//讀取陀螺儀Z軸的低8位數據*GyroZ = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"uint8_t ID;								//定義用于存放ID號的變量
int16_t AX, AY, AZ, GX, GY, GZ;			//定義用于存放各個數據的變量int main(void)
{/*模塊初始化*/OLED_Init();		//OLED初始化MPU6050_Init();		//MPU6050初始化/*顯示ID號*/OLED_ShowString(1, 1, "ID:");		//顯示靜態字符串ID = MPU6050_GetID();				//獲取MPU6050的ID號OLED_ShowHexNum(1, 4, ID, 2);		//OLED顯示ID號while (1){MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);		//獲取MPU6050的數據OLED_ShowSignedNum(2, 1, AX, 5);					//OLED顯示數據OLED_ShowSignedNum(3, 1, AY, 5);OLED_ShowSignedNum(4, 1, AZ, 5);OLED_ShowSignedNum(2, 8, GX, 5);OLED_ShowSignedNum(3, 8, GY, 5);OLED_ShowSignedNum(4, 8, GZ, 5);}
}

那之前的課程我們用的是軟件I2C,手動拉低或釋放時鐘線,然后再手動對每個數據位進行判斷,拉低或釋放數據線,這樣來產生這個的波形,這是軟件I2C。由于12C是同步時序,這每一位的持續時間要求不嚴格,或許中途暫停一下時序,影響都不大,所以2C是比較容易用軟件模擬的。
在實際項目中,軟件模擬的I2C也是非常常見的,但是作為一個協議標準,I2C通信,也是可以有硬件收發電路的。就像之前的串口通信一樣,我們先講了串口的時序波形,但是在程序中,我們并沒有用軟件去手動翻轉電平來實現這個波形,這是因為串口是異步時序,每一位的時間要求很嚴格,不能過長也不能過短,所以串口時序雖然可以用軟件模擬,但是操作起來比較困難。另外,由于串口的硬件收發器在單片機中的普及程度非常高,基本上每個單片機都有串口的硬件資源,而且硬件實現的串口使用起來還非常簡單,所以,串口通信,我們基本都是借助硬件收發器來實現的。

I2C通信外設

硬件實現串口(USART)的使用流程:首先配置USART外設,然后寫入數據寄存器DR,然后硬件收發器就會自動生成波形發送出去,最后我們等待發送完成的標志位即可。
回到I2C這里,I2C也可以有軟件模擬和硬件收發器自動操作這兩種異步時序,對于串口這樣的異步時序,軟件實現麻煩,硬件實現簡單,所以串口的實現基本是全部倒向硬件。而對于I2C這樣的同步時序來說,軟件實現簡單靈活,硬件實現麻煩,但可以節省軟件資源、可以實現完整的多主機通信模型等,各有優缺點。

在這里插入圖片描述

I2C框圖

在這里插入圖片描述

I2C基本結構

在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述
在這里插入圖片描述

代碼實戰:10-2硬件I2C讀寫MPU6050

MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_Hvoid MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);#endif

MPU6050_REG.h

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H#define	MPU6050_SMPLRT_DIV		0x19
#define	MPU6050_CONFIG			0x1A
#define	MPU6050_GYRO_CONFIG		0x1B
#define	MPU6050_ACCEL_CONFIG	0x1C#define	MPU6050_ACCEL_XOUT_H	0x3B
#define	MPU6050_ACCEL_XOUT_L	0x3C
#define	MPU6050_ACCEL_YOUT_H	0x3D
#define	MPU6050_ACCEL_YOUT_L	0x3E
#define	MPU6050_ACCEL_ZOUT_H	0x3F
#define	MPU6050_ACCEL_ZOUT_L	0x40
#define	MPU6050_TEMP_OUT_H		0x41
#define	MPU6050_TEMP_OUT_L		0x42
#define	MPU6050_GYRO_XOUT_H		0x43
#define	MPU6050_GYRO_XOUT_L		0x44
#define	MPU6050_GYRO_YOUT_H		0x45
#define	MPU6050_GYRO_YOUT_L		0x46
#define	MPU6050_GYRO_ZOUT_H		0x47
#define	MPU6050_GYRO_ZOUT_L		0x48#define	MPU6050_PWR_MGMT_1		0x6B
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75#endif

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"#define MPU6050_ADDRESS		0xD0		//MPU6050的I2C從機地址/*** 函    數:MPU6050等待事件* 參    數:同I2C_CheckEvent* 返 回 值:無*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{uint32_t Timeout;Timeout = 10000;									//給定超時計數時間while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)	//循環等待指定事件{Timeout --;										//等待時,計數值自減if (Timeout == 0)								//自減到0后,等待超時{/*超時的錯誤處理代碼,可以添加到此處*/break;										//跳出等待,不等了}}
}/*** 函    數:MPU6050寫寄存器* 參    數:RegAddress 寄存器地址,范圍:參考MPU6050手冊的寄存器描述* 參    數:Data 要寫入寄存器的數據,范圍:0x00~0xFF* 返 回 值:無*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始條件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C發送從機地址,方向為發送MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6I2C_SendData(I2C2, RegAddress);											//硬件I2C發送寄存器地址MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);			//等待EV8I2C_SendData(I2C2, Data);												//硬件I2C發送數據MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2I2C_GenerateSTOP(I2C2, ENABLE);											//硬件I2C生成終止條件
}/*** 函    數:MPU6050讀寄存器* 參    數:RegAddress 寄存器地址,范圍:參考MPU6050手冊的寄存器描述* 返 回 值:讀取寄存器的數據,范圍:0x00~0xFF*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{uint8_t Data;I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成起始條件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);	//硬件I2C發送從機地址,方向為發送MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);	//等待EV6I2C_SendData(I2C2, RegAddress);											//硬件I2C發送寄存器地址MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);				//等待EV8_2I2C_GenerateSTART(I2C2, ENABLE);										//硬件I2C生成重復起始條件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);					//等待EV5I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);		//硬件I2C發送從機地址,方向為接收MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);		//等待EV6I2C_AcknowledgeConfig(I2C2, DISABLE);									//在接收最后一個字節之前提前將應答失能I2C_GenerateSTOP(I2C2, ENABLE);											//在接收最后一個字節之前提前申請停止條件MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);				//等待EV7Data = I2C_ReceiveData(I2C2);											//接收數據寄存器I2C_AcknowledgeConfig(I2C2, ENABLE);									//將應答恢復為使能,為了不影響后續可能產生的讀取多字節操作return Data;
}/*** 函    數:MPU6050初始化* 參    數:無* 返 回 值:無*/
void MPU6050_Init(void)
{/*開啟時鐘*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);		//開啟I2C2的時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		//開啟GPIOB的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOB, &GPIO_InitStructure);					//將PB10和PB11引腳初始化為復用開漏輸出/*I2C初始化*/I2C_InitTypeDef I2C_InitStructure;						//定義結構體變量I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;				//模式,選擇為I2C模式I2C_InitStructure.I2C_ClockSpeed = 50000;				//時鐘速度,選擇為50KHzI2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;		//時鐘占空比,選擇Tlow/Thigh = 2I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;				//應答,選擇使能I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	//應答地址,選擇7位,從機模式下才有效I2C_InitStructure.I2C_OwnAddress1 = 0x00;				//自身地址,從機模式下才有效I2C_Init(I2C2, &I2C_InitStructure);						//將結構體變量交給I2C_Init,配置I2C2/*I2C使能*/I2C_Cmd(I2C2, ENABLE);									//使能I2C2,開始運行/*MPU6050寄存器初始化,需要對照MPU6050手冊的寄存器描述配置,此處僅配置了部分重要的寄存器*/MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);				//電源管理寄存器1,取消睡眠模式,選擇時鐘源為X軸陀螺儀MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);				//電源管理寄存器2,保持默認值0,所有軸均不待機MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);				//采樣率分頻寄存器,配置采樣率MPU6050_WriteReg(MPU6050_CONFIG, 0x06);					//配置寄存器,配置DLPFMPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);			//陀螺儀配置寄存器,選擇滿量程為±2000°/sMPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);			//加速度計配置寄存器,選擇滿量程為±16g
}/*** 函    數:MPU6050獲取ID號* 參    數:無* 返 回 值:MPU6050的ID號*/
uint8_t MPU6050_GetID(void)
{return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}/*** 函    數:MPU6050獲取數據* 參    數:AccX AccY AccZ 加速度計X、Y、Z軸的數據,使用輸出參數的形式返回,范圍:-32768~32767* 參    數:GyroX GyroY GyroZ 陀螺儀X、Y、Z軸的數據,使用輸出參數的形式返回,范圍:-32768~32767* 返 回 值:無*/
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{uint8_t DataH, DataL;								//定義數據高8位和低8位的變量DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);		//讀取加速度計X軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);		//讀取加速度計X軸的低8位數據*AccX = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);		//讀取加速度計Y軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);		//讀取加速度計Y軸的低8位數據*AccY = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);		//讀取加速度計Z軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);		//讀取加速度計Z軸的低8位數據*AccZ = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);		//讀取陀螺儀X軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);		//讀取陀螺儀X軸的低8位數據*GyroX = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);		//讀取陀螺儀Y軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);		//讀取陀螺儀Y軸的低8位數據*GyroY = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);		//讀取陀螺儀Z軸的高8位數據DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);		//讀取陀螺儀Z軸的低8位數據*GyroZ = (DataH << 8) | DataL;						//數據拼接,通過輸出參數返回
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"uint8_t ID;								//定義用于存放ID號的變量
int16_t AX, AY, AZ, GX, GY, GZ;			//定義用于存放各個數據的變量int main(void)
{/*模塊初始化*/OLED_Init();		//OLED初始化MPU6050_Init();		//MPU6050初始化/*顯示ID號*/OLED_ShowString(1, 1, "ID:");		//顯示靜態字符串ID = MPU6050_GetID();				//獲取MPU6050的ID號OLED_ShowHexNum(1, 4, ID, 2);		//OLED顯示ID號while (1){MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);		//獲取MPU6050的數據OLED_ShowSignedNum(2, 1, AX, 5);					//OLED顯示數據OLED_ShowSignedNum(3, 1, AY, 5);OLED_ShowSignedNum(4, 1, AZ, 5);OLED_ShowSignedNum(2, 8, GX, 5);OLED_ShowSignedNum(3, 8, GY, 5);OLED_ShowSignedNum(4, 8, GZ, 5);}
}

SPI通信協議

??SPI協議是由摩托羅拉公司提出的通訊協議(Serial Peripheral Interface),即串行外圍設備接口, 是一種高速全雙工的通信總線。它被廣泛地使用在ADC、LCD等設備與MCU間,要求通訊速率較高的場合。
??學習本章時,可與I2C章節對比閱讀,體會兩種通訊總線的差異以及EEPROM存儲器與FLASH存儲器的區別。下面我們分別對SPI協議的物理層及協議層進行講解。

SPI物理層

SPI通訊設備之間的常用連接方式見圖
在這里插入圖片描述

在這里插入圖片描述
??SPI通訊使用3條總線及片選線,3條總線分別為SCK、MOSI、MISO,片選線為SS,它們的作用介紹如下:

(1) SS ( Slave Select):從設備選擇信號線,常稱為片選信號線,也稱為NSS、CS,以下用NSS表示。當有多個SPI從設備與SPI主機相連時, 設備的其它信號線SCK、MOSI及MISO同時并聯到相同的SPI總線上,即無論有多少個從設備,都共同只使用這3條總線; 而每個從設備都有獨立的這一條NSS信號線,本信號線獨占主機的一個引腳,即有多少個從設備,就有多少條片選信號線。 I2C協議中通過設備地址來尋址、選中總線上的某個設備并與其進行通訊;而SPI協議中沒有設備地址,它使用NSS信號線來尋址當主機要選擇從設備時,把該從設備的NSS信號線設置為低電平,該從設備即被選中,即片選有效, 接著主機開始與被選中的從設備進行SPI通訊。所以SPI通訊以NSS線置低電平為開始信號,以NSS線被拉高作為結束信號

(2) SCK (Serial Clock):時鐘信號線,用于通訊數據同步。它由通訊主機產生,決定了通訊的速率,不同的設備支持的最高時鐘頻率不一樣, 如STM32的SPI時鐘頻率最大為fpclk/2,兩個設備之間通訊時,通訊速率受限于低速設備。

(3) MOSI (Master Output, Slave Input):主設備輸出/從設備輸入引腳。主機的數據從這條信號線輸出, 從機由這條信號線讀入主機發送的數據,即這條線上數據的方向為主機到從機。

(4) MISO (Master Input,,Slave Output):主設備輸入/從設備輸出引腳。主機從這條信號線讀入數據, 從機的數據由這條信號線輸出到主機,即在這條線上數據的方向為從機到主機。

SPI協議層

與I2C的類似,SPI協議定義了通訊的起始和停止信號、數據有效性、時鐘同步等環節。

SPI基本通訊過程

先看看SPI通訊的通訊時序,
在這里插入圖片描述
??這是一個主機的通訊時序。NSS、SCK、MOSI信號都由主機控制產生,而MISO的信號由從機產生,主機通過該信號線讀取從機的數據。 MOSI與MISO的信號只在NSS為低電平的時候才有效,在SCK的每個時鐘周期MOSI和MISO傳輸一位數據。

以上通訊流程中包含的各個信號分解如下:

1.通訊的起始和停止信號

??在圖 SPI通訊時序 中的標號處,NSS信號線由高變低,是SPI通訊的起始信號。NSS是每個從機各自獨占的信號線, 當從機在自己的NSS線檢測到起始信號后,就知道自己被主機選中了,開始準備與主機通訊。在圖中的標號處,NSS信號由低變高, 是SPI通訊的停止信號,表示本次通訊結束,從機的選中狀態被取消。

2.數據有效性

MSB高位先行,LSB低位先行

??SPI使用MOSI及MISO信號線來傳輸數據,使用SCK信號線進行數據同步。MOSI及MISO數據線在SCK的每個時鐘周期傳輸一位數據, 且數據輸入輸出是同時進行的。數據傳輸時,MSB先行或LSB先行并沒有作硬性規定,但要保證兩個SPI通訊設備之間使用同樣的協定, 一般都會采用圖 SPI通訊時序 中的MSB先行模式。
??觀察圖中的標號處,MOSI及MISO的數據在SCK的上升沿期間變化輸出,在SCK的下降沿時被采樣。即在SCK的下降沿時刻, MOSI及MISO的數據有效,高電平時表示數據“1”,為低電平時表示數據“0”。在其它時刻,數據無效,MOSI及MISO為下一次表示數據做準備。
??SPI每次數據傳輸可以8位或16位為單位,每次傳輸的單位數不受限制。

3.CPOL/CPHA及通訊模式

??上面講述的圖 SPI通訊時序 中的時序只是SPI中的其中一種通訊模式,SPI一共有四種通訊模式, 它們的主要區別是總線空閑時SCK的時鐘狀態以及數據采樣時刻。為方便說明,在此引入“時鐘極性CPOL”和“時鐘相位CPHA”的概念。

??時鐘極性CPOL是指SPI通訊設備處于空閑狀態時,SCK信號線的電平信號(即SPI通訊開始前、 NSS線為高電平時SCK的狀態)。CPOL=0時, SCK在空閑狀態時為低電平,CPOL=1時,則相反。
??時鐘相位CPHA是指數據的采樣的時刻,當CPHA=0時,MOSI或MISO數據線上的信號將會在SCK時鐘線的“奇數邊沿”被采樣。當CPHA=1時, 數據線在SCK的“偶數邊沿”采樣。見圖 CPHA = 0時的SPI通訊模式 及圖 CPHA = 1時的SPI通訊模式 。
在這里插入圖片描述
??我們來分析這個CPHA=0的時序圖。首先,根據SCK在空閑狀態時的電平,分為兩種情況。 SCK信號線在空閑狀態為低電平時,CPOL=0;空閑狀態為高電平時,CPOL=1。
??無論CPOL=0還是=1,因為我們配置的時鐘相位CPHA=0,在圖中可以看到,采樣時刻都是在SCK的奇數邊沿。 注意當CPOL=0的時候,時鐘的奇數邊沿是上升沿,而CPOL=1的時候,時鐘的奇數邊沿是下降沿。所以SPI的采樣時刻不是由上升/下降沿決定的。 MOSI和MISO數據線的有效信號在SCK的奇數邊沿保持不變,數據信號將在SCK奇數邊沿時被采樣,在非采樣時刻,MOSI和MISO的有效信號才發生切換
??類似地,當CPHA=1時,不受CPOL的影響,數據信號在SCK的偶數邊沿被采樣,見圖 CPHA=1時的SPI通訊模式_ 。
在這里插入圖片描述
??由CPOL及CPHA的不同狀態,SPI分成了四種模式,見表 SPI的四種模式 , 主機與從機需要工作在相同的模式下才可以正常通訊,實際中采用較多的是“模式0”與“模式3”。
在這里插入圖片描述

STM32的SPI特性及架構

與I2C外設一樣,STM32芯片也集成了專門用于SPI協議通訊的外設。

1.STM32的SPI外設簡介

??STM32的SPI外設可用作通訊的主機及從機, 支持最高的SCK時鐘頻率為fpclk/2 (STM32F103型號的芯片默認fpclk1為36MHz, fpclk2為72MHz),完全支持SPI協議的4種模式,數據幀長度可設置為8位或16位, 可設置數據MSB先行或LSB先行。它還支持雙線全雙工(前面小節說明的都是這種模式)、雙線單向以及單線模式。 其中雙線單向模式可以同時使用MOSI及MISO數據線向一個方向傳輸數據,可以加快一倍的傳輸速度。而單線模式則可以減少硬件接線, 當然這樣速率會受到影響。我們只講解雙線全雙工模式。

2. STM32的SPI架構剖析

在這里插入圖片描述

1.通訊引腳

??SPI的所有硬件架構都從圖 SPI架構圖 中左側MOSI、MISO、SCK及NSS線展開的。STM32芯片有多個SPI外設, 它們的SPI通訊信號引出到不同的GPIO引腳上,使用時必須配置到這些指定的引腳,見表 STM32F10x的SPI引腳 。 關于GPIO引腳的復用功能,可查閱《STM32F10x規格書》,以它為準。
在這里插入圖片描述
??其中SPI1是APB2上的設備,最高通信速率達36Mbtis/s,SPI2、SPI3是APB1上的設備,最高通信速率為18Mbits/s。除了通訊速率, 在其它功能上沒有差異。其中SPI3用到了下載接口的引腳,這幾個引腳默認功能是下載,第二功能才是IO口,如果想使用SPI3接口, 則程序上必須先禁用掉這幾個IO口的下載功能。一般在資源不是十分緊張的情況下,這幾個IO口是專門用于下載和調試程序,不會復用為SPI3。

2. 時鐘控制邏輯

??SCK線的時鐘信號,由波特率發生器根據“控制寄存器CR1”中的BR[0:2]位控制,該位是對fpclk時鐘的分頻因子, 對fpclk的分頻結果就是SCK引腳的輸出時鐘頻率,計算方法見表 BR位對fpclk的分頻 。
在這里插入圖片描述
其中的fpclk頻率是指SPI所在的APB總線頻率, APB1為fpclk1,APB2為fpckl2。
通過配置“控制寄存器CR”的“CPOL位”及“CPHA”位可以把SPI設置成前面分析的4種SPI模式。

3. 數據控制邏輯

??SPI的MOSI及MISO都連接到數據移位寄存器上,數據移位寄存器的數據來源及目標接收、發送緩沖區以及MISO、MOSI線。 當向外發送數據的時候,數據移位寄存器以“發送緩沖區”為數據源,把數據一位一位地通過數據線發送出去;當從外部接收數據的時候, 數據移位寄存器把數據線采樣到的數據一位一位地存儲到“接收緩沖區”中。通過寫SPI的“數據寄存器DR”把數據填充到發送緩沖區中, 通訊讀“數據寄存器DR”,可以獲取接收緩沖區中的內容。其中數據幀長度可以通過“控制寄存器CR1”的“DFF位”配置成8位及16位模式; 配置“LSBFIRST位”可選擇MSB先行還是LSB先行。

4. 整體控制邏輯

??整體控制邏輯負責協調整個SPI外設,控制邏輯的工作模式根據我們配置的“控制寄存器(CR1/CR2)”的參數而改變, 基本的控制參數包括前面提到的SPI模式、波特率、LSB先行、主從模式、單雙向模式等等。在外設工作時, 控制邏輯會根據外設的工作狀態修改“狀態寄存器(SR)”,我們只要讀取狀態寄存器相關的寄存器位, 就可以了解SPI的工作狀態了。除此之外,控制邏輯還根據要求,負責控制產生SPI中斷信號、DMA請求及控制NSS信號線。
??實際應用中,我們一般不使用STM32 SPI外設的標準NSS信號線,而是更簡單地使用普通的GPIO,軟件控制它的電平輸出,從而產生通訊起始和停止信號

3.通訊過程

??STM32使用SPI外設通訊時,在通訊的不同階段它會對“狀態寄存器SR”的不同數據位寫入參數,我們通過讀取這些寄存器標志來了解通訊狀態。
??圖 主發送器通訊過程 中的是“主模式”流程,即STM32作為SPI通訊的主機端時的數據收發過程。
在這里插入圖片描述
主模式收發流程及事件說明如下:

(1) 控制NSS信號線, 產生起始信號(圖中沒有畫出);

(2) 把要發送的數據寫入到“數據寄存器DR”中, 該數據會被存儲到發送緩沖區;

(3) 通訊開始,SCK時鐘開始運行。MOSI把發送緩沖區中的數據一位一位地傳輸出去; MISO則把數據一位一位地存儲進接收緩沖區中;

(4) 當發送完一幀數據的時候,“狀態寄存器SR”中的“TXE標志位”會被置1,表示傳輸完一幀,發送緩沖區已空;類似地, 當接收完一幀數據的時候,“RXNE標志位”會被置1,表示傳輸完一幀,接收緩沖區非空;

(5) 等待到“TXE標志位”為1時,若還要繼續發送數據,則再次往“數據寄存器DR”寫入數據即可;等待到“RXNE標志位”為1時, 通過讀取“數據寄存器DR”可以獲取接收緩沖區中的內容。

??假如我們使能了TXE或RXNE中斷,TXE或RXNE置1時會產生SPI中斷信號,進入同一個中斷服務函數,到SPI中斷服務程序后, 可通過檢查寄存器位來了解是哪一個事件,再分別進行處理。也可以使用DMA方式來收發“數據寄存器DR”中的數據。

4.實戰——讀寫串行FLASH

1.硬件連接

在這里插入圖片描述
注意:
如果SPI通訊中的三個從機都是推挽輸出(Push-Pull Output),那么在沒有適當管理的情況下,當多個從設備同時驅動MISO線時,就會發生線路沖突,這可能導致數據錯誤和通信故障。為了解決這個問題,【從機】一般采用下面的處理方式:

片選(Chip Select):每個從設備都有一個獨立的片選線,用于啟用或禁用該設備的輸出。當主設備想要與某個從設備通信時,它會通過激活相應的片選線來選擇該從設備。未被選中的從設備會將其MISO輸出設置為高阻態(High-Impedance),從而不會對MISO線產生影響。

2.軟件設計

在這里插入圖片描述
在這里插入圖片描述
編程要點:

  1. 初始化通訊使用的目標引腳及端口時鐘;
  2. 使能SPI外設的時鐘;
  3. 配置SPI外設的模式、地址、速率等參數并使能SPI外設;
  4. 編寫基本SPI按字節收發的函數;
  5. 編寫對FLASH擦除及讀寫操作的的函數;
  6. 編寫測試程序,對讀寫數據進行校驗。

擴展1:SPI物理層第二種連接方式:菊花鏈

在數字通信世界中,在設備信號(總線信號或中斷信號)以串行的方式從一 個設備依次傳到下一個設備,不斷循環直到數據到達目標設備的方式被稱為菊花鏈

菊花鏈的最大缺點是因為是信號串行傳輸,所以一旦數據鏈路中的某設備發生故障的時候,它下面優先級較低的設備就不可能得到服務了;
另一方面,距離主機越遠的從機,獲得服務的優先級越低,所以需要安排好從機的優先級,并且設置總線檢測器,如果某個從機超時,則對該從機進行短路,防止單個從機損壞造成整個鏈路崩潰的情況

具體的連接如下圖所示:
在這里插入圖片描述
在這里插入圖片描述
采用一個/SS (或者/CS) 信號控制所有從器件的/CS 輸入; 所有從器件接收同一個時鐘信號。只有鏈上的第一個從器件(SLAVE 1) 從微控制器直接接收命令。 其他所有從器件都從鏈上前一個器件的 DOUT 輸出獲得其 DIN 數據。要保證菊鏈正常工作, 每一個從器件就必須能在給定的命令周期內(定義為每一個命令所需的時鐘數) 從 DIN 引腳讀入命令, 而在下一個命令周期從 DOUT 引腳輸出同樣的命令。 顯然,從 DIN 到 DOUT 會有一個命令周期的延遲。 另外, 各個從器件只能在/CS 的上升沿執行寫入的命令。 這意味著只要/CS 保持低電平, 從器件將不會執行命令, 并且會在下一個命令周期將命令通過 DOUT 引腳輸出。 如果在給定命令周期之后/CS 變高, 所有從器件將立即執行寫入 DIN 引腳的命令。 如果/CS 變高, 數據將不會從 DOUT 輸出, 這就使得鏈上每個從器件可以執行不同的命令。只要菊鏈的這些要求能夠滿足, 微控制器只需三個信號(/SS、SCK 和 MOSI)就能控制網絡上的所有從器件。

所以最終的數據流向圖可以表示為:

在這里插入圖片描述

SCK為時鐘信號,8clks表示8個邊沿信號;
其中D為數據,X為無效數據

所以不難發現,菊花鏈模式充分使用了SPI其移位寄存器的功能,整個鏈充當通信移位寄存器,每個從機在下一個時鐘周期將輸入數據復制到輸出。

詳細原理請看:SPI菊花鏈原理和配置

擴展2:關于stm32硬件spi的MISO口配置

在我們剛使用spi時,對于spi的io口配置可能會有一些疑惑吧,miso明明是一個輸入口卻配置成了復用推挽輸出,是不是會有一點疑惑呢?

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;     // 復用的推挽輸出

MISO不是應該設置成為輸入端口(GPIO_Mode_IN_FLOATING)才行的嗎?其實可以設置成為輸入模式,也可以設置成為復用的推挽輸出。其工作都是正常的,不過建議大家還是設置成為輸入端口的好,容易理解。

具體產生這一問題的原因是:從功能上來說,MISO應該配置為輸入模式才對,但為什么也可以配置為GPIO_Mode_AF_PP?請看下面的GPIO復用功能配置框圖。當一個GPIO端口配置為GPIO_Mode_AF_PP是,這個端口的內部結構框圖如下:圖中可以看到,片上外設的復用功能輸出信號會連接到輸出控制電路,然后在端口上產生輸出信號。但是在芯片內部,MISO是SPI模塊的輸入引腳,而不是輸出引腳,也就是說圖中的"復用功能輸出信號"根本不存在,因此"輸出控制電路"不能對外產生輸出信號。而另一方面看,即使在GPIO_Mode_AF_PP模式下,復用功能輸入信號卻與外部引腳之間相互連接,既MISO得到了外部信號的電平,實現了輸入的功能。
在這里插入圖片描述
在這里插入圖片描述


參考文章:

什么是串行與并行?串行和并行各自有什么優越點和應用場景?
什么是同步通信?什么是異步通信?兩者的優缺點是什么?
SPI協議詳解(圖文并茂+超詳細)
關于stm32硬件spi的miso口配置

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/80371.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/80371.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/80371.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【網絡原理】數據鏈路層

目錄 一. 以太網 二. 以太網數據幀 三. MAC地址 四. MTU 五. ARP協議 六. DNS 一. 以太網 以太網是一種基于有線或無線介質的計算機網絡技術&#xff0c;定義了物理層和數據鏈路層的協議&#xff0c;用于在局域網中傳輸數據幀。 二. 以太網數據幀 1&#xff09;目標地址 …

控制臺打印帶格式內容

1. 場景 很多軟件會在控制臺打印帶顏色和格式的文字&#xff0c;需要使用轉義符實現這個功能。 2. 詳細說明 2.1.轉義符說明 樣式開始&#xff1a;\033[參數1;參數2;參數3m 可以多個參數疊加&#xff0c;若同一類型的參數&#xff08;如字體顏色&#xff09;設置了多個&…

[6-2] 定時器定時中斷定時器外部時鐘 江協科技學習筆記(41個知識點)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 V 30 31 32 33 34 35 36 37 38 39 40 41

數據庫的脫敏策略

數據庫的脫敏策略&#xff1a;就是屏蔽敏感的數據 脫敏策略三要求&#xff1a; &#xff08;1&#xff09;表對象 &#xff08;2&#xff09;生效條件&#xff08;脫敏列、脫敏函數&#xff09; &#xff08;3&#xff09;二元組 常見的脫敏策略規則&#xff1a; 替換、重排、…

Python序列化的學習筆記

1. Npy&Numpy O4-mini-Cursor&#xff1a;如果.npy文件里包含了「Python對象」而非純數值數組時&#xff0c;就必須在加載時加上allow_pickleTrue。

[思維模式-27]:《本質思考力》-7- 逆向思考的原理與應用

目錄 一、什么是逆向思考 1.1、逆向思考的六大核心思維模式 1.2、逆向思考的四大實踐方法 1. 假設倒置法 2. 缺陷重構法 3. 用戶反推法 4. 規則解構法 1.3、逆向思考的經典案例庫 1. 商業創新&#xff1a;從“賣產品”到“賣服務” 2. 用戶體驗&#xff1a;從“功能滿…

在python中,為什么要引入事件循環這個概念?

在Python中&#xff0c;事件循環&#xff08;Event Loop&#xff09;是異步編程的核心機制&#xff0c;它的引入解決了傳統同步編程模型在高并發場景下的效率瓶頸問題。以下從技術演進、性能優化和編程范式三個角度&#xff0c;探討這一概念的必要性及其價值。 一、同步模型的局…

Taccel:一個高性能的GPU加速視觸覺機器人模擬平臺

觸覺感知對于實現人類水平的機器人操作能力至關重要。而視覺觸覺傳感器&#xff08;VBTS&#xff09;作為一種有前景的解決方案&#xff0c;通過相機捕捉彈性凝膠墊的形變模式來感知接觸的方式&#xff0c;為視觸覺機器人提供了高空間分辨率和成本效益。然而&#xff0c;這些傳…

oracle 會話管理

會話管理 1&#xff1a;查看當前所有用戶的會話(SESSION)&#xff1a; SELECT * FROM V S E S S I O N W H E R E U S E R N A M E I S N O T N U L L O R D E R B Y L O G O N T I M E , S I D ; 其中 O r a c l e 內部進程的 U S E R N A M E 為空 2 &#xff1a;查看當前…

Python開發后端InfluxDB數據庫測試接口

1、使用PyCharm創建一個Python項目wzClear 2、新建package包wzInfluxdb和wzConfig包&#xff0c;如上圖所示&#xff0c;新建一個DB.json配置文件并添加influxdb配置信息&#xff0c;DB.json為統一配置文件 {"influxdbV1": {"url": "http://192.168.0…

采用LLaMa-Factory對QWen大模型實現微調(效果很好)

前言 LLaMA-factory是一個非常有用的開源框架。關于利用llama-factory實現大模型的微調&#xff0c;研究了有一個多月了&#xff0c;終于相對成功的微調了一個QWen的大模型。其中的曲折愿和大家分享&#xff01; 一、源碼的下載 在github上的網址&#xff1a; GitHub - hiyou…

深入理解深度Q網絡DQN:基于python從零實現

DQN是什么玩意兒&#xff1f; 深度Q網絡&#xff08;DQN&#xff09;是深度強化學習領域里一個超厲害的算法。它把Q學習和深度神經網絡巧妙地結合在了一起&#xff0c;專門用來搞定那些狀態空間維度特別高、特別復雜的難題。它展示了用函數近似來學習價值函數的超能力&#xf…

機械物理:水力發電站工作原理是什么?

水利發電站的工作原理是將水的勢能轉化為電能&#xff0c;主要依賴水體的重力作用與能量轉換設備。以下是其核心步驟和組成部分的詳細解釋&#xff1a; 1. 蓄水與勢能積累 水壩與水庫&#xff1a;通過建造水壩攔截河流&#xff0c;形成水庫蓄水。水位升高后&#xff0c;水體的…

[面試]SoC驗證工程師面試常見問題(五)TLM通信篇

SoC驗證工程師面試常見問題(五) 摘要:UVM (Universal Verification Methodology) 中的 TLM (Transaction Level Modeling) 通信是一種用于在驗證組件之間傳遞事務(Transaction)的高層次抽象機制。它通過端口(Port)和導出(Export)實現組件間的解耦通信,避免了信…

CAD屬性圖框值與Excel聯動(CAD塊屬性導出Excel、excel更新CAD塊屬性)——CAD c#二次開發

CAD插件實現塊屬性值與excel的互動&#xff0c;效果如下&#xff1a; 加載dll插件&#xff08;CAD 命令行輸入netload &#xff0c;運行xx即可導出Excel&#xff0c;運行xx1即可根據excel更新dwg塊屬性值。&#xff09; 部分代碼如下 // 4. 開啟事務更新CAD數據using (Transact…

【數據挖掘】Apriori算法

Apriori算法是經典的關聯規則挖掘算法&#xff0c;用于從事務型數據庫中發現頻繁項集和強關聯規則&#xff0c;特別常用于購物籃分析等場景。 &#x1f9e0; 核心思想&#xff08;Apriori原則&#xff09; 一個項集是頻繁的&#xff0c;前提是它的所有子集也必須是頻繁的。 即&…

單鏈表設計與實現

01. 單鏈表簡介 在數據結構中&#xff0c;單鏈表的實現可以分為 帶頭結點 和 不帶頭結點 兩種方式&#xff0c;這里我們討論第二種方式。 頭結點&#xff1a;鏈表第一個節點不存實際數據&#xff0c;僅作為輔助節點指向首元節點&#xff08;第一個數據節點&#xff09;。頭指…

【論文閱讀】——Articulate AnyMesh: Open-Vocabulary 3D Articulated Objects Modeling

文章目錄 摘要一、介紹二、相關工作2.1. 鉸接對象建模2.2. 部件感知3D生成 三、方法3.1. 概述3.2. 通過VLM助手進行可移動部件分割3.3. 通過幾何感知視覺提示的發音估計3.4. 通過隨機關節狀態進行細化 四、實驗4.1. 定量實驗發音估計設置: 4.2. 應用程序 五、結論六、思考 摘要…

Zookeeper單機版安裝部署

目錄 1.1、概述1.2、系統環境1.3、部署流程1.3.1、下載安裝包1.3.2、解壓文件1.3.3、創建數據目錄和日志目錄1.3.4、配置ZooKeeper1.3.5、啟動ZooKeeper服務1.3.6、連接和驗證 1.4、Zookeeper服務管理命令1.4.1、啟動Zookeeper服務1.4.2、停止Zookeeper服務1.4.3、查看Zookeepe…

在 Elasticsearch 中刪除文檔中的某個字段

作者&#xff1a;來自 Elastic Kofi Bartlett 探索在 Elasticsearch 中刪除文檔字段的方法。 更多有關 Elasticsearch 文檔的操作&#xff0c;請詳細閱讀文章 “開始使用 Elasticsearch &#xff08;1&#xff09;”。 想獲得 Elastic 認證&#xff1f;查看下一期 Elasticsear…