(stm32學習總結)—SPI-FLASH 實驗


SPI總線

SPI 簡介

SPI 的全稱是"Serial Peripheral Interface",意為串行外圍接口,是Motorola 首先在其 MC68HCXX 系列處理器上定義的。SPI 接口主要應用在 EEPROM、
FLASH、實時時鍾、AD 轉換器,還有數字信號處理器和數字信號解碼器之間。SPI是一種高速的,全雙工,同步的通信總線,並且在芯片的管腳上只占用四根線,節約了芯片的管腳,同時為 PCB 的布局上節省空間,提供方便,正是出於這種簡單易用的特性,如今越來越多的芯片集成了這種通信協議,比如 STM32 系列芯片。下面我們看下 SPI 內部結構簡易圖,如圖 39.1.1.1 所示:

SPI 物理層

SPI 通訊設備之間的常用連接方式見圖 25-1。
  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):主設備輸入/從設備輸出引腳。主機從這條信號線
讀入數據,從機的數據由這條信號線輸出到主機,即在這條線上數據的方向為從機到主機。

協議層

與 I2C 的類似,SPI 協議定義了通訊的起始和停止信號、數據有效性、時鍾同步等環節。
1. SPI 基本通訊過程
  先看看 SPI 通訊的通訊時序,見圖 25-2。
這是一個主機的通訊時序。NSS、SCK、MOSI 信號都由主機控制產生,而 MISO 的信號由從機產生,主機通過該信號線讀取從機的數據。MOSI 與 MISO 的信號只在 NSS 為低電平的時候才有效,在 SCK 的每個時鍾周期 MOSI 和 MISO 傳輸一位數據。以上通訊流程中包含的各個信號分解如下: 
2. 通訊的起始和停止信號
  在圖 25-2 中的標號1處,NSS 信號線由高變低,是 SPI 通訊的起始信號。NSS 是每個從機各自獨占的信號線,當從機在自己的 NSS 線檢測到起始信號后,就知道自己被主機選中了,開始准備與主機通訊。在圖中的標號6處,NSS 信號由低變高,是 SPI 通訊的停止信號,表示本次通訊結束,從機的選中狀態被取消。 
3. 數據有效性
  SPI 使用 MOSI 及 MISO 信號線來傳輸數據,使用 SCK 信號線進行數據同步。MOSI及 MISO 數據線在 SCK 的每個時鍾周期傳輸一位數據,且數據輸入輸出是同時進行的。數據傳輸時,MSB 先行或 LSB 先行並沒有作硬性規定,但要保證兩個 SPI 通訊設備之間使用同樣的協定,一般都會采用圖 25-2 中的 MSB 先行模式。觀察圖中的2、3、4、5標號處,MOSI 及 MISO 的數據在 SCK 的上升沿期間變化輸出,在 SCK 的下降沿時被采樣即在 SCK 的下降沿時刻,MOSI 及 MISO 的數據有效,高電平時表示數據“1”,為低電平時表示數據“0”。在其它時刻,數據無效,MOSI 及 MISO為下一次表示數據做准備。SPI 每次數據傳輸可以 8 位或 16 位為單位,每次傳輸的單位數不受限制。 
4. CPOL/CPHA 及通訊模式
  上面講述的圖 25-2 中的時序只是 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 的“偶數邊沿”采樣。見圖 25-3 及圖 25-4。
  我們來分析這個 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 的有效信號才發生切換。 
 
  由 CPOL 及 CPHA 的不同狀態,SPI 分成了四種模式,見表 25-1,主機與從機需要工作在相同的模式下才可以正常通訊,實際中采用較多的是“模式 0”與“模式 3”。
STM32 的 SPI 特性及架構
  與 I2C 外設一樣,STM32 芯片也集成了專門用於 SPI 協議通訊的外設。 
STM32 的 SPI 外設簡介
  STM32 的 SPI 外設可用作通訊的主機及從機,支持最高的 SCK 時鍾頻率為 fpclk/2(STM32F103 型號的芯片默認 fpclk1為 72MHz,fpclk2為 36MHz),完全支持 SPI 協議的 4 種模式,數據幀長度可設置為 8 位或 16 位,可設置數據 MSB 先行或 LSB 先行。它還支持雙線全雙工(前面小節說明的都是這種模式)、雙線單向以及單線模式。其中雙線單向模式可以同時使用 MOSI 及 MISO 數據線向一個方向傳輸數據,可以加快一倍的傳輸速度。而單線模式則可以減少硬件接線,當然這樣速率會受到影響。我們只講解雙線全雙工模式。  
STM32 的 SPI 架構剖析
1.通訊引腳
  SPI 的所有硬件架構都從圖 25-5 中左側 MOSI、MISO、SCK 及 NSS 線展開的。STM32 芯片有多個 SPI 外設,它們的 SPI 通訊信號引出到不同的 GPIO 引腳上,使用時必須配置到這些指定的引腳,見表 25-2。
  其中 SPI1 是 APB2 上的設備,最高通信速率達 36Mbtis/s,SPI2、SPI3 是 APB1 上的設備,最高通信速率為 18Mbits/s。除了通訊速率,在其它功能上沒有差異。其中 SPI3 用到了下載接口的引腳,這幾個引腳默認功能是下載,第二功能才是 IO 口,如果想使用 SPI3接口,則程序上必須先禁用掉這幾個 IO 口的下載功能。一般在資源不是十分緊張的情況下,這幾個 IO 口是專門用於下載和調試程序,不會復用為 SPI3。 
