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

基于STM32CubeMX的SPI总线经验分享

[复制链接]
攻城狮Melo 发布时间:2023-7-1 18:56
1.SPI总线及W25QXX芯片
1.1 SPI总线简介
SPI全称Serial Peripheral Interface,即串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在EEPROM、FLASH、实时时钟、AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的、全双工、同步通讯总线,在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局节省空间提供了方便,正是这种简单易用的特性,如今越来越多的芯片集成了这种通讯协议。下图是SPI内部结构简易图
0 f7 f, p7 w3 ^7 k" i. }
微信图片_20230701185542.png

; v" P, Z1 @+ p6 S2 ]
从上图可以看出,主设备和从设备都有一个串行移位寄存器,主设备通过向它的SPI串行寄存器写入一个字节来发起一次传输,寄存器通过MOSI信号线将字节传送给从设备,从设备也将自已的移位寄存器中的内容通过MISO信号线返回给主设备。这样两个移位寄存器中的内容就被交换。外设的写操作和读操作时同步完成的,如果只进行写操作,主设备只需要忽略接收到的字节,如果主设备要进行读操作,就必须发送一个空字节来引发从设备的传输。
SPI接口一般使用4条线通讯,单向传输时也可以使用3条线,其中3条线为SPI总线(MISO,MOSI,SCLK),1条为SPI片选信号线(CS),它们的作用如下:

    9 z4 Y9 x& _# Y- M
  • MISO:主设备数据输入,从设备数据输出
  • MOSI:主设备数据输出,从设备数据输入
  • SCLK:时钟信号,由主设备产生

    4 z3 s- s7 a+ _$ m5 D0 e" N$ {: V6 s
  • CS:从设备片选信号,由主设备控制

    9 P4 v, x/ Y( d- O* v! g
    # Z" J# b" k1 V
SPI使用MOSI/MISO信号线来传输数据,使用SCLK信号线进行数据同步。MOSI/MISO数据线在SCLK的每个时钟周期传输1位数据,且数据输入输出是同时进行的。数据传输时,MSB先行或LSB先行没有硬性规定,但是两个SPI通讯设备之间必须使用同样的协定,一般都会采用MSB先行模式。
4 a: l0 ^- P# S% X当有多个SPI从设备与SPI主设备相连时,设备的MOSI/MISO/SCLK信号线并联到相同的SPI总线上,即无论有多少个从设备,都共同使用者3条总线;而每个从设备都有独立的1条CS信号线,该信号线独占主设备的一个引脚,即有多少个从设备就有多少条片选信号线。当主设备要选择从设备时,把该从设备的CS信号线设置为低电平,该从设备即被选中(片选有效),接着主设备开始与从设备进行SPI通讯。
% E/ @( c9 O! Z5 ?# `SPI总线根据时钟极性(CPOL)和时钟相位(CPHA)的配置不同,可以有四种工作方式:

    / e! r1 ?7 x* d7 Q
  • CPOL=0:串行同步时钟的空闲状态为低电平
  • CPOL=1:串行同步时钟的空闲状态为高电平
  • CPHA=0:在串行同步时钟的第一个跳变沿(上升或下降)数据被采样
    ) o& A. x0 P6 H$ Y) u3 o0 L
  • CPHA=1:在串行同步时钟的第二个跳变沿(上升或下降)数据被采样
      _4 U6 L. c: r+ d+ Z5 Z

    - e8 P8 {9 I5 v$ t* m# G
微信图片_20230701185539.png

( }; M0 K! w: x1 c4 \' @
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的常用命令表
, f: F3 \5 b# S' N
微信图片_20230701185536.png

* }) p) ~1 A, F1 J) v1 X
2.硬件设计
D1指示灯用来提示系统运行状态,K_UP按键用来控制W25Q64数据写入,K_DOWN按键用来控制W25Q64数据读取,串口1用来打印写入和读取的数据信息
    : n2 ?. q) c$ `+ A( j9 Z3 [/ A5 ?+ S2 K
  • 指示灯D1
  • USART1串口
  • W25Q64
    $ H  l3 Z9 ~/ R- j3 M3 x, q
  • K_UP和K_DOWN按键
    * p3 c) ~1 E! u+ I) ]! a
    ; g5 ~4 U9 ~6 e

    . N& N4 D: p! q( Q6 i
微信图片_20230701185531.png
: P% d7 @3 {8 w3 F

8 a# w0 \; M  a1 P
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为软件控制
微信图片_20230701185526.png
  W0 s- |) E8 J) u

. l& a9 E; j) p+ V7 D+ l) G
➡️输入工程名,选择路径(不要有中文),选择MDK-ARM V5;勾选Generated periphera initialization as a pair of ‘.c/.h’ files per IP ;点击GENERATE CODE,生成工程代码
) ]4 M/ k: ^; Z3 p# b: R
3.2 MDK-ARM软件编程
➡️ 在spi.c文件下可以看到SPI2的初始化函数,片选管脚的初始化在gpio.c中
3 c* {5 i" A2 u$ A
  1. void MX_SPI2_Init(void){" {( Z( L9 I3 T4 }) ]
  2.   hspi2.Instance = SPI2;
    : \6 y  V% P& M% t5 i
  3.   hspi2.Init.Mode = SPI_MODE_MASTER;//设置为主模式. x6 @! a8 C4 P9 {# m
  4.   hspi2.Init.Direction = SPI_DIRECTION_2LINES;//双线模式
    * n3 Z5 a. O3 L- l2 K
  5.   hspi2.Init.DataSize = SPI_DATASIZE_8BIT;//8位数据长度
    & H7 k3 `5 U$ n5 Z6 L
  6.   hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH;//串行同步时钟空闲状态为高电平) }2 L1 @: q  a3 h+ v# y. \6 W
  7.   hspi2.Init.CLKPhase = SPI_PHASE_2EDGE;//第二个跳变沿采样
    5 z9 d6 F3 k8 v' o/ x5 X0 g3 j' b
  8.   hspi2.Init.NSS = SPI_NSS_SOFT;//NSS软件控制( z; b. s4 p9 O0 z& U' ~
  9.   hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;//分配因子256
    7 O5 i" U( F1 @) }9 h* y' I
  10.   hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB先行: R+ C- ]) n; `- I8 O& u" A3 m
  11.   hspi2.Init.TIMode = SPI_TIMODE_DISABLE;//关闭TI模式
    " L5 H/ J6 G+ s) q
  12.   hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//关闭硬件CRC校验" a- h7 F) b. y
  13.   hspi2.Init.CRCPolynomial = 10;
      t2 N9 P4 q4 E" r) s6 H
  14.   if (HAL_SPI_Init(&hspi2) != HAL_OK){9 n8 e, F& F# J
  15.     Error_Handler();
    5 v" C2 _9 U6 i- W+ \' |6 ?
  16.   }: z, e* F; I, m) R. j
  17. }
    # w1 A8 h4 y8 j
  18. . _/ ~( r" A  m1 Q
  19. void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle){* w* Y0 |- F8 I/ Z
  20.   GPIO_InitTypeDef GPIO_InitStruct = {0};, O* Q% C( G7 W! }
  21.   if(spiHandle->Instance==SPI2){
    * T' i: p* g/ m0 P% M% ~6 [
  22.   __HAL_RCC_SPI2_CLK_ENABLE();  
    4 j# M$ @3 L4 p' j9 K. ?
  23.   __HAL_RCC_GPIOB_CLK_ENABLE();. o( ~/ i& {- Z$ X: Z& `
  24.   /**SPI2 GPIO Configuration   
    8 ]; k- g" r7 ]! z
  25.   PB13     ------> SPI2_SCK, g: v) b0 V( w, y; l$ {# ^
  26.   PB14     ------> SPI2_MISO
    ) i2 _' w9 ^5 f) Y) o. q
  27.   PB15     ------> SPI2_MOSI */. d9 m1 F% l3 W4 C* P: H) a% k
  28.   GPIO_InitStruct.Pin = GPIO_PIN_13|GPIO_PIN_15;
    8 \' v9 s1 a- N  B1 ^+ `
  29.   GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    / W, I2 p2 h7 j5 B8 l1 x
  30.   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    + Y/ _/ T+ \  r+ U: e
  31.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    ; d! e& I! A# o
  32.   GPIO_InitStruct.Pin = GPIO_PIN_14;. L: d- N5 W: U- D2 J$ t
  33.   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    1 J) w5 ^1 b" J& z5 h+ z! e
  34.   GPIO_InitStruct.Pull = GPIO_NOPULL;
    $ U& @; s5 s# G
  35.   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);8 u8 Z6 G  R/ U2 Z
  36.   }
    0 z" t/ T* E7 `
  37. }
