• 11:STM32---spl通信


    目录

    一:SPL通信

    1:简历

    2:硬件电路

    3:移动数据图

    4:SPI时序基本单元

    A : 开/ 终条件

    B:SPI时序基本单元

    A:模式0

    B:模式1

    C:模式2

    D:模式3

    C:SPl时序

    A:发送指令

    B: 指定地址写

    C:指定地址读

    二: W25Q64

    1:简历

    2: 硬件电路

    3:W25Q64框图

    4: Flash操作注意事项

    5:指令集

    三:案例

    A: 软件SPI读写W25Q64

    1: 连接图

    2:代码

    B: 硬件SPI读写W25Q64

    1:简历

    2:框图

    3:SPI基本结构

    4: 主模式全双工连续传输

    5: 非连续传输

    6:连接图

    7: 代码 


    一:SPL通信

    1:简历


              SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线

            四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)

             同步(有时钟线),全双工 (传输线有2条,发送和接受线路)

            支持总线挂载多设备(一主多从)

            SPl没有应答机制

    2:硬件电路

            所有SPI设备的SCK、MOSI、MISO分别连在一起

            主机另外引出多条SS控制线,分别接到各从机的SS引脚

            输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入

            SS也叫CS片选信号 : 和全部的从机连接, 用于选择主机和那个从机进行通信, 低电平有效;   每个从机的SS(CS)都和主机的SSX相互连接,  SS对于主机来说的话,就是输出信号, 从机的话就是输入信号

            IO的配置 : 都是以STM32为主角进行的.  主机输出信号配置---推挽输出,  主机输入信号配置----浮空或上拉输入

            SCK : 时钟线,  时钟线完全由主机掌控,  所以对于主机来说,时钟线为输出;   对于所有从机来说,时钟线都为输入;  这样主机的同步时钟,就能送到各个从机了

            MOSI : 主机输出,从机输入

            MISO : 主机输入,从机输出

           关于CS和MISO主机输入,从机输出 : 当从机没有被选中的时候,也就是SS段电平为1; 从机的MISO主机输入,从机输出必须切换为高阻态 , 高阻态就相当于引脚断开,不输出任何电平;   这样就可以防止,一条线有多个输出,而导致的电平冲突的问题了;    在SS为低电平时,MISO才允许变为推挽输出----从机的操作-------一般情况下我们只需要写主机的程序,从机的程序不需要我们操心

    3:移动数据图

    交换数据, 高位先行

    SPI的数据收发,都是基于字节交换,这个基本单元来进行的 (移位模型)

            首先,我们规定波特率发生器时钟的上升沿主机和从机都移出数据;  下将沿移入数据;

      

            数据为从左往右运动,所以是高为先行,  首先波特率发生器时钟产生上生沿, 主机把它的最高位的数据放在MOSI上面, 从机把它最高位置的数据放在MISO上面;          在由特率发生器产生的下降沿移入数据;  在MISO数据线上从机的最高位的数据放在主机的最低位置上面;  MOSI上面主机最高位的数据放在从机的最低位置

    4:SPI时序基本单元

    A : 开/ 终条件

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

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

    B:SPI时序基本单元

    在任何操作下, 我们只管主机(只写主机的代码) , 从机它自动操作(不用写从机的代码) 

    我们经常使用的是模式0

    A:模式0

            交换一个字节(模式0)

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

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

            SCL上升沿主机和从机同步移入数据;  SCL下降沿主机和从机同步移出数据

    1. /**
    2. * @brief SPL交换数据--使用的为模式0
    3. DI(MOSI)----SPI主机输出从机输入
    4. DO(MISO)-------SPI主机输入从机输出
    5. 我们只操作主机:首先主机移出最高位,放在MOSI上面,---主机操作需要我们来
    6. 齐次从机把数据放在MISO上面----从机的操作不需要我们管
    7. * @param ByteSend: 主机给从机发送的数据
    8. * @retval 主机读取的数据----即从机给主机发送的数据
    9. */
    10. uint8_t MySPI_SwapByte(uint8_t ByteSend)
    11. {
    12. MySPI_W_SCK(0);
    13. //一般来说&是用来清零的;
    14. //一般来说|是用来值一的;
    15. uint8_t ByteReceive=0x00;
    16. for (uint8_t i=0;i<8;i++)
    17. {
    18. MySPI_W_MOSI(ByteSend & (0x80>>i)); //MOSI主机输出数据 1000 0000
    19. /*
    20. 我们只操作主机:
    21. SCL上升沿主机和从机同步移入数据, 从机会自动把主机给它的最高为移动到了从机里面---从机不需要我们操作
    22. 主机操作 : 主机需要把从机给它发送的数据移动到了主机里面---即读取MISO线上的数据
    23. */
    24. MySPI_W_SCK(1);
    25. if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}//MySPI_R_MISO主机读取数据
    26. MySPI_W_SCK(0);
    27. //SCL下降沿主机和从机同步移出数据
    28. //|---置1
    29. }
    30. return ByteReceive;
    31. }

    在任何操作下, 我们只管主机(只写主机的代码) , 从机它自动操作(不用写从机的代码) 

    B:模式1

            交换一个字节(模式1)

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

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

            SPl为了可以配置更多的时钟芯片, 给我们了2个可以自己调节的位, 分别为:CPOL (Clock Polarity)时钟极性和CPHA (Clock Phase)时钟相位配置这两个为,  就构成了4种模式

            模式1 : 波特率发生器时钟的上升沿主机和从机都移出数据;  下将沿移入数据;  模式1的数据移动方式和 3:移动数据图 一样 , 详情参考----3:移动数据图

    C:模式2

            交换一个字节(模式2)

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

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

    D:模式3

            交换一个字节(模式3)

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

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

    C:SPl时序

    A:发送指令

    规定 : SPL起始的第一个字节为指令集

    发送指令

    向SS指定的设备,发送指令(0x06)--0x06使能

    B: 指定地址写

            指定地址写

            向SS指定的设备,发送写指令(0x02),---0x02写入的指令集    

            随后在指定地址(Address[23:0])下,写入指定数据(Data)   

            SPl没有应答机制, 交换一个字节后, 直接开始交换下一个字节

    C:指定地址读

            指定地址读

            向SS指定的设备,发送读指令(0x03),---0x03发送指令的指令集   

            随后在指定地址(Address[23:0])下,读取从机数据(Data)

    二: W25Q64

    1:简历

            W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景

            存储介质:Nor Flash(闪存)

            时钟频率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)

            存储容量(24位地址):

            W25Q40:      4Mbit / 512KByte     

            W25Q80:      8Mbit / 1MByte   

             W25Q16:      16Mbit / 2MByte     

            W25Q32:      32Mbit / 4MByte   

             W25Q64:      64Mbit / 8MByte   

             W25Q128:  128Mbit / 16MByte   

             W25Q256:  256Mbit / 32MByte

    2: 硬件电路

    3:W25Q64框图

    4: Flash操作注意事项

    非易失性存储器---掉电不丢失

    写入操作时

            写入操作前,必须先进行写使能------------是一种保护措施,防止你误操作的

            每个数据位只能由1改写为0,不能由0改写为1--------------Flash并没有像RAM那样的,  直接完全覆盖改写的能力. eg:在某一个直接的储存单元首先储存了0xaa 1010 1010 在储存0x55 0101 0101 因为Flash没有直接覆盖数据的能力,  在加上第二条规定的限制实际储存的数据为: 0000 0000 不是0x55, 使用在写入第二给数据前必须擦除之前的数据

            写入数据前必须先擦除,擦除后,所有数据位变为1--------------有专门的擦除电路把之前写的数据都值1(0xFF), 就可以弥补第二条规定的不足

            擦除必须按最小擦除单元进行------------不能指定某一个字节去擦除, 要擦,就得一大片一起擦, 在我们这个芯片里;  你可以选择,整个芯片擦除, 也可以选择,按块擦除,或者按扇区擦除;    最小的擦除单元,就是一个扇区, 个扇区,是4KB,就是4096个字节

            连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入--------一个写入时序,最多只能写一页的数据,也就是256字节;  一个页缓存区,它只有256字节;    Flash的写入,太慢了.  跟不上SPI的频率.  所以写入的数据,会先放在RAM里暂存.             必须得,从页起始位置开始,才能最大写入256字节,  如果从页中间的地址开始写, 那写到页尾时,这个地址就会跳回到页首, 这会导致地址错乱

            写入操作结束后,芯片进入忙状态,不响应新的读写操作--------要想知道芯片什么时候结束忙状态了,  我们可以使用读取状态寄存器的指令,  看一下状态寄存器的BUSY位是否为1,  BUSY位为0时,芯片就不忙了,我们再进行操作

            在发出擦除指令后,芯片也会进入忙状态, 我们也得等待忙状态结束后,才能进行后续操作

            扇区擦除也是写入所以需要使能

    读取操作时

            直接调用读取时序,无需使能,无需额外操作,没有页的限制,

            读取操作结束后不会进入忙状态,但不能在忙状态时读取,

    5:指令集

    INSTRUCTION NAME---指令的名字;     BYTE----字节X

     

    Write Enable----写使能指令集

    Write Disable --------写失能指令集

    Read Status Register-1---------读状态寄存器1--作用: 判断寄存器在不在忙, 具体见 二: 4

    Page Program----------页编程, 写数据,max为256个字节

    Sector Erase (4KB)-------------按4KB的扇区擦除

    JEDEC ID----------读取ID

    Read Data-----读取数据

    三:案例

    A: 软件SPI读写W25Q64

    1: 连接图

            因为我们使用的是软件模拟SPL通信,    所以原则上外设的引脚可以随便和32端口连接, 使用端口模拟SPL通信

    2:代码

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "mySPl.h"
    5. #include "w25q64.h "
    6. //一般来说&是用来清零的;
    7. //一般来说|是用来值一的;
    8. void MySPI_W_SS(uint8_t BitValue)
    9. {
    10. //也叫做CS片选段----在低电平是有效
    11. GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
    12. }
    13. void MySPI_W_SCK(uint8_t BitValue)
    14. {
    15. //CLK(SCK) SPI时钟
    16. GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)BitValue);
    17. }
    18. void MySPI_W_MOSI(uint8_t BitValue)
    19. {
    20. //MOSI-----主机输出
    21. GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)BitValue);
    22. }
    23. uint8_t MySPI_R_MISO(void)
    24. {
    25. //MISO-----主机输入
    26. return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
    27. }
    28. /**
    29. * @brief DO(MISO) SPI主机输入从机输出---连接的是PA6; 都是以主机的角度看
    30. 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
    31. PA6----浮空或上拉输入; 剩下的全部为推挽输出
    32. * @retval 无
    33. */
    34. void MYSPL_init()
    35. {
    36. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    37. GPIO_InitTypeDef GPIO_InitStructure;
    38. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
    39. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5|GPIO_Pin_7;
    40. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    41. GPIO_Init(GPIOA, &GPIO_InitStructure);
    42. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
    43. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    44. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    45. GPIO_Init(GPIOA, &GPIO_InitStructure);
    46. //在开始时候默认SS(CS)为高电平,SCK为低电平
    47. MySPI_W_SS(1);
    48. MySPI_W_SCK(0);
    49. }
    50. void SPL_Start()
    51. {
    52. MySPI_W_SS(1);
    53. MySPI_W_SS(0);
    54. }
    55. void SPL_Stop()
    56. {
    57. MySPI_W_SS(0);
    58. MySPI_W_SS(1);
    59. }
    60. /**
    61. * @brief SPL交换数据--使用的为模式0
    62. DI(MOSI)----SPI主机输出从机输入
    63. DO(MISO)-------SPI主机输入从机输出
    64. 我们只操作主机:首先主机移出最高位,放在MOSI上面,---主机操作需要我们来
    65. 齐次从机把数据放在MISO上面----从机的操作不需要我们管
    66. * @param ByteSend: 主机给从机发送的数据
    67. * @retval 主机读取的数据----即从机给主机发送的数据
    68. */
    69. uint8_t MySPI_SwapByte(uint8_t ByteSend)
    70. {
    71. MySPI_W_SCK(0);
    72. //一般来说&是用来清零的;
    73. //一般来说|是用来值一的;
    74. uint8_t ByteReceive=0x00;
    75. for (uint8_t i=0;i<8;i++)
    76. {
    77. MySPI_W_MOSI(ByteSend & (0x80>>i)); //MOSI主机输出数据 1000 0000
    78. /*
    79. 我们只操作主机:
    80. SCL上升沿主机和从机同步移入数据, 从机会自动把主机给它的最高为移动到了从机里面---从机不需要我们操作
    81. 主机操作 : 主机需要把从机给它发送的数据移动到了主机里面---即读取MISO线上的数据
    82. */
    83. MySPI_W_SCK(1);
    84. if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}//MySPI_R_MISO主机读取数据
    85. MySPI_W_SCK(0);
    86. //SCL下降沿主机和从机同步移出数据
    87. //|---置1
    88. }
    89. return ByteReceive;
    90. }
    91. //W25Q64_WaitBusy等待不忙的函数, 事后等待,只需要在写入操作之后调用---因为在写入后等待不忙读取的时候肯定不忙
    92. //而事前等待,在写入操作和读取操作之前,都得调用
    93. //我们采用事前等待
    94. void W25Q64_init()
    95. {
    96. MYSPL_init();
    97. }
    98. /**
    99. * @brief 读取设备的ID号
    100. 步骤: 起始,先交换发送指令9F,随后连续交换接收3个字节,停止;
    101. 连续接收的3个字节: 第一个字节是广商ID”表示了是哪个广家生产.
    102. 后两个学节---是设备ID; 设备ID的高8为---表示存储器类型, 低8为--表示容量
    103. * @param MID : 输出8位的厂商ID
    104. @param DID : 输出16位的设备ID
    105. 因为函数内的返回值只能返一个,而用指针,只要知道地址就可以写入;
    106. 可以直接改变外补的2个参数, 相当于返回2个参数
    107. * @retval 无
    108. */
    109. void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
    110. {
    111. 一般来说&是用来清零的;
    112. //一般来说|是用来值一的;
    113. SPL_Start();
    114. MySPI_SwapByte(0x9F);//先交换发送指令9F
    115. *MID = MySPI_SwapByte(0xFF);//返回的第一个字节是广商ID; 这时候我们给我从机发送的数据没有实际的意义
    116. *DID = MySPI_SwapByte(0xFF);//返回的第二个字节设备ID的高8为---表示存储器类型
    117. *DID <<= 8;
    118. *DID |= MySPI_SwapByte(0xFF);返回的第二个字节设备低8为--表示容量
    119. SPL_Stop();
    120. }
    121. /**
    122. * @brief 写使能函数
    123. */
    124. void W25Q64_WriteEnable(void)
    125. {
    126. SPL_Start();
    127. MySPI_SwapByte(W25Q64_WRITE_ENABLE);//规定 : SPL起始的第一个字节为指令集
    128. SPL_Stop();
    129. }
    130. /**
    131. * @brief 等待忙函数--状态寄存器1 :作用看寄存器忙不忙
    132.       要想知道芯片什么时候结束忙状态了, 我们可以使用读取状态寄存器的指令, 
    133. 看一下状态寄存器的BUSY位是否为1,  BUSY位为0时,芯片就不忙了,我们再进行操作
    134. */
    135. void W25Q64_WaitBusy(void)
    136. {
    137. uint32_t Count;
    138. SPL_Start();
    139. MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//规定 : SPL起始的第一个字节为指令集
    140. Count=5000;
    141. while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) ==0x01)
    142. {
    143. Count--;
    144. if (Count==0)
    145. {
    146. break;
    147. }
    148. }
    149. SPL_Stop();
    150. }
    151. /**
    152. * @brief 写页编程-----主机给从机发送数据
    153. 步骤 : 1---先发送指令; 2--然后连发3个字节,就是24位地址 3---之后继续发送
    154. DataByte1(数据1)、DataByte2、DataByte3, 最大是DataByte256
    155. * @param Address 步骤中的1和2步---也就是发送的地址
    156. * @param *DataArray 主机给从机发送的数据, 这里面为一个数组
    157. * @param Count 数组的长度
    158. * @retval 无
    159. */
    160. void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
    161. {
    162. W25Q64_WaitBusy();//写入操作结束后,芯片进入忙状态,不响应新的读写操作
    163. W25Q64_WriteEnable();//写入操作前,必须先进行写使能
    164. SPL_Start();
    165. MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//规定 : SPL起始的第一个字节为指令集
    166. MySPI_SwapByte(Address>>16); //一共为24位,把数据后移16为,先把前8为也就是第一个直接发送给从机
    167. MySPI_SwapByte(Address>>8);//发送给从机的第二给字节
    168. MySPI_SwapByte(Address);//发送给从机的第三给字节
    169. //下面为主机给从机发送的真正的数据
    170. for (uint8_t i=0; i
    171. {
    172. MySPI_SwapByte(DataArray[i]);
    173. }
    174. SPL_Stop();
    175. }
    176. /**
    177. * @brief 按4KB的扇区擦除
    178. * @param Address 擦除的地址
    179. 步骤: 需要先发送指令0x20,再发送3个字节的地址,就行了
    180. * @retval 无
    181. */
    182. void W25Q64_SectorErase(uint32_t Address)
    183. {
    184. W25Q64_WaitBusy();//在发出擦除指令后,芯片也会进入忙状态, 我们也得等待忙状态结束后,才能进行后续操作
    185. W25Q64_WriteEnable(); //扇区擦除也是写入所以需要使能
    186. SPL_Start();
    187. MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
    188. MySPI_SwapByte(Address>>16); //一共为24位,把数据后移16为,先把前8为也就是第一个直接发送给从机
    189. MySPI_SwapByte(Address>>8);//发送给从机的第二给字节
    190. MySPI_SwapByte(Address);//发送给从机的第三给字节
    191. SPL_Stop();
    192. }
    193. /**
    194. * @brief 主机接受从机给主机发送的数据
    195. 步骤:流程是,交换发送指令03,再发送3个字节地址; 随后转入接收,就可以依次接收数据了
    196. * @param Address 起始行位置,范围:1~4
    197. * @param DataArray 起始列位置,范围:1~16
    198. * @param Count 要显示的数字,范围:0~1111 1111 1111 1111
    199. * @retval 无
    200. */
    201. void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
    202. {
    203. W25Q64_WaitBusy();//读取操作结束后不会进入忙状态,但不能在忙状态时读取
    204. SPL_Start();
    205. MySPI_SwapByte(W25Q64_READ_DATA);//规定 : SPL起始的第一个字节为指令集
    206. MySPI_SwapByte(Address>>16); //一共为24位,把数据后移16为,先把前8为也就是第一个直接发送给从机
    207. MySPI_SwapByte(Address>>8);//发送给从机的第二给字节
    208. MySPI_SwapByte(Address);//发送给从机的第三给字节
    209. //下面为主机正在接受的数据, 从机给主机发送数据
    210. for (uint8_t i=0; i
    211. {
    212. DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //1111 1111 没有实际的意义
    213. }
    214. SPL_Stop();
    215. }
    216. #define W25Q64_WRITE_ENABLE 0x06
    217. #define W25Q64_WRITE_DISABLE 0x04
    218. #define W25Q64_READ_STATUS_REGISTER_1 0x05
    219. #define W25Q64_READ_STATUS_REGISTER_2 0x35
    220. #define W25Q64_WRITE_STATUS_REGISTER 0x01
    221. #define W25Q64_PAGE_PROGRAM 0x02
    222. #define W25Q64_QUAD_PAGE_PROGRAM 0x32
    223. #define W25Q64_BLOCK_ERASE_64KB 0xD8
    224. #define W25Q64_BLOCK_ERASE_32KB 0x52
    225. #define W25Q64_SECTOR_ERASE_4KB 0x20
    226. #define W25Q64_CHIP_ERASE 0xC7
    227. #define W25Q64_ERASE_SUSPEND 0x75
    228. #define W25Q64_ERASE_RESUME 0x7A
    229. #define W25Q64_POWER_DOWN 0xB9
    230. #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
    231. #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
    232. #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
    233. #define W25Q64_MANUFACTURER_DEVICE_ID 0x90
    234. #define W25Q64_READ_UNIQUE_ID 0x4B
    235. #define W25Q64_JEDEC_ID 0x9F
    236. #define W25Q64_READ_DATA 0x03
    237. #define W25Q64_FAST_READ 0x0B
    238. #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
    239. #define W25Q64_FAST_READ_DUAL_IO 0xBB
    240. #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
    241. #define W25Q64_FAST_READ_QUAD_IO 0xEB
    242. #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
    243. #define W25Q64_DUMMY_BYTE 0xFF //这个数据实际没有意义
    244. #endif
    245. uint8_t MID;
    246. uint16_t DID;
    247. uint8_t ArrayWrite[] = {0x55, 0x66, 0x77, 0x88};
    248. uint8_t ArrayRead[4];
    249. int main(void)
    250. {
    251. OLED_Init();
    252. W25Q64_init();
    253. OLED_ShowString(1, 1, "MID: DID:");
    254. OLED_ShowString(2, 1, "W:");
    255. OLED_ShowString(3, 1, "R:");
    256. W25Q64_ReadID(&MID, &DID);
    257. OLED_ShowHexNum(1, 5, MID, 2);
    258. OLED_ShowHexNum(1, 12, DID, 4);
    259. W25Q64_SectorErase(0x000000);//擦除
    260. W25Q64_PageProgram(0x000000, ArrayWrite, 4);//写页编程
    261. W25Q64_ReadData(0x000000, ArrayRead, 4);//读取
    262. OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);
    263. OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
    264. OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
    265. OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
    266. OLED_ShowHexNum(3, 3, ArrayRead[0], 2);
    267. OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
    268. OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
    269. OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
    270. while (1)
    271. {
    272. }
    273. }

    B: 硬件SPI读写W25Q64

    1:简历

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

            可配置8位/16位数据帧、高位先行/低位先行

            时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)------PCLK在32中为72MHz表示参数的速度

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

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

            支持DMA

            兼容I2S协议

            STM32F103C8T6 硬件SPI资源:SPI1、SPI2

    2:框图

            图中表示的为低位先行,   但是可以通过调节 LSBFIRST来配置低位还是高位先行,  一般我们使用的是高位先行的方式.

            (接受和发送)缓冲区-----实际上就是数据寄存器DR;   下面发送缓冲区,就是发送数据寄存器TDR;   上面接收缓冲区,就是接收数据寄存器RDR;   和串口那里一样,TDR和RDR占用同一个地址,统.叫作DR.

            TEX:  数据奇存器和移位寄存器打配合,可以实现连续的数据流, 具体的流程如下:   第一个数据,写入到TDR,  当移位寄存器没有数据移位时,  TDR的数据会立刻转入移位奇存器,开始移位;   这个转入时刻,会置状态寄存器的TXE为1,  表示发送寄存器空      当我们检查TXE置1后紧跟着,下一个数据,就可以提前写入到TDR里候着了数据发完,下一个数据就可以立刻跟进

            RXNE :  然后移位寄存器这里,一旦有数据过来了,  它就会自动产生时钟,将数据移出去,  在移出的过程中,MISO  (主机输入,从机输出)  的数据也会移入,  一旦数据移出完成,数据移入是不是也完成了,  这时,移入的数据,就会整体地从移位寄存器转入到接收缓冲区RDR ,   这个时刻,会置状态奇存器的RXNE为1  ,   表示接收寄存器非空.   当我们检查RXNE置1后,就要尽快把数据从RDR读出来.   在下一个数据到来之前,读出RDR,就可以实现连续接收

            

    3:SPI基本结构

    4: 主模式全双工连续传输

    这个使用的是SPI时序基本单元的模式3

    5: 非连续传输

    6:连接图

            硬件SPl的连线要根据引脚定义表来连接, 软件的话不用.  和I2C硬件的连接方式相同,都是根据引脚定义表来连接的.

    7: 代码 

    1. #include "stm32f10x.h" // Device header
    2. #include "Delay.h"
    3. #include "OLED.h"
    4. #include "mySPl.h"
    5. #include "w25q64.h "
    6. //一般来说&是用来清零的;
    7. //一般来说|是用来值一的;
    8. void MySPI_W_SS(uint8_t BitValue)
    9. {
    10. //也叫做CS片选段----在低电平是有效
    11. GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)BitValue);
    12. }
    13. /**
    14. * @brief DO(MISO) SPI主机输入从机输出---连接的是PA6; 都是以主机的角度看
    15. 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
    16. PA6----浮空或上拉输入; 剩下的全部为推挽输出
    17. * @retval 无
    18. */
    19. void MYSPL_init()
    20. {
    21. RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    22. RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    23. GPIO_InitTypeDef GPIO_InitStructure;
    24. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
    25. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    26. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    27. GPIO_Init(GPIOA, &GPIO_InitStructure);
    28. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//复用推挽输出
    29. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7| GPIO_Pin_5;//复用--控制的权力交给片上外设
    30. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    31. GPIO_Init(GPIOA, &GPIO_InitStructure);
    32. GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
    33. GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    34. GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    35. GPIO_Init(GPIOA, &GPIO_InitStructure);
    36. //在开始时候默认SS(CS)为高电平,SCK为低电平
    37. SPI_InitTypeDef SPl_initstruct;
    38. SPl_initstruct.SPI_BaudRatePrescaler=SPI_BaudRatePrescaler_128;//波特率预分频器的值
    39. SPl_initstruct.SPI_CPHA=SPI_CPHA_1Edge; //配置SPl的模式
    40. SPl_initstruct.SPI_CPOL=SPI_CPOL_Low; //配置SPl的模式
    41. SPl_initstruct.SPI_CRCPolynomial=7; //填入默认的7即可
    42. SPl_initstruct.SPI_DataSize=SPI_DataSize_8b; //8个字节的大小
    43. SPl_initstruct.SPI_Direction=SPI_Direction_2Lines_FullDuplex; //双线全双工
    44. SPl_initstruct.SPI_FirstBit=SPI_FirstBit_MSB; //选择高位先行还是低位先行; --高位先行
    45. SPl_initstruct.SPI_Mode=SPI_Mode_Master; //指定当前设备为主机还是从机; ---主机
    46. SPl_initstruct.SPI_NSS=SPI_NSS_Soft; //NSS使用软件模拟--软件模拟CS
    47. SPI_Init(SPI1,&SPl_initstruct);
    48. SPI_Cmd(SPI1,ENABLE);
    49. MySPI_W_SS(1);
    50. }
    51. void SPL_Start()
    52. {
    53. MySPI_W_SS(1);
    54. MySPI_W_SS(0);
    55. }
    56. void SPL_Stop()
    57. {
    58. MySPI_W_SS(0);
    59. MySPI_W_SS(1);
    60. }
    61. /**
    62. * @brief SPL交换数据--使用的为模式0
    63. DI(MOSI)----SPI主机输出从机输入
    64. DO(MISO)-------SPI主机输入从机输出
    65. 我们只操作主机:首先主机移出最高位,放在MOSI上面,---主机操作需要我们来
    66. 齐次从机把数据放在MISO上面----从机的操作不需要我们管
    67. * @param ByteSend: 主机给从机发送的数据
    68. * @retval 主机读取的数据----即从机给主机发送的数据
    69. */
    70. uint8_t MySPI_SwapByte(uint8_t ByteSend)
    71. {
    72. //此标志为”1'时表明发送缓冲器为空,可以写下一个待发送的数据进入缓冲器中。
    73. //当写入SPI DR时,TXE标志被清除。
    74. while (SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE)==RESET);//检查标志位
    75. SPI_I2S_SendData(SPI1,ByteSend);
    76. //此标志为'1时表明在接收缓冲器中包含有效的接收数据。读SPI数据寄存器可以清除此标志。
    77. while (SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE)==RESET);//检查标志位
    78. return SPI_I2S_ReceiveData(SPI1);
    79. }
    80. //W25Q64_WaitBusy等待不忙的函数, 事后等待,只需要在写入操作之后调用---因为在写入后等待不忙读取的时候肯定不忙
    81. //而事前等待,在写入操作和读取操作之前,都得调用
    82. //我们采用事前等待
    83. void W25Q64_init()
    84. {
    85. MYSPL_init();
    86. }
    87. /**
    88. * @brief 读取设备的ID号
    89. 步骤: 起始,先交换发送指令9F,随后连续交换接收3个字节,停止;
    90. 连续接收的3个字节: 第一个字节是广商ID”表示了是哪个广家生产.
    91. 后两个学节---是设备ID; 设备ID的高8为---表示存储器类型, 低8为--表示容量
    92. * @param MID : 输出8位的厂商ID
    93. @param DID : 输出16位的设备ID
    94. 因为函数内的返回值只能返一个,而用指针,只要知道地址就可以写入;
    95. 可以直接改变外补的2个参数, 相当于返回2个参数
    96. * @retval 无
    97. */
    98. void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
    99. {
    100. 一般来说&是用来清零的;
    101. //一般来说|是用来值一的;
    102. SPL_Start();
    103. MySPI_SwapByte(0x9F);//先交换发送指令9F
    104. *MID = MySPI_SwapByte(0xFF);//返回的第一个字节是广商ID; 这时候我们给我从机发送的数据没有实际的意义
    105. *DID = MySPI_SwapByte(0xFF);//返回的第二个字节设备ID的高8为---表示存储器类型
    106. *DID <<= 8;
    107. *DID |= MySPI_SwapByte(0xFF);返回的第二个字节设备低8为--表示容量
    108. SPL_Stop();
    109. }
    110. /**
    111. * @brief 写使能函数
    112. */
    113. void W25Q64_WriteEnable(void)
    114. {
    115. SPL_Start();
    116. MySPI_SwapByte(W25Q64_WRITE_ENABLE);//规定 : SPL起始的第一个字节为指令集
    117. SPL_Stop();
    118. }
    119. /**
    120. * @brief 等待忙函数--状态寄存器1 :作用看寄存器忙不忙
    121.       要想知道芯片什么时候结束忙状态了, 我们可以使用读取状态寄存器的指令, 
    122. 看一下状态寄存器的BUSY位是否为1,  BUSY位为0时,芯片就不忙了,我们再进行操作
    123. */
    124. void W25Q64_WaitBusy(void)
    125. {
    126. uint32_t Count;
    127. SPL_Start();
    128. MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);//规定 : SPL起始的第一个字节为指令集
    129. Count=5000;
    130. while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) ==0x01)
    131. {
    132. Count--;
    133. if (Count==0)
    134. {
    135. break;
    136. }
    137. }
    138. SPL_Stop();
    139. }
    140. /**
    141. * @brief 写页编程-----主机给从机发送数据
    142. 步骤 : 1---先发送指令; 2--然后连发3个字节,就是24位地址 3---之后继续发送
    143. DataByte1(数据1)、DataByte2、DataByte3, 最大是DataByte256
    144. * @param Address 步骤中的1和2步---也就是发送的地址
    145. * @param *DataArray 主机给从机发送的数据, 这里面为一个数组
    146. * @param Count 数组的长度
    147. * @retval 无
    148. */
    149. void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
    150. {
    151. W25Q64_WaitBusy();//写入操作结束后,芯片进入忙状态,不响应新的读写操作
    152. W25Q64_WriteEnable();//写入操作前,必须先进行写使能
    153. SPL_Start();
    154. MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//规定 : SPL起始的第一个字节为指令集
    155. MySPI_SwapByte(Address>>16); //一共为24位,把数据后移16为,先把前8为也就是第一个直接发送给从机
    156. MySPI_SwapByte(Address>>8);//发送给从机的第二给字节
    157. MySPI_SwapByte(Address);//发送给从机的第三给字节
    158. //下面为主机给从机发送的真正的数据
    159. for (uint8_t i=0; i
    160. {
    161. MySPI_SwapByte(DataArray[i]);
    162. }
    163. SPL_Stop();
    164. }
    165. /**
    166. * @brief 按4KB的扇区擦除
    167. * @param Address 擦除的地址
    168. 步骤: 需要先发送指令0x20,再发送3个字节的地址,就行了
    169. * @retval 无
    170. */
    171. void W25Q64_SectorErase(uint32_t Address)
    172. {
    173. W25Q64_WaitBusy();//在发出擦除指令后,芯片也会进入忙状态, 我们也得等待忙状态结束后,才能进行后续操作
    174. W25Q64_WriteEnable(); //扇区擦除也是写入所以需要使能
    175. SPL_Start();
    176. MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
    177. MySPI_SwapByte(Address>>16); //一共为24位,把数据后移16为,先把前8为也就是第一个直接发送给从机
    178. MySPI_SwapByte(Address>>8);//发送给从机的第二给字节
    179. MySPI_SwapByte(Address);//发送给从机的第三给字节
    180. SPL_Stop();
    181. }
    182. /**
    183. * @brief 主机接受从机给主机发送的数据
    184. 步骤:流程是,交换发送指令03,再发送3个字节地址; 随后转入接收,就可以依次接收数据了
    185. * @param Address 起始行位置,范围:1~4
    186. * @param DataArray 起始列位置,范围:1~16
    187. * @param Count 要显示的数字,范围:0~1111 1111 1111 1111
    188. * @retval 无
    189. */
    190. void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
    191. {
    192. W25Q64_WaitBusy();//读取操作结束后不会进入忙状态,但不能在忙状态时读取
    193. SPL_Start();
    194. MySPI_SwapByte(W25Q64_READ_DATA);//规定 : SPL起始的第一个字节为指令集
    195. MySPI_SwapByte(Address>>16); //一共为24位,把数据后移16为,先把前8为也就是第一个直接发送给从机
    196. MySPI_SwapByte(Address>>8);//发送给从机的第二给字节
    197. MySPI_SwapByte(Address);//发送给从机的第三给字节
    198. //下面为主机正在接受的数据, 从机给主机发送数据
    199. for (uint8_t i=0; i
    200. {
    201. DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //1111 1111 没有实际的意义
    202. }
    203. SPL_Stop();
    204. }
    205. #ifndef __W25Q64_INS_H
    206. #define __W25Q64_INS_H
    207. #define W25Q64_WRITE_ENABLE 0x06
    208. #define W25Q64_WRITE_DISABLE 0x04
    209. #define W25Q64_READ_STATUS_REGISTER_1 0x05
    210. #define W25Q64_READ_STATUS_REGISTER_2 0x35
    211. #define W25Q64_WRITE_STATUS_REGISTER 0x01
    212. #define W25Q64_PAGE_PROGRAM 0x02
    213. #define W25Q64_QUAD_PAGE_PROGRAM 0x32
    214. #define W25Q64_BLOCK_ERASE_64KB 0xD8
    215. #define W25Q64_BLOCK_ERASE_32KB 0x52
    216. #define W25Q64_SECTOR_ERASE_4KB 0x20
    217. #define W25Q64_CHIP_ERASE 0xC7
    218. #define W25Q64_ERASE_SUSPEND 0x75
    219. #define W25Q64_ERASE_RESUME 0x7A
    220. #define W25Q64_POWER_DOWN 0xB9
    221. #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
    222. #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
    223. #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
    224. #define W25Q64_MANUFACTURER_DEVICE_ID 0x90
    225. #define W25Q64_READ_UNIQUE_ID 0x4B
    226. #define W25Q64_JEDEC_ID 0x9F
    227. #define W25Q64_READ_DATA 0x03
    228. #define W25Q64_FAST_READ 0x0B
    229. #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
    230. #define W25Q64_FAST_READ_DUAL_IO 0xBB
    231. #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
    232. #define W25Q64_FAST_READ_QUAD_IO 0xEB
    233. #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
    234. #define W25Q64_DUMMY_BYTE 0xFF //这个数据实际没有意义
    235. #endif
    236. uint8_t MID;
    237. uint16_t DID;
    238. uint8_t ArrayWrite[] = {0x55, 0x66, 0x77, 0x88};
    239. uint8_t ArrayRead[4];
    240. int main(void)
    241. {
    242. OLED_Init();
    243. W25Q64_init();
    244. OLED_ShowString(1, 1, "MID: DID:");
    245. OLED_ShowString(2, 1, "W:");
    246. OLED_ShowString(3, 1, "R:");
    247. W25Q64_ReadID(&MID, &DID);
    248. OLED_ShowHexNum(1, 5, MID, 2);
    249. OLED_ShowHexNum(1, 12, DID, 4);
    250. W25Q64_SectorErase(0x000000);//擦除
    251. W25Q64_PageProgram(0x000000, ArrayWrite, 4);//写页编程
    252. W25Q64_ReadData(0x000000, ArrayRead, 4);//读取
    253. OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);
    254. OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);
    255. OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);
    256. OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);
    257. OLED_ShowHexNum(3, 3, ArrayRead[0], 2);
    258. OLED_ShowHexNum(3, 6, ArrayRead[1], 2);
    259. OLED_ShowHexNum(3, 9, ArrayRead[2], 2);
    260. OLED_ShowHexNum(3, 12, ArrayRead[3], 2);
    261. while (1)
    262. {
    263. }
    264. }

  • 相关阅读:
    opencv-python图像处理:Canny边缘检测算法,模板匹配,直方图均衡化,傅里叶变换
    集成华为AGC认证服务实用教程-MacOS
    SpringBoot连接redis
    现代企业架构框架-数据架构
    教程:如何制作和分享自定义GPT
    你必须知道的6个免费图片素材网站
    ChatGPT魔法背后的原理:如何做到词语接龙式输出?
    Vite 中怎么添加全局 scss 文件
    联邦学习中的安全多方计算
    性能压测工具:wrk
  • 原文地址:https://blog.csdn.net/m0_74739916/article/details/132857928