• SPI通信


    简介

    SPI Serial Peripheral Interface )是由 Motorola 公司开发的一种通用数据总线
    四根通信线: SCK Serial Clock )、 MOSI Master Output Slave Input )、 MISO Master Input Slave Output )、 SS Slave Select
    同步,全双工
    支持总线挂载多设备(一主多从)
    没有应答机制

    相对I2C的优点缺点:1,传输速度快,可达80MHz;2,设计简单,学习容易;3,硬件资源消耗多,容易造成资源浪费

    硬件电路 

    所有 SPI 设备的 SCK MOSI MISO 分别连在一起
    主机另外引出多条 SS 控制线,分别接到各从机的 SS 引脚
    输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入(得益于其推挽输出,高电平和低电平都有很强的驱动能力,传输数据就快,I2C不使用推挽输出是因为其一根通讯线实现收发数据(半双工)所以需要不断切换输入和输出,且I2C又要实现多主机的时钟同步和总线仲裁,所以I2C放弃了高性能换取了多功能)
    ·多设备需要共地
    ·从机的SS线低电平有效

     移位寄存器

    SPI时序基本单元 

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

     

    交换一个字节(模式 0
    CPOL=0 :空闲状态时, SCK 为低电平
    CPHA=0 SCK 第一个边沿移入数据,第二个边沿移出数据

     

    交换一个字节(模式 1
    CPOL=0 :空闲状态时, SCK 为低电平
    CPHA=1 SCK 第一个边沿移出数据,第二个边沿移入数据

    CPHA决定了是那个SCK开始采样

    模式2和模式3类似与这两个模式,只不过SCK的高低电平翻转了而已

    W25Q64简介

    Dual SPI: 双重SPI模式,在一个SCK电平变化下,一次性交换两个数据

    Quad SPI:四重SPI模式,同双重SPI模式

     硬件电路

     对于引脚旁括号的内容:即双重SPI模式或者四重SPI模式下的多位数据传输的通道,当为双重SPI模式时,DO(I01)和DI(IO0)就是一次性传输的两位数据,IO2和IO3同理。

    W25Q64框图

    一整个存储空间,首先先被划分为若干块,对应每一块又被划分为若干扇区,对于整个空间,又会被划分为很多页,每页256个字节

    Page地址锁存器:用于指定我们要操作的页

    Byte地址锁存器:用于指定操作我们指定的页中指定的字节

    我们发送的24位地址(3字节地址)前两位是Page地址,会发送到Page地址锁存器中,后一位是字节地址,会发送到Byte地址锁存器中。

    以上两个寄存器都有计数器,所以他们的地址指针是可以在读写之后可以自动加1的,实现从指定地址开始,连续读写多个字节的目的了。

    写入的数据会先在RAM缓存区(Column Decode)中存储,在时序结束后,芯片再把缓存区中的数据复制到对应的Flash中,进行永久保存

    因为SPI的写入频率非常高,而数据是要放进Flash中永久存储的,这个过程比较慢,所以写入的数据先放在页缓存区中存着,缓存区是RAM,所以速度非常快,可以跟上SPI总线的速度,但是这个缓存区也是有内存限制的(256Byte),所以写入的一个时序,连续写入的数据量不能超过256字节。

    当我们发送好数据后,芯片才慢慢地把页缓存区的数据传入到Flash中,这会占用一定的时间,写入时序结束后,芯片就会进入“Buzy”的时间内,这时就会置标志位到status寄存器中,在这段时间内,芯片将不会响应新的读写时序。

    Flash注意事项

    写入操作时:

    写入操作前,必须先进行写使能
    每个数据位只能由 1 改写为 0 ,不能由 0 改写为 1
    写入数据前必须先擦除,擦除后,所有数据位变为 1
    擦除必须按最小擦除单元进行(最小单位:扇区)
    连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
    写入操作结束后,芯片进入忙状态,不响应新的读写操作

    读取操作时:

    直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取

    状态 寄存器

     Buzy如之前所说;

    写使能:在每写入一个数据后,状态寄存器会自动配置为写失能,代表我们每写入一个字节之气那都需要配置为写使能,每一个写使能只能保证后续的一条写指令

    指令集

    ID

     写使能

     页编程(前三个字节指定地址,后面一个写入数据,如果继续写入数据,指定的地址会自动加1,但是要注意范围为页)

    擦除指令(标注的是最小的扇区擦除) 

     读取ID

    读取数据

    代码实操 

    1,开启时钟,配置GPIO口(软件SPI任选引脚)

    1. void MySPI_Init(void)
    2. {
    3. //引脚初始化
    4. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    5. GPIO_InitTypeDef GPIO_InitStruct;
    6. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    7. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
    8. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    9. GPIO_Init(GPIOA, &GPIO_InitStruct);
    10. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    11. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
    12. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    13. GPIO_Init(GPIOA, &GPIO_InitStruct);
    14. MySPI_W_SS(1);
    15. MySPI_W_SCK(0);
    16. }

    2,与软件I2C类似,使用函数封装一下置每个引脚高低电平的操作(模拟SPI)

    1. //用于模拟每个端口的操作
    2. void MySPI_W_SS(uint8_t Value)
    3. {
    4. GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)Value);
    5. }
    6. void MySPI_W_SCK(uint8_t Value)
    7. {
    8. GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)Value);
    9. }
    10. void MySPI_W_MOSI(uint8_t Value)
    11. {
    12. GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)Value);
    13. }
    14. uint8_t MySPI_R_MISO(void)
    15. {
    16. GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
    17. }

    同时需要在初始化函数中添加

    1. MySPI_W_SS(1);
    2. MySPI_W_SCK(0);

    3,根据时序来编写各个操作的函数

    例如开始——只需要把SS置0

    1. void MySPI_Start(void){
    2. MySPI_W_SS(0);
    3. }
    4. void MySPI_Stop(void){
    5. MySPI_W_SS(1);
    6. }
    7. //掩码方式(优点,可以保持ByteSend不变)
    8. uint8_t MySPI_WriteReadByte(uint8_t ByteSend)
    9. {
    10. uint8_t i, ByteReceive=0x00;
    11. for (i = 0; i < 8; i ++)
    12. {
    13. MySPI_W_SCK(1);
    14. MySPI_W_MOSI(ByteSend & (0x80 >> i));
    15. if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}
    16. MySPI_W_SCK(0);
    17. }
    18. return ByteReceive;
    19. }
    20. //移位版(实现思路与SPI传输数据相同)
    21. uint8_t MySPI_WriteReadByte1(uint8_t ByteSend)
    22. {
    23. uint8_t i;
    24. for (i = 0; i < 8; i ++)
    25. {
    26. MySPI_W_SCK(1);
    27. MySPI_W_MOSI(ByteSend & 0x80);
    28. ByteSend <<= 1;
    29. if (MySPI_R_MISO() == 1){ ByteSend |= 0x01;}
    30. MySPI_W_SCK(0);
    31. }
    32. return ByteSend;
    33. }

    4,编写W25Q64的相关函数

    1. void W25Q64_Init(void)
    2. {
    3. MySPI_Init();
    4. }
    1. void W25Q64_GetID(uint8_t *MID, uint16_t *DID)
    2. {
    3. MySPI_Start();
    4. //指令集中获取设备ID号的指令
    5. MySPI_WriteReadByte(W25Q64_JEDEC_ID);
    6. //第一个字节为厂商ID
    7. *MID = MySPI_WriteReadByte(W25Q64_DUMMY_BYTE);
    8. //第二个字节为设备ID高8位,第三个字节位设备ID低8位
    9. *DID = MySPI_WriteReadByte(W25Q64_DUMMY_BYTE);
    10. *DID <<= 8;
    11. *DID |= MySPI_WriteReadByte(W25Q64_DUMMY_BYTE);
    12. MySPI_Stop();
    13. }
    1. #include "stm32f10x.h" // Device header
    2. #include "MySPI.h"
    3. #include "W25Q64_ins.h"
    4. void W25Q64_Init(void)
    5. {
    6. MySPI_Init();
    7. }
    8. void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
    9. {
    10. MySPI_Start();
    11. //指令集中获取设备ID号的指令
    12. MySPI_SwapByte(W25Q64_JEDEC_ID);
    13. //第一个字节为厂商ID
    14. *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    15. //第二个字节为设备ID高8位,第三个字节位设备ID低8位
    16. *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    17. *DID <<= 8;
    18. *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    19. MySPI_Stop();
    20. }
    21. void W25Q64_WriteEnable(void)
    22. {
    23. MySPI_Start();
    24. MySPI_SwapByte(W25Q64_WRITE_ENABLE);
    25. MySPI_Stop();
    26. }
    27. void W25Q64_WaitBusy(void)
    28. {
    29. MySPI_Start();
    30. //读取状态寄存器比较特殊,写入以下值后,是连续读出状态寄存器的值
    31. uint32_t Timeout=100000;
    32. MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    33. while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01 )== 0x01)
    34. {
    35. Timeout--;//防止卡死
    36. if (Timeout == 0)
    37. {
    38. break;
    39. }
    40. }
    41. MySPI_Stop();
    42. }
    43. //注意一次性只能发送一页(256个字节)的数据,Count最大值也只能是256
    44. void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
    45. {
    46. //事前等待(更高效,但是在每个操作函数的开头都需要加)
    47. W25Q64_WaitBusy();
    48. //写使能
    49. W25Q64_WriteEnable();
    50. MySPI_Start();
    51. //指令集中的PageProgram
    52. MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
    53. //传输地址
    54. //因为地址是24位的,所以需要把地址分成8位一份发给从机
    55. MySPI_SwapByte(Address >> 16);
    56. MySPI_SwapByte(Address >> 8);
    57. MySPI_SwapByte(Address);
    58. //发送数据
    59. for (uint16_t i = 0; i < Count; i ++)
    60. {
    61. MySPI_SwapByte(DataArray[i]);
    62. }
    63. MySPI_Stop();
    64. }
    65. //擦除一个扇形区
    66. void W25Q64_SectorErase(uint32_t Address)
    67. {
    68. //事前等待(更高效,但是在每个操作函数的开头都需要加)
    69. W25Q64_WaitBusy();
    70. //写使能
    71. W25Q64_WriteEnable();
    72. MySPI_Start();
    73. MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
    74. MySPI_SwapByte(Address >> 16);
    75. MySPI_SwapByte(Address >> 8);
    76. MySPI_SwapByte(Address);
    77. MySPI_Stop();
    78. }
    79. void W25Q64_ReadData(uint32_t Address, uint8_t *Array, uint32_t Count)
    80. {
    81. //事前等待(更高效,但是在每个操作函数的开头都需要加)
    82. W25Q64_WaitBusy();
    83. MySPI_Start();
    84. MySPI_SwapByte(W25Q64_READ_DATA);
    85. MySPI_SwapByte(Address >> 16);
    86. MySPI_SwapByte(Address >> 8);
    87. MySPI_SwapByte(Address);
    88. for (uint32_t i = 0; i < Count; i ++)
    89. {
    90. Array[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
    91. }
    92. MySPI_Stop();
    93. }

    主函数

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "W25Q64.h"
    5. uint8_t MID;
    6. uint16_t DID;
    7. uint8_t Array_W[4] = {0x01, 0x02, 0x03, 0x04};
    8. uint8_t Array_R[4];
    9. int main(void)
    10. {
    11. OLED_Init();
    12. W25Q64_Init();
    13. OLED_ShowString(1, 1, "MID: DID:");
    14. OLED_ShowString(2, 1, "W:");
    15. OLED_ShowString(3, 1, "R:");
    16. W25Q64_ReadID(&MID, &DID);
    17. OLED_ShowHexNum(1, 5, MID, 2);
    18. OLED_ShowHexNum(1, 12, DID, 4);
    19. W25Q64_SectorErase(0x000000);
    20. W25Q64_PageProgram(0x000000, Array_W, 4);
    21. W25Q64_ReadData(0x000000, Array_R, 4);
    22. OLED_ShowHexNum(2,3,Array_W[0],2);
    23. OLED_ShowHexNum(2,6,Array_W[1],2);
    24. OLED_ShowHexNum(2,9,Array_W[2],2);
    25. OLED_ShowHexNum(2,12,Array_W[3],2);
    26. OLED_ShowHexNum(3,3,Array_R[0],2);
    27. OLED_ShowHexNum(3,6,Array_R[1],2);
    28. OLED_ShowHexNum(3,9,Array_R[2],2);
    29. OLED_ShowHexNum(3,12,Array_W[3],2);
    30. while(1)
    31. {
    32. }
    33. }

    SPI硬件

    STM32 内部集成了硬件 SPI 收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻 CPU 的负担
    可配置 8 /16 位数据帧、高位先行 / 低位先行
    时钟频率: f PCLK / (2, 4, 8, 16, 32, 64, 128, 256)
    支持多主机模型、主或从操作
    可精简为半双工 / 单工通信
    支持 DMA
    兼容 I2S 协议
    STM32F103C8T6 硬件 SPI 资源: SPI1 SPI2

     SPI框图

    SPI基本结构 

     主模式全双工连续传输非连续传输

     非连续传输的等待空隙在频率比较高时没太大影响,但是一旦频率很低,这个影响就不能被忽略了,所以我们在传输比较高频率的信号时不能使用非连续传输,可以使用连续输出或者进一步采用DMA自动转运。

    其他内容真得看手册吧啊!

    代码实操

    函数介绍 

    不用介绍

    1. void SPI_I2S_DeInit(SPI_TypeDef* SPIx);
    2. void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);
    3. void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);
    4. void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);
    5. void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);
    6. void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
    7. void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
    8. void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState NewState);
    9. void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq, FunctionalState NewState);
    1. //发送和接收数据
    2. void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);
    3. uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);

     标志位哥们

    1. FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
    2. void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
    3. ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
    4. void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);

     初始化流程

    1,开启时钟,开启SPI和GPIO的时钟

    2,初始化GPIO口,SCK和MOSI是硬件控制的输出引脚,配置为复用推挽输出,MISO是硬件外设的输入引脚,配置为上拉输入,SS引脚可由GPIO来模拟,配置位通用推挽输出

    1. //引脚初始化
    2. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    3. RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    4. GPIO_InitTypeDef GPIO_InitStruct;
    5. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
    6. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4;
    7. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    8. GPIO_Init(GPIOA, &GPIO_InitStruct);
    9. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    10. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    11. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    12. GPIO_Init(GPIOA, &GPIO_InitStruct);
    13. GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
    14. GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
    15. GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    16. GPIO_Init(GPIOA, &GPIO_InitStruct);

    3,配置SPI外设

    1. SPI_InitTypeDef SPI_InitStruct;
    2. //主从
    3. SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
    4. /*裁剪SPI引脚的,即选择:
    5. 单线半双工的接收模式Rx
    6. 单线半双工的发送模式Tx
    7. 双线全双工Full
    8. 双线只接收模式RxOnly*/
    9. SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    10. //数据帧8位还是16位
    11. SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
    12. //低位先行or高位先行
    13. SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
    14. //SCK时钟的频率(即选择分频系数
    15. SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
    16. //SPI模式
    17. //时钟极性-这里选择了默认为低电平
    18. SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
    19. //1Edge即为0,2Enge即为1
    20. SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
    21. //软件实现NSS
    22. SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
    23. //不用了解
    24. SPI_InitStruct.SPI_CRCPolynomial = 7;
    25. SPI_Init(SPI1,&SPI_InitStruct);

    4,开关控制

    1. SPI_Cmd(SPI1, ENABLE);
    2. MySPI_W_SS(1);

    交换数据的函数

    1. uint8_t MySPI_SwapByte(uint8_t ByteSend)
    2. {
    3. //必须发送同时接收,两个过程是绑定进行的
    4. while(!SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE));
    5. SPI_I2S_SendData(SPI1, ByteSend);
    6. //在发送的同时MISO还会移位进行接收,发送和接收是同步的
    7. //接收移位完成了也就代表发送移位完成了
    8. //接收完成时会置标志位RXNE,我们可以借此来判断是否发送完数据
    9. while(!SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE));
    10. //标志位不需手动清除
    11. //读取数据
    12. return SPI_I2S_ReceiveData(SPI1);
    13. }

  • 相关阅读:
    LeetCode(16)接雨水【数组/字符串】【困难】
    Python爬虫实战-小说网站爬虫开发
    SQL中的PowerDesigner逐步深入提问,你能掌握多少?
    (附源码)计算机毕业设计Java巴音学院学生资料管理系统
    Java中Map的4种遍历方式
    包装类与数据类型
    初学后端,如何做好表结构设计?
    vue监听表单输入的身份证号自动填充性别和生日
    JBDC的使用
    泛型的约束不止一面
  • 原文地址:https://blog.csdn.net/m0_74460550/article/details/134000512