1.SPI总线及W25QXX芯片 1.1 SPI总线简介 SPI全称Serial Peripheral Interface,即串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在EEPROM、FLASH、实时时钟、AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的、全双工、同步通讯总线,在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局节省空间提供了方便,正是这种简单易用的特性,如今越来越多的芯片集成了这种通讯协议。下图是SPI内部结构简易图
* M6 g: f) m/ m0 s" w$ c' W0 ]7 b2 T/ f
从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。 SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下:
) ]& l" B+ y5 i; I6 eMISO:主设备数据输入,从设备数据输出 MOSI:主设备数据输出,从设备数据输入 SCLK:时钟信号,由主设备产生
s( |, e- m! T$ A }CS:从设备片选信号,由主设备控制 ; m% @% y: P. r5 c- x
% k/ D8 [- i5 P2 \+ N, S6 M
SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。
- ?3 `$ f( L" d* ~9 n% i当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。
& R0 K$ I- ~! x- YSPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式: 7 H' R' C* v b$ F" Z, f6 U
CPOL=0:串行同步时钟的空闲状态为低电平 CPOL=1:串行同步时钟的空闲状态为高电平 CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样
& a8 h" g3 K ~' c5 CCPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样 ( I$ @1 W% Y, x$ \1 Y( m5 e
3 m/ u$ ^+ K2 r' K4 w ~/ X & ]" [+ e$ g2 J x
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的常用命令表
: \5 G( N( D- y6 h, @4 B6 ^8 B+ m" L
2.硬件设计 D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息 5 h+ _/ ?# A5 v6 Z! T. B6 A8 d
指示灯D1 USART1串口 W25Q64
& m1 n2 b$ n6 UK_UP和K_DOWN按键9 T5 }2 R; u2 w2 U2 r' r v
6 q2 ?( B% l; e5 }6 |! x! s1 S: r
- P5 O. h: S& J k; p0 u
: t2 U* \; u6 f; c2 `6 h6 T2 @* A
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为软件控制
: H) ^, y8 V: e# t
/ ~: Y* J' n# O+ p$ N* ^2 \7 }➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码 c o" l0 j) B3 ]3 V
3.2 MDK-ARM软件编程 ➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中
" D; |/ [+ i" B- h3 r" g# ^( n
- void MX_SPI2_Init(void){- x9 W6 e6 p7 M$ b- G7 v
- hspi2.Instance = SPI2;
1 H- w) G) i8 N6 J/ ?2 H - hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式
2 I& Y- i$ \ e0 ~ - hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式/ l! P2 H2 o7 W7 p2 X
- hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度1 [, O/ f! F- B+ n# Q9 X
- hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平
% x$ Y# T; F3 w - hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样
) @3 y& T. i$ d" ~$ _" _ - hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制9 R+ l# O4 t# f# y, B
- hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子256, p' M1 n8 h, t0 V# \
- hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行. i( j/ n0 V ]2 W; C- s
- hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式
' ]3 f6 p* g. ?2 ] - hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验
1 ` k; \- o4 E& b& s - hspi2.Init.CRCPolynomial = 10;
# N; ^! k$ T, G - if (HAL_SPI_Init(&hspi2) != HAL_OK){
; t" v- Z" E3 Z" D/ a - Error_Handler();) l$ [5 h; z0 q
- }
3 E3 a$ ~! k+ P8 Y' f. a! I/ o8 ] - }
! u; _+ T5 R* t2 a8 d/ V. w7 b
* J g/ H" Q7 q3 x; U0 A! m& n- void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){
' h- `$ U m) {- g3 ?( v3 z - GPIO_InitTypeDef GPIO_InitStruct = {0};
! q7 M9 ], K% I8 v% m - if(spiHandle->Instance==SPI2){, V- R1 N) c q' f1 B) e- Q0 P: ~1 Y
- __HAL_RCC_SPI2_CLK_ENABLE();
( ?7 `4 z) f# X - __HAL_RCC_GPIOB_CLK_ENABLE();4 \6 G% R3 F+ ~$ P/ I5 c
- /**SPI2 GPIO Configuration
. ~$ V$ }8 r( r - PB13 ------> SPI2_SCK/ {$ c! ?% V, Q. L3 |! F
- PB14 ------> SPI2_MISO
# ]3 f6 c3 [8 L - PB15 ------> SPI2_MOSI */
4 u* Y: t" f& \# `+ _ - GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;
8 ^* v+ [5 ?2 n/ J2 v$ ` - GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;; i2 j1 g( t; C, P3 E
- GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;4 R2 q, B$ J$ {. s# B+ J, m
- HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);% r! r1 l X) d! k8 c+ T1 y
- GPIO_InitStruct.Pin = GPIO_PIN_14;* D9 w2 U; W4 ]& L
- GPIO_InitStruct.Mode = GPIO_MODE_INPUT;8 z. E6 k' X' x0 m8 V
- GPIO_InitStruct.Pull = GPIO_NOPULL;
( E. c% m3 B: B* r3 W( H - HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);5 H e7 e+ v2 O m& B+ E/ c: m* O
- }1 Q" Y5 I( M( M; @5 M/ I9 O0 u o8 J" D
- }
复制代码
6 V+ j0 H& @& ~2 p- Z➡️ 创建按键驱动文件key.c 和相关头文件key.h ➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍 - //这里仅介绍几个重要的函数
. C9 a- R' f- [3 o. k0 I1 u - void W25QXX_Init(void){5 p4 |& C! X# N e( S& h% g0 q
- W25Qx_Disable();
* l4 F" R) `) F) B1 H$ r! g - W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID1 N; l K* ]6 A \9 E) l
- printf("FLASH ID:%X\r\n",W25QXX_TYPE);
1 l' B6 H% \( H - if(W25QXX_TYPE == 0xc816): t; Q$ D, R3 F) `# R# h) Z
- printf("FLASH TYPE:W25Q64\r\n");
2 Q; z8 U& }: K# R; P, M - }8 g# o5 w; f5 a+ }9 C2 j4 B; D
. C H4 a; ?/ P' Y0 u- uint16_t W25QXX_ReadID(void){- t0 g$ _, ~8 y
- uint16_t ID;* D7 m( z$ h3 ^& W t2 w( n X
- uint8_t id[2]={0};+ F$ k4 h' x8 l$ F0 A9 o
- uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令
. c! O& d1 Y5 ]3 x' [( Q p% y5 A - W25Qx_Enable();//使能器件+ C. f3 W$ A l6 d5 z) R
- HAL_SPI_Transmit(&hspi2,cmd,4,1000);
3 D. ~$ t% t# ~- h% K; e* i' a# K - HAL_SPI_Receive(&hspi2,id,2,1000);; q, j# V9 A4 }7 ]+ `
- W25Qx_Disable();//取消片选
! Y! M" E% K, N* L& V o" o - ID = (((uint16_t)id[0])<<8)|id[1];
' \% R) y8 ^+ T - return ID;+ X3 Z. ]; S$ e ~) B& \
- }# K& g% ^1 Y$ |+ \) g
- + T3 g- R/ s$ {! [" `0 A8 E; B J
- void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){; a* r% |8 h1 K W9 y* ~% |5 E" a
- uint8_t cmd[4] = {0};
( W( D. r) R' ]% a9 G - cmd[0] = W25X_ReadData;//读取命令: {. l A- M- O8 {
- cmd[1] = ((uint8_t)(ReadAddr>>16));
% d# {: |! o9 V v: |0 Y - cmd[2] = ((uint8_t)(ReadAddr>>8));
: S o, z7 z, ?3 ]9 Y! G" H - cmd[3] = ((uint8_t)ReadAddr);2 j. M) ]4 u3 V% X
- 7 b- {& c* e( M: \3 _( Z
- W25Qx_Enable();//使能器件" g: e; P" G" k! e
- HAL_SPI_Transmit(&hspi2,cmd,4,1000);1 a: X) _+ l. e: b
- HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);/ z9 r3 t4 ] S' d8 A+ h- T
- W25Qx_Disable();//取消片选
+ U) O+ {) P0 Y( E6 T9 B# F - } |. W x$ R: g' N
- 4 I+ ?; g) i8 n. E5 v- ^
- void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){1 c: G6 q/ ~! _, j9 y$ Y0 i' z' [
- uint32_t secpos;$ T6 x& \ O$ d$ ~
- uint16_t secoff;6 U8 j- L% x& J/ y
- uint16_t secremain;3 l% @$ L& k' w% C; Q
- uint16_t i;6 n7 l3 L+ u) S; y. u r! r* \
- uint8_t *W25QXX_BUF;
d0 i% `0 s% A5 o -
/ {$ G' n( e" r& l - W25QXX_BUF = W25QXX_BUFFER;
/ U& t( n2 L9 d& H - secpos = WriteAddr/4096; //扇区地址5 Q ?4 k0 |0 B+ V# k
- secpos = WriteAddr%4096; //在扇区里的偏移9 E2 G% A. v$ m" m w, S
- secremain = 4096-secoff; //扇区剩余空间大小: c& n# R/ s/ {& C
- printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);
3 @$ Y" `7 S1 o% u7 T3 t - if(NumByteToWrite <= secremain) //不大于4K字节
6 K# {6 C% K4 p* R! n - secremain = NumByteToWrite;& e( f( j2 J, M- t- T" O* N
- while(1){/ B2 j% l* v" l* S- Y/ B# o1 u
- W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容
! M8 i$ E& a, S, k- X9 _2 j7 B4 ^ - for(i=0;i<secremain;i++){//校验数据8 i8 U$ \/ M' g/ j1 D1 R- K7 V8 O
- if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除3 z2 d Y$ H1 M, d! }) y+ U
- break; }; x& c1 C n$ @3 y% Y
- }; r. F+ _% p4 i5 s0 g0 ?
- . x4 _: V: b9 U" _/ O) h
- if(i < secremain){//需要擦除
3 n: Y0 a: D3 ~ - W25QXX_Erase_Sector(secpos);//擦除扇区
4 G0 J) f; H, i& [# |3 I1 |. h - for(i=0;i<secremain;i++){) P6 t- g* U) R* j3 v- |/ t
- W25QXX_BUF[i+secoff] = pBuffer[i];
1 C3 o+ O/ \9 C! y1 Q! Q - }4 z: D+ ]+ z6 l$ w# s% ]8 _
- W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区% W J$ ~, ?+ {" s% u( S) G8 H
- }/ b0 D4 A9 {/ a% A
- else{6 G& c8 {3 i0 V' t* K E V w: J
- W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间+ |! @; }. a' n C3 ?! g8 Y! t
- }5 i/ o ~. l* q8 V1 n5 a* P
-
' ~9 X7 P/ N" }0 z/ i5 @' w - if(NumByteToWrite == secremain){//写入结束了
1 E+ ]& P) t% d( d/ n8 c - break;
Q3 c5 s% \ E6 Z4 r - }
2 {' J& E( \+ q/ P - else{ //写入未结束2 X4 B8 m, C# ?0 H
- secpos++; //扇区地址增19 _, ?5 R* D( r( L' Y
- secoff = 0; //偏移位置为0
8 M/ O: }( j( k% N4 ^: v* s& D - pBuffer += secremain; //指针偏移3 q# F' p. B! g" [3 F
- WriteAddr += secremain; //写地址偏移/ N2 ^# y' v5 e3 @. J
- NumByteToWrite -= secremain;//字节数递减
& W0 k8 q" X5 E: A- ~; S; K) Y - if(NumByteToWrite > 4096)
: w( W1 _2 Z. S- o: ?+ W - secremain = 4096; //下个扇区还没是写不完
" X9 ]5 [1 {: ^7 \ - else
" h7 o7 s" j+ f, S% `% _! }# A - secremain = NumByteToWrite;//下个扇区可以写完了
6 L, g2 G, w/ ^" [2 S4 M/ ~ - }6 q, ?- W8 ]% u, J2 J
- }* j$ k% W" y+ I2 V% h- ]* X$ k
- }
复制代码
6 E3 G! b! g, z➡️ 在main.c文件下编写SPI测试代码 - /* USER CODE BEGIN PV */ O9 q7 H4 i1 F( P. F2 z4 M4 y% S* I
- uint8_t wData[0x100];
( V$ d% m+ L( o" ^6 K' n - uint8_t rData[0x100];
& }/ l6 x3 w8 P. h - uint32_t i;! C% ^ b5 u) g( V2 d' ]
- /* USER CODE END PV */4 d: `! o3 p0 \" w8 \
- int main(void){
" C# G* i5 Y' i7 P. t* \7 ] - /* USER CODE BEGIN 1 */4 \" H, c m# H$ F, ^9 D- o0 \
- uint8_t key;
6 [5 ^% q0 ?( Z - /* USER CODE END 1 */: \3 L: K6 e2 b3 f; e4 l
- HAL_Init();9 x2 ?5 d, c$ @% f2 `
- SystemClock_Config();1 O6 s4 T4 p% Y. [
- MX_GPIO_Init();
* L4 o4 j+ j7 f6 h; }+ F - MX_SPI2_Init();0 M1 P( q+ Z5 Z! k1 y
- MX_USART1_UART_Init();
' d1 Y& s# D* M/ ? - /* USER CODE BEGIN 2 */
$ V6 i( l8 L( ?: l8 ? - W25QXX_Init();+ l3 ^' Z9 P& k z$ c$ M m
- for(i=0;i<0x100;i++){
: J: f& S" y+ m! z" V - wData[i] = i;7 T* J/ h! h. w b% E; D" g
- rData[i] = 0;
( _& [4 w4 a) k - } & n# {2 x. d! Y+ ]5 D$ J
- /* USER CODE END 2 */6 }" R+ H* T' g% d+ ]5 c! i
- while (1){
& a5 P! q' J" h: W, w: a - key = KEY_Scan(0);
* u; m n4 b' c - if(key == KEY_UP_PRES){9 Y M3 i: b% _/ s
- printf("KEY_UP_PRES write data...\r\n");' s$ J( b& \1 z5 @. {2 d
- W25QXX_Erase_Sector(0);
4 c8 D; z2 y( `+ o: c/ _0 H7 u - W25QXX_Write(wData,0,256); $ l2 x, A {* z: U
- }" q, A( @* ]# k1 P
- 7 z$ Z/ g8 C" j* }
- if(key == KEY_DOWN_PRES){. z2 }, e$ Q6 s' b0 N; U
- printf("KEY_DOWN_PRES read data...\r\n");
, ^9 H5 k! R4 A8 E3 z% H - W25QXX_Read(rData,0,256);$ ^( }" L! N, ]* {
- for(i=0;i<256;i++){
# Q' E+ L* D! i6 C# Q ?4 w# o - printf("0x%02X ",rData[i]);# O* i- o, i! e& Q5 Z1 @$ y) m
- }
d! I4 l3 s2 m/ T+ H* Q2 Y - }* `8 k6 C5 O9 |3 @! G
-
' n1 r- [( Q5 ~: L4 i7 o9 x* z - HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);) v& H) l5 ?& u) ~: X, D
- HAL_Delay(200);
: @2 \8 m% [* g, Y* D3 T% k - }$ A' G, I! c+ M9 N7 w3 l/ K% a
- }
复制代码 ' p5 k2 E/ H$ H6 |$ R" o6 \! c9 G
4.下载验证 编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息 8 O5 D: E) }) P+ u1 L
% T5 r, Q3 e7 Q4 Y8 p( T$ `& E
1 W( {+ ?% J' Q8 ^7 J% k' Z9 F+ J2 Y( t转载自: 嵌入式攻城狮 _5 {4 X3 g2 z. c
如有侵权请联系删除
- n4 @$ [, H# T" Q. \, A
: V H( M; r; o1 C1 s* j2 \
8 r$ {! }( i( G; V2 ]4 E7 Q$ ?+ Z( }# ~5 U; B! O% m, D
|