你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】HAL库 STM32CubeMX教程十四---SPI

[复制链接]
STMCU小助手 发布时间:2022-3-23 14:00
在我们的HAL库中,对硬件SPI函数做了很好的集成,使得之前SPI几百行代码,在HAL库中,只需要寥寥几行就可以完成 那么这篇文章将带你去感受下它的优异之处,这些优异的函数,也正是HAL库的优点所在) y' Q$ n& ^' U4 a

4 o3 g0 J8 Q0 j4 I8 O8 j所用工具:4 D1 s% A. n$ N& O( L

4 I7 R/ ]9 I* D- f1、芯片: STM32F103ZET68 j5 Y: c2 e: E0 @2 P" E9 f

9 o  Q- M# C: c2、STM32CubeMx软件
! A: n3 [2 i* O" T; F. h; O& W& \8 `4 s9 Q* p* a0 ^  A. ?, v7 ?
3、IDE: MDK-Keil软件
3 v% n+ t5 D( r. J5 L. U  F4 z! L; }
4、STM32F1xx/STM32F4xxHAL库+ `# y+ V* L8 m4 Y* ?! u7 o: z

1 G: p( B4 q% N$ |! ^! K0 m, R5、SPI: 使用硬件SPI1) e# B& ^8 e9 `* i* T6 U

$ `) k% k2 m- e; S知识概括:( ?; E: {: ~4 z" ^

. H8 O8 ?3 d* a/ I# X6 j0 B, s' o* u( e通过本篇博客您将学到:
% M# c+ {: _9 L4 \
; [" z* t1 u4 [; gSPI的基本原理6 m% O2 i8 A+ M5 w. ~

7 z* c# x* X# ASTM32CubeMX创建SPI例程
2 l% _; P7 B3 X2 n! ]# J  y7 |, G& n2 ]
HAL库SPI函数库3 v! B; Q6 p  n8 P9 X- Q

/ [% u0 b" j; @% J% l0 @4 h$ c什么是SPI7 K; {! g: N6 v$ V1 Z
SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola(摩托罗拉)首先在其MC68HCXX系列处理器上定义的。) m) }+ @2 V1 t& v- t1 g# T% `
; j$ T6 ?* E/ A/ g% c4 N
SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。. L& [  R% O2 v) Y8 U8 U6 G
2 d4 R6 L( j1 R# U
SPI主从模式0 N$ k# T# k7 B% s2 q! d& [# y
SPI分为主、从两种模式,一个SPI通讯系统需要包含一个(且只能是一个)主设备,一个或多个从设备。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起。当存在多个从设备时,通过各自的片选信号进行管理。
2 }/ j8 x0 u9 ?) G( D
& t& [# \8 B5 @; bSPI是全双工且SPI没有定义速度限制,一般的实现通常能达到甚至超过10 Mbps: I0 {1 G* p& ^3 G1 r
. U5 @' R- \/ s$ [- p1 |# q) n4 m
SPI信号线

, A0 J5 m6 R6 r& d4 S& z. N3 V3 h: OSPI接口一般使用四条信号线通信:
: _% J: R( _; W3 T. cSDI(数据输入),SDO(数据输出),SCK(时钟),CS(片选)9 z/ I) L: S* X5 }. L# @

; A* r$ M$ E& N& eMISO: 主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
( D; q9 M% U) \* @, [- TMOSI: 主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。8 W" l# G* C: y5 d( R/ i
SCLK:串行时钟信号,由主设备产生。* r" e# u$ T3 d1 ^3 u
CS/SS:从设备片选信号,由主设备控制。它的功能是用来作为“片选引脚”,也就是选择指定的从设备,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。
6 D9 c/ N" z; ?# E9 {% l" ^硬件上为4根线。
( H/ Y8 E% w1 y' ~2 O/ L: n3 w- x$ d- ~* X6 v3 C7 f
SPI一对一
3 R6 h8 I* E2 h  d) Q0 ?
" d/ p' T( J7 T7 \, I, t
U4AND{]Q35]1A2B[VX~{)$P.png 1 t0 E/ a4 v' N5 j% e6 Y1 x  c4 g
# w9 d* `/ B: T8 B  B7 F
SPI一对多' W* T% ^& S$ P* `6 |

7 L0 c" Z5 A! O( P$ x& [ ~EU(]W4UD{ZK@3[0U}6[WT5.png 5 r6 s) L; a' _: l; }9 p

. _( O7 @1 L" Q5 T* [SPI数据发送接收
* b# q* W* {& R  }" _1 R' f% _0 ]& FSPI主机和从机都有一个串行移位寄存器,主机通过向它的SPI串行寄存器写入一个字节来发起一次传输。
0 l& ?5 ?1 C/ l" b( F
. j/ r& j1 P% t- S1 m7 H1.首先拉低对应SS信号线,表示与该设备进行通信
% _" w6 H2 p2 ?9 @7 A! n% x2.主机通过发送SCLK时钟信号,来告诉从机写数据或者读数据
. [* G( u0 z4 c8 u1 c. ^这里要注意,SCLK时钟信号可能是低电平有效,也可能是高电平有效,因为SPI有四种模式,这个我们在下面会介绍- f, X+ N2 n1 u, S' t* I6 }* J
3.主机(Master)将要发送的数据写到发送数据缓存区(Menory),缓存区经过移位寄存器(0~7),串行移位寄存器通过MOSI信号线将字节一位一位的移出去传送给从机,,同时MISO接口接收到的数据经过移位寄存器一位一位的移到接收缓存区。6 j: C5 F+ d5 Y( q; W4 f# b5 |
4.从机(Slave)也将自己的串行移位寄存器(0~7)中的内容通过MISO信号线返回给主机。同时通过MOSI信号线接收主机发送的数据,这样,两个移位寄存器中的内容就被交换。; W( A. g8 ]! \; k

$ Y/ g; j, B$ b8 q0 r }9IPL%WBI(SXK~POT{1{A.png
) F5 L, @2 p/ M6 |8 u- ?1 {; b" r2 z3 E$ n( ^( a
SPI只有主模式和从模式之分,没有读和写的说法,外设的写操作和读操作是同步完成的。如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。也就是说,你发一个数据必然会收到一个数据;你要收一个数据必须也要先发一个数据。
# ]4 M# Z$ {0 ~1 B: P; _6 H. Z1 l4 J& D5 a$ S$ ?
SPI工作模式" O7 h  p& ?7 Z  Q4 Q* Z
根据时钟极性(CPOL)及相位(CPHA)不同,SPI有四种工作模式。
2 g  s" M& ?- r4 k时钟极性(CPOL)定义了时钟空闲状态电平:
+ A  i0 Y; Y- i+ jCPOL=0为时钟空闲时为低电平
3 H1 m- c/ N5 T, m# C1 QCPOL=1为时钟空闲时为高电平" `( J' N- x2 a1 p' E
9 a: c  Z; [( X0 T" f) d- W
时钟相位(CPHA)定义数据的采集时间。
! O0 Z2 C1 d: O9 Q$ WCPHA=0:在时钟的第一个跳变沿(上升沿或下降沿)进行数据采样。1 D. q, o1 j3 J# h& F: ~! w6 ]
CPHA=1:在时钟的第二个跳变沿(上升沿或下降沿)进行数据采样。
8 m# A7 B2 v+ g% d/ L
- L; B, ^+ Z$ Q. m WSKLH~CQVK{U~ENH5BE{QEP.png 0 z. p5 P% [( U- f7 [) T. P' |) P
% k: w* b3 k' }' r
W25Q128 FLASH芯片介绍

- ^9 g  ^0 o# jW25Q128是一款SPI通信的FLASH芯片,可以通过标准/两线/四线SPI控制,其FLASH的大小为16M,分为 256 个块(Block),每个块大小为 64K 字节,每个块又分为 16个扇区(Sector),每个扇区 4K 个字节。通过SPI通信协议即可实现MCU(STM32)和 W25Q128 之间的通信。实现W25Q128的控制需要通过SPI协议发送相应的控制指令,并满足一定的时序。
; H; V9 I: |7 ^5 G* x# n4 ]
) j# s/ y8 Y9 Z0 A, t原理图连接

. ?" Z0 i' K8 \* ?/ w! d2 j0 v
5 H5 k) g& a4 H# s9 W9 R S65UQWR]EZ(@1G2M{E3~IV2.png ) p2 K6 u) @0 e! P  P

7 }: T# ?  j4 I6 M) \- Q) ~: v. X常用指令:
4 R1 L' g8 c2 p( Q' \+ ^8 J9 k8 \! n3 c" h: m9 W
写使能(Write Enable) (06h)
: T8 f1 n- n1 d0 }* p* q
% G7 D4 t, T, u# S, q# j: [& `
XPQX%GRHXNY))9_Z864R%SE.png , t6 C  o$ r/ U/ w
+ z7 v+ M8 M- P* a0 w! ^4 B
向FLASH发送0x06 写使能命令即可开启写使能,首先CS片选拉低,控制写入字节函数写入命令,CS片选拉高。
; p% b* H3 d! e
# A5 t7 \4 E1 n& ]% l# M0 T5 ^扇区擦除指令(Sector Erase) (0x20h)6 r, ^9 b  c, E+ D0 i$ F$ ?7 o: i
  p. V! Y! V2 M% G3 c0 A
_%`E7{PK0@02IY3PO3~E1.png
2 h: u% W2 q7 o, b扇区擦除指令,数据写入前必须擦除对应的存储单元,该指令先拉低/CS引脚电平,接着传输“20H”指令和要24位要擦除扇区的地址。% u) J; d8 V# w$ K/ L' E
! Y% g/ ^+ p5 U' s
读命令(Read Data) (03h)
5 Y4 m/ O5 _6 z- k6 _: |+ H  H4 }+ ^( |1 Q4 _. R
读数据指令可从存储器依次一个或多个数据字节,该指令通过主器件拉低/CS电平使能设备开始传输,然后传输“03H”指令,接着通过DI管脚传输24位地址,从器件接到地址后,寻址存储器中的数据通过DO引脚输出。每传输一个字节地址自动递增,所以只要时钟继续传输,可以不断读取存储器中的数据。
' B) T# F# z+ C+ U. p8 B* a/ w0 `5 S" r6 i4 K8 h
* u( M9 L5 P4 d$ \" q/ V  }# }
/ U% _3 W- r8 n; r5 y, A
状态读取命令(Read Status Register)
  j" k: d$ i# Y0 u6 m6 \
1 M$ f1 ]( z3 C3 d- O- j* @7 }/ S
读状态寄存器1(05H),状态寄存器2(35H),状态寄存器3(15H)7 L8 r3 @% k+ m9 Q
写入命令0x05,即可读取状态寄存器的值。- t- r& M+ V$ K) c3 l

/ I: z6 _( h9 w) Q" y2 ^% { `~_0F1_G[R2)T(M80VV1S8J.png
+ S0 h8 g# g2 Y
4 ^2 o8 t( r2 y% w( p, u, U+ E写入命令(Page Program) (02h)
: n$ W* _  a% a( P& h$ N8 F, j
4 ?( x' {% ~" k
REU_LMITYA3M}MQZPO8G2T3.png
* v+ @, c' M" L) \& Q5 |, l% l: ]# E3 h# j. |/ w
在对W25Q128 FLASH的写入数据的操作中一定要先擦出扇区,在进行写入,否则将会发生数据错误。
0 p& p3 |" V5 n+ z) xW25Q128 FLASH一次性最大写入只有256个字节。
$ a, U3 y! h4 G6 W在进行写操作之前,一定要开启写使能(Write Enable)。- }. g# L6 u' p
当只接收数据时不但能只检测RXNE状态 ,必须同时向发送缓冲区发送数据才能驱动SCK时钟跳变。
- m+ [7 c" K6 m% w0 i8 |$ M# I4 i$ }' S$ f7 U. t
基于CubeMx的讲解3 \2 D; m2 U5 q: [8 n
1设置RCC时钟

; B; A1 P6 M9 I) _) A& s$ l/ h+ i8 T1 E- n1 @
A9XVC[L){CQYZLTRY6A]3~B.png 6 h+ }7 c7 V+ X$ ?( n+ J
; Q2 `4 W* f6 k' ^* x
设置高速外部时钟HSE 选择外部时钟源
$ q0 ^2 _8 x9 t3 g
) G/ {1 J6 s2 q6 j4 l0 R2 SPI设置
1 Q  b1 R; d2 W  t  l( o( @. H' Q" h. C/ a) Z! L1 b
SPI2设置为全双工主模式,硬件NSS关闭,如下图:

6 N1 K& N( M# o. d) u! ~" \
0 M" h$ p( R( U, L, y/ `# B+ g _OJ{EZE)BGBQ5P@`)0tLU.png   V* E( p9 G* @" @* B  ~# [! w6 Y$ l
* v, P2 ?- \3 W9 m6 A" Z3 J
08)B_EEMT$II8GC[19Y_K9K.png
1 S( Z8 C2 J( B
/ P* Y! d; `0 L& k& Z+ Y模式设置:
! B1 y; k2 m4 G  k
3 b- r- v9 H$ k  k3 l$ R有主机模式全双工/半双工
) ^2 \% J; m7 i9 t$ Y' T) ~从机模式全双工/半双工, a9 W# ?% I4 j. C( `; ~. z
只接收主机模式/只接收从机模式
7 T" B8 L$ [0 r8 g5 a0 F" C只发送主机模式
/ S5 l' Z5 D* H- F因为我们是和W25Q128V芯片闪存芯片进行通信,所以设置为主机全双工" @5 E' o0 t9 i/ r9 T" I0 f

7 m/ Z5 v' U; e不使能硬件NSS

. e: ?0 A- Z; w8 g8 {
. |. `- R5 _3 R- k 7OP}KVCLW_WDG@T$N9}Y`BJ.png & ~" ]+ D1 L) t* y: H' l6 C
7 d+ X1 w; o7 t5 V8 \
STM32有硬件NSS(片选信号),可以选择使能,也可以使用其他IO口接到芯片的NSS上进行代替3 B# ^' u$ |; I$ w5 L! ~- W
/ n. s; d# u, w1 Q
其中SIP1的片选NSS : SPI1_NSS(PA4)
7 O7 r- ?: }! ^5 v% w其中SIP2的片选NSS : SPI2_NSS(PB12)
/ P' ~% ^7 `: V! p5 W4 e

( |% q( a* b6 i  t& `3 Y如果片选引脚没有连接 SPI1_NSS(PA4)或者SPI2_NSS(PB12),则需要选择软件片选. [4 m$ a! j$ V* m
9 k* r9 j, f- T8 @, b
NSS管脚及我们熟知的片选信号,作为主设备NSS管脚为高电平,从设备NSS管脚为低电平。当NSS管脚为低电平时,该spi设备被选中,可以和主设备进行通信。在stm32中,每个spi控制器的NSS信号引脚都具有两种功能,即输入和输出。所谓的输入就是NSS管脚的信号给自己。所谓的输出就是将NSS的信号送出去,给从机。: Q2 B5 W3 p, A, H" F4 M4 M$ |, c7 b
对于NSS的输入,又分为软件输入和硬件输入。
* h$ z& p# s) c: [+ S软件输入:4 _* y/ i( Z; [0 k- H
NSS分为内部管脚和外部管脚,通过设置spi_cr1寄存器的ssm位和ssi位都为1可以设置NSS管脚为软件输入模式且内部管脚提供的电平为高电平,其中SSM位为使能软件输入位。SSI位为设置内部管脚电平位。同理通过设置SSM和SSI位1和0则此时的NSS管脚为软件输入模式但内部管脚提供的电平为0。若从设备是一个其他的带有spi接口的芯片,并不能选择NSS管脚的方式,则可以有两种办法,一种是将NSS管脚直接接低电平。另一种就是通过主设备的任何一个gpio口去输出低电平选中从设备。
; z! Q& G; S. g6 `: x硬件输入:
. H& Q" x6 {4 m7 A主机接高电平,从机接低电平。
- d  X; O- d8 g2 W) c; z6 Z- `! m  O4 W1 ~
左键对应的软件片选引脚,选择GPIO_Output(输出模式),然后点击GPIO,设置一下备注。
' v7 i" a) f; g3 e6 Y. M- i% P1 ^3 q
我这里虽然PB12是SPI2的硬件片选NSS,但是我想用软件片选,所以关闭了硬件NSS
4 [. c9 s1 S# |; K/ P- U+ {, G( t: @2 u1 }, s
~%UZ[)HI6C((0BNCD{K(H]7.png
( C( f- l+ `' Y* t! q* U- V7 {" u; ~2 V
3 ]2 K! c4 Q% j- H: ? S%%O[%JBTU~PH@~2S~J9@.png 4 @+ x8 }& b/ |

$ q+ ]( ?  h2 m- W+ s( m; @
" F6 p" v$ d: l% Q+ C" u/ FSPI配置默认如下:

- Z: {+ X- H! y( X7 J; F1 ~0 c5 _5 [' M& Y( J* n- Z
SPI配置中设置数据长度为8bit,MSB先输出分频为64分频,则波特率为125KBits/s。其他为默认设置。
4 }% O& y( W. r! D; f5 ]0 aMotorla格式,CPOL设置为Low,CPHA设置为第一个边沿。不开启CRC检验,NSS为软件控制。
/ {7 f  Y1 _( L6 N+ k- o0 C
0 ?; `- ^" [/ \2 r3 T! L MNT@YEQZ`1}LDM0BBI05MGK.png 6 s) R( e3 {2 y& f7 x1 ^: ?
3 X  L- C1 Z2 E. w, I
最后记得初始化一下串口,因为需要测试例程,发送数据到上位机。6 s4 O$ s9 ], v+ @) I+ r- |: C
3 A; n& J3 K2 @$ {: {' j5 Y% U
))_{$KKQG4]2X{X`R(XLEPT.png ' E* u) U' }& U

% ]5 E  z" d8 S3时钟源设置
1 o# Z! X$ @# h; a+ E& J4 X/ ]/ w) e% J$ R
8Y(B0VG8[GH)O[VF06P0JCU.png
6 N3 U* l6 ^) ?* @我的是 外部晶振为8MHz
1 ^( R2 q# \/ Z% e& E
9 i, `  D5 W6 i' W5 E1选择外部时钟HSE 8MHz
4 ~3 }! u; r: A, a2PLL锁相环倍频9倍# c! J& x) K# K  q( ^. a
3系统时钟来源选择为PLL
; F- D$ Z/ L2 T$ Q( z; u* g4设置APB1分频器为 /2) I- r! i' \$ }, U9 ^
5 使能CSS监视时钟
5 b/ N" E# b- D* Y& S
) x# C' w# c6 ^# O( n% \32的时钟树框图 如果不懂的话请看《【STM32】系统时钟RCC详解(超详细,超全面)》# ]- D  R1 g7 G; F7 E

* p% O  J+ j. z* f: f' y/ X. W& N4项目文件设置
, T. y& ^& h* [0 Y1 L
( D- Y+ E# M: f% g. Y5 E0 |( M
K[I448YQ}X]MOK]ZO1[6C.png * Z& s& K/ m; D1 J. W  {) h2 U

3 J. B! }" q) ~. i* b! B# z' R4 ?1 设置项目名称6 i1 N1 U% C! F1 w3 W# x  w9 T- q
2 设置存储路径% u1 H2 E# I& d" k0 {
3 选择所用IDE
, w! o# \+ U4 [
/ I9 E3 Q8 V- L) M GPR)1~0V)H[YO(A)U2I]`LF.png 3 u# R5 s& {5 F* b# o% x' S. K

- G5 l$ y% O; F7 d& m( R. V5 i  f5创建工程文件

* D  `6 L( V4 a  w6 [9 y. o  x& V2 B" d/ k& i$ f
然后点击GENERATE CODE 创建工程- N6 W% U2 j5 q$ B4 i
: l% f3 r8 g' V+ l3 u% I/ ]
配置下载工具

5 L; z  }- u9 J( m新建的工程所有配置都是默认的 我们需要自行选择下载模式,勾选上下载后复位运行
: B. I, v, j1 D' b8 p+ w: B3 x
9 j9 a- x4 U8 ~, G K)5%$]7K[06O]YRL~9QE)Y4.png 7 H+ Y1 L* Q: N, z( ?

5 v8 E: ^, v% R, I- MSPI函数详解2 k/ v, Z: n, D# Y( I8 X3 j( k

$ P, e2 B/ T7 @# g. B5 X$ F在stm32f1xx_hal_spi.h头文件中可以看到spi的操作函数。分别对应轮询,中断和DMA三种控制方式。
' `" O9 h& |2 ~
0 H) u; H% |+ V& {: e+ D KEO@HHK%AOM{_HUQ0H4FNWN.png & X4 \4 O; J; z/ |1 V% D  G

( y  p1 E3 i; C- I+ E# S, m! [轮询: 最基本的发送接收函数,就是正常的发送数据和接收数据
- U$ Q* r$ F0 p# }; m中断: 在SPI发送或者接收完成的时候,会进入SPI回调函数,用户可以编写回调函数,实现设定功能
, t2 U% Q2 Z* k* j1 D% tDMA: DMA传输SPI数据
' e7 O- C# ]8 e' e4 ^
% j; ?) E! C8 B% u利用SPI接口发送和接收数据主要调用以下两个函数:- \$ h( i( J! p* B, d. F
  1. HAL_StatusTypeDef  HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//发送数据
    : }; ^/ C, [$ A7 v
  2. HAL_StatusTypeDef  HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//接收数据
复制代码
# E& P0 y% r6 J1 P: m6 a+ `& \
SPI发送数据函数:3 P6 E: H6 E* h
  1. HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//发送数据
复制代码

1 I  v; y3 d2 u4 u$ s. G参数:
- `/ t; ~5 p' I! {+ P( k6 H7 d) o) z9 y4 W7 \) Z- k( [
*hspi: 选择SPI1/2,比如&hspi1,&hspi26 V0 O$ x% R8 v# b6 c$ N
*pData : 需要发送的数据,可以为数组
+ ?2 l( a% @2 s- k# I9 gSize: 发送数据的字节数,1 就是发送一个字节数据9 f% _' |0 P  E7 V& \
Timeout: 超时时间,就是执行发送函数最长的时间,超过该时间自动退出发送函数
% E3 x+ O5 ^& L, q8 y$ P) X" V$ O+ {  s* J9 k
SPI接收数据函数:7 F/ A+ W* v! @$ r" F
  1. HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);//接收数据
复制代码
9 k: S$ i! Y/ M9 f2 a
参数:- n$ D& b, o( z5 A
*hspi: 选择SPI1/2,比如&hspi1,&hspi27 u" s1 _8 o2 W
*pData : 接收发送过来的数据的数组! \2 @, `, M' L& ^1 H
Size: 接收数据的字节数,1 就是接收一个字节数据
! _, h% J+ Q/ J% o  v, E) L0 |Timeout: 超时时间,就是执行接收函数最长的时间,超过该时间自动退出接收函数
" r5 n$ D( \% t( k5 W6 U) n1 A
4 `" k$ b4 B/ a6 D6 N
, ^1 A% W0 e8 I5 H5 x; d8 ~SPI接收回调函数:  e: t, T! w/ ]9 f1 H
  1.  HAL_SPI_TransmitReceive_IT(&hspi1, TXbuf,RXbuf,CommSize);
复制代码

( J5 P) ]$ E" |" Y当SPI上接收出现了 CommSize个字节的数据后,中断函数会调用SPI回调函数:
  1.    HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
复制代码
$ A& i& s! l: W9 v
用户可以重新定义回调函数,编写预定功能即可,在接收完成之后便会进入回调函数+ E3 N  q$ v# t3 E+ |( Y$ s7 s  W

: ]$ F# k1 ]5 M% n; |* P$ a3 U% l/ T' M片选引脚:
& Y1 w' c" H, y4 n9 P- v/ d1 Q6 I6 J) V- q
因为我们是软件使能片选,定义片选引脚,CS片选低电平为有效使能, CS片选高电平不使能0 j  w: y$ L' `6 B# K- |
4 O  t+ B: ~9 {( q% Y- E: W
这里用两个宏定义来代替
& J  Q1 T, H$ Y( H9 @& |1 N# x9 G# N: @% g" \/ y
在main.h中有宏定义命名,SPI2_CS_Pin 就是PB12
  1. //以W25Q128为例
    ' X# \1 i6 ~+ U, z$ |
  2. #define SPI_CS_Enable()                         HAL_GPIO_WritePin(GPIOB, SPI2_CS_Pin, GPIO_PIN_RESET); W, J$ H$ g% |
  3. #define SPI_CS_Disable()                 HAL_GPIO_WritePin(GPIOB, SPI2_CS_Pin, GPIO_PIN_SET)
复制代码
# V  k0 }9 y4 H, z# C4 n6 O
SPI例程详解
  g4 _1 K" r: N8 D; F/ N
因为不同的flash芯片通信协议以及方式都是不同的,所以这里介绍下具体的SPI的发送和接收应该怎么写,具体的请看芯片手册修改下即可,这里提供下W25QXX的驱动文件,以及测试例程,测试是正常没问题6 Z( i  d  `$ @6 F# l

+ M. g8 q. X3 N1 `) T挑几个函数讲解一下:
% ]! C' E# c. @6 O3 m; A! ]) S  X8 t: Z! \" h  D& f
在w25Qxx.h钟可以修改CS片选引脚,W25Qx_Enable(),W25Qx_Disable()分别为使能和失能SPI设备,即拉低和拉高/CS电平
  1. #define W25Qx_Enable()                         HAL_GPIO_WritePin(SPI2_CS_GPIO_Port, SPI2_CS_Pin, GPIO_PIN_RESET)
    / h; p4 q7 I/ c* r9 V
  2. #define W25Qx_Disable()                 HAL_GPIO_WritePin(SPI2_CS_GPIO_Port, SPI2_CS_Pin, GPIO_PIN_SET)
复制代码

* V7 ]  ^( d) X4 K* K) rw25Qxx复位函数:
/ w: p) |# n. ?% T' ]5 Y
) n' z4 W9 R( ]
函数开始先将要发送的数据(命令(0x66)和地址(0x99))存储在cmd数组中,
5 Q( _; v2 I5 B) g7 \$ A拉低片选信号,开始SPI通信
7 s" G- E' @5 i8 r然后后通过HAL_SPI_Transmit()函数发送出去* c% |8 R) A  {/ ?
拉高片选信号,关闭SPI通信  m/ B$ }4 Z" W( l6 j4 f6 `
2 K% E6 l2 n8 _
W25Qx_TIMEOUT_VALUE是最大超时时间,在w25Qxx.h中定义为1000,单位为us% S5 Z" Z* u1 ?. ^  U1 P
: x# e, A2 l* C
FA)X9VQ]S3IY97FX]V8X1FW.png
* }6 M% c* |& o% h- s. Z
  1. /**% p' D1 p/ o6 u9 ?
  2.   * @brief  This function reset the W25Qx.
    8 \7 j8 q3 F# j# n* b5 F: ?# @
  3.   * @retval None
    $ l/ E% }. _8 z. Y- [- L! S3 P
  4.   */
    5 d; Y& J: @6 @* S* ?
  5. static void        BSP_W25Qx_Reset(void)6 l2 \  I% C. J' Q, d" h# A6 i
  6. {
    . ]' X" F9 q( \; }
  7.         uint8_t cmd[2] = {RESET_ENABLE_CMD,RESET_MEMORY_CMD};: k4 {& {/ U& f9 Z% h
  8.         
    , t& @7 t5 _( K) _: F
  9.         W25Qx_Enable();# m, s% K2 X" e  o
  10.         /* Send the reset command */
    5 C5 D: n. ]# k. ]
  11.         HAL_SPI_Transmit(&hspi2, cmd, 2, W25Qx_TIMEOUT_VALUE);        3 G3 t' w8 f" B) ~, h% y/ R( E3 c
  12.         W25Qx_Disable();
    # Z* X# a! j, m5 R- B

  13. & q1 ]0 s3 t1 ^* z, E
  14. }
复制代码

" \% ?3 j7 `; N2 H8 e& I# W7 m$ PW25QXX读函数:
3 y1 o, b, ~  v, j2 c6 S三个参数:
2 Y5 A9 k/ D' i0 B- [4 h  TpData 存放读取到的数据的数组
' |/ S2 x7 A: h! W9 W* j5 l0 zReadAddr 读取数据的地址+ N' o" G- r( g8 W) C
Size 读取数据的大小4 ~: ]/ @  l7 L8 o8 }+ Q. X
…- `0 ]( H; R+ t9 W: ^
0 V6 c9 t5 x) E
函数开始先将要发送的数据(命令和地址)存储在cmd数组中,
# M6 p, e1 D. [% x/ B8 F+ E拉低片选信号,开始SPI通信
8 f+ c# P8 n! T$ T& a8 {然后后通过HAL_SPI_Transmit()函数发送出去,首先发送写命令(0X03),上方有讲解,然后发送三个字节(24 Bit)的地址
2 p1 n4 {" k! I) C# C4 h7 A% ~* G  h接着通过HAL_SPI_Receive()接收读取的数据。6 i3 h  n4 ]5 t+ Y" j3 |5 g
拉高片选信号,关闭SPI通信
3 D) I8 D, P8 T
4 I+ v3 F: {* m9 f0 H& h9 f$ E ESCEO7$I`2621{[NT1@4MFY.png , m) z5 w: k* r

+ x0 I( [: m& c6 l) R
  1. uint8_t BSP_W25Qx_Read(uint8_t* pData, uint32_t ReadAddr, uint32_t Size)+ f) j+ f3 u* @) A
  2. {  C. o2 e! E7 P! E: H  a
  3.         uint8_t cmd[4];, k; o3 U, k9 d4 `; z" d
  4. 0 Q* X% t* m* @, t% F
  5.         /* Configure the command */
    2 {* B+ t  S; ~3 s
  6.         cmd[0] = READ_CMD;1 b# |) F! f* R5 r5 `
  7.         cmd[1] = (uint8_t)(ReadAddr >> 16);
    0 q4 p( T3 K1 g3 [; B; G: c
  8.         cmd[2] = (uint8_t)(ReadAddr >> 8);) O2 a' S6 T7 |* O# M" H/ W
  9.         cmd[3] = (uint8_t)(ReadAddr);
    ; i! X7 U9 L0 I3 m7 X2 v  R/ F
  10.         : z. |. V. K4 I
  11.         W25Qx_Enable();" n. y& X: P. Z$ D; m
  12.         /* Send the read ID command */9 T( ?/ Z( ]+ c; m. H4 `1 c
  13.         HAL_SPI_Transmit(&hspi2, cmd, 4, W25Qx_TIMEOUT_VALUE);        
      L' ?- T4 B- A: j5 b' e- f
  14.         /* Reception of the data */' B' \4 d, ?! ~, Q2 ~
  15.         if (HAL_SPI_Receive(&hspi2, pData,Size,W25Qx_TIMEOUT_VALUE) != HAL_OK), J6 S3 p( @9 g2 }
  16.   {
    # s4 M* X' `; `6 _4 e; d1 Z
  17.     return W25Qx_ERROR;7 _+ U% H5 b1 Y! o  a0 t
  18.   }3 k0 m$ S: M- g) y# d1 J1 A( N. T$ [
  19.         W25Qx_Disable();
    - Q9 A. p  [) g) x: O
  20.         return W25Qx_OK;9 n$ A  n' j$ u" V+ O- |  @
  21. }
    . H; M" r) |# _) o$ g' A
复制代码
3 B; l3 a% w7 Q
写使能(Write Enable) (06h)
" r8 [! q. F* L5 f' j, G9 T! L5 r2 r
  {# a, E5 l! e* m) P
_H${X10%GJ3V4NONTH0B1}7.png
8 K: k) ^2 Y; Z0 h0 K1 x
5 m8 q2 }/ f% t" X" w) V8 p向FLASH发送0x06 写使能命令即可开启写使能,首先CS片选拉低,控制写入字节函数写入命令,CS片选拉高。1 D/ ^# M' Y, e' G$ @

& T: I: d) s5 w8 v; B& B
  1. uint8_t BSP_W25Qx_WriteEnable(void)- \( Y& N6 X$ J$ q/ n
  2. {
    5 c3 E1 `8 [" [$ Y. R6 F
  3.         uint8_t cmd[] = {WRITE_ENABLE_CMD};  C6 f7 b- S9 S' o
  4.         uint32_t tickstart = HAL_GetTick();
    5 |- f# {. L8 ]: b

  5. / z1 g: V* N) S. H3 y/ ^
  6.         /*Select the FLASH: Chip Select low */( O6 J" t6 W5 @8 n# e1 T
  7.         W25Qx_Enable();3 i- U, i, R$ ?9 f
  8.         /* Send the read ID command */
    % V* S6 I9 ?7 N% y1 b; J  m* w* n
  9.         HAL_SPI_Transmit(&hspi2, cmd, 1, W25Qx_TIMEOUT_VALUE);        
    ! Y% b. U* _- _+ M$ l, ^5 J& p) }
  10.         /*Deselect the FLASH: Chip Select high */, I1 q* x+ Q1 b8 [6 N$ ?$ h
  11.         W25Qx_Disable();* k5 h: j5 A$ l. t6 R' ^/ L, E% N
  12.         
    2 Z1 N4 b$ n1 Z4 s& p* t+ w
  13.         /* Wait the end of Flash writing */
    $ _) F3 A% l" r  X) g$ c' }) e
  14.         while(BSP_W25Qx_GetStatus() == W25Qx_BUSY);7 n( P) l9 Q7 O- R) p! u' _
  15.         {
    9 J/ ^, j( W% Q' G  A' C3 ~$ l) u
  16.                 /* Check for the Timeout */8 c- `; l; ^; R' o. T; i" ]8 s
  17.     if((HAL_GetTick() - tickstart) > W25Qx_TIMEOUT_VALUE)8 C3 |$ p+ V( e! q& c+ Z
  18.     {        
    $ l9 k* L7 ]9 k; b
  19.                         return W25Qx_TIMEOUT;- }, ?& g6 m2 a  j8 j4 ~
  20.     }8 M- o) X9 C8 M% C, E, N3 X! L4 P' H
  21.         }6 c+ c" {6 @. X* I+ W
  22.         , o; |% L" M' ^- i7 ~/ [" }7 d1 W
  23.         return W25Qx_OK;) s1 v- j# M6 E7 B4 I( L1 Z
  24. }
复制代码

2 \! x2 P, b% A1 |/ q9 W. X扇区擦除函数:8 D  \  C% c! S7 T, Q5 m/ t

. b7 `$ f. ]! V& m7 G扇区擦除指令(Sector Erase) (0x20h)
& m3 H+ \7 |) u& e& \
6 `* Z  n: {" w" O6 E' w5 \- U
EY@MZDOQ(DHAJFP0UNAWDTQ.png 4 H0 c3 B$ W. a" }# ~3 K+ s

  V$ j3 U6 d9 T, P扇区擦除指令,数据写入前必须擦除对应的存储单元,并且使能写操作,该指令先拉低/CS引脚电平,接着传输“20H”指令和要24位要擦除扇区的地址。判断flash是否为忙状态,如果不为忙则擦除操作完成。7 c! q9 o& Q6 Y( k/ I8 e$ G& P
. G% f- @' j+ k7 J/ ^% b* q
  1. uint8_t BSP_W25Qx_Erase_Block(uint32_t Address)" Z6 }8 w1 z9 s; h8 ~& [5 j# g
  2. {/ P% o. H: `2 p, V: H, C5 F' m3 M1 m
  3.         uint8_t cmd[4];
    2 }2 t+ B1 w& ?6 I5 P
  4.         uint32_t tickstart = HAL_GetTick();
    9 ~7 A" Q2 }0 A- w2 i- O
  5.         cmd[0] = SECTOR_ERASE_CMD;
    0 ?" w9 \, ^! |( B# v- ~
  6.         cmd[1] = (uint8_t)(Address >> 16);
    % k5 e3 {- e. Y& n4 h, H
  7.         cmd[2] = (uint8_t)(Address >> 8);
    # A  E% w- o9 n. ?8 s
  8.         cmd[3] = (uint8_t)(Address);
    0 L) }9 l7 U9 _- c
  9.         
    * u! E1 b1 c0 v9 j& m5 z% C' K' q
  10.         /* Enable write operations */
    9 e1 `$ @: a' O6 U) f" j! O& S9 |
  11.         BSP_W25Qx_WriteEnable();# m1 u5 }" W& c0 {3 M, [
  12.         
    , N: u, B9 F+ v1 ~9 \
  13.         /*Select the FLASH: Chip Select low */* R& ~0 f! Y1 ~3 l8 _% e9 `
  14.         W25Qx_Enable();
    $ {9 m& B4 S# S% ?' O- J5 ?8 d
  15.         /* Send the read ID command *// I8 K! [  }* a+ o5 f% J4 z
  16.         HAL_SPI_Transmit(&hspi2, cmd, 4, W25Qx_TIMEOUT_VALUE);        $ O# p. ?  E- [2 q
  17.         /*Deselect the FLASH: Chip Select high */
    1 R5 ]6 ~! Y6 B2 L
  18.         W25Qx_Disable();
    8 J6 k: ^+ q9 i( W
  19.         * J- C. {4 `( O6 D$ I3 X
  20.         /* Wait the end of Flash writing */
    % N& D9 B- S9 ^+ q1 q  l
  21.         while(BSP_W25Qx_GetStatus() == W25Qx_BUSY);
    ) e4 k! `' n5 I6 Z
  22.         {2 j9 B5 n0 x' i5 ~4 @/ u; V
  23.                 /* Check for the Timeout */4 d, p0 R; f4 A
  24.     if((HAL_GetTick() - tickstart) > W25Q128FV_SECTOR_ERASE_MAX_TIME)! k) V4 B2 w; N( ?; G
  25.     {        # q. q' M' E6 V9 e" J- L
  26.                         return W25Qx_TIMEOUT;
    ; R4 J& b" h  @- [
  27.     }; R. U! k+ _: I- e* W1 b
  28.         }& ?9 a5 f$ _5 @4 ?3 K/ L- O8 r
  29.         return W25Qx_OK;" c( i7 p* g& j# A- G" O( `7 |' h
  30. }
复制代码

: Z5 V. W7 N, X# c例程测试: h5 z3 V4 Q7 T, `  G& W& [  t1 t4 e
重新定义printf函数

* X( H1 V8 d& Z% j0 C. x7 C" M在 stm32f1xx_hal.c中包含#include <stdio.h>
7 c0 X" a0 R8 {$ z0 M
/ Z/ Q* l2 ^, [" g
  1. #include "stm32f4xx_hal.h"8 {. h3 O. U  }. r  {+ }2 Y
  2. #include <stdio.h>
    3 E) w. f. i0 |! _0 \6 ^& g; D  m7 v
  3. extern UART_HandleTypeDef huart1;   //声明串口
复制代码

7 q4 d* e, n( E. H8 `  ]; E在 stm32f1xx_hal.c 中重写fget和fput函数
: h  A9 R* N/ p, P' v+ T- ?, v5 [
7 P. w+ X4 X  j0 `- g4 n4 M' _& W- `
  1. /**0 J3 ]" t5 g0 o- Z
  2.   * 函数功能: 重定向c库函数printf到DEBUG_USARTx3 H8 q& K9 Z& c9 B! t+ {1 g1 d
  3.   * 输入参数: 无' ?" K1 v" t. h( \0 t6 C
  4.   * 返 回 值: 无
    * O2 l+ L& k. C2 f
  5.   * 说    明:无
    ! [& d, f. W% k
  6.   */: T  r. _5 Y9 ~3 _  T# M
  7. int fputc(int ch, FILE *f)
    3 W% i+ z$ X5 F  p2 ]- e! z+ o
  8. {
    3 @" a+ ^0 f" x) y# I
  9.   HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);" c0 `  m, x2 P
  10.   return ch;5 |, `& E% |- m- {1 o
  11. }9 V, n. K! \# d; [7 t6 x$ \4 k

  12. $ `5 h' h; u- A
  13. /**
    ' X- t9 k7 \+ q5 M/ L% C! e
  14.   * 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx" l1 w9 Q5 g. K
  15.   * 输入参数: 无% S( e2 u/ ]3 X
  16.   * 返 回 值: 无
    ' u' p- {, T( q) n3 S* ]9 H
  17.   * 说    明:无
    + |+ j2 R3 X& ]. ?8 i% B
  18.   */& k" _1 K% r6 Y; C$ F
  19. int fgetc(FILE *f)) D  Z' t+ H8 k9 C/ M" y5 z( C% t, L
  20. {' R* l; r. M7 I) t# f8 E! w! |& W
  21.   uint8_t ch = 0;
    1 l) O' ~3 N: n1 v" f
  22.   HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
    ! {  N3 ~1 B. M0 J# G
  23.   return ch;
    1 a. \/ N- a, s: S) @  W
  24. }
复制代码

) @, ^$ _. V4 t7 Rmain.c
" E- J8 E0 M7 K% y3 }在main.c里添加以下代码:2 M% u4 a) M4 t1 X7 l) B

  |4 V/ A9 ^# _2 P4 x! W: h
  1. #include <string.h>
    , p# P# i! F% _0 W
  2. #include "W25QXX.h"
    % L& q, S0 o7 }9 D: c" N, j
  3. 5 h# j* a0 ^6 z% `
  4. & i- \, S: T5 l0 w2 _
  5. ; @* m$ Q, f) C% i  V
  6. uint8_t wData[0x100];
    1 S1 J) D8 m8 L3 i# `) B, @
  7. uint8_t rData[0x100];
      P& X. V3 c! U& B: m$ O+ R( b
  8. uint32_t i;
    ) l7 ?4 Y# d6 a) ^
  9. uint8_t ID[2];
    8 q# C5 j$ _% Z8 d' V) a0 S
