STM32標準庫——(14)I2C通信協議、MPU6050簡介

1.I2C通信

  1. I2C 通訊協議(Inter-Integrated Circuit)是由Phiilps公司開發的,由于它引腳少,硬件實現簡單,可擴展性強, 不需要USART、CAN等通訊協議的外部收發設備,現在被廣泛地使用在系統內多個集成電路(IC)間的通訊。
  2. I2C總線是一種用于芯片之間進行通信的串行總線。它由兩條線組成:串行時鐘線(SCL)和串行數據線(SDA)。SDA(Serial data)是數據線,D代表Data也就是數據,Send Data 也就是用來傳輸數據的;SCL(Serial clock line)是時鐘線,C代表Clock 也就是時鐘 也就是控制數據發送的時序的這種總線允許多個設備在同一條總線上進行通信
  3. 作為一個通信協議,它必須要在硬件和軟件上都做出規定,硬件上的規定,就是你的電路應該如何連接,端口的輸入輸出模式都是啥樣的,軟件上的規定,就是你的時序是怎么定義的,字節如何傳輸,高位先行還是低位先行,一個完整的時序有哪些部分構成這些東西,硬件的規定和軟件的規定配合起來,就是一個完整的通信協議。

2.I2C硬件電路

? 所有I2C設備的SCL連在一起,SDA連在一起

? 設備的SCL和SDA均要配置成開漏輸出模式

? SCL和SDA各添加一個上拉電阻,阻值一般為4.7KΩ左右

  1. ?第一個就是I2C的典型電路模型,這個模型采用了一主多從的結構。我們可以看到CPU作為主設備,控制著總線并擁有很大的權利。其中,主機對SCL線擁有完全的控制權,無論何時何地,主機都負責掌控SCL線。在空閑狀態下,主機還可以主動發起對SDA的控制。但是,從機發送數據或應答時,主機需要將SDA的控制權轉交給從機
  2. 我們看到了一系列被控IC,它們是掛載在I2C總線上的從機設備,如姿態傳感器、OLED、存儲器、時鐘模塊等。這些從機的權利相對較小。對于SCL時鐘線,它們在任何時刻都只能被動的讀取,不允許控制SCL線;對于SDA數據線,從機也不允許主動發起控制,只有在主機發送讀取從機的命令后,或從機應答時,從機才能短暫地取得SDA的控制權。這就是一主多從模型中協議的規定。

  3. 由于現在是一主多從結構,主機擁有SCL的絕對控制權,因此主機的SCL可以配置成推挽輸出,所有從機的SCL都配置成浮空輸入或上拉輸入數據流向為主機發送、所有從機接收。但是到SDA線這里就比較復雜了,因為這是半雙工協議,所以主機的SDA在發送時是輸出,在接收時是輸入。同樣地,從機的SDA也會在輸入和輸出之間反復切換。如果能夠協調好輸入輸出的切換時機就沒有問題。但是這樣做的話,如果總線時序沒有協調好,就極有可能發生兩個引腳同時處于輸出的狀態如果此時一個引腳輸出高電平,一個引腳輸出低電平,就會造成電源短路的情況,這是要極力避免的。為了避免這種情況的發生,I2C的設計規定所有設備不輸出強上拉的高電平,而是采用外置弱上拉電阻加開漏輸出的電路結構。這兩點規定對應于前面提到的“設備的SCL和SDA均要配置成開漏輸出模式”以及“SCL和SDA各添加一個上拉電阻,阻值一般為4.7KΩ左右”。對應第二個圖。

  4. 推挽輸出:上面一個開關管連接正極,下面一個開關管連接負極。當上面導通時,輸出高電平;下面導通時,輸出低電平。因為這是通過開關管直接連接到正負極的,所以這是強上拉和強下拉的模式。
    開漏輸出:就是去掉這個強上拉的開關管,輸出低電平時,下管導通,是強下拉,輸出高電平時,下管斷開,但是沒有上管了,此時引腳處于浮空的狀態,這就是開漏輸出。? ? ? ? ? ? ? ? ?(我們之前的彈簧和桿子的模型來解釋,就是SCL或SDA就是一根桿子,為了防止有人向上推桿子,有人向下拉桿子造成沖突,我們就規定所有的人不準向上推桿子,只能選擇向下拉或者放手,然后我們再外置一根彈簧向上拉,你要輸出低電平就往下拽,這個彈簧肯定拽不贏你,所以彈簧被拉伸桿子處于低電平狀態,你要輸出高電平就放手,桿子在彈簧的拉力下回彈到高電平,這就是一個弱上拉的高電平,但是完全不影響數據傳輸。)

  5. 就是這個模式會有一個“線與”的現象。就是只要有任意一個或多個設備輸出了低電平,總線就處于低電平,只有所有設備都輸出高電平,總線才處于高電平。I2C可以利用這個電路特性執行多主機模式下的時鐘同步和總線仲裁,所以這里SCL雖然在一主多從模式下可以用推挽輸出,但是它仍然采用了開漏加上拉輸出的模式,因為在多主機模式下會利用到這個特征。

