|
1.SPI总线及W25QXX芯片 1.1 SPI总线简介 SPI全称Serial Peripheral Interface,即串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在EEPROM、FLASH、实时时钟、AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的、全双工、同步通讯总线,在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局节省空间提供了方便,正是这种简单易用的特性,如今越来越多的芯片集成了这种通讯协议。下图是SPI内部结构简易图
& p& i6 t7 u$ W$ Z4 a* p2 B
" l& f; q- ?8 p" C+ {. M从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。 SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下: 1 g7 J, l0 q$ ~4 \* [" d
MISO:主设备数据输入,从设备数据输出 MOSI:主设备数据输出,从设备数据输入 SCLK:时钟信号,由主设备产生 7 H$ i/ u# {2 E7 S
CS:从设备片选信号,由主设备控制 6 T0 }: |9 H1 U6 X+ W
* M3 L R# @0 Z0 ]( } SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。
. Z+ L6 r9 e2 F# K$ V: h% o当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。5 H7 q8 g0 U, C, [( i
SPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式: 2 T, C) ~: q- [: w/ g
CPOL=0:串行同步时钟的空闲状态为低电平 CPOL=1:串行同步时钟的空闲状态为高电平 CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样
d$ l* f7 g# {CPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
+ P! S. @! \9 O r7 N# O% `* O |* D9 o) s* q' r
1 G3 s1 u8 u- H0 O) o
1.2 W25QXX芯片简介 W25QXX芯片是华邦公司推出的大容量SPI FLASH产品,该系列有W25Q16/32/62/128等。本例程使用W25Q64,W25Q64容量为64Mbits(8M字节):8MB的容量分为128个块(Block)(块大小为64KB),每个块又分为16个扇区(Sector)(扇区大小为4KB);W25Q64的最小擦除单位为一个扇区即4KB,因此在选择芯片的时候必须要有4K以上的SRAM(可以开辟4K的缓冲区)。W25Q64的擦写周期多达10万次,具有20年的数据保存期限。下表是W25QXX的常用命令表
4 C5 G, F* j/ Y
" V4 D0 X) E" z6 P, z
2.硬件设计 D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息 & b+ f5 g" {0 T2 [! d
指示灯D1 USART1串口 W25Q64 6 H% N1 H7 ]: \+ E9 z o& \4 C
K_UP和K_DOWN按键" g5 A7 }! O h/ [( D4 L/ J
6 }2 R8 L5 z ?4 D6 F5 V4 x
k; Q/ b1 [8 K- B9 J+ [1 c
$ ?- M3 j. ^2 E- a8 l: N1 O; w" C8 d& b6 F2 S
3.软件设计  3.1 STM32CubeMX设置 ➡️ RCC设置外接HSE,时钟设置为72M ➡️ PC0设置为GPIO推挽输出模式、上拉、高速、默认输出电平为高电平 ➡️ USART1选择为异步通讯方式,波特率设置为115200Bits/s,传输数据长度为8Bit,无奇偶校验,1位停止位 ➡️ PA0设置为GPIO输入模式、下拉模式;PE3设置为GPIO输入模式、上拉模式 ➡️ PG13设置为GPIO推挽输出模式、上拉、高速(片选引脚) ➡️ 激活SPI2,不开启NSS,数据长度8位,MSB先输出,分频因子256,CPOL为HIGH,CPHA为第二个边沿,不开启CRC检验,NSS为软件控制 , W- ~1 c' |: u* m1 |; e: O
3 J/ m6 z0 o4 f4 q- p. U+ V+ c➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码
/ b% P% J4 g& d, H1 t1 z& W
3.2 MDK-ARM软件编程 ➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中
0 s5 W& Q9 i) [1 u7 ~6 c
- void MX_SPI2_Init(void){
1 N6 Q/ N1 s) a% q' M/ v, {0 ]; B' [ - hspi2.Instance = SPI2;$ t7 C \ `7 b/ p. c: K
- hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式& p; |6 H2 F5 E
- hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式
4 `4 I# I" @6 z - hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度
* n. T5 Y) D3 c' m - hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平5 y/ u F4 T+ o% ]. W" t- `
- hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样# \. S3 D/ Q4 t1 g$ A
- hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制( r- G. E# T) {! t, x% Z9 j# K
- hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子256, T k3 o# X% \: Z
- hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行6 Y/ V% n: _0 J+ \: @; j
- hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式4 \ |3 g- V- j! }9 L
- hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验9 n2 S; r$ r$ E( Y/ E# J
- hspi2.Init.CRCPolynomial = 10;9 V( ]( [0 p- f1 x2 _
- if (HAL_SPI_Init(&hspi2) != HAL_OK){8 N2 ]; s" w0 k
- Error_Handler();6 ~5 p( v7 g( E% ~! B
- }
7 c8 }. I' U5 r - }
- m5 p6 e4 b" a+ n2 t - - _, O% u7 I) |5 O ~& U0 h
- void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){ L1 e8 F5 L2 }2 D
- GPIO_InitTypeDef GPIO_InitStruct = {0};2 m2 Z0 {5 O! B4 ]/ g8 v5 n3 x, K
- if(spiHandle->Instance==SPI2){1 C1 F- P$ T% O: H
- __HAL_RCC_SPI2_CLK_ENABLE(); 3 m; R3 v6 x% j, {8 o G( ~0 L5 K
- __HAL_RCC_GPIOB_CLK_ENABLE();- U8 V7 w+ g2 H0 ?) F Q! } o' E
- /**SPI2 GPIO Configuration + {6 g6 t! D. W6 }% s
- PB13 ------> SPI2_SCK
" h9 y, S; R( B) |' F - PB14 ------> SPI2_MISO; Z. f! A4 R' A. ~7 k3 P) w5 ~1 A
- PB15 ------> SPI2_MOSI */9 {8 C& R/ |8 m' p. @
- GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;3 B) S3 Y0 ^/ d1 T5 ^5 x( b
- GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
" b+ o2 `% x6 M - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;2 E! Q# _( r4 {. b- \+ w. A4 M
- HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
. ~7 c- W! X+ J - GPIO_InitStruct.Pin = GPIO_PIN_14;+ E- ~2 p6 s0 Y4 @' {
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
2 f2 h Z) ]2 Q6 V- h; C, Q - GPIO_InitStruct.Pull = GPIO_NOPULL;7 b& ]/ G& ~; R
- HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);* {! I, d4 q* c$ V% Y- n
- }
# M$ U, H1 f, m* o; |2 G6 y - }
复制代码 6 u2 ~# `$ U# f" `* k
➡️ 创建按键驱动文件key.c 和相关头文件key.h ➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍 - //这里仅介绍几个重要的函数0 n- N4 O& g7 g% g4 p' ^* R
- void W25QXX_Init(void){
) ? G7 c, o) U! S& p* N9 @) B3 k - W25Qx_Disable();3 W) D! h8 u5 D3 |. ]
- W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID. |+ H7 u# T) G/ V9 b0 p" Q
- printf("FLASH ID:%X\r\n",W25QXX_TYPE);
; }" w3 P) I, `& \, k& l - if(W25QXX_TYPE == 0xc816)
+ D9 q5 ^# f3 u2 d - printf("FLASH TYPE:W25Q64\r\n");
1 `+ P- V. [* h$ F$ G5 z - }7 i; I G7 j' J8 A; ]4 U0 R
( j- `' A d. k; b: T: Z- uint16_t W25QXX_ReadID(void){
! Q+ i4 h" X5 r4 [ - uint16_t ID;
, D8 U5 K) F/ L+ r5 s) h0 U+ K2 F" X - uint8_t id[2]={0};" n7 h0 P- C* ~, D# P) s
- uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令 : c5 n" T- E) _( J
- W25Qx_Enable();//使能器件8 i$ E" N% ~- R' {% D* j
- HAL_SPI_Transmit(&hspi2,cmd,4,1000);9 a- P; L9 ]" Z+ \) ~
- HAL_SPI_Receive(&hspi2,id,2,1000);
: E! U$ ?% _' V4 g - W25Qx_Disable();//取消片选 J- g/ C4 r" ]' }9 ~
- ID = (((uint16_t)id[0])<<8)|id[1];" Y9 h; t8 Z9 y9 N. {
- return ID;+ \% @' G' G' \* |
- }
/ e$ P: I7 w6 L - 7 ?1 j3 o7 X; Q) [( K* T1 I0 _4 ?! w
- void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){
6 h9 F' W2 r! g% I6 Q6 h - uint8_t cmd[4] = {0};# \$ M- j( `3 K; J5 w: o) `% W
- cmd[0] = W25X_ReadData;//读取命令2 |# Q- T7 d* z
- cmd[1] = ((uint8_t)(ReadAddr>>16));
8 t6 @* ~: j* {5 r - cmd[2] = ((uint8_t)(ReadAddr>>8));2 j3 m( k+ h( z
- cmd[3] = ((uint8_t)ReadAddr);: j, { c* X. e5 E) k' l" ^9 i
- * L& u0 @; M, E4 z; B% D4 R
- W25Qx_Enable();//使能器件& q4 ? i5 t% S5 `. p. y
- HAL_SPI_Transmit(&hspi2,cmd,4,1000);' D" Z* Q: j7 P. M0 P: S
- HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);2 w! k' E, ~* o, o( y1 M% _9 f
- W25Qx_Disable();//取消片选 2 m6 A# ]9 u5 T i7 n3 A
- }
8 T% }1 x; R( p7 a+ l! n* ~! ~+ w - 3 u1 d) r# B; K+ L; s
- void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){5 k1 A. h' l5 D* }# `3 y
- uint32_t secpos;
M) m* J) m: Z& ?$ r! s; k% j- Y1 n - uint16_t secoff;0 }' I% D% U; \% t. d# \
- uint16_t secremain;
D9 B- n# o1 @( N& @$ [1 ~ - uint16_t i;# T% m* S- u' f6 j3 z. G
- uint8_t *W25QXX_BUF;
/ E, M- f/ W6 P! T -
" K m' A1 p# v% r3 N) _ - W25QXX_BUF = W25QXX_BUFFER;
- G3 P" v$ `* Y - secpos = WriteAddr/4096; //扇区地址
j+ f, s; r4 F. z# ~0 w% R) s) f - secpos = WriteAddr%4096; //在扇区里的偏移1 I2 }7 [% j, t9 Y+ R% [! r i
- secremain = 4096-secoff; //扇区剩余空间大小
8 S% m# P6 W! {" \$ D) W - printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);
8 z' {2 i# h. D) W% i W/ K' A# Z - if(NumByteToWrite <= secremain) //不大于4K字节
5 u+ W3 w9 }8 K2 F$ V - secremain = NumByteToWrite;; R d2 |' J8 I+ k& E9 p. l% i
- while(1){
9 y1 @! b& h& I1 l' w _ - W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容& h! U$ I! [4 o, q2 n1 X
- for(i=0;i<secremain;i++){//校验数据
3 l5 S9 A1 P2 @. `4 q - if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除/ g) G! L# | k$ y! P
- break;
3 v6 g) ]8 k/ w3 d |* H7 M+ v - }
$ M; E; I; W }* a2 i0 G, y, j) O" E -
3 x" L2 c3 X6 s9 d0 b8 V - if(i < secremain){//需要擦除! u& Q+ g3 E& T: ^- R5 i" C5 d2 m$ e
- W25QXX_Erase_Sector(secpos);//擦除扇区
5 z1 b$ s. q, d* m' u - for(i=0;i<secremain;i++){
0 S5 ?: \: y* b7 X' z - W25QXX_BUF[i+secoff] = pBuffer[i];
# C, C8 E* \% _8 q - }
! ?' F$ _" D. N+ S9 l( Z - W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
6 a( S7 |3 F4 ~* K" K# T( a - }2 i4 D1 m( R7 p
- else{: g/ R3 }2 c% G: o
- W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间3 X8 y* j8 o& r0 t6 p; u" P: K
- }
$ i7 l, k/ v3 l: d5 l6 Z -
9 _" n$ k7 A! E; F3 D7 x/ @ - if(NumByteToWrite == secremain){//写入结束了
, x2 d+ v4 u/ q F; [. e* @* q - break;8 e$ W5 q6 b, d; ?9 l0 w: n
- }
; F' ^5 s# J4 U3 \- C& Z. | - else{ //写入未结束
1 p- W+ X3 U+ h5 b" K - secpos++; //扇区地址增1
( L/ J* P6 x0 n9 g& o7 z% R: l - secoff = 0; //偏移位置为0 " q& m# v8 I: x# {( F
- pBuffer += secremain; //指针偏移4 r a$ z. j; }( v o }
- WriteAddr += secremain; //写地址偏移: F( {" Q" r- l% O
- NumByteToWrite -= secremain;//字节数递减4 T) `/ k" P3 l6 ]8 d, k
- if(NumByteToWrite > 4096) G( b. |& i! K$ O5 ?
- secremain = 4096; //下个扇区还没是写不完, g3 K1 [( ^0 l" x
- else
. U: S& L Q: ?" U, Q - secremain = NumByteToWrite;//下个扇区可以写完了
9 t* a$ h& I& t* n - }" v* \# @8 Q& Y$ m
- }: Y, F# S/ Q" D
- }
复制代码
/ O9 g" `7 N i- L/ J+ S) W; O ~➡️ 在main.c文件下编写SPI测试代码 - /* USER CODE BEGIN PV */
4 d% }# T) q$ q! w - uint8_t wData[0x100];# U; z8 {: Q8 \+ W( C& }
- uint8_t rData[0x100];
/ R8 J+ R: N" X: Q3 E8 @0 R% Q - uint32_t i;2 g4 j6 T! d6 y2 S7 t" u4 Q+ U
- /* USER CODE END PV */! l7 w& \% ^. n4 q+ P
- int main(void){6 G) [& d: V2 V6 e O3 n
- /* USER CODE BEGIN 1 */) x+ ^) @/ \: A7 k ~7 j. i
- uint8_t key;
+ ~6 k+ y( M6 P8 P/ [ - /* USER CODE END 1 */+ v3 S3 H0 _2 L2 N- M
- HAL_Init();
- L' w9 \! L! I2 |# R7 d - SystemClock_Config();6 E2 g6 t; L5 c% x+ ]- c
- MX_GPIO_Init();! V" K6 G' x8 i. E1 _/ v8 J$ F" Y; ~* Y: B
- MX_SPI2_Init();. v- G, F5 W8 W8 u/ b. d
- MX_USART1_UART_Init();
- f: o( V0 a) E! a - /* USER CODE BEGIN 2 */9 D7 w" {7 ~% a; l' ^/ S
- W25QXX_Init();
1 j3 c1 ?" h1 `8 n% K7 }4 i - for(i=0;i<0x100;i++){
1 @; c1 i9 K- u+ Y4 h& l - wData[i] = i;* r4 X K# V$ I6 o
- rData[i] = 0;
" p' \8 \0 F( L- b8 X - }
( [& E0 X. O. _: v1 y+ g7 \ - /* USER CODE END 2 */
3 V0 o- Z" b% i2 d" m, w - while (1){
* W: `% c9 K s f* ?2 c - key = KEY_Scan(0);; [+ s1 D7 N# ~; e* ^6 X
- if(key == KEY_UP_PRES){1 R4 d' ^ h- X& p- Q' r- t( ^8 p& U" Q
- printf("KEY_UP_PRES write data...\r\n");( f4 l, X9 \4 o5 C- w+ l7 h+ }
- W25QXX_Erase_Sector(0);
! D2 p; n/ T/ F5 h2 M4 A - W25QXX_Write(wData,0,256);
5 o5 a' V2 u/ \7 s, b - }
3 `$ J( E: C' C) \4 @$ l' | -
6 |) o# D, c0 B- e. E - if(key == KEY_DOWN_PRES){
# ^' m+ z: [' s3 t; t7 F - printf("KEY_DOWN_PRES read data...\r\n");
0 K* h+ t9 r& V, }" x - W25QXX_Read(rData,0,256);
. [% A; E. [$ J& I* ^3 W# Z( f - for(i=0;i<256;i++){
$ z" b2 r. j/ Y Z" Q- ~ - printf("0x%02X ",rData[i]);
- h1 Y4 c# Y/ L$ `* M - }
: k& K" B- o* L' U- c0 p1 z - }
6 |' e6 [' J! K. O: T6 U -
8 D3 l4 } J/ T1 f - HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);9 M5 q7 b6 w; m2 q C& B
- HAL_Delay(200);
: U1 u* [1 I G+ o6 [, X! E - }
& a/ _+ ?5 l# T" t" k4 M - }
复制代码 , K- i% j' J0 K( T+ u3 v" {
4.下载验证 编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息 ; q! O$ C5 r! f6 [
. }5 H6 v ]- }& l& K- G8 C
8 E- n" [/ Z/ B1 x# R- E转载自: 嵌入式攻城狮
- ^) y8 Q$ `' d l% L- x如有侵权请联系删除( j, u! w& D. y9 O
# h7 z! W% k# E6 q
( j: H, a- @9 J" `+ @& U. a
8 ^4 [4 @& k+ S$ [, y( R |