1.SPI总线及W25QXX芯片 1.1 SPI总线简介 SPI全称Serial Peripheral Interface,即串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在EEPROM、FLASH、实时时钟、AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的、全双工、同步通讯总线,在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局节省空间提供了方便,正是这种简单易用的特性,如今越来越多的芯片集成了这种通讯协议。下图是SPI内部结构简易图 / { R+ {: r) m9 ]. h
2 V9 T6 A& x2 S! ]( ?: `从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。 SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下: & J# ?* @: q& Q% G2 V" t( g
MISO:主设备数据输入,从设备数据输出 MOSI:主设备数据输出,从设备数据输入 SCLK:时钟信号,由主设备产生
0 B4 k7 v, X x8 J( |5 t! W3 ~0 UCS:从设备片选信号,由主设备控制 4 G0 G \% A* c0 D; e
! _; s6 d5 \3 }1 a5 s% [5 w, @
SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。
4 U( w3 U6 Q( ~# X" q: b当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。, i, }: M* a( S) T3 H/ l7 Y6 S
SPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式: , g) |7 u4 |: U/ C
CPOL=0:串行同步时钟的空闲状态为低电平 CPOL=1:串行同步时钟的空闲状态为高电平 CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样 5 Z/ g6 m) \$ K5 v4 @8 l
CPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
3 z; ^2 y" T/ l+ j; B4 E. A- v7 k" J& A; d9 k) A) f: A
% P- P( t% ?" Q6 E& p' K' h! F1.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的常用命令表
0 G. u& @( q) ]; ?( I ^+ i$ c$ _/ n9 b! _" p& u: X4 H
2.硬件设计 D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息
8 ~ v+ p( m' Q: I3 \指示灯D1 USART1串口 W25Q64 4 K& ^/ x6 X. s# o2 R
K_UP和K_DOWN按键$ M* n- o9 T( a, X3 {
9 a! i$ X( ~4 \- Q
) G6 @0 t) b. S7 F
5 F# z% G; y) _+ q8 T
" E! E6 i. _& x) K. x# k- r
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为软件控制 % \( p+ o" O! a$ j; }
. i5 P, M5 O% F3 c. D8 b
➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码 . P+ [- f9 X) J: ~
3.2 MDK-ARM软件编程 ➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中
) d/ E$ J, p2 g8 ?8 R7 E$ `( {3 F
- void MX_SPI2_Init(void){ {; V+ {" M2 M& J/ p: U
- hspi2.Instance = SPI2;* S* s p3 n4 G) m o$ i7 @
- hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式+ s$ v8 T$ e0 e9 P/ }$ s
- hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式
: K! y" k. {# u* I$ m; [ - hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度5 b$ T2 I1 X3 G8 D+ ?8 s
- hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平
2 s& l; G0 Z6 P! F0 R% p# I- t - hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样
$ r& R$ B6 W3 a" b. J - hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制
% Y4 G7 n s2 i" S6 G$ ]7 k( L - hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子256/ B6 a7 ^4 e) e& O& K3 V
- hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行6 U8 L ~0 a, d+ F3 ~ V6 d
- hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式
* U3 o4 Q/ Q2 w0 \' n4 | - hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验+ d! r( H' L* U
- hspi2.Init.CRCPolynomial = 10;
% v% m! o% D' G8 \8 t9 I8 L" ^ - if (HAL_SPI_Init(&hspi2) != HAL_OK){
3 m2 K+ O: \) |6 C8 F - Error_Handler();
$ e7 T+ a) a" ~, Y - }( Z: U! o7 C {, x
- }
4 ~$ R3 J$ { u9 I+ Y3 s - . M( A: s* ^1 O) S
- void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){
: Y$ n9 U- Q6 C1 ?0 D( \2 |# |, i - GPIO_InitTypeDef GPIO_InitStruct = {0};* F0 P2 L+ h8 e6 T
- if(spiHandle->Instance==SPI2){$ f E2 `8 E* p. ^2 k1 p$ u
- __HAL_RCC_SPI2_CLK_ENABLE(); * b2 |; r; H! U2 D+ F3 r# [
- __HAL_RCC_GPIOB_CLK_ENABLE();
2 [& }8 ]" v' ]$ C( b5 ?9 ]3 P - /**SPI2 GPIO Configuration 4 t& E, }5 \$ @. D$ u! w2 A/ I2 M+ l
- PB13 ------> SPI2_SCK
8 h& x. h1 K3 L+ H+ c( Y) C - PB14 ------> SPI2_MISO
' |8 t1 e5 K6 ?3 W8 Y% l - PB15 ------> SPI2_MOSI */
4 g( ~6 a ]9 C% R- |; W# a - GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;
* _0 S; g3 @, e% [" G1 Q - GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+ T, F6 B0 N. V h6 f* I - GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;, {% d' J% x+ j1 P$ c! d
- HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
8 B, K% v/ C, { - GPIO_InitStruct.Pin = GPIO_PIN_14;
9 b/ _4 [' _- `1 k8 n. Z0 u% H/ o - GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
L4 c2 \# i& U) Q - GPIO_InitStruct.Pull = GPIO_NOPULL;
6 {. X6 O6 d+ _ - HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);$ P) Y: d# r4 ~4 L6 |6 a
- }
- ]) B9 V" \7 a* J: d9 W - }
复制代码
. P( U/ c! P( c➡️ 创建按键驱动文件key.c 和相关头文件key.h ➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍 - //这里仅介绍几个重要的函数
: p. m) c3 v0 g! X. m - void W25QXX_Init(void){% f. O3 M* [ g6 v! j1 t) B4 D
- W25Qx_Disable();( {* W \+ P0 f: `* Q* F
- W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID' I& M5 Q/ T- |; _# H7 s- H4 `
- printf("FLASH ID:%X\r\n",W25QXX_TYPE);
7 G0 Y7 e3 X$ O+ z8 X' I - if(W25QXX_TYPE == 0xc816)1 C" H, Y. v$ H, ^. _1 Y9 ]4 _
- printf("FLASH TYPE:W25Q64\r\n");
6 F; L8 i I7 m3 m( \ - }
# d6 t+ [# a. @( v4 ]1 k
5 i# M# n7 T8 n: F5 i6 |- uint16_t W25QXX_ReadID(void){9 l* M# t7 L7 x! W: H: @
- uint16_t ID;
: y5 r" M. Y4 w( M+ s - uint8_t id[2]={0};# ]% b N4 Y; z
- uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令 / y0 T; U E2 y7 G- X% A/ A
- W25Qx_Enable();//使能器件
- P- n# | a1 x - HAL_SPI_Transmit(&hspi2,cmd,4,1000);" \1 o7 X+ v; i! O5 I6 T3 t
- HAL_SPI_Receive(&hspi2,id,2,1000); s$ Y2 j% ~: J
- W25Qx_Disable();//取消片选 5 c+ v. N' |3 Y; S* j' K0 C: M' b
- ID = (((uint16_t)id[0])<<8)|id[1];4 X4 u6 s# V% u) A! b
- return ID;
6 p' W z/ |6 j. a. J- y - }
$ \0 h) j V8 [7 F
( B; b1 |% ]$ e; e7 a- void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){, n+ u) `3 J6 S( ]& K9 z
- uint8_t cmd[4] = {0};
6 S$ V5 ?# x6 E2 ?6 h" r5 H - cmd[0] = W25X_ReadData;//读取命令2 o/ o3 E2 t+ e9 g/ A
- cmd[1] = ((uint8_t)(ReadAddr>>16));
9 n) v* X+ S: d) D - cmd[2] = ((uint8_t)(ReadAddr>>8));. X n3 b5 J- A& S0 g
- cmd[3] = ((uint8_t)ReadAddr);
$ u, \5 c$ w) R( v& m
8 A0 _; q. d0 N8 P5 K7 f/ K- W25Qx_Enable();//使能器件
4 a5 Y# C1 ~' D% V) L - HAL_SPI_Transmit(&hspi2,cmd,4,1000);5 j4 w+ |7 H8 Q" }* J+ K5 H, [
- HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);. d: L7 i3 b' @
- W25Qx_Disable();//取消片选
0 T$ M) d. \3 Z0 G. a% L/ n8 G - }
9 Q( B! _: H/ y6 I7 Y& y* v - # j/ u3 k \. m h( T
- void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){+ m* a# O0 J) v! m/ K1 s8 I1 p
- uint32_t secpos;
L' r# _: C8 O; Q - uint16_t secoff;# Q4 z. s) ~% c6 c# n- a
- uint16_t secremain;
& @9 N/ O& a E" u0 ?. L - uint16_t i;
( t/ y( N3 A0 w- p0 M' {# s, Y - uint8_t *W25QXX_BUF;( ~ U! b( [; M, C9 @% E6 w
- 0 |2 M9 L E. s! c
- W25QXX_BUF = W25QXX_BUFFER;
/ j6 X3 l& i/ ?0 e) k - secpos = WriteAddr/4096; //扇区地址- ?6 U1 q( A: Z& U
- secpos = WriteAddr%4096; //在扇区里的偏移% v0 n, w! W! ?* D9 A" ^6 o
- secremain = 4096-secoff; //扇区剩余空间大小
, y& ]3 o8 |% a% E - printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);
, u1 l9 ~ w7 [$ H7 m - if(NumByteToWrite <= secremain) //不大于4K字节3 G; ^9 v. ?% \
- secremain = NumByteToWrite;
! V) ]8 `2 V5 v - while(1){/ z3 u5 u: b `3 n' m6 n; \
- W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容 d$ p& K' ^* O" x1 S2 w
- for(i=0;i<secremain;i++){//校验数据
" r! Z$ B$ f! |- w2 d" r - if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除
2 R& z: x7 n' M' U. Z7 v - break;
+ |6 \& z* ?9 J1 [ - }
; `% A! P" q1 r% e' ^; s# {3 w -
4 p! q3 T8 {' `$ U - if(i < secremain){//需要擦除; o9 ?) |- [- j. Q
- W25QXX_Erase_Sector(secpos);//擦除扇区
% Y) h C9 ]0 U; \0 A* X - for(i=0;i<secremain;i++){
1 z5 {: Q4 e8 N1 i3 @& E - W25QXX_BUF[i+secoff] = pBuffer[i];6 h! G. s/ K1 g
- }
1 V+ C$ Z/ m. k4 ~% j - W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区
8 D$ _3 Q# Y1 g - }
" K' l6 l: v( L& i* _" K- q - else{0 c d5 n! ?( C7 i+ {( w
- W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间7 i5 \) l9 i$ d7 z
- }
% o% n; R+ k2 G/ p% x7 U -
& ^# r `0 v' C0 K( }! d& A8 O - if(NumByteToWrite == secremain){//写入结束了
1 ~, D9 t6 O+ l. N* N - break;" |: F3 K% F: n$ X
- }' w2 K! G0 s9 x/ X1 | J" ~6 Y
- else{ //写入未结束8 g7 g, y$ I9 w/ C
- secpos++; //扇区地址增1& S" ~9 C- G/ `& t& q* M
- secoff = 0; //偏移位置为0 " j7 F; I Y3 r
- pBuffer += secremain; //指针偏移
; p+ [! X" s& C, }" s7 r - WriteAddr += secremain; //写地址偏移$ F2 H) i2 k# z1 O( o( t( K
- NumByteToWrite -= secremain;//字节数递减
6 x% I0 {9 R0 }$ O* v6 X# ] - if(NumByteToWrite > 4096)
; ^& t1 @) r. x$ P% Q! I2 g0 s - secremain = 4096; //下个扇区还没是写不完
& ^. y) I# ~# t1 a - else- ?1 _( p0 \" Q
- secremain = NumByteToWrite;//下个扇区可以写完了3 \* t0 j J# t4 c# K3 y& Y8 i
- }
7 V* O# [# o/ T5 I- p2 Q - }
6 b9 q1 P$ S% f; C! K2 P: R - }
复制代码
. Q9 _; L) d) S5 z1 ?3 r6 L5 H6 L➡️ 在main.c文件下编写SPI测试代码 - /* USER CODE BEGIN PV */4 d2 Q4 c' _' ^" Z, ~2 i5 O, U
- uint8_t wData[0x100];
8 @$ C6 p( B% f: G" Y - uint8_t rData[0x100];
$ b4 E& `7 P; x) e - uint32_t i;, }# [1 f9 X2 b: |, J8 t- ?# o) n
- /* USER CODE END PV */! T2 b0 [) Q5 g4 B' Q% a
- int main(void){
. m: Y$ H1 n* V2 I% y - /* USER CODE BEGIN 1 */. [3 R: q) d+ {7 j, C
- uint8_t key;, l$ R7 A7 E8 A# h
- /* USER CODE END 1 */
1 e# c7 P6 d' ~: ?2 T K$ E3 B, V - HAL_Init();" g. K8 D2 s; _8 L2 B' j
- SystemClock_Config();8 E+ O/ \+ ]( c& i
- MX_GPIO_Init();2 D ]# X$ S) t% Y: r' A/ Y1 h/ q# Y
- MX_SPI2_Init();
: I( b" z8 f: d" z" i' b, H - MX_USART1_UART_Init();
7 S4 p1 V; ]$ [0 ?1 k7 j& H* ` - /* USER CODE BEGIN 2 */& ~2 R4 R0 J+ ~. R0 w8 ^! b) d
- W25QXX_Init();% X7 B" l. q5 M9 |
- for(i=0;i<0x100;i++){
e' s' D% U3 k - wData[i] = i;
* P/ z! v+ w ?( X2 f% G - rData[i] = 0;) C$ a G, R8 v
- }
6 t6 b0 z: P6 x3 R) c: X2 i3 w" X6 C - /* USER CODE END 2 */
! m+ R2 S& v3 f$ L& y* ^ - while (1){
* m3 Z' W/ |1 ^, D9 a# U - key = KEY_Scan(0);
3 w- O ?: S `6 G) A) P3 e2 v - if(key == KEY_UP_PRES){' n# d& j- T" \6 u
- printf("KEY_UP_PRES write data...\r\n");
8 y% `. @ U$ T5 Z( A/ ^) z! F& ? - W25QXX_Erase_Sector(0);5 a' M# H; q/ E0 D* }* M2 u4 X
- W25QXX_Write(wData,0,256); # }2 x7 }0 F& [/ U/ |2 a! b1 c! V
- }
5 E" `% R' {( x- f - $ \. }$ J. a6 O; k5 Y9 z
- if(key == KEY_DOWN_PRES){
U" L7 P4 I' } - printf("KEY_DOWN_PRES read data...\r\n");
! f9 G {0 A: X8 F$ F. S - W25QXX_Read(rData,0,256);) _- M' f E- e* W1 d; w
- for(i=0;i<256;i++){
, v2 X8 c% h; v/ y' X7 b6 ] - printf("0x%02X ",rData[i]);
2 Q+ } X# Y }3 e3 _0 v ` } - }
9 }9 X1 S; J6 i+ W/ P - }1 s% w8 Y9 M% }8 B' C) t, A
- # x( ^4 ~; e( h& A
- HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);# `" G+ D4 @2 g5 R5 b
- HAL_Delay(200);
7 t! `+ `$ O. Q2 g; E - }
, `5 h+ Y- d2 B) { o" B# C - }
复制代码
' c2 x" T) I8 @4.下载验证 编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息 6 j$ C' n/ p% Z4 u. P
$ k2 M& |( J; y4 H" A- m! U! e
) q5 ~5 q1 j. \2 l; c转载自: 嵌入式攻城狮
6 u* t! t3 g# y ^3 ?- `7 W. Q如有侵权请联系删除. O' |0 r7 I! U
7 o8 `6 F1 G0 j, Q) f, Q' [
, [. f% i$ l2 G5 x* a- K$ a% L% Z0 e: B" A8 U% Q! D6 C, D/ Z
|