3.I2C時序單元

3.1 起始和終止

  • ?起始條件是指SCL高電平期間,SDA從高電平切換到低電平。在I2C總線處于空閑狀態時,SCL和SDA都處于高電平狀態,也就是沒有任何一個設備去碰SCL和SDA,由外掛的上拉電阻拉高至高電平,總線處于平靜的高電平狀態。當主機需要數據收發時打破平靜,會首先產生一個起始條件。這個起始條件是,SCL保持高電平,然后把SDA拉低,產生一個下降沿。當從機捕獲到這個SCL高電平,SDA下降沿信號時,就會進行自身的復位,等待主機的召喚。之后,主機需要將SCL拉低。這樣做一方面是占用這個總線,另一方面也是為了方便這些基本單元的拼接。這樣,除了起始和終止條件,每個時序單元的SCL都是以低電平開始,低電平結束
  • 終止條件是SCL高電平期間,SDA從低電平切換到高電平。SCL先放開并回彈到高電平,SDA再放開并回彈高電平,產生一個上升沿。這個上升沿觸發終止條件,同時終止條件之后,SCL和SDA都是高電平,回歸到最初的平靜狀態。這個起始條件和終止條件就類似串口時序里的起始位和停止位。一個完整的數據幀總是以起始條件開始、終止條件結束。另外,起始和終止都是由主機產生的。因此,從機必須始終保持雙手放開,不允許主動跳出來去碰總線。如果允許從機這樣做,那么就會變成多主機模型,不在本節的討論范圍之內。這就是起始條件和終止條件的含義。

3.2 發送字節

?就是SCL低電平期間,主機將數據位依次放到SDA線上高位先行,然后釋放SCL從機將在SCL高電平期間讀取數據位,所以SCL高電平期間,SDA不允許有數據變化,依次循環上述過程8次即可發送一個字節,起始條件之后,第一個字節也必須是主機發送的,主機如何發送呢,就是最開始SCL低電平,主機如果想發送0,就拉低SDA到低電平,如果想發送1就放手,SDA回彈到高電平在SCL低電平期間允許改變SDA的電平,當這一位放好之后,主機就松手時鐘線SCL回彈到高電平,在高電平期間是從機讀取SDA的時候,所以高電平期間SDA不允許變化,那主機在放手SCL一段時間后就可以繼續拉低SCL傳輸下一位了,主機也需要在SCL下降沿之后,盡快把數據放在SDA上,但是主機有時鐘的主導權哈,所以主機并不需要那么著急,只需要在低電平的任意時刻把數據放在SDA上就行了,晚點也沒關系,數據放完之后,主機再松手SCL,SCL高電平,從機讀取這一位。就這樣的流程,主機拉低SCL,把數據放在SDA上,主機松開SCL,從機讀取SDA的數據,在SCL的同步下,依次進行主機發送和從機接收,循環8次,就發送了8位數據,也就是一個字節,另外注意,這里是高位先行,所以第一位是一個字節的最高位B7,然后依次是次高位B6…這個和串口是不一樣的,串口時序是低位先行,這里I2C是高位先行

另外由于這里有時鐘線進行同步,所以如果主機一個字節發送一半,突然進中斷了,不操作SCL和SDA的,那時序就會在中段的位置不斷拉長,SCL和SDA電平都暫停變化,傳輸也完全暫停,等中段結束后,主機回來繼續操作,傳輸仍然不會出問題,這就是同步時序的好處

3.3 接收字節

