HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

2024-06-08 1444阅读

文章目录

  • 前言
  • 一、SPI 接口和通讯协议
    • 1.1 什么是SPI
    • 1.2 SPI 的引脚信息
    • 1.3 SPI 的工作原理(了解即可)
    • 1.4 SPI 传输协议
    • 1.5 STM32F407的SPI接口
    • 二、SPI 的HAL库驱动程序
      • 2.1 SPI 寄存器操作的宏函数
      • 2.2 SPI 初始化和阻塞器数据传输
        • 2.2.1 SPI 接口初始化
        • 2.2.2 阻塞式数据发送和接收
        • 2.3 中断方式发送数据
        • 2.4 DMA方式数据传输
        • 三、Flash存储芯片 W25Q128
          • 3.1 硬件接口和连接
          • 3.2 存储空间划分
          • 3.3 数据读写的原则
          • 3.4 操作指令
            • 3.4.1 "写使能"指令
            • 3.4.2 “读数据”指令
            • 3.4.3 “写数据”指令
            • 3.5 状态寄存器
            • 四、示例:轮询方式读写W25Q128
              • 4.1 实例功能
              • 4.2 CubeMX配置
                • 4.2.1 SPI1的CubeMx设置
                • 4.2.2 其余GPIO引脚的配置
                • 4.3 程序设计
                  • 4.3.1 SPI1初始化
                  • 4.3.3 W25Q128的驱动程序
                  • 4.3 W25Q128的功能描述
                    • 4.3.1 主程序
                    • 4.3.1 W25Q128功能测试函数的实现
                    • 4.4 示例结果
                    • 五、总结

                      前言

                      一、SPI 接口和通讯协议

                      1.1 什么是SPI

                        串行外设接口(Serial Peripheral Interface,SPI)是一种传输速率比较高的串行接口,一些ADC芯片、Flash存储器芯片采用SPI接口,MCU通过SPI接口与这些外围器件通信。

                      1.2 SPI 的引脚信息

                        SPI接口的设置分为主设备(Master)和从设备(Slave),一个主设备可以连接一个或多个从设备。SPI通信的连接方式如图1-1 所示,SPI的主设备也可以称为主机,从设备也可称为从机。

                      HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                            图1-1 SPI通信的连接方式

                      SP接口有3个基本信号,功能表述如下:

                      (1)MOSI(Master Output Slave Input),主设备输出/从设备输入信号。MOSI是主设备的串行数据输出,SI是从设备的串行数据输入,主设备和从设备的这两个信号相连。

                      (2)MISO(Master Iuput Slave Output),主设备输入/从设备输出信号,MI是主设备的串行数据输入,SO是从设备的串行数据输出,主设备和从设备的这两个信号连接。

                      (3)SCK,串行时钟信号。时钟信号总是由主设备产生。

                        除了这3个必需的信号,从设备还有一个从设备选择信号CS(Chip Select;也称NSS位,Slave Select),用于选择与主设备进行通信的特定从设备。低电平表示选中从设备,高电平表示未选中。当一个SPI通信网络里有多个SPI从设备时(如图1-1),主设备就通过控制各个从设备的CS信号来保证同一时刻只能一个SPI从设备在线通信,未被选中的SPI从设备的接口引脚是高阻态。SPI主设备可以使用普通的GPIO输出引脚连接从设备的CS引脚,控制从设备的片选信号。

                      1.3 SPI 的工作原理(了解即可)

                        在主机和从机都有一个串行移位寄存器,主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。外设的写操作和读操作是同步完成的。

                      HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                              图1-2 SPI通信原理

                      HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                              图1-3 SPI结构框图

                      1.4 SPI 传输协议

                        SPI数据传输是在时钟信号SCK驱动下的串行数据传输,SPI的传输协议定义了SPI通信的起始信号、结束信号、数据有效性时钟同步等环节。SPI每次传输的数据帧长度是8位或16位,一般是最高有效位(Most Significant Bit,MSB)先行。

                        SPI通信有四种时序,由SPI控制寄存器SPI_CR1中的CPOL位和CPOA位控制。

                        CPHL(Clock Polarity)时钟极性,控制SCK引脚在空闲状态的电平。如果CPOL为0,则空闲时SCK为低电平;如果CPOL为1,则空闲时SCK为高电平。

                        CPHA(Clock Phase)时钟相位。如果CPHA为0,则在SCK的第一个边沿对数据采样;如果CPHA为1,则在CSK的第二个边沿对数据采样。

                                              表1-1 SPI的4种时序模式

                      SPI时序模式CPOL时钟极性CPHA时钟相位空闲时SCK电平采样时刻
                      模式000低电平第1个跳变沿
                      模式101低电平第2个跳变沿
                      模式210高电平第1个跳变沿
                      模式311高电平第2个跳变沿

                        图1-4所示的是CPHA为0时的数据传输时序图。NSS位从高变低是数据传输的起始信号,NSS从低变高是数据数据传输的结束信号,图中是MSB先行的方式。

                      HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                          图1-4 CPHA为0时的数据传输时序图

                        CPHA设置为0表示在SCK的第一个边沿读取数据,读取数据的时刻(捕获选通时刻)就是图1-4中虚线表示的时刻。根据CPOL的取值不同,读取数据的时刻发生在SCK的下跳沿(CPOL为1)时刻或上跳沿(CPOL为0)时刻。MISO、MOSI上的数据是在读取数据的SCK前一个跳变沿时刻发生变化的。

                        图1-5所示的是CPHA为1时的数据传输时序图。CPHA为1表示在SCK的第2个边沿读取数据。也就是图1-5中虚线表示的时刻。根据CPOL的取值不同,读取数据的时刻发生在SCK上跳沿(CPOL为1)时刻或下跳沿(CPOL为0)时刻。MISO、MOSI上的数据是在读取数据的SCK前一个跳变沿时刻发生的。

                      HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                            图1-5 CPHA为1时的数据传输时序图

                        在使用SPI接口通信时,主设备和从设备的SPI时序一定要一致,否则无法正常通信,由CPOL和CPOA的不同组合构成了4种SPI时序模式,如表1-1所示。如果使用硬件SPI接口,只需要设置正确的SPI时序模式,底层的通信时序由SPI硬件处理。有时候需要用普通GPIO引脚模拟SPI接口,这称为软件模拟SPI结接口。

                      1.5 STM32F407的SPI接口

                        STM32F407新芯片上有3个硬件SPI接口,除了支持SPI通信协议,还支持I2S音频协议。STM32F407的SPI接口有如下的特性。

                      • 数据帧长度可选择8位或16位。
                      • 可设置为主模式或从模式。
                      • SPI支持全双工通信。
                      • 可设置8种预分频值用于产生通信波频率,波特率最高位为SPI所在APB总线的频率的二分之一。STM32F407上的SPI1在APB2总线上,SPI2和SPI3在APB1总线上。
                      • 可设置时钟极性(CPOL)和时钟相位(CPHA),也就是4种SPI时序模式都支持。
                      • 可设置MSB先行(Most Significant Bit,高位先行:首先发送数据最高位,依次发送到最低位)或LSB先行(Least Significant Bit,低位先行:先发送数据的最低位,然后依次发送到最高位)。一般会采用MSR(高位先行)的模式。
                      • 可以使用硬件CRC效验。
                      • 可触发中断的主模式故障、上溢和CRC错误标志。
                      • 发送和接收具有独立的DMA请求,DMA传输具有1字节发送和接收缓冲区。

                          MCU的SPI接口实现了SPI硬件通信协议,也就是保证数据帧的正确接收和发送,如同UART接口实现底层数据帧的收发一样。SPI主设备和从设备之间具体的通信内容则需要两者之间规定通信协议,如同串口设备之间的通信协议一样。


                        补充:全双工、单工以及半双工传输方式的理解

                        (1)全双工(Full Duplex):在全双工模式下,主设备和从设备可以同时进行数据的发送和接收。主设备通过MOSI线发送数据给从设备,并通过MISO线接收从设备返回的数据。这种模式允许双向传输,在同一时钟周期内可以同时进行发送和接收操作,如PI。

                        (2)半双工(Half Duplex):在半双工模式下,主设备和从设备交替进行数据的发送和接收。通信双方不能同时发送和接收数据,而是通过切换发送和接收模式来实现双向传输。在每次通信中,首先主设备发送数据给从设备,然后切换为接收模式,从设备发送数据给主设备,如UART、I2C、CAN。

                        (3)单工(Simplex):在单工模式下,数据只能在一个方向上进行传输。通常情况下,SPI总线用于双向通信,因此单工模式在SPI中并不常见。

                        HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)


                        二、SPI 的HAL库驱动程序

                        2.1 SPI 寄存器操作的宏函数

                          SPI的驱动程序头文件是 stm32f4xx_hal_spi.h。SPI寄存器操作的宏函数如表2-1所示,宏函数中的参数_HANDLE_是具体某个SPI接口的对象指针,参数 _ INTERRUPT_ 是SPI的中断事件类型,参数_FLAG_是时间中断标志。

                                               表2-1 SPI寄存器操作的宏函数

                        函数名功能描述
                        __HAL_SPI_DISABLE( __ HANDLE __)禁用某个SPI接口
                        __ HAL_SPI_ENABLE(__ HANDLE __)启用某个SPI接口
                        __ HAL_SPI_DISABLE_IT ( __ HANDLE__, __ INTERRUPT __ ) )禁用某个中断事件源,不允许事件产生硬件中断
                        __ HAL_SPI_ENABLE_IT(__ HANDLE __, __ INTERRUPT __ )开启某个中断事件源,允许事件产生硬件中断
                        __ HAL_SPI_GET_IT_SOURCE(__ HANDLE __, __ INTERRUPT __ )检查某个中断事件源是否被允许产生硬件中断
                        __ HAL_SPI_GET_FLAG (__ HANDLE__, __ FLAG __ )获取某个事件的中断标志 ,检查事件是否发生
                        __ HAL_SPI_CLEAR_CRCERRFLAG(__ HANDLE __)清除CRC校验错误中断标志
                        __ HAL_SPI_CLEAR_FREFLAG( __ HANDLE__)清除主模式故障中断标志
                        __ HAL_SPI_CLEAR_MODFFLAG(__ HANDLE __) )清除主模式故障中断标志
                        __ HAL_SPI_CLEAR_OVRFLAG(__ HANDLE__) )清除溢出错误中断标志

                        2.2 SPI 初始化和阻塞器数据传输

                          SPI 接口初始化、状态查询和阻塞式数据传输的函数列表如表2-2所示。

                                           表2-2 SPI接口初始化和阻塞式数据传输相关函数

                        函数名功能描述
                        HAL_SPI_Init()SPI初始化,配置SPI接口函数
                        HAL_SPI_MspInit()SPI的MSP初始化函数,重新实现时一般用于SPI接口引脚GPIO初始化和中断设置
                        HAL_SPI_GetState()返回SPI接口当前状态,返回值是枚举类型SPI_HandleTypeDef
                        HAL_SPI_GetError()返回SPI接口最后的错误码,错误码有一组宏定义
                        HAL_SPI_Transmit()阻塞式发送一个缓冲区的数据
                        HAL_SPI_Receive()阻塞式接收指定长度的数据保存到缓冲区
                        HAL_SPI_TransmitReceive()阻塞式同时发送和接收一定长度的数据

                        2.2.1 SPI 接口初始化

                          函数 HAL_SPI_Init() 用于具体某个SPI接口的初始化,其原型定义如下:

                        HAL_StatusTypeDef HAL_SPI_Init(SPI_HandleTypeDef *hspi);
                        

                          其中,参数hspi是SPI外设对象指针。hspi->Init是SPI_HandleTypeDef 结构体类型,存储了SPI接口的通信参数。

                        2.2.2 阻塞式数据发送和接收

                          SPI是一种主/行通信方式,通信完全由SPI主机控制,因为SPI主机控制了时钟信号SCK。SPI主机与从机之间一般是应答式通信,主机先用函数 HAL_SPI_Transmit() 在MOSI线上发送指令或数据,忽略MISO线上传入的数据;从机接收指令或数据后会返回响应数据,主机通过函数 HAL_SPI_Receive() 在MISO线上接收响应数据,接收时不会在MOSI线上发送有效数据。

                          函数 HAL_SPI_Transmit() 用于发送数据,其原型定义如下:

                        HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
                        

                          其中,参数hspi是SPI外设对象指针,pData是输出数据缓冲区指针;Size是缓冲区数据的字节数,Timeout是超时等待时间,单位是系统滴答信号节拍数,默认情况下就是ms。函数 HAL_SPI_Transmit() 是阻塞式执行的,也就直到数据发送完成或超时时间后才返回。函数HAL_OK表示发送成功,返回HAL_TIMOUT表示发送超时。

                          函数 HAL_SPI_Receive() 用于从SPI接口接收数据,其原型定义如下:

                        HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
                        

                          其中,参数pData是接收数据缓冲区,Size是要接收的字节数,Timeout是超时等待时间。

                        2.3 中断方式发送数据

                          SPI接口能以中断的方式传输数据,是非阻塞数据传输。中断方式数据传输的相关函数、产生的中断事件类型、对应的中断回调函数等如表2-3所示。中断事件类型用中断事件使能控制位的宏定义表示。

                                             表2-3 SPI中断方式数据传输相关函数

                        函数名功能描述产生的中断事件类型对应的回调函数
                        HAL_SPI_Transmit_IT()中断方式发送一个缓冲区的数据SPI_IT_TXEHAL_SPI_TxCpltCallback()
                        HAL_SPI_Receive_IT()中断方式节后指定长度的数据保存到缓冲区SPI_IT_RXNEHAL_SPI_RxCpltCallback()
                        HAL_SPI_TransmitReceive_IT()中断方式接收指定长度的数据保存到缓冲区SPI_IT_TXE和SPI_IT_RXNHAL_SPI_TxRxCpltCallback()
                        前三个中断方式传输函数前3个中断模式传输函数都可能产生SPI_IT_ERR中断事件SPI_IT_ERRHAL_SPI_ErrorCallback()
                        HAL_SPI_IRQHandler()SPI中断ISR里调用的通道处理函数————
                        HAL_SPI_Abort()取消非阻塞式数据传输,本函数以阻塞模式运行————
                        HAL_SPI_Abort_IT()取消非阻塞式数据传输,本函数以阻塞模式运行——HAL_SPI_AbortCpltCallback()

                          HAL_SPI_Transmit_IT() 用于发送一个缓冲区的数据,发送完成后,会产生发送完成中断事件(SPI_IT_TXE),对应的回调函数是HAL_SPI_TxCpltCallback()

                          函数HAL_SPI_Receive_IT() 用于接收指定长度的数据保存到缓冲区,接收完成后,会产生接收完成中断事件(SPI_IT_RXNE),对应的回调函数是HAL_SPI_RxCpltCallback()

                          函数HAL_SPI_TransmitReceive_IT() 是发送和接收同时进行,由它启动的数据传输会产生 SPI_IT_TXE 和 SPI_IT_RXN 中断事件,但是有专门的回调函数HAL_SPI_TxRxCpltCallback()

                          上面3个函数的原型定义如下:

                        HAL_StatusTypeDef HAL_SPI_Transmit_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
                        HAL_StatusTypeDef HAL_SPI_Receive_IT(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
                        HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData,uint16_t Size);
                        

                          上面的3个函数都是非阻塞式的,函数返回HAL_OK只是表示函数操作成功,并不表示数据传输完成,只有对应的回调函数被调用才表明数据传输完成 。

                          函数HAL_SPI_AbortCpltCallback()是SPI中断ISR里调用的通用处理函数,它会根据中断事件类型调用相应的回调函数。在SPI的HAL驱动程序中。回调函数是用SPI外设对象变量的函数指针重定向的,在启动传输的函数里,为回调函数指针赋值,使用时只需要了解表2-3的对应关系即可。

                          函数HAL_SPI_Abort() 用于取消非阻塞数据传输过程,包括中断方式和DMA方式,这个函数自身以阻塞模式运行。

                          函数HAL_SPI_Abort_IT() 用于取消非阻塞式数据传输过程,包括中断方式和DMA方式,这个函数自身以中断模式运行,所以有回调函数HAL_SPI_AbortCpltCallback()

                        2.4 DMA方式数据传输

                          SPI的发送和接收有各自的DMA请求,能以DMA方式进行数据的发送发送和接收。DMA方式传输时需要触发DMA流的中断事件,主要有DMA传输完成中断事件。SPI的DMA方式数据传输的相关函数如表2-4 所示。DMA流的中断事件的宏定义可查。

                                             表2-4 SPI的DMA方式数据传输的系相关函数

                        DMA方式功能函数函数功能DMA流中断事件对应的回调函数
                        HAL_SPI_Transmit_DMA()DMA方式发送数据DMA传输完成HAL_SPI_TxCpltCallback()
                        HAL_SPI_Transmit_DMA()DMA方式发送数据DMA传输半完成HAL_SPI_TxHalfCpltCallback()
                        HAL_SPI_Receive_DMA()DMA方式接收数据DMA传输完成HAL_SPI_RxCpltCallback()
                        HAL_SPI_Receive_DMA()DMA方式接收数据DMA传输半完成HAL_SPI_RxHalfCpltCallback()
                        HAL_SPI_TransmitReceive_DMA()DMA方式发送/接收数据DMA传输完成HAL_SPI_TxRxCpltCallback()
                        HAL_SPI_TransmitReceive_DMA()DMA方式发送/接收数据DMA传输半完成HAL_SPI_TxRxHalfCpltCallback()
                        前3个DMA方式传输函数()DMA传输错误中断事件DMA传输错误

                          启动DMA方式发送和接收数据的两个函数的原型分别定义如下:

                        HAL_StatusTypeDef HAL_SPI_Transmit_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
                        HAL_StatusTypeDef HAL_SPI_Receive_DMA(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size);
                        

                          其中,hspi是SPI外设对象指针,pData是用于DMA数据发送或接收的数据缓冲区指针,Size是缓冲区的大小,因为SPI接口传输的基本数据单位是字节,所以缓冲区元素类型是uint8_t,缓冲区大小的单位是字节。

                        三、Flash存储芯片 W25Q128

                           为了进一步的了解SPI通信的原理,我们通过一款SPI通信的Flash存储芯片来进行实验。Flash 是常见的用于存储数据的半导体器件, 它具有容量大、可重复擦写、按“扇区/块” 擦除、掉电后数据可继续保存的特性。常见的 Flash 主要有 NOR Flash 和 Nand Flash 两种类型,它们的特性如表3-1所示。 NOR 和 NAND 是两种数字门电路, 可以简单地认为 Flash 内部存储单元使用哪种门作存储单元就是哪类型的 Flash。

                                          表3-1 NOR Flash和Nand Flash对比

                        特性NOR FLASHNAND FLASH
                        容量较小较大
                        同容量存储器成本较贵较便宜
                        擦除单元以“扇区/块”擦除以“扇区/块”擦除
                        读写单元可以基于字节读写必须以“块”为单位读写
                        读取速度较高较低
                        写入速度较低较高
                        集成度较低较高
                        介质类型随机存储连续存储
                        地址线和数据线独立分开共用
                        坏块较少较多
                        是否支持XIP支持不支持
                        应用举例25Qxx、程序ROMEMMC、SSD、U盘等

                        3.1 硬件接口和连接

                           W25Q128是一个Flash存储芯片,容量为128Mbit(位),也就是16MB(字节)。W25Q128支持标准SPI,还支持Dual/Qual SPI。若W25Q128工作于Dual/Qual SPI通信模式,需要连接的MCU也支持Dual/Qual SPI通信。具有QUADSPI接口的MCU才支持Dual/Qual SPI通信,如STM32F214、STM32F469等。

                           STM32F407只有标准SPI接口,不支持Dual/Qual SPI通信通信。开发板上有一个W25Q128芯片,通过标准SPI接口与STM32F407的SPI1接口连接,电路如图3-1。

                        HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                              图3-1 W25Q128电路

                          W25Q128的各个引脚的功能描述如下

                        • SO、SI、CLK这3个SPI引脚与MCU的SPI1接口的相应引脚连接,占用PB4、PB5、PB3 引脚。
                        • 片选信号CS与MCU的PB14连接,由MCU通过GPIO引脚PB14的输出控制W25Q128的片选状态。
                        • WP是写保护设置引脚,WP为低电平时,禁止修改内部的状态寄存器,与状态寄存器的一些位配合使用,可以对内部的一些存储区域进行写保护。电路中将WP接高电平,也就是不使用此写保护信号。
                        • HOLD是硬件保持信号引脚,当器件被选中时,如果HOLD输入为低电平,那么DO引脚变为高组态,DI和CLK的输入被忽略。当HOLE引脚输入高电平时,SPI的操作又继续。这里将HOLD引脚接电源,就是不使用保持功能。

                            W25Q128支持SPI模式0和模式3。在MCU与W25Q128通信时,设置使用SPI1模式3,即设置CPOL = 1,CPOA = 1。

                            开发板上的W25Q128与STM32F407的SPI1连接,因为SPI1接口要用到PB4、PB5、PB3引脚,而5线的JTAG接口要用到PB3、PB4,所以在使用SPI1接口时,系统的Dubug接口不能设置为JTAG接口,只能设置为SW接口,所以,为避免出现错误,在程序中所有例程都用的SW调试接口。

                          注:

                            Flash 也有对应的缺点,我们在使用过程中需要尽量去规避这些问题: 一是 Flash 的使用寿命, 另一个是可能的位反转。使用寿命体现在: 读写上是 FLASH 的擦除次数都是有限的(NOR Flash 普遍是 10 万次左右),当它的使用接近寿命的时候,可能会出现写操作失败。 由于 NAND 通常是整块擦写,块内有一位失效整个块就会失效,这被称为坏块。使用 NAND Flash 最好通过算法扫描介质找出坏块并标记为不可用, 因为坏块上的数据是不准确的。位反转是数据位写入时为 1, 但经过一定时间的环境变化后可能实际变为 0 的情况, 反之亦然。 位反转的原因很多,可能是器件特性也可能与环境、 干扰有关。

                          3.2 存储空间划分

                            W25Q128总容量为16MB,使用24位地址线,地址范围为0x000 000 ~0xFFF FFF。

                            16MB分为256个块(Block),每个块的大小为64KB,16位偏移地址,快内偏移地址范围是0x0000~0XFFFF。

                            每个块又分为16个扇区(Sector),共4096个扇区,每个扇区的大小为4KB,12位偏移地址,扇区内偏移地址的范围是0x000~0xFFF。

                            每个扇区又分为16个页(Page),共65536个页,每个页的大小为256个字节,8位偏移地址,页内偏移地址范围是0x00~0xFF。

                          HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                                图3-2 存储空间划分

                          3.3 数据读写的原则

                            从W25Q128读取数据时,用户可以从任意地址开始读取任意长度的数据。从W25Q128写数据时,用户可以从任意地址开始写数据,但是一次SPI通信写入的数据范围不能超过一个页的边界。所以,如果从页的起始地址开始写数据,一次最多可写入一个页的数据,即256个字节。如果一次写入的数据超过页的边界,会再从页的起始位置开始写。

                            向存储区域写入数据时,存储区域必须是被擦除过的,也就是存储内容是0xFF,否则写入的数据操作无效。用户可以对整个器件、某个块、某个扇区进行擦除操作,但是不能对单个页进行擦除。

                          3.4 操作指令

                            SPI的硬件层和传输协议只是规定了传输一个数据帧的方法,对于具体的SPI期间的操作由器件规定的操作指令实现。W25Q128制定了很多的操作指令,用以实现各种功能。

                            W25Q128的操作指令由一字节或多字节组成,指令的第一个字节是指令码,其后跟随的是指令的参数或返回的数据。W25Q128常用的几个指令如表3-1所示,其全部指令和详细解释见W25Q128的数据手册。表3-1 中用括号表示的部分表示返回的数据,A23~A0是24位的全局地址,dummy表示必须发送的无效字节数据,一般发送0x00。

                                           表3-1 W25Q128常用指令

                          指令名称BYTE1指令码BYTE2BYTE3BYTE4BYTE5BYTE6
                          写使能0x06——————————
                          读状态寄存器10x05(S7~S0)————————
                          读状态寄存器20x35(S15~S8)————————
                          读厂家和设备ID0x90dummydummy0x00(MF7~MF0)(ID7~ID0)
                          读64位序列号0x4Bdummydummydummydummy(ID63 ~ ID0)
                          器件擦除0xC7/0x60——————————
                          块擦除0xD8A23~A16A15~A8A7~A0————
                          扇区擦除0x20A23~A16A15~A8A7~A0————
                          写数据(页编程)0x02A23~A16A15~A8A7~A0D7~D0——
                          读数据0x03A23~A16A15~A8A7~A0(D7~D0)——
                          快速读数据0x0BA23~A16A15~A8A7~A0dummy(D7~D0)

                          下面以几个指令为例,说明指令传输的过程,以及返回数据的读取等原理。

                          3.4.1 "写使能"指令

                            “写使能”指令(指令码0x06)只有一个指令码,其传输过程如图3-1所示,一个指令总是从片选信号CS由高变低的跳变开始,片选信号CS由低变高的跳变中结束。

                          HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                            图3-1 单字节“写使能”指令的时序

                          HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                            CS变为低电平后,MCU向W25Q128传输1字节数据0x06,然后结束SPI传输即可。W25Q128接收数据后,根据指令码判断指令类型,并进行相应的处理。“写使能”指令是将状态寄存器1的WEL位设置为1,在擦除芯片、擦除扇区等操作前必须执行“写使能”指令。

                            无返回数据的指令的操作都有此类似,就是连续将指令码、指令参数发送给W25Q128即可。

                          3.4.2 “读数据”指令

                            “读数据”指令(指令码0x03)运用从某个地址开始读取一定个数的字节数据,其中时序如图3-2所示。地址A23~A0是24位全局地址,分为3个字节,在发送指令码0x03后,再发送3个字节的地址数据。然后MCU开始从DO线上读取数据,一次读取一个字节,可以连续读取,W25Q128会自动返回下一个地址的数据。

                          HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                              图3-2 “读数据”指令的时序

                          HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                          3.4.3 “写数据”指令

                            “ 写数据 ” 指令(指令码0x02)就是数据手册上的“页编程”指令,用于向任意地址写入一定长度的数据。“写数据”指令的时序如图3-3所示,图中是向一个页一次写入256个字节的数据。一个页的容量是256字节,写数据操作操作一次最多写256字节。如果数据长度超过256个字节,会从页的起始位置开始继续写。

                          HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                              图3-3 “写数据”指令的时序

                          HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                            “写数据”指令的起始地址可以是任意地址,数据长度也可以小于256,但如果写的过程中地址超过页的边界,就会从页的起始地址开始继续写。写数据操作的存储单元必须是被擦除过的,也就是内容是0xFF。如果存储单元的内容不是0xFF,那么重新写入数据无效。所以,已经写过的存储区域是不能重复写入的,需要擦除后才能再次写入。

                          3.5 状态寄存器

                            W125Q128有3个状态寄存器(status register),用于对器件的一些参数进行配置,或返回器件的当前状态信息。下面是W25Q128的状态寄存器SR1,其各个位的定义见表

                                           表3-2 状态寄存器SR1各个位的定义

                          位编号位名称功能说明存储特性读/写特性
                          S7SRP0状态寄存器保护位0非易失可写
                          S6SEC扇区保护非易失可写
                          S5TB顶/底保护非易失可写
                          S4BP2块保护2非易失可写
                          S3BP1快保护1非易失可写
                          S2BP0快保护0非易失可写
                          S1WEL写使能锁存易失可写
                          S0BUSY有正在进行的擦除或写操作易失只读

                            通过读状态寄存器SR1的指令(指令码0x05),我们可以读取SR1的内容。状态寄存器中的某些位是可写的,是指可以通过写状态寄存器的指令修改这些位的内容;这些位是非易失的,是指修改的内容可永久保存,掉电也不会丢失。

                          SR1中有2个位在编程中经常用到:WEL位与BUSY位。

                            写使能锁存(Write Enable Latch,WEL)位是只读的。器件上电后,WEL位是0。只有当WEL位是1时,才能进行擦除芯片、擦除扇区、页编程操作。这些操作执行完后,WEL位自动变为0。只有执行“写操作”指令(指令码0x06)后,WEL位才变1。所以,在进行擦除芯片、擦除扇区、页编程等操作之前,“写使能”指令是必须先执行的。

                            BUSY位是只读的,表示器件是否处于忙的状态。如果BUSY位是1,表示器件正在执行页编程、扇区擦除、器件擦除等操作。此时,除了“读状态寄存器”指令和“擦除/编程挂起”指令,器件会忽略其他任何指令。当正在执行的页编程、擦除等指令执行完成以后,BUSY位自动变为0,这意味着可以继续执行其他指令了。

                          四、示例:轮询方式读写W25Q128

                          4.1 实例功能

                            开发板上的W25Q128芯片的电路如图1-1所示,与STM32F407的SPI1接口连接,占用PB3、PB4、PB5引脚,W25Q128的片选信号CS与MCU的PB14连接。在本示例中,会根据这个接口电路,为W25Q128编写常用操作的驱动程序,并且测试轮询方式读写W25Q128。示例功能与操作流程如下。

                          • 使用SPI1接口读写SPI1接口读写Flash存储器W25Q128。
                          • 使用阻塞模式SPI传输函数编写W25Q128常用功能的驱动程序。
                          • 通过模拟菜单测试擦除整个芯片、擦除块、写入数据和读取数据等操作。

                            4.2 CubeMX配置

                            4.2.1 SPI1的CubeMx设置

                              SPI的模式和参数设置界面如图4-1所示。SPI的模式设置只有两个参数,用于设置SPI1的工作模式和硬件NSS信号。

                            HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                                图4-1 SPI1的模式和参数设置

                            (1)Mode,工作模式。有多种工作模式可选:作为主机时,一般选择为Full-Duplex Master(全双工主机);作为从机时,一般选择为Full-Duplex Slave(全双工从机)。所谓全双工(Full-Duplex),是指使用MISO线和MOSI线可以同时接收和发送,相应的还有Half-Duplex(半双工),就是只使用一根数据线,这根数据线既可以发送有可接收,但是需要分时使用发送和接收功能。在本例中,MCU作为主机,并且有MISO和MOSI两根串行信号线,所以选择Full-Duplex。

                            (2)Hardware NSS Output Singal ,硬件NSS信号。有3种选项,"Disable"选项表示不使用硬件NSS信号,而是使用软件方式控制NSS信号 ;Hardware NSS Intput Signal表示硬件NSS输入信号,Hardware Output Single表示硬件NSS输出信号,SPI主机输出片选信号时选择此选项。本示例用一个单独的GPIO引脚PB14作为主机的片选信号,所以设置为Disable。

                              SPI1的参数设置为3组,这些参数的设置应该与W25Q128的SPI通信参数对应。W25Q128的SPI通信使用8位数据,MSB先行,支持SPI0和SPI3。

                            (1)Basic Parameters组,基本参数。

                              ① Frame Format,帧格式。有Motorola和TI两个选项。但只能选Motorla。这个参数对应控制控制寄存器SPI_CR2的FRF位。

                               "Motorola"选项表示使用Motorola SPI帧格式。在Motorola SPI帧格式中,数据传输是以两个时钟边沿进行的,其中一个时钟边沿用于数据采样,另一个时钟边沿用于数据传输。数据的有效位数可以在配置中指定。

                               "TI"选项表示使用TI SPI帧格式。在TI SPI帧格式中,数据传输是以一个时钟边沿进行的,数据在该时钟边沿上同时进行采样和传输。数据的有效位数可以在配置中指定。

                              ②Data Size,数据大小。数据帧的位数,可选8位或16位。本示例选择8位。

                              ③First Bit,首先要传的位。可选MSB First(高位先行)或LSB Frist(低位先行)。本示例选择MSB First。

                            (2)Clock Parameters组,时钟参数。

                              ①Prescaler(for Buaud Rate),用于产生波特率的预分频系数。有8个可选预分频系数,从2到256。SPI的时钟频率就是所在APB总线的时钟频率,SPI1在APB2总线上。最高频率是84MHz。

                              ②Baud Rate,波特率。设置预分频系数后。CubeMx会自动根据APB总线频率和分频系数计算波特率。本示例中的APB2总线频率为84MHz,分频系数为8,所以波特率为10.5 Mbit/s。另外,根据W25Q128的数据手册,读数据指令(0x03)支持的最高频率是33MHz,但是经过测试,如果波特率超过12.5 Mbit/s时,读取数据就会偶尔发生错误,而波特率为5.25Mbit/s(分频系数为16)时传输很稳定。

                              ③Clock Polarity,时钟极性。可选项为High和Low。本示例使用SPI模式3,所以选择High,

                              ④Clock Phase,时钟相位。可选项为1 Edge和 2Edge。本示例使用SPI模式3,即在第2跳变沿采样数据,所以选择2 Edge。

                              图1-1中的CPOL和CPHA的设置对应于SPI模式3,因为W25Q128同时也支持SPI模式0,所以设置CPOL为Low,CPHA为1Edge也是可以的。

                            (3) Advance Parameters组,高级参数。

                              ①CRC Calcution,CRC(循环冗余校验)计算。 STM32F407的SPI通信可以在传输数据的最后加上1个字节的CRC计算结果,在发生CRC错误时可以产生中断。若不使用,就选择Disabled.

                              ②NSS Signal Type,NSS信号类型。这个参数的选项是由模式设置里面的Hardware Nss Signal的选择结果决定的。当模式设置里选择Hardware NSS Signal 为Disabe时,这个参数的选项就只能是Sofware,表示用软件产生NSS输出信号,即本例用PB14输出信号作为从机的片选信号。

                              启用SPI1后,CubeMx将自动将自动分配PA5、PA6、PA7作为SPI1的3个信号引脚,但是从图3-1中可以看出,实际用到的引脚是PB3、PB4、PB5引脚(如图4-2),所以在配置引脚时一定要注意,要按照原理图上的引脚进行配置,或者在Categories配置后一定要检查一遍引脚是都正确。

                            HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                                图4-2 SPI1的GPIO引脚配置

                              本示例使用SPI的阻塞式数据传输方式,不使用SPI的中断,多以无需开启全局中断。

                            4.2.2 其余GPIO引脚的配置

                              该工程通过串口1将W25Q128的操作结果打印出来,串口1具体配置如图4-3所示,需要注意的是,串口工具的配置需要和CubeMx里面的USART配置保持一致。

                            HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                                图4-3 串口配置

                              GPIO引脚包括LED灯引脚配置(PF9、PF10),其余引脚包括按键、蜂鸣器配置和项目《HAL库STM32常用外设教程(二)—— GPIO输入\输出》里面的引脚配置相同(有源码提供),如图4-4所示,此处不再赘述。

                            HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                                                图4-4 GPIO配置

                            4.3 程序设计

                              在CubeMx里面生成keil的代码以后,在keil里面打开项目,代码如下。

                            4.3.1 SPI1初始化

                              配置好CubeMx后,生成工程并打开,在spi.c的文件里是关于SPI外设的一些基础配置,其中定义了SPI1的初始化函数MX_SPI1_Init(),相关代码及其对应的STM32CubeMx里面的配置如下:

                            HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              上述程序定义了一个SPI_HandleTypeDef 结构体类型变量hspi1,这是表示SPI1的外设对象变量。函数MX_SPI1_Init()设置了hspi1各成员变量的值,其代码与CubeMx的配置是对应的。程序中的注释说明了每个成员变量的意义。

                            HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              HAL_SPI_MspInit()是SPI的 MSP函数(“MCU Support Package”,微控制器支持包),在函数MX_SPI1_Init()里被调用,其主要作用是开启SPI1的时钟,并对其3个复用引脚进行GPIO设置。程序中的注释说明了每个成员变量的意义。

                            4.3.3 W25Q128的驱动程序

                              为方便对W25Q128进行操作,我们将W25Q128常用的一些功能编写为函数,也就是实现3.4节介绍的W25Q128常用操作指令。例如擦除芯片、擦除扇区、读取数据、写入数据等,这就是W25Q128的驱动程序。

                              注意W25Q128驱动程序与SPI接口的HAL库驱动程序有区别。SPI的HAL驱动程序实现了SPI接口数据传输的基本功能,是SPI硬件层的驱动;而W25Q128驱动程序是根据W25Q128的指令定义,实现器件具体功能操作的一系列函数。W25Q128驱动程序要用到SPI硬件层的HAL驱动程序,要通过SPI的HAL驱动程序实现数据帧的收发。

                              我们在项目里创建一个名为User的子目录,创建文件w25flash.c和w25flash.h,驱动程序文件w25flash.c和w25flash.h是根据W25Q128的数据手册编写的,并将其存放在这个子目录里。将子目录User添加到项目的头文件和源文件搜索路径。具体的w25flash.c和w25flash.h在文章末尾的网盘文件里有提供。

                            4.3 W25Q128的功能描述

                            4.3.1 主程序

                              下面我们使用W25Q128的驱动程序。

                            (1)在项目中添加代码,对W25Q128进行功能测试。添加完w25flash文件,如图4-5所示。

                            HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                            图4-5 GPIO配置

                            (2)在主程序中添加用户代码,完成后文件main.c的代码如下:

                              程序在完成外设初始化之后,调用Flash_TestReadStatus()读取Flash芯片的器件ID、状态寄存器SR1和SR2的值,并进行打印,在进入while循环之前,打印出了如下的一个菜单,菜单内如如下:

                            在while循环里面,程序检测按键输入,对于4个按键分别进行响应,实现下面的功能:

                            • KEY0键按下时,调用函数Flash_EraseChip()擦除整个器件,擦除操作大约需要30s,不要经常才出整个扇区。
                            • KEY2键按下时,调用Flash_EraseBlock64K()擦除Block0 ,测试写入数据之前应该先擦除。
                            • KEY_UP键按下时,调用函数Flash_TestWrite() 从Page0和Page1写入数据。
                            • KEY1键按下时,调用函数Flash_TestRead()从Page0和Page1读取数据。

                              函数Flash_EraseChip()Flash_EraseBlock64K()是驱动文件w25flash.h中定义的函数。

                              函数Flash_TestReadStatus()Flash_TestWrite()Flash_TestRead()是在文件spi.h中定义的测试函数,在后续的步骤中就。

                              	Flash_TestReadStatus();		/* 读取器件ID并分析芯片类型 */
                              
                              	KEYS curKey = ScanPressedKey(KEY_WAIT_ALWAYS);		/* 获取按下的是哪一个按键 */
                              	  switch(curKey)
                              	  {
                              	  case KEY0:
                              		  LED0_Toggle();									/* 翻转LED0 */
                              			Flash_EraseChip();							/* 擦除整个器件,大约需要25时间 */
                              			printf("Chip is erased\n");
                              		  break;
                              	  case KEY2:
                              		  LED1_Toggle();									/* 翻转LED1 */
                              			uint32_t globalAddr	=	0;
                              			Flash_EraseBlock64K(globalAddr);/* 擦除块 */
                              			printf("Block0 is erased\n");
                              		  break;
                              	  case KEY_UP:
                              		  LED0_Toggle();									/* 翻转LED0 */
                              		  LED1_Toggle();									/* 翻转LED1 */
                              			Flash_TestWrite();							/* Flash写操作 */
                              		  break;
                              	  case KEY1:
                              //		  Buzzer_Toggle();							/* 蜂鸣器 */
                              			Flash_TestRead();								/* Flash读操作 */
                              		  break;
                              		 default:
                              			 break;
                              	  }
                              

                              HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              4.3.1 W25Q128功能测试函数的实现

                                接下来我们利用w25flash.c里面的的W25Q128驱动函数 在spi.c里面实现上一小节中提到的三个函数,即在W25Q128驱动函数上再封装一层来实现我们想要的功能:

                              (1)函数Flash_TestReadStatus()就是调用W25Q128驱动程序中的3个函数,分别读取器件ID、状态寄存器SR1和SR2。

                                函数Flash_ReadID()返回厂家和产品ID,这在器件的手册上可以查到,如W25Q128的ID是0xEF17,如果其他芯片可继续在case里面进行添加,

                              HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              代码如下(示例):

                              /*
                               * @brief 读取器件ID、状态寄存器SR1和SR2
                               * @param
                               */
                              void Flash_TestReadStatus(void)
                              {
                              	uint16_t devID = Flash_ReadID();																/* 读取器件ID */
                              	uint8_t tempStrDevID[50];																				/* 用来接字符串 */
                              	sprintf((char*)tempStrDevID,"Device ID = 0x%04X",devID);       	/* 将一个设备ID(devID)格式化为字符串,并存储在 tempStrDevID 变量中 */
                              	printf("%s\n",tempStrDevID);																		/* 打印tempStrDevID */
                              	
                              	switch(devID){																									/* 判读flash的型号 */
                              		case 0xEF17:
                              			printf("The chip is W25q128\n");
                              		break;
                              		default:
                              			printf("The chip is Unknow\n");
                              		break;
                              	}
                              	/* 读寄存器SR1的内容 */
                              	uint8_t tempStrSR1[50];
                              	sprintf((char*)tempStrSR1,"Status Reg1 = 0x%x",Flash_ReadSR1());  /* 将Flash_ReadSR1函数的返回值直接进行拼接 */
                              	printf("%s\n",tempStrSR1);
                              	
                              		/* 读寄存器SR2的内容 */
                              	uint8_t tempStrSR2[50];
                              	sprintf((char*)tempStrSR2,"Status Reg1 = 0x%x",Flash_ReadSR2());  /* 将Flash_ReadSR1函数的返回值直接进行拼接 */
                              	printf("%s\n",tempStrSR2);
                              }
                              

                              (2)函数Flash_TestWrite()的功能是在Page0和Page1里写入数据,写入数据的存储空间必须是被擦除过的,在Page0里面写入的是两个字符串,分别在Page0的起始位置以及偏移100的位置,对一个页是可以分多次写入的,只要写入的存储的存储单元是被擦除过的。对于Page1则从页的起始地址写入了256字节的数据,写入的内容等于偏移地址的大小,即0~255。

                              HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              /*
                               * @brief  写flash测试
                               * @param
                               */
                              void Flash_TestWrite(void)
                              {
                              	uint8_t BlockNo 		= 0;																									/* 表示块号 */
                              	uint8_t SubSectorNo = 0;																									/* 表示扇区 */
                              	uint8_t  SubPageNo 	= 0;																									/* 表示页号 */
                              	uint32_t memAddress = 0;  																								/* 存储地址 */
                              	
                              	memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算内存地址,并将结果存储在memAddress变量中 */
                              	
                              	uint8_t bufStr1[30] = "Hello from brginning";
                              	Flash_WriteInPage(memAddress,bufStr1,strlen("Hello from brginning")+1);		/* 在指定的内存地址写入bufStr1(第0页的起始地址) */
                              	printf("Write in page 0 = %s\n",bufStr1);
                              	
                              	uint8_t bufStr2[30] = "Hello in page";
                              	Flash_WriteInPage(memAddress + 100,bufStr2,strlen("Hello from brginning")+1); /* 上一个位置偏移100个位置写入字符bufStr2 */
                              	printf("Write in page 100 = %s\n",bufStr2);
                              	
                              	uint8_t bufPage[FLASH_PAGE_SIZE];    					  
                              	for(uint16_t i = 0;i  
                              

                              (3)函数Flash_TestRead()的功能是从Page0和Page1里读取数据 ,即从页的起始地址、偏移12的地址、偏移136的地址、偏移210的地址读出Flash_TestWrite()函数里写入的数值,可以测试读出的内容和写入的是否一致。

                              HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              /*
                               * @brief   读flash测试
                               * @param
                               */
                              void Flash_TestRead(void)
                              {
                              	uint8_t BlockNo 		= 0;																									/* 表示块号 */
                              	uint8_t SubSectorNo = 0;																									/* 表示扇区 */
                              	uint8_t  SubPageNo 	= 0;																									/* 表示页号 */
                              	uint32_t memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);
                              	uint8_t bufStr[50];  
                              	Flash_ReadBytes(memAddress,bufStr,50);  																	/* 读50字节 */
                              	printf("Read in page 0 = %s\n",bufStr);																		/* 将50个字符串打印出来 */
                              	Flash_ReadBytes(memAddress + 100,bufStr,50);  														/* 读50字节 */
                              	printf("Read in page 100 = %s\n",bufStr);																	/* 将50个字符串打印出来 */
                              	
                                SubPageNo 	= 1;
                              	memAddress = Flash_Addr_byBlockSectorPage( BlockNo,SubSectorNo,SubPageNo);/* 计算第一页的地址 */
                              	uint8_t randData12 = Flash_ReadOneByte(memAddress + 12);									/* 读取地址中偏移量为12的字节数据,并将其存储在randData12变量中 */
                              	uint8_t randData136 = Flash_ReadOneByte(memAddress + 136);								/* 读取地址中偏移量为136的字节数据,并将其存储在randData136变量中 */
                              	uint8_t randData210= Flash_ReadOneByte(memAddress + 210);									/* 读取地址中偏移量为210的字节数据,并将其存储在randData210变量中 */
                              	
                              	char tempStrRandData[100]="";      																				/* 使用前先清空 */
                              	sprintf((char*)tempStrRandData,"Page1[12] = %d,Page1[136] = %d,Page1[210] = %d",randData12,randData136,randData210); 
                              	printf("%s\n",tempStrRandData);
                              }
                              

                              4.4 示例结果

                                将完成的程序下载到开发板上,如图5-1所示按下相应的按键,就会得到图5-2串口得到的结果,其中打印多次是因为按键没有进行消抖操作。

                              HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              图5-1 开发板按键

                              HAL库STM32常用外设教程(八)—— SPI (读写W25Q128)

                              图5-2 按键按下后串口收到的指令

                              五、总结

                                通过本文讲解了SPI通信,其中涉及了SPI的原理、HAL库的相关驱动函数,其中涉及了SPI轮询、中断、DMA三种方式。然后又通过Flash芯片W25Q128作为示例来讲解SPI通信,讲解了W25Q128的部分指令,轮询方式读写W25Q128,其中涉及的SPI的CubeMx配置应当熟悉掌握。


                              项目源码:HAL库STM32常用外设教程(九)—— SPI (读写W25Q128)

                              参考书籍:

                              《STM32Cube高效开发教程(基础篇)》王维波

                              《STM32F4xx中文参考手册》

                              《STM32F407 探索者开发指南》


                                人最宝贵的东西是生命,生命对人来说只有一次.因此,人的一生应当这样度过:当一个人回首往事时,不因虚度年华而悔恨,也不因碌碌无为而羞愧;这样,在他临死的时候,能够说,我把整个生命和全部精力都献给了人生最宝贵的事业——为人类的解放而奋斗。

                                                              ——《钢铁是怎样练成的》

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]