2. 時鍾控制邏輯
  SCK 線的時鍾信號,由波特率發生器根據“控制寄存器 CR1”中的 BR[0:2]位控制,該位是對 fpclk時鍾的分頻因子,對 fpclk的分頻結果就是 SCK 引腳的輸出時鍾頻率,計算方法見表 25-3。
  其中的 fpclk頻率是指 SPI 所在的 APB 總線頻率,APB1 為 fpclk1,APB2 為 fpckl2。通過配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 設置成前面分析的 4 種 SPI 模式。
3. 數據控制邏輯
  SPI 的 MOSI 及 MISO 都連接到數據移位寄存器上,數據移位寄存器的數據來源及目標接收、發送緩沖區以及 MISO、MOSI 線。
當向外發送數據的時候,數據移位寄存器以“發送緩沖區”為數據源,把數據一位一位地通過數據線發送出去;
當從外部接收數據的時候,數據移位寄存器把數據線采樣到的數據一位一位地存儲到“接收緩沖區”中。
通過寫 SPI的“數據寄存器 DR”把數據填充到發送 F 緩沖區中,通訊讀“數據寄存器 DR”,可以獲取接收緩沖區中的內容。其中數據幀長度可以通過“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式;配置“LSBFIRST 位”可選擇 MSB 先行還是 LSB 先行。
4. 整體控制邏輯
  整體控制邏輯負責協調整個 SPI 外設,控制邏輯的工作模式根據我們配置的“控制寄存器(CR1/CR2)”的參數而改變,基本的控制參數包括前面提到的 SPI 模式、波特率、LSB先行、主從模式、單雙向模式等等。
  在外設工作時,控制邏輯會根據外設的工作狀態修改“狀態寄存器(SR)”,我們只要讀取狀態寄存器相關的寄存器位,就可以了解 SPI 的工作狀態了。除此之外,控制邏輯還根據要求,負責控制產生 SPI 中斷信號、DMA 請求及控制NSS 信號線。
  實際應用中,我們一般不使用 STM32 SPI 外設的標准 NSS 信號線,而是更簡單地使用普通的 GPIO,軟件控制它的電平輸出,從而產生通訊起始和停止信號。

通訊過程

STM32 使用 SPI 外設通訊時,在通訊的不同階段它會對“狀態寄存器 SR”的不同數據位寫入參數,我們通過讀取這些寄存器標志來了解通訊狀態。 
圖 25-6 中的是“主模式”流程,即 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”中的數據。 

SPI 初始化結構體詳解

跟其它外設一樣,STM32 標准庫提供了 SPI 初始化結構體及初始化函數來配置 SPI 外設。初始化結構體及函數定義在庫文件“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中,編程時我們可以結合這兩個文件內的注釋使用或參考庫幫助文檔。了解初始化結構體后我們就能對 SPI 外設運用自如了,代碼如下。
這些結構體成員說明如下,其中括號內的文字是對應參數在 STM32 標准庫中定義的宏:
(1) SPI_Direction
本成員設置 SPI 的通訊方向,可設置為雙線全雙工(SPI_Direction_2Lines_FullDuplex),雙線只接收(SPI_Direction_2Lines_RxOnly),單線只接收(SPI_Direction_1Line_Rx)、單線只發送模式(SPI_Direction_1Line_Tx)。
(2) SPI_Mode
本成員設置 SPI 工作在主機模式(SPI_Mode_Master)或從機模式(SPI_Mode_Slave ),這兩個模式的最大區別為 SPI 的 SCK 信號線的時序,SCK 的時序是由通訊中的主機產生的。若被配置為從機模式,STM32 的 SPI 外設將接受外來的 SCK 信號。
(3) SPI_DataSize
本成員可以選擇 SPI 通訊的數據幀大小是為 8 位(SPI_DataSize_8b)還是 16 位(SPI_DataSize_16b)。
(4) SPI_CPOL 和 SPI_CPHA
這兩個成員配置 SPI 的時鍾極性 CPOL 和時鍾相位 CPHA,這兩個配置影響到 SPI 的通訊模式,關於 CPOL 和 CPHA 的說明參考前面“通訊模式”小節。
時鍾極性 CPOL 成員,可設置為高電平(SPI_CPOL_High)或低電平(SPI_CPOL_Low )。
時鍾相位 CPHA 則可以設置為 SPI_CPHA_1Edge(在 SCK 的奇數邊沿采集數據) 或SPI_CPHA_2Edge (在 SCK 的偶數邊沿采集數據) 。 
(5) SPI_NSS
本成員配置 NSS 引腳的使用模式,可以選擇為硬件模式(SPI_NSS_Hard )與軟件模式(SPI_NSS_Soft ),在硬件模式中的 SPI 片選信號由 SPI 硬件自動產生,而軟件模式則需要我們親自把相應的 GPIO 端口拉高或置低產生非片選和片選信號。實際中軟件模式應用比較多。
(6) SPI_BaudRatePrescaler
本成員設置波特率分頻因子,分頻后的時鍾即為 SPI 的 SCK 信號線的時鍾頻率。這個成員參數可設置為 fpclk 的 2、4、6、8、16、32、64、128、256 分頻。
(7) SPI_FirstBit
所有串行的通訊協議都會有 MSB 先行(高位數據在前)還是 LSB 先行(低位數據在前)的問題,而 STM32 的 SPI 模塊可以通過這個結構體成員,對這個特性編程控制。
(8) SPI_CRCPolynomial
這是 SPI 的 CRC 校驗中的多項式,若我們使用 CRC 校驗時,就使用這個成員的參數(多項式),來計算 CRC 的值。
  配置完這些結構體成員后,我們要調用 SPI_Init 函數把這些參數寫入到寄存器中,實現 SPI 的初始化,然后調用 SPI_Cmd 來使能 SPI 外設。