和上面接收字節基本一樣,區別就是SDA線,主機在接收之前需要釋放SDA,然后這時從機就取得了SDA的控制權從機需要發送0就把SDA拉低,從機需要發送1就放手,SDA回彈高電平,然后同樣的,低電平變換數據,高電平讀取數據,這里實線部分表示主機控制的電平虛線部分表示從機控制的電平SCL全程由主機控制,SDA主機在接收前要釋放,交由從機控制,之后還是一樣,因為SCL始終是有主機控制的,所以從機的數據變換基本上都是貼著SCL下降沿進行的,而主機可以在SCL高電平的任意時刻讀取,這是接收一個字節的時序。

3.4 發送應答及接收應答

?發送應答和接收應答就是當我們在調用發送一個字節之后,就要緊跟著調用接收應答的時序,用來判斷從機有沒有收到剛才給它的數據,如果從機收到了,那在應答位這里,主機釋放SDA的時候,從機就應該立刻把SDA拉下來,然后在SCL高電平期間,主機讀取應答位,如果應答位為0,就說明從機確實收到了

4.I2C時序

4.1 指定地址寫

  • ?在這里上面的線是SCL,下面的線是SDA空閑狀態都是高電平,然后主機需要給從機寫入數據的時候,首先SCL高電平期間,拉低SDA產生起始條件,在起始條件之后,緊跟著的時序,必須是發送一個字節的時序,字節的內容必須是從機地址+讀寫位,正好從機地址是7位,讀寫位是1位,加起來是一個字節8位發送從機地址,就是確定通信的對象,發送讀寫位,就是確認我接下來是要寫入還是要讀出,具體發送的時候呢,在這里低電平期間SDA變換數據,高電平期間從機讀取SDA,這里我用綠色的線來標明了從機讀到的數據,比如這樣的波形,那從機收到的第一位就是高電平1,然后SCL低電平期間主機繼續變換數據,因為第二位還是1,所以這里SDA電平并沒有變換,然后SCL高電平,從機讀到第二位是1,之后繼續低電平變換數據,高電平讀取數據,第三位就是0,這樣持續8次就發送了一個字節數據,其中這個數據的定義:高7位表示從機地址,比如這個波形下,主機尋找的統計地址就是1101000,這個就是MPU6050的地址,然后最低位表示讀寫位,0表示之后的時序主機要進行寫入操作,1表示之后的時序主機要進行讀出操作,這里是0,說明之后我們要進行寫入操作,那目前主機是發生了一個字節,字節內容轉化為16進制,高位先行就是0xD0 ,然后根據協議規定,緊跟著的單元就得是接收從機的應答位(Receive Ack(RA)),在這個時刻主機要釋放SDA,釋放SDA之后引腳電平回彈到高電平,.但是根據協議規定,從機要在這個位拉低SDA,所以單看從機的波形,該應答的時候從機立刻拽住SDA,然后應答結束之后,從機再放開SDA,那現在綜合兩者的波形,結合線與的特性,在主機釋放SDA之后,由于SDA也被從機拽住了,所以主機松手后,SDA并沒有回彈高電平。
  • 應答結束后,我們要繼續發送一個字節,同樣的時序再來一遍,第二個字節就可以送到指定設備的內部來,從機設備可以自己定義第二個字節和后續字節的用途,一般第二個字節可以是寄存器地址,或者是指令控制字等,比如MPU16050定義的第二個字節就是寄存器地址,比如AD轉換器,第二個字節可能就是指令控制字,比如存儲器,第二個字節可能就是存儲器地址,那圖示這里主機發送這樣一個波形,我們一一判定,數據為00011001,即主機向從機發送了0x19 這個數據,第一部分解讀的前七位出來的是MPU6050,在MPU6050 里,就表示我要操作你0x19地址下的寄存器了,接著同樣是從機應答,主機釋放SDA,從機拽住SDA,SDA表現為低電平,主機收到應答位為0,表示收到了從機的應答,然后繼續同樣的流程。

  • 再來一遍,主機再發送一個字節,這個字節就是主機想要寫入到0x19地址下寄存器的內容了,比如這里發送了0xAA的波形,就表示我在0x19地址下寫入0xAA最后是接收應答位,如果主機不需要繼續傳輸了,就可以產生停止條件,在停止條件之前先拉低SDA,為后續SDA的上升沿做準備,然后釋放SCL,再釋放SDA,這樣就產生了SCL高電平期間SDA的上升沿,這樣一個完整的數據幀就拼接完成了,那套用上面這句話呢,這個數據幀的目的,就是對于指定從機地址為1001000的設備,在其內部0x19地址下的寄存器中,寫入0xAA這個數據

4.2 當前地址讀