复制代码

2 g  l' K0 Z$ b
➡️ 创建按键驱动文件key.c 和相关头文件key.h
➡️ 创建包含W25Q64芯片的相关操作函数及驱动函数的文件w25qxx.c和w25qxx.h,这里仅介绍几个重要的函数,源文件下载方式见文末介绍
  1. //这里仅介绍几个重要的函数- J9 E8 z2 p4 z% B3 h. L0 j0 [
  2. void W25QXX_Init(void){
    - M: p/ R0 J: ~2 W0 i2 D
  3.   W25Qx_Disable();
    ' A7 g1 e/ J  ]  z% p' M
  4.   W25QXX_TYPE = W25QXX_ReadID();//读取芯片ID9 r) b8 B2 X& f0 {& `5 j3 v9 a
  5.   printf("FLASH ID:%X\r\n",W25QXX_TYPE);3 i) J. c3 h, U7 P8 C4 o( h
  6.   if(W25QXX_TYPE == 0xc816)
    $ }" _7 ^2 w- [" f" y! {. _8 V% t
  7.     printf("FLASH TYPE:W25Q64\r\n");
    - I6 g9 W# j& }( L
  8. }
    5 {2 Q+ I$ n3 b, r9 b

  9. 2 ]0 ~0 K- F+ U: i  g
  10. uint16_t W25QXX_ReadID(void){! T( H7 \' r$ Z) n+ X
  11.   uint16_t ID;& n% x" l, f+ B4 p! F  W# A
  12.   uint8_t id[2]={0};
    8 z, M. U9 K2 L6 a% H1 t1 N
  13.   uint8_t cmd[4] = {W25X_ManufactDeviceID,0x00,0x00,0x00};//读取ID命令       
    " I; h# n3 h$ `; v
  14.   W25Qx_Enable();//使能器件2 V# w) @/ R+ K4 T* e1 E2 T5 g
  15.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);% L# r* }% a9 c7 @8 C
  16.   HAL_SPI_Receive(&hspi2,id,2,1000);( |: }" l9 y% W
  17.   W25Qx_Disable();//取消片选       
    " K6 T/ F3 a1 [5 c2 Z) L
  18.   ID = (((uint16_t)id[0])<<8)|id[1];7 z  `+ j" ?: R  i" ?7 `
  19.   return ID;
    ! U# k+ Q) h7 _6 F! Y
  20. }
    : `& {/ [% z* T$ {$ s. |  X
  21. * x7 \' p! R; F- r) j8 G
  22. void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead){+ ]# H/ O  @! w
  23.   uint8_t cmd[4] = {0};) m$ Z5 G1 K0 J4 U
  24.   cmd[0] = W25X_ReadData;//读取命令
    7 k3 \9 \7 u8 O7 i3 S& M! H: {9 U
  25.   cmd[1] = ((uint8_t)(ReadAddr>>16));
    - W( K6 O" j4 |: S5 q
  26.   cmd[2] = ((uint8_t)(ReadAddr>>8));$ W  m. x- p9 M! X2 R
  27.   cmd[3] = ((uint8_t)ReadAddr);
    2 H  @; q7 \! v6 C  ?4 N

  28.   S5 F: ^/ c5 u( D9 d( ^3 p
  29.   W25Qx_Enable();//使能器件+ J+ _7 i% k" F1 z& e: d& v
  30.   HAL_SPI_Transmit(&hspi2,cmd,4,1000);
    6 d$ [" n' V+ `1 W7 L8 @8 k
  31.   HAL_SPI_Receive(&hspi2,pBuffer,NumByteToRead,1000);
    6 x+ j3 D& k1 h+ E, X( @
  32.   W25Qx_Disable();//取消片选        * [5 ?! Z: F; V" S
  33. }
    2 I* v- v, [( e( R8 Y3 r

  34. 0 b+ F$ M( H8 o, o( B3 O1 P  ^
  35. void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite){
    0 g3 R! o9 l- z2 z% u
  36.   uint32_t secpos;
    % R& I% M- s( {
  37.   uint16_t secoff;4 p$ q* d* C2 b( W
  38.   uint16_t secremain;( @; D6 w! O7 r: ~
  39.   uint16_t i;2 H( L- G, D+ [1 M; O
  40.   uint8_t *W25QXX_BUF;
    7 c0 h' b5 `# U$ v% y3 B
  41.        
    5 S$ v. A) y1 I5 ]4 X
  42.   W25QXX_BUF = W25QXX_BUFFER;
    ) J$ c# J; N1 `
  43.   secpos = WriteAddr/4096;        //扇区地址% w# Y6 T! x4 G; o$ [+ n
  44.   secpos = WriteAddr%4096;        //在扇区里的偏移% E3 m: @  }! a# S) t
  45.   secremain = 4096-secoff;        //扇区剩余空间大小
    9 p: L( \# @& C
  46.   printf("WriteAddr:0x%X,NumByteToWrite:%d\r\n",WriteAddr,NumByteToWrite);$ f2 p# ?: h0 f
  47.   if(NumByteToWrite <= secremain)  //不大于4K字节4 [4 z) b5 J; |! L  R+ H& K
  48.     secremain = NumByteToWrite;) d1 C: U! W* Y
  49.   while(1){
    3 P7 f) @. `. s
  50.     W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读取整个扇区内容
    / D1 Q6 C4 h4 }
  51.     for(i=0;i<secremain;i++){//校验数据; u+ R* q3 |7 g3 B
  52.       if(W25QXX_BUF[secoff+i] != 0xff)//需要擦除
    ! ]) @2 w0 r* m7 I$ y% u
  53.         break;
    + w. G: h- B1 T  x5 d) E' r+ T
  54.     }$ W6 x- a! o) c8 A, [- F
  55.                 & k# F1 g( _8 [9 r$ z0 N
  56.     if(i < secremain){//需要擦除
    7 e* F3 e8 G: c' |. Q, i: ?
  57.       W25QXX_Erase_Sector(secpos);//擦除扇区
    4 U+ v! E7 D  A# D- z! I, B
  58.       for(i=0;i<secremain;i++){
    1 m. l3 v3 ]) a- d+ k7 V; @
  59.         W25QXX_BUF[i+secoff] = pBuffer[i];
    / ~2 A$ F: }- g9 @( W$ s8 B
  60.       }3 n4 E/ y. p9 q0 I/ K
  61.       W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区2 e7 r) F- M4 G/ k9 K, t& z8 H
  62.     }) _. m% n4 t% {/ l8 S2 |) B
  63.     else{
    ' h; F* y  L6 P# u& o% G# c6 l" R
  64.       W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//写入扇区剩余空间
    / h6 C1 _. y$ S2 b
  65.     }/ n* V; H+ K+ {; |$ A1 s1 v
  66.                
    # M4 f, o$ z  x  p' t
  67.     if(NumByteToWrite == secremain){//写入结束了# L1 E' s' h' s  @+ x- P9 f
  68.       break;
    5 h% X- p4 M/ k
  69.     }- i3 B8 f; G/ L( i/ V
  70.     else{        //写入未结束
    5 x2 f# S: y5 M2 K4 z
  71.       secpos++;        //扇区地址增1
    3 j6 N" H( i/ H
  72.       secoff = 0;                //偏移位置为0                1 @/ h; m, b2 F3 K  ~3 a
  73.       pBuffer += secremain;        //指针偏移# ^! ?$ K. H; f0 c9 L) q# `$ f
  74.       WriteAddr += secremain;        //写地址偏移8 X8 p# ]+ c9 Q$ F( C0 L8 H
  75.       NumByteToWrite -= secremain;//字节数递减! n: V2 R! t4 I
  76.       if(NumByteToWrite > 4096)                1 a- r8 t/ O' ?
  77.         secremain = 4096;        //下个扇区还没是写不完8 v" K- S( S: x: @. u0 {9 ^3 n
  78.       else
    % h& R3 i# b9 n, P- K$ G
  79.         secremain = NumByteToWrite;//下个扇区可以写完了
    + h4 C1 ~* j8 b! h1 m+ A
  80.     }' Y; E8 Z8 J# \1 h0 t2 Q9 x6 S
  81.   }
    * z6 p: ?" w3 N
  82. }
复制代码
5 z: H; S6 F5 W, v' x8 R, {
➡️ 在main.c文件下编写SPI测试代码
  1. /* USER CODE BEGIN PV */' {, j  L& Y% N8 K% o
  2. uint8_t wData[0x100];
    / e2 k" H- [' k! H) {
  3. uint8_t rData[0x100];3 G: P4 i) {% c+ n. f, ~
  4. uint32_t i;
    , \' k4 F* O- Z& O
  5. /* USER CODE END PV */% O! n, V# X5 P6 U: A
  6. int main(void){% ]$ d/ v& k5 z/ Z$ q# f, K. _
  7.   /* USER CODE BEGIN 1 */7 L: m/ z- {; Y
  8.   uint8_t key;6 ]# W* B6 @( N) e; U
  9.   /* USER CODE END 1 */
    - P2 [" P7 U0 ~. I
  10.   HAL_Init();
    4 a5 g( e2 A3 x2 x- b1 L9 r: [
  11.   SystemClock_Config();9 m) X6 D8 `1 k; n  [" v6 G
  12.   MX_GPIO_Init();- `) t: f" J: q8 n3 e6 B' v. V
  13.   MX_SPI2_Init();1 ], r( j/ ^0 h& Q* j# k1 \7 l" `
  14.   MX_USART1_UART_Init();9 R" P! K/ _( ^: ]0 S# x4 A8 j1 C
  15.   /* USER CODE BEGIN 2 */
      a& R! v% L5 v$ h
  16.   W25QXX_Init();1 s3 N" e* ?& F" u% l
  17.   for(i=0;i<0x100;i++){& Y; D% o' O$ o8 \0 u* Q
  18.     wData[i] = i;) l7 s; v+ u8 I) k' P, Q$ b
  19.     rData[i] = 0;
    $ J6 @' C6 `: f. u; \6 W5 K
  20.   }        + o6 D  ^  Q3 g5 Q% {
  21.   /* USER CODE END 2 */
    6 F& j( b0 K% t
  22.   while (1){
    8 \4 R$ z% r8 ~& |
  23.     key = KEY_Scan(0);* S8 v3 @' G. A! Q
  24.     if(key == KEY_UP_PRES){
    8 l% ]+ [# }" k: g" x9 j0 u% m! E9 x
  25.       printf("KEY_UP_PRES write data...\r\n");
    ' \. T! `5 G, s2 h" f4 O+ Q  m
  26.       W25QXX_Erase_Sector(0);
    $ ]- s1 a  A3 z; T0 h! B/ b
  27.       W25QXX_Write(wData,0,256);               
    $ p7 j1 Z* C- e( a' c% }
  28.     }
    - z1 Y* n$ h9 n6 @
  29.                        
    / R: B5 m4 F. n+ D! M
  30.     if(key == KEY_DOWN_PRES){
    # p  \1 S! h9 a8 N# l
  31.       printf("KEY_DOWN_PRES read data...\r\n");% O, v8 l; W+ r) `) l9 r3 ?
  32.       W25QXX_Read(rData,0,256);& Y) u0 u; J9 D* f
  33.       for(i=0;i<256;i++){
    , ~% L, v1 ~5 Z3 m) h2 ~4 C! ^
  34.         printf("0x%02X ",rData[i]);
    + B) @! E1 H' a) c, N! @; m: B( H
  35.       }
    / Q" ~% v0 q0 h& @2 V9 m
  36.     }
    . M- ^# g! v2 l" l5 e
  37.                 & U# t8 u# t, r+ N% i
  38.     HAL_GPIO_TogglePin(GPIOC,GPIO_PIN_0);. V5 a7 L' v. \6 {% f
  39.     HAL_Delay(200);8 c  g; t4 n* U0 W0 ?
  40.   }+ z+ m4 N: e1 V3 _( Q" X
  41. }
复制代码
# k5 k  ~% a0 R- G4 {4 \+ c$ l
4.下载验证
编译无误下载到开发板后,可以看到D1指示灯不断闪烁,当按下K_UP按键后数据写入到W25Q64芯片内,当按下K_DOWN按键后读取W25Q64芯片的值,同时串口打印出相应信息

2 b+ Q+ N) S5 M1 e 微信图片_20230701185521.png $ w4 a" Y% F' s$ B: _4 X
' l/ m9 r3 J$ @4 ?
转载自: 嵌入式攻城狮% n3 d8 _- F& @% w3 ~
如有侵权请联系删除# h$ {; g! R/ S) B+ w0 R

  p$ g: |9 u6 l0 v
% f  A/ O* M2 ~) V& A+ I5 T; y8 v# S
赞 收藏 评论0 发布时间:2023-7-1 18:56

举报

0个回答

所属标签

相似技术帖

官网相关资源

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