当前位置:   article > 正文

STM32_SPI

STM32_SPI

1、SPI简介

1.1 什么是SPI

        SPI,即Serial Peripheral Interface,串行外设接口。SPI是一种高速的、全双工、同步的串行通信总线;SPI采用主从方式工作,一般有一个主设备和一个或多个从设备;SPI需要至少4根线,分别是MISO(主设备输入,从设备输出)、MOSI(主设备输出,从设备输入)、SCK(时钟)、SS(从机选择)。

        相较于IIC而言,SPI的传输速度更快,SPI协议并没有严格规定最大传输速度,这个最大传输速度一般取决于芯片厂商的设计需求;而IIC由于上拉电阻的原因,电平由低转高时耗时更多,一般速度最快在400KHz。

        所有设备的SCK、MOSI、MISO分别连接在一起;主机另外引出多条SS控制线,分别连接各从机的SS引脚;输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。

1.2 SPI外设

        TM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担。

        可配置8位/16位数据帧、高位先行/低位先行,一般都是8位数据帧、高位先行

        时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)

        支持多主机模型、主或从操作

        可精简为半双工/单工通信

        支持DMA

        兼容I2S协议(音频传输协议)

        STM32F103C8T6 硬件SPI资源:SPI1、SPI2

1.3 极性和相位

        SPI总线有四种不同的工作模式,取决于极性(CPOL)和相位(CPHL)这两个因素。

        CPOL表示CLK空闲时的状态:CPOL=0,空闲时SCK为低电平;反之则为高电平。

        CPHL表示采样时刻:CPHL=0,每个周期的第一个时钟沿采样;CPHL=1,每个周期的第二个时钟沿采样。

2、SPI结构图

        以下结构图来自STM32F103xxx

        这里发送缓冲区就是TDR,接收缓冲区就是RDR,和串口那里一样,TDR和RDR占用同一个地址,统一叫做DR。如果我们需要连续发送一批数据,第一个数据写入到TDR,当移位寄存器没有数据移位时,TDR的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的TXE为1,表示发送寄存器空,当我们检查TXE置1后,紧跟着,下一个数据,就可以提前写入到TDR里候着了,一旦上一个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输。然后移位寄存器这里,一旦有数据过来了,它就会自动产生时钟,将数据移出去,在数据移出的过程中,MISO的数据也会移入,一旦数据移出完成,数据移入也就完成了,这时移入的数据就会整体的从移位寄存器转入到接收缓冲区RDR,这个时刻会置状态寄存器的RXNE为1,表示接收寄存器非空,当我们检查RXNE置1后,就要尽快把数据从RDR读出来,在下一个数据到来之前,读出RDR,就可以实现连续接收。

        基本结构

SPI移位示意图

        从图中可以看出,SPI的数据收发,都是基于字节交换这个基本单元来进行的。当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一下字节交换的时序,这要主机要发送的数据就跑到了从机,主机要从从机接收的数据,就跑到了主机,这就完成了发送同时接收的目的。

3、SPI时序

3.1 基本时序单元

        起始条件:SS从高电平切换到低电平

        终止条件:SS从低电平切换到高电平

        SS低电平选中,高电平代表未选中,那么在低电平期间就代表正在通信,下降沿是通信的开始,上升沿是通信的结束。

        交换一个字节(模式0):

        CPOL=0:空闲状态时,SCK为低电平

        CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

        交换一个字节(模式1):

        CPOL=0:空闲状态时,SCK为低电平

        CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

        交换一个字节(模式2):

        CPOL=1:空闲状态时,SCK为高电平

        CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

        交换一个字节(模式3):

        CPOL=1:空闲状态时,SCK为高电平

        CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

3.2 SPI时序

        发送指令:向SS指定的设备,发送指令(0x06)

         指定地址写:向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)

        指定地址读:向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)

4、示例代码

4.1 软件实现SPI

  1. #include "stm32f10x.h" // Device header
  2. //对SPI四个引脚的封装
  3. //从机选择,写SS的引脚
  4. void MySPI_W_SS(uint8_t BitValue)
  5. {
  6. GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
  7. }
  8. void MySPI_W_SCK(uint8_t BitValue)
  9. {
  10. GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
  11. }
  12. void MySPI_W_MOSI(uint8_t BitValue)
  13. {
  14. GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
  15. }
  16. uint8_t MySPI_R_MISO(void)
  17. {
  18. return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
  19. }
  20. void MySPI_Init(void)
  21. {
  22. //打开时钟
  23. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  24. //配置端口
  25. //先定义一个结构体变量
  26. GPIO_InitTypeDef GPIO_InitStructure;
  27. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
  28. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
  29. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
  30. GPIO_Init(GPIOA, &GPIO_InitStructure);
  31. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
  32. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
  33. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
  34. GPIO_Init(GPIOA, &GPIO_InitStructure);
  35. //引脚初始化之后的默认电平
  36. MySPI_W_SS(1); //默认使用高电平,不选择从机
  37. MySPI_W_SCK(0); //计划使用SPI模式0,所以默认是低电平
  38. }
  39. //写SPI的三个基本时序单元
  40. //起始信号
  41. void MySPI_Start(void)
  42. {
  43. MySPI_W_SS(0); //根据时序图将SS置低电平
  44. }
  45. //终止信号
  46. void MySPI_Stop(void)
  47. {
  48. MySPI_W_SS(1); //根据时序图将SS置高电平
  49. }
  50. //交换一个字节
  51. uint8_t MySPI_SwapByte(uint8_t ByteSend)
  52. {
  53. //定义一个字节,用于接收
  54. uint8_t i, ByteReceive = 0x00;
  55. for (i = 0; i < 8; i++)
  56. {
  57. //在SS下降沿之后开始交换字节,先有下降沿再有数据移出的动作
  58. MySPI_W_MOSI(ByteSend & (0x80 >> i)); //这里相当于通过掩码的模式来获取想要的位
  59. MySPI_W_SCK(1); //写SCK为1,产生上升沿,上升沿时,从机会自动把MOSI的数据读走,主机的任务就是把从机刚才放到MISO的数据位读进来
  60. if (MySPI_R_MISO() == 1)
  61. {
  62. ByteReceive |= (0x80 >> i); //这样就把最高位存在ByteReceive中了
  63. }
  64. MySPI_W_SCK(0); //写SCK为0,产生下降沿,下降沿时,主机和从机移出下一位
  65. //该for循环中的内容还可以进行优化,用掩码的方式提取,好处是不会改变ByteSend本身,后面有需要还可以使用
  66. //用移位数据本身来操作,好处是提高了效率,但是ByteSend这个数据在移位过程中改变了
  67. }
  68. return ByteReceive;
  69. }