?如果主機想要讀取從機的數據,就可以執行這個時序,那最開始還是SCL高電平期間,拉低SDA產生起始條件,起始條件開始后,主機必須首先調用發送一個字節,來進行從機的尋址和指定讀寫標志位,比如圖示的波形,表示本次尋址的目標是1101000的設備,同時最后一位讀寫標志為1,表示主機接下來想要讀取數據,緊跟著發送一個字節之后接收一下從機應答位,從機應答為0代表從機收到了第一個字節,在從機應答之后,從這里開始數據的傳輸方向就要反過來了,因為剛才主機發出了讀的命令,所以這之后主機就不能繼續發送了,要把SDA的控制權交給從機,主機調用接收一個字節的時序進行接收操作,然后在這一塊從機就得到了主機的允許,可以在SCL低電平之間寫入SDA,然后主機在SCL高電平期間讀取SDA,那最終主機在SCL高電平期間,依次讀取8位,就接收到了從機發送的一個字節數據,00001111,也就是0x0f,那現在問題就來了,這個0x0f是從機哪個寄存器的數據呢,我們看到在讀的時序中,I2C協議的規定是,主機進行尋址時,一旦讀寫標志位給1了,下一個字節就要立馬轉為讀的時序,所以主機還來不及指定,我想要讀哪個寄存器就得開始接收了,所以這里就沒有指定地址這個環節,那主機并沒有指定寄存器的地址,從機到底該發哪個寄存器的數據呢,這需要用到我們上面說的當前地址指針了,在從機中,所有的寄存器被分配到了一個線性區域中,并且會有個單獨的指針變量,指示著其中一個寄存器,這個指針上電默認一般指向0地址,并且每寫入一個字節和讀出一個字節后,這個指針就會自動自增一次,移動到下一個位置,主機沒有指定要讀哪個地址,從機就會返回當前指針指向的寄存器的值,那假設我剛剛調用了這個指定地址寫的時序,在0x19 的位置寫出了0xAA,那么指針就會加1移動到0x1A(0x19+1=0x1A)的位置,我再調用這個當前地址讀的時序,返回的就是0x1A地址下的值,如果再調用一次,返回的就是0x1B地址下的值,以此類推,這就是當前地址讀時序的操作邏輯,由于當前地址讀并不能指定讀的地址,所以這個時序用的不是很多。

4.3 指定地址讀

首先最開始仍然是啟動條件,然后發送一個字節進行尋址,這里指定從機地址是1101000讀寫標志位是0,代表我要進行寫的操作,經過從機應答之后,再發送一個字節,第二個字節用來指定地? ?址,這個數據就寫入到了從機的地址指針里了,也就是說從機接收到這個數據之后,它的寄存器指針就指向了0x19 這個位置,之后我們要寫入的數據,不給他發,而是直接再來個起始條件,這個Sr的意思就是重復起始條件,相當于另起一個時序,因為指定讀寫標志位,只能是跟著起始條件的第一個字節,所以如果想切換讀寫方向,只能再來個起始條件,然后起始條件后重新尋址并且指定讀寫標志位,此時讀寫標志位是1代表我要開始讀了,接著主機接收一個字節,這個字節是不是就是0x19 地址下的數據,這就是指定地址讀,你也可以再加一個停止條件,這樣也行哈,這樣的話就是兩個完整的時序了,先起始寫入地址停止,因為寫入的地址會存在地址指針里面,所以這個地址并不會因為時序的停止而消失,我們就可以再提示讀當前位置停止,這樣兩條時序也可以完成任務,但是I2C協議官方規定的復合格式是一整個數據幀,就是先起始再重復起始再停止,相當于把兩條時序拼接成一條。

5.MPU6050

5.1 簡介

?5.2 自身參數