SPI 配置步驟 

(1)使能 SPI 及對應 GPIO 端口時鍾並配置引腳的復用功能
  要使用 SPI 就必須使能它的時鍾,前面介紹框圖時,我們知道 SPI1 是掛接在 APB2 總線上,而 SPI2 和 SPI3 掛接在 APB1 總線上。而且 SPI 總線接口對應不同的 STM32 引腳,所以還需使能對應引腳的端口時鍾,同時配置為復用功能。
(2)初始化 SPI,包括數據幀長度、傳輸模式、MSB 和 LSB 順序等
(3)使能(開啟)SPI
(4)SPI 數據傳輸
  通過上面幾個步驟的配置,SPI 已經可以開始通信了,在通信的過程中肯定會有數據的發送和接收,固件庫也提供了 SPI 的發送和接收函數。
SPI 發送數據函數原型為:
  void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data); 這個函數很好理解,往 SPIx 數據寄存器寫入數據 Data,從而實現發送。
SPI 接收數據函數原型為:
  uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx); 此函數非常簡單,從 SPIx 數據寄存器中讀取接收到的數據。
(5)查看 SPI 傳輸狀態
  在 SPI 傳輸過程中,我們經常要判斷數據是否傳輸完成,發送區是否為空等狀態,這是通過函數 SPI_I2S_GetFlagStatus 實現的,此函數原型為:
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
此函數非常簡單,第二個參數是用來選擇 SPI 傳輸過程中判斷的標志,對應的標志可在 stm32f10x_spi.h 文件中查找到,使用較多的是發送完成標志(SPI_I2S_FLAG_TXE)和接收完成標志(SPI_I2S_FLAG_RXNE)。
判斷發送是否完成的方法是:
SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE);
將以上幾步配置好后,我們就可以使用 STM32F1 的 SPI 和外部 FLASH(EN25QXX)通信了。
 

spi配置代碼(只配置了spi1)

 1 #ifndef _spi_H  2 #define _spi_H
 3 
 4 #include "system.h"
 5 
 6 void SPI1_Init(void);             //初始化SPI1口
 7 void SPI1_SetSpeed(u8 SpeedSet); //設置SPI1速度 
 8 u8 SPI1_ReadWriteByte(u8 TxData);//SPI1總線讀寫一個字節
 9 
10 //void SPI2_Init(void);             //初始化SPI2口
11 //void SPI2_SetSpeed(u8 SpeedSet); //設置SPI2速度 
12 //u8 SPI2_ReadWriteByte(u8 TxData);//SPI2總線讀寫一個字節
13 
14 #endif

 

 1 #include "spi.h"
 2 
 3 //以下是SPI模塊的初始化代碼,配置成主機模式  4 //SPI口初始化  5 //這里針是對SPI1的初始化
 6 void SPI1_Init(void)  7 {  8  GPIO_InitTypeDef GPIO_InitStructure;  9  SPI_InitTypeDef SPI_InitStructure; 10     
11     /* SPI的IO口和SPI外設打開時鍾 */
12  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 13  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); 14     
15     /* SPI的IO口設置 */
16     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; 17     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //復用推挽輸出 18     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 19     GPIO_Init(GPIOA, &GPIO_InitStructure); 20 
21     SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //設置SPI單向或者雙向的數據模式:SPI設置為雙線雙向全雙工
22     SPI_InitStructure.SPI_Mode = SPI_Mode_Master;        //設置SPI工作模式:設置為主SPI
23     SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;        //設置SPI的數據大小:SPI發送接收8位幀結構
24     SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;        //串行同步時鍾的空閑狀態為高電平
25     SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;    //串行同步時鍾的第二個跳變沿(上升或下降)數據被采樣
26     SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;        //NSS信號由硬件(NSS管腳)還是軟件(使用SSI位)管理:內部NSS信號有SSI位控制
27     SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;        //定義波特率預分頻的值:波特率預分頻值為256
28     SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;    //指定數據傳輸從MSB位還是LSB位開始:數據傳輸從MSB位開始
29     SPI_InitStructure.SPI_CRCPolynomial = 7;    //CRC值計算的多項式
30     SPI_Init(SPI1, &SPI_InitStructure);  //根據SPI_InitStruct中指定的參數初始化外設SPIx寄存器
31     
32     SPI_Cmd(SPI1, ENABLE); //使能SPI外設
33     
34     SPI1_ReadWriteByte(0xff);//啟動傳輸 
35 } 36 
37 //SPI1速度設置函數 38 //SPI速度=fAPB2/分頻系數 39 //@ref SPI_BaudRate_Prescaler:SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256 40 //fAPB2時鍾一般為84Mhz:
41 void SPI1_SetSpeed(u8 SPI_BaudRatePrescaler) 42 { 43     SPI1->CR1&=0XFFC7;//位3-5清零,用來設置波特率
44     SPI1->CR1|=SPI_BaudRatePrescaler;    //設置SPI1速度 
45     SPI_Cmd(SPI1,ENABLE); //使能SPI1
46 } 47 
48 //SPI1 讀寫一個字節 49 //TxData:要寫入的字節 50 //返回值:讀取到的字節
51 u8 SPI1_ReadWriteByte(u8 TxData) 52 { 53  
54     while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);//等待發送區空 
55     
56     SPI_I2S_SendData(SPI1, TxData); //通過外設SPIx發送一個byte 數據
57         
58     while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); //等待接收完一個byte 
59  
60     return SPI_I2S_ReceiveData(SPI1); //返回通過SPIx最近接收的數據 
61              
62 }