复制代码
  1. printf("\r\n SPI-W25Qxxx Example \r\n\r\n");
    0 v+ y8 f. {7 J

  2. # a  {9 e! P" @5 I6 F" W( c: Y
  3.   /*##-1- Read the device ID  ########################*/
      X7 O( h: ]6 K  h9 V9 g
  4. - \, Q- s4 X9 e
  5.   BSP_W25Qx_Init();
    ( a: Y3 W, a: n6 L% _% `
  6.   BSP_W25Qx_Read_ID(ID);1 `8 D0 c! D& u5 _, B, C2 ~, h

  7. % N3 t- a. |, F" ~
  8.   printf(" W25Qxxx ID is : 0x%02X 0x%02X \r\n\r\n",ID[0],ID[1]);
    . t7 B! Y+ _+ r' ^# {) a7 F1 r* S
  9.   /*##-2- Erase Block ##################################*/
    $ }3 p9 ]7 [. S( ]* d
  10.   if(BSP_W25Qx_Erase_Block(0) == W25Qx_OK)) M: g. f" a. D! r& M5 H" ^
  11.       printf(" SPI Erase Block ok\r\n");
    1 ?0 ~4 [0 @7 d# K- D$ O
  12.   else$ G' H5 |) j( D( ]9 R; G
  13.       Error_Handler();
    ! y. o! [) L) U3 G0 C' N, k& o
  14.   /*##-3- Written to the flash ########################*/
    / H( Z- z3 d- h# P
  15.   /* fill buffer */
    + A0 d9 S( r! p* G+ c2 H% e3 i
  16.   for(i =0;i<0x100;i ++)
    . T0 b5 O9 Y. F
  17.   {
    ( i: S* _/ @, J) J5 U3 h6 j' [
  18.           wData<i> = i;
    6 f: j; R0 S& H4 `' H
  19.         rData<i> = 0;+ M% K4 E+ [" R
  20.   }
    # @8 I" O; _7 }2 h  e
  21. if(BSP_W25Qx_Write(wData,0x00,0x100)== W25Qx_OK)
    # C  q$ j# D; t, R
  22.       printf(" SPI Write ok\r\n");
    ' f% N5 n! u) N& `
  23.   else, h% ^: J, p* p" h4 N
  24.       Error_Handler();
    1 N3 R: v$ J! C
  25.   /*##-4- Read the flash     ########################*/# G0 I/ M' f+ n& L# P$ w0 v
  26. & ?8 \" Y6 y+ J4 K8 P/ U
  27.   if(BSP_W25Qx_Read(rData,0x00,0x100)== W25Qx_OK)4 @' ]" S; m- {# k
  28.       printf(" SPI Read ok\r\n\r\n");
    9 _; r$ T, `9 Y% B- t& U
  29.   else2 A" y0 k8 Z  y7 a( I
  30.       Error_Handler();
    ' P8 r& b+ ?7 Q
  31.   printf("SPI Read Data : \r\n");8 o1 _$ i6 D0 h) X& N+ t! t% X2 F
  32.   for(i =0;i<0x100;i++)
    * ~# T, L: }% j/ T
  33.       printf("0x%02X  ",rData<i>);
    , z7 \1 g) i$ ~! `" d9 v
  34.   printf("\r\n\r\n");
    $ w/ u0 c) Y3 [7 R/ i
  35.   /*##-5- check date          ########################*/   
    5 @! O  J2 a7 Y& ^( [0 J4 _
  36.   if(memcmp(wData,rData,0x100) == 0 )
    ! n& P# j9 g* h# V5 A6 {
  37.       printf(" W25Q128FV SPI Test OK\r\n");
    0 k" c( ^" L. \, \
  38.   else
    9 }( b, V9 r6 _
  39.       printf(" W25Q128FV SPI Test False\r\n");
    , k5 S- Y0 c% S9 `0 V
  40. </i></i></i>
复制代码

2 b6 p( `- w4 G; g8 \STM32F103测试正常:) C6 x) d- O* a7 _0 N: e  @: |
( C9 x) {/ x% z
W[@WHY6O~O%7YTVLCI@9J(F.png ! x$ h( p0 q! {7 K
1 j' S: T; T8 m* Q. s, F8 K
赞 收藏 评论0 发布时间:2022-3-23 14:00

举报

0个回答

所属标签

相似技术帖

官网相关资源

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版