芯片進行I2C通信的從機地址,這個可以在手冊里查到,當AD0等于0,地址為1001000,當AD0等于1時,地址為1001001,AD0就是板子引出來的一個引腳,可以調節I2C從機地址的最低位,這里地址是七位的。?如果像這樣用二進制來表示的話,一般沒啥問題,如果在程序中用16進制表示的話,一般會有兩種表示方式,以這個1001000的地址為例,第一種就是單純的把這七位的二進制轉化為16進制,這里1001000低4位和高3位切開轉換,16進制就是0x68(100 1000前面補了個0) ,所以有的地方就說MPU6050的從機地址是0x68 ,然后我們看一下之前I2C通信的時序,這里第一個字節的高7位是從機地址,最低位是讀寫位,所以如果你認為0x68是從機地址的話,在發送第一個字節時,要先把0x68 左移一位再按位或上讀寫位讀1寫0,這是認為從機地址是0x68 的操作,當然目前還有另一種常見的表示方式,就是把0x68 左移移位后的數據當做從機地址0x68 左移1位之后是0xD0 ,那這樣MPU6050的從機地址就是0xD0 ,這時在實際發送第一個字節時,如果你要寫,就直接把0xD0 當做第一個字節如果你要讀就把0xd0或上0x01 即0xD1當做第一個字節,這種表示方式就不需要進行左移的操作了,或者說這種表示方式是把讀寫位也融入到了從機地址里來,0xD0 是寫地址,0xD1是讀地址,這樣表示的,所以你之后看到有地方說0xD0是MPU6050的從機地址,那它就是融入了讀寫位的從機地址,如果你看到有地方說0x68是MPU6050的從機地址,這也不要奇怪,這種方式就是直接把7位地址轉換16進制得到的,在實際發送第一個字節時,不要忘了先左移一位,再或上讀寫位,這是兩種統計地址的表示方式。

5.3 硬件電路

  1. ?左上角是一個LDO低壓差線性穩壓器,這部分是供電的邏輯,手冊里介紹這個MPU6050 芯片的VDD供電是2.375~3.46V屬于3.3V供電的設備,不能直接接5V,所以為了擴大供電范圍,這個模塊的設計者就加了個3.3V的穩壓器,輸入端電壓vcc_5v可以在3.3v到5v之間,然后經過3.3伏的穩壓器輸出穩定的3.3伏電壓給芯片端供電,然后這一塊是電源指示燈,只要3.3v端有電,電源指示燈就會亮.
  2. 左下角是一個八針的排針,有VCC和GND這兩個引腳是電源供電,然后SCL和SDA這兩個引腳是I2C通信的引腳,在這里可以看到,SCL和SDA模塊已經內置了兩個4.7k的上拉電阻了,所以在我們接線的時候,直接把SCL和SDA接在GPIO口就行了,不需要再在外面另外接上拉電阻了,接著下面有XCL和XDA這兩個是芯片里面的主機I2C通信引腳,設計這兩個引腳是為了擴展芯片功能,之前我們說過,MPU6050是一個六軸姿態傳感器,這是九軸姿態傳感器多出的磁力計的作用,另外如果你要制作無人機,需要定高飛行,這時候就還需要增加氣壓計,擴展為十軸提供一個高度信息的穩定參考,所以根據項目要求啊,這個六軸傳感器可能不夠用,需要進行擴展,那這個時候這個XCL和XDA就可以起作用了,XCL和XDA通常就是用于外接磁力計或者氣壓計,當接上磁力計或氣壓計之后,MPU6050的主機接口可以直接訪問這些擴展芯片的數據,把這些擴展芯片的數據讀取到MPU6050 里面,在MPU6050 里面會有DMP單元進行數據融合和姿態解算,如果你不需要按MPU6050 的解算功能的話,也可以把這個磁力計或者氣壓計直接掛載在XCL和XDA這條總線上,因為I2C本來就可以掛載多設備,所以把多個設備都掛載在一起也是沒問題的。下面AD0引腳,這個之前說過,他是從機地址的最低位接低電平的話七位從機地址就是1001000接高電平的話七位從機地址就是1001001,這里電路中可以看到有一個電阻默認弱下拉到低電平了,所以引腳懸空的話就是低電平,如果想接高電平,就可以把AD0直接引到VCC強上拉至高電平。最后一個引腳是INT,也就是中斷輸出引腳,可以配置芯片內部的一些事件來觸發中斷引腳的輸出,比如數據準備好了、I2C主機錯誤等,另外芯片內部還內置了一些實用的小功能、比如自由落體檢測、運動檢測、零運動檢測等,這些信號都可以觸發INT引腳產生電平跳變,需要的話可以進行中斷信號的配置,但如果不要的話,那也可以不配置這個引腳。