上述程序中的一個奇怪的地方

在復用SPI總線時,必須先設置總線端口。讀取其他ARM芯片(如NXP)一般很容易看出芯片的設置是否正確。不過對於STM32就容易讓人迷惑了。就像上述程序中,我們在使用SPI總線進行通信時,可以這樣設置:

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

     其他端口如時鍾端口以及MOSI端口都是stm32向外輸出,引腳設置成推挽輸出沒問題, 但是大家對MISO端口的設置就會產生疑惑了,MISO不是應該設置成為輸入端口(GPIO_Mode_IN_FLOATING)才行的嗎?

      答題是肯定的,對於STM32的這一類管腳來說(如USART_RX)即可以設置成為輸入模式,也可以設置成為復用的推挽輸出。其工作都是正常的,不過建議大家還是設置成為輸入端口的好,容易理解。

      具體產生這一問題的原因是:從功能上來說,MISO應該配置為輸入模式才對,但為什么也可以配置為GPIO_Mode_AF_PP?請看下面的GPIO復用功能配置框圖。當一個GPIO端口配置為GPIO_Mode_AF_PP是,這個端口的內部結構框圖如下:

  圖中可以看到,片上外設的復用功能輸出信號會連接到輸出控制電路,然后在端口上產生輸出信號。但是在芯片內部,MISO是SPI模塊的輸入引腳,而不是輸出引腳,也就是說圖中的"復用功能輸出信號"根本不存在(MISO不會產生輸出信號),因此"輸出控制電路"不能對外產生輸出信號。

  而另一方面看,即使在GPIO_Mode_AF_PP模式下,復用功能輸入信號卻與外部引腳之間相互連接,既MISO得到了外部信號的電平,實現了輸入的功能(可以4-5-6-7路線輸入數據,復用的情況下就是4-5-復用功能路線)。

FLASH介紹

控制 FLASH 的指令

  搞定 SPI 的基本收發單元后,還需要了解如何對 FLASH 芯片進行讀寫。FLASH 芯片自定義了很多指令,我們通過控制 STM32 利用 SPI 總線向 FLASH 芯片發送指令,FLASH芯片收到后就會執行相應的操作。而這些指令,對主機端(STM32)來說,只是它遵守最基本的 SPI 通訊協議發送出的數據,但在設備端(FLASH 芯片)把這些數據解釋成不同的意義,所以才成為指令。查看FLASH 芯片的數據手冊《W25Q64》,可了解各種它定義的各種指令的功能及指令格式,見表 25-4。 
 
  該表中的第一列為指令名,第二列為指令編碼,第三至第 N 列的具體內容根據指令的不同而有不同的含義。其中帶括號的字節參數,方向為 FLASH 向主機傳輸,即命令響應,不帶括號的則為主機向 FLASH 傳輸。表中“A0~A23”指 FLASH 芯片內部存儲器組織的地址;“M0~M7”為廠商號(MANUFACTURER ID);“ID0-ID15”為 FLASH 芯片的ID;“dummy”指該處可為任意數據;“D0~D7”為 FLASH 內部存儲矩陣的內容。 
  在 FLSAH 芯片內部,存儲有固定的廠商編號(M7-M0)和不同類型 FLASH 芯片獨有的編號(ID15-ID0),見表 25-5。
  通過指令表中的讀 ID 指令“JEDEC ID”可以獲取這兩個編號,該指令編碼為“9Fh”,其中“9F h”是指 16 進制數“9F” (相當於 C 語言中的 0x9F)。緊跟指令編碼的三個字節分別為 FLASH 芯片輸出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)” 。此處我們以該指令為例,配合其指令時序圖進行講解,見圖 25-8。 

 

  主機首先通過 MOSI 線向 FLASH 芯片發送第一個字節數據為“9F h”,當 FLASH 芯片收到該數據后,它會解讀成主機向它發送了“JEDEC 指令”,然后它就作出該命令的響應:通過 MISO 線把它的廠商 ID(M7-M0)及芯片類型(ID15-0)發送給主機,主機接收到指令響應后可進行校驗。常見的應用是主機端通過讀取設備 ID 來測試硬件是否連接正常,或用於識別設備。對於 FLASH 芯片的其它指令,都是類似的,只是有的指令包含多個字節,或者響應包含更多的數據。
  實際上,編寫設備驅動都是有一定的規律可循的。首先我們要確定設備使用的是什么通訊協議。如上一章的 EEPROM 使用的是 I2C,本章的 FLASH 使用的是 SPI。那么我們就先根據它的通訊協議,選擇好 STM32 的硬件模塊,並進行相應的 I2C 或 SPI 模塊初始化。接着,我們要了解目標設備的相關指令,因為不同的設備,都會有相應的不同的指令。如EEPROM 中會把第一個數據解釋為內部存儲矩陣的地址(實質就是指令)。而 FLASH 則定義了更多的指令,有寫指令,讀指令,讀 ID 指令等等。最后,我們根據這些指令的格式要求,使用通訊協議向設備發送指令,達到控制設備的目標