4.2 硬件SPI读写

  1. #include "stm32f10x.h" // Device header
  2. //对SPI四个引脚的封装
  3. //从机选择,写SS的引脚
  4. //这里还是使用软件模拟
  5. void MySPI_W_SS(uint8_t BitValue)
  6. {
  7. GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
  8. }
  9. //以上代码替换成SPI外设的初始化
  10. //第一步,开启时钟和GPIO的时钟
  11. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  12. RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  13. //第二步,初始化GPIO口,其中SCK和MOSI是由硬件外设控制的输出信号,所以配置为复用推挽输出,MISo是硬件外设的输入信号,设置为上拉拉输入,
  14. //因为输入设备可以有多个,所以不存在复用输入这个东西,SS是软件控制的输出信号,所以配置为通用推挽输出
  15. GPIO_InitTypeDef GPIO_InitStructure;
  16. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
  17. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
  18. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
  19. GPIO_Init(GPIOA, &GPIO_InitStructure);
  20. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
  21. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
  22. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
  23. GPIO_Init(GPIOA, &GPIO_InitStructure);
  24. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
  25. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
  26. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
  27. GPIO_Init(GPIOA, &GPIO_InitStructure);
  28. //第三步,配置SPI外设
  29. SPI_InitTypeDef SPI_InitStructure;
  30. SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //选择SPI的模式,决定当前设备是SPI的主机还是从机
  31. SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //这里选择的是双线全双工
  32. SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //这里选择的是8位数据位
  33. SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //这里选择的是高位先行
  34. SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率预分频器,这里SCK的时钟频率就是72MHz / 128
  35. SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //时钟极性,这里选择的是模式0,空闲时低电平
  36. SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //时钟相位,这里选择第一个边沿采样,这个和SPI的四种模式有关
  37. SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //这里选择软件NSS
  38. SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC校验的多项式,这里填多少都可以,反正我们不用
  39. SPI_Init(SPI1, &SPI_InitStructure);
  40. //第四步,开关控制,调用SPI_Cmd,给SPI使能即可
  41. SPI_Cmd(SPI1, ENABLE);
  42. //开启spi之后,还要调用一下MySPI_W_SS,默认给SS输出高电平,默认是不选中从机的
  43. MySPI_W_SS(1);
  44. }
  45. //写SPI的三个基本时序单元
  46. //起始信号
  47. void MySPI_Start(void)
  48. {
  49. MySPI_W_SS(0); //根据时序图将SS置低电平
  50. }
  51. //终止信号
  52. void MySPI_Stop(void)
  53. {
  54. MySPI_W_SS(1); //根据时序图将SS置高电平
  55. }
  56. //交换一个字节
  57. uint8_t MySPI_SwapByte(uint8_t ByteSend)
  58. {
  59. //当调用这个交换字节的函数时,硬件的SPI外设就要自动控制SCK、MOSI、MISO这三个引脚来生成时序了
  60. while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待TXE的标志位为1
  61. //写入数据至SPD的DR,就是TDR,要发送的数据
  62. //ByteSend传入到TDR中,之后ByteSend会自动转入移位寄存器,一旦移位寄存器有数据了,就会自动产生时序波形
  63. SPI_I2S_SendData(SPI1, ByteSend);
  64. //发送和接收是同步的,到接收完成的时候也就代表发送移位完成了,接收移位完成时,会收到一个字节数据,这时会置标志位RXNE
  65. while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待RXNE的标志位为1,表示收到一个字节,同时也表示发送时序产生完成了
  66. //读取DR,从RDR中,把交换接收的数据读出来
  67. return SPI_I2S_ReceiveData(SPI1);
  68. }
  69. //注意事项1:这里的硬件SPI,必须是发送,同时接收,要想接收必须得先发送,因为只有你给TDR写数据,才会触发时序的生成
  70. //如果不发送只接收,那时序是不会动的
  71. //注意事项2:TXE和RXNE标志位,在写入SPI_DR时,TXE标志被清除,在读SPI数据寄存器可以清除RXNE标志位,所以按照上面程序的步骤就不用手动清除标志位了
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/676655
推荐阅读
相关标签
  

闽ICP备14008679号