5.4 框圖

  • 左上角是時鐘系統,有時鐘輸入腳和輸出腳,不過我們一般使用內部時鐘,硬件電路這里CLKIN直接接了地,CLKOUT沒有引出所以這部分不需要過多關心,然后下面這些灰色的部分就是芯片內部的傳感器,包括x y z軸的陀螺儀陀螺儀,另外這個芯片還內置了一個溫度傳感器,你要是想用它來測量溫度也是沒問題的,那這么多傳感器本質上也都相當于可變電阻,通過分壓后輸出模擬電壓,然后通過ADC進行模數轉換,轉化完成之后呢,這些傳感器的數據統一都放到數據寄存器中,我們讀取數據寄存器就能得到傳感器測量的值了,這個芯片內部的轉換都是全自動進行的,就類似我們之前學的AD連續轉換加DMA轉運,每個ADC輸出,對應16位的數據寄存器,不存在數據覆蓋的問題,我們配置好轉換頻率之后,每個數據就自動以我們設置的頻率刷新到數據寄存器,我們需要數據的時候直接來讀就行

  • 接著每個傳感器都有個自測單元self test,這部分是用來驗證芯片好壞的,當啟動自測后,芯片內部就會模擬一個外力施加在傳感器上,這個外力導致傳感器數據會比平時大一些,那如何進行自測呢,我們可以先使能自測讀取數據,再失能自測讀取數據兩個數據相減得到的數據叫自測響應,芯片手冊里給出了一個范圍,如果自測響應在這個范圍內就說明芯片沒問題,如果不在就說明芯片可能壞了,使用的時候就要小心點,這個是自測的功能。

  • 右邊這一大塊就是寄存器和通信接口部分了,中斷狀態寄存器可以控制內部的哪些事件到中斷引腳的輸出FIFO是先入先出寄存器,可以對數據流進行緩存,我們本節暫時不用,配置寄存器,可以對內部的各個電路進行配置傳感器寄存器也就是數據寄存器,存儲在各個傳感器的數據,工廠校準這個意思就是內部的傳感器都進行了校準我們不用了解,然后右邊這個數字運動處理器簡稱DMP,還是芯片內部自帶的一個姿態解算的硬件算法配合官方的DMP庫可以進行姿態解算,因為姿態解算還是比較難的,而且算法也很復雜,所以如果使用了內部的DMP進行姿態解算,姿態解算就會方便一些,暫時不涉及,這個FSYNC是幀同步,我們用不到,最后上面這塊就是通信接口部分,上面一部分就是從機的I2C和SPI通信接口,用于和stm32通信,下面這一部分是主機的I2C通信接口,用于和MPU6050擴展的設備進行通信,這里有個接口旁路選擇器(MUX)就是一個開關,如果撥到上面,輔助的I2C引腳就和正常的I2C引腳接到一起,這樣兩路總線就合在一起了,stm32可以控制所有設備,這時sm32就是大哥MPU6050和這個擴展設備都是stm32的小弟,如果撥到下面,輔助的I2C引腳就由mpu6050控制,兩條I2C總線獨立分開,這時stm32是mpu6050的大哥,MPU6050又是擴展設備的大哥,我們本節課程不會用到這個擴展功能。

5.5 常用寄存器

  1. ?6B(電源管理寄存器1):設備復位:0(不復位) 睡眠模式:0(解除睡眠) 循環模式:0(不需要循環) 無關位:0? 溫度傳感器使能:0? 選擇時鐘:000(內部時鐘)、001(x軸陀螺儀時鐘(推薦))
  2. 6C(電源管理寄存器2):循環模式喚醒頻率:00 每一個軸的待機位:全給0(不需要待機)
  3. 19(采樣率分頻):八位決定了數據輸出的快慢 值越小越快 后續程序中給0x09 也就是10分頻
  4. 1A(配置寄存器):外步同步:000(不需要)? 數字低通濾波器:110(最平滑的濾波)
  5. 1B(陀螺儀配置寄存器):自測使能:000? 滿量程選擇:11(最大量程)
  6. 1C(加速度計配置寄存器):自測使能:000? 滿量程選擇:11(最大量程) 高通濾波器:000(不需要)

6.軟件I2C讀寫MPU6050

6.1 接線圖

6.2相關代碼

6.2.1 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線//Byte或上1000 0000等于Byte 接著右移一位 即Byte與上0100 0000 等于ByteMyI2C_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);//若Byte等于1 則與上1000 0000就等于1 若Byte等于0 則與上1000 0000還是為0}	//讀取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;							//返回定義應答位變量
}
6.2.2 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
?6.2.3 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;						//數據拼接,通過輸出參數返回
}
6.2.4 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
6.2.5 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  //加速度寄存器X軸的高八位
#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 //電源管理寄存器1
#define	MPU6050_PWR_MGMT_2		0x6C //電源管理寄存器2
#define	MPU6050_WHO_AM_I		0x75#endif
6.2.6 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);}
}