定義 FLASH 指令編碼表

為了方便使用,我們把 FLASH 芯片的常用指令編碼使用宏來封裝起來,后面需要發送指令編碼的時候我們直接使用這些宏即可。
 1 //指令表
 2 #define EN25X_WriteEnable        0x06 
 3 #define EN25X_WriteDisable        0x04 
 4 #define EN25X_ReadStatusReg        0x05 
 5 #define EN25X_WriteStatusReg    0x01 
 6 #define EN25X_ReadData            0x03 
 7 #define EN25X_FastReadData        0x0B 
 8 #define EN25X_FastReadDual        0x3B 
 9 #define EN25X_PageProgram        0x02 
10 #define EN25X_BlockErase        0xD8 
11 #define EN25X_SectorErase        0x20 
12 #define EN25X_ChipErase            0xC7 
13 #define EN25X_PowerDown            0xB9 
14 #define EN25X_ReleasePowerDown    0xAB 
15 #define EN25X_DeviceID            0xAB 
16 #define EN25X_ManufactDeviceID    0x90 
17 #define EN25X_JedecDeviceID        0x9F 

讀取 FLASH 芯片 ID

根據“JEDEC”指令的時序,我們把讀取 FLASH ID 的過程編寫成一個函數。
 1 //讀取芯片ID  2 //返回值如下:  3 //0XEF13,表示芯片型號為EN25Q80  4 //0XEF14,表示芯片型號為EN25Q16  5 //0XEF15,表示芯片型號為EN25Q32  6 //0XEF16,表示芯片型號為EN25Q64  7 //0XEF17,表示芯片型號為EN25Q128 
 8 u16 EN25QXX_ReadID(void)  9 { 10     u16 Temp = 0; 11     EN25QXX_CS=0; 12     SPI2_ReadWriteByte(0x9F);//發送讀取ID命令 
13     SPI2_ReadWriteByte(0x00); 14     SPI2_ReadWriteByte(0x00); 15     SPI2_ReadWriteByte(0x00); 16     Temp|=SPI2_ReadWriteByte(0xFF)<<8; 17     Temp|=SPI2_ReadWriteByte(0xFF); 18     //EN25QXX_CS=1; 
19     return Temp; 20 }  
  這段代碼利用控制 CS 引腳電平的宏“SPI_FLASH_CS_LOW/HIGH”以及前面編寫的單字節收發函數 SPI_FLASH_SendByte,很清晰地實現了“JEDEC ID”指令的時序:發送一個字節的指令編碼“W25X_JedecDeviceID”,然后讀取 3 個字節,獲取 FLASH 芯片對該指令的響應,最后把讀取到的這 3 個數據合並到一個變量 Temp 中,然后作為函數返回值,把該返回值與我們定義的宏“sFLASH_ID”對比,即可知道 FLASH 芯片是否正常。 
 

FLASH 寫使能以及讀取當前狀態

在向 FLASH 芯片存儲矩陣寫入數據前,首先要使能寫操作,通過“Write Enable”命令即可寫使能。
1 //EN25QXX寫使能 2 //將WEL置位 
3 void EN25QXX_Write_Enable(void) 4 { 5     EN25QXX_CS=0;                            //使能器件 
6     SPI2_ReadWriteByte(EN25X_WriteEnable);      //發送寫使能 
7     EN25QXX_CS=1;                            //取消片選 
8 } 
  
  與 EEPROM 一樣,由於 FLASH 芯片向內部存儲矩陣寫入數據需要消耗一定的時間,並不是在總線通訊結束的一瞬間完成的,所以在寫操作后需要確認 FLASH 芯片“空閑”時才能進行再次寫入。為了表示自己的工作狀態,FLASH 芯片定義了一個狀態寄存器,見圖 25-9

我們只關注這個狀態寄存器的第 0 位“BUSY”,當這個位為“1”時,表明 FLASH芯片處於忙碌狀態,它可能正在對內部的存儲矩陣進行“擦除”或“數據寫入”的操作。利用指令表中的“Read Status Register”指令可以獲取 FLASH 芯片狀態寄存器的內容,其時序見圖 25-10。 

 

  只要向 FLASH 芯片發送了讀狀態寄存器的指令,FLASH 芯片就會持續向主機返回最新的狀態寄存器內容,直到收到 SPI 通訊的停止信號。據此我們編寫了具有等待 FLASH 芯片寫入結束功能的函數,見下面代碼。 
 1 //讀取EN25QXX的狀態寄存器  2 //BIT7 6 5 4 3 2 1 0  3 //SPR RV TB BP2 BP1 BP0 WEL BUSY  4 //SPR:默認0,狀態寄存器保護位,配合WP使用  5 //TB,BP2,BP1,BP0:FLASH區域寫保護設置  6 //WEL:寫使能鎖定  7 //BUSY:忙標記位(1,忙;0,空閑)  8 //默認:0x00
 9 u8 EN25QXX_ReadSR(void) 10 { 11     u8 byte=0; 12     EN25QXX_CS=0;                            //使能器件 
13     SPI2_ReadWriteByte(EN25X_ReadStatusReg);    //發送讀取狀態寄存器命令 
14     byte=SPI2_ReadWriteByte(0Xff);             //讀取一個字節 
15     EN25QXX_CS=1;                            //取消片選 
16     return byte; 17 } 
1 //等待空閑
2 void EN25QXX_Wait_Busy(void) 3 { 4     while((EN25QXX_ReadSR()&0x01)==0x01);   // 等待BUSY位清空
5 }