6.3 現象

OLED第一行顯示MPU6050的ID號 下面三行分別是陀螺儀和加速度計X、Y、Z軸的的變化數據

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

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

相關文章

【LeetCode每日一題】【BFS模版與例題】863.二叉樹中所有距離為 K 的結點

BFS的基本概念 BFS 是廣度優先搜索&#xff08;Breadth-First Search&#xff09;的縮寫&#xff0c;是一種圖遍歷算法。它從給定的起始節點開始&#xff0c;逐層遍歷圖中的節點&#xff0c;直到遍歷到目標節點或者遍歷完所有可達節點。 BFS 算法的核心思想是先訪問當前節點的…

計算機網絡_2.2物理層下面的傳輸媒體

2.2物理層下面的傳輸媒體 一、傳輸媒體的分類二、導向型傳輸媒體1、同軸電纜2、雙絞線3、光纖&#xff08;1&#xff09;光纖通信原理&#xff08;2&#xff09;光纖組成&#xff08;4&#xff09;多模光纖與單模光纖對比&#xff08;5&#xff09;光纖的波長與規格&#xff08…

海量淘寶商品數據如何實現自動化抓取?

隨著電子商務的飛速發展&#xff0c;淘寶作為中國最大的網絡購物平臺之一&#xff0c;其商品數據具有極高的商業價值。然而&#xff0c;如何有效地從海量的淘寶商品數據中抓取所需信息&#xff0c;成為了一個技術挑戰。本文將深入探討如何實現淘寶商品數據的自動化抓取&#xf…

c# using 用法

using命令空間 導入命名空間中的所有類型 如&#xff1a;using System.Text; using別名 using別名包括詳細命名空間信息的具體類型&#xff0c;這種做法有個好處就是當同一個cs引用了兩個不同的命名空間&#xff0c;但兩個命名空間都包括了一個相同名字的類型的時候。當需要…

SQL加鎖機制深度解析:不同隔離級別與索引類型的影響

首先&#xff0c;我們先理解一下涉及的幾個核心概念&#xff1a; 主鍵 (Primary Key): 主鍵是數據庫表中的特殊列&#xff0c;用于唯一標識表中的每一行。它不能有重復值&#xff0c;也不能有NULL值。 唯一索引 (Unique Index): 唯一索引類似于主鍵&#xff0c;但它允許NULL值…

數據可視化基礎與應用-02-基于powerbi實現連鎖糕點店數據集的儀表盤制作

總結 本系列是數據可視化基礎與應用的第02篇&#xff0c;主要介紹基于powerbi實現一個連鎖糕點店數據集的儀表盤制作。 數據集描述 有一個數據集&#xff0c;包含四張工作簿&#xff0c;每個工作簿是一張表&#xff0c;其中可以銷售表可以劃分為事實表&#xff0c;產品表&am…

【Python小技巧】將list變量寫入本地txt文件并讀出為list變量的方法(附代碼)

文章目錄 前言一、萬能的txt和eval大法二、具體代碼和使用方法總結 前言 使用Python&#xff0c;我們偶爾需要將一些變量保存到本地&#xff0c;并被其它代碼讀取作為參數&#xff0c;那么怎么辦呢&#xff1f; 一、萬能的txt和eval大法 這里教大家一個簡單的方法&#xff0c…

912. 排序數組(快速排序)

快速排序&#xff1a; 分&#xff1a;找到分成兩部分進行排序的pos&#xff08;使用partition&#xff09;治&#xff1a;分別對這兩部分進行快速排序 重點&#xff1a;partition 找到pivot&#xff08;兩個方法&#xff1a;1. 取第一個值&#xff1b;2. 取隨機值&#xff09…

Linux時間同步(PPS、PTP、chrony)分析筆記

1 PPS(pulse per second) 1.1 簡介 LinuxPPS provides a programming interface (API) to define in the system several PPS sources. PPS means "pulse per second" and a PPS source is just a device which provides a high precision signal each second so t…

每日一題 2673使二叉樹所有路徑值相等的最小代價

2673. 使二叉樹所有路徑值相等的最小代價 題目描述&#xff1a; 給你一個整數 n 表示一棵 滿二叉樹 里面節點的數目&#xff0c;節點編號從 1 到 n 。根節點編號為 1 &#xff0c;樹中每個非葉子節點 i 都有兩個孩子&#xff0c;分別是左孩子 2 * i 和右孩子 2 * i 1 。 樹…