FLASH 扇區擦除

由於 FLASH 存儲器的特性決定了它只能把原來為“1”的數據位改寫成“0”,而原來為“0”的數據位不能直接改寫為“1”。所以這里涉及到數據“擦除”的概念,在寫入前,必須要對目標存儲矩陣進行擦除操作,把矩陣中的數據位擦除為“1”,在數據寫入的時候,如果要存儲數據“1”,那就不修改存儲矩陣 ,在要存儲數據“0”時,才更改該位。通常,對存儲矩陣擦除的基本操作單位都是多個字節進行,如本例子中的 FLASH 芯
片支持“扇區擦除”、“塊擦除”以及“整片擦除”,見表 25-6。 
FLASH 芯片的最小擦除單位為扇區(Sector),而一個塊(Block)包含 16 個扇區,其內部存儲矩陣分布見圖 25-11。
使用扇區擦除指令“Sector Erase”可控制 FLASH 芯片開始擦寫,其指令時序見圖25-14。 
扇區擦除指令的第一個字節為指令編碼,緊接着發送的 3 個字節用於表示要擦除的存儲矩陣地址。要注意的是在扇區擦除指令前,還需要先發送“寫使能”指令,發送扇區擦除指令后,通過讀取寄存器狀態等待扇區擦除操作完畢,代碼如下。
 1 //擦除一個扇區  2 //Dst_Addr:扇區地址 根據實際容量設置  3 //擦除一個山區的最少時間:150ms
 4 void EN25QXX_Erase_Sector(u32 Dst_Addr)  5 {  6     //監視falsh擦除情況,測試用 
 7      printf("fe:%x\r\n",Dst_Addr);  8      Dst_Addr*=4096;  9     EN25QXX_Write_Enable();                  //SET WEL 
10  EN25QXX_Wait_Busy(); 11       EN25QXX_CS=0;                            //使能器件 
12     SPI2_ReadWriteByte(EN25X_SectorErase);      //發送扇區擦除指令 
13     SPI2_ReadWriteByte((u8)((Dst_Addr)>>16));  //發送24bit地址 
14     SPI2_ReadWriteByte((u8)((Dst_Addr)>>8)); 15  SPI2_ReadWriteByte((u8)Dst_Addr); 16     EN25QXX_CS=1;                            //取消片選 
17     EN25QXX_Wait_Busy();                      //等待擦除完成
18 }
這段代碼調用的函數在前面都已講解,只要注意發送擦除地址時高位在前即可。調用扇區擦除指令時注意輸入的地址要對齊到 4KB。
 

FLASH 的頁寫入

目標扇區被擦除完畢后,就可以向它寫入數據了。與 EEPROM 類似,FLASH 芯片也有頁寫入命令,使用頁寫入命令最多可以一次向 FLASH 傳輸 256 個字節的數據,我們把這個單位為頁大小。FLASH 頁寫入的時序見圖 25-13。 
  從時序圖可知,第 1 個字節為“頁寫入指令”編碼,2-4 字節為要寫入的“地址 A”,接着的是要寫入的內容,最多個可以發送 256 字節數據,這些數據將會從“地址 A”開始,按順序寫入到 FLASH 的存儲矩陣。若發送的數據超出 256 個,則會覆蓋前面發送的數據。與擦除指令不一樣,頁寫入指令的地址並不要求按 256 字節對齊,只要確認目標存儲單元是擦除狀態即可(即被擦除后沒有被寫入過)。所以,若對“地址 x”執行頁寫入指令后,發送了 200 個字節數據后終止通訊,下一次再執行頁寫入指令,從“地址(x+200)”開始寫入 200 個字節也是沒有問題的(小於 256 均可)。 只是在實際應用中由於基本擦除單元是4KB,一般都以扇區為單位進行讀寫,想深入了解,可學習我們的“FLASH 文件系統”相關的例子。把頁寫入時序封裝成函數,其實現見下列代碼。 
 1 //寫SPI FLASH  2 //在指定地址開始寫入指定長度的數據  3 //該函數帶擦除操作!  4 //pBuffer:數據存儲區  5 //WriteAddr:開始寫入的地址(24bit)  6 //NumByteToWrite:要寫入的字節數(最大65535) 
 7 u8 EN25QXX_BUFFER[4096];  8 void EN25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)  9 { 10  u32 secpos; 11  u16 secoff; 12  u16 secremain; 13  u16 i; 14     u8 * EN25QXX_BUF; 15        EN25QXX_BUF=EN25QXX_BUFFER; 16      secpos=WriteAddr/4096;//扇區地址 
17     secoff=WriteAddr%4096;//在扇區內的偏移
18     secremain=4096-secoff;//扇區剩余空間大小 19      //printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//測試用
20      if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大於4096個字節
21     while(1) 22  { 23         EN25QXX_Read(EN25QXX_BUF,secpos*4096,4096);//讀出整個扇區的內容
24         for(i=0;i<secremain;i++)//校驗數據
25  { 26             if(EN25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除 
27  } 28         if(i<secremain)//需要擦除
29  { 30             EN25QXX_Erase_Sector(secpos);//擦除這個扇區
31             for(i=0;i<secremain;i++)       //復制
32  { 33                 EN25QXX_BUF[i+secoff]=pBuffer[i]; 34  } 35             EN25QXX_Write_NoCheck(EN25QXX_BUF,secpos*4096,4096);//寫入整個扇區 
36 
37         }else EN25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//寫已經擦除了的,直接寫入扇區剩余區間. 
38         if(NumByteToWrite==secremain)break;//寫入結束了
39         else//寫入未結束
40  { 41             secpos++;//扇區地址增1
42             secoff=0;//偏移位置為0 
43 
44                pBuffer+=secremain;  //指針偏移
45             WriteAddr+=secremain;//寫地址偏移 
46                NumByteToWrite-=secremain;                //字節數遞減
47             if(NumByteToWrite>4096)secremain=4096;    //下一個扇區還是寫不完
48             else secremain=NumByteToWrite;            //下一個扇區可以寫完了
49  } 50  } 51 }
  這段代碼的內容為:先發送“寫使能”命令,接着才開始頁寫入時序,然后發送指令編碼、地址,再把要寫入的數據一個接一個地發送出去,發送完后結束通訊,檢查 FLASH狀態寄存器,等待 FLASH 內部寫入結束。
 

從 FLASH 讀取數據

相對於寫入,FLASH 芯片的數據讀取要簡單得多,使用讀取指令“Read Data”即可,其指令時序見圖 25-14。
 
發送了指令編碼及要讀的起始地址后,FLASH 芯片就會按地址遞增的方式返回存儲矩陣的內容,讀取的數據量沒有限制,只要沒有停止通訊,FLASH 芯片就會一直返回數據。代碼如下。
 1 //讀取SPI FLASH  2 //在指定地址開始讀取指定長度的數據  3 //pBuffer:數據存儲區  4 //ReadAddr:開始讀取的地址(24bit)  5 //NumByteToRead:要讀取的字節數(最大65535)
 6 void EN25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)  7 {  8  u16 i;  9     EN25QXX_CS=0;                            //使能器件 
10     SPI2_ReadWriteByte(EN25X_ReadData);         //發送讀取命令 
11     SPI2_ReadWriteByte((u8)((ReadAddr)>>16));  //發送24bit地址 
12     SPI2_ReadWriteByte((u8)((ReadAddr)>>8)); 13  SPI2_ReadWriteByte((u8)ReadAddr); 14     for(i=0;i<NumByteToRead;i++) 15  { 16         pBuffer[i]=SPI2_ReadWriteByte(0XFF);   //循環讀數 
17  } 18     EN25QXX_CS=1; 19 }  
由於讀取的數據量沒有限制,所以發送讀命令后一直接收 NumByteToRead 個數據到結束即可。 

3. main 函數  

最后我們來編寫 main 函數,進行 FLASH 芯片讀寫校驗,代碼如下。
 1 #include "system.h"
 2 #include "SysTick.h"
 3 #include "led.h"
 4 #include "usart.h"
 5 #include "tftlcd.h"
 6 #include "key.h"
 7 #include "spi.h"
 8 #include "flash.h"
 9 