Java緩存簡介

內存訪問速度和硬盤訪問速度是計算機系統中兩個非常重要的性能指標。 內存訪問速度&#xff1a;內存是計算機中最快的存儲介質&#xff0c;它的訪問速度可以達到幾納秒級別。內存中的數據可以直接被CPU訪問&#xff0c;因此讀寫速度非常快。 硬盤訪問速度&…

學習和工作的投入產出比(節選)

人工智能統領全文 推薦包含關于投入、產出、過剩、市場關注、案例、結果和避雷等主題的信息&#xff1a; 投入與產出&#xff1a; 投入和產出都有直接和間接兩類常見形式。常見的四種組合是&#xff1a;直接投入、直接產出、間接投入、間接產出。 過剩&#xff1a; 過剩是一個重…

力扣SQL50 無效的推文 查詢

Problem: 1683. 無效的推文 思路 &#x1f468;?&#x1f3eb; 參考 char_length(str)&#xff1a;計算 str 的字符長度length(str)&#xff1a;計算 str 的字節長度 Code select tweet_id from Tweets where char_length(content) > 15;

C++與 Fluke5500A設備通過GPIB-USB-B通信的經驗積累

C與 Fluke5500A設備通過GPIB-USB-B通信的經驗積累 以下內容來自&#xff1a;C與 Fluke5500A設備通過GPIB-USB-B通信的經驗積累 - JMarcus - 博客園 (cnblogs.com)START 1.需要安裝NI-488.2.281&#xff0c;安裝好了之后&#xff0c;GPIB-USB-B的驅動就自動安裝好了 注意版本…

動態規劃(算法競賽、藍橋杯)--單調隊列滑動窗口與連續子序列的最大和

1、B站視頻鏈接&#xff1a;E11【模板】單調隊列 滑動窗口最值_嗶哩嗶哩_bilibili 題目鏈接&#xff1a;滑動窗口 /【模板】單調隊列 - 洛谷 #include <bits/stdc.h> using namespace std; const int N1000010; int a[N],q[N];//q存的是元素的下標 int main(){int n,k;…

unity學習(41)——創建(create)角色腳本(panel)——UserHandler(收)+CreateClick(發)——創建發包!

1.客戶端的程序結構被我精簡過&#xff0c;現在去MessageManager.cs中增加一個UserHandler函數&#xff0c;根據收到的包做對應的GameInfo賦值。 2.在Model文件夾下新增一個協議文件UserProtocol&#xff0c;內容很簡單。 using System;public class UserProtocol {public co…

金融短信群發平臺具有那些特點

金融短信群發平臺的特點主要包括以下幾個方面&#xff1a; 1.高效性&#xff1a;金融短信群發平臺能夠快速地發送大量的短信&#xff0c;使得金融信息能夠迅速傳達給目標客戶&#xff0c;保證了信息的及時性和有效性。 2.安全性&#xff1a;金融短信群發平臺對于信息的安全性非…

藍橋杯練習系統(算法訓練)ALGO-995 24點

資源限制 內存限制&#xff1a;256.0MB C/C時間限制&#xff1a;1.0s Java時間限制&#xff1a;3.0s Python時間限制&#xff1a;5.0s 問題描述 24點游戲是一個非常有意思的游戲&#xff0c;很流行&#xff0c;玩法很簡單&#xff1a;給你4張牌&#xff0c;每張牌上有數…

【JS】sort方法的基本使用與雙重、多重排序:對象數組按照多個對象屬性進行排序

【JS】對象數組按照多個對象屬性進行排序&#xff08;sort方法&#xff09; 一、sort():用于對數組的元素進行排序,并返回數組&#xff0c;arr.sort()默認為升序排列二、sort()用法三、雙重、多重排序&#xff1a;對象數組按照多個對象屬性進行排序&#xff08;sort方法&#x…

設備樹學習(DOING)

我的理解本質上還是復用。尤其是嵌入式領域&#xff0c;設備多種多樣&#xff0c;但是很多設備接口都是標準的&#xff0c;或者大同小異。以前驅動開發可能每個設備商都去抄別家的搞進內核&#xff0c;這樣造成了大量的垃圾代碼。后面linux內核就把這些做成公共庫抽象出來&…