10 
11 //要寫入到25Q64的字符串數組
12 const u8 text_buf[]="www.prechin.net"; 13 #define TEXT_LEN sizeof(text_buf)
14 //u16 key3;
15 
16 int main() 17 { 18     u8 i=0; 19  u8 key; 20     u8 buf[30]; 21     
22     SysTick_Init(72); 23     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //中斷優先級分組 分2組
24  LED_Init(); 25     USART1_Init(9600); 26     TFTLCD_Init();            //LCD初始化
27  KEY_Init(); 28  EN25QXX_Init(); 29     
30     FRONT_COLOR=BLACK; 31     LCD_ShowString(10,10,tftlcd_data.width,tftlcd_data.height,16,"PRECHIN STM32F1"); 32     LCD_ShowString(10,30,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net"); 33     LCD_ShowString(10,50,tftlcd_data.width,tftlcd_data.height,16,"FLASH-SPI Test"); 34     LCD_ShowString(10,70,tftlcd_data.width,tftlcd_data.height,16,"K_UP:Write K_DOWN:Read"); 35     FRONT_COLOR=RED; 36     
37     while(EN25QXX_ReadID()!=EN25Q64)            //檢測不到EN25Q64 38     //while(1)
39  { 40         //key3 = EN25QXX_ReadID();
41         printf("EN25Q64 Check Failed! \r\n"); 42         LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"EN25Q64 Check Failed! "); 43  } 44     printf("EN25Q64 Check Success!\r\n"); 45     LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"EN25Q64 Check Success!"); 46         
47     LCD_ShowString(10,170,tftlcd_data.width,tftlcd_data.height,16,"Write Data:"); 48     LCD_ShowString(10,190,tftlcd_data.width,tftlcd_data.height,16,"Read Data :"); 49     
50     while(1) 51  { 52         key=KEY_Scan(0); 53         if(key==KEY_UP) 54  { 55             EN25QXX_Write((u8 *)text_buf,0,TEXT_LEN); 56             printf("發送的數據:%s\r\n",text_buf); 57             LCD_ShowString(10+11*8,170,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net"); 58  } 59         if(key==KEY_DOWN) 60  { 61             EN25QXX_Read(buf,0,TEXT_LEN); 62             printf("接收的數據:%s\r\n",buf); 63             LCD_ShowString(10+11*8,190,tftlcd_data.width,tftlcd_data.height,16,buf); 64  } 65         
66         i++; 67         if(i%20==0) 68  { 69             led1=!led1; 70  } 71         
72         delay_ms(10); 73             
74  } 75 }
注意:
由於實驗板上的 FLASH 芯片默認已經存儲了特定用途的數據,如擦除了這些數據會影響到某些程序的運行。所以我們預留了 FLASH 芯片的“第 0 扇區(0-4096 地址)”專用於本實驗,如非必要,請勿擦除其它地址的內容。如已擦除,可在配套資料里找到“刷外部 FLASH 內容”程序,根據其說明給 FLASH 重新寫入出廠內容。
 
 
 
 
 1 //無檢驗寫SPI FLASH  2 //必須確保所寫的地址范圍內的數據全部為0XFF,否則在非0XFF處寫入的數據將失敗!  3 //具有自動換頁功能  4 //在指定地址開始寫入指定長度的數據,但是要確保地址不越界!  5 //pBuffer:數據存儲區  6 //WriteAddr:開始寫入的地址(24bit)  7 //NumByteToWrite:要寫入的字節數(最大65535)  8 //CHECK OK
 9 void EN25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) 10 { 11  u16 pageremain; 12     pageremain=256-WriteAddr%256; //單頁剩余的字節數 
13     if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大於256個字節
14     while(1) 15  { 16  EN25QXX_Write_Page(pBuffer,WriteAddr,pageremain); 17         if(NumByteToWrite==pageremain)break;//寫入結束了
18          else //NumByteToWrite>pageremain
19  { 20             pBuffer+=pageremain; 21             WriteAddr+=pageremain; 22 
23             NumByteToWrite-=pageremain;              //減去已經寫入了的字節數
24             if(NumByteToWrite>256)pageremain=256; //一次可以寫入256個字節
25             else pageremain=NumByteToWrite;       //不夠256個字節了
26  } 27  } 28 } 

 

 
 1 #ifndef _flash_H  2 #define _flash_H
 3 
 4 #include "system.h"
 5 
 6 
 7 //EN25X系列/Q系列芯片列表  8 //EN25Q80 ID 0XEF13  9 //EN25Q16 ID 0XEF14 10 //EN25Q32 ID 0XEF15 11 //EN25Q64 ID 0XEF16 12 //EN25Q128 ID 0XEF17 
13 #define EN25Q80     0XEF13     
14 #define EN25Q16     0XEF14
15 #define EN25Q32     0XEF15
16 //#define EN25Q64 0XEF16 17 //#define EN25Q128 0XEF17 18 //#define EN25Q64 0XC816 19 //#define EN25Q64 0X1C16 //GD25QXX 20 //#define EN25Q64 0X2016 //XM25QHXX
21 #define EN25Q64     0Xb16        //MXIC C216
22 #define EN25Q128    0XC817
23 
24 extern u16 EN25QXX_TYPE;                    //定義EN25QXX芯片型號 
25 
26 #define    EN25QXX_CS         PGout(13)          //EN25QXX的片選信號
27 
28 
29 //指令表
30 #define EN25X_WriteEnable         0x06 
31 #define EN25X_WriteDisable        0x04 
32 #define EN25X_ReadStatusReg       0x05 
33 #define EN25X_WriteStatusReg      0x01 
34 #define EN25X_ReadData            0x03 
35 #define EN25X_FastReadData        0x0B 
36 #define EN25X_FastReadDual        0x3B 
37 #define EN25X_PageProgram         0x02 
38 #define EN25X_BlockErase          0xD8 
39 #define EN25X_SectorErase         0x20 
40 #define EN25X_ChipErase           0xC7 
41 #define EN25X_PowerDown           0xB9 
42 #define EN25X_ReleasePowerDown    0xAB 
43 #define EN25X_DeviceID            0xAB 
44 #define EN25X_ManufactDeviceID    0x90 
45 #define EN25X_JedecDeviceID       0x9F 
46 
47 void EN25QXX_Init(void); 48 u16  EN25QXX_ReadID(void);                  //讀取FLASH ID
49 u8     EN25QXX_ReadSR(void);                //讀取狀態寄存器 
50 void EN25QXX_Write_SR(u8 sr);              //寫狀態寄存器
51 void EN25QXX_Write_Enable(void);          //寫使能 
52 void EN25QXX_Write_Disable(void);        //寫保護
53 void EN25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite); 54 void EN25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead);   //讀取flash
55 void EN25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//寫入flash
56 void EN25QXX_Erase_Chip(void);              //整片擦除
57 void EN25QXX_Erase_Sector(u32 Dst_Addr);    //扇區擦除
58 void EN25QXX_Wait_Busy(void);               //等待空閑
59 void EN25QXX_PowerDown(void);            //進入掉電模式
60 void EN25QXX_WAKEUP(void);                //喚醒
61 
62 
63 #endif

 

 
 
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM